lib/ai: fix typo in doc
[nit.git] / lib / ai / search.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # This file is free software, which comes along with NIT. This software is
4 # distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
5 # without even the implied warranty of MERCHANTABILITY or FITNESS FOR A
6 # PARTICULAR PURPOSE. You can modify it is you want, provided this header
7 # is kept unaltered, and a notification of the changes is added.
8 # You are allowed to redistribute it and sell it, alone or is a part of
9 # another product.
10
11 # Basic framework for search problems and solver.
12 #
13 # The module provides a simple abstract class `SearchProblem[S,A]` to be specialized for a specific problem.
14 #
15 # The concrete class `SearchSolver` is used to configure, query, and run a solver for a given problem.
16 #
17 # For an example, see the `puzzle.nit` program in the `examples` subdirectory.
18 module search
19
20 import realtime
21 import trees
22
23 # Abstract search problem over immutable states (`S`) and actions (`A`).
24 #
25 # This class serves to model problems of planing and path-finding.
26 # A state, `S`, is a point in the search problem and fully models a given state of the world.
27 # An action, `A`, is an available mean of transition between two states.
28 #
29 # This abstract class is generic made to work with any kind of states and actions.
30 # Therefore, specific subclasses must be developed to implement the required services:
31 #
32 # * `initial_state`
33 # * `actions`
34 # * `apply_action`
35 # * `is_goal`
36 #
37 # Note that the implemented methods should not temper with the parameters since it is expected
38 # that they remain unmodified.
39 #
40 #
41 # # Basic search
42 #
43 # These tree method are enough to trigger a basic search.
44 #
45 # The methods `breadth_first` and `depth_first` return pre-configured solvers for, respectively,
46 # a breadth-first search, a depth-first search.
47 #
48 #
49 # # Advanced search
50 #
51 # The `cost` method can be implemented to represent the cost of a single transition.
52 # By default, the cost is 1.
53 #
54 # The `heuristic` method can be implemented to represent a lower-bound estimation of the remaining
55 # cost to reach a goal state.
56 # By default, the heuristic is 0.
57 #
58 # If one of these (or both) methods are implemented, the `astar` method will return a pre-configured
59 # solver for a A* search.
60 #
61 # More configuration and optimization on the search can to be done in the `SearchSolver` class.
62 interface SearchProblem[S: Object, A]
63 # The starting state for the problem
64 fun initial_state: S is abstract
65
66 # The available applicable actions for a given state.
67 # While there is a potential large number of distinct states and actions, there should be only
68 # a small number of possible action from a specific state (a small, or at least finite, branching factor).
69 fun actions(state: S): nullable SequenceRead[A] is abstract
70
71 # The new state when applying a given action
72 #
73 # The returned state can be:
74 # * a completely new state,
75 # * an existing state,
76 # * a new state but equals to an existing state
77 # in this case, ensure that the `==` and `hash` method
78 # are correctly implemented.
79 #
80 # Remember, this method should not modify its parameters.
81 #
82 # REQUIRE: `actions(state).has(action)`
83 fun apply_action(state:S, action:A): S is abstract
84
85 # Is the state a goal state?
86 # Once a goal state is found, the solver is automatically stopped.
87 # A problem can have 0, one or more goals if it makes sense
88 # but solver must be used accordingly.
89 # Default: no goals
90 fun is_goal(state:S): Bool do return false
91
92 # The cost for `action` from `old_state` to `new_state`
93 # REQUIRE: `apply_action(old_state, action) == new_state`
94 # Default: `1`.
95 # Note that having a 0 or negative value can make some search
96 # algorithms fail, or not terminate.
97 fun cost(state:S, action:A, state2: S): Float do return 1.0
98
99 # An heuristic of the estimated `cost` going from `state` to a goal state.
100 #
101 # Is is expected that the heuristic is *admissible*, it means its is an
102 # optimistic estimation and never an over-estimate, thus is cannot be
103 # higher than the lowest possible remaining cost.
104 # See `SearchSolver::do_revisit` for details.
105 #
106 # Default: `0`.
107 fun heuristic(state: S): Float do return 0.0
108
109 # return a new breadth-first solver
110 fun breadth_first: SearchSolver[S, A]
111 do
112 var todo = (new Array[SearchNode[S, A]]).as_fifo
113 var sol = new SearchSolver[S, A](self, todo)
114 return sol
115 end
116
117 # return a new depth-first solver
118 fun depth_first: SearchSolver[S, A]
119 do
120 var todo = (new List[SearchNode[S, A]]).as_lifo
121 var sol = new SearchSolver[S, A](self, todo)
122 return sol
123 end
124
125 # return a new best-first solver
126 #
127 # notes:
128 # * if `heuristic` is not defined, this is basically a Dijkstra search.
129 # * if `cost` in not defined either, this is basically a breadth-first search.
130 fun astar: SearchSolver[S, A]
131 do
132 var cpt = new NodeComparator[S, A]
133 var todo = new MinHeap[SearchNode[S, A]](cpt)
134 var sol = new SearchSolver[S, A](self, todo)
135 return sol
136 end
137
138 # Create the initial node in the search-tree.
139 # Used internally by the solvers but made public for those who want to replay a plan.
140 fun initial_node: SearchNode[S, A]
141 do
142 var res = new SearchNode[S,A](self, initial_state, null, null, 0.0, 0)
143 res.compute_heuristic
144 return res
145 end
146
147 # Negligible quantity for float comparisons
148 # Because of float imprecision, two really near float values should be considered equals.
149 # However, the specific epsilon value could be specific to the problem.
150 #
151 # The epsilon value is used for cost comparisons.
152 #
153 # Default: 1E-9
154 fun epsilon: Float do return 0.000000001
155
156 # Run and evaluate solvers with multiple configuration.
157 # This method can be used to evaluate which configuration to choose by default for a given problem.
158 #
159 # `steps` is the maximum number of steps a giver configuration can run.
160 fun run_configs(steps: Int)
161 do
162 var s
163
164 var c = 0
165 loop
166 if astar.run_config(steps, c, "A*") then break
167 c += 1
168 end
169 end
170
171 # Various Map implementations of memory.
172 # In order to try more configurations with `run_config`, this method
173 # is called to provide alternative implementation.
174 #
175 # For instance, a subclass can redefine this method and extends the result with an additional `RBTreeMap`.
176 # Note: because the true nature of the sates `S` is left to the user, some
177 # specific Map implementation could be more efficient than a HashMop.
178 #
179 # Default: A `HashMap`
180 fun make_memory: Array[Map[S, SearchNode[S, A]]]
181 do
182 var res = new Array[Map[S, SearchNode[S, A]]]
183 res.add new HashMap[S, SearchNode[S, A]]
184 return res
185 end
186 end
187
188 # A running solver for a given problem, to configure and control.
189 #
190 # For a given problem, a lot of variation of search algorithms can be made.
191 # Thus this class permit the user to control the parameters of the search algorithm.
192 #
193 # Note that this class is not meant to be specialized, and usually not instantiated directly.
194 #
195 #
196 # # Basic run and result.
197 #
198 # 1. Instantiate it with the method `breadth_first`, `depth_first`, or `astar` from `SearchProblem`.
199 # 2. Apply the method `run`, that will search and return a solution.
200 # 3. Retrieve information from the solution.
201 #
202 # ~~~~
203 # var p: SearchProblem = new MyProblem
204 # var res = p.astar.run
205 # if res != null then print "Found plan with {res.depth} actions, that cost {res.cost}: {res.plan.join(", ")}"
206 # ~~~~
207 #
208 #
209 # # Step-by-step runs and multiple runs
210 #
211 # The `run_steps` method (see also `steps`, and `steps_limit`) can be used to run only a maximum number of steps.
212 # This method can be used as a *co-routine* and run them periodically for a small amount of time.
213 #
214 # `run` and `run_steps` return the next solution.
215 # A subsequent call to `run` returns the following solution and so on.
216 #
217 # When there is no more solutions available, `is_running` become false.
218 #
219 #
220 # # Search-trees
221 #
222 # Internally, solvers use a search-tree where nodes are labeled with states, and edges are labeled with actions.
223 # See `SearchNode` for details.
224 #
225 # The `run` method return a `SearchNode` that can be used to retrieve a lot of useful information,
226 # like the full `path` or the `plan`.
227 #
228 #
229 # # Configuration
230 #
231 # The solver provide some *knobs* to control how the search-tree is visited.
232 #
233 # * `memorize` (see also `memorize_late`)
234 # * `do_revisit` (see also `revisits`)
235 # * `depth_limit` (see also `iterative_deepening` and `depth_limit_reached`)
236 class SearchSolver[S: Object, A]
237 # The problem currently solved
238 var problem: SearchProblem[S, A]
239
240 # The currently open nodes to process.
241 # They are the open nodes.
242 #
243 # It is the nature of the queue that control how the solver works.
244 # However, the methods `SearchProblem::breadth_first`, `SearchProblem::depth_first`,
245 # and `SearchProblem::astar` takes care of its correct initialization.
246 private var todo: Queue[SearchNode[S, A]]
247
248 # Is the solver still running?
249 # A running solver has not yet exhausted all the possible solutions.
250 var is_running: Bool = true
251
252 # Does the solver need to memorize visited states?
253 # When activated, there is an increased memory consumption since
254 # all visited states must be kept in memory,
255 # However, there is real a gain, since visited nodes are not
256 # revisited (unless needed by `do_revisit`)
257 #
258 # Default: `true`
259 #
260 # Note: If the graph of states has circuits, then a memory-less search may not terminate.
261 var memorize: Bool = true is writable
262
263 # Use memory only on visited (closed) state.
264 # Less memory operations, but two big drawbacks:
265 # * duplicated nodes can fill the `todo` queue (and the memory)
266 # * duplicated nodes require more invocation of `SearchProblem::heuristic`
267 #
268 # Note: if `memorize` is false, then this has no effect.
269 #
270 # Default: `false`
271 var memorize_late: Bool = false is writable
272
273 # Storage of nodes when `memorize` is activated
274 # Each state is associated with a node.
275 # This permit:
276 # * to avoid to revisit visited nodes (see `do_revisit`)
277 # * to avoid to reinvoke `heuristic` on known states (unless `memorize_late` is set)
278 private var memory: Map[S, SearchNode[S, A]] = new HashMap[S, SearchNode[S, A]]
279
280 # Total number of time an already memorized node is seen again.
281 # If `memorize_late` is set, then only visited nodes are counted.
282 # Otherwise, nodes in the todo-list are also considered.
283 var memorized = 0
284
285 # Revisit states when a better path to them is found.
286 # Such revisits generally triggers more revisits because they yield
287 # better path to their neighbors.
288 #
289 # If `false`, visited states are never revisited.
290 #
291 # With astar and an admissible heuristic, no visited node should be revisited.
292 # If the heuristic is not admissible, one may consider set this to `true`.
293 #
294 # Obviously, if `memorize` is false, then the value has no specific effect
295 # since all states are considered unvisited.
296 #
297 # Default: `false`.
298 #
299 # See also `revisits` and `SearchNode::revisits`.
300 var do_revisit: Bool = false is writable
301
302 # Total number of states (potentially) revisited.
303 #
304 # It is the number of time that a better path to a visited state is found.
305 # With astar and a really admissible heuristic, this number should stay 0.
306 # So check this value if you are not sure of the heuristic.
307 #
308 # Note that states are effectively revisited if `do_revisit` is activated.
309 var revisits = 0
310
311 # The solution found by the last `run`.
312 #
313 # ensure `solution != null implies problem.is_goal(solution.state)`
314 var solution: nullable SearchNode[S,A] = null
315
316 # Nearest solution found (up to date).
317 # The nearest solution is the one with the smallest heuristic value.
318 # The cost is not considered.
319 var nearest_solution: nullable SearchNode[S,A] = null
320
321 # Limit in the depth search.
322 #
323 # States found above this limit are not considered.
324 #
325 # Use 0 for no limit.
326 # Default: 0
327 # See also: `iterative_deepening`
328 var depth_limit: Int = 0 is writable
329
330 # How much time a `depth_limit` was reached?
331 #
332 # This can be used to query if some solutions may have been
333 # ignored because of a `depth_limit`.
334 #
335 # This is also used automatically if `iterative_deepening` is activated.
336 var depth_limit_reached: Int = 0
337
338 # Increase amount for an iterative deepening search.
339 # It =0, then the iterative deepening search is disabled.
340 # If >0, then `depth_limit` is automatically increased when the todo
341 # queue is empty but the `depth_limit` was reached in the previous iteration.
342 # Default: 0
343 var iterative_deepening: Int = 0
344
345 # The total steps executed since the beginning
346 # A step is the visit of a node in the `todo`-list
347 var steps: Int = 0
348
349 # The total number of nodes created
350 var nodes = 0
351
352 # Limit in the number of steps for a `run`.
353 #
354 # One can modify this value then `run` or just call `run_steps`.
355 #
356 # Use 0 for no limit.
357 # Default: 0
358 var steps_limit: Int = 0 is writable
359
360 # Total number of neighbors considered.
361 var neighbors = 0
362
363 # The average number of neighbors by nodes.
364 fun branching_factor: Float do return neighbors.to_f / steps.to_f
365
366 # Update `steps_limit` then just run some additional steps
367 # Return the best solution so far (if any)
368 fun run_steps(steps: Int): nullable SearchNode[S,A]
369 do
370 assert steps > 0
371 self.steps_limit = self.steps + steps
372 return run
373 end
374
375 # Reset the search from the initial state.
376 # Is used at the beginning and with `iterative_deepening`.
377 private fun start
378 do
379 assert todo.is_empty
380 depth_limit_reached = 0
381 var initial_node = problem.initial_node
382 if memorize and not memorize_late then memory[initial_node.state] = initial_node
383 initial_node.id = nodes
384 nodes += 1
385 todo.add initial_node
386 end
387
388 # Run the solver and return the next solution (if any)
389 # Return null is one of these is true:
390 # * `steps_limit` is reached
391 # * the `todo` queue is empty (eg. no reachable solution)
392 fun run: nullable SearchNode[S,A]
393 do
394 if steps == 0 then start
395
396 var nearest = nearest_solution
397 loop
398 # Enough work
399 if steps_limit > 0 and steps >= steps_limit then break
400
401 #print "todo={todo.length}"
402 #print " {todo.join("\n ")}"
403
404 # Next node, please
405 if todo.is_empty then
406 # iterative depth search?
407 if depth_limit <= 0 or iterative_deepening <= 0 or depth_limit_reached == 0 then
408 is_running = false
409 break
410 end
411
412 depth_limit += iterative_deepening
413 start
414 end
415 var node = todo.take
416
417 # Skip `old` stuff
418 # Because `Queue` has no remove :(
419 if node.drop then continue
420
421 var state = node.state
422
423 if memorize and memorize_late then
424 # Is the state already visited?
425 var old = memory.get_or_null(state)
426 if old != null then
427 memorized += 1
428 if old.cost - node.cost < problem.epsilon then continue
429 revisits += 1
430 if not do_revisit then
431 old.revisits += 1
432 continue
433 end
434 node.revisits = old.revisits + 1
435 end
436 memory[state] = node
437 end
438
439 steps += 1
440 assert node.steps == 0
441 node.steps = steps
442 self.node = node
443
444 # Keep trace to the nearest
445 if nearest == null or node.heuristic < nearest.heuristic then
446 nearest = node
447 nearest_solution = node
448 end
449
450 #print "try {node}"
451 #print "try {node}; todo={todo.length}"
452
453 # Won?
454 if problem.is_goal(state) then
455 solution = node
456 return node
457 end
458
459 # Ignore successors states if the depth limit is reached
460 if depth_limit > 0 and node.depth >= depth_limit then
461 depth_limit_reached += 1
462 continue
463 end
464
465 # Now, expand!
466 var actions = problem.actions(state)
467 if actions == null then continue
468 for action in actions do
469 neighbors += 1
470
471 # Fast track if no memory or late memory
472 if not memorize or memorize_late then
473 var new_node = node.apply_action(action)
474 new_node.id = nodes
475 nodes += 1
476 todo.add(new_node)
477 continue
478 end
479
480 # Get the state and the cost. Do not create the node yet.
481 var new_state = problem.apply_action(state, action)
482 var new_cost = node.cost + problem.cost(state, action, new_state)
483
484 # So check if the state was already seen
485 var old = memory.get_or_null(new_state)
486 if old != null then
487 memorized += 1
488 # If not better, then skip
489 if old.cost - new_cost < problem.epsilon then continue
490 # If visited and do not revisit, then skip
491 if old.steps > 0 and not do_revisit then
492 old.revisits += 1
493 revisits += 1
494 continue
495 end
496 # Even if `==`, reuse the same state object so
497 # * it may helps the GC
498 # * user-cached things in the previous state can be reused
499 new_state = old.state
500 end
501
502 # Finally, create the node
503 var new_node = new SearchNode[S, A](problem, new_state, node, action, new_cost, node.depth+1)
504 new_node.id = nodes
505 nodes += 1
506
507 if old == null then
508 # Compute heuristic and cost
509 new_node.compute_heuristic
510 else
511 # Reuse heuristic and update the cost
512 var h = old.heuristic
513 new_node.heuristic = h
514 new_node.score = new_cost + h
515
516 # Is `old` a visited node?
517 if old.steps == 0 then
518 # Old is still in the todo list, so drop it
519 old.drop = true
520 else
521 # Old was visited, so revisit it
522 new_node.revisits = old.revisits + 1
523 revisits += 1
524 #print "found {old.cost}>{new_cost}:{old.cost>new_cost} d={old.cost-new_cost}\n\t{old}\nthat is worse than\n\t{new_node}"
525 end
526 end
527 memory[new_state] = new_node
528
529 todo.add(new_node)
530 end
531 end
532 return null
533 end
534
535 # The last visited node.
536 # Unless when debugging, the last visited node is not really meaningful.
537 var node: nullable SearchNode[S, A] = null
538
539 redef fun to_s
540 do
541 var res ="steps={steps} nodes={nodes} todo={todo.length}"
542 if neighbors > 0 then res += " n={neighbors} (bf={branching_factor})"
543 if revisits > 0 then res += " re={revisits}"
544 if memorized > 0 then res += " mem={memorized}"
545 var n = solution
546 if n != null then
547 res += " last={n}"
548 else
549 n = nearest_solution
550 if n != null then res += " near={n}"
551 end
552 return res
553 end
554
555 # Run the configuration number `i`, for `steps` number of steps.
556 # The message `msg` suffixed by the name of the configuration is printed followed by the result
557 #
558 # This method is used by `SearchProblem::run_configs`
559 fun run_config(steps: Int, i: Int, msg: String): Bool
560 do
561 do
562 if i == 0 then
563 msg += " -mem"
564 memorize = false
565 break
566 end
567 i -= 1
568
569 var mems = problem.make_memory
570 memory = mems[i % mems.length]
571 msg += " {memory.class_name}"
572 i = i / mems.length
573
574 if i % 2 == 0 then
575 msg += " +mem"
576 memorize = true
577 memorize_late = false
578 else
579 msg += " +mem_late"
580 memorize = true
581 memorize_late = true
582 end
583 i = i / 2
584
585 if i % 2 == 0 then
586 msg += " +revisit"
587 do_revisit = true
588 else
589 msg += " -revisit"
590 do_revisit = false
591 end
592 i = i / 2
593
594 if i >= 1 then return true
595
596 end
597 print msg
598
599 var t = new Clock
600 var res = run_steps(steps)
601 print "\t{self}"
602 var l = t.lapse
603 print "\ttime={l}"
604 return false
605 end
606 end
607
608 # Used to compare nodes with their score.
609 # Smaller is score, smaller is the node.
610 private class NodeComparator[S: Object, A]
611 super Comparator[SearchNode[S, A]]
612 redef fun compare(a,b) do return a.score <=> b.score
613 end
614
615 # A node in the search-tree visited by a `SearchSolver`.
616 # In search-trees, nodes are labeled with states (`S`), and edges by actions (`A`).
617 #
618 # The root node is labeled by the initial state of the problem.
619 #
620 # This class is exposed to allow queries on the solution provided by the solver.
621 class SearchNode[S: Object, A]
622 # A flag that indicate that `self` is virtually removed from the todo-list.
623 # `self` was added to the todo-list but that a better node for the
624 # same state was found latter.
625 private var drop = false
626
627 # The associated problem
628 var problem: SearchProblem[S, A]
629
630 # The state associated to `self`.
631 # The state labels the node `self`.
632 var state: S
633
634 # Is `self` a root node of the search-tree?
635 # ensure: `result` == `parent == null` and `result`== `action == null`
636 fun is_root: Bool do return parent == null
637
638 # The previous node in the search-tree (if not root).
639 var parent: nullable SearchNode[S, A]
640
641 # The action used to go from `parent` to `self` (if not root)
642 # The action labels the edge from `parent` to `self`.
643 var action: nullable A
644
645 # The past cost (g) from the root node to `self`.
646 var cost: Float
647
648 # The heuristic from self to the goal (according to `problem.heuristic(state)`
649 # It is the future cost (h)
650 var heuristic: Float is noinit
651
652 # The sum of `cost` and `heuristic`
653 # It is the f function.
654 var score: Float is noinit
655
656 # Update `heuristic` and `score` according to `problem`.
657 private fun compute_heuristic
658 do
659 var h = problem.heuristic(state)
660 heuristic = h
661 score = cost + h
662 end
663
664 # The depth of `self` in the search tree
665 # It is the number of parents to the root node.
666 var depth: Int
667
668 # The number of steps needed by the solver to process `self`
669 # It is just a useless generation number, but could be used to evaluate
670 # the behavior of search algorithms.
671 var steps: Int = 0
672
673 # The rank of creation of nodes by the solver.
674 # It is just a useless generation number, but could be used to evaluate
675 # the behavior of search algorithms.
676 var id: Int = 0
677
678 # The number of (potential) revisits of `node`.
679 # This information can be used to debug search algorithms.
680 # And to detect when heuristics are not admissible.
681 #
682 # See `SearchSolver::revisits` and `SearchSolver::do_revisit`
683 # for details.
684 var revisits: Int = 0
685
686 # Create a new child node for the next state, according to `problem`.
687 # Used internally by the solvers but made public for those who want to replay a plan.
688 #
689 # ensure `result.parent == self`
690 # ensure `result.action == action`
691 fun apply_action(action: A): SearchNode[S, A]
692 do
693 var new_state = problem.apply_action(state, action)
694 var new_cost = problem.cost(state, action, new_state)
695 var new_node = new SearchNode[S, A](problem, new_state, self, action, cost + new_cost, depth+1)
696 new_node.compute_heuristic
697 return new_node
698 end
699
700 # Build the sequence of nodes from the initial node to `self`
701 #
702 # ensure `result.first.is_root and result.last == self`
703 fun path: Sequence[SearchNode[S, A]]
704 do
705 var res = new List[SearchNode[S, A]]
706 res.add(self)
707 var node = parent
708 while node != null do
709 res.unshift(node)
710 node = node.parent
711 end
712 return res
713 end
714
715 # Build a sequence of actions from the initial state to `self`
716 # See `path` for a more detailed plan.
717 fun plan: Sequence[A]
718 do
719 var res = new List[A]
720 var node: nullable SearchNode[S, A] = self
721 while node != null do
722 var a = node.action
723 if a != null then res.unshift(a)
724 node = node.parent
725 end
726 return res
727 end
728
729 # Just print a detailed path on the screen
730 fun dump
731 do
732 print "result:{state}"
733 for n in path do
734 var a = n.action
735 if a != null then print " + {a or else ""}"
736 print " {n.steps}: {n.state} ({n.cost}$)"
737 end
738 end
739
740 redef fun to_s do return "#{steps}/{id} d={depth} f={cost+heuristic} g={cost} h={heuristic}: {state}"
741 #redef fun to_s do return "#{steps} f={(cost+heuristic).to_i} g={cost.to_i} h={heuristic.to_i}"
742 end