Tessel libraries

Tessel libraries / numeric

numeric 0.1.0

Root finding, integration and differential equations

tessel add numeric

MIT licence · 1 version · published 2026-10-19

Root finding, integration and differential equations for Tessel programs: the methods engineering calculations keep needing, each saying how its answer came out (whether it converged, in how many steps, how accurate it is) rather than quietly returning NaN.

import numeric

fn main() {
    // The depth (m) at which a 2 m wide channel carries 3 m³/s
    // (Manning's n 0.013, slope 0.001).
    let r = numeric.brent(low: 0.01, high: 10.0) { y in
        let area = 2.0 * y
        let radius = area / (2.0 + 2.0 * y)
        area * pow(radius, 2.0 / 3.0) * sqrt(0.001) / 0.013 - 3.0
    }
    if r.converged {
        print("normal depth: {r.value} m, in {r.iterations} steps")
    }
}
normal depth: 0.9841196687128626 m, in 8 steps

Using it

Add it to a project (Tessel 0.8 or later):

tessel add numeric

then import numeric and call its functions as numeric.brent(…) (or, with Tessel 0.9 or later, import numeric as num and num.brent(…)). The function each method works on comes last, so it's written as a block after the call, with its parameters before in.

Roots: where f(x) = 0

Write the problem as f(x) = 0; for "f(x) equals a target", use f(x) - target.

Function Needs Use it when
brent(low:high:) { x in … } f(low) and f(high) of opposite signs Almost always: it can't miss a root in the range, and is fast.
bisection(low:high:) { x in … } The same You want the simplest method, and speed doesn't matter.
newton(start:derivative:) { x in … } A first guess, and f's derivative The derivative is known and the guess is good.
secant(start:next:) { x in … } Two first guesses No derivative, and the guesses are good.
bracket(start:step:) { x in … } A starting point Finding a range for brent, widening outwards.
scan(low:high:steps:) { x in … } A range to look in Finding a range for brent where x must stay within limits (a depth above zero), or the root nearest low.

Each root finder takes tolerance: (in x, default 1e-12) and maxIterations:, and returns a Root:

Field
value The root: NaN when there's no answer (no sign change).
residual f(value), to check how close to zero it really is.
iterations How many steps it took.
converged Whether it met the tolerance. When false, value isn't an answer.

bracket and scan return a Bracket: low, high and found.

let range = numeric.scan(low: 0.0, high: 10.0, steps: 100) { x in sin(x) - 0.5 }
if range.found {
    let r = numeric.brent(low: range.low, high: range.high) { x in sin(x) - 0.5 }
    print(r.value)   // 0.5235987755982989 (pi / 6)
}

Integration: the area under a curve

Function
integrate(from:to:) { x in … } To a tolerance (tolerance:, default 1e-10), taking small steps only where f bends sharply. Returns an Integral.
simpson(from:to:steps:) { x in … } Simpson's rule with equal steps; exact for cubics.
trapezoid(from:to:steps:) { x in … } The trapezoid rule with equal steps; exact for straight lines.
trapezoidPoints(x:y:) Measured points rather than a function: the x's needn't be evenly spaced.

An Integral has the value, an error estimate, the number of evaluations of f, and whether it converged (false when f has a jump or spike that maxDepth: halvings can't resolve: the value is then still the best estimate).

let r = numeric.integrate(from: 0.0, to: pi) { x in sin(x) }
print(r.value)   // 2.0, to about 1e-10 (r.error says how close)

// Flows (m³/s) read every hour: the volume that passed, in m³.
let volume = numeric.trapezoidPoints(x: [0.0, 3600.0, 7200.0, 10800.0], y: [10.0, 40.0, 25.0, 12.0])
print(volume)   // 273600.0

Differential equations: dy/dx = f(x, y)

Function
solve(x:y:to:) { x, y in … } To a tolerance (tolerance:, default 1e-8), choosing its own steps (Dormand-Prince). Returns a Solution.
rk4(x:y:to:steps:) { x, y in … } Fourth-order Runge-Kutta with equal steps: a [Sample] at regular x's.
rk4System(x:y:to:steps:) { x, y in … } The same for several quantities that change together, y being a [Float]: a [State].

All of them go backwards when to is less than x, as a water surface profile computed upstream from a control does. A Sample is an x and a y; a State an x and a [Float] y. A Solution has the samples, the number of steps, and whether it converged (reached to): when it didn't, the samples end where it stopped, often where the solution blows up or f gives NaN.

// A reservoir's level h (m) through a day: 50 m³/s in, Q = 30 h^1.5 out
// over the spillway, 2 km² of surface.
let day = numeric.solve(x: 0.0, y: 0.5, to: 86400.0) { t, h in
    (50.0 - 30.0 * pow(max(h, 0.0), 1.5)) / 2000000.0
}
print(day.samples[day.samples.count - 1].y)   // about 1.30

// A second-order equation as a system: a pendulum (small angles),
// angle'' = -angle, with y[0] the angle and y[1] its rate.
let swing = numeric.rk4System(x: 0.0, y: [1.0, 0.0], to: pi, steps: 100) { t, s in [s[1], -s[0]] }
print(swing[100].y[0])   // about -1.0

Tests

tessel run tests

checks every method against answers known exactly, and every example in the documentation, and exits with 1 if any check fails.

What's where

File
roots.tsl brent, bisection, newton, secant, bracket, scan, Root and Bracket.
integrate.tsl integrate, simpson, trapezoid, trapezoidPoints and Integral.
ode.tsl solve, rk4, rk4System, Sample, State and Solution.
tests/main.tsl The tests.
tessel.toml The library's name, version, description and licence.

Every function's comment in the code says what it does, its parameters, what it returns, and gives an example.

Licence

MIT: see LICENSE.

Versions

VersionTagCommitPublishedUses
0.1.0v0.1.0c740c696202026-10-19–

Each version's entry in the index records its commit and the checksum of its code, and tessel add checks the download against them.