Skip to content

Commit 5257bc8

Browse files
committed
Graph pruning on invalidation, graph consistency checks
1 parent da2eedb commit 5257bc8

File tree

6 files changed

+738
-109
lines changed

6 files changed

+738
-109
lines changed

common/src/main/kotlin/com/lambda/command/commands/PathCommand.kt

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,13 @@
1717

1818
package com.lambda.command.commands
1919

20+
import com.lambda.brigadier.argument.boolean
2021
import com.lambda.brigadier.argument.double
2122
import com.lambda.brigadier.argument.integer
2223
import com.lambda.brigadier.argument.literal
2324
import com.lambda.brigadier.argument.value
2425
import com.lambda.brigadier.execute
26+
import com.lambda.brigadier.optional
2527
import com.lambda.brigadier.required
2628
import com.lambda.command.LambdaCommand
2729
import com.lambda.module.modules.movement.Pathfinder
@@ -55,11 +57,16 @@ object PathCommand : LambdaCommand(
5557
required(integer("X", -30000000, 30000000)) { x ->
5658
required(integer("Y", -64, 255)) { y ->
5759
required(integer("Z", -30000000, 30000000)) { z ->
58-
execute {
59-
val v = fastVectorOf(x().value(), y().value(), z().value())
60-
Pathfinder.dStar.invalidate(v)
61-
Pathfinder.needsUpdate = true
62-
this@PathCommand.info("Invalidated ${v.string}")
60+
optional(boolean("prune")) { prune ->
61+
execute {
62+
val v = fastVectorOf(x().value(), y().value(), z().value())
63+
val pruneGraph = if (prune != null) {
64+
prune().value()
65+
} else true
66+
Pathfinder.dStar.invalidate(v, pruneGraph = pruneGraph)
67+
Pathfinder.needsUpdate = true
68+
this@PathCommand.info("Invalidated ${v.string}")
69+
}
6370
}
6471
}
6572
}
@@ -159,4 +166,4 @@ object PathCommand : LambdaCommand(
159166
}
160167
}
161168
}
162-
}
169+
}

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

Lines changed: 177 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import com.lambda.util.math.times
3030
import com.lambda.util.world.FastVector
3131
import com.lambda.util.world.string
3232
import com.lambda.util.world.toCenterVec3d
33+
import kotlin.math.abs
3334
import kotlin.math.min
3435

3536
/**
@@ -144,33 +145,139 @@ class DStarLite(
144145

145146
/**
146147
* Invalidates a node (e.g., it became an obstacle) and updates affected neighbors.
148+
* Also updates the neighbors of neighbors to ensure diagonal paths are correctly recalculated.
149+
* Optionally prunes the graph after invalidation to remove unnecessary nodes and edges.
150+
*
151+
* @param u The node to invalidate
152+
* @param pruneGraph Whether to prune the graph after invalidation
147153
*/
148-
fun invalidate(u: FastVector) {
154+
fun invalidate(u: FastVector, pruneGraph: Boolean = false) {
149155
val newNodes = mutableSetOf<FastVector>()
156+
val affectedNeighbors = mutableSetOf<FastVector>()
157+
val pathNodes = mutableSetOf<FastVector>()
158+
val modifiedNodes = mutableSetOf<FastVector>()
150159

151-
graph.neighbors(u).forEach { v ->
160+
// Add the invalidated node to the modified nodes
161+
modifiedNodes.add(u)
162+
163+
// First, collect all neighbors of the invalidated node
164+
val neighbors = graph.neighbors(u)
165+
affectedNeighbors.addAll(neighbors)
166+
modifiedNodes.addAll(neighbors)
167+
168+
// Set g and rhs values of the invalidated node to infinity
169+
setG(u, INF)
170+
setRHS(u, INF)
171+
updateVertex(u)
172+
173+
// Update edges between the invalidated node and its neighbors
174+
neighbors.forEach { v ->
152175
val current = graph.successors(v)
153176
val updated = graph.nodeInitializer(v)
154177
val removed = current.filter { (w, _) -> w !in updated }
155-
updated.keys.filter { w -> w !in current.keys && w != u }.forEach { newNodes.add(it) }
178+
179+
// Only add new nodes that are directly connected to the current path
180+
// This reduces unnecessary node generation
181+
updated.keys.filter { w -> w !in current.keys && w != u }.forEach {
182+
// Check if this node is likely to be on a new path
183+
if (g(v) < INF) {
184+
newNodes.add(it)
185+
modifiedNodes.add(it)
186+
}
187+
}
188+
189+
// Set the edge cost between u and v to infinity (blocked)
156190
updateEdge(u, v, INF)
191+
updateEdge(v, u, INF)
192+
193+
// Update removed and new edges for this neighbor
157194
removed.forEach { (w, _) ->
158195
updateEdge(v, w, INF)
159196
updateEdge(w, v, INF)
197+
modifiedNodes.add(w)
160198
}
161199
updated.forEach { (w, c) ->
162200
updateEdge(v, w, c)
163201
updateEdge(w, v, c)
202+
modifiedNodes.add(w)
203+
}
204+
}
205+
206+
// Now, update only the neighbors of neighbors that are likely to be on the new path
207+
// This is crucial when a node in a diagonal path is blocked
208+
neighbors.forEach { v ->
209+
// Only process neighbors that are likely to be on the path
210+
if (g(v) >= INF) return@forEach
211+
pathNodes.add(v)
212+
213+
// Get all neighbors of this neighbor (excluding the original invalidated node)
214+
// Only consider neighbors that are likely to be on the path
215+
val secondaryNeighbors = graph.neighbors(v).filter { it != u && g(it) < INF }
216+
217+
// For each secondary neighbor, reinitialize its edges
218+
secondaryNeighbors.forEach { w ->
219+
// Add to affected neighbors for later rhs update
220+
affectedNeighbors.add(w)
221+
pathNodes.add(w)
222+
modifiedNodes.add(w)
223+
224+
// Reinitialize edges for this secondary neighbor
225+
val currentW = graph.successors(w)
226+
val updatedW = graph.nodeInitializer(w)
227+
228+
// Update edges for this secondary neighbor
229+
// Only update edges to nodes that are likely to be on the path
230+
updatedW.forEach { (z, c) ->
231+
if (z != u) { // Don't create edges to the invalidated node
232+
updateEdge(w, z, c)
233+
updateEdge(z, w, c)
234+
modifiedNodes.add(z)
235+
236+
// If this node has a finite g-value, it's likely on the path
237+
if (g(z) < INF) {
238+
pathNodes.add(z)
239+
}
240+
}
241+
}
242+
243+
// Add any new nodes discovered, but only if they're likely to be on the path
244+
updatedW.keys.filter { z -> z !in currentW.keys && z != u }.forEach {
245+
// Check if this node is connected to a node on the path
246+
if (g(w) < INF) {
247+
newNodes.add(it)
248+
modifiedNodes.add(it)
249+
}
250+
}
251+
}
252+
}
253+
254+
// Ensure all edges to/from the invalidated node are set to infinity
255+
// First, get all current successors and predecessors of the invalidated node
256+
val currentSuccessors = graph.successors(u).keys.toSet()
257+
val currentPredecessors = graph.predecessors(u).keys.toSet()
258+
259+
// Set all edges to/from the invalidated node to infinity
260+
(currentSuccessors + currentPredecessors + graph.nodes).forEach { node ->
261+
if (node != u) {
262+
updateEdge(node, u, INF)
263+
updateEdge(u, node, INF)
264+
modifiedNodes.add(node)
164265
}
165266
}
166267

167-
// Update rhs values for all new nodes
168-
newNodes.forEach { node ->
268+
// Update rhs values for all affected nodes
269+
(affectedNeighbors + newNodes).forEach { node ->
169270
if (node != goal) {
170271
setRHS(node, minSuccessorCost(node))
171272
updateVertex(node)
172273
}
173274
}
275+
276+
// Prune the graph if requested
277+
if (pruneGraph) {
278+
// Prune the graph, passing the modified nodes for targeted pruning
279+
graph.prune(modifiedNodes)
280+
}
174281
}
175282

176283
/**
@@ -196,6 +303,9 @@ class DStarLite(
196303
* Retrieves a path from start to goal by always choosing the successor
197304
* with the lowest `g(successor) + cost(current, successor)` value.
198305
* If no path is found (INF cost), the path stops early.
306+
*
307+
* @param maxLength The maximum number of nodes to include in the path
308+
* @return A list of nodes representing the path from start to goal
199309
*/
200310
fun path(maxLength: Int = 10_000): List<FastVector> {
201311
val path = mutableListOf<FastVector>()
@@ -329,18 +439,77 @@ class DStarLite(
329439
}
330440
}
331441

442+
/**
443+
* Verifies that the current graph is consistent with a freshly generated graph.
444+
* 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)
449+
*/
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+
468+
// Compare edge consistency between the two graphs
469+
val edgeConsistency = graph.compareWith(freshGraph)
470+
471+
// Compare g and rhs values for common nodes
472+
val commonNodes = graph.nodes.intersect(freshGraph.nodes)
473+
var consistentValues = 0
474+
475+
commonNodes.forEach { node ->
476+
val g1 = g(node)
477+
val g2 = freshDStar.g(node)
478+
val rhs1 = rhs(node)
479+
val rhs2 = freshDStar.rhs(node)
480+
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)
486+
487+
if (gConsistent && rhsConsistent) {
488+
consistentValues++
489+
}
490+
}
491+
492+
val valueConsistency = if (commonNodes.isNotEmpty()) {
493+
(consistentValues.toDouble() / commonNodes.size) * 100
494+
} else {
495+
100.0
496+
}
497+
498+
return Pair(edgeConsistency, valueConsistency)
499+
}
500+
332501
override fun toString() = buildString {
333502
appendLine("D* Lite State:")
334503
appendLine("Start: ${start.string}, Goal: ${goal.string}, k_m: $km")
335504
appendLine("Queue Size: ${U.size()}")
336505
if (!U.isEmpty()) {
337506
appendLine("Top Key: ${U.topKey(Key.INFINITY)}, Top Node: ${U.top().string}")
338507
}
339-
appendLine("Graph Size: ${graph.size}, Invalidated: ${graph.invalidated.size}")
508+
appendLine("Graph Size: ${graph.size}")
340509
appendLine("Known Nodes (${graph.nodes.size}):")
341-
val show = 10
510+
val show = 30
342511
graph.nodes.take(show).forEach {
343-
appendLine(" ${it.string} g: ${g(it)}, rhs: ${rhs(it)}, key: ${calculateKey(it)}")
512+
appendLine(" ${it.string} g: ${"%.2f".format(g(it))}, rhs: ${"%.2f".format(rhs(it))}, key: ${calculateKey(it)}")
344513
}
345514
if (graph.nodes.size > show) appendLine(" ... (${graph.nodes.size - show} more nodes)")
346515
}

0 commit comments

Comments
 (0)