/* Chalkboard - Differential Equations Namespace Version 3.0.2 Euler Released April 13th, 2026 */ /* This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0. If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /// namespace Chalkboard { /** * The differential equations namespace. * @namespace */ export namespace diff { /** * Samples a solution at a time using linear interpolation between the nearest grid points. (No extrapolation as it clamps to endpoints.) * @param {{ t: number[]; y: number[][] }} sol - The solution. * @param {number} time - The time to sample at. * @returns {number[]} */ export const at = (sol: { t: number[]; y: number[][] }, time: number): number[] => { if (typeof time !== "number" || !Number.isFinite(time)) throw new Error(`Chalkboard.diff.at: Parameter "time" must be a finite number.`); const t = sol.t; const y = sol.y; if (t.length !== y.length || t.length === 0) throw new Error(`Chalkboard.diff.at: Invalid solution object.`); if (time <= t[0]) return y[0].slice(); if (time >= t[t.length - 1]) return y[y.length - 1].slice(); let i = 0; while (i < t.length - 1 && !(t[i] <= time && time <= t[i + 1])) i++; const t0 = t[i], t1 = t[i + 1]; const a = (time - t0) / (t1 - t0); const result: number[] = []; for (let k = 0; k < y[i].length; k++) result.push((1 - a) * y[i][k] + a * y[i + 1][k]); return result; }; /** * Defines a Bernoulli equation: y' + p(t)y = q(t)y^n. Equivalent: y' = -p(t)y + q(t)y^n. * @param {number | ((t: number) => number)} p - p or p(t) * @param {number | ((t: number) => number)} q - q or q(t) * @param {number} n - Exponent * @returns {ChalkboardODE} */ export const Bernoulli = (p: number | ((t: number) => number), q: number | ((t: number) => number), n: number): ChalkboardODE => { if (typeof n !== "number" || !Number.isFinite(n)) throw new Error(`Chalkboard.diff.Bernoulli: Parameter "n" must be a finite number.`); const P = (typeof p === "number") ? ((t: number) => p) : p; const Q = (typeof q === "number") ? ((t: number) => q) : q; return Chalkboard.diff.init((t: number, y: number) => -P(t) * y + Q(t) * Math.pow(y, n)); }; /** * Defines a modified Bessel equation of order ν: x²y'' + xy' - (x² + ν²)y = 0. Equivalent: y'' = -(1/x)y' + (1 + (ν²/x²))y. Note: singular at x=0. Start from x>0. * @param {number} nu - Order ν * @returns {ChalkboardODE} */ export const BesselI = (nu: number = 0): ChalkboardODE => { if (typeof nu !== "number" || !Number.isFinite(nu)) throw new Error(`Chalkboard.diff.BesselI: Parameter "nu" must be a finite number.`); return Chalkboard.diff.init((t: number, y: number, dy: number) => { if (t === 0) throw new Error(`Chalkboard.diff.BesselI: Singular at t = 0.`); const x = t; return -(1 / x) * dy + (1 + (nu * nu) / (x * x)) * y; }); }; /** * Defines a Bessel equation of order ν: x²y'' + xy' + (x² - ν²)y = 0. Equivalent: y'' = -(1/x)y' - (1 - (ν²/x²))y. Note: singular at x=0. Start from x>0. * @param {number} nu - Order ν * @returns {ChalkboardODE} */ export const BesselJ = (nu: number = 0): ChalkboardODE => { if (typeof nu !== "number" || !Number.isFinite(nu)) throw new Error(`Chalkboard.diff.BesselJ: Parameter "nu" must be a finite number.`); return Chalkboard.diff.init((t: number, y: number, dy: number) => { if (t === 0) throw new Error(`Chalkboard.diff.BesselJ: Singular at t = 0.`); const x = t; const term = 1 - (nu * nu) / (x * x); return -(1 / x) * dy - term * y; }); }; /** * Returns the index of the time grid closest to a target time. * @param {number[]} t - Time array. * @param {number} target - Target time. * @returns {number} */ export const closestIndex = (t: number[], target: number): number => { if (!Array.isArray(t) || t.length === 0) throw new Error(`Chalkboard.diff.closestIndex: Parameter "t" must be a non-empty array.`); if (typeof target !== "number" || !Number.isFinite(target)) throw new Error(`Chalkboard.diff.closestIndex: Parameter "target" must be a finite number.`); let result = 0; let resultDist = Math.abs(t[0] - target); for (let i = 1; i < t.length; i++) { const d = Math.abs(t[i] - target); if (d < resultDist) { resultDist = d; result = i; } } return result; }; /** * Returns a single component series from the solution of an ODE. * @param {{ t: number[]; y: number[][] }} sol - The solution of the ODE. * @param {number} index - Component index. * @returns {number[]} */ export const component = (sol: { t: number[]; y: number[][] }, index: number): number[] => { if (!Number.isInteger(index) || index < 0) throw new Error(`Chalkboard.diff.component: Parameter "index" must be an integer >= 0.`); const result: number[] = []; for (let i = 0; i < sol.y.length; i++) { if (index >= sol.y[i].length) throw new Error(`Chalkboard.diff.component: "index" out of range for solution dimension.`); result.push(sol.y[i][index]); } return result; }; /** * Returns an estimate of y'(t) (an estimate of the derivative series) from a solved solution using finite differences on the solution grid. * @param {{ t: number[]; y: number[][] }} sol - Solution * @returns {number[][]} */ export const derivative = (sol: { t: number[]; y: number[][] }): number[][] => { if (!sol || !Array.isArray(sol.t) || !Array.isArray(sol.y)) throw new Error(`Chalkboard.diff.derivative: Invalid solution object.`); const t = sol.t; const y = sol.y; if (t.length !== y.length || t.length < 2) throw new Error(`Chalkboard.diff.derivative: Need at least 2 samples.`); const n = y[0].length; const dy: number[][] = new Array(y.length); for (let i = 0; i < y.length; i++) dy[i] = new Array(n).fill(0); for (let i = 0; i < y.length; i++) { if (i === 0) { const dt = t[1] - t[0]; for (let k = 0; k < n; k++) dy[i][k] = (y[1][k] - y[0][k]) / dt; } else if (i === y.length - 1) { const dt = t[t.length - 1] - t[t.length - 2]; for (let k = 0; k < n; k++) dy[i][k] = (y[y.length - 1][k] - y[y.length - 2][k]) / dt; } else { const dt = t[i + 1] - t[i - 1]; for (let k = 0; k < n; k++) dy[i][k] = (y[i + 1][k] - y[i - 1][k]) / dt; } } return dy; }; /** * Defines a Duffing equation: x'' + δx' + αx + βx^3 = γcos(ωt). Equivalent: x'' = -δx' - αx - βx^3 + γcos(ωt). * @param {number} delta - δ * @param {number} alpha - α * @param {number} beta - β * @param {number} gamma - γ * @param {number} omega - ω * @returns {ChalkboardODE} */ export const Duffing = (delta: number, alpha: number, beta: number, gamma: number, omega: number): ChalkboardODE => { if (![delta, alpha, beta, gamma, omega].every((n) => typeof n === "number" && Number.isFinite(n))) throw new Error(`Chalkboard.diff.Duffing: Parameters must be finite numbers.`); return Chalkboard.diff.init((t: number, x: number, v: number) => -delta * v - alpha * x - beta * x * x * x + gamma * Math.cos(omega * t)); }; /** * Calculates a simple residual error diagnostic for a solution: | |y'(t) - f(t,y) ||. Note that y'(t) is estimated from the discrete solution grid (finite differences) and f(t,y) is the ODE right-hand side in canonical first-order system form. * @param {{ t: number[]; y: number[][] }} sol - Solution * @param {ChalkboardODE} ode - ODE which produced the solution * @param {"L1" | "L2" | "LInfinity"} [norm="L2"] - Norm type for the residual per sample * @returns {{ t: number[]; e: number[]; max: number; mean: number; rmse: number; }} * @example * const ode = Chalkboard.diff.init((t, y) => -2 * y); * const sol = Chalkboard.diff.solveAdaptive(ode, { * t0: 0, t1: 5, * y0: 1, * h0: 0.1, * hMin: 0.01, * hMax: 0.5 * }); * const err = Chalkboard.diff.error(sol, ode, "LInfinity"); * console.log(err.max); */ export const error = (sol: { t: number[]; y: number[][] }, ode: ChalkboardODE, norm: "L1" | "L2" | "LInfinity" = "L2"): { t: number[]; e: number[]; max: number; mean: number; rmse: number } => { if (!sol || !Array.isArray(sol.t) || !Array.isArray(sol.y)) throw new Error(`Chalkboard.diff.error: Invalid solution object.`); if (sol.t.length !== sol.y.length) throw new Error(`Chalkboard.diff.error: "sol.t" and "sol.y" must have the same length.`); if (sol.t.length < 2) throw new Error(`Chalkboard.diff.error: Need at least 2 samples to estimate derivative.`); if (!ode || typeof ode !== "object" || typeof ode.rule !== "function") throw new Error(`Chalkboard.diff.error: Parameter "ode" must be a ChalkboardODE.`); if (["L1", "L2", "LInfinity"].indexOf(norm) === -1) throw new Error(`Chalkboard.diff.error: Unknown norm type.`); const t = sol.t; const y = sol.y; const n = y[0].length; if (!Number.isInteger(ode.dimension) || ode.dimension !== n) throw new Error(`Chalkboard.diff.error: "ode.dimension" must match solution dimension.`); const dydt = Chalkboard.diff.derivative(sol); const e: number[] = []; let maxErr = 0; let sumErr = 0; let sumSq = 0; for (let i = 0; i < t.length; i++) { const fi = ode.rule(t[i], y[i]); if (!Array.isArray(fi) || fi.length !== n) throw new Error(`Chalkboard.diff.error: ODE rule returned invalid derivative at sample ${i}.`); const r: number[] = new Array(n); for (let k = 0; k < n; k++) { r[k] = dydt[i][k] - fi[k]; if (typeof r[k] !== "number" || !Number.isFinite(r[k])) throw new Error(`Chalkboard.diff.error: Non-finite residual at sample ${i}, index ${k}.`); } let ri: number; if (norm === "L1") { let s = 0; for (let k = 0; k < n; k++) s += Math.abs(r[k]); ri = s; } else if (norm === "LInfinity") { let m = 0; for (let k = 0; k < n; k++) m = Math.max(m, Math.abs(r[k])); ri = m; } else { let s2 = 0; for (let k = 0; k < n; k++) s2 += r[k] * r[k]; ri = Math.sqrt(s2); } e.push(ri); maxErr = Math.max(maxErr, ri); sumErr += ri; sumSq += ri * ri; } const mean = sumErr / e.length; const rmse = Math.sqrt(sumSq / e.length); return { t: t.slice(), e, max: maxErr, mean, rmse }; }; /** * Defines an exponential growth/decay equation: y' = ky * @param {number} k - Rate * @returns {ChalkboardODE} */ export const exponential = (k: number = 1): ChalkboardODE => { if (typeof k !== "number" || !Number.isFinite(k)) throw new Error(`Chalkboard.diff.exponential: Parameter "k" must be a finite number.`); return Chalkboard.diff.init((t: number, y: number) => k * y); }; /** * Defines a Gompertz equation: y' = ayln(K/y). * @param {number} a - Growth rate * @param {number} K - Carrying capacity (K > 0) * @returns {ChalkboardODE} */ export const Gompertz = (a: number = 1, K: number = 1): ChalkboardODE => { if (typeof a !== "number" || !Number.isFinite(a)) throw new Error(`Chalkboard.diff.Gompertz: Parameter "a" must be a finite number.`); if (typeof K !== "number" || !Number.isFinite(K) || K <= 0) throw new Error(`Chalkboard.diff.Gompertz: Parameter "K" must be greater than 0.`); return Chalkboard.diff.init((t: number, y: number) => a * y * Math.log(K / y)); }; /** * Defines an undamped harmonic oscillator: y'' + (w²)y = 0. Equivalent: y'' = -(w²)y. * @param {number} w - Angular frequency (must be greater than or equal to 0) * @returns {ChalkboardODE} */ export const harmonic = (w: number = 1): ChalkboardODE => { if (typeof w !== "number" || !Number.isFinite(w) || w < 0) throw new Error(`Chalkboard.diff.harmonic: Parameter "w" must be a finite number greater than or equal to 0.`); return Chalkboard.diff.init((t: number, y: number, dy: number) => -(w * w) * y); }; /** * Defines a damped harmonic oscillator: y'' + (2ζω)y' + (ω²)y = 0. Equivalent: y'' = -(2ζω)y' - (ω²)y. * @param {number} w - Angular frequency (must be greater than or equal to 0) * @param {number} zeta - Damping ratio (must be greater than or equal to 0) * @returns {ChalkboardODE} */ export const harmonicDamped = (w: number = 1, zeta: number = 0.1): ChalkboardODE => { if (typeof w !== "number" || !Number.isFinite(w) || w < 0) throw new Error(`Chalkboard.diff.harmonicDamped: Parameter "w" must be a finite number greater than or equal to 0.`); if (typeof zeta !== "number" || !Number.isFinite(zeta) || zeta < 0) throw new Error(`Chalkboard.diff.harmonicDamped: Parameter "zeta" must be a finite number greater than or equal to 0.`); return Chalkboard.diff.init((t: number, y: number, dy: number) => -2 * zeta * w * dy - (w * w) * y); }; /** * Defines a forced harmonic oscillator: y'' + (2ζω)y' + (ω²)y = F(t). Equivalent: y'' = F(t) - (2ζω)y' - (ω²)y. * @param {number} w - Angular frequency (must be greater than or equal to 0) * @param {number} zeta - Damping ratio (must be greater than or equal to 0) * @param {(t: number) => number} F - Forcing term * @returns {ChalkboardODE} */ export const harmonicForced = (w: number, zeta: number, F: (t: number) => number): ChalkboardODE => { if (typeof w !== "number" || !Number.isFinite(w) || w < 0) throw new Error(`Chalkboard.diff.harmonicForced: Parameter "w" must be a finite number greater than or equal to 0.`); if (typeof zeta !== "number" || !Number.isFinite(zeta) || zeta < 0) throw new Error(`Chalkboard.diff.harmonicForced: Parameter "zeta" must be a finite number greater than or equal to 0.`); if (typeof F !== "function") throw new Error(`Chalkboard.diff.harmonicForced: Parameter "F" must be a function.`); return Chalkboard.diff.init((t: number, y: number, dy: number) => F(t) - 2 * zeta * w * dy - (w * w) * y); }; /** * Defines an ordinary differential equation. * @param {((t: number, y: number) => number) | ((t: number, y: number, dy: number) => number) | ((t: number, y: number[]) => number[])} rule - The differential equation rule function. * @param {number} [dimension] - Optional, the dimension of the system only required for system equations. * @returns {ChalkboardODE} * @example * // First-order scalar ODE: y' = -2y * const ode1 = Chalkboard.diff.init((t, y) => -2 * y); * * // Second-order scalar ODE: y'' = -y (harmonic oscillator) * const ode2 = Chalkboard.diff.init((t, y, dy) => -y); * * // System ODE: dy/dt = [y2, -y1] (2D system) * const odeSys = Chalkboard.diff.init((t, y) => [y[1], -y[0]], 2); */ export const init = ( rule: ((t: number, y: number) => number) | ((t: number, y: number, dy: number) => number) | ((t: number, y: number[]) => number[]), dimension?: number ): ChalkboardODE => { if (typeof rule !== "function") throw new Error(`Chalkboard.diff.init: Parameter "rule" must be a function.`); if (typeof dimension === "number") { if (!Number.isInteger(dimension) || dimension < 1) throw new Error(`Chalkboard.diff.init: Parameter "dimension" must be an integer >= 1.`); const sys = rule as (t: number, y: number[]) => number[]; const ode: ChalkboardODE = { rule: (t: number, y: number[]) => { const out = sys(t, y); if (!Array.isArray(out)) throw new Error(`Chalkboard.diff.init: System rule must return an array of numbers.`); if (out.length !== dimension) throw new Error(`Chalkboard.diff.init: System rule must return an array of length ${dimension}.`); for (let i = 0; i < out.length; i++) if (typeof out[i] !== "number" || !Number.isFinite(out[i])) throw new Error(`Chalkboard.diff.init: System rule output must be finite numbers (index ${i}).`); return out; }, type: "system", order: 1, dimension }; return ode; } const arity = (rule as Function).length; if (arity === 2) { const f = rule as (t: number, y: number) => number; return { rule: (t: number, y: number[]) => { if (y.length !== 1) throw new Error(`Chalkboard.diff.init: Internal error (expected dimension 1).`); const dy = f(t, y[0]); if (typeof dy !== "number" || !Number.isFinite(dy)) throw new Error(`Chalkboard.diff.init: Scalar rule must return a finite number.`); return [dy]; }, type: "single", order: 1, dimension: 1 }; } if (arity === 3) { const g = rule as (t: number, y: number, dy: number) => number; return { rule: (t: number, y: number[]) => { if (y.length !== 2) throw new Error(`Chalkboard.diff.init: Internal error (expected dimension 2 for second-order scalar).`); const ddy = g(t, y[0], y[1]); if (typeof ddy !== "number" || !Number.isFinite(ddy)) throw new Error(`Chalkboard.diff.init: Second-order scalar rule must return a finite number.`); return [y[1], ddy]; }, type: "single", order: 2, dimension: 2 }; } throw new Error(`Chalkboard.diff.init: Invalid "rule" arity. Expected (t,y) or (t,y,dy), or provide dimension for systems.`); }; /** * Defines a Kepler two-body problem in 2D (inverse-square central force). Unit mass: r'' = -μr / |r|^3. State: [x, y, vx, vy]. * @param {number} [mu=1] - Gravitational parameter μ = G(M+m) * @returns {ChalkboardODE} */ export const Kepler2D = (mu: number = 1): ChalkboardODE => { if (typeof mu !== "number" || !Number.isFinite(mu) || mu < 0) throw new Error(`Chalkboard.diff.Kepler2D: Parameter "mu" must be a finite number >= 0.`); return Chalkboard.diff.init((t: number, y: number[]) => { const x = y[0], yy = y[1], vx = y[2], vy = y[3]; const r2 = x * x + yy * yy; const r = Math.sqrt(r2); if (r === 0) throw new Error(`Chalkboard.diff.Kepler2D: Encountered r=0 singularity.`); const invr3 = 1 / (r2 * r); const ax = -mu * x * invr3; const ay = -mu * yy * invr3; return [vx, vy, ax, ay]; }, 4); }; /** * Defines a Kepler two-body problem in 3D (inverse-square central force). Unit mass: r'' = -μr / |r|^3. State: [x, y, z, vx, vy, vz]. * @param {number} [mu=1] - Gravitational parameter μ = G(M+m) * @returns {ChalkboardODE} */ export const Kepler3D = (mu: number = 1): ChalkboardODE => { if (typeof mu !== "number" || !Number.isFinite(mu) || mu < 0) throw new Error(`Chalkboard.diff.Kepler3D: Parameter "mu" must be a finite number >= 0.`); return Chalkboard.diff.init((t: number, y: number[]) => { const x = y[0], yy = y[1], z = y[2]; const vx = y[3], vy = y[4], vz = y[5]; const r2 = x * x + yy * yy + z * z; const r = Math.sqrt(r2); if (r === 0) throw new Error(`Chalkboard.diff.Kepler3D: Encountered r=0 singularity.`); const invr3 = 1 / (r2 * r); const ax = -mu * x * invr3; const ay = -mu * yy * invr3; const az = -mu * z * invr3; return [vx, vy, vz, ax, ay, az]; }, 6); }; /** * Defines a first-order linear scalar ODE: y' = a(t)y + b(t). * @param {((t: number) => number) | number} a - Coefficient a(t) or constant a. * @param {((t: number) => number) | number} b - Coefficient b(t) or constant b. * @returns {ChalkboardODE} */ export const linear1 = (a: ((t: number) => number) | number, b: ((t: number) => number) | number): ChalkboardODE => { const A = typeof a === "number" ? (() => a) : a; const B = typeof b === "number" ? (() => b) : b; return Chalkboard.diff.init((t: number, y: number) => A(t) * y + B(t)); }; /** * Defines a second-order linear scalar ODE: y'' = a(t)y' + b(t)y + c(t). * @param {((t: number) => number) | number} a - Coefficient a(t) or constant a. * @param {((t: number) => number) | number} b - Coefficient b(t) or constant b. * @param {((t: number) => number) | number} c - Coefficient c(t) or constant c. * @returns {ChalkboardODE} */ export const linear2 = (a: ((t: number) => number) | number, b: ((t: number) => number) | number, c: ((t: number) => number) | number): ChalkboardODE => { const A = typeof a === "number" ? (() => a) : a; const B = typeof b === "number" ? (() => b) : b; const C = typeof c === "number" ? (() => c) : c; return Chalkboard.diff.init((t: number, y: number, dy: number) => A(t) * dy + B(t) * y + C(t)); }; /** * Defines a logistic growth model: y' = ry(1 - y/K). * @param {number} r - Growth rate * @param {number} K - Carrying capacity (non-zero) * @returns {ChalkboardODE} */ export const logistic = (r: number = 1, K: number = 1): ChalkboardODE => { if (typeof r !== "number" || !Number.isFinite(r)) throw new Error(`Chalkboard.diff.logistic: Parameter "r" must be a finite number.`); if (typeof K !== "number" || !Number.isFinite(K) || K === 0) throw new Error(`Chalkboard.diff.logistic: Parameter "K" must be a finite non-zero number.`); return Chalkboard.diff.init((t: number, y: number) => r * y * (1 - y / K)); }; /** * Defines a Lorenz attractor: x' = σ(y - x), y' = x(ρ - z) - y, z' = xy - βz, state: [x, y, z]. * @param {number} [sigma=10] - σ, Prandtl number * @param {number} [rho=28] - ρ, Rayleigh number * @param {number} [beta=8/3] - β, geometric factor * @returns {ChalkboardODE} */ export const Lorenz = (sigma: number = 10, rho: number = 28, beta: number = 8 / 3): ChalkboardODE => { if (![sigma, rho, beta].every((n) => typeof n === "number" && Number.isFinite(n))) throw new Error(`Chalkboard.diff.Lorenz: Parameters must be finite numbers.`); return Chalkboard.diff.init((t: number, y: number[]) => { const x = y[0], yy = y[1], z = y[2]; return [ sigma * (yy - x), x * (rho - z) - yy, x * yy - beta * z ]; }, 3); }; /** * Defines a Lotka–Volterra predator-prey model: x' = αx - βxy, y' = δxy - γy. * @param {number} alpha - Prey growth rate * @param {number} beta - Predation rate * @param {number} gamma - Predator death rate * @param {number} delta - Predator reproduction rate * @returns {ChalkboardODE} */ export const LotkaVolterra = (alpha: number = 1, beta: number = 1, gamma: number = 1, delta: number = 1): ChalkboardODE => { if (![alpha, beta, gamma, delta].every((n) => typeof n === "number" && Number.isFinite(n))) throw new Error(`Chalkboard.diff.LotkaVolterra: Parameters must be finite numbers.`); return Chalkboard.diff.init((t: number, y: number[]) => { const x = y[0], p = y[1]; return [alpha * x - beta * x * p, delta * x * p - gamma * p]; }, 2); }; /** * Defines a mass-spring-damper system: mx'' + cx' + kx = 0. Equivalent: x'' = -(c/m)x' - (k/m)x. * @param {number} m - Mass (non-zero) * @param {number} c - Damping * @param {number} k - Spring constant * @returns {ChalkboardODE} */ export const massSpringDamper = (m: number, c: number, k: number): ChalkboardODE => { if (typeof m !== "number" || !Number.isFinite(m) || m === 0) throw new Error(`Chalkboard.diff.massSpringDamper: Parameter "m" must be finite and non-zero.`); if (typeof c !== "number" || !Number.isFinite(c)) throw new Error(`Chalkboard.diff.massSpringDamper: Parameter "c" must be a finite number.`); if (typeof k !== "number" || !Number.isFinite(k)) throw new Error(`Chalkboard.diff.massSpringDamper: Parameter "k" must be a finite number.`); return Chalkboard.diff.init((t: number, x: number, v: number) => -(c / m) * v - (k / m) * x); }; /** * Defines a pendulum (nonlinear) with optional damping and external torque: θ'' + (b)θ' + (g/L)sin(θ) = τ(t). Equivalent: θ'' = τ(t) - bθ' - (g/L)sin(θ). * @param {number} [params.g=9.81] - Gravity (greater than or equal to 0) * @param {number} [params.L=1] - Length (non-zero) * @param {number} [params.b=0] - Damping coefficient * @param {(t:number)=>number} [params.tau] - External torque τ(t) (0 by default) * @returns {ChalkboardODE} */ export const pendulum = (params: { g?: number; L?: number; b?: number; tau?: (t: number) => number; } = {}): ChalkboardODE => { const g = params.g ?? 9.81; const L = params.L ?? 1; const b = params.b ?? 0; const tau = params.tau ?? (() => 0); if (typeof g !== "number" || !Number.isFinite(g) || g < 0) throw new Error(`Chalkboard.diff.pendulum: "g" must be a finite number greater than or equal to 0.`); if (typeof L !== "number" || !Number.isFinite(L) || L === 0) throw new Error(`Chalkboard.diff.pendulum: "L" must be a finite non-zero number.`); if (typeof b !== "number" || !Number.isFinite(b)) throw new Error(`Chalkboard.diff.pendulum: "b" must be a finite number.`); if (typeof tau !== "function") throw new Error(`Chalkboard.diff.pendulum: "tau" must be a function.`); return Chalkboard.diff.init((t, theta, omega) => tau(t) - b * omega - (g / L) * Math.sin(theta)); }; /** * Defines a pendulum with linear (viscous) and quadratic (turbulent-ish) drag: θ'' + bθ' + c|θ'|θ' + (g/L)sin(θ) = τ(t). Equivalent: θ'' = τ(t) - bθ' - c|θ'|θ' - (g/L)sin(θ). * @param {number} [params.g=9.81] - Gravity (greater than or equal to 0) * @param {number} [params.L=1] - Length (non-zero) * @param {number} [params.b=0] - Linear damping coefficient * @param {number} [params.c=0] - Quadratic damping coefficient * @param {(t:number)=>number} [params.tau] - External torque τ(t) (0 by default) * @returns {ChalkboardODE} */ export const pendulumDrag = (params: { g?: number; L?: number; b?: number; c?: number; tau?: (t: number) => number; } = {}): ChalkboardODE => { const g = params.g ?? 9.81; const L = params.L ?? 1; const b = params.b ?? 0; const c = params.c ?? 0; const tau = params.tau ?? (() => 0); if (typeof g !== "number" || !Number.isFinite(g) || g < 0) throw new Error(`Chalkboard.diff.pendulumDrag: "g" must be a finite number greater than or equal to 0.`); if (typeof L !== "number" || !Number.isFinite(L) || L === 0) throw new Error(`Chalkboard.diff.pendulumDrag: "L" must be a finite non-zero number.`); if (typeof b !== "number" || !Number.isFinite(b)) throw new Error(`Chalkboard.diff.pendulumDrag: "b" must be a finite number.`); if (typeof c !== "number" || !Number.isFinite(c)) throw new Error(`Chalkboard.diff.pendulumDrag: "c" must be a finite number.`); if (typeof tau !== "function") throw new Error(`Chalkboard.diff.pendulumDrag: "tau" must be a function.`); return Chalkboard.diff.init((t, theta, omega) => { const quad = c * Math.abs(omega) * omega; return tau(t) - b * omega - quad - (g / L) * Math.sin(theta); }); }; /** * Defines a driven damped pendulum in a common parameterization: θ'' + qθ' + sin(θ) = Acos(Ωt). Equivalent: θ'' = Acos(Ωt) - qθ' - sin(θ). This is the dimensionless form (g/L scaled out). * @param {number} [q=0.5] - Damping * @param {number} [A=1.2] - Drive amplitude * @param {number} [Omega=2/3] - Drive frequency * @returns {ChalkboardODE} */ export const pendulumDriven = (q: number = 0.5, A: number = 1.2, Omega: number = 2 / 3): ChalkboardODE => { if (![q, A, Omega].every((n) => typeof n === "number" && Number.isFinite(n))) throw new Error(`Chalkboard.diff.pendulumDriven: Parameters must be finite numbers.`); return Chalkboard.diff.init((t, theta, omega) => A * Math.cos(Omega * t) - q * omega - Math.sin(theta)); }; /** * Returns phase-plot data from a solution of an ODE (pairs of components). Useful for plotting trajectories such as (y, dy), (x, y), (x, z), etc. * @param {{ t: number[]; y: number[][] }} sol - Solution of an ODE * @param {number} i - The first component index * @param {number} j - The second component index * @returns {number[][]} * @example * // Phase plot of harmonic oscillator (y vs dy) * const ode = Chalkboard.diff.harmonic(); * const sol = Chalkboard.diff.solve(ode, { t0: 0, t1: 20, steps: 2000, y0: { y0: 1, dy0: 0 } }); * const data = Chalkboard.diff.phase(sol, 0, 1); */ export const phase = (sol: { t: number[]; y: number[][] }, i: number, j: number): number[][] => { if (!sol || !Array.isArray(sol.t) || !Array.isArray(sol.y)) throw new Error(`Chalkboard.diff.phase: Invalid solution object.`); if (sol.t.length !== sol.y.length) throw new Error(`Chalkboard.diff.phase: "sol.t" and "sol.y" must have the same length.`); if (sol.y.length === 0) throw new Error(`Chalkboard.diff.phase: Solution has no samples.`); if (!Number.isInteger(i) || i < 0) throw new Error(`Chalkboard.diff.phase: Parameter "i" must be an integer >= 0.`); if (!Number.isInteger(j) || j < 0) throw new Error(`Chalkboard.diff.phase: Parameter "j" must be an integer >= 0.`); if (i === j) throw new Error(`Chalkboard.diff.phase: Parameters "i" and "j" must be different indices.`); if (i >= sol.y[0].length || j >= sol.y[0].length) throw new Error(`Chalkboard.diff.phase: Indices out of bounds for solution dimension.`); const result: number[][] = []; for (let k = 0; k < sol.y.length; k++) { const row = sol.y[k]; result.push([row[i], row[j]]); } return result; }; /** * Samples the solution at multiple times using Chalkboard.diff.at. * @param {{ t: number[]; y: number[][] }} sol - Solution of an ODE * @param {number[]} times - Times to sample at * @returns {number[][]} * @example * const ode = Chalkboard.diff.harmonic(); * const sol = Chalkboard.diff.solve(ode, { t0: 0, t1: 20, steps: 2000, y0: { y0: 1, dy0: 0 } }); * const ys = Chalkboard.diff.sample(sol, [0, 0.5, 1.0, 1.5, 2.0]); */ export const sample = (sol: { t: number[]; y: number[][] }, times: number[]): number[][] => { if (!sol || !Array.isArray(sol.t) || !Array.isArray(sol.y)) throw new Error(`Chalkboard.diff.sample: Invalid solution object.`); if (!Array.isArray(times)) throw new Error(`Chalkboard.diff.sample: Parameter "times" must be an array.`); const result: number[][] = []; for (let i = 0; i < times.length; i++) { if (typeof times[i] !== "number" || !Number.isFinite(times[i])) throw new Error(`Chalkboard.diff.sample: "times"[${i}] must be a finite number.`); result.push(Chalkboard.diff.at(sol, times[i])); } return result; }; /** * Defines a separable ODE: y' = f(t)g(y) * @param {(t: number) => number} f - f(t) * @param {(y: number) => number} g - g(y) * @returns {ChalkboardODE} */ export const separable = (f: (t: number) => number, g: (y: number) => number): ChalkboardODE => { if (typeof f !== "function" || typeof g !== "function") throw new Error(`Chalkboard.diff.separable: Parameters must be functions.`); return Chalkboard.diff.init((t: number, y: number) => f(t) * g(y)); }; /** * Defines a SEIR epidemic model: S' = -βSI, E' = βSI - σE, I' = σE - γI, R' = γI. * @param {number} beta - Infection rate * @param {number} sigma - Incubation rate * @param {number} gamma - Recovery rate * @returns {ChalkboardODE} */ export const SEIR = (beta: number = 1, sigma: number = 1, gamma: number = 1): ChalkboardODE => { if (![beta, sigma, gamma].every((n) => typeof n === "number" && Number.isFinite(n))) throw new Error(`Chalkboard.diff.SEIR: Parameters must be finite numbers.`); return Chalkboard.diff.init((t: number, y: number[]) => { const S = y[0], E = y[1], I = y[2], R = y[3]; const inf = beta * S * I; return [ -inf, inf - sigma * E, sigma * E - gamma * I, gamma * I ]; }, 4); }; /** * Defines a SIR epidemic model: S' = -βSI, I' = βSI - γI, R' = γI. * @param {number} beta - Infection rate * @param {number} gamma - Recovery rate * @returns {ChalkboardODE} */ export const SIR = (beta: number = 1, gamma: number = 1): ChalkboardODE => { if (![beta, gamma].every((n) => typeof n === "number" && Number.isFinite(n))) throw new Error(`Chalkboard.diff.SIR: Parameters must be finite numbers.`); return Chalkboard.diff.init((t: number, y: number[]) => { const S = y[0], I = y[1], R = y[2]; return [-beta * S * I, beta * S * I - gamma * I, gamma * I]; }, 3); }; /** * Defines a SIS epidemic model with total population normalized to 1: S + I = 1, I' = βI(1 - I) - γI. * @param {number} beta - Infection rate * @param {number} gamma - Recovery rate * @returns {ChalkboardODE} */ export const SIS = (beta: number = 1, gamma: number = 0.5): ChalkboardODE => { if (![beta, gamma].every((n) => typeof n === "number" && Number.isFinite(n))) throw new Error(`Chalkboard.diff.SIS: Parameters must be finite numbers.`); return Chalkboard.diff.init((t: number, I: number) => beta * I * (1 - I) - gamma * I); }; /** * Solves an ordinary differential equation using an explicit method. * @param {ChalkboardODE} ode - The ordinary differential equation to solve. * @param {number} [config.t0=0] - The initial time. * @param {number} config.t1 - The final time. * @param {number} [config.h] - The time step size. Either this or `steps` must be provided. * @param {number} [config.steps] - The number of steps to take. Either this or `h` must be provided. * @param {number | number[] | Object} config.y0 - The initial state. * @param {"euler" | "midpoint" | "heun" | "ralston" | "rk4"} [config.method="rk4"] - The integration method to use: "euler", "midpoint", "heun", "ralston", or "rk4". * @param {boolean} [config.returnObject=false] - Whether to return the results as an array of objects (only if initial state was provided as an object). * @returns {{ t: number[]; y: number[][]; yObj?: {[key: string]: number}[]; }} The solution containing time points `t`, state array `y`, and optionally state objects `yObj`. * @example * // Solve first-order ODE using Euler's method: * const ode1 = Chalkboard.diff.init((t, y) => -2 * y); * const sol1 = Chalkboard.diff.solve(ode1, { * t0: 0, t1: 5, h: 0.1, * y0: 1, * method: "euler" * }); * * // Solve second-order ODE using Ralston method: * const ode2 = Chalkboard.diff.init((t, y, dy) => -y); * const sol2 = Chalkboard.diff.solve(ode2, { * t0: 0, t1: 10, steps: 100, * y0: { y0: 1, dy0: 0 }, * method: "ralston", * returnObject: true * }); * * // Solve system ODE using fourth-order Runge-Kutta method: * const odeSys = Chalkboard.diff.init((t, y) => [y[1], -y[0]], 2); * const solSys = Chalkboard.diff.solve(odeSys, { * t0: 0, t1: 10, steps: 100, * y0: [1, 0] * }); */ export const solve = ( ode: ChalkboardODE, config: { t0?: number; t1: number; h?: number; steps?: number; y0: number | number[] | Record; method?: "euler" | "midpoint" | "heun" | "ralston" | "rk4"; returnObject?: boolean; } ): { t: number[]; y: number[][]; yObj?: {[key: string]: number}[]; } => { if (!ode || typeof ode !== "object") throw new Error(`Chalkboard.diff.solve: Parameter "ode" must be a ChalkboardODE.`); if (typeof ode.rule !== "function") throw new Error(`Chalkboard.diff.solve: "ode.rule" must be a function.`); if (!Number.isInteger(ode.dimension) || ode.dimension < 1) throw new Error(`Chalkboard.diff.solve: "ode.dimension" must be an integer >= 1.`); if (typeof config !== "object" || config === null) throw new Error(`Chalkboard.diff.solve: Parameter "config" must be an object.`); if (typeof config.t1 !== "number" || !Number.isFinite(config.t1)) throw new Error(`Chalkboard.diff.solve: "config.t1" must be a finite number.`); const t0 = config.t0 ?? 0; if (typeof t0 !== "number" || !Number.isFinite(t0)) throw new Error(`Chalkboard.diff.solve: "config.t0" must be a finite number.`); if (config.t1 === t0) throw new Error(`Chalkboard.diff.solve: "config.t1" must be different from "config.t0".`); const method: "euler" | "midpoint" | "heun" | "ralston" | "rk4" = (config.method ?? "rk4").toLowerCase() as "euler" | "midpoint" | "heun" | "ralston" | "rk4"; if (["euler", "midpoint", "heun", "ralston", "rk4"].indexOf(method) === -1) throw new Error(`Chalkboard.diff.solve: Unknown method.`); let y0: number[]; let keys: string[] | undefined; if (typeof config.y0 === "number" && Number.isFinite(config.y0)) { if (ode.dimension !== 1) throw new Error(`Chalkboard.diff.solve: Scalar "y0" is only allowed when "ode.dimension" === 1.`); y0 = [config.y0]; } else if (Array.isArray(config.y0)) { if (config.y0.length !== ode.dimension) throw new Error(`Chalkboard.diff.solve: Array "y0" must have length ${ode.dimension}.`); for (let i = 0; i < config.y0.length; i++) if (typeof config.y0[i] !== "number" || !Number.isFinite(config.y0[i])) throw new Error(`Chalkboard.diff.solve: "y0"[${i}] must be a finite number.`); y0 = config.y0.slice(); } else { if (typeof config.y0 !== "object" || config.y0 === null) throw new Error(`Chalkboard.diff.solve: "y0" must be of type number, number[], or object.`); const y0obj = config.y0 as any; if (ode.type === "single" && ode.order === 2) { if (("y0" in y0obj) && ("dy0" in y0obj)) { const a = y0obj.y0; const b = y0obj.dy0; if (typeof a !== "number" || !Number.isFinite(a) || typeof b !== "number" || !Number.isFinite(b)) throw new Error(`Chalkboard.diff.solve: For second-order scalar, "y0.y0" and "y0.dy0" must be finite numbers.`); y0 = [a, b]; if (config.returnObject) keys = ["y", "dy"]; } else if (("y" in y0obj) && ("dy" in y0obj)) { const a = y0obj.y; const b = y0obj.dy; if (typeof a !== "number" || !Number.isFinite(a) || typeof b !== "number" || !Number.isFinite(b)) throw new Error(`Chalkboard.diff.solve: For second-order scalar, "y0.y" and "y0.dy" must be finite numbers.`); y0 = [a, b]; if (config.returnObject) keys = ["y", "dy"]; } else { throw new Error(`Chalkboard.diff.solve: For second-order scalar, provide initial conditions as { y0, dy0 } or { y, dy }.`); } } else if (ode.dimension === 1 && ("y0" in y0obj) && typeof y0obj.y0 === "number" && Number.isFinite(y0obj.y0)) { y0 = [y0obj.y0]; if (config.returnObject) keys = ["y"]; } else if (ode.dimension === 1 && ("y" in y0obj) && typeof y0obj.y === "number" && Number.isFinite(y0obj.y)) { y0 = [y0obj.y]; if (config.returnObject) keys = ["y"]; } else if ("y0" in y0obj && Array.isArray(y0obj.y0)) { const arr = y0obj.y0 as number[]; if (arr.length !== ode.dimension) throw new Error(`Chalkboard.diff.solve: Object "y0.y0" must have length ${ode.dimension}.`); for (let i = 0; i < arr.length; i++) if (typeof arr[i] !== "number" || !Number.isFinite(arr[i])) throw new Error(`Chalkboard.diff.solve: y0.y0[${i}] must be a finite number.`); y0 = arr.slice(); } else { keys = Object.keys(config.y0).sort(); if (keys.length !== ode.dimension) throw new Error(`Chalkboard.diff.solve: Object "y0" must have exactly ${ode.dimension} numeric properties (got ${keys.length}).`); const arr: number[] = []; for (let i = 0; i < keys.length; i++) { const v = (config.y0 as any)[keys[i]]; if (typeof v !== "number" || !Number.isFinite(v)) throw new Error(`Chalkboard.diff.solve: y0.${keys[i]} must be a finite number.`); arr.push(v); } y0 = arr; } } if (config.returnObject && !keys) { if (ode.type === "single" && ode.order === 2 && ode.dimension === 2) { keys = ["y", "dy"]; } else if (ode.dimension === 1) { keys = ["y"]; } else { keys = Array.from({ length: ode.dimension }, (_, i) => `y${i + 1}`); } } let h: number; let steps: number; if (typeof config.h === "number") { if (typeof config.h !== "number" || !Number.isFinite(config.h) || config.h === 0) throw new Error(`Chalkboard.diff.solve: "config.h" must be a finite non-zero number.`); h = config.h; steps = Math.max(1, Math.floor(Math.abs((config.t1 - t0) / h))); h = (config.t1 - t0) / steps; } else if (typeof config.steps === "number") { if (!Number.isInteger(config.steps) || config.steps < 1) throw new Error(`Chalkboard.diff.solve: "config.steps" must be an integer greater than or equal to 1.`); steps = config.steps; h = (config.t1 - t0) / steps; } else { throw new Error(`Chalkboard.diff.solve: Provide either "config.h" or "config.steps".`); } const add = (a: number[], b: number[]): number[] => Chalkboard.stat.add(a, b); const scl = (a: number[], k: number): number[] => Chalkboard.stat.scl(a, k); const f = ode.rule; const t: number[] = new Array(steps + 1); const y: number[][] = new Array(steps + 1); t[0] = t0; y[0] = y0.slice(); const stepper = (() => { if (method === "euler") return (f: (t: number, y: number[]) => number[], t: number, y: number[], h: number): number[] => { const k1 = f(t, y); return add(y, scl(k1, h)); }; if (method === "midpoint") return (f: (t: number, y: number[]) => number[], t: number, y: number[], h: number): number[] => { const k1 = f(t, y); const ymid = add(y, scl(k1, h / 2)); const k2 = f(t + h / 2, ymid); return add(y, scl(k2, h)); }; if (method === "heun") return (f: (t: number, y: number[]) => number[], t: number, y: number[], h: number): number[] => { const k1 = f(t, y); const ypred = add(y, scl(k1, h)); const k2 = f(t + h, ypred); return add(y, scl(add(k1, k2), h / 2)); }; if (method === "ralston") return (f: (t: number, y: number[]) => number[], t: number, y: number[], h: number): number[] => { const k1 = f(t, y); const y2 = add(y, scl(k1, (2 / 3) * h)); const k2 = f(t + (2 / 3) * h, y2); return add(y, scl(add(scl(k1, 1 / 4), scl(k2, 3 / 4)), h)); }; return (f: (t: number, y: number[]) => number[], t: number, y: number[], h: number): number[] => { const k1 = f(t, y); const k2 = f(t + h / 2, add(y, scl(k1, h / 2))); const k3 = f(t + h / 2, add(y, scl(k2, h / 2))); const k4 = f(t + h, add(y, scl(k3, h))); const sum23 = add(scl(k2, 2), scl(k3, 2)); const total = add(add(k1, sum23), k4); return add(y, scl(total, h / 6)); }; })(); for (let i = 0; i < steps; i++) { const ti = t[i]; const yi = y[i]; const yNext = stepper(f, ti, yi, h); if (!Array.isArray(yNext) || yNext.length !== ode.dimension) throw new Error(`Chalkboard.diff.solve: Internal step produced invalid state length (expected ${ode.dimension}).`); for (let k = 0; k < yNext.length; k++) if (typeof yNext[k] !== "number" || !Number.isFinite(yNext[k])) throw new Error(`Chalkboard.diff.solve: State became non-finite at step ${i + 1}, index ${k}.`); t[i + 1] = ti + h; y[i + 1] = yNext; } const result: { t: number[]; y: number[][]; yObj?: {[key: string]: number}[]; } = { t, y }; if (config.returnObject && keys && keys.length === ode.dimension) { result.yObj = y.map((row) => { const obj: { [key: string]: number } = {}; for (let i = 0; i < keys.length; i++) obj[keys[i]] = row[i]; return obj; }); } return result; }; /** * Solves an ordinary differential equation using the adaptive Dormand–Prince (or Runge–Kutta–Fehlberg) method. Produces a variable-step solution. * @param {ChalkboardODE} ode - The ordinary differential equation to solve. * @param {number} [config.t0=0] - Initial time. * @param {number} config.t1 - Final time. * @param {number | number[] | Object} config.y0 - Initial state (same conventions as Chalkboard.diff.solve). * @param {number} [config.h0] - Initial step guess. * @param {number} [config.hMin] - Minimum step size magnitude. * @param {number} [config.hMax] - Maximum step size magnitude. * @param {number} [config.rtol=1e-6] - Relative tolerance. * @param {number} [config.atol=1e-9] - Absolute tolerance. * @param {number} [config.maxSteps=100000] - Maximum accepted+rejected iterations. * @param {boolean} [config.returnObject=false] - Whether to return yObj (same as Chalkboard.diff.solve). * @returns {{ t: number[]; y: number[][]; yObj?: {[key: string]: number}[]; }} * @example * // Solve first-order ODE with adaptive RK45: * const ode1 = Chalkboard.diff.init((t, y) => -2 * y); * const sol1 = Chalkboard.diff.solveAdaptive(ode1, { * t0: 0, t1: 5, * y0: 1, * h0: 0.1, * hMin: 0.01, * hMax: 0.5 * }); * * // Solve second-order ODE with adaptive RK45: * const ode2 = Chalkboard.diff.init((t, y, dy) => dy - y); * const sol2 = Chalkboard.diff.solveAdaptive(ode2, { * t0: 0, t1: 10, * y0: { y0: 1, dy0: 0 }, * h0: 0.1, * hMin: 0.01, * hMax: 0.5, * returnObject: true * }); * * // Solve system ODE with adaptive RK45: * const odeSys = Chalkboard.diff.init((t, y) => [y[1], -y[0]], 2); * const solSys = Chalkboard.diff.solveAdaptive(odeSys, { * t0: 0, t1: 10, * y0: [1, 0], * h0: 0.1, * hMin: 0.01, * hMax: 0.5 * }); */ export const solveAdaptive = ( ode: ChalkboardODE, config: { t0?: number; t1: number; y0: number | number[] | Record; h0?: number; hMin?: number; hMax?: number; rtol?: number; atol?: number; maxSteps?: number; returnObject?: boolean; } ): { t: number[]; y: number[][]; yObj?: { [key: string]: number }[] } => { if (!ode || typeof ode !== "object") throw new Error(`Chalkboard.diff.solveAdaptive: Parameter "ode" must be a ChalkboardODE.`); if (typeof ode.rule !== "function") throw new Error(`Chalkboard.diff.solveAdaptive: "ode.rule" must be a function.`); if (!Number.isInteger(ode.dimension) || ode.dimension < 1) throw new Error(`Chalkboard.diff.solveAdaptive: "ode.dimension" must be an integer >= 1.`); if (typeof config !== "object" || config === null) throw new Error(`Chalkboard.diff.solveAdaptive: Parameter "config" must be an object.`); if (typeof config.t1 !== "number" || !Number.isFinite(config.t1)) throw new Error(`Chalkboard.diff.solveAdaptive: "config.t1" must be a finite number.`); const t0 = config.t0 ?? 0; if (typeof t0 !== "number" || !Number.isFinite(t0)) throw new Error(`Chalkboard.diff.solveAdaptive: "config.t0" must be a finite number.`); if (config.t1 === t0) throw new Error(`Chalkboard.diff.solveAdaptive: "config.t1" must be different from "config.t0".`); const rtol = config.rtol ?? 1e-6; const atol = config.atol ?? 1e-9; if (typeof rtol !== "number" || !Number.isFinite(rtol) || rtol <= 0) throw new Error(`Chalkboard.diff.solveAdaptive: "rtol" must be > 0.`); if (typeof atol !== "number" || !Number.isFinite(atol) || atol < 0) throw new Error(`Chalkboard.diff.solveAdaptive: "atol" must be >= 0.`); const maxSteps = config.maxSteps ?? 100000; if (!Number.isInteger(maxSteps) || maxSteps < 1) throw new Error(`Chalkboard.diff.solveAdaptive: "maxSteps" must be an integer >= 1.`); let y0: number[]; let keys: string[] | undefined; if (typeof config.y0 === "number" && Number.isFinite(config.y0)) { if (ode.dimension !== 1) throw new Error(`Chalkboard.diff.solveAdaptive: Scalar "y0" is only allowed when "ode.dimension" === 1.`); y0 = [config.y0]; } else if (Array.isArray(config.y0)) { if (config.y0.length !== ode.dimension) throw new Error(`Chalkboard.diff.solveAdaptive: Array "y0" must have length ${ode.dimension}.`); for (let i = 0; i < config.y0.length; i++) if (typeof config.y0[i] !== "number" || !Number.isFinite(config.y0[i])) throw new Error(`Chalkboard.diff.solveAdaptive: "y0"[${i}] must be a finite number.`); y0 = config.y0.slice(); } else { if (typeof config.y0 !== "object" || config.y0 === null) throw new Error(`Chalkboard.diff.solveAdaptive: "y0" must be of type number, number[], or object.`); const y0obj = config.y0 as any; if (ode.type === "single" && ode.order === 2) { if (("y0" in y0obj) && ("dy0" in y0obj)) { const a = y0obj.y0; const b = y0obj.dy0; if (typeof a !== "number" || !Number.isFinite(a) || typeof b !== "number" || !Number.isFinite(b)) throw new Error(`Chalkboard.diff.solveAdaptive: For second-order scalar, "y0.y0" and "y0.dy0" must be finite numbers.`); y0 = [a, b]; if (config.returnObject) keys = ["y", "dy"]; } else if (("y" in y0obj) && ("dy" in y0obj)) { const a = y0obj.y; const b = y0obj.dy; if (typeof a !== "number" || !Number.isFinite(a) || typeof b !== "number" || !Number.isFinite(b)) throw new Error(`Chalkboard.diff.solveAdaptive: For second-order scalar, "y0.y" and "y0.dy" must be finite numbers.`); y0 = [a, b]; if (config.returnObject) keys = ["y", "dy"]; } else { throw new Error(`Chalkboard.diff.solveAdaptive: For second-order scalar, provide initial conditions as { y0, dy0 } or { y, dy }.`); } } else if (ode.dimension === 1 && ("y0" in y0obj) && typeof y0obj.y0 === "number" && Number.isFinite(y0obj.y0)) { y0 = [y0obj.y0]; if (config.returnObject) keys = ["y"]; } else if (ode.dimension === 1 && ("y" in y0obj) && typeof y0obj.y === "number" && Number.isFinite(y0obj.y)) { y0 = [y0obj.y]; if (config.returnObject) keys = ["y"]; } else if ("y0" in y0obj && Array.isArray(y0obj.y0)) { const arr = y0obj.y0 as number[]; if (arr.length !== ode.dimension) throw new Error(`Chalkboard.diff.solveAdaptive: Object "y0.y0" must have length ${ode.dimension}.`); for (let i = 0; i < arr.length; i++) if (typeof arr[i] !== "number" || !Number.isFinite(arr[i])) throw new Error(`Chalkboard.diff.solveAdaptive: y0.y0[${i}] must be a finite number.`); y0 = arr.slice(); } else { keys = Object.keys(config.y0).sort(); if (keys.length !== ode.dimension) throw new Error(`Chalkboard.diff.solveAdaptive: Object "y0" must have exactly ${ode.dimension} numeric properties (got ${keys.length}).`); const arr: number[] = []; for (let i = 0; i < keys.length; i++) { const v = (config.y0 as any)[keys[i]]; if (typeof v !== "number" || !Number.isFinite(v)) throw new Error(`Chalkboard.diff.solveAdaptive: y0.${keys[i]} must be a finite number.`); arr.push(v); } y0 = arr; } } if (config.returnObject && !keys) { if (ode.type === "single" && ode.order === 2 && ode.dimension === 2) keys = ["y", "dy"]; else if (ode.dimension === 1) keys = ["y"]; else keys = Array.from({ length: ode.dimension }, (_, i) => `y${i + 1}`); } const sign = Math.sign(config.t1 - t0); let h = config.h0 ?? (config.t1 - t0) / 100; if (typeof h !== "number" || !Number.isFinite(h) || h === 0) throw new Error(`Chalkboard.diff.solveAdaptive: "h0" must be a finite non-zero number (or omitted).`); h = Math.abs(h) * sign; const hMin = (config.hMin ?? 1e-12); const hMax = (config.hMax ?? Math.abs(config.t1 - t0)); if (typeof hMin !== "number" || !Number.isFinite(hMin) || hMin <= 0) throw new Error(`Chalkboard.diff.solveAdaptive: "hMin" must be > 0.`); if (typeof hMax !== "number" || !Number.isFinite(hMax) || hMax <= 0) throw new Error(`Chalkboard.diff.solveAdaptive: "hMax" must be > 0.`); const clampAbs = (value: number, minAbs: number, maxAbs: number): number => { const s = Math.sign(value) || 1; const a = Math.min(maxAbs, Math.max(minAbs, Math.abs(value))); return s * a; }; const add = (a: number[], b: number[]): number[] => Chalkboard.stat.add(a, b); const scl = (a: number[], k: number): number[] => Chalkboard.stat.scl(a, k); const f = ode.rule; const t: number[] = [t0]; const y: number[][] = [y0.slice()]; const c2 = 1 / 5; const c3 = 3 / 10; const c4 = 4 / 5; const c5 = 8 / 9; const c6 = 1; const c7 = 1; const a21 = 1 / 5; const a31 = 3 / 40, a32 = 9 / 40; const a41 = 44 / 45, a42 = -56 / 15, a43 = 32 / 9; const a51 = 19372 / 6561, a52 = -25360 / 2187, a53 = 64448 / 6561, a54 = -212 / 729; const a61 = 9017 / 3168, a62 = -355 / 33, a63 = 46732 / 5247, a64 = 49 / 176, a65 = -5103 / 18656; const a71 = 35 / 384, a72 = 0, a73 = 500 / 1113, a74 = 125 / 192, a75 = -2187 / 6784, a76 = 11 / 84; const b1 = 35 / 384, b2 = 0, b3 = 500 / 1113, b4 = 125 / 192, b5 = -2187 / 6784, b6 = 11 / 84, b7 = 0; const bs1 = 5179 / 57600, bs2 = 0, bs3 = 7571 / 16695, bs4 = 393 / 640, bs5 = -92097 / 339200, bs6 = 187 / 2100, bs7 = 1 / 40; const errNorm = (y5: number[], y4: number[], yScale: number[]): number => { let m = 0; for (let i = 0; i < y5.length; i++) { const e = Math.abs(y5[i] - y4[i]) / yScale[i]; if (e > m) m = e; } return m; }; const makeScale = (yCurr: number[], yNext: number[]): number[] => { const s: number[] = []; for (let i = 0; i < yCurr.length; i++) s.push(atol + rtol * Math.max(Math.abs(yCurr[i]), Math.abs(yNext[i]))); return s; }; let iter = 0; while (iter < maxSteps) { iter++; const ti = t[t.length - 1]; const yi = y[y.length - 1]; if ((sign > 0 && ti >= config.t1) || (sign < 0 && ti <= config.t1)) break; const remaining = config.t1 - ti; if (Math.abs(h) > Math.abs(remaining)) h = remaining; h = clampAbs(h, hMin, hMax); const k1 = f(ti, yi); const y2 = add(yi, scl(k1, h * a21)); const k2 = f(ti + c2 * h, y2); const y3 = add(add(yi, scl(k1, h * a31)), scl(k2, h * a32)); const k3 = f(ti + c3 * h, y3); const y4 = add(add(add(yi, scl(k1, h * a41)), scl(k2, h * a42)), scl(k3, h * a43)); const k4 = f(ti + c4 * h, y4); const y5s = add(add(add(add(yi, scl(k1, h * a51)), scl(k2, h * a52)), scl(k3, h * a53)), scl(k4, h * a54)); const k5 = f(ti + c5 * h, y5s); const y6 = add(add(add(add(add(yi, scl(k1, h * a61)), scl(k2, h * a62)), scl(k3, h * a63)), scl(k4, h * a64)), scl(k5, h * a65)); const k6 = f(ti + c6 * h, y6); const y7 = add(add(add(add(add(add(yi, scl(k1, h * a71)), scl(k2, h * a72)), scl(k3, h * a73)), scl(k4, h * a74)), scl(k5, h * a75)), scl(k6, h * a76)); const k7 = f(ti + c7 * h, y7); const yNext5 = add(yi, scl(add(add(add(scl(k1, b1), scl(k2, b2)), add(scl(k3, b3), scl(k4, b4))), add(add(scl(k5, b5), scl(k6, b6)), scl(k7, b7))), h)); const yNext4 = add(yi, scl(add(add(add(scl(k1, bs1), scl(k2, bs2)), add(scl(k3, bs3), scl(k4, bs4))), add(add(scl(k5, bs5), scl(k6, bs6)), scl(k7, bs7))), h)); const scale = makeScale(yi, yNext5); const e = errNorm(yNext5, yNext4, scale); const safety = 0.9; const minFactor = 0.2; const maxFactor = 5.0; if (e <= 1) { const tNext = ti + h; t.push(tNext); y.push(yNext5); const factor = e === 0 ? maxFactor : Math.min(maxFactor, Math.max(minFactor, safety * Math.pow(1 / e, 1 / 5))); h = h * factor; } else { const factor = Math.min(1.0, Math.max(minFactor, safety * Math.pow(1 / e, 1 / 5))); h = h * factor; if (Math.abs(h) < hMin) throw new Error(`Chalkboard.diff.solveAdaptive: Step size underflow (h < hMin).`); } } if (iter >= maxSteps) throw new Error(`Chalkboard.diff.solveAdaptive: Exceeded maxSteps=${maxSteps}.`); const result: { t: number[]; y: number[][]; yObj?: { [key: string]: number }[] } = { t, y }; if (config.returnObject && keys && keys.length === ode.dimension) { result.yObj = y.map((row) => { const obj: { [key: string]: number } = {}; for (let i = 0; i < keys.length; i++) obj[keys[i]] = row[i]; return obj; }); } return result; }; /** * Returns the first component of a solution of an ODE. * @param {{ t: number[]; y: number[][] }} sol - The solution of the ODE. * @returns {number[]} */ export const toScalarSeries = (sol: { t: number[]; y: number[][] }): number[] => { const result: number[] = []; for (let i = 0; i < sol.y.length; i++) result.push(sol.y[i][0]); return result; }; } }