src/EditRecipeTab7.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 358
d89bc21e2f07
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.

/**
 * EditRecipe.cpp is part of bmsapp.
 *
 * tab 6, 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 EditRecipe::refreshWaters()
{

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

}


/*
 * Z alkalinity is the amount of acid (in mEq/l) needed to bring water to the target pH (Z pH)
 */
double EditRecipe::ZAlkalinity(double pHZ)
{
    double C43 = Utils::Charge(4.3);
    double Cw = Utils::Charge(recipe->wg_ph);
    double Cz = Utils::Charge(pHZ);
    double DeltaCNaught = -C43 + Cw;
    double CT = recipe->wg_total_alkalinity / 50 / DeltaCNaught;
    double DeltaCZ = -Cz + Cw;
    return CT * DeltaCZ;
}


/*
 * Z Residual alkalinity is the amount of acid (in mEq/l) needed
 * to bring the water in the mash to the target pH (Z pH).
 */
double EditRecipe::ZRA(double pHZ)
{
    double Calc = recipe->wg_calcium / (MMCa / 2);
    double Magn = recipe->wg_magnesium / (MMMg / 2);
    double Z = ZAlkalinity(pHZ);
    return Z - (Calc / 3.5 + Magn / 7);
}


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

    if ((F.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 0:					// Base, Special, Kilned
	   case 3:
	   case 5:	C1 = 0.014 * F.color - 34.192;
			break;
	   case 2:	C1 = -0.0597 * F.color - 32.457;	// Crystal
			break;
	   case 1:	C1 = 0.0107 * F.color - 54.768;	// Roast
			break;
	   case 4:	C1 = -149;                      // Sour malt
			break;
	}
    }
    return C1;
}


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


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

    /*
     * proton deficit for the grist
     */
    if (recipe->fermentables.size()) {
	for (i = 0; i < recipe->fermentables.size(); i++) {
	    F = recipe->fermentables.at(i);
	    if (F.added == 0 && F.graintype != 6) { // Added == Mash && 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 EditRecipe::MashpH()
{
    int n = 0;
    double pH = 5.4;
    double deltapH = 0.001;
    double deltapd = 0.1;
    double pd = ProtonDeficit(pH);

    while (((pd < -deltapd) || (pd > deltapd)) && (n < 2000)) {
	n++;
	if (pd < -deltapd)
	    pH -= deltapH;
	else if (pd > deltapd)
	    pH += deltapH;
	pd = ProtonDeficit(pH);
    }
    pH = round(pH * 1000000) / 1000000.0;
    qDebug() << "  MashpH() n:" << n << "pH:" << pH;
    return pH;
}


void EditRecipe::calcWater()
{
    double liters = 0;
    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(recipe->w1_calcium, recipe->w1_magnesium));
    ui->w1_raEdit->setValue(Utils::ResidualAlkalinity(recipe->w1_total_alkalinity, recipe->w1_calcium, recipe->w1_magnesium));

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

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

    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 (recipe->wa_acid_name < 0 || recipe->wa_acid_name >= my_acids.size()) {
	recipe->wa_acid_name = 0;
	recipe->wa_acid_perc = my_acids.at(0).AcidPrc;
	ui->mw_acidPick->setCurrentIndex(0);
	ui->mw_acidpercEdit->setValue(my_acids.at(0).AcidPrc);
    }
    AT = recipe->wa_acid_name;

    /*
     * Note that the next calculations do not correct the pH change by the added salts.
     * This pH change is at most 0.1 pH and is a minor difference in Acid amount.
     */
    if (recipe->calc_acid) {
	/*
	 * Auto calculate the needed acid.
	 */
	TpH = recipe->mash_ph;
	protonDeficit = ProtonDeficit(TpH);
	qDebug() << "  calc_acid tgt:" << TpH << "protonDeficit:" << protonDeficit;
	if (protonDeficit > 0) {
	    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 / (recipe->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) * (recipe->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);
	recipe->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) * (recipe->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() << "  protonDeficit Acid:" << 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);
        recipe->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);

    recipe->wb_calcium = calcium;
    recipe->wb_magnesium = magnesium;
    recipe->wb_total_alkalinity = total_alkalinity;
    recipe->wb_sodium = sodium;
    recipe->wb_chloride = chloride;
    recipe->wb_sulfate = sulfate;
    recipe->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 EditRecipe::calcSparge()
{
    double TargetpH = recipe->sparge_ph;
    double Source_pH = recipe->w1_ph;
    double Source_alkalinity = recipe->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 (recipe->sparge_source == 1) { // Source 2
	if (recipe->w2_ph > 0.0 && recipe->w2_amount > 0) {
	    Source_pH = recipe->w2_ph;
	    Source_alkalinity = recipe->w2_total_alkalinity;
	    ui->sp_caEdit->setValue(recipe->w2_calcium);
            ui->sp_mgEdit->setValue(recipe->w2_magnesium);
            ui->sp_hco3Edit->setValue(Utils::Bicarbonate(recipe->w2_total_alkalinity, recipe->w2_ph));
            ui->sp_caco3Edit->setValue(recipe->w2_total_alkalinity);
            ui->sp_naEdit->setValue(recipe->w2_sodium);
            ui->sp_clEdit->setValue(recipe->w2_chloride);
            ui->sp_so4Edit->setValue(recipe->w2_sulfate);
            ui->sp_phShow->setValue(recipe->w2_ph);
            ui->sp_hardnessEdit->setValue(Utils::Hardness(recipe->w2_calcium, recipe->w2_magnesium));
            ui->sp_raEdit->setValue(Utils::ResidualAlkalinity(recipe->w2_total_alkalinity, recipe->w2_calcium, recipe->w2_magnesium));
            ui->w2_spButton->setChecked(true);
	} else {
	    recipe->sparge_source = 0; // Fallback to source 1
	    ui->w1_spButton->setChecked(true);
	}
    } else if (recipe->sparge_source == 2) { // Mixed
	if (recipe->w2_ph > 0.0 && recipe->w2_amount > 0) {
	    Source_pH = recipe->wg_ph;
	    Source_alkalinity = recipe->wg_total_alkalinity;
	    ui->sp_caEdit->setValue(recipe->wg_calcium);
	    ui->sp_mgEdit->setValue(recipe->wg_magnesium);
	    ui->sp_hco3Edit->setValue(Utils::Bicarbonate(recipe->wg_total_alkalinity, recipe->wg_ph));
	    ui->sp_caco3Edit->setValue(recipe->wg_total_alkalinity);
	    ui->sp_naEdit->setValue(recipe->wg_sodium);
	    ui->sp_clEdit->setValue(recipe->wg_chloride);
	    ui->sp_so4Edit->setValue(recipe->wg_sulfate);
	    ui->sp_phShow->setValue(recipe->wg_ph);
	    ui->sp_hardnessEdit->setValue(Utils::Hardness(recipe->wg_calcium, recipe->wg_magnesium));
	    ui->sp_raEdit->setValue(Utils::ResidualAlkalinity(recipe->wg_total_alkalinity, recipe->wg_calcium, recipe->wg_magnesium));
	    ui->wg_spButton->setChecked(true);
	} else {
	    recipe->sparge_source = 0; // Fallback to source 1
	    ui->w1_spButton->setChecked(true);
	}
    }
    if (recipe->sparge_source == 0) {
       ui->sp_caEdit->setValue(recipe->w1_calcium);
       ui->sp_mgEdit->setValue(recipe->w1_magnesium);
       ui->sp_hco3Edit->setValue(Utils::Bicarbonate(recipe->w1_total_alkalinity, recipe->w1_ph));
       ui->sp_caco3Edit->setValue(recipe->w1_total_alkalinity);
       ui->sp_naEdit->setValue(recipe->w1_sodium);
       ui->sp_clEdit->setValue(recipe->w1_chloride);
       ui->sp_so4Edit->setValue(recipe->w1_sulfate);
       ui->sp_phShow->setValue(recipe->w1_ph);
       ui->sp_hardnessEdit->setValue(Utils::Hardness(recipe->w1_calcium, recipe->w1_magnesium));
       ui->sp_raEdit->setValue(Utils::ResidualAlkalinity(recipe->w1_total_alkalinity, recipe->w1_calcium, recipe->w1_magnesium));
       ui->w1_spButton->setChecked(true);
    }
    // The spargewater is set.

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

    /*
     * Auto calculate the required acid
     */
    if (recipe->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 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) * (recipe->sparge_acid_perc / 100)) + 1000);
	Acid = Acid / RealSG;           //ml
	Acid *= recipe->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;
	recipe->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(recipe->sparge_ph);
    // Finally calculate the estimate preboil pH
    recipe->preboil_ph = -log10(((pow(10, -recipe->mash_ph) * recipe->wg_amount) + (pow(10, -recipe->sparge_ph) * recipe->sparge_volume)) /
                             (recipe->wg_amount + recipe->sparge_volume));
    ui->preboil_phEdit->setValue(recipe->preboil_ph);
}


void EditRecipe::sp_group_changed(int val)
{
    if (val != recipe->sparge_source) {
           qDebug() << "sp_group_changed" << val;
       recipe->sparge_source = val;
       calcSparge();
       is_changed();
    }
}


void EditRecipe::sp_type_changed(int val)
{
    if (val == recipe->sparge_acid_type)
        return;

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

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

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

    calcSparge();
    is_changed();
}


void EditRecipe::sp_ph_changed(double val)
{
    recipe->sparge_ph = val;
    calcSparge();
    is_changed();
}


void EditRecipe::sp_acid_changed(double val)
{
    if (recipe->calc_acid)
        return;

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

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

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

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

    bool go_up = (val < (recipe->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) * (recipe->sparge_acid_perc / 100)) + 1000);
        Acid = Acid / RealSG;           //ml
        Acid *= recipe->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;
        recipe->sparge_acid_amount = Acid / 1000;
       //qDebug() << "  acid" << recipe->sparge_acid_amount;

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

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

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

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


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


double EditRecipe::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 EditRecipe::mw_calc_acid_clicked()
{
    recipe->calc_acid = ! recipe->calc_acid;
    ui->mw_autoEdit->setChecked(recipe->calc_acid);
    ui->mw_phEdit->setReadOnly(! recipe->calc_acid);
    ui->mw_phEdit->setButtonSymbols(recipe->calc_acid ? QAbstractSpinBox::UpDownArrows : QAbstractSpinBox::NoButtons);
    ui->mw_acidvolEdit->setReadOnly(recipe->calc_acid);
    ui->mw_acidvolEdit->setButtonSymbols(recipe->calc_acid ? QAbstractSpinBox::NoButtons : QAbstractSpinBox::UpDownArrows);
    ui->sp_phEdit->setReadOnly(! recipe->calc_acid);
    ui->sp_phEdit->setButtonSymbols(recipe->calc_acid ? QAbstractSpinBox::UpDownArrows : QAbstractSpinBox::NoButtons);
    ui->sp_acidvolEdit->setReadOnly(recipe->calc_acid);
    ui->sp_acidvolEdit->setButtonSymbols(recipe->calc_acid ? QAbstractSpinBox::NoButtons : QAbstractSpinBox::UpDownArrows);
    is_changed();
    calcWater();
}


void EditRecipe::mw_ph_changed(double val)
{
    if (! recipe->calc_acid)
	return;

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


void EditRecipe::mw_acid_changed(double val)
{
    if (recipe->calc_acid)
	return;

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


void EditRecipe::mw_type_changed(int val)
{
    if (val == recipe->wa_acid_name)
	return;

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

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

    recipe->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 EditRecipe::w2_volume_changed(double val)
{
    qDebug() << "w2_vol_changed" << val;

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

    calcWater();
    is_changed();
}


void EditRecipe::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(1).toString();

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

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

    is_changed();
    calcWater();
}


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

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

    if (val == 0) {	// Clear water 2.
	recipe->w2_name = "";
	recipe->w2_calcium = 0;
	recipe->w2_magnesium = 0;
	recipe->w2_total_alkalinity = 0;
	recipe->w2_sodium = 0;
	recipe->w2_chloride = 0;
	recipe->w2_sulfate = 0;
	recipe->w2_ph = 0;
	recipe->w1_amount += recipe->w2_amount;
	recipe->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();

	recipe->w2_name = query.value("name").toString();
	recipe->w2_calcium = query.value("calcium").toDouble();
        recipe->w2_magnesium = query.value("magnesium").toDouble();
        recipe->w2_total_alkalinity = query.value("total_alkalinity").toDouble();
        recipe->w2_sodium = query.value("sodium").toDouble();
        recipe->w2_chloride = query.value("chloride").toDouble();
        recipe->w2_sulfate = query.value("sulfate").toDouble();
        recipe->w2_ph = query.value("ph").toDouble();
	hardness = Utils::Hardness(recipe->w2_calcium, recipe->w2_magnesium);
	ra_ppm = Utils::ResidualAlkalinity(recipe->w2_total_alkalinity, recipe->w2_calcium, recipe->w2_magnesium);
    }
    ui->w1_volEdit->setValue(recipe->w1_amount);
    ui->w2_volEdit->setValue(recipe->w2_amount);
    ui->w2_caEdit->setValue(recipe->w2_calcium);
    ui->w2_mgEdit->setValue(recipe->w2_magnesium);
    ui->w2_hco3Edit->setValue(Utils::Bicarbonate(recipe->w2_total_alkalinity, recipe->w2_ph));
    ui->w2_caco3Edit->setValue(recipe->w2_total_alkalinity);
    ui->w2_naEdit->setValue(recipe->w2_sodium);
    ui->w2_clEdit->setValue(recipe->w2_chloride);
    ui->w2_so4Edit->setValue(recipe->w2_sulfate);
    ui->w2_phEdit->setValue(recipe->w2_ph);
    ui->w2_hardnessEdit->setValue(hardness);
    ui->w2_raEdit->setValue(ra_ppm);

    is_changed();
    calcWater();
}


void EditRecipe::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 EditRecipe::adjustWaters(double factor)
{
    int i;
    double amount;

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

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

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

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


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

mercurial