FnInterval.kt
src/jvmMain/kotlin/afield/math/functions/FnInterval.kt · 1310 lines
package and imports — 87 lines, from line 1
@file:Suppress("MemberVisibilityCanBePrivate")
@file:afield.runtime.completion.CompletionCategory("Function")
package afield.math.functions
import afield.data.collections.m
import afield.data.collections.times
import afield.data.graph.rules
import afield.document.context.Why
import afield.document.context.why
import afield.document.graph.minusAssign
import afield.document.graph.plusAssign
import afield.document.properties.compareTo
import afield.document.workspace.file
import afield.geometry.mesh.bounds
import afield.geometry.mesh.contains
import afield.geometry.mesh.gradient
import afield.geometry.mesh.rounded
import afield.graphics.render.rangeTo
import afield.math.autodiff.clamp
import afield.math.autodiff.compareTo
import afield.math.autodiff.derivative
import afield.math.autodiff.evalFormula
import afield.math.autodiff.evalFormulaAD
import afield.math.autodiff.minus
import afield.math.autodiff.plus
import afield.math.autodiff.product
import afield.math.autodiff.round
import afield.math.autodiff.times
import afield.math.linalg.remAssign
import afield.math.linalg.set
import afield.math.linalg.times
import afield.math.units.minus
import afield.math.units.plus
import afield.math.units.times
import afield.media.image.channel
import afield.media.image.split
import afield.runtime.execution.cell
import afield.runtime.execution.go
import afield.runtime.lifecycle.once
import afield.simulation.agents.step
import afield.simulation.automata.step
import afield.ui.visualization.SignalViewer
import afield.animation.keyframes.h
import afield.audio.synth.*
import afield.data.collections.get
import afield.data.collections.invoke
import afield.data.collections.r
import afield.data.collections.s
import afield.graphics.vulkan.buffers.add
import afield.lang.stacksl.h
import afield.math.autodiff.FnExpr
import afield.math.autodiff.FnFormulas
import afield.math.autodiff.abs
import afield.math.autodiff.atan
import afield.math.autodiff.atan2
import afield.math.autodiff.ceil
import afield.math.autodiff.cos
import afield.math.autodiff.div
import afield.math.autodiff.exp
import afield.math.autodiff.floor
import afield.math.autodiff.hypot
import afield.math.autodiff.ln
import afield.math.autodiff.max
import afield.math.autodiff.maxOf
import afield.math.autodiff.min
import afield.math.autodiff.minOf
import afield.math.autodiff.pow
import afield.math.autodiff.sign
import afield.math.autodiff.sin
import afield.math.autodiff.sqrt
import afield.math.autodiff.tanh
import afield.math.linalg.aa
import afield.math.linalg.bb
import afield.math.linalg.f
import afield.math.linalg.i
import afield.math.linalg.invoke
import afield.math.linalg.isNaN
import afield.math.linalg.l
import afield.math.linalg.x
import afield.math.units.div
import afield.runtime.execution.end
import afield.runtime.execution.start
import afield.study.ui.numbers
import afield.ui.output.h
import java.util.PriorityQueue
Certified bounds for an Fn over an interval — the third walker of FnFormulas, in interval
arithmetic, and what turns a sampled envelope into a guarantee.
with(Functions) {
val f = sin(0.025, amplitude = 0.2, offset = 0.45) + gaussian(1234.57, 0.002, 0.5, 0.0..3600.0)
f.enclose(1234.0, 1235.0) // lo..hi that every f(x) on that interval lies inside
f.certifiedRange(0.0, 3600.0) // the global max, with the x that reaches it
f.certifiedEnvelope(0.0, 3600.0, 1000) // per-bin bounds a plot cannot slip between
}
Why an enclosure, not a sampled envelope
SignalViewer bins a function by evaluating it on a grid and keeping each bin's min and max. A
feature narrower than the grid spacing can sit between two samples and never be drawn, and nothing
in the picture says so — a limit public/docs/function_conversation.md states about its own plots.
An enclosure is a different kind of object: [lo, hi] such that every value the function
takes on the interval lies inside, whether or not any sample landed near it. A spike 10 ms wide
in an hour-long signal cannot hide from it, because nothing is sampled.
One declaration, three walkers
evalFormulaInterval walks the same FnExpr tree that evalFormula walks in Double and
evalFormulaAD in tracked arithmetic, so the enclosure is of the function Fn.eval computes and
not of a third transcription of it. There is still no per-op code: every op with an FnFormulas
entry is enclosed, and an op without one is refused by name (FnEnclosure.refusal) — never
bounded by a guess. Most of the hand-written fn { x -> … } lambdas in the wild are therefore
refused; that is the cost of a guarantee, and giving an op a formula is how to lift it (it gains
an exact gradient at the same time).
What exactly is contained
The contract is the Double values Fn.eval returns for every double x in [a, b] — the
values a sampler draws — not the real-number function those doubles approximate. That choice is
what lets most of the arithmetic go unwidened:
+ − × ÷are correctly rounded, and round-to-nearest is monotone, so evaluating an interval's endpoints (or a product's four corners) in ordinaryDoublealready brackets every rounded result in between. No outward rounding is needed, which is why a held constant certifies as exactly[1, 1]rather than as1 ± ulp.abs,floor,min,max,coerceInand the branch kinds are exact.- The library transcendentals (
sin,exp,pow,tanh,atan2, …) are not correctly rounded: the Javadoc allows up to 1–2.5 ulps, and promises only that they are semi-monotonic. Their endpoint values are widened outward by TRANSCENDENTAL_ULPS ulps each side, which also absorbs the rare last-bit disagreement between an interpreted and a JIT-intrinsicMath.sin.
NaN is tracked separately (FnRange.maybeNaN) rather than widened into the range: log of a
range reaching below zero may produce NaN, and saying so is more useful than an infinite bound.
Tightness and branch-and-bound
Plain interval arithmetic is sound but loose: x - x over [0, 1] encloses as [-1, 1], because
the walker does not know the two xs are the same number (the dependency problem). The bound
converges to the true range as the interval shrinks, so everything above Functions.Fn.enclose
works by subdividing: certifiedRange is a best-first branch-and-bound on the maximum and the
minimum, the certified… predicates split until each piece is decided, and certifiedEnvelope
refines each bin until its bound is within a tolerance of what that bin's samples show.
FnIntervalTest measures how loose the one-shot bound is and ratchets the number.
const val TRANSCENDENTAL_ULPS: Int = 4
Ulps each side a transcendental's endpoint values are widened by. See the file KDoc.
data class FnRange(val lo: Double, val hi: Double, val maybeNaN: Boolean = false)
A set of Double values: every number in [lo, hi], plus NaN when maybeNaN. Empty (no
non-NaN value at all) when lo > hi, which is how log of an all-negative range is represented.
data class FnRange(val lo: Double, val hi: Double, val maybeNaN: Boolean = false) {
val isEmpty: Boolean get() = !(lo <= hi)
True when no non-NaN value is possible.
val width: Double get() = if (isEmpty) 0.0 else hi - lo
hi - lo, or 0 when empty.
val isPoint: Boolean get() = lo == hi && !maybeNaN
True when this is one number, with no NaN.
operator fun contains(value: Double): Boolean
Whether value is a member — NaN is a member exactly when maybeNaN.
operator fun contains(value: Double): Boolean =
if (value.isNaN()) maybeNaN else !isEmpty && value >= lo && value <= hi
fun hull(other: FnRange): FnRange
The smallest range holding both.
fun hull(other: FnRange): FnRange = when {
isEmpty -> other.copy(maybeNaN = other.maybeNaN || maybeNaN)
other.isEmpty -> copy(maybeNaN = maybeNaN || other.maybeNaN)
else -> FnRange(minOf(lo, other.lo), maxOf(hi, other.hi), maybeNaN || other.maybeNaN)
}
fun including(value: Double): FnRange
This range with value added (NaN sets maybeNaN).
fun including(value: Double): FnRange = if (value.isNaN()) copy(maybeNaN = true) else hull(point(value))
override fun toString(): String
override fun toString(): String =
(if (isEmpty) "∅" else "[$lo, $hi]") + if (maybeNaN) " ∪ {NaN}" else ""
companion object
companion object {
fun point(value: Double): FnRange = if (value.isNaN()) NAN else FnRange(value, value)
Exactly value.
val ENTIRE = FnRange(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, true)
Every double, and NaN.
val NAN = FnRange(Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, true)
Only NaN.
val EMPTY = FnRange(Double.POSITIVE_INFINITY, Double.NEGATIVE_INFINITY, false)
Nothing at all.
class FnIntervalRefusal(message: String) : RuntimeException(message)
Thrown inside a walk when no sound bound exists — an op with no formula, an unbounded
shape domain, a clamp whose range may invert (which throws in Fn.eval itself). Caught by
FnIntervals.enclose and reported as FnEnclosure.refusal.
object FnIntervalRules
The declared rule for each FnExpr kind — one line saying how the kind is enclosed and whether the bound is exact at the endpoints, widened, or a hull.
Kept as data rather than left implicit in the when so it can be printed in the design note and
so FnIntervalTest can hold it against FnExpr::class.sealedSubclasses in both directions:
a kind added to FnExpr with no rule fails, and a rule left behind for a kind that is gone fails.
(The when in evalFormulaInterval is exhaustive, so the compiler already refuses a kind with
no code; this refuses one with no explanation.)
val rules: Map<String, String>
val rules: Map<String, String> = linkedMapOf(
"X" to "the x interval itself (moved by an enclosing AtX)",
"Lit" to "the point [v, v]",
"Child" to "the child's own enclosure over the same x interval",
"Alpha" to "(x − min)/length on the op's finite domain: monotone, endpoint-exact; refused when the domain is unbounded",
"Add" to "endpoint sums; round-to-nearest is monotone so no widening; NaN when +inf and −inf can meet",
"Sub" to "Add of the negation — fl(a − b) is fl(a + (−b)) exactly; the fract shape t − floor(t) is bounded as one value: endpoint-exact inside a unit cell, [0, 1] across a boundary",
"Mul" to "min/max of the four corner products (square rule when both factors are the same subexpression; the monotone rule for the t·t·(3 − 2t) smoothstep shape); NaN when 0 can meet ±inf",
"Div" to "corner quotients when the divisor excludes 0; everything otherwise",
"Neg" to "[−hi, −lo], exact",
"Abs" to "exact; folds a range that straddles 0 to [0, max(|lo|, |hi|)]",
"Floor" to "[floor(lo), floor(hi)], exact — the step inside the interval is covered by monotonicity",
"Sqrt" to "monotone and correctly rounded: [sqrt(lo), sqrt(hi)] exact on the non-negative part; NaN when the range reaches below 0",
"Rem" to "endpoint-exact when b is a point and [lo, hi] stays inside one period on one side of 0 (fmod is exact); otherwise sign-of-a, |r| < max|b|, |r| <= |a|; NaN when b may be 0 or a infinite",
"Hash" to "exact hull over the integers a.toLong() can take when there are at most 256 of them; [0, 1] otherwise",
"Sin" to "endpoint values plus ±1 when a crest/trough (π/2 + 2kπ / −π/2 + 2kπ) lies inside; widened",
"Cos" to "endpoint values plus ±1 when 2kπ / π + 2kπ lies inside; widened",
"Exp" to "monotone endpoints, widened, floored at 0",
"Ln" to "monotone endpoints on the non-negative part, widened; NaN when the range reaches below 0",
"Tanh" to "monotone endpoints, widened",
"Atan" to "monotone endpoints, widened",
"Atan2" to "corners when the box avoids the branch cut (x > 0, or y strictly one sign); [−π, π] otherwise; widened",
"Hypot" to "monotone in |a| and |b|: [hypot(min|a|, min|b|), hypot(max|a|, max|b|)], widened",
"Pow" to "integer-exponent cases by parity and sign, non-integer exponents on the non-negative base, corners for a positive base; widened",
"Min" to "[min(lo), min(hi)], exact",
"Max" to "[max(lo), max(hi)], exact",
"Clamp01" to "[clamp(lo), clamp(hi)], exact",
"CoerceIn" to "monotone in all three; refused when the range may invert, because Fn.eval throws there",
"IfZero" to "the taken branch when the test is certainly zero or certainly not; the hull of both otherwise",
"IfEq" to "the taken branch when the comparison is decided (a subexpression against itself: equal unless NaN); the hull of both otherwise",
"IfLt" to "the taken branch when the comparison is decided (a subexpression against itself: never less); otherwise the hull of both, each arm walked with a narrowed to the side of b that selects it",
"AtX" to "the body over the enclosure of the moved position",
)
}
Range arithmetic
object FnRangeMath
The per-kind rules, as functions on FnRange. Public so a test can exercise one rule on its own.
object FnRangeMath {
private const val TAU = Math.PI * 2.0
fun widen(r: FnRange, ulps: Int = TRANSCENDENTAL_ULPS): FnRange
r widened outward by ulps ulps at each finite end.
fun widen(r: FnRange, ulps: Int = TRANSCENDENTAL_ULPS): FnRange {
if (r.isEmpty) return r
var lo = r.lo
var hi = r.hi
repeat(ulps) { lo = Math.nextDown(lo); hi = Math.nextUp(hi) }
return FnRange(lo, hi, r.maybeNaN)
}
private inline fun unary(a: FnRange, f: (FnRange) -> FnRange): FnRange =
if (a.isEmpty) a else f(a).let { if (a.maybeNaN) it.copy(maybeNaN = true) else it }
private inline fun binary(a: FnRange, b: FnRange, f: () -> FnRange): FnRange =
if (a.isEmpty || b.isEmpty) FnRange.EMPTY.copy(maybeNaN = a.maybeNaN || b.maybeNaN)
else f().let { if (a.maybeNaN || b.maybeNaN) it.copy(maybeNaN = true) else it }
private fun hasInfinity(r: FnRange) = r.lo.isInfinite() || r.hi.isInfinite()
private fun containsZero(r: FnRange) = !r.isEmpty && r.lo <= 0.0 && r.hi >= 0.0
fun add(a: FnRange, b: FnRange): FnRange
fun add(a: FnRange, b: FnRange): FnRange = binary(a, b) {
val infinitiesMeet = (a.lo == Double.NEGATIVE_INFINITY && b.hi == Double.POSITIVE_INFINITY) ||
(a.hi == Double.POSITIVE_INFINITY && b.lo == Double.NEGATIVE_INFINITY)
var lo = a.lo + b.lo
var hi = a.hi + b.hi
if (lo.isNaN()) lo = Double.NEGATIVE_INFINITY
if (hi.isNaN()) hi = Double.POSITIVE_INFINITY
FnRange(lo, hi, infinitiesMeet)
}
fun neg(a: FnRange): FnRange = unary(a) { FnRange(-it.hi, -it.lo) }
fun sub(a: FnRange, b: FnRange): FnRange = add(a, neg(b))
fun mul(a: FnRange, b: FnRange): FnRange
fun mul(a: FnRange, b: FnRange): FnRange = binary(a, b) {
val zeroMeetsInfinity = (hasInfinity(a) && containsZero(b)) || (hasInfinity(b) && containsZero(a))
val p1 = a.lo * b.lo
val p2 = a.lo * b.hi
val p3 = a.hi * b.lo
val p4 = a.hi * b.hi
if (p1.isNaN() || p2.isNaN() || p3.isNaN() || p4.isNaN()) FnRange.ENTIRE
else FnRange(minOf(minOf(p1, p2), minOf(p3, p4)), maxOf(maxOf(p1, p2), maxOf(p3, p4)), zeroMeetsInfinity)
}
fun square(a: FnRange): FnRange
v * v for one value drawn from a — tighter than mul of a with itself.
fun square(a: FnRange): FnRange = unary(a) {
val l = it.lo * it.lo
val h = it.hi * it.hi
when {
it.lo >= 0.0 -> FnRange(l, h)
it.hi <= 0.0 -> FnRange(h, l)
else -> FnRange(0.0, maxOf(l, h))
}
}
fun div(a: FnRange, b: FnRange): FnRange
fun div(a: FnRange, b: FnRange): FnRange = binary(a, b) {
if (b.lo > 0.0 || b.hi < 0.0) {
val q1 = a.lo / b.lo
val q2 = a.lo / b.hi
val q3 = a.hi / b.lo
val q4 = a.hi / b.hi
if (q1.isNaN() || q2.isNaN() || q3.isNaN() || q4.isNaN()) FnRange.ENTIRE
else FnRange(
minOf(minOf(q1, q2), minOf(q3, q4)), maxOf(maxOf(q1, q2), maxOf(q3, q4)),
hasInfinity(a) && hasInfinity(b)
)
} else {
FnRange(Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY, containsZero(a) || hasInfinity(a))
}
}
fun abs(a: FnRange): FnRange
fun abs(a: FnRange): FnRange = unary(a) {
when {
it.lo >= 0.0 -> FnRange(it.lo, it.hi)
it.hi <= 0.0 -> FnRange(-it.hi, -it.lo)
else -> FnRange(0.0, maxOf(-it.lo, it.hi))
}
}
fun floor(a: FnRange): FnRange = unary(a) { FnRange(Math.floor(it.lo), Math.floor(it.hi)) }
fun sqrt(a: FnRange): FnRange
sqrt: monotone and correctly rounded, so the endpoints are exact.
fun sqrt(a: FnRange): FnRange = unary(a) {
when {
it.hi < 0.0 -> FnRange.NAN
it.lo < 0.0 -> FnRange(0.0, Math.sqrt(it.hi), true)
else -> FnRange(Math.sqrt(it.lo), Math.sqrt(it.hi))
}
}
fun rem(a: FnRange, b: FnRange): FnRange
Kotlin's a % b. Exact (fmod does not round), with the sign of a and magnitude below |b|
and no larger than |a|. When b is one number and [lo, hi] holds no multiple of it on one
side of zero, a % b is increasing across the interval, so its endpoints bound it — and the
test for that is lo % b <= hi % b with the width below |b|, since a wrap would reverse them.
fun rem(a: FnRange, b: FnRange): FnRange = binary(a, b) {
val bMax = maxOf(Math.abs(b.lo), Math.abs(b.hi))
val nan = containsZero(b) || hasInfinity(a)
if (b.lo == b.hi && b.lo != 0.0 && !hasInfinity(a) && b.lo.isFinite() &&
(a.lo >= 0.0 || a.hi <= 0.0) && a.hi - a.lo < Math.abs(b.lo)
) {
val rl = a.lo % b.lo
val rh = a.hi % b.lo
if (rl <= rh) return@binary FnRange(rl, rh)
}
if (!nan && Math.abs(a.lo) < minOf(Math.abs(b.lo), Math.abs(b.hi)) &&
Math.abs(a.hi) < minOf(Math.abs(b.lo), Math.abs(b.hi)) && (b.lo > 0.0 || b.hi < 0.0)
) return@binary FnRange(a.lo, a.hi)
FnRange(maxOf(minOf(0.0, a.lo), -bMax), minOf(maxOf(0.0, a.hi), bMax), nan)
}
fun hash(seed: Long, a: FnRange): FnRange
hashToUnit(seed, a.toLong()): exact over a few integers, [0, 1] beyond.
fun hash(seed: Long, a: FnRange): FnRange {
var out = if (a.maybeNaN) FnRange.point(Functions.hashToUnit(seed, 0L)) else FnRange.EMPTY
if (a.isEmpty) return out
val lo = a.lo.toLong()
val hi = a.hi.toLong()
if (hi >= lo && hi - lo in 0..255) {
for (k in lo..hi) out = out.hull(FnRange.point(Functions.hashToUnit(seed, k)))
return out
}
return out.hull(FnRange(0.0, 1.0))
}
fun fract(a: FnRange): FnRange
t - floor(t) for one value drawn from a — the fractional part, which sub of a and
floor of a cannot see: over [0, 10] those give [-10, 10].
Inside one unit cell floor is a constant k, and fl(t - k) is monotone in t, so the
endpoints are exact. Across a cell boundary the answer is [0, 1] — closed at 1, because a
tiny negative t rounds t + 1 up to exactly 1.0. An infinite t gives inf - inf, NaN.
fun fract(a: FnRange): FnRange = unary(a) {
if (hasInfinity(it)) return@unary FnRange(0.0, 1.0, true)
val k = Math.floor(it.lo)
if (k == Math.floor(it.hi)) FnRange(it.lo - k, it.hi - k) else FnRange(0.0, 1.0)
}
True when some c + 2πk may lie in [lo, hi] — generous by a hair, since including an
extremum that is not there only loosens the bound, while missing one would break it.
private fun mayContainPhase(lo: Double, hi: Double, c: Double): Boolean {
val slack = 1e-9 + 1e-15 * maxOf(Math.abs(lo), Math.abs(hi))
val kMin = Math.ceil((lo - c) / TAU - slack)
val kMax = Math.floor((hi - c) / TAU + slack)
return kMin <= kMax
}
private fun trig(a: FnRange, f: (Double) -> Double, crest: Double, trough: Double): FnRange = unary(a) {
if (it.lo.isInfinite() || it.hi.isInfinite()) {
// sin(±inf) is NaN; every finite argument in between reaches both extremes.
val nan = it.lo == Double.NEGATIVE_INFINITY || it.hi == Double.POSITIVE_INFINITY
val full = widen(FnRange(-1.0, 1.0), 1)
return@unary if (it.lo == it.hi) FnRange.NAN else full.copy(maybeNaN = nan)
}
if (it.hi - it.lo >= TAU) return@unary widen(FnRange(-1.0, 1.0), 1)
val s1 = f(it.lo)
val s2 = f(it.hi)
var r = widen(FnRange(minOf(s1, s2), maxOf(s1, s2)))
if (mayContainPhase(it.lo, it.hi, crest)) r = FnRange(r.lo, Math.nextUp(1.0))
if (mayContainPhase(it.lo, it.hi, trough)) r = FnRange(Math.nextDown(-1.0), r.hi)
r
}
fun sin(a: FnRange): FnRange = trig(a, Math::sin, Math.PI / 2, -Math.PI / 2)
fun cos(a: FnRange): FnRange = trig(a, Math::cos, 0.0, Math.PI)
A monotone non-decreasing library function, widened.
private inline fun monotone(a: FnRange, f: (Double) -> Double): FnRange = unary(a) {
widen(FnRange(f(it.lo), f(it.hi)))
}
fun exp(a: FnRange): FnRange
fun exp(a: FnRange): FnRange = monotone(a, Math::exp).let { if (it.isEmpty) it else it.copy(lo = maxOf(0.0, it.lo)) }
fun ln(a: FnRange): FnRange
fun ln(a: FnRange): FnRange {
if (a.isEmpty) return a
if (a.hi < 0.0) return FnRange.NAN
val nan = a.maybeNaN || a.lo < 0.0
return widen(FnRange(Math.log(maxOf(0.0, a.lo)), Math.log(a.hi))).copy(maybeNaN = nan)
}
fun tanh(a: FnRange): FnRange = monotone(a, Math::tanh)
fun atan(a: FnRange): FnRange = monotone(a, Math::atan)
fun atan2(y: FnRange, x: FnRange): FnRange
fun atan2(y: FnRange, x: FnRange): FnRange = binary(y, x) {
if (x.lo > 0.0 || y.lo > 0.0 || y.hi < 0.0) {
// Off the branch cut (x ≤ 0, y = 0) and away from the origin, atan2 is monotone in each
// argument separately, so the extremes over the box sit at its corners.
val c1 = Math.atan2(y.lo, x.lo)
val c2 = Math.atan2(y.lo, x.hi)
val c3 = Math.atan2(y.hi, x.lo)
val c4 = Math.atan2(y.hi, x.hi)
widen(FnRange(minOf(minOf(c1, c2), minOf(c3, c4)), maxOf(maxOf(c1, c2), maxOf(c3, c4))))
} else {
widen(FnRange(-Math.PI, Math.PI))
}
}
fun hypot(a: FnRange, b: FnRange): FnRange
fun hypot(a: FnRange, b: FnRange): FnRange {
val finite = binary(a, b) {
val aa = abs(a)
val bb = abs(b)
widen(FnRange(Math.hypot(aa.lo, bb.lo), Math.hypot(aa.hi, bb.hi))).let { it.copy(lo = maxOf(0.0, it.lo)) }
}
// hypot(±inf, NaN) is +inf, not NaN — an infinity on either side wins over a NaN on the
// other. Found by FnIntervalTest.containmentHoldsFarFromTheOrigin, where exp overflowed.
val infinityMeetsNaN = (a.maybeNaN && !b.isEmpty && hasInfinity(b)) || (b.maybeNaN && !a.isEmpty && hasInfinity(a))
return if (infinityMeetsNaN) finite.including(Double.POSITIVE_INFINITY) else finite
}
private fun isInteger(v: Double) = v.isFinite() && v == Math.floor(v)
fun pow(a: FnRange, b: FnRange): FnRange
body — 63 lines, from line 478
fun pow(a: FnRange, b: FnRange): FnRange {
// Math.pow(anything, ±0) is exactly 1, NaN base included.
val zeroExponent = containsZero(b)
val base = powRange(a, b)
return if (zeroExponent && a.maybeNaN) base.including(1.0) else base
}
private fun powRange(a: FnRange, b: FnRange): FnRange = binary(a, b) {
if (b.isPoint && !a.isEmpty) {
val n = b.lo
when {
n == 0.0 -> return@binary FnRange(1.0, 1.0)
isInteger(n) -> {
val pl = Math.pow(a.lo, n)
val ph = Math.pow(a.hi, n)
val even = Math.abs(n % 2.0) == 0.0
return@binary when {
n > 0 && !even -> widen(FnRange(pl, ph))
n > 0 && even -> when {
a.lo >= 0.0 -> widen(FnRange(pl, ph))
a.hi <= 0.0 -> widen(FnRange(ph, pl))
else -> widen(FnRange(0.0, maxOf(pl, ph))).copy(lo = 0.0)
}
containsZero(a) -> FnRange.ENTIRE.copy(maybeNaN = false)
else -> widen(FnRange(minOf(pl, ph), maxOf(pl, ph)))
}
}
n.isNaN() -> return@binary FnRange.NAN
// pow(negative, ±inf) is a signed limit rather than NaN in Java; not worth a case.
n.isInfinite() -> return@binary FnRange.ENTIRE
else -> {
// A negative base with a non-integer exponent is NaN — except at -inf, which
// the Math.pow Javadoc defines as a limit: pow(-inf, 0.5) is +inf, pow(-inf, -0.5)
// is +0.
val atMinusInfinity = if (a.lo == Double.NEGATIVE_INFINITY) Math.pow(a.lo, n) else Double.NaN
if (a.hi < 0.0) {
return@binary if (atMinusInfinity.isNaN()) FnRange.NAN else FnRange.point(atMinusInfinity).copy(maybeNaN = true)
}
val nan = a.lo < 0.0
val lo = maxOf(0.0, a.lo)
val pl = Math.pow(lo, n)
val ph = Math.pow(a.hi, n)
return@binary widen(FnRange(minOf(pl, ph), maxOf(pl, ph))).let {
FnRange(maxOf(0.0, it.lo), it.hi, nan)
}.let { if (atMinusInfinity.isNaN()) it else it.including(atMinusInfinity) }
}
}
}
if (a.lo > 0.0) {
val c1 = Math.pow(a.lo, b.lo)
val c2 = Math.pow(a.lo, b.hi)
val c3 = Math.pow(a.hi, b.lo)
val c4 = Math.pow(a.hi, b.hi)
if (c1.isNaN() || c2.isNaN() || c3.isNaN() || c4.isNaN() || (hasInfinity(b) && 1.0 in a)) {
FnRange.ENTIRE
} else {
widen(FnRange(minOf(minOf(c1, c2), minOf(c3, c4)), maxOf(maxOf(c1, c2), maxOf(c3, c4))))
.let { it.copy(lo = maxOf(0.0, it.lo)) }
}
} else {
FnRange.ENTIRE
}
}
fun cubicSmoothstep(t: FnRange): FnRange
t·t·(3 − 2t) evaluated as Fn.eval evaluates it, for t drawn from t.
The generic rules treat the three ts as independent and overshoot 1 by about 2·Δt near
t = 1 — exactly where a hold begins, so it is the overshoot that matters most. On [0, 1]
the real cubic is increasing, and each of its three roundings is relative (2t is exact), so
every computed value lies within a factor (1 ± u)³ of the real one; the endpoint values
widened by 8 ulps cover (1 + u)⁶ from either side. That is intersected with the generic
bound, and a point t is evaluated exactly as eval would.
fun cubicSmoothstep(t: FnRange): FnRange {
fun s(v: Double) = v * v * (3.0 - 2.0 * v)
if (t.isPoint) return FnRange.point(s(t.lo))
val generic = mul(square(t), sub(FnRange.point(3.0), mul(FnRange.point(2.0), t)))
if (t.isEmpty || t.maybeNaN || t.lo < 0.0 || t.hi > 1.0) return generic
val monotone = widen(FnRange(s(t.lo), s(t.hi)), 8)
val lo = if (s(t.lo) < 1e-290) 0.0 else maxOf(0.0, monotone.lo)
return FnRange(maxOf(lo, generic.lo), minOf(monotone.hi, generic.hi))
}
fun quinticSmootherstep(u: FnRange, generic: () -> FnRange): FnRange {
u·u·u·(10 + u·(−15 + 6u)), the quintic smootherstep, evaluated as Fn.eval evaluates that
graph, for u drawn from u — intersected with generic, the bound the ordinary rules give.
Why a rule: near u = 1 the generic bound overshoots 1 by a multiple of Δu, and a
crossfade (1 − m)·p + m built on it can then never be proven <= 1 near a hold without
bisecting to Δu ~ (1 − u)³. The real quintic is increasing on [0, 1] (its derivative is
30u²(1 − u)²), so its range is its endpoint values. What is left is the rounding: every
computed value is within a factor 1 ± 28u of the real one (10 − 15u + 6u² >= 1 bounds the
one cancellation, −15 + 6u never cancels, and the seven roundings are relative), so the
endpoint values widened by 64 ulps — each ulp is more than u·|v| — cover it from both sides.
Outside [0, 1], or for a point, the generic bound (or the exact value) is used alone.
fun quinticSmootherstep(u: FnRange, generic: () -> FnRange): FnRange {
fun s(v: Double) = v * v * v * (10.0 + v * (-15.0 + 6.0 * v))
if (u.isPoint) return FnRange.point(s(u.lo))
val g = generic()
if (u.isEmpty || u.maybeNaN || u.lo < 0.0 || u.hi > 1.0) return g
val monotone = widen(FnRange(s(u.lo), s(u.hi)), 64)
val lo = if (s(u.lo) < 1e-290) 0.0 else maxOf(0.0, monotone.lo)
return FnRange(maxOf(lo, g.lo), minOf(monotone.hi, g.hi))
}
fun min(a: FnRange, b: FnRange): FnRange
fun min(a: FnRange, b: FnRange): FnRange = binary(a, b) { FnRange(minOf(a.lo, b.lo), minOf(a.hi, b.hi)) }
fun max(a: FnRange, b: FnRange): FnRange
fun max(a: FnRange, b: FnRange): FnRange = binary(a, b) { FnRange(maxOf(a.lo, b.lo), maxOf(a.hi, b.hi)) }
fun clamp01(a: FnRange): FnRange
fun clamp01(a: FnRange): FnRange = unary(a) { FnRange(it.lo.coerceIn(0.0, 1.0), it.hi.coerceIn(0.0, 1.0)) }
fun coerceIn(a: FnRange, lo: FnRange, hi: FnRange): FnRange
fun coerceIn(a: FnRange, lo: FnRange, hi: FnRange): FnRange {
if (lo.isEmpty || hi.isEmpty || lo.maybeNaN || hi.maybeNaN) {
throw FnIntervalRefusal("clamp bounds may be NaN over this interval ($lo, $hi)")
}
if (lo.hi > hi.lo) {
throw FnIntervalRefusal(
"clamp's range may invert over this interval (min reaches ${lo.hi}, max falls to ${hi.lo}); " +
"Fn.eval throws there, so there is no value to bound"
)
}
if (a.isEmpty) return a
// coerceIn(v, l, h) = min(max(v, l), h) once l ≤ h: non-decreasing in all three.
return FnRange(minOf(maxOf(a.lo, lo.lo), hi.lo), minOf(maxOf(a.hi, lo.hi), hi.hi), a.maybeNaN)
}
}
The third walker
How a branch test came out over a whole interval.
private enum class Decided { THEN, OTHERWISE, BOTH }
fun evalFormulaInterval
Walks a formula in interval arithmetic: the FnRange of values the formula can take for any x
in x — the third walker of the same FnExpr tree evalFormula and evalFormulaAD walk.
childRange resolves a role to that child's enclosure over the position range it is asked for;
alpha supplies FnExpr.Alpha. Branch kinds take the decided branch when the test's range
settles it and the hull of both otherwise, evaluating each branch only when it can be taken.
body — 91 lines, from line 622
fun evalFormulaInterval(
expr: FnExpr,
x: FnRange,
alpha: (FnRange) -> FnRange,
childRange: (role: String, at: FnRange) -> FnRange,
known: Map<FnExpr, FnRange> = emptyMap()
): FnRange {
if (known.isNotEmpty()) known[expr]?.let { return it }
fun walk(e: FnExpr, facts: Map<FnExpr, FnRange> = known): FnRange = evalFormulaInterval(e, x, alpha, childRange, facts)
fun branch(decided: Decided, then: FnExpr, otherwise: FnExpr): FnRange = when (decided) {
Decided.THEN -> walk(then)
Decided.OTHERWISE -> walk(otherwise)
Decided.BOTH -> walk(then).hull(walk(otherwise))
}
return when (expr) {
is FnExpr.X -> x
is FnExpr.Lit -> FnRange.point(expr.value)
is FnExpr.Child -> childRange(expr.role, x)
is FnExpr.Alpha -> alpha(x)
is FnExpr.Add -> FnRangeMath.add(walk(expr.a), walk(expr.b))
// `t - floor(t)` is one value's fractional part, not a difference of two unrelated numbers.
is FnExpr.Sub -> if (expr.b is FnExpr.Floor && expr.b.a == expr.a) {
FnRangeMath.fract(walk(expr.a))
} else {
FnRangeMath.sub(walk(expr.a), walk(expr.b))
}
// The same subexpression twice is the same double twice, so its product is a square.
is FnExpr.Mul -> smoothstepArgument(expr)?.let { FnRangeMath.cubicSmoothstep(walk(it)) }
?: if (expr.a == expr.b) FnRangeMath.square(walk(expr.a)) else FnRangeMath.mul(walk(expr.a), walk(expr.b))
is FnExpr.Div -> FnRangeMath.div(walk(expr.a), walk(expr.b))
is FnExpr.Neg -> FnRangeMath.neg(walk(expr.a))
is FnExpr.Abs -> FnRangeMath.abs(walk(expr.a))
is FnExpr.Floor -> FnRangeMath.floor(walk(expr.a))
is FnExpr.Sqrt -> FnRangeMath.sqrt(walk(expr.a))
is FnExpr.Rem -> FnRangeMath.rem(walk(expr.a), walk(expr.b))
is FnExpr.Hash -> FnRangeMath.hash(expr.seed, walk(expr.a))
is FnExpr.Sin -> FnRangeMath.sin(walk(expr.a))
is FnExpr.Cos -> FnRangeMath.cos(walk(expr.a))
is FnExpr.Exp -> FnRangeMath.exp(walk(expr.a))
is FnExpr.Ln -> FnRangeMath.ln(walk(expr.a))
is FnExpr.Tanh -> FnRangeMath.tanh(walk(expr.a))
is FnExpr.Atan -> FnRangeMath.atan(walk(expr.a))
is FnExpr.Atan2 -> FnRangeMath.atan2(walk(expr.y), walk(expr.x))
is FnExpr.Hypot -> FnRangeMath.hypot(walk(expr.a), walk(expr.b))
is FnExpr.Pow -> FnRangeMath.pow(walk(expr.a), walk(expr.b))
is FnExpr.Min -> FnRangeMath.min(walk(expr.a), walk(expr.b))
is FnExpr.Max -> FnRangeMath.max(walk(expr.a), walk(expr.b))
is FnExpr.Clamp01 -> FnRangeMath.clamp01(walk(expr.a))
is FnExpr.CoerceIn -> FnRangeMath.coerceIn(walk(expr.a), walk(expr.lo), walk(expr.hi))
is FnExpr.IfZero -> {
val t = walk(expr.test)
// `t == 0.0` is false for NaN, so NaN always takes `otherwise`.
val maybeZero = 0.0 in t
val maybeNonZero = t.maybeNaN || t.isEmpty || t.lo != 0.0 || t.hi != 0.0
branch(decide(maybeZero, maybeNonZero), expr.then, expr.otherwise)
}
is FnExpr.IfEq -> {
val a = walk(expr.a)
if (expr.a == expr.b) {
// One subexpression compared with itself is one double: equal unless it is NaN.
branch(decide(!a.isEmpty, a.maybeNaN), expr.then, expr.otherwise)
} else {
val b = walk(expr.b)
val maybeEqual = !a.isEmpty && !b.isEmpty && a.lo <= b.hi && b.lo <= a.hi
val maybeUnequal = a.maybeNaN || b.maybeNaN || !a.isPoint || !b.isPoint || a.lo != b.lo
branch(decide(maybeEqual, maybeUnequal), expr.then, expr.otherwise)
}
}
is FnExpr.IfLt -> if (expr.a == expr.b) walk(expr.otherwise) else {
val a = walk(expr.a)
val b = walk(expr.b)
val maybeLess = !a.isEmpty && !b.isEmpty && a.lo < b.hi
val maybeNotLess = a.maybeNaN || b.maybeNaN || a.isEmpty || b.isEmpty || a.hi >= b.lo
when (decide(maybeLess, maybeNotLess)) {
Decided.THEN -> walk(expr.then)
Decided.OTHERWISE -> walk(expr.otherwise)
// Each arm is walked knowing the comparison that selects it: wherever `then` runs,
// the value of `a` is below `b.hi`, and wherever `otherwise` runs it is at least
// `b.lo` (or NaN). Without this, triangle's `if (t < 0.5) 2t else 2 - 2t` bounds
// each arm over all of `t` and the wave encloses as [-1, 3].
Decided.BOTH -> {
val below = FnRange(a.lo, minOf(a.hi, Math.nextDown(b.hi)))
val atOrAbove = FnRange(maxOf(a.lo, b.lo), a.hi, a.maybeNaN)
walk(expr.then, known + (expr.a to below)).hull(walk(expr.otherwise, known + (expr.a to atOrAbove)))
}
}
}
// A moved position invalidates every fact learned at the old one.
is FnExpr.AtX -> evalFormulaInterval(expr.body, walk(expr.newX), alpha, childRange)
}
}
t when expr is t * t * (3 - 2t) — the exact shape of FnFormulas' smoothstep helper — else
null. Recognised structurally, so it applies wherever that shape appears rather than per op.
private fun smoothstepArgument(expr: FnExpr.Mul): FnExpr? {
val square = expr.a as? FnExpr.Mul ?: return null
if (square.a != square.b) return null
val t = square.a
val rest = expr.b as? FnExpr.Sub ?: return null
if (rest.a != FnExpr.Lit(3.0)) return null
val twice = rest.b as? FnExpr.Mul ?: return null
return if (twice.a == FnExpr.Lit(2.0) && twice.b == t) t else null
}
private fun decide(maybeThen: Boolean, maybeOtherwise: Boolean): Decided = when {
maybeThen && maybeOtherwise -> Decided.BOTH
maybeThen -> Decided.THEN
else -> Decided.OTHERWISE
}
Enclosing an Fn
data class FnEnclosure
A certified bound on an Fn over [a, b], or a refusal saying why there is none.
When certified, every value Fn.eval(x) returns for a double x in [a, b] lies in
[lo, hi], or is NaN and maybeNaN is set. When not, lo and hi are NaN — never a guess —
and refusal names the reason, with unsupportedOps listing any ops that have no formula.
data class FnEnclosure(
val a: Double,
val b: Double,
val lo: Double,
val hi: Double,
val maybeNaN: Boolean,
val certified: Boolean,
val refusal: String? = null,
val unsupportedOps: List<String> = emptyList()
) {
operator fun contains(value: Double): Boolean
Whether value is inside the certified bound. False for every value when not certified.
operator fun contains(value: Double): Boolean =
certified && (if (value.isNaN()) maybeNaN else value >= lo && value <= hi)
val width: Double get() = hi - lo
hi - lo, or NaN when refused.
fun requireCertified(): FnEnclosure
This enclosure, or an exception carrying its refusal.
fun requireCertified(): FnEnclosure {
if (!certified) throw FnIntervalRefusal(refusal ?: "not certified")
return this
}
override fun toString(): String
override fun toString(): String =
if (certified) "FnEnclosure([$a, $b] -> [$lo, $hi]${if (maybeNaN) " ∪ {NaN}" else ""})"
else "FnEnclosure([$a, $b] refused: $refusal)"
}
object FnIntervals
The interval walk over an Fn graph: support checks and the one-shot enclosure.
object FnIntervals {
fun unsupportedOps(fn: Functions.Fn): List<String>
The distinct ops in fn that have no formula, in first-seen order — exactly the ops an
enclosure refuses. A node carrying numbers["value"] is a constant and always supported.
fun unsupportedOps(fn: Functions.Fn): List<String> {
val out = LinkedHashSet<String>()
fun walk(f: Functions.Fn) {
val node = f.graphNode()
if (node.numbers.containsKey("value")) return
if (FnFormulas.valueOf(node) == null) out += node.op
for (edge in node.children) walk(edge.fn)
}
walk(fn)
return out.toList()
}
fun supports(fn: Functions.Fn): Boolean = unsupportedOps(fn).isEmpty()
True when every node of fn has a formula or is a constant.
fun range(fn: Functions.Fn, x: FnRange): FnRange = Walk().range(fn, x)
The enclosure of fn over the position range x. Throws FnIntervalRefusal when no sound
bound exists; enclose is the non-throwing form.
One enclosure's worth of memo. A graph is a DAG — the workbench rewrite reaches its
background eleven times per channel and each u eight times — and a subgraph asked for the
same position range twice gives the same answer, so it is computed once. Keyed by identity,
because Fn is a data class whose equals would walk the whole graph.
source — 46 lines, from line 807
private class Walk {
private val ranges = java.util.IdentityHashMap<Functions.Fn, HashMap<FnRange, FnRange>>()
private val quintics = java.util.IdentityHashMap<Functions.Fn, Any>()
private val none = Any()
fun range(fn: Functions.Fn, x: FnRange): FnRange {
val node = fn.graphNode()
node.numbers["value"]?.let { return FnRange.point(it) }
val memo = ranges.getOrPut(fn) { HashMap(2) }
memo[x]?.let { return it }
val formula = FnFormulas.valueOf(node)
?: throw FnIntervalRefusal(refusalFor(listOf(node.op)))
val r = if (node.op == "multiply") {
val left = childOf(fn, "left")
// The same Fn on both sides of a product is the same double twice: a square. The
// graph-level twin of the Mul(a, a) rule inside a formula.
if (left != null && left === childOf(fn, "right")) FnRangeMath.square(range(left, x))
else {
val u = quintics.getOrPut(fn) { quinticArgument(fn) ?: none }
if (u is Functions.Fn) FnRangeMath.quinticSmootherstep(range(u, x), generic = { formulaRange(fn, formula, x) })
else formulaRange(fn, formula, x)
}
} else formulaRange(fn, formula, x)
memo[x] = r
return r
}
private fun formulaRange(fn: Functions.Fn, formula: FnExpr, x: FnRange): FnRange {
val node = fn.graphNode()
return evalFormulaInterval(
formula,
x,
alpha = { at -> alphaRange(fn, at) },
childRange = { role, at ->
val edge = node.children.firstOrNull { it.role == role }
?: throw FnIntervalRefusal("op ${node.op} is missing its '$role' child")
range(edge.fn, at)
}
)
}
}
private fun childOf(fn: Functions.Fn, role: String): Functions.Fn? =
fn.graphNode().children.firstOrNull { it.role == role }?.fn
private fun constOf(fn: Functions.Fn?): Double? = fn?.graphNode()?.numbers?.get("value")
u when fn is the graph u·u·u·(10 + u·(−15 + 6u)) — the quintic smootherstep the
function workbench writes by hand as smooth01 — with the same Fn for every u, else null.
private fun quinticArgument(fn: Functions.Fn): Functions.Fn? {
fun op(f: Functions.Fn?) = f?.graphNode()?.op
val cube = childOf(fn, "left")?.takeIf { op(it) == "multiply" } ?: return null
val square = childOf(cube, "left")?.takeIf { op(it) == "multiply" } ?: return null
val u = childOf(square, "left") ?: return null
if (childOf(square, "right") !== u || childOf(cube, "right") !== u) return null
val poly = childOf(fn, "right")?.takeIf { op(it) == "add" } ?: return null
if (constOf(childOf(poly, "left")) != 10.0) return null
val outer = childOf(poly, "right")?.takeIf { op(it) == "multiply" } ?: return null
if (childOf(outer, "left") !== u) return null
val inner = childOf(outer, "right")?.takeIf { op(it) == "add" } ?: return null
if (constOf(childOf(inner, "left")) != -15.0) return null
val six = childOf(inner, "right")?.takeIf { op(it) == "multiply" } ?: return null
return if (constOf(childOf(six, "left")) == 6.0 && childOf(six, "right") === u) u else null
}
fun enclose(fn: Functions.Fn, a: Double, b: Double): FnEnclosure
The certified bound on fn over [a, b], or a refusal naming why there is none.
fun enclose(fn: Functions.Fn, a: Double, b: Double): FnEnclosure {
require(!a.isNaN() && !b.isNaN() && a <= b) { "enclose needs a <= b, got [$a, $b]" }
val missing = unsupportedOps(fn)
if (missing.isNotEmpty()) {
return FnEnclosure(a, b, Double.NaN, Double.NaN, false, false, refusalFor(missing), missing)
}
return try {
val r = range(fn, FnRange(a, b))
if (r.isEmpty) FnEnclosure(a, b, Double.NaN, Double.NaN, r.maybeNaN, true)
else FnEnclosure(a, b, r.lo, r.hi, r.maybeNaN, true)
} catch (refusal: FnIntervalRefusal) {
FnEnclosure(a, b, Double.NaN, Double.NaN, false, false, refusal.message)
}
}
internal fun refusalFor(ops: List<String>): String
internal fun refusalFor(ops: List<String>): String =
"no certified bound: ${ops.joinToString(", ") { "'$it'" }} ${if (ops.size == 1) "has" else "have"} " +
"no value formula in FnFormulas, so the interval walk cannot see inside " +
(if (ops.size == 1) "it" else "them") +
(if (ops.any { it == "opaque" || it == "lambda" }) " (a hand-written fn { x -> … } lambda is opaque by construction)" else "") +
". Build the signal from formula-bearing ops, or give the op a formula"
private fun alphaRange(fn: Functions.Fn, x: FnRange): FnRange {
val domain = fn.domain
if (!domain.isFinite) {
throw FnIntervalRefusal(
"the x-window of op '${fn.graphNode().op}' is $domain, which is not finite; Functions " +
"samples such an op over a private preview window, and the walk will not guess at it"
)
}
val length = domain.length
if (length == 0.0) return if (x.isEmpty && !x.maybeNaN) x else FnRange(0.0, 0.0)
if (x.isEmpty) return x
// Sub then divide by a positive constant: both correctly rounded, both monotone.
return FnRange((x.lo - domain.min) / length, (x.hi - domain.min) / length, x.maybeNaN)
}
}
fun Functions.Fn.enclose(a: Double, b: Double): FnEnclosure = FnIntervals.enclose(this, a, b)
The certified bound on this function over [a, b] — see FnIntervals.enclose.
Branch-and-bound
data class FnExtremum
One side of a certified range: the true extreme lies between value (reached, at witness) and
bound (proven). certified when that gap is within the requested tolerance.
data class FnExtremum(
/** The best value actually returned by `Fn.eval`, at [witness]. */
val value: Double,
/** An x at which `Fn.eval` returns [value]. */
val witness: Double,
/** The proven bound: no value on the interval is beyond it. */
val bound: Double,
/** `|bound - value|` is within the tolerance asked for. */
val certified: Boolean,
/** How many enclosures the search computed. */
val enclosures: Int
) {
val gap: Double get() = Math.abs(bound - value)
How far apart the reached value and the proven bound are.
data class FnCertifiedRange
The global minimum and maximum of an Fn over [a, b], each bracketed between a reached value
and a proven bound. refusal is set (and both sides are null) when the function cannot be
enclosed at all.
data class FnCertifiedRange(
val a: Double,
val b: Double,
val min: FnExtremum?,
val max: FnExtremum?,
/** Some x in the interval may evaluate to NaN; the extremes are of the non-NaN values. */
val maybeNaN: Boolean,
val refusal: String? = null
) {
val certified: Boolean get()
True when both extremes are certified to the tolerance.
val certified: Boolean get() = refusal == null && min?.certified == true && max?.certified == true
override fun toString(): String
override fun toString(): String =
if (refusal != null) "FnCertifiedRange([$a, $b] refused: $refusal)"
else "FnCertifiedRange([$a, $b]: min ${min!!.value} at ${min.witness} (≥ ${min.bound}), " +
"max ${max!!.value} at ${max.witness} (≤ ${max.bound})${if (certified) "" else ", not converged"})"
}
private class Piece(val lo: Double, val hi: Double, val range: FnRange)
enum class FnSearchSide { MIN, MAX }
Which extreme a branch-and-bound search was looking for.
data class FnSearchPiece(val lo: Double, val hi: Double, val range: FnRange, val side: FnSearchSide, val kept: Boolean)
One enclosure a certifiedRange search computed: every value on [lo, hi] is in range.
kept when the piece could still hold a better extreme than the one already reached, so the
search queued it for splitting; a piece that is not kept was pruned by its bound.
data class FnSearchPiece(val lo: Double, val hi: Double, val range: FnRange, val side: FnSearchSide, val kept: Boolean)
private fun midpoint(lo: Double, hi: Double): Double = lo + (hi - lo) * 0.5
The maximum of f over [a, b] (or the minimum, via sign = -1), by best-first
branch-and-bound: always split the piece whose enclosure promises the most, and stop when the best
value reached is within tolerance of the best any remaining piece could hold.
source — 55 lines, from line 982
private fun searchExtremum(
fn: Functions.Fn,
a: Double,
b: Double,
tolerance: Double,
maxEnclosures: Int,
sign: Double,
stop: (best: Double, topUpper: Double) -> Boolean = { _, _ -> false },
onEnclosure: ((FnSearchPiece) -> Unit)? = null
): FnExtremum {
fun enclosed(lo: Double, hi: Double, r: FnRange, kept: Boolean) {
onEnclosure?.invoke(FnSearchPiece(lo, hi, r, if (sign > 0) FnSearchSide.MAX else FnSearchSide.MIN, kept))
}
fun upper(r: FnRange) = if (r.isEmpty) Double.NEGATIVE_INFINITY else if (sign > 0) r.hi else -r.lo
var best = Double.NEGATIVE_INFINITY
var witness = a
fun probe(x: Double) {
val v = runCatching { fn.eval(x) }.getOrNull() ?: return
if (v.isNaN()) return
if (sign * v > best) { best = sign * v; witness = x }
}
probe(a); probe(b); probe(midpoint(a, b))
val queue = PriorityQueue<Piece>(compareByDescending { upper(it.range) })
queue += Piece(a, b, FnIntervals.range(fn, FnRange(a, b)))
enclosed(a, b, queue.peek().range, true)
var enclosures = 1
var settledBound = Double.NEGATIVE_INFINITY
while (queue.isNotEmpty()) {
val top = queue.peek()
if (upper(top.range) - best <= tolerance || enclosures >= maxEnclosures || stop(best, upper(top.range))) break
queue.poll()
val mid = midpoint(top.lo, top.hi)
if (mid <= top.lo || mid >= top.hi) {
// Cannot be split further in Double: the piece holds exactly the doubles `lo` and `hi`,
// so its bound is the larger of their point enclosures — which are tight — and final.
if (top.lo == top.hi) settledBound = maxOf(settledBound, upper(top.range))
else for (end in listOf(top.lo, top.hi)) {
settledBound = maxOf(settledBound, upper(FnIntervals.range(fn, FnRange(end, end))))
enclosures++
}
continue
}
for ((lo, hi) in listOf(top.lo to mid, mid to top.hi)) {
val r = FnIntervals.range(fn, FnRange(lo, hi))
enclosures++
probe(midpoint(lo, hi))
val kept = upper(r) > best + tolerance
enclosed(lo, hi, r, kept)
if (kept) queue += Piece(lo, hi, r)
else settledBound = maxOf(settledBound, upper(r))
}
}
val bound = maxOf(settledBound, queue.peek()?.let { upper(it.range) } ?: Double.NEGATIVE_INFINITY, best)
return FnExtremum(sign * best, witness, sign * bound, bound - best <= tolerance, enclosures)
}
fun Functions.Fn.certifiedRange
The certified global minimum and maximum of this function over [a, b], each to within
tolerance: the true extreme is at least as far out as the value reached at the witness, and no
further out than the proven bound.
maxEnclosures caps the work per side; a search that hits it returns what it has with
certified = false and the remaining gap, rather than pretending.
onEnclosure, when given, is told about every piece either search encloses, in the order it
encloses them — the raw material of intervalPicture, which draws where the proof spent its work.
fun Functions.Fn.certifiedRange(
a: Double,
b: Double,
tolerance: Double = 1e-6,
maxEnclosures: Int = 200_000,
onEnclosure: ((FnSearchPiece) -> Unit)? = null
): FnCertifiedRange {
val whole = enclose(a, b)
if (!whole.certified) return FnCertifiedRange(a, b, null, null, false, whole.refusal)
return try {
FnCertifiedRange(
a, b,
min = searchExtremum(this, a, b, tolerance, maxEnclosures, -1.0, onEnclosure = onEnclosure),
max = searchExtremum(this, a, b, tolerance, maxEnclosures, 1.0, onEnclosure = onEnclosure),
maybeNaN = whole.maybeNaN
)
} catch (refusal: FnIntervalRefusal) {
FnCertifiedRange(a, b, null, null, false, refusal.message)
}
}
enum class FnVerdictKind
The answer to a certified yes/no question about a function on an interval.
enum class FnVerdictKind {
Proven for every x in the interval.
PROVEN,
Disproven by a witness x at which Fn.eval breaks the claim.
REFUTED,
Neither, within the work budget — or the function could not be enclosed.
UNDECIDED
}
data class FnVerdict
A certified verdict, with the witness that refutes it (or proves an existence claim).
data class FnVerdict(
val kind: FnVerdictKind,
val claim: String,
/** The x that decides it, when there is one. */
val witness: Double? = null,
/** `Fn.eval(witness)`. */
val witnessValue: Double? = null,
val enclosures: Int = 0,
val refusal: String? = null
) {
val proven: Boolean get() = kind == FnVerdictKind.PROVEN
override fun toString(): String
override fun toString(): String = "$kind: $claim" +
(witness?.let { " (witness x=$it, f=$witnessValue)" } ?: "") + (refusal?.let { " — $it" } ?: "")
}
Splits [a, b] until each piece is settled by its enclosure (settled true) or a probe value
breaks the claim (breaks true). Shared by the universal predicates.
source — 50 lines, from line 1101
private fun proveForAll(
fn: Functions.Fn,
a: Double,
b: Double,
claim: String,
maxEnclosures: Int,
settled: (FnRange) -> Boolean,
breaks: (Double) -> Boolean
): FnVerdict {
val missing = FnIntervals.unsupportedOps(fn)
if (missing.isNotEmpty()) return FnVerdict(FnVerdictKind.UNDECIDED, claim, refusal = FnIntervals.refusalFor(missing))
fun check(x: Double): FnVerdict? {
val v = runCatching { fn.eval(x) }.getOrDefault(Double.NaN)
return if (breaks(v)) FnVerdict(FnVerdictKind.REFUTED, claim, x, v) else null
}
check(a)?.let { return it }
check(b)?.let { return it }
val stack = ArrayDeque<Pair<Double, Double>>()
stack.addLast(a to b)
var enclosures = 0
try {
while (stack.isNotEmpty()) {
val (lo, hi) = stack.removeLast()
val r = FnIntervals.range(fn, FnRange(lo, hi))
enclosures++
if (settled(r)) continue
val mid = midpoint(lo, hi)
if (mid <= lo || mid >= hi) {
// Two adjacent doubles and nothing between: decide each as a point, which is tight.
// (Both were probed when their parents were split or as the interval's ends.)
enclosures += 2
if (settled(FnIntervals.range(fn, FnRange(lo, lo))) && settled(FnIntervals.range(fn, FnRange(hi, hi)))) continue
check(lo)?.let { return it.copy(enclosures = enclosures) }
check(hi)?.let { return it.copy(enclosures = enclosures) }
return FnVerdict(FnVerdictKind.UNDECIDED, claim, enclosures = enclosures,
refusal = "unsettled at the adjacent doubles $lo and $hi")
}
check(mid)?.let { return it.copy(enclosures = enclosures) }
if (enclosures >= maxEnclosures) {
return FnVerdict(FnVerdictKind.UNDECIDED, claim, enclosures = enclosures,
refusal = "unsettled near [$lo, $hi] after $enclosures enclosures")
}
stack.addLast(mid to hi)
stack.addLast(lo to mid)
}
} catch (refusal: FnIntervalRefusal) {
return FnVerdict(FnVerdictKind.UNDECIDED, claim, enclosures = enclosures, refusal = refusal.message)
}
return FnVerdict(FnVerdictKind.PROVEN, claim, enclosures = enclosures)
}
fun Functions.Fn.certifiedNeverExceeds
Certifies that f(x) <= limit + slack for every x in [a, b] (and never NaN), or refutes that
same claim with a witness.
slack exists because a bound the function attains — a crossfade that reaches exactly 1 on a
hold — is usually approached tangentially, and an interval bound over any piece touching the
tangency sits a little above it however small the piece. Proving <= 1 exactly there is often
not possible by bisection; proving <= 1 + 1e-12 is quick. Ask with slack = 0.0 to find out
whether the exact claim is false: a witness above the limit by an ulp is a refutation.
fun Functions.Fn.certifiedNeverExceeds(
limit: Double,
a: Double,
b: Double,
slack: Double = 0.0,
maxEnclosures: Int = 200_000
): FnVerdict =
proveForAll(this, a, b, "f(x) <= $limit${if (slack > 0) " (+ $slack)" else ""} on [$a, $b]", maxEnclosures,
settled = { !it.maybeNaN && (it.isEmpty || it.hi <= limit + slack) },
breaks = { it.isNaN() || it > limit + slack })
fun Functions.Fn.certifiedNeverBelow
Certifies that f(x) >= limit - slack for every x in [a, b] (and never NaN), or refutes that claim with a witness.
fun Functions.Fn.certifiedNeverBelow(
limit: Double,
a: Double,
b: Double,
slack: Double = 0.0,
maxEnclosures: Int = 200_000
): FnVerdict =
proveForAll(this, a, b, "f(x) >= $limit${if (slack > 0) " (- $slack)" else ""} on [$a, $b]", maxEnclosures,
settled = { !it.maybeNaN && (it.isEmpty || it.lo >= limit - slack) },
breaks = { it.isNaN() || it < limit - slack })
fun Functions.Fn.certifiedEquals(value: Double, a: Double, b: Double, maxEnclosures: Int = 200_000): FnVerdict
Certifies that f(x) == value exactly for every x in [a, b], or refutes it with a witness.
Exactness is meaningful here because the arithmetic rules are unwidened: a held constant, a clamp pinned against its bound, a product with an exact zero all enclose as the point itself. Anything passing through a transcendental is widened by ulps and can only ever be UNDECIDED or REFUTED.
fun Functions.Fn.certifiedEquals(value: Double, a: Double, b: Double, maxEnclosures: Int = 200_000): FnVerdict =
proveForAll(this, a, b, "f(x) == $value on [$a, $b]", maxEnclosures,
settled = { !it.maybeNaN && it.lo == value && it.hi == value },
breaks = { it != value })
fun Functions.Fn.certifiedReaches(level: Double, a: Double, b: Double, maxEnclosures: Int = 200_000): FnVerdict
Certifies that f(x) >= level somewhere in [a, b] — PROVEN with the witness that reaches it,
REFUTED when the proven maximum is below level.
fun Functions.Fn.certifiedReaches(level: Double, a: Double, b: Double, maxEnclosures: Int = 200_000): FnVerdict {
val claim = "f(x) >= $level somewhere on [$a, $b]"
val whole = enclose(a, b)
if (!whole.certified) return FnVerdict(FnVerdictKind.UNDECIDED, claim, refusal = whole.refusal)
return try {
val max = searchExtremum(this, a, b, 0.0, maxEnclosures, 1.0, stop = { best, top -> best >= level || top < level })
when {
max.value >= level -> FnVerdict(FnVerdictKind.PROVEN, claim, max.witness, max.value, max.enclosures)
max.bound < level -> FnVerdict(FnVerdictKind.REFUTED, claim, max.witness, max.value, max.enclosures)
else -> FnVerdict(FnVerdictKind.UNDECIDED, claim, max.witness, max.value, max.enclosures,
"the maximum is between ${max.value} and ${max.bound}")
}
} catch (refusal: FnIntervalRefusal) {
FnVerdict(FnVerdictKind.UNDECIDED, claim, refusal = refusal.message)
}
}
data class FnCertifiedEnvelope
Per-bin certified bounds: bin i covers [edge(i), edge(i+1)] (closed, so neighbours share an
edge) and every value the function takes there lies in [low[i](../../../../../../reference/source/jvmMain/afield/audio/synth/Subharmonic.md#L240), high[i](../../../../../../reference/source/jvmMain/afield/audio/synth/Subharmonic.md#L240)].
samplesInside is the sampled envelope the bounds were refined against — the same grid a plot
would draw — so the two can be drawn together and compared.
data class FnCertifiedEnvelope(
val start: Double,
val end: Double,
val low: DoubleArray,
val high: DoubleArray,
val certified: Boolean,
val refusal: String? = null,
/** Enclosures computed in total. */
val enclosures: Int = 0,
/** Bins still wider than `observed + tolerance` when the depth cap was reached. */
val unconverged: Int = 0
) {
val bins: Int get() = low.size
fun edge(i: Int): Double = if (i >= bins) end else start + (end - start) * i / bins
The left edge of bin i; edge(bins) is end.
override fun equals(other: Any?): Boolean = this === other
override fun hashCode(): Int = System.identityHashCode(this)
fun Functions.Fn.certifiedEnvelope
Certified per-bin bounds for this function over [a, b] in bins equal bins.
Each bin starts as one enclosure and is bisected — up to maxDepth times — while its bound exceeds
the spread of the values seen at the piece edges by more than tolerance. Refinement only ever
tightens: every piece's bound is itself certified, so their union still contains every value.
A refused function returns certified = false with empty arrays and the reason.
body — 56 lines, from line 1254
fun Functions.Fn.certifiedEnvelope(
a: Double,
b: Double,
bins: Int,
tolerance: Double = 1e-3,
maxDepth: Int = 8
): FnCertifiedEnvelope {
require(bins in 1..100_000) { "bins must be in 1..100000" }
require(a < b && a.isFinite() && b.isFinite()) { "certifiedEnvelope needs a finite a < b" }
val missing = FnIntervals.unsupportedOps(this)
if (missing.isNotEmpty()) {
return FnCertifiedEnvelope(a, b, DoubleArray(0), DoubleArray(0), false, FnIntervals.refusalFor(missing))
}
val low = DoubleArray(bins)
val high = DoubleArray(bins)
var enclosures = 0
var unconverged = 0
val fn = this
fun sampled(x: Double): Double = runCatching { fn.eval(x) }.getOrDefault(Double.NaN)
fun refine(lo: Double, hi: Double, flo: Double, fhi: Double, depth: Int): FnRange {
val r = FnIntervals.range(fn, FnRange(lo, hi))
enclosures++
val seenLo = minOf(flo, fhi)
val seenHi = maxOf(flo, fhi)
val excess = if (r.isEmpty || seenLo.isNaN() || seenHi.isNaN()) 0.0 else (r.hi - seenHi) + (seenLo - r.lo)
val mid = midpoint(lo, hi)
if (excess <= tolerance) return r
if (mid <= lo || mid >= hi) {
// Two adjacent doubles: their point enclosures are the tightest honest bound.
enclosures += 2
return if (lo == hi) r else FnIntervals.range(fn, FnRange(lo, lo)).hull(FnIntervals.range(fn, FnRange(hi, hi)))
}
if (depth >= maxDepth) { unconverged++; return r }
val fmid = sampled(mid)
val left = refine(lo, mid, flo, fmid, depth + 1)
val right = refine(mid, hi, fmid, fhi, depth + 1)
return left.hull(right)
}
return try {
var fLeft = sampled(a)
for (i in 0 until bins) {
val lo = a + (b - a) * i / bins
val hi = if (i == bins - 1) b else a + (b - a) * (i + 1) / bins
val fRight = sampled(hi)
val before = unconverged
val r = refine(lo, hi, fLeft, fRight, 0)
if (unconverged > before) unconverged = before + 1
low[i] = if (r.isEmpty) Double.NaN else r.lo
high[i] = if (r.isEmpty) Double.NaN else r.hi
fLeft = fRight
}
FnCertifiedEnvelope(a, b, low, high, true, null, enclosures, unconverged)
} catch (refusal: FnIntervalRefusal) {
FnCertifiedEnvelope(a, b, DoubleArray(0), DoubleArray(0), false, refusal.message, enclosures)
}
}