# Copyright (C) 2018-2022 Garth N. Wells and Jørgen S. Dokken
#
# This file is part of DOLFINx (https://www.fenicsproject.org)
#
# SPDX-License-Identifier: LGPL-3.0-or-later
"""Assembly functions into PETSc objects for variational forms.
Functions in this module generally apply functions in :mod:`dolfinx.fem`
to PETSc linear algebra objects and handle any PETSc-specific
preparation."""
# mypy: ignore-errors
from __future__ import annotations
import contextlib
import functools
import typing
from petsc4py import PETSc
import numpy as np
import dolfinx.cpp as _cpp
import ufl
from dolfinx import la
from dolfinx.cpp.fem import pack_coefficients as _pack_coefficients
from dolfinx.cpp.fem import pack_constants as _pack_constants
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 assemble as _assemble
from dolfinx.fem.bcs import DirichletBC
from dolfinx.fem.bcs import bcs_by_block as _bcs_by_block
from dolfinx.fem.forms import Form
from dolfinx.fem.forms import extract_function_spaces as _extract_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.la import create_petsc_vector
__all__ = [
"create_vector",
"create_vector_block",
"create_vector_nest",
"create_matrix",
"create_matrix_block",
"create_matrix_nest",
"assemble_vector",
"assemble_vector_nest",
"assemble_vector_block",
"assemble_matrix",
"assemble_matrix_nest",
"assemble_matrix_block",
"apply_lifting",
"apply_lifting_nest",
"set_bc",
"set_bc_nest",
"LinearProblem",
"NonlinearProblem",
"discrete_gradient",
"interpolation_matrix",
]
def _extract_function_spaces(a: list[list[Form]]):
"""From a rectangular array of bilinear forms, extract the function
spaces for each block row and block column.
"""
assert len({len(cols) for cols in a}) == 1, "Array of function spaces is not rectangular"
# Extract (V0, V1) pair for each block in 'a'
def fn(form):
return form.function_spaces if form is not None else None
from functools import partial
Vblock: typing.Iterable = map(partial(map, fn), a)
# Compute spaces for each row/column block
rows: list[set] = [set() for i in range(len(a))]
cols: list[set] = [set() for i in range(len(a[0]))]
for i, Vrow in enumerate(Vblock):
for j, V in enumerate(Vrow):
if V is not None:
rows[i].add(V[0])
cols[j].add(V[1])
rows = [e for row in rows for e in row]
cols = [e for col in cols for e in col]
assert len(rows) == len(a)
assert len(cols) == len(a[0])
return rows, cols
# -- Vector instantiation ----------------------------------------------------
[docs]
def create_vector(L: Form) -> PETSc.Vec:
"""Create a PETSc vector that is compaible with a linear form.
Args:
L: A linear form.
Returns:
A PETSc vector with a layout that is compatible with ``L``.
"""
dofmap = L.function_spaces[0].dofmap
return create_petsc_vector(dofmap.index_map, dofmap.index_map_bs)
[docs]
def create_vector_block(L: list[Form]) -> PETSc.Vec:
"""Create a PETSc vector (blocked) that is compaible with a list of linear forms.
Args:
L: List of linear forms.
Returns:
A PETSc vector with a layout that is compatible with ``L``.
"""
maps = [
(form.function_spaces[0].dofmap.index_map, form.function_spaces[0].dofmap.index_map_bs)
for form in L
]
return _cpp.fem.petsc.create_vector_block(maps)
[docs]
def create_vector_nest(L: list[Form]) -> PETSc.Vec:
"""Create a PETSc nested vector (``VecNest``) that is compatible with a list of linear forms.
Args:
L: List of linear forms.
Returns:
A PETSc nested vector (``VecNest``) with a layout that is
compatible with ``L``.
"""
maps = [
(form.function_spaces[0].dofmap.index_map, form.function_spaces[0].dofmap.index_map_bs)
for form in L
]
return _cpp.fem.petsc.create_vector_nest(maps)
# -- Matrix instantiation ----------------------------------------------------
[docs]
def create_matrix(a: Form, mat_type=None) -> PETSc.Mat:
"""Create a PETSc matrix that is compatible with a bilinear form.
Args:
a: A bilinear form.
mat_type: The PETSc matrix type (``MatType``).
Returns:
A PETSc matrix with a layout that is compatible with ``a``.
"""
if mat_type is None:
return _cpp.fem.petsc.create_matrix(a._cpp_object)
else:
return _cpp.fem.petsc.create_matrix(a._cpp_object, mat_type)
[docs]
def create_matrix_block(a: list[list[Form]]) -> PETSc.Mat:
"""Create a PETSc matrix that is compatible with a rectangular array of bilinear forms.
Args:
a: Rectangular array of bilinear forms.
Returns:
A PETSc matrix with a blocked layout that is compatible with
``a``.
"""
_a = [[None if form is None else form._cpp_object for form in arow] for arow in a]
return _cpp.fem.petsc.create_matrix_block(_a)
[docs]
def create_matrix_nest(a: list[list[Form]]) -> PETSc.Mat:
"""Create a PETSc matrix (``MatNest``) that is compatible with an array of bilinear forms.
Args:
a: Rectangular array of bilinear forms.
Returns:
A PETSc matrix (``MatNest``) that is compatible with ``a``.
"""
_a = [[None if form is None else form._cpp_object for form in arow] for arow in a]
return _cpp.fem.petsc.create_matrix_nest(_a)
# -- Vector assembly ---------------------------------------------------------
[docs]
@functools.singledispatch
def assemble_vector(L: typing.Any, constants=None, coeffs=None) -> PETSc.Vec:
"""Assemble linear form into a new PETSc vector.
Note:
The returned vector is not finalised, i.e. ghost values are not
accumulated on the owning processes.
Args:
L: A linear form.
Returns:
An assembled vector.
"""
b = create_petsc_vector(
L.function_spaces[0].dofmap.index_map, L.function_spaces[0].dofmap.index_map_bs
)
with b.localForm() as b_local:
_assemble._assemble_vector_array(b_local.array_w, L, constants, coeffs)
return b
@assemble_vector.register(PETSc.Vec)
def _assemble_vector_vec(b: PETSc.Vec, L: Form, constants=None, coeffs=None) -> PETSc.Vec:
"""Assemble linear form into an existing PETSc vector.
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 to assemble into ``b``.
Returns:
An assembled vector.
"""
with b.localForm() as b_local:
_assemble._assemble_vector_array(b_local.array_w, L, constants, coeffs)
return b
[docs]
@functools.singledispatch
def assemble_vector_nest(L: typing.Any, constants=None, coeffs=None) -> PETSc.Vec:
"""Assemble linear forms into a new nested PETSc (``VecNest``) vector.
The returned vector is not finalised, i.e. ghost values are not
accumulated on the owning processes.
"""
maps = [
(form.function_spaces[0].dofmap.index_map, form.function_spaces[0].dofmap.index_map_bs)
for form in L
]
b = _cpp.fem.petsc.create_vector_nest(maps)
for b_sub in b.getNestSubVecs():
with b_sub.localForm() as b_local:
b_local.set(0.0)
return _assemble_vector_nest_vec(b, L, constants, coeffs)
@assemble_vector_nest.register
def _assemble_vector_nest_vec(
b: PETSc.Vec, L: list[Form], constants=None, coeffs=None
) -> PETSc.Vec:
"""Assemble linear forms into a nested PETSc (``VecNest``) vector. The
vector is not zeroed before assembly and it is not finalised, i.e.
ghost values are not accumulated on the owning processes.
"""
constants = [None] * len(L) if constants is None else constants
coeffs = [None] * len(L) if coeffs is None else coeffs
for b_sub, L_sub, const, coeff in zip(b.getNestSubVecs(), L, constants, coeffs):
with b_sub.localForm() as b_local:
_assemble._assemble_vector_array(b_local.array_w, L_sub, const, coeff)
return b
# FIXME: Revise this interface
[docs]
@functools.singledispatch
def assemble_vector_block(
L: list[Form],
a: list[list[Form]],
bcs: list[DirichletBC] = [],
x0: typing.Optional[PETSc.Vec] = None,
scale: float = 1.0,
constants_L=None,
coeffs_L=None,
constants_a=None,
coeffs_a=None,
) -> PETSc.Vec:
"""Assemble linear forms into a monolithic vector. The vector is not
finalised, i.e. ghost values are not accumulated.
"""
maps = [
(form.function_spaces[0].dofmap.index_map, form.function_spaces[0].dofmap.index_map_bs)
for form in L
]
b = _cpp.fem.petsc.create_vector_block(maps)
with b.localForm() as b_local:
b_local.set(0.0)
return _assemble_vector_block_vec(
b, L, a, bcs, x0, scale, constants_L, coeffs_L, constants_a, coeffs_a
)
@assemble_vector_block.register
def _assemble_vector_block_vec(
b: PETSc.Vec,
L: list[Form],
a: list[list[Form]],
bcs: list[DirichletBC] = [],
x0: typing.Optional[PETSc.Vec] = None,
scale: float = 1.0,
constants_L=None,
coeffs_L=None,
constants_a=None,
coeffs_a=None,
) -> PETSc.Vec:
"""Assemble linear forms into a monolithic vector. The vector is not
zeroed and it is not finalised, i.e. ghost values are not
accumulated.
"""
maps = [
(form.function_spaces[0].dofmap.index_map, form.function_spaces[0].dofmap.index_map_bs)
for form in L
]
if x0 is not None:
x0_local = _cpp.la.petsc.get_local_vectors(x0, maps)
x0_sub = x0_local
else:
x0_local = []
x0_sub = [None] * len(maps)
constants_L = (
[form and _pack_constants(form._cpp_object) for form in L]
if constants_L is None
else constants_L
)
coeffs_L = (
[{} if form is None else _pack_coefficients(form._cpp_object) for form in L]
if coeffs_L is None
else coeffs_L
)
constants_a = (
[
[
_pack_constants(form._cpp_object)
if form is not None
else np.array([], dtype=PETSc.ScalarType)
for form in forms
]
for forms in a
]
if constants_a is None
else constants_a
)
coeffs_a = (
[
[{} if form is None else _pack_coefficients(form._cpp_object) for form in forms]
for forms in a
]
if coeffs_a is None
else coeffs_a
)
_bcs = [bc._cpp_object for bc in bcs]
bcs1 = _bcs_by_block(_extract_spaces(a, 1), _bcs)
b_local = _cpp.la.petsc.get_local_vectors(b, maps)
for b_sub, L_sub, a_sub, const_L, coeff_L, const_a, coeff_a in zip(
b_local, L, a, constants_L, coeffs_L, constants_a, coeffs_a
):
_cpp.fem.assemble_vector(b_sub, L_sub._cpp_object, const_L, coeff_L)
_a_sub = [None if form is None else form._cpp_object for form in a_sub]
_cpp.fem.apply_lifting(b_sub, _a_sub, const_a, coeff_a, bcs1, x0_local, scale)
_cpp.la.petsc.scatter_local_vectors(b, b_local, maps)
b.ghostUpdate(addv=PETSc.InsertMode.ADD, mode=PETSc.ScatterMode.REVERSE)
bcs0 = _bcs_by_block(_extract_spaces(L), _bcs)
offset = 0
b_array = b.getArray(readonly=False)
for submap, bc, _x0 in zip(maps, bcs0, x0_sub):
size = submap[0].size_local * submap[1]
if _x0 is None:
_cpp.fem.set_bc(b_array[offset : offset + size], bc, scale)
else:
_cpp.fem.set_bc(b_array[offset : offset + size], bc, _x0, scale)
offset += size
return b
# -- Matrix assembly ---------------------------------------------------------
[docs]
@functools.singledispatch
def assemble_matrix(
a: typing.Any, bcs: list[DirichletBC] = [], diagonal: float = 1.0, constants=None, coeffs=None
):
"""Assemble bilinear form into a matrix. The returned matrix is not
finalised, i.e. ghost values are not accumulated.
Note:
The returned matrix is not 'assembled', i.e. ghost contributions
have not been communicated.
Args:
a: Bilinear form to assembled into a matrix.
bc: Dirichlet boundary conditions applied to the system.
diagonal: Value to set on matrix diagonal for Dirichlet boundary
condition constrained degrees-of-freedom.
constants: Constants appearing the in the form.
coeffs: Coefficients appearing the in the form.
Returns:
Matrix representing the bilinear form.
"""
A = _cpp.fem.petsc.create_matrix(a._cpp_object)
assemble_matrix_mat(A, a, bcs, diagonal, constants, coeffs)
return A
@assemble_matrix.register
def assemble_matrix_mat(
A: PETSc.Mat,
a: Form,
bcs: list[DirichletBC] = [],
diagonal: float = 1.0,
constants=None,
coeffs=None,
) -> PETSc.Mat:
"""Assemble bilinear form into a matrix. The returned matrix is not
finalised, i.e. ghost values are not accumulated.
"""
constants = _pack_constants(a._cpp_object) if constants is None else constants
coeffs = _pack_coefficients(a._cpp_object) if coeffs is None else coeffs
_bcs = [bc._cpp_object for bc in bcs]
_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)
A.assemblyEnd(PETSc.Mat.AssemblyType.FLUSH)
_cpp.fem.petsc.insert_diagonal(A, a.function_spaces[0], _bcs, diagonal)
return A
# FIXME: Revise this interface
[docs]
@functools.singledispatch
def assemble_matrix_nest(
a: list[list[Form]],
bcs: list[DirichletBC] = [],
mat_types=[],
diagonal: float = 1.0,
constants=None,
coeffs=None,
) -> PETSc.Mat:
"""Create a nested matrix and assemble bilinear forms into the matrix.
Args:
a: Rectangular (list-of-lists) array for bilinear forms.
bcs: Dirichlet boundary conditions.
mat_types: PETSc matrix type for each matrix block.
diagonal: Value to set on matrix diagonal for Dirichlet boundary
condition constrained degrees-of-freedom.
constants: Constants appearing the in the form.
coeffs: Coefficients appearing the in the form.
Returns:
PETSc matrix (``MatNest``) representing the block of bilinear
forms.
"""
_a = [[None if form is None else form._cpp_object for form in arow] for arow in a]
A = _cpp.fem.petsc.create_matrix_nest(_a, mat_types)
_assemble_matrix_nest_mat(A, a, bcs, diagonal, constants, coeffs)
return A
@assemble_matrix_nest.register
def _assemble_matrix_nest_mat(
A: PETSc.Mat,
a: list[list[Form]],
bcs: list[DirichletBC] = [],
diagonal: float = 1.0,
constants=None,
coeffs=None,
) -> PETSc.Mat:
"""Assemble bilinear forms into a nested matrix
Args:
A: PETSc ``MatNest`` matrix. Matrix must have been correctly
initialized for the bilinear forms.
a: Rectangular (list-of-lists) array for bilinear forms.
bcs: Dirichlet boundary conditions.
mat_types: PETSc matrix type for each matrix block.
diagonal: Value to set on matrix diagonal for Dirichlet boundary
condition constrained degrees-of-freedom.
constants: Constants appearing the in the form.
coeffs: Coefficients appearing the in the form.
Returns:
PETSc matrix (``MatNest``) representing the block of bilinear
forms.
"""
constants = (
[[form and _pack_constants(form._cpp_object) for form in forms] for forms in a]
if constants is None
else constants
)
coeffs = (
[
[{} if form is None else _pack_coefficients(form._cpp_object) for form in forms]
for forms in a
]
if coeffs is None
else coeffs
)
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_mat(Asub, a_block, bcs, diagonal, const, coeff)
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."
)
return A
# FIXME: Revise this interface
[docs]
@functools.singledispatch
def assemble_matrix_block(
a: list[list[Form]],
bcs: list[DirichletBC] = [],
diagonal: float = 1.0,
constants=None,
coeffs=None,
) -> PETSc.Mat: # type: ignore
"""Assemble bilinear forms into a blocked matrix."""
_a = [[None if form is None else form._cpp_object for form in arow] for arow in a]
A = _cpp.fem.petsc.create_matrix_block(_a)
return _assemble_matrix_block_mat(A, a, bcs, diagonal, constants, coeffs)
@assemble_matrix_block.register
def _assemble_matrix_block_mat(
A: PETSc.Mat,
a: list[list[Form]],
bcs: list[DirichletBC] = [],
diagonal: float = 1.0,
constants=None,
coeffs=None,
) -> PETSc.Mat:
"""Assemble bilinear forms into a blocked matrix."""
constants = (
[
[
_pack_constants(form._cpp_object)
if form is not None
else np.array([], dtype=PETSc.ScalarType)
for form in forms
]
for forms in a
]
if constants is None
else constants
)
coeffs = (
[
[{} if form is None else _pack_coefficients(form._cpp_object) for form in forms]
for forms in a
]
if coeffs is None
else coeffs
)
V = _extract_function_spaces(a)
is_rows = _cpp.la.petsc.create_index_sets(
[(Vsub.dofmap.index_map, Vsub.dofmap.index_map_bs) for Vsub in V[0]]
)
is_cols = _cpp.la.petsc.create_index_sets(
[(Vsub.dofmap.index_map, Vsub.dofmap.index_map_bs) for Vsub in V[1]]
)
# Assemble form
_bcs = [bc._cpp_object for bc in bcs]
for i, a_row in enumerate(a):
for j, a_sub in enumerate(a_row):
if a_sub is not None:
Asub = A.getLocalSubMatrix(is_rows[i], is_cols[j])
_cpp.fem.petsc.assemble_matrix(
Asub, a_sub._cpp_object, constants[i][j], coeffs[i][j], _bcs, True
)
A.restoreLocalSubMatrix(is_rows[i], is_cols[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)
# 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(is_rows[i], is_cols[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, diagonal)
A.restoreLocalSubMatrix(is_rows[i], is_cols[j], Asub)
return A
# -- Modifiers for Dirichlet conditions ---------------------------------------
[docs]
def apply_lifting(
b: PETSc.Vec,
a: list[Form],
bcs: list[list[DirichletBC]],
x0: list[PETSc.Vec] = [],
scale: float = 1.0,
constants=None,
coeffs=None,
) -> None:
"""Apply the function :func:`dolfinx.fem.apply_lifting` to a PETSc Vector."""
with contextlib.ExitStack() as stack:
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())
_assemble.apply_lifting(b_local.array_w, a, bcs, x0_r, scale, constants, coeffs)
[docs]
def apply_lifting_nest(
b: PETSc.Vec,
a: list[list[Form]],
bcs: list[DirichletBC],
x0: typing.Optional[PETSc.Vec] = None,
scale: float = 1.0,
constants=None,
coeffs=None,
) -> PETSc.Vec:
"""Apply the function :func:`dolfinx.fem.apply_lifting` to each sub-vector
in a nested PETSc Vector."""
x0 = [] if x0 is None else x0.getNestSubVecs()
bcs1 = _bcs_by_block(_extract_spaces(a, 1), bcs)
constants = (
[
[
_pack_constants(form._cpp_object)
if form is not None
else np.array([], dtype=PETSc.ScalarType)
for form in forms
]
for forms in a
]
if constants is None
else constants
)
coeffs = (
[
[{} if form is None else _pack_coefficients(form._cpp_object) for form in forms]
for forms in a
]
if coeffs is None
else coeffs
)
for b_sub, a_sub, const, coeff in zip(b.getNestSubVecs(), a, constants, coeffs):
apply_lifting(b_sub, a_sub, bcs1, x0, scale, const, coeff)
return b
[docs]
def set_bc(
b: PETSc.Vec, bcs: list[DirichletBC], x0: typing.Optional[PETSc.Vec] = None, scale: float = 1.0
) -> None:
"""Apply the function :func:`dolfinx.fem.set_bc` to a PETSc Vector."""
if x0 is not None:
x0 = x0.array_r
_assemble.set_bc(b.array_w, bcs, x0, scale)
[docs]
def set_bc_nest(
b: PETSc.Vec,
bcs: list[list[DirichletBC]],
x0: typing.Optional[PETSc.Vec] = None,
scale: float = 1.0,
) -> None:
"""Apply the function :func:`dolfinx.fem.set_bc` to each sub-vector of a nested PETSc Vector."""
_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):
set_bc(b_sub, bc, x_sub, scale)
[docs]
class LinearProblem:
"""Class for solving a linear variational problem.
Solves of the form :math:`a(u, v) = L(v) \\, \\forall v \\in V`
using PETSc as a linear algebra backend.
"""
def __init__(
self,
a: ufl.Form,
L: ufl.Form,
bcs: list[DirichletBC] = [],
u: typing.Optional[_Function] = None,
petsc_options: typing.Optional[dict] = None,
form_compiler_options: typing.Optional[dict] = None,
jit_options: typing.Optional[dict] = None,
):
"""Initialize solver for a linear variational problem.
Args:
a: A bilinear UFL form, the left hand side of the
variational problem.
L: A linear UFL form, the right hand side of the variational
problem.
bcs: A list of Dirichlet boundary conditions.
u: The solution function. It will be created if not provided.
petsc_options: Options that are passed to the linear
algebra backend PETSc. For available choices for the
'petsc_options' kwarg, see the `PETSc documentation
<https://petsc4py.readthedocs.io/en/stable/manual/ksp/>`_.
form_compiler_options: Options used in FFCx compilation of
this form. 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.
Example::
problem = LinearProblem(a, L, [bc0, bc1], petsc_options={"ksp_type": "preonly",
"pc_type": "lu",
"pc_factor_mat_solver_type":
"mumps"})
"""
self._a = _create_form(
a, form_compiler_options=form_compiler_options, jit_options=jit_options
)
self._A = create_matrix(self._a)
self._L = _create_form(
L, form_compiler_options=form_compiler_options, jit_options=jit_options
)
self._b = create_vector(self._L)
if u is None:
# Extract function space from TrialFunction (which is at the
# end of the argument list as it is numbered as 1, while the
# Test function is numbered as 0)
self.u = _Function(a.arguments()[-1].ufl_function_space())
else:
self.u = u
self._x = la.create_petsc_vector_wrap(self.u.x)
self.bcs = bcs
self._solver = PETSc.KSP().create(self.u.function_space.mesh.comm)
self._solver.setOperators(self._A)
# Give PETSc solver options a unique prefix
problem_prefix = f"dolfinx_solve_{id(self)}"
self._solver.setOptionsPrefix(problem_prefix)
# Set PETSc options
opts = PETSc.Options()
opts.prefixPush(problem_prefix)
if petsc_options is not None:
for k, v in petsc_options.items():
opts[k] = v
opts.prefixPop()
self._solver.setFromOptions()
# Set matrix and vector PETSc options
self._A.setOptionsPrefix(problem_prefix)
self._A.setFromOptions()
self._b.setOptionsPrefix(problem_prefix)
self._b.setFromOptions()
def __del__(self):
self._solver.destroy()
self._A.destroy()
self._b.destroy()
self._x.destroy()
[docs]
def solve(self) -> _Function:
"""Solve the problem."""
# Assemble lhs
self._A.zeroEntries()
assemble_matrix_mat(self._A, self._a, bcs=self.bcs)
self._A.assemble()
# Assemble rhs
with self._b.localForm() as b_loc:
b_loc.set(0)
assemble_vector(self._b, self._L)
# Apply boundary conditions to the rhs
apply_lifting(self._b, [self._a], bcs=[self.bcs])
self._b.ghostUpdate(addv=PETSc.InsertMode.ADD, mode=PETSc.ScatterMode.REVERSE)
set_bc(self._b, self.bcs)
# Solve linear system and update ghost values in the solution
self._solver.solve(self._b, self._x)
self.u.x.scatter_forward()
return self.u
@property
def L(self) -> Form:
"""The compiled linear form"""
return self._L
@property
def a(self) -> Form:
"""The compiled bilinear form"""
return self._a
@property
def A(self) -> PETSc.Mat:
"""Matrix operator"""
return self._A
@property
def b(self) -> PETSc.Vec:
"""Right-hand side vector"""
return self._b
@property
def solver(self) -> PETSc.KSP:
"""Linear solver object"""
return self._solver
[docs]
class NonlinearProblem:
"""Nonlinear problem class for solving the non-linear problems.
Solves problems of the form :math:`F(u, v) = 0 \\ \\forall v \\in V` using
PETSc as the linear algebra backend.
"""
def __init__(
self,
F: ufl.form.Form,
u: _Function,
bcs: list[DirichletBC] = [],
J: ufl.form.Form = None,
form_compiler_options: typing.Optional[dict] = None,
jit_options: typing.Optional[dict] = 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
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.
Example::
problem = LinearProblem(F, u, [bc0, bc1])
"""
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:
"""Compiled linear form (the residual form)"""
return self._L
@property
def a(self) -> Form:
"""Compiled bilinear form (the Jacobian form)"""
return self._a
[docs]
def F(self, x: PETSc.Vec, b: PETSc.Vec) -> None:
"""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
with b.localForm() as b_local:
b_local.set(0.0)
assemble_vector(b, self._L)
# Apply boundary condition
apply_lifting(b, [self._a], bcs=[self.bcs], x0=[x], scale=-1.0)
b.ghostUpdate(addv=PETSc.InsertMode.ADD, mode=PETSc.ScatterMode.REVERSE)
set_bc(b, self.bcs, x, -1.0)
[docs]
def J(self, x: PETSc.Vec, A: PETSc.Mat) -> None:
"""Assemble the Jacobian matrix.
Args:
x: The vector containing the latest solution
"""
A.zeroEntries()
assemble_matrix_mat(A, self._a, self.bcs)
A.assemble()
[docs]
def discrete_gradient(space0: _FunctionSpace, space1: _FunctionSpace) -> PETSc.Mat:
"""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(space0: _FunctionSpace, space1: _FunctionSpace) -> PETSc.Mat:
"""Assemble an interpolation operator matrix.
Args:
space0: Space to interpolate from
space1: Space to interpolate into
Returns:
Interpolation matrix
"""
return _interpolation_matrix(space0._cpp_object, space1._cpp_object)