src/EditProductTab8.cpp

Mon, 18 Jul 2022 17:04:02 +0200

author
Michiel Broek <mbroek@mbse.eu>
date
Mon, 18 Jul 2022 17:04:02 +0200
changeset 359
dfbb012c631c
parent 350
37b3c690b02c
child 361
ec8de79f6ff6
permissions
-rw-r--r--

Redesign of the water tabs in product and recipe editors. Prepare for sparge water salt additions. Acid additions are now automatic or manual for mash and sparge. Fixed error in acid calculation strength. Fixed phophoric SG value.

/**
 * EditProduct.cpp is part of bmsapp.
 *
 * tab 8, water.
 *
 * bmsapp is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * bmsapp is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */


void EditProduct::refreshWaters()
{

    // calc_acid
    ui->mw_phEdit->setValue(product->mash_ph);
    // mash_name
    //ui->mw_acidpercEdit->setValue(product->

}


/*
 * Z alkalinity is the amount of acid (in mEq/l) needed to bring water to the target pH (Z pH)
 */
double EditProduct::ZAlkalinity(double pHZ)
{
    double C43 = Utils::Charge(4.3);
    double Cw = Utils::Charge(product->wg_ph);
    double Cz = Utils::Charge(pHZ);
    double DeltaCNaught = -C43 + Cw;
    double CT = product->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 EditProduct::ZRA(double pHZ)
{
    double Calc = product->wg_calcium / (MMCa / 2);
    double Magn = product->wg_magnesium / (MMMg / 2);
    double Z = ZAlkalinity(pHZ);
    return Z - (Calc / 3.5 + Magn / 7);
}


double EditProduct::BufferCapacity(Fermentables F)
{
    double C1 = 0;

    if ((F.di_ph != 5.7) && ((F.acid_to_ph_57 < - 0.1) || (F.acid_to_ph_57 > 0.1))) {
	C1 = F.acid_to_ph_57 / (F.di_ph - 5.7);
    } else {
	/*
	 * If the acid_to_ph_5.7 is unknown from the maltster, guess the required acid.
	 */
	switch (F.graintype) {
	   case FERMENTABLE_GRAINTYPE_BASE:
	   case FERMENTABLE_GRAINTYPE_KILNED:
	   case FERMENTABLE_GRAINTYPE_SPECIAL:		C1 = 0.014 * F.color - 34.192;
							break;
	   case FERMENTABLE_GRAINTYPE_CRYSTAL:		C1 = -0.0597 * F.color - 32.457;
							break;
	   case FERMENTABLE_GRAINTYPE_ROAST:		C1 = 0.0107 * F.color - 54.768;
							break;
	   case FERMENTABLE_GRAINTYPE_SOUR_MALT:	C1 = -149;
							break;
	}
    }
    return C1;
}


double EditProduct::AcidRequired(double ZpH, Fermentables F)
{
    double C1 = BufferCapacity(F);
    double x = F.di_ph;
    return C1 * (ZpH - x);
}


double EditProduct::ProtonDeficit(double pHZ)
{
    double C1, x;
    int i, error_count = 0;
    double Result = ZRA(pHZ) * product->wg_amount;
    Fermentables F;

    /*
     * proton deficit for the grist
     */
    if (product->fermentables.size()) {
	for (i = 0; i < product->fermentables.size(); i++) {
	    F = product->fermentables.at(i);
	    if (F.added == FERMENTABLE_ADDED_MASH && F.graintype != FERMENTABLE_GRAINTYPE_NO_MALT) {
		x = AcidRequired(pHZ, F) * F.amount;
		Result += x;
	    }
	}
    } else {
	error_count++;
	if (error_count < 5)
	   qDebug() << "  ProtonDeficit" <<  pHZ << "invalid grist, return" << Result;
    }
    return Result;
}


double EditProduct::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 EditProduct::calcWater()
{
    double liters = 0;
    double calcium = 0;
    double magnesium = 0;
    double sodium = 0;
    double total_alkalinity = 0;
    double bicarbonate = 0;
    double chloride = 0;
    double sulfate = 0;
    double ph = 0;
    double TpH = 0;
    double frac, RA;
    double protonDeficit = 0;
    double Acid = 0, Acidmg = 0;
    int AT;

    qDebug() << "calcWater()";

    ui->w1_hardnessEdit->setValue(Utils::Hardness(product->w1_calcium, product->w1_magnesium));
    ui->w1_raEdit->setValue(Utils::ResidualAlkalinity(product->w1_total_alkalinity, product->w1_calcium, product->w1_magnesium));

    /*
     * If there is a dilute water source, mix the waters.
     */
    if (product->w2_name != "") {
	liters = product->w1_amount + product->w2_amount;
	calcium = Utils::mix(product->w1_amount, product->w2_amount, product->w1_calcium, product->w2_calcium);
	magnesium = Utils::mix(product->w1_amount, product->w2_amount, product->w1_magnesium, product->w2_magnesium);
	sodium = Utils::mix(product->w1_amount, product->w2_amount, product->w1_sodium, product->w2_sodium);
	chloride = Utils::mix(product->w1_amount, product->w2_amount, product->w1_chloride, product->w2_chloride);
	sulfate = Utils::mix(product->w1_amount, product->w2_amount, product->w1_sulfate, product->w2_sulfate);
	total_alkalinity = Utils::mix(product->w1_amount, product->w2_amount, product->w1_total_alkalinity, product->w2_total_alkalinity);
	ph = -log10(((pow(10, -product->w1_ph) * product->w1_amount) + (pow(10, -product->w2_ph) * product->w2_amount)) / liters);
	ui->w2_hardnessEdit->setValue(Utils::Hardness(product->w2_calcium, product->w2_magnesium));
    	ui->w2_raEdit->setValue(Utils::ResidualAlkalinity(product->w2_total_alkalinity, product->w2_calcium, product->w2_magnesium));
    } else {
	liters = product->w1_amount;
	calcium = product->w1_calcium;
	magnesium = product->w1_magnesium;
	sodium = product->w1_sodium;
	chloride = product->w1_chloride;
	sulfate = product->w1_sulfate;
	total_alkalinity = product->w1_total_alkalinity;
	ph = product->w1_ph;
    }

    product->wg_amount = liters;
    product->wg_calcium = round(calcium * 10.0) / 10.0;
    product->wg_magnesium = round(magnesium * 10.0) / 10.0;
    product->wg_sodium = round(sodium * 10.0) / 10.0;
    product->wg_chloride = round(chloride * 10.0) / 10.0;
    product->wg_sulfate = round(sulfate * 10.0) / 10.0;
    product->wg_total_alkalinity = round(total_alkalinity * 10.0) / 10.0;
    product->wg_ph = ph;

    bicarbonate = Utils::Bicarbonate(total_alkalinity, ph);
    ui->wg_volEdit->setValue(liters);
    ui->wg_caEdit->setValue(calcium);
    ui->wg_mgEdit->setValue(magnesium);
    ui->wg_hco3Edit->setValue(bicarbonate);
    ui->wg_caco3Edit->setValue(total_alkalinity);
    ui->wg_naEdit->setValue(sodium);
    ui->wg_clEdit->setValue(chloride);
    ui->wg_so4Edit->setValue(sulfate);
    ui->wg_phEdit->setValue(ph);
    ui->wg_hardnessEdit->setValue(Utils::Hardness(calcium, magnesium));
    ui->wg_raEdit->setValue(Utils::ResidualAlkalinity(total_alkalinity, calcium, magnesium));

    /* Save mixed water ions for later */
    double wg_calcium = calcium;
    double wg_sodium = sodium;
    double wg_total_alkalinity = total_alkalinity;
    double wg_chloride = chloride;
    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;
    }

    const QSignalBlocker blocker1(ui->mw_acidPick);
    const QSignalBlocker blocker2(ui->mw_acidpercEdit);
    const QSignalBlocker blocker3(ui->mw_acidvolEdit);
    const QSignalBlocker blocker4(ui->wb_phEdit);
    const QSignalBlocker blocker5(ui->mw_phEdit);

    if (product->wa_acid_name < 0 || product->wa_acid_name >= my_acids.size()) {
	product->wa_acid_name = 0;
	product->wa_acid_perc = my_acids.at(0).AcidPrc;
	ui->mw_acidPick->setCurrentIndex(0);
	ui->mw_acidpercEdit->setValue(my_acids.at(0).AcidPrc);
    }
    AT = product->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 (product->calc_acid) {
	/*
	 * Auto calculate the needed acid.
	 */
	TpH = product->mash_ph;
	protonDeficit = ProtonDeficit(TpH);
	qDebug() << "  calc_acid tgt:" << TpH << "protonDeficit:" << protonDeficit;
	if (protonDeficit > 0) {
	    frac = Utils::CalcFrac(TpH, my_acids[AT].pK1, my_acids[AT].pK2, my_acids[AT].pK3);
	    Acid = protonDeficit / frac;
	    Acid *= my_acids[AT].MolWt;	// mg.
	    Acidmg = Acid;
	    Acid = Acid / my_acids[AT].AcidSG;
	    Acid = round((Acid / (product->wa_acid_perc / 100.0)) * 100.0) / 100.0;
	    qDebug() << "  Mash auto Acid final old ml:" << Acid;
	    Acid = Acidmg;

	    double RealSG = round(((my_acids[AT].AcidSG - 1000) * (product->wa_acid_perc / 100)) + 1000);
	    Acid /= RealSG;
	    Acid /= my_acids[AT].AcidPrc / 100;
	    Acid = round(Acid * 100.0) / 100.0;
	    qDebug() << "  Mash auto Acid final ml:" << Acid;

	    QString w = my_acids[AT].name_en + ' ' + my_acids[AT].name_nl;
	    brewing_salt_sub(w, Acid, MISC_USES_MASH);
	    ui->mw_acidvolEdit->setValue(Acid);

	    bicarbonate = bicarbonate - protonDeficit * frac / liters;
	    total_alkalinity = bicarbonate * 50 / 61;
	}
	ph = TpH;
	ui->wb_phEdit->setValue(ph);
	product->mash_ph = ph;
    } else { // Manual
	/*
	 * Manual adjust acid, calculate resulting pH.
	 */
	double pHa = ph;	// Mixed water pH.
	double RealSG = round(((my_acids[AT].AcidSG - 1000) * (product->wa_acid_perc / 100)) + 1000);
	// Then calculate the new pH with added acids and malts
	qDebug() << "  Mash pH:" << pHa;
	Acid = RealSG;
	Acid *= ui->mw_acidvolEdit->value();
	Acid *= my_acids[AT].AcidPrc / 100;
	Acid /= my_acids[AT].MolWt;	// mg;
	Acidmg = Acid;

	//find the pH where the protondeficit = protondeficit by the acid
	frac = Utils::CalcFrac(pHa, my_acids[AT].pK1, my_acids[AT].pK2, my_acids[AT].pK3);
	protonDeficit = Acid * frac;
	qDebug() << "  Acid:" << Acid << "protonDeficit:" << protonDeficit << "frac:" << frac << "pH:" << pHa;

	double deltapH = 0.001;
   	double deltapd = 0.1;
   	double pd = round(ProtonDeficit(pHa) * 1000000.0) / 1000000.0;
	int n = 0;
	while (((pd < (protonDeficit - deltapd)) || (pd > (protonDeficit + deltapd))) && (n < 4000)) {
	    n++;
	    if (pd < (protonDeficit - deltapd))
		pHa -= deltapH;
	    else if (pd > (protonDeficit + deltapd))
		pHa += deltapH;
	    frac = Utils::CalcFrac(pHa, my_acids[AT].pK1, my_acids[AT].pK2, my_acids[AT].pK3);
	    protonDeficit = Acid * frac;
	    pd = ProtonDeficit(pHa);
	}
	qDebug() << "  n:" << n << "pd:" << pd << "protonDeficit:" << protonDeficit << "frac:" << frac << "pHa:" << pHa;

	bicarbonate = wg_bicarbonate - protonDeficit * frac / liters;
	total_alkalinity = bicarbonate * 50 / 61;
	ph = pHa;
	ui->wb_phEdit->setValue(ph);
	ui->mw_phEdit->setValue(ph);
        product->mash_ph = ph;
    }

    if ((AT == 3) && (liters > 0)) {        // Sulfuctic / Zwavelzuur
	RA = ui->bs_caso4Edit->value() * MMSO4 / MMCaSO4 + ui->bs_mgso4Edit->value() * MMSO4 / MMMgSO4 + Acidmg / 1000 * MMSO4 / (MMSO4 + 2);
	RA = 1000 * RA / liters;
	sulfate = wg_sulfate + RA;      // Not add to sulfate??
    } else if ((AT == 1) && (liters > 0)) { // Hydrochloric, Zoutzuur
	RA = ui->bs_cacl2Edit->value() * MMCl / MMCaCl2 + ui->bs_naclEdit->value() * MMCl / MMNaCl + Acidmg / 1000 * MMCl / (MMCl + 1);
	RA = 1000 * RA / liters;
	chloride = wg_chloride + RA;
    }

    double BUGU = GetBUGU();
    ui->buguEdit->setValue(BUGU);
    if (BUGU < 0.32)
	ui->buguResult->setText(tr("Very malty and sweet"));
    else if (BUGU < 0.43)
	ui->buguResult->setText(tr("Malty, sweet"));
    else if (BUGU < 0.52)
	ui->buguResult->setText(tr("Balanced"));
    else if (BUGU < 0.63)
	ui->buguResult->setText(tr("Hoppy, bitter"));
    else
	ui->buguResult->setText(tr("Very hoppy, very bitter"));

    double OptSO4Clratio = GetOptSO4Clratio();
    ui->so4clEdit->setValue(OptSO4Clratio);
    ui->cur_so4clResult->setRange(0.7 * OptSO4Clratio, 1.3 * OptSO4Clratio);
    if (OptSO4Clratio < 0.4)
	ui->so4clResult->setText(tr("Too malty"));
    else if (OptSO4Clratio < 0.6)
	ui->so4clResult->setText(tr("Very malty"));
    else if (OptSO4Clratio < 0.8)
	ui->so4clResult->setText(tr("Malty"));
    else if (OptSO4Clratio < 1.5)
	ui->so4clResult->setText(tr("Balanced"));
    else if (OptSO4Clratio < 2.0)
	ui->so4clResult->setText(tr("Little bitter"));
    else if (OptSO4Clratio < 4.0)
	ui->so4clResult->setText(tr("Bitter"));
    else if (OptSO4Clratio < 9.0)
	ui->so4clResult->setText(tr("Very bitter"));
    else
	ui->so4clResult->setText(tr("Too bitter"));
    if (chloride > 0)
	RA = sulfate / chloride;
    else
	RA = 10;
    ui->cur_so4clEdit->setValue(RA);
    ui->cur_so4clResult->setValue(RA);

    product->wb_calcium = calcium;
    product->wb_magnesium = magnesium;
    product->wb_total_alkalinity = total_alkalinity;
    product->wb_sodium = sodium;
    product->wb_chloride = chloride;
    product->wb_sulfate = sulfate;
    product->wb_ph = 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_hardnessEdit->setValue(Utils::Hardness(calcium, magnesium));
    ui->wb_raEdit->setValue(Utils::ResidualAlkalinity(total_alkalinity, calcium, magnesium));

    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();
}


/*
 * Based on the work of ajDeLange.
 */
void EditProduct::calcSparge()
{
    double TargetpH = product->sparge_ph;
    double Source_pH = product->w1_ph;
    double Source_alkalinity = product->w1_total_alkalinity;

    qDebug() << "calcSparge()";

    const QSignalBlocker blocker2(ui->w1_spButton);
    const QSignalBlocker blocker3(ui->w2_spButton);
    const QSignalBlocker blocker4(ui->wg_spButton);

    // Select watersource or fallback to the first source.
    if (product->sparge_source == 1) { // Source 2
	if (product->w2_ph > 0.0 && product->w2_amount > 0) {
	    Source_pH = product->w2_ph;
	    Source_alkalinity = product->w2_total_alkalinity;
	    ui->sp_caEdit->setValue(product->w2_calcium);
            ui->sp_mgEdit->setValue(product->w2_magnesium);
            ui->sp_hco3Edit->setValue(Utils::Bicarbonate(product->w2_total_alkalinity, product->w2_ph));
            ui->sp_caco3Edit->setValue(product->w2_total_alkalinity);
            ui->sp_naEdit->setValue(product->w2_sodium);
            ui->sp_clEdit->setValue(product->w2_chloride);
            ui->sp_so4Edit->setValue(product->w2_sulfate);
            ui->sp_phShow->setValue(product->w2_ph);
            ui->sp_hardnessEdit->setValue(Utils::Hardness(product->w2_calcium, product->w2_magnesium));
            ui->sp_raEdit->setValue(Utils::ResidualAlkalinity(product->w2_total_alkalinity, product->w2_calcium, product->w2_magnesium));
	    ui->w2_spButton->setChecked(true);
	} else {
	    product->sparge_source = 0; // Fallback to source 1
	    ui->w1_spButton->setChecked(true);
	}
    } else if (product->sparge_source == 2) { // Mixed
	if (product->w2_ph > 0.0 && product->w2_amount > 0) {
	    Source_pH = product->wg_ph;
	    Source_alkalinity = product->wg_total_alkalinity;
	    ui->sp_caEdit->setValue(product->wg_calcium);
	    ui->sp_mgEdit->setValue(product->wg_magnesium);
	    ui->sp_hco3Edit->setValue(Utils::Bicarbonate(product->wg_total_alkalinity, product->wg_ph));
	    ui->sp_caco3Edit->setValue(product->wg_total_alkalinity);
	    ui->sp_naEdit->setValue(product->wg_sodium);
	    ui->sp_clEdit->setValue(product->wg_chloride);
	    ui->sp_so4Edit->setValue(product->wg_sulfate);
	    ui->sp_phShow->setValue(product->wg_ph);
	    ui->sp_hardnessEdit->setValue(Utils::Hardness(product->wg_calcium, product->wg_magnesium));
	    ui->sp_raEdit->setValue(Utils::ResidualAlkalinity(product->wg_total_alkalinity, product->wg_calcium, product->wg_magnesium));
	    ui->wg_spButton->setChecked(true);
	} else {
	    product->sparge_source = 0; // Fallback to source 1
	    ui->w1_spButton->setChecked(true);
	}
    }
    if (product->sparge_source == 0) {
	ui->sp_caEdit->setValue(product->w1_calcium);
	ui->sp_mgEdit->setValue(product->w1_magnesium);
	ui->sp_hco3Edit->setValue(Utils::Bicarbonate(product->w1_total_alkalinity, product->w1_ph));
	ui->sp_caco3Edit->setValue(product->w1_total_alkalinity);
	ui->sp_naEdit->setValue(product->w1_sodium);
	ui->sp_clEdit->setValue(product->w1_chloride);
	ui->sp_so4Edit->setValue(product->w1_sulfate);
	ui->sp_phShow->setValue(product->w1_ph);
	ui->sp_hardnessEdit->setValue(Utils::Hardness(product->w1_calcium, product->w1_magnesium));
	ui->sp_raEdit->setValue(Utils::ResidualAlkalinity(product->w1_total_alkalinity, product->w1_calcium, product->w1_magnesium));
	ui->w1_spButton->setChecked(true);
    }
    // The spargewater is set.

    int AT = product->sparge_acid_type;
    if (AT < 0 || AT >= my_acids.size()) {
        AT = 0;
        product->sparge_acid_type = 0;
        ui->sp_acidtypeEdit->setCurrentIndex(0);
        product->sparge_acid_perc = my_acids[0].AcidPrc;
        ui->sp_acidpercEdit->setValue(product->sparge_acid_perc);
    }

    /*
     * Auto calculate the required acid
     */
    if (product->calc_acid) {
    	// Step 1: Compute the mole fractions of carbonic (f1o), bicarbonate (f2o) and carbonate(f3o) at the water pH
    	double r1 = pow(10, Source_pH - 6.35);
    	double r2 = pow(10, Source_pH - 10.33);
    	double d = 1 + r1 + r1 * r2;
    	double f1 = 1 / d;
    	double f3 = r1 * r2 / d;

    	// Step 2. Compute the mole fractions at pH = 4.3 (the pH which defines alkalinity)
    	double r143 = pow(10, 4.3 - 6.35);
    	double r243 = pow(10, 4.3 - 10.33);
    	double d43 = 1 + r143 + r143 * r243;
    	double f143 = 1 / d43;
    	double f343 = r143 * r243 / d43;

    	// Step 4. Solve
    	double Ct = Source_alkalinity / 50 / ((f143 - f1) + (f3 - f343));

    	// Step 5. Compute mole fractions at desired pH
    	double r1g = pow(10, TargetpH - 6.35);
    	double r2g = pow(10, TargetpH - 10.33);
    	double dg = 1 + r1g + r1g * r2g;
    	double f1g = 1 / dg;
    	double f3g = r1g * r2g / dg;

    	// Step 6. Use these to compute the milliequivalents acid required per liter (mEq/L)
    	double Acid = Ct * ((f1g - f1) + (f3 - f3g)) + pow(10, -TargetpH) - pow(10, -Source_pH);  //mEq/l
    	Acid += 0.01;   // Add acid that would be required for distilled water.

    	// Step 7. There is no step 7.

    	// Step 8. Get the acid data.
    	double fract = Utils::CalcFrac(TargetpH, my_acids[AT].pK1, my_acids[AT].pK2, my_acids[AT].pK3);

    	// Step 9. Now divide the mEq required by the "fraction". This is the required number of moles of acid.
    	Acid /= fract;

    	// Step 10. Multiply by molecular weight of the acid
    	Acid *= my_acids[AT].MolWt; //mg

    	// Step 11. Divide by Specific Gravity and Percentage to get the final ml.
    	double RealSG = round(((my_acids[AT].AcidSG - 1000) * (product->sparge_acid_perc / 100)) + 1000);
    	Acid = Acid / RealSG;		//ml
    	Acid *= product->sparge_volume;	//ml acid total at 100%
    	Acid /= my_acids[AT].AcidPrc / 100;	//ml acid at supplied strength
    	Acid = round(Acid * 100.0) / 100.0;
    	product->sparge_acid_amount = Acid / 1000;
    	QString w = my_acids[AT].name_en + ' ' + my_acids[AT].name_nl;
    	brewing_salt_sub(w, Acid, MISC_USES_SPARGE);	// Put it in the miscs table.
    	ui->sp_acidvolEdit->setValue(Acid);
    }

    ui->sp_phShow->setValue(product->sparge_ph);
    // Finally calculate the estimate preboil pH
    product->est_preboil_ph = -log10(((pow(10, -product->mash_ph) * product->wg_amount) + (pow(10, -product->sparge_ph) * product->brew_sparge_est)) /
		     	      (product->wg_amount + product->brew_sparge_est));
    ui->preboil_phEdit->setValue(product->est_preboil_ph);
    ui->brew_preboilphShow->setValue(product->est_preboil_ph);
}


void EditProduct::sp_group_changed(int val)
{
    if (val != product->sparge_source) {
	product->sparge_source = val;
    	calcSparge();
    	is_changed();
    }
}


void EditProduct::sp_volume_changed(double val)
{
    product->sparge_volume = val;
    ui->brew_spargevolShow->setValue(val);
    calcSparge();
    is_changed();
}


void EditProduct::sp_temp_changed(double val)
{
    product->sparge_temp = val;
    calcSparge();
    is_changed();
}


void EditProduct::sp_type_changed(int val)
{
    if (val == product->sparge_acid_type)
        return;

    qDebug() << "sp_type_changed" << val << "old" << product->sparge_acid_type;
    /*
     * First remove current acid.
     */
    QString w = my_acids[product->sparge_acid_type].name_en + ' ' + my_acids[product->sparge_acid_type].name_nl;
    brewing_salt_sub(w, 0, MISC_USES_SPARGE);

    product->sparge_acid_type = val;
    w = my_acids[product->sparge_acid_type].name_en + ' ' + my_acids[product->sparge_acid_type].name_nl;

    product->sparge_acid_perc = my_acids[val].AcidPrc;
    ui->sp_acidpercEdit->setValue(product->sparge_acid_perc);
    brewing_salt_sub(w, ui->sp_acidvolEdit->value(), MISC_USES_SPARGE);	// For now, set old amount.

    calcSparge();
    is_changed();
}


void EditProduct::sp_ph_changed(double val)
{
    product->sparge_ph = val;
    ui->brew_spargephShow->setValue(product->sparge_ph);
    calcSparge();
    is_changed();
}


void EditProduct::sp_acid_changed(double val)
{
    if (product->calc_acid)
        return;

    qDebug() << "sp_acid_changed" << val << product->sparge_acid_amount * 1000.0;

    double TargetpH = product->sparge_ph;
    double Source_pH = product->w1_ph;
    double Source_alkalinity = product->w1_total_alkalinity;

    if (product->sparge_source == 1) { // Source 2
	if (product->w2_ph > 0.0 && product->w2_amount > 0) {
	    Source_pH = product->w2_ph;
            Source_alkalinity = product->w2_total_alkalinity;
	}
    } else if (product->sparge_source == 2) { // Mixed
	if (product->w2_ph > 0.0 && product->w2_amount > 0) {
	    Source_pH = product->wg_ph;
            Source_alkalinity = product->wg_total_alkalinity;
	}
    }

    int AT = product->sparge_acid_type;
    if (AT < 0 || AT >= my_acids.size()) {
        AT = 0;
        product->sparge_acid_type = 0;
        ui->sp_acidtypeEdit->setCurrentIndex(0);
        product->sparge_acid_perc = my_acids[0].AcidPrc;
        ui->sp_acidpercEdit->setValue(product->sparge_acid_perc);
    }

    bool go_up = (val < (product->sparge_acid_amount * 1000.0));
    bool loop = true;

    while (loop) {

	if (go_up)
	    TargetpH += 0.001;
	else
	    TargetpH -= 0.001;
	//qDebug() << "  TargetpH" << TargetpH << "up" << go_up;

        // Step 1: Compute the mole fractions of carbonic (f1o), bicarbonate (f2o) and carbonate(f3o) at the water pH
        double r1 = pow(10, Source_pH - 6.35);
        double r2 = pow(10, Source_pH - 10.33);
        double d = 1 + r1 + r1 * r2;
        double f1 = 1 / d;
        double f3 = r1 * r2 / d;

        // Step 2. Compute the mole fractions at pH = 4.3 (the pH which defines alkalinity)
        double r143 = pow(10, 4.3 - 6.35);
        double r243 = pow(10, 4.3 - 10.33);
        double d43 = 1 + r143 + r143 * r243;
        double f143 = 1 / d43;
        double f343 = r143 * r243 / d43;

        // Step 4. Solve
        double Ct = Source_alkalinity / 50 / ((f143 - f1) + (f3 - f343));

        // Step 5. Compute mole fractions at desired pH
        double r1g = pow(10, TargetpH - 6.35);
        double r2g = pow(10, TargetpH - 10.33);
        double dg = 1 + r1g + r1g * r2g;
        double f1g = 1 / dg;
        double f3g = r1g * r2g / dg;

        // Step 6. Use these to compute the milliequivalents acid required per liter (mEq/L)
        double Acid = Ct * ((f1g - f1) + (f3 - f3g)) + pow(10, -TargetpH) - pow(10, -Source_pH);  //mEq/l
        Acid += 0.01;   // Add acid that would be required for distilled water.

        // Step 7. There is no step 7.

        // Step 8. Get the acid data.
        double fract = Utils::CalcFrac(TargetpH, my_acids[AT].pK1, my_acids[AT].pK2, my_acids[AT].pK3);

        // Step 9. Now divide the mEq required by the "fraction". This is the required number of moles of acid.
        Acid /= fract;

        // Step 10. Multiply by molecular weight of the acid
        Acid *= my_acids[AT].MolWt; //mg

        // Step 11. Divide by Specific Gravity and Percentage to get the final ml.
        double RealSG = round(((my_acids[AT].AcidSG - 1000) * (product->sparge_acid_perc / 100)) + 1000);
        Acid = Acid / RealSG;           //ml
        Acid *= product->sparge_volume; //ml acid total at 100%
        Acid /= my_acids[AT].AcidPrc / 100;     //ml acid at supplied strength
        Acid = round(Acid * 100.0) / 100.0;
        product->sparge_acid_amount = Acid / 1000;
	//qDebug() << "  acid" << product->sparge_acid_amount;

	if (go_up && (val > (product->sparge_acid_amount * 1000.0)))
	    loop = false;
	else if (! go_up && (val < (product->sparge_acid_amount * 1000.0)))
	    loop = false;

	//qDebug() << "  test" << loop << go_up << val << product->sparge_acid_amount * 1000.0;
    }

    const QSignalBlocker blocker1(ui->sp_phEdit);
    product->sparge_ph = round(TargetpH * 100) / 100;
    ui->sp_phEdit->setValue(product->sparge_ph);
    ui->sp_phShow->setValue(product->sparge_ph);

    QString w = my_acids[AT].name_en + ' ' + my_acids[AT].name_nl;
    set_brewing_salt(w, val, MISC_USES_SPARGE);
    //qDebug() << "  new" << product->sparge_ph << val;
}


double EditProduct::GetBUGU()
{
    double gu = (product->est_og - 1) * 1000;
    if (gu > 0)
	return product->est_ibu / gu;
    return 0.5;
}


double EditProduct::GetOptSO4Clratio()
{
    if (ui->wt_so4Edit->value() > 0 && ui->wt_clEdit->value()) {
	/* If target water is selected .. */
	return (ui->wt_so4Edit->value() / ui->wt_clEdit->value());
    }
    double BUGU = GetBUGU();
    return (-1.2 * BUGU + 1.4);
}


void EditProduct::mw_calc_acid_clicked()
{
    product->calc_acid = ! product->calc_acid;
    ui->mw_autoEdit->setChecked(product->calc_acid);
    ui->mw_phEdit->setReadOnly(! product->calc_acid);
    ui->mw_phEdit->setButtonSymbols(product->calc_acid ? QAbstractSpinBox::UpDownArrows : QAbstractSpinBox::NoButtons);
    ui->mw_acidvolEdit->setReadOnly(product->calc_acid);
    ui->mw_acidvolEdit->setButtonSymbols(product->calc_acid ? QAbstractSpinBox::NoButtons : QAbstractSpinBox::UpDownArrows);
    ui->sp_phEdit->setReadOnly(! product->calc_acid);
    ui->sp_phEdit->setButtonSymbols(product->calc_acid ? QAbstractSpinBox::UpDownArrows : QAbstractSpinBox::NoButtons);
    ui->sp_acidvolEdit->setReadOnly(product->calc_acid);
    ui->sp_acidvolEdit->setButtonSymbols(product->calc_acid ? QAbstractSpinBox::NoButtons : QAbstractSpinBox::UpDownArrows);
    is_changed();
    calcWater();
}


void EditProduct::mw_ph_changed(double val)
{
    if (! product->calc_acid)
	return;

    if (product->mash_ph != val) {
	qDebug() << "mw_ph_changed" << val << product->mash_ph;
	product->mash_ph = val;
	is_changed();
	calcWater();
    }
}


void EditProduct::mw_acid_changed(double val)
{
    if (product->calc_acid)
	return;

    qDebug() << "on_mw_acid_changed" << val;
    QString w = my_acids[product->wa_acid_name].name_en + ' ' + my_acids[product->wa_acid_name].name_nl;
    set_brewing_salt(w, val, MISC_USES_MASH);
}


void EditProduct::mw_type_changed(int val)
{
    if (val == product->wa_acid_name)
	return;

    qDebug() << "on_mw_type_changed" << val << "old" << product->wa_acid_name;
    /*
     * First remove current acid.
     */
    QString w = my_acids[product->wa_acid_name].name_en + ' ' + my_acids[product->wa_acid_name].name_nl;
    brewing_salt_sub(w, 0, MISC_USES_MASH);

    product->wa_acid_name = val;
    w = my_acids[product->wa_acid_name].name_en + ' ' + my_acids[product->wa_acid_name].name_nl;

    product->wa_acid_perc = my_acids.at(val).AcidPrc;
    ui->mw_acidpercEdit->setValue(my_acids.at(val).AcidPrc);
    brewing_salt_sub(w, ui->mw_acidvolEdit->value(), MISC_USES_MASH);   // For now, set old amount.

    is_changed();
    calcWater();
}


void EditProduct::w2_volume_changed(double val)
{
    qDebug() << "w2_vol_changed" << val;

    if (product->w2_total_alkalinity && product->w2_sulfate) {
	/*
	 * Seems a valid water, but don't go over the total.
	 */
	if (val < (product->w1_amount + product->w2_amount)) {
	    product->w1_amount -= val - product->w2_amount;
	    product->w2_amount = val;
	    ui->w1_volEdit->setValue(product->w1_amount);
	}
    } else {
	/*
	 * Invalid water, block changes.
	 */
	product->w2_amount = 0;
    }
    ui->w2_volEdit->setValue(product->w2_amount);

    check_waters();
    calcWater();
    is_changed();
}


void EditProduct::check_waters()
{
    QSqlQuery query;
    product->waters_ok = true;

    if (product->w1_name != "") {
	qDebug() << "check_waters 1" << product->w1_name;
    	query.prepare("SELECT unlimited_stock,inventory FROM inventory_waters WHERE name=:name");
    	query.bindValue(":name", product->w1_name);
    	query.exec();
	if (query.first()) {
	    if ((query.value("unlimited_stock").toInt() == 0) && (query.value("inventory").toDouble() < product->w1_amount)) {
		product->waters_ok = false;
		qDebug() << "w1_amount too low";
	    }
	}
    }

    if ((product->w2_name != "") && (product->w2_amount > 0)) {
        qDebug() << "check_waters 2" << product->w2_name;
        query.prepare("SELECT unlimited_stock,inventory FROM inventory_waters WHERE name=:name");
        query.bindValue(":name", product->w2_name);
        query.exec();
        if (query.first()) {
            if ((query.value("unlimited_stock").toInt() == 0) && (query.value("inventory").toDouble() < product->w2_amount)) {
                product->waters_ok = false;
                qDebug() << "w2_amount too low";
            }
	    ui->w2_spButton->setEnabled(true);
            ui->wg_spButton->setEnabled(true);
        }
    } else {
	/*
	 * Block selecting sparge water 2 and mixed water.
	 */
	ui->w2_spButton->setEnabled(false);
	ui->wg_spButton->setEnabled(false);
	product->sparge_source = 0;      // Only water source 1
    }
}


void EditProduct::w1_name_changed(int val)
{
    QSqlQuery query;

    qDebug() << "w1_name_changed" << val;
    const QSignalBlocker blocker1(ui->w1_nameEdit);
    if (val == 0) {
	/*
	 * If no water is selected, take the default water.
	 */
	val = my_default_water;
	ui->w1_nameEdit->setCurrentIndex(val);
    }

    query.prepare("SELECT * FROM inventory_waters ORDER BY record");
    query.exec();
    query.first();
    for (int i = 0; i < (val - 1); i++) {
	query.next();
    }
    qDebug() << "set water" << query.value("name").toString();

    product->w1_name = query.value("name").toString();
    product->w1_calcium = query.value("calcium").toDouble();
    product->w1_magnesium = query.value("magnesium").toDouble();
    product->w1_total_alkalinity = query.value("total_alkalinity").toDouble();
    product->w1_sodium = query.value("sodium").toDouble();
    product->w1_chloride = query.value("chloride").toDouble();
    product->w1_sulfate = query.value("sulfate").toDouble();
    product->w1_ph = query.value("ph").toDouble();

    ui->w1_caEdit->setValue(product->w1_calcium);
    ui->w1_mgEdit->setValue(product->w1_magnesium);
    ui->w1_hco3Edit->setValue(Utils::Bicarbonate(product->w1_total_alkalinity, product->w1_ph));
    ui->w1_caco3Edit->setValue(product->w1_total_alkalinity);
    ui->w1_naEdit->setValue(product->w1_sodium);
    ui->w1_clEdit->setValue(product->w1_chloride);
    ui->w1_so4Edit->setValue(product->w1_sulfate);
    ui->w1_phEdit->setValue(product->w1_ph);
    ui->w1_hardnessEdit->setValue(Utils::Hardness(product->w1_calcium, product->w1_magnesium));
    ui->w1_raEdit->setValue(Utils::ResidualAlkalinity(product->w1_total_alkalinity, product->w1_calcium, product->w1_magnesium));

    check_waters();
    is_changed();
    calcWater();
}


void EditProduct::w2_name_changed(int val)
{
    QSqlQuery query;
    double hardness, ra_ppm;

    qDebug() << "w2_name_changed" << val;

    if (val == 0) {	// Clear water 2.
	product->w2_name = "";
	product->w2_calcium = 0;
	product->w2_magnesium = 0;
	product->w2_total_alkalinity = 0;
	product->w2_sodium = 0;
	product->w2_chloride = 0;
	product->w2_sulfate = 0;
	product->w2_ph = 0;
	product->w1_amount += product->w2_amount;
	product->w2_amount = 0;
	hardness = ra_ppm = 0;
    } else {
        query.prepare("SELECT * FROM inventory_waters ORDER BY record");
        query.exec();
        query.first();
        for (int i = 0; i < (val - 1); i++) {
            query.next();
        }
	qDebug() << "set water" << query.value(1).toString();

	product->w2_name = query.value(1).toString();
	product->w2_calcium = query.value(3).toDouble();
        product->w2_magnesium = query.value(8).toDouble();
        product->w2_total_alkalinity = query.value(11).toDouble();
        product->w2_sodium = query.value(7).toDouble();
        product->w2_chloride = query.value(6).toDouble();
        product->w2_sulfate = query.value(5).toDouble();
        product->w2_ph = query.value(9).toDouble();
	hardness = Utils::Hardness(product->w2_calcium, product->w2_magnesium);
	ra_ppm = Utils::ResidualAlkalinity(product->w2_total_alkalinity, product->w2_calcium, product->w2_magnesium);
    }
    ui->w1_volEdit->setValue(product->w1_amount);
    ui->w2_volEdit->setValue(product->w2_amount);
    ui->w2_caEdit->setValue(product->w2_calcium);
    ui->w2_mgEdit->setValue(product->w2_magnesium);
    ui->w2_hco3Edit->setValue(Utils::Bicarbonate(product->w2_total_alkalinity, product->w2_ph));
    ui->w2_caco3Edit->setValue(product->w2_total_alkalinity);
    ui->w2_naEdit->setValue(product->w2_sodium);
    ui->w2_clEdit->setValue(product->w2_chloride);
    ui->w2_so4Edit->setValue(product->w2_sulfate);
    ui->w2_phEdit->setValue(product->w2_ph);
    ui->w2_hardnessEdit->setValue(hardness);
    ui->w2_raEdit->setValue(ra_ppm);

    check_waters();
    is_changed();
    calcWater();
}


void EditProduct::wt_target_changed(int val)
{
    QSqlQuery query;

    if (val == 0) {
	/* Clear values */
	ui->wt_caEdit->setValue(0);
	ui->wt_mgEdit->setValue(0);
	ui->wt_hco3Edit->setValue(0);
	ui->wt_caco3Edit->setValue(0);
	ui->wt_naEdit->setValue(0);
	ui->wt_clEdit->setValue(0);
	ui->wt_so4Edit->setValue(0);
	ui->wt_hardnessEdit->setValue(0);
    	ui->wt_raEdit->setValue(0);
    } else {
	query.prepare("SELECT * FROM profile_water ORDER BY name");
    	query.exec();
	query.first();
    	for (int i = 0; i < (val - 1); i++) {
            query.next();
    	}
	ui->wt_caEdit->setValue(query.value("calcium").toDouble());
        ui->wt_mgEdit->setValue(query.value("magnesium").toDouble());
        ui->wt_hco3Edit->setValue(query.value("bicarbonate").toDouble());
        ui->wt_caco3Edit->setValue(query.value("total_alkalinity").toDouble());
        ui->wt_naEdit->setValue(query.value("sodium").toDouble());
        ui->wt_clEdit->setValue(query.value("chloride").toDouble());
        ui->wt_so4Edit->setValue(query.value("sulfate").toDouble());
	ui->wt_hardnessEdit->setValue(Utils::Hardness(query.value("calcium").toDouble(), query.value("magnesium").toDouble()));
    	ui->wt_raEdit->setValue(Utils::ResidualAlkalinity(query.value("total_alkalinity").toDouble(), query.value("calcium").toDouble(), query.value("magnesium").toDouble()));
    }
    calcWater();
}


void EditProduct::adjustWaters(double factor)
{
    int i;
    double amount;

    if (product->mashs.size() == 0)
	return;

    double mash_infuse = 0;
    for (i = 0; i < product->mashs.size(); i++) {
	if (product->mashs.at(i).step_type == 0) { // Infusion
	    amount = round(product->mashs.at(i).step_infuse_amount * factor * 1000000.0) / 1000000.0;
	    product->mashs[i].step_infuse_amount = amount;
   	    mash_infuse += amount;
	    product->mashs[i].step_volume = mash_infuse;
  	}
    }

    const QSignalBlocker blocker1(ui->w1_volEdit);
    const QSignalBlocker blocker2(ui->w2_volEdit);

    if (product->w2_amount == 0) {
	product->w1_amount = mash_infuse;
	ui->w1_volEdit->setValue(mash_infuse);
    } else {
	double w1 = (product->w1_amount / (product->w1_amount + product->w2_amount)) * mash_infuse;
	double w2 = (product->w2_amount / (product->w1_amount + product->w2_amount)) * mash_infuse;
	product->w1_amount = w1;
	product->w2_amount = w2;
	ui->w1_volEdit->setValue(product->w1_amount);
	ui->w2_volEdit->setValue(product->w2_amount);
    }
    product->wg_amount = mash_infuse;
    ui->wg_volEdit->setValue(mash_infuse);
}


void EditProduct::wb_cacl2_changed(double val)  { set_brewing_salt("CaCl2", val, MISC_USES_MASH);   }
void EditProduct::wb_caso4_changed(double val)  { set_brewing_salt("CaSO4", val, MISC_USES_MASH);   }
void EditProduct::wb_mgso4_changed(double val)  { set_brewing_salt("MgSO4", val, MISC_USES_MASH);   }
void EditProduct::wb_nacl_changed(double val)   { set_brewing_salt("NaCl", val, MISC_USES_MASH);    }
void EditProduct::wb_mgcl2_changed(double val)  { set_brewing_salt("MgCl2", val, MISC_USES_MASH);   }
void EditProduct::wb_nahco3_changed(double val) { set_brewing_salt("NaHCO3", val, MISC_USES_MASH);  }
void EditProduct::wb_caco3_changed(double val)  { set_brewing_salt("CaCO3", val, MISC_USES_MASH);   }
void EditProduct::sp_cacl2_changed(double val)  { set_brewing_salt("CaCl2", val, MISC_USES_SPARGE); }
void EditProduct::sp_caso4_changed(double val)  { set_brewing_salt("CaSO4", val, MISC_USES_SPARGE); }
void EditProduct::sp_mgso4_changed(double val)  { set_brewing_salt("MgSO4", val, MISC_USES_SPARGE); }
void EditProduct::sp_nacl_changed(double val)   { set_brewing_salt("NaCl", val, MISC_USES_SPARGE);  }
void EditProduct::sp_mgcl2_changed(double val)  { set_brewing_salt("MgCl2", val, MISC_USES_SPARGE); }

mercurial