Skip to content

Commit 778e531

Browse files
committed
feat: add configurable masterclass panel with event bus for external integration
Signed-off-by: rx18-eng <remopanda78@gmail.com>
1 parent 66b4a7f commit 778e531

17 files changed

Lines changed: 1126 additions & 3 deletions

File tree

packages/phoenix-event-display/src/event-display.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ export class EventDisplay {
3939
private onEventsChange: ((events: any) => void)[] = [];
4040
/** Array containing callbacks to be called when the displayed event changes. */
4141
private onDisplayedEventChange: ((nowDisplayingEvent: any) => void)[] = [];
42+
/** Generic event bus for integration with external frameworks. */
43+
private eventBus: Map<string, Set<(data: any) => void>> = new Map();
4244
/** Three manager for three.js operations. */
4345
private graphicsLibrary: ThreeManager;
4446
/** Info logger for storing event display logs. */
@@ -127,6 +129,7 @@ export class EventDisplay {
127129
// Clear accumulated callbacks
128130
this.onEventsChange = [];
129131
this.onDisplayedEventChange = [];
132+
this.eventBus.clear();
130133
// Reset singletons for clean view transition
131134
this.loadingManager?.reset();
132135
this.stateManager?.resetForViewTransition();
@@ -620,6 +623,48 @@ export class EventDisplay {
620623
};
621624
}
622625

626+
/**
627+
* Subscribe to a named event on the integration event bus.
628+
* Allows external frameworks to react to actions like particle tagging
629+
* or result recording.
630+
*
631+
* Standard event names:
632+
* - `'particle-tagged'`: Fired when a particle is tagged in the masterclass panel.
633+
* - `'particle-untagged'`: Fired when a tagged particle is removed.
634+
* - `'result-recorded'`: Fired when an invariant mass result is recorded.
635+
*
636+
* @param eventName The event name to listen for.
637+
* @param callback Callback invoked with event-specific data.
638+
* @returns Unsubscribe function to remove the listener.
639+
*/
640+
public on(eventName: string, callback: (data: any) => void): () => void {
641+
if (!this.eventBus.has(eventName)) {
642+
this.eventBus.set(eventName, new Set());
643+
}
644+
this.eventBus.get(eventName).add(callback);
645+
return () => {
646+
const listeners = this.eventBus.get(eventName);
647+
if (listeners) {
648+
listeners.delete(callback);
649+
if (listeners.size === 0) {
650+
this.eventBus.delete(eventName);
651+
}
652+
}
653+
};
654+
}
655+
656+
/**
657+
* Emit a named event on the integration event bus.
658+
* @param eventName The event name to emit.
659+
* @param data Data to pass to listeners.
660+
*/
661+
public emit(eventName: string, data?: any): void {
662+
const listeners = this.eventBus.get(eventName);
663+
if (listeners) {
664+
listeners.forEach((cb) => cb(data));
665+
}
666+
}
667+
623668
/**
624669
* Get metadata associated to the displayed event (experiment info, time, run, event...).
625670
* @returns Metadata of the displayed event.
Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
/** A Lorentz 4-momentum vector (E, px, py, pz) in MeV. */
2+
export interface FourMomentum {
3+
E: number;
4+
px: number;
5+
py: number;
6+
pz: number;
7+
}
8+
9+
/** A tagged particle with its physics properties. */
10+
export interface TaggedParticle {
11+
uuid: string;
12+
tag: string;
13+
fourMomentum: FourMomentum;
14+
/** Display-friendly properties. */
15+
pT: number;
16+
eta: number;
17+
phi: number;
18+
}
19+
20+
/** Definition of a particle tag for use in masterclass exercises. */
21+
export interface ParticleTagDef {
22+
/** Unique identifier, e.g. 'electron', 'kaon'. */
23+
id: string;
24+
/** Human-readable label, e.g. 'Electron'. */
25+
label: string;
26+
/** Symbol for display, e.g. 'e\u00B1', 'K\u00B1'. */
27+
symbol: string;
28+
/** CSS color for the tag button and badge. */
29+
color: string;
30+
/** Rest mass in MeV/c\u00B2. */
31+
mass: number;
32+
}
33+
34+
/**
35+
* Configuration for experiment-specific masterclass exercises.
36+
* Each experiment (ATLAS, LHCb, CMS, ...) provides its own config.
37+
*/
38+
export interface MasterclassConfig {
39+
/** Panel title, e.g. 'ATLAS Z-Path Masterclass'. */
40+
title: string;
41+
/** Available particle tags for this exercise. */
42+
particleTags: ParticleTagDef[];
43+
/** Educational hints shown when invariant mass is computed. */
44+
hints: string[];
45+
/**
46+
* Classify an event from the tag counts.
47+
* Receives a map of tag id to count, e.g. { electron: 2, muon: 0 }.
48+
* Returns a short label like "e", "4e", "2e2m".
49+
*/
50+
classifyEvent: (tagCounts: Record<string, number>) => string;
51+
}
52+
53+
/**
54+
* Extract a 4-momentum vector from track userData.
55+
* Tracks have pT, eta/phi (or dparams), and we assign mass from the tag definition.
56+
* @param userData Track user data containing kinematic properties.
57+
* @param mass Particle rest mass in MeV/c².
58+
*/
59+
export function fourMomentumFromTrack(
60+
userData: any,
61+
mass: number,
62+
): FourMomentum | null {
63+
const pT = userData.pT;
64+
if (pT == null) return null;
65+
66+
const phi = userData.phi ?? userData.dparams?.[2];
67+
// theta from dparams, or compute from eta
68+
let theta = userData.dparams?.[3];
69+
if (theta == null && userData.eta != null) {
70+
theta = 2 * Math.atan(Math.exp(-userData.eta));
71+
}
72+
if (phi == null || theta == null) return null;
73+
74+
const px = pT * Math.cos(phi);
75+
const py = pT * Math.sin(phi);
76+
const pz = pT / Math.tan(theta);
77+
const p2 = px * px + py * py + pz * pz;
78+
const E = Math.sqrt(p2 + mass * mass);
79+
80+
return { E, px, py, pz };
81+
}
82+
83+
/**
84+
* Extract a 4-momentum vector from a calorimeter cluster.
85+
* Clusters have energy, eta, phi — treated as massless.
86+
*/
87+
export function fourMomentumFromCluster(userData: any): FourMomentum | null {
88+
const energy = userData.energy;
89+
const eta = userData.eta;
90+
const phi = userData.phi;
91+
if (energy == null || eta == null || phi == null) return null;
92+
93+
const theta = 2 * Math.atan(Math.exp(-eta));
94+
const px = energy * Math.sin(theta) * Math.cos(phi);
95+
const py = energy * Math.sin(theta) * Math.sin(phi);
96+
const pz = energy * Math.cos(theta);
97+
98+
return { E: energy, px, py, pz };
99+
}
100+
101+
/**
102+
* Compute the invariant mass of a set of particles in MeV.
103+
* M² = (ΣE)² - (Σpx)² - (Σpy)² - (Σpz)²
104+
*/
105+
export function invariantMass(momenta: FourMomentum[]): number {
106+
if (momenta.length < 2) return 0;
107+
108+
let sumE = 0,
109+
sumPx = 0,
110+
sumPy = 0,
111+
sumPz = 0;
112+
for (const p of momenta) {
113+
sumE += p.E;
114+
sumPx += p.px;
115+
sumPy += p.py;
116+
sumPz += p.pz;
117+
}
118+
119+
const m2 = sumE * sumE - sumPx * sumPx - sumPy * sumPy - sumPz * sumPz;
120+
return m2 > 0 ? Math.sqrt(m2) : 0;
121+
}
122+
123+
/**
124+
* Default event classifier for ATLAS Z-path masterclass.
125+
* Classifies events by electron/muon/photon counts.
126+
*/
127+
export function atlasClassifyEvent(tagCounts: Record<string, number>): string {
128+
const e = tagCounts['electron'] ?? 0;
129+
const m = tagCounts['muon'] ?? 0;
130+
const g = tagCounts['photon'] ?? 0;
131+
132+
if (e === 2 && m === 0 && g === 0) return 'e';
133+
if (e === 0 && m === 2 && g === 0) return 'm';
134+
if (e === 0 && m === 0 && g === 2) return 'g';
135+
if (e === 4 && m === 0 && g === 0) return '4e';
136+
if (e === 2 && m === 2 && g === 0) return '2e2m';
137+
if (e === 0 && m === 4 && g === 0) return '4m';
138+
139+
const parts: string[] = [];
140+
if (e > 0) parts.push(`${e}e`);
141+
if (m > 0) parts.push(`${m}m`);
142+
if (g > 0) parts.push(`${g}g`);
143+
return parts.join('') || '?';
144+
}
145+
146+
/** Default masterclass configuration for ATLAS Z-path exercises. */
147+
export const ATLAS_MASTERCLASS_CONFIG: MasterclassConfig = {
148+
title: 'Masterclass \u2014 Invariant Mass',
149+
particleTags: [
150+
{
151+
id: 'electron',
152+
label: 'Electron',
153+
symbol: 'e\u00B1',
154+
color: '#f0c040',
155+
mass: 0.511,
156+
},
157+
{
158+
id: 'muon',
159+
label: 'Muon',
160+
symbol: '\u03BC\u00B1',
161+
color: '#40c060',
162+
mass: 105.658,
163+
},
164+
{
165+
id: 'photon',
166+
label: 'Photon',
167+
symbol: '\u03B3',
168+
color: '#e04040',
169+
mass: 0,
170+
},
171+
],
172+
hints: [
173+
'Z boson \u2248 91 GeV',
174+
'Higgs \u2248 125 GeV',
175+
'J/\u03C8 \u2248 3.1 GeV',
176+
],
177+
classifyEvent: atlasClassifyEvent,
178+
};

packages/phoenix-event-display/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export * from './helpers/runge-kutta';
3232
export * from './helpers/pretty-symbols';
3333
export * from './helpers/active-variable';
3434
export * from './helpers/zip';
35+
export * from './helpers/invariant-mass';
3536

3637
// Loaders
3738
export * from './loaders/event-data-loader';

packages/phoenix-event-display/src/managers/three-manager/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1197,7 +1197,7 @@ export class ThreeManager {
11971197
* Get the selection manager.
11981198
* @returns Selection manager responsible for managing selection of 3D objects.
11991199
*/
1200-
private getSelectionManager(): SelectionManager {
1200+
public getSelectionManager(): SelectionManager {
12011201
if (!this.selectionManager) {
12021202
this.selectionManager = new SelectionManager();
12031203
}

packages/phoenix-ng/projects/phoenix-app/src/app/sections/atlas/atlas.component.html

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
<app-loader [loaded]="loaded" [progress]="loadingProgress"></app-loader>
22
<app-nav></app-nav>
3-
<app-ui-menu [eventDataImportOptions]="eventDataImportOptions"></app-ui-menu>
3+
<app-ui-menu
4+
[eventDataImportOptions]="eventDataImportOptions"
5+
[masterclassConfig]="masterclassConfig"
6+
></app-ui-menu>
47
<app-embed-menu></app-embed-menu>
58
<app-experiment-info
69
logo="assets/images/atlas.svg"

packages/phoenix-ng/projects/phoenix-app/src/app/sections/atlas/atlas.component.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,9 @@ import {
99
PhoenixMenuNode,
1010
JiveXMLLoader,
1111
StateManager,
12+
ATLAS_MASTERCLASS_CONFIG,
1213
} from 'phoenix-event-display';
13-
import type { Configuration } from 'phoenix-event-display';
14+
import type { Configuration, MasterclassConfig } from 'phoenix-event-display';
1415
import { environment } from '../../../environments/environment';
1516
import eventConfig from '../../../../event-config.json';
1617

@@ -30,6 +31,7 @@ export class AtlasComponent implements OnInit, OnDestroy {
3031
EventDataFormat.JIVEXML,
3132
EventDataFormat.ZIP,
3233
];
34+
masterclassConfig: MasterclassConfig = ATLAS_MASTERCLASS_CONFIG;
3335
loaded = false;
3436
loadingProgress = 0;
3537

Lines changed: 6 additions & 0 deletions
Loading

packages/phoenix-ng/projects/phoenix-ui-components/lib/components/phoenix-ui.module.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ import {
6565
EventDataExplorerComponent,
6666
EventDataExplorerDialogComponent,
6767
CycleEventsComponent,
68+
MasterclassPanelComponent,
69+
MasterclassPanelOverlayComponent,
6870
} from './ui-menu';
6971

7072
import { AttributePipe } from '../services/extras/attribute.pipe';
@@ -127,6 +129,8 @@ const PHOENIX_COMPONENTS: Type<any>[] = [
127129
FileExplorerComponent,
128130
RingLoaderComponent,
129131
CycleEventsComponent,
132+
MasterclassPanelComponent,
133+
MasterclassPanelOverlayComponent,
130134
];
131135

132136
@NgModule({

packages/phoenix-ng/projects/phoenix-ui-components/lib/components/ui-menu/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,5 @@ export * from './event-data-explorer/event-data-explorer.component';
3737
export * from './event-data-explorer/event-data-explorer-dialog/event-data-explorer-dialog.component';
3838
export * from './cycle-events/cycle-events.component';
3939
export * from './ui-menu-wrapper/ui-menu-wrapper.component';
40+
export * from './masterclass-panel/masterclass-panel.component';
41+
export * from './masterclass-panel/masterclass-panel-overlay/masterclass-panel-overlay.component';

0 commit comments

Comments
 (0)