Back to home page

EIC code displayed by LXR

 
 

    


Warning, /firebird/packages/root2dex/src/edm4hep.ts is written in an unsupported language. File is not indexed.

0001 /**
0002  * EDM4hep (ddsim / DD4hep simulation output) -> Firebird DEX.
0003  *
0004  * Mirrors pyrobird's `pyrobird/edm4hep.py` piece for piece.
0005  *
0006  * Collections converted:
0007  *   vector<edm4hep::SimTrackerHitData> -> one BoxHit piece per collection
0008  *   vector<edm4hep::MCParticleData>    -> MC-truth trajectories connecting the
0009  *                                         sim hits of each particle
0010  *
0011  * Sim hits carry no positionError/timeError/edepError, so BoxHit dimensions use
0012  * a fixed configurable box size and the error columns are omitted entirely.
0013  */
0014 
0015 import { PIECE_VERSION, type DexEvent, type DexPiece } from './dex';
0016 import { numbers, type ColumnBag, type PodioEventFile } from './podio-file';
0017 import {
0018   mcParticlesBranchNames,
0019   mcParticlesToTrajectories,
0020   type McParticlesOptions,
0021 } from './mc-particles';
0022 
0023 export const EDM4HEP_SIM_HIT_TYPE = 'vector<edm4hep::SimTrackerHitData>';
0024 
0025 /** Sim hits have no positionError, so boxes get a fixed visualization size [mm]. */
0026 export const DEFAULT_HIT_BOX_SIZE = 2.0;
0027 
0028 /** Which collection groups to convert. Defaults to all of them. */
0029 export type Edm4hepCollection = 'tracker_hits' | 'mc_trajectories' | 'mc_particles';
0030 export const EDM4HEP_DEFAULT_COLLECTIONS: Edm4hepCollection[] = [
0031   'tracker_hits',
0032   'mc_trajectories',
0033   'mc_particles',
0034 ];
0035 
0036 export interface Edm4hepOptions extends McParticlesOptions {
0037   collections?: Edm4hepCollection[];
0038   /** Box size in mm for sim hits, which carry no position error. */
0039   boxSize?: number;
0040   /** Prepend the MCParticle vertex (with its creation time) as trajectory point 0. */
0041   prependVertex?: boolean;
0042   /** Append the MCParticle endpoint as the last trajectory point. */
0043   appendEndpoint?: boolean;
0044   /** Particles with fewer hits are skipped; their hits stay visible as boxes. */
0045   minHits?: number;
0046   /** Collection name the sim-hit relations point to. */
0047   mcBranch?: string;
0048   /**
0049    * Sim hit collections left out of MC-truth trajectories; they still convert
0050    * as BoxHit pieces. Default: none. Use it for Cherenkov/PID collections:
0051    * they record photon detections attributed to the emitting charged particle
0052    * (optical photons are not stored in MCParticles), and joining them into
0053    * that particle's trajectory draws zigzags along the photosensor planes.
0054    * `pyrobird convert` applies its own default list (DEFAULT_TRAJECTORY_EXCLUDED_COLLECTIONS
0055    * in pyrobird/edm4hep.py); pass the same list to get its output.
0056    */
0057   trajectoryExcludedCollections?: string[];
0058   onWarning?: (message: string) => void;
0059 }
0060 
0061 const MC_FIELDS_ALWAYS = [
0062   'PDG',
0063   'charge',
0064   'time',
0065   'momentum.x',
0066   'momentum.y',
0067   'momentum.z',
0068   'vertex.x',
0069   'vertex.y',
0070   'vertex.z',
0071 ];
0072 const MC_FIELDS_ENDPOINT = ['endpoint.x', 'endpoint.y', 'endpoint.z'];
0073 
0074 /**
0075  * Finds the hit -> MCParticle relation branch, or null when missing.
0076  * edm4hep >= 0.99 names it '_<Collection>_particle', older versions
0077  * '_<Collection>_MCParticle'.
0078  */
0079 export function findParticleRelationBranch(file: PodioEventFile, name: string): string | null {
0080   for (const suffix of ['particle', 'MCParticle']) {
0081     const candidate = `_${name}_${suffix}`;
0082     if (file.hasBranch(candidate)) return candidate;
0083   }
0084   return null;
0085 }
0086 
0087 /** Returns the energy deposit field name: 'eDep' (edm4hep >= 0.99) or 'EDep' (older). */
0088 export function findEdepField(file: PodioEventFile, name: string): string | null {
0089   for (const field of ['eDep', 'EDep']) {
0090     if (file.hasBranch(`${name}.${field}`)) return field;
0091   }
0092   return null;
0093 }
0094 
0095 /**
0096  * Every branch an edm4hep entry conversion reads. Collected up front so one
0097  * `readEntry` pass covers the whole event - see PodioEventFile.
0098  */
0099 export function edm4hepBranchNames(file: PodioEventFile, options: Edm4hepOptions = {}): string[] {
0100   const collections = options.collections?.length
0101     ? options.collections
0102     : EDM4HEP_DEFAULT_COLLECTIONS;
0103   const excluded = options.trajectoryExcludedCollections ?? [];
0104   const mcBranch = options.mcBranch ?? 'MCParticles';
0105   const hitBranches = file.collectionsOfType(EDM4HEP_SIM_HIT_TYPE);
0106   const names: string[] = [];
0107 
0108   if (collections.includes('tracker_hits')) {
0109     for (const name of hitBranches) {
0110       names.push(`${name}.position.x`, `${name}.position.y`, `${name}.position.z`, `${name}.time`);
0111       const edep = findEdepField(file, name);
0112       if (edep) names.push(`${name}.${edep}`);
0113     }
0114   }
0115 
0116   if (collections.includes('mc_trajectories')) {
0117     for (const field of MC_FIELDS_ALWAYS) names.push(`${mcBranch}.${field}`);
0118     if (options.appendEndpoint) {
0119       for (const field of MC_FIELDS_ENDPOINT) names.push(`${mcBranch}.${field}`);
0120     }
0121     for (const name of hitBranches) {
0122       if (excluded.includes(name)) continue;
0123       names.push(`${name}.position.x`, `${name}.position.y`, `${name}.position.z`, `${name}.time`);
0124       const relation = findParticleRelationBranch(file, name);
0125       if (relation) names.push(`${relation}.index`, `${relation}.collectionID`);
0126     }
0127   }
0128 
0129   if (collections.includes('mc_particles')) {
0130     names.push(...mcParticlesBranchNames(options));
0131   }
0132 
0133   return names;
0134 }
0135 
0136 /** Converts one vector<edm4hep::SimTrackerHitData> collection to a BoxHit piece. */
0137 export function simTrackerHitsToBoxHits(
0138   file: PodioEventFile,
0139   bag: ColumnBag,
0140   name: string,
0141   boxSize = DEFAULT_HIT_BOX_SIZE,
0142   onWarning?: (message: string) => void,
0143 ): DexPiece {
0144   const posX = numbers(bag, `${name}.position.x`);
0145   const posY = numbers(bag, `${name}.position.y`);
0146   const posZ = numbers(bag, `${name}.position.z`);
0147   const count = posX.length;
0148 
0149   const edepField = findEdepField(file, name);
0150   let edep: number[];
0151   if (edepField) {
0152     edep = numbers(bag, `${name}.${edepField}`);
0153   } else {
0154     onWarning?.(`No eDep/EDep field found in '${name}', writing 0 energy deposit`);
0155     edep = new Array<number>(count).fill(0);
0156   }
0157 
0158   const pos: number[] = [];
0159   for (let i = 0; i < count; i++) pos.push(posX[i], posY[i], posZ[i]);
0160 
0161   return {
0162     name,
0163     type: 'BoxHit',
0164     version: PIECE_VERSION,
0165     origin: { type: 'edm4hep::SimTrackerHitData', name },
0166     count,
0167     columns: {
0168       // Sim data carries no errors, so the error columns are omitted entirely
0169       pos,
0170       dim: new Array<number>(3 * count).fill(boxSize),
0171       time: numbers(bag, `${name}.time`),
0172       edep,
0173     },
0174   };
0175 }
0176 
0177 /**
0178  * Builds MC-truth trajectories connecting SimTrackerHit hits.
0179  *
0180  * Hits from all `hitBranchNames` collections are pooled, grouped by their
0181  * associated MCParticle and sorted by time. Each particle with at least
0182  * `minHits` hits becomes one trajectory in a single PointTrajectory piece.
0183  */
0184 export function simHitsToTrajectories(
0185   file: PodioEventFile,
0186   bag: ColumnBag,
0187   hitBranchNames: string[],
0188   options: Edm4hepOptions & { mcCollectionId?: number | null } = {},
0189 ): DexPiece {
0190   const mcBranch = options.mcBranch ?? 'MCParticles';
0191   const minHits = options.minHits ?? 2;
0192   const prependVertex = options.prependVertex ?? true;
0193   const appendEndpoint = options.appendEndpoint ?? false;
0194   const onWarning = options.onWarning;
0195 
0196   const piece: DexPiece = {
0197     name: 'McHitTrajectories',
0198     type: 'PointTrajectory',
0199     version: PIECE_VERSION,
0200     origin: {
0201       type: ['edm4hep::SimTrackerHitData', 'edm4hep::MCParticleData'],
0202       collections: [...hitBranchNames],
0203     },
0204     count: 0,
0205     columns: {},
0206     pointColumns: ['x', 'y', 'z', 't', 'dx', 'dy', 'dz', 'dt'],
0207     points: [],
0208   };
0209 
0210   if (!file.hasBranch(mcBranch)) {
0211     onWarning?.(`'${mcBranch}' collection not found, no trajectories are built`);
0212     return piece;
0213   }
0214 
0215   const mc = (field: string) => numbers(bag, `${mcBranch}.${field}`);
0216   const mcPdg = mc('PDG');
0217   const mcCharge = mc('charge');
0218   const mcTime = mc('time');
0219   const mcMomX = mc('momentum.x');
0220   const mcMomY = mc('momentum.y');
0221   const mcMomZ = mc('momentum.z');
0222   const mcVtxX = mc('vertex.x');
0223   const mcVtxY = mc('vertex.y');
0224   const mcVtxZ = mc('vertex.z');
0225   const mcEndX = appendEndpoint ? mc('endpoint.x') : [];
0226   const mcEndY = appendEndpoint ? mc('endpoint.y') : [];
0227   const mcEndZ = appendEndpoint ? mc('endpoint.z') : [];
0228 
0229   // eicrecon files carry a second MCParticle collection
0230   // (MCParticlesHeadOnFrameNoBeamFX); all known sim-hit relations point to
0231   // 'MCParticles', but sanity-check via collectionID when podio metadata is
0232   // available
0233   const mcCollectionId = options.mcCollectionId ?? null;
0234 
0235   // Pool hits from all collections grouped by MCParticle index.
0236   // Each hit is [time, x, y, z].
0237   const hitsByParticle = new Map<number, number[][]>();
0238   for (const name of hitBranchNames) {
0239     const relation = findParticleRelationBranch(file, name);
0240     if (relation === null) {
0241       onWarning?.(
0242         `No MCParticle relation branch found for '${name}', ` +
0243           `its hits are skipped in trajectory building`,
0244       );
0245       continue;
0246     }
0247     const posX = numbers(bag, `${name}.position.x`);
0248     const posY = numbers(bag, `${name}.position.y`);
0249     const posZ = numbers(bag, `${name}.position.z`);
0250     const time = numbers(bag, `${name}.time`);
0251     const mcIndex = numbers(bag, `${relation}.index`);
0252     const collectionIds = numbers(bag, `${relation}.collectionID`);
0253 
0254     for (let i = 0; i < posX.length; i++) {
0255       const index = mcIndex[i];
0256       if (index < 0 || index >= mcPdg.length) continue; // unfilled relation
0257       if (mcCollectionId !== null && collectionIds[i] !== mcCollectionId) {
0258         onWarning?.(
0259           `Hit in '${name}' points to an unexpected MCParticle collection ` +
0260             `(collectionID=${collectionIds[i]}), skipping it`,
0261         );
0262         continue;
0263       }
0264       let hits = hitsByParticle.get(index);
0265       if (!hits) {
0266         hits = [];
0267         hitsByParticle.set(index, hits);
0268       }
0269       hits.push([time[i], posX[i], posY[i], posZ[i]]);
0270     }
0271   }
0272 
0273   const columnNames = ['pdg', 'charge', 'px', 'py', 'pz', 'p', 'mc_index'] as const;
0274   const columns: Record<string, number[]> = {};
0275   for (const columnName of columnNames) columns[columnName] = [];
0276   const allPoints: number[][][] = [];
0277 
0278   for (const index of [...hitsByParticle.keys()].sort((a, b) => a - b)) {
0279     const particleHits = hitsByParticle.get(index)!;
0280     if (particleHits.length < minHits) continue;
0281     particleHits.sort((a, b) => a[0] - b[0]);
0282 
0283     // pointColumns => [x, y, z, t, dx, dy, dz, dt]; sim data has no errors
0284     const points: number[][] = [];
0285     if (prependVertex) {
0286       points.push([mcVtxX[index], mcVtxY[index], mcVtxZ[index], mcTime[index], 0, 0, 0, 0]);
0287     }
0288     for (const [hitTime, x, y, z] of particleHits) {
0289       points.push([x, y, z, hitTime, 0, 0, 0, 0]);
0290     }
0291     if (appendEndpoint) {
0292       // edm4hep stores no endpoint time, reuse the last hit time to keep time
0293       // animation monotonic
0294       const lastTime = particleHits[particleHits.length - 1][0];
0295       points.push([mcEndX[index], mcEndY[index], mcEndZ[index], lastTime, 0, 0, 0, 0]);
0296     }
0297 
0298     const momentum = Math.sqrt(
0299       mcMomX[index] ** 2 + mcMomY[index] ** 2 + mcMomZ[index] ** 2,
0300     );
0301     columns['pdg'].push(mcPdg[index]);
0302     columns['charge'].push(mcCharge[index]);
0303     columns['px'].push(mcMomX[index]);
0304     columns['py'].push(mcMomY[index]);
0305     columns['pz'].push(mcMomZ[index]);
0306     columns['p'].push(momentum);
0307     columns['mc_index'].push(index);
0308     allPoints.push(points);
0309   }
0310 
0311   piece.count = allPoints.length;
0312   piece.columns = allPoints.length ? columns : {};
0313   piece.points = allPoints;
0314   return piece;
0315 }
0316 
0317 /** Converts one entry of an edm4hep 'events' tree to a DEX event object. */
0318 export async function edm4hepEntryToDex(
0319   file: PodioEventFile,
0320   entry: number,
0321   options: Edm4hepOptions & { mcCollectionId?: number | null } = {},
0322 ): Promise<DexEvent> {
0323   const collections = options.collections?.length
0324     ? options.collections
0325     : EDM4HEP_DEFAULT_COLLECTIONS;
0326   const excluded = options.trajectoryExcludedCollections ?? [];
0327   const boxSize = options.boxSize ?? DEFAULT_HIT_BOX_SIZE;
0328 
0329   const bag = await file.readEntry(edm4hepBranchNames(file, options), entry);
0330   const hitBranches = file.collectionsOfType(EDM4HEP_SIM_HIT_TYPE);
0331   const pieces: DexPiece[] = [];
0332 
0333   if (collections.includes('tracker_hits')) {
0334     for (const name of hitBranches) {
0335       const piece = simTrackerHitsToBoxHits(file, bag, name, boxSize, options.onWarning);
0336       // Collections empty in this event are skipped
0337       if (piece.count > 0) pieces.push(piece);
0338     }
0339   }
0340 
0341   if (collections.includes('mc_trajectories')) {
0342     const trajectoryBranches = hitBranches.filter(name => !excluded.includes(name));
0343     const piece = simHitsToTrajectories(file, bag, trajectoryBranches, options);
0344     if (piece.count > 0) pieces.push(piece);
0345   }
0346 
0347   if (collections.includes('mc_particles')) {
0348     const piece = mcParticlesToTrajectories(file, bag, options);
0349     if (piece.count > 0) pieces.push(piece);
0350   }
0351 
0352   return { id: entry, pieces };
0353 }