Experiments with ruby-processing (processing-2.2.1) and JRubyArt for processing-3.0

Wednesday 19 March 2014

Time to embrace jdk1.8.0 for ruby-processing (don't try this with vanilla processing).

There are performance improvements to be had if you switch to using jdk8 with ruby-processing, if you are a jEdit user it is dead simple just modify the bsh macro (gist below) to suit your system. While you are at it you can experiment with ruby-2.0 syntax (current default is ruby-1.9.3 with jruby). Check it with esefera.rb (starts of at moderate speed, even slows down briefly but the picks up again as the optimizations kick in). See below or gist (embedded gist looked crap).
// rp5.bsh
//setenv("JAVA_HOME", "/opt/jdk1.7.0_51");
setenv("JAVA_HOME", "/opt/jdk1.8.0");
setenv("GEM_HOME", "/home/tux/.gem/ruby/2.1.0");
setenv("GEM_PATH", "/home/tux/.gem/ruby/2.1.0");
new console.commando.CommandoDialog(view,"commando.rp5");


<!-- rp5.xml monkstone, 2013-August-16 for ruby-processing > 2.0.12 -->
<COMMANDO>
<UI>
<CAPTION LABEL="Run">
<FILE_ENTRY LABEL="ruby file" VARNAME="file" EVAL="buffer.getName()"/>
</CAPTION>
<CAPTION LABEL="Path to rp5">
<ENTRY LABEL="path" VARNAME="rp5path" DEFAULT=""/>
</CAPTION>
<CAPTION LABEL="Choose Run/Watch/Create/App">
<CHOICE LABEL="Select" VARNAME="type" DEFAULT="run" >
<OPTION  LABEL="run" VALUE="run"/>
<OPTION LABEL="watch" VALUE="watch"/>
<OPTION LABEL="create" VALUE="create"/>
<OPTION LABEL="export app" VALUE="app"/>
<OPTION 
</CHOICE>
</CAPTION>
<CAPTION LABEL="JRuby Opt">
<TOGGLE LABEL="jruby-complete" VARNAME="jruby" DEFAULT="FALSE"/>
</CAPTION>
</UI>

<COMMANDS>

<COMMAND SHELL="System" CONFIRM="FALSE">
<!-- cd to working dir -->

   buf = new StringBuilder("cd ");
   buf.append(MiscUtilities.getParentOfPath(buffer.getPath()));
   buf.toString();

</COMMAND>



<COMMAND SHELL="System" CONFIRM="FALSE">

   buf = new StringBuilder(rp5path);
   buf.append("rp5 ");
   if (jruby){
   buf.append("--nojruby ");
   }
   buf.append(type);
   buf.append(" ");
   switch(type){
   case "run":
   case "watch":
       buf.append(file);
       break;
   }
   buf.toString();

</COMMAND>


</COMMANDS>
</COMMANDO>

F.P.S esfera.rb
framejdk7jdk8
303.282163.78027
603.749404.49563
904.433146.03558
1204.287476.36669
1504.323806.30815
1804.297456.16097
2104.322816.37150

The above table was generated by capturing fps at 30 frame intervals (so is not entirely fair), and what you don't see cf watching the sketch running live, is a slightly faster start, followed by a pronounced slowdown (presumably when the optimization kicks in). Following a bit of experimentation best results were obtained with -XX:+TieredCompilation option alone 7+ fps with jdk8.

Saturday 15 March 2014

A gui for ai4r shape recognition in ruby

Here I use ruby-processings control_panel to create a gui for my shape recognition, which can load custom shapes (drawn in processing) or pre-built test data (see CROSS_WITH_NOISE below). See OCRExample here to learn more.
#####################################################################
# Using the ai4r gem in ruby-processing.
# A simple example that demonstrates using
# a backpropagation neural network. Use the drop box menu to
# select a prebuilt shape. To draw a test shape tick drawing checkbox,
# release the mouse when drawing a discontinous shape eg cross.
# Clear the sketch with clear button. 
# Press evaluate and result is printed to the console....
####################################################################

require 'ai4r'
load_library :vecmath, :control_panel
require_relative 'training_patterns.rb'

attr_reader :img, :img_pixels, :ci_input, :cr_input, :tr_input, :sq_input, :net, :points, :panel, :hide, :drawing

def setup
  size(320, 320)
  control_panel do |c|
    c.title = "control"
    c.look_feel "Nimbus"
    c.checkbox  :drawing
    c.button    :clear
    c.button    :evaluate
    c.menu      :shape, ['CIRCLE', 'CROSS', 'CROSS_WITH_NOISE', 'SQUARE', 'TRIANGLE', 'DEFAULT']
    @panel = c
  end
  @hide = false
  @points = []
  srand 1
  @net = Ai4r::NeuralNetwork::Backpropagation.new([256, 3])
  @ci_input = CIRCLE.flatten.collect { |input| input.to_f / 127.0}
  @tr_input = TRIANGLE.flatten.collect { |input| input.to_f / 127.0}
  @sq_input = SQUARE.flatten.collect { |input| input.to_f / 127.0}
  @cr_input = CROSS.flatten.collect { |input| input.to_f / 127.0}
  train
  background 255
end


def draw
  # only make control_panel visible once, or again when hide is false
  unless hide
    @hide = true
    panel.set_visible(hide)
  end
  if drawing
    stroke_weight 32
    stroke 127
    points.each_cons(2) { |ps, pe| line ps.x, ps.y, pe.x, pe.y}
  else
    no_fill
    stroke_weight(32)
    stroke(127)
    case @shape
    when 'CIRCLE'
      background(255)
      draw_circle
      @shape = 'DEFAULT'
    when 'CROSS'
      background(255)
      draw_cross
      @shape = 'DEFAULT'
    when 'CROSS_WITH_NOISE'
      background(255)
      draw_shape CROSS_WITH_NOISE
      @shape = 'DEFAULT'
    when 'SQUARE'
      background(255)
      draw_square
      @shape = 'DEFAULT'
    when 'TRIANGLE'
      background(255)
      draw_triangle
      @shape = 'DEFAULT'
    end
  end
end

def draw_shape shape
  background(255)
  no_stroke
  (0  ... width / 20).each do |i|
    (0  ... height / 20).each do |j|
      col = 255 - shape[i][j]
      fill(col)
      rect(i * 20, j * 20,  20,  20)
    end
  end
end

def train
  puts "Training Network Please Wait"
  101.times do |i|
    error = net.train(tr_input, [1.0, 0, 0])
    error = net.train(sq_input, [0, 1.0, 0])
    error = net.train(cr_input, [0, 0, 1.0])
    error = net.train(ci_input, [0, 1.0, 1.0])
    puts "Error after iteration #{i}:\t#{format("%.5f", error)}" if i%20 == 0
  end
end

def result_label(result)
  if result.inject(0, :+).between?(1.9, 2.1)
    if result[0] < 0.01 && result[1].between?(0.99, 1.0) && result[2].between?(0.99, 1.0)
      return "CIRCLE"
    else
      return "UNKNOWN"
    end
  elsif result.inject(0, :+).between?(0.95, 1.1)
    if result[0].between?(0.95, 1.0) && (result[1] + result[2]) < 0.01
      return "TRIANGLE"
    elsif result[1].between?(0.95, 1.0) && (result[0] + result[2]) < 0.01
      return "SQUARE"
    elsif result[2].between?(0.95, 1.0) && (result[1] + result[0]) < 0.01
      return "CROSS"
    else
      return "UNKNOWN"
    end
  end
  return "UNKNOWN"
end

def mouse_dragged
  points << Vec2D.new(mouse_x, mouse_y)
end

def mouse_released
  points.clear
end

def draw_circle
  ellipse(width / 2, height / 2, 320 - 32, 320 - 32)
end

def draw_square
  rect(16, 16, 320 - 32, 320 - 32)
end

def draw_cross
  line(width / 2, 0, width / 2, 320)
  line(0, height / 2,  320 , height / 2)
end

def draw_triangle
  triangle(width / 2, 32, 24, height - 16,  width - 24, height - 16)
end

def clear
  background 255
end

def evaluate
  load_pixels
  img_pixels = []
  (0...height).step(20) do |y|
    row = []
    (0...width).step(20) do |x|
      row << 255 - brightness(pixels[(y + 10) * width + x + 10])
    end
    img_pixels << row
  end
  puts "#{net.eval(img_pixels.flatten).inspect} => #{result_label(net.eval(img_pixels.flatten))}"
end

Friday 14 March 2014

Pattern recognition using the ai4r gem in ruby-processing

##################################################
# Using the ai4r gem in ruby-processing.
# A simple example that demonstrates using
# a backpropagation neural network. Draw a
# shape and press 'e' or 'E' to evaluate it.
# Clear the sketch with 'c' or 'C'. Release
# mouse when drawing discontinous shape eg cross.
# Result is printed to console....
##################################################

require 'ai4r'
load_library :vecmath
require_relative 'training_patterns.rb'

attr_reader :img, :img_pixels, :ci_input, :cr_input, :tr_input, :sq_input, :net, :points

def setup
  size(320, 320)
  @points = []
  srand 1
  @net = Ai4r::NeuralNetwork::Backpropagation.new([256, 3])
  @tr_input = TRIANGLE.flatten.collect { |input| input.to_f / 127.0}
  @sq_input = SQUARE.flatten.collect { |input| input.to_f / 127.0}
  @cr_input = CROSS.flatten.collect { |input| input.to_f / 127.0}
  @ci_input = CIRCLE.flatten.collect { |input| input.to_f / 127.0}
  train
  background 255
end


def draw
  stroke_weight 32
  stroke 127
  points.each_cons(2) { |ps, pe| line ps.x, ps.y, pe.x, pe.y}
end

def train
  puts "Training Network Please Wait"
  101.times do |i|
    error = net.train(tr_input, [1.0, 0, 0])
    error = net.train(sq_input, [0, 1.0, 0])
    error = net.train(cr_input, [0, 0, 1.0])
    error = net.train(ci_input, [0, 1.0, 1.0])
    puts "Error after iteration #{i}:\t#{error}" if i%20 == 0
  end
end

def result_label(result)
  if result.inject(0, :+) > 1.9
    if result[0] < result[1] && result[0] < result[2]
      return "CIRCLE"
    else
      return "UNKNOWN"
    end
  elsif result[0] > result[1] && result[0] > result[2]
    return "TRIANGLE"
  elsif result[1] > result[2]
    return "SQUARE"
  elsif result[2] > result[0] && result[2] > result[1]
    return "CROSS"
  else
    return "UNKNOWN"
  end
end

def mouse_dragged
  points << Vec2D.new(mouse_x, mouse_y)
end

def mouse_released
  points.clear
end

def key_pressed
  case key
  when 'e', 'E' # load pixels and evaluate shape
   load_pixels
   img_pixels = []
   (0...height).step(20) do |y|
    row = []
    (0...width).step(20) do |x|
     row << 255 - brightness(pixels[(y + 10) * width + x + 10])
    end
    img_pixels << row
   end
   puts "#{net.eval(img_pixels.flatten).inspect} => #{result_label(net.eval(img_pixels.flatten))}"
  when 'c', 'C'
   background 255
  end
end

The training data
# Training data  
# training_patterns.rb
#

TRIANGLE = [
  [ 0,  0,  0,  0,  0,  0,  0, 127, 127,  0,  0,  0,  0,  0,  0,  0],
  [ 0,  0,  0,  0,  0,  0, 25,  229,  229, 25,  0,  0,  0,  0,  0,  0],
  [ 0,  0,  0,  0,  0,  0, 127, 127, 127, 127,  0,  0,  0,  0,  0,  0],
  [ 0,  0,  0,  0,  0, 25,  229, 25, 25,  229, 25,  0,  0,  0,  0,  0],
  [ 0,  0,  0,  0,  0, 127, 127,  0,  0, 127, 127,  0,  0,  0,  0,  0],
  [ 0,  0,  0,  0, 25,  229, 25,  0,  0, 25,  229, 25,  0,  0,  0,  0],
  [ 0,  0,  0,  0, 127, 127,  0,  0,  0,  0, 127, 127,  0,  0,  0,  0],
  [ 0,  0,  0, 25,  229, 25,  0,  0,  0,  0, 25,  229, 25,  0,  0,  0],
  [ 0,  0,  0, 127, 127,  0,  0,  0,  0,  0,  0, 127, 127,  0,  0,  0],
  [ 0,  0, 25,  229, 25,  0,  0,  0,  0,  0,  0, 25,  229, 25,  0,  0],
  [ 0,  0, 127, 127,  0,  0,  0,  0,  0,  0,  0,  0, 127, 127,  0,  0],
  [ 0, 25,  229, 25,  0,  0,  0,  0,  0,  0,  0,  0, 25,  229, 25,  0],
  [ 0, 127, 127,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, 127, 127,  0],
  [25,  229, 25,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, 25,  229, 25],
  [127, 127,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, 127, 127],
  [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255]
]

SQUARE = [
  [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255],
  [255,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, 255],
  [255,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, 255],
  [255,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, 255],
  [255,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, 255],
  [255,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, 255],
  [255,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, 255],
  [255,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, 255],
  [255,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, 255],
  [255,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, 255],
  [255,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, 255],
  [255,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, 255],
  [255,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, 255],
  [255,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, 255],
  [255,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0,  0, 255],
  [255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255]

]

CROSS = [
  [ 0,  0,  0,  0,  0,  0,  0, 127, 127,  0,  0,  0,  0,  0,  0,  0],
  [ 0,  0,  0,  0,  0,  0,  0, 127, 127,  0,  0,  0,  0,  0,  0,  0],
  [ 0,  0,  0,  0,  0,  0,  0, 127, 127,  0,  0,  0,  0,  0,  0,  0],
  [ 0,  0,  0,  0,  0,  0,  0, 127, 127,  0,  0,  0,  0,  0,  0,  0],
  [ 0,  0,  0,  0,  0,  0,  0, 127, 127,  0,  0,  0,  0,  0,  0,  0],
  [ 0,  0,  0,  0,  0,  0,  0, 127, 127,  0,  0,  0,  0,  0,  0,  0],
  [ 0,  0,  0,  0,  0,  0,  0, 127, 127,  0,  0,  0,  0,  0,  0,  0],
  [127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127],
  [127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127, 127],
  [ 0,  0,  0,  0,  0,  0,  0, 127, 127,  0,  0,  0,  0,  0,  0,  0],
  [ 0,  0,  0,  0,  0,  0,  0, 127, 127,  0,  0,  0,  0,  0,  0,  0],
  [ 0,  0,  0,  0,  0,  0,  0, 127, 127,  0,  0,  0,  0,  0,  0,  0],
  [ 0,  0,  0,  0,  0,  0,  0, 127, 127,  0,  0,  0,  0,  0,  0,  0],
  [ 0,  0,  0,  0,  0,  0,  0, 127, 127,  0,  0,  0,  0,  0,  0,  0],
  [ 0,  0,  0,  0,  0,  0,  0, 127, 127,  0,  0,  0,  0,  0,  0,  0],
  [ 0,  0,  0,  0,  0,  0,  0, 127, 127,  0,  0,  0,  0,  0,  0,  0]
]

CIRCLE = [
  [0, 0, 0, 0, 32, 64, 64, 80, 80, 64, 64, 32, 0, 0, 0, 0],
  [0, 0, 32, 64, 96, 103, 64, 64, 64, 64, 96, 96, 64, 32, 0, 0],
  [0, 32, 96, 128, 96, 32, 0, 0, 0, 0, 32, 89, 128, 96, 32, 0],
  [0, 64, 128, 96, 18, 0, 0, 0, 0, 0, 0, 0, 64, 128, 64, 0],
  [32, 96, 96, 18, 0, 0, 0, 0, 0, 0, 0, 0, 0, 82, 101, 32],
  [64, 103, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 96, 64],
  [64, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 64, 68],
  [80, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 64, 104],
  [80, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 64, 106],
  [64, 64, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 64, 70],
  [64, 96, 32, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 32, 96, 64],
  [32, 96, 88, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 64, 119, 32],
  [0, 64, 128, 64, 0, 0, 0, 0, 0, 0, 0, 0, 32, 113, 70, 0],
  [0, 32, 96, 128, 81, 32, 0, 0, 0, 0, 32, 64, 113, 96, 32, 0],
  [0, 0, 32, 64, 102, 96, 64, 64, 64, 64, 96, 119, 70, 32, 0, 0],
  [0, 0, 0, 0, 32, 64, 69, 105, 106, 70, 64, 32, 0, 0, 0, 0]
]


Monday 10 March 2014

Yet Another Nature of Code Example ported to ruby-processing

For the nn library java code see ruby-processing on github.
# The Nature of Code
# Daniel Shiffman
# http://natureofcode.com

# XOR Multi-Layered Neural Network Example
# Neural network java code is all in the "src" folder
load_library :nn

require_relative './landscape'
include_package 'nn'

ITERATIONS_PER_FRAME = 5

attr_reader :inputs, :nn, :count, :land, :theta, :f, :result, :known


def setup

  size(400, 400, P3D)
  @theta = 0.0
  # Create a landscape object
  @land = Landscape.new(20, 300, 300)
  @f = create_font("Courier", 12, true)

  @nn = Network.new(2, 4)
  @count = 0
  # Create a list of 4 training inputs
  @inputs = []
  inputs << [1.0, 0]
  inputs << [0, 1.0]
  inputs << [1.0, 1.0]
  inputs << [0, 0.0]
end

def draw
  lights
  ITERATIONS_PER_FRAME.times do |i|
    inp = inputs.sample
    # Compute XOR    
    @known = ((inp[0] > 0.0 && inp[1] > 0.0) || (inp[0] < 1.0 && inp[1] < 1.0))? 0 : 1.0

    # Train that sucker!
    @result = nn.train(inp, known)
    @count += 1
  end

  # Ok, visualize the solution space
  background(175)
  push_matrix
  translate(width / 2, height / 2 + 20, -160)
  rotate_x(Math::PI / 3)
  rotate_z(theta)

  # Put a little BOX on screen
  push_matrix

  stroke(50)
  no_fill
  translate(-10, -10, 0)
  box(280)
  land.calculate(nn)
  land.render
  # Draw the landscape
  pop_matrix

  @theta += 0.0025
  pop_matrix

  # Display overal neural net stats
  network_status
end

def network_status
  mse = 0.0
  text_font(f)
  fill(0)
  text("Your friendly neighborhood neural network solving XOR.", 10, 20)
  text("Total iterations: #{count}", 10, 40)
  mse += (result - known) * (result - known)
  rmse = Math::sqrt(mse / 4.0)
  out = "Root mean squared error: #{format("%.5f", rmse)}"
  hint DISABLE_DEPTH_SORT
  text(out, 10, 60)
  hint ENABLE_DEPTH_SORT
end

# The Nature of Code
# Daniel Shiffman
# http://natureofcode.com

# "Landscape" example

class Landscape
  include Processing::Proxy

  attr_reader :scl, :w, :h, :rows, :cols, :z, :zoff



  def initialize(scl, w, h)
    @scl, @w, @h  = scl, w, h
    @cols = w / scl
    @rows = h / scl
    @z = Array.new(cols, Array.new(rows, 0.0))
  end


  # Calculate height values (based off a neural network)
  def calculate(nn)
    val = ->(curr, nn, x, y){curr * 0.95 + 0.05 *  (nn.feed_forward([x, y]) * 280.0 - 140.0)}
    @z = (0 ... cols).map{|i|
      (0 ... rows).map{|j|
        val.call(z[i][j], nn, i * 1.0/ cols, j * 1.0/cols)
      }
    }
  end

  # Render landscape as grid of quads
  def render
    # Every cell is an individual quad
    # (could use quad_strip here, but produces funny results, investigate this)
    (0 ... z.size - 1).each do |x|
      (0 ... z[0].size - 1).each do |y|

        # one quad at a time
        # each quad's color is determined by the height value at each vertex
        # (clean this part up)
        no_stroke
        push_matrix
        begin_shape(QUADS)
          translate(x * scl - w * 0.5, y * scl - h * 0.5, 0)
          fill(z[x][y]+127, 220)
          vertex(0, 0, z[x][y])
          fill(z[x+1][y]+127, 220)
          vertex(scl, 0, z[x+1][y])
          fill(z[x+1][y+1]+127, 220)
          vertex(scl, scl, z[x+1][y+1])
          fill(z[x][y+1]+127, 220)
          vertex(0, scl, z[x][y+1])
          end_shape
          pop_matrix
        end
      end
    end
  end

Tuesday 4 March 2014

Using inject with an array of Vec2D (another Nature of Code Example)

See previous blog for background information. Here is another example of using inject to sum an array, however in this case we are summing objects of Vec2D, not numbers!
# The Nature of Code
# http://natureofcode.com

# Simple Perceptron Example
# See: http://en.wikipedia.org/wiki/Perceptron
load_library :vecmath

class Perceptron
  # Perceptron is created with n weights and learning constant
  def initialize(n, c)
    @weights = Array.new(n){ rand(0 .. 1.0) }
    @c = c
  end

  # Function to train the Perceptron
  # Weights are adjusted based on vehicle's error
  def train(forces, error)
    trained = @weights.zip(forces.map{|f| f.to_a}
    .map{|a, b| (a * error.x + b * error.y) * @c})
    .map {|w, c| constrain(w + c, 0.0, 1.0)}
    @weights = trained
  end

  # Give me a steering result
  def feedforward(forces)
    # Sum all values
    forces.zip(@weights).map{|a, b| a * b}.inject(Vec2D.new){|c, d| c + d}
  end
end

# Seek
# Daniel Shiffman <http://www.shiffman.net>

class Vehicle

  MAX_SPEED = 4
  MAX_FORCE = 0.1
  attr_reader :brain, :sz, :location, :targets, :desired
  attr_reader :maxforce_squared, :maxspeed_squared

  def initialize(n, x, y)
    @brain = Perceptron.new(n, 0.001)
    @acceleration = Vec2D.new(0, 0)
    @velocity = Vec2D.new(0, 0)
    @location = Vec2D.new(x, y)
    @sz = 6.0
    @maxspeed_squared = MAX_SPEED * MAX_SPEED
    @maxforce_squared = MAX_FORCE * MAX_FORCE
  end

  # Method to update location
  def update(width, height)
    # Update velocity
    @velocity += @acceleration
    # Limit speed
    @velocity.set_mag(MAX_SPEED) if @velocity.mag_squared > maxspeed_squared
    @location += @velocity
    # Reset acceleration to 0 each cycle
    @acceleration *= 0

    @location.x = constrain(location.x, 0, width)
    @location.y = constrain(location.y, 0, height)
  end

  def apply_force(force)
    # We could add mass here if we want A = F / M
    @acceleration += force
  end

  # Here is where the brain processes everything
  def steer(targets, desired)
    # Steer towards all targets
    forces = targets.map{|target| seek(target) }

    # That array of forces is the input to the brain
    result = brain.feedforward(forces)

    # Use the result to steer the vehicle
    apply_force(result)

    # Train the brain according to the error
    error = desired - location
    brain.train(forces, error)
   end

  # A method that calculates a steering force towards a target
  # STEER = DESIRED MINUS VELOCITY
  def seek(target)
    desired = target - location  # A vector pointing from the location to the target

    # Normalize desired and scale to the maximum speed
    desired.normalize!
    desired *= MAX_SPEED
    # Steering = Desired minus velocity
    steer = desired - @velocity
    steer.set_mag(MAX_FORCE) if steer.mag_squared > maxforce_squared # Limit to a maximum steering force
    steer
  end

  def display

    # Draw a triangle rotated in the direction of velocity
    theta = @velocity.heading + Math::PI / 2
    fill(175)
    stroke(0)
    stroke_weight(1)
    push_matrix
    translate(location.x, location.y)
    rotate(theta)
    begin_shape
    vertex(0, -sz)
    vertex(-sz * 0.5, sz)
    vertex(sz * 0.5, sz)
    end_shape(CLOSE)
    pop_matrix
  end
end

# A Vehicle controlled by a Perceptron
attr_reader :targets, :desired, :v


def setup
  size(640, 360)
  # The Vehicle's desired location
  @desired = Vec2D.new(width/2, height/2)

  # Create a list of targets
  make_targets

  # Create the Vehicle (it has to know about the number of targets
  # in order to configure its brain)
  @v = Vehicle.new(targets.size, rand(width), rand(height))
end

# Make a random ArrayList of targets to steer towards
def make_targets
  @targets = Array.new(8) { Vec2D.new(rand(width), rand(height)) }
end

def draw
  background(255)

  # Draw a circle to show the Vehicle's goal
  stroke(0)
  stroke_weight(2)
  fill(0, 100)
  ellipse(desired.x, desired.y, 36, 36)

  # Draw the targets
  targets.each do |target|
    no_fill
    stroke(0)
    stroke_weight(2)
    ellipse(target.x, target.y, 16, 16)
    line(target.x, target.y - 16, target.x, target.y + 16)
    line(target.x - 16, target.y, target.x + 16, target.y)
  end

  # Update the Vehicle
  v.steer(targets, desired)
  v.update(width, height)
  v.display
end

def mouse_pressed
  make_targets
end

Here I am particulary pleased with the Perceptron very functional looking train and the feedforward code, where I use inject to sum Vec2D.

Getting closer to "functional programming with ruby-processing"

In a previous blog I reported that Dan Shiffmans Nature of Code had been ported to ruby processing. So I took a look at the code, and could not help from meddling here is my modified version of the perceptron sketch:-
# The Nature of Code
# http://natureofcode.com

# Simple Perceptron Example
# See: http://en.wikipedia.org/wiki/Perceptron

class Perceptron
  attr_reader :weights

  # Perceptron is created with n weights and a learning constant
  def initialize(n, c)
    @weights = Array.new(n) { rand(-1.0 .. 1) }
    @c = c # learning constant
  end

  # Function to train the Perceptron
  # Weights are adjusted based on "desired" answer
  def train(inputs, desired)
    # Guess the result
    guess = feedforward(inputs)
    # Compute the factor for changing the weight based on the error
    # Error = desired output - guessed output
    # Note this can only be 0, -2, or 2
    # Multiply by learning constant
    error = desired - guess
    # Adjust weights based on weightChange * input
    @weights.each_index{|i| @weights[i] += error * inputs[i]}
  end

  # Guess -1 or 1 based on input values
  def feedforward(inputs)
    # Sum all values
    sum = @weights.zip(inputs).map{|x, y| x * y}.inject(0){|x, y| x + y}
    # Result is sign of the sum, -1 or 1
    activate(sum)
  end

  def activate(sum)
    sum > 0 ? 1 : -1
  end
end

# A class to describe a training point
# Has an x and y, a "bias" (1) and a known output
# Could also add a variable for "guess" but not required here

class Trainer
  attr_reader :inputs, :answer

  def initialize(x, y, a)
    @inputs = [x, y, 1]
    @answer = a
  end
end

# Code based on text "Artificial Intelligence", George Luger

# The function to describe a line
def f(x)
  0.4 * x + 1
end

def setup
  size(640, 360)
  # Coordinate space
  @xmin = -400
  @ymin = -100
  @xmax =  400
  @ymax =  100

  @count = 0

  # The perceptron has 3 inputs -- x, y, and bias
  # Second value is "Learning Constant"
  @ptron = Perceptron.new(3, 0.00001)  # Learning Constant is low just b/c it's fun to watch, this is not necessarily optimal

   #Create a random set of training points and calculate the "known" answer
  @training = Array.new(2000) do
    x = rand(@xmin .. @xmax)
    y = rand(@ymin .. @ymax)
    answer = y < f(x) ? -1 : 1
    Trainer.new(x, y, answer)
  end

  smooth
end

def draw
  background(255)
  translate(width/2, height/2)

  # Draw the line
  stroke_weight(4)
  stroke(127)
  x1 = @xmin
  y1 = f(x1)
  x2 = @xmax
  y2 = f(x2)
  line(x1, y1, x2, y2)

  # Draw the line based on the current weights
  # Formula is weights[0]*x + weights[1]*y + weights[2] = 0
  stroke(0)
  stroke_weight(1)
  weights = @ptron.weights
  x1 = @xmin
  y1 = (-weights[2] - weights[0]*x1) / weights[1]
  x2 = @xmax
  y2 = (-weights[2] - weights[0]*x2) / weights[1]
  line(x1, y1, x2, y2)

  # Train the Perceptron with one "training" point at a time
  @ptron.train(@training[@count].inputs, @training[@count].answer)
  @count = (@count + 1) % @training.size

  # Draw all the points based on what the Perceptron would "guess"
  # Does not use the "known" correct answer
  @count.times do |i|
    stroke(0)
    stroke_weight(1)
    fill(0)
    train = @training[i]
    guess = @ptron.feedforward(train.inputs)
    no_fill if guess > 0

    ellipse(train.inputs[0], train.inputs[1], 8, 8)
  end
end

Here I am particulary pleased with the Perceptron feedforward code, that sums the weights using zip, map and inject. This code has a somewhat functional feel to it?

Followers

Blog Archive

About Me

My photo
I have developed JRubyArt and propane new versions of ruby-processing for JRuby-9.1.5.0 and processing-3.2.2