From 4b23fb941bd2965ddee7404fbf9393451592527b Mon Sep 17 00:00:00 2001 From: Chris Gilmer Date: Wed, 5 Oct 2016 09:33:25 -0700 Subject: [PATCH] Complete hop addition calculator. Create tests in SI units. Change __eq__ for Grain and Hop additions to look at the overall difference between weights and make them equal if below a half percent tolerance --- brew/constants.py | 3 +++ brew/grains.py | 3 ++- brew/hops.py | 3 ++- brew/recipes.py | 21 +++++++++++++++++++-- tests/test_recipes_imperial.py | 26 ++++++++++++++++++++------ tests/test_recipes_si.py | 25 +++++++++++++++++++++++++ 6 files changed, 71 insertions(+), 10 deletions(-) diff --git a/brew/constants.py b/brew/constants.py index 23f22d5..e39e2a8 100644 --- a/brew/constants.py +++ b/brew/constants.py @@ -146,3 +146,6 @@ DENSITY_ETHANOL = 0.78945 # g/ml @ 20C #: Alcohol by Volume Constant ABV_CONST = RATIO_C2H6O_TO_CO2 / ALCOHOL_SPECIFIC_GRAVITY * 100.0 + +#: Weight Tolerance, considered equal within this range +WEIGHT_TOLERANCE = 0.005 diff --git a/brew/grains.py b/brew/grains.py index c268f18..3a367b5 100644 --- a/brew/grains.py +++ b/brew/grains.py @@ -12,6 +12,7 @@ from .constants import POUND_PER_KG from .constants import SI_TYPES from .constants import SI_UNITS +from .constants import WEIGHT_TOLERANCE from .utilities.malt import dry_to_liquid_malt_weight from .utilities.malt import dry_malt_to_grain_weight from .utilities.malt import grain_to_dry_malt_weight @@ -187,7 +188,7 @@ def __repr__(self): def __eq__(self, other): if not isinstance(other, self.__class__): return False - if (round(self.weight, 2) == round(other.weight, 2)) and \ + if (abs(1.0 - self.weight / other.weight) < WEIGHT_TOLERANCE) and \ (self.grain_type == other.grain_type) and \ (self.units == other.units) and \ (self.grain == other.grain): diff --git a/brew/hops.py b/brew/hops.py index cc09688..bc80ed2 100644 --- a/brew/hops.py +++ b/brew/hops.py @@ -10,6 +10,7 @@ from .constants import OZ_PER_MG from .constants import SI_TYPES from .constants import SI_UNITS +from .constants import WEIGHT_TOLERANCE from .utilities.hops import HopsUtilizationGlennTinseth from .validators import validate_hop_type from .validators import validate_optional_fields @@ -166,7 +167,7 @@ def __eq__(self, other): return False if (self.hop == other.hop) and \ (self.boil_time == other.boil_time) and \ - (round(self.weight, 2) == round(other.weight, 2)) and \ + (abs(1.0 - self.weight / other.weight) < WEIGHT_TOLERANCE) and \ (self.hop_type == other.hop_type) and \ (self.units == other.units): return True diff --git a/brew/recipes.py b/brew/recipes.py index 7b28a1c..bb8a8b0 100644 --- a/brew/recipes.py +++ b/brew/recipes.py @@ -850,7 +850,8 @@ def get_grain_additions(self, percent_list): grain_additions.append(grain_add) return grain_additions - def get_hop_additions(self, boil_time_list, + def get_hop_additions(self, percent_list, boil_time_list, + hop_type=HOP_TYPE_PELLET, utilization_cls=HopsUtilizationGlennTinseth): """ Calculate HopAdditions from list of boil times @@ -859,8 +860,18 @@ def get_hop_additions(self, boil_time_list, :param HopsUtilization utilization_cls: The utilization class used for calculation :return: A list of Hop Additions :rtype: list(HopAddition) + :raises Exception: If sum of percentages does not equal 1.0 + :raises Exception: If length of percent_list does not match length of self.grain_list :raises Exception: If length of boil_time_list does not match length of self.hop_list """ # nopep8 + for percent in percent_list: + validate_percentage(percent) + + if sum(percent_list) != 1.0: + raise Exception("Percentages must sum to 1.0") + + if len(percent_list) != len(self.grain_list): + raise Exception("The length of percent_list must equal length of self.grain_list") # nopep8 if len(boil_time_list) != len(self.hop_list): raise Exception("The length of boil_time_list must equal length of self.hop_list") # nopep8 @@ -871,16 +882,22 @@ def get_hop_additions(self, boil_time_list, hop_additions = [] for index, hop in enumerate(self.hop_list): + percent = percent_list[index] boil_time = boil_time_list[index] + + # Calculate utilization from boil gravity bg = gu_to_sg(sg_to_gu(self.original_gravity) * self.final_volume / self.start_volume) # nopep8 utilization = utilization_cls.get_percent_utilization(bg, boil_time) # nopep8 + if hop_type == HOP_TYPE_PELLET: + utilization *= HOP_UTILIZATION_SCALE_PELLET - num = (self.target_ibu * self.final_volume) + num = (self.target_ibu * percent * self.final_volume) den = (utilization * hop.percent_alpha_acids * hops_constant) weight = num / den hop_add = HopAddition(hop, weight=weight, boil_time=boil_time, + hop_type=hop_type, utilization_cls=utilization_cls, units=self.units) hop_additions.append(hop_add) diff --git a/tests/test_recipes_imperial.py b/tests/test_recipes_imperial.py index c78144d..db49e44 100644 --- a/tests/test_recipes_imperial.py +++ b/tests/test_recipes_imperial.py @@ -336,23 +336,37 @@ def test_get_grain_additions(self): expected = grain_additions self.assertEquals(out, expected) - def test_get_grain_additions_raises_sum_invalid(self): + def test_get_grain_additions_raises_percent_sum_invalid(self): percent_list = [0.90, 0.05] with self.assertRaises(Exception): self.builder.get_grain_additions(percent_list) - def test_get_grain_additions_raises_length_mismatch(self): + def test_get_grain_additions_raises_percent_length_mismatch(self): percent_list = [0.90, 0.05, 0.05] with self.assertRaises(Exception): self.builder.get_grain_additions(percent_list) def test_get_hop_additions(self): + percent_list = [0.8827, 0.1173] boil_time_list = [60.0, 5.0] - out = self.builder.get_hop_additions(boil_time_list) + out = self.builder.get_hop_additions(percent_list, boil_time_list) expected = hop_additions - # self.assertEquals(out, expected) + self.assertEquals(out, expected) + + def test_get_hop_additions_raises_percent_sum_invalid(self): + percent_list = [0.8827, 0.2173] + boil_time_list = [60.0, 5.0] + with self.assertRaises(Exception): + self.builder.get_hop_additions(percent_list, boil_time_list) + + def test_get_hop_additions_raises_percent_length_mismatch(self): + percent_list = [0.8827, 0.0173, 0.10] + boil_time_list = [60.0, 5.0] + with self.assertRaises(Exception): + self.builder.get_hop_additions(percent_list, boil_time_list) - def test_get_hop_additions_raises_length_mismatch(self): + def test_get_hop_additions_raises_boil_time_length_mismatch(self): + percent_list = [0.8827, 0.1173] boil_time_list = [60.0, 5.0, 5.0] with self.assertRaises(Exception): - self.builder.get_hop_additions(boil_time_list) + self.builder.get_hop_additions(percent_list, boil_time_list) diff --git a/tests/test_recipes_si.py b/tests/test_recipes_si.py index 642b5f9..4c5146d 100644 --- a/tests/test_recipes_si.py +++ b/tests/test_recipes_si.py @@ -356,3 +356,28 @@ def test_get_grain_additions_raises_length_mismatch(self): percent_list = [0.90, 0.05, 0.05] with self.assertRaises(Exception): self.builder.get_grain_additions(percent_list) + + def test_get_hop_additions(self): + percent_list = [0.8827, 0.1173] + boil_time_list = [60.0, 5.0] + out = self.builder.get_hop_additions(percent_list, boil_time_list) + expected = [ha.change_units() for ha in hop_additions] + self.assertEquals(out, expected) + + def test_get_hop_additions_raises_percent_sum_invalid(self): + percent_list = [0.8827, 0.2173] + boil_time_list = [60.0, 5.0] + with self.assertRaises(Exception): + self.builder.get_hop_additions(percent_list, boil_time_list) + + def test_get_hop_additions_raises_percent_length_mismatch(self): + percent_list = [0.8827, 0.0173, 0.10] + boil_time_list = [60.0, 5.0] + with self.assertRaises(Exception): + self.builder.get_hop_additions(percent_list, boil_time_list) + + def test_get_hop_additions_raises_boil_time_length_mismatch(self): + percent_list = [0.8827, 0.1173] + boil_time_list = [60.0, 5.0, 5.0] + with self.assertRaises(Exception): + self.builder.get_hop_additions(percent_list, boil_time_list)