Warning, /firebird/packages/root2dex/src/mc-particles.ts is written in an unsupported language. File is not indexed.
0001 /**
0002 * MCParticles (vector<edm4hep::MCParticleData>) -> Firebird DEX.
0003 *
0004 * Mirrors pyrobird's `pyrobird/mc_particles.py` VALUE FOR VALUE - parity.spec.ts
0005 * pins the two against the same reference documents. Keep the arithmetic
0006 * expression-for-expression identical when changing either side.
0007 *
0008 * Both EDM4hep (ddsim) and EDM4eic (eicrecon) files carry this collection, so
0009 * the same conversion serves both models. Every particle becomes one straight
0010 * line from its vertex to its endpoint - no filtering, trajectory id equals the
0011 * MCParticle index. The line is subdivided on a fixed time grid so the event
0012 * display time animation reveals it gradually instead of popping it in whole.
0013 *
0014 * EDM4hep stores no endpoint time; the flight duration is computed from the
0015 * relativistic speed beta = p/E with E = sqrt(p^2 + m^2), which is exact for
0016 * the straight-line (no field, no energy loss) picture this piece draws.
0017 */
0018
0019 import { PIECE_VERSION, type DexPiece } from './dex';
0020 import { numbers, type ColumnBag, type PodioEventFile } from './podio-file';
0021
0022 /** Time step [ns] of the interpolation grid; matches the `pyrobird smooth` default. */
0023 export const DEFAULT_MC_STEP_TIME = 0.2;
0024
0025 /**
0026 * Size guard: a particle flying far (a neutrino crossing the world volume) gets
0027 * its grid coarsened so one line never exceeds this many points.
0028 */
0029 export const DEFAULT_MC_MAX_POINTS = 128;
0030
0031 /** Speed of light [mm/ns]. */
0032 export const C_LIGHT = 299.792458;
0033
0034 export interface McParticlesOptions {
0035 /** Collection name, 'MCParticles' in every known file. */
0036 mcBranch?: string;
0037 /** Time step in ns of the straight-line interpolation grid. */
0038 mcStepTime?: number;
0039 /** Maximum points per particle line; longer flights get a coarser grid. */
0040 mcMaxPoints?: number;
0041 onWarning?: (message: string) => void;
0042 }
0043
0044 const MC_PARTICLE_FIELDS = [
0045 'PDG',
0046 'generatorStatus',
0047 'charge',
0048 'time',
0049 'mass',
0050 'vertex.x',
0051 'vertex.y',
0052 'vertex.z',
0053 'endpoint.x',
0054 'endpoint.y',
0055 'endpoint.z',
0056 'momentum.x',
0057 'momentum.y',
0058 'momentum.z',
0059 ];
0060
0061 /** Every branch the mc_particles conversion reads. */
0062 export function mcParticlesBranchNames(options: McParticlesOptions = {}): string[] {
0063 const mcBranch = options.mcBranch ?? 'MCParticles';
0064 return MC_PARTICLE_FIELDS.map(field => `${mcBranch}.${field}`);
0065 }
0066
0067 /**
0068 * Subdivides the straight line vertex->endpoint on a fixed time grid.
0069 *
0070 * Returns [x, y, z, t] points: the vertex at `startTime`, interior points every
0071 * `stepTime` ns of flight, and the endpoint at the arrival time. A particle
0072 * that cannot move (beta <= 0) or does not move (zero distance) yields the two
0073 * end points with equal times. `maxPoints` bounds the list; when the flight
0074 * needs more grid steps, the step widens to duration / (n + 1) so the
0075 * subdivision stays uniform.
0076 */
0077 export function interpolateLinePoints(
0078 vx: number, vy: number, vz: number,
0079 ex: number, ey: number, ez: number,
0080 startTime: number, beta: number,
0081 stepTime = DEFAULT_MC_STEP_TIME, maxPoints = DEFAULT_MC_MAX_POINTS,
0082 ): number[][] {
0083 const dx = ex - vx;
0084 const dy = ey - vy;
0085 const dz = ez - vz;
0086 const dist = Math.sqrt(dx * dx + dy * dy + dz * dz);
0087 const duration = beta > 0 ? dist / (beta * C_LIGHT) : 0.0;
0088
0089 const points: number[][] = [[vx, vy, vz, startTime]];
0090 if (duration > 0) {
0091 let nMid = Math.max(0, Math.ceil(duration / stepTime) - 1);
0092 if (nMid > maxPoints - 2) {
0093 nMid = maxPoints - 2;
0094 }
0095 const step = duration / (nMid + 1);
0096 for (let k = 1; k <= nMid; k++) {
0097 const tOff = k * step;
0098 const alpha = tOff / duration;
0099 points.push([vx + alpha * dx, vy + alpha * dy, vz + alpha * dz, startTime + tOff]);
0100 }
0101 }
0102 points.push([ex, ey, ez, startTime + duration]);
0103 return points;
0104 }
0105
0106 /**
0107 * Converts the MCParticles collection of one read entry to a PointTrajectory
0108 * piece. Every particle is one straight line vertex->endpoint (trajectory id
0109 * equals the MCParticle index), subdivided on a fixed time grid - see the
0110 * module doc. Returns a count-0 piece when the collection is absent.
0111 */
0112 export function mcParticlesToTrajectories(
0113 file: PodioEventFile,
0114 bag: ColumnBag,
0115 options: McParticlesOptions = {},
0116 ): DexPiece {
0117 const mcBranch = options.mcBranch ?? 'MCParticles';
0118 const stepTime = options.mcStepTime ?? DEFAULT_MC_STEP_TIME;
0119 const maxPoints = options.mcMaxPoints ?? DEFAULT_MC_MAX_POINTS;
0120
0121 const piece: DexPiece = {
0122 name: mcBranch,
0123 type: 'PointTrajectory',
0124 version: PIECE_VERSION,
0125 origin: { type: 'edm4hep::MCParticleData', name: mcBranch },
0126 count: 0,
0127 columns: {},
0128 pointColumns: ['x', 'y', 'z', 't'],
0129 points: [],
0130 };
0131
0132 if (!file.hasBranch(mcBranch)) {
0133 options.onWarning?.(`'${mcBranch}' collection not found, no MC particle lines are built`);
0134 return piece;
0135 }
0136
0137 const field = (name: string) => numbers(bag, `${mcBranch}.${name}`);
0138 const pdg = field('PDG');
0139 const genStatus = field('generatorStatus');
0140 const charge = field('charge');
0141 const time = field('time');
0142 const mass = field('mass');
0143 const vtxX = field('vertex.x');
0144 const vtxY = field('vertex.y');
0145 const vtxZ = field('vertex.z');
0146 const endX = field('endpoint.x');
0147 const endY = field('endpoint.y');
0148 const endZ = field('endpoint.z');
0149 const momX = field('momentum.x');
0150 const momY = field('momentum.y');
0151 const momZ = field('momentum.z');
0152
0153 const count = pdg.length;
0154 const momentum: number[] = [];
0155 const allPoints: number[][][] = [];
0156 for (let i = 0; i < count; i++) {
0157 const p = Math.sqrt(momX[i] * momX[i] + momY[i] * momY[i] + momZ[i] * momZ[i]);
0158 const energy = Math.sqrt(p * p + mass[i] * mass[i]);
0159 const beta = energy > 0 ? p / energy : 0.0;
0160 momentum.push(p);
0161 allPoints.push(interpolateLinePoints(
0162 vtxX[i], vtxY[i], vtxZ[i], endX[i], endY[i], endZ[i],
0163 time[i], beta, stepTime, maxPoints));
0164 }
0165
0166 piece.count = count;
0167 piece.points = allPoints;
0168 if (count) {
0169 piece.columns = {
0170 pdg,
0171 charge,
0172 gen_status: genStatus,
0173 px: momX,
0174 py: momY,
0175 pz: momZ,
0176 p: momentum,
0177 mass,
0178 };
0179 }
0180 return piece;
0181 }