예제 #1
0
class Fermentation(ListTableBase):
    """Tabular definition for fermentation steps outlining the fermentation process."""
    Columns = [
        Column('name', size=Stretch, align=QtCore.Qt.AlignLeft, editable=True),
        Column('startTemperature', 'Start Temp', editable=True),
        Column('endTemperature', 'End Temp', editable=True),
        Column('time', editable=True),
    ]

    # ======================================================================================================================
    # Methods
    # ----------------------------------------------------------------------------------------------------------------------
    def from_excel(self, worksheet):
        """Not supported for fermentable types - they don't get defined in the Excel database."""
        raise NotImplementedError(
            'Fermentation does not support loading library items from Excel worksheets.'
        )

# ----------------------------------------------------------------------------------------------------------------------

    def sort(self):
        """Steps are sorted manually. Deliberately left blank - will be called but nothing will happen."""

# ----------------------------------------------------------------------------------------------------------------------

    def to_dict(self):
        """Convert this fermentation into BeerJSON."""
        return {
            'name': 'Why is the name required at this level?',
            'fermentation_steps': [step.to_dict() for step in self.items]
        }

# ----------------------------------------------------------------------------------------------------------------------

    def from_dict(self, recipe, data):
        """Convert a BeerJSON dict into values for this instance."""
        self.items = []
        for child in data['fermentation_steps']:
            step = FermentationStep(recipe)
            step.from_dict(child)
            self.append(step)
예제 #2
0
class Waters(ListTableBase):
    """Provides for a list of Water objects, specifically created to aid in parsing Excel database files and
    display within a QtTableView."""
    Columns = [
        Column('name', size=Stretch, align=QtCore.Qt.AlignLeft),
        Column('calcium'),
        Column('magnesium'),
        Column('sodium'),
        Column('chloride'),
        Column('sulfate'),
        Column('bicarbonate'),
        Column('ph', 'pH'),
    ]

    # ======================================================================================================================
    # Properties
    # ----------------------------------------------------------------------------------------------------------------------
    @property
    def calcium(self):
        """Calculate and return the total calcium in the water based upon the percentage of each water component."""
        return sum([water.calcium * water.percentage for water in self],
                   ConcentrationType(0, 'ppm'))

# ----------------------------------------------------------------------------------------------------------------------

    @property
    def magnesium(self):
        """Calculate and return the total magnesium in the water based upon the percentage of each water component."""
        return sum([water.magnesium * water.percentage for water in self],
                   ConcentrationType(0, 'ppm'))

# ----------------------------------------------------------------------------------------------------------------------

    @property
    def sodium(self):
        """Calculate and return the total sodium in the water based upon the percentage of each water component."""
        return sum([water.sodium * water.percentage for water in self],
                   ConcentrationType(0, 'ppm'))

# ----------------------------------------------------------------------------------------------------------------------

    @property
    def chloride(self):
        """Calculate and return the total chloride in the water based upon the percentage of each water component."""
        return sum([water.chloride * water.percentage for water in self],
                   ConcentrationType(0, 'ppm'))

# ----------------------------------------------------------------------------------------------------------------------

    @property
    def sulfate(self):
        """Calculate and return the total sulfate in the water based upon the percentage of each water component."""
        return sum([water.sulfate * water.percentage for water in self],
                   ConcentrationType(0, 'ppm'))

# ----------------------------------------------------------------------------------------------------------------------

    @property
    def bicarbonate(self):
        """Calculate and return the total bicarbonate in the water based upon the percentage of each water component."""
        return sum([water.bicarbonate * water.percentage for water in self],
                   ConcentrationType(0, 'ppm'))

# ----------------------------------------------------------------------------------------------------------------------

    @property
    def carbonate(self):
        """Calculate and return the total carbonate in the water based upon the percentage of each water component."""
        return sum([water.carbonate * water.percentage for water in self],
                   ConcentrationType(0, 'ppm'))

# ----------------------------------------------------------------------------------------------------------------------

    @property
    def alkalinity(self):
        """Calculate and return the total alkalinity in the water based upon the percentage of each water component."""
        return sum([water.alkalinity * water.percentage for water in self],
                   ConcentrationType(0, 'ppm'))

# ----------------------------------------------------------------------------------------------------------------------

    @property
    def hardness(self):
        """Calculate and return the total harness in the water based upon the percentage of each water component."""
        return sum([
            water.hardness * water.percentage.percent / 100 for water in self
        ])

# ----------------------------------------------------------------------------------------------------------------------

    @property
    def ph(self):
        """Calculate and return the total bicarbonate in the water based upon the percentage of each water component."""
        return sum(
            [water.ph * water.percentage.percent / 100 for water in self])

# ======================================================================================================================
# Other Methods
# ----------------------------------------------------------------------------------------------------------------------

    def from_excel(self, worksheet):
        """Parses out a list of hop objects from the provided Excel worksheet and appends them to this instance."""
        self.items = []
        for idx, row in enumerate(worksheet):
            # Skip the header row.
            if idx == 0:
                continue

            self.append(
                Water(name=str(row[0].value),
                      calcium=ConcentrationType(row[1].value, 'ppm'),
                      magnesium=ConcentrationType(row[2].value, 'ppm'),
                      sodium=ConcentrationType(row[3].value, 'ppm'),
                      chloride=ConcentrationType(row[4].value, 'ppm'),
                      sulfate=ConcentrationType(row[5].value, 'ppm'),
                      bicarbonate=ConcentrationType(row[6].value, 'ppm'),
                      ph=float(row[7].value),
                      notes=str(row[8].value)))

# ----------------------------------------------------------------------------------------------------------------------

    def sort(self):
        """A void sort function that consistently sorts the water in decreasing order of amount in the recipe."""
        def sorter(water: Water):
            """Give distilled water a higher priority in sorting than any other name."""
            if 'distilled' in water.name.lower():
                return 'A'
            return 'B' + water.name

        self.items.sort(key=sorter, reverse=True)

# ----------------------------------------------------------------------------------------------------------------------

    def to_dict(self):
        """Convert self into a dictionary for BeerJSON storage."""
        return [water.to_dict() for water in self]

# ----------------------------------------------------------------------------------------------------------------------

    def from_dict(self, recipe, data):
        """Parse list of waters from the provided BeerJSON dict."""
        for item in data:
            water = Water(recipe)
            water.from_dict(item)
            self.append(water)
예제 #3
0
class Hops(ListTableBase):
    """Provides for a list of Hop objects, specifically created to aid in parsing Excel database files and
    display within a QtTableView."""
    Columns = [
        Column('amount', editable=True, hideLimited=True),
        Column('timing.use',
               'Use In',
               align=QtCore.Qt.AlignLeft,
               editable=True,
               hideLimited=True),
        Column('timing.duration',
               align=QtCore.Qt.AlignHCenter,
               editable=True,
               hideLimited=True),
        Column('_ibus', 'IBUs', template='%.1f IBUs', hideLimited=True),
        Column('name', size=Stretch, align=QtCore.Qt.AlignLeft),
        Column('htype', 'Type', align=QtCore.Qt.AlignLeft),
        Column('form'),
        Column('origin', align=QtCore.Qt.AlignHCenter),
        Column('alpha')
    ]

    # ======================================================================================================================
    # Properties
    # ----------------------------------------------------------------------------------------------------------------------
    @property
    def trubLoss(self):
        loss = 0
        for hop in self.items:
            if 'Boil' not in hop.timing.use:
                continue

            if 'Leaf' in hop.form:
                loss += hop.amount.oz * 0.0625

            elif 'Pellet' in hop.form:
                loss += hop.amount.oz * 0.025

        return loss

# ======================================================================================================================
# Other Methods
# ----------------------------------------------------------------------------------------------------------------------

    def from_excel(self, worksheet):
        """Parses out a list of hop objects from the provided Excel worksheet and appends them to this instance."""
        self.items = []
        for idx, row in enumerate(worksheet):
            # Skip the header row.
            if idx == 0:
                continue

            self.append(
                Hop(name=str(row[0].value),
                    htype=str(row[1].value),
                    form=str(row[2].value),
                    alpha=PercentType(row[3].value, '%'),
                    beta=PercentType(row[4].value, '%'),
                    hsi=PercentType(row[5].value, '%'),
                    origin=str(row[6].value),
                    substitutes=str(row[7].value),
                    humulene=PercentType(row[8].value, '%'),
                    caryophyllene=PercentType(row[9].value, '%'),
                    cohumulone=PercentType(row[10].value, '%'),
                    myrcene=PercentType(row[11].value, '%'),
                    notes=str(row[12].value)))

# ----------------------------------------------------------------------------------------------------------------------

    def sort(self):
        """A void sort function that consistently sorts the hop in decreasing order of amount in the recipe."""
        uses = ['Mash', 'Boil', 'Fermentation']
        self.items.sort(key=lambda hop: (uses.index(hop.timing.use), -hop.
                                         timing.duration.sec, hop.name))

# ----------------------------------------------------------------------------------------------------------------------

    def to_dict(self):
        """Convert self into a dictionary for BeerJSON storage."""
        return [hop.to_dict() for hop in self]

# ----------------------------------------------------------------------------------------------------------------------

    def from_dict(self, recipe, data):
        """Parse list of hops from the provided BeerJSON dict."""
        for item in data:
            hop = Hop(recipe)
            hop.from_dict(item)
            self.append(hop)
예제 #4
0
class Miscellanea(ListTableBase):
    """Provides for a list of misc objects, specifically created to aid in parsing Excel database files and
    display within a QtTableView."""
    Columns = [
        Column('name', size=Stretch, align=QtCore.Qt.AlignLeft, editable=True),
        Column('mtype', 'Type', align=QtCore.Qt.AlignCenter, editable=True),
        Column('useFor', editable=True),
        Column('amount', editable=True),
        Column('timing.use', editable=True),
        Column('timing.duration', editable=True),
    ]

    # ======================================================================================================================
    # Properties
    # ----------------------------------------------------------------------------------------------------------------------
    @property
    def salts(self):
        """Run through the items in this list a return a list of items that are marked as "water agents" without "acid"
        in there names."""
        return [
            item for item in self.items if item.mtype.lower() == 'water agent'
            and 'acid' not in item.name.lower()
        ]

# ----------------------------------------------------------------------------------------------------------------------

    @property
    def mashSalts(self):
        """Filter the salts down to just those in the mash."""
        return [
            salt for salt in self.salts if salt.timing.use.lower() == 'mash'
        ]

# ----------------------------------------------------------------------------------------------------------------------

    @property
    def kettleSalts(self):
        """Filter the salts down to just those in the kettle/boil."""
        return [
            salt for salt in self.salts if salt.timing.use.lower() == 'boil'
        ]

# ----------------------------------------------------------------------------------------------------------------------

    @property
    def acids(self):
        """Run through the items in this list a return a list of items that are marked as "water agents" with "acid"
        in there names."""
        return [
            item for item in self.items if item.mtype.lower() == 'water agent'
            and 'acid' in item.name.lower()
        ]

# ======================================================================================================================
# Other Methods
# ----------------------------------------------------------------------------------------------------------------------

    def from_excel(self, worksheet):
        """Misc item are not defined in Excel libraries."""
        raise NotImplementedError(
            'Miscellaneous items are not defined in Excel libraries.')

# ----------------------------------------------------------------------------------------------------------------------

    def sort(self):
        """A void sort function that consistently sorts the misc in decreasing order of amount in the recipe."""
        self.items.sort(key=lambda misc: (-misc.amount.root, misc.name))

# ----------------------------------------------------------------------------------------------------------------------

    def to_dict(self):
        """Convert self into a dictionary for BeerJSON storage."""
        return [item.to_dict() for item in self]

# ----------------------------------------------------------------------------------------------------------------------

    def from_dict(self, recipe, data):
        """Parse list of miscellaneous items from the provided BeerJSON dict."""
        for item in data:
            misc = Miscellaneous(recipe)
            misc.from_dict(item)
            self.append(misc)
예제 #5
0
class Cultures(ListTableBase):
    """Provides for a list of Culture objects, specifically created to aid in parsing Excel database files and
    display within a QtTableView."""
    Columns = [
        Column('amount', editable=True, hideLimited=True),
        Column('name', size=Stretch, align=QtCore.Qt.AlignLeft),
        Column('ctype', 'Type'),
        Column('form'),
        Column('producer'),
        Column('productId', 'Product'),
    ]

    # ======================================================================================================================
    # Properties
    # ----------------------------------------------------------------------------------------------------------------------
    @property
    def averageAttenuation(self):
        """Returns the maximum attenuation of all of the cultures in the collection."""
        return max([culture.averageAttenuation for culture in self.items])

# ======================================================================================================================
# Methods
# ----------------------------------------------------------------------------------------------------------------------

    def from_excel(self, worksheet):
        """Dump any items currently associated with this instance and reload from the provided Excel worksheet."""
        self.items = []
        for idx, row in enumerate(worksheet):
            # Skip the header row.
            if idx == 0:
                continue

            self.append(
                Culture(name=str(row[0].value),
                        ctype=str(row[1].value),
                        form=str(row[2].value),
                        producer=str(row[3].value),
                        productId=str(row[4].value),
                        attenuationRange=PercentRangeType(
                            minimum=PercentType(row[5].value * 100, '%'),
                            maximum=PercentType(row[6].value * 100, '%')),
                        notes=str(row[11].value)))

# ----------------------------------------------------------------------------------------------------------------------

    def sort(self):
        """A void sort function that consistently sorts the culture in decreasing order of amount in the recipe."""
        self.items.sort(key=lambda culture: culture.name)

# ----------------------------------------------------------------------------------------------------------------------

    def to_dict(self):
        """Convert self into a dictionary for BeerJSON storage."""
        return [culture.to_dict() for culture in self]

# ----------------------------------------------------------------------------------------------------------------------

    def from_dict(self, recipe, data):
        """Parse list of cultures from the provided BeerJSON dict."""
        for item in data:
            culture = Culture(recipe)
            culture.from_dict(item)
            self.append(culture)
예제 #6
0
class Fermentables(ListTableBase):
    """Provides for a list of Fermentable objects, specifically created to aid in parsing Excel database files and
    display within a QtTableView."""
    Columns = [
        Column('amount', editable=True, hideLimited=True),
        Column('proportion', hideLimited=True),
        Column('name',
               'Grain/Fermentable',
               size=Stretch,
               align=QtCore.Qt.AlignLeft),
        Column('color'),
        Column('ftype', 'Type'),
        Column('group'),
        Column('producer'),
        Column('origin'),
    ]

    # ======================================================================================================================
    # Properties
    # ----------------------------------------------------------------------------------------------------------------------
    @property
    def mashedSugar(self):
        """Return the sucrose equivalent of all of the mashed ingredients."""
        return sum([
            item.sucrose for item in self.items
            if item.isMashed and not item.addAfterBoil
        ])

# ----------------------------------------------------------------------------------------------------------------------

    @property
    def nonMashedSugar(self):
        """Returns the total sucrose equivalent of all the non-mashed ingredients."""
        return sum([item.sucrose for item in self.items if not item.isMashed])

# ----------------------------------------------------------------------------------------------------------------------

    @property
    def fermentableSugar(self):
        """Returns to equivalent amount of sucrose, in pounds, that is fermentable.  Excludes ingredients like lactose
        and Splenda."""
        return sum([item.sucrose for item in self.items if item.isFermentable])

# ----------------------------------------------------------------------------------------------------------------------

    @property
    def nonFermentableSugar(self):
        """Returns to equivalent amount of sucrose, in pounds, that is NOT fermentable.  This includes ingredients like
        lactose or Splenda."""
        return sum(
            [item.sucrose for item in self.items if not item.isFermentable])

# ----------------------------------------------------------------------------------------------------------------------

    @property
    def steepedSugar(self):
        """Returns the equivalent amount of sucrose that is steeped post boil, but where it is still "mashed" so
        Brewhouse efficiency still plays a factor."""
        return sum([
            item.sucrose for item in self.items
            if item.isMashed and item.addAfterBoil
        ])

# ----------------------------------------------------------------------------------------------------------------------

    @property
    def lateAdditionSugar(self):
        """Returns the equivalent amount of sucrose that is added post boil, but where the efficiency isn't a factor."""
        return sum([
            item.sucrose for item in self.items
            if not item.isMashed and item.addAfterBoil
        ])

# ----------------------------------------------------------------------------------------------------------------------

    @property
    def mashWeight(self) -> MassType:
        """Returns the combined weight of all of the mashed ingredients."""
        return sum([item.amount for item in self.items if item.isMashed],
                   MassType(0, 'lb'))

# ----------------------------------------------------------------------------------------------------------------------

    @property
    def mashBiFi(self):
        """A step in calculating the mash pH but also required for calculating the overall water chemistry."""
        total = 0
        for fermentable in self.items:
            total += fermentable.bi * fermentable.proportion.percent / 100
        return total

# ----------------------------------------------------------------------------------------------------------------------

    @property
    def mashPh(self):
        """Calculates the distilled water mash pH for each mashed fermentable and returns the result."""
        phiBiFi = 0
        for fermentable in self.items:
            phiBiFi += fermentable.phi * fermentable.bi * fermentable.proportion.percent / 100
        return phiBiFi / self.mashBiFi

# ======================================================================================================================
# Other Methods
# ----------------------------------------------------------------------------------------------------------------------

    def from_excel(self, worksheet):
        """Parses out a list og Fermentable object and appends them to this instance."""
        def float_or(value, fallback=None):
            """Return the provided value as a float or return fallback if the float conversion fails."""
            try:
                return float(value)
            except TypeError:
                return fallback

        self.items = []
        for idx, row in enumerate(worksheet):
            # Skip the header row.
            if idx == 0:
                continue

            self.append(
                Fermentable(name=str(row[0].value),
                            ftype=str(row[1].value),
                            group=str(row[2].value),
                            producer=str(row[3].value),
                            origin=str(row[4].value),
                            fyield=PercentType(row[5].value, '%'),
                            color=ColorType(row[6].value, 'SRM'),
                            moisture=PercentType(row[7].value, '%'),
                            diastaticPower=DiastaticPowerType(
                                row[8].value, 'Lintner'),
                            addAfterBoil=bool(row[9].value),
                            mashed=bool(row[10].value),
                            phi=float_or(row[11].value),
                            bi=float_or(row[12].value),
                            notes=str(row[13].value)))

# ----------------------------------------------------------------------------------------------------------------------

    def sort(self):
        """A void sort function that consistently sorts the fermentable in decreasing order of amount in the recipe."""
        self.items.sort(
            key=lambda fermentable: (-fermentable.amount.lb, fermentable.name))

# ----------------------------------------------------------------------------------------------------------------------

    def to_dict(self):
        """Convert self into a dictionary for BeerJSON storage."""
        return [fermentable.to_dict() for fermentable in self]

# ----------------------------------------------------------------------------------------------------------------------

    def from_dict(self, recipe, data):
        """Parse list of fermentables from the provided BeerJSON dict."""
        for item in data:
            fermentable = Fermentable(recipe)
            fermentable.from_dict(item)
            self.append(fermentable)
예제 #7
0
class Mash(ListTableBase):
    """Tabular definition for fermentation steps outlining the fermentation process."""
    Columns = [
        Column('name', size=Stretch, align=QtCore.Qt.AlignLeft, editable=True),
        Column('mtype', 'Type', align=QtCore.Qt.AlignLeft, editable=True),
        Column('temperature', editable=True),
        Column('time', editable=True),
        Column('infusionTemperature'),
        Column('infusionVolume'),
    ]

    # ----------------------------------------------------------------------------------------------------------------------
    def __init__(self, recipe):
        super().__init__()
        self.recipe = recipe
        self.changed.connect(self.recalculate)

        self._ambient = TemperatureType(70, 'F')
        self._ratio = SpecificVolumeType(1.25, 'qt/lb')

# ======================================================================================================================
# Properties
# ----------------------------------------------------------------------------------------------------------------------

    @property
    def ambient(self):
        return self._ambient

    @ambient.setter
    def ambient(self, value):
        if self._ambient != value:
            self._ambient = value
            self.changed.emit()

# ----------------------------------------------------------------------------------------------------------------------

    @property
    def ratio(self):
        return self._ratio

    @ratio.setter
    def ratio(self, value):
        if self._ratio != value:
            self._ratio = value
            self.changed.emit()

# ----------------------------------------------------------------------------------------------------------------------

    @property
    def totalWater(self):
        """Calculates the total water required for all of the mashing steps."""
        self.recalculate()
        return sum([
            step.infusionVolume
            for step in self if step.infusionVolume is not None
        ], VolumeType(0, 'gal'))

# ======================================================================================================================
# Methods
# ----------------------------------------------------------------------------------------------------------------------

    def from_excel(self, worksheet):
        """Not supported for fermentable types - they don't get defined in the Excel database."""
        raise NotImplementedError(
            'Mash does not support loading library items from Excel worksheets.'
        )

# ----------------------------------------------------------------------------------------------------------------------

    def sort(self):
        """Steps are sorted manually. Deliberately left blank - will be called but nothing will happen."""

# ----------------------------------------------------------------------------------------------------------------------

    def to_dict(self):
        """Convert this fermentation into BeerJSON."""
        return {
            'name': 'Why is the name required at this level?',
            'grain_temperature': self.recipe.mash.ambient.to_dict(),
            'mash_steps': [step.to_dict() for step in self.items]
        }

# ----------------------------------------------------------------------------------------------------------------------

    def from_dict(self, data):
        """Convert a BeerJSON dict into values for this instance."""
        self.ambient = TemperatureType(json=data['grain_temperature'])
        if 'mash_steps' in data and data[
                'mash_steps'] and 'water_grain_ratio' in data['mash_steps'][0]:
            self.ratio = SpecificVolumeType(
                json=data['mash_steps'][0]['water_grain_ratio'])

        self.items = []
        for child in data['mash_steps']:
            step = MashStep(self.recipe)
            step.from_dict(child)
            self.append(step)

# ----------------------------------------------------------------------------------------------------------------------

    def recalculate(self):
        """Recalculate the temperatures and volumes for the mash steps."""
        # Can't do anything if there aren't any steps yet.
        if not self.items:
            return
        # Calculate the initial step without any previous step.
        previous = self.items[0].calculate(None)

        # Run through the middle steps (stop one short of the end).
        for step in self.items[1:-1]:
            # NOTE: This loop will not run if there are two or less steps in the mash.
            previous = step.calculate(previous)

        if len(self) > 1:
            # Run the final calculation as a special case because we need to get up to our target volume.
            self.items[-1].calculate(previous, final=True)