Source code for uclchem.makerates.species

import logging
from collections import Counter
from warnings import warn

[docs] elementList = [ "H", "D", "HE", "C", "N", "O", "F", "P", "S", "CL", "LI", "NA", "MG", "SI", "PAH", "15N", "13C", "18O", "E-", "FE", ]
[docs] elementMass = [ 1, 2, 4, 12, 14, 16, 19, 31, 32, 35, 3, 23, 24, 28, 420, 15, 13, 18, 0, 56, ]
[docs] symbols = ["#", "@", "*", "+", "-", "(", ")"]
[docs] def is_number(s) -> bool: """Try to convert input to a float, if it succeeds, return True. Args: s: Input element to check for Returns: bool: True if a number, False if not. """ try: float(s) return True except ValueError: return False
[docs] class Species: """Species is a class that holds all the information about an individual species in the network. It also has convenience functions to check whether the species is a gas or grain species and to help compare between species. """ def __init__(self, inputRow): """A class representing chemical species, it reads in rows which are formatted as follows: NAME,MASS,BINDING ENERGY,SOLID FRACTION,MONO FRACTION,VOLCANO FRACTION,ENTHALPY Args: inputRow (list): """
[docs] self.name = inputRow[0].upper()
[docs] self.mass = int(inputRow[1])
[docs] self.is_refractory = str(inputRow[2]).lower() == "inf"
if self.is_refractory: self.binding_energy = 99.9e9 else: self.binding_energy = float(inputRow[2])
[docs] self.solidFraction = float(inputRow[3])
[docs] self.monoFraction = float(inputRow[4])
[docs] self.volcFraction = float(inputRow[5])
[docs] self.enthalpy = float(inputRow[6])
[docs] self.n_atoms = 0
# in first instance, assume species freeze/desorb unchanged # this is updated by `check_freeze_desorbs()` later. if self.is_grain_species(): # this will make any excited species desorb as their base counterparts if "*" in self.name: self.desorb_products = [self.name[1:-1], "NAN", "NAN", "NAN"] else: self.desorb_products = [self.name[1:], "NAN", "NAN", "NAN"] else: self.freeze_products = {}
[docs] def get_name(self) -> str: """Get the name of the chemical species. Returns: str: The name """ return self.name
[docs] def get_mass(self) -> int: """Get the molecular mass of the chemical species Returns: int: The molecular mass """ return self.mass
[docs] def get_charge(self) -> int: """Get the charge of the chemical species in e. Positive integer indicates positive ion, negative indicates negative ion. Assumes species are at most charged +1 or -1. Returns: int: The charge of the species """ if not self.is_ion(): return 0 elif "+" in self.name: return 1 elif "-" in self.name: return -1
[docs] def set_desorb_products(self, new_desorbs: list[str]) -> None: """Set the desorption products for species on the surface or in the bulk. It is assumed that there is only one desorption pathway. Args: new_desorbs (list[str]): The new desorption products """ self.desorb_products = new_desorbs
[docs] def get_desorb_products(self) -> list[str]: """Obtain the desorbtion products of ice species Returns: list[str]: The desorption products """ return self.desorb_products
[docs] def set_freeze_products(self, product_list: list[str], freeze_alpha: float) -> None: """Add the freeze products of the species, one species can have several freeze products. Args: product_list (list[str]): The list of freeze out products freeze_alpha (float): The freeze out ratio. It is called alpha, since it is derived from the alpha column in the UCLCHEM reaction format: https://github.com/uclchem/UCLCHEM/blob/08d37f8c3063f8ff8a9a7aa16d9eff0ed4f99538/Makerates/src/network.py#L160 """ self.freeze_products[",".join(product_list)] = freeze_alpha
[docs] def get_freeze_products(self) -> dict[list[str], float]: """Obtain the product to which the species freeze out Returns: dict[str, float]: Reactions and their respective freeze out ratios. Yields: Iterator[dict[str, float]]: Iterator that returns all of the freeze out reactions with ratios """ keys = self.freeze_products.keys() values = self.freeze_products.values() logging.debug(f"freeze keys: {keys}, products {values}") for key, value in zip(keys, values): yield key.split(","), value
[docs] def get_freeze_products_list(self) -> list[list[str]]: """Returns all the freeze products without their ratios Returns: list[list[str]]: List of freeze products """ # TODO: Write an unit test for get_freeze_product_behaviour return [key.split(",") for key in self.freeze_products.keys()]
[docs] def get_freeze_alpha(self, product_list: list[str]) -> float: """Obtain the freeze out ratio of a species for a certain reaction Args: product_list (list[str]): For a specific reaction, get the freezeout ratio Returns: float: The freezeout ratio """ return self.freeze_products[",".join(product_list)]
[docs] def is_grain_species(self) -> bool: """Return whether the species is a species on the grain Returns: bool: True if it is a grain species. """ warn('This method is deprecated in favour of is_ice_species.', DeprecationWarning, stacklevel=2) return ( self.name in ["BULK", "SURFACE"] or self.name.startswith( "#", ) or self.name.startswith("@") )
[docs] def is_ice_species(self) -> bool: """Return whether the species is a species on the grain Returns: bool: True if it is an ice species. """ return ( self.name in ["BULK", "SURFACE"] or self.name.startswith( "#", ) or self.name.startswith("@") )
[docs] def is_surface_species(self) -> bool: """Checks if the species is on the surface Returns: bool: True if a surface species """ return self.name.startswith("#")
[docs] def is_bulk_species(self) -> bool: """Checks if the species is in the bulk Returns: bool: True if a bulk species """ return self.name.startswith("@")
[docs] def is_ion(self) -> bool: """Checks if the species is ionized, either postively or negatively. Returns: bool: True if it is an ionized """ return self.name.endswith("+") or self.name.endswith("-")
[docs] def add_default_freeze(self) -> None: """Adds a defalt freezeout, which is freezing out to the species itself, but with no ionization.""" freeze = "#" + self.name if freeze[-1] in ["+", "-"]: freeze = freeze[:-1] if self.name == "E-": freeze = "" self.set_freeze_products([freeze, "NAN", "NAN", "NAN"], 1.0)
[docs] def find_constituents(self, quiet=False): """Loop through the species' name and work out what its consituent atoms are. Then calculate mass and alert user if it doesn't match input mass. """ speciesName = self.name[:] i = 0 atoms = [] bracket = False bracketContent = [] # loop over characters in species name to work out what it is made of while i < len(speciesName): # if character isn't a #,+ or - then check it otherwise move on if speciesName[i] not in symbols: if i + 1 < len(speciesName): # if next two characters are (eg) 'MG' then atom is Mg not M and G if speciesName[i : i + 3] in elementList: j = i + 3 elif speciesName[i : i + 2] in elementList: j = i + 2 # otherwise work out which element it is elif speciesName[i] in elementList: j = i + 1 # if there aren't two characters left just try next one elif speciesName[i] in elementList: j = i + 1 # if we've found a new element check for numbers otherwise print error if j > i: if bracket: bracketContent.append(speciesName[i:j]) else: atoms.append(speciesName[i:j]) # add element to list if j < len(speciesName): if is_number(speciesName[j]): if int(speciesName[j]) > 1: for k in range(1, int(speciesName[j])): if bracket: bracketContent.append(speciesName[i:j]) else: atoms.append(speciesName[i:j]) i = j + 1 else: i = j else: i = j else: i = j else: raise ValueError( f"Contains elements not in element list: {speciesName}" ) logging.warning(speciesName[i]) logging.warning( "\t{0} contains elements not in element list:".format( speciesName ) ) logging.warning(elementList) else: # if symbol is start of a bracketed part of molecule, keep track if speciesName[i] == "(": bracket = True bracketContent = [] i += 1 # if it's the end then add bracket contents to list elif speciesName[i] == ")": if is_number(speciesName[i + 1]): for k in range(0, int(speciesName[i + 1])): atoms.extend(bracketContent) i += 2 else: atoms.extend(bracketContent) i += 1 # otherwise move on else: i += 1 self.n_atoms = len(atoms) mass = 0 for atom in atoms: mass += elementMass[elementList.index(atom)] if mass != int(self.mass): if not quiet: logging.warning( f"Input mass of {self.name} ({self.mass}) does not match calculated mass of constituents, using calculated mass: {int(mass)}" ) self.mass = int(mass) counter = Counter() for element in elementList: counter[element] = atoms.count(element) return counter
[docs] def get_n_atoms(self) -> int: """Obtain the number of atoms in the molecule Returns: int: The number of atoms """ return self.n_atoms
[docs] def set_n_atoms(self, new_n_atoms: int) -> None: """Set the number of atoms Args: new_n_atoms (int): The new number of atoms """ self.n_atoms = new_n_atoms
[docs] def to_UCL_format(self) -> str: return f"{self.get_name()},{self.get_mass()},{self.binding_energy},{self.solidFraction},{self.monoFraction},{self.volcFraction},{self.enthalpy}"
def __eq__(self, other): """Check for equality based on either a string or another Species instance. Args: other (str, Species): Another species Raises: NotImplementedError: We can only compare between species or strings of species. Returns: bool: True if two species are identical. """ if isinstance(other, Species): return self.name == other.name elif isinstance(other, str): return self.name == other else: raise NotImplementedError( "We can only compare between species or strings of species" ) def __lt__(self, other) -> bool: """Compare the mass of the species Args: other (Species): Another species instance Returns: bool: True if less than the other species """ return self.mass < other.mass def __gt__(self, other) -> bool: """Compare the mass of the species Args: other (Species): Another species instance Returns: bool: True if larger than than the other species """ return self.mass > other.mass def __repr__(self) -> str: return f"Specie: {self.name}" def __str__(self) -> str: return self.name