Warning, /firebird/packages/root2dex/src/edm4eic.ts is written in an unsupported language. File is not indexed.
0001 /**
0002 * EDM4eic (eicrecon reconstruction output) -> Firebird DEX.
0003 *
0004 * Mirrors pyrobird's `pyrobird/edm4eic.py` piece for piece, so the same input
0005 * entry produces the same DEX document whether it went through the python CLI
0006 * or through this package in the browser.
0007 *
0008 * Collections converted:
0009 * vector<edm4eic::TrackerHitData> -> one BoxHit piece per collection
0010 * edm4eic::TrackSegmentData + its TrackPoints -> one PointTrajectory piece
0011 *
0012 * Empty collections are kept as count-0 pieces (pyrobird does the same here;
0013 * the edm4hep path drops them instead).
0014 */
0015
0016 import { PIECE_VERSION, type DexEvent, type DexPiece } from './dex';
0017 import { numbers, type ColumnBag, type PodioEventFile } from './podio-file';
0018 import {
0019 mcParticlesBranchNames,
0020 mcParticlesToTrajectories,
0021 type McParticlesOptions,
0022 } from './mc-particles';
0023
0024 export const EDM4EIC_TRACKER_HIT_TYPE = 'vector<edm4eic::TrackerHitData>';
0025
0026 /** The TrackSegment collection pyrobird converts; see EICrecon issue #1730. */
0027 const SEGMENT_COLLECTION = 'CentralTrackSegments';
0028 const TRACK_PARAMS_COLLECTION = 'CentralCKFTrackParameters';
0029
0030 /** Which collection groups to convert. Defaults to all of them. */
0031 export type Edm4eicCollection = 'tracker_hits' | 'tracks' | 'mc_particles';
0032 export const EDM4EIC_DEFAULT_COLLECTIONS: Edm4eicCollection[] = [
0033 'tracker_hits',
0034 'tracks',
0035 'mc_particles',
0036 ];
0037
0038 export interface Edm4eicOptions extends McParticlesOptions {
0039 collections?: Edm4eicCollection[];
0040 }
0041
0042 const HIT_FIELDS = [
0043 'position.x',
0044 'position.y',
0045 'position.z',
0046 'positionError.xx',
0047 'positionError.yy',
0048 'positionError.zz',
0049 'time',
0050 'timeError',
0051 'edep',
0052 'edepError',
0053 ];
0054
0055 const POINT_FIELDS = [
0056 'position.x',
0057 'position.y',
0058 'position.z',
0059 'time',
0060 'timeError',
0061 'positionError.xx',
0062 'positionError.yy',
0063 'positionError.zz',
0064 ];
0065
0066 const TRACK_PARAM_FIELDS = ['theta', 'phi', 'qOverP', 'loc.a', 'loc.b', 'time'];
0067
0068 /** positionError.ii is a variance (sigma^2); the box spans +- one sigma. */
0069 function boxWidth(variance: number): number {
0070 return variance > 0 ? 2 * Math.sqrt(variance) : 0.0;
0071 }
0072
0073 /**
0074 * Every branch an edm4eic entry conversion reads. Collected up front so one
0075 * `readEntry` pass covers the whole event - see PodioEventFile.
0076 */
0077 export function edm4eicBranchNames(
0078 file: PodioEventFile,
0079 collections: Edm4eicCollection[] = EDM4EIC_DEFAULT_COLLECTIONS,
0080 options: Edm4eicOptions = {},
0081 ): string[] {
0082 const names: string[] = [];
0083 if (collections.includes('tracker_hits')) {
0084 for (const name of file.collectionsOfType(EDM4EIC_TRACKER_HIT_TYPE)) {
0085 for (const field of HIT_FIELDS) names.push(`${name}.${field}`);
0086 }
0087 }
0088 if (collections.includes('tracks') && file.hasBranch(SEGMENT_COLLECTION)) {
0089 names.push(`${SEGMENT_COLLECTION}.points_begin`, `${SEGMENT_COLLECTION}.points_end`);
0090 const points = `_${SEGMENT_COLLECTION}_points`;
0091 for (const field of POINT_FIELDS) names.push(`${points}.${field}`);
0092 for (const field of TRACK_PARAM_FIELDS) names.push(`${TRACK_PARAMS_COLLECTION}.${field}`);
0093 }
0094 if (collections.includes('mc_particles')) {
0095 names.push(...mcParticlesBranchNames(options));
0096 }
0097 return names;
0098 }
0099
0100 /** Converts one vector<edm4eic::TrackerHitData> collection to a BoxHit piece. */
0101 export function trackerHitsToBoxHits(bag: ColumnBag, name: string): DexPiece {
0102 // The hit count comes from position.x; pyrobird reads cellID for it, which
0103 // this path skips - it is not written to DEX and costs a basket read.
0104 const posX = numbers(bag, `${name}.position.x`);
0105 const posY = numbers(bag, `${name}.position.y`);
0106 const posZ = numbers(bag, `${name}.position.z`);
0107 const errX = numbers(bag, `${name}.positionError.xx`);
0108 const errY = numbers(bag, `${name}.positionError.yy`);
0109 const errZ = numbers(bag, `${name}.positionError.zz`);
0110
0111 const pos: number[] = [];
0112 const dim: number[] = [];
0113 for (let i = 0; i < posX.length; i++) {
0114 pos.push(posX[i], posY[i], posZ[i]);
0115 dim.push(boxWidth(errX[i]), boxWidth(errY[i]), boxWidth(errZ[i]));
0116 }
0117
0118 return {
0119 name,
0120 type: 'BoxHit',
0121 version: PIECE_VERSION,
0122 origin: { type: 'edm4eic::TrackerHitData', name },
0123 count: posX.length,
0124 columns: {
0125 pos,
0126 dim,
0127 time: numbers(bag, `${name}.time`),
0128 timeError: numbers(bag, `${name}.timeError`),
0129 edep: numbers(bag, `${name}.edep`),
0130 edepError: numbers(bag, `${name}.edepError`),
0131 },
0132 };
0133 }
0134
0135 /**
0136 * Converts edm4eic::TrackSegmentData plus its TrackPoints into one
0137 * PointTrajectory piece: each segment becomes one trajectory whose points run
0138 * from points_begin to points_end of the shared point collection.
0139 *
0140 * Track parameters come from CentralCKFTrackParameters positionally, which is
0141 * what pyrobird does - the one-to-one relation to Track cannot be followed
0142 * (EICrecon issue #1730).
0143 */
0144 export function trackSegmentsToTrajectories(
0145 file: PodioEventFile,
0146 bag: ColumnBag,
0147 name: string,
0148 onWarning?: (message: string) => void,
0149 ): DexPiece {
0150 const piece: DexPiece = {
0151 name,
0152 type: 'PointTrajectory',
0153 version: PIECE_VERSION,
0154 origin: ['edm4eic::TrackPoint', 'edm4eic::TrackSegmentData'],
0155 count: 0,
0156 columns: {},
0157 pointColumns: ['x', 'y', 'z', 't', 'dx', 'dy', 'dz', 'dt'],
0158 points: [],
0159 };
0160
0161 const beginIndex = numbers(bag, `${name}.points_begin`);
0162 const endIndex = numbers(bag, `${name}.points_end`);
0163
0164 const pointsCollection = `_${name}_points`;
0165 if (!file.hasBranch(pointsCollection)) {
0166 // The file organizes the points differently, or there are none
0167 return piece;
0168 }
0169
0170 const pointField = (field: string) => numbers(bag, `${pointsCollection}.${field}`);
0171 const px = pointField('position.x');
0172 const py = pointField('position.y');
0173 const pz = pointField('position.z');
0174 const pt = pointField('time');
0175 const pTimeError = pointField('timeError');
0176 const pErrXX = pointField('positionError.xx');
0177 const pErrYY = pointField('positionError.yy');
0178 const pErrZZ = pointField('positionError.zz');
0179
0180 const paramsExist = file.hasBranch(TRACK_PARAMS_COLLECTION);
0181 const param = (field: string) =>
0182 paramsExist ? numbers(bag, `${TRACK_PARAMS_COLLECTION}.${field}`) : [];
0183 const theta = param('theta');
0184 const phi = param('phi');
0185 const qOverP = param('qOverP');
0186 const locA = param('loc.a');
0187 const locB = param('loc.b');
0188 const paramTime = param('time');
0189
0190 const segmentCount = beginIndex.length;
0191 if (paramsExist && segmentCount !== theta.length) {
0192 onWarning?.(
0193 `len(${TRACK_PARAMS_COLLECTION}) != len(${name}). ` +
0194 `Might be a sign of format change or broken tree`,
0195 );
0196 }
0197
0198 const allPoints: number[][][] = [];
0199 for (let segment = 0; segment < segmentCount; segment++) {
0200 const segmentPoints: number[][] = [];
0201 for (let i = beginIndex[segment]; i < endIndex[segment]; i++) {
0202 // pointColumns => [x, y, z, t, dx, dy, dz, dt]; the position errors are
0203 // variances, so the point extent is +- one sigma
0204 segmentPoints.push([
0205 px[i],
0206 py[i],
0207 pz[i],
0208 pt[i],
0209 boxWidth(pErrXX[i]),
0210 boxWidth(pErrYY[i]),
0211 boxWidth(pErrZZ[i]),
0212 pTimeError[i],
0213 ]);
0214 }
0215 allPoints.push(segmentPoints);
0216 }
0217
0218 piece.count = segmentCount;
0219 piece.points = allPoints;
0220 if (paramsExist) {
0221 // trajectory id == index in every column; a missing tail (parameter list
0222 // shorter than segments) is padded with null rather than dropped
0223 const padded = (values: number[]) =>
0224 Array.from({ length: segmentCount }, (_, i) => (i < values.length ? values[i] : null));
0225 piece.columns = {
0226 theta: padded(theta),
0227 phi: padded(phi),
0228 q_over_p: padded(qOverP),
0229 loc_a: padded(locA),
0230 loc_b: padded(locB),
0231 time: padded(paramTime),
0232 };
0233 }
0234 return piece;
0235 }
0236
0237 /** Converts one entry of an edm4eic 'events' tree to a DEX event object. */
0238 export async function edm4eicEntryToDex(
0239 file: PodioEventFile,
0240 entry: number,
0241 options: Edm4eicOptions = {},
0242 ): Promise<DexEvent> {
0243 const collections = options.collections?.length
0244 ? options.collections
0245 : EDM4EIC_DEFAULT_COLLECTIONS;
0246 const bag = await file.readEntry(edm4eicBranchNames(file, collections, options), entry);
0247
0248 const pieces: DexPiece[] = [];
0249
0250 if (collections.includes('tracker_hits')) {
0251 for (const name of file.collectionsOfType(EDM4EIC_TRACKER_HIT_TYPE)) {
0252 pieces.push(trackerHitsToBoxHits(bag, name));
0253 }
0254 }
0255
0256 if (collections.includes('tracks') && file.hasBranch(SEGMENT_COLLECTION)) {
0257 pieces.push(trackSegmentsToTrajectories(file, bag, SEGMENT_COLLECTION, options.onWarning));
0258 }
0259
0260 if (collections.includes('mc_particles')) {
0261 const piece = mcParticlesToTrajectories(file, bag, options);
0262 if (piece.count > 0) pieces.push(piece);
0263 }
0264
0265 return { id: entry, pieces };
0266 }