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
128 changes: 128 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,128 @@
/** Particle type tags for masterclass tagging. */
export type ParticleTag = 'electron' | 'muon' | 'photon';

/** Particle rest masses in MeV/c². */
const PARTICLE_MASSES: Record<ParticleTag, number> = {
electron: 0.511,
muon: 105.658,
photon: 0,
};

/** 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: ParticleTag;
fourMomentum: FourMomentum;
/** Display-friendly properties. */
pT: number;
eta: number;
phi: number;
}

/**
* Extract a 4-momentum vector from track userData.
* Tracks have pT, eta/phi (or dparams), and we assign mass by tag.
*/
export function fourMomentumFromTrack(
userData: any,
tag: ParticleTag,
): 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 mass = PARTICLE_MASSES[tag];
const E = Math.sqrt(p2 + mass * mass);

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

/**
* Extract a 4-momentum vector from a calorimeter cluster (photon).
* 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;

// Massless: |p| = E, direction from eta/phi
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;
}

/**
* Classify the event type from a set of tagged particles.
* Returns: "e", "m", "g", "4e", "2e2m", "4m", or a custom string.
*/
export function classifyEventType(tags: ParticleTag[]): string {
let e = 0,
m = 0,
g = 0;
for (const tag of tags) {
if (tag === 'electron') e++;
else if (tag === 'muon') m++;
else if (tag === 'photon') g++;
}

// Standard Z/Higgs masterclass classifications
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';

// Fallback: descriptive string
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('') || '?';
}
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
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';
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
<app-overlay
overlayTitle="Masterclass — Invariant Mass"
icon="physics"
[resizable]="true"
[active]="showPanel"
>
<div *ngIf="showPanel" class="mc">
<!-- ───── STEP 1: Pick tracks ───── -->
<section class="mc-section">
<div class="mc-step-header">
<span class="mc-step-num">1</span>
<span>Select tracks</span>
</div>

<select
class="mc-select"
[ngModel]="selectedCollection"
(ngModelChange)="selectCollection($event)"
>
<option *ngFor="let c of collectionNames" [value]="c">{{ c }}</option>
</select>

<div class="mc-track-list" *ngIf="collectionItems.length > 0">
<div
*ngFor="let item of collectionItems"
class="mc-track-row"
[class.mc-track-selected]="item.selected"
(click)="toggleItem(item)"
(mouseenter)="highlightItem(item.uuid)"
>
<input
type="checkbox"
[checked]="item.selected"
(click)="$event.stopPropagation()"
(change)="toggleItem(item)"
/>
<span class="mc-track-id">#{{ item.index }}</span>
<span class="mc-track-prop" *ngIf="item.pT">
p<sub>T</sub>={{ ptGeV(item.pT) }}
</span>
<span class="mc-track-prop" *ngIf="item.eta">
&eta;={{ item.eta | number: '1.1-1' }}
</span>
</div>
</div>
<p class="mc-hint" *ngIf="collectionItems.length === 0">
Load an event to see collections.
</p>
</section>

<!-- ───── STEP 2: Tag particles ───── -->
<section class="mc-section">
<div class="mc-step-header">
<span class="mc-step-num">2</span>
<span>Tag as ({{ selectedCount }} selected)</span>
</div>

<div class="mc-tag-row">
<button
class="mc-tag-btn mc-tag-e"
[disabled]="selectedCount === 0"
(click)="tagSelectedAs('electron')"
>
e&plusmn; <small>electron</small>
</button>
<button
class="mc-tag-btn mc-tag-mu"
[disabled]="selectedCount === 0"
(click)="tagSelectedAs('muon')"
>
&mu;&plusmn; <small>muon</small>
</button>
<button
class="mc-tag-btn mc-tag-g"
[disabled]="selectedCount === 0"
(click)="tagSelectedAs('photon')"
>
&gamma; <small>photon</small>
</button>
</div>
</section>

<!-- ───── Status ───── -->
<p class="mc-status mc-status-{{ statusType }}" *ngIf="statusMessage">
{{ statusMessage }}
</p>

<!-- ───── Tagged Particles Table (HYPATIA-style) ───── -->
<section class="mc-section" *ngIf="taggedParticles.length > 0">
<div class="mc-step-header">
<span class="mc-step-num">3</span>
<span>Tagged particles</span>
</div>

<table class="mc-table">
<thead>
<tr>
<th>#</th>
<th>Type</th>
<th>p<sub>T</sub> <small>GeV</small></th>
<th>&eta;</th>
<th>&phi;</th>
<th>E <small>GeV</small></th>
<th></th>
</tr>
</thead>
<tbody>
<tr *ngFor="let p of taggedParticles; index as i">
<td>{{ i + 1 }}</td>
<td>
<span
class="mc-badge"
[style.background]="tagColor(p.tag) + '33'"
[style.color]="tagColor(p.tag)"
>
{{ tagSymbol(p.tag) }}
</span>
</td>
<td>{{ ptGeV(p.pT) }}</td>
<td>{{ p.eta | number: '1.2-2' }}</td>
<td>{{ p.phi | number: '1.2-2' }}</td>
<td>{{ massGeV(p.fourMomentum.E) }}</td>
<td>
<button class="mc-remove" (click)="removeTagged(i)">
&times;
</button>
</td>
</tr>
</tbody>
<tfoot *ngIf="taggedParticles.length >= 2">
<tr class="mc-mass-row">
<td [attr.colspan]="5">
<strong>M({{ liveEventType }})</strong>
</td>
<td colspan="2">
<strong class="mc-mass-value">{{ massGeV(liveMass) }} GeV</strong>
</td>
</tr>
</tfoot>
</table>

<div class="mc-actions">
<button
class="mc-btn mc-btn-primary"
[disabled]="taggedParticles.length < 2"
(click)="recordResult()"
>
Record Result
</button>
<button class="mc-btn mc-btn-ghost" (click)="clearTagged()">
Clear
</button>
</div>

<!-- Educational hint -->
<div class="mc-hint-box" *ngIf="taggedParticles.length >= 2">
<strong>Hint:</strong>
Z boson &asymp; 91 GeV &middot; Higgs &asymp; 125 GeV &middot; J/&psi;
&asymp; 3.1 GeV
</div>
</section>

<!-- ───── Results History ───── -->
<section class="mc-section" *ngIf="massResults.length > 0">
<div class="mc-step-header">
<span class="mc-step-num">4</span>
<span>Results ({{ massResults.length }})</span>
</div>

<table class="mc-table mc-table-results">
<thead>
<tr>
<th>Event</th>
<th>Type</th>
<th>Particles</th>
<th>M <small>GeV</small></th>
<th></th>
</tr>
</thead>
<tbody>
<tr *ngFor="let r of massResults; index as i">
<td>{{ i + 1 }}</td>
<td>
<span class="mc-event-type">{{ r.eventType }}</span>
</td>
<td>{{ r.particleCount }}</td>
<td>
<strong>{{ massGeV(r.mass) }}</strong>
</td>
<td>
<button class="mc-remove" (click)="removeResult(i)">
&times;
</button>
</td>
</tr>
</tbody>
</table>

<div class="mc-actions">
<button class="mc-btn mc-btn-export" (click)="exportResults()">
Export Results
</button>
<button class="mc-btn mc-btn-ghost" (click)="clearResults()">
Clear All
</button>
</div>
</section>
</div>
</app-overlay>
Loading
Loading