convolveLN.py 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150
  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. falpha.append(f2a(sigma))
  41. f2b = scipy.interpolate.interp1d(fx, fb[:,i], kind='cubic')
  42. fbeta.append(f2b(sigma))
  43. return numpy.array(falpha),numpy.array(fbeta)
  44. def getLTransform(z,fa,fb, mu=0):
  45. #evaluate Laplace transform of a gamma-set convolution with
  46. #coefficients alpha,fa and beta,fb
  47. #at point z with an optional parameter mu equal to log-normal parameter mu_s,
  48. #see labeling at ln() and ln_s() functions
  49. s=1
  50. fn=len(fa)
  51. for i in range(fn):
  52. s*=numpy.power((1+z*numpy.exp(mu)/fb[i]),-fa[i])
  53. return s
  54. def getLTransformGrid(sigma,fz, mu=0):
  55. #evaluate the Laplace transform of a gamma-set convolution
  56. #that approximates a log-normal distribution with parameters
  57. #sigma_s=sigma, mu_s=mu at an array of complex numbers fz
  58. fa,fb=readCoef(sigma)
  59. fn=len(fa)
  60. return numpy.array([getLTransform(z,fa,fb,mu) for z in fz])
  61. def getComplexConjugatedLTransformAtMinusComplexConjugatedZGrid(sigma,fz, mu=0):
  62. #evaluate the Laplace transform of a gamma-set convolution
  63. #that approximates a log-normal distribution with parameters
  64. #sigma_s=sigma, mu_s=mu at an array of complex numbers fz
  65. fa,fb=readCoef(sigma)
  66. fn=len(fa)
  67. zPrime=-numpy.conj(fz)
  68. return numpy.conj(numpy.array([getLTransform(z,fa,fb,mu) for z in zPrime]))
  69. def getLTransformPart(z, fa, fb, i):
  70. #get a Laplace transform of a single gamma distribution, specified by index
  71. #into a set of alpha and beta arrays fa and fb, respectively
  72. return numpy.power((1+z/fb[i]),-fa[i])
  73. def getLTransformGridPart(sigma,fz,i):
  74. #evaluate the Laplace transform of a single gamma distribution appearing at
  75. #index i of alpha/beta pairs that approximate log-normal distribution
  76. #for sigma_s=sigma
  77. fa,fb=readCoef(sigma)
  78. fn=len(fa)
  79. print('Using alpha={} beta={}'.format(fa[i],fb[i]))
  80. return numpy.array([getLTransformPart(z,fa,fb,i) for z in fz])
  81. def addExpA(fz,fq,x):
  82. #apply the exp(c*x)*exp(Re(a)*u*x term, see paper for reasoning
  83. return fq*numpy.exp(numpy.real(fz)*x)
  84. def getZArray(n=2000):
  85. #internal computation parameters
  86. #number of grid points 1M suggested in paper, good fit found for few thousand
  87. #n=2000
  88. #integral range, 250 to 500 suggested in paper
  89. u0=0
  90. u1=300
  91. #constant c (to the right of all poles, saw artefacts for c of 2 or above)
  92. c=1
  93. #constant a (inclined integral path, seems to work best for Re(a) close to 0,
  94. #-0.1>a>-0.5 was the range tested in paper, using upper limit
  95. fa=numpy.complex(-0.1,1)
  96. #internal structures
  97. qh=numpy.linspace(u0,u1,2*n+1)
  98. h=(u1-u0)/2/n
  99. #curve segmentation
  100. qz=c+fa/numpy.absolute(fa)*qh
  101. return qz,fa,u0,h
  102. def inverseL(fx,qz,qPhi,fa,u0,h,n):
  103. #Filion quadratures by parts of complex numbers
  104. fI=numpy.zeros(len(fx),dtype=complex)
  105. for i in range(len(fx)):
  106. x=fx[i]
  107. qPhi1=addExpA(qz,qPhi,x)
  108. fRc=filionQuadrature.filionQuadrature(numpy.real(qPhi1),x,u0,h,n,'cos')
  109. fIc=filionQuadrature.filionQuadrature(numpy.imag(qPhi1),x,u0,h,n,'cos')
  110. fRs=filionQuadrature.filionQuadrature(numpy.real(qPhi1),x,u0,h,n,'sin')
  111. fIs=filionQuadrature.filionQuadrature(numpy.imag(qPhi1),x,u0,h,n,'sin')
  112. fI[i]=numpy.complex(fRc-fIs,fIc+fRs)
  113. fI1=fa*fI
  114. return numpy.imag(fI1)
  115. def convolveLN(fx,sigma,mu,sign):
  116. #convolve a set of LN with parameters sigma and mu as numpy arrays and
  117. #return the pdf of convolution evaluated at the set of points fx
  118. #zArray
  119. #number of grid points 1M suggested in paper, good fit found for few thousand
  120. n=2000
  121. qz,fa,u0,h=getZArray(n)
  122. #function values at grid points
  123. qPhi=numpy.ones(qz.size,dtype=complex)
  124. for i in range(len(sigma)):
  125. m=mu[i]
  126. s=sigma[i]
  127. if sign[i]:
  128. qPhi*=getLTransformGrid(sigma[i],qz,mu[i])
  129. continue
  130. qPhi*=getComplexConjugatedLTransformAtMinusComplexConjugatedZGrid(s,qz,m)
  131. return inverseL(fx,qz,qPhi,fa,u0,h,n)