solveMatrix.py 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786
  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='full'):
  111. if method=='full':
  112. obj=_solveMatrix(model,tmax,nt,t0,y0,sIn)
  113. if method=='separately':
  114. obj=_solveMatrixSeparately(model,tmax,nt,t0,y0,sIn)
  115. return obj['t'],obj['sol'],obj['se'],obj['s1']
  116. def _solveMatrix(model,tmax,nt=201,t0=0,y0=numpy.array([]), sIn=numpy.array([])):
  117. t=numpy.linspace(0,tmax-t0,nt)
  118. #solve system Q at time points t for initial values y0
  119. #SS is gradient of system nxn matrix M with respect to m parameters p
  120. SS=model.fSS(0)
  121. #large (M+1)*N by (M+1)*N matrix
  122. m=len(model.lutSE)
  123. A=removeScaled(model,model.M(0))
  124. #adjusted n where only non-terminal(scaled) compartments are counted
  125. n=A.shape[0]
  126. SM=numpy.zeros(((m+1)*n,(m+1)*n))
  127. for k in range(m):
  128. #A is on diagonal
  129. SM[k*n:(k+1)*n,k*n:(k+1)*n]=A
  130. #on the far right, gradient matrix SS sits
  131. SM[k*n:(k+1)*n,-n:]=removeScaled(model,SS[k])
  132. #for original solutions, matrix A is used
  133. SM[-n:,-n:]=A
  134. #matrix SM is now of full rank and can be diagonalized
  135. #print('{}/{}'.format(numpy.linalg.matrix_rank(SM),(m+1)*n))
  136. #RHS
  137. #SU has shape mxn
  138. #we have to remove scaled columns
  139. #Add plain rhs
  140. SU=removeScaled(model,model.Su(0),mode='columnsOnly').flatten()
  141. u=removeScaled(model,model.u(0),mode='vector')
  142. print('{} {}'.format(SU.shape,u.shape))
  143. SU=numpy.concatenate((SU,u))
  144. print(SU.shape)
  145. #diagonalize matrix
  146. lam,P=numpy.linalg.eig(SM)
  147. #no effect down to eps=1e-2
  148. #eps=1e-4
  149. #print(f'Using eps={eps}')
  150. #lam=removeImVector(lam,eps)
  151. #P=removeImMatrix(P,eps)
  152. P1=numpy.linalg.inv(P)
  153. #P1=removeImMatrix(P1,eps)
  154. D=numpy.diag(lam)
  155. #verify
  156. #print(numpy.max(P @ D @ P1-M))
  157. #shift input to diagonal system
  158. v=P1 @ SU
  159. #also no effect
  160. #maxV=numpy.max(numpy.abs(v))
  161. #v=v*(numpy.abs(v)>eps*maxV)
  162. #v=removeImVector(v,eps)
  163. #print(f'v={v}')
  164. #print(f'lam={lam}')
  165. #set initial values if missing
  166. if not y0.size:
  167. y0p=numpy.zeros(n)
  168. else:
  169. #should move this to diagonal space
  170. y0p=removeScaled(model,y0,mode='vector')
  171. if not sIn.size:
  172. s0=numpy.zeros(n*m)
  173. else:
  174. #sIn is n by m matrix, so transpose before reshaping
  175. s0=numpy.transpose(sIn)
  176. s0=removeScaled(model,s0,mode='columnsOnly')
  177. s0=s0.flatten()
  178. print('s0 {} y0p {}'.format(s0.shape,y0p.shape))
  179. S0=numpy.concatenate((s0,y0p))
  180. S0= P1 @ S0
  181. #present results as a (n*(m+1) x N array)
  182. #n number of variables/compartments
  183. #N number of time points
  184. W=numpy.outer(lam,t)
  185. tE=numpy.ones(t.shape)
  186. V0=numpy.outer(S0,tE)
  187. V=numpy.outer(v/lam,tE)
  188. y1=(V0+V)*numpy.exp(W)-V
  189. #convert back to true system
  190. y=P @ y1
  191. #reinsert scaled into array
  192. #start with y of shape (m+1)*n by N where N is number of time-points and n are only non-scaled compartments
  193. #should end up with y of shape (m+1)*n by N, where n is now number of ALL compartments
  194. originalMap=model.lut
  195. updatedMap=skipRedundant(model)
  196. n0=len(originalMap)
  197. n=len(updatedMap)
  198. N=y.shape[1]
  199. sOut=numpy.zeros((N,n0,m+1))
  200. for k in range(m+1):
  201. for c in updatedMap:
  202. i=updatedMap[c]
  203. i0=originalMap[c]
  204. sOut[:,i0,k]=y[k*n+i,:]
  205. #print('Shape: {}, n={} N={} m={}'.format(yout.shape,n0,N,m))
  206. #equivalent of Sout (N,n,m)
  207. #return numpy.transpose(yout).reshape(N,n0,m+1)
  208. sOut=calculateScaled(model,t,sOut,sIn,y0)
  209. sol=sOut[:,:,-1]
  210. s1=sOut[:,:,:-1]
  211. se=getSE(s1)
  212. return {'t':t+t0,'sol':sol,'se':se,'s1':s1,'P':P,'D':D,'SM':SM}
  213. def calculateScaled(model,t,sOut,sIn=numpy.zeros(0),y0=numpy.zeros(0)):
  214. #update sOut w/ calculated scaled values
  215. #sIn,yIn are initial values
  216. #sIn is of shape (n,m)
  217. #yIn is (n,)
  218. #sOut is (N,n,m+1)
  219. lutSE=model.lutSE
  220. lut=model.lut
  221. #if reading a model, keep the read lut
  222. #lutSE=Q['lutSE']
  223. #lut=Q['lut']
  224. m=len(lutSE)
  225. n=len(lut)
  226. N=len(t)
  227. if not sIn.size:
  228. sIn=numpy.zeros((n,m))
  229. if not y0.size:
  230. y0=numpy.zeros(n)
  231. #add column for initial values
  232. sIn=numpy.c_[sIn,y0]
  233. #reshape to n*(m+1) by N
  234. y=numpy.zeros((n*(m+1),N))
  235. for k in range(m+1):
  236. for i in range(n):
  237. y[n*k+i,:]=sOut[:,i,k]
  238. #full version of SM
  239. SS=model.fSS(0)
  240. A=model.M(0)
  241. SM=numpy.zeros(((m+1)*n,(m+1)*n))
  242. for k in range(m):
  243. #A is on diagonal
  244. SM[k*n:(k+1)*n,k*n:(k+1)*n]=A
  245. #on the far right, gradient matrix SS sits
  246. SM[k*n:(k+1)*n,-n:]=model.SS[k]
  247. SM[-n:,-n:]=A
  248. #full version of RHS
  249. SU=model.Su(0).flatten()
  250. u=model.u(0)
  251. #n*(m+1) by N
  252. SU=numpy.outer(numpy.concatenate((SU,u)),numpy.ones(t.shape))
  253. #integral, n*(m+1) by N
  254. fI=integrate(t,y)
  255. fU=integrate(t,SU)
  256. #apply couplings
  257. fI= SM @ fI
  258. #update values; scale compartments to total exposure
  259. iT=lut['total']
  260. fTotal=y0[iT]+fU[m*n+iT,:]
  261. for k in range(m+1):
  262. for c in model.scaled:
  263. i=lut[c]
  264. #sOut[1:,i,k]=fI[k*n+i,:]+fU[k*n+i,:]
  265. sOut[1:,i,k]=(sIn[i,k]*y0[iT]+fI[k*n+i,:]+fU[k*n+i,:])/fTotal
  266. sOut[0,i,k]=sIn[i,k]
  267. #set total for solution only
  268. sOut[1:,iT,-1]=fTotal
  269. sOut[0,iT,-1]=y0[iT]
  270. return sOut
  271. def _solveMatrixSeparately(model,tmax,nt=201,t0=0,y0=numpy.array([]), sIn=numpy.array([])):
  272. #sIn (n,m)
  273. #y0 (n,)
  274. t=numpy.linspace(0,tmax-t0,nt)
  275. n=len(model.lut)
  276. m=len(model.lutSE)
  277. N=len(t)
  278. if not y0.size:
  279. y0=numpy.zeros(n)
  280. iT=model.lut['total']
  281. u0=y0[iT]
  282. solver=solveUnitObj(model,t,u0)
  283. if not sIn.size:
  284. sIn=numpy.zeros([n,m])
  285. sOut=numpy.zeros([N,n,m+1])
  286. #y is (N,n)
  287. #y=solveUnit(model,t,y0,u0,model.u(0))
  288. y=solver.solve(y0,model.u(0))
  289. sOut[:,:,-1]=y
  290. #(N,n,1)
  291. y1=numpy.expand_dims(y,axis=2)
  292. #SS (m,n,n)
  293. SS=model.fSS(0)
  294. #SU (m,n)
  295. SU=model.Su(0) #m by n
  296. #u (n0,)
  297. u=model.u(0)
  298. for p in model.lutSE:
  299. k=model.lutSE[p]
  300. #SS[k] is (n,n)
  301. #y is (N,n)
  302. #rhs is (N,n)=(n,n) @ (N,n,1)
  303. rhs=(SS[k] @ y1).squeeze()
  304. #yIn is (n,)
  305. yIn=sIn[:,k]
  306. #return in (N,n)
  307. # sOut[:,:,k]=solveUnit(model,t,yIn,u0,u,rhs)
  308. #sOut[:,:,k]=solver.solve(sIn[:,k],SU[k],rhs)
  309. sOut[:,:,k]=solver.solveS(SS[k],y1,y0,u=u,z0=sIn[:,k],r=SU[k])
  310. # break
  311. sol=sOut[:,:,-1]
  312. s1=sOut[:,:,:-1]
  313. se=getSE(s1)
  314. return {'t':t+t0,'sol':sol,'se':se,'s1':s1}
  315. class solveUnitObj:
  316. def __init__(self,model,t,u0):
  317. self.model=model
  318. self.t=t
  319. self.u0=u0
  320. #some dimensions
  321. self.originalMap=model.lut
  322. self.updatedMap=skipRedundant(model)
  323. self.n0=len(self.originalMap)
  324. self.n=len(self.updatedMap)
  325. self.N=len(t)
  326. self.A=removeScaled(model,model.M(0))
  327. #diagonalize matrix
  328. self.lam,self.P=numpy.linalg.eig(self.A)
  329. self.P1=numpy.linalg.inv(self.P)
  330. D=numpy.diag(self.lam)
  331. self.D1=numpy.diag(1/self.lam)
  332. #next are all (N,n,n)
  333. self.tE=numpy.ones(t.shape)
  334. E=numpy.einsum('a,bc',self.tE,numpy.eye(self.n))
  335. QT=numpy.einsum('a,bc',t,numpy.eye(self.n))
  336. QTL=numpy.einsum('a,bc',t,D)
  337. self.W=E*numpy.exp(QTL)
  338. invL=numpy.einsum('a,bc',self.tE,self.D1)
  339. self.Q=(self.W-E)*invL
  340. #we have exp(lam*(t-t')) as Green's function
  341. #construct matrix W1 (N,N,n,n) where the last part is diag(lam)
  342. #W[i,j,k,l]=delta(k,l)*exp(lam_k*(t_i-t'_j))
  343. #delta(i,j) is Kronecker's delta
  344. T=numpy.outer(t,self.tE)-numpy.outer(self.tE,t)
  345. T1=numpy.einsum('ab,cd',T,numpy.eye(self.n))
  346. F=numpy.einsum('ab,cd',T>=0,numpy.eye(self.n))
  347. L1=numpy.einsum('ab,cd',numpy.ones(T.shape),D)
  348. self.W1=numpy.zeros(F.shape,dtype='complex')
  349. self.W1[F>0]=numpy.exp(L1[F>0]*T1[F>0])
  350. #a matrix with lam_i in rows LR(i,j)=lam(i)
  351. #(N,n,n)
  352. LR=numpy.einsum('a,b,c',self.tE,self.lam,numpy.ones(self.n))
  353. LRT=numpy.einsum('a,b,c',t,self.lam,numpy.ones(self.n))
  354. #a matrix with lam_i in columns LC(i,j)=lam(j)
  355. #(N,n,n)
  356. LC=numpy.einsum('a,b,c',self.tE,numpy.ones(self.n),self.lam)
  357. LCT=numpy.einsum('a,b,c',t,numpy.ones(self.n),self.lam)
  358. #a matrix with exp(lam_i) in rows DR(i,j)=exp(lam(i))
  359. #(N,n,n)
  360. DR=numpy.exp(LRT)
  361. #a matrix with exp(lam_j) in columns DC(i,j)=exp(lam(j))
  362. #(N,n,n)
  363. DC=numpy.exp(LCT)
  364. #a diagonal matrix with t*exp(D*t) on diagonal
  365. #(N,n,n)
  366. self.H=numpy.zeros(self.W.shape,dtype='complex')
  367. self.H[E>0]=QT[E>0]*numpy.exp(QTL[E>0])
  368. #off diagonal is exp(lam(j))-exp(lam(i))/(lam(j)-lam(i))
  369. self.H[E==0]=(DC[E==0]-DR[E==0])/(LC[E==0]-LR[E==0])
  370. #
  371. def solve(self,y0=numpy.zeros(0),u=numpy.zeros(0),rhs=numpy.zeros(0)):
  372. #solve system Q at time points t for initial values y0
  373. #u is the time independent nohomogeneous part
  374. #rhs is tabulated non-homogeneous part (N,n)
  375. #u0 is initial accumulated xenobiote
  376. #use Green function theorem, since impulse response is simple
  377. #yGREEN=exp(lam*(t-t0)
  378. #matrix solution works for reduced space where all
  379. #non-trivial components are reduced
  380. #typical variables are denoted with trailing p (for prime)
  381. #and have a smaller size (fewer compartments) than
  382. #full variables
  383. #get the matrix and remove trivial components
  384. #remove trivial components from non-homogeneous parts
  385. if not u.size:
  386. u=numpy.zeros(self.n0)
  387. up=removeScaled(self.model,u,mode='vector')
  388. #remove trivial components from initial values
  389. if not y0.size:
  390. y0=numpy.zeros(u.shape)
  391. y0p=removeScaled(self.model,y0,mode='vector')
  392. #overload w/ rhs
  393. #RHS has a time component
  394. #typically of shape n by N
  395. if rhs.size:
  396. #to N by n shape
  397. # rhs=rhs.transpose()
  398. #remove trivial components
  399. rhsp=removeScaled(self.model,rhs,mode='columnsOnly')
  400. #to (N,n,1)
  401. rhsp=numpy.expand_dims(rhsp,axis=2)
  402. else:
  403. rhsp=numpy.zeros(0)
  404. #print(numpy.linalg.matrix_rank(A))
  405. #(n,n) @ (N,n,n) @ (n,n) @ (n,)
  406. #y1 is (N,n)
  407. #sum works for (N,n)+(n,)
  408. #print(up)
  409. #print(P1 @ up)
  410. #print( (W-E)*invL @ P1 @ up)
  411. y= self.P @ self.W @ self.P1 @ y0p + self.P @ self.Q @ self.P1 @ up
  412. if rhsp.size:
  413. #time dependent component
  414. #(a,b,c,e)=(a,b,c,d) @ (b,d,e)
  415. #(N,N,n,1)=(n,n) @ (N,N,n,n) @ (n,n) @ (N,n,1)
  416. #Y1 is (N,N,n,1)
  417. Y1=numpy.real(self.P @ self.W1 @ self.P1 @ rhsp)
  418. #to apply Green's theorem, integral along axis=1 should be performed
  419. #(N,n,1)
  420. Y2=integrateFull(self.t,Y1,axis=1)
  421. #to remove last dimension, squeeze at the end
  422. #(N,n), same as y1
  423. y+=Y2.squeeze()
  424. #back to original system, add trivial compartments
  425. yOut=numpy.zeros((self.N,self.n0))
  426. for c in self.updatedMap:
  427. i=self.updatedMap[c]
  428. i0=self.originalMap[c]
  429. yOut[:,i0]=y[:,i]
  430. #return yOut
  431. #here we need full sized y0,u,rhs, so make sure they are not overwritten by reduced versions
  432. return getScaled(self.model,self.t,yOut,self.u0,y0,u,rhs)
  433. def solveS(self,S,y,y0=numpy.zeros(0),u=numpy.zeros(0),\
  434. z0=numpy.zeros(0),r=numpy.zeros(0)):
  435. #solve gradient system (S) at time points t
  436. #y (N,n,1) is solution of the system
  437. #y0 (n,) initial values of solution
  438. #u (n,)is the non-homogenous (but constant) part of equation
  439. #z0 (n,)initial values of gradients
  440. #r (n,) is the time independent gradient of nonhomogenous part
  441. #use Green function theorem, since impulse response is simple
  442. #yGREEN=exp(lam*(t-t0) and integrate it for exponentail solutions
  443. #matrix solution works for reduced space where all
  444. #non-trivial components are reduced
  445. #typical variables are denoted with trailing p (for prime)
  446. #and have a smaller size (fewer compartments) than
  447. #full variables
  448. #get the matrix and remove trivial components
  449. #remove trivial components from initial values
  450. if not y0.size:
  451. y0=numpy.zeros(self.n0)
  452. y0p=removeScaled(self.model,y0,mode='vector')
  453. #remove trivial components from non-homogenuous part of base equation
  454. if not u.size:
  455. u=numpy.zeros(y0.shape)
  456. up=removeScaled(self.model,u,mode='vector')
  457. #remove trivial components from gradient of non-homogenous part
  458. if not r.size:
  459. r=numpy.zeros(self.n0)
  460. rp=removeScaled(self.model,r,mode='vector')
  461. #remove trivial components from initial values of gradient
  462. if not z0.size:
  463. z0=numpy.zeros(r.shape)
  464. z0p=removeScaled(self.model,z0,mode='vector')
  465. Sp=removeScaled(self.model,S)
  466. #Spp is nearly diagonal in (n,n)
  467. Spp=self.P1 @ Sp @ self.P
  468. #converted to time space for H multiplication
  469. #(N,n,n)
  470. HS= numpy.einsum('a,bc',self.tE,Spp) * self.H
  471. #z=P @ W @ P1 @ z0 + P @ Q @ P1 r +
  472. # + P @ (S * H) @ P1 @ y0 + P @ (S * H) @ D1 @ P1 @ u +P @ S @ Q @ D1 @ P1 @ u
  473. #(n,n) @ (N,n,n) @ (n,n) @ (n,)
  474. #y1 is (N,n)
  475. #sum works for (N,n)+(n,)
  476. z1 = self.P @ self.W @ self.P1 @ z0p
  477. z2 = self.P @ self.Q @ self.P1 @ rp
  478. #intermediate variable for speed-up
  479. #(n,n) @ (n,n) @(n,)
  480. _f=self.D1 @ self.P1 @ up
  481. #combine above for speed-up
  482. #(n,n) @ (N,n,n) @ ( (n,n) @ (n,) + (n,) )
  483. z3 = self.P @ HS @ (self.P1 @ y0p + _f)
  484. #(n,n) @ (n,n) @ (N,n,n) @ (n,)
  485. z4 = self.P @ self.Q @ Spp @ _f
  486. z=z1+z2+(z3-z4)
  487. #back to original system, add trivial compartments
  488. sOut=numpy.zeros((self.N,self.n0))
  489. for c in self.updatedMap:
  490. i=self.updatedMap[c]
  491. i0=self.originalMap[c]
  492. sOut[:,i0]=z[:,i]
  493. #return yOut
  494. #here we need full sized y0,u,rhs, so make sure they are not overwritten by reduced versions
  495. #y = self.P @ self. W @ self.P1 @ y + self.P @ self.Q @ self.P1 @ u
  496. #(N,n0) = (n0,n0) @ (N,n0,1)
  497. rhs = (S @ y).squeeze()
  498. return getScaled(self.model,self.t,sOut,self.u0,z0,r,rhs)
  499. def solveUnit(model,t,y0=numpy.zeros(0),u0=0,u=numpy.zeros(0),rhs=numpy.zeros(0)):
  500. #solve system Q at time points t for initial values y0
  501. #u is the time independent nohomogeneous part
  502. #rhs is tabulated non-homogeneous part (N,n)
  503. #u0 is initial accumulated xenobiote
  504. #use Green function theorem, since impulse response is simple
  505. #yGREEN=exp(lam*(t-t0)
  506. #some dimensions
  507. originalMap=model.lut
  508. updatedMap=skipRedundant(model)
  509. n0=len(originalMap)
  510. n=len(updatedMap)
  511. N=len(t)
  512. #matrix solution works for reduced space where all
  513. #non-trivial components are reduced
  514. #typical variables are denoted with trailing p (for prime)
  515. #and have a smaller size (fewer compartments) than
  516. #full variables
  517. #get the matrix and remove trivial components
  518. A=removeScaled(model,model.M(0))
  519. #remove trivial components from non-homogeneous parts
  520. if not u.size:
  521. u=numpy.zeros(n0)
  522. up=removeScaled(model,u,mode='vector')
  523. #remove trivial components from initial values
  524. if not y0.size:
  525. y0=numpy.zeros(u.shape)
  526. y0p=removeScaled(model,y0,mode='vector')
  527. #overload w/ rhs
  528. #RHS has a time component
  529. #typically of shape n by N
  530. if rhs.size:
  531. #to N by n shape
  532. # rhs=rhs.transpose()
  533. #remove trivial components
  534. rhsp=removeScaled(model,rhs,mode='columnsOnly')
  535. #to (N,n,1)
  536. rhsp=numpy.expand_dims(rhsp,axis=2)
  537. else:
  538. rhsp=numpy.zeros(0)
  539. #print(numpy.linalg.matrix_rank(A))
  540. #diagonalize matrix
  541. lam,P=numpy.linalg.eig(A)
  542. P1=numpy.linalg.inv(P)
  543. D=numpy.diag(lam)
  544. #next are all (N,n,n)
  545. W=numpy.exp(numpy.einsum('a,bc',t,numpy.diag(lam)))
  546. tE=numpy.ones(t.shape)
  547. invL=numpy.einsum('a,bc',tE,numpy.diag(1/lam))
  548. E=numpy.ones(W.shape)
  549. #(n,n) @ (N,n,n) @ (n,n) @ (n,)
  550. #y1 is (N,n)
  551. #sum works for (N,n)+(n,)
  552. #print(up)
  553. #print(P1 @ up)
  554. #print( (W-E)*invL @ P1 @ up)
  555. y= y0p + P @ ((W-E)*invL) @ P1 @ up
  556. if rhsp.size:
  557. #time dependent component
  558. #t-t'
  559. #we have exp(lam*(t-t')) as Green's function
  560. #construct matrix (N,N,n,n) where the last part is diag(lam)
  561. #W[i,j,k,l]=delta(k,l)*exp(lam_k*(t_i-t'_j))
  562. #delta(i,j) is Kronecker's delta
  563. T=numpy.outer(t,tE)-numpy.outer(tE,t)
  564. T1=numpy.einsum('ab,cd',T,numpy.eye(n))
  565. F=numpy.einsum('ab,cd',T>=0,numpy.eye(n))
  566. L1=numpy.einsum('ab,cd',numpy.ones(T.shape),numpy.diag(lam))
  567. W1=numpy.zeros(F.shape,dtype='complex')
  568. W1[F>0]=numpy.exp(L1[F>0]*T1[F>0])
  569. #(a,b,c,e)=(a,b,c,d) @ (b,d,e)
  570. #(N,N,n,1)=(n,n) @ (N,N,n,n) @ (n,n) @ (N,n,1)
  571. #Y1 is (N,N,n,1)
  572. Y1=P @ W1 @ P1 @ rhsp
  573. #to apply Green's theorem, integral along axis=1 should be performed
  574. #(N,n,1)
  575. Y2=integrateFull(t,Y1,axis=1)
  576. #to remove last dimension, squeeze at the end
  577. #(N,n), same as y1
  578. y+=Y2.squeeze()
  579. #back to original system, add trivial compartments
  580. yOut=numpy.zeros((N,n0))
  581. for c in updatedMap:
  582. i=updatedMap[c]
  583. i0=originalMap[c]
  584. yOut[:,i0]=y[:,i]
  585. #here we need full sized y0,u,rhs, so make sure they are not overwritten by reduced versions
  586. return getScaled(model,t,yOut,u0,y0,u,rhs)
  587. def getTotal(model,t,u0=0):
  588. u=model.u(0)
  589. iT=model.lut['total']
  590. U=u[iT]
  591. #*numpy.ones(t.shape)
  592. fU=numpy.zeros(t.shape)
  593. fU[1:]=u0+integrate(t,U).squeeze()
  594. fU[0]=u0
  595. return fU
  596. def getScaled(model,t,y,u0=0,y0=numpy.zeros(0),u=numpy.zeros(0),rhs=numpy.zeros(0)):
  597. #update y with scaled containers, which were excluded from direct calculation to make M of full rank
  598. #u has shape (n,)
  599. #rhs has shape (N,n)
  600. #y is (N,n)
  601. M=model.M(0)
  602. #integrate input
  603. #(N-1,n)
  604. fI=integrate(t,y)
  605. #assemble non-homogenous part of equation
  606. if not rhs.size:
  607. rhs=numpy.zeros(y.shape[1])
  608. if u.size:
  609. #rhs is either (n,) or (N,n) so adding (n,) should work
  610. rhs+=u
  611. #(N-1,n) even if rhs is (n,)
  612. fU=integrate(t,rhs)
  613. #apply coupling
  614. #(N-1,n)
  615. fI=(M @ numpy.expand_dims(fI,axis=2)).squeeze()
  616. #find initial values
  617. #(n,)
  618. if not y0.size:
  619. y0=numpy.zeros(u.shape)
  620. #get starting value for fTotal
  621. #(N-1,)
  622. fTotal=getTotal(model,t,u0)
  623. #update solutions with scaled compartments
  624. for c in model.scaled:
  625. i=model.lut[c]
  626. y[1:,i]=(u0*y0[i]+fI[:,i]+fU[:,i])/fTotal[1:]
  627. y[0,i]=y0[i]
  628. iT=model.lut['total']
  629. y[:,iT]=fTotal
  630. #(N,n)
  631. return y