Skip to content

Reductor for the stationary Stokes equation#2448

Merged
sdrave merged 42 commits into
mainfrom
feature_stokesRB
Jan 8, 2026
Merged

Reductor for the stationary Stokes equation#2448
sdrave merged 42 commits into
mainfrom
feature_stokesRB

Conversation

@ullmannsven

@ullmannsven ullmannsven commented Aug 25, 2025

Copy link
Copy Markdown
Contributor

New features

This PR introduces a StationaryRBStokesReductor for the stationary stokes equation.

Implementation of three reduction methods: supremizer_galerkin, ls-ls (least squares and using least_square=True as solver option) and ls-normal (solves the least-squares problem via the normal equation).

Further adds the stationary stokes equation in 2D posed on a unit circle to model.examples.py, using skfem to discretize the problem and assemble the matrices (currently implemented via Taylor-Hood elements). One can choose the rhs of the stokes equations, but not the domain or the discretization method at the moment.

Open work:

  • Implementation of a StokesAnalyticalProblem (or a general structure for AnalyticalProblem for Block systems)

@ullmannsven ullmannsven requested a review from sdrave August 25, 2025 13:17
@ullmannsven ullmannsven self-assigned this Aug 25, 2025
@ullmannsven ullmannsven added the pr:new-feature Introduces a new feature label Aug 25, 2025

@sdrave sdrave left a comment

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.

Hi @ullmannsven, thanks for your work! I have only looked at the changes in pymor.algorithms so far. I think it would be good to put them into a separate PR to get parts of this merged faster. Also, we would have better documentation about these changes.

Comment thread src/pymor/algorithms/image.py Outdated
Comment thread src/pymor/algorithms/simplify.py Outdated
Comment thread src/pymor/algorithms/simplify.py Outdated
Comment thread src/pymor/algorithms/simplify.py Outdated
Comment thread src/pymor/algorithms/simplify.py Outdated
Comment on lines +127 to +133
zeros = [[None for _ in range(ncols)] for _ in range(nrows)]
for l in range(nrows):
for j in range(ncols):
if op.blocks[l, j] is not None:
source_ = op.blocks[l, j].source
range_ = op.blocks[l, j].range
zeros[l][j] = ZeroOperator(range_, source_)

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.

You can pass None for a zero block to BlockOperator, so I don't think you need this.

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.

Actually, you do, because BlockOperator.__init__ will fail if have a row or column of Nones. However, it might make sense to give that method optional range and source space arguments to handle that special case. Then you could just pass op.range and op.source everywhere without having to worry about the zero operators.

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.

I followed this approach, it first led it to sum unexpected errors when a projected system got projected to a subbasis again. I found a fix, but i would like to here your opinion if this is now good enough.

Comment thread src/pymor/algorithms/simplify.py Outdated
Comment thread src/pymor/algorithms/simplify.py Outdated
Comment thread src/pymor/algorithms/simplify.py Outdated
Comment thread src/pymor/algorithms/simplify.py Outdated
Comment thread src/pymor/algorithms/simplify.py Outdated
@ullmannsven ullmannsven added this to the 2025.2 milestone Sep 15, 2025
@codecov

codecov Bot commented Sep 19, 2025

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.78544% with 11 lines in your changes missing coverage. Please review.
✅ Project coverage is 88.06%. Comparing base (d394490) to head (61433e9).
⚠️ Report is 43 commits behind head on main.

Files with missing lines Patch % Lines
src/pymor/reductors/basic.py 80.00% 7 Missing ⚠️
src/pymordemos/stokes.py 97.29% 2 Missing ⚠️
src/pymor/models/examples.py 98.24% 1 Missing ⚠️
src/pymor/reductors/stokes.py 98.18% 1 Missing ⚠️
Additional details and impacted files
Flag Coverage Δ
github_actions 77.50% <21.83%> (-0.43%) ⬇️
gitlab_ci 88.00% <95.78%> (+0.08%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/pymor/discretizers/skfem/cg.py 81.59% <100.00%> (+0.96%) ⬆️
src/pymor/models/saddle_point.py 100.00% <100.00%> (ø)
src/pymortests/demos.py 96.80% <100.00%> (+0.01%) ⬆️
src/pymortests/models/saddle_point.py 99.53% <100.00%> (+0.02%) ⬆️
src/pymor/models/examples.py 85.02% <98.24%> (+6.84%) ⬆️
src/pymor/reductors/stokes.py 98.18% <98.18%> (ø)
src/pymordemos/stokes.py 97.29% <97.29%> (ø)
src/pymor/reductors/basic.py 78.02% <80.00%> (+0.19%) ⬆️

... and 3 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@sdrave

sdrave commented Nov 7, 2025

Copy link
Copy Markdown
Member

@ullmannsven, could you rebase on main to get a clean commit history?

@ullmannsven ullmannsven changed the title Reductor for the stationary Stokes equation including additional functionality for BlockOperators. Reductor for the stationary Stokes equation Nov 12, 2025
@ullmannsven ullmannsven requested a review from sdrave November 12, 2025 09:52
@ullmannsven

ullmannsven commented Nov 13, 2025

Copy link
Copy Markdown
Contributor Author

I added a StationaryLSRBReductor which is used by the two least-squares variants of the stokesreductor. This reductor has two cases one where the normal equations are formulated and another case that equips the projected operator with the ScipyLSTSQSolver. As I tried to formulate the StationaryLSRBReductor as general as possible, i.e. not specific for blocked systems, I had to introduce a helper_fom in the StokesReductor in order to correctly handle the products (the blocksystem comes with two product operators for the u and p space, but as the StationaryLSRBReductor only gets on RB (in case of the StokesReductor the blockDiagonalArray) and projects the product (or products if the dict contains more) all onto that RB). Therefore, I had two wrap the two product operators into a mixed_product and reproject again after the StationaryLSRBReductor was called in order two keep two seperate products for the rom.

I therefore thought about introducing handling for blocked system for the StationaryLSRBReductor, but as all the other reductors in basic also dont handle this, I left it out. What are your thoughts? @sdrave

@sdrave

sdrave commented Nov 13, 2025

Copy link
Copy Markdown
Member

I had to introduce a helper_fom in the StokesReductor in order to correctly handle the products (the blocksystem comes with two product operators for the u and p space, but as the StationaryLSRBReductor only gets on RB (in case of the StokesReductor the blockDiagonalArray) and projects the product (or products if the dict contains more) all onto that RB). Therefore, I had two wrap the two product operators into a mixed_product and reproject again after the StationaryLSRBReductor was called in order two keep two seperate products for the rom.

I therefore thought about introducing handling for blocked system for the StationaryLSRBReductor, but as all the other reductors in basic also dont handle this, I left it out. What are your thoughts? @sdrave

I think the real issue here is that there is no clear concept of what should be in the products dictionary. StationaryRBReductor assumes all products to act on solution_space. This is violated by SaddlePointModel. Being a subclass of StationaryModel I should be able to use it everywhere a StationaryModel is expected.

I see two viable solutions:

  • Don't add the u and p products to the products dict. Store them as separate attributes (and only add the block diagonal version to the products dict).
  • Modify StationaryRBReductor so that products, which cannot be projected with RB are excluded from the ROM's products dict.

I lean towards the second option. What do you think?

@ullmannsven

ullmannsven commented Nov 13, 2025

Copy link
Copy Markdown
Contributor Author

I lean towards the second option. What do you think?

I went with the second option. Is this what you had in mind? I’m basically only performing the projection when the first two assertions in project pass.

@sdrave sdrave left a comment

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.

Hi @ullmannsven, sorry for the long delay!

Comment thread src/pymor/reductors/basic.py Outdated
Comment thread src/pymor/reductors/basic.py Outdated
Comment thread src/pymor/reductors/stokes.py Outdated
Comment thread src/pymor/reductors/basic.py Outdated
Comment thread src/pymor/reductors/basic.py Outdated
Comment thread src/pymor/reductors/basic.py Outdated
Comment thread src/pymor/reductors/stokes.py Outdated
Comment thread src/pymor/reductors/stokes.py Outdated
Comment thread src/pymor/reductors/stokes.py Outdated
Comment thread src/pymor/reductors/basic.py Outdated
Comment thread src/pymor/reductors/basic.py Outdated
@ullmannsven ullmannsven requested a review from sdrave November 23, 2025 12:36

@sdrave sdrave left a comment

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.

Almost there ...

Comment thread src/pymor/models/examples.py Outdated
Comment thread src/pymor/reductors/basic.py Outdated
Comment on lines +73 to +80
if dims == self._last_rom_dims:
return self._last_rom
else:
elif dims != self._last_rom_dims:
return self._reduce_to_subbasis(dims)

return self._last_rom

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.

Why this change?

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.

This is required in case supremizer enrichment is used. If reduce gets called the first time (without the dims argument), the the lengths of the current RBs are stored in the dims dict. Due to supremizer enrichment, the lengths of the RBs change. These new lengths are stored in self.last_rom_dims, as this gets computed after _reduce, where the actually projection of the operators and the supremizer enrichment takes place. In the old version, the check dims != self._last_rom_dims would always return True in case supremizer enrichment is used, which is certainly not desired as then projection to the subbasis would be performed. Does that make sense?

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.

Ah, now I understand the issue. However, the new version is also problematic:

  • Assume we are using StationaryRBReductor. I initialise it with / extend its basis to a basis of dimension N+1. Then I call reductor.reduce(N). With your code I would get a ROM of order N+1.
    -For StationaryRBStokesReductor, reduce_to_subasis probably does a different thing than what the user would expect. For instance, if I initialize it with a u basis of dimension N and later call reduce with dimension N-1 for the u basis, I would get a ROM where one supremizer has been removed instead of a u basis vector.

I think the only proper solution here is that the supremizers need to be kept separate from the u basis. Then in project_operators/project_operators_to_subbasis one needs to assemble the actual projection basis by appending the right amount of supremizers and orthogonalize them against the other basis vectors.

Downsides:

  • The resulting ROM will have a larger solution space dimension than the sums of the provided dimensions. (But this already happens with your code when I initialize with some bases and then call reduce with the dimensions of these bases.)
  • One cannot use project_to_subbasis to have a faster projection.

Upsides:

  • The ROM will always be the supremizer-enrichment ROM corresponding to the provided RB dims.
  • Each of the ROMs will be stable.

I don't think there are better alternatives, but maybe I'm mistaken?

Comment thread src/pymor/reductors/basic.py Outdated


class StationaryLSRBReductor(ProjectionBasedReductor):
"""Least-squares projection based reductor for stationary problems.

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.

Is "least-squares projection" something people say? Does it really mean to minimize the norm of the resiual? Could this be misleading? (Maybe "least-squares Petrov-Galerkin projection" would be better?)

@ullmannsven ullmannsven Nov 24, 2025

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.

Yes, maybe using Petrov-Galerkin projection here is a bit more clear (at least for the case without normal equations?) I dont have a better phrasing in mind at the moment, so i changed it to Least-squares Petrov-Galerkin. I also changed the next sentence in the documentation.

Comment thread src/pymor/reductors/basic.py Outdated
Comment thread src/pymor/reductors/basic.py Outdated
Comment thread src/pymor/reductors/stokes.py Outdated
Comment thread src/pymor/reductors/stokes.py Outdated
Comment thread src/pymor/reductors/stokes.py Outdated
Comment thread src/pymor/reductors/stokes.py Outdated
Comment thread src/pymor/reductors/stokes.py Outdated
'output_functional': project_to_subbasis(rom.output_functional, None, dim_trial)
}

else:

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.

I think you want to save the StationarayLSRBReductor as an attribute and call its project_operator_to_subasis here.

@ullmannsven ullmannsven Nov 24, 2025

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.

I changed it, but it only works if project_operators_to_subbasis in the StationaryLSRBReductor gets an additional, optional argument last_rom (the StationaryLSRBReductor itself has no _last_rom, as reduce (where this is initialized) gets called on the StationaryRBStokesReductor by the user).

To be consistent with the other reductors in basic.py i changed this for all reductors. Is this okay? It should not break any other code and can be a nice option to have this available?

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.

Maybe it would then be better to call reduce on the sub-reductor and extract the operators from the resulting ROM?

Anyway, I just became aware the project_to_subbasis does not really do what it promises. By just adding the dimensions, you happen to truncate the last basis vectors, to whatever field they belong to. This is clearly not the intention of the user. I fear there is really no efficient implementation of project_operators_to_subbasis here. You just have to assemble a new effective basis an project again.
(Something could be done when the block structure would be preserved during projection. With supremizer enrichment, one would additionally have to do separate projections for the supremizer/non-supremizer parts of the basis and then glue everything together. I don't think it is worth the hassle to change everything just for a faster reduce_to_subbasis.)

@sdrave

sdrave commented Dec 17, 2025

Copy link
Copy Markdown
Member

Tests run fine now, but we get an

ImportError: cannot import name 'bmat' from 'skfem.utils'

in the 'scikit_fem oldest' build. Scikit-FEM is pinned to 6.0.0 in the 'oldest' image. Could you check in which version skfem.utils.bmat has been introduced? Then we can increase the minium required version.

@github-actions

Copy link
Copy Markdown
Contributor

This pull request modifies pyproject.toml or requirements-ci-oldest-pins.in.
In case dependencies were changed, make sure to call

make ci_requirements

and commit the changed files to ensure that CI runs with the updated dependencies.

@sdrave sdrave left a comment

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.

This is, hopefully, my last round of review for this PR. Mostly minor comments. The only thing that might require relevant work is my comment regarding the discretization. But as I wrote: this can also wait until after the release.

Comment thread src/pymor/models/examples.py Outdated
Comment thread src/pymor/models/examples.py Outdated
Comment thread src/pymor/models/examples.py Outdated
Comment thread src/pymor/models/examples.py Outdated
Comment thread src/pymor/models/examples.py Outdated
Comment thread src/pymor/reductors/stokes.py Outdated
Comment thread src/pymor/reductors/stokes.py Outdated
Comment thread src/pymordemos/stokes.py Outdated
Comment thread src/pymordemos/stokes.py Outdated
Comment thread src/pymordemos/stokes.py Outdated
@sdrave sdrave added this pull request to the merge queue Jan 8, 2026
Merged via the queue into main with commit 4247b14 Jan 8, 2026
22 checks passed
@sdrave sdrave deleted the feature_stokesRB branch January 8, 2026 18:32
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

pr:new-feature Introduces a new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants