# HG changeset patch # User Michiel Broek # Date 1657176717 -7200 # Node ID 8133cdb19aa195da21fdbf0831c85f4d82e822e4 # Parent e97f9e87d94baabcbfedde4d83503701e13a4dd5 Added my_height global variable and edit in profile setup. This sets the height of the brewery above or below sealevel. Added calculations for the air pressure, boilpoint and IBU_reduction that all depend on the height. Currently not yet in use. Split out the real IBU calculation from the generic toIBU function. This has no effect on the results. diff -r e97f9e87d94b -r 8133cdb19aa1 src/MainWindow.cpp --- a/src/MainWindow.cpp Tue Jul 05 14:31:39 2022 +0200 +++ b/src/MainWindow.cpp Thu Jul 07 08:51:57 2022 +0200 @@ -126,20 +126,21 @@ */ QSqlQuery query("SELECT * FROM profile_setup WHERE record='1'"); query.first(); - my_brewery_name = query.value(1).toString(); - my_logoByteArray = query.value(2).toByteArray(); - my_factor_mashhop = query.value(3).toInt(); - my_factor_fwh = query.value(4).toInt(); - my_factor_pellet = query.value(5).toInt(); - my_factor_plug = query.value(6).toInt(); - my_factor_wethop = query.value(7).toInt(); - my_factor_cryohop = query.value(8).toInt(); - my_ibu_method = query.value(9).toInt(); - my_color_method = query.value(10).toInt(); - my_brix_correction = query.value(11).toDouble(); - my_grain_absorbtion = query.value(12).toDouble(); - my_default_water = query.value(13).toInt(); - my_yeastlab = query.value(14).toString(); + my_brewery_name = query.value("brewery_name").toString(); + my_logoByteArray = query.value("brewery_logo").toByteArray(); + my_factor_mashhop = query.value("factor_mashhop").toInt(); + my_factor_fwh = query.value("factor_fwh").toInt(); + my_factor_pellet = query.value("factor_pellet").toInt(); + my_factor_plug = query.value("factor_plug").toInt(); + my_factor_wethop = query.value("factor_wethop").toInt(); + my_factor_cryohop = query.value("factor_cryohop").toInt(); + my_ibu_method = query.value("ibu_method").toInt(); + my_color_method = query.value("color_method").toInt(); + my_brix_correction = query.value("brix_correction").toDouble(); + my_grain_absorbtion = query.value("grain_absorbtion").toDouble(); + my_default_water = query.value("default_water").toInt(); + my_yeastlab = query.value("my_yeastlab").toString(); + my_height = query.value("brewery_height").toInt(); qInfo() << "loadSetup" << my_brewery_name; } diff -r e97f9e87d94b -r 8133cdb19aa1 src/Setup.cpp --- a/src/Setup.cpp Tue Jul 05 14:31:39 2022 +0200 +++ b/src/Setup.cpp Thu Jul 07 08:51:57 2022 +0200 @@ -158,6 +158,12 @@ brixLabel->setAlignment(Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter); brixLabel->setText(tr("Brix Correction factor:")); + heightLabel = new QLabel(topWidget); + heightLabel->setObjectName(QString::fromUtf8("heightLabel")); + heightLabel->setGeometry(QRect(400, 180, 161, 20)); + heightLabel->setAlignment(Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter); + heightLabel->setText(tr("Brewery height meters:")); + titleLabel = new QLabel(topWidget); titleLabel->setObjectName(QString::fromUtf8("titleLabel")); titleLabel->setGeometry(QRect(5, 80, 1251, 20)); @@ -219,6 +225,17 @@ brixEdit->setValue(1.000000000000000); brixEdit->setToolTip(tr("Plato to Brix conversion factor.")); + heightEdit = new QSpinBox(topWidget); + heightEdit->setObjectName(QString::fromUtf8("heightEdit")); + heightEdit->setGeometry(QRect(580, 180, 101, 24)); + heightEdit->setAlignment(Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter); + heightEdit->setAccelerated(true); + heightEdit->setMinimum(-430); + heightEdit->setMaximum(8849); + heightEdit->setStepType(QAbstractSpinBox::DefaultStepType); + heightEdit->setValue(0); + heightEdit->setToolTip(tr("Height in meters above/below sealevel to calculate the exact boiling point and hop isomerizon.")); + colorEdit = new QComboBox(topWidget); colorEdit->setObjectName(QString::fromUtf8("colorEdit")); colorEdit->setGeometry(QRect(940, 120, 161, 23)); @@ -314,8 +331,10 @@ grainEdit->setValue(query.value("grain_absorbtion").toDouble()); brixEdit->setValue(query.value("brix_correction").toDouble()); + heightEdit->setValue(query.value("brewery_height").toInt()); connect(grainEdit, &QDoubleSpinBox::textChanged, this, &Setup::is_changed); connect(brixEdit, &QDoubleSpinBox::textChanged, this, &Setup::is_changed); + connect(heightEdit, &QSpinBox::textChanged, this, &Setup::is_changed); colorEdit->addItem("Morey"); colorEdit->addItem("Mosher"); @@ -453,7 +472,8 @@ */ query.prepare("UPDATE profile_setup SET brewery_name=:brewery, brewery_logo=:logo, factor_mashhop=:mashhop, factor_fwh=:fwh, " "factor_pellet=:pellet, factor_plug=:plug, factor_wethop=:wet, factor_cryohop=:cryo, color_method=:color, ibu_method=:ibu, " - "brix_correction=:brix, grain_absorbtion=:grain, default_water=:water, my_yeastlab=:yeast WHERE record='1'"); + "brix_correction=:brix, grain_absorbtion=:grain, default_water=:water, my_yeastlab=:yeast, " + "brewery_height=:height WHERE record='1'"); query.bindValue(":brewery", this->breweryEdit->text()); query.bindValue(":logo", logoByteArray); query.bindValue(":mashhop", this->mashhopEdit->value()); @@ -468,6 +488,7 @@ query.bindValue(":grain", this->grainEdit->value()); query.bindValue(":water", record); query.bindValue(":yeast", this->yeastEdit->currentText()); + query.bindValue(":height", this->heightEdit->value()); query.exec(); if (query.lastError().isValid()) { qDebug() << "Setup Save error:" << query.lastError(); diff -r e97f9e87d94b -r 8133cdb19aa1 src/Setup.h --- a/src/Setup.h Tue Jul 05 14:31:39 2022 +0200 +++ b/src/Setup.h Thu Jul 07 08:51:57 2022 +0200 @@ -56,6 +56,7 @@ QLabel *cryohopLabel; QLabel *grainLabel; QLabel *brixLabel; + QLabel *heightLabel; QLabel *titleLabel; QLabel *colorLabel; QLabel *ibuLabel; @@ -73,6 +74,7 @@ QComboBox *waterEdit; QDoubleSpinBox *grainEdit; QDoubleSpinBox *brixEdit; + QSpinBox *heightEdit; QComboBox *colorEdit; QComboBox *ibuEdit; QComboBox *yeastEdit; diff -r e97f9e87d94b -r 8133cdb19aa1 src/Utils.cpp --- a/src/Utils.cpp Tue Jul 05 14:31:39 2022 +0200 +++ b/src/Utils.cpp Thu Jul 07 08:51:57 2022 +0200 @@ -360,129 +360,163 @@ return round((og * 1000 - fg * 1000) * factor * 100) / 100; } + +double Utils::brewery_hPa() +{ + return Seapressure * exp( - MolMassAir * Gravacc * my_height / (Gasconst * (20 + Kelvin))); +} + + +double Utils::boilPoint() +{ + double P2 = brewery_hPa(); + + return (1 / (1/(100 + Kelvin) - Gasconst * log(P2 / Seapressure) / EoVwater)) - Kelvin; +} + + /* + * Formula is from the 'Mash Made Easy' spreadsheet. + * https://mashmadeeasy.yolasite.com/ + * https://www.homebrewtalk.com/threads/a-rather-simplified-whirlpool-hop-ibu-computation-method.701093/ + */ +double Utils::IBU_reduction(double Tc) +{ + return (2.39 * pow(10, 11) * pow(2.71828182846, - (9773 / (Tc + Kelvin))) ) * 1/1.009231744; +} + + double Utils::boilIBU(int Form, double SG, double Volume, double Amount, double Time, double Alpha, int Method) { + double ibu = 0.0, sgfactor, boilfactor; + + double alpha = Alpha / 100.0; + double mass = Amount * 1000.0; + if (Method == 0) { // Tinseth -*/ /* http://realbeer.com/hops/research.html */ -/* AddedAlphaAcids = (alpha * mass * 1000) / liters; - Bigness_factor = 1.65 * pow(0.000125, gravity - 1); - BoilTime_factor = ((1 - exp(-0.04 * time)) / 4.15); - utiisation = Bigness_factor * BoilTime_factor; - ibu = round((utiisation * AddedAlphaAcids * fmoment * pfactor + whirlibus) * 100) / 100; + /* http://realbeer.com/hops/research.html */ + double AddedAlphaAcids = (alpha * mass * 1000) / Volume; + double Bigness_factor = 1.65 * pow(0.000125, SG - 1); + double BoilTime_factor = ((1 - exp(-0.04 * Time)) / 4.15); + ibu = Bigness_factor * BoilTime_factor * AddedAlphaAcids; } if (Method == 2) { // Daniels if (Form == 2) // Leaf - boilfactor = -(0.0041 * time * time) + (0.6162 * time) + 1.5779; + boilfactor = -(0.0041 * Time * Time) + (0.6162 * Time) + 1.5779; else - boilfactor = -(0.0051 * time * time) + (0.7835 * time) + 1.9348; - if (gravity < 1050) + boilfactor = -(0.0051 * Time * Time) + (0.7835 * Time) + 1.9348; + if (SG < 1.050) sgfactor = 0; else - sgfactor = (gravity - 1050) / 200; - ibu = round((fmoment * ((mass * (alpha * 100) * boilfactor * 0.1) / (liters * (1 + sgfactor))) + whirlibus) * 100) / 100; + sgfactor = ((SG * 1000) - 1050) / 200; + ibu = (mass * (alpha * 100) * boilfactor * 0.1) / (Volume * (1 + sgfactor)); } if (Method == 1) { // Rager - boilfactor = fmoment * 18.11 + 13.86 * tanh((time * 31.32) / 18.27); - if (gravity < 1050) + boilfactor = 18.11 + 13.86 * tanh((Time * 31.32) / 18.27); + if (SG < 1.050) sgfactor = 0; else - sgfactor = (gravity - 1050) / 200; - ibu = round(((mass * (alpha * 100) * boilfactor * 0.1) / (liters * (1 + sgfactor)) + whirlibus) * 100) / 100; + sgfactor = ((SG * 1000) - 1050) / 200; + ibu = (mass * (alpha * 100) * boilfactor * 0.1) / (Volume * (1 + sgfactor)); } + + //qDebug() << "boilIBU" << Form << SG << Volume << Amount << Time << Alpha << Method << "IBU:" << ibu; + return ibu; } -*/ + double Utils::toIBU(int Use, int Form, double SG, double Volume, double Amount, double Boiltime, double Alpha, int Method, double Whirlpool9, double Whirlpool7, double Whirlpool6, double Fulltime) { - double fmoment = 1.0, pfactor = 1.0, ibu = 0, boilfactor; - double sgfactor, AddedAlphaAcids, Bigness_factor, BoilTime_factor, utiisation; - - double gravity = SG; - double liters = Volume; + double ibu = 0.0, whirlibus = 0.0; double alpha = Alpha / 100.0; double mass = Amount * 1000.0; - double time = Boiltime; - - if ((Use == HOP_USEAT_AROMA) || (Use == HOP_USEAT_WHIRLPOOL) || (Use == HOP_USEAT_DRY_HOP) || (Use == HOP_USEAT_BOTTLING)) { - fmoment = 0.0; - } else if (Use == HOP_USEAT_MASH) { - fmoment += my_factor_mashhop / 100.0; // Brouwhulp - time = Fulltime; // Take the full boiltime - } else if (Use == HOP_USEAT_FWH) { - fmoment += my_factor_fwh / 100.0; // Brouwhulp, Louis, Ozzie - time = Fulltime; - } - - if (Form == HOP_FORMS_PELLET) { - pfactor += my_factor_pellet / 100.0; - } else if (Form == HOP_FORMS_PLUG) { - pfactor += my_factor_plug / 100.0; - } else if (Form == HOP_FORMS_LEAF_WET) { - pfactor += my_factor_wethop / 100.0; // From https://github.com/chrisgilmerproj/brewday/blob/master/brew/constants.py - } else if (Form == HOP_FORMS_CRYO) { - pfactor += my_factor_cryohop / 100.0; - } else if (Form == HOP_FORMS_EXTRACT) { - // Nothing for now. - } // Ideas from Zymurgy March-April 2018. These are not exact formulas! - double whirlibus = 0.0; if (Use == HOP_USEAT_AROMA) { if (Whirlpool9) { // Flameout hops are 2 minutes in this range. - whirlibus += (alpha * mass * 20) / liters * (2.0 / 50.0); + whirlibus += (alpha * mass * 20) / Volume * (2.0 / 50.0); } if (Whirlpool7) { // Flameout hops are 4 minutes in this range. - whirlibus += (alpha * mass * 6) / liters * (4.0 / 50.0); + whirlibus += (alpha * mass * 6) / Volume * (4.0 / 50.0); } + // Experiment. +// double wibu = boilIBU(Form, SG, Volume, Amount, 6, Alpha, Method); // IBU's for 6 minutes full +// double fibu = wibu * 0.067 * IBU_reduction(94); // Add timed segments +// fibu += wibu * 0.1 * IBU_reduction(84); +// fibu += wibu * 0.167 * IBU_reduction(74); +// fibu += wibu * 0.250 * IBU_reduction(64); +// fibu += wibu * 0.417 * IBU_reduction(54); + //qDebug() << " 94" << wibu * 0.067 * IBU_reduction(94); + //qDebug() << " 84" << wibu * 0.1 * IBU_reduction(84); + //qDebug() << " 74" << wibu * 0.167 * IBU_reduction(74); + //qDebug() << " 64" << wibu * 0.250 * IBU_reduction(64); + //qDebug() << " 54" << wibu * 0.417 * IBU_reduction(54); +// qDebug() << "flamout" << wibu << fibu; } if (Use == HOP_USEAT_WHIRLPOOL) { // Flameout or any whirlpool if (Whirlpool9) { // 20 mg/l/50 min - whirlibus += (alpha * mass * 20) / liters * (Whirlpool9 / 50.0); + whirlibus += (alpha * mass * 20) / Volume * (Whirlpool9 / 50.0); //qDebug() << "Whirlpool9" << alpha * mass * 20 << " liter:" << liters << " time:" << Whirlpool9 << " ibu" << (alpha * mass * 20) / liters * (Whirlpool9 / 50.0); } if (Whirlpool7) { // 6 mg/l/50 min - whirlibus += (alpha * mass * 6) / liters * (Whirlpool7 / 50.0); + whirlibus += (alpha * mass * 6) / Volume * (Whirlpool7 / 50.0); //qDebug() << "Whirlpool7" << alpha * mass * 6 << " liter:" << liters << " time:" << Whirlpool7 << " ibu" << (alpha * mass * 6) / liters * (Whirlpool7 / 50.0); } if (Whirlpool6) { // 2 mg/l/50 min - whirlibus += (alpha * mass * 2) / liters * (Whirlpool6 / 50.0); + whirlibus += (alpha * mass * 2) / Volume * (Whirlpool6 / 50.0); } +// double wibu = boilIBU(Form, SG, Volume, Amount, Boiltime, Alpha, Method); +// qDebug() << "whirpool" << wibu << wibu * IBU_reduction(74); } - if (Method == 0) { // Tinseth - /* http://realbeer.com/hops/research.html */ - AddedAlphaAcids = (alpha * mass * 1000) / liters; - Bigness_factor = 1.65 * pow(0.000125, gravity - 1); - BoilTime_factor = ((1 - exp(-0.04 * time)) / 4.15); - utiisation = Bigness_factor * BoilTime_factor; - ibu = round((utiisation * AddedAlphaAcids * fmoment * pfactor + whirlibus) * 100) / 100; - } - if (Method == 2) { // Daniels - if (Form == 2) // Leaf - boilfactor = -(0.0041 * time * time) + (0.6162 * time) + 1.5779; - else - boilfactor = -(0.0051 * time * time) + (0.7835 * time) + 1.9348; - if (gravity < 1050) - sgfactor = 0; - else - sgfactor = (gravity - 1050) / 200; - ibu = round((fmoment * ((mass * (alpha * 100) * boilfactor * 0.1) / (liters * (1 + sgfactor))) + whirlibus) * 100) / 100; - } - if (Method == 1) { // Rager - boilfactor = fmoment * 18.11 + 13.86 * tanh((time * 31.32) / 18.27); - if (gravity < 1050) - sgfactor = 0; - else - sgfactor = (gravity - 1050) / 200; - ibu = round(((mass * (alpha * 100) * boilfactor * 0.1) / (liters * (1 + sgfactor)) + whirlibus) * 100) / 100; + /* + * IBU's from hops during Mash, FWH and boil. + */ + if ((Use == HOP_USEAT_MASH) || (Use == HOP_USEAT_FWH) || (Use == HOP_USEAT_BOIL)) { + ibu = boilIBU(Form, SG, Volume, Amount, Fulltime, Alpha, Method); + /* + * Corrections for Mash and FWH + */ + if (Use == HOP_USEAT_MASH) { + ibu *= (1 + my_factor_mashhop / 100.0); + } + if (Use == HOP_USEAT_FWH) { + ibu *= (1 + my_factor_fwh / 100.0); + } + + /* + * Correction for hop forms + */ + if (Form == HOP_FORMS_PELLET) { + ibu *= (1 + my_factor_pellet / 100.0); + } else if (Form == HOP_FORMS_PLUG) { + ibu *= (1 + my_factor_plug / 100.0); + } else if (Form == HOP_FORMS_LEAF_WET) { + ibu *= (1 + my_factor_wethop / 100.0); // From https://github.com/chrisgilmerproj/brewday/blob/master/brew/constants.py + } else if (Form == HOP_FORMS_CRYO) { + ibu *= (1 + my_factor_cryohop / 100.0); + } else if (Form == HOP_FORMS_EXTRACT) { + // Nothing for now. + } +// ibu *= IBU_reduction(boilPoint()); +// qDebug() << "ibu" << ibu << IBU_reduction(boilPoint()); + +// } else if (Use == HOP_USEAT_AROMA) { + /* + * At flameout. The cooling method is important. + * Emersion chiller, Counterflow chiller, Au bain marie or natural. + * Assume the hop is removed for all methods except Emersion chilling. + */ + } else { +// qDebug() << "whirlibus" << whirlibus << Use; } - return ibu; + + return round((ibu + whirlibus) * 100.0) / 100.0; } diff -r e97f9e87d94b -r 8133cdb19aa1 src/Utils.h --- a/src/Utils.h Tue Jul 05 14:31:39 2022 +0200 +++ b/src/Utils.h Thu Jul 07 08:51:57 2022 +0200 @@ -32,8 +32,60 @@ double kw_to_ebc(int colormethod, double c); double kw_to_newebc(int colormethod, double c); double abvol(double og, double fg); + + /** + * @brief Calculate standar air pressure at the brewery. + * Assume 20°C and use the global setup height. + * @return Pressure in hPa. + */ + double brewery_hPa(); + + /** + * @brief Return boil temperature in °C at the brewery height. + * @return Temperature in °C + */ + double boilPoint(); + + /** + * @brief Calculate IBU reduction at given temperature. + * @param Tc temperature in °C. + * @return The reduction factor. + */ + double IBU_reduction(double Tc); + + /** + * @brief Calculate IBU's of a hop at 100°C. + * @param Use HOP_USEAT_MASH HOP_USEAT_FWH HOP_USEAT_BOIL, all others are ignored. + * @param Form HOP_FORMS_PELLET HOP_FORMS_PLUG HOP_FORMS_LEAF HOP_FORMS_LEAF_WET HOP_FORMS_CRYO HOP_FORMS_EXTRACT + * @param SG the density + * @param Volume in liters + * @param Amount in kilograms + * @param Time in minutes + * @param Alpha in procent + * @param Method, 0 = Tinseth, 1 = Rager, 2 = Daniels + * @return The calculated IBU's + */ + double boilIBU(int Form, double SG, double Volume, double Amount, double Time, double Alpha, int Method); + + /** + * @brief Calculate IBU's of a hop during the whole production process. + * @param Use HOP_USEAT_MASH HOP_USEAT_FWH HOP_USEAT_BOIL HOP_USEAT_AROMA HOP_USEAT_WHIRLPOOL HOP_USEAT_DRY_HOP HOP_USEAT_BOTTLING + * @param Form HOP_FORMS_PELLET HOP_FORMS_PLUG HOP_FORMS_LEAF HOP_FORMS_LEAF_WET HOP_FORMS_CRYO HOP_FORMS_EXTRACT + * @param SG the density + * @param Volume in liters + * @param Amount in kilograms + * @param Boiltime in minutes + * @param Alpha in procent + * @param Method, 0 = Tinseth, 1 = Rager, 2 = Daniels + * @param Whirlpool9 time in whirlpool above 80°C or zero. + * @param Whirlpool7 time in whirlpool between 72°C and 77°C. + * @param Whirlpool6 time in whirlpool between 60°C amd 66°C. + * @param Fulltime, full boiltime, even for aroma hops. + * @return The calculated IBU's + */ double toIBU(int Use, int Form, double SG, double Volume, double Amount, double Boiltime, double Alpha, int Method, double Whirlpool9, double Whirlpool7, double Whirlpool6, double Fulltime); + double hopFlavourContribution(double bt, double vol, int use, double amount); double hopAromaContribution(double bt, double vol, int use, double amount); QString hours_to_string(int hours); diff -r e97f9e87d94b -r 8133cdb19aa1 src/global.cpp --- a/src/global.cpp Tue Jul 05 14:31:39 2022 +0200 +++ b/src/global.cpp Thu Jul 07 08:51:57 2022 +0200 @@ -19,6 +19,7 @@ double my_grain_absorbtion = 1.01; int my_default_water = -1; QString my_yeastlab = ""; +int my_height = 0; Recipe *recipe; Product *product; diff -r e97f9e87d94b -r 8133cdb19aa1 src/global.h --- a/src/global.h Tue Jul 05 14:31:39 2022 +0200 +++ b/src/global.h Thu Jul 07 08:51:57 2022 +0200 @@ -35,6 +35,13 @@ #define equip_tun_specific_heat 0.110 #define MaltVolume 0.87 // l/kg 0.688 volgens internetbronnen, gemeten 0.874 l/kg, na enige tijd maischen 0,715 l/kg (A3 Otten). +#define Seapressure 1013.25 // Air pressure at sealevel in hPa +#define MolMassAir 0.0289644 // Air molair mass +#define Gravacc 9.80665 // Gravitational acceleration in m/s2 +#define Gasconst 8.3144621 // Gas constant J K-1 mol-1 +#define Kelvin 273.15 // Kelvin to Celsius +#define EoVwater 40660 // Enthalpy of Vaporization (ΔH) for water + extern QWebSocket *webSocket; struct Acid @@ -667,6 +674,8 @@ extern double my_grain_absorbtion; extern int my_default_water; extern QString my_yeastlab; +extern int my_height; + enum ProdStages { PROD_STAGE_PLAN,