solveMatrix.py 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476
  1. import numpy
  2. def integrate(t,y):
  3. #integrate function y (N,n) or (N,) or a scalar tabulated at t (N,) values
  4. #compose time and value shift arrays
  5. t0=[x for x in t]
  6. t1=[x for x in t]
  7. #t0.insert(0,0)
  8. t0=t0[:-1]
  9. t1=t1[1:]
  10. t0=numpy.array(t0)
  11. t1=numpy.array(t1)
  12. try:
  13. n=y.shape[1]
  14. y0=numpy.matrix.copy(y)
  15. y1=numpy.matrix.copy(y)
  16. except IndexError:
  17. try:
  18. n=len(y)
  19. y0=numpy.outer(numpy.ones(t.shape),y)
  20. y1=numpy.outer(numpy.ones(t.shape),y)
  21. except TypeError:
  22. n=1
  23. y0=y*numpy.outer(numpy.ones(t.shape),numpy.ones(n))
  24. y1=y*numpy.outer(numpy.ones(t.shape),numpy.ones(n))
  25. y0=numpy.delete(y0,-1,axis=0)
  26. y1=numpy.delete(y1,0,axis=0)
  27. #integral (trapezoid) updates
  28. #(N,n)
  29. dt=numpy.outer(t1-t0,numpy.ones(n))
  30. #print('{} {}'.format(dt.shape,(y0+y1).shape))
  31. d=0.5*dt*(y1+y0)
  32. #use cumsum to compute integrals
  33. #(N-1,n)
  34. return numpy.cumsum(d,axis=0)
  35. def integrateFull(t,Y,axis=1):
  36. #calculate integrals of Y along axis, assume value i of each vector along axis to be calculated at ti
  37. #the formula for trapezoid is
  38. #2*I=y[n]*t[n]-y[0]*t[0]
  39. #+[y[0]*t[1]+y[1]*t[2]+...+y[n-2]*t[n-1]]+
  40. #-[y[1]*t[0]+y[2]*t[1]+...+y[n-1]*t[n-2]]=
  41. #y*(t1-t0)
  42. #where
  43. #t0=[t[0],t[0],...,t[n-3],t[n-2]]
  44. #t1=[t[1],t[2],...,t[n-1],t[n-1]]
  45. t0=numpy.concatenate((t[0:1],t[:-1]))
  46. t1=numpy.concatenate((t[1:],t[-1:]))
  47. #this works for arbitrary dimension of Y, so some Einstein magic has to be applied
  48. v='abcdef'
  49. n=len(Y.shape)
  50. einsumStr=v[:n]+','+v[axis]+'->'+v[:n]
  51. print('einsum [{}] {} {}'.format(einsumStr,Y.shape,t0.shape))
  52. T=numpy.einsum(einsumStr,numpy.ones(Y.shape),t1-t0)
  53. return 0.5*numpy.sum(Y*T,axis)
  54. def skipRedundant(model):
  55. redundant=[r for r in model.scaled]
  56. redundant.append('total')
  57. lut=model.lut
  58. v_sorted=numpy.sort([lut[x] for x in redundant])[::-1]
  59. #n=len(Q['lut'])
  60. #sort by value
  61. sMap={x:lut[x] for x in lut if x not in redundant}
  62. #sMap=dict(sorted(redundantMap.items(),key=lambda item:item[1]))
  63. updatedMap={list(sMap.keys())[i]:i for i in range(len(sMap))}
  64. #print(sMap)
  65. #print(updatedMap)
  66. #return v_sorted
  67. return updatedMap
  68. def removeScaled(model,M,mode='matrix'):
  69. originalMap=model.lut
  70. updatedMap=skipRedundant(model)
  71. n=len(updatedMap)
  72. #print(skip)
  73. if mode=='matrix':
  74. A=numpy.zeros((n,n))
  75. for c in updatedMap:
  76. i=updatedMap[c]
  77. i0=originalMap[c]
  78. for c1 in updatedMap:
  79. j=updatedMap[c1]
  80. j0=originalMap[c1]
  81. A[i,j]=M[i0,j0]
  82. return A
  83. if mode=='columnsOnly':
  84. A=numpy.zeros((M.shape[0],n))
  85. for c in updatedMap:
  86. i=updatedMap[c]
  87. i0=originalMap[c]
  88. A[:,i]=M[:,i0]
  89. return A
  90. if mode=='vector':
  91. v=numpy.zeros(n)
  92. for c in updatedMap:
  93. i=updatedMap[c]
  94. i0=originalMap[c]
  95. v[i]=M[i0]
  96. return v
  97. return numpy.zeros(0)
  98. def getSE(s1):
  99. s2=numpy.multiply(s1,s1)
  100. return numpy.sqrt(numpy.dot(s2,numpy.ones(s1.shape[2])))
  101. def removeImVector(v,eps):
  102. for i in range(len(v)):
  103. if numpy.abs(numpy.imag(v[i]))<eps*numpy.abs(numpy.real(v[i])):
  104. v[i]=numpy.real(v[i])
  105. return v
  106. def removeImMatrix(A,eps):
  107. f=A.ravel()
  108. f=removeImVector(f,eps)
  109. return f.reshape(A.shape)
  110. def solveMatrix(model,tmax,nt=201,t0=0,y0=numpy.array([]), sIn=numpy.array([]),method='fraction'):
  111. #method=fraction sets output of excrement compartments to fractions of total ingested chemical
  112. #method=derivate sets output to amount excreted per unit time (in native units, ng/min)
  113. obj=_solveMatrix(model,tmax,nt,t0,y0,sIn,method)
  114. return obj['t'],obj['sol'],obj['se'],obj['s1']
  115. def _solveMatrix(model,tmax,nt=201,t0=0,y0=numpy.array([]), sIn=numpy.array([]),method='fraction'):
  116. #sIn (n,m)
  117. #y0 (n,)
  118. t=numpy.linspace(0,tmax-t0,nt)
  119. n=len(model.lut)
  120. m=len(model.lutSE)
  121. N=len(t)
  122. if not y0.size:
  123. y0=numpy.zeros(n)
  124. iT=model.lut['total']
  125. u0=y0[iT]
  126. solver=solveUnitObj(model,t,u0)
  127. if not sIn.size:
  128. sIn=numpy.zeros([n,m])
  129. sOut=numpy.zeros([N,n,m+1])
  130. #y is (N,n)
  131. y=solver.solve(y0,model.u(0),method=method)
  132. sOut[:,:,-1]=y
  133. #(N,n,1)
  134. y1=numpy.expand_dims(y,axis=2)
  135. #SS (m,n,n)
  136. SS=model.fSS(0)
  137. #SU (m,n)
  138. SU=model.Su(0) #m by n
  139. #u (n0,)
  140. u=model.u(0)
  141. for p in model.lutSE:
  142. k=model.lutSE[p]
  143. #SS[k] is (n,n)
  144. #y is (N,n)
  145. #rhs is (N,n)=(n,n) @ (N,n,1)
  146. rhs=(SS[k] @ y1).squeeze()
  147. #yIn is (n,)
  148. yIn=sIn[:,k]
  149. #return in (N,n)
  150. sOut[:,:,k]=solver.solveS(SS[k],y,y0,u=u,z0=sIn[:,k],r=SU[k],method=method)
  151. sol=sOut[:,:,-1]
  152. s1=sOut[:,:,:-1]
  153. se=getSE(s1)
  154. return {'t':t+t0,'sol':sol,'se':se,'s1':s1}
  155. class solveUnitObj:
  156. def __init__(self,model,t,u0):
  157. self.model=model
  158. self.t=t
  159. self.u0=u0
  160. #some dimensions
  161. self.originalMap=model.lut
  162. self.updatedMap=skipRedundant(model)
  163. self.n0=len(self.originalMap)
  164. self.n=len(self.updatedMap)
  165. self.N=len(t)
  166. self.M=self.model.M(0)
  167. self.A=removeScaled(model,self.M)
  168. #diagonalize matrix
  169. self.lam,self.P=numpy.linalg.eig(self.A)
  170. self.P1=numpy.linalg.inv(self.P)
  171. D=numpy.diag(self.lam)
  172. self.D1=numpy.diag(1/self.lam)
  173. #next are all (N,n,n)
  174. self.tE=numpy.ones(t.shape)
  175. E=numpy.einsum('a,bc',self.tE,numpy.eye(self.n))
  176. QT=numpy.einsum('a,bc',t,numpy.eye(self.n))
  177. QTL=numpy.einsum('a,bc',t,D)
  178. self.W=E*numpy.exp(QTL)
  179. invL=numpy.einsum('a,bc',self.tE,self.D1)
  180. self.Q=(self.W-E)*invL
  181. #we have exp(lam*(t-t')) as Green's function
  182. #construct matrix W1 (N,N,n,n) where the last part is diag(lam)
  183. #W[i,j,k,l]=delta(k,l)*exp(lam_k*(t_i-t'_j))
  184. #delta(i,j) is Kronecker's delta
  185. T=numpy.outer(t,self.tE)-numpy.outer(self.tE,t)
  186. T1=numpy.einsum('ab,cd',T,numpy.eye(self.n))
  187. F=numpy.einsum('ab,cd',T>=0,numpy.eye(self.n))
  188. L1=numpy.einsum('ab,cd',numpy.ones(T.shape),D)
  189. self.W1=numpy.zeros(F.shape,dtype='complex')
  190. self.W1[F>0]=numpy.exp(L1[F>0]*T1[F>0])
  191. #a matrix with lam_i in rows LR(i,j)=lam(i)
  192. #(N,n,n)
  193. LR=numpy.einsum('a,b,c',self.tE,self.lam,numpy.ones(self.n))
  194. LRT=numpy.einsum('a,b,c',t,self.lam,numpy.ones(self.n))
  195. #a matrix with lam_i in columns LC(i,j)=lam(j)
  196. #(N,n,n)
  197. LC=numpy.einsum('a,b,c',self.tE,numpy.ones(self.n),self.lam)
  198. LCT=numpy.einsum('a,b,c',t,numpy.ones(self.n),self.lam)
  199. #a matrix with exp(lam_i) in rows DR(i,j)=exp(lam(i))
  200. #(N,n,n)
  201. DR=numpy.exp(LRT)
  202. #a matrix with exp(lam_j) in columns DC(i,j)=exp(lam(j))
  203. #(N,n,n)
  204. DC=numpy.exp(LCT)
  205. #a diagonal matrix with t*exp(D*t) on diagonal
  206. #(N,n,n)
  207. self.H=numpy.zeros(self.W.shape,dtype='complex')
  208. self.H[E>0]=QT[E>0]*numpy.exp(QTL[E>0])
  209. #off diagonal is exp(lam(j))-exp(lam(i))/(lam(j)-lam(i))
  210. self.H[E==0]=(DC[E==0]-DR[E==0])/(LC[E==0]-LR[E==0])
  211. #
  212. def solve(self,y0=numpy.zeros(0),u=numpy.zeros(0),method='fraction'):
  213. #solve system
  214. #y0 (n,) initial values
  215. #u (n,) the time independent input
  216. #use Green function theorem, since impulse response is simple
  217. #yGREEN=exp(lam*(t-t0)
  218. #matrix solution works for reduced space where all
  219. #non-trivial components are reduced
  220. #typical variables are denoted with trailing p (for prime)
  221. #and have a smaller size (fewer compartments) than
  222. #full variables
  223. #get the matrix and remove trivial components
  224. #remove trivial components from non-homogeneous parts
  225. if not u.size:
  226. u=numpy.zeros(self.n0)
  227. up=removeScaled(self.model,u,mode='vector')
  228. #remove trivial components from initial values
  229. if not y0.size:
  230. y0=numpy.zeros(u.shape)
  231. y0p=removeScaled(self.model,y0,mode='vector')
  232. #(N,n)=(n,n) @ (N,n,n) @ (n,n) @ (n,) + (n,n) @ (N,n,n) @ (n,n) @ (n,)
  233. y= self.P @ self.W @ self.P1 @ y0p + self.P @ self.Q @ self.P1 @ up
  234. #back to original system, add trivial compartments
  235. #(N,n)
  236. yOut=numpy.zeros((self.N,self.n0))
  237. #shift compartments from modified index i (from y) to original index i0 (to yOut)
  238. for c in self.updatedMap:
  239. i=self.updatedMap[c]
  240. i0=self.originalMap[c]
  241. yOut[:,i0]=y[:,i]
  242. #here we need original, full sized y0,u,rhs, so make sure they are not overwritten by reduced versions
  243. #content determined by method
  244. if method=='fraction':
  245. return getScaled(self.model,self.t,yOut,self.u0,y0,u)
  246. if method=='derivative':
  247. return setScaledDerivative(self.model,yOut,u)
  248. def solveS(self,S,y,y0=numpy.zeros(0),u=numpy.zeros(0),\
  249. z0=numpy.zeros(0),r=numpy.zeros(0),method='fraction'):
  250. #solve gradient system (S) at time points t
  251. #y (N,n) is solution of the system
  252. #y0 (n,) initial values of solution
  253. #u (n,) is the non-homogenous (but constant) part of equation
  254. #z0 (n,) initial values of gradients
  255. #r (n,) is the time independent gradient of nonhomogenous part / input (grad u)
  256. #use Green function theorem, since impulse response is simple
  257. #yGREEN=exp(lam*(t-t0) and integrate it for exponentail solutions
  258. #matrix solution works for reduced space where all
  259. #non-trivial components are reduced
  260. #typical variables are denoted with trailing p (for prime)
  261. #and have a smaller size (fewer compartments) than
  262. #full variables
  263. #get the matrix and remove trivial components
  264. #remove trivial components from initial values
  265. if not y0.size:
  266. y0=numpy.zeros(self.n0)
  267. y0p=removeScaled(self.model,y0,mode='vector')
  268. #remove trivial components from non-homogenuous part of base equation
  269. if not u.size:
  270. u=numpy.zeros(y0.shape)
  271. up=removeScaled(self.model,u,mode='vector')
  272. #remove trivial components from gradient of non-homogenous part
  273. if not r.size:
  274. r=numpy.zeros(self.n0)
  275. rp=removeScaled(self.model,r,mode='vector')
  276. #remove trivial components from initial values of gradient
  277. if not z0.size:
  278. z0=numpy.zeros(r.shape)
  279. z0p=removeScaled(self.model,z0,mode='vector')
  280. Sp=removeScaled(self.model,S)
  281. #Spp is nearly diagonal in (n,n)
  282. Spp=self.P1 @ Sp @ self.P
  283. #converted to time space for H multiplication
  284. #(N,n,n)
  285. HS= numpy.einsum('a,bc',self.tE,Spp) * self.H
  286. #(N,n) = (n,n) @ (N,n,n) @ (n,n) @ (n,)
  287. z1 = self.P @ self.W @ self.P1 @ z0p
  288. z2 = self.P @ self.Q @ self.P1 @ rp
  289. #intermediate variable for speed-up
  290. #(n,) = (n,n) @ (n,n) @(n,)
  291. _f=self.D1 @ self.P1 @ up
  292. #combine above for speed-up
  293. #(N,n) = (n,n) @ (N,n,n) @ ( (n,n) @ (n,) + (n,) )
  294. z3 = self.P @ HS @ (self.P1 @ y0p + _f)
  295. #(N,n) = (n,n) @ (n,n) @ (N,n,n) @ (n,)
  296. z4 = self.P @ self.Q @ Spp @ _f
  297. #(N,n)
  298. z=z1+z2+(z3-z4)
  299. #back to original system, add trivial compartments
  300. sOut=numpy.zeros((self.N,self.n0))
  301. for c in self.updatedMap:
  302. i=self.updatedMap[c]
  303. i0=self.originalMap[c]
  304. sOut[:,i0]=z[:,i]
  305. #here we need full sized y0,u,rhs, so make sure they are not overwritten by reduced versions
  306. #(N,n) = (n,n) @ (N,n,1)
  307. rhs = (S @ numpy.expand_dims(y,axis=2)).squeeze()
  308. if method=='fraction':
  309. return setScaled(self.model,self.t,sOut,self.u0,z0,r,rhs)
  310. if method=='derivative':
  311. return setScaledDerivative(self.model,sOut,r,rhs)
  312. def setScaled(model,t,y,u0=0,y0=numpy.zeros(0),u=numpy.zeros(0),rhs=numpy.zeros(0)):
  313. #update y with scaled containers, which were excluded from direct calculation to make M of full rank
  314. #u has shape (n,)
  315. #rhs has shape (N,n)
  316. #y is (N,n)
  317. M=model.M(0)
  318. #integrate input
  319. #(N-1,n)
  320. fI=integrate(t,y)
  321. #assemble non-homogenous part of equation
  322. if not rhs.size:
  323. rhs=numpy.zeros(y.shape[1])
  324. if u.size:
  325. #rhs is either (n,) or (N,n) so adding (n,) should work
  326. rhs+=u
  327. #(N-1,n) even if rhs is (n,)
  328. fU=integrate(t,rhs)
  329. #apply coupling
  330. #(N-1,n)
  331. fI=(M @ numpy.expand_dims(fI,axis=2)).squeeze()
  332. #find initial values
  333. #(n,)
  334. if not y0.size:
  335. y0=numpy.zeros(u.shape)
  336. #get starting value for fTotal
  337. #(N-1,)
  338. fTotal=getTotal(model,t,u0)
  339. #update solutions with scaled compartments
  340. for c in model.scaled:
  341. i=model.lut[c]
  342. y[1:,i]=(u0*y0[i]+fI[:,i]+fU[:,i])/fTotal[1:]
  343. y[0,i]=y0[i]
  344. iT=model.lut['total']
  345. y[:,iT]=fTotal
  346. #(N,n)
  347. return y
  348. def setScaledDerivative(model,y,u=numpy.zeros(0),rhs=numpy.zeros(0)):
  349. #update y with scaled containers, which were excluded from direct calculation to make M of full rank.
  350. #Rhs is added to the argument list so that the same routine can be used for Jacobi calculation
  351. #M is (n,n), the system matrix, M=model.M(0)
  352. #y is (N,n), the solution to the coupled compartments (ie. excluded excrement)
  353. #u is (n,), the input part
  354. #rhs is (N,n), for Jacobi, set it to (grad M) @ y, for k-th component of gradient
  355. #assemble non-homogenous part of equation
  356. if not rhs.size:
  357. #(N,n)
  358. rhs=numpy.zeros(y.shape)
  359. if u.size:
  360. #rhs is (n,n) so adding (n,) should work, rhs remains (N,n)
  361. rhs+=u
  362. #apply couplings and inputs
  363. #(n,n)
  364. M=model.M(0)
  365. #(N,n)=[(n,n) @ (N,n,1)] + (N,n)
  366. fI=( M @ numpy.expand_dims(y,axis=2) ).squeeze() + rhs
  367. #only set values back to y for excrement compartments (scaled)
  368. for c in model.scaled:
  369. i=model.lut[c]
  370. y[:,i]=fI[:,i]
  371. #(N,n)
  372. return y