Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion petsctools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,4 @@
InvalidPetscVersionException,
init,
)
from .options import OptionsManager # noqa: F401
from .options import OptionsManager, get_commandline_options # noqa: F401
4 changes: 4 additions & 0 deletions petsctools/exceptions.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,6 @@
class PetscToolsException(Exception):
"""Generic base class for petsctools exceptions."""


class PetscToolsNotInitialisedException(PetscToolsException):
"""Exception raised when petsctools should have been initialised."""
8 changes: 8 additions & 0 deletions petsctools/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from packaging.specifiers import SpecifierSet
from packaging.version import Version

import petsctools.options
from petsctools.exceptions import PetscToolsException


Expand All @@ -27,6 +28,13 @@ def init(argv=None, *, version_spec=""):
check_environment_matches_petsc4py_config()
check_petsc_version(version_spec)

# Save the command line options so they may be inspected later
from petsc4py import PETSc

petsctools.options._commandline_options = frozenset(
PETSc.Options().getAll()
)


def check_environment_matches_petsc4py_config():
import petsc4py
Expand Down
239 changes: 127 additions & 112 deletions petsctools/options.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,22 @@
import contextlib
import functools
import itertools
import warnings

from petsctools import utils
from petsctools.exceptions import PetscToolsNotInitialisedException


_commandline_options = None


def get_commandline_options() -> frozenset:
"""Return the PETSc options passed on the command line."""
if _commandline_options is None:
raise PetscToolsNotInitialisedException(
"'petsctools.init' has not been called so the command line "
"options have not been set"
)
return _commandline_options


def flatten_parameters(parameters, sep="_"):
Expand Down Expand Up @@ -73,127 +87,128 @@ def munge(keys):
return new


if utils.PETSC4PY_INSTALLED:
from petsc4py import PETSc
class OptionsManager:
"""Mixin class that helps with managing setting PETSc options.

class OptionsManager:
# What appeared on the commandline, we should never clear these.
# They will override options passed in as a dict if an
# options_prefix was supplied.
commandline_options = frozenset(PETSc.Options().getAll())
Parameters
----------

options_object = PETSc.Options()
parameters :
The dictionary of parameters to use.
options_prefix :
The prefix to look up items in the global options database
(may be `None`, in which case only entries from ``parameters``
will be considered. If no trailing underscore is provided,
one is appended. Hence ``foo_`` and ``foo`` are treated
equivalently. As an exception, if the prefix is the empty
string, no underscore is appended.

count = itertools.count()
To use this, you must call its constructor to with the parameters
you want in the options database.

"""Mixin class that helps with managing setting petsc options.
You then call :meth:`set_from_options`, passing the PETSc object
you'd like to call ``setFromOptions`` on. Note that this will
actually only call ``setFromOptions`` the first time (so really
this parameters object is a once-per-PETSc-object thing).

:arg parameters: The dictionary of parameters to use.
:arg options_prefix: The prefix to look up items in the global
options database (may be ``None``, in which case only entries
from ``parameters`` will be considered. If no trailing
underscore is provided, one is appended. Hence ``foo_`` and
``foo`` are treated equivalently. As an exception, if the
prefix is the empty string, no underscore is appended.
So that the runtime monitors which look in the options database
actually see options, you need to ensure that the options database
is populated at the time of a ``SNESSolve`` or ``KSPSolve`` call.
Do that using the :meth:`inserted_options` context manager.

To use this, you must call its constructor to with the parameters
you want in the options database.

You then call :meth:`set_from_options`, passing the PETSc object
you'd like to call ``setFromOptions`` on. Note that this will
actually only call ``setFromOptions`` the first time (so really
this parameters object is a once-per-PETSc-object thing).
.. code-block:: python3

So that the runtime monitors which look in the options database
actually see options, you need to ensure that the options database
is populated at the time of a ``SNESSolve`` or ``KSPSolve`` call.
Do that using the :meth:`inserted_options` context manager.
with self.inserted_options():
self.snes.solve(...)

.. code-block:: python3
This ensures that the options database has the relevant entries
for the duration of the ``with`` block, before removing them
afterwards. This is a much more robust way of dealing with the
fixed-size options database than trying to clear it out using
destructors.

with self.inserted_options():
self.snes.solve(...)
This object can also be used only to manage insertion and deletion
into the PETSc options database, by using the context manager.
"""

This ensures that the options database has the relevant entries
for the duration of the ``with`` block, before removing them
afterwards. This is a much more robust way of dealing with the
fixed-size options database than trying to clear it out using
destructors.
count = itertools.count()

This object can also be used only to manage insertion and deletion
into the PETSc options database, by using the context manager.
def __init__(self, parameters, options_prefix):
super().__init__()
if parameters is None:
parameters = {}
else:
# Convert nested dicts
parameters = flatten_parameters(parameters)
if options_prefix is None:
self.options_prefix = "firedrake_%d_" % next(self.count)
self.parameters = parameters
self.to_delete = set(parameters)
else:
if len(options_prefix) and not options_prefix.endswith("_"):
options_prefix += "_"
self.options_prefix = options_prefix
# Remove those options from the dict that were passed on
# the commandline.
self.parameters = {
k: v
for k, v in parameters.items()
if options_prefix + k not in get_commandline_options()
}
self.to_delete = set(self.parameters)
# Now update parameters from options, so that they're
# available to solver setup (for, e.g., matrix-free).
# Can't ask for the prefixed guy in the options object,
# since that does not DTRT for flag options.
for k, v in self.options_object.getAll().items():
if k.startswith(self.options_prefix):
self.parameters[k[len(self.options_prefix) :]] = v
self._setfromoptions = False

def set_default_parameter(self, key, val):
"""Set a default parameter value.

:arg key: The parameter name
:arg val: The parameter value.

Ensures that the right thing happens cleaning up the options
database.
"""
k = self.options_prefix + key
if k not in self.options_object and key not in self.parameters:
self.parameters[key] = val
self.to_delete.add(key)

def __init__(self, parameters, options_prefix):
super().__init__()
if parameters is None:
parameters = {}
else:
# Convert nested dicts
parameters = flatten_parameters(parameters)
if options_prefix is None:
self.options_prefix = "firedrake_%d_" % next(self.count)
self.parameters = parameters
self.to_delete = set(parameters)
else:
if len(options_prefix) and not options_prefix.endswith("_"):
options_prefix += "_"
self.options_prefix = options_prefix
# Remove those options from the dict that were passed on
# the commandline.
self.parameters = {
k: v
for k, v in parameters.items()
if options_prefix + k not in self.commandline_options
}
self.to_delete = set(self.parameters)
# Now update parameters from options, so that they're
# available to solver setup (for, e.g., matrix-free).
# Can't ask for the prefixed guy in the options object,
# since that does not DTRT for flag options.
for k, v in self.options_object.getAll().items():
if k.startswith(self.options_prefix):
self.parameters[k[len(self.options_prefix) :]] = v
self._setfromoptions = False

def set_default_parameter(self, key, val):
"""Set a default parameter value.

:arg key: The parameter name
:arg val: The parameter value.

Ensures that the right thing happens cleaning up the options
database.
"""
k = self.options_prefix + key
if k not in self.options_object and key not in self.parameters:
self.parameters[key] = val
self.to_delete.add(key)

def set_from_options(self, petsc_obj):
"""Set up petsc_obj from the options database.

:arg petsc_obj: The PETSc object to call setFromOptions on.

Matt says: "Only ever call setFromOptions once". This
function ensures we do so.
"""
if not self._setfromoptions:
with self.inserted_options():
petsc_obj.setOptionsPrefix(self.options_prefix)
# Call setfromoptions inserting appropriate options into
# the options database.
petsc_obj.setFromOptions()
self._setfromoptions = True

@contextlib.contextmanager
def inserted_options(self):
"""Context manager inside which the petsc options database
contains the parameters from this object."""
try:
for k, v in self.parameters.items():
self.options_object[self.options_prefix + k] = v
yield
finally:
for k in self.to_delete:
del self.options_object[self.options_prefix + k]
def set_from_options(self, petsc_obj):
"""Set up petsc_obj from the options database.

:arg petsc_obj: The PETSc object to call setFromOptions on.

Matt says: "Only ever call setFromOptions once". This
function ensures we do so.
"""
if not self._setfromoptions:
with self.inserted_options():
petsc_obj.setOptionsPrefix(self.options_prefix)
# Call setfromoptions inserting appropriate options into
# the options database.
petsc_obj.setFromOptions()
self._setfromoptions = True

@contextlib.contextmanager
def inserted_options(self):
"""Context manager inside which the petsc options database
contains the parameters from this object."""
try:
for k, v in self.parameters.items():
self.options_object[self.options_prefix + k] = v
yield
finally:
for k in self.to_delete:
del self.options_object[self.options_prefix + k]

@functools.cached_property
def options_object(self):
from petsc4py import PETSc

return PETSc.Options()