Source code for genplanner.zones.functional_zones

import math

from ._basic_terr_zones import *
from .abc_zone import Zone
from .territory_zones import TerritoryZone


def _normalize_ratio(zones_ratio: dict["TerritoryZone", float]) -> dict["TerritoryZone", float]:
    """
    Normalize ratios so that they sum to 1.0.

    Args:
        zones_ratio: Unnormalized ratios.

    Returns:
        dict[TerritoryZone, float]: Normalized ratios.

    Raises:
        ValueError: If the total ratio is not > 0.
    """
    total = float(sum(zones_ratio.values()))
    if total <= 0:
        raise ValueError("Total ratio must be > 0")
    return {z: float(v) / total for z, v in zones_ratio.items()}


[docs] class FunctionalZone(Zone): """ A composite zone describing a functional program of territorial zones. `FunctionalZone` groups multiple `TerritoryZone` definitions with target area ratios. Ratios are validated and normalized to sum to 1.0. The `min_area` of a functional zone is computed as the weighted sum of member zones' `min_area` values (for `TerritoryZone` this corresponds to `min_block_area`), using the normalized ratios. Args: zones_ratio: Mapping of `TerritoryZone` to a positive (unnormalized) target ratio. name: Non-empty functional zone name. Raises: ValueError: If `name` is empty, `zones_ratio` is empty, ratios are non-positive, or the ratio sum is invalid. TypeError: If keys are not `TerritoryZone` or values are not finite numbers. """
[docs] def __init__(self, zones_ratio: dict["TerritoryZone", float], name: str): self._name = name self._zones_ratio = dict(zones_ratio) self.validate() self._zones_ratio = _normalize_ratio(self._zones_ratio) self._zones_keys = {z.name: z for z in self._zones_ratio} self._min_area = self._calc_min_area()
@property def name(self) -> str: """ Functional zone name. Returns: str: Name of the functional zone. """ return self._name @property def min_area(self) -> float: """ Weighted minimum area constraint for the functional zone. Computed as `sum(zone.min_area * ratio)` using normalized ratios. Returns: float: Weighted minimum area. """ return self._min_area @property def zones_ratio(self) -> dict["TerritoryZone", float]: """ Normalized zone ratio mapping. Returns: dict[TerritoryZone, float]: A copy of the internal mapping where values sum to 1.0. """ return dict(self._zones_ratio) def validate(self) -> None: """ Validate functional zone invariants. Ensures: - name is a non-empty string, - zones_ratio is a non-empty dict, - keys are `TerritoryZone`, - values are finite numbers greater than 0. Raises: ValueError: If name is empty, zones_ratio is empty, or any ratio is <= 0. TypeError: If keys/values have incorrect types or ratios are not finite. """ if not isinstance(self._name, str) or not self._name.strip(): raise ValueError("FunctionalZone name must be non-empty string") if not isinstance(self._zones_ratio, dict) or not self._zones_ratio: raise ValueError("zones_ratio cannot be empty") for z, r in self._zones_ratio.items(): if not isinstance(z, TerritoryZone): raise TypeError("All keys in zones_ratio must be TerritoryZone") if not isinstance(r, (int, float)) or not math.isfinite(float(r)): raise TypeError("All values in zones_ratio must be finite numbers") if float(r) <= 0: raise ValueError("All values in zones_ratio must be > 0") def _calc_min_area(self) -> float: """ Compute the weighted minimum area for the functional zone. Returns: float: `sum(zone.min_area * ratio)` over normalized ratios. """ return float(sum(z.min_area * r for z, r in self._zones_ratio.items())) def __str__(self) -> str: """ Return a human-readable representation. Returns: str: String representation. """ return f'Functional zone "{self.name}"'
basic_func_zone = FunctionalZone( { residential_terr: 0.25, industrial_terr: 0.12, business_terr: 0.08, recreation_terr: 0.3, transport_terr: 0.1, agriculture_terr: 0.03, special_terr: 0.02, }, "basic", ) residential_func_zone = FunctionalZone( { residential_terr: 0.5, business_terr: 0.1, recreation_terr: 0.1, transport_terr: 0.1, agriculture_terr: 0.05, special_terr: 0.05, }, "residential territory", ) industrial_func_zone = FunctionalZone( { industrial_terr: 0.5, business_terr: 0.1, recreation_terr: 0.05, transport_terr: 0.1, agriculture_terr: 0.05, special_terr: 0.05, }, "industrial territory", ) business_func_zone = FunctionalZone( { residential_terr: 0.1, business_terr: 0.5, recreation_terr: 0.1, transport_terr: 0.1, agriculture_terr: 0.05, special_terr: 0.05, }, "business territory", ) recreation_func_zone = FunctionalZone( { residential_terr: 0.2, business_terr: 0.1, recreation_terr: 0.5, transport_terr: 0.05, agriculture_terr: 0.1, }, "recreation territory", ) transport_func_zone = FunctionalZone( { industrial_terr: 0.1, business_terr: 0.05, recreation_terr: 0.05, transport_terr: 0.5, agriculture_terr: 0.05, special_terr: 0.05, }, "transport territory", ) agricalture_func_zone = FunctionalZone( { residential_terr: 0.1, industrial_terr: 0.1, business_terr: 0.05, recreation_terr: 0.1, transport_terr: 0.05, agriculture_terr: 0.5, special_terr: 0.05, }, "agriculture territory", ) special_func_zone = FunctionalZone( { residential_terr: 0.01, industrial_terr: 0.1, business_terr: 0.05, recreation_terr: 0.05, transport_terr: 0.05, agriculture_terr: 0.05, special_terr: 0.5, }, "special territory", )