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]))=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