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, branch EPM_working) — either by the workflow's own CIMS stage (scripts/cims_epm_runner.py driving 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_requested for requested_quantities, service_request, capital_cost, and the Natural Gas sector for Natural 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 hourly Generation rows × 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.csv sets their gross-output size shares — CIMS's resolution limit, not a modelling choice). Transportation Personal and Residential route to the household proxy L (§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 Scenario and 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 price on each region's CIMS Electricity node (CIMS.CAN.<region>.Electricity in results_tech.csv). In a working linked run this node carries COPPER's LCC — the CIMS-COPPER iteration writes it back as an exogenous lcc_financial on 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 staged copper_electricity_prices_<region>.csv files 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 the uncertainty branch 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-sector multiplier_price rows (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 at exogenous_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 the PCS_*_INF arms: 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 a nominal_scale on 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_stringency overlays each province's own system's tightening rate and reduction factor (TIER, provincial OBPS variants); obps_wci_price prices 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 (the project_transfers schema): 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 (the project_credits schema): 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 new policy_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 quarter max(0, bank long-term firm rate − (policy rate + spread)) × balance is 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 keys concessional_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_employees per 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.05 is an inherited measurement with no independent COPPER estimate yet.