Repository navigation
Stamp IEC 61217 beam geometry into RT Dose export and SR - #13
Conversation
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
There was a problem hiding this comment.
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.
| 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] |
There was a problem hiding this comment.
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.
| 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
Summary
Wires the
BeamGeometrydata (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.What's included
save_dicom_dose(..., geometry=None)— recordsGantryAngle/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 (UCUMdeg), plus beam name and energy. Fully standards-clean (SR is coded free-form content) and reads back viaread_dicom_sr.--rtplanon bothexport-dicomandexport-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.ruff check src/clean,mypy --strictclean (23 files) via the project'suvtoolchain (matches CI).Notes
Nonedefaults — fully backward compatible.🤖 Generated with Claude Code
Generated by Claude Code