gene 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|