Sign in

    moonmultigrid

    Matrix-free geometric multigrid for rectangular 2D diffusion-reaction problems

    multigrid
    poisson
    diffusion
    numerical
    elliptic
    Download zip
    Author
    Version
    0.1.0
    License
    MIT
    Last updated
    2 days ago
    Downloads
    6

    #MoonMultigrid

    A MoonBit library for geometric multigrid on 2D rectangular nodal diffusion–reaction equations:

    -div(k grad u) + c u = f, with prescribed Dirichlet values on all four edges.

    Module jqlssg/moonmultigrid@0.1.0, MIT. This is a geometric solver, not algebraic multigrid, a general sparse solver, or a CFD framework. It is original code, not a PyAMG port.

    #Install and use

    moon add jqlssg/moonmultigrid@0.1.0

    Add "jqlssg/moonmultigrid" @mg to your package imports. A complete public-API example:

    fn main raise {
    let grid = @mg.Grid::new(32, 32)
    let op = @mg.Operator::constant(grid)
    let source = @mg.Field::sample(grid, fn(_x, _y) { 1.0 })
    let boundary = @mg.Field::zeros(grid)
    let problem = @mg.Problem::new(op, source, boundary)
    let hierarchy = @mg.Hierarchy::new(op)
    let report = hierarchy.pcg(problem)
    assert_true(report.converged())
    println(report.relative_residual())
    }

    Do not consume solution() as a successful solve without inspecting converged() or reason(). Iteration and estimated-work exhaustion are explicit unsuccessful reports; invalid inputs and numerical breakdown raise SolverError.

    #Three reusable workflows

    UserInput and processingOutput / runnable integration
    Thermal/diffusion simulation authorPositive conductivity field, source and arbitrary edge temperatures; FMG warm start then MG-PCGTemperature field, true residual and oriented face fluxes; moon run cmd/heat --target js
    Imaging developerSource and destination rectangular channel patches; source or mixed forward gradients plus destination Dirichlet edgesUnclipped Poisson-edited patch, pasted into a new image field; moon run cmd/edit --target js
    Numerical ecosystem authorAnalytic/discrete manufactured equation; compare CG versus MG-PCG and export interior COOConvergence history, discretization error, sparse/vector bridge; moon run cmd/compare --target js

    These are library workflows, not file-format image codecs or an interactive visual editor. Channels are processed separately and physical pixel spacing must match when pasting.

    #Model and indexing contract

    • Grid::new(nx, ny, lx?, ly?): interval counts, hence (nx+1)*(ny+1) samples. Coordinates are (i*lx/nx,j*ly/ny). Flattened index is j*(nx+1)+i (x-fast). The minimum is 2 intervals per axis, maximum 1024.
    • Field::new validates all samples and copies its input. samples(), flux arrays, matrix entries and histories return snapshots. Use with_value, map, add for new fields; interpolate is bilinear within the closed rectangle.
    • Conductivity is positive, finite and nodal; harmonic means define shared faces. Reaction is a constant finite c >= 0. Interior rows are five-point symmetric positive definite under Dirichlet elimination.
    • Operator::apply includes supplied field edges in interior stencils and emits zero edges. Problem uses source interior only, boundary edges only, and lifts nonzero boundary data. Physical boundary values are restored in returned solutions.
    • Problem::effective_rhs() and Field::interior_values() are the RHS to pair with Operator::interior_matrix() in another linear solver. Restore its homogeneous result using Problem::restore.
    • Grid equality includes lengths. Reusing a hierarchy with different coefficients or reaction is rejected.

    #Multilevel algorithms

    Hierarchy::new(op, coarse_limit?=9) halves both even interval counts and injects nodal conductivity. Operators are rediscretized, not Galerkin products. Odd/undersized axes stop coarsening; a terminal system over 81 unknowns is rejected rather than silently becoming a large dense solve. For example 32x16 is supported, while 128x3 cannot reach a bounded coarse system.

    The terminal operator is factored once with dense Cholesky and reused. Full-weighting restriction and bilinear prolongation satisfy R = P^T / 4 on homogeneous boundary interiors. V/W cycles use weighted Jacobi (default 2/3) or symmetric forward+reverse lexicographic Gauss–Seidel. CycleConfig::new allows 1..8 pre/post smoothing sweeps (SGS sweeps are forward/reverse pairs).

    • h.solve(problem, options?, config?, initial?): residual-controlled repeated V/W cycles.
    • h.pcg(...): fixed V-cycle preconditioned CG; preconditioned=false is the plain CG comparison. W-cycle PCG is deliberately rejected. Inner-product positivity is checked and the true residual is recomputed after every step.
    • h.precondition(rhs, config?): reusable zero-initialized homogeneous cycle.
    • h.full_multigrid(problem, cycles_per_level?=1, config?): nested initializer only, not a residual convergence guarantee. Feed its output into a controlled solve.
    • solve(problem, ...): convenience function constructing a new hierarchy; retain a hierarchy for repeated RHSs.

    SolveOptions defaults: rtol=1e-8, atol=1e-12, max_iterations=100, work_budget=100000000. Convergence uses ||b_eff-Au_correction||2 <= max(atol,rtol*||b_eff||2) over interior nodes. The history includes the initial residual. relative_residual() returns the absolute residual when ||b_eff||=0. Boundary lifting can make b_eff large, so choose absolute tolerances appropriate to your physical scaling. Floating-point cancellation may cause small differences when independently evaluating the restored physical equation.

    Work units are a deterministic estimate of row work, not elapsed time, instructions or a full allocation quota. The iterative budget excludes construction/factorization and initial residual evaluation. Coarse work is estimated quadratically; W visits are included. FMG has a separate 1..8 cycles-per-level bound. StopReason distinguishes Converged, IterationLimit, and WorkLimit.

    #Diagnostics and interchange

    op.flux(u) returns -k_face * forward_difference(u). x faces have nx*(ny+1) entries, index j*nx+i; y faces have (nx+1)*ny, index j*(nx+1)+i. Its interior divergence plus c*u agrees with op.apply(u). These arrays include tangential edge-row/column faces; they are not a Neumann boundary API. op.contrast() is nodal max/min and may overflow for extreme ratios.

    InteriorMatrix exposes 0-based interior row-major COO entries with positive diagonal and negative off-diagonals. Export has a conservative 5*unknowns entry budget. It is an integration bridge, not a replacement general sparse library.

    Field::to_json() gives {nx,ny,lx,ly,values}; stringify with .stringify(). field_from_json requires exactly these keys, integer bounded intervals, finite numbers and exact sample count. Text is limited to 32 million UTF-16 units and nesting to four. The core JSON parser's duplicate-key behavior is retained (last value wins); do not use snapshots as signed canonical documents. to_csv() emits i,j,x,y,value including all boundaries. JSON snapshots are fields, not arbitrary solver-execution requests.

    #Reproduce verification

    Local implementation toolchain: moon 0.1.20260920 (914d7da), moonc v0.10.14+7d59c7ec9-dev. CI installs the official stable distribution, records exact versions and tests clean runners; upstream stable is mutable and future changes may require a compatibility update.

    moon info moon fmt --check moon check --deny-warn moon test --target wasm-gc --deny-warn moon test --target wasm --deny-warn moon test --target js --deny-warn moon test --target native --deny-warn moon build --target js --deny-warn python scripts/differential.py --target js python scripts/audit.py

    CI checks/builds/tests all four targets and runs all three downstream examples per target; JS also runs the independent differential harness. Native requires a C compiler (Ubuntu CI provides one); it was not runnable in the original Windows environment. Interface regeneration is checked against tracked .mbti files.

    Current measured tests: 25 MoonBit tests per tested target. Python constructs 12 independent heterogeneous matrices (4/6/8/12 x 4/6/8 interval rectangles), incorporates nonzero boundaries, and solves with partial-pivot Gaussian elimination; JS maximum absolute nodal discrepancy 1.78e-12, acceptance <1e-9. No exported stencil/COO is used by that reference. Analytic quartic refinement verifies approximately second-order error; operator/preconditioner symmetry, linearity, positivity and transfer adjointness are separately tested.

    On the 32x32 interval analytic quartic example, JS in the above Windows toolchain with rtol=1e-10 took 9 MG-PCG versus 100 plain CG iterations and achieved RMS discretization error 0.0001662418888414. This is a single reproducible iteration-count observation, not a wall-clock speedup or a general comparison to PyAMG.

    #Boundaries, provenance and licensing

    No AMG, unstructured mesh, 3D, Neumann/periodic conditions, GPU/distributed solve, nonlinear or transient PDE engine. Point smoothers and coefficient injection can converge slowly for high contrast, anisotropy or unresolved coefficient jumps. No mesh-independent convergence claim for all permitted inputs. Finite inputs can still overflow during arithmetic; numerical guards reject detected breakdowns. Keep tolerances within the useful precision of your scaling.

    See architecture, security, references and AI disclosure, changelog, bounded duplication evidence, and acceptance contract. The library has no third-party MoonCakes dependencies; it uses the MoonBit core library. No registration documents or credentials are included in distribution.

    SolverError

    pub(all) suberror SolverError {
    Invalid(String)
    Shape(String)
    Budget(String)
    Numerical(String)
    } derive(Eq,
    Debug
    )

    SolverError::equal

    fn SolverError::equal(SolverError, SolverError) -> Bool

    SolverError::not_equal

    fn SolverError::not_equal(x : SolverError, y : SolverError) -> Bool

    CoarseSolver

    pub struct CoarseSolver {
    grid : Grid
    lower : Array[Double]
    }

    Bounded dense Cholesky factor, built once and reusable for coarse right-hand sides.

    CoarseSolver::new

    fn CoarseSolver::new(operator : Operator) -> CoarseSolver raise SolverError

    CoarseSolver::solve

    fn CoarseSolver::solve(self : CoarseSolver, rhs : Field) -> Field raise SolverError

    Solves only the homogeneous-boundary interior system; boundary rhs is ignored.

    CycleConfig

    pub struct CycleConfig {
    kind : CycleKind
    smoother : Smoother
    sweeps : Int
    }

    CycleConfig::kind

    fn CycleConfig::kind(self : CycleConfig) -> CycleKind

    CycleConfig::new

    fn CycleConfig::new(kind? : CycleKind, smoother? : Smoother, sweeps? : Int) -> CycleConfig raise SolverError

    CycleConfig::smoother

    fn CycleConfig::smoother(self : CycleConfig) -> Smoother

    CycleConfig::sweeps

    fn CycleConfig::sweeps(self : CycleConfig) -> Int

    CycleKind

    pub(all) enum CycleKind {
    V
    W
    } derive(Eq,
    Debug
    )

    CycleKind::equal

    fn CycleKind::equal(CycleKind, CycleKind) -> Bool

    CycleKind::not_equal

    fn CycleKind::not_equal(x : CycleKind, y : CycleKind) -> Bool

    Field

    pub struct Field {
    grid : Grid
    values : Array[Double]
    }

    Immutable-by-API full nodal field; all public array access is a snapshot.

    Field::add

    fn Field::add(self : Field, other : Field, scale? : Double) -> Field raise SolverError

    Field::boundary_only

    fn Field::boundary_only(self : Field) -> Field

    Field::dot

    fn Field::dot(self : Field, other : Field) -> Double raise SolverError

    Field::from_interior

    fn Field::from_interior(grid : Grid, values : Array[Double]) -> Field raise SolverError

    Restore an external solver's interior vector onto homogeneous edges.

    Field::get

    fn Field::get(self : Field, i : Int, j : Int) -> Double raise SolverError

    Field::grid

    fn Field::grid(self : Field) -> Grid

    Field::interior_norm

    fn Field::interior_norm(self : Field) -> Double

    Field::interior_values

    fn Field::interior_values(self : Field) -> Array[Double]

    Field::interpolate

    fn Field::interpolate(self : Field, x : Double, y : Double) -> Double raise SolverError

    Continuous bilinear interpolation, including the closed rectangle edges.

    Field::map

    fn Field::map(self : Field, f : (Double) -> Double) -> Field raise SolverError

    Field::max_abs

    fn Field::max_abs(self : Field) -> Double

    Field::new

    fn Field::new(grid : Grid, values : Array[Double]) -> Field raise SolverError

    Field::paste

    fn Field::paste(self : Field, patch : Field, left : Int, bottom : Int) -> Field raise SolverError

    Copy a compatible patch into a new field, including the patch boundary.

    Field::patch

    fn Field::patch(self : Field, left : Int, bottom : Int, nx : Int, ny : Int) -> Field raise SolverError

    Extract a rectangular nodal patch, preserving physical pixel spacing.

    Field::rms_error

    fn Field::rms_error(self : Field, reference : Field) -> Double raise SolverError

    Field::sample

    fn Field::sample(grid : Grid, f : (Double, Double) -> Double) -> Field raise SolverError

    Field::samples

    fn Field::samples(self : Field) -> Array[Double]

    Field::to_csv

    fn Field::to_csv(self : Field) -> String

    Long-form CSV with x-fast ordering for plotting and spreadsheet ingestion.

    Field::to_json

    fn Field::to_json(self : Field) -> Json

    Self-describing bounded field snapshot, not a general PDE configuration.

    Field::with_value

    fn Field::with_value(self : Field, i : Int, j : Int, value : Double) -> Field raise SolverError

    Field::zero_boundary

    fn Field::zero_boundary(self : Field) -> Field

    Field::zeros

    fn Field::zeros(grid : Grid) -> Field

    Flux

    pub struct Flux {
    grid : Grid
    x : Array[Double]
    y : Array[Double]
    }

    Oriented face fluxes: x faces are nx*(ny+1), y faces (nx+1)*ny.

    Flux::divergence

    fn Flux::divergence(self : Flux) -> Field raise SolverError

    Interior divergence; boundary nodes are zero, not boundary flux conditions.

    Flux::x_faces

    fn Flux::x_faces(self : Flux) -> Array[Double]

    Flux::y_faces

    fn Flux::y_faces(self : Flux) -> Array[Double]

    Grid

    pub struct Grid {
    nx : Int
    ny : Int
    lx : Double
    ly : Double
    } derive(Eq,
    Debug
    )

    Rectangular nodal grid; nx/ny are interval counts, not point counts.

    Grid::coarsen

    fn Grid::coarsen(self : Grid) -> Grid raise SolverError

    Grid::coordinate

    fn Grid::coordinate(self : Grid, i : Int, j : Int) -> (Double, Double) raise SolverError

    Grid::equal

    fn Grid::equal(Grid, Grid) -> Bool

    Grid::index

    fn Grid::index(self : Grid, i : Int, j : Int) -> Int raise SolverError

    Grid::intervals

    fn Grid::intervals(self : Grid) -> (Int, Int)

    Grid::is_boundary

    fn Grid::is_boundary(self : Grid, i : Int, j : Int) -> Bool

    Grid::lengths

    fn Grid::lengths(self : Grid) -> (Double, Double)

    Grid::new

    fn Grid::new(nx : Int, ny : Int, lx? : Double, ly? : Double) -> Grid raise SolverError

    Grid::not_equal

    fn Grid::not_equal(x : Grid, y : Grid) -> Bool

    Grid::shape

    fn Grid::shape(self : Grid) -> (Int, Int)

    Grid::size

    fn Grid::size(self : Grid) -> Int

    Grid::spacing

    fn Grid::spacing(self : Grid) -> (Double, Double)

    Grid::to_repr

    Grid::unknowns

    fn Grid::unknowns(self : Grid) -> Int

    Hierarchy

    pub struct Hierarchy {
    operators : Array[Operator]
    coarse : CoarseSolver
    }

    Rediscretized geometric levels, finest first. Nodal k is injected.

    Hierarchy::cycle

    fn Hierarchy::cycle(self : Hierarchy, rhs : Field, initial : Field, config? : CycleConfig) -> Field raise SolverError

    One homogeneous-boundary geometric cycle. Input edges are ignored/zeroed.

    Hierarchy::cycle_work

    fn Hierarchy::cycle_work(self : Hierarchy, config? : CycleConfig) -> Int raise SolverError

    Deterministic row-work units, not measured CPU operations or runtime.

    Hierarchy::full_multigrid

    fn Hierarchy::full_multigrid(self : Hierarchy, problem : Problem, cycles_per_level? : Int, config? : CycleConfig) -> Field raise SolverError

    Nested iteration initializer, not a convergence claim. Pass the result as a warm start to solve/pcg for residual-controlled termination.

    Hierarchy::grid_complexity

    fn Hierarchy::grid_complexity(self : Hierarchy) -> Double

    Ratio of total stored interior nodes to fine-grid interior nodes.

    Hierarchy::grids

    fn Hierarchy::grids(self : Hierarchy) -> Array[Grid]

    Hierarchy::level

    fn Hierarchy::level(self : Hierarchy, level : Int) -> Operator raise SolverError

    Hierarchy::levels

    fn Hierarchy::levels(self : Hierarchy) -> Int

    Hierarchy::new

    fn Hierarchy::new(operator : Operator, coarse_limit? : Int) -> Hierarchy raise SolverError

    Hierarchy::pcg

    fn Hierarchy::pcg(self : Hierarchy, problem : Problem, options? : SolveOptions, config? : CycleConfig, initial? : Field?, preconditioned? : Bool) -> SolveReport raise SolverError

    Preconditioned CG with true-residual replacement at every iteration. Only the symmetric V-cycle is admitted as a fixed SPD preconditioner.

    Hierarchy::precondition

    fn Hierarchy::precondition(self : Hierarchy, rhs : Field, config? : CycleConfig) -> Field raise SolverError

    Linear zero-initialized cycle for reusable preconditioning.

    Hierarchy::solve

    fn Hierarchy::solve(self : Hierarchy, problem : Problem, options? : SolveOptions, config? : CycleConfig, initial? : Field?) -> SolveReport raise SolverError

    Hierarchy::solve_coarsest

    fn Hierarchy::solve_coarsest(self : Hierarchy, rhs : Field) -> Field raise SolverError

    InteriorMatrix

    pub struct InteriorMatrix {
    grid : Grid
    entries : Array[(Int, Int, Double)]
    }

    Interior row-major COO, with row/column indices in 0..<unknowns.

    InteriorMatrix::dimension

    fn InteriorMatrix::dimension(self : InteriorMatrix) -> Int

    InteriorMatrix::entries

    fn InteriorMatrix::entries(self : InteriorMatrix) -> Array[(Int, Int, Double)]

    InteriorMatrix::multiply

    fn InteriorMatrix::multiply(self : InteriorMatrix, values : Array[Double]) -> Array[Double] raise SolverError

    Manufactured

    pub struct Manufactured {
    problem : Problem
    exact : Field
    }

    Equation plus independently supplied exact nodal solution.

    Manufactured::error

    fn Manufactured::error(self : Manufactured, solution : Field) -> Double raise SolverError

    RMS interior error. Boundary discrepancy is reported separately by Problem.

    Manufactured::exact

    fn Manufactured::exact(self : Manufactured) -> Field

    Manufactured::problem

    fn Manufactured::problem(self : Manufactured) -> Problem

    Operator

    pub struct Operator {
    grid : Grid
    conductivity : Field
    reaction : Double
    west : Array[Double]
    east : Array[Double]
    south : Array[Double]
    north : Array[Double]
    diagonal : Array[Double]
    }

    Conservative five-point -div(k grad) + reaction on interior nodes.

    Operator::apply

    fn Operator::apply(self : Operator, u : Field) -> Field raise SolverError

    Applies interior PDE rows. Output boundary is zero, not identity rows.

    Operator::conductivity

    fn Operator::conductivity(self : Operator) -> Field

    Operator::constant

    fn Operator::constant(grid : Grid, conductivity? : Double, reaction? : Double) -> Operator raise SolverError

    Operator::contrast

    fn Operator::contrast(self : Operator) -> Double

    Nodal conductivity max/min. Large contrast can spoil geometric convergence.

    Operator::flux

    fn Operator::flux(self : Operator, u : Field) -> Flux raise SolverError

    Operator::grid

    fn Operator::grid(self : Operator) -> Grid

    Operator::interior_matrix

    fn Operator::interior_matrix(self : Operator, max_entries? : Int) -> InteriorMatrix raise SolverError

    Operator::new

    fn Operator::new(conductivity : Field, reaction? : Double) -> Operator raise SolverError

    Operator::reaction

    fn Operator::reaction(self : Operator) -> Double

    Operator::residual

    fn Operator::residual(self : Operator, rhs : Field, u : Field) -> Field raise SolverError

    Operator::stencil

    fn Operator::stencil(self : Operator, i : Int, j : Int) -> (Double, Double, Double, Double, Double) raise SolverError

    Returns (diagonal, west, east, south, north), with positive face magnitudes.

    Problem

    pub struct Problem {
    operator : Operator
    source : Field
    boundary : Field
    rhs : Field
    }

    Source and prescribed rectangle-edge values. Interior boundary-field entries are ignored.

    Problem::boundary

    fn Problem::boundary(self : Problem) -> Field

    Problem::boundary_error

    fn Problem::boundary_error(self : Problem, solution : Field) -> Double raise SolverError

    Problem::correction

    fn Problem::correction(self : Problem, solution : Field) -> Field raise SolverError

    Problem::effective_rhs

    fn Problem::effective_rhs(self : Problem) -> Field

    Homogeneous-boundary equation for the interior correction.

    Problem::new

    fn Problem::new(operator : Operator, source : Field, boundary : Field) -> Problem raise SolverError

    Problem::operator

    fn Problem::operator(self : Problem) -> Operator

    Problem::residual

    fn Problem::residual(self : Problem, solution : Field) -> Field raise SolverError

    Problem::restore

    fn Problem::restore(self : Problem, correction : Field) -> Field raise SolverError

    Problem::source

    fn Problem::source(self : Problem) -> Field

    Smoother

    pub(all) enum Smoother {
    Jacobi(Double)
    SymmetricGaussSeidel
    } derive(Eq,
    Debug
    )

    Smoother::apply

    fn Smoother::apply(self : Smoother, operator : Operator, rhs : Field, initial : Field, sweeps : Int) -> Field raise SolverError

    Smoother::equal

    fn Smoother::equal(Smoother, Smoother) -> Bool

    Smoother::not_equal

    fn Smoother::not_equal(x : Smoother, y : Smoother) -> Bool

    Smoother::to_repr

    SolveOptions

    pub struct SolveOptions {
    rtol : Double
    atol : Double
    max_iterations : Int
    work_budget : Int
    }

    SolveOptions::new

    fn SolveOptions::new(rtol? : Double, atol? : Double, max_iterations? : Int, work_budget? : Int) -> SolveOptions raise SolverError

    SolveReport

    pub struct SolveReport {
    solution : Field
    reason : StopReason
    iterations : Int
    work_units : Int
    rhs_norm : Double
    residuals : Array[Double]
    }

    SolveReport::converged

    fn SolveReport::converged(self : SolveReport) -> Bool

    SolveReport::iterations

    fn SolveReport::iterations(self : SolveReport) -> Int

    SolveReport::reason

    fn SolveReport::reason(self : SolveReport) -> StopReason

    SolveReport::relative_residual

    fn SolveReport::relative_residual(self : SolveReport) -> Double

    SolveReport::residual

    fn SolveReport::residual(self : SolveReport) -> Double

    SolveReport::residual_history

    fn SolveReport::residual_history(self : SolveReport) -> Array[Double]

    SolveReport::solution

    fn SolveReport::solution(self : SolveReport) -> Field

    SolveReport::work_units

    fn SolveReport::work_units(self : SolveReport) -> Int

    StopReason

    pub(all) enum StopReason {
    Converged
    IterationLimit
    WorkLimit
    } derive(Eq,
    Debug
    )

    StopReason::equal

    fn StopReason::equal(StopReason, StopReason) -> Bool

    StopReason::not_equal

    fn StopReason::not_equal(x : StopReason, y : StopReason) -> Bool

    field_from_json

    fn field_from_json(text : String) -> Field raise SolverError

    Strict keys, finite values, bounded text/depth, exact integer intervals.

    inject

    fn inject(fine : Field) -> Field raise SolverError

    Coincident-node restriction, appropriate for nodal coefficients, not residuals.

    manufacture_discrete

    fn manufacture_discrete(operator : Operator, exact : Field) -> Manufactured raise SolverError

    Discrete consistency fixture: useful for variable conductivity algebra, but not an independent test of truncation error.

    manufacture_quartic

    fn manufacture_quartic(grid : Grid, reaction? : Double) -> Manufactured raise SolverError

    Analytic -Laplace(x^4+y^4)+c*(x^4+y^4), unit conductivity. Continuous forcing is intentionally NOT obtained by applying the stencil.

    poisson_edit

    fn poisson_edit(source : Field, target : Field, mixed? : Bool, options? : SolveOptions) -> SolveReport raise SolverError

    Rectangular single-channel Poisson editing. The source provides forward differences; target edges supply Dirichlet values. No clipping is applied.

    prolong

    fn prolong(coarse : Field) -> Field raise SolverError

    Tensor-product linear interpolation onto the exactly doubled grid.

    refinement_order

    fn refinement_order(coarse_error : Double, fine_error : Double, ratio? : Double) -> Double raise SolverError

    Observed order from two errors at a known refinement ratio, using log.

    restrict_full

    fn restrict_full(fine : Field) -> Field raise SolverError

    Full weighting on interior, coincident injection on the boundary. On zero-boundary vectors R = P^T / 4 under interior Euclidean products.

    smooth_gauss_seidel

    fn smooth_gauss_seidel(operator : Operator, rhs : Field, initial : Field, sweeps : Int, reverse? : Bool) -> Field raise SolverError

    One directional lexicographic relaxation; reverse=true is its adjoint order.

    smooth_jacobi

    fn smooth_jacobi(operator : Operator, rhs : Field, initial : Field, sweeps : Int, omega? : Double) -> Field raise SolverError

    Simultaneous weighted Jacobi. Boundary values are held exactly fixed.

    smooth_symmetric

    fn smooth_symmetric(operator : Operator, rhs : Field, initial : Field, pairs : Int) -> Field raise SolverError

    A forward followed by reverse sweep, suitable for symmetric cycles.

    solve

    fn solve(problem : Problem, options? : SolveOptions, config? : CycleConfig, initial? : Field?) -> SolveReport raise SolverError