Warning, /firebird/packages/root2dex/src/podio-file.ts is written in an unsupported language. File is not indexed.
0001 /**
0002 * The JSROOT access layer: opens a podio ROOT file and reads single entries out
0003 * of its 'events' TTree.
0004 *
0005 * Partial reading is the point of this class. JSROOT never loads a whole file:
0006 * `openFile` reads the key directory and the streamer info, `readObject('events')`
0007 * reads the TTree metadata, and a `treeProcess` pass reads only the baskets that
0008 * cover the requested entries of the requested branches. A multi-GB file costs a
0009 * handful of byte ranges per event.
0010 *
0011 * All branches an event needs are read in ONE pass (`readEntry`) so JSROOT can
0012 * batch their baskets into as few reads as possible - a pass per field would
0013 * multiply the round trips by the number of columns.
0014 *
0015 * Sources, in order of what JSROOT does with them:
0016 * - a browser `File` (drag-and-drop / file picker) - sliced with FileReader
0017 * - an http(s) URL - HTTP Range requests
0018 * - a `file://` path or plain path under node - positional reads
0019 * - any object implementing JSROOT's FileProxy contract (`getFileName`,
0020 * `getFileSize`, `readBuffer(pos, len)`) - the seam a future XRootD
0021 * byte-range proxy plugs into without touching this package
0022 */
0023
0024 // jsroot declares its subpath modules (jsroot/io, jsroot/tree) in the types
0025 // of its main entry; the reference loads them without importing the main
0026 // entry, whose module graph is several times the size of the io entry.
0027 /// <reference types="jsroot" />
0028 import { openFile } from 'jsroot/io';
0029 import { treeProcess, TSelector } from 'jsroot/tree';
0030
0031 /** Anything JSROOT's `openFile` accepts. See the class doc for the list. */
0032 export type RootSource = string | File | Blob | ArrayBuffer | object;
0033
0034 /** One column of values as JSROOT returns it (typed array, or Array for 64-bit ints). */
0035 export type Column = ArrayLike<number> & Iterable<number>;
0036
0037 /** The columns read by one `readEntry` call, keyed by full branch name. */
0038 export type ColumnBag = Record<string, Column>;
0039
0040 /** The podio data model of an 'events' tree. */
0041 export type PodioModel = 'edm4eic' | 'edm4hep';
0042
0043 const EDM4EIC_HIT_TYPE = 'vector<edm4eic::TrackerHitData>';
0044 const EDM4HEP_SIM_HIT_TYPE = 'vector<edm4hep::SimTrackerHitData>';
0045
0046 interface JsrootBranch {
0047 fName: string;
0048 fClassName?: string;
0049 fBranches?: { arr?: JsrootBranch[] };
0050 }
0051
0052 interface JsrootTree {
0053 fEntries: number;
0054 fBranches?: { arr?: JsrootBranch[] };
0055 }
0056
0057 interface JsrootFile {
0058 readObject(name: string): Promise<unknown>;
0059 getFileName?(): string;
0060 }
0061
0062 /**
0063 * An opened podio 'events' tree, kept open across conversions so the tree
0064 * metadata is read once.
0065 */
0066 export class PodioEventFile {
0067 /** Full names of every branch and sub-branch, e.g. 'SiBarrelHits.position.x'. */
0068 private readonly branchNames = new Set<string>();
0069 /** Top-level collection name -> its podio C++ type. */
0070 private readonly collectionTypes = new Map<string, string>();
0071
0072 private constructor(
0073 private readonly file: JsrootFile,
0074 private readonly tree: JsrootTree,
0075 readonly sourceName: string,
0076 ) {
0077 for (const branch of tree.fBranches?.arr ?? []) {
0078 this.collectionTypes.set(branch.fName, branch.fClassName ?? '');
0079 this.collectBranchNames(branch);
0080 }
0081 }
0082
0083 private collectBranchNames(branch: JsrootBranch): void {
0084 this.branchNames.add(branch.fName);
0085 for (const child of branch.fBranches?.arr ?? []) {
0086 this.collectBranchNames(child);
0087 }
0088 }
0089
0090 /** Opens `source` and reads the 'events' tree metadata (no event data yet). */
0091 static async open(source: RootSource, treeName = 'events'): Promise<PodioEventFile> {
0092 const file = (await openFile(source as never)) as JsrootFile;
0093 // JSROOT rejects a missing key with a bare "Key not found events"; the
0094 // message goes straight to the user, so say what was actually wrong
0095 const tree = (await file
0096 .readObject(treeName)
0097 .catch(() => null)) as JsrootTree | null;
0098 if (!tree || typeof tree.fEntries !== 'number') {
0099 throw new Error(
0100 `No '${treeName}' TTree found in the ROOT file - this does not look like an ` +
0101 `EDM4eic/EDM4hep event file (a detector geometry file, for instance, has none)`,
0102 );
0103 }
0104 const name =
0105 typeof source === 'string'
0106 ? source
0107 : ((source as File).name ?? file.getFileName?.() ?? 'unknown.root');
0108 return new PodioEventFile(file, tree, name);
0109 }
0110
0111 /** Number of entries (events) in the tree. */
0112 get entryCount(): number {
0113 return this.tree.fEntries;
0114 }
0115
0116 /** True when a branch or sub-branch with this exact full name exists. */
0117 hasBranch(name: string): boolean {
0118 return this.branchNames.has(name);
0119 }
0120
0121 /** Top-level collection names whose podio C++ type is exactly `typeName`. */
0122 collectionsOfType(typeName: string): string[] {
0123 const names: string[] = [];
0124 for (const [name, type] of this.collectionTypes) {
0125 if (type === typeName) names.push(name);
0126 }
0127 return names;
0128 }
0129
0130 /**
0131 * Detects the data model by branch types, the same rule pyrobird uses:
0132 * reconstructed edm4eic collections win over the sim hits that eicrecon
0133 * files also carry.
0134 */
0135 detectModel(): PodioModel {
0136 if (this.collectionsOfType(EDM4EIC_HIT_TYPE).length > 0) return 'edm4eic';
0137 if (this.collectionsOfType(EDM4HEP_SIM_HIT_TYPE).length > 0) return 'edm4hep';
0138 throw new Error(
0139 `Cannot detect file type: no '${EDM4EIC_HIT_TYPE}' or '${EDM4HEP_SIM_HIT_TYPE}' ` +
0140 `branches found in the 'events' tree`,
0141 );
0142 }
0143
0144 /**
0145 * Reads the given branches for ONE entry in a single pass.
0146 *
0147 * Names that do not exist in the tree are dropped silently - callers ask for
0148 * optional fields (edm4hep renamed 'EDep' to 'eDep', relation branches come
0149 * and go between podio versions) and check the result. Every returned column
0150 * holds the values of that one entry, flattened out of the per-event vector.
0151 */
0152 async readEntry(names: Iterable<string>, entry: number): Promise<ColumnBag> {
0153 const wanted = [...new Set(names)].filter(name => this.hasBranch(name));
0154 const bag: ColumnBag = {};
0155 if (wanted.length === 0) return bag;
0156
0157 if (!Number.isInteger(entry) || entry < 0 || entry >= this.entryCount) {
0158 throw new RangeError(
0159 `Entry ${entry} is outside of the tree: it holds ${this.entryCount} entries`,
0160 );
0161 }
0162
0163 const selector = new TSelector();
0164 // Read into slot names ('b0', 'b1', ...): JSROOT would otherwise treat the
0165 // dots in 'SiBarrelHits.position.x' as a member path in the target object.
0166 wanted.forEach((name, index) => selector.addBranch(name, `b${index}`));
0167 selector.Process = function (this: { tgtobj: Record<string, Column> }) {
0168 wanted.forEach((name, index) => {
0169 bag[name] = this.tgtobj[`b${index}`];
0170 });
0171 };
0172
0173 await treeProcess(this.tree, selector, { firstentry: entry, numentries: 1 });
0174 return bag;
0175 }
0176
0177 /**
0178 * Reads the podio collectionID of `collectionName` from the file's
0179 * 'podio_metadata' tree, or null when the metadata is unavailable (trimmed
0180 * test files have none).
0181 */
0182 async readCollectionId(collectionName: string): Promise<number | null> {
0183 try {
0184 const metadata = (await this.file.readObject('podio_metadata')) as JsrootTree | null;
0185 if (!metadata) return null;
0186 const base = 'events___CollectionTypeInfo';
0187 const selector = new TSelector();
0188 selector.addBranch(`${base}.collectionID`, 'ids');
0189 selector.addBranch(`${base}.name`, 'names');
0190 let ids: ArrayLike<number> = [];
0191 let names: ArrayLike<string> = [];
0192 selector.Process = function (this: { tgtobj: Record<string, unknown> }) {
0193 ids = this.tgtobj['ids'] as ArrayLike<number>;
0194 names = this.tgtobj['names'] as ArrayLike<string>;
0195 };
0196 await treeProcess(metadata, selector, { firstentry: 0, numentries: 1 });
0197 for (let i = 0; i < names.length; i++) {
0198 if (names[i] === collectionName) return Number(ids[i]);
0199 }
0200 } catch {
0201 // No podio metadata in this file - the caller skips the consistency check
0202 }
0203 return null;
0204 }
0205 }
0206
0207 /** Reads a column out of a bag, or an empty array when the branch was absent. */
0208 export function column(bag: ColumnBag, name: string): Column {
0209 return bag[name] ?? [];
0210 }
0211
0212 /** Reads a column as a plain number array (JSROOT hands back typed arrays). */
0213 export function numbers(bag: ColumnBag, name: string): number[] {
0214 const values = bag[name];
0215 return values ? Array.from(values, Number) : [];
0216 }