PageRenderTime 59ms CodeModel.GetById 22ms RepoModel.GetById 1ms app.codeStats 0ms

/inverselaplace.py

https://bitbucket.org/klkuhlm/mpmath-transforms
Python | 610 lines | 353 code | 163 blank | 94 comment | 63 complexity | e23ed62311ac3f3b65ca1b518e17eff7 MD5 | raw file
  1. class InverseLaplaceTransform(object):
  2. """
  3. Inverse Laplace transform is implemented using this class
  4. """
  5. def __init__(self, ctx):
  6. self.ctx = ctx
  7. # a default level of approximation appropriate
  8. # for the given precision goal
  9. self.degree = 18
  10. # decimal digits of precision for computing p vectors,
  11. # and f(t) solutions. (Talbot and Stehfest have
  12. # their own expressions that override this minimum)
  13. self.dps_goal = 30
  14. self.debug = 0
  15. def calc_laplace_parameter(self,t,**kwargs):
  16. raise NotImplementedError
  17. def calc_time_domain_solution(self,fp):
  18. raise NotImplementedError
  19. class FixedTalbot(InverseLaplaceTransform):
  20. def calc_laplace_parameter(self, t, **kwargs):
  21. # time of desired approximation
  22. self.t = self.ctx.convert(t)
  23. # maximum time desired (used for scaling)
  24. self.tmax = self.ctx.convert(kwargs.get('tmax',self.t))
  25. # integer order of approximation
  26. self.degree = kwargs.get('degree',self.degree)
  27. # rule for extended precision from Abate & Valko (2004)
  28. # "Multi-precision Laplace Transform Inversion"
  29. self.dps_goal = kwargs.get('dps',max(self.dps_goal,self.degree))
  30. # Abate & Valko rule of thumb
  31. self.r = kwargs.get('r',2*self.degree/(5*self.tmax))
  32. self.debug = kwargs.get('debug',self.debug)
  33. M = self.degree
  34. self.p = self.ctx.matrix(M,1)
  35. self.p[0] = self.r
  36. with self.ctx.workdps(self.dps_goal):
  37. self.theta = self.ctx.linspace(0.0, self.ctx.pi, M+1)
  38. for i in range(1,M):
  39. self.p[i] = self.r*self.theta[i]*(
  40. self.ctx.cot(self.theta[i]) + 1j)
  41. if self.debug > 1:
  42. print 'FixedTalbot p:',self.p
  43. if self.debug > 2:
  44. print ('FixedTalbot tmax,degree,dps_goal,r',
  45. self.tmax,self.degree,self.dps_goal,self.r)
  46. def calc_time_domain_solution(self,fp):
  47. theta = self.theta
  48. M = self.degree
  49. t = self.t
  50. p = self.p
  51. r = self.r
  52. ans = self.ctx.matrix(M,1)
  53. with self.ctx.workdps(self.dps_goal):
  54. for i in range(1,M):
  55. ans[i] = self.ctx.exp(t*p[i])*fp[i]*(
  56. 1 + (theta[i] + (theta[i]*self.ctx.cot(theta[i]) - 1)
  57. *self.ctx.cot(theta[i]))*1j)
  58. result = r/M*(fp[0]/2*self.ctx.exp(r*t) + self.ctx.fsum(ans))
  59. return result.real
  60. # ****************************************
  61. class Weeks(InverseLaplaceTransform):
  62. def calc_laplace_parameter(self, t, **kwargs):
  63. # time of desired approximation
  64. self.t = self.ctx.convert(t)
  65. # maximum time desired (used for scaling)
  66. self.tmax = self.ctx.convert(kwargs.get('tmax',self.t))
  67. # equations defined in terms of 2M below;
  68. # number of quadrature points
  69. self.degree = int(kwargs.get('degree',self.degree)/2.0)
  70. # highest Laguerre polynomial degree
  71. self.N = kwargs.get('N',self.degree)
  72. # F(p) ~ p**(-s), as p -> infinity (also as t -> 0)
  73. self.s = self.ctx.convert(kwargs.get('s',1))
  74. # Weeks' rules of thumb (can improve on these?)
  75. self.kappa = self.ctx.convert(kwargs.get('kappa',1/self.tmax))
  76. self.b = self.ctx.convert(kwargs.get('b',self.N*self.kappa))
  77. # no real rule of thumb for increasing precsion as
  78. # degree of approximation increases
  79. self.dps_goal = kwargs.get('dps',self.dps_goal)
  80. self.debug = kwargs.get('debug',self.debug)
  81. M = self.degree
  82. self.z = self.ctx.matrix(2*M,1)
  83. self.p = self.ctx.matrix(2*M,1)
  84. self.theta = self.ctx.matrix(2*M,1)
  85. with self.ctx.workdps(self.dps_goal):
  86. for i in range(2*M):
  87. self.theta[i] = ((i-M)+0.5)/M
  88. # midpoint rule around unit circle
  89. self.z[i] = self.ctx.expjpi(self.theta[i]) # fcn includes i*pi
  90. for j in range(2*M):
  91. # Mobius mapping back onto right half plane
  92. self.p[j] = self.kappa - self.b/2 + self.b/(1-self.z[j])
  93. if self.debug > 1:
  94. print 'Weeks p:',self.p
  95. if self.debug > 2:
  96. print ('Weeks tmax,degree,N,s,kappa,b,dps_goal:',
  97. self.tmax,self.degree,self.N,self.s,
  98. self.kappa,self.b,self.dps_goal)
  99. def _coeff(self,n,fp):
  100. """use midpoint rule for calculating a_n coefficients
  101. this is the approach of Weideman (1999)."""
  102. M = self.degree
  103. b = self.b
  104. s = self.s
  105. z = self.z
  106. theta = self.theta
  107. arg = self.ctx.matrix(2*M,1)
  108. for i in range(2*M):
  109. # fcn includes i*pi
  110. arg[i] = (self.ctx.power(b/(1-z[i]),s)*
  111. fp[i]*self.ctx.expjpi(-theta[i]*n))
  112. return self.ctx.fsum(arg)/(2*M)
  113. def calc_time_domain_solution(self,fp):
  114. b = self.b
  115. s = self.s
  116. kappa = self.kappa
  117. N = self.N
  118. t = self.t
  119. arg = self.ctx.matrix(N,1)
  120. with self.ctx.workdps(self.dps_goal):
  121. for n in range(N):
  122. arg[n] = (self._coeff(n,fp)*self.ctx.fac(n)/
  123. self.ctx.fac(s+n-1)*self.ctx.laguerre(n,s-1,b*t))
  124. result = (self.ctx.exp(t*(kappa - b/2))*
  125. self.ctx.power(t,s-1)*self.ctx.fsum(arg))
  126. return result.real
  127. # ****************************************
  128. class Piessens(InverseLaplaceTransform):
  129. def calc_laplace_parameter(self, t, **kwargs):
  130. # time of desired approximation
  131. self.t = self.ctx.convert(t)
  132. # maximum time desired (used for scaling)
  133. self.tmax = self.ctx.convert(kwargs.get('tmax',self.t))
  134. # M+1 terms are used below
  135. self.degree = kwargs.get('degree',self.degree)
  136. # highest 2F2 degree
  137. self.N = kwargs.get('N',self.degree)
  138. # F(p) ~ p**(-s), as p -> infinity (and as t -> 0)
  139. self.s = self.ctx.convert(kwargs.get('s',1))
  140. # Piessens didn't have a good "rule of thumb"?
  141. self.b = self.ctx.convert(kwargs.get('b',1/(self.N*self.tmax)))
  142. # no real rule of thumb for increasing precsion as
  143. # degree of approximation increases
  144. self.dps_goal = kwargs.get('dps',self.dps_goal)
  145. self.debug = kwargs.get('debug',self.debug)
  146. M = self.degree
  147. self.theta = self.ctx.matrix(M+1,1)
  148. self.z = self.ctx.matrix(M+1,1)
  149. self.p = self.ctx.matrix(M+1,1)
  150. with self.ctx.workdps(self.dps_goal):
  151. for i in range(M+1):
  152. self.theta[i] = self.ctx.fraction(2*i+1,(M+1)*2)
  153. # pi included in fcn
  154. self.z[i] = self.ctx.cospi(self.theta[i])
  155. for j in range(M+1):
  156. self.p[j] = self.b/(1-self.z[j])
  157. if self.debug > 1:
  158. print 'Piessens p:',self.p
  159. if self.debug > 2:
  160. print ('Piessens tmax,degree,N,s,b,dps_goal:',
  161. self.tmax,self.degree,self.N,self.s,self.b,self.dps_goal)
  162. def _coeff(self,n,fp):
  163. """use quadrature rule for calculating coefficients."""
  164. M = self.degree
  165. s = self.s
  166. b = self.b
  167. z = self.z
  168. theta = self.theta
  169. arg = self.ctx.matrix(M+1,1)
  170. for i in range(M+1):
  171. # pi included in fcn
  172. arg[i] = (self.ctx.power(b/(1-z[i]),s)*
  173. fp[i]*self.ctx.cospi(theta[i]*n))
  174. return self.ctx.fraction(2,M+1)*self.ctx.fsum(arg)
  175. def calc_time_domain_solution(self,fp):
  176. s = self.s
  177. b = self.b
  178. N = self.N
  179. t = self.t
  180. arg = self.ctx.matrix(N,1)
  181. with self.ctx.workdps(self.dps_goal):
  182. for n in range(N):
  183. arg[n] = (self._coeff(n,fp)*
  184. self.ctx.hyp2f2(-n,n,0.5,s,b*t))
  185. arg[0] /= 2.0
  186. result = (self.ctx.power(t,s-1)/self.ctx.gamma(s)*
  187. self.ctx.fsum(arg))
  188. # ignore any small imaginary part
  189. return result.real
  190. # ****************************************
  191. class Stehfest(InverseLaplaceTransform):
  192. def calc_laplace_parameter(self, t, **kwargs):
  193. # time of desired approximation
  194. self.t = self.ctx.convert(t)
  195. self.degree = kwargs.get('degree',self.degree)
  196. # rule for extended precision from Abate & Valko (2004)
  197. # "Multi-precision Laplace Transform Inversion"
  198. self.dps_goal = kwargs.get('dps',max(self.dps_goal,
  199. 2.1*self.degree))
  200. self.debug = kwargs.get('debug',self.debug)
  201. M = self.degree
  202. # don't compute V here, too expensive (?)
  203. self.V = kwargs.get('V',None)
  204. with self.ctx.workdps(self.dps_goal):
  205. self.p = (self.ctx.matrix(self.ctx.arange(1,M+1))*
  206. self.ctx.ln2/self.t)
  207. if self.debug > 1:
  208. print 'Stehfest p:',self.p
  209. if self.debug > 2:
  210. print ('Stehfest degree,dps_goal:',
  211. self.tmax,self.degree,self.dps_goal)
  212. def _coeff(self):
  213. """Stehfest coefficients only depend on M"""
  214. M = self.degree
  215. # order must be odd
  216. assert M%2 == 0
  217. M2 = M/2
  218. self.V = self.ctx.matrix(M,1)
  219. # Salzer summation weights
  220. for k in range(1,M+1):
  221. z = self.ctx.matrix(int(min(k,M2)+1),1)
  222. for j in range(int(self.ctx.floor((k+1)/2.0)),min(k,M2)+1):
  223. z[j] = (j**M2*self.ctx.fac(2*j)/
  224. (self.ctx.fac(M2-j)*self.ctx.fac(j)*
  225. self.ctx.fac(j-1)*
  226. self.ctx.fac(k-j)*self.ctx.fac(2*j-k)))
  227. self.V[k-1] = self.ctx.power(-1,k+M2)*self.ctx.fsum(z)
  228. def calc_time_domain_solution(self,fp):
  229. """Compute time-domain solution using f(p)
  230. and coefficients"""
  231. with self.ctx.workdps(self.dps_goal):
  232. if self.V is None:
  233. self._coeff()
  234. else:
  235. self.V = self.ctx.convert(self.V)
  236. result = self.ctx.fdot(self.V,fp)*self.ctx.ln2/self.t
  237. # ignore any small imaginary part
  238. return result.real
  239. # ****************************************
  240. class GaverRho(InverseLaplaceTransform):
  241. def calc_laplace_parameter(self, t, **kwargs):
  242. # time of desired approximation
  243. self.t = self.ctx.convert(t)
  244. self.degree = kwargs.get('degree',self.degree)
  245. # rule for extended precision from Abate & Valko (2004)
  246. # "Multi-precision Laplace Transform Inversion"
  247. self.dps_goal = kwargs.get('dps',max(self.dps_goal,
  248. 2.1*self.degree))
  249. self.debug = kwargs.get('debug',self.debug)
  250. M = self.degree
  251. if M%2 == 1:
  252. M += 1
  253. self.degree = M
  254. with self.ctx.workdps(self.dps_goal):
  255. self.tau = self.ctx.ln2/self.t
  256. self.p = (self.ctx.matrix(self.ctx.arange(1,M+1))*self.tau)
  257. if self.debug > 1:
  258. print 'Stehfest p:',self.p
  259. if self.debug > 2:
  260. print ('Stehfest degree,dps_goal:',
  261. self.tmax,self.degree,self.dps_goal)
  262. def calc_time_domain_solution(self,fp):
  263. """Compute time-domain solution using f(p)
  264. and coefficients"""
  265. M = self.degree
  266. M2 = M/2
  267. with self.ctx.workdps(self.dps_goal):
  268. # compute Gaver functionals
  269. fkt = self.ctx.matrix(M2,1)
  270. arg = self.ctx.matrix(M2+1,1)
  271. for k in range(1,M2+1):
  272. arg[:] = 0.0
  273. for j in range(k+1):
  274. print M,M2,k,j,fp.rows
  275. arg[j] = (self.ctx.power(-1,j)*
  276. self.ctx.binomial(k,j)*fp[j+k-1])
  277. fkt[k-1] = (k*self.tau*self.ctx.binomial(2*k,k)*
  278. self.ctx.fsum(arg[0:k+1]))
  279. # apply Wynn's Rho non-linear
  280. # sequence transformation
  281. rho = self.ctx.matrix(M2,M2+2)
  282. # rho[:,0] = 0.0
  283. rho[:,1] = fkt
  284. print M,M2,rho.rows,rho.cols
  285. for k in range(2,M2+2):
  286. for n in range(M2-(k-2)*2 - 1):
  287. denom = rho[n+1,k-1] - rho[n,k-1]
  288. print k,n,M2-(k-2)*2 - 1,denom
  289. print rho
  290. if abs(denom) > self.ctx.eps:
  291. rho[n,k] = rho[n+1,k-2] + k/denom
  292. else:
  293. print "Wynn's Rho cancellation at ",k,n
  294. return rho[n-1,k-1+2]
  295. # ignore any small imaginary part
  296. return rho[0,M2+2].real
  297. # ****************************************
  298. class deHoog(InverseLaplaceTransform):
  299. def calc_laplace_parameter(self, t, **kwargs):
  300. self.t = self.ctx.convert(t)
  301. # 2*M+1 terms are used below
  302. self.degree = int(kwargs.get('degree',self.degree)/2.0)
  303. # abcissa of convergence (rightmost pole)
  304. self.alpha = self.ctx.convert(kwargs.get('alpha',1.0E-8))
  305. # desired tolerance
  306. self.tol = self.ctx.convert(kwargs.get('tol',1.0E-7))
  307. self.np = 2*self.degree+1
  308. # scaling factor
  309. self.T = self.ctx.convert(kwargs.get('T',2*self.t))
  310. # no real rule of thumb for increasing precsion as
  311. # degree of approximation increases
  312. self.dps_goal = kwargs.get('dps',self.dps_goal)
  313. self.debug = kwargs.get('debug',self.debug)
  314. M = self.degree
  315. T = self.T
  316. self.p = self.ctx.matrix(2*M+1,1)
  317. with self.ctx.workdps(self.dps_goal):
  318. for i in range(2*M+1):
  319. self.p[i] = (self.alpha -
  320. self.ctx.log(self.tol)/(2*T) +
  321. self.ctx.pi*i/T*1j)
  322. if self.debug > 1:
  323. print 'de Hoog p:',self.p
  324. if self.debug > 2:
  325. print ('de Hoog degree,alpha,tol,T,dps_goal:',
  326. self.degree,self.alpha,self.tol,self.T,self.dps_goal)
  327. def calc_time_domain_solution(self,fp):
  328. M = self.degree
  329. np = self.np
  330. t = self.t
  331. T = self.T
  332. alpha = self.alpha
  333. tol = self.tol
  334. gamma = alpha - self.ctx.log(tol)/(2*T)
  335. e = self.ctx.matrix(np,M+1)
  336. q = self.ctx.matrix(np,M)
  337. d = self.ctx.matrix(np,1)
  338. A = self.ctx.matrix(np+2,1)
  339. B = self.ctx.matrix(np+2,1)
  340. self.dps_orig = self.ctx.dps
  341. self.ctx.dps = self.dps_goal
  342. # initialize Q-D table
  343. e[0:2*M,0] = 0.0
  344. q[0,0] = fp[1]/(fp[0]/2)
  345. for i in range(1,2*M):
  346. q[i,0] = fp[i+1]/fp[i]
  347. # rhombus rule for filling triangular Q-D table
  348. for r in range(1,M+1):
  349. # start with e, column 1, 0:2*M-2
  350. mr = 2*(M-r)
  351. e[0:mr,r] = q[1:mr+1,r-1] - q[0:mr,r-1] + e[1:mr+1,r-1]
  352. if not r == M:
  353. rq = r+1
  354. mr = 2*(M-rq)+1
  355. for i in range(mr):
  356. q[i,rq-1] = q[i+1,rq-2]*e[i+1,rq-1]/e[i,rq-1]
  357. # build up continued fraction coefficients
  358. d[0] = fp[0]/2
  359. for r in range(1,M+1):
  360. d[2*r-1] = -q[0,r-1] # even terms
  361. d[2*r] = -e[0,r] # odd terms
  362. # seed A and B for recurrence
  363. A[0] = 0.0
  364. A[1] = d[0]
  365. B[0:2] = 1.0
  366. # base of the power series
  367. z = self.ctx.expjpi(t/T) # i*pi is already in fcn
  368. # coefficients of Pade approximation
  369. # using recurrence for all but last term
  370. for i in range(1,2*M):
  371. A[i+1] = A[i] + d[i]*A[i-1]*z
  372. B[i+1] = B[i] + d[i]*B[i-1]*z
  373. # "improved remainder" to continued fraction
  374. brem = (1 + (d[2*M-1] - d[2*M])*z)/2
  375. # powm1(x,y) computes x^y - 1 more accurately near zero
  376. rem = brem*self.ctx.powm1(1 + d[2*M]*z/brem,0.5)
  377. # last term of recurrence using new remainder
  378. A[np] = A[2*M] + rem*A[2*M-1]
  379. B[np] = B[2*M] + rem*B[2*M-1]
  380. # diagonal Pade approximation
  381. # F=A/B represents accelerated trapezoid rule
  382. result = self.ctx.exp(gamma*t)/T*(A[np]/B[np]).real
  383. self.ctx.dps = self.dps_orig
  384. return result
  385. # ****************************************
  386. class LaplaceTransformInversionMethods:
  387. def __init__(ctx, *args, **kwargs):
  388. ctx._fixed_talbot = FixedTalbot(ctx)
  389. ctx._weeks = Weeks(ctx)
  390. ctx._piessens = Piessens(ctx)
  391. ctx._stehfest = Stehfest(ctx)
  392. ctx._gaverrho = GaverRho(ctx)
  393. ctx._de_hoog = deHoog(ctx)
  394. def invertlaplace(ctx, f, t, **kwargs):
  395. r"""
  396. Computes the numerical inverse Laplace transform for a
  397. Laplace-space function at a given time. The function being
  398. estimated is assumed to be a real-only function of time.
  399. """
  400. rule = kwargs.get('method','stehfest')
  401. if type(rule) is str:
  402. lrule = rule.lower()
  403. if lrule == 'talbot' or lrule == 'fixed-talbot':
  404. rule = ctx._fixed_talbot
  405. elif lrule == 'weeks':
  406. rule = ctx._weeks
  407. elif lrule == 'piessens':
  408. rule = ctx._piessens
  409. elif lrule == 'stehfest':
  410. rule = ctx._stehfest
  411. elif lrule == 'gaverrho':
  412. rule = ctx._gaverrho
  413. elif lrule == 'dehoog' or lrule == 'de-hoog':
  414. rule = ctx._de_hoog
  415. else:
  416. raise ValueError("unknown inversion method: %s" % rule)
  417. else:
  418. rule = rule(ctx)
  419. rule.calc_laplace_parameter(t,**kwargs)
  420. np = rule.p.rows # p is a column vector
  421. fp = ctx.matrix(np,1)
  422. for i in range(np):
  423. fp[i] = f(rule.p[i])
  424. v = rule.calc_time_domain_solution(fp)
  425. return v
  426. def invlaptalbot(ctx, *args, **kwargs):
  427. kwargs['method'] = 'talbot'
  428. return ctx.invertlaplace(*args, **kwargs)
  429. def invlapweeks(ctx, *args, **kwargs):
  430. kwargs['method'] = 'weeks'
  431. return ctx.invertlaplace(*args, **kwargs)
  432. def invlappiessens(ctx, *args, **kwargs):
  433. kwargs['method'] = 'piessens'
  434. return ctx.invertlaplace(*args, **kwargs)
  435. def invlapstehfest(ctx, *args, **kwargs):
  436. kwargs['method'] = 'stehfest'
  437. return ctx.invertlaplace(*args, **kwargs)
  438. def invlapgaverrho(ctx, *args, **kwargs):
  439. kwargs['method'] = 'gaverrho'
  440. return ctx.invertlaplace(*args, **kwargs)
  441. def invlapdehoog(ctx, *args, **kwargs):
  442. kwargs['method'] = 'dehoog'
  443. return ctx.invertlaplace(*args, **kwargs)
  444. # ****************************************
  445. #if __name__ == '__main__':
  446. #import doctest
  447. #doctest.testmod()