Repository navigation
Isocenter 1.0.0rc15
Pre-releaseExported output changes in this release in WFDB exports only, as the two entries' **Output:** lines name, where the details are. compare --base v1.0.0rc14 reports one new member of the reference cohort, synthetic:ecg_lead_codes, and nothing else; no existing member's output changes:
- #832: in a
.hea, the description field of a signal line, and inannotations.jsontheleadof a finding on that channel, change for three kinds of channel. A lead code with whitespace in front of it is written as the lead's name (2:1toI). AnMDC 2:3channel whose Code Meaning says Lead III is writtenIIIwhere it wasV1. A table-named channel whose name another signal of its record shares is written as its Code Value (IIto2:2), with one newWARNINGrow for that record, which grades the runREVIEW_REQUIREDwhere it gradedPASS. - #963: in
annotations.json, a finding loses itsleadkey when its mark names a channel past the record's sample columns. No row is written for it and the grade does not move.
One entry corrects the 1.0.0rc14 record and changes nothing in this release: ten of that release's **Output:** lines said less than it does (#764, #679, #926, #775, #721, #733, #722, #747, #703, #937). Each is restated as measured against 1.0.0rc13. Where the 1.0.0rc14 section's lead and one of those lines disagree, the line is the measured one.
The nine other entries' **Output:** lines say none (#804, #687, #956, #754, #753, #800, #779, #931, #935).
Changed
-
docs: the waveforms guide's quick start runs as part of the test suite, and no longer sits under
if __name__ == "__main__":(#804, owner ruling Q4 A).docs/waveforms.mdcarries<!-- tutorial: inputs=waveform_ecg.dcm -->, sotests/test_tutorials_run.pyruns it whole over the 12-lead ECG bundled with pydicom, as it runsdocs/tutorials/*.md. The page's directory tree, a```textblock nothing checked, is replaced by shown output: the pathexport()returns, the three files beside the header, and the twelve lead names read from the.heatext.- Why the guard left the code. The tutorial runner executes a page's fences in a namespace whose
__name__is__tutorial__. Measured onmainat bb5e6f1 with the header added and the tree removed: the quick start's one fence, underif __name__ == "__main__":, ran in 0.0 s, did nothing and raised nothing, although the folder it ingests did not exist. One true>>>line elsewhere on the page would have made it green with its quick start never run. The page now shows the code without the guard and says in prose to put it under one in a script, with the link the tutorials use. - The runner refuses a fence under the guard.
_shape_failuresgains one rule, with the others for code the runner would not run: a python fence holding anif __name__ == "__main__":test (either operand order, in a plain fence or a>>>example) is red,a fence under the main guard does not run here. It reads the fence withast, so a comment or a string that quotes the guard is not one, andif __name__ != "__main__":is not one either. No existing run page trips it: the six tutorials name the guard in prose only. - Cost. One more page per run of the runner: 2.7 s on 3.12 and 1.4 s on 3.14t, measured.
pytest --changedselects the runner for any.mdchange. - Output: none. Documentation and one test rule.
- Why the guard left the code. The tutorial runner executes a page's fences in a namespace whose
-
tests: a relative link or image in the documentation that reaches no file is red before merge (#687, owner ruling Q5 A).
tests/test_doc_anchors.pygainsbroken_file_links, beside the fragment check, andtest_every_documentation_file_link_resolvesover the tree. The tree has no such link today (0 broken, measured at bb5e6f1 and on this branch).- What was true before. The issue said nothing caught a broken relative file link.
mkdocs build --strictdoes (mkdocs 1.6.1, measured over a 17-link probe page: 9 warnings, build failed), but it runs only indocs.yml, on a tag or a dispatch, so a broken link failed the release's documentation deploy, after the tag was cut. It also passes five kinds that are dead on the published site: an absolute/page.md, a link to a pageexclude_docskeeps off the site, a directory-styledir/, and raw-HTML<a href>and<img src>. And it never readsREADME.md,CHANGELOG.mdorRELEASING.md. - The rules, each named in the failure message:
[absolute];[no such file]; from a page underdocs/,[leaves docs/: not on the site]and[excluded from the site](the directoriesexclude_docsnames, read frommkdocs.yml; a pattern the check cannot read stops it); and[a directory]. A root page is held to the first, second and last against the repository, as GitHub renders it. - A target is compared with its directory's own listing, component by component. macOS's filesystem is case-insensitive, so
Path("docs/Waveforms.md").exists()is true there and the link is dead on GitHub Pages. - No second rendering. Both checks read one cached parse of each page (
_parsed_pages), so the link check adds 0.05 s to the file's run (measured on 3.12 and 3.14t; the file's 25 tests take 4.4 s on 3.12, of which the one rendering of the tree is 3.4 s). A test pins oneconvertper page over both checks. A fixture therefore builds its whole site before its first call. - Not covered, and said in the module docstring: a link inside a
pymdownx.snippetsinclude or a tab body, the cross-references mkdocstrings generates,naventries, the fragment of a link into a file that is not a checked page, and external URLs, which are never fetched. - Output: none.
- What was true before. The issue said nothing caught a broken relative file link.
-
The released-sections test also compares each section with
origin/main's, andRELEASING.mdsays what to check aftermainis merged into a branch and what a pick does toCHANGELOG.md(#956). Git placed changelog entries wrongly, with no conflict, three times on 2026-10-07. No package code changes.- A merge of
main. #958's branch (d7e1cc9) merged the 1.0.0rc13 record-back (5f0c1ca). The merge was clean, and three### Fixedentries, 37 lines, landed at the end of[1.0.0rc13]; the developer moved them back by hand. Reproduced withgit mergeand withgit merge-fileover the three real files, which give the same bytes. - What the test of #927 did with it.
tests/test_released_changelog_sections_stay_as_released.pycompared each released section with the newest present tag that holds it. Over that merge it is red wherev1.0.0rc13is present, and where it is not it skips[1.0.0rc13]and passes: 45 passed, 1 skipped. The newest tag is the one a clone most often lacks, because release tags sit onrelease/X.Y, whichmaindoes not reach, sogit fetch origin mainbrings the record-back and never its tag. Whether the tag was present in that branch's clone at the time cannot be told now.RELEASING.mdalso never said to run the test after mergingmain. - Now each released section is also compared, by the same rule (byte-identical, or rewritten in place), with the same section of
refs/remotes/origin/main:CHANGELOG.md, which needs no tag. Over the same merge withv1.0.0rc13absent: 1 failed, naming[1.0.0rc13]with "106 lines where the release has 69" and the diff; over the hand-fixed merge (3f72c1d's file): 85 passed, 1 skipped. - What it cannot see, and skips by name: a section
origin/maindoes not hold (a release branch's own new section, a record-back's, or a ref not fetched since the record-back), and every section where the ref does not resolve (CI's depth-1 checkout of a tag, a remote of another name, the sdist). AtHEADequal toorigin/mainit compares the file with itself; what is onmainstays the tags' question, asked at the cut. The in-place allowance of #927 is unchanged, so an entry that rewrites an existing line is still not seen. RELEASING.md, "Changes land onmain", step 3: whenevermainis taken into a branch, and aftergit fetch origin,git diff --unified=0 origin/main -- CHANGELOG.mdmust start every hunk abovemain's first released heading, and the test must pass. The fetch comes first because a staleorigin/mainhides the misplacement: the same merge over anorigin/mainat bb5e6f1 is green, with[1.0.0rc13]skipped by name.RELEASING.md, "Later releases on an existing line", step 2: three of the four picks for 1.0.0rc14 (#967) applied theirCHANGELOG.mdhunks with no conflict onto a branch with no[Unreleased]heading, and before the last commit the file differed frommain's by 7 insertions and 56 deletions. The procedure now says such a pick is left as it is, because the last commit writes the file again frommain's whatever the picks left. It also says that a pick whose whole diff isCHANGELOG.mdand that conflicts is resolved tomain's file at that commit, where--ourswould leave it empty and an empty pick reads as a forward-port. The one such pick so far, #954's at rc13 (#955), had been resolved that way with no rule granting it; rc12's picks (#928) tookmain's file too, in picks that also carried code. By the owner's ruling of 2026-10-07 the pick is followed by its proof, pasted into the PR body and checked by the reviewer:git diff --stat <its sha> HEAD -- . ':!isocenter/_version.py' ':!CITATION.cff'prints nothing, or exactly the files the line's left-out list, its branch-only fixes and an earlier--oursaccount for. A picks PR still runs no tests of its own; the release commit's integration run is what tests the branch's file.- Output: none.
- A merge of
-
The policy fingerprint is pinned for input the loader never gives it, and over what the loader does give (#754, owner ruling Q4 A). A status is compared with
"v1:"plus the sha256 ofconfiguration._canonical_policy_v1, and that form is a store format from 1.0.0. Its tests pinned it over ordinary rules. Measured onmainat bb5e6f1, three edits of the form passed every test: a rule key whose value isNonedropped, a key that is not a string hashed with its type, and the tag lowercased inside the form. Each would have made a 1.x store's statuses read as another policy's, or two policies read as one. The form is unchanged; these are tests. The owner ruled that each property below stays as it is:value: nullis another policy than novalue. A file can say it: it loads as{'action': 'REPLACE', 'value': None},save()writes the line back, and the fingerprint is the same after a save and a reload. A file that gains or loses the line is a re-audit.- A
setvalue is hashed as its text, which is in hash order, so a policy holding one has a different fingerprint in each process, and a status recorded under it reads as another policy's in the next (it fails closed, with the #555 notice). Memory only: the loader refuses a set. The test runs the form in child processes under fixedPYTHONHASHSEEDs. - Rule keys
1and"1"are one key, because a key that is not a string is hashed as itsrepr; of two such entries in one rule the later one is kept. Memory only: the loader refuses a key that is not a string. - An upper-case tag assigned to
phi_tagsdirectly is a second rule and another policy.set_phi_tag("0008,103E", ...)and the loader lowercase; the form does not. - A
listand atupleare one value, andactionis not case-folded (KEEPandkeepare two policies; the loader decides which spellings load). - A string value's surrounding whitespace is kept, and a rule key's case is not folded (owner ruling on the review of #969).
value: " v "is a line a file can hold; it writes another replacement thanvalue: "v"and is another policy.Actionbesideactionis memory only: the loader refuses it as an unknown key. Stripping the value, or lowercasing the key, inside the form passed every test until these. - Also pinned as bytes: a
frozenset,bytes, adateand a rule that isNoneas their type and text;NaN; a nested mapping sorted;phi_tags=Noneas{};remove_private_tagsby truth. - Over the loader (
tests/test_the_policy_fingerprint_is_stable_over_what_the_loader_gives.py): a filesave()writes loads to the policy it was written from, overnone, the floor,basicandcreate_config()'s scaffold; key order, rule order, a tag's case and a rule'snamedo not make another policy;basicandbasic@2026care one; a newSession, a file with noprivacy_profileline and the scaffold are one fingerprint. - Not pinned, deliberately: a rule value that is a mapping with keys of two types makes the form raise
TypeError(#962). An input that raises has no stored fingerprint, so it can be given one after 1.0 without moving any other. - Output: none. No line of
isocenter/changes.
-
The order that keeps a PHI status with its own policy is tested (#753). A status and the policy it was recorded under are two fields, read by the save thread while a scan may be recording the next status, with no lock. What stops a row from holding one status beside another status's policy is an order:
record_phi_statusadvances_revisionfirst and writes_phi_status_revisionlast, and a reader reads_phi_status_revisionfirst and_revisionlast, so a read that overlaps a write sees the revisions disagree and reportsUNSCANNED. The order is kept at three sites, not the one the issue named:TrackedEntity._phi_status_record, the save's restatement of it inpersistence._status_columns, and the writer. Measured onmainat bb5e6f1, swapping the reads in either reader, or writing the status before the revision, left 105 tests green each time.tests/test_a_status_and_its_policy_are_read_as_one_pair.pycaptures the field writes the liverecord_phi_statusmakes, in the order it makes them, and replays them between the live reader's own field reads at every point: all 70 ways one read can overlap one write, and all 495 for two writes. No thread, so it is deterministic on both builds. Onmainno schedule returns a torn pair from either reader. With the reads swapped in one reader, that reader's tests fail; with the writer reordered, both readers' do, naming each schedule and the pair it tore ((IDENTIFIED, base-b),(REMEDIATED, base-a)).- What it does not show: that one attribute read is atomic on the free-threaded build. Nothing in the test runs on two threads.
- Output: none. No line of
isocenter/changes.
-
test_map select's default base and its refusal are tested in scratch repositories (#800). With no--changed-base,selectdiffs from HEAD's merge base withorigin/main, thenmain, and exits asking for a base when neither gives one. One test reached that code, and only in a checkout where a main exists. Measured onmainat bb5e6f1, swapping the two candidates passed every test oftest_changed_code_selects_its_tests.py, and so did turning the fall-through into a raise with the refusal reworded. Five test functions (six cases) now build repositories undertmp_path(first branchtrunk,origin/mainwritten bygit update-ref, no developer configuration,GIT_DIRand its kin removed) and pin:origin/mainbeforemain, by two candidates whose merge bases differ;mainalone; anorigin/mainthat shares no history with HEAD passed over formain; the refusal, by its two phrases, with neither ref and with both refs and no common ancestor; and a named base that does not resolve refused, never replaced bymain.- The script is unchanged. The issue's second item was already true of it:
merge_baseasksgit merge-baseand moves on when that fails. It was the #798 test's own guard that asked whether the ref exists, so in a clone holding an unrelatedorigin/mainthe test took the default path and failed on a correct refusal. The guard now asksgit merge-base HEAD <ref>, and a test shows the two questions differ. - Test only. Output: none.
- The script is unchanged. The issue's second item was already true of it:
-
pytest --changedselects the tests that read a changed module's source, on a function edit too (#779, owner ruling Q7 A). For a function the map covers, the selection was the tests that ran it and the tests that read every*.pyby glob or walk. A test that reads one named module as text runs none of the function:test_pixel_geometry.py::test_module_imports_nothing_heavyparsespixel_geometry.pyfor its imports, and with a fresh map animport numpyadded insidepixel_geometry._contradictionfailed it and did not select it; aWfdbExporter.exportchange did not selecttest_wfdb_privacy.py's test of that method's signature. Between releases both were selected by accident, through the widening for tests a stale map cannot speak for, and the gap opened each time the map was rebuilt.- The rule (
scripts/test_map.py::source_readers): a changedisocenter/**/*.pyalso selects everytests/test_*.pywhose text holds the module's file name (wfdb.py), or that holds a source-reading spelling (getsource,getsourcelines,ast.parse(,.__file__) and the module's name as a whole word. Whole files, found by what the file does, as the glob readers are; it only ever adds, and printsisocenter/x.py: N test files read its source by name -> added. - Measured at this commit, beyond the 16 files every
*.pychange already selects: a mean of 4.7 test files per module over the 35 modules that are not__init__.py, about 45 runner-seconds bytests/shard_timings.json; the largest issession.pywith 26 files and about 225 s; four modules add none (blob_kind.py,builders.py,manifest.py,sidecar.py). An__init__.pyadds 7 files (about 43 s), because__init__is a word test files hold. - Not taken: rule 7's needle (the module's name anywhere in the file) for function edits. Measured in the design, it is 71 files per module on average and the whole suite for
session.py,store.py,entities.py,exporters/dicom.py,persistence.pyandio_handlers.py. - Not seen: a reader that reaches the source through a helper in another test file, or through
importlib,pkgutilorinspect.getfile. The word and the spelling may sit anywhere in the file, so a file that parses one module and mentions another is a reader of both. RELEASING.mdstep 3 states the rule for a by-hand selection. Tooling only. Output: none.
- The rule (
-
A release branch merged into
mainwith its version files is caught, where history shows it (#931, owner ruling Q6 A).RELEASING.mdforbids the merge because git makes it silently:maintakes the release's version and its dated changelog heading, the three version files still agree, andtest_the_changelog_opens_with_unreleased_or_the_declared_releasepasses, because the tree is exactly a release branch's. Measured in four scratch repositories (a line's first release merged intomainby merge commit and by squash, with and without an entry merged tomainsince the cut): no conflict in any,top_heading_problempassed all four, and the released-sections test (#927) was red only for the two wheremainhad its own entry, which it needs the tag to see. No tracked fact can tell the trees apart, since a merge carries every filemaindid not change as the release has it.tests/test_version_contract.py::test_this_checkout_is_not_a_release_merged_forwardasks git two things of a tree whose first changelog heading is its own declared version: is the tagv<version>, where the clone has it, an ancestor of HEAD; and is the commit that last changedisocenter/_version.pyon HEAD's first-parent chain. A squash of a tagged release fails the first, a merge commit the second. Both hold at every tag from v0.9.6 to v1.0.0rc13 and on both release branches (measured in the design), because a PR intorelease/X.Ymerges by squash.- Not asked of a tree that opens with
[Unreleased]: a work branch that tookmain's release record by a merge commit has its version set off the first-parent chain, and is right. - Not caught: a squash of a release commit that has no tag yet; a merge commit whose resolution of
_version.pymatches neither parent; any clone with no tags and no history (a shallow checkout of a branch, where both questions are silent). Agit archivecopy or an sdist skips the test. - Red on a healthy branch if
release/X.Yever takes its release commit by a merge commit rather than the squashRELEASING.mdprescribes; the message says so. - Test only. Output: none.
-
The CI shards' timings are refreshed from each release run, and a test says when the suite has outgrown its shards (#935, owner ruling Q3 A). #935 moved the suite to eight shards cut from runner seconds, taken once from the rc13 rehearsal's logs by a script that was never committed. The file goes stale as tests are added: a file with no number weighs the median, and 78 such files were 36% of the suite's seconds before anything noticed.
tests.yml: theRun Testsstep passes--record-shard-timings=${{ runner.temp }}/shard-timings.json, and a new step uploads that file as the artifactshard-timings-<version>-<shard>, withif: always()(a shard with a failing test still measured its files) andcontinue-on-error(bookkeeping never turns a release red). A shard killed at its timeout writes no file and uploads nothing. The step is capped at 2 minutes, so the step sum is 40 against the job cap of 45, which stands.publish.ymlis unchanged and inherits the step.scripts/shard_timings.py merge <dir> --out <file>reads whatgh run download --pattern 'shard-timings-*'wrote and gives, for each test file, the median across the versions, in the recorder's own format. It reads the shard count fromtests.ymland refuses a run in which any version lacks a shard, or holds one the workflow does not have, or recorded a file in two shards, or when the folder is missing or holds no recording: a sentence each time, never a traceback, and nothing written. A file one version reported no test of takes the median of the versions that ran it.RELEASING.md, "Cutting a release", step 8 gains the refresh, after the record-back and in a PR of its own (owner ruling on the review of #969): only from the publish run of the newest release line, and only when that run uploaded the timings; otherwise the file is left alone and the record-back PR says why. A release line that does not hold the upload step yet, asrelease/1.0did not at rc14, has nothing to refresh from. When the refreshed file turns the capacity test red, the same PR adds the shards.test_the_heaviest_shard_fits_the_step_with_roomfails when the heaviest shard's recorded weight passes half theRun Testsstep, half because one runner was measured 1.75 times slower than another at one commit. Today it is 555 s of 1,500 (37%), so the test turns red when the suite has grown by about a third, at a refresh, and says to add shards or raise the cap. It rests on the file holding runner seconds: a local recording is about 2.3 times lighter and would pass with the suite twice the size, and nothing checks the unit.test_the_test_step_runs_for_every_matrix_entry(review of #964):if: matrix.shard != 8on theRun Testsstep left the matrix reading 1..8 and passed every workflow pin with an eighth of the suite unrun. Noif:that reads the matrix, and nocontinue-on-error, on that step or its job. And the step's script is the onepytestcommand, whole:[ "${{ matrix.shard }}" = 8 ] || pytest ...skipped the same shard in the shell and passed the first version of this pin (review of #969).- Not proven here: the upload itself. No workflow runs on a pull request; the first run of it is the next release rehearsal, or dispatch of
tests.yml, made from a ref that holds this change.tests/shard_timings.jsonis not retaken in this change. - Output: none.
Fixed
-
A WFDB record no longer names a Lead III
V1, nor two signals by one lead name; and a lead's Code Value is compared without its surrounding whitespace (#832, owner rulings Q1 A, Q2 A, Q3 A). Since #828 a Channel Source coded in DICOM's ECG lead context group (CID 3001) is written to the.heasignal line, and toleadinannotations.json, as the lead's name. Three things were wrong with how, each measured onmainat bb5e6f1 throughingest()andexport(format="wfdb"), identically on 3.12 and 3.14t, with noWARNING,ERRORorDATA_LOSSrow written (each export wrote itsEXPORTrow, as every export does):MDC 2:3was alwaysV1. PS3.16 2011 prints CID 3001 "ECG Leads", version 20080927, with two rows for that code,MDC 2:3 Lead IIIandMDC 2:3 Lead V1; the 2013 version (20130613, current in 2026d) moved Lead III to2:61. So a cart built between the two codes Lead III as2:3. A 12-lead file coded that way exportedI II V1 aVR aVL aVF V1 V2 … V6, and its limb leads alone (2:1 2:2 2:3) exportedI II V1: Lead III named V1, with no second V1 beside it to notice. The Quick Start pipeline changes neither:basic@2026cleaves the Channel Source item as it is.- Two signals could share one name. Besides the file above, Lead II coded on two channels exported
II II, and a local code spelledV1beside a coded V1 exportedV1 V1.wfdb.rdrecord(channel_names=["V1"])over such a record returns one of the two signals and says nothing, and the issue reports that the PhysioNet Challenge'sreorder_signalasserts that names are unique (not run here). - A Code Value with a space in front was not named. Code Value is
SH, "a character string that may be padded with leading and/or trailing spaces" (PS3.5 §6.2); pydicom strips the trailing space on read and not the leading one, so' 2:1'reached the table unstripped and was exported as2:1, a bare code whereIwas meant. - Now, the Code Value is compared stripped (Q3 A):
' 2:1'is namedI. Stripped isstr.strip(): any whitespace in front or behind (a tab, a no-break space, a line break), not the space character alone (owner ruling on the review's Q2). Only the comparison changes; a code the table does not name is written as before. An interior space is still significant (2: 1is not2:1). - Now, an
MDC 2:3channel is named by its own Code Meaning, and this code only (Q1 A):IIIwhen the Channel Source item's Code Meaning(0008,0104)says Lead III, andV1otherwise. "Says Lead III" is the comparison a Channel Label already gets: case ignored, whitespace collapsed, an optionalLeadin front, then exactlyiii; soLead III,lead iiiandIIIare, andDerived Lead III,Lead III (Einthoven)andLead IIIIare not. Nothing from the Code Meaning is written: the name is one of two fixed strings. The 2008 12-lead file now exportsI II III aVR aVL aVF V1 V2 … V6and its limb leadsI II III; a file coded to the 2013 table is unchanged.- The cost, and why it was taken. PS3.3 §8.3 says the Code Meaning "shall never be used as a key, index or decision value". This reads it as one, in the one place where the standard's own table made the code ambiguous, because the alternative is to go on calling a Lead III
V1. No other key reads its meaning:SCPECG 5.6.3-9-3andMDC 2:4with a Code Meaning ofLead IIIare stillV1andV2. - The limit. A Lead III coded
2:3whose Code Meaning is empty, or says it in another way (Ableitung III), is still namedV1, with nothing said, when no other signal of the record is named V1.
- The cost, and why it was taken. PS3.3 §8.3 says the Code Meaning "shall never be used as a key, index or decision value". This reads it as one, in the one place where the standard's own table made the code ambiguous, because the alternative is to go on calling a Lead III
- Now, when two or more signals of one record would carry the same name, each one the lead table named is written as its Code Value instead (Q2 A), in the
.heaand inannotations.jsonalike. Names are compared ignoring case, since the consumer the issue names matches leads that way. Lead II coded twice is written2:2 2:2: the file says one code twice, and the header then says exactly that. A 2008-coded record with empty Code Meanings (2:1 2:2 2:3 2:3 2:4) is writtenI II 2:3 2:3 V2. Only a name the table produced falls back: a Code Value written as it stands, a kept Channel Label and ach<N>token have no other spelling, so a local codeV1beside a coded V1 becomesV1and5.6.3-9-3, and one pressure code on two channels (G-DB22 G-DB22) is unchanged and draws no row, because nothing was named.- The row. One
WARNINGaudit row per record, on the instance's SOP Instance UID, and the same sentence in the log:WFDB record: channels 1 and 2 both resolve to the lead name II, so each is written as its Channel Source Code Value. A name shared by two signals cannot say which lead either is.Channel numbers count from 1, as DICOM numbers them. It holds channel numbers and the table's lead name and no Code Value and no Code Meaning, since a non-conformant source can put anything in either. Three or more channels readchannels 1, 2 and 3 all resolve to …; a single coded channel whose name another signal carries without a lead code readschannel 2 resolves to the lead name V1, which another signal of the record carries without a lead code, so it is written as …; several names in one record are listed in the one row; all but the two-channel form endA name shared by more than one signal cannot say which lead each of them is.The two-channel closing sentence is used only when those two are all that carried the name: two coded V1 beside a local codev1readchannels 1 and 2 both resolve to the lead name V1, so each is written as its Channel Source Code Value. A name shared by more than one signal cannot say which lead each of them is.The wording beyond the two-channel sentence is this change's, not the specification's, and row wording is not frozen. - The grade. A
WARNINGrow grades the runREVIEW_REQUIRED(docs/analytics.md, section 4). An export of such a record that gradedPASSnow gradesREVIEW_REQUIRED: a person should look at a record whose leads the export could not name. A record with nothing to fall back from draws no row and grades as before. - A column past the channel definitions.
format_headergives a sample column past the defined channels the last definition, so oneMDC 2:8definition under three columns was writtenV6 V6 V6. Those are three signals with one table name: they are now2:8 2:8 2:8, with the row naming channels 1, 2 and 3.
- The row. One
- How. The rule is about two channels, and
format_headerandmurmur._lead_foreach saw one.exporters.wfdb._signal_descriptions(waveform, n_signals)resolves the whole record;WfdbExporter._write_instancecalls it once, over the sample columns, and hands the same list to both writers, so the two files cannot disagree about a lead's name.format_headerandmurmur.build_annotationstakedescriptions=Noneand compute their own when called directly.WaveformChannelgainssource_meaning, its last field. None of these names is indocs/api/stability.md. CONFIG_VERSIONis unchanged: a lead name is computed by the exporter from the Channel Source item, and no rule's findings, written values or reach move (as #828).docs/waveforms.md"Lead names" says all three, and its claim that leads are "named as PhysioNet records name it" is replaced: PTB spells its orthogonal leadsvx vy vz, notX Y Z.tests/test_waveform_model.pypins the padded codes with their interior-space controls, each spelling that is and is not Lead III, and that no other key reads its meaning.tests/test_two_signals_of_one_record_do_not_share_a_lead_name.pypins fifteen channel lists against literal descriptions and clashes, the three files through a session read from the.heatext and bywfdb.rdheader,annotations.jsonagreeing with the header where the fallback fires (one definition under three columns included), each row whole, the grade from the report file, and the records that draw no row.- Output: in a WFDB
.hea, the description field of a signal line, and inannotations.jsontheleadof a finding on that channel, change for three kinds of channel and no other: a lead code with whitespace in front of it is written as the lead's name where it was the bare code (2:1toI); anMDC 2:3channel whose Code Meaning says Lead III is writtenIIIwhere it wasV1; and a table-named channel whose name another signal of its record shares is written as its Code Value where it was the name (IIto2:2), with one newWARNINGaudit row for that record, which grades itREVIEW_REQUIRED. Nothing else on the signal line, the record line, the.datfile or the start-date comment moves. No golden-cohort member reached any of it (the two.hearecords in the fingerprint hold SCP-ECG codes and reference IDs, with noMDC 2:3, no padded code and no shared name), so a new synthetic member,synthetic:ecg_lead_codes(scripts/golden_cohort.py), is added, four of whose five files are this entry's (the fifth,-short, is #963's, below):-2008-12lead(the 2008 table's twelve leads with marks on channels 3 and 7, recordedI II III aVR aVL aVF V1 V2 V3 V4 V5 V6withleadIIIandV1),-2008-limb(I II III),-padded(' 2:1','2:2 ',' 2:61 ',2:62; recordedI II III aVR) and-coded-twice(Lead II on two channels with a mark on channel 2; recorded2:2 2:2,lead2:2, and theWARNINGrow). Infingerprint/output.json, retaken on 3.12 and checked clean on 3.14t, the member is the one new in section 2, Cohort (1 difference in 1 group); no existing member's output changes (sections 3 to 6: 0 differences), the two existing.hearecords included.
-
A mark on a channel the WFDB record has no signal for carries no
lead(#963, owner ruling). A waveform group may hold more Channel Definitions thanNumberOfWaveformChannels(003A,0005)says it has sample columns. The.heagets one signal line per sample column, so the definitions past the last column describe no signal of the record. A Waveform Annotation whose Referenced Waveform Channels(0040,A0B0)named one of them was still written to<record>.annotations.jsonwith that definition's lead name. Measured onmainat bb5e6f1, three definitions (MDC 2:1,2:2,2:61) over two columns with a mark on each channel: the.heawrote two signal lines,IandII, and the three findings carriedleadI,IIandIII. A reader that places a mark by its lead had noIIIto place it on.- Now that finding has no
leadkey. The mark itself is kept, with its position, category and label: its sample positions are on the record's own axis. The key is absent, notnull, which is how the bridge already writes every mark with no lead (a channel number of 0, for one); Murmur's schema does not requirelead, andbuild_annotationshas never written a null one. - What does not change: the
.hea(it never had a line for such a channel), a mark on a channel that has a signal line, and a mark naming a sample column past the definitions, which carried no lead before and carries none now. No audit row is written for it and the grade does not move: none was ruled. - Only through
export(format="wfdb").murmur.build_annotationscalled directly with nodescriptions=does not know the sample columns, so it gives every defined channel a lead; the exporter hands it the list the header was written from. (That direct call is not unchanged by this release: since #832, above, it applies the shared-name rule over the defined channels, so definitionsI II IIgive2:2on the second and third where they gaveII.) tests/test_two_signals_of_one_record_do_not_share_a_lead_name.pyheld this file as a pin of the old['I', 'II', 'III'], written in #832 while the question was open; it now asserts the two signal lines, the three findings by position, the third with noleadkey, and noWARNING,ERRORorDATA_LOSSrow.docs/waveforms.mdsays it beside the rule for marks on a discarded group.- Output: in
<record>.annotations.json, a finding loses itsleadkey when its mark names a single channel of the kept group whose number is greater thanNumberOfWaveformChannels(a file whose Channel Definitions outnumber its sample columns). Nothing else in that file, and nothing in the.heaor.dat, moves; no audit row is added or removed. No golden-cohort member held such a file, sosynthetic:ecg_lead_codesgets a fifth file,-short(three definitions over two columns, marks on channels 2 and 3; recordedI II, withleadIIon the first finding and none on the second). It is part of the one new member in section 2, Cohort.
- Now that finding has no
-
Corrections to the 1.0.0rc14 record: ten
**Output:**lines said less than the release does (#764, #679, #926, #775, #721, #733, #722, #747, #703, #937). An**Output:**line is where a change to what an export writes, what rows it records or what a run grades is named. Each of the eleven lines that C2 (#947) and C3 (#958) shipped was read against a measurement ofv1.0.0rc13and of the tree 1.0.0rc14 was cut from (mainat28057af2), on 3.12.14 and again on 3.14.7t, which agree: every exported element read from the written file, in the Implicit VR export and in the JPEG 2000 one, which names its VRs on the wire, and every grade read from the report file. No behaviour changes here: this states what 1.0.0rc14 already does. Found by the review of the rc14 release commit (PR #968) and the audit it asked for. The rc14 section's lead names some of this for #764, #926, #775, #747, #703 and #937; where it and a line below disagree, the line below is the measured one. #593's line ("none") was audited too and is accurate: the refusal's message changes and the export does not.- Nothing below puts an identifier into a file graded
PASSor stamped(0012,0062) YES. Where a grade or a marker moves on a file that was already being written, it moves toREVIEW_REQUIREDand to no marker, with one exception: #926's private tag, dropped under a graded row before and written with none now, which is the caller's own number. Elsewhere an export that raised now writes (#926, #722, #679), an export that returned now raises and writes nothing (#775, #721), or a file leaves the export (#747). - The 1.0.0rc14
**Output:**line for #764 said "none. No exported byte changes". The owner's value in the file is unchanged, as it says. The de-identification markers are not: they go only on an instance whose own status isREMEDIATEDorCLEARED, and #764 leaves the instanceIDENTIFIEDin two sequences. They withhold different markers, which the rc14 lead did not separate.- An owner
REPLACEbeside a hand-built instanceREMOVEon Patient's Name, under the floor: three markers. Before: instanceREMEDIATED, the file carriedANONYMIZEDwith(0012,0062) YES,(0012,0063)and(0028,0303) MODIFIED, no decline row,PASS. In 1.0.0rc14: instanceIDENTIFIED, the file carriesANONYMIZEDand none of the three, oneREMEDIATION_DECLINEDrow,REVIEW_REQUIRED(the declined remediation, and one instance readIDENTIFIED). anonymize(<instance findings>)thenanonymize(<owner findings>)underprivacy_profile: noneplus0010,0010: REMOVEor0008,0020: REMOVE: two markers. Before: the element written empty,(0012,0062) YESand(0012,0063),PASS. In 1.0.0rc14: the element written empty, neither marker, no row,REVIEW_REQUIRED(one instance readIDENTIFIED) untilanonymize(audit()), after which both are written and the run gradesPASS.(0028,0303)is in neither file, before or after, and not in the whole-report control either. Under the name rule Study Date is shifted in the file (oneREMEDIATION_SHIFT_DATErow) and under the date rule it is written empty; in both, Series Date, Acquisition Date, Content Date and Instance Creation Date are the source's, and(0028,0303)is written only when no date in the file is as found.- What to do: hand
anonymize()the whole report, or the owners' findings before the instances'; after instances-then-owners, runanonymize(audit())before exporting. test_the_replace_and_remove_pairing_exports_no_markerandtest_the_owners_removal_in_a_later_pass_exports_no_marker_until_a_reaudit(tests/test_an_owner_stamped_copy_follows_its_owner.py) read the three tags from both exports and the grade from the report file, each beside its control; both are red with theREMOVEallowed to run on a copy its owner stamps.
- An owner
- The 1.0.0rc14
**Output:**line for #679 said "none. No exported byte changes; a hand-built finding's audit row changes as above". The markers and the grade change too. Measured after a fullanonymize(audit())under the floor, then a hand-builtREMOVEonSeries.modality(Study.date_shiftedandPatient._phi_statusmeasure the same). Before: the arm ran, aREMEDIATION_REMOVErow, the file carried ModalityCTwith all three markers,PASS. In 1.0.0rc14: the finding declines, the decline holds the entity it named atIDENTIFIED(a Series' instance, the Study or the Patient), and the file carries ModalityCTand none of(0012,0062),(0012,0063),(0028,0303); the run gradesREVIEW_REQUIRED, for the declined remediation and the entity readIDENTIFIED. ForPatient.studies,Study.seriesandSeries.instancesthe export raisedTypeErrorbefore and now writes the file, with no marker and the same grade.- What to do: do not hand
anonymize()aREMOVEnaming a Python attribute the export does not write. A run that did needsanonymize(audit())before its files are stamped again: measured forSeries.modality, the three markers are then written, and the run still gradesREVIEW_REQUIRED, on the decline row, which stays in the log. test_a_declined_stray_remove_after_a_full_pass_exports_no_marker(tests/test_declined_remediation_is_recorded.py) reads Modality and the three tags from both exports and the grade from the report file, beside the full pass alone as its control; red with the arm writing any name the entity carries.
- What to do: do not hand
- The 1.0.0rc14
**Output:**line for #926 said "none", and gave only the reason the fingerprint does not move. The body's "14 of 56 measured cells" did not say by which route a numpy value reached the exporter. Byset_attrand thenexport(), the route a caller takes:- On a standard tag (
US or SS,DS,IS,LO): before,export()raised json'sTypeErrorat its leading save and wrote nothing. In 1.0.0rc14 it writes the file with the Python number:np.int64(7)asSSunder(0028,0106), as7.0in a DS and7in an IS;np.float32(0.1)in a DS as0.10000000149012with #723's roundingWARNING(REVIEW_REQUIRED), in an IS as0with itsWARNING(REVIEW_REQUIRED); any of them in anLOfails that file with anERRORrow, as a Python number does. - On a private tag: before, the save returned (the private tier stored the value's text),
export()wrote the file without the tag under one PRIVATEDATA_LOSSrow, and the run gradedREVIEW_REQUIRED. In 1.0.0rc14 the tag is writtenLO(7;1\2for a list;0.10000000149011612;Truefornp.bool_(True)) with no row, and the run gradesPASS. That includes a file stamped(0012,0062) YES: withremove_private_tags: falseandnp.int64(123456789)set beforeanonymize(audit()), the file was exported without the tag, stampedYES, under the PRIVATEDATA_LOSSrow,REVIEW_REQUIRED; in 1.0.0rc14 it carriesLO123456789, is stampedYESand the run gradesPASS. It is the caller's own value under a policy that keeps private tags, exported as a Pythonintset the same way always was. - Written around the entity and never saved (
inst.attributes[tag] = np.int64(7), nomark_modified()): the entry says the exporter's own reading "is unchanged". Measured, it is unchanged under DS, IS, LO and a private tag, and changed under aUS or SSelement, where a numpy float andnp.bool_are still dropped as they were:np.int64(7),np.uint16(7)and a two-valued list ofnp.int64were dropped with a STANDARDDATA_LOSSrow and are now writtenSSwith no row,PASSbefore and after, under each of the seven tried ((0028,0106),(0028,0107),(0028,0108),(0028,0109),(0028,0120),(0028,0121),(0028,1101)). A three-valued list ofnp.int64is dropped under all seven, before and now. The cause is #703, not #926: pydicom'sUS or SSresolver raisesTypeErrorover a numpy integer (object of type 'numpy.int64' has no len()), and_resolve_one_ambiguous_vrnow takes aTypeErrorfrom the resolver as "the decider cannot decide" where it dropped the element. Narrowing that clause back toAttributeErrordrops the element again with the old row (run fornp.int64(7)under(0028,0106)). #703's entry says the clause is reached only for LUT Data; this is a second way in. It is stated here as measured and deliberately not pinned by a test: whether the clause is kept or narrowed is #972. - Written around the entity and then
mark_modified()(inst.attributes["0028,1050"] = np.float32(0.1)), which neither the entry nor its line describes: before,export()raised json'sTypeErrorat its leading save. In 1.0.0rc14 the save stores the Python float and the graph keeps the numpy object, so the live export writes the DS as0.10000000149011612, 19 characters, with no row, and the run gradesPASS, while a reopened session's export of the same store writes0.10000000149012with the roundingWARNINGand gradesREVIEW_REQUIRED. A live export and a reopened one therefore differ for a value written this way; throughset_attrthey do not. Stated as measured, not pinned, and not changed here. - What to do: nothing, unless a caller relied on a private numpy value being dropped at export; that caller should not set it.
test_what_export_writes_records_and_grades_for_a_numpy_number(tests/test_a_numpy_number_set_into_an_attribute_is_saved.py) pins the element, the rows and the grade read from the report file for a privatenp.int64, a private list and a DSnp.float32.
- On a standard tag (
- The 1.0.0rc14
**Output:**line for #775 said "none. No exported byte and no audit row changes". Withinst.set_attr("0011,1001", {1, 2})(adateand anobject()measure the same): before,export()returned, wrote the file without the tag under one PRIVATEDATA_LOSSrow and gradedREVIEW_REQUIRED; the store held'{1, 2}', and a reopened session's export wrote that text (LOunder Explicit VR) with no row of its own. In 1.0.0rc14export()raises the namedTypeErrorbefore any file is written and writes no row, and the reopened store holds no such tag, so its export writes the file without it. The entry's Breaking bullet says the first half; the**Output:**line said neither.- What to do: set a value the private tier holds (a
str, a number, a list of those) and export again. test_an_export_over_such_a_private_value_writes_no_file_and_no_row(tests/test_a_value_the_store_cannot_hold_is_refused_by_name.py) asserts the refusal whole, that no folder exists, that no row was written, and what the reopened export's file holds.
- What to do: set a value the private tier holds (a
- The 1.0.0rc14
**Output:**line for #721 said "none. No exported byte and no audit row changes". True ofsop_instance_uid = None, which raised before and raises now with nothing written. Not of a UID of another type. Withsop_instance_uid = 12345on one of two instances: before,export()returned, wrote the other instance's file and oneERRORrow (Export failed for instance 12345: TypeError: A UID must be created from a string), and the run gradedREVIEW_REQUIRED; the leading save had already dropped that instance's row from the store. In 1.0.0rc14export()raises theValueErrorbefore anything is written: no file for either instance, no row, and the store keeps both.- What to do: give the instance a
strUID, or remove it from its series, and export again. test_an_export_over_a_uid_of_another_type_writes_no_file_and_no_row(tests/test_an_instance_with_no_uid_is_refused_before_the_save.py) asserts the refusal, no folder and no row from both export arms, and that the same call writes both files once the UID is astr.
- What to do: give the instance a
- The 1.0.0rc14
**Output:**line for #733 said "none". Afterrecover_patient_identity(restore=True)over two instances whose token holds a multi-valued tag (Other Patient Names set as a list), with one instance's list then edited in place andmark_modified()called: before, the live export wrote the edit into both files, and a reopened session's export into the edited instance's alone. In 1.0.0rc14 both exports write it into the edited instance's file alone. The values are a restored identity's, so no de-identified output is involved.test_an_edit_in_place_after_a_restore_is_exported_for_one_instance(tests/test_recovery_returns_the_identity.py) reads(0010,1001)raw from each file of both live exports and of the reopened one.
- The 1.0.0rc14
**Output:**line for #722 said "none", then named a row. Two changes it did not state as changes. Afteros.chdir,export()from a store opened by a relative path raisedsqlite3.OperationalError: no such table: patientsand wrote nothing; in 1.0.0rc14 it writes the files, and itsEXPORTrows go to the original store. And theentity_uidcell of the fourWARNINGnotices about a store from before 1.0 is the absolute path where it was the relative one (measured for the ID-less-group notice:legacy.dbbefore, the absolute path now). No fresh store writes any of the four, as the line says. Both are pinned by the entry's own tests (tests/test_a_store_keeps_its_place_when_the_process_moves.py, andtest_the_notice_names_the_store_by_its_absolute_path). - The 1.0.0rc14
**Output:**line for #747 named the row and the refusal, and not the grade or the file; and "exported with the UID written as a Python list's text", which the rc14 lead repeats, is true only where the UID was not replaced. A file whose Study or Series Instance UID holds two values, under the floor withanonymize(audit()): before, it was ingested, its UID was replaced like any other (the replacement was derived from the list's text, so the file carried a2.25.UID and not that text), and it was exported with all three markers,PASS. The list's text reached a file only where the UID was not replaced, in an export with no pass or under a policy that keeps it:['1.2.3', '1.2.4'],PASS. In 1.0.0rc14 the file is not ingested, so it is not in the export at all, and the run gradesREVIEW_REQUIREDon theERRORrow; a file beside it is exported and stamped as before. For a two-valued Patient ID or SOP Instance UID the run wasREVIEW_REQUIREDbefore and is now, as the line says.- What to do: correct the source file's UID to one value and ingest it again.
test_a_run_over_a_refused_file_exports_only_the_file_beside_it_and_does_not_pass(tests/test_a_multi_valued_linkage_key_is_refused_by_name.py) counts the exported files in both arms, reads the stamp and both UIDs from the file, and reads the grade from the report file, beside a control with no such file.
- The 1.0.0rc14
**Output:**line for #703 named the row, and not the grade. Measured under the floor: the Explicit VR file, exported without LUT Data under a STANDARDDATA_LOSSrow, gradedPASS; in 1.0.0rc14 it exports LUT Data asOWand gradesREVIEW_REQUIREDon theWARNINGrow, one per export. The Implicit VR file was refused (ERRORrow, no file,REVIEW_REQUIRED) and is now exported with the sameWARNINGrow and the same grade. Both files carry(0012,0062) YES. The grade is pinned by the entry's owntest_the_explicit_twins_run_grades_on_the_warning. The sameexceptclause also reaches a numpy integer written around the entity under aUS or SSelement, as the #926 bullet above says (#972).- What to do: read the
WARNINGrow. The bytes are the source's; it is the file's LUT Descriptor that needs correcting, at the source.
- What to do: read the
- The 1.0.0rc14
**Output:**line for #937 named the privatePNrow, and not the grade or the Implicit VR export. A privatePNof two values from an Explicit VR source, kept withremove_private_tags: false: before, both exports carried the bracket text (PNunder Explicit VR), no row,PASS. In 1.0.0rc14 the Explicit VR export writes itUT, the Implicit VR export carries the source's text where it carried the bracket text, each export writes the re-VRWARNINGrow, and the run gradesREVIEW_REQUIRED. The other cases are as the line says and gradePASSas before. Pinned by the entry's owntest_a_private_person_name_of_two_values_is_written_ut_with_its_row. - Output: nothing changes in this release; this corrects ten 1.0.0rc14 lines, as itemised above.
fingerprint/output.jsonis not retaken: no code that writes output changed, and no golden-cohort member handsanonymize()a partial or hand-built list, sets a value by hand, restores an identity or changes directory.
- Nothing below puts an identifier into a file graded