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/lib/generator.rb
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
class Generator < Dsl
|
2
|
+
include Aligner
|
3
|
+
|
4
|
+
DEFAULT_XOVER_FREQ = 0.1
|
5
|
+
DEFAULT_MUTATION_FREQ = 0.25
|
6
|
+
|
7
|
+
attr_accessor :current_sequence
|
8
|
+
attr_reader :cells, :gene_map, :fitness_map, :xover_freq, :mutation_freq
|
9
|
+
|
10
|
+
def initialize(cell_1, cell_2)
|
11
|
+
@cells = (@cell_1, @cell_2 = cell_1, cell_2)
|
12
|
+
|
13
|
+
@gene_map = align_cells
|
14
|
+
@fitness_map = cells.map(&:fitness)
|
15
|
+
@current_sequence = rand(2)
|
16
|
+
super
|
17
|
+
end
|
18
|
+
|
19
|
+
def combine
|
20
|
+
Cell.new(&configuration)
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def finish_init
|
26
|
+
@xover_freq ||= DEFAULT_XOVER_FREQ
|
27
|
+
@mutation_freq ||= DEFAULT_MUTATION_FREQ
|
28
|
+
end
|
29
|
+
|
30
|
+
def align_cells
|
31
|
+
alignment_for = align_crossover
|
32
|
+
|
33
|
+
[
|
34
|
+
@cell_1.genes_from_alignment_map(alignment_for[:cell_1]),
|
35
|
+
@cell_2.genes_from_alignment_map(alignment_for[:cell_2])
|
36
|
+
]
|
37
|
+
end
|
38
|
+
|
39
|
+
def configuration
|
40
|
+
lambda do
|
41
|
+
num_genes.times.each do |index|
|
42
|
+
send(:"gene_#{index}", &new_gene_from(gene_map[read_sequence][index]))
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def new_gene_from(model_gene)
|
48
|
+
lambda do
|
49
|
+
num_points.times.each do |index|
|
50
|
+
[:x, :y].each do |axis|
|
51
|
+
send(:"point_#{index}_#{axis}", &new_trait_from(model_gene.polygon.points[index].send(axis)))
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
model_gene.color.each_pair do |channel, model_trait|
|
56
|
+
send(:"trait_#{channel}", &new_trait_from(model_trait))
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def new_trait_from(model_trait)
|
62
|
+
lambda do
|
63
|
+
set_value mutate(model_trait)
|
64
|
+
deviate_from fitness_map[current_sequence]
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def mutate(trait)
|
69
|
+
rand(0) < mutation_freq ? trait.mutated_value : trait.value
|
70
|
+
end
|
71
|
+
|
72
|
+
def read_sequence
|
73
|
+
rand(0) < xover_freq ? self.current_sequence ^= 1 : current_sequence
|
74
|
+
end
|
75
|
+
|
76
|
+
def method_missing(name, *args, &block)
|
77
|
+
case name.to_s
|
78
|
+
when "set_xover_freq": @xover_freq = args.first
|
79
|
+
when "set_mutation_freq": @mutation_freq = args.first
|
80
|
+
else super
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
data/lib/geometry.rb
ADDED
@@ -0,0 +1,64 @@
|
|
1
|
+
module Geometry
|
2
|
+
def hulled_sequence
|
3
|
+
format_points_in(hull)
|
4
|
+
end
|
5
|
+
|
6
|
+
private
|
7
|
+
|
8
|
+
def hull
|
9
|
+
ensure_hullable_with(polygon)
|
10
|
+
|
11
|
+
list_of_points = polygon.sort_by { |point| point.x.value }
|
12
|
+
left_point = list_of_points.shift
|
13
|
+
right_point = list_of_points.pop
|
14
|
+
determinant = determinant_function(left_point, right_point)
|
15
|
+
|
16
|
+
lower_list, upper_list = [left_point], [left_point]
|
17
|
+
lower_hull, upper_hull = [], []
|
18
|
+
|
19
|
+
partition_points_from(list_of_points, lower_list, upper_list, determinant)
|
20
|
+
|
21
|
+
trim_hull(lower_hull, lower_list << right_point, true)
|
22
|
+
trim_hull(upper_hull, upper_list << right_point, false)
|
23
|
+
|
24
|
+
lower_hull + upper_hull.reverse[1..-2]
|
25
|
+
end
|
26
|
+
|
27
|
+
def ensure_hullable_with(points)
|
28
|
+
assert_at_least 3, points.length, "Can not calculate the convex hull unless there are at least 3 points (#{points.size} #{points.size == 1 ? 'was' : 'were'} provided)"
|
29
|
+
end
|
30
|
+
|
31
|
+
def partition_points_from(list_of_points, lower_list, upper_list, determinant_lambda)
|
32
|
+
until list_of_points.empty?
|
33
|
+
point = list_of_points.shift
|
34
|
+
(determinant_lambda[point] < 0 ? lower_list : upper_list) << point
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def trim_hull(hull, list, xor_boolean)
|
39
|
+
until list.empty?
|
40
|
+
hull << list.shift
|
41
|
+
while hull.size >= 3 && !convex?(hull.last(3), xor_boolean)
|
42
|
+
hull[-2] = hull[-1]
|
43
|
+
hull.pop
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def determinant_function(point_1, point_2)
|
49
|
+
# This is a good candidate for functionals.rb apply_head.
|
50
|
+
lambda { |pivot| multiply_difference(point_1, point_2, pivot) - multiply_difference(pivot, point_2, point_1) }
|
51
|
+
end
|
52
|
+
|
53
|
+
def multiply_difference(point_1, point_2, point_3)
|
54
|
+
(point_1.x.value - point_2.x.value) * (point_3.y.value - point_2.y.value)
|
55
|
+
end
|
56
|
+
|
57
|
+
def convex?(points, xor_boolean)
|
58
|
+
(determinant_function(points[0], points[2])[points[1]] > 0) ^ xor_boolean
|
59
|
+
end
|
60
|
+
|
61
|
+
def format_points_in(array)
|
62
|
+
array.inject([]) { |list, point| list << point.x.value << point.y.value }
|
63
|
+
end
|
64
|
+
end
|
data/lib/hungarian.rb
ADDED
@@ -0,0 +1,205 @@
|
|
1
|
+
# Written by: Evan Senter
|
2
|
+
#
|
3
|
+
# - - - - - U S A G E - - - - -
|
4
|
+
#
|
5
|
+
# hungarian = Hungarian.new(matrix_to_solve)
|
6
|
+
# solution = hungarian.solve
|
7
|
+
#
|
8
|
+
# ...or...
|
9
|
+
#
|
10
|
+
# hungarian = Hungarian.new
|
11
|
+
# solution = hungarian.solve(matrix_to_solve)
|
12
|
+
#
|
13
|
+
# The method solve returns an array of cell locations such that the assignment problem is solved.
|
14
|
+
# See http://en.wikipedia.org/wiki/Hungarian_algorithm for more information on this problem.
|
15
|
+
#
|
16
|
+
# Adapted from: http://www.public.iastate.edu/~ddoty/HungarianAlgorithm.html
|
17
|
+
|
18
|
+
class Hungarian
|
19
|
+
EMPTY = 0
|
20
|
+
STAR = 1
|
21
|
+
PRIME = 2
|
22
|
+
|
23
|
+
def initialize(matrix = nil); setup(matrix) if matrix; end
|
24
|
+
|
25
|
+
def solve(matrix = nil)
|
26
|
+
setup(matrix) if matrix
|
27
|
+
raise(ArgumentError, "You must provide a matrix to solve.") unless @matrix
|
28
|
+
|
29
|
+
method = :minimize_rows
|
30
|
+
while method != :finished
|
31
|
+
method = self.send(*method)
|
32
|
+
end
|
33
|
+
|
34
|
+
return assignment
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
# - - - - - M A I N S U B M E T H O D S - - - - -
|
40
|
+
|
41
|
+
def minimize_rows
|
42
|
+
# For each row of the matrix, find the smallest element and subtract it from every element in its row.
|
43
|
+
|
44
|
+
@matrix.map! do |row|
|
45
|
+
min_value = row.min
|
46
|
+
row.map { |element| element - min_value }
|
47
|
+
end
|
48
|
+
|
49
|
+
return :star_zeroes
|
50
|
+
end
|
51
|
+
|
52
|
+
def star_zeroes
|
53
|
+
# Find a zero (Z) in the resulting matrix. If there is no starred zero in its row or column, star Z. Repeat for each element in the matrix.
|
54
|
+
|
55
|
+
traverse_indices do |row, column|
|
56
|
+
if @matrix[row][column].zero? && !location_covered?(row, column)
|
57
|
+
@mask[row][column] = STAR
|
58
|
+
cover_cell(row, column)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
reset_covered_hash
|
62
|
+
|
63
|
+
return :mask_columns
|
64
|
+
end
|
65
|
+
|
66
|
+
def mask_columns
|
67
|
+
# Cover each column containing a starred zero. If all columns are covered, the starred zeros describe a complete set of unique assignments.
|
68
|
+
|
69
|
+
index_range.each do |index|
|
70
|
+
@covered[:columns][index] = true if column_mask_values_for(index).any? { |value| value == STAR }
|
71
|
+
end
|
72
|
+
|
73
|
+
return @covered[:columns].all? ? :finished : :prime_zeroes
|
74
|
+
end
|
75
|
+
|
76
|
+
def prime_zeroes
|
77
|
+
# Find a noncovered zero and prime it. If there is no starred zero in the row containing this primed zero, call augment_path.
|
78
|
+
# Otherwise, cover this row and uncover the column containing the starred zero. Continue in this manner until there are no uncovered zeros left
|
79
|
+
# and call adjust_matrix.
|
80
|
+
|
81
|
+
while (row, column = find_uncovered_zero) != [-1, -1]
|
82
|
+
@mask[row][column] = PRIME
|
83
|
+
|
84
|
+
if star_loc_in_row = row_mask_values_for(row).index(STAR)
|
85
|
+
@covered[:rows][row] = true
|
86
|
+
@covered[:columns][star_loc_in_row] = false
|
87
|
+
else
|
88
|
+
return :augment_path, row, column
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
return :adjust_matrix
|
93
|
+
end
|
94
|
+
|
95
|
+
def augment_path(starting_row, starting_column)
|
96
|
+
# Construct a series of alternating primed and starred zeroes. Let Z0 represent the uncovered primed zero passed in.
|
97
|
+
# Let Z1 denote the starred zero in the column of Z0 (if any). Let Z2 denote the primed zero in the row of Z1 (there will always be one).
|
98
|
+
# Continue until the series terminates at a primed zero that has no starred zero in its column. Unstar each starred zero of the series,
|
99
|
+
# star each primed zero of the series, erase all primes and uncover every line in the matrix, and call mask_columns.
|
100
|
+
|
101
|
+
path = [[starting_row, starting_column]]
|
102
|
+
path.instance_eval do
|
103
|
+
def previous_row; self.last[0]; end
|
104
|
+
def previous_column; self.last[1]; end
|
105
|
+
end
|
106
|
+
|
107
|
+
loop do
|
108
|
+
if row_containing_star = column_mask_values_for(path.previous_column).index(STAR)
|
109
|
+
path << [row_containing_star, path.previous_column]
|
110
|
+
else
|
111
|
+
break
|
112
|
+
end
|
113
|
+
|
114
|
+
col_containing_prime = row_mask_values_for(path.previous_row).index(PRIME)
|
115
|
+
path << [path.previous_row, col_containing_prime]
|
116
|
+
end
|
117
|
+
|
118
|
+
update_elements_in(path)
|
119
|
+
traverse_indices { |row, column| @mask[row][column] = EMPTY if @mask[row][column] == PRIME }
|
120
|
+
reset_covered_hash
|
121
|
+
|
122
|
+
return :mask_columns
|
123
|
+
end
|
124
|
+
|
125
|
+
def adjust_matrix
|
126
|
+
# Add the smallest value to every element of each covered row, subtract it from every element of each uncovered column, and call prime_zeroes.
|
127
|
+
|
128
|
+
smallest_value = nil
|
129
|
+
traverse_indices do |row, column|
|
130
|
+
if !location_covered?(row, column) && (smallest_value.nil? || @matrix[row][column] < smallest_value)
|
131
|
+
smallest_value = @matrix[row][column]
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
indices_of_covered_rows.each { |index| @matrix[index].map! { |value| value + smallest_value } }
|
136
|
+
|
137
|
+
covered_columns = indices_of_uncovered_columns
|
138
|
+
index_range.each { |row| covered_columns.each { |column| @matrix[row][column] -= smallest_value } }
|
139
|
+
|
140
|
+
return :prime_zeroes
|
141
|
+
end
|
142
|
+
|
143
|
+
# - - - - - H E L P E R M E T H O D S - - - - -
|
144
|
+
|
145
|
+
def setup(matrix)
|
146
|
+
@matrix = matrix
|
147
|
+
@length = @matrix.length
|
148
|
+
@mask = Array.new(@length) { Array.new(@length, EMPTY) } # 2D array of constants (listed above)
|
149
|
+
@covered = { :rows => Array.new(@length, false), :columns => Array.new(@length, false) } # Boolean arrays
|
150
|
+
end
|
151
|
+
|
152
|
+
def assignment
|
153
|
+
index_range.inject([]) { |path, row_index| path << [row_index, @mask[row_index].index(STAR)] }
|
154
|
+
end
|
155
|
+
|
156
|
+
def update_elements_in(path)
|
157
|
+
path.each do |cell|
|
158
|
+
@mask[cell[0]][cell[1]] = case @mask[cell[0]][cell[1]]
|
159
|
+
when STAR then EMPTY
|
160
|
+
when PRIME then STAR
|
161
|
+
end
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
def find_uncovered_zero
|
166
|
+
traverse_indices do |row, column|
|
167
|
+
return [row, column] if @matrix[row][column].zero? && !location_covered?(row, column)
|
168
|
+
end
|
169
|
+
[-1, -1]
|
170
|
+
end
|
171
|
+
|
172
|
+
def cover_cell(row, column)
|
173
|
+
@covered[:rows][row] = @covered[:columns][column] = true
|
174
|
+
end
|
175
|
+
|
176
|
+
def reset_covered_hash
|
177
|
+
@covered.values.each { |cover| cover.fill(false) }
|
178
|
+
end
|
179
|
+
|
180
|
+
def location_covered?(row, column)
|
181
|
+
@covered[:rows][row] || @covered[:columns][column]
|
182
|
+
end
|
183
|
+
|
184
|
+
def row_mask_values_for(row)
|
185
|
+
index_range.map { |column| @mask[row][column] }
|
186
|
+
end
|
187
|
+
|
188
|
+
def column_mask_values_for(column)
|
189
|
+
index_range.map { |row| @mask[row][column] }
|
190
|
+
end
|
191
|
+
|
192
|
+
def indices_of_covered_rows
|
193
|
+
index_range.select { |index| @covered[:rows][index] }
|
194
|
+
end
|
195
|
+
|
196
|
+
def indices_of_uncovered_columns
|
197
|
+
index_range.select { |index| !@covered[:columns][index] }
|
198
|
+
end
|
199
|
+
|
200
|
+
def traverse_indices(&block)
|
201
|
+
index_range.each { |row| index_range.each { |column| yield row, column } }
|
202
|
+
end
|
203
|
+
|
204
|
+
def index_range; 0...@length; end
|
205
|
+
end
|
data/lib/imagine.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
module Imagine
|
2
|
+
def compare_image_to(image)
|
3
|
+
# http://en.wikipedia.org/wiki/RMSD
|
4
|
+
aggregate_deviation = 0.0
|
5
|
+
|
6
|
+
target_image.each_pixel do |target_pixel, x, y|
|
7
|
+
aggregate_deviation += compare_pixels(target_pixel, image.pixel_color(x, y))
|
8
|
+
end
|
9
|
+
|
10
|
+
1 - Math.sqrt(aggregate_deviation / (Magick::MaxRGB ** 2 * image_dimensions.x * image_dimensions.y * 3))
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
|
15
|
+
def compare_pixels(target_pixel, actual_pixel)
|
16
|
+
[:red, :green, :blue].inject(0.0) { |distance, channel| distance + compare_channel(channel, target_pixel, actual_pixel) }
|
17
|
+
end
|
18
|
+
|
19
|
+
def compare_channel(name, target_pixel, actual_pixel)
|
20
|
+
(target_pixel.send(name) - actual_pixel.send(name)) ** 2
|
21
|
+
end
|
22
|
+
end
|
data/lib/petri.rb
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
class Petri < Dsl
|
2
|
+
include Imagine
|
3
|
+
|
4
|
+
attr_reader :round, :target_image, :dish
|
5
|
+
|
6
|
+
def initialize(image_path)
|
7
|
+
@round = 0
|
8
|
+
prepare_image_at(image_path)
|
9
|
+
super
|
10
|
+
end
|
11
|
+
|
12
|
+
def evolve
|
13
|
+
top_k = dish[0, dish.length / 3 * 2]
|
14
|
+
@dish = (top_k + (dish.length - top_k.length).times.inject([]) do |new_cells, index|
|
15
|
+
new_cells << Generator.new(top_k[rand(top_k.length)], top_k[rand(top_k.length)]).combine
|
16
|
+
end)
|
17
|
+
|
18
|
+
calculate_fitnesses
|
19
|
+
sort_by_fitness!
|
20
|
+
|
21
|
+
if (round % 100).zero?
|
22
|
+
dish.first.image.write("#{round}.png")
|
23
|
+
puts dish.first.genes.map { |gene| "[" + gene.polygon.map { |point| "#{point.x.value}, #{point.y.value}" }.join(", ") + "]" }.join(", ")
|
24
|
+
puts "Fitness: #{dish.first.fitness}"
|
25
|
+
end
|
26
|
+
|
27
|
+
next_round
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def prepare_image_at(image_path)
|
33
|
+
@target_image = Magick::Image.read(image_path).first
|
34
|
+
set_parameter(:image_dimensions, Point.new(target_image.columns, target_image.rows))
|
35
|
+
end
|
36
|
+
|
37
|
+
def finish_init
|
38
|
+
set_parameter(:num_cells, 6) unless self.class.respond_to?(:num_cells)
|
39
|
+
set_parameter(:num_genes, 3) unless self.class.respond_to?(:num_genes)
|
40
|
+
set_parameter(:num_points, 4) unless self.class.respond_to?(:num_points)
|
41
|
+
|
42
|
+
fill_out_cells
|
43
|
+
calculate_fitnesses
|
44
|
+
sort_by_fitness!
|
45
|
+
end
|
46
|
+
|
47
|
+
def fill_out_cells
|
48
|
+
@dish = num_cells.times.map { Cell.new }
|
49
|
+
end
|
50
|
+
|
51
|
+
def calculate_fitnesses
|
52
|
+
dish.each do |cell|
|
53
|
+
cell.fitness ||= compare_image_to(cell.image)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def sort_by_fitness!
|
58
|
+
dish.sort! { |a, b| b.fitness <=> a.fitness }
|
59
|
+
end
|
60
|
+
|
61
|
+
def next_round
|
62
|
+
@round += 1
|
63
|
+
end
|
64
|
+
|
65
|
+
def set_num_cells(desired)
|
66
|
+
set_parameter(:num_cells, (desired % 3) == 0 ? desired : desired + 3 - (desired % 3))
|
67
|
+
end
|
68
|
+
|
69
|
+
def set_parameter(name, value)
|
70
|
+
name = name.to_s.gsub(/^set_/, "")
|
71
|
+
|
72
|
+
class_metaclass.instance_eval do
|
73
|
+
attr_reader name
|
74
|
+
end
|
75
|
+
|
76
|
+
self.class.instance_variable_set(:"@#{name}", value)
|
77
|
+
end
|
78
|
+
|
79
|
+
def method_missing(name, *args, &block)
|
80
|
+
case name
|
81
|
+
when :set_num_genes, :set_num_points: set_parameter(name, args.first)
|
82
|
+
else super
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|