energy_penalty() exits after the first technology: five of six CC technologies never get the capture energy penalty
Note: Claude found this issue, so below is a summary by it.
Summary
In scripts/compile_cost_assumptions.py, energy_penalty() loops over six carbon-capture technologies and adds an auxiliary steam boiler to cover the heat demand of the capture unit, rescaling investment, VOM, efficiency and (for CHPs) heat efficiency accordingly.
The loop body contains a break where pass appears to have been intended. central solid biomass CHP CC is the first item in the list and has a non-zero VOM, so the loop exits during the first iteration — after the investment update but before the VOM, efficiency and efficiency-heat updates.
The net effect on the published outputs/costs_*.csv:
central solid biomass CHP CC receives the investment uplift but keeps its unpenalized electrical and heat efficiency and its unadjusted VOM.
waste CHP CC, solid biomass boiler steam CC, direct firing solid fuels CC, direct firing gas CC and biogas CC receive nothing at all — their investment, VOM and efficiency are identical to their non-CC counterparts.
Downstream this makes carbon capture cheaper and more efficient than the function intends, which matters for any study comparing BECCS routes against each other or against unabated conversion.
Location
scripts/compile_cost_assumptions.py, in energy_penalty() — the if begins at line 3313 on current master:
if cost_dataframe.loc[(tech_name, "VOM"), "value"]:
break
else:
cost_dataframe.loc[(tech_name, "VOM"), "value"] = 0.0
Everything after this block — the VOM rescaling, efficiency = eta_main, and the if "CHP" in tech_name heat-efficiency block — is unreachable for every technology whose VOM is non-zero, and unreachable for all subsequent technologies regardless.
The same code is present at least as far back as v0.11.0, so the outputs have been affected for a long time.
Evidence in the shipped outputs
The source field records which lines executed, because the function overwrites it with "Combination of <tech> and <boiler>". From outputs/costs_2050.csv:
| technology |
parameter |
value |
source |
| central solid biomass CHP CC |
investment |
6003.1610 |
Combination of central solid biomass CHP CC and solid biomass boiler steam |
| central solid biomass CHP CC |
efficiency |
0.2652 |
Danish Energy Agency, inputs/technology_data_for_el_and_dh.xlsx |
| central solid biomass CHP CC |
VOM |
6.2350 |
Danish Energy Agency, inputs/technology_data_for_el_and_dh.xlsx |
| waste CHP CC |
investment |
9442.6408 |
Danish Energy Agency, inputs/technology_data_for_el_and_dh.xlsx |
The first row shows the investment line ran; the second and third show the loop had already exited; the fourth shows the second technology was never reached. waste CHP CC, solid biomass boiler steam CC, direct firing solid fuels CC, direct firing gas CC and biogas CC all carry investment and efficiency values identical to their non-CC rows.
Magnitude
Re-running the intended arithmetic on costs_2050.csv (capture heat-input 0.66 MWh/tCO₂; boiler efficiencies 0.90 solid biomass / 0.94 gas; CO₂ intensities 0.3667 solid biomass, 0.198 gas):
| technology |
investment now |
intended |
Δ |
efficiency now |
intended |
Δ |
| central solid biomass CHP CC |
6003 |
6003 (already applied) |
— |
0.2652 |
0.2090 |
−21.2 % |
| waste CHP CC |
9443 |
12 741 |
+34.9 % |
0.2165 |
0.1706 |
−21.2 % |
| solid biomass boiler steam CC |
679 |
1044 |
+53.8 % |
0.9000 |
0.7093 |
−21.2 % |
| direct firing solid fuels CC |
279 |
518 |
+85.9 % |
1.0000 |
0.7881 |
−21.2 % |
| direct firing gas CC |
19 |
29 |
+53.5 % |
1.0000 |
0.8779 |
−12.2 % |
| biogas CC |
1091 |
1250 |
+14.6 % |
1.0000 |
0.8779 |
−12.2 % |
Heat efficiencies also change for the two CHPs: central solid biomass CHP CC 0.8294 → 0.9433, waste CHP CC 0.7625 → 0.8906.
So the shipped cost data understates capture-inclusive investment by 15–86 % for five technologies, and overstates the conversion efficiency of all six by 12–21 %.
Secondary issues in the same function
1. The elif "biogas" branch is unreachable. The dispatch is ordered
if "powerboost" in tech_name: ...
elif "gas" in tech_name: ...
elif "biogas" in tech_name: ...
else: ...
"gas" in "biogas CC" is True, so biogas CC is always caught by the "gas" branch and takes the natural-gas CO₂ intensity (0.198 tCO₂/MWh) instead of its own CO2 stored (0.1447 tCO₂/MWh). Once the break is fixed, this changes biogas CC from investment 1250 / efficiency 0.8779 to 1207 / 0.9078. Testing "biogas" before "gas" would fix it.
2. The "powerboost" branch is dead. central solid biomass CHP powerboost CC is defined in the sheet mappings but is absent from the technology list the loop iterates over, so the branch never runs.
3. The heat-efficiency block hardcodes the solid-biomass CO₂ intensity.
if "CHP" in tech_name:
cost_dataframe.loc[(tech_name, "efficiency-heat"), "value"] = (
cost_dataframe.loc[(tech_name, "efficiency-heat"), "value"] * scalingFactor
+ cost_dataframe.loc[("solid biomass", "CO2 intensity"), "value"] * (...)
)
This uses solid biomass rather than the branch's own co2_capture, which is wrong for waste CHP CC (whose capture is applied to the oil CO₂ intensity downstream in PyPSA-Eur).
4. feedstock is assigned in three branches and never read, and is not assigned in the "biogas" branch — harmless today, but it suggests the dispatch was refactored at some point and the break is a leftover from that.
Suggested fix
- if cost_dataframe.loc[(tech_name, "VOM"), "value"]:
- break
- else:
+ if not cost_dataframe.loc[(tech_name, "VOM"), "value"]:
cost_dataframe.loc[(tech_name, "VOM"), "value"] = 0.0
and, for the dispatch:
if "powerboost" in tech_name:
...
- elif "gas" in tech_name:
- ...
elif "biogas" in tech_name:
boiler = "gas boiler steam"
co2_capture = cost_dataframe.loc[(tech_name, "CO2 stored"), "value"]
+ elif "gas" in tech_name:
+ ...
else:
...
There is also a duplicated VOM assignment block later in the function — the identical rescaling appears twice, once immediately after the break and again after the efficiency-heat block. It is currently harmless because the loop never reaches either, but once the break is fixed the second occurrence would apply the rescaling a second time, so one of them should go.
energy_penalty()exits after the first technology: five of six CC technologies never get the capture energy penaltyNote: Claude found this issue, so below is a summary by it.
Summary
In
scripts/compile_cost_assumptions.py,energy_penalty()loops over six carbon-capture technologies and adds an auxiliary steam boiler to cover the heat demand of the capture unit, rescaling investment, VOM, efficiency and (for CHPs) heat efficiency accordingly.The loop body contains a
breakwherepassappears to have been intended.central solid biomass CHP CCis the first item in the list and has a non-zero VOM, so the loop exits during the first iteration — after theinvestmentupdate but before theVOM,efficiencyandefficiency-heatupdates.The net effect on the published
outputs/costs_*.csv:central solid biomass CHP CCreceives the investment uplift but keeps its unpenalized electrical and heat efficiency and its unadjusted VOM.waste CHP CC,solid biomass boiler steam CC,direct firing solid fuels CC,direct firing gas CCandbiogas CCreceive nothing at all — their investment, VOM and efficiency are identical to their non-CC counterparts.Downstream this makes carbon capture cheaper and more efficient than the function intends, which matters for any study comparing BECCS routes against each other or against unabated conversion.
Location
scripts/compile_cost_assumptions.py, inenergy_penalty()— theifbegins at line 3313 on current master:Everything after this block — the VOM rescaling,
efficiency = eta_main, and theif "CHP" in tech_nameheat-efficiency block — is unreachable for every technology whose VOM is non-zero, and unreachable for all subsequent technologies regardless.The same code is present at least as far back as v0.11.0, so the outputs have been affected for a long time.
Evidence in the shipped outputs
The
sourcefield records which lines executed, because the function overwrites it with"Combination of <tech> and <boiler>". Fromoutputs/costs_2050.csv:Combination of central solid biomass CHP CC and solid biomass boiler steamDanish Energy Agency, inputs/technology_data_for_el_and_dh.xlsxDanish Energy Agency, inputs/technology_data_for_el_and_dh.xlsxDanish Energy Agency, inputs/technology_data_for_el_and_dh.xlsxThe first row shows the investment line ran; the second and third show the loop had already exited; the fourth shows the second technology was never reached.
waste CHP CC,solid biomass boiler steam CC,direct firing solid fuels CC,direct firing gas CCandbiogas CCall carry investment and efficiency values identical to their non-CC rows.Magnitude
Re-running the intended arithmetic on
costs_2050.csv(captureheat-input0.66 MWh/tCO₂; boiler efficiencies 0.90 solid biomass / 0.94 gas; CO₂ intensities 0.3667 solid biomass, 0.198 gas):Heat efficiencies also change for the two CHPs:
central solid biomass CHP CC0.8294 → 0.9433,waste CHP CC0.7625 → 0.8906.So the shipped cost data understates capture-inclusive investment by 15–86 % for five technologies, and overstates the conversion efficiency of all six by 12–21 %.
Secondary issues in the same function
1. The
elif "biogas"branch is unreachable. The dispatch is ordered"gas" in "biogas CC"isTrue, sobiogas CCis always caught by the"gas"branch and takes the natural-gas CO₂ intensity (0.198 tCO₂/MWh) instead of its ownCO2 stored(0.1447 tCO₂/MWh). Once thebreakis fixed, this changesbiogas CCfrom investment 1250 / efficiency 0.8779 to 1207 / 0.9078. Testing"biogas"before"gas"would fix it.2. The
"powerboost"branch is dead.central solid biomass CHP powerboost CCis defined in the sheet mappings but is absent from the technology list the loop iterates over, so the branch never runs.3. The heat-efficiency block hardcodes the solid-biomass CO₂ intensity.
This uses
solid biomassrather than the branch's ownco2_capture, which is wrong forwaste CHP CC(whose capture is applied to the oil CO₂ intensity downstream in PyPSA-Eur).4.
feedstockis assigned in three branches and never read, and is not assigned in the"biogas"branch — harmless today, but it suggests the dispatch was refactored at some point and thebreakis a leftover from that.Suggested fix
and, for the dispatch:
if "powerboost" in tech_name: ... - elif "gas" in tech_name: - ... elif "biogas" in tech_name: boiler = "gas boiler steam" co2_capture = cost_dataframe.loc[(tech_name, "CO2 stored"), "value"] + elif "gas" in tech_name: + ... else: ...There is also a duplicated VOM assignment block later in the function — the identical rescaling appears twice, once immediately after the
breakand again after theefficiency-heatblock. It is currently harmless because the loop never reaches either, but once thebreakis fixed the second occurrence would apply the rescaling a second time, so one of them should go.