Economic Analysis of Results#
This guide describes the economic interpretation framework built into
GreenBubble’s post-processing pipeline (scripts/plots.py). It covers:
how the network represents markets via buses and shadow prices,
the product topology (collection bus + delivery bus),
the Levelised Cost of Production (LCOP) formula and its components,
the KKT-based LCOP verification (zero-profit condition),
the short-run marginal cost (SRMC) and merit order,
the full KKT revenue and annual profit, and
how to interpret LCOP versus the shadow price at the collection bus.
—
Network topology as a market#
GreenBubble is a linear PyPSA model. Every bus is a commodity market
at a specific location and quality level. The KKT multiplier at a bus
(n.buses_t.marginal_price) is the shadow price of the energy balance
constraint there — the marginal value of one additional unit of that
commodity at that time step.
Links are technologies that convert commodities. A multilink with
ports bus0 … busN satisfies:
flow at bus_k = p0 × efficiency_k (k ≥ 1)
flow at bus0 = −p0 (convention: bus0 always consumes)
Positive efficiency_k → the link produces at bus_k (output or by-product).
Negative efficiency_k → the link consumes from bus_k (additional input).
—
Product topology#
Every product (bioCH4, H2, Methanol) uses a two-bus structure:
[technology A] ──┐
[technology B] ──┤──► bioCH4 collection ──► bioCH4 delivery ──► demand / store
[technology C] ──┘ ▲
is_product_bus = True
bioCH4 collectionThe collection bus. All technologies producing bioCH4 inject here directly via
bus1of their multilink. Taggedis_product_bus=Truein the network so post-processing can discover it automatically.bioCH4 deliveryThe delivery bus. The exogenous demand load (demand mode) or the price-setting sale link (price mode) is attached here.
Demand mode: a zero-cost
{product}_collection_to_demandlink transfers product to the delivery bus, where aLoadsets the time profile and an optional cyclicStoreprovides intra-period flexibility.Price mode: a
{product}_collection_to_deliverylink carries a time-varyingmarginal_costequal to the exogenous selling price series. A non-cyclic annualStore(e_nom_max = total_demand) caps cumulative annual production.
The shadow price at the collection bus (λ_collection) is determined at
equilibrium by the marginal producer — the highest-cost technology that is
still dispatched. Technologies with lower cost earn an intra-marginal rent.
—
LCOP — Levelised Cost of Production#
For a multilink lk whose bus1 is a collection bus (is_product_bus=True),
first define the indirect OPEX:
where \(\eta_0 = -1\) for bus0 (primary feedstock, always consumed), and
\(\overline{(\lambda_k \cdot p_0)} = \sum_t p_{0,t} \cdot \lambda_{k,t} \cdot w_t\)
with snapshot weights \(w_t\) from snapshot_weightings["objective"].
The sign convention makes indirect OPEX positive when input costs dominate (the typical case) and negative only if by-product credits exceed all feedstock costs.
Port |
\(\eta_k\) |
Contribution to indirect OPEX |
|---|---|---|
bus0 — primary feedstock |
−1 |
\(+p_0 \cdot \lambda_{\text{bus0}} \cdot w\) — adds feedstock cost |
bus2..N — additional input |
< 0 |
\(+|\eta_k| \cdot p_0 \cdot \lambda_k \cdot w\) — adds feedstock cost |
bus2..N — by-product output |
> 0 |
\(-\eta_k \cdot p_0 \cdot \lambda_k \cdot w\) — subtracts by-product credit |
Then:
where:
\(\text{CAPEX}\) =
n.statistics.capex()for the link (annualised capital cost)\(\text{OPEX}\) =
n.statistics.opex()for the link (explicitmarginal_cost× dispatch — variable O&M such as enzyme or maintenance rates, not feedstock costs)\(Q_\text{main} = \sum_t p_{0,t} \cdot \eta_1 \cdot w_t\) — annual production at bus1
No double-counting: OPEX is the explicit variable O&M declared in
tech_costs (VOM). Indirect OPEX captures the market value of feedstocks
consumed and by-products produced via KKT shadow prices. These measure
different things and do not overlap.
Existing (EXI_) assets: whether CAPEX appears in the LCOP depends on
remaining_investment_fraction (rif) in n_config.yaml:
rif = 0(default — sunk cost):capital_cost = 0in the network, soCAPEX = 0from statistics. LCOP reflects short-run cost only (OPEX + indirect OPEX).rif > 0(residual obligation outstanding):capital_cost = rif × I(construction_year) × annuity(amortization_period, discount_rate). CAPEX is non-zero and the LCOP includes this residual capital charge.
See Brownfield initial conditions for the full parameter description.
—
Revenue, net market value, and annual profit#
This is the market value of the main product at the collection bus — what the market would pay for everything the technology produces there.
Annual profit is the economic rent: what the market pays the technology for its product, minus every cost it incurs (capital, O&M, feedstocks net of by-products).
annual profit ≈ 0 — the technology is the marginal (price-setting) producer.
annual profit > 0 — intra-marginal rent; technology has lower cost than the price-setter.
annual profit < 0 — technology is loss-making under current market conditions (possible for EXI_ assets if market prices are low).
Note
The zero-profit condition holds for optimally expanded technologies in a fully endogenous problem. When exogenous selling prices are set (price mode), the complementary slackness argument breaks down and all dispatched technologies can earn positive profit if the price exceeds the LCOP of the marginal producer.
Intra-marginal rent:
rent_per_MWh ≈ λ̄_collection − LCOP
where λ̄_collection is the energy-weighted mean shadow price at the
collection bus (from shadow_prices_mean.csv). The technology with
LCOP ≈ λ̄_collection is the price-setter; all others earn rent proportional
to the cost gap.
—
Output files#
The following output files are generated by scripts/plots.py:
csv/lcop_by_technology.csvOne row per product link (index column:
link).Column
Description
carrierPyPSA carrier name of the link
productProduct name (bioCH4, H2, Methanol …)
CAPEX (EUR)Annualised capital cost from
n.statistics.capex()OPEX (EUR)Variable O&M from
n.statistics.opex()(explicit marginal cost × dispatch)indirect OPEX (EUR)Feedstock costs minus by-product credits via KKT shadow prices; positive = net cost (typical), negative = by-products exceed feedstock cost
revenue main product (EUR)Market value of annual main-product output at the collection bus
net market value (EUR)revenue main product−indirect OPEXannual production (MWh)Annual energy output at bus1 = \(\sum_t p_{0,t} \cdot \eta_1 \cdot w_t\)
LCOP (EUR/MWh)(CAPEX + OPEX + indirect OPEX) / annual productionannual profit (EUR)net market value − CAPEX − OPEX; economic rent earned by the technologyplots/lcop_by_technology.pngTwo-panel bar chart: LCOP [€/MWh] (top) and annual profit [k€] (bottom).
csv/lcop_kkt_by_technology.csvVerification table produced by
compute_lcop_kkt_by_technology(). For each product link, LCOP is independently computed as the production-weighted average KKT shadow price at the collection bus:\[\text{LCOP}_\text{kkt} = \frac{\displaystyle\sum_t w_t \cdot \eta_1 \cdot p_{0,t} \cdot \lambda_{\text{bus1},t}} {\displaystyle\sum_t w_t \cdot \eta_1 \cdot p_{0,t}}\]At optimum this equals the cost-based LCOP (zero-profit condition). The
diff cost−kktcolumn confirms agreement to < 0.01 €/MWh. Columns:carrier,product,annual_production_MWh,LCOP_cost (EUR/MWh),LCOP_kkt (EUR/MWh),diff cost−kkt (EUR/MWh),π_bus1_mean,π_bus1_std,π_bus1_prod_weighted.csv/srmc_by_technology.csv/plots/srmc_by_technology.pngShort-run marginal cost (SRMC) computed by
compute_srmc_by_technology(). For each product technology at every snapshot:\[\text{SRMC}_{s,t} = \frac{\lambda_{\text{bus0},t} - \displaystyle\sum_{k \geq 2} \eta_k \cdot \lambda_{\text{bus}_k,t} + \text{VOM}_{s}}{\eta_1}\]This is the instantaneous cost of producing one more MWh of main product at time t, given current input market prices. It is distinct from the model input
marginal_coston links (which is the VOM, one term in the formula).The long-form CSV has columns:
snapshot,link,product,SRMC_EUR_per_MWh,dispatch_MW,π_product_bus,in_merit(whether SRMC ≤ product shadow price at that hour).The plot shows one subplot per product with SRMC time series per technology and the product bus shadow price as a dashed reference line.
—
Shadow price outputs#
csv/shadow_prices_mean.csvEnergy-weighted mean KKT for every bus in
plots_config.yaml → bus_list_mp, including collection buses. Computed regardless of whether any technology actively dispatches to the bus.Formula:
\[\bar{\lambda}_\text{bus} = \frac{\sum_t \lambda_{t} \cdot q_t \cdot w_t}{\sum_t q_t \cdot w_t}\]where \(q_t\) is the net injection at the bus (generators + positive-efficiency link outputs + storage discharge), and \(w_t\) is the snapshot weight. Falls back to duration-weighted mean when a bus has no measurable injection.
Columns:
Column
Description
bus(index)Bus name from
bus_list_mpenergy weighted mean (EUR/MWh)Energy-weighted mean shadow price at that bus
plots/shd_prices_mean_bar.pngBar chart of energy-weighted mean shadow prices. Collection buses are excluded — delivery buses are sufficient to read the product market price. Buses with no active injection are also dropped.
plots/shd_prices_violin.png,plots/shd_prices_ldc.pngSnapshot distribution of shadow prices (one observation per time step), with the scenario-weighted mean marked. These show when prices are high or low, not an energy-weighted average. Restricted to the same delivery-bus subset as the bar chart, further filtered to buses with at least one injecting link above the
LINK_THcapacity threshold.
—
Comparing LCOP to shadow prices#
A practical diagnostic workflow:
Look at
shadow_prices_mean.csv— read theenergy weighted mean (EUR/MWh)for the delivery bus of each product (e.g.bioCH4 delivery) — this isλ̄_delivery, which equalsλ̄_collectionwhen the collection-to-delivery link is zero-cost.Look at
lcop_by_technology.csv— compare each technology’sLCOP (EUR/MWh)toλ̄_deliveryof its product.Technologies with
annual_profit_EUR ≈ 0are price-setters.Technologies with
annual_profit_EUR > 0earn intra-marginal rent — check whether they are EXI_ (legacy) assets or optimally expanded.In price mode, all technologies that run may earn positive profit if the exogenous selling price exceeds the LCOP of the marginal producer.
Use
srmc_by_technology.csvto see when each technology is in-merit (in_merit = True). Hours with SRMC near the shadow price reveal the marginal technology. Large SRMC variance indicates strong sensitivity to electricity or H2 price fluctuations.