Skip to content

Commit 19daf8a

Browse files
committed
Proper consistency test
1 parent 5257bc8 commit 19daf8a

File tree

4 files changed

+163
-376
lines changed

4 files changed

+163
-376
lines changed

common/src/main/kotlin/com/lambda/pathing/dstar/DStarLite.kt

Lines changed: 25 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ class DStarLite(
151151
* @param u The node to invalidate
152152
* @param pruneGraph Whether to prune the graph after invalidation
153153
*/
154-
fun invalidate(u: FastVector, pruneGraph: Boolean = false) {
154+
fun invalidate(u: FastVector, pruneGraph: Boolean = true) {
155155
val newNodes = mutableSetOf<FastVector>()
156156
val affectedNeighbors = mutableSetOf<FastVector>()
157157
val pathNodes = mutableSetOf<FastVector>()
@@ -442,60 +442,43 @@ class DStarLite(
442442
/**
443443
* Verifies that the current graph is consistent with a freshly generated graph.
444444
* This is useful for ensuring that incremental updates maintain correctness.
445-
*
446-
* @param nodeInitializer The function used to initialize nodes in the fresh graph
447-
* @param blockedNodes Set of nodes that should be blocked in the fresh graph
448-
* @return A pair of (consistency percentage, g/rhs consistency percentage)
449445
*/
450-
fun verifyGraphConsistency(
451-
nodeInitializer: (FastVector) -> Map<FastVector, Double>,
452-
blockedNodes: Set<FastVector> = emptySet()
453-
): Pair<Double, Double> {
454-
// Create a fresh graph with the same initialization function
455-
val freshGraph = LazyGraph(nodeInitializer)
456-
457-
// Initialize the fresh graph with the same start and goal
458-
val freshDStar = DStarLite(freshGraph, start, goal, heuristic)
459-
460-
// Block nodes in the fresh graph
461-
blockedNodes.forEach { node ->
462-
freshDStar.invalidate(node, pruneGraph = false)
463-
}
464-
465-
// Compute shortest path on the fresh graph
466-
freshDStar.computeShortestPath()
467-
446+
fun compareWith(
447+
other: DStarLite
448+
): Pair<LazyGraph.GraphDifferences, Set<ValueDifference>> {
468449
// Compare edge consistency between the two graphs
469-
val edgeConsistency = graph.compareWith(freshGraph)
450+
val graphDifferences = graph.compareWith(other.graph)
470451

471452
// Compare g and rhs values for common nodes
472-
val commonNodes = graph.nodes.intersect(freshGraph.nodes)
473-
var consistentValues = 0
453+
val commonNodes = graph.nodes.intersect(other.graph.nodes)
454+
val wrong = mutableSetOf<ValueDifference>()
474455

475456
commonNodes.forEach { node ->
476457
val g1 = g(node)
477-
val g2 = freshDStar.g(node)
478-
val rhs1 = rhs(node)
479-
val rhs2 = freshDStar.rhs(node)
458+
val g2 = other.g(node)
480459

481-
// Check if g and rhs values are consistent
482-
val gConsistent = (g1.isInfinite() && g2.isInfinite()) ||
483-
(g1.isFinite() && g2.isFinite() && abs(g1 - g2) < 0.001)
484-
val rhsConsistent = (rhs1.isInfinite() && rhs2.isInfinite()) ||
485-
(rhs1.isFinite() && rhs2.isFinite() && abs(rhs1 - rhs2) < 0.001)
460+
if (abs(g1 - g2) > 1e-6) {
461+
wrong.add(ValueDifference(ValueDifference.Value.G, g1, g2))
462+
}
486463

487-
if (gConsistent && rhsConsistent) {
488-
consistentValues++
464+
val rhs1 = rhs(node)
465+
val rhs2 = other.rhs(node)
466+
467+
if (abs(rhs1 - rhs2) > 1e-6) {
468+
wrong.add(ValueDifference(ValueDifference.Value.RHS, rhs1, rhs2))
489469
}
490470
}
491471

492-
val valueConsistency = if (commonNodes.isNotEmpty()) {
493-
(consistentValues.toDouble() / commonNodes.size) * 100
494-
} else {
495-
100.0
496-
}
472+
return Pair(graphDifferences, wrong)
473+
}
497474

498-
return Pair(edgeConsistency, valueConsistency)
475+
data class ValueDifference(
476+
val type: Value,
477+
val v1: Double,
478+
val v2: Double,
479+
) {
480+
enum class Value { G, RHS }
481+
override fun toString() = "${type.name} is $v1 but should be $v2"
499482
}
500483

501484
override fun toString() = buildString {

common/src/main/kotlin/com/lambda/pathing/dstar/LazyGraph.kt

Lines changed: 69 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import com.lambda.graphics.renderer.esp.builders.buildOutline
2222
import com.lambda.graphics.renderer.esp.global.StaticESP
2323
import com.lambda.pathing.PathingSettings
2424
import com.lambda.util.world.FastVector
25+
import com.lambda.util.world.string
2526
import com.lambda.util.world.toCenterVec3d
2627
import net.minecraft.util.math.Box
2728
import java.awt.Color
@@ -179,44 +180,82 @@ class LazyGraph(
179180

180181
operator fun contains(u: FastVector): Boolean = nodes.contains(u)
181182

183+
/**
184+
* Result of a graph comparison containing categorized edge differences
185+
*/
186+
data class GraphDifferences(
187+
val missingEdges: Set<Edge>, // Edges that should exist but don't
188+
val wrongEdges: Set<Edge>, // Edges that exist but have incorrect costs
189+
val excessEdges: Set<Edge> // Edges that shouldn't exist but do
190+
) {
191+
/**
192+
* Represents an edge difference between two graphs
193+
*/
194+
data class Edge(
195+
val source: FastVector,
196+
val target: FastVector,
197+
val thisGraphCost: Double?, // null if edge doesn't exist in this graph
198+
val otherGraphCost: Double? // null if edge doesn't exist in other graph
199+
) {
200+
override fun toString(): String = when {
201+
thisGraphCost == null -> "Edge from ${source.string} to ${target.string} with cost $otherGraphCost"
202+
otherGraphCost == null -> "Edge from ${source.string} to ${target.string} with cost $thisGraphCost"
203+
else -> "Edge from ${source.string} to ${target.string} (cost: $thisGraphCost vs $otherGraphCost)"
204+
}
205+
}
206+
207+
val hasAnyDifferences: Boolean
208+
get() = missingEdges.isNotEmpty() || wrongEdges.isNotEmpty() /*|| excessEdges.isNotEmpty()*/
209+
210+
override fun toString(): String {
211+
val parts = mutableListOf<String>()
212+
if (missingEdges.isNotEmpty()) {
213+
parts.add("Missing edges: ${missingEdges.joinToString("\n ", prefix = "\n ")}")
214+
}
215+
if (wrongEdges.isNotEmpty()) {
216+
parts.add("Wrong edges: ${wrongEdges.joinToString("\n ", prefix = "\n ")}")
217+
}
218+
if (excessEdges.isNotEmpty()) {
219+
parts.add("Excess edges: ${excessEdges.joinToString("\n ", prefix = "\n ")}")
220+
}
221+
return if (parts.isEmpty()) "No differences" else parts.joinToString("\n")
222+
}
223+
}
224+
182225
/**
183226
* Compares this graph with another graph for edge consistency.
184-
* Returns the percentage of edges that are consistent between the two graphs.
185-
*
227+
*
186228
* @param other The other graph to compare with
187-
* @return The percentage of consistent edges (0-100)
229+
* @return Categorized edge differences between the two graphs
188230
*/
189-
fun compareWith(other: LazyGraph): Double {
190-
val commonNodes = this.nodes.intersect(other.nodes)
191-
var consistentEdges = 0
192-
var totalEdges = 0
193-
194-
commonNodes.forEach { node1 ->
195-
commonNodes.forEach { node2 ->
196-
if (node1 != node2) {
197-
totalEdges++
198-
val cost1 = this.cost(node1, node2)
199-
val cost2 = other.cost(node1, node2)
200-
201-
// Check if costs are consistent
202-
if (cost1.isInfinite() && cost2.isInfinite()) {
203-
// Both infinite, they're consistent
204-
consistentEdges++
205-
} else if (cost1.isFinite() && cost2.isFinite()) {
206-
// Both finite, check if they're close enough
207-
if (abs(cost1 - cost2) < 0.001) {
208-
consistentEdges++
209-
}
210-
}
231+
fun compareWith(other: LazyGraph): GraphDifferences {
232+
val missing = mutableSetOf<GraphDifferences.Edge>()
233+
val wrong = mutableSetOf<GraphDifferences.Edge>()
234+
val excess = mutableSetOf<GraphDifferences.Edge>()
235+
236+
nodes.union(other.nodes).forEach { node ->
237+
val thisSuccessors = getSuccessorsWithoutInitializing(node)
238+
val otherSuccessors = other.getSuccessorsWithoutInitializing(node)
239+
240+
// Check for missing and wrong edges
241+
otherSuccessors.forEach { (neighbor, otherCost) ->
242+
val thisCost = thisSuccessors[neighbor]
243+
if (thisCost == null) {
244+
missing.add(GraphDifferences.Edge(node, neighbor, null, otherCost))
245+
} else if (abs(thisCost - otherCost) > 1e-9) {
246+
wrong.add(GraphDifferences.Edge(node, neighbor, thisCost, otherCost))
211247
}
212248
}
213-
}
214249

215-
return if (totalEdges > 0) {
216-
(consistentEdges.toDouble() / totalEdges) * 100
217-
} else {
218-
100.0
250+
// Check for excess edges
251+
thisSuccessors.forEach { (neighbor, thisCost) ->
252+
if (!otherSuccessors.containsKey(neighbor)) {
253+
excess.add(GraphDifferences.Edge(node, neighbor, thisCost, null))
254+
}
255+
}
219256
}
257+
258+
return GraphDifferences(missing, wrong, excess)
220259
}
221260

222261
fun render(renderer: StaticESP, config: PathingSettings) {
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Copyright 2025 Lambda
3+
*
4+
* This program is free software: you can redistribute it and/or modify
5+
* it under the terms of the GNU General Public License as published by
6+
* the Free Software Foundation, either version 3 of the License, or
7+
* (at your option) any later version.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12+
* GNU General Public License for more details.
13+
*
14+
* You should have received a copy of the GNU General Public License
15+
* along with this program. If not, see <http://www.gnu.org/licenses/>.
16+
*/
17+
18+
package pathing
19+
20+
import com.lambda.pathing.dstar.DStarLite
21+
import com.lambda.util.GraphUtil.createGridGraph6Conn
22+
import com.lambda.util.GraphUtil.euclideanHeuristic
23+
import com.lambda.util.world.FastVector
24+
import com.lambda.util.world.fastVectorOf
25+
import com.lambda.util.world.string
26+
import kotlin.test.Test
27+
import kotlin.test.assertEquals
28+
import kotlin.test.assertFalse
29+
import kotlin.test.assertTrue
30+
31+
/**
32+
* Tests for graph maintenance consistency in the D* Lite algorithm.
33+
* These tests verify that the graph state after invalidation and pruning
34+
* is consistent with a fresh graph created with the same blocked nodes.
35+
*/
36+
class GraphConsistencyTest {
37+
@Test
38+
fun `graph consistency N6`() {
39+
val startNode = fastVectorOf(0, 0, 5)
40+
val goalNode = fastVectorOf(0, 0, 0)
41+
val blockedNodes = mutableSetOf<FastVector>()
42+
val graph1 = createGridGraph6Conn(blockedNodes)
43+
val dStar1 = DStarLite(graph1, startNode, goalNode, ::euclideanHeuristic)
44+
dStar1.computeShortestPath()
45+
val initialPath = dStar1.path()
46+
47+
val blockedNode = fastVectorOf(0, 0, 4)
48+
blockedNodes.add(blockedNode)
49+
50+
dStar1.invalidate(blockedNode)
51+
dStar1.computeShortestPath()
52+
val path1 = dStar1.path()
53+
54+
assertTrue(initialPath.size < path1.size, "Initial path size (${initialPath.size}) is less than blocked path size (${path1.size})")
55+
56+
val graph2 = createGridGraph6Conn(blockedNodes)
57+
val dStar2 = DStarLite(graph2, startNode, goalNode, ::euclideanHeuristic)
58+
dStar2.computeShortestPath()
59+
val path2 = dStar2.path()
60+
61+
assertEquals(path1, path2, "Graph consistency test failed for N6 graph with blocked node at ${blockedNode.string}.\nPath1: ${path1.string()}\nPath2: ${path2.string()}")
62+
63+
val (graphDifferences, valueDifferences) = dStar1.compareWith(dStar2)
64+
assertFalse(graphDifferences.hasAnyDifferences, graphDifferences.toString())
65+
assertFalse(valueDifferences.isNotEmpty(), valueDifferences.joinToString("\n "))
66+
}
67+
68+
private fun List<FastVector>.string() = joinToString(" -> ") { it.string }
69+
}

0 commit comments

Comments
 (0)