Almost finished calcFermentables()

Sun, 03 Apr 2022 17:43:45 +0200

author
Michiel Broek <mbroek@mbse.eu>
date
Sun, 03 Apr 2022 17:43:45 +0200
changeset 102
b017001850df
parent 101
1d14d3bf2465
child 103
6da4e93b6ceb

Almost finished calcFermentables()

src/EditRecipe.cpp file | annotate | diff | comparison | revisions
src/EditRecipe.h file | annotate | diff | comparison | revisions
src/MainWindow.h file | annotate | diff | comparison | revisions
src/Utils.cpp file | annotate | diff | comparison | revisions
src/Utils.h file | annotate | diff | comparison | revisions
ui/EditRecipe.ui file | annotate | diff | comparison | revisions
--- 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();
--- 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") };
--- 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 {
--- 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 <http://www.gnu.org/licenses/>.
  */
 #include "Utils.h"
+#include "MainWindow.h"
 
 #include <QDebug>
 #include <math.h>
@@ -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;
+}
+
+
--- 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
--- 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 @@
        <enum>QTabWidget::Rounded</enum>
       </property>
       <property name="currentIndex">
-       <number>1</number>
+       <number>4</number>
       </property>
       <property name="elideMode">
        <enum>Qt::ElideNone</enum>

mercurial