package aima.core.search.local; import java.util.List; import java.util.Random; import java.util.function.Consumer; import aima.core.agent.Action; import aima.core.search.framework.Metrics; import aima.core.search.framework.Node; import aima.core.search.framework.NodeExpander; import aima.core.search.framework.SearchForActions; import aima.core.search.framework.SearchForStates; import aima.core.search.framework.SearchUtils; import aima.core.search.framework.evalfunc.HeuristicFunction; import aima.core.search.framework.problem.Problem; import aima.core.util.CancelableThread; import aima.core.util.Util; /** * Artificial Intelligence A Modern Approach (3rd Edition): Figure 4.5, page * 126.<br> * <br> * * <pre> * function SIMULATED-ANNEALING(problem, schedule) returns a solution state * * current <- MAKE-NODE(problem.INITIAL-STATE) * for t = 1 to INFINITY 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) * </pre> * * Figure 4.5 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 Ravi Mohan * @author Mike Stampone * @author Ruediger Lunde */ public class SimulatedAnnealingSearch implements SearchForActions, SearchForStates { public enum SearchOutcome { FAILURE, SOLUTION_FOUND } public static final String METRIC_NODES_EXPANDED = "nodesExpanded"; public static final String METRIC_TEMPERATURE = "temp"; public static final String METRIC_NODE_VALUE = "nodeValue"; private final HeuristicFunction hf; private final Scheduler scheduler; private final NodeExpander nodeExpander; private SearchOutcome outcome = SearchOutcome.FAILURE; private Object lastState = null; private Metrics metrics = new Metrics(); /** * Constructs a simulated annealing search from the specified heuristic * function and a default scheduler. * * @param hf * a heuristic function */ public SimulatedAnnealingSearch(HeuristicFunction hf) { this(hf, new Scheduler()); } /** * Constructs a simulated annealing search from the specified heuristic * function and scheduler. * * @param hf * a heuristic function * @param scheduler * a mapping from time to "temperature" */ public SimulatedAnnealingSearch(HeuristicFunction hf, Scheduler scheduler) { this(hf, scheduler, new NodeExpander()); } public SimulatedAnnealingSearch(HeuristicFunction hf, Scheduler scheduler, NodeExpander nodeExpander) { this.hf = hf; this.scheduler = scheduler; this.nodeExpander = nodeExpander; nodeExpander.addNodeListener((node) -> metrics.incrementInt(METRIC_NODES_EXPANDED)); } @Override public List<Action> findActions(Problem p) { nodeExpander.useParentLinks(true); Node node = findNode(p); return node == null ? SearchUtils.failure() : SearchUtils.getSequenceOfActions(node); } @Override public Object findState(Problem p) { nodeExpander.useParentLinks(false); Node node = findNode(p); return node == null ? null : node.getState(); } // function SIMULATED-ANNEALING(problem, schedule) returns a solution state public Node findNode(Problem p) { clearInstrumentation(); outcome = SearchOutcome.FAILURE; lastState = null; // current <- MAKE-NODE(problem.INITIAL-STATE) Node current = nodeExpander.createRootNode(p.getInitialState()); Node next = null; // for t = 1 to INFINITY do int timeStep = 0; while (!CancelableThread.currIsCanceled()) { // temperature <- schedule(t) double temperature = scheduler.getTemp(timeStep); timeStep++; lastState = current.getState(); // if temperature = 0 then return current if (temperature == 0.0) { if (SearchUtils.isGoalState(p, current)) outcome = SearchOutcome.SOLUTION_FOUND; return current; } updateMetrics(temperature, getValue(current)); List<Node> children = nodeExpander.expand(current, p); if (children.size() > 0) { // next <- a randomly selected successor of current next = Util.selectRandomlyFromList(children); // /\E <- next.VALUE - current.value double deltaE = getValue(next) - getValue(current); if (shouldAccept(temperature, deltaE)) { current = next; } } } return null; } /** * Returns <em>e</em><sup>&delta<em>E / T</em></sup> * * @param temperature * <em>T</em>, a "temperature" controlling the probability of * downward steps * @param deltaE * VALUE[<em>next</em>] - VALUE[<em>current</em>] * @return <em>e</em><sup>&delta<em>E / T</em></sup> */ public double probabilityOfAcceptance(double temperature, double deltaE) { return Math.exp(deltaE / temperature); } public SearchOutcome getOutcome() { return outcome; } /** * Returns the last state from which the simulated annealing search found a * solution state. * * @return the last state from which the simulated annealing search found a * solution state. */ public Object getLastSearchState() { return lastState; } /** * Returns all the search metrics. */ @Override public Metrics getMetrics() { return metrics; } private void updateMetrics(double temperature, double value) { metrics.set(METRIC_TEMPERATURE, temperature); metrics.set(METRIC_NODE_VALUE, value); } /** * Sets all metrics to zero. */ private void clearInstrumentation() { metrics.set(METRIC_NODES_EXPANDED, 0); metrics.set(METRIC_TEMPERATURE, 0); metrics.set(METRIC_NODE_VALUE, 0); } @Override public void addNodeListener(Consumer<Node> listener) { nodeExpander.addNodeListener(listener); } @Override public boolean removeNodeListener(Consumer<Node> listener) { return nodeExpander.removeNodeListener(listener); } // // PRIVATE METHODS // // if /\E > 0 then current <- next // else current <- next only with probability e^(/\E/T) private boolean shouldAccept(double temperature, double deltaE) { return (deltaE > 0.0) || (new Random().nextDouble() <= probabilityOfAcceptance( temperature, deltaE)); } private double getValue(Node n) { // assumption greater heuristic value => // HIGHER on hill; 0 == goal state; // SA deals with gardient DESCENT return -1 * hf.h(n.getState()); } }