OrthExp.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  1. '''
  2. An implementation of an orthonormal basis: the orthogonal exponentials. The
  3. implementation relies on the machinery for three-term recurrence relations
  4. implemented in Tools.Base.
  5. The orthonormal exponentials are constructed by orthogonalizing the set of
  6. functions:
  7. F(n) = exp( -n*z )
  8. z = (x-x0)/Lambda) ** Alpha
  9. (ignoring normalization constants) where
  10. n in a positive integer indexing the functions.
  11. x0 is a real number specifying the lower mass cut.
  12. Alpha is a positive, dimensionless real numbers.
  13. Lambda is positive real number with dimensions of energy.
  14. It turns out that this set of functions is complete with respect to real-valued
  15. functions who tend to zero as their argument goes to infinity.
  16. Unfortunately, it is difficult to deal with this series directly: the functions
  17. F(n) are nearly degenerate, and working with more than a small handful of terms
  18. is numerically impossible.
  19. Fortunately, they can be used to produce an orthonormal basis which does not
  20. have these numerical issues and is extraordinarily convenient for modeling the
  21. falling spectra frequently seen in high-energy physics.
  22. This orthonormal basis, the orthonormal exponentials, can be written using
  23. three-term recurrence relations:
  24. E(n+1) = a*exp( -n*z )*E(n) + b*E(n) + c*E(n-1)
  25. The recurrence relations are fast and numerically stable, and can readily be
  26. used to evaluate n ~ 10000 and higher - the upper limit has not been explored.
  27. They are particularly advantageous when all functions from n=1 to n=N must be
  28. evaluated, as each F_n must naturally be computed along the way to F_N!
  29. Hyperparameters
  30. ---------------
  31. x0, Alpha and Lambda are hyperparameters that specify a coordinate
  32. transformation that converts the dimensioned coordinate x into a nondimensional
  33. coordinate z. The hyperparameters are arbitrary - every choice of Alpha and
  34. Lambda results in a complete basis. Some choices are more effective than
  35. others, though, and so Tools.Decomp provides machinery for making some (semi-)
  36. automated optimizations of these.
  37. The OrthExp Module
  38. ------------------
  39. This module implements the orthonormal exponentials and transformations between
  40. different choices of hyperparameters. It relies on the base classes defined
  41. in Tools.Base. The following classes are provided:
  42. ExpDecompFn : Evaluate the orthonormal exponentials at a list of
  43. points. Also compute the series coefficients of this
  44. dataset in the orthonormal exponentials.
  45. ExpDecompMxForm : Use the recursion relations to take a 1-dimensional
  46. moment vector and convert it into its corresponding
  47. matrix form.
  48. ExpPrior : Return the moments of the first orthonormal
  49. exponential, i.e. (0, 1, 0, 0, 0, ...) with a proper
  50. normalization.
  51. ExpDecompTransform : Transform a moment vector created with one set of
  52. hyperparameters into the moment vector with a
  53. different set of hyperparameters.
  54. ExpDecompFactory : Implementation of Tools.Base.Factory to store the
  55. hyperparameters and produce instantiations of the
  56. above objects having those hyperparameters.
  57. Users should normally instantiate an ExpDecompFactory and use that to produce
  58. the other objects.
  59. '''
  60. import numpy as np
  61. import numexpr as ne
  62. import Tools.Base as Base
  63. from Tools.PrintMgr import *
  64. from numpy.core.umath import euler_gamma
  65. from scipy.special import exprel
  66. from fractions import Fraction
  67. from math import log, sqrt, ceil
  68. #### Orthogonal exponentials as functions
  69. class ExpDecompFn (Base.Basis):
  70. '''
  71. The orthonormal exponentials as functions.
  72. This class is instantiated as
  73. En = ExpDecompFn(x, w, **kwargs)
  74. where x and w are one-dimensional, same-sized ndarrays containing the
  75. positions and their weights, respectively. The hyperparameters
  76. (x0, Lambda, and Alpha) must be supplied as keyword arguments, as must
  77. Nbasis (the maximum N to evaluate).
  78. If ExpDecompFn is produced by an ExpDecompFactory object (this is generally
  79. the correct apprach), then the hyperparameters stored in the factory object
  80. will automatically be supplied.
  81. ExpDecompFn acts as an iterator, so the values [ E1(x), E2(x), E3(x), ... ]
  82. can be stepped through in order like this:
  83. for x in En:
  84. print x.N, x.Values()
  85. x.Values() returns an array with the same shape as x, with the values of
  86. the N'th orthonormal exponential at each x. If x is a dataset, its moments
  87. can be obtained using the Moment() method:
  88. for x in En:
  89. print x.N, x.Moment()
  90. '''
  91. _param = Base.Basis._param + ( "x0", "Lambda", "Alpha" )
  92. # User-facing functions.
  93. def Values(self): return self['t'][ self.N % 2 ] * np.sqrt(self.NormSq(self.N)) * self['xf']
  94. def Zeros (self, shape=()): return np.zeros(shape + self['x'].shape)
  95. def NormSq(self, N): return 2. / N if N > 0 else 0
  96. def Base0 (self, out): out[:] = 0
  97. def Base1 (self, out): out[:] = self['rz']
  98. def Raise (self, out): return ne.evaluate( "exp( -(x-x0) * xf / Alpha)", self.ParamVal, out=out)
  99. def Xfrm (self, out): return ne.evaluate( "(Alpha/Lambda) * ((x-x0)/Lambda)**(Alpha-1)", self.ParamVal, out=out)
  100. # The recurrence relation itself.
  101. def Recur(self, N, En, Ep, out):
  102. A = float(4*N + 2) / N
  103. B = float(4*N) / (2*N - 1)
  104. C = float(2*N + 1) / (2*N - 1)
  105. ne.evaluate( "A*rz*En - B*En - C*Ep", global_dict = self, out=out)
  106. def Moment(self): return np.dot(self['t'][ self.N % 2 ], self['w']) * sqrt(self.NormSq(self.N))
  107. def __init__(self, x, w, **kwargs):
  108. Base.Basis.__init__(self, **kwargs)
  109. self['x'] = x
  110. self['w'] = w
  111. def Reinit(self, x, w):
  112. self['x'] = x
  113. self['w'] = w
  114. #### Class to elevate a decomposition to it's matrix form
  115. class ExpDecompMxForm (Base.Basis):
  116. '''
  117. A class to elevate a decomposition from its vector form to its matrix form.
  118. This class is initialized with a moment vector <orig>. It implements the
  119. iterator interface, and each step of the iteration returns the moment
  120. vectors of <E_N * orig>. That is, if <orig> is the decomposition of some
  121. function F(x), then each iteration returns the decomposition of
  122. F(x) * E_n(x).
  123. The primary utility is that this allows easy construction of the matrix
  124. representation of <orig>, and hence provides a convenient mechanism to
  125. construct the covariance matrix between the elements of the <orig> vector.
  126. Instantiate this as
  127. MxForm = ExpDecompMxForm(*mom, Nbasis=N)
  128. where N is the maximum n to evaluate. If multiple original moment vectors
  129. are supplied, then they are iterated through simultaneously, e.g.
  130. MxForm = ExpDecompMxForm(origA, origB, origC, Nbasis=N)
  131. for x in MxForm:
  132. V = x.Values()
  133. print "Decomposition of A*E_%d:" % x.N, V[0]
  134. print "Decomposition of B*E_%d:" % x.N, V[1]
  135. print "Decomposition of C*E_%d:" % x.N, V[2]
  136. '''
  137. def Values(self): return self['t'][ self.N % 2 ][:,1:self["Nbasis"]+1]
  138. def Zeros (self, shape=()): return np.zeros(shape + self['x'].shape)
  139. def Base0 (self, out): out[:] = 0
  140. def Base1 (self, out): out[:] = 0; self.Recur(0, self['x'], self['t'][0], out)
  141. def Raise (self, out):
  142. n = np.arange(self["Nrow"]-2, dtype=np.float)
  143. out = np.zeros((2, self["Nrow"]))
  144. out[0,1:-1] = np.sqrt(n*(n+1)) / ( 2*(2*n+1) ) #sub/sup diagonal
  145. out[1,1:-1] = 2*n**2 / ((2*n-1)*(2*n+1) ) #diagonal
  146. out[1,-2] = 0
  147. return out
  148. def Xfrm (self, out): return None
  149. # The recurrence relation itself.
  150. def Recur(self, N, En, Ep, out):
  151. N = float(N)
  152. if N > 0:
  153. e = sqrt( N / (N+1) )
  154. f = sqrt( (N-1) / (N+1) )
  155. A = e * (4*N + 2) / N
  156. B = e * (4*N) / (2*N - 1)
  157. C = f * (2*N + 1) / (2*N - 1)
  158. else:
  159. A,B,C = 2.0,0.0,0.0
  160. dg = self['rz'][1,1:-1]
  161. us = self['rz'][0,1:-1]
  162. ds = self['rz'][0,0:-2]
  163. x = En[:,1:-1]
  164. u = En[:,2: ]
  165. d = En[:, :-2]
  166. p = Ep[:,1:-1]
  167. return ne.evaluate("A*(dg*x + us*u + ds*d) - B*x - C*p ", out=out[:,1:-1])
  168. def __init__(self, *mom, **kwargs):
  169. Base.Basis.__init__(self, **kwargs)
  170. Nmom = max([x.size for x in mom])
  171. Nbasis = self["Nbasis"]
  172. Nrow = Nmom + Nbasis + 2
  173. self["Nrow"] = Nrow
  174. self['x'] = np.zeros((len(mom), Nrow))
  175. for n,x in enumerate(mom):
  176. self['x'][n,1:len(x)+1] = x
  177. #### Decomposition of a simple exponential e**(-x/Lambda)
  178. class ExpPrior (Base.Basis):
  179. '''
  180. Return the moments of a distribution consisting of only the first
  181. orthonormal exponential, properly normalized.
  182. '''
  183. _param = Base.Basis._param + ( "Alpha", "Lambda" )
  184. def Moment(self):
  185. if self.N != 1:
  186. return 0
  187. return 1./sqrt(2)
  188. #### Transformation matrix generator for orthogonal exponentials.
  189. class ExpDecompTransform(Base.Transform):
  190. '''
  191. Transformation object for the orthonormal exponentials.
  192. ExpDecompTransform implements transformations on Alpha and Lambda for the
  193. orthonormal exponentials (x0 cannot be transformed).
  194. Mandatory argument on initialization are Alpha, Lambda, and Nxfrm. Nxfrm
  195. specifies the size of the transformation matrices (i.e. the maximum number
  196. of moments that can be transformed). Alpha and Lambda specify the
  197. hyperparameters for the starting point of the transformation.
  198. Initialize and use it like this:
  199. Xfrm = ExpDecompTransform(Alpha=AlphaIni, Lambda=LambdaIni, Nxfrm=N)
  200. NewMom = Xfrm(OldMom, Alpha=AlphaNew, Lambda=LambdaNew)
  201. The remaining methods in ExpDecompTransform exist to compute the
  202. transformation matrices, and are not intended for direct use. If AlphaNew
  203. is unspecified, the transformation assumes that AlphaNew = AlphaOld, and
  204. likewise for Lambda.
  205. '''
  206. _param = Base.Transform._param + ("Lambda", "Alpha")
  207. # Return a high-accuracy rational approximation of log(n) for integer n
  208. # Use recursion and cache results to enhance performance.
  209. def _log(self, n, numIni=96, numMin=48):
  210. if n <= 1: return 0
  211. if n not in self:
  212. num = max( numIni - (n - 2), numMin )
  213. s = [ Fraction(2, 2*k+1) * Fraction(1, 2*n-1) ** (2*k+1) for k in range(num) ]
  214. self[n] = self._log(n-1) + sum( s )
  215. return self[n]
  216. # Recursion relations for series coefficients
  217. def CoeffOrth(self, n):
  218. r = [ 0, Fraction((-1)**(n+1) * n) ]
  219. for m in range(1, n):
  220. r.append( Fraction(m**2 - n**2, m*(m+1)) * r[-1] )
  221. return r
  222. def CoeffDeriv(self, n):
  223. r = [ 0, Fraction((-1)**n * n) ]
  224. for m in range(1, n):
  225. r.append( Fraction(m**2 - n**2, m**2) * r[-1] )
  226. return r
  227. def Norm (self, *arg): return sqrt(np.prod([ 2*a for a in arg]))
  228. # Infinitesimal transformations
  229. @Base.Transform.OrthogonalizeHankel
  230. def Alpha (self, n): return (1 - Fraction(euler_gamma) - self._log(n)) / n**2 if n > 0 else 0
  231. @Base.Transform.OrthogonalizeHankel
  232. def Lambda (self, n): return Fraction(1, n**2) if n > 0 else 0
  233. # Infinitesimal transformation parameters
  234. # 'fin' is a dict with the final value for all parameter;
  235. # 'ini' is a dict with the initial value for all parameters
  236. def XfPar (self, fin, ini):
  237. r = log( fin["Lambda"] / ini["Lambda"] )
  238. yc = log( fin["Alpha"] / ini["Alpha"] )
  239. ys = -fin["Alpha"] * r / exprel(yc)
  240. return { "Alpha": yc, "Lambda": ys }
  241. #### A decomposer factory using the orthonormal exponentials
  242. class ExpDecompFactory ( Base.DecompFactory ):
  243. '''
  244. A factory object for the orthonormal exponentials.
  245. The ExpDecompFactory is the preferred way to store hyperparameter values
  246. and to create the ExpDecompFn, ExpDecompMxForm, and ExpDecompTransform
  247. objects.
  248. A simple example, assuming that 'x' and 'w' are a list of points and the
  249. corresponding weights, respectively:
  250. Factory = ExpDecompFactory(Alpha=AlphaOld, Lambda=LambdaOld, x0=x0
  251. Nbasis=Nbasis, Nxfrm=Nxfrm, Ncheck=Ncheck)
  252. Mom = [ r.Moment() for r in Factory.Fn(x, w) ]
  253. MomCov = Factory.CovMatrix( Mom )
  254. Xfrm = Factory.Xfrm()
  255. MomNew = Xfrm(Mom, Alpha=AlphaNew, Lambda=LambdaNew)
  256. MomNewCov = Factory.CovMatrix(MomNew)
  257. This
  258. 1. Creates a factory object with hyperparameters (AlphaOld, LambdaOld).
  259. 2. Computes the moments (i.e. the decomposition) of the dataset x.
  260. 3. Computes the covariance of those moments.
  261. 4. Transforms the moments to the hyperparameters (AlphaNew, LambdaNew).
  262. 5. Computes the covariance matrix in the transformed basis.
  263. The ExpDecompFactory is the preferred top-level interface for using all of
  264. the implementation of the orthonormal exponentials.
  265. '''
  266. _param = tuple(set( Base.DecompFactory._param
  267. + ExpDecompFn._param
  268. + ExpDecompMxForm._param
  269. + ExpPrior._param))
  270. _fitparam = ("Alpha", "Lambda")
  271. # Methods to create the function, matrix and weight objects with correct parameters.
  272. def Fn (self, x, w, **kw): return ExpDecompFn (x, w, **self._arg(kw) )
  273. def MxForm(self, *x, **kw): return ExpDecompMxForm (*x, **self._arg(kw) )
  274. def Pri (self, **kw): return ExpPrior ( **self._arg(kw) )
  275. def Xfrm (self, **kw): return ExpDecompTransform ( **self._arg(kw) )