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
428 lines
14 KiB
Python
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()
|