Skip to content

booleannetworks.kt

src/jvmMain/kotlin/afield/simulation/automata/booleannetworks.kt · 724 lines

package and imports — 71 lines, from line 1
@file:Suppress("MemberVisibilityCanBePrivate")
@file:afield.runtime.completion.CompletionCategory("Data")

package afield.simulation.automata

import afield.animation.keyframes.h
import afield.data.collections.get
import afield.data.collections.invoke
import afield.data.collections.times
import afield.data.graph.Node
import afield.data.graph.net
import afield.data.graph.rules
import afield.document.graph.minusAssign
import afield.document.graph.plusAssign
import afield.document.properties.compareTo
import afield.document.workspace.file
import afield.geometry.mesh.contains
import afield.geometry.mesh.repeated
import afield.geometry.mesh.summary
import afield.graphics.interaction.groups
import afield.graphics.render.label
import afield.graphics.render.rangeTo
import afield.lang.stacksl.h
import afield.math.autodiff.compareTo
import afield.math.autodiff.div
import afield.math.autodiff.minus
import afield.math.autodiff.plus
import afield.math.autodiff.times
import afield.math.linalg.br
import afield.math.linalg.invoke
import afield.math.linalg.remAssign
import afield.math.linalg.set
import afield.math.linalg.times
import afield.math.statistics.random
import afield.math.units.div
import afield.math.units.minus
import afield.math.units.plus
import afield.math.units.times
import afield.runtime.lifecycle.once
import afield.ui.output.h

import afield.audio.synth.*
import afield.data.collections.r
import afield.data.collections.s
import afield.document.workspace.path
import afield.graphics.render.color
import afield.graphics.render.cubicTo
import afield.graphics.render.filled
import afield.graphics.render.fline
import afield.graphics.render.lineTo
import afield.graphics.render.moveTo
import afield.graphics.render.rect
import afield.graphics.render.stroked
import afield.graphics.render.thickness
import afield.math.autodiff.maxOf
import afield.math.autodiff.minOf
import afield.math.linalg.d
import afield.math.linalg.i
import afield.math.linalg.vec
import afield.math.linalg.x
import afield.media.image.close
import afield.media.image.pad
import afield.runtime.execution.start
import afield.simulation.agents.step
import afield.geometry.path.FLine
import kotlin.math.PI
import kotlin.math.cos
import kotlin.math.sin
import kotlin.math.sqrt
import afield.ui.visualization.Table
import afield.ui.visualization.CellRole

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.

data class BoolNode

A single node in a Boolean network.

@param name Human-readable label, e.g. "A", "x1", or "CDK2". @param rule Textual description of the update rule, e.g. "A AND NOT B". @param inputs Indices of nodes whose current states are read by update. @param update Function that receives the full current state array and returns the new state for this node (true = 1, false = 0).

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

class BooleanNetwork(val nodes: List<BoolNode>)

A synchronous Boolean network with size binary-state nodes.

All nodes are updated simultaneously at each discrete time step from the current global state vector. The state space contains exactly 2^size distinct states.

Quick start:

val net = BooleanNetwork(listOf(
    BoolNode("A", "NOT B", listOf(1)) { s -> !s[1] },
    BoolNode("B", "A",     listOf(0)) { s ->  s[0] },
))

val traj = net.trajectory(booleanArrayOf(true, false), steps = 6)
traj.forEach { state -> println(net.stateString(state)) }
lines(net.drawNetworkGraph(), w = 480.0, h = 480.0, fitToSize = true)

class BooleanNetwork(val nodes: List<BoolNode>) {

val size: Int get() = nodes.size

Number of nodes.

fun stateToInt(state: BooleanArray): Int

Pack a state array to an integer (node i maps to bit i, LSB-first).

    fun stateToInt(state: BooleanArray): Int =
        state.foldIndexed(0) { i, acc, b -> if (b) acc or (1 shl i) else acc }

fun intToState(n: Int): BooleanArray

Unpack an integer to a state array (bit i maps to node i).

    fun intToState(n: Int): BooleanArray =
        BooleanArray(size) { i -> (n shr i) and 1 != 0 }

fun step(state: BooleanArray): BooleanArray

Compute the synchronous successor state: apply all update functions once.

    fun step(state: BooleanArray): BooleanArray =
        BooleanArray(size) { i -> nodes[i].update(state) }

fun stateString(state: BooleanArray): String

Return a compact binary string with the highest-indexed node first.

Example: for 3 nodes and state [true, false, true] this returns "101".

    fun stateString(state: BooleanArray): String =
        (size - 1 downTo 0).joinToString("") { i -> if (state[i]) "1" else "0" }

fun stateTable(state: BooleanArray): String

Format the current state as a readable node-by-node table.

    fun stateTable(state: BooleanArray): String = buildString {
        appendLine("Node   | State | Rule")
        appendLine("-------|-------|---------------------------")
        nodes.forEachIndexed { i, n ->
            val bit = if (state[i]) "  1  " else "  0  "
            appendLine("${n.name.padEnd(7)}| $bit | ${n.rule}")
        }
    }
}

fun BooleanNetwork.trajectory(initial: BooleanArray, steps: Int): List<BooleanArray>

Advance initial forward by exactly steps synchronous updates.

Returns a list of length steps + 1 starting with initial.

fun BooleanNetwork.trajectory(initial: BooleanArray, steps: Int): List<BooleanArray> {
    val result = mutableListOf(initial.copyOf())
    var cur = initial.copyOf()
    repeat(steps) {
        cur = step(cur)
        result += cur.copyOf()
    }
    return result
}

fun BooleanNetwork.trajectoryUntilCycle

Follow the trajectory from initial until a state repeats or maxSteps is exceeded.

The returned list starts at initial and ends with the first repeated state (inclusive), so the caller can identify the cycle by checking which earlier entry equals the last element.

fun BooleanNetwork.trajectoryUntilCycle(
    initial: BooleanArray,
    maxSteps: Int = 1024,
): List<BooleanArray> {
    val seen = LinkedHashMap<String, Int>()
    val traj = mutableListOf(initial.copyOf())
    seen[stateString(initial)] = 0
    var cur = initial.copyOf()
    while (traj.size <= maxSteps) {
        cur = step(cur)
        val key = stateString(cur)
        traj += cur.copyOf()
        if (key in seen) break
        seen[key] = traj.size - 1
    }
    return traj
}

data class Attractor

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

@param period 1 = fixed point, > 1 = limit cycle. @param cycle Ordered sequence of states forming the attractor. @param basin Set of all state integers that eventually converge to it.

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

fun Attractor.repeatedCycle(steps: Int): List<BooleanArray>

Repeat the attractor cycle to exactly steps states, preserving the original phase order.

fun Attractor.repeatedCycle(steps: Int): List<BooleanArray> {
    require(steps >= 0) { "steps must be >= 0" }
    if (steps == 0 || cycle.isEmpty()) return emptyList()
    return List(steps) { cycle[it % cycle.size].copyOf() }
}

fun BooleanNetwork.findAttractors(): List<Attractor>

Enumerate every attractor and its basin of attraction by exhaustive traversal of the state-transition graph.

Complexity is O(2^size), so this is only practical for small networks. Returns an empty list for size > 20.

body — 50 lines, from line 231
fun BooleanNetwork.findAttractors(): List<Attractor> {
    if (size > 20) return emptyList()
    val total = 1 shl size
    val attrOf = IntArray(total) { -1 }
    val attractorCycles = mutableListOf<List<BooleanArray>>()

    for (start in 0 until total) {
        if (attrOf[start] != -1) continue

        val path = mutableListOf<Int>()
        val indexInPath = LinkedHashMap<Int, Int>()
        var cur = start

        while (cur !in indexInPath && attrOf[cur] == -1) {
            indexInPath[cur] = path.size
            path += cur
            cur = stateToInt(step(intToState(cur)))
        }

        val attractorIndex = if (attrOf[cur] != -1) {
            attrOf[cur]
        } else {
            val newIndex = attractorCycles.size
            val cycleStart = indexInPath[cur]!!
            attractorCycles += path.subList(cycleStart, path.size).map { intToState(it) }
            newIndex
        }

        path.forEach { attrOf[it] = attractorIndex }
    }

    val basins = Array(attractorCycles.size) { mutableSetOf<Int>() }
    for (s in 0 until total) basins[attrOf[s]] += s

    return attractorCycles.mapIndexed { i, cycle ->
        Attractor(period = cycle.size, cycle = cycle, basin = basins[i])
    }
}

private fun FLine.circle(cx: Double, cy: Double, r: Double, segments: Int = 24) {
    for (j in 0..segments) {
        val a = 2.0 * PI * j / segments
        val nx = cx + r * cos(a)
        val ny = cy + r * sin(a)
        if (j == 0) moveTo(nx, ny) else lineTo(nx, ny)
    }
    close()
}

private fun List<FLine>.withoutEmpty(): List<FLine> = filter { it.nodes.isNotEmpty() }

fun BooleanNetwork.drawUnrolled

Render the time evolution of a Boolean network as a spacetime grid.

Rows are time steps (top row t = 0); columns are nodes.

fun BooleanNetwork.drawUnrolled(
    trajectory: List<BooleanArray>,
    cellSize: Double = 24.0,
): List<FLine> {
    val steps = trajectory.size
    val on = fline { filled = true; stroked = false; color = vec(0.94, 0.93, 0.86, 1.0) }
    val off = fline { filled = true; stroked = false; color = vec(0.14, 0.14, 0.18, 1.0) }
    val grid = fline {
        stroked = true
        filled = false
        thickness = 0.5
        color = vec(0.5, 0.5, 0.5, 0.25)
    }

    for (t in 0 until steps) {
        for (i in 0 until size) {
            val x = i * cellSize
            val y = t * cellSize
            (if (trajectory[t][i]) on else off).rect(x, y, cellSize, cellSize)
        }
    }
    for (t in 0..steps) {
        grid.moveTo(0.0, t * cellSize)
        grid.lineTo(size * cellSize, t * cellSize)
    }
    for (i in 0..size) {
        grid.moveTo(i * cellSize, 0.0)
        grid.lineTo(i * cellSize, steps * cellSize)
    }
    return listOf(off, on, grid).withoutEmpty()
}

fun BooleanNetwork.drawNetworkGraph

Render the wiring diagram of the network as a directed graph.

Nodes are placed on a circle; edges show which nodes feed each update function. Self-loops are drawn as small arcs offset outward.

body — 63 lines, from line 325
fun BooleanNetwork.drawNetworkGraph(
    radius: Double = 120.0,
    nodeR: Double = 14.0,
): List<FLine> {
    val pad = nodeR * 2 + radius
    val pos = Array(size) { i ->
        val angle = 2.0 * PI * i / size - PI / 2.0
        pad + radius * cos(angle) to pad + radius * sin(angle)
    }

    val edgeLines = fline {
        stroked = true
        filled = false
        thickness = 1.2
        color = vec(0.75, 0.75, 0.75, 0.60)
    }
    val selfLoops = fline {
        stroked = true
        filled = false
        thickness = 1.2
        color = vec(1.0, 0.70, 0.30, 0.75)
    }
    val nodeFill = fline {
        filled = true
        stroked = true
        thickness = 1.5
        color = vec(0.28, 0.55, 1.0, 0.88)
    }

    nodes.forEachIndexed { dst, node ->
        node.inputs.forEach { src ->
            val (fx, fy) = pos[src]
            val (tx, ty) = pos[dst]
            if (src == dst) {
                val a = 2.0 * PI * src / size - PI / 2.0
                val ox = pad + (radius + nodeR * 2.5) * cos(a)
                val oy = pad + (radius + nodeR * 2.5) * sin(a)
                selfLoops.moveTo(fx + nodeR * cos(a - 0.4), fy + nodeR * sin(a - 0.4))
                selfLoops.cubicTo(
                    ox + nodeR * cos(a - 0.6), oy + nodeR * sin(a - 0.6),
                    ox + nodeR * cos(a + 0.6), oy + nodeR * sin(a + 0.6),
                    fx + nodeR * cos(a + 0.4), fy + nodeR * sin(a + 0.4),
                )
            } else {
                val dx = tx - fx
                val dy = ty - fy
                val d = sqrt(dx * dx + dy * dy).coerceAtLeast(1e-9)
                edgeLines.moveTo(
                    fx + (nodeR + 1.5) / d * dx,
                    fy + (nodeR + 1.5) / d * dy,
                )
                edgeLines.lineTo(
                    tx - (nodeR + 3.0) / d * dx,
                    ty - (nodeR + 3.0) / d * dy,
                )
            }
        }
    }

    pos.forEach { (cx, cy) -> nodeFill.circle(cx, cy, nodeR) }

    return listOf(edgeLines, selfLoops, nodeFill).withoutEmpty()
}

fun BooleanNetwork.drawStateTransitionGraph(radius: Double = 200.0): List<FLine>

Render the full state-transition graph for small networks (size <= 6).

Each state is laid out on a circle. Arrows show the synchronous successor mapping; attractor states are highlighted in orange.

body — 60 lines, from line 395
fun BooleanNetwork.drawStateTransitionGraph(radius: Double = 200.0): List<FLine> {
    if (size > 6) return emptyList()
    val total = 1 shl size
    val pad = radius + 40.0
    val nodeR = if (total <= 8) 12.0 else 7.0

    val angles = DoubleArray(total) { i -> 2.0 * PI * i / total - PI / 2.0 }
    val pos = Array(total) { i ->
        pad + radius * cos(angles[i]) to pad + radius * sin(angles[i])
    }

    val attractorStateSet = mutableSetOf<Int>()
    findAttractors().forEach { attr -> attr.cycle.forEach { attractorStateSet += stateToInt(it) } }

    val arrows = fline {
        stroked = true
        filled = false
        thickness = 1.0
        color = vec(0.68, 0.68, 0.68, 0.55)
    }
    val attrNodes = fline {
        filled = true
        stroked = true
        thickness = 1.5
        color = vec(1.0, 0.52, 0.18, 0.95)
    }
    val transNodes = fline {
        filled = true
        stroked = false
        color = vec(0.28, 0.48, 0.82, 0.70)
    }

    for (s in 0 until total) {
        val ns = stateToInt(step(intToState(s)))
        val (fx, fy) = pos[s]
        val (tx, ty) = pos[ns]
        if (s == ns) {
            val a = angles[s]
            arrows.moveTo(fx + nodeR * cos(a - 0.35), fy + nodeR * sin(a - 0.35))
            arrows.cubicTo(
                fx + nodeR * 2.6 * cos(a - 0.55), fy + nodeR * 2.6 * sin(a - 0.55),
                fx + nodeR * 2.6 * cos(a + 0.55), fy + nodeR * 2.6 * sin(a + 0.55),
                fx + nodeR * cos(a + 0.35), fy + nodeR * sin(a + 0.35),
            )
        } else {
            val dx = tx - fx
            val dy = ty - fy
            val d = sqrt(dx * dx + dy * dy).coerceAtLeast(1e-9)
            arrows.moveTo(fx + (nodeR + 1.0) / d * dx, fy + (nodeR + 1.0) / d * dy)
            arrows.lineTo(tx - (nodeR + 3.0) / d * dx, ty - (nodeR + 3.0) / d * dy)
        }
    }

    for (s in 0 until total) {
        val (cx, cy) = pos[s]
        (if (s in attractorStateSet) attrNodes else transNodes).circle(cx, cy, nodeR)
    }

    return listOf(arrows, transNodes, attrNodes).withoutEmpty()
}

fun BooleanNetwork.drawAttractorBasins

Render each attractor's basin as a stack of transient spacetime sketches.

body — 56 lines, from line 459
fun BooleanNetwork.drawAttractorBasins(
    attractors: List<Attractor>,
    samplesPerAttractor: Int = 6,
    cellSize: Double = 16.0,
): List<FLine> {
    val on = fline { filled = true; stroked = false; color = vec(0.94, 0.93, 0.86, 1.0) }
    val off = fline { filled = true; stroked = false; color = vec(0.14, 0.14, 0.18, 1.0) }
    val sep = fline {
        stroked = true
        filled = false
        thickness = 1.0
        color = vec(1.0, 0.6, 0.2, 0.6)
    }
    val grid = fline {
        stroked = true
        filled = false
        thickness = 0.4
        color = vec(0.5, 0.5, 0.5, 0.20)
    }

    var xOffset = 0.0

    attractors.forEach { attr ->
        val basinList = attr.basin.toList().shuffled().take(samplesPerAttractor)
        var maxRows = 0

        basinList.forEach { startInt ->
            val traj = trajectoryUntilCycle(intToState(startInt))
            maxRows = maxOf(maxRows, traj.size)
        }

        basinList.forEachIndexed { col, startInt ->
            val traj = trajectoryUntilCycle(intToState(startInt))
            val x0 = xOffset + col * (size + 1) * cellSize
            for (t in traj.indices) {
                for (i in 0 until size) {
                    val x = x0 + i * cellSize
                    val y = t * cellSize
                    (if (traj[t][i]) on else off).rect(x, y, cellSize, cellSize)
                }
            }
            for (t in 0..traj.size) {
                grid.moveTo(x0, t * cellSize)
                grid.lineTo(x0 + size * cellSize, t * cellSize)
            }
        }

        val sepX = xOffset + basinList.size * (size + 1) * cellSize - cellSize * 0.5
        sep.moveTo(sepX, 0.0)
        sep.lineTo(sepX, maxRows * cellSize)

        xOffset += (basinList.size * (size + 1) + 2) * cellSize
    }

    return listOf(off, on, grid, sep).withoutEmpty()
}

fun drawRhythmGrid

Render a step-by-step rhythm grid from a list of Boolean lane states.

Each row is a lane and each column is a step. Active cells are colored by lane, dark cells are inactive, and every accentEveryth step gets a heavier separator to suggest bars / beat groups.

body — 73 lines, from line 523
fun drawRhythmGrid(
    steps: List<BooleanArray>,
    cellW: Double = 24.0,
    cellH: Double = 18.0,
    stepGap: Double = 5.0,
    laneGap: Double = 5.0,
    accentEvery: Int = 4,
): List<FLine> {
    if (steps.isEmpty()) return emptyList()
    val lanes = steps.maxOfOrNull { it.size } ?: 0
    if (lanes <= 0) return emptyList()

    val off = fline {
        filled = true
        stroked = false
        color = vec(0.14, 0.14, 0.18, 0.95)
    }
    val grid = fline {
        filled = false
        stroked = true
        thickness = 0.6
        color = vec(1.0, 1.0, 1.0, 0.08)
    }
    val accents = fline {
        filled = false
        stroked = true
        thickness = 1.2
        color = vec(1.0, 0.62, 0.24, 0.34)
    }
    val fills = listOf(
        vec(0.34, 0.66, 1.0, 0.96),
        vec(1.0, 0.57, 0.22, 0.96),
        vec(0.48, 1.0, 0.62, 0.96),
        vec(0.88, 0.44, 1.0, 0.96),
        vec(1.0, 0.88, 0.28, 0.96),
        vec(0.44, 0.92, 0.92, 0.96),
    ).map { color ->
        fline {
            filled = true
            stroked = false
            this.color = color
        }
    }

    val inset = minOf(cellW, cellH) * 0.14
    val totalHeight = lanes * cellH + (lanes - 1) * laneGap

    steps.forEachIndexed { stepIndex, state ->
        val x0 = stepIndex * (cellW + stepGap)
        if (accentEvery > 0 && stepIndex % accentEvery == 0) {
            val accentX = x0 - stepGap * 0.5
            accents.moveTo(accentX, -laneGap * 0.5)
            accents.lineTo(accentX, totalHeight + laneGap * 0.5)
        }

        for (lane in 0 until lanes) {
            val y0 = lane * (cellH + laneGap)
            off.rect(x0, y0, cellW, cellH)
            grid.rect(x0, y0, cellW, cellH)
            if (lane < state.size && state[lane]) {
                fills[lane % fills.size].rect(x0 + inset, y0 + inset, cellW - 2 * inset, cellH - 2 * inset)
            }
        }
    }

    if (accentEvery > 0) {
        val endX = steps.size * (cellW + stepGap) - stepGap * 0.5
        accents.moveTo(endX, -laneGap * 0.5)
        accents.lineTo(endX, totalHeight + laneGap * 0.5)
    }

    return (listOf(off, grid, accents) + fills).withoutEmpty()
}

fun BooleanNetwork.notateHtml(): String

Generate an HTML table of nodes, input wiring, and update rules.

fun BooleanNetwork.notateHtml(): String =
    Table(listOf("node", "inputs", "update rule"))
        .rows(
            nodes.map { n ->
                listOf(
                    n.name,
                    n.inputs.joinToString(", ") { nodes[it].name }.ifEmpty { "—" },
                    n.rule,
                )
            }
        )
        // A node with no inputs is a constant, which is the thing worth spotting in the wiring.
        .roleBy("inputs") { if (it == "—") CellRole.WARN else null }
        .toHTML()

fun BooleanNetwork.notateAttractorsHtml(attractors: List<Attractor>): String

Generate an HTML summary of attractor type, cycle states, and basin sizes.

fun BooleanNetwork.notateAttractorsHtml(attractors: List<Attractor>): String = buildString {
    appendLine("<div style='font-family:monospace;font-size:13px;color:#ccc;background:#16181f;padding:8px'>")
    if (attractors.isEmpty()) {
        appendLine("<p style='color:#f88'>No attractors found (network size > 20?).</p>")
    } else {
        attractors.forEachIndexed { idx, attr ->
            val typeLabel = if (attr.period == 1) "Fixed point" else "Limit cycle — period ${attr.period}"
            appendLine(
                "<div style='margin:6px 0;padding:6px 12px;background:#1e2030;" +
                    "border-left:3px solid #f80;border-radius:3px'>"
            )
            appendLine(
                "<b style='color:#f80'>Attractor ${idx + 1}</b> — $typeLabel " +
                    "— basin: <b style='color:#7ec'>${attr.basin.size}</b> / ${1 shl size} states<br>"
            )
            attr.cycle.forEachIndexed { t, state ->
                val bits = stateString(state)
                val labels = nodes.mapIndexed { i, n ->
                    "${n.name}=<span style='color:#cfc'>${if (state[i]) "1" else "0"}</span>"
                }.joinToString("  ")
                appendLine(
                    "<span style='color:#8cf'>t+$t:</span>&nbsp;" +
                        "<span style='letter-spacing:3px;color:#cfc'>$bits</span>&nbsp;&nbsp;" +
                        "<span style='color:#888;font-size:11px'>$labels</span><br>"
                )
            }
            if (attr.period > 1) {
                appendLine("<span style='color:#555;font-size:11px'>↩ returns to t+0</span>")
            }
            appendLine("</div>")
        }
    }
    appendLine("</div>")
}

fun BooleanNetwork.printAttractors()

Print a plain-text attractor summary to stdout.

fun BooleanNetwork.printAttractors() {
    val attractors = findAttractors()
    println("${attractors.size} attractor(s) for ${size}-node network:")
    attractors.forEachIndexed { idx, attr ->
        val type = if (attr.period == 1) "fixed-point" else "cycle(${attr.period})"
        println("  [${idx + 1}] $type  basin=${attr.basin.size}")
        attr.cycle.forEachIndexed { t, state ->
            println("      t+$t  ${stateString(state)}  ${stateTable(state).lines().drop(2).joinToString("  ").trimEnd()}")
        }
    }
}

fun toggleNetwork(): BooleanNetwork

A 2-node network with one period-4 cycle through all four states.

A(t+1) = NOT B(t) B(t+1) = A(t)

fun toggleNetwork(): BooleanNetwork = BooleanNetwork(
    listOf(
        BoolNode("A", "NOT B", listOf(1)) { s -> !s[1] },
        BoolNode("B", "A", listOf(0)) { s -> s[0] },
    )
)

fun threeNodeNetwork(): BooleanNetwork

A 3-node network with a single period-5 limit cycle.

This is a compact example for spacetime unrolling and basin sketches.

fun threeNodeNetwork(): BooleanNetwork = BooleanNetwork(
    listOf(
        BoolNode("A", "B AND C", listOf(1, 2)) { s -> s[1] && s[2] },
        BoolNode("B", "A OR C", listOf(0, 2)) { s -> s[0] || s[2] },
        BoolNode("C", "NOT A", listOf(0)) { s -> !s[0] },
    )
)

fun segmentPolarityNetwork(): BooleanNetwork

Simplified 4-node segment-polarity network inspired by Drosophila.

fun segmentPolarityNetwork(): BooleanNetwork = BooleanNetwork(
    listOf(
        BoolNode("en", "wg", listOf(1)) { s -> s[1] },
        BoolNode("wg", "en", listOf(0)) { s -> s[0] },
        BoolNode("ptc", "NOT ci", listOf(3)) { s -> !s[3] },
        BoolNode("ci", "ptc AND NOT en", listOf(2, 0)) { s -> s[2] && !s[0] },
    )
)

fun kauffmanNetwork(n: Int = 5, k: Int = 2, seed: Long = 42L): BooleanNetwork

A random N-K Kauffman network.

Each node has exactly k randomly chosen inputs and a random Boolean lookup table over those inputs.

fun kauffmanNetwork(n: Int = 5, k: Int = 2, seed: Long = 42L): BooleanNetwork {
    val rng = java.util.Random(seed)
    val nodeList = (0 until n).map { i ->
        val inputs = (0 until k).map { rng.nextInt(n) }
        val table = BooleanArray(1 shl k) { rng.nextBoolean() }
        BoolNode(
            name = "x$i",
            rule = "random(${inputs.joinToString(",")})",
            inputs = inputs,
        ) { s ->
            val idx = inputs.foldIndexed(0) { bit, acc, src ->
                if (s[src]) acc or (1 shl bit) else acc
            }
            table[idx]
        }
    }
    return BooleanNetwork(nodeList)
}