1. Fixed point — freeze the coefficient, solve, repeat

\(-\nabla\cdot(A(u_k)\nabla u_{k+1}) = f\)

The oldest strategy, and worth understanding even though it is not the one to reach for by default: evaluate A at the previous iterate, treat it as a known function of x, and solve the resulting LINEAR problem — exactly the FEM or DG solve from the earlier pages. Repeat until two iterates agree. It converges linearly at best, and is not guaranteed to converge at all on a coefficient that is not monotone in u.

# The oldest strategy: FREEZE A at the previous iterate and solve the
# resulting LINEAR problem, repeating until two iterates agree.
#
#   u_{k+1} = solve(-div(A(u_k) grad u) = f)     -- a LINEAR solve
#
# Picard / fixed-point: converges LINEARLY at best, and need not converge
# at all if A is not monotone in u. scimba_jax ships no separate class for
# it -- Newton below needs the same per-step solve and converges
# QUADRATICALLY, so it is the entry point rather than a hand-written loop.
# What fixed point buys over Newton: it never differentiates A(u), useful
# if A is only known numerically (e.g. from a table).

2. Newton — differentiate instead of freezing

\(J(u_k)\,\delta u = -R(u_k), \qquad u_{k+1} = u_k + \delta u\)

Newton solves for a correction using the TRUE Jacobian — A(u) differentiated with respect to u, not held fixed — and converges quadratically once it is close to the solution. In practice it wins whenever it converges at all: scimba_jax's default nonlinear solver is plain Newton, with none of the acceleration or damping strategies (line search, Anderson acceleration, Levenberg-Marquardt) turned on unless a specific problem needs one.

from scimba_jax.linear_approximation.solvers.newton import NewtonSolver

# Exact linearisation: solve J(u_k) delta = -F(u_k) for a correction, with
# J the true Jacobian (differentiated through A(u), not frozen).
solver = NewtonSolver(max_iter=20, tol=1e-10)
scheme, report = EllipticFEscheme.solve(scheme, solver=solver, return_report=True)
# 6 iterations to 1e-10 -- a fixed-point loop on the same problem can need
# dozens, or fail to converge, depending on how strongly A varies with u.

3. Newton, preconditioned by a multigrid that follows it

On a fine mesh, Newton's inner linear solve is the expensive part — which is exactly what Multigrid addresses. The complication is that the operator being preconditioned changes at every Newton step, so the multigrid has to be refreshed without paying for a full rebuild each time.

from scimba_jax.linear_approximation.solvers.multigrid import (
    newton_krylov_relinearised,
)

# Newton's linear solve is the expensive part on a fine mesh -- precondition
# it with the SAME multigrid, refreshed each step since the operator moves.
# `relinearise` redoes only what depends on the Jacobian (smoother diagonal,
# coarse operator), reusing the meshes, transfers and colouring.
dofsl, n_newton, n_krylov, residual = newton_krylov_relinearised(
    scheme, dofsl_init, mg, tol=1e-10, max_iter=20, every=1
)
# 64x64 mesh, 3 levels: 9 Newton steps, 30 CG iterations, 0.4 s -- against
# 46 s (33 800 CG iterations) for a multigrid that is never refreshed.

4. FAS — a multigrid with no Jacobian at all

The Full Approximation Scheme skips linearisation entirely: it runs a multigrid cycle directly on the nonlinear residual, correcting the coarse level's right-hand side so it solves a problem consistent with the fine one (Brandt's "tau" correction). No Newton step, no Jacobian, no Krylov loop — at the cost of a genuinely nonlinear solve on the coarsest level and a smoother that must sweep the current, moving operator rather than a frozen one.

from scimba_jax.linear_approximation.solvers.multigrid import MG
from scimba_jax.linear_approximation.solvers.newton import NewtonSolver
from scimba_jax.linear_approximation.solvers.smoothers import DampedJacobiSmoother

# FAS: a multigrid that never linearises. The ordinary cycle descends a
# CORRECTION (assumes A(u+e) = Au + Ae, false here); FAS descends the
# SOLUTION itself, correcting the coarse RHS with Brandt's "tau" term.
mg = MG(
    hierarchy, DampedJacobiSmoother(omega=0.6, nonlinear=True),
    nu_pre=2, nu_post=2, fas=True,
    coarse_solver=NewtonSolver(max_iter=30, tol=1e-13),
)
zeros = scheme._initial_dofs()
dofsl, n_cycles, residual = mg.solve(
    jnp.zeros(zeros.size), tol=1e-9, relinearise=True
)
# 14 FAS cycles, 1.3 s -- no Newton step, no Jacobian, no Krylov loop; the
# cost moves to a NONLINEAR smoother and a real solve on the coarsest level.
# `relinearise=True` is not a tuning knob: the smoother sweeps A(u_k) at a
# fixed point, and sweeping the WRONG one diverges to nan here, measured.

\(-\nabla\cdot\big((1 + 10 u^2)\nabla u\big) = f\)

Measured on the equation above, a 64×64 mesh, 3 levels: Newton preconditioned by a multigrid that is refreshed at every step needs 9 outer steps and 30 conjugate-gradient iterations in total, 0.4 s; the same Newton loop with a multigrid built once and never refreshed needs 33 800 conjugate-gradient iterations for the same answer, 46 s. FAS reaches the same accuracy in 14 cycles and 1.3 s, with no linear solve at all.