예제 #1
0
    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
예제 #2
0
    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
예제 #3
0
    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()
예제 #4
0
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)
예제 #5
0
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()