// Checks the JS-side .seq parser + waveform reconstruction against the // MATLAB-golden files in test-data/golden (runs the .ts sources directly — // Node >= 22.6 with type stripping). import fs from 'node:fs' import path from 'node:path' import { fileURLToPath } from 'node:url' import { parseSeq, decompressShape } from '../src/seq/parseSeq.ts' import { reconstruct } from '../src/seq/reconstruct.ts' const here = path.dirname(fileURLToPath(import.meta.url)) const goldenDir = path.join(here, '..', 'test-data', 'golden') let failures = 0 function check(cond, label) { if (cond) console.log(` ok: ${label}`) else { console.error(` FAIL: ${label}`) failures++ } } // ── decompressShape unit check ────────────────────────────────────────── // w = [0 0 0 1 2 3 3 3]; deriv = [0 0 0 1 1 1 0 0]; RLE: 0,0,1 1,1,1 0,0,0 { const w = decompressShape(8, [0, 0, 1, 1, 1, 1, 0, 0, 0]) check( JSON.stringify([...w]) === JSON.stringify([0, 0, 0, 1, 2, 3, 3, 3]), 'decompressShape RLE round-trip', ) } // ── golden files ──────────────────────────────────────────────────────── const expectations = { 'fid.seq': { blocks: 32, rfPulses: 16, adcWindows: 16, adcSamples: 16 * 4096 }, 'gre.seq': { blocks: 640, rfPulses: 128, adcWindows: 128, adcSamples: 128 * 128 }, 'seq1.seq': {}, 'seq2.seq': {}, 'seq3.seq': {}, } for (const [name, expect] of Object.entries(expectations)) { const text = fs.readFileSync(path.join(goldenDir, name), 'utf8') console.log(`${name}:`) const seq = parseSeq(text) check(seq.version.major === 1 && seq.version.minor === 5, 'version 1.5.x') check(seq.blocks.length > 0, `has blocks (${seq.blocks.length})`) if (seq.signature) check(seq.signature.valid === true, 'md5 signature verifies') // Every referenced event must resolve let dangling = 0 for (const b of seq.blocks) { if (b.rfId !== 0 && !seq.rf.has(b.rfId)) dangling++ for (const gid of [b.gxId, b.gyId, b.gzId]) { if (gid !== 0 && !seq.grads.has(gid)) dangling++ } if (b.adcId !== 0 && !seq.adcs.has(b.adcId)) dangling++ if (b.extId !== 0 && !seq.extensions.has(b.extId)) dangling++ } check(dangling === 0, 'all block event references resolve') const rec = reconstruct(seq) const totalDef = seq.definitions.get('TotalDuration') if (Array.isArray(totalDef)) { check(Math.abs(rec.duration - totalDef[0]) < 1e-9, `duration matches TotalDuration (${rec.duration})`) } for (const s of [rec.gx, rec.gy, rec.gz, rec.rfMag]) { let monotone = true for (let i = 1; i < s.t.length; i++) { if (s.t[i] < s.t[i - 1] - 1e-12) monotone = false } check(monotone, 'series times monotone') } if (expect.blocks !== undefined) check(seq.blocks.length === expect.blocks, `${expect.blocks} blocks`) if (expect.rfPulses !== undefined) check(rec.rfSpans.length === expect.rfPulses, `${expect.rfPulses} RF pulses`) if (expect.adcWindows !== undefined) check(rec.adcSpans.length === expect.adcWindows, `${expect.adcWindows} ADC windows`) if (expect.adcSamples !== undefined) check(rec.stats.adcSamples === expect.adcSamples, `${expect.adcSamples} ADC samples`) } // Tampering must invalidate the signature { const text = fs.readFileSync(path.join(goldenDir, 'gre.seq'), 'utf8') const tampered = text.replace('FOV 0.256', 'FOV 0.255') const seq = parseSeq(tampered) check(seq.signature?.valid === false, 'tampered file fails signature check') } if (failures > 0) { console.error(`\n${failures} check(s) FAILED`) process.exit(1) } console.log('\nPASS: all parser checks passed')