Skip to content

Make CO2 storage run under PyPSA 1.x and split CCS CO2 correctly - #859

Merged
WeiAi-Energy merged 3 commits into
PyPSA:developfrom
WeiAi-Energy:issue-857
Oct 28, 2026
Merged

WeiAi-Energy merged 3 commits into
PyPSA:developfrom
WeiAi-Energy:issue-857

Conversation

@WeiAi-Energy

Copy link
Copy Markdown
Collaborator

Closes #857, closes #858.

Changes proposed in this Pull Request

The first commit makes co2: storage: true run (#857):

  • The add_extra_components rule provides the storage csv for sector: E.
  • query("Carrier/Bus/Link...") becomes plain index filtering. The CC carriers are added under their new names.
  • CCS generators and links are renamed together with their time series.
  • Each plant captures to the capture bus of its own node. Each node, state or nation gets one atmosphere bus.
  • The sector-less DAC links are built per node with a CC plant.

The second commit splits CCS CO2 correctly (#858):

  • E: the atmosphere gets (1 − cc) of the plant's CO2 and the capture bus gets cc of it (they were swapped).
  • E-G: the split is per MWh of fuel drawn (fuel intensity × cc and × (1 − cc)), no longer divided by efficiency.
  • Coal CCS links use the CO2 intensity of coal.

Testing

  • New test_co2_storage.py (9 tests). The 6 storage tests fail on develop, and the 3 split tests fail without the second commit.
  • End to end on UT (simpl: 10, clusters: 2), with CO2 storage, the CO2 network, DAC, and a binding 10 kt REM limit:
    • E solves.
    • CCGT-95CC is built (1.8 GW). It vents 5% of its CO2, and the other 95% (13.9 kt) goes to underground storage.
    • REM counts 9.3 kt from generators plus 0.7 kt from the atmosphere, which is the 10 kt limit.
    • Before the split fix, no CC plant was built.
  • E-G solves with CO2 storage and the CO2 network. DAC is left off, because it is not supported in sector studies (DAC in Sector Studies #652).
    • CCGT-95CC links are built.
    • 95% of their CO2 (4.0 kt) goes to underground storage and 5% to UT pwr-co2.
    • A 10 kt power sector limit binds.
  • pytest -m fast: 1003 passed, 11 skipped, 4 failed. The 4 failures also fail on develop: the three GoDEEEP CF registry tests and test_dag_dryrun.py::test_interface_limits_input_follows_config_key.

Checklist

  • I tested my contribution locally and it seems to work fine.
  • pre-commit and pytest -m fast pass locally.
  • Code and workflow changes are sufficiently documented.
  • Changed dependencies are added to workflow/envs/environment.yaml.
  • Changes in configuration options are added in workflow/repo_data/config/config.default.yaml.
  • Changes in configuration options are also documented in docs/source/configtables/*.csv.

🤖 Generated with Claude Code

WeiAi-Energy and others added 2 commits October 28, 2026 00:29
With co2: storage: true the workflow stopped in add_extra_components
(sector E) and add_sectors (sector E-G).

- The add_extra_components rule provides the CO2 storage csv for sector
  E, as the script expects, rather than for an empty sector.
- add_co2_storage, add_dac and add_co2_network filter by index instead
  of querying the pre-PyPSA 1.x index names (Carrier, Bus, Link), and the
  CC carriers are added under their new names.
- CCS generators and links are renamed along with their time series.
- Each CCS plant captures to the capture bus of its own node, rather than
  to the list of all storage nodes, and a node, state or nation gets one
  atmosphere bus however many CC plants vent to it.
- Links without a fourth output get an empty bus4.
- The sector-less DAC links are built per node with a CC plant, so their
  buses line up with more than one CCS technology.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Electricity-only studies: a CC plant sent all its CO2 to the
  atmosphere and (1 - cc) / cc of it to the capture bus. It now vents
  (1 - cc) and captures cc of the CO2 of the fuel it burns.
- Sector studies: CCS links are rated on the fuel they draw, so their
  CO2 is the fuel's intensity, split into cc captured and (1 - cc)
  emitted. Dividing it by the efficiency overstated both by 1/(eff * cc).
- get_pwr_co2_intensity gives coal CCS plants the CO2 intensity of coal
  rather than of gas.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@trevorb1 trevorb1 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! Just a couple clarification comments, @WeiAi-Energy. I only reviewed the sector things, and will let @ktehranchi review the electricity side of things if he wants. :)

Comment thread workflow/rules/build_electricity.smk Outdated
Comment on lines +757 to +769
def rename_components(n: pypsa.Network, component: str, names: dict[str, str]) -> None:
"""
Renames components, along with their time series.

pypsa's rename_component_names also renames the references to them in the other components, but fails for
generators (it looks for a "generator" column in every component), which nothing refers to anyway.
"""
c = n.components[component]
c.static = c.static.rename(index=names)
for attr, df in c.dynamic.items():
c.dynamic[attr] = df.rename(columns=names)


Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this same as rename_component_names? Like are we able to just use this built in function already? Or is this function different?

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried it first, but on PyPSA 1.3.0 it only works for buses. After renaming, it updates cross-references by looking for a {port} column (e.g. generator, link) in every component with ports:

https://github.com/PyPSA/PyPSA/blob/f6be86e732604a1b1793d0990d546a7b467309b9/pypsa/components/transform.py#L209-L220

For buses this finds bus, bus0, bus1, ..., but for Generator and Link it looks for generator or link columns that don't exist, so it raises KeyError: "None of [Index(['link'])] are in the [columns]". Nothing refers to generators or links by name, so we only need the first part (renaming the static index and the time series columns), which is all this helper does.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm... this is weird. I would think this method should be general for any of the pypsa components. I have reported to the pypsa maintainers here to get clarity. Lets keep this unresolved until we hear what they say. But I think your fix is good, @WeiAi-Energy, to merge and we can update if needed when we hear back! :)

Comment thread workflow/scripts/add_extra_components.py
Comment thread workflow/scripts/add_sectors.py
Rules are parsed before the Snakefile pads an empty sector to "E", so
the condition has to accept both spellings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@WeiAi-Energy
WeiAi-Energy merged commit 88014a4 into PyPSA:develop Oct 28, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants