CIMS-COPPER → MacroABM → LabourABM: methodology¶
This is the complete, standalone methodology of the cims-macro-linkage-v3
workflow at its second data vintage (2026-08; validated on a test pair of
converged CIMS-COPPER scenarios, a reference and a policy arm). The mechanisms
descend from the
CER-macroABM (Powering Canada's Growth) work, where most of them were first built and
measured; everything needed to understand them is restated here, adapted to how
this workflow sources them from CIMS and COPPER.
0. What this linkage does, in plain language¶
Canada has detailed models of its energy system: CIMS simulates how homes, vehicles and industries choose technologies and fuels under policy (how fast heat pumps replace furnaces, EVs replace gasoline cars, industry electrifies), and COPPER simulates the electricity grid that must power all of that (which plants get built, where, and when). What those models cannot say is what an energy transition does to the economy — jobs, GDP, investment, prices, provincial fortunes — because in them the economy is an assumption, not a result.
This linkage closes that gap. It takes a finished, internally consistent CIMS-COPPER energy scenario and imposes it on MacroABM-CA, an agent-based model of Canada's ten provincial economies in which thousands of simulated firms, households, banks and governments buy, sell, hire, invest and pay taxes. The energy scenario arrives as a set of constraints the economy must live with: industries must use fuels in CIMS's proportions, households must buy the energy CIMS says they buy, electricity must be generated with the capital COPPER says gets built, energy prices follow the scenario's own trajectories, and the scenario's real policies (carbon pricing, tax credits, rebates) apply. Everything else — production, employment, wages, consumption, trade, investment beyond the energy system — the simulated economy works out for itself.
A final stage passes the resulting employment path to the LabourABM, which re-resolves it into 162 occupations across 14 sub-provincial geographies with explicit job vacancies, applications and worker movements — answering who gains and loses work, occupation by occupation, as the energy system changes.
The objective: given an energy pathway you believe, produce its macroeconomic and labour-market consequences in a way where every energy number's provenance is unambiguous, the economic response is generated by explicit agent behaviour rather than assumed elasticities, and two scenarios can be compared knowing that exactly one thing — the energy pathway and its policies — differs between them. The linkage is one-way by design: the economy does not feed back into the energy models, so results should be read as "the economic consequences if this pathway happens", not as proof the pathway would happen.
1. Architecture¶
One-way and non-iterative at every stage:
[CIMS-EPM run ► cims/outputs] (optional stage; or staged outputs)
CIMS results_general/results_tech ─┐
COPPER output_summary_IDEA ────────┼─ adapter ──► <scenario>/data/ (linkage CSV formats)
│ │
pair-level investment prep ─────┘ ▼
(ITC rows + generation-investment cer_macroabm_runner ► run_cer_linkage
multiplier, needs BOTH arms' data) ► provincial MacroABM-CA
│
labourabm_runner (absolute mode)
│
build_idea_outputs ► IDEA/
The scenario folder is the run root (cims/, copper/, iterations/ staged
by you; data/, macroabm/, labourabm/, IDEA/ generated). The
orchestrator (linkage_cims_copper_macroabm.py) fronts all data work
before any model run, because the baseline arm's macro run needs the ITC rows
the pair-level prep writes.
Conventions that govern everything below: annual linkage cadence (5-year model milestones interpolated to annual), anchor year = simulation start (2022) — every index the linkage applies is a change relative to that year — and one macroABM run per scenario (~15 minutes), seed and calibration identical across the pair so the comparison changes one thing at a time.
1b. The linkage channels at a glance¶
A one-way coupling: the CIMS-COPPER scenario is a fixed exogenous input that sets the energy story, and the economy adjusts to it inside MacroABM-CA; the labour market is then re-resolved by the LabourABM. Nothing flows back.
CIMS-COPPER MacroABM-CA
energy-scenario pathways provincial macroeconomic model
┌──────────────┐ ┌──────────────┐
│ │ 1 End-use energy demand (CIMS) │ │
│ CIMS │ sets each sector's energy intensity -- │ firms' │
│ end-use │ ───► fuel per unit of output [3.1] │ input │
│ demand, │ │ coefficients│
│ fuel │ 2 Quantity anchors (CIMS) │ + realised │
│ prices, │ closed-loop controllers hold firms' and │ purchases │
│ oil & gas │ ───► households' realised energy purchases │ │
│ production │ to CIMS's path, guarded and total- │ households' │
│ │ conserving; residential + personal │ energy │
│ │ transport route to households [3.2-3] │ budgets │
│ │ │ │
│ │ 3 Fuel & electricity prices (CIMS) │ energy │
│ │ ───► replace the model's endogenous energy │ prices │
│ │ prices (real path, escalated) [4] │ │
│ │ │ │
│ │ 4 Oil & gas production (CIMS) │ B06 output │
│ │ ───► pins B06 to CIMS's production path, │ + exports │
│ │ surplus to exports [5.1] │ │
├──────────────┤ │ │
│ COPPER │ 5 Generation capacity (COPPER) │ sector D │
│ capacity & │ ───► floors and ceilings power-sector │ capital, │
│ generation │ capital (capacity-factor identity), │ production │
│ by province│ gates and floors production [5.2-3] │ ceiling │
└──────────────┘ └──────┬───────┘
│
DERIVED INSIDE THE LINKAGE │ realised
│ employment
6 Investment (COPPER build x CIMS/ATB capex; [6] ▼
CIMS switching-cost premia) ┌──────────────┐
ITC refunds to sector D, the policy arm's │ LabourABM │
generation-investment uplift, and transition │ occupations │
capital -- the pathway's capital spend, which │ x geography │
neither energy model publishes directly │ (absolute │
│ mode) [8] │
7 Policy environment (shared policies database) [7] └──────────────┘
OBPS + provincial stringency + WCI price,
household transfers, firm grants Note: no return path.
The economy does not
feed back into the
CIMS-COPPER pathway.
| # | Channel | Source | What it does in MacroABM-CA | Section |
|---|---|---|---|---|
| 1 | End-use energy demand | CIMS sector-root demand by fuel | Sets each industry's energy-input coefficients (additive share increments, linkage-owned) | 3.1 |
| 2 | Quantity anchors | CIMS per-(buyer, fuel) demand paths | Hold firms' and households' realised energy purchases to CIMS's path; negligible-base guard; total-conserving rescale; residential + personal transport routed to households | 3.2, 3.3 |
| 3 | Fuel & electricity prices | CIMS price rows (electricity node carries COPPER's LCC in a working linked run; see §4 caveat) | Replace endogenous energy prices; real path made nominal at a fixed 2%/yr or on the model's own CPI (exogenous_price_nominal_anchor) |
4 |
| 4 | Oil & gas production | CIMS crude/gas production shapes × external level anchors | Pin B06 output, surplus routed to exports; flat export pins on services and D; D import cap | 5.1, 5.3 |
| 5 | Generation capacity | COPPER capacity and generation by province | Floor + ceiling on sector-D capital with the capacity-factor identity; production gate (MB/QC/NL/BC/AB/ON) and D production floor (SK/MB) | 5.2 |
| 6 | Investment (derived) | COPPER build priced at CIMS/ATB capex; CIMS switching-cost premia | ITC refunds to D; policy-arm generation-investment uplift on the floor; transition-capital uplift on fuel-switching sectors | 6 |
| 7 | Policy environment | Shared policies database | OBPS with provincial stringency and the WCI price; household transfers; firm grants | 7 |
| 8 | Employment → LabourABM | MacroABM realised employment + labour-force index | Drives the LabourABM's demand and labour supply in absolute mode | 8 |
Channels 1–5 are the energy scenario itself, re-expressed as constraints; 6 exists because neither energy model publishes the capital spend the pathway implies; 7 is the scenario's real policy stack; 8 hands the resulting economy to the labour model. Sections 3–8 give each channel's mechanism, its sourcing in this workflow, and what was measured on this pair.
2. Source data and mappings¶
- CIMS (
cims/outputs/*_results_general.csv,results_tech.csv), produced by CIMS-EPM (dependencies/cims-epm, branchEPM_working) — either by the workflow's own CIMS stage (scripts/cims_epm_runner.pydriving the model's SESIT scenario runner on<scenario>/cims/inputs) or staged from a run made elsewhere. End-use demand is read from sector-root requested-quantity rows via a minimal-ancestor tree filter. The general-results file repeats requested quantities at every level of CIMS's service tree (a parent aggregates its children), so summing all rows counts each PJ several times over and biases the fuel mix — measured on ON Residential 2025: 3,243 PJ summing everything vs 527 PJ at the root, with the electricity share moving 29% → 41%. The adapter keeps exactly the rows whose node has no reporting ancestor in the same group, which covers both root-reporting sectors and the supply-chain sectors that start at deeper branches. Newer CIMS vintages renamed the output vocabulary (quantity_requestedforrequested_quantities,service_request,capital_cost, and theNatural Gassector forNatural Gas Production); the adapter normalises these on read — an unmatched sector name would silently drop B06's demand, not error. - COPPER (
copper/outputs/*_output_summary_IDEA.csv):Total Capacity(MW) and hourlyGenerationrows × the representative-day scaling factor, balancing areas folded to provinces. COPPER supply is genuinely per-province. - Sector map (
scripts/data/cims_copper_sector_map.csv): each CIMS sector is a first-class end-use sector mapped to the macro model's OECD-50 industry codes — steel, cement, pulp & paper, mining, oil & gas each follow their own modelled path. Eleven manufacturing industries share CIMS Light Industrial and move together (cims_copper_allocation.csvsets their gross-output size shares — CIMS's resolution limit, not a modelling choice). Transportation Personal and Residential route to the household proxyL(§3.3). - Atlantic split: CIMS aggregates NB/NS/PE/NL as
AT; the adapter splits that demand to the four provinces using structural anchor-year per-fuel weights, so their levels differ but they share AT's growth path. COPPER's supply side needs no split. - Cadence and zero-fill: every 5-year series is first zero-filled on the file's own milestone grid, then linearly interpolated to annual, held flat outside the covered range. A missing (series, milestone) row means the technology or fuel was not there (retired coal, pre-adoption hydrogen); clamping instead of zero-filling measured +14% on COPPER 2050 generation.
- Scenario isolation: every derived CSV is stamped with the scenario
label, and every other supply CSV the processor consults filters on
Scenarioand comes back empty — no foreign scenario data can leak in through a default path. Two deliberate structural exceptions use base-year quantities only: the Atlantic split weights and the fossil level anchors (§5.1).
3. Demand channels¶
3.1 Intensity targets and coefficient ownership¶
The macro model's industries buy inputs according to technical coefficients
(how much electricity, gas, refined fuel per unit of output). The linkage
writes CIMS's fuel-mix evolution onto those coefficients as additive share
increments relative to the 2022 anchor (additive_intensity = true) —
additive rather than ratio form, so a fuel starting from a small base moves by
CIMS's absolute share change instead of exploding by its growth ratio.
linkage_owns_coefficients = true: the model's own endogenous technical
growth is held off every (industry, input) pair the linkage writes. CIMS's
path already embeds the efficiency improvement it assumes; letting endogenous
growth also move those coefficients double-counts efficiency, and each
milestone re-set then snaps coefficients back to a stale anchor (measured in
the CER work as a 1.24× electricity overshoot, cut to 1.08× by ownership).
Energy substitution stays off for the same reason: the linkage sets the
energy mix, and the model's bundle-price logic would otherwise re-decide it.
3.2 Quantity anchors, the negligible-base guard, and the total-conserving rescale¶
Intensity targets shape coefficients; quantity anchors make realised
purchases follow CIMS. firm_energy_quantities and
household_energy_quantities anchor firms' and households' real energy
purchases to CIMS's per-(buyer, fuel) quantity indices;
household_energy_shares additionally applies residential fuel-share changes
to household consumption weights.
Two protections make this robust:
- The negligible-base guard (
quantity_anchor_floor = 0.001): a pair whose anchor-year quantity is below 0.1% of its row's total energy is anchored by CIMS's level change instead of its growth ratio — a growth ratio on a near-zero base is numerical noise (indices in the thousands). The response is flat between 0.0005 and 0.01, so the threshold is not knife-edge. This guard is what makes the per-province Atlantic artefacts usable. - The total-conserving rescale (
quantity_anchor_conserve_total = true): the per-pair index is a ratio applied to the model's base, and the model's base composition is not CIMS's. Unrescaled, pairs where the model's base weight exceeds CIMS's blow up the total: the first vintage measured Manitoba firms buying 8.14× their anchor-year electricity against CIMS's own 3.02×, taking electricity to 12.9% of intermediate spending and collapsing the run from 2046 in all five seeds. The rescale adjusts each fuel's indices so the anchored total grows at CIMS's own total rate while CIMS still sets composition.
Measured residual in a test run (policy arm, 2050, 2023 = 1.0): firm electricity purchases grow 1.43 vs CIMS's firm-sector 1.35. The dominant mechanism is a freight-base mismatch: CIMS freight electrifies ~18× from a near-zero PJ base, while the macro model's H-sectors carry an already-electric rail/transit base (~4.3% of firm electricity purchases), so the same index counts for far more in the model. The rescale absorbs most of this by pulling incumbent industries below their CIMS paths (Commercial 0.85 vs 1.34, gas extraction 0.58 vs 1.60, …); the residual comes from the guard, provincial conservation scope, and the ~45% of firm electricity purchases made by service industries with no CIMS counterpart. A dedicated freight-transport split was evaluated in the CER work and rejected — the channel cannot deliver near-zero-base electrification credibly — so the +6% residual is accepted and disclosed.
3.3 Household routing¶
CIMS's Transportation Personal and Residential sectors are people, not
firms. Their energy use routes to household agents directly: the share and
quantity channels apply CIMS's residential + personal-transport fuel evolution
to household consumption (EV electricity lands on households, not on the
transport industry). The real-estate proxy row L is the data carrier for
this demand, and the firm-side quantity anchor deliberately excludes L so
household demand is not anchored twice. Their switching investment,
however, is bought by firms: households in this ABM have no investment
instrument (no durables stock, no financing), so heat pumps and household EVs
enter as the real-estate sector's capital purchases — which keeps the GDP
accounting clean (device purchases are GFCF).
Measured tracking in a test run (policy arm, 2050, 2023 = 1.0): household refined fuels CIMS 0.13 vs macro 0.12 — the EV-driven gasoline collapse, reproduced almost exactly; household electricity 2.28 vs 2.35. Any consistency comparison must be population-matched (§10): mixing the firm and household populations manufactured a phantom refined-fuels divergence (all-sector 0.58 vs firms-only 0.91) during this vintage's validation.
4. Price channels¶
exogenous_energy_prices = true: the energy bundle's prices are pinned to the
scenario's own trajectories, severing the endogenous price channel by design —
energy prices are scenario inputs, and running the quantity anchors against
endogenous prices was the last structural departure tried and it collapsed the
first vintage's Net-zero arm. Sources, all CIMS's own price rows
(build_energy_prices in the adapter):
- Electricity (D): by default the
priceon each region's CIMS Electricity node (CIMS.CAN.<region>.Electricityinresults_tech.csv). In a working linked run this node carries COPPER's LCC — the CIMS-COPPER iteration writes it back as an exogenouslcc_financialon that node — so taking it from CIMS outputs is the self-consistent choice. Found 2026-09-04: in the dry-run pair the node is identical across scenarios and equals CIMS's default price file. The stagedcopper_electricity_prices_<region>.csvfiles DO diverge (Budget/Reference 2050: AB 0.89, BC 0.94, MB 0.78, SK 0.90, ON 1.03, QC 0.94, AT 0.90), but the iteration script on theuncertaintybranch writes their Year column as floats ("2025.0") and the cims-epm readers, which match years as strings, drop every row silently; only the per-sectormultiplier_pricerows (integer years) took effect. Raised with the CIMS-COPPER maintainer (a one-line cast fixes it).
Linkage-side repair (copper_lcc_dir, production since 2026-09-04):
the adapter reads the staged LCC files itself and replaces the node path,
in one of three modes (copper_lcc_mode). The LCC is a five-yearly
rate-setting snapshot that swings with the build cycle (Ontario 22.8 → 10.7
→ 25 → 43.5 → 30 $/GJ), so the mode matters:
"differential" (production) multiplies the node path by
LCC(this scenario)/LCC(reference scenario) — only the scenario divergence is
imported onto the common CER-history level path, and the reference arm is
unchanged (×1.000); "splice" grows the node level at COPPER's first year
by the LCC's own ratio (Ontario ×2.8 by 2050 — inherits every swing);
"level" is the raw LCC. This restores a scenario-specific electricity
price on the macro side; it does not repair CIMS's own quantity response
to that price, which needs the CIMS rerun.
* Refined fuels (C19): national means of CIMS's refined-fuel prices,
averaged at fixed weights so the index tracks prices, not the consumption
mix.
* Crude (B06): the CIMS Petroleum Crude price shape via the benchmark
slot (crude dominates the merged oil-and-gas sector's output value).
* Coal (B05): no scenario price series; stays endogenous.
Real→nominal conversion. CIMS prices are real; the macro model's prices
are nominal. Two conventions, exogenous_price_nominal_anchor:
"fixed"(default, every run to date): the processor escalates the pinned path atexogenous_price_inflation(default 2%/yr). Feeding the real path in unadjusted lets the model's general inflation erode the relative price (measured at 58% of CPI by 2035 in the CER work), and 2% was roughly neutral when the model's CPI ran near 2%/yr. The current calibration's CPI rises ~0.6%/yr, so the fixed 2% now makes energy ~50% dearer relative to everything else by 2050 for no reason in CIMS."cpi"(production setting since 2026-09-04, measured on thePCS_*_INFarms: Budget-minus-Reference headline unchanged within noise, household energy share of consumption ~1.4 pp lower by 2050, CPI 2050 1.21 → 1.17): the processor writes the tables in real terms and a read-only prehook (scripts/policy_exo_price_cpi_prehook.py) sets anominal_scaleon each province's price container every step to CPI(t−1)/CPI(0), which the macroabm-ca price setter multiplies into the normalised path. Energy then inflates with the economy it sits in, and the realised energy prices deflated by CPI reproduce CIMS's real paths. The lag avoids a within-step loop (energy is part of the CPI); the attribute defaults to 1.0 so untouched runs are bit-identical. Consequently, any comparison of realised model prices against CIMS must deflate by the same escalator — done so, the realised sector-D price tracks the CIMS path within ~2% on this pair. An apparent 2.32-vs-1.38 "divergence" at 2050 was entirely this conversion plus the first linkage year's ramp-in.
5. Supply-side channels¶
5.1 Fossil production and export pinning¶
The macro model's rest-of-world agent splits its aggregate import demand by frozen base-year industry shares, so no sector's export path can diverge from any other's — and oil and gas, both export-driven, therefore cannot follow a scenario at all on their own. Worse, domestic fossil demand falls on CIMS's path while production has no export absorber; the first vintage's Net-zero arm collapsed terminally from ~2042 through exactly this mechanism before the pins were adopted.
exogenous_fossil_production = true + export_demand_pinning = true: the
oil-and-gas sector (B06) follows CIMS's own crude and gas production
paths, with the surplus over domestic absorption routed to exports — which is
where Canadian oil and gas actually go. CIMS supplies the shape (each
region's sector-root production path, normalised at 2022); external supply
data provides the level and units (thousand bbl/d, Bcf/d), because the pin
blends crude and gas with energy-per-physical-unit weights that are
meaningless in CIMS's own activity units. B05b/B05c thereby become exogenous
inputs to the run — say so wherever the numbers are used.
Flat export pins hold ROW's export demand for the services and for electricity (D) at its base-year level: the frozen-composition ROW otherwise grows phantom export markets in non-tradeables (the CER record measured service markets growing up to 139,474× and a 22-percentage-point provincial unemployment artefact before these pins). Export-flat is not production-flat — domestic production stays endogenous.
Provincial production path (arm, 2026-09-04). The export pin is national by
necessity (the rest of the world is one agent), and the linkage used the same
national, energy-weighted crude-plus-gas index as every province's production
target and capacity floor — so in 2050 every province's B06 output sat at
1.35× while CIMS's provincial paths ran from 0.44× (Newfoundland) to 1.50×
(BC), with Saskatchewan driven up where CIMS has it falling.
fossil_production_per_province = true (driver
--fossil-production-per-province) makes the extractor add a production_index
column to each region's export-demand-index file from that region's own crude
and gas series (national where a province has none), and the linkage applies
it to that province's production target and floor only; the export pin keeps
the national export_index. Runs as the PCS_*_FP arms against the national
control. Result: Saskatchewan's B06 output in 2050 moves from 1.35× to 0.87×
(its CIMS path 0.86×), Ontario 1.34× → 0.75× (0.56×), Newfoundland 0.56× →
0.31× (0.44×, clipped by the same gate that clips its D output), Alberta and BC
1.37× (1.40×, 1.49×); Manitoba's target is written (1.15×) but its small,
mostly exported B06 still follows the national export pin (1.35×). National
production in 2050 is 0.2-0.3% lower than the control and the Budget-minus-
Reference gap is unchanged; Saskatchewan's real GDP 2050/2022 falls from 1.35×
to 1.29× and Newfoundland's from 1.19× to 1.14×, which is CIMS's declining
oil path arriving in the provinces it belongs to.
5.2 The power-sector capacity machinery¶
COPPER owns sector D's capital path through four coordinated instruments. The underlying need: the macro model's demand pool allocates by available supply, not price, and its capital computation uses drifting coefficients that ignore utilisation — so without guidance a slack province absorbs everything, and with one-sided guidance output-per-capital creeps upward.
- Floor (
capacity_floor_per_province,capacity_index_basis = "capacity",capacity_factor_from_cer = true): each province's reference capital stock for D is floored at its COPPER MW path, with a capacity-factor identity: capital tracks MW while a cf(0)/cf(t) intensity uplift returns the output ceiling to COPPER's generation path — a MW of wind is not a MW of gas, and a wind-heavy build must mean more capital per unit of output, not more output. - Ceiling (
capacity_ceiling_margin = 1.3): reference capital capped at floor × margin — the floor alone is one-sided guidance (the CER record measured Manitoba investing 3.3× against a 1.27× path through a slack-capture loop). The first vintage ran 2.0 to compensate for its supply-demand-inconsistent standalone pair; with a converged pair the margin is back at 1.3, and holds (no D rationing on this pair). - Gate (
capacity_gate_provinces = "MB,QC,NL,BC,AB,ON", margin 1.1): a hard cap on target production at 1.1× the province's COPPER generation path, correcting the output-per-capital drift. Surgical by design — gating every province squeezes the national total. AB was added after a measured A/B (1.29/1.22× over path → the margin; GDP and unemployment unchanged to two decimals) and ON likewise (1.12 → 1.08 in the test run's policy arm; the big-province-gate squeeze does not materialise at this pair's demand levels). NB stays ungated deliberately: its COPPER generation falls under transmission-expansion assumptions — COPPER serves NB through imports the macro model cannot replicate past the D import cap — so a gate would impose a genuine shortage, not correct drift. Gated province-sectors are inputs, not results; disclose them as such. - D production floor (
d_production_floor_provinces = "SK,MB", margin 1.0): the gate's under-utilisation twin — target production floored at the COPPER generation path. Measured: lifts SK/MB from 0.87/0.89 to 0.99/1.00 of path in the test run's policy arm at no macro cost.
With this set, all four big provinces are gate-aligned within the 1.1 margin and eight of ten provinces sit within ~0.95–1.2 of their COPPER paths.
The national wedge is structural. In the test run's policy arm, macro D production grows 1.74× (2023 = 1.0) against COPPER's 1.55×, because the anchors conserve CIMS's demand — 1.62× in PJ, 1.67× in value terms (CIMS's own per-sector rates put households at 36% of electricity value vs 29% of PJ, and households are the fast-electrifying side) — plus the §3.2 residual. Gates relocate production between provinces; they cannot shrink anchored demand. Nominal-vs-nominal comparison is a dead end (the pinned price path cancels on both sides), and cross-province aggregation weights are immaterial (dollar-weighting COPPER's provinces moves its national index only 1.546 → 1.560). Displacement from the gated provinces currently concentrates in NS (1.40× a small base) — the next gate candidate to measure if a future pair widens it. Closing the wedge at the source means a COPPER vintage built to CIMS's demand.
Own-use and transmission losses (electricity_own_use,
loss_share_of_generation = 0.05) apply as a buyer-side gross-up; the loss
share is an inherited measurement, as COPPER's representative-day accounting
gives no independent estimate.
5.3 Trade constraints¶
The D import cap (ImportLimits.sectors = ["D"], mode = "level") clamps
ROW's electricity exports into Canada at base-year volume — the physical
reading, since cross-border transfer capacity is fixed infrastructure and does
not grow with GDP. The cap is coherent only because the linkage
simultaneously builds D's supply, and it is what makes the NB gate exclusion
binding (§5.2). Electricity is traded between provinces inside the macro
model — its goods market pools each good's demand and allocates it across
provincial suppliers by available-supply share, at base-year-calibrated
trade shares — but that trade is not steered by the linkage: the optional
channel that routes sector-D interprovincial flows to the energy models'
interchange paths is off (in the CER record it destabilised the economy past
~2038), so provincial D production and interprovincial flows are the pool's
own outcome, bounded by the floor/ceiling/gate instruments above.
One accounting property to keep in mind: the model trades value at frozen base-year price densities, so a province's dollars-per-MWh embeds its base-year realised prices. Provincial generation shares in value terms are not physical shares; present both where it matters.
6. Investment channels¶
Both pair-level artefacts are built by scripts/cims_copper_investment.py,
which needs both arms' adapter data and runs before any model run.
6.1 Investment tax credit¶
investment_tax_credit = true in both arms — these are implemented law
(Clean Technology 30%, Clean Electricity 15%, CCUS 50%), not scenario
content. The amount pipeline: COPPER per-province capacity paths → technology
split (COPPER's technology strings map 1:1 onto the reference technologies)
→ perpetual-inventory gross additions (retirements at technology lifetimes;
3-year smoothing) → priced at overnight capex, with replacement
additions discounted by a replacement-cost ratio and coal→gas conversions
repriced → construction-time spreading → × eligible capital share × the
statutory rate schedule (legislated phase-downs; the Alberta ACCIP top-up on
CCUS) → delivered to sector D as a production subsidy of equal value (the
model has no direct firm-transfer instrument; the dollars are right, the
incidence within D is approximate). COPPER's own Capital Costs output is
annualised (CRF × capex) and cannot give absolute dollars without
unwinding each technology's CRF — pricing COPPER's build with overnight costs
keeps the dollars commensurable. Rows are merged into the scenario-keyed
shared ITC table under each run's own label; a non-matching label yields
silent zeros, which is why the pair prep must precede the baseline's
macro run. Headline statutory rates: an upper bound on real support.
Capex source (default since 2026-09-04): CODERS, the source of COPPER's own
costs. scripts/build_copper_capex.py reads CODERS' generation_generic
table (overnight capital_cost_CAD_per_kW, economic life; the
annualized_capital_cost COPPER consumes is that overnight cost annualised at
~10.5% — not COPPER's 3.74% planner rate) and puts it on the run's own
price_evolution learning path and overnight_cost_multiplier, keyed to the
linkage's technologies through COPPER's IAMC name map; the pair prep takes it
via --capex-override-csv (config Models.CIMSCOPPER.capex_override_csv) and it
replaces the CIMS and ATB costs for every technology it covers. This closed a
real defect: on EPM-era CIMS outputs the CIMS-derived capex read ~10× too low
for wind, solar, nuclear, biomass and geothermal (the technology output field
is no longer per plant, so the inferred plant size was wrong), which had
understated the pipeline's power investment (~$11bn/yr vs ~$25bn/yr) and the
Clean Technology credits (~$2.6bn/yr vs ~$4–5bn/yr). CODERS'
total_project_cost_CAD_per_kW (~23% higher: owner's costs, contingency, IDC)
is selectable with --cost-field; the default is the overnight figure COPPER
optimises on. Dollar year CAD ~2021 at face value. Retrofit/refurbish Types
are still priced as new build at the cims_technology level.
6.2 Generation-investment multiplier¶
The bottom-up investment level does not reproduce observed utilities capex
(asset renewal and grid modernisation do not scale with capacity), but the
scenario difference is credible — so it enters as a ratio.
generation_investment_multiplier = true on the policy arm only (the
table is a policy/baseline ratio; applying it to the baseline doubles the
uplift — config discipline, not enforced in code). Construction: the two
arms' investment paths are smoothed separately (5-year centred — a trough
in the denominator otherwise reads as a policy effect), ratioed, then netted
by the per-province floor ratio (the two arms' generation indices,
anchored 2022, at the same per-province aggregation level as the floor it
scales — netting at a different level is wrong in both directions at once),
with a national fallback below a $50M baseline (ratios of small numbers are
noise). Applied multiplicatively to the capacity floor's index, with the
uplift exported separately so capital requirements rise without moving the
production ceiling. Written per pair into the policy arm's data/
(generation_multiplier_csv); measured range on this pair: 0.88–2.5 across
provinces.
6.3 Transition capital¶
transition_capital = true: switching fuels is not free at the margin — a
firm cannot burn electricity instead of diesel without buying the equipment
that does it. The channel uplifts capital requirements by the premium of
the switched-to option over the incumbent (not the gross device cost:
equipment turns over in both arms through ordinary depreciation-driven
reinvestment, and charging gross costs would double-count the replacement
component), with the capital-goods composition of that spending mapped to the
supplying industries. Costs come from CIMS-sector-keyed technology-cost
reference tables; the reference-run (_ref) tables are used in both
arms — no Conservative-platform rekey exists, and holding technology costs
common keeps the comparison quantity-driven. The channel has its own anchor
at the first CIMS milestone at or after the intensity anchor, because its
denominator is only populated at milestone years and a non-milestone anchor
silently disables it. Known caveats: negative premia are clipped to zero; complementary
infrastructure (panels, wiring, charging) is not in CIMS device costs; and
pricing the premium is a conservative estimate wherever a pathway
requires or implies clean technology to be adopted before the incumbent
would have retired — early retirement makes the whole device cost
incremental, not just the premium, and that early-retirement uplift is not
charged.
7. The policy environment¶
The scenario's real policy stack applies inside the macro model, sourced from
the same policies assumptions database CIMS's own policy files are built
from — which is what makes enabling it self-consistent with the energy
scenario. The database is versioned with the workflow as
scripts/data/policies.csv (a copy of the maintained SharePoint original;
refresh it when the database changes — policy_assumptions.py also honours
an M3_POLICIES_CSV override), and the policy_*.py builders derive the
overlay tables from it:
- Industrial carbon pricing (
use_obps_reg): an output-based pricing system on covered industries, priced at the federal benchmark path, with baseline intensities accumulated from the simulation's own emissions.obps_provincial_stringencyoverlays each province's own system's tightening rate and reduction factor (TIER, provincial OBPS variants);obps_wci_priceprices Quebec's signal at the WCI allowance path rather than the federal benchmark (~2.4× too high for a cap-and-trade province). No tightening extension past 2030: the legislated stop is the correct reading for a non-net-zero pair.
Interaction with the pinned energy prices. CIMS's price for a fuel node
is its financial life-cycle cost net of the output-based price_subsidy
(benchmark × tax), so a pinned fuel price carries the carbon cost of that
fuel's supply chain (e.g. gas-production emissions) less the OBPS rebate
on it; the carbon charge on burning the fuel sits in CIMS's end-use
technology costs and reaches the macro model through the demand quantities
(channels 1–2), not through the price. The macro OBPS then charges covered
industries' own production emissions. The overlap — the supply-chain carbon
cost of fossil sectors priced once in the pinned price and once through
OBPS on the same producers — was examined in the CER work and judged
immaterial (the OBPS rebate nets most of it out at the source), with a
standing revisit trigger: an implausible margin squeeze on the fossil
sectors.
* Household transfers and firm grants: implemented energy-transition
rebate and grant programs as cash transfers, debt-financed. The tables key
on the source database's scenario names, so these runs set
policy_scenario_alias = "Current Measures" to select the
implemented-policy rows.
* The investment tax credits (§6.1).
* Major-projects overlays (pipeline capital programs, project credits,
capture wedges, transfer schedules) remain available as an option but are
deliberately off in the test runs (agreed 2026-08-28): they encode a
specific project narrative, not generic implemented policy.
7b. Layering a budget on top of an energy scenario (EPM dry run, 2026-09)¶
When the policy arm is a budget rather than a different energy pathway, the
division of labour is fixed before anything is coded: CIMS and COPPER own every
quantity the budget moves (heat-pump uptake under a grant, generation and
intertie build under cheaper capital, the regulatory gas path), and those reach
the macro model through the channels above unchanged. The macro side adds only
what the energy models cannot see, from a single measures table
(scripts/data/budget_2026_measures.csv, one row per measure with its fiscal
profile, host industry, provincial allocator and delivery channel) translated by
scripts/policy_budget_2026.py into three tables in the policy arm's data/:
- Who pays —
policy_budget_transfers.csv(theproject_transfersschema): non-repayable federal cash into the balance sheets of the firms the linkage already made buy the capital, debt-financed on the government. CEFF contributions and the Intertie Acceleration Fund land in D; the home electrification grant lands in L, because household heat pumps are the real-estate sector's capital purchases in this ABM (§3.3) — paying households would fund consumption on top of a device L already bought. Allocation follows the paired build: CEFF by the positive part of the policy-minus-baseline generation-plus-transmission capex per province, IAF by policy-arm capex on the named interfaces by endpoint province, the home grant by CIMS's own residential heat-pump new stock (Atlantic split by residential electricity). - Credits —
policy_budget_credits.csv(theproject_creditsschema): the Clean Electricity ITC's transmission extension as credit dollars into D, merged into the single ITC application (§6.1). - Financing cost —
policy_budget_concessional.csv, an outstanding-balance schedule (flat draws, straight-line amortisation) for the newpolicy_concessional_prehook.py. Firm credit demand in the model is a pure financing gap, not interest-elastic, so the principal of a concessional loan is a wash and the whole concessional element is the interest wedge: each quartermax(0, bank long-term firm rate − (policy rate + spread)) × balanceis paid into the borrowing industry's deposits and booked on the government debt, both rates read live from the running model — the bank rate annualised from the per-step convention the credit market charges it at (~4.5%/yr), the policy rate as its annual Taylor rule quotes it (~1.5%/yr) — so the wedge is ~2.5%/yr; the spread (50 bp a year) is the only assumption. Driver flags--concessional-loans-csv/--concessional-spread, config keysconcessional_loans_csv/concessional_spread.
Two additions to the pair prep make transmission visible on the macro side: the
adapter now writes new-transmission.csv (COPPER's New Transmission MW per
corridor with the corridor length from scripts/data/copper_transmission_corridors.csv),
and cims_copper_investment.py prices it at COPPER's own annualised cost backed
out to overnight (243.55 per MW-km-year at 3.74% over 40 years ≈ $5,000 per MW-km),
spread over the five build years and split between the endpoint provinces,
writes transmission-capex.csv for the translator's allocators, and adds it to
both arms' investment paths before the generation-investment multiplier (§6.2).
The applied multiplier is now bounded at run time (generation_multiplier_min
/ _max, driver flags --generation-multiplier-min/--max; unset = unclamped,
every CER run's behaviour). The extractor recomputes the value from the
smoothed dollar columns and the floor ratio, so the pair-prep CSV's clamp column
is informational only — a lesson learned the hard way on this pair: a timing
shift of one large build between the arms (Manitoba's hydro, raw 5.6× then
0.07×) applied to the capital stock put MB's electricity investment ×10 in
2036 and then at zero for a decade, and Saskatchewan's sub-1 years pushed its
capital target under the D production floor and collapsed the province. The
budget arm runs uplift-only at 1.0–2.0.
Disclosures specific to this layer: transfers are debt-financed stocks (the
deficit flow does not show the programs, §11); envelopes are nominal at face
value; IAF cash is booked in full although COPPER's build absorbs only a
fraction of it at a 50% cost share; retrofit CCS is priced at new-build cost in
the ITC pipeline. The ITC base is netted for government assistance (Income
Tax Act s.127(11.1): non-repayable assistance reduces the capital cost a credit
is computed on). CEFF-voted dollars are netted against that province-year's D
generation investment at its effective credit rate; IAF dollars against the
province-year's intertie capex at the 15% transmission-extension rate; both
capped at the investment co-funded, emitted as negative credit rows
(--no-itc-netting disables). Loans are not assistance and household grants
sit outside the ITCs. The no-measures policy arm is the
natural control for the layer (archive it before re-running).
8. The LabourABM stage (absolute mode)¶
Models.LabourABM.demand_mode = "absolute": each scenario runs the LabourABM
as a self-contained absolute world, and the policy effect is the difference
between LabourABM runs (same seed and parameters in both arms). This
replaced an earlier convention where a single LabourABM run was driven by a
policy-minus-baseline differential — absolute outputs are directly
interpretable and the differential is taken where it belongs, at the end.
- Demand follows the scenario's own realized macro employment
(
number_of_employeesper province-industry, referenced against its own first quarter) applied to the LFS baseline employment distribution. Realized employment specifically — not production (whose growth ratio would misread productivity gains as labour demand over a 28-year horizon) and not desired labour (which is in effective-labour units and re-imports the same confound) — keeps the LabourABM consistent with the macro world everything else was computed in. - Supply grows along the per-province labour-force index the macro run
exports (
macroabm/labour_force_index.csv, read back out of the run's own demography configuration so it cannot drift from what the macro model actually used), interpolated to the bridge's monthly axis and allocated within provinces by initial labour-force shares. Entrants join the unemployed pool and must pass through matching like anyone else; exits draw from the unemployed pool only, clamped at what is available, with the shortfall carried forward — mirroring the macro model's own labour-force mechanism exactly. - Interpretation: the LabourABM is a disaggregator of a macro-consistent employment path into occupations and gross flows — not a second forecaster of aggregates. The macro model applies its own labour-market frictions; the LabourABM deliberately tracks its demand input. The national labour force follows the injected index exactly (+21.03%, measured); provincial labour forces are "macro trajectory + endogenous inter-provincial migration" — the mobility network reallocates workers toward demand (an injected AB +55% can end +26% with out-migration), which is a feature, not an error.
- Reporting discipline: quote aggregate employment and unemployment from the macroABM — but note its provincial unemployment rates carry a documented labour-supply drift (labour-force growth minus employment growth under a single national household-demand rate) and are not individually reportable. Use the LabourABM for occupational and geographic composition, gross flows, and cross-scenario differences.
9. Outputs and the IDEA consolidation¶
IDEA/ collects, per scenario: the COPPER *output_summary* CSVs, the CIMS
*results_general_IDEA* CSVs, and macro-labour-shallow_<scenario>.h5 — the macro
shallow summary with, per province, Unemployment Rate overwritten by
the LabourABM's (quarterly, unit-matched) and Employment, Unemployment,
Vacancies, Vacancy Rate columns added in persons (LFS-anchored). The
macro model's per-industry _firms_number_of_employees tables are left
untouched: model-agent units, the industry-dimension view.
Time axis of the IDEA macro file (default since 2026-09-03). The
macro-labour-shallow_<scenario>.h5 in IDEA/ (scenario-tagged since 2026-09-06 so IDEA can load several scenarios side by side) carries calendar quarters, not a positional
index: every quarterly frame is indexed 2022Q1 .. 2050Q4 (Q1 of the config's sim_start_year,
--start-year overrides). The raw shallow file's row 0 is the pre-simulation
initial state (CPI exactly 1, base-year unemployment; the model makes 116
steps) and is dropped, rows 1..116 being the simulated quarters — the model's
own convention. Until 2026-09-04 this export, like the CER quarter-labelled
export, kept row 0 as 2022Q1 and dropped the last row as a "2051Q1 orphan",
a one-quarter shift that also lost 2050Q4; the CER IDEA files still carry that
convention. *_firms_number_of_employees is scaled ×1000 to
persons (the CER exports' convention; --employment-scale 1 keeps agent units),
and a time_index frame spells the series out (step, quarter, year,
quarter_of_year, period_start). The file is written through pandas end to
end and every frame is verified to reload with unchanged macro values. The raw
macroabm/simulation_shallow.h5 is untouched: the LabourABM stage, the analysis
report and the diagnostics keep their positional index.
Household energy wallet (default since 2026-09-04). Total household spending
on energy goods was always in the shallow file
(<prov>_households_consumption_by_good_nominal, columns D = electricity
and gas supply in the OECD-50 scheme, C19 = refined petroleum, B06 =
direct oil-and-gas purchases; deflate by <prov>_firms_price for real terms).
The distributional view needs per-household arrays that the shallow export
omits and trim_agent_history discards, and saving them in the full HDF5 would
multiply its size, so scripts/household_energy_wallet.py reduces them on the
fly: a read-only prehook (bit-identical for the run) takes each quarter's
per-household consumption (spent minus the household's investment purchases)
and income, fixes income quintiles on first-step income, and keeps only the
group sums — spending on D/C19/B06, total consumption, income, household count,
for all households and each quintile. The driver writes the result INTO
simulation_shallow.h5 as <prov>_households_energy_wallet frames
(<group>|<metric> columns, the same integer step index as every other frame,
so IDEA and the analysis report see the wallet next to GDP and prices) and, as a
by-product, household_energy_wallet.csv beside it (--no-household-energy-wallet
to skip; a few hundred kB). python scripts/household_energy_wallet.py --attach
<csv> --shallow <h5> retro-fits the frames into a run made before this change.
The category split below is applied when the wallet is written, so the CSV,
the shallow frames and the IDEA frames all carry the categories; the IDEA build
only recomputes it for runs made before that.
What sits inside the goods. Read the goods columns (spend_D, spend_B06,
spend_C19) as the model's accounting and the category columns
(spend_<category>_est_nominal) as the result: spend_B06_nominal in
particular is households' small direct purchases from extraction that the
linkage moves along the residential gas path, not the gas bill, which sits in
D. The macro model cannot separate them: D is
"electricity and gas supply" (utility bills of both kinds sit in households'
base-year D purchases), and the household routing (§3.3) puts personal-transport
(EV) electricity into households' D purchases and motor fuels into C19 next to
home heating oil; the linkage anchors residential electricity onto D and
residential gas onto B06 (cims_copper_sector_map.csv). The IDEA build
therefore reports an estimated five-way split using CIMS's own household
value shares per province and year: inside the utility spend (D + B06), home
electricity (residential GJ × the Residential electricity rate, COPPER's retail
rate from the iteration feed), EV electricity (personal-transport GJ × that
sector's rate) and natural gas (residential GJ × the gas price × a
delivered-price markup); inside C19, motor fuels (gasoline/ethanol at the
gasoline price, diesel/jet at the diesel price) and heating oil (fuel
oil/propane at the diesel price). --gas-retail-markup defaults to 3.0 because
the adapter's gas price is a commodity price (~$3-4/GJ) where delivered
residential gas is ~$10-15/GJ, while the electricity rates and motor-fuel
prices are already retail; it is an assumption to calibrate against the Survey
of Household Spending. Columns spend_electricity_home_est_nominal,
spend_electricity_transport_est_nominal, spend_natural_gas_est_nominal,
spend_motor_fuel_est_nominal, spend_heating_oil_est_nominal and the
share_* inputs. On the dry-run pair EV charging grows to 20-27% of the
utility spend by 2050 while Ontario's gas share falls from 44% to 18% — the
shares carry CIMS's fuel switching, which is the point of the exercise.
Utility-basket routing (arm, 2026-09-04). The reporting split does not fix
the anchoring itself: because the fuel map sends residential gas to B06 while
the gas bills sit inside D, the household anchor drives all of D along the
electricity path (Ontario ×2.4 by 2050 where the value-weighted basket of
electricity and gas is ×1.65). cims_copper_data.build_household_utility_basket
(--utility-basket) writes end_use_demand_utility_basket.csv, in which the
Residential electricity row is electricity plus gas converted to
electricity-value-equivalent GJ at anchor-year relative prices (delivered-gas
markup as above) and the household gas rows are dropped so the negligible-base
guard leaves households' B06 to the model; personal-transport electricity keeps
its own row. An arm points end_use_csv at that file (PCS_*_UB scenarios) and
is compared against the same run on the raw file before any adoption.
Nominal and real. Every money column has a _real twin at 2022Q1 prices,
deflated by the good's own price in the shallow file (the D price for the
utility categories, the C19 price for the fuels, CPI for income and
consumption). Read quantities from the real columns: on the dry-run pair
Ontario's refined-petroleum price is ×1.5 by 2040 and its electricity-and-gas
price ×1.5 by 2050, so a nominal motor-fuel bill that is flat to 2040 and a
nominal gas bill that is flat to 2050 are real bills falling by 13% and 23%,
which is CIMS's Reference path (personal-transport fuel ×1.03 in 2030, ×0.92
in 2040, ×0.56 in 2050; residential gas ×0.76 in 2050). EV charging grows
×150 from a base near zero but is 3% of personal-transport energy in 2040 and
23% in 2050 (electric drive is 3-4× as efficient per km, so the energy share
understates the vehicle share) — the fuel decline is a 2040s story in CIMS,
not a 2030s one. One more reading note: the recorder is a prehook and stores
the previous quarter's spending, so the driver takes a closing record after
the last step; a run made before that record has three quarters of 2050, and
--attach rebuilds the all-household row of the missing quarter from the
shallow file's consumption-by-good (quintiles and income stay absent there).
Per real household. The model's household count is a fixed number of
agents per province (Ontario 6,601, PEI 57), each standing for many real
households, so "spend per agent" is not a household figure. The wallet scales
instead: households_real = n_households × (census households 2021 /
agents at the first step) with the 2021 Census private-household counts in
scripts/data/households_census_2021.csv (Ontario 5,491,201), and every money
column gets a <column>_per_household twin (nominal dollars per real
household per year; quarterly rows are a quarter of that). For a quintile the
same scale applies, so its households_real is a fifth of the province's.
Because the agent count is constant, the real-household count is constant too:
the figures are per 2021 household, not per projected household. Two reading notes: quintiles are of agent households (PEI has 57, so its
quintiles are ~11 agents); and unless the household configuration's
take_consumption_weights_by_income_quantile is on, every household spends the
same shares of its budget, so quintile energy shares are identical by
construction and only the levels differ. Turning it on is not yet meaningful
for Canada: the model's quintile weights come from an OECD table
(oecd_econ/consumption_by_income_quintiles.csv) with no Canadian rows, so a
Canadian table (Survey of Household Spending by income quintile: electricity,
natural gas, other fuels, motor fuel) has to be built first.
10. Validation, unit conventions, and the analysis report¶
scripts/linkage_analysis.py is the standing validation instrument (see the
quick-start). Its unit conventions are
methodology, not cosmetics — several apparent divergences on this pair were
unit artefacts:
- Index base 2023 (2022 carries the linkage ramp-in spike).
- Population matching (§3.3): CIMS firm sectors vs firms' purchases; household-routed sectors vs households' consumption.
- Value weighting: CIMS PJ × CIMS's own prices (per-sector electricity rates from the iteration feed; per-fuel national prices otherwise), making both sides dollar-commensurable. Moves the CIMS electricity total 1.62 → 1.67 and the household weight 29% → 36% — nearly the macro model's 37%.
- Real-vs-real prices: deflate model prices by the §4 escalator before comparing with CIMS.
- The value-structure-adjusted COPPER trace: COPPER × the CIMS value/PJ ratio, separating the unit-structure share of the generation wedge (~5pp) from the genuine model wedge.
11. Standing limitations and disclosures¶
- One-way: the energy pathway is an input; the linkage cannot say the economy would change it. Where that matters, say so alongside results.
- Single-seed runs (seed 4): provincial macro results carry large seed variance in the historical record; national and cross-scenario statements are the robust ones.
- The pair's residual demand-supply tension (§5.2) is delivered demand-side by design; the macro electricity sector out-produces the COPPER reference and the panel notes quantify by how much and why.
- CIMS demand levels include supply-chain own use (gas production, refining, blending), so national PJ totals sit above end-use conventions — only ratios and anchor-relative indices reach the model.
- The macro provincial unemployment drift (§8) and the §3.2 freight-base residual are known, quantified, and disclosed rather than patched.
- Gated and pinned province-sectors (§5) are inputs, not results.
loss_share_of_generation = 0.05is an inherited measurement with no independent COPPER estimate yet.