This is the guide for a water calculator: what salts to add to a tap water, and what pH the mash will land on with a given grist. It's the biggest area in the package, with about 150 exports, but four functions cover most of what you'd build.
When the recipe has a water section, calculateRecipe runs the last
two for you and returns the results in water, along with the lactic acid it
would take to reach the target pH.
A moderately hard tap water, treated for a pale ale, then the pH of the README's pale ale grist in it:
// A moderately hard tap water, as a US utility report gives it.
const tap = waterProfile({
name: 'Tap',
calciumPpm: 38,
magnesiumPpm: 12,
sodiumPpm: 25,
chloridePpm: 30,
sulfatePpm: 35,
alkalinityPpmAsCaCO3: 110,
ph: 7.8,
});
// At least 75 ppm calcium and twice as much sulfate as chloride.
const treatment = solveWaterRule({
source: tap,
rule: { minCalciumPpm: 75, sulfateChlorideRatio: 2 },
volumes: { mashVolumeL: 14.7, spargeVolumeL: 15.4 },
});
const mash = predictMashPh({
grist: paleAle.fermentables,
water: treatment.treated.mash,
mashVolumeL: 14.7,
});
waterProfile takes the report the way it's printed. US utilities give alkalinity as CaCO₃, European ones give bicarbonate, and German ones often give carbonate hardness in °dH, and it accepts any of the three. Here 110 ppm as CaCO₃ becomes 134 ppm of bicarbonate.
The rule wants 3.4 g of gypsum and 1.2 g of calcium chloride dihydrate across
the 30 L of brewing water, split between mash and sparge by volume. That lands
calcium at 75.2 ppm and the ratio at 1.99, and treatment.rule says both
conditions were met.
The mash comes out at pH 5.67. The grist alone in distilled water would sit
at 5.63, and the water's alkalinity pushes it up more than the calcium and
magnesium pull it down. mash.warnings says so: "Predicted mash pH 5.67 is
above 5.6: expect harsh tannin extraction, dull bitterness and hazy beer."
The fix is acid, and solveAdditionsForTargetPh sizes it:
const lactic = solveAdditionsForTargetPh({
targetPh: 5.4,
agent: { kind: 'acid', acid: 'lactic' },
grist: paleAle.fermentables,
water: treatment.treated.mash,
mashVolumeL: 14.7,
});
That's 5.2 mL of 88 % lactic acid for pH 5.40. The solver re-runs
predictMashPh at every step, so feeding the dose back in gives the target.
mash.explain names the model (charge-balance-troester, Kai Troester's
charge balance) and lists every term: the grist's buffering, the alkalinity the
mash actually titrated, the calcium and magnesium reactions, and any acid.
mash.contributions has the same terms in mEq, which makes a good bar chart.
One of the notes is worth passing on to a brewer. Only 82 % of this water's
measured alkalinity counts, because at mash pH the rest is still bicarbonate
and consumes no acid. MashPhOptions can switch that off if you want
to match a calculator that counts all of it.
SaltAddition.grams is what you
put on the scale, not grams per liter. The target field says which water it
goes in: mash, sparge, or total (the default, split by volume).acidAdditions. That way predictMashPh counts them once.The water chemistry doesn't have its own method page yet. The comments on predictMashPh, solveWaterProfile and solveWaterRule explain the models in full, with their sources. In the reference, the Water and mash pH category has the rest: acid strengths, residual alkalinity, kettle pH, and the ion and salt tables.