Source code for bamengine.events.planning

"""
Planning events for firm production and labor decisions.

This module defines the planning phase events that execute at the start of
each simulation period. Firms make forward-looking decisions about production
targets and labor requirements based on current market conditions.

Event Sequence
--------------
The default planning events execute in this order:

1. FirmsDecideDesiredProduction - Set production targets based on inventory/prices
2. FirmsDecideDesiredLabor - Calculate labor needs from production targets
3. FirmsDecideVacancies - Determine job openings
4. FirmsFireExcessWorkers - Lay off workers when labor exceeds desired

Default planning-phase pricing events (after FirmsDecideDesiredProduction):

- FirmsPlanBreakevenPrice - Breakeven using previous-period costs
- FirmsPlanPrice - Price adjustment with breakeven floor

These are mutually exclusive with the production-phase pricing events
(FirmsCalcBreakevenPrice, FirmsAdjustPrice) defined in
bamengine.events.production. Activate those via ``pricing_phase='production'``.

Design Notes
------------
- Events operate on firm roles (Producer, Employer, Borrower)
- Each event wraps a system function from events._internal.planning
- System functions contain the actual implementation logic
- Events handle simulation state access and parameter passing

Examples
--------
Execute planning events:

>>> import bamengine as be
>>> sim = be.Simulation.init(n_firms=100, seed=42)
>>> # Planning events run as part of default pipeline
>>> sim.step()

Execute individual planning event:

>>> event = sim.get_event("firms_decide_desired_production")
>>> event.execute(sim)
>>> sim.prod.desired_production.mean()  # doctest: +SKIP
105.0

See Also
--------
bamengine.events._internal.planning : System function implementations
bamengine.events.production : Production-phase pricing events
Producer : Production and pricing state
Employer : Labor hiring state
"""

from __future__ import annotations

from typing import TYPE_CHECKING

from bamengine.core.decorators import event

if TYPE_CHECKING:  # pragma: no cover
    from bamengine.simulation import Simulation


[docs] @event class FirmsDecideDesiredProduction: """ Set production targets based on inventory levels and market position. Firms adjust their production targets adaptively based on whether they have unsold inventory and how their prices compare to market average. This implements the adaptive production rule from Delli Gatti et al. (2011). This event first zeroes out the current ``production`` field (preparing it for the new period), then uses ``production_prev`` (the previous period's actual production) as the baseline signal for planning decisions. Algorithm --------- For each firm i: 1. Zero out current production: :math:`Y_i := 0` 2. Generate random shock: :math:`\\varepsilon_i \\sim U(0, h_\\rho)` 3. Use previous production (``production_prev``) as baseline and apply rule: - If :math:`S_i = 0` and :math:`P_i \\geq \\bar{P}`: :math:`Y^*_i = Y^{prev}_i \\times (1 + \\varepsilon_i)` [increase] - If :math:`S_i > 0` and :math:`P_i < \\bar{P}`: :math:`Y^*_i = Y^{prev}_i \\times (1 - \\varepsilon_i)` [decrease] - Otherwise: :math:`Y^*_i = Y^{prev}_i` [maintain] 4. Set desired_production = expected_demand = :math:`Y^*_i` Mathematical Notation --------------------- .. math:: Y^*_{i,t} = \\begin{cases} Y^{prev}_{i}(1 + \\varepsilon_i) & \\text{if } S_{i,t-1}=0 \\land P_i \\geq \\bar{P} \\\\ Y^{prev}_{i}(1 - \\varepsilon_i) & \\text{if } S_{i,t-1}>0 \\land P_i < \\bar{P} \\\\ Y^{prev}_{i} & \\text{otherwise} \\end{cases} where: - :math:`Y^*`: desired production for next period - :math:`Y^{prev}`: actual production in previous period (``production_prev``) - :math:`S`: inventory (unsold goods from previous period) - :math:`P`: firm's individual price - :math:`\\bar{P}`: average market price across all firms - :math:`\\varepsilon`: random shock :math:`\\sim U(0, h_\\rho)` - :math:`h_\\rho`: maximum production shock parameter (config) Examples -------- Execute this event: >>> import bamengine as be >>> sim = be.Simulation.init(n_firms=100, seed=42) >>> event = sim.get_event("firms_decide_desired_production") >>> event.execute(sim) Inspect production changes: >>> prod = sim.prod >>> firms_increasing = (prod.inventory == 0) & (prod.price >= sim.ec.avg_mkt_price) >>> firms_increasing.sum() # doctest: +SKIP 45 Check desired production distribution: >>> prod.desired_production.mean() # doctest: +SKIP 52.5 >>> prod.desired_production.std() # doctest: +SKIP 5.2 Notes ----- This event must execute early in the pipeline as desired production determines labor requirements (see FirmsDecideDesiredLabor). The production shock introduces randomness that prevents firms from settling into static equilibria. See Also -------- FirmsDecideDesiredLabor : Calculate labor needs from production targets Producer : Production and pricing state bamengine.events._internal.planning.firms_decide_desired_production : Implementation """
[docs] def execute(self, sim: Simulation) -> None: from bamengine.events._internal.planning import firms_decide_desired_production firms_decide_desired_production( sim.prod, p_avg=sim.ec.avg_mkt_price, h_rho=sim.config.h_rho, rng=sim.rng, )
[docs] @event class FirmsPlanBreakevenPrice: """ Calculate planning-phase breakeven price from previous period's costs. Computes minimum cost-covering price using last period's wage bill and interest obligations as the numerator, and desired production (the firm's current-period output target) as the denominator. This provides an early price signal before the labor and credit markets operate. Algorithm --------- For each firm i: 1. Calculate total costs: :math:`C_i = W_{i,t-1} + I_{i,t-1}` 2. Calculate breakeven: :math:`P_{\\text{breakeven}} = C_i / Y^d_{i,t}` 3. Apply cap (if configured): :math:`P_{\\text{breakeven}} = \\min(P_{\\text{breakeven}}, P_i \\times \\text{cap\\_factor})` where :math:`W_{t-1}` is previous period's wage bill, :math:`I_{t-1}` is previous period's interest, and :math:`Y^d_{t}` is the desired production for the current period (freshly computed by FirmsDecideDesiredProduction). Notes ----- At planning time, wage_bill and LoanBook interest naturally contain previous period's values (recomputed only in Phases 2-3). This event exploits that timing to avoid introducing separate "previous expenses" fields. Must run AFTER firms_decide_desired_production (needs desired_production). See Also -------- FirmsPlanPrice : Planning-phase price adjustment using this breakeven bamengine.events._internal.planning.firms_plan_breakeven_price : Implementation """
[docs] def execute(self, sim: Simulation) -> None: from bamengine.events._internal.planning import firms_plan_breakeven_price firms_plan_breakeven_price( prod=sim.prod, emp=sim.emp, lb=sim.lb, cap_factor=sim.cap_factor, )
[docs] @event class FirmsPlanPrice: """ Planning-phase price adjustment based on inventory and market position. Adjusts prices using the same inventory/market-position rule as FirmsAdjustPrice, but runs during the planning phase using the breakeven computed from previous period's costs. See Also -------- FirmsPlanBreakevenPrice : Planning-phase breakeven calculation bamengine.events._internal.planning.firms_plan_price : Implementation """
[docs] def execute(self, sim: Simulation) -> None: from bamengine.events._internal.planning import firms_plan_price firms_plan_price( sim.prod, p_avg=sim.ec.avg_mkt_price, h_eta=sim.config.h_eta, rng=sim.rng, )
[docs] @event class FirmsDecideDesiredLabor: """ Calculate labor requirements from production targets and productivity. Firms determine how many workers they need to achieve their desired production level, given their labor productivity (output per worker). Algorithm --------- For each firm i: .. math:: L^d_i = \\lceil Y^d_i / \\phi_i \\rceil where: - :math:`L^d`: desired labor (number of workers needed) - :math:`Y^d`: desired production (from FirmsDecideDesiredProduction) - :math:`\\phi`: labor productivity (output per worker) Examples -------- >>> import bamengine as be >>> sim = be.Simulation.init(n_firms=100, seed=42) >>> event = sim.get_event("firms_decide_desired_labor") >>> event.execute(sim) >>> sim.emp.desired_labor.sum() # doctest: +SKIP 500 See Also -------- FirmsDecideDesiredProduction : Set production targets FirmsDecideVacancies : Calculate job openings bamengine.events._internal.planning.firms_decide_desired_labor : Implementation """
[docs] def execute(self, sim: Simulation) -> None: from bamengine.events._internal.planning import firms_decide_desired_labor firms_decide_desired_labor(sim.prod, sim.emp)
[docs] @event class FirmsDecideVacancies: """ Calculate number of job vacancies to post. Firms compare their desired labor force to their current labor force and post vacancies for the difference (if positive). Algorithm --------- For each firm i: .. math:: V_i = \\max(L^d_i - L_i, 0) where: - :math:`V`: number of vacancies to post - :math:`L^d`: desired labor (from FirmsDecideDesiredLabor) - :math:`L`: current labor force Examples -------- >>> import bamengine as be >>> sim = be.Simulation.init(n_firms=100, seed=42) >>> event = sim.get_event("firms_decide_vacancies") >>> event.execute(sim) >>> (sim.emp.n_vacancies >= 0).all() True >>> ( ... sim.emp.n_vacancies == sim.emp.desired_labor - sim.emp.current_labor ... ).all() # doctest: +SKIP True See Also -------- FirmsDecideDesiredLabor : Calculate labor requirements bamengine.events._internal.planning.firms_decide_vacancies : Implementation """
[docs] def execute(self, sim: Simulation) -> None: from bamengine.events._internal.planning import firms_decide_vacancies firms_decide_vacancies(sim.emp)
[docs] @event class FirmsFireExcessWorkers: """ Fire workers when current labor exceeds desired labor. Firms with more workers than needed (due to reduced production targets) lay off the excess workers. By default, firms fire workers randomly, but can be configured to fire the most expensive workers first. Algorithm --------- For each firm i with :math:`L_i > L^d_i` (current labor exceeds desired): 1. Calculate excess: :math:`E_i = L_i - L^d_i` 2. Select :math:`E_i` workers to fire (by method: random or expensive first) 3. Fire selected workers: - Set worker's employer = -1 (unemployed) - Set worker's wage = 0 - Set worker's fired flag = True - Decrement firm's current_labor Mathematical Notation --------------------- For firm i with :math:`L_i > L^d_i`: .. math:: E_i = L_i - L^d_i Fire :math:`E_i` workers so that: .. math:: L_i \\leftarrow L^d_i Examples -------- Execute this event: >>> import bamengine as be >>> sim = be.Simulation.init(n_firms=100, n_households=500, seed=42) >>> event = sim.get_event("firms_fire_excess_workers") >>> event.execute(sim) Check that firms now have current_labor <= desired_labor: >>> (sim.emp.current_labor <= sim.emp.desired_labor).all() True Notes ----- This event executes in the Planning phase after vacancies are calculated. Workers fired here have their `fired` flag set to True, which affects their job search behavior (loyalty rule does not apply). See Also -------- FirmsDecideVacancies : Calculate job openings FirmsFireWorkers : Fire workers due to financing gaps (credit market) bamengine.events._internal.planning.firms_fire_excess_workers : Implementation """
[docs] def execute(self, sim: Simulation) -> None: from bamengine.events._internal.planning import firms_fire_excess_workers firms_fire_excess_workers( sim.emp, sim.wrk, rng=sim.rng, )