示例#1
0
def test_creation(unit):
    """Verify that a Concentration Type instantiates with the properproperty values from inputs."""
    value = random.randint(0, 1000) / 10
    instance = ConcentrationType(value, unit)
    assert isinstance(instance, ConcentrationType)
    assert instance.value == value
    assert instance.as_(unit) == pytest.approx(value)
示例#2
0
 def __init__(self,
              recipe=None,
              name=None,
              amount=None,
              calcium=None,
              magnesium=None,
              sodium=None,
              chloride=None,
              sulfate=None,
              bicarbonate=None,
              ph=7.0,
              notes=None):
     self.recipe = recipe
     self.name: str = name
     self.amount: VolumeType = amount
     self.calcium: ConcentrationType = calcium if calcium is not None else ConcentrationType(
         0, 'ppm')
     self.magnesium: ConcentrationType = magnesium if magnesium is not None else ConcentrationType(
         0, 'ppm')
     self.sodium: ConcentrationType = sodium if sodium is not None else ConcentrationType(
         0, 'ppm')
     self.chloride: ConcentrationType = chloride if chloride is not None else ConcentrationType(
         0, 'ppm')
     self.sulfate: ConcentrationType = sulfate if sulfate is not None else ConcentrationType(
         0, 'ppm')
     self.bicarbonate: ConcentrationType = bicarbonate if bicarbonate is not None else ConcentrationType(
         0, 'ppm')
     self.ph: float = ph
     self.notes: str = notes
示例#3
0
 def carbonate(self):
     """Calculates and returns the carbonate level for this water source.  Carbonate is a representation of the
     water hardness and is a factor of the water's pH and alkalinity."""
     alkalinityAsCaCO3 = self.bicarbonate.ppm / 1.22
     k2 = 10.3309621991148
     carbonate = alkalinityAsCaCO3 * (10**(self.ph - k2)) / (
         1 + (2 * (10**(self.ph - k2)))) * 60.008 / 50.043
     return ConcentrationType(carbonate, 'ppm')
示例#4
0
    def on_source_changed(self, attribute, value):
        """Fires when the user changes one of the controls for the source water and posts the updates back to the
        recipe.  `attribute` is injected above in a lambda function to allow this single handler function to handle
        updates from all of the source water inputs."""
        if len(self.recipe.waters) == 0:
            # If there are no waters yet then lets get one added to the recipe.
            water = Water(self.recipe)
            water.amount = self.sourceAmount
            self.recipe.waters.append(water)
        else:
            # When there is a water pull it for updates.
            water = self.recipe.waters[0]

        if attribute not in ['name', 'ph']:
            value = ConcentrationType(value, 'ppm')

        setattr(water, attribute, value)

        self.recipe.changed.emit()
示例#5
0
 def from_dict(self, data: dict):
     """Populate the data in this instance from the provided BeerJSON format dict."""
     self.name = data['name']
     self.calcium = ConcentrationType(json=data['calcium'])
     self.magnesium = ConcentrationType(json=data['magnesium'])
     self.sodium = ConcentrationType(json=data['sodium'])
     self.chloride = ConcentrationType(json=data['chloride'])
     self.sulfate = ConcentrationType(json=data['sulfate'])
     self.bicarbonate = ConcentrationType(json=data['bicarbonate'])
     if 'pH' in data:
         self.ph = data['pH']
     self.amount = VolumeType(json=data['amount'])
     if 'notes' in data:
         self.notes = data['notes'].replace('\\n', '\n')
示例#6
0
 def on_accept(self):
     """Fires when the user hits to okay button, stores the data from the GUI back into the water passed into init.
     """
     self.water.name = self.ui.name.text()
     self.water.calcium = ConcentrationType(self.ui.calcium.value(), 'ppm')
     self.water.magnesium = ConcentrationType(self.ui.magnesium.value(),
                                              'ppm')
     self.water.sodium = ConcentrationType(self.ui.sodium.value(), 'ppm')
     self.water.chloride = ConcentrationType(self.ui.chloride.value(),
                                             'ppm')
     self.water.sulfate = ConcentrationType(self.ui.sulfate.value(), 'ppm')
     self.water.bicarbonate = ConcentrationType(self.ui.bicarbonate.value(),
                                                'ppm')
     self.water.ph = self.ui.ph.value()
     self.water.notes = self.ui.notes.toPlainText()
示例#7
0
    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)))
示例#8
0
 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'))
示例#9
0
 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'))
示例#10
0
 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'))
示例#11
0
    def recalculate(self):
        """Connected to the input boxes to recalculate the water chemistry whenever the inputs change.

        This method only updates te read-only, calculated properties of the tab - it does not update any of the user
        input boxes.

        It is intended as a response to user input, and does not trigger actions deliberately to prevent circular
        updates."""

        # There is nothing we can calculate here until the strike volume can be calculated.
        if self.recipe.strikeVolume.value == 0:
            return

        # Start with the base water ion concentrations.
        try:
            calciumWater = self.recipe.waters.calcium
            magnesiumWater = self.recipe.waters.magnesium
            sodiumWater = self.recipe.waters.sodium
            chlorideWater = self.recipe.waters.chloride
            sulfateWater = self.recipe.waters.sulfate
            bicarbonateWater = self.recipe.waters.bicarbonate
        except AttributeError:
            # This gets thrown when the recipe information isn't complete enough to calculate ion concentrations.
            return

        # Calculate the ion concentrations for the mash.
        calciumMash = ConcentrationType(
            0.273 * self.ui.cacl2_mash.value() * 1000, 'ppm')
        calciumMash += ConcentrationType(
            0.233 * self.ui.caso4_mash.value() * 1000, 'ppm')
        calciumMash += ConcentrationType(
            0.541 * self.ui.caoh2_mash.value() * 1000, 'ppm')
        calciumMash /= self.recipe.strikeVolume.liters
        calciumMash += calciumWater

        magnesiumMash = ConcentrationType(
            0.12 * self.ui.mgcl2_mash.value() * 1000, 'ppm')
        magnesiumMash += ConcentrationType(
            0.099 * self.ui.mgso4_mash.value() * 1000, 'ppm')
        magnesiumMash /= self.recipe.strikeVolume.liters
        magnesiumMash += magnesiumWater

        sodiumMash = ConcentrationType(
            0.393 * self.ui.nacl_mash.value() * 1000, 'ppm')
        sodiumMash += ConcentrationType(
            0.274 * self.ui.nahco3_mash.value() * 1000, 'ppm')
        sodiumMash /= self.recipe.strikeVolume.liters
        sodiumMash += sodiumWater

        chlorideMash = ConcentrationType(
            0.482 * self.ui.cacl2_mash.value() * 1000, 'ppm')
        chlorideMash += ConcentrationType(
            0.349 * self.ui.mgcl2_mash.value() * 1000, 'ppm')
        chlorideMash += ConcentrationType(
            0.607 * self.ui.nacl_mash.value() * 1000, 'ppm')
        chlorideMash /= self.recipe.strikeVolume.liters
        chlorideMash += chlorideWater

        sulfateMash = ConcentrationType(
            0.558 * self.ui.caso4_mash.value() * 1000, 'ppm')
        sulfateMash += ConcentrationType(
            0.39 * self.ui.mgso4_mash.value() * 1000, 'ppm')
        sulfateMash /= self.recipe.strikeVolume.liters
        sulfateMash += sulfateWater

        bicarbonateMash = ConcentrationType(
            0.726 * self.ui.nahco3_mash.value() * 1000, 'ppm')
        bicarbonateMash /= self.recipe.strikeVolume.liters
        bicarbonateMash += bicarbonateWater

        # Update the UI totals for the mash contributions.
        self.ui.strikeCalcium.setText(str(calciumMash))
        self.ui.strikeMagnesium.setText(str(magnesiumMash))
        self.ui.strikeSodium.setText(str(sodiumMash))
        self.ui.strikeChloride.setText(str(chlorideMash))
        self.ui.strikeSulfate.setText(str(sulfateMash))
        self.ui.strikeBicarbonate.setText(str(bicarbonateMash))

        # Calculate the ion concentrations for the kettle.
        calciumKettle = ConcentrationType(
            0.273 * self.ui.cacl2_kettle.value() * 1000, 'ppm')
        calciumKettle += ConcentrationType(
            0.233 * self.ui.caso4_kettle.value() * 1000, 'ppm')
        calciumKettle += ConcentrationType(
            0.541 * self.ui.caoh2_kettle.value() * 1000, 'ppm')
        calciumKettle /= self.recipe.spargeVolume.liters
        calciumKettle += calciumWater

        magnesiumKettle = ConcentrationType(
            0.12 * self.ui.mgcl2_kettle.value() * 1000, 'ppm')
        magnesiumKettle += ConcentrationType(
            0.099 * self.ui.mgso4_kettle.value() * 1000, 'ppm')
        magnesiumKettle /= self.recipe.spargeVolume.liters
        magnesiumKettle += magnesiumWater

        sodiumKettle = ConcentrationType(
            0.393 * self.ui.nacl_kettle.value() * 1000, 'ppm')
        sodiumKettle += ConcentrationType(
            0.274 * self.ui.nahco3_kettle.value() * 1000, 'ppm')
        sodiumKettle /= self.recipe.spargeVolume.liters
        sodiumKettle += sodiumWater

        chlorideKettle = ConcentrationType(
            0.482 * self.ui.cacl2_kettle.value() * 1000, 'ppm')
        chlorideKettle += ConcentrationType(
            0.349 * self.ui.mgcl2_kettle.value() * 1000, 'ppm')
        chlorideKettle += ConcentrationType(
            0.607 * self.ui.nacl_kettle.value() * 1000, 'ppm')
        chlorideKettle /= self.recipe.spargeVolume.liters
        chlorideKettle += chlorideWater

        sulfateKettle = ConcentrationType(
            0.558 * self.ui.caso4_kettle.value() * 1000, 'ppm')
        sulfateKettle += ConcentrationType(
            0.39 * self.ui.mgso4_kettle.value() * 1000, 'ppm')
        sulfateKettle /= self.recipe.spargeVolume.liters
        sulfateKettle += sulfateWater

        bicarbonateKettle = ConcentrationType(
            0.726 * self.ui.nahco3_kettle.value() * 1000, 'ppm')
        bicarbonateKettle /= self.recipe.spargeVolume.liters
        bicarbonateKettle += bicarbonateWater

        # Update the UI totals for the kettle contributions.
        self.ui.spargeCalcium.setText(str(calciumKettle))
        self.ui.spargeMagnesium.setText(str(magnesiumKettle))
        self.ui.spargeSodium.setText(str(sodiumKettle))
        self.ui.spargeChloride.setText(str(chlorideKettle))
        self.ui.spargeSulfate.setText(str(sulfateKettle))
        self.ui.spargeBicarbonate.setText(str(bicarbonateKettle))

        def average(mash, kettle):
            mash *= self.recipe.strikeVolume.gallons
            kettle *= self.recipe.spargeVolume.gallons
            return (mash + kettle) / self.recipe.boilVolume.gallons

        # Total up everything
        calcium = average(calciumMash, calciumKettle)
        magnesium = average(magnesiumMash, magnesiumKettle)
        sodium = average(sodiumMash, sodiumKettle)
        chloride = average(chlorideMash, chlorideKettle)
        sulfate = average(sulfateMash, sulfateKettle)
        bicarbonate = average(bicarbonateMash, bicarbonateKettle)

        # Update the output UI boxes and sliders.
        self.ui.calcium.setText(str(calcium))
        self.ui.calciumSlide.setValue(calcium.ppm)
        self.ui.magnesium.setText(str(magnesium))
        self.ui.magnesiumSlide.setValue(magnesium.ppm)
        self.ui.sodium.setText(str(sodium))
        self.ui.sodiumSlide.setValue(sodium.ppm)
        self.ui.chloride.setText(str(chloride))
        self.ui.chlorideSlide.setValue(chloride.ppm)
        self.ui.sulfate.setText(str(sulfate))
        self.ui.sulfateSlide.setValue(sulfate.ppm)
        self.ui.bicarbonate.setText(str(bicarbonate))
        self.ui.bicarbonateSlide.setValue(bicarbonate.ppm)

        # Change the slider color if any of the values go out of range.
        errorStyle = "QSlider::handle:horizontal {background-color: red;}"
        resetStyle = "QSlider::handle:horizontal {}"

        if calcium.ppm < 50 or calcium.ppm > 150:
            self.ui.calciumSlide.setStyleSheet(errorStyle)
        else:
            self.ui.calciumSlide.setStyleSheet(resetStyle)

        if magnesium.ppm < 10 or magnesium.ppm > 30:
            self.ui.magnesiumSlide.setStyleSheet(errorStyle)
        else:
            self.ui.magnesiumSlide.setStyleSheet(resetStyle)

        if sodium.ppm > 150:
            self.ui.sodiumSlide.setStyleSheet(errorStyle)
        else:
            self.ui.sodiumSlide.setStyleSheet(resetStyle)

        if chloride.ppm > 250:
            self.ui.chlorideSlide.setStyleSheet(errorStyle)
        else:
            self.ui.chlorideSlide.setStyleSheet(resetStyle)

        if sulfate.ppm < 50 or sulfate.ppm > 350:
            self.ui.sulfateSlide.setStyleSheet(errorStyle)
        else:
            self.ui.sulfateSlide.setStyleSheet(resetStyle)

        if bicarbonate.ppm > 250:
            self.ui.bicarbonateSlide.setStyleSheet(errorStyle)
        else:
            self.ui.bicarbonateSlide.setStyleSheet(resetStyle)

        strikeL = self.recipe.strikeVolume.liters

        # Strength is fixed in this tool as 10% phosphoric seems pretty typical.
        phosphoricStrength = 0.1
        phosphoricVolume = self.ui.phosphoric.value()
        phosphoricDensity = 1 + (0.49 * phosphoricStrength) + (
            (0.375 * phosphoricStrength)**2)
        phosphoricAlkalinity = -phosphoricStrength * phosphoricDensity / 98 * 1000 * phosphoricVolume / strikeL

        # Strength is fixed in this tool as 88% lactic seems pretty typical.
        lacticStrength = 0.88
        lacticVolume = self.ui.lactic.value()
        lacticDensity = 1 + (0.237 * lacticStrength)
        lacticAlkalinity = -lacticStrength * lacticDensity / 90.09 * 1000 * lacticVolume / strikeL

        acidMaltStrength = 0.03
        actiMaltMass = self.ui.acidMalt.value()  # oz
        acidMaltAlkalinity = -acidMaltStrength * actiMaltMass * 28.35 / 90.09 / strikeL * 1000

        bicarbonateNorm = bicarbonateMash.ppm / 61.016
        carbonateNorm = 2 * self.recipe.waters.carbonate.ppm / 60.008
        cAlkalinity = bicarbonateNorm + carbonateNorm

        mashHardness = self.recipe.waters.hardness + (
            self.ui.nahco3_mash.value() / 61.016 / strikeL * 1000)
        calciumNorm = 2 * calciumMash.ppm / 40.078
        magnesiumNorm = 2 * magnesiumMash.ppm / 24.305

        hydroxide = 0.459 * self.ui.caoh2_mash.value() * 1000 / strikeL
        hydroxideNorm = hydroxide / 17.007

        phRaSlope = self.recipe.mash.ratio.litersPerKilogram / self.recipe.fermentables.mashBiFi
        phRaSlopeCorrected = phRaSlope / self.recipe.calibrations.maltBufferingCorrectionFactor

        def calculate_ph(ph, salts=True, acids=True):
            """Run a single iteration of the pH calculations.  The optional arguments will run the calculations using
            inputs from salts and acids, respectively, when True."""
            for _ in range(25):
                fph = 1
                fph += 4.435e-7 * (10**ph)
                fph += 4.435e-7 * 4.667e-11 * (10**(2 * ph))
                fph /= 4.435e-7 * (10**ph)

                zra = cAlkalinity
                zra -= mashHardness / fph
                zra -= calciumNorm / 2.8
                zra -= magnesiumNorm / 5.6
                if acids:
                    zra += phosphoricAlkalinity
                    zra += lacticAlkalinity
                    zra += acidMaltAlkalinity
                if salts:
                    zra += hydroxideNorm

                ph = self.recipe.fermentables.mashPh + phRaSlopeCorrected * zra

            return ph

        ph = calculate_ph(self.recipe.fermentables.mashPh)
        self.ui.ph.setText(f'{ph:.2f}')
        self.ui.phSlide.setValue(ph * 100)
        if ph < 5.2 or ph > 5.6:
            self.ui.phSlide.setStyleSheet(errorStyle)
        else:
            self.ui.phSlide.setStyleSheet(resetStyle)

        # Calculate the chloride to sulfate ratio.
        try:
            ratio = chloride.ppm / sulfate.ppm
        except ZeroDivisionError:
            ratio = 0
        self.ui.ratio.setText(f'{ratio:.2f}')
        self.ui.ratioSlide.setValue(ratio * 100)
示例#12
0
def test_conversion(inVal, inUnit, outVal, outUnit):
    """Verify appropriate conversions between types."""
    instance = ConcentrationType(inVal, inUnit)
    result = instance.as_(outUnit)
    assert result == pytest.approx(outVal)