Skip to content

feat(clinical): radiology report section parser with stated RADS capture#1849

Merged
maziyarpanahi merged 3 commits into
maziyarpanahi:masterfrom
PouyanJay:feat/radiology-report-parser
Jul 21, 2026
Merged

feat(clinical): radiology report section parser with stated RADS capture#1849
maziyarpanahi merged 3 commits into
maziyarpanahi:masterfrom
PouyanJay:feat/radiology-report-parser

Conversation

@PouyanJay

Copy link
Copy Markdown
Contributor

Summary

Implements #1838 (OM-837): a deterministic radiology report parser that segments a report into a findings/impression/recommendation template and captures a stated BI-RADS or Lung-RADS category. It makes no assessment decision of its own — a captured category is read verbatim from the report text, never computed or inferred.

  • openmed/clinical/radiology_report.py: parse_radiology_report(text) -> RadiologyReportTemplate (TypedDict: findings_text, impression_text, recommendation_text, assessment_system, assessment_category, section_spans).
  • Segmentation uses a documented heading lexicon matched at line starts, with a graceful inline-cue fallback (e.g. IMPRESSION:) when a report has no standalone headings. Per-section character spans are preserved for provenance.
  • Stated-category capture: BI-RADS 0–6 and Lung-RADS 0,1,2,3,4A,4B,4X, captured only when explicitly written (qualifier-introduced or terminal), never inferred.
  • RADIOLOGY_REPORT_ADVISORY records the read-not-computed guarantee; exported from openmed.clinical.
  • openmed/eval/metrics.py: parser-agnostic section_boundary_accuracy and stated_category_accuracy.
  • Synthetic gold fixtures + an offline eval gate asserting section-boundary accuracy ≥ 0.90 and stated-category accuracy ≥ 0.95 (both 1.00 on the current gold set).

Acceptance criteria

  • parse_radiology_report() splits findings/impression/recommendation on all synthetic gold rows.
  • Section-boundary accuracy ≥ 0.90 and stated-category accuracy ≥ 0.95 on the synthetic gold set.
  • Reports without explicit headings still segment via cues (covered by tests).
  • No assessment category is inferred when absent; only explicitly stated categories are captured.
  • RADIOLOGY_REPORT_ADVISORY is emitted and asserted; offline eval gate passes.

Deviations from the issue's letter (with evidence)

  • Extra file — openmed/eval/golden/loader.py. The golden-fixture loader globs **/*.jsonl and raises on any unknown category, so a naively-added openmed/eval/golden/fixtures/radiology_report.jsonl would be picked up as a PII de-identification fixture and break test_golden_fixtures/test_fixture_coverage. The fixture name is added to _NON_DEID_FIXTURE_NAMES so the de-id loader skips it. Verified: load_golden_fixtures() returns 90 fixtures unchanged and the golden-fixture gates still pass.
  • Extra test files. tests/unit/clinical/test_radiology_report.py (unit) and tests/unit/eval/test_radiology_report_eval.py (offline eval gate) — the issue lists metrics/fixtures but not the test files that exercise them.
  • Sequencing. The issue is formally "Depends on: OM-168, OM-836". OM-168's section-detection foundation is already on master (clinical/sections/detect.py); OM-836 (Add a RadLex-coded radiology-finding extractor with finding, laterality, size and location #1837) is a separate module (radiology_finding.py) that this section parser does not use. This lands ahead of Add a RadLex-coded radiology-finding extractor with finding, laterality, size and location #1837 but has no code dependency on it.

Data provenance

All fixtures are synthetic and algorithmically generated (fictional reports, no real patient data). Categories in fixtures are written verbatim, never computed.

Verification

  • New radiology tests: 53 passed. Section-boundary and stated-category accuracy = 1.00 (gates ≥0.90 / ≥0.95).
  • Full suite: 5310 passed, 46 skipped. One unrelated failure, tests/unit/mcp/test_mcp_entrypoint.py::test_console_entry_point_resolves, is pre-existing on master (a stale editable-install artifact for the openmed-mcp console entry point; verified by reproducing it with these changes stashed) and is not caused by this PR.
  • Adversarial audit hardened the stated-only guarantee: counts and scale legends adjacent to the token (e.g. BI-RADS 4 lesions, BI-RADS 0-6 assessment scale) are correctly not captured, and a count in one section never shadows a stated category in another — covered by regression tests and gold rows.
  • pre-commit (ruff, ruff-format, bandit, gitleaks) clean on changed files.
  • Rebased on master; git rev-list --count HEAD..upstream/master is 0.

Closes #1838

…capture

Add openmed/clinical/radiology_report.py: a deterministic parser that segments a
radiology report into a findings/impression/recommendation template and captures
a stated BI-RADS (0-6) or Lung-RADS (0,1,2,3,4A,4B,4X) assessment category only
when it is explicitly written. Segmentation uses a documented heading lexicon
matched at line starts, with an inline-cue fallback (e.g. "IMPRESSION:") when a
report has no standalone headings, and preserves per-section character spans for
provenance. RADIOLOGY_REPORT_ADVISORY records that any captured category is read
verbatim from the report, never computed or inferred.

- Export parse_radiology_report, RadiologyReportTemplate, and the advisory from
  openmed.clinical.
- Add parser-agnostic section_boundary_accuracy and stated_category_accuracy
  metrics to openmed/eval/metrics.py.
- Commit synthetic gold fixtures (tests/fixtures/clinical/radiology_report.jsonl
  and openmed/eval/golden/fixtures/radiology_report.jsonl) and an offline eval
  gate asserting section-boundary accuracy >= 0.90 and stated-category accuracy
  >= 0.95 (both 1.00 on the current gold set).
- Exclude the radiology eval fixture from the PII de-identification golden-fixture
  loader glob so load_golden_fixtures() does not treat it as de-id gold.

All fixtures are synthetic and algorithmically constructed.

Closes maziyarpanahi#1838
@maziyarpanahi
maziyarpanahi self-requested a review July 20, 2026 21:55
@maziyarpanahi maziyarpanahi added feature New capability P2 Medium roadmap-v2 OpenMed V2 roadmap backlog labels Jul 21, 2026
@maziyarpanahi

Copy link
Copy Markdown
Owner

Thank you @PouyanJay — this was a thoughtful, well-scoped implementation. I reviewed it against #1838 and current master, then added maintainer follow-ups f6810cd6 and 3fafae7b.

What changed:

  • Hardened flattened single-line section parsing while avoiding false section labels in ordinary prose.
  • Captured explicit category is ... and assessment-category wording, and rejected RADS legends so reference material is never mistaken for an assessment.
  • Added regression tests and the Unreleased changelog entry, and kept the branch mergeable with current master.
  • Confirmed the parser is code-independent of the planned finding extractor in Add a RadLex-coded radiology-finding extractor with finding, laterality, size and location #1837.

Validation:

  • Focused radiology and golden-fixture tests: 90 passed.
  • Full suite: 5,674 passed, 56 skipped.
  • Ruff format, lint, format-check, and scoped pre-commit: passed.
  • Hosted CI: all 18 checks passed, including Python 3.10–3.12 across Ubuntu, macOS, and Windows; security; package build; SBOM; and amd64/arm64 container smoke.

This now satisfies #1838 and is ready to merge. Thank you for the solid contribution!

@maziyarpanahi
maziyarpanahi merged commit e4d57be into maziyarpanahi:master Jul 21, 2026
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feature New capability P2 Medium roadmap-v2 OpenMed V2 roadmap backlog

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add a structured radiology impression parser separating findings, impression and recommendation

2 participants