Source code for agent_urban_planning.decisions.utility

"""Unified ``UtilityEngine`` API class — V1 / V2 / V3 via kwargs.

This module exposes the public :class:`UtilityEngine` class. Configure via
constructor kwargs to reproduce the paper's three baseline-family variants:

  * ``mode='softmax'``                       → V1 (Baseline-softmax)
  * ``mode='argmax', noise='frechet'``       → V2 (Baseline-ABM argmax)
  * ``mode='argmax', noise='normal'``        → V3 (Normal-ABM argmax)

Internally delegates to :class:`AhlfeldtUtilityEngine` (softmax) or
:class:`AhlfeldtABMEngine` (argmax), both ported from the dev repo. The
public class is a thin compatibility/dispatch wrapper so that user code
references one symbol — ``aup.UtilityEngine`` — regardless of which
underlying implementation handles the call.

Example::

    import agent_urban_planning as aup

    # V1 reproduction:
    engine = aup.UtilityEngine(scenario.ahlfeldt_params, mode="softmax")

    # V2 reproduction:
    engine = aup.UtilityEngine(
        scenario.ahlfeldt_params, mode="argmax", noise="frechet",
    )

    # V3 reproduction:
    engine = aup.UtilityEngine(
        scenario.ahlfeldt_params, mode="argmax", noise="normal",
    )
"""
from __future__ import annotations

from typing import Any, Literal

from agent_urban_planning.decisions.ahlfeldt_abm_engine import AhlfeldtABMEngine
from agent_urban_planning.decisions.ahlfeldt_utility import AhlfeldtUtilityEngine


_VALID_MODES = ("softmax", "argmax")
_VALID_NOISE = ("frechet", "normal")


[docs] class UtilityEngine: """Closed-form Cobb-Douglas + Fréchet utility decision engine. Configure via constructor kwargs to reproduce V1 (Baseline-softmax), V2 (Baseline-ABM argmax with Fréchet noise), or V3 (Normal-ABM argmax with Gaussian noise) from the paper. Args: params: An ``AhlfeldtParams`` instance from :mod:`agent_urban_planning.data.loaders` carrying the model's structural elasticities (``alpha``, ``beta``, ``epsilon``, ``kappa_eps``, etc.). mode: ``"softmax"`` for the deterministic V1 pattern (closed-form softmax over Fréchet utility). ``"argmax"`` for the V2/V3 ABM pattern (per-agent draw + argmax). Default ``"softmax"``. noise: When ``mode="argmax"``, selects the per-agent shock distribution. ``"frechet"`` for V2; ``"normal"`` for V3. Ignored when ``mode="softmax"``. Default ``"frechet"``. **kwargs: Forwarded to the underlying implementation (:class:`AhlfeldtUtilityEngine` for softmax, :class:`AhlfeldtABMEngine` for argmax). Common kwargs: ``num_agents``, ``batch_size``, ``seed``, ``dtype``. Raises: ValueError: If ``mode`` is not one of ``{"softmax", "argmax"}``, or if ``noise`` is not one of ``{"frechet", "normal"}``. Examples: V1 reproduction (Baseline-softmax):: >>> import agent_urban_planning as aup >>> engine = aup.UtilityEngine(params, mode="softmax") >>> # Use as you would any DecisionEngine. >>> # sim = aup.SimulationEngine(scenario=sc, agent_config=ag, engine=engine) >>> # results = sim.run() V2 reproduction (Baseline-ABM argmax, Fréchet shocks):: >>> engine = aup.UtilityEngine( ... params, mode="argmax", noise="frechet", ... num_agents=1_000_000, seed=42, ... ) V3 reproduction (Normal-ABM argmax, Gaussian shocks):: >>> engine = aup.UtilityEngine( ... params, mode="argmax", noise="normal", ... num_agents=1_000_000, seed=42, ... ) Notes: Internally this class is a dispatch wrapper around two implementation classes (:class:`AhlfeldtUtilityEngine` and :class:`AhlfeldtABMEngine`). All other attribute and method access is forwarded transparently to the underlying implementation via ``__getattr__``, so any feature documented on those classes is usable on a ``UtilityEngine`` instance. See Also: :class:`agent_urban_planning.HybridDecisionEngine` — V4 (LLM elicits per-agent preference weights, then closed-form choice). :class:`agent_urban_planning.LLMDecisionEngine` — V5 (full LLM-as-decision-maker hierarchical engine). References: Ahlfeldt, G. M., Redding, S. J., Sturm, D. M., Wolf, N. (2015). The economics of density: Evidence from the Berlin Wall. *Econometrica*, 83(6), 2127-2189. """ def __init__( self, params: Any, *, mode: Literal["softmax", "argmax"] = "softmax", noise: Literal["frechet", "normal"] = "frechet", **kwargs: Any, ) -> None: if mode not in _VALID_MODES: raise ValueError( f"mode={mode!r} is not valid; expected one of {_VALID_MODES}" ) if noise not in _VALID_NOISE: raise ValueError( f"noise={noise!r} is not valid; expected one of {_VALID_NOISE}" ) self._mode = mode self._noise = noise if mode == "softmax": self._impl = AhlfeldtUtilityEngine(params, **kwargs) else: # argmax self._impl = AhlfeldtABMEngine( params, shock_distribution=noise, **kwargs, ) @property def mode(self) -> str: """The configured mode: ``"softmax"`` or ``"argmax"``.""" return self._mode @property def noise(self) -> str: """The configured per-agent noise distribution (only relevant for argmax mode).""" return self._noise
[docs] def decide_batch(self, *args: Any, **kwargs: Any) -> Any: """Forward to the underlying implementation's ``decide_batch``. Transparently forwards to either :meth:`AhlfeldtUtilityEngine.decide_batch` (softmax) or :meth:`AhlfeldtABMEngine.decide_batch` (argmax) depending on the configured mode. Args: *args: Positional arguments forwarded unchanged. **kwargs: Keyword arguments forwarded unchanged. Returns: List of :class:`agent_urban_planning.LocationChoice`, one per input agent and in the same order. Examples: >>> import agent_urban_planning as aup >>> # engine = aup.UtilityEngine(params) >>> # choices = engine.decide_batch(agents, env, zones, prices) """ return self._impl.decide_batch(*args, **kwargs)
def __getattr__(self, name: str) -> Any: # Forward unknown attribute access to the wrapped implementation. # Note: __getattr__ is only called when attribute lookup fails on # the instance itself, so this doesn't interfere with `_impl` etc. if name.startswith("_"): raise AttributeError(name) return getattr(self._impl, name) def __repr__(self) -> str: return ( f"UtilityEngine(mode={self._mode!r}, noise={self._noise!r}, " f"_impl={type(self._impl).__name__})" )