package aima.core.search.uninformed;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import aima.core.agent.Action;
import aima.core.search.framework.BidirectionalProblem;
import aima.core.search.framework.GraphSearch;
import aima.core.search.framework.Metrics;
import aima.core.search.framework.Node;
import aima.core.search.framework.Problem;
import aima.core.search.framework.Search;
import aima.core.search.framework.SearchUtils;
import aima.core.util.datastructure.FIFOQueue;
/**
* Artificial Intelligence A Modern Approach (3rd Edition): page 90.<br>
* <br>
* Bidirectional search.<br>
* <br>
* <b>Note:</b> Based on the description of this algorithm i.e. 'Bidirectional
* search is implemented by replacing the goal test with a check to see whether
* the frontiers of the two searches intersect;', it is possible for the
* searches to pass each other's frontiers by, in particular if the problem is
* not fully reversible (i.e. unidirectional links on a graph), and could
* instead intersect at the explored set.
*
* @author Ciaran O'Reilly
* @author Mike Stampone
*/
public class BidirectionalSearch implements Search {
public enum SearchOutcome {
PATH_FOUND_FROM_ORIGINAL_PROBLEM, PATH_FOUND_FROM_REVERSE_PROBLEM, PATH_FOUND_BETWEEN_PROBLEMS, PATH_NOT_FOUND
};
protected Metrics metrics;
private SearchOutcome searchOutcome = SearchOutcome.PATH_NOT_FOUND;
private static final String NODES_EXPANDED = "nodesExpanded";
private static final String QUEUE_SIZE = "queueSize";
private static final String MAX_QUEUE_SIZE = "maxQueueSize";
private static final String PATH_COST = "pathCost";
public BidirectionalSearch() {
metrics = new Metrics();
}
public List<Action> search(Problem p) throws Exception {
assert (p instanceof BidirectionalProblem);
searchOutcome = SearchOutcome.PATH_NOT_FOUND;
clearInstrumentation();
Problem op = ((BidirectionalProblem) p).getOriginalProblem();
Problem rp = ((BidirectionalProblem) p).getReverseProblem();
CachedStateQueue<Node> opFrontier = new CachedStateQueue<Node>();
CachedStateQueue<Node> rpFrontier = new CachedStateQueue<Node>();
GraphSearch ogs = new GraphSearch();
GraphSearch rgs = new GraphSearch();
// Ensure the instrumentation for these
// are cleared down as their values
// are used in calculating the overall
// bidirectional metrics.
ogs.clearInstrumentation();
rgs.clearInstrumentation();
Node opNode = new Node(op.getInitialState());
Node rpNode = new Node(rp.getInitialState());
opFrontier.insert(opNode);
rpFrontier.insert(rpNode);
setQueueSize(opFrontier.size() + rpFrontier.size());
setNodesExpanded(ogs.getNodesExpanded() + rgs.getNodesExpanded());
while (!(opFrontier.isEmpty() && rpFrontier.isEmpty())) {
// Determine the nodes to work with and expand their fringes
// in preparation for testing whether or not the two
// searches meet or one or other is at the GOAL.
if (!opFrontier.isEmpty()) {
opNode = opFrontier.pop();
opFrontier.addAll(ogs.getResultingNodesToAddToFrontier(opNode,
op));
} else {
opNode = null;
}
if (!rpFrontier.isEmpty()) {
rpNode = rpFrontier.pop();
rpFrontier.addAll(rgs.getResultingNodesToAddToFrontier(rpNode,
rp));
} else {
rpNode = null;
}
setQueueSize(opFrontier.size() + rpFrontier.size());
setNodesExpanded(ogs.getNodesExpanded() + rgs.getNodesExpanded());
//
// First Check if either frontier contains the other's state
if (null != opNode && null != rpNode) {
Node popNode = null;
Node prpNode = null;
if (opFrontier.containsNodeBasedOn(rpNode.getState())) {
popNode = opFrontier.getNodeBasedOn(rpNode.getState());
prpNode = rpNode;
} else if (rpFrontier.containsNodeBasedOn(opNode.getState())) {
popNode = opNode;
prpNode = rpFrontier.getNodeBasedOn(opNode.getState());
// Need to also check whether or not the nodes that
// have been taken off the frontier actually represent the
// same state, otherwise there are instances whereby
// the searches can pass each other by
} else if (opNode.getState().equals(rpNode.getState())) {
popNode = opNode;
prpNode = rpNode;
}
if (null != popNode && null != prpNode) {
List<Action> actions = retrieveActions(op, rp, popNode,
prpNode);
// It may be the case that it is not in fact possible to
// traverse from the original node to the goal node based on
// the reverse path (i.e. unidirectional links: e.g.
// InitialState(A)<->C<-Goal(B) )
if (null != actions) {
return actions;
}
}
}
//
// Check if the original problem is at the GOAL state
if (null != opNode && SearchUtils.isGoalState(op, opNode)) {
// No need to check return value for null here
// as an action path discovered from the goal
// is guaranteed to exist
return retrieveActions(op, rp, opNode, null);
}
//
// Check if the reverse problem is at the GOAL state
if (null != rpNode && SearchUtils.isGoalState(rp, rpNode)) {
List<Action> actions = retrieveActions(op, rp, null, rpNode);
// It may be the case that it is not in fact possible to
// traverse from the original node to the goal node based on
// the reverse path (i.e. unidirectional links: e.g.
// InitialState(A)<-Goal(B) )
if (null != actions) {
return actions;
}
}
}
// Empty List can indicate already at Goal
// or unable to find valid set of actions
return new ArrayList<Action>();
}
/**
* Returns PATH_FOUND_FROM_ORIGINAL_PROBLEM if the path was found from the
* initial state, PATH_FOUND_FROM_REVERSE_PROBLEM if the path was found from
* a goal, PATH_FOUND_FROM_BETWEEN_PROBLEMS if a branch from the initial
* state met a branch from a goal state, or PATH_NOT_FOUND if no path from
* the initial state to a goal state was found.
*
* @return PATH_FOUND_FROM_ORIGINAL_PROBLEM if the path was found from the
* initial state, PATH_FOUND_FROM_REVERSE_PROBLEM if the path was
* found from a goal, PATH_FOUND_FROM_BETWEEN_PROBLEMS if a branch
* from the initial state met a branch from a goal state, or
* PATH_NOT_FOUND if no path from the initial state to a goal state
* was found.
*/
public SearchOutcome getSearchOutcome() {
return searchOutcome;
}
/**
* Returns all the metrics of the search.
*
* @return all the metrics of the search.
*/
public Metrics getMetrics() {
return metrics;
}
/**
* Sets all metrics to zero.
*/
public void clearInstrumentation() {
metrics.set(NODES_EXPANDED, 0);
metrics.set(QUEUE_SIZE, 0);
metrics.set(MAX_QUEUE_SIZE, 0);
metrics.set(PATH_COST, 0.0);
}
/**
* Returns the number of nodes expanded.
*
* @return the number of nodes expanded.
*/
public int getNodesExpanded() {
return metrics.getInt(NODES_EXPANDED);
}
/**
* Sets the number of nodes expanded.
*
* @param nodesExpanded
* the number of nodes expanded
*/
public void setNodesExpanded(int nodesExpanded) {
metrics.set(NODES_EXPANDED, nodesExpanded);
}
/**
* Returns the queue size.
*
* @return the queue size.
*/
public int getQueueSize() {
return metrics.getInt(QUEUE_SIZE);
}
/**
* Sets the queue size and adjusts the maximum queue size if the specified
* size is greater than the current maximum.
*
* @param queueSize
* the number of items in the queue.
*/
public void setQueueSize(int queueSize) {
metrics.set(QUEUE_SIZE, queueSize);
int maxQSize = metrics.getInt(MAX_QUEUE_SIZE);
if (queueSize > maxQSize) {
metrics.set(MAX_QUEUE_SIZE, queueSize);
}
}
/**
* Returns the maximum queue size.
*
* @return the maximum queue size.
*/
public int getMaxQueueSize() {
return metrics.getInt(MAX_QUEUE_SIZE);
}
/**
* Returns the cost of the path from the initial state to a goal state.
*
* @return the cost of the path from the initial state to a goal state.
*/
public double getPathCost() {
return metrics.getDouble(PATH_COST);
}
/**
* Sets the cost of the path from the initial state to a goal state.
*
* @param pathCost
* the cost of the path from the initial state to a goal state.
*/
public void setPathCost(Double pathCost) {
metrics.set(PATH_COST, pathCost);
}
//
// PRIVATE METHODS
//
private List<Action> retrieveActions(Problem op, Problem rp,
Node originalPath, Node reversePath) {
List<Action> actions = new ArrayList<Action>();
if (null == reversePath) {
// This is the simple case whereby the path has been found
// from the original problem first
setPathCost(originalPath.getPathCost());
searchOutcome = SearchOutcome.PATH_FOUND_FROM_ORIGINAL_PROBLEM;
actions = SearchUtils.actionsFromNodes(originalPath
.getPathFromRoot());
} else {
List<Node> nodePath = new ArrayList<Node>();
Object originalState = null;
if (null != originalPath) {
nodePath.addAll(originalPath.getPathFromRoot());
originalState = originalPath.getState();
}
// Only append the reverse path if it is not the
// GOAL state from the original problem (if you don't
// you could end up appending a partial reverse path
// that looks back on its initial state)
if (!SearchUtils.isGoalState(op, reversePath)) {
List<Node> rpath = reversePath.getPathFromRoot();
for (int i = rpath.size() - 1; i >= 0; i--) {
// Ensure do not include the node from the reverse path
// that is the one that potentially overlaps with the
// original path (i.e. if started in goal state or where
// they meet in the middle).
if (!rpath.get(i).getState().equals(originalState)) {
nodePath.add(rpath.get(i));
}
}
}
if (!canTraversePathFromOriginalProblem(op, nodePath, actions)) {
// This is where it is possible to get to the initial state
// from the goal state (i.e. reverse path) but not the other way
// round, null returned to indicate an invalid path found from
// the reverse problem
return null;
}
if (null == originalPath) {
searchOutcome = SearchOutcome.PATH_FOUND_FROM_REVERSE_PROBLEM;
} else {
// Need to ensure that where the original and reverse paths
// overlap, as they can link based on their fringes, that
// the reverse path is actually capable of connecting to
// the previous node in the original path (if not root).
if (canConnectToOriginalFromReverse(rp, originalPath,
reversePath)) {
searchOutcome = SearchOutcome.PATH_FOUND_BETWEEN_PROBLEMS;
} else {
searchOutcome = SearchOutcome.PATH_FOUND_FROM_ORIGINAL_PROBLEM;
}
}
}
return actions;
}
private boolean canTraversePathFromOriginalProblem(Problem op,
List<Node> path, List<Action> actions) {
boolean rVal = true;
double pc = 0.0;
for (int i = 0; i < (path.size() - 1); i++) {
Object currentState = path.get(i).getState();
Object nextState = path.get(i + 1).getState();
boolean found = false;
for (Action a : op.getActionsFunction().actions(currentState)) {
Object isNext = op.getResultFunction().result(currentState, a);
if (nextState.equals(isNext)) {
found = true;
pc += op.getStepCostFunction()
.c(currentState, a, nextState);
actions.add(a);
break;
}
}
if (!found) {
rVal = false;
break;
}
}
setPathCost(true == rVal ? pc : 0.0);
return rVal;
}
private boolean canConnectToOriginalFromReverse(Problem rp,
Node originalPath, Node reversePath) {
boolean rVal = true;
// Only need to test if not already at root
if (!originalPath.isRootNode()) {
rVal = false;
for (Action a : rp.getActionsFunction().actions(
reversePath.getState())) {
Object nextState = rp.getResultFunction().result(
reversePath.getState(), a);
if (originalPath.getParent().getState().equals(nextState)) {
rVal = true;
break;
}
}
}
return rVal;
}
}
class CachedStateQueue<E> extends FIFOQueue<E> {
private static final long serialVersionUID = 1;
//
private Map<Object, Node> cachedState = new HashMap<Object, Node>();
public CachedStateQueue() {
super();
}
public CachedStateQueue(Collection<? extends E> c) {
super(c);
}
public boolean containsNodeBasedOn(Object state) {
return cachedState.containsKey(state);
}
public Node getNodeBasedOn(Object state) {
return cachedState.get(state);
}
//
// START-Queue
public E pop() {
E popped = super.pop();
cachedState.remove(((Node) popped).getState());
return popped;
}
// END-Queue
//
// Note: This is called by FIFOQueue.insert()->LinkedList.offer();
@Override
public boolean add(E element) {
boolean added = super.add(element);
if (added) {
cachedState.put(((Node) element).getState(), (Node) element);
}
return added;
}
@Override
public boolean addAll(Collection<? extends E> c) {
for (E element : c) {
cachedState.put(((Node) element).getState(), (Node) element);
}
return super.addAll(c);
}
}