Source code for pystatsbio.pk._common

"""Result types for non-compartmental pharmacokinetic analysis (NCA).

``NCAParams`` is the frozen payload of computed outputs; ``NCASolution`` is
the public return — it wraps a ``core.result.Result[NCAParams]`` so every NCA
fit exposes the same ``.backend_name`` / ``.timing`` / ``.warnings`` / ``.info``
metadata and Jupyter ``_repr_html_`` as the rest of the ecosystem
(``pystatsbio/CONVENTIONS.md`` B2).
"""

from __future__ import annotations

from dataclasses import dataclass

from pystatistics.core.result import Result, SolutionReprMixin


[docs] @dataclass(frozen=True) class NCAParams: """Computed payload of a non-compartmental pharmacokinetic analysis.""" # Primary PK parameters auc_last: float # AUC from 0 to last measurable concentration auc_inf: float | None # AUC extrapolated to infinity auc_pct_extrap: float | None # % AUC extrapolated cmax: float # peak concentration tmax: float # time of peak concentration half_life: float | None # terminal elimination half-life lambda_z: float | None # terminal elimination rate constant lambda_z_r_squared: float | None # r-squared of terminal slope regression # Derived parameters (require dose) clearance: float | None # CL = Dose / AUC_inf (or CL/F for oral) vz: float | None # Vz = Dose / (lambda_z * AUC_inf) aumc_last: float # AUMC (first moment) from 0 to last measurable concentration aumc_inf: float | None # AUMC extrapolated to infinity mrt: float | None # mean residence time = AUMC_inf / AUC_inf # Metadata dose: float | None route: str # 'iv' or 'ev' (extravascular) auc_method: str # 'linear', 'log-linear', 'linear-up/log-down' n_points: int n_terminal: int # number of points used for terminal slope
[docs] class NCASolution(SolutionReprMixin): """Public result of an NCA fit — a Solution wrapping ``Result[NCAParams]``. Exposes every NCA parameter as a read-only property plus the uniform ``.backend_name`` / ``.timing`` / ``.warnings`` / ``.info`` metadata and a Jupyter ``_repr_html_`` (via :class:`SolutionReprMixin`). """ def __init__(self, result: Result[NCAParams]) -> None: self._result = result # --- Metadata (from the Result envelope) --- @property def backend_name(self) -> str: return self._result.backend_name @property def timing(self) -> dict[str, float] | None: return self._result.timing @property def warnings(self) -> tuple[str, ...]: return self._result.warnings @property def info(self) -> dict: return self._result.info # --- PK parameters (from the payload) --- @property def auc_last(self) -> float: return self._result.params.auc_last @property def auc_inf(self) -> float | None: return self._result.params.auc_inf @property def auc_pct_extrap(self) -> float | None: return self._result.params.auc_pct_extrap @property def cmax(self) -> float: return self._result.params.cmax @property def tmax(self) -> float: return self._result.params.tmax @property def half_life(self) -> float | None: return self._result.params.half_life @property def lambda_z(self) -> float | None: return self._result.params.lambda_z @property def lambda_z_r_squared(self) -> float | None: return self._result.params.lambda_z_r_squared @property def clearance(self) -> float | None: return self._result.params.clearance @property def vz(self) -> float | None: return self._result.params.vz @property def aumc_last(self) -> float: """Area under the first moment curve to the last measurable concentration.""" return self._result.params.aumc_last @property def aumc_inf(self) -> float | None: """AUMC extrapolated to infinity (None if lambda_z is not estimable).""" return self._result.params.aumc_inf @property def mrt(self) -> float | None: """Mean residence time = AUMC_inf / AUC_inf (None if lambda_z unavailable).""" return self._result.params.mrt @property def dose(self) -> float | None: return self._result.params.dose @property def route(self) -> str: return self._result.params.route @property def auc_method(self) -> str: return self._result.params.auc_method @property def n_points(self) -> int: return self._result.params.n_points @property def n_terminal(self) -> int: return self._result.params.n_terminal
[docs] def summary(self) -> str: """Human-readable PK summary.""" p = self._result.params lines = ["Non-Compartmental Analysis", ""] lines.append(f" Route: {p.route.upper()}") lines.append(f" AUC method: {p.auc_method}") if p.dose is not None: lines.append(f" Dose: {p.dose}") lines.append("") lines.append(f" Cmax = {p.cmax:.4g}") lines.append(f" Tmax = {p.tmax:.4g}") lines.append(f" AUC(0-last) = {p.auc_last:.4g}") if p.auc_inf is not None: lines.append(f" AUC(0-inf) = {p.auc_inf:.4g}") lines.append(f" %AUC extrap = {p.auc_pct_extrap:.1f}%") if p.half_life is not None: lines.append(f" t1/2 = {p.half_life:.4g}") lines.append(f" lambda_z = {p.lambda_z:.4g}") lines.append(f" r-squared = {p.lambda_z_r_squared:.4f}") if p.clearance is not None: label = "CL" if p.route == "iv" else "CL/F" lines.append(f" {label:<14s} = {p.clearance:.4g}") if p.vz is not None: label = "Vz" if p.route == "iv" else "Vz/F" lines.append(f" {label:<14s} = {p.vz:.4g}") return "\n".join(lines)
def __repr__(self) -> str: p = self._result.params return ( f"NCASolution(route={p.route!r}, cmax={p.cmax:.4g}, " f"auc_last={p.auc_last:.4g})" )