public Path <TGraphNode> FindPath( TGraphNode sourceNode, TGraphNode targetNode, IEqualityComparer <TGraphNode> targetEqualityComparer, IEqualityComparer <TGraphNode> nodeEqualityComparer ) { // If there is a limit on search time, we need to initialize the timer var timer = new StopwatchExecutionTimer(); // Set of already visited nodes var closed = new HashSet <TGraphNode>(nodeEqualityComparer); // Priority queue of partial and potentially complete paths var open = new SmartPriorityQueue <PartialPath, double> { new PartialPath(heuristic(sourceNode, targetNode)) }; // Best known path to target in the open priority queue PartialPath bestPathToTarget = null; // Cost of the cheapest known path to target var minTotalCost = double.PositiveInfinity; // While there are unexplored nodes while (open.Count > 0) { // Pop partial path with the lowest estimated cost from the priority queue var currentPartialPath = open.PopFront(); // Node to explore // If partial path is empty, we are at the source of our search. var currentNode = currentPartialPath.IsEmpty ? sourceNode : currentPartialPath.LastNode; if (closed.Contains(currentNode)) { continue; } if (targetEqualityComparer.Equals(currentNode, targetNode)) { UnityEngine.Debug.Log( $"Path found of cost {currentPartialPath.CostSoFar} : {currentPartialPath.EdgeCount} edges; {closed.Count} nodes expanded" ); return(currentPartialPath.ToPath()); } // If there is a time limit, check the timer. // If the time limit has been exceeded, return the best known path to target or, // otherwise break and report a failure. if (maxSecondsPerSearch < float.PositiveInfinity && timer.ElapsedSeconds > maxSecondsPerSearch) { if (bestPathToTarget != null) { // Time out, best path so far UnityEngine.Debug.Log( $"Time out! Path found of cost {currentPartialPath.CostSoFar} : {currentPartialPath.EdgeCount} edges" ); return(bestPathToTarget.ToPath()); } throw new PathfindingTimeoutException(GetType(), maxSecondsPerSearch); } // If we have a limit on max search depth, and current path is at max depth limit, // don't add paths to currentNode's neighbours to the open priority queue if (maxSearchDepth >= 0 && currentPartialPath.EdgeCount >= maxSearchDepth) { continue; } if (assumeNonNegativeCosts && currentPartialPath.CostSoFar > minTotalCost) { continue; } // Mark currentNode as visited by placing it into closed set. closed.Add(currentNode); // Insert paths from sourceNode to currentNode's neighbours into the open priority queue foreach (var outgoingEdge in currentNode.OutgoingEdges) { var neighbour = outgoingEdge.TargetNode; var pathToNeighbour = new PartialPath(currentPartialPath); pathToNeighbour.AppendEdge(outgoingEdge, heuristic(neighbour, targetNode)); open.Add(pathToNeighbour); // If there is a time limit, check if the neighbour is the target node // and update bestPathToTarget, if needed. // This allows us to keep track of the best path to target, found so far. if (maxSecondsPerSearch < float.PositiveInfinity && targetEqualityComparer.Equals(neighbour, targetNode) && (bestPathToTarget == null || pathToNeighbour.EstimatedTotalCost < bestPathToTarget.CostSoFar)) { bestPathToTarget = pathToNeighbour; } // If the assumption of non-negative costs is in effect, check if the neighbour // is the target node and update minTotalCost, if needed. if (assumeNonNegativeCosts && targetEqualityComparer.Equals(neighbour, targetNode)) { minTotalCost = Math.Min(minTotalCost, pathToNeighbour.CostSoFar); } } } UnityEngine.Debug.Log("No plan found"); throw new NoPathExistsException( GetType(), maxSearchDepth ); }
/// <inheritdoc /> public Path <GraphNode> FindPath( GraphNode sourceNode, GraphNode targetNode, IEqualityComparer <GraphNode> targetEqualityComparer, IEqualityComparer <GraphNode> nodeEqualityComparer ) { // If there is a limit on search time, we need to initialize the timer var timer = new StopwatchExecutionTimer(); // Set of already visited nodes var closed = new HashSet <GraphNode>(nodeEqualityComparer); // Priority queue of partial and potentially complete paths var open = new ListBasedPriorityQueue <PartialPath>(); open.Add(new PartialPath(this.heuristic(sourceNode, targetNode))); // Best known path to target in the open priority queue // (Used as a fallback, if time limit is exceeded before the search ends) PartialPath bestPathToTarget = null; // Cost of the cheapest known path to target // (Used to discard costlier paths, if the assumption of non-negative edge costs is in effect) // double minTotalCost = double.PositiveInfinity; // While there are unexplored nodes while (open.Count > 0) { // Pop partial path with the lowest estimated cost from the priority queue var currentPartialPath = open.PopFront(); // Node to explore GraphNode currentNode; // If partial path is empty (and therefore has no last node to speak of), // it means we are at the source of our search. if (currentPartialPath.IsEmpty) { // Explore the source node currentNode = sourceNode; } else { // Explore the last node of the current partial path currentNode = currentPartialPath.LastNode; } // If the currently examined node has been examined previously, don't consider it further. // This has the effect of discarding the partial path, which led to it, from the open priority queue. // FIXME: As far as I understand, this is correct behaviour only if the heuristic is admissable! // In general case (i.e. with inadmissable heuristics), closed nodes may need to be reopened. // To control this behaviour an additional flag may be added to the constructor and checked here. if (closed.Contains(currentNode)) { continue; } // Will only be called if ASTAR_VALIDATE_NODE_HASH_IMPLEMENTATION is defined ValidateNodeHashImplementation(closed, currentNode); // If the currently examined node is the target if (targetEqualityComparer.Equals(currentNode, targetNode)) { // Hurray! return(currentPartialPath.ToPath()); } // If there is a time limit, check the timer. // If the time limit has been exceeded, return the best known path to target or, // if there has been no such path discovered, break and report a failure. if (this.maxSecondsPerSearch < float.PositiveInfinity && timer.ElapsedSeconds > this.maxSecondsPerSearch) { if (bestPathToTarget != null) { // "Good enough" return(bestPathToTarget.ToPath()); } else { throw new PathfindingTimeoutException(this.GetType(), this.maxSecondsPerSearch); } } // If we have a limit on max search depth, and current path is at max depth limit, // don't add paths to currentNode's neighbours to the open priority queue, // since those paths will exceed the max search depth, and don't place currentNode // into closed set, since there might be a more expensive path with smaller edge count, // which leads to it without exceeding the max seatch depth limit. if (this.maxSearchDepth >= 0 && currentPartialPath.EdgeCount >= this.maxSearchDepth) { continue; } // FIXME: Remove the commented out code. // The following check makes no sense: priority queue already guarantees that // the current path is at most as expensive as the best known path to target. // Maybe if it checked for >=, instead of >, i.e. assumed positive and not merely non-negative costs, // it would have miniscule effect, but even so the benefits would likely be negligible. // If the assumption of non-negative costs is in effect, we can discard the current path // from further consideration, if it's already costlier than the cheapest known path // that reaches the target node, because if all the edges have non-negative cost, // we can be certain that it will not get any cheaper. // (At this point we know that the current path does not reach the target, // otherwise we would've already returned it earlier in this loop iteration.) // if(this.assumeNonNegativeCosts && currentPartialPath.CostSoFar > minTotalCost) // { // continue; // } // Mark currentNode as visited by placing it into closed set. closed.Add(currentNode); // Insert paths from sourceNode to currentNode's neighbours into the open priority queue foreach (var outgoingEdge in currentNode.OutgoingEdges) { var neighbour = outgoingEdge.TargetNode; var pathToNeighbour = new PartialPath(currentPartialPath); pathToNeighbour.AppendEdge(outgoingEdge, this.heuristic(neighbour, targetNode)); open.Add(pathToNeighbour); // If there is a time limit, check if the neighbour is the target node // and update bestPathToTarget, if needed. // This allows us to keep track of the best path to target, found so far. if (this.maxSecondsPerSearch < float.PositiveInfinity && targetEqualityComparer.Equals(neighbour, targetNode) // Theoretically, EstimatedTotalCost could be used here, instead of CostSoFar, // since heuristic SHOULD return 0 for target node. // However, this behaviour is not enforced, and so CostSoFar is used for reliability. && (bestPathToTarget == null || pathToNeighbour.CostSoFar < bestPathToTarget.CostSoFar)) { bestPathToTarget = pathToNeighbour; } // If the assumption of non-negative costs is in effect, check if the neighbour // is the target node and update minTotalCost, if needed. // if(this.assumeNonNegativeCosts && targetEqualityComparer.Equals(neighbour, targetNode)) // { // // Theoretically, EstimatedTotalCost could be used here, instead of CostSoFar, // // since heuristic SHOULD return 0 for target node. // // However, this behaviour is not enforced, and so CostSoFar is used for reliability. // minTotalCost = Math.Min(minTotalCost, pathToNeighbour.CostSoFar); // } } } // Found no path to targetNode throw new NoPathExistsException( this.GetType(), this.maxSearchDepth != AstarPathfinderConfiguration <GraphNode> .UNLIMITED_SEARCH_DEPTH ? (int?)this.maxSearchDepth : (int?)null ); }