From 2e0dd2c49f18457815e0c7623d376735d48ff903 Mon Sep 17 00:00:00 2001 From: JHopeCollins Date: Thu, 6 Nov 2025 11:39:35 +0000 Subject: [PATCH 1/8] minimal pcbase class --- petsctools/__init__.py | 2 + petsctools/pc.py | 96 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 petsctools/pc.py diff --git a/petsctools/__init__.py b/petsctools/__init__.py index 878b76b..edb4c39 100644 --- a/petsctools/__init__.py +++ b/petsctools/__init__.py @@ -38,6 +38,7 @@ inserted_options, set_default_parameter, ) + from .pc import PCBase # noqa: F401 else: def __getattr__(name): @@ -60,6 +61,7 @@ def __getattr__(name): "is_set_from_options", "inserted_options", "set_default_parameter", + "PCBase", } if name in petsc4py_attrs: raise ImportError( diff --git a/petsctools/pc.py b/petsctools/pc.py new file mode 100644 index 0000000..60208ee --- /dev/null +++ b/petsctools/pc.py @@ -0,0 +1,96 @@ +import abc + + +def obj_name(obj): + return f"{type(obj).__module__}.{type(obj).__name__}" + + +class PCBase(abc.ABC): + needs_python_amat = False + """Set this to True if the A matrix needs to be Python (matfree).""" + + needs_python_pmat = False + """Set this to False if the P matrix needs to be Python (matfree).""" + + def __init__(self): + self.initialized = False + + def setUp(self, pc): + """Called by PETSc to update the PC. + + The first time ``setUp`` is called, the ``initialize`` method will be + called followed by the ``update`` method. In subsequent calls to + ``setUp`` only the ``update`` method will be called. + """ + if not self.initialized: + if pc.getType() != "python": + raise ValueError("Expecting PC type python") + + A, P = pc.getOperators() + pcname = f"{type(self).__module__}.{type(self).__name__}" + if self.needs_python_amat: + atype = A.getType() + if atype != "python": + raise ValueError( + f"PC {pcname} needs a python type amat, not {atype}") + self.amat = A.getPythonContext() + if self.needs_python_pmat: + ptype = P.getType() + if ptype != "python": + raise ValueError( + f"PC {pcname} needs a python type pmat, not {ptype}") + self.pmat = P.getPythonContext() + + self.parent_prefix = pc.getOptionsPrefix() or "" + self.full_prefix = self.parent_prefix + self.prefix + + self.initialize(pc) + self.initialized = True + + self.update(pc) + + @abc.abstractmethod + def initialize(self, pc): + """Initialize any state in this preconditioner. + + This method is only called on the first time that the ``setUp`` + method is called. + """ + pass + + @abc.abstractmethod + def update(self, pc): + """Update any state in this preconditioner. + + This method is called every time that the ``setUp`` method is called. + """ + pass + + @abc.abstractmethod + def apply(self, pc, x, y): + """Apply the preconditioner to x, putting the result in y. + + Both x and y are PETSc Vecs, y is not guaranteed to be zero on entry. + """ + pass + + def applyTranspose(self, pc, x, y): + """Apply the preconditioner transpose to x, putting the result in y. + + Both x and y are PETSc Vecs, y is not guaranteed to be zero on entry. + """ + raise NotImplementedError( + "Need to implement the transpose action of this PC") + + def view(self, pc, viewer=None): + """Write a basic description of this PC. + """ + from petsc4py import PETSc + if viewer is None: + return + typ = viewer.getType() + if typ != PETSc.Viewer.Type.ASCII: + return + pcname = f"{type(self).__module__}.{type(self).__name__}" + viewer.printfASCII( + f"Python type preconditioner {pcname}\n") From 6cc28792a875d6fa2778301f9b3f31c864a9027a Mon Sep 17 00:00:00 2001 From: Josh Hope-Collins Date: Fri, 7 Nov 2025 10:41:41 +0000 Subject: [PATCH 2/8] Use Mat.type not Mat.getType --- petsctools/pc.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/petsctools/pc.py b/petsctools/pc.py index 60208ee..aa44214 100644 --- a/petsctools/pc.py +++ b/petsctools/pc.py @@ -29,13 +29,13 @@ def setUp(self, pc): A, P = pc.getOperators() pcname = f"{type(self).__module__}.{type(self).__name__}" if self.needs_python_amat: - atype = A.getType() + atype = A.type if atype != "python": raise ValueError( f"PC {pcname} needs a python type amat, not {atype}") self.amat = A.getPythonContext() if self.needs_python_pmat: - ptype = P.getType() + ptype = P.type if ptype != "python": raise ValueError( f"PC {pcname} needs a python type pmat, not {ptype}") From 6e8b75dd704d8984728a2e15cbe9635732b8a51c Mon Sep 17 00:00:00 2001 From: Josh Hope-Collins Date: Fri, 7 Nov 2025 11:32:20 +0000 Subject: [PATCH 3/8] Apply suggestions from code review Co-authored-by: Connor Ward --- petsctools/pc.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/petsctools/pc.py b/petsctools/pc.py index aa44214..8fac0e1 100644 --- a/petsctools/pc.py +++ b/petsctools/pc.py @@ -29,16 +29,14 @@ def setUp(self, pc): A, P = pc.getOperators() pcname = f"{type(self).__module__}.{type(self).__name__}" if self.needs_python_amat: - atype = A.type - if atype != "python": + if A.type != "python": raise ValueError( - f"PC {pcname} needs a python type amat, not {atype}") + f"PC {pcname} needs a python type amat, not {A.type}") self.amat = A.getPythonContext() if self.needs_python_pmat: - ptype = P.type - if ptype != "python": + if P.type != "python": raise ValueError( - f"PC {pcname} needs a python type pmat, not {ptype}") + f"PC {pcname} needs a python type pmat, not {P.type}") self.pmat = P.getPythonContext() self.parent_prefix = pc.getOptionsPrefix() or "" From 4e357c5750a39c324a70e329d083fe3e4ce3c941 Mon Sep 17 00:00:00 2001 From: Josh Hope-Collins Date: Fri, 7 Nov 2025 14:13:53 +0000 Subject: [PATCH 4/8] pcbase docstring --- petsctools/pc.py | 58 ++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 54 insertions(+), 4 deletions(-) diff --git a/petsctools/pc.py b/petsctools/pc.py index 8fac0e1..bf806ca 100644 --- a/petsctools/pc.py +++ b/petsctools/pc.py @@ -6,6 +6,53 @@ def obj_name(obj): class PCBase(abc.ABC): + """Abstract base class for python type PETSc PCs. + + This is a convenience base class that provides two common functionalities + for python type preconditioners. + + 1. Checking whether the PC operators are of python type. + Often a python type preconditioner will rely on the Mat operators + also being of python type. If the ``needs_python_amat`` and/or + ``needs_python_pmat`` attributes are set then the type of the + ``A, P = pc.getOperators()`` will be checked, and an error raised + if they are not python type. + If they are python type then their python contexts will be added + as the attributes ``amat`` and/or ``pmat`` (e.g. A.getPythonContext()). + + 2. Separating code to initialize and update the preconditioner. + Often there are operations to set up the preconditioner which only + need to be run once, and operations that are needed each time the + preconditioner is updated. The ``setUp`` method will call the user + implemented ``initialize`` method only on the first time it is called, + but will call the ``update`` method on every subsequent call. + + Inheriting classes should also set the ``prefix`` attribute. + The attributes ``parent_prefix`` and ``full_prefix`` will then be set, + where ``parent_prefix`` is the unqualified pc prefix and ``full_prefix`` + is the qualified prefix of this context (i.e. pc.getOptionsPrefix() and + parent_prefix+self.prefix). + + Inheriting classes should implement the following methods: + * initialize + * update + * apply + + They should also set the following class attributes: + * prefix + * needs_python_amat (optional, defaults to False). + * needs_python_pmat (optional, defaults to False). + + Notes + ----- + The ``update`` method is not called on the first call to setUp(), so for + some preconditioners it may be necessary to call ``update`` at the end of + the ``initialize`` method. + + If the ``prefix`` attribute does not end in an underscore ("_") then one + will automatically be appended to the ``full_prefix`` attribute. + """ + needs_python_amat = False """Set this to True if the A matrix needs to be Python (matfree).""" @@ -22,7 +69,9 @@ def setUp(self, pc): called followed by the ``update`` method. In subsequent calls to ``setUp`` only the ``update`` method will be called. """ - if not self.initialized: + if self.initialized: + self.update(pc) + else: if pc.getType() != "python": raise ValueError("Expecting PC type python") @@ -41,12 +90,12 @@ def setUp(self, pc): self.parent_prefix = pc.getOptionsPrefix() or "" self.full_prefix = self.parent_prefix + self.prefix + if not self.full_prefix.endswith("_"): + self.full_prefix += "_" self.initialize(pc) self.initialized = True - self.update(pc) - @abc.abstractmethod def initialize(self, pc): """Initialize any state in this preconditioner. @@ -60,7 +109,8 @@ def initialize(self, pc): def update(self, pc): """Update any state in this preconditioner. - This method is called every time that the ``setUp`` method is called. + This method is called the on second and later times that the + ``setUp`` method is called. """ pass From 6ad7863a00dfa862ff5975eb27e35171634ef403 Mon Sep 17 00:00:00 2001 From: Josh Hope-Collins Date: Fri, 7 Nov 2025 14:18:43 +0000 Subject: [PATCH 5/8] pcbase docstring --- petsctools/pc.py | 36 +++++++++++++++++++----------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/petsctools/pc.py b/petsctools/pc.py index bf806ca..0abbb1c 100644 --- a/petsctools/pc.py +++ b/petsctools/pc.py @@ -12,15 +12,17 @@ class PCBase(abc.ABC): for python type preconditioners. 1. Checking whether the PC operators are of python type. + Often a python type preconditioner will rely on the Mat operators also being of python type. If the ``needs_python_amat`` and/or ``needs_python_pmat`` attributes are set then the type of the - ``A, P = pc.getOperators()`` will be checked, and an error raised - if they are not python type. - If they are python type then their python contexts will be added - as the attributes ``amat`` and/or ``pmat`` (e.g. A.getPythonContext()). + ``pc.getOperators()`` will be checked, and an error raised if they + are not python type. If they are python type then their python contexts + will be added as the attributes ``amat`` and/or ``pmat`` (e.g. + ``A.getPythonContext()``). 2. Separating code to initialize and update the preconditioner. + Often there are operations to set up the preconditioner which only need to be run once, and operations that are needed each time the preconditioner is updated. The ``setUp`` method will call the user @@ -30,27 +32,27 @@ class PCBase(abc.ABC): Inheriting classes should also set the ``prefix`` attribute. The attributes ``parent_prefix`` and ``full_prefix`` will then be set, where ``parent_prefix`` is the unqualified pc prefix and ``full_prefix`` - is the qualified prefix of this context (i.e. pc.getOptionsPrefix() and - parent_prefix+self.prefix). + is the qualified prefix of this context (i.e. ``pc.getOptionsPrefix()`` + and ``parent_prefix+self.prefix``). Inheriting classes should implement the following methods: - * initialize - * update - * apply + - ``initialize`` + - ``update`` + - ``apply`` They should also set the following class attributes: - * prefix - * needs_python_amat (optional, defaults to False). - * needs_python_pmat (optional, defaults to False). + - ``prefix`` + - ``needs_python_amat`` (optional, defaults to False). + - ``needs_python_pmat`` (optional, defaults to False). Notes ----- - The ``update`` method is not called on the first call to setUp(), so for - some preconditioners it may be necessary to call ``update`` at the end of - the ``initialize`` method. + The ``update`` method is not called on the first call to ``setUp()``, so + for some preconditioners it may be necessary to call ``update`` at the end + of the ``initialize`` method. - If the ``prefix`` attribute does not end in an underscore ("_") then one - will automatically be appended to the ``full_prefix`` attribute. + If the ``prefix`` attribute does not end in an underscore (``"_"``) then + one will automatically be appended to the ``full_prefix`` attribute. """ needs_python_amat = False From 2e5189069e538cd9252a75cd4e418464c8ea128c Mon Sep 17 00:00:00 2001 From: JHopeCollins Date: Fri, 7 Nov 2025 14:26:47 +0000 Subject: [PATCH 6/8] pcbase docstring --- petsctools/pc.py | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/petsctools/pc.py b/petsctools/pc.py index 0abbb1c..1133276 100644 --- a/petsctools/pc.py +++ b/petsctools/pc.py @@ -1,4 +1,5 @@ import abc +from .exceptions import PetscToolsException def obj_name(obj): @@ -36,14 +37,16 @@ class PCBase(abc.ABC): and ``parent_prefix+self.prefix``). Inheriting classes should implement the following methods: - - ``initialize`` - - ``update`` - - ``apply`` + + * ``initialize`` + * ``update`` + * ``apply`` They should also set the following class attributes: - - ``prefix`` - - ``needs_python_amat`` (optional, defaults to False). - - ``needs_python_pmat`` (optional, defaults to False). + + * ``prefix`` + * ``needs_python_amat`` (optional, defaults to False). + * ``needs_python_pmat`` (optional, defaults to False). Notes ----- @@ -61,6 +64,9 @@ class PCBase(abc.ABC): needs_python_pmat = False """Set this to False if the P matrix needs to be Python (matfree).""" + prefix = None + """The options prefix of this PC.""" + def __init__(self): self.initialized = False @@ -75,21 +81,25 @@ def setUp(self, pc): self.update(pc) else: if pc.getType() != "python": - raise ValueError("Expecting PC type python") + raise PetscToolsException("Expecting PC type python") A, P = pc.getOperators() pcname = f"{type(self).__module__}.{type(self).__name__}" if self.needs_python_amat: if A.type != "python": - raise ValueError( + raise PetscToolsException( f"PC {pcname} needs a python type amat, not {A.type}") self.amat = A.getPythonContext() if self.needs_python_pmat: if P.type != "python": - raise ValueError( + raise PetscToolsException( f"PC {pcname} needs a python type pmat, not {P.type}") self.pmat = P.getPythonContext() + if not isinstance(self.prefix, str): + raise PetscToolsException( + f"{pcname}.prefix must be a str not {type(self.prefix)}") + self.parent_prefix = pc.getOptionsPrefix() or "" self.full_prefix = self.parent_prefix + self.prefix if not self.full_prefix.endswith("_"): From d8942c71d5206fe11a22a19541144bb00b020208 Mon Sep 17 00:00:00 2001 From: JHopeCollins Date: Fri, 7 Nov 2025 14:30:25 +0000 Subject: [PATCH 7/8] pcbase note that update can often be a no-op --- petsctools/pc.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/petsctools/pc.py b/petsctools/pc.py index 1133276..76593c2 100644 --- a/petsctools/pc.py +++ b/petsctools/pc.py @@ -123,6 +123,9 @@ def update(self, pc): This method is called the on second and later times that the ``setUp`` method is called. + + This method is not needed for all preconditioners and can often + be a no-op. """ pass From 24a5e9db070fc8f4a1ed1a450c4c5a2307753c19 Mon Sep 17 00:00:00 2001 From: Josh Hope-Collins Date: Fri, 7 Nov 2025 14:54:07 +0000 Subject: [PATCH 8/8] Remove unused convenience function --- petsctools/pc.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/petsctools/pc.py b/petsctools/pc.py index 76593c2..ea4e443 100644 --- a/petsctools/pc.py +++ b/petsctools/pc.py @@ -2,10 +2,6 @@ from .exceptions import PetscToolsException -def obj_name(obj): - return f"{type(obj).__module__}.{type(obj).__name__}" - - class PCBase(abc.ABC): """Abstract base class for python type PETSc PCs.