File Coverage

blib/lib/AI/Pathfinding/SMAstar.pm
Criterion Covered Total %
statement 114 199 57.2
branch 27 82 32.9
condition 5 15 33.3
subroutine 13 21 61.9
pod 8 12 66.6
total 167 329 50.7


line stmt bran cond sub pod time code
1             package AI::Pathfinding::SMAstar;
2              
3 1     1   35737 use 5.006000;
  1         4  
  1         48  
4 1     1   7 use strict;
  1         2  
  1         41  
5 1     1   7 use warnings;
  1         6  
  1         162  
6              
7             require Exporter;
8              
9             our @ISA = qw(Exporter);
10              
11             # Items to export into callers namespace by default. Note: do not export
12             # names by default without a very good reason. Use EXPORT_OK instead.
13             # Do not simply export all your public functions/methods/constants.
14              
15             # This allows declaration use AI::Pathfinding::SMAstar ':all';
16             # If you do not need this, moving things directly into @EXPORT or @EXPORT_OK
17             # will save memory.
18             our %EXPORT_TAGS = ( 'all' => [ qw(
19            
20             ) ] );
21              
22             our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } );
23              
24             our @EXPORT = qw(
25            
26             );
27              
28             our $VERSION = '0.07';
29              
30 1     1   795 use AI::Pathfinding::SMAstar::PriorityQueue;
  1         3  
  1         39  
31 1     1   11 use AI::Pathfinding::SMAstar::Path;
  1         2  
  1         22  
32 1     1   7 use Scalar::Util;
  1         2  
  1         78  
33 1     1   6 use Carp;
  1         2  
  1         2780  
34              
35             my $DEBUG = 0;
36              
37              
38             ##################################################
39             # SMAstar constructor
40             ##################################################
41             sub new {
42 1     1 1 2 my $invocant = shift;
43 1   33     6 my $class = ref($invocant) || $invocant;
44 1         13 my $self = {
45            
46             _priority_queue => AI::Pathfinding::SMAstar::PriorityQueue->new(),
47             _state_eval_func => undef,
48             _state_goal_p_func => undef,
49             _state_num_successors_func => undef,
50             _state_successors_iterator => undef,
51             _show_prog_func => undef,
52             _state_get_data_func => undef,
53              
54              
55             @_, # attribute override
56             };
57 1         6 return bless $self, $class;
58             }
59              
60              
61             sub state_eval_func {
62 0     0 1 0 my $self = shift;
63 0 0       0 if (@_) { $self->{_state_eval_func} = shift }
  0         0  
64 0         0 return $self->{_state_eval_func};
65             }
66              
67             sub state_goal_p_func {
68 0     0 1 0 my $self = shift;
69 0 0       0 if (@_) { $self->{_state_goal_p_func} = shift }
  0         0  
70 0         0 return $self->{_state_goal_p_func};
71             }
72              
73             sub state_num_successors_func {
74 0     0 1 0 my $self = shift;
75 0 0       0 if (@_) { $self->{_state_num_successors_func} = shift }
  0         0  
76 0         0 return $self->{_state_num_successors_func};
77             }
78              
79             sub state_successors_iterator {
80 0     0 1 0 my $self = shift;
81 0 0       0 if (@_) { $self->{_state_successors_iterator} = shift }
  0         0  
82 0         0 return $self->{_state_successors_iterator};
83             }
84              
85             sub state_get_data_func {
86 0     0 1 0 my $self = shift;
87 0 0       0 if (@_) { $self->{_state_get_data_func} = shift }
  0         0  
88 0         0 return $self->{_state_get_data_func};
89             }
90              
91             sub show_prog_func {
92 0     0 1 0 my $self = shift;
93 0 0       0 if (@_) { $self->{_show_prog_func} = shift }
  0         0  
94 0         0 return $self->{_show_prog_func};
95             }
96              
97              
98              
99             ###################################################################
100             #
101             # Add a state from which to begin the search. There can
102             # be multiple start-states.
103             #
104             ###################################################################
105             sub add_start_state
106             {
107 2     2 0 275 my ($self, $state) = @_;
108              
109              
110 2         4 my $state_eval_func = $self->{_state_eval_func};
111 2         3 my $state_goal_p_func = $self->{_state_goal_p_func};
112 2         35 my $state_num_successors_func = $self->{_state_num_successors_func},
113             my $state_successors_iterator = $self->{_state_successors_iterator},
114             my $state_get_data_func = $self->{_state_get_data_func};
115            
116             # make sure required functions have been defined
117 2 50       6 if(!defined($state_eval_func)){
118 0         0 croak "SMAstar: evaluation function is not defined\n";
119             }
120 2 50       4 if(!defined($state_goal_p_func)){
121 0         0 croak "SMAstar: goal function is not defined\n";
122             }
123 2 50       4 if(!defined($state_num_successors_func)){
124 0         0 croak "SMAstar: num successors function is not defined\n";
125             }
126 2 50       4 if(!defined($state_successors_iterator)){
127 0         0 croak "SMAstar: successor iterator is not defined\n";
128             }
129              
130             # create a path object from this state
131 2         16 my $state_obj = AI::Pathfinding::SMAstar::Path->new(
132             _state => $state,
133             _eval_func => $state_eval_func,
134             _goal_p_func => $state_goal_p_func,
135             _num_successors_func => $state_num_successors_func,
136             _successors_iterator => $state_successors_iterator,
137             _get_data_func => $state_get_data_func,
138             );
139            
140            
141 2         6 my $fcost = AI::Pathfinding::SMAstar::Path::fcost($state_obj);
142             # check if the fcost of this node looks OK (is numeric)
143 2 50       11 unless(Scalar::Util::looks_like_number($fcost)){
144 0         0 croak "Error: f-cost of state is not numeric. Cannot add state to queue.\n";
145             }
146 2         15 $state_obj->f_cost($fcost);
147              
148             # check if the num_successors function returns a number
149 2         7 my $num_successors = $state_obj->get_num_successors();
150 2 50       7 unless(Scalar::Util::looks_like_number($num_successors)){
151 0         0 croak "Error: Number of state successors is not numeric. Cannot add state to queue.\n";
152             }
153              
154             # test out the iterator function to make sure it returns
155             # an object of the correct type
156 2         8 my $classname = ref($state);
157 2         6 my $test_successor_iterator = $state_obj->{_successors_iterator}->($state);
158 2         6 my $test_successor = $test_successor_iterator->($state);
159 2         4 my $succ_classname = ref($test_successor);
160              
161 2 50       4 unless($succ_classname eq $classname){
162 0         0 croak "Error: Successor iterator method of object $classname does " .
163             "not return an object of type $classname.\n";
164             }
165              
166            
167             # add this node to the queue
168 2         12 $self->{_priority_queue}->insert($state_obj);
169            
170             }
171              
172             ###################################################################
173             #
174             # start the SMAstar search process
175             #
176             ###################################################################
177             sub start_search
178             {
179 1     1 1 9 my ($self,
180             $log_function,
181             $str_function,
182             $max_states_in_queue,
183             $max_cost,
184             ) = @_;
185              
186 1 50       3 if(!defined($str_function)){
187 0         0 croak "SMAstar start_search: str_function is not defined.\n";
188             }
189              
190 1         9 sma_star_tree_search(\($self->{_priority_queue}),
191             \&AI::Pathfinding::SMAstar::Path::is_goal,
192             \&AI::Pathfinding::SMAstar::Path::get_descendants_iterator_smastar,
193             \&AI::Pathfinding::SMAstar::Path::fcost,
194             \&AI::Pathfinding::SMAstar::Path::backup_fvals,
195             $log_function,
196             $str_function,
197             \&AI::Pathfinding::SMAstar::Path::progress,
198             $self->{_show_prog_func},
199             $max_states_in_queue,
200             $max_cost,
201             );
202             }
203              
204              
205              
206             #################################################################
207             #
208             # SMAstar search
209             # Memory-bounded A* search
210             #
211             #
212             #################################################################
213             sub sma_star_tree_search
214             {
215            
216 1     1 0 2 my ($priority_queue,
217             $goal_p,
218             $successors_func,
219             $eval_func,
220             $backup_func,
221             $log_function, # debug string func; represent state object as a string.
222             $str_function,
223             $prog_function,
224             $show_prog_func,
225             $max_states_in_queue,
226             $max_cost,
227             ) = @_;
228            
229 1         2 my $iteration = 0;
230 1         5 my $num_states_in_queue = $$priority_queue->size();
231 1         1 my $max_extra_states_in_queue = $max_states_in_queue;
232 1         2 $max_states_in_queue = $num_states_in_queue + $max_extra_states_in_queue;
233 1         1 my $max_depth = ($max_states_in_queue - $num_states_in_queue);
234              
235 1         1 my $best; # the best candidate for expansion
236              
237              
238            
239 1 50 33     8 if($$priority_queue->is_empty() || !$$priority_queue){
240 0         0 return;
241             }
242             else{
243 1         2 my $num_successors = 0;
244            
245             # loop over the elements in the priority queue
246 1         3 while(!$$priority_queue->is_empty()){
247            
248             # determine the current size of the queue
249 6         12 my $num_states_in_queue = $$priority_queue->{_size};
250             # get the best candidate for expansion from the queue
251 6         18 $best = $$priority_queue->deepest_lowest_cost_leaf_dont_remove();
252            
253             #------------------------------------------------------
254 6 50       15 if(!$DEBUG){
255 6         18 my $str = $log_function->($best);
256 6         162 $show_prog_func->($iteration, $num_states_in_queue, $str);
257             }
258             else{
259 0         0 my $str = $log_function->($best);
260 0         0 print "best is: " . $str_function->($best) . ", cost: " . $best->{_f_cost} . "\n";
261             }
262             #------------------------------------------------------
263              
264              
265 6 100       22 if($best->$goal_p()) {
    50          
266             # goal achieved! iteration: $iteration, number of
267             # states in queue: $num_states_in_queue.
268 1         7 return $best;
269             }
270             elsif($best->{_f_cost} >= $max_cost){
271 0         0 croak "\n\nSearch unsuccessful. max_cost reached (cost: $max_cost).\n";
272             }
273             else{
274 5         14 my $successors_iterator = $best->$successors_func();
275 5         12 my $succ = $successors_iterator->();
276            
277 5 50       15 if($succ){
278             # if succ is at max depth and is not a goal node, set succ->fcost to infinity
279 5 50 33     12 if($succ->depth() >= $max_depth && !$succ->$goal_p() ){
280 0         0 $succ->{_f_cost} = $max_cost;
281             }
282             else{
283             # calling eval for comparison, and maintaining pathmax property
284 5         13 $succ->{_f_cost} = max($eval_func->($succ), $eval_func->($best));
285 5         10 my $descendant_index = $succ->{_descendant_index};
286 5         11 $best->{_descendant_fcosts}->[$descendant_index] = $succ->{_f_cost};
287             }
288             }
289              
290             # determine if $best is completed, and if so backup values
291 5 50       13 if($best->is_completed()){
292              
293              
294             # remove from queue first, back up fvals, then insert back on queue.
295             # this way, it gets placed in its rightful place on the queue.
296 5         6 my $fval_before_backup = $best->{_f_cost};
297            
298             # STEPS:
299             # 1) remove best and all antecedents from queue, but only if they are
300             # going to be altered by backing-up fvals. This is because
301             # removing and re-inserting in queue changes temporal ordering,
302             # and we don't want to do that unless the node will be
303             # placed in a new cost-bucket/tree.
304             # 2) then backup fvals
305             # 3) then re-insert best and all antecedents back on queue.
306              
307              
308             # Check if need for backup fvals
309 5         12 $best->check_need_fval_change();
310            
311             my $cmp_func = sub {
312 4     4   5 my ($str) = @_;
313             return sub{
314 0         0 my ($obj) = @_;
315 0         0 my $obj_path_str = $str_function->($obj);
316 0 0       0 if($obj_path_str eq $str){
317 0         0 return 1;
318             }
319             else{
320 0         0 return 0;
321             }
322             }
323 5         26 };
  4         22  
324              
325 5         7 my $antecedent = $best->{_antecedent};
326 5         4 my %was_on_queue;
327 5         9 my $i = 0;
328              
329             # Now remove the offending nodes from queue, if any
330 5 50       14 if($best->need_fval_change()){
331            
332             # remove best from the queue
333 5         16 $best = $$priority_queue->deepest_lowest_cost_leaf();
334            
335 5         12 while($antecedent){
336 4         9 my $path_str = $str_function->($antecedent);
337            
338 4 50 33     61 if($antecedent->is_on_queue() && $antecedent->need_fval_change()){
339 4         6 $was_on_queue{$i} = 1;
340 4         7 $$priority_queue->remove($antecedent, $cmp_func->($path_str));
341             }
342 4         17 $antecedent = $antecedent->{_antecedent};
343 4         10 $i++;
344             }
345             }
346            
347            
348             # Backup fvals
349 5 50       13 if($best->need_fval_change()){
350 5         15 $best->$backup_func();
351             }
352              
353            
354             # Put everything back on the queue
355 5 50       15 if($best->need_fval_change()){
356 5         13 $$priority_queue->insert($best);
357 5         7 my $antecedent = $best->{_antecedent};
358 5         8 my $i = 0;
359 5         12 while($antecedent){
360 4 50 33     17 if($was_on_queue{$i} && $antecedent->need_fval_change()){
361             # the antecedent needed fval change too.
362 4         10 $$priority_queue->insert($antecedent);
363             }
364 4 50       11 if($antecedent->need_fval_change()){
365             # set need_fval_change back to 0, so it will not be automatically seen as
366             # needing changed in the future. This is important, since we do not want
367             # to remove an element from the queue *unless* we need to change the fcost.
368             # This is because when we remove it from the queue and re-insert it, it
369             # loses its seniority in the queue (it becomes the newest node at its cost
370             # and depth) and will not be removed at the right time when searching for
371             # deepest_lowest_cost_leafs or shallowest_highest_cost_leafs.
372 4         6 $antecedent->{_need_fcost_change} = 0;
373             }
374              
375 4         6 $antecedent = $antecedent->{_antecedent};
376 4         11 $i++;
377             }
378             # Again, set need_fval_change back to 0, so it will not be automatically
379             # seen as needing changed in the future.
380 5         26 $best->{_need_fcost_change} = 0;
381             }
382             }
383              
384              
385             #
386             # If best's descendants are all in memory, mark best as completed.
387             #
388 5 50       17 if($best->all_in_memory()) {
389            
390 0 0       0 if(!($best->is_completed())){
391 0         0 $best->is_completed(1);
392             }
393              
394             my $cmp_func = sub {
395 0     0   0 my ($str) = @_;
396             return sub{
397 0         0 my ($obj) = @_;
398 0         0 my $obj_str = $str_function->($obj);
399 0 0       0 if($obj_str eq $str){
400 0         0 return 1;
401             }
402             else{
403 0         0 return 0;
404             }
405             }
406 0         0 };
  0         0  
407            
408 0         0 my $best_str = $str_function->($best);
409              
410             # If best is not a root node
411 0 0       0 if($best->{_depth} != 0){
412             # descendant index is the unique index indicating which descendant
413             # this node is of its antecedent.
414 0         0 my $descendant_index = $best->{_descendant_index};
415 0         0 my $antecedent = $best->{_antecedent};
416 0         0 $$priority_queue->remove($best, $cmp_func->($best_str));
417 0 0       0 if($antecedent){
418 0         0 $antecedent->{_descendants_produced}->[$descendant_index] = 0;
419             }
420             }
421             }
422            
423             # there are no more successors of $best
424 5 50       12 if(!$succ){
425 0         0 next;
426             }
427              
428 5         6 my $antecedent;
429             my @antecedents_that_need_to_be_inserted;
430              
431             # If the maximum number of states in the queue has been reached,
432             # we need to remove the shallowest-highest-cost leaf to make room
433             # for more nodes. That means we have to make sure that the antecedent
434             # produces this descendant again at some point in the future if needed.
435 5 50       11 if($num_states_in_queue > $max_states_in_queue){
436 0         0 my $shcl_obj = $$priority_queue->shallowest_highest_cost_leaf($best, $succ, $str_function);
437              
438 0 0       0 if(!$shcl_obj){
439 0         0 croak "Error while pruning queue: shallowest-highest-cost-leaf was null\n";
440             }
441 0         0 $antecedent = $shcl_obj->{_antecedent};
442 0 0       0 if($antecedent){
443 0         0 my $antecedent_successors = \$antecedent->{_descendants_list};
444              
445 0         0 $antecedent->remember_forgotten_nodes_fcost($shcl_obj);
446 0         0 $antecedent->{_forgotten_nodes_num} = $antecedent->{_forgotten_nodes_num} + 1;
447 0         0 my $descendant_index = $shcl_obj->{_descendant_index};
448             # record the index of this descendant in the forgotten_nodes list
449 0         0 $antecedent->{_forgotten_nodes_offsets}->{$descendant_index} = 1;
450             # flag the antecedent as not having this descendant in the queue
451 0         0 $antecedent->{_descendants_produced}->[$descendant_index] = 0;
452 0         0 $antecedent->{_descendant_fcosts}->[$descendant_index] = -1;
453             # flag the ancestor node as having deleted a descendant
454 0         0 $antecedent->descendants_deleted(1);
455             # update the number of descendants this node has in memory
456 0         0 $antecedent->{_num_successors_in_mem} = $antecedent->{_num_successors_in_mem} - 1;
457             # update the total number of nodes in the queue.
458 0         0 $num_states_in_queue--;
459            
460             }
461             } # end if (num_states_on_queue > max_states)
462              
463             # if there is a successor to $best, insert it in the priority queue.
464 5 50       16 if($succ){
465 5         15 $$priority_queue->insert($succ);
466 5         213 $best->{_num_successors_in_mem} = $best->{_num_successors_in_mem} + 1;
467             }
468             else{
469 0         0 croak "Error: no successor to insert\n";
470             }
471             }
472             }
473             continue {
474 5         16 $iteration++;
475             }
476              
477 0         0 print "\n\nreturning unsuccessfully. iteration: $iteration\n";
478 0         0 return;
479             }
480             }
481              
482              
483              
484              
485             sub max
486             {
487 5     5 0 9 my ($n1, $n2) = @_;
488 5 50       13 return ($n1 > $n2 ? $n1 : $n2);
489             }
490              
491              
492             sub fp_compare {
493 0     0 0   my ($a, $b, $dp) = @_;
494 0           my $a_seq = sprintf("%.${dp}g", $a);
495 0           my $b_seq = sprintf("%.${dp}g", $b);
496            
497            
498              
499 0 0         if($a_seq eq $b_seq){
    0          
500 0           return 0;
501             }
502             elsif($a_seq lt $b_seq){
503 0           return -1;
504             }
505             else{
506 0           return 1;
507             }
508             }
509              
510              
511              
512              
513              
514             1;
515             __END__