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
| Declaration | Summary |
|---|---|
val success: Boolean |
|
val message: String |
|
val hits: List<SemanticSearchHit> |
|
val status: SemanticRetrievalStatus |
SemanticSearchHit
| 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
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
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
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
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.
dynamicGraphImgtakes an explicitwindow, andBox.showPlaneGridtakes 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.dynamicGraphWindowcomputes a window once, from a reference state, for you to keep;dynamicGraphPlaneWindowconverts it for the vector path. - Edge weight is read against a
maxWeightnamed 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.
dynamicGraphImgreports how many points fell outside the window inDynamicGraphRaster.clipped, anddynamicGraphOutsideWindowanswers 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
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.
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
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
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
| Declaration | Summary |
|---|---|
fun step(dt: Float, state: DynamicPointState, edges: DynamicEdgeList, spatialHash: Vec3SpatialHash) |
Vec3BreathingAnisotropicKnnSystem
| 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
| 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
| 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
| 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
| 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
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
| Declaration | Summary |
|---|---|
val velocity: MutableList<Vec3> |
|
val resource: MutableList<Double> |
|
val alive: MutableList<Boolean> |
|
fun ensureSize(n: Int) |
DynamicPointProcess
| Declaration | Summary |
|---|---|
fun step(dt: Float, state: DynamicPointState, kinetics: DynamicPointKinetics, edges: DynamicEdgeList, spatialHash: Vec3SpatialHash) |
SpringMotionProcess
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
| 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
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
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
| 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
| 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
| 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
| Declaration | Summary |
|---|---|
var gain: Double |
|
var degreeFloor: Int |
|
override fun step(dt: Float, state: DynamicPointState, kinetics: DynamicPointKinetics, edges: DynamicEdgeList, spatialHash: Vec3SpatialHash) |
PruneIsolatedPointsProcess
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
One named, readable — and possibly writable — edge out of a node.
| Declaration | Summary |
|---|---|
val name: String |
|
val getter: () -> Any? |
|
val setter: ((Any?) -> Unit)? |
GraphWalkOptions
Traversal policy.
GraphPathRef
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
The result of a query: an ordered, immutable collection of live refs.
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
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
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
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
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 aFloat, not anAny?you have to cast. No reified type argument, noas. - Writability is checked too, via overload resolution: a
varresolves to theKMutableProperty0overload and yields a writable lens, avalresolves to theKProperty0one and yields a read-only lens whosesetdoes 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
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
divwants. A member operator always beats an extension, so everyFloat / Float,Vec3 / Float,Signal / Signalkeeps its owndiv. Between two extensions the more specific receiver wins, sokotlin.io.path'sPath / "sub"still builds aPath. 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.divand its siblings), so the typo is caught at the/with a message pointing atgraphPaths. divappears 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
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
SpreadsheetStateBuilder
class SpreadsheetStateBuilder internal constructor(state: SpreadsheetStoredState? = null)
SpreadsheetCellSnapshot
| Declaration | Summary |
|---|---|
val row: Int |
|
val col: Int |
|
val input: String |
|
val type: String |
|
val display: String |
|
val errorDetail: String? |
|
val htmlDisplay: String? |
SpreadsheetSnapshot
SpreadsheetCompletionItem
SpreadsheetEvalHelpers
SpreadsheetScalar
class SpreadsheetScalar internal constructor(val raw: Any?) : Comparable<Any?>
SpreadsheetBoxBinding
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
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
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
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
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
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
| Declaration | Summary |
|---|---|
fun onFrame(frame: TuioReceiver.Frame) |
TuioReceiver
Minimal TUIO/UDP receiver for: - /tuio/2Dcur (cursors) - /tuio/2Dobj (fiducials/objects)
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
Small TCP server for newline-delimited JSON streams.
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
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
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
The byte order a dtype declares, from the <, >, = or | character in its descriptor.
Constants: LITTLE_ENDIAN, BIG_ENDIAN, NATIVE, NOT_APPLICABLE.
NpyKind
The family a dtype belongs to — what decides which conversion will accept it.
Constants: BOOL, SIGNED_INT, UNSIGNED_INT, FLOAT, COMPLEX, OTHER.
NpyDType
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
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
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
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
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
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
The metric the …KMedoids family assigns and re-medoids with.
Constants: EUCLIDEAN, MANHATTAN.
KMedoidsOptions
How the …KMedoids functions run.
KMediansOptions
How the …KMedians functions run.
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
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
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
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
How a stack of ClusterLayers is laid out and drawn.
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
SmileMdsDistance
Distance functions supplied by Field's general-purpose smileMds wrapper.
Constants: COSINE, EUCLIDEAN.
SmileMdsProjection
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
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
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
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
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
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
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
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
BoolNode
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
Top-level values
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.
WfcNeighbourhood
typealias WfcNeighbourhood = WfcNeighborhood
WfcRules
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
Constants: Running, Solved, Contradiction.
WfcObservation
data class WfcObservation(val x: Int, val y: Int, val tile: Int)
WfcSolver
| 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
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
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
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
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:
- The result.
PngTileSet.toImg,PngOverlappingTileSet.toImgand theirFloatcounterparts turn a solved tile grid into an image. Pair them withImg.upscaleNearest— WFC output is pixels-as-pixels, and any filtering upscale destroys exactly the local structure you were trying to show. - The process.
PngTileSet.waveToImgrenders a partially collapsed solver by blending each cell's still-possible tiles, andWfcSolver.possibilityCountImgexposes the possibility count per cell as a scalar field to colour-map. Sample either one inside awhile (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. - The solver, inline.
wfcSolverHtmlis the body ofWfcSolver's ownToHTML, so simply evaluating a solver in a box — orsolver.h— draws its current state: uncertainty, entropy, what has been decided, where it broke. Nothing to set up, no tile set needed. SeeWfcSolver.possibilityCountHeatmapImg,WfcSolver.entropyHeatmapImgandWfcSolver.resolvedTileImgfor 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
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 |