Source code for autods_pet.deauville

"""Deauville Score assignment from ROI statistics.

Implements the standard threshold ladder comparing a target ROI uptake
value against the mediastinal blood pool (MBP) and liver references.
"""

from __future__ import annotations

import math

# Small tolerance to avoid floating-point rounding flipping borderline scores
_DS_TOL = 1e-9


[docs] def assign_ds( target_value: float | None, mbp_value: float, liver_value: float, allow_ds1: bool = False, liver_multiplier: float = 2.0, ) -> int: """Assign a Deauville Score (1-5) from PET uptake values. **Clinical note on DS 1:** Per the Deauville criteria, DS 1 represents *absent residual uptake* (no visible lesion). It is only assigned when ``target_value`` is ``None`` (or ``NaN``) and ``allow_ds1`` is ``True``. A target with any measurable uptake (even very low, e.g. SUV = 0.001) receives DS 2 or higher, not DS 1. Callers should pass ``None`` to indicate the absence of a target lesion. The DS 4/5 boundary is set at ``liver_multiplier × liver_value`` (default 2.0, per the Lugano 2014 consensus - Barrington et al., *J Clin Oncol*, 2014). Some protocols use 3× liver; pass ``liver_multiplier=3.0`` for those. Parameters ---------- target_value : float or None Uptake statistic for the target ROI (e.g. p95, max). If None/NaN, interpreted as "no uptake" (DS 1 when *allow_ds1* is True). mbp_value : float Mediastinal blood pool reference (e.g. voxelwise median of aorta). liver_value : float Liver reference (e.g. voxelwise median of liver). allow_ds1 : bool If True, a missing/NaN *target_value* yields DS 1 (used for focal lesion scoring where absence of a lesion = DS 1). If False, a missing target yields 0 (unassignable). liver_multiplier : float Multiplier applied to *liver_value* for the DS 4/5 threshold (default ``2.0`` per Lugano 2014). Returns ------- int Deauville Score: 1-5, or 0 if the score cannot be assigned. Raises ------ ValueError If *mbp_value* or *liver_value* is <= 0. Examples -------- >>> from autods_pet.deauville import assign_ds >>> assign_ds(target_value=1.5, mbp_value=2.0, liver_value=3.0) 2 >>> assign_ds(target_value=2.5, mbp_value=2.0, liver_value=3.0) 3 >>> assign_ds(target_value=5.0, mbp_value=2.0, liver_value=3.0) 4 >>> assign_ds(target_value=7.0, mbp_value=2.0, liver_value=3.0) 5 >>> assign_ds(target_value=None, mbp_value=2.0, liver_value=3.0, allow_ds1=True) 1 """ # Validate references if mbp_value <= 0: raise ValueError(f"mbp_value must be > 0, got {mbp_value}") if liver_value <= 0: raise ValueError(f"liver_value must be > 0, got {liver_value}") # Handle missing target if target_value is None or ( isinstance(target_value, float) and math.isnan(target_value) ): return 1 if allow_ds1 else 0 if target_value <= mbp_value + _DS_TOL: return 2 if target_value <= liver_value + _DS_TOL: return 3 if target_value <= liver_multiplier * liver_value + _DS_TOL: return 4 return 5