lib/rubyneat/rubyneat.rb in rubyneat-0.3.5.alpha.6 vs lib/rubyneat/rubyneat.rb in rubyneat-0.4.0.alpha.3

- old
+ new

@@ -1,10 +1,12 @@ +require 'semver' require 'distribution' require 'yaml' require 'logger' require 'awesome_print' require 'deep_dive' +require 'queue_ding' =begin rdoc = RubyNEAT -- a Ruby Implementation of the Neural Evolution of Augmenting Topologies. The RubyNEAT system incorporates the basis of the NEAT algorithm. Flexibility @@ -88,13 +90,17 @@ # PrettyPrint to log.debug def self.dpp ob #$log.ap ob end - # Basis of all NEAT objects + # Basis of all NEAT objects. + # NeatOb has support for NEAT attributes with + # special support for hooks and queues. class NeatOb include DeepDive + extend QueueDing + exclude :controller, :name # Designation of this particular object instance attr_reader :name @@ -123,10 +129,138 @@ end def to_s "%s<%s>" % [self.class, self.name] end + + + class << self + # Defaultable attributes of neat attributes. + # + # If hooks: true is given, two hook functions are + # created: + ## <sym>_add() -- add a hook + ## <sym>_set() -- set a hook, overwriting all other hooks set or added. + ## <sym>_clear -- clear all hooks + ## <sym>_none? -- return true if no hooks are defined. + ## <sym>_one? -- return true if exactly hook is defined. + ## <sym>_hook() -- for passing unnamed parameters to a singular hook. + ## <sym>_np_hook() -- for passing unnamed parameters to a singular hook. + ## <sym>_hook_itself() -- for getting the proc reference to the hook. + ## <sym>_hooks() -- for passing unnamed parameters. + ## <sym>_np_hooks() -- for passing a named parameter list. + # + # For *_hook(), the function returns the single result. + # For *_hooks(), the hook function return an array of results + # from all the actual registered hooks called. + def attr_neat(sym, + default: nil, + cloneable: nil, + hooks: false, + queue: false) + svar = "@#{sym}" + + # Guess what clonable should be. + # This is meant to cover "90%" of the cases. + cloneable = case + when default.nil? + false + when default.kind_of?(Numeric) + false + else + true + end if cloneable.nil? + + # Sanity checks + raise NeatException("Both hooks and queue cannot both be set for #{sym}.") if hooks and queue + raise NeatException("Defaults cannot be defined for hooks and queues for #{sym}.") if (hooks or queue) and not default.nil? + + if hooks + default = [] + cloneable = true + hook_setup sym + end + + if queue + default = QDing.new + cloneable = true + queue_setup sym + end + + define_method("#{sym}=") do |v| + instance_variable_set(svar, v) + end unless hooks or queue + + # TODO: Enhance this getter method for performance. + define_method(sym) do + instance_variable_set(svar, + instance_variable_get(svar) || + ((cloneable) ? default.clone + : default)) + end + end + + private + def hook_setup(sym) + define_method("#{sym}_add") do |&hook| + send(sym) << hook + end + + define_method("#{sym}_set") do |&hook| + send(sym).clear + send(sym) << hook + end + + define_method("#{sym}_clear") do + send(sym).clear + end + + define_method("#{sym}_none?") do + send(sym).empty? + end + + define_method("#{sym}_one?") do + send(sym).size == 1 + end + + # hooks with named parameters + define_method("#{sym}_np_hooks") do |**hparams| + send(sym).map{|funct| funct.(**hparams)} + end + + # hooks with traditional parameters + define_method("#{sym}_hooks") do |*params| + send(sym).map{|funct| funct.(*params)} + end + + # TODO: DRY up the following functions, which does size checking in exacly the same way. + # Single hook with named parameters + define_method("#{sym}_np_hook") do |**hparams| + sz = send(sym).size + raise NeatException.new("#{sym}_np_hook must have exactly one hook (#{sz})") unless sz == 1 + send(sym).map{|funct| funct.(**hparams)}.first + end + + # Single hook with traditional parameters + define_method("#{sym}_hook") do |*params| + sz = send(sym).size + raise NeatException.new("#{sym}_hook must have exactly one hook (#{sz})") unless sz == 1 + send(sym).map{|funct| funct.(*params)}.first + end + + # Get the singular hook function + define_method("#{sym}_hook_itself") do + sz = send(sym).size + raise NeatException.new("#{sym}_hook_itself must have exactly one hook (#{sz})") unless sz == 1 + send(sym).first + end + end + + def queue_setup(sym) + # Add boilerplate code for queues here. + end + end end class NeatException < Exception end @@ -159,13 +293,28 @@ # This object contains all the specifications and details for # evolving and evaluation of the RubyNEAT system. It is # a type of "World", if you will, for the entire enterprise. # # Your application shall only have one Controller. + # + # FIXME: The function hooks really should be able to take more + # FIXME: than one hook! we don't need that functionality right + # FIXME: now. Also, the Controller 'god' object itself will need + # FIXME: to undergo some refactorization so that we can have many + # FIXME: of them for HyperNEAT, co-evolution, etc. + # + # FIXME: An alternative approach would be to have demigod objects + # FIXME: where the controller would lord it over them all. Attention + # FIXME: must also be given to Rubinius and JRuby so that we can + # FIXME: run under multiple cores. class Controller < NeatOb + # Version of RubyNEAT runing + attr_neat :version, default: SemVer.find.format("%M.%m.%p%s") + attr_neat :neater, default: '--unspecified--' + # global innovation number - attr_reader :glob_innov_num + attr_neat :glob_innov_num, default: 0, cloneable: false # current sequence number being evaluated attr_reader :seq_num # Current generation count @@ -191,34 +340,40 @@ # Global verbosity level: ## 1 - normal (the default) ## 2 - really verbose ## 3 - maximally verbose # Use in conjunction with log.debug - attr_accessor :verbosity + attr_neat :verbosity, default: 1 # Query function that Critters shall call. - attr_accessor :query_func + attr_neat :query_func, hooks: true # Fitness function that Critters shall be rated on. - attr_accessor :fitness_func + attr_neat :fitness_func, hooks: true # Recurrence function that Critters will yield to. - attr_accessor :recurrence_func + attr_neat :recurrence_func, hooks: true # Compare function for fitness # Cost function for integrating in the cost to the fitness scalar. - attr_accessor :compare_func, :cost_func, :stop_on_fit_func + attr_neat :compare_func, hooks: true + attr_neat :cost_func, hooks: true + attr_neat :stop_on_fit_func, hooks: true # End run function to call at the end of each generational run # Also report_hook to dump reports for the user, etc. - attr_accessor :end_run_func, :report_hook + attr_neat :end_run, hooks: true + attr_neat :report, hooks: true + # Hook to handle pre_exit functionality + attr_neat :pre_exit, hooks: true + # Logger object for all of RubyNEAT attr_reader :log # Various parameters affecting evolution. - # Based somewhat on the C version of NEAT. + # Based somewhat on the Ken Stanley C version of NEAT. # TODO not all of these parameters are implemented yet!!! class NeatSettings < NeatOb ## RubyNEAT specific # Set to true to returned named parameters as hashes to the fitness function @@ -253,14 +408,14 @@ attr_accessor :mate_multipoint_prob attr_accessor :mate_only_prob attr_accessor :mate_singlepoint_prob # Maximum number of generations to run, if given. - attr_accessor :max_generations + attr_neat :max_generations, default: 1000 - # Maximun number of populations to maintain in the history buffer. - attr_accessor :max_population_history + # Maximum number of populations to maintain in the history buffer. + attr_neat :max_population_history, default: 10 attr_accessor :mutate_add_gene_prob attr_accessor :mutate_add_neuron_prob attr_accessor :mutate_gene_disable_prob @@ -268,13 +423,13 @@ attr_accessor :mutate_gene_trait_prob # For gene weights perturbations and changes (complete overwrites) attr_accessor :mutate_perturb_gene_weights_prob, - :mutate_perturb_gene_weights_sd, - :mutate_change_gene_weights_prob, - :mutate_change_gene_weights_sd + :mutate_perturb_gene_weights_sd, + :mutate_change_gene_weights_prob, + :mutate_change_gene_weights_sd attr_accessor :mutate_neuron_trait_prob attr_accessor :mutate_only_prob attr_accessor :mutate_random_trait_prob attr_accessor :mutate_toggle_enable_prob @@ -282,19 +437,20 @@ attr_accessor :newlink_tries attr_accessor :neuron_trait_mut_sig # fitness costs, if given, use in the computation of fitness # AFTER the overall fitness for the applied stimuli have been - # caclulated. + # calculated. attr_accessor :fitness_cost_per_neuron attr_accessor :fitness_cost_per_gene # If set, will start off at the specified size and # grow to the bigger population size attr_accessor :start_population_size, :population_size - attr_accessor :start_sequence_at, :end_sequence_at + attr_neat :start_sequence_at, default: 0 + attr_neat :end_sequence_at, default: 100 attr_accessor :print_every attr_accessor :recur_only_prob attr_accessor :recur_prob @@ -327,14 +483,10 @@ attr_accessor :recurrency_switch # Set up defaults for mandatory entries. def initialize super - @start_sequence_at = 0 - @end_sequence_at = 100 - @max_generations = 1000 - # Default operators @evaluator = Evaluator.new self @expressor = Expressor.new self @evolver = Evolver.new self end @@ -347,12 +499,10 @@ neural_outputs: nil, neural_hidden: nil, parameters: NeatSettings.new, &block) super(self) - @verbosity = 1 - @glob_innov_num = 0 @gaussian = Distribution::Normal.rng @population_history = [] @evolver = Evolver.new self @expressor = Expressor.new self @@ -374,23 +524,24 @@ open(parameters, 'r') { |fd| YAML::load fd.read } end block.(self) unless block.nil? end - def new_innovation ; @glob_innov_num += 1; end + def new_innovation ; self.glob_innov_num += 1 ; end def gaussian ; @gaussian.() ; end # Run this evolution. def run pre_run_initialize (1..@parms.max_generations).each do |gen_number| - @generation_num = gen_number + @generation_num = gen_number # must be set first @population_history << unless @population.nil? @population else @population = @population_class.new(self) end + @population.generation = gen_number @population_history.shift unless @population_history.size <= @parms.max_population_history @population.mutate! @population.express! ## Evaluate population @@ -401,41 +552,48 @@ end @population.analyze! @population.speciate! - $log.debug @population.dump_s unless @verbosity < 3 + $log.debug @population.dump_s unless self.verbosity < 3 new_pop = @population.evolve ## Report hook for evaluation - @report_hook.(@population.report) unless @report_hook.nil? + report_hooks(@population.report) ## Exit if fitness criteria is reached #FIXME handle this exit condition better!!!!! - exit if @stop_on_fit_func.(@population.report[:fitness], self) unless @stop_on_fit_func.nil? + exit_neat if stop_on_fit_func_hook(@population.report.last[:fitness], self) unless stop_on_fit_func_none? ## Evolve population @population = new_pop ## Finish up this run - @end_run_func.(self) unless @end_run_func.nil? + end_run_hooks(self) end end private # We must set up the objects we need prior to the run, if not set. def pre_run_initialize @evaluator = @evaluator_class.new(self) if @evaluator.nil? @evolver = @evolver_class.new(self) if @evolver.nil? end + + # Allow us to hook in pre-exit functionality here + # This function shall never return. + def exit_neat + pre_exit_hook(self) unless pre_exit_none? + exit + end end @controller = Controller.new def self.controller ; @controller ; end def self.controller=(controller) ; @controller = controller ; end def self.create_controller(*parms); @controller = Controller.new(*parms); end end # We put all the internal requires at the end to avoid conflicts. -require 'rubyneat/neuron' -require 'rubyneat/population' +require_relative 'neuron' +require_relative 'population'