Warning, /firebird/packages/root2dex/src/parity.spec.ts is written in an unsupported language. File is not indexed.
0001 /**
0002 * The parity contract: for the same ROOT file and the same entries, this
0003 * package must produce the DEX document pyrobird produces - not approximately,
0004 * value for value. Both sides read the same IEEE754 bytes out of the file and
0005 * do the same arithmetic on them, so every number matches exactly.
0006 *
0007 * The reference documents in ../test-data were written by `pyrobird convert`;
0008 * see the README there for how to regenerate them. The ROOT inputs are
0009 * pyrobird's own test files - the same bytes both implementations read.
0010 */
0011
0012 import { describe, expect, it } from 'vitest';
0013 import { readFileSync, existsSync } from 'node:fs';
0014 import { resolve } from 'node:path';
0015 import { convertRootToDex, type ConvertOptions } from './convert';
0016 import type { DexDocument } from './dex';
0017
0018 const REPO = resolve(__dirname, '../../..');
0019 const ROOT_DATA = resolve(REPO, 'pyrobird/tests/unit_tests/data');
0020 const REFERENCE = resolve(__dirname, '../test-data');
0021
0022 /**
0023 * pyrobird's default trajectory exclusion list (DEFAULT_TRAJECTORY_EXCLUDED_COLLECTIONS
0024 * in pyrobird/edm4hep.py). root2dex excludes nothing unless told, so the
0025 * edm4hep case passes the list; without it the Cherenkov hits join the
0026 * MC-truth trajectories and the documents differ.
0027 */
0028 const PYROBIRD_TRAJECTORY_EXCLUDED_COLLECTIONS = ['DIRCBarHits', 'DRICHHits', 'PFRICHHits'];
0029
0030 const CASES: Array<{ label: string; root: string; reference: string; model: string; options: ConvertOptions }> = [
0031 {
0032 label: 'edm4eic (eicrecon reconstruction)',
0033 root: 'reco_2024-09_craterlake_2evt.edm4eic.root',
0034 reference: 'reco_2024-09_craterlake_2evt.pyrobird.json',
0035 model: 'edm4eic',
0036 options: {},
0037 },
0038 {
0039 label: 'edm4hep (ddsim simulation)',
0040 root: 'k_lambda_10x100_2evt.edm4hep.root',
0041 reference: 'k_lambda_10x100_2evt.pyrobird.json',
0042 model: 'edm4hep',
0043 options: { trajectoryExcludedCollections: PYROBIRD_TRAJECTORY_EXCLUDED_COLLECTIONS },
0044 },
0045 ];
0046
0047 describe('root2dex / pyrobird parity', () => {
0048 for (const testCase of CASES) {
0049 const rootPath = resolve(ROOT_DATA, testCase.root);
0050 const referencePath = resolve(REFERENCE, testCase.reference);
0051 const available = existsSync(rootPath) && existsSync(referencePath);
0052
0053 it.runIf(available)(`converts ${testCase.label} exactly like pyrobird`, async () => {
0054 const expected = JSON.parse(readFileSync(referencePath, 'utf8')) as DexDocument;
0055 // The JSON round trip drops undefined optional fields the same way
0056 // writing the document to a file would
0057 const actual = JSON.parse(
0058 JSON.stringify(await convertRootToDex(rootPath, '0-1', testCase.options)),
0059 ) as DexDocument;
0060
0061 expect(actual.type).toBe(expected.type);
0062 expect(actual.version).toBe(expected.version);
0063
0064 // origin.file is the path each side was invoked with, so only the parts
0065 // derived from the file itself are compared
0066 const origin = actual.origin as Record<string, unknown>;
0067 const expectedOrigin = expected.origin as Record<string, unknown>;
0068 expect(origin['entries_count']).toBe(expectedOrigin['entries_count']);
0069 expect(origin['file_type']).toBe(testCase.model);
0070 expect(expectedOrigin['file_type']).toBe(testCase.model);
0071
0072 expect(actual.events.length).toBe(expected.events.length);
0073 actual.events.forEach((event, e) => {
0074 const expectedEvent = expected.events[e];
0075 expect(event.id, `events[${e}].id`).toBe(expectedEvent.id);
0076 // Piece names first: a mismatch here reads far better than a diff of
0077 // two thousand-element column arrays
0078 expect(
0079 event.pieces.map(piece => `${piece.name}:${piece.type}:${piece.count}`),
0080 `events[${e}] pieces`,
0081 ).toEqual(expectedEvent.pieces.map(piece => `${piece.name}:${piece.type}:${piece.count}`));
0082 event.pieces.forEach((piece, p) => {
0083 expect(piece, `events[${e}].pieces[${p}] (${piece.name})`).toEqual(
0084 expectedEvent.pieces[p],
0085 );
0086 });
0087 });
0088 });
0089 }
0090 });