001    package aima.search.uninformed;
002    
003    import java.util.ArrayList;
004    import java.util.List;
005    
006    import aima.search.framework.BidirectionalProblem;
007    import aima.search.framework.GraphSearch;
008    import aima.search.framework.Metrics;
009    import aima.search.framework.Node;
010    import aima.search.framework.Problem;
011    import aima.search.framework.Search;
012    import aima.search.framework.SearchUtils;
013    import aima.search.framework.Successor;
014    import aima.search.nodestore.CachedStateNodeStore;
015    import aima.search.nodestore.FIFONodeStore;
016    
017    /**
018     * Artificial Intelligence A Modern Approach (2nd Edition): page 79.
019     * Bidirectional search.
020     */
021    
022    /**
023     * @author Ciaran O'Reilly
024     * 
025     */
026    public class BidirectionalSearch implements Search {
027            public enum SearchOutcome {
028                    PATH_FOUND_FROM_ORIGINAL_PROBLEM, PATH_FOUND_FROM_REVERSE_PROBLEM, PATH_FOUND_BETWEEN_PROBLEMS, PATH_NOT_FOUND
029            };
030    
031            protected Metrics metrics;
032    
033            private SearchOutcome searchOutcome = SearchOutcome.PATH_NOT_FOUND;
034    
035            private static final String NODES_EXPANDED = "nodesExpanded";
036    
037            private static final String QUEUE_SIZE = "queueSize";
038    
039            private static final String MAX_QUEUE_SIZE = "maxQueueSize";
040    
041            private static final String PATH_COST = "pathCost";
042    
043            public BidirectionalSearch() {
044                    metrics = new Metrics();
045            }
046    
047            public List<String> search(Problem p) throws Exception {
048    
049                    assert (p instanceof BidirectionalProblem);
050    
051                    searchOutcome = SearchOutcome.PATH_NOT_FOUND;
052    
053                    clearInstrumentation();
054    
055                    Problem op = ((BidirectionalProblem) p).getOriginalProblem();
056                    Problem rp = ((BidirectionalProblem) p).getReverseProblem();
057    
058                    CachedStateNodeStore opFringe = new CachedStateNodeStore(
059                                    new FIFONodeStore());
060                    CachedStateNodeStore rpFringe = new CachedStateNodeStore(
061                                    new FIFONodeStore());
062    
063                    GraphSearch ogs = new GraphSearch();
064                    GraphSearch rgs = new GraphSearch();
065                    // Ensure the instrumentation for these
066                    // are cleared down as their values
067                    // are used in calculating the overall
068                    // bidirectional metrics.
069                    ogs.clearInstrumentation();
070                    rgs.clearInstrumentation();
071    
072                    Node opNode = new Node(op.getInitialState());
073                    Node rpNode = new Node(rp.getInitialState());
074                    opFringe.add(opNode);
075                    rpFringe.add(rpNode);
076    
077                    setQueueSize(opFringe.size() + rpFringe.size());
078                    setNodesExpanded(ogs.getNodesExpanded() + rgs.getNodesExpanded());
079    
080                    while (!(opFringe.isEmpty() && rpFringe.isEmpty())) {
081                            // Determine the nodes to work with and expand their fringes
082                            // in preparation for testing whether or not the two
083                            // searches meet or one or other is at the GOAL.
084                            if (!opFringe.isEmpty()) {
085                                    opNode = opFringe.remove();
086                                    ogs.addExpandedNodesToFringe(opFringe, opNode, op);
087                            } else {
088                                    opNode = null;
089                            }
090                            if (!rpFringe.isEmpty()) {
091                                    rpNode = rpFringe.remove();
092                                    rgs.addExpandedNodesToFringe(rpFringe, rpNode, rp);
093                            } else {
094                                    rpNode = null;
095                            }
096    
097                            setQueueSize(opFringe.size() + rpFringe.size());
098                            setNodesExpanded(ogs.getNodesExpanded() + rgs.getNodesExpanded());
099    
100                            //
101                            // First Check if either fringe contains the other's state
102                            if (null != opNode && null != rpNode) {
103                                    Node popNode = null;
104                                    Node prpNode = null;
105                                    if (opFringe.containsNodeBasedOn(rpNode.getState())) {
106                                            popNode = opFringe.getNodeBasedOn(rpNode.getState());
107                                            prpNode = rpNode;
108                                    } else if (rpFringe.containsNodeBasedOn(opNode.getState())) {
109                                            popNode = opNode;
110                                            prpNode = rpFringe.getNodeBasedOn(opNode.getState());
111                                            // Need to also check whether or not the nodes that
112                                            // have been taken off the fringe actually represent the
113                                            // same state, otherwise there are instances whereby
114                                            // the searches can pass each other by
115                                    } else if (opNode.getState().equals(rpNode.getState())) {
116                                            popNode = opNode;
117                                            prpNode = rpNode;
118                                    }
119                                    if (null != popNode && null != prpNode) {
120                                            List<String> actions = retrieveActions(op, rp, popNode,
121                                                            prpNode);
122                                            // It may be the case that it is not in fact possible to
123                                            // traverse from the original node to the goal node based on
124                                            // the reverse path (i.e. unidirectional links: e.g.
125                                            // InitialState(A)<->C<-Goal(B) )
126                                            if (null != actions) {
127                                                    return actions;
128                                            }
129                                    }
130                            }
131    
132                            //
133                            // Check if the original problem is at the GOAL state
134                            if (null != opNode && op.isGoalState(opNode.getState())) {
135                                    // No need to check return value for null here
136                                    // as an action path discovered from the goal
137                                    // is guaranteed to exist
138                                    return retrieveActions(op, rp, opNode, null);
139                            }
140                            //
141                            // Check if the reverse problem is at the GOAL state
142                            if (null != rpNode && rp.isGoalState(rpNode.getState())) {
143                                    List<String> actions = retrieveActions(op, rp, null, rpNode);
144                                    // It may be the case that it is not in fact possible to
145                                    // traverse from the original node to the goal node based on
146                                    // the reverse path (i.e. unidirectional links: e.g.
147                                    // InitialState(A)<-Goal(B) )
148                                    if (null != actions) {
149                                            return actions;
150                                    }
151                            }
152                    }
153    
154                    // Empty List can indicate already at Goal
155                    // or unable to find valid set of actions
156                    return new ArrayList<String>();
157            }
158    
159            public SearchOutcome getSearchOutcome() {
160                    return searchOutcome;
161            }
162    
163            public Metrics getMetrics() {
164                    return metrics;
165            }
166    
167            public void clearInstrumentation() {
168                    metrics.set(NODES_EXPANDED, 0);
169                    metrics.set(QUEUE_SIZE, 0);
170                    metrics.set(MAX_QUEUE_SIZE, 0);
171                    metrics.set(PATH_COST, 0.0);
172            }
173    
174            public int getNodesExpanded() {
175                    return metrics.getInt(NODES_EXPANDED);
176            }
177    
178            public void setNodesExpanded(int nodesExpanded) {
179                    metrics.set(NODES_EXPANDED, nodesExpanded);
180            }
181    
182            public int getQueueSize() {
183                    return metrics.getInt(QUEUE_SIZE);
184            }
185    
186            public void setQueueSize(int queueSize) {
187                    metrics.set(QUEUE_SIZE, queueSize);
188                    int maxQSize = metrics.getInt(MAX_QUEUE_SIZE);
189                    if (queueSize > maxQSize) {
190                            metrics.set(MAX_QUEUE_SIZE, queueSize);
191                    }
192            }
193    
194            public int getMaxQueueSize() {
195                    return metrics.getInt(MAX_QUEUE_SIZE);
196            }
197    
198            public double getPathCost() {
199                    return metrics.getDouble(PATH_COST);
200            }
201    
202            public void setPathCost(Double pathCost) {
203                    metrics.set(PATH_COST, pathCost);
204            }
205    
206            //
207            // PRIVATE METHODS
208            //      
209            private List<String> retrieveActions(Problem op, Problem rp,
210                            Node originalPath, Node reversePath) {
211                    List<String> actions = new ArrayList<String>();
212    
213                    if (null == reversePath) {
214                            // This is the simple case whereby the path has been found
215                            // from the original problem first
216                            setPathCost(originalPath.getPathCost());
217                            searchOutcome = SearchOutcome.PATH_FOUND_FROM_ORIGINAL_PROBLEM;
218                            actions = SearchUtils.actionsFromNodes(originalPath
219                                            .getPathFromRoot());
220                    } else {
221                            List<Node> nodePath = new ArrayList<Node>();
222                            Object originalState = null;
223                            if (null != originalPath) {
224                                    nodePath.addAll(originalPath.getPathFromRoot());
225                                    originalState = originalPath.getState();
226                            }
227                            // Only append the reverse path if it is not the
228                            // GOAL state from the original problem (if you don't
229                            // you could end up appending a partial reverse path
230                            // that looks back on its initial state)
231                            if (!op.isGoalState(reversePath.getState())) {
232                                    List<Node> rpath = reversePath.getPathFromRoot();
233                                    for (int i = rpath.size() - 1; i >= 0; i--) {
234                                            // Ensure do not include the node from the reverse path
235                                            // that is the one that potentially overlaps with the
236                                            // original path (i.e. if started in goal state or where
237                                            // they meet in the middle).
238                                            if (!rpath.get(i).getState().equals(originalState)) {
239                                                    nodePath.add(rpath.get(i));
240                                            }
241                                    }
242                            }
243    
244                            if (!canTraversePathFromOriginalProblem(op, nodePath, actions)) {
245                                    // This is where it is possible to get to the initial state
246                                    // from the goal state (i.e. reverse path) but not the other way
247                                    // round, null returned to indicate an invalid path found from
248                                    // the reverse problem
249                                    return null;
250                            }
251    
252                            if (null == originalPath) {
253                                    searchOutcome = SearchOutcome.PATH_FOUND_FROM_REVERSE_PROBLEM;
254                            } else {
255                                    // Need to ensure that where the original and reverse paths
256                                    // overlap, as they can link based on their fringes, that
257                                    // the reverse path is actually capable of connecting to
258                                    // the previous node in the original path (if not root).
259                                    if (canConnectToOriginalFromReverse(rp, originalPath,
260                                                    reversePath)) {
261                                            searchOutcome = SearchOutcome.PATH_FOUND_BETWEEN_PROBLEMS;
262                                    } else {
263                                            searchOutcome = SearchOutcome.PATH_FOUND_FROM_ORIGINAL_PROBLEM;
264                                    }
265                            }
266                    }
267    
268                    return actions;
269            }
270    
271            private boolean canTraversePathFromOriginalProblem(Problem op,
272                            List<Node> path, List<String> actions) {
273                    boolean rVal = true;
274                    double pc = 0.0;
275    
276                    for (int i = 0; i < (path.size() - 1); i++) {
277                            Object currentState = path.get(i).getState();
278                            Object nextState = path.get(i + 1).getState();
279                            List<Successor> successors = op.getSuccessorFunction()
280                                            .getSuccessors(currentState);
281                            boolean found = false;
282                            for (Successor s : successors) {
283                                    if (nextState.equals(s.getState())) {
284                                            found = true;
285                                            pc += op.getStepCostFunction().calculateStepCost(
286                                                            currentState, nextState, s.getAction());
287                                            actions.add(s.getAction());
288                                            break;
289                                    }
290                            }
291    
292                            if (!found) {
293                                    rVal = false;
294                                    break;
295                            }
296                    }
297    
298                    setPathCost(true == rVal ? pc : 0.0);
299    
300                    return rVal;
301            }
302    
303            private boolean canConnectToOriginalFromReverse(Problem rp,
304                            Node originalPath, Node reversePath) {
305                    boolean rVal = true;
306    
307                    // Only need to test if not already at root
308                    if (!originalPath.isRootNode()) {
309                            rVal = false;
310                            List<Successor> successors = rp.getSuccessorFunction()
311                                            .getSuccessors(reversePath.getState());
312                            for (Successor s : successors) {
313                                    if (originalPath.getParent().getState().equals(s.getState())) {
314                                            rVal = true;
315                                            break;
316                                    }
317                            }
318                    }
319    
320                    return rVal;
321            }
322    }