@@ -30,6 +30,7 @@ import com.lambda.util.math.times
3030import com.lambda.util.world.FastVector
3131import com.lambda.util.world.string
3232import com.lambda.util.world.toCenterVec3d
33+ import kotlin.math.abs
3334import 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