27 //ui->mw_acidpercEdit->setValue(recipe-> |
27 //ui->mw_acidpercEdit->setValue(recipe-> |
28 |
28 |
29 } |
29 } |
30 |
30 |
31 |
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 EditRecipe::ZAlkalinity(double pHZ) |
|
36 { |
|
37 double C43 = Utils::Charge(4.3); |
|
38 double Cw = Utils::Charge(recipe->wg_ph); |
|
39 double Cz = Utils::Charge(pHZ); |
|
40 double DeltaCNaught = -C43 + Cw; |
|
41 double CT = recipe->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 EditRecipe::ZRA(double pHZ) |
|
52 { |
|
53 double Calc = recipe->wg_calcium / (MMCa / 2); |
|
54 double Magn = recipe->wg_magnesium / (MMMg / 2); |
|
55 double Z = ZAlkalinity(pHZ); |
|
56 return Z - (Calc / 3.5 + Magn / 7); |
|
57 } |
|
58 |
|
59 |
|
60 double EditRecipe::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 EditRecipe::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 EditRecipe::ProtonDeficit(double pHZ) |
|
96 { |
|
97 double C1, x; |
|
98 int i, error_count = 0; |
|
99 double Result = ZRA(pHZ) * recipe->wg_amount; |
|
100 Fermentables F; |
|
101 |
|
102 /* |
|
103 * proton deficit for the grist |
|
104 */ |
|
105 if (recipe->fermentables.size()) { |
|
106 for (i = 0; i < recipe->fermentables.size(); i++) { |
|
107 F = recipe->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 EditRecipe::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 |
32 void EditRecipe::calcWater() |
144 void EditRecipe::calcWater() |
33 { |
145 { |
34 double liters = 0; |
146 double liters = 0; |
35 double calcium = 0; |
147 double calcium = 0; |
36 double magnesium = 0; |
148 double magnesium = 0; |
91 double wg_total_alkalinity = total_alkalinity; |
209 double wg_total_alkalinity = total_alkalinity; |
92 double wg_chloride = chloride; |
210 double wg_chloride = chloride; |
93 double wg_sulfate = sulfate; |
211 double wg_sulfate = sulfate; |
94 double wg_bicarbonate = bicarbonate; |
212 double wg_bicarbonate = bicarbonate; |
95 |
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 if (recipe->wa_acid_name < 0 || recipe->wa_acid_name >= my_acids.size()) { |
|
230 recipe->wa_acid_name = 0; |
|
231 recipe->wa_acid_perc = my_acids.at(0).AcidPrc; |
|
232 this->ignoreChanges = true; |
|
233 ui->mw_acidPick->setCurrentIndex(0); |
|
234 ui->mw_acidpercEdit->setValue(my_acids.at(0).AcidPrc); |
|
235 this->ignoreChanges = false; |
|
236 } |
|
237 AT = recipe->wa_acid_name; |
|
238 |
|
239 /* |
|
240 * Note that the next calculations do not correct the pH change by the added salts. |
|
241 * This pH change is at most 0.1 pH and is a minor difference in Acid amount. |
|
242 */ |
|
243 if (recipe->calc_acid) { |
|
244 /* |
|
245 * Auto calculate the needed acid. |
|
246 */ |
|
247 TpH = recipe->mash_ph; |
|
248 protonDeficit = ProtonDeficit(TpH); |
|
249 qDebug() << "calc_acid tgt:" << TpH << "protonDeficit:" << protonDeficit; |
|
250 if (protonDeficit > 0) { |
|
251 qDebug() << "pkn:" << AT << my_acids[AT].pK1 << my_acids[AT].pK2 << my_acids[AT].pK3; |
|
252 frac = Utils::CalcFrac(TpH, my_acids[AT].pK1, my_acids[AT].pK2, my_acids[AT].pK3); |
|
253 Acid = protonDeficit / frac; |
|
254 qDebug() << "1" << frac << Acid << protonDeficit; |
|
255 Acid *= my_acids[AT].MolWt; // mg. |
|
256 Acidmg = Acid; |
|
257 Acid = Acid / my_acids[AT].AcidSG; |
|
258 Acid = round((Acid / (recipe->wa_acid_perc / 100.0)) * 100.0) / 100.0; |
|
259 qDebug() << "Mash auto Acid final ml:" << Acid; |
|
260 |
|
261 for (int i = 0; i < recipe->miscs.size(); i++) { |
|
262 qDebug() << i << recipe->miscs.at(i).m_name << my_acids[AT].name_en; |
|
263 if (recipe->miscs.at(i).m_name == my_acids[AT].name_en || recipe->miscs.at(i).m_name == my_acids[AT].name_nl) { |
|
264 qDebug() << "found at" << i << recipe->miscs.at(i).m_amount << Acid / 1000.0; |
|
265 recipe->miscs[i].m_amount = Acid / 1000.0; |
|
266 QTableWidgetItem *item = new QTableWidgetItem(QString("%1 ml").arg(Acid, 3, 'f', 2, '0')); |
|
267 item->setTextAlignment(Qt::AlignRight|Qt::AlignVCenter); |
|
268 ui->miscsTable->setItem(i, 4, item); |
|
269 this->ignoreChanges = true; |
|
270 ui->mw_acidvolEdit->setValue(Acid); |
|
271 this->ignoreChanges = false; |
|
272 break; |
|
273 } |
|
274 } |
|
275 bicarbonate = bicarbonate - protonDeficit * frac / liters; |
|
276 total_alkalinity = bicarbonate * 50 / 61; |
|
277 } |
|
278 ph = TpH; |
|
279 ui->wb_phEdit->setValue(ph); |
|
280 |
|
281 //recipe->est_mash_ph = ph |
|
282 } else { // Manual |
|
283 /* |
|
284 * Manual adjust acid, calculate resulting pH. |
|
285 */ |
|
286 } |
|
287 |
|
288 ui->wb_caEdit->setValue(calcium); |
|
289 ui->wb_mgEdit->setValue(magnesium); |
|
290 ui->wb_hco3Edit->setValue(bicarbonate); |
|
291 ui->wb_caco3Edit->setValue(total_alkalinity); |
|
292 ui->wb_naEdit->setValue(sodium); |
|
293 ui->wb_clEdit->setValue(chloride); |
|
294 ui->wb_so4Edit->setValue(sulfate); |
|
295 |
|
296 ui->wb_caEdit->setStyleSheet((calcium < 40 || calcium > 150) ? "background-color: red":"background-color: green"); |
|
297 ui->wb_mgEdit->setStyleSheet((magnesium < 5 || magnesium > 40) ? "background-color: red":"background-color: green"); |
|
298 ui->wb_naEdit->setStyleSheet((sodium > 150) ? "background-color: red":"background-color: green"); |
|
299 /* |
|
300 * Both chloride and sulfate should be above 50 according to |
|
301 * John Palmer. So the Cl/SO4 ratio calculation will work. |
|
302 */ |
|
303 ui->wb_clEdit->setStyleSheet((chloride <= 50 || chloride > 150) ? "background-color: red":"background-color: green"); |
|
304 ui->wb_so4Edit->setStyleSheet((sulfate <= 50 || sulfate > 400) ? "background-color: red":"background-color: green"); |
|
305 /* |
|
306 * (cloride + sulfate) > 500 is too high |
|
307 */ |
|
308 if ((chloride + sulfate) > 500) { |
|
309 ui->wb_clEdit->setStyleSheet("background-color: red"); |
|
310 ui->wb_so4Edit->setStyleSheet("background-color: red"); |
|
311 } |
|
312 ui->wb_phEdit->setStyleSheet((ph < 5.2 || ph > 5.6) ? "background-color: red":"background-color: green"); |
|
313 ui->wb_hco3Edit->setStyleSheet((bicarbonate > 250) ? "background-color: red":"background-color: green"); |
|
314 ui->wb_caco3Edit->setStyleSheet((bicarbonate > 250) ? "background-color: red":"background-color: green"); |
|
315 |
|
316 // calcSparge(); |
|
317 } |
|
318 |
|
319 |
|
320 void EditRecipe::on_calc_acid_clicked() |
|
321 { |
|
322 recipe->calc_acid = ! recipe->calc_acid; |
|
323 ui->mw_autoEdit->setChecked(recipe->calc_acid); |
|
324 ui->mw_phEdit->setReadOnly(! recipe->calc_acid); |
|
325 ui->mw_phEdit->setButtonSymbols(recipe->calc_acid ? QAbstractSpinBox::UpDownArrows : QAbstractSpinBox::NoButtons); |
|
326 ui->mw_acidvolEdit->setReadOnly(recipe->calc_acid); |
|
327 ui->mw_acidvolEdit->setButtonSymbols(recipe->calc_acid ? QAbstractSpinBox::NoButtons : QAbstractSpinBox::UpDownArrows); |
|
328 is_changed(); |
|
329 calcWater(); |
96 } |
330 } |
97 |
331 |
98 |
332 |
99 void EditRecipe::on_w2_vol_changed(double val) |
333 void EditRecipe::on_w2_vol_changed(double val) |
100 { |
334 { |