gene 0.0.1 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/Manifest +46 -0
- data/Rakefile +12 -9
- data/TODO +7 -0
- data/gene.gemspec +34 -0
- data/initializers/functional_extensions.rb +44 -0
- data/initializers/module_extensions.rb +14 -0
- data/initializers/object_extensions.rb +25 -0
- data/initializers/range_extensions.rb +9 -0
- data/initializers/runner.rb +15 -0
- data/initializers/symbol_extensions.rb +11 -0
- data/initializers/unbound_method_extensions.rb +3 -0
- data/lib/aligner.rb +34 -0
- data/lib/calculator.rb +42 -0
- data/lib/cell.rb +41 -0
- data/lib/color.rb +9 -0
- data/lib/dsl.rb +16 -0
- data/lib/gene.rb +67 -3
- data/lib/generator.rb +83 -0
- data/lib/geometry.rb +64 -0
- data/lib/hungarian.rb +205 -0
- data/lib/imagine.rb +22 -0
- data/lib/petri.rb +85 -0
- data/lib/point.rb +1 -0
- data/lib/trait.rb +60 -0
- data/tasks/test.rake +23 -0
- data/test/assets/Nova.jpg +0 -0
- data/test/assets/Rex.jpg +0 -0
- data/test/assets/Squares.jpg +0 -0
- data/test/test_helper.rb +6 -0
- data/test/unit/aligner_test.rb +91 -0
- data/test/unit/calculator_test.rb +100 -0
- data/test/unit/cell_test.rb +64 -0
- data/test/unit/color_test.rb +23 -0
- data/test/unit/dsl_test.rb +45 -0
- data/test/unit/functionals_extensions_test.rb +51 -0
- data/test/unit/gene_test.rb +76 -0
- data/test/unit/generator_test.rb +76 -0
- data/test/unit/geometry_test.rb +57 -0
- data/test/unit/hungarian_test.rb +196 -0
- data/test/unit/imagine_test.rb +54 -0
- data/test/unit/module_extensions_test.rb +40 -0
- data/test/unit/object_extensions_test.rb +34 -0
- data/test/unit/petri_test.rb +87 -0
- data/test/unit/range_extensions_test.rb +29 -0
- data/test/unit/symbol_extensions_test.rb +18 -0
- data/test/unit/trait_test.rb +97 -0
- data/test/unit/unbound_method_extensions_test.rb +11 -0
- metadata +118 -30
- data/History.txt +0 -6
- data/Manifest.txt +0 -7
- data/README.txt +0 -48
- data/bin/gene +0 -3
- data/test/test_gene.rb +0 -8
data/Manifest
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
Manifest
|
2
|
+
Rakefile
|
3
|
+
TODO
|
4
|
+
initializers/functional_extensions.rb
|
5
|
+
initializers/module_extensions.rb
|
6
|
+
initializers/object_extensions.rb
|
7
|
+
initializers/range_extensions.rb
|
8
|
+
initializers/runner.rb
|
9
|
+
initializers/symbol_extensions.rb
|
10
|
+
initializers/unbound_method_extensions.rb
|
11
|
+
lib/aligner.rb
|
12
|
+
lib/calculator.rb
|
13
|
+
lib/cell.rb
|
14
|
+
lib/color.rb
|
15
|
+
lib/dsl.rb
|
16
|
+
lib/gene.rb
|
17
|
+
lib/generator.rb
|
18
|
+
lib/geometry.rb
|
19
|
+
lib/hungarian.rb
|
20
|
+
lib/imagine.rb
|
21
|
+
lib/petri.rb
|
22
|
+
lib/point.rb
|
23
|
+
lib/trait.rb
|
24
|
+
tasks/test.rake
|
25
|
+
test/assets/Nova.jpg
|
26
|
+
test/assets/Rex.jpg
|
27
|
+
test/assets/Squares.jpg
|
28
|
+
test/test_helper.rb
|
29
|
+
test/unit/aligner_test.rb
|
30
|
+
test/unit/calculator_test.rb
|
31
|
+
test/unit/cell_test.rb
|
32
|
+
test/unit/color_test.rb
|
33
|
+
test/unit/dsl_test.rb
|
34
|
+
test/unit/functionals_extensions_test.rb
|
35
|
+
test/unit/gene_test.rb
|
36
|
+
test/unit/generator_test.rb
|
37
|
+
test/unit/geometry_test.rb
|
38
|
+
test/unit/hungarian_test.rb
|
39
|
+
test/unit/imagine_test.rb
|
40
|
+
test/unit/module_extensions_test.rb
|
41
|
+
test/unit/object_extensions_test.rb
|
42
|
+
test/unit/petri_test.rb
|
43
|
+
test/unit/range_extensions_test.rb
|
44
|
+
test/unit/symbol_extensions_test.rb
|
45
|
+
test/unit/trait_test.rb
|
46
|
+
test/unit/unbound_method_extensions_test.rb
|
data/Rakefile
CHANGED
@@ -1,12 +1,15 @@
|
|
1
|
-
|
1
|
+
require "rubygems"
|
2
|
+
require "rake"
|
3
|
+
require "echoe"
|
2
4
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
5
|
+
Echoe.new("gene", "0.1.0") do |config|
|
6
|
+
config.description = "Sample genetic program in Ruby"
|
7
|
+
config.url = "http://github.com/evansenter/gene"
|
8
|
+
config.author = "Evan Senter"
|
9
|
+
config.email = "evansenter@gmail.com"
|
10
|
+
config.ignore_pattern = ["tmp/*", "script/*"]
|
11
|
+
config.dependencies = ["rmagick"]
|
12
|
+
config.development_dependencies = []
|
10
13
|
end
|
11
14
|
|
12
|
-
#
|
15
|
+
Dir["#{File.dirname(__FILE__)}/tasks/*.rake"].sort.each { |task| load task }
|
data/TODO
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
* Things aren't working because the image is coming out white! (Cell#draw_image)
|
2
|
+
* Reflesh test coverage in Generator
|
3
|
+
* Think about using the garbage collector...
|
4
|
+
|
5
|
+
require "initializers/runner.rb"
|
6
|
+
petri = Petri.new("./test/assets/Squares.jpg")
|
7
|
+
petri.evolve while petri.dish.first.fitness < 0.7
|
data/gene.gemspec
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
Gem::Specification.new do |s|
|
4
|
+
s.name = %q{gene}
|
5
|
+
s.version = "0.1.0"
|
6
|
+
|
7
|
+
s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
|
8
|
+
s.authors = ["Evan Senter"]
|
9
|
+
s.date = %q{2010-07-20}
|
10
|
+
s.description = %q{Sample genetic program in Ruby}
|
11
|
+
s.email = %q{evansenter@gmail.com}
|
12
|
+
s.extra_rdoc_files = ["TODO", "lib/aligner.rb", "lib/calculator.rb", "lib/cell.rb", "lib/color.rb", "lib/dsl.rb", "lib/gene.rb", "lib/generator.rb", "lib/geometry.rb", "lib/hungarian.rb", "lib/imagine.rb", "lib/petri.rb", "lib/point.rb", "lib/trait.rb", "tasks/test.rake"]
|
13
|
+
s.files = ["Manifest", "Rakefile", "TODO", "initializers/functional_extensions.rb", "initializers/module_extensions.rb", "initializers/object_extensions.rb", "initializers/range_extensions.rb", "initializers/runner.rb", "initializers/symbol_extensions.rb", "initializers/unbound_method_extensions.rb", "lib/aligner.rb", "lib/calculator.rb", "lib/cell.rb", "lib/color.rb", "lib/dsl.rb", "lib/gene.rb", "lib/generator.rb", "lib/geometry.rb", "lib/hungarian.rb", "lib/imagine.rb", "lib/petri.rb", "lib/point.rb", "lib/trait.rb", "tasks/test.rake", "test/assets/Nova.jpg", "test/assets/Rex.jpg", "test/assets/Squares.jpg", "test/test_helper.rb", "test/unit/aligner_test.rb", "test/unit/calculator_test.rb", "test/unit/cell_test.rb", "test/unit/color_test.rb", "test/unit/dsl_test.rb", "test/unit/functionals_extensions_test.rb", "test/unit/gene_test.rb", "test/unit/generator_test.rb", "test/unit/geometry_test.rb", "test/unit/hungarian_test.rb", "test/unit/imagine_test.rb", "test/unit/module_extensions_test.rb", "test/unit/object_extensions_test.rb", "test/unit/petri_test.rb", "test/unit/range_extensions_test.rb", "test/unit/symbol_extensions_test.rb", "test/unit/trait_test.rb", "test/unit/unbound_method_extensions_test.rb", "gene.gemspec"]
|
14
|
+
s.homepage = %q{http://github.com/evansenter/gene}
|
15
|
+
s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Gene"]
|
16
|
+
s.require_paths = ["lib"]
|
17
|
+
s.rubyforge_project = %q{gene}
|
18
|
+
s.rubygems_version = %q{1.3.7}
|
19
|
+
s.summary = %q{Sample genetic program in Ruby}
|
20
|
+
s.test_files = ["test/test_helper.rb", "test/unit/aligner_test.rb", "test/unit/calculator_test.rb", "test/unit/cell_test.rb", "test/unit/color_test.rb", "test/unit/dsl_test.rb", "test/unit/functionals_extensions_test.rb", "test/unit/gene_test.rb", "test/unit/generator_test.rb", "test/unit/geometry_test.rb", "test/unit/hungarian_test.rb", "test/unit/imagine_test.rb", "test/unit/module_extensions_test.rb", "test/unit/object_extensions_test.rb", "test/unit/petri_test.rb", "test/unit/range_extensions_test.rb", "test/unit/symbol_extensions_test.rb", "test/unit/trait_test.rb", "test/unit/unbound_method_extensions_test.rb"]
|
21
|
+
|
22
|
+
if s.respond_to? :specification_version then
|
23
|
+
current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
|
24
|
+
s.specification_version = 3
|
25
|
+
|
26
|
+
if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
|
27
|
+
s.add_runtime_dependency(%q<rmagick>, [">= 0"])
|
28
|
+
else
|
29
|
+
s.add_dependency(%q<rmagick>, [">= 0"])
|
30
|
+
end
|
31
|
+
else
|
32
|
+
s.add_dependency(%q<rmagick>, [">= 0"])
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module FunctionalExtensions
|
2
|
+
def apply(enum)
|
3
|
+
enum.map &self
|
4
|
+
end
|
5
|
+
alias | apply
|
6
|
+
|
7
|
+
def reduce(enum)
|
8
|
+
enum.inject &self
|
9
|
+
end
|
10
|
+
alias <= reduce
|
11
|
+
|
12
|
+
def compose(function)
|
13
|
+
if self.respond_to?(:arity) && self.arity == 1
|
14
|
+
lambda { |*args| self[function[*args]] }
|
15
|
+
else
|
16
|
+
lambda { |*args| self[*function[*args]] }
|
17
|
+
end
|
18
|
+
end
|
19
|
+
alias * compose
|
20
|
+
|
21
|
+
def apply_head(*first)
|
22
|
+
lambda { |*rest| self[*first.concat(rest)] }
|
23
|
+
end
|
24
|
+
alias >> apply_head
|
25
|
+
|
26
|
+
def apply_tail(*last)
|
27
|
+
lambda { |*rest| self[*rest.concat(last)] }
|
28
|
+
end
|
29
|
+
alias << apply_tail
|
30
|
+
|
31
|
+
def memoize
|
32
|
+
cache = {}
|
33
|
+
lambda do |*args|
|
34
|
+
unless cache.has_key?(args)
|
35
|
+
cache[args] = self[*args]
|
36
|
+
end
|
37
|
+
cache[args]
|
38
|
+
end
|
39
|
+
end
|
40
|
+
alias +@ memoize
|
41
|
+
end
|
42
|
+
|
43
|
+
class Method; include FunctionalExtensions; end
|
44
|
+
class Proc; include FunctionalExtensions; end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
class Module
|
2
|
+
alias [] instance_method
|
3
|
+
|
4
|
+
def []=(symbol, function)
|
5
|
+
self.instance_eval { define_method(symbol, function) }
|
6
|
+
end
|
7
|
+
|
8
|
+
def alias_method_chain(target, feature)
|
9
|
+
method_name, extension = target.to_s.match(/(\w+)(\W?)/).to_a[1..-1]
|
10
|
+
|
11
|
+
alias_method "#{method_name}_without_#{feature}#{extension}", target
|
12
|
+
alias_method target, "#{method_name}_with_#{feature}#{extension}"
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
class Object
|
2
|
+
def returning(value)
|
3
|
+
yield value
|
4
|
+
value
|
5
|
+
end
|
6
|
+
|
7
|
+
def send_if(condition, method, *arguments)
|
8
|
+
block = arguments.pop if arguments.last.is_a?(Proc) && arguments.length == 1
|
9
|
+
if condition
|
10
|
+
block ? eval("self.#{method} &block") : self.send(method, *arguments)
|
11
|
+
else
|
12
|
+
self
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def assert_at_least(number_required, number_present, message = nil)
|
17
|
+
unless number_present >= number_required
|
18
|
+
raise(ArgumentError, message || "You must provide at least #{number_required} objects (#{number_present} #{number_present == 1 ? 'was' : 'were'} provided)")
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def class_metaclass
|
23
|
+
class << self.class; self; end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# Pull in necessary libraries.
|
2
|
+
require "RMagick"
|
3
|
+
|
4
|
+
# Get all source files except this one, pulling initializers before the lib_files.
|
5
|
+
initializers = Dir.glob(File.join(File.dirname(__FILE__), "**", "*.rb")) - [__FILE__]
|
6
|
+
lib_files = Dir.glob(File.join(File.dirname(__FILE__), "..", "lib", "**", "*.rb"))
|
7
|
+
|
8
|
+
# Set dynamic autoload on all of them based on their filename.
|
9
|
+
constants = (initializers + lib_files).inject([]) do |constants, file_path|
|
10
|
+
constant = File.basename(file_path, ".rb").split(/_/).map(&:capitalize).join.to_sym
|
11
|
+
autoload(constant, file_path)
|
12
|
+
end
|
13
|
+
|
14
|
+
# Pull in the initializer files!
|
15
|
+
initializers.map(&method(:require))
|
data/lib/aligner.rb
ADDED
@@ -0,0 +1,34 @@
|
|
1
|
+
module Aligner
|
2
|
+
def align_crossover
|
3
|
+
optimal_alignment = Hungarian.new(crossover_map).solve
|
4
|
+
|
5
|
+
returning({ :cell_1 => [], :cell_2 => [] }) do |alignment_hash|
|
6
|
+
optimal_alignment.each do |tuple|
|
7
|
+
alignment_hash[:cell_1] << tuple.first
|
8
|
+
alignment_hash[:cell_2] << tuple.last
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def crossover_map
|
16
|
+
@cells.first.genes.map do |gene_1|
|
17
|
+
@cells.last.genes.map do |gene_2|
|
18
|
+
distance_between(middle_point_of(gene_1), middle_point_of(gene_2))
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def distance_between(point_1, point_2)
|
24
|
+
square = lambda { |value| value ** 2 }
|
25
|
+
Math.sqrt(square[point_2.x - point_1.x] + square[point_2.y - point_1.y])
|
26
|
+
end
|
27
|
+
|
28
|
+
def middle_point_of(gene)
|
29
|
+
sum = lambda { |a, b| a + b }
|
30
|
+
average = lambda { |axis| (sum <= gene.polygon.points.map(&axis).map(&:value)) / gene.polygon.num_points.to_f }
|
31
|
+
|
32
|
+
Point.new(average[:x], average[:y])
|
33
|
+
end
|
34
|
+
end
|
data/lib/calculator.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
module Calculator
|
2
|
+
def self.included(base)
|
3
|
+
base.extend ClassMethods
|
4
|
+
end
|
5
|
+
|
6
|
+
module ClassMethods
|
7
|
+
def generate_value(max)
|
8
|
+
case max
|
9
|
+
when 0: raise(ArgumentError, "Trying to call Calculator.generate_value(0), which is not supported at this time due to ambiguity of intention")
|
10
|
+
when Float: rand(0)
|
11
|
+
when Fixnum: rand(max)
|
12
|
+
else raise(ArgumentError, "Can't generate values of type #{max.class} in Calculator.generate_value(max)")
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def get_normal_random_variable
|
17
|
+
x = y = sum = 0
|
18
|
+
loop do
|
19
|
+
x = get_uniform_random_variable
|
20
|
+
y = get_uniform_random_variable
|
21
|
+
break if (sum = (x ** 2) + (y ** 2)) < 1
|
22
|
+
end
|
23
|
+
rand(2).zero? ? convert(x, sum) : convert(y, sum)
|
24
|
+
end
|
25
|
+
|
26
|
+
def get_uniform_random_variable
|
27
|
+
rand(0) * random_sign_change
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def random_sign_change
|
33
|
+
rand(2).zero? ? 1 : -1
|
34
|
+
end
|
35
|
+
|
36
|
+
def convert(variable, sum)
|
37
|
+
# http://en.wikipedia.org/wiki/Marsaglia_polar_method
|
38
|
+
return 0 if sum.zero?
|
39
|
+
variable * Math.sqrt(-2 * Math.log(sum) / sum)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
data/lib/cell.rb
ADDED
@@ -0,0 +1,41 @@
|
|
1
|
+
class Cell < Dsl
|
2
|
+
attr_accessor :fitness
|
3
|
+
attr_reader :genes, :image
|
4
|
+
|
5
|
+
def genes_by_alpha
|
6
|
+
genes.sort { |gene_1, gene_2| gene_2.color.a.value <=> gene_1.color.a.value }
|
7
|
+
end
|
8
|
+
|
9
|
+
def genes_from_alignment_map(alignment)
|
10
|
+
alignment.map { |index| genes[index] }
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def finish_init
|
16
|
+
fill_out_genes
|
17
|
+
draw_image
|
18
|
+
end
|
19
|
+
|
20
|
+
def fill_out_genes
|
21
|
+
@genes = num_genes.times.map { |index| (@genes ||= [])[index] || Gene.new }
|
22
|
+
end
|
23
|
+
|
24
|
+
def draw_image
|
25
|
+
@image = Magick::Image.new(image_dimensions.x, image_dimensions.y)
|
26
|
+
pen = Magick::Draw.new
|
27
|
+
|
28
|
+
genes_by_alpha.each do |gene|
|
29
|
+
pen.fill(gene.color.rgba_format)
|
30
|
+
pen.polygon(*gene.hulled_sequence)
|
31
|
+
pen.draw(@image)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def method_missing(name, *args, &block)
|
36
|
+
case name.to_s
|
37
|
+
when /^gene_(\d+)$/: (@genes ||= [])[$1.to_i] = Gene.new(&block)
|
38
|
+
else super
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
data/lib/color.rb
ADDED
data/lib/dsl.rb
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
class Dsl
|
2
|
+
def initialize(*args, &block)
|
3
|
+
if block_given?
|
4
|
+
@_original_self = block.binding.eval("self")
|
5
|
+
instance_eval(&block)
|
6
|
+
remove_instance_variable :@_original_self
|
7
|
+
end
|
8
|
+
finish_init if defined? :finish_init
|
9
|
+
end
|
10
|
+
|
11
|
+
def method_missing(name, *args, &block)
|
12
|
+
return Petri.send(name) if Petri.respond_to?(name)
|
13
|
+
|
14
|
+
@_original_self ? @_original_self.send(name, *args, &block) : super
|
15
|
+
end
|
16
|
+
end
|
data/lib/gene.rb
CHANGED
@@ -1,3 +1,67 @@
|
|
1
|
-
class Gene
|
2
|
-
|
3
|
-
|
1
|
+
class Gene < Dsl
|
2
|
+
include Geometry
|
3
|
+
|
4
|
+
attr_reader :polygon, :color
|
5
|
+
|
6
|
+
private
|
7
|
+
|
8
|
+
def finish_init
|
9
|
+
fill_out_polygon
|
10
|
+
fill_out_color
|
11
|
+
initialize_singleton_methods_for_polygon_and_color
|
12
|
+
end
|
13
|
+
|
14
|
+
def fill_out_polygon
|
15
|
+
@polygon ||= []
|
16
|
+
@polygon = num_points.times.map do |index|
|
17
|
+
if polygon[index]
|
18
|
+
returning(polygon[index]) do |point|
|
19
|
+
point.x ||= Trait.new(0...image_dimensions.x)
|
20
|
+
point.y ||= Trait.new(0...image_dimensions.y)
|
21
|
+
end
|
22
|
+
else
|
23
|
+
Point.new(
|
24
|
+
Trait.new(0...image_dimensions.x),
|
25
|
+
Trait.new(0...image_dimensions.y)
|
26
|
+
)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def fill_out_color
|
32
|
+
@color ||= Color.new
|
33
|
+
color.each_pair { |channel, trait| color.send(:"#{channel}=", Trait.new(0.0..1.0)) unless trait }
|
34
|
+
end
|
35
|
+
|
36
|
+
def initialize_singleton_methods_for_polygon_and_color
|
37
|
+
:points[polygon] = lambda { self }
|
38
|
+
:num_points[polygon] = lambda { size }
|
39
|
+
end
|
40
|
+
|
41
|
+
def point_at(index, x, y)
|
42
|
+
point = (@polygon ||= [])[index] ||= Point.new
|
43
|
+
|
44
|
+
point.x = Trait.new(0...image_dimensions.x) { set_value x }
|
45
|
+
point.y = Trait.new(0...image_dimensions.y) { set_value y }
|
46
|
+
end
|
47
|
+
|
48
|
+
def point_from(index, name, &block)
|
49
|
+
point = (@polygon ||= [])[index] ||= Point.new
|
50
|
+
|
51
|
+
point.send(:"#{name}=", Trait.new(0...image_dimensions.send(:"#{name}"), &block))
|
52
|
+
end
|
53
|
+
|
54
|
+
def color_from(channel, &block)
|
55
|
+
@color ||= Color.new
|
56
|
+
color.send(:"#{channel}=", Trait.new(0.0..1.0, &block))
|
57
|
+
end
|
58
|
+
|
59
|
+
def method_missing(name, *args, &block)
|
60
|
+
case name.to_s
|
61
|
+
when /^point_(\d+)$/: point_at($1.to_i, *args)
|
62
|
+
when /^point_(\d+)_(\w)$/: point_from($1.to_i, $2, &block)
|
63
|
+
when /^trait_(\w)$/: color_from($1, &block)
|
64
|
+
else super
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|