Skip to content

Data

Structures and processes over data rather than over pixels: clustering and nearest-neighbour search, generative and constraint systems, graph dynamics, and the I/O helpers (files, JSON, subprocesses, OSC-adjacent devices) that get data into a box.

26 source file(s), 1061 public symbol(s), 502 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.

SemanticSearchManager.kt

Box-facing wrapper around afield.ai.retrieval.SemanticRetrievalService, Field's embedding-based search over source, docs and workspace boxes. Make a SemanticSearchManager and call SemanticSearchManager.find for the full SemanticSearchResult (message, status, hits) or SemanticSearchManager.findTopK / SemanticSearchManager.findWorkspaceTopK when you just want a List<SemanticSearchHit>; each hit carries score, kind, corpus, path and line range, and prints as one readable line. Queries can be narrowed by corpus, path prefix, kind, document, box or property, and note that find passes rebuildIfEmpty = true by default.

The remaining methods administer the service rather than query it: SemanticSearchManager.status, SemanticSearchManager.settings, SemanticSearchManager.enable, SemanticSearchManager.saveSettings, SemanticSearchManager.downloadPreset, SemanticSearchManager.rebuild, SemanticSearchManager.deleteIndex and SemanticSearchManager.deleteModels. Every call delegates straight through and blocks; the only state kept here is SemanticSearchManager.lastResult.

src/jvmMain/kotlin/afield/ai/retrieval/SemanticSearchManager.kt — read the source

SemanticSearchResult

data class SemanticSearchResult(val success: Boolean, val message: String, val hits: List<SemanticSearchHit>, val status: SemanticRetrievalStatus)

Declaration Summary
val success: Boolean
val message: String
val hits: List<SemanticSearchHit>
val status: SemanticRetrievalStatus

SemanticSearchHit

data class SemanticSearchHit(val title: String, val symbol: String, val kind: String, val corpus: String, val path: String, val line: Int, val endLine: Int, val score: Double, val id: String, val scale: String, val documentName: String = "", val boxName: String = "", val boxId: String = "", val property: String = "", val language: String = "", val capsule: String = "", val sourcePreview: String = "")

Declaration Summary
val title: String
val symbol: String
val kind: String
val corpus: String
val path: String
val line: Int
val endLine: Int
val score: Double
val id: String
val scale: String
val documentName: String
val boxName: String
val boxId: String
val property: String
val language: String
val capsule: String
val sourcePreview: String
val location: String
override fun toString(): String

SemanticSearchManager

class SemanticSearchManager

Declaration Summary
var lastResult: SemanticSearchResult?
fun status(): SemanticRetrievalStatus
fun settings(): SemanticRetrievalSettingsStatus
fun enable(enabled: Boolean = true): SemanticRetrievalSettingsStatus
fun saveSettings(enabled: Boolean? = null, presetId: String? = null, modelPath: String? = null, tokenizerPath: String? = null, dimension: Int? = null, maxTokens: Int? = null, queryPrefix: String? = null, documentPrefix: String? = null): SemanticRetrievalSettingsStatus
fun downloadPreset(presetId: String = settings().config.presetId): SemanticRetrievalSettingsStatus
fun rebuild(): SemanticRetrievalBuildResult
fun deleteIndex(): SemanticRetrievalSettingsStatus
fun deleteModels(): SemanticRetrievalSettingsStatus
fun find(query: String, k: Int = 10, corpus: String = "", corpora: List<String> = emptyList(), pathPrefix: String = "", kinds: List<String> = emptyList(), documentName: String = "", boxId: String = "", property: String = "", rebuildIfEmpty: Boolean = true, includeCapsule: Boolean = false, includeSource: Boolean = false): SemanticSearchResult
fun findTopK(query: String, k: Int = 10, corpus: String = "", corpora: List<String> = emptyList(), pathPrefix: String = "", kinds: List<String> = emptyList(), documentName: String = "", boxId: String = "", property: String = "", includeCapsule: Boolean = false, includeSource: Boolean = false): List<SemanticSearchHit>
fun findWorkspaceTopK(query: String, k: Int = 10, documentName: String = "", boxId: String = "", property: String = "", includeSource: Boolean = false): List<SemanticSearchHit>

jsonparsing.kt

Two related things: a small hand-written JSON reader, and a macro that generates a typed parser from a terse schema. parseJson turns text into a JValue tree — JObject, JArray, JString, JNumber (which keeps the raw digits rather than converting), JBool, JNull — and throws JsonParseException with a character offset on bad input. No external JSON library is involved.

JsonSchemaGen.generateSource compiles a schema such as {name: s, at: [f, f], tags:s..., ...} into Kotlin source for a data class with a fromString companion: f/i/s are the leaves, x... is a list, [a, b] is a Pair (2, 3 or 4 floats collapse to Vec2/Vec3/ Vec4), and a trailing ... permits extra JSON fields instead of rejecting them. In a box you reach this through the jsonParser<Name>("schema") macro registered by registerJsonParsingMacros, which inserts the generated source into the preamble. Inline nested objects are not supported.

src/jvmMain/kotlin/afield/data/formats/jsonparsing.kt — read the source

Top-level functions

Declaration Summary
fun registerJsonParsingMacros() Registers the jsonParser<Name>("schema") macro. Called by Field during macro registration, not by you.
fun parseJson(src: String): JValue Parses JSON text into a JValue tree. No external library, no reflection, no schema needed.

JValue

sealed interface JValue

A node of a parsed JSON document — one of JObject, JArray, JString, JNumber, JBool or JNull.

JObject

data class JObject(val fields: Map<String, JValue>) : JValue

A JSON object. Field order is whatever the Map preserves, and a duplicate key keeps the last occurrence.

JArray

data class JArray(val items: List<JValue>) : JValue

A JSON array, in document order. Mixed element types are fine — each item is just a JValue.

JString

data class JString(val value: String) : JValue

A JSON string, with escapes already decoded — so value is the real text, not the source form.

JNumber

data class JNumber(val raw: String) : JValue

A JSON number, kept as the raw digits rather than converted.

JBool

data class JBool(val value: Boolean) : JValue

A JSON true or false.

JNull

data object JNull : JValue

JSON null. A singleton, so value === JNull works as well as is JNull.

JsonParseException

class JsonParseException(message: String) : RuntimeException(message)

Thrown by parseJson on malformed input, with the character offset in the message.

JsonSchemaGen

object JsonSchemaGen

Turns a terse schema string into Kotlin source for a data class with a fromString companion — the code generator behind the jsonParser macro.

Declaration Summary
fun generateSource(className: String, schemaText: String): String Generates the Kotlin source for a data class named className that parses JSON matching schemaText, with a fromString companion function.
sealed interface SchemaNode
data class Field(val name: String, val schema: SchemaNode)
enum class TokKind
data class Tok(val kind: TokKind, val text: String, val pos: Int)
class SchemaTokenizer(private val src: String)
class SchemaParser(schemaText: String)

dynamic_graph_field.kt

The two useful ways to look at dynamic_graph_systems.kt — the point cloud and the weighted edges joining it, so a time-varying graph can be watched rather than described.

dynamic_graph_systems.kt imports nothing but kotlin.math: every mention of Img, FLine or Field's visualizer in this family is here.

Which of the two to use

you want call why
a figure — one state, on a page, in a box dynamicGraphFLines → Box.showPlaneGrid vector: real FLines through drawToSVG, so it scales, and edge weight drives colour and thickness
a frame of an animation dynamicGraphPanel → Box.animateEvolution raster: Box.animateEvolution encodes a montage of Imgs per frame, and there is no vector path into an mp4

dynamicGraphFLines is the default, because a figure here is FLine → planeFigureHtml/drawToSVG rather than a scan-converted pixel buffer. The raster path is for clips: Box.animateEvolution's panels lambda is typed List<Pair<String, Img>> (alife_animate.kt), so pixels are what it will take. Outside an animateEvolution, dynamicGraphPanel is the wrong call.

The range policy, which applies to both paths:

  • The world window is the caller's, and it is fixed. dynamicGraphImg takes an explicit window, and Box.showPlaneGrid takes one too, rather than fitting to the cloud: a cloud that is moving and growing re-fitted every frame appears to stand still while the world shrinks around it. dynamicGraphWindow computes a window once, from a reference state, for you to keep; dynamicGraphPlaneWindow converts it for the vector path.
  • Edge weight is read against a maxWeight named by the caller, not normalised per frame or per panel. Per-frame normalisation makes a graph that is uniformly saturating look identical to one that is uniformly dying.
  • Points are drawn at the top of the ramp and edges below it, so the population is legible even where the graph has decayed to nothing — an empty frame and a frame of isolated points are different states and must look different.
  • A figure that clips says so. dynamicGraphImg reports how many points fell outside the window in DynamicGraphRaster.clipped, and dynamicGraphOutsideWindow answers the same question for the vector path.

src/jvmMain/kotlin/afield/data/graph/dynamic_graph_field.kt — read the source

Used in: Graphs that change while you watch

Top-level functions

Declaration Summary
fun dynamicGraphWindow(points: List<Vec3>, padFraction: Double = 0.12, project: (Vec3) -> Pair<Double, Double> = { it.x.toDouble() to it.y.toDouble() }): DoubleArray A world window [xMin, yMin, xMax, yMax] around points, padded by padFraction of the larger span and forced square so the aspect ratio of the raster matches the aspect ratio of the world.
fun dynamicGraphFLines(points: List<Vec3>, edges: Collection<DynamicGraphEdge>, maxWeight: Double = 1.0, colormap: String = "magma", minEdgeIntensity: Double = 0.12, thinEdge: Double = 0.8, thickEdge: Double = 1.8, pointColor: Vec4? = null, pointSize: Double = 0.5, project: (Vec3) -> Pair<Double, Double> = { it.x.toDouble() to it.y.toDouble() }): List<FLine> A dynamic graph as vector geometry: one two-node polyline per edge, coloured and thickened by that edge's weight, followed by one dot-drawn FLine carrying every point.
fun dynamicGraphPlaneWindow(window: DoubleArray): Rect A dynamicGraphWindow result as the Rect Box.showPlaneGrid and planeFigureHtml want, with y negated so it frames what dynamicGraphFLines actually emitted.
fun dynamicGraphOutsideWindow(points: List<Vec3>, window: DoubleArray, project: (Vec3) -> Pair<Double, Double> = { it.x.toDouble() to it.y.toDouble() }): Int How many of points fall outside window — so a caption can say it.
fun dynamicGraphImg(points: List<Vec3>, edges: Collection<DynamicGraphEdge>, window: DoubleArray, width: Int = 480, height: Int = 480, maxWeight: Double = 1.0, pointRadius: Int = 1, minEdgeIntensity: Float = 0.12f, project: (Vec3) -> Pair<Double, Double> = { it.x.toDouble() to it.y.toDouble() }): DynamicGraphRaster Rasterise a dynamic graph into one frame of an animation: every edge as a scan-converted line whose brightness is its weight, every point as a dot at full intensity.
fun dynamicGraphPanel(points: List<Vec3>, edges: Collection<DynamicGraphEdge>, window: DoubleArray, width: Int = 480, height: Int = 480, maxWeight: Double = 1.0, colormap: String = "magma", pointRadius: Int = 1, project: (Vec3) -> Pair<Double, Double> = { it.x.toDouble() to it.y.toDouble() }): Img dynamicGraphImg straight through Img.colormap with a fixed 0 .. 1 range — the panel of an animation frame, not a figure.
fun dynamicGraphSummary(state: DynamicPointState, edges: DynamicEdgeList): String What a dynamic-graph figure needs in its caption: point and edge counts, the weight range and mean, mean degree, how many points are isolated, and the state's clock.

DynamicGraphRaster

data class DynamicGraphRaster(val img: Img, val clipped: Int, val heaviestWeight: Double)

What dynamicGraphImg drew into one animation frame, and what it had to leave out.

Declaration Summary
val img: Img 1-channel intensity in 0 .. 1: 1 at a point, weight / maxWeight along an edge.
val clipped: Int Points that fell outside the window and were not drawn.
val heaviestWeight: Double The heaviest edge actually drawn, in weight units — for a caption that names its own scale.

dynamic_graph_systems.kt

Time-varying point-and-edge systems: point clouds that move every frame while the graph joining them is continually reinforced and decayed. State lives in DynamicPointState (points, optional Vec4 labels, a clock), edges in DynamicEdgeList (weight / age / ttl per edge, thinned by DynamicEdgeList.decayLinear or DynamicEdgeList.decayExponential), and neighbour queries go through Vec3SpatialHash, which is rebuilt each step because the cloud moves.

There are two extension points. A DynamicGraphSystem only adds and reinforces edges — Vec3BreathingAnisotropicKnnSystem, Vec3FlowlineAdvectionGraphSystem, Vec3LocalTangentTriangulationSystem, Vec3PersistentProximityHysteresisSystem, Vec3SignalRoutingGraphSystem, Vec3AlphaEventHypergraphCollapseSystem. A DynamicPointProcess moves, adds and removes points against DynamicPointKinetics (velocity / resource / alive) and several are composed in order by DynamicProcessPipeline.

Drive one frame with stepDynamicGraph or its decaying variants; dynamicGraphSnapshot and DynamicEdgeList.toPointGraph convert the live edge store into the static PointGraph the rest of the graph library expects, and exampleDynamicMorphogenesisStep wires a whole frame together. Pure Kotlin, single-threaded, no external dependency.

The population is not fixed, and three things have to agree about its size: state.points, DynamicPointKinetics (via DynamicPointKinetics.ensureSize) and DynamicEdgeList.pointCount (via DynamicEdgeList.ensurePointCount / DynamicEdgeList.setPointCount). stepDynamicGraph, DynamicProcessPipeline.step and exampleDynamicMorphogenesisStep keep all three in step for you; a hand-written DynamicPointProcess that appends a point and joins an edge to it in the same call has to widen the store itself. See DynamicEdgeList for what happens when it does not.

src/jvmMain/kotlin/afield/data/graph/dynamic_graph_systems.kt — read the source

Used in: Graphs that change while you watch

Top-level functions

Declaration Summary
fun dynamicGraphSnapshot(dynamicEdges: Collection<DynamicGraphEdge>, pointCount: Int, minWeight: Double = 0.0): PointGraph The live edges as a static PointGraph, keeping those above minWeight. The lifetime fields are dropped, so the rest of the graph library can take the result.
fun stepDynamicGraph(dt: Float, state: DynamicPointState, edges: DynamicEdgeList, spatialHash: Vec3SpatialHash, system: DynamicGraphSystem) One frame of system: widen the store to the cloud's current size, rebuild spatialHash over the points, then step. Nothing here decays: no edge ages, none has its time to live counted down, and none is dropped. The decaying variants below add that.
fun stepDynamicGraphWithLinearDecay(dt: Float, state: DynamicPointState, edges: DynamicEdgeList, spatialHash: Vec3SpatialHash, system: DynamicGraphSystem, weightDecayPerSecond: Double) stepDynamicGraph, then DynamicEdgeList.decayLinear: every edge ages by dt and loses weightDecayPerSecond × dt of weight, and one that reaches zero weight or runs out of time to live is dropped. Weight falls to zero in a time proportional to it, so a strong edge outlives a weak one in proportion.
fun stepDynamicGraphWithExponentialDecay(dt: Float, state: DynamicPointState, edges: DynamicEdgeList, spatialHash: Vec3SpatialHash, system: DynamicGraphSystem, lambda: Double) stepDynamicGraph, then DynamicEdgeList.decayExponential: every edge ages by dt and its weight is multiplied by exp(−lambda × dt), with the edge dropped once that falls to 1e-9 or it runs out of time to live. Weight approaches zero without reaching it, so unlike the linear variant the time an edge survives grows only with the logarithm of its weight.
fun exampleDynamicMorphogenesisStep(state: DynamicPointState, kinetics: DynamicPointKinetics, edges: DynamicEdgeList, spatialHash: Vec3SpatialHash, edgeSystem: DynamicGraphSystem, dt: Float = 1f / 60f, maxPointCount: Int = Int.MAX_VALUE) One whole frame of morphogenesis: build and decay the graph with edgeSystem, then run five standard processes over it in order — diffuse resource and climb its gradient, relax the springs, grow tips, merge what has collided, smooth.

Top-level values

Declaration Summary
const val DYNAMIC_EDGE_FLAG_MERGED Set on an edge that arrived on its endpoint by way of a MergeClosePointsProcess transfer.

DynamicPointState

data class DynamicPointState(val points: MutableList<Vec3>, val labels: List<Vec4>? = null, var time: Float = 0f)

The point cloud a DynamicGraphSystem reads and a DynamicPointProcess moves: the positions, optional per-point labels, and the clock.

Declaration Summary
val points: MutableList<Vec3> The positions, mutated in place; a process may append to or shorten this list.
val labels: List<Vec4>? One label per point, read by the systems that bias towards like neighbours. A process that adds or removes points needs this to be a MutableList, and says so by name if it is not.
var time: Float Seconds accumulated by the systems: each DynamicGraphSystem's step adds its own dt.
val pointCount: Int How many points there are now, which is the index space every system walks.

DynamicGraphEdge

data class DynamicGraphEdge(val i: Int, val j: Int, var weight: Double, var age: Float = 0f, var ttl: Float = 0f, var flags: Int = 0)

An edge with a lifetime: a weight the systems reinforce, plus an age, a countdown and a flag word. GraphEdge, which the static graph library takes, carries only the weight.

Declaration Summary
val i: Int One end. In an undirected DynamicEdgeList this is the lower index; otherwise the source.
val j: Int The other end. In an undirected DynamicEdgeList this is the higher index; otherwise the target.
var weight: Double Strength, raised by DynamicEdgeList.reinforce and lowered by the two decay passes.
var age: Float Seconds this edge has existed. Only the decay passes advance it.
var ttl: Float Seconds of life left; the decay passes count it down and drop the edge at zero.
var flags: Int Bit field, currently just DYNAMIC_EDGE_FLAG_MERGED.
fun toGraphEdge(): GraphEdge This edge as a plain GraphEdge: the two ends and the weight, with the lifetime dropped.

DynamicEdgeList

class DynamicEdgeList(pointCount: Int, val undirected: Boolean = true)

Evolving edge store. Undirected by default to match GraphEdge equality semantics.

Declaration Summary
val undirected: Boolean
var pointCount: Int The index space: every edge endpoint must lie in 0 until pointCount.
fun values(): Collection<DynamicGraphEdge>
fun get(a: Int, b: Int): DynamicGraphEdge?
fun ensurePointCount(n: Int) Widen the index space to at least n. Never narrows, so it is always safe: call it right after appending points to a DynamicPointState and before joining an edge to the new index.
fun setPointCount(n: Int) Set the index space to exactly n, which is what a compaction needs — after removeDeadPointsInPlace has renumbered the survivors, a store still claiming the old count makes toPointGraph hand every traversal downstream a PointGraph full of vertices that no longer exist.
fun incident(index: Int): List<DynamicGraphEdge> Every edge with index as an endpoint, in O(1) after an O(V + E) index build that is shared by every call until the store next changes.
fun getOrCreate(a: Int, b: Int, initialWeight: Double = 0.0, ttl: Float = 0f): DynamicGraphEdge
fun reinforce(a: Int, b: Int, addWeight: Double, ttl: Float, maxWeight: Double)
fun clear()
fun decayLinear(dt: Float, weightDecayPerSecond: Double)
fun decayExponential(dt: Float, lambda: Double)
fun toPointGraph(minWeight: Double = 0.0): PointGraph Snapshot into your existing static PointGraph type.

Vec3SpatialHash

class Vec3SpatialHash(var cellSize: Double)

Spatial hash for dynamic local-neighborhood queries. This is the best fit here because the point cloud is expected to move each frame.

Declaration Summary
fun rebuild(points: List<Vec3>)
fun radiusNeighbors(i: Int, radius: Double): List<Neighbor>
fun kNearest(i: Int, k: Int, maxRadius: Double = Double.POSITIVE_INFINITY): List<Neighbor>
fun sample(center: Vec3, radius: Double): List<Neighbor> Direct sample query for off-point field evaluation. Unlike radiusNeighbors this includes a point sitting exactly on center — it is a query about a position, not about a member.

Neighbor

data class Neighbor(val index: Int, val distanceSquared: Double)

DynamicGraphSystem

interface DynamicGraphSystem

Declaration Summary
fun step(dt: Float, state: DynamicPointState, edges: DynamicEdgeList, spatialHash: Vec3SpatialHash)

Vec3BreathingAnisotropicKnnSystem

class Vec3BreathingAnisotropicKnnSystem(var k: Int, var ttl: Float, var reinforce: Double, var maxWeight: Double, var mutualOnly: Boolean = true, var queryRadius: Double = Double.POSITIVE_INFINITY) : DynamicGraphSystem

Declaration Summary
var k: Int
var ttl: Float
var reinforce: Double
var maxWeight: Double
var mutualOnly: Boolean
var queryRadius: Double
override fun step(dt: Float, state: DynamicPointState, edges: DynamicEdgeList, spatialHash: Vec3SpatialHash)

Vec3FlowlineAdvectionGraphSystem

class Vec3FlowlineAdvectionGraphSystem(var influenceRadius: Double, var stepLength: Double, var ttl: Float, var reinforce: Double, var maxWeight: Double, var swirlMix: Double = 0.45) : DynamicGraphSystem

Declaration Summary
var influenceRadius: Double
var stepLength: Double
var ttl: Float
var reinforce: Double
var maxWeight: Double
var swirlMix: Double
override fun step(dt: Float, state: DynamicPointState, edges: DynamicEdgeList, spatialHash: Vec3SpatialHash)

Vec3LocalTangentTriangulationSystem

class Vec3LocalTangentTriangulationSystem(var neighborhoodK: Int, var ttl: Float, var reinforce: Double, var maxWeight: Double, var maxNeighborRadius: Double = Double.POSITIVE_INFINITY) : DynamicGraphSystem

Declaration Summary
var neighborhoodK: Int
var ttl: Float
var reinforce: Double
var maxWeight: Double
var maxNeighborRadius: Double
override fun step(dt: Float, state: DynamicPointState, edges: DynamicEdgeList, spatialHash: Vec3SpatialHash)

Vec3PersistentProximityHysteresisSystem

class Vec3PersistentProximityHysteresisSystem(var rEnter: Double, var rExit: Double, var ttl: Float, var reinforce: Double, var maxWeight: Double, var labelBias: Double = 0.0) : DynamicGraphSystem

Declaration Summary
var rEnter: Double
var rExit: Double
var ttl: Float
var reinforce: Double
var maxWeight: Double
var labelBias: Double
override fun step(dt: Float, state: DynamicPointState, edges: DynamicEdgeList, spatialHash: Vec3SpatialHash)

Vec3SignalRoutingGraphSystem

class Vec3SignalRoutingGraphSystem(var neighborhoodK: Int, var ttl: Float, var reinforce: Double, var maxWeight: Double, var routeCount: Int = 1, var maxNeighborRadius: Double = Double.POSITIVE_INFINITY) : DynamicGraphSystem

Declaration Summary
var neighborhoodK: Int
var ttl: Float
var reinforce: Double
var maxWeight: Double
var routeCount: Int
var maxNeighborRadius: Double
override fun step(dt: Float, state: DynamicPointState, edges: DynamicEdgeList, spatialHash: Vec3SpatialHash)

Vec3AlphaEventHypergraphCollapseSystem

class Vec3AlphaEventHypergraphCollapseSystem(var neighborhoodK: Int, var alphaRadius: Double, var ttl: Float, var reinforcePerUnitArea: Double, var maxWeight: Double, var maxNeighborRadius: Double = Double.POSITIVE_INFINITY, var minArea: Double = 1e-5) : DynamicGraphSystem

Closes triangles rather than adding edges one at a time: every triple whose three sides all fit inside alphaRadius and whose area clears minArea gets all three of its sides reinforced by the same amount, so the graph this builds is made of faces.

Declaration Summary
var neighborhoodK: Int
var alphaRadius: Double
var ttl: Float
var reinforcePerUnitArea: Double
var maxWeight: Double
var maxNeighborRadius: Double
var minArea: Double
override fun step(dt: Float, state: DynamicPointState, edges: DynamicEdgeList, spatialHash: Vec3SpatialHash)

DynamicPointKinetics

data class DynamicPointKinetics(val velocity: MutableList<Vec3>, val resource: MutableList<Double>, val alive: MutableList<Boolean>)

Declaration Summary
val velocity: MutableList<Vec3>
val resource: MutableList<Double>
val alive: MutableList<Boolean>
fun ensureSize(n: Int)

DynamicPointProcess

interface DynamicPointProcess

Declaration Summary
fun step(dt: Float, state: DynamicPointState, kinetics: DynamicPointKinetics, edges: DynamicEdgeList, spatialHash: Vec3SpatialHash)

SpringMotionProcess

class SpringMotionProcess(var springK: Double = 1.0, var damping: Double = 0.15, var restLengthTimesWeight: Double = 1.0, var maxSpeed: Double = 0.25, var edgeWeightFloor: Double = 1e-6) : DynamicPointProcess

Hooke's law along the live edges: each edge pulls its endpoints toward a rest length, damped, with the speed clamped.

Declaration Summary
var springK: Double
var damping: Double
var restLengthTimesWeight: Double
var maxSpeed: Double
var edgeWeightFloor: Double
override fun step(dt: Float, state: DynamicPointState, kinetics: DynamicPointKinetics, edges: DynamicEdgeList, spatialHash: Vec3SpatialHash)

DirectedEdgeAdvectionProcess

class DirectedEdgeAdvectionProcess(var gain: Double = 0.2, var damping: Double = 0.1, var maxSpeed: Double = 0.3) : DynamicPointProcess

Declaration Summary
var gain: Double
var damping: Double
var maxSpeed: Double
override fun step(dt: Float, state: DynamicPointState, kinetics: DynamicPointKinetics, edges: DynamicEdgeList, spatialHash: Vec3SpatialHash)

DirectedTipGrowthProcess

class DirectedTipGrowthProcess(var coherenceThreshold: Double = 1.25, var spawnDistance: Double = 0.05, var cooldownWeight: Double = 0.25, var childEdgeWeight: Double = 1.0, var childEdgeTtl: Float = 2.0f, var maxWeight: Double = 8.0, var inheritResourceFactor: Double = 0.5, var localDensityRadius: Double = 0.10, var localDensityMax: Int = 8, var maxPointCount: Int = Int.MAX_VALUE) : DynamicPointProcess

Births a child ahead of any point whose incident edges pull coherently in one direction — the family's growth rule, and the one process here that makes the population a variable.

Declaration Summary
var coherenceThreshold: Double
var spawnDistance: Double
var cooldownWeight: Double
var childEdgeWeight: Double
var childEdgeTtl: Float
var maxWeight: Double
var inheritResourceFactor: Double
var localDensityRadius: Double
var localDensityMax: Int
var maxPointCount: Int
override fun step(dt: Float, state: DynamicPointState, kinetics: DynamicPointKinetics, edges: DynamicEdgeList, spatialHash: Vec3SpatialHash)

MergeClosePointsProcess

class MergeClosePointsProcess(var mergeDistance: Double = 0.025, var minCombinedWeight: Double = 1.5, var blend: Double = 0.5) : DynamicPointProcess

Collapses pairs of points that are both within mergeDistance of each other and joined by an edge of at least minCombinedWeight: the higher-indexed one is absorbed into the lower, which takes the blended position, the blended velocity, the summed resource and every edge the absorbed point had.

Declaration Summary
var mergeDistance: Double
var minCombinedWeight: Double
var blend: Double
override fun step(dt: Float, state: DynamicPointState, kinetics: DynamicPointKinetics, edges: DynamicEdgeList, spatialHash: Vec3SpatialHash)

SignalDiffusionAndChemotaxisProcess

class SignalDiffusionAndChemotaxisProcess(var diffusionRate: Double = 0.35, var chemoGain: Double = 0.2, var downhill: Boolean = false, var damping: Double = 0.08, var maxSpeed: Double = 0.25) : DynamicPointProcess

Declaration Summary
var diffusionRate: Double
var chemoGain: Double
var downhill: Boolean
var damping: Double
var maxSpeed: Double
override fun step(dt: Float, state: DynamicPointState, kinetics: DynamicPointKinetics, edges: DynamicEdgeList, spatialHash: Vec3SpatialHash)

AddNearLowDensityProcess

class AddNearLowDensityProcess(var probeRadius: Double = 0.10, var attemptsPerStep: Int = 1, var offsetScale: Double = 0.06) : DynamicPointProcess

Declaration Summary
var probeRadius: Double
var attemptsPerStep: Int
var offsetScale: Double
override fun step(dt: Float, state: DynamicPointState, kinetics: DynamicPointKinetics, edges: DynamicEdgeList, spatialHash: Vec3SpatialHash)

AddNearHighDensityProcess

class AddNearHighDensityProcess(var probeRadius: Double = 0.10, var minNeighbors: Int = 8, var attemptsPerStep: Int = 1, var jitterScale: Double = 0.02) : DynamicPointProcess

Declaration Summary
var probeRadius: Double
var minNeighbors: Int
var attemptsPerStep: Int
var jitterScale: Double
override fun step(dt: Float, state: DynamicPointState, kinetics: DynamicPointKinetics, edges: DynamicEdgeList, spatialHash: Vec3SpatialHash)

RelaxToMidpointProcess

class RelaxToMidpointProcess(var gain: Double = 0.12, var degreeFloor: Int = 1) : DynamicPointProcess

Declaration Summary
var gain: Double
var degreeFloor: Int
override fun step(dt: Float, state: DynamicPointState, kinetics: DynamicPointKinetics, edges: DynamicEdgeList, spatialHash: Vec3SpatialHash)

PruneIsolatedPointsProcess

class PruneIsolatedPointsProcess(var minDegree: Int = 1, var minResource: Double = -Double.MAX_VALUE) : DynamicPointProcess

Kills, and then compacts away, every point below minDegree incident edges or below minResource resource.

Declaration Summary
var minDegree: Int
var minResource: Double
override fun step(dt: Float, state: DynamicPointState, kinetics: DynamicPointKinetics, edges: DynamicEdgeList, spatialHash: Vec3SpatialHash)

JitterProcess

class JitterProcess(var amplitude: Double = 0.005) : DynamicPointProcess

Declaration Summary
var amplitude: Double
override fun step(dt: Float, state: DynamicPointState, kinetics: DynamicPointKinetics, edges: DynamicEdgeList, spatialHash: Vec3SpatialHash)

DynamicProcessPipeline

class DynamicProcessPipeline(var processes: List<DynamicPointProcess>)

Declaration Summary
var processes: List<DynamicPointProcess>
fun step(dt: Float, state: DynamicPointState, kinetics: DynamicPointKinetics, edges: DynamicEdgeList, spatialHash: Vec3SpatialHash)

growing.kt

A 3-regular graph that grows by local rewriting. Every node in a GrowingGraph has exactly three neighbours and one bit of state, and GrowingGraph.grow alternates two phases: compute each node's next state and a "dividing" flag by indexing an integer rule with neighbour-count + 4 × state, then apply a division to each flagged node that adds two nodes and rewires their edges.

Seed the adjacency with GrowingGraph.setNeighbors (it must be a consistent 3-regular graph or the rewrite throws), set states with GrowingGraph.setState or GrowingGraph.randomizeStates, then call grow() per step and use the returned GrowingGraph.GrowDelta together with GrowingGraph.newEdgeU / GrowingGraph.newEdgeV to draw only what changed. Storage is flat IntArray/ByteArray with no per-node allocation and no allocation in the inner loops except when capacity doubles; pure Kotlin, single-threaded, with a built-in xorshift64* generator for the optional flipProb noise. main is a runnable 10-node demo.

src/jvmMain/kotlin/afield/data/graph/growing.kt — read the source

Top-level functions

Declaration Summary
fun main() Small demo: start from K4 (tetrahedron), run a few steps, print size/state stats.

GrowingGraph

class GrowingGraph(initialNodeCount: Int, rule: Int = 2182, flipProb: Double = 0.0, seed: Long = 1L)

Self-contained Kotlin port of the "GrowingGraph" algorithm: - 3-regular graph: each node has exactly 3 neighbors - Binary state per node (0/1) - Two-phase grow(): Phase 0: compute next state + "dividing" flag from local neighborhood Phase 1: apply local rewrite (division) that adds 2 nodes and rewires edges

Declaration Summary
var nodeCount: Int
var rule: Int
var flipProb: Double
data class GrowDelta(val newNodeStart: Int, val newNodeCount: Int, val newEdgeStart: Int, val newEdgeCount: Int)
val newEdgeU: IntArray
val newEdgeV: IntArray
fun setNeighbors(node: Int, a: Int, b: Int, c: Int) Set neighbor list for a node (must be exactly 3 neighbors).
fun neighborAt(node: Int, slot: Int): Int Get the neighbor at slot 0..2 for a node.
fun stateOf(node: Int): Int Get state (0/1).
fun setState(node: Int, s: Int) Set state (0/1).
fun randomizeStates(p: Double) Randomize initial states with probability p of being 1.
fun grow(): GrowDelta One step. Alternates between: - Phase 0: compute next states + dividing flags from current graph - Phase 1: apply division rewrites for nodes with dividing=1

GraphQuery.kt

A reflective, XPath-style query layer over arbitrary live object graphs.

The model is small: graphPaths walks any object and returns a PathSet of GraphPathRefs. A ref is a live lens — a slash-joined address plus a getter and, when the underlying slot is mutable, a setter. Nothing is snapshotted, so reading a ref twice can legitimately give two answers.

val cutoffs = patch.graphPaths("**\/cutoff")     // every cutoff at any depth
patch.graphPaths("osc/" + "*").writable()        // one level down, settable only
patch.graphPaths("**" + "/" + "*Alpha")         // a glob inside one segment
patch.graphPaths("gain | **" + "/cutoff")        // either pattern, each slot once
patch.graphPaths("**").where { (it.get() as? Float ?: 0f) > 0.5f }

This package is domain-agnostic: it knows nothing about audio, and in particular it does not touch the runloop. The synth-specific parameter head (ParamSet, modulation, supplier binding) lives in afield.audio.synth and is layered on top via PathSet.params().

Traversal rules

Edges come from LiveGraphInspectable.liveGraphChildren when an object opts in, merged ahead of reflected edges (maps by key, lists/arrays by index, everything else by Kotlin member property in primary-constructor order). Descent stops at scalars, strings, enums, primitive arrays, functions, class objects, and anything in the JDK's own packages — see GraphWalkOptions.descendInto to override that policy.

Cycles are broken by path-scoped detection: a node is skipped only when it already appears on the current ancestor chain. A node shared by several edges is therefore expanded under each of them (so an alias is fully queryable), while a -> b -> a still terminates. GraphWalkOptions.maxRefs bounds the pathological dense-DAG case.

src/jvmMain/kotlin/afield/data/query/graph/GraphQuery.kt — read the source

Used in: Querying object graphs, Visualizers, Displays, and Little Instruments

Top-level functions

Declaration Summary
fun Any?.graphPaths(pattern: String = "**", options: GraphWalkOptions = GraphWalkOptions()): PathSet Walk this object graph and return every slot whose address matches pattern.
fun Any?.graphPath(path: String, options: GraphWalkOptions = GraphWalkOptions()): GraphPathRef? The single slot at a literal path — matched ignoring case — or null.
inline fun <reified T : Any> Any?.graphLenses(pattern: String = "**", options: GraphWalkOptions = GraphWalkOptions()): LensSet<T> Typed lenses over every matching slot holding a T.

LiveGraphInspectable

interface LiveGraphInspectable

Optional protocol for objects that want to expose meaningful live-graph children instead of whatever general reflection would discover. Custom edges win on name collisions.

Declaration Summary
fun liveGraphChildren(): Iterable<LiveGraphEdge>

LiveGraphEdge

data class LiveGraphEdge(val name: String, val getter: () -> Any?, val setter: ((Any?) -> Unit)? = null)

One named, readable — and possibly writable — edge out of a node.

Declaration Summary
val name: String
val getter: () -> Any?
val setter: ((Any?) -> Unit)?

GraphWalkOptions

data class GraphWalkOptions(val includePrivate: Boolean = false, val includeNulls: Boolean = true, val maxDepth: Int = 32, val maxRefs: Int = 200_000, val descendInto: ((value: Any, byDefault: Boolean) -> Boolean)? = null)

Traversal policy.

Declaration Summary
val includePrivate: Boolean Reflect over private properties too, forcing accessibility on each one.
val includeNulls: Boolean With this off, a slot whose current value is null is walked through but left out of the result.
val maxDepth: Int How many edges deep to expand.
val maxRefs: Int Hard bound on collected refs; the walk stops cleanly once it is reached, rather than failing.
val descendInto: ((value: Any, byDefault: Boolean) -> Boolean)? Refines the "should I open this value up?" policy. It receives the value and the default decision, so { value, byDefault -> byDefault && value !is Osc } tightens the default rather than replacing it. Taking the default as an argument is how a one-line prune avoids reflecting into String and reporting name/length.

GraphPathRef

class GraphPathRef internal constructor(internal val pathSegments: List<String>, val owner: Any?, private val getter: () -> Any?, private val setter: ((Any?) -> Unit)?)

A live, addressable slot in an object graph.

Declaration Summary
val owner: Any?
val rawPath: String
val path: String
val name: String The last segment of the address — the slot's own name.
val depth: Int How many edges from the root this slot sits at.
val writable: Boolean
fun get(): Any?
fun <T : Any> getAs(type: KClass<T>): T? Typed read: the value when it is a T, else null.
inline fun <reified T : Any> getAs(): T?
fun set(value: Any?)
fun trySet(value: Any?): Boolean Write only if the slot is writable; reports whether it happened.
override fun toString(): String

PathSet

class PathSet internal constructor(val root: Any?, internal val refs: List<GraphPathRef>) : Iterable<GraphPathRef>, ToHTML

The result of a query: an ordered, immutable collection of live refs.

Declaration Summary
val root: Any?
val size: Int
val isEmpty: Boolean
val isNotEmpty: Boolean
fun paths(): List<String>
fun values(): List<Any?>
fun writable(): PathSet
fun dedup(): PathSet
fun deduplicated(): PathSet
fun matching(pattern: String): PathSet Glob-filter an existing set with the same * / ** / \| rules as graphPaths.
fun excluding(pattern: String): PathSet Glob-filter out the matches — the complement of matching.
fun where(predicate: (GraphPathRef) -> Boolean): PathSet Keep refs satisfying an arbitrary predicate over the whole ref.
fun whereValue(predicate: (Any?) -> Boolean): PathSet Keep refs whose current value satisfies a predicate.
fun <T : Any> whereValueIs(type: KClass<T>, predicate: (T) -> Boolean): PathSet Keep refs whose value is a T and satisfies a typed predicate — the common case, without the cast at every call site.
inline fun <reified T : Any> whereValueIs(noinline predicate: (T) -> Boolean): PathSet
fun ownedBy(owner: Any?): PathSet Keep refs owned by a specific object, compared by identity.
fun nonNull(): PathSet Drop refs whose current value is null.
fun atDepth(range: IntRange): PathSet Restrict to a depth range, e.g. atDepth(1..2).
fun ofType(type: KClass<*>): PathSet
inline fun <reified T : Any> ofType(): PathSet
fun <T : Any> lenses(type: KClass<T>): LensSet<T> Narrow to a typed LensSet over the refs whose value is a T. This is the general terminal: the float-only parameter head is one specialization of the same idea.
inline fun <reified T : Any> lenses(): LensSet<T>
fun firstOrNull(): GraphPathRef?
fun first(): GraphPathRef Returns the first path reference in this set.
operator fun get(path: String): GraphPathRef? Look up a single literal path — case-insensitive, like the patterns — or null.
override fun iterator(): Iterator<GraphPathRef>
override fun toString(): String
override fun toHTML(): String

Lens

class Lens<T : Any> internal constructor(val ref: GraphPathRef, val type: KClass<T>)

A typed handle on one slot. Unlike a bare GraphPathRef the type is fixed up front, so get() needs no cast and set() cannot install the wrong kind of value.

Declaration Summary
val ref: GraphPathRef
val type: KClass<T>
val path: String
val name: String
val owner: Any?
val writable: Boolean
fun get(): T? The value when it is still a T; null if the slot has since changed shape.
fun getOrElse(fallback: T): T
fun set(value: T)
fun trySet(value: T): Boolean
fun update(transform: (T) -> T): Boolean Read-modify-write through the lens; no-op when the slot no longer holds a T.
override fun toString(): String

LensSet

class LensSet<T : Any> internal constructor(val type: KClass<T>, val lenses: List<Lens<T>>) : Iterable<Lens<T>>, ToHTML

A set of typed lenses — the general counterpart to the synth ParamSet, for graphs whose interesting leaves are strings, booleans, enums, colours, vectors, anything at all.

Declaration Summary
val type: KClass<T>
val lenses: List<Lens<T>>
val size: Int
val isEmpty: Boolean
fun paths(): List<String>
fun values(): List<T?>
fun presentValues(): List<T> Present values only, in order.
fun writable(): LensSet<T>
fun where(predicate: (Lens<T>) -> Boolean): LensSet<T>
fun set(value: T): Int Assign one value to every writable lens; returns the number written.
fun set(values: Iterable<T>): Int Assign positionally, stopping when either side runs out.
fun update(transform: (T) -> T): Int Read-modify-write every lens; returns the number actually written.
fun updateIndexed(transform: (Int, T) -> T): Int Read-modify-write with the index available.
fun firstOrNull(): Lens<T>?
override fun iterator(): Iterator<Lens<T>>
override fun toString(): String
override fun toHTML(): String

GraphPattern

class GraphPattern internal constructor(val pattern: String, internal val branches: List<List<String>>)

A parsed query pattern.

Declaration Summary
val isStatic: Boolean True when the whole pattern is literal, so exactly one slot can match.
val alternatives: Int How many alternatives this pattern was written with.
fun matches(path: String): Boolean
fun resolve(root: Any?, options: GraphWalkOptions = GraphWalkOptions()): PathSet Run this pattern against a root.
override fun toString(): String

GraphQuery

object GraphQuery

Entry point for compiled queries.

Declaration Summary
fun compile(pattern: String): GraphPattern Parse (and cache) a pattern for repeated use.

GraphQuerySmokeTest.kt

Smoke test for the reflective path layer in afield.data.query.graph, the XPath-style query engine over arbitrary live object graphs.

It exercises only the generic core, which is what lets it run as a plain jvm-main entrypoint: the core touches no Runloop, spawns no threads and opens no devices, so the JVM exits naturally. The synth-coupled parameter head (modulation, suppliers, GraphBindingRuntime) does register a runloop hook and is covered by the jvmTest suites instead.

The central case is testFastPathAgreesWithFullWalk: the literal-prefix optimization must return exactly what a full traversal plus glob filter returns.

src/jvmMain/kotlin/afield/data/query/graph/GraphQuerySmokeTest.kt — read the source

GraphQuerySmokeTest

object GraphQuerySmokeTest

The graphquery.core.smoke entrypoint; prints "GraphQuery smoke passed" on success.

Declaration Summary
fun main(args: Array<String>)
fun runTests(): Boolean

PathMacro.kt

A compile-checked path expression — the string-free counterpart to graphPaths.

graphPaths("osc/filter/cutoff") is convenient but unverified: a typo, or a rename three months later, produces an empty result at runtime and nothing tells you why. That is the usual price of a reflective query layer.

graphLens avoids paying it. It is a source macro: before the box is compiled, the call

val cutoff = graphLens(rig, "carrier/freq")

is rewritten into an ordinary bound property reference

afield.data.query.graph.boundLens(rig.carrier::freq, "carrier/freq")

and then handed to kotlinc like any other code. Three things fall out of that:

  • A bad path is a compile error. graphLens(rig, "carrier/frequency") fails with Kotlin's own "unresolved reference: frequency", positioned in your editor, because by the time the compiler sees it there is no path — only a property access.
  • The element type is inferred, so cutoff.get() is a Float, not an Any? you have to cast. No reified type argument, no as.
  • Writability is checked too, via overload resolution: a var resolves to the KMutableProperty0 overload and yields a writable lens, a val resolves to the KProperty0 one and yields a read-only lens whose set does not exist to be called.

There is no reflection over the path at runtime and no traversal at all — this is the fastest of the three resolution strategies, below even the literal-prefix fast path.

The trade is deliberate: the guarantee only holds for paths made of Kotlin identifiers. Wildcards, map keys and list indices cannot become property accesses, so rather than silently degrading to an unchecked runtime lookup, the macro refuses to expand and points at graphPaths / graphPath. If graphLens compiles, it was checked.

src/jvmMain/kotlin/afield/data/query/graph/PathMacro.kt — read the source

Top-level functions

Declaration Summary
fun <T> boundLens(property: KMutableProperty0<T>, path: String): StaticLens<T> Writable overload — selected by overload resolution when the referenced property is a var. Being more specific than the KProperty0 overload, it always wins for a var.
fun <T> boundLens(property: KProperty0<T>, path: String): StaticLens<T> Read-only overload — the only applicable one when the property is a val.
fun registerGraphQueryMacros()

StaticLens

class StaticLens<T> internal constructor(val path: String, private val getter: () -> T, private val setter: ((T) -> Unit)?)

One statically-resolved slot. Carries no reflection: the accessors are the property reference the compiler already resolved.

Declaration Summary
val path: String The original path text, kept for diagnostics and display.
val writable: Boolean
fun get(): T
fun set(value: T)
fun update(transform: (T) -> T)
override fun toString(): String

PathOperator.kt

The / operator queries an object graph by path: rig / "**\/freq" is shorthand for graphPaths.

(rig / "**\/freq").whereValueIs<Float> { it > 300f }

Path syntax already uses / as its separator, so the operator and the string it takes are spelled the same way: rig / "carrier" / "freq" and rig / "carrier/freq" are the same query, and both report carrier/freq.

What defining this on Any? costs

It is a wide extension, in the same spirit as patch matches "**\/cutoff", and it is auto-imported into every box. Three consequences:

  • It claims only the case no other div wants. A member operator always beats an extension, so every Float / Float, Vec3 / Float, Signal / Signal keeps its own div. Between two extensions the more specific receiver wins, so kotlin.io.path's Path / "sub" still builds a Path. That leaves this overload a String on the right of /, and nothing else.
  • A leaf on the left is a compile error. "a" / "b", a number or a boolean over a string resolve to deprecated-at-ERROR overloads (String.div and its siblings), so the typo is caught at the / with a message pointing at graphPaths.
  • div appears on every receiver in completion. One extra entry, everywhere.

When you need GraphWalkOptions (a depth cap, private properties, a custom descendInto), call graphPaths directly; the operator has nowhere to put them.

src/jvmMain/kotlin/afield/data/query/graph/PathOperator.kt — read the source

Top-level functions

Declaration Summary
operator fun Any?.div(pattern: String): PathSet Queries this object's graph with a path pattern; the same as graphPaths(pattern).
operator fun String.div(pattern: String): PathSet Refused at compile time: a string has no slots, so "a" / "b" is a typo rather than a query.
operator fun Number.div(pattern: String): PathSet Refused at compile time, like String.div: a number has no slots.
operator fun Boolean.div(pattern: String): PathSet Refused at compile time, like String.div: a boolean has no slots.
operator fun PathSet.div(pattern: String): PathSet Continue a query from a set: match pattern against the graph below each ref, with the results re-addressed under the ref they came from.
operator fun GraphPathRef.div(pattern: String): PathSet Continue a query from one slot — (rig / "carrier").first() / "freq".
operator fun <T : Any> LensSet<T>.div(pattern: String): PathSet Continue a query from a typed set. Typed leaves are usually the end of the road, so this normally returns nothing; it is here because falling through to the Any? overload would instead report the LensSet's own internals.
operator fun <T : Any> Lens<T>.div(pattern: String): PathSet Continue a query from one typed slot. See LensSet.div.

sorting_networks.kt

Sorting networks as data: a SortingNetwork is a wire count plus an ordered list of SortingComparator compare-and-swaps. Build one with SortingNetwork.bubble, SortingNetwork.oddEvenMerge (Batcher's iterative odd-even merge) or SortingNetwork.anneal (simulated-annealing search for a shorter network), then check it with SortingNetwork.isSorting, which enumerates all 2^n binary inputs by the 0-1 principle up to n = 20 and falls back to 4096 random probes above that.

SortingNetwork.layers assigns comparators to their earliest parallel layer (ASAP scheduling) and drives SortingNetwork.depth, SortingNetwork.notation — a "0:1 2:3 | 0:2 1:3" string that round-trips through SortingNetwork.fromNotation — SortingNetwork.asciiDiagram and SortingNetwork.toFLines for Box.lines(...). Pure Kotlin apart from FLine; the annealing search is CPU-bound (each validity check is O(2^n × |comparators|)) and reports through afield.ui.output.progressReporter.

src/jvmMain/kotlin/afield/data/sorting/sorting_networks.kt — read the source

Used in: Sorting networks to FLine examples

SortingComparator

data class SortingComparator(val lo: Int, val hi: Int)

A single compare-and-swap unit on wires lo and hi.

Declaration Summary
override fun toString()

SortingNetwork

class SortingNetwork(val n: Int, val comparators: List<SortingComparator>)

A sorting network on n wires.

Declaration Summary
fun sort(input: IntArray): IntArray Apply this network to input and return the sorted result.
fun layerTrace(input: IntArray): List<IntArray> Apply this network layer-by-layer and return a snapshot after each layer.
fun isSorting(): Boolean Check whether this network correctly sorts all possible inputs (0-1 principle).
fun layers(): List<List<SortingComparator>> Assign each comparator to its earliest valid parallel layer.
fun depth(): Int Minimum parallel depth (number of non-overlapping layers).
fun notation(): String Encode this network as a compact, human-readable string.
fun asciiDiagram(): String Render the network as a human-readable ASCII-art diagram.
fun toFLines(wireSpacing: Double = 40.0, layerSpacing: Double = 60.0, margin: Double = 40.0, wireColor: Vec4 = vec(0.55, 0.55, 0.55, 0.75), dotRadius: Double = 2.0): List<FLine> Render this sorting network as a list of FLines suitable for Box.lines(...).
override fun toString(): String
override fun equals(other: Any?): Boolean
override fun hashCode(): Int

spreadsheet.kt

A spreadsheet that lives in a box property, shown in the box's output area, whose formulas are Kotlin expressions.

Box.spreadsheet (alias Box.sheet) stores the sheet as a SpreadsheetStoredState in a box property (spreadsheet by default) and displays an editor for it in an output channel. A cell whose input starts with = is a formula, compiled with the same K2 engine as box code; inside it, SpreadsheetEvalHelpers supplies cell("A1"), range("A1:A3"), box("name") and aggregates such as sum and avg. The sheet records which members of another box a formula reads through box(...), and open sheets are polled once per frame so they follow those values.

From box code, build or edit the stored state with spreadsheetState / Box.editSpreadsheet, and read evaluated values with Box.getSpreadsheetSnapshot and Box.getSpreadsheetCellValue.

The guide, with captured sheets and the editing keys, is Spreadsheets in a Box.

src/jvmMain/kotlin/afield/data/table/spreadsheet.kt — read the source

Used in: Spreadsheets

Top-level functions

Declaration Summary
fun spreadsheetState(init: SpreadsheetStateBuilder.() -> Unit): SpreadsheetStoredState
fun sheetState(init: SpreadsheetStateBuilder.() -> Unit): SpreadsheetStoredState
fun SpreadsheetStoredState.cell(ref: String): SpreadsheetStoredCell?
operator fun SpreadsheetStoredState.get(ref: String): SpreadsheetStoredCell?
fun SpreadsheetSnapshot.cell(ref: String): SpreadsheetCellSnapshot?
operator fun SpreadsheetSnapshot.get(ref: String): SpreadsheetCellSnapshot?
operator fun Number.plus(other: SpreadsheetScalar): SpreadsheetScalar
operator fun Number.minus(other: SpreadsheetScalar): SpreadsheetScalar
operator fun Number.times(other: SpreadsheetScalar): SpreadsheetScalar
operator fun Number.div(other: SpreadsheetScalar): SpreadsheetScalar
operator fun Number.rem(other: SpreadsheetScalar): SpreadsheetScalar
operator fun Vec2.plus(other: SpreadsheetScalar): SpreadsheetScalar
operator fun Vec2.minus(other: SpreadsheetScalar): SpreadsheetScalar
operator fun Vec2.times(other: SpreadsheetScalar): SpreadsheetScalar
operator fun Vec2.div(other: SpreadsheetScalar): SpreadsheetScalar
operator fun Vec2.rem(other: SpreadsheetScalar): SpreadsheetScalar
operator fun Vec3.plus(other: SpreadsheetScalar): SpreadsheetScalar
operator fun Vec3.minus(other: SpreadsheetScalar): SpreadsheetScalar
operator fun Vec3.times(other: SpreadsheetScalar): SpreadsheetScalar
operator fun Vec3.div(other: SpreadsheetScalar): SpreadsheetScalar
operator fun Vec3.rem(other: SpreadsheetScalar): SpreadsheetScalar
operator fun Vec4.plus(other: SpreadsheetScalar): SpreadsheetScalar
operator fun Vec4.minus(other: SpreadsheetScalar): SpreadsheetScalar
operator fun Vec4.times(other: SpreadsheetScalar): SpreadsheetScalar
operator fun Vec4.div(other: SpreadsheetScalar): SpreadsheetScalar
operator fun Vec4.rem(other: SpreadsheetScalar): SpreadsheetScalar
operator fun FLine.plus(other: SpreadsheetScalar): SpreadsheetScalar
operator fun FLine.minus(other: SpreadsheetScalar): SpreadsheetScalar
operator fun FLine.times(other: SpreadsheetScalar): SpreadsheetScalar
operator fun FLine.div(other: SpreadsheetScalar): SpreadsheetScalar
operator fun FLine.rem(other: SpreadsheetScalar): SpreadsheetScalar
fun spreadsheetValue(value: Any?): SpreadsheetScalar
fun sheetValue(value: Any?): SpreadsheetScalar
fun Box.spreadsheet(channel: String = "spreadsheet", property: String = "spreadsheet", height: Int = 360, defaultState: SpreadsheetStoredState? = null, width: Int? = null) Show the sheet stored in property in output channel, first filling an empty sheet from defaultState. width defaults to the sheet's own width — its columns plus the row header, up to 960 px, beyond which the grid scrolls — so a four-column sheet is not squeezed into a narrow output or stretched across a wide one.
fun Box.spreadsheet(channel: String = "spreadsheet", property: String = "spreadsheet", height: Int = 360, width: Int? = null, defaultState: SpreadsheetStateBuilder.() -> Unit)
fun Box.sheet(channel: String = "spreadsheet", property: String = "spreadsheet", height: Int = 360, defaultState: SpreadsheetStoredState? = null, width: Int? = null)
fun Box.sheet(channel: String = "spreadsheet", property: String = "spreadsheet", height: Int = 360, width: Int? = null, defaultState: SpreadsheetStateBuilder.() -> Unit)
fun Box.getSpreadsheetState(property: String = "spreadsheet"): SpreadsheetStoredState
fun Box.sheetState(property: String = "spreadsheet"): SpreadsheetStoredState
fun Box.getSpreadsheetSnapshot(property: String = "spreadsheet", forceReload: Boolean = false, forceRecompute: Boolean = true): SpreadsheetSnapshot
fun Box.sheetSnapshot(property: String = "spreadsheet", forceReload: Boolean = false, forceRecompute: Boolean = true): SpreadsheetSnapshot
fun Box.getSpreadsheetCellValue(ref: String, property: String = "spreadsheet", forceReload: Boolean = false, forceRecompute: Boolean = true): Any?
fun Box.getSpreadsheetCell(ref: String, property: String = "spreadsheet", forceReload: Boolean = false, forceRecompute: Boolean = true): SpreadsheetScalar
fun Box.sheetCellValue(ref: String, property: String = "spreadsheet", forceReload: Boolean = false, forceRecompute: Boolean = true): Any?
fun Box.sheetCell(ref: String, property: String = "spreadsheet", forceReload: Boolean = false, forceRecompute: Boolean = true): SpreadsheetScalar
fun Box.setSpreadsheetState(state: SpreadsheetStoredState, property: String = "spreadsheet", broadcast: Boolean = true): SpreadsheetStoredState
fun Box.setSpreadsheetState(property: String = "spreadsheet", broadcast: Boolean = true, init: SpreadsheetStateBuilder.() -> Unit): SpreadsheetStoredState
fun Box.seedSpreadsheet(property: String = "spreadsheet", state: SpreadsheetStoredState, broadcast: Boolean = true): SpreadsheetStoredState
fun Box.seedSpreadsheet(property: String = "spreadsheet", broadcast: Boolean = true, init: SpreadsheetStateBuilder.() -> Unit): SpreadsheetStoredState
fun Box.seedSheet(property: String = "spreadsheet", state: SpreadsheetStoredState, broadcast: Boolean = true): SpreadsheetStoredState
fun Box.editSpreadsheet(property: String = "spreadsheet", broadcast: Boolean = true, edit: SpreadsheetStateBuilder.() -> Unit): SpreadsheetStoredState
fun Box.editSheet(property: String = "spreadsheet", broadcast: Boolean = true, edit: SpreadsheetStateBuilder.() -> Unit): SpreadsheetStoredState
fun installSpreadsheetSupport()

Top-level values

Declaration Summary
val SpreadsheetCellSnapshot.ref: String

SpreadsheetStoredCell

data class SpreadsheetStoredCell(val row: Int, val col: Int, val input: String)

Declaration Summary
val row: Int
val col: Int
val input: String

SpreadsheetStoredState

data class SpreadsheetStoredState(val rows: Int = DEFAULT_SHEET_ROWS, val cols: Int = DEFAULT_SHEET_COLS, val colWidths: List<Double> = emptyList(), val rowHeights: List<Double> = emptyList(), val cells: List<SpreadsheetStoredCell> = emptyList(), val solved: Map<String, String> = emptyMap())

Declaration Summary
val rows: Int
val cols: Int
val colWidths: List<Double>
val rowHeights: List<Double>
val cells: List<SpreadsheetStoredCell>
val solved: Map<String, String> The =free(...) cells' inputs as the last solve left them, by row:col. A variable whose input differs from this was edited since, and the next solve holds it at edit strength. Stored, so an edit made while the sheet was closed, or by editSheet { } before it opened, still counts.

SpreadsheetStateBuilder

class SpreadsheetStateBuilder internal constructor(state: SpreadsheetStoredState? = null)

Declaration Summary
var rows: Int
var cols: Int
fun cell(ref: String, input: Any?)
fun formula(ref: String, expression: String)
fun clear(ref: String)
fun clearAll()
fun columnWidth(label: String, width: Int)
fun columnWidthAt(col: Int, width: Int)
fun rowHeight(rowNumber: Int, height: Int)
fun rowHeightAt(row: Int, height: Int)
operator fun set(ref: String, input: Any?)
operator fun get(ref: String): String?

SpreadsheetCellSnapshot

data class SpreadsheetCellSnapshot(val row: Int, val col: Int, val input: String, val type: String, val display: String, val errorDetail: String? = null, val htmlDisplay: String? = null)

Declaration Summary
val row: Int
val col: Int
val input: String
val type: String
val display: String
val errorDetail: String?
val htmlDisplay: String?

SpreadsheetSnapshot

data class SpreadsheetSnapshot(val uid: String, val property: String, val rows: Int, val cols: Int, val colWidths: List<Int>, val rowHeights: List<Int>, val cells: List<SpreadsheetCellSnapshot>, val version: Long, val solve: String? = null)

Declaration Summary
val uid: String
val property: String
val rows: Int
val cols: Int
val colWidths: List<Int>
val rowHeights: List<Int>
val cells: List<SpreadsheetCellSnapshot>
val version: Long
val solve: String? When the sheet has assertions over variable cells: what the last solve did, in one line.

SpreadsheetCompletionItem

data class SpreadsheetCompletionItem(val label: String, val insertText: String, val kind: String = "item", val detail: String = "", val documentation: String = "", val replaceStart: Int, val replaceEnd: Int, val cursorOffset: Int? = null, val selectInsertedText: Boolean = false)

Declaration Summary
val label: String
val insertText: String
val kind: String
val detail: String
val documentation: String
val replaceStart: Int
val replaceEnd: Int
val cursorOffset: Int?
val selectInsertedText: Boolean

SpreadsheetEvalHelpers

object SpreadsheetEvalHelpers

Declaration Summary
fun cell(ref: String): SpreadsheetScalar
fun ref(index: Int): String The indexth reference written in the formula; see rewriteSpreadsheetFormula.
fun range(ref: String): List<SpreadsheetScalar>
fun box(ref: String): SpreadsheetBoxBinding
fun free(initial: Any?): SpreadsheetScalar =free(3): this cell is a variable the sheet may solve for, currently 3. The number inside is its previous value, which the sheet prefers to keep; a solve writes the new value back into the formula.
fun assertThat(relation: SpreadsheetRelation, left: Any?, right: Any?): SpreadsheetAssertion An assertion between two values; see SpreadsheetScalar.eq.
fun total(values: Any?): SpreadsheetScalar sum, keeping the trace when any term comes from a variable cell.
fun sqrt(x: Any?): SpreadsheetScalar
fun abs(x: Any?): SpreadsheetScalar
fun exp(x: Any?): SpreadsheetScalar
fun ln(x: Any?): SpreadsheetScalar
fun sin(x: Any?): SpreadsheetScalar
fun cos(x: Any?): SpreadsheetScalar
fun tan(x: Any?): SpreadsheetScalar
fun atan2(y: Any?, x: Any?): SpreadsheetScalar
fun pow(x: Any?, p: Any?): SpreadsheetScalar
val self: SpreadsheetBoxBinding
fun sum(values: Any?): Double
fun avg(values: Any?): Double
fun mean(values: Any?): Double
fun min(values: Any?): Double
fun max(values: Any?): Double
fun count(values: Any?): Int
fun nonEmpty(values: Any?): List<Any?>
class Failure internal constructor(val error: Throwable) What a formula that threw evaluates to: the formula body runs inside a try, so the error that reaches the cell is the one the formula raised, not however the REPL reports a throw.
fun failed(error: Throwable): Any
fun product(values: Any?): Double
fun num(value: Any?): Double A cell, a number, a vector (its length) or a line (its diagonal) as a Double, for sqrt(A1).

SpreadsheetScalar

class SpreadsheetScalar internal constructor(val raw: Any?) : Comparable<Any?>

Declaration Summary
val d: Double
val v2: Vec2
val v3: Vec3
val v4: Vec4
val x: SpreadsheetScalar
val y: SpreadsheetScalar
val z: SpreadsheetScalar
val w: SpreadsheetScalar
val width: SpreadsheetScalar The width of a line's bounds. x, y, z, w of a line are its bounds as the components (x, y, width, height) — so .w is the height — which is why the two have names of their own.
val height: SpreadsheetScalar The height of a line's bounds; see width.
operator fun get(index: Int): SpreadsheetScalar
operator fun plus(other: Any?): SpreadsheetScalar
operator fun minus(other: Any?): SpreadsheetScalar
operator fun times(other: Any?): SpreadsheetScalar
operator fun div(other: Any?): SpreadsheetScalar
operator fun rem(other: Any?): SpreadsheetScalar
operator fun unaryMinus(): SpreadsheetScalar
operator fun unaryPlus(): SpreadsheetScalar
infix fun eq(other: Any?): SpreadsheetAssertion Assert this == other. In a cell: =A3 eq 10. Over a variable cell, the sheet solves for it.
infix fun le(other: Any?): SpreadsheetAssertion Assert this <= other; see eq.
infix fun ge(other: Any?): SpreadsheetAssertion Assert this >= other; see eq.
fun toDouble(): Double
fun toInt(): Int
fun toLong(): Long
override fun compareTo(other: Any?): Int
override fun equals(other: Any?): Boolean
override fun hashCode(): Int
override fun toString(): String

SpreadsheetBoxBinding

class SpreadsheetBoxBinding internal constructor(private val touchMember: (String, SpreadsheetBoxMemberAccess) -> Unit, private val memberNamesProvider: () -> List<String>, private val target: Box)

Declaration Summary
val id: String
val name: String
operator fun get(member: String): SpreadsheetScalar
fun prop(member: String): SpreadsheetScalar
fun variable(member: String): SpreadsheetScalar
fun keys(): List<String>
override fun toString(): String

spreadsheetConstraints.kt

Constraint-solving spreadsheets: cells that hold Ceres variables, formulas that trace back to them, and assertion cells the sheet solves for.

A cell whose formula is free(4) is a variable the sheet may move, starting at 4. Arithmetic over one yields a SpreadsheetTraced, a number that also carries the expression that produced it, so a cell written B3 eq 40 (or le, or ge) becomes a SpreadsheetAssertion the solver can act on: Ceres moves the variables until every assertion holds, each variable preferring the value it had. The three types here are what such a cell holds; the collector and the solver around them are internal to this file.

src/jvmMain/kotlin/afield/data/table/spreadsheetConstraints.kt — read the source

SpreadsheetTraced

class SpreadsheetTraced internal constructor(val value: Double, internal val expr: CeresExpr)

A number a formula computed from one or more variable cells (=free(3)), carrying the expression it was computed by. It displays and coerces as its value; arithmetic on it keeps the trace, so an assertion over it can move the variables it came from.

Declaration Summary
override fun toString(): String The value alone, so a traced cell reads as the number it currently is.

SpreadsheetRelation

enum class SpreadsheetRelation(val symbol: String)

The three relations an assertion cell can state.

Constants: EQ, LE, GE.

SpreadsheetAssertion

class SpreadsheetAssertion internal constructor(val relation: SpreadsheetRelation, val left: Double, val right: Double, internal val leftExpr: CeresExpr?, internal val rightExpr: CeresExpr?)

An assertion cell's value: left relation right at the sheet's current values. The sheet solves so that every assertion holds; one that still does not after the solve is shown violated, with how far.

Declaration Summary
val relation: SpreadsheetRelation Which of the three comparisons the cell states.
val left: Double The left-hand side at the sheet's current numbers.
val right: Double The right-hand side at the sheet's current numbers.
val violation: Double How far the assertion is from holding; 0 when it holds.
val satisfied: Boolean Whether violation is inside a tolerance that scales with the two sides, 1e-6 × (1 + \|left\| + \|right\|), so a comparison between large numbers is not held to an absolute one.
val traced: Boolean True when either side depends on a variable cell, so the solver can do something about it.
override fun toString(): String A tick or a cross, then the comparison as the sheet currently reads it.

saved.kt

Per-box persistence for Kotlin values, on top of kotlinx.serialization. var n by saved { 0 } declares a property that loads itself on first read and writes itself back on every assignment, as JSON at <document directory>/<box id>/<property name>.json, encoded with Field's FieldJson and written through the atomic safeSave. The no-argument saved overload skips the default lambda but only for List, Set and Map (see defaultSavedValue); anything else demands an explicit default.

The delegate itself is SavedJsonValue; it takes the box from its receiver or from the ambient Box.context, so it needs an initialised box and session and errors out otherwise. toJSONFile and fromJSONFile do the same encode/decode against an explicit File with no delegate. Any failure to read, write or serialize is rethrown as a SerializationException naming the file.

src/jvmMain/kotlin/afield/document/properties/saved.kt — read the source

Top-level functions

Declaration Summary
inline fun <reified T> toJSONFile(file: File, value: T) Writes any serializable value to a JSON file, atomically, creating parent directories as needed.
inline fun <reified T> fromJSONFile(file: File, defaultValue: () -> T): T Reads a value back from a JSON file, or returns defaultValue if the file does not exist.
inline fun <reified T> saved(noinline defaultValue: () -> T): SavedJsonValue<T> var n by saved { 0 } — a box property that persists across runs, restarts and reopening the document.
inline fun <reified T> saved(): SavedJsonValue<T> val log by saved<List<String>>() — saved without a default, for the collection types that have an obvious empty value.

SavedJsonValue

class SavedJsonValue<T>(private val serializer: KSerializer<T>, private val defaultValue: () -> T) : ReadWriteProperty<Any?, T>

The delegate behind var n by saved { 0 }: a property that loads itself from disk on first read and writes itself back on every assignment.

Declaration Summary
override fun getValue(thisRef: Any?, property: KProperty<*>): T The stored value, loading it from disk on the first read of this box's run.
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) Stores the value and writes it to disk immediately — there is no batching and no dirty flag, so an assignment in a tight loop is a file write per iteration.

FileManager.kt

Path resolution for the running Field session: turns a short spec into a real File or Path. The files / fileManager properties (and the String.file, String.path, String.absolutePath extensions) hand you the FieldFileManager for the current box or session, which resolves, in order: absolute and ~/ paths; %document/… and %project/…; any name registered with FileManager.add or FileManager["name"] = dir; globs, by walking the search roots and taking the first sorted match; and otherwise a plain relative name looked up in the document's files/ directory, then in the files/ directory of every workspace project the document belongs to, and then in every directory put on the search list with FileManager.add(name, dir, search = true), in the order they were added; finally, an exact filename registered with FileManager.addFile.

Resolutions are cached per session and dropped when a directory or file registration changes, and a child path that would escape its root is rejected. Every lookup is appended to the session root's FILE_MANAGER_ACCESS_HISTORY_PROPERTY property, trimmed to FileManager.maxAccessHistoryEntries — so these calls need an active session or Box context and will error without one.

src/jvmMain/kotlin/afield/document/workspace/FileManager.kt — read the source

Used in: File management

Top-level values

Declaration Summary
const val FILE_MANAGER_ACCESS_HISTORY_PROPERTY The property on the session root that accumulates every file lookup this session has made — request, resolved path, which root answered it, whether it existed, and which box asked.
val files: FieldFileManager The current session's FieldFileManager — the way into all of this from a box: files.file("data.csv").
val fileManager: FieldFileManager Alias for files, for code that reads better spelled out.
val String.file: File "data.csv".file — the terse form of files.file(this), and the idiom worth learning first.
val String.path: Path "data.csv".path — the same resolution as String.file, as an NIO Path.
val String.absolutePath: String "data.csv".absolutePath — the same resolution, as an absolute normalised string.
val String.abslutePath: String A misspelling of absolutePath that works anyway.

FieldFileManager

class FieldFileManager internal constructor(private val session: SessionInformation)

One session's view of the filesystem: hand it a short spec and it gives you a real File or Path.

Declaration Summary
val documentDirectory: Path The directory this document lives in — a directory, not a single file, since a Field document is a folder.
val documentFilesDirectory: Path This document's own files/ directory, the first place a relative name is looked for.
val projectFilesDirectories: List<Path> The files/ directory of every workspace project this document belongs to — where a name not found in the document itself is looked for next. Empty when the document is in no project.
fun file(spec: String): File Resolves a spec to a File. The everyday call, usually written as "data.csv".file.
fun path(spec: String): Path The same resolution as file, as an NIO Path. Prefer it when you are going on to use java.nio.file.
fun absolutePath(spec: String): String The same resolution as file, as an absolute normalised String — for handing a path to a subprocess.
fun clearCache() Forgets this session's cached resolutions. Rarely needed: the cache already drops a resolution whose file has gone, and is cleared wholesale whenever a directory or file registration changes.

FileManager

object FileManager

File-related utilities for the active AField session.

Declaration Summary
fun addFile(name: String, file: Path): Path Registers an exact bare filename as a process-global fallback after document, project and search directories. Returns the absolute normalized target. Re-registering replaces it and invalidates session caches. The target may not exist yet (for an optional helper built later); only regular files answer lookups. Names may include extensions but cannot be paths, globs, or explicit root selectors. This does not build, copy or execute the file. Globs do not search these registrations.
fun addFile(name: String, file: File): Path addFile with a File target.
fun addFile(name: String, file: String): Path addFile with a string target; expands a leading ~/.
fun removeFile(name: String): Boolean Removes an exact-file fallback and invalidates caches; leaves the target on disk.
var maxAccessHistoryEntries: Int How many lookups FILE_MANAGER_ACCESS_HISTORY_PROPERTY keeps before dropping the oldest. Applied to the whole list on the next lookup, so lowering it does trim the history already recorded.
fun current(): FieldFileManager The file manager for whatever session is in scope right now — the current box's, or the ambient session's.
fun forSession(session: SessionInformation): FieldFileManager The manager for a named session rather than the ambient one — for code holding a session explicitly.
operator fun set(name: String, directory: String) FileManager["textures"] = "~/Pictures" — registers a named directory, so %textures/wall.png resolves.
operator fun set(name: String, directory: File) FileManager["name"] = someFile — as the String overload, taking a File.
operator fun set(name: String, directory: Path) FileManager["name"] = somePath — as the String overload, taking a Path.
operator fun get(name: String): Path? The directory registered under this name, or null if nothing is registered under it. The % prefix is optional — get("x") and get("%x") agree.
fun add(name: String, directory: String, create: Boolean = false, search: Boolean = false): Path add from a string path, with ~ expanded first.
fun add(name: String, directory: File, create: Boolean = false, search: Boolean = false): Path add from a File.
fun add(name: String, directory: Path, create: Boolean = false, search: Boolean = false): Path Registers directory under name so %name/… resolves into it, returning the absolute path stored.
fun remove(name: String): Boolean Unregisters a named directory, returning whether there was one. Nothing on disk is touched.
val searchNames: List<String> The names on the search list — registered with search = true — in the order a plain name tries them, after the document's and the projects' files/. A snapshot; change it with add and remove.
fun clearCache() Clears every session's resolution cache. Directory and exact-file registration changes already do this.

Bvh.kt

Motion capture in BVH format: a joint hierarchy and one line of channel values per frame.

Bvh.read parses a file; Bvh.positions runs forward kinematics to give every joint's position (and every end site's: fingertips, toes, the top of the head) at a frame. Bvh.toTrajectory turns a take into a Trajectory of joint positions for analysis or sequence memory, optionally in the performer's own frame (travel and turning removed), and Bvh.skeleton draws a pose as 3D FLines for three(...). Tests: BvhTest.

src/jvmMain/kotlin/afield/io/mocap/Bvh.kt — read the source

BvhJoint

class BvhJoint internal constructor(val name: String, val parent: Int, val offset: DoubleArray, val channels: List<String>, val isEndSite: Boolean, internal val channelStart: Int)

One joint of a Bvh hierarchy, or an end site (no channels, isEndSite).

Declaration Summary
val name: String
val parent: Int Index of the parent in Bvh.joints, or -1 for the root.
val offset: DoubleArray Offset from the parent, in the file's units.
val channels: List<String> Channel names in file order, e.g. Zrotation Yrotation Xrotation.
val isEndSite: Boolean

Bvh

class Bvh internal constructor(val joints: List<BvhJoint>, val frameTime: Double, val frames: Array<DoubleArray>, val name: String)

A parsed BVH take: joints in file order (end sites included, named <parent>_end), one frames row of channel values per frame, frameTime seconds apart.

Declaration Summary
val joints: List<BvhJoint>
val frameTime: Double
val frames: Array<DoubleArray>
val name: String
val frameCount: Int Number of frames.
val sampleRate: Double Frames per second.
val duration: Double Duration in seconds.
fun joint(name: String): Int Index of the joint called name.
val bones: List<Pair<Int, Int>> Parent–child pairs, one per bone.
fun positions(frame: Int, bodyFrame: Boolean = false): Array<DoubleArray> World position of every joint at frame, [joint](data.md#bvh-kt--bvh--joint)[x, y, z], y up as in the file. With bodyFrame the root's horizontal position and heading are removed: positions are relative to the point on the floor under the root, facing +z, so the same movement gives the same numbers wherever the performer is and whichever way they face. Height is kept.
val restPoseFrames: Int by lazy Frames at the start that are a rest pose rather than motion: 1 when frame 0 jumps to frame 1 by far more (over 20×) than the take's typical frame-to-frame motion, as in the CMU BVH conversion, whose first frame is a T-pose; otherwise 0.
fun toTrajectory(rate: Double = 30.0, jointNames: List<String> = joints.map { it.name }, bodyFrame: Boolean = true, from: Double = restPoseFrames * frameTime, to: Double = duration): Trajectory The take as a Trajectory of joint positions resampled to rate Hz: channels named Joint.x, Joint.y, Joint.z for each of jointNames (all joints and end sites by default). bodyFrame as in positions. from and to trim the take, in seconds; from starts after any restPoseFrames.
fun skeleton(frame: DoubleArray, channels: List<String>, at: DoubleArray = DoubleArray(3)): List<FLine> A pose drawn as 3D FLines, one per bone, for three(...). frame is one sample of a toTrajectory trajectory (all joints, in order), or of any trajectory with the same channels; joints missing from channels are skipped. at shifts the figure, e.g. to lay out poses side by side.
fun skeletonMesh(frame: DoubleArray, channels: List<String>, radius: Double = 0.5, color: DoubleArray = doubleArrayOf(0.5, 0.85, 1.0), at: DoubleArray = DoubleArray(3)): TriMesh A pose as one solid mesh: a round tube of radius along every bone, painted color (linear rgb), for three { mesh(...) }. Unlike skeleton's lines, which the viewer draws one pixel wide, tubes keep their thickness when a figure is scaled down, as in a recorded film. frame, channels and at as for skeleton.
override fun toString()

tuio.kt

A minimal TUIO receiver. TuioReceiver opens a UDP OSC port (3333 by default) through JavaOSC and listens on /tuio/2Dcur and /tuio/2Dobj only — no blobs, no 2.5D or 3D profiles. It keeps the live cursor and object sets, deduces additions and removals by diffing successive ALIVE messages, and on each FSEQ message hands your TuioListener one TuioReceiver.Frame holding both the full state and the per-frame added / updated / removed lists.

Call TuioReceiver.start to bind and TuioReceiver.close (or stop()) to release the port; both are idempotent, and starting a receiver closes the one previously started on its port, so a box can build a fresh receiver on every run. Callbacks arrive on JavaOSC's receive thread, not yours, and all internal state is guarded by one lock. Each profile's FSEQ emits a frame, so a tracker that sends both cursors and objects produces two frames per tracker frame, each carrying the full state of both. Numeric OSC arguments are coerced leniently, and malformed set messages are skipped rather than reported. main is a demo that prints frame deltas.

src/jvmMain/kotlin/afield/io/osc/tuio.kt — read the source

Top-level functions

Declaration Summary
fun main() Tiny demo: prints frame deltas.

TuioListener

fun interface TuioListener

Declaration Summary
fun onFrame(frame: TuioReceiver.Frame)

TuioReceiver

class TuioReceiver(private val localPort: Int = 3333, private val listener: TuioListener = TuioListener {}) : Closeable

Minimal TUIO/UDP receiver for: - /tuio/2Dcur (cursors) - /tuio/2Dobj (fiducials/objects)

Declaration Summary
data class TuioCursor(val sessionId: Long, val x: Float, val y: Float, val xVel: Float, val yVel: Float, val motionAccel: Float)
data class TuioObject(val sessionId: Long, val classId: Long, val x: Float, val y: Float, val angle: Float, val xVel: Float, val yVel: Float, val aVel: Float, val motionAccel: Float, val rotationAccel: Float)
data class Frame(val fseq: Int, val source: String?, val cursors: Collection<TuioCursor>, val objects: Collection<TuioObject>, val cursorAdded: List<TuioCursor>, val cursorUpdated: List<TuioCursor>, val cursorRemoved: List<TuioCursor>, val objectAdded: List<TuioObject>, val objectUpdated: List<TuioObject>, val objectRemoved: List<TuioObject>)
fun start() Bind the port and start delivering frames. Idempotent.
fun stop()
override fun close()

JSONLineServer.kt

A TCP listener that turns newline-delimited JSON into a value a box can read each frame. JSONLineServer binds a ServerSocket, accepts any number of clients on daemon threads, and keeps only the most recently received valid JSON line — JSONLineServer.output is a snapshot, not a queue, so lines that arrive between reads are dropped. Non-JSON lines are counted in JSONLineServer.invalidLineCount and logged, never thrown.

jsonl and JSONLineServers hand out one cached, already-started server per (port, bind host, backlog) so that re-running a box does not fight itself for the port; ephemeral port 0 servers are rejected by the cache and must be built with autoStart = false. Returning a JSONLineServer from a box keeps it running for that box's run — registerJSONLineServerReturnValue installs the ReturnValueRegistry handler that does it, and an uncached server is stopped when the run ends.

src/jvmMain/kotlin/afield/io/process/JSONLineServer.kt — read the source

Top-level functions

Declaration Summary
fun jsonl(port: Int = 7777, bindHost: String = "0.0.0.0", backlog: Int = 4): JSONLineServer
fun registerJSONLineServerReturnValue()

JSONLineServer

class JSONLineServer private constructor(localPort: Int, val bindHost: String = "0.0.0.0", val backlog: Int = 4, autoStart: Boolean = true, private val cacheKey: JSONLineServerKey? = null) : Closeable

Small TCP server for newline-delimited JSON streams.

Declaration Summary
val bindHost: String
val backlog: Int
val requestedPort: Int
var port: Int
var running: Boolean
var latestReceivedAtMillis: Long
var latestConnectedAtMillis: Long
var latestDisconnectedAtMillis: Long
val output: JSONObject
val latest: JSONObject?
val outputText: String?
val latestLine: String?
val latestRawLine: String?
val latestRemoteAddress: String?
val latestConnectionRemoteAddress: String?
val latestDisconnectedRemoteAddress: String?
val lastError: String?
val lineCount: Long
val invalidLineCount: Long
val connectionCount: Long
val activeConnectionCount: Long
fun hasOutput(): Boolean
fun start(): JSONLineServer
fun stop()
override fun close()
override fun toString(): String

JSONLineServers

object JSONLineServers

Declaration Summary
fun get(port: Int = 7777, bindHost: String = "0.0.0.0", backlog: Int = 4): JSONLineServer
fun remove(server: JSONLineServer)

JSONProcess.kt

Runs an external command and reads newline-delimited JSON from its stdout. Build a JSONProcess with a working directory, a command and an optional environment; the most recent parsable stdout line is exposed as JSONProcess.output (an org.json.JSONObject) — a snapshot of the latest line, not a queue. A leading ~ is expanded in the working directory and in the executable, but not in the remaining arguments.

Returning a JSONProcess from a box starts the subprocess and ties it to that run: registerJSONProcessReturnValue installs the ReturnValueRegistry handler, and the process is destroyed — then force-destroyed if it will not exit — when the box re-runs or the job completes. stdout and stderr are drained on daemon threads; non-JSON lines, stderr output and a failure to start are recorded in JSONProcess.lastError and logged rather than thrown, so check JSONProcess.running, JSONProcess.exitCode and JSONProcess.hasOutput to know what happened.

src/jvmMain/kotlin/afield/io/process/JSONProcess.kt — read the source

Top-level functions

Declaration Summary
fun registerJSONProcessReturnValue()

JSONProcess

class JSONProcess(workingDirectory: String? = null, vararg command: String, val env: Map<String, String> = emptyMap())

Declaration Summary
val env: Map<String, String>
val command: List<String>
var running: Boolean
var exitCode: Int?
val output: JSONObject
val outputText: String?
val latestStdoutLine: String?
val latestStderrLine: String?
val lastError: String?
fun hasOutput(): Boolean
override fun toString(): String
fun stop()

numpy.kt

Reader for NumPy's .npy and .npz on-disk formats. npy and npz are the short entry points over NumpyIO; .npz is unzipped into a LinkedHashMap of entry name to NpyArray and must contain at least one .npy member. An NpyArray holds the raw payload bytes plus the shape, the Fortran-order flag and an NpyDType, and converts on demand via NpyArray.asDoubleArray, NpyArray.asFloatArray, NpyArray.asLongArray, NpyArray.asULongArray, NpyArray.asBooleanArray and NpyArray.asComplexDoublePairs — Fortran-ordered data is remapped to C order unless you ask for NpyLinearOrder.STORAGE.

Read-only and pure JVM: there is no writer and no ndarray arithmetic. Format versions 1–3 are accepted; bool, signed/unsigned int, half/float/double and complex64/128 decode, and any other dtype kind (NpyKind.OTHER) throws. Element and byte counts must fit in an Int.

src/jvmMain/kotlin/afield/lang/python/numpy.kt — read the source

Top-level functions

Declaration Summary
fun npy(path: String): NpyArray Reads a .npy file — npy("data.npy").asDoubleArray() is the whole idiom.
fun npy(file: File): NpyArray Reads a .npy from a File — the overload to pair with "data.npy".file.
fun npz(path: String): LinkedHashMap<String, NpyArray> Reads a .npz archive into a map of member name to array, preserving the archive's own order.
fun npz(file: File): LinkedHashMap<String, NpyArray> Reads a .npz archive from a File, keyed by member name with .npy stripped.

NpyLinearOrder

enum class NpyLinearOrder

Which order to walk a multidimensional array in: C is row-major, last index varying fastest; STORAGE is however the file happens to hold it.

Constants: C, STORAGE.

NpyByteOrder

enum class NpyByteOrder

The byte order a dtype declares, from the <, >, = or | character in its descriptor.

Constants: LITTLE_ENDIAN, BIG_ENDIAN, NATIVE, NOT_APPLICABLE.

NpyKind

enum class NpyKind

The family a dtype belongs to — what decides which conversion will accept it.

Constants: BOOL, SIGNED_INT, UNSIGNED_INT, FLOAT, COMPLEX, OTHER.

NpyDType

data class NpyDType(val descriptor: String, val byteOrder: NpyByteOrder, val kind: NpyKind, val itemSize: Int)

A parsed NumPy dtype: the descriptor exactly as the file wrote it, plus the three things worth acting on — byte order, kind, and bytes per element.

Declaration Summary
val descriptor: String
val byteOrder: NpyByteOrder
val kind: NpyKind
val itemSize: Int
val isNumeric: Boolean Whether this dtype is one the reader can convert at all — anything but NpyKind.OTHER.

NpyArray

class NpyArray internal constructor(shape: IntArray, val dtype: NpyDType, val fortranOrder: Boolean, private val rawBytes: ByteArray)

One array read out of a .npy file: the raw payload bytes, the shape, the Fortran-order flag and the dtype, with conversion to Kotlin arrays on demand.

Declaration Summary
val dtype: NpyDType The dtype as declared in the file's header.
val fortranOrder: Boolean True when the payload is column-major, as NumPy writes for an array created with order='F' or produced by transposing. Only meaningful above rank 1.
val shape: List<Int> The array's dimensions, outermost first — NumPy's .shape. Empty for a 0-d scalar array.
val rank: Int How many dimensions there are — NumPy's .ndim.
val elementCount: Int Total number of elements, the product of shape — NumPy's .size. 1 for a 0-d array, not 0.
val nbytes: Int Size of the payload in bytes, elementCount * dtype.itemSize — NumPy's .nbytes.
fun copyBytes(): ByteArray A copy of the undecoded payload, exactly as it sat in the file. For handing to your own decoder.
fun linearIndex(vararg indices: Int): Int The flat offset of an element from its per-dimension indices, in storage order.
fun linearIndex(indices: IntArray, order: NpyLinearOrder = NpyLinearOrder.STORAGE): Int The flat offset of an element from its indices, in the order you name. Defaults to storage order, not C.
fun getDouble(vararg indices: Int): Double One element as a Double, whatever its dtype — a.getDouble(2, 3).
fun getDouble(indices: IntArray, order: NpyLinearOrder = NpyLinearOrder.STORAGE): Double One element as a Double, with the index order named. Defaults to storage order.
fun asBooleanArray(order: NpyLinearOrder = NpyLinearOrder.C): BooleanArray The array as a BooleanArray, C order by default. Only for a bool dtype — anything else throws rather than being coerced, so a 0/1 integer array has to go through asLongArray.
fun asLongArray(order: NpyLinearOrder = NpyLinearOrder.C): LongArray The array as a LongArray, C order by default. For bool and every integer dtype, signed or unsigned.
fun asULongArray(order: NpyLinearOrder = NpyLinearOrder.C): ULongArray The array as a ULongArray, C order by default — the unsigned counterpart of asLongArray, and the one to use for a uint64 that may run past the signed range.
fun asDoubleArray(order: NpyLinearOrder = NpyLinearOrder.C): DoubleArray The array as a DoubleArray, C order by default — the workhorse conversion, and what most callers want.
fun asFloatArray(order: NpyLinearOrder = NpyLinearOrder.C): FloatArray The array as a FloatArray, C order by default — asDoubleArray narrowed, for feeding a GPU buffer or Field's Vec types.
fun asComplexDoublePairs(order: NpyLinearOrder = NpyLinearOrder.C): DoubleArray A complex array flattened to interleaved real/imaginary pairs: 2 * elementCount doubles, real at even indices and imaginary at odd. C order by default.
override fun toString(): String

NumpyIO

object NumpyIO

The reader itself: .npy and .npz off disk, with the header parsing and dtype decoding behind it.

Declaration Summary
fun readNpy(path: String): NpyArray Reads one .npy from a path. The path is used verbatim — it does not go through FileManager.
fun readNpy(file: File): NpyArray Reads one .npy file into an NpyArray.
fun readNpz(path: String): LinkedHashMap<String, NpyArray> Reads a .npz archive from a path, keyed by member name with the .npy suffix removed.
fun readNpz(file: File): LinkedHashMap<String, NpyArray> Reads every .npy member of a .npz archive, keyed by member name without the .npy suffix, in the order the zip stores them.

EmbeddingSimplex.kt

A handful of embeddings as a shape you can look at: every pairwise distance, and the points placed in 3D so that those distances are the lengths of the edges between them.

Any four points of a Euclidean space sit exactly in three dimensions, however many the space has. So four 512-dimensional CLIP vectors make a tetrahedron whose six edges are their six distances, not an approximation of them: classical MDS on four points recovers them up to rotation and reflection. Three points make a triangle (exact in a plane), two a segment. Five or more do not fit in general: EmbeddingSimplex.stress1 says by how much, and the edges drawn are then the best 3D fit rather than the distances in the table.

val simplex = embeddingSimplex(
    listOf(clipEncodeImage(cat), clipEncodeImage(dog), clipEncodeImage(tiger), clipEncode("a photo of a cat")),
    labels = listOf("cat", "dog", "tiger", "text"),
)
simplex.edges.maxBy { it.distance }      // the two that are furthest apart
showSimplex(simplex)                      // draw it (afield.graphics.three)

Which distance

With normalize on (the default), each vector is scaled to unit length and EmbeddingSimplex.distances are chord lengths, |a − b| = √(2 − 2 cos θ), which range over 0..2. Chord length is a true Euclidean distance between the points on the sphere, which is what makes the embedding exact; cosine distance 1 − cos θ is half the square of it and is not a metric, so it is carried along in EmbeddingSimplex.cosineDistances for reading but never fed to MDS.

The 3D coordinates

EmbeddingSimplex.points come from smileMdsDistances with positiveCorrection = false: Torgerson's classical MDS, the double-centred −½ D² and its leading eigenvectors scaled by √λ, three of them once there are four points or more. They are centred on the origin and multiplied by EmbeddingSimplex.scale so that the point farthest from the centre lies at EmbeddingSimplex.radius; multiply a distance by that scale to get an edge's drawn length. Signs are not canonicalised, so two machines may draw mirror images of one simplex.

src/jvmMain/kotlin/afield/math/statistics/EmbeddingSimplex.kt — read the source

Top-level functions

Declaration Summary
fun embeddingSimplex(vectors: List<DoubleArray>, labels: List<String> = vectors.indices.map { "${it + 1}" }, normalize: Boolean = true, radius: Double = 3.0): EmbeddingSimplex Build an EmbeddingSimplex from equal-length vectors: any embedding, not only CLIP.
fun embeddingSimplex(vectors: List<FloatArray>, labels: List<String> = vectors.indices.map { "${it + 1}" }, normalize: Boolean = true, radius: Double = 3.0): EmbeddingSimplex embeddingSimplex over FloatArrays.
fun embeddingSimplex(vectors: List<ClipVector>, labels: List<String> = vectors.indices.map { "${it + 1}" }, radius: Double = 3.0): EmbeddingSimplex embeddingSimplex over CLIP vectors, from images and prompts alike: they share one space.

EmbeddingSimplex

class EmbeddingSimplex internal constructor(val labels: List<String>, val distances: Array<DoubleArray>, val cosineDistances: Array<DoubleArray>, val points: List<Vec3>, val scale: Double, val radius: Double, val edges: List<SimplexEdge>, val stress1: Double)

The distances among a few embeddings, with a 3D placement of them whose edge lengths are those distances. exact says whether the drawing is faithful; showSimplex draws it.

Declaration Summary
val labels: List<String> One per point, in input order.
val distances: Array<DoubleArray> Chord distances between the embeddings, n × n; plain Euclidean ones with normalize = false.
val cosineDistances: Array<DoubleArray> 1 − cos θ between the points, n × n. Carried for reading; never fed to MDS.
val points: List<Vec3> The 3D positions, centred on the origin and scaled by scale; points[i](workspace.md#stdlib-kt--i) is input i.
val scale: Double Display units per unit of distance.
val radius: Double Where the point farthest from the centre is drawn, in display units.
val edges: List<SimplexEdge> Every pair a < b, in the order (0,1), (0,2), … (n−2,n−1).
val stress1: Double Kruskal's stress-1 of the 3D placement against distances. It is at or below the threshold exact tests against whenever the fit is exact (four points or fewer), and larger when the points need more than three dimensions.
val size: Int How many points.
val exact: Boolean Whether the drawn edges are the distances, to EXACT_STRESS: always for four or fewer points.
fun edge(a: Int, b: Int): SimplexEdge The edge between a and b, in either order.
fun edgesFrom(i: Int): List<SimplexEdge> The edges that touch point i, closest first.
override fun toString(): String A header line, then one line per edge: the two labels, the distance, and the cosine distance.

SimplexEdge

data class SimplexEdge(val a: Int, val b: Int, val distance: Double, val cosineDistance: Double, val rankFromA: Int, val rankFromB: Int)

One pair of an EmbeddingSimplex: the two points, how far apart they are, and where that distance ranks at each end.

Declaration Summary
val a: Int Index of one end, the lower of the two.
val b: Int Index of the other end, always greater than a.
val distance: Double Chord (or Euclidean) distance between the two embeddings.
val cosineDistance: Double 1 − cos θ between them.
val rankFromA: Int Where this edge comes among the edges that touch a, 0 being the closest of them; ties go to the lower point index. With four points the ranks run 0, 1, 2, and showSimplex with flags = true draws them as green, orange and red diamonds at each end of each edge.
val rankFromB: Int The same for b.
fun rankFrom(i: Int): Int rankFromA or rankFromB, for whichever end i is.

clustering.kt

Clustering for lists of Float, Vec2, Vec3 and Vec4.

Every algorithm here returns either a hierarchy or a centre drawn from the data it summarises: a medoid is one of your own samples, and a median is coordinate-wise robust against an outlier. There is no k-means, whose centroid is neither.

Hierarchical clustering — floatAgglomerative, vec2Agglomerative, vec3Agglomerative, vec4Agglomerative, which wrap Smile's HierarchicalClustering over the seven AgglomerativeLinkages — returns an AgglomerativeClustering holding the merge tree, which you cut into a ClusterLayer with AgglomerativeClustering.partition, AgglomerativeClustering.partitionByHeight or AgglomerativeClustering.layers. The …KMedoids and …KMedians functions are both local implementations in this file, each with D²-weighted seeding and KMedoidsOptions.runs/KMediansOptions.runs restarts keeping the best objective: k-medoids alternates assignment with an exact per-cluster re-medoid, k-medians is Lloyd's algorithm on the L1 metric with a coordinate-wise median. Cuts of the merge tree come back with labels renumbered by first appearance; every result type can be turned into a ClusterLayer.

visualizeVec3ClusterLayers3D, visualizeVec2ClusterLayers3D and visualizeFloatClusterLayers3D — plus the AgglomerativeClustering.visualizeVec3-style extensions that build the layers for you — stack a list of ClusterLayers along one axis and return FLines in a map keyed "cluster-points", "cluster-points-halo", "cluster-centers", "cluster-centers-halo", "cluster-links" and "cluster-guides". ClusterLayerVizOptions controls spacing, sizes and which of those layers are emitted.

src/jvmMain/kotlin/afield/math/statistics/clustering.kt — read the source

Used in: Points into structure — Voronoi, Apollonius, contours and clusters, Typography as geometry — a string becomes contours

Top-level functions

Declaration Summary
fun floatAgglomerative(values: List<Float>, linkage: AgglomerativeLinkage = AgglomerativeLinkage.WARD): AgglomerativeClustering Hierarchical clustering of one-dimensional values: build the whole merge tree now, choose where to cut later.
fun vec2Agglomerative(points: List<Vec2>, linkage: AgglomerativeLinkage = AgglomerativeLinkage.WARD): AgglomerativeClustering Hierarchical clustering of 2-D points: build the whole merge tree now, choose where to cut later.
fun vec3Agglomerative(points: List<Vec3>, linkage: AgglomerativeLinkage = AgglomerativeLinkage.WARD): AgglomerativeClustering Hierarchical clustering of 3-D points: build the whole merge tree now, choose where to cut later.
fun vec4Agglomerative(points: List<Vec4>, linkage: AgglomerativeLinkage = AgglomerativeLinkage.WARD): AgglomerativeClustering Hierarchical clustering of 4-D points: build the whole merge tree now, choose where to cut later.
fun floatKMedoids(values: List<Float>, k: Int, options: KMedoidsOptions = KMedoidsOptions()): KMedoidsResult<Float> Partition one-dimensional values into k clusters, each summarised by one of your own samples.
fun vec2KMedoids(points: List<Vec2>, k: Int, options: KMedoidsOptions = KMedoidsOptions()): KMedoidsResult<Vec2> Partition 2-D points into k clusters, each summarised by one of your own samples.
fun vec3KMedoids(points: List<Vec3>, k: Int, options: KMedoidsOptions = KMedoidsOptions()): KMedoidsResult<Vec3> Partition 3-D points into k clusters, each summarised by one of your own samples.
fun vec4KMedoids(points: List<Vec4>, k: Int, options: KMedoidsOptions = KMedoidsOptions()): KMedoidsResult<Vec4> Partition 4-D points into k clusters, each summarised by one of your own samples.
fun floatKMedians(values: List<Float>, k: Int, options: KMediansOptions = KMediansOptions()): KMediansResult<Float> Partition one-dimensional values into k clusters around coordinate-wise medians (Lloyd's algorithm on L1).
fun vec2KMedians(points: List<Vec2>, k: Int, options: KMediansOptions = KMediansOptions()): KMediansResult<Vec2> Partition 2-D points into k clusters around coordinate-wise medians (Lloyd's algorithm on L1).
fun vec3KMedians(points: List<Vec3>, k: Int, options: KMediansOptions = KMediansOptions()): KMediansResult<Vec3> Partition 3-D points into k clusters around coordinate-wise medians (Lloyd's algorithm on L1).
fun vec4KMedians(points: List<Vec4>, k: Int, options: KMediansOptions = KMediansOptions()): KMediansResult<Vec4> Partition 4-D points into k clusters around coordinate-wise medians (Lloyd's algorithm on L1).
fun visualizeVec3ClusterLayers3D(points: List<Vec3>, layers: List<ClusterLayer>, options: ClusterLayerVizOptions = ClusterLayerVizOptions()): Map<String, List<FLine>> Draw a stack of clusterings of the same 3-D point cloud as FLines.
fun visualizeVec2ClusterLayers3D(points: List<Vec2>, layers: List<ClusterLayer>, options: ClusterLayerVizOptions = ClusterLayerVizOptions()): Map<String, List<FLine>> Draw a stack of clusterings of the same 2-D point cloud, one layer per z-plane.
fun visualizeFloatClusterLayers3D(values: List<Float>, layers: List<ClusterLayer>, options: ClusterLayerVizOptions = ClusterLayerVizOptions()): Map<String, List<FLine>> Draw a stack of clusterings of the same one-dimensional data.
fun AgglomerativeClustering.visualizeVec3(points: List<Vec3>, kValues: IntProgression = 2..6, options: ClusterLayerVizOptions = ClusterLayerVizOptions()): Map<String, List<FLine>> Cut this hierarchy at each of kValues and draw the stack in one call — the two-step layers + visualizeVec3ClusterLayers3D with the range filtered to what the data supports.
fun AgglomerativeClustering.visualizeVec2(points: List<Vec2>, kValues: IntProgression = 2..6, options: ClusterLayerVizOptions = ClusterLayerVizOptions()): Map<String, List<FLine>> Cut this hierarchy at each of kValues and draw the stack of 2-D layers in one call.
fun AgglomerativeClustering.visualizeFloat(values: List<Float>, kValues: IntProgression = 2..6, options: ClusterLayerVizOptions = ClusterLayerVizOptions()): Map<String, List<FLine>> Cut this hierarchy at each of kValues and draw the stack of 1-D layers in one call.

AgglomerativeLinkage

enum class AgglomerativeLinkage

How the distance between two clusters is defined, which is the whole content of a hierarchical clustering — the point distances are the same either way.

Constants: SINGLE, COMPLETE, AVERAGE, WEIGHTED_AVERAGE, CENTROID, MEDIAN, WARD.

MedoidDistance

enum class MedoidDistance

The metric the …KMedoids family assigns and re-medoids with.

Constants: EUCLIDEAN, MANHATTAN.

KMedoidsOptions

data class KMedoidsOptions(val maxIterations: Int = 100, val runs: Int = 6, val distance: MedoidDistance = MedoidDistance.MANHATTAN, val randomSeed: Long? = null)

How the …KMedoids functions run.

Declaration Summary
val maxIterations: Int Most assign/re-medoid rounds per restart. The loop stops early the first time the medoid set does not move, which is usually well inside ten rounds, so this is a safety bound rather than a setting to tune.
val runs: Int Independent restarts from different D²-weighted seeds; the lowest-distortion one wins.
val distance: MedoidDistance The metric, used both for assignment and for choosing each cluster's medoid.
val randomSeed: Long? Fixes the seeding, so the whole thing is reproducible. null uses Random.Default and successive calls may differ.

KMediansOptions

data class KMediansOptions(val maxIterations: Int = 100, val tolerance: Double = 1e-4, val runs: Int = 6, val randomSeed: Long? = null)

How the …KMedians functions run.

Declaration Summary
val maxIterations: Int Most assign/re-centre rounds per restart, as a safety bound.
val tolerance: Double Convergence threshold, applied to both the change in objective and the total L1 movement of the centres — the round has to be quiet on both counts to stop. Must be finite and non-negative.
val runs: Int Independent restarts from different D²-weighted seeds; the lowest-objective one wins.
val randomSeed: Long? Fixes the seeding for reproducibility; null uses Random.Default.

ClusterLayer

class ClusterLayer(val name: String, labels: IntArray)

One clustering of one dataset: a label per sample, and a name to draw it under.

Declaration Summary
val name: String What this cut is called — "k=4", "h=0.3", "kmedoids(k=3)" by default.
val labels: IntArray One cluster index per sample, in input order. A defensive copy: mutating it changes nothing.
val sampleCount: Int How many samples were labelled.
val clusterCount: Int Highest label plus one — so an unused index in the middle still counts, and this is the length a palette needs rather than the number of non-empty clusters.
fun indices(cluster: Int): IntArray Which samples landed in cluster, as indices into the original list. Empty for a cluster nothing was assigned to.

AgglomerativeClustering

class AgglomerativeClustering internal constructor(val linkage: AgglomerativeLinkage, rawModel: HierarchicalClustering)

A whole merge hierarchy, not one clustering — the answer the …Agglomerative functions return.

Declaration Summary
val linkage: AgglomerativeLinkage Which cluster-distance rule built this hierarchy. See AgglomerativeLinkage.
val sampleCount: Int How many samples were clustered — one more than the number of merges, since each merge removes a cluster.
val tree: Array<IntArray> The merge tree, one [left, right] pair per merge in merge order.
val heights: DoubleArray The dissimilarity at which each merge happened, parallel to tree — the y-axis of a dendrogram and the scale partitionByHeight cuts on.
fun partition(k: Int, name: String = "k=$k"): ClusterLayer Cut the hierarchy into exactly k clusters. k must be within 1..sampleCount.
fun partitionByHeight(height: Double, name: String = "h=$height"): ClusterLayer Cut every branch taller than height, letting the data decide how many clusters that is.
fun layers(kValues: Iterable<Int>): List<ClusterLayer> One ClusterLayer per requested k, ready to stack in a visualize… call.
fun defaultLayers(maxK: Int = 6): List<ClusterLayer> Cuts at 2..maxK, clamped to what the data can support — a sensible stack with no arguments.

KMedoidsResult

data class KMedoidsResult<T>(val labels: IntArray, val medoids: List<T>, val medoidIndices: IntArray, val distortion: Double, val iterations: Int, val converged: Boolean, val clusterSizes: IntArray, val options: KMedoidsOptions)

What a …KMedoids call found: the labels, the medoids themselves, and enough to judge whether to believe them.

Declaration Summary
val labels: IntArray One cluster index per input sample, in input order, each in 0 until k.
val medoids: List<T> The chosen medoids themselves — always actual input samples, which is the whole point of k-medoids over k-means: medoids[c](solvers.md#ceresexpression-kt--c) is input[medoidIndices[c](solvers.md#ceresexpression-kt--c)].
val medoidIndices: IntArray Where each medoid sits in the input list, parallel to medoids.
val distortion: Double The sum over samples of the distance to the assigned medoid, in KMedoidsOptions.distance's metric.
val iterations: Int Assign/re-medoid rounds taken by the winning restart.
val converged: Boolean true if the winning restart stopped because the medoid set repeated, rather than because it hit KMedoidsOptions.maxIterations. A false here means the numbers below are a snapshot of an unfinished run.
val clusterSizes: IntArray How many samples each cluster holds. A zero is possible: an empty cluster keeps its medoid.
val options: KMedoidsOptions The options this run used, carried along so a result is self-describing.
val k: Int How many clusters this result actually has.
fun asLayer(name: String = "kmedoids(k=$k)"): ClusterLayer This result as a ClusterLayer, so it can be stacked and drawn beside other cuts.

KMediansResult

data class KMediansResult<T>(val labels: IntArray, val centers: List<T>, val objective: Double, val iterations: Int, val converged: Boolean, val clusterSizes: IntArray, val options: KMediansOptions)

What a …KMedians call found.

Declaration Summary
val labels: IntArray One cluster index per input sample, in input order, each in 0 until k.
val centers: List<T> The coordinate-wise median of each cluster. Not necessarily an input sample.
val objective: Double Sum of L1 distances from every sample to its assigned centre. Lower is better, and it is what the restarts minimise; as with KMedoidsResult.distortion it cannot be used to choose k.
val iterations: Int Assign/re-centre rounds taken by the winning restart.
val converged: Boolean true if the winning restart stopped because both the objective and the centres went quiet within KMediansOptions.tolerance, rather than on KMediansOptions.maxIterations.
val clusterSizes: IntArray How many samples each cluster holds. An emptied cluster is re-seeded from the worst-fit sample.
val options: KMediansOptions The options this run used.
val k: Int How many clusters this result actually has.
fun asLayer(name: String = "kmedians(k=$k)"): ClusterLayer This result as a ClusterLayer, so it can be stacked and drawn beside other cuts.

ClusterLayerVizOptions

data class ClusterLayerVizOptions(val layerSpacing: Float = 0.7f, val clusterOffset: Float = 0.0f, val pointSize: Double = 0.1, val pointHaloSize: Double = 0.3, val centerPointSize: Double = 0.14, val centerHaloSize: Double = 22.0, val linkThickness: Double = 0.85, val pointAlpha: Float = 0.92f, val haloAlpha: Float = 0.14f, val centerAlpha: Float = 0.98f, val linkAlpha: Float = 0.16f, val drawLinks: Boolean = true, val drawCenters: Boolean = true, val drawGuides: Boolean = true)

How a stack of ClusterLayers is laid out and drawn.

Declaration Summary
val layerSpacing: Float Distance between consecutive layers along the stacking axis. Zero superimposes them.
val clusterOffset: Float Extra separation between clusters within a layer — a fan-out that makes membership visible when colour alone is not enough. Zero keeps every point at its own coordinates.
val pointSize: Double Size of a sample's core dot.
val pointHaloSize: Double Size of the soft halo drawn behind each sample, which is what makes dense regions read as bright.
val centerPointSize: Double Size of a cluster centre's core dot — slightly larger than a sample's, so centres read as centres.
val centerHaloSize: Double Size of the glow behind a cluster centre. Much larger than the others.
val linkThickness: Double Stroke thickness of the sample-to-centre links.
val pointAlpha: Float Opacity of a sample's core dot.
val haloAlpha: Float Opacity of the halos. Low, because they accumulate: overlap is the signal.
val centerAlpha: Float Opacity of a cluster centre's core dot.
val linkAlpha: Float Opacity of the links, low enough that a thousand of them still show shape rather than ink.
val drawLinks: Boolean Emit cluster-links: one segment from every sample to its cluster centre.
val drawCenters: Boolean Emit cluster-centers and cluster-centers-halo.
val drawGuides: Boolean Emit cluster-guides: a rectangle around each layer, so the stacking is legible. Ignored by the Vec3 visualization, which has no plane to draw one on.

smile_mds.kt

Classical multidimensional scaling and t-SNE, over equal-length vectors or over a distance matrix you computed yourself, wrapping Smile.

Four entry points in two pairs. smileMds and smileTsne take an array of equal-length vectors and build the dissimilarities for you, cosine or Euclidean by SmileMdsDistance. smileMdsDistances and smileTsneDistances take the symmetric matrix instead, which is the route for an edit distance, a transport distance or a hop count, and carry a label you supply through to the result. Each returns its own type, and in all four the ith row of coordinates is the ith thing you passed in. public/docs/projection_examples.md works both routes end to end.

Which of the two methods to reach for is the familiar split, and that page measures it on one dataset: classical MDS reproduces the global distances and scrambles local neighbourhoods, t-SNE the reverse.

What all four do before Smile sees anything

Values have to be finite; a supplied matrix has to be square, non-negative, symmetric to 1e-9 and zero on the diagonal; dimensions has to be below the number of rows, and at least 1 for MDS and 2 for t-SNE. Smile's MDS.Options refuses one dimension, so MDS fits that case through the LAPACK path; the wrappers refuse first so the message names the argument you passed rather than arriving from inside Smile.

Memory is quadratic in the number of points before any fitting starts, because the dissimilarity matrix is dense: 191 MiB of double at 5000 points, 3 GiB at 20 000. Sample a long trajectory rather than projecting all of it.

The classical path has two eigensolvers

smile.manifold.MDS takes its top-d eigenpairs from ARPACK, whose native library cannot be loaded on macOS/arm64 in this build. So smileMds and smileMdsDistances probe that binding once and, where it is missing, run a transcription of Smile's own algorithm over LAPACK's dsyevd: the same steps in the same order, at O(n^3) for all n eigenpairs against ARPACK's O(n^2 d) for the d largest. Signs are not canonicalised on either path, so a projection can come back reflected from a machine that takes the other one. Neither t-SNE entry point touches ARPACK.

What Smile does with three of the t-SNE settings

Read off Smile 5.0.1, at the defaults these wrappers pass it.

maxIterations is a ceiling rather than a count: past iteration 250 Smile stops as soon as the best cost it has seen is more than 50 iterations old, or the gradient falls under 1e-7. That 250 is also why the wrappers require a maxIterations above it, since Smile's momentum switch is fixed there and TSNE.Options refuses a maxIter that does not exceed it.

Leave earlyExaggeration at 12.0. Smile symmetrises the affinity matrix with a hard-coded factor of 12 and then divides by this value when the early phase ends, so any other setting leaves the affinities scaled by 12 / earlyExaggeration for the rest of the fit instead of changing only the early phase.

seed is installed in smile.math.MathEx's process-wide random source, which both t-SNE entry points do inside a lock. Two fits of the same input at one seed are therefore bit-identical, which is what makes a live-coded box redraw the same picture, and concurrent fits queue rather than overlap.

src/jvmMain/kotlin/afield/math/statistics/smile_mds.kt — read the source

Used in: Projecting a distance matrix

Top-level functions

Declaration Summary
fun smileMds(vectors: Array<DoubleArray>, dimensions: Int = 2, distance: SmileMdsDistance = SmileMdsDistance.COSINE, positiveCorrection: Boolean = true): SmileMdsProjection Project arbitrary equal-length vectors with Smile's classical MDS implementation.
fun smileMdsDistances(distances: Array<DoubleArray>, dimensions: Int = 2, label: String = "precomputed", positiveCorrection: Boolean = true): SmileMdsProximityProjection Project an explicit distance matrix; useful for transport, edit, and graph distances.
fun smileTsne(vectors: Array<DoubleArray>, dimensions: Int = 2, distance: SmileMdsDistance = SmileMdsDistance.COSINE, perplexity: Double = 30.0, learningRate: Double = 200.0, earlyExaggeration: Double = 12.0, maxIterations: Int = 750, seed: Long = 184_921L): SmileTsneProjection Project equal-length vectors with Smile's exact t-SNE implementation.
fun smileTsneDistances(distances: Array<DoubleArray>, dimensions: Int = 2, label: String = "precomputed", perplexity: Double = 30.0, learningRate: Double = 200.0, earlyExaggeration: Double = 12.0, maxIterations: Int = 750, seed: Long = 184_921L): SmileTsneProximityProjection Run fixed-seed t-SNE from an explicit distance matrix.

SmileMdsDistance

enum class SmileMdsDistance

Distance functions supplied by Field's general-purpose smileMds wrapper.

Constants: COSINE, EUCLIDEAN.

SmileMdsProjection

data class SmileMdsProjection(val coordinates: Array<DoubleArray>, val scores: DoubleArray, val retainedShare: DoubleArray, val distance: SmileMdsDistance, val positiveCorrection: Boolean, val stress1: Double, val distanceCorrelation: Double)

A classical multidimensional-scaling result produced in Field by Smile.

Declaration Summary
val coordinates: Array<DoubleArray> One row per input vector, in the order they were passed, dimensions columns wide. The configuration is centred on the origin and its axes are eigenvectors of the double-centred matrix, so read distances off it rather than coordinates: the axes have no names and their signs are not canonicalised.
val scores: DoubleArray The retained eigenvalues, largest first, one per column of coordinates. A value at or near zero says that column carries nothing, which is how to find the rank of a configuration: ask for one more dimension than you think it has and look at the last score.
val retainedShare: DoubleArray scores normalised over themselves (Smile's MathEx.unitize1), so this sums to 1 whatever fraction of the structure was kept. It says how the retained components divide up between each other and nothing about how much was thrown away; stress1 says that.
val distance: SmileMdsDistance Which dissimilarity smileMds built the projection from, echoed back so a plot can be labelled from the result alone.
val positiveCorrection: Boolean Whether an additive constant was applied to the off-diagonal dissimilarities before fitting. Echoed back, because it changes which matrix was decomposed: two projections are only comparable when they agree on it.
val stress1: Double Kruskal's stress-1 between the input dissimilarities and the distances between rows of coordinates, at the scale that minimises it (classical MDS does not promise the projection is in the input's units). 0 is a perfect fit; above about 0.2 is usually called poor. Measured against the dissimilarities as given, before any positiveCorrection constant.
val distanceCorrelation: Double Pearson correlation between the input dissimilarities and the projected distances, over every pair.
val proportion: DoubleArray The old name of retainedShare, which read as explained variance and is not.

SmileTsneProjection

data class SmileTsneProjection(val coordinates: Array<DoubleArray>, val cost: Double, val distance: SmileMdsDistance, val perplexity: Double, val seed: Long)

A reproducible t-SNE result produced by Smile from Field-owned vectors.

Declaration Summary
val coordinates: Array<DoubleArray> One row per input vector, in the order they were passed, dimensions columns wide, and zero-mean by column because Smile subtracts the column means as its last step. What survives here is which points are near which; the axes and the distance between two separated groups do not, and public/docs/projection_examples.md measures the gap between the true ratio of two cluster separations and the one a t-SNE plot of them shows.
val cost: Double Smile's Kullback-Leibler divergence between the input affinities and the projected ones, in bits, as last measured rather than as minimised: the fit evaluates it every tenth iteration and returns the final evaluation, not the best one it saw. It compares iterations within one fit and does not rank two fits against each other.
val distance: SmileMdsDistance Which dissimilarity the vectors were reduced to before fitting. EUCLIDEAN hands Smile the vectors and lets it compute distances itself; COSINE hands it a cosine-distance matrix Field built first.
val perplexity: Double The perplexity this fit ran at. A t-SNE plot cannot be read without it, since it is the dial between keeping local neighbourhoods and keeping the distance between clusters, and it is carried here so a caption can be written from the result.
val seed: Long The seed installed in Smile's process-wide random source for this fit. Two fits of the same input at the same seed are bit-identical; two at different seeds are two equally good answers that look nothing alike, so the seed belongs in the caption of a figure anyone else will read.

SmileMdsProximityProjection

data class SmileMdsProximityProjection(val coordinates: Array<DoubleArray>, val scores: DoubleArray, val retainedShare: DoubleArray, val label: String, val positiveCorrection: Boolean, val stress1: Double, val distanceCorrelation: Double)

Classical MDS over a caller-supplied symmetric dissimilarity matrix.

Declaration Summary
val coordinates: Array<DoubleArray> One row per row of the distance matrix, in its order, dimensions columns wide. These are invented coordinates whose pairwise distances approximate the ones you supplied: for a matrix that really is Euclidean and enough dimensions to hold it, they reproduce them exactly, and SmileMdsTest pins that to 1e-9 over a rank-3 configuration.
val scores: DoubleArray The retained eigenvalues, largest first, one per column of coordinates. A value at or near zero says that column carries nothing, which is how to find the rank of whatever the matrix describes.
val retainedShare: DoubleArray scores normalised over themselves (Smile's MathEx.unitize1), so this sums to 1 whatever fraction of the structure was kept. It says how the retained components divide up between each other and nothing about how much was thrown away; stress1 says that.
val label: String Whatever you passed to smileMdsDistances, carried through untouched. Nothing in the fit reads it: it is here so a box holding several projections can tell which matrix each came from.
val positiveCorrection: Boolean Whether an additive constant was applied to the off-diagonal dissimilarities before fitting. A distance that is a metric is not thereby Euclidean, and an edit distance or a hop count usually needs this on before classical MDS has more than a couple of usable dimensions.
val stress1: Double Kruskal's stress-1 between the input dissimilarities and the distances between rows of coordinates, at the scale that minimises it (classical MDS does not promise the projection is in the input's units). 0 is a perfect fit; above about 0.2 is usually called poor. Measured against the dissimilarities as given, before any positiveCorrection constant.
val distanceCorrelation: Double Pearson correlation between the input dissimilarities and the projected distances, over every pair.
val proportion: DoubleArray The old name of retainedShare, which read as explained variance and is not.

SmileTsneProximityProjection

data class SmileTsneProximityProjection(val coordinates: Array<DoubleArray>, val cost: Double, val label: String, val perplexity: Double, val seed: Long)

Exact t-SNE over a caller-supplied symmetric dissimilarity matrix.

Declaration Summary
val coordinates: Array<DoubleArray> One row per row of the distance matrix, in its order, dimensions columns wide, and zero-mean by column because Smile subtracts the column means as its last step. Near stays near; the axes and the distance between two separated groups carry nothing.
val cost: Double Smile's Kullback-Leibler divergence between the supplied affinities and the projected ones, in bits, as last measured rather than as minimised: the fit evaluates it every tenth iteration and returns the final evaluation, not the best one it saw. Useful within a fit, not between two.
val label: String Whatever you passed to smileTsneDistances, carried through untouched and read by nothing in the fit.
val perplexity: Double The perplexity this fit ran at, which is the dial between keeping local neighbourhoods and keeping the distance between clusters. A t-SNE plot cannot be read without it.
val seed: Long The seed installed in Smile's process-wide random source for this fit. Two fits of the same matrix at the same seed are bit-identical; two at different seeds are two equally good answers that look nothing alike.

smile_neighbors.kt

Nearest-neighbour indexes over Vec2 / Vec3 point lists, wrapping Smile's KDTree and LSH. Build one with vec2NN or vec3NN: both copy the points, reject non-finite coordinates, and return a Vec2NNIndex / Vec3NNIndex exposing nearest, kNearest and withinRadius. Results come back as VecNeighbor2 / VecNeighbor3 carrying the index into the original list and the true Euclidean distance, radius results sorted nearest-first.

VecNNMode.APPROXIMATE (the default) builds an LSH alongside the KD-tree and silently degrades to exact mode, with a message on stderr, if LSH construction fails. The brute-force fallbacks do not make approximate mode exact: kNearest tops up an answer that came back short, and withinRadius only runs one when the structure returns nothing at all, so a full-length or merely-incomplete LSH answer is passed straight through. Ask for VecNNMode.EXACT when the answer has to be right. VecNNBuildOptions tunes the LSH parameters and can forbid duplicate points.

src/jvmMain/kotlin/afield/math/statistics/smile_neighbors.kt — read the source

Used in: Neighbours and graphs on a point cloud

Top-level functions

Declaration Summary
fun vec2NN(points: List<Vec2>, mode: VecNNMode = VecNNMode.APPROXIMATE, options: VecNNBuildOptions = VecNNBuildOptions()): Vec2NNIndex Builds a 2D nearest-neighbor index from a non-empty list of finite points.
fun vec3NN(points: List<Vec3>, mode: VecNNMode = VecNNMode.APPROXIMATE, options: VecNNBuildOptions = VecNNBuildOptions()): Vec3NNIndex Builds a 3D nearest-neighbor index from a non-empty list of finite points.

VecNNMode

enum class VecNNMode

Which structure a Vec2NNIndex / Vec3NNIndex actually queries. EXACT searches the Smile KD-tree and returns the true nearest neighbours; APPROXIMATE (the default) searches a locality-sensitive hash instead, which only looks at points sharing a hash bucket with the query — so it can return the wrong neighbours, not merely take a different route to the right ones.

Constants: APPROXIMATE, EXACT.

VecNNBuildOptions

data class VecNNBuildOptions(val lshBucketWidth: Double = 1.0, val lshProjectionCount: Int = 8, val lshUniversalHashSize: Int = 9973, val allowDuplicatePoints: Boolean = true)

LSH build parameters and the duplicate-point policy for vec2NN / vec3NN. Only the approximate path reads the three lsh values; a VecNNMode.EXACT index ignores them entirely, so tuning them there changes nothing.

Declaration Summary
val lshBucketWidth: Double Width of the LSH random projections, in the same units as your point coordinates. Wider buckets collect more points, so a query considers more candidates and is likelier to find the true neighbours, at more work per query; narrower buckets are cheaper and miss more. Must be finite and greater than zero.
val lshProjectionCount: Int How many random projections make up one LSH hash value. More projections mean more selective buckets, which is the same trade as a narrower lshBucketWidth: fewer candidates examined, more true neighbours missed. Must be greater than zero.
val lshUniversalHashSize: Int Size of the universal hash table behind each LSH hash; the default 9973 is prime. Bigger costs memory and reduces accidental collisions between unrelated buckets. Must be greater than zero.
val allowDuplicatePoints: Boolean Leave this true and duplicate points are indexed normally, simply showing up as several neighbours at the same distance. Set it false and vec2NN / vec3NN reject any list holding two bit-for-bit identical points, with an IllegalArgumentException naming the offending index. Note that this compares raw coordinate bits, so 0.0 and -0.0 count as different points.

VecNeighbor2

data class VecNeighbor2(val index: Int, val point: Vec2, val distance: Double)

One 2D neighbour hit: where the point sits in the list you built the index from, a copy of the point itself, and how far it is from the query. Handed back by Vec2NNIndex; there is no reason to construct one by hand.

Declaration Summary
val index: Int Position of this point in the list originally passed to vec2NN. The index takes a copy but preserves order, so this is safe to use as a key into your own parallel arrays.
val point: Vec2 A fresh copy of the indexed point, not the instance you passed in. Vec2 is Field's mutable Vector2f, so writing to this cannot corrupt the index — and equally, mutating your original point after the build does not change what the index holds.
val distance: Double True Euclidean distance from the query to this point: not squared, and recomputed in Double from the stored coordinates rather than trusted from whatever the KD-tree or LSH reported. Zero when the query coincides with the point, which happens whenever you query with a point that is itself in the index.

VecNeighbor3

data class VecNeighbor3(val index: Int, val point: Vec3, val distance: Double)

One 3D neighbour hit: where the point sits in the list you built the index from, a copy of the point itself, and how far it is from the query. Handed back by Vec3NNIndex; there is no reason to construct one by hand.

Declaration Summary
val index: Int Position of this point in the list originally passed to vec3NN. The index takes a copy but preserves order, so this is safe to use as a key into your own parallel arrays.
val point: Vec3 A fresh copy of the indexed point, not the instance you passed in. Vec3 is Field's mutable Vector3f, so writing to this cannot corrupt the index — and equally, mutating your original point after the build does not change what the index holds.
val distance: Double True Euclidean distance from the query to this point: not squared, and recomputed in Double from the stored coordinates rather than trusted from whatever the KD-tree or LSH reported. Zero when the query coincides with the point, which happens whenever you query with a point that is itself in the index.

Vec2NNIndex

class Vec2NNIndex internal constructor(private val points: List<Vec2>, private val mode: VecNNMode, private val exactIndex: KDTree<Int>?, private val approxIndex: LSH<Int>?)

A built 2D nearest-neighbour index — a private snapshot of the points plus the Smile structures that search them. Get one from vec2NN (the constructor is internal) and ask it for nearest, kNearest or withinRadius.

Declaration Summary
val size: Int How many points the index holds. This is the length of the snapshot taken at build time, so it never changes, and it is at least 1 because vec2NN rejects an empty list.
fun nearest(query: Vec2): VecNeighbor2 The single closest indexed point to query, with its true Euclidean distance. Never null: the index is non-empty by construction, and if the backing structure declines to answer this falls through to a full scan.
fun kNearest(query: Vec2, k: Int): List<VecNeighbor2> The k closest indexed points to query, with distinct indices and no guaranteed order — unlike withinRadius this never sorts, it returns candidates in whatever order the backing structure produced them, so sort by distance yourself if you need nearest-first.
fun withinRadius(query: Vec2, radius: Double): List<VecNeighbor2> Every indexed point within radius of query — a true Euclidean distance, not a squared one — sorted nearest-first. Returns an empty list when nothing is in range; radius must be finite and at least zero or this throws.

Vec3NNIndex

class Vec3NNIndex internal constructor(private val points: List<Vec3>, private val mode: VecNNMode, private val exactIndex: KDTree<Int>?, private val approxIndex: LSH<Int>?)

A built 3D nearest-neighbour index — a private snapshot of the points plus the Smile structures that search them. Get one from vec3NN (the constructor is internal) and ask it for nearest, kNearest or withinRadius.

Declaration Summary
val size: Int How many points the index holds. This is the length of the snapshot taken at build time, so it never changes, and it is at least 1 because vec3NN rejects an empty list.
fun nearest(query: Vec3): VecNeighbor3 The single closest indexed point to query, with its true Euclidean distance. Never null: the index is non-empty by construction, and if the backing structure declines to answer this falls through to a full scan.
fun kNearest(query: Vec3, k: Int): List<VecNeighbor3> The k closest indexed points to query, with distinct indices and no guaranteed order — unlike withinRadius this never sorts, it returns candidates in whatever order the backing structure produced them, so sort by distance yourself if you need nearest-first.
fun withinRadius(query: Vec3, radius: Double): List<VecNeighbor3> Every indexed point within radius of query — a true Euclidean distance, not a squared one — sorted nearest-first. Returns an empty list when nothing is in range; radius must be finite and at least zero or this throws.

booleannetworks.kt

Synchronous Boolean networks and the pictures you draw of them. A BooleanNetwork is a list of BoolNodes whose update lambdas all fire at once on the current state vector; BooleanNetwork.trajectory and BooleanNetwork.trajectoryUntilCycle run one initial state forward, and BooleanNetwork.findAttractors walks the whole 2^size state-transition graph to return every Attractor with its basin — it is O(2^size) and returns an empty list above 20 nodes.

The drawing helpers return FLines for a box: BooleanNetwork.drawUnrolled (spacetime grid), BooleanNetwork.drawNetworkGraph (wiring diagram on a circle), BooleanNetwork.drawStateTransitionGraph (only for size <= 6), BooleanNetwork.drawAttractorBasins, and the standalone drawRhythmGrid for any list of Boolean lane states. BooleanNetwork.notateHtml and BooleanNetwork.notateAttractorsHtml emit HTML tables instead, BooleanNetwork.printAttractors plain text. Ready-made networks: toggleNetwork, threeNodeNetwork, segmentPolarityNetwork and the random N-K kauffmanNetwork.

src/jvmMain/kotlin/afield/simulation/automata/booleannetworks.kt — read the source

Used in: Boolean networks to FLine examples, Boolean network music examples

Top-level functions

Declaration Summary
fun BooleanNetwork.trajectory(initial: BooleanArray, steps: Int): List<BooleanArray> Advance initial forward by exactly steps synchronous updates.
fun BooleanNetwork.trajectoryUntilCycle(initial: BooleanArray, maxSteps: Int = 1024): List<BooleanArray> Follow the trajectory from initial until a state repeats or maxSteps is exceeded.
fun Attractor.repeatedCycle(steps: Int): List<BooleanArray> Repeat the attractor cycle to exactly steps states, preserving the original phase order.
fun BooleanNetwork.findAttractors(): List<Attractor> Enumerate every attractor and its basin of attraction by exhaustive traversal of the state-transition graph.
fun BooleanNetwork.drawUnrolled(trajectory: List<BooleanArray>, cellSize: Double = 24.0): List<FLine> Render the time evolution of a Boolean network as a spacetime grid.
fun BooleanNetwork.drawNetworkGraph(radius: Double = 120.0, nodeR: Double = 14.0): List<FLine> Render the wiring diagram of the network as a directed graph.
fun BooleanNetwork.drawStateTransitionGraph(radius: Double = 200.0): List<FLine> Render the full state-transition graph for small networks (size <= 6).
fun BooleanNetwork.drawAttractorBasins(attractors: List<Attractor>, samplesPerAttractor: Int = 6, cellSize: Double = 16.0): List<FLine> Render each attractor's basin as a stack of transient spacetime sketches.
fun drawRhythmGrid(steps: List<BooleanArray>, cellW: Double = 24.0, cellH: Double = 18.0, stepGap: Double = 5.0, laneGap: Double = 5.0, accentEvery: Int = 4): List<FLine> Render a step-by-step rhythm grid from a list of Boolean lane states.
fun BooleanNetwork.notateHtml(): String Generate an HTML table of nodes, input wiring, and update rules.
fun BooleanNetwork.notateAttractorsHtml(attractors: List<Attractor>): String Generate an HTML summary of attractor type, cycle states, and basin sizes.
fun BooleanNetwork.printAttractors() Print a plain-text attractor summary to stdout.
fun toggleNetwork(): BooleanNetwork A 2-node network with one period-4 cycle through all four states.
fun threeNodeNetwork(): BooleanNetwork A 3-node network with a single period-5 limit cycle.
fun segmentPolarityNetwork(): BooleanNetwork Simplified 4-node segment-polarity network inspired by Drosophila.
fun kauffmanNetwork(n: Int = 5, k: Int = 2, seed: Long = 42L): BooleanNetwork A random N-K Kauffman network.

BoolNode

data class BoolNode(val name: String, val rule: String, val inputs: List<Int>, val update: (BooleanArray) -> Boolean)

A single node in a Boolean network.

Declaration Summary
val name: String
val rule: String
val inputs: List<Int>
val update: (BooleanArray) -> Boolean

BooleanNetwork

class BooleanNetwork(val nodes: List<BoolNode>)

A synchronous Boolean network with size binary-state nodes.

Declaration Summary
val size: Int Number of nodes.
fun stateToInt(state: BooleanArray): Int Pack a state array to an integer (node i maps to bit i, LSB-first).
fun intToState(n: Int): BooleanArray Unpack an integer to a state array (bit i maps to node i).
fun step(state: BooleanArray): BooleanArray Compute the synchronous successor state: apply all update functions once.
fun stateString(state: BooleanArray): String Return a compact binary string with the highest-indexed node first.
fun stateTable(state: BooleanArray): String Format the current state as a readable node-by-node table.

Attractor

data class Attractor(val period: Int, val cycle: List<BooleanArray>, val basin: Set<Int>)

An attractor of the Boolean network: the minimal invariant set(s) reached after transients die out.

Declaration Summary
val period: Int
val cycle: List<BooleanArray>
val basin: Set<Int>

wfc.kt

Single-file, dependency-free Wave Function Collapse for Kotlin/JVM.

What this implements: - Learns unique fixed-size tiles from a source PNG or 2D FloatArray. - Learns adjacency constraints from a configurable symmetric neighborhood. - Learns overlapping N x M patterns from a PNG or 2D FloatArray. - Generates an output tile grid with an explicit step() loop for visualization. - Avoids per-neighbor/per-tile garbage in propagation by using reusable LongArray bitsets.

Notes: - The WFC solver operates only on integer tile IDs and constraints. The PNG/FloatArray simple-tiled and overlapping helpers sit outside it. - WfcSolver implements afield.ui.visualization.ToHTML, so evaluating a solver in a box draws the collapse instead of a reflective property tree. Every pixel of that view lives in wfc_field.kt (afield.simulation.automata.wfcSolverHtml), which this file names once, in WfcSolver.toHTML.

src/jvmMain/kotlin/afield/simulation/automata/wfc.kt — read the source

Used in: Wave Function Collapse examples

Top-level functions

Declaration Summary
fun wfcRules(tileCount: Int, neighborhood: WfcNeighborhood = WfcNeighborhood.VonNeumann, block: WfcRulesBuilder.() -> Unit): WfcRules Hand-author WFC adjacency constraints; shorthand for WfcRules.build.
fun WfcSolver.solveWithRetries(maxAttempts: Int = 16, prepare: WfcSolver.() -> Unit = {}): WfcOutcome Run the solver to completion, restarting after a contradiction, up to maxAttempts times.
fun BufferedImage.sliceIntoUniqueTiles(tileWidth: Int, tileHeight: Int): PngTileSet
fun loadPngTiles(file: File, tileWidth: Int, tileHeight: Int): PngTileSet
fun BufferedImage.sliceIntoOverlappingPatterns(patternWidth: Int, patternHeight: Int, wrapX: Boolean = true, wrapY: Boolean = true): PngOverlappingTileSet
fun loadPngOverlappingPatterns(file: File, patternWidth: Int, patternHeight: Int, wrapX: Boolean = true, wrapY: Boolean = true): PngOverlappingTileSet
fun savePng(image: BufferedImage, file: File)
fun FloatArray.sliceIntoUniqueTiles2D(width: Int, height: Int, tileWidth: Int, tileHeight: Int): FloatTileSet
fun FloatArray.sliceIntoOverlappingPatterns2D(width: Int, height: Int, patternWidth: Int, patternHeight: Int, wrapX: Boolean = true, wrapY: Boolean = true): FloatOverlappingTileSet
fun FloatOverlappingTileSet.expandSymmetry(symmetry: WfcSymmetry): FloatOverlappingTileSet Close this pattern set under symmetry, so the model stops caring which way up the source was.
fun PngOverlappingTileSet.expandSymmetry(symmetry: WfcSymmetry): PngOverlappingTileSet FloatOverlappingTileSet.expandSymmetry for ARGB pattern sets; see its KDoc and WfcSymmetry.
fun main(args: Array<String>) Tiny CLI demo: java -jar app.jar input.png output.png tileW tileH outTilesW outTilesH seed

Top-level values

Declaration Summary
const val WFC_NORTH: Int
const val WFC_EAST: Int
const val WFC_SOUTH: Int
const val WFC_WEST: Int
val WFC_VON_NEUMANN_NEIGHBORHOOD: WfcNeighborhood
val WFC_MOORE_NEIGHBORHOOD: WfcNeighborhood
val WFC_VON_NEUMANN_NEIGHBOURHOOD: WfcNeighbourhood
val WFC_MOORE_NEIGHBOURHOOD: WfcNeighbourhood

WfcOffset

data class WfcOffset(val dx: Int, val dy: Int)

WfcNeighborhood

class WfcNeighborhood(offsets: Iterable<WfcOffset>)

Symmetric set of relative cell offsets used for learning and propagation.

Declaration Summary
val directionCount: Int
val offsets: List<WfcOffset>
fun offset(direction: Int): WfcOffset
fun opposite(direction: Int): Int
fun directionOf(dx: Int, dy: Int): Int
override fun toString(): String

WfcNeighbourhood

typealias WfcNeighbourhood = WfcNeighborhood

WfcRules

class WfcRules private constructor(val tileCount: Int, val weights: DoubleArray, val neighborhood: WfcNeighborhood, val allowed: Array<Array<LongArray>>, private val widenedMasks: Array<BooleanArray>? = null, private val observedAllowed: Array<Array<LongArray>>? = null) : afield.ui.visualization.ToHTML

The learned model: tile weights plus directional adjacency masks.

Declaration Summary
val tileCount: Int
val weights: DoubleArray
val neighborhood: WfcNeighborhood
val allowed: Array<Array<LongArray>> alloweddirection is a bitset of tile IDs allowed in that direction from tile.
val wordCount: Int
fun allows(direction: Int, a: Int, b: Int): Boolean May tile b sit at direction from tile a?
fun findAsymmetry(): String? The first (direction, a, b) where adjacency is not symmetric, or null when the rules are sound. Returned as a human-readable string because it is a diagnostic, not a control path.
fun allowedCount(direction: Int, a: Int): Int How many tiles may sit at direction from a — the branching factor of the model.
fun wasWidened(direction: Int, tile: Int): Boolean Did the widening fallback fill in alloweddirection`` because the sample left it empty?
val widenedMaskCount: Int How many (direction, tile) masks the widening fallback filled in; 0 when it never fired.
fun observedAllows(direction: Int, a: Int, b: Int): Boolean allows, restricted to what the sample actually contained — false for a pair that exists only because of the widening fallback.
fun observedAndInventedPairCounts(): Pair<Int, Int> How many allowed pairs exist only because of widening, and how many the sample showed.
override fun toString(): String
override fun toHTML(): String The rule set's own inline view: one adjacency matrix per direction plus the weights strip, with the three things that silently break a solve — empty masks, asymmetry, zero weights — marked.

WfcRulesBuilder

class WfcRulesBuilder internal constructor(val tileCount: Int, val neighborhood: WfcNeighborhood)

Accumulates hand-authored WFC adjacency; see WfcRules.build, which is how you get one.

Declaration Summary
val tileCount: Int
val neighborhood: WfcNeighborhood
fun weight(tile: Int, weight: Double) Set the relative frequency of tile. Weights bias both which tile a collapse picks and the entropy that decides which cell to collapse next; they default to 1.0 (uniform).
fun weights(vararg values: Double) Set every tile weight at once, in tile-ID order.
fun allow(direction: Int, a: Int, b: Int) Allow tile b to sit at direction from tile a — and, symmetrically, a at the opposite direction from b.
fun allow(dx: Int, dy: Int, a: Int, b: Int) allow, addressing the direction by its offset instead of its index.
fun allowAny(direction: Int, a: Int, vararg others: Int) allow(direction, a, b) for every b in others — the common "a tolerates any of" case.
fun allowMissingAnything() Fill in "anything goes" for every (direction, tile) pair still left empty, which is the defensive fallback WfcRules.fromSampleGrid applies to sample tiles it only ever saw at an edge. Off by default for hand-authored rules, because there an empty mask is more often a typo than an intention.

WfcStatus

enum class WfcStatus

Constants: Running, Solved, Contradiction.

WfcObservation

data class WfcObservation(val x: Int, val y: Int, val tile: Int)

WfcSolver

class WfcSolver(val width: Int, val height: Int, val rules: WfcRules, seed: Long = 1L, private val noise: Double = 1e-6) : afield.ui.visualization.ToHTML

Declaration Summary
val width: Int
val height: Int
val rules: WfcRules
var status: WfcStatus
var lastObservation: WfcObservation?
fun reset() Return every cell to "any tile is still possible" and clear status and lastObservation.
fun possibilitiesAt(x: Int, y: Int, out: IntArray): Int
fun possibilityCountAt(x: Int, y: Int): Int
fun resolvedTileAt(x: Int, y: Int): Int
fun forceTile(x: Int, y: Int, tile: Int): Boolean
fun step(): WfcStatus One visualizable WFC iteration: observe one cell, then fully propagate constraints.
fun run(maxSteps: Int = Int.MAX_VALUE): WfcStatus
fun resolvedCellCount(): Int Cells collapsed to a single possibility — the WFC notion of "done" is all of them.
fun resolvedTileGridOrNull(): IntArray?
override fun toString(): String
override fun toHTML(): String The solver's own inline view, so that evaluating it in a box — or solver.h — shows the collapse: remaining possibilities per cell, entropy, what has been decided, and any contradiction, each panel captioned with the colour scale that produced it.

WfcOutcome

data class WfcOutcome(val status: WfcStatus, val attempts: Int)

What solveWithRetries ended up doing: the final status and how many attempts it took.

Declaration Summary
val solved: Boolean

WfcSymmetry

enum class WfcSymmetry(internal val rotations: Int, internal val reflect: Boolean)

Which symmetries of the square a learned pattern set should be closed under.

Constants: None, Reflections, Rotations, All.

Declaration Summary
val variantCount: Int How many variants of one pattern this generates, before deduplication.

PngTileSet

class PngTileSet(val tileWidth: Int, val tileHeight: Int, val tiles: Array<IntArray>, val sampleIds: IntArray, val sampleWidthInTiles: Int, val sampleHeightInTiles: Int)

A fixed-size tile atlas extracted from a PNG. Pixels are ARGB ints.

Declaration Summary
val tileWidth: Int
val tileHeight: Int
val tiles: Array<IntArray>
val sampleIds: IntArray
val sampleWidthInTiles: Int
val sampleHeightInTiles: Int
val tileCount: Int
fun toRules(wrapX: Boolean = false, wrapY: Boolean = false, neighborhood: WfcNeighborhood = WfcNeighborhood.VonNeumann): WfcRules
fun render(tileIds: IntArray, outWidthInTiles: Int, outHeightInTiles: Int): BufferedImage

PngOverlappingTileSet

class PngOverlappingTileSet(val patternWidth: Int, val patternHeight: Int, val patterns: Array<IntArray>, val sampleIds: IntArray, val sampleWidthInPatterns: Int, val sampleHeightInPatterns: Int, val patternWeights: DoubleArray? = null)

Overlapping WFC patterns extracted from a PNG. Each solver cell is a pattern anchor.

Declaration Summary
val patternWidth: Int
val patternHeight: Int
val patterns: Array<IntArray>
val sampleIds: IntArray
val sampleWidthInPatterns: Int
val sampleHeightInPatterns: Int
val patternWeights: DoubleArray? Explicit per-pattern weights, or null to count occurrences in sampleIds as usual.
val tileCount: Int
fun toRules(neighborhood: WfcNeighborhood = WfcNeighborhood.VonNeumann): WfcRules
fun render(patternIds: IntArray, outWidth: Int, outHeight: Int, originX: Int = 0, originY: Int = 0): BufferedImage

FloatTileSet

class FloatTileSet(val tileWidth: Int, val tileHeight: Int, val tiles: Array<FloatArray>, val sampleIds: IntArray, val sampleWidthInTiles: Int, val sampleHeightInTiles: Int)

A fixed-size tile atlas extracted from a row-major 2D FloatArray.

Declaration Summary
val tileWidth: Int
val tileHeight: Int
val tiles: Array<FloatArray>
val sampleIds: IntArray
val sampleWidthInTiles: Int
val sampleHeightInTiles: Int
val tileCount: Int
fun toRules(wrapX: Boolean = false, wrapY: Boolean = false, neighborhood: WfcNeighborhood = WfcNeighborhood.VonNeumann): WfcRules
fun render(tileIds: IntArray, outWidthInTiles: Int, outHeightInTiles: Int): FloatArray

FloatOverlappingTileSet

class FloatOverlappingTileSet(val patternWidth: Int, val patternHeight: Int, val patterns: Array<FloatArray>, val sampleIds: IntArray, val sampleWidthInPatterns: Int, val sampleHeightInPatterns: Int, val patternWeights: DoubleArray? = null)

Overlapping WFC patterns extracted from a row-major 2D FloatArray.

Declaration Summary
val patternWidth: Int
val patternHeight: Int
val patterns: Array<FloatArray>
val sampleIds: IntArray
val sampleWidthInPatterns: Int
val sampleHeightInPatterns: Int
val patternWeights: DoubleArray? Explicit per-pattern weights, or null to count occurrences in sampleIds; see PngOverlappingTileSet.patternWeights.
val tileCount: Int
fun toRules(neighborhood: WfcNeighborhood = WfcNeighborhood.VonNeumann): WfcRules
fun render(patternIds: IntArray, outWidth: Int, outHeight: Int, originX: Int = 0, originY: Int = 0): FloatArray

wfc_field.kt

The bridge from Wave Function Collapse into the things Field already displays.

wfc.kt knows about integer tile IDs, BufferedImage and FloatArray, and nothing else. The hop into Img is here, and with it Box.showImg, showImgGrid, montage, Img.colormap and the ToHTML thumbnail a box shows when you evaluate a value.

Three kinds of view:

  1. The result. PngTileSet.toImg, PngOverlappingTileSet.toImg and their Float counterparts turn a solved tile grid into an image. Pair them with Img.upscaleNearest — WFC output is pixels-as-pixels, and any filtering upscale destroys exactly the local structure you were trying to show.
  2. The process. PngTileSet.waveToImg renders a partially collapsed solver by blending each cell's still-possible tiles, and WfcSolver.possibilityCountImg exposes the possibility count per cell as a scalar field to colour-map. Sample either one inside a while (solver.step() == WfcStatus.Running) loop and you have the collapse itself as a sequence of frames, which is what to watch when rules are not behaving.
  3. The solver, inline. wfcSolverHtml is the body of WfcSolver's own ToHTML, so simply evaluating a solver in a box — or solver.h — draws its current state: uncertainty, entropy, what has been decided, where it broke. Nothing to set up, no tile set needed. See WfcSolver.possibilityCountHeatmapImg, WfcSolver.entropyHeatmapImg and WfcSolver.resolvedTileImg for the three panels on their own.

A solved cell has exactly one possibility, so waveToImg on a WfcStatus.Solved solver is the finished output: the animation's last frame and the result are the same picture.

src/jvmMain/kotlin/afield/simulation/automata/wfc_field.kt — read the source

Used in: Wave Function Collapse examples

Top-level functions

Declaration Summary
fun PngTileSet.toImg(tileIds: IntArray, outWidthInTiles: Int, outHeightInTiles: Int): Img Render a solved simple-tiled grid as an RGB Img.
fun PngOverlappingTileSet.toImg(patternIds: IntArray, outWidth: Int, outHeight: Int, originX: Int = 0, originY: Int = 0): Img Render a solved overlapping-pattern grid as an RGB Img. See PngOverlappingTileSet.render.
fun FloatTileSet.toImg(tileIds: IntArray, outWidthInTiles: Int, outHeightInTiles: Int): Img Render a solved simple-tiled grid of scalars as a 1-channel Img.
fun FloatOverlappingTileSet.toImg(patternIds: IntArray, outWidth: Int, outHeight: Int, originX: Int = 0, originY: Int = 0): Img Render a solved overlapping grid of scalars as a 1-channel Img.
fun PngTileSet.sampleToImg(): Img The source image a tile set was learned from, as an Img — useful next to the output.
fun PngTileSet.waveToImg(solver: WfcSolver): Img Render a solver mid-collapse: each cell is the average of the tiles still possible there.
fun PngOverlappingTileSet.waveToImg(solver: WfcSolver, originX: Int = 0, originY: Int = 0): Img PngTileSet.waveToImg for the overlapping model: one output pixel per solver cell, averaging the origin pixel of every pattern still possible there.
fun WfcSolver.possibilityCountGrid(): IntArray How many tiles are still possible in every cell, row-major — the solver's remaining uncertainty.
fun WfcSolver.possibilityCountImg(): Img possibilityCountGrid as a 1-channel Img, ready to colour-map.
fun WfcSolver.resolvedFraction(): Double WfcSolver.resolvedCellCount as a 0..1 fraction — a progress read-out for a step() loop.
fun WfcSolver.possibilityCountHeatmap(colormap: String = "viridis"): WfcHeatmap Uncertainty as a picture: one pixel per cell, coloured by how many tiles are still possible there.
fun WfcSolver.possibilityCountHeatmapImg(colormap: String = "viridis"): Img possibilityCountHeatmap as just the image.
fun WfcSolver.entropyGrid(): FloatArray The weighted Shannon entropy of every cell, in nats, row-major — the quantity the solver itself minimises when it picks the next cell to observe, unlike the raw count from possibilityCountGrid.
fun WfcRules.fullSetEntropy(): Double The entropy of a cell in which every tile is still possible — the top of the entropyHeatmap scale, and the value WfcSolver.entropyGrid returns for an untouched cell.
fun WfcSolver.entropyHeatmap(colormap: String = "magma"): WfcHeatmap entropyGrid colour-mapped over a fixed 0 .. WfcRules.fullSetEntropy domain — decided cells dark, wide-open cells bright, and cells whose entropy is undefined painted WFC_UNDEFINED_COLOR and excluded from the range rather than clamped to the "decided" end.
fun WfcSolver.entropyHeatmapImg(colormap: String = "magma"): Img entropyHeatmap as just the image.
fun wfcTileColor(tile: Int): Int A stable, tileset-independent display colour for tile id tile, packed 0xRRGGBB.
fun WfcSolver.resolvedTileImg(): Img What the solver has actually decided: one pixel per cell, wfcTileColor where the cell has collapsed to a single tile, WFC_UNDECIDED_COLOR where it has not, WFC_CONTRADICTION_COLOR where its possibility set is empty.
fun wfcSolverHtml(solver: WfcSolver): String The inline view of a solver — the body of WfcSolver's ToHTML, so that evaluating a solver in a box (or solver.h) shows the collapse rather than a reflective property tree.
fun WfcRules.adjacencyImg(direction: Int): Img The adjacency matrix for one direction as an Img, one pixel per (tile, neighbour) pair: row a is "what may sit at direction from tile a", column b is the candidate.
fun WfcRules.weightsImg(colormap: String = "viridis"): WfcHeatmap The tile weights as a one-row Img, viridis over a fixed 0 .. the largest weight.
fun wfcRulesHtml(rules: WfcRules): String The inline view of a rule set — the body of WfcRules' ToHTML, so evaluating one in a box shows the model rather than three nested arrays of longs.

Top-level values

Declaration Summary
val WFC_CONTRADICTION_COLOR: Int Packed 0xRRGGBB of the contradiction marker — magenta, matching PngTileSet.waveToImg.
const val WFC_UNDEFINED_COLOR: Int Packed 0xRRGGBB of the "this value has no place on the scale" marker — cyan. NaN, or infinite.
const val WFC_UNDECIDED_COLOR: Int Packed 0xRRGGBB drawn for a cell that is still undecided in WfcSolver.resolvedTileImg — dark grey.
const val WFC_ALLOWED_COLOR: Int Packed 0xRRGGBB for an allowed (a, b) pair in wfcRulesHtml — viridis teal.
const val WFC_DISALLOWED_COLOR: Int Packed 0xRRGGBB for a disallowed pair — the same near-black as an undecided cell.
const val WFC_WIDENED_COLOR: Int Packed 0xRRGGBB for a pair that is allowed but was never observed — amber.

WfcHeatmap

class WfcHeatmap internal constructor(val img: Img, val lo: Float, val hi: Float, val finiteCount: Int, val nonFiniteCount: Int, val flat: Boolean, val colormap: String)

A colour-mapped scalar-per-cell field, together with the range policy that produced it — so a caption can state the scale rather than implying one.

Declaration Summary
val img: Img
val lo: Float
val hi: Float
val finiteCount: Int
val nonFiniteCount: Int
val flat: Boolean
val colormap: String
override fun toString(): String