How OSeMOSYS decides
Five rules behind every result
Schematic, not model output.
One OSeMOSYS region, CAN. Geography lives in the names: every technology and fuel ends in a
two-character zone code, as in PWRHYDB2.
ZoneDemandShareCLEWs · NRCan · OSeCAN v0.3
OSeCAN model sheet v10 Last updated 17 October 2026
This page explains how C2070 is built: how Canada is divided, how a run goes from data to an answer, which technologies the model can choose, how it links to the forestry and farm models, and where each piece sits in the code. Pick a block below to open it.
How OSeMOSYS decides
Schematic, not model output.
OSeMOSYS region
The OSeMOSYS REGION set has a single member, CAN. Every
parameter OSeMOSYS indexes by region — discount rate, carbon price, emission limit, reserve
margin — is set once for the whole country.
Geography lives in the names instead. A two-character zone code closes every
technology and fuel name (PWRHYDB2, ELCB202) and is read back by
taking the last two characters. Thirteen zones cover nine provinces; the four largest
provinces have two zones each.
B1 South Coast · B2 Interior & NorthA1 Calgary–Edmonton · A2 North (oil sands)O1 Southern · O2 NorthernQ1 Southern · Q2 Northern (hydro)S1 · M1 · L1T1 — NB + NS + PE aggregatedN1 — YT + NT + NU aggregated, no grid linksA province is a grouping used for policy and reporting only. The solver
never sees it; zones_of(province) resolves it to zones when constraints are built.
End-use demand
Demand comes from CER Energy Futures 2026 end-use demand: provincial totals for
four sectors (IND, RES, COM, TRA) and
their fuels. clean_demand.py maps the thirteen CER regions onto the nine model
provinces, then onto zones. National all-fuel demand is 9,328 PJ in 2025, of which
electricity is about 2,040 PJ.
Each provincial total is split across that province's zones by a fixed fraction,
ZoneDemandShare in model_structure.py. The fractions are
the modeller's estimates of where population and load centres sit, not measurements, and
they must sum to 1 within each province. Every sector and fuel in a province uses the same
split.
| Province | Zone | Share | Zone | Share |
|---|---|---|---|---|
| BC | B1 | 0.75 | B2 | 0.25 |
| AB | A1 | 0.65 | A2 | 0.35 |
| ON | O1 | 0.85 | O2 | 0.15 |
| QC | Q1 | 0.80 | Q2 | 0.20 |
| Others | single zone, share 1.0 | |||
This split, not the CER total, decides where transmission has to deliver and where new generation is worth building.
Reference energy system
The same set of components repeats in each of the 13 zones. Boxes are technologies: they have capacity, cost and activity, and the model decides how much of each to build and run. Rounded tags are fuels: each must balance every year. Hover over any component to bring it forward, or press its i for a plain-language note.
TRN crosses from one zone's copy of this diagram to another's. The territories
(N1) have no TRN link.Transmission
12 interconnectors join the 12 grid-connected zones; N1 (territories) has none. Each is two
technologies, one per direction, so 24 TRN technologies in all. The rating below becomes the
base-year ResidualCapacity of both directions; the model can add capacity on any link when it pays.
| Link | Rating (GW) | Kind | Technologies |
|---|---|---|---|
B1↔B2 | 3.00 | within a province | TRNB1B2 · TRNB2B1 |
A1↔A2 | 2.50 | within a province | TRNA1A2 · TRNA2A1 |
O1↔O2 | 4.00 | within a province | TRNO1O2 · TRNO2O1 |
Q1↔Q2 | 6.00 | within a province | TRNQ1Q2 · TRNQ2Q1 |
B1↔A1 | 1.20 | between provinces | TRNB1A1 · TRNA1B1 |
A1↔S1 | 0.15 | between provinces | TRNA1S1 · TRNS1A1 |
S1↔M1 | 0.25 | between provinces | TRNS1M1 · TRNM1S1 |
M1↔O1 | 0.30 | between provinces | TRNM1O1 · TRNO1M1 |
O1↔Q1 | 2.70 | between provinces | TRNO1Q1 · TRNQ1O1 |
Q1↔T1 | 1.10 | between provinces | TRNQ1T1 · TRNT1Q1 |
Q2↔L1 | 5.40 | between provinces | TRNQ2L1 · TRNL1Q2 |
T1↔L1 | 0.50 | between provinces | TRNT1L1 · TRNL1T1 |
Placeholder ratings. Intra-provincial values have no public source at zone
resolution, and interprovincial values are order-of-magnitude figures, not citations. One number per link
implies a symmetric rating, which real interties often are not (BC–AB, QC–ON). Capital cost is a flat
500 $/kW regardless of length, so a 50 km tie and a 1,100 km corridor look equally cheap. Phase 1 replaces
the interprovincial ratings with CER electricity-interchange-2026.
Source: TransmissionLinks, LineCapexPerKW in model_structure.py.
Trade-offs
One region with zones in the names, instead of one OSeMOSYS region per province. What that buys, and what it costs:
Trade has none of these: it is free, lossless, unbounded and
cannot be built.AnnualEmissionLimit row, and the discount rate and carbon price are set
once, so they cannot drift apart between provinces. With nine regions, stock OSeMOSYS has
no constraint that adds emissions across regions, so a national cap would itself need a
custom constraint. The gain is consistency and what can be written natively, not a smaller
problem for the solver.build_params.py collects a
province's technologies by composing names — PWRTRN{z} and
PWR{arch}{z} for each zone in zones_of(province) — so there is no
membership table to maintain. Move a zone into a province and every policy for that province
includes it on the next build.ELCO101 and ELCA101 differ by one character,
and the typo gives a feasible, plausible, wrong answer. harmonize.py checks
that codes exist, not that a technology's input and output share a zone.ZONE_WIDTH = 2 is assumed by every slice;
a three-character zone breaks the cleaners and plots without an error.AnnualEmissionLimit and
REMinProductionTarget now apply to Canada as a whole, so provincial caps and
targets had to be rebuilt as UDC, which needed a patch to the model file.osemosys_fast_osecan.txt is our
addition, reconstructed from the MUIO documentation. It does not ship with stock OSeMOSYS.Policy lever
Because a province is a group of technologies rather than a region, each provincial policy is one weighted sum over that group:
Σt ( mactt·Activityt + mcapt·TotalCapacityt + mnewt·NewCapacityt ) ≤ UDCConstant
| Policy | Multipliers written by build_params |
|---|---|
| Renewable share at least share s of a province's generation |
+1.11·s on each PWRTRN<z> in the province, −1 on each renewable plant, constant 0. A 50% Alberta target gives PWRTRNA1 = +0.555 and each Alberta renewable −1. |
| Emission cap at most L Mt CO₂ a year |
Each emitting plant's factor in Mt/PJ, constant L. |
The same mechanism reaches, without new model code: capacity floors and ceilings by
technology class (TotalCapacity multiplier), phase-out dates, build-rate limits
(NewCapacity), minimum intertie use, and a cap on biomass feedstock in chosen
zones. Adding one takes a key in ProvincialPolicies, a branch in the UDC loop and
a rebuild.
# build_params.py — renewable share for province p
for z in zones_of(p): # "AB" → A1, A2
mult[f"PWRTRN{z}"] = +1.11 * s
for arch in RenewableArchetypes: # HYD WND SOL BIO GEO
mult[f"PWR{arch}{z}"] = -1
No lookup table says which technologies belong to Alberta; the zone suffix does. This is the practical payoff of the naming convention for policy work.
Check before the first policy run. RenewableArchetypes does not
include BSW or BCW, so electricity from the WS3 and FABLE biomass
plants does not count toward a renewable share, while generic BIO does. It
also excludes nuclear, which most Canadian clean or non-emitting targets
include.
Keep UDC_TAG_INEQUALITY = 0. An equality on a renewable share forbids
over-achievement and turns a floor into a ceiling.
Status. ProvincialPolicies is empty, so no policy is
loaded. The mechanism is verified binding: a stress test held PWRHYDA1 at exactly
100.0 PJ in every year. A UDC that does not bind looks identical to a broken one, so diff every
policy run against base.
From data to decisions
The model is not run like a simulation. Its data and equations are compiled into one large linear program — a matrix of constraints — and a solver finds the single cheapest point that satisfies all of them at once.
Build pipeline
Stages 01, 04 and 05 run end to end. Stages 02 and 03 are the live front: two parameters have a real Canadian source, demand (CER EF2026, by zone) and the existing fleet (145 GW observed in 2021, with explicit retirement). Costs, efficiencies, capacity factors, emission factors and intertie ratings are still placeholders.
The stages are shown in the order they must run. Parameter skeleton comes before the
cleaners because build-params overwrites demand and fleet with placeholders if
it runs after them.
Select a stage to see its modules, configuration, raw sources and output files, then the assumptions and decisions behind it.
CLEWs coupling
OSeCAN models no land and no water (INCLUDE_LAND_WATER = False; the set builder
refuses to build them), and it has no agriculture sector. The whole connection to the forestry and
agriculture models is biomass, entering as a fuel. The figures below show that connection as
the code builds it.
WS3 → woody-residue supply SUPBCW
Today one fixed national ceiling, 50 PJ a year (WS3_FEEDSTOCK_CAP_PJ), for every
year of the 50-year horizon.
WS3 produces residue availability that changes over time with the harvest schedule. A flat cap removes the dynamics that are the reason to couple.
FABLE → switchgrass supply SUPBSW
Switchgrass area competes with food production in FABLE. OSeCAN receives only energy (PJ), so the step from hectares × yield × heating value to PJ sits outside both models and is written down nowhere yet.
Agree who owns that conversion before either side reports a bioenergy number.
model structure
Two national feedstock supplies feed 26 zonal plants (switchgrass and woody residue, 13 zones each).
A British Columbia harvest limit cannot restrain a BC plant, and residue can move from
Newfoundland to Alberta at no cost. Giving each zone its own feedstock supply
is the single most valuable structural change for the coupling, and it is cheap: the naming
convention already supports it (SUP<fs><z>).
WS3 → plant efficiency
Switchgrass and woody residue share one efficiency, 26.9% (input ratio 3.72), a generic placeholder. Moisture content and handling differ between them, which is what the WS3 supply curves are meant to resolve.
WS3 → feedstock price
A flat $4/GJ. Real feedstock cost rises steeply with the quantity mobilised, which is what makes a bioenergy result sensitive. A flat price with a hard cap gives an all-or-nothing answer.
WS3 ↔ emissions accounting
All three biomass plant types have zero CO₂ (co2 = 0.0), so burning biomass is
treated as carbon-neutral.
That holds only if WS3's forest carbon-stock accounting records the matching debit. If neither model does, the coupled system has a free decarbonisation lever.
PWRHYD), so a water limit in PJ is directly a generation limit in
PJ, with no new sets.There is no agri-food-water chain in OSeCAN. Demand has four sectors — industry, residential,
commercial and transport (IND RES COM TRA) — because those are the four CER Energy
Futures 2026 reports. Agriculture is folded into industry in the source data, and the demand
cleaner drops any sector it does not recognise without a warning.
The consequence for FABLE is specific: FABLE's agricultural energy demand has no place in this model. It cannot be constrained, reported or shocked on its own, and splitting it out of industry would need an assumption no current data source supports. Whether that split is in scope is a question for the next coupling meeting, not a data-cleaning task.
Technology register
OSeCAN has 522 technologies in six categories. Each row below is one technology
type; its code ends in a zone code wherever it repeats across the 13 zones. Values are
read from data/params/ and SETs/ as they stand today. Costs are
in 2020 CAD, energy in PJ, capacity in GW.
PWR<arch><z> · 10 plant types × 13 zones
The only technologies that compete. Each converts one input fuel into electricity at the
zone's generation bus ELC<z>01. The model chooses how much of each to build
and run, subject to cost, capacity factor, existing fleet and build limits.
| Technology | Code | Input fuel | Input per unit out → efficiency |
Capital $/kW | Fixed $/kW·yr | Variable $/GJ |
Life yr | Capacity factor | CO₂ Mt/PJ |
Existing 2021 GW | Build limit | Renewable share |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Thermal | ||||||||||||
| Natural gas | PWRNGS<z> | NGS national | 2.70 → 37.0% | 1,100 | 25 | 3.0 | 30 | 0.85 | 0.051 | 18.37 | none | no |
| Coal | PWRCOA<z> | COA national | 2.90 → 34.5% | 2,500 | 60 | 2.0 | 40 | 0.80 | 0.090 | 8.37 | 8.4 GW cap → 0 by 2030, in A1 A2 S1 T1 only | no |
| Nuclear | PWRURN<z> | URN national | 3.00 → 33.3% | 6,000 | 120 | 1.0 | 50 | 0.90 | 0 | 14.50 | 14.5 → 30 GW cap; 0 → 2 GW/yr build; A1 O1 Q1 S1 T1 only | no |
| Biomass, generic | PWRBIO<z> | BIO national | 2.85 → 35.1% | 3,000 | 90 | 2.5 | 30 | 0.70 | 0 | 2.57 | none | yes |
| Renewable, zonal resource | ||||||||||||
| Hydro | PWRHYD<z> | HYD<z> | 1.00 → 100% | 3,500 | 40 | 0 | 60 | 0.45 | 0 | 82.24 | 0.3 → 0.5 GW/yr build, each zone | yes |
| Wind | PWRWND<z> | WND<z> | 3.33 → 30.0% | 1,600 | 40 | 0 | 25 | 0.35 | 0 | 14.32 | 1 → 5 GW/yr build, each zone | yes |
| Solar PV | PWRSOL<z> | SOL<z> | 4.34 → 23.0% | 1,200 | 20 | 0 | 25 | 0.16 | 0 | 4.65 | 1 → 5 GW/yr build, each zone | yes |
| Geothermal | PWRGEO<z> | GEO<z> | 6.67 → 15.0% | 4,500 | 100 | 0 | 30 | 0.80 | 0 | 0 | none | yes |
| Biomass, WS3 / FABLE feedstock | ||||||||||||
| Switchgrass biomass | PWRBSW<z> | BSW from SUPBSW | 3.72 → 26.9% | 3,200 | 95 | 0 | 30 | 0.70 | 0 | 0 | feedstock ≤ 50 PJ/yr, national | no |
| Woody-residue biomass | PWRBCW<z> | BCW from SUPBCW | 3.72 → 26.9% | 3,200 | 95 | 0 | 30 | 0.70 | 0 | 0 | feedstock ≤ 50 PJ/yr, national | no |
| Total existing fleet, 2021 (CER EF2026, observed) | 145.02 | |||||||||||
All plants: output 1 PJ to ELC<z>01 per unit of activity;
one annual timeslice; CapacityToActivityUnit 31.536 PJ per GW-year. Every cost
and performance value is a generic placeholder, held flat from 2021 to 2070.
RNW<f><z>
resource technology carries a $3.0/GJ variable cost, and the plants above convert at an
input ratio above 1. Per GJ of electricity the model therefore pays $10.0 for wind,
$13.0 for solar, $20.0 for geothermal and $3.0 for hydro, against $11.1 for gas
($8.1 fuel + $3.0 variable) before carbon. model_structure.py describes those
ratios as harmless bookkeeping unless a resource limit is added; with a price on the
resource they are not. Setting VariableCost to 0 on RNW<f><z>
removes the distortion without touching the ratios.TotalAnnualMaxCapacity and …Investment to each technology, and
clean_capacity.py writes the same value from capacity.yaml to every
zone. The 1 GW/yr wind limit is therefore 13 GW/yr nationally (65 GW/yr by 2050), and
nuclear's 14.5 GW cap — the size of the whole Canadian fleet — appears in five zones, 72.5 GW
in total. Zones with no bound row are unbounded: nuclear in eight zones, new coal in nine.What the inputs imply
Computed on this page from the model's own inputs: capital annualised at the 5% discount rate over each plant's life, plus fixed cost, both spread over output at the capacity factor; then fuel or resource, variable cost and carbon at the model's penalty. OSeMOSYS never computes this number — it weighs all costs together across years and zones — but it shows quickly which inputs favour which plant.
1 $/GJ = 3.6 $/MWh. Ignores the timeslice, transmission, the existing fleet and build limits, all of which the model does account for. Hover a bar for its breakdown.
TRN<z><z′> · PWRTRN<z>
Moves electricity within and between zones. Transmission links are ordinary technologies,
so the model can invest in new capacity; OSeMOSYS Trade is not used.
| Technology | Code | Count | Takes | Delivers | Loss | Capital $/kW | Life yr | Capacity factor | Existing capacity |
|---|---|---|---|---|---|---|---|---|---|
| Interzonal transmission line | TRN<z><z′> | 24 | ELC<z>02 | ELC<z′>02 | 5% | 500 | 50 | 1.0 | 12 links, both directions; 27.1 GW each way, 54.2 GW total |
| Within-zone distribution | PWRTRN<z> | 13 | ELC<z>01 | ELC<z>02 | 9.9% | — | 50 | 1.0 | none needed: no capital cost, so capacity is free |
B1–B2 3.0A1–A2 2.5
O1–O2 4.0Q1–Q2 6.0
B1–A1 1.2A1–S1 0.15S1–M1 0.25
M1–O1 0.30O1–Q1 2.7Q1–T1 1.1
Q2–L1 5.4T1–L1 0.5
Shaded: within a province. Ratings are placeholders pending the CER
interchange cleaner. N1 has no link.
RNW · MIN · IMP
Creates the fuels everything else consumes. None has a quantity limit; each costs $3.0/GJ and has a one-year life, so supply is effectively a price.
| Technology | Code | Count | Produces |
|---|---|---|---|
| Zonal renewable resource | RNW<f><z> | 52 | WND HYD SOL GEO per zone |
| Generic biomass | RNWBIO | 1 | BIO, national, uncapped |
| Domestic extraction | MIN<f> | 4 | natural gas, uranium, crude, coal |
| Imports | IMP<f> | 8 | jet fuel, LPG, gasoline, diesel, heavy fuel oil, crude, coal, refined products |
SUP<fs> · the WS3 / FABLE seam
The two places where the forestry and agriculture models feed this one. Each supplies one feedstock to the 13 zonal plants of its type.
| Technology | Code | Partner | Cost $/GJ | Limit PJ/yr |
|---|---|---|---|---|
| Switchgrass supply | SUPBSW | FABLE | 4.0 | 50 |
| Woody-residue supply | SUPBCW | WS3 | 4.0 | 50 |
Both national and flat. Generic RNWBIO is cheaper ($3.0) and
uncapped, so these limits cannot bind yet — see Energy chains & coupling.
DEM<s><f><z> · 22 sector–fuel pairs × 13 zones
Each takes one fuel and delivers it, one for one, to a sector's demand in a zone, where CER EF2026 fixes the quantity. They have no cost and no choice between them: the fuel mix within a sector is CER's projection, not the model's decision. More than half the technology count sits here.
INDELC NGS DSL BIO HDG
RESELC NGS RPP BIO HDG
COMELC NGS RPP BIO HDG
TRAELC GSL DSL JFL NGS BIO HDG
Electricity is taken from the grid bus ELC<z>02; every other
fuel comes straight from national supply. Example: DEMTRAGSLO1 delivers gasoline to
transport in Southern Ontario.
HDG · CCS
| Technology | Code | Takes | Produces | What it means today |
|---|---|---|---|---|
| Hydrogen production | HDG | nothing | HDG | Serves all hydrogen demand in every sector at no cost and no energy input. |
| Carbon capture & storage | CCS | nothing | CO2 | Reserves the name only; not linked to any emitting plant. |
Outside the model today. Results say nothing about these.
Storage sets are not built; battery capacity in the CER data is dropped.
No plant type; 5.9 GW of observed capacity is dropped, mostly in Newfoundland and the North.
Export fuels are declared, but no export technology is generated.
Nothing lets the model electrify heat or transport; only the demand projection can.
One annual timeslice: no peak, no ramping, no seasonal hydro.
CO₂ only; no methane, so upstream oil and gas emissions are outside the model.
Codebase
The package lives in src/osecan/. Each summary below is the first line of the module's own
docstring, read when this page was built, so it follows the code rather than a hand-kept list.
Layers run in order: structure defines sets and wiring, data stages and cleans parameters,
pipeline assembles and solves, vis reports.
| Module | Layer | Role |
|---|---|---|
cli.py | interface | OSeCAN command-line interface (Typer). |
data/build_params.py | data | Build a complete placeholder parameter set for OSeCAN. |
data/clean_capacity.py | data | Convert CER electricity-capacity data into OSeCAN capacity parameters. |
data/clean_demand.py | data | Convert CER end-use demand data to OSeCAN demand parameters. |
data/harmonize.py | data | Validate Stage-3 parameters against the built OSeMOSYS sets. |
data/profile.py | data | Profile a raw dataset before writing a cleaner. |
data/retrieve.py | data | Retrieve open datasets and record their provenance. |
model_structure.py | structure | OSeCAN model structure for the Canadian energy system. |
pipeline/assemble.py | pipeline | Assemble a complete otoole input directory from sets and parameters. |
pipeline/diagnostics.py | pipeline | Capture and summarize solver diagnostics. |
pipeline/review.py | pipeline | Maintain the OSeCAN modelling review log. |
pipeline/runner.py | pipeline | Run the complete OSeCAN model pipeline. |
pipeline/solve.py | pipeline | Solve the OSeMOSYS model with GLPK bindings and extract result CSVs. |
sets_n_ratios.py | structure | Build OSeCAN OSeMOSYS sets and activity ratios. |
vis/canada_svg.py | vis | Build a Canada silhouette SVG for the project website. |
vis/report.py | vis | Generate a template-driven OSeCAN model report. |
vis/site.py | vis | Generate a multi-page static website from the live codebase and documentation. |
vis/transmission_map.py | vis | Render the OSeCAN transmission network on Canadian province boundaries. |
Command line
All steps run through one Typer CLI, uv run osecan <command>. The order to run them
is on the API page.
| Command | Does |
|---|---|
osecan build-params | Generate placeholder parameters for the current model sets. |
osecan build-sets | Generate OSeMOSYS sets and activity-ratio CSV files. |
osecan clean-capacity | Convert observed CER capacity to residual capacity and annual bounds. |
osecan clean-demand | Convert CER end-use demand to SpecifiedAnnualDemand parameters. |
osecan harmonize | Check that parameter CSVs reference valid generated codes and years. |
osecan info | Print the model name, horizon, provinces, and land/water scope.""" |
osecan map | Generate a transmission map using capacity or solved flows. |
osecan profile | Print a dataset's schema and categorical values before cleaning. |
osecan report | Generate the interactive HTML report for model inputs and outputs. |
osecan retrieve | Download configured open datasets and record SHA-256 provenance. |
osecan run | Assemble inputs, solve the model, and extract results with diagnostics. |
osecan site | Generate the project website from the live codebase and documentation. |
osecan solve | Solve a prepared run with GLPK bindings and extract result CSVs. |