123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786 |
- import numpy
- def integrate(t,y):
- #integrate function y (N,n) or (N,) or a scalar tabulated at t (N,) values
- #compose time and value shift arrays
- t0=[x for x in t]
- t1=[x for x in t]
- #t0.insert(0,0)
- t0=t0[:-1]
- t1=t1[1:]
- t0=numpy.array(t0)
- t1=numpy.array(t1)
- try:
- n=y.shape[1]
- y0=numpy.matrix.copy(y)
- y1=numpy.matrix.copy(y)
- except IndexError:
- try:
- n=len(y)
- y0=numpy.outer(numpy.ones(t.shape),y)
- y1=numpy.outer(numpy.ones(t.shape),y)
- except TypeError:
- n=1
- y0=y*numpy.outer(numpy.ones(t.shape),numpy.ones(n))
- y1=y*numpy.outer(numpy.ones(t.shape),numpy.ones(n))
- y0=numpy.delete(y0,-1,axis=0)
- y1=numpy.delete(y1,0,axis=0)
-
- #integral (trapezoid) updates
- #(N,n)
- dt=numpy.outer(t1-t0,numpy.ones(n))
- #print('{} {}'.format(dt.shape,(y0+y1).shape))
- d=0.5*dt*(y1+y0)
- #use cumsum to compute integrals
- #(N-1,n)
- return numpy.cumsum(d,axis=0)
- def integrateFull(t,Y,axis=1):
- #calculate integrals of Y along axis, assume value i of each vector along axis to be calculated at ti
- #the formula for trapezoid is
- #2*I=y[n]*t[n]-y[0]*t[0]
- #+[y[0]*t[1]+y[1]*t[2]+...+y[n-2]*t[n-1]]+
- #-[y[1]*t[0]+y[2]*t[1]+...+y[n-1]*t[n-2]]=
- #y*(t1-t0)
- #where
- #t0=[t[0],t[0],...,t[n-3],t[n-2]]
- #t1=[t[1],t[2],...,t[n-1],t[n-1]]
- t0=numpy.concatenate((t[0:1],t[:-1]))
- t1=numpy.concatenate((t[1:],t[-1:]))
- #this works for arbitrary dimension of Y, so some Einstein magic has to be applied
- v='abcdef'
- n=len(Y.shape)
- einsumStr=v[:n]+','+v[axis]+'->'+v[:n]
- print('einsum [{}] {} {}'.format(einsumStr,Y.shape,t0.shape))
- T=numpy.einsum(einsumStr,numpy.ones(Y.shape),t1-t0)
- return 0.5*numpy.sum(Y*T,axis)
- def skipRedundant(model):
- redundant=[r for r in model.scaled]
- redundant.append('total')
- lut=model.lut
- v_sorted=numpy.sort([lut[x] for x in redundant])[::-1]
- #n=len(Q['lut'])
- #sort by value
- sMap={x:lut[x] for x in lut if x not in redundant}
- #sMap=dict(sorted(redundantMap.items(),key=lambda item:item[1]))
- updatedMap={list(sMap.keys())[i]:i for i in range(len(sMap))}
- #print(sMap)
- #print(updatedMap)
- #return v_sorted
- return updatedMap
- def removeScaled(model,M,mode='matrix'):
- originalMap=model.lut
- updatedMap=skipRedundant(model)
- n=len(updatedMap)
- #print(skip)
- if mode=='matrix':
- A=numpy.zeros((n,n))
- for c in updatedMap:
- i=updatedMap[c]
- i0=originalMap[c]
- for c1 in updatedMap:
- j=updatedMap[c1]
- j0=originalMap[c1]
- A[i,j]=M[i0,j0]
- return A
- if mode=='columnsOnly':
- A=numpy.zeros((M.shape[0],n))
- for c in updatedMap:
- i=updatedMap[c]
- i0=originalMap[c]
- A[:,i]=M[:,i0]
- return A
- if mode=='vector':
- v=numpy.zeros(n)
- for c in updatedMap:
- i=updatedMap[c]
- i0=originalMap[c]
- v[i]=M[i0]
- return v
- return numpy.zeros(0)
- def getSE(s1):
- s2=numpy.multiply(s1,s1)
- return numpy.sqrt(numpy.dot(s2,numpy.ones(s1.shape[2])))
- def removeImVector(v,eps):
- for i in range(len(v)):
- if numpy.abs(numpy.imag(v[i]))<eps*numpy.abs(numpy.real(v[i])):
- v[i]=numpy.real(v[i])
- return v
- def removeImMatrix(A,eps):
- f=A.ravel()
- f=removeImVector(f,eps)
- return f.reshape(A.shape)
-
- def solveMatrix(model,tmax,nt=201,t0=0,y0=numpy.array([]), sIn=numpy.array([]),method='full'):
- if method=='full':
- obj=_solveMatrix(model,tmax,nt,t0,y0,sIn)
- if method=='separately':
- obj=_solveMatrixSeparately(model,tmax,nt,t0,y0,sIn)
- return obj['t'],obj['sol'],obj['se'],obj['s1']
- def _solveMatrix(model,tmax,nt=201,t0=0,y0=numpy.array([]), sIn=numpy.array([])):
- t=numpy.linspace(0,tmax-t0,nt)
- #solve system Q at time points t for initial values y0
- #SS is gradient of system nxn matrix M with respect to m parameters p
- SS=model.fSS(0)
- #large (M+1)*N by (M+1)*N matrix
- m=len(model.lutSE)
- A=removeScaled(model,model.M(0))
- #adjusted n where only non-terminal(scaled) compartments are counted
- n=A.shape[0]
- SM=numpy.zeros(((m+1)*n,(m+1)*n))
- for k in range(m):
- #A is on diagonal
- SM[k*n:(k+1)*n,k*n:(k+1)*n]=A
- #on the far right, gradient matrix SS sits
- SM[k*n:(k+1)*n,-n:]=removeScaled(model,SS[k])
- #for original solutions, matrix A is used
- SM[-n:,-n:]=A
- #matrix SM is now of full rank and can be diagonalized
- #print('{}/{}'.format(numpy.linalg.matrix_rank(SM),(m+1)*n))
- #RHS
- #SU has shape mxn
- #we have to remove scaled columns
- #Add plain rhs
- SU=removeScaled(model,model.Su(0),mode='columnsOnly').flatten()
- u=removeScaled(model,model.u(0),mode='vector')
- print('{} {}'.format(SU.shape,u.shape))
- SU=numpy.concatenate((SU,u))
- print(SU.shape)
- #diagonalize matrix
- lam,P=numpy.linalg.eig(SM)
- #no effect down to eps=1e-2
- #eps=1e-4
- #print(f'Using eps={eps}')
- #lam=removeImVector(lam,eps)
- #P=removeImMatrix(P,eps)
- P1=numpy.linalg.inv(P)
- #P1=removeImMatrix(P1,eps)
- D=numpy.diag(lam)
- #verify
- #print(numpy.max(P @ D @ P1-M))
- #shift input to diagonal system
- v=P1 @ SU
- #also no effect
- #maxV=numpy.max(numpy.abs(v))
- #v=v*(numpy.abs(v)>eps*maxV)
- #v=removeImVector(v,eps)
- #print(f'v={v}')
- #print(f'lam={lam}')
- #set initial values if missing
- if not y0.size:
- y0p=numpy.zeros(n)
- else:
- #should move this to diagonal space
- y0p=removeScaled(model,y0,mode='vector')
- if not sIn.size:
- s0=numpy.zeros(n*m)
- else:
- #sIn is n by m matrix, so transpose before reshaping
- s0=numpy.transpose(sIn)
- s0=removeScaled(model,s0,mode='columnsOnly')
- s0=s0.flatten()
- print('s0 {} y0p {}'.format(s0.shape,y0p.shape))
- S0=numpy.concatenate((s0,y0p))
- S0= P1 @ S0
- #present results as a (n*(m+1) x N array)
- #n number of variables/compartments
- #N number of time points
- W=numpy.outer(lam,t)
- tE=numpy.ones(t.shape)
- V0=numpy.outer(S0,tE)
- V=numpy.outer(v/lam,tE)
- y1=(V0+V)*numpy.exp(W)-V
- #convert back to true system
- y=P @ y1
- #reinsert scaled into array
- #start with y of shape (m+1)*n by N where N is number of time-points and n are only non-scaled compartments
- #should end up with y of shape (m+1)*n by N, where n is now number of ALL compartments
- originalMap=model.lut
- updatedMap=skipRedundant(model)
- n0=len(originalMap)
- n=len(updatedMap)
- N=y.shape[1]
- sOut=numpy.zeros((N,n0,m+1))
- for k in range(m+1):
- for c in updatedMap:
- i=updatedMap[c]
- i0=originalMap[c]
- sOut[:,i0,k]=y[k*n+i,:]
- #print('Shape: {}, n={} N={} m={}'.format(yout.shape,n0,N,m))
- #equivalent of Sout (N,n,m)
- #return numpy.transpose(yout).reshape(N,n0,m+1)
- sOut=calculateScaled(model,t,sOut,sIn,y0)
- sol=sOut[:,:,-1]
- s1=sOut[:,:,:-1]
- se=getSE(s1)
- return {'t':t+t0,'sol':sol,'se':se,'s1':s1,'P':P,'D':D,'SM':SM}
- def calculateScaled(model,t,sOut,sIn=numpy.zeros(0),y0=numpy.zeros(0)):
- #update sOut w/ calculated scaled values
- #sIn,yIn are initial values
- #sIn is of shape (n,m)
- #yIn is (n,)
- #sOut is (N,n,m+1)
- lutSE=model.lutSE
- lut=model.lut
- #if reading a model, keep the read lut
- #lutSE=Q['lutSE']
- #lut=Q['lut']
- m=len(lutSE)
- n=len(lut)
- N=len(t)
-
- if not sIn.size:
- sIn=numpy.zeros((n,m))
- if not y0.size:
- y0=numpy.zeros(n)
- #add column for initial values
- sIn=numpy.c_[sIn,y0]
- #reshape to n*(m+1) by N
- y=numpy.zeros((n*(m+1),N))
- for k in range(m+1):
- for i in range(n):
- y[n*k+i,:]=sOut[:,i,k]
- #full version of SM
- SS=model.fSS(0)
- A=model.M(0)
- SM=numpy.zeros(((m+1)*n,(m+1)*n))
- for k in range(m):
- #A is on diagonal
- SM[k*n:(k+1)*n,k*n:(k+1)*n]=A
- #on the far right, gradient matrix SS sits
- SM[k*n:(k+1)*n,-n:]=model.SS[k]
- SM[-n:,-n:]=A
- #full version of RHS
- SU=model.Su(0).flatten()
- u=model.u(0)
- #n*(m+1) by N
- SU=numpy.outer(numpy.concatenate((SU,u)),numpy.ones(t.shape))
- #integral, n*(m+1) by N
- fI=integrate(t,y)
- fU=integrate(t,SU)
- #apply couplings
- fI= SM @ fI
- #update values; scale compartments to total exposure
- iT=lut['total']
- fTotal=y0[iT]+fU[m*n+iT,:]
- for k in range(m+1):
- for c in model.scaled:
- i=lut[c]
- #sOut[1:,i,k]=fI[k*n+i,:]+fU[k*n+i,:]
- sOut[1:,i,k]=(sIn[i,k]*y0[iT]+fI[k*n+i,:]+fU[k*n+i,:])/fTotal
- sOut[0,i,k]=sIn[i,k]
- #set total for solution only
- sOut[1:,iT,-1]=fTotal
- sOut[0,iT,-1]=y0[iT]
- return sOut
- def _solveMatrixSeparately(model,tmax,nt=201,t0=0,y0=numpy.array([]), sIn=numpy.array([])):
- #sIn (n,m)
- #y0 (n,)
- t=numpy.linspace(0,tmax-t0,nt)
- n=len(model.lut)
- m=len(model.lutSE)
- N=len(t)
-
- if not y0.size:
- y0=numpy.zeros(n)
- iT=model.lut['total']
- u0=y0[iT]
- solver=solveUnitObj(model,t,u0)
- if not sIn.size:
- sIn=numpy.zeros([n,m])
-
-
- sOut=numpy.zeros([N,n,m+1])
- #y is (N,n)
- #y=solveUnit(model,t,y0,u0,model.u(0))
- y=solver.solve(y0,model.u(0))
- sOut[:,:,-1]=y
- #(N,n,1)
- y1=numpy.expand_dims(y,axis=2)
- #SS (m,n,n)
- SS=model.fSS(0)
- #SU (m,n)
- SU=model.Su(0) #m by n
- #u (n0,)
- u=model.u(0)
- for p in model.lutSE:
- k=model.lutSE[p]
- #SS[k] is (n,n)
- #y is (N,n)
- #rhs is (N,n)=(n,n) @ (N,n,1)
- rhs=(SS[k] @ y1).squeeze()
- #yIn is (n,)
- yIn=sIn[:,k]
- #return in (N,n)
- # sOut[:,:,k]=solveUnit(model,t,yIn,u0,u,rhs)
- #sOut[:,:,k]=solver.solve(sIn[:,k],SU[k],rhs)
- sOut[:,:,k]=solver.solveS(SS[k],y1,y0,u=u,z0=sIn[:,k],r=SU[k])
- # break
-
- sol=sOut[:,:,-1]
- s1=sOut[:,:,:-1]
- se=getSE(s1)
- return {'t':t+t0,'sol':sol,'se':se,'s1':s1}
- class solveUnitObj:
- def __init__(self,model,t,u0):
- self.model=model
- self.t=t
- self.u0=u0
- #some dimensions
- self.originalMap=model.lut
- self.updatedMap=skipRedundant(model)
- self.n0=len(self.originalMap)
- self.n=len(self.updatedMap)
-
- self.N=len(t)
-
- self.A=removeScaled(model,model.M(0))
- #diagonalize matrix
- self.lam,self.P=numpy.linalg.eig(self.A)
- self.P1=numpy.linalg.inv(self.P)
- D=numpy.diag(self.lam)
- self.D1=numpy.diag(1/self.lam)
- #next are all (N,n,n)
- self.tE=numpy.ones(t.shape)
- E=numpy.einsum('a,bc',self.tE,numpy.eye(self.n))
- QT=numpy.einsum('a,bc',t,numpy.eye(self.n))
- QTL=numpy.einsum('a,bc',t,D)
- self.W=E*numpy.exp(QTL)
- invL=numpy.einsum('a,bc',self.tE,self.D1)
- self.Q=(self.W-E)*invL
- #we have exp(lam*(t-t')) as Green's function
- #construct matrix W1 (N,N,n,n) where the last part is diag(lam)
- #W[i,j,k,l]=delta(k,l)*exp(lam_k*(t_i-t'_j))
- #delta(i,j) is Kronecker's delta
- T=numpy.outer(t,self.tE)-numpy.outer(self.tE,t)
- T1=numpy.einsum('ab,cd',T,numpy.eye(self.n))
- F=numpy.einsum('ab,cd',T>=0,numpy.eye(self.n))
- L1=numpy.einsum('ab,cd',numpy.ones(T.shape),D)
- self.W1=numpy.zeros(F.shape,dtype='complex')
- self.W1[F>0]=numpy.exp(L1[F>0]*T1[F>0])
- #a matrix with lam_i in rows LR(i,j)=lam(i)
- #(N,n,n)
- LR=numpy.einsum('a,b,c',self.tE,self.lam,numpy.ones(self.n))
- LRT=numpy.einsum('a,b,c',t,self.lam,numpy.ones(self.n))
- #a matrix with lam_i in columns LC(i,j)=lam(j)
- #(N,n,n)
- LC=numpy.einsum('a,b,c',self.tE,numpy.ones(self.n),self.lam)
- LCT=numpy.einsum('a,b,c',t,numpy.ones(self.n),self.lam)
- #a matrix with exp(lam_i) in rows DR(i,j)=exp(lam(i))
- #(N,n,n)
- DR=numpy.exp(LRT)
- #a matrix with exp(lam_j) in columns DC(i,j)=exp(lam(j))
- #(N,n,n)
- DC=numpy.exp(LCT)
- #a diagonal matrix with t*exp(D*t) on diagonal
- #(N,n,n)
- self.H=numpy.zeros(self.W.shape,dtype='complex')
- self.H[E>0]=QT[E>0]*numpy.exp(QTL[E>0])
- #off diagonal is exp(lam(j))-exp(lam(i))/(lam(j)-lam(i))
- self.H[E==0]=(DC[E==0]-DR[E==0])/(LC[E==0]-LR[E==0])
-
-
- #
- def solve(self,y0=numpy.zeros(0),u=numpy.zeros(0),rhs=numpy.zeros(0)):
- #solve system Q at time points t for initial values y0
- #u is the time independent nohomogeneous part
- #rhs is tabulated non-homogeneous part (N,n)
- #u0 is initial accumulated xenobiote
- #use Green function theorem, since impulse response is simple
- #yGREEN=exp(lam*(t-t0)
-
- #matrix solution works for reduced space where all
- #non-trivial components are reduced
- #typical variables are denoted with trailing p (for prime)
- #and have a smaller size (fewer compartments) than
- #full variables
- #get the matrix and remove trivial components
- #remove trivial components from non-homogeneous parts
- if not u.size:
- u=numpy.zeros(self.n0)
- up=removeScaled(self.model,u,mode='vector')
- #remove trivial components from initial values
- if not y0.size:
- y0=numpy.zeros(u.shape)
- y0p=removeScaled(self.model,y0,mode='vector')
- #overload w/ rhs
- #RHS has a time component
- #typically of shape n by N
- if rhs.size:
- #to N by n shape
- # rhs=rhs.transpose()
- #remove trivial components
- rhsp=removeScaled(self.model,rhs,mode='columnsOnly')
- #to (N,n,1)
- rhsp=numpy.expand_dims(rhsp,axis=2)
- else:
- rhsp=numpy.zeros(0)
- #print(numpy.linalg.matrix_rank(A))
- #(n,n) @ (N,n,n) @ (n,n) @ (n,)
- #y1 is (N,n)
- #sum works for (N,n)+(n,)
- #print(up)
- #print(P1 @ up)
- #print( (W-E)*invL @ P1 @ up)
- y= self.P @ self.W @ self.P1 @ y0p + self.P @ self.Q @ self.P1 @ up
- if rhsp.size:
- #time dependent component
- #(a,b,c,e)=(a,b,c,d) @ (b,d,e)
- #(N,N,n,1)=(n,n) @ (N,N,n,n) @ (n,n) @ (N,n,1)
- #Y1 is (N,N,n,1)
- Y1=numpy.real(self.P @ self.W1 @ self.P1 @ rhsp)
- #to apply Green's theorem, integral along axis=1 should be performed
- #(N,n,1)
-
- Y2=integrateFull(self.t,Y1,axis=1)
- #to remove last dimension, squeeze at the end
- #(N,n), same as y1
- y+=Y2.squeeze()
-
- #back to original system, add trivial compartments
- yOut=numpy.zeros((self.N,self.n0))
-
- for c in self.updatedMap:
- i=self.updatedMap[c]
- i0=self.originalMap[c]
- yOut[:,i0]=y[:,i]
-
- #return yOut
- #here we need full sized y0,u,rhs, so make sure they are not overwritten by reduced versions
- return getScaled(self.model,self.t,yOut,self.u0,y0,u,rhs)
- def solveS(self,S,y,y0=numpy.zeros(0),u=numpy.zeros(0),\
- z0=numpy.zeros(0),r=numpy.zeros(0)):
- #solve gradient system (S) at time points t
- #y (N,n,1) is solution of the system
- #y0 (n,) initial values of solution
- #u (n,)is the non-homogenous (but constant) part of equation
- #z0 (n,)initial values of gradients
- #r (n,) is the time independent gradient of nonhomogenous part
- #use Green function theorem, since impulse response is simple
- #yGREEN=exp(lam*(t-t0) and integrate it for exponentail solutions
-
- #matrix solution works for reduced space where all
- #non-trivial components are reduced
- #typical variables are denoted with trailing p (for prime)
- #and have a smaller size (fewer compartments) than
- #full variables
- #get the matrix and remove trivial components
- #remove trivial components from initial values
- if not y0.size:
- y0=numpy.zeros(self.n0)
- y0p=removeScaled(self.model,y0,mode='vector')
- #remove trivial components from non-homogenuous part of base equation
- if not u.size:
- u=numpy.zeros(y0.shape)
- up=removeScaled(self.model,u,mode='vector')
- #remove trivial components from gradient of non-homogenous part
- if not r.size:
- r=numpy.zeros(self.n0)
- rp=removeScaled(self.model,r,mode='vector')
- #remove trivial components from initial values of gradient
- if not z0.size:
- z0=numpy.zeros(r.shape)
- z0p=removeScaled(self.model,z0,mode='vector')
- Sp=removeScaled(self.model,S)
- #Spp is nearly diagonal in (n,n)
- Spp=self.P1 @ Sp @ self.P
- #converted to time space for H multiplication
- #(N,n,n)
- HS= numpy.einsum('a,bc',self.tE,Spp) * self.H
- #z=P @ W @ P1 @ z0 + P @ Q @ P1 r +
- # + P @ (S * H) @ P1 @ y0 + P @ (S * H) @ D1 @ P1 @ u +P @ S @ Q @ D1 @ P1 @ u
- #(n,n) @ (N,n,n) @ (n,n) @ (n,)
- #y1 is (N,n)
- #sum works for (N,n)+(n,)
- z1 = self.P @ self.W @ self.P1 @ z0p
- z2 = self.P @ self.Q @ self.P1 @ rp
- #intermediate variable for speed-up
- #(n,n) @ (n,n) @(n,)
- _f=self.D1 @ self.P1 @ up
- #combine above for speed-up
- #(n,n) @ (N,n,n) @ ( (n,n) @ (n,) + (n,) )
- z3 = self.P @ HS @ (self.P1 @ y0p + _f)
- #(n,n) @ (n,n) @ (N,n,n) @ (n,)
- z4 = self.P @ self.Q @ Spp @ _f
-
- z=z1+z2+(z3-z4)
- #back to original system, add trivial compartments
- sOut=numpy.zeros((self.N,self.n0))
-
- for c in self.updatedMap:
- i=self.updatedMap[c]
- i0=self.originalMap[c]
- sOut[:,i0]=z[:,i]
-
- #return yOut
- #here we need full sized y0,u,rhs, so make sure they are not overwritten by reduced versions
- #y = self.P @ self. W @ self.P1 @ y + self.P @ self.Q @ self.P1 @ u
- #(N,n0) = (n0,n0) @ (N,n0,1)
- rhs = (S @ y).squeeze()
- return getScaled(self.model,self.t,sOut,self.u0,z0,r,rhs)
-
- def solveUnit(model,t,y0=numpy.zeros(0),u0=0,u=numpy.zeros(0),rhs=numpy.zeros(0)):
- #solve system Q at time points t for initial values y0
- #u is the time independent nohomogeneous part
- #rhs is tabulated non-homogeneous part (N,n)
- #u0 is initial accumulated xenobiote
- #use Green function theorem, since impulse response is simple
- #yGREEN=exp(lam*(t-t0)
-
- #some dimensions
- originalMap=model.lut
- updatedMap=skipRedundant(model)
- n0=len(originalMap)
- n=len(updatedMap)
- N=len(t)
- #matrix solution works for reduced space where all
- #non-trivial components are reduced
- #typical variables are denoted with trailing p (for prime)
- #and have a smaller size (fewer compartments) than
- #full variables
- #get the matrix and remove trivial components
- A=removeScaled(model,model.M(0))
- #remove trivial components from non-homogeneous parts
- if not u.size:
- u=numpy.zeros(n0)
- up=removeScaled(model,u,mode='vector')
- #remove trivial components from initial values
- if not y0.size:
- y0=numpy.zeros(u.shape)
- y0p=removeScaled(model,y0,mode='vector')
- #overload w/ rhs
- #RHS has a time component
- #typically of shape n by N
- if rhs.size:
- #to N by n shape
- # rhs=rhs.transpose()
- #remove trivial components
- rhsp=removeScaled(model,rhs,mode='columnsOnly')
- #to (N,n,1)
- rhsp=numpy.expand_dims(rhsp,axis=2)
- else:
- rhsp=numpy.zeros(0)
- #print(numpy.linalg.matrix_rank(A))
- #diagonalize matrix
- lam,P=numpy.linalg.eig(A)
- P1=numpy.linalg.inv(P)
- D=numpy.diag(lam)
- #next are all (N,n,n)
- W=numpy.exp(numpy.einsum('a,bc',t,numpy.diag(lam)))
- tE=numpy.ones(t.shape)
- invL=numpy.einsum('a,bc',tE,numpy.diag(1/lam))
- E=numpy.ones(W.shape)
- #(n,n) @ (N,n,n) @ (n,n) @ (n,)
- #y1 is (N,n)
- #sum works for (N,n)+(n,)
- #print(up)
- #print(P1 @ up)
- #print( (W-E)*invL @ P1 @ up)
- y= y0p + P @ ((W-E)*invL) @ P1 @ up
- if rhsp.size:
- #time dependent component
- #t-t'
- #we have exp(lam*(t-t')) as Green's function
- #construct matrix (N,N,n,n) where the last part is diag(lam)
- #W[i,j,k,l]=delta(k,l)*exp(lam_k*(t_i-t'_j))
- #delta(i,j) is Kronecker's delta
- T=numpy.outer(t,tE)-numpy.outer(tE,t)
- T1=numpy.einsum('ab,cd',T,numpy.eye(n))
- F=numpy.einsum('ab,cd',T>=0,numpy.eye(n))
- L1=numpy.einsum('ab,cd',numpy.ones(T.shape),numpy.diag(lam))
- W1=numpy.zeros(F.shape,dtype='complex')
- W1[F>0]=numpy.exp(L1[F>0]*T1[F>0])
- #(a,b,c,e)=(a,b,c,d) @ (b,d,e)
- #(N,N,n,1)=(n,n) @ (N,N,n,n) @ (n,n) @ (N,n,1)
- #Y1 is (N,N,n,1)
- Y1=P @ W1 @ P1 @ rhsp
- #to apply Green's theorem, integral along axis=1 should be performed
- #(N,n,1)
-
- Y2=integrateFull(t,Y1,axis=1)
- #to remove last dimension, squeeze at the end
- #(N,n), same as y1
- y+=Y2.squeeze()
-
- #back to original system, add trivial compartments
- yOut=numpy.zeros((N,n0))
-
- for c in updatedMap:
- i=updatedMap[c]
- i0=originalMap[c]
- yOut[:,i0]=y[:,i]
-
- #here we need full sized y0,u,rhs, so make sure they are not overwritten by reduced versions
- return getScaled(model,t,yOut,u0,y0,u,rhs)
- def getTotal(model,t,u0=0):
- u=model.u(0)
- iT=model.lut['total']
- U=u[iT]
- #*numpy.ones(t.shape)
- fU=numpy.zeros(t.shape)
- fU[1:]=u0+integrate(t,U).squeeze()
- fU[0]=u0
- return fU
- def getScaled(model,t,y,u0=0,y0=numpy.zeros(0),u=numpy.zeros(0),rhs=numpy.zeros(0)):
- #update y with scaled containers, which were excluded from direct calculation to make M of full rank
- #u has shape (n,)
- #rhs has shape (N,n)
- #y is (N,n)
- M=model.M(0)
- #integrate input
- #(N-1,n)
- fI=integrate(t,y)
- #assemble non-homogenous part of equation
- if not rhs.size:
- rhs=numpy.zeros(y.shape[1])
- if u.size:
- #rhs is either (n,) or (N,n) so adding (n,) should work
- rhs+=u
- #(N-1,n) even if rhs is (n,)
- fU=integrate(t,rhs)
- #apply coupling
- #(N-1,n)
- fI=(M @ numpy.expand_dims(fI,axis=2)).squeeze()
- #find initial values
- #(n,)
- if not y0.size:
- y0=numpy.zeros(u.shape)
- #get starting value for fTotal
- #(N-1,)
- fTotal=getTotal(model,t,u0)
- #update solutions with scaled compartments
- for c in model.scaled:
- i=model.lut[c]
- y[1:,i]=(u0*y0[i]+fI[:,i]+fU[:,i])/fTotal[1:]
- y[0,i]=y0[i]
- iT=model.lut['total']
- y[:,iT]=fTotal
-
- #(N,n)
-
- return y
|