DICOM de-identification design#
This document specifies the DICOM de-identification engine that will replace pymedphys.dicom.anonymise and the experimental pseudonymisation module, and records the decisions behind it. A run now de-identifies, checks, and reports a collection, but no preset is enabled until the first supported release; the register records what is implemented. The current tools share one engine in lib/pymedphys/_dicom/anonymise/, do not implement a DICOM confidentiality profile, and have the limitations listed in DICOM de-identification.
Pull requests for this work are listed in the tracking issue. A pull request that changes a decision updates this document, the user documentation, and any affected code in the same change.
Scope#
The engine de-identifies a collection of DICOM instances as a unit and preserves the references between them. The library API, the command-line interface, and the Streamlit app all call the same engine.
The target coverage is:
Objects: CT, MR, PET, RT Structure Set, RT Plan, RT Dose, RT Beams Treatment Record, RT Image, spatial registration, and segmentation.
Profile and options: the Basic Application Level Confidentiality Profile with the Retain Safe Private, Retain Device Identity, Retain Patient Characteristics, Retain Longitudinal Temporal Information with Modified Dates, and Clean Descriptors Options.
Evidence: a generated conformance statement, a requirements register traced to tests, and published benchmark results (D-018).
The first supported release covers the Basic Profile, alone (basic) and with the Clean Descriptors Option (basic-clean-descriptors), with a run-scoped key, for uncompressed CT Image, RT Structure Set, RT Plan, and RT Dose instances, including those of CT Image Storage - For Processing (D-010). Clean Descriptors is included because, under the Basic Profile alone, RT Structure Sets would keep no usable structure names (D-009). Later releases extend it to the target coverage, each with its own evidence.
The first supported release ships the library API and the command line, which is a thin layer over the library: batch users need the command line, and the surface to validate stays small. A library alone was rejected because most users will not script it. The command prints a summary of counts, statuses, and reason names, never a source value or source path (the only directories it names are the release directory, its staging area, and the QC destination), and its exit status tells a script the outcome: 0 when every input was released or is a duplicate of one released, 1 when the release was published but an input was refused, sequestered, or held for review, 2 when its arguments cannot be parsed, 3 when the run did not start, its first pass stopped it, or its QC pack could not be written, 4 when its staging area was left behind, which outranks every other status since the staging area may still identify people and must be deleted by hand, and 70 for any other failure, reported by the exception’s type alone. The app comes in a later release, because pymedphys gui has no login and can serve its apps to other computers, so a de-identification app needs its own design for keys and QC material. Until the new app ships, the legacy pseudonymisation app stays, with its limitation banner (D-019).
The following are not supported. Input that needs them is rejected or sequestered, without a conformance claim:
second-generation RT objects, RT Ion objects, and brachytherapy treatment records;
the Clean Pixel Data and Clean Recognizable Visual Features Options (D-015), the Clean Graphics Option, and the Retain UIDs, Retain Institution Identity, and Retain Longitudinal Temporal Information with Full Dates Options;
Structured Reports, Key Object Selection documents, Presentation States, and Private SOP Classes (D-010);
legal determinations, since whether data are anonymous depends on the release context, not on the software.
Terminology and claims#
The engine performs de-identification, the term used by DICOM PS3.15 and the MIDI Task Group report. Anonymisation and pseudonymisation are not separate features: both apply the same transformation. They differ in whether anyone retains a means of re-identification, such as a project key or a crosswalk, and in which indirect identifiers the selected options keep.
Whether data are anonymous is a legal conclusion about the data in their release context (for example GDPR Recital 26), which software cannot establish. The Australian Privacy Act 1988 (Cth) s 6(1) and the HIPAA Privacy Rule (45 CFR 164.514) use “de-identified” for such a legal status.
In code, command-line output, reports, and documentation:
Describe output only as “de-identified in accordance with the DICOM PS3.15 edition Basic Application Level Confidentiality Profile with options”, and only after validating its effective policy and content against that profile and those options (D-011). Never use “de-identified” without that qualification.
Never describe output as “anonymised”.
Where a key or crosswalk is retained, state that the data remain pseudonymised personal data for whoever holds it, and that any other recipient needs its own assessment.
An instance is sequestered when the engine does not write it to the release output, whether it is set aside before de-identification or fails a check during or after it: the run report lists it by opaque identifiers only, it is excluded from the conformance claim, and its source is left unchanged.
Standards basis#
Role |
Source |
|---|---|
Normative rules |
DICOM PS3.15 Annex E, edition 2026d: E.1.1 (de-identifier), E.1.3 (conformance statement), E.2 (Basic Profile), E.3 (Options), and Tables E.1-1, E.1-1a, E.3.4-1, and E.3.10-1 |
Supporting DICOM parts |
PS3.3 (attribute Types per IOD), PS3.4 (Table B.5-1, the IOD of each Storage SOP Class), PS3.5 (UID encoding, registration, and UUID-derived UIDs), PS3.6 (data dictionary and well-known UIDs), PS3.7 (Implementation Class UIDs), PS3.10 (File Meta Information), PS3.16 (Tables 8-1 and 8-2 coding schemes and their UIDs, CID 7050 de-identification methods, and CID 7005 contributing equipment purposes) |
Best practice |
Clunie DA et al., Report of the Medical Image De-Identification (MIDI) Task Group: Best Practices and Recommendations, 7 February 2025, arXiv:2303.10473 |
Validation |
NCI MIDI synthetic-identifier datasets, answer keys, and validation script (D-018); |
Governance context (documentation only) |
GDPR Article 4(5) and Recital 26; CJEU C-413/23 P EDPS v SRB (4 September 2025); UK ICO anonymisation guidance; Privacy Act 1988 (Cth) and OAIC de-identification guidance; ISO 25237:2017 |
Where this document relies on an informative Note in PS3.15 or on a MIDI recommendation, the choice it supports is a design decision, not a conformance requirement. Browsing links follow the current edition; the table generator uses the pinned publication (D-001).
Requirements register#
The requirements register traces the standard’s “shall” statements and the MIDI best practices to the decisions, code, and tests that satisfy them. It is lib/pymedphys/_dicom/deidentify/requirements.toml, curated by hand, and records:
every paragraph of PS3.15 Annex E in the pinned edition that contains “shall”, outside Notes and tables, verbatim with any list it introduces and a link to the paragraph;
the 18 best practices of MIDI §1.6, summarised in PyMedPhys’s words.
Like the generated tables, the file carries the attribution “DICOM PS3.15 2026d, © NEMA” (D-001).
Identifiers are stable and never reused. PS3.15-E.1.1-01 numbers the paragraphs of a section in the standard’s order, and MIDI-BP-01 to MIDI-BP-18 follow the report’s numbering. Each entry cites the decisions that address it and has one of these statuses:
Status |
Meaning |
Entry also records |
|---|---|---|
|
In scope, not yet implemented |
The milestone that completes it |
|
Partly implemented |
Modules so far, what remains, and the milestone that completes it; tests so far cite it |
|
Met for the supported scope |
Modules; the tests that show it cite it |
|
Not supported, such as an Option that Scope excludes |
A note saying why |
|
Outside PyMedPhys’s role, such as re-identification |
A note saying why |
A pull request that implements part of a requirement updates its entry in the same change.
A test cites the requirements it shows are met with a marker on the test function or its
Testclass,@pytest.mark.deid_requirement("PS3.15-E.1.1-01"), and the register does not list tests. The register’s tests for a requirement are those that cite it, read from the tests’ source without importing them, in the order of their files and then of their definitions. A note may be written one sentence per line, and its lines are joined with spaces. Both keep pull requests that add tests or sentences to the same requirement from editing the same lines of the register.pymedphys._dicom.deidentify.requirements.load_requirementsrejects malformed or inconsistent entries, such as an implemented requirement without tests or an exclusion without a note, and a test that cites a requirement the register does not record. Tests check that every cited decision and module exists, that pytest collects every citing test and sees the same citations as the source, and that the register follows the edition of the generated tables.A new edition updates the register with the tables, after comparing it with every Annex E page of that edition: add paragraphs, update changed text, and remove deleted paragraphs without reusing their identifiers.
Normative text without “shall”, such as the paragraphs after Table E.1-1a on actions for Sequences and on Options overriding the Profile, gets entries when the engine implements it.
The traceability matrix for each release (D-018) is generated from the register by
pymedphys dev deid-matrix, as Markdown, and is not committed. It summarises the entries by status and source, and lists each requirement with its milestone, decisions, modules, and traced tests, the exclusions with the reason for each, and the remaining work with its milestone. Given pytest JUnit XML reports with--junit, one for each tested environment, it gives each traced test’s outcome. A requirement’s traced tests pass only when every case of every test that traces it passed in every report: a skipped case, or a test or case that one report lacks although another ran it, leaves them incomplete; passing tests do not make a partial requirement met.--checkexits with status 1 when a traced test failed, was skipped, or did not run. Each release renders the matrix from the reports of every environment in the unit-test matrix, checks it, and attaches it to the GitHub release.
Architecture#
The engine will live in lib/pymedphys/_dicom/deidentify/, with its public API in pymedphys.dicom. Its components are listed below; they are not a pull request sequence.
Generated standard tables. Table E.1-1 with every option column; Tables E.1-1a and E.3.10-1; the PS3.6 data dictionary (Table 6-1), with each attribute’s keyword, VR, VM, and retirement; the PS3.6 Annex A registries of UIDs, well-known frames of reference, and the UIDs of context groups and templates (Tables A-1 to A-4); the PS3.16 coding schemes (Tables 8-1 and 8-2) and context groups CID 7050 and CID 7005; the PS3.4 Storage SOP Classes with the IOD of each (Table B.5-1); and, from PS3.3, the modules of every composite IOD that the pin does not leave out, each table of PS3.3 Annex A titled with an IOD’s name and “IOD Modules”, with the IOD’s Functional Group Macros where its modules include them, and the attribute tables of those modules and Functional Group Macros and of the macros they include, which give each attribute’s Type (D-001).
Rule layers. Rules apply in this order: generated tables (L1), reviewed supplementary rules (L2), then validated user rules (L3).
L1 gives each attribute of Table E.1-1 the action of the selected options that give one, and otherwise the Profile’s, since “a requirement for an Option, when implemented, overrides any requirement for the underlying Profile” (E.1.1). E.3.6 specifies Retain Longitudinal Temporal Information with Full Dates and with Modified Dates as mutually exclusive, so a selection of both is rejected. Otherwise PS3.15 defines no precedence between options, so an attribute to which two selected options give different actions has no L1 action and is reported as a conflict for policy validation (D-007, D-011). In 2026d, the only such options are Retain Device Identity and Modified Dates, on eleven attributes. Policy validation rejects every conflict except these, which only the
tps-importpreset resolves (D-011).L2 covers attributes that Table E.1-1 omits but the de-identifier remains responsible for (E.1.1, Note 1 after Table E.1-1a). These are person names, and dates and times (D-023), by VR; text attributes, such as Beam Name, Dose Comment, and Wedge ID, by a reviewed list, with removal by Type for any it does not cover (D-022); URI and URL attributes, and some sequences and binary attributes, by reviewed rules (D-022); the role of every UI attribute (D-003); and the role of every temporal attribute (D-007). A test fails if the pinned dictionary contains a date, time, datetime, person name, or UI attribute with no rule.
The L2 file
supplementary_actions.tomlgives an action of Table E.1-1a, with a note saying why, to attributes that Table E.1-1 omits, other than UI attributes, which have roles instead (D-003). Every person name that the table omits has one: in 2026d, Secondary Reviewer Name and Evaluator Name, which PS3.6 marks DICONDE and no supported IOD uses, get X/Z/D, and the retired Document Author (Trial) gets X, as the table gives the other (Trial) person names. Burned In Annotation and Recognizable Visual Features are kept (K), as D-015 requires. So does every text attribute that the table omits and a supported IOD uses (D-022). URN Code Value and Coding Scheme URL are kept (K) (D-022). Every date, time, and datetime that the table omits has one too, besides its role (D-007, D-023). Like a row of the table, a rule can also give an action under an option, which overrides its Basic Profile action when that option is selected: for a text attribute, only C under Clean Descriptors and K under Retain Device Identity; for a date, time, or datetime, only K under Retain Longitudinal Temporal Information with Full Dates and C under Modified Dates; and only over an action that removes or replaces the value. Policy composition resolves these option actions as it does the table’s, and rejects a rule to which two selected options give different actions (D-011). The file never covers an attribute that Table E.1-1 lists.L2 only strengthens L1. It never retains a value that L1 removes, replaces, or cleans, unless a selected option permits it.
Each element is decoded with the VR that the pinned dictionary gives its attribute, not with pydicom’s dictionary, and then handled by its rule. A value of VR UN is decoded as Implicit VR Little Endian, whatever the transfer syntax (PS3.5 Section 6.2.2), and as a sequence only if items fill it, since pydicom reads malformed items without complaint. The element decoder, the first pass’s reference records, the removal of private attributes, and the pixel data risk indicators pass each sequence value that pydicom holds undecoded through
pymedphys._dicom.deidentify.sequences.decode_itemsbefore they read its items, which has pydicom decode it only oncefile_layout.reads_as_itemsfinds that items fill it, and each refuses one that is not only items by the sequence’s path, without quoting a value; a test keeps any other module of the engine from having pydicom decode items itself. A VR in the file must be one that the dictionary gives, and must not contradict the one that the standard decides of alternatives. Of alternatives, US or SS follows Pixel Representation (0028,0103) in the nearest data set that has it (PS3.3 Sections C.7.5.1, C.7.6.3, and C.11.1.1.1), but for LUT Descriptor (0028,3002) in VOI LUT Sequence (0028,3010), PS3.3 Section C.11.2.1.1 has the input to the VOI LUT decide it: Pixel Representation “if there is no Modality LUT or Rescale Slope and Intercept specified”, SS “if the possible output range after application of the Rescale Slope and Intercept may be signed”, as Hounsfield Units always can, and “US otherwise”, which includes the output of a Modality LUT, “always unsigned” (Section C.11.1.1.1). There, in explicit VR, either is accepted, since “the explicit VR actually used is dictated by the VR needed to represent the second Value”. In Presentation LUT Sequence (2050,0010), “the Value Representation of the second Value is always US” (Section C.11.4.1). Whatever the VR, “the first and third values are always by definition interpreted as unsigned” (Sections C.11.1.1.1 and C.11.2.1.1), so they are decoded and written as unsigned where the VR is SS, and a table of 40000 entries keeps its count. Real World Value First Value Mapped (0040,9216) and Last Value Mapped (0040,9211) are SS for floating point pixel data (Section C.7.6.16.2.11.1.1), which the first supported release’s IODs do not have, so without a VR in the file they cannot be decoded where Pixel Representation is absent. Without a VR in the file, OB or OW, and US or OW, are OW, as PS3.5 Section A.1 gives Pixel Data, Overlay Data, and Waveform Data, whose bytes are the same either way; and in explicit VR, Pixel Data is OB if encapsulated and OW where Bits Allocated (0028,0100) is more than 8 (Sections A.2 and A.4). An element whose VR is not one the dictionary gives or contradicts the one the standard decides, whose alternatives nothing decides, or whose value does not decode exactly with its VR, such as a DS that is not a number, cannot be decoded. Elements are decoded from data sets asdcmreadreturned them: once pydicom has decoded an element read from a file, as indexing, attribute access,get, and iteration do, it holds no encoded value to check, and pydicom has chosen any VR in doubt itself and read a UN sequence’s items unchecked, so such an element cannot be decoded either. Specific Character Set (0008,0005), whichdcmreaddecodes itself, and a sequence of undefined length, which it reads as items, are the exceptions. Building the first pass’s records decodes elements in this way, so the second pass reads each file again rather than reuse those data sets. What happens to an instance with an element that cannot be decoded depends on that element’s rule. As the maintainer decided on 1 October 2026, text with bytes outside the Default Character Repertoire where no Specific Character Set applies sequesters its instance only where the text would be kept or cleaned; where its rule removes or replaces it, the instance is de-identified. As the maintainer clarified on 2 October 2026, that transformation proceeds, but whether the output may be released depends on the coverage of the residual search (D-010, D-027). Decoding, reading the first pass’s records, adding the de-identification markers, and writing output redact pydicom’s warnings and log records, including those of loggers belowpydicomand debug records, with the engine’s ownredacted_diagnostics, only in the thread that calls pydicom and only while it does, and without adding to Python’s process-wide warning filters. Attributes absent from the pinned dictionary and without an L2 rule are removed. Retired attributes follow Table E.1-1 where listed and L2 otherwise.As the maintainer decided on 1 October 2026, the engine admits sources strictly, decodes selectively, and verifies what it preserves (B+); it is not a general DICOM normaliser. A source file is admitted only once its whole structure is read from its own bytes, independently of pydicom: the preamble, the File Meta Information, every element’s header and length, rising tags in each data set and item, so that no tag repeats, each item and delimiter, a bound on nesting, and Data Set Trailing Padding, with nothing after the last element, in Implicit or Explicit VR Little Endian. Every Value Length and Item Length but the undefined length must be even (PS3.5 Sections 7.1.1 and 7.5), in the File Meta Information and at every depth of the data set, although the layout reader used to search written files for residual values reads odd lengths, so that nothing in such a file escapes the search. The engine keeps an immutable copy of those bytes as the source evidence, with where each element’s Value Field is and its VR as written; a pydicom data set is only a mutable view of it, read afresh when needed. An element read for a decision is decoded from the evidence’s Value Field, so pydicom decoding it on access changes neither the evidence nor what the engine reads. An element still encoded whose bytes or VR differ from the evidence, an element set in memory, and a decoded sequence whose items differ in number from the source’s cannot be read as source. An element that pydicom has already decoded is read from the evidence whatever value the view now holds, since pydicom keeps no encoded form to compare, so the view is never the authority on a source value. Source text is decoded in the character set that the source declares, whatever the view’s Specific Character Set now holds. Everything else is refused with a reason code and the offset where reading stopped, never a value.
The engine verifies what it preserves from the bytes of each written file’s data set, including Data Set Trailing Padding, read as a source is, against the plan for its instance: which elements, by path, were kept, which emptied, replaced, or introduced, and which removed, with everything inside a removed sequence. Paths number items as the source does, so an item is never removed on its own. The preamble and File Meta Information are replaced (D-025) and not compared. An unchanged transfer syntax is necessary but not sufficient. The written file must have its source’s transfer syntax, since the first supported release does not transcode; every source element must be planned once; nothing outside the plan may be written; every kept element must be in both files; and every emptied, replaced, or introduced element must be written. A kept element must have the VR as written in its source, which is absent in implicit VR, and, unless it holds items, the same header length and length form and the same Value Field, byte for byte; a kept sequence must have as many items, and its items’ elements are verified by their own paths, so its length may be defined in one file and undefined in the other. A kept private element, including a private sequence or a private UN that holds items, must have a Private Creator with the same bytes in its own data set, or none in either file. Kept text of SH, LO, ST, LT, UT, UC, or PN, or of a VR not known, must also be in a Specific Character Set with the same bytes, that of its own data set or of the nearest item or data set that holds it and has one (PS3.5 Section 7.5.3); an empty one counts as absent, since both give the Default Character Repertoire. Nothing is decoded to compare: two encodings that decode to the same value, such as
1and01in an IS, differ. A kept value of undefined length without items, such as fragments, cannot yet be verified. Each failure names a reason and an element path, never a value, and its instance is to be sequestered. Emptied, replaced, and introduced values are validated before they are written.The engine therefore writes each data set from its source evidence rather than through pydicom’s data set writer, which resolves an ambiguous VR, such as US or SS, from the data set it writes wherever it holds the VR as ambiguous, as for an element read from implicit VR or from the items of a UN sequence. Once a data set has been walked, pydicom writes a VOI LUT Sequence (0028,3010) that the source holds as UN as SQ, with the LUT Descriptor (0028,3002) of a CT image in Hounsfield Units as US rather than SS, which verification refuses. The engine’s writer copies each kept element, header and value, from the source’s bytes, including a sequence or other element holding items when nothing in it changes, so that an opaque value of defined length that happens to read as items is never rewritten as a sequence; it leaves out each removed element with everything in it. It writes each kept sequence with a change in it with its tag and VR as written and an undefined length, and each of its items with an undefined length, ended by the Item and Sequence Delimitation Items (PS3.5 Sections 6.2.2, 7.5.1, and 7.5.2), so that no length is worked out again when an element in an item changes; every item is written, even one whose elements are all removed. Only replacements and introduced elements are encoded, each by pydicom’s writer for one element, with the VR it is given and the Specific Character Set in force at its place in the output, and placed in ascending order of tag; an introduced sequence’s items are encoded in the same way, from their elements as held, without pydicom resolving any VR. A replacement is refused if it is still encoded, has an ambiguous VR, is a File Meta, command, or group length element or a delimiter, would be written with another VR, as pydicom writes UN for a value too long for its VR, or has text that cannot be written in the character set in force, which must be one the element decoder accepts (item 5), or, for a VR of the Default Character Repertoire alone, outside ISO 646. A plan is refused where it keeps an element inside one that is removed or replaced, keeps a group length, which could go stale, or changes the character set in force over kept text. A kept sequence stored as UN stays UN, with its items in implicit VR (PS3.5 Section 6.2.2), even where an element in them changes: its kept elements stay byte for byte as the source has them, and a reader resolves each VR in its items from its dictionary, as it does for the source. Rewriting it as SQ with explicit VR items would give every kept element in it a new header, and needs a VR for each, which a private attribute may not have. Where no VR is written, in implicit VR or in such items, a replacement is refused unless the pinned dictionary gives its VR alone, since an ambiguous VR such as US or SS, or another VR, could be read otherwise. A kept value of fragments is refused, since verification could not show it preserved. The File Meta Information and preamble come from D-025.
An attribute of the pinned dictionary that no rule covers, and that the engine does not itself remove or write (D-012, D-013), is kept if its VR is CS, AT, or a numeric VR, and a sequence or a binary value is kept only where the IOD defines the attribute at that place, with the attributes in a kept sequence’s items handled by the same rules. Such an attribute of any other VR is removed (D-022).
Content that cannot be decoded inside a sequence that is removed is removed with it, and its values are listed as not collected in the confidential QC material, which makes the collection for the residual search incomplete (D-027). An instance is sequestered only where content that cannot be decoded would be kept or cleaned.
Each data set is planned before any of its values is read, from the source evidence’s structure alone: each element’s rule and action at its place, with compound actions resolved by Type, and what must read its value. The readers are UID replacement, the comparison of a dummy value with the source’s, cleaning, and residual collection, which takes every value that is removed or replaced, those of a removed sequence’s descendants included, whatever its VR, leaving what is searched to the residual search’s own rules (D-027). A sequence is descended only under K or U, and its items’ elements take their own rules; under any other action, its descendants go with it and take no rule of their own. The plan sequesters the instance where a dummy value is to be written but neither the VR nor a reviewed rule gives one (D-021), and where an attribute that is not removed is written with a VR, other than UN, that the pinned dictionary does not give it. Only the values that a reader needs are then decoded.
Compound actions such as X/Z/D are resolved from the strictest Type that the IOD gives the attribute at its place in the data set, as are plain actions where the IOD requires the attribute there or does not define it there (D-020). Z and D write the values that D-021 sets.
L3 rules are validated as D-011 requires.
Each element takes the rule of the first of these that covers it: the engine’s own removals, of groups 0000, 0002, and 0004 within a data set, group lengths (gggg,0000), and Data Set Trailing Padding (item 7, D-016, D-025), and of Encrypted Attributes Sequence (D-013); Table E.1-1’s Private Attributes row, for an odd group other than 0001, 0003, 0005, 0007, and FFFF, which PS3.5 Section 7.8.1 does not allow for private use; the table’s other rows, by exact tag and then by masked tag, where (50xx,eeee) and (60xx,eeee) match only the repeating groups 5000 to 501E and 6000 to 601E (PS3.5 Section 7.6); the L2 rules; U by the role of a UI attribute (D-003); removal by Type of a text attribute (D-022); the rule for attributes that no rule covers (D-022); and, for an attribute absent from the pinned dictionary, removal. A sequence to which the policy gives C takes its Basic Profile action (D-009). A test fails if a person name, date, time, datetime, or UI attribute of the pinned dictionary reaches the rule for attributes that no rule covers.
Identifiers. Keyed replacement of UIDs (D-003) and patient identifiers (D-005), using keys and subject profiles held by a custodian (D-004).
Dates and times. Per-subject shifts (D-006), handling of each temporal attribute by its role (D-007), and stable synthetic birth dates for
tps-import(D-008).Free text. Descriptor cleaning (D-009) and the reviewed rules for text attributes that Table E.1-1 omits (D-022). Text is decoded according to the Specific Character Set (0008,0005) of its data set, or of the sequence item that has its own (PS3.5 Section 7.5.3), before matching, cleaning, or hashing. An instance whose Specific Character Set is not supported or is invalid is sequestered (D-010). Specific Character Set is accepted only as the Defined Terms of PS3.3 Section C.12.1.1.2 that pydicom can decode, combined as that section and PS3.5 Section 6.1.2.4 allow; in 2026d this leaves out ISO_IR 203 and ISO 2022 IR 203. ISO_IR 6 is also accepted alone: that section gives the Default Character Repertoire by the absence of a value, not by ISO_IR 6, but real data commonly holds ISO_IR 6, which names the same repertoire, so it is read as an absent value is, and decoded just as strictly. With other values it is refused, since ISO 2022 IR 6 is the Defined Term for that repertoire with code extensions. Text that does not decode exactly in its character set cannot be decoded; where the Default Character Repertoire is the only character set, that includes any character outside ISO 646 (PS3.5 Section 6.1.2.1), although pydicom reads that repertoire as ISO 8859-1, and item 2 sets out when such text sequesters its instance. Nor is reading more lenient than writing: text that D-011’s check of a written value finds could not be written back in its character set cannot be decoded either, such as a kanji in Shift JIS where Specific Character Set is ISO_IR 13, which is single-byte. That check encodes each whole value as pydicom’s writer does, with the codec it chooses, and reads it back: a value that does not read back as the same single value, such as Latin letters and katakana together in ISO_IR 13, which pydicom writes with replacement characters, or a yen sign there, which JIS X 0201 places at the backslash that separates values (PS3.5 Section 6.1.2.3), cannot be written. Replacement values are encoded in the dataset’s character set, and the dataset is converted to UTF-8 (ISO_IR 192) when a value cannot otherwise be encoded. The residual search (item 8) encodes each decoded source value again, in its source character set, UTF-8, ISO 8859-1, and UTF-16LE. Text with bytes outside the Default Character Repertoire that no Specific Character Set applies to is read for the search as ISO 8859-1, in which every byte is a character, so encoding it again in ISO 8859-1 gives its own bytes, and a copy of it left in the output is still found. That byte search supplements the coverage but does not stand in for the decoded text, whose copies in other character sets are not searched, so the value counts as read only as bytes and its collection as incomplete (D-027).
Private attributes. Removed unless the Retain Safe Private Option is selected.
Without that option, every element of an odd group is removed, at the top level and in the items of every standard sequence, and a private sequence is removed with everything in it (D-028). A policy that selects the option is refused until reviewed rules say which private attributes are safe.
With that option, a private attribute is retained only when shown to be safe by a route E.3.10 allows. It is matched by private creator, whichever block it occupies (MIDI §1.17.3), and the basis for retaining each element is recorded.
Table E.1-1 gives retained private attributes C, so their values are still cleaned: UIDs by D-003, dates by D-007, and text as descriptors (D-009).
Deidentification Action (0008,0307) is followed where present, as E.3.10 requires.
A private sequence not known to be safe is parsed and each nested attribute is handled on its own merits.
File container.
The engine replaces the File Meta Information and the preamble, as E.1.1 requires: it builds the File Meta Information itself, describing the de-identifying application as D-025 sets, and writes a zeroed preamble (D-002).
It removes group 0004 from every file other than a DICOMDIR, and removes Data Set Trailing Padding. It also removes every element of group 0000, the DIMSE command set (PS3.7 Section 6.3), wherever it appears in a data set, as the maintainer decided on 1 October 2026: a command element has no place in a stored instance, and one such as Requested SOP Instance UID (0000,1001) would otherwise carry a source UID that no UID role replaces. A source DICOMDIR is never passed through, and none is written, since the output is a plain directory tree rather than a File-set (D-016); E.1.1 would allow one only if regenerated from the de-identified files.
Encapsulated Document (0042,0011), which Table E.1-1 gives D, is replaced, or the instance is sequestered, where the IOD defines it at that place. Elsewhere, as in every IOD of the first supported release, it is removed, since D acts as X on an attribute that the IOD does not define at that place (D-020).
Metadata in compressed pixel data bitstreams, such as JPEG APPn segments, is removed without recompression.
A run lists every entry below its source directory, in an order that depends only on their paths relative to it, and names each input by its position in that order, its run position. It follows no symbolic link or other link, opens no file that is not a regular file, and reads a file only while it is still the one that it listed.
Output file and directory names are built only from replacement identifiers, never from source paths, file names, or attribute values. Each instance is written to
<Patient ID>/<Study Instance UID>/<Series Instance UID>/<SOP Instance UID>.dcmbelow the output directory, from its replacement values (D-016).De-identification markers follow D-012.
Referential integrity.
A first pass builds a graph of instances and references. It reports dangling references, frame of reference mismatches, and inconsistent hierarchy using opaque identifiers, before anything is written. It follows the reference attributes that the PS3.3 tables define inside a sequence of the instance’s IOD, and resolves each against the inputs’ SOP Instance, Series Instance, or Study Instance UIDs, by the level of what it names. The attribute gives the level, except that Referenced SOP Instance UID names a study in the items of Referenced Study Sequence and RT Referenced Study Sequence, whose Referenced SOP Class UID is the study’s own class, such as the retired Detached Study Management (PS3.3 10.6.1 and C.8.8.5.4). An empty value where the IOD makes the attribute Type 3 means the same as an absent one (PS3.5 7.4.5), so it is no reference; an empty Type 1 value is invalid and an empty Type 2 value is unknown (PS3.5 7.4.1 and 7.4.3), so each names nothing, and is dangling unless the class rule below applies. Where the IOD’s definitions give an attribute several Types at one place, the strictest applies, with 1C as 1 and 2C as 2. A reference to an instance whose Referenced SOP Class UID is not a Standard Storage SOP Class of PS3.4 Table B.5-1, such as a Private or retired SOP Class, resolves if it names an input; otherwise it is not reported, because such a class may name something that is not a stored instance, such as a performed procedure step, or a patient through a retired or private management class. The class rule does not apply to a reference to a study, which is dangling unless an input has that Study Instance UID. A UID that the pinned tables register is never dangling. Findings name inputs by their position in the run and attributes by their tags, and contain no values.
The first pass also reports an inconsistent hierarchy: instances that share a SOP Instance UID, as identical duplicates where their source bytes are the same and as conflicting copies otherwise; a series whose instances name several Study Instance UIDs; and a study whose instances name several patients, each the identity of a Patient ID with its issuer (D-005). The instances without a Patient ID together count as one more patient, apart from every identity (D-026), and an instance without the UID that a check compares is left out of that check, since it is already reported as missing that identifier. The first pass also reports a frame of reference mismatch: a series whose instances name several Frame of Reference UIDs (0020,0052), though “each Series shall have a single Frame of Reference UID” (PS3.3 C.7.4.1.1.1); an RT Structure Set ROI whose Referenced Frame of Reference UID (3006,0024) has no value or is not listed “once and only once” in the structure set’s Referenced Frame of Reference Sequence (3006,0010) (PS3.3 C.8.8.5.1); and an input image that an item of that sequence lists in its Contour Image Sequences under a frame of reference other than the image’s own, since the frame of reference of a set of ROIs is “obtained from the source images” (PS3.3 C.8.8.5.1, Note). An input or item without a Frame of Reference UID is left out of the comparisons it would take part in. A run reports each such mismatch only, as it does a dangling reference (D-026). An instance’s source bytes are its Transfer Syntax UID (0002,0010) and the bytes of its data set from the end of the File Meta Information to the end of the file, leaving out each group length (gggg,0000), which PS3.5 7.2 retires, and the Data Set Trailing Padding (FFFC,FFFC) at the top level of the data set; the preamble and the File Meta Information are not part of the data set (PS3.10 7.1). A group length or padding inside an item is compared as it is. Copies are identical only when their source bytes are the same (D-026). The first pass finds the bytes with PyMedPhys’s own reader of a file’s structure, which decodes no value, so no decoder or guessed VR takes part in the comparison. Copies that differ only in their encoding therefore conflict, which sequesters them: copies in Implicit VR and in Explicit VR Little Endian, with sequences or items of defined and of undefined length, with values padded differently, or with compressed and uncompressed Pixel Data. A copy whose bytes cannot be shown to be sound conflicts with every other copy: one whose structure cannot be read to the end of the file, one without a Transfer Syntax UID, one whose transfer syntax deflates the data set or is the retired Explicit VR Big Endian, and one with a top-level group length or Data Set Trailing Padding that holds items, whose elements would otherwise be compared as if they were at the top level. A file in which a sequence that the first pass follows does not hold only items, including any such sequence in Explicit VR Big Endian, which only pydicom can read, has no record, since its references cannot be followed; the first pass refuses it by the sequence’s path. The first pass reads each file in full, and compares SHA-256 digests of the source bytes, which it keeps only for the run.
After writing, the engine verifies one-to-one UID mappings; consistent references; consistent values for each entity above the instance level, such as Patient ID, Study ID, and Series Number (E.1.1 step 2, Note 3); absence of original UIDs where replacement or removal is required; and unchanged values where retention is required. A run checks the UIDs and references of the files that its gate releases against the first pass before it publishes anything. A finding there is a fault of the engine, so the run sequesters each instance that it names and checks the rest again; a reference to an instance that was withheld is reported only, as a dangling reference is (D-026); and a check that fails, or a finding that no sequestration isolates, publishes nothing.
It searches every byte of each written file for the source values that had to be removed or replaced: the instance’s own, and its subject’s from the run’s other instances. It reports each residual, a source value found in a form the search derives, by the value’s kind (person name, UID, date, datetime, or other text), its source attribute path, the form and encoding found, and its place: the preamble, a File Meta Information element, a data set element by its path, with sequence items numbered from 0, Data Set Trailing Padding, or the bytes after the last readable element. It also lists each form it did not search, and why. Neither list contains a source value (D-016). The search itself decides nothing: output is written to a staging area, and the release gate (
pymedphys._dicom.deidentify.release_gate) runs the search of each written file itself, for exactly the values collected across the file’s subject, and decides from the search and the coverage of that collection whether the file is released, goes to QC review, or is withheld, with the consequences that D-027 sets. A search that finds nothing is not on its own a release.Forms. Each value is searched as given, composed (NFC), decomposed (NFD), with compatibility characters replaced (NFKC and NFKD), and with each character that has a half-width or full-width form written in its other width, such as
ヤマダforヤマダorYAMADAforYAMADA. It is split at backslashes unless its VR is LT, ST, UR, or UT, and stripped of spaces, NULs, and whitespace at either end. A person name is searched whole, by each component group, by its family, given, and middle names, by each word of those names between spaces or hyphens, and with its family and given names run together in either order, as Chinese, Japanese, and Korean names are usually written. Where the family or given name has fewer than four characters, they are also searched in either order with a space or an ideographic space (U+3000) between them, and as “family, given”. A name with an apostrophe, whether ASCII’s or U+2018, U+2019, or U+02BC, is also searched with ASCII’s, with U+2019, and without it. A date, including one written as YYYY.MM.DD, as ACR-NEMA wrote dates, is also searched as YYYYMMDD, YYYY-MM-DD, YYYY:MM:DD (as EXIF writes dates in bitstream metadata), DD/MM/YYYY, MM/DD/YYYY, and DD.MM.YYYY, and a datetime by its date in each of these. A UID or other text is searched as it is, by up to its first 256 characters.Not searched. Forms of fewer than four characters, counted once composed (NFKC); binary values; values that cannot be decoded, which are removed with the sequence that contains them (item 2), apart from text with bytes outside the Default Character Repertoire that no Specific Character Set applies to, which is read as ISO 8859-1 (item 5); datetimes without a full date, such as
2024, which would match any number that contains it; and values of VR AS, AT, CS, DS, FD, FL, IS, SL, SS, SV, TM, UL, US, and UV, whose codes, numbers, ages, times, and tags occur throughout files and are checked attribute by attribute instead. Each is listed as not searched (D-027).Not collected. As the maintainer clarified on 2 October 2026, a value that cannot be decoded is never collected, so it is also listed as not collected, as text read only as ISO 8859-1 (item 5) is listed as read as bytes, and both make the collection incomplete (D-027).
Constants. A value that equals a constant the engine writes whatever the source held is not searched, since finding it would reveal nothing about the source. The constants are D’s constants and second constants of the VRs that are searched, the values of the item that D writes in Person Identification Code Sequence (D-021), and the markers’ fixed values: Patient Identity Removed, Longitudinal Temporal Information Modified, Manufacturer, and the Code Value, Coding Scheme Designator, and Code Meaning of each code that the markers can write (D-012). Keyed values, such as replacement UIDs and pseudonyms, and the markers’ versions and method digest are not constants. A value is compared with each constant as D compares a source value with its constant, as text of the source value’s VR (D-021), so that
Deidentifiedand1900.01.01are skipped; a value of several values is compared value by value, and only those equal to a constant are skipped. Each form that the search derives from a value, such as a person name’s family name or a datetime’s date, is compared in the same way, and a form equal to a constant is skipped while the value’s other forms are still searched. Each source attribute skipped in whole or in part is recorded once by its attribute, never by its value (D-027). A value of a VR that is never searched is listed as not searched for that reason instead.Encodings and case. Each form is searched in UTF-8, ISO 8859-1, UTF-16LE, and its source character set (item 5). In the source character set, a form can also be spelt with ISO 2022 escape sequences, whose bytes depend on the text around them: where the character set’s codec writes them itself, or, for a form that is not ASCII, where Specific Character Set has more than one value, so that a writer puts one before each component in another character set (PS3.5 Section 6.1.2.5.3). Such a form is still searched as the codec encodes it, which finds a single component after its escape sequence, or text written without them, as pydicom writes ISO 2022 IR 58 text; it is also listed, since its spellings with other escape sequences are not searched. Where the codec designates a character set before the form and resets after it, as ISO 2022 JP does, the bytes between them are also searched, as a longer run in that character set, such as the form followed by an honorific, writes them. ASCII letters match in either case, and text with other letters is also searched in upper, lower, and title case; a capital inside a word on a letter outside ASCII, as in a McDonald-style spelling in Cyrillic, is found only where the whole form is in one of those cases. Case is folded byte by byte, so a byte of an ASCII capital inside a character of several bytes, as in UTF-16LE, Shift_JIS, or GBK, is folded too, and some text outside ASCII matches a form by chance; this errs towards a finding.
Matches. Letters and digits are those of ASCII, and in UTF-16LE each neighbouring character is two bytes. A match does not count where a digit precedes a form that starts with a digit, as in a 39-digit component of a
2.25.UID; where a digit follows a form that ends with one, unless the form is a date or datetime, which a time may follow, or the first 256 characters of a longer value; where a letter adjoins a person name form at an edge that is a letter, as “MARY” in “PRIMARY”; for a form of digits alone, inside a DS or IS value, such as contour data; or, for a form of digits alone that a digit follows, inside the integer of a UI value under the2.25.root (PS3.5 B.2), which the engine uses for keyed replacement UIDs, so that a date at the start of a keyed replacement UID is not a finding. That rule recognises the root, not who wrote the UID, so it applies to a source UID kept under that root too, and should be reassessed before a preset keeps source UIDs; digits that make up the whole integer, and UIDs under other roots, are still found.Values that hold numbers. Native Pixel Data, Float Pixel Data, and Double Float Pixel Data of the top-level data set, and values of VR OD, OF, OL, OV, and OW, are searched only for forms of at least 8 bytes that are not UTF-16LE, since UTF-16LE text and shorter forms match sample values by chance. Every other byte, including encapsulated fragments, OB and UN values, and bytes that cannot be read as elements, is searched for every form. Before compressed transfer syntaxes are supported, how often short forms match compressed pixel data by chance is to be measured, and this rule for fragments reviewed.
Reports. Each source attribute is reported once for each place it is found, by its widest form there, with the encoding and byte offset of that form’s first match there. A file whose structure cannot be read to its end is still searched in full, and reported as not fully readable.
It reads each written file’s structure independently of the library that wrote it, so that anything verification finds is located by element path, or in the preamble, the File Meta Information, Data Set Trailing Padding, or the bytes after the last readable element.
The first pass’s findings have graded consequences, from reporting a dangling reference to stopping the run for a study with several patients (D-026).
Risk detection and quality control. Indicators for burned-in text and reconstructable faces (D-015), a restricted QC pack for human review (D-016), and statistical disclosure control (D-017).
Reports.
Each run writes a machine-readable and human-readable release report, which contains no source values or original paths (D-016) and names released instances only by their output names and sequestered instances only by labels (D-026). It records the method digest with its components (D-024) and the runtime environment that ran (D-029). Its human-readable form,
release-report.mdbesiderelease-report.json, is CommonMark, with GitHub Flavored Markdown tables, generated from the JSON text alone, which shows each of its values as a code span and draws nothing else from it, so it can hold no value or path that the JSON lacks. Its machine-readable form is a JSON document labelled with its format,pymedphys-deid-release-report/10, whosepolicysection names the PS3.15 edition, the preset (none for a custom option set), the selected options, and whether the policy can claim conformance, and whosemethodandruntimesections hold the fields that D-024 and D-029 list. Itsqc_reviewsection gives the run’s QC pack by its opaque reference, with the outcome of its attestation (attested, rejected, or not attested) and the releaser’s two confirmations in it, that the output was checked for its intended use and that its residual risk was accepted, each true, false, or null where not stated and null without an attestation, or is null for a run that wrote no QC pack (D-016). Itsreleasedsection lists the output name of each released instance, sorted (D-026). Itssequesteredsection names each sequestered instance by a random label for the run, such asS-0007, with each reason: the stage that sequestered it (scope, admission, which refuses a source file and sets it aside, the first pass’s references, the walker, the run’s own checks, the transform, including descriptor cleaning, the writer, the check that the source is preserved, or the release gate), the stage’s reason code, the attribute’s tags without items from the walker, and from the release gate where its reason names one, and, from the walker only, the action and the VR (D-026). Itsheld_for_reviewsection counts the instances held for review by stage (descriptor cleaning’s ROI names, or the release gate) and reason code, each instance once for each reason however many of its names or attributes have it, and names none of them (D-009). The labels areS-0001toS-nfornsequestered instances, in an order drawn at random. Itsroi_namessection counts the ROI Names of the structure sets that descriptor cleaning cleaned in the run, each name by its outcome (renamed, empty, kept, mapped, emptied, held, or emptied unreviewed), and the distinct names sent for review, whether then held or emptied unreviewed, each once for each of its reasons, as the confidential QC pack lists them, and names none of them (D-009). Itsreference_findingssection counts the instances that have each kind of first-pass finding that the run reports without acting on it, such as a dangling reference, each instance once for each kind, an identical copy counting as the instance it copies, and names none of them (D-026). Itspixel_riskssection counts the released instances, and those held for review, that show each risk in their pixel data (burned-in text or a reconstructable face), and each indicator of it, among them a CT volume, the head or neck, and unreadable evidence, each instance once for each, an identical copy counting as the instance it copies, as the confidential QC pack lists them, and names none of them (D-015). Itssearch_coveragesection counts the source values that the residual search did not search, in full or in part, by attribute and reason, each value once for each reason however many of its forms that reason left out, at the instance that holds it, although each search of its subject’s files looks for it (D-027). Itssource_gapssection counts, by attribute and Type, the instances whose source lacks an attribute that their IOD unconditionally requires, which the run reports and never acts on (MIDI-BP-03); the QC pack lists each by instance and place. Itsstructural_checkssection summarises the engine’s checks that the release is well formed and refers to itself as the source did, as the maintainer decided on 6 October 2026: for the first pass’s references (references), the transform’s check that each written instance keeps the attributes its IOD requires where its source had them, with the source’s own gaps (iod-requirements), and the second pass’s check of what was written (written-references), always in that order, how many instances the check sequestered, which the report refuses to put above those that itssequesteredsection lists with the check’s reasons, and how many it reported without acting on them, each instance once, an identical copy counting as the instance it copies, naming none; the QC pack lists each reported finding by instance, including a written reference to an instance that was not written (D-026). Every field is built from the policy, the engine’s own files, the versions that run it, the keyed digest of the reviewed-names list, and codes and tags that the engine defines, never from a DICOM value directly, and as a backstop, writing the report refuses any field that is not a digest, a version, a known edition, preset, or option, a file name or path within the engine’s package, a QC pack’s reference, an attestation outcome, or a confirmation given only with an attestation, an output name listed once, one of the run’s labels, a path of tags, a code that the engine defines, a positive count, or a structural check’s count from 0. A change to its fields takes a new format label.Each run also writes a conformance statement generated from the rule tables and the effective policy, beside the release report at the root of the release, since it holds no instance value. It covers everything E.1.3 requires, the descriptions that E.3.5, E.3.6, E.3.7, and E.3.10 require for selected options, and each interpretation recorded in this document.
It is generated as CommonMark from the policy, the pinned tables, and the engine’s parameters, so it is regenerated whenever they change and never edited by hand, and the same inputs always give the same text. It names tags, actions, and parameters, never a value from an instance. It lists the action that the engine applies to each attribute, with each compound action, and each plain X, Z, and D, resolved by Type at every place where a supported IOD defines the attribute, other than within a sequence that is removed with its contents (D-020), and says why where that is not the policy’s action, as for the engine’s own removals and a sequence to which the policy gives C. It describes the rules that give every other element its action (D-022), the values that Z, D, and U write (D-003, D-005, D-021), how dates and times are handled without a Retain Longitudinal Temporal Information Option (D-006, D-023), what the residual search covers and lists as not searched (D-027), and how the release report names sequestered instances by labels and counts the values that the residual search does not search by attribute and reason, with per-instance detail only in the confidential QC pack (D-016, D-026, D-027), how ROI Names are cleaned where the policy gives ROI Name C (D-009), gives the policy’s method digest (D-024), and describes the markers that each instance gains as the engine computes them for the policy (D-012).
Where the engine does not yet decide something that the statement must describe, such as the manner of cleaning an attribute to which the policy gives C other than under Clean Descriptors alone, as under Modified Dates, or a run under a policy whose other options cannot be composed without Clean Descriptors, the statement lists it under “Not yet described” and makes no conformance claim. Where an IOD requires an attribute to which the policy gives a plain X, the statement names the enclosing sequence or overlay group that D-020 removes with it, or the sequestration that it decides instead, as the walker plans them. A preset is enabled only once its statement lists nothing there.
Each release publishes a traceability matrix from requirements to tests (D-018). The documentation shows the register’s part of it, without test results, as the requirements matrix, which every documentation build generates.
Presets#
Preset |
Intended use |
Profile and options |
|---|---|---|
|
Baseline transformation |
Basic Profile, with no options |
|
Baseline transformation that keeps cleaned descriptors, such as structure names that match TG-263 |
Basic Profile; Clean Descriptors |
|
Copies for non-clinical treatment planning system databases, such as research copies of patient data, or duplicates of phantom data that must coexist without UID conflicts |
Basic Profile; Retain Longitudinal Temporal Information with Modified Dates; Retain Patient Characteristics; Retain Device Identity; Clean Descriptors |
|
Collections being assessed for unrestricted public sharing |
Basic Profile; Retain Longitudinal Temporal Information with Modified Dates; Clean Descriptors; Retain Safe Private |
These are the target option sets. Each preset’s policy is composed from its options and validated before processing (D-011). A preset is enabled only once its behaviour is implemented and validated for the documented input scope. No preset is enabled yet; the first supported release is to enable
basicandbasic-clean-descriptors. A custom option set is validated in the same way, except that every conflict between its options is rejected, so its policy can claim conformance, unlike that oftps-import. A custom option set is not enabled, and no planned release enables one. Selecting a preset is not evidence of conformance (D-011).Profile implementations strictly match DICOM definitions. A preset named after a profile, such as
basicorbasic-clean-descriptors, applies exactly the profile and options its name gives, and any claim a preset makes names exactly the profile and options it applies (D-011).basictherefore stays exactly the Basic Profile, with no options, and the Clean Descriptors Option that the first supported release includes has a preset of its own rather than joiningbasic.tps-importmakes no PS3.15 conformance claim, because its Retain Device Identity and Modified Dates options conflict (D-007). Its output is marked as D-012 describes.tps-importwrites synthetic birth dates (D-008), cleans ROI names asbasic-clean-descriptorsdoes, keeping no name that has not been reviewed apart from those that automatic cleaning writes in the vocabulary’s spelling, and retains other cleaned descriptors only after pooled human review, otherwise applying the Basic Profile’s actions (D-009). Each run with a new key gives UIDs and patient pseudonyms unrelated to earlier copies, so several copies of one source, such as a phantom, can coexist in one planning system (D-003).public-releaseomits Retain Device Identity, because device serial numbers and machine names can identify an institution and, with dates, individual treatments.Open question. Whether the naming principle above should also rename
tps-importandpublic-release, which are named after their purposes rather than after a profile, is not yet decided.Profile conformance and readiness to release are reported separately.
basicandbasic-clean-descriptorsoutput is not thereby ready to share.tps-importoutput is for non-clinical databases only.public-releaseoutput also needs a statistical assessment, pixel and face review, and a human QC attestation (D-016 and D-017).
Decisions#
These are the active design decisions, not statements that the code implements them. Decision identifiers are stable and never reused.
D-001: Tables generated from a pinned edition#
Decision.
A development command generates the L1 tables from the pinned edition of the standard, currently 2026d. It downloads the published HTML pages, verifies each page’s SHA-256 against the pinned digest, and parses them with
html.parser. Most pages are NEMA’s chtml pages, one per section. PS3.3’s module and macro tables span dozens of those, so PS3.3 is read from its single-page HTML, which marks up tables in the same way. It records the edition, the source digests, and a digest of the generated content. The PS3.6 data dictionary and Annex A tables, and the PS3.16 coding schemes and context groups, parse from the HTML as reliably as the Annex E tables, so no DocBook XML parser is needed.The repository and the package keep the generated tables, not the standard’s source files. Each generated file carries the copyright attribution
DICOM PS3.x <edition>, © NEMA, such as “DICOM PS3.15 2026d, © NEMA”.A loader reads each table, checks each row’s fields, types, and actions, and rejects a table whose rows no longer match the digest recorded in the file. This catches a table edited without updating that digest; it does not authenticate the file.
Table E.1-1a must define exactly the action codes the engine implements, so a new edition that adds or removes a code fails generation until the engine handles it.
PS3.3 Types are generated only for the composite IODs, ahead of the releases that support them: each table of PS3.3 Annex A titled with an IOD’s name and “IOD Modules”, such as “CT Image IOD Modules”, 174 tables in 2026d. The first supported release supports four of these IODs: CT Image, RT Dose, RT Structure Set, and RT Plan. Generated Types do not make an IOD supported: its instances are sequestered until a release supports it (D-010). PS3.3 publishes 18 other tables titled “IOD Modules” in 2026d, for the Normalized IODs of Annex B and the Basic Directory IOD of Annex F, and their Types are deliberately not generated. No instance of them is de-identified: Normalized IODs describe the attributes of DIMSE-N services, so their module tables give no Types apart from the SOP Common Module’s, since PS3.4 sets their attributes’ requirements for each service, and the Basic Directory IOD is a DICOMDIR, which is never passed through (D-016). For each IOD, its modules table gives each module’s information entity, section, and usage, with any condition; a section number can end in a letter, as C.7.6.4b does. Each module’s attribute table is the one in that section titled with the module’s name and “Module Attributes”, such as “Patient Module Attributes”. Attribute tables, and every macro table they include, are kept as published, without descriptions: each row’s nesting depth, name, tag, and Type, or the macro an “Include” row adds. Generation checks every tag against the data dictionary and allows nesting only below a sequence, or one level below an “Include” of a macro whose only top-level attribute is a sequence, whose items the nested rows extend, as Table C.18.4-1 “Image Reference Macro Attributes” extends Referenced SOP Sequence (0008,1199) of Table C.18.3-1 “Composite Object Reference Macro Attributes”. A table may include itself below one of its own sequences, as Table C.17-6 “Document Relationship Macro Attributes” does in Content Sequence (0040,A730) and Table C.38.2-3 “Inventory Reference Macro Attributes” does in Incorporated Inventory Instance Sequence (0008,0422), so that content nests to any depth; generation rejects any other cycle of includes.
Errors in the published tables are corrected by named corrections in the pin. Each replaces text that must occur in exactly one row of its table, or in a stated number of rows where identical cells need it, counting its title and header as rows, so a correction fails generation once an edition changes that text. In 2026d there are 18: usages separated from their conditions by an en dash, a hyphen without a space, or nothing; module tables titled differently from their modules, and a macro table titled differently from its section, “Frame VOI LUT With LUT Macro”; two modules listed with the wrong section; a name column headed “Attribute name”; an “Include” row with a space after its “>” characters; and the X-Ray Grid Macro’s Include of the X-Ray Grid Description Macro, published outside the macro’s only sequence, X-Ray Grid Sequence (0018,9555), whose Grid (0018,1166) it describes, which the pin nests in that sequence, as the X-Ray Filter Macro nests its description in its own.
The Multi-frame Functional Groups Module (C.7.6.16), and three other modules like it, include the IOD’s Functional Group Macros in the items of their Functional Groups Sequences: of the Multi-frame Functional Groups Module, the one item of Shared Functional Groups Sequence (5200,9229) and the item for each frame of Per-Frame Functional Groups Sequence (5200,9230). Each IOD whose modules include them lists its macros, each with a usage, in a table of Annex A titled with its name and “Functional Group Macros”, such as Table A.38-2 for Enhanced CT Image, except that the text gives the Enhanced MR Color Image IOD the Enhanced MR Image IOD’s table, which the pin records by the two IODs’ names. Generation keeps the row that includes the macros as an “Include” of them, records the IOD’s macros with their usage and condition, and keeps each macro’s attribute table, the one in its section titled with its name and “Macro Attributes” or “Functional Group Macro Attributes”. It fails if an IOD’s modules include Functional Group Macros and no table lists them, or a table lists them and no module includes them; if such a table names no IOD of Annex A, or an IOD has two; if rows are nested below the row that includes them; and if a macro does not define exactly one top-level attribute, a sequence, or includes Functional Group Macros itself, since each Functional Group is one sequence (C.7.6.16.1.1).
The three real-time IODs, Real-Time Video Endoscopic Image, Real-Time Video Photographic Image, and Real-Time Audio Waveform, are left out: their Current Frame Functional Groups Module gives Current Frame Functional Groups Sequence (0006,0001), which PS3.6 defines in Table 9-1, not in the data dictionary of Table 6-1. The pin names each by its table and name, since table numbers can change between editions, and records the attribute it lacks, (0006,0001). Generation fails if a named table is not the modules table of Annex A of the IOD named with it, if an IOD it names can be generated or fails for any reason other than the attribute recorded with it, or if an IOD it does not name cannot be generated. In 2026d, Types are generated for 171 of the 174 composite IODs, including 30 of the 33 whose modules include Functional Group Macros, such as Enhanced CT Image and Segmentation.
The loader checks both PS3.3 files when it reads them, and expands an IOD’s modules when its Types are first needed, following every “Include”, into each attribute’s Type in each module and within each enclosing sequence, and keeps the result. An Include by which a table includes itself is kept as a reference back to that table rather than expanded again, so the attributes below that sequence have the same Types at every level of nesting, and the loader finds them at any depth. An Include of Functional Group Macros expands the IOD’s macros there, so in both Functional Groups Sequences of the Multi-frame Functional Groups Module, and each attribute defined in a macro names it. A macro’s usage applies to the macro as a module’s does to the module: whether an instance must, under its condition must, or may include the macro’s sequence, in the Shared item or in each Per-Frame item, and the Types of the macro’s attributes apply wherever it is included. A condition, such as Frame Content’s “May not be used as a Shared Functional Group.”, is kept as text, and the macro is expanded in both sequences, as C.7.6.16 defines them. An attribute of a repeating group, such as Overlay Data (6002,3000), matches its group’s definition, (60xx,3000). Different modules can give an attribute different Types, and an attribute the IOD does not define has none.
An instance names its SOP Class in SOP Class UID (0008,0016), not its IOD, so PS3.4 Table B.5-1, which gives the IOD of each Standard Storage SOP Class, is generated whole from its chtml page. The engine finds an instance’s IOD, and with it the Types, from its SOP Class UID without trailing NUL and space padding, as it does before replacing a UID (D-003), since PS3.5 Section 9.1 pads a UID of odd length, such as each of the first release’s SOP Class UIDs, with a NUL. PS3.3 Annex A publishes a modules table for the IOD of every Storage SOP Class in Table B.5-1, and none of them is left out, so each of those SOP Classes has Types. Ten generated IODs in 2026d, such as Hanging Protocol and Color Palette, have SOP Classes that other parts of PS3.4 define, so no SOP Class of Table B.5-1 finds them. Any other SOP Class, including one the table does not list, such as a retired or Private SOP Class, has none and is outside the supported scope (D-010).
Table E.3.4-1 is not generated until the Clean Structured Content Option is designed.
pymedphys dev deid-tables --check-currentdownloads the pinned pages from NEMA’s unpinnedcurrent/directory, generates each table from them with the same development-only parsers and the pin’s corrections, and compares the digest of each table’s rows with the one recorded in the committed table, without writing the tables. It names the edition the pages give, each table that would change, each page that could not be fetched, and each table that could not be generated, with the pages it read and the type of error, never quoting a page. It exits with status 1 when a table would change and 3 when a page could not be fetched or a table generated, and--jsonalso writes the result as JSON. A monthly workflow runs it and opens or updates one issue when a table would change, a page fails, or the check stops without a result. The workflow is defined in.github/workflows/deid-edition-check.yml. Its check job can only read the repository, and only the job that opens the issue, which runs no repository code, can write issues. The check compares generated tables only, so it does not see a change to the Annex E paragraphs that the requirements register transcribes by hand: when the pin moves, the register is compared by hand with every Annex E page of the new edition, and an edition that changes only such a paragraph is not noticed until the pin next moves. Generated files are never edited by hand.
Open question. Whether the generator should also extract each Annex E paragraph that contains “shall”, outside Notes and tables, with any list it introduces and its anchor, as generated content is not yet decided. A test would then check that the register’s PS3.15 entries are exactly those paragraphs, and the monthly check would open an issue when an edition adds, changes, or removes one.
Rationale. The legacy keyword list was transcribed by hand and has drifted: it has 217 entries, drawn from Supplement 142, while Table E.1-1 in 2026d has 657 rows. The standard is revised about five times a year. NEMA holds the copyright in the standard, and each table records that attribution. Table E.3.4-1 also contains SNOMED CT, LOINC, NCDR, NCIt, and UMLS codes, which carry their own terms. The standard library parsers add no dependency and only ever read verified publications, in a development-only tool. PS3.3 tables are kept as published because expanding each macro in each IOD would repeat shared modules and macros: the 171 IODs generated in 2026d expand to about 709,000 definitions, and the first supported release’s four to about 18,600, but the two files that hold them and their 664 tables take 2.0 MB. Expanding each IOD only when its Types are first needed keeps loading the tables quick however many IODs they hold. For the same reason each IOD records its Functional Group Macros, and the module that includes them keeps the row that does, rather than each IOD having a copy of that module with its macros expanded: 29 IODs share the Multi-frame Functional Groups Module, each with its own macros. Table B.5-1 is small (173 rows in 2026d), so it is generated whole. Generating the Types of every composite IOD means that every stored object type’s Types and reference sites are known, so no later release, the reference graph, or the resolution of compound actions is limited by which tables were generated, and instances that are sequestered can still have their references recorded. The costs are larger generated data, review load, and edition-check issues on most editions, since almost every edition changes some IOD. The monthly check compares generated content rather than source pages, so it raises an issue only when the tables would actually change; comparing source digests alone was rejected because it would raise one for every edition, about five a year, even when no generated table would change, which trains people to ignore it. The parser executes nothing from the page, and a page that cannot be parsed opens an issue itself, as does a check that stops without a result, so the check never fails silently. The report quotes no page, so the issue reproduces none of the standard’s text. Errors in the published tables are corrected by name, rather than by a parser that accepts variants, so that an unexpected layout in a later edition still fails generation. The real-time IODs are left out until Table 9-1 is generated too, rather than generated without Current Frame Functional Groups Sequence, so that no consumer can read incomplete Types; for the same reason the loader rejects an IOD whose modules include Functional Group Macros that it does not list.
Tests. Regeneration reproduces the committed tables and digests; a source file with a different digest is rejected; a table whose rows differ from their recorded digest, or whose rows have the wrong fields, types, or actions, is rejected; and every generated file carries the copyright attribution. For PS3.3, only the IOD Modules tables of Annex A are selected, and each is generated or named in the pin; each IOD whose modules include Functional Group Macros has exactly one table of them, its own or the one the pin gives it, and every such table belongs to an IOD of Annex A; a row that includes them becomes an Include of them, while one in other words is rejected, as are rows nested below it, a macro without a table in its section, and a macro that does not define exactly one sequence or that includes Functional Group Macros; the pin leaves out exactly the IODs that cannot be generated because of an attribute the data dictionary lacks, which a test also lists with the attribute each lacks, and generation fails if one of them first lacks an attribute other than the one recorded with it; every other composite IOD is generated, including the enhanced multi-frame IODs; each correction applies to its stated number of rows, and fails generation otherwise; merged cells are expanded; modules are found by section and title, including a section that ends in a letter; macros are reached through nested includes; rows nested below an “Include” extend the included macro’s only sequence, and nesting below an Include of any other table is rejected; spot Types match the published tables, including different Types in different modules and sequences, for IODs beyond the first release such as MR Image, Positron Emission Tomography Image, RT Image, and RT Beams Treatment Record; spot Types inside both Functional Groups Sequences, such as Pixel Spacing (0028,0030), Type 1C, in Pixel Measures Sequence (0028,9110) of Enhanced CT Image and Segmentation, each macro’s usage and condition, and each macro in every sequence that includes the macros; an IOD is expanded only when its Types are first needed; Types are found at every level of a table that includes itself, such as Relationship Type (0040,A010), Type 1, in Content Sequence items nested three deep in an Encapsulated PDF; and malformed nesting, missing tables, and any other cycle of includes, including a table that includes itself outside its own sequences, are rejected. For PS3.4, every Storage SOP Class is a current SOP Class in PS3.6 Table A-1; the IOD of every Storage SOP Class is generated, and every Storage SOP Class finds it; the generated IODs that no SOP Class of Table B.5-1 names are listed; and the first release’s SOP Classes find their IODs, with or without padding and when read from a file, as do the SOP Classes of other generated IODs, such as MR Image, RT Beams Treatment Record, Enhanced CT Image, and Segmentation, while SOP Classes that the table does not list find none, and so does one that it lists whose IOD has no generated Types, as when a later edition lists an IOD that the pin leaves out. The edition check downloads each pinned page from
current/; reports no change for the pinned edition’s pages, or for a page that changes without changing a table; reports exactly the table that a changed cell changes; reports a page that cannot be fetched or parsed, and the tables it prevents, without quoting the page, as a failure even when another table would change; and writes nothing but its report.
D-002: pydicom 3.0.2 minimum#
Decision. Require pydicom 3.0.2 or later, with no compatibility layer for pydicom 2, as the maintainer decided on 1 October 2026.
Rationale. The package declared
pydicom>=2.0.0, but continuous integration tested only the locked 3.0.2. The engine usesdcmwrite(..., enforce_file_format=True), added in pydicom 3.0. That call updates the Media Storage SOP Class and Instance UIDs but keeps other File Meta elements and any existing preamble, so the engine builds both itself. The engine also reads each element without pydicom decoding it, throughDataset.get_item(..., keep_deferred=True), which pydicom 3.0 added too. pydicom 3.0.0 and 3.0.1 are affected by CVE-2026-32711, a path-traversal flaw in reading DICOMDIR files that 3.0.2 fixes. The engine writes no DICOMDIR and never passes one through (D-016), but other PyMedPhys code and users’ own code read files with the same pydicom, so the minimum excludes the affected releases rather than leaving them installable.Tests. Continuous integration runs the de-identification tests with pydicom 3.0.2, the lowest release that this minimum allows, as well as with the locked version and the latest release (D-029).
D-003: UID replacement#
Decision.
Resolve each UID attribute’s effective action from the L1, L2, and validated L3 rules before transforming its value.
Semantic identifiers need explicit L2 rules. SOP Class UIDs, transfer syntaxes, and well-known coding-scheme and context-group UIDs are retained. Local coding-scheme identifiers and private codes are retained only when a reviewed L2 rule classifies them as non-identifying; this is stricter than Table E.1-1, which gives C only to the Code Sequences it lists (E.1.1, Notes 7 and 10 after Table E.1-1a). A UI attribute with no rule is rejected, never hashed merely because of its VR.
The L2 file
uid_roles.tomlgives every UI attribute of the pinned dictionary a role. Aninstanceattribute identifies an instance, event, entity, device, or organisation; every UI attribute that Table E.1-1 replaces has this role, as do instance UIDs the table omits, such as Target Frame of Reference UID (0018,991E). Adefinitionattribute identifies something a standard or vendor publishes: a SOP Class, transfer syntax, coding scheme, context group, mapping resource, or template.A value that the pinned tables register (PS3.6 Annex A and PS3.16 Tables 8-1 and 8-2) is retained whatever the attribute’s role, because it names a public definition rather than an instance. Examples are a well-known frame of reference (PS3.6 Table A-2) and the Universal Coordinated Time synchronization frame of reference. This interprets U for such values, and the conformance statement records it. Any other value is replaced; in a
definitionattribute, where it may be a local definition under an institution’s root, the replacement is also reported.Where replacement is required, remove trailing NUL and space padding, then compute a 32-byte token with HMAC-SHA256 under the run’s key (D-004) and a UID-specific domain label. The replacement is the name-based SHA-1 (version 5) UUID of that token in a fixed namespace defined by PyMedPhys, written as a
2.25.UID of at most 44 characters. The namespace is the version 5 UUID ofhttps://docs.pymedphys.com/deidentify/uid-namespacein the URL namespace, and never changes.Use the same derivation for every occurrence, including nested references, File Meta Information, and retained private elements. The same key always gives the same replacement, within a run and across runs. A new key gives unrelated replacements, for example to import another copy of a phantom alongside earlier copies.
Replacement UIDs never use an organisation root.
Device UID (0018,1002) is replaced under the Basic Profile (U) and retained under Retain Device Identity (K).
Rationale.
Standards basis. PS3.5 B.2 derives
2.25.UIDs from UUIDs as defined by ISO/IEC 9834-8 and ITU-T X.667. X.667 defines the name-based SHA-1 UUID as version 5 (clause 14.3). Its Table 3 reserves versions 0 and 6 to 15, and a note in clause 12.2 expects any new format to use different variant bits, so the version 8 layout that RFC 9562 adds is not an X.667 UUID. X.667 does not limit names to text (Annex B), so the 32-byte token is a valid name. Using the token as the name keeps the standard version 5 construction; replacing its SHA-1 step with HMAC-SHA256 would not. A2.25.UID needs no registered root, whereas an organisation root can identify the institution (E.3.9 Note 2).Re-linking. The UUID’s name is a keyed token, so without the key, knowing a source UID does not reveal its replacement. Unkeyed hashes, as in the legacy pseudonymisation, and version 5 UUIDs of the source UIDs themselves both reveal it. Published SHA-1 collision attacks need inputs the attacker chooses, and without the key nobody can choose the HMAC outputs that form the names. A compromised key reveals the replacements of known source UIDs (D-004).
Uniqueness. A version 5 UUID has 122 variable bits, so the probability of any collision among \(10^9\) replacement UIDs is about \(10^{-19}\).
State. Replacements are computed from the key alone, so no UID map is needed: parallel workers need no coordination, output does not depend on the number of workers, and incremental export needs only the key. Random version 4 UUIDs would instead need a map equivalent to a crosswalk, protected, shared between workers, and kept for incremental export.
Tests. Known-answer vectors for the derivation; version and variant bits; length at most 44 characters; a role for every UI attribute, and the instance role for every UI attribute Table E.1-1 replaces; registered values retained under either role; consistent replacements across workers, nested references, File Meta Information, retained private elements, and incremental runs; unrelated replacements under different keys; Device UID under the Basic Profile and under Retain Device Identity; retained class, coding-scheme, and context-group identifiers; rejection of unclassified UIDs; and, after writing, both replacements and required retentions.
D-004: Keys and subject profiles#
Decision.
Keys are 256 bits from a cryptographically secure generator. An ephemeral key and its run-only state are discarded after the run, so generated values are consistent only within that run. A project key and its subject profiles are held by a custodian, so generated values stay consistent across runs, as incremental RT collections need.
Use a separate key for each project and recipient, with subject profiles isolated per key. Releases under one key are linkable by design. Separate keys separate only the generated values: pixel data, geometry, and retained attributes can still link releases, and each release’s assessment covers them (D-017).
Derivations for UIDs, patient identifiers, date offsets, and the keyed digest of the reviewed-names list (D-024) are domain-separated. Each is an HMAC-SHA256 under the key of the version label
pymedphys-deid/1, the domain, and each input, every one preceded by its length, so no two domains or splits of the input share a message. A new derivation takes a new version label.Each key has a non-secret identifier derived from it. Reports may record the identifier, never the key.
Project keys and subject profiles are stored only in custodian-controlled locations outside output and QC directories, and never in logs, reports, or the PyMedPhys user configuration. A key file is JSON (
pymedphys-deid-key/1) holding the key and its identifier. It is created exclusively, so an existing file or symbolic link is never overwritten or followed, and with owner-only permissions where the platform enforces file modes; elsewhere writing it warns.The subject profile is the single source of truth for a subject’s date offset and synthetic values. They are derived at first export, persisted, and read back afterwards; a change of derivation never silently changes an exported subject.
A profile store is JSON (
pymedphys-deid-profiles/1) that records its key’s identifier and refuses any other key. It holds each subject under the key’ssubjectderivation of the identity, never under an identifier, with the values and the derivation version that produced them. It is replaced atomically, created with owner-only permissions where the platform enforces file modes, and kept out of the configuration directory and any protected directory, like a key file. Under an ephemeral key, profiles are kept for the run only.Incremental export needs both the key and the subject profile. Recipients receive neither.
Rotating a key starts a new domain of generated values. If a key or profile is lost, earlier exports cannot be extended consistently. A compromised key is treated as disclosure of the crosswalk, and affected releases are reassessed.
Rationale. Medical record numbers and similar identifiers have little entropy, so anyone holding the key can enumerate candidates and re-identify every subject: a project key is equivalent to a crosswalk. Profiles shared between keys would give every recipient the same shifted dates and synthetic values.
Tests. Key-file creation and permissions; refusal to write keys or profiles into output, QC, or configuration locations; stable key identifiers; offsets read from an existing profile after a derivation change; profiles isolated per key; consistent generated values across incremental releases; and a demonstration that a key enumerates low-entropy identifiers.
D-005: Keyed patient pseudonyms#
Decision. Every preset replaces Patient ID and Patient’s Name with values derived from the subject’s identity (identifier and issuer, or a curator-supplied identity) under the key (D-004), rather than with empty values. One format serves every preset. It is conspicuously synthetic, deterministic under the key, valid for the VR (64 characters for LO and for each PN component group), identical for every instance of a subject, and does not embed the source identifier. The conformance statement describes it, as E.1.3 requires for dummy values.
Format. The subject’s code is the base32 form (RFC 4648) of the first 80 bits of the key’s
patientderivation of the identity. Patient ID becomesDEID-and the code (21 characters), and Patient’s Name becomesDEIDENTIFIED^and the code. An identity is either a Patient ID with its issuer, ignoring LO padding, or a curated subject identifier; the two kinds are framed differently, so the same text in each is two subjects. An empty Patient ID names no subject and needs a curated identity. Among a million subjects the chance that any two share a code is below \(10^{-12}\).Rationale. Patient’s Name is Z and Patient ID is Z/D in the Basic Profile, both of which permit a dummy value. Empty values would merge every patient in a multi-patient collection, whereas keyed pseudonyms keep subjects separate and, under a project key, consistent across runs. Conspicuous values stop a research copy from being mistaken for a clinical record.
Tests. Determinism under one key and difference across keys; VR validity; distinct pseudonyms for distinct subjects, including the same identifier from different issuers; and consistency across a subject’s instances.
D-006: Date shifting#
Decision. Where Retain Longitudinal Temporal Information with Modified Dates is selected, shift each subject’s subject-event and radiation-source dates (D-007) backwards by a whole number of weeks between 52 and 520, never zero. With a project key, the offset is read from the subject profile (D-004); with an ephemeral key, it is derived for the run. Times are unchanged. Dates found in free text are removed, not shifted. Without the option, the Basic Profile’s actions apply.
Derivation. A new subject’s offset is 52 weeks plus the first 64 bits of the key’s
date-offsetderivation of the identity, modulo 469, so each of the 469 offsets is equally likely to within one part in \(10^{16}\). A DA value moves by the offset; a DT value moves its date and keeps its time, and any UTC offset it carries becomes the nominal offset once the value is in the instance’s local time (D-007). A DT value without a full date, such as a year alone, cannot move by whole weeks, so it is not shifted and the engine decides what to write. A DT value at a leap second, with a second of 60, is neither shifted nor converted to local time, because PS3.5 allows 60 only at a real leap second, which the moved value would not be; its instance is sequestered.Rationale. Whole weeks preserve intervals, times of day, and weekdays, which fractionation analyses need. Shifting backwards avoids future-dated plans, which planning systems may reject. Excluding zero ensures every date moves (MIDI §1.17.9). Shifting a free-text string wrongly recognised as a date can disclose the offset (MIDI §1.17.9).
Tests. Offset range and exclusion of zero; preservation of intervals, times, and weekdays; rejection of leap seconds; consistency across the studies and runs of one subject; and removal of dates in free text.
D-007: Temporal attributes by role#
Decision. L2 gives every date, time, and datetime attribute a role. Where Modified Dates is selected, each role is handled as follows:
Subject event, such as Study Date, Treatment Date, and Beam Hold Transition DateTime (300C,0127): shifted by the subject’s offset (D-006).
Radiation source, Source Strength Reference Date (300A,022C) and Time (300A,022E): shifted by the same offset, so that decay intervals to treatment remain correct.
Device, such as the calibration, installation, and manufacture attributes listed below; vocabulary version, published or local, such as Context Group Version (0008,0106) and Context Group Local Version (0008,0107); and other: replaced with a fixed, conspicuous dummy value that differs from the original.
Time zone, Timezone Offset From UTC (0008,0201): replaced with the nominal offset
+0000, after every DT value in the instance that carries its own offset is converted to the instance’s local time and given the same nominal offset. The instance’s dates and times are then all local times under one offset, so their intervals are kept, including across midnight, and no offset reveals the season of the original date through daylight saving, such as +1100 rather than +1000, or the location. The local time is the instance’s Timezone Offset From UTC where present, which PS3.3 C.12.1.1.8 makes the offset of every value without its own, and otherwise the offset of its earliest DT value that has one, compared to the microsecond. The replacement is a valid offset for the instance’s times, as C requires, and the conformance statement records that it is nominal. Intervals between instances at different offsets, such as either side of a daylight-saving change, move by the difference between their offsets. An instance with a value whose precision cannot express its conversion exactly, such as a date alone, is sequestered.
Without Modified Dates, Table E.1-1 applies unchanged. The conformance statement describes these modifications, as E.3.6 requires.
The L2 file
temporal_roles.tomlgives each DA, DT, and TM attribute of the pinned dictionary a role, including attributes that Table E.1-1 omits, such as DICONDE and DICOS attributes. It also gives a role to every attribute of another VR that Table E.1-1 cleans under Modified Dates, so a new edition that cleans another fails to load until that attribute has a reviewed rule. In 2026d it has 187 rules: 152 subject event, 2 radiation source, 12 device, 4 vocabulary version, 1 time zone, and 16 other. An attribute that PS3.3 requires to follow a subject event, such as Item Inventory DateTime (0008,0404), which must be at or after Content Date (0008,0023), is itself a subject event, so shifting keeps the order. The UI and temporal roles files share one format and loader.Besides Timezone Offset From UTC, Table E.1-1 cleans two attributes of other VRs under Modified Dates. Frame Origin Timestamp (0034,0007), each frame’s capture time as an IEEE 1588 timestamp in TAI (PS3.3 C.7.6.27.1.1), is a subject event; its seconds move by the subject’s offset, which keeps the timing between frames exactly. Certified Timestamp (0400,0310) occurs only inside the Digital Signatures Sequence (FFFA,FFFA), which Table E.1-1 removes under every option, so its role, other, never applies.
In 2026d, eleven attributes are K under Retain Device Identity and C under Modified Dates. Ten are device attributes: Date of Last Calibration (0018,1200), Time of Last Calibration (0018,1201), DateTime of Last Calibration (0018,1202), Calibration DateTime (0018,1203), Date of Manufacture (0018,1204), Date of Installation (0018,1205), Calibration Time (0014,407C), Calibration Date (0014,407E), Date of Last Detector Calibration (0018,700C), and Time of Last Detector Calibration (0018,700E). The eleventh is Beam Hold Transition DateTime. K keeps a non-Sequence value unchanged (Table E.1-1a) and E.3.8 requires retention, whereas E.3.6 requires modification, and PS3.15 defines no precedence between options. No output can satisfy both options while one of these attributes has a value, so a policy that selects both makes no conformance claim.
tps-importdoes this deliberately and handles these attributes by role, as above; any other such policy is rejected (D-011). A test fails if a regenerated Table E.1-1 adds a conflict.Rationale. E.3.6 requires every date and time in a Table E.1-1 attribute to be modified, in a manner that reduces the possibility of matching and preserves temporal relationships only to the extent the application needs. Neither removal nor retention is modification. Shifting a device or vocabulary date by the subject’s offset would disclose that offset whenever the original is known or guessable, such as a published version date or a known calibration date; MIDI §1.17.9 describes the same mechanism for free text. It would also give one device different dates in different subjects’ output, revealing the differences between their offsets. No preset needs these dates’ relationships to the images, so a fixed dummy value satisfies E.3.6 and C (a value of similar meaning that contains no identifying information) and discloses nothing.
Consequences. Dose and decay calculations stay consistent; device calibration history and vocabulary versions are lost. The first supported release is unaffected, since neither of its presets selects Modified Dates. Under
tps-import, Retain Device Identity keeps Source Serial Number (3008,0105), so subjects treated with the same source can be linked and the differences between their offsets disclosed; this residual risk is documented for non-clinical use.public-releasedoes not retain source serial numbers.Tests. Every temporal attribute in the pinned dictionary has a role; every value in a Table E.1-1 attribute is modified, including one equal to the dummy value; known device and vocabulary dates alongside shifted subject dates do not expose the offset; source-to-treatment intervals are preserved; a custom policy selecting both conflicting options is rejected; and a regenerated Table E.1-1 with a new option conflict fails the tests.
D-008: Synthetic birth dates for TPS import#
Decision.
Resolve a stable subject identity, as D-005 does.
Before first export, designate one reference study with a validated date and an age in completed years. The synthetic birth date is that study’s shifted date minus the age in calendar years, with 29 February mapped to 28 February.
Persist the reference, offset, derivation version, and chosen date in the subject profile (D-004; run-only under an ephemeral key), and reuse the date for every instance and later run. A study that arrives later, whatever its date, never changes the reference.
Failure handling. Unresolved subject identity stops the affected objects for curator resolution. For an identified subject, missing or invalid reference dates or ages, or inconsistent source ages, must be resolved before synthesis. Otherwise an empty birth date is written consistently for that subject, and that choice is persisted too. A conflict with an existing profile stops the subject for review. Changing an exported choice requires an explicit migration of the related collection.
Rationale. Patient’s Birth Date is Z in the Basic Profile, and no option retains or cleans it, so a synthetic value is a permitted dummy value. Planning systems may reject records with empty or invalid values. Recomputing the date from each study’s date and rounded age could give one subject several birth dates. The synthetic date expresses approximate age at the reference study, not the true birthday; the retained Patient’s Age remains the source for age analyses.
Tests. Several studies at one age; birthdays between studies; leap years; independence from processing order; earlier studies arriving later; missing or inconsistent ages; missing subject state; and identical birth dates across a subject’s instances and incremental runs.
D-009: Descriptor cleaning#
Decision.
Start with a vocabulary-based cleaner, combined with context-aware checks and checks for echoes of known patient and other person identifiers, including reordered names and reformatted dates (MIDI §1.17.2).
Treat unresolved ambiguous text conservatively, subject to D-011. Remove optional attributes, use permitted empty or dummy values for required ones, or hold the output for confidential review.
Other validated methods, including removing optional descriptors, may satisfy the option. Describe the method and its limits in the conformance statement, as E.3.5 requires.
Claim Clean Descriptors (DCM 113105) only for output that retains no descriptor string that has not passed pooled human review. A descriptor that is not cleaned takes its action under the policy without the option instead, and where that action removes or replaces it, the output still meets the option, since E.3.5 specifies what the option removes, not what it retains, and E.1.1 makes Table E.1-1 the minimum actions; where that action keeps it, the output does not claim the option. A ROI Name that automatic cleaning, below, writes in the spelling of the published TG-263 vocabulary is a cleaned value (C), not a retained string, so it needs no review before the claim.
The first supported release includes the Clean Descriptors Option, in the
basic-clean-descriptorspreset. The maintainer decided on 1 October 2026 that it cleans ROI Name (3006,0026), using a vocabulary made with the TG-263 converter below, in two tiers:Automatic. A ROI Name that matches a vocabulary name once case, padding, and the separators
_,-, and space are disregarded is written in the vocabulary’s own spelling:lung l,LUNG-L, andLung Lall becomeLung_L. TG-263 writes-for a subtraction, as inLungs-PTV, so a vocabulary name that contains-matches only a name with-in the same place:lungs - ptvbecomesLungs-PTV, butLungs PTVandLungs_PTVgo to review. A name that starts with_or-also goes to review, since TG-263 marks a structure not used for dose evaluation, such as an optimisation contour, with a leading_, so_Heartis notHeart; separators elsewhere in a name are still disregarded. Only names of printable ASCII are matched, since TG-263 names are ASCII and Unicode case folding and compatibility forms would match look-alike characters. Each such rename is recorded for audit in the confidential QC material (D-016), and needs no review before the option is claimed. This applies only to the list that AAPM publishes, whose entries are generic names of anatomy and targets: a vocabulary is accepted for automatic renaming only where the digest of its entries is that of a published edition of the TG-263 spreadsheet, as PyMedPhys records it, since the converter writes AAPM’s attribution for any workbook with the TG-263 columns, including one extended with local names. A vocabulary converted from an institutional list can hold an entry that names a site or a person, such asClinicX_Lung, and its source record and digest show which list was used, not that its entries are safe to write, so every match against such a vocabulary goes to review, in the second tier, instead. As the maintainer decided on 6 October 2026, a run can be given such a list, converted as below. It renames nothing and changes no value written: a ROI Name held for review that matches one of its names, once case, padding, and separators are disregarded as above, but with any leading_or-the same in both, keeps the reason it was held for, and the confidential QC material shows its reviewer the list’s names it matched (D-016). Since the list changes no value written, the method digest does not cover it (D-024). A name also goes to review where its normalised form matches more than one vocabulary name; where the checks for echoes above find that it echoes a known patient or other person identifier; or where a different ROI Name of the same structure set would be written as the same vocabulary name, asLung_LandLUNG-Lwould, since duplicate ROI Names can make a planning system reject the import.Reviewed and remembered. Every other distinct ROI Name is listed in the confidential QC material for review. A reviewer keeps it, maps it to another name, including by a bulk rename across a collection, or has it emptied. The decisions are kept in a reviewed-names list for the site or project, held by the custodian with the key and subject profiles (D-016), and reused on later runs; this is the pooled human review that the claim above requires. A reviewer records decisions by writing them into a CSV file with the columns
roi_name,decision(keep,map, orempty), andto, which a command records in the list, starting it on first use, as the maintainer decided on 6 October 2026. Recording is all or nothing: a name given different decisions in the file, or a decision that differs from the list’s without an explicit choice to replace it, is refused and nothing is recorded, and neither its summary nor its messages quote a name or path. An instance with a ROI Name that the list does not cover and automatic cleaning does not rename is held for review (D-027), and counted by reason, without its name or any label, in the release report’sheld_for_reviewsection, rather than emptied silently, unless the user chooses explicitly to empty such names so that the run proceeds. As the maintainer decided on 6 October 2026, a held instance’s file is deleted with the staging area when the run ends, and a later run writes it once a reviewer’s decision for the name is in the list; since the current command gives each run an ephemeral key (D-004), that run’s release replaces the earlier one rather than adding to it. A decision applies to every ROI Name with exactly its spelling, once LO padding is removed, and only where automatic cleaning does not rename the name; a name sent to review only because another name of the structure set would be written the same takes the list’s decision, so mapping one of two such names lets the other be renamed. What a kept or mapped name would write is checked, as an automatic rename is, for echoes of the instance’s patient and other person identifiers and for duplicates within the structure set, ignoring case, since some planning systems compare structure names without case, and the name is held for review if either check fails, since a reviewer may have decided on it for another patient; the user’s choice to empty held names covers such a name too, so that the run proceeds. The list changes what the engine writes, so its keyed digest is an input to the method digest (D-024).
tps-importcleans ROI names in the same way and keeps no name that has not been reviewed, apart from those that automatic cleaning writes in the vocabulary’s spelling, since its output carries Patient Identity Removed YES only when every deviation from its profile and options removes or modifies more than they require (D-012).Recognising well-formed TG-263 target names by their grammar, such as
PTV_HighorCTVp_5040, which TG-263 defines by pattern rather than by list, is future development.Under Clean Descriptors, a sequence that Table E.1-1 gives C takes its Basic Profile action rather than being cleaned item by item, since rules for cleaning its contents are not designed, as the maintainer decided on 1 October 2026. In 2026d there are nine: Admitting Diagnoses Code Sequence (0008,1084), Principal Diagnosis Code Sequence (0008,1301), Primary Diagnosis Code Sequence (0008,1302), Secondary Diagnoses Code Sequence (0008,1303), Histological Diagnoses Code Sequence (0008,1304), Reason for Visit Code Sequence (0032,1067), Reason for Requested Procedure Code Sequence (0040,100A), and Request Attributes Sequence (0040,0275), which the Basic Profile removes (X), and Prescription Notes Sequence (3010,0081), which it empties (Z) and which no IOD of the first supported release uses.
Under Clean Descriptors, every other attribute to which the policy gives C, such as Study Description, Series Description, and Beam Name, takes the action that the policy gives it without that option, and an instance in which that action removes or replaces each such attribute still claims the option, since E.3.5 specifies what the option removes and Table E.1-1 gives the minimum actions, as the maintainer decided on 6 October 2026, over leaving the option unclaimed for such output or cleaning short labels too. An instance in which that action keeps such an attribute does not claim the option. The conformance statement gives that action for each such attribute, at each place.
PyMedPhys downloads the published TG-263 spreadsheet itself and uses it as the default vocabulary, as the maintainer decided on 2 October 2026; it bundles no copy. The edition it uses is pinned in PyMedPhys by its URL on AAPM’s Radiation Oncology Nomenclature Resource Page, the SHA-256 of the file, and the digest of its entries; the pin is the 2017-08-15 edition. The download is cached in the PyMedPhys data directory and used only if it has the pinned SHA-256, and a file that does not is refused. A user without internet access can convert a copy obtained elsewhere, which is checked against the pin in the same way. A monthly check, like the check for a new DICOM edition, opens an issue when AAPM’s page links to a download that maintainers have not reviewed, no longer links to the pinned spreadsheet, or the pinned file has changed, so that moving to a new edition is a reviewed change to the pin, never automatic.
A vocabulary is a UTF-8 JSON file that holds its entries together with their source, version, source-file SHA-256, and required attribution. A converter command produces it from the TG-263 spreadsheet, or from an institutional list exported as CSV with a
Namecolumn, as the maintainer decided on 1 October 2026, and converted files are never edited by hand. The converter commands are kept out of thepymedphyscommand line until the tool is released, as the maintainer decided on 2 October 2026, as is every other command of the tool, and until then run aspython -m pymedphys._nomenclature, the command that records reviewers’ decisions aspython -m pymedphys._dicom.deidentify.reviewed_names_command, and the command that records a QC reviewer’s attestation aspython -m pymedphys._dicom.deidentify.attestation_command; a test fails if apymedphyscommand outside the maintainers’ ownpymedphys devcommands belongs to the tool. Releasing the tool makes them public commands with user documentation, in the pull request of the first supported release (M6), as the maintainer decided on 6 October 2026. Without a vocabulary, no ROI Name is renamed automatically: each needs a reviewer’s decision from the reviewed-names list, or is held for review.pymedphys._nomenclature.tg263converts the TG-263 spreadsheet. It reads the downloaded or a local copy, maps its columns by header text, and writes JSON with the spreadsheet’s file name, worksheet version, SHA-256, AAPM’s attribution, and a digest of its entries; its loader rejects a file whose entries no longer match that digest. Entries are kept as published, except that surrounding whitespace is removed, the derived “N Characters” column is dropped, and unlabelled columns are ignored. The module sits outside the de-identification package so that other tools, such as a check of structure names against TG-263, can use it.
Rationale. E.3.5 specifies what to remove, not an algorithm (Note 4). A vocabulary can contain words that are also names, such as “Hand” (MIDI §1.17.2), so a word’s presence in a vocabulary is not evidence that its use within free text is safe. JSON keeps the provenance, attribution, and value types in the same file as the entries, which CSV cannot, and matches the repository’s existing data files. The TG-263 journal article is licensed CC BY-NC-ND 4.0, which forbids the commercial use and adaptation that Apache-2.0 permits, and no licence compatible with Apache-2.0 has been found for its nomenclature spreadsheet, so, under the packaging rule for bundled data in
AGENTS.md, PyMedPhys downloads the published file from AAPM when it is needed rather than bundling one. Pinning the file’s SHA-256 and the digest of its entries makes the default vocabulary reproducible and detects a changed or substituted file; a hash taken from a third-party mirror was rejected, since it would show only what the mirror holds. AAPM publishes the spreadsheet in the Excel 97-2003 format, whichxlrdreads. Under the Basic Profile alone, ROI Name is Z, ROI Observation Label and Beam Description are X, and RT Plan Label is D, so a first release with the Basic Profile alone would produce RT Structure Sets without usable structure names, which are of limited use for radiotherapy research. Including the option is a real increase in scope, with review risk, because Clean Descriptors applies to every descriptor, such as Study Description and Series Description, not only to ROI names. Writing the vocabulary’s spelling of a ROI name that matches it once case, whitespace, and separators are disregarded is cleaning as Table E.1-1a defines C, replacing the value “with values of similar meaning known not to contain identifying information”, since the whole value written is a vocabulary entry. The caution about “Hand” (MIDI §1.17.2; PS3.15 E.3.5 Note 2) concerns words found inside free text, such as a Study Description, and matching a whole ROI Name never yields such a word:Dr Handnever normalises toHand. Echoes of known patient and other person identifiers are still checked, and the safety of the vocabulary’s own entries rests on its source, which is why only a published edition’s entries are renamed automatically. Matching only exact spellings was rejected because it would empty structure names that differ from TG-263 only in case or separators. A name whose normalised form matches several vocabulary names cannot be renamed without a choice, so a reviewer makes it. Remembering reviewed decisions for a site or project means that each distinct name is reviewed once, and holding an instance whose name nobody has reviewed, rather than emptying the name silently, keeps names that a reviewer would keep; a user who cannot wait for review can choose to empty them. TG-263 target names are defined by pattern, so no list holds them all. Cleaning a sequence means changing the values within it where the meaning of the sequence in the IOD is specified, or applying the Profile’s rules to each of its items otherwise (Table E.1-1a); neither is designed for the codes of diagnoses and reasons or for the contents of Request Attributes Sequence, so their Basic Profile action applies.Tests. A ROI name that matches a vocabulary name once case, whitespace, and the separators
_,-, and space are disregarded is written in the vocabulary’s spelling, and the rename is recorded in the confidential QC material; a name whose normalised form matches more than one vocabulary name, a name that echoes a known patient or other person identifier, and different ROI names of one structure set that would be written as the same vocabulary name, such asLung_LandLUNG-L, go to review, as does every match against a vocabulary converted from an institutional list, which, given to a run, writes no name and shows the reviewer of a held name the list’s names it matched; reviewed decisions to keep, map, or empty a name, including a bulk rename across a collection, are reused on later runs, apply only to the exact spelling, and are held where what they would write echoes an identifier or duplicates another name of the structure set; decisions recorded from a reviewer’s CSV file start the list on first use and are added to it later, a decision that differs from the list’s is refused unless replaced explicitly, and a malformed file, a name given different decisions, or a list in a protected location records nothing, with messages that quote no name or path; the list’s keyed digest is the same for the same decisions under the same key, whatever order they were recorded in, and changes with any decision, name, or key; an instance with a ROI name that the reviewed-names list does not cover and automatic cleaning does not rename is held in the staging area, or written with that name emptied where the user has chosen so explicitly;tps-importkeeps no name that has not been reviewed, apart from those that automatic cleaning writes in the vocabulary’s spelling; each sequence that Table E.1-1 gives C under Clean Descriptors takes its Basic Profile action; names that overlap anatomical terms; clinician names; mixed descriptive and identifying text; non-English text; missing source identifiers; converting the AAPM spreadsheet and an institutional list; downloading the pinned edition, and refusing a file that does not match the pin; refusing automatic renaming with any vocabulary whose entries are not those of a published edition; matching a hyphen in a vocabulary name only with a hyphen; sending a name with a leading_or-to review; rejecting a vocabulary without its source record; and processing without one. Output that retains a descriptor string that has not passed pooled human review never claims the option, and output whose other descriptors take their Basic Profile actions, which remove or replace them, claims it, as an RT Plan whose Type 1 RT Plan Label is given a dummy value does; a vocabulary word found within free text never on its own lets a descriptor be retained; and output whose ROI names are all automatic renames claims the option without review.
D-010: Unsupported object types#
Decision. Sequester Structured Reports (including dose reports), Key Object Selection documents, Presentation States, instances of Private SOP Classes, and objects outside the supported scope. List them in the report and exclude them from the conformance claim. An instance is classified from its SOP Class UID, through PS3.4 Table B.5-1, and from its file’s transfer syntax. Support is decided by IOD, so every Storage SOP Class that Table B.5-1 maps to a supported IOD is supported. The first supported release de-identifies instances of the CT Image, RT Structure Set, RT Plan, and RT Dose IODs in the Implicit VR Little Endian and Explicit VR Little Endian transfer syntaxes; CT Image Storage - For Processing (1.2.840.10008.5.1.4.1.1.2.3) is among them, since Table B.5-1 maps it to the CT Image IOD. The report says why any other instance was sequestered: it had no SOP Class UID or one that is not text, its SOP Class is not a Standard Storage SOP Class of Table B.5-1, its IOD is not supported (the report names the IOD), or its transfer syntax is missing or not supported. The maintainer decided on 1 October 2026 that an instance whose Specific Character Set (0008,0005) is not supported or is invalid is also sequestered, with that reason. Specific Character Set is invalid where its values break the rules of PS3.3 Section C.12.1.1.2.
ISO_IR 6as a single value, which is not a Defined Term, since Table C.12-2 gives the Default Character Repertoire no Defined Term, is read as that repertoire. A value of VR SH, LO, ST, PN, LT, UC, or UT that has bytes outside the Default Character Repertoire, in a data set or item to which no Specific Character Set applies, cannot be decoded reliably, since PS3.3 Table C.12-1 makes Specific Character Set “Required if an expanded or replacement character set is used” and nothing says which was used. The maintainer decided on 1 October 2026 that such text sequesters its instance only where the text would be kept or cleaned; where it is removed or replaced, the instance is de-identified, as for other content that cannot be decoded (Architecture item 2, D-027). The residual search reads such text as ISO 8859-1, in which every byte is a character (Architecture item 5). Clarification, 2 October 2026. The maintainer clarified that “the instance is de-identified” means that the transformation proceeds, not that the output may be released: a value read only as ISO 8859-1 is incomplete coverage for the residual search, and the release gate withholds the file or sends it to QC review by the value’s kind (D-027 sets out the examples).Rationale. The Basic Profile gives Content Sequence D, and E.3.4 Note 2 warns of significant risk in de-identifying Structured Reports without the Clean Structured Content Option. Presentation States can carry graphic and text annotations (MIDI §1.22.3). De-identification of Private SOP Classes is not defined (E.1.1, Note 6 after Table E.1-1a). Supporting every Storage SOP Class of a supported IOD, including CT Image Storage - For Processing, is consistent with deciding support by IOD, and the benchmark data should include such instances. Text decoded with the wrong character set would defeat descriptor matching (D-009), the check of a source value against the dummy value (D-021), and the residual search (D-027). Text with bytes outside the Default Character Repertoire that no Specific Character Set applies to is handled as other content that cannot be decoded is, for consistency: removing or replacing it writes nothing of it into the output, whereas keeping or cleaning it would write or match text whose characters are unknown. The residual search still finds a copy of such text left in the output, because it encodes each source value again in several character sets, including ISO 8859-1, which gives the text’s own bytes back; like every form of fewer than four characters, a shorter one is listed as not searched instead (D-027), so a copy of it is not found.
Tests. Each type is sequestered and reported, and never written to the release output. The Storage SOP Classes of the first release’s IODs, including CT Image Storage - For Processing, are supported in both transfer syntaxes, and each other kind of SOP Class, and each other transfer syntax, is classified with its reason. An instance whose Specific Character Set is not supported, or is invalid, is sequestered with that reason, and
ISO_IR 6alone is read as the Default Character Repertoire. Text with bytes outside the Default Character Repertoire, in a data set or in an item to which no Specific Character Set applies, sequesters its instance where the text would be kept or cleaned; where it is removed or replaced, the transformation proceeds, the release gate withholds the file where the text is a person name and sends it to QC review where it is a description, and a copy of the text’s bytes of at least four characters left in the output, such as in a private element, is found by the residual search, while a shorter one is listed as not searched.
D-011: Validated overrides and derived claims#
Decision.
Validate the effective policy across L1, L2, and L3 before processing. Reject unresolved conflicts between selected options; a documented deviation never authorises a conformance claim.
A policy is a preset’s options, or a custom option set, with the resulting action of each attribute of Table E.1-1. Validation rejects an option outside the target options in Scope; a Table E.1-1 of another edition than the data dictionary, against which the L2 rules were reviewed; and every conflict between selected options except those
tps-importresolves.That exemption belongs to the
tps-importpreset. Where Retain Device Identity gives an attribute K and Modified Dates gives it C, and the attribute has a temporal role, the preset applies C by that role (D-007) and records Retain Device Identity as unmet, so its policy claims no conformance. The same options chosen as a custom set are rejected, as is any other conflict, including one that a regenerated table adds.The engine accepts only the policy of an enabled preset, composed from the pinned tables, so a policy composed from another table is never enabled. A custom option set is validated but not enabled; no planned release enables one.
Validate every L3 removal, replacement, and retention against attribute Type, VR and VM, conditional requirements, referential integrity, and the effective profile and options. Reject a change that invalidates a supported IOD.
Check every value the engine writes, whether a replacement, a dummy value, or an L3 value, against its VR’s definition in PS3.5 Table 6.2-1 and its attribute’s VM in the pinned dictionary before writing it; a value of LT, ST, UR, UT, OB, OD, OF, OL, OV, OW, or UN is single whatever the dictionary gives, as PS3.5 Section 6.4 requires. A text value is checked as the engine sets it, before encoding adds a padding byte. Where PS3.6 gives an attribute alternative VRs, such as “US or SS” or “OB or OW”, the value is checked against the VR that applies in the data set, as the standard determines it from other attributes and the transfer syntax: for example Pixel Representation (0028,0103) decides US or SS for Pixel Padding Value (PS3.3 C.7.5.1), and Bits Allocated (0028,0100) and the transfer syntax determine which of OB and OW Pixel Data may have (PS3.5 Annex A). The checks of a value on its own accept any alternative it fits. As a text value is written, the engine also checks that the data set’s Specific Character Set can encode it, in which the Default Character Repertoire, given by an absent value or by ISO_IR 6 alone (Architecture item 5), is ISO 646 (PS3.5 Section 6.1.2.1) and a single-byte character set takes one byte for each character, and where that repertoire is Value 1 with code extensions, no character of ISO 8859-1 outside ISO 646 is accepted, since pydicom would write it in ISO 8859-1 without the escape sequence of the code extension that holds it (PS3.5 Section 6.1.2.5.3); and that the first component group of a Person Name uses only the characters PS3.5 Section 6.2.1.2 allows there. What is wrong is reported without quoting the value.
A change incompatible with a selected option requires an explicitly revised policy that is revalidated in full; an option is never dropped silently.
Derive all report wording and De-identification Method and Code Sequence content from each instance’s validated result. A run with mixed results cannot claim that every output conforms.
tps-importis the only policy that produces nonconformant output, marked as D-012 describes. Any other policy that cannot conform, such as one retaining attributes its options do not keep, is rejected, and instances of Private SOP Classes are sequestered (D-010).
Rationale. E.1.1 requires that each attribute specified to be retained “shall be retained”, and an option’s requirements override the Profile’s. Removing Patient’s Weight therefore contradicts Retain Patient Characteristics, even though removal discloses less. Maintaining IOD integrity is the de-identifier’s responsibility (E.1.1 step 2, Note 1).
Tests. The presets’ option sets; the eleven conflicts that
tps-importresolves, each modified by its role, and their rejection in a custom option set; rejection of options outside Scope, of a table of another edition, and of a conflict that a regenerated table adds; only enabled presets reach the engine; removal of Type 1 and Type 2 attributes; removal of retained patient characteristics; retention the profile forbids; conditional attributes; revised option sets; and agreement between report claims and inserted markers.
D-012: De-identification markers#
Decision. Markers are derived from each instance’s validated result (D-011): its policy, the method digest, and the options the instance satisfies. They are added after every other action, and change nothing else in the data set.
Every preset adds two values to De-identification Method (0012,0063), an LO attribute of VM 1-n, after the values already present: first a value that is exactly the 64 hexadecimal digits of the method digest (D-024), and then a readable value of at most 63 characters. No other digest, such as one of the runtime environment or of the run, is added. Output that already carries markers gains them only as set out below.
For conformant output, set Patient Identity Removed (0012,0062) to YES; add the CID 7050 codes for the profile and each satisfied option to De-identification Method Code Sequence (0012,0064), after the items already present and apart from any already among them; and write the readable De-identification Method value
PyMedPhys <version>; PS3.15 <edition>; <preset>, such asPyMedPhys 0.42.0; PS3.15 2026d; basic. A custom option set has no preset, so its readable value names itcustom option setinstead.The satisfied options are the policy’s options that the instance’s validated result meets. Under a policy that claims conformance, every selected option is satisfied except Clean Descriptors, which goes unsatisfied where an instance retains a descriptor string that has not passed pooled human review (D-009); such an instance carries the profile’s code but not the option’s. A descriptor that its Basic Profile action removes or replaces instead still meets the option. Markers whose satisfied options leave out any other selected option are refused, with an error that names each option left out. The profile’s code, DCM 113100, comes first, then each satisfied option’s, in Table E.1-1’s order of options. Each item holds the Code Value, Coding Scheme Designator, and Code Meaning of its row of the pinned CID 7050, and no Coding Scheme Version, which the Basic Code Sequence Macro (PS3.3 Table 8.8-1a) requires only where the designator does not identify the code unambiguously, as DCM does.
For
tps-importoutput, which makes no conformance claim (D-007), set Patient Identity Removed to YES; add no CID 7050 codes, keeping existing items; and write the readable valuePyMedPhys <version>; no PS3.15 claim; tps-import, which names PyMedPhys, its version, and the preset, and states that no PS3.15 conformance is claimed. Where no item was present, De-identification Method Code Sequence is left out, because the Patient Module (PS3.3 Table C.7-1) makes it Type 1C, so that where present it needs an item.For both, set Longitudinal Temporal Information Modified (0028,0303) to REMOVED, or to MODIFIED where Modified Dates is applied, unless a stricter value is already present, and add a Contributing Equipment Sequence (0018,A001) item whose purpose of reference is DCM 109104 (De-identifying Equipment, CID 7005), after the items already present unless an equal one is among them.
Every de-identified instance names PyMedPhys in these markers, never in dummy values, apart from the Coding Scheme Designator of the dummy item of Person Identification Code Sequence (D-021). The Contributing Equipment Sequence item names PyMedPhys as its Manufacturer (0008,0070), and the readable De-identification Method value names PyMedPhys too. In the SOP Common Module (PS3.3 Table C.12-1), which each of the first supported release’s IODs includes as mandatory, the item’s only Type 1 attributes are Manufacturer and Purpose of Reference Code Sequence (0040,A170), whose item carries the code: Code Value
109104, Coding Scheme DesignatorDCM, and Code MeaningDe-identifying Equipment. Every other attribute of the item, including Software Versions (0018,1020), is Type 3. The item holds only Manufacturer, Software Versions, and Purpose of Reference Code Sequence, so it has no Contribution DateTime (0018,A002).Software Versions of the item, an LO attribute of VM 1-n, records the software that ran as four values, in this order: the PyMedPhys version, such as
0.42.0; the Python implementation, asplatform.python_implementation()gives it, a space, and the Python version, such asCPython 3.14.0;pydicom, a space, and its version, such aspydicom 3.0.2; andtomlkit, a space, and its version, such astomlkit 0.15.1. Each value is checked as an LO value, like every value the engine writes (D-011). These values are provenance of the run, not inputs to the method digest (D-024), and the release report records the same values (D-029). They are read from the running process each time the markers are computed.<version>is PyMedPhys’s full version, as in the first value of Software Versions, and<edition>is the edition of the policy’s Table E.1-1. Every marker value is checked against its VR and VM in the pinned data dictionary before it is written (D-011). A value that does not fit, such as a Software Versions value longer than the 64 characters of LO or a readable value longer than 63 characters, is never shortened or written without the version: the markers are refused with an error that names each attribute that does not fit. They are refused in the same way, before anything is written, where an attribute that they write would have an odd length in all and end in a value of its VR’s maximum length, as Software Versions could, or De-identification Method where such a value from another de-identifier follows a pair already present. With edition 2026d, every version of up to 14 characters fits every preset’s readable value;basic-clean-descriptorsgives the longest.Never set Patient Identity Removed to NO. Nonconformant output carries YES only when every deviation from the selected profile and options removes or modifies more than they require, as
tps-import’s modification of device dates does, so a policy that resolves a conflict between options by keeping the value gets no markers.On output that already carries de-identification markers, PyMedPhys’s own or another de-identifier’s, such as output de-identified a second time, the markers are added as the maintainer decided on 1 October 2026:
De-identification Method gains this run’s digest and readable value only where that pair is not already present as two consecutive values, the digest first, and otherwise gains nothing. It never gains only one of the pair, so a digest is never separated from its readable value.
De-identification Method Code Sequence gains each code only where no item already present has the same Code Value and Coding Scheme Designator, and the same Coding Scheme Version where either item has one. Items already present keep their order, and new ones follow in this run’s order.
Contributing Equipment Sequence gains this run’s item only where no item already present is equal to it: the same Manufacturer, the same Software Versions values in the same order, and the same purpose of reference code, by its Code Value and Coding Scheme Designator.
Longitudinal Temporal Information Modified becomes the stricter of the value already present and this run’s, in the order UNMODIFIED, MODIFIED, REMOVED, so that it never returns towards a less strict state. A value already present that is not one of these three, or more than one value, is refused rather than guessed at, as an earlier marker that cannot be read as its VR is, and the markers are not written. An empty value counts as absent.
Patient Identity Removed is YES, replacing any value already present.
Rationale. E.1.1, E.2, and E.3.6 require these markers in their text, not in Table E.1-1, so the table generator does not produce them. E.1.1 says codes and text are “added to” these attributes, so earlier markers are kept. The CID 7050 codes carry the profile claim, so output without them claims none, and each code comes from a satisfied option rather than a selected one because D-009 claims Clean Descriptors only after review. PS3.3 notes that De-identification Method may describe the extent of de-identification, for example a HIPAA Limited Data Set, so YES with a descriptive value is an established way to record de-identification that stops short of a profile. NO would be false for
tps-import, whose patient identity is removed, and E.1.2 has a re-identifier set NO while removing the method attributes. MIDI §1.14.3 recommends recording the de-identifying equipment in Contributing Equipment Sequence. The full SHA-256 digest fits one value exactly, since LO allows 64 characters, and the attribute is multi-valued. A shortened digest inside the readable value was rejected because a truncated digest proves less, and recording the digest in the report only was rejected because a file on its own could not be traced to its method. Software Versions takes one value for each piece of software, so each stays readable and within the 64 characters of LO, and the PyMedPhys version stands alone because the Manufacturer already names PyMedPhys. Recording the runtime there, rather than in the digest, shows what ran without changing the method’s identity (D-024). The markers’ fixed values, such as the ManufacturerPyMedPhys, the namecustom option set, the textno PS3.15 claim, and the codes, are not parameters of generated values, but the method digest covers the engine’s source files, which define them, so a change to any of them changes the digest (D-024).The digest comes first and the readable value second so that output reads cleanly with pydicom’s strict reading. An attribute whose values have an odd length in all is padded with one trailing space, which follows its last value. PS3.5 Section 6.4 allows the last value to exceed its VR’s maximum length by that space, but pydicom checks each value’s length before it removes the padding, so it reads a last value of the maximum length as too long: with a warning, or with an error under its strict reading. The readable value is last, and has at most 63 characters, so the padding never makes it too long. Putting the digest last was rejected because output whose two values have an odd length in all, which depends on the length of the version and the preset, could then not be read strictly. For the same reason, any other attribute that the markers would write ending in a value of its VR’s maximum length with an odd length in all is refused rather than written in a form that pydicom reads as invalid.
Every selected option other than Clean Descriptors must be satisfied, because an option’s code must be present wherever the instance carries any of the option’s actions. A Retain option keeps or modifies what the Basic Profile removes, so leaving out its code would claim more removal than was done:
public-releaseoutput without the code of Modified Dates would claim that the Basic Profile removed the dates that were shifted. Longitudinal Temporal Information Modified, set from the selected options, then agrees with the codes. Clean Descriptors may go unsatisfied because the descriptors then receive the Basic Profile’s actions, which the profile’s code claims.The
tps-importreadable value keeps the conformant value’s three fields, so that the same reading finds the software, the claim, and the preset, and says in the claim’s place that none is made. It names no edition, which could be read as a claim, and is short enough to limit the version no more thanbasic-clean-descriptorsdoes.A custom option set is named by the term this document uses for it. No preset’s name contains a space, so the two cannot be confused, and the codes and the method digest identify its options. Codes follow Table E.1-1’s order of options, the order in which every policy holds them.
A value that does not fit is refused rather than shortened, because a shortened value would misstate the version or the preset. Dropping the version where it does not fit, as D-025 does for Implementation Version Name, was rejected because the readable value must name it.
The Contributing Equipment Sequence item holds only what this decision lists. Contribution DateTime would record when each instance was de-identified, a date that Table E.1-1 removes under the Basic Profile.
Software Versions is read each time the markers are computed, rather than once when the module is imported, so that it gives the versions of the software that computes them, and importing the module loads neither pydicom nor tomlkit, which PyMedPhys imports only where they are used.
An empty De-identification Method Code Sequence would make the output invalid, so it is left out rather than written.
E.1.1 has codes and text “added to” these attributes but does not address a second run, and a value, code, or item equal to one already present adds no information, so it is not added again. A run with another release or policy has a different digest, so its readable value and digest are still recorded, and a run in another runtime environment, which has the same digest, still adds its Contributing Equipment Sequence item, whose Software Versions differ (D-024).
Longitudinal Temporal Information Modified must stay true of the instance. PS3.3’s SOP Common Module defines it as indicating “whether or not the date and time Attributes in the Instance have been modified during de-identification”, which describes the instance’s dates as they are now, and a later run cannot restore what an earlier one removed or shifted. Replacing the value would make
basicoutput given Modified Dates in a second run say MODIFIED for dates that were removed and may now be shifted dummy values, and Modified Dates output given Full Dates by another de-identifier say UNMODIFIED for shifted dates. The codes stay a flat set across runs, so this attribute, not the codes, carries the state of the dates.
Tests. Markers for each preset, including
tps-import, for a custom option set, and for input that already carries markers, whose values and items are kept; the same release and policy applied twice giving exactly the markers of one run; a different digest adding its pair, and a pair that is not present as two consecutive values added; a code already present, from another de-identifier, not added again, with new codes after it; an equal Contributing Equipment Sequence item not added again, and one with other Software Versions added; the stricter Longitudinal Temporal Information Modified for every pair of present and new values; a present value that is not one of the three refused without quoting it; values within their VR and VM limits, every version of up to 14 characters fitting every readable value within 63 characters, and a longer one refused rather than shortened; the markers of every preset, with Clean Descriptors satisfied and not, and of a custom option set, written at two versions that give their De-identification Method values an odd and an even length in all, in Implicit and Explicit VR Little Endian, read back by pydicom’s strict reading without a warning, and gaining nothing when added again to what was read back; Software Versions, and De-identification Method with another de-identifier’s value of LO’s maximum length after a pair already present, refused where they would have an odd length in all and written where they would have an even one; the markers’ pair following another de-identifier’s value of LO’s maximum length, which then reads strictly; a single value of its VR’s maximum length written; markers changed after they are computed, such as to NO or to a value too long for its VR, refused before anything is written; both De-identification Method values from every preset, the first exactly the method digest and the second the readable value, and no other digest; atps-importreadable value that claims no PS3.15 conformance; codes that are rows of the pinned CID 7050, in order, for the profile and the satisfied options only, with Clean Descriptors claimed only where satisfied; refusal of satisfied options that leave out a selected option other than Clean Descriptors, naming each option left out, except undertps-import, which needs none satisfied; a Contributing Equipment Sequence item that names PyMedPhys, has every Type 1 attribute of the SOP Common Module, and records in Software Versions the PyMedPhys version, the Python implementation and version, and the versions of pydicom and tomlkit, in that order, each a valid LO value, with a value too long for LO refused; each of the four values read from the running process when the markers are computed, so that changing its source changes it; changing the Python implementation and version and the versions of pydicom and tomlkit changing Software Versions but not the method digest in De-identification Method, so that output marked again in another runtime gains only that runtime’s Contributing Equipment Sequence item;tps-importoutput without new CID 7050 codes or an empty code sequence; no output with Patient Identity Removed set to NO; markers that quote no source value and leave every other attribute unchanged; and agreement between markers and report claims.
D-013: No Encrypted Attributes Sequence#
Decision.
The engine does not write the Encrypted Attributes Sequence and stores no recovery or consistency data in its output. Consistency comes from custodian-held keys and subject profiles outside the output (D-004). If the sequence is added later, it is for controlled sharing only and is never enabled by
public-release. The conformance statement records this interpretation.Any Encrypted Attributes Sequence (0400,0500) in the input is removed with its items, whatever its Type, before the rule for attributes that no rule covers applies (D-022). Each IOD of the first supported release defines it in the SOP Common Module as Type 1C: “Required if application level confidentiality is needed and certain recipients are allowed to decrypt all or portions of the Encrypted Attributes Data Set” (PS3.3 Table C.12-1). That condition never holds in the output, since no recipient of the output may decrypt anything in it, just as ROI Interpreter Sequence’s condition lapses once ROI Creator Sequence is removed (D-020).
Rationale. E.1.1 step 1, Note 3, deprecates other mechanisms in the data for identity recovery or longitudinal consistency “in favor of the Encrypted Attributes Data Set mechanism”. MIDI §1.17.11 calls encrypting original values into de-identified images “generally inadvisable”, especially for publicly released data. Table E.1-1 omits the sequence and both attributes of its items, Encrypted Content Transfer Syntax UID (0400,0510) and Encrypted Content (0400,0520), so, without the removal above, the rule for attributes that no rule covers would keep the sequence, keep the UID by its
definitionrole (D-003), and keep Encrypted Content as a binary value that the IOD defines there (D-022). An input’s sequence can hold original values encrypted for recipients who may re-identify them (E.1.1 and E.1.2), which this decision keeps out of the output. Giving the sequence X/Z/D, as D-022 gives the sequences it reviews, was rejected: at its Type 1C place X/Z/D resolves to D, for which D-021 gives SQ no dummy value, so every instance that carries one would be sequestered.Tests. No output contains an Encrypted Attributes Sequence or other recovery data; an instance whose input carries one is written without it, with its items removed.
D-014: No preset named after a regulation#
Decision. No preset carries a regulatory name. The documentation maps DICOM profile behaviour to regulatory frameworks without claiming compliance.
Rationale. The HIPAA Safe Harbor method is not a DICOM profile. It permits dates reduced to a year, which a DA value cannot hold, and its “comparable images” provision is open to interpretation for head imaging (MIDI §1.18.3.3).
D-015: Pixel data: detect and warn#
Decision.
Detect and report indicators of risk for review: burned-in text; reconstructable faces, including RT Structure Set contours of the head (MIDI §1.20.4); and PET series from which body weight can be recovered, such as SUV-scaled series released with attenuation-corrected images (MIDI §1.20.2). The absence of an indicator is not proof of absence.
Each instance’s attributes give its own indicators, whatever its IOD. Of burned-in text: Burned In Annotation (0028,0301) of YES; Image Type (0008,0008) whose second value is SECONDARY, since screen captures, such as dose screens, are made after the examination and usually carry text (MIDI §1.19.1); Conversion Type (0008,0064), which describes how an image was converted or captured, such as from a workstation’s screen or a scanned document (PS3.3 C.8.6.1); and an overlay group without Overlay Data (60xx,3000), whose overlay PS3.3 before 2004 let lie in unused bits of the pixel data (PS3.3 C.9.2), where it stays when the Basic Profile removes the overlay’s attributes. Of a reconstructable face: Recognizable Visual Features (0028,0302) of YES, and the contours of an RT Structure Set ROI whose RT ROI Interpreted Type (3006,00A4) or ROI Name (3006,0026) is EXTERNAL, BODY, or SKIN, the patient’s outline, comparing the whole value whatever its case. An attribute that cannot be read as its VR in the pinned data dictionary is reported as unreadable evidence of the risk it bears on. A received NO counts for nothing, as an absent attribute does. Each CT series that is a volume, whose CT images, single-frame or enhanced, hold at least two frames that are not localizers, is reported as one from which a face may be reconstructed, whatever its slice spacing: how finely it samples the face changes how readily the face is recognised (MIDI §1.18.3.2), but no spacing makes it safe, and only the pixel data could show whether it covers the face. A volume whose Body Part Examined (0018,0015) or Anatomic Region Sequence (0008,2218), at the top level or in the Frame Anatomy functional group of an enhanced image, names a region of the head or neck, or one containing it, in a reviewed list from PS3.16 Annex L, is also reported as showing the head or neck.
Never claim the Clean Pixel Data or Clean Recognizable Visual Features Options.
Keep Burned In Annotation (0028,0301) and Recognizable Visual Features (0028,0302) as received, since the pixel data are unchanged. Never change either to NO, and do not treat a received NO as evidence of safety.
Report the indicators of each released or held instance, its own and its series’, in the confidential QC pack by instance (D-016), and in the release report as counts of the instances with each risk and each indicator, by disposition, naming none.
A reported risk does not waive the
public-releasegates (D-017). Optical character recognition and defacing may later be added as optional plugins.
Rationale. PS3.15 sets these attributes to NO only as part of the Clean Pixel Data and Clean Recognizable Visual Features Options (E.3.1 and E.3.2), which the engine does not implement. Neither attribute is in Table E.1-1, so L2 needs an explicit rule.
Tests. The L2 rules keep both attributes; received values pass through unchanged, no output changes either attribute to NO, and detection results appear in the report.
D-016: Confidential QC separate from release reports#
Decision.
Logs, standard output and standard error, warnings, exception messages, output file and directory names, and release reports contain no source attribute values, original paths, or keys. They use attribute paths, opaque object identifiers, rule identifiers, and aggregate results.
Each instance is written to
<Patient ID>/<Study Instance UID>/<Series Instance UID>/<SOP Instance UID>.dcmbelow the output directory, from its replacement Patient ID (D-005) and replacement UIDs (D-003). Names therefore hold only ASCII digits, upper-case letters,.and-, and the.dcmsuffix, and a path below the output directory has at most 160 characters. A value that is not a replacement, such as a retained UID, names nothing. Instances that share a file name, and so a SOP Instance UID, are never renamed: D-026 sets what happens to them, and their run positions appear only in the confidential QC pack. The output is a plain directory tree, not a PS3.10 File-set: a File ID component holds at most eight characters from A to Z, 0 to 9, and underscore (PS3.10 8.2 and 8.5), which a UID cannot meet, so no DICOMDIR describes it.A QC pack contains only the retained strings, contextual excerpts, and image previews needed for review. It is written to an explicitly designated location restricted to authorised reviewers, treated as potentially identifying, excluded from release directories and archives and kept out of the source directory, and has documented retention and deletion.
A run’s QC pack is the JSON document
qc-pack.json, labelled with its format,pymedphys-deid-qc-pack/1. It lists each input by its run position and source path, with what the run did with it: its output path if released or a duplicate of one released; the opaque per-run label by which the release report refers to it, S-0001 onwards at one width, and why, if sequestered, as the maintainer decided on 2 October 2026 (D-026); and why, if held for review or refused; each residual by instance and place, with an excerpt of up to 48 bytes before its offset and 96 from it, decoded with the residual’s encoding, or ISO 8859-1 where Python cannot decode with it, and with each undecodable byte and each backslash written as a backslash escape; each value left out of the search, because the policy retains it, it equals a constant that the engine always writes, it cannot be decoded inside a removed sequence, or it is a UID that the pinned tables register, and each form not searched, by instance and place, as the maintainer decided on 2 October 2026 (D-027); each distinct retained string with every place it was retained, a retained string being a value that the instance’s plan keeps as it is, in an element with a text VR other than UI, outside any removed sequence, and other than Specific Character Set, an instance whose retained string cannot be decoded for review, such as text outside ISO 646 where no Specific Character Set applies, being sequestered rather than released unreviewed (D-017); and what descriptor cleaning wrote for each ROI Name, renamed, empty, kept, mapped, emptied, held for review, or emptied unreviewed, with why a held name was held and the names of the run’s institutional list that it matched (D-009); each first-pass finding that the run reports without acting on it, at each instance it names, with its kind, the attribute’s tags, and its count (D-026); each instance in a high-risk category, with the indicators of risk in its pixel data that put it there (D-015, D-017); each series of released or held instances that is a CT volume, with the instances that make it one, and the attributes that name the head or neck and the instances that hold them, and each attribute that its series assessment could not read, the series being grouped by the source’s Series Instance UID, which the pack does not write (D-015); and each image preview for D-017’s review, by its file in apreviewsdirectory beside the pack, with the frames it shows, by run position and frame index, how many frames its series or instance has, and the file’s SHA-256, and each instance whose pixel data could not be previewed, with why. Its opaque reference is random,A-and 32 hexadecimal digits, not derived from its content. Each run writes its QC pack before it publishes the release, from each input’s outcome and what its transform and gate found, so a release never exists without one; a pack that cannot be written publishes nothing, and the pack of a release that cannot then be published is removed, or named in the error if it cannot be. The location has no default, and a run checks it before it creates anything. It must be neither inside the source, release, or staging directory nor contain any of them, compared by resolved path, without case on Windows and macOS, and by the device and inode of each existing directory, which finds a directory spelt otherwise on a case-insensitive share or reached through a bind mount; the checks are repeated once the location is created. It must be new or an empty directory; on POSIX an existing one must belong to the current user and grant its group and others no access, and the pack’s directory is opened without following a symbolic link and given mode 0700, as is itspreviewsdirectory, which holds a copy of the marker, and its files 0600, while on Windows access rests on the location’s access control lists. A marker file,.pymedphys-deid-qc-pack, written first, lets a release step refuse any tree that holds QC material, or a symbolic link to some, and a tree that cannot be read is reported rather than passed. A handling notice beside the pack states that it may identify people, is for authorised reviewers only, is never copied into a release, and is deleted once the review, the attestation, and the custodian’s documented retention period no longer need it. No file is overwritten, and errors name the check that failed and the operating system’s reason, never a path or value.Key and subject-profile stores are separate custodian-controlled state and are included in neither. The reviewed-names list that ROI name cleaning reuses (D-009) is confidential state too, since it holds source ROI names verbatim and persists across runs. The maintainer decided on 1 October 2026 that it is held in the custodian’s store with the key and subject profiles, protected as they are (D-004), and included in neither; only its keyed digest enters the method digest (D-024).
Release reports record an opaque attestation reference and outcome, not the review material.
A reviewer attests to a QC pack once, in
attestation.jsonbeside it, labelledpymedphys-deid-qc-attestation/1, naming themselves and the time in UTC, stating whether they reviewed every distinct retained string, every series, and every instance in high-risk categories (D-017), and giving the outcome, attested or rejected; attested needs all three reviews. The attestation records the SHA-256 of the pack’s file, which records the SHA-256 of each of its previews, so a pack or preview changed after it was attested is refused, and a pack whose previews do not match it cannot be attested. The attestation is confidential with the pack, since it names the reviewer, and the release report records only the pack’s reference, the outcome (attested, rejected, or not attested where no attestation has been written), and the releaser’s two confirmations described below, each true, false, or not stated. As the maintainer decided on 6 October 2026, a public command records the attestation in the QC pack’s directory, through the same library function, naming the pack’s directory, the reviewer, the outcome, and each review done, and kept out of thepymedphyscommand line until the first supported release, like the tool’s other commands. It refuses a directory that holds no QC pack, and neither its summary nor its messages quote the reviewer or a path. It does not change the release report published with the run, which records the outcome when the run wrote it, not attested, since the design does not provide for rewriting a published report; the attestation stays with the pack, from which the record a report takes is read. The person releasing the data may also confirm in the attestation, each yes or no and neither required, that the output was checked for its intended use and that its residual risk was accepted under the institution’s governance; the confirmations hold no text, are bound to the pack with the rest of the attestation, and reach the record that a release report takes as their outcomes alone, since fitness for a use and an acceptable residual risk depend on the purpose and the recipient, which only the releaser knows. This was decided on 6 October 2026 for the first release, which assigns both confirmations to whoever releases the data and has each run’s release report also summarise the engine’s structural checks of its output.
Rationale. Human review needs material that may still identify people; routine diagnostics and distributable reports do not. Names built from replacement values are the same under one key in every run and processing order, whereas renaming instances that share a name would make each name depend on the other instances, and so rename earlier output when an incremental export adds instances. Naming a file by its replacement SOP Instance UID lets a reviewer find output from the identifiers in reports. The 160-character bound leaves 98 characters for the output directory within the 259 that Windows allows a path without long-path support.
Tests. Captured logs and errors contain no source values or paths; residual search results and errors contain no source value in any form; output names contain no source path, file name, or value, are portable and within the length bound, and are refused for any value that is not a replacement; instances that share a file name are found by their run positions, not renamed; release archives contain no QC or state artefacts; the QC destination and access requirements are enforced; and a reviewer can locate affected output from opaque identifiers.
D-017: Public-release risk assessment#
Decision. A
public-releaserun is marked ready for release only when three gates pass: statistical disclosure control, pixel and face review, and a human QC attestation.Statistical gate. Accept a maximum per-subject re-identification probability of 0.09 by default, or 0.05 when selected, under an explicitly selected and validated model.
The assessment names and versions the model and its implementation. It defines the attacker’s assumed knowledge, including knowledge of membership, and identifies the reference population and sampling assumptions, or justifies assessment within the release cohort. It records matching variables, generalisation, missing-data handling, uncertainty, and exclusions.
It counts distinct subjects, not images, frames, or visits, and accounts for linked longitudinal records and earlier linked releases. A sample-based
1/kcalculation is permissible only with a documented known-membership or equivalence-class model.The assessment is bound to the exact collection and policy versions, and any change requires reassessment. A missing model, incomplete subject coverage, an unavailable estimate, or a failed threshold fails the gate; no default score substitutes.
A documented external assessment may supply the evidence. An automated estimator is enabled only after it has been selected and validated.
Human review. Review every distinct retained string, every series (by maximum intensity projection or cine strip), and every instance in high-risk categories. Potentially reconstructable facial information blocks release unless a risk assessment reference is recorded. Review material follows D-016.
The QC pack previews the files that the run released or held for review, since the engine never changes pixel data. A series, the instances that share a Series Instance UID, ordered along the normal where they share an orientation and every one has a position, or else by Instance Number where every one has one, or else by run position, is shown as a cine strip of up to 16 frames, evenly spaced from its first frame to its last, each scaled to fit 256 pixels. A series of at least three single-frame instances of one size, orientation, and pixel spacing, at distinct positions stacked along the normal, and with axes along the patient’s three directions, each with its other two direction cosines at most \(10^{-4}\), is also shown as a frontal maximum intensity projection, projected along the anterior-posterior axis, head up and with the patient’s right on the viewer’s left, sampled to square pixels. An oblique stack has a cine strip only, since a projection along an oblique axis would superimpose what a frontal view keeps apart. Pixel padding is shown black.
An instance is in a high-risk category where its source’s attributes show an indicator of risk in its pixel data (D-015). It is listed with its indicators and previewed at full resolution, every frame or 16 evenly spaced, since burned-in text may be small. The indicators of a whole series, of a CT volume and of the head or neck, are assessed over the series’ released and held instances, since only they reach a recipient, listed with the series, and reviewed through its series’ previews; an RT Structure Set with the patient’s outline has no pixel data, so it is listed as not previewed, and its outline’s face is reviewed through the images it is drawn on.
Monochrome pixel data are rescaled by Rescale Slope and Intercept and windowed from the 0.5th to the 99.5th percentile of what each preview shows, leaving out the values that Pixel Padding Value names, which are shown black, and MONOCHROME1 is inverted. RGB pixel data of 8 bits are shown as they are. Pixel data in a compressed transfer syntax, of another photometric interpretation, as float values, or that cannot be decoded are listed as not previewed, with why, rather than left out, and so is a released or held instance whose written file the run did not hand to the pack. These are the defaults that the QC pack thread chose on 6 October 2026.
Open question. A ROI Name that automatic cleaning writes in the vocabulary’s spelling is a cleaned value, not a retained string (D-009), so the human review gate’s review of every distinct retained string does not cover these renames. Whether
public-releaseshould review them too is not yet decided.Rationale. MIDI §1.15.2.2 describes 0.09 as a common choice and 0.05 for more sensitive collections. Sections 1.15.1.1 and 1.15.1.2 show that such a number is meaningful only with a group size, reference population, threat model, and choice of maximum or average risk, and that public releases are assessed on maximum risk with attack assumed certain. The statistic covers only the modelled disclosure channels, so it cannot replace pixel review or human QC, or establish legal anonymity.
Tests. Subject versus instance counts; longitudinal and prior-release linkage; missing model, assumptions, or coverage; threshold boundaries; failed or stale assessments; and independence of the three gates.
D-018: Published benchmark results#
Decision.
For each release of the engine, publish results against the NCI MIDI validation resources as separate metrics rather than a single score, recording versions and supported coverage. Each implementation pull request adds its own tests and traceability, and M6 consolidates the evidence for the first supported release. Releases that change only documentation or legacy code claim no engine results.
Use the MIDI-B collection, with its synthetic and curated datasets, answer keys, and mapping files (doi:10.7937/cf2p-aw56), and the Pseudo-PHI-DICOM-Data collection (doi:10.7937/s17z-r072), both licensed CC BY 4.0, with the NCI validation script (Apache-2.0).
Benchmark runs download the collections. Tests may use small subsets cached through the PyMedPhys Zenodo data store, with attribution, the licence, and a note of changes. The validation script’s repository is not copied into PyMedPhys, because it also bundles third-party executables.
The benchmark harness is run on the TCIA collections only on the maintainer’s own machine, not in automated sessions, as the maintainer decided on 6 October 2026.
Report results per answer-key category, and document each category where PyMedPhys’s policy deliberately differs from the answer key.
Rationale. Separate, versioned metrics let users judge performance for their own data and coverage. The validation manual states that many answers follow TCIA’s original curation of each source collection rather than strict DICOM requirements, so an aggregate score would penalise deliberate, conformant differences. CC BY 4.0 permits redistribution with attribution, and the collections are too large to keep in the repository.
Tests. The benchmark workflow reproduces the published results, and cached subsets carry their attribution.
D-019: No deprecation before a released replacement#
Decision.
Disclose the legacy tools’ limitations now with warnings that are not deprecations and promise no removal. For experimental pseudonymisation, these are a
UserWarningsubclass in the library, a standard-error notice on the command line, and a banner in the app. Forpymedphys.dicom.anonymise, they are corrected documentation and docstrings, aUserWarningsubclass from the library function, and a standard-error notice on the command line.Deprecate a legacy interface (library, command line, or app) only once a released replacement covers its use and migration guidance is published, which is no earlier than the first supported release (M6). That release replaces the library and the command line, not the app (Scope), so the legacy pseudonymisation app stays, with its limitation banner, until the new app ships.
Keep each legacy interface for at least one full minor release with deprecation warnings, then remove both in the same release. Record exact versions in the release notes when they are scheduled.
Until removal, change the legacy tools only to stop them disclosing identifying information or to prevent harm to data, such as silently altering or losing source files. List other defects among the limitations in the user guidance instead of fixing them.
Rationale. Deprecating before a replacement exists would leave users nowhere to go and, if the work stalled, would make the warning permanent noise. If the replacement is not delivered, both legacy interfaces remain available with their documented limitations. Other fixes to the legacy tools are spent on code with a limited life, and can change behaviour that users rely on, as a new pseudonym format would; disclosure and data-safety fixes protect people and data until the replacement is available, which justifies that cost.
Tests. Limitation warnings are not
DeprecationWarnings and mention no removal; each public call warns once; and the command-line and app notices appear. Once deprecation starts, the deprecation warnings are tested and the migration guide is published.
D-020: Compound actions from the strictest Type#
Decision.
A compound action of Table E.1-1a (X/Z, X/D, Z/D, X/Z/D, or X/Z/U*) is resolved from the strictest Type that the instance’s IOD gives the attribute at that place in the data set, across every definition of it there (D-001). Type 1C counts as 1 and 2C as 2, and modules of conditional (C) and user-optional (U) usage are included, so no condition is evaluated.
The action is then D for Type 1, Z for Type 2, and X for Type 3. Where the compound action does not offer that action, the next of X, Z, and D that it offers applies, as the definitions in Table E.1-1a imply: X/D gives D for Type 2, as for RT Plan Date (300A,0006) in the RT Plan IOD, and Z/D gives Z for Type 3. In X/Z/U*, U, the replacement of the instance UIDs that the sequence contains, takes the place of D.
X/Z on an attribute that is Type 1 or 1C at its place writes D’s dummy value, as a plain Z there does, which the maintainer decided on 1 October 2026. Table E.1-1a defines Z as “replace with a zero length value, or a non-zero length value that may be a dummy value and consistent with the VR”, so this is the Z that X/Z offers, with a non-zero-length dummy value, and the conformance statement describes it so. In 2026d, no attribute with X/Z is Type 1 or 1C anywhere in the first supported release’s IODs. Of the other generated IODs, only Referenced Study Sequence (0008,1110) is, in three sequences of the RT Patient Position Acquisition Instruction and RT Physician Intent IODs; it is a sequence, which has no generic dummy value, so it needs a reviewed rule, and until one exists its instance is sequestered (D-021).
An attribute that the IOD does not define at that place is treated as Type 3, so it is removed, or emptied under Z/D.
The Type also decides how a plain X, D, or Z is applied; the maintainer decided these rules on 1 October 2026:
Where Table E.1-1 gives a plain X, the attribute is removed, as Table E.1-1a defines X, whatever its Type. Where the IOD requires it at that place, by its strictest Type as above, the engine keeps the output valid by also removing the innermost sequence that encloses it and that the IOD makes Type 3 at its own place, with everything in that sequence, and sequesters the instance where no such sequence encloses it. This includes an X that applies under Clean Descriptors where descriptor cleaning applies the Basic Profile’s action (D-009). Two attributes need neither. Where Overlay Data (60xx,3000) is removed, every attribute of its repeating group is removed with it, since, of the first supported release’s IODs, only CT Image includes the Overlay Plane Module, and as user-optional (U). ROI Interpreter Sequence (3006,004E) is removed, as the table says. PS3.3 Table C.8-44 makes it Type 1C: “Required if ROI Creator Sequence (3006,004D) is present in the Structure Set Module, and the person or device performing the interpretation differs from ROI Creator Sequence (3006,004D). May be present otherwise.” Table E.1-1 removes ROI Creator Sequence too, so that condition never holds in the output.
The basis is Table E.1-1a, which defines X as removal and gives an alternative by Type only through its compound actions, such as X/Z and X/D, and E.1.1’s Note, which makes it “the responsibility of the de-identifier to ensure that this process does not negatively affect the integrity of the Information Object Definition”. Removing an optional sequence that encloses the attribute does both: the attribute is removed, as the table says, and the IOD stays valid, so the instance keeps an exact Basic Profile claim. For example, Responsible Person (0010,2297) is Type 2C in the Patient Module, “Required if the Patient is a non-human organism. May be present otherwise.” Since conditions are not evaluated, it counts as required there, and no sequence encloses it, so an instance that has it is sequestered, as is one with Responsible Organization (0010,2299); both are rare outside veterinary data.
In 2026d, the first supported release’s IODs have seven attributes with a plain X that the IOD requires and that no removed sequence encloses: Series Description (0008,103E), Type 1 in Source Series Information Sequence (3006,004C) of the RT Structure Set IOD; ROI Interpreter Sequence, Type 1C in RT ROI Observations Sequence (3006,0080) of the RT Structure Set IOD; Responsible Person (0010,2297) and Responsible Organization (0010,2299), Type 2C in the Patient Module of each IOD; Patient Treatment Preparation Procedure Parameter Description (300A,078E), Type 2 in Patient Treatment Preparation Procedure Sequence (300A,0790), and Patient Setup Photo Description (300A,0794), Type 2 in Referenced Patient Setup Photo Sequence (300A,078C), both sequences within Patient Treatment Preparation Sequence (300A,079F), at the top level of the CT Image IOD and within Patient Setup Sequence (300A,0180) of the RT Plan IOD; and Overlay Data, Type 1 in the Overlay Plane Module of the CT Image IOD. Source Series Information Sequence and Referenced Patient Setup Photo Sequence are Type 3 wherever these IODs define them, and Patient Treatment Preparation Sequence is Type 3 at the top level of the CT Image IOD and within Patient Setup Sequence, so Series Description and the two descriptions are removed with the sequence that holds them; Overlay Data and ROI Interpreter Sequence are removed as above; and an instance with Responsible Person or Responsible Organization is sequestered.
A plain D on an attribute that the IOD does not define at that place acts as X there, and a plain Z there stays Z, following Note 13 after Table E.1-1a: Verifying Observer Sequence (0040,A073) “is only defined in structured report IODs and hence is described in Table E.1-1 as D since it is Type 1C; if encountered in an image instance, it should simply be removed (treated as X)”.
A plain Z on an attribute that is Type 1 or 1C at that place writes D’s dummy value, which Table E.1-1a’s definition of Z allows: “replace with a zero length value, or a non-zero length value that may be a dummy value and consistent with the VR”. In 2026d, no IOD of the first supported release has such a place.
Z and D write the values that D-021 sets.
Rationale. The strictest Type never removes an attribute that the IOD may require, so compound actions never make the output invalid, and it never retains more, because Z and D still replace the value. The cost is occasionally writing an empty or dummy value where removal was allowed. Evaluating conditions was rejected: PS3.3 writes them as free text, so an interpreter would be hand-built and error-prone, and it would gain almost no privacy. Removing every attribute that is not Type 1 was rejected because removing a Type 2 attribute makes the object invalid, which breaks import into planning systems. A plain X is applied differently: Table E.1-1a defines X as removal, so where the IOD requires the attribute, removing an optional sequence that encloses it keeps both the table’s action and the IOD’s integrity, and the rare instance where no such sequence exists is sequestered. Writing an empty or dummy value in place of a plain X was rejected because it is not the removal that the table specifies, so the exact Basic Profile claim would rest on reading E.1.1’s general permission to replace a value as overriding the table, which the standard does not state. Leaving such instances out of the conformance claim was rejected for the same reason: removal keeps the claim for every instance that it can keep valid. Removing the whole overlay group leaves a valid CT Image, since its Overlay Plane Module is optional, and removing ROI Interpreter Sequence leaves a valid RT Structure Set once ROI Creator Sequence is gone. An attribute that the IOD does not define at that place needs no value, so D removes it, as Note 13 describes, and Z, which writes nothing identifying, is kept as the table gives it. A plain Z on a Type 1 attribute writes a dummy value because an empty value would make the output invalid. Table E.1-1a defines X/Z as “X unless Z is required to maintain IOD conformance (Type 3 versus Type 2)”, so it does not say what X/Z does at a Type 1 place; the Z it offers allows a dummy value, as a plain Z does, so X/Z there writes one too. Sequestering the instance instead was rejected, where the attribute’s VR has a dummy value (D-021), because it would discard an instance that the profile can de-identify.
Tests. Each compound action gives the specified action for Types 1, 1C, 2, 2C, and 3; an attribute with different Types in different modules, including modules of conditional or user-optional usage, gets the action of its strictest Type; the Type depends on the enclosing sequence; an attribute that the IOD does not define at that place is removed, or emptied under Z/D; X/Z on a Type 1 or 1C attribute writes the dummy value, and does so in the generated IODs only for Referenced Study Sequence in the three sequences named above; and no Type 1 or Type 2 attribute with a compound action is removed. A plain X removes the attribute, and where the IOD requires it at that place, also the innermost enclosing sequence that is Type 3 at its own place, as for Series Description in Source Series Information Sequence and the two Patient Treatment Preparation descriptions, or sequesters the instance where there is none, as for Responsible Person and Responsible Organization; removing Overlay Data removes its whole repeating group, and ROI Interpreter Sequence is removed; a plain D on an attribute that the IOD does not define at that place, such as Verifying Observer Sequence in a CT image, removes it, and a plain Z there empties it; and a plain Z on a Type 1 or 1C attribute writes the dummy value.
D-021: Dummy values#
Decision.
Z writes a zero-length value, except where a dummy value is already decided: the keyed pseudonyms for Patient ID and Patient’s Name (D-005), the synthetic birth dates of
tps-import(D-008), and D’s dummy value where a plain Z, or the Z that X/Z offers, applies to an attribute that is Type 1 or 1C at its place (D-020).D writes one conspicuous constant for each VR, valid for that VR:
DEIDENTIFIEDfor the text VRs (LO, SH, LT, ST, UC, and UT) and PN;19000101for DA;000000for TM;19000101000000for DT; zero for the numeric string VRs (DS and IS) and the binary number VRs (FL, FD, SL, SS, SV, UL, US, and UV); and the keyed replacement for UI (D-003). Where the constant equals the source value, a second constant is used, so the value always changes.The second constants are
DE-IDENTIFIED,19000102,000001,19000101000001, and one. A source value equals a constant when the two match as their VR defines them: text and PN ignoring case and padding, and a PN’s trailing^and=delimiters; DA, TM, and DT as the same date or time, ignoring a DT’s UTC offset and taking absent components at their least values; and the numeric VRs as the same number, so0.0equals zero. Equality is deliberately broad, since a value treated as equal still changes. Where any of an attribute’s source values equals the constant, the second is used. D writes the fewest values that the attribute’s VM allows, each the constant, and for UI the replacement of each source UID, even one that the pinned tables register, since D must replace the value.CS, SQ, and every other VR have no generic dummy value, nor has a UI attribute without a source UID to replace. A D on an attribute of such a VR needs a reviewed rule for that attribute; without one, the instance is sequestered rather than given an invalid value.
Person Identification Code Sequence (0040,1101), which Table E.1-1 gives D, has such a rule, which the maintainer decided on 1 October 2026. D writes one item, with Code Value (0008,0100)
DEIDENTIFIED, Coding Scheme Designator (0008,0102)99PYMEDPHYS, and Code Meaning (0008,0104)DEIDENTIFIED^DEIDENTIFIED. Where a source item’s Code Value or Code Meaning equals its constant, compared as above, the item takes the second constants,DE-IDENTIFIEDandDE-IDENTIFIED^DE-IDENTIFIED, with the same designator. In the first supported release’s IODs, the only place where Table E.1-1 removes no sequence that encloses it is Asserter Identification Sequence (0044,0103) in RT Assertions Sequence (0044,0110) of the RT Plan IOD, where it is Type 2C. The rule is implemented with the engine’s writing of values (M3).Dummy values never name PyMedPhys; the markers do (D-012). The one exception is the Coding Scheme Designator
99PYMEDPHYSof the item above, which the maintainer chose on 1 October 2026: it names who defines the code, not the person, and the item’s Code Value and Code Meaning are the constants. The conformance statement describes each dummy value, as E.1.3 requires.
Rationale. Constants are obviously synthetic, reveal nothing, are deterministic, and are always valid for their VR, and a zero-length value carries the least information and is always valid for Type 2. Plausible random values were rejected because they can be mistaken for real data, may match real values, and are not reproducible. Keyed values derived from the source were rejected because equal source values would give equal dummy values, which leaks equality and links records. Dummy values never name PyMedPhys because they sit in clinical fields, where they read as data:
DEIDENTIFIEDmeans the same to any reader, and the markers are where the de-identifier describes itself. The Coding Scheme Designator of the dummy code is a narrow exception, because a private coding scheme’s designator conventionally identifies who defines its codes. PS3.16 Chapter 8 requires a local or private coding scheme to be identified by an alphanumeric designator beginning with “99”, as HL7 V2 Table 0396 specifies, and PS3.3 Section 8.2 notes that HL7 V2 defines such designators as private or local; the rest of a private designator conventionally names the scheme’s owner.99PYMEDPHYS, 11 of the 16 characters that SH allows, therefore says which organisation defined the code. It is metadata about the code, not content that reads as data about the person, and the Code Value and Code Meaning stay the conspicuous constants. The item follows PS3.3 Table 10-1, the Person Identification Macro, which requires “One or more Items” in the sequence and lets Code Meaning follow the rules of PN, “except that a single component (i.e., the whole name unseparated by caret delimiters) is not permitted”, so the constant has two components. At the place above, in Asserter Identification Sequence, the Assertion Macro (PS3.3 Table 10.30-1) defines the sequence instead through Table C.17-3b, the Identified Person or Device Macro, as Type 2C with “Zero or one Item shall be included in this Sequence”; the one item satisfies both macros.Tests. Z writes a zero-length value except for the decided pseudonyms and synthetic birth dates, and D’s dummy value where the attribute is Type 1 or 1C; D writes its VR’s constant, or the second constant where the source value equals the first in any form, such as padded, in another case, or
0.0for DS; every dummy value is valid for its VR and its attribute’s VM; different source values give the same dummy value; a D on an attribute of another VR without a reviewed rule sequesters the instance; and D on Person Identification Code Sequence writes one item with the constants above, or with the second constants where a source item’s Code Value or Code Meaning equals the first, each valid for its VR.
D-022: Text and other attributes that Table E.1-1 omits#
Decision.
A reviewed L2 list gives an action, attribute by attribute, to each text attribute (VR LO, SH, LT, ST, UC, or UT) that Table E.1-1 omits and a supported IOD uses. In 2026d, the first supported release’s four IODs use 110. They range from coded vocabulary, such as Code Value, Coding Scheme Designator, and Code Meaning; through equipment descriptions, such as Manufacturer, Software Versions, and Convolution Kernel; to operator-typed RT text, such as Beam Name, Block Name, Patient Setup Label, Dose Comment, Setup Image Comment, and Text Value (0040,A160); accessory identifiers, such as Wedge ID, Block Tray ID, Applicator ID, Compensator ID, and General Accessory ID; and accessory codes that a device reads, such as Accessory Code.
The list puts each attribute in one of three groups, apart from the stricter rules below:
operator-typed labels and text: X/Z/D, resolved by Type (D-020), and C under Clean Descriptors (D-009). In 2026d, there are 46.
accessory and equipment identifiers, treated as device identity: X/Z/D, and K under Retain Device Identity. In 2026d, there are 19.
coded and technical values: K. In 2026d, there are 27: codes and the vocabulary that qualifies them, Code Value, Long Code Value, Coding Scheme Designator, Coding Scheme Version, Coding Scheme Registry, Coding Scheme External ID, and Code Meaning; values that the standard constrains, Strain Nomenclature, Genetic Modifications Nomenclature, Rescale Type, Overlay Subtype, Fraction Pattern, and Device Alternate Identifier Format; values that equipment or software writes, Manufacturer, Manufacturer’s Model Name, Manufacturer’s Model Version, Software Versions, Algorithm Version, Convolution Kernel, Filter Type, Fluence Mode ID, and Source Isotope Name; and the identifiers of private data elements that software writes, Private Creator Reference, Private Data Element Name, Private Data Element Keyword, Selector Sequence Pointer Private Creator, and Selector Attribute Private Creator. Free text that a person types, such as a comment, label, or description, is never in this group.
Names and identifiers of people, organisations, records, and networks, and identifiers that a device reads, have a rule stricter than that of operator-typed text: X/Z/D, resolved by Type, under every option, with no action under Clean Descriptors or Retain Device Identity. It is chosen because these values name people, organisations, records, or networks, or, read by a device such as a bar-code reader, identify accessories often made for one patient, rather than describe content as descriptors do. In 2026d, there are 13: Coding Scheme Responsible Organization (0008,0116), Mapping Resource Name (0008,0122), Strain Stock Number (0010,0214), Strain Source (0010,0217), Breed Registration Number (0010,2295), NTP Source Address (0018,1803), Algorithm Source (0024,0202), Local Namespace Entity ID (0040,0031), Universal Entity ID (0040,0032), HL7 Instance Identifier (0040,E001), Storage Media File-set ID (0088,0130), and the accessory identifiers that a device reads, Accessory Code (300A,00F9) and Tray Accessory Code (300A,0355).
The text of overlays, Overlay Description (60xx,0022) and Overlay Label (60xx,1500), has the same stricter rule, because Table E.1-1 cleans overlay text only under Clean Graphics, which Scope excludes. With the two alternative calendar dates and De-identification Method, below, the three groups (46, 19, and 27) and these 15 make the 110.
Every rule carries a note giving the reason for its action.
Patient’s Birth Date in Alternative Calendar (0010,0033) and Patient’s Death Date in Alternative Calendar (0010,0034) are among the 110: their VR is LO, but they hold dates. They are removed (X), whatever group rule would otherwise apply. Each is Type 3 in the Patient Module of the first supported release’s IODs.
De-identification Method (0012,0063), which Table E.1-1 also omits, is among the 110 too. The engine writes it itself: it keeps the existing values and adds its own, as D-012 sets.
A text attribute that the list does not cover is removed by default: X/Z/D, resolved by Type.
The maintainer decided on 1 October 2026 the rules for the other attributes that Table E.1-1 omits:
Of the 12 URI and URL attributes, of VR UR, that the table omits, URN Code Value (0008,0120), which holds a code’s value where that value is a URN or URL, and Coding Scheme URL (0008,010E) are kept (K), like Code Value and Coding Scheme Designator, since they name coding concepts and schemes. The maintainer decided this on 1 October 2026. URN Code Value is Type 1C in the Basic Code Sequence Macro (PS3.3 Table 8.8-1a), and Coding Scheme URL is Type 1 in the items of Coding Scheme Resources Sequence (0008,0109), which is Type 3 within the Type 3 Coding Scheme Identification Sequence (0008,0110) of the SOP Common Module.
Each of the other ten gets X/Z/D, resolved by Type. The first supported release’s IODs use three of them: Retrieve URL (0008,1190), Pixel Data Provider URL (0028,7FE0), and Retrieve URI (0040,E010). The other seven are Stored Instance Base URI (0008,0407), Folder Access URI (0008,0408), File Access URI (0008,0409), Filename in Container (0008,040B), Storage URL (0040,4073), Relative URI Reference Within Encapsulated Document (0068,7005), and Contact URI (0074,100A).
Seven sequences whose content can identify an institution or other patients or say something about the patient get X/Z/D, resolved by Type: Requesting Service Code Sequence (0032,1034), the “Institutional department, unit or service where the request initiated” (PS3.3 Table C.7-3, General Study Module); Patient’s Size Code Sequence (0010,1021) (Table C.7-4a, Patient Study Module); Issuer of Patient ID Qualifiers Sequence (0010,0024) (Table 10-18, the Issuer of Patient ID Macro, which the Patient Module includes), which holds Assigning Facility Sequence (0040,0036), Assigning Jurisdiction Code Sequence (0040,0039), and Assigning Agency or Department Code Sequence (0040,003A); Issuer of Accession Number Sequence (0008,0051) (Table C.7-3); Reason For Performed Procedure Code Sequence (0040,1012) (Table C.7-3); and Source Patient Group Identification Sequence (0010,0026) and Group of Patients Identification Sequence (0010,0027) (Table C.7-1, Patient Module).
ICC Profile (0028,2000) and MAC Parameters Sequence (4FFE,0001) get X/Z/D, resolved by Type.
Patient’s Body Mass Index (0010,1022), Measured AP Dimension (0010,1023), and Measured Lateral Dimension (0010,1024), measures of the patient’s body like Patient’s Size (0010,1020) and Patient’s Weight (0010,1030), which Table E.1-1 removes (X), get X/Z/D, resolved by Type. They are Type 3 in the Patient Study Module (PS3.3 Table C.7-4a), so they are removed in the first supported release’s IODs under every option, including Retain Patient Characteristics, as Patient’s Size Code Sequence is.
The seven sequences, ICC Profile, and MAC Parameters Sequence are Type 3 wherever the first supported release’s IODs define them, so each is removed there, as are Retrieve URL and Retrieve URI, which those IODs require only inside sequences that Table E.1-1 removes. The third, Pixel Data Provider URL, is Type 1C in the Image Pixel Module of the CT Image and RT Dose IODs, which requires it only for the JPIP Referenced transfer syntaxes, which the first supported release does not read. There X/Z/D resolves to D, for which D-021 gives UR no dummy value, so an instance with such a value there is sequestered unless a reviewed rule gives one.
Any other attribute of the pinned dictionary that no rule covers, after Table E.1-1, these rules, the UID roles (D-003), the rule for text above, and the engine’s own removals and writes (D-012, D-013), is kept if its VR is CS, AT, or a numeric VR (DS, IS, FL, FD, SL, SS, SV, UL, US, or UV). A sequence, or a binary value of VR OB, OD, OF, OL, OV, OW, or UN, is kept only where the IOD defines the attribute at that place, and removed elsewhere, so that a sequence that no supported IOD defines, such as Discharge Diagnosis Code Sequence (0038,0044), cannot keep the codes in its items. The attributes in a kept sequence’s items are handled by the same rules. Such an attribute of any other VR is removed; in 2026d there is none, since every AE and AS attribute is in Table E.1-1. An attribute absent from the pinned dictionary is removed, as Architecture item 2 sets.
The maintainer decided on 6 October 2026 how the first supported release treats an institution’s name in local codes and private creators. Codes of a local coding scheme, whose Coding Scheme Designator begins with “99” or is “L”, which PS3.3 Section 8.2 says HL7 V2 defines as private or local Coding Schemes, keep their Code Value, Coding Scheme Designator, and Code Meaning, like every other code, although they can carry an institution’s name or abbreviation. The conformance statement says so, and says that each such value is listed among the QC pack’s retained strings (D-017). Private Data Element Characteristics Sequence (0008,0300), whose items hold Private Creator Reference (0008,0302), is removed whenever no private attribute is kept (D-028). Selector Attribute Private Creator and Selector Sequence Pointer Private Creator are kept, as above.
Rationale. E.1.1 leaves the de-identifier responsible for identifying information in these attributes. The list is auditable and consistent with how Table E.1-1 treats similar attributes: RT Plan Label is D, RT Plan Name and Beam Description X, and ROI Name Z, all C under Clean Descriptors, and Treatment Machine Name is X/Z, and K under Retain Device Identity. Removing every omitted text attribute that is not listed as safe was rejected: it would replace Code Value (0008,0100), which is Type 1C in the Basic Code Sequence Macro (PS3.3 Table 8.8-1a), and so destroy coded meaning, and it would strip technical values that are useful and not identifying, such as Convolution Kernel. Keeping them all was rejected because it fails E.1.1: beam names and comments often contain patient names and dates. Accessory identifiers are not kept in the first supported release, whose presets do not select Retain Device Identity;
tps-importkeeps them as typed, including those of accessories usually made for one patient, such as Compensator ID. Names and identifiers of people, organisations, records, and networks are removed under every option, as Table E.1-1 removes comparable attributes, such as Operators’ Name, Institution Name, Clinical Trial Sponsor Name, Issuer of Patient ID, and Container Identifier, under every supported option; Mapping Resource Name is among them because PS3.3 Section 8.4 says it may denote the institution or organisation that specified the value set. Accessory Code and Tray Accessory Code are removed under every option because PS3.3 describes each as an identifier intended to be read by a device such as a bar-code reader, nearly as it describes Device Alternate Identifier (3010,001B), which Table E.1-1 empties under every option, including Retain Device Identity. Table E.1-1 at most cleans network identities, such as Station AE Title, even under Retain Device Identity, so NTP Source Address is removed under every option; and it cleans Overlay Comments only under Clean Graphics, which Scope excludes, so Overlay Description and Overlay Label are removed under every option too. Manufacturer, Manufacturer’s Model Name, and Software Versions are kept because they identify products, which every site that uses them shares, rather than devices: Table E.1-1 omits them while listing Device Serial Number, and Manufacturer is Type 1 in Contributing Equipment Sequence (PS3.3 Table C.12-1). A device made in-house can carry the hospital as its Manufacturer. That residual risk is accepted because the value names an institution, not a patient:public-releasereviews every distinct retained string (D-017), but the first supported release’s presets keep such a value without review. A URI or URL can carry host names and file paths, so the UR attributes are removed by Type, as the Basic Profile removes or replaces the three UR attributes that Table E.1-1 lists, such as Display URI (0040,E012). URN Code Value and Coding Scheme URL are the exceptions: they name coding concepts and schemes, as Code Value and Coding Scheme Designator do, and removing them where they are required would sequester an instance, since UR has no dummy value, although Coding Scheme URL is often the public URL of a published coding scheme. A local scheme’s URL can name an institution’s host; that residual risk is accepted, as for Manufacturer, because it names an institution, not a patient. The first five of the seven sequences hold codes or identifiers like those of attributes that Table E.1-1 removes or empties: it gives Requesting Service (0032,1033), Patient’s Size (0010,1020), and Issuer of Patient ID (0010,0021) X, Accession Number (0008,0050) Z, and Reason for Visit Code Sequence (0032,1067) and the other reason and diagnosis code sequences X. Table E.1-1 keeps Patient’s Size under Retain Patient Characteristics, whichtps-importselects, whereas Patient’s Size Code Sequence is removed under every option. The difference is deliberate: E.3.7 requires the option’s characteristics to be retained “as described in Table E.1-1”, which does not list the sequence, so removing it is consistent with the option, and Architecture item 2 lets a rule give an action under an option only to a text attribute or to a date, time, or datetime. The other two, Source Patient Group Identification Sequence and Group of Patients Identification Sequence, hold Patient IDs other than the instance’s own, of the subjects in an instance that holds a group of subjects acquired together, and of the group’s source instances, from which an instance was extracted (PS3.3 Table C.7-1); removing them avoids deciding pseudonyms for patients outside the export. MAC Parameters Sequence describes the digital signatures that Table E.1-1 removes with Digital Signatures Sequence (FFFA,FFFA), and ICC Profile holds a file in another format, whose contents the engine does not parse. Any other attribute is kept because Table E.1-1 “defines the minimum actions required for conformance” (Note 5 after Table E.1-1a), and PS3.15 notes that “it is usually safe to assume that code strings do not contain identifying information” (Note 9), and that the same is usually true of coded sequence entries, except codes for providers and staff (Note 10). Keeping a sequence requires “recursively applying the Profile rules to each Data Set in each Item of the Sequence” (Table E.1-1a), so its items get the same rules. A binary value of an attribute that the IOD does not define at that place has no meaning there that the engine can check, so it is removed.Tests. Every text attribute that Table E.1-1 omits and a supported IOD uses has a rule with a note, and a regenerated table that adds one fails to load until it has a rule; each group gets its actions under the Basic Profile, Clean Descriptors, and Retain Device Identity, and the names and identifiers are removed under every option; a malformed option action, or one on a rule whose group allows none, is rejected; coded values keep their meaning; both alternative calendar dates are removed under every preset; De-identification Method keeps its existing values and gains the engine’s; a text attribute without a rule is removed, or emptied or given a dummy value where its Type requires; URN Code Value and Coding Scheme URL are kept under every option, with a note; each other UR attribute that Table E.1-1 omits, each of the seven sequences, ICC Profile, and MAC Parameters Sequence has X/Z/D with a note, and each is removed where the first supported release’s IODs make it Type 3, under every option, as Patient’s Size Code Sequence is under Retain Patient Characteristics, while a UR value that the IOD requires sequesters its instance; an attribute that no rule covers is kept if its VR is CS, AT, or a numeric VR; a sequence or a binary value is kept where the IOD defines the attribute at that place and removed elsewhere, with the attributes in a kept sequence’s items handled by their own rules, and an Encrypted Attributes Sequence is removed with its items (D-013); such an attribute of any other VR is removed; and an attribute absent from the pinned dictionary is removed.
D-023: Dates and times that Table E.1-1 omits#
Decision. Every date, time, and datetime attribute of the pinned dictionary that Table E.1-1 omits gets X/Z/D under the Basic Profile, as a reviewed L2 action, besides the role that D-007 gives it. In 2026d there are 15: three general attributes, Item Inventory DateTime (0008,0404), Expiration DateTime (0008,0416), and Study Update DateTime (0008,041F); eight that PS3.6 marks DICONDE; and four that it marks DICOS. As the table does for every date and time it lists except Patient’s Birth Date, Patient’s Birth Time, and GPS Time Stamp, each rule also gives K under Retain Longitudinal Temporal Information with Full Dates and C under Modified Dates, which modifies the attribute by its role (D-007), and no action under any other option, so Retain Device Identity keeps neither Date of Gain Calibration (0014,3076) nor Time of Gain Calibration (0014,3077) and conflicts with Modified Dates on neither.
Rationale. This is consistent with the Basic Profile removing every date it lists, and none of the 15 appears in the first supported release’s IODs, so the choice costs nothing there. Keeping them under the Basic Profile was rejected because dates are quasi-identifiers; they are kept only under Full Dates, as the table keeps the dates it lists, and Full Dates is outside the Scope. Acting on them by temporal role under the Basic Profile was rejected as more nuance than the risk justifies.
Tests. Each of these attributes has X/Z/D under the Basic Profile, with a note, K under Full Dates, and C under Modified Dates, and every policy gives it C exactly when it selects Modified Dates; and a regenerated dictionary that adds a date, time, or datetime that Table E.1-1 omits fails the tests until the attribute has a rule.
D-024: De-identification method digest#
Decision.
The de-identification method digest is a SHA-256 identifier of the policy and the PyMedPhys implementation that determine the de-identification method. It covers the policy (D-011) and everything the engine could apply, whether or not the selected options use it: PyMedPhys’s version; the content digest of every generated table; every L2 rule; every L3 rule, or their absence; the digest of the entries of the TG-263 vocabulary that descriptor cleaning matches ROI names against (D-009), or the absence of a vocabulary; the keyed digest of the reviewed-names list whose decisions descriptor cleaning applies (D-009), or the absence of a list; every parameter of generated values; and the source and rule files of the de-identification package. Each instance records it in De-identification Method (D-012) as 64 lowercase hexadecimal digits.
A generated table’s content digest is computed from its rows and must match the digest the table records, as its loader requires (D-001).
The source and rule files are every
.py,.toml, and.jsonfile of thepymedphys._dicom.deidentifypackage, at any depth: its modules, its L2 rule files and the requirements register beside them, and its generated tables. Each is read as bytes, with CRLF line endings normalised to LF, so a Windows checkout gives each file the same digest.__pycache__and names that start with.within the package, such as the caches that tools write, are left out. The digest is refused when none of these files can be found, or when they do not include the module that computes the digest, so that an installation whose files cannot be read never gives a digest that covers none of its code. These files and the generated tables are read once per process, when the first digest is computed, and stand for the engine as read then, not as imported. An edit after the first digest is not seen until the process restarts, so every instance of a run carries the same digest; an edit between importing the engine and the first digest can give the old code the new files’ digest. Editing or reloading the engine’s modules in a running process, for example withimportlib.reloador Jupyter’s autoreload, is not supported, and the digest is then not guaranteed to match the code that runs. The engine is to compute the digest before it processes anything, so that the digest stands for the code that runs.The digest is computed over a canonical serialisation labelled with its format,
pymedphys-deid-method-digest/2: one JSON object encoded as UTF-8, with every object’s members sorted by key, no whitespace outside text, every character other than",\, and the control characters written as itself, and integers in decimal. Its members areformat,engine_version,policy,tables,l2_rules,l3_rules,vocabulary,reviewed_roi_names,generated_values, andfiles. Dataclasses and mappings are objects, sequences are arrays in their own order, and enumerations are their values. The parameters of generated values have different types, so each is a pair of its type and value, such as["int", 52], with floating-point numbers and bytes in hexadecimal, and a set’s members are sorted. A value of any other type is rejected. A change to the serialisation takes a new format label.There are no L3 rules yet, so the serialisation records their absence as
null, as it does an absent vocabulary, and adding them will change every digest.The runtime environment is not an input to the method digest. The Python implementation and version, and the versions of the third-party packages whose behaviour matters, such as pydicom and tomlkit, are provenance of the execution rather than identity of the method, and are recorded separately (D-012, D-029). The same method, implemented by the same PyMedPhys code and resources, therefore has the same method digest in every supported runtime environment. Of PyMedPhys’s code outside the package, including the modules that the package imports,
pymedphys._version,pymedphys._config,pymedphys._dicom.uid,pymedphys._imports, andpymedphys._nomenclature.tg263, the digest covers only PyMedPhys’s version, which a development build keeps across commits.Runtime software can affect execution. Equality of method digests therefore means equality of the components that the method digest covers; it is not proof of byte-for-byte identical execution or output. Reproducing or auditing an execution also requires its recorded runtime environment and any other relevant external state.
Releases and development builds compute the digest in the same way. An unmodified installation of an official release therefore gives the same method digest for a given policy and set of external resources in every supported runtime environment; an edited or repacked installation that claims the release’s version gives another; and two development checkouts that share a
.devversion give different digests when their de-identification source differs. Each release’s conformance statement (Architecture item 10) is to publish the method digest of each enabled preset without a vocabulary or reviewed-names list, naming the PyMedPhys release, the DICOM edition, the preset, and the absence of a vocabulary and of a reviewed-names list, so that anyone can compare a file’s digest with it. A run with a site’s own vocabulary, reviewed-names list, or other resource has a digest other than that published baseline, as intended. No digest is published yet, since no preset is enabled.De-identifying a file again with the same policy, PyMedPhys files, and resources gives the same method digest under any supported version of Python, pydicom, or tomlkit, so a change of runtime alone never appears as a new method, and the later run’s Contributing Equipment Sequence item records its own runtime (D-012). A change to the policy, the rules, the source, the parameters of generated values, or an external resource changes the digest, so the later processing is identifiable as a different method.
A confidential site resource that can change what the engine does contributes to the method digest through a stable keyed digest, never through the resource or an unkeyed digest of it, which anyone could test a guessed resource against. The first is the reviewed-names list of ROI name cleaning (D-009, D-016), which holds source ROI names. Its keyed digest is the HMAC-SHA-256 of the list’s canonical form, its file written without whitespace outside text, with every object’s members sorted, under the run’s key in a derivation domain of its own,
reviewed-roi-names(D-004), so that no UID, patient identifier, or date offset shares its message. The key is never written to DICOM output or reports. The keyed digest is an input to the method digest,reviewed_roi_namesin the serialisation, ornullwithout a list, and is recorded wherever the digest’s components are. The same list under the same key gives the same value in every run, and a change to a decision or a name changes the method digest. Under an ephemeral key, each run’s keyed digest, and so its method digest, differs even for the same list, as intended: without the key, nobody can tell whether two runs used the same list. Resources that are not confidential, such as the TG-263 vocabulary, keep their unkeyed content digests.Each release report records the method digest with the components it is computed from, as structured fields:
method_digest;method_digest_format;engine_version;table_digests, the content digest of each generated table by file name;l2_rules_digest, the SHA-256 of the canonical bytes of thel2_rulesmember of the serialisation;l3_rules, their absence or, once they exist, the SHA-256 of their member;vocabulary_digest, or the absence of a vocabulary;reviewed_roi_names, the keyed digest of the reviewed-names list, or the absence of a list;generated_values_digest, the SHA-256 of thegenerated_valuesmember; andengine_files, the SHA-256 of each source and rule file by its path. These show which component differs between two digests. Like the rest of the report, they contain no key, source identifier, source value, or patient data (D-016).
Rationale. Two runs carry the same digest only if they have the same policy, PyMedPhys version, generated tables, rules, vocabulary, parameters of generated values, and package files, with the files and tables as each run’s process read them at its first digest. The vocabulary and the L3 rules are not generated tables, but they decide what the engine keeps and writes as much as the tables do, and the engine’s version stands for its code in a release. Every call states the vocabulary, or None for its absence, by name: with a default, a caller that forgot the vocabulary would record no vocabulary while descriptor cleaning used one. The digest also changes when an unrelated rule changes, which is harmless. Covering only what the policy uses was rejected: deciding which rules are used would be another dependency calculation that could be wrong, and if it were, behaviour could change silently under the same digest, which is the dangerous failure; a change of digest that proves unnecessary is the safer error. A serialisation that fixes every byte, and states each value’s type where JSON alone would not, encodes the same inputs as the same bytes whatever platform and Python version computes it, which hashing Python’s own representations of the values would not. Development builds and editable installs share a version, such as
0.42.0.dev0, across commits, and an edited or repacked installation can claim a release’s version, so the engine’s files identify their code where the version cannot; limiting the guarantee to released versions was rejected because tests and pilots run such builds, and selecting the files by type, rather than by a list of those that decide behaviour, cannot leave out a new module of the package. Reading the files and tables once per process gives every instance of a run the same digest, even when a file of an editable install is edited after the first digest, which would otherwise give later output of the old code the new files’ digest. The runtime environment is left out of the digest for two reasons. The first is comparability: with the versions of Python, pydicom, and tomlkit as inputs, every routine upgrade would give the same method a new identity, so no stable digest could be published for a release, policy, and set of resources, sites could not compare their digests without also matching their environments, and de-identifying a file again after an upgrade would record what looks like a new method. The second is that a few version strings do not identify an execution: it can also depend on how packages were built, on the libraries they load, on details of the platform, and on other external state, so a digest of selected versions would look like a complete fingerprint of behaviour without being one. Recording the runtime as readable provenance shows a custodian what actually ran, which a second, opaque digest in De-identification Method would not, so none is written there, and the effect of the runtime on behaviour is managed by testing the supported environments (D-029). A plain SHA-256 of a small or guessable confidential list could be reversed by hashing candidate lists offline, which a keyed digest prevents, and a derivation domain of its own keeps the resource’s identifier apart from patient pseudonyms, UIDs, and date offsets derived under the same key. The keyed digest, like the vocabulary, must be stated by name with no default, so a caller that forgot the list would be refused rather than record no list while descriptor cleaning used one.Tests. Changing PyMedPhys’s version, any part of the policy, its preset, any generated table, L2 rule, L3 rule, the vocabulary’s entries, any parameter of generated values, or any source or rule file of the engine changes the digest, including one that the selected options do not use, as does adding, removing, or renaming a file, adding or removing a vocabulary, or adding, removing, or changing the reviewed-names list’s keyed digest, which shows only in its own component, and a keyed digest that is not 64 lowercase hexadecimal digits, or a call that does not state the list by name, is refused; changing the Python implementation, the Python version, or the version of pydicom or tomlkit, one at a time or all together, leaves the digest unchanged; the serialisation holds exactly the members listed, and while there are no L3 rules, records their absence; the same version, policy, tables, rules, vocabulary, parameters, and files always give the same digest, in whatever order mappings and sets hold them; a small synthetic input gives canonical bytes written out by hand and their independently computed SHA-256, and changing only a value’s type changes them; files with CRLF and LF line endings give the same digest, and caches and files of other types are left out; every file of the package other than caches has a type that the digest covers, so a file of a new type fails the tests until the digest covers it; every file is covered when the package is installed below a directory whose name starts with
.; the digest is refused when the package’s files cannot be found or do not include the module that computes it; an edit to the files or tables after the first digest is not seen until they are read again, as in a new process; a table whose rows do not match its recorded digest is rejected; a reformatted table keeps its content digest, which is computed from its rows, but changes its file’s digest; a call that does not state the vocabulary by name is refused; and the digest is 64 lowercase hexadecimal digits, so it reveals no value.
D-025: File Meta Information#
Decision.
The File Meta Information that the engine builds (D-002) holds exactly these elements: File Meta Information Group Length (0002,0000), File Meta Information Version (0002,0001), Media Storage SOP Class UID (0002,0002), Media Storage SOP Instance UID (0002,0003), Transfer Syntax UID (0002,0010), Implementation Class UID (0002,0012), and Implementation Version Name (0002,0013).
It names the de-identifier by the fixed Implementation Class UID
1.2.826.0.1.3680043.10.188.1.1, the first UID of the arc1.2.826.0.1.3680043.10.188.1that PyMedPhys reserves for UIDs fixed in its design, under its registered root,1.2.826.0.1.3680043.10.188. The maintainer decided this on 1 October 2026, in place of a2.25.UID derived from a documented name.Implementation Version Name is
PYMEDPHYS, followed by the version where the result fits the 16 characters of SH. A single space separates them, as inPYMEDPHYS 0.42.0, and a development or local version that does not fit, such as0.42.0.dev1, givesPYMEDPHYSalone.Every other element is omitted, whatever the source file held: Source Application Entity Title (0002,0016), Sending Application Entity Title (0002,0017), and Receiving Application Entity Title (0002,0018); Source Presentation Address (0002,0026), Sending Presentation Address (0002,0027), and Receiving Presentation Address (0002,0028); and private File Meta elements, such as Private Information Creator UID (0002,0100) and Private Information (0002,0102).
Rationale. The elements written are the six that PS3.10 Table 7.1-1 makes Type 1, and Implementation Version Name. The table’s other elements are Type 3, except Private Information, which is required only with a Private Information Creator UID, so omitting them keeps the file valid. The titles and addresses of the systems that created, sent, and received the source file can identify the institution, so none is carried into the output. PS3.7 D.3.3.2 requires Implementation Class UIDs to be registered by the implementing organisation per the policies of PS3.5; PS3.5 9.2.2 makes an organisation responsible for registering its UIDs, at least by obtaining a registered root; and PS3.5 B.2, which is informative, advises that a UUID-derived UID is usually not appropriate for an Implementation Class UID. A UID under the registered PyMedPhys root meets all three and leads back to PyMedPhys. Every UID that PyMedPhys generates under the root adds at most one component after it, so none falls under the reserved arc, whose reservation is recorded beside the root in
pymedphys._dicom.uid. A version that does not fit beside the name is left out rather than shortened, because a shortened version would name another release. PS3.7 D.3.3.2 requires, where there is no Implementation Version Name, that one Implementation Class UID means one version of the implementation, and notes that a version name is expected to include version information;PYMEDPHYSalone is therefore still written, since the UID stays the same across versions, and the full version is always in Software Versions (0018,1020) of the Contributing Equipment item (D-012).Tests. Every output file’s File Meta Information holds exactly the seven elements listed, with the fixed Implementation Class UID
1.2.826.0.1.3680043.10.188.1.1and an Implementation Version Name ofPYMEDPHYSwith the version where it fits 16 characters; and no output file carries an Application Entity Title, a Presentation Address, or a private element in its File Meta Information, whatever the source file’s File Meta Information held.
D-026: Reference graph findings#
Decision. The first pass’s findings (Architecture item 8) have graded consequences:
A dangling reference is reported only: the release report counts the instances with each kind of finding reported only, and the confidential QC pack lists each such finding at each instance it names, with the attribute’s tags and its count.
A frame of reference mismatch is reported only, and its instances are still written. The maintainer decided this on 6 October 2026. The mismatches are a series whose instances name several frames of reference, an RT Structure Set ROI whose frame of reference the structure set does not list once and only once, and an image that a structure set lists under another frame of reference than its own (Architecture item 8).
Identical duplicate instances are written once. The maintainer decided on 1 October 2026 that instances that share a SOP Instance UID are identical only when their source bytes are the same: the same Transfer Syntax UID, and the same bytes of the data set after the File Meta Information, ignoring the preamble, the File Meta Information, and the top-level group lengths (gggg,0000) and Data Set Trailing Padding (FFFC,FFFC) (Architecture item 8).
Instances that share a SOP Instance UID with different source bytes, or whose bytes the first pass cannot show to be sound, are all sequestered.
A series whose instances name several studies is sequestered.
A study whose instances name several patients stops the run before anything is written. The maintainer decided on 1 October 2026 that a patient is a Patient ID with its Issuer of Patient ID (0010,0021), the identity from which D-005 derives pseudonyms, so a study in which a planning system has dropped the issuer from some instances stops the run. An instance without a Patient ID counts as a patient apart from every identity, which is the conservative reading of that decision: the instances without one together form one patient, so a study that mixes instances with and without a Patient ID stops the run, and a study whose instances all lack one does not.
An instance without its SOP Instance UID, Series Instance UID, or Study Instance UID is sequestered: it cannot be named (D-016), and the first pass cannot check it against its series and study.
Release reports name instances by their output names only. Run positions and source paths appear only in the confidential QC pack (D-016).
A sequestered instance has no output name. The maintainer decided on 2 October 2026 that release reports name each one by an opaque label for the run, such as
S-0007, with its reasons, and that only the confidential QC pack maps labels to sources. Labels are assigned in an order drawn at random, so a label says nothing of the instance or its place in the run.
Rationale. Real exports routinely contain dangling references, such as a plan that references a dose that was not exported, so stopping on every finding would make the engine unusable, and reporting every finding without acting on it could release inconsistent collections. A study with several patients is a curation error that no action on individual instances can repair safely. An empty Patient ID needs a curated identity (D-005), and an unresolved identity stops its instances for curator resolution (D-008); a curated identity would give such an instance a different pseudonym from the rest of its study, the outcome that stopping a study with several patients exists to prevent. Comparing source bytes needs no decoder and no resolution of VRs, so no behaviour of pydicom, such as guessing the VR of an element that an Implicit VR file holds without one, can make different values compare equal, and comparing copies cannot change the data set that a caller holds. The same bytes can be different values in different encodings,
ff ffbeing -1 as SS and 65535 as US, and01 00being 1 in a little endian OW value and 256 in a big endian one, so the transfer syntax, which decides the encoding of the bytes, is part of the comparison. Writing one of two copies that differ would silently drop the other’s content, while sequestering both loses nothing that a curator cannot recover. So the cost of comparing bytes, that copies that differ only in their encoding are sequestered for a curator, loses nothing, and a copy whose bytes cannot be shown to be sound counts as a conflict, which fails safe. A frame of reference mismatch is a matter of the data’s integrity, not of disclosure: replacing every UID consistently neither creates nor hides one, and contours drawn on an image registered to the planning CT legitimately sit in another frame of reference, so withholding for it would block sound collections.Tests. A dangling reference is reported and its instance still written; copies with another preamble or File Meta Information, with top-level group lengths, or with top-level Data Set Trailing Padding are identical; the digest is the SHA-256 of the Transfer Syntax UID and the data set’s bytes, computed by hand from the file; copies of one data set in Implicit VR and in Explicit VR Little Endian conflict, with or without a private element, as do copies with sequences of defined and of undefined length, with more padding of a value, with a group length or Data Set Trailing Padding inside an item, and with the same data set bytes under another transfer syntax; any changed value or VR, added or removed element, or changed private element makes copies conflict, as do a private element written as SS -1 in one copy and as US 65535 in the other, the same OW bytes in little and big endian, a private or unlisted element read as UN in one copy and with a VR in the other, compressed and uncompressed Pixel Data, and 8-bit Pixel Data in Implicit VR and in Explicit VR; a file in a deflated transfer syntax, without a Transfer Syntax UID, followed by bytes that cannot be read, or with a top-level group length or Data Set Trailing Padding that holds items has no digest, and conflicts with every other copy, even another copy of the same file, while one in Explicit VR Big Endian or truncated within a sequence that the first pass follows, or with such a sequence of Explicit VR, of Implicit VR, or of VR UN whose items run past it, at the top level or nested, has no record; the same Patient ID from another issuer, or without one, is another patient, a study that mixes instances with and without a Patient ID has several patients, and a study whose instances all lack one has one; identical duplicates give one output file; every instance that shares a SOP Instance UID with different source bytes is sequestered; a series that names several studies is sequestered; a study that names several patients stops the run with nothing written; release reports name instances by their output names, never by run positions or source paths; they name sequestered instances by labels that carry nothing of the instance or the run’s order, with reasons that hold only codes and tags; and an instance without its SOP Instance, Series Instance, or Study Instance UID is sequestered; and a series in several frames of reference, an ROI whose frame of reference is not listed once, and a contour image in another frame of reference are each reported, and their instances still written.
D-027: Residual search#
Decision. The search of each output file after writing (Architecture item 8) is strict:
Any residual person name, UID, or date, and any finding outside the data set, in the preamble, the File Meta Information, or trailing bytes, fails the file, and its instance is sequestered.
A residual direct identifier fails the file in the same way, although the search reports it as text. A direct identifier is an attribute that Table E.1-1 lists in a PS3.6 group of the patient, a visit, the study, or a procedure or order (0008, 0010, 0020, 0032, 0038, or 0040), that is neither a sequence nor a UID, that no option retains or cleans, and whose keyword has
ID,IDs,Number,Numbers, orLocatoras a word, such asPatientIDorPlacerOrderNumberImagingServiceRequest; it counts wherever it occurs, as in the items of Other Patient IDs Sequence (0010,1002). In 2026d this selects 22 attributes, 15 of them current: Patient ID (0010,0020), Issuer of Patient ID (0010,0021), Patient’s Telephone Numbers (0010,2154), Accession Number (0008,0050), Referring Physician’s Telephone Numbers (0008,0094), Study ID (0020,0010), Admission ID (0038,0010), Service Episode ID (0038,0060), Scheduled Procedure Step ID (0040,0009), Performed Procedure Step ID (0040,0253), Requested Procedure ID (0040,1001), Person’s Telephone Numbers (0040,1103), Order Callback Phone Number (0040,2010), Placer Order Number / Imaging Service Request (0040,2016), and Filler Order Number / Imaging Service Request (0040,2017), and the retired Other Patient IDs (0010,1000), Medical Record Locator (0010,1090), Study ID Issuer (0032,0012), Issuer of Admission ID (0038,0011), Issuer of Service Episode ID (0038,0061), Specimen Accession Number (0040,050A), and Telephone Number (Trial) (0040,A354).Text findings inside the data set go to confidential QC review (D-016).
As the maintainer decided on 2 October 2026, transforming a value and collecting it for the search are separate, and release is conditional on both. The release gate enforces exactly this:
Coverage. The walker reports the paths of the values it planned to collect, those it collected, those it could not collect, with a reason that holds no value, and those it read only as bytes. Collection is complete only when every planned value was collected from its decoded form under a VR that the pinned dictionary gives its attribute. It is incomplete where a planned value was neither collected nor reported as not collected, so an empty collection is never taken as complete; where a value could not be collected; where text outside ISO 646 with no Specific Character Set was read only as ISO 8859-1 (Architecture item 5), since searching its bytes supplements the coverage but does not stand in for the decoded text, whose copies in other attributes or encodings were not searched for; and where a value was collected under another VR, such as a person name collected as CS, which the search would leave out or search in the wrong forms. Coverage is pooled across the subject, as the values searched for are, so a sibling instance’s gap counts against every file of the subject, even where another instance collected a value at the same path. An instance out of scope whose IOD the pinned tables define, such as an MR or PET image, a spatial registration, or an RT image, is read, planned, and edited only so that its values are collected for its subject’s search, and is never written, so that it withholds its subject’s files only where its collection is incomplete, as any instance’s would. This is “Collect first”, which builds on “Withhold”, as the maintainer decided on 6 October 2026. An instance of the subject whose values were not collected at all, because its source was refused, its SOP Class names no IOD of the pinned tables, its transform failed, or it changed during the run, withholds every file of the subject, since its values could be in them unsearched for; and where the walker sequesters an instance, the planned values it did not reach are not collected, rather than not reported. Values in an instance whose file could not be read as DICOM, which names no subject, are not pooled. File Meta Information and preamble values are not collected; the writer rewrites both, so neither is released, but a copy of one in kept text is not searched for.
Search. The gate runs the search itself, for exactly the pooled collected values, so no other search can stand in for it. The forms that the search leaves out by the rules below, such as those shorter than four characters and binary values, are its exclusions: they are listed with their reasons and never count as incomplete.
Withheld. A file is withheld where collection is incomplete for an attribute to which the pinned dictionary gives VR PN, UI, DA, DT, or SQ, whose items’ content is unknown, or for a direct identifier; where its structure cannot be read to its end; and where the search finds a person name, UID, date, datetime, or direct identifier anywhere, or any value in the preamble, the File Meta Information, Data Set Trailing Padding, which holds no value, or bytes after the data set. Kinds are taken from the VR that the pinned dictionary gives the source attribute, whatever VR the value was collected under, and from the search only where the dictionary gives none.
QC review. A file goes to QC review where collection is incomplete for any other value, including a private value or one to which the dictionary gives no VR, or the search finds other text inside the data set. Binary values are outside the search’s contract, which leaves them out, and a private value that could not be collected is outside its reach, although decoded private text is searched like any other text; withholding every file with a vendor’s private value that could not be collected would sequester nearly every RT file, so the residual risk is listed in the QC pack instead.
Released otherwise. A search that finds nothing is not on its own a release. The gate takes the most severe consequence of every reason, each named by an attribute path and a reason code, never a value, and has no setting that relaxes it. Release reports are to count the reasons by attribute and code, and the QC pack to list them in full (D-016, D-026).
Clarification of the decisions of 1 October 2026 on content that cannot be decoded, 2 October 2026. Where those decisions say that “the instance is de-identified” (D-010) and that values that cannot be decoded “are listed as not searched”, the transformation proceeds and the values are listed as not collected; release remains conditional on the required coverage. For example:
Source
Before the clarification
After it
Patient’s Name removed, with bytes outside ISO 646 and no Specific Character Set
Read as ISO 8859-1, searched, and released after a clean search
Withheld: incomplete collection of a person name
Study Description removed, with the same bytes
Released after a clean search
QC review: incomplete collection of other text
Study Description kept or cleaned, with the same bytes
Instance sequestered
Instance sequestered, as before
The maintainer decided on 2 October 2026 that release reports count the drops and the values not searched by attribute and reason, and that the confidential QC pack lists each one by instance and place (D-016).
As the maintainer decided on 6 October 2026, a residual person name found inside a retained label, such as a beam name, ROI name, or plan label, still withholds the file in the first supported release, even where it is also an ordinary word, as a surname can be in a beam name (
FIELD^MARGARETagainst a Beam Name ofField 1). Before that changes, for example to send such findings to QC review, their effect is first measured with a list of surnames.Values that the policy legitimately retains are dropped from the search, and each drop is recorded.
Values shorter than 4 characters are not searched, and are listed as not searched. Binary values, values of VR AS, AT, CS, DS, FD, FL, IS, SL, SS, SV, TM, UL, US, and UV, and datetimes without a full date are also listed as not searched, and are checked attribute by attribute instead (Architecture item 8).
Content that cannot be decoded inside a sequence that is removed is removed with it, and its values are listed as not searched in the confidential QC material (D-016). An instance is sequestered only where content that cannot be decoded would be kept or cleaned (Architecture item 2). The maintainer decided this on 1 October 2026.
A source value that exactly equals a constant that the engine always writes, such as a dummy date or
DEIDENTIFIED, is not searched, and each such skip is recorded by its source attribute, never by its value. The maintainer decided this on 1 October 2026.The constants for that skip are D’s first and second constants, the values of D’s item in Person Identification Code Sequence, and the markers’ fixed values, and not the keyed pseudonyms or the markers’ versions and method digest; a source value equals one when D would treat it as equal to its constant (Architecture item 8). The maintainer decided this on 5 October 2026.
A form of a source value that equals such a constant is skipped in the same way, and the value’s other forms are still searched. So the family name of a person name
DEIDENTIFIED^ABCDEFfrom data that was de-identified before, which is the family name of every keyed pseudonym, is not searched, and nor are the words of a family nameDE-IDENTIFIED, butABCDEFis; and the date of a datetime19000101120000, written as YYYYMMDD, is not searched, but the whole datetime and the date’s other spellings are. The maintainer decided this on 5 October 2026.Output is written to a staging area and moved to the release directory only after a clean search. The staging area is a directory beside the release directory, named for it, so on its file system, and created for the run with permissions for its owner alone where the platform has them, since it holds output that may still identify people, as QC material does (D-016). Each file is written there under a temporary name and renamed once its bytes are on disk, read back for the search, and deleted at once if it is not released. A file held for review is deleted too, and is written again by a run after its review, whose release replaces the earlier one, since the current command gives each run an ephemeral key (D-004); the maintainer decided this on 6 October 2026, for a file held for ROI name review (D-009) and for one held for a text finding of the search alike. The release directory must not exist before the run, and is published by one rename of the staged tree, so it appears whole, with only files that passed, or not at all. A run that fails deletes its staging area; one that a crash leaves holds output that may still identify people, so a run refuses to start beside it until someone has reviewed and deleted it.
Open questions.
Whether dropping a retained value removes it from the search entirely, so that the same value removed from another attribute is not searched for either, or only skips the retained copy, is not yet decided.
Values that hold numbers are searched only for forms of at least 8 bytes that are not UTF-16LE (Architecture item 8), and the search does not list this for each value; whether release reports state this rule is not yet decided.
Whether clinical trial identifiers (group 0012), such as Clinical Trial Subject ID (0012,0040), are direct identifiers is not yet decided; most of them identify the trial, its site, or a time point rather than the patient.
Rationale. Nothing leaks silently, at the cost of some false positives that need human review. Reporting findings only was rejected because the search would become advisory. Failing a file on any match was rejected because legitimate overlaps would make the engine unusable: a removed Study Description that reads
CHESTis found inside a retained Body Part Examined that readsCHESTABDOMEN, and review, not failure, settles it. A direct identifier identifies the patient, a visit, or the study on its own, as a name does, so it fails the file although the search reports it as text. Selecting direct identifiers from the pinned tables by a rule, rather than keeping a list by hand, covers those that a new edition adds, and leaving out those that an option retains or cleans, such as device identifiers, keeps to attributes that every option removes or replaces. A constant that the engine always writes is in the output whatever the source held, so finding it reveals nothing about the source and would fail every file whose source happened to hold it; recording the skip by attribute keeps it visible without repeating the value. Content that cannot be decoded cannot be matched, cleaned, or searched. Removing it with a sequence that is removed anyway loses nothing the output keeps, listing its values keeps the gap visible to reviewers, and sequestering an instance that would keep or clean such content means that nothing unchecked is written.Tests. A residual person name, UID, date, or datetime, and any residual in the preamble, File Meta Information, Data Set Trailing Padding, or trailing bytes, sequesters the instance and leaves nothing in the release directory; incomplete collection of a person name, UID, date, datetime, direct identifier, or unread sequence withholds the file after a search that finds nothing, including where a planned value was neither collected nor reported, where the gap is a sibling instance’s, and where a person name was collected as CS, OB, or LO, while incomplete collection of other text, of a private value, or of an attribute without a VR in the dictionary goes to QC review; the clarification’s examples give the outcomes in its table; the search covers the pooled values; the search’s exclusions are listed and leave collection complete; a file whose structure cannot be read to its end is withheld; the most severe reason decides, in either order; reasons and reprs hold no value; a residual of each direct identifier that the rule selects from the pinned tables, such as Patient ID in an item of Other Patient IDs Sequence or Accession Number in a private element, does the same, and the rule selects exactly the attributes listed above; a text finding inside the data set reaches QC review; retained values are dropped from the search, and each drop is recorded, counted in the release report by attribute and reason; before the pipeline drops retained values, a regression case shows that a legitimately retained occurrence of a value cannot conceal the same source string in the preamble or trailing bytes; values shorter than 4 characters are listed as not searched; a source value that exactly equals a constant the engine always writes is skipped and recorded by its source attribute, never by its value, while one that differs from it is still searched; a form of a value that equals such a constant, such as the family name of an earlier pseudonym, is skipped and recorded in the same way, while the value’s other forms are still found; content that cannot be decoded inside a removed sequence is removed and its values are listed as not collected in the confidential QC material, and an instance that would keep or clean such content is sequestered; nothing reaches the release directory before its search is clean; the release directory appears whole or not at all; and a staging area that an interrupted run leaves stops the next run beside it.
D-028: Private attributes without Retain Safe Private#
Decision.
Without the Retain Safe Private Option, every element of an odd group is removed, since Table E.1-1 gives Private Attributes, “(gggg,eeee) where gggg is odd”, X: each private data element, whether or not its block has a private creator; each private creator, including one that reserves a block with no elements; and each element that PS3.5 Section 7.8.1 does not allow, in groups 0001, 0003, 0005, 0007, and FFFF or at (gggg,0001) to (gggg,000F) and (gggg,0100) to (gggg,0FFF), and each retired group length, (gggg,0000).
They are removed at the top level and in the items of every standard sequence, as E.1.1 requires for every attribute of the table. A private sequence is removed with its items and everything in them, as X requires (Table E.1-1a), so its contents are not examined.
A value of VR UN whose attribute the pinned data dictionary gives VR SQ, as pydicom reads a sequence that it does not know from Implicit VR Little Endian, is decoded as Implicit VR Little Endian, as PS3.5 Section 6.2.2 allows, with the character set of the data set that holds it, and its items are searched. So is every raw value of VR UN, or read without a VR from Implicit VR Little Endian, of a sequence that pydicom’s own dictionary knows, which pydicom would otherwise convert to SQ without checking its bytes. A value that pydicom has not yet read, because the file was read with
defer_size, is read from the file or buffer first, and refused if it cannot be read again or is not of its stored length. A decoded value is accepted only if items fill it (Architecture item 2) and they encode to the same bytes. Each sequence nested in those items, at any depth, is decoded and checked in the same way, with the character set of the item that holds it. Where something is removed from a decoded value, or from a sequence nested in it, the output holds its decoded items, with VR SQ; otherwise its value is unchanged.A value of VR UN, or one read without a VR from Implicit VR Little Endian, is decoded only where the pinned dictionary or pydicom’s gives its attribute VR SQ, and any other is left as it was read. A value of VR UN whose attribute the dictionary gives another VR holds no items, and one whose attribute the dictionary does not list is not searched, since that attribute is itself removed (Architecture item 2).
A standard sequence that Explicit VR Little Endian stores with a VR other than SQ or UN, such as OB, is refused, since its items cannot be searched.
A Specific Character Set (0008,0005) that pydicom does not map to codecs as it is given, a value that is not among its terms or several values that include ISO_IR 192, GB18030, or GBK, which allow no code extensions, or one stored with a VR other than CS, which pydicom would convert with an error that can quote it, is refused, as content that cannot be decoded (Architecture item 2), rather than read with pydicom’s default encoding or one that it guesses: in an item, by the path of the sequence that holds the item, and at the top level by its own path.
A standard sequence that cannot be decoded, or a decoded UN value, or a sequence nested at any depth in its items, that does not encode to the same bytes, is refused by its path, and no copy is returned. Content that cannot be decoded inside a sequence that is removed is removed with it, so the engine sequesters the instance only where that content would be kept (Architecture item 2).
Elements are found by their tags, and named by their paths. No private value is decoded. pydicom’s warnings and log records are redacted while it decodes, and its errors are replaced, not chained, since each can quote a value.
A policy that selects Retain Safe Private, or gives Private Attributes any action other than X, is refused until reviewed rules say which private attributes are safe.
Private Data Element Characteristics Sequence (0008,0300) describes an instance’s private blocks, and D-022 keeps Private Creator Reference (0008,0302) in its items, so the private creator of each block that this decision removes would survive there after every block that it describes has gone. The maintainer decided on 6 October 2026 that the sequence is removed whenever no private attribute is kept, which, while Retain Safe Private is refused, is under every policy: a supplementary rule gives it X/Z/D, which removes it at its Type 3 place in the SOP Common Module.
Open questions.
The search refuses the whole data set at the first value that it cannot decode, so the engine must either remove the sequences that its rules remove before it removes private attributes, or remove the removed sequence that holds a refused path and try again. Which it does is not yet decided.
The search decodes sequences for itself, as it was written to run on any data set before the engine had an element reader. Whether it should use the engine’s element reader, which reads UN sequences and character sets by its own rules, is not yet decided.
Rationale.
PS3.5 Section 7.8.1 lets a de-identifier keep or remove a private creator whose block is removed. Removing every one follows the table, which names Private Attributes by their group alone, and E.3.10, which keeps only “the Private Creator IDs that are required to fully define the retained Private Attributes”. A private creator can name the institution, as D-022 notes for in-house software. Keeping private creators was rejected because, without their blocks, they carry nothing useful, and no rule reviews their values.
The elements that PS3.5 does not allow have no defined content, so nothing shows that keeping them is safe, and the table’s X covers them by their group.
pydicom 3.0.2 does not know some sequences of the first supported release’s IODs, such as RT Assertions Sequence (0044,0110) and Dose Calculation Model Sequence (3004,0080), and E.1.1 requires a private attribute in their items to be removed too. Sequestering every instance with such a sequence that holds a private attribute was rejected, because Architecture item 2 already decodes UN values with their dictionary VR. pydicom decodes some malformed values without an error, as items that leave out part of the value, and those bytes could hold a private attribute, so a value whose items do not encode to the same bytes is not trusted: a value from which nothing is removed is written as it was read, with the bytes its items leave out. pydicom also writes the elements of a decoded item as it read them, and decodes a sequence of defined length among them only when it is read, so the check on the value that holds such a sequence does not see into it, and each is checked as it is decoded, at every depth. A sequence that pydicom decodes as it is read needs no such check, because it is written from its decoded items, and so does a raw value of VR SQ from Explicit VR, which pydicom decodes in place when it is first accessed, once its items are shown to fill it. A raw value that pydicom converts later, when it is first accessed, even of a sequence that its dictionary knows, is decoded and checked here instead, since pydicom’s conversion leaves out the same bytes without an error; this also searches a sequence that pydicom cannot look up when it raises its validation errors, rather than refusing it. The cost is that pydicom’s writer leaves out retired group lengths, so a UN sequence that has them in its items, at any depth, is refused: it is removed where a rule removes it or a sequence that holds it, and otherwise its instance is sequestered. The character set matters because the engine reads, and writes, the text in the decoded items.
Decoding every value of VR UN, or read without a VR, with its attribute’s dictionary VR was rejected. pydicom checks a value as it decodes it, and can raise an error or a warning that quotes it, as it does for a value of VR US of the wrong length and for an invalid UID, which D-016 forbids; and decoding would change values that other rules act on (Architecture item 2), such as a Patient’s Name stored as UN. Only a sequence can hold private attributes. pydicom’s dictionary is consulted as well as the pinned one because pydicom decodes the sequences that it lists, including the retired Curve Referenced Overlay Sequence (50xx,2600) in each group of its repeating group.
pydicom reads the items of a sequence, with their character sets, as it decodes the sequence, so an item’s character set is refused by the path of the sequence: raising its validation errors, pydicom raises its error before any item can be examined. Reading the text with another encoding than the one given was rejected because the engine reads, and writes, that text. Running the search within the engine’s redaction of pydicom’s diagnostics, which summarises each warning and log record only in the thread that decodes and only while it decodes, was chosen over catching warnings around each value that pydicom decodes, because Python’s warning filters are process-wide and not thread-safe to change and restore, and pydicom also logs each warning.
E.3.10 retains the private attributes “known by the de-identifier to be safe”, and Architecture item 6 records the basis for retaining each. Applying the option by removing every private attribute was rejected: the output would claim an option whose purpose, such as keeping the SUV factors of Table E.3.10-1, it does not serve. Refusing it leaves the option’s rules to a later change, which can examine private sequences as E.3.10 requires without changing removal under the Basic Profile.
Tests. Every element of an odd group is removed, at the top level and in standard sequences at every depth, including private creators with and without elements, private data elements without a creator, each kind of element that PS3.5 does not allow, and retired group lengths, and each is listed by its path in the data set’s order; a private sequence is removed whole and listed without its contents; nothing else changes, in the copy or the source; files in Implicit and Explicit VR Little Endian, with private sequences of defined and undefined length, keep no private attribute; a sequence that pydicom reads as UN from Implicit VR Little Endian, or that Explicit VR Little Endian carries as UN, is searched, decoded in its items’ character set, and holds its decoded items only where something is removed from it, and so is a sequence of defined or undefined length nested in its items at depth 1 or 2, with items of defined or undefined length; a sequence of a repeating group is searched; a sequence that pydicom cannot look up when it raises its validation errors is searched; a UN value that does not decode to the same bytes, a malformed raw value of a sequence that pydicom knows, stored as UN in Explicit VR or without a VR in Implicit VR Little Endian, a malformed sequence nested in its items at depth 1 or 2, whether or not the sequences and items around it are of undefined length or hold a private attribute, and a sequence that cannot be decoded, are refused by path without quoting a value; a Specific Character Set that pydicom does not map as given, such as an unknown one, a Python codec name, a misspelt term, or ISO_IR 192 with a code extension, or that is stored with another VR, at the top level or in an item, in memory, in a sequence read from Implicit or Explicit VR Little Endian, or in the items of a UN value or of a sequence nested in them, is refused by path, whether or not pydicom raises its validation errors, without a warning, log record, message, or traceback that quotes it, and one that pydicom maps as given is accepted; a standard sequence that Explicit VR Little Endian stores as OB or UT, at the top level or in an item, is refused by path; a UN value whose attribute is not a sequence, or is not in the dictionary, is not searched, and one read from Explicit VR Little Endian, such as a value of VR US of the wrong length, an invalid UID, or a person name, is left as it was read, without an error or a warning, whether or not pydicom raises its validation errors; a standard sequence that pydicom left unread under
defer_size, from a file or a buffer, is searched and its private attributes removed, a malformed one is refused by path, and one whose source has gone is refused by path; no private value is decoded; and a policy that selects Retain Safe Private, or gives Private Attributes another action, is refused.
D-029: Runtime environments#
Decision.
Each run records its runtime environment as provenance, apart from the method digest (D-024): in Software Versions of its Contributing Equipment Sequence item (D-012), and in a runtime section of its release report, which holds the same values as structured fields rather than one display string:
pymedphys_version,python_implementation,python_version,pydicom_version, andtomlkit_version. The section contains no key, path, or source value (D-016).An execution fingerprint, such as the SHA-256 of a canonical form of the method digest and the runtime section under a format label of its own, may later be added to the release report alone, once something needs it. None is planned, and none is written to DICOM output.
PyMedPhys keeps declaring its dependencies as ranges, such as
pydicom>=3.0.2(D-002), so that it installs as an ordinary library. It does not pin them exactly to keep the method digest stable, since the runtime environment is not part of the method.The tested environment is what continuous integration runs. The de-identification tests are unit tests, which run with the versions of pydicom, tomlkit, and every other dependency that
uv.locklocks: on Python 3.11, 3.12, 3.13, and 3.14, on Ubuntu, Windows, and macOS, in the full matrix that pushes tomain, releases, and pull requests labelledfull-testrun; on Python 3.14 on Ubuntu for merge groups and other pull requests; on Python 3.14 on Ubuntu with only thedicomextra installed; and on Python 3.11 with the declared minimum versions of NumPy and pandas. Wherever the unit tests run, which includes every pull request that changes the de-identification package, its tests,pyproject.toml, oruv.lock, and every release before it is published, the de-identification tests (thetest_deidentify_*modules intests/dicomand the table generator’s tests intests/dev/test_deid_tables.py) also have coverage on Python 3.14 on Ubuntu, with every other dependency at its locked version: with pydicom 3.0.2, the lowest release that the declared minimum allows, and with the latest release of pydicom, which continuous integration resolves from the package index each time, as the workflow guide describes. A minimum or latest version identical to the locked version reuses the normal Ubuntu/Python 3.14 run when that cell runs the complete selection without extra pytest arguments and the lock identifies one registry version. Its imported-version metadata and JUnit report must be present, both compatibility jobs record their evidence references, and the whole unit-test workflow must succeed. Other requested versions run the complete de-identification selection separately; if both roles request the same version distinct from locked, both run. A release of pydicom known to change behaviour that the engine depends on joins these runs, and another dependency is tested at other versions only where its behaviour can plausibly change the output. No other version of pydicom or tomlkit is tested, so releases of pydicom between the minimum and the latest, and releases of tomlkit other than the locked one, are supported by declaration, not by test.
Deferred.
Warn, in a run’s output and its release report, when the run uses a version of Python, pydicom, or tomlkit outside the tested environment, and refuse to run only in an environment known to be incompatible or in an explicit strict or validated mode.
Publish with each release a constraints file of the exact versions that continuous integration tested, so that a deployment of the command line that must be reproducible or validated can install exactly those versions, while library users keep the declared ranges.
Rationale. Runtime software can change what the engine sees: pydicom’s data dictionary decides whether an attribute in an Implicit VR file is read as typed values or, when the dictionary does not know it, as UN bytes, which date shifting, VR checks, and text cleaning then treat differently. That risk is managed by testing the supported environments and recording the one that ran, not by the method’s identity, which would otherwise change with every upgrade (D-024). The declared minimum and the latest release are the oldest and newest versions of pydicom that an installation can resolve, and the latest is what a new installation gets unless another package constrains it, so these runs test what a new installation uses as well as what
uv.lockpins, and resolving the latest each time tests a new pydicom release beforeuv.lockadopts it. Separate, readable fields let a custodian see what ran. Pinning the library’s dependencies exactly would make PyMedPhys hard to install beside other packages, for every user, to serve the deployments that need an exact environment, which a constraints file serves instead.Tests. The release report’s runtime section has each field, with the values that Software Versions records; and continuous integration runs the de-identification tests in the environments listed.
Roadmap#
Milestones group outcomes; they are not a sequence of pull requests or releases. Each pull request ships its own tests, documentation, and traceability, and M6 consolidates the evidence. The first supported release needs only the parts of M1 to M5 that its scope uses.
Milestone |
Outcomes |
Done when |
|---|---|---|
M0 Legacy hygiene |
Diagnostics free of identifying values and paths; limitation warnings and notices (D-019); pydicom minimum (D-002) |
Each change has regression tests and documentation, and remaining disclosure channels are listed in the tracking issue. Hygiene does not make the legacy tools conform. |
M1 Standard |
Table generator and generated tables, including the PS3.3 Types of every composite IOD with its Functional Group Macros, generated for all but the three real-time IODs, which also need PS3.6 Table 9-1 (D-001); requirements register; monthly edition check (D-001) |
Source provenance and table coverage are verified, and the generator and generated data are reviewed separately. |
M2 Primitives |
Keys and subject profiles; UID replacement; patient pseudonyms; date offsets; VR and VM validators and dummy values (D-021); rule layers, including the reviewed rules for the text, dates, times, and other attributes that Table E.1-1 omits (D-022, D-023), and preset composition; method digest (D-024) |
Primitives and policy conflicts are validated before integration. Hypothesis is added with the first property tests that use it. |
M3 Engine |
Dataset transformation, with actions resolved by Type (D-020) and markers (D-012); file container (D-025); private attributes (D-028); descriptor cleaning; bitstream metadata; dataset-level API |
Each component has tests, API documentation, and conformance evidence for explicitly supported input. A dataset-level API does not establish collection integrity or readiness to release. |
M4 Pipeline |
Discovery and writing through a staging area; reference graph with graded consequences (D-026) and two-pass verification; residual search (D-027); reports; risk detection; QC pack and attestation; statistical assessment; end-to-end preset validation |
Graph and subject consistency, the separation of reports, QC material, and state, and every preset gate are verified. |
M5 Interfaces and migration |
Command line using the same engine as the library, for the first supported release, including the nomenclature converter commands that run as |
Only supported behaviour is exposed, with user documentation. |
M6 First supported release |
|
Evidence is published for the shipped coverage and exclusions are documented; the evidence is refreshed for each later release. |
M7 Legacy removal |
Removal of both legacy interfaces |
The deprecation window in D-019 has passed. |
Later work needs its own design, tests, and conformance review: Clean Structured Content, accompanying spreadsheets pseudonymised with the same key, the Encrypted Attributes Sequence for controlled sharing (D-013), and the unsupported objects and options listed under Scope. Generating a table in M1 does not enable the corresponding option or support the corresponding IOD.