""" Risk Model - Layer 8 of IOM Risk calculation engine with weights per object type """ from typing import Dict, List, Optional, Tuple from dataclasses import dataclass from enum import Enum class RiskDimension(str, Enum): """Risk dimensions""" SAFETY = "safety" ECONOMIC = "economic" ENVIRONMENTAL = "environmental" OPERATIONAL = "operational" LEGAL = "legal" AESTHETIC = "aesthetic" @dataclass class RiskScores: """Risk scores for all dimensions""" safety: int = 0 economic: int = 0 environmental: int = 0 operational: int = 0 legal: int = 0 aesthetic: int = 0 def to_dict(self) -> Dict: return { "safety": self.safety, "economic": self.economic, "environmental": self.environmental, "operational": self.operational, "legal": self.legal, "aesthetic": self.aesthetic } class RiskWeights: """Risk weights per object type""" # Default weights DEFAULT = { RiskDimension.SAFETY: 0.30, RiskDimension.ECONOMIC: 0.20, RiskDimension.OPERATIONAL: 0.20, RiskDimension.LEGAL: 0.10, RiskDimension.ENVIRONMENTAL: 0.10, RiskDimension.AESTHETIC: 0.10 } # Bridge weights - safety critical BRIDGE = { RiskDimension.SAFETY: 0.40, RiskDimension.ECONOMIC: 0.15, RiskDimension.OPERATIONAL: 0.20, RiskDimension.LEGAL: 0.10, RiskDimension.ENVIRONMENTAL: 0.05, RiskDimension.AESTHETIC: 0.10 } # Road weights - operational focus ROAD = { RiskDimension.SAFETY: 0.35, RiskDimension.ECONOMIC: 0.15, RiskDimension.OPERATIONAL: 0.25, RiskDimension.LEGAL: 0.10, RiskDimension.ENVIRONMENTAL: 0.05, RiskDimension.AESTHETIC: 0.10 } # Building facade weights - aesthetic focus FACADE = { RiskDimension.SAFETY: 0.15, RiskDimension.ECONOMIC: 0.20, RiskDimension.OPERATIONAL: 0.10, RiskDimension.LEGAL: 0.10, RiskDimension.ENVIRONMENTAL: 0.10, RiskDimension.AESTHETIC: 0.35 } # Window weights - aesthetic + safety WINDOW = { RiskDimension.SAFETY: 0.20, RiskDimension.ECONOMIC: 0.15, RiskDimension.OPERATIONAL: 0.10, RiskDimension.LEGAL: 0.10, RiskDimension.ENVIRONMENTAL: 0.10, RiskDimension.AESTHETIC: 0.35 } # EV Charger weights - safety critical EV_CHARGER = { RiskDimension.SAFETY: 0.45, RiskDimension.ECONOMIC: 0.15, RiskDimension.OPERATIONAL: 0.15, RiskDimension.LEGAL: 0.10, RiskDimension.ENVIRONMENTAL: 0.05, RiskDimension.AESTHETIC: 0.10 } # Street light weights - safety + operational STREET_LIGHT = { RiskDimension.SAFETY: 0.30, RiskDimension.ECONOMIC: 0.15, RiskDimension.OPERATIONAL: 0.25, RiskDimension.LEGAL: 0.10, RiskDimension.ENVIRONMENTAL: 0.10, RiskDimension.AESTHETIC: 0.10 } # Traffic signal weights - safety critical TRAFFIC_SIGNAL = { RiskDimension.SAFETY: 0.50, RiskDimension.ECONOMIC: 0.10, RiskDimension.OPERATIONAL: 0.20, RiskDimension.LEGAL: 0.10, RiskDimension.ENVIRONMENTAL: 0.05, RiskDimension.AESTHETIC: 0.05 } # Parking weights - operational + economic PARKING = { RiskDimension.SAFETY: 0.20, RiskDimension.ECONOMIC: 0.25, RiskDimension.OPERATIONAL: 0.25, RiskDimension.LEGAL: 0.15, RiskDimension.ENVIRONMENTAL: 0.05, RiskDimension.AESTHETIC: 0.10 } # Sign weights - aesthetic + legal SIGN = { RiskDimension.SAFETY: 0.20, RiskDimension.ECONOMIC: 0.15, RiskDimension.OPERATIONAL: 0.15, RiskDimension.LEGAL: 0.25, RiskDimension.ENVIRONMENTAL: 0.10, RiskDimension.AESTHETIC: 0.15 } class RiskLevel(str, Enum): """Risk levels""" MINIMAL = "minimal" LOW = "low" MEDIUM = "medium" HIGH = "high" CRITICAL = "critical" class RiskCalculator: """Calculate risk for infrastructure objects""" def __init__(self): self.weights_map = { "default": RiskWeights.DEFAULT, "bridge": RiskWeights.BRIDGE, "road": RiskWeights.ROAD, "facade": RiskWeights.FACADE, "window": RiskWeights.WINDOW, "ev_charger": RiskWeights.EV_CHARGER, "street_light": RiskWeights.STREET_LIGHT, "traffic_signal": RiskWeights.TRAFFIC_SIGNAL, "parking": RiskWeights.PARKING, "sign": RiskWeights.SIGN, } def calculate( self, scores: RiskScores, object_type: str = "default" ) -> Dict: """ Calculate total risk score Args: scores: Risk scores for each dimension object_type: Type of object for weight selection Returns: Dict with total score, level, and breakdown """ weights = self.weights_map.get(object_type.lower(), RiskWeights.DEFAULT) # Calculate weighted sum total = sum( getattr(scores, dim.value) * weight for dim, weight in weights.items() ) # Normalize to 0-10 scale total = min(10.0, max(0.0, total)) # Determine risk level level = self._get_level(total) return { "total": round(total, 2), "level": level.value, "level_description": self._get_level_description(level), "breakdown": scores.to_dict(), "weights": {dim.value: weight for dim, weight in weights.items()}, "object_type": object_type } def calculate_from_observation( self, condition: int, defect_codes: List[str], object_type: str = "default" ) -> Dict: """ Calculate risk from observation data Args: condition: Overall condition (1-5) defect_codes: List of defect codes object_type: Type of object Returns: Risk calculation result """ # Map condition to base risk scores base_scores = self._condition_to_scores(condition) # Adjust based on defect codes adjusted_scores = self._adjust_for_defects(base_scores, defect_codes) return self.calculate(adjusted_scores, object_type) def _condition_to_scores(self, condition: int) -> RiskScores: """Convert condition (1-5) to base risk scores""" # Condition 1 = excellent (low risk) # Condition 5 = critical (high risk) risk_multiplier = condition * 2 # 2, 4, 6, 8, 10 return RiskScores( safety=risk_multiplier, economic=risk_multiplier, environmental=risk_multiplier // 2, operational=risk_multiplier, legal=risk_multiplier // 2, aesthetic=risk_multiplier ) def _adjust_for_defects( self, scores: RiskScores, defect_codes: List[str] ) -> RiskScores: """Adjust risk scores based on defect codes""" # This would integrate with defect registry # For now, simple adjustment adjusted = RiskScores(**scores.to_dict()) for code in defect_codes: code_prefix = code[:2] # Structural defects increase safety risk if code_prefix in ["13", "14", "16"]: adjusted.safety = min(10, adjusted.safety + 2) # Corrosion increases economic risk if code_prefix in ["10", "11", "12"]: adjusted.economic = min(10, adjusted.economic + 2) # Surface defects increase aesthetic risk if code_prefix in ["20", "21", "22", "23", "24"]: adjusted.aesthetic = min(10, adjusted.aesthetic + 2) # Missing parts increase operational risk if code_prefix in ["15", "40", "41", "42", "43"]: adjusted.operational = min(10, adjusted.operational + 2) # Blockages increase operational and safety risk if code_prefix in ["19", "50", "51", "52"]: adjusted.operational = min(10, adjusted.operational + 1) adjusted.safety = min(10, adjusted.safety + 1) return adjusted def _get_level(self, score: float) -> RiskLevel: """Get risk level from score""" if score >= 8.0: return RiskLevel.CRITICAL elif score >= 6.0: return RiskLevel.HIGH elif score >= 4.0: return RiskLevel.MEDIUM elif score >= 2.0: return RiskLevel.LOW else: return RiskLevel.MINIMAL def _get_level_description(self, level: RiskLevel) -> str: """Get description for risk level""" descriptions = { RiskLevel.MINIMAL: "Minimal risk - no action needed", RiskLevel.LOW: "Low risk - routine monitoring", RiskLevel.MEDIUM: "Medium risk - plan maintenance within 12 months", RiskLevel.HIGH: "High risk - action required within 3 months", RiskLevel.CRITICAL: "Critical risk - immediate action required" } return descriptions[level] def get_weights_for_type(self, object_type: str) -> Dict: """Get weights for an object type""" weights = self.weights_map.get(object_type.lower(), RiskWeights.DEFAULT) return {dim.value: weight for dim, weight in weights.items()} def compare( self, scores1: RiskScores, scores2: RiskScores, object_type: str = "default" ) -> Dict: """ Compare two risk profiles Returns: Comparison result with differences """ result1 = self.calculate(scores1, object_type) result2 = self.calculate(scores2, object_type) return { "object_type": object_type, "risk1": result1, "risk2": result2, "difference": round(result2["total"] - result1["total"], 2), "trend": "improving" if result2["total"] < result1["total"] else "degrading", "dimension_differences": { dim.value: round(getattr(scores2, dim.value) - getattr(scores1, dim.value), 2) for dim in RiskDimension } } # Singleton instance risk_calculator = RiskCalculator() def calculate_risk( scores: RiskScores, object_type: str = "default" ) -> Dict: """Convenience function""" return risk_calculator.calculate(scores, object_type) def calculate_risk_from_observation( condition: int, defect_codes: List[str], object_type: str = "default" ) -> Dict: """Convenience function""" return risk_calculator.calculate_from_observation(condition, defect_codes, object_type) if __name__ == '__main__': # Example usage calc = RiskCalculator() # Example 1: Bridge abutment with crack print("=== Bridge Abutment with Crack ===") scores = RiskScores( safety=8, economic=6, environmental=2, operational=5, legal=4, aesthetic=1 ) result = calc.calculate(scores, "bridge") print(f"Total risk: {result['total']} ({result['level']})") print(f"Description: {result['level_description']}") # Example 2: Window with dirt print("\n=== Window with Dirt ===") scores = RiskScores( safety=2, economic=3, environmental=1, operational=2, legal=1, aesthetic=7 ) result = calc.calculate(scores, "window") print(f"Total risk: {result['total']} ({result['level']})") # Example 3: From observation print("\n=== From Observation (Condition 3, Defects: 2100, 2200) ===") result = calc.calculate_from_observation( condition=3, defect_codes=["2100", "2200"], object_type="facade" ) print(f"Total risk: {result['total']} ({result['level']})") print(f"Breakdown: {result['breakdown']}") # Example 4: Compare before/after print("\n=== Compare Before/After Repair ===") before = RiskScores(safety=8, economic=6, operational=5, legal=4, aesthetic=3, environmental=2) after = RiskScores(safety=3, economic=2, operational=2, legal=1, aesthetic=2, environmental=1) comparison = calc.compare(before, after, "bridge") print(f"Difference: {comparison['difference']}") print(f"Trend: {comparison['trend']}")