|
1 /** |
|
2 * EditProduct.cpp is part of bmsapp. |
|
3 * |
|
4 * tab 8, water. |
|
5 * |
|
6 * bmsapp is free software: you can redistribute it and/or modify |
|
7 * it under the terms of the GNU General Public License as published by |
|
8 * the Free Software Foundation, either version 3 of the License, or |
|
9 * (at your option) any later version. |
|
10 * |
|
11 * bmsapp is distributed in the hope that it will be useful, |
|
12 * but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
13 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
14 * GNU General Public License for more details. |
|
15 * |
|
16 * You should have received a copy of the GNU General Public License |
|
17 * along with this program. If not, see <http://www.gnu.org/licenses/>. |
|
18 */ |
|
19 |
|
20 |
|
21 void EditProduct::refreshWaters() |
|
22 { |
|
23 |
|
24 // calc_acid |
|
25 ui->mw_phEdit->setValue(product->mash_ph); |
|
26 // mash_name |
|
27 //ui->mw_acidpercEdit->setValue(product-> |
|
28 |
|
29 } |
|
30 |
|
31 |
|
32 /* |
|
33 * Z alkalinity is the amount of acid (in mEq/l) needed to bring water to the target pH (Z pH) |
|
34 */ |
|
35 double EditProduct::ZAlkalinity(double pHZ) |
|
36 { |
|
37 double C43 = Utils::Charge(4.3); |
|
38 double Cw = Utils::Charge(product->wg_ph); |
|
39 double Cz = Utils::Charge(pHZ); |
|
40 double DeltaCNaught = -C43 + Cw; |
|
41 double CT = product->wg_total_alkalinity / 50 / DeltaCNaught; |
|
42 double DeltaCZ = -Cz + Cw; |
|
43 return CT * DeltaCZ; |
|
44 } |
|
45 |
|
46 |
|
47 /* |
|
48 * Z Residual alkalinity is the amount of acid (in mEq/l) needed |
|
49 * to bring the water in the mash to the target pH (Z pH). |
|
50 */ |
|
51 double EditProduct::ZRA(double pHZ) |
|
52 { |
|
53 double Calc = product->wg_calcium / (MMCa / 2); |
|
54 double Magn = product->wg_magnesium / (MMMg / 2); |
|
55 double Z = ZAlkalinity(pHZ); |
|
56 return Z - (Calc / 3.5 + Magn / 7); |
|
57 } |
|
58 |
|
59 |
|
60 double EditProduct::BufferCapacity(Fermentables F) |
|
61 { |
|
62 double C1 = 0; |
|
63 |
|
64 if ((F.f_di_ph != 5.7) && ((F.f_acid_to_ph_57 < - 0.1) || (F.f_acid_to_ph_57 > 0.1))) { |
|
65 C1 = F.f_acid_to_ph_57 / (F.f_di_ph - 5.7); |
|
66 } else { |
|
67 /* |
|
68 * If the acid_to_ph_5.7 is unknown from the maltster, guess the required acid. |
|
69 */ |
|
70 switch (F.f_graintype) { |
|
71 case 0: // Base, Special, Kilned |
|
72 case 3: |
|
73 case 5: C1 = 0.014 * F.f_color - 34.192; |
|
74 break; |
|
75 case 2: C1 = -0.0597 * F.f_color - 32.457; // Crystal |
|
76 break; |
|
77 case 1: C1 = 0.0107 * F.f_color - 54.768; // Roast |
|
78 break; |
|
79 case 4: C1 = -149; // Sour malt |
|
80 break; |
|
81 } |
|
82 } |
|
83 return C1; |
|
84 } |
|
85 |
|
86 |
|
87 double EditProduct::AcidRequired(double ZpH, Fermentables F) |
|
88 { |
|
89 double C1 = BufferCapacity(F); |
|
90 double x = F.f_di_ph; |
|
91 return C1 * (ZpH - x); |
|
92 } |
|
93 |
|
94 |
|
95 double EditProduct::ProtonDeficit(double pHZ) |
|
96 { |
|
97 double C1, x; |
|
98 int i, error_count = 0; |
|
99 double Result = ZRA(pHZ) * product->wg_amount; |
|
100 Fermentables F; |
|
101 |
|
102 /* |
|
103 * proton deficit for the grist |
|
104 */ |
|
105 if (product->fermentables.size()) { |
|
106 for (i = 0; i < product->fermentables.size(); i++) { |
|
107 F = product->fermentables.at(i); |
|
108 if (F.f_added == 0 && F.f_graintype != 6) { // Added == Mash && graintype != No Malt |
|
109 x = AcidRequired(pHZ, F) * F.f_amount; |
|
110 Result += x; |
|
111 } |
|
112 } |
|
113 } else { |
|
114 error_count++; |
|
115 if (error_count < 5) |
|
116 qDebug() << " ProtonDeficit" << pHZ << "invalid grist, return" << Result; |
|
117 } |
|
118 return Result; |
|
119 } |
|
120 |
|
121 |
|
122 double EditProduct::MashpH() |
|
123 { |
|
124 int n = 0; |
|
125 double pH = 5.4; |
|
126 double deltapH = 0.001; |
|
127 double deltapd = 0.1; |
|
128 double pd = ProtonDeficit(pH); |
|
129 |
|
130 while (((pd < -deltapd) || (pd > deltapd)) && (n < 2000)) { |
|
131 n++; |
|
132 if (pd < -deltapd) |
|
133 pH -= deltapH; |
|
134 else if (pd > deltapd) |
|
135 pH += deltapH; |
|
136 pd = ProtonDeficit(pH); |
|
137 } |
|
138 pH = round(pH * 1000000) / 1000000.0; |
|
139 qDebug() << " MashpH() n:" << n << "pH:" << pH; |
|
140 return pH; |
|
141 } |
|
142 |
|
143 |
|
144 void EditProduct::calcWater() |
|
145 { |
|
146 double liters = 0; |
|
147 double calcium = 0; |
|
148 double magnesium = 0; |
|
149 double sodium = 0; |
|
150 double total_alkalinity = 0; |
|
151 double bicarbonate = 0; |
|
152 double chloride = 0; |
|
153 double sulfate = 0; |
|
154 double ph = 0; |
|
155 double TpH = 0; |
|
156 double frac, RA; |
|
157 double protonDeficit = 0; |
|
158 double Acid = 0, Acidmg = 0; |
|
159 int AT; |
|
160 |
|
161 qDebug() << "calcWater()"; |
|
162 |
|
163 /* |
|
164 * If there is a dilute water source, mix the waters. |
|
165 */ |
|
166 if (product->w2_name != "") { |
|
167 liters = product->w1_amount + product->w2_amount; |
|
168 calcium = Utils::mix(product->w1_amount, product->w2_amount, product->w1_calcium, product->w2_calcium); |
|
169 magnesium = Utils::mix(product->w1_amount, product->w2_amount, product->w1_magnesium, product->w2_magnesium); |
|
170 sodium = Utils::mix(product->w1_amount, product->w2_amount, product->w1_sodium, product->w2_sodium); |
|
171 chloride = Utils::mix(product->w1_amount, product->w2_amount, product->w1_chloride, product->w2_chloride); |
|
172 sulfate = Utils::mix(product->w1_amount, product->w2_amount, product->w1_sulfate, product->w2_sulfate); |
|
173 total_alkalinity = Utils::mix(product->w1_amount, product->w2_amount, product->w1_total_alkalinity, product->w2_total_alkalinity); |
|
174 ph = -log10(((pow(10, -product->w1_ph) * product->w1_amount) + (pow(10, -product->w2_ph) * product->w2_amount)) / liters); |
|
175 } else { |
|
176 liters = product->w1_amount; |
|
177 calcium = product->w1_calcium; |
|
178 magnesium = product->w1_magnesium; |
|
179 sodium = product->w1_sodium; |
|
180 chloride = product->w1_chloride; |
|
181 sulfate = product->w1_sulfate; |
|
182 total_alkalinity = product->w1_total_alkalinity; |
|
183 ph = product->w1_ph; |
|
184 } |
|
185 |
|
186 product->wg_amount = liters; |
|
187 product->wg_calcium = round(calcium * 10.0) / 10.0; |
|
188 product->wg_magnesium = round(magnesium * 10.0) / 10.0; |
|
189 product->wg_sodium = round(sodium * 10.0) / 10.0; |
|
190 product->wg_chloride = round(chloride * 10.0) / 10.0; |
|
191 product->wg_sulfate = round(sulfate * 10.0) / 10.0; |
|
192 product->wg_total_alkalinity = round(total_alkalinity * 10.0) / 10.0; |
|
193 product->wg_ph = ph; |
|
194 |
|
195 ui->wg_volEdit->setValue(liters); |
|
196 ui->wg_caEdit->setValue(calcium); |
|
197 ui->wg_mgEdit->setValue(magnesium); |
|
198 ui->wg_hco3Edit->setValue(total_alkalinity * 1.22); |
|
199 ui->wg_caco3Edit->setValue(total_alkalinity); |
|
200 ui->wg_naEdit->setValue(sodium); |
|
201 ui->wg_clEdit->setValue(chloride); |
|
202 ui->wg_so4Edit->setValue(sulfate); |
|
203 ui->wg_phEdit->setValue(ph); |
|
204 bicarbonate = total_alkalinity * 1.22; |
|
205 |
|
206 /* Save mixed water ions for later */ |
|
207 double wg_calcium = calcium; |
|
208 double wg_sodium = sodium; |
|
209 double wg_total_alkalinity = total_alkalinity; |
|
210 double wg_chloride = chloride; |
|
211 double wg_sulfate = sulfate; |
|
212 double wg_bicarbonate = bicarbonate; |
|
213 |
|
214 double mash_ph = MashpH(); |
|
215 qDebug() << " Distilled water mash pH:" << mash_ph; |
|
216 |
|
217 /* Calculate Salt additions */ |
|
218 if (liters > 0) { |
|
219 calcium += ( ui->bs_cacl2Edit->value() * MMCa / MMCaCl2 * 1000 + ui->bs_caso4Edit->value() * MMCa / MMCaSO4 * 1000 + |
|
220 ui->bs_caco3Edit->value() * MMCa / MMCaCO3 * 1000) / liters; |
|
221 magnesium += (ui->bs_mgso4Edit->value() * MMMg / MMMgSO4 * 1000 + ui->bs_mgcl2Edit->value() * MMMg / MMMgCl2 * 1000) / liters; |
|
222 sodium += (ui->bs_naclEdit->value() * MMNa / MMNaCl * 1000 + ui->bs_nahco3Edit->value() * MMNa / MMNaHCO3 * 1000) / liters; |
|
223 sulfate += (ui->bs_caso4Edit->value() * MMSO4 / MMCaSO4 * 1000 + ui->bs_mgso4Edit->value() * MMSO4 / MMMgSO4 * 1000) / liters; |
|
224 chloride += (2 * ui->bs_cacl2Edit->value() * MMCl / MMCaCl2 * 1000 + ui->bs_naclEdit->value() * MMCl / MMNaCl * 1000 + |
|
225 ui->bs_mgcl2Edit->value() * MMCl / MMMgCl2 * 1000) / liters; |
|
226 bicarbonate += (ui->bs_nahco3Edit->value() * MMHCO3 / MMNaHCO3 * 1000 + ui->bs_caco3Edit->value() / 3 * MMHCO3 / MMCaCO3 * 1000) / liters; |
|
227 } |
|
228 |
|
229 const QSignalBlocker blocker1(ui->mw_acidPick); |
|
230 const QSignalBlocker blocker2(ui->mw_acidpercEdit); |
|
231 const QSignalBlocker blocker3(ui->mw_acidvolEdit); |
|
232 const QSignalBlocker blocker4(ui->wb_phEdit); |
|
233 const QSignalBlocker blocker5(ui->mw_phEdit); |
|
234 |
|
235 if (product->wa_acid_name < 0 || product->wa_acid_name >= my_acids.size()) { |
|
236 product->wa_acid_name = 0; |
|
237 product->wa_acid_perc = my_acids.at(0).AcidPrc; |
|
238 ui->mw_acidPick->setCurrentIndex(0); |
|
239 ui->mw_acidpercEdit->setValue(my_acids.at(0).AcidPrc); |
|
240 } |
|
241 AT = product->wa_acid_name; |
|
242 |
|
243 /* |
|
244 * Note that the next calculations do not correct the pH change by the added salts. |
|
245 * This pH change is at most 0.1 pH and is a minor difference in Acid amount. |
|
246 */ |
|
247 if (product->calc_acid) { |
|
248 /* |
|
249 * Auto calculate the needed acid. |
|
250 */ |
|
251 TpH = product->mash_ph; |
|
252 protonDeficit = ProtonDeficit(TpH); |
|
253 qDebug() << " calc_acid tgt:" << TpH << "protonDeficit:" << protonDeficit; |
|
254 if (protonDeficit > 0) { |
|
255 frac = Utils::CalcFrac(TpH, my_acids[AT].pK1, my_acids[AT].pK2, my_acids[AT].pK3); |
|
256 Acid = protonDeficit / frac; |
|
257 Acid *= my_acids[AT].MolWt; // mg. |
|
258 Acidmg = Acid; |
|
259 Acid = Acid / my_acids[AT].AcidSG; |
|
260 Acid = round((Acid / (product->wa_acid_perc / 100.0)) * 100.0) / 100.0; |
|
261 qDebug() << " Mash auto Acid final ml:" << Acid; |
|
262 |
|
263 QString w = my_acids[AT].name_en + ' ' + my_acids[AT].name_nl; |
|
264 brewing_salt_sub(w, Acid); |
|
265 ui->mw_acidvolEdit->setValue(Acid); |
|
266 |
|
267 bicarbonate = bicarbonate - protonDeficit * frac / liters; |
|
268 total_alkalinity = bicarbonate * 50 / 61; |
|
269 } |
|
270 ph = TpH; |
|
271 ui->wb_phEdit->setValue(ph); |
|
272 product->mash_ph = ph; |
|
273 } else { // Manual |
|
274 /* |
|
275 * Manual adjust acid, calculate resulting pH. |
|
276 */ |
|
277 double pHa = ph; // Mixed water pH. |
|
278 // Then calculate the new pH with added acids and malts |
|
279 qDebug() << " Mash pH:" << pHa; |
|
280 Acid = my_acids[AT].AcidSG * (product->wa_acid_perc / 100.0); // ml |
|
281 Acid *= ui->mw_acidvolEdit->value(); |
|
282 Acid /= my_acids[AT].MolWt; // mg; |
|
283 Acidmg = Acid; |
|
284 |
|
285 //find the pH where the protondeficit = protondeficit by the acid |
|
286 frac = Utils::CalcFrac(pHa, my_acids[AT].pK1, my_acids[AT].pK2, my_acids[AT].pK3); |
|
287 protonDeficit = Acid * frac; |
|
288 //qDebug() << " protonDeficit Acid:" << protonDeficit << "frac:" << frac << "pH:" << pHa; |
|
289 |
|
290 double deltapH = 0.001; |
|
291 double deltapd = 0.1; |
|
292 double pd = round(ProtonDeficit(pHa) * 1000000.0) / 1000000.0; |
|
293 int n = 0; |
|
294 while (((pd < (protonDeficit - deltapd)) || (pd > (protonDeficit + deltapd))) && (n < 4000)) { |
|
295 n++; |
|
296 if (pd < (protonDeficit - deltapd)) |
|
297 pHa -= deltapH; |
|
298 else if (pd > (protonDeficit + deltapd)) |
|
299 pHa += deltapH; |
|
300 frac = Utils::CalcFrac(pHa, my_acids[AT].pK1, my_acids[AT].pK2, my_acids[AT].pK3); |
|
301 protonDeficit = Acid * frac; |
|
302 pd = ProtonDeficit(pHa); |
|
303 } |
|
304 //qDebug() << " n:" << n << "pd:" << pd << "protonDeficit:" << protonDeficit << "frac:" << frac << "pHa:" << pHa; |
|
305 |
|
306 bicarbonate = wg_bicarbonate - protonDeficit * frac / liters; |
|
307 total_alkalinity = bicarbonate * 50 / 61; |
|
308 ph = pHa; |
|
309 ui->wb_phEdit->setValue(ph); |
|
310 ui->mw_phEdit->setValue(ph); |
|
311 product->mash_ph = ph; |
|
312 } |
|
313 |
|
314 if ((AT == 3) && (liters > 0)) { // Sulfuctic / Zwavelzuur |
|
315 RA = ui->bs_caso4Edit->value() * MMSO4 / MMCaSO4 + ui->bs_mgso4Edit->value() * MMSO4 / MMMgSO4 + Acidmg / 1000 * MMSO4 / (MMSO4 + 2); |
|
316 RA = 1000 * RA / liters; |
|
317 sulfate = wg_sulfate + RA; // Not add to sulfate?? |
|
318 } else if ((AT == 1) && (liters > 0)) { // Hydrochloric, Zoutzuur |
|
319 RA = ui->bs_cacl2Edit->value() * MMCl / MMCaCl2 + ui->bs_naclEdit->value() * MMCl / MMNaCl + Acidmg / 1000 * MMCl / (MMCl + 1); |
|
320 RA = 1000 * RA / liters; |
|
321 chloride = wg_chloride + RA; |
|
322 } |
|
323 |
|
324 double BUGU = GetBUGU(); |
|
325 ui->buguEdit->setValue(BUGU); |
|
326 if (BUGU < 0.32) |
|
327 ui->buguResult->setText(tr("Very malty and sweet")); |
|
328 else if (BUGU < 0.43) |
|
329 ui->buguResult->setText(tr("Malty, sweet")); |
|
330 else if (BUGU < 0.52) |
|
331 ui->buguResult->setText(tr("Balanced")); |
|
332 else if (BUGU < 0.63) |
|
333 ui->buguResult->setText(tr("Hoppy, bitter")); |
|
334 else |
|
335 ui->buguResult->setText(tr("Very hoppy, very bitter")); |
|
336 |
|
337 double OptSO4Clratio = GetOptSO4Clratio(); |
|
338 ui->so4clEdit->setValue(OptSO4Clratio); |
|
339 ui->cur_so4clResult->setRange(0.7 * OptSO4Clratio, 1.3 * OptSO4Clratio); |
|
340 if (OptSO4Clratio < 0.4) |
|
341 ui->so4clResult->setText(tr("Too malty")); |
|
342 else if (OptSO4Clratio < 0.6) |
|
343 ui->so4clResult->setText(tr("Very malty")); |
|
344 else if (OptSO4Clratio < 0.8) |
|
345 ui->so4clResult->setText(tr("Malty")); |
|
346 else if (OptSO4Clratio < 1.5) |
|
347 ui->so4clResult->setText(tr("Balanced")); |
|
348 else if (OptSO4Clratio < 2.0) |
|
349 ui->so4clResult->setText(tr("Little bitter")); |
|
350 else if (OptSO4Clratio < 4.0) |
|
351 ui->so4clResult->setText(tr("Bitter")); |
|
352 else if (OptSO4Clratio < 9.0) |
|
353 ui->so4clResult->setText(tr("Very bitter")); |
|
354 else |
|
355 ui->so4clResult->setText(tr("Too bitter")); |
|
356 if (chloride > 0) |
|
357 RA = sulfate / chloride; |
|
358 else |
|
359 RA = 10; |
|
360 ui->cur_so4clEdit->setValue(RA); |
|
361 ui->cur_so4clResult->setValue(RA); |
|
362 |
|
363 product->wb_calcium = calcium; |
|
364 product->wb_magnesium = magnesium; |
|
365 product->wb_total_alkalinity = total_alkalinity; |
|
366 product->wb_sodium = sodium; |
|
367 product->wb_chloride = chloride; |
|
368 product->wb_sulfate = sulfate; |
|
369 product->wb_ph = ph; |
|
370 |
|
371 ui->wb_caEdit->setValue(calcium); |
|
372 ui->wb_mgEdit->setValue(magnesium); |
|
373 ui->wb_hco3Edit->setValue(bicarbonate); |
|
374 ui->wb_caco3Edit->setValue(total_alkalinity); |
|
375 ui->wb_naEdit->setValue(sodium); |
|
376 ui->wb_clEdit->setValue(chloride); |
|
377 ui->wb_so4Edit->setValue(sulfate); |
|
378 |
|
379 ui->wb_caEdit->setStyleSheet((calcium < 40 || calcium > 150) ? "background-color: red":"background-color: green"); |
|
380 ui->wb_mgEdit->setStyleSheet((magnesium < 5 || magnesium > 40) ? "background-color: red":"background-color: green"); |
|
381 ui->wb_naEdit->setStyleSheet((sodium > 150) ? "background-color: red":"background-color: green"); |
|
382 /* |
|
383 * Both chloride and sulfate should be above 50 according to |
|
384 * John Palmer. So the Cl/SO4 ratio calculation will work. |
|
385 */ |
|
386 ui->wb_clEdit->setStyleSheet((chloride <= 50 || chloride > 150) ? "background-color: red":"background-color: green"); |
|
387 ui->wb_so4Edit->setStyleSheet((sulfate <= 50 || sulfate > 400) ? "background-color: red":"background-color: green"); |
|
388 /* |
|
389 * (cloride + sulfate) > 500 is too high |
|
390 */ |
|
391 if ((chloride + sulfate) > 500) { |
|
392 ui->wb_clEdit->setStyleSheet("background-color: red"); |
|
393 ui->wb_so4Edit->setStyleSheet("background-color: red"); |
|
394 } |
|
395 ui->wb_phEdit->setStyleSheet((ph < 5.2 || ph > 5.6) ? "background-color: red":"background-color: green"); |
|
396 ui->wb_hco3Edit->setStyleSheet((bicarbonate > 250) ? "background-color: red":"background-color: green"); |
|
397 ui->wb_caco3Edit->setStyleSheet((bicarbonate > 250) ? "background-color: red":"background-color: green"); |
|
398 |
|
399 calcSparge(); |
|
400 } |
|
401 |
|
402 |
|
403 /* |
|
404 * Based on the work of ajDeLange. |
|
405 */ |
|
406 void EditProduct::calcSparge() |
|
407 { |
|
408 double TargetpH = product->sparge_ph; |
|
409 double Source_pH = product->w1_ph; |
|
410 double Source_alkalinity = product->w1_total_alkalinity; |
|
411 |
|
412 qDebug() << "calcSparge()"; |
|
413 |
|
414 const QSignalBlocker blocker1(ui->sp_sourceEdit); |
|
415 |
|
416 // Select watersource or fallback to the first source. |
|
417 if (product->sparge_source == 1) { // Source 2 |
|
418 if (product->w2_ph > 0.0 && product->w2_amount > 0) { |
|
419 Source_pH = product->w2_ph; |
|
420 Source_alkalinity = product->w2_total_alkalinity; |
|
421 } else { |
|
422 product->sparge_source = 0; // Source 1 |
|
423 ui->sp_sourceEdit->setCurrentIndex(0); |
|
424 } |
|
425 } else if (product->sparge_source == 2) { // Mixed |
|
426 if (product->w2_ph > 0.0 && product->w2_amount > 0) { |
|
427 Source_pH = product->wg_ph; |
|
428 Source_alkalinity = product->wg_total_alkalinity; |
|
429 } else { |
|
430 product->sparge_source = 0; // Source 1 |
|
431 ui->sp_sourceEdit->setCurrentIndex(0); |
|
432 } |
|
433 } |
|
434 |
|
435 // Step 1: Compute the mole fractions of carbonic (f1o), bicarbonate (f2o) and carbonate(f3o) at the water pH |
|
436 double r1 = pow(10, Source_pH - 6.35); |
|
437 double r2 = pow(10, Source_pH - 10.33); |
|
438 double d = 1 + r1 + r1 * r2; |
|
439 double f1 = 1 / d; |
|
440 double f3 = r1 * r2 / d; |
|
441 |
|
442 // Step 2. Compute the mole fractions at pH = 4.3 (the pH which defines alkalinity) |
|
443 double r143 = pow(10, 4.3 - 6.35); |
|
444 double r243 = pow(10, 4.3 - 10.33); |
|
445 double d43 = 1 + r143 + r143 * r243; |
|
446 double f143 = 1 / d43; |
|
447 double f343 = r143 * r243 / d43; |
|
448 |
|
449 // Step 4. Solve |
|
450 //double Ct = (Source_alkalinity - 1000 * (pow(10, -4.3) - pow(10, -Source_pH))) / ((f143 - f1) + (f3 - f343)); |
|
451 double Ct = Source_alkalinity / 50 / ((f143 - f1) + (f3 - f343)); |
|
452 |
|
453 // Step 5. Compute mole fractions at desired pH |
|
454 double r1g = pow(10, TargetpH - 6.35); |
|
455 double r2g = pow(10, TargetpH - 10.33); |
|
456 double dg = 1 + r1g + r1g * r2g; |
|
457 double f1g = 1 / dg; |
|
458 double f3g = r1g * r2g / dg; |
|
459 |
|
460 // Step 6. Use these to compute the milliequivalents acid required per liter (mEq/L) |
|
461 double Acid = Ct * ((f1g - f1) + (f3 - f3g)) + pow(10, -TargetpH) - pow(10, -Source_pH); //mEq/l |
|
462 Acid += 0.01; // Add acid that would be required for distilled water. |
|
463 |
|
464 //Step 8. Get the acid data. |
|
465 int AT = product->sparge_acid_type; |
|
466 if (AT < 0 || AT >= my_acids.size()) { |
|
467 AT = 0; |
|
468 product->sparge_acid_type = 0; |
|
469 ui->sp_acidtypeEdit->setCurrentIndex(0); |
|
470 product->sparge_acid_perc = my_acids[0].AcidPrc; |
|
471 ui->sp_acidpercEdit->setValue(product->sparge_acid_perc); |
|
472 } |
|
473 double fract = Utils::CalcFrac(TargetpH, my_acids[AT].pK1, my_acids[AT].pK2, my_acids[AT].pK3); |
|
474 |
|
475 // Step 9. Now divide the mEq required by the "fraction". This is the required number of moles of acid. |
|
476 Acid /= fract; |
|
477 |
|
478 // Step 10. Multiply by molecular weight of the acid |
|
479 Acid *= my_acids[AT].MolWt; //mg |
|
480 |
|
481 // Step 11. Divide by Specific Gravity and Percentage to get the final ml. |
|
482 Acid = Acid / my_acids[AT].AcidSG / (product->sparge_acid_perc / 100); //ml |
|
483 Acid *= product->sparge_volume; //ml acid total |
|
484 Acid = round(Acid * 100.0) / 100.0; |
|
485 product->sparge_acid_amount = Acid / 1000; |
|
486 ui->sp_acidvolEdit->setValue(Acid); |
|
487 } |
|
488 |
|
489 |
|
490 void EditProduct::sp_source_changed(int val) |
|
491 { |
|
492 product->sparge_source = val; |
|
493 calcSparge(); |
|
494 is_changed(); |
|
495 } |
|
496 |
|
497 |
|
498 void EditProduct::sp_type_changed(int val) |
|
499 { |
|
500 product->sparge_acid_type = val; |
|
501 product->sparge_acid_perc = my_acids[val].AcidPrc; |
|
502 ui->sp_acidpercEdit->setValue(product->sparge_acid_perc); |
|
503 calcSparge(); |
|
504 is_changed(); |
|
505 } |
|
506 |
|
507 |
|
508 void EditProduct::sp_ph_changed(double val) |
|
509 { |
|
510 product->sparge_ph = val; |
|
511 calcSparge(); |
|
512 is_changed(); |
|
513 } |
|
514 |
|
515 |
|
516 double EditProduct::GetBUGU() |
|
517 { |
|
518 double gu = (product->est_og - 1) * 1000; |
|
519 if (gu > 0) |
|
520 return product->est_ibu / gu; |
|
521 return 0.5; |
|
522 } |
|
523 |
|
524 |
|
525 double EditProduct::GetOptSO4Clratio() |
|
526 { |
|
527 if (ui->wt_so4Edit->value() > 0 && ui->wt_clEdit->value()) { |
|
528 /* If target water is selected .. */ |
|
529 return (ui->wt_so4Edit->value() / ui->wt_clEdit->value()); |
|
530 } |
|
531 double BUGU = GetBUGU(); |
|
532 return (-1.2 * BUGU + 1.4); |
|
533 } |
|
534 |
|
535 |
|
536 void EditProduct::mw_calc_acid_clicked() |
|
537 { |
|
538 product->calc_acid = ! product->calc_acid; |
|
539 ui->mw_autoEdit->setChecked(product->calc_acid); |
|
540 ui->mw_phEdit->setReadOnly(! product->calc_acid); |
|
541 ui->mw_phEdit->setButtonSymbols(product->calc_acid ? QAbstractSpinBox::UpDownArrows : QAbstractSpinBox::NoButtons); |
|
542 ui->mw_acidvolEdit->setReadOnly(product->calc_acid); |
|
543 ui->mw_acidvolEdit->setButtonSymbols(product->calc_acid ? QAbstractSpinBox::NoButtons : QAbstractSpinBox::UpDownArrows); |
|
544 is_changed(); |
|
545 calcWater(); |
|
546 } |
|
547 |
|
548 |
|
549 void EditProduct::mw_ph_changed(double val) |
|
550 { |
|
551 if (! product->calc_acid) |
|
552 return; |
|
553 |
|
554 if (product->mash_ph != val) { |
|
555 qDebug() << "mw_ph_changed" << val << product->mash_ph; |
|
556 product->mash_ph = val; |
|
557 is_changed(); |
|
558 calcWater(); |
|
559 } |
|
560 } |
|
561 |
|
562 |
|
563 void EditProduct::mw_acid_changed(double val) |
|
564 { |
|
565 if (product->calc_acid) |
|
566 return; |
|
567 |
|
568 qDebug() << "on_mw_acid_changed" << val; |
|
569 QString w = my_acids[product->wa_acid_name].name_en + ' ' + my_acids[product->wa_acid_name].name_nl; |
|
570 set_brewing_salt(w, val); |
|
571 } |
|
572 |
|
573 |
|
574 void EditProduct::mw_type_changed(int val) |
|
575 { |
|
576 if (val == product->wa_acid_name) |
|
577 return; |
|
578 |
|
579 qDebug() << "on_mw_type_changed" << val << "old" << product->wa_acid_name; |
|
580 /* |
|
581 * First remove current acid. |
|
582 */ |
|
583 QString w = my_acids[product->wa_acid_name].name_en + ' ' + my_acids[product->wa_acid_name].name_nl; |
|
584 brewing_salt_sub(w, 0); |
|
585 |
|
586 product->wa_acid_name = val; |
|
587 w = my_acids[product->wa_acid_name].name_en + ' ' + my_acids[product->wa_acid_name].name_nl; |
|
588 |
|
589 product->wa_acid_perc = my_acids.at(val).AcidPrc; |
|
590 ui->mw_acidpercEdit->setValue(my_acids.at(val).AcidPrc); |
|
591 brewing_salt_sub(w, ui->mw_acidvolEdit->value()); // For now, set old amount. |
|
592 |
|
593 is_changed(); |
|
594 calcWater(); |
|
595 } |
|
596 |
|
597 |
|
598 void EditProduct::w2_volume_changed(double val) |
|
599 { |
|
600 qDebug() << "w2_vol_changed" << val; |
|
601 |
|
602 if (product->w2_total_alkalinity && product->w2_sulfate) { |
|
603 /* |
|
604 * Seems a valid water, but don't go over the total. |
|
605 */ |
|
606 if (val < (product->w1_amount + product->w2_amount)) { |
|
607 product->w1_amount -= val - product->w2_amount; |
|
608 product->w2_amount = val; |
|
609 ui->w1_volEdit->setValue(product->w1_amount); |
|
610 } |
|
611 } else { |
|
612 /* |
|
613 * Invalid water, block changes. |
|
614 */ |
|
615 product->w2_amount = 0; |
|
616 } |
|
617 ui->w2_volEdit->setValue(product->w2_amount); |
|
618 |
|
619 calcWater(); |
|
620 is_changed(); |
|
621 } |
|
622 |
|
623 |
|
624 void EditProduct::w1_name_changed(int val) |
|
625 { |
|
626 QSqlQuery query; |
|
627 |
|
628 qDebug() << "w1_name_changed" << val; |
|
629 const QSignalBlocker blocker1(ui->w1_nameEdit); |
|
630 if (val == 0) { |
|
631 /* |
|
632 * If no water is selected, take the default water. |
|
633 */ |
|
634 val = my_default_water; |
|
635 ui->w1_nameEdit->setCurrentIndex(val); |
|
636 } |
|
637 |
|
638 query.prepare("SELECT * FROM inventory_waters ORDER BY record"); |
|
639 query.exec(); |
|
640 query.first(); |
|
641 for (int i = 0; i < (val - 1); i++) { |
|
642 query.next(); |
|
643 } |
|
644 qDebug() << "set water" << query.value(1).toString(); |
|
645 |
|
646 product->w1_name = query.value(1).toString(); |
|
647 product->w1_calcium = query.value(3).toDouble(); |
|
648 product->w1_magnesium = query.value(8).toDouble(); |
|
649 product->w1_total_alkalinity = query.value(11).toDouble(); |
|
650 product->w1_sodium = query.value(7).toDouble(); |
|
651 product->w1_chloride = query.value(6).toDouble(); |
|
652 product->w1_sulfate = query.value(5).toDouble(); |
|
653 product->w1_ph = query.value(9).toDouble(); |
|
654 |
|
655 ui->w1_caEdit->setValue(product->w1_calcium); |
|
656 ui->w1_mgEdit->setValue(product->w1_magnesium); |
|
657 ui->w1_hco3Edit->setValue(product->w1_total_alkalinity * 1.22); |
|
658 ui->w1_caco3Edit->setValue(product->w1_total_alkalinity); |
|
659 ui->w1_naEdit->setValue(product->w1_sodium); |
|
660 ui->w1_clEdit->setValue(product->w1_chloride); |
|
661 ui->w1_so4Edit->setValue(product->w1_sulfate); |
|
662 ui->w1_phEdit->setValue(product->w1_ph); |
|
663 |
|
664 is_changed(); |
|
665 calcWater(); |
|
666 } |
|
667 |
|
668 |
|
669 void EditProduct::w2_name_changed(int val) |
|
670 { |
|
671 QSqlQuery query; |
|
672 |
|
673 qDebug() << "w2_name_changed" << val; |
|
674 |
|
675 if (val == 0) { // Clear water 2. |
|
676 product->w2_name = ""; |
|
677 product->w2_calcium = 0; |
|
678 product->w2_magnesium = 0; |
|
679 product->w2_total_alkalinity = 0; |
|
680 product->w2_sodium = 0; |
|
681 product->w2_chloride = 0; |
|
682 product->w2_sulfate = 0; |
|
683 product->w2_ph = 0; |
|
684 product->w1_amount += product->w2_amount; |
|
685 product->w2_amount = 0; |
|
686 } else { |
|
687 query.prepare("SELECT * FROM inventory_waters ORDER BY record"); |
|
688 query.exec(); |
|
689 query.first(); |
|
690 for (int i = 0; i < (val - 1); i++) { |
|
691 query.next(); |
|
692 } |
|
693 qDebug() << "set water" << query.value(1).toString(); |
|
694 |
|
695 product->w2_name = query.value(1).toString(); |
|
696 product->w2_calcium = query.value(3).toDouble(); |
|
697 product->w2_magnesium = query.value(8).toDouble(); |
|
698 product->w2_total_alkalinity = query.value(11).toDouble(); |
|
699 product->w2_sodium = query.value(7).toDouble(); |
|
700 product->w2_chloride = query.value(6).toDouble(); |
|
701 product->w2_sulfate = query.value(5).toDouble(); |
|
702 product->w2_ph = query.value(9).toDouble(); |
|
703 } |
|
704 ui->w1_volEdit->setValue(product->w1_amount); |
|
705 ui->w2_volEdit->setValue(product->w2_amount); |
|
706 ui->w2_caEdit->setValue(product->w2_calcium); |
|
707 ui->w2_mgEdit->setValue(product->w2_magnesium); |
|
708 ui->w2_hco3Edit->setValue(product->w2_total_alkalinity * 1.22); |
|
709 ui->w2_caco3Edit->setValue(product->w2_total_alkalinity); |
|
710 ui->w2_naEdit->setValue(product->w2_sodium); |
|
711 ui->w2_clEdit->setValue(product->w2_chloride); |
|
712 ui->w2_so4Edit->setValue(product->w2_sulfate); |
|
713 ui->w2_phEdit->setValue(product->w2_ph); |
|
714 |
|
715 is_changed(); |
|
716 calcWater(); |
|
717 } |
|
718 |
|
719 |
|
720 void EditProduct::wt_target_changed(int val) |
|
721 { |
|
722 QSqlQuery query; |
|
723 |
|
724 if (val == 0) { |
|
725 /* Clear values */ |
|
726 ui->wt_caEdit->setValue(0); |
|
727 ui->wt_mgEdit->setValue(0); |
|
728 ui->wt_hco3Edit->setValue(0); |
|
729 ui->wt_caco3Edit->setValue(0); |
|
730 ui->wt_naEdit->setValue(0); |
|
731 ui->wt_clEdit->setValue(0); |
|
732 ui->wt_so4Edit->setValue(0); |
|
733 } else { |
|
734 query.prepare("SELECT * FROM profile_water ORDER BY name"); |
|
735 query.exec(); |
|
736 query.first(); |
|
737 for (int i = 0; i < (val - 1); i++) { |
|
738 query.next(); |
|
739 } |
|
740 ui->wt_caEdit->setValue(query.value(2).toDouble()); |
|
741 ui->wt_mgEdit->setValue(query.value(7).toDouble()); |
|
742 ui->wt_hco3Edit->setValue(query.value(3).toDouble()); |
|
743 ui->wt_caco3Edit->setValue(query.value(10).toDouble()); |
|
744 ui->wt_naEdit->setValue(query.value(6).toDouble()); |
|
745 ui->wt_clEdit->setValue(query.value(5).toDouble()); |
|
746 ui->wt_so4Edit->setValue(query.value(4).toDouble()); |
|
747 } |
|
748 calcWater(); |
|
749 } |
|
750 |
|
751 |
|
752 void EditProduct::adjustWaters(double factor) |
|
753 { |
|
754 int i; |
|
755 double amount; |
|
756 |
|
757 if (product->mashs.size() == 0) |
|
758 return; |
|
759 |
|
760 double mash_infuse = 0; |
|
761 for (i = 0; i < product->mashs.size(); i++) { |
|
762 if (product->mashs.at(i).step_type == 0) { // Infusion |
|
763 amount = round(product->mashs.at(i).step_infuse_amount * factor * 1000000.0) / 1000000.0; |
|
764 product->mashs[i].step_infuse_amount = amount; |
|
765 mash_infuse += amount; |
|
766 product->mashs[i].step_volume = mash_infuse; |
|
767 } |
|
768 } |
|
769 |
|
770 const QSignalBlocker blocker1(ui->w1_volEdit); |
|
771 const QSignalBlocker blocker2(ui->w2_volEdit); |
|
772 |
|
773 if (product->w2_amount == 0) { |
|
774 product->w1_amount = mash_infuse; |
|
775 ui->w1_volEdit->setValue(mash_infuse); |
|
776 } else { |
|
777 double w1 = (product->w1_amount / (product->w1_amount + product->w2_amount)) * mash_infuse; |
|
778 double w2 = (product->w2_amount / (product->w1_amount + product->w2_amount)) * mash_infuse; |
|
779 product->w1_amount = w1; |
|
780 product->w2_amount = w2; |
|
781 ui->w1_volEdit->setValue(product->w1_amount); |
|
782 ui->w2_volEdit->setValue(product->w2_amount); |
|
783 } |
|
784 product->wg_amount = mash_infuse; |
|
785 ui->wg_volEdit->setValue(mash_infuse); |
|
786 } |
|
787 |
|
788 |
|
789 void EditProduct::wb_cacl2_changed(double val) { set_brewing_salt("CaCl2", val); } |
|
790 void EditProduct::wb_caso4_changed(double val) { set_brewing_salt("CaSO4", val); } |
|
791 void EditProduct::wb_mgso4_changed(double val) { set_brewing_salt("MgSO4", val); } |
|
792 void EditProduct::wb_nacl_changed(double val) { set_brewing_salt("NaCl", val); } |
|
793 void EditProduct::wb_mgcl2_changed(double val) { set_brewing_salt("MgCl2", val); } |
|
794 void EditProduct::wb_nahco3_changed(double val) { set_brewing_salt("NaHCO3", val); } |
|
795 void EditProduct::wb_caco3_changed(double val) { set_brewing_salt("CaCO3", val); } |
|
796 |
|
797 |