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)
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
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')
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()
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')
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()
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 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'))
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'))
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'))
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)
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)