CacheMgr.py 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. '''
  2. CacheMgr is a collection of functions to handle the automatic result caching
  3. for FD. It handles primitive Python types as well as Numpy ndarrays, and has
  4. facilities for caching both in the numpy ".npy" format as well as JSON.
  5. There are three user-facing functions:
  6. CacheMgr.Element : Decorator; cache the result of the decorated function
  7. CacheMgr.AtomicElement : Decorator; atomically cache the result of the
  8. decorated function
  9. CacheMgr.SymArray : Decorator; cache the result of the deorated function
  10. assuming that the result is a symmetric ndarray and
  11. save only unique elements to disk.
  12. '''
  13. import itertools, json, os
  14. import numpy as np
  15. from numpy.lib.format import open_memmap
  16. from Tools.PrintMgr import *
  17. # Format path elements from pathTpl, create the directory chain,
  18. # return the full path, filename and extension.
  19. def _mkPath(pathTpl, *args, **kwargs):
  20. path = [x.format(*args, **kwargs) for x in pathTpl]
  21. FullPath = os.path.join(*path)
  22. Ext = os.path.splitext(path[-1])[1].lower()
  23. for n in range(1, len(path)):
  24. try:
  25. os.mkdir( os.path.join(*path[:n]) )
  26. except OSError:
  27. pass
  28. return FullPath, path[-1], Ext
  29. # Atomic file creation method
  30. def _acreate(filename):
  31. fd = os.open(filename, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0644)
  32. return os.fdopen(fd, 'wb')
  33. def _fcreate(filename):
  34. fd = os.open(filename, os.O_CREAT | os.O_WRONLY, 0644)
  35. return os.fdopen(fd, 'wb')
  36. # Save a sparse symmetric array.
  37. def _saveSparseSym(Mx, file):
  38. idx = np.ndindex(*Mx.shape)
  39. sdict = { tuple(sorted(i)) : Mx[i] for i in idx if Mx[i] != 0 }
  40. keys = zip(*sdict.keys())
  41. vals = sdict.values()
  42. json.dump( (Mx.shape, keys, vals), file)
  43. # Load a spare symmetric array.
  44. def _loadSparseSym(file):
  45. shp, keys, vals = json.load( file )
  46. Mx = np.zeros(shp)
  47. for x in itertools.permutations(keys):
  48. Mx[x] = vals
  49. return Mx
  50. # Cache / calculate a symmetric, multi-dimensional numpy array.
  51. def SymArray(*pathTpl):
  52. '''
  53. Cache a symmetric, multi-dimensional ndarray.
  54. This decorator should wrap a function that returns a symmetric,
  55. n-dimensional ndarray. The decorator's positional arguments should specify
  56. the path for the cache file (one path component per argument). The ndarray
  57. is saved as a sparse JSON file, that is, if the wrapped function returns a
  58. 3-axis ndarray, then only a single element is written to disk for the six
  59. components
  60. (1,2,3), (2,1,3), (1,3,2), (2,3,1), (3,2,1), (3,1,2)
  61. which, by symmetric, are presumed to be equal.
  62. When the cache is loaded, the saved file is expanded to a full non-sparse
  63. ndarray.
  64. '''
  65. def owrap(func):
  66. def wrap(self, *args, **kwargs):
  67. FullPath, Name, Ext = _mkPath(pathTpl, *args, self=self, **kwargs)
  68. desc = kwargs.get("desc", "")
  69. try:
  70. with open(FullPath, 'rb') as file:
  71. pini("Loading %s (%s)" % (desc, Name) )
  72. M = _loadSparseSym(file)
  73. pend("Success")
  74. return M
  75. except IOError:
  76. pass
  77. pini("Calculating %s" % desc)
  78. M = func(self, *args)
  79. pend("Done")
  80. with open(FullPath, 'wb') as file:
  81. pini("Saving %s (%s)" % (desc, Name))
  82. _saveSparseSym(M, file)
  83. pend("Success")
  84. return M
  85. return wrap
  86. return owrap
  87. # Cache / calculate a generic numpy array or JSON-serializable object.
  88. def _fsave(file, Ext, res):
  89. if Ext == ".npy":
  90. np.save (file, res)
  91. elif Ext== ".json":
  92. json.dump(res, file)
  93. else:
  94. print "Unknown file extension '%s' from file '%s'." % (Ext, Name)
  95. def _fload(Ext, FullPath):
  96. if Ext == ".npy":
  97. return np.load(FullPath, mmap_mode='r')
  98. elif Ext==".json":
  99. with open(FullPath, 'rb') as file:
  100. return json.load(file)
  101. else:
  102. print "Unknown file extension '%s' from file '%s'." % (Ext, Name)
  103. raise ValueError
  104. def Element(*pathTpl):
  105. '''
  106. Cache a function result.
  107. The wrapped function can return an ndarray or any JSON-serializable Python
  108. structure. The decorator's positional arguments should specify the path
  109. for the cache file (one path component per argument). The result is saved
  110. as a JSON or npy file depending on the extension of the last path element.
  111. If the file already exists but cannot be deserialized, Element re-computes
  112. the wrapped function.
  113. '''
  114. def owrap(func):
  115. def wrap(self, *args, **kwargs):
  116. FullPath, Name, Ext = _mkPath(pathTpl, *args, self=self, **kwargs)
  117. # Try to load the file
  118. try:
  119. return _fload(Ext, FullPath)
  120. except (ValueError, IOError):
  121. pass
  122. # Otherwise, re-create the file and re-compute contents.
  123. with _fcreate(FullPath) as file:
  124. res = func(self, *args, **kwargs)
  125. _fsave(file, Ext, res)
  126. return _fload(Ext, FullPath)
  127. return wrap
  128. return owrap
  129. def AtomicElement(*pathTpl):
  130. '''
  131. Cache a function result.
  132. The wrapped function can return an ndarray or any JSON-serializable Python
  133. structure. The decorator's positional arguments should specify the path
  134. for the cache file (one path component per argument). The result is saved
  135. as a JSON or npy file depending on the extension of the last path element.
  136. AtomicElement uses an atomic file creation to check if the result cache is
  137. either existing or in progress. If the file exists and can be loaded, it
  138. is loaded. If it exists and cannot be loaded, AtomicElement returns a
  139. value of zero.
  140. '''
  141. def owrap(func):
  142. def wrap(self, *args, **kwargs):
  143. FullPath, Name, Ext = _mkPath(pathTpl, *args, self=self, **kwargs)
  144. # Try an atomic file creation; if successful, run and save wrapped calculation.
  145. try:
  146. with _acreate(FullPath) as file:
  147. res = func(self, *args, **kwargs)
  148. _fsave(file, Ext, res)
  149. except OSError:
  150. pass
  151. # Otherwise, the file exists, so just load it.
  152. try:
  153. return _fload(Ext, FullPath)
  154. except (ValueError, IOError):
  155. return 0.0
  156. return wrap
  157. return owrap