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

474 lines
18 KiB
Python

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