Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -70,4 +70,7 @@ outputs/
output/
test_runs/
.claude/
site/
site/

# Local-only pipeline verification helper (machine-specific data paths)
scripts/verify_local_pipelines.sh
25 changes: 0 additions & 25 deletions asl_config.yaml

This file was deleted.

21 changes: 11 additions & 10 deletions docs/explanation/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -251,17 +251,13 @@ Used by DCE pharmacokinetic models for convolving the AIF with tissue response f
!!! example "Convolution functions"

```python
# Piecewise-linear convolution (registered as "piecewise_linear")
def convolve_aif(aif, time, kernel_func):
"""Convolve AIF with a model kernel."""
# Discrete AIF convolution with a tissue residue/response function
def convolve_aif(aif, residue, dt):
"""Convolve the AIF with a model kernel."""

# Recursive exponential convolution (registered as "exponential")
# Recursive exponential convolution (Flouri et al. 2016)
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
Expand All @@ -270,7 +266,7 @@ All four modalities use the shared `LevenbergMarquardtFitter` via binding adapte

#### DCE Module

The most coupled modality — uses shared Fitting (LM optimizer), AIF (population models and detection), and Convolution (piecewise-linear, exponential, FFT) from common.
The most coupled modality — uses shared Fitting (LM optimizer), AIF (population models and detection), and Convolution (`convolve_aif`, `expconv`) from common.

!!! example "DCE module key functions and classes"

Expand Down Expand Up @@ -500,10 +496,15 @@ All extension points use the registry pattern — one file, one decorator. 17+ r
| 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.

Each selectable component also declares a pydantic `MethodConfig`, so the CLI
config, interactive wizard, and `--dump-defaults` templates are generated
directly from `registry × schema`. See
[Registry-Driven Configuration](configuration.md) for how a single decorator
plus a config model turns a component into a validated, wired CLI toggle.

### Adding a New Model

1. Create class inheriting from `BasePerfusionModel`
Expand Down
89 changes: 89 additions & 0 deletions docs/explanation/configuration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Registry-Driven Configuration

osipy's YAML/CLI configuration is **generated from the component registries**.
Each pipeline-component selection — the DCE pharmacokinetic model, the DSC
deconvolution method, the ASL M0 calibration, the IVIM fitting strategy, and so
on — is a nested, validated block whose options and parameters come directly
from what is registered.

## How it works

### Each component declares a `MethodConfig`

Every selectable component ships a small pydantic model
(`osipy.common.config.MethodConfig`) with a discriminator field — a `Literal`
equal to the component's registry name — plus that component's tunable knobs:

```python
class OSVDConfig(MethodConfig):
method: Literal["oSVD"] = "oSVD"
oscillation_index: float = Field(0.035, gt=0.0)
default_threshold: float = Field(0.2, gt=0.0, lt=1.0)
```

`MethodConfig` sets `extra="forbid"`, so a typo or a knob belonging to a
different method raises a validation error instead of being silently ignored.

### The discriminator selects the parameters you see

In YAML you pick a component by its discriminator, and only that component's
knobs are valid:

```yaml
deconvolution:
method: oSVD # oSVD | sSVD | cSVD
oscillation_index: 0.035
default_threshold: 0.2
```

Switch the method and the surfaced knobs change with it (`sSVD`/`cSVD` expose a
single `threshold`; `oSVD` exposes `oscillation_index` and `default_threshold`).
The discriminator is `method` for most components, `mode` for the ASL
quantification block, `model` for the IVIM signal model, and `name` for the
population AIF.

### The config is generated from `registry × schema`

The per-component models are composed into discriminated unions
(`method_union()`) that form the modality config models. The `--dump-defaults`
templates and the `--help-me-pls` wizard are produced from these same models —
including the `# A | B | C` option comments, which are derived from the union
members (the registry keys), not hand-written.

### The same schema validates *and* builds the component

On load, the discriminator picks the registry entry and the remaining fields
become its constructor arguments (`construct_from_config()`):

```python
deconvolver = construct_from_config(DECONVOLVER_REGISTRY, cfg) # cfg.method -> instance
```

Validation and construction share one schema, so every accepted knob is
guaranteed to reach the live component.

### Adding a method

1. Register the component, e.g. `@register_deconvolver("mymethod")` (see
[Extension Points](architecture.md#extension-points)).
2. Give it a `MethodConfig` subclass and add it to the modality's `*_CONFIGS`
mapping.

It then appears automatically as a selectable option in the config,
`--dump-defaults`, and the wizard — no hand-editing of the schema, runner, or
wizard.

## Per-modality shape

| Modality | Nested component blocks |
|----------|-------------------------|
| DCE | `model.method`, `t1_mapping_method.method` (+ `fit_method`), `concentration.method`, `population_aif.name` |
| DSC | `deconvolution.method` (+ method-specific thresholds) |
| ASL | `m0.method`, `difference.method`, `quantification.mode` (single-PLD or multi-PLD + ATT) |
| IVIM | `fitting.method` (segmented / full / bayesian), `model.model` (biexponential / simplified) |

Non-method-specific parameters (ASL labeling timing, DSC echo time, IVIM
`normalize_signal`, …) stay as flat keys in the `pipeline` block. Generate an
authoritative template with `osipy --dump-defaults dce` (or `dsc`/`asl`/`ivim`);
see [How to Run a Pipeline from YAML](../how-to/run-pipeline-cli.md) for runnable
examples.
2 changes: 2 additions & 0 deletions docs/explanation/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
## Software Architecture

- [Architecture Overview](architecture.md) — Module structure, data flow, and registry-driven extensibility.
- [Registry-Driven Configuration](configuration.md) — How the CLI/YAML config is generated from the registries via `MethodConfig` schemas.
- [The xp Abstraction Pattern](xp-abstraction.md) — GPU/CPU agnostic code using `xp = get_array_module()`.
- [OSIPI Standards](osipi-standards.md) — CAPLEX naming, units, and validation against DROs.

Expand All @@ -18,6 +19,7 @@
| If you want to understand... | Read |
|------------------------------|------|
| How the code is organized | [Architecture Overview](architecture.md) |
| Why the YAML config is shaped the way it is | [Registry-Driven Configuration](configuration.md) |
| How GPU acceleration works | [xp Abstraction Pattern](xp-abstraction.md) |
| What OSIPI standards mean | [OSIPI Standards](osipi-standards.md) |
| How DCE models work mathematically | [Pharmacokinetic Models](pharmacokinetic-models.md) |
Expand Down
Loading
Loading