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
474 lines
18 KiB
Python
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()
|