def __init__(self, forward, reverse, cell, n, env=environment(), H2O=True, *args, **kwargs): # recall that in redox calculations, the forward reaction is the one # where it goes aq -> s, i.e. a reaction.electrolyte going # backwards! Vice versa for reverse. self.forward = forward # forward reaction as a redox half self.reverse = reverse # reverse reaction as a redox half self.cell = cell # the full cell reaction, probably as a # reaction.reaction object. # Useful for clarity, and housing the environment parameters. # Still important that the molalities etc are correct though, # this is the one we'll be updating! self.equation = self.cell.get_equation() # assert that each of these has the same surroundings self.forward.env = env self.reverse.env = env self.cell.env = env self.env = env self.reactants = cell.reactants self.products = cell.products self.stdE_RTP = self.forward.stdE_RTP - self.reverse.stdE_RTP self.n = n # number of moles of electrons transferred per mole of # product self.H2O = H2O
def r_from_db(cls, name, LocID, dbpath=nmp.std_dbpath): """Extract a reactor from the SQL database at dbpath. Returns the saved reactor object Parameters ---------- name : str name of the reactor. Required for table name. LocID : str LocID of the reaactor to extract. dbpath : str, optional location of the database file. Default is NutMEG_db outside the module directory. """ dbdict = rdb_helper.from_db(name, LocID, dbpath=dbpath) R = cls(name, env=environment(T=dbdict['Temperature'][1], P=dbdict['Pressure'][1], V=dbdict['Volume'][1]), pH=dbdict['pH'][1], workoutID=False, composition_inputs=ast.literal_eval( dbdict['composition_inputs'][1]), dbpath=dbpath) R.dbh.extract_from_Composition(dbdict['CompID'][1]) R.rlist_from_ReactIDs(ast.literal_eval(dbdict['reactions'][1])) R.CompID = dbdict['CompID'][1] R.LocID = LocID return R
def __init__(self, name, env=None, reactionlist={}, composition={}, pH=7., workoutID=True, *args, **kwargs): self.name = name if env == None: self.env = environment() else: self.env = env self.reactionlist = reactionlist self.composition = composition self.volume = kwargs.pop('volume', self.env.V) self.env.V = self.volume self.CompID = '' self.pH = pH self.composition_inputs = kwargs.pop('composition_inputs', {}) self.ReactIDs = tuple() self.dbh = rdb_helper(self, dbpath=kwargs.pop('dbpath', nmp.std_dbpath)) if workoutID: self.dbh.workoutID()
class redox(reaction.reaction): """ Class for redox reactions in solution Will make extensive use of electrolye class, so in an ideal world our reactants have conc, molal, radius, and the V and V_RTP parameters arise from somewhere. For now, we are considering a changing temperature and constant pressure at 1 bar, for reading off tables, though with reaktoro our entropies and heat capacities are updated with pressure. Whether this is the whole story is unlikely. In the future if we decide to consider buffers or dissociating acids etc, these methods can be extended. For now they are limited to dissociated salts. NOTE: This class only deals in dissociations/half reactions, if you want to work out the full redox potentials,use two redox objects and find the difference like you would on pen and paper. """ forward = None # reaction.electrolyte object describing the # forward reaction solid -> electrolytes reverse = None # reaction.electrolyte object describing the # reverse reaction solid -> electrolytes H2O = True # our solvent stdE_RTP = None # standard electrode potential of our couple, in V. stdE = None # standard electrode potential at temperature T, in V E = None # electrode potential at some non-RTP environment. n = 0.0 # number of electrons transferred / reaction Fcons = 96485.3329 # Faraday constant C/mol env = environment() def __init__(self, forward, reverse, cell, n, env=environment(), H2O=True, *args, **kwargs): # recall that in redox calculations, the forward reaction is the one # where it goes aq -> s, i.e. a reaction.electrolyte going # backwards! Vice versa for reverse. self.forward = forward # forward reaction as a redox half self.reverse = reverse # reverse reaction as a redox half self.cell = cell # the full cell reaction, probably as a # reaction.reaction object. # Useful for clarity, and housing the environment parameters. # Still important that the molalities etc are correct though, # this is the one we'll be updating! self.equation = self.cell.get_equation() # assert that each of these has the same surroundings self.forward.env = env self.reverse.env = env self.cell.env = env self.env = env self.reactants = cell.reactants self.products = cell.products self.stdE_RTP = self.forward.stdE_RTP - self.reverse.stdE_RTP self.n = n # number of moles of electrons transferred per mole of # product self.H2O = H2O #self.sol = reaction.electrolyte(reactants, products) def update_quotient(self, getgamma=True, qconc=False, qmolal=False): """Calculate the reaction quotient of the redox reaction. Note that the forward reaction is the one which appears to be dissociating backwards --- its kind of tricky to get your head around. We use the method in MT pp 544--546, which I have written up in a more mathematically general manner in my notes. NOTE: this assumes only the ions taking part have an activity other than 1! In most cases this will be valid but not all. """ if self.cell.all_activities == True and not qconc and not qmolal: # we have the activities of all reagents, so Q can be calculated # using the typical expression self.cell.update_quotient(qconc=False, qmolal=False) # ^ use the default activity calculator self.quotient = self.cell.quotient else: # we do not have the activities of all our constituents, so # calculate the quotient from the mean activity coefficients # (find them if needed) and the molalities. This assumes # the oxidised reaction becomes its standard state and the # reduced reaction leaves its std state. # See the theory in the faux-documentation # get the salt activities fwd_a_salt = self.forward.get_a_salt(getgamma=getgamma) rvs_a_salt = self.reverse.get_a_salt(getgamma=getgamma) # Find the correct ratio to put into the quotient expression # for both the forward: fwd_ratio = None for r1, mr1 in chain(self.forward.reactants.items(), self.forward.products / items()): for r2, mr2 in chain(self.cell.reactants.items(), self.cell.products.items()): if r1.name == r2.name: # i.e. how many forward reactions # are needed in the cell reaction! fwd_ratio = mr2 # and reverse reactions rvs_ratio = None for r1, mr1 in chain(self.reverse.reactants.items(), self.reverse.products.items()): for r2, mr2 in chain(self.cell.reactants.items(), self.cell.products.items()): if r1.name == r2.name: rvs_ratio = mr2 self.quotient = ((rvs_a_salt**rvs_ratio) / (fwd_a_salt**fwd_ratio)) def update_E(self, getgamma=True, estimateDifferentials=True): """Calculate the electrode potential at the environmental temperature and pressure, though the latter is a little ambiguous. If we have neither, then we can use the reaction quotient, which requires our reactants have assigned activities. """ self.forward.update_E(estimate=estimateDifferentials) self.reverse.update_E(estimate=estimateDifferentials) self.stdE = self.forward.stdE - self.reverse.stdE # now correct for nonstandard conditions using activities self.update_quotient(getgamma=getgamma) self.E = self.stdE - ( (self.R * self.env.T / (self.n * self.Fcons)) * math.log(self.quotient)) def update_molar_gibbs(self): """Update the standard molar gibbs free energy of this reaction. """ if self.E == None: raise ValueError( "Please first update the electrode potential " "with any tabulated data you have before trying to calculate " "the free energy!") self.molar_gibbs = -self.n * self.Fcons * self.E def get_equation(self): """Return the overall cell reaction""" return self.equation def react(self, n): """Perform a reaction, consuming unit n moles of reactant. Perform the update to the cell reaction. As all of the reactants are shared, this should automatically update the fwd and reverse reactions too. """ # this can be looked into in more detial later, if we wish to # persue with the redox class. It would be a nice idea to update # the gammas after performing the reaction. self.cell.react(n)
class reagent: """ Class for storing and calculating individual reagent properties such as concentrations, activities, etc. Attributes ------------ name : str name of the reagent conc : float molarity in mol/L. If gaseous, conc describes pressure in bar. Can be None if molal or activity are known gamma : float activity coefficient activity : float Activity. If None, estimate using gamma and conc. molal : float molality in mol/kg. Can be None is conc or activity are known charge : float Charge of reagent. Default 0. """ #name = '' #: name of the reagent conc = None # mol/l # for a gaseous reagent in gaseous reactions, conc describes # pressure (in bar). gamma = 1. # activity coefficient activity = None molal = None # molality in mol/kg charge = 0 radius = None #ionic radius used for electrolytes phase = None # must be one of 'aq', 'g', 'l', or 's' when initialised phase_ss = False # is the reactant in its standard state? Cp_RTP = None # specific heat capacity at 298.15 K 100000 Pa Cp_env = None Cp_T_poly = None # specific heat capacity as a polynomial of temperature # type(np.poly1d) std_formation_enthalpy_RTP = None # J/mol std_formation_entropy_RTP = None # J/mol K std_formation_gibbs_RTP = 0. # J/mol # thermodynamic quantities at the current evironment env = environment(T=298.15, P=101325.0) # use RTP as the default std_formation_gibbs_env = None std_formation_entropy_env = None std_formation_enthalpy_env = None # booleans of state thermo = True # whether we have the thermodynamic data available #### INITIALISATION METHODS def __init__(self, name, env, thermo=True, conc=None, activity=None, molal=None, charge=0, gamma=1., radius=None, phase='aq', phase_ss=False, Cp_T_poly=None, new=True): if new: logger.info('Initialising ' + name) self.name = name self.env = env if phase != 'aq' and phase != 's' and phase != 'g' and phase != 'l': raise ValueError("Incorrectly defined phase for reagent " + str(name) + ", must be one of 's', 'l', 'g', or 'aq'.") # pass Thermo as False to update thermochemical parameters yourself if name != 'e-' and thermo: self.GetThermoParams() elif name == 'e-': self.std_formation_enthalpy_RTP = 0. # J/mol self.std_formation_entropy_RTP = 0. # J/mol K self.std_formation_gibbs_env = 0. self.std_formation_entropy_env = 0. self.std_formation_enthalpy_env = 0. self.thermo = thermo self.conc = conc self.activity = activity self.charge = charge self.molal = molal self.gamma = gamma self.phase = phase self.phase_ss = phase_ss self.Cp_T_poly = Cp_T_poly self.radius = radius def __str__(self): return self.name @staticmethod def get_phase_str(namestr): """Return the phase of a reagent from its name: eg 'aq' from Al(aq)""" if namestr[-2] == 'q': return 'aq' elif namestr[-2] == 's' or namestr[-2] == 'l' or namestr[-2] == 'g': return namestr[-2] else: # phase unknown, assume it is aqueous return 'aq' def redefine(self, re): """re-initialisethis reagent as a new or updated reagent""" logger.debug('Redefining ' + self.name) self.name = re.name self.env = re.env self.std_formation_gibbs_RTP = re.std_formation_gibbs_RTP self.std_formation_enthalpy_RTP = re.std_formation_enthalpy_RTP self.std_formation_entropy_RTP = re.std_formation_entropy_RTP self.std_formation_gibbs_env = re.std_formation_gibbs_env self.std_formation_entropy_env = re.std_formation_entropy_env self.std_formation_enthalpy_env = re.std_formation_enthalpy_env self.Cp_env = re.Cp_env self.Cp_RTP = re.Cp_RTP self.thermo = re.thermo self.conc = re.conc self.activity = re.activity self.charge = re.charge self.molal = re.molal self.gamma = re.gamma self.phase = re.phase self.phase_ss = re.phase_ss self.Cp_T_poly = re.Cp_T_poly self.radius = re.radius # self = re def GetThermoParams(self): """Import the reagent's thermal parameters at both RTP and in the current environment. """ if self.std_formation_entropy_RTP is None: self.import_RTP_params() if self.env.T != 298.15 or self.env.P != 101325.0: self.import_params_db() else: # we're in RTP so no need to look up the data again self.std_formation_enthalpy_env = self.std_formation_enthalpy_RTP self.std_formation_entropy_env = self.std_formation_entropy_RTP self.std_formation_gibbs_env = self.std_formation_gibbs_RTP self.Cp_env = self.Cp_RTP def import_RTP_params(self): """Import thermodynamic RTP data from the SQLite database data/TPdb. If the data doesn't exist, calculate it using reaktoro. Updates std formation gibbs, enthalpy, entropy at RTP. """ rt = reagent_thermo(self) try: ThermoData = rt.db_select(T=298.15, P=101325.0) self.std_formation_gibbs_RTP = float(ThermoData[0]) self.std_formation_enthalpy_RTP = float(ThermoData[1]) self.std_formation_entropy_RTP = float(ThermoData[2]) self.Cp_RTP = float(ThermoData[3]) except Exception as e: # looks like it isn't in the database, better add it! logger.info('Adding ' + self.name + ' properties at RTP to the database') datasent = rt.thermo_to_db(T=298.15, P=101325.0) # now try again if it went in if datasent: ThermoData = rt.db_select(T=298.15, P=101325.0) self.std_formation_gibbs_RTP = float(ThermoData[0]) self.std_formation_enthalpy_RTP = float(ThermoData[1]) self.std_formation_entropy_RTP = float(ThermoData[2]) self.Cp_RTP = float(ThermoData[3]) else: # this thing shouldn't be using thermo params self.thermo = False def import_params_db(self): """Import thermodynamic data from the SQLite database data/TPdb. If the data doesn't exist, calculate it using reaktoro. Updates std formation gibbs, enthalpy, entropy in current environment. """ rt = reagent_thermo(self) try: ThermoData = rt.db_select() self.std_formation_gibbs_env = float(ThermoData[0]) self.std_formation_enthalpy_env = float(ThermoData[1]) self.std_formation_entropy_env = float(ThermoData[2]) self.Cp_env = float(ThermoData[3]) except Exception as e: # looks like it isn't in the database, better add it! logger.info('Adding ' + self.name + ' properties at T = ' + str(round(self.env.T, 2)) + ' K and P = ' + str(round(self.env.P, 0)) + ' Pa to the database...') datasent = rt.thermo_to_db() if datasent: # now try again ThermoData = rt.db_select() self.std_formation_gibbs_env = float(ThermoData[0]) self.std_formation_enthalpy_env = float(ThermoData[1]) self.std_formation_entropy_env = float(ThermoData[2]) self.Cp_env = float(ThermoData[3]) else: # this thing shouldn't be using thermo params self.thermo = False """ SETS FOR UPDATING PARAMETERS """ def update_reagent(self): # more to come I'm sure """Update the reagent's parameters based on a changing environment. For now, it just updates the thermodynamic data. """ if name != 'e-' and thermo: self.GetThermoParams() def set_concentration(self, newconc): """Update reagent concentration to be newconc in mol/L. Parameters ---------- newconc : float New molarity to set in mol/L """ self.conc = newconc def set_molality(self, newmolal): """Update molality in mol/kg solvent. Parameters ---------- newmolal : float New molality to set. """ self.molal = newmolal def set_activitycoefficient(self, newg): """Update the activity coefficients. Parameters ---------- newg : float New activity coefficient to set. """ self.gamma = newg def set_phase(self, newphase): """Change the reagent's phase, and update thermodynamic parameters accordingly. Parameters --------- newphase : str New phase to set. Must be one of 'aq'. 's', 'g', 'l' """ if phase != 'aq' and phase != 's' and phase != 'g' and phase != 'l': raise ValueError("Incorrectly defined phase for reagent " + str(name) + ", must be one of 's', 'l', 'g', or 'aq'.") self.phase = newphase if name != 'e-' and thermo: self.GetThermoParams()