Source code for validation.robustness.structural

"""Structural experiments (Section 3.10.2).

Implements two structural experiments that test model mechanisms:

1. **Preferential Attachment (PA) experiment**: Disable consumer loyalty
   and show that volatility drops and deep crises vanish. Then sweep Z
   with PA off.

2. **Entry neutrality experiment**: Apply heavy profit taxation without
   redistribution to increase bankruptcies, confirming that the automatic
   firm entry mechanism does NOT artificially drive recovery.
"""

from __future__ import annotations

from collections.abc import Callable
from dataclasses import dataclass
from typing import Any

from validation.robustness.internal_validity import (
    InternalValidityResult,
    run_internal_validity,
)
from validation.robustness.sensitivity import (
    SensitivityResult,
    run_sensitivity_analysis,
)


[docs] @dataclass class PAExperimentResult: """Result of the preferential attachment experiment (Section 3.10.2). Attributes ---------- pa_off_validity : InternalValidityResult Internal validity with PA disabled (consumer_matching="random"). pa_off_z_sweep : SensitivityResult Z-sweep sensitivity with PA disabled. baseline_validity : InternalValidityResult or None Optional baseline (PA on) for comparison. """ pa_off_validity: InternalValidityResult pa_off_z_sweep: SensitivityResult baseline_validity: InternalValidityResult | None = None
[docs] @dataclass class EntryExperimentResult: """Result of the entry neutrality experiment (Section 3.10.2). Attributes ---------- tax_sweep : SensitivityResult Sensitivity analysis sweeping profit_tax_rate. """ tax_sweep: SensitivityResult
[docs] def run_pa_experiment( n_seeds: int = 20, n_periods: int = 1000, burn_in: int = 500, n_workers: int = 10, verbose: bool = True, include_baseline: bool = True, setup_hook: Callable[..., None] | None = None, collect_config: dict[str, Any] | None = None, **config_overrides: Any, ) -> PAExperimentResult: """Run the preferential attachment experiment (Section 3.10.2). Phase 1: Run internal validity with PA disabled to show volatility drops and deep crises vanish. Phase 2: Run Z-sweep sensitivity with PA disabled. Optionally runs baseline (PA on) for comparison. Parameters ---------- n_seeds : int Number of random seeds. n_periods : int Simulation periods per seed. burn_in : int Burn-in periods to discard. n_workers : int Parallel workers. verbose : bool Print progress. include_baseline : bool Also run baseline (PA on) for comparison. setup_hook : callable or None Global setup hook (e.g. Growth+ R&D). collect_config : dict or None Custom collection config. **config_overrides Additional config overrides. Returns ------- PAExperimentResult """ if verbose: print("\n" + "=" * 60) print(" PA EXPERIMENT (Section 3.10.2)") print("=" * 60) # Phase 1: Internal validity with PA off if verbose: print("\n--- Phase 1: Internal validity (PA off) ---") pa_off_validity = run_internal_validity( n_seeds=n_seeds, n_periods=n_periods, burn_in=burn_in, n_workers=n_workers, verbose=verbose, setup_hook=setup_hook, collect_config=collect_config, consumer_matching="random", **config_overrides, ) # Phase 2: Z-sweep with PA off if verbose: print("\n--- Phase 2: Z-sweep sensitivity (PA off) ---") pa_off_z_sweep = run_sensitivity_analysis( experiments=["goods_market_no_pa"], n_seeds=n_seeds, n_periods=n_periods, burn_in=burn_in, n_workers=n_workers, verbose=verbose, setup_hook=setup_hook, collect_config=collect_config, **config_overrides, ) # Optional baseline for comparison baseline_validity = None if include_baseline: if verbose: print("\n--- Baseline: Internal validity (PA on) ---") baseline_validity = run_internal_validity( n_seeds=n_seeds, n_periods=n_periods, burn_in=burn_in, n_workers=n_workers, verbose=verbose, setup_hook=setup_hook, collect_config=collect_config, **config_overrides, ) return PAExperimentResult( pa_off_validity=pa_off_validity, pa_off_z_sweep=pa_off_z_sweep, baseline_validity=baseline_validity, )
[docs] def run_entry_experiment( n_seeds: int = 20, n_periods: int = 1000, burn_in: int = 500, n_workers: int = 10, verbose: bool = True, setup_hook: Callable[..., None] | None = None, collect_config: dict[str, Any] | None = None, **config_overrides: Any, ) -> EntryExperimentResult: """Run the entry neutrality experiment (Section 3.10.2). Sweeps profit_tax_rate from 0% to 90% to show monotonic degradation, confirming that the automatic firm entry mechanism does NOT artificially drive recovery. Parameters ---------- n_seeds : int Number of random seeds. n_periods : int Simulation periods per seed. burn_in : int Burn-in periods to discard. n_workers : int Parallel workers. verbose : bool Print progress. setup_hook : callable or None Global setup hook (e.g. Growth+ R&D). collect_config : dict or None Custom collection config. **config_overrides Additional config overrides. Returns ------- EntryExperimentResult """ if verbose: print("\n" + "=" * 60) print(" ENTRY NEUTRALITY EXPERIMENT (Section 3.10.2)") print("=" * 60) tax_sweep = run_sensitivity_analysis( experiments=["entry_neutrality"], n_seeds=n_seeds, n_periods=n_periods, burn_in=burn_in, n_workers=n_workers, verbose=verbose, setup_hook=setup_hook, collect_config=collect_config, **config_overrides, ) return EntryExperimentResult(tax_sweep=tax_sweep)