A simple-as-possible stylized representation of the tradeoff between investment in income redistribution versus investment in emissions abatement.
- Overview
- Installation and Requirements
- Quick Start
- Analyzing Results
- Model Overview
- Basic Usage
- Output Files
- Running Multiple Cases in Parallel
- Project Structure
- Documentation
- References
- License
- Authors
This project develops a highly stylized model of an economy with income inequality, where a specified fraction of gross production is allocated to social good. The central question is how to optimally allocate resources between two competing objectives:
- Income redistribution - reducing inequality by transferring income from high-income to low-income individuals
- Emissions abatement - reducing carbon emissions to mitigate future climate damage
The model extends the COIN framework presented in Caldeira et al. (2023) to incorporate income inequality and diminishing marginal utility of income.
For detailed technical documentation, see README_DETAIL.md.
- Python 3.8 or higher
- pip package manager
Install all required Python packages:
pip install -r requirements.txtRequired packages include:
- numpy - numerical computations
- scipy - scientific computing and optimization
- matplotlib - plotting and visualization
- nlopt - nonlinear optimization
- openpyxl - Excel file generation
Test the model with an example configuration:
python scripts/test_integration.py test_f-f-f-t-t.jsonThis will:
- Load the configuration
- Run the forward integration over the specified time period
- Display results summary
- Generate timestamped output directory with CSV data and PDF plots
Optimize the allocation between redistribution and abatement:
python scripts/run_optimization.py test_f-f-f-t-t.jsonThis will find the optimal time trajectory of the control variable f(t) (fraction allocated to abatement vs. redistribution) that maximizes the discounted time-integral of aggregate utility.
This section covers tools for post-processing and analyzing optimization results.
Use compare_results.py to compare results across multiple optimization runs:
python scripts/compare_results.py "data/output/scenario_*/"The script accepts directory paths or glob patterns and generates:
optimization_comparison_summary.xlsx- Optimization metrics by iterationresults_comparison_summary.xlsx- Time series results for all variablescomparison_plots.pdf- PDF report with comparative visualizations (full time range)comparison_plots_2025-2100.pdf- PDF report focused on 2025-2100 period
Example comparing specific runs:
python scripts/compare_results.py data/output/config_011_f-f-f-f-f_25_1_100k_el_* data/output/config_011_t-f-t-f-f_25_1_100k_el_*Use calculate_scc.py to compute the Social Cost of Carbon (SCC) from optimization results using the perturbation method:
python scripts/calculate_scc.py --baseline data/output/config_019_..._YYYYMMDD-HHMMSS/ --pulse-year 2025This script:
- Loads optimized control trajectories (f and s) from a baseline optimization run
- Runs three integrations: baseline, +emission pulse, +consumption pulse
- Computes SCC from welfare differences using the formula: SCC = -m_E / m_C
The SCC represents the marginal welfare cost of emitting one additional tonne of CO2, expressed in dollars. See README_DETAIL.md for the detailed methodology.
Options:
--pulse-year: Year to apply perturbation pulses--emission-amount: Emission pulse size in tCO2 (default: from config)--consumption-amount: Consumption pulse size in $ (default: from config)--sensitivity-test: Run pulse-size and time-shift invariance tests--scaling-factors: Comma-separated scaling factors for sensitivity test (default: "1")
Use batch_scc.py to calculate SCC for multiple result directories at once. The script runs a cross-product of pulse years × scaling factors for comprehensive sensitivity analysis.
# Default: process all directories in data/output/reference/
python scripts/batch_scc.py
# Process specific directories with wildcards
python scripts/batch_scc.py "data/output/config_022_*"
# Multiple pulse years and scaling factors (runs 3×3 = 9 combinations per directory)
python scripts/batch_scc.py --pulse-years "2025,2050,2100" --scaling-factors "0.001,1,1000"
# Save detailed integration results as CSV files
python scripts/batch_scc.py --save-integration-resultsOptions:
--pulse-years: Comma-separated years to apply pulses (default: "2025")--pulse-year: Single year (legacy, same as --pulse-years with one value)--scaling-factors: Comma-separated scaling factors (default: "1")--emission-amount: Emission pulse size in tCO2 (default: from config)--consumption-amount: Consumption pulse size in $ (default: from config)--save-integration-results: Save full integration results as CSV files
Output is saved to a timestamped directory data/output/scc_YYYYMMDD-HHMMSS/ containing:
scc_results.xlsx: Consolidated SCC results from all directories*_results.csv: Integration results for each scenario (if --save-integration-results)
Use compare_optimization_convergence.py to analyze how well different optimization runs have converged:
python scripts/compare_optimization_convergence.pyThis script:
- Scans
data/output/for optimization result directories matching specified patterns - Loads terminal output to extract final objective values
- Computes RMS differences in control trajectories (f and s) relative to baseline cases
- Generates a summary CSV with convergence statistics
The output CSV includes:
- Flag pattern and configuration details
- Objective function values and departure from best result
- RMS differences in f and s trajectories vs. baseline
- Mean, standard deviation, and median of control variables
This is useful for:
- Identifying which runs have converged to similar solutions
- Comparing optimization quality across different configurations
- Detecting outlier runs that may need re-optimization
The model optimizes the time-integral of aggregate utility by choosing the allocation fraction f(t) between emissions abatement and income redistribution:
max∫₀^∞ e^(-ρt) · U(t) · L(t) dt, subject to 0 ≤ f(t) ≤ 1
where:
- ρ = pure rate of time preference
- U(t) = mean utility of the population at time t
- L(t) = population at time t
- f(t) = fraction of resources allocated to abatement (control variable)
The model combines three subsystems:
-
Economic Model (Solow-Swan Growth)
- Cobb-Douglas production function
- Capital accumulation with depreciation
- Climate damage reducing output
- Income distribution (Pareto-Lorenz or Empirical Lorenz)
-
Climate Model
- Temperature proportional to cumulative emissions
- Industrial emissions from economic activity (scaled by emission_ratio for non-CO2 GHGs)
- Exogenous land-use emissions (Eland)
- Abatement reducing emissions
-
Utility and Inequality
- CRRA (isoelastic) utility function
- Income-dependent climate damage
- Redistribution via progressive taxation
The model supports an empirical Lorenz curve formulation as an alternative to the Pareto-Lorenz distribution. The base empirical Lorenz curve is defined as:
L_base(F) = w₀·F^p₀ + w₁·F^p₁ + w₂·F^p₂ + w₃·F^p₃
where w₀ = (1 - w₁ - w₂ - w₃), and the parameters are:
| Parameter | Value |
|---|---|
| p₀ | 1.500036 |
| w₁ | 0.3776187268483524 |
| p₁ | 4.367440 |
| w₂ | 0.3671247620949191 |
| p₂ | 14.072005 |
| w₃ | 0.09538538350961864 |
| p₃ | 135.059674 |
The base Gini coefficient is computed as:
Gini_base = 1 - 2·[w₀/(p₀+1) + w₁/(p₁+1) + w₂/(p₂+1) + w₃/(p₃+1)]
To construct a Lorenz curve for an arbitrary Gini coefficient G, we use linear interpolation between perfect equality and the base curve:
L(F) = (1 - G/Gini_base)·F + (G/Gini_base)·L_base(F)
This formulation is controlled by the use_empirical_lorenz boolean parameter in the configuration.
-
Redistribution vs. Climate Action Tradeoff: Resources allocated to income redistribution provide immediate utility gains (especially with high inequality aversion), while emissions abatement provides future benefits by reducing climate damage.
-
Diminishing Marginal Utility: Higher values of η (risk aversion) mean that redistributing income from rich to poor has greater utility benefits, favoring redistribution over abatement.
-
Time Preference: Higher discount rates (ρ) favor immediate redistribution over long-term climate benefits.
All model parameters are specified in JSON configuration files. See test_f-f-f-t-t.json for an example.
Key configuration sections:
scalar_parameters- Time-invariant constants (α, δ, η, ρ, etc.)time_functions- Time-dependent functions (A(t), L(t), σ(t), θ₁(t), gini(t), emission_ratio(t), Eland(t), etc.)control_function- Allocation policy f(t)integration_parameters- Time span and step sizeoptimization_parameters- Optimization settings
The model uses several time-dependent exogenous functions specified in the time_functions section:
| Function | Description | Typical Type |
|---|---|---|
A |
Total factor productivity | gompertz_growth |
L |
Population | gompertz_growth |
sigma |
Carbon intensity of GDP (tCO2/$) | gompertz_growth |
theta1 |
Abatement cost coefficient ($/tCO2) | double_exponential_growth |
gini |
Background Gini index | exponential_growth |
emission_ratio |
CO2-equivalent to CO2 ratio (accounts for non-CO2 GHGs) | exponential_growth |
Eland |
Total land emissions (tCO2/yr) | exponential_growth |
emission_ratio: Converts industrial CO2 emissions to CO2-equivalent emissions by accounting for non-CO2 greenhouse gases (e.g., methane, N2O). Uses exponential decline from 1.40 in 2020 to 1.21 in 2100 (Barrage & Nordhaus 2023).
Eland: Exogenous land-use emissions that decline exponentially over time. At t_base=2020, Eland ≈ 4.5 GtCO2/yr with a decline rate of -2.107%/yr.
The model has 5 boolean switches controlling different policy features. Three are primary switches and two are sub-switches that are only meaningful when their parent switch is enabled:
-
income_dependent_damage_distribution(boolean, default: true)- Controls whether climate damage varies by income level
- When true: Poor people experience higher damage (exponential in income)
- When false: Everyone experiences the same damage (uniform)
- Parent switch for:
income_dependent_aggregate_damage
-
income_redistribution(boolean, default: true)- Controls whether income redistribution is enabled
- When true: Redistributes from high to low income according to
redistribution_amount - When false: No redistribution occurs
- Parent switch for:
income_dependent_redistribution_policy
-
income_dependent_tax_policy(boolean, default: false)- Controls whether taxation is progressive or uniform
- When true: Progressive tax using
find_Fmax()to determine tax threshold - When false: Uniform tax rate applied to all income
-
income_dependent_aggregate_damage(boolean, default: true)- Parent switch:
income_dependent_damage_distribution - Only meaningful when parent is true
- Controls how aggregate damage is calculated when damage varies by income
- When true: Aggregate damage computed directly from income distribution
- When false: Rescale damage distribution to match
Omega_base * y_net_aggregate, wherey_net_aggregateis mean net income after taxes/transfers - Note: When
income_dependent_damage_distribution = false, damages are naturally proportional toy_net, so this flag has no effect - Code enforces: Only checked when
income_dependent_damage_distributionis true
- Parent switch:
-
income_dependent_redistribution_policy(boolean, default: false)- Parent switch:
income_redistribution - Only meaningful when parent is true
- Controls whether redistribution is targeted or uniform
- When true: Targeted to lowest incomes using
find_Fmin()to determine threshold - When false: Uniform per-capita redistribution to all
- Code enforces: Only checked when
income_redistributionis true
- Parent switch:
Important: The code automatically enforces these dependencies. Sub-switches are only evaluated when their parent switch is enabled, ensuring logically consistent behavior.
The model supports a time-varying upper bound on the abatement fraction μ. This cap limits how much abatement can occur at different times, reflecting technological and political constraints on rapid decarbonization.
Set use_mu_up to true and provide a mu_up_schedule in the scalar_parameters section:
"scalar_parameters": {
"use_mu_up": true,
"mu_up_schedule": [
[2020.0, 0.05],
[2025.0, 0.10],
[2030.0, 0.12],
[2060.0, 0.9],
[2070.0, 1.0],
[2119.999, 1.0],
[2120.0, 1.1],
[2200.0, 1.1],
[2200.001, 1.05],
[2300.0, 1.05],
[2300.001, 1.0]
]
}| Parameter | Type | Default | Description |
|---|---|---|---|
use_mu_up |
bool | false |
Enable/disable the μ cap |
mu_up_schedule |
list | (required) | List of [year, mu_cap] pairs defining the cap schedule |
The mu_up_schedule is a list of [year, mu_cap] pairs:
- Linear interpolation between points
- Flat extrapolation outside the range (before first point uses first value, after last point uses last value)
- Points should be sorted by year (ascending)
Example interpretation:
- Year 2020: μ_cap = 0.05
- Year 2022.5: μ_cap = 0.075 (interpolated between 2020→0.05 and 2025→0.10)
- Year 2010: μ_cap = 0.05 (uses first value for years before schedule)
- Year 2400: μ_cap = 1.0 (uses last value for years after schedule)
Values above 1.0 allow carbon dioxide removal (negative emissions).
When use_mu_up is false, the model sets mu_max to a very large value (effectively no cap on abatement).
Each run creates a timestamped directory:
./data/output/{run_name}_YYYYMMDD-HHMMSS/
├── results.csv # Complete time series data
├── plots.pdf # Multi-page charts
└── terminal_output.txt # Console output
Launch multiple optimizations simultaneously:
python scripts/run_parallel.py "config_*.json"Each optimization runs on its own CPU core, with progress logged to terminal_output.txt in each output directory.
To override parameters for every launched job, append --key value pairs exactly as you would for run_optimization.py; dot notation targets nested fields (e.g., --optimization_params.max_evaluations 100). Example:
python scripts/run_parallel.py "config_*.json" --optimization_params.max_evaluations 100 --run_name quick_testGenerate a two-panel utility-ratio diagnostic figure:
- Panel A: Climate damage utility ratios - comparing income-dependent damage distribution vs. uniform damage (colormap: plasma_r, range: 1.0-3.0)
- Panel B: Progressive taxation utility ratios - comparing progressive tax vs. uniform tax (colormap: viridis_r, range: 1.0-10.0)
This creates a timestamped folder under data/output/ containing a PDF plus optional data exports for both panels:
- PDF only:
python scripts/plot_utility_ratios.py json/config_008_f-t-f-f-f_10_1_1000.json
- PDF + CSV export for both panels:
python scripts/plot_utility_ratios.py json/config_008_f-t-f-f-f_10_1_1000.json --csv
- PDF + CSV + XLSX export for both panels (requires pandas):
python scripts/plot_utility_ratios.py json/config_008_f-t-f-f-f_10_1_1000.json --csv --xlsx
Notes:
- Panel A holds aggregate damage fixed and compares how redistributing the same total damage across incomes affects utility
- Panel B compares utility under progressive taxation (taxing only highest earners) vs. uniform taxation
- All utility ratios are computed relative to the no-tax/uniform-damage baseline
- All output files (PDF, CSV, XLSX) are written to a timestamped directory alongside terminal output
All overrides are applied to every configuration file matched by the patterns.
The model includes test scripts to verify that different policy flag combinations produce identical climate and economic outcomes when using the same control trajectories (f and s). This validates that aggregate climate-economy dynamics are independent of distributional policy choices.
Test all 16 variants of the -f--- flag pattern (fixing income_dependent_aggregate_damage to false):
python scripts/test_all_flag_variants.py data/output/config_010_f-f-f-f-f_10_0.02_1000_el_20260106-070053This script:
- Loads optimized control trajectories (f and s) from the base directory
- Runs 16 flag combinations (2^4 variants):
- Position 1:
income_dependent_damage_distribution(varies) - Position 2:
income_dependent_aggregate_damage(fixed to false) - Position 3:
income_dependent_tax_policy(varies) - Position 4:
income_redistribution(varies) - Position 5:
income_dependent_redistribution_policy(varies)
- Position 1:
- Compares results across all variants
Expected outcomes:
- Climate and economic variables should be identical (within ~1e-6 relative tolerance)
- Capital (K), Output (Y_gross, Y_net), Consumption, Savings, Emissions (E), Temperature (delta_T)
- Small differences (<0.0002%) arise from O(dt) numerical coupling between income and damage distributions
- Distribution-dependent variables should differ appropriately
- Utility (U), Gini coefficients, income rank boundaries (Fmin/Fmax)
- These differences confirm that policy flags correctly modify distributional outcomes
The lag-1 coupling between income and damage distributions causes tiny differences (~1e-6) in aggregate variables:
At each timestep:
- Income distribution depends on damage distribution (from previous timestep)
- Damage distribution depends on income distribution (computed at current timestep)
This circular dependency is broken using a lag-1 approach: damage from timestep t determines income at timestep t+1. Different policy flags → slightly different damage distributions → slightly different capital accumulation over 400 years → tiny drift in emissions and temperature.
These O(dt) differences are:
- Expected and unavoidable given the model structure
- Negligible compared to parameter uncertainties (6 orders of magnitude smaller)
- Within acceptable tolerance for climate-economy models
The test validates that policy choices affect distribution without materially changing aggregate climate-economy outcomes.
coin_equality/
├── README.md # This file (quick start guide)
├── README_DETAIL.md # Detailed technical documentation
├── CLAUDE.md # AI coding style guide
├── requirements.txt # Python dependencies
├── src/ # Core library modules
│ ├── __init__.py # Package marker
│ ├── constants.py # Numerical constants and tolerances
│ ├── parameters.py # Parameter definitions and configuration
│ ├── distribution_utilities.py # Income distribution and utility integration
│ ├── economic_model.py # Economic production and tendencies
│ ├── optimization.py # Optimization framework
│ ├── output.py # Output generation (CSV and PDF)
│ ├── visualization_utils.py # Unified visualization functions
│ └── comparison_utils.py # Multi-run comparison utilities
├── scripts/ # Command-line entry points
│ ├── run_optimization.py # Main optimization script
│ ├── run_integration.py # Run forward integration from optimization results
│ ├── run_parallel.py # Launch multiple optimizations in parallel
│ ├── calculate_scc.py # Calculate Social Cost of Carbon
│ ├── compare_results.py # Compare multiple optimization runs
│ ├── compare_optimization_convergence.py # Analyze optimization convergence
│ ├── test_integration.py # Test script for forward integration
│ ├── test_all_flag_variants.py # Test policy flag independence
│ ├── plot_*.py # Various plotting scripts
│ └── ... # Other utility scripts
├── json/ # Configuration files for production runs
│ └── config_*.json # Policy variant configurations
├── data/output/ # Output directory (timestamped subdirs)
├── test_f-f-f-t-t.json # Example configuration file
├── test_f-f-f-t-f.json # Example configuration file
└── barrage_nordhaus_2023/ # Reference materials (protected)
- README_DETAIL.md - Complete technical documentation including:
- Detailed model structure with all equations
- Complete parameter descriptions
- Configuration file format
- Optimization algorithms and settings
- Implementation details
- Performance optimizations
Barrage, L., & Nordhaus, W. (2024). "Policies, projections, and the social cost of carbon: Results from the DICE-2023 model." Proceedings of the National Academy of Sciences, 121(13), e2312030121. https://doi.org/10.1073/pnas.2312030121
Caldeira, K., Bala, G., & Cao, L. (2023). "Climate sensitivity uncertainty and the need for energy without CO₂ emission." Environmental Research Letters, 18(9), 094021. https://doi.org/10.1088/1748-9326/acf949
Nordhaus, W. D. (1992). "An optimal transition path for controlling greenhouse gases." Science, 258(5086), 1315-1319. https://doi.org/10.1126/science.258.5086.1315
Nordhaus, W. D. (2017). "Revisiting the social cost of carbon." Proceedings of the National Academy of Sciences, 114(7), 1518-1523. https://doi.org/10.1073/pnas.1609244114
MIT License
Copyright (c) 2025 Lamprini Papargyri, ..., and Ken Caldeira
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
Lamprini Papargyri, ..., and Ken Caldeira