PageRenderTime 32ms CodeModel.GetById 0ms RepoModel.GetById 1ms app.codeStats 0ms

/src/permm/ReactionGroup.py

https://code.google.com/p/permm/
Python | 627 lines | 620 code | 5 blank | 2 comment | 3 complexity | 167f3dbd5127d0fdb742d5d1c4882b75 MD5 | raw file
  1. from numpy import ndarray, \
  2. array, \
  3. newaxis, \
  4. float64, \
  5. vectorize, \
  6. rollaxis, \
  7. indices
  8. from numpy.ma import sum
  9. from permm.SpeciesGroup import Species
  10. from warnings import warn
  11. import operator
  12. import re
  13. ReactionGroup = str
  14. __all__ = ['Stoic', 'ReactionFromString', 'Reaction']
  15. class Stoic(ndarray):
  16. """
  17. Stoic is a sub-class of float with a role
  18. property. The role property is 'r', 'p', or 'u'.
  19. r = reactant
  20. p = product
  21. u = undetermined
  22. Undetermined is useful when a species is part of a
  23. net reaction and may play either role
  24. Stoic are created by
  25. Stoic(2., 'r')
  26. Stoic supports:
  27. __mul__: Stoic*(Stoic|Number)
  28. __rmul__: (Stoic|Number)*Stoic
  29. __add__: Stoic+(Stoic|Number)
  30. """
  31. __array_priority__ = 1001.
  32. def __new__(subtype, f, role):
  33. result = array(f).view(subtype)
  34. result.role = role
  35. return result
  36. def __array_wrap__(self, obj, context = None):
  37. result = obj.view(Stoic)
  38. if context is not None:
  39. args = context[1]
  40. if len(args) > 1:
  41. lhs, rhs = args[:2]
  42. if hasattr(lhs, 'role'):
  43. if hasattr(rhs, 'role'):
  44. if lhs.role != rhs.role:
  45. result.role = 'u'
  46. else:
  47. result.role = lhs.role
  48. else:
  49. result.role = lhs.role
  50. else:
  51. result.role = 'u'
  52. return result
  53. def __repr__(self):
  54. return "Stoic(%s, role = '%s')" % (str(self.asarray()), getattr(self, 'role', 'None'))
  55. def __str__(self):
  56. return self.__repr__()
  57. def asarray(self):
  58. return self.view(ndarray)
  59. StoicArray = vectorize(Stoic, otypes = [object])
  60. def AddRole(s1, s2):
  61. if isinstance(s1,Stoic):
  62. return s1
  63. else:
  64. return Stoic(s1, s2.role)
  65. def StoicAdd(s1, s2):
  66. return Stoic.__add__(AddRole(s1,s2), s2)
  67. StoicAdd = vectorize(StoicAdd, otypes = [object])
  68. def ReactionFromString(rxn_str):
  69. """
  70. ReactionFromString is a convenience function. It creates
  71. Reaction objects from a string with the following pattern:
  72. "(?P<reactants>.*)=(?P<rxn_type>[kj])[>]\s*(?P<products>.*)"
  73. where each reactant or product matches the following pattern:
  74. "(\s?(?P<sign>[+-])?\s?)?((?P<stoic>\d(\.(\d{1,3}(E\d{2})?)?)?)\*)?(?P<name>[A-Z]\w{0,3})\s?"
  75. For example:
  76. OH + OLE =k> 0.8*FORM + 0.33*ALD2 + 0.62*ALDX + 0.8*XO2 + 0.95*HO2 - 0.7 PAR
  77. """
  78. stoics = {}
  79. roles = {}
  80. species = ()
  81. reaction_re = re.compile("(?P<reactants>.*)->\[(?P<rxn_type>[kj])\]\s*(?P<products>.*)")
  82. species_re = re.compile("(\s?(?P<sign>[+-])?\s?)?((?P<stoic>\d{0,1}(\.(\d{1,3}(E\d{2})?)?)?)\*)?(?P<name>[a-zA-Z]\w*)(?:[ +=]|$)+",re.M)
  83. reaction_match = reaction_re.match(rxn_str)
  84. if reaction_match is None:
  85. raise SyntaxError, "Reactions must match the following patter\n\n<%%d> stoic*spc ([+-] stoic*spc)* =[kju]> [stoic*spc] ([+-] stoic*spc)*\n\n%s" % (rxn_str,)
  86. reactants = reaction_match.groupdict()['reactants']
  87. reaction_type = reaction_match.groupdict()['rxn_type']
  88. products = reaction_match.groupdict()['products']
  89. for spc in species_re.finditer(reactants):
  90. spc_g = spc.groupdict()
  91. name = spc_g['name']
  92. sign = spc_g['sign']
  93. stoic = spc_g['stoic']
  94. if sign is None:
  95. sign = ''
  96. if stoic is None:
  97. stoic = '1'
  98. if name in species:
  99. stoics[name] += -float(sign + stoic)
  100. else:
  101. species += (name,)
  102. stoics[name] = -float(sign + stoic)
  103. roles[name] = 'r'
  104. for spc in species_re.finditer(products):
  105. spc_g = spc.groupdict()
  106. name = spc_g['name']
  107. sign = spc_g['sign']
  108. stoic = spc_g['stoic']
  109. if sign is None:
  110. sign = '+'
  111. if stoic is None:
  112. stoic = '1'
  113. value = float(sign + stoic)
  114. if name in species:
  115. stoics[name] += value
  116. role = roles[name]
  117. if role == 'r':
  118. role = 'u'
  119. roles[name] = role
  120. else:
  121. stoics[name] = value
  122. roles[name] = 'p'
  123. species += (name,)
  124. return Reaction(reaction_type = reaction_type, roles = roles, **stoics)
  125. class Reaction(object):
  126. """
  127. Reaction is an object that represents reaction groups. The simplest
  128. case being a "single reaction" reaction group.
  129. Reaction groups support the following interfaces
  130. 1) indexing (__getitem__) for stoiciometry
  131. 2) multiplication (__mul__ and __rmul__) by arrays and numbers
  132. 3) addition (__add__) of reactions or species
  133. A Reaction can also determine when a species is a reactant, product
  134. or unspecified. When a species is only present as a one role or the other
  135. (reactant or product), it is always that role. When a species is present
  136. in the group as both, its current role is determined by the stoichiometry.
  137. Convenience functions:
  138. has_spc = spc in Reaction.species()
  139. has_rct = spc in Reaction.reactants()
  140. has_prd = spc in Reaction.products()
  141. Other functions:
  142. add_rct_spc
  143. add_prd_spc
  144. """
  145. __array_priority__ = 1000.
  146. def __init__(self, reaction_type = 'k', roles = {}, **stoic):
  147. """
  148. roles - dictionary of specific roles; for species whose
  149. stoichiometric sign is inconsistent with their
  150. role (i.e. X + Y =k> Z - .6*PAR)
  151. reaction_type - 'k' is kinetic, 'j' is photolysis, 'n' is net
  152. stoic - stoichiometry values provided as keywords by species; values
  153. can be scalars or ndarrays
  154. """
  155. self.reaction_type = reaction_type
  156. self._species = tuple([k for k in stoic.keys()])
  157. self._roles = roles.copy()
  158. self._stoic = stoic.copy()
  159. try:
  160. self.shape = self._stoic[self._species[0]].shape
  161. except AttributeError, (e):
  162. self.shape = ()
  163. for spc in self._species:
  164. self._stoic[spc] = float64(self._stoic[spc])
  165. for k in self._species:
  166. if k not in self._roles:
  167. self._roles[k] = {True: 'r', False: 'p'}[sum(stoic[k]) < 0]
  168. self._update_roles()
  169. def _update_roles(self):
  170. """
  171. Create static copy of species roles
  172. """
  173. self._reactants = ()
  174. self._products = ()
  175. self._unspecified = ()
  176. for k, v in self._roles.iteritems():
  177. if v == 'r':
  178. self._reactants += (k,)
  179. elif v == 'p':
  180. self._products += (k,)
  181. elif v == 'u':
  182. self._unspecified += (k,)
  183. if sum(self._stoic[k]) > 0:
  184. self._products += (k,)
  185. else:
  186. self._reactants += (k,)
  187. def __contains__(self, lhs):
  188. """
  189. Test if reaction has a species
  190. """
  191. if isinstance(lhs, Species):
  192. return len(set(lhs.names()).intersection(self._species)) > 0
  193. elif isinstance(lhs, str):
  194. return lhs in self._species
  195. else:
  196. raise TypeError, 'Unknown comparison: __contains__ for Reaction and %s' % str(type(lhs))
  197. def __getitem__(self, item):
  198. """
  199. Return a stoichiometry for a species or, if item is not a species, return
  200. a new reaction where stoichiometry is subset using item
  201. """
  202. if isinstance(item, Species):
  203. try:
  204. return self._stoic[item.name]
  205. except KeyError, (e):
  206. return self.get_spc(item)
  207. elif isinstance(item, str):
  208. return Stoic(self._stoic[item], role = self._roles[item])
  209. else:
  210. return Reaction(roles = self._roles, reaction_type = self.reaction_type, **dict([(k,v[item]) for k, v in self._stoic.iteritems()]))
  211. def __str__(self):
  212. """
  213. Report all values followed by the sum value for the reaction
  214. """
  215. result = ''
  216. temp = self.copy()
  217. temp.shape = ()
  218. if self.shape != ():
  219. result += '%d Reactions with shape %s\n' % (reduce(int.__mul__, self.shape), str(self.shape))
  220. for idx in indices(self.shape).reshape(len(self.shape), -1).swapaxes(0,1):
  221. idx = tuple(idx)
  222. result += str(idx) + ': '
  223. for spc in self._species:
  224. temp._stoic[spc] = self._stoic[spc][idx]
  225. temp._update_roles()
  226. result += str(temp)+', \n'
  227. result = result[:-3]+'\n'
  228. sum_result = self.display(digits = 5, nspc = 1000)
  229. if result != '':
  230. result += '-' * (len(sum_result)+5) + '\nSum: '
  231. result += sum_result
  232. return result
  233. def __repr__(self):
  234. """
  235. Representation is same as string
  236. """
  237. return self.__str__()
  238. def display(self, digits = 5, nspc = 3, formatter = 'g'):
  239. reactants = [(self._stoic[rct].sum(), rct) for rct in self.reactants()]
  240. reactants.sort(reverse=False)
  241. products = [(self._stoic[prd].sum(), prd) for prd in self.products()]
  242. products.sort(reverse=True)
  243. if digits == None:
  244. str_temp = '%s'
  245. reactant_str = ' + '.join([str_temp % rct for stoic, rct in reactants][:nspc])
  246. product_str = ' + '.join([str_temp % prd for stoic, prd in products][:nspc])
  247. elif digits == -1:
  248. reactant_str = ' + '.join(['*'.join([str(-stoic), str(rct)]) for stoic, rct in reactants][:nspc])
  249. product_str = ' + '.join(['*'.join([str(stoic), str(prd)]) for stoic, prd in products][:nspc])
  250. else:
  251. str_temp = '%%.%d%s*%%s' % (digits, formatter)
  252. reactant_str = ' + '.join([str_temp % (-stoic,rct) for stoic, rct in reactants][:nspc])
  253. product_str = ' + '.join([str_temp % (stoic,prd) for stoic, prd in products][:nspc])
  254. if len(reactants) > nspc:
  255. reactant_str += ' + ...'
  256. if len(products) > nspc:
  257. product_str += ' + ...'
  258. return '%s ->[%s] %s' % (reactant_str, self.reaction_type, product_str)
  259. def __add__(self,rhs):
  260. """
  261. Add reactions to make a net reaction or add species to an existing reaction.
  262. """
  263. if isinstance(rhs,Reaction):
  264. kwds = {}
  265. for spc in self._species:
  266. kwds[spc] = self._stoic[spc]
  267. for spc in rhs._species:
  268. if kwds.has_key(spc):
  269. kwds[spc] = kwds[spc] + rhs._stoic[spc]
  270. else:
  271. kwds[spc] = rhs._stoic[spc]
  272. kwds['roles'] = roles = {}
  273. for spc in set(self._species+rhs._species):
  274. new_role = ''.join(set(self._roles.get(spc,'') + rhs._roles.get(spc,'')))
  275. if len(new_role) == 1:
  276. roles[spc] = new_role
  277. else:
  278. roles[spc] = 'u'
  279. if self.reaction_type == rhs.reaction_type:
  280. kwds['reaction_type'] = self.reaction_type
  281. else:
  282. kwds['reaction_type'] = 'n'
  283. elif isinstance(rhs,Species):
  284. return self.__add_if_in_spclist(rhs,self._species)
  285. else:
  286. raise TypeError, "Currently, only reactions can be added together"
  287. return Reaction(**kwds)
  288. def __rmul__(self,irrs):
  289. return self.__mul__(irrs)
  290. def __mul__(self,irrs):
  291. species = self._species
  292. values = dict([(k,v*irrs) for k, v in self._stoic.iteritems()])
  293. result = Reaction(roles = self._roles, reaction_type = self.reaction_type, **values)
  294. return result
  295. def __add_if_in_spclist(self,rhs,spc_list):
  296. if not self.has_spc(rhs):
  297. raise KeyError, 'Reaction has no components of %s' % rhs.name
  298. elif rhs.name in self._species:
  299. warn('Already has %s' % rhs.name)
  300. return self.copy()
  301. elif rhs.exclude:
  302. raise ValueError, 'Exclude is not supported'
  303. result = self.copy()
  304. new_stoic = result[rhs]
  305. result._roles[rhs.name] = new_stoic.role
  306. result._species += (rhs.name,)
  307. result._stoic[rhs.name] = new_stoic.view(ndarray)
  308. result._update_roles()
  309. return result
  310. def copy(self):
  311. """
  312. Create a copy of the reaction such that stoichiometry
  313. are not shared
  314. """
  315. return Reaction(roles = self._roles, reaction_type = self.reaction_type, **dict([(k, v.copy()) for k, v in self._stoic.iteritems()]))
  316. def sum(self, axis = None):
  317. """
  318. Sum stoichiometries and create a scalar reaction
  319. """
  320. result = self.copy()
  321. for spc in result._species:
  322. result._stoic[spc] = self._stoic[spc].sum(axis)
  323. result.shape = ()
  324. result._update_roles()
  325. return result
  326. def mean(self, axis = None):
  327. """
  328. Mean stoichiometries and create a scalar reaction
  329. """
  330. result = self.copy()
  331. for spc in result._species:
  332. result._stoic[spc] = self._stoic[spc].mean(axis)
  333. result.shape = ()
  334. result._update_roles()
  335. return result
  336. def reactants(self):
  337. """
  338. Report all species acting as reactants; includes negative
  339. stoichiometries for unspecified role species
  340. """
  341. return self._reactants
  342. def products(self):
  343. """
  344. Report all species acting as products; includes positive
  345. stoichiometries for unspecified role species
  346. """
  347. return self._products
  348. def species(self):
  349. """
  350. Report all species
  351. """
  352. return self._species
  353. def unspecified(self):
  354. """
  355. Report all species with unspecified roles
  356. """
  357. return self._unspecified
  358. def get(self, item, default = None):
  359. try:
  360. return self.__getitem__(item)
  361. except:
  362. return default
  363. def get_spc(self, item, role = 'rpu'):
  364. species_list = ()
  365. if 'r' in role:
  366. species_list = species_list + self._reactants
  367. if 'p' in role:
  368. species_list = species_list + self._products
  369. if 'u' in role:
  370. species_list = species_list + self._unspecified
  371. species = set(item.names()).intersection(species_list)
  372. if len(species) == 0:
  373. raise KeyError, "%s does not contain %s" % (str(self.sum()), str(item))
  374. value = 0.
  375. for spc in species:
  376. value = value + item[spc] * self._stoic[spc]
  377. first_spc_role = self._roles[species.pop()]
  378. same_role = all([first_spc_role == self._roles[spc] for spc in species])
  379. if same_role:
  380. role = first_spc_role
  381. else:
  382. role = 'u'
  383. return Stoic(value, role = role)
  384. def produces(self, item):
  385. try:
  386. return self.get_spc(item, role = 'pu')
  387. except KeyError:
  388. return 0 * self[(self._products+self._reactants)[0]]
  389. def consumes(self, item):
  390. try:
  391. return -self.get_spc(item, role = 'ru')
  392. except KeyError:
  393. return 0 * self[(self._reactants+self._products)[0]]
  394. def has_spc(self,spc_grp):
  395. return spc_in_list(spc_grp,self._species)
  396. def has_rct(self,spc_grp):
  397. return spc_in_list(spc_grp,self._reactants)
  398. def has_prd(self,spc_grp):
  399. return spc_in_list(spc_grp,self._products)
  400. def add_rct_spc(self,rhs):
  401. return self.__add_if_in_spclist(rhs,self._reactants)
  402. def add_prd_spc(self,rhs):
  403. return self.__add_if_in_spclist(rhs,self._products)
  404. def spc_in_list(spc_grp,local_list):
  405. if spc_grp.exclude:
  406. return not spc_in_list(-spc_grp, local_list)
  407. else:
  408. for s in spc_grp.names():
  409. if s in local_list:
  410. return True
  411. else:
  412. return False
  413. import unittest
  414. class ReactionTestCase(unittest.TestCase):
  415. def setUp(self):
  416. self.spcs = dict(NO2 = Species(name = 'NO2', names = ['NO2'], stoic = [1.]),
  417. NO = Species(name = 'NO', names = ['NO'], stoic = [1.]),
  418. HNO3 = Species(name = 'HNO3', names = ['HNO3'], stoic = [1.]),
  419. O = Species(name = 'O', names = ['O'], stoic = [1.]),
  420. OH = Species(name = 'OH', names = ['OH'], stoic = [1.]),
  421. HO2 = Species(name = 'HO2', names = ['HO2'], stoic = [1.]),
  422. O3 = Species(name = 'O3', names = ['O3'], stoic = [1.]),
  423. PAR = Species(name = 'PAR', names = ['PAR'], stoic = [1.]),
  424. NTR = Species(name = 'NTR', names = ['NTR'], stoic = [1.]),
  425. FORM = Species(name = 'FORM', names = ['FORM'], stoic = [1.]),
  426. ALD2 = Species(name = 'ALD2', names = ['ALD2'], stoic = [1.]),
  427. ALDX = Species(name = 'ALDX', names = ['ALDX'], stoic = [1.]),
  428. )
  429. exec('NOx = NO2 + NO', globals(), self.spcs)
  430. exec('ALD = ALDX + ALD2 + FORM', globals(), self.spcs)
  431. rxn_strings = {'NO2hv': 'NO2 ->[j] NO + O',
  432. 'OplO3': 'O + O2 + M ->[k] O3 + M',
  433. 'NTRplOH': 'NTR + OH ->[k] HNO3 + HO2 + 0.330*FORM + 0.330*ALD2 + 0.330*ALDX - 0.660*PAR'
  434. }
  435. self.rxns = {}
  436. for label, rxn_str in rxn_strings.iteritems():
  437. self.rxns[label] = ReactionFromString(rxn_str)
  438. def testHasSpc(self):
  439. r1 = self.rxns['NO2hv']
  440. spcs = [self.spcs[k] for k in ['NO2', 'NO', 'NOx']]
  441. for spc in spcs:
  442. self.assertTrue(r1.has_spc(spc))
  443. def testHasRole(self):
  444. r1 = self.rxns['NO2hv']
  445. rs = self.spcs['NO2']
  446. ps = self.spcs['NO']
  447. self.assertTrue(r1.has_rct(rs))
  448. self.assertFalse(r1.has_rct(ps))
  449. self.assertFalse(r1.has_prd(rs))
  450. self.assertTrue(r1.has_prd(ps))
  451. def testConsumes(self):
  452. r1 = self.rxns['NO2hv']
  453. rs = self.spcs['NO2']
  454. ps = self.spcs['NO']
  455. ns = self.spcs['O3']
  456. self.assertTrue(r1.consumes(rs) == 1)
  457. self.assertTrue(r1.consumes(ps) == 0)
  458. self.assertTrue(r1.consumes(ns) == 0)
  459. def testProduces(self):
  460. r1 = self.rxns['NO2hv']
  461. rs = self.spcs['NO2']
  462. ps = self.spcs['NO']
  463. ns = self.spcs['O3']
  464. self.assertTrue(r1.produces(rs) == 0)
  465. self.assertTrue(r1.produces(ps) == 1)
  466. self.assertTrue(r1.produces(ns) == 0)
  467. def testGet(self):
  468. r1 = self.rxns['NO2hv']
  469. rs = self.spcs['NO2']
  470. self.assertTrue(r1[rs] == -1)
  471. def testReactants(self):
  472. r1 = self.rxns['NTRplOH']
  473. self.assertEquals(set(r1.reactants()), set(('OH', 'NTR')))
  474. def testProducts(self):
  475. r1 = self.rxns['NTRplOH']
  476. self.assertEquals(set(r1.products()), set(('PAR', 'FORM', 'ALD2', 'HNO3', 'HO2', 'ALDX')))
  477. def testMulScalar(self):
  478. r1 = self.rxns['NTRplOH']
  479. a = 5.83
  480. r2 = r1 * a
  481. for spcn in "NTR OH".split():
  482. spc = self.spcs[spcn]
  483. self.assertAlmostEqual(r2[spc], -a)
  484. for spcn in "HNO3 HO2".split():
  485. spc = self.spcs[spcn]
  486. self.assertAlmostEqual(r2[spc], a)
  487. for spcn in "FORM ALD2 ALDX".split():
  488. spc = self.spcs[spcn]
  489. self.assertAlmostEqual(r2[spc], a * .33)
  490. for spcn in ["ALD"]:
  491. spc = self.spcs[spcn]
  492. self.assertAlmostEqual(r2[spc], a * .99)
  493. for spcn in ["PAR"]:
  494. spc = self.spcs[spcn]
  495. self.assertAlmostEqual(r2[spc], -a * .66)
  496. def testMulArray(self):
  497. from numpy import arange, round
  498. a = arange(0, 60, dtype = 'd').reshape(3,4,5) + .3
  499. r1 = self.rxns['NTRplOH']
  500. r2 = r1 * a
  501. for spcn in "NTR OH".split():
  502. spc = self.spcs[spcn]
  503. self.assertTrue((r2[spc] == -a).all())
  504. for spcn in "HNO3 HO2".split():
  505. spc = self.spcs[spcn]
  506. self.assertTrue((r2[spc] == a).all())
  507. for spcn in "FORM ALD2 ALDX".split():
  508. spc = self.spcs[spcn]
  509. self.assertTrue((r2[spc] == (a * float64(.33))).all())
  510. for spcn in ["ALD"]:
  511. spc = self.spcs[spcn]
  512. # Value is not exact because of addition
  513. self.assertTrue((round(r2[spc], decimals = 8) == round(a * float64(.99), decimals = 8)).all())
  514. for spcn in ["PAR"]:
  515. spc = self.spcs[spcn]
  516. self.assertTrue((r2[spc] == (-a * .66)).all())
  517. if __name__ == '__main__':
  518. unittest.main()