PageRenderTime 44ms CodeModel.GetById 11ms RepoModel.GetById 0ms app.codeStats 0ms

/src/permm/Mechanism.py

https://code.google.com/p/permm/
Python | 609 lines | 582 code | 4 blank | 23 comment | 8 complexity | c846dcf7f50c0e229fd3057d5e37ddff MD5 | raw file
  1. from utils import AttrDict
  2. from collections import defaultdict
  3. import operator
  4. import yaml
  5. import re
  6. import sys
  7. from numpy import * # Explicitly using dtype, array and ndarray; providing all default numpy to __call__interface
  8. from warnings import warn
  9. from SpeciesGroup import Species, species_sum
  10. from ProcessGroup import Process
  11. from ReactionGroup import ReactionFromString
  12. from IPRArray import Processes_ProcDelimSpcDict
  13. from graphing.timeseries import irr_plot, phy_plot, plot as tplot
  14. from Shell import load_environ
  15. from PseudoNetCDF.sci_var import PseudoNetCDFVariable
  16. from netcdf import NetCDFVariable
  17. __all__ = ['Mechanism']
  18. _spc_def_re = re.compile(r'(?P<stoic>[-+]?[0-9]*\.?[0-9]+)(?P<atom>\S+)(?=\s*\+\s*)?')
  19. _numre = re.compile('(\d+)')
  20. class Mechanism(object):
  21. """
  22. The mechanism object is the central repository for information
  23. about a chemcial model. It is aware of species, reactions, species
  24. groups (e.g. NOy), net reactions (e.g. PAN <-> C2O3 + NO2).
  25. The mechanism object also has introspective tools. In addition, it
  26. provides an interpreter through its call interface.
  27. Reaction data can be augmented with IRR data via set_irr or set_mrg
  28. """
  29. def __init__(self, yaml_path):
  30. """
  31. Initialization requires a yaml_path or a dictionary.
  32. The dictionary should have the following keys:
  33. species_list - a list of string names for chemical species
  34. (i.e. O3)
  35. reaction_list - a list of strings representing reactions
  36. (see ReactionGroup.ReactionFromString)
  37. net_reaction_list - a dictionary of reaction additions
  38. (i.e. NAME: RXN_01 + RXN_02)
  39. process_group_list - a dictionary of process additions
  40. (i.e. VertAdv = Top_Adv + Bottom_Adv)
  41. """
  42. import os
  43. if isinstance(yaml_path,str):
  44. if os.path.exists(yaml_path):
  45. yaml_file = yaml.load(file(yaml_path))
  46. else:
  47. yaml_file = yaml.load(yaml_path)
  48. elif isinstance(yaml_path,dict):
  49. yaml_file = yaml_path
  50. yaml_file = AttrDict(yaml_file)
  51. self.__yaml_file = yaml_file
  52. self.mechanism_comment = yaml_file.get('comment', '')
  53. self.species_dict = AttrDict()
  54. if yaml_file.has_key('species_list'):
  55. for spc, spc_def in yaml_file.species_list.iteritems():
  56. if spc_def == 'IGNORE':
  57. self.species_dict[spc] = Species(name = spc, names = [spc], stoic = [1])
  58. else:
  59. spc_atoms = dict([(atom, eval(stoic)) for stoic, atom in _spc_def_re.findall(spc_def)])
  60. self.species_dict[spc] = Species(name = spc, names = [spc], stoic = [1], atom_dict = {spc: spc_atoms})
  61. self.reaction_dict = AttrDict()
  62. reaction_species = []
  63. for rxn_name, rxn_str in yaml_file.reaction_list.iteritems():
  64. rxn = self.reaction_dict[rxn_name] = ReactionFromString(rxn_str)
  65. reaction_species += rxn.species()
  66. reaction_species = set(reaction_species)
  67. for spc in [spc for spc in reaction_species if not self.species_dict.has_key(spc)]:
  68. self.species_dict[spc] = Species(name = spc, names = [spc], stoic = [1])
  69. for spc_grp_def in yaml_file.get('species_group_list',[]):
  70. grp_name = spc_grp_def.split('=')[0].strip()
  71. if (spc_grp_def.count('+') + spc_grp_def.count('-')) > 4:
  72. spc_grp_def = '=species_sum(['.join(spc_grp_def.replace('+',',').replace('-',',').split('='))+'])'
  73. exec(spc_grp_def, None, self.species_dict)
  74. self.species_dict[grp_name].name = grp_name
  75. self.net_reaction_dict = yaml_file.get('net_reaction_list',{})
  76. self.variables = {}
  77. load_environ(self, self.variables)
  78. def __call__(self, expr, env = globals()):
  79. """
  80. Evaluate string expression in the context of mechanism
  81. species, species groups, reactions, net reactions and processes
  82. """
  83. return eval(expr, env, self.variables)
  84. def __getitem__(self,item):
  85. """
  86. Provide a single getitem interface for mechanism species, species
  87. groups, reactions, net reactions and processes
  88. """
  89. return self.variables[item]
  90. def __add_spc_to_reactions(self,rxn_list, spc):
  91. """
  92. Add a species to a reaction as an accumulation of components
  93. if RXN1 = CH3C(O)CH3 -j> CH3C(O)O2 + CH3O2
  94. and SPC = Species(CH3C(O)O2 + CH3O2, name = 'AO2')
  95. mech.__add_spc_to_reactions([RXN1],SPC)
  96. would make RXN1 = CH3C(O)CH3 -j> CH3C(O)O2 + CH3O2 + 2*AO2
  97. For more detail, see ReactionGroup.Reaction.__add__ interface
  98. """
  99. if not self.species_dict.has_key(spc.name):
  100. self.species_dict[spc.name] = spc
  101. for rxn in rxn_list:
  102. self.reaction_dict[rxn] = self.reaction_dict[rxn] + spc
  103. return len(rxn_list)
  104. def __ensure_species(self,spc):
  105. """
  106. Return species (created if necessary)
  107. """
  108. if isinstance(spc,str):
  109. spc = self.species_dict[spc]
  110. return spc
  111. def add_rct_to_reactions(self, spc):
  112. """
  113. Add spc to reactions where components are reactants
  114. see __ensure_species
  115. """
  116. spc = self.__ensure_species(spc)
  117. rxns = self.find_rxns(spc,[])
  118. return self.__add_spc_to_reactions(rxns, spc)
  119. def add_prd_to_reactions(self, spc):
  120. """
  121. Add spc to reactions where components are products
  122. see __ensure_species
  123. """
  124. spc = self.__ensure_species(spc)
  125. rxns = self.find_rxns([],spc)
  126. return self.__add_spc_to_reactions(rxns, spc)
  127. def add_spc_to_reactions(self, spc):
  128. """
  129. Add spc to reactions where components are reactants
  130. or products see __ensure_species
  131. """
  132. nrcts = self.add_rct_to_reactions(spc)
  133. nprds = self.add_prd_to_reactions(spc)
  134. return nrcts+nprds
  135. def add_spc_to_reactions_with_condition(self, spc, condition):
  136. """
  137. spc - must be a species object or string
  138. condition - must be a function that, given a reaction, evaluates to true or false
  139. """
  140. spc = self.__ensure_species(spc)
  141. new_rxn_def = {}
  142. for rxn_name, rxn in self.reaction_dict.iteritems():
  143. if condition(rxn):
  144. rxn = rxn + spc
  145. new_rxn_def[rxn_name] = rxn
  146. self.reaction_dict.update(new_rxn_def)
  147. return len(new_rxn_def)
  148. def get_rxns(self, reactants = [], products =[], logical_and = True, reaction_type = None):
  149. """
  150. Get reaction objects that meet the criteria specified by reactants, products and logical_and.
  151. reactants - filter mechanism reactions for reactions with all reactant(s)
  152. products - filter mechanism reactions for reactions with all product(s)
  153. logical_and - a boolean indicating how to combine reactant and product filters
  154. True: reaction is in both filters (i.e. reactants AND products)
  155. False: reaction is in either filter (i.e. reactants OR products)
  156. """
  157. return [self.reaction_dict[rk] for rk in self.find_rxns(reactants = reactants, products = products, logical_and = logical_and, reaction_type = reaction_type)]
  158. def get_irrs(self, reactants = [], products =[], logical_and = True, reaction_type = None):
  159. """
  160. Get reaction objects that meet the criteria specified by reactants, products and logical_and.
  161. reactants - filter mechanism reactions for reactions with all reactant(s)
  162. products - filter mechanism reactions for reactions with all product(s)
  163. logical_and - a boolean indicating how to combine reactant and product filters
  164. True: reaction is in both filters (i.e. reactants AND products)
  165. False: reaction is in either filter (i.e. reactants OR products)
  166. """
  167. return [self(rk) for rk in self.find_rxns(reactants = reactants, products = products, logical_and = logical_and, reaction_type = reaction_type)]
  168. def find_rxns(self, reactants = [], products =[], logical_and = True, reaction_type = None):
  169. """
  170. Get reaction names that meet the criteria specified by reactants, products and logical_and.
  171. reactants - filter mechanism reactions for reactions with all reactant(s)
  172. products - filter mechanism reactions for reactions with all product(s)
  173. logical_and - a boolean indicating how to combine reactant and product filters
  174. True: reaction is in both filters (i.e. reactants AND products)
  175. False: reaction is in either filter (i.e. reactants OR products)
  176. """
  177. if isinstance(reactants, (Species, str)):
  178. reactants = [reactants]
  179. reactants = [self.__ensure_species(spc) for spc in reactants]
  180. if isinstance(products, (Species, str)):
  181. products = [products]
  182. products = [self.__ensure_species(spc) for spc in products]
  183. result = [(rxn_name, rxn) for rxn_name, rxn in self.reaction_dict.iteritems()]
  184. if reactants != []:
  185. reactant_result = [rxn_name for rxn_name, rxn in result if all([rxn.has_rct(rct) for rct in reactants])]
  186. else:
  187. reactant_result = [rn for rn, rv in result]
  188. if products != []:
  189. product_result = [rxn_name for rxn_name, rxn in result if all([rxn.has_prd(prd) for prd in products])]
  190. else:
  191. product_result = [rn for rn, rv in result]
  192. if logical_and:
  193. reaction_list = set(reactant_result).intersection(product_result)
  194. else:
  195. reaction_list = set(reactant_result+product_result)
  196. if reaction_type is not None:
  197. reaction_result = [rn for rn, rv in result if rv.reaction_type in reaction_type]
  198. reaction_list = reaction_list.intersection(reaction_result)
  199. result = list(reaction_list)
  200. result.sort()
  201. return result
  202. def yaml_net_rxn(self, rxns):
  203. """
  204. Create the YAML representation of a net reaction for the supplied
  205. reactions
  206. """
  207. species = list(set(reduce(operator.add, [self.reaction_dict[rxn].species() for rxn in rxns])))
  208. for role in ('r','p'):
  209. for spc in species:
  210. for rxn in rxns:
  211. method = dict( r = 'has_rct', p = 'has_prd')
  212. if getattr(self.reaction_dict[rxn], method)(spc):
  213. pass
  214. def subst_net_rxn(self, reactants = [], products = [], logical_and = True, reaction_type = None, name = None):
  215. rxns = self.find_rxns(reactants = reactants, products = products, logical_and = logical_and, reaction_type = reaction_type)
  216. if len(rxns) > 1:
  217. eval_str = ' + '.join(rxns)
  218. if name is None:
  219. name = eval_str
  220. nrxn = eval(eval_str, globals(), self.irr_dict)
  221. for rxn in rxns:
  222. del self.irr_dict[rxn]
  223. del self.reaction_dict[rxn]
  224. self.irr_dict[name] = nrxn
  225. self.reaction_dict[name] = nrxn.sum()
  226. load_environ(self, self.variables)
  227. def make_net_rxn(self, reactants = [], products = [], logical_and = True, reaction_type = None):
  228. """
  229. Sum each reaction in find_rxns(reactants, procucts, logical_and)
  230. and return a net reaction
  231. for more information see find_rxns
  232. """
  233. rxns = self.find_rxns(reactants, products, logical_and, reaction_type)
  234. result = self(' + '.join(rxns))
  235. return result
  236. def print_net_rxn(self, reactants = [], products = [], logical_and = True, reaction_type = None):
  237. """
  238. Sum each reaction in find_rxns(reactants, procucts, logical_and)
  239. and return a net reaction
  240. for more information see find_rxns
  241. """
  242. net_rxn = self.make_net_rxn(reactants, products, logical_and, reaction_type)
  243. print net_rxn
  244. def print_rxns(self, reactants = [], products = [], logical_and = True, reaction_type = None, sortby = None, reverse = False, digits = -1, nspc = 10000):
  245. """
  246. For each reaction in find_rxns(reactants, procucts, logical_and),
  247. print the reaction
  248. for more information see find_rxns
  249. """
  250. rxns = self.find_rxns(reactants, products, logical_and, reaction_type)
  251. rxns = [(rxn, self.reaction_dict[rxn]) for rxn in rxns]
  252. try:
  253. if sortby is None:
  254. sortby = ([p for p in _ensure_list(products) if not p.exclude]+[r for r in _ensure_list(reactants) if not r.exclude])[0]
  255. rxns = [(rxno[sortby], rxn, rxno) for rxn, rxno in rxns]
  256. rxns.sort(reverse = reverse)
  257. except:
  258. rxns = [(_rxn_ordinal(rxn), rxn, rxno) for rxn, rxno in rxns]
  259. rxns.sort()
  260. warn("Not all reactions contain %s; check query and/or explicitly define sortby species" % str(sortby))
  261. for mass, rxn, rxno in rxns:
  262. print rxn, rxno.display(nspc = nspc, digits = digits)
  263. def plot_rxn_list(self, reactions, plot_spc = None, path = None, show = False, **kwds):
  264. """
  265. Returns:
  266. a matplotlib figure with a line for each reaction indexed by plot_spc
  267. Parameters:
  268. reactions - list of reaction objects
  269. plot_spc - species to plot
  270. path - path to savefigure
  271. show - try to display figure interactively
  272. Other keywords:
  273. accepts any keyword accepted by permm.graphing.timeseries.irr_plot
  274. """
  275. if plot_spc is None:
  276. plot_spc = self((reactions[0][0].products() + reactions[0][0].reactants())[0])
  277. fig = irr_plot(self, reactions = reactions, species = plot_spc, **kwds)
  278. if path is not None and show:
  279. fig.savefig(path)
  280. else:
  281. from pylab import show
  282. show()
  283. return fig
  284. def plot(self, y, path = None, stepped = True, end_date = True, time_slice = slice(None), figure_settings = {}, axis_settings = {}, line_settings = {}):
  285. """
  286. plot creates a timeseries plot of any numerical array.
  287. """
  288. fig = tplot(self, y, stepped = stepped, end_date = end_date,
  289. figure_settings = figure_settings,
  290. axis_settings = axis_settings,
  291. line_settings = line_settings)
  292. if path is not None:
  293. fig.savefig(path)
  294. return fig
  295. def plot_rxns(self, reactants = [], products = [], logical_and = True, reaction_type = None,
  296. plot_spc = None, combine = [()], nlines = 8, **kwds):
  297. """
  298. Creates figure of reactions
  299. Steps:
  300. 1. Query reactions using get_rxns(reactants, procucts, logical_and)
  301. 2. net reaction sets specified in combine (i.e. combine = [('IRR_1', 'IRR_2'), ...])
  302. 3. sort queried reactions by absolute plot_spc change (+/-)
  303. 3. combine all reactions that are not in the top (nlines - 1)
  304. 4. plot the plot_spc change from each (nlines - 1) individual reactions
  305. and the 1 net reaction
  306. Returns:
  307. matplotlib figure with each queried reaction plotted for production/consumption
  308. of plot_spc.
  309. Parameters:
  310. reactants - instance or list of Species objects to require
  311. products - instance or list of Species objects to require
  312. logical_and - if true, require reactants and products; false, require reactants or products
  313. reaction_type - some combination of kjn: thermal (k); photolysis (j); net (n)
  314. plot_spc - Species instance to be plotted, defaults to first product or reactant
  315. combine - reaction keys to combine into net reactions
  316. nlines - maximum reactions to plot; selected based on maximum absolute plot_spc change
  317. Other keywords:
  318. permm.Mechanism.plot_rxn_list or
  319. permm.graphing.timeseries.irr_plot
  320. """
  321. if plot_spc is None:
  322. plot_spc = (_ensure_list(products)+_ensure_list(reactants))[0]
  323. reactions = self.find_rxns(reactants, products, logical_and, reaction_type)
  324. if reactions == []:
  325. raise ValueError, "Your query didn't match any reactions; check your query and try again (try print_rxns)."
  326. reactions = [ rxn for rxn in reactions if rxn not in reduce(operator.add, combine) ]
  327. nlines = min(nlines, len(reactions)+1)
  328. if combine != [()]:
  329. reactions = reactions + map(lambda t2: '+'.join(t2), combine)
  330. reactions = [ (abs(self('(%s)' % (rxn))[plot_spc]).sum(),rxn) for rxn in reactions]
  331. reactions.sort(reverse = True)
  332. reactions = [r for v,r in reactions]
  333. for rxn in reactions[nlines-1:]:
  334. try:
  335. other = other + self('(%s)' % (rxn,))
  336. except:
  337. other = self('(%s)' % (rxn,))
  338. try:
  339. reactions = [self('(%s)' % (rxn, )) for rxn in reactions[:nlines-1]] + [other]
  340. except:
  341. reactions = [self('(%s)' % (rxn, )) for rxn in reactions[:nlines-1]]
  342. return self.plot_rxn_list(reactions = reactions, plot_spc = plot_spc, **kwds)
  343. def print_irrs(self, reactants = [], products = [], logical_and = True, reaction_type = None, factor = 1., sortby = None, reverse = False, slice = slice(None), nspc = 100000, digits = -1, formatter = 'g'):
  344. """
  345. For each reaction in find_rxns(reactants, procucts, logical_and),
  346. print the reaction summed for the entire timeseries.
  347. for more information see find_rxns
  348. """
  349. if not hasattr(self,'irr_dict'):
  350. raise ValueError, "Net reactions are only available when IRR has been loaded"
  351. rxns = self.find_rxns(reactants, products, logical_and, reaction_type)
  352. irrs = [(rxn, self.irr_dict[rxn][slice].sum()) for rxn in rxns]
  353. try:
  354. if sortby is None:
  355. sortby = ([p for p in _ensure_list(products) if not p.exclude]+[r for r in _ensure_list(reactants) if not r.exclude])[0]
  356. irrs = [(irr[sortby], rxn, irr) for rxn, irr in irrs]
  357. irrs.sort(reverse = reverse)
  358. except:
  359. irrs = [(_rxn_ordinal(rxn), rxn, irr) for rxn, irr in irrs]
  360. irrs.sort()
  361. warn("Not all reactions contain %s; check query and/or explicitly define sortby species" % str(sortby))
  362. irrs = [(rxn, irr) for mass, rxn, irr in irrs]
  363. for rxn, irr in irrs:
  364. print rxn, (irr * factor).display(nspc = nspc, digits = digits)
  365. def set_mrg(self,mrg, use_net_rxns = True, use_irr = True, use_ipr = True):
  366. """
  367. Add process analysis from a 1D merged IRR/IPR file
  368. """
  369. self.mrg = mrg
  370. if use_irr:
  371. try:
  372. self.set_irr(mrg.variables['IRR'], mrg.Reactions.split(), use_net_rxns = use_net_rxns)
  373. except:
  374. self.set_irr()
  375. if use_ipr:
  376. try:
  377. self.set_ipr(mrg.variables['IPR'])
  378. except:
  379. self.set_ipr()
  380. load_environ(self, self.variables)
  381. def set_irr(self,irr = None, ReactionNames = None, use_net_rxns = True):
  382. """
  383. Add process analysis from a 2D merged IRR array dim(TIME,RXN)
  384. """
  385. if not irr is None:
  386. irr_type = dtype(dict(names = ReactionNames, formats = irr[:].dtype.char*len(ReactionNames)))
  387. self.irr = irr[:].view(dtype = irr_type).squeeze().view(type = PseudoNetCDFVariable)
  388. self.irr.units = irr.units
  389. self.__use_net_rxns = use_net_rxns
  390. self.apply_irr()
  391. def apply_irr(self):
  392. self.irr_dict = {}
  393. for rxn_name, rxn in self.reaction_dict.iteritems():
  394. if hasattr(self, 'irr'):
  395. try:
  396. self.irr_dict[rxn_name] = rxn * self.irr[rxn_name]
  397. except ValueError, (e):
  398. self.irr_dict[rxn_name] = rxn * zeros(self.irr.shape, 'f')
  399. warn("IRR does not contain %s: skipped." % rxn_name)
  400. else:
  401. try:
  402. self.irr_dict[rxn_name] = rxn * self.mrg.variables[rxn_name][:].view(type = PseudoNetCDFVariable)
  403. except (KeyError, ValueError), (e):
  404. warn("IRR does not contain %s: skipped." % rxn_name)
  405. if self.__use_net_rxns and len(self.irr_dict)>0:
  406. self.nreaction_dict = {}
  407. for nrxn_name, nrxn in self.net_reaction_dict.iteritems():
  408. try:
  409. self.nreaction_dict[nrxn_name] = eval(nrxn, None, self.irr_dict)
  410. except Exception, (e):
  411. warn("Predefined net rxn %s is not available; %s" % (nrxn_name, str(e)))
  412. load_environ(self, self.variables)
  413. def set_ipr(self, ipr = None, processes = None):
  414. """
  415. Add process analysis from a 3D merged IPR array (TIME,SPC,PROC)
  416. """
  417. if ipr is None:
  418. if hasattr(self.mrg, 'Processes'):
  419. processes = self.mrg.Processes.split()
  420. self.process_dict = Processes_ProcDelimSpcDict(processes, self.mrg.variables)
  421. else:
  422. warn("Unable to load IPR; missing Species and Processes attributes")
  423. return
  424. elif isinstance(ipr, dict):
  425. if processes is None:
  426. raise ValueError, "When ipr is a dictionary, processes must be provided as a list of process names"
  427. self.process_dict = Processes_ProcDelimSpcDict(processes, self.mrg.variables)
  428. elif isinstance(ipr, (PseudoNetCDFVariable, NetCDFVariable)):
  429. self.process_dict = {}
  430. for pi, prc in enumerate(processes):
  431. self.process_dict[prc] = Process(prc, default_unit = getattr(ipr, 'units', 'Unknown'), **dict([(spc, ipr[si, pi]) for si, spc in enumerate(species)]))
  432. else:
  433. return
  434. for prc_name, prc in self.__yaml_file.get('process_group_list', {}).iteritems():
  435. try:
  436. self.process_dict[prc_name] = eval(prc,{},self.process_dict)
  437. self.process_dict[prc_name].name = prc_name
  438. except:
  439. warn("Cannot create %s process group" % prc_name)
  440. # Add extra species for names in IPR
  441. spcs = set(reduce(list.__add__, [[spc for spc in proc.keys()] for proc in self.process_dict.values()]))
  442. new_spcs = spcs.difference(self.species_dict.keys())
  443. for name in new_spcs:
  444. if not self.species_dict.has_key(name):
  445. self.species_dict[name] = Species(name = name, names = [name], stoic = [1.])
  446. def add_rxn(self, rxn_key, rxn_str):
  447. self.reaction_dict[rxn_key] = ReactionFromString(rxn_str)
  448. if hasattr(self, 'irr_dict'):
  449. self.irr_dict[rxn_key] = ReactionFromString(rxn_str)
  450. self.irr_dict[rxn_key] *= self.irr[rxn_key]
  451. load_environ(self, self.variables)
  452. def plot_proc(self, species, path = None, **kwds):
  453. """
  454. species - permm.SpeciesGroup.Species object
  455. path - path for saved figure
  456. kwds - * title - title
  457. * init - Name of initial concentration process
  458. * final - Name of final concentration process
  459. * linestyle - default line style (default: '-')
  460. * linewidth - default line width (default: 3)
  461. * marker - default line marker style (default: None)
  462. * ncol - number of legend columns (default: 1)
  463. * fig - figure to plot on (default: None)
  464. * cmap - matplotlib color map for lines (default: None)
  465. * filter - remove processes with zero values (default: True)
  466. * <process name1> - process names from mech.process_dict can be
  467. provided to limit the processes shown. When
  468. provided, a process should be a dictionary of
  469. matplotlib plot options (common: linestyle,
  470. linewidth, label, marker). The dictionary can
  471. be empty.
  472. * <process name2> - same as process 1
  473. * <process nameN> - same as process 1
  474. * end_date - times are for the time period end
  475. """
  476. fig = phy_plot(self, species, **kwds)
  477. if path is not None:
  478. fig.savefig(path)
  479. else:
  480. if kwds.get('show', True):
  481. from pylab import show
  482. show()
  483. return fig
  484. def globalize(self, env):
  485. load_environ(self, env)
  486. def _ensure_list(x):
  487. if isinstance(x, Species):
  488. return [x]
  489. else:
  490. return x
  491. def _rxn_ordinal(rxnlabel):
  492. result = _numre.search(rxnlabel)
  493. if result is None:
  494. return None
  495. else:
  496. val = result.groups()[0]
  497. try:
  498. return eval(val)
  499. except:
  500. return int(val)