""" Urban Layer Index (ULI) - Extension to IOM Measures coexisting urban realities on the same place """ from typing import Dict, List, Optional, Tuple from dataclasses import dataclass from enum import Enum from datetime import datetime class UrbanLayer(str, Enum): """The five urban layers""" FORMAL = "formal" # Planned city: zoning, property values, architecture FUNCTIONAL = "functional" # How people actually use the place INFORMAL = "informal" # Spontaneous commerce, self-built, street life HIDDEN = "hidden" # Social networks, power structures (measured separately) @dataclass class LayerPresence: """Presence of a layer at a location""" layer: UrbanLayer intensity: float # 0-10 confidence: float # 0-1 evidence: List[str] # What indicates this layer def to_dict(self) -> Dict: return { "layer": self.layer.value, "intensity": round(self.intensity, 2), "confidence": round(self.confidence, 2), "evidence": self.evidence } @dataclass class ULIScores: """Urban Layer Index component scores""" physical_formality: float = 0.0 # PF: How close to original plan informal_usage: float = 0.0 # IU: Spontaneous commerce, self-build social_complexity: float = 0.0 # SC: Groups and activities over 24h economic_contrast: float = 0.0 # EC: Investment/income differences institutional_presence: float = 0.0 # IP: Authority/control over place def to_dict(self) -> Dict: return { "physical_formality": self.physical_formality, "informal_usage": self.informal_usage, "social_complexity": self.social_complexity, "economic_contrast": self.economic_contrast, "institutional_presence": self.institutional_presence } class UrbanLayerAnalyzer: """Analyzes urban layers from observations""" def __init__(self): self.weights = { "physical_formality": 0.20, "informal_usage": 0.25, "social_complexity": 0.20, "economic_contrast": 0.20, "institutional_presence": 0.15 } def analyze_layers( self, observations: List[Dict], temporal_data: Optional[Dict] = None, official_data: Optional[Dict] = None ) -> Dict: """ Analyze all urban layers at a location Args: observations: IOM observations temporal_data: Time-based activity data official_data: Planning/authority data Returns: Layer analysis """ # Analyze each layer layers = [] # Formal layer formal = self._analyze_formal_layer(observations, official_data) layers.append(formal) # Functional layer functional = self._analyze_functional_layer(observations, temporal_data) layers.append(functional) # Informal layer informal = self._analyze_informal_layer(observations) layers.append(informal) # Calculate ULI uli = self._calculate_uli(layers) # Layer interaction analysis interactions = self._analyze_interactions(layers) return { "uli_score": round(uli, 2), "uli_interpretation": self._interpret_uli(uli), "layers": [layer.to_dict() for layer in layers], "interactions": interactions, "reality_count": self._count_realities(layers), "dominant_reality": self._dominant_reality(layers), "measurement_time": datetime.utcnow().isoformat() } def _analyze_formal_layer( self, observations: List[Dict], official_data: Optional[Dict] ) -> LayerPresence: """Analyze formal/planned layer""" evidence = [] score = 10.0 # Start assuming fully formal # Check against official plans if official_data: planned = official_data.get("planned_buildings", 0) actual = len(observations) if planned > 0 and actual > planned * 1.5: score -= 3.0 evidence.append("Significant overbuilding vs plan") elif planned > 0 and actual > planned * 1.2: score -= 1.5 evidence.append("Moderate overbuilding vs plan") # Check for formal architecture indicators formal_indicators = 0 informal_indicators = 0 for obs in observations: findings = obs.get("findings", []) for finding in findings: code = finding.get("code", "") # Informal indicators reduce formal score if code in ["2100", "2300", "2400"]: # Dirt, damage, graffiti informal_indicators += 1 if code in ["4100", "4200", "4300"]: # Missing/broken parts informal_indicators += 1.5 if code in ["5100", "5200"]: # Blockages informal_indicators += 0.5 # Well-maintained indicators if obs.get("overall_condition", 3) <= 2: formal_indicators += 0.5 # Adjust score if informal_indicators > formal_indicators * 2: score -= 4.0 evidence.append("Predominantly informal structures") elif informal_indicators > formal_indicators: score -= 2.0 evidence.append("Mixed formal/informal") score = max(0.0, min(10.0, score)) if score > 7: evidence.append("Well-maintained, planned appearance") return LayerPresence( layer=UrbanLayer.FORMAL, intensity=score, confidence=0.7, evidence=evidence ) def _analyze_functional_layer( self, observations: List[Dict], temporal_data: Optional[Dict] ) -> LayerPresence: """Analyze how people actually use the place""" evidence = [] score = 5.0 # Neutral start # Check for commercial activity commercial = 0 residential = 0 mixed = 0 for obs in observations: goid = obs.get("goid", "") # Domain indicates use if goid.startswith("COM"): commercial += 1 elif goid.startswith("BYG"): residential += 1 elif goid.startswith("TRN") or goid.startswith("ENE"): mixed += 1 # Findings indicate activity findings = obs.get("findings", []) for finding in findings: code = finding.get("code", "") if code in ["2100", "2200"]: # Dirt, color change commercial += 0.3 # High traffic # Calculate diversity total = commercial + residential + mixed if total > 0: diversity = len(set([ "commercial" if commercial > 0 else "", "residential" if residential > 0 else "", "mixed" if mixed > 0 else "" ])) - 1 # Remove empty score = diversity * 3.0 # 0-9 if diversity >= 2: evidence.append(f"Mixed use: {commercial:.0f} commercial, {residential:.0f} residential") # Temporal data (if available) if temporal_data: activity_hours = temporal_data.get("active_hours", 12) if activity_hours > 16: score += 1.0 evidence.append(f"High activity: {activity_hours}h/day") score = max(0.0, min(10.0, score)) return LayerPresence( layer=UrbanLayer.FUNCTIONAL, intensity=score, confidence=0.6, evidence=evidence ) def _analyze_informal_layer(self, observations: List[Dict]) -> LayerPresence: """Analyze informal/spontaneous layer""" evidence = [] score = 0.0 # Count informal indicators informal_score = 0.0 for obs in observations: findings = obs.get("findings", []) for finding in findings: code = finding.get("code", "") # Self-build indicators if code in ["2300", "3300"]: # Surface damage, material loss informal_score += 1.0 evidence.append("Self-modified structures") if code in ["4100", "4200", "4300"]: # Missing/broken/loose informal_score += 1.5 evidence.append("Improvised repairs") if code in ["2400"]: # Graffiti informal_score += 0.5 if code in ["2100"]: # Dirt accumulation informal_score += 0.3 # Infrastructure improvisation if code in ["6200", "6300"]: # Water/ice damage informal_score += 0.8 evidence.append("Infrastructure improvisation") score = min(10.0, informal_score) if score > 5: evidence.append("Significant informal presence") return LayerPresence( layer=UrbanLayer.INFORMAL, intensity=score, confidence=0.65, evidence=list(set(evidence)) # Deduplicate ) def _calculate_uli(self, layers: List[LayerPresence]) -> float: """Calculate Urban Layer Index""" # ULI measures complexity - how many layers are present and intense # Higher when multiple layers coexist with high intensity intensities = [layer.intensity for layer in layers] active_layers = sum(1 for i in intensities if i > 3.0) strong_layers = sum(1 for i in intensities if i > 6.0) # Base: average intensity avg_intensity = sum(intensities) / len(layers) # Multiplier: exponential for multiple strong layers # 1 strong layer = 1x, 2 strong = 3x, 3 strong = 6x layer_multiplier = 1 + strong_layers * (strong_layers + 1) / 2 # Variance bonus: high contrast between layers adds complexity # Bangkok: formal 3, informal 10 = variance 7 -> high complexity variance = max(intensities) - min(intensities) variance_bonus = variance * 1.5 # Activity bonus: more active layers = more complex activity_bonus = active_layers * 3.0 # Coexistence tension: when formal and informal both strong formal_intensity = next((l.intensity for l in layers if l.layer == UrbanLayer.FORMAL), 0) informal_intensity = next((l.intensity for l in layers if l.layer == UrbanLayer.INFORMAL), 0) tension = (formal_intensity * informal_intensity) / 10 uli = (avg_intensity + variance_bonus + activity_bonus + tension) * layer_multiplier return min(100.0, uli) def _analyze_interactions(self, layers: List[LayerPresence]) -> List[Dict]: """Analyze interactions between layers""" interactions = [] # Find layer pairs layer_dict = {layer.layer: layer for layer in layers} # Formal vs Informal tension if UrbanLayer.FORMAL in layer_dict and UrbanLayer.INFORMAL in layer_dict: formal = layer_dict[UrbanLayer.FORMAL] informal = layer_dict[UrbanLayer.INFORMAL] if formal.intensity > 6 and informal.intensity > 6: interactions.append({ "type": "coexistence_tension", "description": "Strong formal and informal layers coexisting", "intensity": round((formal.intensity + informal.intensity) / 2, 2) }) elif formal.intensity < 3 and informal.intensity > 7: interactions.append({ "type": "informal_domination", "description": "Informal layer dominates formal planning", "intensity": round(informal.intensity, 2) }) # Functional diversity if UrbanLayer.FUNCTIONAL in layer_dict: functional = layer_dict[UrbanLayer.FUNCTIONAL] if functional.intensity > 7: interactions.append({ "type": "high_functional_diversity", "description": "Many different activities share space", "intensity": round(functional.intensity, 2) }) return interactions def _count_realities(self, layers: List[LayerPresence]) -> int: """Count how many distinct realities are present""" return sum(1 for layer in layers if layer.intensity > 3.0) def _dominant_reality(self, layers: List[LayerPresence]) -> str: """Find dominant reality""" if not layers: return "unknown" dominant = max(layers, key=lambda x: x.intensity) return dominant.layer.value def _interpret_uli(self, uli: float) -> str: """Interpret ULI score""" if uli < 20: return "Simple urban reality - one dominant layer" elif uli < 40: return "Moderate complexity - two layers active" elif uli < 60: return "High complexity - multiple realities coexist" elif uli < 80: return "Very high complexity - intense layer interaction" else: return "Extreme complexity - many strong realities in tension" # Example analyses def example_stockholm_inner_city(): """Stockholm inner city - GUI 1""" analyzer = UrbanLayerAnalyzer() observations = [ {"goid": "BYG-FAC-WIN-GLA-001", "overall_condition": 2, "findings": [{"code": "2100"}]}, {"goid": "COM-DIS-SGN-001", "overall_condition": 2, "findings": [{"code": "2200"}]}, {"goid": "TRN-ROD-SGN-001", "overall_condition": 2, "findings": []}, ] official_data = {"planned_buildings": 3, "zoning": "mixed"} temporal_data = {"active_hours": 14} result = analyzer.analyze_layers(observations, temporal_data, official_data) print("=== Stockholm Inner City ===") print(f"ULI: {result['uli_score']}") print(f"Realities: {result['reality_count']}") print(f"Dominant: {result['dominant_reality']}") print(f"Interpretation: {result['uli_interpretation']}") print("\nLayers:") for layer in result['layers']: print(f" {layer['layer']}: {layer['intensity']}/10 ({', '.join(layer['evidence'][:2])})") return result def example_bangkok_silom(): """Bangkok Silom - GUI 4""" analyzer = UrbanLayerAnalyzer() observations = [ {"goid": "BYG-FAC-WIN-GLA-001", "overall_condition": 4, "findings": [{"code": "2100"}, {"code": "2300"}]}, {"goid": "BYG-FAC-WIN-GLA-002", "overall_condition": 5, "findings": [{"code": "4100"}, {"code": "4200"}]}, {"goid": "COM-DIS-SGN-001", "overall_condition": 3, "findings": [{"code": "2400"}, {"code": "2100"}]}, {"goid": "COM-DIS-AWN-001", "overall_condition": 4, "findings": [{"code": "2300"}]}, {"goid": "ENE-EVC-CHA-001", "overall_condition": 3, "findings": [{"code": "6200"}]}, ] official_data = {"planned_buildings": 2, "zoning": "commercial"} temporal_data = {"active_hours": 20} result = analyzer.analyze_layers(observations, temporal_data, official_data) print("\n=== Bangkok Silom ===") print(f"ULI: {result['uli_score']}") print(f"Realities: {result['reality_count']}") print(f"Dominant: {result['dominant_reality']}") print(f"Interpretation: {result['uli_interpretation']}") print("\nLayers:") for layer in result['layers']: print(f" {layer['layer']}: {layer['intensity']}/10 ({', '.join(layer['evidence'][:2])})") print("\nInteractions:") for interaction in result['interactions']: print(f" {interaction['type']}: {interaction['description']}") return result def example_dharavi(): """Dharavi - GUI 5""" analyzer = UrbanLayerAnalyzer() observations = [ {"goid": "BYG-FAC-WIN-GLA-001", "overall_condition": 5, "findings": [{"code": "4100"}, {"code": "4200"}, {"code": "2300"}]}, {"goid": "BYG-FAC-WIN-GLA-002", "overall_condition": 5, "findings": [{"code": "4300"}, {"code": "3300"}]}, {"goid": "COM-DIS-SGN-001", "overall_condition": 4, "findings": [{"code": "2100"}, {"code": "2400"}]}, {"goid": "ENE-EVC-CHA-001", "overall_condition": 4, "findings": [{"code": "6200"}, {"code": "6300"}]}, {"goid": "BYG-ROF-SUR-001", "overall_condition": 5, "findings": [{"code": "2300"}, {"code": "6200"}]}, ] official_data = {"planned_buildings": 1, "zoning": "industrial"} temporal_data = {"active_hours": 22} result = analyzer.analyze_layers(observations, temporal_data, official_data) print("\n=== Dharavi ===") print(f"ULI: {result['uli_score']}") print(f"Realities: {result['reality_count']}") print(f"Dominant: {result['dominant_reality']}") print(f"Interpretation: {result['uli_interpretation']}") print("\nLayers:") for layer in result['layers']: print(f" {layer['layer']}: {layer['intensity']}/10 ({', '.join(layer['evidence'][:2])})") return result if __name__ == '__main__': example_stockholm_inner_city() example_bangkok_silom() example_dharavi()