From 0b1297a22ae76ecf740425cecb8b604db8df4dd1 Mon Sep 17 00:00:00 2001 From: Connor Ward Date: Wed, 25 Jun 2025 10:50:22 +0100 Subject: [PATCH 1/6] Do not import PETSc at the top-level This avoids us initialising PETSc before petsctools.init is called. --- petsctools/options.py | 234 ++++++++++++++++++++++-------------------- 1 file changed, 124 insertions(+), 110 deletions(-) diff --git a/petsctools/options.py b/petsctools/options.py index 58fcff5..92469a0 100644 --- a/petsctools/options.py +++ b/petsctools/options.py @@ -1,4 +1,5 @@ import contextlib +import functools import itertools import warnings @@ -73,127 +74,140 @@ 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. + .. code-block:: python3 + + with self.inserted_options(): + self.snes.solve(...) - 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). + 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. - 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. + This object can also be used only to manage insertion and deletion + into the PETSc options database, by using the context manager. + """ - .. code-block:: python3 + @classmethod + def get_commandline_options(cls): + """ + 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. + """ + from petsc4py import PETSc - with self.inserted_options(): - self.snes.solve(...) + return frozenset(PETSc.Options().getAll()) - 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 self.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 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] + + @property + def options_object(self): + from petsc4py import PETSc + + return PETSc.Options() - 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] From 18a77df9e057aabc0457003644dc82af849340bd Mon Sep 17 00:00:00 2001 From: Connor Ward Date: Wed, 25 Jun 2025 10:51:34 +0100 Subject: [PATCH 2/6] fixup --- petsctools/options.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/petsctools/options.py b/petsctools/options.py index 92469a0..7672d54 100644 --- a/petsctools/options.py +++ b/petsctools/options.py @@ -1,10 +1,7 @@ import contextlib -import functools import itertools import warnings -from petsctools import utils - def flatten_parameters(parameters, sep="_"): """Flatten a nested parameters dict, joining keys with sep. From af65685564dab26ba6051c8d108bc5eb01b0e017 Mon Sep 17 00:00:00 2001 From: Connor Ward Date: Wed, 25 Jun 2025 12:21:08 +0100 Subject: [PATCH 3/6] Make options_object a cached_property --- petsctools/options.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/petsctools/options.py b/petsctools/options.py index 7672d54..84ca6ed 100644 --- a/petsctools/options.py +++ b/petsctools/options.py @@ -1,4 +1,5 @@ import contextlib +import functools import itertools import warnings @@ -202,7 +203,7 @@ def inserted_options(self): for k in self.to_delete: del self.options_object[self.options_prefix + k] - @property + @functools.cached_property def options_object(self): from petsc4py import PETSc From 39411a29b6f740fac9f72ca6c8aaa5f9f87928be Mon Sep 17 00:00:00 2001 From: Connor Ward Date: Tue, 1 Jul 2025 13:41:48 +0100 Subject: [PATCH 4/6] Refactor to eagerly save cmd options --- petsctools/__init__.py | 2 +- petsctools/exceptions.py | 4 ++++ petsctools/init.py | 5 +++++ petsctools/options.py | 27 ++++++++++++++++----------- 4 files changed, 26 insertions(+), 12 deletions(-) diff --git a/petsctools/__init__.py b/petsctools/__init__.py index ea2a920..4c1b799 100644 --- a/petsctools/__init__.py +++ b/petsctools/__init__.py @@ -19,4 +19,4 @@ InvalidPetscVersionException, init, ) - from .options import OptionsManager # noqa: F401 + from .options import OptionsManager, get_commandline_options # noqa: F401 diff --git a/petsctools/exceptions.py b/petsctools/exceptions.py index 5e04248..afb19fc 100644 --- a/petsctools/exceptions.py +++ b/petsctools/exceptions.py @@ -1,2 +1,6 @@ class PetscToolsException(Exception): """Generic base class for petsctools exceptions.""" + + +class PetscToolsNotInitialisedException(PetscToolsException): + """Exception raised when petsctools should have been initialised.""" diff --git a/petsctools/init.py b/petsctools/init.py index 78a9932..ac290d6 100644 --- a/petsctools/init.py +++ b/petsctools/init.py @@ -5,6 +5,7 @@ from packaging.specifiers import SpecifierSet from packaging.version import Version +import petsctools.options from petsctools.exceptions import PetscToolsException @@ -27,6 +28,10 @@ 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 diff --git a/petsctools/options.py b/petsctools/options.py index 84ca6ed..86b9e3c 100644 --- a/petsctools/options.py +++ b/petsctools/options.py @@ -3,6 +3,22 @@ import itertools import warnings +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="_"): """Flatten a nested parameters dict, joining keys with sep. @@ -116,17 +132,6 @@ class OptionsManager: into the PETSc options database, by using the context manager. """ - @classmethod - def get_commandline_options(cls): - """ - 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. - """ - from petsc4py import PETSc - - return frozenset(PETSc.Options().getAll()) - count = itertools.count() def __init__(self, parameters, options_prefix): From 81a6b588bc25d2ccd5bf0a68fe54b6968a8c3a64 Mon Sep 17 00:00:00 2001 From: Connor Ward Date: Tue, 1 Jul 2025 13:42:34 +0100 Subject: [PATCH 5/6] fixup --- petsctools/options.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/petsctools/options.py b/petsctools/options.py index 86b9e3c..c79786c 100644 --- a/petsctools/options.py +++ b/petsctools/options.py @@ -154,7 +154,7 @@ def __init__(self, parameters, options_prefix): self.parameters = { k: v for k, v in parameters.items() - if options_prefix + k not in self.get_commandline_options() + if options_prefix + k not in get_commandline_options() } self.to_delete = set(self.parameters) # Now update parameters from options, so that they're From 2eb9a52b5d07c6d6921351e9807c6570fe5b3115 Mon Sep 17 00:00:00 2001 From: Connor Ward Date: Wed, 2 Jul 2025 13:12:13 +0100 Subject: [PATCH 6/6] linting --- petsctools/init.py | 5 ++++- petsctools/options.py | 6 ++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/petsctools/init.py b/petsctools/init.py index ac290d6..9305ca6 100644 --- a/petsctools/init.py +++ b/petsctools/init.py @@ -30,7 +30,10 @@ def init(argv=None, *, 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()) + + petsctools.options._commandline_options = frozenset( + PETSc.Options().getAll() + ) def check_environment_matches_petsc4py_config(): diff --git a/petsctools/options.py b/petsctools/options.py index c79786c..83cbd3a 100644 --- a/petsctools/options.py +++ b/petsctools/options.py @@ -13,13 +13,12 @@ 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" + "'petsctools.init' has not been called so the command line " + "options have not been set" ) return _commandline_options - def flatten_parameters(parameters, sep="_"): """Flatten a nested parameters dict, joining keys with sep. @@ -213,4 +212,3 @@ def options_object(self): from petsc4py import PETSc return PETSc.Options() -