Created
March 7, 2015 12:28
-
-
Save jcipar/6247f03350e8ab629bc5 to your computer and use it in GitHub Desktop.
The third Brewing Salts prototype. This one separates mash and sparge modification, and calculates residual alkalinity.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
This is a program that automatically determines appropriate mineral additions for brewing water. | |
I was playing with Bru'n Water, trying to determine how to adjust my water (Boston MWRA), and | |
was frustrated with the somewhat manual trial-and-error process. With this program I type in my | |
current and desired water profiles, and it automatically determines a suggested list of | |
salt and acidadditions. It solves the problem of salt additions, but it is not a replacement | |
for tools like Bru'n Water: it does not verify the water report, and currently will only suggest | |
mash adjustments, and ignores sparge water completely. | |
The way I am currently using this is to select the target water profile from Bru'n Water, type | |
the targets into this program, and run it. It produces a list of suggested salt additions, often | |
some of those salts are in very small quantities, so I will use the max_XXX (e.g. max_gypsum) | |
parameter to completely disallow certain salts, then re-run the program to get a new suggestion. | |
To run the program type: | |
glpsol -m brewing-water.glpk | |
If you copy the "data" section from the bottom of this file to a new file, you can have separate | |
configurations for different water profiles. To choose which one to use, run a command like this: | |
glpsol -m brewing-water.glpk -d boston-to-amber.dat | |
This is a work in progress. It would be nice to provide more example water profiles, and an | |
improved user interface. Automating the "minimum salt addition" part would make it a lot easier. | |
Ultimately, turning it into an online tool would make it easiest for other people to use, since | |
most people don't have GLPK installed. | |
*/ | |
param mash_gallons; | |
param sparge_gallons; | |
param target_residual_alkalinity; | |
param target_calcium; | |
param target_magnesium; | |
param target_sodium; | |
param target_sulfate; | |
param target_chloride; | |
param target_bicarbonate; | |
param initial_calcium; | |
param initial_magnesium; | |
param initial_sodium; | |
param initial_sulfate; | |
param initial_chloride; | |
param initial_bicarbonate; | |
param max_gypsum; | |
param max_epsom; | |
param max_table; | |
param max_cacl2; | |
param max_mgcl2; | |
param max_soda; | |
param max_lime; | |
param max_chalk; | |
param max_lactic; | |
param max_phosphoric; | |
param max_sauermalz; | |
var mash_gypsum >= 0; | |
var mash_epsom >= 0; | |
var mash_table >= 0; | |
var mash_cacl2 >= 0; | |
var mash_mgcl2 >= 0; | |
var mash_soda >= 0; | |
var mash_lime >= 0; | |
var mash_chalk >= 0; | |
var sparge_gypsum >= 0; | |
var sparge_epsom >= 0; | |
var sparge_table >= 0; | |
var sparge_cacl2 >= 0; | |
var sparge_mgcl2 >= 0; | |
var sparge_soda >= 0; | |
var sparge_lime >= 0; | |
var sparge_chalk >= 0; | |
var residual_alkalinity; | |
var mash_calcium >= 0; | |
var mash_magnesium >= 0; | |
var mash_sodium >= 0; | |
var mash_sulfate >= 0; | |
var mash_chloride >= 0; | |
var mash_bicarbonate >=0; | |
var sparge_calcium >= 0; | |
var sparge_magnesium >= 0; | |
var sparge_sodium >= 0; | |
var sparge_sulfate >= 0; | |
var sparge_chloride >= 0; | |
var sparge_bicarbonate >=0; | |
var added_calcium >= 0; | |
var added_magnesium >= 0; | |
var added_sodium >= 0; | |
var added_sulfate >= 0; | |
var added_chloride >= 0; | |
var added_bicarbonate >=0; | |
/* Acids | |
It seems a bit atypical for a tool like this | |
to suggest sauermalz additions, but I think | |
it makes sense. For most recipes, sauermalz | |
is being used for water adjustment, and not | |
for its flavor. | |
*/ | |
var mash_lactic >= 0; | |
var mash_phosphoric >=0; | |
var mash_sauermalz >= 0; | |
var e_residual_alkalinity; | |
var e_calcium; | |
var e_magnesium; | |
var e_sodium; | |
var e_sulfate; | |
var e_chloride; | |
var e_bicarbonate; | |
var abse_residual_alkalinity; | |
var abse_calcium; | |
var abse_magnesium; | |
var abse_sodium; | |
var abse_sulfate; | |
var abse_chloride; | |
var abse_bicarbonate; | |
/* Assume RA is most important, followed by sulfate and chloride, then sodium and magnesium, then everything else */ | |
minimize z: abse_calcium + 10*abse_magnesium + 10*abse_sodium | |
+ 100*abse_sulfate + 100*abse_chloride + abse_bicarbonate | |
+ 1000*abse_residual_alkalinity | |
/* L1 regularization terms to encourage a sparse solution. Probably not necessary with | |
a simplex method solver, but neccesary for the web version that uses an interior | |
point method */ | |
+ 0.001 * mash_gypsum | |
+ 0.001 * mash_epsom | |
+ 0.001 * mash_table | |
+ 0.001 * mash_cacl2 | |
+ 0.001 * mash_mgcl2 | |
+ 0.001 * mash_soda | |
+ 0.001 * mash_lime | |
+ 0.001 * mash_chalk | |
+ 0.001 * mash_phosphoric | |
+ 0.001 * mash_lactic | |
+ 0.001 * mash_sauermalz | |
+ 0.001 * sparge_gypsum | |
+ 0.001 * sparge_epsom | |
+ 0.001 * sparge_table | |
+ 0.001 * sparge_cacl2 | |
+ 0.001 * sparge_mgcl2 | |
+ 0.001 * sparge_soda | |
+ 0.001 * sparge_lime | |
+ 0.001 * sparge_chalk | |
; | |
s.t. ABS_RA_P : abse_residual_alkalinity >= e_residual_alkalinity; | |
s.t. ABS_RA_N : abse_residual_alkalinity >= -e_residual_alkalinity; | |
s.t. ABS_CALCIUM_P : abse_calcium >= e_calcium; | |
s.t. ABS_CALCIUM_N : abse_calcium >= -e_calcium; | |
s.t. ABS_MAGNESIUM_P : abse_magnesium >= e_magnesium; | |
s.t. ABS_MAGNESIUM_N : abse_magnesium >= -e_magnesium; | |
s.t. ABS_SODIUM_P : abse_sodium >= e_sodium; | |
s.t. ABS_SODIUM_N : abse_sodium >= -e_sodium; | |
s.t. ABS_SULFATE_P : abse_sulfate >= e_sulfate; | |
s.t. ABS_SULFATE_N : abse_sulfate >= -e_sulfate; | |
s.t. ABS_CHLORIDE_P : abse_chloride >= e_chloride; | |
s.t. ABS_CHLORIDE_N : abse_chloride >= -e_chloride; | |
s.t. ABS_BICARBONATE_P : abse_bicarbonate >= e_bicarbonate; | |
s.t. ABS_BICARBONATE_N : abse_bicarbonate >= -e_bicarbonate; | |
s.t. MAX_GYPSUM : mash_gypsum + sparge_gypsum <= max_gypsum; | |
s.t. MAX_EPSOM : mash_epsom + sparge_epsom <= max_epsom; | |
s.t. MAX_TABLE : mash_table + sparge_table <= max_table; | |
s.t. MAX_CACL2 : mash_cacl2 + sparge_cacl2 <= max_cacl2; | |
s.t. MAX_MGCL2 : mash_mgcl2 + sparge_mgcl2 <= max_mgcl2; | |
s.t. MAX_SODA : mash_soda + sparge_soda <= max_soda; | |
s.t. MAX_LIME : mash_lime + sparge_lime <= max_lime; | |
s.t. MAX_CHALK : mash_chalk +sparge_chalk <= max_chalk; | |
s.t. MAX_LACTIC : mash_lactic <= max_lactic; | |
s.t. MAX_PHOSPHORIC: mash_phosphoric <= max_phosphoric; | |
s.t. MAX_SAUERMALZ: mash_sauermalz <= max_sauermalz; | |
/* Residual alkalinity as CaCO3 */ | |
s.t. RESIDUAL_ALKALINITY: | |
(mash_bicarbonate) * 0.8197 | |
- (mash_calcium) * 0.7143 | |
- (mash_magnesium) * 0.5882 | |
- mash_lactic * 153.1 | |
- mash_phosphoric * 14.88 | |
- mash_sauermalz * 125 | |
= residual_alkalinity; | |
s.t. E_RESIDUAL_ALKALINITY: | |
residual_alkalinity | |
+ e_residual_alkalinity | |
= target_residual_alkalinity; | |
s.t. MASH_CALCIUM: 61.5 * mash_gypsum | |
+ 72.0 * mash_cacl2 | |
+ 142.8 * mash_lime | |
+ 105.7 * mash_chalk | |
+ initial_calcium | |
= mash_calcium; | |
s.t. SPARGE_CALCIUM: 61.5 * sparge_gypsum | |
+ 72.0 * sparge_cacl2 | |
+ initial_calcium | |
= sparge_calcium; | |
s.t. TOTAL_CALCIUM : mash_calcium * (mash_gallons / (mash_gallons + sparge_gallons)) | |
+ sparge_calcium * (sparge_gallons / (mash_gallons + sparge_gallons)) | |
+ e_calcium | |
= target_calcium; | |
s.t. MASH_MAGNESIUM : 26.0 * mash_epsom | |
+ 31.6 * mash_mgcl2 | |
+ initial_magnesium | |
= mash_magnesium; | |
s.t. SPARGE_MAGNESIUM : 26.0 * sparge_epsom | |
+ 31.6 * sparge_mgcl2 | |
+ initial_magnesium | |
= sparge_magnesium; | |
s.t. TOTAL_MAGNESIUM : mash_magnesium * (mash_gallons / (mash_gallons + sparge_gallons)) | |
+ sparge_magnesium * (sparge_gallons / (mash_gallons + sparge_gallons)) | |
+ e_magnesium | |
= target_magnesium; | |
s.t. MASH_SODIUM : 104.0 * mash_table | |
+ 73.2 * mash_soda | |
+ initial_sodium | |
= mash_sodium; | |
s.t. SPARGE_SODIUM : 104.0 * sparge_table | |
+ 73.2 * sparge_soda | |
+ initial_sodium | |
= sparge_sodium; | |
s.t. TOTAL_SODIUM : mash_sodium * (mash_gallons / (mash_gallons + sparge_gallons)) | |
+ sparge_sodium * (sparge_gallons / (mash_gallons + sparge_gallons)) | |
+ e_sodium | |
= target_sodium; | |
s.t. MASH_SULFATE : 147.4 * mash_gypsum | |
+ 103.0 * mash_epsom | |
+ initial_sulfate | |
= mash_sulfate; | |
s.t. SPARGE_SULFATE : 147.4 * sparge_gypsum | |
+ 103.0 * sparge_epsom | |
+ initial_sulfate | |
= sparge_sulfate; | |
s.t. TOTAL_SULFATE : mash_sulfate * (mash_gallons / (mash_gallons + sparge_gallons)) | |
+ sparge_sulfate * (sparge_gallons / (mash_gallons + sparge_gallons)) | |
+ e_sulfate | |
= target_sulfate; | |
s.t. MASH_CHLORIDE : 160.3 * mash_table | |
+ 127.0 * mash_cacl2 | |
+ 92.2 * mash_mgcl2 | |
+ initial_chloride | |
= mash_chloride; | |
s.t. SPARGE_CHLORIDE : 160.3 * sparge_table | |
+ 127.0 * sparge_cacl2 | |
+ 92.2 * sparge_mgcl2 | |
+ initial_chloride | |
= sparge_chloride; | |
s.t. TOTAL_CHLORIDE : mash_chloride * (mash_gallons / (mash_gallons + sparge_gallons)) | |
+ sparge_chloride * (sparge_gallons / (mash_gallons + sparge_gallons)) | |
+ e_chloride | |
= target_chloride; | |
s.t. MASH_BICARBONATE : 191.9 * mash_soda | |
+ 434.8 * mash_lime | |
+ 322.3 * mash_chalk | |
+ initial_bicarbonate | |
= mash_bicarbonate; | |
s.t. SPARGE_BICARBONATE : | |
initial_bicarbonate | |
= sparge_bicarbonate; | |
s.t. TOTAL_BICARBONATE : mash_bicarbonate * (mash_gallons / (mash_gallons + sparge_gallons)) | |
+ sparge_bicarbonate * (sparge_gallons / (mash_gallons + sparge_gallons)) | |
+ e_bicarbonate | |
= target_bicarbonate; | |
solve; | |
printf "\nIon Error\n"; | |
printf "---------\n"; | |
printf "Calcium error: %f\n", -e_calcium; | |
printf "Magnesium error: %f\n", -e_magnesium; | |
printf "Sodium error: %f\n", -e_sodium; | |
printf "Sulfate error: %f\n", -e_sulfate; | |
printf "Chloride error: %f\n", -e_chloride; | |
printf "Bicarbonate error: %f\n", -e_bicarbonate; | |
printf "\nIons Added / Total / Target\n"; | |
printf "---------------------------\n"; | |
printf "Calcium: %f / %f / %f\n", target_calcium - e_calcium - initial_calcium, target_calcium - e_calcium, target_calcium; | |
printf "Magnesium: %f / %f / %f\n", target_magnesium - e_magnesium - initial_magnesium, target_magnesium - e_magnesium, target_magnesium; | |
printf "Sodium: %f / %f / %f\n", target_sodium - e_sodium - initial_sodium, target_sodium - e_sodium, target_sodium; | |
printf "Sulfate: %f / %f / %f\n", target_sulfate - e_sulfate - initial_sulfate, target_sulfate - e_sulfate, target_sulfate; | |
printf "Chloride: %f / %f / %f\n", target_chloride - e_chloride - initial_chloride, target_chloride - e_chloride, target_chloride; | |
printf "Bicarbonate: %f / %f / %f\n", target_bicarbonate - e_bicarbonate - initial_bicarbonate, target_bicarbonate - e_bicarbonate, target_bicarbonate; | |
printf "\npH Data\n"; | |
printf "-------\n"; | |
printf "Residual alkalinity: %f (%f error)\n", residual_alkalinity, | |
residual_alkalinity - target_residual_alkalinity; | |
printf "\n# Mash Water Additions (g/gal) (tsp / %.2f gal) #\n", mash_gallons; | |
printf "------------------------------------------\n"; | |
printf "---- Mineral Additions ----\n"; | |
printf "Gypsum (CaSO4): %f g/gal (%f g, ~ %f tsp)\n", mash_gypsum, | |
mash_gypsum * mash_gallons, mash_gypsum * mash_gallons / 4.0; | |
printf "Epsom Salt (MgSO4): %f g/gal (%f g, ~ %f tsp)\n", mash_epsom, | |
mash_epsom * mash_gallons, mash_epsom * mash_gallons / 4.5; | |
printf "Table Salt (NaCl): %f g/gal (%f g, ~ %f tsp)\n", mash_table, | |
mash_table * mash_gallons, mash_table * mash_gallons / 5.7; | |
printf "Calcium Chloride (CaCl2): %f g/gal (%f g, ~ %f tsp)\n", mash_cacl2, | |
mash_cacl2 * mash_gallons, mash_cacl2 * mash_gallons / 3.4; | |
printf "Magnesium Chloride (MgCl2): %f g/gal (%f g, ~ %f tsp)\n", mash_mgcl2, | |
mash_mgcl2 * mash_gallons, mash_mgcl2 * mash_gallons / 3.2; | |
printf "Baking Soda (NaHCO3): %f g/gal (%f g, ~ %f tsp)\n", mash_soda, | |
mash_soda * mash_gallons, mash_soda * mash_gallons / 4.4; | |
printf "Pickling lime (Ca(OH)2): %f g/gal (%f g, ~ %f tsp)\n", mash_lime, | |
mash_lime * mash_gallons, mash_lime * mash_gallons / 4.4; | |
printf "Chalk (CaCO3): %f g/gal (%f g, ~ %f tsp)\n", mash_chalk, | |
mash_chalk * mash_gallons, mash_chalk * mash_gallons / 4.4; | |
printf "---- Acid Additions ----\n"; | |
printf "Lactic Acid: %f ml/gal (%f ml)\n", mash_lactic, | |
mash_lactic * mash_gallons; | |
printf "Phosphoric Acid: %f ml/gal (%f ml)\n", mash_phosphoric, | |
mash_phosphoric * mash_gallons; | |
printf "Sauermalz: %f oz/gal (%f oz)\n", mash_sauermalz, | |
mash_sauermalz * mash_gallons; | |
printf "\n# Sparge Water Additions (g/gal) (tsp / %.2f gal) #\n", sparge_gallons; | |
printf "------------------------------------------\n"; | |
printf "---- Mineral Additions ----\n"; | |
printf "Gypsum (CaSO4): %f g/gal (%f g, ~ %f tsp)\n", sparge_gypsum, | |
sparge_gypsum * sparge_gallons, sparge_gypsum * sparge_gallons / 4.0; | |
printf "Epsom Salt (MgSO4): %f g/gal (%f g, ~ %f tsp)\n", sparge_epsom, | |
sparge_epsom * sparge_gallons, sparge_epsom * sparge_gallons / 4.5; | |
printf "Table Salt (NaCl): %f g/gal (%f g, ~ %f tsp)\n", sparge_table, | |
sparge_table * sparge_gallons, sparge_table * sparge_gallons / 5.7; | |
printf "Calcium Chloride (CaCl2): %f g/gal (%f g, ~ %f tsp)\n", sparge_cacl2, | |
sparge_cacl2 * sparge_gallons, sparge_cacl2 * sparge_gallons / 3.4; | |
printf "Magnesium Chloride (MgCl2): %f g/gal (%f g, ~ %f tsp)\n", sparge_mgcl2, | |
sparge_mgcl2 * sparge_gallons, sparge_mgcl2 * sparge_gallons / 3.2; | |
printf "\n"; | |
printf "Mash sodium: %f\n", mash_sodium; | |
printf "Sparge sodium: %f\n", sparge_sodium; | |
data; | |
param mash_gallons := 5.5; | |
param sparge_gallons := 0.0; | |
/***** Initial Profiles *****/ | |
/* Cambride Water Report, 2013, merged w/ HBT thread from 2012 */ | |
param initial_calcium := 20.8; | |
param initial_magnesium := 4.0; | |
param initial_sodium := 79.0; | |
param initial_sulfate := 28.0; | |
param initial_chloride := 120.0; | |
param initial_bicarbonate := 60.5; | |
/***** Target Profiles *****/ | |
/* Bru'n Water "Yellow Malty" */ | |
param target_calcium := 50.0; | |
param target_magnesium := 5.0; | |
param target_sodium := 5; | |
param target_sulfate := 55.0; | |
/* param target_sulfate := 94.3; */ | |
param target_chloride := 70.0; | |
param target_bicarbonate := 0.0; | |
param target_residual_alkalinity := -95; | |
/***** Salt Limits (g / gal)*****/ | |
param max_gypsum := 1000; | |
param max_epsom := 000; | |
param max_table := 1000; | |
param max_cacl2 := 1000; | |
param max_mgcl2 := 1000; | |
param max_soda := 1000; | |
param max_lime := 1000; | |
param max_chalk := 1000; | |
param max_lactic := 000; | |
param max_phosphoric := 1000; | |
param max_sauermalz := 1000; | |
end; | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment