1. A 2D Laplacian, node by node

\(-\Delta u = f\)

A Mesh (see Domains & meshes), a basis of local shape functions, and VariablesFE to glue them into one global vector of nodal values. EllipticFEscheme assembles the weak form and solves — matrix-free here, by conjugate gradient, since a pure diffusion operator is symmetric positive definite.

import jax.numpy as jnp
from scimba_jax.linear_approximation.basis.analytic_bases import local_lagrange_basis
from scimba_jax.linear_approximation.basis.general_bases import AnalyticBasis
from scimba_jax.linear_approximation.galerkin.fem.elliptic_fe_scheme import (
    EllipticFEscheme,
)
from scimba_jax.linear_approximation.meshes.mesh import Mesh
from scimba_jax.linear_approximation.quad.gauss_quad import UnitSquareTensorized
from scimba_jax.linear_approximation.variables.variables_fe import VariablesFE
from scimba_jax.mapping.mapping import InvertibleFunction, Mapping
from scimba_jax.physical_models.abstract_physical_weak_model import (
    AbstractPhysicalWeakModel,
)
from scimba_jax.physical_models.classical_weakform.laplacian_weak_form import (
    LaplacianWeakForm,
)

order = 2
mesh = Mesh(
    dim=2,
    n_cells=(24, 24),
    ref_quad=UnitSquareTensorized(dim=2, order=2 * order + 1),
    mapping=Mapping(mappings=[InvertibleFunction(lambda x: x, lambda y: y)]),
)
basis = AnalyticBasis(
    nb_basis=(order + 1) ** 2,
    out_dim=1,
    mesh=mesh,
    local_basis=lambda y, i, m: local_lagrange_basis(y, i, m, order=order, out_dim=1),
    basis_type="scalar",
)
# One value per NODE, shared between cells -- sparse-and-symmetric where DG is block-diagonal.
variables = VariablesFE(basis=basis, nb_variables=1)


def source(x):  # so that u(x, y) = sin(pi x) sin(pi y)
    return 2 * jnp.pi**2 * jnp.sin(jnp.pi * x[0]) * jnp.sin(jnp.pi * x[1])


pde = LaplacianWeakForm(dim=2, f=source)
model = AbstractPhysicalWeakModel.from_weak_form(pde, dirichlet=lambda x: jnp.zeros(1))
scheme = EllipticFEscheme(model, variables)
scheme = EllipticFEscheme.solve(scheme, matrix_free=True, tol=1e-10)
# Q2 on 24x24 cells: 1.8e-05 relative L2 error against the manufactured solution.

2. A nonlinear coefficient needs Newton

\(-\nabla\cdot(A(u)\nabla u) = f\)

As soon as the diffusion coefficient A depends on u itself, the problem is no longer linear — a bare CG solve has nothing to converge to. NLEllipticWeakForm differentiates A(u) automatically, and NewtonSolver takes it from there. See Nonlinear solvers for what else is on offer once a problem needs it.

from scimba_jax.physical_models.classical_weakform.nl_diffusion_advection_reaction_weak_form import (  # noqa: E501
    NLEllipticWeakForm,
)
from scimba_jax.linear_approximation.solvers.newton import NewtonSolver

# A(u) = 1 + 10 u^2, not a constant -- see Nonlinear solvers for what changes.
pde = NLEllipticWeakForm(
    dim=2, A=lambda x, u: (1.0 + 10.0 * u**2) * jnp.eye(2), f=source
)
model = AbstractPhysicalWeakModel.from_weak_form(pde, dirichlet=lambda x: jnp.zeros(1))
scheme = EllipticFEscheme(model, variables)
scheme, report = EllipticFEscheme.solve(
    scheme, solver=NewtonSolver(max_iter=20, tol=1e-10), return_report=True
)
# 6 Newton iterations, residual 6.4e-11.

3. Time-dependent problems

\(\partial_t u - \Delta u = 0\)

TimeDiscreteFEscheme turns the elliptic scheme above into one stage of a Butcher tableau, and re-solves it at every stage of every time step — the basis, the mesh and the Dirichlet lift are untouched, only the right-hand side changes. Explicit Euler is the cheapest choice per step but conditionally stable; implicit Euler and the second-order Pareschi–Russo scheme trade a (still linear, still solved by the same machinery) implicit stage for a time step that does not shrink with the mesh.

from scimba_jax.linear_approximation.galerkin.fem.time_discrete_fe_scheme import (
    TimeDiscreteFEscheme,
)
from scimba_jax.time_discrete.butcher_tableau import build_implicit_euler_tableau

# du/dt - u_xx = 0, u(., 0) = sin(pi x). TimeDiscreteFEscheme re-solves the
# SAME elliptic weak form at every stage of a Butcher tableau.
mesh_1d = Mesh(dim=1, n_cells=(16,), ref_quad=UnitSquareTensorized(dim=1, order=4),
               mapping=Mapping(mappings=[InvertibleFunction(lambda x: x, lambda y: y)]))
basis_1d = AnalyticBasis(
    nb_basis=2, out_dim=1, mesh=mesh_1d,
    local_basis=lambda y, i, m: local_lagrange_basis(y, i, m, order=1, out_dim=1),
    basis_type="scalar",
)
spatial_pde = LaplacianWeakForm(dim=1, f=lambda x: jnp.zeros(()))
scheme = TimeDiscreteFEscheme(
    spatial_weak_form_factory=spatial_pde,
    variables=VariablesFE(basis=basis_1d, nb_variables=1),
    butcher_tableau=build_implicit_euler_tableau(),
    dt=0.1 / 8,
    dirichlet=lambda x: jnp.zeros(1),
)
dofsl_init = scheme.initialize(lambda x: jnp.sin(jnp.pi * x))
dofsl_final, history = scheme.solve(dofsl_init, t0=0.0, nt=8)
# Implicit Euler: unconditionally stable, first order. 8 steps to t=0.1,
# 16 P1 cells: 5.5e-02 relative L2 error (dominated by the coarse mesh).

A fine mesh makes each Krylov iteration slower to converge, not just slower to run — see Multigrid for the fix that keeps the iteration count flat as the mesh refines.