Guide for Adding New Technologies#
This guide explains how to add a new technology inside an existing main
add_ function in scripts/prepare_network.py. A later section will
cover adding an entirely new add_ function.
Overview#
Each sector of the GreenBubble network is built by a top-level function:
add_biogas() add_electrolysis() add_meoh()
add_methanation() add_central_heat_MT() add_symbiosis()
These are called in sequence by build_network(). Each function must work
independently — the optimisation problem must be feasible whether or not the
other functions run.
—
Step 1 — Register the technology in n_config#
n_config is loaded from a CSV file and indexed by technology name.
Every technology that can be switched on/off or sized needs the following
columns:
Column |
Meaning |
|---|---|
|
MW installed from day one (0 = none) |
|
|
|
Multiplier applied to the base capital cost from |
Add a row for each variant of the new technology
(e.g. "biomethanation CO2").
—
Step 2 — Declare the tech name in the techs list#
Inside the relevant add_ function, append the technology name to the
techs list exactly as it appears in n_config:
techs = ["methanolisation", "my new tech"] # existing + new
cap_to_add, exp_to_add = tech_to_add(techs, n0_dict)
tech_to_add() compares n_config with the current network state and
returns:
cap_to_add— techs that need an initial-capacity component (EXI_prefix)exp_to_add— techs that need an extendable expansion component
—
Step 3 — Call add_targets() before building the plant#
If the technology produces a product sold or delivered to an external bus
(H2, bioCH4, Methanol), call add_targets before adding the multilink.
for t in techs:
if t in cap_to_add or t in exp_to_add: # correct form — see Key Rules
n.add("Carrier", t)
n, product_bus = add_targets(
n, plant=t,
inputs_dict=inputs_dict,
tech_costs=tech_costs,
n_options=n_options,
targets_dict=targets_dict,
)
my_buses.at["product bus", t] = product_bus
add_targets creates the following network elements (once per product, shared
across all plants that produce it):
"{product} collection"— shared collection bus, returned asproduct_busand taggedis_product_bus=Trueso other modules can discover itCollection-to-delivery/demand link and Store/Load at the delivery bus (
"{product} delivery") — created once regardless of how many plants run
Plants inject directly into the collection bus via their multilink’s bus1.
There is no per-technology intermediate bus.
Important
Do not copy product bus from another column of my_buses inside
your add_*_cap_exp function. add_targets has already set it
correctly for both price and demand drivers. Overwriting it breaks
price mode.
—
Step 4 — Write the add_*_cap_exp inner function#
This inner function receives prefix, capital_cost, capacity,
expansion, carrier, and the buses DataFrame. It should:
Update the buses DataFrame for the new tech by copying shared buses from the base column — never the
product busrow.Add multilinks or generators using those buses.
Return
(n, my_buses).
def add_my_tech_cap_exp(n, prefix, capital_cost, capacity, expansion, carrier, my_buses):
# Copy shared buses from base column — never overwrite 'product bus'
my_buses.at["H2 in bus", t] = my_buses.at["H2 in bus", "base"]
my_buses.at["CO2 in bus", t] = my_buses.at["CO2 in bus", "base"]
my_buses.at["local EL bus", t] = my_buses.at["local EL bus", "base"]
# product bus is already set by add_targets — do NOT copy it here
name = f"{prefix}{t}"
n.add(
"Link", name,
carrier=carrier,
bus0=my_buses.at["H2 in bus", t],
bus1=my_buses.at["product bus", t], # collection bus — direct injection
efficiency=...,
p_nom=capacity,
p_nom_extendable=expansion,
capital_cost=capital_cost if expansion else 0,
marginal_cost=...,
)
return n, my_buses
—
Step 5 — Call the inner function for capacity and expansion#
if t in cap_to_add:
cap = n_config.at[t, "initial capacity"]
n, my_buses = add_my_tech_cap_exp(
n, "EXI_", 0, cap, False, carrier=t, my_buses=my_buses)
if t in exp_to_add:
cost = tech_costs.at["my base tech", "fixed"] * n_config.at[t, "cost factor"]
n, my_buses = add_my_tech_cap_exp(
n, "", cost, 0, True, carrier=t, my_buses=my_buses)
The "EXI_" prefix marks initial-capacity (fixed) components. The
expansion component uses an empty prefix and carries the capital cost.
—
Step 6 — Log new components and return#
new_components = log_new_components(n, n0_dict)
return n, new_components
log_new_components diffs the network before and after to return a dict of
newly added links, buses, stores, etc., used for reporting and stochastic setup.
—
Step 7 — Handle the case where nothing is added#
If neither list has entries for this sector, return an empty dict immediately so the function is a clean no-op:
if not (cap_to_add or exp_to_add):
empty = {k: [] for k in
["links", "generators", "loads", "stores", "buses", "storage_units"]}
return n, empty
—
Key Rules#
Rule |
Why |
|---|---|
Always write |
|
Call |
Sets |
Never overwrite |
|
Set |
Plants inject directly into the collection bus — no per-tech intermediate bus |
Use |
Idempotent — safe even if another tech already created the bus |
Tag any shared bus via |
Lets |
Return |
Every |
—
Product keyword matching in add_targets#
add_targets infers which product bus topology to use from the plant name:
Keywords in plant name (case-insensitive) |
Product |
Collection bus |
|---|---|---|
|
bioCH4 |
|
|
H2 |
|
|
Methanol |
|
Name the new technology so it contains the right keyword, or extend
build_bus_list_demand_or_price inside add_targets for a new product
type.
Adding a demand for a new product#
If the new technology produces a product whose demand and delivery store are not yet defined, you also need to configure the demand system. See the full reference at Setting Product Demands. The short version:
Config — add the demand target and mode keys in
config/config.yamlundertargets:targets: demand_MyProduct: 50000 # MWh/year MyProduct_demand_mode: bins_flat # flat | profile | bins_flat | bins_profile MyProduct_bins: 12 MyProduct_flexibility: 0.0
Preprocessing — call
build_product_demand_tsinsidebuild_demands_TSinscripts/preprocessing.py:myproduct_ts, myproduct_e_nom_max = build_product_demand_ts( annual_demand = cfg["targets"]["demand_MyProduct"], mode = cfg["targets"].get("MyProduct_demand_mode", "bins_flat"), snapshots = snapshots, n_bins = cfg["targets"].get("MyProduct_bins", 1), flexibility_fraction = cfg["targets"].get("MyProduct_flexibility", 0.0), store_buffer = cfg["targets"].get("demand_store_buffer", 0.0), col_name = "demand MWh", ) inputs_dict["MyProduct_demand_ts"] = myproduct_ts inputs_dict["MyProduct_store_e_nom_max"] = myproduct_e_nom_max
Network — in
scripts/prepare_network.py, wire the load and delivery store inside the relevantadd_<sector>()function following the same pattern used inadd_targets_per_product.
Note
The delivery store is automatically sized by build_product_demand_ts
based on the chosen mode. Do not set e_nom_max manually in the network
script — read it from inputs_dict["MyProduct_store_e_nom_max"].
Economic interpretation of the new technology#
Once the network is built and solved, the post-processing pipeline automatically computes economic metrics for every technology injecting into a collection bus. See Economic Analysis of Results for the full derivation. The short version relevant to technology design:
- LCOP (Levelised Cost of Production)
Defined over the multilink’s bus ports:
\[\text{LCOP} = \frac{\text{CAPEX} + \text{OPEX} - \sum_{k \neq \text{bus1}} \eta_k \cdot \overline{(\lambda_k \cdot p_0)}}{Q_\text{main}}\]where
indirect OPEX= feedstock costs − by-product credits via KKT (positive = net cost). Bus0 uses \(\eta_0 = -1\) (primary feedstock consumed); additional input ports add cost; by-product ports give credit. Shared components (compressors, storage) are excluded — their cost enters via the KKT at the shared interface bus.
Topology choices that affect LCOP:
bus1must be the collection bus (is_product_bus=True) so the pipeline discovers the link automatically.By-product ports (heat, CO2, etc.) with positive efficiency contribute a KKT credit. Connect them to the correct shared bus so their shadow price is non-zero.
Additional input ports (eff < 0) will add to the LCOP numerator via the KKT at that bus — make sure the bus has a meaningful shadow price (i.e. it is connected to a real supply with cost).
Annual profit
\[\text{annual profit} = \text{revenue main product} - \text{indirect OPEX} - \text{CAPEX} - \text{OPEX}\]A value near zero means the technology is the marginal (price-setting) producer for its product; a positive value indicates intra-marginal rent.