Skip to content

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 patient_id.

required

Returns:

Type Description
bool

True for a string that starts with NO_PATIENT_ID_PREFIX.

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 date_shift_vouches_for.

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). anonymize() and redact() can replace it.

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 get_pixel_data().

waveform_array Optional[np.ndarray]

The waveform samples while resident, else None. Read them with get_waveform_data().

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:

  1. Returns the cached pixel_array.
  2. Loads the frame from the store's sidecar, if it holds one.
  3. Reads file_path with the decode ingest() 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: Lazy load failed for instance <uid>: RuntimeError: <table> names N frames; NumberOfFrames declares M. The message names the instance, never the source file. From a file ingest would refuse, in ingest's words: a header pydicom's validation rejects, whether or not pydicom has a plugin for its syntax (Missing required element: (0028,0006) 'Planar Configuration'), or a 16-bit YBR_FULL frame. From the sidecar, when a descriptor written since the frame was stored asks for a reading the stored bytes cannot satisfy (BitsAllocated 16 -> 8, or Rows x Columns smaller than the stored samples): Pixel Loader failed for <uid>: Integrity Error: .... A reopened session gives the same refusal. From a file, the same refusal for the same edit, as Lazy load failed for instance <uid>: RuntimeError: Integrity Error: frame for <uid> holds N samples; geometry (R, C) needs M (one trailing pad byte is tolerated, nothing else).

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_data refuses 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, and discard_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() and not written since, and the edit reads its bytes as a different number of bytes, or does not parse as an integer: "BitsAllocated would read the unsaved uint16 (4, 4) array set by set_pixel_data() as 16 1-byte uint8 samples, 16 bytes, and the array holds 32. Pass the array you mean to set_pixel_data(), which writes its own descriptors." The value is never in the message. Raised before anything is written: the attribute and the revision are unchanged.

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 array.dtype is one the sidecar cannot round-trip. The accepted set is unsigned and signed integers of 1, 2, 4 or 8 bytes, bool, and float16/float32/float64; complex64, object, strings, structured and void dtypes, datetimes and float128 are refused. Byte order is normalised, not refused, so a big-endian array is accepted and stored native-order. This check runs before any mutation, so a caught ValueError leaves the instance exactly as it was.

ValueError

If the instance declares a SamplesPerPixel that no axis of array can carry, or if the rank is unsupported. This one raises after pixel_array has been assigned.

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 tag, existing or new.

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 None when neither identifying field is present.

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

"v1:" and a sha256 hex digest; equal fingerprints scan identically.

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 into.sequences.

required

Returns:

Type Description
dict

{tag: DicomSequence} of the copies.

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; '' for a synthetic key.

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; () for a top-level walk.

()

Yields:

Type Description
tuple

(DicomItem, path), where path is a tuple of (sequence_tag, index) steps from the root, empty for the root itself.

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 date, or None.

required

Returns:

Type Description
Any

The date it names; None for None; anything it cannot read comes back exactly as it was given, neither invented nor discarded.

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

(sequence_tag, index) steps; empty or None for the root itself.

required

Returns:

Type Description
Optional[DicomItem]

The item at path, or None when the graph changed after the path was recorded (an item removed, or a sequence shortened).

SOURCE_SOP_UID_ATTR = '_ISOCENTER_SOURCE_SOP_UID' module-attribute