Skip to content

graph_connectivity.kt

src/jvmMain/kotlin/afield/data/graph/graph_connectivity.kt · 2205 lines · cited by Projecting a distance matrix

Self-contained graph / computational-geometry library over 2D and 3D point sets (Vec2/Vec3). Every algorithm has a vec2* and (where meaningful) a vec3* entry point returning a PointGraph; see each function's own KDoc for its definition and complexity.

Proximity / neighbourhood families: KNN (vec2KnnGraph), Orthant (vec2OrthantGraph), Relative Neighborhood (vec2RelativeNeighborhoodGraph), Gabriel (vec2GabrielGraph), Beta-Skeleton (vec2BetaSkeleton), Nearest-Neighbor (vec2NearestNeighborGraph), Minimum Spanning Tree (vec2MinimumSpanningTree), Maximum-Diameter/radius (vec2MaximumDiameterGraph), Maximum-Cut (vec2MaximumCutGraph) and Complete (vec2CompleteGraph) graphs.

JTS-backed (2D only): full Delaunay triangulation (vec2DelaunayGraph, vec2DelaunayTriangles), Urquhart (vec2UrquhartGraph, a real Urquhart graph built by removing the longest edge of each Delaunay triangle; there is no 3D one, because Field has no 3D Delaunay of a point set), Voronoi diagram/dual (vec2VoronoiDiagram, vec2VoronoiDualGraph) and convex hull (vec2ConvexHull, vec2ConvexHullGraph, vec2HullProjectionGraph).

Traversal / analysis: BFS/DFS/A* path search, GraphEdgeVisitState greedy walks and graph-diameter path decomposition (see the search/path functions below), plus FLine rendering integration in fline_graph_connectivity.kt.

Custom distance metrics: pass GraphConnectivityOptions.customDistanceMetric. It is honoured by the distance-driven algorithms (KNN, MST, RNG, Gabriel, Beta-Skeleton, Delaunay/Urquhart edge weights, etc.). This includes vec2OrthantGraph/vec3OrthantGraph, whose nearest-neighbour ranking within each orthant honours customDistanceMetric when supplied (Euclidean otherwise).

Smoke tests: afield.examples.stdlib.GraphConnectivitySmokeTest (its runTests() drives 35 checks, covering path search, edge-visit-state and FLine rendering as well as the core graphs). Live rendered examples: public/docs/graph_connectivity_examples.md.

package and imports — 62 lines, from line 31
@file:Suppress("MemberVisibilityCanBePrivate")
@file:afield.runtime.completion.CompletionCategory("Graphics")

package afield.data.graph

import afield.data.collections.get
import afield.data.collections.s
import afield.data.collections.times
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.spatial.closest
import afield.graphics.render.circle
import afield.graphics.render.rangeTo
import afield.graphics.shader.intersection
import afield.graphics.shader.vertex
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.functions.Functions
import afield.math.linalg.d
import afield.math.linalg.remAssign
import afield.math.linalg.times
import afield.math.statistics.noise
import afield.math.units.div
import afield.math.units.minus
import afield.math.units.plus
import afield.math.units.times
import afield.runtime.execution.cell
import afield.runtime.execution.end

import afield.audio.synth.*
import afield.data.collections.invoke
import afield.document.workspace.path
import afield.geometry.mesh.centroid
import afield.geometry.mesh.convexHull
import afield.graphics.vulkan.buffers.add
import afield.math.autodiff.maxOf
import afield.math.autodiff.minOf
import afield.math.linalg.Vec2
import afield.math.linalg.Vec3
import afield.math.linalg.ab
import afield.math.linalg.i
import afield.math.linalg.invoke
import afield.math.linalg.vec
import afield.math.linalg.x
import afield.runtime.execution.start
import org.locationtech.jts.geom.Coordinate
import org.locationtech.jts.geom.GeometryFactory
import org.locationtech.jts.triangulate.DelaunayTriangulationBuilder
import org.locationtech.jts.triangulate.VoronoiDiagramBuilder
import java.util.PriorityQueue
import kotlin.collections.ArrayDeque
import kotlin.collections.set
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
import kotlin.math.sqrt

data class GraphEdge(val i: Int, val j: Int, val weight: Double)

Represents an edge between two points in a graph.

@param i Index of the first point @param j Index of the second point @param weight Weight/distance of the edge

override fun equals(other: Any?): Boolean

    override fun equals(other: Any?): Boolean {
        if (this === other) return true
        if (other !is GraphEdge) return false
        return (i == other.i && j == other.j) || (i == other.j && j == other.i)
    }

override fun hashCode(): Int

    override fun hashCode(): Int {
        val min = min(i, j)
        val max = max(i, j)
        return 31 * min + max
    }
}

data class PointGraph(val edges: List<GraphEdge>, val pointCount: Int)

Represents a graph computed from a set of points.

@param edges List of edges in the graph @param pointCount Number of points in the graph

data class PointGraph(val edges: List<GraphEdge>, val pointCount: Int) {

fun neighbors(pointIndex: Int): List<Int>

Get all neighbors of a point.

    fun neighbors(pointIndex: Int): List<Int> {
        return edges.filter { it.i == pointIndex || it.j == pointIndex }
            .map { if (it.i == pointIndex) it.j else it.i }
    }

fun neighbors1(pointIndex: Int): List<Int>

    fun neighbors1(pointIndex: Int): List<Int> {
        return edges.filter { it.i == pointIndex  }
            .map { it.j }
    }

fun degree(pointIndex: Int): Int = neighbors(pointIndex).size

Get the degree (number of neighbors) of a point.

data class GraphConnectivityOptions

Options for graph connectivity algorithms.

@param allowDuplicatePoints Whether to allow duplicate points @param customDistanceMetric Custom distance function taking two point indices and the point list. If null, uses standard Euclidean distance.

data class GraphConnectivityOptions(
    val allowDuplicatePoints: Boolean = true,
    val customDistanceMetric: ((Int, Int, List<*>) -> Double)? = null
)

data class GraphPath

Represents a path through a PointGraph.

@param nodes Ordered sequence of node indices in the path @param edges Ordered sequence of edges traversed by the path @param totalWeight Sum of traversed edge weights

data class GraphPath(
    val nodes: List<Int>,
    val edges: List<GraphEdge>,
    val totalWeight: Double
) {

init

    init {
        require(nodes.isNotEmpty()) { "GraphPath.nodes must not be empty" }
        require(nodes.size == edges.size + 1) {
            "GraphPath requires nodes.size == edges.size + 1, found ${nodes.size} and ${edges.size}"
        }
    }

val start: Int get() = nodes.first()

val end: Int get() = nodes.last()

data class GraphEdgeVisitState

Tracks visited and unvisited edges for path cover generation.

Edges are always treated as undirected according to GraphEdge.equals.

data class GraphEdgeVisitState(
    val pointCount: Int,
    val visitedEdges: Set<GraphEdge>,
    val unvisitedEdges: Set<GraphEdge>
) {

init

    init {
        require(pointCount >= 0) { "GraphEdgeVisitState.pointCount must be >= 0, found $pointCount" }
        require((visitedEdges intersect unvisitedEdges).isEmpty()) {
            "GraphEdgeVisitState: visited and unvisited sets must be disjoint"
        }

        val allEdges = visitedEdges + unvisitedEdges
        allEdges.forEachIndexed { idx, edge ->
            require(edge.i in 0 until pointCount) {
                "GraphEdgeVisitState: edge[$idx].i=${edge.i} is out of range for pointCount=$pointCount"
            }
            require(edge.j in 0 until pointCount) {
                "GraphEdgeVisitState: edge[$idx].j=${edge.j} is out of range for pointCount=$pointCount"
            }
            require(edge.weight.isFinite()) {
                "GraphEdgeVisitState: edge[$idx] weight must be finite, found ${edge.weight}"
            }
        }
    }

val allEdges: Set<GraphEdge>

    val allEdges: Set<GraphEdge>
        get() = visitedEdges + unvisitedEdges

fun isVisited(edge: GraphEdge): Boolean = edge in visitedEdges

fun isUnvisited(edge: GraphEdge): Boolean = edge in unvisitedEdges

fun markVisited(edges: Iterable<GraphEdge>): GraphEdgeVisitState

    fun markVisited(edges: Iterable<GraphEdge>): GraphEdgeVisitState {
        val moved = edges.filter { it in unvisitedEdges }.toSet()
        if (moved.isEmpty()) return this
        return copy(
            visitedEdges = visitedEdges + moved,
            unvisitedEdges = unvisitedEdges - moved
        )
    }

fun markVisited(edge: GraphEdge): GraphEdgeVisitState = markVisited(listOf(edge))

fun markUnvisited(edges: Iterable<GraphEdge>): GraphEdgeVisitState

    fun markUnvisited(edges: Iterable<GraphEdge>): GraphEdgeVisitState {
        val moved = edges.filter { it in visitedEdges }.toSet()
        if (moved.isEmpty()) return this
        return copy(
            visitedEdges = visitedEdges - moved,
            unvisitedEdges = unvisitedEdges + moved
        )
    }

fun markUnvisited(edge: GraphEdge): GraphEdgeVisitState = markUnvisited(listOf(edge))

fun visitedGraph(): PointGraph = PointGraph(visitedEdges.toList(), pointCount)

fun unvisitedGraph(): PointGraph = PointGraph(unvisitedEdges.toList(), pointCount)

data class GraphPathSetResult

Result containing a list of paths and the updated edge-visit state.

data class GraphPathSetResult(
    val paths: List<GraphPath>,
    val edgeState: GraphEdgeVisitState
)

fun pointGraphEdgeVisitState

Build a GraphEdgeVisitState from a PointGraph.

fun pointGraphEdgeVisitState(
    graph: PointGraph,
    initiallyVisited: Collection<GraphEdge> = emptyList()
): GraphEdgeVisitState {
    validatePointGraph(graph, "pointGraphEdgeVisitState")
    val allEdges = graph.edges.toSet()
    val visited = initiallyVisited.filter { it in allEdges }.toSet()
    return GraphEdgeVisitState(
        pointCount = graph.pointCount,
        visitedEdges = visited,
        unvisitedEdges = allEdges - visited
    )
}

fun PointGraph.edgeVisitState

Convenience extension for pointGraphEdgeVisitState.

fun PointGraph.edgeVisitState(
    initiallyVisited: Collection<GraphEdge> = emptyList()
): GraphEdgeVisitState = pointGraphEdgeVisitState(this, initiallyVisited)

fun graphBfsPath

Find an unweighted shortest path from start to goal using breadth-first search.

@param graph Graph to traverse @param start Start node index @param goal Goal node index @param allowedEdges Optional edge subset constraint @return Path if reachable, else null

fun graphBfsPath(
    graph: PointGraph,
    start: Int,
    goal: Int,
    allowedEdges: Set<GraphEdge>? = null
): GraphPath? {
    validatePointGraph(graph, "graphBfsPath")
    validatePointIndex(start, graph.pointCount, "graphBfsPath", "start")
    validatePointIndex(goal, graph.pointCount, "graphBfsPath", "goal")

    if (start == goal) return GraphPath(listOf(start), emptyList(), 0.0)

    val filteredEdges = filterGraphEdges(graph, allowedEdges)
    val adjacency = buildAdjacency(graph.pointCount, filteredEdges)
    val previous = IntArray(graph.pointCount) { -1 }
    val discovered = BooleanArray(graph.pointCount)
    val queue = ArrayDeque<Int>()

    discovered[start] = true
    queue.addLast(start)

    while (queue.isNotEmpty()) {
        val current = queue.removeFirst()
        if (current == goal) break

        for (entry in adjacency[current]) {
            if (discovered[entry.to]) continue
            discovered[entry.to] = true
            previous[entry.to] = current
            queue.addLast(entry.to)
        }
    }

    if (!discovered[goal]) return null
    val nodes = reconstructPathNodes(previous, start, goal) ?: return null
    return pathFromNodes(nodes, buildEdgeLookup(filteredEdges))
}

fun graphDfsPath

Find a depth-first path from start to goal.

@param graph Graph to traverse @param start Start node index @param goal Goal node index @param allowedEdges Optional edge subset constraint @return Path if reachable, else null

fun graphDfsPath(
    graph: PointGraph,
    start: Int,
    goal: Int,
    allowedEdges: Set<GraphEdge>? = null
): GraphPath? {
    validatePointGraph(graph, "graphDfsPath")
    validatePointIndex(start, graph.pointCount, "graphDfsPath", "start")
    validatePointIndex(goal, graph.pointCount, "graphDfsPath", "goal")

    if (start == goal) return GraphPath(listOf(start), emptyList(), 0.0)

    val filteredEdges = filterGraphEdges(graph, allowedEdges)
    val adjacency = buildAdjacency(graph.pointCount, filteredEdges)
    val previous = IntArray(graph.pointCount) { -1 }
    val discovered = BooleanArray(graph.pointCount)
    val stack = ArrayDeque<Int>()

    discovered[start] = true
    stack.addLast(start)

    while (stack.isNotEmpty()) {
        val current = stack.removeLast()
        if (current == goal) break

        for (idx in adjacency[current].lastIndex downTo 0) {
            val entry = adjacency[current][idx]
            if (discovered[entry.to]) continue
            discovered[entry.to] = true
            previous[entry.to] = current
            stack.addLast(entry.to)
        }
    }

    if (!discovered[goal]) return null
    val nodes = reconstructPathNodes(previous, start, goal) ?: return null
    return pathFromNodes(nodes, buildEdgeLookup(filteredEdges))
}

fun graphAStarPath

Find a weighted path from start to goal using A*.

@param graph Graph to traverse @param start Start node index @param goal Goal node index @param heuristic Heuristic estimate from current node to goal @param allowedEdges Optional edge subset constraint @return Path if reachable, else null

body — 48 lines, from line 377
fun graphAStarPath(
    graph: PointGraph,
    start: Int,
    goal: Int,
    heuristic: (Int, Int) -> Double = { _, _ -> 0.0 },
    allowedEdges: Set<GraphEdge>? = null
): GraphPath? {
    validatePointGraph(graph, "graphAStarPath")
    validatePointIndex(start, graph.pointCount, "graphAStarPath", "start")
    validatePointIndex(goal, graph.pointCount, "graphAStarPath", "goal")

    if (start == goal) return GraphPath(listOf(start), emptyList(), 0.0)

    val filteredEdges = filterGraphEdges(graph, allowedEdges)
    requireNonNegativeEdgeWeights(filteredEdges, "graphAStarPath")

    val adjacency = buildAdjacency(graph.pointCount, filteredEdges)
    val edgeLookup = buildEdgeLookup(filteredEdges)
    val gScore = DoubleArray(graph.pointCount) { Double.POSITIVE_INFINITY }
    val fScore = DoubleArray(graph.pointCount) { Double.POSITIVE_INFINITY }
    val previous = IntArray(graph.pointCount) { -1 }
    val queue = PriorityQueue<PriorityNode>(compareBy<PriorityNode> { it.priority }.thenBy { it.node })

    gScore[start] = 0.0
    fScore[start] = heuristic(start, goal)
    queue.add(PriorityNode(start, fScore[start]))

    while (queue.isNotEmpty()) {
        val current = queue.poll()
        if (current.priority > fScore[current.node] + GRAPH_SEARCH_EPSILON) continue
        if (current.node == goal) break

        for (entry in adjacency[current.node]) {
            val tentative = gScore[current.node] + entry.edge.weight
            if (tentative + GRAPH_SEARCH_EPSILON < gScore[entry.to]) {
                previous[entry.to] = current.node
                gScore[entry.to] = tentative
                fScore[entry.to] = tentative + heuristic(entry.to, goal)
                queue.add(PriorityNode(entry.to, fScore[entry.to]))
            }
        }
    }

    if (gScore[goal].isInfinite()) return null
    val nodes = reconstructPathNodes(previous, start, goal) ?: return null
    val path = pathFromNodes(nodes, edgeLookup)
    return path.copy(totalWeight = gScore[goal])
}

fun vec2AStarPath

A* path helper using Euclidean distance on 2D points as the heuristic.

fun vec2AStarPath(
    graph: PointGraph,
    points: List<Vec2>,
    start: Int,
    goal: Int,
    allowedEdges: Set<GraphEdge>? = null
): GraphPath? {
    require(points.size >= graph.pointCount) {
        "vec2AStarPath: points must contain at least graph.pointCount values (${graph.pointCount}), found ${points.size}"
    }
    validatePoints2D(points.take(graph.pointCount), "vec2AStarPath")

    return graphAStarPath(
        graph = graph,
        start = start,
        goal = goal,
        heuristic = { a, b -> distance2D(points[a], points[b]) },
        allowedEdges = allowedEdges
    )
}

fun vec3AStarPath

A* path helper using Euclidean distance on 3D points as the heuristic.

fun vec3AStarPath(
    graph: PointGraph,
    points: List<Vec3>,
    start: Int,
    goal: Int,
    allowedEdges: Set<GraphEdge>? = null
): GraphPath? {
    require(points.size >= graph.pointCount) {
        "vec3AStarPath: points must contain at least graph.pointCount values (${graph.pointCount}), found ${points.size}"
    }
    validatePoints3D(points.take(graph.pointCount), "vec3AStarPath")

    return graphAStarPath(
        graph = graph,
        start = start,
        goal = goal,
        heuristic = { a, b -> distance3D(points[a], points[b]) },
        allowedEdges = allowedEdges
    )
}

fun graphGreedyClosestUnvisitedPaths

Build a set of paths from anchor by repeatedly choosing the closest reachable point that is incident to at least one unvisited edge.

Paths are computed on the current unvisited subgraph, so each selected path always contributes newly visited edges.

body — 49 lines, from line 481
fun graphGreedyClosestUnvisitedPaths(
    graph: PointGraph,
    anchor: Int,
    edgeState: GraphEdgeVisitState = graph.edgeVisitState()
): GraphPathSetResult {
    validatePointGraph(graph, "graphGreedyClosestUnvisitedPaths")
    validatePointIndex(anchor, graph.pointCount, "graphGreedyClosestUnvisitedPaths", "anchor")
    var normalizedState = normalizeEdgeVisitState(graph, edgeState, "graphGreedyClosestUnvisitedPaths")

    val paths = mutableListOf<GraphPath>()

    while (normalizedState.unvisitedEdges.isNotEmpty()) {
        requireNonNegativeEdgeWeights(normalizedState.unvisitedEdges, "graphGreedyClosestUnvisitedPaths")

        val adjacency = buildAdjacency(graph.pointCount, normalizedState.unvisitedEdges)
        val dijkstra = runDijkstra(adjacency, anchor)
        val edgeLookup = buildEdgeLookup(normalizedState.unvisitedEdges)
        val candidates = normalizedState.unvisitedEdges.asSequence()
            .flatMap { sequenceOf(it.i, it.j) }
            .toSet()

        var bestTarget = -1
        var bestDistance = Double.POSITIVE_INFINITY

        for (candidate in candidates) {
            if (candidate == anchor) continue
            val distance = dijkstra.distances[candidate]
            if (distance.isInfinite()) continue

            val isBetterDistance = distance < bestDistance - GRAPH_SEARCH_EPSILON
            val isTieBreak = abs(distance - bestDistance) <= GRAPH_SEARCH_EPSILON && (bestTarget < 0 || candidate < bestTarget)
            if (bestTarget < 0 || isBetterDistance || isTieBreak) {
                bestTarget = candidate
                bestDistance = distance
            }
        }

        if (bestTarget < 0) break

        val nodes = reconstructPathNodes(dijkstra.previous, anchor, bestTarget) ?: break
        val path = pathFromNodes(nodes, edgeLookup)
        if (path.edges.isEmpty()) break

        paths += path
        normalizedState = normalizedState.markVisited(path.edges)
    }

    return GraphPathSetResult(paths, normalizedState)
}

fun graphDiameterPath

Compute a weighted graph diameter path (the shortest path between the farthest pair of nodes).

This is the "longest shortest path" over the provided graph/edge subset.

fun graphDiameterPath(
    graph: PointGraph,
    allowedEdges: Set<GraphEdge>? = null
): GraphPath? {
    validatePointGraph(graph, "graphDiameterPath")
    val filteredEdges = filterGraphEdges(graph, allowedEdges)
    if (filteredEdges.isEmpty()) return null
    requireNonNegativeEdgeWeights(filteredEdges, "graphDiameterPath")

    val adjacency = buildAdjacency(graph.pointCount, filteredEdges)
    val edgeLookup = buildEdgeLookup(filteredEdges)
    val activeNodes = adjacency.indices.filter { adjacency[it].isNotEmpty() }
    if (activeNodes.isEmpty()) return null

    var bestDistance = Double.NEGATIVE_INFINITY
    var bestPath: GraphPath? = null

    for (source in activeNodes) {
        val dijkstra = runDijkstra(adjacency, source)
        for (target in activeNodes) {
            if (target == source) continue
            val distance = dijkstra.distances[target]
            if (distance.isInfinite()) continue
            if (distance <= bestDistance + GRAPH_SEARCH_EPSILON) continue

            val nodes = reconstructPathNodes(dijkstra.previous, source, target) ?: continue
            val path = pathFromNodes(nodes, edgeLookup)
            if (path.edges.isEmpty()) continue

            bestDistance = distance
            bestPath = path.copy(totalWeight = distance)
        }
    }

    return bestPath ?: edgeToPath(filteredEdges.first())
}

fun graphDiameterPathDecomposition

Iteratively removes diameter paths from the unvisited edge set until all reachable edges are consumed, producing a long-to-short path decomposition.

fun graphDiameterPathDecomposition(
    graph: PointGraph,
    edgeState: GraphEdgeVisitState = graph.edgeVisitState()
): GraphPathSetResult {
    validatePointGraph(graph, "graphDiameterPathDecomposition")
    var normalizedState = normalizeEdgeVisitState(graph, edgeState, "graphDiameterPathDecomposition")
    val paths = mutableListOf<GraphPath>()

    while (normalizedState.unvisitedEdges.isNotEmpty()) {
        val path = graphDiameterPath(graph, normalizedState.unvisitedEdges)
            ?: edgeToPath(normalizedState.unvisitedEdges.first())
        if (path.edges.isEmpty()) break

        paths += path
        normalizedState = normalizedState.markVisited(path.edges)
    }

    return GraphPathSetResult(paths, normalizedState)
}

fun graphAnchoredGreedyThenDiameterCover

Build an anchored cover by combining: 1) Greedy closest-unvisited anchored paths 2) Diameter decomposition for any disconnected leftovers

fun graphAnchoredGreedyThenDiameterCover(
    graph: PointGraph,
    anchor: Int,
    edgeState: GraphEdgeVisitState = graph.edgeVisitState()
): GraphPathSetResult {
    val greedy = graphGreedyClosestUnvisitedPaths(graph, anchor, edgeState)
    if (greedy.edgeState.unvisitedEdges.isEmpty()) return greedy

    val remainder = graphDiameterPathDecomposition(graph, greedy.edgeState)
    return GraphPathSetResult(
        paths = greedy.paths + remainder.paths,
        edgeState = remainder.edgeState
    )
}

2D Graph Algorithms

fun vec2KnnGraph

Compute a K-Nearest Neighbors graph for 2D points.

Each point is connected to its k nearest neighbors.

@param points List of 2D points @param k Number of nearest neighbors per point @param options Graph connectivity options @return PointGraph with KNN edges

fun vec2KnnGraph(
    points: List<Vec2>,
    k: Int,
    options: GraphConnectivityOptions = GraphConnectivityOptions()
): PointGraph {
    require(points.isNotEmpty()) { "vec2KnnGraph: points must not be empty" }
    require(k > 0) { "vec2KnnGraph: k must be > 0, found $k" }
    validatePoints2D(points, "vec2KnnGraph")

    val edges = mutableSetOf<GraphEdge>()

    for (i in points.indices) {
        val distances = points.indices
            .filter { it != i }
            .map { j -> j to computeDistance2D(points[i], points[j], options, i, j, points) }
            .sortedBy { it.second }
            .take(minOf(k, points.size - 1))

        for ((j, dist) in distances) {
            edges.add(GraphEdge(i, j, dist))
        }
    }

    return PointGraph(edges.toList(), points.size)
}

fun vec3KnnGraph

Compute a K-Nearest Neighbors graph for 3D points.

Each point is connected to its k nearest neighbors.

@param points List of 3D points @param k Number of nearest neighbors per point @param options Graph connectivity options @return PointGraph with KNN edges

fun vec3KnnGraph(
    points: List<Vec3>,
    k: Int,
    options: GraphConnectivityOptions = GraphConnectivityOptions()
): PointGraph {
    require(points.isNotEmpty()) { "vec3KnnGraph: points must not be empty" }
    require(k > 0) { "vec3KnnGraph: k must be > 0, found $k" }
    validatePoints3D(points, "vec3KnnGraph")

    val edges = mutableSetOf<GraphEdge>()

    for (i in points.indices) {
        val distances = points.indices
            .filter { it != i }
            .map { j -> j to computeDistance3D(points[i], points[j], options, i, j, points) }
            .sortedBy { it.second }
            .take(minOf(k, points.size - 1))

        for ((j, dist) in distances) {
            edges.add(GraphEdge(i, j, dist))
        }
    }

    return PointGraph(edges.toList(), points.size)
}

fun vec2OrthantGraph

Compute an Orthant Neighborhood Graph for 2D points.

Each point links to its nearest neighbour in every quadrant that holds one; an edge need not be chosen by both ends. An orthant is a quadrant defined by the coordinate axes relative to a point.

Note: orthant membership is axis-based; the nearest-neighbour distance ranking within each orthant uses GraphConnectivityOptions.customDistanceMetric when provided (Euclidean otherwise).

@param points List of 2D points @param options Graph connectivity options @return PointGraph with orthant neighborhood edges

body — 54 lines, from line 704
fun vec2OrthantGraph(
    points: List<Vec2>,
    options: GraphConnectivityOptions = GraphConnectivityOptions()
): PointGraph {
    require(points.isNotEmpty()) { "vec2OrthantGraph: points must not be empty" }
    validatePoints2D(points, "vec2OrthantGraph")

    val edges = mutableSetOf<GraphEdge>()

    for (i in points.indices) {
        val pi = points[i]

        // Four orthants: (+,+), (+,-), (-,+), (-,-)
        val orthants = listOf(
            listOf(true, true),   // NE: x>0, y>0
            listOf(true, false),  // SE: x>0, y<0
            listOf(false, true),  // NW: x<0, y>0
            listOf(false, false)  // SW: x<0, y<0
        )

        for (orthant in orthants) {
            val (xPos, yPos) = orthant

            // Find nearest neighbor in this orthant
            var nearest: Int? = null
            var nearestDist = Double.POSITIVE_INFINITY

            for (j in points.indices) {
                if (i == j) continue
                val pj = points[j]
                val dx = pj.x - pi.x
                val dy = pj.y - pi.y

                // Check if point is in the correct orthant
                val inOrthant = if (xPos) dx > 0 else dx < 0
                val inOrthantY = if (yPos) dy > 0 else dy < 0

                if (inOrthant && inOrthantY) {
                    val dist = computeDistance2D(pi, pj, options, i, j, points)
                    if (dist < nearestDist) {
                        nearestDist = dist
                        nearest = j
                    }
                }
            }

            if (nearest != null) {
                edges.add(GraphEdge(i, nearest, nearestDist))
            }
        }
    }

    return PointGraph(edges.toList(), points.size)
}

fun vec3OrthantGraph

Compute an Orthant Neighborhood Graph for 3D points.

Each point links to its nearest neighbour in every octant that holds one; an edge need not be chosen by both ends. An orthant is an octant defined by the coordinate axes relative to a point.

Note: orthant membership is axis-based; the nearest-neighbour distance ranking within each orthant uses GraphConnectivityOptions.customDistanceMetric when provided (Euclidean otherwise).

@param points List of 3D points @param options Graph connectivity options @return PointGraph with orthant neighborhood edges

body — 60 lines, from line 772
fun vec3OrthantGraph(
    points: List<Vec3>,
    options: GraphConnectivityOptions = GraphConnectivityOptions()
): PointGraph {
    require(points.isNotEmpty()) { "vec3OrthantGraph: points must not be empty" }
    validatePoints3D(points, "vec3OrthantGraph")

    val edges = mutableSetOf<GraphEdge>()

    for (i in points.indices) {
        val pi = points[i]

        // Eight orthants (octants) in 3D
        val orthants = listOf(
            listOf(true, true, true),     // +,+,+
            listOf(true, true, false),    // +,+,-
            listOf(true, false, true),    // +,-,+
            listOf(true, false, false),   // +,-,-
            listOf(false, true, true),    // -,+,+
            listOf(false, true, false),   // -,+,-
            listOf(false, false, true),   // -,-,+
            listOf(false, false, false)   // -,-,-
        )

        for (orthant in orthants) {
            val (xPos, yPos, zPos) = orthant

            // Find nearest neighbor in this orthant
            var nearest: Int? = null
            var nearestDist = Double.POSITIVE_INFINITY

            for (j in points.indices) {
                if (i == j) continue
                val pj = points[j]
                val dx = pj.x - pi.x
                val dy = pj.y - pi.y
                val dz = pj.z - pi.z

                // Check if point is in the correct orthant
                val inOrthantX = if (xPos) dx > 0 else dx < 0
                val inOrthantY = if (yPos) dy > 0 else dy < 0
                val inOrthantZ = if (zPos) dz > 0 else dz < 0

                if (inOrthantX && inOrthantY && inOrthantZ) {
                    val dist = computeDistance3D(pi, pj, options, i, j, points)
                    if (dist < nearestDist) {
                        nearestDist = dist
                        nearest = j
                    }
                }
            }

            if (nearest != null) {
                edges.add(GraphEdge(i, nearest, nearestDist))
            }
        }
    }

    return PointGraph(edges.toList(), points.size)
}

fun vec2RelativeNeighborhoodGraph

Compute a Relative Neighborhood Graph (RNG) for 2D points.

An edge (u,v) is in RNG if there is no other point w such that max(d(u,w), d(v,w)) < d(u,v).

@param points List of 2D points @param options Graph connectivity options @return PointGraph with RNG edges

fun vec2RelativeNeighborhoodGraph(
    points: List<Vec2>,
    options: GraphConnectivityOptions = GraphConnectivityOptions()
): PointGraph {
    require(points.isNotEmpty()) { "vec2RelativeNeighborhoodGraph: points must not be empty" }
    validatePoints2D(points, "vec2RelativeNeighborhoodGraph")

    val edges = mutableSetOf<GraphEdge>()

    for (i in points.indices) {
        for (j in (i + 1) until points.size) {
            val dij = computeDistance2D(points[i], points[j], options, i, j, points)
            var isRelativeNeighbor = true

            // Check if any other point violates the RNG condition
            for (k in points.indices) {
                if (k == i || k == j) continue
                val dik = computeDistance2D(points[i], points[k], options, i, k, points)
                val djk = computeDistance2D(points[j], points[k], options, j, k, points)

                if (max(dik, djk) < dij) {
                    isRelativeNeighbor = false
                    break
                }
            }

            if (isRelativeNeighbor) {
                edges.add(GraphEdge(i, j, dij))
            }
        }
    }

    return PointGraph(edges.toList(), points.size)
}

fun vec3RelativeNeighborhoodGraph

Compute a Relative Neighborhood Graph (RNG) for 3D points.

An edge (u,v) is in RNG if there is no other point w such that max(d(u,w), d(v,w)) < d(u,v).

@param points List of 3D points @param options Graph connectivity options @return PointGraph with RNG edges

fun vec3RelativeNeighborhoodGraph(
    points: List<Vec3>,
    options: GraphConnectivityOptions = GraphConnectivityOptions()
): PointGraph {
    require(points.isNotEmpty()) { "vec3RelativeNeighborhoodGraph: points must not be empty" }
    validatePoints3D(points, "vec3RelativeNeighborhoodGraph")

    val edges = mutableSetOf<GraphEdge>()

    for (i in points.indices) {
        for (j in (i + 1) until points.size) {
            val dij = computeDistance3D(points[i], points[j], options, i, j, points)
            var isRelativeNeighbor = true

            // Check if any other point violates the RNG condition
            for (k in points.indices) {
                if (k == i || k == j) continue
                val dik = computeDistance3D(points[i], points[k], options, i, k, points)
                val djk = computeDistance3D(points[j], points[k], options, j, k, points)

                if (max(dik, djk) < dij) {
                    isRelativeNeighbor = false
                    break
                }
            }

            if (isRelativeNeighbor) {
                edges.add(GraphEdge(i, j, dij))
            }
        }
    }

    return PointGraph(edges.toList(), points.size)
}

fun vec2GabrielGraph

Compute a Gabriel Graph for 2D points.

An edge (u,v) is in the Gabriel graph if the circle with diameter uv contains no other points. This is equivalent to: for all other points w, d(u,w)^2 + d(v,w)^2 >= d(u,v)^2.

@param points List of 2D points @param options Graph connectivity options @return PointGraph with Gabriel graph edges

fun vec2GabrielGraph(
    points: List<Vec2>,
    options: GraphConnectivityOptions = GraphConnectivityOptions()
): PointGraph {
    require(points.isNotEmpty()) { "vec2GabrielGraph: points must not be empty" }
    validatePoints2D(points, "vec2GabrielGraph")

    val edges = mutableSetOf<GraphEdge>()

    for (i in points.indices) {
        for (j in (i + 1) until points.size) {
            val dij = computeDistance2D(points[i], points[j], options, i, j, points)
            val dijSq = dij * dij
            var isGabriel = true

            // Check if any other point is inside the circle
            for (k in points.indices) {
                if (k == i || k == j) continue
                val dik = computeDistance2D(points[i], points[k], options, i, k, points)
                val djk = computeDistance2D(points[j], points[k], options, j, k, points)

                // If point is inside the circle with diameter (i,j)
                if (dik * dik + djk * djk < dijSq - 1e-10) {
                    isGabriel = false
                    break
                }
            }

            if (isGabriel) {
                edges.add(GraphEdge(i, j, dij))
            }
        }
    }

    return PointGraph(edges.toList(), points.size)
}

fun vec3GabrielGraph

Compute a Gabriel Graph for 3D points.

An edge (u,v) is in the Gabriel graph if the sphere with diameter uv contains no other points.

@param points List of 3D points @param options Graph connectivity options @return PointGraph with Gabriel graph edges

fun vec3GabrielGraph(
    points: List<Vec3>,
    options: GraphConnectivityOptions = GraphConnectivityOptions()
): PointGraph {
    require(points.isNotEmpty()) { "vec3GabrielGraph: points must not be empty" }
    validatePoints3D(points, "vec3GabrielGraph")

    val edges = mutableSetOf<GraphEdge>()

    for (i in points.indices) {
        for (j in (i + 1) until points.size) {
            val dij = computeDistance3D(points[i], points[j], options, i, j, points)
            val dijSq = dij * dij
            var isGabriel = true

            // Check if any other point is inside the sphere
            for (k in points.indices) {
                if (k == i || k == j) continue
                val dik = computeDistance3D(points[i], points[k], options, i, k, points)
                val djk = computeDistance3D(points[j], points[k], options, j, k, points)

                // If point is inside the sphere with diameter (i,j)
                if (dik * dik + djk * djk < dijSq - 1e-10) {
                    isGabriel = false
                    break
                }
            }

            if (isGabriel) {
                edges.add(GraphEdge(i, j, dij))
            }
        }
    }

    return PointGraph(edges.toList(), points.size)
}

fun vec2BetaSkeleton

Compute a Beta-Skeleton for 2D points.

An edge (u,v) is in the beta-skeleton if the lune (intersection of two circles) defined by parameter beta contains no other points. - beta = 0: nearest neighbor graph - beta = 1: Gabriel graph - beta = 2: Relative neighborhood graph

@param points List of 2D points @param beta Parameter controlling the lune shape (must be >= 0) @param options Graph connectivity options @return PointGraph with beta-skeleton edges

body — 70 lines, from line 1030
fun vec2BetaSkeleton(
    points: List<Vec2>,
    beta: Double = 1.0,
    options: GraphConnectivityOptions = GraphConnectivityOptions()
): PointGraph {
    require(points.isNotEmpty()) { "vec2BetaSkeleton: points must not be empty" }
    require(beta >= 0.0) { "vec2BetaSkeleton: beta must be >= 0, found $beta" }
    validatePoints2D(points, "vec2BetaSkeleton")

    val edges = mutableSetOf<GraphEdge>()

    for (i in points.indices) {
        for (j in (i + 1) until points.size) {
            val dij = computeDistance2D(points[i], points[j], options, i, j, points)
            var isBetaSkeleton = true

            // Check if any other point violates the beta-skeleton condition
            for (k in points.indices) {
                if (k == i || k == j) continue
                val dik = computeDistance2D(points[i], points[k], options, i, k, points)
                val djk = computeDistance2D(points[j], points[k], options, j, k, points)

                // Beta-skeleton condition
                when {
                    beta == 0.0 -> {
                        // Nearest neighbor graph - always include if it's the nearest
                        if (dik < dij || djk < dij) {
                            isBetaSkeleton = false
                            break
                        }
                    }
                    beta == 1.0 -> {
                        // Gabriel graph condition
                        if (dik * dik + djk * djk < dij * dij - 1e-10) {
                            isBetaSkeleton = false
                            break
                        }
                    }
                    beta >= 2.0 -> {
                        // RNG-like condition
                        if (max(dik, djk) < dij * beta / 2.0) {
                            isBetaSkeleton = false
                            break
                        }
                    }
                    else -> {
                        // General beta-skeleton condition (simplified)
                        val radius = dij * beta / 2.0
                        val midX = (points[i].x + points[j].x) / 2.0
                        val midY = (points[i].y + points[j].y) / 2.0
                        val dx = points[k].x - midX
                        val dy = points[k].y - midY
                        val distToMid = sqrt(dx * dx + dy * dy)

                        if (distToMid < radius - 1e-10) {
                            isBetaSkeleton = false
                            break
                        }
                    }
                }
            }

            if (isBetaSkeleton) {
                edges.add(GraphEdge(i, j, dij))
            }
        }
    }

    return PointGraph(edges.toList(), points.size)
}

fun vec3BetaSkeleton

Compute a Beta-Skeleton for 3D points.

@param points List of 3D points @param beta Parameter controlling the skeleton shape (must be >= 0) @param options Graph connectivity options @return PointGraph with beta-skeleton edges

body — 67 lines, from line 1109
fun vec3BetaSkeleton(
    points: List<Vec3>,
    beta: Double = 1.0,
    options: GraphConnectivityOptions = GraphConnectivityOptions()
): PointGraph {
    require(points.isNotEmpty()) { "vec3BetaSkeleton: points must not be empty" }
    require(beta >= 0.0) { "vec3BetaSkeleton: beta must be >= 0, found $beta" }
    validatePoints3D(points, "vec3BetaSkeleton")

    val edges = mutableSetOf<GraphEdge>()

    for (i in points.indices) {
        for (j in (i + 1) until points.size) {
            val dij = computeDistance3D(points[i], points[j], options, i, j, points)
            var isBetaSkeleton = true

            for (k in points.indices) {
                if (k == i || k == j) continue
                val dik = computeDistance3D(points[i], points[k], options, i, k, points)
                val djk = computeDistance3D(points[j], points[k], options, j, k, points)

                when {
                    beta == 0.0 -> {
                        if (dik < dij || djk < dij) {
                            isBetaSkeleton = false
                            break
                        }
                    }
                    beta == 1.0 -> {
                        // Gabriel graph condition
                        if (dik * dik + djk * djk < dij * dij - 1e-10) {
                            isBetaSkeleton = false
                            break
                        }
                    }
                    beta >= 2.0 -> {
                        if (max(dik, djk) < dij * beta / 2.0) {
                            isBetaSkeleton = false
                            break
                        }
                    }
                    else -> {
                        val radius = dij * beta / 2.0
                        val midX = (points[i].x + points[j].x) / 2.0
                        val midY = (points[i].y + points[j].y) / 2.0
                        val midZ = (points[i].z + points[j].z) / 2.0
                        val dx = points[k].x - midX
                        val dy = points[k].y - midY
                        val dz = points[k].z - midZ
                        val distToMid = sqrt(dx * dx + dy * dy + dz * dz)

                        if (distToMid < radius - 1e-10) {
                            isBetaSkeleton = false
                            break
                        }
                    }
                }
            }

            if (isBetaSkeleton) {
                edges.add(GraphEdge(i, j, dij))
            }
        }
    }

    return PointGraph(edges.toList(), points.size)
}

fun vec2MinimumSpanningTree

Compute a KNN-constrained minimum spanning tree for 2D points.

Uses Kruskal's algorithm over an undirected K-nearest-neighbor graph. The neighborhood size grows adaptively until a spanning tree exists.

@param points List of 2D points @param options Graph connectivity options @return PointGraph with MST edges

fun vec2MinimumSpanningTree(
    points: List<Vec2>,
    options: GraphConnectivityOptions = GraphConnectivityOptions()
): PointGraph {
    require(points.isNotEmpty()) { "vec2MinimumSpanningTree: points must not be empty" }
    validatePoints2D(points, "vec2MinimumSpanningTree")

    return adaptiveKnnSpanningTree(
        pointCount = points.size,
        knnEdges = { k -> vec2KnnGraph(points, k, options).edges }
    )
}

fun vec3MinimumSpanningTree

Compute a KNN-constrained minimum spanning tree for 3D points.

Uses Kruskal's algorithm over an undirected K-nearest-neighbor graph. The neighborhood size grows adaptively until a spanning tree exists.

@param points List of 3D points @param options Graph connectivity options @return PointGraph with MST edges

fun vec3MinimumSpanningTree(
    points: List<Vec3>,
    options: GraphConnectivityOptions = GraphConnectivityOptions()
): PointGraph {
    require(points.isNotEmpty()) { "vec3MinimumSpanningTree: points must not be empty" }
    validatePoints3D(points, "vec3MinimumSpanningTree")

    return adaptiveKnnSpanningTree(
        pointCount = points.size,
        knnEdges = { k -> vec3KnnGraph(points, k, options).edges }
    )
}

fun vec2MaximumDiameterGraph

Compute a Maximum Diameter Graph for 2D points.

Connects all points within a specified maximum distance (diameter).

@param points List of 2D points @param maxDiameter Maximum distance for edge creation @param options Graph connectivity options @return PointGraph with edges within maximum diameter

fun vec2MaximumDiameterGraph(
    points: List<Vec2>,
    maxDiameter: Double,
    options: GraphConnectivityOptions = GraphConnectivityOptions()
): PointGraph {
    require(points.isNotEmpty()) { "vec2MaximumDiameterGraph: points must not be empty" }
    require(maxDiameter > 0.0) { "vec2MaximumDiameterGraph: maxDiameter must be > 0, found $maxDiameter" }
    validatePoints2D(points, "vec2MaximumDiameterGraph")

    val edges = mutableListOf<GraphEdge>()

    for (i in points.indices) {
        for (j in (i + 1) until points.size) {
            val dist = computeDistance2D(points[i], points[j], options, i, j, points)
            if (dist <= maxDiameter) {
                edges.add(GraphEdge(i, j, dist))
            }
        }
    }

    return PointGraph(edges, points.size)
}

fun vec3MaximumDiameterGraph

Compute a Maximum Diameter Graph for 3D points.

Connects all points within a specified maximum distance (diameter).

@param points List of 3D points @param maxDiameter Maximum distance for edge creation @param options Graph connectivity options @return PointGraph with edges within maximum diameter

fun vec3MaximumDiameterGraph(
    points: List<Vec3>,
    maxDiameter: Double,
    options: GraphConnectivityOptions = GraphConnectivityOptions()
): PointGraph {
    require(points.isNotEmpty()) { "vec3MaximumDiameterGraph: points must not be empty" }
    require(maxDiameter > 0.0) { "vec3MaximumDiameterGraph: maxDiameter must be > 0, found $maxDiameter" }
    validatePoints3D(points, "vec3MaximumDiameterGraph")

    val edges = mutableListOf<GraphEdge>()

    for (i in points.indices) {
        for (j in (i + 1) until points.size) {
            val dist = computeDistance3D(points[i], points[j], options, i, j, points)
            if (dist <= maxDiameter) {
                edges.add(GraphEdge(i, j, dist))
            }
        }
    }

    return PointGraph(edges, points.size)
}

fun vec2NearestNeighborGraph

Compute a Nearest Neighbor Graph (1-NN) for 2D points.

Each point is connected to its single nearest neighbor.

@param points List of 2D points @param options Graph connectivity options @return PointGraph with nearest neighbor edges

fun vec2NearestNeighborGraph(
    points: List<Vec2>,
    options: GraphConnectivityOptions = GraphConnectivityOptions()
): PointGraph {
    return vec2KnnGraph(points, 1, options)
}

fun vec3NearestNeighborGraph

Compute a Nearest Neighbor Graph (1-NN) for 3D points.

Each point is connected to its single nearest neighbor.

@param points List of 3D points @param options Graph connectivity options @return PointGraph with nearest neighbor edges

fun vec3NearestNeighborGraph(
    points: List<Vec3>,
    options: GraphConnectivityOptions = GraphConnectivityOptions()
): PointGraph {
    return vec3KnnGraph(points, 1, options)
}

fun vec2UrquhartGraph

Compute an Urquhart Graph for 2D points.

The Urquhart graph is formed by removing the longest edge from each triangle in the Delaunay triangulation.

@param points List of 2D points @param options Graph connectivity options @return PointGraph with Urquhart graph edges

fun vec2UrquhartGraph(
    points: List<Vec2>,
    options: GraphConnectivityOptions = GraphConnectivityOptions()
): PointGraph {
    require(points.isNotEmpty()) { "vec2UrquhartGraph: points must not be empty" }
    validatePoints2D(points, "vec2UrquhartGraph")

    if (points.size < 3) return vec2DelaunayGraph(points, options)

    val triangles = vec2DelaunayTriangles(points)
    if (triangles.isEmpty()) return vec2DelaunayGraph(points, options)

    val candidateEdges = mutableSetOf<GraphEdge>()
    val removedEdges = mutableSetOf<GraphEdge>()
    val tieTolerance = 1e-9

    for ((a, b, c) in triangles) {
        val ab = GraphEdge(a, b, computeDistance2D(points[a], points[b], options, a, b, points))
        val bc = GraphEdge(b, c, computeDistance2D(points[b], points[c], options, b, c, points))
        val ca = GraphEdge(c, a, computeDistance2D(points[c], points[a], options, c, a, points))
        val triangleEdges = listOf(ab, bc, ca)

        candidateEdges.addAll(triangleEdges)

        val maxWeight = triangleEdges.maxOf { it.weight }
        val longestEdges = triangleEdges.filter { abs(it.weight - maxWeight) <= tieTolerance }

        // When a triangle has a tied longest edge, keep those edges rather than
        // deleting multiple edges due to symmetry or floating-point noise.
        if (longestEdges.size == 1) {
            removedEdges.add(longestEdges.first())
        }
    }

    return PointGraph((candidateEdges - removedEdges).toList(), points.size)
}

fun vec3UrquhartGraph

Not an Urquhart graph: a 3D one would remove the longest edges of a 3D Delaunay tetrahedralization of the points, and Field has no 3D Delaunay of a point set, so this returned the minimum spanning tree under the wrong name. Deprecated at ERROR level so a call says so at compile time; use vec3MinimumSpanningTree for what it computed.

@param points List of 3D points @param options Graph connectivity options @return PointGraph with Urquhart graph edges

@Deprecated(
    "Not an Urquhart graph: Field has no 3D Delaunay of a point set, so this was the minimum spanning tree",
    ReplaceWith("vec3MinimumSpanningTree(points, options)"),
    level = DeprecationLevel.ERROR,
)
fun vec3UrquhartGraph(
    points: List<Vec3>,
    options: GraphConnectivityOptions = GraphConnectivityOptions()
): PointGraph {
    require(points.isNotEmpty()) { "vec3UrquhartGraph: points must not be empty" }
    validatePoints3D(points, "vec3UrquhartGraph")

    return vec3MinimumSpanningTree(points, options)
}

fun vec2DelaunayGraph

Compute a full Delaunay triangulation for 2D points using JTS.

The Delaunay triangulation is a triangulation where no point is inside the circumcircle of any triangle.

@param points List of 2D points @param options Graph connectivity options @return PointGraph with Delaunay triangulation edges

body — 41 lines, from line 1403
fun vec2DelaunayGraph(
    points: List<Vec2>,
    options: GraphConnectivityOptions = GraphConnectivityOptions()
): PointGraph {
    require(points.isNotEmpty()) { "vec2DelaunayGraph: points must not be empty" }
    validatePoints2D(points, "vec2DelaunayGraph")

    if (points.size < 2) return PointGraph(emptyList(), points.size)

    // Convert to JTS coordinates
    val coords = points.map { Coordinate(it.x.toDouble(), it.y.toDouble()) }

    // Build Delaunay triangulation
    val builder = DelaunayTriangulationBuilder()
    builder.setSites(coords)
    val triangulation = builder.getEdges(GeometryFactory())

    // Extract edges and map back to point indices
    val edges = mutableSetOf<GraphEdge>()

    for (i in 0 until triangulation.numGeometries) {
        val edge = triangulation.getGeometryN(i)
        val edgeCoords = edge.coordinates

        if (edgeCoords.size >= 2) {
            val c0 = edgeCoords[0]
            val c1 = edgeCoords[1]

            // Find indices of these coordinates in original points
            val idx0 = coords.indexOfFirst { it.equals2D(c0) }
            val idx1 = coords.indexOfFirst { it.equals2D(c1) }

            if (idx0 >= 0 && idx1 >= 0) {
                val dist = computeDistance2D(points[idx0], points[idx1], options, idx0, idx1, points)
                edges.add(GraphEdge(idx0, idx1, dist))
            }
        }
    }

    return PointGraph(edges.toList(), points.size)
}

data class VoronoiCell

Represents a Voronoi cell in the Voronoi diagram.

@param siteIndex Index of the site point that generated this cell @param vertices List of vertex coordinates defining the cell boundary

data class VoronoiCell(
    val siteIndex: Int,
    val vertices: List<Vec2>
)

data class VoronoiDiagram

Represents a Voronoi diagram computed from a set of points.

@param cells List of Voronoi cells @param pointCount Number of input points

data class VoronoiDiagram(
    val cells: List<VoronoiCell>,
    val pointCount: Int
) {

fun cellForSite(siteIndex: Int): VoronoiCell?

Get the Voronoi cell for a specific site point.

    fun cellForSite(siteIndex: Int): VoronoiCell? {
        return cells.find { it.siteIndex == siteIndex }
    }
}

fun vec2VoronoiDiagram

Compute a Voronoi diagram for 2D points using JTS.

The Voronoi diagram divides the plane into regions, where each region contains all points closer to one site than to any other site.

@param points List of 2D points (sites) @param options Graph connectivity options @return VoronoiDiagram with cells for each site

body — 48 lines, from line 1484
fun vec2VoronoiDiagram(
    points: List<Vec2>,
    options: GraphConnectivityOptions = GraphConnectivityOptions()
): VoronoiDiagram {
    require(points.isNotEmpty()) { "vec2VoronoiDiagram: points must not be empty" }
    validatePoints2D(points, "vec2VoronoiDiagram")

    if (points.size < 2) {
        return VoronoiDiagram(emptyList(), points.size)
    }

    // Convert to JTS coordinates
    val coords = points.map { Coordinate(it.x.toDouble(), it.y.toDouble()) }

    // Build Voronoi diagram
    val builder = VoronoiDiagramBuilder()
    builder.setSites(coords)
    val voronoi = builder.getDiagram(GeometryFactory())

    // Extract cells
    val cells = mutableListOf<VoronoiCell>()

    for (i in 0 until voronoi.numGeometries) {
        val polygon = voronoi.getGeometryN(i)

        // Find which site this cell corresponds to
        val centroid = polygon.centroid
        var closestSite = 0
        var minDist = Double.POSITIVE_INFINITY

        for (j in coords.indices) {
            val dist = centroid.coordinate.distance(coords[j])
            if (dist < minDist) {
                minDist = dist
                closestSite = j
            }
        }

        // Extract vertices
        val vertices = polygon.coordinates.map {
            vec(it.x.toFloat(), it.y.toFloat())
        }

        cells.add(VoronoiCell(closestSite, vertices))
    }

    return VoronoiDiagram(cells, points.size)
}

fun vec2VoronoiDualGraph

Compute the dual graph of a Voronoi diagram (equivalent to Delaunay triangulation).

Two sites are connected if their Voronoi cells share an edge.

@param points List of 2D points @param options Graph connectivity options @return PointGraph with edges connecting adjacent Voronoi cells

fun vec2VoronoiDualGraph(
    points: List<Vec2>,
    options: GraphConnectivityOptions = GraphConnectivityOptions()
): PointGraph {
    // The dual of Voronoi is the Delaunay triangulation
    return vec2DelaunayGraph(points, options)
}

fun vec2MaximumCutGraph

Compute a Maximum Cut approximation graph for 2D points.

Creates a bipartite-like graph by partitioning points and maximizing cross-partition edges. Uses a greedy approximation.

@param points List of 2D points @param options Graph connectivity options @return PointGraph with maximum cut edges

fun vec2MaximumCutGraph(
    points: List<Vec2>,
    options: GraphConnectivityOptions = GraphConnectivityOptions()
): PointGraph {
    require(points.isNotEmpty()) { "vec2MaximumCutGraph: points must not be empty" }
    validatePoints2D(points, "vec2MaximumCutGraph")

    if (points.size < 2) return PointGraph(emptyList(), points.size)

    // Simple greedy partitioning based on coordinate
    val partition = BooleanArray(points.size)

    // Partition by alternating sorted order (by x-coordinate)
    val sorted = points.indices.sortedBy { points[it].x }
    sorted.forEachIndexed { index, pointIndex ->
        partition[pointIndex] = index % 2 == 0
    }

    // Add edges between different partitions
    val edges = mutableListOf<GraphEdge>()
    for (i in points.indices) {
        for (j in (i + 1) until points.size) {
            if (partition[i] != partition[j]) {
                edges.add(GraphEdge(i, j, computeDistance2D(points[i], points[j], options, i, j, points)))
            }
        }
    }

    return PointGraph(edges, points.size)
}

fun vec3MaximumCutGraph

Compute a Maximum Cut approximation graph for 3D points.

@param points List of 3D points @param options Graph connectivity options @return PointGraph with maximum cut edges

fun vec3MaximumCutGraph(
    points: List<Vec3>,
    options: GraphConnectivityOptions = GraphConnectivityOptions()
): PointGraph {
    require(points.isNotEmpty()) { "vec3MaximumCutGraph: points must not be empty" }
    validatePoints3D(points, "vec3MaximumCutGraph")

    if (points.size < 2) return PointGraph(emptyList(), points.size)

    // Partition by alternating sorted order (by x-coordinate)
    val partition = BooleanArray(points.size)
    val sorted = points.indices.sortedBy { points[it].x }
    sorted.forEachIndexed { index, pointIndex ->
        partition[pointIndex] = index % 2 == 0
    }

    // Add edges between different partitions
    val edges = mutableListOf<GraphEdge>()
    for (i in points.indices) {
        for (j in (i + 1) until points.size) {
            if (partition[i] != partition[j]) {
                edges.add(GraphEdge(i, j, computeDistance3D(points[i], points[j], options, i, j, points)))
            }
        }
    }

    return PointGraph(edges, points.size)
}

fun vec2CompleteGraph

Compute a Complete Euclidean Graph for 2D points.

Connects every pair of points.

@param points List of 2D points @param options Graph connectivity options @return PointGraph with all possible edges

fun vec2CompleteGraph(
    points: List<Vec2>,
    options: GraphConnectivityOptions = GraphConnectivityOptions()
): PointGraph {
    require(points.isNotEmpty()) { "vec2CompleteGraph: points must not be empty" }
    validatePoints2D(points, "vec2CompleteGraph")

    val edges = mutableListOf<GraphEdge>()
    for (i in points.indices) {
        for (j in (i + 1) until points.size) {
            edges.add(GraphEdge(i, j, computeDistance2D(points[i], points[j], options, i, j, points)))
        }
    }

    return PointGraph(edges, points.size)
}

fun vec3CompleteGraph

Compute a Complete Euclidean Graph for 3D points.

Connects every pair of points.

@param points List of 3D points @param options Graph connectivity options @return PointGraph with all possible edges

fun vec3CompleteGraph(
    points: List<Vec3>,
    options: GraphConnectivityOptions = GraphConnectivityOptions()
): PointGraph {
    require(points.isNotEmpty()) { "vec3CompleteGraph: points must not be empty" }
    validatePoints3D(points, "vec3CompleteGraph")

    val edges = mutableListOf<GraphEdge>()
    for (i in points.indices) {
        for (j in (i + 1) until points.size) {
            edges.add(GraphEdge(i, j, computeDistance3D(points[i], points[j], options, i, j, points)))
        }
    }

    return PointGraph(edges, points.size)
}

Helper Functions

source — 197 lines, from line 1681
private const val GRAPH_SEARCH_EPSILON = 1e-12

private data class GraphAdjacencyEntry(
    val to: Int,
    val edge: GraphEdge
)

private data class DijkstraResult(
    val distances: DoubleArray,
    val previous: IntArray
)

private data class PriorityNode(
    val node: Int,
    val priority: Double
)

private fun buildAdjacency(
    pointCount: Int,
    edges: Collection<GraphEdge>
): Array<List<GraphAdjacencyEntry>> {
    val adjacency = Array(pointCount) { mutableListOf<GraphAdjacencyEntry>() }
    for (edge in edges) {
        adjacency[edge.i].add(GraphAdjacencyEntry(edge.j, edge))
        adjacency[edge.j].add(GraphAdjacencyEntry(edge.i, edge))
    }
    return Array(pointCount) { index -> adjacency[index].sortedBy { it.to } }
}

private fun filterGraphEdges(graph: PointGraph, allowedEdges: Set<GraphEdge>?): List<GraphEdge> {
    if (allowedEdges == null) return graph.edges
    if (allowedEdges.isEmpty()) return emptyList()
    return graph.edges.filter { it in allowedEdges }
}

private fun buildEdgeLookup(edges: Collection<GraphEdge>): Map<Long, GraphEdge> {
    val lookup = mutableMapOf<Long, GraphEdge>()
    edges.forEach { edge ->
        lookup[edgeKey(edge.i, edge.j)] = edge
    }
    return lookup
}

private fun edgeKey(a: Int, b: Int): Long {
    val lo = min(a, b).toLong() and 0xffffffffL
    val hi = max(a, b).toLong() and 0xffffffffL
    return (lo shl 32) or hi
}

private fun reconstructPathNodes(
    previous: IntArray,
    start: Int,
    goal: Int
): List<Int>? {
    if (start == goal) return listOf(start)
    if (previous[goal] == -1) return null

    val reversePath = mutableListOf<Int>()
    var current = goal

    while (current != -1) {
        reversePath += current
        if (current == start) break
        current = previous[current]
    }

    if (reversePath.lastOrNull() != start) return null
    reversePath.reverse()
    return reversePath
}

private fun pathFromNodes(
    nodes: List<Int>,
    edgeLookup: Map<Long, GraphEdge>
): GraphPath {
    if (nodes.size == 1) return GraphPath(nodes, emptyList(), 0.0)

    val edges = mutableListOf<GraphEdge>()
    var totalWeight = 0.0
    for (i in 0 until nodes.lastIndex) {
        val a = nodes[i]
        val b = nodes[i + 1]
        val edge = requireNotNull(edgeLookup[edgeKey(a, b)]) {
            "Internal graph path error: missing edge between $a and $b"
        }
        edges += edge
        totalWeight += edge.weight
    }
    return GraphPath(nodes, edges, totalWeight)
}

private fun edgeToPath(edge: GraphEdge): GraphPath {
    return GraphPath(
        nodes = listOf(edge.i, edge.j),
        edges = listOf(edge),
        totalWeight = edge.weight
    )
}

private fun runDijkstra(
    adjacency: Array<List<GraphAdjacencyEntry>>,
    source: Int
): DijkstraResult {
    val distance = DoubleArray(adjacency.size) { Double.POSITIVE_INFINITY }
    val previous = IntArray(adjacency.size) { -1 }
    val queue = PriorityQueue<PriorityNode>(compareBy<PriorityNode> { it.priority }.thenBy { it.node })

    distance[source] = 0.0
    queue.add(PriorityNode(source, 0.0))

    while (queue.isNotEmpty()) {
        val current = queue.poll()
        if (current.priority > distance[current.node] + GRAPH_SEARCH_EPSILON) continue

        for (entry in adjacency[current.node]) {
            val tentative = distance[current.node] + entry.edge.weight
            if (tentative + GRAPH_SEARCH_EPSILON < distance[entry.to]) {
                distance[entry.to] = tentative
                previous[entry.to] = current.node
                queue.add(PriorityNode(entry.to, tentative))
            }
        }
    }

    return DijkstraResult(distance, previous)
}

private fun validatePointIndex(
    index: Int,
    pointCount: Int,
    fn: String,
    argument: String
) {
    require(index in 0 until pointCount) {
        "$fn: $argument must be in [0, ${pointCount - 1}], found $index"
    }
}

private fun validatePointGraph(graph: PointGraph, fn: String) {
    require(graph.pointCount >= 0) {
        "$fn: graph.pointCount must be >= 0, found ${graph.pointCount}"
    }

    graph.edges.forEachIndexed { idx, edge ->
        require(edge.i in 0 until graph.pointCount) {
            "$fn: edges[$idx].i=${edge.i} is out of range [0, ${graph.pointCount - 1}]"
        }
        require(edge.j in 0 until graph.pointCount) {
            "$fn: edges[$idx].j=${edge.j} is out of range [0, ${graph.pointCount - 1}]"
        }
        require(edge.weight.isFinite()) {
            "$fn: edges[$idx] weight must be finite, found ${edge.weight}"
        }
    }
}

private fun requireNonNegativeEdgeWeights(edges: Collection<GraphEdge>, fn: String) {
    edges.forEachIndexed { idx, edge ->
        require(edge.weight >= 0.0) {
            "$fn: edge weights must be >= 0, found edges[$idx].weight=${edge.weight}"
        }
    }
}

private fun normalizeEdgeVisitState(
    graph: PointGraph,
    edgeState: GraphEdgeVisitState,
    fn: String
): GraphEdgeVisitState {
    require(edgeState.pointCount == graph.pointCount) {
        "$fn: edgeState.pointCount (${edgeState.pointCount}) must equal graph.pointCount (${graph.pointCount})"
    }

    val allEdges = graph.edges.toSet()
    val visited = edgeState.visitedEdges.filter { it in allEdges }.toSet()
    val explicitlyUnvisited = edgeState.unvisitedEdges.filter { it in allEdges }.toSet() - visited
    val missing = allEdges - visited - explicitlyUnvisited

    return GraphEdgeVisitState(
        pointCount = graph.pointCount,
        visitedEdges = visited,
        unvisitedEdges = explicitlyUnvisited + missing
    )
}

private fun distance2D(a: Vec2, b: Vec2): Double {
    val dx = a.x.toDouble() - b.x.toDouble()
    val dy = a.y.toDouble() - b.y.toDouble()
    return sqrt(dx * dx + dy * dy)
}

private fun distance3D(a: Vec3, b: Vec3): Double {
    val dx = a.x.toDouble() - b.x.toDouble()
    val dy = a.y.toDouble() - b.y.toDouble()
    val dz = a.z.toDouble() - b.z.toDouble()
    return sqrt(dx * dx + dy * dy + dz * dz)
}

Compute distance between two 2D points, using custom metric if provided.

private fun computeDistance2D(
    a: Vec2,
    b: Vec2,
    options: GraphConnectivityOptions,
    idxA: Int,
    idxB: Int,
    points: List<Vec2>
): Double {
    return options.customDistanceMetric?.invoke(idxA, idxB, points) ?: distance2D(a, b)
}

Compute distance between two 3D points, using custom metric if provided.

private fun computeDistance3D(
    a: Vec3,
    b: Vec3,
    options: GraphConnectivityOptions,
    idxA: Int,
    idxB: Int,
    points: List<Vec3>
): Double {
    return options.customDistanceMetric?.invoke(idxA, idxB, points) ?: distance3D(a, b)
}

private fun validatePoints2D(points: List<Vec2>, fn: String) {
    points.forEachIndexed { i, p ->
        require(p.x.isFinite() && p.y.isFinite()) {
            "$fn: points[$i] must be finite, found (${p.x}, ${p.y})"
        }
    }
}

private fun validatePoints3D(points: List<Vec3>, fn: String) {
    points.forEachIndexed { i, p ->
        require(p.x.isFinite() && p.y.isFinite() && p.z.isFinite()) {
            "$fn: points[$i] must be finite, found (${p.x}, ${p.y}, ${p.z})"
        }
    }
}

Union-Find (Disjoint Set Union) data structure for Kruskal's algorithm.

source — 95 lines, from line 1926
private class UnionFind(size: Int) {
    private val parent = IntArray(size) { it }
    private val rank = IntArray(size) { 0 }

    fun find(x: Int): Int {
        if (parent[x] != x) {
            parent[x] = find(parent[x]) // Path compression
        }
        return parent[x]
    }

    fun union(x: Int, y: Int): Boolean {
        val rootX = find(x)
        val rootY = find(y)

        if (rootX == rootY) return false

        // Union by rank
        when {
            rank[rootX] < rank[rootY] -> parent[rootX] = rootY
            rank[rootX] > rank[rootY] -> parent[rootY] = rootX
            else -> {
                parent[rootY] = rootX
                rank[rootX]++
            }
        }

        return true
    }
}

private fun kruskalSpanningForest(pointCount: Int, edges: List<GraphEdge>): List<GraphEdge> {
    if (pointCount <= 1 || edges.isEmpty()) return emptyList()

    val sortedEdges = edges.sortedBy { it.weight }
    val uf = UnionFind(pointCount)
    val treeEdges = mutableListOf<GraphEdge>()

    for (edge in sortedEdges) {
        if (uf.union(edge.i, edge.j)) {
            treeEdges.add(edge)
            if (treeEdges.size == pointCount - 1) break
        }
    }

    return treeEdges
}

private fun isConnectedGraph(pointCount: Int, edges: List<GraphEdge>): Boolean {
    if (pointCount <= 1) return true
    if (edges.isEmpty()) return false

    val adjacency = Array(pointCount) { mutableListOf<Int>() }
    for (edge in edges) {
        adjacency[edge.i].add(edge.j)
        adjacency[edge.j].add(edge.i)
    }

    val visited = BooleanArray(pointCount)
    val queue = ArrayDeque<Int>()
    visited[0] = true
    queue.addLast(0)

    while (queue.isNotEmpty()) {
        val current = queue.removeFirst()
        for (neighbor in adjacency[current]) {
            if (visited[neighbor]) continue
            visited[neighbor] = true
            queue.addLast(neighbor)
        }
    }

    return visited.all { it }
}

private fun adaptiveKnnSpanningTree(
    pointCount: Int,
    knnEdges: (k: Int) -> List<GraphEdge>
): PointGraph {
    if (pointCount <= 1) return PointGraph(emptyList(), pointCount)

    var k = 1
    var candidateEdges = knnEdges(k)

    while (k < pointCount - 1 && !isConnectedGraph(pointCount, candidateEdges)) {
        k = min(pointCount - 1, k * 2)
        candidateEdges = knnEdges(k)
    }

    val treeEdges = kruskalSpanningForest(pointCount, candidateEdges)
    require(treeEdges.size == pointCount - 1) {
        "adaptiveKnnSpanningTree: failed to build a spanning tree for $pointCount points"
    }
    return PointGraph(treeEdges, pointCount)
}

Additional JTS Wrappers

data class ConvexHull2D

Represents a convex hull of a point set.

@param hullPoints List of points forming the convex hull in counter-clockwise order @param hullIndices Indices of hull points in the original point list

data class ConvexHull2D(
    val hullPoints: List<Vec2>,
    val hullIndices: List<Int>
)

fun vec2ConvexHull(points: List<Vec2>): ConvexHull2D

Compute the convex hull of 2D points using JTS.

The convex hull is the smallest convex polygon containing all points.

@param points List of 2D points @return ConvexHull2D with hull vertices

fun vec2ConvexHull(points: List<Vec2>): ConvexHull2D {
    require(points.isNotEmpty()) { "vec2ConvexHull: points must not be empty" }
    validatePoints2D(points, "vec2ConvexHull")

    if (points.size < 3) {
        return ConvexHull2D(points.toList(), points.indices.toList())
    }

    // Convert to JTS coordinates
    val coords = points.map { Coordinate(it.x.toDouble(), it.y.toDouble()) }

    // Compute convex hull
    val geomFactory = GeometryFactory()
    val multiPoint = geomFactory.createMultiPointFromCoords(coords.toTypedArray())
    val hull = multiPoint.convexHull()

    // Extract hull vertices and find their indices
    val hullCoords = hull.coordinates
    val hullPoints = mutableListOf<Vec2>()
    val hullIndices = mutableListOf<Int>()

    for (hullCoord in hullCoords) {
        // Skip the duplicate closing point
        if (hullPoints.isNotEmpty() &&
            hullCoord.equals2D(hullCoords[0])) {
            continue
        }

        val idx = coords.indexOfFirst { it.equals2D(hullCoord) }
        if (idx >= 0) {
            hullPoints.add(points[idx])
            hullIndices.add(idx)
        }
    }

    return ConvexHull2D(hullPoints, hullIndices)
}

fun vec2ConvexHullGraph

Compute a graph connecting convex hull points in order.

Creates edges along the boundary of the convex hull.

@param points List of 2D points @param options Graph connectivity options @return PointGraph with convex hull boundary edges

fun vec2ConvexHullGraph(
    points: List<Vec2>,
    options: GraphConnectivityOptions = GraphConnectivityOptions()
): PointGraph {
    require(points.isNotEmpty()) { "vec2ConvexHullGraph: points must not be empty" }
    validatePoints2D(points, "vec2ConvexHullGraph")

    val hull = vec2ConvexHull(points)
    val edges = mutableListOf<GraphEdge>()

    // Connect consecutive hull points
    for (i in hull.hullIndices.indices) {
        val curr = hull.hullIndices[i]
        val next = hull.hullIndices[(i + 1) % hull.hullIndices.size]

        val dist = computeDistance2D(
            points[curr],
            points[next],
            options,
            curr,
            next,
            points
        )
        edges.add(GraphEdge(curr, next, dist))
    }

    return PointGraph(edges, points.size)
}

fun vec2DelaunayTriangles(points: List<Vec2>): List<Triple<Int, Int, Int>>

Compute triangles from Delaunay triangulation.

@param points List of 2D points @return List of triangles, where each triangle is a triple of point indices

fun vec2DelaunayTriangles(points: List<Vec2>): List<Triple<Int, Int, Int>> {
    require(points.isNotEmpty()) { "vec2DelaunayTriangles: points must not be empty" }
    validatePoints2D(points, "vec2DelaunayTriangles")

    if (points.size < 3) return emptyList()

    // Convert to JTS coordinates
    val coords = points.map { Coordinate(it.x.toDouble(), it.y.toDouble()) }

    // Build Delaunay triangulation
    val builder = DelaunayTriangulationBuilder()
    builder.setSites(coords)
    val triangles = builder.getTriangles(GeometryFactory())

    // Extract triangles
    val result = mutableListOf<Triple<Int, Int, Int>>()

    for (i in 0 until triangles.numGeometries) {
        val triangle = triangles.getGeometryN(i)
        val triCoords = triangle.coordinates

        if (triCoords.size >= 4) {  // Triangle has 4 coords (last = first)
            val idx0 = coords.indexOfFirst { it.equals2D(triCoords[0]) }
            val idx1 = coords.indexOfFirst { it.equals2D(triCoords[1]) }
            val idx2 = coords.indexOfFirst { it.equals2D(triCoords[2]) }

            if (idx0 >= 0 && idx1 >= 0 && idx2 >= 0) {
                result.add(Triple(idx0, idx1, idx2))
            }
        }
    }

    return result
}

fun vec2HullProjectionGraph

Compute the nearest point on the convex hull for each interior point.

@param points List of 2D points @param options Graph connectivity options @return PointGraph connecting each interior point to its nearest hull point

fun vec2HullProjectionGraph(
    points: List<Vec2>,
    options: GraphConnectivityOptions = GraphConnectivityOptions()
): PointGraph {
    require(points.isNotEmpty()) { "vec2HullProjectionGraph: points must not be empty" }
    validatePoints2D(points, "vec2HullProjectionGraph")

    val hull = vec2ConvexHull(points)
    val hullSet = hull.hullIndices.toSet()
    val edges = mutableListOf<GraphEdge>()

    // For each non-hull point, connect to nearest hull point
    for (i in points.indices) {
        if (i in hullSet) continue

        var nearestHullIdx = hull.hullIndices[0]
        var minDist = Double.POSITIVE_INFINITY

        for (hullIdx in hull.hullIndices) {
            val dist = computeDistance2D(
                points[i],
                points[hullIdx],
                options,
                i,
                hullIdx,
                points
            )
            if (dist < minDist) {
                minDist = dist
                nearestHullIdx = hullIdx
            }
        }

        edges.add(GraphEdge(i, nearestHullIdx, minDist))
    }

    return PointGraph(edges, points.size)
}