🇬🇧 English · 🇪🇸 Español
Climate Macro (DICE)
puremacro.climate.simulate_dice_model is a compact forward simulator of
Nordhaus's DICE-2016R model. You supply a carbon-tax path, a climate
sensitivity and a damage coefficient; it returns the joint 2020-2165 trajectory
of output, emissions, the three carbon reservoirs, warming and damages, plus a
social cost of carbon (SCC) computed as DICE defines it: the present value
of the consumption lost to one extra ton of CO2 emitted today, obtained by
perturbing the emissions path and discounting with the Ramsey factor.
It is a scenario tool, not an optimiser: nothing is maximised, the savings rate is fixed and the abatement rate follows DICE's first-order condition against the tax you pass in. Within that scope the blocks are the published DICE-2016R ones, applied on their published time basis (see "Model blocks").
from puremacro.climate import simulate_dice_model
res = simulate_dice_model() # defaults: 2020-2165, $40/tCO2 growing 2%/yr, ECS 3.1
print(res.summary())
res.plot()
simulate_dice_model() — every argument is keyword-only
| argument | default | meaning |
|---|---|---|
n_periods |
30 |
number of steps; with 5-year steps 30 covers 2020-2165 |
time_step_years |
5 |
years per step. DICE's constants are per 5-year step; for other values the carbon-cycle and temperature operators are raised to the power dt/5 (matrix fractional power), so a 10-year run samples the same dynamics |
start_year |
2020 |
calendar year of the initial state |
carbon_tax_initial |
40.0 |
carbon price in the start year, $/tCO2; 0.0 is the no-policy path |
carbon_tax_growth |
0.02 |
annual growth rate of the tax |
climate_sensitivity |
3.1 |
equilibrium warming per CO2 doubling, °C (DICE-2016R) |
damage_coef |
0.00236 |
a2 in the quadratic damage function D(T) = a2 T^2 |
discount_rate |
0.015 |
pure rate of time preference rho used in the SCC |
elasticity_marginal_utility |
1.45 |
eta in the Ramsey discount factor used in the SCC |
savings_rate |
0.22 |
fixed gross savings rate |
scc_horizon_years |
300 |
horizon over which marginal damages are integrated for the SCC |
Inputs are validated: a non-positive n_periods, time_step_years or
climate_sensitivity, or a negative carbon tax, raise ValueError.
What comes back
DICEResult is a frozen dataclass:
| field / method | what it is |
|---|---|
trajectories |
DataFrame indexed by year with output_gross, output_net, capital, consumption, population, emissions (GtCO2/yr), atmospheric_carbon, upper_ocean_carbon, deep_ocean_carbon (GtC), radiative_forcing (W/m2), temperature_anomaly, ocean_temperature (°C), climate_damages ($T/yr), damage_fraction, abatement_rate (mu), abatement_fraction, carbon_tax and social_cost_of_carbon ($/tCO2) |
peak_temperature |
max of temperature_anomaly over the horizon |
scc_initial |
SCC in the start year |
end_century_damages |
damage_fraction in 2100 |
parameters |
the arguments the run used |
summary(), to_frame(), to_markdown(), to_latex(), to_typst(), plot() |
the standard puremacro presentation interface |
Running a baseline and a policy scenario
from puremacro.climate import simulate_dice_model
base = simulate_dice_model(
n_periods=30, time_step_years=5, start_year=2020,
carbon_tax_initial=40.0, carbon_tax_growth=0.02,
)
policy = simulate_dice_model(
n_periods=30, time_step_years=5, start_year=2020,
carbon_tax_initial=80.0, carbon_tax_growth=0.035,
)
no_policy = simulate_dice_model(carbon_tax_initial=0.0, carbon_tax_growth=0.0)
print(base.summary())
warming_saved = (base.trajectories["temperature_anomaly"]
- policy.trajectories["temperature_anomaly"])
print(warming_saved.loc[2100])
What the runs produce (default calibration, 5-year steps):
| baseline ($40, +2%/yr) | accelerated ($80, +3.5%/yr) | no policy | |
|---|---|---|---|
abatement rate mu in 2020 |
0.194 | 0.300 | 0.000 |
first year mu reaches 1.0 |
2130 | 2070 | never |
| emissions 2020, GtCO2 | 32.4 | 28.5 | 39.6 |
| emissions 2100, GtCO2 | 23.1 | 0.4 | 70.1 |
atmospheric_carbon 2100, GtC |
1325 | 996 | 1679 |
| warming 2100, °C | 3.36 | 2.68 | 3.91 |
peak_temperature, °C |
4.13 | 2.96 | 5.94 |
end_century_damages |
2.66% | 1.70% | 3.62% |
scc_initial, $/tCO2 |
34.7 | 33.5 | 35.2 |
The SCC is almost the same across the three runs because it is a marginal damage evaluated on each path, not a function of the tax: the no-policy path is slightly warmer, so its marginal ton does slightly more harm. The no-policy peak of about 6 °C and the 2100 value of about 3.9 °C are in line with DICE-2016R's own baseline.
Model blocks
Macro
L(t) : logistic path from 7.79 to 11.5 billion
A(t) : TFP growing at 0.076/5yr, declining 0.5%/yr
Y_gross = A K^0.30 L^0.70 $T/yr
K' = (1 - 0.10)^dt K + dt * savings_rate * Y_net
Emissions and abatement
sigma(t) : carbon intensity, GtCO2 per $T, declining at DICE's rate
p_back(t): backstop price, 550 $/tCO2 in 2015 declining 0.5%/yr
tau(t) = carbon_tax_initial * (1 + carbon_tax_growth)^(t - t0)
mu = min(1, (tau / p_back)^(1/(theta2 - 1))), theta2 = 2.6
abatement_fraction = theta1 * mu^theta2, theta1 = p_back * sigma / (1000 theta2)
E_ind = sigma * (1 - mu) * Y_gross ; E_land = 2.6 GtCO2/yr declining 11.5%/5yr
Carbon cycle
Three reservoirs, initialised at [851, 460, 1740] GtC. DICE-2016R's transfer
coefficients b12 = 0.12, b23 = 0.007 per 5-year step, with the reverse
flows pinned to the equilibrium stocks (b21 = b12 * 588/360,
b32 = b23 * 360/1720). The operator is applied so that carbon is
conserved exactly: with zero emissions the three stocks always sum to the
same total. Emissions are converted at 3.666 GtCO2 per GtC and injected into
the atmosphere.
Temperature
Two layers with DICE-2016R's per-step coefficients c1 = 0.1005,
c3 = 0.088, c4 = 0.025 (they are not multiplied by the step length;
for time_step_years != 5 the whole 5-year operator is raised to the power
dt/5). Forcing is 3.68 log2(M_AT / 588) plus DICE's exogenous non-CO2
path. The recursion is stable and monotone for any positive climate
sensitivity: at climate_sensitivity=1.0 warming peaks at 1.45 °C, and at
5.0 at 5.91 °C.
Damages
damage_fraction = min(0.90, damage_coef * T_atm^2) — quadratic only; no
other damage form is provided.
Social cost of carbon
For each period t the emissions path is perturbed by one extra ton of CO2,
the model is re-run, and the consumption losses over the next
scc_horizon_years are discounted with (1 + rho)^-(s-t) (c_t / c_s)^eta.
The SCC therefore responds to the discount rate and to the climate
parameters:
discount_rate |
0.001 | 0.015 (default) | 0.05 | 0.10 |
|---|---|---|---|---|
scc_initial, $/tCO2 |
151 | 34.7 | 6.1 | 1.8 |
Raising climate_sensitivity from 3.1 to 5.0 raises the initial SCC from
$34.7 to $60.3 (+74%) and the peak warming from 4.13 °C to 5.91 °C.
What is simplified relative to published DICE
- No optimisation: the savings rate is fixed and the abatement rate follows the supplied tax; DICE's optimal-policy runs are not reproduced.
- The population, TFP, carbon-intensity, backstop and land-emission paths are the closed-form approximations of DICE-2016R's exogenous processes.
- One quadratic damage function; no tipping points, no catastrophic damages.
- The SCC is a marginal-damage calculation along the simulated path with a fixed 300-year horizon; DICE reports it from the optimiser's shadow prices.
Gotchas
time_step_yearsother than 5 changes the sampling of the same continuous-time dynamics, not the dynamics themselves; a 10-year run reports a peak of 4.25 °C against the 5-year run's 4.13 °C because it samples the path at different dates.carbon_tax_initial=0.0still prices carbon in the SCC column: the SCC is a damage, not the tax.
The rest of puremacro.climate
The subpackage also holds four empirical modules, added in 0.52.0, that have
nothing to do with DICE. They are not
re-exported — puremacro.climate.__all__ is ["DICEResult",
"simulate_dice_model"] only, so from puremacro.climate import
compute_annual_cdd_hdd raises ImportError. Import from the submodule:
from puremacro.climate.degree_days import compute_monthly_cdd_hdd, compute_annual_cdd_hdd
from puremacro.climate.annual_lp import climate_annual_lp
from puremacro.climate.mediation import climate_mediation_lp
from puremacro.climate.monthly_dl import monthly_dl, make_dl_lags
| function | what it does |
|---|---|
compute_monthly_cdd_hdd(df, *, temp_col="temp_c", threshold=18.0) |
adds cdd = max(T-18, 0) and hdd = max(18-T, 0) columns |
compute_annual_cdd_hdd(df, *, temp_col, threshold, region_col="region", year_col="year", month_col="month") |
sums those to annual_cdd / annual_hdd per region-year |
climate_annual_lp(panel, *, response, cdd_col="annual_cdd", hdd_col="annual_hdd", horizons=range(0,11), n_lags=2, controls=(), region_col="region", year_col="year", alpha=0.10) |
returns {"cdd": ..., "hdd": ...}, each a panel_lp_dk frame with [h, beta, se, t, lo, hi] |
climate_mediation_lp(panel, *, mediator_col, response, ..., n_bins=5) |
baseline vs top-quintile-interacted LP plus mediation_share_cdd / mediation_share_hdd arrays |
monthly_dl(df, *, shock_cols=("cdd","hdd"), response_col="log_births", n_lags=12, ...) |
distributed-lag OLS; HC1 for one region, cluster-by-region in panel mode |
climate_annual_lp runs the CDD regression with HDD as a control and vice
versa, so each reported IRF is the partial response — heating and cooling
degree days are strongly negatively correlated within a region-year and an
unconditional CDD response is largely the absence of winter.
One dead parameter here too: climate_mediation_lp(..., top_quintile_only=True)
is accepted and never read. The top quintile is always the one interacted.