1// Checks the JS-side .seq parser + waveform reconstruction against the
2// MATLAB-golden files in test-data/golden (runs the .ts sources directly —
3// Node >= 22.6 with type stripping).
4import fs from 'node:fs'
5import path from 'node:path'
6import { fileURLToPath } from 'node:url'
7import { parseSeq, decompressShape } from '../src/seq/parseSeq.ts'
8import { reconstruct } from '../src/seq/reconstruct.ts'
10const here = path.dirname(fileURLToPath(import.meta.url))
11const goldenDir = path.join(here, '..', 'test-data', 'golden')
13let failures = 0
14function check(cond, label) {
15 if (cond) console.log(` ok: ${label}`)
16 else {
17 console.error(` FAIL: ${label}`)
18 failures++
19 }
20}
22// ── decompressShape unit check ──────────────────────────────────────────
23// 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
24{
25 const w = decompressShape(8, [0, 0, 1, 1, 1, 1, 0, 0, 0])
26 check(
27 JSON.stringify([...w]) === JSON.stringify([0, 0, 0, 1, 2, 3, 3, 3]),
28 'decompressShape RLE round-trip',
29 )
30}
32// ── golden files ────────────────────────────────────────────────────────
33const expectations = {
34 'fid.seq': { blocks: 32, rfPulses: 16, adcWindows: 16, adcSamples: 16 * 4096 },
35 'gre.seq': { blocks: 640, rfPulses: 128, adcWindows: 128, adcSamples: 128 * 128 },
36 'seq1.seq': {},
37 'seq2.seq': {},
38 'seq3.seq': {},
39}
41for (const [name, expect] of Object.entries(expectations)) {
42 const text = fs.readFileSync(path.join(goldenDir, name), 'utf8')
43 console.log(`${name}:`)
44 const seq = parseSeq(text)
45 check(seq.version.major === 1 && seq.version.minor === 5, 'version 1.5.x')
46 check(seq.blocks.length > 0, `has blocks (${seq.blocks.length})`)
47 if (seq.signature) check(seq.signature.valid === true, 'md5 signature verifies')
49 // Every referenced event must resolve
50 let dangling = 0
51 for (const b of seq.blocks) {
52 if (b.rfId !== 0 && !seq.rf.has(b.rfId)) dangling++
53 for (const gid of [b.gxId, b.gyId, b.gzId]) {
54 if (gid !== 0 && !seq.grads.has(gid)) dangling++
55 }
56 if (b.adcId !== 0 && !seq.adcs.has(b.adcId)) dangling++
57 if (b.extId !== 0 && !seq.extensions.has(b.extId)) dangling++
58 }
59 check(dangling === 0, 'all block event references resolve')
61 const rec = reconstruct(seq)
62 const totalDef = seq.definitions.get('TotalDuration')
63 if (Array.isArray(totalDef)) {
64 check(Math.abs(rec.duration - totalDef[0]) < 1e-9, `duration matches TotalDuration (${rec.duration})`)
65 }
66 for (const s of [rec.gx, rec.gy, rec.gz, rec.rfMag]) {
67 let monotone = true
68 for (let i = 1; i < s.t.length; i++) {
69 if (s.t[i] < s.t[i - 1] - 1e-12) monotone = false
70 }
71 check(monotone, 'series times monotone')
72 }
73 if (expect.blocks !== undefined) check(seq.blocks.length === expect.blocks, `${expect.blocks} blocks`)
74 if (expect.rfPulses !== undefined) check(rec.rfSpans.length === expect.rfPulses, `${expect.rfPulses} RF pulses`)
75 if (expect.adcWindows !== undefined) check(rec.adcSpans.length === expect.adcWindows, `${expect.adcWindows} ADC windows`)
76 if (expect.adcSamples !== undefined) check(rec.stats.adcSamples === expect.adcSamples, `${expect.adcSamples} ADC samples`)
77}
79// Tampering must invalidate the signature
80{
81 const text = fs.readFileSync(path.join(goldenDir, 'gre.seq'), 'utf8')
82 const tampered = text.replace('FOV 0.256', 'FOV 0.255')
83 const seq = parseSeq(tampered)
84 check(seq.signature?.valid === false, 'tampered file fails signature check')
85}
87if (failures > 0) {
88 console.error(`\n${failures} check(s) FAILED`)
89 process.exit(1)
90}
91console.log('\nPASS: all parser checks passed')