Source code for AFL.automation.mixcalc.BalanceDiagnosis
from __future__ import annotations
from dataclasses import dataclass, field
from enum import Enum
from typing import Any, Dict, List
[docs]
class FailureCode(str, Enum):
"""Machine-readable codes describing why a mass balance failed.
Codes are ordered from specific root causes to general symptoms so that
the most actionable information appears first.
"""
MISSING_STOCK_COMPONENT = "missing_stock_component"
STOCK_CONCENTRATION_TOO_LOW = "stock_concentration_too_low"
TARGET_OUTSIDE_REACHABLE_COMPOSITIONS = "target_outside_reachable_compositions"
BELOW_MINIMUM_PIPETTE_VOLUME = "below_minimum_pipette_volume"
UNWANTED_STOCK_COMPONENT = "unwanted_stock_component"
TOLERANCE_EXCEEDED = "tolerance_exceeded"
[docs]
@dataclass
class FailureDetail:
"""One specific failure mode detected during diagnosis."""
code: FailureCode
description: str
affected_components: List[str] = field(default_factory=list)
affected_stocks: List[str] = field(default_factory=list)
data: Dict[str, Any] = field(default_factory=dict)
[docs]
def to_dict(self) -> Dict[str, Any]:
return {
"code": self.code.value,
"description": self.description,
"affected_components": self.affected_components,
"affected_stocks": self.affected_stocks,
"data": self.data,
}
[docs]
@dataclass
class BalanceDiagnosis:
"""Full diagnosis of a single balance result."""
success: bool
details: List[FailureDetail] = field(default_factory=list)
component_errors: Dict[str, float] = field(default_factory=dict)
[docs]
def to_dict(self) -> Dict[str, Any]:
return {
"success": self.success,
"details": [d.to_dict() for d in self.details],
"component_errors": self.component_errors,
}
[docs]
def summary(self) -> str:
"""Return a human-readable failure summary."""
if self.success:
return "Balance succeeded."
lines = ["Balance failed:"]
for detail in self.details:
lines.append(f" [{detail.code.value}] {detail.description}")
if detail.affected_components:
lines.append(f" Components: {', '.join(detail.affected_components)}")
if detail.affected_stocks:
lines.append(f" Stocks: {', '.join(detail.affected_stocks)}")
if self.component_errors:
worst_comp = max(self.component_errors, key=lambda k: abs(self.component_errors[k]))
worst_err = self.component_errors[worst_comp] * 100.0
lines.append(f" Worst error: {worst_comp} at {worst_err:.1f}%")
return "\n".join(lines)