It’s Thanksgiving weekend in the U.S., and that can only mean one thing: it’s time to start homebrewing again. The temperature’s right for fermenting and there’s no better weather for hanging out in the backyard, drinking a beer, and watching 6 gallons of amber liquid boil for an hour or so.
Of course, that leaves plenty of time to whip up some Python scripts for homebrewers (well, homebrewers that dig on Python, anyway). Armed with a netbook and 3 Victory Storm King stouts:
ccbrew.py
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 |
#!/usr/bin/env python
class recipe():
"""Define ingredients and instructions for a homebrew beer recipe and
calculate characteristics such as gravity, bitterness, color, etc.
Instantiate like this:
r = ccbrew.recipe("American Pale Ale", "American Pale Ale", 5.5, 6.5, 0.8)
Or like this:
r = ccbrew.recipe("American Pale Ale", "American Pale Ale")
r.batch_size = 5.5
r.boil_volume = 6.5
r.extract_efficiency = 0.8
Add ingredients like this:
r.add_fermentable(ccbrew.fermentable("American 2 Row", 36, 1.7, 11))
r.add_hop(ccbrew.hop("Perle", 1.0, 7.9, 60, "whole"))
etc.
"""
def __init__(self, name, style, batch_size = None, boil_gallons = None, \
extract_efficiency = None):
self.name = name
self.style = style
self.batch_size = batch_size
self.boil_gallons = boil_gallons
self.extract_efficiency = extract_efficiency
self._fermentables = []
self._hops = []
self._yeast = []
@property
def fermentables(self):
"""Returns a list of the recipe's fermentable objects."""
return self._fermentables
@property
def hops(self):
"""Returns a list of the recipe's hop objects."""
return self._hops
def add_fermentable(self, f):
"""Append a fermentable object to the recipe's list of fermentable
objects."""
self._fermentables.append(f)
def add_hop(self, h):
"""Append a hop object to the recipe's list of hop objects."""
self._hops.append(h)
def fermentables_lbs(self):
"""Returns the total weight of the recipe's fermentables in lbs."""
ret = 0.0
for f in self.fermentables:
ret += f.lbs
return ret
def fermentable_yield_contrib(self, f):
"""Returns the gravity points yielded by a given fermentable in a
format such as 52.8."""
return (f.potential * f.lbs / self.boil_gallons) * self.extract_efficiency
def fermentables_yield(self):
"""Returns the total gravity units yielded by the recipe's fermentables
in a format such as 1.059."""
ret = 0.0
for f in self.fermentables:
ret += self.fermentable_yield_contrib(f)
return (ret / 1000) + 1
def hop_ibu_contrib(self, h):
"""Returns IBU contribution of a given hop addition in a format such
as 23.66978."""
return (h.oz * self.get_hop_util(h) * (h.alpha/100) * 7489) \
/ (self.boil_gallons * self.get_gravity_correction())
def get_hop_util(self, h):
"""Returns the utilization percentage of a given hop addition (based
on Ray Daniels' 'Designing Great Beers', p80, Table 9.3)."""
ret = 0.0
if h.form == "whole" or h.form == "w":
if h.boil_mins >= 0 and h.boil_mins < = 9:
ret = 0.05
elif h.boil_mins >= 10 and h.boil_mins < = 19:
ret = 0.12
elif h.boil_mins >= 20 and h.boil_mins < = 29:
ret = 0.15
elif h.boil_mins >= 30 and h.boil_mins < = 44:
ret = 0.19
elif h.boil_mins >= 35 and h.boil_mins < = 59:
ret = 0.22
elif h.boil_mins >= 60 and h.boil_mins < = 74:
ret = 0.24
elif h.boil_mins >= 75:
ret = 0.27
elif h.form == "pellet" or h.form == "p":
if h.boil_mins >= 0 and h.boil_mins < = 9:
ret = 0.06
elif h.boil_mins >= 10 and h.boil_mins < = 19:
ret = 0.15
elif h.boil_mins >= 20 and h.boil_mins < = 29:
ret = 0.19
elif h.boil_mins >= 30 and h.boil_mins < = 44:
ret = 0.24
elif h.boil_mins >= 35 and h.boil_mins < = 59:
ret = 0.27
elif h.boil_mins >= 60 and h.boil_mins < = 74:
ret = 0.30
elif h.boil_mins >= 75:
ret = 0.34
return ret
def get_gravity_correction(self):
"""Returns correction factor for worts above 1.050 for purposes of
calculating IBU contribution."""
return 1 + ((self.fermentables_yield() - 1.050)) / 0.2
def ibu(self):
"""Returns the total IBU of the recipe."""
ret = 0.0
for h in self.hops:
ret += self.hop_ibu_contrib(h)
return ret
def color(self):
"""Returns the color of the beer in Lovibond points."""
pass
class fermentable():
"""Define a fermentable in the recipe.
Instantiate like this:
f = ccbrew.fermentable("American 2 Row", 36, 1.7, 11)
Or like this:
f = ccbrew.fermentable("American 2 Row")
f.potential = 36
f.color = 1.7
f.lbs = 11
"""
def __init__(self, name, potential = None, color = None, lbs = None):
self.name = name
self.potential = potential
self.color = color
self.lbs = lbs
class hop():
"""Define a hop (more exactly: a hop addition) in the recipe. 'form' can be
'pellet' or 'p' for pellet hops, or 'whole' or 'w' for whole hops.
Instantiate like this:
h = ccbrew.hop("East Kent Golding", 3.0, 5.0, 60, "pellet")
Or like this:
h = ccbrew.hop("East Kent Golding")
h.oz = 3.0
h.alpha = 5.0
h.boil mins = 60
h.form = "pellet"
"""
def __init__(self, name, oz = None, alpha = None, boil_mins = None, \
form = "whole"):
self.name = name
self.oz = oz
self.alpha = alpha
self.boil_mins = boil_mins
self.form = form
class yeast(): pass |
And all good boys write unit tests, right? (OK, OK … this is more like a glorified
main() intermingled with some unittest assertions.)
test_ccbrew.py
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 |
#!/usr/bin/env python
import ccbrew
import unittest
class recipe_tests(unittest.TestCase):
def test_pale_ale(self):
"""Load up a classic American Pale Ale recipe"""
r = ccbrew.recipe("American Pale Ale", "American Pale Ale", 5.5, 6.5, 0.8)
r.add_fermentable(ccbrew.fermentable("American 2 Row", 36, 1.7, 11))
r.add_fermentable(ccbrew.fermentable("Cara-Pils", 34, 1.7, 0.75))
r.add_fermentable(ccbrew.fermentable("Crystal 60", 32, 60, 0.75))
r.add_hop(ccbrew.hop("Perle", 1.0, 7.9, 60, "whole"))
r.add_hop(ccbrew.hop("Cascade", 0.5, 5.6, 45, "whole"))
r.add_hop(ccbrew.hop("Cascade", 0.5, 5.6, 30, "whole"))
r.add_hop(ccbrew.hop("Cascade", 0.5, 5.6, 15, "whole"))
r.add_hop(ccbrew.hop("Cascade", 0.5, 5.6, 2, "whole"))
self.assertEqual(r.name, "American Pale Ale")
self.assertEqual(r.style, "American Pale Ale")
self.assertEqual(r.fermentables_lbs(), 12.5)
self.assertAlmostEqual(r.fermentables_yield(), 1.05483076)
self.assertAlmostEqual(r.ibu(), 39.5993390)
def test_old_ale(self):
"""Load up the house Calebs Creek Old Ale recipe"""
r = ccbrew.recipe("Calebs Creek Old Ale", "Old Ale", 5.5, 6.5, 0.8)
r.add_fermentable(ccbrew.fermentable("American 2 Row", 36, 1.7, 15))
r.add_fermentable(ccbrew.fermentable("Victory", 35, 5, 0.75))
r.add_fermentable(ccbrew.fermentable("Crystal 40", 33, 40, 1.0))
r.add_hop(ccbrew.hop("East Kent Golding", 3.0, 5.0, 60, "pellet"))
r.add_hop(ccbrew.hop("Fuggle", 1.0, 5.0, 15, "pellet"))
self.assertEqual(r.name, "Calebs Creek Old Ale")
self.assertEqual(r.style, "Old Ale")
self.assertEqual(r.fermentables_lbs(), 16.75)
self.assertAlmostEqual(r.fermentables_yield(), 1.0737538)
self.assertAlmostEqual(r.ibu(), 54.0666254)
suite = unittest.TestLoader().loadTestsFromTestCase(recipe_tests)
unittest.TextTestRunner(verbosity=2).run(suite) |
TODO
Pretty much everything relevant to water chemistry, mash schedules, gravity, color, and much, much more. I only had an hour or so. Relax.



