#!/usr/bin/perl -w
# $Id$
# This program implements a fun little neural network.  The network
# doesn't accomplish anything particularly useful; it just shows
# another way that POE can be used to simulate concurrency.
use strict;
use POE;

# Initialize a neuron session when it's first instantiated.  Save its
# parameters within itself, and set an alias for the neuron so it can
# be referred to by name later.
sub neuron_start {
  my ($kernel, $session, $heap, $neuron_name, $threshhold, $low_fire,
    $high_fire) = @_[KERNEL, SESSION, HEAP, ARG0 .. ARG3];

  # Save things this neuron needs to know about itself.
  $heap->{name}       = $neuron_name;
  $heap->{threshhold} = $threshhold;
  $heap->{low_fire}   = $low_fire;
  $heap->{high_fire}  = $high_fire;
  $heap->{value}      = 0;

  # Register the neuron's name.
  $kernel->alias_set($neuron_name);

  # Be noisy for the sake of science.
  print "Neuron '$heap->{name}' started.\n";
}

# The neuron has received a stimulus.  Accumulate the stimulus, and
# fire outbound stimuli if its incoming stimulus threshhold has been
# exceeded.
sub neuron_stimulated {
  my ($kernel, $session, $heap, $value) = @_[KERNEL, SESSION, HEAP, ARG0];

  # Add the stimulus to the neuron's accumulator, and be noisy.
  $heap->{value} += $value;
  print "Neuron $heap->{name} received $value and now has $heap->{value}.\n";

  # If this stimulus pushes the neuron over a threshhold, then fire a
  # stimulus to the "high" neighbor neuron.  If there's no such
  # neuron, then move on to "low" bit.
  if ($heap->{value} >= $heap->{threshhold}) {
    if (length $heap->{high_fire}) {
      $kernel->post($heap->{high_fire}, stimulate => 10);
      print("Neuron $heap->{name} reached its threshhold and fired to ",
        "$heap->{high_fire}.\n");
    }
    else {
      print("Neuron $heap->{name} reached its threshhold and has ",
        "nothing to do.\n");
    }

    # Reset the accumulator so this neuron is never stuck in a "high"
    # state.
    $heap->{value} = 0;
  }

  # Always fire a stimulus to the low-threshhold neuron, but do this
  # after a brief, random delay.  This uses delay_add() to enqueue
  # multiple redundant delays.  If it used delay(), each call would
  # overwrite the previous one.
  if (length $heap->{low_fire}) {
    $kernel->delay_add(respond_later => rand(5));
  }
}

# Respond after a delay.  The delay is done, and we can now fire a
# low-threshhold event.
sub neuron_delayed_response {
  my ($kernel, $heap) = @_[KERNEL, HEAP];
  $kernel->post($heap->{low_fire}, stimulate => 10);
  print "Neuron $heap->{name} has fired a stimulus to $heap->{low_fire}.\n";
}

# This defines a small (probably pointless) neural network.  Each
# record has four fields.  The first two are about the neuron itself:
# Its name, and its stimulus accumulator's overflow threshhold.  The
# other two fields are neurons to send stimuli to when an incoming
# stimulus is too weak or overflows the neuron's accumulator.
my @neural_net = (
  ['one',   10,  'one',   'two'],
  ['two',   20,  'three', 'four'],
  ['three', 30,  'five',  'six'],
  ['four',  50,  'seven', 'eight'],
  ['five',  100, 'nine',  'ten'],
  ['six',   70,  'seven', 'eight'],
  ['seven', 80,  'nine',  'ten'],
  ['eight', 40,  'nine',  'ten'],
  ['nine',  60,  '',      'ten'],
  ['ten',   90,  '',      ''],
);

# A quick and dirty way to spawn neurons.  It scans the list of
# neurons and passes each record to a new Session.  POE::Session maps
# event names to the functions that will handle those events.  For
# example, "_start" will be handled by neuron_start().
foreach (@neural_net) {
  my ($name, $threshhold, $low, $high) = @$_;
  POE::Session->create(
    inline_states => {
      _start        => \&neuron_start,
      stimulate     => \&neuron_stimulated,
      respond_later => \&neuron_delayed_response,
    },

    #         ARG0,  ARG1,        ARG2, ARG3 ... for neuron_start
    args => [$name, $threshhold, $low, $high],
  );
}

# Prod the network into life by giving it an initial stimulus.  Neuron
# "one" is designed to constantly generate new stimuli, so poking it
# once will set it throbbing indefinitely.
$poe_kernel->post(one => stimulate => 10);

# Run the neural network until Dave arrives to unplug it, or Zaphod
# pops 'round with an axe.  If you don't know a Dave or Zaphod, you
# can simply stop it with Ctrl+C.
$poe_kernel->run();
exit 0;