Skip to content

Commit 10f153d

Browse files
Add SR "get_evidence" methods (#303)
* Enforce repo-review rules (#296) * Enforce repo-review rule PP302 PP302: Sets a minimum pytest to at least 6 Must have a `minversion=`, and must be at least 6 (first version to support `pyproject.toml` configuration). * Enforce repo-review rule PP305 PP305: Specifies xfail_strict `xfail_strict` should be set. You can manually specify if a check should be strict when setting each xfail. * Enforce repo-review rule PP306 PP306: Specifies strict config `--strict-config` should be in `addopts = [...]`. This forces an error if a config setting is misspelled. * Enforce repo-review rule PP307 PP307: Specifies strict markers `--strict-markers` should be in `addopts = [...]`. This forces all markers to be specified in config, avoiding misspellings. * Enforce repo-review rule PP308 PP308: Specifies useful pytest summary An explicit summary flag like `-ra` should be in `addopts = [...]` (print summary of all fails/errors). * Enforce pytest ≥ 7.3.2 This is the first version to support Python 3.12: https://docs.pytest.org/en/stable/changelog.html#pytest-7-3-2-2023-06-10 * Enforce repo-review rule MY104 MY104: MyPy enables ignore-without-code Must have `"ignore-without-code"` in `enable_error_code = [...]`. This will force all skips in your project to include the error code, which makes them more readable, and avoids skipping something unintended. * Enforce repo-review rule MY105 MY105: MyPy enables redundant-expr Must have `"redundant-expr"` in `enable_error_code = [...]`. This helps catch useless lines of code, like checking the same condition twice. * Enforce repo-review rule MY106 MY106: MyPy enables truthy-bool Must have `"truthy-bool"` in `enable_error_code = []`. This catches mistakes in using a value as truthy if it cannot be falsy. * Add get_evidence and get_evidence_series methods for _SR * Fix to docstrings --------- Co-authored-by: Dimitri Papadopoulos Orfanos <3234522+DimitriPapadopoulos@users.noreply.github.com>
1 parent 534efa9 commit 10f153d

2 files changed

Lines changed: 226 additions & 0 deletions

File tree

src/highdicom/sr/sop.py

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
"""Module for SOP Classes of Structured Report (SR) IODs."""
22
import datetime
3+
from itertools import chain
34
import logging
45
from collections import defaultdict
56
from copy import deepcopy
67
from os import PathLike
78
from typing import (
89
Any,
10+
Generator,
911
cast,
1012
Mapping,
1113
List,
@@ -351,6 +353,111 @@ def content(self) -> ContentSequence:
351353
"""highdicom.sr.value_types.ContentSequence: SR document content"""
352354
return self._content
353355

356+
def get_evidence(
357+
self,
358+
current_procedure_only: bool = False,
359+
) -> List[Tuple[UID, UID, UID, UID]]:
360+
"""Get a list of all SOP Instances listed as evidence in this SR.
361+
362+
Parameters
363+
----------
364+
current_procedure_only: bool, optional
365+
If True, return only evidence created in order to satisfy the
366+
current requested procedure (found in the
367+
*CurrentRequestedProcedureEvidenceSequence*). If False, also
368+
include other evidence (found in the
369+
*PertinentOtherEvidenceSequence*).
370+
371+
Returns
372+
-------
373+
List[Tuple[highdicom.UID, highdicom.UID, highdicom.UID, highdicom.UID]]:
374+
List of tuples of UIDs, each representing a single instance. Each
375+
tuple consists of (StudyInstanceUID, SeriesInstanceUID,
376+
SOPInstanceUID, SOPClassUID).
377+
378+
"""
379+
def extract_evidence(
380+
sequence: DataElementSequence,
381+
) -> Generator[Tuple[UID, UID, UID, UID], None, None]:
382+
for item in sequence:
383+
for series_ds in item.ReferencedSeriesSequence:
384+
for instance_ds in series_ds.ReferencedSOPSequence:
385+
yield (
386+
UID(item.StudyInstanceUID),
387+
UID(series_ds.SeriesInstanceUID),
388+
UID(instance_ds.ReferencedSOPInstanceUID),
389+
UID(instance_ds.ReferencedSOPClassUID),
390+
)
391+
392+
current_evidence_seq = self.get('CurrentRequestedProcedureEvidenceSequence')
393+
if current_evidence_seq is not None:
394+
current_evidence = extract_evidence(current_evidence_seq)
395+
else:
396+
current_evidence = []
397+
398+
other_evidence_seq = self.get('PertinentOtherEvidenceSequence')
399+
if other_evidence_seq is not None and not current_procedure_only:
400+
other_evidence = extract_evidence(other_evidence_seq)
401+
else:
402+
other_evidence = []
403+
404+
evidence = list(chain(current_evidence, other_evidence))
405+
406+
# Deduplicate the list while preserving order
407+
evidence = list(dict.fromkeys(evidence))
408+
409+
return evidence
410+
411+
def get_evidence_series(
412+
self,
413+
current_procedure_only: bool = False,
414+
) -> List[Tuple[UID, UID]]:
415+
"""Get a list of all series listed as evidence in this SR.
416+
417+
Parameters
418+
----------
419+
current_procedure_only: bool, optional
420+
If True, return only evidence created in order to satisfy the
421+
current requested procedure (found in the
422+
*CurrentRequestedProcedureEvidenceSequence*). If False, also
423+
include other evidence (found in the
424+
*PertinentOtherEvidenceSequence*).
425+
426+
Returns
427+
-------
428+
List[Tuple[highdicom.UID, highdicom.UID]]:
429+
List of tuples of UIDs, each representing a single series. Each
430+
tuple consists of (StudyInstanceUID, SeriesInstanceUID).
431+
432+
"""
433+
def extract_evidence_series(
434+
sequence: DataElementSequence,
435+
) -> Generator[Tuple[UID, UID], None, None]:
436+
for item in sequence:
437+
for series_ds in item.ReferencedSeriesSequence:
438+
yield (
439+
UID(item.StudyInstanceUID),
440+
UID(series_ds.SeriesInstanceUID),
441+
)
442+
443+
current_evidence_seq = self.get('CurrentRequestedProcedureEvidenceSequence')
444+
if current_evidence_seq is not None:
445+
current_evidence = extract_evidence_series(current_evidence_seq)
446+
else:
447+
current_evidence = []
448+
449+
other_evidence_seq = self.get('PertinentOtherEvidenceSequence')
450+
if other_evidence_seq is not None and not current_procedure_only:
451+
other_evidence = extract_evidence_series(other_evidence_seq)
452+
else:
453+
other_evidence = []
454+
455+
evidence = list(chain(current_evidence, other_evidence))
456+
457+
# Deduplicate the list while preserving order
458+
evidence = list(dict.fromkeys(evidence))
459+
460+
return evidence
354461

355462
class EnhancedSR(_SR):
356463

tests/test_sr.py

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3772,6 +3772,20 @@ def test_construction(self):
37723772
performed_procedure_codes=self._performed_procedures
37733773
)
37743774
assert report.SOPClassUID == '1.2.840.10008.5.1.4.1.1.88.22'
3775+
evidence = report.get_evidence()
3776+
assert len(evidence) == 1
3777+
assert evidence[0] == (
3778+
self._ref_dataset.StudyInstanceUID,
3779+
self._ref_dataset.SeriesInstanceUID,
3780+
self._ref_dataset.SOPInstanceUID,
3781+
self._ref_dataset.SOPClassUID,
3782+
)
3783+
evidence_series = report.get_evidence_series()
3784+
assert len(evidence_series) == 1
3785+
assert evidence_series[0] == (
3786+
self._ref_dataset.StudyInstanceUID,
3787+
self._ref_dataset.SeriesInstanceUID,
3788+
)
37753789

37763790
def test_construction_content_is_sequence(self):
37773791
report = EnhancedSR(
@@ -3913,6 +3927,21 @@ def test_construction(self):
39133927
with pytest.raises(AttributeError):
39143928
assert report.PertinentOtherEvidenceSequence
39153929

3930+
evidence = report.get_evidence()
3931+
assert len(evidence) == 1
3932+
assert evidence[0] == (
3933+
self._ref_dataset.StudyInstanceUID,
3934+
self._ref_dataset.SeriesInstanceUID,
3935+
self._ref_dataset.SOPInstanceUID,
3936+
self._ref_dataset.SOPClassUID,
3937+
)
3938+
evidence_series = report.get_evidence_series()
3939+
assert len(evidence_series) == 1
3940+
assert evidence_series[0] == (
3941+
self._ref_dataset.StudyInstanceUID,
3942+
self._ref_dataset.SeriesInstanceUID,
3943+
)
3944+
39163945
def test_construction_content_is_sequence(self):
39173946
report = ComprehensiveSR(
39183947
evidence=[self._ref_dataset],
@@ -4029,6 +4058,96 @@ def test_evidence_multiple_studies(self):
40294058
with pytest.raises(AttributeError):
40304059
assert report.PertinentOtherEvidenceSequence
40314060

4061+
evidence = report.get_evidence()
4062+
assert len(evidence) == 2
4063+
assert evidence[0] == (
4064+
self._ref_dataset.StudyInstanceUID,
4065+
self._ref_dataset.SeriesInstanceUID,
4066+
self._ref_dataset.SOPInstanceUID,
4067+
self._ref_dataset.SOPClassUID,
4068+
)
4069+
assert evidence[1] == (
4070+
ref_dataset.StudyInstanceUID,
4071+
ref_dataset.SeriesInstanceUID,
4072+
ref_dataset.SOPInstanceUID,
4073+
ref_dataset.SOPClassUID,
4074+
)
4075+
evidence_series = report.get_evidence_series()
4076+
assert len(evidence_series) == 2
4077+
assert evidence_series[0] == (
4078+
self._ref_dataset.StudyInstanceUID,
4079+
self._ref_dataset.SeriesInstanceUID,
4080+
)
4081+
assert evidence_series[1] == (
4082+
ref_dataset.StudyInstanceUID,
4083+
ref_dataset.SeriesInstanceUID,
4084+
)
4085+
4086+
def test_current_and_other_evidence(self):
4087+
ref_dataset2 = deepcopy(self._ref_dataset)
4088+
ref_dataset2.SeriesInstanceUID = '1.2.3'
4089+
ref_dataset2.SOPInstanceUID = '1.2.3'
4090+
4091+
report = Comprehensive3DSR(
4092+
evidence=[self._ref_dataset, ref_dataset2],
4093+
content=self._content,
4094+
series_instance_uid=self._series_instance_uid,
4095+
series_number=self._series_number,
4096+
sop_instance_uid=self._sop_instance_uid,
4097+
instance_number=self._instance_number,
4098+
institution_name=self._institution_name,
4099+
institutional_department_name=self._department_name,
4100+
manufacturer=self._manufacturer
4101+
)
4102+
ref_evd_items = report.CurrentRequestedProcedureEvidenceSequence
4103+
assert len(ref_evd_items) == 1
4104+
unref_evd_items = report.PertinentOtherEvidenceSequence
4105+
assert len(unref_evd_items) == 1
4106+
4107+
evidence = report.get_evidence()
4108+
assert len(evidence) == 2
4109+
assert evidence[0] == (
4110+
self._ref_dataset.StudyInstanceUID,
4111+
self._ref_dataset.SeriesInstanceUID,
4112+
self._ref_dataset.SOPInstanceUID,
4113+
self._ref_dataset.SOPClassUID,
4114+
)
4115+
assert evidence[1] == (
4116+
ref_dataset2.StudyInstanceUID,
4117+
ref_dataset2.SeriesInstanceUID,
4118+
ref_dataset2.SOPInstanceUID,
4119+
ref_dataset2.SOPClassUID,
4120+
)
4121+
evidence_series = report.get_evidence_series()
4122+
assert len(evidence_series) == 2
4123+
assert evidence_series[0] == (
4124+
self._ref_dataset.StudyInstanceUID,
4125+
self._ref_dataset.SeriesInstanceUID,
4126+
)
4127+
assert evidence_series[1] == (
4128+
ref_dataset2.StudyInstanceUID,
4129+
ref_dataset2.SeriesInstanceUID,
4130+
)
4131+
4132+
current_evidence = report.get_evidence(
4133+
current_procedure_only=True
4134+
)
4135+
assert len(current_evidence) == 1
4136+
assert current_evidence[0] == (
4137+
self._ref_dataset.StudyInstanceUID,
4138+
self._ref_dataset.SeriesInstanceUID,
4139+
self._ref_dataset.SOPInstanceUID,
4140+
self._ref_dataset.SOPClassUID,
4141+
)
4142+
current_evidence_series = report.get_evidence_series(
4143+
current_procedure_only=True
4144+
)
4145+
assert len(current_evidence_series) == 1
4146+
assert current_evidence_series[0] == (
4147+
self._ref_dataset.StudyInstanceUID,
4148+
self._ref_dataset.SeriesInstanceUID,
4149+
)
4150+
40324151
def test_srread(self):
40334152
report = ComprehensiveSR(
40344153
evidence=[self._ref_dataset],

0 commit comments

Comments
 (0)