# HG changeset patch # User Michiel Broek # Date 1649000625 -7200 # Node ID b017001850dfc56eb93dbaa193f1d511290dcd0c # Parent 1d14d3bf24653b415b2491314215c6afc498328b Almost finished calcFermentables() diff -r 1d14d3bf2465 -r b017001850df src/EditRecipe.cpp --- a/src/EditRecipe.cpp Sat Apr 02 23:01:13 2022 +0200 +++ b/src/EditRecipe.cpp Sun Apr 03 17:43:45 2022 +0200 @@ -97,7 +97,7 @@ ui->est_abvShow->setMarkerTextIsValue(true); ui->est_abvShow->setValue(query.value(30).toDouble()); - QColor color = Utils::ebc_to_color(query.value(31).toInt()); + //QColor color = Utils::ebc_to_color(query.value(31).toInt()); ui->est_colorEdit->setValue(query.value(31).toDouble()); ui->est_colorEdit->setStyleSheet(Utils::ebc_to_style(query.value(31).toInt())); ui->est_color2Edit->setValue(query.value(31).toDouble()); @@ -173,46 +173,11 @@ // 82 wa_acid_perc // 83 wa_base_name - /* - * Progress bars. - * perc_maltShow pmalts = mashkg / (dataRecord.boil_size / 3) * 100; - * perc_sugarsShow if (row.f_type == 1 && row.f_added < 4) // Sugar - * psugar += row.f_percentage; - * perc_caraShow if (row.f_graintype == 2 && row.f_added < 4) // Crystal - * pcara += row.f_percentage; - * lintner if (row.f_added == 0 && (row.f_type == 0 || row.f_type == 4) && row.f_color < 50) - * lintner += row.f_diastatic_power * row.f_amount; - * lintner = parseFloat(lintner / mashkg) - * - * perc_malts range(0, 120) - * stop: 90, color: '#008C00' - * stop: 100, color: '#EB7331' - * stop: 120, color: '#FF0000' - * - * perc_sugars range(0, 50) - * stop: 20, color: '#008C00' - * stop: 50, color: '#FF0000' - * perc_cara range(0, 50) - * stop: 25, color: '#008C00' - * stop: 50, color: '#FF0000' - * lintner range(0, 200) - * stop: 30, color: '#FF0000' red - * stop: 40, color: '#EB7331' orange - * stop: 200, color: '#008C00' green - */ - -// ui->lintnerShow->setValue(52); - if (ui->lintnerShow->value() < 30) - ui->lintnerShow->setStyleSheet(bar_red); - else if (ui->lintnerShow->value() < 40) - ui->lintnerShow->setStyleSheet(bar_orange); - else - ui->lintnerShow->setStyleSheet(bar_green); QJsonParseError parseError; - const auto& json = query.value(84).toString(); - if (!json.trimmed().isEmpty()) { - const auto& formattedJson = QString("%1").arg(json); + const auto& f_json = query.value(84).toString(); + if (!f_json.trimmed().isEmpty()) { + const auto& formattedJson = QString("%1").arg(f_json); this->fermentables = QJsonDocument::fromJson(formattedJson.toUtf8(), &parseError); if (parseError.error != QJsonParseError::NoError) qDebug() << "Parse error: " << parseError.errorString() << "at" << parseError.offset ; @@ -220,10 +185,45 @@ qDebug() << "empty fermentables"; } - // 85 json_hops - // 86 json_miscs - // 87 json_yeasts - // 88 json_mashs + const auto& h_json = query.value(85).toString(); + if (!h_json.trimmed().isEmpty()) { + const auto& formattedJson = QString("%1").arg(h_json); + this->hops = QJsonDocument::fromJson(formattedJson.toUtf8(), &parseError); + if (parseError.error != QJsonParseError::NoError) + qDebug() << "Parse error: " << parseError.errorString() << "at" << parseError.offset ; + } else { + qDebug() << "empty hops"; + } + + const auto& m_json = query.value(86).toString(); + if (!m_json.trimmed().isEmpty()) { + const auto& formattedJson = QString("%1").arg(m_json); + this->miscs = QJsonDocument::fromJson(formattedJson.toUtf8(), &parseError); + if (parseError.error != QJsonParseError::NoError) + qDebug() << "Parse error: " << parseError.errorString() << "at" << parseError.offset ; + } else { + qDebug() << "empty miscs"; + } + + const auto& y_json = query.value(87).toString(); + if (!y_json.trimmed().isEmpty()) { + const auto& formattedJson = QString("%1").arg(y_json); + this->yeasts = QJsonDocument::fromJson(formattedJson.toUtf8(), &parseError); + if (parseError.error != QJsonParseError::NoError) + qDebug() << "Parse error: " << parseError.errorString() << "at" << parseError.offset ; + } else { + qDebug() << "empty yeasts"; + } + + const auto& ma_json = query.value(88).toString(); + if (!ma_json.trimmed().isEmpty()) { + const auto& formattedJson = QString("%1").arg(ma_json); + this->mashs = QJsonDocument::fromJson(formattedJson.toUtf8(), &parseError); + if (parseError.error != QJsonParseError::NoError) + qDebug() << "Parse error: " << parseError.errorString() << "at" << parseError.offset ; + } else { + qDebug() << "empty mashs"; + } } else { /* Set some defaults */ @@ -245,11 +245,15 @@ connect(ui->efficiencyEdit, &QDoubleSpinBox::textChanged, this, &EditRecipe::is_changed); connect(ui->beerstyleEdit, &QComboBox::currentTextChanged, this, &EditRecipe::style_changed); connect(ui->est_ogEdit, &QDoubleSpinBox::textChanged, this, &EditRecipe::is_changed); - connect(ui->color_methodEdit, &QComboBox::currentTextChanged, this, &EditRecipe::is_changed); + connect(ui->color_methodEdit, &QComboBox::currentTextChanged, this, &EditRecipe::colormethod_changed); connect(ui->ibu_methodEdit, &QComboBox::currentTextChanged, this, &EditRecipe::is_changed); // All signals from tab "Fermentables" connect(ui->est_og2Edit, &QDoubleSpinBox::textChanged, this, &EditRecipe::is_changed); + connect(ui->perc_mashShow, &QProgressBar::valueChanged, this, &EditRecipe::on_perc_mash_valueChanged); + connect(ui->perc_sugarsShow, &QProgressBar::valueChanged, this, &EditRecipe::on_perc_sugars_valueChanged); + connect(ui->perc_caraShow, &QProgressBar::valueChanged, this, &EditRecipe::on_perc_cara_valueChanged); + connect(ui->lintnerShow, &QProgressBar::valueChanged, this, &EditRecipe::on_lintner_valueChanged); // connect(ui->fermentablesTable, SIGNAL(cellChanged(int, int)), this, SLOT(cell_Changed(int, int))); // All signals from tab "Hops" @@ -445,7 +449,7 @@ { int i; bool my_100 = false; - double psugar = 0, pcara = 0, d, s = 0, x; + double psugar = 0, pcara = 0, d, s = 0, x, color; double vol = 0; // Volume sugars after boil double addedS = 0; // Added sugars after boil double addedmass = 0; // Added mass after boil @@ -455,11 +459,36 @@ double sugarsf = 0; // fermentable sugars mash + boil double sugarsm = 0; // fermentable sugars in mash double sugardensity = 1.611; // kg/l in solution + double mashtime = 0; // Total mash time + double mashtemp = 0; // Average mash temperature + double mashinfuse = 0; // Mash infuse amount + double colort = 0; // Colors srm * vol totals + double colorh = 0; // Colors ebc * vol * kt + double colorn = 0; // Colors ebc * pt * pct QJsonObject obj; qDebug() << "calcFermentables()"; - // Get mashtemp and mashtime from the Mash schedule. + /* + * Get average mashtemp and mashtime from the Mash schedule. + * It is possible that the schedule is not (yet) present. + */ + if (this->mashs.array().size() > 0) { + for (i = 0; i < this->mashs.array().size(); i++) { + obj = this->mashs.array().at(i).toObject(); + if (obj["step_type"].toInt() == 0) // Infusion + mashinfuse += obj["step_infuse_amount"].toDouble(); + if (obj["step_temp"].toDouble() < 75) { // Ignore mashout + mashtime += obj["step_time"].toDouble(); + mashtemp += obj["step_time"].toDouble() * obj["step_temp"].toDouble(); + } + } + mashtemp = mashtemp / mashtime; + mvol = mashinfuse; + qDebug() << " mash time" << mashtime << "temp" << mashtemp << "infuse" << mashinfuse; + } else { + qDebug() << " no mash schedule"; + } if (this->fermentables.array().size() < 1) { qDebug() << " no fermentables, return."; @@ -497,12 +526,139 @@ lintner += obj["f_diastatic_power"].toDouble() * obj["f_amount"].toDouble(); } if (obj["f_added"].toInt() < 4) { - // colors + colort += obj["f_amount"].toDouble() * Utils::ebc_to_srm(obj["f_color"].toDouble()); + colorh += obj["f_amount"].toDouble() * obj["f_color"].toDouble() * Utils::get_kt(obj["f_color"].toDouble()); + colorn += (obj["f_percentage"].toDouble() / 100) * obj["f_color"].toDouble(); // For 8.6 Pt wort. } } + qDebug() << " colort" << colort << "colorh" << colorh << "colorn" << colorn; + qDebug() << " psugar" << psugar << "pcara" << pcara << "mvol" << mvol; + qDebug() << " sugarsf" << sugarsf << "sugarsm" << sugarsm; + double og = Utils::estimate_sg(sugarsf + addedS, ui->batch_sizeEdit->value()); + qDebug() << " OG" << ui->est_ogEdit->value() << og; + ui->est_ogEdit->setValue(og); + ui->est_og2Edit->setValue(og); + ui->est_og3Edit->setValue(og); + ui->est_ogShow->setValue(og); + + double preboil_sg = Utils::estimate_sg(sugarsm, ui->boil_sizeEdit->value()); + qDebug() << " preboil SG" << preboil_sg; + + /* + * Color of the wort + */ + if (ui->color_methodEdit->currentIndex() == 4) { // Naudts + color = round(((Utils::sg_to_plato(og) / 8.6) * colorn) + (ui->boil_timeEdit->value() / 60)); + } else if (ui->color_methodEdit->currentIndex() == 3) { // Hans Halberstadt + double bv = 0.925; // Beer loss efficiency + double sr = 0.95; // Mash and sparge efficiency + color = round((4.46 * bv * sr) / ui->batch_sizeEdit->value() * colorh); + } else { + double cw = colort / ui->batch_sizeEdit->value() * 8.34436; + color = Utils::kw_to_ebc(ui->color_methodEdit->currentIndex(), cw); + } + qDebug() << " color" << ui->est_colorEdit->value() << color; + ui->est_colorEdit->setValue(color); + ui->est_colorEdit->setStyleSheet(Utils::ebc_to_style(color)); + ui->est_color2Edit->setValue(color); + ui->est_color2Edit->setStyleSheet(Utils::ebc_to_style(color)); + ui->est_colorShow->setValue(color); + + /* + * We don't have a equipment profile in recipes, + * so we assume a certain guessed mashtun size. + */ + ui->perc_mashShow->setValue(round(mashkg / (ui->boil_sizeEdit->value() / 3) * 100)); + ui->perc_sugarsShow->setValue(round(psugar)); + ui->perc_caraShow->setValue(round(pcara)); qDebug() << " lintner" << lintner << " mashkg" << mashkg << "final" << round(lintner / mashkg); ui->lintnerShow->setValue(round(lintner / mashkg)); + + /* + * Calculate the apparant attenuation. + */ + double svg = 0; + if (this->yeasts.array().size() > 0) { + for (i = 0; i < this->yeasts.array().size(); i++) { + obj = this->yeasts.array().at(i).toObject(); + if (obj["y_use"].toInt() == 0) { // Used in primary + if (obj["y_attenuation"].toDouble() > svg) + svg = obj["y_attenuation"].toDouble(); // Take the highest if multiple yeasts. + } + // TODO: brett or others in secondary. + } + qDebug() << " SVG" << svg; + } + if (svg == 0) + svg = 77.0; + + double fg; + if (mashkg > 0 && mashinfuse > 0 && mashtime > 0 && mashtemp > 0) + fg = Utils::estimate_fg(psugar, pcara, mashinfuse / mashkg, mashtime, mashtemp, svg, og); + else + fg = Utils::estimate_fg(psugar, pcara, 0, 0, 0, svg, og); + qDebug() << " FG" << ui->est_fgEdit->value() << fg; + ui->est_fgEdit->setValue(fg); + ui->est_fg3Edit->setValue(fg); + ui->est_fgShow->setValue(fg); + + double abv = Utils::abvol(og, fg); + qDebug() << " ABV" << ui->est_abvEdit->value() << abv; + ui->est_abvEdit->setValue(abv); + ui->est_abv2Edit->setValue(abv); + ui->est_abvShow->setValue(abv); + + /* + * Calculate kilocalories/liter. Formula from brouwhulp. + * Take the alcohol and sugar parts and then combine. + */ + double alc = 1881.22 * fg * (og - fg) / (1.775 - og); + double sug = 3550 * fg * (0.1808 * og + 0.8192 * fg - 1.0004); + qDebug() << " kcal" << round((alc + sug) / (12 * 0.0295735296)); + + // If to_100 then make all amount fields t/o and percent fields r/w + +} + + +void EditRecipe::on_perc_mash_valueChanged(int value) +{ + if (value < 90) + ui->perc_mashShow->setStyleSheet(bar_green); + else if (value < 100) + ui->perc_mashShow->setStyleSheet(bar_orange); + else + ui->perc_mashShow->setStyleSheet(bar_red); +} + + +void EditRecipe::on_perc_sugars_valueChanged(int value) +{ + if (value < 20) + ui->perc_sugarsShow->setStyleSheet(bar_green); + else + ui->perc_sugarsShow->setStyleSheet(bar_red); +} + + +void EditRecipe::on_perc_cara_valueChanged(int value) +{ + if (value < 25) + ui->perc_caraShow->setStyleSheet(bar_green); + else + ui->perc_caraShow->setStyleSheet(bar_red); +} + + +void EditRecipe::on_lintner_valueChanged(int value) +{ + if (value < 30) + ui->lintnerShow->setStyleSheet(bar_red); + else if (value < 40) + ui->lintnerShow->setStyleSheet(bar_orange); + else + ui->lintnerShow->setStyleSheet(bar_green); } @@ -637,6 +793,13 @@ } +void EditRecipe::colormethod_changed() +{ + calcFermentables(); + is_changed(); +} + + void EditRecipe::time_changed() { is_changed(); diff -r 1d14d3bf2465 -r b017001850df src/EditRecipe.h --- a/src/EditRecipe.h Sat Apr 02 23:01:13 2022 +0200 +++ b/src/EditRecipe.h Sun Apr 03 17:43:45 2022 +0200 @@ -26,6 +26,7 @@ void on_deleteButton_clicked(); void is_changed(); void style_changed(); + void colormethod_changed(); void time_changed(); void refreshFermentables(); void refreshHops(); @@ -35,6 +36,11 @@ void refreshAll(); void on_deleteFermentRow_clicked(); + void on_perc_mash_valueChanged(int value); + void on_perc_sugars_valueChanged(int value); + void on_perc_cara_valueChanged(int value); + void on_lintner_valueChanged(int value); + private: Ui::EditRecipe *ui; QStringList s_types = { tr("Lager"), tr("Ale"), tr("Mead"), tr("Wheat"), tr("Mixed"), tr("Cider") }; diff -r 1d14d3bf2465 -r b017001850df src/MainWindow.h --- a/src/MainWindow.h Sat Apr 02 23:01:13 2022 +0200 +++ b/src/MainWindow.h Sun Apr 03 17:43:45 2022 +0200 @@ -77,6 +77,7 @@ static IniWS wsProd; static IniWS wsDev; +static double my_brix_correction = 1.04; namespace Ui { diff -r 1d14d3bf2465 -r b017001850df src/Utils.cpp --- a/src/Utils.cpp Sat Apr 02 23:01:13 2022 +0200 +++ b/src/Utils.cpp Sun Apr 03 17:43:45 2022 +0200 @@ -15,6 +15,7 @@ * along with this program. If not, see . */ #include "Utils.h" +#include "MainWindow.h" #include #include @@ -176,3 +177,156 @@ return srm_to_style(ebc_to_srm(ebc)); } + +/* + * Return incremented color by the boil and yeast. + * https://www.hobbybrouwen.nl/forum/index.php/topic,19020.msg281132.html#msg281132 + */ +double Utils::get_kt(int ebc) +{ + double kt = 1; + + if (ebc < 3) + kt = 3.5; + else if (ebc < 6) + kt = 3; + else if (ebc < 8) + kt = 2.75; + else if (ebc < 10) + kt = 2.5; + else if (ebc < 20) + kt = 1.8; + else if (ebc < 30) + kt = 1.6; + else if (ebc < 60) + kt = 1.3; + else if (ebc < 100) + kt = 1.2; + else if (ebc < 300) + kt = 1.1; + return kt; +} + + +double Utils::sg_to_plato(double sg) +{ + return -668.962 + (1262.45 * sg) - (776.43 * sg * sg) + (182.94 * sg * sg * sg); +} + + +double Utils::plato_to_sg(double plato) +{ + return 1.00001 + (0.0038661 * plato) + (1.3488e-5 * plato * plato) + (4.3074e-8 * plato * plato * plato); +} + + +double Utils::sg_to_brix(double sg) +{ + return sg_to_plato(sg) * my_brix_correction; +} + + +double Utils::brix_to_sg(double brix) +{ + if (my_brix_correction > 0) + return plato_to_sg(brix / my_brix_correction); + return plato_to_sg(brix); +} + + +double Utils::calc_svg(double og, double fg) +{ + double oe = sg_to_plato(og); + double ae = sg_to_plato(fg); + + return (oe - ae) / oe * 100; +} + + +double Utils::estimate_sg(double sugars, double batch_size) +{ + double plato = 100 * sugars / batch_size; + double sg = plato_to_sg(plato); + for (int i = 0; i < 20; i++) { + if (sg > 0) + plato = 100 * sugars / (batch_size * sg); + sg = plato_to_sg(plato); + } + + return round(sg * 10000) / 10000; +} + + +double Utils::estimate_fg(double psugar, double pcara, double wgratio, double mashtime, double mashtemp, double svg, double og) +{ + double BD; + + if (psugar > 40) + psugar = 0; + if (pcara > 50) + pcara = 0; + + if (wgratio > 0 && mashtime > 0) { + BD = wgratio; + if (BD < 2) + BD = 2; + if (BD > 5.5) + BD = 5.5; + if (mashtemp < 60) + mashtemp = 60; + if (mashtemp > 72) + mashtemp = 72; + } else { + BD = 3.5; + mashtemp = 67; + mashtime = 75; + } + if (svg < 30) + svg = 77; + + /* + * From brouwhulp: + * 0.00825 Attenuation factor yeast + * 0.00817 Attenuation factor water/grain ratio + * -0.00684 Attenuation factor mash temperature + * 0.00026 Attenuation factor total mash time (at some places this is 0.0026 this is wrong!) + * -0.00356 Attenuation factor percentage crystal malt + * 0.00553 Attenuation factor percentage simple sugars + * 0.547 Attenuation factor constant + */ + double AttBeer = 0.00825 * svg + 0.00817 * BD - 0.00684 * mashtemp + 0.00026 * mashtime - 0.00356 * pcara + 0.00553 * psugar + 0.547; + return round((1 + (1 - AttBeer) * (og -1)) * 10000) / 10000; +} + + +/* + * Kleurwerking to SRM. Not for Halberstadt, Naudts. + */ +double Utils::kw_to_srm(int colormethod, double c) +{ + if (colormethod == 0) + return 1.4922 * pow(c, 0.6859); //Morey + if (colormethod == 1) + return 0.3 * c + 4.7; //Mosher + if (colormethod == 2) + return 0.2 * c + 8.4; //Daniels + return 0; //Halberstadt,Naudts +} + + +double Utils::kw_to_ebc(int colormethod, double c) +{ + return srm_to_ebc(kw_to_srm(colormethod, c)); +} + + +double Utils::abvol(double og, double fg) +{ + if (((og - fg) < 0) || (fg < 0.9)) + return 0; + + double factor = og * 3157 * pow(10, -5) + 9.716 * pow(10, -2); + return round((og * 1000 - fg * 1000) * factor * 100) / 100; +} + + diff -r 1d14d3bf2465 -r b017001850df src/Utils.h --- a/src/Utils.h Sat Apr 02 23:01:13 2022 +0200 +++ b/src/Utils.h Sun Apr 03 17:43:45 2022 +0200 @@ -16,7 +16,18 @@ double kolbach_to_lintner(double kolbach); double ebc_to_srm(double ebc); double srm_to_ebc(double srm); - + double get_kt(int ebc); + double plato_to_sg(double plato); + double sg_to_plato(double sg); + double brix_to_sg(double brix); + double sg_to_brix(double sg); + double brix_to_fg(double o_plato, double refracto); + double calc_svg(double og, double fg); + double estimate_sg(double sugars, double batch_size); + double estimate_fg(double psugar, double pcara, double wgratio, double mashtime, double mashtemp, double svg, double og); + double kw_to_srm(int colormethod, double c); + double kw_to_ebc(int colormethod, double c); + double abvol(double og, double fg); QString hours_to_string(int hours); /** @@ -46,6 +57,8 @@ * @return A QString with stylesheet colors. */ QString ebc_to_style(int srm); + +// double my_brix_correction = 1.04; } #endif diff -r 1d14d3bf2465 -r b017001850df ui/EditRecipe.ui --- a/ui/EditRecipe.ui Sat Apr 02 23:01:13 2022 +0200 +++ b/ui/EditRecipe.ui Sun Apr 03 17:43:45 2022 +0200 @@ -95,7 +95,7 @@ QTabWidget::Rounded - 1 + 4 Qt::ElideNone