| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476 |
- 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='fraction'):
- #method=fraction sets output of excrement compartments to fractions of total ingested chemical
- #method=derivate sets output to amount excreted per unit time (in native units, ng/min)
- obj=_solveMatrix(model,tmax,nt,t0,y0,sIn,method)
- return obj['t'],obj['sol'],obj['se'],obj['s1']
- def _solveMatrix(model,tmax,nt=201,t0=0,y0=numpy.array([]), sIn=numpy.array([]),method='fraction'):
- #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=solver.solve(y0,model.u(0),method=method)
- 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]=solver.solveS(SS[k],y,y0,u=u,z0=sIn[:,k],r=SU[k],method=method)
-
- 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.M=self.model.M(0)
-
- self.A=removeScaled(model,self.M)
- #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),method='fraction'):
- #solve system
- #y0 (n,) initial values
- #u (n,) the time independent input
- #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')
- #(N,n)=(n,n) @ (N,n,n) @ (n,n) @ (n,) + (n,n) @ (N,n,n) @ (n,n) @ (n,)
- y= self.P @ self.W @ self.P1 @ y0p + self.P @ self.Q @ self.P1 @ up
- #back to original system, add trivial compartments
- #(N,n)
- yOut=numpy.zeros((self.N,self.n0))
- #shift compartments from modified index i (from y) to original index i0 (to yOut)
- for c in self.updatedMap:
- i=self.updatedMap[c]
- i0=self.originalMap[c]
- yOut[:,i0]=y[:,i]
-
- #here we need original, full sized y0,u,rhs, so make sure they are not overwritten by reduced versions
- #content determined by method
- if method=='fraction':
- return getScaled(self.model,self.t,yOut,self.u0,y0,u)
- if method=='derivative':
- return setScaledDerivative(self.model,yOut,u)
- def solveS(self,S,y,y0=numpy.zeros(0),u=numpy.zeros(0),\
- z0=numpy.zeros(0),r=numpy.zeros(0),method='fraction'):
- #solve gradient system (S) at time points t
- #y (N,n) 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 / input (grad u)
- #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
- #(N,n) = (n,n) @ (N,n,n) @ (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) @(n,)
- _f=self.D1 @ self.P1 @ up
- #combine above for speed-up
- #(N,n) = (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,n) @ (n,)
- z4 = self.P @ self.Q @ Spp @ _f
-
- #(N,n)
- 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]
-
- #here we need full sized y0,u,rhs, so make sure they are not overwritten by reduced versions
- #(N,n) = (n,n) @ (N,n,1)
- rhs = (S @ numpy.expand_dims(y,axis=2)).squeeze()
- if method=='fraction':
- return setScaled(self.model,self.t,sOut,self.u0,z0,r,rhs)
- if method=='derivative':
- return setScaledDerivative(self.model,sOut,r,rhs)
-
- def setScaled(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
- def setScaledDerivative(model,y,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.
- #Rhs is added to the argument list so that the same routine can be used for Jacobi calculation
- #M is (n,n), the system matrix, M=model.M(0)
- #y is (N,n), the solution to the coupled compartments (ie. excluded excrement)
- #u is (n,), the input part
- #rhs is (N,n), for Jacobi, set it to (grad M) @ y, for k-th component of gradient
- #assemble non-homogenous part of equation
- if not rhs.size:
- #(N,n)
- rhs=numpy.zeros(y.shape)
- if u.size:
- #rhs is (n,n) so adding (n,) should work, rhs remains (N,n)
- rhs+=u
- #apply couplings and inputs
- #(n,n)
- M=model.M(0)
- #(N,n)=[(n,n) @ (N,n,1)] + (N,n)
- fI=( M @ numpy.expand_dims(y,axis=2) ).squeeze() + rhs
- #only set values back to y for excrement compartments (scaled)
- for c in model.scaled:
- i=model.lut[c]
- y[:,i]=fI[:,i]
- #(N,n)
- return y
|