Skip to content

Line losses with error control#1495

Merged
lkstrp merged 27 commits intomasterfrom
atol-rtol-losses
Feb 11, 2026
Merged

Line losses with error control#1495
lkstrp merged 27 commits intomasterfrom
atol-rtol-losses

Conversation

@lindnemi
Copy link
Copy Markdown
Contributor

@lindnemi lindnemi commented Dec 16, 2025

Closes #1320.
Closes #1470.

Improved version of #1470.
Related to #1409.

Changes proposed in this Pull Request

  • Make the approximation of the loss parabola independent of s_nom_max. To be able to do this a bound for the approximation error is needed, which has the added benefit of giving more fine-grained control of the approximation quality. Here an error bound based on absolute and relative error tolerances is used. This fixes Changing s_nom_max affects line losses #1320.

  • Use secants instead of tangents for the approximation. That way, the approximation error is zero at the "corners" of the stepwise linear approximation. Since the optimum of the linear program tends to be located at a corner, this will hopefully reduce the effective error of the loss approximation

  • Introduce lines losses close to 0. At the moment, flows smaller than s_nom_max/(2*n_tangents) are lossless.

Example

Here is a plot of how the approximation with secants for an exemplary line looks like with atol = 1 #MW and rtol = 0.1. I chose the parameters such that the numbers of loss constraints is relatively small.

secants_example

The approximation could be improved by setting the endpoint of the last secant to s_nom_max. However, that would re-introduce a dependency on s_nom_max and should ideally only be done when it can be guaranteed that s_nom_max does not change between consecutive runs of the model.

Questions

  • Are we happy with the overestimation of losses that may occur at s_nom_max? Of course a similar approximation with tangents could be constructed. However, that would underestimate the error at s_nom_max.
  • Should we use equality constraints instead of inequality constraints? The plot in Improving transmission loss approximation for DC power flow #1409 suggests that there are many snapshots when the losses are bigger than they should be, i.e. the model uses them to destroy energy.

Derivation of formulas for relative and absolute error

Find a piecewise linear approximation $l(x)$ of the loss parabola $L(x) := rx^2$ on $[0, X]$ with:

  1. $x_0 = 0$
  2. $l(x_i) = L(x_i) = r x_i^2$, where $x_i$ are the endpoints of the linear segments

We want the endpoints to be on the parabola, since the optimal solution tends to be on the corners of a simplex, and hence that is where the approximation should be best.

(2) implies that our approximation consists of secants. Denote the endpoints of a secant as $a,b$ then

$l_{ab}(x) = \frac{L(b) - L(a)}{b - a }(x-a) + L(a) = r\frac{b^2 - a^2}{b - a }(x-a) + ra^2 = r(b+a)(x-a) + ra^2 = r((a+b)x - ab)$

The absolute error $AE(x)$ between $l_{ab}(x)$ and $L(x)$ is

$AE(x) = r((a+b)x - ab) - rx^2$

This is positive for $a < x < b$, since there the secant is above the parabola. To compute it's maximum, remember that the derivative has to be 0.

$0 = \frac{d}{dx} AE(x) = r(a+b)x - 2rx \Rightarrow x_{max} = \frac{a+b}{2}$

Hence the error is maximal at the midpoint. Assume we specify the absolute error tolerance as $\varepsilon$. From this we can compute the interval length $b-a$

$\varepsilon = AE(\frac{a+b}{2}) = r((a+b)\frac{a+b}{2} - ab) - r(\frac{a+b}{2})^2 = r( \frac{(a+b)^2}{2} - \frac{(a+b)^2}{4} - ab) = r(\frac{(a+b)^2}{4} - \frac{4ab}{4}) = \frac{r(a-b)^2}{4}$

which implies $b(a, \varepsilon) = 2\sqrt{\frac{\varepsilon}{r}} + a$.

Hence, to construct an approximation with absolute error at most $\varepsilon$, we choose the endpoints of the secants at $x_i = \dots, -4\sqrt{\frac{\varepsilon}{r}}, -2\sqrt{\frac{\varepsilon}{r}}, 0, 2\sqrt{\frac{\varepsilon}{r}}, 4\sqrt{\frac{\varepsilon}{r}}, \dots$

For small error tolerances and large line flows we might need many secants, which would translate to many additional constraint and slow down solving. So, in this case we will use an approximation based on the relative error instead.

The relative error is

$RE(x) = \frac{r((a+b)x - ab) - rx^2}{rx^2} = \frac{a+b}{x} - \frac{ab}{x^2} -1$

It's derivative is $\frac{2ab - (a+b)x}{x^3}$, which is zero at $x_{max}=\frac{2ab}{a+b}$.
Assuming the relative error tolerance is $\delta$ we get:

$\delta = RE(\frac{2ab}{a+b})= \dots = \frac{(b-a)^2}{4ab}$

We want to build a step rule $b(a)$ to construct a new end point $b$ from a given starting point $a$. Hence we rewrite the equation above as a polynomial in $b$ with constants $a$ and $\delta$:

$\delta = \frac{(b-a)^2}{4ab} \Leftrightarrow b^2 - a(2+4\delta)b + a^2 = 0$

This polynomial has the roots $b = a(1 + 2\delta \pm 2\sqrt{\delta(1+\delta)})$. It can be shown that the root with - term is smaller than $a$. Since we want $b>a$ we take the other root and get

$b(a,\delta) = a(1 + 2\delta + 2\sqrt{\delta(1+\delta)})$

For small $a$ this gets very impractically small. Therefore we use the step based on absolute error for small $a$, and the step based on relative error for large $a$. Combined we have

$x_{i+1}(x_{i},\varepsilon,\delta) := b(a,\varepsilon,\delta) = \max(2\sqrt{\frac{\varepsilon}{r}}+ a, 2(\delta + \sqrt{\delta +\delta^2})a + a)$

We compute new endpoints until $x_I > X$. Then $l(x) = \sum_{i=0}^I l_{x_{i}x_{i+1}}(x)$.

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.

@fneum
Copy link
Copy Markdown
Member

fneum commented Dec 16, 2025

Should we use equality constraints instead of inequality constraints? The plot in #1409 suggests that there are many snapshots when the losses are bigger than they should be, i.e. the model uses them to destroy energy.

Equality constraints would require binary variables to identify the segments wouldn't it?

Copy link
Copy Markdown
Member

@fneum fneum left a comment

Choose a reason for hiding this comment

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

Very nice! Thanks @lindnemi! I like that the option to go with tangents is kept.

I have some suggestions for handling the keyword arguments and perhaps @lkstrp can chip in on making the transition as smooth as possible.

A short unit test could be added to assert that losses in secant are >= tangent mode.

Comment on lines +1622 to +1623
transmission_losses: int,
mode: str = "secants",
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
transmission_losses: int,
mode: str = "secants",
transmission_losses: int, # deprecate, use max_segments instead
atol: float = 1,
rtol: float = 0.1,
max_segments = 30,
mode: str = "secants",

I would give a few more new keyword arguments here. The argument transmission_losses was never well designed and I would suggest to deprecate it here.

What is still needed is that the arguments can be passed through n.optimize() and n.optimize.create_model() (everywhere where transmission_losses shows up and define_loss_constraints is called).

Maybe here, the arguments could be bundled in a dictionary for transmission_losses?

n.optimize(
    transmission_losses: int | dict = 0
)

transmission_losses = dict(
    max_segments = 10,
    atol = 10,
    rtol = 0.2,
    mode = "secants",
)

transmission_losses = dict(
    max_segments = 3,
    mode = "tangents",
)

Maybe @lkstrp can help with making the transition as smooth as possible and has an idea for a good keyword argument design.

The default should probably stay as tangents for now, and change with a future version.

@lindnemi
Copy link
Copy Markdown
Contributor Author

Thank you @fneum for the quick review and the valuable suggestions.

I implemented the changes and refactored the code for computing the secants one last time. By algebraically rearranging the computation, it was possible to improve the numerical stability, s.t. the offsets should all be the same now (before that there were some very small rounding errors, which should not impact results, but potentially solver speed??)

I also sketched a potential interface for tranmissions losses based on dicts, and a few unit tests. @lkstrp maybe you can take over from here? Of course i am very happy to support if any questions arise.

@lindnemi
Copy link
Copy Markdown
Contributor Author

btw, i noticed that for passive branch components like Transformers the loss variables get defined, but there are no constraints on them, s.t. they end up NA. This becomes apparent when solvend the ac_dc_meshed example with losses and inspecting the Line loss variables - a lot of nans turn up:

image @fneum

In the screenshot there is also a UserWarning visbile: Coordinates across variables not equal. Perform outer join.

@lkstrp do you if this is because i introduced the new secant coordinate?

@lindnemi lindnemi requested a review from fneum December 18, 2025 13:52
@lkstrp
Copy link
Copy Markdown
Member

lkstrp commented Dec 18, 2025

@lkstrp do you if this is because i introduced the new secant coordinate?

What you are showing there is just the solution, which is a single xarray dataset and shares a single name coord, which has all component names. So those nans are probably the generators of ac_dc_meshed? It shouldn't have a Transformer

And the user warning should be gone if you update to linopy>=0.5.8.

I need to properly understand this and have a detailed look. But sure all the API stuff, tests etc I can take over. What about docs? Can you write up something about the additional formulation? https://docs.pypsa.org/latest/user-guide/optimization/power-flow/?h=line+loss#loss-approximation

@lindnemi
Copy link
Copy Markdown
Contributor Author

lindnemi commented Dec 19, 2025

So those nans are probably the generators of ac_dc_meshed?

Yes, that was a misunderstanding on my side.

And the user warning should be gone if you update to linopy>=0.5.8.

I am actually using linopy 0.5.8.

What about docs? Can you write up something about the additional formulation?

I didn't know we had docs on line losses, but that's great, I will write something up.

But sure all the API stuff, tests etc I can take over.

Great, thanks! The new secant formulation should become the default with the next PyPSA version. So maybe we need some deprecation warnings?

@lkstrp lkstrp added this to the v1.1 milestone Dec 22, 2025
@lkstrp lkstrp merged commit 391870d into master Feb 11, 2026
27 checks passed
@lkstrp lkstrp deleted the atol-rtol-losses branch February 11, 2026 11:39
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.

Changing s_nom_max affects line losses

3 participants