Skip to content

Drive an archive from a printout, and check it on a scanner - #16

Merged
bbfrederick merged 3 commits into
mainfrom
exar1-driver
Sep 4, 2026
Merged

Drive an archive from a printout, and check it on a scanner#16
bbfrederick merged 3 commits into
mainfrom
exar1-driver

Conversation

@bbfrederick

Copy link
Copy Markdown
Owner

Three commits taking the exar driver from "produces a file" to "produces a file a scanner accepts", and fixing what the scanner's return exposed.

The driver's output has now been loaded

examples/XA60/driver_loadtest.{exar1,pdf} is Potpourri_P1 driven by the Potpourri_P1_changed printout, loaded on an XA60 scanner and re-exported. 18 scans in, 18 out, running order preserved, program still named DRIVER_TEST, nothing greyed out.

That is what the answer-key comparison could not establish. Only ~30% of what a protocol prints has a verified mapping, so the result is a hybrid — 226 fields from the printout beside 173 inherited from the template — and a hybrid of two consistent parameter sets is not itself consistent. For this protocol it is.

ASCCONV fields the driver wrote 226
...returned byte-identical 226
...that genuinely moved off the template 226
Printed parameters the console changed 87
...reproduced in our build 80 (92%)

insert_ascconv is validated on a scanner for the first time: sWipMemBlock.adFree[3] is absent from the template, was created between its siblings, and came back in place with the console's spelling.

And it exposed a defect that field-by-field agreement could not

Slice Thickness went 2.3 → 2.2 across all 64 elements of a CMRR EPI while sPosition kept describing the 2.3 mm geometry — the outermost slices (64-1)/2 × 0.1 = 3.15 mm from where they belong. The console recomputes all 64 on its own edit; we did not.

The comparison above agreed on all 226 written fields and could never have found this, because the defect is in fields nobody wrote: correct in the template, correct in what we sent, correct in what came back, and wrong relative to the thickness beside them. The corpus sweep found it, which is the argument for shipping the return as an example.

The scanner did not catch it either. The scan loaded, was not greyed out, printed no complaint, and returned the array untouched. So the console's consistency check does not cover the slice array against its group inputs — Include Nav. = Off gets refused, an incoherent 64-slice group does not. "It loaded" is a weaker signal than the greying-out rule suggests.

build.recentre rebuilds an array this write invalidated, leaves one that arrived broken alone, and skips multi-group arrays. Against the same answer key: 201 fields exact, up from 158.

The return keeps its bad array, pinned as KNOWN_INCONSISTENT_ARRAYS rather than excluded, so a second one fails.

Reading the direction letter

A Siemens printout gives a coordinate as a magnitude beside a direction letter — F32 for a stored -32 — and on a two-column card the letter lands in a field of its own. Table Position was reading the number alone, which flipped the sign on every scan whose position is negative. Mapping.sign_from now reads the letter: H against non-negative, F against negative, on all 300 corpus comparisons, and a letter that is neither is refused rather than defaulted.

Also fixed: expand searched for the mapped field across array indices instead of asking which elements exist, so a sparse member like dDistFact was refused with a reason that was not true; and lRepetitions was not marked sparse, making Measurements unwritable on 585 scans. Both surfaced from the manifest reporting refusals.

Coverage the pair adds

With its printout beside it, driver_loadtest joins two corpus sweeps:

  • Self-drive — 18 scans matched, 0 values written. First time that invariant runs against a protocol this package wrote rather than one it only read.
  • Flag bits — 50 comparisons against a card the scanner printed from what we stored. Every prior comparison decoded a console's word.

One actionable gap named

Averaging is mapped for tfl_mgh_multiecho and printed by tfl_mgh_epinav_ABCD, so the driver refuses it — correctly, since a sWipMemBlock index means whatever its own sequence says. Decoding alFree[4] with the same table nonetheless agrees on 37 of 37 corpus scans across both sequences. That is evidence of the wrong kind: printout-against-stored tests the decoder, since the console displays what it stores. Settling it needs an option scan varying Averaging on the second sequence.

BuildReport.out_of_scope now names that class instead of burying it among the forty parameters nothing has looked at. build.covered_elsewhere asks the mapping table, not resolve's prose, so rewording a reason cannot reclassify anything.

Verification

  • 2486 passed, 19 skipped on the full suite
  • black, isort, codespell clean
  • New tests: driver round-trip (writes survived; scanner changed nothing unexplained), driver geometry consistency with a mutation check, recentre's three branches, and the manifest classification

🤖 Generated with Claude Code

bbfrederick and others added 3 commits September 3, 2026 15:29
The capabilities archive was rebuilt as CAPABILITIES_TEST and came back as
CAPABILITIES_TEST, so generate.rename does what it claims where it counts.

The rest of the round trip is worth recording as much as the name. All 54
scans returned in their original order, all 25 edits held, all ten links came
back with the same source, target and group, and their eleven copies were
still byte-identical. 44 of the 54 protocols were byte-identical in ASCCONV;
nine differed only in the derived scan times Manifest.stale already names.

rslh_ep3d_vaso reproduced the same three coupled formulas to the digit --
alTI[0] = 24*TR + 10320, alTI[1] = 72*TR + 10320, alTR[1] = 96*TR + 20640.
This was a rebuild with every GUID regenerated rather than a re-export of the
file already loaded, so the console derived them again from scratch: two
independent passes agreeing, not one observation repeated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Driving an archive from its own PDF must write nothing. That check has been
here a while on one archive, and it is the strongest one available offline --
units, scales, the derived basis, sparse arrays and change detection at once.
Run across every console-authored pair it caught Table Position writing a
sign flip on the nineteen scans holding -32.

The first diagnosis was wrong in an instructive way. No export prints a
negative table position across 300 comparisons, so the printed value looked
like a magnitude with the sign unrecoverable, and the mapping looked like it
had to go. It does not: a Siemens printout gives a coordinate as a magnitude
beside a direction letter -- F32 for a protocol holding -32 -- and on a
two-column card the letter lands in a field of its own, which the parser
names "Table Position #2". The number alone is a magnitude because the sign
is in the letter, not because it is missing. Supplied by the user; nothing in
the corpus would have said so, since reading the number and reading the pair
agree wherever the value is positive.

Mapping.sign_from names that companion field. L, P, F and I are the negative
halves of the three axes and R, A, S and H the positive ones; a letter that
is neither is refused rather than read as positive by default, since the
alternative is a silent flip on a release spelling one differently. H against
a non-negative stored value and F against a negative one, on all 300
comparisons, none against.

Five pairs remain named exceptions where printout and storage legitimately
disagree -- four scanner returns, where an off-grid value is stored
faithfully and displayed snapped, and the 31P export, whose spectroscopy scan
prints 400 under FOV Phase where the stored ratio is 100% of a 400 mm read
FOV. A second test requires each to still write something, so an exception
that stops being one is removed rather than left hiding the next offender.

Two refusals in the driver's manifest were untrue rather than honest. expand
searched for the mapped field across array indices, so a sparse member like
sGroupArray.asGroup[0].dDistFact resolved to nothing while its group sat
right there. And lRepetitions is sparse -- absent on 585 corpus scans, which
is one measurement -- so Measurements was unwritable on every one of them.

Coverage goes from a tenth of printed parameters to about 30%, and the
driver's output now has an answer key: driving Potpourri_P1 with the
Potpourri_P1_changed printout against the console's own edit of the same
protocol gives 158 fields matching exactly, 173 left alone as unmapped, and 0
wrong. The 68 that differ are the two documented classes -- 67 dPhaseFOV
within 0.083 mm of the console's quantised ratio, and one alFree[0] differing
in bit 29, which no mapping claims.

Also folds in the confirmation that the renamed protocol round-tripped:
CAPABILITIES_TEST arrived and returned under its own name, 54 scans, 25 edits
and 10 links intact, with rslh_ep3d_vaso reproducing the same three coupled
formulas on a rebuild with every GUID regenerated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A scanner loaded the first archive built end to end by the exar driver and
returned it. All 18 scans loaded, none greyed out, and all 226 ASCCONV fields
the driver wrote came back byte-identical -- and that comparison could never
have found what was actually wrong, because the defect was in fields nobody
wrote.

Slice Thickness went 2.3 to 2.2 across all 64 elements of a CMRR EPI while
sPosition kept describing the 2.3 mm geometry, putting the outermost slices
(64 - 1) / 2 * 0.1 = 3.15 mm from where they belong. The console recomputes
all 64 on its own edit of the same protocol; we did not. The scanner accepted
the scan anyway, printed no complaint and returned the array untouched, so its
consistency check does not cover the slice array against its group inputs --
which makes "it loaded" a weaker signal than the greying-out rule suggests.

build.recentre rebuilds an array that this write invalidated, leaves one that
arrived broken alone, and skips multi-group arrays that read_group declines to
describe. Against the same answer key that is 201 fields exact where it was
158: 43 slice positions became right, and 85 moved out of the inherited column
to carry the template's group centre visibly rather than silently, since the
printed Position is still unmapped.

The return ships as examples/XA60/driver_loadtest.{exar1,pdf} with the bad
array intact, pinned as KNOWN_INCONSISTENT_ARRAYS rather than excluded, so a
second one fails. With its printout beside it the pair joins the self-drive
sweep -- 18 scans matched, 0 values written, the first time that invariant runs
against a protocol this package wrote -- and adds 50 comparisons to the flag
sweep against a card the scanner printed from what we stored.

Measured at the printout, 80 of the 87 parameters the console changed were
reproduced. Six of the seven misses are Special-card parameters nothing maps;
Bandwidth is neither the target nor the template because the console derived
it from the base resolution and FOV we did change. The seventh is Averaging,
which is mapped for tfl_mgh_multiecho and refused on tfl_mgh_epinav_ABCD --
correctly, since a sWipMemBlock index means whatever its own sequence says.
BuildReport.out_of_scope now names that class instead of burying it among the
forty parameters nothing has ever looked at; covered_elsewhere asks the mapping
table rather than parsing resolve's prose.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@bbfrederick
bbfrederick merged commit af12090 into main Sep 4, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant