Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
e3a71d3
Strengthen MATLAB<->Python parity testing
ironictoo Jul 2, 2026
2a86556
ascii art and version number
ironictoo Jul 8, 2026
2806551
Rework DCE parity: GM/WM regions, RMSE+CI metrics, ROI-only fit mode
ironictoo Jul 10, 2026
2bce8fc
Remove --run-parity/--run-full-parity; roi_xls parity default-on
ironictoo Jul 10, 2026
9cff785
Phantom GT: real Python CIs, GT-in-CI coverage metric, BIDS data docs
ironictoo Jul 10, 2026
077c863
new t1 gm roi, notes
ironictoo Jul 10, 2026
8cd2cb2
Consolidate test fixtures into BIDS_test; remove ci_fixtures
ironictoo Jul 11, 2026
5192cd1
docs: reconcile parity/preferences/fixture changes; ignore RUNNER_DATA
ironictoo Jul 12, 2026
9244803
OSIPI: gate on official tolerances, make peer spread reproducible
ironictoo Jul 13, 2026
25a5c83
OSIPI: multi-backend verification + backend-agnostic multi-start (2CU…
ironictoo Jul 13, 2026
86d9704
OSIPI: adopt Gpufit-harness random multi-start; correct 2CXM root cause
ironictoo Jul 13, 2026
62ba659
OSIPI: reparameterize accelerated 2CXM/2CUM to E=Ktrans/Fp (analytic …
ironictoo Jul 14, 2026
3fd2676
Update all fp lower limits to 1e-4
ironictoo Jul 14, 2026
1f96a68
OSIPI finished patched in gpufit, remove xfail
ironictoo Jul 14, 2026
9a91c74
Fix runtime parity bug
ironictoo Jul 15, 2026
72719a8
Fix CI paths left stale by BIDS_test fixture consolidation
ironictoo Jul 15, 2026
313d3b6
Refresh sub-10bbbdownsample MATLAB parity baseline maps
ironictoo Jul 15, 2026
3c17ff3
Add AIF-sidecar SteadyStateEndTimeIndex mechanism; default auto-detec…
ironictoo Jul 15, 2026
66fd795
Switch sub-10bbbdownsample parity fixture to always auto-detect stead…
ironictoo Jul 15, 2026
3abdc0c
Remove dead _baseline_window function
ironictoo Jul 15, 2026
b3f5a6a
Add MATLAB region-parity baseline drift guard (sub-10bbbdownsample)
ironictoo Jul 15, 2026
6140e1d
Migrate settings-matrix test and DCE benchmark to the AIF sidecar mec…
ironictoo Jul 15, 2026
e5a7554
Migrate phantom GT baseline alignment to the AIF sidecar mechanism
ironictoo Jul 15, 2026
4022173
Fix matlabref drift guard: gate on correlation, not max-abs-diff
ironictoo Jul 15, 2026
0daf3a4
feat: Consolidate Stage-D fitting backend for Patlak model
ironictoo Jul 17, 2026
910c307
feat: Migrate Tofts and Extended Tofts models to shared Stage-D fit m…
ironictoo Jul 17, 2026
14f3b01
Refactor DCE model fitting to utilize shared backend machinery
ironictoo Jul 17, 2026
75db58d
Refactor DCE fitting pipeline to unify model fitting and improve erro…
ironictoo Jul 17, 2026
cb0cc68
Enhance Patlak candidate assembly and consistently clamp initial para…
ironictoo Jul 17, 2026
276e4f2
update agents file
ironictoo Jul 17, 2026
8ddec76
Fix Stage-D unit-scaling and CI/guard regressions found in self-review
ironictoo Jul 22, 2026
ddc35c0
Switch MATLAB region-parity drift guard to Spearman correlation
ironictoo Jul 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
76 changes: 70 additions & 6 deletions .github/workflows/run_DCE.yml
Original file line number Diff line number Diff line change
Expand Up @@ -110,21 +110,85 @@ jobs:
products: Curve_Fitting_Toolbox Image_Processing_Toolbox Optimization_Toolbox Parallel_Computing_Toolbox Statistics_and_Machine_Learning_Toolbox
release: R2022a

- name: Regenerate MATLAB parity baseline (for drift guard)
uses: matlab-actions/run-command@v2
with:
command: addpath('tests/matlab'); addpath('tests/matlab/helpers'); export_parity_baseline('_ci_baseline_candidate');

- name: Check MATLAB baseline drift
# Fails if committed matlab_reference_v1.json no longer matches current MATLAB
# output, i.e. MATLAB algorithm code changed without regenerating the baseline.
# Together with the contract parity step below (Python vs committed baseline),
# this transitively verifies Python matches *current* MATLAB, not a stale snapshot.
run: |
python tests/contracts/check_baseline_drift.py \
--candidate _ci_baseline_candidate/matlab_reference_v1.json

- name: Regenerate MATLAB region-parity baseline (drift-check candidate)
uses: matlab-actions/run-command@v2
with:
command: >-
addpath('.'); addpath('tests/matlab');
generate_dce_tofts_parity_map(
'outputRoot', '_ci_matlabref_candidate/sub-10bbbdownsample',
'dynamicPath', 'tests/data/BIDS_test/rawdata/sub-10bbbdownsample/ses-01/dce/sub-10bbbdownsample_ses-01_DCE.nii',
'aifRoiPath', 'tests/data/BIDS_test/derivatives/sub-10bbbdownsample/ses-01/dce/sub-10bbbdownsample_ses-01_desc-AIFroi_mask.nii',
'brainRoiPath', 'tests/data/BIDS_test/derivatives/sub-10bbbdownsample/ses-01/anat/sub-10bbbdownsample_ses-01_desc-brain_mask.nii',
't1MapPath', 'tests/data/BIDS_test/derivatives/sub-10bbbdownsample/ses-01/anat/sub-10bbbdownsample_ses-01_space-DCEref_T1map.nii',
'noiseRoiPath', 'tests/data/BIDS_test/derivatives/sub-10bbbdownsample/ses-01/anat/sub-10bbbdownsample_ses-01_desc-noise_mask.nii',
'models', {'tofts', 'patlak'});

- name: Check MATLAB region-parity baseline drift (sub-10bbbdownsample)
# Fails if a fresh MATLAB run of generate_dce_tofts_parity_map no longer matches
# the committed derivatives/matlabref/... maps, i.e. MATLAB's own pipeline
# (steady-state auto-detect, fitting, etc.) changed without regenerating the
# fixture. Complements the contract drift guard above, which never exercises
# A_make_R1maps_func/find_end_ss.
run: |
python tests/contracts/check_matlabref_map_drift.py \
--candidate-root _ci_matlabref_candidate/sub-10bbbdownsample

- name: Generate MATLAB parity baseline maps (downsample fixture)
uses: matlab-actions/run-command@v2
with:
command: addpath .; addpath tests/matlab/; generate_dce_tofts_parity_map('subjectRoot', 'tests/data/ci_fixtures/dce/bbb_p19_downsample_x3y3', 'models', {'tofts'});
command: >-
addpath('.'); addpath('tests/matlab');
generate_dce_tofts_parity_map(
'outputRoot', 'tests/data/BIDS_test/derivatives/matlabref/sub-10bbbdownsample/ses-01/dce',
'dynamicPath', 'tests/data/BIDS_test/rawdata/sub-10bbbdownsample/ses-01/dce/sub-10bbbdownsample_ses-01_DCE.nii',
'aifRoiPath', 'tests/data/BIDS_test/derivatives/sub-10bbbdownsample/ses-01/dce/sub-10bbbdownsample_ses-01_desc-AIFroi_mask.nii',
'brainRoiPath', 'tests/data/BIDS_test/derivatives/sub-10bbbdownsample/ses-01/anat/sub-10bbbdownsample_ses-01_desc-brain_mask.nii',
't1MapPath', 'tests/data/BIDS_test/derivatives/sub-10bbbdownsample/ses-01/anat/sub-10bbbdownsample_ses-01_space-DCEref_T1map.nii',
'noiseRoiPath', 'tests/data/BIDS_test/derivatives/sub-10bbbdownsample/ses-01/anat/sub-10bbbdownsample_ses-01_desc-noise_mask.nii',
'models', {'tofts', 'patlak'});

- name: Run Python contract parity checks
run: |
python tests/contracts/generate_python_results.py --output /tmp/python_results.json
python tests/contracts/compare_with_matlab_baseline.py --python-results /tmp/python_results.json --require-all

- name: Run Python DCE pipeline parity (downsample fixture)
- name: Run Python DCE region parity (downsample fixture, gated tofts/patlak Ktrans + ROI xls)
run: |
python -m pytest tests/python/test_dce_pipeline_parity_metrics.py::test_bbb_p19_region_parity \
--dataset-root tests/data/BIDS_test \
-q

- name: Generate MATLAB T1 parity map (small VFA fixture)
uses: matlab-actions/run-command@v2
with:
command: >-
addpath('tests/matlab'); addpath('tests/matlab/helpers');
generate_t1_parity_map(
'vfaFiles', {'tests/data/BIDS_test/rawdata/sub-11tiny/ses-01/anat/sub-11tiny_ses-01_flip-01_VFA.nii.gz',
'tests/data/BIDS_test/rawdata/sub-11tiny/ses-01/anat/sub-11tiny_ses-01_flip-02_VFA.nii.gz',
'tests/data/BIDS_test/rawdata/sub-11tiny/ses-01/anat/sub-11tiny_ses-01_flip-03_VFA.nii.gz'},
'flipAngles', [2 5 10], 'trMs', 8.012, 'fitType', 't1_fa_fit',
'outputPath', 'tests/data/BIDS_test/derivatives/matlabref/sub-11tiny/ses-01/anat/sub-11tiny_ses-01_desc-t1fafit_T1map.nii',
'rsquaredThreshold', 0);

- name: Run Python T1 map parity (small VFA fixture)
run: |
python -m pytest tests/python/test_dce_pipeline_parity_metrics.py::test_downsample_bbb_p19_tofts_ktrans \
--run-parity \
--dataset-root tests/data/ci_fixtures/dce/bbb_p19_downsample_x3y3 \
python -m pytest tests/python/test_t1_map_parity.py::test_bids_t1_map_parity_nonlinear \
-q

matlab_checks:
Expand Down Expand Up @@ -152,7 +216,7 @@ jobs:
- name: Run T1 mapping
uses: matlab-actions/run-command@v2
with:
command: addpath .; addpath dce/; addpath external_programs/; addpath external_programs/niftitools/; addpath parametric_scripts/; addpath parametric_scripts/custom_scripts/; T1mapping_fit('tests/data/BIDS_test/rawdata/sub-01original/ses-01/anat/', 'tests/data/BIDS_test/derivatives/sub-01original/ses-01/anat/', 'sub-01_ses-01_space-DCEref_desc-bfczunified_VFA.nii')
command: addpath .; addpath dce/; addpath external_programs/; addpath external_programs/niftitools/; addpath parametric_scripts/; addpath parametric_scripts/custom_scripts/; T1mapping_fit('tests/data/BIDS_test/rawdata/sub-01original/ses-01/anat/', 'tests/data/BIDS_test/derivatives/sub-01original/ses-01/anat/', 'sub-01original_ses-01_space-DCEref_desc-bfczunified_VFA.nii')

- name: Run Algorithm Unit Tests
uses: matlab-actions/run-command@v2
Expand Down
9 changes: 9 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -209,3 +209,12 @@ external_programs/ModelID.m
external_programs/libCpufit.*
external_programs/libGpufit.*


# CI drift-guard: freshly regenerated baseline candidate (never committed)
_ci_baseline_candidate/

# Local runner scratch output (never committed)
RUNNER_DATA/

# Reference papers (copyrighted PDFs; cited by DOI, not committed)
tests/data/osipi/reference/*.pdf
200 changes: 192 additions & 8 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -1,16 +1,181 @@
# ROCKETSHIP Agent Guidance

## Project Direction
ROCKETSHIP is actively transitioning core workflows from MATLAB to Python.
## What this is

Canonical planning and status docs:
- `~/code/ROCKETSHIP/docs/project-management/ROADMAP.md`
- `~/code/ROCKETSHIP/docs/project-management/PORTING_STATUS.md`
- `~/code/ROCKETSHIP/docs/project-management/TODO.md`
- `~/code/ROCKETSHIP/docs/project-management/COMPLETED.md`
ROCKETSHIP is a toolbox for processing and analyzing parametric MRI and DCE-MRI
(dynamic contrast-enhanced) data. It has two parallel implementations:

- **MATLAB** (`dce/`, `parametric_scripts/`, `external_programs/`, `dsc/`) — the original,
still-maintained implementation. GUIs (`dce.m`, `fitting_gui.m`) and CLI entry points
(`run_dce.m`, `run_parametric.m`, `run_dce_cli.m`).
- **Python** (`python/`) — the actively-developed port and the recommended path for new
work. No production users yet, so prioritize correctness and clean architecture over
preserving legacy MATLAB behavior that isn't required for parity.

Because the Python port must reproduce MATLAB's numeric behavior, most non-trivial
changes touch both sides and get validated by the MATLAB-vs-Python parity test suite
(see Testing below). When MATLAB and Python disagree, prefer fixing whichever one
deviates from the *intended* algorithm rather than tuning tolerances to paper over it.

## Setup

```bash
python3 install_python_acceleration.py # creates .venv, installs deps + pyGpufit/pyCpufit
```

Manual alternative: `python3 -m venv .venv && .venv/bin/pip install -r requirements.txt`
(add `-r requirements_gui.txt` for the PySide6 GUI). MATLAB toolboxes required: Curve
Fitting, Parallel Computing, Statistics and Machine Learning, Image Processing.

## Running the pipelines

```bash
# DCE (Python), built-in tiny fixture:
.venv/bin/python run_dce_python_cli.py
# DCE with an explicit config + overrides:
.venv/bin/python run_dce_python_cli.py --config tests/python/dce_cli_config.example.json --set voxel_MaxFunEvals=100

# Parametric T1 (Python):
.venv/bin/python run_parametric_python_cli.py

# BIDS batch processing across a dataset:
.venv/bin/python run_dce_bids_batch.py --bids-root <path> --pipeline-folder dceprep --backend gpufit
.venv/bin/python run_parametric_bids_batch.py --bids-root <path> --pipeline-folder t1prep

# MATLAB (from the MATLAB command line, repo root on path):
run_dce_cli('rawdata/sub-01/ses-01/', 'derivatives/sub-01/ses-01/')
```

Full CLI reference, config precedence, and output formats: `python/README.md` and
`docs/dce_options.md`.

## Testing

```bash
# Fast Python unit/integration suite (the default; includes gated DCE parity):
.venv/bin/python -m pytest tests/python

# With coverage (matches CI's python_checks job, --cov-fail-under=60):
.venv/bin/python -m pytest tests/python -q --cov=python --cov-report=term-missing --cov-fail-under=60

# Single test:
.venv/bin/python -m pytest tests/python/test_dce_pipeline.py::TestDcePipeline::test_resolve_baseline_window_accepts_glr_alias -v

# DCE parity vs MATLAB (Tofts/Patlak Ktrans, gated on corr+RMSE over brain/GM/WM):
.venv/bin/python -m pytest tests/python -m parity
.venv/bin/python -m pytest tests/python -m parity --parity-suite=allmodels -s # + reported-only extras

# ROI-summary .xls parity (separate, default-on, few seconds):
.venv/bin/python -m pytest tests/python/test_dce_pipeline_parity_metrics.py::test_bbb_p19_roi_xls_parity

# Runtime parity vs a live MATLAB run (needs MATLAB on PATH):
.venv/bin/python -m pytest tests/python/test_runtime_parity.py --run-runtime-parity

# OSIPI reliability (ground truth vs published peer tolerances):
.venv/bin/python -m pytest tests/python -m osipi -v

# BIDS-level qualification:
.venv/bin/python -m pytest tests/python --run-qualification

# MATLAB tests (from MATLAB):
results = run_unit_tests();
results = run_all_tests('suite', 'all', 'includeIntegration', true);
```

Full test-suite docs (regions, gated-vs-reported split, thresholds, fixture regeneration
commands): `tests/README.md`. Pytest markers are declared in `pytest.ini`
(`unit`, `integration`, `parity`, `slow`, `portability`, `osipi`, `qualification`, `fast`).

CI (`.github/workflows/run_DCE.yml`) runs, per push/PR to `master`/`dev`: `python_checks`
(unit tests + coverage + OSIPI summary), `parity_checks` (MATLAB contract/baseline drift
guards + Python-vs-MATLAB dataset parity), `python_portability` (Windows/macOS, non-parity),
and `matlab_checks` (a MATLAB release × OS matrix: unit/integration tests + a full DCE CLI
run). `matlab_checks` runners have no GPU — `backend="auto"` always resolves to plain CPU
there, so accelerated-backend-only issues won't surface in CI.

## Architecture

### DCE pipeline stages (both languages implement the same A → B → D shape)

- **Stage A** — signal-to-concentration conversion from dynamic images + T1 maps + AIF/ROI
masks, and steady-state baseline window resolution. MATLAB: `A_make_R1maps_func.m`.
Python: `run_dce_pipeline`'s Stage A path in `python/dce_pipeline.py`.
- **Stage B** — AIF fitting/timing (biexponential fit or reference-region). MATLAB:
`B_AIF_fitting_func.m`. Python: same file, `_fit_aif_biexp` and friends.
- **Stage D** — per-voxel or per-ROI kinetic model fitting, producing parameter maps.
MATLAB: `D_fit_voxels_func.m` + `FXLfit_generic.m` + per-model `model_*.m`/`model_*_cfit.m`.
Python: same file, dispatching to `python/dce_models.py` (CPU/scipy) or the accelerated
path (see below).
- **Part E** (post-fit statistical comparison, f-test/AIC) is Python-only so far:
`python/dce_postfit_analysis.py`.

`python/dce_pipeline.py` is the core (~4500 lines) — nearly everything for Stage A/B/D
config resolution, metadata/sidecar discovery, and backend dispatch lives there.
`python/dce_models.py` holds the CPU/scipy model implementations (ports of the MATLAB
`model_*.m` math). `python/dce_cli.py` is a thin CLI wrapper; `python/parametric_pipeline.py`
+ `python/parametric_models.py` are the equivalent stack for VFA T1 mapping.

### Config resolution

Python config precedence (highest to lowest): CLI `--set` overrides → `stage_overrides` in
the JSON config → `dce_default.json`/`dceprep_default.json` base values → built-in
fallback defaults. Scan parameters (TR/FA/time-resolution) are resolved strictly from a
DCE metadata JSON sidecar when present; partial manual override alongside a sidecar is
rejected (all three or none — no silent per-field fallback).

The steady-state/baseline window follows its own precedence in `_resolve_baseline_window`
(`python/dce_pipeline.py`): explicit `stage_overrides.steady_state_end` → a
`SteadyStateEndTimeIndex` field in the AIF file's JSON sidecar (the documented mechanism
for a fixed/reproducible run — same discovery convention as the metadata sidecar, `.nii`/
`.nii.gz` swapped for `.json`) → auto-detect via `stage_overrides.steady_state_auto_method`
(`piecewise_constant` is the default and is the MATLAB `find_end_ss` port; `legacy_sobel`
ports the different `dce_auto_aif.m` heuristic; `glr`/`tv` are additional ported detectors).

### Backend selection (Stage D acceleration)

`backend` is `auto` | `cpu` | `gpufit`. `auto` tries `pygpufit`+CUDA, then `pycpufit`
(CPU), then falls back to the pure Python/scipy path — see `probe_acceleration_backend`/
`_resolve_backend_selection` in `dce_pipeline.py`. Accelerated models (`tofts`, `ex_tofts`,
`patlak`, `tissue_uptake`, `2cxm`) all fit through the shared `python/dce_fit_backends.py`
multi-start machinery (`FitInputs`, per-model `assemble_*_candidates`, `fit_with_multistart`),
so every backend — cpufit/gpufit or plain Python — sees the same candidate starting points
and the same bounds clamp; `tissue_uptake`/`2cxm` are fit in E-space (`E = Ktrans/Fp`) and
converted back on output. `pygpufit`/`pycpufit` are not in `requirements.txt` (installed
separately via `install_python_acceleration.py`), so CI's `auto` always resolves to pure CPU.

### Data layout and fixtures

BIDS-style `rawdata/` (raw images, scan-parameter sidecars) + `derivatives/` (masks, T1
maps, pipeline outputs) trees, discovered via `python/bids_discovery.py`. Test fixtures
live under `tests/data/BIDS_test/`, committed and lightweight (no per-run regeneration in
CI) — key subjects: `sub-10bbbdownsample` (DCE Tofts/Patlak parity), `sub-11tiny` (T1/DCE
settings matrix), `sub-0Xphantom` (synthetic ground-truth reliability, diagnostic only).
MATLAB reference maps live under `derivatives/matlabref/...`; regenerate them only when
the MATLAB algorithm actually changes (commands in `tests/README.md`), and expect to
update the committed maps in the same change — a stale committed baseline vs. a
freshly-regenerated one is a real, previously-hit failure mode
(`tests/contracts/check_matlabref_map_drift.py` guards against it in CI).

### Cross-language parity contracts

`tests/contracts/` holds the MATLAB↔Python numeric contract: `export_parity_baseline.m`
(MATLAB) writes `tests/contracts/baselines/matlab_reference_v1.json` from synthetic
curves fed straight to the model math (no imaging pipeline involved);
`generate_python_results.py` + `compare_with_matlab_baseline.py` check Python against it.
`check_baseline_drift.py` catches MATLAB algorithm drift at that (synthetic-curve) layer;
`check_matlabref_map_drift.py` catches drift at the full-pipeline NIfTI-map layer — these
are deliberately separate because the synthetic-curve contract never exercises
`A_make_R1maps_func`/`find_end_ss` (steady-state detection, AIF extraction, etc.).

## Documentation Discipline
- Keep planning docs non-overlapping. Do not update all planning docs by default.

Canonical planning and status docs:
- `docs/project-management/ROADMAP.md` — strategy/sequencing, merge-readiness criteria.
- `docs/project-management/TODO.md` — active open tasks/blockers only.
- `docs/project-management/PORTING_STATUS.md` — current measurable port state.
- `docs/project-management/COMPLETED.md` — historical completion log.

Keep planning docs non-overlapping. Do not update all planning docs by default.

Document roles:
- `ROADMAP.md`: strategy and sequencing only.
Expand All @@ -34,6 +199,25 @@ Update decision rule (apply smallest necessary set):

Do not leave important caveats only in commit messages or chat; record them in the single appropriate document above.

When you discover a problem that cannot be fixed immediately, document it before moving on:
- Write it up under `docs/project-management/projects/<initiative>/` -- integrate it into
an existing initiative folder if the problem clearly belongs to one (e.g. a Stage-D
backend divergence found while working on batch parity goes in `projects/batch-parity/`),
or create a new `projects/<slug>/` folder if it doesn't fit any existing initiative.
Include what's confirmed (root cause, evidence), what's still open, and any agreed
near-term mitigation, so the next person (or your future self) doesn't have to
re-derive it from scratch.
- Still add a one-line pointer in `TODO.md` per the update rule above, since undocumented
open work is easy to lose track of and `TODO.md` is where open work is expected to be
discoverable; keep the actual detail in the project folder, not duplicated in `TODO.md`.

When asked to plan or estimate a new initiative, also write it up under `docs/project-management/projects/<initiative>/`. Upon completion, move the initiative folder to `docs/project-management/projects/archived/` and add a one-line pointer in `COMPLETED.md` per the update rule above.

Other reference docs:
- `docs/dce_options.md` — full `stage_overrides` field reference (shared by CLI + GUI).
- `python/README.md` — Python usage guide (CLIs, batch processing, GUI, output formats).
- `tests/README.md` — full test-suite reference.

OSIPI reference repos available locally for verification work:
- `~/code/DCE-DSC-MRI_CodeCollection`
- `~/code/DCE-DSC-MRI_TestResults`
Expand Down
7 changes: 7 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

All project guidance — what this is, setup, running the pipelines, testing, architecture,
documentation discipline, engineering priorities, and porting focus — lives in `AGENTS.md`.
Read it before doing any work here; it is the authoritative doc, not this file.
Loading
Loading