Skip to content

Commit 8a6784c

Browse files
committed
feat: add masterclass panel and ATLAS masterclass landing page (closes #835, closes #915)
Signed-off-by: rx18-eng <remopanda78@gmail.com>
1 parent 75e97fc commit 8a6784c

22 files changed

Lines changed: 1545 additions & 3 deletions

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

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ export class EventDisplay {
4545
private onEventsChange: ((events: any) => void)[] = [];
4646
/** Array containing callbacks to be called when the displayed event changes. */
4747
private onDisplayedEventChange: ((nowDisplayingEvent: any) => void)[] = [];
48+
/** Generic event bus for integration with external frameworks. */
49+
private eventBus: Map<string, Set<(data: any) => void>> = new Map();
4850
/** Three manager for three.js operations. */
4951
private graphicsLibrary: ThreeManager;
5052
/** Info logger for storing event display logs. */
@@ -130,11 +132,54 @@ export class EventDisplay {
130132
// Clear accumulated callbacks
131133
this.onEventsChange = [];
132134
this.onDisplayedEventChange = [];
135+
this.eventBus.clear();
133136
// Reset singletons for clean view transition
134137
this.loadingManager?.reset();
135138
this.stateManager?.resetForViewTransition();
136139
}
137140

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

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ export * from './helpers/zip';
3535
export * from './helpers/event-summary';
3636
export * from './helpers/eta-phi-config';
3737
export * from './helpers/kinematics-config';
38+
export * from './helpers/invariant-mass';
3839

3940
// Loaders
4041
export * from './loaders/event-data-loader';

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

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1182,10 +1182,11 @@ export class ThreeManager {
11821182
// ********************************
11831183

11841184
/**
1185-
* Get the selection manager.
1185+
* Get the selection manager. Public so external integrations (e.g. the
1186+
* masterclass panel) can hook into hover and click events directly.
11861187
* @returns Selection manager responsible for managing selection of 3D objects.
11871188
*/
1188-
private getSelectionManager(): SelectionManager {
1189+
public getSelectionManager(): SelectionManager {
11891190
if (!this.selectionManager) {
11901191
this.selectionManager = new SelectionManager();
11911192
}

packages/phoenix-ng/projects/phoenix-app/src/app/app.module.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { AppComponent } from './app.component';
66
import { HomeComponent } from './home/home.component';
77
import { GeometryComponent } from './sections/geometry/geometry.component';
88
import { AtlasComponent } from './sections/atlas/atlas.component';
9+
import { AtlasMasterclassComponent } from './sections/atlas-masterclass/atlas-masterclass.component';
910
import { LHCbComponent } from './sections/lhcb/lhcb.component';
1011
import { VPToggleComponent } from './sections/lhcb/vp-toggle/vp-toggle.component';
1112
import { CMSComponent } from './sections/cms/cms.component';
@@ -26,6 +27,7 @@ const routes: Routes = [
2627
{ path: 'home', component: HomeComponent },
2728
{ path: 'geometry', component: GeometryComponent },
2829
{ path: 'atlas', component: AtlasComponent },
30+
{ path: 'atlas-masterclass', component: AtlasMasterclassComponent },
2931
{ path: 'lhcb', component: LHCbComponent },
3032
{ path: 'cms', component: CMSComponent },
3133
{ path: 'trackml', component: TrackmlComponent },
@@ -39,6 +41,7 @@ const routes: Routes = [
3941
HomeComponent,
4042
GeometryComponent,
4143
AtlasComponent,
44+
AtlasMasterclassComponent,
4245
LHCbComponent,
4346
VPToggleComponent,
4447
CMSComponent,

packages/phoenix-ng/projects/phoenix-app/src/app/home/home.component.html

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,21 @@ <h5 class="card-title">ATLAS</h5>
5050
</div>
5151
</div>
5252

53+
<div class="card">
54+
<img
55+
class="card-img-top"
56+
src="assets/icons/physics.svg"
57+
alt="Masterclass particle tagging illustration"
58+
/>
59+
<div class="card-body d-flex flex-column">
60+
<h5 class="card-title">Masterclass</h5>
61+
<p class="card-text">
62+
Tag particles and measure invariant mass. Simplified UI for students.
63+
</p>
64+
<a routerLink="/atlas-masterclass" class="btn btn-primary">Show</a>
65+
</div>
66+
</div>
67+
5368
<div class="card">
5469
<img
5570
class="card-img-top"
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<app-loader [loaded]="loaded" [progress]="loadingProgress"></app-loader>
2+
<app-nav></app-nav>
3+
<app-ui-menu
4+
[eventDataImportOptions]="eventDataImportOptions"
5+
[uiConfig]="uiConfig"
6+
[masterclassConfig]="masterclassConfig"
7+
></app-ui-menu>
8+
<app-embed-menu></app-embed-menu>
9+
<app-experiment-info
10+
logo="assets/images/atlas.svg"
11+
url="https://home.cern/science/experiments/atlas"
12+
tagline="ATLAS Masterclass"
13+
></app-experiment-info>
14+
<app-phoenix-menu [rootNode]="phoenixMenuRoot"></app-phoenix-menu>
15+
<div id="eventDisplay"></div>

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

Whitespace-only changes.

0 commit comments

Comments
 (0)