convolveLN.py 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171
  1. import numpy
  2. import os
  3. import scipy.interpolate
  4. import filionQuadrature
  5. def ln(mu,cv):
  6. #generate log-normal distributed variable with mean mu and coefficient of variation cv
  7. m=numpy.log(mu/numpy.sqrt(1+cv*cv))
  8. s=numpy.sqrt(numpy.log(1+cv*cv))
  9. return ln_s(m,s)
  10. def ln_s(mu_s,sigma_s):
  11. #generate r.v. with log-normal distribution
  12. #mu_s and sigma_s are parameters of the normal distribution in the exponent
  13. #see ln() for targeted mean and coefficient of variation
  14. z=numpy.random.normal()
  15. return numpy.exp(mu_s+sigma_s*z)
  16. def readCoef(sigma):
  17. #read coefficients of gamma distribution set that approximates LN distro
  18. #with mu_s=0 and sigma_s=sigma, valid for 0.05<=sigma<=3
  19. #from
  20. #Furman E, Hackmann D, Kuznetsov A.
  21. #On log-normal convolutions: An analytical–numerical method with
  22. #applications to economic capital determination.
  23. #Insurance: Mathematics and Economics, 90 (2020) 120-134.
  24. #return fa and fb which are alpha and beta for n=20 approximations
  25. fdir=os.path.join(os.path.expanduser('~'),'software','src','PBPK',
  26. 'lnnCoeficients20','cBase.txt')
  27. with open(fdir,'r') as f:
  28. lines=f.read().splitlines()
  29. fm=[q.split(' ') for q in lines]
  30. fm=numpy.array([[float(q) for q in x] for x in fm])
  31. fn=(fm.shape[1]-1)//2
  32. #print(fn)
  33. fa=fm[:,1:fn+1]
  34. fb=fm[:,fn+1:2*fn+1]
  35. fx=fm[:,0]
  36. falpha=[]
  37. fbeta=[]
  38. for i in range(fn):
  39. f2a = scipy.interpolate.interp1d(fx, fa[:,i], kind='cubic')
  40. try:
  41. falpha.append(f2a(sigma))
  42. except ValueError:
  43. #interpolation failed
  44. if sigma<fx[0]:
  45. falpha.append(fa[0,i])
  46. print('Using alpha[{}]={} at sigma={}'.format(fx[0],fa[0,i],sigma))
  47. f2b = scipy.interpolate.interp1d(fx, fb[:,i], kind='cubic')
  48. try:
  49. fbeta.append(f2b(sigma))
  50. except ValueError:
  51. if sigma<fx[0]:
  52. fbeta.append(fb[0,i])
  53. print('Using beta[{}]={} at sigma={}'.format(fx[0],fb[0,i],sigma))
  54. return numpy.array(falpha),numpy.array(fbeta)
  55. def getLTransform(z,fa,fb, mu=0):
  56. #evaluate Laplace transform of a gamma-set convolution with
  57. #coefficients alpha,fa and beta,fb
  58. #at point z with an optional parameter mu equal to log-normal parameter mu_s,
  59. #see labeling at ln() and ln_s() functions
  60. s=1
  61. fn=len(fa)
  62. for i in range(fn):
  63. s*=numpy.power((1+z*numpy.exp(mu)/fb[i]),-fa[i])
  64. return s
  65. def getLTransformGrid(sigma,fz, mu=0):
  66. #evaluate the Laplace transform of a gamma-set convolution
  67. #that approximates a log-normal distribution with parameters
  68. #sigma_s=sigma, mu_s=mu at an array of complex numbers fz
  69. print('Reading for sigma={}'.format(sigma))
  70. fa,fb=readCoef(sigma)
  71. fn=len(fa)
  72. return numpy.array([getLTransform(z,fa,fb,mu) for z in fz])
  73. def getComplexConjugatedLTransformAtMinusComplexConjugatedZGrid(sigma,fz, mu=0):
  74. #evaluate the Laplace transform of a gamma-set convolution
  75. #that approximates a log-normal distribution with parameters
  76. #sigma_s=sigma, mu_s=mu at an array of complex numbers fz
  77. print('Reading for sigma={}'.format(sigma))
  78. fa,fb=readCoef(sigma)
  79. fn=len(fa)
  80. zPrime=-numpy.conj(fz)
  81. return numpy.conj(numpy.array([getLTransform(z,fa,fb,mu) for z in zPrime]))
  82. def getLTransformPart(z, fa, fb, i):
  83. #get a Laplace transform of a single gamma distribution, specified by index
  84. #into a set of alpha and beta arrays fa and fb, respectively
  85. return numpy.power((1+z/fb[i]),-fa[i])
  86. def getLTransformGridPart(sigma,fz,i):
  87. #evaluate the Laplace transform of a single gamma distribution appearing at
  88. #index i of alpha/beta pairs that approximate log-normal distribution
  89. #for sigma_s=sigma
  90. fa,fb=readCoef(sigma)
  91. fn=len(fa)
  92. print('Using alpha={} beta={}'.format(fa[i],fb[i]))
  93. return numpy.array([getLTransformPart(z,fa,fb,i) for z in fz])
  94. def addExpA(fz,fq,x):
  95. #apply the exp(c*x)*exp(Re(a)*u*x term, see paper for reasoning
  96. return fq*numpy.exp(numpy.real(fz)*x)
  97. def getZArray(n=2000):
  98. #internal computation parameters
  99. #number of grid points 1M suggested in paper, good fit found for few thousand
  100. #n=2000
  101. #integral range, 250 to 500 suggested in paper
  102. u0=0
  103. u1=300
  104. #constant c (to the right of all poles, saw artefacts for c of 2 or above)
  105. c=1
  106. #constant a (inclined integral path, seems to work best for Re(a) close to 0,
  107. #-0.1>a>-0.5 was the range tested in paper, using upper limit
  108. fa=complex(-0.1,1)
  109. #internal structures
  110. qh=numpy.linspace(u0,u1,2*n+1)
  111. h=(u1-u0)/2/n
  112. #curve segmentation
  113. qz=c+fa/numpy.absolute(fa)*qh
  114. return qz,fa,u0,h
  115. def inverseL(fx,qz,qPhi,fa,u0,h,n):
  116. #Filion quadratures by parts of complex numbers
  117. fI=numpy.zeros(len(fx),dtype=complex)
  118. for i in range(len(fx)):
  119. x=fx[i]
  120. qPhi1=addExpA(qz,qPhi,x)
  121. fRc=filionQuadrature.filionQuadrature(numpy.real(qPhi1),x,u0,h,n,'cos')
  122. fIc=filionQuadrature.filionQuadrature(numpy.imag(qPhi1),x,u0,h,n,'cos')
  123. fRs=filionQuadrature.filionQuadrature(numpy.real(qPhi1),x,u0,h,n,'sin')
  124. fIs=filionQuadrature.filionQuadrature(numpy.imag(qPhi1),x,u0,h,n,'sin')
  125. fI[i]=complex(fRc-fIs,fIc+fRs)
  126. fI1=fa*fI
  127. return numpy.imag(fI1)
  128. def convolveLN(fx,sigma,mu,sign=numpy.array([])):
  129. #convolve a set of LN with parameters sigma and mu as numpy arrays and
  130. #return the pdf of convolution evaluated at the set of points fx
  131. #zArray
  132. #number of grid points 1M suggested in paper, good fit found for few thousand
  133. n=2000
  134. qz,fa,u0,h=getZArray(n)
  135. #function values at grid points
  136. qPhi=numpy.ones(qz.size,dtype=complex)
  137. for i in range(len(sigma)):
  138. m=mu[i]
  139. s=sigma[i]
  140. try:
  141. if not sign[i]:
  142. qPhi*=getComplexConjugatedLTransformAtMinusComplexConjugatedZGrid(s,qz,m)
  143. continue
  144. except IndexError:
  145. pass
  146. qPhi*=getLTransformGrid(sigma[i],qz,mu[i])
  147. return inverseL(fx,qz,qPhi,fa,u0,h,n)