Skip to content

Create scans in an archive, and drive one from a parsed PDF - #7

Merged
bbfrederick merged 2 commits into
mainfrom
exar1-generate
Aug 28, 2026
Merged

Create scans in an archive, and drive one from a parsed PDF#7
bbfrederick merged 2 commits into
mainfrom
exar1-generate

Conversation

@bbfrederick

Copy link
Copy Markdown
Owner

Two commits: creating scans in an archive, and the driver that writes a parsed
PDF into one.

Creating scans

A scanner accepts archives built this way -- four added scans loaded, kept
their running order, and the one carrying an edit kept its own protocol, 19
distinct protocols in and 19 out. Generation is not blocked by the format.

Getting there cost two defects, and both are why validate.py exists.

Rejected outright. EdfProgramContent describes the running order in
five maps keyed by step id -- LinksFrom, LinksTo, Ranks,
RelationsFrom, RelationsTo -- and the first attempt updated one. A step
present in only some leaves the console unable to build the program, which it
reports by showing the folder tree with no protocols in it. That is
indistinguishable from an archive exported off an empty folder node, which is
how it was first misread.

Loaded, and silently wrong. A protocol and a label each carry
ParentElementId pointing at their own step, and the console resolves a
step's protocol through that reverse link rather than through the step's
Children blob. Copies keeping the source's pointer were served the source's
protocol. Three of four duplicates looked perfect, because they duplicate
content identical to their source by design; only the copy carrying a
deliberate edit could show it, returning its source's TR. A generated object
must differ from its template in some observable way or the test cannot tell
"created correctly" from "aliased to the original".

Both archives were internally coherent throughout -- every reference resolved,
every id unique. So validate.problems() states rules as relationships that
hold in every console-authored archive and re-derives them from the archive
under test. Four negative tests reintroduce each defect and require it to be
reported, because a checker that never fires carries the appearance of
assurance without the fact.

Writing those found a flaw in the checker itself: "does the chain cover every
step" compared the chain against archive.steps, which is derived by walking
the chain
, so a chain stopping early agreed with itself. It now counts
EdfMeasurementStep instances independently.

The driver

siemens-protocol-tool exar <template.exar1> <protocol.pdf> --out built.exar1

The manifest is as much the point as the output. Roughly a tenth of what a
protocol prints has a verified mapping, so a built archive is mostly the
template it started from. The report states that fraction, counts inherited
values, and names the unmapped parameters by frequency -- which is what says
where the next mapping is worth deriving.

Driving the unmodified archive from the changed protocol's PDF lands on the
console's own values in all 1353 mapped fields.

Driving an archive from its own PDF must write nothing, and that one check
exercises units, scales, the derived basis, sparse arrays and change detection
together. It caught two defects: change detection compared the displayed
value, which the Special card does not have, so every ASCCONV-only mapping
counted as a write and rewrote content that had not moved; and a printed
0.00 against an assignment a sparse array omits was reported as changing to
0.0, when writing zero into a sparse array in fact removes the line.

patch.resolve now falls back to the label Preview prints. A multi-echo scan
prints TE 1 where a single-echo one prints TE, so the driver had been
silently skipping TE on exactly the scans that print it differently -- 2 fields
out of 1353, the kind of miss that reads as a rounding difference.

Scans are matched to the template by name and an unmatched one is reported
rather than guessed at: the PDF names a sequence by kernel (epfid) and the
archive by sequence file (cmrr_mbep2d_bold), so a donor to copy cannot be
chosen reliably. generate.duplicate_step is there for a caller who knows
which scan to copy; the driver will not choose one.

The GUI exposes the command through the single description in
gui/commands.py, as its own test requires.

Verification

1757 passed, 2 skipped
black / isort / codespell     clean

Not yet done

No archive built end to end by exar has been through a loader. The parts are
individually validated -- patched values on five archives, created scans on
one -- but the combination has not been. That is the next cheap test.

Coverage is 10%: 218 of the 2276 parameters an 18-scan protocol prints. The
unmapped list is dominated by console UI settings rather than acquisition
parameters, which is reassuring and worth confirming before deriving more.

🤖 Generated with Claude Code

bbfrederick and others added 2 commits August 28, 2026 15:27
A scanner accepts archives built this way: four added scans loaded, kept
their running order, and the one carrying an edit kept its own protocol --
19 distinct protocols in and 19 out. Generation is not blocked by the
format. Getting there cost two defects, and both are the reason
validate.py exists rather than a comment.

The first was rejected outright. EdfProgramContent describes the running
order in five maps keyed by step id -- LinksFrom, LinksTo, Ranks,
RelationsFrom, RelationsTo -- and the first attempt updated one. A step
present in only some leaves the console unable to build the program, which
it reports by showing the folder tree with no protocols in it. That is
indistinguishable from an archive exported off an empty folder node, which
is how it was first misread.

The second loaded and was silently wrong. A protocol and a label each
carry ParentElementId pointing at their own step, and the console resolves
a step's protocol through that reverse link rather than through the step's
Children blob. Copies that kept the source's pointer were served the
source's protocol. Three of the four duplicates looked perfect, because
they duplicate content identical to their source by design; only the copy
carrying a deliberate edit could show it, returning its source's TR. A
generated object must differ from its template in some observable way or
the test cannot tell "created correctly" from "aliased to the original".

Both archives were internally coherent throughout: every reference
resolved, every id was unique. So the checks in validate.py are stated as
relationships that hold in every console-authored archive and re-derived
from the archive under test, not as self-consistency. Four negative tests
reintroduce each defect and require it to be reported, because a checker
that never fires carries the appearance of assurance without the fact.

Writing those found a flaw in the checker itself: "does the chain cover
every step" compared the chain against archive.steps, which is derived by
walking the chain. A chain stopping early agreed with itself. It now
counts EdfMeasurementStep instances independently. A validator built from
the same model as the thing it validates inherits its blind spots, which
is how the original defects got past.

Also: Instance.Tags on a protocol carries #ContentHash|<sha1 of the Data
string>, which is neither the ContentHash column nor the stored blob's
hash, and matches all 18 protocols in the reference archive.
replace_content recomputes it. A stale one is tolerated on load -- two
scans in the NAV option scan shared one and both kept their values -- but
it is derivable and describes the content.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Adds `siemens-protocol-tool exar <archive> <pdf>`: take a template .exar1,
write every parameter with a verified mapping, and report the rest.

The manifest is as much the point as the output. Roughly a tenth of what a
protocol prints is mapped, so a built archive is mostly the template it
started from. A tool reporting only its successes would describe a small
part of the result and imply the whole, so the report states the fraction,
counts inherited values, and names the unmapped parameters by frequency --
which is what says where the next mapping is worth deriving rather than
guessing at one.

Driving an archive from its *own* PDF must write nothing, and that single
check exercises units, scales, the derived basis, sparse arrays and change
detection together. It caught two defects. Change detection compared the
displayed value, which the Special card does not have -- every ASCCONV-only
mapping therefore counted as a write and rewrote content that had not
moved. And a printed "0.00" against an assignment a sparse array omits was
reported as a change to "0.0" when writing zero into a sparse array in fact
removes the line; records now say what is actually stored, for which
"(absent)" is a value rather than a gap.

patch.resolve now falls back to the label Preview prints. A multi-echo scan
prints "TE 1" where a single-echo one prints "TE", and the preview entry is
labelled the same way, so resolving through it follows the printout instead
of duplicating every spelling in the table. Without it the driver silently
skipped TE on exactly the scans that print it differently -- 2 fields out of
1353 wrong, which is the kind of miss that reads as a rounding difference.
With it, driving the unmodified archive from the changed protocol's PDF
lands on the console's own values in all 1353 mapped fields.

Scans are matched to the template by name, and an unmatched one is reported
rather than guessed at: the PDF names a sequence by kernel and the archive
by sequence file, so there is no reliable way to choose a donor to copy.
generate.duplicate_step is there for a caller who knows which scan to copy;
the driver will not choose one.

The GUI exposes the command through the one description in gui/commands.py,
as its own test requires.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@bbfrederick
bbfrederick merged commit f5133c1 into main Aug 28, 2026
8 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