Skip to content

Piecewise linear basin profile approx in allocation #2108

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 27 commits into
base: allocation_feature
Choose a base branch
from

Conversation

SouthEndMusic
Copy link
Collaborator

@SouthEndMusic SouthEndMusic commented Feb 26, 2025

Fixes #2060.

An additional complexity here is that because we change the way we represent basins in the allocation problems, level demand has to be reformulated as well. The previous approach was to have a 'buffer' attached to basins with a level demand which acted as a source when the level in the basin was too high, and as a UserDemand when the level in the basin was too low. This source or demand was computed with data from the physical layer.

This PR introduces the level of each basin explicitly as a variable in the optimization problems. Therefore we can optimize for the level directly. The error for other demand nodes are still formulated in terms of flow though, so the error for level demand should be formulated that way too, so all terms are treated the same way in the loss function. Therefore we formulate the error term for the level demand as

$$ \left| \frac{L_\text{end} - l_\text{target}}{\Delta t_\text{allocation}} \right|, $$

where $L_\text{end}$ is the level of a particular basin at the end of the allocation timestep.

One thing to properly think trough here is what the behavior of a basin with a level demand should be when optimizing for a different priority than the priority of the level demand. I would say intuitively: for priorities higher than the level demand, all storage of the basin should be available as a source, and for the priority of the level demand and lower priorities only the disk of water above the maximum tolerated level should be available, if applicable.

@SouthEndMusic SouthEndMusic marked this pull request as draft February 26, 2025 13:47
@SouthEndMusic SouthEndMusic changed the title Attempt at piecewise linear basin profile approx in allocation Piecewise linear basin profile approx in allocation Feb 26, 2025
@SouthEndMusic
Copy link
Collaborator Author

Here's a bit of theoretical background to the goal of this PR.

For each basin, a table is given with levels and associated areas: $h_i, A_i \in \mathbb{R}, i = 1, \ldots n$. This data is sorted by level and the areas are strictly positive and non-decreasing. To get a continuous profile we interpolate this data linearly:

$$ A(h) = A_i + (A_{i+1} - A_i)\frac{h - h_i}{h_{i+1} - h_i}, \quad h \in [h_i, h_{i+1}]. $$

Then to get the volume (storage) of water at some level $\ell$ we integrate from the bottom, given by the first and lowest level in the table:

$$ V(\ell) = \int_{h_1}^\ell A(h)\text{d}h, $$

giving a piecewise quadratic storage - level relationship. This is the relationship we want to approximate linearly. In the code we actually use the inverse of this relationship (storage_to_level) which is more useful in the physical layer, which I implemented upstream and is described here. In this PR I evaluate this relationship in an arbitrary `n_samples_per_interval = 5 equi-spaced points in each interval between data points.

The level can also increase above the last level $h_n$, for which we extend the linear area and quadratic storage increase from the last data interval in the physical layer. It seems however to do this in the allocation problem that we have to make some educated guess for the maximum level that is going to be attained at model initialization, but @visr says that in practice this is probably doable.

@jarsarasty
Copy link
Collaborator

jarsarasty commented Mar 24, 2025

Hi @SouthEndMusic,

Originally, the objective function of this problem is quadratic, and this pull request introduces binary decision variables, so your problem becomes a mixed-integer quadratic one. Unfortunately, there are no very good open source solvers for this type of discrete nonlinear optimization problem. In particular, HiGHS cannot solve QP models where some of the variables must take integer values. You could try other free solvers like bonmin, but the solution will probably not be very fast.

Other alternatives, as discussed in our meeting last week, would be 1) to convert the objective function into a linear one (this is probably a no regret action), 2) to try to exploit the problem structure to avoid the use of binary variables (this will require a bit more investigation, and is not guaranteed to be achievable), or 3) try to solve the problem in stages, e.g. by first solving a simplified version with linear storage level tables, and then using that initial solution to solve a more detailed model with the piecewise storage curve representation (this staged approach will require some effort to set up).

I would suggest trying first action 1), implemented as an option, so that the user can choose between the quadratic or linear objective. We could discuss this approach further and refine what has been suggested here.

@visr
Copy link
Member

visr commented Mar 24, 2025

It makes sense to try to use a linear objective function. I'd prefer to not support multiple objective functions unless there is a good reason for it, to keep the maintenance lower.

@SouthEndMusic
Copy link
Collaborator Author

@jarsarasty thanks for your reply. I'll start implementing the linear objective function you suggested.

@SouthEndMusic SouthEndMusic changed the base branch from main to allocation_feature March 27, 2025 10:12
@SouthEndMusic
Copy link
Collaborator Author

SouthEndMusic commented Apr 1, 2025

The current implementation of the storage level relationship is like this:

Say for basin $i$ we have the storage data and level data $s_{i, j}, h_{i, j}$ for $j = 1, \ldots, n$. Then we define:

  • The Boolean variables $B_{i,j}$ for $j = 1, \ldots, n - 1$ ;
  • The auxiliary variables $A_{i,j} \ge 0$ for $j = 1, \ldots n$;
  • The storage $S_i$ after the time step;
  • The level $H_i$.

Then we define the constraints:

  • Exactly one binary variable is $1$:

$$ \sum_{j=1}^{n - 1} B_{i,j} = 1; $$

  • The auxiliary variables yield a convex combination:

$$ \sum_{j=1}^{n - 1} A_{i,j} = 1; $$

  • The storage (only one of these terms contributes) (quadratic expression in the variables):

$$ S_i = \sum_{j=1}^{n-1} B_{i,j} (s_{i,j}A_{i,j} + s_{i,j+1}A_{i,j+1}); $$

  • The level (only one of these terms contributes) (quadratic expression in the variables):

$$ H_i = \sum_{j=1}^{n-1} B_{i,j} (h_{i,j}A_{i,j} + h_{i,j+1}A_{i,j+1}). $$

Now that I write this out, I doesn't make that much sense to me anymore. It seems that I only need 1 or 2 auxiliary variables to define the linear combination of the successive storage and level data. But then still I end up with the product of the binary variable and auxiliary variable.

Asking Claude about this, it seems that what I should do is simply have

$$ S_i = \sum_{j=1}^n A_{i,j}s_{i,j}, \quad H_i = \sum_{j=1}^n A_{i,j}h_{i,j} $$

and then make sure that only 2 consecutive auxiliary variables are non-zero by

$$ A_{i,j} \le B_{i,j-1} + B_{i, j} \quad \text{ for } j = 2, \ldots, n-1 $$

and $A_{i,1} \le B_{i,1}$, $A_{i,n} \le B_{i, n-1}$. This avoids products of variables.

@jarsarasty
Copy link
Collaborator

Hi @SouthEndMusic,

Indeed, the product of decision variables in the optimization model should be avoided because it introduces nonlinearity, which can significantly complicate the problem and make it harder to solve.

@SouthEndMusic
Copy link
Collaborator Author

@jarsarasty this package looks very interesting for constructing linear approximations:

https://github.com/LICO-labs/PiecewiseLinApprox.jl

evetion and others added 6 commits April 14, 2025 09:49
Bumps [prefix-dev/setup-pixi](https://github.com/prefix-dev/setup-pixi)
from 0.8.4 to 0.8.5.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/prefix-dev/setup-pixi/releases">prefix-dev/setup-pixi's
releases</a>.</em></p>
<blockquote>
<h2>v0.8.5</h2>
<!-- raw HTML omitted -->
<h2>What's Changed</h2>
<h3>🐛 Bug fixes</h3>
<ul>
<li>fix: Support TOML 1.0 for pyproject.toml parsing by <a
href="https://github.com/pavelzw"><code>@​pavelzw</code></a> in <a
href="https://redirect.github.com/prefix-dev/setup-pixi/pull/191">prefix-dev/setup-pixi#191</a></li>
</ul>
<h3>⬆️ Dependency updates</h3>
<ul>
<li>Bump the gh-actions group with 3 updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/prefix-dev/setup-pixi/pull/188">prefix-dev/setup-pixi#188</a></li>
<li>Bump the nodejs group with 6 updates by <a
href="https://github.com/dependabot"><code>@​dependabot</code></a> in <a
href="https://redirect.github.com/prefix-dev/setup-pixi/pull/187">prefix-dev/setup-pixi#187</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/prefix-dev/setup-pixi/compare/v0.8.4...v0.8.5">https://github.com/prefix-dev/setup-pixi/compare/v0.8.4...v0.8.5</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/prefix-dev/setup-pixi/commit/dbaed5efa0bd33d6bd6d162bec8a06d8fc43c807"><code>dbaed5e</code></a>
fix: Support TOML 1.0 for pyproject.toml parsing (<a
href="https://redirect.github.com/prefix-dev/setup-pixi/issues/191">#191</a>)</li>
<li><a
href="https://github.com/prefix-dev/setup-pixi/commit/30d4899438de90430e40752205f9bb6f67939dab"><code>30d4899</code></a>
Bump the nodejs group with 6 updates (<a
href="https://redirect.github.com/prefix-dev/setup-pixi/issues/187">#187</a>)</li>
<li><a
href="https://github.com/prefix-dev/setup-pixi/commit/00ff9955aafc3069179be56a98a299a34332d59b"><code>00ff995</code></a>
Bump the gh-actions group with 3 updates (<a
href="https://redirect.github.com/prefix-dev/setup-pixi/issues/188">#188</a>)</li>
<li>See full diff in <a
href="https://github.com/prefix-dev/setup-pixi/compare/v0.8.4...v0.8.5">compare
view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility
score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=prefix-dev/setup-pixi&package-manager=github_actions&previous-version=0.8.4&new-version=0.8.5)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after
your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge
and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating
it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore this major version` will close this PR and stop
Dependabot creating any more for this major version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop
Dependabot creating any more for this minor version (unless you reopen
the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop
Dependabot creating any more for this dependency (unless you reopen the
PR or upgrade to it yourself)


</details>

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Continuing from #2224:

```
18.086399 seconds (4.01 M allocations: 3.255 GiB, 2.81% gc time)
17.879659 seconds (2.95 M allocations: 2.949 GiB, 1.69% gc time)
```

Adding the type assert on the function does the trick. I also go rid of
the `isdae` check because it shouldn't be needed, and I noticed
`get_dae_uprev` leads to runtime dispatch since it is using
`isdefined(integrator, ...)`

There are still more optimizations to be made to backtracking, but good
to bag this one already.
I also noticed many extra allocations if `concentration = true`, we
should also look at that.
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.

Linear Basin implementation for allocation
4 participants