convolveLN.py 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126
  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 getLTransformPart(z, fa, fb, i):
  62. #get a Laplace transform of a single gamma distribution, specified by index
  63. #into a set of alpha and beta arrays fa and fb, respectively
  64. return numpy.power((1+z/fb[i]),-fa[i])
  65. def getLTransformGridPart(sigma,fz,i):
  66. #evaluate the Laplace transform of a single gamma distribution appearing at
  67. #index i of alpha/beta pairs that approximate log-normal distribution
  68. #for sigma_s=sigma
  69. fa,fb=readCoef(sigma)
  70. fn=len(fa)
  71. print('Using alpha={} beta={}'.format(fa[i],fb[i]))
  72. return numpy.array([getLTransformPart(z,fa,fb,i) for z in fz])
  73. def addExpA(fz,fq,x):
  74. #apply the exp(c*x)*exp(Re(a)*u*x term, see paper for reasoning
  75. return fq*numpy.exp(numpy.real(fz)*x)
  76. def convolveLN(fx,sigma,mu):
  77. #convolve a set of LN with parameters sigma and mu as numpy arrays and
  78. #return the pdf of convolution evaluated at the set of points fx
  79. #internal computation parameters
  80. #number of grid points 1M suggested in paper, good fit found for few thousand
  81. n=2000
  82. #integral range, 250 to 500 suggested in paper
  83. u0=0
  84. u1=300
  85. #constant c (to the right of all poles, saw artefacts for c of 2 or above)
  86. c=1
  87. #constant a (inclined integral path, seems to work best for Re(a) close to 0,
  88. #-0.1>a>-0.5 was the range tested in paper, using upper limit
  89. fa=numpy.complex(-0.1,1)
  90. #internal structures
  91. qh=numpy.linspace(u0,u1,2*n+1)
  92. h=(u1-u0)/2/n
  93. #curve segmentation
  94. qz=c+fa/numpy.absolute(fa)*qh
  95. #function values at grid points
  96. qPhi=numpy.ones(qh.size,dtype=complex)
  97. for i in range(len(sigma)):
  98. m=mu[i]
  99. s=sigma[i]
  100. qPhi*=getLTransformGrid(sigma[i],qz,mu[i])
  101. #Filion quadratures by parts of complex numbers
  102. fI=numpy.zeros(len(fx),dtype=complex)
  103. for i in range(len(fx)):
  104. x=fx[i]
  105. qPhi1=addExpA(qz,qPhi,x)
  106. fRc=filionQuadrature.filionQuadrature(numpy.real(qPhi1),x,u0,h,n,'cos')
  107. fIc=filionQuadrature.filionQuadrature(numpy.imag(qPhi1),x,u0,h,n,'cos')
  108. fRs=filionQuadrature.filionQuadrature(numpy.real(qPhi1),x,u0,h,n,'sin')
  109. fIs=filionQuadrature.filionQuadrature(numpy.imag(qPhi1),x,u0,h,n,'sin')
  110. fI[i]=numpy.complex(fRc-fIs,fIc+fRs)
  111. fI1=fa*fI
  112. return numpy.imag(fI1)