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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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/13] 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