This is the guide for a recipe builder, a brew-day sheet, or anything else that holds a whole recipe and wants all its numbers at once. It also covers the gravity readings you take along the way.
calculateRecipe returned and reports problems in the
order you'd hit them on the day: kettle, mash, fermenter, pH, pitch,
fermentation temperature, style.Start here ran the README's pale ale on the default equipment. Here it is on a 25 L kettle with a 23 L fermenter:
const equipment: EquipmentProfileInput = {
name: '25 L kettle',
kettleVolumeL: 25,
fermenterVolumeL: 23,
};
const calculation = calculateRecipe(paleAle, equipment);
const { findings, supersedes } = recipeChecks({ recipe: paleAle, equipment, calculation });
const summary = findings.map((finding) => `${finding.severity}: ${finding.id}`);
The pale ale needs 25.2 L in the kettle when the boil starts, so findings
has two entries:
warning with id boil-over-kettle, saying the boil overflows the kettle
by 0.19 L;note with id fermenter-headspace. 20 L in a 23 L fermenter leaves 3 L
of headspace, and the check would like a quarter of the fermenter, 5.75 L.Each finding is a typed object with its numbers on it, discriminated on id,
so you write the sentence yourself in whatever words and units your app uses.
calculateRecipe also put "Pre-boil volume 25.2 L exceeds the kettle's 25 L."
in its own warnings. supersedes returns that exact string, so if you show
both lists you can drop the duplicate.
Two of the checks need something extra. The pitch check needs now, the date
to judge the yeast's age against, because a function that read the clock
would give different answers on different days. The style check needs
styleBands, the OG, FG, IBU and SRM ranges of the style you're aiming at.
Leave either out and that check is skipped.
result.explain has an Explanation for each headline number. For the
pale ale, explain.og says the formula is extract-bookkeeping, shows the
expression, and lists its inputs: 21.3 L post-boil, 75 % mash efficiency,
1,081.9 point-liters of extract out of a theoretical 1,442.5. Its source is
Palmer's How to Brew.
CalculationOptions picks the formula where there's a choice. The same recipe under Rager instead of Tinseth:
const tinseth = calculation.vitals.ibu;
const rager = calculateRecipe(paleAle, equipment, { ibuFormula: 'rager' }).vitals.ibu;
Tinseth gives 33.0 IBU and Rager 37.7. The two are fits to different data and a gap that size is normal between them, which is why the explanation always says which one ran.
A hydrometer sample at 32 °C that reads 1.048, and refractometer readings of 12.4 °Bx before pitching and 6.6 °Bx at the end:
// The hydrometer read 1.048 in a sample still at 32 °C.
const og = correctHydrometerReading(1.048, 32);
// The refractometer read 12.4 °Bx before pitching and 6.6 °Bx at the end.
const fg = refractometerFinalGravityRaw(12.4, 6.6);
const strength = abv(og, fg);
The warm sample is really 1.052. The refractometer's final reading works out to 1.011, and the beer is 5.5 % ABV.
batchVolumeL means chilled wort
going into the fermenter unless you set batchVolumeBasis: 'packaged'.
Everything upstream (strike, sparge, pre-boil) is worked back from it.amountKg: 0.028. It's the
most common mistake, and the IBU comes out a thousand times too high.extractFineGrindDryBasisPercent (the maltster's figure) or ppg. If you
pass both, ppg wins, on the reasoning that it's the number someone typed
in to override the catalog.vitals.og is what a hydrometer reads on the wort and
vitals.ogEquivalent includes the sugar. ABV uses the second.