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

ccbrew.py
Python
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

test_ccbrew.py
Python
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.

Share

 Leave a Reply

(required)

(required)

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>

 
Your Ad Here