NewlyInstalledUnits, cap_max_unit, and base costs
This page documents the integer-unit investment formulation that the
geopipe branch of CESM adds on top of the continuous capacity
expansion formulation in CESM/main. It is not part of upstream
CESM/main — anyone reading the upstream model will not find the
NewlyInstalledUnits variable, the build_activation constraint, or
the capex_cost_base / cap_max_unit parameters there.
The relevant code lives in CESM/src/cesm/core/model.py and the
schema in CESM/src/cesm/core/init_queries.sql.
Motivation
The geopipe branch adds an explicit per-unit investment decision so that:
- installing a new unit incurs a fixed cost (
capex_cost_base) on top of the size-proportional cost (capex_cost_power), - the size of each newly built unit is capped from above
(
cap_max_unit), so building 1 GW of a technology that comes in 100 MW blocks requires 10 units, and - the per-unit decision is integer (or binary, see below)
Inputs
Two param_cs_y / param_cs columns drive the formulation. Both
default to values that make the formulation a no-op, so existing
techmaps continue to behave like upstream CESM/main until the user
sets them.
| Parameter | Index | Default | Meaning |
|---|---|---|---|
capex_cost_base |
(CS, Y) | 0 |
Fixed investment cost incurred per newly installed unit, in the model's currency unit. |
cap_max_unit |
(CS) | 1e6 |
Maximum capacity (in power units) of a single newly installed unit. Sentinel = no limit. |
The 1e6 default for cap_max_unit is a sentinel, not a real
cap — see Binary fallback for what it triggers.
The base_cost_cs set
NewlyInstalledUnits and build_activation are only defined for
conversion subprocesses that actually have a non-zero base cost. The
DAO exposes this as the set
conversion_subprocess_base_costs (referred to in the code as
base_cost_cs):
SELECT DISTINCT cp.name, cin.name, cout.name
FROM param_cs_y
JOIN conversion_subprocess AS cs ON cs_id = cs.id
...
WHERE capex_cost_base > 0;
A CS is included iff at least one (cs, y) row has
capex_cost_base > 0. Setting capex_cost_base = 0 for every year
keeps a CS out of the set entirely, so it pays no fixed cost and is
not subject to the unit-size cap — the model behaves exactly like
upstream for that CS.
Decision variable
Defined in _add_var (model.py:53).
Integer by default; downgraded to binary for any CS whose
cap_max_unit is left at the \(10^{6}\) sentinel (see below).
Constraint: build_activation
Source: _add_constr (model.py:109).
This is the link between the continuous capacity variable and the integer unit-count variable. Three regimes:
- \(\mathrm{NewlyInstalledUnits} = 0\) \(\Rightarrow\) \(\mathrm{Cap\_new} \le 0\), i.e. no capacity may be added in year \(y\) (\(\mathrm{Cap\_new} \ge 0\) is implicit from Gurobi defaults).
- \(\mathrm{NewlyInstalledUnits} = k > 0\) \(\Rightarrow\) \(\mathrm{Cap\_new} \le k \cdot \mathrm{cap\_max\_unit}\), i.e. \(k\) units' worth of capacity may be added.
- No constraint in the other direction. The model is free to pay for \(k\) units and use less than \(k \cdot \mathrm{cap\_max\_unit}\) of capacity. The fixed cost still gets paid in full — the optimiser will only do this if it is cheaper than the alternative (typically it is not).
The constraint is only added for cs ∈ base_cost_cs. Subprocesses
without a base cost have Cap_new unconstrained from above by this
mechanism, exactly as in upstream CESM/main. Other caps (cap_max)
still apply.
Binary fallback
If a user enables base costs (\(\mathrm{capex\_cost\_base} > 0\)) but
does not set a meaningful cap_max_unit, the parameter takes its
default \(10^{6}\). In that case we know nothing about the size of
one unit, so build_activation would let NewlyInstalledUnits blow
up to large integers without changing the feasible region of
Cap_new. To keep the integer domain small, the variable is
downgraded to binary (vtype = GRB.BINARY) per \((cs, y)\):
if cap_max_unit == 1e6:
for y in get_set("year"):
vars["NewlyInstalledUnits"][cs, y].vtype = GRB.BINARY
Source: model.py:59-65.
Effect: a binary "did we build anything in year \(y\)?" decision
that turns the fixed cost into a one-shot charge, while leaving
Cap_new otherwise unconstrained by build_activation (since
\(\mathrm{Cap\_new} \le 10^{6} \cdot 1\) is non-binding for any
realistic input). This makes the formulation behave like a classic
fixed-charge-on-investment model when the unit size is unknown.
Practical note. If you care about unit count, always set
cap_max_unitexplicitly. Leaving the default makesNewlyInstalledUnitsa 0/1 indicator, not a unit count, and theoutput_cs_y.newly_installed_unitscolumn will reflect that.
Cost contributions
CAPEX
build_activation only ties Cap_new to NewlyInstalledUnits; the
fixed cost itself is added to the CAPEX expression in
_add_constr (model.py:97-102):
where \(d(y)\) is the discount factor returned by
DAO.get_discount_factor(y). The first sum is the upstream term;
the second sum is the addition introduced by this branch.
Salvage value
Salvage is computed in salvage_value_rule per \((cs, y)\) for any
year \(y\) that is within technical_lifetime of the planning
horizon. For CSs in base_cost_cs, the per-unit fixed cost is
salvaged on the same straight-line basis as the size-proportional
cost
(model.py:146-153):
The whole term is then discounted to the last planning year. Without
this addition (i.e. on CESM/main), the model would charge the
fixed cost in the year of installation but never refund its unused
remainder at the horizon, biasing late-horizon investments against
base-cost technologies.
Output
NewlyInstalledUnits is persisted to the output_cs_y table via the
newly_installed_units column (added on this branch; default 0).
For CSs outside base_cost_cs, 0 is written:
newly_installed_units = (
vars['NewlyInstalledUnits'][cs, y].X
if cs in base_cost_cs_set
else 0
)
Source: model.py:488.
Quick sanity checklist
- A CS that should keep upstream behaviour: leave
capex_cost_baseunset (or0) for every year. It is not added tobase_cost_cs, noNewlyInstalledUnitsvariable is created, and nobuild_activationconstraint is added. - A CS that should be modelled as discrete blocks of a known size:
set both
capex_cost_base > 0andcap_max_unitto the block size.NewlyInstalledUnitsis integer. - A CS that should be modelled with a fixed-charge on investment but
no per-block size: set
capex_cost_base > 0and leavecap_max_unitat the default.NewlyInstalledUnitsis binary.