def AV(self, value): if self.cloud is None: if self.info is None: self._AV = value elif 'AV' in self.info: self.info['AV'] = value else: self._AV = value else: if self.info is None: raise despoticError( "cannot set AV directly unless it is part of info") elif 'AV' not in self.info: raise despoticError( "cannot set AV directly unless it is part of info") else: self.info['AV'] = value
def dxdt(self, xin, time): """ This routine returns the time rate of change of the abundances for all species in the network. Parameters xin : array array of starting abundances time : float current time in sec Returns dxdt : array the time derivative of all species abundances """ raise despoticError( "chemNetwork is an abstract class, " + "and should never be instantiated directly. Only " + "instantiate classes derived from it.")
def applyAbundances(self, addEmitters=False): """ This method writes abundances from the chemical network back to the cloud to which this network is attached. Parameters addEmitters : bool if True, and the network contains emitters that are not part of the parent cloud, then the network will attempt to add them using cloud.addEmitter. Otherwise this routine will change the abundances of whatever emitters are already attached to the cloud, but will not add new ones. Returns: Nothing """ raise despoticError( "chemNetwork is an abstract class, " + "and should never be instantiated directly. Only " + "instantiate classes derived from it.")
def setChemEq(cloud, tEqGuess=None, network=None, info=None, addEmitters=False, tol=1e-6, maxTime=1e16, verbose=False, smallabd=1e-15, convList=None, evolveTemp='fixed', isobaric=False, tempEqParam=None, dEdtParam=None, maxTempIter=50): """ Set the chemical abundances for a cloud to their equilibrium values, computed using a specified chemical netowrk. Parameters cloud : class cloud cloud on which computation is to be performed tEqGuess : float a guess at the timescale over which equilibrium will be achieved; if left unspecified, the code will attempt to estimate this time scale on its own network : chemNetwork class a valid chemNetwork class; this class must define the methods __init__, dxdt, and applyAbundances; if None, the existing chemical network for the cloud is used info : dict a dict of additional initialization information to be passed to the chemical network class when it is instantiated addEmitters : Boolean if True, emitters that are included in the chemical network but not in the cloud's existing emitter list will be added; if False, abundances of emitters already in the emitter list will be updated, but new emiters will not be added to the cloud evolveTemp : 'fixed' | 'iterate' | 'iterateDust' | 'gasEq' | 'fullEq' | 'evol' how to treat the temperature evolution during the chemical evolution: * 'fixed' = treat tempeature as fixed * 'iterate' = iterate between setting the gas temperature and chemistry to equilibrium * 'iterateDust' = iterate between setting the gas and dust temperatures and the chemistry to equilibrium * 'gasEq' = hold dust temperature fixed, set gas temperature to instantaneous equilibrium value as the chemistry evolves * 'fullEq' = set gas and dust temperatures to instantaneous equilibrium values while evolving the chemistry network * 'evol' = evolve gas temperature in time along with the chemistry, assuming the dust is always in instantaneous equilibrium isobaric : Boolean if set to True, the gas is assumed to be isobaric during the evolution (constant pressure); otherwise it is assumed to be isochoric; note that (since chemistry networks at present are not allowed to change the mean molecular weight), this option has no effect if evolveTemp is 'fixed' tempEqParam : None | dict if this is not None, then it must be a dict of values that will be passed as keyword arguments to the cloud.setTempEq, cloud.setGasTempEq, or cloud.setDustTempEq routines; only used if evolveTemp is not 'fixed' dEdtParam : None | dict if this is not None, then it must be a dict of values that will be passed as keyword arguments to the cloud.dEdt routine; only used if evolveTemp is 'evol' tol : float tolerance requirement on the equilibrium solution convList : list list of species to include when calculating tolerances to decide if network is converged; species not listed are not considered. If this is None, then all species are considered in deciding if the calculation is converged. smallabd : float abundances below smallabd are not considered when checking for convergence; set to 0 or a negative value to consider all abundances, but beware that this may result in false non-convergence due to roundoff error in very small abundances maxTempIter : int maximum number of iterations when iterating between chemistry and temperature; only used if evolveTemp is 'iterate' or 'iterateDust' verbose : Boolean if True, diagnostic information is printed as the calculation proceeds Returns converged : Boolean True if the calculation converged, False if not Raises despoticError, if network is None and the cloud does not already have a defined chemical network associated with it Remarks The final abundances are written to the cloud whether or not the calculation converges. """ # Check if we have been passed a new chemical network. If so, # initialize it and associate it with the cloud, unless it is the # same type as the current network; if not, make sure the cloud # has a network associated with it before proceeding. if network is not None: if not hasattr(cloud, 'chemnetwork'): cloud.chemnetwork = network(cloud=cloud, info=info) elif not isinstance(cloud.chemnetwork, network): cloud.chemnetwork = network(cloud=cloud, info=info) elif not hasattr(cloud, 'chemnetwork'): raise despoticError('if network is None, cloud must have' + ' an existing chemnetwork') # Get initial timesale estimate if we were not given one. Picking # this is very tricky, because chemical networks often involve # reactions with a wide range of timescales, and the rates of # change starting from arbitrary initial conditions may be highly # non-representative of those found elsewhere in parameter # space. To make a guess, we compute dx/dt at the initial # conditions and at a point slightly perturbed from them, and then # use the most restrictive timestep we find. if tEqGuess == None: # Print status if verbose: print("setChemEquil: estimating characteristic " + "equilibration timescale...") # Compute current time derivatives xdot1 = cloud.chemnetwork.dxdt(cloud.chemnetwork.x, 0.0) dt1 = np.amin( np.abs( (cloud.chemnetwork.x + max(smallabd, 0)) / (xdot1 + __small))) x2 = cloud.chemnetwork.x + dt1 * xdot1 xdot2 = cloud.chemnetwork.dxdt(x2, 0.0) dt2 = np.amin(np.abs((x2 + max(smallabd, 0)) / (xdot2 + __small))) # Use larger of the two xdot's to define a timescale, but # divide by 10 for safety, with a minimum of 10^7 sec tEqGuess = max(dt1, dt2) tEqGuess /= 10.0 tEqGuess = max(tEqGuess, 1e7) # If we're evolving the gas temperature too, estimate a # timescale for its evolution if evolveTemp == 'evol': if dEdtParam is None: rates = cloud.dEdt(gasOnly=True, sumOnly=True) else: dEdtParam1 = deepcopy(dEdtParam) dEdtParam1['gasOnly'] = True dEdtParam1['sumOnly'] = True rates = cloud.dEdt(**dEdtParam1) if isobaric: dTdt = rates['dEdtGas'] / \ ((cloud.comp.computeCv(cloud.Tg)+1)*kB) else: dTdt = rates['dEdtGas'] / \ (cloud.comp.computeCv(cloud.Tg)*kB) tEqGuess = max(tEqGuess, cloud.Tg / (np.abs(dTdt) + __small)) # Make sure tEqGuess doesn't exceed maxTime if tEqGuess > maxTime: tEqGuess = maxTime # Print status if verbose: print("setChemEquil: estimated equilibration timescale = " + str(tEqGuess) + " sec") # Decide which species we will consider in determining if # abundances are converged if convList == None: convList = cloud.chemnetwork.specList convArray = np.array( [cloud.chemnetwork.specList.index(spec) for spec in convList]) # If we're isobaric, save the isobar if isobaric: isobar = cloud.Tg * cloud.nH # Outer loop, if we're iterating between temperature and chemistry if evolveTemp == 'iterate' or evolveTemp == 'iterateDust': tempConverge = False else: tempConverge = True itCount = 0 while True: # Evolve the chemistry in time for estimated equilibrium # timescale and check convergence; if not converged, increase # time and keep running until we converge or maximum time is # reached. err = np.zeros(convArray.size) + 10.0 * tol t = 0.0 tEvol = tEqGuess lastCycle = False while True: # Evolve for specified time if evolveTemp != 'iterate' and evolveTemp != 'iterateDust': out = chemEvol(cloud, t + tEvol, tInit=t, nOut=3, evolveTemp=evolveTemp, isobaric=isobaric, tempEqParam=tempEqParam, dEdtParam=dEdtParam) else: out = chemEvol(cloud, t + tEvol, tInit=t, nOut=3, evolveTemp='fixed', addEmitters=addEmitters) xOut = np.array(out[1].values()) # Compute residual err = abs(xOut[convArray, -2] / (xOut[convArray, -1] + __small) - 1) # If smallabd is set, exclude species will small abundances # from the calculation if smallabd > 0.0: err[xOut[convArray, -1] < smallabd] = 0.1 * tol # Add temperature to residual if we're evolving it if evolveTemp == 'evol': TOut = xOut[2] err = np.append(err, abs(TOut[-2] / (TOut[-1] + __small) - 1)) # Print status if verbose: if evolveTemp != 'evol' or np.argmax(err) < len(err - 1): print( "setChemEquil: evolved from t = " + str(t) + " to " + str(t + tEvol) + " sec, residual = " + str(np.amax(err)) + " for species " + cloud.chemnetwork.specList[convArray[np.argmax(err)]]) else: print("setChemEquil: evolved from t = " + str(t) + " to " + str(t + tEvol) + " sec, residual = " + str(np.amax(err)) + " for temperature") # Check for convergence if np.amax(err) < tol: break # Update time and timestep, or break if we've exceed maximum # allowed time if t + tEvol < maxTime: t += tEvol tEvol *= 2.0 else: if lastCycle: break else: tEvol = maxTime - t lastCycle = True # Print status if verbose: if np.amax(err) < tol: ad = abundanceDict(cloud.chemnetwork.specList, cloud.chemnetwork.x) print("setChemEquil: abundances converged: " + str(ad)) else: print("setChemEquil: reached maximum time of " + str(maxTime) + " sec without converging") # Floor small negative abundances to avoid numerical problems idx = np.where( np.logical_and(cloud.chemnetwork.x <= 0.0, np.abs(cloud.chemnetwork.x) < smallabd)) cloud.chemnetwork.x[idx] = smallabd # If we failed to converge on the chemistry, bail out now if np.amax(err) >= tol: return False # Are we iterating on temperature? if not tempConverge: # Yes, so update temperature Tglast = cloud.Tg Tdlast = cloud.Td if evolveTemp == 'iterate': if tempEqParam is None: cloud.setGasTempEq() else: cloud.setGasTempEq(**tempEqParam) else: if tempEqParam is None: cloud.setTempEq() else: cloud.setTempEq(**tempEqParam) # If we're isobaric, also update the density if isobaric: cloud.nH = isobar / cloud.Tg # Check for temperature convergence resid = max(abs((cloud.Tg - Tglast) / cloud.Tg), abs((cloud.Td - Tdlast) / cloud.Td)) if resid < tol: tempConverge = True else: tempConverge = False # Print status if verbose: print(("setChemEquil: updated temperatures to " + "Tg = {:f}, Td = {:f}, residual = {:e}").format( cloud.Tg, cloud.Td, resid)) if tempConverge: print("Temperature converged!") # Break if we've also converged on the temperature if tempConverge: break # Update iteration counter and see if we have gone too many # times itCount += 1 if itCount > maxTempIter: break # Write results to the cloud if we converged if tempConverge: cloud.chemnetwork.applyAbundances(addEmitters=addEmitters) # Report on whether we converged return tempConverge
def chemEvol(cloud, tFin, tInit=0.0, nOut=100, dt=None, tOut=None, network=None, info=None, addEmitters=False, evolveTemp='fixed', isobaric=False, tempEqParam=None, dEdtParam=None): """ Evolve the abundances of a cloud using the specified chemical network. Parameters cloud : class cloud cloud on which computation is to be performed tFin : float end time of integration, in sec tInit : float start time of integration, in sec nOut : int number of times at which to report the temperature; this is ignored if dt or tOut are set dt : float time interval between outputs, in sec; this is ignored if tOut is set tOut : array list of times at which to output the temperature, in s; must be sorted in increasing order network : chemical network class a valid chemical network class; this class must define the methods __init__, dxdt, and applyAbundances; if None, the existing chemical network for the cloud is used info : dict a dict of additional initialization information to be passed to the chemical network class when it is instantiated addEmitters : Boolean if True, emitters that are included in the chemical network but not in the cloud's existing emitter list will be added; if False, abundances of emitters already in the emitter list will be updated, but new emiters will not be added to the cloud evolveTemp : 'fixed' | 'gasEq' | 'fullEq' | 'evol' how to treat the temperature evolution during the chemical evolution; 'fixed' = treat tempeature as fixed; 'gasEq' = hold dust temperature fixed, set gas temperature to instantaneous equilibrium value; 'fullEq' = set gas and dust temperatures to instantaneous equilibrium values; 'evol' = evolve gas temperature in time along with the chemistry, assuming the dust is always in instantaneous equilibrium isobaric : Boolean if set to True, the gas is assumed to be isobaric during the evolution (constant pressure); otherwise it is assumed to be isochoric; note that (since chemistry networks at present are not allowed to change the mean molecular weight), this option has no effect if evolveTemp is 'fixed' tempEqParam : None | dict if this is not None, then it must be a dict of values that will be passed as keyword arguments to the cloud.setTempEq, cloud.setGasTempEq, or cloud.setDustTempEq routines; only used if evolveTemp is not 'fixed' dEdtParam : None | dict if this is not None, then it must be a dict of values that will be passed as keyword arguments to the cloud.dEdt routine; only used if evolveTemp is 'evol' Returns time : array array of output times, in sec abundances : class abundanceDict an abundanceDict giving the abundances as a function of time Tg : array gas temperature as a function of time; returned only if evolveTemp is not 'fixed' Td : array dust temperature as a function of time; returned only if evolveTemp is not 'fixed' or 'gasEq' Raises despoticError, if network is None and the cloud does not already have a defined chemical network associated with it """ # Check if we have been passed a new chemical network. If so, # initialize it and associate it with the cloud, unless it is the # same type as the current network; if not, make sure the cloud # has a network associated with it before proceeding. if network is not None: if not hasattr(cloud, 'chemnetwork'): cloud.chemnetwork = network(cloud=cloud, info=info) elif not isinstance(cloud.chemnetwork, network): cloud.chemnetwork = network(cloud=cloud, info=info) elif not hasattr(cloud, 'chemnetwork'): raise despoticError( 'if network is None, cloud must have' + ' an existing chemnetwork') # Set up output times if tOut==None: if dt==None: tOut = tInit + np.arange(nOut+1)*float(tFin-tInit)/nOut else: tOut = np.arange(tInit, (tFin-tInit)*(1+1e-10), dt) # Sanity check on output times: eliminate any output times # that are not between tInit and tFin, and make sure final time is # tFin tOut1 = tOut[tOut >= tInit] tOut1 = tOut1[tOut1 <= tFin] if tOut1[-1] < tFin: tOut1 = np.append(tOut1, tFin) # Set the isobar if isobaric: isobar = cloud.Tg * cloud.nH else: isobar = -1 # See how we're handling the temperature if evolveTemp == 'fixed': # Simplest case: fixed temperature, so just evolve the # chemical network alone xOut = odeint(cloud.chemnetwork.dxdt, cloud.chemnetwork.x, tOut1) elif evolveTemp == 'gasEq': # We're evolving the temperature as well as the chemical # abundances, but we're doing so assuming the temperature is # always in equilibrium; we therefore use our wrapper class, # defined below dxdtwrap = _dxdt_wrapper(cloud, isobar, gasOnly=True, tempEqParam=tempEqParam) xOut = odeint(dxdtwrap.dxdt_Teq, cloud.chemnetwork.x, tOut1) # Go back and compute the equilibrium gas temperature at each # of the requested output times Tg = np.zeros(len(tOut1)) for i in range(Tg.size): cloud.chemnetwork.x = xOut[i,:] cloud.chemnetwork.applyAbundances() if tempEqParam is not None: cloud.setGasTempEq(**tempEqParam) else: cloud.setGasTempEq() Tg[i] = cloud.Tg elif evolveTemp == 'fullEq': # Same as the gasEq case, but now we set but the gas and dust # temperature to equilibrium dxdtwrap = _dxdt_wrapper(cloud, isobar, gasOnly=False, tempEqParam=tempEqParam) xOut = odeint(dxdtwrap.dxdt_Teq, cloud.chemnetwork.x, tOut1) # Go back and compute the equilibrium gas and dust # temperatures at each of the requested output times Tg = np.zeros(len(tOut1)) Td = np.zeros(len(tOut1)) for i in range(Tg.size): cloud.chemnetwork.x = xOut[i,:] cloud.chemnetwork.applyAbundances() if tempEqParam is not None: cloud.setTempEq(**tempEqParam) else: cloud.setTempEq() Tg[i] = cloud.Tg Td[i] = cloud.Td elif evolveTemp == 'evol': # Evolve the chemistry, gas, and dust temperatures # simultaneously dxdtwrap = _dxdt_wrapper(cloud, isobar, tempEqParam=tempEqParam, dEdtParam=dEdtParam) xTInit = np.append(cloud.chemnetwork.x, cloud.Tg) xTOut = odeint(dxdtwrap.dxTdt, xTInit, tOut1) xOut = xTOut[:,:-1] Tg = xTOut[:,-1] # Go back and compute equilibrium dust temperatures at each # output time Td = np.zeros(Tg.size) for i in range(Tg.size): cloud.chemnetwork.x = xOut[i,:] cloud.chemnetwork.applyAbundances() if tempEqParam is not None: cloud.setDustTempEq(**tempEqParam) else: cloud.setDustTempEq() Td[i] = cloud.Td else: raise despoticError( 'chemEvol: invalid option ' + str(evolveTemp) + 'for evolveTemp') # Write final results to chemnetwork cloud.chemnetwork.x = xOut[-1,:] # Write final abundances back to cloud cloud.chemnetwork.applyAbundances(addEmitters=addEmitters) # If the final time was not one of the requested output times, # chop it off the data to be returned if np.sum(tFin == tOut): xOut = xOut[:-1,:] if evolveTemp != 'fixed': Tg = Tg[:-1] if evolveTemp == 'evol' or evolveTemp == 'fullEq': Td = Td[:-1] # Return output if evolveTemp == 'fixed': return tOut, abundanceDict(cloud.chemnetwork.specList, np.transpose(xOut)), elif evolveTemp == 'gasEq': return tOut, abundanceDict(cloud.chemnetwork.specList, np.transpose(xOut)), Tg else: return tOut, abundanceDict(cloud.chemnetwork.specList, np.transpose(xOut)), Tg, Td
def cfac(self, value): raise despoticError( "cannot set cfac directly; set sigmaNT or temp instead")
def __init__(self, cloud=None, info=None): """ Parameters cloud : class cloud a DESPOTIC cloud object from which initial data are to be taken info : dict a dict containing additional parameters Remarks The dict info may contain the following key - value pairs: 'xC' : float the total C abundance per H nucleus; defaults to 2.0e-4 'xO' : float the total H abundance per H nucleus; defaults to 4.0e-4 'xM' : float the total refractory metal abundance per H nucleus; defaults to 2.0e-7 'sigmaDustV' : float V band dust extinction cross section per H nucleus; if not set, the default behavior is to assume that sigmaDustV = 0.4 * cloud.dust.sigmaPE 'AV' : float total visual extinction; ignored if sigmaDustV is set 'noClump' : bool if True, the clump factor is set to 1.0; defaults to False """ # List of species for this network; provide a pointer here so # that it can be accessed through the class self.specList = specList self.specListExtended = specListExtended # Store the input info dict self.info = info # Array to hold abundances self.x = np.zeros(10) # Total metal abundance if info is None: self.xM = _xMdefault else: if 'xM' in info: self.xM = info['xM'] else: self.xM = _xMdefault # Extract information from the cloud if one is given if cloud is None: # No cloud given, so set some defaults self.cloud = None # Physical properties self._xHe = _xHedefault self._ionRate = 2.0e-17 self._NH = _small self._temp = _small self._chi = 1.0 self._nH = _small self._AV = 0.0 if info is not None: if 'AV' in info: self._AV = info['AV'] # Set initial abundances if info is None: self.x[6] = _xCdefault self.x[8] = _xOdefault else: if 'xC' in info: self.x[6] = info['xC'] else: self.x[6] = _xCdefault if 'xO' in info: self.x[8] = info['xO'] else: self.x[8] = _xOdefault self.x[9] = self.xM else: # Cloud is given, so get information out of it self.cloud = cloud # Sanity check: make sure cloud is pure H2 if cloud.comp.xH2 != 0.5: raise despoticError( "NL99 network only valid " + "for pure H2 composition") # Sanity check: make sure cloud contains some He, since # network will not function properly at He abundance of 0 if cloud.comp.xHe == 0.0: raise despoticError( "NL99 network requires " + "non-zero He abundance") # Set abundances # Make a case-insensitive version of the emitter list for # convenience try: emList = dict(zip(map(string.lower, cloud.emitters.keys()), cloud.emitters.values())) except: # This somewhat more bulky construction is required in # python 3 lowkeys = [k.lower() for k in cloud.emitters.keys()] lowvalues = list(cloud.emitters.values()) emList = dict(zip(lowkeys, lowvalues)) # OH and H2O if 'oh' in emList: self.x[2] += emList['oh'].abundance if 'ph2o' in emList: self.x[2] += emList['ph2o'].abundance if 'oh2o' in emList: self.x[2] += emList['oh2o'].abundance if 'p-h2o' in emList: self.x[2] += emList['p-h2o'].abundance if 'o-h2o' in emList: self.x[2] += emList['o-h2o'].abundance # CO if 'co' in emList: self.x[4] = emList['co'].abundance # Neutral carbon if 'c' in emList: self.x[5] = emList['c'].abundance # Ionized carbon if 'c+' in emList: self.x[6] = emList['c+'].abundance # HCO+ if 'hco+' in emList: self.x[7] = emList['hco+'].abundance # Sum input abundances of C, C+, CO, HCO+ to ensure that # all carbon is accounted for. If there is too little, # assume the excess is C+. If there is too much, throw an # error. if info is None: xC = _xCdefault elif 'xC' in info: xC = info['xC'] else: xC = _xCdefault xCtot = self.x[4] + self.x[5] + self.x[6] + self.x[7] if xCtot < xC: # Print warning if we're altering existing C+ # abundance. if 'c' in emList: print("Warning: input C abundance is " + str(xC) + ", but total input C, C+, CHx, CO, " + "HCO+ abundance is " + str(xCtot) + "; increasing xC+ to " + str(self.x[6]+xC-xCtot)) self.x[6] += xC - xCtot elif xCtot > xC: # Throw an error if input C abundance is smaller than # what is accounted for in initial conditions raise despoticError( "input C abundance is " + str(xC) + ", but total input C, C+, CHx, CO, " + "HCO+ abundance is " + str(xCtot)) # O if 'o' in emList: self.x[8] = emList['o'].abundance elif info is None: self.x[8] = _xOdefault - self.x[2] - self.x[4] - \ self.x[7] elif 'xO' in info: self.x[8] = info['xO'] - self.x[2] - self.x[4] - \ self.x[7] else: self.x[8] = _xOdefault - self.x[2] - self.x[4] - \ self.x[7] # As with C, make sure all O is accounted for, and if not # park the extra in OI if info is None: xO = _xOdefault elif 'xC' in info: xO = info['xO'] else: xO = _xOdefault xOtot = self.x[2] + self.x[4] + self.x[7] + self.x[8] if xOtot < xO: # Print warning if we're altering existing O # abundance. if 'o' in emList: print("Warning: input O abundance is " + str(xO) + ", but total input O, OHx, CO, " + "HCO+ abundance is " + str(xOtot) + "; increasing xO to " + str(self.x[8]+xO-xOtot)) self.x[8] += xO - xOtot elif xOtot > xO: # Throw an error if input O abundance is smaller than # what is accounted for in initial conditions raise despoticError( "input C abundance is " + str(xO) + ", but total input O, OHx, CO, " + "HCO+ abundance is " + str(xOtot)) # Initial electrons = metals + C+ + HCO+ xeinit = self.xM + self.x[6] + self.x[7] # Initial He+ self.x[0] = self.xHe*self.ionRate / \ (self.nH*(_k2[9]*self.temp**_k2Texp[9]*xeinit+_k2[3]*_xH2)) # Initial H3+ self.x[1] = _xH2*self.ionRate / \ (self.nH*(_k2[10]*self.temp**_k2Texp[10]*xeinit+_k2[2]*self.x[8])) # Initial M+ self.x[9] = self.xM
def __init__(self, cloud=None, info=None): raise despoticError( "chemNetwork is an abstract class, " + "and should never be instantiated directly. Only " + "instantiate classes derived from it.")
def chemEvol(cloud, tFin, tInit=0.0, nOut=100, dt=None, tOut=None, network=None, info=None, addEmitters=False, evolveTemp='fixed', isobaric=False, tempEqParam=None, dEdtParam=None): """ Evolve the abundances of a cloud using the specified chemical network. Parameters cloud : class cloud cloud on which computation is to be performed tFin : float end time of integration, in sec tInit : float start time of integration, in sec nOut : int number of times at which to report the temperature; this is ignored if dt or tOut are set dt : float time interval between outputs, in sec; this is ignored if tOut is set tOut : array list of times at which to output the temperature, in s; must be sorted in increasing order network : chemical network class a valid chemical network class; this class must define the methods __init__, dxdt, and applyAbundances; if None, the existing chemical network for the cloud is used info : dict a dict of additional initialization information to be passed to the chemical network class when it is instantiated addEmitters : Boolean if True, emitters that are included in the chemical network but not in the cloud's existing emitter list will be added; if False, abundances of emitters already in the emitter list will be updated, but new emiters will not be added to the cloud evolveTemp : 'fixed' | 'gasEq' | 'fullEq' | 'evol' how to treat the temperature evolution during the chemical evolution; 'fixed' = treat tempeature as fixed; 'gasEq' = hold dust temperature fixed, set gas temperature to instantaneous equilibrium value; 'fullEq' = set gas and dust temperatures to instantaneous equilibrium values; 'evol' = evolve gas temperature in time along with the chemistry, assuming the dust is always in instantaneous equilibrium isobaric : Boolean if set to True, the gas is assumed to be isobaric during the evolution (constant pressure); otherwise it is assumed to be isochoric; note that (since chemistry networks at present are not allowed to change the mean molecular weight), this option has no effect if evolveTemp is 'fixed' tempEqParam : None | dict if this is not None, then it must be a dict of values that will be passed as keyword arguments to the cloud.setTempEq, cloud.setGasTempEq, or cloud.setDustTempEq routines; only used if evolveTemp is not 'fixed' dEdtParam : None | dict if this is not None, then it must be a dict of values that will be passed as keyword arguments to the cloud.dEdt routine; only used if evolveTemp is 'evol' Returns time : array array of output times, in sec abundances : class abundanceDict an abundanceDict giving the abundances as a function of time Tg : array gas temperature as a function of time; returned only if evolveTemp is not 'fixed' Td : array dust temperature as a function of time; returned only if evolveTemp is not 'fixed' or 'gasEq' Raises despoticError, if network is None and the cloud does not already have a defined chemical network associated with it """ # Check if we have been passed a new chemical network. If so, # initialize it and associate it with the cloud, unless it is the # same type as the current network; if not, make sure the cloud # has a network associated with it before proceeding. if network is not None: if not hasattr(cloud, 'chemnetwork'): cloud.chemnetwork = network(cloud=cloud, info=info) elif not isinstance(cloud.chemnetwork, network): cloud.chemnetwork = network(cloud=cloud, info=info) elif not hasattr(cloud, 'chemnetwork'): raise despoticError('if network is None, cloud must have' + ' an existing chemnetwork') # Set up output times if tOut == None: if dt == None: tOut = tInit + np.arange(nOut + 1) * float(tFin - tInit) / nOut else: tOut = np.arange(tInit, (tFin - tInit) * (1 + 1e-10), dt) # Sanity check on output times: eliminate any output times # that are not between tInit and tFin, and make sure final time is # tFin tOut1 = tOut[tOut >= tInit] tOut1 = tOut1[tOut1 <= tFin] if tOut1[-1] < tFin: tOut1 = np.append(tOut1, tFin) # Set the isobar if isobaric: isobar = cloud.Tg * cloud.nH else: isobar = -1 # See how we're handling the temperature if evolveTemp == 'fixed': # Simplest case: fixed temperature, so just evolve the # chemical network alone xOut = odeint(cloud.chemnetwork.dxdt, cloud.chemnetwork.x, tOut1) elif evolveTemp == 'gasEq': # We're evolving the temperature as well as the chemical # abundances, but we're doing so assuming the temperature is # always in equilibrium; we therefore use our wrapper class, # defined below dxdtwrap = _dxdt_wrapper(cloud, isobar, gasOnly=True, tempEqParam=tempEqParam) xOut = odeint(dxdtwrap.dxdt_Teq, cloud.chemnetwork.x, tOut1) # Go back and compute the equilibrium gas temperature at each # of the requested output times Tg = np.zeros(len(tOut1)) for i in range(Tg.size): cloud.chemnetwork.x = xOut[i, :] cloud.chemnetwork.applyAbundances() if tempEqParam is not None: cloud.setGasTempEq(**tempEqParam) else: cloud.setGasTempEq() Tg[i] = cloud.Tg elif evolveTemp == 'fullEq': # Same as the gasEq case, but now we set but the gas and dust # temperature to equilibrium dxdtwrap = _dxdt_wrapper(cloud, isobar, gasOnly=False, tempEqParam=tempEqParam) xOut = odeint(dxdtwrap.dxdt_Teq, cloud.chemnetwork.x, tOut1) # Go back and compute the equilibrium gas and dust # temperatures at each of the requested output times Tg = np.zeros(len(tOut1)) Td = np.zeros(len(tOut1)) for i in range(Tg.size): cloud.chemnetwork.x = xOut[i, :] cloud.chemnetwork.applyAbundances() if tempEqParam is not None: cloud.setTempEq(**tempEqParam) else: cloud.setTempEq() Tg[i] = cloud.Tg Td[i] = cloud.Td elif evolveTemp == 'evol': # Evolve the chemistry, gas, and dust temperatures # simultaneously dxdtwrap = _dxdt_wrapper(cloud, isobar, tempEqParam=tempEqParam, dEdtParam=dEdtParam) xTInit = np.append(cloud.chemnetwork.x, cloud.Tg) xTOut = odeint(dxdtwrap.dxTdt, xTInit, tOut1) xOut = xTOut[:, :-1] Tg = xTOut[:, -1] # Go back and compute equilibrium dust temperatures at each # output time Td = np.zeros(Tg.size) for i in range(Tg.size): cloud.chemnetwork.x = xOut[i, :] cloud.chemnetwork.applyAbundances() if tempEqParam is not None: cloud.setDustTempEq(**tempEqParam) else: cloud.setDustTempEq() Td[i] = cloud.Td else: raise despoticError('chemEvol: invalid option ' + str(evolveTemp) + 'for evolveTemp') # Write final results to chemnetwork cloud.chemnetwork.x = xOut[-1, :] # Write final abundances back to cloud cloud.chemnetwork.applyAbundances(addEmitters=addEmitters) # If the final time was not one of the requested output times, # chop it off the data to be returned if np.sum(tFin == tOut): xOut = xOut[:-1, :] if evolveTemp != 'fixed': Tg = Tg[:-1] if evolveTemp == 'evol' or evolveTemp == 'fullEq': Td = Td[:-1] # Return output if evolveTemp == 'fixed': return tOut, abundanceDict(cloud.chemnetwork.specList, np.transpose(xOut)), elif evolveTemp == 'gasEq': return tOut, abundanceDict(cloud.chemnetwork.specList, np.transpose(xOut)), Tg else: return tOut, abundanceDict(cloud.chemnetwork.specList, np.transpose(xOut)), Tg, Td
def setChemEq(cloud, tEqGuess=None, network=None, info=None, addEmitters=False, tol=1e-6, maxTime=1e16, verbose=False, smallabd=1e-15, convList=None, evolveTemp='fixed', isobaric=False, tempEqParam=None, dEdtParam=None, maxTempIter=50): """ Set the chemical abundances for a cloud to their equilibrium values, computed using a specified chemical netowrk. Parameters cloud : class cloud cloud on which computation is to be performed tEqGuess : float a guess at the timescale over which equilibrium will be achieved; if left unspecified, the code will attempt to estimate this time scale on its own network : chemNetwork class a valid chemNetwork class; this class must define the methods __init__, dxdt, and applyAbundances; if None, the existing chemical network for the cloud is used info : dict a dict of additional initialization information to be passed to the chemical network class when it is instantiated addEmitters : Boolean if True, emitters that are included in the chemical network but not in the cloud's existing emitter list will be added; if False, abundances of emitters already in the emitter list will be updated, but new emiters will not be added to the cloud evolveTemp : 'fixed' | 'iterate' | 'iterateDust' | 'gasEq' | 'fullEq' | 'evol' how to treat the temperature evolution during the chemical evolution: * 'fixed' = treat tempeature as fixed * 'iterate' = iterate between setting the gas temperature and chemistry to equilibrium * 'iterateDust' = iterate between setting the gas and dust temperatures and the chemistry to equilibrium * 'gasEq' = hold dust temperature fixed, set gas temperature to instantaneous equilibrium value as the chemistry evolves * 'fullEq' = set gas and dust temperatures to instantaneous equilibrium values while evolving the chemistry network * 'evol' = evolve gas temperature in time along with the chemistry, assuming the dust is always in instantaneous equilibrium isobaric : Boolean if set to True, the gas is assumed to be isobaric during the evolution (constant pressure); otherwise it is assumed to be isochoric; note that (since chemistry networks at present are not allowed to change the mean molecular weight), this option has no effect if evolveTemp is 'fixed' tempEqParam : None | dict if this is not None, then it must be a dict of values that will be passed as keyword arguments to the cloud.setTempEq, cloud.setGasTempEq, or cloud.setDustTempEq routines; only used if evolveTemp is not 'fixed' dEdtParam : None | dict if this is not None, then it must be a dict of values that will be passed as keyword arguments to the cloud.dEdt routine; only used if evolveTemp is 'evol' tol : float tolerance requirement on the equilibrium solution convList : list list of species to include when calculating tolerances to decide if network is converged; species not listed are not considered. If this is None, then all species are considered in deciding if the calculation is converged. smallabd : float abundances below smallabd are not considered when checking for convergence; set to 0 or a negative value to consider all abundances, but beware that this may result in false non-convergence due to roundoff error in very small abundances maxTempIter : int maximum number of iterations when iterating between chemistry and temperature; only used if evolveTemp is 'iterate' or 'iterateDust' verbose : Boolean if True, diagnostic information is printed as the calculation proceeds Returns converged : Boolean True if the calculation converged, False if not Raises despoticError, if network is None and the cloud does not already have a defined chemical network associated with it Remarks The final abundances are written to the cloud whether or not the calculation converges. """ # Check if we have been passed a new chemical network. If so, # initialize it and associate it with the cloud, unless it is the # same type as the current network; if not, make sure the cloud # has a network associated with it before proceeding. if network is not None: if not hasattr(cloud, 'chemnetwork'): cloud.chemnetwork = network(cloud=cloud, info=info) elif not isinstance(cloud.chemnetwork, network): cloud.chemnetwork = network(cloud=cloud, info=info) elif not hasattr(cloud, 'chemnetwork'): raise despoticError( 'if network is None, cloud must have' + ' an existing chemnetwork') # Get initial timesale estimate if we were not given one. Picking # this is very tricky, because chemical networks often involve # reactions with a wide range of timescales, and the rates of # change starting from arbitrary initial conditions may be highly # non-representative of those found elsewhere in parameter # space. To make a guess, we compute dx/dt at the initial # conditions and at a point slightly perturbed from them, and then # use the most restrictive timestep we find. if tEqGuess == None: # Print status if verbose: print("setChemEquil: estimating characteristic " + "equilibration timescale...") # Compute current time derivatives xdot1 = cloud.chemnetwork.dxdt(cloud.chemnetwork.x, 0.0) dt1 = np.amin(np.abs((cloud.chemnetwork.x+max(smallabd,0)) / (xdot1+__small))) x2 = cloud.chemnetwork.x + dt1*xdot1 xdot2 = cloud.chemnetwork.dxdt(x2, 0.0) dt2 = np.amin(np.abs((x2+max(smallabd,0)) / (xdot2+__small))) # Use larger of the two xdot's to define a timescale, but # divide by 10 for safety, with a minimum of 10^7 sec tEqGuess = max(dt1, dt2) tEqGuess /= 10.0 tEqGuess = max(tEqGuess, 1e7) # If we're evolving the gas temperature too, estimate a # timescale for its evolution if evolveTemp == 'evol': if dEdtParam is None: rates = cloud.dEdt(gasOnly=True, sumOnly=True) else: dEdtParam1 = deepcopy(dEdtParam) dEdtParam1['gasOnly'] = True dEdtParam1['sumOnly'] = True rates = cloud.dEdt(**dEdtParam1) if isobaric: dTdt = rates['dEdtGas'] / \ ((cloud.comp.computeCv(cloud.Tg)+1)*kB) else: dTdt = rates['dEdtGas'] / \ (cloud.comp.computeCv(cloud.Tg)*kB) tEqGuess = max(tEqGuess, cloud.Tg/(np.abs(dTdt)+__small)) # Make sure tEqGuess doesn't exceed maxTime if tEqGuess > maxTime: tEqGuess = maxTime # Print status if verbose: print("setChemEquil: estimated equilibration timescale = " + str(tEqGuess) + " sec") # Decide which species we will consider in determining if # abundances are converged if convList == None: convList = cloud.chemnetwork.specList convArray = np.array([cloud.chemnetwork.specList.index(spec) for spec in convList]) # If we're isobaric, save the isobar if isobaric: isobar = cloud.Tg * cloud.nH # Outer loop, if we're iterating between temperature and chemistry if evolveTemp == 'iterate' or evolveTemp == 'iterateDust': tempConverge = False else: tempConverge = True itCount = 0 while True: # Evolve the chemistry in time for estimated equilibrium # timescale and check convergence; if not converged, increase # time and keep running until we converge or maximum time is # reached. err = np.zeros(convArray.size)+10.0*tol t = 0.0 tEvol = tEqGuess lastCycle = False while True: # Evolve for specified time if evolveTemp != 'iterate' and evolveTemp != 'iterateDust': out = chemEvol(cloud, t+tEvol, tInit=t, nOut=3, evolveTemp=evolveTemp, isobaric=isobaric, tempEqParam=tempEqParam, dEdtParam=dEdtParam) else: out = chemEvol(cloud, t+tEvol, tInit=t, nOut=3, evolveTemp='fixed', addEmitters=addEmitters) xOut = np.array(out[1].values()) # Compute residual err = abs(xOut[convArray,-2]/(xOut[convArray,-1]+__small)-1) # If smallabd is set, exclude species will small abundances # from the calculation if smallabd > 0.0: err[xOut[convArray,-1] < smallabd] = 0.1*tol # Add temperature to residual if we're evolving it if evolveTemp == 'evol': TOut = xOut[2] err = np.append(err, abs(TOut[-2]/(TOut[-1]+__small)-1)) # Print status if verbose: if evolveTemp != 'evol' or np.argmax(err) < len(err-1): print( "setChemEquil: evolved from t = " + str(t) + " to "+str(t+tEvol)+" sec, residual = " + str(np.amax(err)) + " for species " + cloud.chemnetwork.specList[convArray[np.argmax(err)]]) else: print( "setChemEquil: evolved from t = " + str(t) + " to "+str(t+tEvol)+" sec, residual = " + str(np.amax(err)) + " for temperature") # Check for convergence if np.amax(err) < tol: break # Update time and timestep, or break if we've exceed maximum # allowed time if t + tEvol < maxTime: t += tEvol tEvol *= 2.0 else: if lastCycle: break else: tEvol = maxTime-t lastCycle = True # Print status if verbose: if np.amax(err) < tol: ad = abundanceDict(cloud.chemnetwork.specList, cloud.chemnetwork.x) print( "setChemEquil: abundances converged: " + str(ad)) else: print( "setChemEquil: reached maximum time of " + str(maxTime) + " sec without converging") # Floor small negative abundances to avoid numerical problems idx = np.where(np.logical_and(cloud.chemnetwork.x <= 0.0, np.abs(cloud.chemnetwork.x) < smallabd)) cloud.chemnetwork.x[idx] = smallabd # If we failed to converge on the chemistry, bail out now if np.amax(err) >= tol: return False # Are we iterating on temperature? if not tempConverge: # Yes, so update temperature Tglast = cloud.Tg Tdlast = cloud.Td if evolveTemp == 'iterate': if tempEqParam is None: cloud.setGasTempEq() else: cloud.setGasTempEq(**tempEqParam) else: if tempEqParam is None: cloud.setTempEq() else: cloud.setTempEq(**tempEqParam) # If we're isobaric, also update the density if isobaric: cloud.nH = isobar / cloud.Tg # Check for temperature convergence resid = max(abs((cloud.Tg-Tglast)/cloud.Tg), abs((cloud.Td-Tdlast)/cloud.Td)) if resid < tol: tempConverge = True else: tempConverge = False # Print status if verbose: print("setChemEquil: updated temperatures to " + "Tg = {:f}, Td = {:f}, residual = {:e}"). \ format(cloud.Tg, cloud.Td, resid) if tempConverge: print("Temperature converged!") # Break if we've also converged on the temperature if tempConverge: break # Update iteration counter and see if we have gone too many # times itCount += 1 if itCount > maxTempIter: break # Write results to the cloud if we converged if tempConverge: cloud.chemnetwork.applyAbundances(addEmitters=addEmitters) # Report on whether we converged return tempConverge
def cfac(self, value): raise despoticError("cannot set cfac directly")
def __init__(self, cloud=None, info=None): """ Parameters cloud : class cloud a DESPOTIC cloud object from which initial data are to be taken info : dict a dict containing additional parameters Returns Nothing Remarks The dict info may contain the following key - value pairs: 'xC' : float the total C abundance per H nucleus; defaults to 2.0e-4 'xO' : float the total H abundance per H nucleus; defaults to 4.0e-4 'xM' : float the total refractory metal abundance per H nucleus; defaults to 2.0e-7 'Zd' : float dust abundance in solar units; defaults to 1.0 'sigmaDustV' : float V band dust extinction cross section per H nucleus; if not set, the default behavior is to assume that sigmaDustV = 0.4 * cloud.dust.sigmaPE 'AV' : float total visual extinction; ignored if sigmaDustV is set 'noClump' : bool if True, the clumping factor is set to 1.0; defaults to False 'sigmaNT' : float non-thermal velocity dispersion 'temp' : float gas kinetic temperature """ # List of species for this network; provide a pointer here so # that it can be accessed through the class self.specList = specList self.specListExtended = specListExtended # Store the input info dict self.info = info # Array to hold abundances; wrap it in an abundanceDict for # convenience self.x = np.zeros(len(specList)) abd = abundanceDict(specList, self.x) # Total metal abundance if info is None: self.xM = _xMdefault else: if 'xM' in info: self.xM = info['xM'] else: self.xM = _xMdefault # Extract information from the cloud if one is given if cloud is None: # No cloud given, so set some defaults self.cloud = None # Physical properties self._xHe = _xHedefault self._ionRate = 2.0e-17 self._NH = _small self._temp = _small self._chi = 1.0 self._nH = _small self._AV = 0.0 self._sigmaNT = _small self._Zd = _Zddefault if info is not None: if 'AV' in info.keys(): self._AV = info['AV'] if 'sigmaNT' in info.keys(): self._sigmaNT = info['sigmaNT'] if 'Zd' in info.keys(): self._Zd = info['Zd'] if 'Tg' in info.keys(): self._temp = info['Tg'] if 'Td' in info.keys(): self._Td = info['Td'] # Set initial abundances if info is None: # If not specied, start all hydrogen as H, all C as C+, # all O as OI abd['C+'] = _xCdefault abd['O'] = _xOdefault else: if 'xC' in info.keys(): abd['C+'] = info['xC'] else: abd['C+'] = _xCdefault if 'xO' in info.keys(): abd['O'] = info['xO'] else: abd['O'] = _xOdefault abd['M+'] = self.xM else: # Cloud is given, so get information out of it self.cloud = cloud # Sanity check: make sure cloud contains some He, since # network will not function properly at He abundance of 0 if cloud.comp.xHe == 0.0: raise despoticError( "NL99_GC network requires " + "non-zero He abundance") # Set abundances # Make a case-insensitive version of the emitter list for # convenience try: # This construction is elegant, but it relies on the # existence of a freestanding string.lower function, # which has been removed in python 3 emList = dict(zip(map(string.lower, cloud.emitters.keys()), cloud.emitters.values())) except: # This somewhat more bulky construction is required in # python 3 lowkeys = [k.lower() for k in cloud.emitters.keys()] lowvalues = list(cloud.emitters.values()) emList = dict(zip(lowkeys, lowvalues)) # Hydrogen abd['H+'] = cloud.comp.xHplus abd['H2'] = cloud.comp.xpH2 + cloud.comp.xoH2 # OH and H2O if 'oh' in emList: abd['OHx'] += emList['oh'].abundance if 'ph2o' in emList: abd['OHx'] += emList['ph2o'].abundance if 'oh2o' in emList: abd['OHx'] += emList['oh2o'].abundance if 'p-h2o' in emList: abd['OHx'] += emList['p-h2o'].abundance if 'o-h2o' in emList: abd['OHx'] += emList['o-h2o'].abundance # CO if 'co' in emList: abd['CO'] = emList['co'].abundance # Neutral carbon if 'c' in emList: abd['C'] = emList['c'].abundance # Ionized carbon if 'c+' in emList: abd['C+'] = emList['c+'].abundance # HCO+ if 'hco+' in emList: abd['HCO+'] = emList['hco+'].abundance # Sum input abundances of C, C+, CO, HCO+ to ensure that # all carbon is accounted for. If there is too little, # assume the excess is C+. If there is too much, throw an # error. if info is None: xC = _xCdefault elif 'xC' in info: xC = info['xC'] else: xC = _xCdefault xCtot = abd['CO'] + abd['C'] + abd['C+'] + abd['HCO+'] if xCtot < xC: # Print warning if we're altering existing C+ # abundance. if 'c' in emList: print("Warning: input C abundance is " + str(xC) + ", but total input C, C+, CHx, CO, " + "HCO+ abundance is " + str(xCtot) + "; increasing xC+ to " + str(abd['C+']+xC-xCtot)) abd['C+'] += xC - xCtot elif xCtot > xC: # Throw an error if input C abundance is smaller than # what is accounted for in initial conditions raise despoticError( "input C abundance is " + str(xC) + ", but total input C, C+, CHx, CO, " + "HCO+ abundance is " + str(xCtot)) # O if 'o' in emList: abd['O'] = emList['o'].abundance elif info is None: abd['O'] = _xOdefault - abd['OHx'] - abd['CO'] - \ abd['HCO+'] elif 'xO' in info: abd['O'] = info['xO'] - abd['OHx'] - abd['CO'] - \ abd['HCO+'] else: abd['O'] = _xOdefault - abd['OHx'] - abd['CO'] - \ abd['HCO+'] # As with C, make sure all O is accounted for, and if not # park the extra in OI if info is None: xO = _xOdefault elif 'xC' in info: xO = info['xO'] else: xO = _xOdefault xOtot = abd['OHx'] + abd['CO'] + abd['HCO+'] + abd['O'] if xOtot < xO: # Print warning if we're altering existing O # abundance. if 'o' in emList: print("Warning: input O abundance is " + str(xO) + ", but total input O, OHx, CO, " + "HCO+ abundance is " + str(xOtot) + "; increasing xO to " + str(abd['O']+xO-xOtot)) abd['O'] += xO - xOtot elif xOtot > xO: # Throw an error if input O abundance is smaller than # what is accounted for in initial conditions raise despoticError( "input O abundance is " + str(xO) + ", but total input O, OHx, CO, " + "HCO+ abundance is " + str(xOtot)) # Finally, make sure all H nuclei are accounted for and # that all hydrogenic abundances are >= 0 abd1 = self.abundances xH = abd1['H+'] + abd1['OHx'] + abd1['CHx'] + abd1['HCO+'] + \ abd1['H'] + 2*abd1['H2'] + 3*abd1['H3+'] if (abs(xH-1.0) > 1.0e-8): raise despoticError( "input hydrogen abundances " + "add up to xH = "+str(xH)+" != 1!") if (abd1['H+'] < 0) or \ (abd1['OHx'] < 0) or (abd1['CHx'] < 0) or \ (abd1['HCO+'] < 0) or (abd1['H'] < 0) or \ (abd1['H2'] < 0) or (abd1['H3+'] < 0): raise despoticError( "abundances of some " + "hydrogenic species are < 0; abundances " + "are: " + repr(abd1)) # Get rate coefficients in starting state; we will use these # to put some species close to equilibrium initially abd1 = self.abundances cfac = self.cfac n = self.nH * cfac rcoef = _twobody_ratecoef( self.temp, self.cloud.Td, n, abd1['H2']*n, abd1['e-']*n, np.exp(-self.AV)*self.chi) # Set initial He+ to equilibrium value between creation by # cosmic rays and destruction by recombination with free # electrons, H2, and CO abd['He+'] \ = self.xHe * self.ionRate / \ (n * (abd1['H2'] * (rcoef[3]+rcoef[18]) + abd1['CO'] * rcoef[4] + abd1['e-'] * rcoef[9])) # Set initial H3+ in the same way as for He+; then correct H2 # abundance to conserve total hydrogen abd['H3+'] \ = abd1['H2'] * self.ionRate / \ (n * (abd1['C'] * rcoef[0] + abd1['O'] * rcoef[1] + abd1['CO'] * rcoef[2] + abd1['e-'] * (rcoef[10] + rcoef[11]))) abd['H2'] -= 1.5*abd['H3+'] # Initial M+ abd['M+'] = self.xM