Skip to content

feat: cadCAD economic simulations for M012-M015 parameter validation#58

Open
brawlaphant wants to merge 3 commits intoregen-network:mainfrom
brawlaphant:pr/cadcad-economic-simulations
Open

feat: cadCAD economic simulations for M012-M015 parameter validation#58
brawlaphant wants to merge 3 commits intoregen-network:mainfrom
brawlaphant:pr/cadcad-economic-simulations

Conversation

@brawlaphant
Copy link
Copy Markdown
Contributor

Summary

  • Implements a complete cadCAD 0.5.x simulation model for the Regen Economic Reboot (M012-M015), based on docs/economics/economic-simulation-spec.md
  • Provides 4 runnable scripts: baseline simulation, parameter sweeps, Monte Carlo runs, and 8 stress test scenarios
  • Includes closed-form equilibrium derivations validating the numerical simulation results
  • All code tested and verified to run with pip install -r requirements.txt && python run_baseline.py

Model Architecture

7 policy functions implement the full economic pipeline each epoch:

  1. P1: Credit Market — Generates transaction volume from agent populations (lognormal distributions)
  2. P2: Fee Collection (M013) — Calculates value-based fees across 4 transaction types
  3. P3: Fee Distribution (M013) — Routes fees to burn/validator/community/agent pools (30/40/25/5)
  4. P4: Mint/Burn (M012) — Computes regrowth and burning with cap enforcement
  5. P5: Validator Compensation (M014) — Distributes validator fund with 10% performance bonus
  6. P6: Contribution Rewards (M015) — Stability tier allocation + activity-based distribution
  7. P7: Agent Dynamics — Validator churn, stability adoption, GBM price model

Key Findings

Finding Value
Equilibrium supply S* ~219.85M REGEN (1.15M below cap)
Time to equilibrium ~2.4 years from activation
Min viable weekly volume $1.3M/week (current baseline $500K is insufficient for validators alone)
Wash trading break-even 7% reward rate (32x above baseline — deeply unprofitable)
Stress tests 7/8 pass; SC-001 (90% volume crash) correctly identifies validator income failure

Files (14 total)

File Purpose
simulations/cadcad/model/state_variables.py 44 state variables with initial values
simulations/cadcad/model/params.py 48 parameters, 5 sweep configs, 8 stress configs
simulations/cadcad/model/policies.py 7 policy functions (P1-P7)
simulations/cadcad/model/state_updates.py State update functions
simulations/cadcad/model/config.py cadCAD experiment configuration with 3 PSUBs
simulations/cadcad/run_baseline.py 260-epoch baseline with 6 success criteria
simulations/cadcad/run_sweep.py 5 parameter sweeps
simulations/cadcad/run_monte_carlo.py N-run Monte Carlo with CIs
simulations/cadcad/run_stress_tests.py 8 stress scenarios (SC-001 to SC-008)
simulations/cadcad/analysis.py Equilibrium analysis and summary tables
simulations/cadcad/equilibrium_analysis.md Closed-form equilibrium derivations
simulations/cadcad/README.md Usage guide and interpretation
simulations/cadcad/requirements.txt Python dependencies
simulations/cadcad/model/__init__.py Package docstring

Test plan

  • pip install -r requirements.txt installs all dependencies
  • python run_baseline.py completes in <1s, passes 5/5 measurable success criteria
  • python run_sweep.py --sweep volume_sweep --epochs 52 runs all 8 volume configs
  • python run_stress_tests.py --all --epochs 130 runs all 8 scenarios (7/8 pass)
  • python analysis.py --from-run produces equilibrium summary
  • python run_monte_carlo.py --runs 100 (quick MC test)
  • Review equilibrium derivations in equilibrium_analysis.md against spec formulas

🤖 Generated with Claude Code

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>
@gemini-code-assist
Copy link
Copy Markdown
Contributor

Summary of Changes

Hello, 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

  • Comprehensive Economic Simulation Model: Implemented a complete cadCAD 0.5.x simulation model for the Regen Economic Reboot (M012-M015), based on the detailed economic simulation specification.
  • Diverse Simulation Capabilities: Provided four distinct runnable scripts for various analyses: baseline simulation, parameter sweeps for sensitivity analysis, Monte Carlo runs for stochastic variation, and eight stress test scenarios to evaluate resilience.
  • Analytical Validation: Included closed-form equilibrium derivations to validate the numerical simulation results, ensuring the model's theoretical soundness.
  • Key Economic Insights: Derived critical findings such as the equilibrium supply (~219.85M REGEN), time to equilibrium (~2.4 years), minimum viable weekly volume ($1.3M/week for validator sustainability), and confirmed the deep unprofitability of wash trading.
Using Gemini Code Assist

The 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 /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

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 .gemini/ folder in the base of the repository. Detailed instructions can be found here.

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

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment on lines +84 to +267
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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

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.

Comment on lines +123 to +125
if vs < 0:
vs = 0.0
cs = 1.0 - bs - ags
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

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.

Suggested change
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
  1. The comment highlights an ambiguity and contradiction in the implementation of share allocation logic (validator_share, community_share) when validator_share is 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/
model/
__init__.py # Package docstring
state_variables.py # Initial state vector (37 variables)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Suggested change
state_variables.py # Initial state vector (37 variables)
state_variables.py # Initial state vector (44 variables)
References
  1. 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.

Comment on lines +163 to +164
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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.15M REGEN.
  • The epsilon for 1% convergence is 0.01 * S* ≈ 2.2M REGEN.

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.

Comment on lines +47 to +90
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)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

brawlaphant and others added 2 commits March 25, 2026 11:53
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant