MixedForm and Block Utilities#

This page shows the minimal usage of mixed formulations and block utilities in FluxFEM.

Mixed form (minimal)#

Define a mixed space, write per-field residuals, and solve in a single system:

import fluxfem as ff
import fluxfem.helpers_wf as h_wf
import numpy as np

mesh = ff.StructuredHexBox(nx=2, ny=1, nz=1, lx=1.0, ly=0.1, lz=0.1).build()
space = ff.make_hex_space(mesh, dim=1, intorder=2)
mixed = ff.MixedSpaces(
    {
        "u": ff.ResidualSpaces(
            test=ff.NamedSpace("V", space),
            unknown=ff.NamedSpace("U", space),
        ),
        "T": ff.ResidualSpaces(
            test=ff.NamedSpace("PSI", space),
            unknown=ff.NamedSpace("THETA", space),
        ),
    }
).to_fe_space()

def res_T(v, T, p):
    return (p.kappa * h_wf.gaction(v, h_wf.grad(T)) - v * p.q) * h_wf.dOmega()

def res_u(v, u, p):
    T_ref = ff.unknown_ref("T", space="THETA")
    return p.E * h_wf.gaction(v, h_wf.grad(u)) * h_wf.dOmega() - p.alpha * h_wf.gaction(v, T_ref.val) * h_wf.dOmega()

residuals = ff.make_mixed_residuals(
    u=ff.bind_mixed_residual("u", res_u, space="U"),
    T=ff.bind_mixed_residual("T", res_T, space="THETA"),
)
params = ff.Params(kappa=1.0, q=1.0, E=1.0, alpha=1.0e-3)

bc = mixed.make_dirichlet(
    u=([0], [0.0]),
    T=([0], [0.0]),
)

u0 = np.zeros(mixed.n_dofs)
problem = ff.MixedProblem(mixed, residuals, params=params)
K = problem.assemble_jacobian(u0)
R0 = problem.assemble_residual(u0)
b = -R0
sol, _ = ff.LinearSolver(method="spsolve").solve(
    K, b, dirichlet=bc.as_dirichlet_bc(), dirichlet_mode="condense"
)
solution_fields = mixed.unpack_fields(sol)

The standard mixed lookup rules are:

  • use ctx.test / ctx.trial for the local residual arguments

  • use ctx.bindings["u"] when you want a named mixed field from the full mixed context

  • use ctx.spaces["U"] when the discrete space itself must be selected explicitly

  • if a mixed field was declared with ResidualSpaces, alias keys such as ctx.spaces["V"] or ctx.spaces["PSI"] resolve to the same field

Related mixed tutorials:

  • tutorials/thermoelastic_bar_1d_mixed.py

  • tutorials/nitsche_contact_supermesh_api.py

Mixed PDE view#

The residual definitions above correspond to a coupled continuum system:

\[\begin{split}-\nabla\cdot(\kappa\,\nabla T) &= q, \\ -\nabla\cdot(E\,\nabla u) &= \alpha\,\nabla\cdot T.\end{split}\]

The temperature residual res_T mirrors \(\int_\Omega \kappa \nabla v\cdot\nabla T - v q\) and res_u mimics \(\int_\Omega E \nabla v\cdot\nabla u - \alpha \nabla v\cdot T\). Because both weak forms live in the same MixedProblem, FluxFEM assembles the full coupled Jacobian and RHS, including the off-diagonal coupling between u and T.

Block utilities (minimal)#

Build a lazy block matrix (for manual assembly or inspection):

import numpy as np
from fluxfem import solver as ff_solver

diag = ff_solver.block_diag(order=("a", "b"), a=np.eye(2), b=2.0 * np.eye(2))
blocks = ff_solver.make_block_matrix(
    diag=diag,
)

# blocks["a"]["a"], blocks["a"]["b"], blocks["b"]["a"], blocks["b"]["b"]
K = blocks.assemble()

Off-diagonal coupling (K12 / K21)#

For coupled/contact problems, you often have off-diagonal blocks. Use rel to insert K12 and (optionally) its transpose:

rel = {
    ("u", "p"): K_up,
}
blocks = ff_solver.make_block_matrix(
    diag=ff_solver.block_diag(order=("u", "p"), u=K_uu, p=K_pp),
    rel=rel,
    symmetric=True,
    transpose_rule="T",
)

This is commonly used in contact or mixed formulations. See the following tutorial scripts for full examples:

  • tutorials/nitsche_contact_supermesh_api.py

  • tutorials/nitsche_contact_supermesh_demo_fluxfem.py

Block matrix intuition#

The assembled Jacobian from the mixed residuals can be viewed as a block operator

\[\begin{split}\begin{bmatrix} K_{uu} & K_{uT} \\ K_{Tu} & K_{TT} \end{bmatrix},\end{split}\]

where the diagonal entries come from the self-residuals (res_u / res_T) and the off-diagonal blocks arise from cross-couplings. The block helpers above let you construct this matrix explicitly: diag supplies K_{uu}/K_{TT} and rel inserts pairs such as K_{uT}. Passing the resulting blocks dictionary into mixed.build_block_system makes it easy to swap in Schur-complement solvers, block preconditioners, or other multiphysics strategies.