Source code for extensions.rnd.events

"""R&D events for Growth+ extension.

This module provides the three custom events for the Growth+ scenario
from section 3.8 of Delli Gatti et al. (2011).

The extension adds endogenous productivity growth via R&D investment:
- Firms invest a portion of profits in R&D (sigma = RnD share)
- R&D intensity determines expected productivity gains
- Productivity increments are drawn from exponential distribution
- Higher financial fragility leads to lower R&D investment

Key Equations
-------------

**Productivity Evolution (Equation 3.15):**

.. math::

    \\alpha_{t+1} = \\alpha_t + z_t

Where :math:`z_t \\sim \\text{Exponential}(\\mu)` represents the productivity
increment drawn from an exponential distribution with scale parameter :math:`\\mu`.

**R&D Intensity (expected productivity gain):**

.. math::

    \\mu = \\sigma \\cdot \\frac{\\pi}{p \\cdot Y}

**R&D Share (parameterized):**

.. math::

    \\sigma = \\sigma_{min} + (\\sigma_{max} - \\sigma_{min}) \\cdot \\exp(k \\cdot \\text{fragility})

**Net Worth Evolution (Equation 3.16):**

.. math::

    A_t = A_{t-1} + (1-\\sigma)(1-\\delta)\\pi_{t-1}
"""

from __future__ import annotations

import bamengine as bam
from bamengine import event, ops


[docs] @event( name="firms_compute_rnd_intensity", after="firms_validate_debt_commitments", ) class FirmsComputeRnDIntensity: """Compute R&D share and intensity for firms. Calculates: - fragility = wage_bill / net_worth - sigma = sigma_min + (sigma_max - sigma_min) * exp(sigma_decay * fragility) - mu = sigma * net_profit / (price * production) Requires extension parameters: sigma_min, sigma_max, sigma_decay Firms with non-positive profits have sigma = 0 (no R&D). Note: This event is positioned after 'firms_validate_debt_commitments' (before 'firms_pay_dividends') via the ``@event(after=...)`` hook so that R&D deducts from net profit *before* dividend distribution. Apply with ``sim.use_events(*RND_EVENTS)``. """
[docs] def execute(self, sim: bam.Simulation) -> None: """Execute R&D intensity computation.""" bor = sim.get_role("Borrower") prod = sim.get_role("Producer") emp = sim.get_role("Employer") rnd = sim.get_role("RnD") # Access extension parameters directly via sim.param_name sigma_min = sim.sigma_min sigma_max = sim.sigma_max sigma_decay = sim.sigma_decay # Calculate fragility = W/A (wage_bill / net_worth) fragility = ops.divide(emp.wage_bill, bor.net_worth) # Guard: firms with non-positive net worth get zero fragility # (ops.divide preserves negative denominators, producing negative ratios) fragility = ops.where(ops.greater(bor.net_worth, 0.0), fragility, 0.0) # Store fragility ops.assign(rnd.fragility, fragility) # Calculate sigma = sigma_min + (sigma_max - sigma_min) * exp(sigma_decay * fragility) decay_factor = ops.exp(ops.multiply(sigma_decay, fragility)) sigma_range = sigma_max - sigma_min sigma = ops.add(sigma_min, ops.multiply(sigma_range, decay_factor)) # Set sigma = 0 for firms with non-positive net profit sigma = ops.where(ops.greater(bor.net_profit, 0.0), sigma, 0.0) ops.assign(rnd.sigma, sigma) # Calculate mu = sigma * net_profit / (price * production) revenue = ops.multiply(prod.price, prod.production) mu = ops.divide(ops.multiply(sigma, bor.net_profit), revenue) # Guard: firms with zero production get zero intensity mu = ops.where(ops.greater(prod.production, 0.0), mu, 0.0) # Clamp mu to non-negative range mu = ops.maximum(mu, 0.0) ops.assign(rnd.rnd_intensity, mu)
[docs] @event(after="firms_compute_rnd_intensity") class FirmsApplyProductivityGrowth: """Apply productivity growth based on R&D. For firms with positive R&D intensity (mu > 0): - Draw z from Exponential(scale=mu) - Update: labor_productivity += z This implements equation 3.15 from Macroeconomics from the Bottom-up. Note: This event is positioned after 'firms_compute_rnd_intensity' via the ``@event(after=...)`` hook. Apply with ``sim.use_events(*RND_EVENTS)``. """
[docs] def execute(self, sim: bam.Simulation) -> None: """Execute productivity growth.""" prod = sim.get_role("Producer") rnd = sim.get_role("RnD") # Draw productivity increments from exponential distribution # z ~ Exponential(scale=mu), where E[z] = mu # Only for firms with mu > 0 n_firms = sim.n_firms mu = rnd.rnd_intensity # Draw from exponential - use sim.rng for reproducibility # For firms with mu=0, we set z=0 z = ops.zeros(n_firms) active = ops.greater(mu, 0.0) if ops.any(active): # Draw from exponential with scale=mu for active firms # Note: sim.rng.exponential is proper RNG usage z[active] = sim.rng.exponential(scale=mu[active]) # Store the increment ops.assign(rnd.productivity_increment, z) # Apply to labor productivity: alpha_{t+1} = alpha_t + z new_productivity = ops.add(prod.labor_productivity, z) ops.assign(prod.labor_productivity, new_productivity)
[docs] @event(after="firms_apply_productivity_growth") class FirmsDeductRnDExpenditure: """Adjust net profit for R&D expenditure. Modifies net profit before dividend distribution: - new_net_profit = old_net_profit * (1 - sigma) This implements the (1-sigma) factor in equation 3.16. By reducing net_profit before ``firms_pay_dividends``, dividends correctly equal delta * (1-sigma) * pi, matching book Section 3.8. Note: This event is positioned after 'firms_apply_productivity_growth' via the ``@event(after=...)`` hook. Apply with ``sim.use_events(*RND_EVENTS)``. """
[docs] def execute(self, sim: bam.Simulation) -> None: """Execute R&D expenditure deduction.""" bor = sim.get_role("Borrower") rnd = sim.get_role("RnD") # Adjust net profit: multiply by (1 - sigma) # This captures the R&D expenditure before dividend distribution one_minus_sigma = ops.subtract(1.0, rnd.sigma) new_net_profit = ops.multiply(bor.net_profit, one_minus_sigma) ops.assign(bor.net_profit, new_net_profit)