AI-Pathfinding-SMAstar

 view release on metacpan or  search on metacpan

Changes  view on Meta::CPAN

Revision history for Perl extension AI::Pathfinding::SMAstar.

0.01  Tue Feb 23 12:06:47 2010
	- original version; created by h2xs 1.23 with options
		-XAn AI::Pathfinding::SMAstar

0.02  Fri Feb 25 11:17:01 2010
	- updated pod documentation

0.03  Sun Feb 28 12:26:58 2010
	- updated pod documentation

0.04  Tue Mar  2 13:17:53 2010
      	- updated error handling in add_start_state method
	- perldoc edits 

0.05  Thu Mar  4 11:06:10 2010
      	- fixed an issue where search did not terminate when max_cost
	  is reached.

0.06  Thu Mar  4 11:06:10 2010
      	- fixed an issue with successor iterator in Path class.

META.yml  view on Meta::CPAN

--- #YAML:1.0
name:                AI-Pathfinding-SMAstar
version:             0.07
abstract:            Simplified Memory-bounded A* Search
license:             ~
author:              
    - Matthias Beebe <mbeebe@cpan.org>
generated_by:        ExtUtils::MakeMaker version 6.42
distribution_type:   module
requires:     
    Test::More:                    0
    Tree::AVL:                     0
meta-spec:
    url:     http://module-build.sourceforge.net/META-spec-v1.3.html
    version: 1.3

Makefile.PL  view on Meta::CPAN

use 5.006000;
use ExtUtils::MakeMaker;
# See lib/ExtUtils/MakeMaker.pm for details of how to influence
# the contents of the Makefile that is written.
WriteMakefile(
    NAME              => 'AI::Pathfinding::SMAstar',
    VERSION_FROM      => 'lib/AI/Pathfinding/SMAstar.pm', # finds $VERSION
    PREREQ_PM         => {Test::More => 0,
			  Tree::AVL =>  0,}, # e.g., Module::Name => 1.1
    ($] >= 5.005 ?     ## Add these new keywords supported since 5.005
      (ABSTRACT_FROM  => 'lib/AI/Pathfinding/SMAstar.pm', # retrieve abstract from module
       AUTHOR         => 'Matthias Beebe <mbeebe@cpan.org>') : ()),
);

README  view on Meta::CPAN

AI-Pathfinding-SMAstar version 0.07
===================================

NAME

AI::Pathfinding::SMAstar - Memory-bounded A* Search

SYNOPSIS

 use AI::Pathfinding::SMAstar;

EXAMPLE

 ##################################################################
 #
 # This example uses a hypothetical object called FrontierObj, and
 # shows the functions that FrontierObj must feature in order to 
 # perform a path search in a solution-space populated by 
 # FrontierObj objects.
 #
 ##################################################################

 my $smastar = AI::Pathfinding::SMAstar->new(
        # evaluates f(n) = g(n) + h(n), returns a number
        _state_eval_func           => \&FrontierObj::evaluate,

        # when called on a node, returns 1 if it is a goal
        _state_goal_p_func         => \&FrontierObj::goal_test,

        # must return the number of successors of a node
        _state_num_successors_func => \&FrontierObj::get_num_successors,

        # must return *one* successor at a time
        _state_successors_iterator => \&FrontierObj::get_successors_iterator,

        # can be any suitable string representation 
        _state_get_data_func       => \&FrontierObj::string_representation,

        # gets called once per iteration, useful for showing algorithm progress
        _show_prog_func            => \&FrontierObj::progress_callback,      
    );

 # you can start the search from multiple start-states
 # Add the initial states to the smastar object before starting the search.
 foreach my $frontierObj (@start_states){
    $smastar->add_start_state($frontierObj);
 }

 # Start the search.  If successful, frontierGoalObj will contain the 
 # goal node.   The optimal path to the goal node will be encoded in the 
 # ancestry of the goal node.   $frontierGoalObj->antecedent() contains
 # the goal object's parent, and so forth back to the start state.
 my $frontierGoalObj = $smastar->start_search(
    \&log_function,       # returns a string used for logging progress
    \&str_function,       # returns a string used to *uniquely* identify a node 
    $max_states_in_queue, # indicate the maximum states allowed in memory
    $MAX_COST,            # indicate the maximum cost allowed in search
    );

Explanation

In the example above, a hypothetical object, FrontierObj, is used to 
represent a node in your search space. To use SMA* search to find a shortest 
path from a starting node to a goal in your search space, you must define what 
a node is, in your search space (or point, or state).

A common example used for informed search methods, and one that is 
used in Russell's original paper, is a N-puzzle, such as an 8-puzzle or 
15-puzzle. If trying to solve such a puzzle, a node in the search space 
could be defined as a particular configuration of that puzzle.    In the 
/t directory of this module's distribution, SMA* is applied to the problem 
of finding the shortest palindrome that contains a minimum number of letters 
specified, over a given lexicon of words.

Once you have a definition and representation of a node in your search space, SMA* 
search requires the following functions to work:

  ** State evaluation function (_state_eval_func above)

      This function must return the cost of this node in the search space. In all 
forms of A* search, this means the cost paid to arrive at this node along a path, 
plus the estimated cost of going from this node to a goal state. This function 
must be positive and monotonic, meaning that successor nodes mustn't be less 
expensive than their antecedent nodes. Monotonicity is ensured in this implementation 
of SMA*, so even if your function is not monotonic, SMA* will assign the antecedent 
node's cost to a successor if that successor costs less than the antecedent.
  
  * State goal predicate function (_state_goal_p_func above)

      This function must return 1 if the node is a goal node, or 0 otherwise.
    

  * State number of successors function (_state_num_successors_func above)

      This function must return the number of successors of this node, i.e. all 
      nodes that are reachable from this node via a single operation.
    

  * State successors iterator (_state_iterator above)

      This function must return a *handle to a function* that returns next 
      successor of this node, i.e. it must return an iterator that produces 
      the successors of this node *one* at a time. This is 
      necessary to maintain the memory-bounded constraint of SMA* search.
    

  * State get-data function (_state_get_data_func above)

      This function returns a string representation of this node.
    

  * State show-progress function (_show_prog_func above)

      This is a callback function for displaying the progress of the 
      search. It can be an empty callback if you do not need this output.
    

  * log string function (log_function above)

      This is an arbitrary string used for logging. It also gets passed to 
      the show-progress function above.
    

  * str_function (str_function above)

      This function returns a *unique* string representation of this node. 
      Uniqueness is required for SMA* to work properly.
    

  * max states allowed in memory (max_states_in_queue above)

      An integer indicating the maximum number of expanded nodes to 
      hold in memory at any given time.
    

  * maximum cost (MAX_COST above)

      An integer indicating the maximum cost, beyond which nodes will not be 
      expanded.



DESCRIPTION

Overview

Memory-bounded A* search (or SMA* search) addresses some of the limitations of 
conventional A* search, by bounding the amount of space required to perform a 
shortest-path search. This module is an implementation of SMA*, which was first 

README  view on Meta::CPAN

always find a path to the goal if such a path exists.

In general, A* search works using a calculated cost function on each node 
along a path, in addition to an admissible heuristic estimating the distance 
from that node to the goal. The cost is calculated as:

f(n) = g(n) + h(n)

Where:

    

   * n is a state (node) along a path
    

   * g(n) is the total cost of the path leading up to n
    

   * h(n) is the heuristic function, or estimated cost of the path from n 
      to the goal node.

The to be admissible, the heuristic must never over-estimate the distance 
from the node to the goal. If the heuristic is set to zero, A* search reduces 
to Branch and Bound search.

For a given heuristic function, it can be shown that A* search is optimally 
efficient, meaning that, in its calculation of the shortest path, it expands 
fewer nodes in the search space than any other algorithm.

The space complexity of A* search is bounded by an exponential of the 

README  view on Meta::CPAN

memory for search without any danger of overflow. It can, however, make SMA* 
search significantly slower than a theoretical unbounded-memory search, due to 
the extra bookkeeping it must do, and because nodes may need to be re-expanded 
(the overall number of node expansions may increase).

It can be shown that of the memory-bounded variations of A* search, such MA*, 
IDA*, Iterative Expansion, etc., SMA* search expands the least number of nodes 
on average. However, for certain classes of problems, guaranteeing optimality 
can be costly. This is particularly true in solution spaces where:

    * the branching factor of the search space is large
    * there are multiple equivalent optimal solutions (or shortest paths)

For solution spaces with these characteristics, stochastic methods or 
approximation algorithms such as Simulated Annealing can provide a massive 
reduction in time and space requirements, while introducing a tunable 
probability of producing a sub-optimal solution.



METHODS

README  view on Meta::CPAN


AUTHOR

Matthias Beebe, <mbeebe@cpan.org>


INSTALLATION

To install this module type the following:

   perl Makefile.PL
   make
   make test
   make install

DEPENDENCIES

This module requires these other modules and libraries:

  Tree::AVL
  Test::More

COPYRIGHT AND LICENCE

Copyright (C) 2010 by Matthias Beebe

This library is free software; you can redistribute it and/or modify it 
under the same terms as Perl itself, either Perl version 5.10.0 or, at 
your option, any later version of Perl 5 you may have available.

lib/AI/Pathfinding/SMAstar.pm  view on Meta::CPAN

our @ISA = qw(Exporter);

# Items to export into callers namespace by default. Note: do not export
# names by default without a very good reason. Use EXPORT_OK instead.
# Do not simply export all your public functions/methods/constants.

# This allows declaration	use AI::Pathfinding::SMAstar ':all';
# If you do not need this, moving things directly into @EXPORT or @EXPORT_OK
# will save memory.
our %EXPORT_TAGS = ( 'all' => [ qw(
	
) ] );

our @EXPORT_OK = ( @{ $EXPORT_TAGS{'all'} } );

our @EXPORT = qw(
	
);

our $VERSION = '0.07';

use AI::Pathfinding::SMAstar::PriorityQueue;
use AI::Pathfinding::SMAstar::Path;
use Scalar::Util;
use Carp;

my $DEBUG = 0;


##################################################
# SMAstar constructor 
##################################################
sub new {
    my $invocant = shift;
    my $class   = ref($invocant) || $invocant;
    my $self = { 
     
	_priority_queue => AI::Pathfinding::SMAstar::PriorityQueue->new(),
	_state_eval_func => undef,	
	_state_goal_p_func => undef,
	_state_num_successors_func => undef,
	_state_successors_iterator => undef,
	_show_prog_func => undef,
	_state_get_data_func => undef,


	@_, # attribute override
    };
    return bless $self, $class;
}


sub state_eval_func {
    my $self = shift;
    if (@_) { $self->{_state_eval_func} = shift }
    return $self->{_state_eval_func};
}

sub state_goal_p_func {
    my $self = shift;
    if (@_) { $self->{_state_goal_p_func} = shift }
    return $self->{_state_goal_p_func};    
}

sub state_num_successors_func {
    my $self = shift;
    if (@_) { $self->{_state_num_successors_func} = shift }
    return $self->{_state_num_successors_func};    
}

sub state_successors_iterator {
    my $self = shift;
    if (@_) { $self->{_state_successors_iterator} = shift }
    return $self->{_state_successors_iterator};    
}

sub state_get_data_func {
    my $self = shift;
    if (@_) { $self->{_state_get_data_func} = shift }
    return $self->{_state_get_data_func};    
}

sub show_prog_func {
    my $self = shift;
    if (@_) { $self->{_show_prog_func} = shift }
    return $self->{_show_prog_func};    
}



###################################################################
#
# Add a state from which to begin the search.   There can 
# be multiple start-states.
#
###################################################################
sub add_start_state
{
    my ($self, $state) = @_;


    my $state_eval_func = $self->{_state_eval_func};
    my $state_goal_p_func = $self->{_state_goal_p_func};
    my $state_num_successors_func = $self->{_state_num_successors_func},
    my $state_successors_iterator = $self->{_state_successors_iterator},
    my $state_get_data_func = $self->{_state_get_data_func};
    
    # make sure required functions have been defined
    if(!defined($state_eval_func)){
	croak "SMAstar:  evaluation function is not defined\n";
    }
    if(!defined($state_goal_p_func)){
	croak "SMAstar:  goal function is not defined\n";
    }
    if(!defined($state_num_successors_func)){
	croak "SMAstar:  num successors function is not defined\n";
    }
   if(!defined($state_successors_iterator)){
	croak "SMAstar:  successor iterator is not defined\n";
    }

    # create a path object from this state
    my $state_obj = AI::Pathfinding::SMAstar::Path->new(
	_state           => $state,
	_eval_func      => $state_eval_func,
	_goal_p_func    => $state_goal_p_func,
	_num_successors_func => $state_num_successors_func,
	_successors_iterator => $state_successors_iterator,
	_get_data_func  => $state_get_data_func,
	);
    
    
    my $fcost = AI::Pathfinding::SMAstar::Path::fcost($state_obj);
    # check if the fcost of this node looks OK (is numeric)
    unless(Scalar::Util::looks_like_number($fcost)){
	croak "Error:  f-cost of state is not numeric.  Cannot add state to queue.\n";	
    }
    $state_obj->f_cost($fcost);

    # check if the num_successors function returns a number
    my $num_successors = $state_obj->get_num_successors();
    unless(Scalar::Util::looks_like_number($num_successors)){
	croak "Error:  Number of state successors is not numeric.  Cannot add state to queue.\n";	
    }

    # test out the iterator function to make sure it returns
    #  an object of the correct type
    my $classname = ref($state);
    my $test_successor_iterator = $state_obj->{_successors_iterator}->($state);
    my $test_successor = $test_successor_iterator->($state);
    my $succ_classname = ref($test_successor);

    unless($succ_classname eq $classname){
	croak "Error:  Successor iterator method of object $classname does " .
	    "not return an object of type $classname.\n";	
    }

    
    # add this node to the queue
    $self->{_priority_queue}->insert($state_obj);
 
}

###################################################################
#
# start the SMAstar search process
#
###################################################################
sub start_search
{
    my ($self, 
	$log_function,
	$str_function,
	$max_states_in_queue,
	$max_cost,
	) = @_;

    if(!defined($str_function)){
	croak "SMAstar start_search:  str_function is not defined.\n";
    }

    sma_star_tree_search(\($self->{_priority_queue}), 
                         \&AI::Pathfinding::SMAstar::Path::is_goal, 
                         \&AI::Pathfinding::SMAstar::Path::get_descendants_iterator_smastar,
                         \&AI::Pathfinding::SMAstar::Path::fcost,
			 \&AI::Pathfinding::SMAstar::Path::backup_fvals,
			 $log_function,
			 $str_function,
			 \&AI::Pathfinding::SMAstar::Path::progress,
                         $self->{_show_prog_func},
			 $max_states_in_queue,
                         $max_cost,
	);
}



#################################################################
#
#  SMAstar search
#  Memory-bounded A* search
#
#
#################################################################
sub sma_star_tree_search
{
   
    my ($priority_queue,
	$goal_p,
	$successors_func,
	$eval_func,
	$backup_func,
	$log_function, # debug string func;  represent state object as a string.
	$str_function,
	$prog_function,
	$show_prog_func,
	$max_states_in_queue,
	$max_cost,
	) = @_;
    
    my $iteration = 0;
    my $num_states_in_queue = $$priority_queue->size();
    my $max_extra_states_in_queue = $max_states_in_queue;
    $max_states_in_queue = $num_states_in_queue + $max_extra_states_in_queue;    
    my $max_depth = ($max_states_in_queue - $num_states_in_queue);

    my $best; # the best candidate for expansion


    
    if($$priority_queue->is_empty() || !$$priority_queue){
	return;
    }
    else{
	my $num_successors = 0;
	
	# loop over the elements in the priority queue
	while(!$$priority_queue->is_empty()){
	    
	    # determine the current size of the queue
	    my $num_states_in_queue = $$priority_queue->{_size};
	    # get the best candidate for expansion from the queue
	    $best = $$priority_queue->deepest_lowest_cost_leaf_dont_remove();
    
	    #------------------------------------------------------
	    if(!$DEBUG){
		my $str = $log_function->($best);		 
		$show_prog_func->($iteration, $num_states_in_queue, $str);		    
	    }
	    else{	
		my $str = $log_function->($best);
		print "best is: " . $str_function->($best) . ", cost: " . $best->{_f_cost}  . "\n";
	    }
	    #------------------------------------------------------


	    if($best->$goal_p()) {			
		# goal achieved! iteration: $iteration, number of 
		# states in queue: $num_states_in_queue.
		return $best; 
	    }
	    elsif($best->{_f_cost} >= $max_cost){
		croak "\n\nSearch unsuccessful.  max_cost reached (cost:  $max_cost).\n";
	    }
	    else{	    
		my $successors_iterator = $best->$successors_func();		
		my $succ = $successors_iterator->();
			
		if($succ){
		    # if succ is at max depth and is not a goal node, set succ->fcost to infinity 
		    if($succ->depth() >= $max_depth && !$succ->$goal_p() ){                       
			$succ->{_f_cost} = $max_cost;                                                    
		    }                                                                             
		    else{                 
			# calling eval for comparison, and maintaining pathmax property		
			$succ->{_f_cost} = max($eval_func->($succ), $eval_func->($best));	
			my $descendant_index = $succ->{_descendant_index};
			$best->{_descendant_fcosts}->[$descendant_index] = $succ->{_f_cost};
		    }           
		}

		# determine if $best is completed, and if so backup values
		if($best->is_completed()){


		    # remove from queue first, back up fvals, then insert back on queue. 
		    # this way, it gets placed in its rightful place on the queue.		    
		    my $fval_before_backup = $best->{_f_cost};
		   
		    # STEPS:
		    # 1) remove best and all antecedents from queue, but only if they are 
		    #    going to be altered by backing-up fvals.    This is because 
		    #    removing and re-inserting in queue changes temporal ordering,
		    #    and we don't want to do that unless the node will be
		    #    placed in a new cost-bucket/tree.
		    # 2) then backup fvals
		    # 3) then re-insert best and all antecedents back on queue.


		    # Check if need for backup fvals		    
		    $best->check_need_fval_change();
		   
		    my $cmp_func = sub {
			my ($str) = @_;			
			return sub{
			    my ($obj) = @_;
			    my $obj_path_str = $str_function->($obj);
			    if($obj_path_str eq $str){
				return 1;
			    }
			    else{ 
				return 0; 
			    }	    
			}
		    };

		    my $antecedent = $best->{_antecedent};
		    my %was_on_queue;
		    my $i = 0;

		    # Now remove the offending nodes from queue, if any
		    if($best->need_fval_change()){
			
			# remove best from the queue
			$best = $$priority_queue->deepest_lowest_cost_leaf();  
		    
			while($antecedent){
			    my $path_str = $str_function->($antecedent);	
			    
			    if($antecedent->is_on_queue() && $antecedent->need_fval_change()){
				$was_on_queue{$i} = 1;
				$$priority_queue->remove($antecedent, $cmp_func->($path_str));  	
			    }
			    $antecedent = $antecedent->{_antecedent};
			    $i++;
			}
		    }
		    
	
		    #   Backup fvals
		    if($best->need_fval_change()){
			$best->$backup_func();			
		    }

		    
		    # Put everything back on the queue
		    if($best->need_fval_change()){
			$$priority_queue->insert($best);
			my $antecedent = $best->{_antecedent};
			my $i = 0;
			while($antecedent){
			    if($was_on_queue{$i} && $antecedent->need_fval_change()){  
                                # the antecedent needed fval change too.
				$$priority_queue->insert($antecedent);
			    }
			    if($antecedent->need_fval_change()){
				# set need_fval_change back to 0, so it will not be automatically  seen as 
				# needing changed in the future.  This is important, since we do not want
				# to remove an element from the queue *unless* we need to change the fcost. 
				# This is because when we remove it from the queue and re-insert it, it
				# loses its seniority in the queue (it becomes the newest node at its cost 
				# and depth) and will not be removed at the right time when searching for
				# deepest_lowest_cost_leafs or shallowest_highest_cost_leafs.
				$antecedent->{_need_fcost_change} = 0;
			    }

			    $antecedent = $antecedent->{_antecedent};
			    $i++;			    
			}
			# Again, set need_fval_change back to 0, so it will not be automatically 
			# seen as needing changed in the future.
			$best->{_need_fcost_change} = 0;
		    }
		}


		#
		# If best's descendants are all in memory, mark best as completed.
                #
		if($best->all_in_memory()) { 
		    
		    if(!($best->is_completed())){
			$best->is_completed(1);
		    }

		    my $cmp_func = sub {
			my ($str) = @_;			
			return sub{
			    my ($obj) = @_;
			    my $obj_str = $str_function->($obj);
			    if($obj_str eq $str){
				return 1;
			    }
			    else{ 
				return 0; 
			    }	    
			}
		    };			   
		    
		    my $best_str = $str_function->($best);

		    # If best is not a root node
		    if($best->{_depth} != 0){
			# descendant index is the unique index indicating which descendant
			# this node is of its antecedent.
			my $descendant_index = $best->{_descendant_index};
			my $antecedent = $best->{_antecedent};
			$$priority_queue->remove($best, $cmp_func->($best_str)); 
			if($antecedent){
			    $antecedent->{_descendants_produced}->[$descendant_index] = 0;			   
			}
		    }
		}
		
	        # there are no more successors of $best
		if(!$succ){ 
		    next;
		}

		my $antecedent;
		my @antecedents_that_need_to_be_inserted;

		# If the maximum number of states in the queue has been reached,
		# we need to remove the shallowest-highest-cost leaf to make room 
		# for more nodes.   That means we have to make sure that the antecedent
		# produces this descendant again at some point in the future if needed.
		if($num_states_in_queue > $max_states_in_queue){
		    my $shcl_obj = $$priority_queue->shallowest_highest_cost_leaf($best, $succ, $str_function);	

		    if(!$shcl_obj){
			croak "Error while pruning queue:   shallowest-highest-cost-leaf was null\n";	
		    }
		    $antecedent = $shcl_obj->{_antecedent};
		    if($antecedent){		
			my $antecedent_successors = \$antecedent->{_descendants_list};

			$antecedent->remember_forgotten_nodes_fcost($shcl_obj);
			$antecedent->{_forgotten_nodes_num} = $antecedent->{_forgotten_nodes_num} + 1;
			my $descendant_index = $shcl_obj->{_descendant_index};
		        # record the index of this descendant in the forgotten_nodes list
			$antecedent->{_forgotten_nodes_offsets}->{$descendant_index} = 1;			
			# flag the antecedent as not having this descendant in the queue
			$antecedent->{_descendants_produced}->[$descendant_index] = 0;
			$antecedent->{_descendant_fcosts}->[$descendant_index] = -1;		
			# flag the ancestor node as having deleted a descendant
			$antecedent->descendants_deleted(1);
			# update the number of descendants this node has in memory
			$antecedent->{_num_successors_in_mem} = $antecedent->{_num_successors_in_mem} - 1;				     
			# update the total number of nodes in the queue.
			$num_states_in_queue--;
			
		    }
		} # end if (num_states_on_queue > max_states)

		# if there is a successor to $best, insert it in the priority queue.
		if($succ){
		    $$priority_queue->insert($succ);
		    $best->{_num_successors_in_mem} = $best->{_num_successors_in_mem} + 1;
		}
		else{
		    croak "Error:  no successor to insert\n";
		}
	    }
	}
	continue {
	    $iteration++;
	}

	print "\n\nreturning unsuccessfully.   iteration: $iteration\n";	
	return;
    }
}    




sub max
{
    my ($n1, $n2) = @_;
    return ($n1 > $n2 ? $n1 : $n2);
}


sub fp_compare {
    my ($a, $b, $dp) = @_;
    my $a_seq = sprintf("%.${dp}g", $a);
    my $b_seq = sprintf("%.${dp}g", $b);
    
    

    if($a_seq eq $b_seq){
	return 0;
    }
    elsif($a_seq lt $b_seq){
	return -1;
    }
    else{ 
	return 1;
    }
}





1;
__END__
# Below is stub documentation for your module. You'd better edit it!

=head1 NAME

AI::Pathfinding::SMAstar - Simplified Memory-bounded A* Search


=head1 SYNOPSIS

 use AI::Pathfinding::SMAstar;
  

=head2 EXAMPLE

 ##################################################################
 #
 # This example uses a hypothetical object called FrontierObj, and
 # shows the functions that the FrontierObj class must feature in 
 # order to perform a path-search in a solution space populated by 
 # FrontierObj objects.
 #
 ##################################################################
 
 my $smastar = AI::Pathfinding::SMAstar->new(
        # evaluates f(n) = g(n) + h(n), returns a number
    	_state_eval_func           => \&FrontierObj::evaluate,

        # when called on a node, returns 1 if it is a goal
	_state_goal_p_func         => \&FrontierObj::goal_test,

        # must return the number of successors of a node
        _state_num_successors_func => \&FrontierObj::get_num_successors,      

        # must return *one* successor at a time
        _state_successors_iterator => \&FrontierObj::get_successors_iterator,   

        # can be any suitable string representation 
        _state_get_data_func       => \&FrontierObj::string_representation,  

        # gets called once per iteration, useful for showing algorithm progress
        _show_prog_func            => \&FrontierObj::progress_callback,      
    );

 # You can start the search from multiple start-states.
 # Add the initial states to the smastar object before starting the search.
 foreach my $frontierObj (@start_states){
    $smastar->add_start_state($frontierObj);
 }

 
 #
 # Start the search.  If successful, $frontierGoalPath will contain the
 # goal path.   The optimal path to the goal node will be encoded in the
 # ancestry of the goal path.   $frontierGoalPath->antecedent() contains
 # the goal path's parent path, and so forth back to the start path, which
 # contains only the start state.
 #
 # $frontierGoalPath->state() contains the goal FrontierObj itself.
 #
 my $frontierGoalPath = $smastar->start_search(
    \&log_function,       # returns a string used for logging progress
    \&str_function,       # returns a string used to *uniquely* identify a node 
    $max_states_in_queue, # indicate the maximum states allowed in memory
    $MAX_COST,            # indicate the maximum cost allowed in search
    );



In the example above, a hypothetical object, C<FrontierObj>, is used to
represent a state, or I<node> in your search space.   To use SMA* search to
find a shortest path from a starting node to a goal in your search space, you must
define what a I<node> is, in your search space (or I<point>, or I<state>).

A common example used for informed search methods, and one that is used in Russell's
original paper, is optimal puzzle solving, such as solving an 8 or 15-tile puzzle

lib/AI/Pathfinding/SMAstar.pm  view on Meta::CPAN

approximation algorithms such as I<Simulated Annealing> can provide a
massive reduction in time and space requirements, while introducing a
tunable probability of producing a sub-optimal solution.


=head1 METHODS


=head2 new()

  my $smastar = AI::Pathfinding::SMAstar->new();

Creates a new SMA* search object.


=head2 start_search()

  my $frontierGoalObj = $smastar->start_search(
    \&log_function,       # returns a string used for logging progress
    \&str_function,       # returns a string used to *uniquely* identify a node 
    $max_states_in_queue, # indicate the maximum states allowed in memory
    $MAX_COST,            # indicate the maximum cost allowed in search
    );

Initiates a memory-bounded search.  When calling this function, pass a handle to
a function for recording current status( C<log_function> above- this can be
an empty subroutine if you don't care), a function that returns a *unique* string
representing a node in the search-space (this *cannot* be an empty subroutine), a
maximum number of expanded states to store in the queue, and a maximum cost
value (beyond which the search will cease).


=head2 state_eval_func()

 $smastar->state_eval_func(\&FrontierObj::evaluate);

Set or get the handle to the function that returns the cost of the object 
argument (node) in the search space. 


=head2 state_goal_p_func()

 $smastar->state_goal_p_func(\&FrontierObj::goal_test);

Set/get the handle to the goal predicate function.   This is a function 
that returns 1 if the argument object is a goal node, or 0 otherwise.



=head2 state_num_successors_func()

 $smastar->state_num_successors_func(\&FrontierObj::get_num_successors);

Set/get the handle to the function that returns the number of successors 
of this the object argument (node).


=head2 state_successors_iterator()

 $smastar->state_successors_iterator(\&FrontierObj::get_successors_iterator);

Set/get the handle to the function that returns iterator that produces the 
next successor of this node.


=head2 state_get_data_func()

 $smastar->state_get_data_func(\&FrontierObj::string_representation);

Set/get the handle to the function that returns a string 
representation of this node.


=head2 show_prog_func()

 $smatar->show_prog_func(\&FrontierObj::progress_callback);

Sets/gets the callback function for displaying the progress of the search.
It can be an empty callback (sub{}) if you do not need this output.



=head2 DEPENDENCIES

 Tree::AVL
 Test::More


=head2 INCLUDED MODULES

 AI::Pathfinding::SMAstar
 AI::Pathfinding::SMAstar::Path
 AI::Pathfinding::SMAstar::PriorityQueue
 AI::Pathfinding::SMAstar::TreeOfQueues



=head2 EXPORT

None by default.



=head1 SEE ALSO

lib/AI/Pathfinding/SMAstar/AVLQueue.pm  view on Meta::CPAN

use AI::Pathfinding::SMAstar::PairObj;
use Carp;
use strict;



##################################################
#  AVLQueue constructor 
##################################################
sub new {
    my $invocant = shift;
    my $class   = ref($invocant) || $invocant;
    my $self = {
	_key         => undef, # for comparisons with other queues, etc.

	_avltree         => Tree::AVL->new(fcompare => \&AI::Pathfinding::SMAstar::AVLQueue::compare_obj_counters,
					   fget_key => \&AI::Pathfinding::SMAstar::AVLQueue::obj_counter,
					   fget_data => \&AI::Pathfinding::SMAstar::AVLQueue::obj_value),
	
	_counter     => 0,
	
	_obj_counts_tree => Tree::AVL->new(fcompare => \&AI::Pathfinding::SMAstar::PairObj::compare_keys_numeric,
					   fget_key => \&AI::Pathfinding::SMAstar::PairObj::key,
					   fget_data => \&AI::Pathfinding::SMAstar::PairObj::val),
		
        @_,    # Override previous attributes
    };
    return bless $self, $class;
}



##############################################
# accessor
##############################################

sub key
{
    my $self = shift;
    if (@_) { $self->{_key} = shift }
    return $self->{_key};	
}





#############################################################################
#
# other methods
#
#############################################################################


sub get_keys_iterator
{
    my ($self) = @_;
    return $self->{_obj_counts_tree}->get_keys_iterator();
}



sub compare_obj_counters{
    my ($obj, $arg_obj) = @_;

     if ($arg_obj){
	my $arg_key = $arg_obj->{_queue_counter};
	my $key = $obj->{_queue_counter};
	
	if($arg_key > $key){
	    return(-1);
	}
	elsif($arg_key == $key){
	    return(0);
	}
	elsif($arg_key < $key){
	    return(1);
	}	
    }
    else{
	croak "AVLQueue::compare_obj_counters: error: null argument object\n";
    }
}


sub obj_counter{
    my ($obj) = @_;
    return $obj->{_queue_counter};
}

sub obj_value{
    my ($obj) = @_;
    return $obj->{_value};
}



sub compare {
    my ($self, $arg_obj) = @_;

    if ($arg_obj){
	my $arg_key = $arg_obj->{_key};
	my $key = $self->{_key};
	
	if($arg_key > $key){
	    return(-1);
	}
	elsif($arg_key == $key){
	    return(0);
	}
	elsif($arg_key < $key){
	    return(1);
	}	
    }
    else{
	croak "AVLQueue::compare error: null argument object\n";
    }
}

sub lookup {    
    my ($self, $obj) = @_;        
    my $found_obj = $self->{_avltree}->lookup_obj($obj);

    if(!$found_obj){
	croak "AVLQueue::lookup:  did not find obj in queue\n";
	return;
    }    
    return $found_obj;
}

sub lookup_by_key {    
    my ($self, $key) = @_;    
    my $pair =  AI::Pathfinding::SMAstar::PairObj->new(
	_queue_counter => $key,
	);	       
    my $found_obj = $self->{_avltree}->lookup_obj($pair);

    if(!$found_obj){
	croak "AVLQueue::lookup:  did not find obj in queue\n";
	return;
    }    
    return $found_obj;
}


sub remove {
    my ($self, $obj, $compare_func) = @_;
    my $found_obj;
    
    $found_obj = $self->{_avltree}->remove($obj);

    if(!$found_obj){
	croak "AVLQueue::remove:  did not find obj in queue\n";
	return;
    }
    
    my $count = $found_obj->{_queue_counter};
   

    my $pairobj = AI::Pathfinding::SMAstar::PairObj->new(_key => $count,
			       _value => $count);
    $self->{_obj_counts_tree}->remove($pairobj);

    return $found_obj;
}



sub is_empty
{
    my ($self) = @_; 
    
    if($self->{_avltree}->is_empty()){
	return 1;
    }
    return 0;    
}


sub insert
{
    my ($self,
	$obj) = @_;
        
    my $count = $self->{_counter};

    $obj->{_queue_counter} = $count;       
    $self->{_avltree}->insert($obj);
    


    my $pairobj = AI::Pathfinding::SMAstar::PairObj->new(_key => $count,
			       _value => $count);
    $self->{_obj_counts_tree}->insert($pairobj);

    $self->{_counter} = $self->{_counter} + 1;

    
    return;
}


sub pop_top
{
    my ($self) = @_;
   
    my $top = $self->{_avltree}->pop_smallest();
    my $count = $top->{_queue_counter};
  

    my $pairobj = AI::Pathfinding::SMAstar::PairObj->new(_key => $count,
			       _value => $count);
    $self->{_obj_counts_tree}->remove($pairobj);


    return $top;
}



sub top
{
    my ($self) = @_;
    
    my $top = $self->{_avltree}->smallest();
    return $top;
    

}


sub get_list{
    my ($self) = @_;
    return $self->{_avltree}->get_list();
}


sub get_size{
    my ($self) = @_;
    my $avltree = $self->{_avltree};
    my $size = $avltree->get_size();    
    return $size;
}


sub print{
    my ($self, $delim) = @_;
    my @tree_elts = $self->{_avltree}->get_list(); 
    
    foreach my $obj (@tree_elts){
	print $obj->{_start_word} . ", " . $obj->{_phrase} . ", " . $obj->{_queue_counter} . "\n";
	
    }

     print "\n\nobj_counts_tree:\n";
    $self->{_obj_counts_tree}->print("*");



    my $iterator = $self->{_obj_counts_tree}->get_keys_iterator();
    print "\n\niterator keys:\n";
    while(defined(my $key = $iterator->())){
	print "iterator key: $key\n";
    }
    

}




1;

lib/AI/Pathfinding/SMAstar/Examples/PalUtils.pm  view on Meta::CPAN


use Tree::AVL;
use AI::Pathfinding::SMAstar::Examples::WordObj;
package AI::Pathfinding::SMAstar::Examples::PalUtils;


my $max_nodes_in_mem = 0;

sub length_no_spaces
{
    my ($w) = @_;    
    $w =~ s/\ //g;
    return length($w);
}



sub get_word_number_of_letters_that_have_repeats
{
    my ($word) = @_;    
    my @letters = split('', $word);
    my %letters_hash = ();

    foreach my $element (@letters) { $letters_hash{$element}++ }

    my $repeated_letters = 0;
    foreach my $element (keys %letters_hash){
	if($letters_hash{$element} > 1){
	    $repeated_letters++;
	}
    }
    
    return $repeated_letters;
}


#
# finds the number of times each letter appears within
# an entire list of words.   returns a hash of the letters
#
sub find_letter_frequencies
{
    my (@words) = @_;
    my %letters_freq;

    foreach my $w (@words)
    {
	@letters = split('', $w); 
	
	foreach my $l (@letters)
	{
	    $letters_freq{$l}++;
	}
    }

    return %letters_freq;
}


sub collisions_per_length
{
    my ($w, $phrase) = @_;

    if(!$w){ $w = "" }
    if(!$phrase){ $phrase = "" }


    my $length = length($w);
    $phrase =~ s/ //g;
    my @letters = split('', $w); 
    my @letters_seen = split('', $phrase); 
    my $collisions = 0;
    foreach my $l (@letters){	
	foreach my $ls (@letters_seen){
	    if($l eq $ls){
		$collisions++;
	    }
	}
    }
    my $val = $collisions/$length;

    return $val;
}




sub get_word_sparsity
{
    my ($word) = @_; 

    my $length = length($word);
    my $num_letters = num_chars_in_word_memo($word);

    my $sparseness = $length - $num_letters;

    return $sparseness;
}


{ my %memo_cache;
sub get_word_sparsity_memo
{
    my ($word) = @_; 

    if($memo_cache{$word}){
	return $memo_cache{$word};
    }
    else{
	my $length = length($word);
	my $num_letters = num_chars_in_word_memo($word);
	
	my $sparseness = $length - $num_letters;
	
	$memo_cache{$word} = $sparseness;
	return $sparseness;
    }
}
}


# get the highest number of times a letter 
# is repeated within a word.
sub get_word_highest_frequency
{
    my ($word) = @_;    
    my @letters = split('', $word);
    my %letters_hash = ();

    foreach my $element (@letters) { $letters_hash{$element}++ }

    my $most_frequent_letter_freq = 0;
    foreach my $element (keys %letters_hash){
	if($letters_hash{$element} > $most_frequent_letter_freq){
	    $most_frequent_letter_freq = $letters_hash{$element};
	}
    }    
    return $most_frequent_letter_freq;
}




sub get_letters
{
    my ($word) = @_;
    my @letter_set = ();
    my %letters_hash = ();
    my @letters = split('', $word);

    foreach my $element (@letters) { $letters_hash{$element}++ }

    foreach my $element (keys %letters_hash)
    {
	push(@letter_set, $element);
    }
    return @letter_set;
}



{ my %memo_cache;
sub word_collision_memo
{
    my ($word,
	$sorted_letters_seen) = @_;

    my $sorted_letters_seen_str = join('', @$sorted_letters_seen);
    my $memo_key = $word . $sorted_letters_seen_str;
    
    #print "sorted_letters_seen_str:  $sorted_letters_seen_str\n";
    
    if($memo_cache{$memo_key}){
	return @{$memo_cache{$memo_key}};	
    }
    else{
    my @letters = split('', $word);
  
    my @difference = ();
    my %letters_hash = ();
    my %letters_seen_hash = ();
    
    my $intersect_num = 0;
    my @intersection;

    foreach my $element (@$sorted_letters_seen) { $letters_seen_hash{$element}++ }

    foreach my $element (@letters) { $letters_hash{$element}++ }
    
    foreach my $element (keys %letters_hash) {       	
	if($letters_seen_hash{$element}){
	    push(@intersection, $element);
	    $intersect_num++;	    
	}
	else{
	    push(@difference, $element);
	}	
    }
   
    my @answer = ($intersect_num, @difference);

    $memo_cache{$memo_key} = \@answer;
    return ($intersect_num, @difference);
    }
}
}




sub word_collision{
    my ($word,
	$letters_seen) = @_;
    
    my @letters = split('', $word);
  
    my @difference = ();
    my %letters_hash = ();
    my %letters_seen_hash = ();
    
    my $intersect_num = 0;
    my @intersection;

    foreach my $element (@$letters_seen) { $letters_seen_hash{$element}++ }
    
    foreach my $element (@letters) { $letters_hash{$element}++ }
    
    foreach my $element (keys %letters_hash) {       	
	if($letters_seen_hash{$element}){
	    push(@intersection, $element);
	    $intersect_num++;	    
	}
	else{
	    push(@difference, $element);
	}
    }
    
    return ($intersect_num, @difference);   
}



sub get_cands_from_left
{   

    my ($word,
	$dictionary,
	$dictionary_rev) = @_;

    my @cands = get_cands_memo($word, $dictionary_rev);    
    
    foreach my $c (@cands){
	$c = reverse($c);
    }
    my @sorted_cands = sort(@cands);
    return @sorted_cands;    
}

sub get_cands_from_right
{
    my ($word,
	$dictionary,
	$dictionary_rev) = @_;
   
    my $rev_word = reverse($word);

    my @cands = get_cands_memo($rev_word, $dictionary);    
    my @sorted_cands = sort(@cands);
    return @sorted_cands;
}


{my $memo_hash_ref  = {}; 
 sub get_cands_memo
 {
     my ($word, $dictionary_rev) = @_;    
     
     my $cand = AI::Pathfinding::SMAstar::Examples::WordObj->new(
	 _word => $word
	 );

     my $cache_key = $word . $dictionary_rev;
     my $cached_vals = $memo_hash_ref->{$cache_key};
     if($cached_vals){
	 #print $spaces . "DING DING DING. cache hit!\n";
	 return @$cached_vals;
	     
     }
     else{
	 
	 my @substr_cands = get_substrs_memo($word, $dictionary_rev);
	 my @superstr_cands = $dictionary_rev->acc_lookup_memo($cand, 
							       \&AI::Pathfinding::SMAstar::Examples::WordObj::compare_up_to, 
							       \&AI::Pathfinding::SMAstar::Examples::WordObj::compare);        
	 my @cands = (@substr_cands, @superstr_cands); 
	 # these are all the words in the dictionary that could end this pal.
	 $memo_hash_ref->{$cache_key} = \@cands;
	 return @cands;
     }
 }
}

sub get_cands
{
    my ($word, $dictionary_rev) = @_;    
    
    my $cand = AI::Pathfinding::SMAstar::Examples::WordObj->new(
	_word => $word
	);

    my @substr_cands = get_substrs_memo($word, $dictionary_rev);
    my @superstr_cands = $dictionary_rev->acc_lookup($cand, 
						     \&AI::Pathfinding::SMAstar::Examples::WordObj::compare_up_to, 
						     \&AI::Pathfinding::SMAstar::Examples::WordObj::compare);        
    my @cands = (@substr_cands, @superstr_cands); 
    # these are all the words in the dictionary that could end this pal.
    return @cands;
}


sub match_remainder
{
    my ($word1, $word2) = @_;
   
    $word1 =~ s/\ //g;
    $word2 =~ s/\ //g;

    my $len1 = length($word1);
    my $len2 = length($word2);

    if(index($word1, $word2) != 0)
    {
	return;
    }
    my $remainder_word = substr($word1, $len2);
    return $remainder_word;
}



#
# memoized version of get_substrs-  for speed
#
{my $memo_hash_ref = {};
sub get_substrs_memo
{
    my ($word, $dictionary) = @_;
   
    my @words;
    my @matches;
    
   
    my $cache_key = $word . $dictionary;
    my $cached_vals = $memo_hash_ref->{$cache_key};
    if($cached_vals1){
	#print $spaces . "DING DING DING. cache hit!\n";
	return @$cached_vals;
	
    }
    else{	
	for(my $i = 1; $i < length($word); $i++){
	    push(@words, substr($word, 0, $i));
	}
	
	foreach my $substring (@words){
	    #print "looking for matches on: \"$substring\"\n";
	    
	    my $cand = AI::Pathfinding::SMAstar::Examples::WordObj->new(
		_word => $substring
		);
	    my $match_word = $dictionary->lookup($cand, \&AI::Pathfinding::SMAstar::Examples::WordObj::compare);
	    if($match_word){
		# print "FOUND A MATCH: $match_word\n";
		push(@matches, $match_word);
	    }
	    
	}
	#print "no hashed value yet, creating one.\n";
	$memo_hash_ref->{$cache_key} = \@matches;
	return @matches;
    }
}
}


sub get_substrs
{
    my ($word, $dictionary) = @_;
   
    my @words;
    my @matches;

    for(my $i = 1; $i < length($word); $i++){
	push(@words, substr($word, 0, $i));
    }

    foreach my $substring (@words){
	#print "looking for matches on: \"$substring\"\n";

	my $cand = AI::Pathfinding::SMAstar::Examples::WordObj->new(
	    _word => $substring
	    );
	my $match_word = $dictionary->lookup($cand, \&AI::Pathfinding::SMAstar::Examples::WordObj::compare);
	if($match_word){
	   # print "FOUND A MATCH: $match_word\n";
	    push(@matches, $match_word);
	}
	
    }
    return @matches;
}



# randomize an array.  Accepts a reference to an array.
sub fisher_yates_shuffle {
    my ($array) = @_;
    my $i;
    for ($i = @$array; --$i; ) {
        my $j = int rand ($i+1);
        next if $i == $j;
        @$array[$i,$j] = @$array[$j,$i];
    }
}

sub process_words
{
    my ($words) = @_;	
    my @word_objs;
    
    for(my $i = 0; $i < @$words; $i++) 
    {       
	my $word = $words->[$i];
	chomp($word);

	$word_objs[$i] = AI::Pathfinding::SMAstar::Examples::WordObj->new(
	    _word => $word,
	   
	    );		
    }
    return @word_objs;
}

sub process_words_by_density
{
    my ($words, 
	$max_score # 0:  no repeats, 1: 1 repeat, etc.
	) = @_;
    
    my @word_objs;
    
    my $i = 0;
    foreach my $word (@$words)
    {       	
	chomp($word);		
	my $sparsity = get_word_sparsity($word);	

	if($sparsity <= $max_score){	  
	    $word_objs[$i] = AI::Pathfinding::SMAstar::Examples::WordObj->new(
		_word => $word,		
		);	
	    $i++;
	}	
    }
    return @word_objs;
}




sub process_rev_words
{
    my ($words) = @_;
    my @word_objs;
    
    for(my $i = 0; $i < @$words; $i++) 
    {       
	my $word = $words->[$i];
	chomp($word);

	my $rev_word = reverse($word);

	$word_objs[$i] = AI::Pathfinding::SMAstar::Examples::WordObj->new(
	    _word => $rev_word,	    
	    );		
    }
    return @word_objs;
}

sub process_rev_words_by_density
{
    my ($words, 
	$max_score # 0:  no repeats, 1: 1 repeat, etc.
	) = @_;
    
    my @word_objs;
    
    my $i = 0;
    foreach my $word (@$words)
    {       	
	chomp($word);

	my $rev_word = reverse($word);
	my $sparsity = get_word_sparsity($word);	

	if($sparsity <= $max_score){
	    $word_objs[$i] = AI::Pathfinding::SMAstar::Examples::WordObj->new(
		_word => $rev_word,		
		);	
	    $i++;
	}	
    }
    return @word_objs;
}


sub is_palindrome
{
    my ($candidate) = @_;
    if(!$candidate){
	return 0;
    }
    $candidate =~ s/\ //g;
    return($candidate eq reverse($candidate));
}

sub join_strings
{
    my ($strings) = @_;
    my $candidate = join(' ', @$strings);
    
    return $candidate;    
}

sub make_one_word
{
    my ($phrase) = @_;    
    $phrase =~ s/\s//g;  
    return $phrase;
}


sub num_chars_in_word
{
    my ($word) = @_;
    my %hash;
    
    if(!$word) { return 0; }
    
    @hash{ split '', $word} = 1;
    my $num_keys = keys(%hash);
    
    return $num_keys;
}


{my %memo_cache;
sub num_chars_in_word_memo
{
    my ($word) = @_;

    if($memo_cache{$word}){	
	return $memo_cache{$word};		
    }
    else{
	my %hash;
	@hash{ split '', $word} = 1;
	my $num_keys = keys(%hash);
	
	$memo_cache{$word} = $num_keys;
	return $num_keys;
    }
}
}


{my %memo_cache;
sub num_chars_in_pal
{
    my ($pal) = @_;    
    my $num_keys;

    $pal =~ s/\ //g;
    my $length = length($pal);
    my $first_half = substr($pal, 0, $length/2 + 1);


    if($memo_cache{$first_half}){	
	return $memo_cache{$first_half};		
    }
    else{

	my %hash;
	@hash{ split '', $first_half } = 1;
	$num_keys = keys(%hash);
	
	$memo_cache{$pal} = $num_keys;
	return $num_keys;
    }
}
}

sub read_dictionary
{
    my ($in_file) = @_;
    
    unless(open(READF, "+<$in_file")){	
	return;
    }
	
    my @lines = <READF>;
       
    close(READF);
    
    return @lines;
}

sub read_dictionary_filter_by_density
{
    my ($in_file, $max_score) = @_;
    
    unless(open(READF, "+<$in_file")){	
	return;
    }
	
    my @lines = <READF>;
    my $num_lines = @lines;
       
    close(READF);

    my @filtered_words;
    
    my $i = 0;
    foreach my $word (@lines)
    {       	
	chomp($word);	
	my $sparsity = get_word_sparsity($word);

	if($sparsity <= $max_score){	  
	    $filtered_words[$i] = $word;			
	    $i++;
	}	
    }

    return ($num_lines, @filtered_words);
}

sub read_dictionary_filter_by_density_rev
{
    my ($in_file, $max_score) = @_;
    
    unless(open(READF, "+<$in_file")){	
	return;
    }
	
    my @lines = <READF>;
    my $num_lines = @lines;
       
    close(READF);

    my @filtered_words;
    
    my $i = 0;
    foreach my $word (@lines)
    {       	
	chomp($word);	
	my $sparsity = get_word_sparsity($word);

	if($sparsity <= $max_score){
	    my $rev_word = reverse($word);
	    $filtered_words[$i] = $rev_word;			
	    $i++;
	}	
    }

    return ($num_lines, @filtered_words);
}



sub flush {
   my $h = select($_[0]); my $a=$|; $|=1; $|=$a; select($h);
}

{my $spinny_thing = "-";
 my $call_num = 0;
 my $state;
sub show_progress {
    $call_num++;
    $state = $call_num % 4;
    if($state == 0){
	$spinny_thing = "-";
    }
    elsif($state == 1){
	$spinny_thing = "\\";
    }
    elsif($state == 2){
	$spinny_thing = "|";
    }
    elsif($state == 3){
	$spinny_thing = "/";
    }

    my ($progress) = @_;
    my $stars   = '*' x int($progress*10);
    my $percent = sprintf("%.2f", $progress*100);
    $percent = $percent >= 100 ? '100.00%' : $percent.'%';
    
    print("\r$stars $spinny_thing $percent.");
    flush(STDOUT);
}
}



sub show_search_depth_and_percentage {
    my ($depth, $so_far, $total) = @_;
    my $stars   = '*' x int($depth);   

    my $amount_completed = $so_far/$total;
    
    my $percentage = sprintf("%0.2f", $amount_completed*100);

    print("\r$stars depth: $depth. completed:  $percentage %");
    flush(STDOUT);
}


sub show_search_depth_and_num_states {
    my ($depth, $states) = @_;
    my $stars   = '*' x int($depth);   
    my $num_states = @$states;

    print("\rdepth: $depth. num_states:  $num_states");
    flush(STDOUT);
}





{my $LINES=`tput lines`; # number of rows in current terminal window
 my $COLUMNS=`tput cols`; # number of columns in current terminal window

sub show_progress_so_far {
    my ($iteration, $num_states, $str, $opt_datum, $opt_datum2) = @_;
    my $stars   = '*' x int($iteration);   
    

#     print  "\e[H";              # Put the cursor on the first line
#     print  "\e[J";              # Clear from cursor to end of screen
#     print  "\e[H\e[J";          # Clear entire screen (just a combination of the above)
#     print  "\e[K";              # Clear to end of current line (as stated previously)
#     print  "\e[m";              # Turn off character attributes (eg. colors)
#     printf "\e[%dm", $N;        # Set color to $N (for values of 30-37, or 100-107)
#     printf "\e[%d;%dH", $R, $C; # Put cursor at row $R, column $C (good for "drawing")

   
    
    
    #print "\e[H\e[J"; #clears the entire screen
    printf "\e[%d;%dH", $LINES-1, 1; # Put cursor at row $R, column $C (good for "drawing")
    
    print "\e[J";  #clears to end of screen

    if($num_states > $max_nodes_in_mem){
	$max_nodes_in_mem = $num_states;
    }


    print "\riteration: $iteration, num_states_in_memory: $num_states, max_states_in_mem: $max_nodes_in_mem\n"; 
    

    printf "\e[%d;%dH", $LINES, 1; # Put cursor at row $R, column $C (good for "drawing")

    print "\e[J";  #clears to end of screen

    print "string: $str\e[J";


    flush(STDOUT);
}
}


sub show_search_depth_and_num_states_debug {
   
}


{my $LINES=`tput lines`; # number of rows in current terminal window
 my $COLUMNS=`tput cols`; # number of columns in current terminal window

sub show_progress_so_far_debug {
    my ($depth, $prog, $num_states, $str, $num_successors) = @_;
    my $stars   = '*' x int($depth);   
    
  
    print "depth: $depth, string: $str, num_successors:  $num_successors\n";

    flush(STDOUT);
}
}








lib/AI/Pathfinding/SMAstar/Examples/Phrase.pm  view on Meta::CPAN

# Date :  June 2008
#
#

package AI::Pathfinding::SMAstar::Examples::Phrase;
use Tree::AVL;
use AI::Pathfinding::SMAstar::Examples::PalUtils;
use strict;

BEGIN {
    use Exporter ();
    @AI::Pathfinding::SMAstar::Examples::Phrase::ISA         = qw(Exporter);
    @AI::Pathfinding::SMAstar::Examples::Phrase::EXPORT      = qw();
    @AI::Pathfinding::SMAstar::Examples::Phrase::EXPORT_OK   = qw($d);

  }

use vars qw($d $max_forgotten_nodes);  # used to debug destroy method for accounting purposes
$d = 0;
$max_forgotten_nodes = 0;


##################################################
## the Phrase constructor 
##################################################
sub new {
    my $invocant = shift;
    my $class   = ref($invocant) || $invocant;
    my $self = {
	_word_list               => undef,
	_words_w_cands_list      => undef,
	_dictionary              => undef,
	_dictionary_rev          => undef,
	_start_word              => undef,  # remainder on cand for antecedent of this obj
	_word                    => undef,
	_cand                    => undef,  # cand found for the antecedent of this obj
	_predecessor             => undef,
	_dir                     => 0,
	_repeated_pal_hash_ref   => {},
        _match_remainder_left    => undef,  
	_match_remainder_right   => undef,
	_letters_seen            => undef,  # letters seen, up to/including antecedent
	_cost                    => undef,  # cost used for heuristic search
	_cost_so_far             => undef,
	_num_chars_so_far        => undef,  # cummulative cost used for heuristic
	_num_new_chars           => undef,
	_no_match_remainder      => undef,  # flag specifying whether there was a remainder
	_phrase                  => undef,
	_depth                   => 0,
	_f_cost                  => undef,
	@_,    # Override previous attributes
    };

    return bless $self, $class;
 
}

##############################################
## methods to access per-object data        
##                                    
## With args, they set the value.  Without  
## any, they only retrieve it/them.         
##############################################

sub start_word {
    my $self = shift;
    if (@_) { $self->{_start_word} = shift }
    return $self->{_start_word};
}

sub word {
    my $self = shift;
    if (@_) { $self->{_word} = shift }
    return $self->{_word};
}

sub cand {
    my $self = shift;
    if (@_) { $self->{_cand} = shift }
    return $self->{_cand};
}

sub antecedent{
    my $self = shift;
    if (@_) { $self->{_predecessor} = shift }
    return $self->{_predecessor};
}



sub dir{
    my $self = shift;
    if (@_) { $self->{_dir} = shift }
    return  $self->{_dir};
}

sub match_remainder_left{
    my $self = shift;
    if (@_) { $self->{_match_remainder_left} = shift }
    return  $self->{_match_remainder_left};
}

sub match_remainder_right {
    my $self = shift;
    if (@_) { $self->{_match_remainder_right} = shift }
    return  $self->{_match_remainder_right};
}

sub intersect_threshold {
    my $self = shift;
    if (@_) { $self->{_intersect_threshold} = shift }
    return  $self->{_intersect_threshold};
}

sub max_collisions{
    my $self = shift;
    if (@_) { $self->{_max_collisions} = shift }
    return  $self->{_max_collisions};
}

sub letters_seen{
    my $self = shift;
    if (@_) { $self->{_letters_seen} = shift }
    return  $self->{_letters_seen};
}

sub f_cost{
    my $self = shift;
    if (@_) { $self->{_f_cost} = shift }
    return  $self->{_f_cost};
}

sub depth{
    my $self = shift;
    if (@_) { $self->{_depth} = shift }
    return  $self->{_depth};
}

sub is_completed{
    my $self = shift;
    if (@_) { $self->{_is_completed} = shift }
    return  $self->{_is_completed};
}

sub is_on_queue{
    my $self = shift;
    if (@_) { $self->{_is_on_queue} = shift }
    return  $self->{_is_on_queue};
}

sub descendants_deleted{
    my $self = shift;
    if (@_) { $self->{_descendants_deleted} = shift }
    return  $self->{_descendants_deleted};
}

sub need_fval_change{
    my $self = shift;
    if (@_) { $self->{_need_fcost_change} = shift }
    return  $self->{_need_fcost_change};
}



	

sub compare
{
    my ($min_letters) = @_;

    return sub{
	my ($self, $arg_obj) = @_;

	my $self_eval_func = evaluate($min_letters);
	my $argobj_eval_func = evaluate($min_letters);
	my $self_eval = $self->$self_eval_func;
	my $arg_obj_eval = $arg_obj->$argobj_eval_func;
	
	return $self_eval - $arg_obj_eval;
    }
}



sub compare_by_depth
{
    my ($self, $arg_obj) = @_;
    
    my $self_depth = $self->{_depth};
    my $argobj_depth = $arg_obj->{_depth};
    
    my $result = $self_depth - $argobj_depth;
    
    return $result;    
}



# compare_phrase_word_strings
#
# usage:  $phrase_obj->compare_phrase_word_strings($other_word_obj)
#
# Accepts another Phrase object as an argument.
# Returns 1 if greater than argument, 0 if equal, and -1 if 
# less than argument
sub compare_phrase_word_strings
{
    my ($self, $arg_obj) = @_;
   
    my $arg_phrase_plus_word = $arg_obj->{_phrase} . $arg_obj->{_word};          
    my $phrase_plus_word = $self->{_phrase} . $self->{_word};
    
    if($arg_phrase_plus_word gt $phrase_plus_word){
	return -1;
    }
    elsif($arg_phrase_plus_word eq $phrase_plus_word){
	return 0;
    }
    return 1;   
}



#----------------------------------------------------------------------------
# evaluation function f(n) = g(n) + h(n) where 
#
# g(n) = cost of path through this node
# h(n) = distance from this node to goal (optimistic)
#
# used for A* search.
#
sub evaluate
{    
    my ($min_num_letters) = @_;
    return sub{
		
	my ($self) = @_;

	# if fcost has already been calculated (or reassigned during a backup)
	# then return it.   otherwise calculate it
	my $fcost = $self->{_f_cost};
	if(defined($fcost)){	    
	    return $fcost;
	}

	my $word = $self->{_start_word};
	my $cost = $self->{_cost};
	my $cost_so_far = $self->{_cost_so_far};
	my $num_new_chars = $self->{_num_new_chars};
	my $num_chars_so_far = $self->{_num_chars_so_far};

	my $phrase = defined($self->{_phrase}) ? $self->{_phrase} : "";
	my $len_phrase = length($phrase);
	my $phrase_num_chars = AI::Pathfinding::SMAstar::Examples::PalUtils::num_chars_in_word($phrase);
	
	my $ratio = 0;
	if($phrase_num_chars){	    
	    $ratio = $len_phrase/$phrase_num_chars;	
	}


	#my $total_cost = $cost_so_far + $cost;
	my $total_cost = $cost_so_far + $cost + $ratio;
	#my $total_cost = 0;  # greedy search (best-first search)	
	#my $distance_from_goal = 0; # branch and bound search.  optimistic/admissible.
        
        my $distance_from_goal = $min_num_letters - ($num_chars_so_far + $num_new_chars);  #1 optimistic/admissible

	my $evaluation = $total_cost + $distance_from_goal;	
	$self->{_f_cost} = $evaluation;

	return $evaluation;
    }
}

#-----------------------------------------------------------------------------
sub phrase_is_palindrome_min_num_chars
{
    my ($min_num_chars) = @_;
    
    return sub{
	my ($self) = @_;
	
	my $phrase = $self->{_phrase};
	
	if(AI::Pathfinding::SMAstar::Examples::PalUtils::is_palindrome($phrase) && 
	   (AI::Pathfinding::SMAstar::Examples::PalUtils::num_chars_in_pal($phrase) >= $min_num_chars)){
	    return 1;
	}
	else{ 
	    return 0; 
	}
    }
}

    
    
#----------------------------------------------------------------------------
sub letters_seen_so_far
{
    my ($self) = @_;      
    my $num_letters_seen = $self->{_num_chars_so_far};    
  
    return $num_letters_seen;
}









lib/AI/Pathfinding/SMAstar/Examples/Phrase.pm  view on Meta::CPAN



#-----------------------------------------------------------------------------
# Get descendants iterator function.
# Generate the next descendant of a phrase object. Each descendant adds
# another word to the phrase that could possibly lead to a palindrome
#
#-----------------------------------------------------------------------------
sub get_descendants_iterator
{
    my ($phrase_obj) = @_;
    if(!$phrase_obj){
	return;
    }
	
    my $words = $phrase_obj->{_word_list};
    my $words_w_cands = $phrase_obj->{_words_w_cands_list};
    my $dictionary = $phrase_obj->{_dictionary};
    my $dictionary_rev = $phrase_obj->{_dictionary_rev};
    my $repeated_pal_hash_ref = $phrase_obj->{_repeated_pal_hash_ref};
    my $letters_seen = $phrase_obj->{_letters_seen};
    my $cost = $phrase_obj->{_cost};
    my $cost_so_far = $phrase_obj->{_cost_so_far};
    my $num_chars_so_far = $phrase_obj->{_num_chars_so_far};
    my $no_match_remainder = $phrase_obj->{_no_match_remainder};
    my $depth = $phrase_obj->{_depth};    
    my $direction = $phrase_obj->{_dir};
    my $word = $phrase_obj->{_start_word};
    my $whole_word = $phrase_obj->{_cand};
    my $len_whole_word = defined($whole_word) ? length($whole_word) : 0;
    my $rev_word = reverse($word);
    my $len_word = length($word);
    my @cands;
    my @descendants;

   
    if($direction == 0){
	@cands = AI::Pathfinding::SMAstar::Examples::PalUtils::get_cands_from_left($word, $dictionary, $dictionary_rev);
    }
    elsif($direction == 1){
	@cands = AI::Pathfinding::SMAstar::Examples::PalUtils::get_cands_from_right($word, $dictionary, $dictionary_rev);
    }
    
  
    
    #----------------Letters Seen-----------------------------------------------
    my @sorted_letters_seen = sort(@$letters_seen);
    # how much does this word collide with the letters seen so far, and what are the new letters?
    my ($word_intersect, @differences) = AI::Pathfinding::SMAstar::Examples::PalUtils::word_collision($word, \@sorted_letters_seen);
    # store the difference in new letters_seen value.
    push(@sorted_letters_seen, @differences);
         
    my $new_num_chars_so_far = @sorted_letters_seen;  
    #-----------------------------------------------------------
    

 

    my @words_to_make_phrases;
    my $stored_c;

    return sub{
		
      LABEL1:
	# this is a continuation of the second case below, where there were no 
	# match-remainders for the phrase-so-far, i.e. the palindrome has a space
	# in the middle with mirrored phrases on each side. 'cat tac' for example.
	my $next_word = shift(@words_to_make_phrases);
	if($next_word){
	    
	    my $w = $next_word;

	    my $repeated_word_p = 0;
	    my $antecedent = $phrase_obj->{_predecessor};
	    my $antecedent_dir = $antecedent->{_dir};
	    while($antecedent){

		if(defined($antecedent->{_word}) && $w eq $antecedent->{_word} && $antecedent_dir == 0){
		    $repeated_word_p = 1;
		    last;
		}
		$antecedent = $antecedent->{_predecessor};	
		if($antecedent){
		    $antecedent_dir = $antecedent->{_dir};
		}
	    }

	    if($repeated_word_p || $w eq $word){
		goto LABEL1;
		#next;  #skip this word, we are already looking at it
	    }

	    #----------------Compute the Cost-------------------------------------------
	    my $len_w = length($w);
	    my $collisions_per_length = AI::Pathfinding::SMAstar::Examples::PalUtils::collisions_per_length($w, $phrase_obj->{_phrase});
	    my $sparsity = AI::Pathfinding::SMAstar::Examples::PalUtils::get_word_sparsity_memo($w);
	    my $num_chars = AI::Pathfinding::SMAstar::Examples::PalUtils::num_chars_in_word_memo($w);
	    my ($word_intersect, @differences) = AI::Pathfinding::SMAstar::Examples::PalUtils::word_collision($w, 
									  \@sorted_letters_seen);
	    my $num_new_chars = $num_chars - $word_intersect;	
	    #my $newcost = $collisions_per_length + $sparsity;	
	    my $newcost = $collisions_per_length + $len_w;
	    my $new_cost_so_far = $cost + $cost_so_far;

	    #---------------------------------------------------------------------------
	    my $new_phrase = AI::Pathfinding::SMAstar::Examples::Phrase->new(		
		_word_list => $words,
		#_words_w_cands_list  => \@words_to_make_phrases,
		_words_w_cands_list  => $words_w_cands,
		_dictionary => $dictionary,
		_dictionary_rev => $dictionary_rev,		   
		_start_word => $w,
		_cand => $stored_c,	
		_word => $w,
		_predecessor => $phrase_obj,	
		_dir => 0,
		_repeated_pal_hash_ref => $repeated_pal_hash_ref,
		_letters_seen => \@sorted_letters_seen,
		_cost => $newcost,
		_cost_so_far => $new_cost_so_far,
		_num_chars_so_far => $new_num_chars_so_far,
		_num_new_chars => $num_new_chars,
		_no_match_remainder => 1,
		_depth => $depth+1,
		);	
	    
	    #print "returning new phrase from first cond.\n";
	    $new_phrase->{_phrase} = $new_phrase->roll_up_phrase();
	    return $new_phrase;	  
			    	   
	}
	else{	

	    my $c  = shift(@cands);	
	    if(!$c){
		return;
	    }
	    
	    # ------------- filter for repeated palcands for a particular word------
	    # ----------------------------------------------------------------------
	    # This will avoid many repeated patterns among palindromes to trim down the
	    # number redundant palindromes generated.
	    # 		
	    my $letters_seen_str = join("", @{$phrase_obj->{_letters_seen}});
	    if($letters_seen_str){
		my $repeated_pal_hash_key;
		$repeated_pal_hash_key = $word . "^" . $c . "^" . $letters_seen_str;	
		
		#print "\n\nrepeated_pal_hash_key: $repeated_pal_hash_key\n";
		if(my $hash_val = $repeated_pal_hash_ref->{$repeated_pal_hash_key}){
		    # skip because '$word' <--> '$p' pattern has already appeared in a previous palindrome.
		    if($hash_val != $depth){
			goto LABEL1;
			# next; # skip  
		    }
		}
		else{
		    #flag this candidate as already having been tested (below).
		    $repeated_pal_hash_ref->{$repeated_pal_hash_key} = $depth;
		}	
	    }
	    #--------------------------------------------------------------------------
	    #--------------------------------------------------------------------------
	    
	    my $len_c = length($c);
	    my $rev_c = reverse($c);	
	    my $word_remainder;
	    
	    if($len_c < $len_word){
		$word_remainder = $c;
	    }
	    elsif($len_c > $len_word){	
		$word_remainder = $word;
	    }
	    my $rev_word_remainder = reverse($word);
	    
	    my $num_cands = @cands;
	    
	    my $match_remainder;
	    my $len_match_remainder;
	    my $newcost;
	    my $new_cost_so_far;
	    my $num_new_chars;
	    my $new_direction;
	    
	    if($direction == 0){	 	   
		if($len_c < $len_word){		
		    $match_remainder = AI::Pathfinding::SMAstar::Examples::PalUtils::match_remainder($word, $rev_c);		
		    $new_direction = 0;
		}
		elsif($len_c > $len_word){	
		    $match_remainder = AI::Pathfinding::SMAstar::Examples::PalUtils::match_remainder($rev_c, $word);
		    $match_remainder = reverse($match_remainder);		
		    $new_direction = 1;
		}
	    }
	    elsif($direction == 1){
		if($len_c < $len_word){
		    $match_remainder = AI::Pathfinding::SMAstar::Examples::PalUtils::match_remainder($rev_word, $c);
		    $match_remainder = reverse($match_remainder);		
		    $new_direction = 1;	
		}
		elsif($len_c > $len_word){
		    $match_remainder = AI::Pathfinding::SMAstar::Examples::PalUtils::match_remainder($c, $rev_word);		
		    $new_direction = 0;
		}
	    }
	    
	    $len_match_remainder = defined($match_remainder) ? length($match_remainder) : 0;
	    
	    #----------------Compute the Cost-------------------------------------------
	    if($len_c < $len_word){	   		
		$num_new_chars = 0;
		$newcost = 0;  # new candidate is a (reversed) substring of word
		$new_cost_so_far = $cost + $cost_so_far;			    
	    }
	    elsif($len_c > $len_word){
		
		#if($len_c != $len_word){
		my $collisions_per_length = AI::Pathfinding::SMAstar::Examples::PalUtils::collisions_per_length($match_remainder, $phrase_obj->{_phrase});
		my $num_chars = AI::Pathfinding::SMAstar::Examples::PalUtils::num_chars_in_word_memo($match_remainder);
		my $sparsity = AI::Pathfinding::SMAstar::Examples::PalUtils::get_word_sparsity_memo($match_remainder);
		my ($word_intersect, @differences) = AI::Pathfinding::SMAstar::Examples::PalUtils::word_collision_memo($match_remainder, 
										       \@sorted_letters_seen);	    
		$num_new_chars = $num_chars - $word_intersect;		
		#$newcost = $sparsity + $collisions_per_length;
		$newcost = $collisions_per_length + $len_match_remainder;
		$new_cost_so_far = $cost + $cost_so_far;			    
	    }
	    #---------------------------------------------------------------------------
	    
	    if($match_remainder){  # there is a length difference between the candidate and this word.
		my $new_phrase = AI::Pathfinding::SMAstar::Examples::Phrase->new(
		    _word_list => $words,
		    _words_w_cands_list  => $words_w_cands,
		    _dictionary => $dictionary,
		    _dictionary_rev => $dictionary_rev,
		    _start_word => $match_remainder,
		    _cand => $c,
		    _word => $whole_word,
		    _predecessor => $phrase_obj,	
		    _dir => $new_direction,
		    _repeated_pal_hash_ref => $repeated_pal_hash_ref,
		    _letters_seen => \@sorted_letters_seen,
		    _cost => $newcost,
		    _cost_so_far => $new_cost_so_far,
		    _num_chars_so_far => $new_num_chars_so_far,		
		    _num_new_chars => $num_new_chars,
		    _depth => $depth+1,
		    );
		#print "returning new phrase from second cond.\n";
		$new_phrase->{_phrase} = $new_phrase->roll_up_phrase();
		return $new_phrase;
	    }
	    else{
		#
		# There is no match_remainder, so this candidate is the reverse
		# of $word, or the phrase built so far is an "even" palindrome,
		# i.e. it has a word boundary (space) in the middle.
		#
		#
		# This is a special case since there is no match remainder.
		# Because there is no remainder to create new phrase obj from, this 
		# section goes through the whole word list and creates phrase objects
		# for each new word.   The number of new characters offered by each
		# word is recorded to help with guided search.
		#
		# Update:  this case now only goes through the word list for which there
		# are cands.
		
		@words_to_make_phrases = @$words_w_cands;
		#@words_to_make_phrases = @$words;
		
		
		$stored_c = $c;
		my $next_word = shift(@words_to_make_phrases);
		my $w = $next_word;
		
		my $repeated_word_p = 0;
		my $antecedent = $phrase_obj->{_predecessor};
		my $antecedent_dir = $antecedent->{_dir};
		while($antecedent){
		    
		    if(defined($antecedent->{_word}) && $w eq $antecedent->{_word} && $antecedent_dir == 0){
			$repeated_word_p = 1;
			last;
		    }
		    $antecedent = $antecedent->{_predecessor};	
		    if($antecedent){
			$antecedent_dir = $antecedent->{_dir};
		    }
		}
		
		if($repeated_word_p || $w eq $word){	
		    goto LABEL1;
		    #next;  #skip this word, we are already looking at it
		}
		
		#----------------Compute the Cost-------------------------------------------
		my $len_w = length($w);
		my $collisions_per_length = AI::Pathfinding::SMAstar::Examples::PalUtils::collisions_per_length($w, $phrase_obj->{_phrase});
		my $sparsity = AI::Pathfinding::SMAstar::Examples::PalUtils::get_word_sparsity_memo($w);
		my $num_chars = AI::Pathfinding::SMAstar::Examples::PalUtils::num_chars_in_word_memo($w);
		my ($word_intersect, @differences) = AI::Pathfinding::SMAstar::Examples::PalUtils::word_collision_memo($w, 
										   \@sorted_letters_seen);
		my $num_new_chars = $num_chars - $word_intersect;	
		#my $newcost = $collisions_per_length + $sparsity;
		my $newcost = $collisions_per_length + $len_w;
		my $new_cost_so_far = $cost + $cost_so_far;
		
		#---------------------------------------------------------------------------
		my $new_phrase = AI::Pathfinding::SMAstar::Examples::Phrase->new(
		    _word_list => $words,
		    _words_w_cands_list  => $words_w_cands,
		    _dictionary => $dictionary,
		    _dictionary_rev => $dictionary_rev,		   
		    _start_word => $w,
		    _cand => $c,	
		    _word => $w,
		    _predecessor => $phrase_obj,
	
		    _dir => 0,
		    _repeated_pal_hash_ref => $repeated_pal_hash_ref,
		    _letters_seen => \@sorted_letters_seen,
		    _cost => $newcost,
		    _cost_so_far => $new_cost_so_far,
		    _num_chars_so_far => $new_num_chars_so_far,
		    _num_new_chars => $num_new_chars,
		    _no_match_remainder => 1,
		    _depth => $depth+1,
		    );	
		
		#print "returning new phrase from third cond.\n";
		$new_phrase->{_phrase} = $new_phrase->roll_up_phrase();
		return $new_phrase;	  
		
	    }		
	}	
    }
}




#-----------------------------------------------------------------------------
# Return the number of successors of this phrase
#-----------------------------------------------------------------------------
sub get_num_successors
{
    my ($self) = @_;
    
    my $num_successors = 0;
    my $iterator = $self->get_descendants_num_iterator();

    while(my $next_descendant = $iterator->()){
	$num_successors++;
    }

    return $num_successors
}





#-----------------------------------------------------------------------------
# Get descendants number function.
#
# 
#
#-----------------------------------------------------------------------------
sub get_descendants_number
{
    my ($phrase_obj) = @_;
    if(!$phrase_obj){
	return;
    }
	
    my $words = $phrase_obj->{_word_list};
    my $words_w_cands = $phrase_obj->{_words_w_cands_list};
    my $dictionary = $phrase_obj->{_dictionary};
    my $dictionary_rev = $phrase_obj->{_dictionary_rev};
    my $repeated_pal_hash_ref = $phrase_obj->{_repeated_pal_hash_ref};
    my $letters_seen = $phrase_obj->{_letters_seen};
    my $cost = $phrase_obj->{_cost};
    my $cost_so_far = $phrase_obj->{_cost_so_far};
    my $num_chars_so_far = $phrase_obj->{_num_chars_so_far};
    my $no_match_remainder = $phrase_obj->{_no_match_remainder};
    my $depth = $phrase_obj->{_depth};
    
    my $direction = $phrase_obj->{_dir};
    my $word = $phrase_obj->{_start_word};
    my $whole_word = $phrase_obj->{_cand};    
    my $len_word = length($word);
    my @cands;
    my @descendants;

   
    if($direction == 0){
	@cands = AI::Pathfinding::SMAstar::Examples::PalUtils::get_cands_from_left($word, $dictionary, $dictionary_rev);
    }
    elsif($direction == 1){
	@cands = AI::Pathfinding::SMAstar::Examples::PalUtils::get_cands_from_right($word, $dictionary, $dictionary_rev);
    }
        
  
    my @words_to_make_phrases;
    my $stored_c;

    my $num_successors = 0;

    while(1){
	# this is a continuation of the second case below, where there were no 
	# match-remainders for the phrase-so-far, i.e. the palindrome has a space
	# in the middle with mirrored phrases on each side. 'cat tac' for example.
	my $next_word = shift(@words_to_make_phrases);
	if($next_word){
	    
	    my $w = $next_word;

	    my $repeated_word_p = 0;
	    my $antecedent = $phrase_obj->{_predecessor};
	    my $antecedent_dir = $antecedent->{_dir};
	    while($antecedent){

		if(defined($antecedent->{_word}) && $w eq $antecedent->{_word} && $antecedent_dir == 0){
		    $repeated_word_p = 1;
		    last;
		}
		$antecedent = $antecedent->{_predecessor};	
		if($antecedent){
		    $antecedent_dir = $antecedent->{_dir};
		}
	    }

	    if($repeated_word_p || $w eq $word){		
		next;  #skip this word, we are already looking at it
	    }
	    $num_successors++;	  
			    	   
	}
	else{	
	    my $c  = shift(@cands);	
	    if(!$c){
		return $num_successors;
	    }
	    
	    # ------------- filter for repeated palcands for a particular word------
	    # ----------------------------------------------------------------------
	    # This will avoid many repeated patterns among palindromes to trim down the
	    # number redundant palindromes generated.
	    # 		
	    my $letters_seen_str = join("", @{$phrase_obj->{_letters_seen}});
	    if($letters_seen_str){
		my $repeated_pal_hash_key;
		$repeated_pal_hash_key = $word . "^" . $c . "^" . $letters_seen_str;	
		
		#print "\n\nrepeated_pal_hash_key: $repeated_pal_hash_key\n";
		if(my $hash_val = $repeated_pal_hash_ref->{$repeated_pal_hash_key}){
		    # skip because '$word' <--> '$p' pattern has already appeared in a previous palindrome.
		    if($hash_val != $depth){
			next;  #skip
		    }
		}
		else{
		    #flag this candidate as already having been tested (below).
		    $repeated_pal_hash_ref->{$repeated_pal_hash_key} = $depth;
		}	
	    }
	    #--------------------------------------------------------------------------
	    #--------------------------------------------------------------------------
	    
	    my $len_c = length($c);
	    my $rev_c = reverse($c);	
	    my $word_remainder;
	    
	    if($len_c < $len_word){
		$word_remainder = $c;
	    }
	    elsif($len_c > $len_word){	
		$word_remainder = $word;
	    }
	    my $rev_word_remainder = reverse($word);
	    
	    my $num_cands = @cands;
	    
	    my $match_remainder;
	    my $len_match_remainder;
	    
	    
	    
	    if($len_c != $len_word){		
		$match_remainder = 1;				       
	    }
	    
	    
	    if($match_remainder){  # there is a length difference between the candidate and this word.		    
		$num_successors++;
	    }
	    else{
		#
		# There is no match_remainder, so this candidate is the reverse
		# of $word, or the phrase built so far is an "even" palindrome,
		# i.e. it has a word boundary (space) in the middle.
		#
		#
		# This is a special case since there is no match remainder.
		# Because there is no remainder to create new phrase obj from, this 
		# section goes through the whole word list and creates phrase objects
		# for each new word.   The number of new characters offered by each
		# word is recorded to help with guided search.
		#
		# Update:  this case now only goes through the word list for which there
		# are cands.
		
		@words_to_make_phrases = @$words_w_cands;
		#@words_to_make_phrases = @$words;
		
		
		$stored_c = $c;
		my $next_word = shift(@words_to_make_phrases);
		my $w = $next_word;
		
		my $repeated_word_p = 0;
		my $antecedent = $phrase_obj->{_predecessor};
		my $antecedent_dir = $antecedent->{_dir};
		while($antecedent){
		    
		    if(defined($antecedent->{_word}) && $w eq $antecedent->{_word} && $antecedent_dir == 0){
			$repeated_word_p = 1;
			last;
		    }
		    $antecedent = $antecedent->{_predecessor};	
		    if($antecedent){
			$antecedent_dir = $antecedent->{_dir};
		    }
		}
		
		if($repeated_word_p || $w eq $word){
		    next; #skip this word, we are already looking at it
		}
		$num_successors++;	  		
	    }		
	}	
    }

    return $num_successors;

}



#-----------------------------------------------------------------------------
# Get descendants iterator function.
# Generate the next descendant of a phrase object. Each descendant adds
# another word to the phrase that could possibly lead to a palindrome
#
#-----------------------------------------------------------------------------
sub get_descendants_num_iterator
{
    my ($phrase_obj) = @_;
    if(!$phrase_obj){
	return;
    }
	
    my $words = $phrase_obj->{_word_list};
    my $words_w_cands = $phrase_obj->{_words_w_cands_list};
    my $dictionary = $phrase_obj->{_dictionary};
    my $dictionary_rev = $phrase_obj->{_dictionary_rev};
    my $repeated_pal_hash_ref = $phrase_obj->{_repeated_pal_hash_ref};
    my $letters_seen = $phrase_obj->{_letters_seen};
    my $cost = $phrase_obj->{_cost};
    my $cost_so_far = $phrase_obj->{_cost_so_far};
    my $num_chars_so_far = $phrase_obj->{_num_chars_so_far};
    my $no_match_remainder = $phrase_obj->{_no_match_remainder};
    my $depth = $phrase_obj->{_depth};
    
    my $direction = $phrase_obj->{_dir};
    my $word = $phrase_obj->{_start_word};
    my $whole_word = $phrase_obj->{_cand};    
    my $len_word = length($word);
    my @cands;
    my @descendants;

   
    if($direction == 0){
	@cands = AI::Pathfinding::SMAstar::Examples::PalUtils::get_cands_from_left($word, $dictionary, $dictionary_rev);
    }
    elsif($direction == 1){
	@cands = AI::Pathfinding::SMAstar::Examples::PalUtils::get_cands_from_right($word, $dictionary, $dictionary_rev);
    }
        
  
    my @words_to_make_phrases;
    my $stored_c;

    return sub{	       

      LABEL:
	# this is a continuation of the second case below, where there were no 
	# match-remainders for the phrase-so-far, i.e. the palindrome has a space
	# in the middle with mirrored phrases on each side. 'cat tac' for example.
	my $next_word = shift(@words_to_make_phrases);
	if($next_word){
	    
	    my $w = $next_word;

	    my $repeated_word_p = 0;
	    my $antecedent = $phrase_obj->{_predecessor};
	    my $antecedent_dir = $antecedent->{_dir};
	    while($antecedent){

		if(defined($antecedent->{_word}) && $w eq $antecedent->{_word} && $antecedent_dir == 0){
		    $repeated_word_p = 1;
		    last;
		}
		$antecedent = $antecedent->{_predecessor};	
		if($antecedent){
		    $antecedent_dir = $antecedent->{_dir};
		}
	    }

	    if($repeated_word_p || $w eq $word){
		goto LABEL;
		#next;  #skip this word, we are already looking at it
	    }
	    return 1;	  
			    	   
	}
	else{	
	    my $c  = shift(@cands);	
	    if(!$c){
		return;
	    }
	    
	    # ------------- filter for repeated palcands for a particular word------
	    # ----------------------------------------------------------------------
	    # This will avoid many repeated patterns among palindromes to trim down the
	    # number redundant palindromes generated.
	    # 		
	    my $letters_seen_str = join("", @{$phrase_obj->{_letters_seen}});
	    if($letters_seen_str){
		my $repeated_pal_hash_key;
		$repeated_pal_hash_key = $word . "^" . $c . "^" . $letters_seen_str;	
		
		#print "\n\nrepeated_pal_hash_key: $repeated_pal_hash_key\n";
		if(my $hash_val = $repeated_pal_hash_ref->{$repeated_pal_hash_key}){
		    # skip because '$word' <--> '$p' pattern has already appeared in a previous palindrome.
		    if($hash_val != $depth){
			goto LABEL;
			# next;  #skip
		    }
		}
		else{
		    #flag this candidate as already having been tested (below).
		    $repeated_pal_hash_ref->{$repeated_pal_hash_key} = $depth;
		}	
	    }
	    #--------------------------------------------------------------------------
	    #--------------------------------------------------------------------------
	    
	    my $len_c = length($c);
	    my $rev_c = reverse($c);	
	    my $word_remainder;
	    
	    if($len_c < $len_word){
		$word_remainder = $c;
	    }
	    elsif($len_c > $len_word){	
		$word_remainder = $word;
	    }
	    my $rev_word_remainder = reverse($word);
	    
	    my $num_cands = @cands;
	    
	    my $match_remainder;
	    my $len_match_remainder;
	    
	    
	    
	    if($len_c != $len_word){		
		$match_remainder = 1;				       
	    }
	    
	    
	    if($match_remainder){  # there is a length difference between the candidate and this word.		    
		return 1;
	    }
	    else{
		#
		# There is no match_remainder, so this candidate is the reverse
		# of $word, or the phrase built so far is an "even" palindrome,
		# i.e. it has a word boundary (space) in the middle.
		#
		#
		# This is a special case since there is no match remainder.
		# Because there is no remainder to create new phrase obj from, this 
		# section goes through the whole word list and creates phrase objects
		# for each new word.   The number of new characters offered by each
		# word is recorded to help with guided search.
		#
		# Update:  this case now only goes through the word list for which there
		# are cands.
		
		@words_to_make_phrases = @$words_w_cands;
		#@words_to_make_phrases = @$words;
		
		
		$stored_c = $c;
		my $next_word = shift(@words_to_make_phrases);
		my $w = $next_word;
		
		my $repeated_word_p = 0;
		my $antecedent = $phrase_obj->{_predecessor};
		my $antecedent_dir = $antecedent->{_dir};
		while($antecedent){
		    
		    if(defined($antecedent->{_word}) && $w eq $antecedent->{_word} && $antecedent_dir == 0){
			$repeated_word_p = 1;
			last;
		    }
		    $antecedent = $antecedent->{_predecessor};	
		    if($antecedent){
			$antecedent_dir = $antecedent->{_dir};
		    }
		}
		
		if($repeated_word_p || $w eq $word){
		    goto LABEL;
		    #next; #skip this word, we are already looking at it
		}
		return 1;	  		
	    }		
	}	
    }
}









lib/AI/Pathfinding/SMAstar/Examples/Phrase.pm  view on Meta::CPAN





#-----------------------------------------------------------------------------
# traverse from candidate phrase-object back up to start word, building up the 
# phrase string. iterative version.
#-----------------------------------------------------------------------------
sub roll_up_phrase
{
    my ($pobj, $phrase, $depth) = @_;  # depth == depth of recursion

    if(!$depth){
	$depth = 0;
    }
    
    while($pobj){
	if(!$pobj->{_cand} && $depth == 0){ 
	    # top-level call to roll_up_phrase is called on a root node.
	    return $pobj->{_start_word};
	}
	else{
	    # if depth is 0, that means this is a top-level call.
	    # otherwise this is the nth iteration within this while loop.


	    # if this is a top-level call and _phrase is already defined,
	    # just return _phrase.
	    if(defined($pobj->{_phrase}) && !$depth){  
		return $pobj->{_phrase};		    
	    }
	    
	    my $direction = $pobj->{_dir};
	    my $antecedent = $pobj->{_predecessor};
	    my $antecedent_predecessor;
	    my $no_match_remainder = $pobj->{_no_match_remainder};	   	    
	    my $ant_direction = 0;
	    my $ant_cand;
	   
	    if($antecedent){
		$antecedent_predecessor = $antecedent->{_predecessor};
		$ant_direction = $antecedent->{_dir};
		$ant_cand = $antecedent->{_cand};
	    }
	    
	    

	    my $word = defined($pobj->{_word}) ? $pobj->{_word} : "";
	    my $startword = defined($pobj->{_start_word}) ? $pobj->{_start_word} : "";	
	    my $cand = defined($pobj->{_cand}) ? $pobj->{_cand} : "";
	    
	    if(!$phrase){
		if($direction == 0){	
		    $phrase = $cand;		    
		}
		elsif($direction == 1){		
		    $phrase = $cand;		
		}
	    }
	    else{	    
		if($direction == 0){
		    if($ant_direction == 0){
			#**** special case for root node descendant***
			if(!$antecedent_predecessor){ # antecedent is root node.  
			    if($word){
				$phrase =  $word . " " . $phrase . " " . $cand;
			    }
			    else{
				$phrase = $phrase . " " . $cand;
			    }		    
			}		    
			else{			
			    if($no_match_remainder){  # handle the case where there was no match remainder
				$phrase = $word . " " . $phrase . " " . $cand;
			    }
			    else{
				$phrase = "" . $phrase . " " . $cand;		
			    }			
			}		    
		    }
		    elsif($ant_direction == 1){
			if($no_match_remainder){  # handle the case where there was no match remainder
			    $phrase = $cand . " " . $word . " " . $phrase . "";
			}
			else{
			    $phrase = $cand . " " . $phrase . "";	
			}
		    }
		}
		elsif($direction == 1){
		    if($ant_direction == 0){		    
			$phrase = "" . $phrase . " " . $cand;
			
		    }
		    elsif($ant_direction == 1){
			$phrase = $cand . " " . $phrase . "";
		    }
		}
	    }
	}
	
	$pobj = $pobj->{_predecessor};
	$depth++;
	
    }  # end while($pobj);
    
    return $phrase;
}




sub roll_up_phrase_plus_word
{
    my ($self) = @_;

    my $phrase = $self->{_phrase};
    my $word = $self->{_start_word};
    my $phrase_plus_cand = $phrase . ": " . $word;

    return $phrase_plus_cand;
}




sub DESTROY
{
    my ($self) = @_;

    my $antecedent;
    my $ant_phrase;

    my ($pkg, $filename, $line_num) = caller(); 

    if($self->{_predecessor}){
	$antecedent = $self->{_predecessor};
	$ant_phrase = $antecedent->{_phrase} ? $antecedent->{_phrase} : $antecedent->{_start_word};
    }
    else{	
	$antecedent->{_phrase} = "none";
    }

#    print "     $line_num, destroying phrase object $self, '" . $self->{_start_word} . ", " . $self->{_phrase} .
#	"', parent is $antecedent: '" .  $ant_phrase . "' \n";
    
#    if($line_num != 0){ # if not final sweep at program exit
#	print "        caller is: $pkg, $filename, $line_num\n";	
#    }
    
    if($line_num == 0){ # line_num is zero
	$d++;
#	print "\$d : $d\n";
    }
    
    #${$self->{_predecessor}} = 0;
    #${$self->{_descendants_list}} = 0;

    delete $self->{_predecessor};
    
   
}









lib/AI/Pathfinding/SMAstar/Examples/WordObj.pm  view on Meta::CPAN

package AI::Pathfinding::SMAstar::Examples::WordObj;
use strict;

##################################################
## the object constructor (simplistic version)  ##
##################################################
sub new {
    my $invocant = shift;
    my $class   = ref($invocant) || $invocant;
    my $self = {
        _word  => undef,      
        @_,                 # Override previous attributes
    };
    return bless $self, $class;
}

##############################################
## methods to access per-object data        ##
##                                          ##
## With args, they set the value.  Without  ##
## any, they only retrieve it/them.         ##
##############################################
sub word {
    my $self = shift;
    if (@_) { $self->{_word} = shift }
    return $self->{_word};
}



# compare
#
# usage:  $word_obj->compare($other_word_obj)
#
# Accepts another WordObj object as an argument.
# Returns 1 if greater than argument, 0 if equal, and -1 if 
# less than argument
sub compare{
    my ($self,$arg_wordobj) = @_;
    
    my $arg_word = $arg_wordobj->{_word};
    my $word = $self->{_word};
    
    if($arg_word gt $word){
	return -1;
    }
    elsif($arg_word eq $word){
	return 0;
    }
    return 1;	    
}


# compare_up_to
#
# usage:  $word_obj->compare_up_to($other_word_obj)
#
# Accepts another WordObj object as an argument.
# Returns 1 if greater than argument, 0 if $other_word_obj 
# is a substring of $word_obj
# that appears at the beginning of $word_obj 
# and -1 if less than argument $other_word_obj
sub compare_up_to{
    my $self = shift;
    if (@_){
	my $arg_wordobj = shift;
	my $arg_word = $arg_wordobj->{_word};
	my $word = $self->{_word};
       	
	# perl's index function works like: index($string, $substr);
	if(index($word, $arg_word) == 0){
	    return(0);
	}
	elsif($arg_word gt $word){
	    return(-1);
	}       
	elsif($arg_word lt $word){
	    return(1);
	}	
    }    
}


# compare_up_to
#
# usage:  $word_obj->compare_down_to($other_word_obj)
#
# Returns 0 if $word_obj is a substring of 
# $other_word_obj, that appears at the beginning
# of $other_word_obj.
#
sub compare_down_to{
    my $self = shift;
    if (@_){
	my $arg_wordobj = shift;
	my $arg_word = $arg_wordobj->{_word};
	my $word = $self->{_word};
	
	# perl's index function works like: index($string, $substr);
	if(index($arg_word, $word) == 0){
	    return(0);
	}
	elsif($arg_word gt $word){
	    return(-1);
	}       
	elsif($arg_word lt $word){
	    return(1);
	}	
    }    
}






1;  # so the require or use succeeds

lib/AI/Pathfinding/SMAstar/PairObj.pm  view on Meta::CPAN

package AI::Pathfinding::SMAstar::PairObj;
use strict;

##################################################
# PairObj constructor
##################################################
sub new {
    my $invocant = shift;
    my $class   = ref($invocant) || $invocant;
    my $self = {
	_key    => undef,
        _value  => undef,      
        @_,  # Override previous attributes
    };
    return bless $self, $class;
}

##############################################
# accessors
##############################################
sub value {
    my $self = shift;
    if (@_) { $self->{_value} = shift }
    return $self->{_value};
}

sub val {
    my $self = shift;
    if (@_) { $self->{_value} = shift }
    return $self->{_value};
}

sub key {
    my $self = shift;
    if (@_) { $self->{_key} = shift }
    return $self->{_key};
}




# compare_vals
#
# usage:  $pair_obj->compare($other_pair_obj)
#
# Accepts another PairObj object as an argument.
# Returns 1 if greater than argument, 0 if equal, and -1 if 
# less than argument
sub compare_vals{
    my ($self,$arg_obj) = @_;
    
    my $arg_value = $arg_obj->{_value};
    my $value = $self->{_value};
    
    if($arg_value gt $value){
	return -1;
    }
    elsif($arg_value eq $value){
	return 0;
    }
    return 1;	    
}


# compare_keys
#
# usage:  $pair_obj->compare($other_pair_obj)
#
# Accepts another PairObj object as an argument.
# Returns 1 if greater than argument, 0 if equal, and -1 if 
# less than argument
sub compare_keys{
    my ($self,$arg_obj) = @_;
    
    my $arg_key = $arg_obj->{_key};
    my $key = $self->{_key};
    
    if($arg_key gt $key){
	return -1;
    }
    elsif($arg_key eq $key){
	return 0;
    }
    return 1;	    
}


sub compare_keys_numeric{
    my ($self,$arg_obj) = @_;
    
    my $arg_key = $arg_obj->{_key};
    my $key = $self->{_key};
    
    if($arg_key > $key){
	return -1;
    }
    elsif($self->fp_equal($arg_key, $key, 10)){
	return 0;
    }
    return 1;	    
}




sub fp_equal {
    my ($self, $A, $B, $dp) = @_;

    return sprintf("%.${dp}g", $A) eq sprintf("%.${dp}g", $B);
}




1;  # so the require or use succeeds

lib/AI/Pathfinding/SMAstar/Path.pm  view on Meta::CPAN

# Author:  matthias beebe
# Date :  June 2008
#
#

package AI::Pathfinding::SMAstar::Path;

use strict;

BEGIN {
    use Exporter ();
    @Path::ISA         = qw(Exporter);
    @Path::EXPORT      = qw();
    @Path::EXPORT_OK   = qw($d);

  }

use vars qw($d $max_forgotten_nodes);  # used to debug destroy method for accounting purposes
$d = 0;
$max_forgotten_nodes = 0;


##################################################
# Path constructor 
##################################################
sub new {
    my $invocant = shift;
    my $class   = ref($invocant) || $invocant;
    my $self = {
	
	_state                    => undef,  # node in the search space
	_eval_func               => undef,
	_goal_p_func             => undef,
	_num_successors_func     => undef,
	_successors_iterator     => undef,
	_get_data_func           => undef,

	###########################################
	#
	#   path stuff
	#
	###########################################	
	_antecedent              => undef,  # pointer to the antecedent of this obj
	_f_cost                  => undef,  # g + h where g = cost so far, h = estimated cost to goal.

	_forgotten_node_fcosts   => [],     # array to store fcosts of forgotten nodes
	_forgotten_nodes_num     => 0,

	_forgotten_nodes_offsets => {},

	_depth                   => 0,     # depth used for memory-bounded search
	_descendants_produced    => [],
	_descendant_index        => undef,	
	_descendant_fcosts       => [],
	_descendants_on_queue    => 0,

	_descendands_deleted     => 0,
	_is_completed            => 0,
	_num_successors          => undef,
	_num_successors_in_mem   => 0,
	_is_on_queue             => 0,
	_iterator_index          => 0,      # to remember index of iterator for descendants
	_need_fcost_change       => 0,      # boolean

	@_,    # attribute override
    };

    return bless $self, $class;
        
}

##############################################
# accessors
##############################################

sub state{
    my $self = shift;
    if (@_) { $self->{_state} = shift }
    return $self->{_state};
}

sub antecedent{
    my $self = shift;
    if (@_) { $self->{_antecedent} = shift }
    return $self->{_antecedent};
}

sub f_cost{
    my $self = shift;
    if (@_) { $self->{_f_cost} = shift }
    return  $self->{_f_cost};
}

sub depth{
    my $self = shift;
    if (@_) { $self->{_depth} = shift }
    return  $self->{_depth};
}

sub is_completed{
    my $self = shift;
    if (@_) { $self->{_is_completed} = shift }
    return  $self->{_is_completed};
}

sub is_on_queue{
    my $self = shift;
    if (@_) { $self->{_is_on_queue} = shift }
    return  $self->{_is_on_queue};
}

sub descendants_deleted{
    my $self = shift;
    if (@_) { $self->{_descendants_deleted} = shift }
    return  $self->{_descendants_deleted};
}

sub need_fval_change{
    my $self = shift;
    if (@_) { $self->{_need_fcost_change} = shift }
    return  $self->{_need_fcost_change};
}




# new version 8
sub remember_forgotten_nodes_fcost
{
    my ($self, $node) = @_;      

    my $fcost = $node->{_f_cost};
    my $index = $node->{_descendant_index};

    $self->{_forgotten_node_fcosts}->[$index] = $fcost;
    
    return;
}






#----------------------------------------------------------------------------
# evaluation function f(n) = g(n) + h(n) where 
#
# g(n) = cost of path through this node
# h(n) = distance from this node to goal (optimistic)
#
# used for A* search.
#
sub fcost
{    
    my ($self) = @_;
    
    my $fcost = $self->{_f_cost};
    if(defined($fcost)){	    
	return $fcost;
    }

    my $eval_func = $self->{_eval_func};
    my $result =  $eval_func->($self->{_state});
    $self->{_f_cost} = $result;

    return $result;
}





sub is_goal
{
    my ($self) = @_;
      
    my $goal_p_func = $self->{_goal_p_func};
    my $result =  $goal_p_func->($self->{_state});

    return $result;
}



sub get_num_successors
{
    my ($self) = @_;
      
    my $num_successors_func = $self->{_num_successors_func};
    my $result =  $num_successors_func->($self->{_state});

    return $result;    
}


sub get_successors_iterator
{
    my ($self) = @_;
      
    my $successors_iterator = $self->{_successors_iterator};

    my $iterator = $successors_iterator->($self->{_state});
    
    return $iterator;    
}


    
    

#-----------------------------------------------------------------------------------------------
#
# Check whether we need to backup the fvals for a node when it is completed (recursive)
# Sets flags throughout path object's lineage, indicating whether fvals need to be updated.
#
#-----------------------------------------------------------------------------------------------
sub check_need_fval_change
{
    my ($self, $descendant_fcost, $descendant_ind) = @_;
 

    my $descendant_index = $self->{_descendant_index};

    if(!$self->is_completed()){
        # node not completed. no need to update fcost.
	$self->need_fval_change(0);
	return;
    }

    my $fcost = $self->{_f_cost};
    my $least_fcost2 = 99;
       
    
    my $min = sub {	
	my ($n1, $n2) = @_;
	return ($n1 < $n2 ? $n1 : $n2);
    };

    if($self->{_forgotten_nodes_num} != 0){ 
	foreach my $ind (keys %{$self->{_forgotten_nodes_offsets}}){	  
	    my $cost = $self->{_forgotten_node_fcosts}->[$ind];	    
	    if($cost != -1 && $cost < $least_fcost2){
		$least_fcost2 = $cost;
	    }		    
	}
    }    
   
    my $j = 0;
    foreach my $fc (@{$self->{_descendant_fcosts}}){
	if(defined($descendant_ind) && $j != $descendant_ind){
	    if($fc != -1 && $fc < $least_fcost2){
		$least_fcost2 = $fc;
	    }
	}
	else{
	    # special case for index $j:  it is the caller's index.
	    if(defined($descendant_fcost)){	
		if($descendant_fcost < $least_fcost2) {
		    $least_fcost2 = $descendant_fcost;
		}
	    }
	    elsif($fc != -1 && $fc < $least_fcost2){
		$least_fcost2 = $fc;
	    }
	}	
	$j++;	
    }
    
    # if no successors, this node cannot lead to 
    # goal, so set fcost to infinity.
    if($self->{_num_successors} == 0){ 
	$least_fcost2 = 99;
    }
  
    if($least_fcost2 != $fcost){		
        # setting need_fcost_change to 1
	$self->need_fval_change(1);
	my $antecedent = $self->{_antecedent};
	
	# recurse on the antecedent
	if($antecedent){
	    $antecedent->check_need_fval_change($least_fcost2, $descendant_index);
	}	
    }
}





#-----------------------------------------------------------------------------------------------
#
# Backup the fvals for a node when it is completed.
#
#-----------------------------------------------------------------------------------------------
sub backup_fvals
{
    my ($self) = @_;
    
    while($self){
	
	if(!$self->is_completed()){
            # node not completed, return
	    return;
	}
	
	my $fcost = $self->{_f_cost};
	my $least_fcost = 99;

	my $min = sub {	
	    my ($n1, $n2) = @_;
	    return ($n1 < $n2 ? $n1 : $n2);
	};
	
	if($self->{_forgotten_nodes_num} != 0){ 
	    foreach my $ind (keys %{$self->{_forgotten_nodes_offsets}}){	  
		my $cost = $self->{_forgotten_node_fcosts}->[$ind];	    
		if($cost != -1 && $cost < $least_fcost){
		    $least_fcost = $cost;
		}		    
	    }
	}    

	foreach my $fc (@{$self->{_descendant_fcosts}}){
	    if($fc != -1 && $fc < $least_fcost){
		$least_fcost = $fc;
	    }
	}

	# if no successors, this node cannot lead to 
	# goal, so set fcost to infinity.
	if($self->{_num_successors} == 0){ 
	    $least_fcost = 99;
	}
	
	if($least_fcost != $fcost){		
        # changing fcost from $self->{_f_cost} to $least_fcost	    
	    $self->{_f_cost} = $least_fcost;
	    
	    my $antecedent = $self->{_antecedent};
	    if($antecedent){
		my $descendant_index = $self->{_descendant_index};
		$antecedent->{_descendant_fcosts}->[$descendant_index] = $least_fcost;
	    }	    
	}
	else{
            # not changing fcost. current fcost: $self->{_f_cost}, least_fcost: $least_fcost
	    last;
	}
	
	$self = $self->{_antecedent};
        	
    }  #end while
    
    return;
}






#
# return 1 if all descendants of this path are in
# memory, return 0 otherwise.
#
sub all_in_memory
{
    my ($self) = @_;
    my $is_completed = $self->is_completed();
    my $num_successors_in_mem = $self->{_num_successors_in_mem};
    my $num_successors = $self->{_num_successors};

    my $num_forgotten_fcosts = @{$self->{_forgotten_node_fcosts}};

    if($is_completed || $num_successors == 0){		
	if($num_successors == $num_successors_in_mem){
	    return 1;
	}
	return 0;	    
    }    
    return 0;    
}



#
# return 1 if *any* descendants are in memory
#
sub has_descendants_in_memory
{
    my ($self) = @_;

    my $num_descendants_on_queue = $self->{_descendants_on_queue};
  
    if($num_descendants_on_queue){
	return $num_descendants_on_queue;
    }
  
    return;
}



#-----------------------------------------------------------------------------
# Get descendants iterator function, for for SMA* search.  Returns one new
# node at a time.
#
# The SMA* algorithm must handle "forgotten" nodes.
#
# Generate the next descendant of a path object. Each descendant adds
# another node on the path that may lead to the goal.
#
#-----------------------------------------------------------------------------
sub get_descendants_iterator_smastar
{
    my ($self) = @_;
    
    my $depth = $self->{_depth};
    my $iterator;
    my $num_successors = 0;
    my $next_descendant;

    # if we haven't counted the number of successors yet,
    # count and record the number, so we only have to do
    # this once.
    if(!defined($self->{_num_successors})){

	$num_successors = $self->get_num_successors();

	$self->{_num_successors} = $num_successors;	

	$#{$self->{_descendants_produced}}  = $num_successors;
	$#{$self->{_descendant_fcosts}}     = $num_successors;
	$#{$self->{_forgotten_node_fcosts}} = $num_successors;

	for (my $i = 0;  $i <= $num_successors; $i++){
	    $self->{_descendants_produced}->[$i] = 0;
	    $self->{_descendant_fcosts}->[$i] = -1;
	    $self->{_forgotten_node_fcosts}->[$i] = -1;
	}
    }
    else{
	# if number of successors has already been recorded, update 
	# num_successors variable with stored value.
	$num_successors = $self->{_num_successors};	
    }
	
    return sub{	
	my $i = 0;
	
        # entering get_descendants_iterator_smastar() sub	
	$iterator = $self->get_successors_iterator();

	my $descendants_deleted = 0;
	my $descendants_found = 0;
	

	# loop over nodes returned by iterator
	while(my $next_state = $iterator->()){	

	    $next_descendant = AI::Pathfinding::SMAstar::Path->new(
		_state => $next_state,
		_eval_func => $self->{_eval_func},
		_goal_p_func => $self->{_goal_p_func},
		_get_data_func => $self->{_get_data_func},
		_num_successors_func => $self->{_num_successors_func},
		_successors_iterator => $self->{_successors_iterator},
		_antecedent => $self,	
		_depth => $depth + 1,				
		);

    		
	    my $start_word = $next_descendant->{_state}->{_start_word};
	    my $phrase = $next_descendant->{_state}->{_phrase};
	    
	    my $already_produced_p = $self->{_descendants_produced}->[$i] || ($self->{_descendant_fcosts}->[$i] != -1);
	    

	    if($already_produced_p){
		# have already produced this descendant
		$descendants_found++;
                # found descendant in tree\n";		

		if($i == $num_successors - 1 && $descendants_deleted){
		    # !!! resetting iterator index. descendants have been deleted. clearing forgotten_fcosts on next expansion.
		    $iterator = $self->get_successors_iterator();
		    $self->{_iterator_index} = 0;
		    $i = 0;		

                    # setting completed to 1 (true)
		    $self->is_completed(1);	    		    
		    next;
		}
		else{
		    $i++;
		}


		if($descendants_found == $num_successors){
                    # setting completed to 1.
		    $self->is_completed(1);
		}	

		$next_descendant = undef;  # found this one in list, so undef next descendant.
		
	    }
	    else{	    	
		# did not find descendant in descendant's list 

		if($i < $self->{_iterator_index} && $self->{_forgotten_nodes_num} != 0){
                    # did not find descendant in list, but may have already produced this 
		    # descendant since this node was created.
		    $i++;
		    $descendants_deleted++;
		    next;
		}		
                # did not find descendant in list, adding now.

				
		$next_descendant->{_descendant_index} = $i;
		$self->{_descendants_produced}->[$i] = 1;
                # new descendant's index is $i

		
		$self->{_iterator_index} = $i + 1;
		
		if($self->{_iterator_index} == $self->{_num_successors}){
		    $iterator = $self->get_successors_iterator();
		    $self->{_iterator_index} = 0;
		    $i = 0;
		    	

		    # node is completed, setting completed to 1\n";
		    $self->is_completed(1);
		}
		
		# break out of while() loop
		last;
	    }	 	   
	}


	if($i >= $num_successors - 1 && $descendants_deleted && $self->depth() == 0){
            # root node.  going to reset iterator index. descendants have been deleted.  Also, will be
            # clearing out forgotten_descendants fcost list, since those descendants will be re-generated anyway.
	    $iterator = $self->get_successors_iterator();
	    $self->{_iterator_index} = 0;
	    $i = 0;
	    	   
            # setting completed to 1
	    $self->is_completed(1);	    	  
	}
	
 	if($next_descendant){
	    
	    if($self->{_forgotten_node_fcosts}->[$next_descendant->{_descendant_index}] != -1){
		# erase the index of this node in the forgotten_nodes list
		$self->{_forgotten_node_fcosts}->[$next_descendant->{_descendant_index}] = -1;
		# decrement the number of forgotten nodes
		$self->{_forgotten_nodes_num} = $self->{_forgotten_nodes_num} - 1;
		delete $self->{_forgotten_nodes_offsets}->{$next_descendant->{_descendant_index}};
	    }

	}
	else{
            # no next successor found
	    $self->is_completed(1);
	}

	return $next_descendant;
    }     	
}



sub get_data
{
    my ($self) = @_;

    my $get_data_func = $self->{_get_data_func};
    my $data = $get_data_func->($self->{_state});
    
    return $data;
}



sub DESTROY
{
    my ($self) = @_;

    # antecedent is no longer pointing at this object, or else
    # DESTROY would not have been called.  
    if($self->{_antecedent}){
	delete $self->{_antecedent};
    }
}









lib/AI/Pathfinding/SMAstar/PriorityQueue.pm  view on Meta::CPAN

use AI::Pathfinding::SMAstar::TreeOfQueues;
use Carp;
use strict;



##################################################
# PriorityQueue constructor 
##################################################
sub new {
    my $invocant = shift;
    my $class   = ref($invocant) || $invocant;
    my $self = { 
        _hash_of_trees_ref    => {},
	
	_cost_min_max_tree   => Tree::AVL->new( fcompare => \&fp_compare,  # floating-point compare
						fget_key => sub { $_[0] },
						fget_data => sub { $_[0] },),

	f_depth        => \&AI::Pathfinding::SMAstar::Path::depth,
	f_fcost        => \&AI::Pathfinding::SMAstar::Path::fcost,
	f_avl_compare  => \&AI::Pathfinding::SMAstar::Path::compare_by_depth,
	f_avl_get_key  => \&AI::Pathfinding::SMAstar::Path::depth,
	f_avl_get_data => \&AI::Pathfinding::SMAstar::Path::get_data,

	_size                 => 0,

	@_,    # attribute override
    };
    return bless $self, $class;
}

################################################
# accessors
################################################

sub hash_of_trees {
    my $self = shift;
    if (@_) { $self->{_hash_of_trees_ref} = shift }
    return $self->{_hash_of_trees_ref};
}

sub size {
    my $self = shift;
    if (@_) { $self->{_size} = shift }
    return $self->{_size};    
}



################################################
##
## other methods       
##
################################################

sub insert {
    my ($self, $pobj) = @_;

    my $cost_hash_ref = $self->{_hash_of_trees_ref};
    my $cost_hash_key_func = $self->{f_fcost};

    my $cost_min_max_tree = $self->{_cost_min_max_tree};

    my $depth_func = $self->{f_depth};
    
    my $avl_compare_func = $self->{f_avl_compare};
    my $avl_get_key_func = $self->{f_avl_get_key};
    my $avl_get_data_func = $self->{f_avl_get_data};

    my $cost_key = $pobj->$cost_hash_key_func();
    my $data = $pobj->$avl_get_data_func();

    
    # inserting pobj with key: $cost_key, data: $data    
    if(!$cost_hash_ref->{$cost_key}){
	# no tree for this depth yet, so create one.
	my $avltree = AI::Pathfinding::SMAstar::TreeOfQueues->new(
	    f_avl_compare => $avl_compare_func,
	    f_obj_get_key => $avl_get_key_func,
	    f_obj_get_data => $avl_get_data_func,
	    );
       
	$avltree->insert($pobj);	
	$cost_hash_ref->{$cost_key} = \$avltree;
	# insert the cost_key in the cost tree
	$cost_min_max_tree->insert($cost_key);
    }
    else{
    # there is already a tree at $cost_key, so inserting there	
	my $avltree = $cost_hash_ref->{$cost_key};
	$$avltree->insert($pobj);	
    }    
    $self->{_size} = $self->{_size} + 1;
    my $antecedent = $pobj->{_antecedent};
    if($antecedent){
	$antecedent->{_descendants_on_queue} = $antecedent->{_descendants_on_queue} + 1;
    }
    $pobj->is_on_queue(1);
}


sub print_trees_in_order
{
     my ($self) = @_;

     my $cost_hash_ref = $self->{_hash_of_trees_ref};
               
     for my $cost_key (keys %$cost_hash_ref){
	 if(!$cost_hash_ref->{$cost_key}){
	     # no tree for this depth.	     
	     #print "no tree at key $depth_key\n";
	 }
	 else{
	     #print "contents of tree with depth $depth_key\n";	     
	     my $avltree = $cost_hash_ref->{$cost_key};	     
	     $$avltree->print();
	 }	 
     }      
}


#-----------------------------------
# get_list
#
# return a list of all objects in queue
#
#-----------------------------------
sub get_list
{
     my ($self) = @_;

     my $cost_hash_ref = $self->{_hash_of_trees_ref};
          
     my @list;
     
     for my $cost_key (keys %$cost_hash_ref){
	 if($cost_hash_ref->{$cost_key}){
	     my $avltree = $cost_hash_ref->{$cost_key};	     
	     push(@list, $$avltree->get_list());
	 }	 
     }     
     return @list;
}


sub is_empty
{
    my ($self) = @_;
    
    my $cost_hash_ref = $self->{_hash_of_trees_ref};
    my @cost_keys = (keys %$cost_hash_ref);
    
    if(!@cost_keys){
	return 1;
    }
    else{
	return 0;
    }
}


sub remove
{
    my ($self, $obj, $cmp_func) = @_;

    my $cost_hash_ref = $self->{_hash_of_trees_ref};
    my @cost_keys = (keys %$cost_hash_ref);
    

    my $cost_min_max_tree = $self->{_cost_min_max_tree};
    
    
    my $avl_get_data_func = $self->{f_avl_get_data};
    my $cost_hash_key_func = $self->{f_fcost};
    my $depth_func = $self->{f_depth};
    

    my $cost_key = $obj->$cost_hash_key_func();
    my $data = $obj->$avl_get_data_func();
    
    if(!$cost_hash_ref->{$cost_key}){
	# no tree for this cost_key 	
	return;
    }
    else{
	# found the tree at $cost_key, trying to remove obj from there
	
	my $avltree = $cost_hash_ref->{$cost_key};
	$$avltree->remove($obj, $cmp_func);	

	# if tree is empty, remove it from hash
	if($$avltree->is_empty()){
	    delete $cost_hash_ref->{$cost_key}; 
	    $cost_min_max_tree->remove($cost_key);
	}	
	$self->{_size} = $self->{_size} - 1;
    }    
    my $antecedent = $obj->{_antecedent};
    if($antecedent){
	$antecedent->{_descendants_on_queue} = $antecedent->{_descendants_on_queue} - 1;
    }

    $obj->is_on_queue(0);
    return;
}

sub deepest_lowest_cost_leaf 
{
    my ($self) = @_;
   
    my $cost_hash_ref = $self->{_hash_of_trees_ref};
    my @cost_keys = (keys %$cost_hash_ref);

 
    my $cost_min_max_tree = $self->{_cost_min_max_tree};

    if(!@cost_keys){
	# queue is empty
	return;
    }

    # get the lowest cost from cost_keys  
    my $lowest_cost_key = $cost_min_max_tree->smallest();
    if(!$lowest_cost_key){
	croak "deepest_lowest_cost_leaf: object not found in min-max heap\n";	
    }    
 
    
    if(!$cost_hash_ref->{$lowest_cost_key}){
	# no tree for this cost.	     
	return;
    }
    else{
	my $avltree = $cost_hash_ref->{$lowest_cost_key};
	my $obj = $$avltree->pop_largest_oldest();  # get the deepest one
	my $antecedent = $obj->{_antecedent};
	
	# if tree is empty, remove it from hash and heap.
	if($$avltree->is_empty()){
	    #tree is empty, removing key $lowest_cost_key	    
	    delete $cost_hash_ref->{$lowest_cost_key}; 
	    $cost_min_max_tree->pop_smallest();
	}		

	if($antecedent){
	    $antecedent->{_descendants_on_queue} = $antecedent->{_descendants_on_queue} - 1;
	}
	
	$obj->is_on_queue(0);
	$self->{_size} = $self->{_size} - 1;
	return $obj;   
    }
}

sub deepest_lowest_cost_leaf_dont_remove
{
    my ($self) = @_;
    
    my $avl_compare_func = $self->{f_avl_compare};
    my $avl_get_key_func = $self->{f_avl_get_key};
    my $avl_get_data_func = $self->{f_avl_get_data};
    
    my $cost_hash_ref = $self->{_hash_of_trees_ref};
    my @cost_keys = (keys %$cost_hash_ref);



    my $cost_min_max_tree = $self->{_cost_min_max_tree};

    if(!@cost_keys){
	# queue is empty
	return;
    }

    # get the lowest cost from @cost_keys 
    my $lowest_cost_key = $cost_min_max_tree->smallest();
    if(!$lowest_cost_key){
	croak "deepest_lowest_cost_leaf_dont_remove: object not found in min-max heap\n";
    }
    
    # obtaining object from lowest-cost tree at cost:  $lowest_cost_key\n";
    if(!$cost_hash_ref->{$lowest_cost_key}){
	# no tree for this cost.	     
	return;
    }
    else{
	my $avltree = $cost_hash_ref->{$lowest_cost_key};
        # found tree at key $lowest_cost_key.

	my $obj = $$avltree->largest_oldest();  # get the deepest one	
	my $cost_key = $obj->$avl_get_key_func();
	my $data = $obj->$avl_get_data_func();
	return $obj;   
    }
}


# Return the shallowest, highest-cost leaf
sub shallowest_highest_cost_leaf
{
    my ($self, $best, $succ, $str_function) = @_;
    
    my $cost_hash_ref = $self->{_hash_of_trees_ref};
    my @cost_keys = (keys %$cost_hash_ref);
 
    my $cost_min_max_tree = $self->{_cost_min_max_tree};
      
    my $obj;

    if(!@cost_keys){
	return;
    }

    my $compare_func = sub{
	my ($obj1, $obj2) = @_;
	my $obj1_str = $str_function->($obj1);
	my $obj2_str = $str_function->($obj2);	
	if($obj1_str eq $obj2_str){
	    return 1;
	}
	return 0;
    };
    
    my $cmp_func = sub {
	my ($phrase) = @_;			
	return sub{
	    my ($obj) = @_;
	    my $obj_phrase = $str_function->($obj);
	    if($obj_phrase eq $phrase){
		return 1;
	    }
	    else{ 
		return 0; 
	    }	    
	}
    };	

    # get the highest cost from @cost_keys
  
    my $highest_cost_key = $cost_min_max_tree->largest();
    if(!$highest_cost_key){
	croak "shallowest_highest_cost_leaf_dont_remove: object not found in min-max heap\n";
    }

    if(!$cost_hash_ref->{$highest_cost_key}){
	# no tree for this cost.	     
	croak "shallowest_highest_cost_leaf: no tree at key $highest_cost_key\n";
	return;
    }
    else{
	my $least_depth = 0;
	my $avltree;
	my $depth_keys_iterator;

	while(1){

	    while($least_depth == 0){
		$avltree = $cost_hash_ref->{$highest_cost_key};  #tree with highest cost
		
		# get the deepest queue in the tree
		# so we can use it to step backward to the smallest non-zero 
		# depth in the following loop
		my $queue_at_largest_depth = $$avltree->largest(); 
		$least_depth = $queue_at_largest_depth->key();
		$depth_keys_iterator = $$avltree->get_keys_iterator();
		

		# get lowest non-zero key of tree (smallest non-zero depth)
		while (defined(my $key = $depth_keys_iterator->())){
		    #########################################################################
		    #
		    # Does this need to be a non-zero depth element? yes. (example: test68.lst)
		    # 
		    #########################################################################		  
		    if($key != 0){
			$least_depth = $key;
			last;
		    }
		}

		# if no non-zero depths, find the next highest key and loop back
		my $next_highest_cost_key;
		if($least_depth == 0){
		    $next_highest_cost_key = next_largest_element(\@cost_keys, $highest_cost_key);
		    $highest_cost_key = $next_highest_cost_key;
		    if(!$highest_cost_key){
			print "no highest_cost_key found\n";
			exit;
		    }
		}
		else{ # least depth is non-zero, so it's good
		    last;
		}
		
	    }  # Now have a good highest_cost_key, with a tree that has a good non-zero key queue somewhere in it.
	    

	    my $queue = $$avltree->get_queue($least_depth);  # get the queue at least_depth	    

	    my $queue_keys_iterator = $queue->get_keys_iterator();
	    my $queue_key = $queue_keys_iterator->(); # burn the first value from the iterator since we're getting first object on next line.	    
	    $obj = $$avltree->oldest_at($least_depth); # get the shallowest one that is not at zero depth
	    
	    my $i = 1;

	    while($compare_func->($obj, $best) || $compare_func->($obj, $succ) || $obj->has_descendants_in_memory()){		
		
		if($queue_key = $queue_keys_iterator->()){					    
		    $obj = $queue->lookup_by_key($queue_key);		
	       
		}
		else{
		    # need a new least_depth.  check if there are any more queues with non-zero depth in this tree.
		    # if not, need a new highest_cost_key.
		    $obj = undef;

		    my $next_smallest = $depth_keys_iterator->();
		    if(!defined($next_smallest)){
			last;
		    }
		    else{
			$least_depth = $next_smallest;
			$queue = $$avltree->get_queue($least_depth);  # get the queue at least_depth		
			$queue_keys_iterator = $queue->get_keys_iterator();
			$queue_key = $queue_keys_iterator->(); # burn the first value from the iterator		
			$obj = $$avltree->oldest_at($least_depth); # get the shallowest one that is not at zero depth			
			$i = 1;		
			next;
		    }
		}
		
		$i++;
	    } # end while($compare_func->($obj, $best) || $compare_func->($obj, $succ) || $obj->has_descendants_in_memory())
	    	    
	    # done loop on last highest_cost_key.  if obj is not found, get another highest_cost_key, and loop back again.
	    if(!$obj){
		$least_depth = 0;
		$highest_cost_key = next_largest_element(\@cost_keys, $highest_cost_key);		
	    }
	    else{
		last;
	    }
	    
	} # end while(1)

	my $obj_str = $str_function->($obj);
	$$avltree->remove($obj, $cmp_func->($obj_str));

	if($obj){
	    $self->{_size} = $self->{_size} - 1;
	    
	    my $antecedent = $obj->{_antecedent};
	    if($antecedent){
		$antecedent->{_descendants_on_queue} = $antecedent->{_descendants_on_queue} - 1;
	    }
	    $obj->is_on_queue(0);
	    if($$avltree->is_empty()){
		delete $cost_hash_ref->{$highest_cost_key}; 
	

		$cost_min_max_tree->remove($highest_cost_key);
	    }		
	    return $obj;   
	}
	else{	
	    return;
	}
    }
}


sub largest_element
{
    my ($array) = @_;
    
    if(!@$array){
	return;
    }
    else{
	my $i = 0;
	my $largest = $$array[$i];
	for($i = 1; $i < @$array; $i++)
	{
	    if($largest < $$array[$i]){
		$largest  = $$array[$i];
	    }
	}
	return $largest;
    }
}


sub next_largest_element
{
    my ($array, $val) = @_;
    
    if(!@$array){
	return;
    }
    else{
	my $i = 0;
	my $largest = -1;
	for($i = 0; $i < @$array; $i++)
	{
	    if($$array[$i] < $val && $largest < $$array[$i]){
		$largest  = $$array[$i];
	    }
	}

	if($largest != -1){
	    return $largest;
	}
	else{
	    return;
	}
    }
}



sub next_smallest_non_zero_element
{
    my ($array, $val) = @_;
    
    my $max = 2^32-1;

    if(!@$array){
	return;
    }
    else{
	my $i = 0;
	my $smallest = $max;
	for($i = 0; $i < @$array; $i++)
	{
	    if($$array[$i] > $val && $$array[$i] < $smallest){
		$smallest  = $$array[$i];
	    }
	}

	if($smallest != $max){
	    return $smallest;
	}
	else{
	    return;
	}
    }
}


sub smallest_element
{
    my ($array) = @_;
     if(!@$array){
	return;
     }
     else{
	my $i = 0;
	my $smallest = $$array[$i];
	for($i = 1; $i < @$array; $i++){
	    if($smallest > $$array[$i]){
		$smallest  = $$array[$i];
	    }
	}
	return $smallest;
    }
}



sub get_size{
    my ($self) = @_;       
    my $cost_hash_ref = $self->{_hash_of_trees_ref};    
    my $size = 0; 
    
    foreach my $key (keys %$cost_hash_ref){
	my $tree = $cost_hash_ref->{$key};
	my $tree_size = $$tree->get_size();
	$size += $tree_size;
    }
    return $size;
}



sub fp_compare
{
    my ($obj1, $obj2) = @_;
   
    if(fp_equal($obj1, $obj2, 10)){
	return 0;
    }
    if($obj1 < $obj2){	
	return -1;
    }
    return 1;
}

sub fp_equal {
    my ($A, $B, $dp) = @_;

    return sprintf("%.${dp}g", $A) eq sprintf("%.${dp}g", $B);
}









lib/AI/Pathfinding/SMAstar/TreeOfQueues.pm  view on Meta::CPAN

package AI::Pathfinding::SMAstar::TreeOfQueues;
use strict;
use Tree::AVL;
use AI::Pathfinding::SMAstar::AVLQueue;


##################################################
# TreeOfQueues constructor 
##################################################
sub new {
    my $invocant = shift;
    my $class   = ref($invocant) || $invocant;
    my $self = {
	f_avl_compare => undef,
	f_obj_get_key  => undef,
	f_obj_get_data => undef,
	_avl_tree   => Tree::AVL->new(fcompare => \&AI::Pathfinding::SMAstar::AVLQueue::compare,
				      fget_key => \&AI::Pathfinding::SMAstar::AVLQueue::key,
				      fget_data => \&AI::Pathfinding::SMAstar::AVLQueue::key),
        @_, # attribute override
    };

    return bless $self, $class;
}


sub insert{
    my ($self, $obj) = @_;

    # check to see if there is a Queue in the tree with the key of obj.
    # if not, create one and insert
    my $fget_key = $self->{f_obj_get_key};
    my $avl_compare = $self->{f_avl_compare};
    my $key = $obj->$fget_key();
    my $queue = AI::Pathfinding::SMAstar::AVLQueue->new(_key => $key);

    my $found_queue = $self->{_avl_tree}->lookup_obj($queue);

    if(!$found_queue){
	$self->{_avl_tree}->insert($queue); # insert queue, with no duplicates	
	$queue->insert($obj); # insert object onto new queue
    }
    else { # found a queue here.  insert obj
	$found_queue->insert($obj);
    }
}


sub remove{
    my ($self, $obj, $cmp_func) = @_;

    # check to see if there is a Queue in the tree with the key of obj.
    # if not, create one and insert
    my $fget_key = $self->{f_obj_get_key};
    my $avl_compare = $self->{f_avl_compare};
    my $key = $obj->$fget_key();
    my $queue = AI::Pathfinding::SMAstar::AVLQueue->new(_key => $key);
    my $avltree = \$self->{_avl_tree};
    my $found_queue = $self->{_avl_tree}->lookup_obj($queue);



    if(!$found_queue){
#	print "TreeOfQueues::remove: did not find queue with key $key\n";
#	$self->{_avl_tree}->print();
    }
    else { # found a queue here.  remove obj
	#print "TreeOfQueues::remove: found queue, removing obj using $cmp_func\n";
	$found_queue->remove($obj, $cmp_func);
	if($found_queue->is_empty()){
	    #print "TreeOfQueues::remove: found queue is now empty, removing queue from tree\n";
	    $$avltree->remove($found_queue);	    
	}	
    }    
}

sub largest_oldest{
    my ($self) = @_;    
    my $avltree = \$self->{_avl_tree};

#    $$avltree->print("-");

    # get the avl tree with the largest key
    my $queue = $$avltree->largest();
    if($queue){
	my $key = $queue->key();
	my $obj = $queue->top();      
    	return $obj;
    }
    else{
	return;
    }
}


sub pop_largest_oldest{
    my ($self) = @_;    
    my $avltree = \$self->{_avl_tree};
    
#    $$avltree->print("*");
    
    # get the avl tree with the largest key
    my $queue = $$avltree->largest();
    if($queue){
	my $key = $queue->key();
	my $obj = $queue->pop_top();
	
	if($queue->is_empty()){
	    $$avltree->remove($queue);	    
	}	
	return $obj;
    }
    else{
	return;
    }
}

sub smallest_oldest{
    my ($self) = @_;    
    my $avltree = \$self->{_avl_tree};

#    $$avltree->print("-");

    # get the avl tree with the largest key
    my $queue = $$avltree->smallest();
    if($queue){
	my $key = $queue->key();
	my $obj = $queue->top();	
	return $obj;
    }
    else{
	return;
    }
}


sub pop_smallest_oldest{
    my ($self) = @_;    
    my $avltree = \$self->{_avl_tree};
    
#   $$avltree->print("*");
    
    # get the avl tree with the largest key
    my $queue = $$avltree->smallest();
    if($queue){
	my $key = $queue->key();
	my $obj = $queue->pop_top();
	
	if($queue->is_empty()){
	    $$avltree->remove($queue);	    	    
	}
	return $obj;
    }
    else{
	return;
    }
}


sub pop_oldest_at{
    my ($self, $key) = @_;    
    my $avltree = \$self->{_avl_tree};
    
    my $queue_to_find = AI::Pathfinding::SMAstar::AVLQueue->new(_key => $key);

    my $queue = $$avltree->lookup_obj($queue_to_find);

    if($queue){
#	print "TreeOfQueues::pop_oldest_at: found queue with key: $key\n";	
	my $obj = $queue->pop_top();
	if($queue->is_empty()){
	    $$avltree->remove($queue);	    	    
	}
	return $obj;
    }
    else{
#	print "TreeOfQueues::pop_oldest_at: did not find queue with key: $key\n";
	return;
    }
}




sub oldest_at{
    my ($self, $key) = @_;    
    my $avltree = \$self->{_avl_tree};
    
    my $queue_to_find = AI::Pathfinding::SMAstar::AVLQueue->new(_key => $key);

    my $queue = $$avltree->lookup_obj($queue_to_find);

    if($queue){
#	print "TreeOfQueues::oldest_at: found queue with key: $key\n";	
	my $obj = $queue->top();
	return $obj;
    }
    else{
#	print "TreeOfQueues::oldest_at: did not find queue with key: $key\n";
	return;
    }
}


sub largest{
    my ($self) = @_;    
    my $avltree = \$self->{_avl_tree};
    
    return $$avltree->largest();    
}





sub get_queue{
    my ($self, $key) = @_;    
    my $avltree = \$self->{_avl_tree};
    
    my $queue_to_find = AI::Pathfinding::SMAstar::AVLQueue->new(_key => $key);

    my $queue = $$avltree->lookup_obj($queue_to_find);

    if($queue){
#	print "TreeOfQueues::get_queue: found queue with key: $key\n";	
	return $queue;
    }
    else{
#	print "TreeOfQueues::get_queue: did not find queue with key: $key\n";
	return;
    }
}





sub get_keys_iterator
{
    my ($self) = @_;
    my $avltree = \$self->{_avl_tree};    
    return $$avltree->get_keys_iterator();
}


sub get_keys
{
    my ($self) = @_;    
    my $avltree = \$self->{_avl_tree};
    
    return $$avltree->get_keys();
}


sub print{
    my ($self) = @_;  

    if($self->{_avl_tree}->is_empty()){
	print "tree is empty\n";
    }

    my $get_key_func = $self->{f_obj_get_key};
    my $get_data_func = $self->{f_obj_get_data};

    my @queue_list = $self->{_avl_tree}->get_list();

    foreach my $queue (@queue_list){
	#print "queue is $queue\n";

	my $queue_key = $queue->key();
	#print "queue key: $queue_key\n";
	
	my @objlist = $queue->get_list();

	if(!@objlist){
	    print "queue at key $queue_key is empty\n";
	}

	print "queue at key $queue_key:\n";
	foreach my $obj (@objlist){
	    my $key = $obj->$get_key_func;
	    my $word = $obj->$get_data_func;
	    
	    print " key: $key, data: $word\n";
	}
    }
}



sub is_empty{    
    my ($self) = @_;
    if($self->{_avl_tree}->is_empty()){
	return 1;
    }
    return 0;
}



sub get_size{
    my ($self) = @_;  
    
    my $size = 0;
    
    if($self->{_avl_tree}->is_empty()){
	return $size;
    }
    
    my @queue_list = $self->{_avl_tree}->get_list();
    
    foreach my $queue (@queue_list){
	$size = $size + $queue->get_size();
    }
    return $size;
}

sub get_list{
    my ($self) = @_;  

    my @objs;

    if($self->{_avl_tree}->is_empty()){
	return;
    }

    #$self->{_avl_tree}->print(">>>");

    my @queue_list = $self->{_avl_tree}->get_list();

    foreach my $queue (@queue_list){
	my $queue_key = $queue->key();	


	my @objlist = $queue->get_list();	

	#print "get_list: size of queue at key: $queue_key is:  " . @objlist . "\n";

	push(@objs, @objlist);		 
    }
    return @objs;
}




1;

t/AI-Pathfinding-SMAstar.t  view on Meta::CPAN


#########################

# change 'tests => 1' to 'tests => last_test_to_print';

use Test::More tests => 9;



BEGIN { use_ok('AI::Pathfinding::SMAstar');
        use_ok('Tree::AVL');
	use_ok('AI::Pathfinding::SMAstar::Examples::PalUtils');
	use_ok('AI::Pathfinding::SMAstar::Examples::WordObj');
	use_ok('AI::Pathfinding::SMAstar::Examples::Phrase');
};

#########################

# Insert your test code below, the Test::More module is use()ed here so read
# its man page ( perldoc Test::More ) for help writing this test script.

my $dictionary_file;
my $min_letters;
my $caching;

t/AI-Pathfinding-SMAstar.t  view on Meta::CPAN


#my $collisions_per_length = PalUtils::collisions_per_length("ocid", "abo gad abalones rot abdicators enol aba dagoba");
#print "collisions: $collisions_per_length\n";
#exit;


$dictionary_file = 't/test8.lst';
$min_letters = 4;
$sparsity = 2;
$max_states_in_queue = 4;
  
diag("\ncreating AVL trees");

# create trees of WordObj objects, so that we can use
# WordObj::compare_up_to(), the 'relaxed' comparison function
my $avltree = Tree::AVL->new(
     fcompare => \&AI::Pathfinding::SMAstar::Examples::WordObj::compare,
     fget_key => \&AI::Pathfinding::SMAstar::Examples::WordObj::word,
     fget_data => \&AI::Pathfinding::SMAstar::Examples::WordObj::word,
    );

my $avltree_rev = Tree::AVL->new(
    fcompare => \&AI::Pathfinding::SMAstar::Examples::WordObj::compare,
    fget_key => \&AI::Pathfinding::SMAstar::Examples::WordObj::word,
    fget_data => \&AI::Pathfinding::SMAstar::Examples::WordObj::word,
    );


print STDERR "-" x 80 . "\n";
print STDERR "-" x 80 . "\n";


diag("reading dictionary '$dictionary_file'");
eval{

    ($num_words, @words) = AI::Pathfinding::SMAstar::Examples::PalUtils::read_dictionary_filter_by_density($dictionary_file, $sparsity);
};
is( $@, '', '$@ is not set after object insert' );

diag("loaded words: '$num_words'");
isnt( $num_words, undef, 'num_words is $num_words');



%letter_freq = AI::Pathfinding::SMAstar::Examples::PalUtils::find_letter_frequencies(@words);


foreach my $w (@words){
    my $length = length($w);
    if($length > $max_word_length){
	$max_word_length = $length;
    }
}


$num_words_filtered = @words;
diag("$num_words words in the currently loaded dictionary.  Minimum letters specified = $min_letters");
diag("$num_words_filtered words that meet the initial sparsity constraint max_sparsity = $sparsity.");

if(!@words){
    print STDERR "no words to process.  exiting\n";
    exit;
}

@word_objs = AI::Pathfinding::SMAstar::Examples::PalUtils::process_words_by_density(\@words, $sparsity);
@rev_word_objs = AI::Pathfinding::SMAstar::Examples::PalUtils::process_rev_words_by_density(\@words, $sparsity);
if(!@word_objs){ 
    print STDERR "no words achieve density specified by max sparsity $sparsity\n"; 
    exit;
}
$num_word_objs = @word_objs;


diag("loading avl trees.");
for (my $i = 0; $i < @word_objs; $i++) {
    show_progress($i/$num_words); 
    
    my $word = $word_objs[$i]->{_word};
    my $rev_word = $rev_word_objs[$i]->{_word};
 
    $avltree->insert($word_objs[$i]);    
    $avltree_rev->insert($rev_word_objs[$i]);
}
show_progress(1);
print STDERR "\n";


#
# Build the words-with-candidates list.   This will be used for phrases that are
# palindromes with a space in the middle position.   The descendants of these
# types of palindromes are found by sort-of starting all over again... any word becomes
# a candidate for the extension of the palindrome-  any word that has candidates,
# that is.   By building a list of only the words that have candidates, 
# the search time is greatly reduced.
#
my $i = 0;
diag("building words_w_cands_list.");
foreach my $w (@words){
    show_progress($i/$num_words); 
    my @candidates = AI::Pathfinding::SMAstar::Examples::PalUtils::get_cands_from_left($w, $avltree, $avltree_rev);
    if(@candidates){
	push(@words_w_cands, $w);
    }
    $i++;
}
show_progress(1);
print STDERR "\n";
my $num_words_w_cands = @words_w_cands;
diag("number of word/candidate pairs is: $num_words_w_cands.");

$avltree_height = $avltree->get_height();
$avltree_rev_height = $avltree_rev->get_height();

diag("AVL trees loaded.  Heights are $avltree_height, $avltree_rev_height\n\n");


my @phrase_obj_list;
my $smastar;

ok(
$smastar = AI::Pathfinding::SMAstar->new(
    _state_eval_func           => AI::Pathfinding::SMAstar::Examples::Phrase::evaluate($min_letters),
    _state_goal_p_func         => AI::Pathfinding::SMAstar::Examples::Phrase::phrase_is_palindrome_min_num_chars($min_letters),
    _state_num_successors_func => \&AI::Pathfinding::SMAstar::Examples::Phrase::get_num_successors,
    _state_successors_iterator => \&AI::Pathfinding::SMAstar::Examples::Phrase::get_descendants_iterator,		
    _state_get_data_func       => \&AI::Pathfinding::SMAstar::Examples::Phrase::roll_up_phrase,
    _show_prog_func            => sub{ },
    #_show_prog_func            => \&AI::Pathfinding::SMAstar::Examples::PalUtils::show_progress_so_far,
    ),
    'created smastar');


diag("smastar object created");


foreach my $word (@words_w_cands){
    my $sparsity = AI::Pathfinding::SMAstar::Examples::PalUtils::get_word_sparsity($word);   
    my $len_word = length($word);
    my $num_chars = AI::Pathfinding::SMAstar::Examples::PalUtils::num_chars_in_word($word);
    my $cost = $sparsity + $len_word;
    my $phrase = AI::Pathfinding::SMAstar::Examples::Phrase->new(
	_word_list      => \@words,
	_words_w_cands_list => \@words_w_cands,
	_dictionary     => $avltree,
	_dictionary_rev => $avltree_rev,
	_start_word     => $word,
	_word           => $word,
	_cost           => $cost,
	_letters_seen   => [],
	_cost_so_far    => 0,
	_num_chars_so_far => 0,	
	_num_new_chars  => $num_chars,
	);
    
    diag("inserting word $word");
    $smastar->add_start_state($phrase);

}


# diag("starting SMA* search...");
my $palindorme_phr_obj;
$palindrome_phr_obj = $smastar->start_search(
	\&log_function,
	\&str_function,
	$max_states_in_queue,
	$MAX_COST,
    );

my $palindrome;
if($palindrome_phr_obj){
    $palindrome = $palindrome_phr_obj->{_state}->roll_up_phrase();
}
diag("ran SMA search:   palindrome is '$palindrome'");

is( $palindrome, 'lid off a daffodil ', 'palindrome is [lid off a daffodil ]' );






t/AI-Pathfinding-SMAstar.t  view on Meta::CPAN

#
#  Auxiliary functions
#
###########################################################################



#----------------------------------------------------------------------------
sub log_function
{
    my ($path_obj) = @_;  
    
    if(!$path_obj){

	my ($pkg, $filename, $line) = caller();
	
	print "$pkg, $filename, $line\n";
	

    }

    my $str = "";
    # $cand is the parent's word (the candidate that generated this phrase)
    my $cand = "";  
    my $cost = "";
    my $cost_so_far = "";
    my $num_new_chars = "";
    my $num_chars_so_far = "";
    my $letters_seen = [];
    my $letters_seen_str = join("", @$letters_seen); 
    my $phrase = "";   
    my $evaluation = -1;
    my $depth = 0;
    
    $str = $path_obj->{_state}->{_start_word};
    # $cand is the parent's word (the candidate that generated this phrase)
    $cand = defined($path_obj->{_state}->{_cand}) ? $path_obj->{_state}->{_cand} : "";  
    $cost = $path_obj->{_state}->{_cost};
    $cost_so_far = $path_obj->{_state}->{_cost_so_far};
    $num_new_chars = $path_obj->{_state}->{_num_new_chars};
    $num_chars_so_far = $path_obj->{_state}->{_num_chars_so_far};
    $letters_seen = $path_obj->{_state}->{_letters_seen};
    $letters_seen_str = join("", @$letters_seen); 
    $phrase = defined($path_obj->{_state}->{_phrase}) ? $path_obj->{_state}->{_phrase} : "";    
    $evaluation = AI::Pathfinding::SMAstar::Path::fcost($path_obj);
    $depth = $path_obj->{_depth};
        
    
    $num_chars_so_far = sprintf("%02d", $num_chars_so_far);
    $num_new_chars = sprintf("%02d", $num_new_chars);
    $cost = sprintf("%02d", $cost);
    $cost_so_far = sprintf("%02d", $cost_so_far);
    $depth = sprintf("%02d", $depth);

    my $specifier = "%" . $max_word_length . "s";
    $str = sprintf($specifier, $str);
    $evaluation = sprintf("%04f", $evaluation);

    $letters_seen_str = sprintf("%26s", $letters_seen_str);
    
    my $log_str = "";

    $log_str = $log_str . "depth: $depth, ";
    $log_str = $log_str . "eval: $evaluation, ";
    $log_str = $log_str . "letters: '$letters_seen_str', ";
    $log_str = $log_str . "'$str', ";
    $log_str = $log_str . "'$phrase', ";
    $log_str = $log_str . "cand: $cand";
    

    
    return $log_str;   
}



#----------------------------------------------------------------------------

sub str_function
{
    my ($path_obj) = @_;    
    
    my $sw = defined($path_obj->{_state}->{_start_word}) ? $path_obj->{_state}->{_start_word} : "";    
    my $phrase = defined($path_obj->{_state}->{_phrase}) ? $path_obj->{_state}->{_phrase} : "";    
 
    my $str = "$sw, $phrase";
    
    return $str;   
}











sub flush {
   my $h = select($_[0]); my $a=$|; $|=1; $|=$a; select($h);
}

{my $spinny_thing = "-";
 my $call_num = 0;
 my $state;
sub show_progress {
    $call_num++;
    $state = $call_num % 4;
    if($state == 0){
	$spinny_thing = "-";
    }
    elsif($state == 1){
	$spinny_thing = "\\";
    }
    elsif($state == 2){
	$spinny_thing = "|";
    }
    elsif($state == 3){
	$spinny_thing = "/";
    }

    my ($progress) = @_;
    my $stars   = '*' x int($progress*10);
    my $percent = sprintf("%.2f", $progress*100);
    $percent = $percent >= 100 ? '100.00%' : $percent.'%';
    
    print STDERR "\r$stars $spinny_thing $percent.";
    flush(STDERR);
}
}



( run in 0.317 second using v1.01-cache-2.11-cpan-4d50c553e7e )