copycats 0.0.1 → 0.5.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.
- 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
|