HOP_TYPE_PELLET = u"pellet" HOP_TYPE_PLUG = u"plug" HOP_TYPE_WHOLE = u"whole" HOP_TYPE_WHOLE_WET = u"whole wet" HOP_UTILIZATION_SCALE_PELLET = 1.1  # 110% HOP_WHOLE_DRY_TO_WET = 5.5 IMPERIAL_UNITS = u"imperial" SI_UNITS = u"metric" class HopAddition(object):
    """
    A representation of the Hop as added to a Recipe.
    """

    def __init__(
        self,
        hop,
        weight=None,
        boil_time=None,
        hop_type=HOP_TYPE_PELLET,
        utilization_cls=HopsUtilizationGlennTinseth,
        utilization_cls_kwargs=None,
        units=IMPERIAL_UNITS,
    ):
        """
        :param Hop hop: The Hop object
        :param float weight: The weight of the hop addition
        :param float boil_time: The amount of time the hop is boiled
        :param float hop_type: The type of the hop being used
        :param HopsUtilization utilization_cls: The utilization class used for calculation
        :param dict utilization_cls_kwargs: The kwargs to initialize the utilization_cls object
        :param str units: The units
        """  # noqa
        self.hop = hop
        self.weight = weight
        self.boil_time = boil_time
        self.hop_type = validate_hop_type(hop_type)
        self.utilization_cls_kwargs = utilization_cls_kwargs or {}
        self.utilization_cls = utilization_cls(
            self, **self.utilization_cls_kwargs
        )  # noqa

        # Manage units
        self.set_units(units)

    def set_units(self, units):
        """
        Set the units and unit types

        :param str units: The units
        """
        self.units = validate_units(units)
        if self.units == IMPERIAL_UNITS:
            self.types = IMPERIAL_TYPES
        elif self.units == SI_UNITS:
            self.types = SI_TYPES

    def change_units(self):
        """
        Change units of the class from one type to the other

        :return: Hop Addition in new unit type
        :rtype: HopAddition
        """
        if self.units == IMPERIAL_UNITS:
            weight = self.weight * MG_PER_OZ
            units = SI_UNITS
        elif self.units == SI_UNITS:
            weight = self.weight * OZ_PER_MG
            units = IMPERIAL_UNITS
        return HopAddition(
            self.hop,
            weight=weight,
            boil_time=self.boil_time,
            utilization_cls_kwargs={u"units": units},
            units=units,
        )

    def __str__(self):
        if sys.version_info[0] >= 3:
            return self.__unicode__()
        else:
            return self.__unicode__().encode(u"utf8")

    def __unicode__(self):
        return u"{hop}, {weight} {weight_small}, {boil_time} min, {hop_type}".format(  # noqa
            hop=self.hop,
            weight=self.weight,
            boil_time=self.boil_time,
            hop_type=self.hop_type,
            **self.types
        )

    def __repr__(self):
        out = u"{0}({1}".format(type(self).__name__, repr(self.hop))
        if self.weight:
            out = u"{0}, weight={1}".format(out, self.weight)
        if self.boil_time:
            out = u"{0}, boil_time={1}".format(out, self.boil_time)
        if self.hop_type:
            out = u"{0}, hop_type='{1}'".format(out, self.hop_type)
        if self.utilization_cls:
            out = u"{0}, utilization_cls={1}".format(
                out, type(self.utilization_cls).__name__
            )  # noqa
        if self.utilization_cls_kwargs:
            out = u"{0}, utilization_cls_kwargs={1}".format(
                out, str(self.utilization_cls_kwargs)
            )  # noqa
        out = u"{0}, units='{1}'".format(out, self.units)
        out = u"{0})".format(out)
        return out

    def __eq__(self, other):
        if not isinstance(other, self.__class__):
            return False
        if (
            (self.hop == other.hop)
            and (self.boil_time == other.boil_time)
            and (abs(1.0 - self.weight / other.weight) < WEIGHT_TOLERANCE)
            and (self.hop_type == other.hop_type)
            and (self.units == other.units)
        ):
            return True
        return False

    def __ne__(self, other):
        return not self.__eq__(other)

    def to_dict(self):
        hop_data = self.hop.to_dict()
        return {
            u"name": hop_data.pop(u"name"),
            u"data": hop_data,
            u"weight": round(self.weight, 2),
            u"boil_time": round(self.boil_time, 1),
            u"hop_type": self.hop_type,
            u"utilization_cls": str(self.utilization_cls),
            u"utilization_cls_kwargs": self.utilization_cls_kwargs,
            u"units": self.units,
        }

    def to_json(self):
        return json.dumps(self.to_dict(), sort_keys=True)

    @classmethod
    def validate(cls, hop_data):
        required_fields = [(u"name", str), (u"weight", float), (u"boil_time", float)]
        optional_fields = [
            (u"percent_alpha_acids", float),
            (u"hop_type", str),
            (u"units", str),
        ]
        validate_required_fields(hop_data, required_fields)
        validate_optional_fields(hop_data, optional_fields)

    def format(self):
        kwargs = {}
        kwargs.update(self.to_dict())
        kwargs.update(self.types)
        msg = textwrap.dedent(
            u"""\
                {name} Addition
                -----------------------------------
                Hop Type:     {hop_type}
                AA %:         {data[percent_alpha_acids]:0.1%}
                Weight:       {weight:0.2f} {weight_small}
                Boil Time:    {boil_time:0.1f} min""".format(
                **kwargs
            )
        )
        return msg

    def get_ibus(self, sg, final_volume):
        """
        Get the IBUs

        :param float sg: The specific gravity of the wort
        :param float final_volume: The final volume of the wort
        :return: The IBUs of the wort
        :rtype: float
        """
        return self.utilization_cls.get_ibus(sg, final_volume)

    def get_alpha_acid_units(self):
        """
        Get Alpha Acid Units

        :return: alpha acid units
        :rtype: float
        """
        alpha_acids = self.hop.percent_alpha_acids * 100
        if self.units == IMPERIAL_UNITS:
            return self.weight * alpha_acids
        elif self.units == SI_UNITS:
            return self.weight * OZ_PER_MG * alpha_acids def hop_type_weight_conversion(weight, old_type, new_type):
    """
    Convert weight of hops between one type and another

    :param float weight: Weight of the hops
    :param str old_type: The old hop type to convert from
    :param str new_type: The new hop type to convert to
    :return: New weight of the hops
    :rtype: float
    """
    # If same type then no change
    if old_type == new_type:
        return weight

    # If hop types are equivalent then no change
    equivalent_types = [HOP_TYPE_PLUG, HOP_TYPE_WHOLE]
    if old_type in equivalent_types and new_type in equivalent_types:
        return weight

    conversion = 1.0
    if old_type == HOP_TYPE_WHOLE_WET:
        conversion /= HOP_WHOLE_DRY_TO_WET
    elif new_type == HOP_TYPE_WHOLE_WET:
        conversion *= HOP_WHOLE_DRY_TO_WET

    if old_type == HOP_TYPE_PELLET:
        conversion *= HOP_UTILIZATION_SCALE_PELLET
    elif new_type == HOP_TYPE_PELLET:
        conversion /= HOP_UTILIZATION_SCALE_PELLET

    return weight * conversion class HopsUtilization(object):
    """
    http://www.boondocks-brewing.com/hops
    """

    def __init__(self, hop_addition, units=IMPERIAL_UNITS):
        """
        :param HopAddition hop_addition: A hop addition
        :param str units: The units
        """
        self.hop_addition = hop_addition

        # Manage units
        self.set_units(units)

    def set_units(self, units):
        """
        Set the units and unit types

        :param str units: The units
        """
        self.units = validate_units(units)
        if self.units == IMPERIAL_UNITS:
            self.types = IMPERIAL_TYPES
        elif self.units == SI_UNITS:
            self.types = SI_TYPES

    def change_units(self):
        """
        Change units of the class from one type to the other

        :return: Hop Addition in new unit type
        :rtype: HopAddition
        """
        if self.units == IMPERIAL_UNITS:
            units = SI_UNITS
        elif self.units == SI_UNITS:
            units = IMPERIAL_UNITS
        return HopsUtilization(self.hop_addition, units=units)

    def get_ibus(self, sg, final_volume):
        """
        Get the IBUs

        :param float sg: Specific Gravity
        :param float final_volume: The Final Volume of the wort
        :return: The IBUs of the wort
        :rtype: float
        """
        hops_constant = HOPS_CONSTANT_IMPERIAL
        if self.units == SI_UNITS:
            hops_constant = HOPS_CONSTANT_SI
        utilization = self.get_percent_utilization(sg, self.hop_addition.boil_time)
        # Hop weight for wet is greater than dry
        hop_weight = self.hop_addition.weight
        if self.hop_addition.hop_type == HOP_TYPE_WHOLE_WET:
            hop_weight = hop_type_weight_conversion(
                hop_weight, self.hop_addition.hop_type, HOP_TYPE_WHOLE
            )
        # Utilization is 10% higher for pellet vs whole/plug
        if self.hop_addition.hop_type == HOP_TYPE_PELLET:
            utilization *= HOP_UTILIZATION_SCALE_PELLET
        num = (
            hop_weight
            * utilization
            * self.hop_addition.hop.percent_alpha_acids
            * hops_constant
        )
        return num / final_volume

    @classmethod
    def get_percent_utilization(cls, sg, boil_time):
        """
        Get the percent utilization

        :param float sg: Specific Gravity
        :param float boil_time: The Boil Time in minutes
        :raise NotImplementedError: This must be overridden
        """
        raise NotImplementedError

    @classmethod
    def get_utilization_table(cls, gravity_list, boil_time_list, sig=3):
        """
        Get the utilization table for the class

        :param list gravity_list: A list of specific gravities
        :param list boil_time_list: A list of boil times in minutes
        :param int sig: Significant figures to round
        :return: A table of utilization for specific gravity vs boil time
        :rtype: list
        """
        table = []
        for boil_time in boil_time_list:
            line = []
            for sg in gravity_list:
                aau = cls.get_percent_utilization(sg / 1000.0, boil_time)
                line.append(round(aau, sig))
            table.append(line)
        return table

    @classmethod
    def format_utilization_table(cls):
        """
        Percent Alpha Acid Utilization - Boil Time vs Wort Original Gravity

        :return: The formatted utilization table
        :rtype: str

        Source:

        * http://www.realbeer.com/hops/research.html
        """
        gravity_list = list(range(1030, 1140, 10))
        boil_time_list = list(range(0, 60, 3)) + list(range(60, 130, 10))
        table = cls.get_utilization_table(gravity_list, boil_time_list)

        title = (
            u"Percent Alpha Acid Utilization - " u"Boil Time vs Wort Original Gravity"
        )
        table_size = 92

        out = []
        out.append(title.center(table_size))
        out.append(str(u"=" * len(title)).center(table_size))
        out.append(u"\n")
        out.append(
            u" ".join(
                [u" " * 4] + [u"{0:7.3f}".format(l / 1000.0) for l in gravity_list]
            )
        )
        out.append(u"-" * table_size)
        for index, line in enumerate(table):
            boil_time = boil_time_list[index]
            out.append(
                u"{0} {1}".format(
                    str(boil_time).rjust(4),
                    u" ".join([u"{0:7.3f}".format(aau) for aau in line]),
                )
            )
        return u"\n".join([o.rstrip() for o in out if o != u"\n"]) class HopsUtilizationGlennTinseth(HopsUtilization):
    """
    Glenn Tinseth Hops Utilization Method

    Best for all grain brewing.
    """

    def __str__(self):
        return u"Glenn Tinseth"

    @classmethod
    def get_bigness_factor(cls, sg):
        """
        Get Bigness Factor

        :param float sg: Specific Gravity
        :return: Bigness Factor
        :rtype: float

        The Bigness factor accounts for reduced utilization due to higher wort
        gravities. Use an average gravity value for the entire boil to account
        for changes in the wort volume.

        :math:`\\text{Bigness factor} = 1.65 \\times 0.000125^{\\text{wort gravity} - 1}`
        """  # noqa
        return 1.65 * 0.000125 ** (sg - 1)

    @classmethod
    def get_boil_time_factor(cls, boil_time):
        """
        Get Boil Time Factor

        :param float boil_time: Boil Time in minutes
        :return: Boil Time Factor
        :rtype: float

        The Boil Time factor accounts for the change in utilization due to
        boil time:

        :math:`\\text{Boil Time factor} = \\frac{1 - e^{-0.04 \\times \\text{time in mins}}}{4.15}`
        """  # noqa
        return (1 - math.exp(-0.04 * boil_time)) / 4.15

    @classmethod
    def get_percent_utilization(cls, sg, boil_time):
        """
        Get the percent utilization

        :param float sg: Specific Gravity
        :param float boil_time: The Boil Time in minutes
        :return: The percent utilization
        :rtype: float

        Source:

        * http://www.realbeer.com/hops/research.html
        * http://www.rooftopbrew.net/ibu.php
        """
        bigness_factor = cls.get_bigness_factor(sg)
        boil_time_factor = cls.get_boil_time_factor(boil_time)
        return bigness_factor * boil_time_factor class HopsUtilizationJackieRager(HopsUtilization):
    """
    Jackie Rager Hops Utilization Method

    Best for extract and partial mash brewing.
    """

    def __str__(self):
        return u"Jackie Rager"

    @classmethod
    def get_c_gravity(cls, sg):
        """
        Get Cgravity

        :param float sg: Specific Gravity
        :return: Cgravity
        :rtype: float

        Cgravity is a constant to adjust the boil size when dealing with
        specific gravity greater than 1.050 in the calculation of IBUs.
        """
        cgravity = 1
        if sg > 1.050:
            cgravity += (sg - 1.050) / 0.2
        return cgravity

    @classmethod
    def get_percent_utilization(cls, sg, boil_time):
        """
        Get the percent utilization

        :param float sg: Specific Gravity
        :param float boil_time: The Boil Time in minutes
        :return: The percent utilization
        :rtype: float

        Source:

        * http://www.rooftopbrew.net/ibu.php
        """
        num = (18.11 + 13.86 * math.tanh((boil_time - 31.32) / 18.27)) / 100.0
        return num / cls.get_c_gravity(sg) def plato_to_sg(deg_plato):
    """
    Degrees Plato to Specific Gravity

    :param float deg_plato: Degrees Plato
    :return: Specific Gravity
    :rtype: float

    The simple formula for S.G. is:

    :math:`\\text{SG} = 1 + 0.004 \\times \\text{Plato}`

    The more precise calculation of SG is:

    :math:`\\text{SG} = \\frac{Plato}{258.6 - \\big(\\frac{Plato}{258.2} \\times 227.1\\big)} + 1`

    Source:

    * http://www.learntobrew.com/page/1mdhe/Shopping/Beer_Calculations.html
    """  # noqa
    return (deg_plato / (258.6 - ((deg_plato / 258.2) * 227.1))) + 1.0
import unittest
from brew.constants import HOP_TYPE_PELLET
from brew.constants import HOP_TYPE_PLUG
from brew.constants import HOP_TYPE_WHOLE
from brew.constants import HOP_TYPE_WHOLE_WET
from brew.constants import HOP_UTILIZATION_SCALE_PELLET
from brew.constants import HOP_WHOLE_DRY_TO_WET
from brew.constants import IMPERIAL_UNITS
from brew.constants import SI_UNITS
from brew.hops import HopAddition
from brew.utilities.hops import hop_type_weight_conversion
from brew.utilities.hops import HopsUtilization
from brew.utilities.hops import HopsUtilizationGlennTinseth
from brew.utilities.hops import HopsUtilizationJackieRager
from brew.utilities.sugar import plato_to_sg
from fixtures import centennial
# -*- coding: utf-8 -*-



class TestHopUtilities(unittest.TestCase):
    def test_hope_type_weight_conversion_same_type(self):
        weight = 1.0
        old_type = HOP_TYPE_PELLET
        new_type = HOP_TYPE_PELLET
        new_weight = hop_type_weight_conversion(weight, old_type, new_type)
        expected = 1.0
        self.assertEquals(new_weight, expected)

    def test_hope_type_weight_conversion_equivalent_types(self):
        weight = 1.0
        old_type = HOP_TYPE_PLUG
        new_type = HOP_TYPE_WHOLE
        new_weight = hop_type_weight_conversion(weight, old_type, new_type)
        expected = 1.0
        self.assertEquals(new_weight, expected)

    def test_hope_type_weight_conversion_whole_wet_to_dry(self):
        weight = HOP_WHOLE_DRY_TO_WET
        old_type = HOP_TYPE_WHOLE_WET
        new_type = HOP_TYPE_WHOLE
        new_weight = hop_type_weight_conversion(weight, old_type, new_type)
        expected = 1.0
        self.assertEquals(new_weight, expected)

    def test_hope_type_weight_conversion_whole_dry_to_wet(self):
        weight = 1.0
        old_type = HOP_TYPE_WHOLE
        new_type = HOP_TYPE_WHOLE_WET
        new_weight = hop_type_weight_conversion(weight, old_type, new_type)
        expected = HOP_WHOLE_DRY_TO_WET
        self.assertEquals(new_weight, expected)

    def test_hope_type_weight_conversion_pellet_to_whole(self):
        weight = 1.0
        old_type = HOP_TYPE_PELLET
        new_type = HOP_TYPE_WHOLE
        new_weight = hop_type_weight_conversion(weight, old_type, new_type)

        expected = HOP_UTILIZATION_SCALE_PELLET