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

Showing posts with label re-factor. Show all posts
Showing posts with label re-factor. Show all posts

Sunday, 3 February 2013

rspec and re-factoring cs_grammar

I have been taking another look a context sensitive L-Systems when I found this. Which kind of inspired me to have another look at my cs_lsysteme library, and I made use of the rspec framework to do some re-factoring of my cs_grammar lib. Here is the updated rspec that describes the expected performance (context no longer scrolls around the 'production string' in either direction, this behaviour matches lmusej):-
require 'cs_grammar'

IGNORE='[]'

describe "Grammar" do
  context "given axiom 'baaaaaa'" do
    context "with function b<a" do
      describe "#generate(x)" do
        it "should equal 'baaaaaa' at zero iteration" do
          Grammar.new('baaaaaa', {'b' => 'a', 'b<a' => 'b'}).generate(0).should == 'baaaaaa'
        end
        it "should equal 'abaaaaa' after one iteration" do
          Grammar.new('baaaaaa', {'b' => 'a', 'b<a' => 'b'}).generate(1).should == 'abaaaaa'
        end
        it "should equal 'aabaaaa' after one iteration" do
          Grammar.new('abaaaaa', {'b' => 'a', 'b<a' => 'b'}).generate(1).should == 'aabaaaa'
        end
        it "should equal 'aabaaaa' after two iterations" do
          Grammar.new('baaaaaa', {'b' => 'a', 'b<a' => 'b'}).generate(2).should == 'aabaaaa'
        end
      end
    end
    context "with function b<a and ignore []" do
      describe "#generate(x)" do
        it "should equal 'baaa[a]aa' at zero iteration" do
          Grammar.new('baaa[a]aa', {'b' => 'a', 'b<a' => 'b'}, IGNORE).generate(0).should == 'baaa[a]aa'
        end
        it "should equal 'abaaaaa' after one iteration" do
          Grammar.new('baaa[a]aa', {'b' => 'a', 'b<a' => 'b'}, IGNORE).generate(1).should == 'abaa[a]aa'
        end
        it "should equal 'aabaaaa' after one iteration" do
          Grammar.new('abaa[a]aa', {'b' => 'a', 'b<a' => 'b'}, IGNORE).generate(1).should == 'aaba[a]aa'
        end
        it "should equal 'aabaaaa' after two iterations" do
          Grammar.new('baaa[a]aa', {'b' => 'a', 'b<a' => 'b'}, IGNORE).generate(2).should == 'aaba[a]aa'
        end
      end
    end
    context "with function a>b" do
      describe "#generate(x)" do
        it "should equal 'aaaaaaab' at zero iteration" do
          Grammar.new('aaaaaaab', {'b' => 'a', 'a>b' => 'b'}).generate(0).should == 'aaaaaaab'
        end
        it "should equal 'aaaaaba' after one iteration" do
          Grammar.new('aaaaaab', {'b' => 'a', 'a>b' => 'b'}).generate(1).should == 'aaaaaba'
        end
        it "should equal 'aaaabaa' after one iteration" do
          Grammar.new('aaaaaba', {'b' => 'a', 'a>b' => 'b'}).generate(1).should == 'aaaabaa'
        end
        it "should equal 'aaaabaa' after two iterations" do
          Grammar.new('aaaaaab', {'b' => 'a', 'a>b' => 'b'}).generate(2).should == 'aaaabaa'
        end
      end
    end
  end
end

Here is the revised library code:-
######################################
# cs_grammar.rb a context sensitive
# 1-L lsystem grammar for 
# ruby/ruby-processing
# by Martin Prout (January 2013)
######################################



##################################
# The grammar class stores rules
# in two Hashes, one for cs rules,
# one for context free rules. Rules
# are filtered on input, and context
# is checked using get_rule in production
##################################

class Grammar
  attr_accessor :lh_context
  attr_reader :axiom, :context, :no_context, :idx, :ignore, :lh_buf
  def initialize(axiom, rules, ignore = '')
    @axiom = axiom
    @no_context = {}
    @context = {}
    @ignore = ignore
    @lh_context = false
    rules.each_pair do |pair|
      add_rule pair[0], pair[1]
    end
  end

  def add_rule(pre, rule)
    if pre.length == 3
      if pre[1] == '<'
        @lh_context = true
        @context[pre[2]] = pre
      elsif pre[1] == '>'
        @context[pre[0]] = pre
      end
      @no_context[pre] = rule # key length == 3
    elsif pre.length == 1
      @no_context[pre] = rule # key length == 1
    else
      print "unrecognized grammar '#{pre}'"
    end
  end

  def generate(repeat = 0) # repeat iteration grammar rules
    prod = axiom
    repeat.times do
      prod = new_production(prod)
    end
    return prod
  end


  def new_production prod  # single iteration grammar rules
    @idx = 0
    @lh_buf = [] if lh_context  # create buffer to store lefthand context
    prod.gsub!(/./) do |ch|
      if lh_context
        lh_buf << ch unless ignore.include? ch # store lh context
      end
      get_rule(prod, ch)
    end
  end

  def get_rule prod, ch
    rule = ch # default is return original character as rule (no change)
    @idx += 1 # increment the index of axiom/production as a side effect
    if (context.has_key?(ch))
      if context[ch][1] == '<'
        cs_char = context[ch][0]
        rule = no_context[context[ch]] if cs_char == lh_buf[lh_buf.length - 2] # use context lh sensitive rule
      elsif context[ch][1] == '>'
        cs_char = context[ch][2]
        rule = no_context[context[ch]] if cs_char == get_rh_context(prod[idx .. prod.length]) # use context rh sensitive rule
      end
    else
      rule = no_context[ch] if no_context.has_key?(ch) # context free rule if it exists
    end
    return rule
  end

  def get_rh_context prod
    index = 0
    if ignore != ''
      while (ignore.include? prod[index])
        index += 1
      end
    end
    return prod[index]
  end
end


Thursday, 14 January 2010

Refactoring my Penrose Snowflake

Dan Mayer over at devver.net has kindly analysed and re-factored my penrose_snowflake with caliper (something I've never heard of but then I'm not really in the community, just ploughing my own furrow). He also extended my code in an interesting way to produce a "snowflake screensaver" which labours a bit.
Well I tried out Dans refactoring on my own code (which I had already changed to have a separate grammar library).  Here is my revised penrose_snowflake.rb (requires grammar.rb and runs with penrose.rb) incorporating some additional refactoring, which you may or may not like depending on your view of the ternary operator (my multiplier has fewer lines of code and is 4% faster according to my benchmark test). However I have since come up with a different version which I have now used to replace my original posting!!! I think the new version will come into its own for my bracketed L-Systems and my Island fractal which is next on the list for refactoring.



class PenroseSnowflake
  include Processing::Proxy

  attr_accessor :axiom, :grammar, :start_length, :theta, :production, :draw_length,
    :repeats, :xpos, :ypos
  DELTA = (Math::PI/180) * 18.0 # convert degrees to radians

  def initialize
    @axiom = "F3-F3-F3-F3-F"
    @grammar = Grammar.new axiom
    grammar.add_rule "F", "F3-F3-F45-F++F3-F"
    @start_length = 450.0
    @theta = 0
    @xpos = width * 0.8
    @ypos = height * 0.95
    @production = axiom
    @draw_length = start_length
  end

  ##############################################################################
  # Not strictly in the spirit of either processing or L-Systems in my iterate
  # function I have ignored the processing translate/rotate functions in favour
  # of the direct calculation of the new x and y positions, thus avoiding such
  # affine transformations.
  ##############################################################################

  def render()
    repeats = 1
    production.each_char do |element|
      case element
      when 'F'
        line(xpos, ypos, (@xpos -= multiplier(repeats, :cos)), (@ypos += multiplier(repeats, :sin)))
        repeats = 1
      when '+'
        @theta += DELTA * repeats
        repeats = 1
      when '-'
        @theta -= DELTA * repeats
        repeats = 1
      when '3', '4', '5'
        repeats += Integer(element)
      else
        puts "Character '#{element}' is not in grammar"
      end
    end
  end

  ##############################
  # create grammar from axiom and
  # rules (adjust scale)
  ##############################

  def create_grammar(gen)
    @draw_length *= 0.4**gen
    @production = grammar.generate gen
  end

  def multiplier(repeats, type)
    value = draw_length * repeats
    (type == :cos)?  value * Math.cos(theta) : value *  Math.sin(theta)
  end
end

Followers

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