# HG changeset patch # User Michiel Broek # Date 1650046822 -7200 # Node ID e68b27ad8a406930161313ba0eafead37a37e00e # Parent 5099df8ba6c64d49d754d3792b9a0d59c49f91b1 Added dutch translations to the internal acids array. Added slot for calc_acid checkbox. Added more water calculations. The miscs amount fields now have two decimal digits. Show treated waters and good/bad indicators. diff -r 5099df8ba6c6 -r e68b27ad8a40 src/EditRecipe.cpp --- a/src/EditRecipe.cpp Thu Apr 14 22:47:05 2022 +0200 +++ b/src/EditRecipe.cpp Fri Apr 15 20:20:22 2022 +0200 @@ -50,9 +50,9 @@ ui->ibu_methodEdit->addItem("Daniels"); for (int i = 0; i < my_acids.size(); i++) { - qDebug() << i << my_acids.at(i).name; - ui->mw_acidPick->addItem(my_acids.at(i).name); - ui->sp_acidtypeEdit->addItem(my_acids.at(i).name); + qDebug() << i << my_acids.at(i).name_en; + ui->mw_acidPick->addItem(my_acids.at(i).name_en); + ui->sp_acidtypeEdit->addItem(my_acids.at(i).name_en); } ui->sp_sourceEdit->addItem(tr("Source 1")); @@ -595,6 +595,11 @@ ui->w2_clEdit->setValue(recipe->w2_chloride); ui->w2_so4Edit->setValue(recipe->w2_sulfate); ui->w2_phEdit->setValue(recipe->w2_ph); + ui->mw_autoEdit->setChecked(recipe->calc_acid); + ui->mw_phEdit->setReadOnly(! recipe->calc_acid); + ui->mw_phEdit->setButtonSymbols(recipe->calc_acid ? QAbstractSpinBox::UpDownArrows : QAbstractSpinBox::NoButtons); + ui->mw_acidvolEdit->setReadOnly(recipe->calc_acid); + ui->mw_acidvolEdit->setButtonSymbols(recipe->calc_acid ? QAbstractSpinBox::NoButtons : QAbstractSpinBox::UpDownArrows); ui->sp_volEdit->setValue(recipe->sparge_volume); ui->sp_tempEdit->setValue(recipe->sparge_temp); @@ -647,6 +652,7 @@ connect(ui->bs_mgcl2Edit, QOverload::of(&QDoubleSpinBox::valueChanged), this, &EditRecipe::on_mgcl2_changed); connect(ui->bs_nahco3Edit, QOverload::of(&QDoubleSpinBox::valueChanged), this, &EditRecipe::on_nahco3_changed); connect(ui->bs_caco3Edit, QOverload::of(&QDoubleSpinBox::valueChanged), this, &EditRecipe::on_caco3_changed); + connect(ui->mw_autoEdit, &QCheckBox::stateChanged, this, &EditRecipe::on_calc_acid_clicked); ui->saveButton->setEnabled(false); ui->deleteButton->setEnabled((id >= 0) ? true:false); diff -r 5099df8ba6c6 -r e68b27ad8a40 src/EditRecipe.h --- a/src/EditRecipe.h Thu Apr 14 22:47:05 2022 +0200 +++ b/src/EditRecipe.h Fri Apr 15 20:20:22 2022 +0200 @@ -303,6 +303,7 @@ void on_mgcl2_changed(double val); void on_nahco3_changed(double val); void on_caco3_changed(double val); + void on_calc_acid_clicked(); void on_perc_mash_valueChanged(int value); void on_perc_sugars_valueChanged(int value); @@ -352,6 +353,12 @@ void set_brewing_salt(QString salt, double val); void calcFermentables(); void calcIBUs(); + double ZAlkalinity(double pHZ); + double ZRA(double pHZ); + double BufferCapacity(Fermentables F); + double AcidRequired(double ZpH, Fermentables F); + double ProtonDeficit(double pHZ); + double MashpH(); void calcWater(); }; diff -r 5099df8ba6c6 -r e68b27ad8a40 src/EditRecipeTab4.cpp --- a/src/EditRecipeTab4.cpp Thu Apr 14 22:47:05 2022 +0200 +++ b/src/EditRecipeTab4.cpp Fri Apr 15 20:20:22 2022 +0200 @@ -85,9 +85,9 @@ ui->miscsTable->setItem(i, 3, item); if (recipe->miscs.at(i).m_amount_is_weight) - item = new QTableWidgetItem(QString("%1 gr").arg(recipe->miscs.at(i).m_amount * 1000.0, 2, 'f', 1, '0')); + item = new QTableWidgetItem(QString("%1 gr").arg(recipe->miscs.at(i).m_amount * 1000.0, 3, 'f', 2, '0')); else - item = new QTableWidgetItem(QString("%1 ml").arg(recipe->miscs.at(i).m_amount * 1000.0, 2, 'f', 1, '0')); + item = new QTableWidgetItem(QString("%1 ml").arg(recipe->miscs.at(i).m_amount * 1000.0, 3, 'f', 2, '0')); item->setTextAlignment(Qt::AlignRight|Qt::AlignVCenter); ui->miscsTable->setItem(i, 4, item); diff -r 5099df8ba6c6 -r e68b27ad8a40 src/EditRecipeTab7.cpp --- a/src/EditRecipeTab7.cpp Thu Apr 14 22:47:05 2022 +0200 +++ b/src/EditRecipeTab7.cpp Fri Apr 15 20:20:22 2022 +0200 @@ -29,6 +29,118 @@ } +/* + * Z alkalinity is the amount of acid (in mEq/l) needed to bring water to the target pH (Z pH) + */ +double EditRecipe::ZAlkalinity(double pHZ) +{ + double C43 = Utils::Charge(4.3); + double Cw = Utils::Charge(recipe->wg_ph); + double Cz = Utils::Charge(pHZ); + double DeltaCNaught = -C43 + Cw; + double CT = recipe->wg_total_alkalinity / 50 / DeltaCNaught; + double DeltaCZ = -Cz + Cw; + return CT * DeltaCZ; +} + + +/* + * Z Residual alkalinity is the amount of acid (in mEq/l) needed + * to bring the water in the mash to the target pH (Z pH). + */ +double EditRecipe::ZRA(double pHZ) +{ + double Calc = recipe->wg_calcium / (MMCa / 2); + double Magn = recipe->wg_magnesium / (MMMg / 2); + double Z = ZAlkalinity(pHZ); + return Z - (Calc / 3.5 + Magn / 7); +} + + +double EditRecipe::BufferCapacity(Fermentables F) +{ + double C1 = 0; + + if ((F.f_di_ph != 5.7) && ((F.f_acid_to_ph_57 < - 0.1) || (F.f_acid_to_ph_57 > 0.1))) { + C1 = F.f_acid_to_ph_57 / (F.f_di_ph - 5.7); + } else { + /* + * If the acid_to_ph_5.7 is unknown from the maltster, guess the required acid. + */ + switch (F.f_graintype) { + case 0: // Base, Special, Kilned + case 3: + case 5: C1 = 0.014 * F.f_color - 34.192; + break; + case 2: C1 = -0.0597 * F.f_color - 32.457; // Crystal + break; + case 1: C1 = 0.0107 * F.f_color - 54.768; // Roast + break; + case 4: C1 = -149; // Sour malt + break; + } + } + return C1; +} + + +double EditRecipe::AcidRequired(double ZpH, Fermentables F) +{ + double C1 = BufferCapacity(F); + double x = F.f_di_ph; + return C1 * (ZpH - x); +} + + +double EditRecipe::ProtonDeficit(double pHZ) +{ + double C1, x; + int i, error_count = 0; + double Result = ZRA(pHZ) * recipe->wg_amount; + Fermentables F; + + /* + * proton deficit for the grist + */ + if (recipe->fermentables.size()) { + for (i = 0; i < recipe->fermentables.size(); i++) { + F = recipe->fermentables.at(i); + if (F.f_added == 0 && F.f_graintype != 6) { // Added == Mash && graintype != No Malt + x = AcidRequired(pHZ, F) * F.f_amount; + Result += x; + } + } + } else { + error_count++; + if (error_count < 5) + qDebug() << "ProtonDeficit" << pHZ << "invalid grist, return" << Result; + } + return Result; +} + + +double EditRecipe::MashpH() +{ + int n = 0; + double pH = 5.4; + double deltapH = 0.001; + double deltapd = 0.1; + double pd = ProtonDeficit(pH); + + while (((pd < -deltapd) || (pd > deltapd)) && (n < 2000)) { + n++; + if (pd < -deltapd) + pH -= deltapH; + else if (pd > deltapd) + pH += deltapH; + pd = ProtonDeficit(pH); + } + pH = round(pH * 1000000) / 1000000.0; + qDebug() << "MashpH() n:" << n << "pH:" << pH; + return pH; +} + + void EditRecipe::calcWater() { double liters = 0; @@ -40,6 +152,11 @@ double chloride = 0; double sulfate = 0; double ph = 0; + double TpH = 0; + double frac; + double protonDeficit = 0; + double Acid = 0, Acidmg = 0; + int AT; qDebug() << "calcWater"; @@ -73,6 +190,7 @@ recipe->wg_chloride = round(chloride * 10.0) / 10.0; recipe->wg_sulfate = round(sulfate * 10.0) / 10.0; recipe->wg_total_alkalinity = round(total_alkalinity * 10.0) / 10.0; + recipe->wg_ph = ph; ui->wg_volEdit->setValue(liters); ui->wg_caEdit->setValue(calcium); @@ -93,6 +211,122 @@ double wg_sulfate = sulfate; double wg_bicarbonate = bicarbonate; + double mash_ph = MashpH(); + qDebug() << "Distilled water mash pH:" << mash_ph; + + /* Calculate Salt additions */ + if (liters > 0) { + calcium += ( ui->bs_cacl2Edit->value() * MMCa / MMCaCl2 * 1000 + ui->bs_caso4Edit->value() * MMCa / MMCaSO4 * 1000 + + ui->bs_caco3Edit->value() * MMCa / MMCaCO3 * 1000) / liters; + magnesium += (ui->bs_mgso4Edit->value() * MMMg / MMMgSO4 * 1000 + ui->bs_mgcl2Edit->value() * MMMg / MMMgCl2 * 1000) / liters; + sodium += (ui->bs_naclEdit->value() * MMNa / MMNaCl * 1000 + ui->bs_nahco3Edit->value() * MMNa / MMNaHCO3 * 1000) / liters; + sulfate += (ui->bs_caso4Edit->value() * MMSO4 / MMCaSO4 * 1000 + ui->bs_mgso4Edit->value() * MMSO4 / MMMgSO4 * 1000) / liters; + chloride += (2 * ui->bs_cacl2Edit->value() * MMCl / MMCaCl2 * 1000 + ui->bs_naclEdit->value() * MMCl / MMNaCl * 1000 + + ui->bs_mgcl2Edit->value() * MMCl / MMMgCl2 * 1000) / liters; + bicarbonate += (ui->bs_nahco3Edit->value() * MMHCO3 / MMNaHCO3 * 1000 + ui->bs_caco3Edit->value() / 3 * MMHCO3 / MMCaCO3 * 1000) / liters; + } + + if (recipe->wa_acid_name < 0 || recipe->wa_acid_name >= my_acids.size()) { + recipe->wa_acid_name = 0; + recipe->wa_acid_perc = my_acids.at(0).AcidPrc; + this->ignoreChanges = true; + ui->mw_acidPick->setCurrentIndex(0); + ui->mw_acidpercEdit->setValue(my_acids.at(0).AcidPrc); + this->ignoreChanges = false; + } + AT = recipe->wa_acid_name; + + /* + * Note that the next calculations do not correct the pH change by the added salts. + * This pH change is at most 0.1 pH and is a minor difference in Acid amount. + */ + if (recipe->calc_acid) { + /* + * Auto calculate the needed acid. + */ + TpH = recipe->mash_ph; + protonDeficit = ProtonDeficit(TpH); + qDebug() << "calc_acid tgt:" << TpH << "protonDeficit:" << protonDeficit; + if (protonDeficit > 0) { + qDebug() << "pkn:" << AT << my_acids[AT].pK1 << my_acids[AT].pK2 << my_acids[AT].pK3; + frac = Utils::CalcFrac(TpH, my_acids[AT].pK1, my_acids[AT].pK2, my_acids[AT].pK3); + Acid = protonDeficit / frac; + qDebug() << "1" << frac << Acid << protonDeficit; + Acid *= my_acids[AT].MolWt; // mg. + Acidmg = Acid; + Acid = Acid / my_acids[AT].AcidSG; + Acid = round((Acid / (recipe->wa_acid_perc / 100.0)) * 100.0) / 100.0; + qDebug() << "Mash auto Acid final ml:" << Acid; + + for (int i = 0; i < recipe->miscs.size(); i++) { + qDebug() << i << recipe->miscs.at(i).m_name << my_acids[AT].name_en; + if (recipe->miscs.at(i).m_name == my_acids[AT].name_en || recipe->miscs.at(i).m_name == my_acids[AT].name_nl) { + qDebug() << "found at" << i << recipe->miscs.at(i).m_amount << Acid / 1000.0; + recipe->miscs[i].m_amount = Acid / 1000.0; + QTableWidgetItem *item = new QTableWidgetItem(QString("%1 ml").arg(Acid, 3, 'f', 2, '0')); + item->setTextAlignment(Qt::AlignRight|Qt::AlignVCenter); + ui->miscsTable->setItem(i, 4, item); + this->ignoreChanges = true; + ui->mw_acidvolEdit->setValue(Acid); + this->ignoreChanges = false; + break; + } + } + bicarbonate = bicarbonate - protonDeficit * frac / liters; + total_alkalinity = bicarbonate * 50 / 61; + } + ph = TpH; + ui->wb_phEdit->setValue(ph); + + //recipe->est_mash_ph = ph + } else { // Manual + /* + * Manual adjust acid, calculate resulting pH. + */ + } + + ui->wb_caEdit->setValue(calcium); + ui->wb_mgEdit->setValue(magnesium); + ui->wb_hco3Edit->setValue(bicarbonate); + ui->wb_caco3Edit->setValue(total_alkalinity); + ui->wb_naEdit->setValue(sodium); + ui->wb_clEdit->setValue(chloride); + ui->wb_so4Edit->setValue(sulfate); + + ui->wb_caEdit->setStyleSheet((calcium < 40 || calcium > 150) ? "background-color: red":"background-color: green"); + ui->wb_mgEdit->setStyleSheet((magnesium < 5 || magnesium > 40) ? "background-color: red":"background-color: green"); + ui->wb_naEdit->setStyleSheet((sodium > 150) ? "background-color: red":"background-color: green"); + /* + * Both chloride and sulfate should be above 50 according to + * John Palmer. So the Cl/SO4 ratio calculation will work. + */ + ui->wb_clEdit->setStyleSheet((chloride <= 50 || chloride > 150) ? "background-color: red":"background-color: green"); + ui->wb_so4Edit->setStyleSheet((sulfate <= 50 || sulfate > 400) ? "background-color: red":"background-color: green"); + /* + * (cloride + sulfate) > 500 is too high + */ + if ((chloride + sulfate) > 500) { + ui->wb_clEdit->setStyleSheet("background-color: red"); + ui->wb_so4Edit->setStyleSheet("background-color: red"); + } + ui->wb_phEdit->setStyleSheet((ph < 5.2 || ph > 5.6) ? "background-color: red":"background-color: green"); + ui->wb_hco3Edit->setStyleSheet((bicarbonate > 250) ? "background-color: red":"background-color: green"); + ui->wb_caco3Edit->setStyleSheet((bicarbonate > 250) ? "background-color: red":"background-color: green"); + + // calcSparge(); +} + + +void EditRecipe::on_calc_acid_clicked() +{ + recipe->calc_acid = ! recipe->calc_acid; + ui->mw_autoEdit->setChecked(recipe->calc_acid); + ui->mw_phEdit->setReadOnly(! recipe->calc_acid); + ui->mw_phEdit->setButtonSymbols(recipe->calc_acid ? QAbstractSpinBox::UpDownArrows : QAbstractSpinBox::NoButtons); + ui->mw_acidvolEdit->setReadOnly(recipe->calc_acid); + ui->mw_acidvolEdit->setButtonSymbols(recipe->calc_acid ? QAbstractSpinBox::NoButtons : QAbstractSpinBox::UpDownArrows); + is_changed(); + calcWater(); } diff -r 5099df8ba6c6 -r e68b27ad8a40 src/MainWindow.cpp --- a/src/MainWindow.cpp Thu Apr 14 22:47:05 2022 +0200 +++ b/src/MainWindow.cpp Fri Apr 15 20:20:22 2022 +0200 @@ -60,37 +60,41 @@ openWS(useDevelopOption); Acid a; - a.name = "Lactic"; + a.name_en = "Lactic"; + a.name_nl = "Melkzuur"; a.pK1 = 3.86; - a.pK2 = 20; - a.pK3 = 20; + a.pK2 = 20.0; + a.pK3 = 20.0; a.MolWt = 90.08; - a.AcidSG = 1238; - a.AcidPrc = 80; + a.AcidSG = 1238.0; + a.AcidPrc = 80.0; my_acids.append(a); - a.name = "Hydrochloric"; - a.pK1 = -7; - a.pK2 = 20; - a.pK3 = 20; + a.name_en = "Hydrochloric"; + a.name_nl = "Zoutzuur"; + a.pK1 = -7.0; + a.pK2 = 20.0; + a.pK3 = 20.0; a.MolWt = 36.46; - a.AcidSG = 1497; - a.AcidPrc = 28; + a.AcidSG = 1497.0; + a.AcidPrc = 28.0; my_acids.append(a); - a.name = "Phosphoric"; + a.name_en = "Phosphoric"; + a.name_nl = "Fosforzuur"; a.pK1 = 2.12; a.pK2 = 7.20; a.pK3 = 12.44; a.MolWt = 98.00; - a.AcidSG = 1982; - a.AcidPrc = 75; + a.AcidSG = 1982.0; + a.AcidPrc = 75.0; my_acids.append(a); - a.name = "Sulfuric"; - a.pK1 = -1; + a.name_en = "Sulfuric"; + a.name_nl = "Zwavelzuur"; + a.pK1 = -1.0; a.pK2 = 1.92; - a.pK3 = 20; + a.pK3 = 20.0; a.MolWt = 98.07; - a.AcidSG = 1884; - a.AcidPrc = 93; + a.AcidSG = 1884.0; + a.AcidPrc = 93.0; my_acids.append(a); qDebug() << "acids" << my_acids.size(); diff -r 5099df8ba6c6 -r e68b27ad8a40 src/Utils.cpp --- a/src/Utils.cpp Thu Apr 14 22:47:05 2022 +0200 +++ b/src/Utils.cpp Fri Apr 15 20:20:22 2022 +0200 @@ -481,20 +481,34 @@ double Utils::PartCO3(double pH) { - double H = pow(10, -pH); - return 100 * Ka1 * Ka2 / (H * H + H * Ka1 + Ka1 * Ka2); + double H = pow(10.0, -pH); + return 100.0 * Ka1 * Ka2 / (H * H + H * Ka1 + Ka1 * Ka2); } double Utils::PartHCO3(double pH) { - double H = pow(10, -pH); - return 100 * Ka1 * H / (H * H + H * Ka1 + Ka1 * Ka2); + double H = pow(10.0, -pH); + return 100.0 * Ka1 * H / (H * H + H * Ka1 + Ka1 * Ka2); } double Utils::Charge(double pH) { - return (-2 * PartCO3(pH) - PartHCO3(pH)); + return (-2.0 * PartCO3(pH) - PartHCO3(pH)); } + +double Utils::CalcFrac(double TpH, double pK1, double pK2, double pK3) +{ + double r1d = pow(10.0, TpH - pK1); + double r2d = pow(10.0, TpH - pK2); + double r3d = pow(10.0, TpH - pK3); + double dd = 1.0 / (1.0 + r1d + r1d * r2d + r1d * r2d * r3d); + double f2d = r1d * dd; + double f3d = r1d * r2d * dd; + double f4d = r1d * r2d * r3d * dd; + return f2d + 2.0 * f3d + 3.0 * f4d; +} + + diff -r 5099df8ba6c6 -r e68b27ad8a40 src/Utils.h --- a/src/Utils.h Thu Apr 14 22:47:05 2022 +0200 +++ b/src/Utils.h Fri Apr 15 20:20:22 2022 +0200 @@ -81,6 +81,8 @@ double PartHCO3(double pH); double Charge(double pH); + + double CalcFrac(double TpH, double pK1, double pK2, double pK3); } #endif diff -r 5099df8ba6c6 -r e68b27ad8a40 src/global.h --- a/src/global.h Thu Apr 14 22:47:05 2022 +0200 +++ b/src/global.h Fri Apr 15 20:20:22 2022 +0200 @@ -34,7 +34,8 @@ struct Acid { - QString name; + QString name_en; + QString name_nl; double pK1; double pK2; double pK3; diff -r 5099df8ba6c6 -r e68b27ad8a40 ui/EditRecipe.ui --- a/ui/EditRecipe.ui Thu Apr 14 22:47:05 2022 +0200 +++ b/ui/EditRecipe.ui Fri Apr 15 20:20:22 2022 +0200 @@ -3733,8 +3733,14 @@ Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + true + + + QAbstractSpinBox::NoButtons + - true + false %