el-defect-detection / src /pipeline /decision_engine.py
nithishbasireddy's picture
Upload src/pipeline/decision_engine.py with huggingface_hub
8c5546c verified
"""
Decision Engine: PASS/FAIL Module-Level Assessment.
Takes cell-level analysis results and produces a module-level decision.
Decision logic:
1. Single-cell failure: any cell with defect_score > threshold → FAIL
2. Aggregate failure: too many cells with moderate defects → FAIL
3. Critical defects: any crack > 30mm or dark > 50% → immediate FAIL
4. Cumulative: total crack length across all cells exceeds limit → FAIL
Thresholds are configurable for different quality standards:
- IEC: Standard international electrotechnical commission thresholds
- Strict: Premium quality (e.g., residential rooftop)
- Lenient: Utility-scale where minor defects are acceptable
Design decision: Conservative defaults (IEC standard). Better to flag
a good module for review than to pass a defective one.
"""
from typing import List, Dict
from dataclasses import dataclass
from .crack_analysis import CellAnalysisResult
@dataclass
class ModuleDecision:
"""Module-level PASS/FAIL decision with supporting evidence."""
decision: str # "PASS" or "FAIL"
confidence: float # 0.0 - 1.0
overall_score: float # 0-100
num_cells: int
num_defective_cells: int
failure_reasons: List[str]
cell_results: List[dict]
summary: Dict[str, float]
def to_dict(self) -> dict:
return {
"decision": self.decision,
"confidence": round(self.confidence, 2),
"overall_score": round(self.overall_score, 1),
"num_cells": self.num_cells,
"num_defective_cells": self.num_defective_cells,
"failure_reasons": self.failure_reasons,
"summary": {k: round(v, 2) for k, v in self.summary.items()},
"cell_results": self.cell_results,
}
class DecisionEngine:
"""
Module-level PASS/FAIL decision engine.
Configurable thresholds for different quality standards.
"""
# Default thresholds (IEC-aligned)
DEFAULT_THRESHOLDS = {
# Per-cell thresholds
"max_cell_defect_score": 50.0, # Score above this → cell FAIL
"max_crack_length_mm": 30.0, # Single crack > this → cell FAIL
"max_dark_area_pct": 40.0, # Dark area > this → cell FAIL
"max_cracks_per_cell": 5, # More cracks than this → cell FAIL
# Module-level thresholds
"max_defective_cell_ratio": 0.15, # >15% cells defective → module FAIL
"max_total_crack_length_mm": 200.0, # Total across all cells
"max_avg_defect_score": 25.0, # Average score across cells
}
STRICT_THRESHOLDS = {
"max_cell_defect_score": 30.0,
"max_crack_length_mm": 15.0,
"max_dark_area_pct": 20.0,
"max_cracks_per_cell": 3,
"max_defective_cell_ratio": 0.05,
"max_total_crack_length_mm": 100.0,
"max_avg_defect_score": 15.0,
}
LENIENT_THRESHOLDS = {
"max_cell_defect_score": 70.0,
"max_crack_length_mm": 50.0,
"max_dark_area_pct": 60.0,
"max_cracks_per_cell": 8,
"max_defective_cell_ratio": 0.30,
"max_total_crack_length_mm": 500.0,
"max_avg_defect_score": 40.0,
}
def __init__(
self,
thresholds: Dict[str, float] = None,
standard: str = "default",
):
"""
Args:
thresholds: Custom thresholds dict. Overrides standard.
standard: 'default' (IEC), 'strict', or 'lenient'
"""
if thresholds is not None:
self.thresholds = thresholds
elif standard == "strict":
self.thresholds = self.STRICT_THRESHOLDS.copy()
elif standard == "lenient":
self.thresholds = self.LENIENT_THRESHOLDS.copy()
else:
self.thresholds = self.DEFAULT_THRESHOLDS.copy()
def decide(self, cell_results: List[CellAnalysisResult]) -> ModuleDecision:
"""
Make module-level PASS/FAIL decision.
Args:
cell_results: List of per-cell analysis results
Returns:
ModuleDecision with decision, confidence, and evidence
"""
if not cell_results:
return ModuleDecision(
decision="PASS",
confidence=0.5,
overall_score=0.0,
num_cells=0,
num_defective_cells=0,
failure_reasons=["No cells analyzed"],
cell_results=[],
summary={},
)
num_cells = len(cell_results)
failure_reasons = []
defective_cells = []
# Per-cell analysis
for result in cell_results:
cell_fails = []
# Check defect score
if result.defect_score > self.thresholds["max_cell_defect_score"]:
cell_fails.append(
f"Cell {result.cell_id}: defect score "
f"{result.defect_score:.1f} > {self.thresholds['max_cell_defect_score']}"
)
# Check crack length
if result.total_crack_length_mm > self.thresholds["max_crack_length_mm"]:
cell_fails.append(
f"Cell {result.cell_id}: crack length "
f"{result.total_crack_length_mm:.1f}mm > {self.thresholds['max_crack_length_mm']}mm"
)
# Check dark area
if result.dark.dark_area_pct > self.thresholds["max_dark_area_pct"]:
cell_fails.append(
f"Cell {result.cell_id}: dark area "
f"{result.dark.dark_area_pct:.1f}% > {self.thresholds['max_dark_area_pct']}%"
)
# Check crack count
total_cracks = result.num_cracks + result.num_cross_cracks
if total_cracks > self.thresholds["max_cracks_per_cell"]:
cell_fails.append(
f"Cell {result.cell_id}: {total_cracks} cracks "
f"> {self.thresholds['max_cracks_per_cell']}"
)
if cell_fails:
defective_cells.append(result.cell_id)
failure_reasons.extend(cell_fails)
# Module-level checks
defective_ratio = len(defective_cells) / num_cells
if defective_ratio > self.thresholds["max_defective_cell_ratio"]:
failure_reasons.append(
f"Module: {len(defective_cells)}/{num_cells} cells defective "
f"({defective_ratio:.1%} > {self.thresholds['max_defective_cell_ratio']:.1%})"
)
total_crack_length = sum(r.total_crack_length_mm for r in cell_results)
if total_crack_length > self.thresholds["max_total_crack_length_mm"]:
failure_reasons.append(
f"Module: total crack length {total_crack_length:.1f}mm "
f"> {self.thresholds['max_total_crack_length_mm']}mm"
)
avg_score = sum(r.defect_score for r in cell_results) / num_cells
if avg_score > self.thresholds["max_avg_defect_score"]:
failure_reasons.append(
f"Module: avg defect score {avg_score:.1f} "
f"> {self.thresholds['max_avg_defect_score']}"
)
# Final decision
decision = "FAIL" if failure_reasons else "PASS"
# Confidence: based on how far metrics are from thresholds
overall_score = avg_score
if decision == "FAIL":
confidence = min(0.5 + avg_score / 200.0, 0.99)
else:
confidence = min(0.5 + (100 - avg_score) / 200.0, 0.99)
# Summary statistics
summary = {
"avg_defect_score": avg_score,
"max_defect_score": max(r.defect_score for r in cell_results),
"total_crack_length_mm": total_crack_length,
"avg_dark_area_pct": sum(r.dark.dark_area_pct for r in cell_results) / num_cells,
"max_dark_area_pct": max(r.dark.dark_area_pct for r in cell_results),
"defective_cell_ratio": defective_ratio,
"total_cracks": sum(r.num_cracks for r in cell_results),
"total_cross_cracks": sum(r.num_cross_cracks for r in cell_results),
}
return ModuleDecision(
decision=decision,
confidence=confidence,
overall_score=overall_score,
num_cells=num_cells,
num_defective_cells=len(defective_cells),
failure_reasons=failure_reasons,
cell_results=[r.to_dict() for r in cell_results],
summary=summary,
)
def update_thresholds(self, **kwargs):
"""Update individual thresholds."""
for key, value in kwargs.items():
if key in self.thresholds:
self.thresholds[key] = value
else:
raise ValueError(f"Unknown threshold: {key}")