From 8d680b13cd4bae8ebd9741d1c0be45017beb54cf Mon Sep 17 00:00:00 2001 From: Josh Hope-Collins Date: Thu, 17 Jul 2025 20:51:53 +0100 Subject: [PATCH 01/24] appctx --- petsctools/appctx.py | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 petsctools/appctx.py diff --git a/petsctools/appctx.py b/petsctools/appctx.py new file mode 100644 index 0000000..639ebb7 --- /dev/null +++ b/petsctools/appctx.py @@ -0,0 +1,28 @@ +import itertools + + +class Appctx: + def __init__(self): + self._count = itertools.count() + self._data = {} + self._missing_key = next(self._count) + + @property + def missing_key(self): + return self._missing_key + + def insert(self, val): + key = next(self._count) + self._data[key] = val + return key + + def __getitem__(self, key): + return self._data[key] + + def get(self, key, default=None): + if key == self.missing_key: + return default + return self._data.get(key, default=default) + + def values(self): + return self._data.values() From 54cd6db223ceea6c51bb52b1580d1077b32c3ba5 Mon Sep 17 00:00:00 2001 From: Josh Hope-Collins Date: Mon, 18 Aug 2025 17:34:46 +0100 Subject: [PATCH 02/24] AppContext in __init__ --- petsctools/__init__.py | 1 + petsctools/appctx.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/petsctools/__init__.py b/petsctools/__init__.py index 03b3682..a783ada 100644 --- a/petsctools/__init__.py +++ b/petsctools/__init__.py @@ -10,6 +10,7 @@ from .exceptions import PetscToolsException # noqa: F401 from .options import flatten_parameters # noqa: F401 from .utils import PETSC4PY_INSTALLED +from .appctx import AppContext # Now conditionally import the functions that depend on petsc4py. If petsc4py # is not available then attempting to access these attributes will raise an diff --git a/petsctools/appctx.py b/petsctools/appctx.py index 639ebb7..f0aa647 100644 --- a/petsctools/appctx.py +++ b/petsctools/appctx.py @@ -1,7 +1,7 @@ import itertools -class Appctx: +class AppContext: def __init__(self): self._count = itertools.count() self._data = {} From c7e366ad4642582a976f81b379a79b9c50533966 Mon Sep 17 00:00:00 2001 From: Josh Hope-Collins Date: Wed, 27 Aug 2025 15:56:51 +0100 Subject: [PATCH 03/24] Hide AppContext internal keys from user --- petsctools/__init__.py | 2 +- petsctools/appctx.py | 74 +++++++++++++++++++++++++++++++++++----- petsctools/exceptions.py | 4 +++ tests/test_options.py | 24 +++++++++++++ 4 files changed, 94 insertions(+), 10 deletions(-) create mode 100644 tests/test_options.py diff --git a/petsctools/__init__.py b/petsctools/__init__.py index a783ada..6b0569f 100644 --- a/petsctools/__init__.py +++ b/petsctools/__init__.py @@ -1,3 +1,4 @@ +from .appctx import AppContext # noqa: F401 from .config import ( # noqa: F401 MissingPetscException, get_config, @@ -10,7 +11,6 @@ from .exceptions import PetscToolsException # noqa: F401 from .options import flatten_parameters # noqa: F401 from .utils import PETSC4PY_INSTALLED -from .appctx import AppContext # Now conditionally import the functions that depend on petsc4py. If petsc4py # is not available then attempting to access these attributes will raise an diff --git a/petsctools/appctx.py b/petsctools/appctx.py index f0aa647..427b46f 100644 --- a/petsctools/appctx.py +++ b/petsctools/appctx.py @@ -1,28 +1,84 @@ import itertools +from functools import cached_property +from petsctools.exceptions import PetscToolsAppctxException + + +class _AppContextKey(int): + pass class AppContext: + """ + + .. code-block:: python3 + + appctx = AppContext() + some_data = MyCustomObject() + + opts = OptionsManager( + parameters={ + 'pc_type': 'python', + 'pc_python_type': 'MyCustomPC', + 'custompc_somedata': appctx.add(some_data)}, + options_prefix="") + + with opts.inserted_options(): + data = appctx.get('custompc_somedata') + + """ + def __init__(self): self._count = itertools.count() self._data = {} - self._missing_key = next(self._count) + self._missing_key = self._keygen() + + def _keygen(self, key=None): + return _AppContextKey(next(self._count) if key is None else key) + + def _option_to_key(self, option): + key = self.options_object.getInt(option, self.missing_key) + print(f"_option_to_key: {key = }") + return self._keygen(key) @property def missing_key(self): + """ + Key instance representing a missing AppContext entry. + """ + # PETSc requires the default value for Options.getObj() + # to be the correct type, so we need a dummy key. return self._missing_key - def insert(self, val): - key = next(self._count) + def add(self, val): + """ + Add a value to the application context and + return the autogenerated key for that value. + + The autogenerated key should the value for the + corresponding entry in the solver_parameters dictionary. + """ + key = self._keygen() self._data[key] = val + print(f"add: {key = }") return key - def __getitem__(self, key): - return self._data[key] + def __getitem__(self, option): + """ + Return the value with the key saved in `PETSc.Options()[option]`. + """ + try: + return self._data[self._option_to_key(option)] + except KeyError: + raise PetscToolsAppctxException( + f"AppContext does not have an entry for {option}") - def get(self, key, default=None): + def get(self, option, default=None): + key = self._option_to_key(option) if key == self.missing_key: return default - return self._data.get(key, default=default) + return self._data[key] - def values(self): - return self._data.values() + @cached_property + def options_object(self): + from petsc4py import PETSc + return PETSc.Options() diff --git a/petsctools/exceptions.py b/petsctools/exceptions.py index 70b0926..5d35e4c 100644 --- a/petsctools/exceptions.py +++ b/petsctools/exceptions.py @@ -6,5 +6,9 @@ class PetscToolsNotInitialisedException(PetscToolsException): """Exception raised when petsctools should have been initialised.""" +class PetscToolsAppctxException(PetscToolsException): + """Exception raised when the Appctx is missing an entry.""" + + class PetscToolsWarning(UserWarning): """Generic base class for petsctools warnings.""" diff --git a/tests/test_options.py b/tests/test_options.py new file mode 100644 index 0000000..3816540 --- /dev/null +++ b/tests/test_options.py @@ -0,0 +1,24 @@ +import petsctools + +PETSc = petsctools.init() + +appctx = petsctools.AppContext() +reynolds = 10 + +opts = petsctools.OptionsManager( + parameters={ + 'fieldsplit_1_pc_type': 'python', + 'fieldsplit_1_pc_python_type': 'firedrake.MassInvPC', + 'fieldsplit_1_mass_reynolds': appctx.add(reynolds)}, + options_prefix="") + +with opts.inserted_options(): + re = appctx.get('fieldsplit_1_mass_reynolds') + print(f"{re = }") + re = appctx['fieldsplit_1_mass_reynolds'] + print(f"{re = }") + + re = appctx.get('fieldsplit_0_mass_reynolds', 20) + print(f"{re = }") + re = appctx['fieldsplit_0_mass_reynolds'] + print(f"{re = }") From 913eb56186ca60de9f9f8ed278bfa065a9080034 Mon Sep 17 00:00:00 2001 From: Josh Hope-Collins Date: Wed, 27 Aug 2025 16:41:13 +0100 Subject: [PATCH 04/24] updates --- petsctools/appctx.py | 32 +++++++++++++++++++++----------- 1 file changed, 21 insertions(+), 11 deletions(-) diff --git a/petsctools/appctx.py b/petsctools/appctx.py index 427b46f..f2ef50e 100644 --- a/petsctools/appctx.py +++ b/petsctools/appctx.py @@ -4,6 +4,7 @@ class _AppContextKey(int): + """A custom key type for AppContext.""" pass @@ -30,36 +31,40 @@ class AppContext: def __init__(self): self._count = itertools.count() self._data = {} - self._missing_key = self._keygen() def _keygen(self, key=None): + """ + Generate a new internal key, optionally with a given value. + """ return _AppContextKey(next(self._count) if key is None else key) def _option_to_key(self, option): - key = self.options_object.getInt(option, self.missing_key) - print(f"_option_to_key: {key = }") + """ + Return the internal key for the parameter `option`. + """ + key = self.options_object.getInt(option, self._missing_key) return self._keygen(key) - @property - def missing_key(self): + @cached_property + def _missing_key(self): """ Key instance representing a missing AppContext entry. + + PETSc requires the default value for Options.getObj() + to be the correct type, so we need a dummy key. """ - # PETSc requires the default value for Options.getObj() - # to be the correct type, so we need a dummy key. - return self._missing_key + return self._keygen() def add(self, val): """ Add a value to the application context and return the autogenerated key for that value. - The autogenerated key should the value for the + The autogenerated key should be used as the value for the corresponding entry in the solver_parameters dictionary. """ key = self._keygen() self._data[key] = val - print(f"add: {key = }") return key def __getitem__(self, option): @@ -73,12 +78,17 @@ def __getitem__(self, option): f"AppContext does not have an entry for {option}") def get(self, option, default=None): + """ + Return the value with the key saved in PETSc.Options()[option], + or if it does not exist return default. + """ key = self._option_to_key(option) - if key == self.missing_key: + if key == self._missing_key: return default return self._data[key] @cached_property def options_object(self): + """A PETSc.Options instance.""" from petsc4py import PETSc return PETSc.Options() From cfe9954c3b27595a5e2c4cdc27c21d4da470e414 Mon Sep 17 00:00:00 2001 From: Josh Hope-Collins Date: Thu, 28 Aug 2025 15:27:22 +0100 Subject: [PATCH 05/24] appctx docstrings --- petsctools/appctx.py | 108 ++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 101 insertions(+), 7 deletions(-) diff --git a/petsctools/appctx.py b/petsctools/appctx.py index f2ef50e..75c1d2a 100644 --- a/petsctools/appctx.py +++ b/petsctools/appctx.py @@ -3,29 +3,57 @@ from petsctools.exceptions import PetscToolsAppctxException -class _AppContextKey(int): +class AppContextKey(int): """A custom key type for AppContext.""" pass class AppContext: """ + Class for passing non-primitive types to PETSc python contexts. + + The PETSc.Options dictionary can only contain primitive types (str, + int, float, bool) as values. The AppContext allows other types to be + passed into PETSc solvers while still making use of the namespacing + provided by options prefixing. + + A typical usage is shown below. In this example we have a python PC + type `MyCustomPC` which requires additional data in the form of a + `MyCustomData` instance. + We can add the data to the AppContext with the `appctx.add` method, + but we need to tell `MyCustomPC` how to retrieve that data. The + `add` method returns a key which is a valid PETSc.Options entry, + i.e. a primitive type instance. This key is passed via PETSc.Options + with the 'custompc_somedata' prefix. + + The data can be retrieved in two ways. + 1) Giving the AppContext the (fully prefixed) option for the key, + in which case the AppContext will internally fetch the key from + the PETSc.Options and return the data. + 2) By manually fetching the AppContext key from the PETSc.Options, + then retrieving the data from the `AppContext` using that key. .. code-block:: python3 appctx = AppContext() - some_data = MyCustomObject() + some_data = MyCustomData(5) opts = OptionsManager( parameters={ 'pc_type': 'python', 'pc_python_type': 'MyCustomPC', 'custompc_somedata': appctx.add(some_data)}, - options_prefix="") + options_prefix='solver') with opts.inserted_options(): - data = appctx.get('custompc_somedata') - + # 1) Let AppContext fetch key. + # Also shows providing default data. + default = MyCustomData(10) + data = appctx.get('solver_custompc_somedata', default) + + # 2) Fetch key directly. + key = PETSc.Options()['solver_custompc_somedata'] + data = appctx[key] """ def __init__(self): @@ -35,13 +63,40 @@ def __init__(self): def _keygen(self, key=None): """ Generate a new internal key, optionally with a given value. + + This should not called directly by the user. + + Parameters + ---------- + key : Optional[int] + The value of the key. + + Returns + ------- + new_key : AppContextKey + A new internal key. """ - return _AppContextKey(next(self._count) if key is None else key) + return AppContextKey(next(self._count) if key is None else key) def _option_to_key(self, option): """ - Return the internal key for the parameter `option`. + Return the internal key for the PETSc option `option`. + If `option` is already an AppContextKey, `option` is returned. + + This should not called directly by the user. + + Parameters + ---------- + option : Union[str, AppContextKey] + The PETSc option. + + Returns + ------- + key : AppContextKey + An internal key corresponding to `option`. """ + if isinstance(option, int): + return AppContextKey(option) key = self.options_object.getInt(option, self._missing_key) return self._keygen(key) @@ -50,6 +105,8 @@ def _missing_key(self): """ Key instance representing a missing AppContext entry. + This should not called directly by the user. + PETSc requires the default value for Options.getObj() to be the correct type, so we need a dummy key. """ @@ -62,6 +119,16 @@ def add(self, val): The autogenerated key should be used as the value for the corresponding entry in the solver_parameters dictionary. + + Parameters + ---------- + val : Any + The value to add to the AppContext. + + Returns + ------- + key : AppContextKey + The key to put into the PETSc Options dictionary. """ key = self._keygen() self._data[key] = val @@ -70,6 +137,21 @@ def add(self, val): def __getitem__(self, option): """ Return the value with the key saved in `PETSc.Options()[option]`. + + Parameters + ---------- + option : Union[str, AppContextKey] + The PETSc option or key. + + Returns + ------- + val : Any + The value for the key `option`. + + Raises + ------ + PetscToolsAppctxException + If the AppContext does contain a value for `option`. """ try: return self._data[self._option_to_key(option)] @@ -81,6 +163,18 @@ def get(self, option, default=None): """ Return the value with the key saved in PETSc.Options()[option], or if it does not exist return default. + + Parameters + ---------- + option : Union[str, AppContextKey] + The PETSc option or key. + default : Any + The value to return if `option` is not in the AppContext + + Returns + ------- + val : Any + The value for the key `option`, or `default`. """ key = self._option_to_key(option) if key == self._missing_key: From ec9c9009a13a5cf103c86329c0e9cf08a94c5fa2 Mon Sep 17 00:00:00 2001 From: Josh Hope-Collins Date: Thu, 28 Aug 2025 15:37:53 +0100 Subject: [PATCH 06/24] test appctx --- tests/test_appctx.py | 37 +++++++++++++++++++++++++++++++++++++ tests/test_options.py | 24 ------------------------ 2 files changed, 37 insertions(+), 24 deletions(-) create mode 100644 tests/test_appctx.py delete mode 100644 tests/test_options.py diff --git a/tests/test_appctx.py b/tests/test_appctx.py new file mode 100644 index 0000000..67cec57 --- /dev/null +++ b/tests/test_appctx.py @@ -0,0 +1,37 @@ +import pytest +import petsctools + + +@pytest.mark.skipnopetsc4py +def test_appctx(): + PETSc = petsctools.init() + + appctx = petsctools.AppContext() + + param = 10 + key = appctx.add(param) + PETSc.Options()['solver_param'] = key + + # Can we access param via the prefixed option? + prm = appctx.get('solver_param') + assert prm is param + + prm = appctx['solver_param'] + assert prm is param + + # Can we access param via the key? + prm = appctx.get(key, 20) + assert prm is param + + prm = appctx[key] + assert prm is param + + # Can we set a default value? + default = 20 + prm = appctx.get('param', default) + assert prm is default + + # Will an invalid key raise an error + from petsctools.appctx import PetscToolsAppctxException + with pytest.raises(PetscToolsAppctxException): + appctx['param'] diff --git a/tests/test_options.py b/tests/test_options.py deleted file mode 100644 index 3816540..0000000 --- a/tests/test_options.py +++ /dev/null @@ -1,24 +0,0 @@ -import petsctools - -PETSc = petsctools.init() - -appctx = petsctools.AppContext() -reynolds = 10 - -opts = petsctools.OptionsManager( - parameters={ - 'fieldsplit_1_pc_type': 'python', - 'fieldsplit_1_pc_python_type': 'firedrake.MassInvPC', - 'fieldsplit_1_mass_reynolds': appctx.add(reynolds)}, - options_prefix="") - -with opts.inserted_options(): - re = appctx.get('fieldsplit_1_mass_reynolds') - print(f"{re = }") - re = appctx['fieldsplit_1_mass_reynolds'] - print(f"{re = }") - - re = appctx.get('fieldsplit_0_mass_reynolds', 20) - print(f"{re = }") - re = appctx['fieldsplit_0_mass_reynolds'] - print(f"{re = }") From 0a4ba8f74b01f55c28466373b05fef5be7eaddac Mon Sep 17 00:00:00 2001 From: Josh Hope-Collins Date: Thu, 28 Aug 2025 17:02:02 +0100 Subject: [PATCH 07/24] tidy up appctx keygen --- petsctools/appctx.py | 57 +++++++++++++++++++++----------------------- tests/test_appctx.py | 6 ++++- 2 files changed, 32 insertions(+), 31 deletions(-) diff --git a/petsctools/appctx.py b/petsctools/appctx.py index 75c1d2a..d5824c6 100644 --- a/petsctools/appctx.py +++ b/petsctools/appctx.py @@ -60,58 +60,55 @@ def __init__(self): self._count = itertools.count() self._data = {} - def _keygen(self, key=None): + def _keygen(self): """ - Generate a new internal key, optionally with a given value. + Generate a new unique internal key. This should not called directly by the user. - - Parameters - ---------- - key : Optional[int] - The value of the key. - - Returns - ------- - new_key : AppContextKey - A new internal key. """ - return AppContextKey(next(self._count) if key is None else key) + return AppContextKey(next(self._count)) - def _option_to_key(self, option): + def _to_key(self, option): """ Return the internal key for the PETSc option `option`. If `option` is already an AppContextKey, `option` is returned. This should not called directly by the user. - - Parameters - ---------- - option : Union[str, AppContextKey] - The PETSc option. - - Returns - ------- - key : AppContextKey - An internal key corresponding to `option`. """ if isinstance(option, int): return AppContextKey(option) - key = self.options_object.getInt(option, self._missing_key) - return self._keygen(key) + else: + return self.getKey(option) @cached_property def _missing_key(self): """ Key instance representing a missing AppContext entry. - This should not called directly by the user. - PETSc requires the default value for Options.getObj() to be the correct type, so we need a dummy key. + + This should not called directly by the user. """ return self._keygen() + def getKey(self, option): + """ + Return the internal key for the PETSc option `option`. + + Parameters + ---------- + option : str + The PETSc option. + + Returns + ------- + key : AppContextKey + An internal key corresponding to `option`. + """ + key = self.options_object.getInt(option, self._missing_key) + return AppContextKey(key) + def add(self, val): """ Add a value to the application context and @@ -154,7 +151,7 @@ def __getitem__(self, option): If the AppContext does contain a value for `option`. """ try: - return self._data[self._option_to_key(option)] + return self._data[self._to_key(option)] except KeyError: raise PetscToolsAppctxException( f"AppContext does not have an entry for {option}") @@ -176,7 +173,7 @@ def get(self, option, default=None): val : Any The value for the key `option`, or `default`. """ - key = self._option_to_key(option) + key = self._to_key(option) if key == self._missing_key: return default return self._data[key] diff --git a/tests/test_appctx.py b/tests/test_appctx.py index 67cec57..3706f82 100644 --- a/tests/test_appctx.py +++ b/tests/test_appctx.py @@ -10,7 +10,11 @@ def test_appctx(): param = 10 key = appctx.add(param) - PETSc.Options()['solver_param'] = key + options = PETSc.Options() + options['solver_param'] = key + + # Can we get the key string back? + assert str(appctx.getKey('solver_param')) == options['solver_param'] # Can we access param via the prefixed option? prm = appctx.get('solver_param') From 8104028d7427ef93100dfe43d6b6270d4f6aea6d Mon Sep 17 00:00:00 2001 From: Josh Hope-Collins Date: Wed, 3 Sep 2025 15:49:11 +0100 Subject: [PATCH 08/24] review comments - hide key from user completely --- petsctools/appctx.py | 55 ++++++++++---------------------------------- tests/test_appctx.py | 10 +------- 2 files changed, 13 insertions(+), 52 deletions(-) diff --git a/petsctools/appctx.py b/petsctools/appctx.py index d5824c6..6f86640 100644 --- a/petsctools/appctx.py +++ b/petsctools/appctx.py @@ -26,12 +26,12 @@ class AppContext: i.e. a primitive type instance. This key is passed via PETSc.Options with the 'custompc_somedata' prefix. - The data can be retrieved in two ways. - 1) Giving the AppContext the (fully prefixed) option for the key, - in which case the AppContext will internally fetch the key from - the PETSc.Options and return the data. - 2) By manually fetching the AppContext key from the PETSc.Options, - then retrieving the data from the `AppContext` using that key. + NB: The user should never handle this key directly, it should only + ever be placed directly into the options dictionary. + + The data can be retrieved by giving the AppContext the (fully + prefixed) option for the key, in which case the AppContext will + internally fetch the key from the PETSc.Options and return the data. .. code-block:: python3 @@ -46,18 +46,12 @@ class AppContext: options_prefix='solver') with opts.inserted_options(): - # 1) Let AppContext fetch key. - # Also shows providing default data. default = MyCustomData(10) data = appctx.get('solver_custompc_somedata', default) - - # 2) Fetch key directly. - key = PETSc.Options()['solver_custompc_somedata'] - data = appctx[key] """ def __init__(self): - self._count = itertools.count() + self._count = itertools.count(start=0) self._data = {} def _keygen(self): @@ -68,30 +62,6 @@ def _keygen(self): """ return AppContextKey(next(self._count)) - def _to_key(self, option): - """ - Return the internal key for the PETSc option `option`. - If `option` is already an AppContextKey, `option` is returned. - - This should not called directly by the user. - """ - if isinstance(option, int): - return AppContextKey(option) - else: - return self.getKey(option) - - @cached_property - def _missing_key(self): - """ - Key instance representing a missing AppContext entry. - - PETSc requires the default value for Options.getObj() - to be the correct type, so we need a dummy key. - - This should not called directly by the user. - """ - return self._keygen() - def getKey(self, option): """ Return the internal key for the PETSc option `option`. @@ -106,8 +76,7 @@ def getKey(self, option): key : AppContextKey An internal key corresponding to `option`. """ - key = self.options_object.getInt(option, self._missing_key) - return AppContextKey(key) + return AppContextKey(self.options_object.getInt(option)) def add(self, val): """ @@ -151,7 +120,7 @@ def __getitem__(self, option): If the AppContext does contain a value for `option`. """ try: - return self._data[self._to_key(option)] + return self._data[self.getKey(option)] except KeyError: raise PetscToolsAppctxException( f"AppContext does not have an entry for {option}") @@ -173,10 +142,10 @@ def get(self, option, default=None): val : Any The value for the key `option`, or `default`. """ - key = self._to_key(option) - if key == self._missing_key: + try: + return self[option] + except PetscToolsAppctxException: return default - return self._data[key] @cached_property def options_object(self): diff --git a/tests/test_appctx.py b/tests/test_appctx.py index 3706f82..05ce43f 100644 --- a/tests/test_appctx.py +++ b/tests/test_appctx.py @@ -9,9 +9,8 @@ def test_appctx(): appctx = petsctools.AppContext() param = 10 - key = appctx.add(param) options = PETSc.Options() - options['solver_param'] = key + options['solver_param'] = appctx.add(param) # Can we get the key string back? assert str(appctx.getKey('solver_param')) == options['solver_param'] @@ -23,13 +22,6 @@ def test_appctx(): prm = appctx['solver_param'] assert prm is param - # Can we access param via the key? - prm = appctx.get(key, 20) - assert prm is param - - prm = appctx[key] - assert prm is param - # Can we set a default value? default = 20 prm = appctx.get('param', default) From d231b367c15d50674e2679869a331fa67493e1d8 Mon Sep 17 00:00:00 2001 From: Josh Hope-Collins Date: Wed, 3 Sep 2025 15:52:11 +0100 Subject: [PATCH 09/24] move import to top of test file --- tests/test_appctx.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_appctx.py b/tests/test_appctx.py index 05ce43f..32a8a53 100644 --- a/tests/test_appctx.py +++ b/tests/test_appctx.py @@ -1,5 +1,6 @@ import pytest import petsctools +from petsctools.exceptions import PetscToolsAppctxException @pytest.mark.skipnopetsc4py @@ -28,6 +29,5 @@ def test_appctx(): assert prm is default # Will an invalid key raise an error - from petsctools.appctx import PetscToolsAppctxException with pytest.raises(PetscToolsAppctxException): appctx['param'] From 36587cc0ad3e73c2f263bad8e1ccfa3416363b6a Mon Sep 17 00:00:00 2001 From: Josh Hope-Collins Date: Thu, 4 Sep 2025 10:31:29 +0100 Subject: [PATCH 10/24] Update petsctools/appctx.py Co-authored-by: Connor Ward --- petsctools/appctx.py | 1 - 1 file changed, 1 deletion(-) diff --git a/petsctools/appctx.py b/petsctools/appctx.py index 6f86640..4059d4f 100644 --- a/petsctools/appctx.py +++ b/petsctools/appctx.py @@ -5,7 +5,6 @@ class AppContextKey(int): """A custom key type for AppContext.""" - pass class AppContext: From 3c1b6cfbfecf39297519421b59e5883a1c0c115d Mon Sep 17 00:00:00 2001 From: Josh Hope-Collins Date: Thu, 4 Sep 2025 10:34:16 +0100 Subject: [PATCH 11/24] appctx: hide key_from_option from user. --- petsctools/appctx.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/petsctools/appctx.py b/petsctools/appctx.py index 4059d4f..cf44c38 100644 --- a/petsctools/appctx.py +++ b/petsctools/appctx.py @@ -61,7 +61,7 @@ def _keygen(self): """ return AppContextKey(next(self._count)) - def getKey(self, option): + def _key_from_option(self, option): """ Return the internal key for the PETSc option `option`. @@ -119,7 +119,7 @@ def __getitem__(self, option): If the AppContext does contain a value for `option`. """ try: - return self._data[self.getKey(option)] + return self._data[self._key_from_option(option)] except KeyError: raise PetscToolsAppctxException( f"AppContext does not have an entry for {option}") From 5975b826dcf5b98f95efdc3a3a553d967660da0e Mon Sep 17 00:00:00 2001 From: Josh Hope-Collins Date: Wed, 10 Sep 2025 16:00:58 +0100 Subject: [PATCH 12/24] global appctx stack --- petsctools/__init__.py | 6 ++++- petsctools/appctx.py | 13 ++++++++++ petsctools/options.py | 10 ++++++-- tests/test_appctx.py | 57 +++++++++++++++++++++++++++++++++++++++--- 4 files changed, 79 insertions(+), 7 deletions(-) diff --git a/petsctools/__init__.py b/petsctools/__init__.py index 6b0569f..caf4f23 100644 --- a/petsctools/__init__.py +++ b/petsctools/__init__.py @@ -1,4 +1,8 @@ -from .appctx import AppContext # noqa: F401 +from .appctx import ( # noqa: F401 + AppContext, + push_appctx, + get_appctx, +) from .config import ( # noqa: F401 MissingPetscException, get_config, diff --git a/petsctools/appctx.py b/petsctools/appctx.py index cf44c38..e3f3ddc 100644 --- a/petsctools/appctx.py +++ b/petsctools/appctx.py @@ -1,7 +1,20 @@ import itertools from functools import cached_property +from contextlib import contextmanager from petsctools.exceptions import PetscToolsAppctxException +_global_appctx_stack = [] + +@contextmanager +def push_appctx(appctx): + _global_appctx_stack.append(appctx) + yield + _global_appctx_stack.pop() + + +def get_appctx(): + return _global_appctx_stack[-1] + class AppContextKey(int): """A custom key type for AppContext.""" diff --git a/petsctools/options.py b/petsctools/options.py index eaf972e..f955fd5 100644 --- a/petsctools/options.py +++ b/petsctools/options.py @@ -2,6 +2,7 @@ import functools import itertools import warnings +from petsctools.appctx import push_appctx from petsctools.exceptions import ( PetscToolsException, PetscToolsWarning, PetscToolsNotInitialisedException) @@ -408,7 +409,8 @@ def set_default_parameter(obj, key, val): def set_from_options(obj, parameters=None, - options_prefix=None): + options_prefix=None, + appctx=None): """Set up a PETSc object from the options in its OptionsManager. Calls ``obj.setOptionsPrefix`` and ``obj.setFromOptions`` whilst @@ -467,7 +469,11 @@ def set_from_options(obj, parameters=None, f" called for {petscobj2str(obj)}", PetscToolsWarning) - get_options(obj).set_from_options(obj) + if appctx is None: + get_options(obj).set_from_options(obj) + else: + with push_appctx(appctx): + get_options(obj).set_from_options(obj) def is_set_from_options(obj): diff --git a/tests/test_appctx.py b/tests/test_appctx.py index 32a8a53..130f361 100644 --- a/tests/test_appctx.py +++ b/tests/test_appctx.py @@ -1,10 +1,62 @@ +import numpy as np import pytest import petsctools from petsctools.exceptions import PetscToolsAppctxException +class JacobiTestPC: + prefix = "jacobi_" + def setFromOptions(self, pc): + appctx = petsctools.get_appctx() + prefix = (pc.getOptionsPrefix() or "") + self.prefix + self.scale = appctx[prefix + "scale"] + + def apply(self, pc, x, y): + y.pointwiseMult(x, self.scale) + + @pytest.mark.skipnopetsc4py -def test_appctx(): +def test_get_appctx(): + PETSc = petsctools.init() + n = 4 + sizes = (n, n) + + appctx = petsctools.AppContext() + + diag = PETSc.Vec().createSeq(sizes) + diag.setSizes((n, n)) + diag.array[:] = [1, 2, 3, 4] + + mat = PETSc.Mat().createConstantDiagonal((sizes, sizes), 1.0) + + ksp = PETSc.KSP().create() + ksp.setOperators(mat, mat) + petsctools.set_from_options( + ksp, + parameters={ + 'ksp_type': 'preonly', + 'pc_type': 'python', + 'pc_python_type': f'{__name__}.JacobiTestPC', + 'jacobi_scale': appctx.add(diag) + }, + options_prefix="myksp", + appctx=appctx, + ) + + x, b = mat.createVecs() + b.setRandom() + + xcheck = x.duplicate() + xcheck.pointwiseMult(b, diag) + + with petsctools.inserted_options(ksp), petsctools.push_appctx(appctx): + ksp.solve(b, x) + + assert np.allclose(x.array_r, xcheck.array_r) + + +@pytest.mark.skipnopetsc4py +def test_appctx_key(): PETSc = petsctools.init() appctx = petsctools.AppContext() @@ -13,9 +65,6 @@ def test_appctx(): options = PETSc.Options() options['solver_param'] = appctx.add(param) - # Can we get the key string back? - assert str(appctx.getKey('solver_param')) == options['solver_param'] - # Can we access param via the prefixed option? prm = appctx.get('solver_param') assert prm is param From d51cd2501aeeced32e4c6f103800825c802d5ee9 Mon Sep 17 00:00:00 2001 From: Josh Hope-Collins Date: Wed, 10 Sep 2025 18:36:39 +0100 Subject: [PATCH 13/24] numpy import inside test --- tests/test_appctx.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_appctx.py b/tests/test_appctx.py index 130f361..544d43b 100644 --- a/tests/test_appctx.py +++ b/tests/test_appctx.py @@ -1,4 +1,3 @@ -import numpy as np import pytest import petsctools from petsctools.exceptions import PetscToolsAppctxException @@ -17,6 +16,7 @@ def apply(self, pc, x, y): @pytest.mark.skipnopetsc4py def test_get_appctx(): + from numpy import allclose PETSc = petsctools.init() n = 4 sizes = (n, n) @@ -52,7 +52,7 @@ def test_get_appctx(): with petsctools.inserted_options(ksp), petsctools.push_appctx(appctx): ksp.solve(b, x) - assert np.allclose(x.array_r, xcheck.array_r) + assert allclose(x.array_r, xcheck.array_r) @pytest.mark.skipnopetsc4py From 3e7928bf1f0bcda456bdbba1a68860e56e4db785 Mon Sep 17 00:00:00 2001 From: Josh Hope-Collins Date: Wed, 22 Apr 2026 13:59:18 +0100 Subject: [PATCH 14/24] use petsc.vec.norm rather than numpy.allclose for test --- tests/test_appctx.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/test_appctx.py b/tests/test_appctx.py index 9bb5fa8..8dd7357 100644 --- a/tests/test_appctx.py +++ b/tests/test_appctx.py @@ -1,5 +1,4 @@ import pytest -from numpy import allclose import petsctools from petsctools.exceptions import PetscToolsAppctxException @@ -52,7 +51,7 @@ def test_get_appctx(): with petsctools.inserted_options(ksp), petsctools.push_appctx(appctx): ksp.solve(b, x) - assert allclose(x.array_r, xcheck.array_r) + assert (x - xcheck).norm() < 1e-14 @pytest.mark.skipnopetsc4py From 660475ac8ca5933f98cb9e8f50b2869113c75d37 Mon Sep 17 00:00:00 2001 From: Josh Hope-Collins Date: Tue, 28 Apr 2026 09:18:12 +0100 Subject: [PATCH 15/24] trailing whitespace --- tests/test_config.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_config.py b/tests/test_config.py index 708b38a..82aef55 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -24,7 +24,7 @@ def test_get_petsc_dirs(): petsc_dir = petsctools.get_petsc_dir() petsc_arch = petsctools.get_petsc_arch() - expected = (petsc_dir, f"{petsc_dir}/{petsc_arch}") + expected = (petsc_dir, f"{petsc_dir}/{petsc_arch}") assert petsctools.get_petsc_dirs() == expected expected = ( From e0424622f8aa4d47e90b141e91de938f9a681e62 Mon Sep 17 00:00:00 2001 From: Josh Hope-Collins Date: Tue, 28 Apr 2026 09:21:35 +0100 Subject: [PATCH 16/24] attach the appctx directly to the OptionsManager --- petsctools/appctx.py | 12 ++++++++- petsctools/options.py | 61 +++++++++++++++++++++++++++++++------------ tests/test_appctx.py | 3 ++- 3 files changed, 57 insertions(+), 19 deletions(-) diff --git a/petsctools/appctx.py b/petsctools/appctx.py index e3f3ddc..2db85ad 100644 --- a/petsctools/appctx.py +++ b/petsctools/appctx.py @@ -5,14 +5,24 @@ _global_appctx_stack = [] + @contextmanager -def push_appctx(appctx): +def push_appctx(appctx: "AppContext"): + """ + Context manager for pushing an :class:`AppContext` onto the global stack. + + The ``appctx`` will then be available via :func:`get_appctx`. + At the end of the context manager the ``appctx`` will be popped + from the global stack. + """ _global_appctx_stack.append(appctx) yield _global_appctx_stack.pop() def get_appctx(): + """Return the :class:`AppContext` at the top of the global stack. + """ return _global_appctx_stack[-1] diff --git a/petsctools/options.py b/petsctools/options.py index c351c5c..ac9750d 100644 --- a/petsctools/options.py +++ b/petsctools/options.py @@ -389,6 +389,8 @@ class OptionsManager: default_options_set The prefix set for any default shared with other solvers. See :class:`DefaultOptionSet` for more information. + appctx + The :class:`AppContext` containing user python data. See Also -------- @@ -399,6 +401,7 @@ class OptionsManager: is_set_from_options inserted_options DefaultOptionSet + AppContext """ count = itertools.count() @@ -406,7 +409,8 @@ class OptionsManager: def __init__(self, parameters: dict, options_prefix: str | None = None, default_prefix: str | None = None, - default_options_set: DefaultOptionSet | None = None): + default_options_set: DefaultOptionSet | None = None, + appctx: AppContext | None = None): super().__init__() if parameters is None: parameters = {} @@ -472,6 +476,10 @@ def __init__(self, parameters: dict, self.parameters[k[len(self.options_prefix):]] = v self._setfromoptions = False + + # user data + self.appctx = appctx + # Keep track of options used between invocations of inserted_options(). self._used_options = set() @@ -504,18 +512,29 @@ def set_default_parameter(self, key: str, val: Any) -> None: 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. + Before calling ``petsc_obj.setFromOptions``, the options from + this OptionsManager's ``parameters`` are inserted into the global + :class:`PETSc.Options`, and if this OptionsManager has an ``appctx`` + then it is pushed onto the global :class:`AppContext` stack. + + Parameters + ---------- + petsc_obj + The PETSc object to call setFromOptions on. - Raises PetscToolsWarning if this method has already been called. + Raises + ------ + PetscToolsWarning + If this method has already been called. - Matt says: "Only ever call setFromOptions once". This - function ensures we do so. """ + # Matt says: "Only ever call setFromOptions once". This + # function ensures we do so. if not self._setfromoptions: + # Call setfromoptions inserting appropriate options + # and user data into the global databases. 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 else: @@ -526,11 +545,18 @@ def set_from_options(self, petsc_obj): @contextlib.contextmanager def inserted_options(self): """Context manager inside which the petsc options database - contains the parameters from this object.""" + contains the parameters from this object. + If this OptionsManager has an ``appctx`` then it is pushed + onto the global :class:`AppContext` stack. + """ try: for k, v in self.parameters.items(): self.options_object[self.options_prefix + k] = v - yield + if self.appctx: + with push_appctx(self.appctx): + yield + else: + yield finally: for k in self.to_delete: if self.options_object.used(self.options_prefix + k): @@ -564,7 +590,8 @@ def attach_options( parameters: dict | None = None, options_prefix: str | None = None, default_prefix: str | None = None, - default_options_set: DefaultOptionSet | None = None + default_options_set: DefaultOptionSet | None = None, + appctx: AppContext | None = None, ) -> None: """Set up an :class:`OptionsManager` and attach it to a PETSc Object. @@ -580,6 +607,8 @@ def attach_options( Base string for autogenerated default prefixes. default_options_set The prefix set for any default shared with other solvers. + appctx + The :class:`AppContext` containing user python data. See Also -------- @@ -597,7 +626,8 @@ def attach_options( parameters=parameters, options_prefix=options_prefix, default_prefix=default_prefix, - default_options_set=default_options_set + default_options_set=default_options_set, + appctx=appctx, ) obj.setAttr("options", options) @@ -758,7 +788,8 @@ def set_from_options( obj, parameters=parameters, options_prefix=options_prefix, default_prefix=default_prefix, - default_options_set=default_options_set + default_options_set=default_options_set, + appctx=appctx, ) if is_set_from_options(obj): @@ -767,11 +798,7 @@ def set_from_options( PetscToolsWarning, ) - if appctx is None: - get_options(obj).set_from_options(obj) - else: - with push_appctx(appctx): - get_options(obj).set_from_options(obj) + get_options(obj).set_from_options(obj) def is_set_from_options(obj: petsc4py.PETSc.Object) -> bool: diff --git a/tests/test_appctx.py b/tests/test_appctx.py index 8dd7357..54d7544 100644 --- a/tests/test_appctx.py +++ b/tests/test_appctx.py @@ -5,6 +5,7 @@ class JacobiTestPC: prefix = "jacobi_" + def setFromOptions(self, pc): appctx = petsctools.get_appctx() prefix = (pc.getOptionsPrefix() or "") + self.prefix @@ -48,7 +49,7 @@ def test_get_appctx(): xcheck = x.duplicate() xcheck.pointwiseMult(b, diag) - with petsctools.inserted_options(ksp), petsctools.push_appctx(appctx): + with petsctools.inserted_options(ksp): ksp.solve(b, x) assert (x - xcheck).norm() < 1e-14 From ffb03ef01fda7187d158993b6f0a06c4f0d342bc Mon Sep 17 00:00:00 2001 From: Josh Hope-Collins Date: Wed, 29 Apr 2026 11:33:29 +0100 Subject: [PATCH 17/24] Separate global AppContext and scoped AppContextManager --- petsctools/__init__.py | 11 +-- petsctools/appctx.py | 195 ++++++++++++++++++++++------------------- petsctools/options.py | 25 +++--- tests/test_appctx.py | 83 +++++++++++++----- 4 files changed, 187 insertions(+), 127 deletions(-) diff --git a/petsctools/__init__.py b/petsctools/__init__.py index c3ddbc6..9e3cc67 100644 --- a/petsctools/__init__.py +++ b/petsctools/__init__.py @@ -1,8 +1,3 @@ -from .appctx import ( # noqa: F401 - AppContext, - push_appctx, - get_appctx, -) from .config import ( # noqa: F401 MissingPetscException, get_config, @@ -20,6 +15,10 @@ # is not available then attempting to access these attributes will raise an # informative error. if PETSC4PY_INSTALLED: + from .appctx import ( # noqa: F401 + AppContext, + AppContextManager, + ) from .citation import ( # noqa: F401 add_citation, cite, @@ -70,6 +69,8 @@ def __getattr__(name): "set_default_parameter", "DefaultOptionSet", "PCBase", + "AppContext", + "AppContextManager", } if name in petsc4py_attrs: raise ImportError( diff --git a/petsctools/appctx.py b/petsctools/appctx.py index 2db85ad..e99e119 100644 --- a/petsctools/appctx.py +++ b/petsctools/appctx.py @@ -1,36 +1,99 @@ +from typing import Any import itertools from functools import cached_property from contextlib import contextmanager from petsctools.exceptions import PetscToolsAppctxException -_global_appctx_stack = [] +_global_appctx_data = {} +"""The global storage for user data with arbitrary python types.""" -@contextmanager -def push_appctx(appctx: "AppContext"): - """ - Context manager for pushing an :class:`AppContext` onto the global stack. +class AppContextKey(int): + """A custom key type for AppContext.""" - The ``appctx`` will then be available via :func:`get_appctx`. - At the end of the context manager the ``appctx`` will be popped - from the global stack. - """ - _global_appctx_stack.append(appctx) - yield - _global_appctx_stack.pop() +class AppContext: + def __init__(self, prefix: str | None = None): + from petsctools.options import _validate_prefix + # possibly append underscore or cast to str + self._prefix = _validate_prefix(prefix or "") -def get_appctx(): - """Return the :class:`AppContext` at the top of the global stack. - """ - return _global_appctx_stack[-1] + @property + def prefix(self) -> str: + return self._prefix + @cached_property + def options_object(self): + """A PETSc.Options instance.""" + from petsc4py import PETSc + return PETSc.Options() -class AppContextKey(int): - """A custom key type for AppContext.""" + def _key_from_option(self, option: str) -> AppContextKey: + """ + Return the internal key for the PETSc option `option`. + Parameters + ---------- + option + The PETSc option. -class AppContext: + Returns + ------- + key + An internal key corresponding to ``option``. + """ + return AppContextKey(self.options_object.getInt(self.prefix + option)) + + def __getitem__(self, option: str | AppContextKey) -> Any: + """ + Return the value with the key saved in ``PETSc.Options()[option]``. + + Parameters + ---------- + option : + The PETSc option or key. + + Returns + ------- + val : + The value for the key `option`. + + Raises + ------ + PetscToolsAppctxException + If the AppContext does contain a value for `option`. + """ + try: + return _global_appctx_data[self._key_from_option(option)] + except KeyError: + raise PetscToolsAppctxException( + f"AppContext does not have an entry for {option}") + + def get(self, option: str | AppContextKey, + default: Any | None = None) -> Any: + """ + Return the value with the key saved in ``PETSc.Options()[option]``, + or if it does not exist return default. + + Parameters + ---------- + option : + The PETSc option or key. + default : + The value to return if ``option`` is not in the ``AppContext`` + + Returns + ------- + val : + The value for the key ``option``, or ``default``. + """ + try: + return self[option] + except PetscToolsAppctxException: + return default + + +class AppContextManager: """ Class for passing non-primitive types to PETSc python contexts. @@ -71,12 +134,12 @@ class AppContext: default = MyCustomData(10) data = appctx.get('solver_custompc_somedata', default) """ + _count = itertools.count() def __init__(self): - self._count = itertools.count(start=0) self._data = {} - def _keygen(self): + def _keygen(self) -> AppContextKey: """ Generate a new unique internal key. @@ -84,23 +147,7 @@ def _keygen(self): """ return AppContextKey(next(self._count)) - def _key_from_option(self, option): - """ - Return the internal key for the PETSc option `option`. - - Parameters - ---------- - option : str - The PETSc option. - - Returns - ------- - key : AppContextKey - An internal key corresponding to `option`. - """ - return AppContextKey(self.options_object.getInt(option)) - - def add(self, val): + def add(self, val: Any) -> AppContextKey: """ Add a value to the application context and return the autogenerated key for that value. @@ -110,67 +157,33 @@ def add(self, val): Parameters ---------- - val : Any + val The value to add to the AppContext. Returns ------- - key : AppContextKey + key The key to put into the PETSc Options dictionary. """ key = self._keygen() self._data[key] = val return key - def __getitem__(self, option): - """ - Return the value with the key saved in `PETSc.Options()[option]`. - - Parameters - ---------- - option : Union[str, AppContextKey] - The PETSc option or key. - - Returns - ------- - val : Any - The value for the key `option`. - - Raises - ------ - PetscToolsAppctxException - If the AppContext does contain a value for `option`. - """ - try: - return self._data[self._key_from_option(option)] - except KeyError: - raise PetscToolsAppctxException( - f"AppContext does not have an entry for {option}") - - def get(self, option, default=None): - """ - Return the value with the key saved in PETSc.Options()[option], - or if it does not exist return default. - - Parameters - ---------- - option : Union[str, AppContextKey] - The PETSc option or key. - default : Any - The value to return if `option` is not in the AppContext - - Returns - ------- - val : Any - The value for the key `option`, or `default`. - """ + @contextmanager + def inserted_appctx(self): + # We don't overwrite existing entries in the global data, + # so we need to keep track of what we do actually put in + # so we don't accidentally remove something we shouldn't. + to_delete = set() try: - return self[option] - except PetscToolsAppctxException: - return default - - @cached_property - def options_object(self): - """A PETSc.Options instance.""" - from petsc4py import PETSc - return PETSc.Options() + for k, v in self._data.items(): + if k not in _global_appctx_data: + _global_appctx_data[k] = v + to_delete.add(k) + yield + finally: + for k in self._data: + if k in to_delete: + del _global_appctx_data[k] + to_delete.remove(k) + assert len(to_delete) == 0 diff --git a/petsctools/options.py b/petsctools/options.py index ac9750d..a06f1ad 100644 --- a/petsctools/options.py +++ b/petsctools/options.py @@ -15,7 +15,7 @@ PetscToolsWarning, PetscToolsNotInitialisedException, ) -from petsctools.appctx import push_appctx, AppContext +from petsctools.appctx import AppContextManager _commandline_options = None @@ -390,7 +390,7 @@ class OptionsManager: The prefix set for any default shared with other solvers. See :class:`DefaultOptionSet` for more information. appctx - The :class:`AppContext` containing user python data. + The :class:`AppContextManager` containing user python data. See Also -------- @@ -402,6 +402,7 @@ class OptionsManager: inserted_options DefaultOptionSet AppContext + AppContextManager """ count = itertools.count() @@ -410,7 +411,7 @@ def __init__(self, parameters: dict, options_prefix: str | None = None, default_prefix: str | None = None, default_options_set: DefaultOptionSet | None = None, - appctx: AppContext | None = None): + appctx: AppContextManager | None = None): super().__init__() if parameters is None: parameters = {} @@ -515,7 +516,7 @@ def set_from_options(self, petsc_obj): Before calling ``petsc_obj.setFromOptions``, the options from this OptionsManager's ``parameters`` are inserted into the global :class:`PETSc.Options`, and if this OptionsManager has an ``appctx`` - then it is pushed onto the global :class:`AppContext` stack. + then all entries are inserted into the :class:`AppContext`. Parameters ---------- @@ -546,14 +547,14 @@ def set_from_options(self, petsc_obj): def inserted_options(self): """Context manager inside which the petsc options database contains the parameters from this object. - If this OptionsManager has an ``appctx`` then it is pushed - onto the global :class:`AppContext` stack. + If this OptionsManager has an ``appctx`` then all entries + are inserted into the :class:`AppContext`. """ try: for k, v in self.parameters.items(): self.options_object[self.options_prefix + k] = v if self.appctx: - with push_appctx(self.appctx): + with self.appctx.inserted_appctx(): yield else: yield @@ -591,7 +592,7 @@ def attach_options( options_prefix: str | None = None, default_prefix: str | None = None, default_options_set: DefaultOptionSet | None = None, - appctx: AppContext | None = None, + appctx: AppContextManager | None = None, ) -> None: """Set up an :class:`OptionsManager` and attach it to a PETSc Object. @@ -608,7 +609,7 @@ def attach_options( default_options_set The prefix set for any default shared with other solvers. appctx - The :class:`AppContext` containing user python data. + The :class:`AppContextManager` containing user python data. See Also -------- @@ -722,7 +723,7 @@ def set_from_options( options_prefix: str | None = None, default_prefix: str | None = None, default_options_set: DefaultOptionSet | None = None, - appctx: AppContext | None = None, + appctx: AppContextManager | None = None, ) -> None: """Set up a PETSc object from the options in its :class:`OptionsManager`. @@ -768,7 +769,7 @@ def set_from_options( OptionsManager.set_from_options attach_options DefaultOptionSet - AppContext + AppContextManager """ if has_options(obj): if parameters is not None or options_prefix is not None: @@ -833,6 +834,8 @@ def is_set_from_options(obj: petsc4py.PETSc.Object) -> bool: def inserted_options(obj): """Context manager inside which the PETSc options database contains the parameters from this object's :class:`OptionsManager`. + If the OptionsManager has an ``appctx`` then all entries are + inserted into the :class:`AppContext`. Parameters ---------- diff --git a/tests/test_appctx.py b/tests/test_appctx.py index 54d7544..b30f01d 100644 --- a/tests/test_appctx.py +++ b/tests/test_appctx.py @@ -7,22 +7,30 @@ class JacobiTestPC: prefix = "jacobi_" def setFromOptions(self, pc): - appctx = petsctools.get_appctx() + from petsc4py import PETSc prefix = (pc.getOptionsPrefix() or "") + self.prefix - self.scale = appctx[prefix + "scale"] + + prefixed_appctx = PETSc.Options().getBool( + prefix + "prefixed_appctx") + + if prefixed_appctx: + appctx = petsctools.AppContext(prefix) + self.scale = appctx["scale"] + else: + appctx = petsctools.AppContext() + self.scale = appctx[prefix + "scale"] def apply(self, pc, x, y): y.pointwiseMult(x, self.scale) @pytest.mark.skipnopetsc4py -def test_get_appctx(): +@pytest.mark.parametrize("use_prefix", ["with_prefix", "without_prefix"]) +def test_appctx_context_manager(use_prefix): PETSc = petsctools.init() n = 4 sizes = (n, n) - appctx = petsctools.AppContext() - diag = PETSc.Vec().createSeq(sizes) diag.setSizes((n, n)) diag.array[:] = [1, 2, 3, 4] @@ -31,13 +39,17 @@ def test_get_appctx(): ksp = PETSc.KSP().create() ksp.setOperators(mat, mat) + + appctx = petsctools.AppContextManager() + petsctools.set_from_options( ksp, parameters={ 'ksp_type': 'preonly', 'pc_type': 'python', 'pc_python_type': f'{__name__}.JacobiTestPC', - 'jacobi_scale': appctx.add(diag) + 'jacobi_scale': appctx.add(diag), + 'jacobi_prefixed_appctx': use_prefix == "with_prefix", }, options_prefix="myksp", appctx=appctx, @@ -59,24 +71,55 @@ def test_get_appctx(): def test_appctx_key(): PETSc = petsctools.init() - appctx = petsctools.AppContext() + manager = petsctools.AppContextManager() - param = 10 + prefix0_param = 10 options = PETSc.Options() - options['solver_param'] = appctx.add(param) + options['prefix0_param'] = manager.add(prefix0_param) + + appctx = petsctools.AppContext() + + # The param shouldn't be in the global dictionary yet + with pytest.raises(PetscToolsAppctxException): + appctx['param'] # Can we access param via the prefixed option? - prm = appctx.get('solver_param') - assert prm is param + with manager.inserted_appctx(): + prm = appctx.get('prefix0_param') + assert prm is prefix0_param - prm = appctx['solver_param'] - assert prm is param + prm = appctx['prefix0_param'] + assert prm is prefix0_param - # Can we set a default value? - default = 20 - prm = appctx.get('param', default) - assert prm is default + # Can we set a default value? + default = 20 + prm = appctx.get('param', default) + assert prm is default - # Will an invalid key raise an error - with pytest.raises(PetscToolsAppctxException): - appctx['param'] + # Will an invalid key raise an error + with pytest.raises(PetscToolsAppctxException): + appctx['param'] + + # Now try with a prefixed AppContext + + # First add a param option with a different prefix + prefix1_param = 20 + options['prefix1_param'] = manager.add(prefix1_param) + + appctx0 = petsctools.AppContext('prefix0') + appctx1 = petsctools.AppContext('prefix1') + + with manager.inserted_appctx(): + # This should only see prefix0 entries + prm = appctx0.get('param') + assert prm is prefix0_param + + prm = appctx0['param'] + assert prm is prefix0_param + + # This should only see prefix1 entries + prm = appctx1.get('param') + assert prm is prefix1_param + + prm = appctx1['param'] + assert prm is prefix1_param From f8930637f6448a01456e196e7797d3e51ae06d7a Mon Sep 17 00:00:00 2001 From: Josh Hope-Collins Date: Wed, 29 Apr 2026 11:44:44 +0100 Subject: [PATCH 18/24] AppContext()[option] = value? --- petsctools/appctx.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/petsctools/appctx.py b/petsctools/appctx.py index e99e119..32ae83b 100644 --- a/petsctools/appctx.py +++ b/petsctools/appctx.py @@ -44,7 +44,7 @@ def _key_from_option(self, option: str) -> AppContextKey: """ return AppContextKey(self.options_object.getInt(self.prefix + option)) - def __getitem__(self, option: str | AppContextKey) -> Any: + def __getitem__(self, option: str | AppContextKey, /) -> Any: """ Return the value with the key saved in ``PETSc.Options()[option]``. @@ -69,6 +69,11 @@ def __getitem__(self, option: str | AppContextKey) -> Any: raise PetscToolsAppctxException( f"AppContext does not have an entry for {option}") + def __setitem__(self, option: str, value: Any, /): + key = AppContextManager._keygen() + self.options_object[self.prefix + option] = key + _global_appctx_data[key] = value + def get(self, option: str | AppContextKey, default: Any | None = None) -> Any: """ @@ -139,6 +144,7 @@ class AppContextManager: def __init__(self): self._data = {} + @classmethod def _keygen(self) -> AppContextKey: """ Generate a new unique internal key. From 65f166a4a0293801723c5a3a7197150144b24262 Mon Sep 17 00:00:00 2001 From: Josh Hope-Collins Date: Wed, 29 Apr 2026 12:02:57 +0100 Subject: [PATCH 19/24] make the AppContextKey a helpfully named string --- petsctools/appctx.py | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/petsctools/appctx.py b/petsctools/appctx.py index 32ae83b..1b3e9bc 100644 --- a/petsctools/appctx.py +++ b/petsctools/appctx.py @@ -8,13 +8,20 @@ """The global storage for user data with arbitrary python types.""" -class AppContextKey(int): +class AppContextKey(str): """A custom key type for AppContext.""" + _count = itertools.count() + + @classmethod + def _generate_key(cls): + return f"petsctools_appctx_key_{next(cls._count)}" + class AppContext: def __init__(self, prefix: str | None = None): from petsctools.options import _validate_prefix + # possibly append underscore or cast to str self._prefix = _validate_prefix(prefix or "") @@ -26,6 +33,7 @@ def prefix(self) -> str: def options_object(self): """A PETSc.Options instance.""" from petsc4py import PETSc + return PETSc.Options() def _key_from_option(self, option: str) -> AppContextKey: @@ -42,7 +50,9 @@ def _key_from_option(self, option: str) -> AppContextKey: key An internal key corresponding to ``option``. """ - return AppContextKey(self.options_object.getInt(self.prefix + option)) + return AppContextKey( + self.options_object.getString(self.prefix + option) + ) def __getitem__(self, option: str | AppContextKey, /) -> Any: """ @@ -67,15 +77,17 @@ def __getitem__(self, option: str | AppContextKey, /) -> Any: return _global_appctx_data[self._key_from_option(option)] except KeyError: raise PetscToolsAppctxException( - f"AppContext does not have an entry for {option}") + f"AppContext does not have an entry for {option}" + ) def __setitem__(self, option: str, value: Any, /): - key = AppContextManager._keygen() + key = AppContextKey._generate_key() self.options_object[self.prefix + option] = key _global_appctx_data[key] = value - def get(self, option: str | AppContextKey, - default: Any | None = None) -> Any: + def get( + self, option: str | AppContextKey, default: Any | None = None + ) -> Any: """ Return the value with the key saved in ``PETSc.Options()[option]``, or if it does not exist return default. @@ -139,20 +151,10 @@ class AppContextManager: default = MyCustomData(10) data = appctx.get('solver_custompc_somedata', default) """ - _count = itertools.count() def __init__(self): self._data = {} - @classmethod - def _keygen(self) -> AppContextKey: - """ - Generate a new unique internal key. - - This should not called directly by the user. - """ - return AppContextKey(next(self._count)) - def add(self, val: Any) -> AppContextKey: """ Add a value to the application context and @@ -171,7 +173,7 @@ def add(self, val: Any) -> AppContextKey: key The key to put into the PETSc Options dictionary. """ - key = self._keygen() + key = AppContextKey._generate_key() self._data[key] = val return key From b66867ef3f0b6016be4675c3c7422f0b006bddca Mon Sep 17 00:00:00 2001 From: Josh Hope-Collins Date: Tue, 12 May 2026 15:58:44 +0100 Subject: [PATCH 20/24] appctx demo --- docs/source/appctx.rst | 98 ++++++++++++++++++++++++++++ docs/source/index.rst | 1 + tests/docs/test_appctx_docs.py | 114 +++++++++++++++++++++++++++++++++ 3 files changed, 213 insertions(+) create mode 100644 docs/source/appctx.rst create mode 100644 tests/docs/test_appctx_docs.py diff --git a/docs/source/appctx.rst b/docs/source/appctx.rst new file mode 100644 index 0000000..550956a --- /dev/null +++ b/docs/source/appctx.rst @@ -0,0 +1,98 @@ +AppContext demo +--------------- + +The PETSc options provide a simple but powerful DSL for configuring composable solvers. +However, their main limitation is that the values of each option is limited to intrinsic C types, e.g. ``str``, ``float``, ``int``, or ``complex``. +Sometimes more advanced data is useful or essential for building a particular solver. + +The ``AppContext`` fulfils this need by providing a means of passing arbitrary Python types through to Python PETSc types (e.g. Python type PCs). +In this demo we show how to use the ``AppContext`` to pass data to a custom Python type PC using the variable coefficient diffusion equation as an example. + + +Diffusion equation with variable coefficients +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +The diffusion equation with coefficient :math:`\sigma(x)` depending on the spatial coordinate is: + +.. math:: + + u - \nabla\cdot\left(\sigma(x)\nabla u\right) = b + +We will solve this matrix with finite differences with the standard 3 point central stencil. +The particular details of the discretisation are not essential for this demo so we will be brief in the description. + +If :math:`D` is the assembled matrix for the finite difference gradient stencil, and :math:`\Sigma` is a diagonal matrix with the value of the diffusion coefficient at each grid point, then the assembled matrix for the diffusion equation is: + +.. math:: + + \left(I + D^{T}\Sigma D\right)u = b + +The following Python function takes a numpy array ``sigma`` with the value of :math:`\sigma` at each grid point and assembles a sparse (``aij``) PETSc Mat for the diffusion equation. +We will use it later to build the ``Mat`` for a ``KSP`` to solve the diffusion equation. + +.. literalinclude:: ../../tests/docs/test_appctx_docs.py + :language: python3 + :dedent: + :start-after: [appctx_docs create_mat-start] + :end-before: [appctx_docs create_mat-end] + +A PC needing Python data +~~~~~~~~~~~~~~~~~~~~~~~~ + +To precondition this ``Mat`` we will use a diagonal matrix with user specified values on the diagonal. +This might be useful for example if we were to solve multiple diffusion equations with a slightly different diffusion coefficient each time. +We could build the preconditioner from some average diffusion coefficient and reuse the same PC each time. + +The following code defines the Python type PC. We need two values, a ``scale`` (``float``), and a ``vec`` (``PETSc.Vec``). + +1. The ``scale`` value is a ``float``, and can therefore be passed as standard via the ``PETSc.Options`` using the ``"diagonal_scale"`` option. + +2. The ``vec`` value is a ``PETSc.Vec`` which specifies the diagonal matrix to use as the preconditioner. + This is clearly not an intrinsic type to cannot be passed via the ``PETSc.Options`` directly. + Instead we access it via the ``petsctools.AppContext`` using the ``"diagonal_vec"`` key. We will see below how to insert this value into the ``AppContext``. + +.. literalinclude:: ../../tests/docs/test_appctx_docs.py + :language: python3 + :dedent: + :start-after: [appctx_docs pc-start] + :end-before: [appctx_docs pc-end] + +Building the KSP +~~~~~~~~~~~~~~~~ + +We specify a diffusion coefficient as some random variations :math:`\sigma'` around a mean value :math:`\overline{\sigma}`, i.e. :math:`\sigma(x) = \overline{\sigma} + \sigma'(x)`. +The diagonal for the preconditioner matrix is the diagonal that we would if assembling the matrix with a constant diffusion coefficient. + +.. literalinclude:: ../../tests/docs/test_appctx_docs.py + :language: python3 + :dedent: + :start-after: [appctx_docs create_ksp-start] + :end-before: [appctx_docs create_ksp-end] + +The Options and the AppContext +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Now we configure the ``KSP`` using the PETSc options with ``petsctools.set_from_options``. +We can see that common options, e.g. ``ksp_type`` are set as usual in the ``parameters`` dictionary. +However, when we come to passing the ``"diagonal_vec"`` we use the ``AppContextManager`` class. +This class associates entries in the ``Options`` database with arbitrary Python objects, in this instance ``pdiag``. +We then pass the ``appmngr`` to ``set_from_options`` so that it is available later on when solving the ``KSP``. + +.. literalinclude:: ../../tests/docs/test_appctx_docs.py + :language: python3 + :dedent: + :start-after: [appctx_docs set_from_options-start] + :end-before: [appctx_docs set_from_options-end] + +Solving the KSP +~~~~~~~~~~~~~~~ + +Now we come to solving the system. +The ``petsctools.inserted_options`` context manager makes sure that any options in the ``parameters`` dictionary are made available in the global ``Options`` database during the solve. +It also makes sure that any objects in the associated ``AppContextManager`` are made available in the global ``AppContext`` database, so that when we access ``"diagonal_vec"`` in the ``DiagonalPC`` we find the ``pdiag`` ``Vec``. + +.. literalinclude:: ../../tests/docs/test_appctx_docs.py + :language: python3 + :dedent: + :start-after: [appctx_docs solve-start] + :end-before: [appctx_docs solve-end] diff --git a/docs/source/index.rst b/docs/source/index.rst index 978da07..f4bb9f6 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -9,4 +9,5 @@ petsctools provides Pythonic extensions for petsc4py and slepc4py. examples cython + appctx generated/modules diff --git a/tests/docs/test_appctx_docs.py b/tests/docs/test_appctx_docs.py new file mode 100644 index 0000000..45635d3 --- /dev/null +++ b/tests/docs/test_appctx_docs.py @@ -0,0 +1,114 @@ +import pytest +# [appctx_docs create_mat-start] +import numpy as np +import petsctools + + +def diffusion_mat(sigma): + """ + AIJ Mat for the diffusion equation with variable coefficient. + q - div(sigma*grad(q)) = b + (I + D.T@sigma@D)q = b + """ + from petsc4py import PETSc + n = sigma.shape[0] + dtype = sigma.dtype + + # top row + row_start = [0] + col_indices = [0, 1] + row_start.append(row_start[-1]+2) + + # interior rows + for j in range(1, n-1): + col_indices.extend([j-1, j, j+1]) + row_start.append(row_start[-1]+3) + + # bottom row + col_indices.extend([n-2, n-1]) + row_start.append(row_start[-1]+2) + + # values for leading and upper/lower diagonals + diagonal = 1 + sigma.copy() + diagonal[:-1] += sigma[1:] + offdiags = -sigma[1:] + + # interleave diagonal entries + Avals = np.zeros(3*n-2, dtype=dtype) + Avals[::3] = diagonal + Avals[1::3] = offdiags + Avals[2::3] = offdiags + + amat = PETSc.Mat().createAIJWithArrays( + size=(n, n), + csr=(row_start, col_indices, Avals) + ) + return amat +# [appctx_docs create_mat-end] + + +# [appctx_docs pc-start] +class DiagonalPC: + prefix = "diagonal_" + + def setFromOptions(self, pc): + from petsc4py import PETSc + prefix = (pc.getOptionsPrefix() or "") + self.prefix + + options = PETSc.Options() + self.scale = options.getReal(prefix + "scale", 1.0) + + appctx = petsctools.AppContext() + self.vec = appctx[prefix + "vec"] + + def apply(self, pc, x, y): + y.pointwiseMult(x, self.vec) + y.scale(self.scale) +# [appctx_docs pc-end] + + +@pytest.mark.skipnopetsc4py +def test_appctx_docs(): + # [appctx_docs create_ksp-start] + PETSc = petsctools.init() + np.random.seed(13) + + n = 50 + sigma_bar = 2 + sigma = sigma_bar*(1 + 0.1*np.random.random_sample(n)) + + amat = diffusion_mat(sigma) + + pdiag = PETSc.Vec().createSeq(n) + pdiag.set(1/(1 + 2*sigma_bar)) + pdiag.setValue(n-1, 1/(1 + sigma_bar)) + + ksp = PETSc.KSP().create() + ksp.setOperators(amat) + # [appctx_docs create_ksp-end] + + # [appctx_docs set_from_options-start] + appmngr = petsctools.AppContextManager() + + petsctools.set_from_options( + ksp, parameters={ + 'ksp_converged_reason': None, + 'ksp_type': 'richardson', + 'ksp_richardson_scale': 0.9, + 'pc_type': 'python', + 'pc_python_type': f'{__name__}.DiagonalPC', + 'diagonal_vec': appmngr.add(pdiag), + }, + appctx=appmngr, + options_prefix="", + ) + # [appctx_docs set_from_options-end] + + # [appctx_docs solve-start] + u, b = amat.createVecs() + u.zeroEntries() + b.array[:] = np.random.random_sample(n) + + with petsctools.inserted_options(ksp): + ksp.solve(b, u) + # [appctx_docs solve-end] From 814967f1efd08540c0e296a5478b7088e37a2d92 Mon Sep 17 00:00:00 2001 From: Josh Hope-Collins Date: Tue, 12 May 2026 16:24:59 +0100 Subject: [PATCH 21/24] numpy for tests and demos --- pyproject.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 1bf7d60..81efaec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,9 +32,9 @@ requires = ["setuptools>=77.0.3"] build-backend = "setuptools.build_meta" [dependency-groups] -docs = ["sphinx"] +docs = ["sphinx", "numpy"] lint = ["ruff"] -test = ["pytest"] +test = ["pytest", "numpy"] ci = [ {include-group = "docs"}, {include-group = "lint"}, From 813504cd037be6105b9a5f480cdd4af74959fea3 Mon Sep 17 00:00:00 2001 From: Josh Hope-Collins Date: Tue, 12 May 2026 19:20:21 +0100 Subject: [PATCH 22/24] tidy up appctx demo --- docs/source/appctx.rst | 73 +++++++++++++++++++++------------ docs/source/conf.py | 14 +++++-- petsctools/options.py | 28 ++++++------- tests/docs/test_appctx_docs.py | 74 +++++++++++++++++++--------------- tests/test_appctx.py | 14 +++---- 5 files changed, 121 insertions(+), 82 deletions(-) diff --git a/docs/source/appctx.rst b/docs/source/appctx.rst index 550956a..7a6d0c8 100644 --- a/docs/source/appctx.rst +++ b/docs/source/appctx.rst @@ -1,12 +1,12 @@ -AppContext demo ---------------- +The OptionsManager and the AppContext +------------------------------------- The PETSc options provide a simple but powerful DSL for configuring composable solvers. However, their main limitation is that the values of each option is limited to intrinsic C types, e.g. ``str``, ``float``, ``int``, or ``complex``. Sometimes more advanced data is useful or essential for building a particular solver. -The ``AppContext`` fulfils this need by providing a means of passing arbitrary Python types through to Python PETSc types (e.g. Python type PCs). -In this demo we show how to use the ``AppContext`` to pass data to a custom Python type PC using the variable coefficient diffusion equation as an example. +:class:`petsctools.AppContext <.appctx.AppContext>` fulfils this need by providing a means of passing arbitrary Python types through to Python PETSc types (e.g. Python type PCs). +In this demo we show how to use the :class:`~.appctx.AppContext` to pass data to a custom Python type PC using the variable coefficient diffusion equation as an example. Diffusion equation with variable coefficients @@ -25,10 +25,12 @@ If :math:`D` is the assembled matrix for the finite difference gradient stencil, .. math:: - \left(I + D^{T}\Sigma D\right)u = b + Au = \left(I + D^{T}\Sigma D\right)u = b, + \quad + \Sigma_{ii} = \sigma(x_{i}) The following Python function takes a numpy array ``sigma`` with the value of :math:`\sigma` at each grid point and assembles a sparse (``aij``) PETSc Mat for the diffusion equation. -We will use it later to build the ``Mat`` for a ``KSP`` to solve the diffusion equation. +We will use it later to build the :class:`~petsc4py.PETSc.Mat` for a :class:`~petsc4py.PETSc.KSP` to solve the diffusion equation. .. literalinclude:: ../../tests/docs/test_appctx_docs.py :language: python3 @@ -39,17 +41,30 @@ We will use it later to build the ``Mat`` for a ``KSP`` to solve the diffusion e A PC needing Python data ~~~~~~~~~~~~~~~~~~~~~~~~ -To precondition this ``Mat`` we will use a diagonal matrix with user specified values on the diagonal. -This might be useful for example if we were to solve multiple diffusion equations with a slightly different diffusion coefficient each time. -We could build the preconditioner from some average diffusion coefficient and reuse the same PC each time. +Imagine a scenario where we need to solve :math:`A` multiple times and :math:`\sigma` changes slightly each time, for example if we are solving the unsteady diffusion equation with time-varying coefficients. +Rather than recomputing a preconditioner every time :math:`A` changes, we might instead find a representative :math:`\sigma_{p}` and use that to compute a preconditioner which can be reused for all solves. -The following code defines the Python type PC. We need two values, a ``scale`` (``float``), and a ``vec`` (``PETSc.Vec``). +For simplicity, in this demo the preconditioner :math:`P` will just be the diagonal of the assembled matrix :math:`A_{p}` for the diffusion equation with :math:`\sigma_{p}` with a simple scaling factor :math:`\omega`: -1. The ``scale`` value is a ``float``, and can therefore be passed as standard via the ``PETSc.Options`` using the ``"diagonal_scale"`` option. +.. math:: + + A_{p} = I + D^{T}\Sigma_{p} D, + \quad + P = \omega^{-1}\mathrm{diag}(A_{p}). + +The diagonal of a matrix is clearly not expensive to compute, but in practice we would use a factorisation of :math:`A_{p}` which would be more expensive to compute and so would be more worthwhile reusing. + +The preconditioner defined above is implemented with a Python type PC called ``DiffusionJacobiPC`` in the code below. +Constructing :math:`P` requires two values, :math:`\sigma_{p}` and :math:`\omega`, which must be provided by the user. -2. The ``vec`` value is a ``PETSc.Vec`` which specifies the diagonal matrix to use as the preconditioner. - This is clearly not an intrinsic type to cannot be passed via the ``PETSc.Options`` directly. - Instead we access it via the ``petsctools.AppContext`` using the ``"diagonal_vec"`` key. We will see below how to insert this value into the ``AppContext``. +1. The scaling factor :math:`\omega` is just a real number, and can therefore be passed as usual via the :class:`PETSc.Options ` using the ``"djacobi_scale"`` option. + +2. The diffusion coefficient at each grid point :math:`\sigma_{p}(x_{i})` is defined as a numpy array. + This is clearly not an intrinsic type and so cannot be passed via the :class:`PETSc.Options ` directly. + Instead, we access it via the :class:`~.appctx.AppContext` using the ``"djacobi_sigma"`` key. + +The :class:`~petsctools.appctx.AppContext` mimics the :class:`PETSc.Options ` very closely, but can contain arbitrary Python data. +We will see below how to add ``sigma`` into the :class:`~petsctools.appctx.AppContext` so that it is available to the ``DiffusionJacobiPC``. .. literalinclude:: ../../tests/docs/test_appctx_docs.py :language: python3 @@ -57,11 +72,11 @@ The following code defines the Python type PC. We need two values, a ``scale`` ( :start-after: [appctx_docs pc-start] :end-before: [appctx_docs pc-end] -Building the KSP -~~~~~~~~~~~~~~~~ +Assembling the system +~~~~~~~~~~~~~~~~~~~~~ -We specify a diffusion coefficient as some random variations :math:`\sigma'` around a mean value :math:`\overline{\sigma}`, i.e. :math:`\sigma(x) = \overline{\sigma} + \sigma'(x)`. -The diagonal for the preconditioner matrix is the diagonal that we would if assembling the matrix with a constant diffusion coefficient. +We specify the diffusion coefficient as some random variations :math:`\sigma'` around a constant value :math:`\overline{\sigma}`, i.e. :math:`\sigma(x) = \overline{\sigma} + \sigma'(x)`. +Assuming that :math:`\sigma'` is the component that may vary from solve to solve, we use :math:`\sigma_{p}=\overline{\sigma}`. .. literalinclude:: ../../tests/docs/test_appctx_docs.py :language: python3 @@ -72,11 +87,15 @@ The diagonal for the preconditioner matrix is the diagonal that we would if asse The Options and the AppContext ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -Now we configure the ``KSP`` using the PETSc options with ``petsctools.set_from_options``. -We can see that common options, e.g. ``ksp_type`` are set as usual in the ``parameters`` dictionary. -However, when we come to passing the ``"diagonal_vec"`` we use the ``AppContextManager`` class. -This class associates entries in the ``Options`` database with arbitrary Python objects, in this instance ``pdiag``. -We then pass the ``appmngr`` to ``set_from_options`` so that it is available later on when solving the ``KSP``. +Now we configure ``ksp`` by passing PETSc options as key-value pairs in the ``parameters`` dictionary to :func:`petsctools.set_from_options <.options.set_from_options>`. +This function will create a :class:`petsctools.OptionsManager <.options.OptionsManager>` and attach it to ``ksp``. + +We can see that common options, e.g. ``"ksp_type"`` are set as usual in the ``parameters`` dictionary. +However, when we come to the ``"djacobi_sigma"`` value we use the :class:`petsctools.AppContextManager <.appctx.AppContextManager>` class. +The :meth:`AppContextManager.add <.appctx.AppContextManager.add>` method returns a unique value which is used to associate a PETSc option to whatever data was passed to ``add``. +For example, adding the key-value pair ``"djacobi_sigma": appmngr.add(sigma_p)`` to the ``parameters`` dictionary means that, during the solve, we will be able to access ``sigma_p`` via ``AppContext()["djacobi_sigma"]``. + +We then pass the :class:`~.appctx.AppContextManager` to :func:`~.options.set_from_options` so that it can be attached to ``ksp`` and its data can be made available later on during the solve. .. literalinclude:: ../../tests/docs/test_appctx_docs.py :language: python3 @@ -87,9 +106,11 @@ We then pass the ``appmngr`` to ``set_from_options`` so that it is available lat Solving the KSP ~~~~~~~~~~~~~~~ -Now we come to solving the system. -The ``petsctools.inserted_options`` context manager makes sure that any options in the ``parameters`` dictionary are made available in the global ``Options`` database during the solve. -It also makes sure that any objects in the associated ``AppContextManager`` are made available in the global ``AppContext`` database, so that when we access ``"diagonal_vec"`` in the ``DiagonalPC`` we find the ``pdiag`` ``Vec``. +Now we come to actually solving the linear equation :math:`Au=b`. +To avoid memory leaks, :func:`~.options.set_from_options` does not permanently insert the contents of ``parameters`` and the ``appmngr`` into the global :class:`PETSc.Options ` and :class:`~.appctx.AppContext` databases respectively. +Instead, we use the :func:`petsctools.inserted_options <.options.inserted_options>` context manager. +On entry, this context manager inserts the contents of ``parameters`` and ``appmngr`` into the global databases, and on exit it removes them again. +This means that we need to use the :func:`~.options.inserted_options` context manager whenever these entries will be needed, for example during the solve when the KSP and PC are being set up. .. literalinclude:: ../../tests/docs/test_appctx_docs.py :language: python3 diff --git a/docs/source/conf.py b/docs/source/conf.py index eadb2c7..7951b12 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -19,6 +19,7 @@ extensions = [ "sphinx.ext.apidoc", "sphinx.ext.napoleon", + "sphinx.ext.intersphinx" ] templates_path = ["_templates"] @@ -32,6 +33,13 @@ # -- sphinx.ext.apidoc configuration ------------------------------------------ -apidoc_modules = [ - {"path": "../../petsctools", "destination": "generated"}, -] +apidoc_modules = [{ + "path": "../../petsctools", + "destination": "generated", +}] + +# -- sphinx.ext.intersphinx configuration ------------------------------------------ + +intersphinx_mapping = { + 'petsc4py': ('https://petsc.org/release/petsc4py/', None), +} diff --git a/petsctools/options.py b/petsctools/options.py index a06f1ad..be92fd5 100644 --- a/petsctools/options.py +++ b/petsctools/options.py @@ -389,7 +389,7 @@ class OptionsManager: default_options_set The prefix set for any default shared with other solvers. See :class:`DefaultOptionSet` for more information. - appctx + appmngr The :class:`AppContextManager` containing user python data. See Also @@ -411,7 +411,7 @@ def __init__(self, parameters: dict, options_prefix: str | None = None, default_prefix: str | None = None, default_options_set: DefaultOptionSet | None = None, - appctx: AppContextManager | None = None): + appmngr: AppContextManager | None = None): super().__init__() if parameters is None: parameters = {} @@ -479,7 +479,7 @@ def __init__(self, parameters: dict, self._setfromoptions = False # user data - self.appctx = appctx + self.appmngr = appmngr # Keep track of options used between invocations of inserted_options(). self._used_options = set() @@ -515,7 +515,7 @@ def set_from_options(self, petsc_obj): Before calling ``petsc_obj.setFromOptions``, the options from this OptionsManager's ``parameters`` are inserted into the global - :class:`PETSc.Options`, and if this OptionsManager has an ``appctx`` + :class:`PETSc.Options`, and if this OptionsManager has an ``appmngr`` then all entries are inserted into the :class:`AppContext`. Parameters @@ -547,14 +547,14 @@ def set_from_options(self, petsc_obj): def inserted_options(self): """Context manager inside which the petsc options database contains the parameters from this object. - If this OptionsManager has an ``appctx`` then all entries + If this OptionsManager has an ``appmngr`` then all entries are inserted into the :class:`AppContext`. """ try: for k, v in self.parameters.items(): self.options_object[self.options_prefix + k] = v - if self.appctx: - with self.appctx.inserted_appctx(): + if self.appmngr: + with self.appmngr.inserted_appctx(): yield else: yield @@ -592,7 +592,7 @@ def attach_options( options_prefix: str | None = None, default_prefix: str | None = None, default_options_set: DefaultOptionSet | None = None, - appctx: AppContextManager | None = None, + appmngr: AppContextManager | None = None, ) -> None: """Set up an :class:`OptionsManager` and attach it to a PETSc Object. @@ -608,7 +608,7 @@ def attach_options( Base string for autogenerated default prefixes. default_options_set The prefix set for any default shared with other solvers. - appctx + appmngr The :class:`AppContextManager` containing user python data. See Also @@ -628,7 +628,7 @@ def attach_options( options_prefix=options_prefix, default_prefix=default_prefix, default_options_set=default_options_set, - appctx=appctx, + appmngr=appmngr, ) obj.setAttr("options", options) @@ -723,7 +723,7 @@ def set_from_options( options_prefix: str | None = None, default_prefix: str | None = None, default_options_set: DefaultOptionSet | None = None, - appctx: AppContextManager | None = None, + appmngr: AppContextManager | None = None, ) -> None: """Set up a PETSc object from the options in its :class:`OptionsManager`. @@ -749,7 +749,7 @@ def set_from_options( Base string for autogenerated default prefixes. default_options_set The prefix set for any default shared with other solvers. - appctx + appmngr An application context for passing non-native python types. Raises @@ -790,7 +790,7 @@ def set_from_options( options_prefix=options_prefix, default_prefix=default_prefix, default_options_set=default_options_set, - appctx=appctx, + appmngr=appmngr, ) if is_set_from_options(obj): @@ -834,7 +834,7 @@ def is_set_from_options(obj: petsc4py.PETSc.Object) -> bool: def inserted_options(obj): """Context manager inside which the PETSc options database contains the parameters from this object's :class:`OptionsManager`. - If the OptionsManager has an ``appctx`` then all entries are + If the OptionsManager has an ``appmngr`` then all entries are inserted into the :class:`AppContext`. Parameters diff --git a/tests/docs/test_appctx_docs.py b/tests/docs/test_appctx_docs.py index 45635d3..3f5cdf1 100644 --- a/tests/docs/test_appctx_docs.py +++ b/tests/docs/test_appctx_docs.py @@ -6,27 +6,33 @@ def diffusion_mat(sigma): """ - AIJ Mat for the diffusion equation with variable coefficient. - q - div(sigma*grad(q)) = b - (I + D.T@sigma@D)q = b + AIJ Mat for the diffusion equation with a variable diffusion coefficient. + + (I + D.T@sigma@D)u = b """ from petsc4py import PETSc n = sigma.shape[0] dtype = sigma.dtype - # top row + # index lists for CSR format row_start = [0] - col_indices = [0, 1] - row_start.append(row_start[-1]+2) + col_indices = [] + + # top row + idxs = [0, 1] + col_indices.extend(idxs) + row_start.append(row_start[-1] + len(idxs)) # interior rows for j in range(1, n-1): - col_indices.extend([j-1, j, j+1]) - row_start.append(row_start[-1]+3) + idxs = [j-1, j, j+1] + col_indices.extend(idxs) + row_start.append(row_start[-1] + len(idxs)) # bottom row - col_indices.extend([n-2, n-1]) - row_start.append(row_start[-1]+2) + idxs = [n-2, n-1] + col_indices.extend(idxs) + row_start.append(row_start[-1] + len(idxs)) # values for leading and upper/lower diagonals diagonal = 1 + sigma.copy() @@ -39,31 +45,35 @@ def diffusion_mat(sigma): Avals[1::3] = offdiags Avals[2::3] = offdiags - amat = PETSc.Mat().createAIJWithArrays( + A = PETSc.Mat().createAIJWithArrays( size=(n, n), csr=(row_start, col_indices, Avals) ) - return amat + return A # [appctx_docs create_mat-end] # [appctx_docs pc-start] -class DiagonalPC: - prefix = "diagonal_" +class DiffusionJacobiPC: + prefix = "djacobi_" def setFromOptions(self, pc): from petsc4py import PETSc prefix = (pc.getOptionsPrefix() or "") + self.prefix options = PETSc.Options() - self.scale = options.getReal(prefix + "scale", 1.0) + scale = options.getReal(prefix + "scale", 1.0) appctx = petsctools.AppContext() - self.vec = appctx[prefix + "vec"] + sigma = appctx[prefix + "sigma"] + + Ap = diffusion_mat(sigma) + P = Ap.getDiagonal() + P.scale(1/scale) + self.P = P def apply(self, pc, x, y): - y.pointwiseMult(x, self.vec) - y.scale(self.scale) + y.pointwiseDivide(x, self.P) # [appctx_docs pc-end] @@ -72,40 +82,40 @@ def test_appctx_docs(): # [appctx_docs create_ksp-start] PETSc = petsctools.init() np.random.seed(13) - n = 50 - sigma_bar = 2 - sigma = sigma_bar*(1 + 0.1*np.random.random_sample(n)) - amat = diffusion_mat(sigma) + sigma_bar = 2*np.ones(n) + sigma_prime = -0.2 + 0.4*np.random.random_sample(n) + + sigma = sigma_bar + sigma_prime + sigma_p = sigma_bar - pdiag = PETSc.Vec().createSeq(n) - pdiag.set(1/(1 + 2*sigma_bar)) - pdiag.setValue(n-1, 1/(1 + sigma_bar)) + A = diffusion_mat(sigma) ksp = PETSc.KSP().create() - ksp.setOperators(amat) + ksp.setOperators(A) # [appctx_docs create_ksp-end] # [appctx_docs set_from_options-start] appmngr = petsctools.AppContextManager() petsctools.set_from_options( - ksp, parameters={ + ksp, + parameters={ 'ksp_converged_reason': None, 'ksp_type': 'richardson', - 'ksp_richardson_scale': 0.9, 'pc_type': 'python', - 'pc_python_type': f'{__name__}.DiagonalPC', - 'diagonal_vec': appmngr.add(pdiag), + 'pc_python_type': f'{__name__}.DiffusionJacobiPC', + 'djacobi_scale': 0.9, + 'djacobi_sigma': appmngr.add(sigma_p), }, - appctx=appmngr, + appmngr=appmngr, options_prefix="", ) # [appctx_docs set_from_options-end] # [appctx_docs solve-start] - u, b = amat.createVecs() + u, b = A.createVecs() u.zeroEntries() b.array[:] = np.random.random_sample(n) diff --git a/tests/test_appctx.py b/tests/test_appctx.py index b30f01d..4e94eed 100644 --- a/tests/test_appctx.py +++ b/tests/test_appctx.py @@ -10,10 +10,10 @@ def setFromOptions(self, pc): from petsc4py import PETSc prefix = (pc.getOptionsPrefix() or "") + self.prefix - prefixed_appctx = PETSc.Options().getBool( - prefix + "prefixed_appctx") + use_prefixed_appctx = PETSc.Options().getBool( + prefix + "use_prefixed_appctx") - if prefixed_appctx: + if use_prefixed_appctx: appctx = petsctools.AppContext(prefix) self.scale = appctx["scale"] else: @@ -40,7 +40,7 @@ def test_appctx_context_manager(use_prefix): ksp = PETSc.KSP().create() ksp.setOperators(mat, mat) - appctx = petsctools.AppContextManager() + appmngr = petsctools.AppContextManager() petsctools.set_from_options( ksp, @@ -48,11 +48,11 @@ def test_appctx_context_manager(use_prefix): 'ksp_type': 'preonly', 'pc_type': 'python', 'pc_python_type': f'{__name__}.JacobiTestPC', - 'jacobi_scale': appctx.add(diag), - 'jacobi_prefixed_appctx': use_prefix == "with_prefix", + 'jacobi_scale': appmngr.add(diag), + 'jacobi_use_prefixed_appctx': use_prefix == "with_prefix", }, options_prefix="myksp", - appctx=appctx, + appmngr=appmngr, ) x, b = mat.createVecs() From e1d3597f40d65b76920b9b4721a771543cdaed42 Mon Sep 17 00:00:00 2001 From: Josh Hope-Collins Date: Tue, 12 May 2026 19:25:21 +0100 Subject: [PATCH 23/24] appctx exception --- petsctools/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/petsctools/__init__.py b/petsctools/__init__.py index 9e3cc67..88df751 100644 --- a/petsctools/__init__.py +++ b/petsctools/__init__.py @@ -18,6 +18,7 @@ from .appctx import ( # noqa: F401 AppContext, AppContextManager, + PetscToolsAppctxException, ) from .citation import ( # noqa: F401 add_citation, @@ -71,6 +72,7 @@ def __getattr__(name): "PCBase", "AppContext", "AppContextManager", + "PetscToolsAppctxException", } if name in petsc4py_attrs: raise ImportError( From f0d6db2657064ab15e4fdefd0097b57cfc2179e7 Mon Sep 17 00:00:00 2001 From: Josh Hope-Collins Date: Tue, 12 May 2026 19:38:20 +0100 Subject: [PATCH 24/24] lint --- docs/source/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 7951b12..2cbe856 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -38,7 +38,7 @@ "destination": "generated", }] -# -- sphinx.ext.intersphinx configuration ------------------------------------------ +# -- sphinx.ext.intersphinx configuration ------------------------------------- intersphinx_mapping = { 'petsc4py': ('https://petsc.org/release/petsc4py/', None),