chriscrick 
12032011 05:28 AM 
Hit your gravity targets!
Most brewing software out there calculates grain bills based on a brewhouse efficiency provided by the user. The problem is, one's efficiency is totally dependent on the actual grain bill. Especially for BIABers, nospargers and batchspargers, the more grain, the lower the efficiency, so the more grain needed. Figuring out exactly how much is a pain, but it _is_ straightforward.
I present beer_calc, a Python program that will figure out exactly how much of each kind of grain you need in order to hit your target gravity. Give it a gravity target, a list of malts, their proportions and sugar content, and the program will give you:
How many pounds of each grain are needed.
How much mash water is needed.
How much sparge water to use (per sparge, if any).
How thick your mash will be.
If you have Python installed and can run a program from the command line, just paste the following into a text file. Change the defaults and constants in the file to suit your own system.
Let me know if this is useful to you!
Thanks.
Chris
beer_calc.py:
Code:
#!/usr/bin/python
#Copyright 2011 Christopher Crick
#Licensed under GPLv3
#Let me know if this is useful, or if I can help solve problems:
#chriscrick@gmail.com
#goal_gravity: the specific gravity you wish to reach (in points)
#fermentables: [(pct, ppg)] pct is the percentage of the grain bill,
#ppg is the points per pound per gallon
#sparges: the number of batch sparges you will do (optional, defaults
#to 0)
#boil_time: the number of minutes you will boil the wort (optional,
#defaults to 90)
#post_boil volume: the number of gallons you want to be left with at
#the end of the boil (optional, defaults to 6)
#Example: You are trying to end up with five gallons, and want six
#gallons after the boil to account for transfer and fermentation
#losses. You have worked out the following recipe: 9 lbs US 2row
#pale malt (38 ppg), 8 oz Munich malt (35 ppg), 8 oz 80L crystal malt
#(34 ppg) and 4 oz Belgian Caramunich malt (33 ppg). You intend an
#original gravity of 1.054. With some East Kent Goldings and
#Willamette hops, this makes one of my favorite pale ales.
#./beer_calc.py 54 "[(9,38),(.5,35),(.5,34),(.25,33)]"
#Malt 1 : 9.67332793445 lbs
#Malt 2 : 0.53740710747 lbs
#Malt 3 : 0.53740710747 lbs
#Malt 4 : 0.268703553735 lbs
#Total grain: 11.0168457031 lbs
#Mash water : 9.57116699219 gals
#Sparge water (each batch): 7.5 gals
#Mash thickness (qts/lb): 3.47510249307
#This mash is plenty thin, so we can easily do a batch sparge to save
#a pound and a half of grain:
#./beer_calc.py 54 "[(9, 38),(.5,35),(.5,34),(.25,33)]" 1
#Malt 1 : 8.48358898628 lbs
#Malt 2 : 0.471310499238 lbs
#Malt 3 : 0.471310499238 lbs
#Malt 4 : 0.235655249619 lbs
#Total grain: 9.66186523438 lbs
#Mash water: 5.56643066406 gals
#Sparge water (each batch): 3.75 gals
#Mash thickness (qts/lb): 2.30449526216
#Or even two sparges, though the extra effort only saves us another
#half pound total.
import sys
if len(sys.argv) < 3 or len(sys.argv) > 6:
print "Usage: beer_calc.py <goal_gravity> <fermentables> [sparges] [boil_time] [post_boil_volume]"
sys.exit(1)
goal_gravity = float(sys.argv[1])
fermentables = eval(sys.argv[2])
# These default values can be changed as desired
if len(sys.argv) > 3:
sparges = int(sys.argv[3])
else:
sparges = 0
if len(sys.argv) > 4:
boil_time = float(sys.argv[4])
else:
boil_time = 90.0
if len(sys.argv) > 5:
post_boil_volume = float(sys.argv[5])
else:
post_boil_volume = 6.0
# Change these constants to reflect your particular system. Once set,
# they should be permanent.
absorption_ratio = 0.188 # Grain absorption ratio, in lbs/gallon
deadspace = 0.05 # Gallons of liquid left in mash tun after draining
boiloff_rate = 1.0 # Rate of evaporation at full boil, in gallons/hour
conversion_efficiency = 100.0 # The percentage of starches your mash
# converts to sugars. It should be close
# to 100 if you are doing everything
# right
boil_volume = post_boil_volume + boil_time*boiloff_rate/60.0
# Normalization to allow fermentables to be listed in arbitrary units.
grain_bill = 0.0
for (pct, ppg) in fermentables:
grain_bill += pct
for i in range(len(fermentables)):
(pct, ppg) = fermentables[i]
fermentables[i] = (pct/grain_bill, ppg)
# Binary search to zero in on proper lbs
low_est = 0
high_est = boil_volume * 10 # Gratuitously larger than any possible
# grain bill
while (high_est  low_est) > 0.01:
test = low_est + ((high_est  low_est) / 2.0)
total_ppg = 0
for (pct, ppg) in fermentables:
total_ppg += pct*ppg
points_remaining = total_ppg*test*conversion_efficiency*0.01
absorption = test*absorption_ratio + deadspace
mash_size = boil_volume/(sparges+1)
gravity = 0.0
for count in range(sparges+1):
sugar_transfer = points_remaining*mash_size/(mash_size+absorption)
gravity += sugar_transfer
points_remaining = sugar_transfer
gravity /= post_boil_volume
if gravity < goal_gravity:
low_est = test
else:
high_est = test
counter = 1
for (pct, ppg) in fermentables:
print "Malt", counter, ":", (pct*test), "lbs"
counter += 1
print "Total grain:", test, "lbs"
print "Mash water:", (mash_size + absorption), "gals"
print "Sparge water (each batch):", mash_size, "gals"
print "Mash thickness (qts/lb):", (mash_size + absorption)*4.0/test
