diff --git a/core/src/main/java/aima/core/search/basic/SearchUtils.java b/core/src/main/java/aima/core/search/basic/SearchUtils.java new file mode 100644 index 0000000000..a2d3af6482 --- /dev/null +++ b/core/src/main/java/aima/core/search/basic/SearchUtils.java @@ -0,0 +1,75 @@ +package aima.core.search.basic; + +import aima.core.search.api.Node; +import aima.core.search.api.NodeFactory; +import aima.core.search.api.Problem; +import aima.core.search.basic.support.BasicNodeFactory; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Some utility functions for the search module + */ +public class SearchUtils { + + /** + * Calculates the successors of a given node for a given problem. + * + * @param problem + * @param parent + * @param + * @param + * @return + */ + public static List> successors(Problem problem, Node parent) { + S s = parent.state(); + List> nodes = new ArrayList<>(); + + NodeFactory nodeFactory = new BasicNodeFactory<>(); + for (A action : + problem.actions(s)) { + S sPrime = problem.result(s, action); + double cost = parent.pathCost() + problem.stepCost(s, action, sPrime); + Node node = nodeFactory.newChildNode(problem, parent, action); + nodes.add(node); + } + return nodes; + } + + /** + * Calculates the depth of a node in a particular tree. + * + * @param node + * @return + */ + public static int depth(Node node) { + Node temp = node; + int count = 0; + while (temp != null) { + count++; + temp = temp.parent(); + } + return count; + } + + /** + * Extracts the list of actions from a solution state. + * + * @param solution + * @param + * @param + * @return + */ + public static List generateActions(Node solution) { + Node parent = solution; + List actions = new ArrayList<>(); + while (parent.parent() != null) { + actions.add(parent.action()); + parent = parent.parent(); + } + Collections.reverse(actions); + return actions; + } +} diff --git a/core/src/main/java/aima/core/search/basic/informed/AStarSearch.java b/core/src/main/java/aima/core/search/basic/informed/AStarSearch.java index 64c0829b66..df52288f8e 100644 --- a/core/src/main/java/aima/core/search/basic/informed/AStarSearch.java +++ b/core/src/main/java/aima/core/search/basic/informed/AStarSearch.java @@ -1,137 +1,118 @@ package aima.core.search.basic.informed; -import java.util.Comparator; -import java.util.HashSet; -import java.util.List; -import java.util.PriorityQueue; -import java.util.Queue; -import java.util.Set; -import java.util.function.ToDoubleFunction; - import aima.core.search.api.Node; import aima.core.search.api.NodeFactory; import aima.core.search.api.Problem; -import aima.core.search.api.SearchController; import aima.core.search.api.SearchForActionsFunction; +import aima.core.search.basic.SearchUtils; import aima.core.search.basic.support.BasicNodeFactory; -import aima.core.search.basic.support.BasicSearchController; +import aima.core.search.basic.uninformedsearch.GenericSearchInterface; + +import java.util.*; +import java.util.function.ToDoubleFunction; /** *
  * function A*-SEARCH(problem) returns a solution, or failure
- *   node ← a node with STATE = problem.INITIAL-STATE, PATH-COST=0
- *   frontier ← a priority queue ordered by PATH-COST + h(NODE), with node as the only element
- *   explored ← an empty set
- *   loop do
- *      if EMPTY?(frontier) then return failure
- *      node <- POP(frontier) // chooses the lowest-cost node in frontier
- *      if problem.GOAL-TEST(node.STATE) then return SOLUTION(node)
- *      add node.STATE to explored
- *      for each action in problem.ACTIONS(node.STATE) do
- *          child ← CHILD-NODE(problem, node, action)
- *          if child.STATE is not in explored or frontier then
- *             frontier ← INSERT(child, frontier)
- *          else if child.STATE is in frontier with higher COST then
- *             replace that frontier node with child
+ *  if problem's initial state is a goal then return empty path to initial state
+ *  frontier ← a priority queue ordered by f(n) = h(n) + g(n), with a node for the initial state
+ *  reached ← a table of {state: the best path that reached state}; initially empty
+ *  solution ← failure
+ *  while frontier is not empty and top(frontier) is cheaper than solution do
+ *    parent ← pop(frontier)
+ *    for child in successors(parent) do
+ *      s ← child.state
+ *      if s is not in reached or child is a cheaper path than reached[s] then
+ *        reached[s] ← child
+ *        add child to the frontier
+ *        if child is a goal and is cheaper than solution then
+ *          solution = child
+ *  return solution
  * 
- * + * * @author Ciaran O'Reilly + * @author samagra */ -public class AStarSearch implements SearchForActionsFunction { - // function A*-SEARCH((problem) returns a solution, or failure - @Override - public List
apply(Problem problem) { - // node <- a node with STATE = problem.INITIAL-STATE, PATH-COST=0 - Node node = newRootNode(problem.initialState(), 0); - // frontier <- a priority queue ordered by PATH-COST + h(NODE), with - // node as the - // only element - Queue> frontier = newPriorityQueueOrderedByPathCostPlusH(node); - // explored <- an empty set - Set explored = newExploredSet(); - // loop do - while (true) { - // if EMPTY?(frontier) then return failure - if (frontier.isEmpty()) { - return failure(); - } - // node <- POP(frontier) // chooses the lowest-cost node in frontier - node = frontier.remove(); - // if problem.GOAL-TEST(node.STATE) then return SOLUTION(node) - if (isGoalState(node, problem)) { - return solution(node); - } - // add node.STATE to explored - explored.add(node.state()); - // for each action in problem.ACTIONS(node.STATE) do - for (A action : problem.actions(node.state())) { - // child <- CHILD-NODE(problem, node, action) - Node child = newChildNode(problem, node, action); - // if child.STATE is not in explored or frontier then - if (!(explored.contains(child.state()) || containsState(frontier, child.state()))) { - // frontier <- INSERT(child, frontier) - frontier.add(child); - } // else if child.STATE is in frontier with higher COST then - else if (removedNodeFromFrontierWithSameStateAndHigherCost(child, frontier)) { - // replace that frontier node with child - frontier.add(child); - } - } - } - } - - // - // Supporting Code - protected ToDoubleFunction> h; - protected NodeFactory nodeFactory = new BasicNodeFactory<>(); - protected SearchController searchController = new BasicSearchController(); - - public AStarSearch(ToDoubleFunction> h) { - this.h = h; - } - - public ToDoubleFunction> getHeuristicFunctionH() { - return h; - } - - public Node newRootNode(S initialState, double pathCost) { - return nodeFactory.newRootNode(initialState, pathCost); - } +public class AStarSearch implements GenericSearchInterface, SearchForActionsFunction { - public Node newChildNode(Problem problem, Node node, A action) { - return nodeFactory.newChildNode(problem, node, action); - } + // The heuristic function + protected ToDoubleFunction> h; + // A helper class to generate new nodes. + protected NodeFactory nodeFactory = new BasicNodeFactory<>(); - public Queue> newPriorityQueueOrderedByPathCostPlusH(Node initialNode) { - Queue> frontier = new PriorityQueue<>( - Comparator.comparingDouble(n -> n.pathCost() + h.applyAsDouble(n))); - frontier.add(initialNode); - return frontier; - } + // frontier ← a priority queue ordered by f(n) = h(n)+g(n), with a node for the initial state + PriorityQueue> frontier = new PriorityQueue<>(new Comparator>() { + @Override + public int compare(Node o1, Node o2) { + return (int) (getCostValue(o1) - getCostValue(o2)); + } + }); - public Set newExploredSet() { - return new HashSet<>(); - } - public List failure() { - return searchController.failure(); - } + /** + * The constructor that takes in the heuristics function. + * + * @param h + */ + public AStarSearch(ToDoubleFunction> h) { + this.h = h; + } - public List solution(Node node) { - return searchController.solution(node); - } + @Override + public Node search(Problem problem) { + if (problem.isGoalState(problem.initialState())) { + return nodeFactory.newRootNode(problem.initialState()); + } + frontier.clear(); + frontier.add(nodeFactory.newRootNode(problem.initialState())); + // reached ← a table of {state: the best path that reached state}; initially empty + HashMap> reached = new HashMap<>(); + Node solution = null; + // while frontier is not empty and top(frontier) is cheaper than solution do + while (!frontier.isEmpty() && + (solution == null || getCostValue(frontier.peek()) < getCostValue(solution))) { + Node parent = frontier.poll(); + for (Node child : + SearchUtils.successors(problem, parent)) { + S s = child.state(); + // if s is not in reached or child is a cheaper path than reached[s] then + if (!reached.containsKey(s) || + getCostValue(child) < getCostValue(reached.get(s))) { + reached.put(s, child); + frontier.add(child); + // if child is a goal and is cheaper than solution + if (problem.isGoalState(s) && + (solution == null || getCostValue(child) < getCostValue(solution))) { + solution = child; + } + } + } + } + return solution; + } - public boolean isGoalState(Node node, Problem problem) { - return searchController.isGoalState(node, problem); - } - - public boolean containsState(Queue> frontier, S state) { - // NOTE: Not very efficient (i.e. linear in the size of the frontier) - return frontier.stream().anyMatch(frontierNode -> frontierNode.state().equals(state)); - } + /** + * Returns the list of actions that need to be taken in order to achieve the goal. + * + * @param problem The search problem + * @return the list of actions + */ + @Override + public List apply(Problem problem) { + Node solution = this.search(problem); + if (solution == null) + return new ArrayList<>(); + else + return SearchUtils.generateActions(solution); + } - public boolean removedNodeFromFrontierWithSameStateAndHigherCost(Node child, Queue> frontier) { - // NOTE: Not very efficient (i.e. linear in the size of the frontier) - return frontier.removeIf(n -> n.state().equals(child.state()) && n.pathCost() > child.pathCost()); - } + /** + * Finds the value of f(n) = g(n)+h(n) for a node n. + * + * @param node The node n + * @return f(n) + */ + private double getCostValue(Node node) { + return node.pathCost() + h.applyAsDouble(node); + } } diff --git a/core/src/main/java/aima/core/search/basic/informed/GreedyBestFirstSearch.java b/core/src/main/java/aima/core/search/basic/informed/GreedyBestFirstSearch.java index 259cf07e17..3bfe6e1b61 100644 --- a/core/src/main/java/aima/core/search/basic/informed/GreedyBestFirstSearch.java +++ b/core/src/main/java/aima/core/search/basic/informed/GreedyBestFirstSearch.java @@ -3,127 +3,101 @@ import aima.core.search.api.Node; import aima.core.search.api.NodeFactory; import aima.core.search.api.Problem; -import aima.core.search.api.SearchController; import aima.core.search.api.SearchForActionsFunction; +import aima.core.search.basic.SearchUtils; import aima.core.search.basic.support.BasicNodeFactory; -import aima.core.search.basic.support.BasicSearchController; -import java.util.Comparator; -import java.util.HashSet; -import java.util.List; -import java.util.PriorityQueue; -import java.util.Queue; -import java.util.Set; +import aima.core.search.basic.uninformedsearch.GenericSearchInterface; + +import java.util.*; import java.util.function.ToDoubleFunction; /** *
  * function GREEDY-BEST-FIRST-SEARCH(problem) returns a solution, or failure
- *   node ← a node with STATE = problem.INITIAL-STATE, PATH-COST=0
- *   frontier ← a priority queue ordered by h(NODE), with node as the only element
- *   explored ← an empty set
- *   loop do
- *      if EMPTY?(frontier) then return failure
- *      node ← POP(frontier) // chooses the lowest-cost node in frontier
- *      if problem.GOAL-TEST(node.STATE) then return SOLUTION(node)
- *      add node.STATE to explored
- *      for each action in problem.ACTIONS(node.STATE) do
- *         child ← CHILD-NODE(problem, node, action)
- *         if child.STATE is not in explored or frontier then
- *            frontier ← INSERT(child, frontier)
- *         else if child.STATE is in frontier with higher COST then
- *            replace that frontier node with child
+ *  if problem's initial state is a goal then return empty path to initial state
+ *  frontier ← a priority queue ordered by h(n), with a node for the initial state
+ *  reached ← a table of {state: the best path that reached state}; initially empty
+ *  solution ← failure
+ *  while frontier is not empty and top(frontier) is cheaper than solution do
+ *    parent ← pop(frontier)
+ *    for child in successors(parent) do
+ *      s ← child.state
+ *      if s is not in reached or child is a cheaper path than reached[s] then
+ *        reached[s] ← child
+ *        add child to the frontier
+ *        if child is a goal and is cheaper than solution then
+ *          solution = child
+ *  return solution
  * 
+ * + * @author samagra */ -public class GreedyBestFirstSearch implements SearchForActionsFunction { - // function GREEDY-BEST-FIRST-SEARCH(problem) returns a solution, or failure - @Override - public List
apply(Problem problem) { - // node <- a node with STATE = problem.INITIAL-STATE, PATH-COST=0 - Node node = newRootNode(problem.initialState(), 0); - // frontier <- a priority queue ordered by h(NODE), with node as the only element - Queue> frontier = newPriorityQueueOrderedByH(node); - // explored <- an empty set - Set explored = newExploredSet(); - // loop do - while (true) { - // if EMPTY?(frontier) then return failure - if (frontier.isEmpty()) { - return failure(); - } - // node <- POP(frontier) // chooses the lowest-cost node in frontier - node = frontier.remove(); - // if problem.GOAL-TEST(node.STATE) then return SOLUTION(node) - if (problem.isGoalState(node.state())) { - return solution(node); - } - // add node.STATE to explored - explored.add(node.state()); - // for each action in problem.ACTIONS(node.STATE) do - for (A action : problem.actions(node.state())) { - // child <- CHILD-NODE(problem, node, action) - Node child = newChildNode(problem, node, action); - // if child.STATE is not in explored or frontier then - if (!(explored.contains(child.state())) || containsState(frontier, child.state())) { - // frontier <- INSERT(child, frontier) - frontier.add(child); - } // else if child.STATE is in frontier with higher COST then - else if (removedNodeFromFrontierWithSameStateAndHigherCost(child, frontier)) { - // replace that frontier node with child - frontier.add(child); - } - } - } - } - - /* Supporting Code */ - protected ToDoubleFunction> h; - protected NodeFactory nodeFactory = new BasicNodeFactory<>(); - protected SearchController searchController = new BasicSearchController<>(); - - public GreedyBestFirstSearch(ToDoubleFunction> h) { - this.h = h; - } - - public Node newRootNode(S initialState, double pathCost) { - return nodeFactory.newRootNode(initialState, pathCost); - } - - public Queue> newPriorityQueueOrderedByH(Node initialNode) { - Queue> frontier = new PriorityQueue<>( - Comparator.comparingDouble(value -> h.applyAsDouble(value))); - frontier.add(initialNode); - return frontier; - } +public class GreedyBestFirstSearch implements GenericSearchInterface, SearchForActionsFunction { - public Set newExploredSet() { - return new HashSet<>(); - } + // The heuristic function + protected ToDoubleFunction> h; + // The helper class to generate nodes. + protected NodeFactory nodeFactory = new BasicNodeFactory<>(); + // frontier ← a priority queue ordered by h(n), with a node for the initial state + PriorityQueue> frontier = new PriorityQueue<>(new Comparator>() { + @Override + public int compare(Node o1, Node o2) { + return (int) (h.applyAsDouble(o1) - h.applyAsDouble(o2)); + } + }); - public List failure() { - return searchController.failure(); - } + // Constructor for the class which takes in a heuristic function. + public GreedyBestFirstSearch(ToDoubleFunction> h) { + this.h = h; + } - public boolean isGoalState(Node node, Problem problem) { - return searchController.isGoalState(node, problem); - } + @Override + public Node search(Problem problem) { + if (problem.isGoalState(problem.initialState())) { + return nodeFactory.newRootNode(problem.initialState()); + } - public List solution(Node node) { - return searchController.solution(node); - } + frontier.clear(); + frontier.add(nodeFactory.newRootNode(problem.initialState())); + // reached ← a table of {state: the best path that reached state}; initially empty + HashMap> reached = new HashMap<>(); + Node solution = null; + while (!frontier.isEmpty() && + (solution == null || h.applyAsDouble(frontier.peek()) + < h.applyAsDouble(solution))) { + Node parent = frontier.poll(); + for (Node child : + SearchUtils.successors(problem, parent)) { + S s = child.state(); + // if s is not in reached or child is a cheaper path than reached[s] then + if (!reached.containsKey(s) || + h.applyAsDouble(child) < h.applyAsDouble(reached.get(s))) { + reached.put(s, child); + frontier.add(child); + if (problem.isGoalState(s) && + (solution == null || h.applyAsDouble(child) < h.applyAsDouble(solution))) { + solution = child; + } + } + } + } + return solution; + } - public Node newChildNode(Problem problem, Node node, A action) { - return nodeFactory.newChildNode(problem, node, action); - } - public boolean containsState(Queue> frontier, S state) { - // NOTE: Not very efficient (i.e. linear in the size of the frontier) - return frontier.stream().anyMatch(frontierNode -> frontierNode.state().equals(state)); - } + /** + * Returns the list of actions that need to be taken in order to achieve the goal. + * + * @param problem The search problem. + * @return The list of actions. + */ + @Override + public List apply(Problem problem) { + Node solution = this.search(problem); + if (solution == null) + return new ArrayList<>(); + else + return SearchUtils.generateActions(solution); + } - private boolean removedNodeFromFrontierWithSameStateAndHigherCost(Node child, - Queue> frontier) { - // NOTE: Not very efficient (i.e. linear in the size of the frontier) - return frontier.removeIf( - n -> n.state().equals(child.state()) && h.applyAsDouble(n) > h.applyAsDouble(child)); - } } diff --git a/core/src/main/java/aima/core/search/basic/local/HillClimbingSearch.java b/core/src/main/java/aima/core/search/basic/local/HillClimbingSearch.java index accb03371d..2cd107cf32 100644 --- a/core/src/main/java/aima/core/search/basic/local/HillClimbingSearch.java +++ b/core/src/main/java/aima/core/search/basic/local/HillClimbingSearch.java @@ -1,24 +1,22 @@ package aima.core.search.basic.local; -import java.util.function.ToDoubleFunction; - import aima.core.search.api.Problem; import aima.core.search.api.SearchForStateFunction; +import java.util.function.ToDoubleFunction; + /** * Artificial Intelligence A Modern Approach (4th Edition): Figure ??, page ??. *
*

- * + * *

  * function HILL-CLIMBING(problem) returns a state that is a local maximum
- *
- *   current ← MAKE-NODE(problem.INITIAL-STATE)
- *   loop do
- *     neighbor ← a highest-valued successor of current
- *     if neighbor.VALUE ≤ current.VALUE then return current.STATE
- *     current ← neighbor
- * 
+ *  current ← problem.INITIAL-STATE + *  loop do + *    neighbor ← a highest-valued successor of current + *    if VALUE(neighbour) ≤ VALUE(current) then return current + *    current ← neighbor *

* Figure ?? The hill-climbing search algorithm, which is the most basic local * search technique. At each step the current node is replaced by the best @@ -31,92 +29,66 @@ * @author Ravi Mohan * @author Paul Anton * @author Mike Stampone + * @author samagra */ public class HillClimbingSearch implements SearchForStateFunction { - // function HILL-CLIMBING(problem) returns a state that is a local maximum - @Override - public S apply(Problem problem) { - // current <- MAKE-NODE(problem.INITIAL-STATE) - Node current = makeNode(problem.initialState()); - // loop do - while (true) { - // neighbor <- a highest-valued successor of current - Node neighbor = highestValuedSuccessor(current, problem); - // if neighbor.VALUE <= current.VALUE then return current.STATE - if (neighbor.value <= current.value) { - return current.state; - } - // current <- neighbor - current = neighbor; - } - } - - // - // Supporting Code - - /** - * The algorithm does not maintain a search tree, so the data structure for - * the current node need only record the state and value of the - * objective/cost function. - * - * @author oreilly - * - * @param - * the type of the state space - */ - public static class Node { - S state; - double value; - - Node(S state, double value) { - this.state = state; - this.value = value; - } - - @Override - public String toString() { - return "N(" + state + ", " + value + ")"; - } - } - /* - * Represents an objective (higher better) or cost/heuristic (lower better) - * function. If a cost/heuristic function is passed in the - * 'isSteepestAscentVersion' should be set to false for the algorithm to - * search for minimums. - */ - protected ToDoubleFunction stateValueFn; + /* + * Represents an objective (higher better) or cost/heuristic (lower better) + * function. If a cost/heuristic function is passed in the + * 'isSteepestAscentVersion' should be set to false for the algorithm to + * search for minimums. + */ + protected ToDoubleFunction stateValueFn; - public HillClimbingSearch(ToDoubleFunction stateValueFn) { - this(stateValueFn, true); - } + // Constructors. + public HillClimbingSearch(ToDoubleFunction stateValueFn) { + this(stateValueFn, true); + } - public HillClimbingSearch(ToDoubleFunction stateValueFn, boolean isSteepestAscentVersion) { - this.stateValueFn = stateValueFn; - if (!isSteepestAscentVersion) { - // Convert from one to the other by switching the sign - this.stateValueFn = (state) -> stateValueFn.applyAsDouble(state) * -1; - } - } + public HillClimbingSearch(ToDoubleFunction stateValueFn, boolean isSteepestAscentVersion) { + this.stateValueFn = stateValueFn; + if (!isSteepestAscentVersion) { + // Convert from one to the other by switching the sign + this.stateValueFn = (state) -> stateValueFn.applyAsDouble(state) * -1; + } + } - public Node makeNode(S state) { - return new Node<>(state, stateValueFn.applyAsDouble(state)); - } + // function HILL-CLIMBING(problem) returns a state that is a local maximum + @Override + public S apply(Problem problem) { + S current = problem.initialState(); + while (true) { + S neighbor = highestValuedSuccessor(current, problem); + if (stateValueFn.applyAsDouble(neighbor) <= stateValueFn.applyAsDouble(current)) { + return current; + } + current = neighbor; + } + } - public Node highestValuedSuccessor(Node current, Problem problem) { - Node highestValueSuccessor = null; - for (A action : problem.actions(current.state)) { - Node successor = makeNode(problem.result(current.state, action)); - if (highestValueSuccessor == null || successor.value > highestValueSuccessor.value) { - highestValueSuccessor = successor; - } - } - // If no successor then just be our own neighbor - // as the calling code will just return 'current' as - // the <= test will be true - if (highestValueSuccessor == null) { - highestValueSuccessor = current; - } - return highestValueSuccessor; - } + /** + * Calculates the successor with the highest value. + * + * @param current + * @param problem + * @return + */ + public S highestValuedSuccessor(S current, Problem problem) { + S highestValueSuccessor = null; + for (A action : problem.actions(current)) { + S successor = problem.result(current, action); + if (highestValueSuccessor == null || stateValueFn.applyAsDouble(successor) + > stateValueFn.applyAsDouble(highestValueSuccessor)) { + highestValueSuccessor = successor; + } + } + // If no successor then just be our own neighbor + // as the calling code will just return 'current' as + // the <= test will be true + if (highestValueSuccessor == null) { + highestValueSuccessor = current; + } + return highestValueSuccessor; + } } \ No newline at end of file diff --git a/core/src/main/java/aima/core/search/basic/local/SimulatedAnnealingSearch.java b/core/src/main/java/aima/core/search/basic/local/SimulatedAnnealingSearch.java index a38d63aa4f..c34cd50117 100644 --- a/core/src/main/java/aima/core/search/basic/local/SimulatedAnnealingSearch.java +++ b/core/src/main/java/aima/core/search/basic/local/SimulatedAnnealingSearch.java @@ -1,156 +1,119 @@ package aima.core.search.basic.local; -import java.util.List; -import java.util.Random; -import java.util.function.ToDoubleFunction; - import aima.core.search.api.Problem; import aima.core.search.api.SearchForStateFunction; import aima.core.search.basic.support.BasicSchedule; +import java.util.List; +import java.util.Random; +import java.util.function.ToDoubleFunction; + /** * Artificial Intelligence A Modern Approach (4th Edition): Figure ??, page ??. *
*
- * + * *

- * function SIMULATED-ANNEALING(problem, schedule) returns a solution state
- *   inputs: problem, a problem
- *           schedule, a mapping from time to "temperature"
+ * function SIMULATED-ANNEALING(problem,schedule) returns a solution state
  *
- *   current ← MAKE-NODE(problem.INITIAL-STATE)
- *   for t = 1 to ∞ do
- *     T ← schedule(t)
- *     if T = 0 then return current
- *     next ← a randomly selected successor of current
- *     ΔE ← next.VALUE - current.value
- *     if ΔE > 0 then current ← next
- *     else current ← next only with probability eΔE/T
+ *  current ← problem.INITIAL-STATE
+ *  for t = 1 to ∞ do
+ *    T ← schedule(t)
+ *    if T = 0 then return current
+ *    next ← a randomly selected successor of current
+ *    ΔE ← VALUE(next) - VALUE(current)
+ *    if ΔE > 0 then current ← next
+ *    else current ← next only with probability eΔE/T
  * 
- * + *

* Figure ?? The simulated annealing search algorithm, a version of stochastic * hill climbing where some downhill moves are allowed. Downhill moves are * accepted readily early in the annealing schedule and then less often as time * goes on. The schedule input determines the value of the temperature T as a * function of time. - * + * * @author Ciaran O'Reilly * @author Anurag Rai * @author Ravi Mohan * @author Mike Stampone * @author Ruediger Lunde + * @author samagra */ public class SimulatedAnnealingSearch implements SearchForStateFunction { - // function SIMULATED-ANNEALING(problem, schedule) returns a solution state - public S simulatedAnnealing(Problem problem, ToDoubleFunction schedule) { - // current <- MAKE-NODE(problem.INITIAL-STATE) - Node current = makeNode(problem.initialState()); - // for t = 1 to INFINITY do - for (int t = 1; true; t++) { - // T <- schedule(t) - double T = schedule(t); - // if T = 0 then return current - if (T == 0) { - return current.state; - } - // next <- a randomly selected successor of current - Node next = randomlySelectSuccessor(current, problem); - // ΔE <- next.VALUE - current.VALUE - double DeltaE = next.value - current.value; - // if ΔE > 0 then current <- next - if (DeltaE > 0) { - current = next; - } - // else current <- next only with probability e^ΔE/T - else if (Math.exp(DeltaE / T) > random.nextDouble()) { - current = next; - } - } - } - - // - // Supporting Code - @Override - public S apply(Problem problem) { - return simulatedAnnealing(problem, schedule); - } - - /** - * The algorithm does not maintain a search tree, so the data structure for - * the current node need only record the state and value of the - * objective/cost function. - * - * @author oreilly - * - * @param - * the type of the state space - */ - public static class Node { - S state; - double value; - - Node(S state, double value) { - this.state = state; - this.value = value; - } - - @Override - public String toString() { - return "N(" + state + ", " + value + ")"; - } - } - - /* - * Represents an objective (higher better) or cost/heuristic (lower better) - * function. If a cost/heuristic function is passed in the - * 'isGradientAscentVersion' should be set to false for the algorithm to - * search for minimums. - */ - protected ToDoubleFunction stateValueFn; - protected ToDoubleFunction schedule; - protected Random random = new Random(); - - public SimulatedAnnealingSearch(ToDoubleFunction stateValueFn) { - this(stateValueFn, true); - } + /* + * Represents an objective (higher better) or cost/heuristic (lower better) + * function. If a cost/heuristic function is passed in the + * 'isGradientAscentVersion' should be set to false for the algorithm to + * search for minimums. + */ + protected ToDoubleFunction stateValueFn; + // The schedule input determines the value of the “temperature” T as a function of time; + protected ToDoubleFunction schedule; + // Pseudo-random number generator for handling probabilities + protected Random random = new Random(); + // Constructors + public SimulatedAnnealingSearch(ToDoubleFunction stateValueFn) { + this(stateValueFn, true); + } + public SimulatedAnnealingSearch(ToDoubleFunction stateValueFn, boolean isGradientAscentVersion) { + this(stateValueFn, isGradientAscentVersion, new BasicSchedule()); + } - public SimulatedAnnealingSearch(ToDoubleFunction stateValueFn, boolean isGradientAscentVersion) { - this(stateValueFn, isGradientAscentVersion, new BasicSchedule()); - } + public SimulatedAnnealingSearch(ToDoubleFunction stateValueFn, boolean isGradientAscentVersion, + ToDoubleFunction scheduler) { + this.stateValueFn = stateValueFn; + if (!isGradientAscentVersion) { + // Convert from one to the other by switching the sign + this.stateValueFn = (state) -> stateValueFn.applyAsDouble(state) * -1; + } + this.schedule = scheduler; + } - public SimulatedAnnealingSearch(ToDoubleFunction stateValueFn, boolean isGradientAscentVersion, - ToDoubleFunction scheduler) { - this.stateValueFn = stateValueFn; - if (!isGradientAscentVersion) { - // Convert from one to the other by switching the sign - this.stateValueFn = (state) -> stateValueFn.applyAsDouble(state) * -1; - } - this.schedule = scheduler; - } + // function SIMULATED-ANNEALING(problem, schedule) returns a solution state + public S simulatedAnnealing(Problem problem, ToDoubleFunction schedule) { + S current = problem.initialState(); + for (int t = 1; true; t++) { + double T = schedule(t); + if (T == 0) { + return current; + } + S next = randomlySelectSuccessor(current, problem); + double DeltaE = stateValueFn.applyAsDouble(next) - + stateValueFn.applyAsDouble(current); + if (DeltaE > 0) { + current = next; + } + // else current <- next only with probability e^ΔE/T + else if (Math.exp(DeltaE / T) > random.nextDouble()) { + current = next; + } + } + } - public Node makeNode(S state) { - return new Node<>(state, stateValueFn.applyAsDouble(state)); - } + @Override + public S apply(Problem problem) { + return simulatedAnnealing(problem, schedule); + } - public double schedule(int t) { - double T = schedule.applyAsDouble(t); - if (T < 0) { - throw new IllegalArgumentException( - "Configured schedule returns negative temperatures: t=" + t + ", T=" + T); - } - return T; - } + public double schedule(int t) { + double T = schedule.applyAsDouble(t); + if (T < 0) { + throw new IllegalArgumentException( + "Configured schedule returns negative temperatures: t=" + t + ", T=" + T); + } + return T; + } - public Node randomlySelectSuccessor(Node current, Problem problem) { - // Default successor to current, so that in the case we reach a dead-end - // state i.e. one without reversible actions we will return something. - // This will not break the code above as the loop will exit when the - // temperature winds down to 0. - Node successor = current; - List actions = problem.actions(current.state); - if (actions.size() > 0) { - successor = makeNode(problem.result(current.state, actions.get(random.nextInt(actions.size())))); - } - return successor; - } + public S randomlySelectSuccessor(S current, Problem problem) { + // Default successor to current, so that in the case we reach a dead-end + // state i.e. one without reversible actions we will return something. + // This will not break the code above as the loop will exit when the + // temperature winds down to 0. + S successor = current; + List actions = problem.actions(current); + if (actions.size() > 0) { + successor = problem.result(current, actions.get(random.nextInt(actions.size()))); + } + return successor; + } } diff --git a/core/src/main/java/aima/core/search/basic/uninformedsearch/BreadthFirstSearch.java b/core/src/main/java/aima/core/search/basic/uninformedsearch/BreadthFirstSearch.java new file mode 100644 index 0000000000..85f3cd0112 --- /dev/null +++ b/core/src/main/java/aima/core/search/basic/uninformedsearch/BreadthFirstSearch.java @@ -0,0 +1,91 @@ +package aima.core.search.basic.uninformedsearch; + +import aima.core.search.api.Node; +import aima.core.search.api.NodeFactory; +import aima.core.search.api.Problem; +import aima.core.search.api.SearchForActionsFunction; +import aima.core.search.basic.SearchUtils; +import aima.core.search.basic.support.BasicNodeFactory; + +import java.util.*; + +/** + *

+ *  function BREADTH-FIRST-SEARCH(problem) returns a solution, or failure
+ *  if problem's initial state is a goal then return empty path to initial state
+ *  frontier ← a FIFO queue initially containing one path, for the problem's initial state
+ *  reached ← a set of states; initially empty
+ *  solution ← failure
+ *  while frontier is not empty do
+ *    parent ← the first node in frontier
+ *    for child in successors(parent) do
+ *      s ← child.state
+ *      if s is a goal then
+ *        return child
+ *      if s is not in reached then
+ *        add s to reached
+ *        add child to the end of frontier
+ *  return solution
+ * 
+ *

+ * Figure 3.9 Breadth-first search algorithm. + * + * @param The generic object representing actions. + * @param The generic object representing state. + * @author samagra + */ +public class BreadthFirstSearch implements GenericSearchInterface, SearchForActionsFunction { + // Node factory is just a helper class to generate new nodes + // It takes the problem and current state in order to generate new nodes + NodeFactory nodeFactory = new BasicNodeFactory(); + + public BreadthFirstSearch() { + } + + /** + * function BREADTH-FIRST-SEARCH(problem) returns a solution, or failure + * + * @param problem The search problem. + * @return The goal node if any else null. + */ + @Override + public Node search(Problem problem) { + if (problem.isGoalState(problem.initialState())) { + return nodeFactory.newRootNode(problem.initialState()); + } + // frontier ← a FIFO queue initially containing one path, for the problem's initial state + Queue frontier = new LinkedList<>(); + ((LinkedList) frontier).add(nodeFactory.newRootNode(problem.initialState())); + HashSet reached = new HashSet<>(); + Node solution = null; + while (!frontier.isEmpty()) { + Node parent = frontier.remove(); + for (Node child : SearchUtils.successors(problem, parent)) { + S s = child.state(); + if (problem.isGoalState(s)) { + return child; + } + // if s is not in reached then + if (!reached.contains(s)) { + reached.add(s); + frontier.add(child); + } + } + } + return solution; + } + + /** + * Extracts a list of actions from a solution state. + * + * @param problem + * @return + */ + @Override + public List apply(Problem problem) { + Node solution = this.search(problem); + if (solution == null) + return new ArrayList<>(); + return SearchUtils.generateActions(solution); + } +} diff --git a/core/src/main/java/aima/core/search/basic/uninformedsearch/DepthLimitedSearch.java b/core/src/main/java/aima/core/search/basic/uninformedsearch/DepthLimitedSearch.java new file mode 100644 index 0000000000..f35c248b7f --- /dev/null +++ b/core/src/main/java/aima/core/search/basic/uninformedsearch/DepthLimitedSearch.java @@ -0,0 +1,71 @@ +package aima.core.search.basic.uninformedsearch; + +import aima.core.search.api.Node; +import aima.core.search.api.NodeFactory; +import aima.core.search.api.Problem; +import aima.core.search.basic.SearchUtils; +import aima.core.search.basic.support.BasicNodeFactory; + +import java.util.Stack; + +/** + *

+ *  function DEPTH-LIMITED-SEARCH(problem, l) returns a solution, or failure, or cutoff
+ *  frontier ← a FIFO queue initially containing one path, for the problem's initial state
+ *  solution ← failure
+ *  while frontier is not empty do
+ *    parent ← pop(frontier)
+ *    if depth(parent) > l then
+ *      solution ← cutoff
+ *    else
+ *        for child in successors(parent) do
+ *          if child is a goal then
+ *            return child
+ *          add child to frontier
+ *  return solution
+ * 
+ *

+ * Figure 3.14 An implementation of depth-limited tree search. The + * algorithm has two different ways to signal failure to find a solution: + * it returns failure when it has exhausted all paths and proved there is no + * solution at any depth, and returns cutoff to mean there might be a solution + * at a deeper depth than l. Note that this algorithm does not keep track of + * reached states, and thus might visit the same state multiple times on different paths. + * + * @param The generic object representing actions. + * @param The generic object representing state. + * @author samagra + */ +public class DepthLimitedSearch { + + // A helper class to generate new nodes. + NodeFactory nodeFactory = new BasicNodeFactory<>(); + + /** + * function DEPTH-LIMITED-SEARCH(problem, l) returns a solution, or failure, or cutoff + * + * @param problem The search problem. + * @param l The cutoff limit. + * @return The goal state if exists/found else null + */ + public Node search(Problem problem, int l) { + Stack> frontier = new Stack<>(); + frontier.push(nodeFactory.newRootNode(problem.initialState())); + Node solution = null; + while (!frontier.isEmpty()) { + Node parent = frontier.pop(); + if (SearchUtils.depth(parent) > l) { + solution = null; + } else { + for (Node child : + SearchUtils.successors(problem, parent)) { + if (problem.isGoalState(child.state())) { + return child; + } + frontier.push(child); + } + } + } + return solution; + } +} diff --git a/core/src/main/java/aima/core/search/basic/uninformedsearch/GenericSearch.java b/core/src/main/java/aima/core/search/basic/uninformedsearch/GenericSearch.java new file mode 100644 index 0000000000..b69c998376 --- /dev/null +++ b/core/src/main/java/aima/core/search/basic/uninformedsearch/GenericSearch.java @@ -0,0 +1,125 @@ +package aima.core.search.basic.uninformedsearch; + +import aima.core.search.api.Node; +import aima.core.search.api.NodeFactory; +import aima.core.search.api.Problem; +import aima.core.search.api.SearchForActionsFunction; +import aima.core.search.basic.support.BasicNodeFactory; + +import java.util.*; + +/** + * Artificial Intelligence A Modern Approach (4th Edition): Figure ??, page ??. + *
+ *
+ * + *

+ * function GENERIC-SEARCH(problem) returns a solution, or failure
+ *  frontier ← a queue initially containing one path, for the problem's initial state
+ *  reached ← a table of {state: the best path that reached state}; initially empty
+ *  solution ← failure
+ *  while frontier is not empty and solution can possibly be improved do
+ *    parent ← some node that we choose to remove from frontier
+ *    for child in successors(parent) do
+ *      s ← child.state
+ *      if s is not in reached or child is a cheaper path than reached[s] then
+ *        reached[s] ← child
+ *        add child to frontier
+ *        if child is a goal and is cheaper than solution then
+ *          solution = child
+ *  return solution
+ * 
+ *

+ * Figure ?? In the GENERIC-SEARCH algorithm, we keep track of the best + * solution found so far, as well as a set of states that we have already + * reached, and a frontier of paths from which we will choose the next path + * to expand. In any specific search algorithm, we specify (1) the criteria + * for ordering the paths in the frontier, and (2) the procedure for + * determining when it is no longer possible to improve on a solution. + * + * @author samagra + */ + +public abstract class GenericSearch implements SearchForActionsFunction { + public NodeFactory nodeFactory = new BasicNodeFactory<>(); // to generate new nodes. + public HashMap> reached = new HashMap<>(); + + /** + * function GENERIC-SEARCH(problem) returns a solution, or failure + * + * @param problem The search problem. + * @return The solution to the search problem. + */ + public Node genericSearch(Problem problem) { + //  frontier ← a queue initially containing one path, for the problem's initial state + Queue> frontier = newFrontier(problem.initialState()); + // reached ← a table of {state: the best path that reached state}; initially empty + reached.clear(); + Node solution = null; + // while frontier is not empty and solution can possibly be improved do + while (!frontier.isEmpty() && this.canImprove(reached, solution)) { + // parent ← some node that we choose to remove from frontier + Node parent = frontier.remove(); + for (A action : + problem.actions(parent.state())) { + Node child = nodeFactory.newChildNode(problem, parent, action); + S s = child.state(); + // if s is not in reached or child is a cheaper path than reached[s] then + if (!reached.containsKey(s) || (child.pathCost() < reached.get(s).pathCost())) { + reached.put(s, child); + frontier = this.addToFrontier(child, frontier); + // if child is a goal and is cheaper than solution then + if (problem.isGoalState(child.state()) && (solution == null || (child.pathCost() < solution.pathCost()))) { + solution = child; + } + } + } + } + return solution; + } + + /** + * The strategy for adding nodes to the frontier + * + * @param child + * @param frontier + * @return + */ + public abstract Queue> addToFrontier(Node child, + Queue> frontier); + + /** + * the procedure for determining when it is no longer possible to improve on a solution. + * + * @param reached The reached states. + * @param solution The current solution. + * @return A boolean stating if we can improve the solution. + */ + public abstract boolean canImprove(HashMap> reached, + Node solution); + + /** + * This method initialises a new frontier. + * + * @param initialState + * @return + */ + public Queue> newFrontier(S initialState) { + Queue> frontier = new LinkedList<>(); + frontier.add(nodeFactory.newRootNode(initialState)); + return frontier; + } + + @Override + public List apply(Problem problem) { + Node solution = this.genericSearch(problem); + Node parent = solution.parent(); + List actions = new ArrayList<>(); + while (parent != null) { + actions.add(parent.action()); + parent = parent.parent(); + } + Collections.reverse(actions); + return actions; + } +} diff --git a/core/src/main/java/aima/core/search/basic/uninformedsearch/GenericSearchInterface.java b/core/src/main/java/aima/core/search/basic/uninformedsearch/GenericSearchInterface.java new file mode 100644 index 0000000000..492676894d --- /dev/null +++ b/core/src/main/java/aima/core/search/basic/uninformedsearch/GenericSearchInterface.java @@ -0,0 +1,41 @@ +package aima.core.search.basic.uninformedsearch; + +import aima.core.search.api.Node; +import aima.core.search.api.Problem; + +/** + * An interface that identifies a generic search algorithm. + *

+ * Artificial Intelligence A Modern Approach (4th Edition): Figure ??, page ??. + *
+ *
+ * + *

+ * function GENERIC-SEARCH(problem) returns a solution, or failure
+ *  frontier ← a queue initially containing one path, for the problem's initial state
+ *  reached ← a table of {state: the best path that reached state}; initially empty
+ *  solution ← failure
+ *  while frontier is not empty and solution can possibly be improved do
+ *    parent ← some node that we choose to remove from frontier
+ *    for child in successors(parent) do
+ *      s ← child.state
+ *      if s is not in reached or child is a cheaper path than reached[s] then
+ *        reached[s] ← child
+ *        add child to frontier
+ *        if child is a goal and is cheaper than solution then
+ *          solution = child
+ *  return solution
+ * 
+ *

+ * Figure ?? In the GENERIC-SEARCH algorithm, we keep track of the best + * solution found so far, as well as a set of states that we have already + * reached, and a frontier of paths from which we will choose the next path + * to expand. In any specific search algorithm, we specify (1) the criteria + * for ordering the paths in the frontier, and (2) the procedure for + * determining when it is no longer possible to improve on a solution. + * + * @author samagra + */ +public interface GenericSearchInterface { + Node search(Problem problem); +} diff --git a/core/src/main/java/aima/core/search/basic/uninformedsearch/IterativeDeepeningSearch.java b/core/src/main/java/aima/core/search/basic/uninformedsearch/IterativeDeepeningSearch.java new file mode 100644 index 0000000000..592e4117a5 --- /dev/null +++ b/core/src/main/java/aima/core/search/basic/uninformedsearch/IterativeDeepeningSearch.java @@ -0,0 +1,65 @@ +package aima.core.search.basic.uninformedsearch; + +import aima.core.search.api.Node; +import aima.core.search.api.Problem; +import aima.core.search.api.SearchForActionsFunction; +import aima.core.search.basic.SearchUtils; + +import java.util.List; + +/** + *

+ *
+ * /**
+ * Artificial Intelligence A Modern Approach (4th Edition): Figure ??, page ??.
+ * 
+ *
+ * + *
+ * function ITERATIVE-DEEPENING-SEARCH(problem) returns a solution, or failure
+ *   for depth = 0 to infinity  do
+ *     result ← DEPTH-LIMITED-SEARCH(problem, depth)
+ *     if result != cutoff then return result
+ * 
+ *

+ * Figure ?? The iterative deepening search algorithm, which repeatedly applies + * depth-limited search with increasing limits. It terminates when a solution is + * found or if the depth-limited search returns failure, meaning that no + * solution exists. + * + * @author Ciaran O'Reilly + * @author Ravi Mohan + * @author Ruediger Lunde + * @author samagra + */ +public class IterativeDeepeningSearch implements GenericSearchInterface, SearchForActionsFunction { + DepthLimitedSearch depthLimitedSearch = new DepthLimitedSearch<>(); + + /** + * function ITERATIVE-DEEPENING-SEARCH(problem) returns a solution, or failure + * + * @param problem The search problem + * @return The solution if exists else continues to run + */ + @Override + public Node search(Problem problem) { + for (int depth = 0; depth >= 0; depth++) { + Node result = depthLimitedSearch.search(problem, depth); + if (result != null) + return result; + } + return null; + } + + /** + * Returns a list of actions to be taken to reach the goal state. + * + * @param problem + * @return + */ + @Override + public List apply(Problem problem) { + Node solution = this.search(problem); + return SearchUtils.generateActions(solution); + } +} diff --git a/core/src/main/java/aima/core/search/basic/uninformedsearch/UniformCostSearch.java b/core/src/main/java/aima/core/search/basic/uninformedsearch/UniformCostSearch.java new file mode 100644 index 0000000000..03c972e356 --- /dev/null +++ b/core/src/main/java/aima/core/search/basic/uninformedsearch/UniformCostSearch.java @@ -0,0 +1,95 @@ +package aima.core.search.basic.uninformedsearch; + +import aima.core.search.api.Node; +import aima.core.search.api.NodeFactory; +import aima.core.search.api.Problem; +import aima.core.search.api.SearchForActionsFunction; +import aima.core.search.basic.SearchUtils; +import aima.core.search.basic.support.BasicNodeFactory; + +import java.util.*; + +/** + *

+ *  function UNIFORM-COST-SEARCH(problem) returns a solution, or failure
+ *  if problem's initial state is a goal then return empty path to initial state
+ *  frontier ← a priority queue ordered by pathCost, with a node for the initial state
+ *  reached ← a table of {state: the best path that reached state}; initially empty
+ *  solution ← failure
+ *  while frontier is not empty and top(frontier) is cheaper than solution do
+ *    parent ← pop(frontier)
+ *    for child in successors(parent) do
+ *      s ← child.state
+ *      if s is not in reached or child is a cheaper path than reached[s] then
+ *        reached[s] ← child
+ *        add child to the frontier
+ *        if child is a goal and is cheaper than solution then
+ *          solution = child
+ *  return solution
+ * 
+ *

+ * Figure 3.11 Uniform-cost search on a graph. Finds optimal + * paths for problems with varying step costs. + * + * @param The generic class representing action. + * @param The generic class representing states. + * @author samagra + */ +public class UniformCostSearch implements GenericSearchInterface, SearchForActionsFunction { + + // frontier ← a priority queue ordered by pathCost, with a node for the initial state + PriorityQueue> frontier = new PriorityQueue<>(new Comparator>() { + @Override + public int compare(Node o1, Node o2) { + return (int) (o1.pathCost() - o2.pathCost()); + } + }); + + private NodeFactory nodeFactory = new BasicNodeFactory<>(); + + @Override + public Node search(Problem problem) { + if (problem.isGoalState(problem.initialState())) { + return nodeFactory.newRootNode(problem.initialState()); + } + // frontier ← a priority queue ordered by pathCost, with a node for the initial state + frontier.clear(); + frontier.add(nodeFactory.newRootNode(problem.initialState())); + HashMap> reached = new HashMap<>(); + Node solution = null; + // while frontier is not empty and top(frontier) is cheaper than solution do + while (!frontier.isEmpty() && + (solution == null || frontier.peek().pathCost() < solution.pathCost())) { + Node parent = frontier.poll(); + for (Node child : + SearchUtils.successors(problem, parent)) { + S s = child.state(); + // if s is not in reached or child is a cheaper path than reached[s] then + if (!reached.containsKey(s) || + child.pathCost() < reached.get(s).pathCost()) { + reached.put(s, child); + frontier.add(child); + // if child is a goal and is cheaper than solution then + if (problem.isGoalState(s) && (solution == null || child.pathCost() < solution.pathCost())) { + solution = child; + } + } + } + } + return solution; + } + + /** + * Returns a list of actions to be taken to reach the goal state. + * + * @param problem + * @return + */ + @Override + public List apply(Problem problem) { + Node solution = this.search(problem); + if (solution == null) + return new ArrayList<>(); + return SearchUtils.generateActions(solution); + } +} diff --git a/test/src/test/java/aima/test/unit/search/informed/GreedyBestFirstSearchTest.java b/test/src/test/java/aima/test/unit/search/informed/GreedyBestFirstSearchTest.java index 372bdc97e9..9d51a437f3 100644 --- a/test/src/test/java/aima/test/unit/search/informed/GreedyBestFirstSearchTest.java +++ b/test/src/test/java/aima/test/unit/search/informed/GreedyBestFirstSearchTest.java @@ -1,70 +1,67 @@ package aima.test.unit.search.informed; -import static aima.core.environment.map2d.SimplifiedRoadMapOfPartOfRomania.*; -import static org.junit.Assert.assertEquals; - -import aima.core.environment.map2d.GoAction; -import aima.core.environment.map2d.InState; -import aima.core.environment.map2d.Map2D; -import aima.core.environment.map2d.Map2DFunctionFactory; -import aima.core.environment.map2d.SimplifiedRoadMapOfPartOfRomania; +import aima.core.environment.map2d.*; import aima.core.environment.support.ProblemFactory; import aima.core.search.api.Node; import aima.core.search.api.Problem; import aima.core.search.api.SearchForActionsFunction; import aima.core.search.basic.informed.GreedyBestFirstSearch; -import java.util.Arrays; -import java.util.Collection; -import java.util.List; -import java.util.function.ToDoubleFunction; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; import org.junit.runners.Parameterized.Parameter; import org.junit.runners.Parameterized.Parameters; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.List; +import java.util.function.ToDoubleFunction; + +import static aima.core.environment.map2d.SimplifiedRoadMapOfPartOfRomania.*; +import static org.junit.Assert.assertEquals; + @RunWith(Parameterized.class) public class GreedyBestFirstSearchTest { - private static final String GREEDYBFS = "GreedyBestFirstSearch"; + private static final String GREEDYBFS = "GreedyBestFirstSearch"; + @Parameter + public String searchFunctionName; - @Parameters - public static Collection implementations() { - return Arrays.asList(new Object[][]{{GREEDYBFS}}); - } + @Parameters + public static Collection implementations() { + return Arrays.asList(new Object[][]{{GREEDYBFS}}); + } - @Parameter - public String searchFunctionName; + @Test + public void testSimplifiedRoadMapOfPartOfRomania() { + Map2D map = new SimplifiedRoadMapOfPartOfRomania(); + String initialLocation = ARAD; - @Test - public void testSimplifiedRoadMapOfPartOfRomania() { - Map2D map = new SimplifiedRoadMapOfPartOfRomania(); - String initialLocation = ARAD; + String finalLocation = initialLocation; + Problem problem = ProblemFactory + .getSimplifiedRoadMapOfPartOfRomaniaProblem(initialLocation, finalLocation); + List goal = new ArrayList<>(); + assertEquals(goal, searchForActions(problem, + new Map2DFunctionFactory.StraightLineDistanceHeuristic(map, finalLocation))); - String finalLocation = initialLocation; - Problem problem = ProblemFactory - .getSimplifiedRoadMapOfPartOfRomaniaProblem(initialLocation, finalLocation); - List goal = Arrays.asList((GoAction) null); - assertEquals(goal, searchForActions(problem, - new Map2DFunctionFactory.StraightLineDistanceHeuristic(map, finalLocation))); + finalLocation = BUCHAREST; + problem = ProblemFactory + .getSimplifiedRoadMapOfPartOfRomaniaProblem(initialLocation, finalLocation); + goal = Arrays.asList(new GoAction(SIBIU), new GoAction(FAGARAS), new GoAction(BUCHAREST)); + assertEquals(goal, searchForActions(problem, + new Map2DFunctionFactory.StraightLineDistanceHeuristic(map, finalLocation))); + } - finalLocation = BUCHAREST; - problem = ProblemFactory - .getSimplifiedRoadMapOfPartOfRomaniaProblem(initialLocation, finalLocation); - goal = Arrays.asList(new GoAction(SIBIU), new GoAction(FAGARAS), new GoAction(BUCHAREST)); - assertEquals(goal, searchForActions(problem, - new Map2DFunctionFactory.StraightLineDistanceHeuristic(map, finalLocation))); - } + private List searchForActions(Problem problem, ToDoubleFunction> h) { + SearchForActionsFunction searchForActionsFunction; - private List searchForActions(Problem problem, ToDoubleFunction> h) { - SearchForActionsFunction searchForActionsFunction; + if (GREEDYBFS.equals(searchFunctionName)) { + searchForActionsFunction = new GreedyBestFirstSearch<>(h); + } else { + throw new UnsupportedOperationException(); + } - if (GREEDYBFS.equals(searchFunctionName)) { - searchForActionsFunction = new GreedyBestFirstSearch<>(h); - } else { - throw new UnsupportedOperationException(); + return searchForActionsFunction.apply(problem); } - - return searchForActionsFunction.apply(problem); - } } diff --git a/test/src/test/java/aima/test/unit/search/informed/InformedSearchTest.java b/test/src/test/java/aima/test/unit/search/informed/InformedSearchTest.java index b8fba21470..8d72476e72 100644 --- a/test/src/test/java/aima/test/unit/search/informed/InformedSearchTest.java +++ b/test/src/test/java/aima/test/unit/search/informed/InformedSearchTest.java @@ -1,10 +1,6 @@ package aima.test.unit.search.informed; -import aima.core.environment.map2d.GoAction; -import aima.core.environment.map2d.InState; -import aima.core.environment.map2d.Map2D; -import aima.core.environment.map2d.Map2DFunctionFactory; -import aima.core.environment.map2d.SimplifiedRoadMapOfPartOfRomania; +import aima.core.environment.map2d.*; import aima.core.environment.support.ProblemFactory; import aima.core.search.api.Node; import aima.core.search.api.Problem; @@ -17,6 +13,7 @@ import org.junit.runners.Parameterized.Parameter; import org.junit.runners.Parameterized.Parameters; +import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.List; @@ -27,47 +24,49 @@ @RunWith(Parameterized.class) public class InformedSearchTest { - private static final String A_STAR = "AStarSearch"; - private static final String RBFS = "RecursiveBestFirstSearch"; + private static final String A_STAR = "AStarSearch"; + private static final String RBFS = "RecursiveBestFirstSearch"; + @Parameter + public String searchFunctionName; - @Parameters(name = "{index}: {0}") - public static Collection implementations() { - return Arrays.asList(new Object[][] { { RBFS }, { A_STAR } }); - } + @Parameters(name = "{index}: {0}") + public static Collection implementations() { + return Arrays.asList(new Object[][]{{RBFS}, {A_STAR}}); + } - @Parameter - public String searchFunctionName; + public List searchForActions(Problem problem, ToDoubleFunction> hf) { - public List searchForActions(Problem problem, ToDoubleFunction> hf) { + SearchForActionsFunction searchForActionsFunction; + if (A_STAR.equals(searchFunctionName)) { + searchForActionsFunction = new AStarSearch<>(hf); + } else if (RBFS.equals(searchFunctionName)) { + searchForActionsFunction = new RecursiveBestFirstSearch<>(hf); + } else { + throw new UnsupportedOperationException(); + } + return searchForActionsFunction.apply(problem); + } - SearchForActionsFunction searchForActionsFunction; - if (A_STAR.equals(searchFunctionName)) { - searchForActionsFunction = new AStarSearch<>(hf); - } else if (RBFS.equals(searchFunctionName)) { - searchForActionsFunction = new RecursiveBestFirstSearch<>(hf); - } else { - throw new UnsupportedOperationException(); - } - return searchForActionsFunction.apply(problem); - } + @Test + public void testSimplifiedRoadMapOfPartOfRomania() { + Map2D map = new SimplifiedRoadMapOfPartOfRomania(); + String initialLocation = SimplifiedRoadMapOfPartOfRomania.ARAD; + String goal = initialLocation; - @Test - public void testSimplifiedRoadMapOfPartOfRomania() { - Map2D map = new SimplifiedRoadMapOfPartOfRomania(); - String initialLocation = SimplifiedRoadMapOfPartOfRomania.ARAD; - String goal = initialLocation; + Problem problem = ProblemFactory.getSimplifiedRoadMapOfPartOfRomaniaProblem(initialLocation, + goal); + if (A_STAR.equals(searchFunctionName)) + assertEquals(new ArrayList<>(), searchForActions(problem, new Map2DFunctionFactory.StraightLineDistanceHeuristic(map, goal))); + else + assertEquals(Arrays.asList((String) null), searchForActions(problem, new Map2DFunctionFactory.StraightLineDistanceHeuristic(map, goal))); - Problem problem = ProblemFactory.getSimplifiedRoadMapOfPartOfRomaniaProblem(initialLocation, - goal); - assertEquals(Arrays.asList((String) null), searchForActions(problem, new Map2DFunctionFactory.StraightLineDistanceHeuristic(map, goal))); - - goal = SimplifiedRoadMapOfPartOfRomania.BUCHAREST; - problem = ProblemFactory.getSimplifiedRoadMapOfPartOfRomaniaProblem(initialLocation, goal); - assertEquals( - Arrays.asList(new GoAction(SimplifiedRoadMapOfPartOfRomania.SIBIU), - new GoAction(SimplifiedRoadMapOfPartOfRomania.RIMNICU_VILCEA), - new GoAction(SimplifiedRoadMapOfPartOfRomania.PITESTI), - new GoAction(SimplifiedRoadMapOfPartOfRomania.BUCHAREST)), - searchForActions(problem, new Map2DFunctionFactory.StraightLineDistanceHeuristic(map, goal))); - } + goal = SimplifiedRoadMapOfPartOfRomania.BUCHAREST; + problem = ProblemFactory.getSimplifiedRoadMapOfPartOfRomaniaProblem(initialLocation, goal); + assertEquals( + Arrays.asList(new GoAction(SimplifiedRoadMapOfPartOfRomania.SIBIU), + new GoAction(SimplifiedRoadMapOfPartOfRomania.RIMNICU_VILCEA), + new GoAction(SimplifiedRoadMapOfPartOfRomania.PITESTI), + new GoAction(SimplifiedRoadMapOfPartOfRomania.BUCHAREST)), + searchForActions(problem, new Map2DFunctionFactory.StraightLineDistanceHeuristic(map, goal))); + } } diff --git a/test/src/test/java/aima/test/unit/search/local/HillClimbingSearchTest.java b/test/src/test/java/aima/test/unit/search/local/HillClimbingSearchTest.java index baeb7abcdc..75c8a614b8 100644 --- a/test/src/test/java/aima/test/unit/search/local/HillClimbingSearchTest.java +++ b/test/src/test/java/aima/test/unit/search/local/HillClimbingSearchTest.java @@ -1,9 +1,11 @@ package aima.test.unit.search.local; -import java.util.Arrays; -import java.util.Collection; -import java.util.function.ToDoubleFunction; - +import aima.core.environment.support.ProblemFactory; +import aima.core.search.api.Problem; +import aima.core.search.api.SearchForStateFunction; +import aima.core.search.basic.local.HillClimbingSearch; +import aima.core.util.datastructure.Pair; +import aima.extra.search.local.HillClimbingSearchWithSidewaysMoves; import org.junit.Assert; import org.junit.Test; import org.junit.runner.RunWith; @@ -11,12 +13,9 @@ import org.junit.runners.Parameterized.Parameter; import org.junit.runners.Parameterized.Parameters; -import aima.core.environment.support.ProblemFactory; -import aima.core.search.api.Problem; -import aima.core.search.api.SearchForStateFunction; -import aima.core.search.basic.local.HillClimbingSearch; -import aima.core.util.datastructure.Pair; -import aima.extra.search.local.HillClimbingSearchWithSidewaysMoves; +import java.util.Arrays; +import java.util.Collection; +import java.util.function.ToDoubleFunction; /** * @author Ciaran O'Reilly @@ -25,83 +24,81 @@ @RunWith(Parameterized.class) public class HillClimbingSearchTest { - @Parameters(name = "{index}: {0}") - public static Collection implementations() { - return Arrays.asList(new Object[][] { { "HillClimbingSearch" }, { "HillClimbingSearchWithSidewaysMoves" } }); - } + @Parameter + public String searchForStateFunctionName; + // The state value function will be represented by the ascii value of the + // first character in the state name. + ToDoubleFunction asciiChar0StateValueFn = state -> { + return (double) state.charAt(0); + }; + ToDoubleFunction> y_valueFn = x_y -> x_y.getSecond().doubleValue(); + + @Parameters(name = "{index}: {0}") + public static Collection implementations() { + return Arrays.asList(new Object[][]{{"HillClimbingSearch"}, {"HillClimbingSearchWithSidewaysMoves"}}); + } + + public S searchForState(Problem problem, ToDoubleFunction stateValueFn, + boolean isSteepestAscentVersion) { + SearchForStateFunction searchForStateFunction; + + if ("HillClimbingSearch".equals(searchForStateFunctionName)) { + searchForStateFunction = new HillClimbingSearch(stateValueFn, isSteepestAscentVersion); + } else { + searchForStateFunction = new HillClimbingSearchWithSidewaysMoves(stateValueFn, isSteepestAscentVersion); + } + return searchForStateFunction.apply(problem); + } - @Parameter - public String searchForStateFunctionName; + @Test + public void testReachableGlobalMaximum() { + Assert.assertEquals("Z", + searchForState(ProblemFactory.getSimpleBinaryTreeProblem("F", "Z"), asciiChar0StateValueFn, true)); - // The state value function will be represented by the ascii value of the - // first character in the state name. - ToDoubleFunction asciiChar0StateValueFn = state -> { - return (double) state.charAt(0); - }; + Assert.assertEquals(ProblemFactory.DEFAULT_DISCRETE_FUNCTION_GLOBAL_MAXIMIM, + searchForState(ProblemFactory.getDefaultSimpleDiscreteFunctionProblem(6, true), y_valueFn, true) + .getSecond().intValue()); - ToDoubleFunction> y_valueFn = x_y -> x_y.getSecond().doubleValue(); + Assert.assertEquals(ProblemFactory.DEFAULT_DISCRETE_FUNCTION_GLOBAL_MAXIMIM, + searchForState(ProblemFactory.getDefaultSimpleDiscreteFunctionProblem(12, true), y_valueFn, true) + .getSecond().intValue()); + } - public S searchForState(Problem problem, ToDoubleFunction stateValueFn, - boolean isSteepestAscentVersion) { - SearchForStateFunction searchForStateFunction; + @Test + public void testReachableLocalMaximum() { + Assert.assertEquals("O", + searchForState(ProblemFactory.getSimpleBinaryTreeProblem("A", "Z"), asciiChar0StateValueFn, true)); - if ("HillClimbingSearch".equals(searchForStateFunctionName)) { - searchForStateFunction = new HillClimbingSearch(stateValueFn, isSteepestAscentVersion); - } else { - searchForStateFunction = new HillClimbingSearchWithSidewaysMoves(stateValueFn, isSteepestAscentVersion); - } - return searchForStateFunction.apply(problem); - } + Assert.assertEquals(ProblemFactory.DEFAULT_DISCRETE_FUNCTION_LOCAL_MAXIMUM_VALUE, + searchForState(ProblemFactory.getDefaultSimpleDiscreteFunctionProblem(14, true), y_valueFn, true) + .getSecond().intValue()); + } - @Test - public void testReachableGlobalMaximum() { - Assert.assertEquals("Z", - searchForState(ProblemFactory.getSimpleBinaryTreeProblem("F", "Z"), asciiChar0StateValueFn, true)); - - Assert.assertEquals(ProblemFactory.DEFAULT_DISCRETE_FUNCTION_GLOBAL_MAXIMIM, - searchForState(ProblemFactory.getDefaultSimpleDiscreteFunctionProblem(6, true), y_valueFn, true) - .getSecond().intValue()); - - Assert.assertEquals(ProblemFactory.DEFAULT_DISCRETE_FUNCTION_GLOBAL_MAXIMIM, - searchForState(ProblemFactory.getDefaultSimpleDiscreteFunctionProblem(12, true), y_valueFn, true) - .getSecond().intValue()); - } + @Test + public void testNoSuccessors() { + Assert.assertEquals("P", + searchForState(ProblemFactory.getSimpleBinaryTreeProblem("P", "Z"), asciiChar0StateValueFn, true)); + Assert.assertEquals("F", + searchForState(ProblemFactory.getSimpleBinaryTreeProblem("F", "Z"), asciiChar0StateValueFn, false)); - @Test - public void testReachableLocalMaximum() { - Assert.assertEquals("O", - searchForState(ProblemFactory.getSimpleBinaryTreeProblem("A", "Z"), asciiChar0StateValueFn, true)); - - Assert.assertEquals(ProblemFactory.DEFAULT_DISCRETE_FUNCTION_LOCAL_MAXIMUM_VALUE, - searchForState(ProblemFactory.getDefaultSimpleDiscreteFunctionProblem(14, true), y_valueFn, true) - .getSecond().intValue()); - } + Assert.assertEquals(ProblemFactory.DEFAULT_DISCRETE_FUNCTION_LOCAL_MAXIMUM_VALUE, + searchForState(ProblemFactory.getDefaultSimpleDiscreteFunctionProblem(16, true), y_valueFn, true) + .getSecond().intValue()); + } - @Test - public void testNoSuccessors() { - Assert.assertEquals("P", - searchForState(ProblemFactory.getSimpleBinaryTreeProblem("P", "Z"), asciiChar0StateValueFn, true)); - Assert.assertEquals("F", - searchForState(ProblemFactory.getSimpleBinaryTreeProblem("F", "Z"), asciiChar0StateValueFn, false)); - - Assert.assertEquals(ProblemFactory.DEFAULT_DISCRETE_FUNCTION_LOCAL_MAXIMUM_VALUE, - searchForState(ProblemFactory.getDefaultSimpleDiscreteFunctionProblem(16, true), y_valueFn, true) - .getSecond().intValue()); - } - - @Test - public void testSidewaysMoves() { - if ("HillClimbingSearch".equals(searchForStateFunctionName)) { - // Not supported by simple example implementation - Assert.assertEquals(ProblemFactory.DEFAULT_DISCRETE_FUNCTION_SHOULDER_VALUE, - searchForState(ProblemFactory.getDefaultSimpleDiscreteFunctionProblem(3, true), y_valueFn, true) - .getSecond().intValue()); - } - // Supported by alternative HillClimbing search implementation - else { - Assert.assertEquals(ProblemFactory.DEFAULT_DISCRETE_FUNCTION_GLOBAL_MAXIMIM, - searchForState(ProblemFactory.getDefaultSimpleDiscreteFunctionProblem(3, true), y_valueFn, true) - .getSecond().intValue()); - } - } + @Test + public void testSidewaysMoves() { + if ("HillClimbingSearch".equals(searchForStateFunctionName)) { + // Not supported by simple example implementation + Assert.assertEquals(ProblemFactory.DEFAULT_DISCRETE_FUNCTION_SHOULDER_VALUE, + searchForState(ProblemFactory.getDefaultSimpleDiscreteFunctionProblem(3, true), y_valueFn, true) + .getSecond().intValue()); + } + // Supported by alternative HillClimbing search implementation + else { + Assert.assertEquals(ProblemFactory.DEFAULT_DISCRETE_FUNCTION_GLOBAL_MAXIMIM, + searchForState(ProblemFactory.getDefaultSimpleDiscreteFunctionProblem(3, true), y_valueFn, true) + .getSecond().intValue()); + } + } } diff --git a/test/src/test/java/aima/test/unit/search/local/SimulatedAnnealingTest.java b/test/src/test/java/aima/test/unit/search/local/SimulatedAnnealingTest.java index 795bee945e..f4735a5482 100644 --- a/test/src/test/java/aima/test/unit/search/local/SimulatedAnnealingTest.java +++ b/test/src/test/java/aima/test/unit/search/local/SimulatedAnnealingTest.java @@ -1,74 +1,73 @@ package aima.test.unit.search.local; -import java.util.Arrays; -import java.util.Collection; -import java.util.function.ToDoubleFunction; - -import org.junit.Test; -import org.junit.runners.Parameterized.Parameter; -import org.junit.runners.Parameterized.Parameters; - import aima.core.environment.support.ProblemFactory; import aima.core.search.api.Problem; import aima.core.search.api.SearchForStateFunction; import aima.core.search.basic.local.SimulatedAnnealingSearch; import aima.core.util.datastructure.Pair; +import org.junit.Test; +import org.junit.runners.Parameterized.Parameter; +import org.junit.runners.Parameterized.Parameters; + +import java.util.Arrays; +import java.util.Collection; +import java.util.function.ToDoubleFunction; /** - * * @author Ciaran O'Reilly * @author Anurag Rai */ public class SimulatedAnnealingTest { - @Parameters(name = "{index}: {0}") - public static Collection implementations() { - return Arrays.asList(new Object[][] { { "SimulatedAnnealingSearch" } }); - } - - @Parameter - public String searchForStateFunctionName; + @Parameter + public String searchForStateFunctionName; + // The state value function will be represented by the ascii value of the + // first character in the state name. + ToDoubleFunction asciiChar0StateValueFn = state -> { + return (double) state.charAt(0); + }; + ToDoubleFunction> y_valueFn = x_y -> x_y.getSecond().doubleValue(); + + @Parameters(name = "{index}: {0}") + public static Collection implementations() { + return Arrays.asList(new Object[][]{{"SimulatedAnnealingSearch"}}); + } + + public S searchForState(Problem problem, ToDoubleFunction stateValueFn, boolean isGradientAscentVersion) { + SearchForStateFunction searchForStateFunction; + + searchForStateFunction = new SimulatedAnnealingSearch(stateValueFn, isGradientAscentVersion); + + return searchForStateFunction.apply(problem); + } + + // + // NOTE: We use timeouts as simulated-annealing selects a random action so in most cases you cannot predetermine its result. + @Test(timeout = 1000) + public void testReachableGlobalMaximum() { + while (!"Z".equals(searchForState(ProblemFactory.getSimpleBinaryTreeProblem("F", "Z"), asciiChar0StateValueFn, true))) + ; - // The state value function will be represented by the ascii value of the - // first character in the state name. - ToDoubleFunction asciiChar0StateValueFn = state -> { - return (double) state.charAt(0); - }; - - ToDoubleFunction> y_valueFn = x_y -> x_y.getSecond().doubleValue(); + while (ProblemFactory.DEFAULT_DISCRETE_FUNCTION_GLOBAL_MAXIMIM != + searchForState(ProblemFactory.getDefaultSimpleDiscreteFunctionProblem(6, true), y_valueFn, true) + .getSecond().intValue()) ; - public S searchForState(Problem problem, ToDoubleFunction stateValueFn, boolean isGradientAscentVersion) { - SearchForStateFunction searchForStateFunction; - - searchForStateFunction = new SimulatedAnnealingSearch(stateValueFn, isGradientAscentVersion); - - return searchForStateFunction.apply(problem); - } - - // - // NOTE: We use timeouts as simulated-annealing selects a random action so in most cases you cannot predetermine its result. - @Test(timeout=1000) - public void testReachableGlobalMaximum() { - while (!"Z".equals(searchForState(ProblemFactory.getSimpleBinaryTreeProblem("F", "Z"), asciiChar0StateValueFn, true))); - - while(ProblemFactory.DEFAULT_DISCRETE_FUNCTION_GLOBAL_MAXIMIM != - searchForState(ProblemFactory.getDefaultSimpleDiscreteFunctionProblem(6, true), y_valueFn, true) - .getSecond().intValue()); - - // Simulated annealing should always find the global maximum in this search space within a reasonable number of attempts. - for (int x = 0; x < ProblemFactory.DEFAULT_DISCRETE_FUNCTION_DEPENDENT_VALUES.length; x++) { - while(ProblemFactory.DEFAULT_DISCRETE_FUNCTION_GLOBAL_MAXIMIM != - searchForState(ProblemFactory.getDefaultSimpleDiscreteFunctionProblem(x, true), y_valueFn, true) - .getSecond().intValue()); - } - } + // Simulated annealing should always find the global maximum in this search space within a reasonable number of attempts. + for (int x = 0; x < ProblemFactory.DEFAULT_DISCRETE_FUNCTION_DEPENDENT_VALUES.length; x++) { + while (ProblemFactory.DEFAULT_DISCRETE_FUNCTION_GLOBAL_MAXIMIM != + searchForState(ProblemFactory.getDefaultSimpleDiscreteFunctionProblem(x, true), y_valueFn, true) + .getSecond().intValue()) ; + } + } - @Test(timeout=1000) - public void testReachableLocalMaximum() { - while(!"O".equals(searchForState(ProblemFactory.getSimpleBinaryTreeProblem("A", "Z"), asciiChar0StateValueFn, true))); - } + @Test(timeout = 1000) + public void testReachableLocalMaximum() { + while (!"O".equals(searchForState(ProblemFactory.getSimpleBinaryTreeProblem("A", "Z"), asciiChar0StateValueFn, true))) + ; + } - @Test(timeout=1000) - public void testNoSuccessors() { - while (!"P".equals(searchForState(ProblemFactory.getSimpleBinaryTreeProblem("P", "Z"), asciiChar0StateValueFn, true))); - } + @Test(timeout = 1000) + public void testNoSuccessors() { + while (!"P".equals(searchForState(ProblemFactory.getSimpleBinaryTreeProblem("P", "Z"), asciiChar0StateValueFn, true))) + ; + } } diff --git a/test/src/test/java/aima/test/unit/search/uninformedsearch/BreadthFirstSearchTest.java b/test/src/test/java/aima/test/unit/search/uninformedsearch/BreadthFirstSearchTest.java new file mode 100644 index 0000000000..d02d3ee649 --- /dev/null +++ b/test/src/test/java/aima/test/unit/search/uninformedsearch/BreadthFirstSearchTest.java @@ -0,0 +1,136 @@ +package aima.test.unit.search.uninformedsearch; + +import aima.core.environment.map2d.GoAction; +import aima.core.environment.map2d.SimplifiedRoadMapOfPartOfRomania; +import aima.core.environment.support.ProblemFactory; +import aima.core.environment.vacuum.VELocalState; +import aima.core.environment.vacuum.VacuumEnvironment; +import aima.core.search.api.Node; +import aima.core.search.api.Problem; +import aima.core.search.basic.uninformedsearch.BreadthFirstSearch; +import org.junit.Assert; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +/** + * @author samagra + */ +public class BreadthFirstSearchTest { + BreadthFirstSearch breadthFirstSearch = new BreadthFirstSearch(); + + @Test + public void testSimplifiedRomania() { + Problem problem = ProblemFactory.getSimplifiedRoadMapOfPartOfRomaniaProblem( + SimplifiedRoadMapOfPartOfRomania.ARAD, SimplifiedRoadMapOfPartOfRomania.BUCHAREST); + Node result = breadthFirstSearch.search(problem); + Assert.assertEquals("In(Bucharest)", result.state().toString()); + List actions = Arrays.asList(new GoAction(SimplifiedRoadMapOfPartOfRomania.SIBIU), + new GoAction(SimplifiedRoadMapOfPartOfRomania.FAGARAS), + new GoAction(SimplifiedRoadMapOfPartOfRomania.BUCHAREST)); + Assert.assertEquals(actions, breadthFirstSearch.apply(problem)); + } + + @Test + public void testVacuumEnvironment() { + Assert.assertEquals(new ArrayList(), + breadthFirstSearch.apply(ProblemFactory.getSimpleVacuumWorldProblem("A", + new VELocalState("A", VacuumEnvironment.Status.Clean), + new VELocalState("B", VacuumEnvironment.Status.Clean)))); + + Assert.assertEquals(Arrays.asList(VacuumEnvironment.ACTION_RIGHT, VacuumEnvironment.ACTION_SUCK), + breadthFirstSearch.apply(ProblemFactory.getSimpleVacuumWorldProblem("A", + new VELocalState("A", VacuumEnvironment.Status.Clean), + new VELocalState("B", VacuumEnvironment.Status.Dirty)))); + + Assert.assertEquals(Arrays.asList(VacuumEnvironment.ACTION_SUCK), + breadthFirstSearch.apply(ProblemFactory.getSimpleVacuumWorldProblem("A", + new VELocalState("A", VacuumEnvironment.Status.Dirty), + new VELocalState("B", VacuumEnvironment.Status.Clean)))); + + Assert.assertEquals( + Arrays.asList(VacuumEnvironment.ACTION_SUCK, VacuumEnvironment.ACTION_RIGHT, + VacuumEnvironment.ACTION_SUCK), + breadthFirstSearch.apply(ProblemFactory.getSimpleVacuumWorldProblem("A", + new VELocalState("A", VacuumEnvironment.Status.Dirty), + new VELocalState("B", VacuumEnvironment.Status.Dirty)))); + + Assert.assertEquals(new ArrayList(), + breadthFirstSearch.apply(ProblemFactory.getSimpleVacuumWorldProblem("B", + new VELocalState("A", VacuumEnvironment.Status.Clean), + new VELocalState("B", VacuumEnvironment.Status.Clean)))); + + Assert.assertEquals(Arrays.asList(VacuumEnvironment.ACTION_SUCK), + breadthFirstSearch.apply(ProblemFactory.getSimpleVacuumWorldProblem("B", + new VELocalState("A", VacuumEnvironment.Status.Clean), + new VELocalState("B", VacuumEnvironment.Status.Dirty)))); + + Assert.assertEquals(Arrays.asList(VacuumEnvironment.ACTION_LEFT, VacuumEnvironment.ACTION_SUCK), + breadthFirstSearch.apply(ProblemFactory.getSimpleVacuumWorldProblem("B", + new VELocalState("A", VacuumEnvironment.Status.Dirty), + new VELocalState("B", VacuumEnvironment.Status.Clean)))); + + Assert.assertEquals( + Arrays.asList(VacuumEnvironment.ACTION_SUCK, VacuumEnvironment.ACTION_LEFT, + VacuumEnvironment.ACTION_SUCK), + breadthFirstSearch.apply(ProblemFactory.getSimpleVacuumWorldProblem("B", + new VELocalState("A", VacuumEnvironment.Status.Dirty), + new VELocalState("B", VacuumEnvironment.Status.Dirty)))); + + // A slightly larger world + Assert.assertEquals( + Arrays.asList(VacuumEnvironment.ACTION_SUCK, VacuumEnvironment.ACTION_RIGHT, + VacuumEnvironment.ACTION_SUCK, VacuumEnvironment.ACTION_RIGHT, VacuumEnvironment.ACTION_SUCK), + breadthFirstSearch.apply(ProblemFactory.getSimpleVacuumWorldProblem("A", + new VELocalState("A", VacuumEnvironment.Status.Dirty), + new VELocalState("B", VacuumEnvironment.Status.Dirty), + new VELocalState("C", VacuumEnvironment.Status.Dirty)))); + + Assert.assertEquals( + Arrays.asList(VacuumEnvironment.ACTION_SUCK, VacuumEnvironment.ACTION_LEFT, + VacuumEnvironment.ACTION_SUCK, VacuumEnvironment.ACTION_LEFT, VacuumEnvironment.ACTION_SUCK), + breadthFirstSearch.apply(ProblemFactory.getSimpleVacuumWorldProblem("C", + new VELocalState("A", VacuumEnvironment.Status.Dirty), + new VELocalState("B", VacuumEnvironment.Status.Dirty), + new VELocalState("C", VacuumEnvironment.Status.Dirty)))); + + Assert.assertEquals( + // NOTE: This sequence of actions is only correct because we + // always order our possible actions [Left, Suck, Right] in + // ProblemFactory.getSimpleVacuumWorldProblem(), if the order + // was different we would likely get a different answer. + Arrays.asList(VacuumEnvironment.ACTION_LEFT, VacuumEnvironment.ACTION_SUCK, + VacuumEnvironment.ACTION_RIGHT, VacuumEnvironment.ACTION_SUCK, VacuumEnvironment.ACTION_RIGHT, + VacuumEnvironment.ACTION_SUCK), + breadthFirstSearch.apply(ProblemFactory.getSimpleVacuumWorldProblem("B", + new VELocalState("A", VacuumEnvironment.Status.Dirty), + new VELocalState("B", VacuumEnvironment.Status.Dirty), + new VELocalState("C", VacuumEnvironment.Status.Dirty)))); + + } + + @Test + public void simpleBinaryTreeTests() { + Assert.assertEquals(new ArrayList(), + breadthFirstSearch.apply(ProblemFactory.getSimpleBinaryTreeProblem("A", "A"))); + + Assert.assertEquals(Arrays.asList("B"), breadthFirstSearch.apply(ProblemFactory.getSimpleBinaryTreeProblem("A", "B"))); + + Assert.assertEquals(Arrays.asList("C"), breadthFirstSearch.apply(ProblemFactory.getSimpleBinaryTreeProblem("A", "C"))); + + Assert.assertEquals(Arrays.asList("B", "E"), + breadthFirstSearch.apply(ProblemFactory.getSimpleBinaryTreeProblem("A", "E"))); + + Assert.assertEquals(Arrays.asList("C", "F", "L"), + breadthFirstSearch.apply(ProblemFactory.getSimpleBinaryTreeProblem("A", "L"))); + } + + @Test + public void unreachableStates() { + Assert.assertEquals(Collections.emptyList(), + breadthFirstSearch.apply(ProblemFactory.getSimpleBinaryTreeProblem("B", "A"))); + } +} diff --git a/test/src/test/java/aima/test/unit/search/uninformedsearch/GenericSearchTest.java b/test/src/test/java/aima/test/unit/search/uninformedsearch/GenericSearchTest.java new file mode 100644 index 0000000000..db7e52f137 --- /dev/null +++ b/test/src/test/java/aima/test/unit/search/uninformedsearch/GenericSearchTest.java @@ -0,0 +1,61 @@ +package aima.test.unit.search.uninformedsearch; + +import aima.core.environment.map2d.SimplifiedRoadMapOfPartOfRomania; +import aima.core.environment.support.ProblemFactory; +import aima.core.search.api.Node; +import aima.core.search.basic.uninformedsearch.GenericSearch; +import org.junit.Assert; +import org.junit.Test; + +import java.util.HashMap; +import java.util.LinkedList; +import java.util.Queue; + +/** + * @author samagra + */ +public class GenericSearchTest { + // Generic Search as breadth first search + GenericSearch bfs = new GenericSearch() { + @Override + public Queue addToFrontier(Node child, Queue frontier) { + ((LinkedList) frontier).addLast(child); + return frontier; + } + + @Override + public boolean canImprove(HashMap reached, Node solution) { + return solution == null; + } + }; + + // GenericSearch as depth first search + GenericSearch dfs = new GenericSearch() { + @Override + public Queue addToFrontier(Node child, Queue frontier) { + ((LinkedList) frontier).addFirst(child); + return frontier; + } + + @Override + public boolean canImprove(HashMap reached, Node solution) { + return solution == null; + } + }; + + @Test + public void romaniabfsTest() { + Node solution = bfs.genericSearch(ProblemFactory.getSimplifiedRoadMapOfPartOfRomaniaProblem( + SimplifiedRoadMapOfPartOfRomania.ARAD, SimplifiedRoadMapOfPartOfRomania.ARAD)); + Assert.assertEquals("In(Arad)", solution.state().toString()); + System.out.println(bfs.reached.toString()); + } + + @Test + public void romaniadfsTest() { + Node solution = dfs.genericSearch(ProblemFactory.getSimplifiedRoadMapOfPartOfRomaniaProblem( + SimplifiedRoadMapOfPartOfRomania.ARAD, SimplifiedRoadMapOfPartOfRomania.ARAD)); + Assert.assertEquals("In(Arad)", solution.state().toString()); + System.out.println(dfs.reached.toString()); + } +} diff --git a/test/src/test/java/aima/test/unit/search/uninformedsearch/IterativeDeepeningSearchTest.java b/test/src/test/java/aima/test/unit/search/uninformedsearch/IterativeDeepeningSearchTest.java new file mode 100644 index 0000000000..618dd21f75 --- /dev/null +++ b/test/src/test/java/aima/test/unit/search/uninformedsearch/IterativeDeepeningSearchTest.java @@ -0,0 +1,112 @@ +package aima.test.unit.search.uninformedsearch; + +import aima.core.environment.map2d.GoAction; +import aima.core.environment.map2d.SimplifiedRoadMapOfPartOfRomania; +import aima.core.environment.support.ProblemFactory; +import aima.core.environment.vacuum.VELocalState; +import aima.core.environment.vacuum.VacuumEnvironment; +import aima.core.search.api.Node; +import aima.core.search.api.Problem; +import aima.core.search.basic.uninformedsearch.IterativeDeepeningSearch; +import org.junit.Assert; +import org.junit.Test; + +import java.util.Arrays; +import java.util.List; + +public class IterativeDeepeningSearchTest { + IterativeDeepeningSearch iterativeDeepeningSearch = new IterativeDeepeningSearch(); + + @Test + public void testSimplifiedRomania() { + Problem problem = ProblemFactory.getSimplifiedRoadMapOfPartOfRomaniaProblem( + SimplifiedRoadMapOfPartOfRomania.ARAD, SimplifiedRoadMapOfPartOfRomania.BUCHAREST); + Node result = iterativeDeepeningSearch.search(problem); + Assert.assertEquals("In(Bucharest)", result.state().toString()); + List actions = Arrays.asList(new GoAction(SimplifiedRoadMapOfPartOfRomania.SIBIU), + new GoAction(SimplifiedRoadMapOfPartOfRomania.FAGARAS), + new GoAction(SimplifiedRoadMapOfPartOfRomania.BUCHAREST)); + Assert.assertEquals(actions, iterativeDeepeningSearch.apply(problem)); + } + + @Test + public void testVacuumEnvironment() { + Assert.assertEquals(Arrays.asList(VacuumEnvironment.ACTION_RIGHT, VacuumEnvironment.ACTION_SUCK), + iterativeDeepeningSearch.apply(ProblemFactory.getSimpleVacuumWorldProblem("A", + new VELocalState("A", VacuumEnvironment.Status.Clean), + new VELocalState("B", VacuumEnvironment.Status.Dirty)))); + + Assert.assertEquals(Arrays.asList(VacuumEnvironment.ACTION_SUCK), + iterativeDeepeningSearch.apply(ProblemFactory.getSimpleVacuumWorldProblem("A", + new VELocalState("A", VacuumEnvironment.Status.Dirty), + new VELocalState("B", VacuumEnvironment.Status.Clean)))); + + Assert.assertEquals( + Arrays.asList(VacuumEnvironment.ACTION_SUCK, VacuumEnvironment.ACTION_RIGHT, + VacuumEnvironment.ACTION_SUCK), + iterativeDeepeningSearch.apply(ProblemFactory.getSimpleVacuumWorldProblem("A", + new VELocalState("A", VacuumEnvironment.Status.Dirty), + new VELocalState("B", VacuumEnvironment.Status.Dirty)))); + + Assert.assertEquals(Arrays.asList(VacuumEnvironment.ACTION_SUCK), + iterativeDeepeningSearch.apply(ProblemFactory.getSimpleVacuumWorldProblem("B", + new VELocalState("A", VacuumEnvironment.Status.Clean), + new VELocalState("B", VacuumEnvironment.Status.Dirty)))); + + Assert.assertEquals(Arrays.asList(VacuumEnvironment.ACTION_LEFT, VacuumEnvironment.ACTION_SUCK), + iterativeDeepeningSearch.apply(ProblemFactory.getSimpleVacuumWorldProblem("B", + new VELocalState("A", VacuumEnvironment.Status.Dirty), + new VELocalState("B", VacuumEnvironment.Status.Clean)))); + + Assert.assertEquals( + Arrays.asList(VacuumEnvironment.ACTION_SUCK, VacuumEnvironment.ACTION_LEFT, + VacuumEnvironment.ACTION_SUCK), + iterativeDeepeningSearch.apply(ProblemFactory.getSimpleVacuumWorldProblem("B", + new VELocalState("A", VacuumEnvironment.Status.Dirty), + new VELocalState("B", VacuumEnvironment.Status.Dirty)))); + + // A slightly larger world + Assert.assertEquals( + Arrays.asList(VacuumEnvironment.ACTION_SUCK, VacuumEnvironment.ACTION_RIGHT, + VacuumEnvironment.ACTION_SUCK, VacuumEnvironment.ACTION_RIGHT, VacuumEnvironment.ACTION_SUCK), + iterativeDeepeningSearch.apply(ProblemFactory.getSimpleVacuumWorldProblem("A", + new VELocalState("A", VacuumEnvironment.Status.Dirty), + new VELocalState("B", VacuumEnvironment.Status.Dirty), + new VELocalState("C", VacuumEnvironment.Status.Dirty)))); + + Assert.assertEquals( + Arrays.asList(VacuumEnvironment.ACTION_SUCK, VacuumEnvironment.ACTION_LEFT, + VacuumEnvironment.ACTION_SUCK, VacuumEnvironment.ACTION_LEFT, VacuumEnvironment.ACTION_SUCK), + iterativeDeepeningSearch.apply(ProblemFactory.getSimpleVacuumWorldProblem("C", + new VELocalState("A", VacuumEnvironment.Status.Dirty), + new VELocalState("B", VacuumEnvironment.Status.Dirty), + new VELocalState("C", VacuumEnvironment.Status.Dirty)))); + + Assert.assertEquals( + // NOTE: This sequence of actions is only correct because we + // always order our possible actions [Left, Suck, Right] in + // ProblemFactory.getSimpleVacuumWorldProblem(), if the order + // was different we would likely get a different answer. + Arrays.asList(VacuumEnvironment.ACTION_RIGHT, VacuumEnvironment.ACTION_SUCK, + VacuumEnvironment.ACTION_LEFT, VacuumEnvironment.ACTION_SUCK, VacuumEnvironment.ACTION_LEFT, + VacuumEnvironment.ACTION_SUCK), + iterativeDeepeningSearch.apply(ProblemFactory.getSimpleVacuumWorldProblem("B", + new VELocalState("A", VacuumEnvironment.Status.Dirty), + new VELocalState("B", VacuumEnvironment.Status.Dirty), + new VELocalState("C", VacuumEnvironment.Status.Dirty)))); + + } + + @Test + public void simpleBinaryTreeTests() { + Assert.assertEquals(Arrays.asList("B"), iterativeDeepeningSearch.apply(ProblemFactory.getSimpleBinaryTreeProblem("A", "B"))); + + Assert.assertEquals(Arrays.asList("C"), iterativeDeepeningSearch.apply(ProblemFactory.getSimpleBinaryTreeProblem("A", "C"))); + + Assert.assertEquals(Arrays.asList("B", "E"), + iterativeDeepeningSearch.apply(ProblemFactory.getSimpleBinaryTreeProblem("A", "E"))); + + Assert.assertEquals(Arrays.asList("C", "F", "L"), + iterativeDeepeningSearch.apply(ProblemFactory.getSimpleBinaryTreeProblem("A", "L"))); + } +} diff --git a/test/src/test/java/aima/test/unit/search/uninformedsearch/UniformCostSearchTest.java b/test/src/test/java/aima/test/unit/search/uninformedsearch/UniformCostSearchTest.java new file mode 100644 index 0000000000..81e858253e --- /dev/null +++ b/test/src/test/java/aima/test/unit/search/uninformedsearch/UniformCostSearchTest.java @@ -0,0 +1,25 @@ +package aima.test.unit.search.uninformedsearch; + +import aima.core.environment.map2d.GoAction; +import aima.core.environment.map2d.SimplifiedRoadMapOfPartOfRomania; +import aima.core.environment.support.ProblemFactory; +import aima.core.search.basic.uninformedsearch.UniformCostSearch; +import org.junit.Assert; +import org.junit.Test; + +import java.util.Arrays; + +public class UniformCostSearchTest { + UniformCostSearch uniformCostSearch = new UniformCostSearch(); + + @Test + public void testUniformCostSearch() { + Assert.assertEquals( + Arrays.asList(new GoAction(SimplifiedRoadMapOfPartOfRomania.SIBIU), + new GoAction(SimplifiedRoadMapOfPartOfRomania.RIMNICU_VILCEA), + new GoAction(SimplifiedRoadMapOfPartOfRomania.PITESTI), + new GoAction(SimplifiedRoadMapOfPartOfRomania.BUCHAREST)), + uniformCostSearch.apply(ProblemFactory.getSimplifiedRoadMapOfPartOfRomaniaProblem( + SimplifiedRoadMapOfPartOfRomania.ARAD, SimplifiedRoadMapOfPartOfRomania.BUCHAREST))); + } +}