Skip to content

Conversation

Shawniz
Copy link

@Shawniz Shawniz commented Aug 29, 2025

This PR introduces a new eigenproblem/eigensolver abstraction that enables one to compute spectra of the linearisation of a sequence of Firedrake operations. The RFEigenproblem/RFEigensolver tandem closely mirror the LinearEigenproblem/LinearEigensolver tandem.
The main changes are introduced in the RFEigenproblem abstraction. Inspired by the interfacing and ability to define matrices implicitly introduced by @JHopeCollins in the JHopeCollins/tao-rf-mats branch, a new context RestrictedReducedFunctionalCtx object is used in the creation of the RestrictedReducedFunctionalMat which represents the linearisation of a sequence of FIredrake operations on a restricted function space.

Copy link
Contributor

@connorjward connorjward left a comment

Choose a reason for hiding this comment

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

Some early feedback. I think @JHopeCollins needs to review this.



class RFEigenproblem:
"""Generalised linear eigenvalue problem.
Copy link
Contributor

Choose a reason for hiding this comment

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

Docstring does not really describe the reduced functional aspect of this class.

Copy link
Author

Choose a reason for hiding this comment

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

I modified the docstring. Now I believe it is clearer.

PETSc = None


def new_restricted_control_variable(reduced_functional, function_space, dual=False):
Copy link
Contributor

Choose a reason for hiding this comment

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

Please use type hints throughout.

Copy link
Author

Choose a reason for hiding this comment

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

I added type hints where I believed arguments are unclear.

Copy link
Contributor

Choose a reason for hiding this comment

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

Thanks, but please include them everywhere that you've written a docstring.

Copy link
Author

Choose a reason for hiding this comment

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

Done.

@JHopeCollins JHopeCollins self-requested a review September 1, 2025 13:06


class RFEigensolver:
r"""Solve a LinearEigenproblem.
Copy link
Contributor

Choose a reason for hiding this comment

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

Shouldn't this be a RFEigenProblem?

Copy link
Author

Choose a reason for hiding this comment

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

I think now it is clearer. Apologies, forgot to change.



class RFEigenproblem:
"""Generalised eigenvalue problem.
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think that this line really describes what is going on. This is just copied from eigensolver.py.

Copy link
Author

Choose a reason for hiding this comment

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

Updated for more clarity.

apply_riez : bool
If True, when defining A's adjoint action, we can output into the dual space.
action : str
Determine whether forward action is TLM or adjoint.
Copy link
Contributor

Choose a reason for hiding this comment

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

Please enumerate the valid options here.

Copy link
Member

Choose a reason for hiding this comment

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

The action docstring is now duplicated so this one can be removed.

Copy link
Member

@JHopeCollins JHopeCollins left a comment

Choose a reason for hiding this comment

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

This is really great, thanks for your contribution!

I've left a lot of comments/suggestions but the vast majority are minor style changes, in general I'm very happy with this.

If you have the time, then a little demo would be fantastic to have. It could just be showing the Helmholtz problem, but it will be much easier for people to find as a demo than in the tests.

problem_form = LinearEigenproblem(a, bcs=bc, restrict=True)
solver_form = LinearEigensolver(problem_form, n_evals=num_eigenvalues)
solver_form.solve()
nconv = solver_form.es.getConverged()
Copy link
Member

Choose a reason for hiding this comment

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

Should we be checking how many converged here?

Copy link
Author

Choose a reason for hiding this comment

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

I have tried but faced a bit of obstacles. The aim of the test is to check that two eigenvalue abstractions outputs the same results.

Comment on lines +65 to +67
evals_rf = [
1 / solver_rf.eigenvalue(i) for i in range(0, min(num_eigenvalues, nconv))
]
Copy link
Member

Choose a reason for hiding this comment

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

Why are we grabbing eigenvalues from the LinearEigenSolver but inverse eigenvalues from the RFEigenSolver?
Is this the right thing to do? If so, please add a comment explaining why.

Suggested change
evals_rf = [
1 / solver_rf.eigenvalue(i) for i in range(0, min(num_eigenvalues, nconv))
]
evals_rf = [
1 / solver_rf.eigenvalue(i) for i in range(min(num_eigenvalues, nconv))
]

Copy link
Author

Choose a reason for hiding this comment

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

This is done because the eigenvalue problem in LinearEigenproblem arises from the spatial discretisation of the bilinear forms arising from the Helmholtz PDE with forcing function f=cu for eigenvalue c. The solution operator in this case is a mapping from the forcing function to the solution. A wave-of-hands argument is that Jf=u with the f substituted from above, and using the fact that J is linear (which it is as the PDE is steady and linear) rearranges to `Ju = 1/cu'.

Comment on lines +397 to +405
A = self._problem.A
M = self._problem.M
if not isinstance(A, PETSc.Mat):
raise TypeError(f"A must be a PETSc matrix, got {A.__class__}")
if not isinstance(M, PETSc.Mat) and (M is not None):
raise TypeError(f"M must be a PETSc matrix or None (identity), got {M.__class__}")

self.es.setDimensions(nev=self.n_evals, ncv=self.ncv, mpd=self.mpd)
self.es.setOperators(A, M)
Copy link
Member

Choose a reason for hiding this comment

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

The operators should be set in __init__, before the call to set_from_options. This is so that:

  1. The solver actually has access to the Mats when everything is being set up during set_from_options,
  2. We don't incorrectly/unnecessarily reset the operators every time we call solve.

Copy link
Author

Choose a reason for hiding this comment

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

Agreed, this change could probably be mirrored in eigensolver.py in the LinearEigensolver class.

Copy link
Member

Choose a reason for hiding this comment

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

Yes, if they are also set in solve in the LinearEigenSolver then that is also a bug.

They also need to be before the call to set_from_option in the init to make sure that it's all set up correctly.

Comment on lines +491 to +501
if isinstance(variables, (tuple, list)):
if isinstance(variables[0], Function):
return tuple(Function(function_space).interpolate(v) for v in variables)
else:
return tuple(
Cofunction(function_space.dual()).interpolate(v) for v in variables
)
elif isinstance(variables, Function):
return Function(function_space).interpolate(variables)
else:
return Cofunction(function_space.dual()).interpolate(variables)
Copy link
Member

Choose a reason for hiding this comment

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

You can use pyadjoint.Enlist to handle whether we have a list or not. I think this is the right way to do it.

Suggested change
if isinstance(variables, (tuple, list)):
if isinstance(variables[0], Function):
return tuple(Function(function_space).interpolate(v) for v in variables)
else:
return tuple(
Cofunction(function_space.dual()).interpolate(v) for v in variables
)
elif isinstance(variables, Function):
return Function(function_space).interpolate(variables)
else:
return Cofunction(function_space.dual()).interpolate(variables)
from ufl.duals import is_primal
variables = Enlist(variables)
V = function_space if is_primal(variables[0]) else function_space.dual()
interp_vars = [Function(V).interpolate(v) for v in variables)
return variables.delist(interp_vars)

Comment on lines +97 to +103
# Check adjoint action
problem_rf_true = RFEigenproblem(
J, bcs=bc, apply_riesz=True, restrict=restrict, action="adjoint"
)
problem_rf_false = RFEigenproblem(
J, bcs=bc, apply_riesz=False, restrict=restrict, action="adjoint"
)
Copy link
Member

Choose a reason for hiding this comment

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

Seeing as we're specifically testing the implicit matrix here, I think it'd be simpler to just construct them explicitly rather than going via the RFEigenProblem.

Copy link
Author

Choose a reason for hiding this comment

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

I actually thought that the code is less messy in this case. Otherwise, would have to do casework depending on restricted an unrestricted stuff. Also, would have to add functions to determine restricted space. Much more clunky whilst this happens under the hood of RFEigenproblem.

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