cModel.py 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657
  1. import numpy
  2. import json
  3. import os
  4. import scipy.interpolate
  5. #for partial function specializations
  6. import functools
  7. import function
  8. import importlib
  9. importlib.reload(function)
  10. class model:
  11. def __init__(self):
  12. self.compartments={}
  13. self.seJ={}
  14. def add_source(self,compartmentName,formula):
  15. self.compartments[compartmentName]['source']=formula
  16. def add_compartment(self,compartmentName):
  17. self.compartments[compartmentName]={}
  18. self.compartments[compartmentName]['targets']={}
  19. self.compartments[compartmentName]['sensTargets']={}
  20. def getTimeUnit(self):
  21. try:
  22. return self.mod['timeUnit']
  23. except KeyError:
  24. return 's'
  25. def bind(self,src,target,qName,pcName,useVolume=0):
  26. #establish a flow from source compartment to the target
  27. #get volume names
  28. srcVName=self.getVolumePar(src,useVolume)
  29. targetVName=self.getVolumePar(target,useVolume)
  30. #the source equation (where we subtract the current)
  31. #in fact, this is the diagonal element
  32. pSrc=self.couplingObject(-1,qName,pcName,srcVName)
  33. #this includes derivatives and value!
  34. self.addValueObject(src,src,pSrc)
  35. #the target equation (where we add the current)
  36. pTarget=self.couplingObject(1,qName,pcName,targetVName)
  37. #equation is for target compartment, but binding for source
  38. self.addValueObject(target,src,pTarget)
  39. def addValueObject(self,targetName,srcName,cObject):
  40. #always binds equation id and a variable
  41. targetList=self.compartments[targetName]['targets']
  42. addValue(targetList,srcName,cObject["value"])
  43. der=cObject["derivatives"]
  44. for d in der:
  45. targetSE=self.getSEJ_comp(d,targetName)
  46. addValue(targetSE,srcName,der[d])
  47. def couplingObject(self,sign, qParName, pcParName, vParName):
  48. qPar=self.get(qParName)
  49. pcPar=self.get(pcParName)
  50. vPar=self.get(vParName)
  51. q=qPar['value']
  52. pc=pcPar['value']
  53. v=vPar['value']
  54. DPC=pcPar['derivatives']
  55. DQ=qPar['derivatives']
  56. DV=vPar['derivatives']
  57. if any(['function' in qPar,'function' in pcPar, 'function' in vPar]):
  58. fq=function.to(q)
  59. fpc=function.to(pc)
  60. fv=function.to(v)
  61. f=lambda t,q=fq,pc=fpc,v=fv,s=sign:s*q(t)/v(t)/pc(t)
  62. dfdPC=lambda t,f=f,pc=fpc:-f(t)/pc(t)
  63. dPC=function.generate(dfdPC,DPC)
  64. dfdQ=lambda t,f=f,q=fq: f(t)/q(t)
  65. dQ=function.generate(dfdQ,DQ)
  66. dfdV=lambda t,f=f,v=fv: -f(t)/v(t)
  67. dV=function.generate(dfdV,DV)
  68. return function.Object(f,[dPC,dQ,dV])
  69. else:
  70. f=sign*q/v/pc
  71. return derivedObject(sign*q/v/pc,\
  72. [{'df':-f/pc,'D':DPC},\
  73. {'df':sign/v/pc,'D':DQ},\
  74. {'df':-f/v,'D':DV}])
  75. #derivatives is the combination of the above
  76. def getVolumePar(self,compartment,useVolume=1):
  77. #returnis volume name, if found and useVolume is directed,
  78. #or a standard parameter one
  79. if not useVolume:
  80. return "one"
  81. try:
  82. return self.mod["volumes"][compartment]
  83. #parV=self.mod["parameters"][parVName]
  84. except KeyError:
  85. pass
  86. return "one"
  87. def build(self):
  88. comps=self.compartments
  89. self.n=len(comps)
  90. self.fu=[lambda t:0]*self.n
  91. self.lut={c:i for (i,c) in zip(range(self.n),comps.keys())}
  92. self.dM={}
  93. self.fM=numpy.zeros((self.n,self.n))
  94. for c in comps:
  95. comp=comps[c]
  96. if 'source' in comp:
  97. self.fu[self.lut[c]]=parseFunction(comp['source'])
  98. for t in comp['targets']:
  99. arr=comp['targets'][t]
  100. if function.contains(arr):
  101. try:
  102. self.dM[self.lut[c]][self.lut[t]]=\
  103. function.sumArray(arr)
  104. except (KeyError,TypeError):
  105. self.dM[self.lut[c]]={}
  106. self.dM[self.lut[c]][self.lut[t]]=\
  107. function.sumArray(arr)
  108. else:
  109. #just set once
  110. self.fM[self.lut[c],self.lut[t]]=sum(arr)
  111. #build SE part
  112. self.buildSE()
  113. def buildSE(self):
  114. #check which parameterst to include
  115. parList=[]
  116. pars=self.parSetup['parameters']
  117. for parName in self.seJ:
  118. #print(par)
  119. par=pars[parName]
  120. usePar=calculateDerivative(par)
  121. #print('[{}]: {}'.format(usePar,par))
  122. if not usePar:
  123. continue
  124. parList.append(parName)
  125. #print(parList)
  126. self.m=len(parList)
  127. self.lutSE={c:i for (i,c) in zip(range(self.m),parList)}
  128. self.qSS={}
  129. self.SS=numpy.zeros((self.m,self.n,self.n))
  130. for parName in parList:
  131. sources=self.seJ[parName]
  132. for compartment in sources:
  133. targets=sources[compartment]
  134. for t in targets:
  135. k=self.lutSE[parName]
  136. i=self.lut[compartment]
  137. j=self.lut[t]
  138. #print('[{} {} {}] {}'.format(parName,compartment,t,targets[t]))
  139. arr=targets[t]
  140. if not function.contains(arr):
  141. self.SS[k,i,j]=sum(arr)
  142. continue
  143. ft=function.sumArray(arr)
  144. try:
  145. self.qSS[k][i][j]=ft
  146. except (KeyError,TypeError):
  147. try:
  148. self.qSS[k][i]={}
  149. self.qSS[k][i][j]=ft
  150. except (KeyError,TypeError):
  151. self.qSS[k]={}
  152. self.qSS[k][i]={}
  153. self.qSS[k][i][j]=ft
  154. def inspect(self):
  155. comps=self.compartments
  156. pars=self.parSetup['parameters']
  157. #pars=self.mod['parameters']
  158. tu=self.getTimeUnit()
  159. print('Time unit: {}'.format(tu))
  160. print('Compartments')
  161. for c in comps:
  162. print('{}/{}:'.format(c,self.lut[c]))
  163. comp=comps[c]
  164. if 'source' in comp:
  165. print('\tsource\n\t\t{}'.format(comp['source']))
  166. print('\ttargets')
  167. for t in comp['targets']:
  168. print('\t\t{}[{},{}]: {}'.format(t,self.lut[c],self.lut[t],\
  169. comp['targets'][t]))
  170. print('Flows')
  171. for f in self.flows:
  172. fName=self.flows[f]
  173. fParName=self.mod['flows'][fName]
  174. fPar=pars[fParName]
  175. print('\t{}[{}]:{} [{}]'.format(f,fName,fParName,self.get(fParName)))
  176. print('Volumes')
  177. for v in self.mod['volumes']:
  178. vParName=self.mod['volumes'][v]
  179. vPar=pars[vParName]
  180. print('\t{}:{} [{}]'.format(v,vParName,self.get(vParName)))
  181. print('Partition coefficients')
  182. for pc in self.mod['partitionCoefficients']:
  183. pcParName=self.mod['partitionCoefficients'][pc]
  184. pcPar=pars[pcParName]
  185. print('\t{}:{} [{}]'.format(pc,pcParName,self.get(pcParName)))
  186. print('SE parameters')
  187. for p in self.seJ:
  188. print(p)
  189. sources=self.seJ[p]
  190. for compartment in sources:
  191. targets=sources[compartment]
  192. for t in targets:
  193. print('\t SE bind {}/{}:{}'.format(compartment,t,targets[t]))
  194. def parse(self,setupFile,parameterFile):
  195. with open(setupFile,'r') as f:
  196. self.mod=json.load(f)
  197. with open(parameterFile,'r') as f:
  198. self.parSetup=json.load(f)
  199. for m in self.mod['compartments']:
  200. self.add_compartment(m)
  201. self.add_default_parameters()
  202. #standard parameters such as one,zero etc.
  203. for s in self.mod['sources']:
  204. #src=mod['sources'][s]
  205. self.add_source(s,self.mod['sources'][s])
  206. self.flows={}
  207. #pars=self.mod['parameters']
  208. pars=self.parSetup['parameters']
  209. for f in self.mod['flows']:
  210. #skip comments
  211. if f.find(':')<0:
  212. continue
  213. comps=f.split(':')
  214. c0=splitVector(comps[0])
  215. c1=splitVector(comps[1])
  216. for x in c0:
  217. for y in c1:
  218. pairName='{}:{}'.format(x,y)
  219. self.flows[pairName]=f
  220. for b in self.mod['bindings']['diffusion']:
  221. #whether to scale transfer constants to organ volume
  222. #default is true, but changing here will assume no scaling
  223. useVolume=1
  224. comps=b.split('->')
  225. try:
  226. pcParName=self.mod['partitionCoefficients'][b]
  227. except KeyError:
  228. pcParName="one"
  229. kParName=self.mod['bindings']['diffusion'][b]
  230. #operate with names to allow for value/function/derived infrastructure
  231. self.bind(comps[0],comps[1],kParName,pcParName,useVolume)
  232. for q in self.mod['bindings']['flow']:
  233. comps=q.split('->')
  234. srcs=splitVector(comps[0])
  235. tgts=splitVector(comps[1])
  236. for cs in srcs:
  237. for ct in tgts:
  238. #get partition coefficient
  239. try:
  240. pcParName=self.mod['partitionCoefficients'][cs]
  241. except KeyError:
  242. pcParName="one"
  243. #get flow (direction could be reversed)
  244. try:
  245. qName=self.flows['{}:{}'.format(cs,ct)]
  246. except KeyError:
  247. qName=self.flows['{}:{}'.format(ct,cs)]
  248. flowParName=self.mod['flows'][qName]
  249. #flowPar=pars[flowParName]
  250. self.bind(cs,ct,flowParName,pcParName,1)
  251. self.build()
  252. def add_default_parameters(self):
  253. pars=self.parSetup['parameters']
  254. pars['one']={'value':1}
  255. pars['zero']={'value':0}
  256. def M(self,t):
  257. for i in self.dM:
  258. for j in self.dM[i]:
  259. self.fM[i,j]=self.dM[i][j](t)
  260. #create an array and fill it with outputs of function at t
  261. return self.fM
  262. def u(self,t):
  263. ub=[f(t) for f in self.fu]
  264. return numpy.array(ub)
  265. def fSS(self,t):
  266. for k in self.qSS:
  267. for i in self.qSS[k]:
  268. for j in self.qSS[k][i]:
  269. #print('[{},{},{}] {}'.format(k,i,j,self.qSS[k][i][j]))
  270. self.SS[k,i,j]=(self.qSS[k][i][j])(t)
  271. return self.SS
  272. def fSY(self,y,t):
  273. #M number of sensitivity parameters
  274. #N number of equations
  275. #fSS is MxNxN
  276. #assume a tabulated solution y(t) at t spaced intervals
  277. qS=self.fSS(t).dot(y)
  278. #qS is MxN
  279. #but NxM is expected, so do a transpose
  280. #for simultaneous calculation, a Nx(M+1) matrix is expected
  281. tS=numpy.zeros((self.n,self.m+1))
  282. #columns from 2..M+1 are the partial derivatives
  283. tS[:,1:]=numpy.transpose(qS)
  284. #first column is the original function
  285. tS[:,0]=self.u(t)
  286. return tS
  287. def fS(self,t):
  288. #M number of sensitivity parameters
  289. #N number of equations
  290. #fSS is MxNxN
  291. #assume a tabulated solution y(t) at t spaced intervals
  292. qS=self.fSS(t).dot(self.getY(t))
  293. return numpy.transpose(qS)
  294. def getSEJ(self,parName):
  295. #find the sensitivity (SE) derivative of Jacobi with
  296. #respect to parameter
  297. try:
  298. return self.seJ[parName]
  299. except KeyError:
  300. self.seJ[parName]={}
  301. return self.seJ[parName]
  302. def getSEJ_comp(self,parName,compartmentName):
  303. #find equation dictating concentration in compartmentName
  304. #for jacobi-parameter derivative
  305. seJ=self.getSEJ(parName)
  306. try:
  307. return seJ[compartmentName]
  308. except KeyError:
  309. seJ[compartmentName]={}
  310. return seJ[compartmentName]
  311. def setY(self,t,y):
  312. self.tck=[None]*self.n
  313. for i in range(self.n):
  314. self.tck[i] = scipy.interpolate.splrep(t, y[:,i], s=0)
  315. def getY(self,t):
  316. fY=numpy.zeros(self.n)
  317. for i in range(self.n):
  318. fY[i]=scipy.interpolate.splev(t, self.tck[i], der=0)
  319. return fY
  320. def calculateUncertainty(self,sol,se):
  321. s2out=numpy.zeros(sol.shape)
  322. #pars=self.mod['parameters']
  323. pars=self.parSetup['parameters']
  324. for parName in pars:
  325. par=pars[parName]
  326. if not calculateDerivative(par):
  327. continue
  328. v=par["value"]
  329. if par['dist']=='lognormal':
  330. #this is sigma^2_lnx
  331. sln2=numpy.log(par["cv"]*par["cv"]+1)
  332. #have to multiplied by value to get the derivative
  333. #with respect to lnx
  334. s2=sln2*v*v
  335. else:
  336. #for Gaussian, cv is sigma/value; get sigma by value multiplication
  337. s2=par["cv"]*par["cv"]*v*v
  338. j=self.lutSE[parName]
  339. fse=se[:,:,j]
  340. print('Calculating for {}/{}:{} [{}]'.format(parName,j,fse.shape,s2))
  341. #se2 is nt x n
  342. se2=numpy.multiply(fse,fse)
  343. s2out+=se2*s2
  344. return numpy.sqrt(s2out)
  345. def get(self,parName):
  346. pars=self.parSetup['parameters']
  347. par=pars[parName]
  348. par['name']=parName
  349. if "value" in par:
  350. return self.getValue(par)
  351. if "function" in par:
  352. return self.getFunction(par)
  353. if "derived" in par:
  354. return self.getDerived(par)
  355. print('Paramter {} not found!'.format(parName))
  356. def getValue(self,par):
  357. v=par["value"]
  358. parName=par['name']
  359. #convert to seconds
  360. try:
  361. parUnits=par['unit'].split('/')
  362. except (KeyError,IndexError):
  363. #no unit given
  364. return valueObject(v,parName)
  365. timeUnit=self.getTimeUnit()
  366. try:
  367. if parUnits[1]==timeUnit:
  368. return valueObject(v,parName)
  369. except IndexError:
  370. #no / in unit name
  371. return valueObject(v,parName)
  372. if parUnits[1]=='min' and timeUnit=='s':
  373. return valueObject(lambda t,v=v: v(t)/60,parName)
  374. if parUnits[1]=='s' and timeUnit=='min':
  375. return valueObject(lambda t,v=v: 60*v(t),parName)
  376. #no idea what to do
  377. return valueObject(v,parName)
  378. def getFunction(self,par):
  379. fName=par['function']
  380. #print('[{}]: getFunction({})'.format(par['name'],par['function']))
  381. df=self.parSetup['functions'][fName]
  382. skip=['type']
  383. par1={x:self.get(df[x]) for x in df if x not in skip}
  384. if df['type']=='linearGrowth':
  385. #print(par1)
  386. return function.linearGrowth(par1)
  387. if df['type']=='linearGrowthFixedSlope':
  388. return function.linearGrowthFixedSlope(par1)
  389. print('Function {} not found!'.format(fName))
  390. def getDerived(self,par):
  391. dName=par['derived']
  392. d=self.parSetup['derivedParameters'][dName]
  393. #print('Derived [{}]: type {}'.format(dName,d['type']))
  394. if d['type']=='product':
  395. #print('{}*{}'.format(d['a'],d['b']))
  396. pA=self.get(d['a'])
  397. a=pA['value']
  398. DA=pA['derivatives']
  399. pB=self.get(d['b'])
  400. b=pB['value']
  401. DB=pB['derivatives']
  402. #even more generic -> df/dp=[df/dA*dA/dp+df/dB*dB/dp]
  403. if any(['function' in pA,'function' in pB]):
  404. fa=function.to(a)
  405. fb=function.to(b)
  406. f=lambda t,a=fa,b=fb:a(t)*b(t)
  407. dfdA=lambda t,b=fb: b(t)
  408. dfdB=lambda t,a=fa: a(t)
  409. dA=function.generate(dfdA,DA)
  410. dB=function.generate(dfdB,DB)
  411. return function.Object(f,[dA,dB])
  412. else:
  413. return derivedObject(a*b,[{'df':b,'D':DA},{'df':a,'D':DB}])
  414. if d['type']=='power':
  415. #print('{}^{}'.format(d['a'],d['n']))
  416. pA=self.get(d['a'])
  417. a=pA['value']
  418. DA=pA['derivatives']
  419. pN=self.get(d['n'])
  420. n=pN['value']
  421. DN=pN['derivatives']
  422. if any(['function' in pA,'function' in pN]):
  423. fa=function.to(a)
  424. fn=function.to(n)
  425. f=lambda t,a=fa,n=fn:numpy.power(a(t),n(t))
  426. dfdA=lambda t,n=fn,f=f,a=fa:n(t)*f(t)/a(t)
  427. dfdN=lambda t,a=fa,f=f:numpy.log(a(t))*f(t)
  428. dA=function.generate(dfdA,DA)
  429. dN=function.generate(dfdN,DN)
  430. return function.Object(f,[dA,dN])
  431. else:
  432. f=numpy.power(a,n)
  433. return derivedObject(f,[{'df':n*f/a,'D':DA},{'df':f*numpy.log(a),'D':DN}])
  434. if d['type']=='ratio':
  435. #print('{}/{}'.format(d['a'],d['b']))
  436. pA=self.get(d['a'])
  437. a=pA['value']
  438. DA=pA['derivatives']
  439. pB=self.get(d['b'])
  440. b=pB['value']
  441. DB=pB['derivatives']
  442. #even more generic -> df/dp=[df/dA*dA/dp+df/dB*dB/dp]
  443. if any(['function' in pA,'function' in pB]):
  444. fa=function.to(a)
  445. fb=function.to(b)
  446. f=lambda t,a=fa,b=fb,:a(t)/b(t)
  447. dfdA=lambda t,f=f,a=fa: f(t)/a(t)
  448. dfdB=lambda t,f=f,b=fb: -f(t)/b(t)
  449. dA=function.generate(dfdA,DA)
  450. dB=function.generate(dfdB,DB)
  451. return function.Object(f,[dA,dB])
  452. else:
  453. return derivedObject(a/b,[{'df':1/b,'D':DA},{'df':-a/b/b,'D':DB}])
  454. if d['type']=='sum':
  455. #print('{}+{}'.format(d['a'],d['b']))
  456. pA=self.get(d['a'])
  457. a=pA['value']
  458. DA=pA['derivatives']
  459. pB=self.get(d['b'])
  460. b=pB['value']
  461. DB=pB['derivatives']
  462. #even more generic -> df/dp=[df/dA*dA/dp+df/dB*dB/dp]
  463. if any(['function' in pA,'function' in pB]):
  464. fa=function.to(a)
  465. fb=function.to(b)
  466. f=lambda t,a=fa,b=fb,:a(t)+b(t)
  467. dfdA=lambda t: 1
  468. dfdB=lambda t: 1
  469. dA=function.generate(dfdA,DA)
  470. dB=function.generate(dfdB,DB)
  471. return function.Object(f,[dA,dB])
  472. else:
  473. return derivedObject(a+b,[{'df':1,'D':DA},{'df':1,'D':DB}])
  474. def calculateDerivative(par):
  475. #add derivatives if dist(short for distribution) is specified
  476. return "dist" in par
  477. def sumValues(dArray,x):
  478. s=0
  479. for a in dArray:
  480. try:
  481. s+=a[x]
  482. except KeyError:
  483. continue
  484. return s
  485. def valueObject(v,parName):
  486. #convert everything to functions
  487. d0={parName:1}
  488. return {'value':v,'derivatives':{parName:1}}
  489. def derivedObject(f,ders):
  490. o={'value':f}
  491. DD=[]
  492. for d in ders:
  493. df=d['df']
  494. D=d['D']
  495. DD.append({x:df*D[x] for x in D})
  496. allKeys=[]
  497. for x in DD:
  498. allKeys.extend(x.keys())
  499. allKeys=list(set(allKeys))
  500. o['derivatives']={x:sumValues(DD,x) for x in allKeys}
  501. return o
  502. def splitVector(v):
  503. if v.find('(')<0:
  504. return [v]
  505. return v[1:-1].split(',')
  506. def parseFunction(formula):
  507. if formula['name']=='exponential':
  508. c0=formula['constant']
  509. k=formula['k']
  510. return lambda t,c=c0,k=k:c*numpy.exp(k*t)
  511. if formula['name']=='constant':
  512. c0=formula['value']
  513. return lambda t,c0=c0:c0
  514. if formula['name']=='Heavyside':
  515. t0=formula['limit']
  516. v=formula['value']
  517. return lambda t,v=v,t0=t0:v if t<t0 else 0
  518. return lambda t:1
  519. def addValue(qdict,compName,v):
  520. #add function to compName of dictionary qdict,
  521. #check if compName exists and handle the potential error
  522. #lambda functions can't be summed directly, so qdict is a list
  523. #that will be merged at matrix generation time
  524. try:
  525. qdict[compName].append(v)
  526. except KeyError:
  527. qdict[compName]=[v]
  528. #also add derivatives
  529. #
  530. # for d in dTarget:
  531. # ctarget=self.getSEJ_comp(d,target)
  532. # addValue(ctarget,target,dTarget[d])
  533. def get(timeUnit,par):
  534. v=par["value"]
  535. #convert to seconds
  536. try:
  537. parUnits=par['unit'].split('/')
  538. except (KeyError,IndexError):
  539. #no unit given
  540. return v
  541. try:
  542. if parUnits[1]==timeUnit:
  543. return v
  544. except IndexError:
  545. #no / in unit name
  546. return v
  547. if parUnits[1]=='min' and timeUnit=='s':
  548. return v/60
  549. if parUnits[1]=='s' and timeUnit=='min':
  550. return 60*v
  551. #no idea what to do
  552. return v