Entities API
The object graph session.store holds: session.store.patients is a
list of Patient, and each holds Study → Series → Instance.
attributes on every entity is keyed by lowercase "gggg,eeee" tag
strings, not keywords. This page renders the names of
isocenter.entities that API stability places in tier 1
or tier 2.
Frozen (tier 1)
The entity classes as reached from session.store, and the key a subject
with no Patient ID is held under, with its test. A class being here does
not make every method rendered under it tier 1: the frozen ones are
Instance.get_pixel_data(), set_pixel_data(), unload_pixel_data(),
discard_pixel_data(), get_waveform_data() and set_attr(). The other
methods shown, such as Instance.regenerate_uid() and the sequence
methods, are tier 2, and so are pixel_array and waveform_array in
Instance's attribute table.
The in-memory DICOM object graph.
Patient, Study, Series and Instance, the DicomItem and
DicomSequence nesting beneath an instance, Equipment, and the
persistence (TrackedEntity) and PHI-status (PhiStatus, ScanPolicy)
bookkeeping every graph entity carries.
NO_PATIENT_ID_PREFIX = '\\no-patient-id\\'
module-attribute
is_synthetic_patient_id(value)
Whether value is the key ingest gave a subject with no Patient ID.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
value
|
Any
|
A |
required |
Returns:
| Type | Description |
|---|---|
bool
|
True for a string that starts with |
NO_PATIENT_ID_PREFIX is the start of the key a subject whose files
carry no Patient ID is held under. The key never reaches an exported
file.
Patient
dataclass
Bases: TrackedEntity
Root of the object hierarchy. Groups Studies by Patient ID.
Attributes:
| Name | Type | Description |
|---|---|---|
patient_id |
str
|
The primary patient identifier. |
patient_name |
str
|
The patient's name. |
studies |
List[Study]
|
List of studies belonging to this patient. |
Study
dataclass
Bases: TrackedEntity
Groups Series by Study Instance UID. Represents a single patient visit or examination.
Attributes:
| Name | Type | Description |
|---|---|---|
study_instance_uid |
str
|
The unique identifier for the study. |
study_date |
Any
|
The date of the study. |
series |
List[Series]
|
List of series belonging to this study. |
date_shifted |
bool
|
Whether dates in this study have been shifted.
Whether this study date is one the shift produced is
|
study_time |
Optional[str]
|
The time of the study. |
Series
dataclass
Bases: TrackedEntity
Groups Instances by Series Instance UID. Typically represents a single scan or reconstruction.
Attributes:
| Name | Type | Description |
|---|---|---|
series_instance_uid |
str
|
The unique identifier for the series. |
modality |
str
|
The modality type (e.g., 'CT', 'MR'). |
series_number |
int
|
The series number. |
equipment |
Optional[Equipment]
|
The equipment used for this series. |
instances |
List[Instance]
|
List of instances belonging to this series. |
Instance
dataclass
Bases: DicomItem
One DICOM file's worth of data (an SOP Instance), with its pixels loaded on demand.
attributes, sequences and attribute_vrs come from DicomItem.
Attributes:
| Name | Type | Description |
|---|---|---|
sop_instance_uid |
str
|
SOP Instance UID (0008,0018). |
sop_class_uid |
str
|
SOP Class UID (0008,0016). |
instance_number |
int
|
Instance Number (0020,0013). Not unique within a series. |
file_path |
Optional[str]
|
A file whose pixels match this instance now, read when the store holds no frame for it. None after redaction. |
source_path |
Optional[str]
|
The file this instance was read from. Redaction does not change it. |
pixel_array |
Optional[np.ndarray]
|
The pixels while resident, else
None. Read them with |
waveform_array |
Optional[np.ndarray]
|
The waveform samples while
resident, else None. Read them with |
discard_pixel_data()
Frees the cached pixel_array even if it has unwritten changes.
For a caller who means to throw the resident array away, so that
the next get_pixel_data() reloads the stored frame, read under the
instance's current descriptors.
It undoes the whole set_pixel_data() it discards: the pixels,
and every descriptor that call wrote (Rows, Columns,
SamplesPerPixel, NumberOfFrames, PhotometricInterpretation,
PlanarConfiguration, BitsAllocated, PixelRepresentation and the
float/bool dtype carrier), go back to what they were before the
first unwritten replacement, absent ones included. A pixel
descriptor edited between the set and the discard goes back with
them: while the replacement is resident, that edit describes the
replacement. So the next read is the stored frame as it was stored.
Once the replacement is written (by a save or by redaction) it is
the stored frame, and there is nothing to undo: the array is
dropped and the descriptors, which describe it, stay. A refusal
(below) keeps both the array and the descriptors that describe it.
Dropping an unwritten replacement leaves the instance dirty, as the
set did.
unload_pixel_data() answers "free this if it is safe"; this
answers "throw this away".
Returns:
| Type | Description |
|---|---|
bool
|
True if discarded (or already absent), False if there is nowhere to reload from at all. |
get_pixel_data()
Returns pixel_array, loading it if it is not in memory.
In order, it:
- Returns the cached
pixel_array. - Loads the frame from the store's sidecar, if it holds one.
- Reads
file_pathwith the decodeingest()uses: pydicom, then imagecodecs where pydicom has no plugin. A file ingest refuses is refused here, in the same words. The decoded samples are read under the instance's pixel descriptors where it holds them, as the sidecar's are: the file supplies the samples and any descriptor the instance lacks.
A read whose decoder returns RGB from a YBR-labelled file says so.
An 8-bit YBR_FULL JPEG-LS file read through the imagecodecs
fallback comes back converted to RGB, as ingest() stores it. So
does a JPEG 2000 YBR_RCT/YBR_ICT file, whose codec undoes the
colour transform, and any 8-bit YBR source pydicom decodes, which it
returns as RGB by default. An instance that carries a
PhotometricInterpretation is relabelled RGB to match, which
advances its revision. This is the one write a read makes, and it is
made only when the decode converted.
Returns:
| Type | Description |
|---|---|
Optional[np.ndarray]
|
The pixel data as a numpy array, or None when the instance carries no pixel element. A frame that could not be decoded is not None: it raises. |
Raises:
| Type | Description |
|---|---|
RuntimeError
|
If loading fails due to transfer syntax issues,
missing codecs, or a pixel element the reader could not
decode. Also, from a file, when an encapsulated pixel
element's offset table names a different number of frames
from NumberOfFrames: |
FileNotFoundError
|
If the file path does not exist. |
get_waveform_bytes()
Return the original Waveform Data (5400,1010) bytes, undecoded.
The DICOM export writes these back verbatim, so a DICOM to DICOM round trip is byte-exact. Not cached, so an instance does not hold both the bytes and the decoded array.
Returns:
| Type | Description |
|---|---|
Optional[bytes]
|
Raw sample bytes, or None when this instance has no waveform or its samples are not backed by the sidecar. |
get_waveform_data()
Return decoded waveform samples, loading from the sidecar if needed.
Returns:
| Type | Description |
|---|---|
Optional[np.ndarray]
|
int16 array of shape (num_samples, num_channels), or None if this instance has no waveform. |
regenerate_uid(new_uid)
Gives this instance the SOP Instance UID its redacted pixels take.
Call this whenever pixel data is modified, so the changed image is never
mistaken for the original. new_uid is required: the redaction pass
derives it from the source SOP Instance UID, the redaction's zones and
the project secret, so the same redaction gives the same UID, and no
worker process holds the secret.
Sets the property and the 0008,0018 element together, records the
UID the instance held before in SOURCE_SOP_UID_ATTR the first time
its UID moves, and detaches the instance from its source file
(file_path = None), whose pixels it no longer matches.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
new_uid
|
str
|
The derived SOP Instance UID. |
required |
set_attr(tag, value)
Sets an attribute, and keeps resident pixels reading as it declares.
Every tag is written as DicomItem.set_attr writes it. An edit to
a descriptor a frame is read by (Rows, Columns, SamplesPerPixel,
NumberOfFrames, BitsAllocated, PixelRepresentation) while pixels
are resident also settles the resident array, because a save writes
the array's bytes and every later read takes them under the edit.
The declaration wins:
- An edit under which the bytes read as they read now (the same dtype and shape) is a plain write.
- An array a save has written is released after the write, so the
next read rebuilds it from the store under the edit, or, for an
instance read from its source file, from the file, under the edit
too. Its bytes are stored; nothing is lost. A memory-only array
has nowhere to be reloaded from and stays, as
unload_pixel_datarefuses to drop it. - An array set through
set_pixel_data()and not yet written is republished as the edit reads its bytes: a view under the new dtype, in the new shape, published together with the write, so no read sees the new declaration beside the old array. It stays unwritten, anddiscard_pixel_data()still restores what the set replaced.
Each edit is judged alone. Changing a geometry in two steps whose end state the bytes fit (BitsAllocated 8, then Columns 8, over a 4x4 uint16 set) is refused at the first step; set the array you mean instead, which writes its own descriptors.
A read in flight when the edit lands is not published under the old declaration: it reads again under the new descriptors.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tag
|
str
|
The DICOM tag string. Case-insensitive. |
required |
value
|
Any
|
The value to set. |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
If the edit is to a described tag, the resident
array was set through |
set_pixel_data(array)
Sets the pixel array and updates the descriptors that describe it.
Which axis of the array means what is decided by the instance's own
attributes, not by the array's shape: (frames, rows, cols) and
(rows, cols, samples) are the same rank. How large each axis is
comes from the array.
Updates tags, each only when the value actually changes:
- Rows (0028,0010)
- Columns (0028,0011)
- SamplesPerPixel (0028,0002)
- NumberOfFrames (0028,0008), if > 1 or already declared
- PhotometricInterpretation (0028,0004), only to correct an outright contradiction -- YBR_FULL and MONOCHROME1 survive
- PlanarConfiguration (0028,0006), only when colour and undeclared
- BitsAllocated (0028,0100), from the array's itemsize
- PixelRepresentation (0028,0103), from the array's dtype kind: 1 for signed integers, 0 for unsigned and bool. Left alone for a float array, because PS3.5 Section 8.2 forbids it beside a float pixel element and the export deletes it there.
A genuinely ambiguous shape is accepted with a WARNING, so a
hand-built graph can take pixels before its attributes (for
DicomExporter.write_tree()). The export worker refuses the same
geometry.
The prior value, or absence, of each descriptor it can write is
recorded once, at the first replacement since the array was last
written; a later set keeps that first record. It is kept until the
array is written (by a save or by redaction), or discarded, when
discard_pixel_data() puts it back. Until the array is written,
unload_pixel_data() refuses to drop it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
array
|
np.ndarray
|
The pixel data to set. Can be 1D, 2D, 3D, or 4D. |
required |
Raises:
| Type | Description |
|---|---|
ValueError
|
If |
ValueError
|
If the instance declares a SamplesPerPixel that no
axis of |
unload_pixel_data()
Frees the cached pixel_array, but only when it can be brought back.
Two things have to be true: there must be somewhere to reload from
(the source file, or the store's sidecar), and the resident
array must not have been replaced through set_pixel_data() since
it was last written. Otherwise the array is the only copy of those
pixels, and it is kept.
An array mutated in place is not detected. Only a writeable
array can be mutated in place, and a frame read from a file or from
the store is read-only (assigning into it raises). The reachable
case is a replacement a save has since written: that array is
writeable, so arr = inst.get_pixel_data(); arr[...] = 0 on it
diverges from the stored frame, and unloading it then drops the
mutation.
Use discard_pixel_data() where dropping unsaved pixels is the
intent.
Returns:
| Type | Description |
|---|---|
bool
|
True if unloaded (or already absent), False if it was unsafe to unload: either the data is in memory only and nothing could bring it back, or it has diverged from what is stored. |
unload_waveform_data()
Clear cached waveform samples to free memory.
Unloads only when the store holds the samples to read them back
from; a source file alone is not enough, unlike for
unload_pixel_data.
Returns:
| Type | Description |
|---|---|
bool
|
True if unloaded (or already absent), False if unsafe -- i.e. the samples are in memory only and nothing could reload them. |
DicomItem
dataclass
Bases: TrackedEntity
Base class for any entity that holds DICOM attributes and sequences.
This class provides a dictionary-like interface for managing DICOM attributes. Persistence state comes from TrackedEntity. Items nested in sequences are not stored separately, so the subtree form below reaches them.
Attributes:
| Name | Type | Description |
|---|---|---|
attributes |
Dict[str, Any]
|
A dictionary mapping generic DICOM tags to values. |
sequences |
Dict[str, DicomSequence]
|
A dictionary mapping tags to nested DicomSequences. |
attribute_vrs |
Dict[str, str]
|
The source Value Representation of private tags, where one was known. |
add_sequence(tag)
The sequence at tag, created empty if it is not there yet.
A sequence with no items is kept as one: ingest and hydration call
this before adding items, so a zero-item SQ survives both.
Calls mark_modified() only when it creates the sequence. A call on
a tag that already has one changes nothing the store must hold.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tag
|
str
|
The DICOM tag for the sequence. Case-insensitive. |
required |
Returns:
| Type | Description |
|---|---|
DicomSequence
|
the sequence now at |
add_sequence_item(tag, item)
Appends a new item to a sequence, creating the sequence if needed.
Sets the item's _parent to this item and marks this item modified.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tag
|
str
|
The DICOM tag for the sequence. |
required |
item
|
DicomItem
|
The item to append. |
required |
set_attr(tag, value)
Sets a generic attribute by its hex tag (e.g., '0010,0010').
The tag is lowercased first, so 0008,103E and 0008,103e name
one attribute: ingested keys are always lowercase, and a key in
another casing would read as absent rather than raise.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
tag
|
str
|
The DICOM tag string. Case-insensitive. |
required |
value
|
Any
|
The value to set. |
required |
Equipment
dataclass
Immutable Equipment definition. Frozen=True allows hashing, enabling unique set generation.
Attributes:
| Name | Type | Description |
|---|---|---|
manufacturer |
str
|
The manufacturer of the equipment. |
model_name |
str
|
The model name of the equipment. |
device_serial_number |
str
|
The serial number (optional). |
from_parts(manufacturer, model_name, device_serial_number)
classmethod
Builds an Equipment from a file's or a row's three fields, or nothing.
A series has equipment if and only if it has a manufacturer or a model
name; the serial number is the optional field. A serial number alone is
not equipment: the store keeps device_serial_number for such a series,
and a reload discards it, so after a reload no machine rule matches
that series by serial.
Positional, in field order, all three required. No normalisation:
None stays None, so from_parts("ACME", None, None) equals
Equipment("ACME", None, None).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
manufacturer
|
Optional[str]
|
Manufacturer (0008,0070). |
required |
model_name
|
Optional[str]
|
Manufacturer's Model Name (0008,1090). |
required |
device_serial_number
|
Optional[str]
|
Device Serial Number (0018,1000). |
required |
Returns:
| Type | Description |
|---|---|
Optional[Equipment]
|
the equipment, or |
Documented but internal (tier 2)
Safe to call, and may change in a 1.x release with a CHANGELOG entry that names the old spelling and the new one.
TrackedEntity
dataclass
Tracks whether an entity holds changes the session store does not have.
This is persistence bookkeeping and nothing else. It says whether the
object in memory has been written to the session store -- not whether
it still carries identifiers, which is a separate question with its
own vocabulary (phi_status).
State is read through has_unsaved_changes and moved through
mark_modified() and mark_persisted(). There is no setter: an entity
can be told what happened to it, but not told what it is.
has_unsaved_changes
property
Whether this entity holds changes the store does not have.
phi_status
property
What the last scan concluded, if it still applies.
UNSCANNED once the entity has changed since the scan ran: no status can
describe content the entity no longer holds. A status is recorded under
a policy, which phi_status_policy names; this reads the status as
recorded and never consults the policy in force. A nested DicomItem
takes a status from remediation only (audit() records none on it),
and the store keeps it across a reopen.
phi_status_policy
property
The ScanPolicy the current phi_status was recorded under.
None when the status is UNSCANNED, when it was recorded by a store written before 1.0, and on a nested item, which is never scanned. Stale exactly when the status is, by the same revision check.
mark_modified()
Records that this entity changed and needs writing again.
mark_persisted(revision=None)
Records that a revision of this entity reached the store.
Never moves backwards: a revision older than one already recorded changes nothing.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
revision
|
int
|
The revision that was written. Defaults to the current one, which is only correct when nothing can have changed since the write. A save that takes time should capture the revision before it starts and pass that here. |
None
|
mark_subtree_persisted()
Marks this entity and everything beneath it as stored.
For hydration only, where a whole graph comes from the store and
the claim is true of every node at once. After committing a single
row, call mark_persisted() instead: it speaks for one entity and
does not vouch for unsaved siblings.
record_phi_status(status, policy=_KEEP)
Records what a scan concluded about this entity's current state.
Call this after any change the status describes: remediation modifies the entity, so recording REMEDIATED first would stamp a revision the entity immediately leaves behind.
A new status, or the same status under a different policy (compared
by value), advances the revision and leaves the entity with unsaved
changes. Recording the status an entity already carries, under the
same policy, changes nothing. So an entity loaded at REMEDIATED and
remediated again records nothing here, and the remediation must call
mark_modified() itself for the next save to write it.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
status
|
PhiStatus
|
What the scan concluded. |
required |
policy
|
Optional[ScanPolicy]
|
The policy the status is recorded under. Omitted, the entity keeps the policy it was last recorded under, even when that status has gone stale. Only a scan, hydration and the patient merge pass one. |
_KEEP
|
PhiStatus
Bases: Enum
What the last scan concluded about an entity, and when.
A different question from has_unsaved_changes, which is persistence
bookkeeping.
A status is only valid for the revision it was computed at. Edit the entity and it reads UNSCANNED, because a conclusion about earlier content says nothing about the current content.
One exception: pixel redaction. Redaction re-records an instance's REMEDIATED or CLEARED after its own writes. The pixels, their descriptors, the new SOP Instance UID and its bookkeeping are left out of the comparison; the flags it writes (ImageType, BurnedInAnnotation, DerivationDescription, the Derivation Code Sequence) are accepted only as they were or at exactly what redaction writes. Every other attribute and nested item must be exactly as before the pass; if anything else changed, the status is left UNSCANNED. So the anonymize, redact, export order keeps the redacted instances' statuses.
Attributes:
| Name | Type | Description |
|---|---|---|
UNSCANNED |
Never inspected, or inspected before the entity's current revision. |
|
IDENTIFIED |
The scan found identifiers here, and nothing has acted on them. |
|
REMEDIATED |
Identifiers were found and remediation was applied. |
|
CLEARED |
The tag scan found no identifiers. Burned-in pixel text is a separate scan, and CLEARED does not approve the entity for release. |
ScanPolicy
dataclass
The policy a PHI status was recorded under.
A status says what a scan concluded, and a scan concludes under the rules it ran with. The store keeps the policy beside each status, so a store scanned under one configuration and exported under another says so.
fingerprint decides whether two policies are the same: "v1:" and
the sha256 of the policy's canonical form, which covers what a scan
reads: every rule key but name, remove_private_tags and the
configuration version. base is what a person reads: basic@2026c,
floor over basic@2026c, none, or an external profile's path. Two
policies with one fingerprint and different bases scan identically (a
scaffold and the bare floor, say), so compare fingerprints, never
bases.
Frozen, so a slot holding one is replaced whole, and compared by value, so each audit's fresh object equals the last one's.
Attributes:
| Name | Type | Description |
|---|---|---|
fingerprint |
str
|
|
base |
str
|
The profile the policy was built on, for a reader. |
DicomSequence
dataclass
Represents a DICOM Sequence (SQ) containing multiple DicomItems.
Attributes:
| Name | Type | Description |
|---|---|---|
tag |
str
|
The DICOM tag for this sequence (e.g., "0008,1111"). |
items |
List[DicomItem]
|
A list of DicomItem objects contained in this sequence. |
clone_sequences(item, into)
Deep-copies an item's sequences, for into to hold.
Each copied item carries its attributes and its date-shift record, and
is linked to its copied container as its parent. A worker's copy shares
no sequence item with the session; match items between copies by the
iter_item_tree path, not by identity.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
item
|
DicomItem
|
The item whose sequences are copied. |
required |
into
|
DicomItem
|
The container the copies will sit in. The caller
assigns the result to |
required |
Returns:
| Type | Description |
|---|---|
dict
|
|
exported_patient_id(patient)
The Patient ID a writer puts in a file for patient.
'' for a subject whose files carried no Patient ID (what the source
had, under KEEP and REPLACE alike), and patient.patient_id
otherwise. Every output path reads the Patient ID through this: the
stamp, the folder name, the WFDB record name and the instance copies,
so the synthetic key, and the source Study Instance UID inside it, never
reach an exported file or path.
A third-party exporter (exporters.register) calls this too, and never
writes patient.patient_id: the built-ins' write path, which applies
this rule for them, does not run for a plugin. Documented but internal
(tier 2), like the registry it serves.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
patient
|
Patient
|
The patient being written. |
required |
Returns:
| Type | Description |
|---|---|
str
|
The Patient ID to write; |
iter_item_tree(item, path=())
Yields (item, path) for item and every item nested below it.
Depth-first, and in declaration order, so two copies of the same graph are walked identically. A path matches a sequence item back to the same item in another copy of the graph: nested items carry no UID, so position is their only identity.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
item
|
DicomItem
|
The root of the walk. |
required |
path
|
tuple
|
The root's own path; |
()
|
Yields:
| Type | Description |
|---|---|
tuple
|
|
normalize_study_date(value)
The one spelling of "text that names a day becomes a date".
Reads the extended form 2024-01-15, which the store writes, and the
DICOM basic form 20240115, which a hand-built graph or a
DicomBuilder.add_study call supplies. Study applies the same rule
when study_date is assigned.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
value
|
Any
|
A date string, a |
required |
Returns:
| Type | Description |
|---|---|
Any
|
The |
resolve_item_path(root, path)
Follows a path from iter_item_tree back to an item, or None.
The caller must treat None as "this item is gone", never as "use the root instead": writing a nested tag onto the root fabricates a top-level element that was never in the file, and leaves the real value in place.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
root
|
DicomItem
|
The item the path starts from. |
required |
path
|
tuple
|
|
required |
Returns:
| Type | Description |
|---|---|
Optional[DicomItem]
|
The item at |