Skip to content

Add link delay for output ports#1569

Merged
FabianHofmann merged 35 commits intomasterfrom
feat/link-delay
Feb 24, 2026
Merged

Add link delay for output ports#1569
FabianHofmann merged 35 commits intomasterfrom
feat/link-delay

Conversation

@FabianHofmann
Copy link
Copy Markdown
Contributor

@FabianHofmann FabianHofmann commented Feb 17, 2026

Changes proposed in this Pull Request

Adds configurable time delays for Link output ports. Energy withdrawn from bus0 at snapshot t arrives at output ports after a delay measured in snapshot_weightings.generators (best choice I could make wo introducing a new field) units. This enables modeling transport delays (e.g. gas pipelines, shipping).

New Link attributes:

  • delay / delay2, delay3, ... — delay in generator weighting time units per port
  • cyclic_delay / cyclic_delay2, cyclic_delay3, ... — whether delayed energy wraps cyclically or is lost at boundaries

Implementation:

  • Links.get_delay_source_indexer() computes per-snapshot source mappings using cumulative generator weightings, supporting both cyclic and non-cyclic modes
  • Links.split_by_port_delay() groups links by delay/cyclicity for efficient constraint generation
  • Nodal balance constraints apply the delay
  • Post-solve result extraction applies matching shifts to p1, p2, ... DataFrames
  • check_link_delays consistency check validates delay values are non-negative and within the snapshot horizon

Bug fix: Link-port attribute expansion (bus2, bus3, efficiency2, ...) no longer mutates shared Link defaults across Network instances. A targeted test was added for this.

Checklist

  • Code changes are sufficiently documented; i.e. new functions contain docstrings and further explanations may be given in docs.
  • Unit tests for new features were added (if applicable).
  • A note for the release notes docs/release-notes.md of the upcoming release is included.
  • I consent to the release of this PR's code under the MIT license.

- Add get_delay_source_indexer using cumulative generator weightings
- Add check_link_delays consistency check for negative/excessive delays
- Fix non_delayed.any() -> not non_delayed.empty for pd.Index
- Use c_name variable to avoid str/Component confusion in descriptors
- Handle MultiIndex active_assets in nodal balance constraints
FabianHofmann and others added 2 commits February 17, 2026 13:45
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
@FabianHofmann FabianHofmann changed the title Add weighted-time link delay for output ports Add link delay for output ports Feb 18, 2026
Add user guide section, energy balance formulation, link-delay notebook
with bar plots, hydro-reservoirs delay section, and navigation entries.

Links can model time-delayed energy transport via the `delay` and `cyclic_delay` attributes. This is useful for representing transport delays in gas pipelines, hydrogen shipping, long-distance HVDC transmission, or any process where energy withdrawn at `bus0` arrives at output ports after a configurable time lag.

- **`delay`**: The delay in units of elapsed time, referencing `n.snapshot_weightings.generators`. Energy withdrawn from `bus0` at snapshot `t` arrives at `bus1` at `t + delay`. That is, for uniform hourly snapshots with unit weightings, `delay=3` means a 3-hour delay. In case of snapshot weightings greater than 1, the energy arrives at `bus1` the first snapshot at whose time step at least `delay` units later.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

a) I don't understand the second part about snapshot weightings > 1
b) What is we have snapshots with weighting = 0? Are they skipped?
c) What if port0 is activated in snapshot t with a delay=1, and t+1 has a different weighting. How are e.g. marginal_costs or efficiency affected? Assume efficiency is snapshot dependent? Would that give an incentive to operate the link at port0 in t? Not sure if that is fine or not.
d) In many instances we have started to allow pd.Series instead of scalar values for properties. Would it be possible or meaningful to do the same here? I.e. allow for a Series of delays, specifying different delays during different snapshots.
e) If the users changes their snapshots, they will also need to adjust the delay, right? I think it should fail rather than trying to automatically fix it and aligning it with a guessed snapshot.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

a) there is a "as" too much, I will add an example to clearify.
b) weighting of 0? interesting use case. they are not skip, the would just not contribute to the cumulative sum of the snapshot weightings when comparing delays and time elapsed. if you want to have that case bullet proof I can add a test for that. It could be that atm the a delay of 1 and a weighting of 0 would lead to energy traveling back in time.
c) marginal_cost is always referencing p0 so this is implicitly handled by the optimization, efficiency is only touching outputs p1+. if the efficiency is time-dependent, it will reference the efficiency at the arrival time, ie. the target snapshot. after thinking about it, I found this cleaner then the other way round, but open to change if this seems counter-intuitive.
d) honestly I wouldn't do it. I would be quite a complex thing. the cumulative comparison is already complex and this would make it even more complicated to review later.
e) do you mean if the user change the snapshots or their weightings? Since the delay takes the time elapsed into account, changing the snapshot alongside with the snapshot weighting is no problem. suppose delay is 3 and snapshot resolution is hourly (sn weights = 1) . now you are resampling to 3 hourly resolution (adjusting the sn weights to 3), everything works out of the box without touching the delay.

The **incidence matrices** $K_{n,l}$ for the [`Line`][pypsa.components.Lines] and [`Transformer`][pypsa.components.Transformers] components and $L_{n,l,t}$ for the [`Link`][pypsa.components.Links] components govern flows between buses. The incidence matrix $K_{n,l}$ takes non-zero values $-1$ if the line or transformer $l$ starts at bus $n$ and $1$ if it ends at
bus $n$. If $p_{l,t}>0$ it withdraws from the starting bus. The time-varying incidence matrix $L_{n,l,t}$ takes non-zero values $-1$ if the link $l$ starts at bus $n$ and efficiency $\eta_{n,l,t}$ if it ends at bus $n$. If $f_{l,t}>0$ it withdraws from `bus0` and feeds in $\eta_{n,l,t} f_{l,t}$ to `bus1`. For a link with more than two outputs (e.g. a combined heat and power plant), the incidence matrix $L_{n,l,t}$ has more than two non-zero entries with efficiencies $\eta_{n,l,t}$ for `bus2`, `bus3`, etc.. The entries may also be negative to denote additional inputs rather than multiple outputs.

When a link has a non-zero `delay` attribute, the output flow at snapshot $t$ corresponds to the input at an earlier source snapshot $s(t)$ rather than at $t$ itself. The source snapshot $s(t)$ is the latest snapshot such that $\tau(s) \leq \tau(t) - \delta$, where $\tau$ denotes the cumulative start time derived from `snapshot_weightings.generators` and $\delta$ is the delay value. The delayed contribution to the energy balance at output bus $n$ becomes:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think we should enforce that the delay ends on an existing snapshot, not matching it to the next snapshot. The user should explicitly need to handle this.

Copy link
Copy Markdown
Contributor

@euronion euronion left a comment

Choose a reason for hiding this comment

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

I love seeing this feature.

Left some comments without looking at it in more detail.

Another concern: How does this combine with the statistics module, especially if aggregate_time=False. Do we then need to specify at_port to get the right results for time-series, as they will be different depending on which port is selected.

Copy link
Copy Markdown
Member

@lkstrp lkstrp left a comment

Choose a reason for hiding this comment

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

Following the discussion yesterday, I fully agree with delays weighted by snapshot weightings. It adds extra complexity but with strict consistency checks we can just handle this

I have a couple of code simplifications. Instead of adding all the comments, can I just commit a bit in here?

@FabianHofmann
Copy link
Copy Markdown
Contributor Author

FabianHofmann commented Feb 19, 2026

I love seeing this feature.

Left some comments without looking at it in more detail.

Another concern: How does this combine with the statistics module, especially if aggregate_time=False. Do we then need to specify at_port to get the right results for time-series, as they will be different depending on which port is selected.

thanks for the review @euronion. before we make the decision of killing the flexibility of having delay != sn weight or rather delay != sn weight * n, I would like to advocate for the convention I introduced dealing with these cases (last two sections) in link-delay.ipynb

run it with

gh pr checkout 1569
uv run jupyter notebook docs/examples/link-delay.ipynb

or have a look at the rdt built in a minute when it (hopefully) runs through

About the stastics module. that is working right away as it uses the already delayed dispatch time series at p{port}

@lkstrp
Copy link
Copy Markdown
Member

lkstrp commented Feb 20, 2026

before we make the decision of killing the flexibility of having delay != sn weight or rather delay != sn weight * n, I would like to advocate for the convention I introduced dealing with these cases (last two sections) in link-delay.ipynb

I am not too sure about the use cases. The convention looks clean and is also explained in a good way, but is it just convenience and it would always be better to make delays match with weights? I feel that this can be hard to investigate/ understand. If we keep the convention we could add warnings that some delay rounding is going on

@FabianHofmann
Copy link
Copy Markdown
Contributor Author

@lkstrp should we then introduce a consistency check? I still think delay != weights * n is valid as long as you think about snapshots with weights != 1 as time spans.

@lkstrp
Copy link
Copy Markdown
Member

lkstrp commented Feb 20, 2026

I changed a couple of things and added multi horizon support (raised by @Irieo). @FabianHofmann If you could have another look?

Otherwise two more things:

  • Should we also disallow negative delays? For new attributes I wanna be as strict as possible so we can have quick great user feedback with the validation layer without breaking things
  • What about Add processes components as multi-link alternative #1333 ? Do we wanna support delays there as well?

@euronion
Copy link
Copy Markdown
Contributor

Otherwise two more things:

  • Should we also disallow negative delays? For new attributes I wanna be as strict as possible so we can have quick great user feedback with the validation layer without breaking things
  • What about Add processes components as multi-link alternative #1333 ? Do we wanna support delays there as well?
  1. I think it would be good to allow for negative delays. That allows users to flexibly use the different buses as input and output. If we only allow for positive delays, then the user needs to start manually preprocessing data again.
  2. Yes!

Related to the convention / automatic matching:
Consistency check + automatic matching would be an ok compromise for me.
I think the documentation is clear, although I don't look forward debugging this feature with non-uniform weightings. ;)

But I would be keen on hearing the comments from others.

@FabianHofmann
Copy link
Copy Markdown
Contributor Author

  1. I also tend to keep it strictly zero/positive for now to avoid confusion in the models. formulation is already quite complex. but if you are saying this will be really useful we can ship that later @euronion?
  2. yes, I would definitely expand this to processes. but let's do it one by one though. completely separate pr

@lkstrp fine with pulling in now?

@lkstrp
Copy link
Copy Markdown
Member

lkstrp commented Feb 23, 2026

I also tend to keep it strictly zero/positive for now to avoid confusion in the models. formulation is already quite complex. but if you are saying this will be really useful we can ship that later @euronion?

This would still need to be checked then

Add tests for zero-weight cyclic delay error and empty snapshots.
@FabianHofmann
Copy link
Copy Markdown
Contributor Author

I also tend to keep it strictly zero/positive for now to avoid confusion in the models. formulation is already quite complex. but if you are saying this will be really useful we can ship that later @euronion?

This would still need to be checked then

I it already checked by the consistency check but only warns. should we make that particular check always strict, ie. always raising? it is currently inside a function with another check, so setting the strict default to True would also affect the other check

@FabianHofmann FabianHofmann enabled auto-merge (squash) February 23, 2026 09:19
@lkstrp lkstrp disabled auto-merge February 23, 2026 10:28
@FabianHofmann FabianHofmann merged commit f9436c0 into master Feb 24, 2026
27 checks passed
@FabianHofmann FabianHofmann deleted the feat/link-delay branch February 24, 2026 06:33
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.

3 participants