Source code for polo.crystallography.cocktail

# from cockatoo.screen import _parse_cocktail_csv
# from cockatoo.metric import distance
import re

from molmass import Formula
from polo import *

logger = make_default_logger(__name__)

[docs]class Cocktail(): '''Cocktail instances are used to hold a collection of :class:`~polo.crystallography.cocktail.Reagent` instances that form one chemical cocktail. Cocktails also hold other metadata including their commercial code, the cocktail pH and the well they are assigned to. Currently, cocktails are only supported for HWIRuns. :param number: The cocktail number, defaults to None :type number: str, optional :param well_assignment: Well number in the screening plate this cocktail belongs to, defaults to None :type well_assignment: int, optional :param commercial_code: Commercial code of cocktail, defaults to None :type commercial_code: str, optional :param pH: pH of the cocktail, defaults to None :type pH: float, optional :param reagents: list of :class:`~polo.crystallography.cocktail.Reagent` instances that make up the contents of the cocktail, defaults to None :type reagents: Reagent, optional ''' def __init__(self, number=None, well_assignment=None, commercial_code=None, pH=None, reagents=[]): self.well_assignment = well_assignment self.number = number self.commercial_code = commercial_code self.pH = pH self.reagents = reagents @property def cocktail_index(self): '''Attempt to pull out the cocktail number as an integer from the :attr:`~polo.crystallography.cocktail.Cocktail.number` attribute. This property is dependent on consistent formating between cocktail menus that has not checked at this time. :return: Cocktail number :rtype: int ''' try: return int(self.number.split('_C')[-1].lstrip('0')) except IndexError as e: logger.error('Caught {} at cocktail_index property'.format(e)) return None # normal cocktail number format 13_C0001 @property def well_assignment(self): '''Return the current well assignment for this Cocktail. :return: well assignment :rtype: int ''' return self._well_assignment @well_assignment.setter def well_assignment(self, value): if isinstance(value, (str, float)): value = int(value) self._well_assignment = value
[docs] def add_reagent(self, new_reagent): '''Adds a :class:`~polo.crystallography.cocktail.Reagent` to the existing list of reagents referenced by the :attr:`~polo.crystallography.cocktail.Cocktail.reagents` attribute. :param new_reagent: Reagent to add to this cocktail :type new_reagent: Reagent ''' if new_reagent: self.reagents.append(new_reagent)
# def compute_distance(self, other_cocktail): # if isinstance(other_cocktail, Cocktail): # this_cockatoo_cocktail = self._to_cockatoo_cocktail() # other_cockatoo_cocktail = other_cocktail._to_cockatoo_cocktail() # if this_cockatoo_cocktail and other_cockatoo_cocktail: # return distance(this_cockatoo_cocktail, other_cockatoo_cocktail) # return False # def _to_cockatoo_cocktail(self): # # convert cocktail object to a "row" as read from a csv file for use with # # cockatoo package # # name,overall_ph,[conc,unit,name,ph]* # row = [self.number, str(self.pH)] # for reagent in self.reagents: # if reagent.molarity != False: # reagent_row = [ # str(reagent.concentration.value), # str(reagent.concentration.units), # reagent.chemical_additive, # self.pH] # doesnt seem like reagent pH is used in distance calc # row += reagent_row # return _parse_cocktail_csv(row) # should return cocktail object if it worked # # then can compare two cocktail objects # else: # break # return False def __repr__(self): return ''.join(sorted(['{}: {}\n'.format(key, value) for key, value in self.__dict__.items()])) def __str__(self): cocktail_string = 'Cocktail {}\n'.format(self.number) cocktail_string += '-'*len(cocktail_string) + '\n' cocktail_string += 'pH: {}\n'.format(self.pH) for reagent in self.reagents: cocktail_string += '{} {}\n'.format( reagent.chemical_additive, reagent.concentration) return cocktail_string
[docs]class Reagent(): '''Reagent instances represent one specific kind of chemical compound at a specific concentration. Multiple Reagents make up a cocktail. Reagents are generally created from the contents of HWI cocktail csv files which describe all 1536 cocktails and the reagents that compose them in one file. The cocktail csv files can be found in the `data` directory. :param chemical_additive: Name of the chemical reagent, defaults to None :type chemical_additive: str, optional :param concentration: Concentration of the reagent, defaults to None :type concentration: UnitValue, optional :param chemical_formula: Chemical formula for this reagent, defaults to None :type chemical_formula: Formula, optional :param stock_con: Concentration of this reagent's stock solution, defaults to None :type stock_con: UnitValue, optional ''' units = ['M', '(w/v)', '(v/v)'] def __init__(self, chemical_additive=None, concentration=None, chemical_formula=None, stock_con=None): self.chemical_additive = chemical_additive self.concentration = concentration self._chemical_formula = chemical_formula self.stock_con = stock_con # should be in Molarity @property def chemical_formula(self): '''The chemical formula for of this Reagent. Not all Reagents have available chemical formulas as cocktail csv files do not include formulas for all reagents. See the setter method for more details on how chemical formulas are converted from strings to `Formula` objects. :return: Chemical formula :rtype: Formula ''' return self._chemical_formula @chemical_formula.setter def chemical_formula(self, new_formula): '''Setter function for the chemical formula attribute. Assumes that a string will be passed in and attempts to convert that string to a Formula instance. The HWI formating for associated water molecules is not understood by Formula objects so this method uses regex to extract the number of water molecules are rewrite the formula to an equivalent one that Formula object can understand. :param new_formula: chemical formula :type new_formula: str :raises TypeError: Raised when attemping to set chemical_formula to\ something other than a string ''' if isinstance(new_formula, str): water = water_regex.findall(new_formula) if water: num_waters = num_regex.findall(water[0])[0] new_formula = new_formula.replace( water[0], '[H2O]{}'.format(num_waters) ) self._chemical_formula = Formula(new_formula) else: raise TypeError @property def concentration(self): '''The current concentration of this Reagent. Concentration ultimately refers back to a condition in a specific screening well. :return: Chemical concentration :rtype: UnitValue ''' return self._concentration @concentration.setter def concentration(self, new_con): '''Setter function for the concentration attribute. :param new_con: New value for concentration :type new_con: UnitValue :raises TypeError: Raised when attempt to pass object that is not an\ instance of UnitValue as the new_con ''' if isinstance(new_con, UnitValue): self._concentration = new_con else: raise TypeError @property def molarity(self): '''Attempt to calculate the molarity of this :class:`~polo.crystallography.cocktail.Reagent` at its current concentration. This calculation is not certain to return a value as HWI cocktail menu files use a variety of units to describe chemical concentrations, including %w/v or %v/v. %w/v is defined as grams of colute per 100 ml of solution * 100. This can be converted to molarity when the molar mass of the :class:`~polo.crystallography.cocktail.Reagent` is known. %v/v is defined as the volume of solute over the total volume of solution * 100. The density of the :class:`~polo.crystallography.cocktail.Reagent` is required to convert %w/v to molarity which is not included in HWI cocktail menu files. This makes conversion from %w/v out of reach for now. If the :class:`~polo.crystallography.cocktail.Reagent` concentration cannot be converted to molarity then this function will return False. :return: molarity or False :rtype: UnitValue or Bool ''' base_con = self._concentration.to_base() if base_con.units == 'M': return base_con elif base_con.units == 'w/v' and self.molar_mass: M = (base_con.value / self.molar_mass) * 10 return UnitValue(M, 'M') else: return False @property def molar_mass(self): '''Attempt to calculate the molar mass of this reagent. Closely related to the molarity property. The molar mass of the :class:`~polo.crystallography.cocktail.Reagent` cannot be calculated for all HWI reagents. :return: Molar mass of the :class:`~polo.crystallography.cocktail.Reagent` if it is calculable, False otherwise. :rtype: UnitValue or bool ''' mm = None if isinstance(self.chemical_formula, Formula): mm = self.chemical_formula.mass PEG = self.peg_parser(self.chemical_additive) if PEG: mm = PEG if mm: return mm return False
[docs] def peg_parser(self, peg_string): '''Attempts to pull out a molar mass from a PEG species since the molar mass is often included in the name of PEG species. A string is considered to be a potential PEG species if it contains 'PEG' or 'Polyethylene glycol' in it. :param peg_string: String to look for PEG species in :type peg_string: str :return: molar mass if found to be valid PEG species, False otherwise. :rtype: float or Bool ''' keywords = set(['PEG', 'Polyethylene glycol']) for k in keywords: if k in peg_string: peg_string = peg_string.replace(',', '') mm = peg_regex.findall(peg_string) if mm: return float(mm[0]) return False
[docs] def stock_volume(self, target_volume): # stock con must be in molarity '''Attempt to calculate the required amount of stock solution to produce the Reagent's set concentration in the given `target_volume` argument. Stock concentration is taken from the :attr:`~polo.crystallography.cocktail.Cocktail.stock_con` attribute. If :attr:`~polo.crystallography.cocktail.Cocktail.stock_con` is not set or the molarity of the :class:`~polo.crystallography.cocktail.Reagent` can not be calculated this method will return False. :param target_volume: Volume in which stock will be diluted into :type target_volume: UnitValue :return: Volume of stock or False :rtype: UnitValue or False ''' # target volume in liters if self.stock_con and self.molarity: L = (self.molarity.value * target_volume.value) / \ self.stock_con.to_base().value return UnitValue(L, 'L') else: return False
def __str__(self): return '{} {}'.format(self.chemical_additive, self.concentration)
[docs]class UnitValue(): # class for handling anything that comes with a unit '''UnitValues are used to help handle numbers with units. They are not the most robust but help to keep things more organized. UnitValues can be created by either passing values to the `values` and `units` args explicitly or by calling the classmethod :meth:`~polo.crystallography.cocktail.UnitValue.make_from_string` which will use regex to pull out supported units and values. ''' saved_scalers = {'u': 1e-6, 'm': 1e-3, 'c': 1e-2} def __init__(self, value=None, units=None): self.value = value self.units = units
[docs] def set_from_string(self, string): self.value = string self.units = string
[docs] @classmethod def make_from_string(cls, string): '''Create a `UnitValue` from a string containing a value and a unit. Utilizes the :const:`polo.unit_regex` expression to pull out the units. .. highlight:: python .. code-block:: python unit_string = '10.0 M' # concentration of 10 molar sv = UnitValue.make_from_string(unit_string) # sv.value = 10 sv.units = 'M' :param string: The string to extract the UnitValue from :type string: str :return: UnitValue instance :rtype: UnitValue ''' units = unit_regex.findall(string) if units: units = units[0] return cls(value=string, units=units)
@property def value(self): return self._value @value.setter def value(self, value): if isinstance(value, (int, float)): self._value = value elif isinstance(value, str): value = num_regex.findall(value) if value: value = float(value[0]) else: value = 0.0 self._value = value
[docs] def scale(self, scale_key): '''Scale the :attr:`~polo.crystallography.cocktail.UnitValue.value` using a key character that exists in the :const:`~polo.crystallography.cocktail.UnitValue.saved_scalers` dictionary. First converts the value to its base unit and then divides by the `scale_key` argument value. The `scale_key` can be thought of as a SI prefix for a base unit. .. highlight:: python .. code-block:: python # self.saved_scalers = {'u': 1e-6, 'm': 1e-3, 'c': 1e-2} v_one = UnitValue(10, 'L') # value of 10 liters v_one = v_one.scale('u') # get v_one in microliters :param scale_key: Character in :const:`~polo.crystallography.cocktail.UnitValue.saved_scalers` to convert value to. :type scale_key: str :return: UnitValue converted to scale_key unit prefix :rtype: UnitValue ''' if scale_key in self.saved_scalers: temp = self.to_base() # send to base unit return UnitValue( temp.value / self.saved_scalers[scale_key], scale_key + temp.units)
[docs] def to_base(self): '''Converts the :attr:`~polo.crystallography.cocktail.UnitValue.value` to the base unit, if it is not already in the base unit. :return: UnitValue converted to base unit :rtype: UnitValue ''' if self.units and self.units[0] in self.saved_scalers: return UnitValue( self.value * self.saved_scalers[self.units[0]], self.units[1:]) else: return self
def __add__(self, other): if self.units == other.units: self.value += other.value def __round__(self, ndigits=1): self.value = round(self.value, ndigits) return self def __sub__(self, other): if self.units == other.units: self.value -= other.value def __str__(self): # Had issue where value would be a list object. Currently looking into # this but guessing it could be an issue reading weird cocktails # from their csv files. try: return '{} {}'.format(self.value, self.units) except Exception as e: return 'Could not retrieve data' def __float__(self): return float(self.value)