Pure MoonBit ODE suite with dense output, events, direct/iterative/banded linear systems, root finding, and interpolation
moon add noweizongqing/numsolve///|
let root = @numsolve.brent(fn(x) { x * x - 2.0 }, 1.0, 2.0) // 1.4142135623731364
///|
let faster = @numsolve.newton(fn(x) { x * x - 2.0 }, fn(x) { 2.0 * x }, 1.5) // 1.4142135623730951, in four steps| Function | Method | Steps to 1e-10 on x² − 2 from [1, 2] |
|---|---|---|
| bisection | halve the interval, keep the half the sign change is in | 32 |
| false_position | the line through the two ends of the bracket, with the Illinois halving | 8 |
| secant | the line through the last two points, unbracketed | 6 |
| newton | the tangent at the current point, derivative needed | 4 |
| brent | bisection, the secant and inverse quadratic interpolation, chosen between | 6 |
///|
let cubic = @numsolve.brent(fn(x) { x * x * x - x - 2.0 }, 1.0, 2.0)
///|
let sine = @numsolve.false_position(fn(x) { @math.sin(x) - 0.5 }, 0.0, 1.0)| Method | Stops when |
|---|---|
| bisection, false_position, brent | the interval left is no wider than tolerance · (1 + \|x\|) |
| secant, newton | the step is that small, or the function value is no larger than tolerance |
| Variant | Payload | Raised when |
|---|---|---|
| NotBracketed(Double, Double) | f(a) and f(b) | the two points given as a bracket have the same sign |
| ZeroDerivative(Double) | the point where it happened | a step would divide by a slope of exactly zero |
| NonPositiveTolerance(Double) | the tolerance | the tolerance is negative |
| MaxIterations(Int) | the limit | the search took max_iterations steps without meeting its tolerance |
///|
let root = @numsolve.bisection(fn(x) { x * x - 2.0 }, 1.0, 2.0) catch {
_ => 0.0
}| Claim | Measured |
|---|---|
| Bisection after k steps is within (b − a) / 2^k of the root | held for k = 1, 2, 4, 8, 12, 16, 20 |
| Newton's error is about the square of the error before it | 8.6e-2, 2.5e-3, 2.1e-6, 1.6e-12, 0 |
| The secant's error falls as the golden ratio's power, 1.618 | log-ratios of 1.83, 1.68 and 1.67 for the last steps |
| The Illinois halving is what keeps false position off an end | exact in 17 steps where the plain method is 2.2e-3 out at 100 |
///|
let doubling = @numsolve.rk4(fn(_t, y) { y }, 1.0, 0.0, 1.0, 10) // 2.718279744135166
///|
// The harmonic oscillator y'' = -y, written as [position, velocity]' = [v, -y].
let oscillator = @numsolve.solve_ivp(
fn(_t, y) { [y[1], -y[0]] },
[1.0, 0.0],
0.0,
6.283185307179586,
solver=@numsolve.OdeMethod::RK4,
steps=2000,
)| API | State | Step selection |
|---|---|---|
| euler, midpoint, rk4 | scalar Double | fixed number of equal steps |
| rk45 | scalar Double | adaptive Dormand-Prince 5(4) |
| *_system counterparts | Array[Double] | fixed or adaptive, as above |
| solve_ivp | Array[Double] | dispatches to Euler, Midpoint, RK4, or RK45 |
| first_event | VectorTrajectory | locates the first sign change and its time/state |
| truncate_at | VectorTrajectory | returns the prefix ending exactly at a requested time |
| Stepper | Order | Evaluations per step | Error when the step halves |
|---|---|---|---|
| euler | 1 | 1 | ÷ 2 |
| midpoint | 2 | 2 | ÷ 4 |
| rk4 | 4 | 4 | ÷ 16 |
| rk45 | 5, adaptive | 7 | set by the tolerances |
| Stepper | Ten steps | Error |
|---|---|---|
| euler | 2.5937424601 | 1.2e-1 |
| midpoint | 2.714080846608224 | 4.2e-3 |
| rk4 | 2.718279744135166 | 2.1e-6 |
| rk45 at 1e-8 | 2.7182818362088534 | 7.8e-9, in 11 steps and 77 evaluations |
| Variant | Payload | Raised when |
|---|---|---|
| EmptyState | none | a vector system was given an empty initial state |
| DimensionMismatch(Int, Int) | state dimension, derivative dimension | the right-hand side returned an array whose length does not match the state |
| NonPositiveSteps(Int) | the step count | a fixed-step stepper was given zero steps or fewer |
| NonPositiveTolerance(Double) | the offending tolerance | an adaptive solver was given a tolerance that is not positive |
| MaxEvaluations(Int) | the evaluation budget | an adaptive solver spent its budget without reaching t1 |
| OutOfRange(Double, Double, Double) | start, end, requested time | a dense-output sample lies outside the trajectory |
///|
let a = [[2.0, 1.0, 1.0], [4.0, 3.0, 3.0], [8.0, 7.0, 9.0]]
///|
let x = @numsolve.solve_linear_system(a, [1.0, 2.0, 4.0]) // [0.5, 0.0, 0.0]| Function | Method | Use it when |
|---|---|---|
| LU::decompose and solve / solve_many / solve_transposed | Gaussian elimination with partial pivoting, P · A = L · U | the matrix is general, and one factorization is reused for several right-hand sides or the transpose |
| Cholesky::decompose and solve / solve_many | A = L · Lᵀ, no pivoting | the matrix is symmetric positive definite: half the work of an LU |
| solve_tridiagonal | the Thomas recurrence over three diagonals | the matrix is tridiagonal — O(n) instead of O(n³) |
| solve_banded | banded elimination with pivoting inside the lower bandwidth | the matrix has a declared p/q bandwidth — O(n·(p+q)²) instead of O(n³) |
| residual_norm | compensated residual ‖b − A·x‖∞ | a computed solution has to be judged, especially when A is ill-conditioned |
| LU::solve_refined / solve_linear_system_refined | residual correction passes over one factorization | a direct solve needs another pass without refactoring |
| jacobi / gauss_seidel | stationary iterations using only matrix-vector products | the system is large or diagonally dominant and a factorization is not wanted |
| conjugate_gradient / preconditioned_conjugate_gradient | Krylov iterations for symmetric positive definite systems | the matrix is SPD and a factorization is not wanted |
| Variant | Payloads | Raised when |
|---|---|---|
| NotSquare(Int, Int) | rows, columns | the matrix is not square, or its rows are not all the same length |
| SizeMismatch(Int, Int) | needed, given | a right-hand side or a diagonal is the wrong length |
| Singular(Int) | the column | a pivot is exactly zero and no row below it can be exchanged for it |
| NotPositiveDefinite(Int) | the index | Cholesky was given a matrix whose pivot is not positive |
| NonPositiveRefinementSteps(Int) | the step count | iterative refinement was asked for zero passes or fewer |
| NonPositiveTolerance(Double) | the tolerance | a tolerance for refinement or an iterative solve is negative |
| NonPositiveMaxIterations(Int) | the limit | an iterative solve was given zero iterations or fewer |
| ZeroDiagonal(Int) | the row | Jacobi or Gauss-Seidel met a zero diagonal entry |
| NoConvergence(Int, Double) | iterations, residual norm | an iterative solve exhausted its budget without meeting the tolerance |
| Breakdown(Int) | the iteration | conjugate gradient met a non-positive pᵀ·A·p, so the matrix is not positive definite or the iteration broke down |
| InvalidBandwidth(Int) | the bandwidth | the banded solver was given a negative bandwidth |
| NotBanded(Int, Int) | row, column | the dense matrix has a non-zero entry outside its declared band |
///|
let table_x = [0.0, 1.0, 2.0, 3.0]
///|
let table_y = [1.0, 2.0, 0.0, 4.0]
///|
let line = @numsolve.LinearInterp::new(table_x, table_y)
///|
let spline = @numsolve.CubicSpline::new(
table_x,
table_y,
@numsolve.SplineBoundary::Natural,
)| Type | The curve | Use it when |
|---|---|---|
| LinearInterp | the straight line between the two knots around x | the table is dense, or a kink at every knot does not matter |
| CubicSpline | one cubic per interval, joined so that value, slope and second derivative agree at every knot | the curve has to be smooth: it is C², and its slope is continuous across the knots |
| Boundary | The two extra conditions | End behaviour |
|---|---|---|
| Natural | M[0] = M[n-1] = 0 | free to bend at both ends: the choice when nothing is known about them |
| Clamped(d0, dn) | S'(x0) = d0 and S'(xn) = dn | prescribed slopes at the two ends |
| NotAKnot | the third derivative is continuous at the second and second-to-last knot | no assumption about the ends: the two outermost cubics are one polynomial |
| Boundary | S(0.5) | S(1.5) | S(2.5) |
|---|---|---|---|
| Natural | 1.95 | 0.775 | 1.325 |
| Clamped(0, 0) | 1.65 | 0.625 | 2.225 |
| NotAKnot | 2.4375 | 0.8125 | 0.6875 |
| Variant | Payload | Raised when |
|---|---|---|
| TooFewKnots(Int) | the number of points | fewer than two points were given, or fewer than three for a not-a-knot spline |
| UnevenLengths(Int, Int) | the two lengths | xs and ys are not the same length |
| NotSorted(Double) | the first knot out of order | the knots do not increase |
| DuplicateKnots(Double) | the repeated knot | two knots are the same point |
///|
let value = @numsolve.CubicSpline::new(table_x, table_y, @numsolve.SplineBoundary::Natural) catch {
_ => abort("the table is a valid one")
}.evaluate(1.5) // Some(0.775)moon run cmd/main -- root sqrt2
moon run cmd/main -- ode growth --steps 100
moon run cmd/main -- spline natural
moon run cmd/main -- solve hilbert| Command | Subjects | Solver | Prints |
|---|---|---|---|
| root | sqrt2, cubic, trig | brent, false_position | the root, against √2 or π/6 |
| ode | growth, decay, optionally --steps N | rk45, or rk4 with N steps | the end value, e or 1/e, and the error |
| spline | natural, clamped, notaknot | CubicSpline through (0,1), (1,2), (2,0), (3,4) | every piece's coefficients and three samples |
| solve | demo, hilbert | solve_linear_system | the 3×3 solution, or how close Hilbert 8's is to all ones |
moon testmoon bench --releasepub suberror InterpError {
TooFewKnots(Int)
UnevenLengths(Int, Int)
NotSorted(Double)
DuplicateKnots(Double)
}pub suberror LinError {
NotSquare(Int, Int)
SizeMismatch(Int, Int)
Singular(Int)
NotPositiveDefinite(Int)
NonPositiveRefinementSteps(Int)
NonPositiveTolerance(Double)
NonPositiveMaxIterations(Int)
ZeroDiagonal(Int)
NoConvergence(Int, Double)
Breakdown(Int)
InvalidBandwidth(Int)
NotBanded(Int, Int)
}pub suberror OdeError {
EmptyState
DimensionMismatch(Int, Int)
NonPositiveSteps(Int)
NonPositiveTolerance(Double)
MaxEvaluations(Int)
OutOfRange(Double, Double, Double)
}pub suberror RootError {
NotBracketed(Double, Double)
ZeroDerivative(Double)
NonPositiveTolerance(Double)
MaxIterations(Int)
}pub struct Cholesky {
// private fields
}pub struct CubicSpline {
// private fields
}fn CubicSpline::new(xs : Array[Double], ys : Array[Double], boundary : SplineBoundary) -> CubicSpline raise InterpErrorpub struct Event {
// private fields
}pub(all) enum EventDirection {
Any
Increasing
Decreasing
}pub struct LU {
// private fields
}pub struct LinearInterp {
// private fields
}pub(all) enum SplineBoundary {
Natural
Clamped(Double, Double)
NotAKnot
}pub struct Trajectory {
// private fields
}pub struct VectorTrajectory {
// private fields
}fn VectorTrajectory::interpolate_at(self : VectorTrajectory, t : Double) -> Array[Double] raise OdeErrorfn VectorTrajectory::sample_at(self : VectorTrajectory, sample_times : Array[Double]) -> Array[Array[Double]] raise OdeErrorfn VectorTrajectory::truncate_at(self : VectorTrajectory, t : Double) -> VectorTrajectory raise OdeErrorfn bisection(f : (Double) -> Double, a : Double, b : Double, tolerance? : Double, max_iterations? : Int) -> Double raise RootErrorfn brent(f : (Double) -> Double, a : Double, b : Double, tolerance? : Double, max_iterations? : Int) -> Double raise RootErrorfn euler(f : (Double, Double) -> Double, y0 : Double, t0 : Double, t1 : Double, steps : Int) -> Trajectory raise OdeErrorfn euler_system(f : (Double, Array[Double]) -> Array[Double], y0 : Array[Double], t0 : Double, t1 : Double, steps : Int) -> VectorTrajectory raise OdeErrorfn false_position(f : (Double) -> Double, a : Double, b : Double, tolerance? : Double, max_iterations? : Int) -> Double raise RootErrorfn first_event(trajectory : VectorTrajectory, event : (Double, Array[Double]) -> Double, direction? : EventDirection, tolerance? : Double) -> Event? raise OdeErrorfn midpoint(f : (Double, Double) -> Double, y0 : Double, t0 : Double, t1 : Double, steps : Int) -> Trajectory raise OdeErrorfn midpoint_system(f : (Double, Array[Double]) -> Array[Double], y0 : Array[Double], t0 : Double, t1 : Double, steps : Int) -> VectorTrajectory raise OdeErrorfn newton(f : (Double) -> Double, df : (Double) -> Double, x0 : Double, tolerance? : Double, max_iterations? : Int) -> Double raise RootErrorfn rk4(f : (Double, Double) -> Double, y0 : Double, t0 : Double, t1 : Double, steps : Int) -> Trajectory raise OdeErrorfn rk45(f : (Double, Double) -> Double, y0 : Double, t0 : Double, t1 : Double, tolerance? : Double, max_evaluations? : Int) -> Trajectory raise OdeErrorfn rk45_system(f : (Double, Array[Double]) -> Array[Double], y0 : Array[Double], t0 : Double, t1 : Double, rtol? : Double, atol? : Double, max_evaluations? : Int) -> VectorTrajectory raise OdeErrorfn rk4_system(f : (Double, Array[Double]) -> Array[Double], y0 : Array[Double], t0 : Double, t1 : Double, steps : Int) -> VectorTrajectory raise OdeErrorfn secant(f : (Double) -> Double, x0 : Double, x1 : Double, tolerance? : Double, max_iterations? : Int) -> Double raise RootError | d₀ u₀ 0 | lower = [l₀, l₁]
| l₀ d₁ u₁ | diagonal = [d₀, d₁, d₂]
| 0 l₁ d₂ | upper = [u₀, u₁]Install
Download zipPure MoonBit ODE suite with dense output, events, direct/iterative/banded linear systems, root finding, and interpolation