Skip to content

energy_penalty() applies the capture energy penalty to only one of six technologies #283

Description

@Timon-R

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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions