Files
boc/iom/risk/risk_model.py
T
Bernt bae705aa97 ARCHITECTURE: NFC roadmap, edge AI, audit logging
- 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
2026-06-29 16:24:48 +00:00

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']}")