# Copyright (C) 2018-2025 Garth N. Wells, Nathan Sime and Jørgen S. Dokken
#
# This file is part of DOLFINx (https://www.fenicsproject.org)
#
# SPDX-License-Identifier: LGPL-3.0-or-later
"""High-level solver classes and functions for assembling PETSc objects.
Functions in this module generally apply functions in :mod:`dolfinx.fem`
to PETSc linear algebra objects and handle any PETSc-specific
preparation.
Note:
The following does not apply to the high-level classes
:class:`dolfinx.fem.petsc.LinearProblem`
:class:`dolfinx.fem.petsc.NonlinearProblem`.
Due to subtle issues in the interaction between petsc4py memory
management and the Python garbage collector, it is recommended that
the PETSc method ``destroy()`` is called on returned PETSc objects
once the object is no longer required. Note that ``destroy()`` is
collective over the object's MPI communicator.
"""
from __future__ import annotations
import contextlib
import ctypes as _ctypes
import functools
import os
import pathlib
from collections.abc import Sequence
from petsc4py import PETSc
# ruff: noqa: E402
import dolfinx
assert dolfinx.has_petsc4py
import warnings
from functools import partial
import numpy as np
from numpy import typing as npt
import dolfinx.cpp as _cpp
import dolfinx.la.petsc
import ufl
from dolfinx.cpp.fem.petsc import discrete_curl as _discrete_curl
from dolfinx.cpp.fem.petsc import discrete_gradient as _discrete_gradient
from dolfinx.cpp.fem.petsc import interpolation_matrix as _interpolation_matrix
from dolfinx.fem import IntegralType, pack_coefficients, pack_constants
from dolfinx.fem.assemble import _assemble_vector_array
from dolfinx.fem.assemble import apply_lifting as _apply_lifting
from dolfinx.fem.bcs import DirichletBC
from dolfinx.fem.bcs import bcs_by_block as _bcs_by_block
from dolfinx.fem.forms import Form, derivative_block
from dolfinx.fem.forms import extract_function_spaces as _extract_function_spaces
from dolfinx.fem.forms import form as _create_form
from dolfinx.fem.function import Function as _Function
from dolfinx.fem.function import FunctionSpace as _FunctionSpace
from dolfinx.mesh import EntityMap as _EntityMap
__all__ = [
"LinearProblem",
"NewtonSolverNonlinearProblem",
"NonlinearProblem",
"apply_lifting",
"assemble_jacobian",
"assemble_matrix",
"assemble_residual",
"assemble_vector",
"assign",
"cffi_utils",
"create_matrix",
"create_vector",
"ctypes_utils",
"discrete_curl",
"discrete_gradient",
"interpolation_matrix",
"numba_utils",
"set_bc",
]
# -- Vector instantiation -------------------------------------------------
[docs]
def create_vector(
V: _FunctionSpace | Sequence[_FunctionSpace | None],
/,
kind: str | None = None,
) -> PETSc.Vec: # type: ignore[name-defined]
"""Create a PETSc vector that is compatible with a linear form(s)
or functionspace(s).
Three cases are supported:
1. For a single space ``V``, if ``kind`` is ``None`` or is
``PETSc.Vec.Type.MPI``, a ghosted PETSc vector which is compatible
with ``V`` is created.
2. If ``V`` is a sequence of functionspaces and ``kind`` is ``None`` or
is ``PETSc.Vec.Type.MPI``, a ghosted PETSc vector which is
compatible with ``V`` is created. The created vector ``b``
is initialized such that on each MPI process ``b = [b_0, b_1, ...,
b_n, b_0g, b_1g, ..., b_ng]``, where ``b_i`` are the entries
associated with the 'owned' degrees-of-freedom for ``V[i]`` and
``b_ig`` are the 'unowned' (ghost) entries for ``V[i]``.
For this case, the returned vector has an attribute ``_blocks``
that holds the local offsets into ``b`` for the (i) owned and
(ii) ghost entries for each ``V_i``. It can be accessed by
``b.getAttr("_blocks")``. The offsets can be used to get views
into ``b`` for blocks, e.g.::
>>> offsets0, offsets1, = b.getAttr("_blocks")
>>> offsets0
(0, 12, 28)
>>> offsets1
(28, 32, 35)
>>> b0_owned = b.array[offsets0[0]:offsets0[1]]
>>> b0_ghost = b.array[offsets1[0]:offsets1[1]]
>>> b1_owned = b.array[offsets0[1]:offsets0[2]]
>>> b1_ghost = b.array[offsets1[1]:offsets1[2]]
3. If ``L/V`` is a sequence of linear forms/functionspaces and ``kind``
is ``PETSc.Vec.Type.NEST``, a PETSc nested vector (a 'nest' of
ghosted PETSc vectors) which is compatible with ``L/V`` is created.
Args:
V: Function space or a sequence of such.
kind: PETSc vector type (``VecType``) to create.
Returns:
A PETSc vector with a layout that is compatible with ``V``. The
vector is not initialised to zero.
"""
if isinstance(
V, _FunctionSpace | _cpp.fem.FunctionSpace_float32 | _cpp.fem.FunctionSpace_float64
):
V = [V]
elif any(_V is None for _V in V):
raise RuntimeError("Can not create vector for None block.")
maps = [(_V.dofmap.index_map, _V.dofmap.index_map_bs) for _V in V] # type: ignore
return dolfinx.la.petsc.create_vector(maps, kind=kind)
# -- Matrix instantiation -------------------------------------------------
[docs]
def create_matrix(
a: Form | Sequence[Sequence[Form]],
kind: str | Sequence[Sequence[str]] | None = None,
) -> PETSc.Mat: # type: ignore[name-defined]
"""Create a PETSc matrix that is compatible with the (sequence) of
bilinear form(s).
Three cases are supported:
1. For a single bilinear form, it creates a compatible PETSc matrix
of type ``kind``.
2. For a rectangular array of bilinear forms, if ``kind`` is
``PETSc.Mat.Type.NEST`` or ``kind`` is an array of PETSc ``Mat``
types (with the same shape as ``a``), a matrix of type
``PETSc.Mat.Type.NEST`` is created. The matrix is compatible
with the forms ``a``.
3. For a rectangular array of bilinear forms, it create a single
(non-nested) matrix of type ``kind`` that is compatible with the
array of for forms ``a``. If ``kind`` is ``None``, then the
matrix is the default type.
In this case, the matrix is arranged::
A = [a_00 ... a_0n]
[a_10 ... a_1n]
[ ... ]
[a_m0 .. a_mn]
Args:
a: A bilinear form or a nested sequence of bilinear forms.
kind: The PETSc matrix type (``MatType``).
Returns:
A PETSc matrix.
"""
if isinstance(a, Sequence):
_a = [[None if form is None else form._cpp_object for form in arow] for arow in a]
if kind == PETSc.Mat.Type.NEST: # type: ignore[attr-defined]
# Create nest matrix with default types
return _cpp.fem.petsc.create_matrix_nest(_a, None)
else:
if kind is None or isinstance(kind, str): # Single 'kind' type
return _cpp.fem.petsc.create_matrix_block(_a, kind)
else: # Array of 'kind' types
return _cpp.fem.petsc.create_matrix_nest(_a, kind)
else: # Single form
return _cpp.fem.petsc.create_matrix(a._cpp_object, kind)
# -- Vector assembly ------------------------------------------------------
[docs]
@functools.singledispatch
def assemble_vector(
L: Form | Sequence[Form],
constants: npt.NDArray | Sequence[npt.NDArray] | None = None,
coeffs: (
dict[tuple[IntegralType, int], npt.NDArray]
| Sequence[dict[tuple[IntegralType, int], npt.NDArray]]
| None
) = None,
kind: str | None = None,
) -> PETSc.Vec: # type: ignore[name-defined]
"""Assemble linear form(s) into a new PETSc vector.
Three cases are supported:
1. If ``L`` is a single linear form, the form is assembled into a
ghosted PETSc vector.
2. If ``L`` is a sequence of linear forms and ``kind`` is ``None``
or is ``PETSc.Vec.Type.MPI``, the forms are assembled into a
vector ``b`` such that ``b = [b_0, b_1, ..., b_n, b_0g, b_1g,
..., b_ng]`` where ``b_i`` are the entries associated with the
'owned' degrees-of-freedom for ``L[i]`` and ``b_ig`` are the
'unowned' (ghost) entries for ``L[i]``.
For this case, the returned vector has an attribute ``_blocks``
that holds the local offsets into ``b`` for the (i) owned and
(ii) ghost entries for each ``L[i]``. See :func:`create_vector`
for a description of the offset blocks.
3. If ``L`` is a sequence of linear forms and ``kind`` is
``PETSc.Vec.Type.NEST``, the forms are assembled into a PETSc
nested vector ``b`` (a nest of ghosted PETSc vectors) such that
``L[i]`` is assembled into into the ith nested matrix in ``b``.
Constant and coefficient data that appear in the forms(s) can be
packed outside of this function to avoid re-packing by this
function. The functions :func:`dolfinx.fem.pack_constants` and
:func:`dolfinx.fem.pack_coefficients` can be used to 'pre-pack' the
data.
Note:
The returned vector is not finalised, i.e. ghost values are not
accumulated on the owning processes.
Args:
L: A linear form or sequence of linear forms.
constants: Constants appearing in the form. For a single form,
``constants.ndim==1``. For multiple forms, the constants for
form ``L[i]`` are ``constants[i]``.
coeffs: Coefficients appearing in the form. For a single form,
``coeffs.shape=(num_cells, n)``. For multiple forms, the
coefficients for form ``L[i]`` are ``coeffs[i]``.
kind: PETSc vector type.
Returns:
An assembled vector.
"""
b = create_vector(_extract_function_spaces(L), kind=kind) # type: ignore
dolfinx.la.petsc._zero_vector(b)
return assemble_vector(b, L, constants, coeffs) # type: ignore[arg-type]
@assemble_vector.register
def _(
b: PETSc.Vec, # type: ignore[name-defined]
L: Form | Sequence[Form],
constants: npt.NDArray | Sequence[npt.NDArray] | None = None,
coeffs: (
dict[tuple[IntegralType, int], npt.NDArray]
| Sequence[dict[tuple[IntegralType, int], npt.NDArray]]
| None
) = None,
) -> PETSc.Vec: # type: ignore[name-defined]
"""Assemble linear form(s) into a PETSc vector.
The vector ``b`` must have been initialized with a size/layout that
is consistent with the linear form. The vector ``b`` is normally
created by :func:`create_vector`.
Constants and coefficients that appear in the forms(s) can be passed
to avoid re-computation of constants and coefficients. The functions
:func:`dolfinx.fem.assemble.pack_constants` and
:func:`dolfinx.fem.assemble.pack_coefficients` can be called.
Note:
The vector is not zeroed before assembly and it is not
finalised, i.e. ghost values are not accumulated on the owning
processes.
Args:
b: Vector to assemble the contribution of the linear form into.
L: A linear form or sequence of linear forms to assemble into
``b``.
constants: Constants appearing in the form. For a single form,
``constants.ndim==1``. For multiple forms, the constants for
form ``L[i]`` are ``constants[i]``.
coeffs: Coefficients appearing in the form. For a single form,
``coeffs.shape=(num_cells, n)``. For multiple forms, the
coefficients for form ``L[i]`` are ``coeffs[i]``.
Returns:
Assembled vector.
"""
if b.getType() == PETSc.Vec.Type.NEST: # type: ignore[attr-defined]
if not isinstance(L, Sequence):
raise ValueError("Must provide a sequence of forms when assembling a nest vector")
constants = [None] * len(L) if constants is None else constants # type: ignore[list-item]
coeffs = [None] * len(L) if coeffs is None else coeffs # type: ignore[list-item]
for b_sub, L_sub, const, coeff in zip(b.getNestSubVecs(), L, constants, coeffs):
with b_sub.localForm() as b_local:
_assemble_vector_array(b_local.array_w, L_sub, const, coeff) # type: ignore[arg-type]
elif isinstance(L, Sequence):
constants = pack_constants(L) if constants is None else constants
coeffs = pack_coefficients(L) if coeffs is None else coeffs
offset0, offset1 = b.getAttr("_blocks")
with b.localForm() as b_l:
for L_, const, coeff, off0, off1, offg0, offg1 in zip(
L, constants, coeffs, offset0, offset0[1:], offset1, offset1[1:]
):
bx_ = np.zeros((off1 - off0) + (offg1 - offg0), dtype=PETSc.ScalarType) # type: ignore[attr-defined]
_assemble_vector_array(bx_, L_, const, coeff) # type: ignore[arg-type]
size = off1 - off0
b_l.array_w[off0:off1] += bx_[:size]
b_l.array_w[offg0:offg1] += bx_[size:]
else:
with b.localForm() as b_local:
_assemble_vector_array(b_local.array_w, L, constants, coeffs) # type: ignore[arg-type]
return b
# -- Matrix assembly ------------------------------------------------------
[docs]
@functools.singledispatch
def assemble_matrix(
a: Form | Sequence[Sequence[Form]],
bcs: Sequence[DirichletBC] | None = None,
diag: float = 1,
constants: Sequence[npt.NDArray] | Sequence[Sequence[npt.NDArray]] | None = None,
coeffs: (
dict[tuple[IntegralType, int], npt.NDArray]
| Sequence[dict[tuple[IntegralType, int], npt.NDArray]]
| None
) = None,
kind=None,
):
"""Assemble a bilinear form into a matrix.
The following cases are supported:
1. If ``a`` is a single bilinear form, the form is assembled
into PETSc matrix of type ``kind``.
#. If ``a`` is a :math:`m \\times n` rectangular array of forms the
forms in ``a`` are assembled into a matrix such that::
A = [A_00 ... A_0n]
[A_10 ... A_1n]
[ ... ]
[A_m0 .. A_mn]
where ``A_ij`` is the matrix associated with the form
``a[i][j]``.
a. If ``kind`` is a ``PETSc.Mat.Type`` (other than
``PETSc.Mat.Type.NEST``) or is ``None``, the matrix type is
``kind`` of the default type (if ``kind`` is ``None``).
#. If ``kind`` is ``PETSc.Mat.Type.NEST`` or a rectangular array
of PETSc matrix types, the returned matrix has type
``PETSc.Mat.Type.NEST``.
Rows/columns that are constrained by a Dirichlet boundary condition
are zeroed, with the diagonal to set to ``diag``.
Constant and coefficient data that appear in the forms(s) can be
packed outside of this function to avoid re-packing by this
function. The functions :func:`dolfinx.fem.pack_constants` and
:func:`dolfinx.fem.pack_coefficients` can be used to 'pre-pack' the
data.
Note:
The returned matrix is not 'assembled', i.e. ghost contributions
are not accumulated.
Args:
a: Bilinear form(s) to assembled into a matrix.
bc: Dirichlet boundary conditions applied to the system.
diag: Value to set on the matrix diagonal for Dirichlet
boundary condition constrained degrees-of-freedom belonging
to the same trial and test space.
constants: Constants appearing the in the form.
coeffs: Coefficients appearing the in the form.
Returns:
Matrix representing the bilinear form.
"""
A = create_matrix(a, kind)
assemble_matrix(A, a, bcs, diag, constants, coeffs) # type: ignore[arg-type]
return A
@assemble_matrix.register
def _(
A: PETSc.Mat, # type: ignore[name-defined]
a: Form | Sequence[Sequence[Form]],
bcs: Sequence[DirichletBC] | None = None,
diag: float = 1,
constants: npt.NDArray | Sequence[Sequence[npt.NDArray]] | None = None,
coeffs: (
dict[tuple[IntegralType, int], npt.NDArray]
| Sequence[Sequence[dict[tuple[IntegralType, int], npt.NDArray]]]
| None
) = None,
) -> PETSc.Mat: # type: ignore[name-defined]
"""Assemble bilinear form into a matrix.
The matrix vector ``A`` must have been initialized with a
size/layout that is consistent with the bilinear form(s). The PETSc
matrix ``A`` is normally created by :func:`create_matrix`.
The returned matrix is not finalised, i.e. ghost values are not
accumulated.
"""
if A.getType() == PETSc.Mat.Type.NEST: # type: ignore[attr-defined]
if not isinstance(a, Sequence):
raise ValueError("Must provide a sequence of forms when assembling a nest matrix")
constants = [pack_constants(forms) for forms in a] if constants is None else constants # type: ignore[misc]
coeffs = [pack_coefficients(forms) for forms in a] if coeffs is None else coeffs # type: ignore[misc]
for i, (a_row, const_row, coeff_row) in enumerate(zip(a, constants, coeffs)):
for j, (a_block, const, coeff) in enumerate(zip(a_row, const_row, coeff_row)):
if a_block is not None:
Asub = A.getNestSubMatrix(i, j)
assemble_matrix(Asub, a_block, bcs, diag, const, coeff) # type: ignore[arg-type]
elif i == j:
for bc in bcs:
row_forms = [row_form for row_form in a_row if row_form is not None]
assert len(row_forms) > 0
if row_forms[0].function_spaces[0].contains(bc.function_space):
raise RuntimeError(
f"Diagonal sub-block ({i}, {j}) cannot be 'None'"
" and have DirichletBC applied."
" Consider assembling a zero block."
)
elif isinstance(a, Sequence): # Blocked
consts = [pack_constants(forms) for forms in a] if constants is None else constants # type: ignore[misc]
coeffs = [pack_coefficients(forms) for forms in a] if coeffs is None else coeffs # type: ignore[misc]
V = (_extract_function_spaces(a, 0), _extract_function_spaces(a, 1))
for index in range(2):
# the check below is to ensure that a .dofmaps attribute is
# available when creating is0 and is1 below
Vi = V[index]
assert isinstance(Vi, list)
if all(Vsub is None for Vsub in Vi):
raise ValueError(
"Cannot have a entire {'row' if index == 0 else 'column'} of a full of None"
)
is0 = _cpp.la.petsc.create_index_sets(
[(Vsub.dofmaps(0).index_map, Vsub.dofmaps(0).index_map_bs) for Vsub in V[0]] # type: ignore[union-attr]
)
is1 = _cpp.la.petsc.create_index_sets(
[(Vsub.dofmaps(0).index_map, Vsub.dofmaps(0).index_map_bs) for Vsub in V[1]] # type: ignore[union-attr]
)
_bcs = [bc._cpp_object for bc in bcs] if bcs is not None else []
for i, a_row in enumerate(a):
for j, a_sub in enumerate(a_row):
if a_sub is not None:
Asub = A.getLocalSubMatrix(is0[i], is1[j])
_cpp.fem.petsc.assemble_matrix(
Asub,
a_sub._cpp_object,
consts[i][j],
coeffs[i][j], # type: ignore[index]
_bcs,
True,
)
A.restoreLocalSubMatrix(is0[i], is1[j], Asub)
elif i == j:
for bc in _bcs:
row_forms = [row_form for row_form in a_row if row_form is not None]
assert len(row_forms) > 0
if row_forms[0].function_spaces[0].contains(bc.function_space):
raise RuntimeError(
f"Diagonal sub-block ({i}, {j}) cannot be 'None' "
" and have DirichletBC applied."
" Consider assembling a zero block."
)
# Flush to enable switch from add to set in the matrix
A.assemble(PETSc.Mat.AssemblyType.FLUSH) # type: ignore[attr-defined]
# Set diagonal
for i, a_row in enumerate(a):
for j, a_sub in enumerate(a_row):
if a_sub is not None:
Asub = A.getLocalSubMatrix(is0[i], is1[j])
if a_sub.function_spaces[0] is a_sub.function_spaces[1]:
_cpp.fem.petsc.insert_diagonal(Asub, a_sub.function_spaces[0], _bcs, diag)
A.restoreLocalSubMatrix(is0[i], is1[j], Asub)
else: # Non-blocked
constants = pack_constants(a) if constants is None else constants # type: ignore[assignment]
coeffs = pack_coefficients(a) if coeffs is None else coeffs # type: ignore[assignment]
_bcs = [bc._cpp_object for bc in bcs] if bcs is not None else []
_cpp.fem.petsc.assemble_matrix(A, a._cpp_object, constants, coeffs, _bcs)
if a.function_spaces[0] is a.function_spaces[1]:
A.assemblyBegin(PETSc.Mat.AssemblyType.FLUSH) # type: ignore[attr-defined]
A.assemblyEnd(PETSc.Mat.AssemblyType.FLUSH) # type: ignore[attr-defined]
_cpp.fem.petsc.insert_diagonal(A, a.function_spaces[0], _bcs, diag)
return A
# -- Modifiers for Dirichlet conditions -----------------------------------
[docs]
def apply_lifting(
b: PETSc.Vec, # type: ignore[name-defined]
a: Sequence[Form] | Sequence[Sequence[Form]],
bcs: Sequence[DirichletBC] | Sequence[Sequence[DirichletBC]] | None,
x0: Sequence[PETSc.Vec] | None = None, # type: ignore[name-defined]
alpha: float = 1,
constants: Sequence[npt.NDArray] | Sequence[Sequence[npt.NDArray]] | None = None,
coeffs: (
dict[tuple[IntegralType, int], npt.NDArray]
| Sequence[Sequence[dict[tuple[IntegralType, int], npt.NDArray]]]
| None
) = None,
) -> None:
r"""Modify the right-hand side PETSc vector ``b`` to account for
constraints (Dirichlet boundary conitions).
See :func:`dolfinx.fem.apply_lifting` for a mathematical
descriptions of the lifting operation.
Args:
b: Vector to modify in-place.
a: List of bilinear forms. If ``b`` is not blocked or a nest,
then ``a`` is a 1D sequence. If ``b`` is blocked or a nest,
then ``a`` is a 2D array of forms, with the ``a[i]`` forms
used to modify the block/nest vector ``b[i]``.
bcs: Boundary conditions to apply, which form a 2D array.
If ``b`` is nested or blocked then ``bcs[i]`` are the
boundary conditions to apply to block/nest ``i``.
The function :func:`dolfinx.fem.bcs_by_block` can be
used to prepare the 2D array of ``DirichletBC`` objects
from the 2D sequence ``a``::
bcs1 = fem.bcs_by_block(
fem.extract_function_spaces(a, 1),
bcs
)
If ``b`` is not blocked or nest, then ``len(bcs)`` must be
equal to 1. The function :func:`dolfinx.fem.bcs_by_block`
can be used to prepare the 2D array of ``DirichletBC``
from the 1D sequence ``a``::
bcs1 = fem.bcs_by_block(
fem.extract_function_spaces([a], 1),
bcs
)
x0: Vector to use in modify ``b`` (see
:func:`dolfinx.fem.apply_lifting`). Treated as zero if
``None``.
alpha: Scalar parameter in lifting (see
:func:`dolfinx.fem.apply_lifting`).
constants: Packed constant data appearing in the forms ``a``. If
``None``, the constant data will be packed by the function.
coeffs: Packed coefficient data appearing in the forms ``a``. If
``None``, the coefficient data will be packed by the
function.
Note:
Ghost contributions are not accumulated (not sent to owner).
Caller is responsible for reverse-scatter to update the ghosts.
Note:
Boundary condition values are *not* set in ``b`` by this
function. Use :func:`dolfinx.fem.DirichletBC.set` to set values
in ``b``.
"""
if b.getType() == PETSc.Vec.Type.NEST: # type: ignore[attr-defined]
x0 = [] if x0 is None else x0.getNestSubVecs() # type: ignore[attr-defined]
constants = [pack_constants(forms) for forms in a] if constants is None else constants # type: ignore[assignment]
coeffs = [pack_coefficients(forms) for forms in a] if coeffs is None else coeffs # type: ignore[misc]
for b_sub, a_sub, const, coeff in zip(b.getNestSubVecs(), a, constants, coeffs): # type: ignore[arg-type]
const_ = list(
map(lambda x: np.array([], dtype=PETSc.ScalarType) if x is None else x, const) # type: ignore[attr-defined, call-overload]
)
apply_lifting(b_sub, a_sub, bcs, x0, alpha, const_, coeff) # type: ignore[arg-type]
else:
with contextlib.ExitStack() as stack:
if b.getAttr("_blocks") is not None:
if x0 is not None:
offset0, offset1 = x0.getAttr("_blocks") # type: ignore[attr-defined]
xl = stack.enter_context(x0.localForm()) # type: ignore[attr-defined]
xlocal = [
np.concatenate((xl[off0:off1], xl[offg0:offg1]))
for (off0, off1, offg0, offg1) in zip(
offset0, offset0[1:], offset1, offset1[1:]
)
]
else:
xlocal = None
offset0, offset1 = b.getAttr("_blocks")
with b.localForm() as b_l:
for i, (a_, off0, off1, offg0, offg1) in enumerate(
zip(a, offset0, offset0[1:], offset1, offset1[1:])
):
const = pack_constants(a_) if constants is None else constants[i] # type: ignore[arg-type]
coeff = pack_coefficients(a_) if coeffs is None else coeffs[i] # type: ignore[arg-type, assignment, index]
const_ = [
np.empty(0, dtype=PETSc.ScalarType) if val is None else val # type: ignore[attr-defined]
for val in const
]
bx_ = np.concatenate((b_l[off0:off1], b_l[offg0:offg1]))
_apply_lifting(bx_, a_, bcs, xlocal, float(alpha), const_, coeff) # type: ignore[arg-type]
size = off1 - off0
b_l.array_w[off0:off1] = bx_[:size]
b_l.array_w[offg0:offg1] = bx_[size:]
else:
x0 = [] if x0 is None else x0
x0 = [stack.enter_context(x.localForm()) for x in x0]
x0_r = [x.array_r for x in x0]
b_local = stack.enter_context(b.localForm())
_apply_lifting(b_local.array_w, a, bcs, x0_r, alpha, constants, coeffs) # type: ignore[arg-type]
return b
[docs]
def set_bc(
b: PETSc.Vec, # type: ignore[name-defined]
bcs: Sequence[DirichletBC] | Sequence[Sequence[DirichletBC]],
x0: PETSc.Vec | None = None, # type: ignore[name-defined]
alpha: float = 1,
) -> None:
r"""Set constraint (Dirchlet boundary condition) values in an vector.
For degrees-of-freedoms that are constrained by a Dirichlet boundary
condition, this function sets that degrees-of-freedom to ``alpha *
(g - x0)``, where ``g`` is the boundary condition value.
Only owned entries in ``b`` (owned by the MPI process) are modified
by this function.
Args:
b: Vector to modify by setting boundary condition values.
bcs: Boundary conditions to apply. If ``b`` is nested or
blocked, ``bcs`` is a 2D array and ``bcs[i]`` are the
boundary conditions to apply to block/nest ``i``. Otherwise
``bcs`` should be a sequence of ``DirichletBC``\s. For
block/nest problems, :func:`dolfinx.fem.bcs_by_block` can be
used to prepare the 2D array of ``DirichletBC`` objects.
x0: Vector used in the value that constrained entries are set
to. If ``None``, ``x0`` is treated as zero.
alpha: Scalar value used in the value that constrained entries
are set to.
"""
if len(bcs) == 0:
return
if not isinstance(bcs[0], Sequence):
x0 = x0.array_r if x0 is not None else None
for bc in bcs:
bc.set(b.array_w, x0, alpha) # type: ignore[union-attr]
elif b.getType() == PETSc.Vec.Type.NEST: # type: ignore[attr-defined]
_b = b.getNestSubVecs()
x0 = len(_b) * [None] if x0 is None else x0.getNestSubVecs()
for b_sub, bc, x_sub in zip(_b, bcs, x0): # type: ignore[assignment, arg-type]
set_bc(b_sub, bc, x_sub, alpha) # type: ignore[arg-type]
else: # block vector
offset0, _ = b.getAttr("_blocks")
b_array = b.getArray(readonly=False)
x_array = x0.getArray(readonly=True) if x0 is not None else None
for bcs, off0, off1 in zip(bcs, offset0, offset0[1:]): # type: ignore[assignment]
x0_sub = x_array[off0:off1] if x0 is not None else None # type: ignore[index]
for bc in bcs:
bc.set(b_array[off0:off1], x0_sub, alpha) # type: ignore[arg-type, union-attr]
# -- High-level interface for KSP ---------------------------------------
[docs]
class LinearProblem:
"""High-level class for solving a linear variational problem using
a PETSc KSP.
Solves problems of the form
:math:`a_{ij}(u, v) = f_i(v), i,j=0,\\ldots,N\\
\\forall v \\in V` where
:math:`u=(u_0,\\ldots,u_N), v=(v_0,\\ldots,v_N)`
using PETSc KSP as the linear solver.
Note:
This high-level class automatically handles PETSc memory
management. The user does not need to manually call
``.destroy()`` on returned PETSc objects.
"""
def __init__(
self,
a: ufl.Form | Sequence[Sequence[ufl.Form]],
L: ufl.Form | Sequence[ufl.Form],
*,
petsc_options_prefix: str,
bcs: Sequence[DirichletBC] | None = None,
u: _Function | Sequence[_Function] | None = None,
P: ufl.Form | Sequence[Sequence[ufl.Form]] | None = None,
kind: str | Sequence[Sequence[str]] | None = None,
petsc_options: dict | None = None,
form_compiler_options: dict | None = None,
jit_options: dict | None = None,
entity_maps: Sequence[_EntityMap] | None = None,
) -> None:
"""Initialize solver for a linear variational problem.
By default, the underlying KSP solver uses PETSc's default
options, usually GMRES + ILU preconditioning. To use the robust
combination of LU via MUMPS
Example::
problem = LinearProblem(a, L, bcs=[bc0, bc1],
petsc_options_prefix="basic_linear_problem",
petsc_options= {
"ksp_type": "preonly",
"pc_type": "lu",
"pc_factor_mat_solver_type": "mumps"
})
This class also supports nested block-structured problems.
Example::
problem = LinearProblem([[a00, a01], [None, a11]], [L0, L1],
bcs=[bc0, bc1], u=[uh0, uh1],
kind="nest",
petsc_options_prefix="nest_linear_problem")
Every PETSc object created will have a unique options prefix set.
We recommend discovering these prefixes dynamically via the
petsc4py API rather than hard-coding each prefix value
into the programme.
Example::
ksp_options_prefix = problem.solver.getOptionsPrefix()
A_options_prefix = problem.A.getOptionsPrefix()
Args:
a: Bilinear UFL form or a nested sequence of bilinear
forms, the left-hand side of the variational problem.
L: Linear UFL form or a sequence of linear forms, the
right-hand side of the variational problem.
bcs: Sequence of Dirichlet boundary conditions to apply to
the variational problem and the preconditioner matrix.
u: Solution function. It is created if not provided.
P: Bilinear UFL form or a sequence of sequence of bilinear
forms, used as a preconditioner.
kind: The PETSc matrix and vector kind. Common choices
are ``mpi`` and ``nest``. See
:func:`dolfinx.fem.petsc.create_matrix` and
:func:`dolfinx.fem.petsc.create_vector` for more
information.
petsc_options_prefix: Mandatory named argument. Options prefix
used as root prefix on all internally created PETSc
objects. Typically ends with ``_``. Must be the same on
all ranks, and is usually unique within the programme.
petsc_options: Options set on the underlying PETSc KSP only.
The options must be the same on all ranks. For available
choices for the ``petsc_options`` kwarg, see the `PETSc KSP
documentation
<https://petsc4py.readthedocs.io/en/stable/manual/ksp/>`_.
Options on other objects (matrices, vectors) should be set
explicitly by the user.
form_compiler_options: Options used in FFCx compilation of
all forms. Run ``ffcx --help`` at the commandline to see
all available options.
jit_options: Options used in CFFI JIT compilation of C
code generated by FFCx. See ``python/dolfinx/jit.py`` for
all available options. Takes priority over all other
option values.
entity_maps: If any trial functions, test functions, or
coefficients in the form are not defined over the same mesh
as the integration domain, a corresponding :class:
`EntityMap<dolfinx.mesh.EntityMap>` must be provided.
"""
self._a = _create_form(
a,
dtype=PETSc.ScalarType, # type: ignore[attr-defined]
form_compiler_options=form_compiler_options,
jit_options=jit_options,
entity_maps=entity_maps,
)
self._L = _create_form(
L,
dtype=PETSc.ScalarType, # type: ignore[attr-defined]
form_compiler_options=form_compiler_options,
jit_options=jit_options,
entity_maps=entity_maps,
)
self._A = create_matrix(self._a, kind=kind)
self._preconditioner = _create_form(
P, # type: ignore[arg-type]
dtype=PETSc.ScalarType, # type: ignore[attr-defined]
form_compiler_options=form_compiler_options,
jit_options=jit_options,
entity_maps=entity_maps,
)
self._P_mat = (
create_matrix(self._preconditioner, kind=kind)
if self._preconditioner is not None
else None
)
# For nest matrices kind can be a nested list.
kind = "nest" if self.A.getType() == PETSc.Mat.Type.NEST else kind # type: ignore[attr-defined]
assert kind is None or isinstance(kind, str)
self._b = create_vector(_extract_function_spaces(self.L), kind=kind) # type: ignore
self._x = create_vector(_extract_function_spaces(self.L), kind=kind) # type: ignore
self._u: _Function | Sequence[_Function]
if u is None:
# Extract function space for unknown from the right hand
# side of the equation.
if isinstance(L, Sequence):
self._u = [_Function(Li.arguments()[0].ufl_function_space()) for Li in L]
else:
self._u = _Function(L.arguments()[0].ufl_function_space())
else:
self._u = u
self.bcs = [] if bcs is None else bcs
self._solver = PETSc.KSP().create(self.A.comm) # type: ignore[attr-defined]
self.solver.setOperators(self.A, self.P_mat)
if petsc_options_prefix == "":
raise ValueError("PETSc options prefix cannot be empty.")
self._petsc_options_prefix = petsc_options_prefix
self.solver.setOptionsPrefix(petsc_options_prefix)
self.A.setOptionsPrefix(f"{petsc_options_prefix}A_")
self.b.setOptionsPrefix(f"{petsc_options_prefix}b_")
self.x.setOptionsPrefix(f"{petsc_options_prefix}x_")
if self.P_mat is not None:
self.P_mat.setOptionsPrefix(f"{petsc_options_prefix}P_mat_")
# Set options on KSP only
if petsc_options is not None:
opts = PETSc.Options() # type: ignore[attr-defined]
opts.prefixPush(self.solver.getOptionsPrefix())
for k, v in petsc_options.items():
opts[k] = v
self.solver.setFromOptions()
# Tidy up global options
for k in petsc_options.keys():
del opts[k]
opts.prefixPop()
if kind == "nest":
# Transfer nest IS on self.A to PC of main KSP. This allows
# fieldsplit preconditioning to be applied, if desired.
nest_IS = self.A.getNestISs()
fieldsplit_IS = tuple(
[
(f"{u.name + '_' if u.name != 'f' else ''}{i}", IS)
for i, (u, IS) in enumerate(zip(self.u, nest_IS[0]))
]
)
self.solver.getPC().setFieldSplitIS(*fieldsplit_IS)
def __del__(self):
for obj in filter(
lambda obj: obj is not None, (self._solver, self._A, self._b, self._x, self._P_mat)
):
obj.destroy()
[docs]
def solve(self) -> _Function | Sequence[_Function]:
"""Solve the problem.
This method updates the solution ``u`` function(s) stored in the
problem instance.
Note:
The user is responsible for asserting convergence of the KSP
solver e.g. ``problem.solver.getConvergedReason() > 0``.
Alternatively, pass ``"ksp_error_if_not_converged" : True`` in
``petsc_options`` to raise a ``PETScError`` on failure.
Returns:
The solution function(s).
"""
# Assemble lhs
self.A.zeroEntries()
assemble_matrix(self.A, self.a, bcs=self.bcs) # type: ignore[arg-type, misc]
self.A.assemble()
# Assemble preconditioner
if self.P_mat is not None:
self.P_mat.zeroEntries()
assemble_matrix(self.P_mat, self.preconditioner, bcs=self.bcs) # type: ignore[arg-type, misc]
self.P_mat.assemble()
# Assemble rhs
dolfinx.la.petsc._zero_vector(self.b)
assemble_vector(self.b, self.L) # type: ignore[arg-type]
# Apply boundary conditions to the rhs
if self.bcs is not None:
if isinstance(self.u, Sequence): # block or nest
bcs1 = _bcs_by_block(_extract_function_spaces(self.a, 1), self.bcs) # type: ignore[arg-type]
apply_lifting(self.b, self.a, bcs=bcs1) # type: ignore[arg-type]
dolfinx.la.petsc._ghost_update(
self.b,
PETSc.InsertMode.ADD, # type: ignore[attr-defined]
PETSc.ScatterMode.REVERSE, # type: ignore[attr-defined]
)
bcs0 = _bcs_by_block(_extract_function_spaces(self.L), self.bcs) # type: ignore[arg-type]
dolfinx.fem.petsc.set_bc(self.b, bcs0)
else: # single form
apply_lifting(self.b, [self.a], bcs=[self.bcs]) # type: ignore[arg-type]
dolfinx.la.petsc._ghost_update(
self.b,
PETSc.InsertMode.ADD, # type: ignore[attr-defined]
PETSc.ScatterMode.REVERSE, # type: ignore[attr-defined]
)
for bc in self.bcs:
bc.set(self.b.array_w)
else:
dolfinx.la.petsc._ghost_update(self.b, PETSc.InsertMode.ADD, PETSc.ScatterMode.REVERSE) # type: ignore[attr-defined]
# Solve linear system and update ghost values in the solution
self.solver.solve(self.b, self.x)
dolfinx.la.petsc._ghost_update(self.x, PETSc.InsertMode.INSERT, PETSc.ScatterMode.FORWARD) # type: ignore[attr-defined]
dolfinx.fem.petsc.assign(self.x, self.u)
return self.u
@property
def L(self) -> Form | Sequence[Form]:
"""The compiled linear form representing the left-hand side."""
return self._L
@property
def a(self) -> Form | Sequence[Form]:
"""The compiled bilinear form representing the right-hand side."""
return self._a
@property
def preconditioner(self) -> Form | Sequence[Form]:
"""The compiled bilinear form representing the preconditioner."""
return self._preconditioner
@property
def A(self) -> PETSc.Mat: # type: ignore[name-defined]
"""Left-hand side matrix."""
return self._A
@property
def P_mat(self) -> PETSc.Mat: # type: ignore[name-defined]
"""Preconditioner matrix."""
return self._P_mat
@property
def b(self) -> PETSc.Vec: # type: ignore[name-defined]
"""Right-hand side vector."""
return self._b
@property
def x(self) -> PETSc.Vec: # type: ignore[name-defined]
"""Solution vector.
Note:
The vector does not share memory with the solution
function(s) ``u``.
"""
return self._x
@property
def solver(self) -> PETSc.KSP: # type: ignore[name-defined]
"""The PETSc KSP solver."""
return self._solver
@property
def u(self) -> _Function | Sequence[_Function]:
"""Solution function(s).
Note:
The function(s) do not share memory with the solution
vector ``x``.
"""
return self._u
# -- High-level interface for SNES ---------------------------------------
def _assign_block_data(forms: Sequence[Form], vec: PETSc.Vec): # type: ignore[name-defined]
"""Assign block data to a PETSc vector.
Args:
forms: List of forms to extract block data from.
vec: PETSc vector to assign block data to.
"""
maps = (
(
form.function_spaces[0].dofmaps(0).index_map, # type: ignore[attr-defined]
form.function_spaces[0].dofmaps(0).index_map_bs, # type: ignore[attr-defined]
)
for form in forms
)
return dolfinx.la.petsc._assign_block_data(maps, vec)
[docs]
def assemble_residual(
u: _Function | Sequence[_Function],
residual: Form | Sequence[Form],
jacobian: Form | Sequence[Sequence[Form]],
bcs: Sequence[DirichletBC],
_snes: PETSc.SNES, # type: ignore[name-defined]
x: PETSc.Vec, # type: ignore[name-defined]
b: PETSc.Vec, # type: ignore[name-defined]
):
"""Assemble the residual at ``x`` into the vector ``b``.
A function conforming to the interface expected by ``SNES.setFunction``
can be created by fixing the first four arguments, e.g.:
Example::
snes = PETSc.SNES().create(mesh.comm)
assemble_residual = functools.partial(
dolfinx.fem.petsc.assemble_residual,
u, residual, jacobian, bcs)
snes.setFunction(assemble_residual, x, b)
Args:
u: Function(s) tied to the solution vector within the residual and
Jacobian.
residual: Form of the residual. It can be a sequence of forms.
jacobian: Form of the Jacobian. It can be a nested sequence of
forms.
bcs: List of Dirichlet boundary conditions to lift the residual.
_snes: The solver instance.
x: The vector containing the point to evaluate the residual at.
b: Vector to assemble the residual into.
"""
# Update input vector before assigning
dolfinx.la.petsc._ghost_update(x, PETSc.InsertMode.INSERT, PETSc.ScatterMode.FORWARD) # type: ignore[attr-defined]
# Assign the input vector to the unknowns
assign(x, u)
# Assign block data if block assembly is requested
if isinstance(residual, Sequence) and b.getType() != PETSc.Vec.Type.NEST: # type: ignore[attr-defined]
_assign_block_data(residual, b)
_assign_block_data(residual, x)
# Assemble the residual
dolfinx.la.petsc._zero_vector(b)
assemble_vector(b, residual) # type: ignore[arg-type]
# Lift vector
if isinstance(jacobian, Sequence):
# Nest and blocked lifting
bcs1 = _bcs_by_block(_extract_function_spaces(jacobian, 1), bcs) # type: ignore[arg-type]
apply_lifting(b, jacobian, bcs=bcs1, x0=x, alpha=-1.0)
dolfinx.la.petsc._ghost_update(b, PETSc.InsertMode.ADD, PETSc.ScatterMode.REVERSE) # type: ignore[attr-defined]
bcs0 = _bcs_by_block(_extract_function_spaces(residual), bcs) # type: ignore[arg-type]
set_bc(b, bcs0, x0=x, alpha=-1.0)
else:
# Single form lifting
apply_lifting(b, [jacobian], bcs=[bcs], x0=[x], alpha=-1.0)
dolfinx.la.petsc._ghost_update(b, PETSc.InsertMode.ADD, PETSc.ScatterMode.REVERSE) # type: ignore[attr-defined]
set_bc(b, bcs, x0=x, alpha=-1.0)
dolfinx.la.petsc._ghost_update(b, PETSc.InsertMode.INSERT, PETSc.ScatterMode.FORWARD) # type: ignore[attr-defined]
[docs]
def assemble_jacobian(
u: Sequence[_Function] | _Function,
jacobian: Form | Sequence[Sequence[Form]],
preconditioner: Form | Sequence[Sequence[Form]] | None,
bcs: Sequence[DirichletBC],
_snes: PETSc.SNES, # type: ignore[name-defined]
x: PETSc.Vec, # type: ignore[name-defined]
J: PETSc.Mat, # type: ignore[name-defined]
P_mat: PETSc.Mat, # type: ignore[name-defined]
):
"""Assemble the Jacobian and preconditioner matrices at ``x``
into ``J`` and ``P_mat``.
A function conforming to the interface expected by ``SNES.setJacobian``
can be created by fixing the first four arguments e.g.:
Example::
snes = PETSc.SNES().create(mesh.comm)
assemble_jacobian = functools.partial(
dolfinx.fem.petsc.assemble_jacobian,
u, jacobian, preconditioner, bcs)
snes.setJacobian(assemble_jacobian, A, P_mat)
Args:
u: Function tied to the solution vector within the residual and
jacobian.
jacobian: Compiled form of the Jacobian.
preconditioner: Compiled form of the preconditioner.
bcs: List of Dirichlet boundary conditions to apply to the Jacobian
and preconditioner matrices.
_snes: The solver instance.
x: The vector containing the point to evaluate at.
J: Matrix to assemble the Jacobian into.
P_mat: Matrix to assemble the preconditioner into.
"""
# Copy existing soultion into the function used in the residual and
# Jacobian
dolfinx.la.petsc._ghost_update(x, PETSc.InsertMode.INSERT, PETSc.ScatterMode.FORWARD) # type: ignore[attr-defined]
assign(x, u)
# Assemble Jacobian
J.zeroEntries()
assemble_matrix(J, jacobian, bcs, diag=1.0) # type: ignore[arg-type, misc]
J.assemble()
if preconditioner is not None:
P_mat.zeroEntries()
assemble_matrix(P_mat, preconditioner, bcs, diag=1.0) # type: ignore[arg-type, misc]
P_mat.assemble()
[docs]
class NonlinearProblem:
"""High-level class for solving nonlinear variational problems
with PETSc SNES.
Solves problems of the form
:math:`F_i(u, v) = 0, i=0,\\ldots,N\\ \\forall v \\in V` where
:math:`u=(u_0,\\ldots,u_N), v=(v_0,\\ldots,v_N)` using PETSc
SNES as the non-linear solver.
Note:
The deprecated version of this class for use with
:class:`dolfinx.nls.petsc.NewtonSolver` has been renamed
:class:`dolfinx.fem.petsc.NewtonSolverNonlinearProblem`.
Note:
This high-level class automatically handles PETSc memory
management. The user does not need to manually call
``.destroy()`` on returned PETSc objects.
"""
def __init__(
self,
F: ufl.form.Form | Sequence[ufl.form.Form],
u: _Function | Sequence[_Function],
*,
petsc_options_prefix: str,
bcs: Sequence[DirichletBC] | None = None,
J: ufl.form.Form | Sequence[Sequence[ufl.form.Form]] | None = None,
P: ufl.form.Form | Sequence[Sequence[ufl.form.Form]] | None = None,
kind: str | Sequence[Sequence[str]] | None = None,
petsc_options: dict | None = None,
form_compiler_options: dict | None = None,
jit_options: dict | None = None,
entity_maps: Sequence[_EntityMap] | None = None,
):
"""
Initialize solver for a nonlinear variational problem.
By default, the underlying SNES solver uses PETSc's default
options. To use the robust combination of LU via MUMPS with
a backtracking linesearch, pass:
Example::
petsc_options = {"ksp_type": "preonly",
"pc_type": "lu",
"pc_factor_mat_solver_type": "mumps",
"snes_linesearch_type": "bt",
}
Every PETSc object will have a unique options prefix set. We
recommend discovering these prefixes dynamically via the
petsc4py API rather than hard-coding each prefix value into
the programme.
Example::
snes_options_prefix = problem.solver.getOptionsPrefix()
jacobian_options_prefix = problem.A.getOptionsPrefix()
Args:
F: UFL form(s) representing the residual :math:`F_i`.
u: Function(s) used to define the residual and Jacobian.
bcs: Dirichlet boundary conditions.
J: UFL form(s) representing the Jacobian
:math:`J_{ij} = dF_i/du_j`. If not passed, derived
automatically.
P: UFL form(s) representing the preconditioner.
kind: The PETSc matrix and vector kind. Common choices
are ``mpi`` and ``nest``. See
:func:`dolfinx.fem.petsc.create_matrix` and
:func:`dolfinx.fem.petsc.create_vector` for more
information.
petsc_options_prefix: Mandatory named argument.
Options prefix used as root prefix on all
internally created PETSc objects. Typically ends with `_`.
Must be the same on all ranks, and is usually unique within
the programme.
petsc_options: Options set on the underlying PETSc SNES only.
The options must be the same on all ranks. For available
choices for ``petsc_options``, see the
`PETSc SNES documentation
<https://petsc4py.readthedocs.io/en/stable/manual/snes/>`_.
Options on other objects (matrices, vectors) should be set
explicitly by the user.
form_compiler_options: Options used in FFCx compilation of all
forms. Run ``ffcx --help`` at the command line to see all
available options.
jit_options: Options used in CFFI JIT compilation of C code
generated by FFCx. See ``python/dolfinx/jit.py`` for all
available options. Takes priority over all other option
values.
entity_maps: If any trial functions, test functions, or
coefficients in the form are not defined over the same mesh
as the integration domain, a corresponding :class:
`EntityMap<dolfinx.mesh.EntityMap>` must be provided.
"""
# Compile residual and Jacobian forms
self._F = _create_form(
F,
form_compiler_options=form_compiler_options,
jit_options=jit_options,
entity_maps=entity_maps,
)
if J is None:
J = derivative_block(F, u)
self._J = _create_form(
J,
form_compiler_options=form_compiler_options,
jit_options=jit_options,
entity_maps=entity_maps,
)
if P is not None:
self._preconditioner = _create_form(
P,
form_compiler_options=form_compiler_options,
jit_options=jit_options,
entity_maps=entity_maps,
)
else:
self._preconditioner = None
self._u = u
# Set default values if not supplied
bcs = [] if bcs is None else bcs
# Create PETSc structures for the residual, Jacobian and solution
# vector
self._A = create_matrix(self.J, kind=kind)
# Create PETSc structure for preconditioner if provided
if self._preconditioner is not None:
self._P_mat = create_matrix(self._preconditioner, kind=kind)
else:
self._P_mat = None
# Determine the vector kind based on the matrix type
kind = "nest" if self._A.getType() == PETSc.Mat.Type.NEST else kind # type: ignore[attr-defined]
assert kind is None or isinstance(kind, str)
self._b = create_vector(_extract_function_spaces(self.F), kind=kind) # type: ignore
self._x = create_vector(_extract_function_spaces(self.F), kind=kind) # type: ignore
# Create the SNES solver and attach the corresponding Jacobian and
# residual computation functions
self._snes = PETSc.SNES().create(self.A.comm) # type: ignore[attr-defined]
self.solver.setJacobian(
partial(assemble_jacobian, u, self.J, self.preconditioner, bcs), self.A, self.P_mat
)
self.solver.setFunction(partial(assemble_residual, u, self.F, self.J, bcs), self.b)
if petsc_options_prefix == "":
raise ValueError("PETSc options prefix cannot be empty.")
self.solver.setOptionsPrefix(petsc_options_prefix)
self.A.setOptionsPrefix(f"{petsc_options_prefix}A_")
if self.P_mat is not None:
self.P_mat.setOptionsPrefix(f"{petsc_options_prefix}P_mat_")
self.b.setOptionsPrefix(f"{petsc_options_prefix}b_")
self.x.setOptionsPrefix(f"{petsc_options_prefix}x_")
# Set options for SNES only
if petsc_options is not None:
opts = PETSc.Options() # type: ignore[attr-defined]
opts.prefixPush(self.solver.getOptionsPrefix())
for k, v in petsc_options.items():
opts[k] = v
self.solver.setFromOptions()
# Tidy up global options
for k in petsc_options.keys():
del opts[k]
opts.prefixPop()
if self.P_mat is not None and kind == "nest":
# Transfer nest IS on self.P_mat to PC of main KSP. This allows
# fieldsplit preconditioning to be applied, if desired.
nest_IS = self.P_mat.getNestISs()
fieldsplit_IS = tuple(
[
(f"{u.name + '_' if u.name != 'f' else ''}{i}", IS)
for i, (u, IS) in enumerate(zip(self.u, nest_IS[0]))
]
)
self.solver.getKSP().getPC().setFieldSplitIS(*fieldsplit_IS)
[docs]
def solve(self) -> _Function | Sequence[_Function]:
"""Solve the problem.
This method updates the solution ``u`` function(s) stored in the
problem instance.
Note:
The user is responsible for asserting convergence of the SNES
solver e.g. ``assert problem.solver.getConvergedReason() > 0``.
Alternatively, pass ``"snes_error_if_not_converged": True`` and
``"ksp_error_if_not_converged" : True`` in ``petsc_options`` to
raise a ``PETScError`` on failure.
Returns:
The solution function(s).
"""
# Copy current iterate into the work array.
assign(self.u, self.x)
# Solve problem
self.solver.solve(None, self.x)
dolfinx.la.petsc._ghost_update(self.x, PETSc.InsertMode.INSERT, PETSc.ScatterMode.FORWARD) # type: ignore[attr-defined]
# Copy solution back to function
assign(self.x, self.u)
return self.u
def __del__(self):
for obj in filter(
lambda obj: obj is not None, (self._snes, self._A, self._b, self._x, self._P_mat)
):
obj.destroy()
@property
def F(self) -> Form | Sequence[Form]:
"""The compiled residual."""
return self._F
@property
def J(self) -> Form | Sequence[Sequence[Form]]:
"""The compiled Jacobian."""
return self._J
@property
def preconditioner(self) -> Form | Sequence[Sequence[Form]] | None:
"""The compiled preconditioner."""
return self._preconditioner
@property
def A(self) -> PETSc.Mat: # type: ignore[name-defined]
"""Jacobian matrix."""
return self._A
@property
def P_mat(self) -> PETSc.Mat | None: # type: ignore[name-defined]
"""Preconditioner matrix."""
return self._P_mat
@property
def b(self) -> PETSc.Vec: # type: ignore[name-defined]
"""Residual vector."""
return self._b
@property
def x(self) -> PETSc.Vec: # type: ignore[name-defined]
"""Solution vector.
Note:
The vector does not share memory with the
solution function(s) ``u``.
"""
return self._x
@property
def solver(self) -> PETSc.SNES: # type: ignore[name-defined]
"""The SNES solver."""
return self._snes
@property
def u(self) -> _Function | Sequence[_Function]:
"""Solution function(s).
Note:
The function(s) do not share memory with the solution
vector ``x``.
"""
return self._u
# -- Deprecated non-linear problem class for NewtonSolver -----------------
[docs]
class NewtonSolverNonlinearProblem:
"""(Deprecated) Nonlinear problem class for solving nonlinear
problems using :class:`dolfinx.nls.petsc.NewtonSolver`.
Solves problems of the form :math:`F(u, v) = 0 \\ \\forall v \\in V`
using PETSc as the linear algebra backend.
Note:
This class is deprecated in favour of
:class:`dolfinx.fem.petsc.NonlinearProblem`, a high level
interface to SNES.
Note:
This class was previously called
``dolfinx.fem.petsc.NonlinearProblem``.
"""
def __init__(
self,
F: ufl.form.Form,
u: _Function,
bcs: Sequence[DirichletBC] | None = None,
J: ufl.form.Form = None,
form_compiler_options: dict | None = None,
jit_options: dict | None = None,
):
"""Initialize solver for solving a non-linear problem using
Newton's method`.
Args:
F: The PDE residual F(u, v).
u: The unknown.
bcs: List of Dirichlet boundary conditions.
J: UFL representation of the Jacobian (optional)
form_compiler_options: Options used in FFCx
compilation of this form. Run ``ffcx --help`` at the
command line to see all available options.
jit_options: Options used in CFFI JIT compilation of C
code generated by FFCx. See ``python/dolfinx/jit.py``
for all available options. Takes priority over all other
option values.
Example::
problem = NonlinearProblem(F, u, [bc0, bc1])
"""
warnings.warn(
(
"dolfinx.nls.petsc.NewtonSolver is deprecated. "
+ "Use dolfinx.fem.petsc.NonlinearProblem, "
+ "a high level interface to PETSc SNES, instead."
),
DeprecationWarning,
)
self._L = _create_form(
F, form_compiler_options=form_compiler_options, jit_options=jit_options
)
# Create the Jacobian matrix, dF/du
if J is None:
V = u.function_space
du = ufl.TrialFunction(V)
J = ufl.derivative(F, u, du)
self._a = _create_form(
J, form_compiler_options=form_compiler_options, jit_options=jit_options
)
self.bcs = bcs
@property
def L(self) -> Form:
"""The compiled linear form (the residual form)."""
return self._L
@property
def a(self) -> Form:
"""The compiled bilinear form (the Jacobian form)."""
return self._a
[docs]
def F(self, x: PETSc.Vec, b: PETSc.Vec) -> None: # type: ignore[name-defined]
"""Assemble the residual F into the vector b.
Args:
x: The vector containing the latest solution
b: Vector to assemble the residual into
"""
# Reset the residual vector
dolfinx.la.petsc._zero_vector(b)
assemble_vector(b, self._L)
# Apply boundary condition
if self.bcs is not None:
apply_lifting(b, [self._a], bcs=[self.bcs], x0=[x], alpha=-1.0)
b.ghostUpdate(addv=PETSc.InsertMode.ADD, mode=PETSc.ScatterMode.REVERSE) # type: ignore[attr-defined]
set_bc(b, self.bcs, x, -1.0)
else:
b.ghostUpdate(addv=PETSc.InsertMode.ADD, mode=PETSc.ScatterMode.REVERSE) # type: ignore[attr-defined]
[docs]
def J(self, x: PETSc.Vec, A: PETSc.Mat) -> None: # type: ignore[name-defined]
"""Assemble the Jacobian matrix.
Args:
x: The vector containing the latest solution
"""
A.zeroEntries()
assemble_matrix(A, self._a, self.bcs) # type: ignore[arg-type]
A.assemble()
# -- Additional free helper functions (interpolations, assignments etc.) --
[docs]
def discrete_curl(space0: _FunctionSpace, space1: _FunctionSpace) -> PETSc.Mat: # type: ignore[name-defined]
"""Assemble a discrete curl operator.
Args:
space0: H1 space to interpolate the gradient from.
space1: H(curl) space to interpolate into.
Returns:
Discrete curl operator.
"""
return _discrete_curl(space0._cpp_object, space1._cpp_object)
[docs]
def discrete_gradient(space0: _FunctionSpace, space1: _FunctionSpace) -> PETSc.Mat: # type: ignore[name-defined]
"""Assemble a discrete gradient operator.
The discrete gradient operator interpolates the gradient of a H1
finite element function into a H(curl) space. It is assumed that the
H1 space uses an identity map and the H(curl) space uses a covariant
Piola map.
Args:
space0: H1 space to interpolate the gradient from.
space1: H(curl) space to interpolate into.
Returns:
Discrete gradient operator.
"""
return _discrete_gradient(space0._cpp_object, space1._cpp_object)
[docs]
def interpolation_matrix(V0: _FunctionSpace, V1: _FunctionSpace) -> PETSc.Mat: # type: ignore[name-defined]
r"""Assemble an interpolation operator matrix for discreye
interpolation between finite element spaces.
Consider is the vector of degrees-of-freedom :math:`u_{i}`
associated with a function in :math:`V_{i}`. This function returns
the matrix :math:`\Pi` sucht that
.. math::
u_{1} = \Pi u_{0}.
Args:
V0: Space to interpolate from.
V1: Space to interpolate into.
Returns:
The interpolation matrix :math:`\Pi`.
Note:
The returned matrix is not finalised, i.e. ghost values are not
accumulated.
"""
return _interpolation_matrix(V0._cpp_object, V1._cpp_object)
[docs]
@functools.singledispatch
def assign(u: _Function | Sequence[_Function], x: PETSc.Vec): # type: ignore[name-defined]
"""Assign :class:`Function` degrees-of-freedom to a vector.
Assigns degree-of-freedom values in ``u``, which is possibly a
sequence of ``Function``s, to ``x``. When ``u`` is a sequence of
``Function``s, degrees-of-freedom for the ``Function``s in ``u`` are
'stacked' and assigned to ``x``. See :func:`assign` for
documentation on how stacked assignment is handled.
Args:
u: ``Function`` (s) to assign degree-of-freedom value from.
x: Vector to assign degree-of-freedom values in ``u`` to.
"""
if x.getType() == PETSc.Vec.Type().NEST: # type: ignore[attr-defined]
dolfinx.la.petsc.assign([v.x.array for v in u], x)
else:
if isinstance(u, Sequence):
data0, data1 = [], []
for v in u:
bs = v.function_space.dofmap.bs
n = v.function_space.dofmap.index_map.size_local
data0.append(v.x.array[: bs * n])
data1.append(v.x.array[bs * n :])
dolfinx.la.petsc.assign(data0 + data1, x)
else:
dolfinx.la.petsc.assign(u.x.array, x)
@assign.register
def _(x: PETSc.Vec, u: _Function | Sequence[_Function]): # type: ignore[name-defined]
"""Assign vector entries to :class:`Function` degrees-of-freedom.
Assigns values in ``x`` to the degrees-of-freedom of ``u``, which is
possibly a Sequence of ``Function``s. When ``u`` is a Sequence of
``Function``s, values in ``x`` are assigned block-wise to the
``Function``s. See :func:`assign` for documentation on how blocked
assignment is handled.
Args:
x: Vector with values to assign values from.
u: ``Function`` (s) to assign degree-of-freedom values to.
"""
if x.getType() == PETSc.Vec.Type().NEST: # type: ignore[attr-defined]
dolfinx.la.petsc.assign(x, [v.x.array for v in u])
else:
if isinstance(u, Sequence):
data0, data1 = [], []
for v in u:
bs = v.function_space.dofmap.bs
n = v.function_space.dofmap.index_map.size_local
data0.append(v.x.array[: bs * n])
data1.append(v.x.array[bs * n :])
dolfinx.la.petsc.assign(x, data0 + data1)
else:
dolfinx.la.petsc.assign(x, u.x.array)
def get_petsc_lib() -> pathlib.Path:
"""Find the full path of the PETSc shared library.
Returns:
Full path to the PETSc shared library.
Raises:
RuntimeError: If PETSc library cannot be found for if more than
one library is found.
"""
import petsc4py as _petsc4py
petsc_dir = _petsc4py.get_config()["PETSC_DIR"]
petsc_arch = _petsc4py.lib.getPathArchPETSc()[1]
candidate_paths = [
os.path.join(petsc_dir, petsc_arch, "lib", "libpetsc.so"),
os.path.join(petsc_dir, petsc_arch, "lib", "libpetsc.dylib"),
]
exists_paths = []
for candidate_path in candidate_paths:
if os.path.exists(candidate_path):
exists_paths.append(candidate_path)
if len(exists_paths) == 0:
raise RuntimeError(
f"Could not find a PETSc shared library. Candidate paths: {candidate_paths}"
)
elif len(exists_paths) > 1:
raise RuntimeError(f"More than one PETSc shared library found. Paths: {exists_paths}")
return pathlib.Path(exists_paths[0])
[docs]
class numba_utils:
"""Utility attributes for working with Numba and PETSc.
These attributes are convenience functions for calling PETSc C
functions from within Numba functions.
Note:
`Numba <https://numba.pydata.org/>`_ must be available
to use these utilities.
Examples:
A typical use of these utility functions is::
import numpy as np
import numpy.typing as npt
def set_vals(A: int,
m: int, rows: npt.NDArray[PETSc.IntType],
n: int, cols: npt.NDArray[PETSc.IntType],
data: npt.NDArray[PETSc.ScalarTYpe], mode: int):
MatSetValuesLocal(A, m, rows.ctypes, n, cols.ctypes,
data.ctypes, mode)
"""
try:
import petsc4py.PETSc as _PETSc
import llvmlite as _llvmlite
import numba as _numba
_llvmlite.binding.load_library_permanently(str(get_petsc_lib()))
_int = _numba.from_dtype(_PETSc.IntType) # type: ignore
_scalar = _numba.from_dtype(_PETSc.ScalarType) # type: ignore
_real = _numba.from_dtype(_PETSc.RealType) # type: ignore
_int_ptr = _numba.core.types.CPointer(_int)
_scalar_ptr = _numba.core.types.CPointer(_scalar)
_MatSetValues_sig = _numba.core.typing.signature(
_numba.core.types.intc,
_numba.core.types.uintp,
_int,
_int_ptr,
_int,
_int_ptr,
_scalar_ptr,
_numba.core.types.intc,
)
MatSetValuesLocal = _numba.core.types.ExternalFunction(
"MatSetValuesLocal", _MatSetValues_sig
)
"""See PETSc `MatSetValuesLocal
<https://petsc.org/release/manualpages/Mat/MatSetValuesLocal>`_
documentation."""
MatSetValuesBlockedLocal = _numba.core.types.ExternalFunction(
"MatSetValuesBlockedLocal", _MatSetValues_sig
)
"""See PETSc `MatSetValuesBlockedLocal
<https://petsc.org/release/manualpages/Mat/MatSetValuesBlockedLocal>`_
documentation."""
except ImportError:
pass
[docs]
class ctypes_utils:
"""Utility attributes for working with ctypes and PETSc.
These attributes are convenience functions for calling PETSc C
functions, typically from within Numba functions.
Examples:
A typical use of these utility functions is::
import numpy as np
import numpy.typing as npt
def set_vals(A: int,
m: int, rows: npt.NDArray[PETSc.IntType],
n: int, cols: npt.NDArray[PETSc.IntType],
data: npt.NDArray[PETSc.ScalarTYpe], mode: int):
MatSetValuesLocal(A, m, rows.ctypes, n, cols.ctypes,
data.ctypes, mode)
"""
try:
import petsc4py.PETSc as _PETSc
_lib_ctypes = _ctypes.cdll.LoadLibrary(str(get_petsc_lib()))
# Note: ctypes does not have complex types, hence we use void* for
# scalar data
_int = np.ctypeslib.as_ctypes_type(_PETSc.IntType) # type: ignore
MatSetValuesLocal = _lib_ctypes.MatSetValuesLocal
"""See PETSc `MatSetValuesLocal
<https://petsc.org/release/manualpages/Mat/MatSetValuesLocal>`_
documentation."""
MatSetValuesLocal.argtypes = [
_ctypes.c_void_p,
_int,
_ctypes.POINTER(_int),
_int,
_ctypes.POINTER(_int),
_ctypes.c_void_p,
_ctypes.c_int,
]
MatSetValuesBlockedLocal = _lib_ctypes.MatSetValuesBlockedLocal
"""See PETSc `MatSetValuesBlockedLocal
<https://petsc.org/release/manualpages/Mat/MatSetValuesBlockedLocal>`_
documentation."""
MatSetValuesBlockedLocal.argtypes = [
_ctypes.c_void_p,
_int,
_ctypes.POINTER(_int),
_int,
_ctypes.POINTER(_int),
_ctypes.c_void_p,
_ctypes.c_int,
]
except ImportError:
pass
[docs]
class cffi_utils:
"""Utility attributes for working with CFFI (ABI mode) and Numba.
Registers Numba's complex types with CFFI.
If PETSc is available, CFFI convenience functions for calling PETSc C
functions are also created. These are typically called from within
Numba functions.
Note:
`CFFI <https://cffi.readthedocs.io/>`_ and `Numba
<https://numba.pydata.org/>`_ must be available to use these
utilities.
Examples:
A typical use of these utility functions is::
import numpy as np
import numpy.typing as npt
def set_vals(A: int,
m: int, rows: npt.NDArray[PETSc.IntType],
n: int, cols: npt.NDArray[PETSc.IntType],
data: npt.NDArray[PETSc.ScalarType], mode: int):
MatSetValuesLocal(A, m, ffi.from_buffer(rows), n,
ffi.from_buffer(cols),
ffi.from_buffer(rows(data), mode)
"""
import cffi as _cffi
_ffi = _cffi.FFI()
try:
import numba as _numba
import numba.core.typing.cffi_utils as _cffi_support
# Register complex types
_cffi_support.register_type(_ffi.typeof("float _Complex"), _numba.types.complex64)
_cffi_support.register_type(_ffi.typeof("double _Complex"), _numba.types.complex128)
except KeyError:
pass
except ImportError:
from dolfinx.log import LogLevel, log
log(
LogLevel.DEBUG,
"Could not import numba, so cffi/numba complex types were not registered.",
)
try:
from petsc4py import PETSc as _PETSc
_lib_cffi = _ffi.dlopen(str(get_petsc_lib()))
_CTYPES = {
np.int32: "int32_t",
np.int64: "int64_t",
np.float32: "float",
np.float64: "double",
np.complex64: "float _Complex",
np.complex128: "double _Complex",
np.longlong: "long long",
}
_c_int_t = _CTYPES[_PETSc.IntType] # type: ignore
_c_scalar_t = _CTYPES[_PETSc.ScalarType] # type: ignore
_ffi.cdef(
f"""
int MatSetValuesLocal(void* mat, {_c_int_t} nrow, const {_c_int_t}* irow,
{_c_int_t} ncol, const {_c_int_t}* icol,
const {_c_scalar_t}* y, int addv);
int MatSetValuesBlockedLocal(void* mat, {_c_int_t} nrow, const {_c_int_t}* irow,
{_c_int_t} ncol, const {_c_int_t}* icol,
const {_c_scalar_t}* y, int addv);
"""
)
MatSetValuesLocal = _lib_cffi.MatSetValuesLocal # type: ignore[attr-defined]
"""See PETSc `MatSetValuesLocal
<https://petsc.org/release/manualpages/Mat/MatSetValuesLocal>`_
documentation."""
MatSetValuesBlockedLocal = _lib_cffi.MatSetValuesBlockedLocal # type: ignore[attr-defined]
"""See PETSc `MatSetValuesBlockedLocal
<https://petsc.org/release/manualpages/Mat/MatSetValuesBlockedLocal>`_
documentation."""
except KeyError:
pass
except ImportError:
from dolfinx.log import LogLevel, log
log(
LogLevel.DEBUG,
"Could not import petsc4py, so cffi/PETSc ABI mode interface was not created.",
)