AnemiaLens / backend /app /ml /runtime_stack.py
3v324v23's picture
Update backend for account workflows and calibration
68bc1c7
Raw
History Blame Contribute Delete
3.93 kB
from __future__ import annotations
from typing import Literal
from app.ml.archive_model import clamp
RUNTIME_STACK_VERSION = "archive-evidence-fusion-v4"
SourceHint = Literal["roi_original", "palpebral", "forniceal_palpebral"]
DEFAULT_SOURCE_THRESHOLDS: dict[SourceHint, float] = {
"roi_original": 0.495,
"palpebral": 0.65,
"forniceal_palpebral": 0.65,
}
DEFAULT_RISK_ARCHIVE_WEIGHTS: dict[SourceHint, float] = {
"roi_original": 0.55,
"palpebral": 1.0,
"forniceal_palpebral": 1.0,
}
DEFAULT_HB_ARCHIVE_WEIGHTS: dict[SourceHint, float] = {
"roi_original": 0.70,
"palpebral": 1.0,
"forniceal_palpebral": 1.0,
}
def decision_threshold_for_source(source_hint: SourceHint = "roi_original") -> float:
return float(DEFAULT_SOURCE_THRESHOLDS.get(source_hint, DEFAULT_SOURCE_THRESHOLDS["roi_original"]))
def risk_archive_weight_for_source(source_hint: SourceHint = "roi_original") -> float:
return float(DEFAULT_RISK_ARCHIVE_WEIGHTS.get(source_hint, DEFAULT_RISK_ARCHIVE_WEIGHTS["roi_original"]))
def hb_archive_weight_for_source(source_hint: SourceHint = "roi_original") -> float:
return float(DEFAULT_HB_ARCHIVE_WEIGHTS.get(source_hint, DEFAULT_HB_ARCHIVE_WEIGHTS["roi_original"]))
def build_runtime_stack_prediction(
archive_prediction: dict[str, float],
*,
efficientnet_prediction: dict[str, float] | None = None,
source_hint: SourceHint = "roi_original",
) -> dict[str, float]:
archive_risk = float(archive_prediction["anemia_risk"])
archive_hb = float(archive_prediction["predicted_hemoglobin"])
archive_uncertainty = float(archive_prediction["uncertainty"])
risk = archive_risk
predicted_hemoglobin = archive_hb
uncertainty = archive_uncertainty
if efficientnet_prediction is not None:
efficientnet_risk = float(efficientnet_prediction["anemia_risk"])
efficientnet_hb = float(efficientnet_prediction["predicted_hemoglobin"])
efficientnet_uncertainty = float(efficientnet_prediction.get("uncertainty", 0.35))
# Confidence-weighted ensemble: lower uncertainty → higher weight
archive_conf = clamp(1.0 - archive_uncertainty)
efficientnet_conf = clamp(1.0 - efficientnet_uncertainty)
total_conf = archive_conf + efficientnet_conf + 1e-9
# Apply source-specific floor weight for archive (it has calibrated features)
source_floor = risk_archive_weight_for_source(source_hint)
raw_archive_w = archive_conf / total_conf
# Blend floor weight with confidence-derived weight
archive_w = clamp(0.5 * source_floor + 0.5 * raw_archive_w, 0.25, 0.80)
efficientnet_w = 1.0 - archive_w
disagreement = abs(archive_risk - efficientnet_risk)
hemoglobin_gap = abs(archive_hb - efficientnet_hb)
# Agreement bonus: both models agree on direction → reduce uncertainty
agreement_bonus = 0.0
if (archive_risk > 0.5) == (efficientnet_risk > 0.5):
agreement_bonus = 0.04 + disagreement * 0.06
risk = (archive_w * archive_risk) + (efficientnet_w * efficientnet_risk)
hb_archive_w = hb_archive_weight_for_source(source_hint)
predicted_hemoglobin = (hb_archive_w * archive_hb) + ((1.0 - hb_archive_w) * efficientnet_hb)
uncertainty = clamp(
(archive_w * archive_uncertainty)
+ (efficientnet_w * efficientnet_uncertainty)
+ (disagreement * 0.08)
+ (min(hemoglobin_gap / 12.0, 1.0) * 0.025)
- agreement_bonus,
0.04,
0.92,
)
return {
"anemia_risk": clamp(risk, 0.0, 1.0),
"predicted_hemoglobin": predicted_hemoglobin,
"uncertainty": clamp(uncertainty, 0.04, 0.95),
"decision_threshold": decision_threshold_for_source(source_hint),
}