""" 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()