Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions packages/phoenix-event-display/src/event-display.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ export class EventDisplay {
private onEventsChange: ((events: any) => void)[] = [];
/** Array containing callbacks to be called when the displayed event changes. */
private onDisplayedEventChange: ((nowDisplayingEvent: any) => void)[] = [];
/** Generic event bus for integration with external frameworks. */
private eventBus: Map<string, Set<(data: any) => void>> = new Map();
/** Three manager for three.js operations. */
private graphicsLibrary: ThreeManager;
/** Info logger for storing event display logs. */
Expand Down Expand Up @@ -127,6 +129,7 @@ export class EventDisplay {
// Clear accumulated callbacks
this.onEventsChange = [];
this.onDisplayedEventChange = [];
this.eventBus.clear();
// Reset singletons for clean view transition
this.loadingManager?.reset();
this.stateManager?.resetForViewTransition();
Expand Down Expand Up @@ -620,6 +623,48 @@ export class EventDisplay {
};
}

/**
* Subscribe to a named event on the integration event bus.
* Allows external frameworks to react to actions like particle tagging
* or result recording.
*
* Standard event names:
* - `'particle-tagged'`: Fired when a particle is tagged in the masterclass panel.
* - `'particle-untagged'`: Fired when a tagged particle is removed.
* - `'result-recorded'`: Fired when an invariant mass result is recorded.
*
* @param eventName The event name to listen for.
* @param callback Callback invoked with event-specific data.
* @returns Unsubscribe function to remove the listener.
*/
public on(eventName: string, callback: (data: any) => void): () => void {
if (!this.eventBus.has(eventName)) {
this.eventBus.set(eventName, new Set());
}
this.eventBus.get(eventName).add(callback);
return () => {
const listeners = this.eventBus.get(eventName);
if (listeners) {
listeners.delete(callback);
if (listeners.size === 0) {
this.eventBus.delete(eventName);
}
}
};
}

/**
* Emit a named event on the integration event bus.
* @param eventName The event name to emit.
* @param data Data to pass to listeners.
*/
public emit(eventName: string, data?: any): void {
const listeners = this.eventBus.get(eventName);
if (listeners) {
listeners.forEach((cb) => cb(data));
}
}

/**
* Get metadata associated to the displayed event (experiment info, time, run, event...).
* @returns Metadata of the displayed event.
Expand Down
178 changes: 178 additions & 0 deletions packages/phoenix-event-display/src/helpers/invariant-mass.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
/** A Lorentz 4-momentum vector (E, px, py, pz) in MeV. */
export interface FourMomentum {
E: number;
px: number;
py: number;
pz: number;
}

/** A tagged particle with its physics properties. */
export interface TaggedParticle {
uuid: string;
tag: string;
fourMomentum: FourMomentum;
/** Display-friendly properties. */
pT: number;
eta: number;
phi: number;
}

/** Definition of a particle tag for use in masterclass exercises. */
export interface ParticleTagDef {
/** Unique identifier, e.g. 'electron', 'kaon'. */
id: string;
/** Human-readable label, e.g. 'Electron'. */
label: string;
/** Symbol for display, e.g. 'e\u00B1', 'K\u00B1'. */
symbol: string;
/** CSS color for the tag button and badge. */
color: string;
/** Rest mass in MeV/c\u00B2. */
mass: number;
}

/**
* Configuration for experiment-specific masterclass exercises.
* Each experiment (ATLAS, LHCb, CMS, ...) provides its own config.
*/
export interface MasterclassConfig {
/** Panel title, e.g. 'ATLAS Z-Path Masterclass'. */
title: string;
/** Available particle tags for this exercise. */
particleTags: ParticleTagDef[];
/** Educational hints shown when invariant mass is computed. */
hints: string[];
/**
* Classify an event from the tag counts.
* Receives a map of tag id to count, e.g. { electron: 2, muon: 0 }.
* Returns a short label like "e", "4e", "2e2m".
*/
classifyEvent: (tagCounts: Record<string, number>) => string;
}

/**
* Extract a 4-momentum vector from track userData.
* Tracks have pT, eta/phi (or dparams), and we assign mass from the tag definition.
* @param userData Track user data containing kinematic properties.
* @param mass Particle rest mass in MeV/c².
*/
export function fourMomentumFromTrack(
userData: any,
mass: number,
): FourMomentum | null {
const pT = userData.pT;
if (pT == null) return null;

const phi = userData.phi ?? userData.dparams?.[2];
// theta from dparams, or compute from eta
let theta = userData.dparams?.[3];
if (theta == null && userData.eta != null) {
theta = 2 * Math.atan(Math.exp(-userData.eta));
}
if (phi == null || theta == null) return null;

const px = pT * Math.cos(phi);
const py = pT * Math.sin(phi);
const pz = pT / Math.tan(theta);
const p2 = px * px + py * py + pz * pz;
const E = Math.sqrt(p2 + mass * mass);

return { E, px, py, pz };
}

/**
* Extract a 4-momentum vector from a calorimeter cluster.
* Clusters have energy, eta, phi — treated as massless.
*/
export function fourMomentumFromCluster(userData: any): FourMomentum | null {
const energy = userData.energy;
const eta = userData.eta;
const phi = userData.phi;
if (energy == null || eta == null || phi == null) return null;

const theta = 2 * Math.atan(Math.exp(-eta));
const px = energy * Math.sin(theta) * Math.cos(phi);
const py = energy * Math.sin(theta) * Math.sin(phi);
const pz = energy * Math.cos(theta);

return { E: energy, px, py, pz };
}

/**
* Compute the invariant mass of a set of particles in MeV.
* M² = (ΣE)² - (Σpx)² - (Σpy)² - (Σpz)²
*/
export function invariantMass(momenta: FourMomentum[]): number {
if (momenta.length < 2) return 0;

let sumE = 0,
sumPx = 0,
sumPy = 0,
sumPz = 0;
for (const p of momenta) {
sumE += p.E;
sumPx += p.px;
sumPy += p.py;
sumPz += p.pz;
}

const m2 = sumE * sumE - sumPx * sumPx - sumPy * sumPy - sumPz * sumPz;
return m2 > 0 ? Math.sqrt(m2) : 0;
}

/**
* Default event classifier for ATLAS Z-path masterclass.
* Classifies events by electron/muon/photon counts.
*/
export function atlasClassifyEvent(tagCounts: Record<string, number>): string {
const e = tagCounts['electron'] ?? 0;
const m = tagCounts['muon'] ?? 0;
const g = tagCounts['photon'] ?? 0;

if (e === 2 && m === 0 && g === 0) return 'e';
if (e === 0 && m === 2 && g === 0) return 'm';
if (e === 0 && m === 0 && g === 2) return 'g';
if (e === 4 && m === 0 && g === 0) return '4e';
if (e === 2 && m === 2 && g === 0) return '2e2m';
if (e === 0 && m === 4 && g === 0) return '4m';

const parts: string[] = [];
if (e > 0) parts.push(`${e}e`);
if (m > 0) parts.push(`${m}m`);
if (g > 0) parts.push(`${g}g`);
return parts.join('') || '?';
}

/** Default masterclass configuration for ATLAS Z-path exercises. */
export const ATLAS_MASTERCLASS_CONFIG: MasterclassConfig = {
title: 'Masterclass \u2014 Invariant Mass',
particleTags: [
{
id: 'electron',
label: 'Electron',
symbol: 'e\u00B1',
color: '#f0c040',
mass: 0.511,
},
{
id: 'muon',
label: 'Muon',
symbol: '\u03BC\u00B1',
color: '#40c060',
mass: 105.658,
},
{
id: 'photon',
label: 'Photon',
symbol: '\u03B3',
color: '#e04040',
mass: 0,
},
],
hints: [
'Z boson \u2248 91 GeV',
'Higgs \u2248 125 GeV',
'J/\u03C8 \u2248 3.1 GeV',
],
classifyEvent: atlasClassifyEvent,
};
1 change: 1 addition & 0 deletions packages/phoenix-event-display/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ export * from './helpers/runge-kutta';
export * from './helpers/pretty-symbols';
export * from './helpers/active-variable';
export * from './helpers/zip';
export * from './helpers/invariant-mass';

// Loaders
export * from './loaders/event-data-loader';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1197,7 +1197,7 @@ export class ThreeManager {
* Get the selection manager.
* @returns Selection manager responsible for managing selection of 3D objects.
*/
private getSelectionManager(): SelectionManager {
public getSelectionManager(): SelectionManager {
if (!this.selectionManager) {
this.selectionManager = new SelectionManager();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
<app-loader [loaded]="loaded" [progress]="loadingProgress"></app-loader>
<app-nav></app-nav>
<app-ui-menu [eventDataImportOptions]="eventDataImportOptions"></app-ui-menu>
<app-ui-menu
[eventDataImportOptions]="eventDataImportOptions"
[masterclassConfig]="masterclassConfig"
></app-ui-menu>
<app-embed-menu></app-embed-menu>
<app-experiment-info
logo="assets/images/atlas.svg"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ import {
PhoenixMenuNode,
JiveXMLLoader,
StateManager,
ATLAS_MASTERCLASS_CONFIG,
} from 'phoenix-event-display';
import type { Configuration } from 'phoenix-event-display';
import type { Configuration, MasterclassConfig } from 'phoenix-event-display';
import { environment } from '../../../environments/environment';
import eventConfig from '../../../../event-config.json';

Expand All @@ -30,6 +31,7 @@ export class AtlasComponent implements OnInit, OnDestroy {
EventDataFormat.JIVEXML,
EventDataFormat.ZIP,
];
masterclassConfig: MasterclassConfig = ATLAS_MASTERCLASS_CONFIG;
loaded = false;
loadingProgress = 0;

Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@ import {
EventDataExplorerComponent,
EventDataExplorerDialogComponent,
CycleEventsComponent,
MasterclassPanelComponent,
MasterclassPanelOverlayComponent,
} from './ui-menu';

import { AttributePipe } from '../services/extras/attribute.pipe';
Expand Down Expand Up @@ -127,6 +129,8 @@ const PHOENIX_COMPONENTS: Type<any>[] = [
FileExplorerComponent,
RingLoaderComponent,
CycleEventsComponent,
MasterclassPanelComponent,
MasterclassPanelOverlayComponent,
];

@NgModule({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,5 @@ export * from './event-data-explorer/event-data-explorer.component';
export * from './event-data-explorer/event-data-explorer-dialog/event-data-explorer-dialog.component';
export * from './cycle-events/cycle-events.component';
export * from './ui-menu-wrapper/ui-menu-wrapper.component';
export * from './masterclass-panel/masterclass-panel.component';
export * from './masterclass-panel/masterclass-panel-overlay/masterclass-panel-overlay.component';
Loading