"""unsprawl.core.schemas.
This module defines the Universal Modular Design (UMD) schema layer for Unsprawl.
These Pydantic models are the *language* of the deterministic core engine:
- They are global (no country/city-specific assumptions).
- They are stable contracts between messy local datasets (adapters) and the core simulation.
Anti-circular protocol
----------------------
`unsprawl.core` MUST NOT import from adapters/providers/loaders.
"""
from __future__ import annotations
from typing import Any, Literal
from pydantic import BaseModel, Field, field_validator
LatLon = tuple[float, float]
[docs]
class Entity(BaseModel):
"""Universal base class for the Unsprawl simulation.
Everything in the simulation (static or moving) is an Entity.
Notes
-----
Coordinate ordering is **strictly (lat, lon)** across the entire platform.
Adapters must normalize any source data into this convention at the boundary.
"""
id: str
location: LatLon # (lat, lon)
[docs]
@field_validator("location")
@classmethod
def _validate_location_lat_lon(cls, value: LatLon) -> LatLon:
"""Validate that location is (lat, lon) in sane numeric ranges.
This validator is not meant to be geo-precise; it is a defensive check that:
- enforces the ordering contract at runtime
- catches common adapter mistakes early (swapped lon/lat)
"""
lat, lon = value
# Range checks (WGS84-ish). Keep permissive but useful.
if not (-90.0 <= float(lat) <= 90.0):
raise ValueError("location[0] must be latitude in [-90, 90]")
if not (-180.0 <= float(lon) <= 180.0):
raise ValueError("location[1] must be longitude in [-180, 180]")
return float(lat), float(lon)
[docs]
class Asset(Entity):
"""A static economic unit (Building, Park, Transit Station).
This replaces the legacy Singapore-specific concept of "HDB Flat" with a generic
container that can represent any asset class across any region.
The physics engine treats `local_metadata` as an opaque payload.
"""
asset_type: Literal["residential", "commercial", "transport"]
# Physics Properties
floor_area_sqm: float
lease_remaining_years: float # Keep float (may include month fractions)
# Financial Properties
valuation_currency: str = "USD"
predicted_valuation: float = 0.0
# Waste-bin for local context (UI uses it; physics ignores it)
local_metadata: dict[str, Any] = Field(default_factory=dict)
[docs]
class Agent(Entity):
"""A dynamic actor (Commuter, Bus, Car).
Agents flow through the city graph / continuous space depending on the simulation
backend.
"""
velocity: tuple[float, float] = (0.0, 0.0)
goal: LatLon
state: Literal["idle", "moving", "stuck"] = "idle"