bae705aa97
- Add NFC ePassport roadmap (ICAO 9303, eIDAS) - Add TensorFlow.js edge face detection (BlazeFace) - Add structured audit logger (GDPR-compliant) - Risk scoring support Part of KYC Apple Native UX v1.1.0
414 lines
13 KiB
Python
414 lines
13 KiB
Python
"""
|
|
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']}")
|