Source code for bamengine.events.production

"""
Production events for wage payments, production execution, and contract management.

This module defines the production phase events that execute after credit market
events. Firms pay wages, workers receive income, firms produce goods using labor,
and employment contracts are updated (decrementing periods remaining).

Event Sequence
--------------
The production events execute in this order:

1. FirmsPayWages - Firms deduct wage bill from available funds
2. WorkersReceiveWage - Workers add wages to income
3. FirmsRunProduction - Firms produce goods using labor
4. WorkersUpdateContracts - Decrement contract duration, handle expiration

Note: FirmsCalcBreakevenPrice and FirmsAdjustPrice are activated via
``pricing_phase='production'`` config. The default pipeline uses planning-phase
pricing (FirmsPlanBreakevenPrice/FirmsPlanPrice in planning.py).

Design Notes
------------
- Events operate on producer, employer, worker, and consumer roles
- Production function: Y = φ × L (output = productivity × labor)
- Contract expiration: periods_left decremented each period, expires at 0
- Firms with zero labor produce zero output (marked for bankruptcy)
- Wage payments reduce firm funds but increase worker income (money transfer)

Examples
--------
Execute production events:

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

Execute individual production event:

>>> event = sim.get_event("firms_run_production")
>>> event.execute(sim)
>>> sim.prod.production.mean()  # doctest: +SKIP
52.5

Check production output:

>>> sim.prod.inventory.sum()  # doctest: +SKIP
5250.0

See Also
--------
bamengine.events._internal.production : System function implementations
Producer : Production state
Employer : Labor force state
Worker : Employment state
Consumer : Income 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 FirmsPayWages: """ Firms pay wages by deducting wage bill from available funds. Firms transfer funds to cover their wage obligations. This reduces firm cash (total_funds) by the wage bill amount. Workers receive these wages in the next event (WorkersReceiveWage). Algorithm --------- For each firm i: .. math:: A_i \\leftarrow A_i - W_i where :math:`A_i` = total_funds, :math:`W_i` = wage_bill. Examples -------- >>> import bamengine as be >>> sim = be.Simulation.init(n_firms=100, seed=42) >>> initial_funds = sim.emp.total_funds.copy() >>> event = sim.get_event("firms_pay_wages") >>> event.execute(sim) >>> # Funds reduced by wage bill >>> import numpy as np >>> reduction = initial_funds - sim.emp.total_funds >>> np.allclose(reduction, sim.emp.wage_bill) True Notes ----- This event must execute after FirmsFireWorkers (wage_bill finalized). See Also -------- WorkersReceiveWage : Workers receive wages (counterpart event) bamengine.events._internal.production.firms_pay_wages : Implementation """
[docs] def execute(self, sim: Simulation) -> None: from bamengine.events._internal.production import firms_pay_wages firms_pay_wages(sim.emp)
[docs] @event class WorkersReceiveWage: """ Workers receive wage income from employment. Employed workers add their wages to their income (Consumer role). This is the counterpart to FirmsPayWages: money flows from firms to households. Algorithm --------- For each employed worker j (:math:`\\text{employer}_j \\geq 0`): .. math:: I_j \\leftarrow I_j + w_j where :math:`I_j` = income, :math:`w_j` = wage. Examples -------- >>> import bamengine as be >>> sim = be.Simulation.init(n_households=500, seed=42) >>> initial_income = sim.con.income.copy() >>> event = sim.get_event("workers_receive_wage") >>> event.execute(sim) >>> # Employed workers gained income >>> import numpy as np >>> employed_mask = sim.wrk.employed >>> income_gain = sim.con.income - initial_income >>> np.allclose(income_gain[employed_mask], sim.wrk.wage[employed_mask]) True Notes ----- This event must execute after FirmsPayWages (firms have paid). Only employed workers receive wages. Unemployed workers gain no income. See Also -------- FirmsPayWages : Firms pay wages (counterpart event) bamengine.events._internal.production.workers_receive_wage : Implementation """
[docs] def execute(self, sim: Simulation) -> None: from bamengine.events._internal.production import workers_receive_wage workers_receive_wage(sim.con, sim.wrk)
[docs] @event class FirmsRunProduction: """ Firms produce goods using labor and add output to inventory. Production follows a simple linear technology: output equals labor productivity times number of workers. The produced goods are added to inventory for sale in the goods market. The calculated production is saved to both ``production`` (current period's output) and ``production_prev`` (for use as next period's planning signal). Algorithm --------- For each firm i: 1. Calculate output: :math:`Y_i = \\phi_i \\times L_i` 2. Store production: :math:`\\text{production}_i = Y_i` 3. Store for next period's planning: :math:`\\text{production\\_prev}_i = Y_i` 4. Add to inventory: :math:`S_i \\leftarrow Y_i` Mathematical Notation --------------------- .. math:: Y_i = \\phi_i \\times L_i S_i \\leftarrow Y_i where: - :math:`Y_i`: production output for firm i - :math:`\\phi_i`: labor productivity (output per worker) - :math:`L_i`: current labor force (number of workers) - :math:`S_i`: inventory (replaces previous inventory with current production) Examples -------- >>> import bamengine as be >>> sim = be.Simulation.init(n_firms=100, seed=42) >>> event = sim.get_event("firms_run_production") >>> event.execute(sim) Check production output: >>> sim.prod.production.mean() # doctest: +SKIP 52.5 Verify production formula: >>> import numpy as np >>> expected_output = sim.prod.labor_productivity * sim.emp.current_labor >>> np.allclose(sim.prod.production, expected_output) True Check inventory accumulation: >>> total_inventory = sim.prod.inventory.sum() >>> total_production = sim.prod.production.sum() >>> total_inventory >= total_production # Inventory includes previous unsold goods True Notes ----- This event must execute after WorkersReceiveWage. Firms with zero labor (L_i = 0) produce zero output and are marked for bankruptcy in later events. Inventory accumulates: S_new = S_old + Y. Unsold goods from previous periods remain in inventory. See Also -------- FirmsDecideDesiredProduction : Plans production targets Producer : Production state with labor_prod, production, inventory bamengine.events._internal.production.firms_run_production : Implementation """
[docs] def execute(self, sim: Simulation) -> None: from bamengine.events._internal.production import firms_run_production firms_run_production(sim.prod, sim.emp)
[docs] @event class WorkersUpdateContracts: """ Decrement contract duration and handle expiration for employed workers. Each employed worker's contract duration (periods_left) decrements by 1. Contracts that reach 0 expire: workers become unemployed but remain loyal (can reapply to previous employer). Firm labor counts are recalculated to reflect expired contracts. Algorithm --------- For each employed worker j: 1. Decrement contract: :math:`\\text{periods\\_left}_j \\leftarrow \\text{periods\\_left}_j - 1` 2. If :math:`\\text{periods\\_left}_j = 0`: - Set :math:`\\text{contract\\_expired}_j = \\text{True}` - Set :math:`\\text{employer\\_prev}_j = \\text{employer}_j` (store for loyalty) - Set :math:`\\text{employer}_j = -1` (unemployed) - Set :math:`w_j = 0` 3. Recalculate firm labor counts: - For each firm i: :math:`L_i = |\\{j : \\text{employer}_j = i\\}|` Mathematical Notation --------------------- For firm i after contract updates: .. math:: L_i = |\\{j : \\text{employer}_j = i\\}| Examples -------- >>> import bamengine as be >>> sim = be.Simulation.init(n_households=500, seed=42) >>> event = sim.get_event("workers_update_contracts") >>> event.execute(sim) Check contracts decremented: >>> import numpy as np >>> # All employed workers should have periods_left >= 0 >>> employed_mask = sim.wrk.employed >>> (sim.wrk.periods_left[employed_mask] >= 0).all() True Check expired contracts: >>> expired_mask = sim.wrk.contract_expired >>> expired_mask.sum() # doctest: +SKIP 12 Verify expired workers are unemployed: >>> (sim.wrk.employer[expired_mask] == -1).all() True Verify labor counts match: >>> worker_counts = np.bincount( ... sim.wrk.employer[sim.wrk.employed], minlength=sim.emp.current_labor.size ... ) >>> np.array_equal(worker_counts, sim.emp.current_labor) True Notes ----- This event must execute after FirmsRunProduction (end of period). Workers with expired contracts have contract_expired flag set, which triggers loyalty behavior in the next labor market phase (workers apply to previous employer first if not fired). Firm labor counts are recalculated to reflect contract expirations. Wage bills are NOT recalculated here — they retain the values from when wages were actually paid (FirmsCalcWageBill), ensuring that revenue-phase gross_profit correctly reflects actual expenses. See Also -------- LaborMarketRound : Sets initial contract duration WorkersDecideFirmsToApply : Uses contract_expired flag for loyalty Worker : Employment state with contract fields Employer : Labor force state with current_labor bamengine.events._internal.production.workers_update_contracts : Implementation """
[docs] def execute(self, sim: Simulation) -> None: from bamengine.events._internal.production import workers_update_contracts workers_update_contracts(sim.wrk, sim.emp)