Math
Numerics: signal processing in both domains, estimation and fitting, interpolation, statistics, randomness and units. Everything here is plain Double/Float Kotlin with no GPU and no window, so it runs in a box, a test or a headless docs capture equally well.
47 source file(s), 2162 public symbol(s), 1623 of them carrying KDoc. Everything below is the documentation that lives beside the code — the same text the completion documentation panel shows in the editor. A blank summary means the declaration has no KDoc yet, not that this page dropped it.
interpolants.kt
smoothTo wraps a target-producing lambda so that reading it eases toward the target instead
of jumping to it.
The returned Interpolant is also a HasUniform/ProgressingUniform, so it can sit
where Field expects a uniform and be re-evaluated per frame. It implements
NeedsStartingValue, and whoever binds it is responsible for calling startAt with the
current value — until that happens the interpolant simply reports the target and marks itself
complete.
Interpolation is linear and only covers Float, Double, Int, Long and
Vector2f/Vector3f/Vector4f; any other type completes immediately at the target. Progress
is either time-based (time seconds), a fixed number of evaluations (steps, default 100), or
frame-based by supplying clock = frameClock(fps) { frameNumber }. Time-based interpolation uses
System.nanoTime by default; an injected clock makes offline and fixed-rate rendering independent
of scheduler pauses. The interpolation is one-shot: once it reaches the target it stays there.
inertialTo is the other half of the library, for the case smoothTo handles badly: a target that
keeps moving. It takes the same time/steps/clock arguments, but eases on a smoothstep whose
timing it re-solves whenever the target changes, so a re-aim carries the velocity it already had
instead of restarting from a standstill.
src/jvmMain/kotlin/afield/animation/interpolation/interpolants.kt — read the source
Used in: Values over time: interpolants & keyframes
Top-level functions
| Declaration | Summary |
|---|---|
fun frameClock(fps: Double, frameNumber: () -> Number): () -> Double |
Converts an authoritative rendered-frame counter into the seconds clock accepted by smoothTo. |
fun <T> smoothTo(time: Double? = null, steps: Int? = 100, clock: (() -> Double)? = null, a: () -> T): Interpolant<T> |
Wraps a target-producing lambda so that reading it eases toward the target instead of jumping there — the one-word way to take the snap out of a value assignment. |
fun <T> inertialTo(time: Double? = null, steps: Int? = 100, clock: (() -> Double)? = null, overshoot: Double = 0.05, a: () -> T): Interpolant<T> |
Wraps a target-producing lambda so that reading it eases toward the target with inertia — the version of smoothTo for a target that keeps moving. |
NeedsStartingValue
interface NeedsStartingValue<T>
Implemented by a value that can only interpolate once something tells it where it is starting from.
| Declaration | Summary |
|---|---|
fun startAt(a: Any?) |
Sets the value to ease away from, and restarts the interpolation. |
Interpolant
interface Interpolant<T> : () -> T, NeedsStartingValue<T>
A callable interpolation whose starting value can be set or restarted explicitly.
InertialEasing
The arithmetic behind inertialTo, for driving an inertial blend by hand — in a game loop, or on something the interpolant itself cannot carry, like a quaternion.
| Declaration | Summary |
|---|---|
data class Solved(val t: Double, val s: Double) |
A time and a scale for smoothstep, as returned by solve. |
fun smoothstep(t: Double, s: Double): Double |
The scaled smoothstep s * (3t² - 2t³), held at s once t passes 1. |
fun smoothstepDt(t: Double, s: Double): Double |
The derivative of smoothstep with respect to t — the velocity the curve is travelling at. |
fun solve(x: Double, v: Double, overshoot: Double = 0.05, eps: Double = 1e-8): Solved |
Finds the smoothstep that is x away from its destination and moving at velocity v. |
spectral.kt
Frequency-domain / spectral analysis family for the stdlib DSP toolkit.
This is the frequency-domain companion to timeseries.kt (which is entirely time-domain): a
pure-Kotlin FFT, analysis windows, a Spectrum and a Spectrogram (STFT), classic spectral
features (centroid / spread / rolloff / flux / flatness / RMS / energy), and streaming
FloatFilter wrappers so those features drop straight into the time-domain filter chain in
timeseries.kt (compose with then).
Everything is plain Double/Float math — no external DSP dependency — so it works headless in
a box, a test, or a docs capture. The FFT is an iterative Cooley–Tukey radix-2 core with a
Bluestein (chirp-z) fallback so arbitrary (non-power-of-two) lengths transform exactly.
src/jvmMain/kotlin/afield/audio/analysis/spectral.kt — read the source
Used in: Signal examples: time and frequency
Top-level functions
| Declaration | Summary |
|---|---|
fun fft(signal: DoubleArray, sampleRate: Double = signal.size.toDouble()): Spectrum+2 overloads · also FloatArray, List<Float> |
Forward FFT of a real signal. sampleRate defaults to the signal length (bins == cycles/frame). |
fun fft(signal: DoubleArray, window: WindowType, sampleRate: Double = signal.size.toDouble()): Spectrum |
Windowed forward FFT: multiplies by window first (amplitude compensated by coherent gain). |
fun ifft(spectrum: Spectrum): DoubleArray |
Inverse FFT; returns the real part of the reconstructed signal. |
fun ifftComplex(spectrum: Spectrum): Pair<DoubleArray, DoubleArray> |
Inverse FFT keeping both real and imaginary parts. |
fun stft(signal: DoubleArray, fftSize: Int = 1024, hopSize: Int = fftSize / 4, window: WindowType = WindowType.HANN, sampleRate: Double = signal.size.toDouble()): Spectrogram+2 overloads · also FloatArray, List<Float> |
Short-time Fourier transform. Frames of fftSize samples are windowed by window and advanced by hopSize; the tail is zero-padded so the final partial frame is kept. |
fun istft(spec: Spectrogram): DoubleArray |
Inverse STFT — weighted overlap-add reconstruction of a real signal from a Spectrogram. Each frame is inverse-transformed, re-windowed (synthesis window == analysis window) and overlap-added; the running window-energy is divided out so a Hann-windowed COLA STFT reconstructs faithfully. |
FFT
The transform itself: an in-place, self-contained complex FFT that accepts any length, not just a power of two.
| Declaration | Summary |
|---|---|
fun isPowerOfTwo(n: Int): Boolean |
True for 1, 2, 4, 8, … — and false for zero and negatives, so it is safe on unvalidated input. |
fun nextPowerOfTwo(n: Int): Int |
Smallest power of two >= n (>= 1). |
fun transform(re: DoubleArray, im: DoubleArray) |
Forward FFT in place. re/im must be the same length; any length >= 1 is accepted (power-of-two goes through the radix-2 kernel, otherwise Bluestein). |
fun inverseTransform(re: DoubleArray, im: DoubleArray) |
Inverse FFT in place, normalized (divides by N). By the standard identity the inverse is the forward transform with real/imag swapped, then scaled by 1/N. |
fun transformRadix2(re: DoubleArray, im: DoubleArray) |
In-place radix-2 decimation-in-time forward transform; requires power-of-two length. |
fun transformBluestein(re: DoubleArray, im: DoubleArray) |
In-place arbitrary-length forward transform via Bluestein's chirp-z algorithm. |
WindowType
Which analysis window to taper a frame with before transforming it — the trade between how sharply you can separate two nearby frequencies and how much a strong one smears into its neighbours.
Constants: RECTANGULAR, HANN, HAMMING, BLACKMAN.
Windows
The window functions themselves, as plain DoubleArrays you multiply a frame by.
| Declaration | Summary |
|---|---|
fun make(type: WindowType, n: Int, periodic: Boolean = false): DoubleArray |
Build a length-n window of the given type. Symmetric (periodic=false) by default. |
fun rectangular(n: Int): DoubleArray |
All ones — i.e. no window. The periodic distinction does not apply, so there is no such parameter. |
fun hann(n: Int, periodic: Boolean = false): DoubleArray |
The raised-cosine window: 0.5 - 0.5·cos(2πi/d), zero at both ends, and the default worth reaching for. Symmetric unless periodic is true. |
fun hamming(n: Int, periodic: Boolean = false): DoubleArray |
0.54 - 0.46·cos(2πi/d) — like hann but it does not reach zero at the ends, which is what buys its slightly better resolution and slightly worse far-field leakage. |
fun blackman(n: Int, periodic: Boolean = false): DoubleArray |
The three-term 0.42 - 0.5·cos(w) + 0.08·cos(2w) window: the strongest leakage suppression here, and the widest main lobe — about 1.5 times hann's null-to-null width, so two frequencies Hann would just separate blur into one here. |
fun coherentGain(window: DoubleArray): Double |
Coherent gain (mean) of a window — used to compensate amplitude when a window is applied. |
Spectrum
class Spectrum(val re: DoubleArray, val im: DoubleArray, val sampleRate: Double)
A single complex spectrum plus the sampleRate it was measured at. re/im are the full length-N transform; for a real input the useful ("one-sided") part is bins 0..N/2.
| Declaration | Summary |
|---|---|
val re: DoubleArray |
|
val im: DoubleArray |
|
val sampleRate: Double |
|
val size: Int |
|
val binCount: Int |
Number of one-sided bins (DC .. Nyquist), i.e. N/2 + 1 for even N. |
fun binFrequency(k: Int): Double |
Frequency (Hz) of bin k. |
fun frequencies(): DoubleArray |
Frequencies (Hz) of the one-sided bins 0..N/2. |
fun magnitude(): DoubleArray |
Full-length magnitude |Xk|. |
fun magnitudeOneSided(): DoubleArray |
One-sided magnitude (bins 0..N/2). |
fun powerOneSided(): DoubleArray |
One-sided power |Xk|². |
fun magnitudeDbOneSided(floorDb: Double = -120.0, ref: Double = 1.0): DoubleArray |
One-sided magnitude in decibels, floored at floorDb. |
fun phase(): DoubleArray |
Phase (radians) per bin. |
fun peakBin(): Int |
Bin index of the peak one-sided magnitude. |
fun peakFrequency(): Double |
Frequency (Hz) of the dominant one-sided component. |
fun centroid(): Double |
|
fun spread(): Double |
|
fun rolloff(rolloffFraction: Double = 0.85): Double |
|
fun flatness(): Double |
Spectrogram
The result of a short-time Fourier transform: a stack of per-frame one-sided magnitude spectra. magnitudesframe`, bin in 0..fftSize/2. Complex frames are kept too so an inverse-STFT overlap-add reconstruction is possible ([istft`](math.md#spectral-kt--istft)).
| Declaration | Summary |
|---|---|
val framesRe: Array<DoubleArray> |
Complex STFT frames (full length-fftSize), kept for reconstruction. |
val framesIm: Array<DoubleArray> |
|
val sampleRate: Double |
|
val fftSize: Int |
|
val hopSize: Int |
|
val window: WindowType |
|
val numFrames: Int |
|
val numBins: Int |
|
fun binFrequency(k: Int): Double |
|
fun frameTime(i: Int): Double |
Center time (seconds) of frame i. |
fun magnitude(i: Int): DoubleArray |
One-sided magnitude of frame i. |
fun magnitudes(): Array<DoubleArray> |
All frames as one-sided magnitudes: frame. |
fun maxMagnitude(): Double |
Largest magnitude anywhere in the spectrogram (for normalizing a heatmap). |
fun centroidTrace(): DoubleArray |
Per-frame spectral centroid (Hz). |
fun rolloffTrace(rolloffFraction: Double = 0.85): DoubleArray |
Per-frame spectral rolloff (Hz). |
fun fluxTrace(): DoubleArray |
Per-frame spectral flux (positive change vs previous frame). |
SpectralFeatures
Single numbers that describe the shape of a spectrum — brightness, spread, where the energy stops — for driving something from a sound without looking at every bin.
| Declaration | Summary |
|---|---|
fun centroid(magnitude: DoubleArray, sampleRate: Double): Double |
Spectral centroid (Hz): magnitude-weighted mean frequency. |
fun spread(magnitude: DoubleArray, sampleRate: Double): Double |
Spectral spread / bandwidth (Hz): magnitude-weighted std-dev of frequency about the centroid. |
fun rolloff(magnitude: DoubleArray, sampleRate: Double, rolloffFraction: Double = 0.85): Double |
Spectral rolloff (Hz): the frequency below which rolloffFraction of the total spectral energy (Σ magnitude) lies. Classic timbre / brightness descriptor. |
fun flux(previous: DoubleArray, current: DoubleArray): Double |
Spectral flux: the (positive, L2) change between two consecutive magnitude spectra. A standard onset / novelty descriptor. Only positive differences are summed (half-wave rectified). |
fun flatness(magnitude: DoubleArray): Double |
Spectral flatness (Wiener entropy): geometric mean / arithmetic mean of the power spectrum. ~1 for noise-like (flat) spectra, ->0 for tonal (peaky) spectra. |
fun rms(signal: DoubleArray): Double |
Root-mean-square level of a time-domain buffer. |
fun rms(signal: FloatArray): Double |
Root-mean-square level of a time-domain buffer — loudness, roughly, and the number to drive a meter from. 0.0 for an empty buffer. |
fun energy(signal: DoubleArray): Double |
Total energy Σ x² of a time-domain buffer. |
Mel
The mel scale and its triangular filter bank — frequency re-spaced the way hearing actually works, so that a step of one band means about as much low down as high up.
| Declaration | Summary |
|---|---|
fun hzToMel(hz: Double): Double |
Hz to mels, 2595·log10(1 + hz/700). Monotonic and 0 at 0 Hz. |
fun melToHz(mel: Double): Double |
Mels back to Hz — the exact inverse of hzToMel. |
fun filterBank(magnitude: DoubleArray, sampleRate: Double, numBands: Int = 26, minHz: Double = 0.0, maxHz: Double = sampleRate / 2.0): DoubleArray |
Apply a triangular mel filter bank to a one-sided magnitude spectrum, returning numBands mel-band energies spanning minHz..maxHz. |
SpectralFrameFilter
Base class: buffers samples, fires onFrame every hop with the current windowed frame.
| Declaration | Summary |
|---|---|
val fftSize: Int |
|
val hopSize: Int |
|
val sampleRate: Float |
|
val window: WindowType |
|
override fun invoke(value: Float): Float |
|
fun reset() |
SpectralCentroidFilter
Streaming spectral centroid (Hz), held between hops.
| Declaration | Summary |
|---|---|
override fun onFrame(spectrum: Spectrum): Float |
SpectralRolloffFilter
Streaming spectral rolloff (Hz), held between hops.
| Declaration | Summary |
|---|---|
var rolloffFraction: Double |
|
override fun onFrame(spectrum: Spectrum): Float |
SpectralFluxFilter
Streaming spectral flux (onset novelty), held between hops.
| Declaration | Summary |
|---|---|
override fun onFrame(spectrum: Spectrum): Float |
RmsFilter
class RmsFilter(val windowSize: Int = 256) : FloatFilter
Streaming windowed RMS: a rolling root-mean-square over the last windowSize samples. Cheap O(1)-per-sample running sum-of-squares — the time-domain energy companion to the spectral filters.
| Declaration | Summary |
|---|---|
override fun invoke(value: Float): Float |
|
fun reset() |
linalg_mesh.kt
The 3D convex hull of a list of Vec3, computed with the QuickHull3D library.
One function, convexHull, which keeps the vertices on the outside of the cloud and drops the
interior ones. Vertices only, in the library's own order, with no faces and no winding. Fewer
than four points, or a cloud QuickHull cannot triangulate, returns the input unchanged rather
than throwing, so a result the same size as the input may be a hull or a refusal and the two
cannot be told apart from the return value.
The vector types themselves are in linalg.kt. Centroids, medians and bounding boxes for the
same point collections are in linalg_spatial.kt, which also holds convexHullPeelingMedian
and the asQuickHullPoints packer.
src/jvmMain/kotlin/afield/geometry/mesh/linalg_mesh.kt — read the source
Top-level functions
| Declaration | Summary |
|---|---|
fun List<Vec3>.convexHull(): List<Vec3> |
The vertices of the 3D convex hull, via QuickHull3D — the points on the outside, with the interior ones dropped. |
CubicBezier3D.kt
A double-precision 3D cubic Bézier for planning paths with a minimum turning radius.
CubicBezier3D evaluates positions and tangents, gives a conservative lower bound on the
curve's radius with CubicBezier3D.minimumRadiusBound, and turns the curve into a polyline
within a caller-chosen deviation with CubicBezier3D.flatten. Degenerate curves, cusps and
invalid requests throw rather than return a guess. CubicBezier3DTest covers these cases,
including invariance under translation, rotation and change of units.
src/jvmMain/kotlin/afield/geometry/path/planning/CubicBezier3D.kt — read the source
CubicBezier3D
An immutable double-precision cubic in caller-chosen units. start and end are endpoints; control1 and control2 are the inner controls. The caller supplies all approximation tolerances and curvature thresholds.
| Declaration | Summary |
|---|---|
fun valueAt(t: Double): Point3D |
Position at a finite parameter t in the closed unit interval. |
fun tangentAt(t: Double): Point3D |
Unnormalised derivative with respect to the unit parameter; it may be zero. |
fun minimumRadiusBound(minimum: Double, maximumDepth: Int = 18): Double |
Conservative lower bound on radius across the continuous cubic, at least minimum. Subdivides Bernstein control hulls, bounding speed below and |r' × r''| above. Throws if the bound cannot be established within maximumDepth; failure is not proof that no valid curve exists. Zero chords/cusps and arithmetic overflow fail closed. This is a numerical bound, not interval-arithmetic certification. |
fun flatten(maximumDeviation: Double, maximumDepth: Int = 24): List<Point3D> |
Polyline including both endpoints. Every subcurve's control hull is within maximumDeviation of its finite chord. This bounds curve-to-polyline distance; it does not prove containment in another shape. Throws on degenerate chords or failure to converge within maximumDepth. |
Point3D.kt
A small immutable 3D point type used by the path-planning curves in this package.
Point3D carries finite double coordinates with the vector operations a curve needs:
addition, difference, scaling, Point3D.dot, Point3D.cross, Point3D.length and
Point3D.normalized.
CubicBezier3D is built from it.
src/jvmMain/kotlin/afield/geometry/path/planning/Point3D.kt — read the source
Point3D
data class Point3D(val x: Double, val y: Double, val z: Double)
Immutable double-precision coordinates; units are chosen by the caller.
| Declaration | Summary |
|---|---|
operator fun plus(b: Point3D) |
Coordinate-wise addition, returning a new value. |
operator fun minus(b: Point3D) |
Coordinate-wise difference, also usable as a displacement. |
operator fun unaryMinus() |
Negates all coordinates without changing this value. |
operator fun times(scale: Double) |
Multiplies every coordinate by scale; non-finite results are rejected. |
infix fun dot(b: Point3D) |
Euclidean dot product, treating the coordinates as vectors. |
infix fun cross(b: Point3D) |
Right-handed cross product, treating the coordinates as vectors. |
val length: Double |
Euclidean distance from the origin in the caller's units. |
fun normalized(): Point3D |
Unit direction; rejects zero length and lengths that overflow. |
fun distanceTo(b: Point3D): Double |
Euclidean distance to b in the caller's units. |
linalg_spatial.kt
What a collection of points adds up to: centroids, outlier-resistant centres, and the bounding boxes that enclose them.
center is the plain mean and medianVec the per-axis median, both available for Vec2, Vec3
and Vec4 collections and both returning a fresh vector. aabbBounds gives the axis-aligned box
over those same three dimensions, as AxisAlignedBoundingBox2D, AxisAlignedBoundingBox3D or
AxisAlignedBoundingBox4D. closest is a linear scan for the nearest member of a collection.
Two summaries exist only in 3D. obbBounds returns an OrientedBoundingBox whose axes come from
a PCA of the cloud, so an elongated diagonal cluster gets a tight box where aabbBounds gets a
loose one; its own documentation sets out how far the power-iteration axes can be trusted.
convexHullPeelingMedian strips hulls off the outside of the cloud and averages what is left,
which discards outliers instead of averaging them in.
asQuickHullPoints packs a collection into the flat DoubleArray that QuickHull3D and other
array-based libraries expect; the hull itself is in linalg_mesh.kt, and the vector types and
their constructors are in linalg.kt.
Cuboid is a different thing from the bounding boxes above: an immutable corner-plus-size 3D box
for layout arithmetic, with union, inset, translate and containment, rather than a fit to
geometry.
src/jvmMain/kotlin/afield/geometry/spatial/linalg_spatial.kt — read the source
Top-level functions
| Declaration | Summary |
|---|---|
fun Vec3.closest(a: Iterable<Vec3>) |
The member of a nearest this point by Euclidean distance. |
fun Vec4.closest(a: Iterable<Vec4>) |
The member of a nearest this point in 4D. Linear scan; throws if a is empty. |
fun Vec2.closest(a: Iterable<Vec2>) |
The member of a nearest this point in the plane. Linear scan; throws if a is empty, and the result is the collection's own instance rather than a copy. |
fun Collection<Vec2>.center(): Vec2 |
The centroid — the plain mean of every point, in a fresh Vec2. |
fun Collection<Vec2>.medianVec(): Vec2 |
The component-wise median, as a point — the outlier-resistant answer to "where is this cloud". |
fun Collection<Vec2>.aabbBounds(): AxisAlignedBoundingBox2D |
Axis-aligned bounds of the points, as an AxisAlignedBoundingBox2D. |
fun Collection<Vec3>.center(): Vec3 |
The centroid — the plain mean of every point, in a fresh Vec3. (0, 0, 0) when empty. |
fun Collection<Vec3>.medianVec(): Vec3 |
The component-wise median, as a point — robust to outliers, and generally not one of the input points, since each axis is medianed independently. center is the true mean. |
fun Collection<Vec3>.aabbBounds(): AxisAlignedBoundingBox3D |
Axis-aligned bounds of the points, as an AxisAlignedBoundingBox3D — the cheap bounding volume, one pass and no rotation. |
fun Collection<Vec3>.obbBounds(): OrientedBoundingBox |
Bounds that turn with the data: an OrientedBoundingBox whose axes come from a PCA of the point cloud, so an elongated diagonal cluster gets a tight box where aabbBounds would get a loose one. |
fun List<Vec3>.convexHullPeelingMedian(peelSteps: Int = 5): Vec3 |
A deeply outlier-resistant centre: strip the convex hull off the cloud peelSteps times, then take the mean of whatever is left. |
fun Collection<Vec3>.asQuickHullPoints(): DoubleArray |
Flattens the points into the packed [x0, y0, z0, x1, …] DoubleArray that QuickHull3D and other plain-array libraries expect. |
fun Collection<Vec4>.center(): Vec4 |
The component-wise mean of every element, in a fresh Vec4. (0, 0, 0, 0) when empty. |
fun Collection<Vec4>.medianVec(): Vec4 |
The component-wise median across all four axes, as one Vec4. Robust to outliers; see medianVec on Collection<Vec2> for why the result is generally not one of the inputs. |
fun Collection<Vec4>.aabbBounds(): AxisAlignedBoundingBox4D |
Axis-aligned bounds on all four axes, as an AxisAlignedBoundingBox4D. One pass; degenerate box at the origin when empty. |
OrientedBoundingBox
data class OrientedBoundingBox(val center: Vec3, val axes: Array<Vec3>, val extents: FloatArray)
Oriented Bounding Box (OBB) representation.
| Declaration | Summary |
|---|---|
val center: Vec3 |
|
val axes: Array<Vec3> |
|
val extents: FloatArray |
AxisAlignedBoundingBox2D
data class AxisAlignedBoundingBox2D(val min: Vec2, val max: Vec2)
Axis-Aligned Bounding Box for 2D vectors.
| Declaration | Summary |
|---|---|
val min: Vec2 |
|
val max: Vec2 |
|
val size: Vec2 |
Width and height of the box, as max - min. Zero on both axes for a single point. |
val center: Vec2 |
The midpoint of the box — not the centroid of the points it was built from. |
AxisAlignedBoundingBox3D
data class AxisAlignedBoundingBox3D(val min: Vec3, val max: Vec3)
Axis-Aligned Bounding Box for 3D vectors.
| Declaration | Summary |
|---|---|
val min: Vec3 |
|
val max: Vec3 |
|
val size: Vec3 |
Width, height and depth of the box, as max - min. |
val center: Vec3 |
The midpoint of the box — not the centroid of the points it was built from. |
AxisAlignedBoundingBox4D
data class AxisAlignedBoundingBox4D(val min: Vec4, val max: Vec4)
Axis-Aligned Bounding Box for 4D vectors.
| Declaration | Summary |
|---|---|
val min: Vec4 |
|
val max: Vec4 |
|
val size: Vec4 |
Extent of the box on each of the four axes, as max - min. |
val center: Vec4 |
The midpoint of the box — not the centroid of the points it was built from. |
Cuboid
class Cuboid(val x: Float, val y: Float, val z: Float, val w: Float, val h: Float, val d: Float)
An axis-aligned 3D box held as a corner plus a size — x, y, z and w, h, d — and immutable.
| Declaration | Summary |
|---|---|
fun union(r: Cuboid?): Cuboid |
The smallest cuboid containing both this one and r — with a null r treated as "nothing", so this is safe to fold over a nullable accumulator. |
fun translate(by: Vector2d): Cuboid |
A copy moved in x and y only, leaving z where it was. Size is unchanged. |
fun translate(by: Vector3d): Cuboid |
A copy moved on all three axes. Size is unchanged. |
fun intersects(point: Vector3d): Boolean |
Does this box contain point? Closed on every side, so a point exactly on a face or corner counts as inside — two boxes sharing a face therefore both claim the points on it. |
fun intersects(r: Cuboid): Boolean |
Do the two boxes overlap in volume? Open here, unlike the point overload: boxes that merely touch — one's face flush against the other's — do not intersect, which is what you want when packing things edge to edge. |
override fun toString(): String |
Cuboid{x=… y=… z=… w=… h=… d=…} — the corner and the size, for debugging. |
fun inset(by: Double): Cuboid |
A copy shrunk by by on every side: the corner moves in by that much and each dimension loses twice it. Negative values grow the box instead. |
fun intersectsX(x: Double): Boolean |
Does the box's x span cover the coordinate x? Half-open — [this.x, this.x + w) — so neighbouring spans laid end to end claim each coordinate exactly once. |
fun inside(start: Double, end: Double): Boolean |
Is the box's x span entirely within start..end? Containment, not overlap; y and z are ignored. |
fun convert(x: Double, y: Double, z: Double): Vector3d |
Turns a normalized position in the box into a world one: (0,0,0) is the corner and (1,1,1) the far corner. |
fun duplicate(): Cuboid |
An identical copy. Cuboid is immutable, so this is only needed for identity, not for safety. |
override fun equals(other: Any?): Boolean |
Value equality on all six fields — exact Float comparison, so arithmetic drift will fail it. |
override fun hashCode(): Int |
Consistent with equals: all six fields, so a Cuboid is safe as a map key. |
autodiff.kt
Reverse-mode automatic differentiation over Double scalars — exact gradients for ordinary
Kotlin arithmetic, in one pass, whatever the expression's size.
The alternative is a numerical gradient, which is the default in fitGradient and
leastSquares: central differences over the loss or the residuals, 2·n extra evaluations per
step, and a truncation-against-round-off compromise you tune with an fdStep. Reverse mode
costs one forward pass plus one backward sweep for all n derivatives, and the answer is
exact to the last few ulps.
AD and the tape
AD is a scalar that remembers how it was made. Arithmetic on AD values appends to an ADTape
— a flat array of (value, up-to-two parents, up-to-two local partials) — so
(a * b + sin(c)) / 2.0 reads exactly like Double arithmetic and records a graph as it goes. The
backward sweep walks that array once in reverse accumulating adjoints. Because every node has at
most two parents, the tape is five parallel arrays of primitives — value, two parent indices,
two partials — and there is no object churn beyond the AD handles themselves.
val g = grad { p -> (p[0] - 3.0).sq() + 2.0 * (p[1] + 1.0).sq() }
g(doubleArrayOf(0.0, 0.0)) // [-6.0, 4.0], exact
val (v, d) = valueAndDerivative(0.4) { x -> exp(-x * x) * sin(6.0 * x) }
grad returns a function, so it composes with the optimizers in autodiff_optimize.kt
(minimize, adam, sgd, lbfgs) without a wrapper. gradAt, valueAndGrad, jacobian and
hessian are the immediate forms.
Forward mode
Dual2 is a second-order forward-mode number (value, first derivative, second derivative along a
direction). It is the right tool when there is one input and many outputs — the reverse of
reverse mode — and it is what makes hessian exact rather than a finite difference of an exact
gradient: vᵀHv falls out of a single Dual2 pass, and polarisation recovers the off-diagonal
entries. See directionalDerivative and secondDerivative.
The kink convention
abs, min, max and relu are not differentiable at their kink, so a convention has to be
chosen. This library splits the derivative evenly at an exact tie: min(a, b) with a == b
sends 0.5 to each argument, and relu (which is max(x, 0)) has derivative 0.5 at zero. That is
what a central difference reports at a kink, so the analytic gradient and the finite-difference
oracle agree there too, and it makes abs(x) and max(x, -x) agree on derivative 0 at zero,
which is the minimum-norm subgradient. PyTorch instead defines relu'(0) = 0.
What is not covered
- Mutation.
ADis immutable.x += yon avarrebinds it to a fresh handle and appends a node; the handle it replaced stays valid, and an accumulation loop therefore grows the tape by one node per iteration. - Control flow is differentiated as taken: an
if (x > 0.0)records only the branch that ran, which is the correct derivative of the piecewise function everywhere except at the switch-over. Comparisons onADtherefore return plainBooleanand carry no derivative. - Piecewise-constant ops (
floor,round,sign,quantize) have derivative 0 by definition, and no finite-difference oracle exists at their jumps. - Third and higher derivatives are not available.
hessianis exact; a third-order term would need aDual3or a nestable tape and neither is here. - Tape size grows with the number of operations executed, not with
n. A loss over 100k samples builds a 100k-node tape per evaluation; that is the memory cost of reverse mode and it is not chunked or checkpointed.
src/jvmMain/kotlin/afield/math/autodiff/autodiff.kt — read the source
Used in: Spatial artificial life, Functions to FLine examples (Field graphics), Frames and the animation loop
Top-level functions
| Declaration | Summary |
|---|---|
operator fun Double.plus(other: AD): AD |
2.0 + x, with the literal folded in at no tape cost. |
operator fun Double.minus(other: AD): AD |
2.0 - x. |
operator fun Double.times(other: AD): AD |
2.0 * x. |
operator fun Double.div(other: AD): AD |
2.0 / x; ∂/∂x = -k/x². |
operator fun Int.plus(other: AD): AD |
2 + x for integer literals. |
operator fun Int.minus(other: AD): AD |
2 - x for integer literals. |
operator fun Int.times(other: AD): AD |
2 * x for integer literals. |
operator fun Int.div(other: AD): AD |
2 / x for integer literals. |
operator fun Double.compareTo(other: AD): Int |
Compares a plain number against a tracked one, by value. |
fun AD.reciprocal(): AD |
Reciprocal 1/x; ∂ = -1/x². |
fun AD.sq(): AD |
The square x², cheaper and more readable than x * x; ∂ = 2x. |
fun exp(x: AD): AD |
Natural exponential; ∂ = eˣ. |
fun ln(x: AD): AD |
Natural logarithm; ∂ = 1/x. Non-positive input gives NaN/-∞ as kotlin.math does. |
fun log10(x: AD): AD |
Base-10 logarithm; ∂ = 1/(x·ln 10). |
fun log2(x: AD): AD |
Base-2 logarithm; ∂ = 1/(x·ln 2). |
fun sqrt(x: AD): AD |
Square root; ∂ = 1/(2√x), which is +∞ at zero. |
fun cbrt(x: AD): AD |
Cube root, defined for negative input too; ∂ = 1/(3·x^(2/3)). |
fun sin(x: AD): AD |
Sine of an angle in radians; ∂ = cos x. |
fun cos(x: AD): AD |
Cosine of an angle in radians; ∂ = -sin x. |
fun tan(x: AD): AD |
Tangent; ∂ = 1 + tan²x. |
fun asin(x: AD): AD |
Arc sine; ∂ = 1/√(1-x²). |
fun acos(x: AD): AD |
Arc cosine; ∂ = -1/√(1-x²). |
fun atan(x: AD): AD |
Arc tangent; ∂ = 1/(1+x²). |
fun sinh(x: AD): AD |
Hyperbolic sine; ∂ = cosh x. |
fun cosh(x: AD): AD |
Hyperbolic cosine; ∂ = sinh x. |
fun tanh(x: AD): AD |
Hyperbolic tangent — the classic squashing nonlinearity; ∂ = 1 - tanh²x. |
fun abs(x: AD): AD |
Absolute value; ∂ = sign x, and 0 at exactly zero — see the kink convention in the file KDoc, which is also what a central difference reports there. |
fun relu(x: AD): AD |
The rectifier max(x, 0); ∂ = 1 for positive input, 0 for negative, and 0.5 at exactly zero under this library's even-split tie convention. |
fun sigmoid(x: AD): AD |
The logistic sigmoid 1/(1+e⁻ˣ); ∂ = σ(1-σ). Computed in the numerically stable branch. |
fun softplus(x: AD): AD |
The smooth rectifier ln(1+eˣ); ∂ = σ(x). Stable for large \|x\|. |
fun floor(x: AD): AD |
Floor. Piecewise constant, so the derivative is 0 everywhere it exists. |
fun ceil(x: AD): AD |
Ceiling. Piecewise constant, so the derivative is 0 everywhere it exists. |
fun round(x: AD): AD |
Nearest integer. Piecewise constant, so the derivative is 0 everywhere it exists. |
fun sign(x: AD): Double |
The sign of a tracked value as a plain Double — not an AD, since its derivative is 0 away from the origin and undefined at it. |
fun AD.pow(exponent: Double): AD |
x raised to a constant power; ∂ = k·x^(k-1). |
fun AD.pow(exponent: Int): AD |
x raised to a constant integer power. |
fun pow(base: AD, exponent: AD): AD |
a raised to the power b, both tracked. |
fun max(a: AD, b: AD): AD |
The larger of two tracked values. At an exact tie the derivative splits 0.5/0.5 — see the kink convention in the file KDoc. |
fun max(a: AD, b: Double): AD |
The larger of a tracked value and a constant; ties split 0.5/0.5 as elsewhere. |
fun min(a: AD, b: AD): AD |
The smaller of two tracked values. At an exact tie the derivative splits 0.5/0.5 — see the kink convention in the file KDoc. |
fun min(a: AD, b: Double): AD |
The smaller of a tracked value and a constant; ties split 0.5/0.5 as elsewhere. |
fun AD.clamp(lo: Double, hi: Double): AD |
Clamps into [lo, hi]; the derivative is 1 strictly inside and 0 strictly outside, with the usual even split exactly on a boundary. |
fun atan2(y: AD, x: AD): AD |
Two-argument arc tangent, atan2(y, x), correct in all four quadrants. |
fun hypot(x: AD, y: AD): AD |
√(x²+y²) without intermediate overflow; the partials are the components of the unit vector. |
fun sum(values: List<AD>): AD |
The sum of a list of tracked values, as a left fold — the empty list is an error, since there is no tape to hang a zero on. |
fun mean(values: List<AD>): AD |
The arithmetic mean of a list of tracked values. |
fun product(values: List<AD>): AD |
The product of a list of tracked values. |
fun dot(a: List<AD>, b: List<AD>): AD |
The dot product of two equal-length lists of tracked values. |
fun sumOfSquares(values: List<AD>): AD |
The sum of squares — the squared Euclidean norm. |
fun norm(values: List<AD>): AD |
The Euclidean norm √Σxᵢ². Its derivative is undefined at the origin, where √ reports ∞. |
fun maxOf(values: List<AD>): AD |
The largest element, folded pairwise with max (so ties split evenly at each comparison). |
fun minOf(values: List<AD>): AD |
The smallest element, folded pairwise with min. |
fun logSumExp(values: List<AD>): AD |
ln Σ eˣⁱ, shifted by the maximum so it never overflows — the smooth maximum, and the numerically safe way to write a soft-min/soft-max penalty inside a differentiated loss. |
fun softmax(values: List<AD>): List<AD> |
The softmax of a list — each element's share of Σeˣ, summing to one. |
fun valueAndGrad(x: DoubleArray, f: (List<AD>) -> AD): ADGradient |
The exact value and gradient of f at x, in one forward pass and one backward sweep. |
fun gradAt(x: DoubleArray, f: (List<AD>) -> AD): DoubleArray |
The exact gradient of f at x; valueAndGrad when you also want the value. |
fun grad(f: (List<AD>) -> AD): (DoubleArray) -> DoubleArray |
Turns an objective into its gradient function — the curried form, and the one the optimizers in autodiff_optimize.kt take. |
fun valueAndGradOf(f: (List<AD>) -> AD): (DoubleArray) -> ADGradient |
Turns an objective into a function returning both value and gradient — what minimize wants. |
fun derivative(x: Double, f: (AD) -> AD): Double |
The exact derivative of a single-variable function at x. |
fun valueAndDerivative(x: Double, f: (AD) -> AD): Pair<Double, Double> |
The value and the exact derivative of a single-variable function at x. |
fun jacobian(x: DoubleArray, f: (List<AD>) -> List<AD>): Array<DoubleArray> |
The exact Jacobian ∂fᵢ/∂xⱼ of a vector-valued function, as rows × columns. |
fun hessian(x: DoubleArray, f: (List<Dual2>) -> Dual2): Array<DoubleArray> |
The exact Hessian ∂²f/∂xᵢ∂xⱼ, symmetric, via second-order forward mode. |
ADTape
The append-only record of every operation performed on the AD values that belong to it.
| Declaration | Summary |
|---|---|
val size: Int |
The number of operations recorded so far — the tape's length, useful for budgeting. |
fun variable(value: Double): AD |
A tracked scalar holding value with no parents — a leaf of the tape. |
fun constant(value: Double): AD |
A tracked scalar holding value that is treated as a leaf — identical in mechanics to variable, named differently to say "I do not intend to differentiate this". |
fun variables(values: DoubleArray): List<AD> |
variable for a whole vector, in order — the usual way to start a gradient. |
fun backward(output: AD): DoubleArray |
Sweeps backwards from output, returning the adjoint (∂output/∂node) of every node on the tape, indexed by AD.index. |
AD
class AD internal constructor(val tape: ADTape, internal val index: Int) : Comparable<AD>
A scalar that remembers how it was computed: a value plus its position on an ADTape.
| Declaration | Summary |
|---|---|
val tape: ADTape |
The tape this value was recorded on. Two ADs can only be combined if they share one. |
val value: Double |
The plain numeric value, with no derivative information. |
operator fun plus(other: AD): AD |
Sum. |
operator fun plus(other: Double): AD |
Sum with a constant — the constant contributes no derivative. |
operator fun minus(other: AD): AD |
Difference. |
operator fun minus(other: Double): AD |
Difference with a constant. |
operator fun times(other: AD): AD |
Product; ∂(ab)/∂a = b. |
operator fun times(other: Double): AD |
Product with a constant, whose value becomes the local partial. |
operator fun div(other: AD): AD |
Quotient; ∂(a/b)/∂b = -a/b². |
operator fun div(other: Double): AD |
Quotient by a constant. |
operator fun unaryMinus(): AD |
Negation. |
operator fun unaryPlus(): AD |
Identity, so +x is legal and free of surprises. |
override fun compareTo(other: AD): Int |
Compares by value only — comparisons carry no derivative; see the control-flow caveat. |
operator fun compareTo(other: Double): Int |
Compares by value against a plain number. |
override fun toString(): String |
ADGradient
data class ADGradient(val value: Double, val gradient: DoubleArray)
A value together with its exact gradient — the result of valueAndGrad.
| Declaration | Summary |
|---|---|
val value: Double |
The objective's value at the point that was differentiated. |
val gradient: DoubleArray |
∂value/∂xᵢ, in the order the inputs were given. |
val gradientInfNorm: Double |
The largest absolute component of the gradient — the usual convergence test. |
override fun equals(other: Any?): Boolean |
|
override fun hashCode(): Int |
autodiff_forward.kt
Forward-mode automatic differentiation to second order — the other half of autodiff.kt.
A Dual2 is a truncated Taylor series (v, v', v'') carried along a single direction. Every
operation propagates all three components, so one evaluation of an expression in Dual2
arithmetic returns the value, the directional first derivative and the directional curvature at
once — no tape, no backward sweep, and no allocation beyond one three-Double object per node.
val t = taylor2(0.6) { x -> exp(-x * x) * sin(6.0 * x) }
t.value; t.first; t.second // f, f', f'' at 0.6, all exact
Forward or reverse
Reverse mode (grad) wins when there are many inputs and one output — a loss over a parameter
vector. Forward mode wins in the opposite shape (one input, many outputs: a parametric curve and
every quantity derived from it), and it is the only one of the two that reaches second order.
hessian in autodiff.kt is built on this file: vᵀHv is exactly Dual2.second after a pass
seeded along v.
The op vocabulary mirrors autodiff.kt name for name — sin, exp, tanh, max,
pow and friends are overloaded on both AD and Dual2, so the same expression body can be
written once against a shape and evaluated either way (see ADFunction2).
Conventions and gaps
- The kink convention matches
autodiff.kt:max/minsplit evenly at an exact tie, sorelu''is 0 everywhere andrelu'(0)is 0.5. powwith a tracked exponent is evaluated asexp(b · ln a)and therefore requires a strictly positive base; the constant-exponentDual2.powhas no such restriction.- Third and higher derivatives are not represented. A
Dual3would be a mechanical extension of the same chain rule; it is not here.
src/jvmMain/kotlin/afield/math/autodiff/autodiff_forward.kt — read the source
Used in: Differentiating Functions
Top-level functions
| Declaration | Summary |
|---|---|
operator fun Double.plus(other: Dual2): Dual2 |
2.0 + x. |
operator fun Double.minus(other: Dual2): Dual2 |
2.0 - x. |
operator fun Double.times(other: Dual2): Dual2 |
2.0 * x. |
operator fun Double.div(other: Dual2): Dual2 |
2.0 / x. |
operator fun Int.plus(other: Dual2): Dual2 |
2 + x for integer literals. |
operator fun Int.minus(other: Dual2): Dual2 |
2 - x for integer literals. |
operator fun Int.times(other: Dual2): Dual2 |
2 * x for integer literals. |
operator fun Int.div(other: Dual2): Dual2 |
2 / x for integer literals. |
fun exp(x: Dual2): Dual2 |
Natural exponential. |
fun ln(x: Dual2): Dual2 |
Natural logarithm. |
fun log10(x: Dual2): Dual2 |
Base-10 logarithm. |
fun log2(x: Dual2): Dual2 |
Base-2 logarithm. |
fun sqrt(x: Dual2): Dual2 |
Square root. |
fun cbrt(x: Dual2): Dual2 |
Cube root. Written out rather than delegated to Dual2.pow, because Math.pow is undefined for a negative base at a fractional exponent while the cube root is perfectly well defined there. |
fun sin(x: Dual2): Dual2 |
Sine of an angle in radians. |
fun cos(x: Dual2): Dual2 |
Cosine of an angle in radians. |
fun tan(x: Dual2): Dual2 |
Tangent. |
fun asin(x: Dual2): Dual2 |
Arc sine. |
fun acos(x: Dual2): Dual2 |
Arc cosine. |
fun atan(x: Dual2): Dual2 |
Arc tangent. |
fun sinh(x: Dual2): Dual2 |
Hyperbolic sine. |
fun cosh(x: Dual2): Dual2 |
Hyperbolic cosine. |
fun tanh(x: Dual2): Dual2 |
Hyperbolic tangent. |
fun abs(x: Dual2): Dual2 |
Absolute value; the derivative is 0 at exactly zero, and the curvature is 0 everywhere. |
fun relu(x: Dual2): Dual2 |
The rectifier max(x, 0); derivative 0.5 at exactly zero, curvature 0 everywhere. |
fun sigmoid(x: Dual2): Dual2 |
The logistic sigmoid 1/(1+e⁻ˣ); σ'' = σ(1-σ)(1-2σ). |
fun softplus(x: Dual2): Dual2 |
The smooth rectifier ln(1+eˣ); its derivative is the sigmoid and its curvature σ(1-σ). |
fun floor(x: Dual2): Dual2 |
Floor — piecewise constant, so both derivatives are 0. |
fun ceil(x: Dual2): Dual2 |
Ceiling — piecewise constant, so both derivatives are 0. |
fun round(x: Dual2): Dual2 |
Nearest integer — piecewise constant, so both derivatives are 0. |
fun pow(base: Dual2, exponent: Dual2): Dual2 |
a raised to the power b, both tracked, as exp(b · ln a); needs a strictly positive base. |
fun max(a: Dual2, b: Dual2): Dual2 |
The larger of two, selecting a branch; an exact tie averages both, matching the AD convention. |
fun max(a: Dual2, b: Double): Dual2 |
The larger of a tracked value and a constant. |
fun min(a: Dual2, b: Dual2): Dual2 |
The smaller of two, selecting a branch; an exact tie averages both. |
fun min(a: Dual2, b: Double): Dual2 |
The smaller of a tracked value and a constant. |
fun hypot(x: Dual2, y: Dual2): Dual2 |
√(x²+y²). |
fun atan2(y: Dual2, x: Dual2): Dual2 |
Two-argument arc tangent, correct in all four quadrants. |
fun sumDual(values: List<Dual2>): Dual2 |
The sum of a list. |
fun meanDual(values: List<Dual2>): Dual2 |
The arithmetic mean of a list. |
fun sumOfSquaresDual(values: List<Dual2>): Dual2 |
The sum of squares of a list. |
fun dotDual(a: List<Dual2>, b: List<Dual2>): Dual2 |
The dot product of two equal-length lists. |
fun normDual(values: List<Dual2>): Dual2 |
The Euclidean norm of a list. |
fun taylor2(x: Double, f: (Dual2) -> Dual2): Dual2 |
The value, first and second derivative of a single-variable function at x, in one pass. |
fun secondDerivative(x: Double, f: (Dual2) -> Dual2): Double |
The exact second derivative of a single-variable function at x. |
fun directionalDerivative(x: DoubleArray, direction: DoubleArray, f: ADFunction2): Dual2 |
The exact directional derivative ∇f(x)·v — one forward pass, whatever the dimension. |
fun gradForward(x: DoubleArray, f: ADFunction2): DoubleArray |
The gradient by forward mode — n passes, one per axis. |
Dual2
data class Dual2(val value: Double, val first: Double, val second: Double)
A second-order forward-mode number: a value together with its first and second derivative along one direction.
| Declaration | Summary |
|---|---|
val value: Double |
The plain value f. |
val first: Double |
The first derivative df/dt along the seeded direction. |
val second: Double |
The second derivative d²f/dt² along the seeded direction. |
operator fun plus(other: Dual2): Dual2 |
Sum, componentwise. |
operator fun plus(other: Double): Dual2 |
Sum with a constant, which shifts only the value. |
operator fun minus(other: Dual2): Dual2 |
Difference, componentwise. |
operator fun minus(other: Double): Dual2 |
Difference with a constant. |
operator fun times(other: Dual2): Dual2 |
Product; the second order picks up the 2·u'·w' cross term. |
operator fun times(other: Double): Dual2 |
Product with a constant, which scales all three orders. |
operator fun div(other: Dual2): Dual2 |
Quotient, as this * other.reciprocal(). |
operator fun div(other: Double): Dual2 |
Quotient by a constant. |
operator fun unaryMinus(): Dual2 |
Negation. |
operator fun unaryPlus(): Dual2 |
Identity. |
fun chain(fv: Double, d1: Double, d2: Double): Dual2 |
Applies an outer scalar function given its value and its first two derivatives at value — the second-order chain rule (f∘u)'' = f''·u'² + f'·u'', and the single place every unary op in this file is implemented. |
fun reciprocal(): Dual2 |
Reciprocal 1/x. |
fun sq(): Dual2 |
The square x². |
fun pow(exponent: Double): Dual2 |
x raised to a constant power, valid for negative value at integer exponents. |
fun pow(exponent: Int): Dual2 |
x raised to a constant integer power. |
fun clamp(lo: Double, hi: Double): Dual2 |
Clamps into [lo, hi], with the even-split tie convention on each boundary. |
override fun toString(): String |
ADFunction2
typealias ADFunction2 = (List<Dual2>) -> Dual2
A scalar function of a vector written once against Dual2, so it can be handed to both hessian and directionalDerivative without being restated.
autodiff_geometry.kt
Differentiable geometry: tracked vectors, planar inverse kinematics on an FLine chain, and a
differentiable circle packing — the part of the AD layer whose results are drawable.
ADVec2 and ADVec3 are pairs and triples of AD. They convert to and from Field's own
Vec2 / Vec3 (the generated, mutable, float-backed Vector2f / Vector3f), and every
operation on them is expressed in the same scalar ops as the rest of autodiff.kt. There are no
vector-specific derivative rules here; the point is that (joint - target).length() reads as
itself inside a loss instead of as unrolled components.
Inverse kinematics
solveIk finds the joint angles of a planar chain of fixed-length links by minimising
|endEffector - target|² with exact gradients. Forward kinematics is a cumulative sum of angles
and a running position — differentiable throughout — so L-BFGS reaches a sub-micron reach in tens
of iterations, and the result is an FLine you can draw.
val chain = IkChain(vec(120f, 300f), doubleArrayOf(90.0, 80.0, 70.0, 55.0))
val solved = solveIk(chain, vec(360f, 150f))
showPlane(listOf(chain.toFLine(solved.angles)))
A pure reach objective is under-determined for more than two links, so IkOptions.smoothness
offers a curvature penalty between consecutive links. It is off by default, and it is a trade
rather than a free regulariser: the two terms compete, so a positive smoothness buys a straighter
pose and charges reach accuracy for it. The charge is small out in the open and large near the
base, where reaching at all needs tight folds. On the four-link chain in the example above,
AutoDiffGeometryTest pins a target 216 units away as exact to under a thousandth of a unit
with no penalty and inside 12 units under a heavy one; a target close to the anchor is exact
without the penalty and more than a unit short at smoothness = 0.02.
IkSolution.reachError reports the shortfall either way, so a solve that traded accuracy away
can be told from one that converged.
Packing
packCircles minimises pairwise overlap and wall penetration under a gentle pull toward the
centre. Every term is a relu(...)² hinge, which is exactly where the even-split kink convention
in autodiff.kt matters: at the moment two circles touch, the penalty's gradient is half what a
naive one-sided rule would give, and the packing settles rather than chattering.
src/jvmMain/kotlin/afield/math/autodiff/autodiff_geometry.kt — read the source
Used in: Differentiating Functions
Top-level functions
| Declaration | Summary |
|---|---|
fun Vec2.onTape(tape: ADTape): ADVec2 |
This vector as an untracked constant on tape — the way a fixed target enters a loss. |
fun Vec3.onTape(tape: ADTape): ADVec3 |
This vector as an untracked constant on tape. |
fun solveIk(chain: IkChain, target: Vec2, initialAngles: DoubleArray = DoubleArray(chain.linkCount), options: IkOptions = IkOptions()): IkSolution |
Solves the planar inverse-kinematics problem: find joint angles putting chain's tip on target. |
fun packCircles(radii: DoubleArray, width: Double, height: Double, wallWeight: Double = 4.0, pullWeight: Double = 0.004, iterations: Int = 600, rngSeed: Long = 0L): PackingRun |
Packs discs of the given radii into the rectangle [0, width] × [0, height] by gradient descent on a differentiable overlap penalty. |
ADVec2
data class ADVec2(val x: AD, val y: AD)
A two-dimensional vector whose components are tracked AD scalars.
| Declaration | Summary |
|---|---|
val x: AD |
The x component. |
val y: AD |
The y component. |
operator fun plus(other: ADVec2): ADVec2 |
Sum. |
operator fun minus(other: ADVec2): ADVec2 |
Difference. |
operator fun times(other: ADVec2): ADVec2 |
Componentwise product — the Hadamard product, not a dot or a cross. |
operator fun times(scalar: AD): ADVec2 |
Scaling by a tracked scalar. |
operator fun times(scalar: Double): ADVec2 |
Scaling by a constant. |
operator fun div(scalar: Double): ADVec2 |
Division by a constant. |
operator fun unaryMinus(): ADVec2 |
Negation. |
infix fun dot(other: ADVec2): AD |
The dot product. |
infix fun cross(other: ADVec2): AD |
The 2D cross product x₁y₂ - y₁x₂ — the signed area of the parallelogram. |
fun lengthSquared(): AD |
The squared length, which is smooth at the origin where length is not. |
fun length(): AD |
The Euclidean length. Its derivative is undefined at the origin; prefer lengthSquared in a loss that might pass through zero. |
fun distanceTo(other: ADVec2): AD |
The distance to other. |
fun distanceSquaredTo(other: ADVec2): AD |
The squared distance to other — the well-behaved choice inside a loss. |
fun normalized(): ADVec2 |
This vector scaled to unit length. Undefined at the origin. |
fun angle(): AD |
The angle from the positive x axis, via atan2, so it is correct in all four quadrants. |
fun rotated(radians: AD): ADVec2 |
Rotated by radians about the origin. |
fun lerp(other: ADVec2, t: AD): ADVec2 |
Linear interpolation toward other by t. |
val value: Vec2 |
The plain, untracked value as one of Field's own Vec2s. |
override fun toString(): String |
ADVec3
data class ADVec3(val x: AD, val y: AD, val z: AD)
A three-dimensional vector whose components are tracked AD scalars — ADVec2's sibling, with the same immutability and the same conversion story to Field's Vec3.
| Declaration | Summary |
|---|---|
val x: AD |
The x component. |
val y: AD |
The y component. |
val z: AD |
The z component. |
operator fun plus(other: ADVec3): ADVec3 |
Sum. |
operator fun minus(other: ADVec3): ADVec3 |
Difference. |
operator fun times(scalar: AD): ADVec3 |
Scaling by a tracked scalar. |
operator fun times(scalar: Double): ADVec3 |
Scaling by a constant. |
operator fun unaryMinus(): ADVec3 |
Negation. |
infix fun dot(other: ADVec3): AD |
The dot product. |
infix fun cross(other: ADVec3): ADVec3 |
The cross product. |
fun lengthSquared(): AD |
The squared length, smooth at the origin. |
fun length(): AD |
The Euclidean length; undefined derivative at the origin. |
fun distanceTo(other: ADVec3): AD |
The distance to other. |
fun distanceSquaredTo(other: ADVec3): AD |
The squared distance to other. |
fun normalized(): ADVec3 |
This vector scaled to unit length. |
fun lerp(other: ADVec3, t: AD): ADVec3 |
Linear interpolation toward other by t. |
val value: Vec3 |
The plain, untracked value as one of Field's own Vec3s. |
override fun toString(): String |
IkChain
data class IkChain(val origin: Vec2, val lengths: DoubleArray)
A planar chain of rigid links: where it is anchored, and how long each link is.
| Declaration | Summary |
|---|---|
val origin: Vec2 |
Where the first link starts. |
val lengths: DoubleArray |
The link lengths, root to tip. |
val linkCount: Int |
How many links, and therefore how many joint angles. |
val reach: Double |
The furthest the tip can possibly get from origin — the chain fully extended. |
fun jointsAt(angles: DoubleArray): List<Vec2> |
The joint positions, origin first and the end effector last, for the configuration angles. |
fun endEffectorAt(angles: DoubleArray): Vec2 |
Where the tip lands for the configuration angles. |
fun toFLine(angles: DoubleArray): FLine |
The chain drawn as a polyline, root to tip — ready for showPlane or lines(...). |
fun endEffectorAD(angles: List<AD>): ADVec2 |
Forward kinematics in AD arithmetic: the tip position as a differentiable function of the tracked joint angles. The one function solveIk differentiates. |
override fun equals(other: Any?): Boolean |
|
override fun hashCode(): Int |
IkOptions
What shapes an solveIk solve.
IkSolution
An solveIk result: the configuration found, where it puts the chain, and the full descent.
| Declaration | Summary |
|---|---|
val angles: DoubleArray |
The solved joint angles, relative link to link. |
val joints: List<Vec2> |
The joint positions for angles, origin first, tip last. |
val endEffector: Vec2 |
Where the tip ended up. |
val reachError: Double |
The distance from endEffector to the requested target — the number that says "it reached". |
val run: OptimizeRun |
The optimizer's own record, including the full loss trajectory. |
fun toFLine(chain: IkChain): FLine |
The chain as a polyline for drawing. |
override fun equals(other: Any?): Boolean |
|
override fun hashCode(): Int |
PackedCircle
data class PackedCircle(val centre: Vec2, val radius: Double)
One packed disc: where it ended up and how big it is.
| Declaration | Summary |
|---|---|
val centre: Vec2 |
The centre. |
val radius: Double |
The radius, which packCircles does not change. |
fun toFLine(segments: Int = 48): FLine |
This disc as a closed polygonal FLine, for showPlane or lines(...). |
PackingRun
An packCircles result: the packing, its residual overlap, and the descent that produced it.
autodiff_optimize.kt
Gradient-descent optimizers that consume the exact gradients from autodiff.kt — sgd, adam
and lbfgs, behind one minimize front door.
All three are deterministic given their seed and all three record a full OptimizeRun.losses
trajectory which you can hand straight to showSignals("loss" to run.losses) and look at.
FnEvolveRun and FnFitRun carry a losses list of the same shape, so a fit, an evolve and an
optimisation all replay and plot the same way.
val run = minimize(doubleArrayOf(-1.2, 1.0), OptimizeOptions(method = OptimizeMethod.LBFGS)) { p ->
(1.0 - p[0]).sq() + 100.0 * (p[1] - p[0].sq()).sq() // Rosenbrock
}
run.x // ~[1, 1]
run.losses // the trajectory, ready to plot
Which one
lbfgsis the right default for a smooth deterministic objective with a handful to a few hundred parameters: it builds a curvature model from the lastmsteps and takes Newton-shaped strides, so it reaches machine-precision minima in tens of iterations where Adam is still crawling. It needs the objective to actually be smooth; a kinked or noisy loss will make its line search flail.adamis the robust workhorse for a rough, kinked or stochastic landscape, and the one that tolerates a badly scaled parameter vector without preconditioning.sgdwith momentum is here because it is sometimes what you want artistically — a slower, visibly settling trajectory with a tunable inertia, which reads as motion rather than as a solver converging.
The line search inside lbfgs is backtracking Armijo, which guarantees monotone descent but not
the curvature (Wolfe) condition; the L-BFGS memory therefore skips any update pair with
non-positive curvature rather than letting an indefinite model through.
src/jvmMain/kotlin/afield/math/autodiff/autodiff_optimize.kt — read the source
Used in: Differentiating Functions
Top-level functions
OptimizeMethod
Which descent to run. See the file KDoc for when to use each.
Constants: SGD, ADAM, LBFGS.
OptimizeOptions
Everything that shapes a minimize run, kept as one immutable value so a result can carry its own provenance.
| Declaration | Summary |
|---|---|
val method: OptimizeMethod |
Which optimizer to run. |
val iterations: Int |
Maximum number of iterations. |
val learningRate: Double |
Step size. Ignored by OptimizeMethod.LBFGS, whose line search chooses its own. |
val momentum: Double |
Momentum coefficient for OptimizeMethod.SGD. |
val nesterov: Boolean |
Use the Nesterov look-ahead form of momentum. |
val beta1: Double |
Adam's first-moment decay. |
val beta2: Double |
Adam's second-moment decay. |
val epsilon: Double |
Adam's denominator floor. |
val memory: Int |
How many (s, y) curvature pairs L-BFGS remembers. |
val armijo: Double |
Armijo sufficient-decrease constant for the L-BFGS line search. |
val maxLineSearchSteps: Int |
How many halvings the line search will try before giving up on an iteration. |
val gradientTolerance: Double |
Stop when the gradient's infinity norm falls to this. |
val stepTolerance: Double |
Stop when a step moves the parameters less than this. |
val initJitter: Double |
If > 0, jitter the start by ±initJitter·(1+\|x\|) using rngSeed before descending. |
val rngSeed: Long |
The single source of randomness; with initJitter = 0 the run is randomness-free anyway. |
val showProgress: Boolean |
Whether to offer a progress bar. Turn it off for an inner loop (a restart sweep, a demo). |
val recordTrajectory: Boolean |
Retain a copy of every parameter vector. Disable for large simulations; scalar history remains. |
OptimizeStep
data class OptimizeStep(val iteration: Int, val value: Double, val gradientInfNorm: Double)
One recorded iteration: the objective and the gradient's infinity norm after the step.
| Declaration | Summary |
|---|---|
val iteration: Int |
1-based iteration index; iteration 0 is the starting point and lives only in OptimizeRun.losses. |
val value: Double |
The objective value after this step. |
val gradientInfNorm: Double |
The gradient's infinity norm after this step — the convergence signal that counts. |
OptimizeRun
The result and provenance of a minimize run, shaped like FnFitRun so the two read the same way at a call site.
| Declaration | Summary |
|---|---|
val x: DoubleArray |
The parameters at the end of the run. |
val value: Double |
The objective at x. |
val gradient: DoubleArray |
The exact gradient at x. |
val initialX: DoubleArray |
The starting parameters, after any OptimizeOptions.initJitter. |
val initialValue: Double |
The objective at initialX. |
val steps: List<OptimizeStep> |
Per-iteration records. |
val losses: List<Double> |
The objective trajectory including the starting point. |
val trajectory: List<DoubleArray> |
The parameter trajectory, indexed exactly like losses: trajectory[i](workspace.md#stdlib-kt--i) is the point at which losses[i](workspace.md#stdlib-kt--i) was measured, so trajectory.first() is initialX and trajectory.last() is x. |
val iterations: Int |
How many iterations actually ran. |
val evaluations: Int |
How many times the objective (and its gradient) was evaluated. |
val converged: Boolean |
Whether a tolerance was met, as opposed to running out of iterations. |
val reason: String |
One of "observer", "gradient", "step", "line search", "max iterations", "no iterations". |
val options: OptimizeOptions |
The options this run was made with. |
val improved: Boolean |
True when the run ended strictly below where it started. |
val gradientInfNorm: Double |
The gradient's infinity norm at x. |
override fun equals(other: Any?): Boolean |
Two runs are equal when x, value, losses, iterations, evaluations, converged, reason and options all match. trajectory is left out: it is a List<DoubleArray> and DoubleArray.equals is identity, so including it would make any two separately-computed runs unequal whatever their contents. It is fully determined by initialX, options and the objective, and x and losses compare its endpoint and its whole value trace. |
override fun hashCode(): Int |
OptimizeProgress
An accepted optimizer state (including iteration zero), with line-search diagnostics. x and gradient are borrowed read-only arrays, valid only during the callback. Copy them only if retaining them. Rejected trial points are never reported.
| Declaration | Summary |
|---|---|
val gradient: DoubleArray, val evaluations: Int, val stepNorm: Double |
|
val lineSearchReductions: Int, val curvatureAccepted: Boolean?) |
pf.kt
An unscented Kalman filter, and a particle filter whose particles each carry one.
UnscentedKalmanFilter keeps a mean and covariance and takes the transition and observation
models as lambdas; UnscentedKalmanFilter.updateWithLikelihood returns the log measurement
likelihood, which is exactly what UKFParticleFilter accumulates into each particle's log
weight. A UKFParticle holds a UKF for the continuous state plus a hypothesis of your own
type H for the discrete or non-Gaussian part. UKFParticleFilter.resampleIfNeeded does
systematic resampling once the effective sample size drops below a fraction of the particle
count, and can immediately roughen the resampled means (RougheningOptions) so exact clones
separate again.
State and measurement dimensions are restricted to 1..3 — the private determinant helper only
covers those sizes and everything requires it. The linear algebra (Matrix is just
Array<DoubleArray>; Cholesky with escalating jitter, Gauss–Jordan inverse) is private to this
file, has no external dependency, and allocates fresh arrays on every step.
src/jvmMain/kotlin/afield/math/estimation/pf.kt — read the source
Used in: State estimation: tracking what you cannot measure
Top-level functions
| Declaration | Summary |
|---|---|
fun diag(vararg values: Double): Matrix |
A square Matrix with values down the diagonal and zeros elsewhere — the usual covariance. |
Matrix
typealias Matrix = Array<DoubleArray>
UKFOptions
The three sigma-point tuning constants of the unscented transform, plus the numerical fudge factor.
| Declaration | Summary |
|---|---|
val alpha: Double |
|
val beta: Double |
|
val kappa: Double |
|
val jitter: Double |
RougheningOptions
How hard to jiggle particles apart immediately after resampling — the cure for a filter that has collapsed, without saying so, onto a handful of identical clones.
UnscentedKalmanFilter
An unscented Kalman filter: it tracks a quantity you cannot measure directly, keeping a best guess x and an explicit account p of how unsure it is.
| Declaration | Summary |
|---|---|
val options: UKFOptions |
|
var x: DoubleArray |
The current best estimate of the state. Replaced wholesale by each predict and update. |
var p: Matrix |
The current state covariance — how unsure the filter is, and in which directions the uncertainty is correlated. Grows on predict, shrinks on update. |
val dim: Int |
The state dimension, fixed at construction and always in 1..3. |
fun predict(processNoise: Matrix, transition: (DoubleArray) -> DoubleArray) |
Advances the estimate one time step through transition, widening the covariance by processNoise. |
fun update(measurement: DoubleArray, measurementNoise: Matrix, observe: (DoubleArray) -> DoubleArray) |
Folds one measurement into the estimate, pulling x toward what was observed and shrinking p. |
fun updateWithLikelihood(measurement: DoubleArray, measurementNoise: Matrix, observe: (DoubleArray) -> DoubleArray): Double |
Performs a UKF measurement update and returns log p(measurement | predicted state). This likelihood is what the particle filter uses to update particle weights. |
fun roughenMean(noiseStdDev: DoubleArray, random: Random = Random.Default, inflateCovariance: Boolean = true) |
Perturbs the UKF mean and optionally inflates covariance by the same variance. |
fun stateCopy(): DoubleArray |
A snapshot of the current estimate that will not change when the filter next steps. |
fun covarianceCopy(): Matrix |
A snapshot of the current covariance, deep-copied row by row. |
UKFParticle
data class UKFParticle<H>(val ukf: UnscentedKalmanFilter, var hypothesis: H, var logWeight: Double)
One hypothesis in a UKFParticleFilter: a whole UnscentedKalmanFilter for the continuous part of the state, a value of your own type H for the discrete part, and how much the filter currently believes it.
| Declaration | Summary |
|---|---|
val ukf: UnscentedKalmanFilter |
|
var hypothesis: H |
|
var logWeight: Double |
UKFParticleFilter
A particle filter whose particles each carry an UnscentedKalmanFilter — for tracking something whose state is partly smooth and partly a discrete guess that might be wrong.
| Declaration | Summary |
|---|---|
var particles: MutableList<UKFParticle<H>> |
The live ensemble, in no meaningful order. Read it to summarise the estimate; the list itself is replaced by resampling, and the particles in it are mutated in place by every step. |
val size: Int |
How many particles the ensemble holds. Constant unless you resample, which preserves it. |
fun predict(processNoise: (UKFParticle<H>) -> Matrix, transition: (hypothesis: H, state: DoubleArray) -> DoubleArray, evolveHypothesis: ((H) -> H)? = null) |
Advances every particle one time step, each through its own hypothesis. |
fun update(measurement: DoubleArray, measurementNoise: (UKFParticle<H>) -> Matrix, observe: (hypothesis: H, state: DoubleArray) -> DoubleArray) |
Scores every particle against the same measurement: each one's own UKF absorbs it, and its log weight gains that particle's log likelihood — so hypotheses that predicted the measurement well gain ground on those that did not. |
fun weights(): DoubleArray |
The particles' weights as ordinary probabilities summing to 1, in the same order as particles — for taking a weighted mean of the ensemble, or for drawing it. |
fun effectiveSampleSize(): Double |
How many particles are effectively still contributing: 1 / Σw², which is the particle count when the weights are equal and approaches 1 as one particle takes over. |
fun resampleIfNeeded(thresholdFraction: Double = 0.5, random: Random = Random.Default, roughening: RougheningOptions? = null): Boolean |
Resamples only if the ensemble has degenerated — effectiveSampleSize below thresholdFraction of the particle count — and reports whether it did. |
fun resampleSystematic(random: Random = Random.Default, roughening: RougheningOptions? = null) |
Resamples unconditionally by systematic resampling: one uniform draw, then n evenly spaced strides through the cumulative weights, so a particle with weight w gets about n·w copies. |
fun roughenStateMeans(options: RougheningOptions = RougheningOptions(), random: Random = Random.Default) |
Roughens only the continuous UKF means. This is useful immediately after resampling, when many particles may be exact clones. |
fun roughenHypotheses(random: Random = Random.Default, roughen: (H, Random) -> H) |
Roughen non-UKF / discrete parameters, if your hypothesis type has mutable-like dynamics. |
fun estimateMean(): DoubleArray |
The filter's single best answer: every particle's state mean, averaged by weight. |
fun estimateCovariance(): Matrix |
The uncertainty of the whole ensemble: the weighted average of the particles' own covariances plus the scatter of their means about estimateMean. |
DenseDoubleOps.kt
SIMD helpers for dense DoubleArray arithmetic, using the JDK Vector API: DenseDoubleOps
provides dot, combine and scale.
src/jvmMain/kotlin/afield/math/functions/DenseDoubleOps.kt — read the source
DenseDoubleOps
Allocation-free SIMD operations on equal-length dense double arrays, with scalar tails. Reductions use lane-wise accumulation, so last-bit rounding can differ from a scalar sum. Destination arrays may alias either input for elementwise operations.
| Declaration | Summary |
|---|---|
fun dot(a: DoubleArray, b: DoubleArray): Double |
Sum of elementwise products; neither input is modified. |
fun combine(out: DoubleArray, a: DoubleArray, factor: Double, b: DoubleArray) |
Overwrites out with a + factor * b, including when out is one of the inputs. |
fun scale(a: DoubleArray, factor: Double) |
Multiplies every element of a by factor in place. |
Functions.kt
The Fn toolkit: one-dimensional signals as values you can build, compose, sample and draw.
Nearly everything lives inside the Functions object, and most of it as extension functions,
so callers normally work inside with(Functions) { ... }. A Functions.Fn is an evaluator
plus the Functions.Domain it is meant to be sampled over (default 0.0..1.0). Sources —
sin, saw, triangle, square, the pulseTrain / pulseBurst family, the noise and
fbm generators, and keyframe interpolants such as catmullRomSpline and monotoneCubic —
combine through arithmetic operators, through chainable Fn extensions (stretch, repeat,
fold, quantize, echo, convolve, blur, ...), and through reusable FnTransform
pipelines applied with /. Arrangement in time is timeline { } and the List<Fn>
combinators (sequence, randomPermute, morph, stack, average); those rearrange Fns
and timeline chunks and hand the result back to the same evaluator rather than introducing a
second one.
Every combinator also records an FnGraphNode recipe alongside the closure (see
FunctionGraph.kt), which is what makes graphs inspectable and rewritable rather than opaque
lambdas. On the output side: toDoubleArray and the sample* helpers for numbers, toFLine
for drawing, plotHtml for a self-contained plot, and Fn.plot, which displays into the
active Box and throws when there is none (Box.plot takes the Fn explicitly instead).
RbfKernel, RbfKernels and RbfInterpolator at the top of the file are the scattered-data
fitter behind Functions.rbf; it solves a dense n×n system per construction, so it is sized
for handfuls of control points, not thousands.
Its counterpart in two or more dimensions, RbfField in RbfField.kt, fits a surface through
values at scattered points with the same kernels.
src/jvmMain/kotlin/afield/math/functions/Functions.kt — read the source
Used in: Functions to FLine examples (Field graphics)
RbfKernel
A radial basis function: how strongly a control point still counts at distance r from the x being evaluated.
| Declaration | Summary |
|---|---|
fun eval(r: Double): Double |
The kernel's value at radius r. r is never negative; the result may be any sign. |
RbfKernels
The ready-made RbfKernel family behind Functions.rbf: four parameter-free kernels as values, and four shape-parameter kernels as factories.
| Declaration | Summary |
|---|---|
val linear |
r itself: interpolation degenerates to straight segments between neighbouring points. |
val quadratic |
r². Grows quickly with distance, so distant control points pull hard on the fit. |
val cubic |
r³. The stiffest of the polynomial kernels; smooth, but prone to overshoot far from data. |
val thinPlate |
The thin-plate spline kernel r²·ln r, with the removable singularity at r = 0 defined as zero. The default for Functions.rbf, and the one to reach for first: it needs no shape parameter, so it cannot be mis-scaled. Note that it is negative for r < 1. |
fun gaussian(epsilon: Double = 1.0) |
A Gaussian bump exp(-(εr)²) — the one genuinely local kernel here, so influence dies away within roughly 1/ε of each control point. |
fun multiquadric(c: Double = 1.0) |
sqrt(r² + c²) — a softened linear, rounded off near r = 0 over a width of about c. |
fun inverseMultiquadric(c: Double = 1.0) |
1 / sqrt(r² + c²) — decays with distance, so the fit stays local-ish; c sets the width. |
fun inverseQuadratic(c: Double = 1.0) |
1 / (r² + c²) — the fastest-decaying of the inverse kernels; c sets the width. |
RbfInterpolator
The scattered-data fitter behind Functions.rbf: given control points anywhere on the x axis, solve once for the kernel weights that reproduce them, then evaluate anywhere.
| Declaration | Summary |
|---|---|
fun eval(x: Double): Double |
The fitted value at x — the base function (if any) plus every control point's weighted kernel contribution. |
override fun toString(): String |
Functions
| Declaration | Summary |
|---|---|
public data class Domain(val min: Double, val max: Double) : ClosedRange<Double> |
The interval an Fn is meant to be sampled over: closed at both ends, possibly infinite, possibly empty. |
operator fun Double.rangeTo(end: Double): Domain |
Human-facing domain notation. |
operator fun Double.rangeTo(end: Number): Domain |
a..b as a Domain inside a Functions scope. The endpoints are sorted, so a descending range is not empty. |
operator fun Number.rangeTo(end: Double): Domain |
a..b as a Domain inside a Functions scope. The endpoints are sorted, so a descending range is not empty. |
fun domain(range: ClosedRange<*>): Domain |
Converts any numeric ClosedRange — including Kotlin's IntRange, which member operators would otherwise claim — into a Domain. Throws if the endpoints are not numbers. |
fun ClosedRange<*>.toDomain(): Domain |
This numeric range as a Domain, with the endpoints sorted. |
enum class EdgeMode |
What happens when a sampler asks for an x outside an Fn's domain. |
enum class Interpolation |
How a table of samples is reconstructed between its entries, for toFn and resample. |
enum class PulseEdge |
The shape of a pulse's rise and fall ramps in the pulseTrain family. |
enum class PulseProfile |
The envelope across the body of one pulse in the pulseTrain family, evaluated over the duty portion of each cycle. |
enum class StretchShape |
How time is paced when stretch maps one interval onto another: LINEAR at constant speed, SMOOTHSTEP and SMOOTHERSTEP easing in and out so the source is played slowly at the ends and quickly through the middle. |
enum class RectifyMode |
Rectifier polarity for the rectify waveshaping transform: half-wave clamps the negative excursion to zero; full-wave reflects it (|x|). |
enum class Easing |
Named motion curves for tween, Fn.ease, and other alpha-shaping helpers — the common normalized-time shapes for procedural motion and value tweening. |
val HARD: PulseEdge |
PulseEdge.HARD, re-exported so edge = HARD reads as DSL inside with(Functions). |
val SMOOTH: PulseEdge |
PulseEdge.SMOOTH, re-exported so edge = SMOOTH reads as DSL inside with(Functions). |
val EXP: PulseEdge |
PulseEdge.EXP, re-exported so edge = EXP reads as DSL inside with(Functions). |
val RECT: PulseProfile |
PulseProfile.RECT, re-exported so profile = RECT reads as DSL inside with(Functions). |
val TRIANGLE: PulseProfile |
PulseProfile.TRIANGLE, re-exported so profile = TRIANGLE reads as DSL — note that this is the pulse profile, not the triangle oscillator that shares the word. |
val HANN: PulseProfile |
PulseProfile.HANN, re-exported so profile = HANN reads as DSL — the pulse profile, distinct from the standalone hannWindow function. |
val GAUSSIAN: PulseProfile |
PulseProfile.GAUSSIAN, re-exported so profile = GAUSSIAN reads as DSL — the pulse profile, distinct from the standalone gaussian bump function. |
val NEAREST: Interpolation |
Interpolation.NEAREST, re-exported so interpolation = NEAREST reads as DSL. |
val CUBIC: Interpolation |
Interpolation.CUBIC, re-exported so interpolation = CUBIC reads as DSL. |
data class Fn(val domain: Domain = Domain.DEFAULT, val name: String? = null, val graph: FnGraphNode? = null, private val evalFn: (Double) -> Double) |
A one-dimensional signal together with the interval on which it is meant to be sampled. |
fun fn(domain: Domain = Domain.DEFAULT, name: String? = null, eval: (Double) -> Double): Fn |
Wraps an arbitrary Kotlin lambda as an Fn — the escape hatch for anything the library's own combinators cannot express. |
fun fn(domain: ClosedRange<*>, name: String? = null, eval: (Double) -> Double): Fn |
Wraps an arbitrary Kotlin lambda as an Fn over the given numeric range — see fn; the node is opaque to graph rewriting. |
fun c(value: Number, domain: Domain = Domain.DEFAULT): Fn |
A constant function: value everywhere, over domain (0.0..1.0 by default). |
fun c(value: Number, domain: ClosedRange<*>): Fn |
A constant function: value everywhere, over the given numeric range. See c. |
val Number.c: Fn |
This number as a parameter signal: a constant over Domain.ALL, so using it to configure a function will not shrink that function's domain. |
fun constant(value: Number, domain: Domain = Domain.DEFAULT): Fn+1 overload · also ClosedRange<*> |
A constant function: value everywhere. A spelled-out alias of c, for code that would rather read as prose. |
fun zero(domain: Domain = Domain.DEFAULT): Fn |
The constant zero over domain — the additive identity, and what the list combinators return for an empty list. |
fun zero(domain: ClosedRange<*>): Fn |
The constant zero over the given numeric range. |
fun one(domain: Domain = Domain.DEFAULT): Fn |
The constant one over domain — the multiplicative identity, handy as the 1 in expressions like one(d) - f. |
fun one(domain: ClosedRange<*>): Fn |
The constant one over the given numeric range. |
fun identity(domain: Domain = Domain.DEFAULT): Fn+1 overload · also ClosedRange<*> |
f(x) = x: the raw coordinate as a signal, named x in graphs and plots. |
fun ramp(start: Fn = param(0.0), end: Fn = param(1.0), domain: Domain = Domain.DEFAULT): Fn |
A value ramp: a straight line whose value goes from start to end as x sweeps the function's domain. ramp(1.0, 6.0) over (0.0..1.0) is 1 at x=0 and 6 at x=1; ramp(0.0, 1.0) is the identity over the default 0..1 domain. This is the linear sibling of tween (which eases the same endpoints). An unbounded domain is previewed over a finite window via Domain.finiteSweep, so a ramp on Domain.ALL stays finite to enumerate. |
fun ramp(start: Fn = param(0.0), end: Fn = param(1.0), domain: ClosedRange<*>): Fn |
A value ramp: a straight line whose value travels from start to end as x sweeps the domain. Endpoints may be plain numbers or signals; the Any overloads throw for anything that is neither. For the opposite reading — normalizing an x-window onto 0..1 — use remap. |
fun ramp(start: Number, end: Number, domain: Domain = Domain.DEFAULT): Fn |
A value ramp: a straight line whose value travels from start to end as x sweeps the domain. Endpoints may be plain numbers or signals; the Any overloads throw for anything that is neither. For the opposite reading — normalizing an x-window onto 0..1 — use remap. |
fun ramp(start: Number, end: Number, domain: ClosedRange<*>): Fn |
A value ramp: a straight line whose value travels from start to end as x sweeps the domain. Endpoints may be plain numbers or signals; the Any overloads throw for anything that is neither. For the opposite reading — normalizing an x-window onto 0..1 — use remap. |
fun ramp(start: Any, end: Any, domain: Domain = Domain.DEFAULT): Fn |
A value ramp: a straight line whose value travels from start to end as x sweeps the domain. Endpoints may be plain numbers or signals; the Any overloads throw for anything that is neither. For the opposite reading — normalizing an x-window onto 0..1 — use remap. |
fun ramp(start: Any, end: Any, domain: ClosedRange<*>): Fn |
A value ramp: a straight line whose value travels from start to end as x sweeps the domain. Endpoints may be plain numbers or signals; the Any overloads throw for anything that is neither. For the opposite reading — normalizing an x-window onto 0..1 — use remap. |
fun remap(from: Fn = param(0.0), to: Fn = param(1.0), domain: Domain = Domain.DEFAULT): Fn |
A domain normalizer: maps the x-window [from, to] onto 0..1, i.e. (x - from) / (to - from). remap(0.0, 10.0) turns an x in 0..10 into a 0..1 alpha; remap(0.0, 1.0) is the identity. This is the position-of-x reading — distinct from ramp, which produces a value that travels from one number to another across the domain. |
fun remap(from: Fn = param(0.0), to: Fn = param(1.0), domain: ClosedRange<*>): Fn |
A domain normalizer: (x - from) / (to - from), so an x in the window from..to reads out as a 0..1 alpha. Not clamped — x outside the window gives an alpha outside 0..1 — and a zero-width window answers 0.0 rather than dividing by zero. The value-travelling counterpart is ramp. |
fun remap(from: Number, to: Number, domain: Domain = Domain.DEFAULT): Fn |
A domain normalizer: (x - from) / (to - from), so an x in the window from..to reads out as a 0..1 alpha. Not clamped — x outside the window gives an alpha outside 0..1 — and a zero-width window answers 0.0 rather than dividing by zero. The value-travelling counterpart is ramp. |
fun remap(from: Number, to: Number, domain: ClosedRange<*>): Fn |
A domain normalizer: (x - from) / (to - from), so an x in the window from..to reads out as a 0..1 alpha. Not clamped — x outside the window gives an alpha outside 0..1 — and a zero-width window answers 0.0 rather than dividing by zero. The value-travelling counterpart is ramp. |
fun remap(from: Any, to: Any, domain: Domain = Domain.DEFAULT): Fn |
A domain normalizer: (x - from) / (to - from), so an x in the window from..to reads out as a 0..1 alpha. Not clamped — x outside the window gives an alpha outside 0..1 — and a zero-width window answers 0.0 rather than dividing by zero. The value-travelling counterpart is ramp. |
fun remap(from: Any, to: Any, domain: ClosedRange<*>): Fn |
A domain normalizer: (x - from) / (to - from), so an x in the window from..to reads out as a 0..1 alpha. Not clamped — x outside the window gives an alpha outside 0..1 — and a zero-width window answers 0.0 rather than dividing by zero. The value-travelling counterpart is ramp. |
fun step(edge: Fn = param(0.0), domain: Domain = Domain.DEFAULT): Fn |
A hard step: 0 below edge, 1 from edge onwards — the Heaviside function, inclusive at the threshold itself. |
fun step(edge: Fn = param(0.0), domain: ClosedRange<*>): Fn |
A hard step: 0 below edge, 1 from edge onwards, the threshold itself included. The soft version is smoothStep. |
fun step(edge: Number, domain: Domain = Domain.DEFAULT): Fn |
A hard step: 0 below edge, 1 from edge onwards, the threshold itself included. The soft version is smoothStep. |
fun step(edge: Number, domain: ClosedRange<*>): Fn |
A hard step: 0 below edge, 1 from edge onwards, the threshold itself included. The soft version is smoothStep. |
fun smoothStep(edge0: Fn = param(0.0), edge1: Fn = param(1.0), domain: Domain = Domain.DEFAULT): Fn |
A soft step in x: 0 up to edge0, 1 from edge1 on, and the classic 3t² - 2t³ ease between them. |
fun smoothStep(edge0: Fn = param(0.0), edge1: Fn = param(1.0), domain: ClosedRange<*>): Fn |
A soft step in x: 0 up to edge0, 1 from edge1 on, 3t² - 2t³ between. The edges are sorted, so their order does not matter; equal edges give a hard step. |
fun smoothStep(edge0: Number, edge1: Number, domain: Domain = Domain.DEFAULT): Fn |
A soft step in x: 0 up to edge0, 1 from edge1 on, 3t² - 2t³ between. The edges are sorted, so their order does not matter; equal edges give a hard step. |
fun smoothStep(edge0: Number, edge1: Number, domain: ClosedRange<*>): Fn |
A soft step in x: 0 up to edge0, 1 from edge1 on, 3t² - 2t³ between. The edges are sorted, so their order does not matter; equal edges give a hard step. |
fun smoothStep(edge0: Any, edge1: Any, domain: Domain = Domain.DEFAULT): Fn |
A soft step in x: 0 up to edge0, 1 from edge1 on, 3t² - 2t³ between. The edges are sorted, so their order does not matter; equal edges give a hard step. |
fun smoothStep(edge0: Any, edge1: Any, domain: ClosedRange<*>): Fn |
A soft step in x: 0 up to edge0, 1 from edge1 on, 3t² - 2t³ between. The edges are sorted, so their order does not matter; equal edges give a hard step. |
fun tween(from: Fn = param(0.0), to: Fn = param(1.0), domain: Domain = Domain.DEFAULT, easing: Easing = Easing.SMOOTHSTEP): Fn |
Interpolates from from to to across domain with a named motion curve. The endpoints may themselves be signals, which keeps tweening compositional. |
fun tween(from: Number, to: Number, domain: Domain = Domain.DEFAULT, easing: Easing = Easing.SMOOTHSTEP): Fn |
Interpolates from from to to across the domain with a named Easing — the eased sibling of ramp, and the usual way to write a piece of procedural motion. An unbounded domain is previewed over a finite window, so the alpha stays meaningful. |
fun tween(from: Number, to: Number, domain: ClosedRange<*>, easing: Easing = Easing.SMOOTHSTEP): Fn |
Interpolates from from to to across the domain with a named Easing — the eased sibling of ramp, and the usual way to write a piece of procedural motion. An unbounded domain is previewed over a finite window, so the alpha stays meaningful. |
fun Fn.ease(easing: Easing = Easing.SMOOTHSTEP, clampInput: Boolean = true): Fn |
Shapes a normalized signal through easing, optionally clamping it to 0..1 first. |
fun sin(frequency: Fn = param(1.0), phase: Fn = param(0.0), amplitude: Fn = param(1.0), offset: Fn = param(0.0), domain: Domain = Domain.ALL): Fn |
A sine oscillator: offset + amplitude · sin(2π · frequency · x + phase). |
fun sin(frequency: Fn = param(1.0), phase: Fn = param(0.0), amplitude: Fn = param(1.0), offset: Fn = param(0.0), domain: ClosedRange<*>): Fn |
A sine oscillator, offset + amplitude · sin(2π · frequency · x + phase): frequency in cycles per unit x, phase in radians. Any parameter may be a signal. |
fun sin(frequency: Number, phase: Number = 0.0, amplitude: Number = 1.0, offset: Number = 0.0, domain: Domain = Domain.ALL): Fn |
|
fun sin(frequency: Number, phase: Number = 0.0, amplitude: Number = 1.0, offset: Number = 0.0, domain: ClosedRange<*>): Fn |
|
fun sin(frequency: Any, phase: Any = 0.0, amplitude: Any = 1.0, offset: Any = 0.0, domain: Domain = Domain.ALL): Fn |
|
fun sin(frequency: Any, phase: Any = 0.0, amplitude: Any = 1.0, offset: Any = 0.0, domain: ClosedRange<*>): Fn |
|
fun cos(frequency: Fn = param(1.0), phase: Fn = param(0.0), amplitude: Fn = param(1.0), offset: Fn = param(0.0), domain: Domain = Domain.ALL): Fn |
A cosine oscillator: offset + amplitude · cos(2π · frequency · x + phase) — sin shifted a quarter turn, so it starts at its peak rather than at zero. |
fun cos(frequency: Fn = param(1.0), phase: Fn = param(0.0), amplitude: Fn = param(1.0), offset: Fn = param(0.0), domain: ClosedRange<*>): Fn |
A cosine oscillator, offset + amplitude · cos(2π · frequency · x + phase): frequency in cycles per unit x, phase in radians. Any parameter may be a signal. |
fun cos(frequency: Number, phase: Number = 0.0, amplitude: Number = 1.0, offset: Number = 0.0, domain: Domain = Domain.ALL): Fn |
|
fun cos(frequency: Number, phase: Number = 0.0, amplitude: Number = 1.0, offset: Number = 0.0, domain: ClosedRange<*>): Fn |
|
fun cos(frequency: Any, phase: Any = 0.0, amplitude: Any = 1.0, offset: Any = 0.0, domain: Domain = Domain.ALL): Fn |
|
fun cos(frequency: Any, phase: Any = 0.0, amplitude: Any = 1.0, offset: Any = 0.0, domain: ClosedRange<*>): Fn |
|
fun saw(period: Fn = param(1.0), phase: Fn = param(0.0), amplitude: Fn = param(1.0), offset: Fn = param(0.0), domain: Domain = Domain.ALL): Fn |
A rising sawtooth: a straight ramp from -amplitude to +amplitude across each period, then an instantaneous jump back down. |
fun saw(period: Fn = param(1.0), phase: Fn = param(0.0), amplitude: Fn = param(1.0), offset: Fn = param(0.0), domain: ClosedRange<*>): Fn |
A rising sawtooth from -amplitude to +amplitude across each period, then a jump back. period is a width in x units and phase an offset in those units, not radians. |
fun saw(period: Number, phase: Number = 0.0, amplitude: Number = 1.0, offset: Number = 0.0, domain: Domain = Domain.ALL): Fn |
|
fun saw(period: Number, phase: Number = 0.0, amplitude: Number = 1.0, offset: Number = 0.0, domain: ClosedRange<*>): Fn |
|
fun saw(period: Any, phase: Any = 0.0, amplitude: Any = 1.0, offset: Any = 0.0, domain: Domain = Domain.ALL): Fn |
|
fun saw(period: Any, phase: Any = 0.0, amplitude: Any = 1.0, offset: Any = 0.0, domain: ClosedRange<*>): Fn |
|
fun triangle(period: Fn = param(1.0), phase: Fn = param(0.0), amplitude: Fn = param(1.0), offset: Fn = param(0.0), domain: Domain = Domain.ALL): Fn |
A triangle wave: linear up from -amplitude to +amplitude over the first half of each period, linear back down over the second — continuous everywhere, unlike saw. |
fun triangle(period: Fn = param(1.0), phase: Fn = param(0.0), amplitude: Fn = param(1.0), offset: Fn = param(0.0), domain: ClosedRange<*>): Fn |
A triangle wave: linear up then linear down across each period, continuous throughout. period is a width in x units and phase an offset in those units, not radians. |
fun triangle(period: Number, phase: Number = 0.0, amplitude: Number = 1.0, offset: Number = 0.0, domain: Domain = Domain.ALL): Fn |
|
fun triangle(period: Number, phase: Number = 0.0, amplitude: Number = 1.0, offset: Number = 0.0, domain: ClosedRange<*>): Fn |
|
fun triangle(period: Any, phase: Any = 0.0, amplitude: Any = 1.0, offset: Any = 0.0, domain: Domain = Domain.ALL): Fn |
|
fun triangle(period: Any, phase: Any = 0.0, amplitude: Any = 1.0, offset: Any = 0.0, domain: ClosedRange<*>): Fn |
|
fun square(period: Fn = param(1.0), phase: Fn = param(0.0), duty: Fn = param(0.5), amplitude: Fn = param(1.0), offset: Fn = param(0.0), domain: Domain = Domain.ALL): Fn |
A square wave: +amplitude for the first duty fraction of each period and -amplitude for the rest. |
fun square(period: Fn = param(1.0), phase: Fn = param(0.0), duty: Fn = param(0.5), amplitude: Fn = param(1.0), offset: Fn = param(0.0), domain: ClosedRange<*>): Fn |
A bipolar square wave: +amplitude for the first duty fraction of each period, -amplitude for the rest. period and phase are in x units; duty is clamped to 0..1. |
fun square(period: Number, phase: Number = 0.0, duty: Number = 0.5, amplitude: Number = 1.0, offset: Number = 0.0, domain: Domain = Domain.ALL): Fn |
|
fun square(period: Number, phase: Number = 0.0, duty: Number = 0.5, amplitude: Number = 1.0, offset: Number = 0.0, domain: ClosedRange<*>): Fn |
|
fun square(period: Any, phase: Any = 0.0, duty: Any = 0.5, amplitude: Any = 1.0, offset: Any = 0.0, domain: Domain = Domain.ALL): Fn |
|
fun square(period: Any, phase: Any = 0.0, duty: Any = 0.5, amplitude: Any = 1.0, offset: Any = 0.0, domain: ClosedRange<*>): Fn |
|
fun pulseTrain(period: Fn = param(1.0), phase: Fn = param(0.0), duty: Fn = param(0.5), amplitude: Fn = param(1.0), offset: Fn = param(0.0), rise: Fn = param(0.0), fall: Fn = param(0.0), edge: PulseEdge = PulseEdge.SMOOTH, curve: Fn = param(2.0), profile: PulseProfile = PulseProfile.RECT, profileSigma: Fn = param(0.15), jitter: Fn = param(0.0), seed: Long = 0L, bipolar: Boolean = false, dutyMod: Fn = param(0.0), ampMod: Fn = param(1.0), phaseMod: Fn = param(0.0), domain: Domain = Domain.ALL): Fn |
The general pulse generator: one shaped pulse per period, with control over width, edges, body profile and per-cycle jitter. Where square gives you a bipolar two-level wave, this gives you an envelope you can actually design. |
fun pulseTrain(period: Fn = param(1.0), phase: Fn = param(0.0), duty: Fn = param(0.5), amplitude: Fn = param(1.0), offset: Fn = param(0.0), rise: Fn = param(0.0), fall: Fn = param(0.0), edge: PulseEdge = PulseEdge.SMOOTH, curve: Fn = param(2.0), profile: PulseProfile = PulseProfile.RECT, profileSigma: Fn = param(0.15), jitter: Fn = param(0.0), seed: Long = 0L, bipolar: Boolean = false, dutyMod: Fn = param(0.0), ampMod: Fn = param(1.0), phaseMod: Fn = param(0.0), domain: ClosedRange<*>): Fn |
The general pulse generator: one shaped pulse per period, unipolar from offset to offset + amplitude unless bipolar is set. period, phase, rise and fall are all in x units; duty is a fraction clamped to 0..1. See the primary overload for the full conventions. |
fun pulseTrain(period: Number, phase: Number = 0.0, duty: Number = 0.5, amplitude: Number = 1.0, offset: Number = 0.0, rise: Number = 0.0, fall: Number = 0.0, edge: PulseEdge = PulseEdge.SMOOTH, curve: Number = 2.0, profile: PulseProfile = PulseProfile.RECT, profileSigma: Number = 0.15, jitter: Number = 0.0, seed: Long = 0L, bipolar: Boolean = false, dutyMod: Number = 0.0, ampMod: Number = 1.0, phaseMod: Number = 0.0, domain: Domain = Domain.ALL): Fn |
The general pulse generator: one shaped pulse per period, unipolar from offset to offset + amplitude unless bipolar is set. period, phase, rise and fall are all in x units; duty is a fraction clamped to 0..1. See the primary overload for the full conventions. |
fun pulseTrain(period: Number, phase: Number = 0.0, duty: Number = 0.5, amplitude: Number = 1.0, offset: Number = 0.0, rise: Number = 0.0, fall: Number = 0.0, edge: PulseEdge = PulseEdge.SMOOTH, curve: Number = 2.0, profile: PulseProfile = PulseProfile.RECT, profileSigma: Number = 0.15, jitter: Number = 0.0, seed: Long = 0L, bipolar: Boolean = false, dutyMod: Number = 0.0, ampMod: Number = 1.0, phaseMod: Number = 0.0, domain: ClosedRange<*>): Fn |
The general pulse generator: one shaped pulse per period, unipolar from offset to offset + amplitude unless bipolar is set. period, phase, rise and fall are all in x units; duty is a fraction clamped to 0..1. See the primary overload for the full conventions. |
fun pulseTrain(period: Any, phase: Any = 0.0, duty: Any = 0.5, amplitude: Any = 1.0, offset: Any = 0.0, rise: Any = 0.0, fall: Any = 0.0, edge: PulseEdge = PulseEdge.SMOOTH, curve: Any = 2.0, profile: PulseProfile = PulseProfile.RECT, profileSigma: Any = 0.15, jitter: Any = 0.0, seed: Long = 0L, bipolar: Boolean = false, dutyMod: Any = 0.0, ampMod: Any = 1.0, phaseMod: Any = 0.0, domain: Domain = Domain.ALL): Fn |
The general pulse generator, taking each parameter as either a number or an Fn and throwing for anything else: one shaped pulse per period, unipolar from offset to offset + amplitude unless bipolar is set. period, phase, rise and fall are in x units; duty is a fraction clamped to 0..1. See the primary overload for the full conventions. |
fun pulseTrain(period: Any, phase: Any = 0.0, duty: Any = 0.5, amplitude: Any = 1.0, offset: Any = 0.0, rise: Any = 0.0, fall: Any = 0.0, edge: PulseEdge = PulseEdge.SMOOTH, curve: Any = 2.0, profile: PulseProfile = PulseProfile.RECT, profileSigma: Any = 0.15, jitter: Any = 0.0, seed: Long = 0L, bipolar: Boolean = false, dutyMod: Any = 0.0, ampMod: Any = 1.0, phaseMod: Any = 0.0, domain: ClosedRange<*>): Fn |
The general pulse generator, taking each parameter as either a number or an Fn and throwing for anything else: one shaped pulse per period, unipolar from offset to offset + amplitude unless bipolar is set. period, phase, rise and fall are in x units; duty is a fraction clamped to 0..1. See the primary overload for the full conventions. |
fun pulseTrainWidth(period: Fn = param(1.0), width: Fn = param(0.5), phase: Fn = param(0.0), amplitude: Fn = param(1.0), offset: Fn = param(0.0), rise: Fn = param(0.0), fall: Fn = param(0.0), edge: PulseEdge = PulseEdge.SMOOTH, curve: Fn = param(2.0), profile: PulseProfile = PulseProfile.RECT, profileSigma: Fn = param(0.15), jitter: Fn = param(0.0), seed: Long = 0L, bipolar: Boolean = false, dutyMod: Fn = param(0.0), ampMod: Fn = param(1.0), phaseMod: Fn = param(0.0), domain: Domain = Domain.ALL): Fn |
pulseTrain specified by absolute pulse width rather than duty fraction — the form you want when the pulse should keep its length while the period changes. |
fun pulseTrainWidth(period: Number, width: Number = 0.5, phase: Number = 0.0, amplitude: Number = 1.0, offset: Number = 0.0, rise: Number = 0.0, fall: Number = 0.0, edge: PulseEdge = PulseEdge.SMOOTH, curve: Number = 2.0, profile: PulseProfile = PulseProfile.RECT, profileSigma: Number = 0.15, jitter: Number = 0.0, seed: Long = 0L, bipolar: Boolean = false, dutyMod: Number = 0.0, ampMod: Number = 1.0, phaseMod: Number = 0.0, domain: Domain = Domain.ALL): Fn |
pulseTrain specified by absolute pulse width in x units instead of a duty fraction; the duty is derived as width / period, clamped to 0..1, so an over-wide pulse saturates rather than wrapping. See pulseTrain for the remaining conventions. |
fun pulseTrainWidth(period: Number, width: Number = 0.5, phase: Number = 0.0, amplitude: Number = 1.0, offset: Number = 0.0, rise: Number = 0.0, fall: Number = 0.0, edge: PulseEdge = PulseEdge.SMOOTH, curve: Number = 2.0, profile: PulseProfile = PulseProfile.RECT, profileSigma: Number = 0.15, jitter: Number = 0.0, seed: Long = 0L, bipolar: Boolean = false, dutyMod: Number = 0.0, ampMod: Number = 1.0, phaseMod: Number = 0.0, domain: ClosedRange<*>): Fn |
pulseTrain specified by absolute pulse width in x units instead of a duty fraction; the duty is derived as width / period, clamped to 0..1, so an over-wide pulse saturates rather than wrapping. See pulseTrain for the remaining conventions. |
fun pulseTrainWidth(period: Any, width: Any = 0.5, phase: Any = 0.0, amplitude: Any = 1.0, offset: Any = 0.0, rise: Any = 0.0, fall: Any = 0.0, edge: PulseEdge = PulseEdge.SMOOTH, curve: Any = 2.0, profile: PulseProfile = PulseProfile.RECT, profileSigma: Any = 0.15, jitter: Any = 0.0, seed: Long = 0L, bipolar: Boolean = false, dutyMod: Any = 0.0, ampMod: Any = 1.0, phaseMod: Any = 0.0, domain: Domain = Domain.ALL): Fn |
pulseTrain specified by absolute pulse width in x units instead of a duty fraction, taking each parameter as either a number or an Fn and throwing for anything else. The duty is derived as width / period, clamped to 0..1. See pulseTrain for the remaining conventions. |
fun pulseTrainWidth(period: Any, width: Any = 0.5, phase: Any = 0.0, amplitude: Any = 1.0, offset: Any = 0.0, rise: Any = 0.0, fall: Any = 0.0, edge: PulseEdge = PulseEdge.SMOOTH, curve: Any = 2.0, profile: PulseProfile = PulseProfile.RECT, profileSigma: Any = 0.15, jitter: Any = 0.0, seed: Long = 0L, bipolar: Boolean = false, dutyMod: Any = 0.0, ampMod: Any = 1.0, phaseMod: Any = 0.0, domain: ClosedRange<*>): Fn |
pulseTrain specified by absolute pulse width in x units instead of a duty fraction, taking each parameter as either a number or an Fn and throwing for anything else. The duty is derived as width / period, clamped to 0..1. See pulseTrain for the remaining conventions. |
fun pulseTrainHz(frequency: Fn = param(1.0), phase: Fn = param(0.0), duty: Fn = param(0.5), amplitude: Fn = param(1.0), offset: Fn = param(0.0), rise: Fn = param(0.0), fall: Fn = param(0.0), edge: PulseEdge = PulseEdge.SMOOTH, curve: Fn = param(2.0), profile: PulseProfile = PulseProfile.RECT, profileSigma: Fn = param(0.15), jitter: Fn = param(0.0), seed: Long = 0L, bipolar: Boolean = false, dutyMod: Fn = param(0.0), ampMod: Fn = param(1.0), phaseMod: Fn = param(0.0), domain: Domain = Domain.ALL): Fn |
pulseTrain driven by frequency instead of period, for when repetition rate is the natural way to think about it. |
fun pulseTrainHz(frequency: Number, phase: Number = 0.0, duty: Number = 0.5, amplitude: Number = 1.0, offset: Number = 0.0, rise: Number = 0.0, fall: Number = 0.0, edge: PulseEdge = PulseEdge.SMOOTH, curve: Number = 2.0, profile: PulseProfile = PulseProfile.RECT, profileSigma: Number = 0.15, jitter: Number = 0.0, seed: Long = 0L, bipolar: Boolean = false, dutyMod: Number = 0.0, ampMod: Number = 1.0, phaseMod: Number = 0.0, domain: Domain = Domain.ALL): Fn |
pulseTrain driven by frequency in cycles per unit x rather than by period; the period is derived as 1 / frequency, and a frequency of zero degenerates to just offset. See pulseTrain for the remaining conventions. |
fun pulseTrainHz(frequency: Number, phase: Number = 0.0, duty: Number = 0.5, amplitude: Number = 1.0, offset: Number = 0.0, rise: Number = 0.0, fall: Number = 0.0, edge: PulseEdge = PulseEdge.SMOOTH, curve: Number = 2.0, profile: PulseProfile = PulseProfile.RECT, profileSigma: Number = 0.15, jitter: Number = 0.0, seed: Long = 0L, bipolar: Boolean = false, dutyMod: Number = 0.0, ampMod: Number = 1.0, phaseMod: Number = 0.0, domain: ClosedRange<*>): Fn |
pulseTrain driven by frequency in cycles per unit x rather than by period; the period is derived as 1 / frequency, and a frequency of zero degenerates to just offset. See pulseTrain for the remaining conventions. |
fun pulseTrainHz(frequency: Any, phase: Any = 0.0, duty: Any = 0.5, amplitude: Any = 1.0, offset: Any = 0.0, rise: Any = 0.0, fall: Any = 0.0, edge: PulseEdge = PulseEdge.SMOOTH, curve: Any = 2.0, profile: PulseProfile = PulseProfile.RECT, profileSigma: Any = 0.15, jitter: Any = 0.0, seed: Long = 0L, bipolar: Boolean = false, dutyMod: Any = 0.0, ampMod: Any = 1.0, phaseMod: Any = 0.0, domain: Domain = Domain.ALL): Fn |
pulseTrain driven by frequency in cycles per unit x rather than by period, taking each parameter as either a number or an Fn and throwing for anything else. The period is derived as 1 / frequency, and a frequency of zero degenerates to just offset. |
fun pulseTrainHz(frequency: Any, phase: Any = 0.0, duty: Any = 0.5, amplitude: Any = 1.0, offset: Any = 0.0, rise: Any = 0.0, fall: Any = 0.0, edge: PulseEdge = PulseEdge.SMOOTH, curve: Any = 2.0, profile: PulseProfile = PulseProfile.RECT, profileSigma: Any = 0.15, jitter: Any = 0.0, seed: Long = 0L, bipolar: Boolean = false, dutyMod: Any = 0.0, ampMod: Any = 1.0, phaseMod: Any = 0.0, domain: ClosedRange<*>): Fn |
pulseTrain driven by frequency in cycles per unit x rather than by period, taking each parameter as either a number or an Fn and throwing for anything else. The period is derived as 1 / frequency, and a frequency of zero degenerates to just offset. |
fun pulseTrainPattern(pattern: DoubleArray, period: Fn = param(1.0), phase: Fn = param(0.0), duty: Fn = param(0.5), amplitude: Fn = param(1.0), offset: Fn = param(0.0), rise: Fn = param(0.0), fall: Fn = param(0.0), edge: PulseEdge = PulseEdge.SMOOTH, curve: Fn = param(2.0), profile: PulseProfile = PulseProfile.RECT, profileSigma: Fn = param(0.15), jitter: Fn = param(0.0), seed: Long = 0L, bipolar: Boolean = false, dutyMod: Fn = param(0.0), ampMod: Fn = param(1.0), phaseMod: Fn = param(0.0), domain: Domain = Domain.ALL): Fn |
pulseTrain with a repeating per-cycle amplitude pattern — an accent rhythm rather than an even train. |
fun pulseTrainPattern(pattern: DoubleArray, period: Number, phase: Number = 0.0, duty: Number = 0.5, amplitude: Number = 1.0, offset: Number = 0.0, rise: Number = 0.0, fall: Number = 0.0, edge: PulseEdge = PulseEdge.SMOOTH, curve: Number = 2.0, profile: PulseProfile = PulseProfile.RECT, profileSigma: Number = 0.15, jitter: Number = 0.0, seed: Long = 0L, bipolar: Boolean = false, dutyMod: Number = 0.0, ampMod: Number = 1.0, phaseMod: Number = 0.0, domain: Domain = Domain.ALL): Fn |
pulseTrain with a repeating per-cycle amplitude pattern: cycle n is scaled by pattern[n mod pattern.size]. An empty pattern returns the constant zero over the domain. See pulseTrain for the remaining conventions. |
fun pulseTrainPattern(pattern: DoubleArray, period: Number, phase: Number = 0.0, duty: Number = 0.5, amplitude: Number = 1.0, offset: Number = 0.0, rise: Number = 0.0, fall: Number = 0.0, edge: PulseEdge = PulseEdge.SMOOTH, curve: Number = 2.0, profile: PulseProfile = PulseProfile.RECT, profileSigma: Number = 0.15, jitter: Number = 0.0, seed: Long = 0L, bipolar: Boolean = false, dutyMod: Number = 0.0, ampMod: Number = 1.0, phaseMod: Number = 0.0, domain: ClosedRange<*>): Fn |
pulseTrain with a repeating per-cycle amplitude pattern: cycle n is scaled by pattern[n mod pattern.size]. An empty pattern returns the constant zero over the domain. See pulseTrain for the remaining conventions. |
fun pulseTrainPattern(pattern: DoubleArray, period: Any, phase: Any = 0.0, duty: Any = 0.5, amplitude: Any = 1.0, offset: Any = 0.0, rise: Any = 0.0, fall: Any = 0.0, edge: PulseEdge = PulseEdge.SMOOTH, curve: Any = 2.0, profile: PulseProfile = PulseProfile.RECT, profileSigma: Any = 0.15, jitter: Any = 0.0, seed: Long = 0L, bipolar: Boolean = false, dutyMod: Any = 0.0, ampMod: Any = 1.0, phaseMod: Any = 0.0, domain: Domain = Domain.ALL): Fn |
pulseTrain with a repeating per-cycle amplitude pattern, taking each parameter as either a number or an Fn and throwing for anything else: cycle n is scaled by pattern[n mod pattern.size], and an empty pattern returns the constant zero over the domain. |
fun pulseTrainPattern(pattern: DoubleArray, period: Any, phase: Any = 0.0, duty: Any = 0.5, amplitude: Any = 1.0, offset: Any = 0.0, rise: Any = 0.0, fall: Any = 0.0, edge: PulseEdge = PulseEdge.SMOOTH, curve: Any = 2.0, profile: PulseProfile = PulseProfile.RECT, profileSigma: Any = 0.15, jitter: Any = 0.0, seed: Long = 0L, bipolar: Boolean = false, dutyMod: Any = 0.0, ampMod: Any = 1.0, phaseMod: Any = 0.0, domain: ClosedRange<*>): Fn |
pulseTrain with a repeating per-cycle amplitude pattern, taking each parameter as either a number or an Fn and throwing for anything else: cycle n is scaled by pattern[n mod pattern.size], and an empty pattern returns the constant zero over the domain. |
fun impulseTrain(period: Fn = param(1.0), phase: Fn = param(0.0), width: Fn = param(0.02), amplitude: Fn = param(1.0), offset: Fn = param(0.0), sigma: Fn = param(0.15), jitter: Fn = param(0.0), seed: Long = 0L, domain: Domain = Domain.ALL): Fn |
A train of narrow Gaussian bumps — pulseTrainWidth pre-set to a GAUSSIAN profile with smooth edges, which is as close as a continuously-sampleable function gets to a Dirac comb. |
fun impulseTrain(period: Number, phase: Number = 0.0, width: Number = 0.02, amplitude: Number = 1.0, offset: Number = 0.0, sigma: Number = 0.15, jitter: Number = 0.0, seed: Long = 0L, domain: Domain = Domain.ALL): Fn |
A train of narrow Gaussian bumps, one width wide (in x units) every period — pulseTrainWidth pre-set to a GAUSSIAN profile. Unipolar, from offset to offset + amplitude; the bumps never quite reach zero between them. |
fun impulseTrain(period: Number, phase: Number = 0.0, width: Number = 0.02, amplitude: Number = 1.0, offset: Number = 0.0, sigma: Number = 0.15, jitter: Number = 0.0, seed: Long = 0L, domain: ClosedRange<*>): Fn |
A train of narrow Gaussian bumps, one width wide (in x units) every period — pulseTrainWidth pre-set to a GAUSSIAN profile. Unipolar, from offset to offset + amplitude; the bumps never quite reach zero between them. |
fun impulseTrain(period: Any, phase: Any = 0.0, width: Any = 0.02, amplitude: Any = 1.0, offset: Any = 0.0, sigma: Any = 0.15, jitter: Any = 0.0, seed: Long = 0L, domain: Domain = Domain.ALL): Fn |
A train of narrow Gaussian bumps, one width wide (in x units) every period, taking each parameter as either a number or an Fn and throwing for anything else. Unipolar, from offset to offset + amplitude. |
fun impulseTrain(period: Any, phase: Any = 0.0, width: Any = 0.02, amplitude: Any = 1.0, offset: Any = 0.0, sigma: Any = 0.15, jitter: Any = 0.0, seed: Long = 0L, domain: ClosedRange<*>): Fn |
A train of narrow Gaussian bumps, one width wide (in x units) every period, taking each parameter as either a number or an Fn and throwing for anything else. Unipolar, from offset to offset + amplitude. |
fun pulseBurst(count: Int, period: Fn = param(1.0), start: Fn = param(0.0), phase: Fn = param(0.0), duty: Fn = param(0.5), amplitude: Fn = param(1.0), offset: Fn = param(0.0), rise: Fn = param(0.0), fall: Fn = param(0.0), edge: PulseEdge = PulseEdge.SMOOTH, curve: Fn = param(2.0), profile: PulseProfile = PulseProfile.RECT, profileSigma: Fn = param(0.15), domain: Domain = Domain.ALL): Fn |
A finite burst of count pulses beginning at start — a pulseTrain gated to a window instead of running forever. |
fun pulseBurst(count: Int, period: Number, start: Number = 0.0, phase: Number = 0.0, duty: Number = 0.5, amplitude: Number = 1.0, offset: Number = 0.0, rise: Number = 0.0, fall: Number = 0.0, edge: PulseEdge = PulseEdge.SMOOTH, curve: Number = 2.0, profile: PulseProfile = PulseProfile.RECT, profileSigma: Number = 0.15, domain: Domain = Domain.ALL): Fn |
A finite burst of count pulses beginning at start, with offset everywhere outside the window start .. start + period * count. A count of zero or less gives the constant zero over the domain. See pulseTrain for the shaping conventions. |
fun pulseBurst(count: Int, period: Number, start: Number = 0.0, phase: Number = 0.0, duty: Number = 0.5, amplitude: Number = 1.0, offset: Number = 0.0, rise: Number = 0.0, fall: Number = 0.0, edge: PulseEdge = PulseEdge.SMOOTH, curve: Number = 2.0, profile: PulseProfile = PulseProfile.RECT, profileSigma: Number = 0.15, domain: ClosedRange<*>): Fn |
A finite burst of count pulses beginning at start, with offset everywhere outside the window start .. start + period * count. A count of zero or less gives the constant zero over the domain. See pulseTrain for the shaping conventions. |
fun pulseBurst(count: Int, period: Any, start: Any = 0.0, phase: Any = 0.0, duty: Any = 0.5, amplitude: Any = 1.0, offset: Any = 0.0, rise: Any = 0.0, fall: Any = 0.0, edge: PulseEdge = PulseEdge.SMOOTH, curve: Any = 2.0, profile: PulseProfile = PulseProfile.RECT, profileSigma: Any = 0.15, domain: Domain = Domain.ALL): Fn |
A finite burst of count pulses beginning at start, taking each parameter as either a number or an Fn and throwing for anything else. offset applies everywhere outside the window start .. start + period * count; a count of zero or less gives the constant zero. |
fun pulseBurst(count: Int, period: Any, start: Any = 0.0, phase: Any = 0.0, duty: Any = 0.5, amplitude: Any = 1.0, offset: Any = 0.0, rise: Any = 0.0, fall: Any = 0.0, edge: PulseEdge = PulseEdge.SMOOTH, curve: Any = 2.0, profile: PulseProfile = PulseProfile.RECT, profileSigma: Any = 0.15, domain: ClosedRange<*>): Fn |
A finite burst of count pulses beginning at start, taking each parameter as either a number or an Fn and throwing for anything else. offset applies everywhere outside the window start .. start + period * count; a count of zero or less gives the constant zero. |
fun gaussian(mean: Fn = param(0.0), sigma: Fn = param(1.0), amplitude: Fn = param(1.0), domain: Domain = Domain.DEFAULT): Fn |
A Gaussian bump: amplitude · exp(-½ ((x - mean) / sigma)²) — peak amplitude at mean, down to about 0.61 of the peak one sigma away, and never quite zero anywhere. |
fun gaussian(mean: Number, sigma: Number = 1.0, amplitude: Number = 1.0, domain: Domain = Domain.DEFAULT): Fn |
A Gaussian bump: amplitude · exp(-½ ((x - mean) / sigma)²), peaking at mean and never quite reaching zero. A sigma of zero gives the constant zero, and the domain defaults to 0.0..1.0 rather than the whole line. |
fun gaussian(mean: Number, sigma: Number = 1.0, amplitude: Number = 1.0, domain: ClosedRange<*>): Fn |
A Gaussian bump: amplitude · exp(-½ ((x - mean) / sigma)²), peaking at mean and never quite reaching zero. A sigma of zero gives the constant zero, and the domain defaults to 0.0..1.0 rather than the whole line. |
fun gaussian(mean: Any, sigma: Any = 1.0, amplitude: Any = 1.0, domain: Domain = Domain.DEFAULT): Fn |
A Gaussian bump peaking at mean, taking each parameter as either a number or an Fn and throwing for anything else. A sigma of zero gives the constant zero; the bump never quite reaches zero elsewhere. |
fun gaussian(mean: Any, sigma: Any = 1.0, amplitude: Any = 1.0, domain: ClosedRange<*>): Fn |
A Gaussian bump peaking at mean, taking each parameter as either a number or an Fn and throwing for anything else. A sigma of zero gives the constant zero; the bump never quite reaches zero elsewhere. |
fun DoubleArray.toFn(domain: Domain = Domain.of(0.0, (size - 1).toDouble()), interpolation: Interpolation = Interpolation.LINEAR, outside: Double = 0.0): Fn |
Turns a table of samples into a continuous Fn, reading between the entries with the chosen interpolation. |
fun DoubleArray.toFn(domain: ClosedRange<*>, interpolation: Interpolation = Interpolation.LINEAR, outside: Double = 0.0): Fn |
A table of samples as a continuous Fn, spread evenly across the given range and reading outside beyond it. See the Domain overload of toFn. |
fun List<Number>.toFn(domain: Domain = Domain.of(0.0, (size - 1).toDouble()), interpolation: Interpolation = Interpolation.LINEAR, outside: Double = 0.0): Fn |
A list of numbers as a continuous Fn, spread evenly across domain (by default 0.0 .. size - 1) and reading outside beyond it. Copies the list into a DoubleArray first, so later mutation of the list does not affect the function. |
fun List<Number>.toFn(domain: ClosedRange<*>, interpolation: Interpolation = Interpolation.LINEAR, outside: Double = 0.0): Fn |
A list of numbers as a continuous Fn, spread evenly across the given range and reading outside beyond it. Copies the list first, so later mutation of the list does not affect the function. |
fun Fn.sampleAt(xs: DoubleArray, edge: EdgeMode = EdgeMode.ZERO, outside: Double = 0.0): DoubleArray |
Evaluates this function at an explicit list of coordinates, returning one value per entry in the same order. |
fun Fn.toDoubleArray(count: Int, sampleDomain: Domain = domain, includeEnd: Boolean = true, edge: EdgeMode = EdgeMode.ZERO, outside: Double = 0.0): DoubleArray |
Samples this function at count evenly-spaced points over sampleDomain — the workhorse for getting numbers out. |
fun Fn.toDoubleArray(count: Int, sampleDomain: ClosedRange<*>, includeEnd: Boolean = true, edge: EdgeMode = EdgeMode.ZERO, outside: Double = 0.0): DoubleArray |
Samples this function at count evenly-spaced points over the given range; includeEnd decides whether the last sample lands exactly on the upper bound. |
fun Fn.toDoubleArray(step: Double, sampleDomain: Domain = domain, includeEnd: Boolean = true, edge: EdgeMode = EdgeMode.ZERO, outside: Double = 0.0): DoubleArray |
Samples this function every step units across sampleDomain, letting the domain's width decide how many samples that is. |
fun Fn.toDoubleArray(step: Double, sampleDomain: ClosedRange<*>, includeEnd: Boolean = true, edge: EdgeMode = EdgeMode.ZERO, outside: Double = 0.0): DoubleArray |
Samples this function every step units across the given range; the step's sign is ignored and a step of zero gives an empty array. |
fun Fn.toVec2List(count: Int, sampleDomain: Domain = domain, includeEnd: Boolean = true, edge: EdgeMode = EdgeMode.ZERO, outside: Double = 0.0): List<Vec2> |
Sample this function at count uniformly-spaced points and return the results as List<Vec2> where each entry is Vec2(time, value). |
fun Fn.toVec2List(count: Int, sampleDomain: ClosedRange<*>, includeEnd: Boolean = true, edge: EdgeMode = EdgeMode.ZERO, outside: Double = 0.0): List<Vec2> |
Samples this function at count evenly-spaced points over the given range as Vec2(x, value) pairs. Note that the coordinates are narrowed to Float on the way out, so this is for feeding drawing and overlay APIs rather than for numerics. |
fun Fn.plotHtml(sampleDomain: Domain? = null, samples: Int = 1024, heightPx: Int = 360, title: String = name ?: "Fn", xLabel: String = "x", yLabel: String = "f(x)", yDomain: Domain? = null, edge: EdgeMode = EdgeMode.ZERO, outside: Double = Double.NaN, background: String = "#000000", primaryColor: String = "#0af", secondaryColor: String = "#f80", fontFamily: String = "\"Iosevka Web\", \"IBM Plex Mono\", monospace", syncGroup: String = ""): String |
Renders an interactive plot for an Fn. |
fun Fn.plotHtml(sampleDomain: ClosedRange<*>, samples: Int = 1024, heightPx: Int = 360, title: String = name ?: "Fn", xLabel: String = "x", yLabel: String = "f(x)", yDomain: Domain? = null, edge: EdgeMode = EdgeMode.ZERO, outside: Double = Double.NaN, background: String = "#000000", primaryColor: String = "#0af", secondaryColor: String = "#f80", fontFamily: String = "\"Iosevka Web\", \"IBM Plex Mono\", monospace", syncGroup: String = ""): String |
Renders an interactive plot of this function as self-contained HTML (samples embedded), sampling over the given range. Non-finite samples — including anything that throws while being evaluated, which is caught and turned into a gap — are drawn as breaks in the curve rather than spikes. |
fun Fn.plotHtml(sampleDomain: ClosedRange<*>, samples: Int = 1024, heightPx: Int = 360, title: String = name ?: "Fn", xLabel: String = "x", yLabel: String = "f(x)", yDomain: ClosedRange<*>, edge: EdgeMode = EdgeMode.ZERO, outside: Double = Double.NaN, background: String = "#000000", primaryColor: String = "#0af", secondaryColor: String = "#f80", fontFamily: String = "\"Iosevka Web\", \"IBM Plex Mono\", monospace", syncGroup: String = ""): String |
Renders an interactive plot of this function as self-contained HTML (samples embedded) over the given x range, with an explicit y range instead of one fitted to the data. Non-finite samples are drawn as gaps. |
fun Fn.plot(channel: String = "plot", sampleDomain: Domain? = null, samples: Int = 1024, heightPx: Int = 360, title: String = name ?: "Fn", xLabel: String = "x", yLabel: String = "f(x)", yDomain: Domain? = null, edge: EdgeMode = EdgeMode.ZERO, outside: Double = Double.NaN, syncGroup: String = ""): Fn |
Draws this function into the currently executing box, on the given output channel, and returns the function unchanged so it can sit mid-chain. |
fun Fn.plot(sampleDomain: ClosedRange<*>, channel: String = "plot", samples: Int = 1024, heightPx: Int = 360, title: String = name ?: "Fn", xLabel: String = "x", yLabel: String = "f(x)", yDomain: Domain? = null, edge: EdgeMode = EdgeMode.ZERO, outside: Double = Double.NaN, syncGroup: String = ""): Fn |
Draws this function into the currently executing box over an explicit x range, returning the function unchanged. Throws IllegalStateException if there is no active box context. |
fun Fn.plot(sampleDomain: ClosedRange<*>, channel: String = "plot", samples: Int = 1024, heightPx: Int = 360, title: String = name ?: "Fn", xLabel: String = "x", yLabel: String = "f(x)", yDomain: ClosedRange<*>, edge: EdgeMode = EdgeMode.ZERO, outside: Double = Double.NaN, syncGroup: String = ""): Fn |
Draws this function into the currently executing box over explicit x and y ranges, returning the function unchanged. Throws IllegalStateException if there is no active box context. |
fun Box.plot(fn: Fn, channel: String = "plot", sampleDomain: Domain? = null, samples: Int = 1024, heightPx: Int = 360, title: String = fn.name ?: "Fn", xLabel: String = "x", yLabel: String = "f(x)", yDomain: Domain? = null, edge: EdgeMode = EdgeMode.ZERO, outside: Double = Double.NaN, syncGroup: String = ""): Fn |
Draws fn into this box on the given output channel, returning fn unchanged. |
fun Box.plot(fn: Fn, sampleDomain: ClosedRange<*>, channel: String = "plot", samples: Int = 1024, heightPx: Int = 360, title: String = fn.name ?: "Fn", xLabel: String = "x", yLabel: String = "f(x)", yDomain: Domain? = null, edge: EdgeMode = EdgeMode.ZERO, outside: Double = Double.NaN, syncGroup: String = ""): Fn |
Draws fn into this box over an explicit x range, returning fn unchanged. Needs no ambient box context. |
fun Box.plot(fn: Fn, sampleDomain: ClosedRange<*>, channel: String = "plot", samples: Int = 1024, heightPx: Int = 360, title: String = fn.name ?: "Fn", xLabel: String = "x", yLabel: String = "f(x)", yDomain: ClosedRange<*>, edge: EdgeMode = EdgeMode.ZERO, outside: Double = Double.NaN, syncGroup: String = ""): Fn |
Draws fn into this box over explicit x and y ranges, returning fn unchanged. Needs no ambient box context. |
fun plot(fn: Fn, channel: String = "plot", sampleDomain: Domain? = null, samples: Int = 1024, heightPx: Int = 360, title: String = fn.name ?: "Fn", xLabel: String = "x", yLabel: String = "f(x)", yDomain: Domain? = null, edge: EdgeMode = EdgeMode.ZERO, outside: Double = Double.NaN, syncGroup: String = ""): Fn |
Plots fn into the currently executing box — the prefix spelling of Fn.plot, for when plot(f) reads better than f.plot(). Returns fn unchanged, and throws IllegalStateException if there is no active box context. |
fun plot(fn: Fn, sampleDomain: ClosedRange<*>, channel: String = "plot", samples: Int = 1024, heightPx: Int = 360, title: String = fn.name ?: "Fn", xLabel: String = "x", yLabel: String = "f(x)", yDomain: Domain? = null, edge: EdgeMode = EdgeMode.ZERO, outside: Double = Double.NaN, syncGroup: String = ""): Fn |
Plots fn into the currently executing box over an explicit x range. Returns fn unchanged; throws IllegalStateException if there is no active box context. |
fun plot(fn: Fn, sampleDomain: ClosedRange<*>, channel: String = "plot", samples: Int = 1024, heightPx: Int = 360, title: String = fn.name ?: "Fn", xLabel: String = "x", yLabel: String = "f(x)", yDomain: ClosedRange<*>, edge: EdgeMode = EdgeMode.ZERO, outside: Double = Double.NaN, syncGroup: String = ""): Fn |
Plots fn into the currently executing box over explicit x and y ranges. Returns fn unchanged; throws IllegalStateException if there is no active box context. |
class FunctionCursor(val fn: Fn, var x: Double = fn.domain.finiteSweep().min, var step: Double = 1.0) |
A movable read head over an Fn: remembers where it is and how far to move, so a caller can pull values one at a time instead of asking for an array. |
fun Fn.cursor(start: Double = domain.finiteSweep().min, step: Double = 1.0): FunctionCursor |
A FunctionCursor over this function, positioned at the start of its domain with a step of 1.0 unless told otherwise. Note that a step of 1.0 across a 0.0..1.0 function covers the whole thing in one move — set step explicitly. |
fun Fn.stream(start: Double = domain.finiteSweep().min, step: Double = 1.0): Sequence<Double> |
An infinite lazy sequence of values, starting at start and advancing by step forever. |
fun Fn.stream(domain: Domain, step: Double = 1.0, includeEnd: Boolean = true): Sequence<Double> |
A finite lazy sequence of values across domain, one every step — the terminating counterpart to the start-based overload. |
fun Fn.streamWithX(start: Double = domain.finiteSweep().min, step: Double = 1.0): Sequence<Pair<Double, Double>> |
An infinite lazy sequence of (x, value) pairs from start, advancing by step — stream when you need to know where each sample came from. Never terminates, so bound it with take. |
enum class ChunkMapMode |
How a timeline chunk maps a position on the timeline into its source function's own coordinates. |
enum class ChunkBlend |
How a timeline chunk combines with whatever earlier chunks have already contributed at that x. |
val ADD: ChunkBlend |
ChunkBlend.ADD, re-exported so blend = ADD reads as DSL inside with(Functions). |
val REPLACE: ChunkBlend |
ChunkBlend.REPLACE, re-exported so blend = REPLACE reads as DSL inside with(Functions). |
val MULTIPLY: ChunkBlend |
ChunkBlend.MULTIPLY, re-exported so blend = MULTIPLY reads as DSL inside with(Functions). |
data class TimelineChunk(val source: Fn, val sourceDomain: Domain = source.domain, val domain: Domain, val mapMode: ChunkMapMode = ChunkMapMode.DIRECT, val blend: ChunkBlend = ChunkBlend.REPLACE, val edge: EdgeMode = EdgeMode.ZERO, val outside: Double = 0.0, val window: Fn? = null, val gain: Fn? = null, val name: String? = source.name) |
One placement on a timeline: which function, which slice of it, where that slice goes, and how it is mapped and blended once it gets there. |
class TimelineGraph(chunks: List<TimelineChunk>, val outside: Double = 0.0, val defaultDomain: Domain = Domain.point(0.0)) |
A finished arrangement: a list of TimelineChunks that can be evaluated as one signal, or turned into an Fn. |
class TimelineBuilder(start: Double = 0.0, private val outside: Double = 0.0) |
The receiver inside timeline { }: a moving cursor, a set of named marks, and the placement verbs (Fn.place, Fn.overlap, Fn.splice, repeatToFit, stretchToFit) that lay functions out in time. |
fun spliceWindow(fadeIn: Double = 0.15, fadeOut: Double = fadeIn, domain: Domain = Domain.DEFAULT): Fn |
A fade-in / fade-out window: 1.0 through the middle, smoothstepped down to 0.0 at whichever ends you asked to fade. |
fun timelineGraph(start: Double = 0.0, outside: Double = 0.0, init: TimelineBuilder.() -> Unit): TimelineGraph |
Runs a TimelineBuilder block and returns the resulting TimelineGraph — timeline when you want the chunk structure to inspect or re-blend, rather than a finished Fn. |
fun timeline(start: Double = 0.0, outside: Double = 0.0, name: String = "timeline", domain: Domain? = null, init: TimelineBuilder.() -> Unit): Fn |
Arranges functions in time and returns the result as one Fn — the main entry point for composition across x, as opposed to the arithmetic that stacks signals on top of each other. |
fun List<Fn>.sequence(overlap: Number = 0.0, edge: EdgeMode = EdgeMode.ZERO, outside: Double = 0.0, name: String = "sequence"): Fn |
Places each function once in list order. A positive overlap turns hard joins into additive crossfades at the joins. |
fun List<Fn>.sequence(overlap: Fn, edge: EdgeMode = EdgeMode.ZERO, outside: Double = 0.0, name: String = "sequence"): Fn |
Places each function once, in list order, with a signal-valued overlap so each join can be crossfaded by a different amount — the overlap in force at a join is sampled around that join. |
fun List<Fn>.sequenceLoop(overlap: Number = 0.0, domain: Domain = Domain.of(0.0, Double.POSITIVE_INFINITY), edge: EdgeMode = EdgeMode.ZERO, outside: Double = 0.0, name: String = "sequenceLoop"): Fn |
Plays the list in order and then repeats forever, wrapping the last join back onto the first item so the loop is seamless when overlap is positive. |
fun List<Fn>.sequenceLoop(overlap: Fn, domain: Domain = Domain.of(0.0, Double.POSITIVE_INFINITY), edge: EdgeMode = EdgeMode.ZERO, outside: Double = 0.0, name: String = "sequenceLoop"): Fn |
Plays the list in order and repeats forever, wrapping the final join onto the first item, with a signal-valued overlap sampled around each join. Unbounded above by default; below the start it reads outside. An empty list gives a zero function over an empty domain. |
fun List<Fn>.randomPermute(seed: Long = 0L, overlap: Number = 0.0, edge: EdgeMode = EdgeMode.ZERO, outside: Double = 0.0, name: String = "randomPermute"): Fn |
Plays every function exactly once, in an order shuffled from seed — a permutation, so nothing is repeated and nothing dropped. |
fun List<Fn>.randomPermute(seed: Long = 0L, overlap: Fn, edge: EdgeMode = EdgeMode.ZERO, outside: Double = 0.0, name: String = "randomPermute"): Fn |
Plays every function exactly once in an order shuffled deterministically from seed, with a signal-valued overlap sampled around each join. A permutation — nothing repeated, nothing dropped. |
fun List<Fn>.randomPermuteLoop(seed: Long = 0L, overlap: Number = 0.0, domain: Domain = Domain.of(0.0, Double.POSITIVE_INFINITY), edge: EdgeMode = EdgeMode.ZERO, outside: Double = 0.0, name: String = "randomPermuteLoop"): Fn |
Loops forever, re-shuffling the order on every pass — so each cycle contains all the material exactly once, but in a different arrangement. |
fun List<Fn>.randomPermuteLoop(seed: Long = 0L, overlap: Fn, domain: Domain = Domain.of(0.0, Double.POSITIVE_INFINITY), edge: EdgeMode = EdgeMode.ZERO, outside: Double = 0.0, name: String = "randomPermuteLoop"): Fn |
Loops forever, re-shuffling the order on every pass from seed and the cycle number, with a signal-valued overlap sampled around each join. Every cycle contains all the material once; the cycle length stays fixed as the contents are reordered. |
fun List<Fn>.randomSequence(seed: Long = 0L, overlap: Number = 0.0, slot: Number? = null, domain: Domain = Domain.of(0.0, Double.POSITIVE_INFINITY), edge: EdgeMode = EdgeMode.ZERO, outside: Double = 0.0, name: String = "randomSequence"): Fn |
Fills an endless run of fixed-length slots, choosing an item at random for each one — so items repeat, items get skipped, and the rhythm stays regular. |
fun List<Fn>.randomSequence(seed: Long = 0L, overlap: Number = 0.0, slot: Fn, domain: Domain = Domain.of(0.0, Double.POSITIVE_INFINITY), edge: EdgeMode = EdgeMode.ZERO, outside: Double = 0.0, name: String = "randomSequence"): Fn |
Fills an endless run of slots, choosing an item at random for each, with a signal-valued slot length — so the pulse of the sequence can itself be modulated. Items repeat and are skipped freely; each choice is hashed from seed and the slot index, and each chosen item is time-scaled to the slot. |
fun List<Fn>.randomSequence(seed: Long = 0L, overlap: Fn, slot: Number, domain: Domain = Domain.of(0.0, Double.POSITIVE_INFINITY), edge: EdgeMode = EdgeMode.ZERO, outside: Double = 0.0, name: String = "randomSequence"): Fn |
Fills an endless run of fixed-length slots, choosing an item at random for each, with a signal-valued overlap sampled around each join. Items repeat and are skipped freely; every chosen item is time-scaled to the slot length. |
fun List<Fn>.randomSequence(seed: Long = 0L, overlap: Fn, slot: Fn? = null, domain: Domain = Domain.of(0.0, Double.POSITIVE_INFINITY), edge: EdgeMode = EdgeMode.ZERO, outside: Double = 0.0, name: String = "randomSequence"): Fn |
Fills an endless run of slots, choosing an item at random for each, with both overlap and slot length as signals. slot defaults to the longest item's duration; each chosen item is time-scaled to fit, and each choice is hashed from seed and the slot index so the stream is reproducible. |
fun List<Fn>.weightedSequence(weights: List<Any>, seed: Long = 0L, overlap: Number = 0.0, slot: Number? = null, domain: Domain = Domain.of(0.0, Double.POSITIVE_INFINITY), edge: EdgeMode = EdgeMode.ZERO, outside: Double = 0.0, name: String = "weightedSequence"): Fn |
randomSequence with a thumb on the scale: each item's chance of being drawn is proportional to its weight. |
fun List<Fn>.weightedSequence(weights: List<Any>, seed: Long = 0L, overlap: Fn, slot: Fn? = null, domain: Domain = Domain.of(0.0, Double.POSITIVE_INFINITY), edge: EdgeMode = EdgeMode.ZERO, outside: Double = 0.0, name: String = "weightedSequence"): Fn |
randomSequence with per-item weights deciding the odds, and a signal-valued overlap. Weights are positional and relative, may be numbers or signals, and count as zero when missing or not positive; if all of them are zero the draw falls back to a uniform choice. |
fun List<Fn>.weightedSequence(weights: DoubleArray, seed: Long = 0L, overlap: Number = 0.0, slot: Number? = null, domain: Domain = Domain.of(0.0, Double.POSITIVE_INFINITY), edge: EdgeMode = EdgeMode.ZERO, outside: Double = 0.0, name: String = "weightedSequence"): Fn |
randomSequence with per-item weights deciding the odds. Weights line up positionally with the list and are relative rather than normalized; a weight of zero (or a missing one) excludes that item, and if every weight is zero the draw falls back to a uniform choice. |
fun List<Fn>.weightedSequence(weights: DoubleArray, seed: Long = 0L, overlap: Fn, slot: Fn? = null, domain: Domain = Domain.of(0.0, Double.POSITIVE_INFINITY), edge: EdgeMode = EdgeMode.ZERO, outside: Double = 0.0, name: String = "weightedSequence"): Fn |
randomSequence with per-item weights deciding the odds, and a signal-valued overlap. Weights are positional and relative; a zero or missing weight excludes that item, and if all are zero the draw falls back to a uniform choice. |
fun List<Fn>.shuffle(seed: Long = 0L, overlap: Number = 0.0, edge: EdgeMode = EdgeMode.ZERO, outside: Double = 0.0, name: String = "shuffle"): Fn |
Plays every function once in a shuffled order — the friendlier name for randomPermute, with identical behaviour. Deterministic from seed, and note that it shuffles once: use shuffleLoop to keep going. |
fun List<Fn>.shuffle(seed: Long = 0L, overlap: Fn, edge: EdgeMode = EdgeMode.ZERO, outside: Double = 0.0, name: String = "shuffle"): Fn |
Plays every function once in a shuffled order, with a signal-valued overlap — an alias of randomPermute with identical behaviour. |
fun List<Fn>.shuffleLoop(seed: Long = 0L, overlap: Number = 0.0, domain: Domain = Domain.of(0.0, Double.POSITIVE_INFINITY), edge: EdgeMode = EdgeMode.ZERO, outside: Double = 0.0, name: String = "shuffleLoop"): Fn |
Loops forever, re-shuffling on every pass — the friendlier name for randomPermuteLoop, with identical behaviour. Each cycle plays all the material once, in a fresh order derived from seed and the cycle number. |
fun List<Fn>.shuffleLoop(seed: Long = 0L, overlap: Fn, domain: Domain = Domain.of(0.0, Double.POSITIVE_INFINITY), edge: EdgeMode = EdgeMode.ZERO, outside: Double = 0.0, name: String = "shuffleLoop"): Fn |
Loops forever, re-shuffling on every pass, with a signal-valued overlap — an alias of randomPermuteLoop with identical behaviour. |
fun List<Fn>.palindrome(overlap: Number = 0.0, includeEndsTwice: Boolean = false, edge: EdgeMode = EdgeMode.ZERO, outside: Double = 0.0, name: String = "palindrome"): Fn |
Plays the list forwards and then backwards, once — an out-and-back gesture rather than a loop. |
fun List<Fn>.palindrome(overlap: Fn, includeEndsTwice: Boolean = false, edge: EdgeMode = EdgeMode.ZERO, outside: Double = 0.0, name: String = "palindrome"): Fn |
Plays the list forwards then backwards once, with a signal-valued overlap. The turning point — the last item — is not repeated unless includeEndsTwice is set, so [a, b, c] gives a b c b a, and lists of one or two items always double their ends. |
fun List<Fn>.roundRobin(slot: Number, overlap: Number = 0.0, domain: Domain = Domain.of(0.0, Double.POSITIVE_INFINITY), edge: EdgeMode = EdgeMode.ZERO, outside: Double = 0.0, name: String = "roundRobin"): Fn |
Cycles through the list in order, one item per fixed-length slot, forever — the deterministic counterpart to randomSequence. |
fun List<Fn>.roundRobin(slot: Fn, overlap: Fn = param(0.0), domain: Domain = Domain.of(0.0, Double.POSITIVE_INFINITY), edge: EdgeMode = EdgeMode.ZERO, outside: Double = 0.0, name: String = "roundRobin"): Fn |
Cycles through the list in order, one item per slot, forever, with slot and overlap as signals — so the rotation's pulse can itself be modulated. Every item is time-scaled to the current slot length; anything before the domain's lower bound reads outside. |
fun List<Fn>.stack(edge: EdgeMode = EdgeMode.ZERO, outside: Double = 0.0, name: String = "stack"): Fn |
Sums every function in the list at each x — layering rather than sequencing, and the list form of a + b + c. |
fun List<Fn>.average(edge: EdgeMode = EdgeMode.ZERO, outside: Double = 0.0, name: String = "average"): Fn |
The mean of every function in the list at each x — stack divided by the list size, so layering many signals keeps the level of one. |
fun List<Fn>.select(selector: Fn, edge: EdgeMode = EdgeMode.ZERO, outside: Double = 0.0, name: String = "select"): Fn |
Switches between the functions: at each x, selector names an index and that member alone is heard. |
fun List<Fn>.morph(selector: Fn, edge: EdgeMode = EdgeMode.ZERO, outside: Double = 0.0, name: String = "morph"): Fn |
Crossfades along the list: selector gives a fractional index, and the two neighbouring members are blended by its fractional part with a smootherstep ease — the continuous counterpart to select. |
data class FnTransform(val name: String, private val applyFn: (Fn) -> Fn) |
A reusable function-to-function operation: a transform you can name, hold on to, compose with others, and then apply to any number of sources. |
infix fun Fn.over(domain: Domain): Fn |
Relabels the function's domain to domain without touching its values, exactly withDomain. To rescale the function into a new interval use stretch. |
infix fun Fn.over(range: ClosedRange<*>): Fn |
f over (0.0..2.0) — relabels the function's domain without touching its values, exactly withDomain. To rescale the function into a new interval use stretch. |
infix fun Fn.stretch(domain: Domain): Fn |
f stretch (0.0..2.0) — replays this function's whole current domain across the target interval, rescaling time rather than relabelling it. Requires both domains to be finite. |
infix fun Fn.stretch(range: ClosedRange<*>): Fn |
f stretch (0.0..2.0) — replays this function's whole current domain across the target interval, rescaling time rather than relabelling it. Requires both domains to be finite. |
infix fun Fn.through(transform: FnTransform): Fn |
f through gain(0.5) — applies an FnTransform, for when the infix word reads better than the slash. |
infix fun Fn.pipe(transform: FnTransform): Fn |
f pipe gain(0.5) — applies an FnTransform; a synonym of through. |
operator fun Fn.div(transform: FnTransform): Fn |
f / gain(0.5) — applies an FnTransform to this function. |
val all: FnTransform |
A transform that widens a function's domain to the whole real line, so it stops constraining anything it is combined with. Values are untouched. |
fun stretch(to: Domain, from: Domain? = null, shape: StretchShape = StretchShape.LINEAR): FnTransform |
A reusable transform that replays a source's whole domain across to, rescaling time — the FnTransform form of Fn.stretch. |
fun stretch(to: ClosedRange<*>, from: Domain? = null, shape: StretchShape = StretchShape.LINEAR): FnTransform |
A reusable transform replaying a source's domain across the given range, rescaling time. Both domains must be finite or it throws. |
fun stretchSmooth(to: ClosedRange<*>, from: Domain? = null): FnTransform+1 overload · also Domain |
stretch with a smoothstep pacing: the endpoints still line up, but the source is played slowly at the ends and quickly through the middle. |
fun stretchSmoother(to: ClosedRange<*>, from: Domain? = null): FnTransform+1 overload · also Domain |
stretch with a smootherstep pacing — like stretchSmooth but with zero acceleration as well as zero velocity at the ends. |
fun stretchBy(to: Domain, warp: Fn, from: Domain? = null): FnTransform |
stretch with the pacing given by a function rather than a named shape: warp maps normalized progress through the target onto normalized progress through the source. |
fun stretchBy(to: ClosedRange<*>, warp: Fn, from: Domain? = null): FnTransform |
stretch onto the given range with the pacing given by warp, a function read on 0..1 mapping target progress to source progress. |
fun delay(amount: Number): FnTransform |
A reusable transform that delays a source by amount, positive being later. A constant delay carries the domain with it. |
fun delay(amount: Fn): FnTransform |
A reusable transform delaying a source by amount; a signal-valued amount makes it a time warp rather than a delay, and leaves the domain alone. |
fun gain(amount: Number): FnTransform |
A reusable transform scaling a source's values by amount — f / gain(0.5) is f * 0.5. |
fun gain(amount: Fn): FnTransform |
A reusable transform scaling a source's values by amount; a signal-valued amount is an amplitude envelope. Note that a bounded amount will crop the source's domain. |
fun bias(amount: Number): FnTransform |
A reusable transform adding amount to a source's values — a DC offset. |
fun bias(amount: Fn): FnTransform |
A reusable transform adding amount to a source's values. A signal-valued amount is a drifting offset; a bounded one will crop the source's domain. |
fun clipAt(threshold: Number): FnTransform |
A reusable transform hard-clipping a source symmetrically to ±threshold. Named to avoid colliding with Fn.clip. |
fun clipAt(threshold: Fn): FnTransform |
A reusable transform hard-clipping a source symmetrically to ±threshold, which may itself be a signal. |
fun softClipBy(amount: Number): FnTransform |
A reusable transform saturating a source through a normalized tanh curve; amount is drive, not a ceiling. Named to avoid colliding with Fn.softClip. |
fun softClipBy(amount: Fn): FnTransform |
A reusable transform saturating a source through a normalized tanh curve, with a possibly signal-valued drive amount. |
fun hold(step: Number): FnTransform |
A reusable sample-and-hold: reads the source only every step and holds each value until the next. Stateless, so it stays freely sampleable. |
fun hold(step: Fn): FnTransform |
A reusable sample-and-hold on a grid of step, which may itself be a signal. Stateless, so it stays freely sampleable. |
fun faster(factor: Number = 2.0, about: Number = 0.0): FnTransform+1 overload · also Fn |
Time-scale transforms use musical language: faster(2.0) samples the source twice as quickly, while slower(2.0) stretches it by two. |
fun slower(factor: Number = 2.0, about: Number = 0.0): FnTransform+1 overload · also Fn |
|
fun named(name: String): FnTransform |
|
fun lowPass(window: Number, samples: Int = 64): FnTransform+1 overload · also Fn |
|
fun highPass(window: Number, samples: Int = 64): FnTransform+1 overload · also Fn |
|
fun allPass(delay: Number, feedback: Number = 0.5, repeats: Int = 3): FnTransform+2 overloads · also Any, Fn |
A stateless phase color, not a recursive DSP all-pass. That distinction is intentional here: an Fn should stay pure and freely sampleable. |
fun turbulence(amount: Number, frequency: Number = 8.0, octaves: Int = 4, seed: Long = 0L): FnTransform+3 overloads · also Fn |
|
fun gateHold(gate: Fn, step: Fn, threshold: Fn = param(0.5), lookBack: Fn = param(1.0)): FnTransform+2 overloads · also Number, Any |
Stateless sample-and-hold driven by an external gate. Because this is a pure function, it searches backward for the latest open gate rather than storing latch state between calls. |
fun crush(bits: Int): FnTransform |
|
fun fold(threshold: Number = 1.0): FnTransform+1 overload · also Fn |
Waveshaping / distortion transforms — the value-domain (west-coast) counterparts to the time-domain hold / crush pair. Each is a thin factory over a pure value shaper (see Fn.fold, Fn.rectify, Fn.abs, Fn.quantize) so it composes in through / pipe / / chains and surfaces in the type-aware infix completion. They shape the signal's VALUE, distinct from hold/stepHold which clock its time. |
fun rectify(mode: RectifyMode = RectifyMode.FULL): FnTransform |
|
fun quantize(levels: Int): FnTransform |
|
fun chebyshev(order: Int = 2): FnTransform+2 overloads · also Number, Fn |
|
fun copyPaste(from: ClosedRange<*>, to: ClosedRange<*>, stretch: Boolean = true, edge: EdgeMode = EdgeMode.CLAMP): FnTransform+1 overload · also Domain |
|
fun mirror(about: Number? = null): FnTransform+1 overload · also Fn |
|
fun repeatEvery(period: Number, origin: Number = 0.0, domain: Domain = Domain.ALL): FnTransform+5 overloads · also Any, ClosedRange<*>, Fn |
|
fun mirrorEvery(period: Number, origin: Number = 0.0, domain: Domain = Domain.ALL): FnTransform+5 overloads · also Any, ClosedRange<*>, Fn |
|
fun randomRepeat(period: Number, seed: Long = 0L, domain: Domain = Domain.of(0.0, Double.POSITIVE_INFINITY), sourceDomain: Domain? = null, edge: EdgeMode = EdgeMode.CLAMP): FnTransform+3 overloads · also ClosedRange<*>, Fn |
|
fun randomCopyPaste(grain: Number, seed: Long = 0L, domain: Domain = Domain.of(0.0, Double.POSITIVE_INFINITY), sourceDomain: Domain? = null, density: Number = 1.0, edge: EdgeMode = EdgeMode.CLAMP, outside: Double = 0.0): FnTransform+5 overloads · also ClosedRange<*>, Any, Fn |
|
infix fun Fn.then(next: Fn): Fn |
a then b — plays this function and then next, butted end to end with no crossfade. |
data class OverlapStart(val first: Fn, val amount: Fn) |
The half-finished middle of a overlapping 0.2 with b — it carries the first function and the overlap until the with arrives. |
infix fun Fn.overlapping(amount: Number): OverlapStart |
a overlapping 0.2 with b — begins a crossfaded join; the overlap is in x units, and nothing happens until with supplies the second function. |
infix fun Fn.overlapping(amount: Fn): OverlapStart |
a overlapping f with b — begins a crossfaded join with a signal-valued overlap, sampled around the join itself. Nothing happens until with supplies the second function. |
fun Fn.overlapping(amount: Number, next: Fn): Fn |
Plays this function then next, their join crossfaded over amount x units — the call-syntax form of a overlapping amount with next. |
fun Fn.overlapping(amount: Fn, next: Fn): Fn |
Plays this function then next with a signal-valued crossfade amount — the call-syntax form of a overlapping amount with next. |
fun Fn.overlapWith(next: Fn, amount: Number, blend: ChunkBlend = ChunkBlend.REPLACE, window: Fn = hannWindow(Domain.DEFAULT)): Fn |
Plays this function then next, with next starting amount units early so the two crossfade under a Hann window. |
fun Fn.overlapWith(next: Fn, amount: Fn, blend: ChunkBlend = ChunkBlend.REPLACE, window: Fn = hannWindow(Domain.DEFAULT)): Fn |
Plays this function then next starting amount units early, crossfading under a Hann window. With a signal-valued amount the overlap actually used is sampled around the join itself. blend decides how the overlapping region combines. |
fun Fn.toFLine(sampleDomain: Domain = domain, samples: Int = 256, includeEnd: Boolean = true, xScale: Double = 1.0, yScale: Double = 1.0, xOffset: Double = 0.0, yOffset: Double = 0.0): FLine |
Samples this function into a drawable FLine polyline: x becomes xOffset + x * xScale and the value becomes yOffset + value * yScale. |
fun Fn.toFLine(sampleDomain: ClosedRange<*>, samples: Int = 256, includeEnd: Boolean = true, xScale: Double = 1.0, yScale: Double = 1.0, xOffset: Double = 0.0, yOffset: Double = 0.0): FLine |
Samples this function over the given range into a drawable FLine, mapping x through xOffset + x * xScale and the value through yOffset + value * yScale. No y flip is applied — negate yScale for a y-down screen space. |
operator fun Fn.plus(other: Fn): Fn |
Adds two functions pointwise — mixing, in signal terms. |
operator fun Fn.minus(other: Fn): Fn |
Subtracts other from this function pointwise. The result's domain is the intersection of the two. |
operator fun Fn.times(other: Fn): Fn |
Multiplies two functions pointwise — windowing, enveloping and ring modulation are all this operator. The result's domain is the intersection of the two. |
operator fun Fn.div(other: Fn): Fn |
Divides this function by other pointwise. The result's domain is the intersection of the two. |
operator fun Fn.plus(value: Number): Fn |
Adds a constant to every value — a DC offset. The number becomes an unbounded parameter signal, so this never shrinks the domain. |
operator fun Fn.minus(value: Number): Fn |
Subtracts a constant from every value. The number becomes an unbounded parameter signal, so this never shrinks the domain. |
operator fun Fn.times(value: Number): Fn |
Scales every value by a constant — gain. The number becomes an unbounded parameter signal, so this never shrinks the domain. |
operator fun Fn.div(value: Number): Fn |
Divides every value by a constant. The number becomes an unbounded parameter signal, so this never shrinks the domain. |
operator fun Number.plus(f: Fn): Fn |
2.0 + f — a constant plus a function, which is the same as f + 2.0. |
operator fun Number.minus(f: Fn): Fn |
1.0 - f — a constant minus a function, in that order (not the same as f - 1.0), which is how an inverted envelope is written. |
operator fun Number.times(f: Fn): Fn |
2.0 * f — a constant times a function, which is the same as f * 2.0. |
operator fun Number.div(f: Fn): Fn |
1.0 / f — a constant divided by a function, in that order; the way to write a reciprocal. Unguarded where f is zero. |
fun Fn.mapY(newName: String? = name, transform: (Double) -> Double): Fn |
Applies an arbitrary lambda to this function's value, leaving its domain alone — the general-purpose waveshaper. |
fun Fn.mapX(newName: String? = name, newDomain: Domain = domain, transform: (Double) -> Double): Fn |
Applies an arbitrary lambda to the coordinate before evaluating — the general-purpose time warp, the counterpart to mapY. |
fun Fn.stretch(to: ClosedRange<*>, from: Domain = domain, shape: StretchShape = StretchShape.LINEAR): Fn+1 overload · also Domain |
Remap this function's existing coordinate domain onto to. |
fun Fn.stretchFrom(from: ClosedRange<*>, to: ClosedRange<*>, shape: StretchShape = StretchShape.LINEAR): Fn+1 overload · also Domain |
|
fun Fn.stretchSmooth(to: ClosedRange<*>, from: Domain = domain): Fn+1 overload · also Domain |
|
fun Fn.stretchSmoother(to: ClosedRange<*>, from: Domain = domain): Fn+1 overload · also Domain |
|
fun Fn.stretchBy(to: ClosedRange<*>, warp: Fn, from: Domain = domain): Fn+1 overload · also Domain |
stretch onto to with the pacing given by warp rather than a named StretchShape — see the Functions.stretchBy transform for what a warp is and how it is read. |
fun Fn.stretchBy(to: ClosedRange<*>, from: Domain = domain, warp: (Double) -> Double): Fn+1 overload · also Domain |
|
fun Fn.shift(dx: Fn = param(0.0)): Fn |
Slides this function along x by dx: positive values move it later (f.shift(2.0) evaluates f(x - 2)). |
fun Fn.shift(dx: Number): Fn |
Slides this function along x by dx, positive being later. A constant shift moves the domain with it. |
fun Fn.scaleX(scale: Fn = param(1.0), about: Fn = param(0.0)): Fn |
Scales this function along x about the point about — scaleX(2.0) makes it twice as wide, and therefore half as fast. |
fun Fn.scaleX(scale: Number, about: Number = 0.0): Fn |
Scales this function along x about about — bigger is wider and slower. about defaults to the origin, not the function's centre; a scale of zero collapses to the value at about. |
fun Fn.scaleX(scale: Any, about: Any = 0.0): Fn |
Scales this function along x about about, taking either argument as a number or an Fn and throwing for anything else. about defaults to the origin. |
fun Fn.faster(factor: Number = 2.0, about: Number = 0.0): Fn |
Plays this function factor times faster about about — faster(2.0) runs at double speed. The factor's magnitude is used and floored just above zero, so it can never invert or stall. |
fun Fn.faster(factor: Any, about: Any = 0.0): Fn |
Plays this function factor times faster about about, taking either argument as a number or an Fn and throwing for anything else. |
fun Fn.faster(factor: Fn, about: Fn = param(0.0)): Fn |
Plays this function factor times faster about about — the musician's spelling of scaleX(1 / factor). |
fun Fn.slower(factor: Number = 2.0, about: Number = 0.0): Fn |
Plays this function factor times slower about about — slower(2.0) stretches it by two. The factor's magnitude is used and floored just above zero. |
fun Fn.slower(factor: Any, about: Any = 0.0): Fn |
Plays this function factor times slower about about, taking either argument as a number or an Fn and throwing for anything else. |
fun Fn.slower(factor: Fn, about: Fn = param(0.0)): Fn |
Plays this function factor times slower about about — the musician's spelling of scaleX(factor), and the inverse of faster. |
fun Fn.scaleY(scale: Fn = param(1.0)): Fn |
Scales this function's values by scale — gain. Identical to f * scale, spelled as a method for chaining; a signal-valued scale is an amplitude envelope. |
fun Fn.scaleY(scale: Number): Fn |
Scales this function's values by scale — gain. The same as f * scale, spelled as a method for chaining. |
fun Fn.offsetY(offset: Fn = param(0.0)): Fn |
Shifts this function's values by offset — a DC bias, moving the signal up or down without touching its timing. Identical to f + offset, spelled as a method for chaining. |
fun Fn.offsetY(offset: Number): Fn |
Shifts this function's values by offset — a DC bias. The same as f + offset, spelled as a method for chaining. |
fun Fn.reverse(about: Fn = param(domain.center)): Fn |
Plays this function backwards, mirrored about about — which defaults to the centre of its own domain, so a bounded function reverses in place. |
fun Fn.reverse(about: Number): Fn |
Plays this function backwards, mirrored about the coordinate about. Unlike the no-argument form there is no reliance on a finite domain centre. |
fun Fn.flip(): Fn |
Negates every value, turning the signal upside down. This flips it in value; reverse flips it in time. Differentiable, with derivative -1 everywhere. |
fun Fn.abs(): Fn |
The absolute value of the signal — full-wave rectification, folding the negative half upward. The same shape as rectify(FULL), and like it a first-class, rewritable graph node. |
fun Fn.floor(): Fn |
The greatest integer at or below every value — a staircase. |
fun Fn.sign(): Fn |
The sign of the signal: -1, 0 or +1. A hard three-level comparator — everything about the magnitude is discarded. |
fun Fn.atan(): Fn |
The arc tangent of every value, in radians, in (-pi/2, pi/2). |
fun Fn.atan2(x: Fn = param(1.0)): Fn |
The quadrant-correct angle of the vector whose y component is this signal and whose x component is x — atan2(this, x), in radians, in [-pi, pi]. |
fun Fn.atan2(x: Number): Fn |
The angle of the vector (x, this), in radians. See the Fn overload. |
fun Fn.hypot(y: Fn = param(0.0)): Fn |
The length of the vector whose components are this signal and y — sqrt(this² + y²), computed as Math.hypot so it neither overflows for large components nor underflows for tiny ones. |
fun Fn.hypot(y: Number): Fn |
The length of the vector (this, y). See the Fn overload. |
fun Fn.pow(exponent: Fn = param(1.0)): Fn |
Raises every value to the power exponent, which may itself be a signal. |
fun Fn.pow(exponent: Number): Fn |
Raises every value to the power exponent. Unguarded, so negative values under fractional exponents give NaN. |
fun Fn.exp(): Fn |
e to the power of every value. Grows fast: a signal reaching 10 comes out above 22000, so expect to scale afterwards. |
fun Fn.log(base: Fn = param(Math.E)): Fn |
The logarithm of every value, in base base (natural by default), computed as ln(v) / ln(base). |
fun Fn.log(base: Number): Fn |
The logarithm of every value in base base. Zero gives negative infinity and negative values NaN. |
fun Fn.clamp(min: Fn = param(-1.0), max: Fn = param(1.0)): Fn |
Holds every value inside min .. max, hard — an asymmetric limiter, defaulting to the -1 .. 1 range the rest of the shaping family assumes. |
fun Fn.clamp(min: Number, max: Number): Fn |
Holds every value inside min .. max, hard-clipped at both bounds. An inverted range throws rather than being sorted. |
fun Fn.clamp(min: Any, max: Any): Fn |
Holds every value inside min .. max, taking either bound as a number or an Fn and throwing for anything else. |
fun Fn.clip(threshold: Fn = param(1.0)): Fn |
Hard-clips the signal symmetrically to ±threshold — clamp for the common case where the limit is the same either side. |
fun Fn.clip(threshold: Number): Fn |
Hard-clips the signal symmetrically to ±threshold, with a corner rather than a knee. |
fun Fn.rectify(mode: RectifyMode = RectifyMode.HALF): Fn |
Rectifies the signal: half-wave clamps the negative excursion to zero, full-wave reflects it upward (\|x\|). |
fun Fn.softClip(amount: Fn = param(1.0)): Fn |
Saturates the signal through a tanh curve, normalized so that an input of ±1 still comes out at ±1 — a soft knee where clip has a corner. |
fun Fn.softClip(amount: Number): Fn |
Saturates the signal through a normalized tanh curve; amount is drive rather than a threshold, and ±1 in still gives ±1 out. An amount of zero collapses the signal to zero. |
fun Fn.fold(threshold: Fn = param(1.0)): Fn |
A west-coast wavefolder: passes the signal through untouched while it stays inside ±threshold, and reflects it back each time it crosses — so pushing harder adds folds rather than flattening the peaks. |
fun Fn.fold(threshold: Number): Fn |
A wavefolder: the signal passes through inside ±threshold and reflects back each time it crosses. Drive it with gain rather than by lowering the threshold; a threshold of zero or less gives the constant zero. |
fun Fn.chebyshev(order: Fn = param(2.0)): Fn |
The harmonic-generating Chebyshev waveshaper: pushing a unit-amplitude cosine through order n yields the n-th harmonic. |
fun Fn.chebyshev(order: Int): Fn |
The Chebyshev waveshaper of whole-number order: a unit cosine in gives that harmonic out. The drive is clamped to ±1 first. |
fun Fn.chebyshev(order: Number): Fn |
The Chebyshev waveshaper of order, which need not be a whole number — the shaper is continuous in the order. The drive is clamped to ±1 first. |
fun Fn.wrapY(min: Fn = param(-1.0), max: Fn = param(1.0)): Fn |
Wraps every value into min .. max by repetition instead of clipping it — the value-domain sibling of Domain.wrap. |
fun Fn.wrapY(min: Number, max: Number): Fn |
Wraps every value into min .. max by repetition rather than clipping — discontinuous by design. The bounds are sorted, so their order does not matter. |
fun Fn.wrapY(min: Any, max: Any): Fn |
Wraps every value into min .. max by repetition, taking either bound as a number or an Fn and throwing for anything else. |
fun Fn.withEdge(edge: EdgeMode, outside: Fn = param(0.0)): Fn |
Bakes an out-of-domain policy into the function itself, so plain Fn.eval behaves the way Fn.evalSafe would have with these arguments. |
fun Fn.withEdge(edge: EdgeMode, outside: Number): Fn |
Bakes an out-of-domain policy into the function, so plain evaluation clamps, wraps, mirrors or returns outside without the caller having to ask. |
fun Fn.quantize(levels: Fn): Fn |
Snaps the signal to levels evenly-spaced steps — a staircase quantiser. |
fun Fn.quantize(levels: Int): Fn |
Snaps the signal to levels evenly-spaced steps across -1 .. 1. Fewer than two levels gives the constant zero; values beyond ±1 land on the end steps. |
fun Fn.bitCrush(bits: Fn): Fn |
Snaps the signal to 2^bits - 1 steps across -1 .. 1 — quantize counted in bits, for when the digital-audio idiom is what you mean. |
fun Fn.bitCrush(bits: Int): Fn |
Snaps the signal to 2^bits - 1 steps across -1 .. 1. Zero bits or fewer gives the constant zero. |
fun Fn.mix(other: Fn, amount: Fn = param(0.5)): Fn |
Blends this function with other by amount — 0 is all of this one, 1 is all of other, 0.5 an even mix. |
fun Fn.mix(other: Fn, amount: Number): Fn |
Blends this function with other by amount: 0 is all of this one, 1 all of other. Not clamped, so values outside 0..1 extrapolate. |
fun Fn.crossfade(other: Fn, center: Fn = param(0.0), width: Fn = param(0.0)): Fn |
Hands over from this function to other at a place along x: this one before center, other after, with a smootherstep transition width wide straddling the centre. |
fun Fn.crossfade(other: Fn, center: Number, width: Number): Fn |
Hands over from this function to other at center, easing across a transition width wide. A width of zero or less is a hard cut. |
fun Fn.crossfade(other: Fn, center: Any, width: Any): Fn |
Hands over from this function to other at center over a transition width wide, taking either as a number or an Fn and throwing for anything else. |
fun Fn.cut(range: Domain, outside: Fn = param(0.0)): Fn |
Gates the function to range: inside it (both ends included) the original values, outside it outside. |
fun Fn.cut(range: Domain, outside: Number): Fn |
Gates the function to range, reading outside beyond it. The declared domain is unchanged. |
fun Fn.cut(range: ClosedRange<*>, outside: Fn = param(0.0)): Fn |
Gates the function to range, reading outside beyond it. The declared domain is unchanged. |
fun Fn.cut(range: ClosedRange<*>, outside: Number): Fn |
Gates the function to range, reading outside beyond it. The declared domain is unchanged. |
fun Fn.trim(range: ClosedRange<*>, outside: Fn = param(0.0)): Fn+3 overloads · also Domain, Number |
Gates the function to range, reading outside beyond it — a synonym of cut. The declared domain is unchanged. |
fun Fn.slice(range: ClosedRange<*>, outside: Fn = param(0.0)): Fn+3 overloads · also Domain, Number |
Gates the function to range, reading outside beyond it — a synonym of cut. The declared domain is unchanged. |
fun Fn.replace(range: Domain, replacement: Fn): Fn |
Substitutes replacement for this function over range, keeping the original everywhere else — a patch, where cut is a gate. |
fun Fn.replace(range: ClosedRange<*>, replacement: Fn): Fn |
Substitutes replacement for this function over range and keeps the original elsewhere. The result's domain is the union of the two, and the seams are not faded. |
fun Fn.window(window: Fn): Fn |
Multiplies this function by window — spelled as a method to make the intent ("apply an envelope") obvious. Exactly this * window, which means the result's domain is the intersection of the two, so a bounded window crops an unbounded signal. |
fun Fn.amplitudeMod(mod: Fn, depth: Fn = param(1.0), bias: Fn = param(1.0)): Fn |
Amplitude modulation: scales this function by bias + depth * mod. |
fun Fn.amplitudeMod(mod: Fn, depth: Number, bias: Number = 1.0): Fn |
Amplitude modulation: scales this function by bias + depth * mod. The default bias of 1 keeps the carrier present, making this tremolo rather than ring modulation. |
fun Fn.amplitudeMod(mod: Fn, depth: Any, bias: Any = 1.0): Fn |
Amplitude modulation by bias + depth * mod, taking depth and bias as numbers or Fns and throwing for anything else. |
fun Fn.ringMod(mod: Fn, depth: Fn = param(1.0)): Fn |
Ring modulation: multiplies this function by depth * mod, with no bias term — so wherever the modulator crosses zero the carrier disappears entirely. |
fun Fn.ringMod(mod: Fn, depth: Number): Fn |
Ring modulation: multiplies this function by depth * mod with no bias, so the carrier vanishes wherever the modulator crosses zero. |
fun Fn.warp(mod: Fn, depth: Fn = param(1.0)): Fn |
Warps time by the modulator: evaluates this function at x + depth * mod(x) instead of at x. |
fun Fn.warp(mod: Fn, depth: Number): Fn |
Warps time by the modulator, evaluating this function at x + depth * mod(x) — vibrato and phase modulation in one operator. |
fun Fn.quantizeX(step: Fn = param(1.0), mode: EdgeMode = EdgeMode.CLAMP): Fn |
Quantises the coordinate to a grid of step, so the function is only ever read at grid positions — a staircase in time rather than in value. |
fun Fn.quantizeX(step: Number, mode: EdgeMode = EdgeMode.CLAMP): Fn |
Quantises the coordinate to a grid of step, flooring to the grid so the function reads as a staircase in time. A step of zero passes x through unchanged. |
fun Fn.sampleHold(step: Fn = param(1.0), offset: Fn = param(0.0)): Fn |
Sample-and-hold: reads the function only at multiples of step (offset by offset) and holds each value until the next one — the classic stepped-random or arpeggio effect. |
fun Fn.sampleHold(step: Number, offset: Number = 0.0): Fn |
Sample-and-hold: reads the function only at multiples of step from offset and holds each value until the next. Stateless; a step of zero is a no-op. |
fun Fn.sampleHold(step: Any, offset: Any = 0.0): Fn |
Sample-and-hold on a grid of step from offset, taking either as a number or an Fn and throwing for anything else. |
fun Fn.phaseMod(mod: Fn, depth: Fn = param(1.0)): Fn |
Phase modulation — displaces the read position by depth * mod(x). Exactly warp, under the name the synth literature uses; there is no separate integration step here. |
fun Fn.phaseMod(mod: Fn, depth: Number): Fn |
Phase modulation — displaces the read position by depth * mod(x). Exactly warp, under a synth-flavoured name. |
fun Fn.frequencyMod(mod: Fn, depth: Fn = param(1.0)): Fn |
Frequency modulation — displaces the read position by depth * mod(x). |
fun Fn.frequencyMod(mod: Fn, depth: Number): Fn |
Frequency modulation — displaces the read position by depth * mod(x). The same operation as warp and phaseMod, not an integrating FM. |
fun Fn.saturate(amount: Fn = param(1.0)): Fn |
A gentle saturator, v / (1 + \|v * amount\|) — compresses large values toward ±1 without ever quite reaching a ceiling, so nothing is ever truly flat-topped. |
fun Fn.saturate(amount: Number): Fn |
A gentle saturator, v / (1 + \|v * amount\|): compresses toward ±1 without a hard ceiling, and without normalizing, so it also loses level. An amount of zero gives zero. |
fun Fn.jitterX(amount: Fn = param(0.0), seed: Long = 0L): Fn |
Displaces the read position by a random amount of up to half amount either way — a deterministic wobble in time. |
fun Fn.jitterX(amount: Number, seed: Long = 0L): Fn |
Displaces the read position by up to half amount either way, hashed from seed and floor(x) — so the displacement is constant within each unit band of x. |
fun Fn.repeat(period: Fn = param(1.0), origin: Fn = param(domain.finiteSweep().min), domainOverride: Domain = Domain.ALL): Fn |
Loops this function every period from origin, forever — a periodic tiling of whatever the function does in one period's worth of x. |
fun Fn.repeat(period: Number, origin: Number = domain.finiteSweep().min, domainOverride: Domain = Domain.ALL): Fn |
Loops this function every period from origin, forever, with hard cuts at the joins. The result claims domainOverride — the whole line by default. |
fun Fn.repeat(period: Any, origin: Any = domain.finiteSweep().min, domainOverride: Domain = Domain.ALL): Fn |
Loops this function every period from origin, taking either as a number or an Fn and throwing for anything else. |
fun Fn.mirrorRepeat(period: Fn = param(1.0), origin: Fn = param(domain.finiteSweep().min), domainOverride: Domain = Domain.ALL): Fn |
Tiles this function every period from origin, alternating forwards and backwards — so the result is continuous at the joins where plain repeat would cut. |
fun Fn.mirrorRepeat(period: Number, origin: Number = domain.finiteSweep().min, domainOverride: Domain = Domain.ALL): Fn |
Tiles this function every period from origin, alternating forwards and backwards so the joins are continuous. The full pattern is twice the period long. |
fun Fn.mirrorRepeat(period: Any, origin: Any = domain.finiteSweep().min, domainOverride: Domain = Domain.ALL): Fn |
Tiles this function every period from origin with alternating direction, taking either as a number or an Fn and throwing for anything else. |
fun Fn.tile(count: Int, period: Fn = param(domain.finiteSweep().length), origin: Fn = param(domain.finiteSweep().min)): Fn |
Repeats this function exactly count times from origin and is zero outside that span — the finite counterpart to repeat. |
fun Fn.tile(count: Int, period: Number, origin: Number = domain.finiteSweep().min): Fn |
Repeats this function count times every period from origin, and is zero outside that span. A count of zero or less gives the constant zero. |
fun Fn.tile(count: Int, period: Any, origin: Any = domain.finiteSweep().min): Fn |
Repeats this function count times from origin and is zero outside, taking period and origin as numbers or Fns and throwing for anything else. |
fun Fn.pad(left: Fn = param(0.0), right: Fn = param(0.0), value: Fn = param(0.0)): Fn |
Substitutes value for the function beyond left units below its domain and right units above it — a border of silence around the material, with the original passed through in between. |
fun Fn.pad(left: Number, right: Number = 0.0, value: Number = 0.0): Fn |
Substitutes value beyond left units below the domain and right above it; inside that padded band the original evaluator still applies. |
fun Fn.pad(left: Any, right: Any = 0.0, value: Any = 0.0): Fn |
Substitutes value beyond the padded band, taking each argument as a number or an Fn and throwing for anything else. |
fun Fn.delay(amount: Fn = param(0.0)): Fn |
Delays the function by amount, positive being later — exactly shift, under the name that suits time-series and audio. A constant delay moves the domain with it. |
fun Fn.delay(amount: Number): Fn |
Delays the function by amount, positive being later — exactly shift. A constant delay moves the domain with it. |
fun Fn.echo(delay: Fn = param(1.0), decay: Fn = param(0.5), repeats: Int = 4): Fn |
Sums repeats progressively quieter copies of the function, each delay further on — a feed-forward echo. |
fun Fn.echo(delay: Number, decay: Number = 0.5, repeats: Int = 4): Fn |
Sums repeats progressively quieter copies, each delay further on — a finite feed-forward echo with no feedback or state. A decay above 1 grows instead of fading. |
fun Fn.echo(delay: Any, decay: Any = 0.5, repeats: Int = 4): Fn |
A finite feed-forward echo of repeats copies, taking delay and decay as numbers or Fns and throwing for anything else. |
fun Fn.derivative(step: Double = 1e-3): Fn |
The numerical derivative, by central difference over ±step. |
fun Fn.integral(step: Double = 1e-3, origin: Double = domain.finiteSweep().min): Fn |
The running integral from origin to x, by rectangle summation in steps of step. |
fun Fn.convolve(kernel: Fn, samples: Int = 64, kernelDomain: Domain = kernel.domain, normalize: Boolean = false): Fn |
Convolves this function with kernel, approximated by samples rectangles across the kernel's domain. |
fun Fn.correlate(kernel: Fn, samples: Int = 64, kernelDomain: Domain = kernel.domain): Fn |
Cross-correlates this function with kernel — convolve with the kernel reversed about its own centre, which is the difference between "how does this respond to that" and "how much does this look like that". |
fun Fn.movingAverage(window: Fn = param(1.0), samples: Int = 64): Fn |
A box smoother: the mean of the function over a window-wide span centred on each x. |
fun Fn.movingAverage(window: Number, samples: Int = 64): Fn |
A centred box smoother: the mean over a window-wide span around each x. A window of zero is a no-op; costs samples evaluations per sample. |
fun Fn.blur(sigma: Fn = param(1.0), samples: Int = 64): Fn |
A Gaussian smoother of standard deviation sigma — softer than movingAverage, with no ringing, and the usual choice for taking the edge off a curve. |
fun Fn.blur(sigma: Number, samples: Int = 64): Fn |
A Gaussian smoother of standard deviation sigma, truncated at ±3 sigma and normalized so the level is kept. A sigma of zero is a no-op. |
fun hannWindow(domain: Domain = Domain.DEFAULT): Fn |
The Hann window: a raised cosine reaching zero at both ends of domain and 1 at the centre. |
fun hannWindow(domain: ClosedRange<*>): Fn |
The Hann window over the given range: a raised cosine, zero at both ends and 1 at the centre. |
fun hammingWindow(domain: Domain = Domain.DEFAULT): Fn |
The Hamming window: like hannWindow, but stopping at 0.08 rather than zero at the ends. |
fun hammingWindow(domain: ClosedRange<*>): Fn |
The Hamming window over the given range — a raised cosine that stops at 0.08 rather than zero at the ends. |
fun blackmanWindow(domain: Domain = Domain.DEFAULT): Fn |
The Blackman window: a three-term cosine taper, narrower in the middle than hannWindow and with markedly lower spectral sidelobes. |
fun blackmanWindow(domain: ClosedRange<*>): Fn |
The Blackman window over the given range — a three-term cosine taper reaching zero at both ends, with low spectral sidelobes. |
fun gaussianWindow(domain: Domain = Domain.DEFAULT, sigma: Fn = param(0.4)): Fn |
A Gaussian window centred in domain, with sigma measured as a fraction of the domain rather than in x units. |
fun gaussianWindow(domain: Domain = Domain.DEFAULT, sigma: Number): Fn |
A Gaussian window centred in domain, with sigma as a fraction of the domain's width. It never reaches zero at the ends — use hannWindow if you need it to. |
fun gaussianWindow(domain: ClosedRange<*>, sigma: Fn = param(0.4)): Fn |
A Gaussian window centred in the given range, with sigma as a fraction of its width. It never reaches zero at the ends. |
fun gaussianWindow(domain: ClosedRange<*>, sigma: Number): Fn |
A Gaussian window centred in the given range, with sigma as a fraction of its width. It never reaches zero at the ends. |
fun whiteNoise(seed: Long = 0L, frequency: Fn = param(1.0), amplitude: Fn = param(1.0), domain: Domain = Domain.DEFAULT): Fn |
White noise: an independent random value in -amplitude .. amplitude for each cell of width 1 / frequency, with a hard jump between cells. |
fun whiteNoise(seed: Long = 0L, frequency: Number, amplitude: Number = 1.0, domain: Domain = Domain.DEFAULT): Fn |
White noise: an independent random value in ±amplitude per cell of width 1 / frequency, jumping between cells. Deterministic in seed and position, so it is reproducible and seekable. |
fun whiteNoise(seed: Long = 0L, frequency: Number, amplitude: Number = 1.0, domain: ClosedRange<*>): Fn |
White noise: an independent random value in ±amplitude per cell of width 1 / frequency, jumping between cells. Deterministic in seed and position, so it is reproducible and seekable. |
fun whiteNoise(seed: Long = 0L, frequency: Fn = param(1.0), amplitude: Fn = param(1.0), domain: ClosedRange<*>): Fn |
White noise: an independent random value in ±amplitude per cell of width 1 / frequency, jumping between cells. Deterministic in seed and position, so it is reproducible and seekable. |
fun whiteNoise(seed: Long = 0L, frequency: Any, amplitude: Any = 1.0, domain: Domain = Domain.DEFAULT): Fn |
White noise per cell of width 1 / frequency, taking frequency and amplitude as numbers or Fns and throwing for anything else. |
fun whiteNoise(seed: Long = 0L, frequency: Any, amplitude: Any = 1.0, domain: ClosedRange<*>): Fn |
White noise per cell of width 1 / frequency, taking frequency and amplitude as numbers or Fns and throwing for anything else. |
fun valueNoise(seed: Long = 0L, frequency: Fn = param(1.0), amplitude: Fn = param(1.0), domain: Domain = Domain.DEFAULT): Fn |
Smooth value noise: a random value per lattice cell of width 1 / frequency, interpolated between cells with a smootherstep, so the result is continuous and gently wandering rather than jumping. |
fun valueNoise(seed: Long = 0L, frequency: Number, amplitude: Number = 1.0, domain: Domain = Domain.DEFAULT): Fn |
Smooth value noise: a random value per lattice cell of width 1 / frequency, interpolated between cells so the result is continuous. Range roughly ±amplitude; deterministic in seed and position. |
fun valueNoise(seed: Long = 0L, frequency: Number, amplitude: Number = 1.0, domain: ClosedRange<*>): Fn |
Smooth value noise: a random value per lattice cell of width 1 / frequency, interpolated between cells so the result is continuous. Range roughly ±amplitude; deterministic in seed and position. |
fun valueNoise(seed: Long = 0L, frequency: Fn = param(1.0), amplitude: Fn = param(1.0), domain: ClosedRange<*>): Fn |
Smooth value noise: a random value per lattice cell of width 1 / frequency, interpolated between cells so the result is continuous. Range roughly ±amplitude; deterministic in seed and position. |
fun valueNoise(seed: Long = 0L, frequency: Any, amplitude: Any = 1.0, domain: Domain = Domain.DEFAULT): Fn |
Smooth value noise on a lattice of width 1 / frequency, taking frequency and amplitude as numbers or Fns and throwing for anything else. |
fun valueNoise(seed: Long = 0L, frequency: Any, amplitude: Any = 1.0, domain: ClosedRange<*>): Fn |
Smooth value noise on a lattice of width 1 / frequency, taking frequency and amplitude as numbers or Fns and throwing for anything else. |
fun perlinNoise(seed: Long = 0L, frequency: Fn = param(1.0), amplitude: Fn = param(1.0), domain: Domain = Domain.DEFAULT): Fn |
One-dimensional Perlin-style gradient noise: each lattice point of width 1 / frequency carries a random slope rather than a random height, so the curve passes through zero at every lattice point and bulges between them. |
fun perlinNoise(seed: Long = 0L, frequency: Number, amplitude: Number = 1.0, domain: Domain = Domain.DEFAULT): Fn |
Perlin-style gradient noise on a lattice of width 1 / frequency: zero at every lattice point, bulging between them, and quieter than valueNoise at the same amplitude. |
fun perlinNoise(seed: Long = 0L, frequency: Number, amplitude: Number = 1.0, domain: ClosedRange<*>): Fn |
Perlin-style gradient noise on a lattice of width 1 / frequency: zero at every lattice point, bulging between them, and quieter than valueNoise at the same amplitude. |
fun perlinNoise(seed: Long = 0L, frequency: Fn = param(1.0), amplitude: Fn = param(1.0), domain: ClosedRange<*>): Fn |
Perlin-style gradient noise on a lattice of width 1 / frequency: zero at every lattice point, bulging between them, and quieter than valueNoise at the same amplitude. |
fun perlinNoise(seed: Long = 0L, frequency: Any, amplitude: Any = 1.0, domain: Domain = Domain.DEFAULT): Fn |
Perlin-style gradient noise on a lattice of width 1 / frequency, taking frequency and amplitude as numbers or Fns and throwing for anything else. |
fun perlinNoise(seed: Long = 0L, frequency: Any, amplitude: Any = 1.0, domain: ClosedRange<*>): Fn |
Perlin-style gradient noise on a lattice of width 1 / frequency, taking frequency and amplitude as numbers or Fns and throwing for anything else. |
fun fbm(seed: Long = 0L, frequency: Fn = param(1.0), amplitude: Fn = param(1.0), octaves: Int = 5, lacunarity: Fn = param(2.0), gain: Fn = param(0.5), domain: Domain = Domain.DEFAULT): Fn |
Fractal Brownian motion: octaves layers of valueNoise summed, each layer lacunarity times finer and gain times quieter than the last — noise with detail at several scales at once. |
fun fbm(seed: Long = 0L, frequency: Number, amplitude: Number = 1.0, octaves: Int = 5, lacunarity: Number = 2.0, gain: Number = 0.5, domain: Domain = Domain.DEFAULT): Fn |
Fractal Brownian motion: octaves layers of valueNoise, each lacunarity times finer and gain times quieter. Normalized, so the range stays near ±amplitude however many octaves you add. |
fun fbm(seed: Long = 0L, frequency: Number, amplitude: Number = 1.0, octaves: Int = 5, lacunarity: Number = 2.0, gain: Number = 0.5, domain: ClosedRange<*>): Fn |
Fractal Brownian motion: octaves layers of valueNoise, each lacunarity times finer and gain times quieter. Normalized, so the range stays near ±amplitude however many octaves you add. |
fun fbm(seed: Long = 0L, frequency: Any, amplitude: Any = 1.0, octaves: Int = 5, lacunarity: Any = 2.0, gain: Any = 0.5, domain: Domain = Domain.DEFAULT): Fn |
Fractal Brownian motion over octaves layers of valueNoise, taking the numeric parameters as numbers or Fns and throwing for anything else. |
fun fbm(seed: Long = 0L, frequency: Any, amplitude: Any = 1.0, octaves: Int = 5, lacunarity: Any = 2.0, gain: Any = 0.5, domain: ClosedRange<*>): Fn |
Fractal Brownian motion over octaves layers of valueNoise, taking the numeric parameters as numbers or Fns and throwing for anything else. |
fun fbm(seed: Long = 0L, frequency: Fn = param(1.0), amplitude: Fn = param(1.0), octaves: Int = 5, lacunarity: Fn = param(2.0), gain: Fn = param(0.5), domain: ClosedRange<*>): Fn |
Fractal Brownian motion: octaves layers of valueNoise, each lacunarity times finer and gain times quieter. Normalized, so the range stays near ±amplitude however many octaves you add. |
fun fbmPerlin(seed: Long = 0L, frequency: Fn = param(1.0), amplitude: Fn = param(1.0), octaves: Int = 5, lacunarity: Fn = param(2.0), gain: Fn = param(0.5), domain: Domain = Domain.DEFAULT): Fn |
Fractal Brownian motion built from perlinNoise layers instead of valueNoise ones — fbm with a different grain. |
fun fbmPerlin(seed: Long = 0L, frequency: Number, amplitude: Number = 1.0, octaves: Int = 5, lacunarity: Number = 2.0, gain: Number = 0.5, domain: Domain = Domain.DEFAULT): Fn |
Fractal Brownian motion built from perlinNoise layers: octaves of them, each lacunarity times finer and gain times quieter. Normalized, so the range stays near ±amplitude. |
fun fbmPerlin(seed: Long = 0L, frequency: Number, amplitude: Number = 1.0, octaves: Int = 5, lacunarity: Number = 2.0, gain: Number = 0.5, domain: ClosedRange<*>): Fn |
Fractal Brownian motion built from perlinNoise layers: octaves of them, each lacunarity times finer and gain times quieter. Normalized, so the range stays near ±amplitude. |
fun fbmPerlin(seed: Long = 0L, frequency: Any, amplitude: Any = 1.0, octaves: Int = 5, lacunarity: Any = 2.0, gain: Any = 0.5, domain: Domain = Domain.DEFAULT): Fn |
Fractal Brownian motion over octaves layers of perlinNoise, taking the numeric parameters as numbers or Fns and throwing for anything else. |
fun fbmPerlin(seed: Long = 0L, frequency: Any, amplitude: Any = 1.0, octaves: Int = 5, lacunarity: Any = 2.0, gain: Any = 0.5, domain: ClosedRange<*>): Fn |
Fractal Brownian motion over octaves layers of perlinNoise, taking the numeric parameters as numbers or Fns and throwing for anything else. |
fun fbmPerlin(seed: Long = 0L, frequency: Fn = param(1.0), amplitude: Fn = param(1.0), octaves: Int = 5, lacunarity: Fn = param(2.0), gain: Fn = param(0.5), domain: ClosedRange<*>): Fn |
Fractal Brownian motion built from perlinNoise layers: octaves of them, each lacunarity times finer and gain times quieter. Normalized, so the range stays near ±amplitude. |
fun randomWalk(seed: Long = 0L, step: Fn = param(1.0), amplitude: Fn = param(1.0), domain: Domain = Domain.DEFAULT): Fn |
A random walk: the accumulated sum of one random increment of up to ±amplitude per step of x — Brownian motion, wandering without any tendency to return. |
fun randomWalk(seed: Long = 0L, step: Number, amplitude: Number = 1.0, domain: Domain = Domain.DEFAULT): Fn |
A random walk: the accumulated sum of one increment of up to ±amplitude per step of x. Unbounded, and costs x / step hashes per evaluation — expensive far from zero. |
fun randomWalk(seed: Long = 0L, step: Number, amplitude: Number = 1.0, domain: ClosedRange<*>): Fn |
A random walk: the accumulated sum of one increment of up to ±amplitude per step of x. Unbounded, and costs x / step hashes per evaluation — expensive far from zero. |
fun randomWalk(seed: Long = 0L, step: Fn = param(1.0), amplitude: Fn = param(1.0), domain: ClosedRange<*>): Fn |
A random walk: the accumulated sum of one increment of up to ±amplitude per step of x. Unbounded, and costs x / step hashes per evaluation — expensive far from zero. |
fun randomWalk(seed: Long = 0L, step: Any, amplitude: Any = 1.0, domain: Domain = Domain.DEFAULT): Fn |
A random walk accumulating one increment per step, taking step and amplitude as numbers or Fns and throwing for anything else. |
fun randomWalk(seed: Long = 0L, step: Any, amplitude: Any = 1.0, domain: ClosedRange<*>): Fn |
A random walk accumulating one increment per step, taking step and amplitude as numbers or Fns and throwing for anything else. |
data class Sample(val x: Double, val y: Double, val w: Double = 1.0) |
One (x, y) observation, with an optional weight — the currency of the scattered-data and sampling APIs here. |
fun points(vararg pairs: Pair<Double, Double>): List<Sample> |
Builds a control-point list from x to y pairs, all with weight 1 — the terse way to hand-write input for rbf and friends. |
fun Fn.samplesAt(xs: DoubleArray): List<Sample> |
Observes this function at the given coordinates, as (x, value) Samples. Not domain-checked: each point is a raw evaluation, so coordinates outside the domain may be extrapolated. |
fun Fn.autoScale(sampleDomain: Domain = domain, samples: Int = 256, seed: Long = 0L, targetMin: Double = 0.0, targetMax: Double = 1.0, clamp: Boolean = false): Fn |
Rescales the function so that its observed range lands on targetMin .. targetMax — normalization by measurement. |
fun Fn.autoScale(sampleDomain: ClosedRange<*>, samples: Int = 256, seed: Long = 0L, targetMin: Double = 0.0, targetMax: Double = 1.0, clamp: Boolean = false): Fn |
Rescales the function so its observed range lands on targetMin .. targetMax, measuring over the given range. The range is estimated once from samples probes, so unmeasured peaks can overshoot unless clamp is set. |
fun Fn.zNormalize(sampleDomain: Domain = domain, samples: Int = 256, seed: Long = 0L, epsilon: Double = 1e-9): Fn |
Standardizes the function to zero mean and unit standard deviation — (v - mean) / sd, with both estimated from samples pseudo-random probes at construction. |
fun Fn.zNormalize(sampleDomain: ClosedRange<*>, samples: Int = 256, seed: Long = 0L, epsilon: Double = 1e-9): Fn |
Standardizes the function to zero mean and unit standard deviation, measuring over the given range. A signal whose measured deviation is below epsilon becomes the constant zero. |
fun Fn.whitenCdf(sampleDomain: Domain = domain, samples: Int = 2048, bins: Int = 256, seed: Long = 0L, targetMin: Double = 0.0, targetMax: Double = 1.0, clamp: Boolean = true): Fn |
Histogram equalization: pushes every value through the function's own cumulative distribution, so the output is spread uniformly across targetMin .. targetMax however lopsidedly the input was distributed. |
fun Fn.whitenCdf(sampleDomain: ClosedRange<*>, samples: Int = 2048, bins: Int = 256, seed: Long = 0L, targetMin: Double = 0.0, targetMax: Double = 1.0, clamp: Boolean = true): Fn |
Histogram equalization over the given range: values are pushed through the function's own estimated CDF so the output is spread uniformly across targetMin .. targetMax. Monotone but non-linear, and estimated lazily on first use. |
fun Fn.sampleUniform(count: Int, sampleDomain: Domain = domain, includeEnd: Boolean = true): List<Sample> |
count evenly-spaced observations across sampleDomain, as (x, value) Samples. |
fun Fn.sampleUniform(count: Int, sampleDomain: ClosedRange<*>, includeEnd: Boolean = true): List<Sample> |
count evenly-spaced observations across the given range, as Samples. includeEnd decides whether the last one lands on the upper bound. |
fun Fn.sampleJittered(count: Int, sampleDomain: Domain = domain, jitter: Double = 0.25, seed: Long = 0L): List<Sample> |
count observations, one per equal cell, each displaced from its cell centre by up to half jitter of a cell width — stratified sampling, which covers the domain evenly while avoiding the aliasing a regular grid produces. |
fun Fn.sampleJittered(count: Int, sampleDomain: ClosedRange<*>, jitter: Double = 0.25, seed: Long = 0L): List<Sample> |
count stratified observations across the given range, one per cell, displaced by up to half jitter of a cell width. Deterministic in seed. |
fun Fn.sampleRandom(count: Int, sampleDomain: Domain = domain, seed: Long = 0L): List<Sample> |
count observations at pseudo-random positions across sampleDomain — unstratified, so expect clumps and gaps; sampleJittered is usually the better-behaved choice. |
fun Fn.sampleRandom(count: Int, sampleDomain: ClosedRange<*>, seed: Long = 0L): List<Sample> |
count observations at pseudo-random positions across the given range, unstratified and not sorted by x. Deterministic in seed. |
fun Fn.sampleByDensity(count: Int, sampleDomain: Domain = domain, density: Fn, seed: Long = 0L, maxRejects: Int = 10000): List<Sample> |
Observations concentrated where density is high, by rejection sampling — the way to sample a signal more closely in the regions you care about. |
fun Fn.sampleByDensity(count: Int, sampleDomain: ClosedRange<*>, density: Fn, seed: Long = 0L, maxRejects: Int = 10000): List<Sample> |
Observations concentrated where density is high, by rejection sampling over the given range. May return fewer than count samples, since it gives up after maxRejects attempts. |
fun Fn.sampleZeroCrossings(sampleDomain: Domain = domain, step: Double = sampleDomain.finiteSweep().length / 128.0): List<Sample> |
Finds where the function crosses zero, walking sampleDomain in steps of step and linearly interpolating the crossing between the two straddling samples. Every returned Sample has a y of exactly 0.0. |
fun Fn.sampleZeroCrossings(sampleDomain: ClosedRange<*>, step: Double = sampleDomain.toDomain().length / 128.0): List<Sample> |
Finds sign changes across the given range in steps of step, interpolating each crossing. Tangential touches, and pairs of crossings closer together than one step, are missed. |
fun Fn.samplePeaks(count: Int, sampleDomain: Domain = domain, step: Double = sampleDomain.finiteSweep().length / 256.0): List<Sample> |
Finds the count most prominent local maxima, by three-point comparison on a grid of step. |
fun Fn.samplePeaks(count: Int, sampleDomain: ClosedRange<*>, step: Double = sampleDomain.toDomain().length / 256.0): List<Sample> |
Finds the count most prominent local maxima across the given range, on a grid of step. Maxima only — never troughs — and ranked by absolute value. |
val RbfKernels: afield.math.functions.RbfKernels |
The RbfKernels catalogue, re-exported so RbfKernels.thinPlate resolves inside with(Functions) without an import. |
fun RbfInterpolator(points: List<Sample>, kernel: RbfKernel, smoothing: Double = 0.0, base: Fn? = null): afield.math.functions.RbfInterpolator |
Builds an afield.math.functions.RbfInterpolator from inside a Functions scope, so Sample and Fn need no qualification. Solves its dense system eagerly — see that class for the sizing caveats. |
fun rbf(points: List<Sample>, kernel: RbfKernel = RbfKernels.thinPlate, smoothing: Double = 0.0, base: Fn? = null): Fn |
Fits a smooth curve through scattered control points with radial basis functions — the tool for "make me a function that passes through these", where the points need not be evenly spaced or even sorted. |
fun rbfQuadratic(points: List<Sample>, smoothing: Double = 0.0, base: Fn? = null): Fn |
rbf with the r² kernel — stiffer than thin-plate, and more prone to overshoot away from the data. |
fun rbfCubic(points: List<Sample>, smoothing: Double = 0.0, base: Fn? = null): Fn |
rbf with the r³ kernel — the stiffest of the polynomial family, smooth through the points but wild beyond them. |
fun rbfThinPlate(points: List<Sample>, smoothing: Double = 0.0, base: Fn? = null): Fn |
rbf with the thin-plate kernel — the same as the default, spelled out. The one to reach for first, since it has no shape parameter to mis-scale. |
fun Fn.rbf(points: List<Sample>, kernel: RbfKernel = RbfKernels.thinPlate, smoothing: Double = 0.0): Fn |
Adds an RBF correction to this function so the result passes through points while otherwise following this function's shape — the "nudge this curve through my control points" operation. |
fun Fn.rbfQuadratic(points: List<Sample>, smoothing: Double = 0.0): Fn |
Nudges this function through points with an RBF correction using the r² kernel. See Fn.rbf. |
fun Fn.rbfCubic(points: List<Sample>, smoothing: Double = 0.0): Fn |
Nudges this function through points with an RBF correction using the r³ kernel. See Fn.rbf. |
fun Fn.rbfThinPlate(points: List<Sample>, smoothing: Double = 0.0): Fn |
Nudges this function through points with a thin-plate RBF correction — the default kernel, spelled out. See Fn.rbf. |
fun Fn.granular(grainSize: Fn = param(1.0), spacing: Fn = grainSize, jitter: Fn = param(0.0), overlap: Int = 1, seed: Long = 0L, window: Fn = hannWindow(Domain.DEFAULT)): Fn |
Granular resynthesis: chops the function into windowed grains of grainSize laid down every spacing, and sums them back together. |
fun Fn.granular(grainSize: Any, spacing: Any = grainSize, jitter: Any = 0.0, overlap: Int = 1, seed: Long = 0L, window: Fn = hannWindow(Domain.DEFAULT)): Fn |
Granular resynthesis into windowed grains of grainSize every spacing, taking those and jitter as numbers or Fns and throwing for anything else. overlap bounds how many neighbouring grains can be heard at once. |
fun Fn.lowpassStream(alpha: Double, start: Double = domain.finiteSweep().min, step: Double = 1.0, initial: Double = 0.0): Sequence<Double> |
A stateful one-pole low-pass over the sampled function, as an infinite lazy sequence: y += alpha * (x - y). |
fun Fn.highpassStream(alpha: Double, start: Double = domain.finiteSweep().min, step: Double = 1.0, initial: Double = 0.0): Sequence<Double> |
A stateful one-pole high-pass over the sampled function, as an infinite lazy sequence: y = alpha * (y + x - xPrev). |
fun piecewise(default: Fn = zero(), vararg pieces: Pair<Domain, Fn>): Fn |
Assembles a function from explicitly-placed pieces: at each x the first piece whose range contains it wins, and default covers everything no piece claims. |
data class DemoStats(val min: Double, val max: Double, val mean: Double, val rms: Double) |
|
data class DemoSeries(val name: String, val domain: Domain, val xs: DoubleArray, val ys: DoubleArray, val stats: DemoStats) |
|
data class DemoResult(val series: List<DemoSeries>, val notes: List<String>) |
|
fun piecewiseLinear(points: List<Keyframe>): Fn |
Piecewise-linear interpolation through the keyframe positions. Outside the keyframe range the first/last value is held. |
fun piecewiseLinear(points: List<Vec2>): Fn |
Overload accepting Vec2 pairs instead of Keyframe objects. |
fun catmullRomSpline(points: List<Keyframe>, alpha: Double = 0.5): Fn |
Catmull-Rom spline through keyframe positions. |
fun monotoneCubic(points: List<Keyframe>): Fn |
Monotone cubic (Fritsch-Carlson) interpolation. |
fun hermiteFromHandles(points: List<Keyframe>): Fn |
Cubic Hermite interpolation with user-specified tangent slopes. |
fun bSplineQuadratic(points: List<Keyframe>): Fn |
Quadratic B-spline approximation of the keyframe curve. |
fun stepHold(points: List<Keyframe>): Fn |
Step / sample-and-hold through keyframes. Holds each keyframe value until the next keyframe time. |
fun nearestValue(points: List<Keyframe>): Fn |
Nearest-value interpolation — returns the value of the closest keyframe. |
fun cosineInterp(points: List<Keyframe>): Fn |
Cosine interpolation through keyframes. |
fun smoothStepInterp(points: List<Keyframe>): Fn |
Smoothstep (Hermite) interpolation through keyframes. |
fun smootherStepInterp(points: List<Keyframe>): Fn |
Smootherstep (Ken Perlin's 6t⁵ − 15t⁴ + 10t³) interpolation. |
fun gaussianSmooth(source: Fn, sigma: Double = 0.1, taps: Int = 31): Fn |
Gaussian-smoothed version of a source Fn. |
fun differentiate(source: Fn, h: Double = 1e-4): Fn |
Numerically differentiate a source Fn (central differences). |
fun fromKeyframeEditor(keyframes: List<Keyframe>, sampleCount: Int = 512, interpolation: Interpolation = Interpolation.LINEAR): Fn |
Produces a Fn that exactly matches the keyframe editor's native evaluation (hold / linear / rational-bezier), by constructing a temporary KeyframeEvaluator and sampling it. |
fun resample(source: Fn, sampleCount: Int = 256, interpolation: Interpolation = Interpolation.CUBIC): Fn |
Samples source at sampleCount uniform points across its own domain and returns a table-backed Fn that reconstructs between them with interpolation. |
fun demo(sampleCount: Int = 512, seed: Long = 1L, domain: Domain = Domain.DEFAULT, printSummary: Boolean = true): DemoResult |
|
fun main(args: Array<String>) |
PatternRewriteSmokeTest.kt
Smoke for the pattern-rewrite layer (PatternRewrite), a structural source-to-source rewriter
that lets a box author its own rewrite { ... } rules on top of the PSI macro pass.
Most cases are black-box: they feed a source string (rule plus code) through
PatternRewrite.expand with the standalone FieldKtParser as the PSI environment and assert on
the emitted text, with no REPL. The unquote and fragment-rule cases drive a real
KotlinInterface2 through applyMacros. The cases cover match and no-match, non-linearity,
paren-modulo, the precedence hazard, the quote barrier, first-match-wins declaration order,
guards, variadic holes, unquote, ambient and local rules, and termination of a non-converging
rule.
src/jvmMain/kotlin/afield/math/functions/PatternRewriteSmokeTest.kt — read the source
PatternRewriteSmokeTest
object PatternRewriteSmokeTest
| Declaration | Summary |
|---|---|
fun runTests(): Boolean |
|
fun main(args: Array<String>) |
RbfField.kt
Scattered-data interpolation over two or more dimensions with radial basis functions: RbfField
and its rbfField builder, the multi-dimensional counterpart of Functions.rbf.
src/jvmMain/kotlin/afield/math/functions/RbfField.kt — read the source
Used in: Functions to FLine examples (Field graphics)
Top-level functions
| Declaration | Summary |
|---|---|
fun rbfField(samples: List<Pair<Vec2, Number>>, kernel: RbfKernel = RbfKernels.thinPlate, smoothing: Double = 0.0, trend: Boolean = true): RbfField |
A 2D RbfField through samples, each a point and the value there. The thin-plate kernel and the affine trend are the defaults, so with no shape parameter to choose this is the one to try first. |
RbfField
Radial basis function interpolation in two or more dimensions: a smooth surface through values scattered at points in a plane, or a volume, with no grid. The 1D version, RbfInterpolator behind Functions.rbf, fits a curve through (x, y) samples; this one fits f(point) through (point, value) samples, with the same RbfKernels.
| Declaration | Summary |
|---|---|
val dimension: Int |
The number of coordinates per point: 2 for a surface over a plane. |
val kernel: RbfKernel |
The radial function; distances are in the points' units, divided by each sample's radius. |
val smoothing: Double |
Added to the kernel matrix's diagonal as a ridge term. 0 passes exactly through every sample; a positive value trades that for a calmer surface through noisy data. |
val trend: Boolean |
Whether to fit an affine trend β₀ + β·p alongside the kernels. Needed for the thin-plate kernel to be well posed; switched off automatically while there are fewer than dimension + 1 samples, too few to determine it. |
val size: Int |
How many samples the field passes through. |
fun add(point: DoubleArray, value: Double, radius: Double = 1.0): Int |
Add a sample: value at point, reaching radius times as far as a radius-1 sample. Returns its index. |
fun add(point: Vec2, value: Double, radius: Double = 1.0): Int |
add for a 2D point. |
fun add(x: Double, y: Double, value: Double, radius: Double = 1.0): Int |
add for a 2D point. |
fun removeAt(index: Int) |
Remove the sample at index; later samples move down one. |
fun clear() |
Remove every sample. |
operator fun invoke(point: DoubleArray): Double |
The fitted value at point. With no samples it is 0. |
operator fun invoke(point: Vec2): Double |
The fitted value at point. |
operator fun invoke(x: Double, y: Double): Double |
The fitted value at (x, y). |
fun leaveOneOut(): DoubleArray |
For each sample, the value the field would predict there if that sample were left out and the rest refitted. Computed from one inverse of the system matrix (Rippa's formula), not n refits. Needs at least two samples. |
fun leaveOneOutResiduals(): DoubleArray |
For each sample, its value minus the leaveOneOut prediction there. |
fun leaveOneOutRmse(): Double |
The root mean square of leaveOneOutResiduals: one number for how well the field generalises. |
override fun toString(): String |
math.kt
Number-taking overloads of the trigonometric functions, so sin(1) and sin(1f) work
without writing .toDouble() at every call site.
Covers the circular functions and their inverses (sin, cos, tan, asin, acos, atan,
atan2) and the hyperbolic ones (sinh, cosh, tanh, asinh, acosh, atanh). Each is a
one-line delegate to the matching kotlin.math function and always returns Double.
src/jvmMain/kotlin/afield/math/functions/math.kt — read the source
Top-level functions
| Declaration | Summary |
|---|---|
fun sin(n: Number): Double |
Sine of n radians, in -1..1. Multiply by PI / 180 first if you are thinking in degrees. |
fun cos(n: Number): Double |
Cosine of n radians, in -1..1. |
fun tan(n: Number): Double |
Tangent of n radians; unbounded, and enormous near odd multiples of PI/2. |
fun asin(n: Number): Double |
Arc sine of n, in radians over -PI/2..PI/2. NaN outside the domain -1..1. |
fun acos(n: Number): Double |
Arc cosine of n, in radians over 0..PI. NaN outside the domain -1..1. |
fun atan(n: Number): Double |
Arc tangent of n, in radians over -PI/2..PI/2. For a full-circle angle use atan2. |
fun atan2(y: Number, x: Number): Double |
The angle of the vector (x, y) in radians over -PI..PI, measured counter-clockwise from the positive x axis. |
fun sinh(n: Number): Double |
Hyperbolic sine of n. Unbounded, and overflows to infinity somewhere past n = 710. |
fun cosh(n: Number): Double |
Hyperbolic cosine of n; 1 at zero and rising either side of it. |
fun tanh(n: Number): Double |
Hyperbolic tangent of n, in -1..1. |
fun asinh(n: Number): Double |
Inverse hyperbolic sine of n. Defined for every real input. |
fun acosh(n: Number): Double |
Inverse hyperbolic cosine of n, >= 0. NaN below n = 1. |
fun atanh(n: Number): Double |
Inverse hyperbolic tangent of n. Infinite at ±1 and NaN outside it — the inverse of tanh. |
GaussianProcess.kt
Exact Gaussian-process regression: fit scalar observations (values and, optionally, slopes) and query the posterior mean, variance and joint covariance.
GaussianProcess factors the training covariance once and reuses that Cholesky factor for every
prediction. GpPrediction is a single mean and variance with an interval;
GpFunctional names a value or a derivative to query jointly; GpDerivativeObservation trains on a
measured slope. GpNumerics holds the jitter and size limits, and the jitter actually used is
reported back as jitterUsed. logMarginalLikelihood is the evidence that the fitters in
GpFit.kt maximise.
src/jvmMain/kotlin/afield/math/gp/GaussianProcess.kt — read the source
Used in: Drawing with Gaussian processes
Top-level functions
| Declaration | Summary |
|---|---|
fun gp1D(xs: DoubleArray, values: DoubleArray, kernel: GpKernel = GpKernels.rbf(), noiseStd: Double = 0.0, mean: (Double) -> Double = { 0.0 }, numerics: GpNumerics = GpNumerics(), slopes: List<GpDerivativeObservation> = emptyList()): GaussianProcess |
1D observations may be unsorted, and repeated noisy observations remain independent evidence. |
fun gp1D(xs: DoubleArray, values: DoubleArray, noiseStd: DoubleArray, kernel: GpKernel = GpKernels.rbf(), mean: (Double) -> Double = { 0.0 }, numerics: GpNumerics = GpNumerics(), slopes: List<GpDerivativeObservation> = emptyList()): GaussianProcess |
Per-observation noise: one standard deviation for each value, rather than one for all of them. Slopes carry their own. |
GpNumerics
Numerical regularisation is separate from observation noise, and its actual value is reported.
| Declaration | Summary |
|---|---|
val relativeJitter: Double |
The first jitter tried when the training Cholesky fails, as a fraction of the largest covariance diagonal. |
val maxRelativeJitter: Double |
The ceiling on that jitter, in the same units. Each retry multiplies it by ten, and the fit throws rather than go past this. |
val covarianceTolerance: Double |
Relative slack on kernel symmetry, on a posterior variance that comes out slightly negative, and on the noiseless-consistency check. |
val samplingTolerance: Double |
Relative rank tolerance for the pivoted factorisation behind sampling: it decides when a direction of the joint counts as exactly pinned. |
val maxTrainingSize: Int |
The largest number of observations, values and slopes together, that GaussianProcess.fit accepts. |
val maxJointSize: Int |
The largest joint query GaussianProcess.posterior accepts, and the default point budget of a sampled path. |
GpPrediction
data class GpPrediction(val mean: Double, val variance: Double)
Latent function uncertainty, excluding any future measurement noise.
| Declaration | Summary |
|---|---|
val std: Double |
The standard deviation, the square root of variance. |
fun interval(standardDeviations: Double = 1.96): ClosedFloatingPointRange<Double> |
The predicted mean plus or minus standardDeviations standard deviations; the default 1.96 is the central 95% of a normal. Latent, like the variance it is built from. |
GpGradient
The posterior gradient at one point: its mean vector and full covariance between axes.
| Declaration | Summary |
|---|---|
val mean: DoubleArray |
The mean gradient, one component per input axis. A fresh copy on each read. |
val covariance: Array<DoubleArray> |
Covariance between the gradient components, which the per-axis variances alone would lose. A fresh copy on each read. |
val variance: DoubleArray |
The per-axis variances, the diagonal of covariance. |
GpLeaveOneOut
Predictive distribution of each held-out observation (including its own noise).
GpFunctional
class GpFunctional private constructor(point: DoubleArray, val axis: Int?)
A linear functional of the latent function: its value at point, or its partial derivative along input axis there. Derivatives need a GpDifferentiableKernel.
| Declaration | Summary |
|---|---|
val point: DoubleArray |
Where the functional is evaluated. A fresh copy on each read, so the query cannot be moved after the fact. |
GpDerivativeObservation
A measured slope ∂f/∂xaxis = value at point; train on these alongside values.
| Declaration | Summary |
|---|---|
val point: DoubleArray |
Where the slope was measured. A fresh copy on each read. |
GaussianProcess
Exact dense GP regression in any finite input dimension, with 1D/2D/3D conveniences below. Fit once; the training Cholesky is reused by predictions. No matrix inverse is formed. Samples come from posterior, whose full covariance retains correlations between queries. Training data may include slopes (GpDerivativeObservation); values and slopes can be queried jointly (posterior of GpFunctionals, gradient, derivative). Inputs and outputs are copied. Kernel/mean closures must not change after fitting. The slope of a mean function is taken by central difference; the default zero mean has none.
| Declaration | Summary |
|---|---|
val dimensions: Int |
The input dimension fixed at the fit. Every query point must carry this many coordinates. |
val kernel: GpKernel |
The covariance function this fit used. Querying or training on slopes needs a GpDifferentiableKernel. |
val numerics: GpNumerics |
The jitter and size limits in force. jitterUsed reports what the factorisation actually needed. |
val trainingSize: Int |
Training values and slopes together. |
val jitterUsed: Double |
Added training-diagonal variance in output-squared units; zero when no repair was needed. |
val logMarginalLikelihood: Double |
Log p(observations | kernel, mean, noise), including any reported numerical jitter. |
fun predict(vararg coordinates: Double): GpPrediction |
A point prediction; pass one, two or three coordinates (or *aDoubleArray). |
operator fun invoke(vararg coordinates: Double): Double |
The posterior mean, so a fitted process can be called like the function it estimates. |
fun mean(vararg coordinates: Double): Double |
Mean-only prediction avoids a triangular solve, O(n) kernel work. |
fun variance(vararg coordinates: Double): Double |
The latent variance on its own, by way of a full predict — so it pays the triangular solve that GaussianProcess.mean skips. |
fun derivative(axis: Int, vararg coordinates: Double): GpPrediction |
The latent slope ∂f/∂xaxis at a point, with its uncertainty. |
fun gradient(vararg coordinates: Double): GpGradient |
The latent gradient at a point, with the covariance between its components. |
fun predictObservation(point: DoubleArray, noiseStd: Double): GpPrediction |
Future measurement uncertainty adds noiseStd²; fitting noise never appears here implicitly. |
fun covariance(a: DoubleArray, b: DoubleArray): Double |
Cross-covariance between two latent values, including off-diagonal posterior correlations. |
fun covariance(a: GpFunctional, b: GpFunctional): Double |
Posterior covariance between any two functionals (values or slopes). |
fun marginals(points: Array<DoubleArray>): List<GpPrediction> |
Marginals in O(m n²) time and O(n) working storage, without an m×m query covariance. |
fun marginals(xs: DoubleArray): List<GpPrediction> |
One prediction per x, for a 1D fit, with no m×m query covariance formed. Throws unless the process was fitted in one dimension. |
fun posterior(points: Array<DoubleArray>): GpJoint |
Full latent joint distribution. Query order is preserved; repeated points are allowed. |
fun posterior(xs: DoubleArray): GpJoint |
The full joint over a 1D fit's values at the given xs, so a draw from it is a coherent curve. Throws unless the process was fitted in one dimension. |
fun derivativePosterior(points: Array<DoubleArray>, axis: Int = 0): GpJoint |
The joint slope ∂f/∂xaxis at every point, e.g. a 1D derivative curve with coherent draws. |
fun derivativePosterior(xs: DoubleArray): GpJoint |
The joint slope along a 1D fit's only input axis at each x. Throws unless the process was fitted in one dimension. |
fun posterior(functionals: List<GpFunctional>): GpJoint |
One joint over any mix of values and slopes, in the order given. |
fun condition(points: Array<DoubleArray>, values: DoubleArray, noiseStd: DoubleArray = DoubleArray(values.size), derivatives: List<GpDerivativeObservation> = emptyList()): GaussianProcess |
Add observations and refit, preserving the original mean/kernel/noise contract and any slopes. |
fun leaveOneOut(): GpLeaveOneOut |
Exact leave-one-out identities from the Cholesky; does not refit n times. Values first, then slopes. |
GpFields.kt
Gaussian-process regression of a scalar field sampled at Vec2 or Vec3 points.
gp2D and gp3D turn a list of points and their values into a GaussianProcess, with either one
noise level for every observation or one per observation; the predict overloads read the fitted
field back at a point as a GpPrediction (mean and variance).
src/jvmMain/kotlin/afield/math/gp/GpFields.kt — read the source
Used in: Drawing with Gaussian processes
Top-level functions
GpFit.kt
Choosing GP hyperparameters from data by maximising the log marginal likelihood.
fitGpHyperparameters runs a bounded pattern search in log space over GpParameters and returns a
GpFitResult with the fitted process and whether the search step converged; it finds a local
optimum, so compare several starts on multimodal problems. scanGpEvidence evaluates the evidence
over a fixed list of candidate values and returns a GpEvidenceScan with normalised posterior
weights.
src/jvmMain/kotlin/afield/math/gp/GpFit.kt — read the source
Used in: Drawing with Gaussian processes
Top-level functions
| Declaration | Summary |
|---|---|
fun fitGpHyperparameters(parameters: List<GpParameter>, maxIterations: Int = 100, logTolerance: Double = 1e-3, build: (DoubleArray) -> GaussianProcess): GpFitResult |
Deterministic bounded coordinate pattern search over log parameters. Works with any composed kernel, mean or noise model supplied by build. No hidden normalization, derivatives or native optimizer. Run from several initial settings and compare likelihoods for multimodal models. Each iteration tries both directions on every axis, halving the log step only when none improve. converged means the search step fell below logTolerance, not global optimality. |
fun scanGpEvidence(candidates: DoubleArray, build: (Double) -> GaussianProcess): GpEvidenceScan |
The log marginal likelihood of the model build makes at each of candidates, as a GpEvidenceScan. Every model must have observations. One parameter at a time: whatever else build varies with the candidate is folded into the evidence without being reported. |
GpParameter
data class GpParameter(val name: String, val initial: Double, val lower: Double, val upper: Double)
Positive parameter and explicit bounds, searched in log space. Same units as the model builder.
GpFitResult
A bounded local likelihood fit, not a posterior over hyperparameters or a global optimum.
| Declaration | Summary |
|---|---|
val process: GaussianProcess |
The process built at the best parameters found, ready to predict from. |
val evaluations: Int |
How many models the search built, including the one at the initial setting. |
val iterations: Int |
How many pattern-search passes ran, at most the maxIterations asked for. |
val converged: Boolean |
True when the search step fell below the tolerance, false when the iterations ran out. |
val initialLogMarginalLikelihood: Double |
Log marginal likelihood at the initial setting, to compare against process's. |
val parameters: Map<String, Double> |
The fitted value of each parameter, by name, clamped to the bounds it was given. |
GpEvidenceScan
class GpEvidenceScan internal constructor(candidates: DoubleArray, logEvidence: DoubleArray)
Evidence for each candidate value of one parameter, with every other choice fixed by build. posteriorWeights normalise exp(log evidence): the posterior over the candidates under a uniform prior on the grid. Spread candidates in log space for a scale such as a period or length scale. A broad posterior means the data does not choose between the candidates.
| Declaration | Summary |
|---|---|
val candidates: DoubleArray |
The candidate values that were scanned, in the order they were given. A copy. |
val logEvidence: DoubleArray |
The log marginal likelihood at each candidate, aligned with candidates. A copy. |
val posteriorWeights: DoubleArray |
The posterior over candidates under a uniform prior on the grid: logEvidence exponentiated against its maximum and normalised, so the weights sum to one. |
val best: Double |
The candidate with the largest evidence. The first of them if several tie. |
GpJoint.kt
A finite joint Gaussian over a set of query points: mean, covariance, and correlated samples.
GpJoint is what posterior queries return. It samples through a cached pivoted factor of its
covariance (so rank-deficient covariances sample correctly; samplingRank reports the rank), and
supports linear transforms and added observation noise. Getters return copies.
src/jvmMain/kotlin/afield/math/gp/GpJoint.kt — read the source
GpJoint
A finite joint Gaussian. Defensive getters keep its cached sampling factor consistent.
| Declaration | Summary |
|---|---|
val size: Int |
How many query points this joint covers. Every array here has this length. |
val mean: DoubleArray |
The mean at each query point. A copy. |
val variance: DoubleArray |
The marginal variance at each query point: the diagonal of covariance. |
val covariance: Array<DoubleArray> |
The whole covariance matrix, symmetrised on construction. A copy of every row. |
fun covarianceAt(i: Int, j: Int): Double |
One covariance entry, without copying the matrix covariance would hand back. |
val samplingRank: Int |
The rank of that factor, which is how many standard normals a draw consumes. Below size when the query points leave the covariance rank-deficient — repeated points, or a distribution pinned exactly by the observations. |
val samplingResidualVariance: Double |
Maximum unrepresented marginal variance (numerical rank truncation only). |
fun sample(seed: Long): DoubleArray |
One correlated draw at every query point. The same seed always gives the same draw. |
fun sample(random: Random): DoubleArray |
One correlated draw, taking its randomness from random and advancing it by samplingRank normals. Call it repeatedly on one generator for independent draws. |
fun sample(seed: Long, spread: Double): DoubleArray |
A draw whose departure from the mean is scaled by spread: 0 is the mean, 1 a draw from the distribution itself, above 1 an exaggerated one. Same seed, same direction of departure. |
fun samples(count: Int, seed: Long): Array<DoubleArray> |
One factorisation, then independent whole-function draws. |
fun fromStandardNormal(z: DoubleArray): DoubleArray |
Reuse standard normal coefficients to couple draws. Pivot/rank changes can still cause discontinuities. |
fun linearTransform(weights: Array<DoubleArray>): GpJoint |
Distribution of A f: differences, weighted averages, finite-difference slopes, integrals. |
fun withObservationNoise(noiseStd: DoubleArray): GpJoint |
Distribution of noisy future observations, with independent measurement errors. |
GpKernel.kt
Covariance functions for Field's Gaussian processes, and the GpKernels catalogue of standard ones.
A GpKernel is any pure function of two input points; kernels add, multiply and scale with + and
*. A GpDifferentiableKernel also supplies gradients and Hessians, which derivative observations
and slope queries need, and sums, products and scalings of differentiable kernels stay
differentiable. GpKernels provides RBF, Matérn, rational quadratic, periodic and periodic
mixtures, constant and linear kernels, plus warp for an input transform.
src/jvmMain/kotlin/afield/math/gp/GpKernel.kt — read the source
GpKernel
A covariance function. Implementations and mean functions must be pure and thread safe. Sums, products and scalings of GpDifferentiableKernels are differentiable whatever their static type, so rbf() + matern(1.5) still answers derivative queries.
| Declaration | Summary |
|---|---|
fun covariance(a: DoubleArray, b: DoubleArray): Double |
The prior covariance between the function's values at a and b, in output-squared units. The one method an implementation supplies. |
operator fun plus(other: GpKernel): GpKernel |
A kernel whose covariance is the sum of both: a model that is the sum of two independent functions, such as a slow trend plus fast detail. Differentiable when both sides are. |
operator fun times(other: GpKernel): GpKernel |
A kernel whose covariance is the product of both, which is how a periodic kernel is made to decay or a kernel is restricted to a region. Differentiable when both sides are. |
operator fun times(variance: Double): GpKernel |
This kernel with its covariance scaled by variance, which must be nonnegative. |
GpDifferentiableKernel
interface GpDifferentiableKernel : GpKernel
A kernel that also knows its input derivatives, so a process can be queried for, and trained on, slopes, gradients and curve velocities without finite differences. GpKernels.warp, Matérn ν = 1/2 and Smile kernels are not differentiable.
| Declaration | Summary |
|---|---|
fun covarianceGradient(a: DoubleArray, b: DoubleArray, axis: Int): Double |
∂k(a, b) / ∂baxis: the covariance between f(a) and ∂f/∂xaxis at b. |
fun covarianceHessian(a: DoubleArray, b: DoubleArray, i: Int, j: Int): Double |
∂²k(a, b) / ∂ai ∂bj: the covariance between the slope along i at a and along j at b. |
operator fun plus(other: GpDifferentiableKernel): GpDifferentiableKernel |
The sum of two differentiable kernels, differentiable by linearity. |
operator fun times(other: GpDifferentiableKernel): GpDifferentiableKernel |
The product of two differentiable kernels, differentiated by the product rule. |
override operator fun times(variance: Double): GpDifferentiableKernel |
This kernel scaled by variance, gradients and Hessians scaled with it. |
GpKernels
Covariances in output-squared units. Length scales are in input units, one or one per axis. Every kernel here except Matérn ν = 1/2 and warp is a GpDifferentiableKernel.
| Declaration | Summary |
|---|---|
fun rbf(lengthScale: Double = 1.0, variance: Double = 1.0): GpDifferentiableKernel |
Squared exponential: infinitely mean-square differentiable. |
fun rbf(lengthScales: DoubleArray, variance: Double = 1.0): GpDifferentiableKernel |
The squared exponential with one length scale per input axis, so each axis can vary at its own rate. A single length scale is applied to every axis. |
fun matern(nu: Double = 1.5, lengthScale: Double = 1.0, variance: Double = 1.0): GpKernel |
Matérn ν = 1/2, 3/2 or 5/2: respectively rough, once or twice mean-square differentiable. ν = 3/2 and 5/2 are GpDifferentiableKernels; ν = 1/2 has no derivative process. |
fun matern(nu: Double, lengthScales: DoubleArray, variance: Double = 1.0): GpKernel |
Matérn with one length scale per input axis. nu must be 0.5, 1.5 or 2.5; anything else throws. A single length scale is applied to every axis. |
fun rationalQuadratic(lengthScales: DoubleArray = doubleArrayOf(1.0), alpha: Double = 1.0, variance: Double = 1.0): GpDifferentiableKernel |
A scale mixture of RBF kernels; alpha controls the mixture. |
fun periodic(period: Double = 1.0, lengthScale: Double = 1.0, variance: Double = 1.0): GpDifferentiableKernel |
Exactly periodic along every axis. Multiply by an RBF to let cycles evolve. |
fun periodicMixture(periods: DoubleArray, weights: DoubleArray = DoubleArray(periods.size) { 1.0 }, lengthScale: Double = 1.0, variance: Double = 1.0): GpDifferentiableKernel |
Uncertainty over the period itself: a weighted sum of periodic kernels, one per candidate. Weights are normalised to sum to one, so variance keeps its meaning. |
fun octavePeriodic(centerPeriod: Double, components: Int = 7, octaves: Double = 1.0, lengthScale: Double = 1.0, variance: Double = 1.0): GpDifferentiableKernel |
components candidate periods spread evenly in log₂ over octaves octaves centred on centerPeriod. |
fun constant(variance: Double = 1.0): GpDifferentiableKernel |
The same covariance between every pair of points: one unknown level, drawn once, shared by the whole function. Add it to another kernel so the process can settle away from its mean function instead of returning to it between observations. |
fun linear(variance: Double = 1.0, origin: DoubleArray = doubleArrayOf(0.0)): GpDifferentiableKernel |
Random linear functions through origin, plus a separately composable constant kernel. |
fun warp(kernel: GpKernel, transform: (DoubleArray) -> DoubleArray): GpKernel |
Pull a covariance back through a deterministic input map (e.g. circular coordinates). |
GpOptimizer.kt
Bayesian optimisation over named, bounded parameters: a GP surrogate proposes the next point to try, and you report what it scored.
val opt = GpOptimizer(listOf(GpRange("x", -2.0, 2.0)))
val best = opt.run(20) { p -> val d = p["x"]!! - 0.5; -d * d }
GpRange declares a parameter, GpAcquisition chooses how untried points are scored (expected
improvement, upper confidence bound, probability of improvement), and GpTrial is one evaluated
point. Drive GpOptimizer either with run/step and an objective, or by hand with suggest and
observe when the score comes from somewhere else, such as a person. Covered by GpOptimizerTest.
src/jvmMain/kotlin/afield/math/gp/GpOptimizer.kt — read the source
Used in: Tuning by Gaussian process: optimisation and A/B preference
GpRange
data class GpRange(val name: String, val lower: Double, val upper: Double)
One named, bounded parameter an optimiser or preference model searches over.
GpAcquisition
How a GpOptimizer scores an untried point from the surrogate's mean and standard deviation.
| Declaration | Summary |
|---|---|
data class ExpectedImprovement(val xi: Double = 0.01) : GpAcquisition() |
Expected improvement over the best value seen, less a margin xi (in standardised units). |
data class UpperConfidenceBound(val beta: Double = 2.0) : GpAcquisition() |
Optimism: mean + beta · std. Larger beta explores more. |
data class ProbabilityOfImprovement(val xi: Double = 0.01) : GpAcquisition() |
Probability of improving on the best value seen by more than xi. Greedy; mostly for comparison. |
GpTrial
data class GpTrial(val parameters: Map<String, Double>, val value: Double)
One evaluated point: parameter values by name, and the objective there.
| Declaration | Summary |
|---|---|
operator fun get(name: String): Double |
The value of one parameter by name. An unknown name is an error, not a null. |
GpOptimizer
Bayesian optimisation of an expensive or hand-judged objective over named, bounded parameters.
| Declaration | Summary |
|---|---|
val minimize: Boolean |
Search for the smallest value rather than the largest. |
val acquisition: GpAcquisition |
How an untried point is scored from the surrogate's mean and standard deviation. |
val seed: Long |
Seeds the opening design and the candidate draws, so a run repeats exactly. |
val initialPoints: Int |
How many space-filling suggestions come before the surrogate is used at all. |
val candidates: Int |
How many uniform random points the acquisition is scored at, besides 32 jitters of the best four. |
val refineStarts: Int |
How many of the best candidates are refined by projected gradient ascent. |
val refitEvery: Int |
Fit hyperparameters every this many observations; reuse the last fit in between. |
val parameters: List<GpRange> |
The parameters being searched over, in the order their coordinates are held. |
val dimensions: Int |
How many parameters there are. |
val history: List<GpTrial> |
Everything observed, in order. |
val observations: Int |
How many trials have been observed. |
val best: GpTrial? |
The best observed trial (largest value, or smallest when minimize); null before any. |
val bestSoFar: List<Double> |
Best value after each observation — the convergence trace. |
val hyperparameters: Map<String, Double> |
Fitted kernel length scales (unit-cube units, by parameter name) and noise; empty before a fit. |
fun observe(parameters: Map<String, Double>, value: Double) |
Record that parameters scored value. Parameters missing from the map are an error. |
fun observe(trial: GpTrial) |
Record a whole GpTrial, as returned by best or carried in history. |
fun suggest(): Map<String, Double> |
The next point to evaluate. |
fun suggest(count: Int): List<Map<String, Double>> |
count points to evaluate together. After choosing each, it is provisionally "observed" at the surrogate's own mean (the kriging believer), which shrinks the uncertainty there and pushes the next choice elsewhere. Nothing provisional is kept. |
fun step(objective: (Map<String, Double>) -> Double): GpTrial |
Suggest, evaluate with objective, observe; returns the trial. |
fun run(iterations: Int, objective: (Map<String, Double>) -> Double): GpTrial |
iterations calls of step; returns best. |
fun predict(parameters: Map<String, Double>): GpPrediction? |
Surrogate posterior of the objective at parameters, in the objective's own units; null before data. |
fun acquisitionAt(parameters: Map<String, Double>): Double? |
The acquisition value the next suggest would assign to parameters; null during the initial design. |
GpPaths.kt
Sample paths drawn from a Gaussian process: functions you can evaluate anywhere, consistently, after the draw.
GpSamplePath (from samplePath) is an exact draw extended lazily, conditioned on every point
already evaluated, and limited to a few thousand points. For large fields, GpRbfFeatures builds a
random-Fourier-feature approximation of an RBF kernel; its GpFeatureModel gives a path that costs
the same at every query.
src/jvmMain/kotlin/afield/math/gp/GpPaths.kt — read the source
Used in: Drawing with Gaussian processes
Top-level functions
| Declaration | Summary |
|---|---|
fun GaussianProcess.samplePath(seed: Long, maxPoints: Int = numerics.maxJointSize): GpSamplePath |
One draw from this process as a function you can call anywhere, extended lazily and exactly. maxPoints caps how many distinct locations it will answer for, and it throws past that. Use GpRbfFeatures when a field needs more. |
GpSamplePath
One lazily extended exact GP draw. Repeated queries return the same value; new locations are drawn conditional on all previous locations. The law is independent of query order, but a seed's realised values are not. Mutable, single-threaded, and dense: use a feature path for large fields.
| Declaration | Summary |
|---|---|
val evaluatedPoints: Int |
How many distinct locations have been asked for. The path stops at its point limit. |
operator fun invoke(vararg coordinates: Double): Double |
This draw's value at coordinates, conditioned on every location already asked for. A repeated location returns its cached value; a new one costs work that grows with evaluatedPoints, and throws once the limit is reached. |
GpRbfFeatures
Paired sine/cosine random Fourier features for an ARD RBF kernel. This is an explicit finite-rank approximation, never an automatic substitute for exact inference. The same features are used for fitting, uncertainty and Matheron-rule samples. Feature seeds describe a model, sample seeds describe functions from it. Amplitude is a variance, and length scales are in input units.
| Declaration | Summary |
|---|---|
val dimensions: Int |
How many input coordinates these features expect: the length of lengthScales. |
val featureCount: Int |
The length of a feature vector: two, sine and cosine, for each of frequencyCount. |
fun fit(points: Array<DoubleArray>, values: DoubleArray, noiseStd: DoubleArray = DoubleArray(values.size), mean: (DoubleArray) -> Double = { 0.0 }, numerics: GpNumerics = GpNumerics()): GpFeatureModel |
A GpFeatureModel conditioned on values observed at points, with per-observation noiseStd and an optional prior mean function. Inference is exact for the finite-rank kernel these features define, which approximates the RBF kernel they were drawn for. |
fun prior(mean: (DoubleArray) -> Double = { 0.0 }, numerics: GpNumerics = GpNumerics()): GpFeatureModel |
A model with no observations, for drawing functions from the prior. fit with nothing in it. |
GpFeatureModel
Finite-feature posterior: fast callable means/draws, plus the exact covariance of this approximation.
| Declaration | Summary |
|---|---|
val features: GpRbfFeatures |
The features this model was fitted with; reuse them to build a comparable model. |
val process: GaussianProcess |
The underlying exact process over the finite-rank kernel features define. |
fun mean(vararg point: Double): Double |
The posterior mean at point: a dot product against weights computed once, so it costs the same however many observations the model carries. |
fun predict(vararg point: Double): GpPrediction |
Mean and variance at point, from the underlying process. |
fun samplePath(seed: Long): GpFeaturePath |
Draw a function once, then evaluate it anywhere, in any order, without growing a cache. |
GpFeaturePath
Immutable coefficients; thread safe provided the supplied mean function is thread safe.
| Declaration | Summary |
|---|---|
operator fun invoke(vararg point: Double): Double |
This drawn function's value at point. The same cost at every query, in any order. |
GpPreference.kt
Preference learning from pairwise "A or B?" judgements, using a probit GP over a latent utility.
GpPreference records each GpComparison, estimates the utility of any parameter setting, ranks
what it has seen, and proposes the next pair to ask about with nextPair. It shares GpRange with
GpOptimizer, and suits tuning by eye when no numeric score exists.
src/jvmMain/kotlin/afield/math/gp/GpPreference.kt — read the source
GpComparison
data class GpComparison(val winner: Map<String, Double>, val loser: Map<String, Double>)
One judgement: winner was preferred to loser.
GpPreference
Learns what someone prefers from "A or B?" answers, and proposes the next pair to ask about.
| Declaration | Summary |
|---|---|
val lengthScale: Double |
RBF length scale of the latent utility, in the unit cube every parameter is rescaled to. |
val noise: Double |
How inconsistent the judge is allowed to be. Smaller trusts each answer more. |
val exploration: Double |
How far nextPair's challenger reaches past the mean: it maximises mean + this · std. |
val seed: Long |
Seeds the candidate draws, so the same answers in the same order propose the same pairs. |
val candidates: Int |
How many uniform random candidates go into nextPair's pool, besides 32 jitters of the favourite. |
val parameters: List<GpRange> |
The parameters being compared over, in the order their coordinates are held. |
val dimensions: Int |
How many parameters there are. |
val comparisons: List<GpComparison> |
Every answer so far, in order. |
val newtonIterations: Int |
Newton iterations the last Laplace fit took (0 before any answer). |
fun prefer(winner: Map<String, Double>, loser: Map<String, Double>) |
Record one answer. |
fun choose(pair: Pair<Map<String, Double>, Map<String, Double>>, aWins: Boolean) |
Record one answer by which of a pair won: choose(pair, aWins = true). |
fun utility(parameters: Map<String, Double>): GpPrediction |
Posterior latent utility at parameters (Laplace approximation); the prior before any answer. |
fun utilityRelativeTo(reference: Map<String, Double>, parameters: Map<String, Double>): GpPrediction |
Posterior of u(parameters) − u(reference). Because answers only constrain differences, this is the useful uncertainty to draw: it is zero at the reference and small wherever comparisons tie a point to it, whereas utility's own variance keeps the unconstrained common offset. |
val best: Map<String, Double>? |
The shown point with the largest posterior mean utility; null before any answer. |
fun ranking(): List<Pair<Map<String, Double>, Double>> |
Every point shown so far with its posterior mean utility, best first. |
fun nextPair(): Pair<Map<String, Double>, Map<String, Double>> |
The next two candidates to compare: the favourite, and the most promising challenger. |
GpSmile.kt
Bridges from the Smile library into Field's Gaussian processes: a Smile kernel as a GpKernel, and
Smile's bounded L-BFGS optimizer for fitting hyperparameters.
fitGpHyperparametersWithSmile searches log-parameter space between fixed bounds, rebuilding the
process through a caller-supplied build function, and returns the best process found together
with the projected gradient at the end. GaussianProcessSmileTest covers both bridges.
src/jvmMain/kotlin/afield/math/gp/GpSmile.kt — read the source
Used in: Drawing with Gaussian processes
Top-level functions
| Declaration | Summary |
|---|---|
fun MercerKernel<DoubleArray>.asGpKernel(): GpKernel |
Use an existing Smile covariance unchanged in Field's exact inference/sampling machinery. This calls k(a,b), not Smile's fitted regression or global random source. The supplied kernel must be immutable, positive semidefinite, and accept the GP's input dimension. |
fun fitGpHyperparametersWithSmile(parameters: List<GpParameter>, maxIterations: Int = 100, tolerance: Double = 1e-5, build: (DoubleArray) -> GaussianProcess): GpSmileFitResult |
Smile's bounded L-BFGS optimizer, with bounded finite differences in log-parameter space. Unlike Smile's regression fitter, the objective here includes Field's per-observation noise, composed kernels and mean functions. Fixed positive bounds are required; zero-noise constraints can be held fixed in build. A native optimizer failure is propagated, never hidden. |
GpSmileFitResult
Smile L-BFGS-B result. A small projected gradient indicates local stationarity, not a global optimum.
| Declaration | Summary |
|---|---|
val process: GaussianProcess |
The process rebuilt at the best parameters seen, ranked by marginal likelihood rather than by where the optimizer stopped. |
val parameters: Map<String, Double> |
Those parameters by name, exponentiated back out of log space and held inside their bounds. |
val evaluations: Int |
How many times fitGpHyperparametersWithSmile called its build function, the finite differences included. |
val projectedGradientMax: Double |
The largest gradient component at the end, counting a component the optimizer is holding against a bound as zero. |
GpVectorProcess.kt
Vector-valued Gaussian processes with separable covariance, for fitting curves and fields whose outputs are several coordinates.
GpVectorProcess lifts each observation to (input, output index) and reuses the exact scalar
GaussianProcess, so cost is cubic in observations times output dimensions. With a
GpDifferentiableKernel it trains on and answers derivatives through
GpVectorDerivativeObservation and derivativePosterior. GpVectorJoint is the joint posterior
over a set of points, with per-point means, covariances and samples.
src/jvmMain/kotlin/afield/math/gp/GpVectorProcess.kt — read the source
GpVectorDerivativeObservation
A measured vector derivative ∂f/∂xaxis (every output coordinate) at point.
| Declaration | Summary |
|---|---|
val point: DoubleArray |
Where the derivative was measured. A copy. |
val value: DoubleArray |
The measured derivative, one entry per output coordinate. A copy. |
GpVectorProcess
A vector-valued GP with separable covariance k(input,input') * outputCovariance[axis,axis']. Identity output covariance gives independent coordinates; a PSD matrix gives correlated outputs. This uses exact scalar inference on lifted (input, output-index) observations, so costs are cubic in observationCount * outputDimensions. Use scalar models for large independent-output datasets. With a GpDifferentiableKernel it also answers, and trains on, input derivatives (velocities).
| Declaration | Summary |
|---|---|
fun mean(vararg point: Double): DoubleArray |
The posterior mean vector at point, one entry per output coordinate. |
fun variance(vararg point: Double): DoubleArray |
The marginal variance of each output coordinate at point. Coordinates may be correlated, which this does not say: for that, ask covariance or a posterior. |
fun covariance(a: DoubleArray, b: DoubleArray): Array<DoubleArray> |
The output-by-output covariance between the vectors at a and b. |
fun posterior(points: Array<DoubleArray>): GpVectorJoint |
The joint posterior over the vectors at points — means, covariances and correlated samples together. Limited by the scalar joint size divided by the output dimensions. |
fun derivativePosterior(points: Array<DoubleArray>, axis: Int = 0): GpVectorJoint |
The vector derivative ∂f/∂xaxis at every point, jointly (e.g. a curve's velocity). |
GpVectorJoint
class GpVectorJoint internal constructor(val dimensions: Int, val joint: GpJoint)
Input-major joint: (point0-axis0, point0-axis1, ..., point1-axis0, ...).
| Declaration | Summary |
|---|---|
val size: Int |
How many input points this joint covers, each carrying dimensions outputs. |
val mean: Array<DoubleArray> |
The mean vector at each point, outermost index the point. |
val variance: Array<DoubleArray> |
The marginal variance of each output coordinate at each point. |
fun covarianceAt(i: Int, j: Int = i): Array<DoubleArray> |
The output-by-output covariance between points i and j, which defaults to the block at one point: how that point's own coordinates covary. |
fun sample(seed: Long): Array<DoubleArray> |
One draw: a vector at every point, correlated across both points and coordinates. |
fun sample(random: java.util.Random): Array<DoubleArray> |
The same draw, taking its randomness from random; call it repeatedly for independent draws. |
fun sample(seed: Long, spread: Double): Array<DoubleArray> |
See GpJoint.sample: spread scales the departure from the mean. |
fun projectedVariance(index: Int, direction: DoubleArray): Double |
Variance of the scalar projection direction·value at one input. Direction need not be unit. |
DualQuaternionf.kt
Rigid 3D transforms (rotation plus translation) as mutable unit dual quaternions, with blending.
DualQuaternionf is built from a rotation and a translation, composes with *, transforms points
with transform or *, and converts to a Matrix4f with toMatrix. Two transforms blend with
nlerp (fast, normalised component blend) or sclerp (screw-linear interpolation, which moves at
constant speed along a single screw motion); pow scales a transform by a fraction. Both blends
take the shorter path when the two quaternions have opposite signs.
Methods without a dest argument change the receiver and return it, following the rest of
afield.math.linalg.
src/jvmMain/kotlin/afield/math/linalg/DualQuaternionf.kt — read the source
DualQuaternionf
A mutable single-precision unit dual quaternion for rigid 3D transforms.
Geometry.kt
Closest-point, containment and intersection tests for segments, triangles, boxes, spheres and rays.
Each function takes read-only vectors and writes its answer into a caller-supplied dest, or
returns a Boolean or distance, so none of them allocates. Boxes are axis-aligned and given as a
minimum and maximum corner. Segment and triangle functions handle a zero-length segment or a
collapsed triangle explicitly instead of dividing by zero.
afield.math.linalg.arena has allocation-free versions of some of these that work on arena
vectors inside frameScratch.
src/jvmMain/kotlin/afield/math/linalg/Geometry.kt — read the source
Used in: Linear algebra in Field
Top-level functions
ArenaGeometry.kt
Arena-vector versions of a few afield.math.linalg geometry tests, for use inside frameScratch.
Covers the closest point on a segment and on an axis-aligned box, point-in-box, and ray-box intersection. Each one reads and writes arena vectors, so it allocates nothing on the heap.
src/jvmMain/kotlin/afield/math/linalg/arena/ArenaGeometry.kt — read the source
Top-level functions
| Declaration | Summary |
|---|---|
fun FrameScope.closestPointOnSegment(point: ArenaVec3f, a: ArenaVec3f, b: ArenaVec3f, dest: ArenaVec3f): ArenaVec3f |
Allocation-free arena form of afield.math.linalg.closestPointOnSegment. |
fun FrameScope.closestPointOnAabb(point: ArenaVec3f, minimum: ArenaVec3f, maximum: ArenaVec3f, dest: ArenaVec3f): ArenaVec3f |
Allocation-free arena form of afield.math.linalg.closestPointOnAabb. |
fun FrameScope.containsPointAabb(point: ArenaVec3f, minimum: ArenaVec3f, maximum: ArenaVec3f): Boolean |
|
fun FrameScope.intersectRayAabb(origin: ArenaVec3f, direction: ArenaVec3f, minimum: ArenaVec3f, maximum: ArenaVec3f, nearFar: ArenaVec2f, epsilon: Float = 1e-12f): Boolean |
Writes forward (near, far) ray distances without allocating heap vectors. |
FrameArena.kt
Per-thread scratch memory for temporary vectors, reclaimed all at once when a scope ends.
frameScratch runs a block against the current thread's FrameArena; vectors allocated inside it
become invalid when the block returns. Scopes nest, each restoring its own ArenaMark. Use an
explicit FrameArena (and close it) for short-lived threads, since FrameArena.current keeps its
native storage until the thread calls FrameArena.closeCurrent.
With debug checks on (the default), using a vector after its scope has ended, or from another thread, throws instead of reading reused memory.
src/jvmMain/kotlin/afield/math/linalg/arena/FrameArena.kt — read the source
Top-level functions
| Declaration | Summary |
|---|---|
inline fun <R> frameScratch(block: FrameScope.() -> R): R |
Runs an allocation-free, non-suspending scratch region on this platform thread. Nested calls are supported and restore their arena marks in LIFO order. |
FrameArena
Thread-confined, resettable scratch storage for generated linear-algebra values.
| Declaration | Summary |
|---|---|
val debugChecks: Boolean |
|
var highWaterBytes: Int |
|
var allocationCount: Long |
|
var growthCount: Long |
|
var bytesCopiedDuringGrowth: Long |
|
var resetCount: Long |
|
val capacityBytes: Int |
|
val usedBytes: Int |
|
val isClosed: Boolean |
|
val checksEnabled: Boolean |
|
inline fun <R> scoped(block: FrameScope.() -> R): R |
Runs a non-suspending scratch region and invalidates everything allocated in the block when it returns. |
fun reset() |
|
fun mark(): ArenaMark |
|
fun restore(mark: ArenaMark) |
|
override fun close() |
ArenaMark
value class ArenaMark internal constructor(internal val token: Long)
linalg.kt
The short names for Field's vector and rotation types (Vec2, Vec3, Vec4, Quat,
DualQuat) and the vec, quat and dualquat constructors that build them.
vec(...) takes numbers, copies an existing vector, or concatenates a scalar with a smaller one.
quat turns an angle in radians and an axis into a rotation, and dualquat pairs a rotation with
a translation to give a rigid transform. v3 lifts a 2D vector into 3D with z = 0 and v2
drops the z again, for the float and the double types alike.
The rest of the file is the arithmetic the generated types do not cover: scalar times vector,
vector times quaternion, vector times dual quaternion, the x infix cross product, isNaN for
the 2D and 3D vectors, and %= for Quat and DualQuat. median(index) reads a single
component's median across a collection of vectors and returns that one Float.
These types are mutable and most operations write into the receiver, so copy first
(Vec3(existing)) when the original still matters. Swizzles and the per-vector invoke, %= and
len operators are absent here because tools/linalg-generator emits them for every float vector
into Swizzles.kt and VectorOperators.kt; %= for Quat and DualQuat is written out here
because no generator covers those two.
Point-cloud geometry built on these types lives elsewhere: centroids, medians and bounding boxes
in linalg_spatial.kt, convex hulls in linalg_mesh.kt.
src/jvmMain/kotlin/afield/math/linalg/linalg.kt — read the source
Used in: FLine examples (Field graphics), A Drawn Morph, Heard, Datalog in a box
Top-level functions
| Declaration | Summary |
|---|---|
fun Collection<Vec2>.median(index: Int = 0): Float |
The median of one component across the collection: index 0 for x, 1 for y. |
fun Collection<Vec3>.median(index: Int = 0): Float |
The median of one component: index 0 for x, 1 for y, 2 for z. |
fun Collection<Vec4>.median(index: Int = 0): Float |
The median of one component: index 0 for x, 1 for y, 2 for z, 3 for w. |
fun vec(x: Number, y: Number) |
Creates a mutable 2D Vec2 from numeric coordinates, converting each component to Float. |
fun vec(x: Number, y: Number, z: Number) |
Creates a mutable 3D Vec3 from numeric coordinates, converting each component to Float. |
fun vec(x: Number, y: Number, z: Number, w: Number) |
Creates a mutable 4D Vec4 from numeric coordinates, converting each component to Float. |
fun vec(v: Vector2fc) |
Copies a 2D vector into a new mutable Vec2. |
fun vec(v: Vector3fc) |
Copies a 3D vector into a new mutable Vec3. |
fun vec(v: Vector4fc) |
Copies a 4D vector into a new mutable Vec4. |
fun vec(v: Vec2, z: Number) |
A Vec3 from a Vec2 and a z — the "give this planar point a height" constructor. Copies v. |
fun vec(x : Number, yz: Vec2) |
A Vec3 from a leading scalar and a Vec2 tail, GLSL's vec3(x, yz). Note the argument order. |
fun vec(x: Number, yzw: Vec3) |
A Vec4 from a leading scalar and a Vec3 tail, GLSL's vec4(x, yzw). |
operator fun Quat.remAssign(a: Quat) |
q %= other — overwrite this rotation in place with another's, keeping the same instance. |
operator fun DualQuat.remAssign(a: DualQuat) |
t %= other — overwrite this rigid transform in place, for the same reasons as Quat.remAssign. |
infix fun Vector3f.x(b: Vector3f) |
Cross product as an infix operator: a x b, a vector perpendicular to both, right-handed. |
operator fun Number.times(s: Vector3fc): Vector3f |
2 * v — scalar on the left, so the arithmetic reads the way it does on paper. New Vec3. |
operator fun Number.times(s: Vector2fc): Vector2f |
2 * v with the scalar on the left, for Vec2. New vector; operands untouched. |
operator fun Number.times(s: Vector4fc): Vector4f |
2 * v with the scalar on the left, for Vec4. New vector; operands untouched. |
fun Vector3dc.isNaN() |
True if any component of this 3D double vector is NaN — the check == cannot do for you. |
fun Vector2dc.isNaN() |
True if either component of this 2D double vector is NaN. |
fun Vector3fc.isNaN() |
True if any component of this Vec3 is NaN. |
fun Vector2fc.isNaN() |
True if either component of this Vec2 is NaN. Infinities are not covered. |
operator fun Vector3fc.times(v: Quaternionfc): Vector3f |
p * q — the point rotated by the quaternion, into a new Vec3. Neither operand is modified. |
operator fun Vector3fc.times(v: DualQuaternionf): Vector3f |
p * t — the point moved by the rigid transform (rotate then translate), into a new Vec3. |
operator fun Vector2fc.times(v: Quaternionfc): Vector2fc |
p * q for a planar point: rotates (x, y, 0) by the quaternion and returns the xy of the result. |
fun quat(angle: Number = 0.0, axis: Vector3f = vec(0, 0, 1)) |
A rotation of angle radians about axis, as a Quat. |
fun dualquat(rotation: Quaternionfc = Quaternionf(), translation: Vector3fc = vec(0, 0, 0)) |
A rigid transform from a rotation and a translation — the everyday DualQuat constructor. |
fun dualquat(real: Quaternionfc, dual: Quaternionfc) |
A DualQuat from its raw real and dual parts, normalized on the way out. |
Top-level values
| Declaration | Summary |
|---|---|
val Vector2dc.v3: Vector3d |
This 2D double vector lifted into 3D with z = 0, as a new mutable vector. |
val Vector3dc.v2: Vector2d |
This 3D double vector with its z dropped, as a new mutable 2D vector — a projection, not a copy. |
val Vector2fc.v3: Vector3f |
This Vec2 lifted into 3D with z = 0, as a new mutable vector. |
val Vector3fc.v2: Vector2f |
This Vec3 with its z dropped, as a new mutable Vec2 — flattening, not a perspective divide. |
Vec4
Field's 4D vector alias, backed by Field's generated mutable Vector4f.
Vec3
Field's 3D vector alias, backed by Field's generated mutable Vector3f.
Vec2
Field's 2D vector alias, backed by Field's generated mutable Vector2f.
Quat
Field's rotation alias, backed by the generated mutable Quaternionf — a rotation with no position, and no gimbal lock.
DualQuat
typealias DualQuat = DualQuaternionf
Field's rigid-transform alias, backed by the generated mutable DualQuaternionf — a rotation and a translation in one value.
MatrixProfile.kt
Matrix profiles of a single series: for every subsequence, the distance to its nearest
neighbour. The result, MatrixProfile, yields motifs (repeated shapes) and discords (shapes
that occur once).
matrixProfile computes a self-join or, given two series, an AB-join; joinBothWays returns
both directions of a join, and distanceProfile the distances from one query to every
subsequence of a series. Its motifs and discords return Motifs and Discords. The
multichannel and contrast variants are in MultiDimensional.kt. Tests: MatrixProfileTest.
src/jvmMain/kotlin/afield/math/matrixprofile/MatrixProfile.kt — read the source
Used in: Motifs, Discords and Contrast: the Matrix Profile
Top-level functions
| Declaration | Summary |
|---|---|
fun defaultExclusion(m: Int): Int |
Default trivial-match exclusion zone: a quarter of the window, as in STUMPY and SCAMP. |
fun matrixProfile(series: List<Number>, m: Int, exclusion: Int = defaultExclusion(m), fraction: Double = 1.0, seed: Long = 0): MatrixProfile+2 overloads · also DoubleArray, FloatArray |
The self-join matrix profile of series with subsequence length m. |
fun matrixProfile(a: List<Number>, b: List<Number>, m: Int, fraction: Double = 1.0, seed: Long = 0): MatrixProfile+1 overload · also DoubleArray |
The join matrix profile: for every subsequence of a, its nearest neighbour in b. There is no exclusion zone, since the two series are different. The profile of b against a comes out of the same pass; ask for it with joinBothWays. |
fun joinBothWays(a: DoubleArray, b: DoubleArray, m: Int, fraction: Double = 1.0, seed: Long = 0): Pair<MatrixProfile, MatrixProfile> |
The join profiles of a against b and of b against a, from one pass over the distance matrix. |
fun distanceProfile(query: DoubleArray, series: DoubleArray): DoubleArray |
The z-normalized Euclidean distance from query to every subsequence of series of the same length, by Mueen's MASS: one FFT convolution for all the dot products, so O(n log n). |
MatrixProfile
The matrix profile of a series (Yeh, Keogh et al., 2016): for every subsequence of length m, the z-normalized Euclidean distance to its nearest neighbour somewhere else in the series (or, for a join, in another series) and where that neighbour is.
| Declaration | Summary |
|---|---|
val m: Int |
The subsequence length. |
val distances: DoubleArray |
Nearest-neighbour distance per subsequence start, length n - m + 1. |
val indices: IntArray |
Start of the nearest neighbour per subsequence, or -1. |
val leftDistances: DoubleArray |
Nearest neighbour that starts earlier (self-joins only; empty for a join). |
val leftIndices: IntArray |
Index of leftDistances' neighbour, or -1. |
val rightDistances: DoubleArray |
Nearest neighbour that starts later (self-joins only; empty for a join). |
val rightIndices: IntArray |
Index of rightDistances' neighbour, or -1. |
val exclusion: Int |
Half-width of the trivial-match exclusion zone used. |
val series: DoubleArray |
The series the subsequences come from. |
val other: DoubleArray |
For a join, the series the neighbours come from; for a self-join, series. |
val fraction: Double |
Fraction of the distance matrix's diagonals evaluated (1.0 = exact). |
val size: Int |
Number of subsequences. |
val isSelfJoin: Boolean |
True for a self-join. |
val profile: List<Double> |
distances as a list, ready for showSignals("mp" to mp.profile). Infinite values become NaN. |
fun motifs(count: Int = 3, radius: Double = 2.0, maxMembers: Int = 10, maxDistance: Double = Double.POSITIVE_INFINITY): List<Motif> |
The count best motifs, best first. Each starts from the closest remaining pair in the profile; its members are every subsequence within radius × pair distance of the first (at most maxMembers, no two trivially overlapping). Members of a motif, and their exclusion zones, are not used again. |
fun discords(count: Int = 3): List<Discord> |
The count most unusual subsequences, most unusual first, none trivially overlapping. |
fun subsequence(i: Int): DoubleArray |
The subsequence starting at i. |
override fun toString(): String |
Motif
A repeated shape: where it occurs (closest first), each occurrence's distance from seed, the distance of the pair it was found from, and the subsequence it was grown from.
Discord
data class Discord(val index: Int, val distance: Double, val neighbour: Int)
An unusual shape: where it starts, how far it is from its nearest neighbour, and where that is.
MultiDimensional.kt
Matrix profiles across several channels and between two classes of examples, built on the
single-series MatrixProfile.
multiMatrixProfile finds motifs that appear in only some channels of a multichannel series
and reports which channels share them (MultiMatrixProfile, MultiMotif). contrastProfile
finds shapes that repeat in a set of positive examples and are absent from a set of negative
ones (ContrastProfile, Plato). Tests: MatrixProfileTest.
src/jvmMain/kotlin/afield/math/matrixprofile/MultiDimensional.kt — read the source
Used in: Motifs, Discords and Contrast: the Matrix Profile
Top-level functions
| Declaration | Summary |
|---|---|
fun multiMatrixProfile(channels: List<DoubleArray>, m: Int, exclusion: Int = defaultExclusion(m)): MultiMatrixProfile |
The multidimensional self-join matrix profile of channels (all the same length) with subsequence length m. Exact, O(d · n² + d log d · n²) — the per-channel covariance recurrence of matrixProfile run in lock-step over every channel, with the channels sorted per cell. |
fun multiMatrixProfile(channels: List<List<Number>>, m: Int, exclusion: Int = defaultExclusion(m)): MultiMatrixProfile |
multiMatrixProfile for channels given as lists. |
fun contrastProfile(positive: List<DoubleArray>, negative: List<DoubleArray>, m: Int, exclusion: Int = defaultExclusion(m)): ContrastProfile |
The ContrastProfile of the positive examples against the negative ones. |
fun contrastProfile(positive: DoubleArray, negative: DoubleArray, m: Int, exclusion: Int = defaultExclusion(m)): ContrastProfile |
contrastProfile for one positive and one negative series. |
MultiMatrixProfile
The multidimensional matrix profile of a d-channel series (mSTAMP, Yeh, Kavantzas & Keogh, 2017). A motif in many channels is rarely in all of them: a gesture shows up in the wrist's x and z but not y, a fault in two of eight sensors. So instead of one profile there are d: profile k (1-based, profile) is, for every subsequence, the smallest mean distance to a neighbour over the best k channels, choosing the channels per pair.
| Declaration | Summary |
|---|---|
val m: Int |
The subsequence length. |
val channels: List<DoubleArray> |
The channels, each the same length. |
val exclusion: Int |
Half-width of the trivial-match exclusion zone used. |
val dimensions: Int |
Number of channels. |
val size: Int |
Number of subsequences. |
fun distances(k: Int): DoubleArray |
Distances of the k-channel profile (1..dimensions). |
fun indices(k: Int): IntArray |
Neighbour indices of the k-channel profile. |
fun profile(k: Int): List<Double> |
The k-channel profile as a plot-ready list (infinities become NaN). |
fun asMatrixProfile(k: Int, channel: Int = 0): MatrixProfile |
The k-channel profile as a one-dimensional MatrixProfile over channel channel, so MatrixProfile.motifs-style questions can be asked of it — the distances are the k-channel ones. |
fun subspace(k: Int, i: Int): IntArray |
The k channels, most similar first, in which subsequence i matches its k-channel neighbour. |
fun motifs(k: Int, count: Int = 3, radius: Double = 2.0, maxMembers: Int = 10): List<MultiMotif> |
The count best k-channel motifs. Each grows from the closest remaining pair; its members are subsequences within radius × that pair's distance, measured as the mean distance over the pair's own subspace. |
fun discords(k: Int, count: Int = 3): List<Discord> |
The count most unusual k-channel subsequences. |
override fun toString(): String |
MultiMotif
A motif in a subset of channels: occurrences, their distances, the seed pair's distance and the channels it lives in.
ContrastProfile
The contrast profile (Mercer, Alaee, Abdoli, Singh, Murillo, Keogh, 2021): which shapes occur in the positive examples and not in the negative ones.
| Declaration | Summary |
|---|---|
val m: Int |
The subsequence length. |
val contrast: DoubleArray |
Contrast per subsequence of the concatenated positives; NaN where undefined. |
val self: MatrixProfile |
Self-join of the concatenated positives. |
val join: MatrixProfile |
Join of the concatenated positives against the concatenated negatives. |
val segmentStarts: IntArray |
Start of each positive example within the concatenation (examples are separated by one NaN). |
val profile: List<Double> |
contrast as a plot-ready list. |
val positive: DoubleArray |
The concatenated positives that contrast indexes. |
fun platos(count: Int = 1): List<Plato> |
The count highest-contrast subsequences, best first, none trivially overlapping. |
fun subsequence(i: Int): DoubleArray |
The positive subsequence starting at i. |
Plato
A high-contrast subsequence: where it starts in the concatenated positives, its contrast, which positive example that is and where within it, and its nearest neighbours in the positives and the negatives.
LeastSquares.kt
A Levenberg–Marquardt nonlinear least-squares solver, pure Kotlin and self-contained.
leastSquares takes an initial parameter vector and a residuals: (DoubleArray) -> DoubleArray
lambda and minimises half the squared residual norm. LeastSquaresOptions controls the
iteration cap, the damping schedule and three convergence tolerances; LeastSquaresResult
reports the parameters, final residuals, cost, iteration count, an rms, and a reason string
— one of "gradient", "step", "improvement", "no accepted step" or "max iterations".
The Jacobian is central finite differences by default, so you never have to supply
derivatives, but each iteration then costs 2 * parameters extra residual evaluations. Pass an
explicit jacobian — or, easier, write the residuals in AD arithmetic and call
leastSquaresAD, which derives an exact Jacobian by reverse-mode automatic differentiation — to
trade those 2n evaluations for one backward sweep and get derivatives that are exact rather
than truncated.
The damped normal equations are solved by Gaussian elimination with partial pivoting; a singular system or a non-finite candidate raises the damping and retries (up to 12 times per iteration) rather than failing. Non-finite initial residuals throw.
src/jvmMain/kotlin/afield/math/optimization/LeastSquares.kt — read the source
Top-level functions
| Declaration | Summary |
|---|---|
fun leastSquares(initial: DoubleArray, options: LeastSquaresOptions = LeastSquaresOptions(), jacobian: ((DoubleArray) -> Array<DoubleArray>)? = null, residuals: (DoubleArray) -> DoubleArray): LeastSquaresResult |
Levenberg-Marquardt minimisation of half the squared residual norm. |
fun leastSquaresAD(initial: DoubleArray, options: LeastSquaresOptions = LeastSquaresOptions(), residuals: (List<AD>) -> List<AD>): LeastSquaresResult |
leastSquares with an exact Jacobian, from residuals written once in AD arithmetic. |
LeastSquaresOptions
LeastSquaresResult
| Declaration | Summary |
|---|---|
val x: DoubleArray |
|
val residuals: DoubleArray |
|
val cost: Double |
|
val iterations: Int |
|
val converged: Boolean |
|
val reason: String |
|
val rms: Double |
Improviser.kt
Playing a sequence memory back as a new performance: when to leave the passage being played, how to join the next one, and how to tell whether what came out still resembles the recording.
Improviser advances one sample at a time. It follows the recording, and once it has stayed on
a passage for at least a phrase it may jump to another that shares context, joining the two
with a correction that starts at the discontinuity and fades out. Every sample it emits is
taken from the memory, and Performance records where from: the memory position behind each
output sample, the size of the correction applied there, which channels the performer held, and
a Segment wherever the source changed, carrying the Reason it changed.
Performance also carries the measurements, since whether a recombination still sounds like
the recording is not something the player can answer about itself. reuse and coverage count
memory positions; outputRepetition looks at the output alone, and so sees a loop assembled
from copies of one gesture taken from different places in memory, which reuse cannot;
joinRoughness and accelerationDistance compare the result against the original recording.
ContinuationJudge is declared here and implemented elsewhere: a second opinion that rates
proposed continuations, which Improviser either folds into its own scores or, in veto mode,
uses only to drop candidates. afield.math.temporal.ForecastJudge is the one that ships. A
judge can change which passage is played next; it proposes no samples of its own.
Policy selects between the oracle and two baselines (jumping to the nearest match in recent
history, with no oracle structure, and looping each episode unchanged), so a recombination can
be compared against doing less. Tests: VariableMarkovOracleTest, SeveralTakesTest and
ContinuationJudgeTest.
src/jvmMain/kotlin/afield/math/oracle/Improviser.kt — read the source
Used in: Continuing a Gesture: Sequence Memory and Forecasting
Policy
How an Improviser chooses where to go next.
Constants: Loop, NearestHistory, Oracle.
ContinuationJudge
Something that knows how a signal tends to move, asked to rate proposed futures — for an Improviser that recombines memory, a second opinion on which continuation is plausible. It sees every frame actually played (observe) and returns a loss for a proposed path (pathchannel`, starting at output sampleindex); lower is better, null for no opinion.afield.math.temporal.ForecastJudge` rates paths against a Timestral forecast.
| Declaration | Summary |
|---|---|
fun observe(index: Long, frame: DoubleArray) |
|
fun loss(index: Long, path: Array<DoubleArray>): Double? |
JudgeMode
How an Improviser uses its ContinuationJudge.
Constants: Prefer, Veto.
Reason
Why a Segment began. Hold repeats the last frame because no acceptable continuation exists; after holding for a phrase, Restart takes the closest match there is, whatever its distance or correction, rather than hold for ever.
Constants: Start, Continue, SuffixLink, EpisodeEnd, Cue, Takeover, Hold, Restart.
Segment
One stretch of output played from one place in memory. outputStart is the output sample it began on; source the memory sample it began from; episode which demonstration that is. distance is the join distance (feature units) that admitted the jump, context the shared context length in samples, and correction the size of the join correction it needed.
Performance
What an improviser played: the output itself, and for every output sample the memory sample it came from, the size of the join correction applied, and which channels a performer owned.
| Declaration | Summary |
|---|---|
val output: Trajectory |
|
val source: IntArray |
Memory sample per output sample (-1 while holding). |
val correction: DoubleArray |
Join-correction size per output sample: RMS over channels, in channel spreads (0 = played as recorded). |
val owned: Array<BooleanArray> |
ownedchannel``: true where the performer drove that channel. |
val segments: List<Segment> |
|
fun sourceSeconds(): List<Double> |
The source trace in seconds of memory, ready to plot: a staircase of straight runs and jumps. |
fun sourceReuse(window: Int = (output.sampleRate * 10).toInt()): Double |
Fraction of output samples whose source passage was played at least once before within window samples. |
fun coverage(memoryLength: Int): Double |
Fraction of memory samples that appear somewhere in the output. |
fun outputRepetition(window: Double = 2.0, threshold: Double = 0.25, channels: List<Int> = output.channels.indices.toList(), rate: Double = 10.0): Double |
How much the output repeats itself, whatever memory it came from: the fraction of window-second stretches of output that closely match some earlier stretch, at least one window back. "Closely" is an RMS difference below threshold channel spreads, measured on channels (all by default; pick a few for a wide trajectory), with the output resampled to rate Hz to keep the comparison cheap. |
fun joinRoughness(memory: Trajectory): Double |
How rough the joins are, against the recording: the mean |second difference| within three samples of every jump, divided by the memory's mean |second difference|, averaged over channels. 1 means joins are as smooth as the recording is on average; above 1, the seams show. NaN with no jumps. |
fun accelerationDistance(memory: Trajectory): Double |
Kolmogorov–Smirnov distance between the output's and the memory's distributions of |second difference| (how sharply things change), worst channel: 0 = indistinguishable, 1 = disjoint. |
override fun toString() |
Improviser
Plays an oracle's memory forward one sample at a time, recombining it.
| Declaration | Summary |
|---|---|
val oracle: VariableMarkovOracle |
|
val policy: Policy |
|
val minPhrase: Double |
|
val jumpProbability: Double |
|
val maxJoin: Double |
|
val temperature: Double |
|
val joinTime: Double |
|
val cueWindow: Double |
|
val cueInterval: Double |
|
val declineDistance: Double |
|
val maxCorrection: Double |
|
val takeoverMargin: Double |
|
val judge: ContinuationJudge? |
|
val judgeWeight: Double |
|
val judgeAhead: Double |
|
val judgeMode: JudgeMode |
|
val reuseWeight: Double |
|
val reuseMemory: Double |
|
val bridges: Boolean |
|
val restartTolerance: Double |
|
val escapeReplays: Boolean |
|
val vetoRatio: Double |
|
val segments |
Segments played so far (the most recent 10 000). |
var judgedDecisions |
Jump decisions the judge scored. |
var judgeChanged |
Judged decisions the judge altered: in JudgeMode.Prefer, its most likely choice differed from the unjudged one; in JudgeMode.Veto, it removed at least one candidate. |
var judgeVetoed |
Candidate jumps the judge vetoed (JudgeMode.Veto). |
var judgeStayed |
Judged decisions that ended with staying on the current passage. |
val position: Int |
Memory sample of the last output (or -1 when holding). |
var lastCorrection |
Correction magnitude applied to the last output. |
fun perform(channel: Int, value: Double) |
Hands channel to the performer at value; call every sample while performing. |
fun perform(name: String, value: Double) |
Hands the channel named name to the performer. |
fun release(channel: Int) |
Gives channel back to the memory, fading from the performed value over joinTime. |
fun release(name: String) |
|
fun isPerformed(channel: Int) |
True while channel is performer-owned. |
fun reset(seed: Long, at: Int = 0) |
Restart from memory sample at with a fresh random sequence. |
fun step(): DoubleArray |
Advances one sample and returns the output frame. |
fun take(samples: Int, script: (Improviser.(Int) -> Unit)? = null): Performance |
Runs samples steps and returns the Performance. script runs before each step with the step index, for scripted takeovers: { i -> if (i in 300..450) perform("x", 0.0) else if (i == 451) release("x") }. |
fun take(seconds: Double, script: (Improviser.(Int) -> Unit)? = null): Performance |
Runs for seconds of output. |
Trajectory.kt
Sequence memory for low-dimensional continuous time series: remember one or a few demonstrations, cue the memory with a recent trajectory, and let it continue in a related style while a performer takes over channels.
Trajectory holds uniformly sampled multichannel recordings with episode boundaries;
GestureFeatures turns one into scaled value-and-velocity frames; VariableMarkovOracle
(Wang and Dubnov's VMO, a Factor Oracle over thresholded continuous frames) indexes the
repeated material; Improviser walks it, recording where every output sample came from.
Tests: VariableMarkovOracleTest.
src/jvmMain/kotlin/afield/math/oracle/Trajectory.kt — read the source
Used in: Continuing a Gesture: Sequence Memory and Forecasting
Trajectory
A uniformly sampled multichannel recording: valueschannel` at [sampleRate`](math.md#trajectory-kt--trajectory--samplerate) samples per second, split into one or more episodes.
| Declaration | Summary |
|---|---|
val sampleRate: Double |
Samples per second. |
val channels: List<String> |
One name per channel. |
val values: Array<DoubleArray> |
valueschannel``; every channel has the same length. |
val episodeStarts: IntArray |
First sample of each episode, ascending, starting with 0. |
val name: String |
Where this recording came from (free text, kept for provenance). |
val length: Int |
Number of samples. |
val width: Int |
Number of channels. |
val duration: Double |
Duration in seconds. |
fun channel(name: String): DoubleArray |
The samples of channel name. |
fun frame(t: Int): DoubleArray |
All channels at sample t. |
fun episodeOf(t: Int): Int |
Index of the episode containing sample t. |
fun endsEpisode(t: Int): Boolean |
True when t is the last sample of its episode, so t + 1 is not its continuation. |
fun startsEpisode(t: Int): Boolean |
True when t begins an episode. |
fun episode(e: Int): Trajectory |
Episode e as a trajectory of its own. |
fun series(prefix: String = ""): Array<Pair<String, List<Double>>> |
Every channel as a name to samples pair, ready for showSignals(*t.series()). |
fun resample(rate: Double): Trajectory |
Resampled to rate by linear interpolation, episode by episode. |
fun trimStill(threshold: Double = 0.25, pad: Double = 0.3): Trajectory |
Each episode with its still lead-in and tail removed: samples at the start and end where the speed (RMS over channels of the step, each channel in units of its own typical step) stays below threshold times the episode's median speed. pad seconds of the stillness are kept at each end, so a take still begins and ends at rest. An episode that is still throughout is kept whole. |
override fun toString() |
GestureFeatures
Frozen per-channel scaling and the feature frames a VariableMarkovOracle compares.
| Declaration | Summary |
|---|---|
val center: DoubleArray |
|
val scale: DoubleArray |
|
val velocityScale: DoubleArray |
|
val velocityWeight: Double |
|
val channelWeights: DoubleArray |
|
val shapeOnly: Boolean |
|
val width: Int |
Channels. |
val dimension: Int |
Length of one feature frame. |
fun frame(current: DoubleArray, previous: DoubleArray?, into: DoubleArray = DoubleArray(dimension)): DoubleArray |
Features of a single sample given its predecessor's values (previous = null at an episode start). |
fun frames(t: Trajectory): Array<DoubleArray> |
Feature frames for every sample of t; velocity is 0 at each episode start. |
VariableMarkovOracle.kt
Building a Variable Markov Oracle over a recording, and asking it where a performance could continue from.
VariableMarkovOracle indexes a Trajectory once, in its constructor, and then answers three
kinds of question about it. Its suffix links (sfx) and repeated-suffix lengths (lrs) say
where the material repeats itself, which is what Improviser walks. cue ranks positions by
how well the samples before them match a window of recent history, over any subset of channels,
which is how a performer's input finds a place in memory. bridges finds the nearest moments
in other episodes, where suffix links rarely reach, so several separately recorded takes can
be played as one memory.
The one parameter that matters is the threshold at which two frames count as the same symbol,
and build chooses it rather than asking for it: it constructs an oracle at each of a range of
candidate thresholds and keeps the one with the highest total information rate. That rate is
near zero at both ends of the range, since a threshold too small finds no repeats and one too
large makes everything a repeat, so the choice is a peak rather than a limit.
The construction is a Kotlin port of Cheng-i Wang's reference Python vmo package, kept in
Kotlin so an oracle can be built and queried inside the frame loop.
VmoReferenceTest checks the suffix links, the repeat lengths and the summed information rate
against a fixture generated from the pinned package.
src/jvmMain/kotlin/afield/math/oracle/VariableMarkovOracle.kt — read the source
VariableMarkovOracle
A Variable Markov Oracle (Wang and Dubnov, 2014) over a Trajectory: a Factor Oracle (Allauzen, Crochemore and Raffinot, 1999) built on continuous frames, where two frames count as "the same symbol" when their GestureFeatures lie within threshold of each other.
| Declaration | Summary |
|---|---|
val trajectory: Trajectory |
The remembered material. |
val features: GestureFeatures |
The scaling and feature construction used for every comparison. |
val frames: Array<DoubleArray> |
Feature frames, one per sample. |
val threshold: Double |
Frames closer than this are the same symbol. |
val sfx |
Suffix link per state; sfx[0] = -1. |
val lrs |
Length of the repeated suffix per state. |
val symbol |
Symbol (cluster of frames within threshold) per state; state 0 is symbol 0. |
val size: Int |
Number of states (samples + 1). |
fun transitions(i: Int): IntArray |
Forward transitions out of state i. |
fun reverseSuffixes(i: Int): IntArray |
States whose suffix link points at i. |
val alphabetSize: Int |
Number of distinct symbols: states whose suffix link is the start state. |
fun informationRate(): DoubleArray |
Information rate per sample, in bits: the reference package's IR(ir_type='cum'). |
fun totalInformationRate(): Double |
Sum of informationRate over the oracle: the quantity the threshold search maximises. |
fun cue(history: List<DoubleArray>, window: Int = min(history.size, (trajectory.sampleRate).toInt()), mask: BooleanArray = BooleanArray(features.width) { true }, count: Int = 5, separation: Int = window / 2): List<Cue> |
Ranked places in memory whose preceding window samples best match history, the most recent samples last (history[t](imaging.md#imaging-kt--channel), raw values). |
fun bridges(sample: Int): IntArray |
Cross-episode transitions from sample sample: up to three samples in other episodes whose feature frames (value and velocity) are nearest to it, nearest first. The oracle's own suffix links rarely cross between separately recorded takes; these give an improviser a way across. Computed for every sample on first use (all pairs of samples, so a few seconds for a few thousand samples). Empty for a single episode. |
fun matchDistance(history: List<DoubleArray>, position: Int, window: Int = min(history.size, (trajectory.sampleRate).toInt()), mask: BooleanArray = BooleanArray(features.width) { true }): Double |
How well history matches the window samples of memory ending at position (the same distance cue ranks by), or +∞ if that window crosses an episode boundary. |
override fun toString() |
Cue
data class Cue(val position: Int, val distance: Double, val context: Int)
A place in memory matching a cue: continue from sample position. Lower distance is better.
correlations.kt
Measures of statistical dependence between two equal-length DoubleArrays, batch and rolling.
StatsDependence carries the batch versions: pearson, spearman (Pearson on average-tie
ranks), kendallTauB, chatterjeeXi (aliased as xi), distanceCorrelation and
micTicNaive. Kendall's tau-b and distance correlation are both O(n²) in time, and distance
correlation additionally materialises two n×n matrices, so neither suits long series.
RollingDistanceCorrelation holds a fixed window and updates its maintained row and grand sums
in O(n) per push; RollingXiCorrelation only keeps the window and recomputes xi from
scratch. All of it is pure Kotlin with no external dependency.
micTicNaive is an equipartition-grid approximation rather than either published algorithm: it
bins x and y by rank into equal counts, scans every (xBins, yBins) whose product stays inside a
budget that defaults to n^0.6, and takes the largest normalised mutual information it finds
(the sum over the same grids is its TIC). That is MIC- and TIC-shaped without being either
MICe/TICe or the MINE dynamic program, so its values will not match a reference
implementation's. Use it for intuition and prototyping.
src/jvmMain/kotlin/afield/math/statistics/correlations.kt — read the source
Used in: Values over time: interpolants & keyframes, Measuring dependence between two signals
Top-level functions
| Declaration | Summary |
|---|---|
fun main() |
A scratch demo rather than part of the API: prints every coefficient in this file for y = x² over an x grid symmetric about zero — the textbook case where pearson reads about 0 while the dependence is in fact total, so StatsDependence.chatterjeeXi and StatsDependence.distanceCorrelation read high. |
StatsDependence
The batch dependence coefficients: hand any of these two equal-length DoubleArrays and get one number back per notion of "related" — pearson for linear association, spearman and kendallTauB for monotone association on ranks, chatterjeeXi for "is y a function of x", distanceCorrelation for dependence of any shape, micTicNaive for a grid mutual-information scan.
| Declaration | Summary |
|---|---|
fun ranksAverageTies(values: DoubleArray): DoubleArray |
Ranks in 1..n, smallest value first, with tied values sharing the average of the positions they occupy: 3, 1, 4, 1, 5 comes back as 3.0, 1.5, 4.0, 1.5, 5.0, since the two 1s sit at positions 1 and 2. The result therefore always sums to n(n+1)/2 whatever the input looks like. |
fun pearson(x: DoubleArray, y: DoubleArray): Double |
Pearson's product-moment correlation, in -1..1: 1 is a perfect increasing straight line, -1 a perfect decreasing one, and 0 means no linear relationship — which is emphatically not the same as no relationship, as the symmetric parabola in main demonstrates. |
fun spearman(x: DoubleArray, y: DoubleArray): Double |
Spearman's rank correlation — pearson applied to each array's ranksAverageTies, which puts a monotone-but-curved relationship at ±1 where Pearson reads less: exactly +1 for any strictly increasing pair, exactly -1 for any strictly decreasing one, whatever curve connects them. |
fun kendallTauB(x: DoubleArray, y: DoubleArray): Double |
Kendall's tau-b, in -1..1: concordant minus discordant pairs over all pairs, corrected for ties in either variable. Like spearman it measures monotone rather than linear association, and the two agree in sign throughout; tau-b is the more conservative of the pair in magnitude. |
fun chatterjeeXi(x: DoubleArray, y: DoubleArray): Double |
Chatterjee's xi, which is asymmetric: it measures how nearly y is a function of x, so swapping the arguments asks a different question and returns a different number. Near 0 for independence, near 1 when y is a noiseless function of x — including a non-monotone one such as a parabola, where pearson reads 0 and kendallTauB not much more. |
fun xi(x: DoubleArray, y: DoubleArray): Double |
Short alias for chatterjeeXi, for when the formula rather than the surname is what you have in mind. Same asymmetry: the first argument is the predictor. |
fun distanceCorrelation(x: DoubleArray, y: DoubleArray): Double |
Székely–Rizzo distance correlation, in 0..1 — and unlike pearson it is 0 only for genuine independence, which is what makes it the one to reach for when you do not want to guess the shape of the relationship first. It is symmetric in its arguments and never negative, so it tells you how much dependence there is and nothing at all about direction. |
data class MineLikeResult(val mic: Double, val tic: Double, val bestXBins: Int, val bestYBins: Int) |
What micTicNaive reports: the best (mic) and the summed (tic) normalized mutual-information score across every grid it scanned, plus the bestXBins × bestYBins grid that produced the maximum. Read the caveat on micTicNaive before trusting any of the four. |
fun micTicNaive(x: DoubleArray, y: DoubleArray, bMax: Int? = null): MineLikeResult |
A MIC/TIC-style scan: bin x and y onto every grid whose cell count fits a budget, score normalized mutual information on each, and report the maximum and the sum. Unlike the correlation coefficients above it is blind to the shape of the relationship: a parabola or a sine scores about as high as a straight line. |
RollingDistanceCorrelation
class RollingDistanceCorrelation(private val windowSize: Int)
Distance correlation over a sliding window, maintained incrementally: push a sample pair, get the current 0..1 coefficient back, at O(w) work per sample for a window of w rather than the O(w²) rebuild StatsDependence.distanceCorrelation would charge you.
| Declaration | Summary |
|---|---|
fun push(x: Double, y: Double): Double? |
Push one (x,y) sample. If the window is full, evicts the oldest. |
fun current(): Double? |
Current distance correlation (no mutation). |
RollingXiCorrelation
class RollingXiCorrelation(private val windowSize: Int)
Chatterjee's xi over a sliding window, recomputed from scratch on every push — no incremental trickery, just StatsDependence.chatterjeeXi over the window's current contents, which costs O(w log w) per sample and is exactly as trustworthy as the batch answer.
| Declaration | Summary |
|---|---|
fun push(x: Double, y: Double): Double? |
Push one (x, y) sample — evicting the oldest if the window is already full — and return xi over the window as it now stands, or null while fewer than two samples have arrived. |
fun current(): Double? |
Xi over the window's current contents without pushing anything, or null while fewer than two samples have arrived. Copies the window out in arrival order first, though that order does not reach the answer: StatsDependence.chatterjeeXi sorts by x itself and resolves x-ties by expectation rather than by position, so the result depends only on which pairs are in the window and not on where in the ring buffer they happen to sit. |
fun clear() |
Forget every buffered sample. The next push starts a fresh window and returns null again, exactly as it did on a new instance. |
dirichlet.kt
2D multiplicatively weighted Voronoi (Apollonius) diagrams. Each site is a Vec3 of
(x, y, weight) and cells are decided by the score |x - p|² / w².
CircularDirichlet.build returns a Diagram holding one CellBoundary per site, its pieces
sorted angularly around that site. Because the bisector of two differently weighted sites is a
circle — and a line only when the weights match — boundaries arrive as ArcPiece and
LinePiece rather than polygon edges; call toPolyline / toPolylines on a piece to
tessellate it, and pass an Aabb clip, since unbounded lines cannot be flattened without one
and clipping an arc may split it into several polylines.
All weights must be > 0 (build requires it). The construction is triple-nested over the
sites — every bisector is intersected against every other, and each candidate piece is kept
only if a midpoint sample really is on the cell boundary — so it is aimed at modest site
counts. Pure Kotlin on Field's Vec2/Vec3; the 3D counterpart is CircularDirichlet3d in
voronoi.kt.
src/jvmMain/kotlin/afield/math/statistics/dirichlet.kt — read the source
CircularDirichlet
randoms.kt
Box-scoped randomness. random, randomGaussian and setRandomSeed draw from a
java.util.Random held as a property on the current Box, so re-running a box replays the
same sequence; with no Box in context they fall back to one process-wide generator seeded
with STDLIB_RANDOM_DEFAULT_SEED.
Alongside the seed each box carries a noise phase you advance yourself (noisePhase,
noiseForward, setNoisePhase). That phase and seed drive noise1D, a small Perlin-style
hashed 1D noise, and the perlinNoise extensions on Vector2f/Vector3f/Vector4f, which
take one independent noise channel per component.
All the vector extensions — perlinNoise and the uniform-jitter noise(d) — mutate the
receiver in place and return it rather than allocating. nextGaussian is a Box-Muller
extension that fills the gap left by kotlin.random.Random, which has no Gaussian of its own.
src/jvmMain/kotlin/afield/math/statistics/randoms.kt — read the source
Used in: Flow Matching, State estimation: tracking what you cannot measure
Top-level functions
| Declaration | Summary |
|---|---|
fun random(): Double |
A uniform sample from [0, 1) — zero possible, one not — drawn from the current box's generator. |
fun randomGaussian(): Double |
A standard-normal sample (mean 0, standard deviation 1) from the current box's generator. |
fun KotlinRandom.nextGaussian(): Double |
Standard-normal (Box-Muller) sample from any kotlin.random.Random. The Kotlin stdlib Random has no nextGaussian() of its own (unlike java.util.Random), so this fills the gap for code that carries its own seeded generator. |
fun setRandomSeed(seed: Long? = null): Long |
Reseeds the current box's generator and resets its noise phase to 0, returning the seed used. |
fun noisePhase(): Float |
Where along its noise curve this box currently is — the default phase noise1D samples at. |
fun noiseForward(delta: Float): Float |
Advances this box's noise phase by delta and returns the new phase. |
fun setNoisePhase(phase: Float): Float |
Jumps this box's noise phase straight to phase, returning it. Use to drive noise from a clock. |
fun noise1D(phase: Float = currentStdlibPhase(), seedOffset: Long = 0L, frequency: Float = 1f, extraHash: Long = 0L): Float |
Repeatable 1D Perlin-style noise: smooth, continuous, and bounded by ±0.5. |
fun Vector2f.perlinNoise(amplitude: Float, phaseAdvance: Float = 0f, frequency: Float = 1f, seedOffset: Long = 0L, extraHash: Long = 0L): Vector2f |
Adds smooth noise1D wander to this point in place, one independent channel per component, and returns the same instance. |
fun Vector3f.perlinNoise(amplitude: Float, phaseAdvance: Float = 0f, frequency: Float = 1f, seedOffset: Long = 0L, extraHash: Long = 0L): Vector3f |
Adds smooth noise1D wander to this point in place — three independent channels, one per component — and returns the same instance. Conventions exactly as the Vector2f overload; this one consumes noise channels seedOffset through seedOffset + 2. |
fun Vector4f.perlinNoise(amplitude: Float, phaseAdvance: Float = 0f, frequency: Float = 1f, seedOffset: Long = 0L, extraHash: Long = 0L): Vector4f |
Adds smooth noise1D wander to all four components in place, one independent channel each, and returns the same instance. Conventions as the Vector2f overload. Note that on a colour this wanders alpha too, which is rarely what you want — jitter the Vector3f and rebuild the alpha. |
fun Vector2f.noise(d: Number) : Vector2f |
Jitters this point in place by an independent uniform draw per component and returns the same instance. |
fun Vector3f.noise(d: Number) : Vector3f |
Jitters all three components in place by up to ±d/2 each, uniformly and independently, and returns the same instance. As the Vector2f overload: white noise, not smooth wander. |
fun Vector4f.noise(d: Number) : Vector4f |
Jitters all four components in place by up to ±d/2 each, uniformly and independently, and returns the same instance. As the Vector2f overload — including alpha, if this is a colour. |
Top-level values
| Declaration | Summary |
|---|---|
const val STDLIB_RANDOM_DEFAULT_SEED: Long |
The seed every box's generator starts from, and the seed setRandomSeed undoes. |
smile_interpolation_regression.kt
Two 1D fitters that wrap the Smile library: smileCubicSpline (cubic spline interpolation
through the data) and smileGaussianProcess (Gaussian-process regression with a Gaussian
kernel, configured by SmileGpOptions).
Both take parallel xs/ys arrays, copy them, and require at least two finite samples with
xs strictly increasing — a violation throws rather than producing a bad fit with no complaint. The
returned SmileCubicSpline1D / SmileGaussianProcess1D are callable via invoke and also
expose predict for a single x or for a whole array.
GP fitting failures propagate. No interpolator is substituted for a failed probabilistic model.
For 1D/2D/3D inference, composed kernels, heteroscedastic noise and coherent samples see afield.math.gp.GaussianProcess.
src/jvmMain/kotlin/afield/math/statistics/smile_interpolation_regression.kt — read the source
Top-level functions
SmileGpOptions
SmileCubicSpline1D
| Declaration | Summary |
|---|---|
val xs: DoubleArray |
|
val ys: DoubleArray |
|
fun predict(x: Double): Double |
|
fun predict(xs: DoubleArray): DoubleArray |
|
operator fun invoke(x: Double): Double |
SmileGaussianProcess1D
| Declaration | Summary |
|---|---|
val xs: DoubleArray |
|
val ys: DoubleArray |
|
val options: SmileGpOptions |
|
fun predict(x: Double): Double |
|
fun predictWithVariance(x: Double): GpPrediction |
Latent posterior uncertainty in the original output units. |
fun posterior(xs: DoubleArray): GpJoint |
Seedable coherent draws through Field's semidefinite-aware joint sampler. |
fun predict(xs: DoubleArray): DoubleArray |
|
operator fun invoke(x: Double): Double |
stats.kt
Descriptive statistics, and the SVG/HTML renderings Field uses to put them in a box output.
buildSummaryStats reduces a sequence of doubles to a SummaryStats — count, quartiles, IQR,
MAD, variance, skewness, excess kurtosis — dropping and counting non-finite values as it goes.
buildHistogram picks a bin width as the smallest of the Freedman–Diaconis, Scott and Sturges
estimates, clamped to at most 2000 bins. SummaryStatsBuffer wraps both behind a synchronized,
capped sliding window you can add to sample by sample; there are parallel
Vec2SummaryStats / Vec3SummaryStats / Vec4SummaryStats forms that carry per-component
stats, vector length and the pairwise component correlations, each with their own buffer.
The rendering half — histogramToSvg, seriesPlotToSvg and summaryStatsBoxToHtml — builds
SVG and HTML by string interpolation, with no drawing library involved, and defaults to
Field's cyan-on-black look through a long list of colour and padding parameters. Pass the
previous SummaryStats and changed numbers are tinted by the sign and relative size of the
change. seriesPlotToSvg envelope-downsamples to maxSeriesPoints (500) before drawing.
src/jvmMain/kotlin/afield/math/statistics/stats.kt — read the source
Used in: Measuring dependence between two signals
Top-level functions
HistogramBin
data class HistogramBin(val start: Double, val end: Double, val count: Int)
One bin of a Histogram: the half-open interval [start, end) and how many samples fell in it.
Histogram
A histogram and the five-number summary of the same data, as buildHistogram returns them.
| Declaration | Summary |
|---|---|
val bins: List<HistogramBin> |
|
val min: Double |
|
val q1: Double |
|
val median: Double |
|
val q3: Double |
|
val max: Double |
|
val binWidth: Double |
|
val count: Int |
|
val modeBin: HistogramBin? |
SummaryStats
Seventeen descriptive numbers about one sample, as buildSummaryStats returns them.
| Declaration | Summary |
|---|---|
val count: Int |
|
val droppedCount: Int |
|
val min: Double |
|
val q1: Double |
|
val median: Double |
|
val q3: Double |
|
val max: Double |
|
val sum: Double |
|
val mean: Double |
|
val sampleVariance: Double |
|
val sampleStdDev: Double |
|
val populationVariance: Double |
|
val populationStdDev: Double |
|
val iqr: Double |
|
val mad: Double |
|
val skewness: Double |
|
val kurtosisExcess: Double |
|
val isEmpty: Boolean |
|
val range: Double |
|
val coeffOfVariation: Double |
SummaryStatsBox
Everything SummaryStatsBuffer.summaryBox knows in one value: the window's values in arrival order, their stats and histogram, and the buffer's three counters — so it is what summaryStatsBoxToHtml needs to render a whole card, and what you keep if you want to pass a previous card to it and get the changed numbers tinted.
| Declaration | Summary |
|---|---|
val values: List<Double> |
|
val stats: SummaryStats |
|
val histogram: Histogram |
|
val windowSize: Int |
|
val samplesSeen: Long |
|
val droppedNonFinite: Long |
|
val droppedByCapacity: Long |
Vec2SummaryStats
buildVec2SummaryStats's answer: a full SummaryStats per component, plus one for the vector length and one for the angleRadians (atan2(y, x), so in −π..π), plus the Pearson correlation between the two components.
| Declaration | Summary |
|---|---|
val x: SummaryStats |
|
val y: SummaryStats |
|
val length: SummaryStats |
|
val angleRadians: SummaryStats |
|
val correlationXY: Double |
|
val count: Int |
|
val droppedCount: Int |
|
val mean: Vec2 |
|
val median: Vec2 |
|
val min: Vec2 |
|
val max: Vec2 |
Vec3SummaryStats
buildVec3SummaryStats's answer: a SummaryStats per component and one for the vector length, plus the three pairwise component correlations. There is no angle summary here — Vec2SummaryStats has one because a 2D direction is a single number and a 3D one is not.
| Declaration | Summary |
|---|---|
val x: SummaryStats |
|
val y: SummaryStats |
|
val z: SummaryStats |
|
val length: SummaryStats |
|
val correlationXY: Double |
|
val correlationXZ: Double |
|
val correlationYZ: Double |
|
val count: Int |
|
val droppedCount: Int |
|
val mean: Vec3 |
|
val median: Vec3 |
|
val min: Vec3 |
|
val max: Vec3 |
Vec4SummaryStats
buildVec4SummaryStats's answer: a SummaryStats per component and one for the vector length, plus all six pairwise component correlations.
| Declaration | Summary |
|---|---|
val x: SummaryStats |
|
val y: SummaryStats |
|
val z: SummaryStats |
|
val w: SummaryStats |
|
val length: SummaryStats |
|
val correlationXY: Double |
|
val correlationXZ: Double |
|
val correlationXW: Double |
|
val correlationYZ: Double |
|
val correlationYW: Double |
|
val correlationZW: Double |
|
val count: Int |
|
val droppedCount: Int |
|
val mean: Vec4 |
|
val median: Vec4 |
|
val min: Vec4 |
|
val max: Vec4 |
Vec2SummaryStatsBox
What Vec2SummaryStatsBuffer.summaryBox returns: the window's vectors in arrival order, their stats, and the buffer's three counters. The scalar twin, SummaryStatsBox, also carries a Histogram; these do not, because there is no single histogram of a vector — ask the buffer for Vec2SummaryStatsBuffer.lengthHistogram if that is what you want.
| Declaration | Summary |
|---|---|
val values: List<Vec2> |
|
val stats: Vec2SummaryStats |
|
val windowSize: Int |
|
val samplesSeen: Long |
|
val droppedNonFinite: Long |
|
val droppedByCapacity: Long |
Vec3SummaryStatsBox
What Vec3SummaryStatsBuffer.summaryBox returns — see Vec2SummaryStatsBox; the only difference is the stats type.
| Declaration | Summary |
|---|---|
val values: List<Vec3> |
|
val stats: Vec3SummaryStats |
|
val windowSize: Int |
|
val samplesSeen: Long |
|
val droppedNonFinite: Long |
|
val droppedByCapacity: Long |
Vec4SummaryStatsBox
What Vec4SummaryStatsBuffer.summaryBox returns — see Vec2SummaryStatsBox; the only difference is the stats type.
| Declaration | Summary |
|---|---|
val values: List<Vec4> |
|
val stats: Vec4SummaryStats |
|
val windowSize: Int |
|
val samplesSeen: Long |
|
val droppedNonFinite: Long |
|
val droppedByCapacity: Long |
SummaryStatsBuffer
class SummaryStatsBuffer(maxSize: Int = 100)
Rolling numeric summary + histogram buffer: add samples one at a time, ask for a snapshot, a histogram, a whole summaryBox or a rendered summaryHtml whenever you like.
Vec2SummaryStatsBuffer
class Vec2SummaryStatsBuffer(maxSize: Int = 100)
The Vec2 twin of SummaryStatsBuffer: add vectors one at a time into a capped, synchronized sliding window, then ask for a snapshot, a summaryBox or a lengthHistogram.
Vec3SummaryStatsBuffer
class Vec3SummaryStatsBuffer(maxSize: Int = 100)
The Vec3 twin of SummaryStatsBuffer — see Vec2SummaryStatsBuffer for the counters and the all-components-must-be-finite rule. snapshot carries the three pairwise component correlations.
Vec4SummaryStatsBuffer
class Vec4SummaryStatsBuffer(maxSize: Int = 100)
The Vec4 twin of SummaryStatsBuffer — see Vec2SummaryStatsBuffer for the counters and the all-components-must-be-finite rule. snapshot carries all six pairwise component correlations.
timeseries.kt
Streaming, time-domain filters over Float signals — the sample-at-a-time counterpart to the
frequency-domain code in spectral.kt.
Everything here implements the FloatFilter fun interface (one Float in, one Float out)
and composes with then, so a chain reads as
DCBlocker() then EMAFilter(0.1f) then NoiseGate(0.05f). Three families share that interface:
smoothers and classic filters (EMAFilter, AlphaBetaFilter, OneEuroFilter, BiquadFilter
with its LowPassFilter / HighPassFilter / BandPassFilter / BandRejectFilter
constructors); rolling distribution fits — exponential, gamma, beta and Rayleigh — each
paired with a Normalizer mapping the fitted CDF into a target range and an EventDetector
firing on tail probability (ZScoreEventDetector instead thresholds on standard deviations
from a rolling mean); and utility stages such as MedianFilter, SchmittTrigger, AGC,
PIDController, LeakyIntegrator and DynamicRangeCompressor.
Pure Kotlin with no external dependency: the log-gamma, regularized incomplete gamma and
regularized incomplete beta functions the distribution fits need are implemented privately at
the top of this file. Filters are stateful objects, so use one instance per signal, and the
window-based ones allocate their ring buffer up front from windowSize.
src/jvmMain/kotlin/afield/math/statistics/timeseries.kt — read the source
Used in: Signal examples: time and frequency
Top-level functions
| Declaration | Summary |
|---|---|
fun LowPassFilter(frequency: Float, sampleRate: Float, q: Float = 0.707f) |
A BiquadFilter that passes what is below frequency (Hz) and rolls off above it at 12 dB/octave. sampleRate is the rate of the stream in Hz — both are needed, because only their ratio means anything. q defaults to the maximally-flat 0.707; raise it for a resonant bump at the corner. |
fun HighPassFilter(frequency: Float, sampleRate: Float, q: Float = 0.707f) |
A BiquadFilter that passes what is above frequency (Hz) and rolls off below it at 12 dB/octave — the steeper, better-specified alternative to DCBlocker once you know the corner you actually want. |
fun BandPassFilter(frequency: Float, sampleRate: Float, q: Float = 0.707f) |
A BiquadFilter that keeps a band around frequency (Hz) and rejects both sides of it. With a single biquad, q is the bandwidth (bandwidth ≈ frequency / q), so the 0.707 default is very wide indeed — push it to 4 or 8 for something that isolates a component. |
fun BandRejectFilter(frequency: Float, sampleRate: Float, q: Float = 0.707f) |
A BiquadFilter that notches out frequency (Hz) and passes everything else — mains hum, a rig's resonance, one offending tone. Higher q gives a narrower notch; this is BiquadType.NOTCH under a friendlier name. |
FloatFilter
One Float in, one Float out — the shape every filter in this file takes, and the reason they chain. A filter is a stateful object called once per sample, in arrival order: it is not a pure function, and calling it is what advances its state, so never call one twice on the same sample just to read a number back out of it.
| Declaration | Summary |
|---|---|
operator fun invoke(value: Float): Float |
Push one sample through and take this stage's output for it, advancing the filter's state. |
infix fun then(next: FloatFilter): FloatFilter |
Series composition, left to right: a then b feeds a's output into b, so DCBlocker() then EMAFilter(0.1f) takes the offset out first and smooths what is left. |
EMAFilter
class EMAFilter(var alpha: Float) : FloatFilter
Exponential moving average — the cheapest useful smoother, out = alpha * in + (1 - alpha) * out. One multiply-add and one field of state, which is why it is so often the first thing in a chain.
| Declaration | Summary |
|---|---|
override fun invoke(value: Float): Float |
The smoothed value for value — on the very first call, value itself. |
AlphaBetaFilter
class AlphaBetaFilter(var alpha: Float, var beta: Float, var dt: Float = 1f) : FloatFilter
A constant-velocity tracker with fixed gains — a Kalman filter's structure without its bookkeeping. It carries a position and a velocity, predicts one step ahead, then corrects both from the residual, so unlike EMAFilter it keeps up with a steadily moving signal instead of trailing behind it.
| Declaration | Summary |
|---|---|
override fun invoke(value: Float): Float |
The corrected position estimate for value. The internal velocity is updated too, but is not exposed. |
OneEuroFilter
The 1€ filter (Casiez, Roussel & Vogel): a low-pass whose cutoff rises with the signal's own speed, so a slow-moving hand gets smoothed hard while a fast gesture is barely delayed. The usual pick for pointer, touch or tracker input, where lag is more objectionable than jitter.
| Declaration | Summary |
|---|---|
var minCutoff: Float |
Cutoff (Hz) used when the signal is standing still — lower is smoother and laggier. |
var beta: Float |
How strongly measured speed raises the cutoff: cutoff = minCutoff + beta * \|speed\|. At 0 the adaptation is off and this is an ordinary one-pole low-pass at minCutoff. |
var dCutoff: Float |
Cutoff (Hz) of the low-pass applied to the speed estimate itself. Raise it if the adaptation feels sluggish, lower it if noise in the speed makes the cutoff jitter. |
var sampleRate: Float? |
The rate in Hz the samples were taken at. When set, dt is 1 / sampleRate on every call; when null, dt is measured from the wall clock between calls. |
override fun invoke(value: Float): Float |
The filtered sample. Without sampleRate this reads the clock, so the amount of smoothing depends on when you call it, not only on what you pass in. |
BiquadType
Which of the RBJ audio-EQ-cookbook shapes a BiquadFilter realises. LOWPASS, HIGHPASS, BANDPASS and NOTCH are determined entirely by frequency and Q; PEAK, LOWSHELF and HIGHSHELF are the only three that read gainDb — the others accept it and ignore it.
Constants: LOWPASS, HIGHPASS, BANDPASS, NOTCH, PEAK, LOWSHELF, HIGHSHELF.
BiquadFilter
A second-order IIR section — two poles, two zeros — run as a direct-form I recursion with coefficients from the RBJ audio-EQ cookbook. This is the machinery behind LowPassFilter, HighPassFilter, BandPassFilter and BandRejectFilter, and what to reach for when you want a filter with an actual corner frequency rather than a vague smoothing coefficient.
| Declaration | Summary |
|---|---|
val type: BiquadType |
The response shape. Fixed at construction — build a new filter to change it. |
var frequency: Float |
Corner (or centre) frequency in Hz. Call calculateCoefficients after changing it. |
var sampleRate: Float |
The rate in Hz of the stream you will push through: the reference frequency is measured against, and not something the filter can discover for itself. |
var q: Float |
Resonance. 0.707 is maximally flat (Butterworth); higher peaks at the corner and rings, and for BiquadType.BANDPASS / BiquadType.NOTCH higher also means a narrower band. |
var gainDb: Float |
Boost (positive) or cut (negative) in dB — read only by BiquadType.PEAK, BiquadType.LOWSHELF and BiquadType.HIGHSHELF, and silently ignored by the other four. |
fun calculateCoefficients() |
Re-derive the five coefficients from the current type, frequency, sampleRate, q and gainDb. Run once at construction; run it again yourself after mutating any of those, because nothing else will. It leaves the delay line alone, so a mid-stream retune stays continuous in the signal but can step the output audibly. |
override fun invoke(value: Float): Float |
One step of y = b0·x + b1·x₋₁ + b2·x₋₂ − a1·y₋₁ − a2·y₋₂, shifting the two input and two output delays along afterwards. |
RollingStats
class RollingStats(val windowSize: Int) : FloatFilter
A tap rather than a filter: every sample passes through unchanged, and the window's mean, variance, stdDev and the current sample's zScore are left behind for you to read. Drop it anywhere in a then chain to find out what the signal is doing at that point without disturbing it.
| Declaration | Summary |
|---|---|
var mean: Float |
Arithmetic mean of the window, the sample just pushed included. |
var variance: Float |
Sample (n−1) variance of the window; 0 until a second sample has arrived. |
var stdDev: Float |
Square root of variance. |
var zScore: Float |
How many standard deviations the sample just pushed sits from mean — signed, and 0 whenever stdDev is 0. Note the sample is itself in the window it is being compared against. |
override fun invoke(value: Float): Float |
Records value into the window, refreshes all four statistics, and hands value straight back. |
RollingExponentialStats
Fits an exponential distribution to the last windowSize samples by moment matching (lambda = 1 / mean) and reports where the newest sample falls within it. A tap: the sample passes through unchanged.
| Declaration | Summary |
|---|---|
val windowSize: Int |
How many samples the fit sees. Must be >= 2; the ring buffer is allocated up front. |
var epsilon: Float |
Degeneracy floor. While the fitted mean or rate is at or below this the fit is abandoned rather than dividing by something near zero. |
var mean: Float |
Mean of the window after negative clamping — the only statistic an exponential fit needs. |
var lambda: Float |
Fitted rate 1 / mean, or 0 while the window mean is at or below epsilon. |
var variance: Float |
Variance of the fitted distribution, 1 / lambda² — not the sample variance of the window. |
var stdDev: Float |
Square root of variance, so also 1 / lambda. |
var cdf: Float |
P(X <= x) under the fit for the sample just pushed, in 0..1. |
var tailProbability: Float |
P(X >= x) = 1 - cdf for the sample just pushed: small means a surprisingly large sample. Sits at 1 during the degenerate case, so a silent signal reports nothing as unusual. |
var clippedNegativeCount: Long |
How many negative samples have been clamped to zero so far. Anything but 0 means the exponential is the wrong model for this signal — take abs() first, or use RollingBetaStats. |
override fun invoke(value: Float): Float |
Clamps value to >= 0, refits the window, updates cdf and tailProbability — and returns the original value, clamping and all left behind in the fit. |
ExponentialNormalizer
Rescales a non-negative stream by rank rather than by magnitude: each sample's fitted exponential CDF, mapped onto targetMin..targetMax. What you want when an unbounded intensity has to drive something that expects a bounded control value, and "big for this signal lately" matters more than the units.
| Declaration | Summary |
|---|---|
var targetMin: Float |
The value a CDF of 0 maps to. |
var targetMax: Float |
The value a CDF of 1 maps to. May be set below targetMin to invert the mapping. |
val stats |
The fit doing the work — read stats.lambda, stats.tailProbability or stats.clippedNegativeCount to see what the normaliser is basing its answer on. |
override fun invoke(value: Float): Float |
Pushes value through the fit and returns its CDF mapped into targetMin..targetMax. |
ExponentialEventDetector
Fires when a sample is improbably large for the exponential distribution the preceding window fits — an onset / spike detector for non-negative streams, in the same family as ZScoreEventDetector but thresholding tail probability instead of standard deviations.
| Declaration | Summary |
|---|---|
val windowSize: Int |
Samples of history behind the fit. Must be >= 2, and nothing can fire until this many have arrived. |
var tailProbabilityThreshold: Float |
Fire when the sample's upper-tail probability falls to this or below — at 0.01, "in the top 1% of the fitted distribution". Clamped into 0..1 on use. |
var influence: Float |
How much of an event's own sample is written into the window, 0..1 (clamped): 0 keeps events out of the fit entirely, 1 lets them recalibrate it so a sustained shift stops being news. |
var epsilon: Float |
Degeneracy floor — while the window mean is at or below this, no events are reported at all. |
var isEvent: Boolean |
Whether the most recent sample fired — the same news as a nonzero return from invoke. |
var tailProbability: Float |
Upper-tail probability of the most recent sample; 1 during warm-up and while the fit is degenerate. |
var lambda: Float |
Fitted exponential rate 1 / mean over the window, or 0 while degenerate. |
var clippedNegativeCount: Long |
How many negative samples have been clamped to zero — anything but 0 means the wrong model for this signal. |
override fun invoke(value: Float): Float |
1f if value is an event, 0f otherwise — including for every sample of the warm-up. |
RollingGammaStats
Fits a gamma distribution to the last windowSize samples by matching mean and variance (shape = mean²/variance, scale = variance/mean) and reports where the newest sample falls within it. A tap: the sample passes through unchanged.
| Declaration | Summary |
|---|---|
val windowSize: Int |
How many samples the fit sees. Must be >= 2; the ring buffer is allocated up front. |
var epsilon: Float |
Degeneracy floor for the window's mean and variance, and the lower bound clamped onto the fitted shape and scale. |
var mean: Float |
Arithmetic mean of the window after negative clamping — half of the moment-matching fit. |
var variance: Float |
Sample (n−1) variance of the window — a genuine sample statistic here, and the other half of the fit. |
var stdDev: Float |
Square root of variance. |
var shape: Float |
Fitted gamma shape mean²/variance (the k parameter): 1 would be an exponential, larger is more symmetric. 0 while the fit is degenerate. |
var scale: Float |
Fitted gamma scale variance/mean (the θ parameter), in the units of your signal. 0 while degenerate. |
var cdf: Float |
P(X <= x) under the fit for the sample just pushed, in 0..1. |
var tailProbability: Float |
1 - cdf: the share of the fitted distribution lying above the current sample. Small means surprisingly large. |
var clippedNegativeCount: Long |
How many negative samples have been clamped to zero — anything but 0 means a gamma is the wrong model here. |
override fun invoke(value: Float): Float |
Clamps value to >= 0, re-matches shape and scale over the window, updates cdf and tailProbability, and returns the original value untouched. |
GammaNormalizer
Rescales a non-negative, skewed stream onto targetMin..targetMax by its fitted gamma CDF — the two-parameter sibling of ExponentialNormalizer, and the better pick whenever the signal's bulk sits somewhere away from zero rather than hugging it.
| Declaration | Summary |
|---|---|
var targetMin: Float |
The value a CDF of 0 maps to. |
var targetMax: Float |
The value a CDF of 1 maps to. May be set below targetMin to invert the mapping. |
val stats |
The fit doing the work — read stats.shape and stats.scale to see what distribution the answers came from. |
override fun invoke(value: Float): Float |
Pushes value through the fit and returns its CDF mapped into targetMin..targetMax. |
GammaEventDetector
Fires when a sample lands in the upper tail of the gamma distribution the preceding window fits. The general-purpose distribution-aware onset detector in this file: it copes with the skewed, non-negative streams — magnitudes, energies, abs() of a motion signal — where a z-score's assumption of symmetry makes it over-fire on the low side. This is the detector the published signal examples page (spectral_examples) reaches for.
| Declaration | Summary |
|---|---|
val windowSize: Int |
Samples of history behind the fit. Must be >= 2, and nothing fires until this many have arrived. |
var tailProbabilityThreshold: Float |
Fire when the sample's upper-tail probability falls to this or below — at 0.01, "in the top 1% of the fitted distribution". Clamped into 0..1 on use. |
var influence: Float |
How much of an event's own sample enters the window, 0..1 (clamped): 0 keeps events out of the fit, 1 lets them recalibrate it. |
var epsilon: Float |
Degeneracy floor for the window's mean and variance — below it, no events are reported. |
var isEvent: Boolean |
Whether the most recent sample fired. |
var cdf: Float |
P(X <= x) of the most recent sample under the fit; 0 during warm-up and while degenerate. |
var tailProbability: Float |
1 - cdf for the most recent sample — the quantity actually compared with tailProbabilityThreshold. |
var shape: Float |
Fitted gamma shape over the window, or 0 while the fit is degenerate. |
var scale: Float |
Fitted gamma scale over the window, in the units of your signal, or 0 while degenerate. |
var clippedNegativeCount: Long |
How many negative samples have been clamped to zero — anything but 0 means the wrong model for this signal. |
override fun invoke(value: Float): Float |
1f if value is an event, 0f otherwise — including for every sample of the warm-up. |
RollingBetaStats
Fits a beta distribution to the last windowSize samples by moment matching and reports where the newest one falls within it. A tap: the sample passes through unchanged.
| Declaration | Summary |
|---|---|
val windowSize: Int |
How many samples the fit sees. Must be >= 2; the ring buffer is allocated up front. |
var supportMin: Float |
Bottom of your signal's assumed range; anything below maps to 0 and counts as clipped. |
var supportMax: Float |
Top of your signal's assumed range; anything above maps to 1 and counts as clipped. Must be strictly greater than supportMin at construction. |
var epsilon: Float |
How far inside the open interval (0, 1) stored samples are held, and the degeneracy floor for the moment fit. Defaults to 1e-4, looser than the other fits in this file. |
var mean: Float |
Mean of the window in normalised 0..1 units, not in the units of your signal. |
var variance: Float |
Sample (n−1) variance of the window, also in normalised units. |
var stdDev: Float |
Square root of variance. |
var alpha: Float |
Fitted beta α, the shape pulling mass toward 1; 0 when the fit fell back to min/max normalisation. |
var beta: Float |
Fitted beta β, the shape pulling mass toward 0; 0 when the fit fell back to min/max normalisation. |
var cdf: Float |
P(X <= x) under the fit for the sample just pushed, in 0..1 — or, when the fit is degenerate, simply that sample's normalised position within the support. |
var tailProbabilityLow: Float |
Lower-tail probability of the current sample; equal to cdf. Small means unusually low for this window. |
var tailProbabilityHigh: Float |
Upper-tail probability of the current sample, 1 - cdf. Small means unusually high. |
var clippedToSupportCount: Long |
How many samples have fallen outside supportMin..supportMax and been clipped into it. A count that keeps climbing means the support is wrong. |
var normalizedValue: Float |
The sample just pushed, mapped onto 0..1 across the support — clipped there but before the epsilon nudge, so unlike the value stored in the window this one can be exactly 0 or 1. |
override fun invoke(value: Float): Float |
Normalises value into the support, refits the window, updates the CDF and both tails, and returns the original value unchanged. |
BetaNormalizer
Rescales a bounded stream onto targetMin..targetMax by its fitted beta CDF — histogram equalisation with a two-parameter distribution standing in for the histogram. Where RollingNormalizer merely stretches the window's extremes apart, this spreads samples out according to how they are actually distributed, so a signal that spends its life bunched in one corner of its range comes out using the whole target range.
| Declaration | Summary |
|---|---|
var targetMin: Float |
The value a CDF of 0 maps to. |
var targetMax: Float |
The value a CDF of 1 maps to. May be set below targetMin to invert the mapping. |
val stats |
The fit doing the work. Also where the support lives after construction — and where clippedToSupportCount will tell you the support is wrong. |
override fun invoke(value: Float): Float |
Pushes value through the fit and returns its CDF mapped into targetMin..targetMax. |
BetaEventDetector
A two-sided event detector for a bounded stream: it fires both when a sample is improbably high for the window's fitted beta and when it is improbably low, and the sign of the output says which. -1f for a lower-tail event, +1f for an upper-tail one, 0f for nothing — including for the whole windowSize-sample warm-up.
| Declaration | Summary |
|---|---|
val windowSize: Int |
Samples of history behind the fit. Must be >= 2, and nothing fires until this many have arrived. |
var supportMin: Float |
Bottom of your signal's assumed range; anything below is clipped to it and counted. |
var supportMax: Float |
Top of your signal's assumed range; must be strictly greater than supportMin at construction. |
var lowerTailThreshold: Float |
Fire low when the CDF falls to this or below — at 0.01, "in the bottom 1% of the fit". Clamped into 0..1; set above 0.5 and nearly everything becomes an event. |
var upperTailThreshold: Float |
Fire high when 1 - cdf falls to this or below — the top 1% at 0.01. Clamped into 0..1. |
var influence: Float |
How much of an event's own sample enters the window, 0..1 (clamped): 0 keeps events out of the fit, 1 lets a sustained shift recalibrate it. |
var epsilon: Float |
Boundary nudge for stored samples and degeneracy floor for the moment fit; 1e-4 by default. |
var isEvent: Boolean |
Whether the most recent sample fired at either end. It does not say which end — the sign of invoke's return does. |
var cdf: Float |
CDF of the most recent sample under the fit, or its normalised position when degenerate; parked at 0.5 during warm-up. |
var alpha: Float |
Fitted beta α over the window; 0 when the fit fell back to plain normalisation. |
var beta: Float |
Fitted beta β over the window; 0 when the fit fell back to plain normalisation. |
var clippedToSupportCount: Long |
How many samples have been clipped into supportMin..supportMax — a climbing count means the support is wrong. |
override fun invoke(value: Float): Float |
-1f for a lower-tail event, +1f for an upper-tail one, 0f for neither and for the whole warm-up. If both thresholds somehow fire at once, the lower tail wins. |
RollingRayleighStats
Fits a Rayleigh distribution to the last windowSize samples and reports where the newest one falls within it. A tap: the sample passes through unchanged.
| Declaration | Summary |
|---|---|
val windowSize: Int |
How many samples the fit sees. Must be >= 2; the ring buffer is allocated up front. |
var epsilon: Float |
Degeneracy floor on the window's second moment and on sigma — below it, nothing is reported. |
var sigma: Float |
Fitted Rayleigh scale, sqrt(mean(x²) / 2); 0 while the window's second moment is at or below epsilon. |
var mean: Float |
Mean of the fitted distribution, sigma * sqrt(π/2) — derived from sigma, not averaged from the window. |
var variance: Float |
Variance of the fitted distribution, ((4 - π)/2) * sigma². |
var stdDev: Float |
Square root of variance. |
var cdf: Float |
P(X <= x) = 1 - exp(-x² / 2sigma²) for the sample just pushed. |
var tailProbability: Float |
1 - cdf: the share of the fitted distribution above the current sample. Small means a surprisingly large magnitude. |
var clippedNegativeCount: Long |
How many negative samples have been clamped to zero. A magnitude should never be negative, so anything but 0 means the wrong signal is arriving here. |
override fun invoke(value: Float): Float |
Clamps value to >= 0, re-estimates sigma from the window's second moment, updates the fitted moments and the CDF, and returns the original value unchanged. |
RayleighNormalizer
Rescales a stream of magnitudes onto targetMin..targetMax by its fitted Rayleigh CDF. The one to use when the numbers arriving are lengths of 2-D vectors — cursor speed, optical-flow magnitude, gradient strength — because a single-parameter fit adapts on far less history than GammaNormalizer needs.
| Declaration | Summary |
|---|---|
var targetMin: Float |
The value a CDF of 0 maps to. |
var targetMax: Float |
The value a CDF of 1 maps to. May be set below targetMin to invert the mapping. |
val stats |
The fit doing the work — stats.sigma is the single number the whole mapping rests on. |
override fun invoke(value: Float): Float |
Pushes value through the fit and returns its CDF mapped into targetMin..targetMax. |
RayleighEventDetector
Fires when a magnitude is improbably large for the Rayleigh distribution the preceding window fits. The detector to pair with a speed or vector-length signal: a sudden lunge, a flick, a burst of optical flow.
| Declaration | Summary |
|---|---|
val windowSize: Int |
Samples of history behind the fit. Must be >= 2, and nothing fires until this many have arrived. |
var tailProbabilityThreshold: Float |
Fire when the sample's upper-tail probability falls to this or below — at 0.01, "in the top 1% of the fitted distribution". Clamped into 0..1 on use. |
var influence: Float |
How much of an event's own sample enters the window, 0..1 (clamped): 0 keeps events out of the fit, 1 lets them recalibrate it. |
var epsilon: Float |
Degeneracy floor on the window's second moment and on sigma — below it, no events are reported. |
var isEvent: Boolean |
Whether the most recent sample fired. |
var sigma: Float |
Fitted Rayleigh scale over the window, sqrt(mean(x²) / 2), or 0 while degenerate. |
var cdf: Float |
P(X <= x) of the most recent sample under the fit; 0 during warm-up and while degenerate. |
var tailProbability: Float |
1 - cdf for the most recent sample — the quantity compared with tailProbabilityThreshold. |
var clippedNegativeCount: Long |
How many negative samples have been clamped to zero; a magnitude should never be negative. |
override fun invoke(value: Float): Float |
1f if value is an event, 0f otherwise — including for every sample of the warm-up. |
ZScoreEventDetector
The classic smoothed z-score peak detector: it fires when the newest sample sits more than threshold standard deviations from the mean of the window, and its output carries the direction — +1f above, -1f below, 0f for no event and throughout the warm-up.
| Declaration | Summary |
|---|---|
val windowSize: Int |
Samples of history behind the mean and standard deviation; nothing fires until this many have arrived. |
var threshold: Float |
How many standard deviations from the window mean counts as an event — 3 to 4 is a conventional starting point. The test is strict and on \|z\|, so it fires in both directions. |
var influence: Float |
How much of an event's own sample enters the window, nominally 0..1 but not clamped: 0 substitutes the previous sample, so a sustained level shift keeps being reported forever, while 1 lets the detector adapt to it and fall silent. |
var isEvent: Boolean |
Whether the most recent sample fired, in either direction. |
override fun invoke(value: Float): Float |
+1f or -1f for an event above or below the window, 0f for none — and 0f for the first windowSize samples while the window fills, and whenever the window is perfectly flat. |
AdaptiveCompander
An envelope follower plus a static gain curve: it either squashes the loud parts toward threshold (compressor) or pushes the quiet parts further down (expander), depending on isCompressor. Everything is in linear units, which makes this the plain, cheap sibling of DynamicRangeCompressor — that one does the same job in dB, with a soft knee and real attack and release times.
| Declaration | Summary |
|---|---|
var attack: Float |
Per-sample coefficient (0..1) used while the envelope is rising; larger reacts faster to a growing signal. |
var release: Float |
Per-sample coefficient (0..1) used while the envelope is falling; larger lets the gain recover faster after a peak. |
var threshold: Float |
The linear amplitude where the curve bends — above it for a compressor, below it for an expander. Not dB. |
var ratio: Float |
How hard the curve bends: as a compressor the excess above threshold is divided by this; as an expander the shortfall below it is multiplied by it. |
var isCompressor: Boolean |
true compresses (pull the loud parts in), false expands (push the quiet parts down). Switchable mid-stream. |
override fun invoke(value: Float): Float |
Updates the envelope from abs(value) and returns value scaled by the gain the curve asks for at that envelope. |
AGC
Automatic gain control: follows the signal's envelope and scales the signal so that envelope sits at targetLevel. Reach for it when you care about the shape of a signal whose absolute scale you cannot predict — a sensor whose range depends on how it was mounted, a control stream you want to hand to something expecting 0..1.
| Declaration | Summary |
|---|---|
var targetLevel: Float |
The envelope level the gain chases, in the same linear units as your signal. |
var attack: Float |
Per-sample coefficient (0..1) used when the envelope rises and when the gain must increase; larger is faster. |
var release: Float |
Per-sample coefficient (0..1) used when the envelope falls and when the gain must decrease. The default is ten times slower than attack, so the gain backs off gently. |
var maxGain: Float |
Ceiling on the multiplier, so a near-silent stretch cannot be amplified without limit. |
override fun invoke(value: Float): Float |
Updates the envelope and the smoothed gain, then returns value multiplied by that gain. |
LeakyIntegrator
class LeakyIntegrator(var leakRate: Float = 0.99f) : FloatFilter
A running sum that forgets: sum = sum * leakRate + value. The signal-processing way to ask "how much has been going on lately" — feed it event flags or rectified magnitudes and it climbs with activity, then decays back down when things go quiet.
| Declaration | Summary |
|---|---|
override fun invoke(value: Float): Float |
Leaks the accumulator once, adds value, and returns the new total. Starts from 0 and has no reset. |
RollingNormalizer
Stretches the last windowSize samples onto targetMin..targetMax using their own minimum and maximum — the simplest auto-ranging there is, and the one to use when you know nothing about the signal's distribution and just need something bounded. Note the default target is -1..1, not 0..1.
| Declaration | Summary |
|---|---|
override fun invoke(value: Float): Float |
Records value, then returns it rescaled from the window's current min..max onto targetMin..targetMax — or the midpoint of that range while the window is flat. |
RollingOneDollarNormalizer
class RollingOneDollarNormalizer(val windowSize: Int) : FloatFilter
Centres the stream on its window mean and divides by the largest absolute deviation in that window, so the output lands in -1..1 with ±1 reached exactly by whichever sample is currently furthest from the mean. A symmetric, zero-centred alternative to RollingNormalizer: "which side of typical" stays meaningful, at the price of letting one outlier set the scale for a whole window.
| Declaration | Summary |
|---|---|
override fun invoke(value: Float): Float |
Records value and returns its deviation from the window mean divided by the window's largest absolute deviation; 0 while the window is constant. |
AdaptiveThresholder
Despite the name this returns a continuous value, not a decision: the sample's distance from the window mean, measured in units of stdDevMultiplier standard deviations. A sample sitting exactly stdDevMultiplier sigma above the mean comes out as +1, and it is thresholding that downstream — or a SchmittTrigger — that turns it into a decision.
| Declaration | Summary |
|---|---|
val windowSize: Int |
Samples of history behind the mean and standard deviation. The buffer is allocated up front. |
var stdDevMultiplier: Float |
How many standard deviations map to an output of 1. Larger is a gentler scale. |
var clamp: Boolean |
Coerce the output into -1..1 instead of letting it run past the ends. |
override fun invoke(value: Float): Float |
Records value and returns its deviation from the window mean divided by stdDev * stdDevMultiplier, optionally clamped to -1..1. |
DerivativeFilter
class DerivativeFilter : FloatFilter
First difference: this sample minus the previous one. The cheapest way to turn something position-shaped into something velocity-shaped, or to find the edges in a slow signal.
| Declaration | Summary |
|---|---|
override fun invoke(value: Float): Float |
The change since the previous sample, or 0 on the first call. |
IntegralFilter
class IntegralFilter : FloatFilter
A plain running total of everything it has ever been given — for accumulating an event count, or for turning a velocity-shaped signal back into a position-shaped one.
| Declaration | Summary |
|---|---|
override fun invoke(value: Float): Float |
Adds value to the total and returns the new total. |
DCBlocker
class DCBlocker(var r: Float = 0.995f) : FloatFilter
Removes whatever constant offset a signal is riding on, without waiting for a window to fill: a one-pole, one-zero high-pass, y = x - x₋₁ + r·y₋₁, parked just above DC. The standard thing to put at the head of a chain when you care how a sensor is changing rather than where its zero happens to sit.
| Declaration | Summary |
|---|---|
override fun invoke(value: Float): Float |
One step of y = x - x₋₁ + r·y₋₁ — the input with its slow-moving offset taken out. |
PeakDetector
class PeakDetector(var decay: Float = 0.99f) : FloatFilter
A peak-hold envelope: it jumps instantly to abs(value) whenever that exceeds the current peak, and otherwise decays toward zero by a factor of decay each sample. What you want for a level meter, a "how hard was that hit" reading, or the denominator of a hand-rolled normaliser.
| Declaration | Summary |
|---|---|
override fun invoke(value: Float): Float |
The held peak after value: its magnitude if that is a new high, otherwise the previous peak decayed once — but never less than the magnitude in hand. |
SchmittTrigger
class SchmittTrigger(var lowThreshold: Float, var highThreshold: Float) : FloatFilter
A comparator with hysteresis: it goes high once the input rises above highThreshold and only comes back down once the input falls below lowThreshold, so a signal loitering around a single threshold cannot chatter. Returns 1f while high and 0f while low.
| Declaration | Summary |
|---|---|
override fun invoke(value: Float): Float |
1f while latched high and 0f while low, after letting value set or clear the latch. |
MedianFilter
class MedianFilter(val windowSize: Int) : FloatFilter
Rolling median of the last windowSize samples. The spike killer: unlike any moving average it removes an isolated outlier completely instead of spreading it over its neighbourhood, and it leaves real edges where they are instead of smearing them into ramps.
| Declaration | Summary |
|---|---|
override fun invoke(value: Float): Float |
Records value and returns the median of the window, averaging the two middle samples when the count is even. |
NoiseGate
Passes the signal through untouched while it is loud enough and hard-zeroes it when it is not. The gate opens when the tracked envelope rises above threshold and closes only once the envelope falls below half that, so a signal hovering at the threshold does not flutter.
| Declaration | Summary |
|---|---|
override fun invoke(value: Float): Float |
value unchanged while the gate is open, 0f while it is shut — after this sample has updated the envelope. |
PIDController
class PIDController(var kp: Float, var ki: Float, var kd: Float) : FloatFilter
A textbook proportional-integral-derivative controller wired up as a filter: it reads the measured value and returns the control effort that should push it toward setpoint. In a box this is what to reach for when one thing should chase another — a camera trailing a target, a value easing toward the mouse — with overshoot you get to tune rather than inherit.
| Declaration | Summary |
|---|---|
var setpoint |
Where the controller is trying to get the measured value to. Safe to move mid-stream, though the accumulated integral term does not know it moved. |
override fun invoke(value: Float): Float |
The control output for a measurement of value: kp·e + ki·Σe + kd·Δe, where e = setpoint - value. |
CooldownFilter
class CooldownFilter(val cooldownSamples: Int) : FloatFilter
A debouncer for a stream of event flags: it lets a positive sample through, then swallows everything for the next cooldownSamples samples. Put it after a detector to turn a burst of flags at one onset into a single flag.
| Declaration | Summary |
|---|---|
override fun invoke(value: Float): Float |
value itself if it is positive and the cooldown has elapsed, otherwise 0f. |
StreamingWhitener
class StreamingWhitener(var alpha: Float = 0.01f, var epsilon: Float = 1e-5f) : FloatFilter
A z-score with no window: it tracks the mean and the variance exponentially and returns how far the current sample sits from that running mean, in running standard deviations. The O(1)-memory alternative to RollingStats plus arithmetic, and the natural pre-stage for anything that wants a roughly zero-mean, unit-variance input.
| Declaration | Summary |
|---|---|
override fun invoke(value: Float): Float |
The current sample's distance from the running mean in running standard deviations; 0 on the first call and whenever the signal is flat. |
BimodalThresholder
class BimodalThresholder(var alpha: Float = 0.05f, var interpolate: Boolean = false) : FloatFilter
Online two-means clustering of a stream: it keeps two centroids, moves whichever is nearer toward each new sample, and reports which of the two the sample joined. For a signal that really does live at two levels — a switch, a contact sensor, a light that is on or off, a gesture held or not — this finds the two levels for you instead of making you guess a threshold, and it follows them as they drift.
| Declaration | Summary |
|---|---|
override fun invoke(value: Float): Float |
-1f or +1f for the lower or upper cluster — or the interpolated position between the centroids when interpolate is set — and 0f until both centroids have been seeded. |
PercentileThresholder
Reports where the current sample ranks within the last windowSize samples: the fraction of the rest of the window lying strictly below it. (The current sample is in the window but can never count itself, so the denominator is one less than the fill level and a new high reads exactly 1.0.) Completely distribution-free — no mean, no standard deviation, no fitted law — so a heavy-tailed or lopsided signal cannot mislead it, and one wild outlier moves the output by exactly one rank.
| Declaration | Summary |
|---|---|
override fun invoke(value: Float): Float |
The fraction of the rest of the window strictly below value — mapped onto -1..1, or left as 0..1 when mapToMinusOneToOne is false. |
DynamicRangeCompressor
A proper dB-domain compressor: envelope follower, soft knee, attack and release in milliseconds, makeup gain. Where AdaptiveCompander works in linear amplitude with per-sample coefficients, this one is specified the way a compressor usually is, which makes it the one to use on anything audio-shaped, or on anything whose timing you would rather state in milliseconds than in samples.
| Declaration | Summary |
|---|---|
var thresholdDb: Float |
Level (dB, full scale = 0) above which gain reduction begins. Negative values are the norm. |
var ratio: Float |
Compression ratio above the knee: 4 means 4 dB in over threshold yields 1 dB out. 1 is no compression at all. |
var kneeWidthDb: Float |
Total width (dB) of the soft knee straddling thresholdDb — compression eases in across this span instead of switching on abruptly. 0 gives a hard knee. |
var attackMs: Float |
How quickly (ms) the envelope follows a rising signal. Converted to a per-sample coefficient using sampleRate. |
var releaseMs: Float |
How quickly (ms) the envelope falls after a peak, and so how quickly the gain recovers. Also converted using sampleRate. |
var sampleRate: Float |
The rate (Hz) at which samples arrive, which is what gives attackMs and releaseMs their meaning. The 44100 default is an audio assumption — set it to your frame rate for a control signal. |
var makeupGainDb: Float |
Flat gain (dB) applied after the compression, to win back the headroom it just took. Applied at all levels, reduction or not. |
override fun invoke(value: Float): Float |
Updates the envelope, reads the knee curve at that level, and returns value scaled by the resulting gain plus makeupGainDb. |
ForecastJudge.kt
A forecaster used as a second opinion on a sequence memory's choices, and the one place where the two ways of continuing a gesture meet.
ForecastJudge implements afield.math.oracle.ContinuationJudge over any TemporalSession. It
watches every frame the improviser actually plays, asks the session for a fresh forecast at a
fixed interval in the background, and scores a proposed passage by its mean pinball loss
against that forecast's quantiles, per channel and divided by the channel's spread. The score
never reaches the output: the improviser still plays its memory, and the judge only changes
which part of it.
What the score can and cannot say is set by the forecast it reads. The quantiles are per-step marginals, so the loss asks whether each step of a passage sits inside the forecast's fan at that step, not how likely the passage is as a whole. Until the first forecast arrives, and wherever a passage runs past the end of the latest one, the judge returns no opinion and the improviser decides alone. The first failure the session reports is recorded and no further forecasts are requested.
Forecasts are collected without blocking the frame that asked for them, so an interactive
session's timing does not depend on the model's latency, which the judge also records. For an
offline render that has to come out the same on any machine, lockstep waits for each request
instead.
src/jvmMain/kotlin/afield/math/temporal/ForecastJudge.kt — read the source
ForecastJudge
A forecaster as a ContinuationJudge: the hybrid where the oracle proposes and the forecaster judges.
| Declaration | Summary |
|---|---|
val session: TemporalSession |
|
val sampleRate: Double |
|
val channels: List<String> |
|
val history: Double |
|
val horizon: Double |
|
val refresh: Double |
|
val lockstep: Boolean |
|
var forecasts |
Forecasts received. |
val latencies |
Request-to-reply latency of every forecast, in seconds. |
var failure: Throwable? |
First failure reported by the session; the judge then has no opinion. |
override fun observe(index: Long, frame: DoubleArray) |
|
override fun loss(index: Long, path: Array<DoubleArray>): Double? |
|
override fun close() |
TemporalSession.kt
Continuing a multichannel time series live, from any backend that can propose a future.
A TemporalSession answers ContinuationRequests (recent history in, proposed future out)
asynchronously. RecedingHorizon turns a session into something a frame loop can step
without ever waiting: it keeps a buffer of planned samples, commits only a short prefix of
each plan, replans in the background, drops replies made stale by a takeover, and reports
underruns instead of extrapolating. Two sessions ship: OracleSession (explicit sequence
memory, afield.math.oracle) and TimestralSession (Mistral's pretrained forecaster, in a
separate Python worker). Tests: RecedingHorizonTest.
src/jvmMain/kotlin/afield/math/temporal/TemporalSession.kt — read the source
Used in: Continuing a Gesture: Sequence Memory and Forecasting
Top-level functions
| Declaration | Summary |
|---|---|
fun Continuation.quantileIndex(q: Double): Int |
Index of the quantile level nearest q (0.5 is the median). Throws without quantiles. |
fun Continuation.medianFLine(channel: Int, xs: DoubleArray): afield.geometry.path.FLine |
The proposal on channel drawn as y(x), with xs (one x per horizon step) — the thick line of a forecast plot. |
fun Continuation.quantileFLine(channel: Int, q: Double, xs: DoubleArray): afield.geometry.path.FLine |
The q quantile on channel drawn as y(x). |
fun Continuation.bandFLine(channel: Int, lower: Double, upper: Double, xs: DoubleArray): afield.geometry.path.FLine |
The region between the lower and upper quantiles on channel, as one closed FLine ready to fill. Nest several (0.01..0.99, 0.1..0.9, 0.25..0.75) for a fan chart. Like a Gaussian process's interval, each band is pointwise: it describes one step at a time, and no single performance is expected to stay inside it. |
TemporalCapabilities
What a TemporalSession can do, declared rather than discovered.
ContinuationRequest
A request for horizon samples continuing history. revision identifies the history the request was made from; owned marks performer-driven channels (owned[channel](imaging.md#imaging-kt--channel)), and known carries covariates over history + horizon for a session with futureCovariates.
| Declaration | Summary |
|---|---|
val revision: Long |
|
val history: Trajectory |
|
val horizon: Int |
|
val owned: BooleanArray |
|
val known: Array<DoubleArray>? |
Continuation
A proposed future: proposalchannel for t in 0 until horizon, starting at the sample after the request's history. quantileschannelq` at [quantileLevels](math.md#temporalsession-kt--continuation--quantilelevels) when the session provides them — an uncertainty display per sample, not a set of joint performances. [diagnostics`](math.md#temporalsession-kt--continuation--diagnostics) is backend-specific (latency, source positions, model identity).
| Declaration | Summary |
|---|---|
val revision: Long |
|
val proposal: Array<DoubleArray> |
|
val quantiles: Array<Array<DoubleArray>>? |
|
val quantileLevels: DoubleArray? |
|
val diagnostics: Map<String, Any?> |
|
val horizon: Int |
TemporalSession
interface TemporalSession : AutoCloseable
A backend that proposes continuations. Replies may arrive on any thread.
| Declaration | Summary |
|---|---|
val capabilities: TemporalCapabilities |
|
fun propose(request: ContinuationRequest): CompletableFuture<Continuation> |
|
override fun close() |
OracleSession
The oracle as a TemporalSession: cue the memory with the request's history (only the performer's channels, if any are owned), then let an Improviser play forward from the best match for the horizon. Diagnostics carry source (memory sample per proposed sample) and cueDistance. Proposals are computed on the caller's thread and complete immediately.
| Declaration | Summary |
|---|---|
override val capabilities |
|
override fun propose(request: ContinuationRequest): CompletableFuture<Continuation> |
Underflow
What RecedingHorizon does when the plan runs out before a new one arrives.
Constants: Hold, Stop.
RecedingHorizon
Plays a TemporalSession in a frame loop without blocking on it.
| Declaration | Summary |
|---|---|
val session: TemporalSession |
|
val history: Double |
|
val horizon: Double |
|
val commit: Double |
|
val releaseTime: Double |
|
val underflow: Underflow |
|
val lockstep: Boolean |
|
var revision |
Bumped by every takeover, release and reset; replies from older revisions are dropped. |
var underruns |
Steps on which no planned sample was available. |
var staleReplies |
Replies discarded as stale. |
val latencies |
Request-to-reply latency of every accepted plan, in seconds. |
val currentPlan: Continuation? |
The most recent accepted plan, for plotting its envelope. |
val currentPlanStart: Long |
Output sample index at which currentPlan starts. |
var releaseCorrection |
Size of the fade applied on the last step (scaled by channel spread), 0 when none. |
var failure: Throwable? |
First failure reported by the session, if any; the player then follows underflow. |
fun perform(channel: Int, value: Double) |
|
fun perform(name: String, value: Double) |
|
fun release(channel: Int) |
|
fun release(name: String) |
|
fun reset() |
Drops the plan and anything in flight; the next step replans from the current history. |
fun step(): DoubleArray |
Advances one sample; never blocks unless lockstep. |
fun take(samples: Int, script: (RecedingHorizon.(Int) -> Unit)? = null): Played |
Runs samples steps (with an optional per-step script) and returns what was played. |
fun take(seconds: Double, script: (RecedingHorizon.(Int) -> Unit)? = null) |
|
override fun close() |
Played
What a RecedingHorizon played. band[channel](imaging.md#imaging-kt--channel)0|1`` is the outer quantile pair of the plan each sample came from (NaN without quantiles) — the forecaster's uncertainty at that sample, not a bound on the output.
| Declaration | Summary |
|---|---|
val output: Trajectory |
|
val owned: Array<BooleanArray> |
|
val band: Array<Array<DoubleArray>> |
|
val underruns: Int |
|
val staleReplies: Int |
|
val latencies: List<Double> |
|
fun latency(p: Double): Double |
The pth percentile of latencies, in seconds. |
TimestralSession.kt
Mistral's pretrained Timestral forecaster behind this package's session interface, running in a Python worker that Field starts and owns.
TimestralSession has never seen the signal it is continuing and needs no training step: it
reads the recent history and returns a median forecast with a quantile envelope around it. The
model runs in a separate process under uv run --script, in an environment uv builds from the
worker script's own pinned dependencies, so Torch never loads into Field or into the venv the
Python boxes share. Field talks to the worker over length-prefixed frames on its standard
input and output, one worker per session, and a session created inside a box run stops its
worker when the box stops or re-runs.
Because the model is optional, this file also answers whether it can be used at all. status
reports whether uv, the worker script and the checkpoint are all present, without starting
anything, and names the step that would supply the first one missing; ProviderStatus is that
answer. Nothing downloads until install is called.
The forecast's quantiles are per-sample marginals, so this session offers no sampled
alternatives: drawing each step independently from its own quantile would invent a dependence
between steps that the model never predicted. TimestralSessionTest exercises a real worker,
and only when AFIELD_TIMESTRAL_TEST=1 and the checkpoint is already installed.
src/jvmMain/kotlin/afield/math/temporal/TimestralSession.kt — read the source
ProviderStatus
Where an optional provider stands, with the reason when it is not usable.
TimestralSession
Mistral's Timestral forecaster (Apache-2.0 code, Modified-MIT weights) as a TemporalSession, running zero-shot with no fine-tuning.
| Declaration | Summary |
|---|---|
val model: String |
|
val device: String |
|
val symmetricAveraging: Boolean |
|
val clipNonnegative: Boolean |
|
val threads: Int |
|
override val capabilities |
|
val info: JsonObject |
What the worker reported at start: model, versions, device, load time. |
val quantileLevels: DoubleArray |
Quantile levels of every envelope, lowest first. |
override fun propose(request: ContinuationRequest): CompletableFuture<Continuation> |
|
override fun close() |
|
override fun toString() |
units.kt
Physical units for Field's spreadsheet-style numbers. A Quantity pairs a KotUniL
Expression (normalized to SI) with the display unit the user actually wrote, so 3 | mm
still prints as "3 mm".
withUnit is what that | unit syntax lowers to, and the operator overloads here let a
Quantity mix with plain Numbers and bare Expressions in either order under ordinary
dimensional-analysis rules. The rest of the file is presentation: recognizeDerivedUnit
matches a dimensional signature against a table of named SI derived units (N, J, W, Pa, Hz, V,
Ω, ...), autoScaleExpression chooses an SI prefix, and formatUnitSuperscripts rewrites
kg*m2/s2 as kg·m²/s². formatExpression runs those in that order.
The unit algebra itself is KotUniL's, not Field's. Two limits are visible in the code: only single-factor dimensions are auto-scaled (compound ones like m/s pass through unchanged), and the derived-unit table is a fixed list, so anything outside it falls back to raw SI symbols.
src/jvmMain/kotlin/afield/math/units/units.kt — read the source
Top-level functions
Quantity
A numeric value with tracked physical units.