copycats 0.0.1 → 0.5.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Manifest.txt +16 -0
- data/README.md +247 -1
- data/Rakefile +3 -1
- data/bin/kitty +17 -0
- data/lib/copycats.rb +53 -0
- data/lib/copycats/base32.rb +155 -0
- data/lib/copycats/data.rb +99 -0
- data/lib/copycats/gene.rb +87 -0
- data/lib/copycats/genome.rb +177 -0
- data/lib/copycats/models/kitty.rb +13 -0
- data/lib/copycats/reports/genes.rb +82 -0
- data/lib/copycats/reports/kitty.rb +45 -0
- data/lib/copycats/reports/mix.rb +45 -0
- data/lib/copycats/reports/traits.rb +122 -0
- data/lib/copycats/schema.rb +23 -0
- data/lib/copycats/tool.rb +66 -0
- data/lib/copycats/traits.rb +298 -0
- data/lib/copycats/version.rb +2 -2
- data/test/test_base32.rb +77 -0
- data/test/test_genome.rb +92 -0
- data/test/test_mixgenes.rb +63 -0
- metadata +53 -6
@@ -0,0 +1,99 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
|
4
|
+
## load all *.file in data folder
|
5
|
+
|
6
|
+
|
7
|
+
|
8
|
+
def find_datafiles( root='.' )
|
9
|
+
files = []
|
10
|
+
## todo/check: include all subfolders - why? why not?
|
11
|
+
Dir.glob( root + '/**/*.csv' ).each do |file|
|
12
|
+
files << file
|
13
|
+
end
|
14
|
+
files
|
15
|
+
end
|
16
|
+
|
17
|
+
|
18
|
+
def connect( config={} )
|
19
|
+
if config.empty?
|
20
|
+
puts "ENV['DATBASE_URL'] - >#{ENV['DATABASE_URL']}<"
|
21
|
+
|
22
|
+
### change default to ./copycats.db ?? why? why not?
|
23
|
+
db = URI.parse( ENV['DATABASE_URL'] || 'sqlite3:///kitties.db' )
|
24
|
+
|
25
|
+
if db.scheme == 'postgres'
|
26
|
+
config = {
|
27
|
+
adapter: 'postgresql',
|
28
|
+
host: db.host,
|
29
|
+
port: db.port,
|
30
|
+
username: db.user,
|
31
|
+
password: db.password,
|
32
|
+
database: db.path[1..-1],
|
33
|
+
encoding: 'utf8'
|
34
|
+
}
|
35
|
+
else # assume sqlite3
|
36
|
+
config = {
|
37
|
+
adapter: db.scheme, # sqlite3
|
38
|
+
database: db.path[1..-1] # world.db (NB: cut off leading /, thus 1..-1)
|
39
|
+
}
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
puts "Connecting to db using settings: "
|
44
|
+
pp config
|
45
|
+
ActiveRecord::Base.establish_connection( config )
|
46
|
+
# ActiveRecord::Base.logger = Logger.new( STDOUT )
|
47
|
+
end
|
48
|
+
|
49
|
+
|
50
|
+
def setup_in_memory_db
|
51
|
+
# Database Setup & Config
|
52
|
+
|
53
|
+
## ActiveRecord::Base.logger = Logger.new( STDOUT )
|
54
|
+
## ActiveRecord::Base.colorize_logging = false - no longer exists - check new api/config setting?
|
55
|
+
|
56
|
+
connect( adapter: 'sqlite3',
|
57
|
+
database: ':memory:' )
|
58
|
+
|
59
|
+
## build schema
|
60
|
+
CreateDb.new.up
|
61
|
+
end # setup_in_memory_db (using SQLite :memory:)
|
62
|
+
|
63
|
+
|
64
|
+
|
65
|
+
def setup( data_dir: './data' )
|
66
|
+
files = find_datafiles( data_dir )
|
67
|
+
pp files
|
68
|
+
|
69
|
+
## check if files found
|
70
|
+
|
71
|
+
## setup sqlite in-memory db
|
72
|
+
setup_in_memory_db()
|
73
|
+
|
74
|
+
|
75
|
+
## add / read / load all datafiles
|
76
|
+
files.each_with_index do |file,i|
|
77
|
+
|
78
|
+
puts "== #{i+1}/#{files.size} reading datafile '#{file}'..."
|
79
|
+
|
80
|
+
kitties = CSV.read( file, headers:true )
|
81
|
+
pp kitties.headers
|
82
|
+
|
83
|
+
## note: for now use first 5 rows for testing
|
84
|
+
## kitties[0..4].each do |row|
|
85
|
+
kitties.each do |row|
|
86
|
+
## puts row['id'] + '|' + row['gen'] + '|' + row['genes_kai']
|
87
|
+
k = Copycats::Model::Kitty.new
|
88
|
+
k.id = row['id'].to_i
|
89
|
+
k.gen = row['gen'].to_i
|
90
|
+
k.genes = row['genes_kai']
|
91
|
+
## pp k
|
92
|
+
|
93
|
+
## print ids for progress report - why? why not?
|
94
|
+
print "#{k.id}."
|
95
|
+
k.save!
|
96
|
+
end
|
97
|
+
print "\n"
|
98
|
+
end
|
99
|
+
end
|
@@ -0,0 +1,87 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
class Gene
|
4
|
+
|
5
|
+
attr_reader :d, :r1, :r2, :r3
|
6
|
+
# d (dominant gene) -- todo/check: rename to just d instead of d0 - why? why not?
|
7
|
+
# r1 (1st order recessive gene)
|
8
|
+
# r2 (2nd order recessive gene)
|
9
|
+
# r3 (3rd order recessive gene)
|
10
|
+
|
11
|
+
def initialize( arg )
|
12
|
+
## (always) assume String in base32/kai for now
|
13
|
+
kai = arg
|
14
|
+
## puts "Gene.initialize #{kai}"
|
15
|
+
kai = kai.reverse
|
16
|
+
@d = kai[0]
|
17
|
+
@r1 = kai[1]
|
18
|
+
@r2 = kai[2]
|
19
|
+
@r3 = kai[3]
|
20
|
+
end
|
21
|
+
|
22
|
+
def to_kai() @r3 + @r2 + @r1 + @d; end ## return a string in kai/base32 notation
|
23
|
+
|
24
|
+
def swap
|
25
|
+
puts "Gene#swap"
|
26
|
+
kai = to_kai.reverse # note: use reverse kai string (kai[0] is first char/digit/letter)
|
27
|
+
|
28
|
+
3.downto(1) do |i|
|
29
|
+
if Lottery.rand(100) < 25
|
30
|
+
puts " bingo! swap #{i}<>#{i-1}"
|
31
|
+
kai[i-1], kai[i] = kai[i], kai[i-1]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
Gene.new( kai.reverse ) ## note: do NOT forget to pass in kai (unreversed)
|
35
|
+
end
|
36
|
+
|
37
|
+
|
38
|
+
def mutate( other )
|
39
|
+
puts "Gene#mutate"
|
40
|
+
|
41
|
+
gene1 = KAI_TO_INT[d] ## dominant gene1
|
42
|
+
gene2 = KAI_TO_INT[other.d] ## dominant gene2
|
43
|
+
if gene1 > gene2
|
44
|
+
gene1, gene2 = gene2, gene1 ## make sure gene2 is always bigger
|
45
|
+
end
|
46
|
+
if (gene2 - gene1) == 1 && gene1.even?
|
47
|
+
probability = 25
|
48
|
+
probability /= 2 if gene1 > 23
|
49
|
+
if Lottery.rand(100) < probability
|
50
|
+
genex = (gene1 / 2) + 16 ## 16=2^4
|
51
|
+
puts " bingo! mutation #{gene2}+#{gene1} => #{genex}"
|
52
|
+
puts " #{Kai::ALPHABET[gene2]}+#{Kai::ALPHABET[gene1]} => #{Kai::ALPHABET[genex]}"
|
53
|
+
return Kai::ALPHABET[genex]
|
54
|
+
end
|
55
|
+
end
|
56
|
+
nil # no mutation
|
57
|
+
end
|
58
|
+
|
59
|
+
def mix_inner( other )
|
60
|
+
puts "Gene#mix_inner"
|
61
|
+
|
62
|
+
new_d = mutate( other )
|
63
|
+
if new_d.nil? ## no mutation of gene.d - use "regular" formula
|
64
|
+
new_d = Lottery.rand(100) < 50 ? d : other.d
|
65
|
+
end
|
66
|
+
|
67
|
+
new_r1 = Lottery.rand(100) < 50 ? r1 : other.r1
|
68
|
+
new_r2 = Lottery.rand(100) < 50 ? r2 : other.r2
|
69
|
+
new_r3 = Lottery.rand(100) < 50 ? r3 : other.r3
|
70
|
+
|
71
|
+
gene = Gene.new( new_r3 + new_r2 + new_r1 + new_d )
|
72
|
+
pp gene
|
73
|
+
gene
|
74
|
+
end
|
75
|
+
|
76
|
+
|
77
|
+
def mix( other )
|
78
|
+
puts "Gene#mix"
|
79
|
+
self_swapped = swap
|
80
|
+
other_swapped = other.swap
|
81
|
+
|
82
|
+
gene = self_swapped.mix_inner( other_swapped )
|
83
|
+
pp gene
|
84
|
+
gene
|
85
|
+
end
|
86
|
+
|
87
|
+
end # class Gene
|
@@ -0,0 +1,177 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
|
4
|
+
class Genome
|
5
|
+
attr_reader :genes ## hash of genes (key is gene type)
|
6
|
+
|
7
|
+
def initialize( arg )
|
8
|
+
if arg.is_a? Hash
|
9
|
+
hash = arg ## assumes (pre-built) hash with genes
|
10
|
+
@genes = hash
|
11
|
+
else
|
12
|
+
if arg.is_a? Integer ## use Integer (Fixnum+Bignum??) - why? why not?
|
13
|
+
num = arg
|
14
|
+
kai = Kai.encode( num )
|
15
|
+
else # else assume string in kai/base32 format
|
16
|
+
kai = arg.dup # just in case; make a clean (fresh) copy
|
17
|
+
kai = kai.gsub( ' ', '' ) ## allow spaces (strip/remove)
|
18
|
+
end
|
19
|
+
## puts "Genome.initialize #{kai}"
|
20
|
+
build_genes( kai )
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def build_genes( kai )
|
25
|
+
kai = kai.reverse ## note: reserve for easy left-to-right access
|
26
|
+
@genes = {} ## hash of genes (key is gene type)
|
27
|
+
## fix/todo: use as_json for "official" api order
|
28
|
+
## note: use insert order from "official" api
|
29
|
+
@genes[:body] = Gene.new( kai[0,4].reverse )
|
30
|
+
@genes[:pattern] = Gene.new( kai[4,4].reverse )
|
31
|
+
@genes[:coloreyes] = Gene.new( kai[8,4].reverse )
|
32
|
+
@genes[:eyes] = Gene.new( kai[12,4].reverse )
|
33
|
+
@genes[:color1] = Gene.new( kai[16,4].reverse ) ## colorprimary / body color / base color
|
34
|
+
@genes[:color2] = Gene.new( kai[20,4].reverse ) ## colorsecondary / sec color / pattern color / hi(light) color
|
35
|
+
@genes[:color3] = Gene.new( kai[24,4].reverse ) ## colortertiary / acc(ent) color
|
36
|
+
@genes[:wild] = Gene.new( kai[28,4].reverse )
|
37
|
+
@genes[:mouth] = Gene.new( kai[32,4].reverse )
|
38
|
+
end
|
39
|
+
|
40
|
+
def body() TRAITS[:body][:kai][ @genes[:body].d ]; end
|
41
|
+
def coloreyes() TRAITS[:coloreyes][:kai][ @genes[:coloreyes].d ]; end
|
42
|
+
def eyes() TRAITS[:eyes][:kai][ @genes[:eyes].d ]; end
|
43
|
+
def pattern() TRAITS[:pattern][:kai][ @genes[:pattern].d ]; end
|
44
|
+
def mouth() TRAITS[:mouth][:kai][ @genes[:mouth].d ]; end
|
45
|
+
def color1() TRAITS[:color1][:kai][ @genes[:color1].d ]; end
|
46
|
+
def color2() TRAITS[:color2][:kai][ @genes[:color2].d ]; end
|
47
|
+
def color3() TRAITS[:color3][:kai][ @genes[:color3].d ]; end
|
48
|
+
|
49
|
+
|
50
|
+
|
51
|
+
def genes_color1() @genes[:color1]; end ## rename to color1_genes instead - why? why not?
|
52
|
+
def genes_eyes() @genes[:eyes]; end
|
53
|
+
## ....
|
54
|
+
|
55
|
+
## add cattributes ?? why? why not?
|
56
|
+
|
57
|
+
|
58
|
+
def mix( other )
|
59
|
+
mgenes = genes ## matron genes
|
60
|
+
sgenes = other.genes ## sire genes
|
61
|
+
new_genes = {}
|
62
|
+
|
63
|
+
## todo/fix: use insertion order from "official" api - why? why not?
|
64
|
+
## -- preinitialize with empty hash and than use byte order ??
|
65
|
+
TRAIT_KEYS.each do |key|
|
66
|
+
mgene = mgenes[key]
|
67
|
+
sgene = sgenes[key]
|
68
|
+
|
69
|
+
new_gene = mgene.mix( sgene )
|
70
|
+
new_genes[key] = new_gene
|
71
|
+
end
|
72
|
+
|
73
|
+
Genome.new( new_genes ) ## return new genome from (pre-built) hash (with genes)
|
74
|
+
end
|
75
|
+
|
76
|
+
|
77
|
+
def build_tables() GenomeTables.new( self ).build; end
|
78
|
+
|
79
|
+
def build_mix_tables( other ) GenomeMixTables.new( self, other ).build; end
|
80
|
+
|
81
|
+
end # class Genome
|
82
|
+
|
83
|
+
|
84
|
+
|
85
|
+
class GenomeTables
|
86
|
+
def initialize( genome )
|
87
|
+
@genome = genome
|
88
|
+
end
|
89
|
+
|
90
|
+
def build
|
91
|
+
pos = 0
|
92
|
+
buf = ""
|
93
|
+
|
94
|
+
genes = @genome.genes
|
95
|
+
|
96
|
+
TRAITS.each do |key, trait|
|
97
|
+
gene = genes[key]
|
98
|
+
next if gene.nil? ## skip future_1, future_2, etc.
|
99
|
+
|
100
|
+
buf << "#{trait[:name]} (Genes #{trait[:genes]})\n\n"
|
101
|
+
|
102
|
+
###
|
103
|
+
## fix/todo: add stars for purity?
|
104
|
+
## **** - all traits the same
|
105
|
+
## *** - two same pairs of traits
|
106
|
+
## ** - one pair of same traits
|
107
|
+
|
108
|
+
buf << "|Gene |Binary |Kai |Trait | |\n"
|
109
|
+
buf << "|------|---------|-----|---------|---|\n"
|
110
|
+
buf << "| #{pos} | #{Kai::BINARY[gene.d]} | #{gene.d} | **#{fmt_trait(trait[:kai][gene.d])}** | d |\n"; pos+=1
|
111
|
+
buf << "| #{pos} | #{Kai::BINARY[gene.r1]} | #{gene.r1} | #{fmt_trait(trait[:kai][gene.r1])} | r1 |\n"; pos+=1
|
112
|
+
buf << "| #{pos} | #{Kai::BINARY[gene.r2]} | #{gene.r2} | #{fmt_trait(trait[:kai][gene.r2])} | r2 |\n"; pos+=1
|
113
|
+
buf << "| #{pos} | #{Kai::BINARY[gene.r3]} | #{gene.r3} | #{fmt_trait(trait[:kai][gene.r3])} | r3 |\n"; pos+=1
|
114
|
+
buf << "\n"
|
115
|
+
|
116
|
+
if key == :body ## add legend for first entry
|
117
|
+
buf << "d = dominant, r1 = 1st order recessive, r2 = 2nd order recessive, r3 = 3rd order recessive\n\n"
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
buf
|
122
|
+
end
|
123
|
+
|
124
|
+
####################
|
125
|
+
## helpers
|
126
|
+
|
127
|
+
def fmt_trait( trait )
|
128
|
+
(trait.nil? || trait.empty?) ? '?' : trait
|
129
|
+
end
|
130
|
+
end # class GenomeTables
|
131
|
+
|
132
|
+
|
133
|
+
|
134
|
+
|
135
|
+
class GenomeMixTables
|
136
|
+
def initialize( matron, sire )
|
137
|
+
@matron = matron
|
138
|
+
@sire = sire
|
139
|
+
end
|
140
|
+
|
141
|
+
def build
|
142
|
+
pos = 0
|
143
|
+
buf = ""
|
144
|
+
|
145
|
+
mgenes = @matron.genes
|
146
|
+
sgenes = @sire.genes
|
147
|
+
|
148
|
+
TRAITS.each do |key, trait|
|
149
|
+
mgene = mgenes[key]
|
150
|
+
sgene = sgenes[key]
|
151
|
+
next if mgene.nil? ## skip future_1, future_2, etc.
|
152
|
+
|
153
|
+
buf << "#{trait[:name]} (Genes #{trait[:genes]})\n\n"
|
154
|
+
|
155
|
+
buf << "|Gene |Kai |Trait (Matron)|Kai|Trait (Sire)| |\n"
|
156
|
+
buf << "|------|-----|---------|-----|---------|---|\n"
|
157
|
+
buf << "| #{pos} | #{mgene.d} | **#{fmt_trait(trait[:kai][mgene.d])}** | #{sgene.d} | **#{fmt_trait(trait[:kai][sgene.d])}** | d |\n"; pos+=1
|
158
|
+
buf << "| #{pos} | #{mgene.r1} | #{fmt_trait(trait[:kai][mgene.r1])} | #{sgene.r1} | #{fmt_trait(trait[:kai][sgene.r1])} | r1 |\n"; pos+=1
|
159
|
+
buf << "| #{pos} | #{mgene.r2} | #{fmt_trait(trait[:kai][mgene.r2])} | #{sgene.r2} | #{fmt_trait(trait[:kai][sgene.r2])} | r2 |\n"; pos+=1
|
160
|
+
buf << "| #{pos} | #{mgene.r3} | #{fmt_trait(trait[:kai][mgene.r3])} | #{sgene.r3} | #{fmt_trait(trait[:kai][sgene.r3])} | r3 |\n"; pos+=1
|
161
|
+
buf << "\n"
|
162
|
+
|
163
|
+
if key == :body ## add legend for first entry
|
164
|
+
buf << "d = dominant, r1 = 1st order recessive, r2 = 2nd order recessive, r3 = 3rd order recessive\n\n"
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
buf
|
169
|
+
end
|
170
|
+
|
171
|
+
####################
|
172
|
+
## helpers
|
173
|
+
|
174
|
+
def fmt_trait( trait )
|
175
|
+
(trait.nil? || trait.empty?) ? '?' : trait
|
176
|
+
end
|
177
|
+
end # class GenomeMixTables
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
|
4
|
+
class GenesReport
|
5
|
+
|
6
|
+
def build
|
7
|
+
buf = ""
|
8
|
+
buf << "# Genes (#{TRAITS.keys.size} x 4)\n\n"
|
9
|
+
|
10
|
+
headings = []
|
11
|
+
TRAITS.values.each do |trait|
|
12
|
+
headings << "#{trait[:name]} (#{trait[:genes]})"
|
13
|
+
end
|
14
|
+
|
15
|
+
buf << headings.join( " • " )
|
16
|
+
buf << "\n\n"
|
17
|
+
|
18
|
+
|
19
|
+
## pp TRAITS
|
20
|
+
TRAITS.values.each do |trait|
|
21
|
+
|
22
|
+
puts "Kai Cattribute"
|
23
|
+
items = []
|
24
|
+
Kai::ALPHABET.each_char do |kai|
|
25
|
+
value = trait[:kai][kai]
|
26
|
+
value = '?' if value.nil? || value.empty?
|
27
|
+
items << [kai, value]
|
28
|
+
end
|
29
|
+
|
30
|
+
items.each do |item|
|
31
|
+
puts "#{item[0]} #{item[1]}"
|
32
|
+
end
|
33
|
+
|
34
|
+
buf << "## #{trait[:name]} (Genes #{trait[:genes]})\n\n"
|
35
|
+
buf << make_table( items )
|
36
|
+
buf << "\n\n"
|
37
|
+
end
|
38
|
+
|
39
|
+
puts buf
|
40
|
+
|
41
|
+
buf
|
42
|
+
end ## method build
|
43
|
+
|
44
|
+
def make_table( items )
|
45
|
+
rows = make_rows( items, columns: 4 )
|
46
|
+
pp rows
|
47
|
+
|
48
|
+
buf = ""
|
49
|
+
buf << "|Kai|Cattribute |Kai|Cattribute |Kai|Cattribute |Kai|Cattribute |\n"
|
50
|
+
buf << "|---|-------------|---|------------|---|------------|---|------------|\n"
|
51
|
+
|
52
|
+
rows.each do |row|
|
53
|
+
buf << "| "
|
54
|
+
buf << row.map {|item| "#{item[0]} | #{item[1]}" }.join( " | " )
|
55
|
+
buf << " |\n"
|
56
|
+
end
|
57
|
+
|
58
|
+
buf
|
59
|
+
end
|
60
|
+
|
61
|
+
|
62
|
+
## helpers
|
63
|
+
def make_rows( items, columns: 4 )
|
64
|
+
offset = items.size / columns
|
65
|
+
pp offset
|
66
|
+
|
67
|
+
rows = []
|
68
|
+
offset.times.with_index do |row|
|
69
|
+
## note: construct [items[row],items[offset+row],items[offset*2+row], ...]
|
70
|
+
rows << columns.times.with_index.map { |col| items[offset*col+row] }
|
71
|
+
end
|
72
|
+
rows
|
73
|
+
end
|
74
|
+
|
75
|
+
|
76
|
+
def save( path )
|
77
|
+
File.open( path, "w" ) do |f|
|
78
|
+
f.write build
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
end # class GenesReport
|