Skip to content

Stamp IEC 61217 beam geometry into RT Dose export and SR - #13

Merged
cptfinch merged 2 commits into
mainfrom
claude/chromadose-geometry-stamp
Jul 7, 2026
Merged

cptfinch merged 2 commits into
mainfrom
claude/chromadose-geometry-stamp

Conversation

@cptfinch

@cptfinch cptfinch commented Jul 3, 2026

Copy link
Copy Markdown
Owner

Summary

Wires the BeamGeometry data (from #12) into the DICOM exports (#10 RT Dose, #11 SR) — so a QA export can carry the IEC 61217 gantry / collimator / couch angles it was delivered at.

from chromadose.io import save_dicom_dose, save_dicom_sr, load_beam_geometry
geom = load_beam_geometry("rtplan.dcm")[0]
save_dicom_dose(dose, "dose.dcm", geometry=geom)
save_dicom_sr("qa_sr.dcm", gamma_result=g, geometry=geom)
chromadose export-dicom --dose dose.npy --rtplan plan.dcm -o dose.dcm
chromadose export-sr   --measured dose.npy --rtplan plan.dcm -o qa_sr.dcm

What's included

  • save_dicom_dose(..., geometry=None) — records GantryAngle / BeamLimitingDeviceAngle / PatientSupportAngle (+ BeamName). These are standard DICOM keywords; I've documented them as a supplementary annotation (they belong to the RT Beam module, not the RT Dose IOD) so it's honest about conformance.
  • save_dicom_sr(..., geometry=None) — adds a "Beam geometry" container to the SR content tree: gantry/collimator/couch angles in degrees (UCUM deg), plus beam name and energy. Fully standards-clean (SR is coded free-form content) and reads back via read_dicom_sr.
  • CLI --rtplan on both export-dicom and export-sr: pulls geometry from the RT Plan's first treatment beam, printing a note when the plan has >1 beam (composite QA dose has no single gantry angle — the caller/CLI picks the relevant beam).

Tests

Geometry round-trips through the RT Dose angle tags and the SR content tree; plus a CLI test that reads geometry from a synthetic RT Plan via export-sr. All DICOM tests skip cleanly without pydicom.

  • Full suite: 123 passed (was 120). ruff check src/ clean, mypy --strict clean (23 files) via the project's uv toolchain (matches CI).

Notes

  • Both new arguments are optional with None defaults — fully backward compatible.
  • For multi-beam (VMAT/IMRT) plans a single gantry angle isn't well-defined for a composite dose; the CLI records the first beam and warns. Per-beam geometry lists in the SR would be a reasonable future extension.

🤖 Generated with Claude Code


Generated by Claude Code

Wires the BeamGeometry data from #12 into the DICOM exports (#10/#11):

- save_dicom_dose gains an optional `geometry` argument; when given it records
  the gantry / collimator / couch angles as the standard DICOM angle attributes
  (a supplementary annotation — they belong to the RT Beam module, not the RT
  Dose IOD, but are widely readable) plus the beam name.
- save_dicom_sr gains an optional `geometry` argument; when given it adds a
  "Beam geometry" container to the SR content tree (gantry / collimator / couch
  angles in degrees via UCUM, plus beam name and energy).
- CLI: `export-dicom` and `export-sr` take `--rtplan PATH` to pull the geometry
  from an RT Plan's first treatment beam (with a note when the plan has more
  than one beam).

Tests: geometry round-trips through the RT Dose angle tags and the SR content
tree, plus a CLI test that reads geometry from an RT Plan via export-sr.
Full suite 123 passed; ruff (src) and mypy strict clean. Docs + CHANGELOG updated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yuan3yEbpjdBsYaCooLmB

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces support for recording IEC 61217 beam geometry (gantry, collimator, and couch angles) in DICOM RT Dose and Structured Report (SR) exports. The export-dicom and export-sr CLI commands are updated to accept a --rtplan option to extract this geometry from the first treatment beam of an RT Plan. Feedback on the implementation suggests catching potential exceptions gracefully when loading the RT Plan in the CLI and redirecting informational output to sys.stderr instead of stdout.

Important

The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.

Comment on lines +411 to +425
def _geometry_from_rtplan(path: str | None) -> BeamGeometry | None:
"""Load the first treatment beam's IEC 61217 geometry from an RT Plan."""
if not path:
return None

from chromadose.io.dicom import load_beam_geometry

beams = load_beam_geometry(path)
if not beams:
print(f"Warning: no treatment beams in {path}; geometry not recorded", file=sys.stderr)
return None
if len(beams) > 1:
label = beams[0].beam_name or "beam 1"
print(f"Note: RT Plan has {len(beams)} beams; recording geometry of the first ({label})")
return beams[0]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The _geometry_from_rtplan helper function calls load_beam_geometry(path) which can raise several exceptions (e.g., FileNotFoundError if the file is missing, ValueError if the file is not a valid RT Plan, or ImportError if pydicom is not installed). Currently, these exceptions are not caught, which will cause the CLI to crash with a traceback.

Additionally, the informational note when multiple beams are found is printed to stdout instead of sys.stderr. All diagnostic/informational logs should ideally be directed to sys.stderr to keep stdout clean.

We should wrap the loading logic in a try-except block to handle errors gracefully and print the note to sys.stderr.

Suggested change
def _geometry_from_rtplan(path: str | None) -> BeamGeometry | None:
"""Load the first treatment beam's IEC 61217 geometry from an RT Plan."""
if not path:
return None
from chromadose.io.dicom import load_beam_geometry
beams = load_beam_geometry(path)
if not beams:
print(f"Warning: no treatment beams in {path}; geometry not recorded", file=sys.stderr)
return None
if len(beams) > 1:
label = beams[0].beam_name or "beam 1"
print(f"Note: RT Plan has {len(beams)} beams; recording geometry of the first ({label})")
return beams[0]
def _geometry_from_rtplan(path: str | None) -> BeamGeometry | None:
"""Load the first treatment beam's IEC 61217 geometry from an RT Plan."""
if not path:
return None
from chromadose.io.dicom import load_beam_geometry
try:
beams = load_beam_geometry(path)
except Exception as exc:
print(f"Error loading RT Plan '{path}': {exc}", file=sys.stderr)
sys.exit(1)
if not beams:
print(f"Warning: no treatment beams in {path}; geometry not recorded", file=sys.stderr)
return None
if len(beams) > 1:
label = beams[0].beam_name or "beam 1"
print(f"Note: RT Plan has {len(beams)} beams; recording geometry of the first ({label})", file=sys.stderr)
return beams[0]

Addresses review feedback on #13:
- Wrap load_beam_geometry in _geometry_from_rtplan in try/except so a missing
  or invalid RT Plan (or missing pydicom) prints a clean error and exits 1
  instead of dumping a traceback.
- Send the multi-beam informational note to stderr, keeping stdout clean.

Adds a test that a missing --rtplan exits with code 1.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016yuan3yEbpjdBsYaCooLmB
@cptfinch
cptfinch merged commit 5c49bf4 into main Jul 7, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants