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.trialfor the local residual argumentsuse
ctx.bindings["u"]when you want a named mixed field from the full mixed contextuse
ctx.spaces["U"]when the discrete space itself must be selected explicitlyif a mixed field was declared with
ResidualSpaces, alias keys such asctx.spaces["V"]orctx.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:
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
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.