Source code for dolfinx_adjoint.blocks.dirichletbc

import dolfinx
import numpy as np
import numpy.typing as npt
from packaging.version import Version
from pyadjoint.block import Block


def build_cpp_bc_and_kwargs(
    g: dolfinx.fem.Function, dofs: npt.NDArray[np.int32], V: dolfinx.fem.FunctionSpace | None
) -> tuple[
    dolfinx.cpp.fem.DirichletBC_float32
    | dolfinx.cpp.fem.DirichletBC_float64
    | dolfinx.cpp.fem.DirichletBC_complex64
    | dolfinx.cpp.fem.DirichletBC_complex128,
    dict,
]:
    """Build the cpp Dirichlet bc object and the kwargs a Python
    :py:class:`dolfinx.fem.DirichletBC` wrapper needs, decoupling the *Python-level*
    constrained space ``V`` from the cpp-level construction.

    Shared by :py:class:`~dolfinx_adjoint.types.dirichletbc.DirichletBC` (the tape-tracked
    constructor) and :py:meth:`DirichletBCBlock.evaluate_tlm_component` (a plain,
    untracked bc built fresh each TLM call).

    The cpp constructor is always called the V-less way regardless of whether the caller
    passed ``V``: the 3-arg cpp overload for a `Function`-valued ``g`` requires ``dofs`` to
    be a *paired* ``(dofs_in_V, dofs_in_g_space)`` sequence -- the mechanism for
    constraining a sub-space with a value on its collapsed counterpart, not what a flat
    ``dofs`` array plus a broadcast-style value (e.g. a
    :py:class:`~dolfinx_adjoint.Constant`, itself a `Function` on a single-dof real space)
    needs. The V-less overload already broadcasts a real-space `Function`'s single value
    across an arbitrary flat ``dofs`` array correctly. ``V`` is instead threaded through
    purely at the *Python* level, which :py:class:`dolfinx.fem.DirichletBC` keeps entirely
    independent of the cpp object: ``bc.function_space`` is whatever ``V`` is passed to the
    Python wrapper, never introspected from the cpp bc. See dolfinx-adjoint-knowledge's
    scratch/boundary-control/issues/01 for the bug this avoids.
    """
    dtype = g.dtype
    V_used = V if V is not None else g.function_space
    cpp_bc: (
        dolfinx.cpp.fem.DirichletBC_float32
        | dolfinx.cpp.fem.DirichletBC_float64
        | dolfinx.cpp.fem.DirichletBC_complex64
        | dolfinx.cpp.fem.DirichletBC_complex128
    )
    # cpp_bc is constructed inside each branch, immediately after its own isinstance
    # assert, rather than deferred to one call after the if/elif chain -- mypy can only
    # narrow g._cpp_object's union type within the branch the assert itself is in.
    if np.issubdtype(dtype, np.float32):
        assert isinstance(g._cpp_object, dolfinx.cpp.fem.Function_float32)
        cpp_bc = dolfinx.cpp.fem.DirichletBC_float32(g._cpp_object, dofs)
    elif np.issubdtype(dtype, np.float64):
        assert isinstance(g._cpp_object, dolfinx.cpp.fem.Function_float64)
        cpp_bc = dolfinx.cpp.fem.DirichletBC_float64(g._cpp_object, dofs)
    elif np.issubdtype(dtype, np.complex64):
        assert isinstance(g._cpp_object, dolfinx.cpp.fem.Function_complex64)
        cpp_bc = dolfinx.cpp.fem.DirichletBC_complex64(g._cpp_object, dofs)
    elif np.issubdtype(dtype, np.complex128):
        assert isinstance(g._cpp_object, dolfinx.cpp.fem.Function_complex128)
        cpp_bc = dolfinx.cpp.fem.DirichletBC_complex128(g._cpp_object, dofs)
    else:
        raise NotImplementedError(f"Type {dtype} not supported.")

    bc_kwargs: dict = {}
    # If dolfinx-version is 0.12 we need to pass the following
    # due to https://github.com/FEniCS/dolfinx/pull/4342/
    if Version(dolfinx.__version__).minor > 11:
        bc_kwargs["V"] = V_used
        bc_kwargs["g"] = g
    return cpp_bc, bc_kwargs


[docs] class DirichletBCBlock(Block): """A block representing a DirichletBC in the adjoint framework. Args: value: The value of the Dirichlet BC. dofs: An array of degree-of-freedom indices in `V` where the BC should be applied. V: The function space associated with the Dirichlet BC. ad_block_tag: An optional tag to identify this block in the adjoint framework. """ def __init__( self, value: dolfinx.fem.Function | dolfinx.fem.Constant, dofs: npt.NDArray[np.int32], V: dolfinx.fem.FunctionSpace | None = None, ad_block_tag: str | None = None, ): super().__init__(ad_block_tag=ad_block_tag) self._dofs = dofs self._V = V self.add_dependency(value) @property def dofs(self): return self._dofs @property def V(self): return self._V
[docs] def prepare_recompute_component(self, inputs, relevant_outputs): return inputs[0] if inputs else None
[docs] def recompute_component(self, inputs, block_variable, idx, prepared): """Return the (aliased) bc, having first resynced its live ``g.x.array`` from the value's own checkpoint at this tape position. ``DirichletBC._ad_create_checkpoint``/``_ad_restore_at_checkpoint`` (types/dirichletbc.py) both ``return self`` -- the bc's own "checkpoint" aliases the *live* bc object rather than snapshotting a value -- so nothing else writes a replayed/perturbed value back into ``bc.g``'s array. ``prepared`` (this block's single dependency's own, correctly-checkpointed value, from ``prepare_recompute_component``) is exactly that value: writing it into ``bc.g`` here, at the position in the tape this block itself occupies (always *before* any solve block that consumes ``bc``, since the bc must be constructed first), is what makes a later solve block see the right value regardless of which tape position is being replayed. See dolfinx-adjoint-knowledge's scratch/boundary-control/issues/02 for what breaks without this. """ bc = block_variable.saved_output if prepared is not None: bc.g.x.array[:] = prepared.x.array[:] bc.g.x.scatter_forward() return bc
[docs] def evaluate_tlm_component(self, inputs, tlm_inputs, block_variable, idx, prepared=None): """Return this bc's own tangent-linear value: itself a (plain, untracked) DirichletBC, with its value replaced by the perturbation direction. A bc perturbation is *not* an ordinary right-hand-side contribution -- it enters the tangent-linear solve as an inhomogeneous condition (`u_dot = g_dot` on this bc's dofs, see ``HomogeneousBCLinearProblem.tlm_bcs``/``solve()``), consumed by ``_ProblemBlockBase.prepare_evaluate_tlm``. Built plain (``dolfinx.fem.dirichletbc``, not the overloaded ``dolfinx_adjoint`` one) so it is never itself tape-recorded -- it exists only for this one TLM evaluation, not as a new control. """ tlm_input = tlm_inputs[0] if tlm_input is None: return None cpp_bc, bc_kwargs = build_cpp_bc_and_kwargs(tlm_input, self._dofs, self._V) return dolfinx.fem.DirichletBC(cpp_bc, **bc_kwargs)
[docs] def evaluate_adj_component(self, inputs, adj_inputs, block_variable, idx, prepared=None): """Return this bc's contribution to the adjoint action. ``adj_inputs[0]`` is the boundary reaction the consuming solve block(s) already computed (see ``_ProblemBlockBase._mask_reaction_to_bc``/``prepare_evaluate_adj``), already living on this bc's own constrained space -- no reduction needed here: ``types.dirichletbc._pack_bc_value`` always packs the bc's value into a Function on exactly that space before this block is ever created, whatever the original value was (a plain `Function`, a broadcasting `Constant`, or a general expression), so this block's single dependency and the masked reaction always agree. """ return adj_inputs[0]
[docs] def evaluate_hessian_component( self, inputs, hessian_inputs, adj_inputs, block_variable, idx, relevant_dependencies, prepared=None ): """Return this bc's contribution to the Hessian action. Same pass-through as ``evaluate_adj_component``, applied to the second-order boundary reaction (``prepare_evaluate_hessian``'s ``_adj_sol2_bdy``) instead of the first-order one -- the *entire* Hessian-action contribution for a Dirichlet bc control (see dolfinx-adjoint-knowledge's scratch/boundary-control/spec.md for why). """ return hessian_inputs[0]