Architecture Overview¶
High-level overview of osipy's architecture, design principles, and code organization.
Design Philosophy¶
osipy is built on four core principles:
- GPU/CPU Agnostic: All numerical code works on both
- OSIPI Compliant: Standard parameter names and validation
- Modular Design: Independent, composable components
- Extensible: Supports adding new methods
System Architecture¶
graph TD
UI["<b>Public API</b><br/>fit_model(), quantify_cbf(), run_analysis()"]
PL["<b>Pipeline Layer</b><br/>DCEPipeline · DSCPipeline · ASLPipeline · IVIMPipeline"]
DCE["<b>DCE</b><br/>T1 Mapping · Signal-to-Conc<br/>PK Models · Fitting"]
DSC["<b>DSC</b><br/>Signal-to-DR2 · SVD Deconv<br/>Leakage · Perfusion Maps"]
ASL["<b>ASL</b><br/>M0 Calibration<br/>CBF Quantification"]
IVIM["<b>IVIM</b><br/>Bi-exponential<br/>Segmented / Bayesian"]
FIT["Fitting<br/>(LM Optimizer)"]
AIF["AIF<br/>(Population, Detection)"]
CONV["Convolution<br/>(Piecewise, Exp, FFT)"]
MOD["Base Models"]
IO["I/O<br/>(NIfTI, DICOM, BIDS)"]
VAL["Validation<br/>(DRO)"]
VIZ["Visualization<br/>(matplotlib)"]
INFRA["ParameterMap · Exceptions · Types · Dataset"]
BE["<b>Backend Layer</b><br/>xp = get_array_module() → numpy / cupy"]
UI --> PL
PL --> DCE & DSC & ASL & IVIM
PL -.-> IO & VAL & VIZ
DCE & DSC & ASL & IVIM --> FIT
DCE & DSC --> AIF
DCE --> CONV
DCE & DSC & ASL & IVIM --> MOD
DCE & DSC & ASL & IVIM --> INFRA
INFRA --> BE
FIT & AIF & CONV & MOD --> BE
IO --> BE
subgraph Modality Modules
DCE
DSC
ASL
IVIM
end
subgraph "Common Layer (Core)"
FIT
AIF
CONV
MOD
end
subgraph "Common Layer (Shared Infrastructure)"
IO
VAL
VIZ
INFRA
end
Not every modality depends on every common submodule. The diagram above shows the actual dependency edges:
- All four modalities use the shared Fitting engine (
LevenbergMarquardtFitter) via theFittableModelprotocol, plus Backend (array module), ParameterMap, Exceptions, and Types. Each modality wraps its signal model in a binding adapter (BoundDCEModel,BoundIVIMModel,BoundASLModel,BoundDSCModel,BoundGammaVariateModel) that fixes independent variables so the fitter only sees free parameters. - All four modalities share the
BaseSignalModelbase class from common/models (BasePerfusionModelfor DCE,IVIMModelfor IVIM,BaseASLModelfor ASL,DSCConvolutionModelfor DSC). - DCE and DSC use AIF utilities (population AIFs and automatic detection). DCE additionally uses Convolution functions (piecewise-linear, exponential, FFT).
- I/O, Validation, and Visualization are used at the pipeline level and by end users, not by the modality internals directly.
Module Structure¶
Overview¶
osipy/
├── __init__.py # Public API exports
├── _version.py # Version management
│
├── common/ # Shared infrastructure
│ ├── backend/ # Array module abstraction (CPU/GPU)
│ ├── aif/ # Arterial input functions (DCE)
│ ├── convolution/ # AIF convolution methods (DCE)
│ ├── fitting/ # Model fitting algorithms (all modalities)
│ ├── io/ # File I/O (NIfTI, DICOM, BIDS)
│ ├── models/ # Base model classes + FittableModel protocol
│ ├── signal/ # Signal processing
│ ├── validation/ # DRO comparison
│ ├── visualization/ # Plotting utilities
│ ├── dataset.py # PerfusionDataset container
│ ├── parameter_map.py # ParameterMap container
│ ├── types.py # Enums and type definitions
│ ├── exceptions.py # Custom exception hierarchy
│ ├── caching.py # Result caching utilities
│ └── logging.py # Logging configuration
│
├── dce/ # DCE-MRI analysis
│ ├── t1_mapping/ # T1 estimation methods
│ ├── concentration/ # Signal → concentration
│ └── models/ # PK models (Tofts, etc.)
│
├── dsc/ # DSC-MRI analysis
│ ├── concentration/ # Signal → ΔR2*
│ ├── deconvolution/ # SVD methods + signal model + SVD fitters
│ ├── leakage/ # BSW + bidirectional leakage correction
│ ├── parameters/ # CBV, CBF, MTT
│ ├── arrival/ # Bolus arrival detection
│ └── normalization.py # Signal normalization
│
├── asl/ # ASL analysis
│ ├── labeling/ # Labeling schemes
│ ├── calibration/ # M0 calibration
│ └── quantification/ # CBF calculation
│
├── ivim/ # IVIM analysis
│ ├── models/ # Bi-exponential model
│ └── fitting/ # Segmented/full fitting
│
├── pipeline/ # End-to-end workflows
│ ├── runner.py # Unified entry point
│ └── *_pipeline.py # Modality-specific pipelines
│
└── cli/ # Command-line interface
├── main.py # argparse CLI (osipy)
├── config.py # Pydantic v2 config models
├── runner.py # Pipeline orchestration from config
└── wizard.py # Interactive configuration wizard
Backend Layer¶
The backend layer provides GPU/CPU abstraction:
Backend layer implementation
Key exports:
get_array_module()- Returns appropriate array libraryto_numpy()- Convert to numpy (for I/O)to_gpu()- Convert to cupy (for acceleration)is_gpu_available()- Check GPU support
Common Layer¶
Shared infrastructure with varying usage across modalities. Backend, ParameterMap, Exceptions, Base Models, and Fitting are used by all four modalities. AIF is used by DCE and DSC. Convolution is specific to DCE:
I/O (common/io/)¶
Used at the pipeline level and by end users for data loading and export. The modality modules themselves do not import I/O directly — they operate on in-memory arrays.
I/O function signatures
# NIfTI loading
def load_nifti(path, time_points=None):
"""Load NIfTI file into PerfusionDataset."""
# DICOM loading
def load_dicom(path, series_uid=None):
"""Load DICOM series into PerfusionDataset."""
# BIDS export
def export_bids(output_dir, subject, parameters, ...):
"""Export results in BIDS derivatives format."""
Fitting (common/fitting/)¶
The shared LevenbergMarquardtFitter is a generic, GPU-accelerated non-linear optimizer used by all four modalities. It works with any model implementing the FittableModel protocol (predict_array_batch(), get_bounds(), parameters). Each modality wraps its signal model in a binding adapter that fixes independent variables (time+AIF for DCE, b-values for IVIM, PLDs for ASL, time for DSC gamma-variate), so the fitter only sees free parameters.
Levenberg-Marquardt fitter interface
# Levenberg-Marquardt optimizer (registered as "lm")
class LevenbergMarquardtFitter(BaseFitter):
"""GPU-accelerated optimizer for per-voxel fitting."""
def fit_batch(self, model, observed_batch, ...):
"""Fit model to a batch of voxels."""
def fit_image(self, model, data_4d, mask=None, ...):
"""Fit model to all voxels (handles chunking, GPU)."""
The FittableModel protocol and BaseBoundModel base class (common/models/fittable.py) handle the binding pattern. BaseBoundModel provides shared fixed-parameter logic — any parameter can be pinned to a constant value, removing it from the optimization.
Binding adapters per modality:
| Adapter | Module | Fixes |
|---|---|---|
BoundDCEModel |
dce/models/binding.py |
time, AIF |
BoundIVIMModel |
ivim/models/binding.py |
b-values (+ analytical Jacobian) |
BoundASLModel |
asl/quantification/binding.py |
PLDs, labeling params |
BoundGammaVariateModel |
dsc/concentration/gamma_model.py |
time |
BoundDSCModel |
dsc/deconvolution/signal_model.py |
AIF, time (pre-computes SVD) |
AIF (common/aif/)¶
Used by DCE for arterial input function selection and detection. Not used by DSC, ASL, or IVIM.
Population AIF classes and automatic detection
# Population AIFs (all extend BaseAIF)
class ParkerAIF(BaseAIF): ...
class GeorgiouAIF(BaseAIF): ...
class FritzHansenAIF(BaseAIF): ...
class WeinmannAIF(BaseAIF): ...
class McGrathAIF(BaseAIF): ... # Preclinical
# Automatic detection
def detect_aif(dataset, params=None, roi_mask=None, method=None):
"""Automatically detect AIF from data."""
# Arterial delay
def shift_aif(aif, time, delay):
"""Shift AIF by a delay (seconds) for delay fitting."""
Convolution (common/convolution/)¶
Used by DCE pharmacokinetic models for convolving the AIF with tissue response functions. Not used by DSC, ASL, or IVIM.
Convolution functions
# Piecewise-linear convolution (registered as "piecewise_linear")
def convolve_aif(aif, time, kernel_func):
"""Convolve AIF with a model kernel."""
# Recursive exponential convolution (registered as "exponential")
def expconv(time_constant, time, input_function):
"""Fast exponential convolution for compartment models."""
# FFT-based convolution (registered as "fft")
def fft_convolve(signal_a, signal_b, dt):
"""Frequency-domain convolution."""
Modality Modules¶
All four modalities use the shared LevenbergMarquardtFitter via binding adapters, plus the Backend abstraction, ParameterMap output, and exception types. DCE and DSC additionally use AIF from common. DCE additionally uses Convolution from common.
DCE Module¶
The most coupled modality — uses shared Fitting (LM optimizer), AIF (population models and detection), and Convolution (piecewise-linear, exponential, FFT) from common.
DCE module key functions and classes
# osipy/dce/
# T1 mapping
def compute_t1_map(dataset, method="vfa"):
"""Compute T1 map from VFA or Look-Locker data."""
# Signal conversion
def signal_to_concentration(signal, t1_map, acquisition_params, t1_blood=1440.0, method="spgr"):
"""Convert DCE signal to Gd concentration."""
# Models
class ToftsModel(BasePerfusionModel):
"""Standard Tofts pharmacokinetic model."""
class ExtendedToftsModel(BasePerfusionModel):
"""Extended Tofts with plasma term."""
# Fitting
def fit_model(model_name, concentration, aif, time):
"""Fit a pharmacokinetic model to concentration data."""
DSC Module¶
DSC deconvolution is implemented via two paths:
- Signal model path (preferred):
DSCConvolutionModel(BaseSignalModel)defines the forward model C(t) = CBF * AIF ⊛ R(t).BoundDSCModel(BaseBoundModel)fixes AIF and time, pre-computes SVD components. SVD fitter classes (SSVDFitter,CSVDFitter,OSVDFitter,TikhonovFitter) inherit fromBaseFitterand use the shared fitting infrastructure. - Legacy deconvolver path:
BaseDeconvolverABC withSSVDDeconvolver,CSVDDeconvolver,OSVDDeconvolveraccessed viaget_deconvolver(). Retained for backward compatibility.
Gamma-variate fitting for recirculation removal uses the shared LevenbergMarquardtFitter via BoundGammaVariateModel. Leakage correction provides BSW and bidirectional correctors. Bolus arrival detection is also registry-driven.
DSC module key functions
# osipy/dsc/
# Signal conversion
def signal_to_delta_r2(signal, te, baseline_end):
"""Convert DSC signal to ΔR2*."""
# Deconvolution (legacy path)
def get_deconvolver(method):
"""Get a deconvolver by name (e.g., 'oSVD', 'cSVD')."""
# Deconvolution (signal model path)
class DSCConvolutionModel(BaseSignalModel):
"""Forward model C(t) = CBF * AIF ⊛ R(t)."""
# Leakage correction
def correct_leakage(signal, delta_r2, method='bsw'):
"""BSW or bidirectional leakage correction."""
# Parameters
def compute_perfusion_maps(concentration, aif, time):
"""Calculate CBV, CBF, MTT, TTP, Tmax."""
Pipeline Layer¶
End-to-end workflows that orchestrate the modality modules:
Unified pipeline runner entry point
# osipy/pipeline/runner.py
def run_analysis(data, modality, **kwargs):
"""Unified analysis entry point.
Automatically selects appropriate pipeline
based on modality.
"""
pipelines = {
'dce': DCEPipeline,
'dsc': DSCPipeline,
'asl': ASLPipeline,
'ivim': IVIMPipeline,
}
pipeline = pipelines[modality](**kwargs)
return pipeline.run()
Data Flow¶
Default pipeline paths. Each modality supports additional options (AIF sources, fitting methods, etc.) — see the how-to guides for alternatives.
DCE-MRI¶
graph LR
VFA["VFA Images"] --> T1["compute_t1_map()"]
T1 --> STC["signal_to_concentration()"]
DCE["DCE Signal"] --> STC
STC --> FIT["fit_model('extended_tofts')"]
AIF["ParkerAIF"] --> FIT
FIT --> PM["Ktrans · ve · vp"]
DSC-MRI¶
graph LR
DSC["DSC Signal"] --> S2R["signal_to_delta_r2()"]
S2R --> LC["correct_leakage()"]
LC --> PERF["compute_perfusion_maps()<br/>oSVD deconvolution"]
AIF["AIF"] --> PERF
PERF --> PM["CBV · CBF · MTT · Tmax"]
ASL¶
graph LR
CL["Control / Label"] --> DIFF["Control − Label"]
DIFF --> CAL["apply_m0_calibration()"]
M0["M0 Image"] --> CAL
CAL --> Q["quantify_cbf()<br/>pCASL model"]
Q --> PM["CBF"]
IVIM¶
graph LR
DWI["DWI Signal"] --> FIT["fit_ivim()<br/>segmented"]
BV["b-values"] --> FIT
FIT --> PM["D · D* · f"]
Key Data Structures¶
Core data container dataclasses
# PerfusionDataset: Input data container
@dataclass
class PerfusionDataset:
data: NDArray # 3D or 4D image data
affine: NDArray # Spatial transformation
time_points: NDArray # Time array (for 4D)
modality: Modality # DCE, DSC, ASL, IVIM
acquisition_params: AcquisitionParams # Typed dataclass (e.g., DCEAcquisitionParams)
# ParameterMap: Output parameter container
@dataclass
class ParameterMap:
values: NDArray # 3D parameter values
name: str # CAPLEX name (e.g., "Ktrans")
units: str # Standard units
quality_mask: NDArray # Valid voxel mask
provenance: dict # Analysis metadata
Class Hierarchy¶
All registered components inherit from BaseComponent(ABC), which provides name and reference properties. The full hierarchy:
BaseComponent (osipy.common.models.base) — name + reference
├── BaseSignalModel — + parameters, parameter_units, get_bounds()
│ ├── BasePerfusionModel[P] (dce/models/base) — DCE pharmacokinetic models
│ │ ├── ToftsModel, ExtendedToftsModel, PatlakModel
│ │ ├── TwoCompartmentModel (2CXM)
│ │ └── TwoCompartmentUptakeModel (2CUM)
│ ├── IVIMModel (ivim/models/biexponential) — IVIM signal models
│ │ ├── IVIMBiexponentialModel
│ │ └── IVIMSimplifiedModel
│ ├── BaseASLModel (asl/quantification/base) — ASL signal models
│ │ ├── PCASLSinglePLDModel, PASLSinglePLDModel, CASLSinglePLDModel
│ │ └── BuxtonMultiPLDModel
│ └── DSCConvolutionModel (dsc/deconvolution/signal_model) — DSC forward model
├── BaseFitter (common/fitting/base) — + fit_batch(), fit_image()
│ ├── LevenbergMarquardtFitter — iterative parametric (all modalities)
│ ├── BayesianFitter — MAP estimation
│ ├── SSVDFitter — standard SVD truncation
│ ├── CSVDFitter — circular SVD truncation
│ ├── OSVDFitter — oscillation-index SVD
│ └── TikhonovFitter — Tikhonov regularization
├── BaseLeakageCorrector — DSC leakage correction
│ ├── BSWCorrector — Boxerman-Schmainda-Weisskoff
│ └── BidirectionalCorrector
├── BaseM0Calibration — ASL M0 calibration
│ ├── SingleM0Calibration, VoxelwiseM0Calibration, ReferenceRegionM0Calibration
├── BaseArrivalDetector — DSC bolus arrival detection
│ └── ResiduePeakDetector
├── BaseAIFDetector — AIF detection from image data
│ └── MultiCriteriaAIFDetector
├── BaseAIF — population AIF models
│ ├── ParkerAIF, GeorgiouAIF, FritzHansenAIF, WeinmannAIF, McGrathAIF
└── BaseDeconvolver — DSC deconvolution (legacy interface)
├── StandardSVDDeconvolver, CircularSVDDeconvolver, OscillationSVDDeconvolver
FittableModel protocol (common/models/fittable)
└── BaseBoundModel — shared fixed-param logic
├── BoundDCEModel — fixes time + AIF
├── BoundSPGRModel — fixes flip angles + TR (VFA T1 mapping)
├── BoundLookLockerModel — fixes inversion times (Look-Locker T1 mapping)
├── BoundIVIMModel — fixes b-values, analytical Jacobian
├── BoundASLModel — fixes PLDs + labeling params
├── BoundDSCModel — fixes AIF + time, pre-computes SVD
└── BoundGammaVariateModel — fixes time
Extension Points¶
All extension points use the registry pattern — one file, one decorator. 17+ registries span all modalities:
| Extension | Decorator | Lookup | List |
|---|---|---|---|
| DCE PK model | @register_model("name") |
get_model("name") |
list_models() |
| IVIM signal model | @register_ivim_model("name") |
get_ivim_model("name") |
list_ivim_models() |
| DSC deconvolver | @register_deconvolver("name") |
get_deconvolver("name") |
list_deconvolvers() |
| DSC leakage corrector | @register_leakage_corrector("name") |
get_leakage_corrector("name") |
list_leakage_correctors() |
| DSC normalizer | @register_normalizer("name") |
get_normalizer("name") |
list_normalizers() |
| DSC arrival detector | @register_arrival_detector("name") |
get_arrival_detector("name") |
list_arrival_detectors() |
| ASL quantification model | @register_quantification_model("name") |
get_quantification_model("name") |
list_quantification_models() |
| ASL ATT model | @register_att_model("name") |
get_att_model("name") |
list_att_models() |
| ASL difference method | @register_difference_method("name") |
get_difference_method("name") |
list_difference_methods() |
| M0 calibration method | @register_m0_calibration("name") |
get_m0_calibration("name") |
list_m0_calibrations() |
| Population AIF | @register_aif("name") |
get_population_aif("name") |
list_aifs() |
| AIF detector | @register_aif_detector("name") |
get_aif_detector("name") |
list_aif_detectors() |
| Fitter | @register_fitter("name") |
get_fitter("name") |
list_fitters() |
| T1 mapping method | @register_t1_method("name") |
get_t1_method("name") |
list_t1_methods() |
| Concentration model | @register_concentration_model("name") |
get_concentration_model("name") |
list_concentration_models() |
| IVIM fitting strategy | @register_ivim_fitter("name") |
get_ivim_fitter("name") |
list_ivim_fitters() |
| Convolution method | @register_convolution("name") |
get_convolution("name") |
list_convolutions() |
All registries use DataValidationError for unknown names and logging.getLogger(__name__) with warnings for overwrites.
Adding a New Model¶
- Create class inheriting from
BasePerfusionModel - Implement
_predict()using xp pattern - Decorate with
@register_model("name")
Adding a New AIF¶
- Create class inheriting from
BaseAIF - Decorate with
@register_aif("name")
Adding a New Pipeline¶
- Create
NewPipelineclass - Implement required steps
- Add dispatch in
runner.py
Testing Architecture¶
tests/
├── unit/ # Unit tests (isolated)
│ ├── common/ # Common module tests
│ ├── dce/ # DCE module tests
│ ├── dsc/ # DSC module tests
│ ├── asl/ # ASL module tests
│ └── ivim/ # IVIM module tests
│
├── integration/ # End-to-end tests
│ └── test_pipeline.py
│
└── conftest.py # Shared fixtures (synthetic DROs)
Fixture Strategy¶
Test fixtures for synthetic data and dual backends
# conftest.py
@pytest.fixture
def dce_dro():
"""Synthetic DCE-MRI data with known ground truth."""
return generate_dce_synthetic(
shape=(32, 32, 8, 50),
ktrans_true=0.1,
ve_true=0.2,
vp_true=0.02
)
@pytest.fixture(params=['numpy', 'cupy'])
def xp(request):
"""Parametrize tests for both backends."""
if request.param == 'cupy':
pytest.importorskip('cupy')
return request.param
Performance Considerations¶
Memory Management¶
Chunked processing for large datasets
# Large dataset handling
def fit_large_dataset(data, chunk_size=10000):
"""Process in memory-efficient chunks."""
n_voxels = data.reshape(-1, data.shape[-1]).shape[0]
for start in range(0, n_voxels, chunk_size):
chunk = data_chunk(start, chunk_size)
result_chunk = fit(chunk)
store_result(result_chunk, start)
GPU Memory¶
Monitor and free GPU memory
Dependencies¶
Core Dependencies¶
| Package | Purpose | Required |
|---|---|---|
| numpy | Array operations | Yes |
| nibabel | NIfTI I/O | Yes |
| pydicom | DICOM I/O | Yes |
| pybids | BIDS support | Yes |
| matplotlib | Visualization | Yes |
Optional Dependencies¶
| Package | Purpose | Required |
|---|---|---|
| cupy | GPU acceleration | No |
Development Dependencies¶
| Package | Purpose |
|---|---|
| pytest | Testing |
| ruff | Linting |
| mypy | Type checking |
| mkdocs | Documentation |