toji 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (50) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +8 -0
  3. data/.travis.yml +7 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +39 -0
  7. data/Rakefile +10 -0
  8. data/bin/console +14 -0
  9. data/bin/setup +8 -0
  10. data/example/koji_recipe.rb +34 -0
  11. data/example/make_koji.rb +18 -0
  12. data/example/rice_recipe.rb +28 -0
  13. data/example/three_step_mashing_recipe.rb +65 -0
  14. data/lib/toji.rb +12 -0
  15. data/lib/toji/graph.rb +6 -0
  16. data/lib/toji/graph/ab.rb +75 -0
  17. data/lib/toji/ingredient.rb +8 -0
  18. data/lib/toji/ingredient/koji.rb +19 -0
  19. data/lib/toji/ingredient/koji/actual.rb +21 -0
  20. data/lib/toji/ingredient/koji/actual_fermentable.rb +15 -0
  21. data/lib/toji/ingredient/koji/base.rb +26 -0
  22. data/lib/toji/ingredient/koji/expected.rb +51 -0
  23. data/lib/toji/ingredient/koji/expected_fermentable.rb +15 -0
  24. data/lib/toji/ingredient/rice.rb +19 -0
  25. data/lib/toji/ingredient/rice/actual.rb +18 -0
  26. data/lib/toji/ingredient/rice/actual_steamable.rb +27 -0
  27. data/lib/toji/ingredient/rice/base.rb +40 -0
  28. data/lib/toji/ingredient/rice/expected.rb +46 -0
  29. data/lib/toji/ingredient/rice/expected_steamable.rb +29 -0
  30. data/lib/toji/ingredient/yeast.rb +9 -0
  31. data/lib/toji/ingredient/yeast/base.rb +21 -0
  32. data/lib/toji/ingredient/yeast/red_star.rb +30 -0
  33. data/lib/toji/progress.rb +11 -0
  34. data/lib/toji/progress/job.rb +165 -0
  35. data/lib/toji/progress/job_accessor.rb +13 -0
  36. data/lib/toji/progress/jobs.rb +142 -0
  37. data/lib/toji/progress/make_koji.rb +100 -0
  38. data/lib/toji/progress/moromi.rb +276 -0
  39. data/lib/toji/progress/shubo.rb +158 -0
  40. data/lib/toji/recipe.rb +9 -0
  41. data/lib/toji/recipe/koji_rate.rb +16 -0
  42. data/lib/toji/recipe/rice_rate.rb +10 -0
  43. data/lib/toji/recipe/rice_rate/base.rb +21 -0
  44. data/lib/toji/recipe/rice_rate/cooked.rb +61 -0
  45. data/lib/toji/recipe/rice_rate/steamed.rb +42 -0
  46. data/lib/toji/recipe/step.rb +65 -0
  47. data/lib/toji/recipe/three_step_mashing.rb +87 -0
  48. data/lib/toji/version.rb +3 -0
  49. data/toji.gemspec +36 -0
  50. metadata +178 -0
@@ -0,0 +1,13 @@
1
+ module Toji
2
+ module Progress
3
+ module JobAccessor
4
+ def job_reader(*args)
5
+ args.each {|arg|
6
+ define_method(arg) {
7
+ @jobs[arg]
8
+ }
9
+ }
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,142 @@
1
+ module Toji
2
+ module Progress
3
+ class Jobs
4
+ include Enumerable
5
+
6
+ attr_reader :day_offset
7
+
8
+ def initialize(arr)
9
+ @arr = []
10
+ @hash = {}
11
+ @day_offset = 0
12
+
13
+ arr.each {|j|
14
+ self << j
15
+ }
16
+ end
17
+
18
+ def [](id)
19
+ @hash[id]
20
+ end
21
+
22
+ def <<(job)
23
+ if job.id
24
+ if @hash[job.id]
25
+ @arr.delete(@hash[job.id])
26
+ end
27
+ @hash[job.id] = job
28
+ end
29
+ @arr << job
30
+ job.jobs = self
31
+ normalization
32
+ end
33
+
34
+ def normalization
35
+ min_time = @arr.select{|j| j.time}.map(&:time).sort.first
36
+
37
+ if min_time
38
+ @arr.each {|j|
39
+ if j.time
40
+ j.elapsed_time = (j.time - min_time).to_i
41
+ else
42
+ j.time = min_time + j.elapsed_time
43
+ end
44
+ }
45
+ end
46
+
47
+ @arr = @arr.sort{|a,b| a.elapsed_time<=>b.elapsed_time}
48
+
49
+ t = @arr.first&.time
50
+ if t
51
+ @day_offset = t - Time.mktime(t.year, t.month, t.day)
52
+ else
53
+ @day_offset = 0
54
+ end
55
+ end
56
+
57
+ def each(&block)
58
+ normalization
59
+ @arr.each(&block)
60
+ end
61
+
62
+ def plot_data(keys=nil)
63
+ normalization
64
+
65
+ data = map(&:to_h).map(&:compact)
66
+ if !keys
67
+ keys = data.map(&:keys).flatten.uniq
68
+ end
69
+
70
+ result = []
71
+
72
+ if keys.include?(:temps)
73
+ temps = data.map{|h| h[:temps]}
74
+ if 0<temps.select{|a| a.present?}.size
75
+ temps_x = []
76
+ temps_y = []
77
+ temps.each_with_index {|ts,i|
78
+ ts.each_with_index {|t,j|
79
+ temps_x << data[i][:elapsed_time] + j + day_offset
80
+ temps_y << t
81
+ }
82
+ }
83
+ result << {x: temps_x, y: temps_y, name: :temps}
84
+ end
85
+ end
86
+
87
+ keys &= [:preset_temp, :room_temp, :room_psychrometry, :baume, :acid, :amino_acid, :alcohol]
88
+
89
+ keys.each {|key|
90
+ xs = []
91
+ ys = []
92
+ data.each {|h|
93
+ if h[key]
94
+ ys << h[key]
95
+ xs << h[:elapsed_time] + day_offset
96
+ end
97
+ }
98
+
99
+ line_shape = :linear
100
+ if key==:preset_temp
101
+ line_shape = :hv
102
+ end
103
+
104
+ result << {x: xs, y: ys, name: key, line: {shape: line_shape}}
105
+ }
106
+
107
+ if 0<day_offset
108
+ result = result.map{|h|
109
+ h[:x].unshift(0)
110
+ h[:y].unshift(nil)
111
+ h
112
+ }
113
+ end
114
+
115
+ result
116
+ end
117
+
118
+ def bmd_plot_data
119
+ normalization
120
+
121
+ data = map{|j| [j.time || j.elapsed_time + day_offset, j.moromi_time, j.moromi_day, j.baume, j.bmd]}.select{|a| a[1] && a[4]}
122
+ xs = data.map{|a| a[1]}
123
+ ys = data.map{|a| a[4]}
124
+ texts = data.map{|a| "%s<br />moromi day=%d, be=%s, bmd=%s" % [a[0], a[2], a[3], a[4]]}
125
+
126
+ {x: xs, y: ys, text: texts, name: :bmd}
127
+ end
128
+
129
+ def ab_plot_data
130
+ normalization
131
+
132
+ result = []
133
+
134
+ data = map{|j| [j.time || j.elapsed_time + day_offset, j.alcohol, j.baume]}.select{|a| a[1] && a[2]}
135
+ xs = data.map{|a| a[1]}
136
+ ys = data.map{|a| a[2]}
137
+ texts = data.map{|a| "%s<br />alc=%s, be=%s" % a}
138
+ {x: xs, y: ys, text: texts, name: :actual}
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,100 @@
1
+ module Toji
2
+ module Progress
3
+ class MakeKoji
4
+ extend JobAccessor
5
+
6
+ TEMPLATES = {
7
+ default: [
8
+ Job.new(
9
+ elapsed_time: 0 * Job::HOUR,
10
+ id: :hikikomi,
11
+ after_temp: 35.0,
12
+ room_temp: 28.0,
13
+ room_psychrometry: nil,
14
+ ),
15
+ Job.new(
16
+ elapsed_time: 1 * Job::HOUR,
17
+ id: :tokomomi,
18
+ after_temp: 32.0,
19
+ room_temp: 28.0,
20
+ room_psychrometry: nil,
21
+ ),
22
+ Job.new(
23
+ elapsed_time: 9 * Job::HOUR,
24
+ id: :kirikaeshi,
25
+ before_temp: 32.0,
26
+ after_temp: 31.0,
27
+ room_temp: 28.0,
28
+ room_psychrometry: nil,
29
+ ),
30
+ Job.new(
31
+ elapsed_time: 21 * Job::HOUR,
32
+ id: :mori,
33
+ before_temp: 35.0,
34
+ after_temp: 33.0,
35
+ room_temp: 28.0,
36
+ room_psychrometry: 4,
37
+ ),
38
+ Job.new(
39
+ elapsed_time: 29 * Job::HOUR,
40
+ id: :naka_shigoto,
41
+ before_temp: 37.0,
42
+ after_temp: 35.0,
43
+ room_temp: 28.0,
44
+ room_psychrometry: 4,
45
+ ),
46
+ Job.new(
47
+ elapsed_time: 35 * Job::HOUR,
48
+ id: :shimai_shigoto,
49
+ before_temp: 38.0,
50
+ after_temp: 37.0,
51
+ room_temp: 28.0,
52
+ room_psychrometry: 5,
53
+ ),
54
+ Job.new(
55
+ elapsed_time: 39 * Job::HOUR,
56
+ id: :tsumikae,
57
+ after_temp: 40.0,
58
+ room_temp: 28.0,
59
+ room_psychrometry: 5,
60
+ ),
61
+ Job.new(
62
+ elapsed_time: 44 * Job::HOUR,
63
+ id: :dekoji,
64
+ after_temp: 40.0,
65
+ room_temp: 28.0,
66
+ room_psychrometry: 5,
67
+ ),
68
+ ],
69
+ }
70
+
71
+
72
+ job_reader :hikikomi
73
+ job_reader :tokomomi
74
+ job_reader :kirikaeshi
75
+ job_reader :mori
76
+ job_reader :naka_shigoto
77
+ job_reader :shimai_shigoto
78
+ job_reader :tsumikae
79
+ job_reader :dekoji
80
+
81
+
82
+ def initialize(jobs=[])
83
+ @jobs = Jobs.new(jobs)
84
+ end
85
+
86
+ def add(job)
87
+ @jobs << job
88
+ self
89
+ end
90
+
91
+ def jobs
92
+ @jobs.to_a
93
+ end
94
+
95
+ def self.template(key=:default)
96
+ new(TEMPLATES[key])
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,276 @@
1
+ module Toji
2
+ module Progress
3
+ class Moromi
4
+ extend JobAccessor
5
+
6
+ TEMPLATES = {
7
+ default: [
8
+ Job.new(
9
+ time: Time.mktime(2019, 1, 16),
10
+ id: :soe,
11
+ room_temp: 9,
12
+ before_temp: 14.8,
13
+ after_temp: 11.0,
14
+ ),
15
+ Job.new(
16
+ time: Time.mktime(2019, 1, 17),
17
+ room_temp: 9,
18
+ before_temp: 14.3,
19
+ ),
20
+ Job.new(
21
+ time: Time.mktime(2019, 1, 18),
22
+ room_temp: 8.5,
23
+ before_temp: 11.3,
24
+ ),
25
+ Job.new(
26
+ time: Time.mktime(2019, 1, 19),
27
+ id: :naka,
28
+ room_temp: 9,
29
+ before_temp: 6.9,
30
+ after_temp: 10.6,
31
+ ),
32
+ Job.new(
33
+ time: Time.mktime(2019, 1, 20),
34
+ id: :tome,
35
+ room_temp: 6,
36
+ before_temp: 7.5,
37
+ after_temp: 8.0,
38
+ ),
39
+ Job.new(
40
+ time: Time.mktime(2019, 1, 21),
41
+ room_temp: 7,
42
+ before_temp: 9.1,
43
+ ),
44
+ Job.new(
45
+ time: Time.mktime(2019, 1, 22),
46
+ room_temp: 7,
47
+ before_temp: 10.4,
48
+ ),
49
+ Job.new(
50
+ time: Time.mktime(2019, 1, 23),
51
+ room_temp: 8,
52
+ before_temp: 11.9,
53
+ baume: 8.6,
54
+ ),
55
+ Job.new(
56
+ time: Time.mktime(2019, 1, 24),
57
+ room_temp: 8,
58
+ before_temp: 12.3,
59
+ baume: 8.0,
60
+ alcohol: 4.8,
61
+ ),
62
+ Job.new(
63
+ time: Time.mktime(2019, 1, 25),
64
+ room_temp: 7.5,
65
+ before_temp: 13.1,
66
+ baume: 7.2,
67
+ alcohol: 6.75,
68
+ ),
69
+ Job.new(
70
+ time: Time.mktime(2019, 1, 26),
71
+ room_temp: 7.5,
72
+ before_temp: 13.1,
73
+ baume: 5.8,
74
+ alcohol: 7.992,
75
+ ),
76
+ Job.new(
77
+ time: Time.mktime(2019, 1, 27),
78
+ room_temp: 7.5,
79
+ before_temp: 12.9,
80
+ baume: 4.8,
81
+ ),
82
+ Job.new(
83
+ time: Time.mktime(2019, 1, 28),
84
+ room_temp: 7.0,
85
+ before_temp: 12.8,
86
+ baume: 4.0,
87
+ alcohol: 10.7,
88
+ ),
89
+ Job.new(
90
+ time: Time.mktime(2019, 1, 29),
91
+ room_temp: 8.0,
92
+ before_temp: 12.9,
93
+ baume: 3.4,
94
+ alcohol: 11.6,
95
+ ),
96
+ Job.new(
97
+ time: Time.mktime(2019, 1, 30),
98
+ room_temp: 8.0,
99
+ before_temp: 12.6,
100
+ nihonshudo: -27,
101
+ alcohol: 12.7,
102
+ ),
103
+ Job.new(
104
+ time: Time.mktime(2019, 1, 31),
105
+ room_temp: 7.0,
106
+ before_temp: 12.0,
107
+ nihonshudo: -21,
108
+ alcohol: 13.7,
109
+ ),
110
+ Job.new(
111
+ time: Time.mktime(2019, 2, 1),
112
+ room_temp: 7.0,
113
+ before_temp: 11.2,
114
+ nihonshudo: -17,
115
+ ),
116
+ Job.new(
117
+ time: Time.mktime(2019, 2, 2),
118
+ room_temp: 6.5,
119
+ before_temp: 10.2,
120
+ nihonshudo: -13,
121
+ ),
122
+ Job.new(
123
+ time: Time.mktime(2019, 2, 3),
124
+ room_temp: 7.0,
125
+ before_temp: 9.7,
126
+ ),
127
+ Job.new(
128
+ time: Time.mktime(2019, 2, 4),
129
+ room_temp: 8.0,
130
+ before_temp: 9.8,
131
+ nihonshudo: -8,
132
+ alcohol: 16.0,
133
+ ),
134
+ Job.new(
135
+ time: Time.mktime(2019, 2, 5),
136
+ room_temp: 6.0,
137
+ before_temp: 9.1,
138
+ nihonshudo: -5,
139
+ ),
140
+ Job.new(
141
+ time: Time.mktime(2019, 2, 6),
142
+ room_temp: 5.0,
143
+ before_temp: 8.4,
144
+ nihonshudo: -3,
145
+ alcohol: 16.3,
146
+ ),
147
+ Job.new(
148
+ time: Time.mktime(2019, 2, 7),
149
+ room_temp: 5.0,
150
+ before_temp: 8.1,
151
+ nihonshudo: -2,
152
+ ),
153
+ Job.new(
154
+ time: Time.mktime(2019, 2, 8),
155
+ room_temp: 7.0,
156
+ before_temp: 8.3,
157
+ nihonshudo: 0,
158
+ ),
159
+ Job.new(
160
+ time: Time.mktime(2019, 2, 9),
161
+ room_temp: 6.0,
162
+ before_temp: 8.1,
163
+ ),
164
+ ],
165
+ }
166
+
167
+
168
+ job_reader :moto
169
+ job_reader :soe
170
+ job_reader :naka
171
+ job_reader :tome
172
+
173
+ def initialize(jobs=[])
174
+ @jobs = Jobs.new(jobs)
175
+ end
176
+
177
+ def add(job)
178
+ @jobs << job
179
+ self
180
+ end
181
+
182
+ def jobs
183
+ @jobs.to_a
184
+ end
185
+
186
+ def days
187
+ (jobs.last.elapsed_time.to_f / Job::DAY).ceil + 1
188
+ end
189
+
190
+ def moromi_days
191
+ tome = @jobs[:tome]
192
+ if tome
193
+ tome_day = (tome.elapsed_time.to_f / Job::DAY).floor + 1
194
+ days - tome_day
195
+ end
196
+ end
197
+
198
+ def day_labels
199
+ texts = Array.new(days)
200
+ [:soe, :naka, :tome].each {|id|
201
+ j = jobs.select{|j| j.id==id}.first
202
+ if j
203
+ i = ((j.elapsed_time + @jobs.day_offset) / Job::DAY.to_f).floor
204
+ texts[i] = id
205
+ end
206
+ }
207
+
208
+ soe_i = texts.index(:soe)
209
+ tome_i = texts.index(:tome)
210
+ moromi_day = 1
211
+ texts.map.with_index {|text,i|
212
+ if text
213
+ text
214
+ elsif !soe_i || i<soe_i
215
+ :moto
216
+ elsif !tome_i || i<tome_i
217
+ :odori
218
+ else
219
+ moromi_day += 1
220
+ end
221
+ }
222
+ end
223
+
224
+ def plot_data
225
+ @jobs.plot_data
226
+ end
227
+
228
+ def plot
229
+ Plotly::Plot.new(
230
+ data: plot_data,
231
+ layout: {
232
+ xaxis: {
233
+ dtick: Job::DAY,
234
+ tickvals: days.times.map{|d| d*Job::DAY},
235
+ ticktext: day_labels
236
+ }
237
+ }
238
+ )
239
+ end
240
+
241
+ def bmd_plot_data
242
+ @jobs.bmd_plot_data
243
+ end
244
+
245
+ def bmd_plot
246
+ data = [bmd_plot_data]
247
+ max_moromi_day = moromi_days || 0
248
+ max_moromi_day = [max_moromi_day, 14].max
249
+
250
+ Plotly::Plot.new(
251
+ data: data,
252
+ layout: {
253
+ xaxis: {
254
+ title: "Moromi day",
255
+ dtick: Job::DAY,
256
+ range: [1, max_moromi_day].map{|d| d*Job::DAY},
257
+ tickvals: days.times.map{|d| d*Job::DAY},
258
+ ticktext: days.times.map(&:succ)
259
+ },
260
+ yaxis: {
261
+ title: "BMD",
262
+ }
263
+ }
264
+ )
265
+ end
266
+
267
+ def ab(coef=1.5)
268
+ Graph::Ab.new(coef).actual(@jobs)
269
+ end
270
+
271
+ def self.template(key=:default)
272
+ new(TEMPLATES[key])
273
+ end
274
+ end
275
+ end
276
+ end