toji 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.
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