mixle.analysis.carcinogenic_risk module

Carcinogenic-risk models: linear no-threshold slope-factor / unit-risk (EPA-IRIS convention).

The regulatory-toxicology answer to “how much extra cancer risk does this exposure add over a lifetime”: a chronic dose is pushed through a chemical-specific potency coefficient under the linear no-threshold (LNT) low-dose assumption.

  • SlopeFactor – a chemical’s potency: an oral cancer slope factor (per mg/kg-day) and/or an inhalation unit risk (per ug/m3), as published by EPA IRIS (or supplied by the caller/knowledge layer – this module carries no slope-factor database, see Non-goals).

  • excess_lifetime_cancer_risk() – risk = LADD * oral_csf (oral route) or risk = conc * inhalation_iur (inhalation route), the EPA-IRIS linear low-dose form; falls back to the exact 1 - exp(-dose * slope) once the linear approximation would push risk above ~0.01. Exposure may be a scalar, a plain sample array, or an IC-1 Posterior – in the last case the posterior’s own derived_quantity() pushforward is used so the honesty flag prior_dominated propagates untouched from the exposure posterior.

  • radon_wlm_risk() – the BEIR-VI radon working-level-month (WLM) coefficient, risk = wlm * risk_per_wlm.

Both return an IC-8-style DerivedQuantity (samples + credible interval + prior_dominated), matching the construction used across the rest of mixle.analysis health/risk models. The concrete carrier here is RiskQuantity, which satisfies the frozen mixle.reason.posterior_protocol.DerivedQuantity protocol structurally (each health/risk module in mixle.analysis mints its own small concrete carrier of the same shape rather than sharing one class, so modules stay independent siblings – see analysis/health_risk.py’s equivalent).

class SlopeFactor(oral_csf=None, inhalation_iur=None, sigma_log=0.0, source='EPA-IRIS')[source]

Bases: object

A chemical’s cancer potency, EPA-IRIS style.

Parameters:
oral_csf

oral cancer slope factor, (mg/kg-day)^-1. None if the chemical has no established oral potency.

Type:

float | None

inhalation_iur

inhalation unit risk, (ug/m3)^-1. None if no established inhalation potency.

Type:

float | None

sigma_log

log-scale standard deviation of a multiplicative log-normal uncertainty band around the point-estimate slope factor (0.0 = treat the slope factor as fixed). Must be finite and non-negative – validated at construction (a negative or NaN value has no valid meaning as a standard deviation, so it is rejected here rather than silently treated as zero/fixed uncertainty downstream).

Type:

float

source

provenance tag for the potency values (default the EPA IRIS database, the regulatory-toxicology standard; caller-supplied values should override this).

Type:

str

class RiskQuantity(samples, prior_dominated=False)[source]

Bases: object

A pushforward risk distribution: draws + credible interval + the prior_dominated flag.

Structurally satisfies the IC-1 DerivedQuantity protocol (samples, prior_dominated, credible_interval) and additionally exposes mean – the point estimate callers actually read off first.

Construction validates samples: non-empty, finite, and a valid probability-like value in [0, 1] (the range the excess_lifetime_cancer_risk/radon_wlm_risk LNT pushforward always produces from validated non-negative inputs – see _lnt_risk()). This is defense-in-depth: invalid state cannot silently flow downstream to a caller even if some upstream pushforward fails to validate its own inputs.

Parameters:
property mean: float

the sample-mean excess risk.

Type:

Point estimate

credible_interval(level=0.9)[source]

Central level credible interval of the risk samples (e.g. level=0.9 -> 5%/95%).

level goes through the shared mixle.analysis interval contract (validated_level()), so every IC-1 carrier in the package rejects the same out-of-range levels the same way (MXR-080-1580).

Parameters:

level (float)

Return type:

tuple[float, float]

excess_lifetime_cancer_risk(exposure, sf, *, route='oral', n=2000, rng=None)[source]

Excess lifetime cancer risk under the linear no-threshold model (DR-ALG K7).

route="oral" treats exposure as an LADD (lifetime average daily dose, mg/kg-day) and multiplies through SlopeFactor.oral_csf; route="inhalation" treats exposure as an air concentration (ug/m3) multiplied through SlopeFactor.inhalation_iur.

Parameters:
  • exposure (Posterior | ndarray | float) – the lifetime-average dose/concentration. An IC-1 Posterior (its derived_quantity pushforward is used, so prior_dominated propagates from the exposure posterior), a plain array of exposure samples (already representing exposure uncertainty), or a single deterministic scalar. Every exposure draw must reduce to a single value per sample – one route/chemical/time-point/receptor already selected or aggregated by the caller – and must be finite and non-negative; this is enforced uniformly for all three forms, including a Posterior’s own draws (there is no honest way to guess, on the caller’s behalf, which route/chemical/time point/receptor a genuinely multi-dimensional draw was meant to represent, so that case is rejected with a clear error rather than silently collapsed to its first column).

  • sf (SlopeFactor) – the chemical’s SlopeFactor. Its potency coefficient for route, and its sigma_log, must be finite and non-negative (sigma_log is also validated at SlopeFactor construction).

  • route (str) – "oral" or "inhalation".

  • n (int) – number of posterior draws to take when exposure is a Posterior, or the number of slope-factor draws to take when exposure is a bare scalar and sf.sigma_log > 0. Must be a positive exact integer. On the Posterior path this is now a hard delivery receipt (MXR-080-1900): a posterior that returns fewer than n draws is an error, not a quietly smaller risk distribution.

  • rng (Generator | None) – numpy random Generator (a fresh default one is created if omitted).

Returns:

A DerivedQuantity of excess lifetime cancer risk (samples + CI + prior_dominated).

Return type:

DerivedQuantity

A Boolean exposure (scalar or dtype=bool array) is rejected rather than coerced: True is a flag, not a dose, and float(True) == 1.0 made one indistinguishable from a measured 1 mg/kg-day LADD once it was inside the LNT product (MXR-080-1900).

radon_wlm_risk(wlm, *, risk_per_wlm=5.38e-4, n=2000, rng=None)[source]

Radon lung-cancer risk from cumulative working-level-months (BEIR-VI linear coefficient).

risk = wlm * risk_per_wlm for small products (the BEIR-VI committee’s linear excess-relative-risk coefficient per WLM of cumulative radon-progeny exposure, default 5.38e-4 per WLM), saturating via the same 1 - exp(-x) low-dose-linear-no-threshold form _lnt_risk() uses once that product would exceed ~0.01 – without it, cumulative exposures large enough to be realistic over a working lifetime push the bare linear form above 1, which is not a valid probability. Both wlm and risk_per_wlm must be finite and non-negative (exposure and potency are never negative).

Parameters:
  • wlm (Posterior | ndarray | float) – cumulative working-level-months. An IC-1 Posterior over exposure (its own derived_quantity pushforward is used, so prior_dominated propagates from the exposure posterior), a plain array of exposure samples (already representing exposure uncertainty), or a single deterministic scalar. Each draw must reduce to one value per sample, exactly as excess_lifetime_cancer_risk() requires – a (n, d>1) matrix mixes route/receptor/time axes this function has no honest way to collapse.

  • risk_per_wlm (float) – BEIR-VI risk coefficient per WLM.

  • n (int) – number of posterior draws to take when wlm is a Posterior. Must be a positive exact integer. Ignored for the array/scalar paths, which carry their own sample count.

  • rng (Generator | None) – numpy random Generator used to draw from a Posterior (a fresh default one is created if omitted). Unused for the array/scalar paths, which are deterministic.

Returns:

A DerivedQuantity of radon-attributable lung-cancer risk.

Return type:

DerivedQuantity

n/rng were previously accepted and never used at all, on any path – a negative, fractional or Boolean draw count was silently taken and discarded (MXR-080-1574). They now drive the Posterior path they were reserved for, and n is validated on every path so an impossible request is rejected rather than ignored. On that path n is additionally a delivery receipt: a posterior returning fewer than n draws raises instead of yielding a thinner risk distribution that reads like the full one (MXR-080-1900). A Boolean wlm is rejected for the same reason a Boolean dose is – float(True) == 1.0 is not one working-level-month.