From 56705cdc98eb06b29d9d9c8af5bd3dbb2a503d04 Mon Sep 17 00:00:00 2001 From: JHopeCollins Date: Thu, 6 Nov 2025 10:46:29 +0000 Subject: [PATCH 1/7] get_default_options --- petsctools/__init__.py | 2 + petsctools/options.py | 85 ++++++++++++++++++++++++++++++++++++++++++ tests/test_options.py | 51 +++++++++++++++++++++++++ 3 files changed, 138 insertions(+) diff --git a/petsctools/__init__.py b/petsctools/__init__.py index 878b76b..6571620 100644 --- a/petsctools/__init__.py +++ b/petsctools/__init__.py @@ -37,6 +37,7 @@ is_set_from_options, inserted_options, set_default_parameter, + get_default_options, ) else: @@ -60,6 +61,7 @@ def __getattr__(name): "is_set_from_options", "inserted_options", "set_default_parameter", + "get_default_options", } if name in petsc4py_attrs: raise ImportError( diff --git a/petsctools/options.py b/petsctools/options.py index 89e2cce..6a3ee68 100644 --- a/petsctools/options.py +++ b/petsctools/options.py @@ -586,6 +586,16 @@ def inserted_options(obj): """Context manager inside which the PETSc options database contains the parameters from this object's OptionsManager. + Parameters + ---------- + obj : + The object which may have been set from options. + + Raises + ------ + PetscToolsException + If the object does not have an OptionsManager. + See Also -------- OptionsManager @@ -593,3 +603,78 @@ def inserted_options(obj): """ with get_options(obj).inserted_options(): yield + + +def get_default_options(base_prefix: str, custom_prefix_endings: str, + options: petsc4py.PETSc.Options | None =None) -> dict: + """ + Extract default options for subsolvers with similar prefixes. + + Some solvers, e.g. PCFieldsplit, create multiple subsolvers whose prefixes + differ only by the final characters, e.g. 'fieldsplit_0', 'fieldsplit_1'. + It is often useful to be able to set default options for these subsolvers + using the un-specialised prefix e.g. 'fieldsplit_ksp_type'. However, just + grabbing all options with the 'fieldsplit' prefix will erroneously find + options like '0_ksp_type' and '1_ksp_type' that were meant for a specific + subsolver. + + Given a base prefix (e.g. 'fieldsplit') and a set of custom prefix endings + (e.g. '0', '1'), this function will return a dictionary of all options with + the base prefix except those which start with the base prefix and one of the + custom endings. + + For example, to set up a fieldsplit solver you might have the following + options, where both fields are to use ILU as the preconditioner. + + .. code-block:: python3 + + -fieldsplit_pc_type ilu + -fieldsplit_0_ksp_type preonly + -fieldsplit_1_ksp_type richardson + -fielspllit_1_ksp_richardson_scale 0.9 + + To get a dictionary with just the default option ({'pc_type': 'ilu'}) you + would call: + + .. code-block:: python3 + + defaults = get_default_options(base_prefix='fieldsplit', + custom_prefix_endings=('0', '1')) + + Parameters + ---------- + base_prefix : + The prefix for the default options, which must be the beginning of + each full custom prefix. If this does not end in an underscore then + one will be added. + custom_prefix_endings : + The ends of each individual custom prefix. Usually a range of integers. + Each one will be converted to a string and have an underscore appended + if it does not already have one. + + Returns + ------- + The dictionary of default options with the base prefix stripped. + """ + if options is None: + from petsc4py import PETSc + options = PETSc.Options() + + if not base_prefix.endswith("_"): + base_prefix += "_" + custom_prefixes = [base_prefix + str(ending) + for ending in custom_prefix_endings] + for prefix in custom_prefixes: + if not prefix.endswith("_"): + prefix += "_" + + default_options = { + k.removeprefix(base_prefix): v + for k, v in options.getAll().items() + if (k.startswith(base_prefix) + and not any(k.startswith(prefix) for prefix in custom_prefixes)) + } + assert not any(k.startswith(str(end)) + for k in default_options.keys() + for end in custom_prefix_endings) + return default_options diff --git a/tests/test_options.py b/tests/test_options.py index b33bde8..9cc6f96 100644 --- a/tests/test_options.py +++ b/tests/test_options.py @@ -96,3 +96,54 @@ def test_options_prefix(): options = petsctools.OptionsManager({}, options_prefix="myobj", default_prefix="firedrake") assert options.options_prefix.startswith("myobj_") + + +@pytest.mark.skipnopetsc4py +def test_default_options(): + from petsc4py import PETSc + + parameters = { + 'options_left': 0, + 'unrelated_option': 0, + 'base_opt1': 1, + 'base_opt2': 2, + 'base_opt3': 3, + 'base_0_opt3': 4, + 'base_1_opt3': 5, + 'base_2_opt4': 6, + } + options = PETSc.Options() + for k, v in parameters.items(): + options[k] = v + + # default_options = {"opt1": 1, "opt2": 2, "opt3": 3} + default_options = petsctools.get_default_options( + base_prefix="base", custom_prefix_endings=("0", "1", "2")) + + assert len(default_options) == 3 + assert default_options["opt1"] == "1" + assert default_options["opt2"] == "2" + assert default_options["opt3"] == "3" + + options0 = petsctools.OptionsManager( + parameters=default_options, options_prefix="base_0") + options1 = petsctools.OptionsManager( + parameters=default_options, options_prefix="base_1") + options2 = petsctools.OptionsManager( + parameters=default_options, options_prefix="base_2") + + assert len(options0.parameters) == 3 + assert options0.parameters["opt1"] == "1" + assert options0.parameters["opt2"] == "2" + assert options0.parameters["opt3"] == "4" + + assert len(options1.parameters) == 3 + assert options1.parameters["opt1"] == "1" + assert options1.parameters["opt2"] == "2" + assert options1.parameters["opt3"] == "5" + + assert len(options2.parameters) == 4 + assert options2.parameters["opt1"] == "1" + assert options2.parameters["opt2"] == "2" + assert options2.parameters["opt3"] == "3" + assert options2.parameters["opt4"] == "6" From d1bdc5efd74611fab741c215322a8f8573bd33eb Mon Sep 17 00:00:00 2001 From: JHopeCollins Date: Thu, 6 Nov 2025 11:07:21 +0000 Subject: [PATCH 2/7] tidy up docstring --- petsctools/options.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/petsctools/options.py b/petsctools/options.py index 6a3ee68..de1222f 100644 --- a/petsctools/options.py +++ b/petsctools/options.py @@ -606,7 +606,7 @@ def inserted_options(obj): def get_default_options(base_prefix: str, custom_prefix_endings: str, - options: petsc4py.PETSc.Options | None =None) -> dict: + options: petsc4py.PETSc.Options | None = None) -> dict: """ Extract default options for subsolvers with similar prefixes. @@ -619,9 +619,9 @@ def get_default_options(base_prefix: str, custom_prefix_endings: str, subsolver. Given a base prefix (e.g. 'fieldsplit') and a set of custom prefix endings - (e.g. '0', '1'), this function will return a dictionary of all options with - the base prefix except those which start with the base prefix and one of the - custom endings. + (e.g. '0', '1'), this function will return a dictionary of all options + with the base prefix except those which start with the base prefix and one + of the custom endings. For example, to set up a fieldsplit solver you might have the following options, where both fields are to use ILU as the preconditioner. @@ -631,7 +631,6 @@ def get_default_options(base_prefix: str, custom_prefix_endings: str, -fieldsplit_pc_type ilu -fieldsplit_0_ksp_type preonly -fieldsplit_1_ksp_type richardson - -fielspllit_1_ksp_richardson_scale 0.9 To get a dictionary with just the default option ({'pc_type': 'ilu'}) you would call: @@ -651,6 +650,9 @@ def get_default_options(base_prefix: str, custom_prefix_endings: str, The ends of each individual custom prefix. Usually a range of integers. Each one will be converted to a string and have an underscore appended if it does not already have one. + options : + The PETSc.Options database to use. If not provided then the global + database will be used. Returns ------- From dd5f7e4196117d0bb413d931d8bc3fd249f48d58 Mon Sep 17 00:00:00 2001 From: Josh Hope-Collins Date: Thu, 20 Nov 2025 13:38:04 +0000 Subject: [PATCH 3/7] DefaultOptionsSet for shared default options --- petsctools/__init__.py | 2 + petsctools/options.py | 285 ++++++++++++++++++++++++++++------------- tests/test_options.py | 26 ++-- 3 files changed, 217 insertions(+), 96 deletions(-) diff --git a/petsctools/__init__.py b/petsctools/__init__.py index e9c4e55..5527f29 100644 --- a/petsctools/__init__.py +++ b/petsctools/__init__.py @@ -38,6 +38,7 @@ inserted_options, set_default_parameter, get_default_options, + DefaultOptionSet, ) from .pc import PCBase # noqa: F401 else: @@ -63,6 +64,7 @@ def __getattr__(name): "inserted_options", "set_default_parameter", "get_default_options", + "DefaultOptionSet", "PCBase", } if name in petsc4py_attrs: diff --git a/petsctools/options.py b/petsctools/options.py index de1222f..c82e2c4 100644 --- a/petsctools/options.py +++ b/petsctools/options.py @@ -5,6 +5,7 @@ import functools import itertools import warnings +from functools import cached_property from typing import Any, Iterable import petsc4py @@ -127,6 +128,147 @@ def _warn_unused_options(all_options: Iterable, used_options: Iterable, ) +def _validate_prefix(prefix): + """Valid prefixes are strings ending with an underscore. + """ + prefix = str(prefix) + if not prefix.endswith("_"): + prefix += "_" + return prefix + + +class DefaultOptionSet: + """ + Defines a set of common default options shared by multiple PETSc objects. + + Some solvers, e.g. PCFieldsplit, create multiple subsolvers whose prefixes + differ only by the final characters, e.g. 'fieldsplit_0', 'fieldsplit_1'. + It is often useful to be able to set default options for these subsolvers + using the un-specialised prefix e.g. 'fieldsplit_ksp_type'. However, just + grabbing all options with the 'fieldsplit' prefix will erroneously find + options like '0_ksp_type' and '1_ksp_type' that were meant for a specific + subsolver. + + DefaultOptionSet defines a base prefix (e.g. 'fieldsplit') and a set of + custom prefix endings (e.g. [0, 1]). If passed to an ``OptionsManager`` + then any default options present in the global options database will be + used if those options + + For example, to set up a fieldsplit solver you might have the following + options, where both fields are to use ILU as the preconditioner but each + field uses a different KSP type. + + .. code-block:: python3 + + -fieldsplit_pc_type ilu + -fieldsplit_0_ksp_type preonly + -fieldsplit_1_ksp_type richardson + + To create an OptionsManager for each field you would call: + + .. code-block:: python3 + + default_options_set = DefaultOptionSet( + base_prefix='fieldsplit', + custom_prefix_endings=(0, 1)) + + fieldsplit_0_options = OptionsManager( + parameters={}, + options_prefix="fieldsplit_0", + default_options_set=default_options_set) + + fieldsplit_1_options = OptionsManager( + parameters={}, + options_prefix="fieldsplit_1", + default_options_set=default_options_set) + + Attributes + ---------- + base_prefix : + The prefix for the default options, which is the beginning of + each full custom prefix. + custom_prefix_endings : + The ends of each individual custom prefix. Often a range of integers. + + Notes + ----- + The base prefix and each custom prefix ending will be converted to a + string and have an underscore appended if they do not already have one. + + See Also + -------- + OptionsManager + get_default_options + attach_options + set_from_options + """ + + def __init__(self, base_prefix: str, custom_prefix_endings: Iterable): + if not custom_prefix_endings: + raise ValueError("custom_prefix_endings cannot be empty") + + base_prefix = _validate_prefix(base_prefix) + + self._base_prefix = base_prefix + self._custom_prefix_endings = tuple( + _validate_prefix(end) for end in custom_prefix_endings) + + @property + def base_prefix(self): + return self._base_prefix + + @property + def custom_prefix_endings(self): + return self._custom_prefix_endings + + @cached_property + def custom_prefixes(self): + return tuple(self.base_prefix + ending + for ending in self.custom_prefix_endings) + + +def get_default_options(default_options_set: DefaultOptionSet, + options: petsc4py.PETSc.Options | None = None) -> dict: + """ + Extract default options for subsolvers with similar prefixes. + + Parameters + ---------- + default_options_set : + The DefaultOptionSet which defines the shared options. + options : + The PETSc.Options database to use. If not provided then the global + database will be used. + + Returns + ------- + The dictionary of default options with the base prefix stripped. + + See Also + -------- + DefaultOptionSet + """ + if options is None: + from petsc4py import PETSc + options = PETSc.Options() + + base_prefix = default_options_set.base_prefix + custom_prefixes = default_options_set.custom_prefixes + custom_prefix_endings = default_options_set.custom_prefix_endings + + default_options = { + k.removeprefix(base_prefix): v + for k, v in options.getAll().items() + if (k.startswith(base_prefix) + and not any(k.startswith(prefix) for prefix in custom_prefixes)) + } + # Sanity check, this should never happen. + assert not any(k.startswith(str(end)) + for k in default_options.keys() + for end in custom_prefix_endings) + return default_options + + class OptionsManager: """Class that helps with managing setting PETSc options. @@ -233,6 +375,9 @@ class OptionsManager: form "{default_prefix}_{n}", where n is a unique integer. Note that because the unique integer is not stable any options passed via the command line with a matching prefix will be ignored. + default_options_set + The prefix set for any default shared with other solvers. + See ``DefaultOptionSet`` for more information. See Also -------- @@ -242,38 +387,70 @@ class OptionsManager: set_from_options is_set_from_options inserted_options + DefaultOptionSet """ count = itertools.count() def __init__(self, parameters: dict, options_prefix: str | None = None, - default_prefix: str | None = None): + default_prefix: str | None = None, + default_options_set: DefaultOptionSet | None = None): super().__init__() if parameters is None: parameters = {} else: # Convert nested dicts parameters = flatten_parameters(parameters) + + # If no prefix is provided generate a default prefix + # and ignore any command line options if options_prefix is None: default_prefix = default_prefix or "petsctools_" - if not default_prefix.endswith("_"): - default_prefix += "_" + default_prefix = _validate_prefix(default_prefix) self.options_prefix = f"{default_prefix}{next(self.count)}_" self.parameters = parameters self.to_delete = set(parameters) + else: - if options_prefix and not options_prefix.endswith("_"): - options_prefix += "_" + options_prefix = _validate_prefix(options_prefix) self.options_prefix = options_prefix - # Remove those options from the dict that were passed on - # the commandline. + + # Are we part of a solver set sharing defaults? + if default_options_set: + if options_prefix not in default_options_set.custom_prefixes: + raise ValueError( + f"The options_prefix {options_prefix} must be one" + f" of the custom_prefixes of the DefaultOptionSet" + f" {default_options_set.custom_prefixes}") + default_options = get_default_options( + default_options_set, self.options_object) + else: + default_options = {} + + # Note: we need to know which parameters to_delete + # so we need to exclude the relevant command line + # options when combining the parameters from the + # defaults and the source code. + + # Start building parameters from the defaults so + # that they will overwritten by any other source. self.parameters = { k: v - for k, v in parameters.items() + for k, v in default_options.items() if options_prefix + k not in get_commandline_options() } + + # Update using the parameters passed in the code but + # exclude those options from the dict that were passed + # on the commandline. + self.parameters.update({ + 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, @@ -281,6 +458,7 @@ def __init__(self, parameters: dict, 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 # Keep track of options used between invocations of inserted_options(). self._used_options = set() @@ -373,7 +551,8 @@ def attach_options( obj: petsc4py.PETSc.Object, parameters: dict | None = None, options_prefix: str | None = None, - default_prefix: str | None = None + default_prefix: str | None = None, + default_options_set: DefaultOptionSet | None = None ) -> None: """Set up an OptionsManager and attach it to a PETSc Object. @@ -387,10 +566,14 @@ def attach_options( The options prefix to use for this object. default_prefix Base string for autogenerated default prefixes. + default_options_set + The prefix set for any default shared with other solvers. See Also -------- OptionsManager + set_from_options + DefaultOptionSet """ if has_options(obj): raise PetscToolsException( @@ -402,6 +585,7 @@ def attach_options( parameters=parameters, options_prefix=options_prefix, default_prefix=default_prefix, + default_options_set=default_options_set ) obj.setAttr("options", options) @@ -487,6 +671,7 @@ def set_from_options( parameters: dict | None = None, options_prefix: str | None = None, default_prefix: str | None = None, + default_options_set: DefaultOptionSet | None = None ) -> None: """Set up a PETSc object from the options in its OptionsManager. @@ -510,6 +695,8 @@ def set_from_options( The options prefix to use for this object. default_prefix Base string for autogenerated default prefixes. + default_options_set + The prefix set for any default shared with other solvers. Raises ------ @@ -526,6 +713,8 @@ def set_from_options( -------- OptionsManager OptionsManager.set_from_options + attach_options + DefaultOptionSet """ if has_options(obj): if parameters is not None or options_prefix is not None: @@ -545,6 +734,7 @@ def set_from_options( obj, parameters=parameters, options_prefix=options_prefix, default_prefix=default_prefix, + default_options_set=default_options_set ) if is_set_from_options(obj): @@ -603,80 +793,3 @@ def inserted_options(obj): """ with get_options(obj).inserted_options(): yield - - -def get_default_options(base_prefix: str, custom_prefix_endings: str, - options: petsc4py.PETSc.Options | None = None) -> dict: - """ - Extract default options for subsolvers with similar prefixes. - - Some solvers, e.g. PCFieldsplit, create multiple subsolvers whose prefixes - differ only by the final characters, e.g. 'fieldsplit_0', 'fieldsplit_1'. - It is often useful to be able to set default options for these subsolvers - using the un-specialised prefix e.g. 'fieldsplit_ksp_type'. However, just - grabbing all options with the 'fieldsplit' prefix will erroneously find - options like '0_ksp_type' and '1_ksp_type' that were meant for a specific - subsolver. - - Given a base prefix (e.g. 'fieldsplit') and a set of custom prefix endings - (e.g. '0', '1'), this function will return a dictionary of all options - with the base prefix except those which start with the base prefix and one - of the custom endings. - - For example, to set up a fieldsplit solver you might have the following - options, where both fields are to use ILU as the preconditioner. - - .. code-block:: python3 - - -fieldsplit_pc_type ilu - -fieldsplit_0_ksp_type preonly - -fieldsplit_1_ksp_type richardson - - To get a dictionary with just the default option ({'pc_type': 'ilu'}) you - would call: - - .. code-block:: python3 - - defaults = get_default_options(base_prefix='fieldsplit', - custom_prefix_endings=('0', '1')) - - Parameters - ---------- - base_prefix : - The prefix for the default options, which must be the beginning of - each full custom prefix. If this does not end in an underscore then - one will be added. - custom_prefix_endings : - The ends of each individual custom prefix. Usually a range of integers. - Each one will be converted to a string and have an underscore appended - if it does not already have one. - options : - The PETSc.Options database to use. If not provided then the global - database will be used. - - Returns - ------- - The dictionary of default options with the base prefix stripped. - """ - if options is None: - from petsc4py import PETSc - options = PETSc.Options() - - if not base_prefix.endswith("_"): - base_prefix += "_" - custom_prefixes = [base_prefix + str(ending) - for ending in custom_prefix_endings] - for prefix in custom_prefixes: - if not prefix.endswith("_"): - prefix += "_" - - default_options = { - k.removeprefix(base_prefix): v - for k, v in options.getAll().items() - if (k.startswith(base_prefix) - and not any(k.startswith(prefix) for prefix in custom_prefixes)) - } - assert not any(k.startswith(str(end)) - for k in default_options.keys() - for end in custom_prefix_endings) - return default_options diff --git a/tests/test_options.py b/tests/test_options.py index 9cc6f96..67527fc 100644 --- a/tests/test_options.py +++ b/tests/test_options.py @@ -117,20 +117,26 @@ def test_default_options(): options[k] = v # default_options = {"opt1": 1, "opt2": 2, "opt3": 3} - default_options = petsctools.get_default_options( + default_option_set = petsctools.DefaultOptionSet( base_prefix="base", custom_prefix_endings=("0", "1", "2")) - assert len(default_options) == 3 - assert default_options["opt1"] == "1" - assert default_options["opt2"] == "2" - assert default_options["opt3"] == "3" - + # test default is overriden by command line options0 = petsctools.OptionsManager( - parameters=default_options, options_prefix="base_0") + parameters={}, + options_prefix="base_0", + default_options_set=default_option_set) + + # test defaults is overriden by command line and source-code options1 = petsctools.OptionsManager( - parameters=default_options, options_prefix="base_1") + parameters={"opt2": "7"}, + options_prefix="base_1", + default_options_set=default_option_set) + + # test both defaults and non-defaults are picked up options2 = petsctools.OptionsManager( - parameters=default_options, options_prefix="base_2") + parameters={}, + options_prefix="base_2", + default_options_set=default_option_set) assert len(options0.parameters) == 3 assert options0.parameters["opt1"] == "1" @@ -139,7 +145,7 @@ def test_default_options(): assert len(options1.parameters) == 3 assert options1.parameters["opt1"] == "1" - assert options1.parameters["opt2"] == "2" + assert options1.parameters["opt2"] == "7" assert options1.parameters["opt3"] == "5" assert len(options2.parameters) == 4 From db178b0d59d8df9a17f0b57752aa5e552a963ede Mon Sep 17 00:00:00 2001 From: Josh Hope-Collins Date: Thu, 20 Nov 2025 13:39:13 +0000 Subject: [PATCH 4/7] remove get_default_options from global namespace --- petsctools/__init__.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/petsctools/__init__.py b/petsctools/__init__.py index 5527f29..d6a7490 100644 --- a/petsctools/__init__.py +++ b/petsctools/__init__.py @@ -37,7 +37,6 @@ is_set_from_options, inserted_options, set_default_parameter, - get_default_options, DefaultOptionSet, ) from .pc import PCBase # noqa: F401 @@ -63,7 +62,6 @@ def __getattr__(name): "is_set_from_options", "inserted_options", "set_default_parameter", - "get_default_options", "DefaultOptionSet", "PCBase", } From 7b1dc09dfad8994f5612388dac7bd5dc48318a1b Mon Sep 17 00:00:00 2001 From: JHopeCollins Date: Thu, 20 Nov 2025 14:03:56 +0000 Subject: [PATCH 5/7] default options docstrings --- petsctools/options.py | 117 +++++++++++++++++++++++------------------- 1 file changed, 65 insertions(+), 52 deletions(-) diff --git a/petsctools/options.py b/petsctools/options.py index c82e2c4..3d71bda 100644 --- a/petsctools/options.py +++ b/petsctools/options.py @@ -103,16 +103,16 @@ def _warn_unused_options(all_options: Iterable, used_options: Iterable, """ Raise warnings for PETSc options which were not used. - This is meant only as a weakref.finalize callback for the OptionsManager. + This is meant only as a weakref.finalize callback for the :class:`OptionsManager`. Parameters ---------- all_options : - The full set of options passed to the OptionsManager. + The full set of options passed to the :class:`OptionsManager`. used_options : - The options which were used during the OptionsManager's lifetime. + The options which were used during the :class:`OptionsManager`'s lifetime. options_prefix : - The options_prefix of the OptionsManager. + The options_prefix of the :class:`OptionsManager`. Raises ------ @@ -142,17 +142,17 @@ class DefaultOptionSet: Defines a set of common default options shared by multiple PETSc objects. Some solvers, e.g. PCFieldsplit, create multiple subsolvers whose prefixes - differ only by the final characters, e.g. 'fieldsplit_0', 'fieldsplit_1'. - It is often useful to be able to set default options for these subsolvers - using the un-specialised prefix e.g. 'fieldsplit_ksp_type'. However, just - grabbing all options with the 'fieldsplit' prefix will erroneously find - options like '0_ksp_type' and '1_ksp_type' that were meant for a specific - subsolver. - - DefaultOptionSet defines a base prefix (e.g. 'fieldsplit') and a set of - custom prefix endings (e.g. [0, 1]). If passed to an ``OptionsManager`` - then any default options present in the global options database will be - used if those options + differ only by the final characters, e.g. ``'fieldsplit_0'``, + ``'fieldsplit_1'``. It is often useful to be able to set default options + for these subsolvers using the un-specialised prefix e.g. + ``'fieldsplit_ksp_type'``. However, just grabbing all options with the + ``'fieldsplit'`` prefix will erroneously find options like ``'0_ksp_type'`` + and ``'1_ksp_type'`` that were meant for a specific subsolver. + + ``DefaultOptionSet`` defines a base prefix (e.g. ``'fieldsplit'``) and a + set of custom prefix endings (e.g. ``[0, 1]``). If passed to an + :class:`OptionsManager` then any default options present in the global + options database will be used if those options For example, to set up a fieldsplit solver you might have the following options, where both fields are to use ILU as the preconditioner but each @@ -164,7 +164,7 @@ class DefaultOptionSet: -fieldsplit_0_ksp_type preonly -fieldsplit_1_ksp_type richardson - To create an OptionsManager for each field you would call: + To create an :class:`OptionsManager` for each field you would call: .. code-block:: python3 @@ -182,7 +182,7 @@ class DefaultOptionSet: options_prefix="fieldsplit_1", default_options_set=default_options_set) - Attributes + Parameters ---------- base_prefix : The prefix for the default options, which is the beginning of @@ -215,14 +215,17 @@ def __init__(self, base_prefix: str, custom_prefix_endings: Iterable): @property def base_prefix(self): + """The prefix for the default options.""" return self._base_prefix @property def custom_prefix_endings(self): + """The ends of each individual custom prefix.""" return self._custom_prefix_endings @cached_property def custom_prefixes(self): + """The full custom prefixes.""" return tuple(self.base_prefix + ending for ending in self.custom_prefix_endings) @@ -234,10 +237,10 @@ def get_default_options(default_options_set: DefaultOptionSet, Parameters ---------- - default_options_set : - The DefaultOptionSet which defines the shared options. - options : - The PETSc.Options database to use. If not provided then the global + default_options_set + The :class:`DefaultOptionSet` which defines the shared options. + options + The ``PETSc.Options`` database to use. If not provided then the global database will be used. Returns @@ -273,9 +276,9 @@ class OptionsManager: """Class that helps with managing setting PETSc options. The recommended way to use the ``OptionsManager`` is by using the - ``attach_options``, ``set_from_options``, and ``inserted_options`` - free functions. These functions ensure that each ``OptionsManager`` - is associated to a single PETSc object. + :func:`attach_options`, :func:`set_from_options`, and + :func:`inserted_options` free functions. These functions ensure that + each ``OptionsManager`` is associated to a single PETSc object. For detail on the previous approach of using ``OptionsManager`` as a mixin class (where the user takes responsibility for ensuring @@ -284,15 +287,15 @@ class OptionsManager: To use the ``OptionsManager``: 1. Pass a PETSc object a parameters dictionary, and optionally - an options prefix, to ``attach_options``. This will create an - ``OptionsManager`` and set the prefix of the PETSc object, + an options prefix, to :func:`attach_options`. This will create + an ``OptionsManager`` and set the prefix of the PETSc object, but will not yet set it up. - 2. Once the object is ready, pass it to ``set_from_options``, + 2. Once the object is ready, pass it to :func:`set_from_options`, which will insert the solver options into ``PETSc.Options`` and call ``obj.setFromOptions``. - 3. The ``inserted_options`` context manager must be used when - calling methods on the PETSc object within which solver - options will be read, for example ``solve``. + 3. The :func:`inserted_options` context manager must be used when + calling methods on the PETSc object within which solver options + will be read, for example ``solve``. This will insert the provided ``parameters`` into PETSc's global options dictionary within the context manager, and remove them afterwards. This ensures that the global options @@ -316,8 +319,8 @@ class OptionsManager: with inserted_options(ksp): ksp.solve(b, x) - To access the OptionsManager for a PETSc object directly, use - the ``get_options`` function: + To access the ``OptionsManager`` for a PETSc object directly + use the :func:`get_options` function: .. code-block:: python3 @@ -337,7 +340,7 @@ class OptionsManager: 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 `OptionsManager.inserted_options` context manager. + Do that using the :meth:`OptionsManager.inserted_options` context manager. If using as a mixin class, call the ``OptionsManager`` methods directly: @@ -554,12 +557,12 @@ def attach_options( default_prefix: str | None = None, default_options_set: DefaultOptionSet | None = None ) -> None: - """Set up an OptionsManager and attach it to a PETSc Object. + """Set up an :class:`OptionsManager` and attach it to a PETSc Object. Parameters ---------- obj - The object to attach an OptionsManager to. + The object to attach an :class:`OptionsManager` to. parameters The dictionary of parameters to use. options_prefix @@ -591,20 +594,22 @@ def attach_options( def has_options(obj: petsc4py.PETSc.Object) -> bool: - """Return whether this PETSc object has an OptionsManager attached. + """Return whether this PETSc object has an :class:`OptionsManager` attached. Parameters ---------- obj - The object which may have an OptionsManager. + The object which may have an :class:`OptionsManager`. Returns ------- - Whether the object has an OptionsManager. + Whether the object has an :class:`OptionsManager`. See Also -------- OptionsManager + attach_options + set_from_options """ return "options" in obj.getDict() and isinstance( obj.getAttr("options"), OptionsManager @@ -612,25 +617,27 @@ def has_options(obj: petsc4py.PETSc.Object) -> bool: def get_options(obj: petsc4py.PETSc.Object) -> OptionsManager: - """Return the OptionsManager attached to this PETSc object. + """Return the :class:`OptionsManager` attached to this PETSc object. Parameters ---------- obj - The object to get the OptionsManager from. + The object to get the :class:`OptionsManager` from. Returns ------- - The OptionsManager attached to the object. + The :class:`OptionsManager` attached to the object. Raises ------ PetscToolsException - If the object does not have an OptionsManager. + If the object does not have an :class:`OptionsManager`. See Also -------- OptionsManager + attach_options + set_from_options """ if not has_options(obj): raise PetscToolsException( @@ -642,12 +649,12 @@ def get_options(obj: petsc4py.PETSc.Object) -> OptionsManager: def set_default_parameter( obj: petsc4py.PETSc.Object, key: str, val: Any ) -> None: - """Set a default parameter value in the OptionsManager of a PETSc object. + """Set a default parameter value in the :class:`OptionsManager` of a PETSc object. Parameters ---------- obj - The object to get the OptionsManager from. + The object to get the :class:`OptionsManager` from. key The options parameter name val @@ -656,12 +663,14 @@ def set_default_parameter( Raises ------ PetscToolsException - If the object does not have an OptionsManager. + If the object does not have an :class:`OptionsManager`. See Also -------- OptionsManager OptionsManager.set_default_parameter + attach_options + set_from_options """ get_options(obj).set_default_parameter(key, val) @@ -673,7 +682,7 @@ def set_from_options( default_prefix: str | None = None, default_options_set: DefaultOptionSet | None = None ) -> None: - """Set up a PETSc object from the options in its OptionsManager. + """Set up a PETSc object from the options in its :class:`OptionsManager`. Calls ``obj.setOptionsPrefix`` and ``obj.setFromOptions`` whilst inside the ``inserted_options`` context manager, which ensures @@ -702,10 +711,10 @@ def set_from_options( ------ PetscToolsException If the neither ``parameters`` nor ``options_prefix`` are - provided but ``obj`` does not have an OptionsManager attached. + provided but ``obj`` does not have an :class:`OptionsManager` attached. PetscToolsException If the either ``parameters`` or ``options_prefix`` are provided - but ``obj`` already has an OptionsManager attached. + but ``obj`` already has an :class:`OptionsManager` attached. PetscToolsWarning If set_from_options has already been called for this object. @@ -748,7 +757,7 @@ def set_from_options( def is_set_from_options(obj: petsc4py.PETSc.Object) -> bool: """ - Return whether this PETSc object has been set by the OptionsManager. + Return whether this PETSc object has been set by the :class:`OptionsManager`. Parameters ---------- @@ -762,11 +771,13 @@ def is_set_from_options(obj: petsc4py.PETSc.Object) -> bool: Raises ------ PetscToolsException - If the object does not have an OptionsManager. + If the object does not have an :class:`OptionsManager`. See Also -------- OptionsManager + attach_options + set_from_options """ return get_options(obj)._setfromoptions @@ -774,7 +785,7 @@ def is_set_from_options(obj: petsc4py.PETSc.Object) -> bool: @contextlib.contextmanager def inserted_options(obj): """Context manager inside which the PETSc options database - contains the parameters from this object's OptionsManager. + contains the parameters from this object's :class:`OptionsManager`. Parameters ---------- @@ -784,12 +795,14 @@ def inserted_options(obj): Raises ------ PetscToolsException - If the object does not have an OptionsManager. + If the object does not have an :class:`OptionsManager`. See Also -------- OptionsManager OptionsManager.inserted_options + attach_options + set_from_options """ with get_options(obj).inserted_options(): yield From 3baf2e4b863b6994022be50da0326184dcc75c16 Mon Sep 17 00:00:00 2001 From: Josh Hope-Collins Date: Thu, 20 Nov 2025 14:15:30 +0000 Subject: [PATCH 6/7] add in end of accidentally deleted sentence. --- petsctools/options.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/petsctools/options.py b/petsctools/options.py index 3d71bda..6b53c99 100644 --- a/petsctools/options.py +++ b/petsctools/options.py @@ -103,14 +103,16 @@ def _warn_unused_options(all_options: Iterable, used_options: Iterable, """ Raise warnings for PETSc options which were not used. - This is meant only as a weakref.finalize callback for the :class:`OptionsManager`. + This is meant only as a weakref.finalize callback for the + :class:`OptionsManager`. Parameters ---------- all_options : The full set of options passed to the :class:`OptionsManager`. used_options : - The options which were used during the :class:`OptionsManager`'s lifetime. + The options which were used during the :class:`OptionsManager`'s + lifetime. options_prefix : The options_prefix of the :class:`OptionsManager`. @@ -152,7 +154,10 @@ class DefaultOptionSet: ``DefaultOptionSet`` defines a base prefix (e.g. ``'fieldsplit'``) and a set of custom prefix endings (e.g. ``[0, 1]``). If passed to an :class:`OptionsManager` then any default options present in the global - options database will be used if those options + options database will be used if those options are not present either: + in the ``parameters`` passed to the :class:`OptionsManager`; or in the + global ``PETSc.Options`` database with the ``options_prefix`` passed to + the :class:`OptionsManager`. For example, to set up a fieldsplit solver you might have the following options, where both fields are to use ILU as the preconditioner but each @@ -594,7 +599,8 @@ def attach_options( def has_options(obj: petsc4py.PETSc.Object) -> bool: - """Return whether this PETSc object has an :class:`OptionsManager` attached. + """Return whether this PETSc object has an :class:`OptionsManager` + attached. Parameters ---------- @@ -649,7 +655,8 @@ def get_options(obj: petsc4py.PETSc.Object) -> OptionsManager: def set_default_parameter( obj: petsc4py.PETSc.Object, key: str, val: Any ) -> None: - """Set a default parameter value in the :class:`OptionsManager` of a PETSc object. + """Set a default parameter value in the :class:`OptionsManager` of a + PETSc object. Parameters ---------- @@ -757,7 +764,8 @@ def set_from_options( def is_set_from_options(obj: petsc4py.PETSc.Object) -> bool: """ - Return whether this PETSc object has been set by the :class:`OptionsManager`. + Return whether this PETSc object has been set by the + :class:`OptionsManager`. Parameters ---------- From 745f0e231b61740b46501ad1cccf465c0f57efff Mon Sep 17 00:00:00 2001 From: Josh Hope-Collins Date: Thu, 20 Nov 2025 14:34:30 +0000 Subject: [PATCH 7/7] Apply suggestions from code review Co-authored-by: Connor Ward --- petsctools/options.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/petsctools/options.py b/petsctools/options.py index 6b53c99..413c2bd 100644 --- a/petsctools/options.py +++ b/petsctools/options.py @@ -385,7 +385,7 @@ class OptionsManager: command line with a matching prefix will be ignored. default_options_set The prefix set for any default shared with other solvers. - See ``DefaultOptionSet`` for more information. + See :class:`DefaultOptionSet` for more information. See Also -------- @@ -451,7 +451,8 @@ def __init__(self, parameters: dict, # Update using the parameters passed in the code but # exclude those options from the dict that were passed - # on the commandline. + # on the commandline because those have global scope and are + # not under the control of the options manager. self.parameters.update({ k: v for k, v in parameters.items()