Example #1
0
    def get_maximum_leak_species(self, T, P):
        """
        Get the unexplored (unimolecular) isomer with the maximum leak flux.
        Note that the leak rate coefficients vary with temperature and
        pressure, so you must provide these in order to get a meaningful result.
        """
        # Choose species with maximum leak flux
        max_k = 0.0
        max_species = None
        if len(self.net_reactions) == 0 and len(self.path_reactions) == 1:
            max_k = self.get_leak_coefficient(T, P)
            rxn = self.path_reactions[0]
            if rxn.products == self.source:
                assert len(rxn.reactants) == 1
                max_species = rxn.reactants[0]
            else:
                assert len(rxn.products) == 1
                max_species = rxn.products[0]
        else:
            for rxn in self.net_reactions:
                if len(rxn.products
                       ) == 1 and rxn.products[0] not in self.explored:
                    k = rxn.get_rate_coefficient(T, P)
                    if max_species is None or k > max_k:
                        max_species = rxn.products[0]
                        max_k = k

        # Make sure we've identified a species
        if max_species is None:
            raise NetworkError('No unimolecular isomers left to explore!')
        # Return the species
        return max_species
Example #2
0
    def selectEnergyGrains(self, T, grainSize=0.0, grainCount=0):
        """
        Select a suitable list of energies to use for subsequent calculations.
        This is done by finding the minimum and maximum energies on the 
        potential energy surface, then adding a multiple of 
        :math:`k_\\mathrm{B} T` onto the maximum energy.

        You must specify either the desired grain spacing `grainSize` in J/mol
        or the desired number of grains `Ngrains`, as well as a temperature
        `T` in K to use for the equilibrium calculation. You can specify both
        `grainSize` and `grainCount`, in which case the one that gives the more
        accurate result will be used (i.e. they represent a maximum grain size
        and a minimum number of grains). An array containing the energy grains
        in J/mol is returned.
        """

        if grainSize == 0.0 and grainCount == 0:
            raise NetworkError('Must provide either grainSize or Ngrains parameter to Network.determineEnergyGrains().')

        # The minimum energy is the lowest isomer or reactant or product energy on the PES
        Emin = numpy.min(self.E0)
        Emin = math.floor(Emin) # Round to nearest whole number

        # Use the highest energy on the PES as the initial guess for Emax0
        Emax = numpy.max(self.E0)
        for rxn in self.pathReactions:
            E0 = float(rxn.transitionState.conformer.E0.value_si)
            if E0 > Emax: Emax = E0
        
        # Choose the actual Emax as many kB * T above the maximum energy on the PES
        # You should check that this is high enough so that the Boltzmann distributions have trailed off to negligible values
        Emax += 40. * constants.R * T

        return self.__getEnergyGrains(Emin, Emax, grainSize, grainCount)
Example #3
0
    def __getEnergyGrains(self, Emin, Emax, grainSize=0.0, grainCount=0):
        """
        Return an array of energy grains that have a minimum of `Emin`, a
        maximum of `Emax`, and either a spacing of `grainSize` or have number of
        grains `grainCount`. The first three parameters are in J/mol, as is the
        returned array of energy grains.
        """
        
        # Now determine the grain size and number of grains to use
        if grainCount <= 0 and grainSize <= 0.0:
            # Neither grain size nor number of grains specified, so raise exception
            raise NetworkError('You must specify a positive value for either dE or Ngrains.')
        elif grainCount <= 0 and grainSize > 0.0:
            # Only grain size was specified, so we must use it
            useGrainSize = True
        elif grainCount > 0 and grainSize <= 0.0:
            # Only number of grains was specified, so we must use it
            useGrainSize = False
        else:
            # Both were specified, so we choose the tighter constraint
            # (i.e. the one that will give more grains, and so better accuracy)
            grainSize0 = (Emax - Emin) / (grainCount - 1)
            useGrainSize = (grainSize0 > grainSize)

        # Generate the array of energies
        if useGrainSize:
            Elist = numpy.arange(Emin, Emax + grainSize, grainSize, numpy.float64)
        else:
            Elist = numpy.linspace(Emin, Emax, grainCount, numpy.float64)

        return Elist
    def initialize(self,
                   Tmin,
                   Tmax,
                   Pmin,
                   Pmax,
                   maximumGrainSize=0.0,
                   minimumGrainCount=0,
                   activeJRotor=True,
                   activeKRotor=True,
                   rmgmode=False):
        """
        Initialize a pressure dependence calculation by computing several
        quantities that are independent of the conditions. You must specify
        the temperature and pressure ranges of interesting using `Tmin` and
        `Tmax` in K and `Pmin` and `Pmax` in Pa. You must also specify the
        maximum energy grain size `grainSize` in J/mol and/or the minimum
        number of grains `grainCount`.
        """
        if maximumGrainSize == 0.0 and minimumGrainCount == 0:
            raise NetworkError(
                'Must provide either grainSize or Ngrains parameter to Network.determineEnergyGrains().'
            )

        self.Tmin = Tmin
        self.Tmax = Tmax
        self.Pmin = Pmin
        self.Pmax = Pmax
        self.grainSize = maximumGrainSize
        self.grainCount = minimumGrainCount

        self.Nisom = len(self.isomers)
        self.Nreac = len(self.reactants)
        self.Nprod = len(self.products)
        self.Ngrains = 0
        self.NJ = 0

        # Calculate ground-state energies
        self.E0 = numpy.zeros((self.Nisom + self.Nreac + self.Nprod),
                              numpy.float64)
        for i in range(self.Nisom):
            self.E0[i] = self.isomers[i].E0
        for n in range(self.Nreac):
            self.E0[n + self.Nisom] = self.reactants[n].E0
        for n in range(self.Nprod):
            self.E0[n + self.Nisom + self.Nreac] = self.products[n].E0

        # Calculate densities of states
        self.activeJRotor = activeJRotor
        self.activeKRotor = activeKRotor
        self.rmgmode = rmgmode

        self.calculateDensitiesOfStates()
Example #5
0
    def calculateMicrocanonicalRates(self):
        """
        Calculate and return arrays containing the microcanonical rate
        coefficients :math:`k(E)` for the isomerization, dissociation, and
        association path reactions in the network.
        """

        T = self.T
        Elist = self.Elist
        Jlist = self.Jlist
        densStates = self.densStates
        Ngrains = len(Elist)
        Nisom = len(self.isomers)
        Nreac = len(self.reactants)
        Nprod = len(self.products)
        NJ = 1 if self.activeJRotor else len(Jlist)

        self.Kij = numpy.zeros([Nisom,Nisom,Ngrains,NJ], numpy.float64)
        self.Gnj = numpy.zeros([Nreac+Nprod,Nisom,Ngrains,NJ], numpy.float64)
        self.Fim = numpy.zeros([Nisom,Nreac,Ngrains,NJ], numpy.float64)

        isomers = [isomer.species[0] for isomer in self.isomers]
        reactants = [reactant.species for reactant in self.reactants]
        products = [product.species for product in self.products]

        for rxn in self.pathReactions:
            if rxn.reactants[0] in isomers and rxn.products[0] in isomers:
                # Isomerization
                reac = isomers.index(rxn.reactants[0])
                prod = isomers.index(rxn.products[0])
            elif rxn.reactants[0] in isomers and rxn.products in reactants:
                # Dissociation (reversible)
                reac = isomers.index(rxn.reactants[0])
                prod = reactants.index(rxn.products) + Nisom
            elif rxn.reactants[0] in isomers and rxn.products in products:
                # Dissociation (irreversible)
                reac = isomers.index(rxn.reactants[0])
                prod = products.index(rxn.products) + Nisom + Nreac
            elif rxn.reactants in reactants and rxn.products[0] in isomers:
                # Association (reversible)
                reac = reactants.index(rxn.reactants) + Nisom
                prod = isomers.index(rxn.products[0])
            elif rxn.reactants in products and rxn.products[0] in isomers:
                # Association (irreversible)
                reac = products.index(rxn.reactants) + Nisom + Nreac
                prod = isomers.index(rxn.products[0])
            else:
                raise NetworkError('Unexpected type of path reaction "{0}"'.format(rxn))
        
            # Compute the microcanonical rate coefficient k(E)
            reacDensStates = densStates[reac,:,:]
            prodDensStates = densStates[prod,:,:]
            kf, kr = rxn.calculateMicrocanonicalRateCoefficient(self.Elist, self.Jlist, reacDensStates, prodDensStates, T)
                        
            # Check for NaN (just to be safe)
            if numpy.isnan(kf).any() or numpy.isnan(kr).any():
                raise NetworkError('One or more k(E) values is NaN for path reaction "{0}".'.format(rxn))

            # Determine the expected value of the rate coefficient k(T)
            if rxn.canTST():
                # RRKM theory was used to compute k(E), so use TST to compute k(T)
                kf_expected = rxn.calculateTSTRateCoefficient(T)
            else:
                # ILT was used to compute k(E), so use high-P kinetics to compute k(T)
                kf_expected = rxn.kinetics.getRateCoefficient(T)
            
            # Determine the expected value of the equilibrium constant (Kc)
            Keq_expected = self.eqRatios[prod] / self.eqRatios[reac] 

            # Determine the actual values of k(T) and Keq
            C0 = 1e5 / (constants.R * T)
            kf0 = 0.0; kr0 = 0.0; Qreac = 0.0; Qprod = 0.0
            for s in range(NJ):
                kf0 += numpy.sum(kf[:,s] * reacDensStates[:,s] * (2*Jlist[s]+1) * numpy.exp(-Elist / constants.R / T)) 
                kr0 += numpy.sum(kr[:,s] * prodDensStates[:,s] * (2*Jlist[s]+1) * numpy.exp(-Elist / constants.R / T)) 
                Qreac += numpy.sum(reacDensStates[:,s] * (2*Jlist[s]+1) * numpy.exp(-Elist / constants.R / T)) 
                Qprod += numpy.sum(prodDensStates[:,s] * (2*Jlist[s]+1) * numpy.exp(-Elist / constants.R / T)) 
            kr0 *= C0 ** (len(rxn.products) - len(rxn.reactants))
            Qprod *= C0 ** (len(rxn.products) - len(rxn.reactants))
            kf_actual = kf0 / Qreac if Qreac > 0 else 0
            kr_actual = kr0 / Qprod if Qprod > 0 else 0
            Keq_actual = kf_actual / kr_actual if kr_actual > 0 else 0

            error = False; warning = False
            k_ratio = 1.0
            Keq_ratio = 1.0
            # Check that the forward rate coefficient is correct
            if kf_actual > 0:
                k_ratio = kf_expected / kf_actual
                # Rescale kf and kr so that we get kf_expected
                kf *= k_ratio
                kr *= k_ratio
                # Decide if the disagreement warrants a warning or error
                if 0.8 < k_ratio < 1.25:
                    # The difference is probably just due to numerical error
                    pass
                elif 0.5 < k_ratio < 2.0:
                    # Might be numerical error, but is pretty large, so warn
                    warning = True
                else:
                    # Disagreement is too large, so raise exception
                    error = True
                    
            # Check that the equilibrium constant is correct
            if Keq_actual > 0:
                Keq_ratio = Keq_expected / Keq_actual
                # Rescale kr so that we get Keq_expected
                kr /= Keq_ratio
                # In RMG jobs this never represents an error because we are
                # missing or using approximate degrees of freedom anyway
                if self.rmgmode:
                    pass
                # Decide if the disagreement warrants a warning or error
                elif 0.8 < Keq_ratio < 1.25:
                    # The difference is probably just due to numerical error
                    pass
                elif 0.5 < Keq_ratio < 2.0:
                    # Might be numerical error, but is pretty large, so warn
                    warning = True
                else:
                    # Disagreement is too large, so raise exception
                    error = True
                               
            if rxn.reactants[0] in isomers and rxn.products[0] in isomers:
                # Isomerization
                self.Kij[prod,reac,:,:] = kf
                self.Kij[reac,prod,:,:] = kr
            elif rxn.reactants[0] in isomers and rxn.products in reactants:
                # Dissociation (reversible)
                self.Gnj[prod-Nisom,reac,:,:] = kf
                self.Fim[reac,prod-Nisom,:,:] = kr
            elif rxn.reactants[0] in isomers and rxn.products in products:
                # Dissociation (irreversible)
                self.Gnj[prod-Nisom,reac,:,:] = kf
            elif rxn.reactants in reactants and rxn.products[0] in isomers:
                # Association (reversible)
                self.Fim[prod,reac-Nisom,:,:] = kf 
                self.Gnj[reac-Nisom,prod,:,:] = kr
            elif rxn.reactants in products and rxn.products[0] in isomers:
                # Association (irreversible)
                self.Gnj[reac-Nisom,prod,:,:] = kr
            else:
                raise NetworkError('Unexpected type of path reaction "{0}"'.format(rxn))

            # If the k(E) values are invalid (in that they give the wrong 
            # kf(T) or kr(T) when integrated), then raise an exception
            if error or warning:
                logging.warning('For path reaction {0!s}:'.format(rxn))
                logging.warning('    Expected kf({0:g} K) = {1:g}'.format(T, kf_expected))
                logging.warning('      Actual kf({0:g} K) = {1:g}'.format(T, kf_actual))
                logging.warning('    Expected Keq({0:g} K) = {1:g}'.format(T, Keq_expected))
                logging.warning('      Actual Keq({0:g} K) = {1:g}'.format(T, Keq_actual))
                if error:
                    raise InvalidMicrocanonicalRateError('Invalid k(E) values computed for path reaction "{0}".'.format(rxn), k_ratio, Keq_ratio)
                else:
                    logging.warning('Significant corrections to k(E) to be consistent with high-pressure limit for path reaction "{0}".'.format(rxn))

#        import pylab
#        for prod in range(Nisom):
#            for reac in range(prod):
#                pylab.semilogy(self.Elist*0.001, self.Kij[prod,reac,:])
#        for prod in range(Nreac+Nprod):
#            for reac in range(Nisom):
#                pylab.semilogy(self.Elist*0.001, self.Gnj[prod,reac,:])
#        pylab.show()

        return self.Kij, self.Gnj, self.Fim
Example #6
0
    def calculateRateCoefficients(self, Tlist, Plist, method, errorCheck=True):
        
        Nisom = len(self.isomers)
        Nreac = len(self.reactants)
        Nprod = len(self.products)

        for rxn in self.pathReactions:
            if len(rxn.transitionState.conformer.modes) > 0:
                logging.debug('Using RRKM theory to compute k(E) for path reaction {0}.'.format(rxn))
            elif rxn.kinetics is not None:
                logging.debug('Using ILT method to compute k(E) for path reaction {0}.'.format(rxn))
        logging.debug('')
        
        logging.info('Calculating phenomenological rate coefficients for {0}...'.format(rxn))
        K = numpy.zeros((len(Tlist),len(Plist),Nisom+Nreac+Nprod,Nisom+Nreac+Nprod), numpy.float64)
        
        for t, T in enumerate(Tlist):
            for p, P in enumerate(Plist):
                self.setConditions(T, P)
                
                # Apply method
                if method.lower() == 'modified strong collision':
                    self.applyModifiedStrongCollisionMethod()
                elif method.lower() == 'reservoir state':
                    self.applyReservoirStateMethod()
                elif method.lower() == 'chemically-significant eigenvalues':
                    self.applyChemicallySignificantEigenvaluesMethod()
                else:
                    raise NetworkError('Unknown method "{0}".'.format(method))

                K[t,p,:,:] = self.K
                
                # Check that the k(T,P) values satisfy macroscopic equilibrium
                eqRatios = self.eqRatios
                for i in range(Nisom+Nreac):
                    for j in range(i):
                        Keq0 = K[t,p,j,i] / K[t,p,i,j]
                        Keq = eqRatios[j] / eqRatios[i]
                        if Keq0 / Keq < 0.5 or Keq0 / Keq > 2.0:
                            if i < Nisom:
                                reactants = self.isomers[i]
                            elif i < Nisom+Nreac:
                                reactants = self.reactants[i-Nisom]
                            else:
                                reactants = self.products[i-Nisom-Nreac]
                            if j < Nisom:
                                products = self.isomers[j]
                            elif j < Nisom+Nreac:
                                products = self.reactants[j-Nisom]
                            else:
                                products = self.products[j-Nisom-Nreac]
                            reaction = Reaction(reactants=reactants.species[:], products=products.species[:])
                            logging.error('For net reaction {0!s}:'.format(reaction))
                            logging.error('Expected Keq({1:g} K, {2:g} bar) = {0:11.3e}'.format(Keq, T, P*1e-5))
                            logging.error('  Actual Keq({1:g} K, {2:g} bar) = {0:11.3e}'.format(Keq0, T, P*1e-5))
                            raise NetworkError('Computed k(T,P) values for reaction {0!s} do not satisfy macroscopic equilibrium.'.format(reaction))
                            
                # Reject if any rate coefficients are negative
                if errorCheck:
                    negativeRate = False
                    for i in range(Nisom+Nreac+Nprod):
                        for j in range(i):
                            if (K[t,p,i,j] < 0 or K[t,p,j,i] < 0) and not negativeRate:
                                negativeRate = True
                                logging.error('Negative rate coefficient generated; rejecting result.')
                                logging.info(K[t,p,0:Nisom+Nreac+Nprod,0:Nisom+Nreac])
                                K[t,p,:,:] = 0 * K[t,p,:,:]
                                self.K = 0 * self.K
        logging.debug('Finished calculating rate coefficients for network {0}.'.format(self.label))
        logging.debug('The nework now has values of {0}'.format(repr(self)))
        logging.debug('Master equation matrix found for network {0} is {1}'.format(self.label, K))
        return K