How to Add a Custom DCE Pharmacokinetic Model¶
Add a new pharmacokinetic model to osipy in a single file using the @register_model decorator.
Steps¶
1. Create your model file¶
Create a new file in osipy/dce/models/, e.g., osipy/dce/models/my_model.py:
Define a custom pharmacokinetic model
"""My custom pharmacokinetic model."""
from dataclasses import dataclass
from typing import TYPE_CHECKING, Any
import numpy as np
from osipy.common.convolution import convolve_aif
from osipy.dce.models.base import BasePerfusionModel, ModelParameters
from osipy.dce.models.registry import register_model
if TYPE_CHECKING:
from numpy.typing import NDArray
@dataclass
class MyModelParams(ModelParameters):
"""Parameters for my custom model."""
ktrans: float = 0.1
ve: float = 0.2
@register_model("my_model")
class MyModel(BasePerfusionModel[MyModelParams]):
"""My custom pharmacokinetic model."""
@property
def name(self) -> str:
return "My Custom Model"
@property
def parameters(self) -> list[str]:
return ["Ktrans", "ve"]
@property
def parameter_units(self) -> dict[str, str]:
return {"Ktrans": "1/min", "ve": "mL/100mL"}
@property
def reference(self) -> str:
return "Author et al. (2025). Journal Name."
@property
def time_unit(self) -> str:
return "minutes"
def _predict(self, t, aif, params, xp):
ktrans = params[0] # scalar OR (n_voxels,) — broadcasting handles both
ve = params[1]
t_min = self._convert_time(t, xp)
ve_safe = xp.where(ve > 0, ve, xp.asarray(1e-10))
kep = ktrans / ve_safe
dt = float(t_min.ravel()[1] - t_min.ravel()[0]) if t_min.size > 1 else 1.0
irf = ktrans * xp.exp(-kep * t_min)
return convolve_aif(aif, irf, dt=dt)
def get_bounds(self):
return {"Ktrans": (0.0, 5.0), "ve": (0.001, 1.0)}
def get_initial_guess(self, ct, aif, t):
return MyModelParams(ktrans=0.1, ve=0.2)
The key method is _predict(self, t, aif, params, xp):
paramsis an array:params[0]is Ktrans,params[1]is ve (matchingself.parametersorder)xpis the array module (numpy or cupy) — use it for all math- Works for single voxels and batches automatically — when
params[i]is a scalar you get a single curve; when it's(n_voxels,)you get(n_time, n_voxels)output via broadcasting - Use
xp.where()instead ofif,xp.abs()instead ofabs() - Use
t.ravel()when extractingdtsincetmay be(n_time, 1)in batch mode
You do not need to implement predict() or predict_batch() — the base class handles parameter conversion and shape setup.
2. Use it¶
That's it. Your model is now available everywhere:
Use the registered model via fit_model()
from osipy.dce.fitting import fit_model
from osipy.dce.models import get_model, list_models
# Import your model file to trigger registration
import osipy.dce.models.my_model
# Verify it's registered
print(list_models()) # [..., 'my_model', ...]
# Use via fit_model()
result = fit_model("my_model", concentration, aif, time)
# Or get the model instance directly
model = get_model("my_model")
ct = model.predict(time, aif, {"Ktrans": 0.1, "ve": 0.2})
The DCE pipeline also works automatically with any registered model:
Use custom model in DCE pipeline
What you get for free¶
- Registry-driven lookup via
get_model("my_model") - Unified fitting via
fit_model("my_model", ...) - Pipeline integration (no code changes needed)
- GPU acceleration if you use
xpoperations in_predict() - Batch processing — base class
predict_batch()calls your_predict()with broadcasting - Quality masks, R² maps, and fitting statistics