Files
boc/iom/urban_morphology/urban_morphology_index.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

428 lines
14 KiB
Python

"""
Urban Morphology Index (UMI) - Extension to IOM
Measures the gap between planned city and actual city
"""
from typing import Dict, List, Optional, Tuple
from dataclasses import dataclass
from enum import Enum
class GUINivel(str, Enum):
"""Guerrilla Urbanization Index levels"""
GUI_0 = "GUI_0" # Fully planned
GUI_1 = "GUI_1" # Minor informal elements
GUI_2 = "GUI_2" # Small spontaneous activities
GUI_3 = "GUI_3" # Clear mix planned/informal
GUI_4 = "GUI_4" # Large informal presence
GUI_5 = "GUI_5" # Informal city dominates
@dataclass
class MorphologyScores:
"""Component scores for UMI"""
structural_improvisation: float = 0.0 # SI: 0-10
planning_discrepancy: float = 0.0 # PD: 0-10
infrastructure_improvisation: float = 0.0 # II: 0-10
economic_density: float = 0.0 # ED: 0-10
vertical_contrast: float = 0.0 # VC: 0-10
def to_dict(self) -> Dict:
return {
"structural_improvisation": self.structural_improvisation,
"planning_discrepancy": self.planning_discrepancy,
"infrastructure_improvisation": self.infrastructure_improvisation,
"economic_density": self.economic_density,
"vertical_contrast": self.vertical_contrast
}
class UrbanMorphologyAnalyzer:
"""Analyzes urban morphology from observations"""
def __init__(self):
self.weights = {
"structural_improvisation": 0.20,
"planning_discrepancy": 0.25,
"infrastructure_improvisation": 0.20,
"economic_density": 0.20,
"vertical_contrast": 0.15
}
def calculate_gui(
self,
scores: MorphologyScores
) -> Dict:
"""
Calculate Guerrilla Urbanization Index
Returns:
Dict with GUI level, score, and breakdown
"""
# Calculate weighted average
total = sum(
getattr(scores, dim) * weight
for dim, weight in self.weights.items()
)
# Map to GUI level
gui_level = self._score_to_gui(total)
return {
"gui_score": round(total, 2),
"gui_level": gui_level.value,
"gui_description": self._gui_description(gui_level),
"components": scores.to_dict(),
"weights": self.weights
}
def calculate_umi(
self,
observations: List[Dict],
official_data: Optional[Dict] = None
) -> Dict:
"""
Calculate full Urban Morphology Index
Args:
observations: List of IOM observations
official_data: Official planning data
Returns:
UMI analysis
"""
# Analyze observations
si = self._analyze_structural_improvisation(observations)
pd = self._analyze_planning_discrepancy(observations, official_data)
ii = self._analyze_infrastructure_improvisation(observations)
ed = self._analyze_economic_density(observations)
vc = self._analyze_vertical_contrast(observations)
scores = MorphologyScores(
structural_improvisation=si,
planning_discrepancy=pd,
infrastructure_improvisation=ii,
economic_density=ed,
vertical_contrast=vc
)
gui = self.calculate_gui(scores)
# Additional metrics
informal_percentage = self._estimate_informal_percentage(observations)
self_construction_ratio = self._estimate_self_construction(observations)
return {
"umi_score": round(gui["gui_score"] * 10, 2), # 0-100 scale
"gui": gui,
"informal_percentage": round(informal_percentage, 2),
"self_construction_ratio": round(self_construction_ratio, 2),
"analysis": {
"structural_improvisation": {
"score": si,
"description": self._si_description(si)
},
"planning_discrepancy": {
"score": pd,
"description": self._pd_description(pd)
},
"infrastructure_improvisation": {
"score": ii,
"description": self._ii_description(ii)
},
"economic_density": {
"score": ed,
"description": self._ed_description(ed)
},
"vertical_contrast": {
"score": vc,
"description": self._vc_description(vc)
}
}
}
def _analyze_structural_improvisation(self, observations: List[Dict]) -> float:
"""Analyze structural improvisation from observations"""
# Look for:
# - Self-built structures
# - Organic growth patterns
# - Non-standard materials
# - Additions/modifications
score = 0.0
for obs in observations:
findings = obs.get("findings", [])
for finding in findings:
code = finding.get("code", "")
# Self-construction indicators
if code in ["2300", "3300"]: # Surface damage, material loss
score += 0.5
if code in ["4100", "4200", "4300"]: # Missing/broken/loose parts
score += 1.0
if code in ["2100", "2400"]: # Dirt, graffiti
score += 0.3
# Normalize to 0-10
return min(10.0, score)
def _analyze_planning_discrepancy(
self,
observations: List[Dict],
official_data: Optional[Dict]
) -> float:
"""Analyze discrepancy between planned and actual"""
score = 0.0
# Compare observations with official plans
if official_data:
planned_buildings = official_data.get("planned_buildings", 0)
actual_buildings = len(observations)
if planned_buildings > 0:
ratio = actual_buildings / planned_buildings
if ratio > 1.5:
score += 5.0 # Significant overbuilding
elif ratio > 1.2:
score += 3.0 # Moderate overbuilding
# Check for mixed-use indicators
for obs in observations:
findings = obs.get("findings", [])
for finding in findings:
code = finding.get("code", "")
if code in ["5100", "5200"]: # Blockages
score += 0.5
return min(10.0, score)
def _analyze_infrastructure_improvisation(self, observations: List[Dict]) -> float:
"""Analyze infrastructure improvisation"""
score = 0.0
# Look for:
# - Visible hoses/pipes
# - Temporary electrical
# - Sheet metal roofs
# - Additions
for obs in observations:
findings = obs.get("findings", [])
for finding in findings:
code = finding.get("code", "")
# Infrastructure improvisation
if code in ["6200", "6300"]: # Water/ice damage
score += 0.8
if code in ["1500", "1510", "1520"]: # Missing components
score += 1.0
if code in ["1600", "1610", "1620"]: # Settlement/deformation
score += 0.7
return min(10.0, score)
def _analyze_economic_density(self, observations: List[Dict]) -> float:
"""Analyze economic density"""
score = 0.0
# High economic density indicators:
# - Many commercial signs
# - Active storefronts
# - Mixed use
commercial_count = sum(
1 for obs in observations
if any(f.get("code", "").startswith("2") for f in obs.get("findings", []))
)
if len(observations) > 0:
ratio = commercial_count / len(observations)
score = ratio * 10
return min(10.0, score)
def _analyze_vertical_contrast(self, observations: List[Dict]) -> float:
"""Analyze vertical economic contrast"""
score = 0.0
# Look for:
# - Luxury buildings adjacent to informal
# - Modern vs traditional
# - High-rise vs low-rise
# This would require geospatial analysis
# For now, use condition variance as proxy
conditions = [
obs.get("overall_condition", 3)
for obs in observations
]
if len(conditions) > 1:
variance = max(conditions) - min(conditions)
score = variance * 2 # Scale to 0-10
return min(10.0, score)
def _estimate_informal_percentage(self, observations: List[Dict]) -> float:
"""Estimate percentage of informal structures"""
if not observations:
return 0.0
informal_indicators = 0
for obs in observations:
findings = obs.get("findings", [])
for finding in findings:
code = finding.get("code", "")
if code in ["2100", "2300", "2400", "4100", "4200", "4300"]:
informal_indicators += 1
return (informal_indicators / len(observations)) * 100
def _estimate_self_construction(self, observations: List[Dict]) -> float:
"""Estimate self-construction ratio"""
if not observations:
return 0.0
self_build_indicators = 0
for obs in observations:
findings = obs.get("findings", [])
for finding in findings:
code = finding.get("code", "")
if code in ["2300", "3300", "4100", "4200"]:
self_build_indicators += 1
return (self_build_indicators / len(observations)) * 100
def _score_to_gui(self, score: float) -> GUINivel:
"""Convert score to GUI level"""
if score < 1.5:
return GUINivel.GUI_0
elif score < 3.0:
return GUINivel.GUI_1
elif score < 4.5:
return GUINivel.GUI_2
elif score < 6.0:
return GUINivel.GUI_3
elif score < 8.0:
return GUINivel.GUI_4
else:
return GUINivel.GUI_5
def _gui_description(self, level: GUINivel) -> str:
"""Get description for GUI level"""
descriptions = {
GUINivel.GUI_0: "Fully planned environment (Singapore Marina Bay)",
GUINivel.GUI_1: "Minor informal elements (Stockholm inner city)",
GUINivel.GUI_2: "Small spontaneous activities (Tokyo back streets)",
GUINivel.GUI_3: "Clear mix planned/informal (Sukhumvit, Bangkok)",
GUINivel.GUI_4: "Large informal presence between modern buildings (Silom, Bangkok)",
GUINivel.GUI_5: "Informal city dominates (Dharavi, parts of Lagos)"
}
return descriptions[level]
def _si_description(self, score: float) -> str:
if score < 3: return "Minimal improvisation"
elif score < 5: return "Some organic growth"
elif score < 7: return "Significant improvisation"
else: return "Highly improvised structures"
def _pd_description(self, score: float) -> str:
if score < 3: return "Close to plan"
elif score < 5: return "Moderate deviations"
elif score < 7: return "Significant deviations"
else: return "Major planning failure"
def _ii_description(self, score: float) -> str:
if score < 3: return "Standard infrastructure"
elif score < 5: return "Some temporary solutions"
elif score < 7: return "Visible improvisation"
else: return "Extensive improvised infrastructure"
def _ed_description(self, score: float) -> str:
if score < 3: return "Low economic density"
elif score < 5: return "Moderate density"
elif score < 7: return "High density"
else: return "Extreme economic density"
def _vc_description(self, score: float) -> str:
if score < 3: return "Homogeneous area"
elif score < 5: return "Some contrast"
elif score < 7: return "Significant contrast"
else: return "Extreme vertical contrast"
# Example: Bangkok Silom analysis
def example_bangkok_silom():
"""Example analysis of Bangkok Silom area"""
analyzer = UrbanMorphologyAnalyzer()
# Simulated observations for Silom area
observations = [
{
"goid": "BYG-FAC-WIN-GLA-001",
"overall_condition": 4,
"findings": [
{"code": "2100", "type": "dirt_accumulation"},
{"code": "2300", "type": "surface_damage"}
]
},
{
"goid": "BYG-FAC-WIN-GLA-002",
"overall_condition": 5,
"findings": [
{"code": "4100", "type": "missing_part"},
{"code": "4200", "type": "broken_part"}
]
},
{
"goid": "COM-DIS-SGN-001",
"overall_condition": 3,
"findings": [
{"code": "2400", "type": "graffiti"},
{"code": "2100", "type": "dirt_accumulation"}
]
},
{
"goid": "BYG-ROF-SUR-001",
"overall_condition": 4,
"findings": [
{"code": "6200", "type": "water_damage"},
{"code": "2300", "type": "surface_damage"}
]
},
{
"goid": "ENE-EVC-CHA-001",
"overall_condition": 2,
"findings": [
{"code": "2100", "type": "dirt_accumulation"}
]
}
]
official_data = {
"planned_buildings": 3,
"planned_uses": ["residential", "commercial"]
}
result = analyzer.calculate_umi(observations, official_data)
print("=== Bangkok Silom Analysis ===")
print(f"UMI Score: {result['umi_score']}/100")
print(f"GUI Level: {result['gui']['gui_level']}")
print(f"GUI Score: {result['gui']['gui_score']}/10")
print(f"Informal %: {result['informal_percentage']}%")
print(f"Self-construction: {result['self_construction_ratio']}%")
print("\nComponents:")
for component, data in result['analysis'].items():
print(f" {component}: {data['score']}/10 - {data['description']}")
return result
if __name__ == '__main__':
example_bangkok_silom()