agent_urban_planning.AhlfeldtUtilityEngine

class AhlfeldtUtilityEngine(params, seed=None, large_N_threshold=200, budget_constraint=True, sampling_method='auto', deterministic=False, dtype='float64')[source]

Bases: object

Cobb-Douglas + Fréchet joint residence-workplace decision engine.

The closed-form softmax engine driving paper variant V1. Implements the Ahlfeldt et al. (2015) indirect utility ln u_ij = ln B_i + ln w_j - (1 - beta) * ln Q_i - kappa * tau_ij and dispatches sampling to one of three paths depending on scenario scale and the sampling_method kwarg:

  • gumbel (small-N default) — per-agent N x N Gumbel shock matrix stored in _shock_cache; argmax over log_Phi + g / epsilon. Used for Bezirke (N=23) and Ortsteile (N=97) where memory is trivial.

  • multinomial (factorized, large-N default) — shared log_Phi computed once per decide_batch call; each agent samples from softmax(epsilon * log_Phi) via inverse-CDF lookup. Memory footprint is independent of agent count; used at block scale (N=12k).

  • deterministic — continuum-limit interpretation: each agent contributes fractional weight P_ij * weight to every (i, j). Matches Ahlfeldt’s closed-form aggregate equilibrium exactly with zero Monte Carlo noise. Used as the primary block-level path for pack-reproducing results.

Most users should configure this through UtilityEngine rather than instantiating it directly.

Parameters:
  • params (AhlfeldtParams) – Structural parameters (alpha, beta, epsilon, kappa, …).

  • seed (Optional[int]) – Optional integer seed for the per-agent shock RNG.

  • large_N_threshold (int) – Size at which auto-dispatch switches from gumbel to multinomial. Defaults to 200.

  • budget_constraint (bool) – Whether to mask out (residence, workplace) pairs that violate a loose Cobb-Douglas affordability check.

  • sampling_method (str) – "auto" / "gumbel" / "multinomial".

  • deterministic (bool) – When True, force the continuum-limit path regardless of sampling_method.

  • dtype (str) – "float32" or "float64".

Examples

>>> import agent_urban_planning as aup
>>> # Prefer the public wrapper for V1 reproduction:
>>> # engine = aup.UtilityEngine(params, mode="softmax")
>>> # The advanced class is used internally by that wrapper.

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.

set_cache(cache)[source]

Accept and discard a cache reference (no-op for this engine).

AhlfeldtUtilityEngine runs entirely closed-form and does not use an LLM cache. Implements the protocol to keep market loops uniform.

Parameters:

cache – Ignored.

Return type:

None

Returns:

None.

Examples

>>> import agent_urban_planning as aup
>>> # engine = aup.AhlfeldtUtilityEngine(params)
>>> # engine.set_cache(None)  # safe no-op
set_current_wages(wages)[source]

Inject the current wage vector from the market loop.

Called by AhlfeldtMarket once per iteration before decide_batch so the engine can use the freshly updated wages in its utility computation. Missing keys fall back to Zone.wage_observed at utility-computation time.

Parameters:

wages (dict[str, float]) – Mapping zone -> wage for the current iteration.

Return type:

None

Returns:

None.

Examples

>>> import agent_urban_planning as aup
>>> # engine.set_current_wages({"Mitte": 1.1, "Charlottenburg": 0.9})
set_current_productivity(A)[source]

Inject per-zone productivity A_i from the market loop.

Called each iteration by AhlfeldtMarket when endogenous agglomeration is active. Missing keys fall back to Zone.productivity_A at utility-computation time.

Parameters:

A (dict[str, float]) – Mapping zone -> A_i (damped post-update productivity).

Return type:

None

Returns:

None.

Examples

>>> import agent_urban_planning as aup
>>> # engine.set_current_productivity({"Mitte": 1.5})
set_current_amenity(B)[source]

Inject per-zone amenity B_i from the market loop.

Mirrors set_current_productivity(). The indirect-utility formula reads ln B_i from this injection when present; otherwise falls back to Zone.amenity_B per zone.

Parameters:

B (dict[str, float]) – Mapping zone -> B_i (damped post-update amenity).

Return type:

None

Returns:

None.

Examples

>>> import agent_urban_planning as aup
>>> # engine.set_current_amenity({"Mitte": 2.1})
property price_elasticity: float

Market clearing code queries this for tatonnement step sizing.

For Ahlfeldt scenarios the floor-price elasticity is derived structurally: (1 - beta) * epsilon.

decide(agent, environment, zone_options, prices)[source]

Choose a (residence, workplace) pair for a single agent.

Delegates to decide_batch() with a single-element list, so that single-agent and batch paths share the same numerical kernel.

Parameters:
Return type:

LocationChoice

Returns:

A LocationChoice with the chosen residence, workplace, realized utility, and per-zone diagnostic utilities.

Examples

>>> import agent_urban_planning as aup
>>> # engine = aup.AhlfeldtUtilityEngine(params)
>>> # choice = engine.decide(agent, env, zones, prices)
decide_batch(agents, environment, zone_options, prices)[source]

Choose joint (residence, workplace) pairs for a batch of agents.

Builds the shared log u_ij matrix once from the current prices, wages, productivity and amenity vectors, then dispatches to one of three sampling paths (gumbel / multinomial / deterministic) depending on scale and the engine’s sampling_method / deterministic flags.

prices holds residential floor prices Q_i keyed by zone name. Current wages are read from the value set via set_current_wages() or fall back to Zone.wage_observed.

Parameters:
  • agents (list[Agent]) – List of Agent instances to decide for.

  • environment (Environment) – The Environment they operate in.

  • zone_options (list[str]) – Allowed residence and workplace zone names.

  • prices (dict[str, float]) – Mapping zone -> Q_i (residential floor price).

Return type:

list[LocationChoice]

Returns:

List of LocationChoice, one per input agent. Order matches agents. The deterministic path additionally populates self.last_choice_probabilities with the full (N, N) choice-probability matrix consumed by AhlfeldtMarket for continuum aggregation.

Examples

>>> import agent_urban_planning as aup
>>> # engine = aup.AhlfeldtUtilityEngine(params, deterministic=True)
>>> # choices = engine.decide_batch(agents, env, zones, prices)
clear_shock_cache()[source]

Drop cached per-agent Gumbel shocks.

Useful between Monte Carlo replicates: each replicate then regenerates fresh shocks from the (seeded) sub-RNGs.

Return type:

None

Returns:

None.

Examples

>>> import agent_urban_planning as aup
>>> # engine.clear_shock_cache()