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
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)
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()
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
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