feat: cadCAD economic simulations for M012-M015 parameter validation#58
feat: cadCAD economic simulations for M012-M015 parameter validation#58brawlaphant wants to merge 3 commits intoregen-network:mainfrom
Conversation
Implements the complete simulation model specified in docs/economics/economic-simulation-spec.md using cadCAD 0.5.x. Model structure: - 7 policy functions (credit market, fee collection, fee distribution, mint/burn, validator compensation, contribution rewards, agent dynamics) - 44 state variables tracking supply, fees, pools, validators, rewards - 48 parameters with baseline values, sweep ranges, and stress configs Runners: - run_baseline.py: 260-epoch baseline with success criteria evaluation - run_sweep.py: Parameter sweeps across r_base, burn_share, fee rates, stability rate, and weekly volume - run_monte_carlo.py: N-run stochastic simulation with confidence intervals - run_stress_tests.py: 8 adversarial/failure scenarios (SC-001 to SC-008) - analysis.py: Closed-form equilibrium calculations and summary statistics Key findings from equilibrium analysis: - Equilibrium supply S* ≈ 219.85M REGEN (1.15M below cap) - Minimum viable weekly volume for validator sustainability: $1.3M/week - Wash trading break-even at 7% reward rate (32x above baseline) - System is asymptotically stable with self-correcting feedback Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request introduces a robust economic simulation framework for the Regen Network's M012-M015 mechanisms. It enables in-depth analysis of the network's economic policies, allowing for parameter validation, sensitivity testing, and stress scenario evaluation. The model provides crucial insights into the long-term stability and sustainability of the Regen economy, informing future policy decisions. Highlights
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request introduces a comprehensive cadCAD economic simulation model for Regen M012-M015, including model definition, parameters, policies, state updates, and initial state. It also provides various runner scripts for baseline, Monte Carlo, parameter sweeps, and stress tests, along with extensive documentation. Key feedback highlights a critical architectural issue in run_stress_tests.py due to manual re-implementation of the simulation loop, posing maintainability risks. A high-severity bug was identified in model/params.py concerning incorrect validator_share calculation during the burn_share_sweep. Additionally, there are medium-severity issues including an outdated state variable count in README.md, an error in the time to convergence calculation in equilibrium_analysis.md, and duplicated logic for periods_near_equilibrium in model/state_updates.py.
| def run_stress_scenario(scenario_id, T=260, seed=42): | ||
| """ | ||
| Run a single stress test scenario. | ||
|
|
||
| Instead of modifying cadCAD mid-run (which is complex), we run the full | ||
| simulation with baseline parameters but hook into the policy functions | ||
| via modified parameters that encode the schedule. | ||
|
|
||
| For simplicity, we run the simulation epoch by epoch, injecting state | ||
| overrides at schedule boundaries. | ||
| """ | ||
| scenario = stress_test_params[scenario_id] | ||
| np.random.seed(seed) | ||
|
|
||
| print(f"\n Scenario: {scenario['name']}") | ||
| print(f" Description: {scenario['description']}") | ||
|
|
||
| # We run the full simulation with a wrapper that modifies state per-epoch. | ||
| # To keep it simple with cadCAD, we run a standard simulation and post-process. | ||
| # The stress effects are modeled by adjusting the initial conditions in params. | ||
|
|
||
| # Build full params with schedule-aware overrides | ||
| sim_params = copy.deepcopy(baseline_params) | ||
| sim_params.update(scenario.get('overrides', {})) | ||
|
|
||
| # For stress tests with volume schedules, we embed the schedule in params | ||
| sim_params['_stress_scenario'] = scenario_id | ||
| sim_params['_volume_schedule'] = scenario.get('volume_schedule', None) | ||
| sim_params['_churn_schedule'] = scenario.get('churn_schedule', None) | ||
| sim_params['_wash_trader_schedule'] = scenario.get('wash_trader_schedule', None) | ||
| sim_params['_eco_mult_schedule'] = scenario.get('eco_mult_schedule', None) | ||
| sim_params['_price_crash_epoch'] = scenario.get('price_crash_epoch', None) | ||
| sim_params['_price_crash_factor'] = scenario.get('price_crash_factor', None) | ||
| sim_params['_bank_run_epoch'] = scenario.get('stability_bank_run_epoch', None) | ||
| sim_params['_bank_run_exit_fraction'] = scenario.get('bank_run_exit_fraction', None) | ||
|
|
||
| # Run epoch-by-epoch simulation with stress injection | ||
| state = copy.deepcopy(initial_state) | ||
| state['timestep'] = 0 | ||
| records = [copy.deepcopy(state)] | ||
|
|
||
| for epoch in range(1, T + 1): | ||
| state['timestep'] = epoch | ||
|
|
||
| # --- Inject stress conditions --- | ||
|
|
||
| # Volume schedule | ||
| if sim_params['_volume_schedule'] is not None: | ||
| target_vol = _get_volume_for_epoch( | ||
| sim_params['_volume_schedule'], epoch, | ||
| baseline_params['initial_weekly_volume_usd'] | ||
| ) | ||
| # Scale agent counts to approximate target volume | ||
| vol_ratio = target_vol / max(baseline_params['initial_weekly_volume_usd'], 1) | ||
| state['num_buyers'] = max(5, int(initial_state['num_buyers'] * vol_ratio)) | ||
| state['num_issuers'] = max(3, int(initial_state['num_issuers'] * vol_ratio)) | ||
| state['num_retirees'] = max(3, int(initial_state['num_retirees'] * vol_ratio)) | ||
|
|
||
| # Churn schedule | ||
| if sim_params['_churn_schedule'] is not None: | ||
| churn = _get_schedule_value(sim_params['_churn_schedule'], epoch, | ||
| baseline_params['base_validator_churn']) | ||
| sim_params['base_validator_churn'] = churn | ||
|
|
||
| # Wash trader schedule | ||
| if sim_params['_wash_trader_schedule'] is not None: | ||
| wt = _get_schedule_value(sim_params['_wash_trader_schedule'], epoch, 0) | ||
| state['num_wash_traders'] = int(wt) | ||
|
|
||
| # Ecological multiplier schedule | ||
| if sim_params['_eco_mult_schedule'] is not None: | ||
| em = _get_schedule_value(sim_params['_eco_mult_schedule'], epoch, 1.0) | ||
| state['ecological_multiplier'] = em | ||
|
|
||
| # Price crash | ||
| if (sim_params['_price_crash_epoch'] is not None and | ||
| epoch == sim_params['_price_crash_epoch']): | ||
| state['regen_price_usd'] *= sim_params['_price_crash_factor'] | ||
|
|
||
| # Stability bank run | ||
| if (sim_params['_bank_run_epoch'] is not None and | ||
| epoch == sim_params['_bank_run_epoch']): | ||
| exit_frac = sim_params['_bank_run_exit_fraction'] | ||
| state['stability_committed'] *= (1.0 - exit_frac) | ||
|
|
||
| # --- Run one epoch --- | ||
| # We simulate one step by running a cadCAD config of T=1 | ||
| # This is equivalent to stepping the model forward once. | ||
| from model.policies import ( | ||
| p_credit_market, p_fee_collection, p_fee_distribution, | ||
| p_mint_burn, p_validator_compensation, p_contribution_rewards, | ||
| p_agent_dynamics, | ||
| ) | ||
|
|
||
| # P1: Credit market | ||
| market = p_credit_market(sim_params, 0, [], state) | ||
|
|
||
| # P2: Fee collection | ||
| fees = p_fee_collection(sim_params, 0, [], state, market) | ||
|
|
||
| # P3: Fee distribution | ||
| dist = p_fee_distribution(sim_params, 0, [], state, fees) | ||
|
|
||
| # Update pool state | ||
| state['burn_pool_balance'] = dist['burn_allocation'] | ||
| state['validator_fund_balance'] = dist['validator_allocation'] | ||
| state['community_pool_balance'] = dist['community_allocation'] | ||
| state['agent_infra_balance'] = dist['agent_allocation'] | ||
| state['total_fees_collected'] = fees['total_fees_regen'] | ||
| state['total_fees_usd'] = fees['total_fees_usd'] | ||
| state['cumulative_fees'] += fees['total_fees_regen'] | ||
|
|
||
| # Store transaction data | ||
| state['issuance_count'] = market['issuance_count'] | ||
| state['trade_count'] = market['trade_count'] | ||
| state['retirement_count'] = market['retirement_count'] | ||
| state['transfer_count'] = market['transfer_count'] | ||
| state['issuance_value_usd'] = market['issuance_value_usd'] | ||
| state['trade_value_usd'] = market['trade_value_usd'] | ||
| state['retirement_value_usd'] = market['retirement_value_usd'] | ||
| state['transfer_value_usd'] = market['transfer_value_usd'] | ||
| state['total_volume_usd'] = market['total_volume_usd'] | ||
| state['credit_volume_weekly_usd'] = market['total_volume_usd'] | ||
|
|
||
| # P4: Mint/burn | ||
| pool_input = { | ||
| 'burn_allocation': dist['burn_allocation'], | ||
| 'validator_allocation': dist['validator_allocation'], | ||
| 'community_allocation': dist['community_allocation'], | ||
| 'issuance_value_usd': market['issuance_value_usd'], | ||
| 'retirement_value_usd': market['retirement_value_usd'], | ||
| 'trade_value_usd': market['trade_value_usd'], | ||
| } | ||
| mint_burn = p_mint_burn(sim_params, 0, [], state, pool_input) | ||
| state['S'] = mint_burn['new_S'] | ||
| state['M_t'] = mint_burn['M_t'] | ||
| state['B_t'] = mint_burn['B_t'] | ||
| state['cumulative_minted'] += mint_burn['M_t'] | ||
| state['cumulative_burned'] += mint_burn['B_t'] | ||
| state['r_effective'] = mint_burn['r_effective'] | ||
|
|
||
| # Supply state machine | ||
| threshold = sim_params['equilibrium_threshold'] | ||
| req_periods = sim_params['equilibrium_periods'] | ||
| S = state['S'] | ||
| if S > 0 and abs(state['M_t'] - state['B_t']) < threshold * S: | ||
| state['periods_near_equilibrium'] += 1 | ||
| else: | ||
| state['periods_near_equilibrium'] = 0 | ||
|
|
||
| if state['supply_state'] == 'TRANSITION' and state['B_t'] > 0: | ||
| state['supply_state'] = 'DYNAMIC' | ||
| elif state['supply_state'] == 'DYNAMIC' and state['periods_near_equilibrium'] >= req_periods: | ||
| state['supply_state'] = 'EQUILIBRIUM' | ||
| elif state['supply_state'] == 'EQUILIBRIUM': | ||
| if S > 0 and abs(state['M_t'] - state['B_t']) >= threshold * S: | ||
| state['supply_state'] = 'DYNAMIC' | ||
| state['periods_near_equilibrium'] = 0 | ||
|
|
||
| # P5: Validator compensation | ||
| val_comp = p_validator_compensation(sim_params, 0, [], state, pool_input) | ||
| state['validator_income_period'] = val_comp['validator_income_period'] | ||
| state['validator_income_annual'] = val_comp['validator_income_annual'] | ||
| state['validator_income_usd'] = val_comp['validator_income_usd'] | ||
|
|
||
| # P6: Contribution rewards | ||
| rewards = p_contribution_rewards(sim_params, 0, [], state, pool_input) | ||
| state['stability_allocation'] = rewards['stability_allocation'] | ||
| state['activity_pool'] = rewards['activity_pool'] | ||
| state['total_activity_score'] = rewards['total_activity_score'] | ||
| state['reward_per_unit_activity'] = rewards['reward_per_unit_activity'] | ||
| state['stability_utilization'] = rewards['stability_utilization'] | ||
|
|
||
| # P7: Agent dynamics | ||
| agent_input = {'validator_income_usd': val_comp['validator_income_usd']} | ||
| agent = p_agent_dynamics(sim_params, 0, [], state, agent_input) | ||
| state['active_validators'] = agent['new_active_validators'] | ||
| state['stability_committed'] = agent['new_stability_committed'] | ||
| state['regen_price_usd'] = agent['new_regen_price_usd'] | ||
|
|
||
| records.append(copy.deepcopy(state)) | ||
|
|
||
| df = pd.DataFrame(records) | ||
| return df |
There was a problem hiding this comment.
This script manually re-implements the entire simulation execution loop (lines 120-267) instead of using the cadCAD engine or the defined state update functions from model/state_updates.py. This is a significant maintainability risk and can lead to divergence between the stress test model and the baseline model. Any changes to the model's PSUB structure, policies, or state updates in the core model package will not be reflected here, potentially invalidating the stress test results.
It is highly recommended to refactor this script to run the simulation step-by-step using the cadCAD engine, or at the very least, to call the state update functions from state_updates.py instead of manually updating the state dictionary. This will ensure that the stress tests are always testing the canonical model implementation.
For example, you could structure the loop to build and execute a 1-timestep cadCAD experiment for each epoch, passing the updated state from the previous epoch as the new initial state.
simulations/cadcad/model/params.py
Outdated
| if vs < 0: | ||
| vs = 0.0 | ||
| cs = 1.0 - bs - ags |
There was a problem hiding this comment.
The logic to handle negative validator_share (vs) in the burn_share_sweep seems incorrect. The comment on line 119 states "Keep community and agent shares fixed; validator gets the remainder", but the code on line 125 modifies community_share (cs) if vs is negative. This contradicts the comment and could lead to unexpected behavior in sweeps with high burn_share values.
To adhere to the stated intent, you should either raise an error for invalid share combinations or ensure the logic correctly reflects the documented behavior. For the current sweep range, vs is not negative, but this should be fixed for robustness.
| if vs < 0: | |
| vs = 0.0 | |
| cs = 1.0 - bs - ags | |
| if vs < 0: | |
| raise ValueError(f"Invalid share combination for burn_share={bs}. Validator share would be negative.") |
References
- The comment highlights an ambiguity and contradiction in the implementation of share allocation logic (
validator_share,community_share) whenvalidator_shareis negative. This violates the principle of explicitly defining transition logic and state-based conditions to prevent implementation ambiguity, as the code's behavior deviates from its stated intent.
simulations/cadcad/README.md
Outdated
| simulations/cadcad/ | ||
| model/ | ||
| __init__.py # Package docstring | ||
| state_variables.py # Initial state vector (37 variables) |
There was a problem hiding this comment.
The documentation states there are 37 state variables, but there are actually 44 in state_variables.py. Please update the README to reflect the correct number of state variables for accuracy.
| state_variables.py # Initial state vector (37 variables) | |
| state_variables.py # Initial state vector (44 variables) |
References
- This comment addresses an inconsistency between the documented number of state variables and the actual implementation, which impacts document clarity and the accuracy of state definitions. Aligning the README with the code ensures that state definitions are accurately represented in the primary documentation, as per the rule to co-locate all state definitions and transitions within the primary state machine documentation.
| For 1% convergence (epsilon = 0.01 * S*): | ||
| t = log(2.2M / 1.15M) / log(0.974) = log(1.91) / (-0.0263) = 0.648 / 0.0263 ≈ 25 periods |
There was a problem hiding this comment.
There appears to be an error in the time to convergence calculation for the 1% convergence case.
- The initial gap after burn-down is
|221M - 219.85M| = 1.15MREGEN. - The epsilon for 1% convergence is
0.01 * S* ≈ 2.2MREGEN.
Since the initial gap is smaller than epsilon, the system is already within the 1% convergence band, and the time to convergence should be 0.
The formula log(2.2M / 1.15M) seems to have the fraction inverted, and it doesn't account for the case where the system starts within the convergence band. Please review and correct the analysis.
| def s_supply_state(params, substep, state_history, prev_state, policy_input): | ||
| """Update supply state machine (TRANSITION -> DYNAMIC -> EQUILIBRIUM).""" | ||
| M_t = policy_input['M_t'] | ||
| B_t = policy_input['B_t'] | ||
| S = policy_input['new_S'] | ||
| threshold = params['equilibrium_threshold'] | ||
| required_periods = params['equilibrium_periods'] | ||
|
|
||
| current_state = prev_state['supply_state'] | ||
| periods_near_eq = prev_state['periods_near_equilibrium'] | ||
|
|
||
| # Check if near equilibrium: |M - B| < threshold * S | ||
| if S > 0 and abs(M_t - B_t) < threshold * S: | ||
| periods_near_eq += 1 | ||
| else: | ||
| periods_near_eq = 0 | ||
|
|
||
| # State transitions | ||
| if current_state == 'TRANSITION': | ||
| if B_t > 0: # First burn occurred | ||
| current_state = 'DYNAMIC' | ||
| elif current_state == 'DYNAMIC': | ||
| if periods_near_eq >= required_periods: | ||
| current_state = 'EQUILIBRIUM' | ||
| elif current_state == 'EQUILIBRIUM': | ||
| if S > 0 and abs(M_t - B_t) >= threshold * S: | ||
| current_state = 'DYNAMIC' | ||
| periods_near_eq = 0 | ||
|
|
||
| return ('supply_state', current_state) | ||
|
|
||
|
|
||
| def s_periods_near_equilibrium(params, substep, state_history, prev_state, policy_input): | ||
| """Track consecutive near-equilibrium periods.""" | ||
| M_t = policy_input['M_t'] | ||
| B_t = policy_input['B_t'] | ||
| S = policy_input['new_S'] | ||
| threshold = params['equilibrium_threshold'] | ||
|
|
||
| periods = prev_state['periods_near_equilibrium'] | ||
| if S > 0 and abs(M_t - B_t) < threshold * S: | ||
| return ('periods_near_equilibrium', periods + 1) | ||
| else: | ||
| return ('periods_near_equilibrium', 0) |
There was a problem hiding this comment.
The logic for calculating periods_near_equilibrium is duplicated in s_supply_state (lines 58-62) and s_periods_near_equilibrium (lines 86-90). This creates a maintainability issue, as any change would need to be applied in two places.
Consider refactoring this logic into a shared helper function to avoid duplication and improve clarity.
The baseline simulation uses SPEC Model A (30% burn) but the governance proposals recommend 15% burn. Document this in params.py and add a governance-variant equilibrium derivation showing S* ≈ 220.42M at 15% burn vs 219.85M at 30% burn. The sweep already covers both values. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…work#58) - Refactor run_stress_tests.py to use cadCAD Executor instead of hand-rolling the simulation loop; stress schedules are now injected via params and interpreted by stress-aware composite policy functions - Fix burn_share_sweep: redistribute validator/community/agent shares proportionally when burn_share varies, preventing negative shares - Fix state variable count in README (37 -> 44 actual variables) - Fix convergence time calculation in equilibrium_analysis.md: the 1% epsilon (2.2M) exceeds the gap (1.15M) so convergence is immediate at end of burn-down; corrected total time to 3.1 years at 0.1% - Consolidate duplicated periods_near_equilibrium logic into shared _compute_periods_near_equilibrium() helper in state_updates.py Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
docs/economics/economic-simulation-spec.mdpip install -r requirements.txt && python run_baseline.pyModel Architecture
7 policy functions implement the full economic pipeline each epoch:
Key Findings
Files (14 total)
simulations/cadcad/model/state_variables.pysimulations/cadcad/model/params.pysimulations/cadcad/model/policies.pysimulations/cadcad/model/state_updates.pysimulations/cadcad/model/config.pysimulations/cadcad/run_baseline.pysimulations/cadcad/run_sweep.pysimulations/cadcad/run_monte_carlo.pysimulations/cadcad/run_stress_tests.pysimulations/cadcad/analysis.pysimulations/cadcad/equilibrium_analysis.mdsimulations/cadcad/README.mdsimulations/cadcad/requirements.txtsimulations/cadcad/model/__init__.pyTest plan
pip install -r requirements.txtinstalls all dependenciespython run_baseline.pycompletes in <1s, passes 5/5 measurable success criteriapython run_sweep.py --sweep volume_sweep --epochs 52runs all 8 volume configspython run_stress_tests.py --all --epochs 130runs all 8 scenarios (7/8 pass)python analysis.py --from-runproduces equilibrium summarypython run_monte_carlo.py --runs 100(quick MC test)equilibrium_analysis.mdagainst spec formulas🤖 Generated with Claude Code