Warning, /firebird/packages/root2dex/src/convert.ts is written in an unsupported language. File is not indexed.
0001 /**
0002 * The public conversion entry points: open a podio ROOT file and turn selected
0003 * entries into a Firebird DEX document.
0004 *
0005 * `Root2DexConverter` keeps the file open so a UI can convert one event, then
0006 * another, paying the tree-metadata read only once; `convertRootToDex` is the
0007 * one-shot form used by scripts and tests.
0008 */
0009
0010 import { makeDex, type DexDocument, type DexEvent } from './dex';
0011 import { PodioEventFile, type PodioModel, type RootSource } from './podio-file';
0012 import {
0013 edm4eicEntryToDex,
0014 EDM4EIC_DEFAULT_COLLECTIONS,
0015 type Edm4eicCollection,
0016 } from './edm4eic';
0017 import { EDM4HEP_DEFAULT_COLLECTIONS, edm4hepEntryToDex, type Edm4hepOptions } from './edm4hep';
0018 import { parseEntryRanges, selectEntries, type EntryRange } from './entries';
0019
0020 export interface ConvertOptions extends Omit<Edm4hepOptions, 'collections'> {
0021 /**
0022 * Collection groups to convert. edm4eic knows 'tracker_hits', 'tracks' and
0023 * 'mc_particles'; edm4hep knows 'tracker_hits', 'mc_trajectories' and
0024 * 'mc_particles'. Empty means all.
0025 */
0026 collections?: string[];
0027 /** Overrides model auto-detection. */
0028 model?: PodioModel;
0029 /** Overrides the "file" field of the DEX origin block. */
0030 sourceName?: string;
0031 }
0032
0033 /**
0034 * Parses entry numbers written as '3', '1-5', or '1,2-5,8' into a list of
0035 * integers. Arrays of numbers pass through.
0036 *
0037 * This expands without bounds; to convert from a file, select with
0038 * `parseEntryRanges()` and `selectEntries()` first, which reject oversized
0039 * selections and clamp the ranges to the file before expanding.
0040 */
0041 export function parseEntryNumbers(value: string | number | Iterable<number>): number[] {
0042 if (typeof value === 'number') return [Math.trunc(value)];
0043 if (typeof value !== 'string') return [...value].map(v => Math.trunc(v));
0044 const entries: number[] = [];
0045 for (const [start, end] of parseEntryRanges(value)) {
0046 for (let entry = start; entry <= end; entry++) entries.push(entry);
0047 }
0048 return entries;
0049 }
0050
0051 /**
0052 * The collection groups a model's conversion knows, in conversion order. This
0053 * is what a UI offers as per-group conversion checkboxes; the names match
0054 * `pyrobird convert --collections` and the convert endpoint's `collections`
0055 * query parameter.
0056 */
0057 export function collectionGroupsFor(model: PodioModel): string[] {
0058 return model === 'edm4hep'
0059 ? [...EDM4HEP_DEFAULT_COLLECTIONS]
0060 : [...EDM4EIC_DEFAULT_COLLECTIONS];
0061 }
0062
0063 /**
0064 * An open podio ROOT file that converts entries on demand.
0065 *
0066 * Only the bytes of the requested entry are read - see PodioEventFile for how
0067 * that works and which sources are supported.
0068 */
0069 export class Root2DexConverter {
0070 private mcCollectionId: number | null | undefined;
0071
0072 private constructor(
0073 readonly file: PodioEventFile,
0074 readonly model: PodioModel,
0075 private readonly options: ConvertOptions,
0076 ) {}
0077
0078 static async open(source: RootSource, options: ConvertOptions = {}): Promise<Root2DexConverter> {
0079 const file = await PodioEventFile.open(source);
0080 const model = options.model ?? file.detectModel();
0081 return new Root2DexConverter(file, model, options);
0082 }
0083
0084 get entryCount(): number {
0085 return this.file.entryCount;
0086 }
0087
0088 get sourceName(): string {
0089 return this.options.sourceName ?? this.file.sourceName;
0090 }
0091
0092 /** Converts one entry to a DEX event object (no document wrapper). */
0093 async convertEntry(entry: number, overrides: ConvertOptions = {}): Promise<DexEvent> {
0094 const options = { ...this.options, ...overrides };
0095 if (this.model === 'edm4hep') {
0096 if (this.mcCollectionId === undefined) {
0097 this.mcCollectionId = await this.file.readCollectionId(options.mcBranch ?? 'MCParticles');
0098 }
0099 return edm4hepEntryToDex(this.file, entry, {
0100 ...options,
0101 collections: options.collections as Edm4hepOptions['collections'],
0102 mcCollectionId: this.mcCollectionId,
0103 });
0104 }
0105 return edm4eicEntryToDex(this.file, entry, {
0106 ...options,
0107 collections: options.collections as Edm4eicCollection[] | undefined,
0108 });
0109 }
0110
0111 /** Converts entries to a complete DEX document with an origin block. */
0112 async convert(entries: number[] | number, overrides: ConvertOptions = {}): Promise<DexDocument> {
0113 const list = typeof entries === 'number' ? [entries] : entries;
0114 const events: DexEvent[] = [];
0115 for (const entry of list) {
0116 events.push(await this.convertEntry(entry, overrides));
0117 }
0118 return makeDex(events, {
0119 file: overrides.sourceName ?? this.sourceName,
0120 entries_count: this.entryCount,
0121 file_type: this.model,
0122 });
0123 }
0124 }
0125
0126 /**
0127 * Opens `source`, converts `entries` and returns the DEX document. Equivalent
0128 * to `pyrobird convert <file> -e <entries>`: requested entries outside the
0129 * file are dropped before anything is converted, with one warning through
0130 * `options.onWarning` (console.warn without it).
0131 *
0132 * @throws EntrySelectionError when no requested entry is in the file.
0133 */
0134 export async function convertRootToDex(
0135 source: RootSource,
0136 entries: string | number | number[] = 0,
0137 options: ConvertOptions = {},
0138 ): Promise<DexDocument> {
0139 const converter = await Root2DexConverter.open(source, options);
0140 const ranges: EntryRange[] = typeof entries === 'string'
0141 ? parseEntryRanges(entries)
0142 : (typeof entries === 'number' ? [entries] : entries).map(entry => [Math.trunc(entry), Math.trunc(entry)] as const);
0143 const selection = selectEntries(ranges, converter.entryCount);
0144 if (selection.warning) {
0145 (options.onWarning ?? console.warn)(selection.warning);
0146 }
0147 return converter.convert(selection.entries);
0148 }