Skip to content

How to Use a Custom AIF

Provide your own measured arterial input function for DCE or DSC analysis.

Via CLI (YAML Config)

Point to your AIF file in the config:

modality: dce
pipeline:
  model: extended_tofts
  aif_source: file
data:
  aif_file: /path/to/aif.npy  # NumPy array with shape (n_timepoints,)

The AIF file should contain concentration values in mM, sampled at the same time points as your data.

Via Python API

Create AIF from Measured Data

Create an ArterialInputFunction from your measured values:

import numpy as np
import osipy
from osipy.common.types import AIFType

# Your measured AIF data
time = np.array([0, 5, 10, 15, 20, 25, 30, 35, 40, 45])  # seconds
concentration = np.array([0, 0.1, 2.5, 5.0, 3.2, 1.8, 1.0, 0.6, 0.4, 0.3])  # mM

# Create AIF object
aif = osipy.ArterialInputFunction(
    time=time,
    concentration=concentration,
    aif_type=AIFType.MEASURED,
)

print(f"AIF peak: {aif.peak_concentration:.2f} mM at {aif.peak_time:.1f} s")

Extract AIF from DCE Data

Select arterial voxels and create an AIF:

import numpy as np
import osipy
from osipy.common.types import AIFType

# Load DCE concentration data
concentration = np.load("concentration_4d.npy")  # shape: (x, y, z, time)
time = np.linspace(0, 300, concentration.shape[-1])

# Method 1: Manual ROI selection
# Define arterial voxel coordinates (e.g., from ROI drawing)
aif_coords = [(32, 45, 12), (33, 45, 12), (32, 46, 12)]

# Extract and average
aif_curves = [concentration[x, y, z, :] for x, y, z in aif_coords]
aif_mean = np.mean(aif_curves, axis=0)

# Create AIF
aif = osipy.ArterialInputFunction(time=time, concentration=aif_mean, aif_type=AIFType.MEASURED)

Automatic AIF Detection

Use osipy's automatic detection:

import osipy

# Automatic AIF detection for DCE
# detect_aif takes a PerfusionDataset, not raw arrays
aif_result = osipy.detect_aif(
    dataset=dce_dataset,         # PerfusionDataset from load_nifti()
    roi_mask=brain_mask,
    method="cluster",            # or "threshold"
)

# Access the detected AIF (AIFDetectionResult object)
aif = aif_result.aif             # ArterialInputFunction object
aif_mask = aif_result.voxel_mask # Which voxels were selected

print(f"Selected {aif_mask.sum()} arterial voxels")

Interpolate AIF to Match Data

When AIF time points don't match your data:

import numpy as np
import osipy
from osipy.common.types import AIFType

# Original AIF at different time points
aif_time = np.array([0, 10, 20, 30, 40])
aif_conc = np.array([0, 3.0, 5.0, 2.0, 1.0])

# Your data time points
data_time = np.linspace(0, 40, 100)

# Interpolate using numpy
aif_interp = np.interp(data_time, aif_time, aif_conc)

# Create interpolated AIF
aif = osipy.ArterialInputFunction(time=data_time, concentration=aif_interp, aif_type=AIFType.MEASURED)

AIF Quality Checks

Verify your AIF is suitable:

import numpy as np

def check_aif_quality(aif):
    """Check AIF quality metrics."""
    time = aif.time
    conc = aif.concentration

    # Check for baseline before bolus
    baseline = conc[:5].mean()
    if baseline > 0.1:
        print("Warning: High baseline concentration")

    # Check peak is well-defined
    peak_idx = np.argmax(conc)
    peak_time = time[peak_idx]
    peak_conc = conc[peak_idx]

    if peak_idx < 3:
        print("Warning: Peak too early, may miss bolus arrival")
    if peak_idx > len(conc) - 5:
        print("Warning: Peak too late, may be truncated")

    # Check signal-to-noise
    tail_std = conc[-10:].std()
    snr = peak_conc / tail_std if tail_std > 0 else np.inf
    if snr < 10:
        print(f"Warning: Low SNR ({snr:.1f})")

    print(f"Peak: {peak_conc:.2f} mM at {peak_time:.1f} s")
    print(f"Estimated SNR: {snr:.1f}")

check_aif_quality(aif)

AIF Correction

Apply partial volume or dispersion correction:

from osipy.common.types import AIFType

def correct_aif_partial_volume(aif, pv_fraction):
    """Correct AIF for partial volume effects.

    Parameters
    ----------
    aif : ArterialInputFunction
        Original AIF
    pv_fraction : float
        Estimated arterial volume fraction (0-1)
    """
    corrected_conc = aif.concentration / pv_fraction
    return osipy.ArterialInputFunction(
        time=aif.time,
        concentration=corrected_conc,
        aif_type=AIFType.MEASURED,
    )

# Apply 30% partial volume correction
aif_corrected = correct_aif_partial_volume(aif, pv_fraction=0.3)

Use AIF in Fitting

Pass the AIF to fitting functions:

# DCE fitting with custom AIF
result = osipy.fit_model(
    "extended_tofts",
    concentration=concentration,
    aif=aif,  # Your custom AIF
    time=time,
)

# DSC deconvolution with custom AIF
perfusion_maps = osipy.compute_perfusion_maps(
    concentration=delta_r2,
    aif=aif.concentration,  # Pass the concentration array
    time=time,
    mask=brain_mask
)

Visualize AIF

Plot your AIF for verification:

import matplotlib.pyplot as plt

fig, ax = plt.subplots(figsize=(8, 4))

# Plot AIF
ax.plot(aif.time, aif.concentration, 'r-', linewidth=2, label='Custom AIF')

# Add reference population AIF for comparison
parker = osipy.ParkerAIF()(aif.time)
ax.plot(aif.time, parker.concentration, 'b--', label='Parker AIF (reference)')

ax.set_xlabel('Time (s)')
ax.set_ylabel('Concentration (mM)')
ax.set_title('Arterial Input Function Comparison')
ax.legend()
ax.grid(True, alpha=0.3)

plt.savefig('aif_comparison.png', dpi=150)
plt.show()

Common Issues

Time Unit Mismatch

Ensure time units are consistent (osipy expects seconds):

# If your AIF is in minutes:
aif_time_seconds = aif_time_minutes * 60

aif = osipy.ArterialInputFunction(
    time=aif_time_seconds,
    concentration=aif_conc,
    aif_type=AIFType.MEASURED,
)

Negative Concentrations

Clip negative values (can arise from noise):

aif_conc_clipped = np.maximum(aif_conc, 0)

aif = osipy.ArterialInputFunction(
    time=time,
    concentration=aif_conc_clipped,
    aif_type=AIFType.MEASURED,
)

Late Bolus Arrival

Shift AIF using the shift_aif utility:

from osipy.common.aif import shift_aif

# Shift AIF concentration by a known delay (in seconds)
shifted_conc = shift_aif(aif_conc, time, delay=20.0, xp=np)

aif = osipy.ArterialInputFunction(
    time=time,
    concentration=shifted_conc,
    aif_type=AIFType.MEASURED,
)

Alternatively, use fit_delay=True in fit_model() to estimate the delay automatically per voxel:

result = osipy.fit_model(
    "extended_tofts", concentration, aif, time,
    fit_delay=True  # Estimates per-voxel arterial delay
)
delay_map = result.parameter_maps["delay"].values  # in seconds

See Also