Borehole NMR processing
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

MRProc.py 35KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797
  1. from __future__ import division # in case this is called from python2
  2. from __future__ import print_function
  3. import numpy as np
  4. import matplotlib.pyplot as plt
  5. import sys, os
  6. import notch
  7. from scipy import signal
  8. from scipy.interpolate import splprep, splev, spline # for smoothing spline FITPACK
  9. from scipy.interpolate import UnivariateSpline
  10. from decay import *
  11. import scipy.stats as stats
  12. from pwctimeWhite import *
  13. # for signal slot communication with GUI
  14. #from PyQt4.QtCore import QObject, SIGNAL
  15. from PySide.QtCore import QObject, SIGNAL
  16. from pandas.io.parsers import read_csv #<<- faster than Numpy but not working right now
  17. from timeit import timeit
  18. class MRProc(QObject):
  19. """ Class to process read and invert ORS borehole data
  20. """
  21. def __init__(self):
  22. QObject.__init__(self)
  23. self.burst = False # For Schlumberger or VC burst data
  24. #def __init__(self, filename):
  25. # # what do we need to know that isn't in new files?
  26. # fileName, fileExtension = os.path.splitext(filename)
  27. # if fileExtension == ".ors":
  28. # self.loadORSFile(filename)
  29. #self.DistRes = { }
  30. #self.DistRes = { 'env':np.array(0), 'mod':np.array(0), 'Time':np.array(0) } #[a0,b0,rt20] }
  31. def loadORSFile_OLD(self, filename):
  32. """ Loads ORS files with minimal header info
  33. """
  34. f = open(filename)
  35. header = f.readline().split() #np.loadtxt(filename, comment="#", )
  36. ih = 1
  37. while (header[0] == "#"):
  38. ih += 1
  39. header = f.readline().split() #np.loadtxt(filename, comment="#", )
  40. self.NR = eval(header[0]) # number of records in an echo
  41. self.NE = eval(header[1]) # number of echoes
  42. self.DT = eval(header[2]) # in s
  43. self.TAUE = eval(header[3]) # in s
  44. self.RL = self.DT * self.NR # in s
  45. # pandas is about ~16 times faster than numpy for import (7 sec vs. .5 sec on tested example)
  46. # Parse in datafile, TODO update for newer format and remove hardcoded 3 in pandas
  47. #DATA = np.loadtxt(filename, comments="#", skiprows=ih)
  48. DATA = read_csv(filename, comment="#", sep='\t', skiprows=3, engine='c', header=None, names=('time','inphase','quadrature')).as_matrix()
  49. self.NS = int(np.shape(DATA)[0] / (self.NE * self.NR))
  50. # reshape DATA
  51. #self.DATA = np.reshape( DATA[:,1] + 1j*DATA[:,2], ( self.NS, self.NE, self.NR) )
  52. self.DATA = np.reshape( self.A2D(DATA[:,1]) + 1j*self.A2D(DATA[:,2]), ( self.NS, self.NE, self.NR) )
  53. self.WTIME = np.reshape( DATA[:,0], ( self.NS, self.NE, self.NR) )
  54. self.TIMES = np.arange(self.DT, self.RL + 1e-8, self.DT) # 1 ms record, 1 us dwell
  55. self.emit(SIGNAL("plotRAW()"))
  56. self.emit(SIGNAL("enableDSP()"))
  57. self.emit(SIGNAL("doneStatus()"))
  58. def evaluate(self, value):
  59. if len(value) == 1:
  60. if value[0][-1].isdigit():
  61. return eval(value[0])
  62. else:
  63. if value[0][-1] == "u": # micro
  64. return eval(value[0][0:-1]) * 1e-6
  65. elif value[0][-1] == "M": # mega
  66. return eval(value[0][0:-1]) * 1e6
  67. elif value[0][-1] == "s": # second
  68. return eval(value[0][0:-1])
  69. else:
  70. print("DOOM!")
  71. exit(1)
  72. else:
  73. # TODO
  74. # logic here is a little lacking, but current tool does not do anything
  75. # sneaky with array values, could break in the future though
  76. vals = []
  77. for val in value:
  78. vals.append( eval(val) )
  79. return vals
  80. def loadORSFile(self, fname):
  81. """ Loads ORS files with extensive header info
  82. """
  83. parameters = False
  84. data = False
  85. pars = {}
  86. with open(fname) as f:
  87. iline = 0
  88. for line in f:
  89. if len(line.split()) == 0:
  90. pass # empty do nothing
  91. else:
  92. if line.split()[0] == "[ps]":
  93. parameters = False
  94. # these entries specify the chip programming
  95. if parameters:
  96. # proc header info
  97. split,comment = line.split("#")
  98. var, val = split.split("=")
  99. val = val.split(";")[0].split()
  100. pars[var.strip()] = self.evaluate(val)
  101. if line.split()[0] == "[parameters]":
  102. parameters = True
  103. if data:
  104. if line[0] == "#":
  105. pass
  106. else:
  107. header = line.split()
  108. iline += 1
  109. break
  110. if line.split()[0] == "[data]":
  111. data = True
  112. iline += 1
  113. self.NR = eval(header[0]) # number of records in an echo
  114. self.NE = eval(header[1]) # number of echoes
  115. self.DT = eval(header[2]) # in s
  116. self.TAUE = eval(header[3]) # in s
  117. self.RL = self.DT * self.NR # in s
  118. # Pandas is much faster than numpy
  119. DATA = read_csv(fname, comment="#", sep='\t', skiprows=iline, engine='c', header=None, names=('time','inphase','quadrature')).as_matrix()
  120. self.NS = int(np.shape(DATA)[0] / (self.NE * self.NR))
  121. # reshape DATA
  122. #self.DATA = np.reshape( DATA[:,1] + 1j*DATA[:,2], ( self.NS, self.NE, self.NR) )
  123. self.DATA = np.reshape( self.A2D(DATA[:,1], pars) + 1j*self.A2D(DATA[:,2], pars), ( self.NS, self.NE, self.NR) )
  124. self.WTIME = np.reshape( DATA[:,0], ( self.NS, self.NE, self.NR) )
  125. self.TIMES = np.arange(self.DT, self.RL + 1e-8, self.DT) # 1 ms record, 1 us dwell
  126. self.emit(SIGNAL("plotRAW()"))
  127. self.emit(SIGNAL("enableDSP()"))
  128. self.emit(SIGNAL("doneStatus()"))
  129. # analogue to digital conversion, 'don't quote me on this -- kevin'
  130. def A2D(self, i, pars):
  131. """ Actual voltage is not known. This routine will need to be expaned to account for
  132. calibration datasets. Or perhaps the inversion and fitting routines instead.
  133. TODO, if prestacked data, we need to account for this.
  134. """
  135. #return (i/(.5 * (2.**16)))
  136. #cal = 7.85 # TODO query for this
  137. cal = 5.51 # 1/2 full
  138. return i / ( pars['n'] * pars['cal'] )
  139. def loadJavelinLog(self, filename):
  140. """Loads processed Javelin data"""
  141. #print("Javelin time")
  142. import scipy.io as sio
  143. Data = sio.loadmat(filename)
  144. self.T2T = Data['time'][:,0]
  145. self.TAUE = self.T2T[3] - self.T2T[2]
  146. # Better sigma estimate needed
  147. self.sigma = 2.5
  148. self.T2N = np.random.normal( 0, self.sigma, len(self.T2T) )
  149. self.NS = 1
  150. self.T2D = self.T2N + 1j*Data['se_vector_wc'][:,56] # depth 0
  151. # What about each depth? How is modelling handled.
  152. def loadVCMFile(self, filename):
  153. #print("LOADING VCM file")
  154. Data = np.loadtxt(filename, comments = "#")
  155. self.T2T = Data[:,0]
  156. self.sigma = eval(open( filename, 'r' ).readline().split()[1])
  157. self.T2N = np.random.normal( 0, self.sigma, len(self.T2T) )
  158. self.T2D = self.T2N + 1j*Data[:,1]
  159. self.NS = 1
  160. self.TAUE = self.T2T[3] - self.T2T[2]
  161. #self.emit(SIGNAL("plotLOG10ENV()"))
  162. #self.emit(SIGNAL("enableDSP()"))
  163. #self.emit(SIGNAL("enableINV()"))
  164. #self.emit(SIGNAL("doneStatus()"))
  165. def loadTextFile(self, filename, NS, NE, NR):
  166. """ Loads older format files without header info """
  167. print ("OLD TEXT FORMAT DETECTED, not SUPPORTED RIGHT NOW")
  168. exit()
  169. pass
  170. def batchThread(self, pars, tag, bload=True, bwindow=True, benvelope=True, bphase=True, bgate=True):
  171. #print("batdacheing", load, window, envelope, phase, gate)
  172. fileName, fileExtension = os.path.splitext( str(tag) )
  173. if bload:
  174. #print ("loading", tag)
  175. if fileExtension == ".ors":
  176. self.loadORSFile(str(tag))
  177. elif fileExtension == ".vcm":
  178. self.loadVCMFile(str(tag))
  179. if bwindow:
  180. #print("windowing", pars['ftype'], pars['winWidth'])
  181. self.WindowAndStack( pars['ftype'] , pars['winTrimLeft'], pars['winTrimRight'] )
  182. if benvelope:
  183. #print("enveloping", pars['offset'])
  184. self.T2EnvelopeDetect( pars['offset'] )
  185. if bphase:
  186. self.CorrectedAmplitude( 0,0 )
  187. if bgate:
  188. #print("gating", pars['gpd'])
  189. self.gateIntegrate( pars['gpd'], pars['stackEfficiency'] )
  190. self.emit(SIGNAL("doneStatus()"))
  191. def WindowAndStack(self, ftype="Blackman-Harris", triml=0, trimr=0):
  192. # TODO, consider lowpass filter and interpolate to higher DT in order to minimize window amplitude effects
  193. # what would cost of such an operation be?
  194. # TODO, should record be overwritten? I don't think so. I think we should make a deep copy here and then you can always fall back.
  195. # Trim ends to centre echo, this many are REMOVED
  196. #triml = 18
  197. #trimr = 15
  198. self.TIMES = self.TIMES[triml:-(trimr)]
  199. self.NR -= triml + trimr
  200. if ftype == "Blackman-Harris":
  201. window = signal.blackmanharris(self.NR)
  202. elif ftype == "Blackman":
  203. window = signal.blackman(self.NR)
  204. elif ftype == "Hann":
  205. window = signal.hann(self.NR)
  206. elif ftype == "sinc":
  207. window = np.ones( (self.NR) )
  208. self.DATAP = self.DATA[:,:,triml:-trimr].copy()
  209. for istk in range(0,self.NS):
  210. for ie in range(0,self.NE,1):
  211. self.DATAP[istk, ie,:] *= window # signal.blackmanharris(self.NR)
  212. #pass
  213. #self.DATA[istk, ie, :] *= signal.hann(self.NR)
  214. #self.DATA[istk, ie, :] *= signal.blackman(self.NR)
  215. #DATA2[istk, ie, :] *= signal.blackmanharris(self.NR)
  216. self.emit(SIGNAL("updateProgress(int)"), (int)(1e2*istk/self.NS))
  217. self.DATASTACK = np.average(self.DATAP, axis=0)
  218. if (self.NS > 1):
  219. # for noise calculation, destructively average phase-cycled echoes
  220. self.DATAP[1::2,:,:] *= -1.
  221. self.NOISE = np.average(self.DATAP, axis=0)
  222. self.NOISE -= np.mean(self.NOISE) # The noise can be biased? Don't understand why!!
  223. #self.DATA[0::2,:,:] *= -1.
  224. self.emit(SIGNAL("plotWIN()"))
  225. self.emit(SIGNAL("enableDSP()"))
  226. self.emit(SIGNAL("doneStatus()"))
  227. def ResampleEnvelopeData(self, Q, Mask=0, GatesPD=0):
  228. """ Mirror the data and then apply window function and resample in Fourier domain at twice the number.
  229. Then take half. Should be better.
  230. GatesPD is Gates Per Decade
  231. """
  232. tt = np.concatenate( [self.T2T[Mask::], self.T2T[Mask::][-1]+self.T2T[Mask::] ] )
  233. xx = np.concatenate( [self.T2D[Mask::][::-1] , self.T2D[Mask::] ] )
  234. xx,tx = signal.resample( xx, 2*Q, t=tt)#, window='hanning')
  235. self.T2D = xx[0:Q][::-1]
  236. #self.T2D = self.T2D[::-1]
  237. xx = np.concatenate( [self.T2N[::-1] , self.T2N] )
  238. xx,tx = signal.resample( xx, 2*Q, t=tt)#, window='hanning')
  239. self.T2N = xx[0:Q][::-1]
  240. # don't reverse times dummy!
  241. self.T2T = tx[0:Q] #[::-1] ## <-- No! bad Trevor
  242. self.NE = len(self.T2D)
  243. def computeSTD(self):
  244. if self.NS >= 2:
  245. self.sigma = np.std( np.imag(self.T2N) )
  246. else:
  247. #N = np.real(self.T2D)
  248. s0 = np.std( np.real(self.T2D) )
  249. #sr = ( np.std( (np.exp(-1j*self.zeta)*N).real ))
  250. #si = ( np.std( (np.exp(-1j*self.zeta)*N).imag ))
  251. print("s0", s0)
  252. self.sigma = s0 # np.array((.15)) #s0 #+ (sr+si-s0)/2.
  253. #self.phasegain = (s0 + (sr+si-s0)/2) / s0
  254. #print("ratio", s0, (s0+(sr+si-s0)/2.)/s0 )
  255. #nr = (np.exp(-1j*self.zeta)*N).real
  256. #ni = (np.exp(-1j*self.zeta)*N).imag
  257. #print ("phasegain", self.phasegain)
  258. #self.T2D.real *= self.phasegain
  259. #print("SIGMA CALCULATION HERE", s0, self.sigma)
  260. def computeSTDBurst(self):
  261. if self.NS >= 2:
  262. self.sigmaBurst = np.std( np.imag(self.T2Nb) )
  263. else:
  264. N = np.real(self.T2Db)
  265. s0 = np.std( np.real(self.T2Db) )
  266. sr = ( np.std( (np.exp(-1j*self.zeta)*N).real) )
  267. si = ( np.std( (np.exp(-1j*self.zeta)*N).imag) )
  268. self.sigmaBurst = s0 #+ (sr+si-s0)/2.
  269. #self.T2Db.real *= self.sigmaBurst/s0 #nr #(nr+ni)/2.
  270. #self.sigmaBurst = np.std( np.real(self.T2Db) )
  271. def gateIntegrateBurst(self, gpd, stackEfficiency):
  272. self.computeSTDBurst()
  273. # calculate total number of decades
  274. nd = np.log10(self.T2Tb[-1]/self.T2Tb[0]) #np.log10(self.T2T[-1]) - np.log10(self.T2T[-1])
  275. tdd = np.logspace( np.log10(self.T2Tb[0]), np.log10(self.T2Tb[-1]), (int)(gpd*nd)+1, base=10, endpoint=True)
  276. tdl = tdd[0:-1] # these are left edges
  277. tdr = tdd[1::] # these are left edges
  278. td = (tdl+tdr) / 2. # window centres
  279. htd = np.zeros( len(td), dtype=complex )
  280. htn = np.zeros( len(td), dtype=complex )
  281. isum = np.zeros( len(td), dtype=int )
  282. Vars = np.ones( len(td) ) * self.sigmaBurst**2 #* .15**2
  283. ii = 0
  284. for itd in range(len(self.T2Tb)):
  285. if ( self.T2Tb[itd] > tdr[ii] ):
  286. if (ii < len(td)-1):
  287. ii += 1
  288. else:
  289. break
  290. isum[ii] += 1
  291. htd[ii] += self.T2Db[ itd ]
  292. #htn[ii] += self.T2N[ itd ]
  293. Vars[ii] += self.sigmaBurst**2
  294. #self.emit(SIGNAL("updateProgress(int)"), (int)(1e2*itd/len(self.T2T)))
  295. # The 1.5 should be 2 in the theory, the 1.5 compensates for correlated noise, etc. Bascically
  296. # we don't see noise reduce like ideal averaging.
  297. self.sigmaBurst = np.sqrt( Vars * (1/isum)**stackEfficiency ) # Var (aX) = a^2 Var(x)
  298. # Bootstrap
  299. bootstrap = False
  300. if bootstrap:
  301. Means, isumbs = self.bootstrapWindows(np.real(self.T2Db), 300, isum)
  302. ts,sm = self.smooth( isumbs, np.std(Means, axis=1) )
  303. for it in range(ii):
  304. self.sigmaBurst[it] = sm[isum[it]]
  305. # RESET times where isum == 1
  306. ii = 0
  307. while (isum[ii] == 1):
  308. td[ii] = self.T2Tb[ii]
  309. ii += 1
  310. htd /= isum
  311. htn /= isum
  312. self.T2Tb = td
  313. self.T2Db = htd
  314. def gateIntegrate(self, gpd, stackEfficiency):
  315. """
  316. Gate integrate the signal to gpd, gates per decade
  317. """
  318. self.computeSTD()
  319. self.gpd = gpd
  320. if self.burst:
  321. self.gateIntegrateBurst(gpd, stackEfficiency)
  322. # calculate total number of decades
  323. nd = np.log10(self.T2T[-1]/self.T2T[0]) #np.log10(self.T2T[-1]) - np.log10(self.T2T[-1])
  324. tdd = np.logspace( np.log10(self.T2T[0]), np.log10(self.T2T[-1]), (int)(gpd*nd)+1, base=10, endpoint=True)
  325. tdl = tdd[0:-1] # these are left edges
  326. tdr = tdd[1::] # these are left edges
  327. td = (tdl+tdr) / 2. # window centres
  328. htd = np.zeros( len(td), dtype=complex )
  329. htn = np.zeros( len(td), dtype=complex )
  330. isum = np.zeros( len(td), dtype=int )
  331. Vars = np.zeros( len(td) )
  332. ii = 0
  333. for itd in range(len(self.T2T)):
  334. if ( self.T2T[itd] > tdr[ii] ):
  335. if (ii < len(td)-1):
  336. ii += 1
  337. else:
  338. break
  339. isum[ii] += 1
  340. htd[ii] += self.T2D[ itd ]
  341. #htn[ii] += self.T2N[ itd ]
  342. Vars[ii] += self.sigma**2
  343. self.emit(SIGNAL("updateProgress(int)"), (int)(1e2*itd/len(self.T2T)))
  344. # Theoretical gates
  345. # The 1.5 should be 2 in the theory, the 1.5 compensates for correlated noise, etc. Bascically
  346. # we don't see noise reduce like ideal averaging.
  347. self.sigma = np.sqrt(Vars * (1./isum)**stackEfficiency ) # Var (aX) = a^2 Var(x)
  348. # Bootstrap
  349. bootstrap = False # False
  350. bs = open("bootstrap.dat", "a")
  351. if bootstrap:
  352. nboot=10000
  353. #Means, isumbs = self.bootstrapWindows(np.real(self.T2D), 100, isum[isum!=1])
  354. Means, isumbs = self.bootstrapWindows(np.real(self.T2D), nboot, np.arange(1,isum[-1]+1))
  355. #ts,sm = self.smooth(isumbs, np.std(Means - np.tile(np.mean(Means,axis=1),(nboot,1)).T , axis=1, ddof=2)) # unbiased
  356. # TODO use MAD instead of STD??
  357. ts,sm = self.smooth(isumbs, np.std(Means, axis=1, ddof=1)) # unbiased
  358. #scale = (1. + (nboot*isum)/(np.ones(ii+1)*len(self.T2D)))**.125
  359. #print (scale)
  360. for item in (sm[isum[isum!=1]-1]-self.sigma[isum!=1]): #/self.sigma[isum!=1]:
  361. bs.write( str(item) + " " )
  362. bs.write("\n")
  363. #ts,sm = self.smooth( isumbs, np.std(Means, axis=1))
  364. #bias = np.average(Means,axis=1)
  365. for it in range(len(self.sigma)):
  366. self.sigma[it] = sm[isum[it]-1]
  367. print('sigma boot', self.sigma)
  368. # RESET times where isum == 1
  369. ii = 0
  370. while (isum[ii] == 1):
  371. td[ii] = self.T2T[ii]
  372. ii += 1
  373. htd /= isum
  374. htn /= isum
  375. self.T2T = td
  376. self.T2D = htd
  377. self.emit(SIGNAL("plotLOG10ENV()"))
  378. self.emit(SIGNAL("enableDSP()"))
  379. self.emit(SIGNAL("enableINV()"))
  380. self.emit(SIGNAL("doneStatus()"))
  381. def bootstrapWindows(self, N, nboot, isum):
  382. """ Bootstraps noise as a function of gate width
  383. """
  384. nc = np.shape(N)[0]
  385. Means = {}
  386. Means = np.zeros( (len(isum), nboot) )
  387. for ii, nwin in enumerate(isum):
  388. for iboot in range(nboot):
  389. cs = np.random.randint(0,nc-nwin)
  390. #Means[ii,iboot] = np.mean( N[cs:cs+nwin] )
  391. Means[ii,iboot] = np.mean( np.random.normal(0, self.sigma[0], nwin))
  392. return Means, np.array(isum)
  393. def smooth(self, x, y):
  394. w = np.ones(len(x))
  395. w[0] = 100. # force first time gate
  396. xs = np.arange(1, x[-1]+1, .1) # resample
  397. #s = UnivariateSpline( np.log(x), np.log(y), s=2.5, w=w)
  398. s = UnivariateSpline( np.log(x), np.log(y), s=4.5, w=w)
  399. #s = UnivariateSpline( x, y, s=2.5, w=w)
  400. ys = s(np.log(xs))
  401. #ys = s(xs)
  402. # resample
  403. ys = ys[0::10]
  404. xs = xs[0::10]
  405. return xs,np.exp(ys)
  406. def T2EnvelopeDetect(self, offset=0):
  407. self.NRS = (int)(2**12) # Number of zeros to pad with, total record length
  408. # Zero pad
  409. DATASTACKZPF = np.append( self.DATASTACK, np.zeros( (self.NE, self.NRS - self.NR) ), axis=1 )
  410. if (self.NS > 1):
  411. NOISEZPF = np.append( self.NOISE, np.zeros( (self.NE, self.NRS - self.NR) ), axis=1 )
  412. # TODO check for proper split
  413. SPLIT = self.NR//2 + offset # 20 # 15 #2
  414. # containers for array
  415. self.T2D = np.zeros( (self.NE), dtype='complex' )
  416. self.T2N = np.zeros( (self.NE), dtype='complex' )
  417. self.T2T = np.zeros( (self.NE) )
  418. # frequency-domain processing
  419. for ie in range(0,self.NE):
  420. COLOUR = np.array([ie/self.NE, 0., 1.-ie/self.NE]) # for plots
  421. ###############################################################
  422. # fold data need to reverse some of these
  423. #FOLD = ((DATASTACK[ie,0:NR/2] + DATASTACK[ie,:][::-1][0:NR/2])/2.)[::-1]
  424. #plt.figure(223)
  425. #plt.plot( self.TAUE*(float)(ie) + self.TIMES, np.imag(self.DATASTACK[ie]), color=COLOUR)
  426. #plt.plot( self.TAUE*(float)(ie) + self.TIMES, np.imag(self.DATA[0,ie]), color='green')
  427. #plt.plot( self.TAUE*(float)(ie) + self.TIMES, np.imag(self.DATA[1,ie]), color='black')
  428. #plt.plot( self.TAUE*(float)(ie) + self.TIMES, np.imag(self.DATA[2,ie]), color='cyan')
  429. #plt.plot( self.TAUE*(float)(ie) + self.TIMES, np.imag(self.DATA[3,ie]), color='purple')
  430. #plt.plot( TAUE*(1e-6)*(float)(ie) + TIMES[SPLIT], np.imag(DATASTACK[ie])[SPLIT], 'o', markersize=6, color=COLOUR)
  431. #plt.plot( TAUE*DT*ie + TIMES, np.real(DATASTACK[ie]), color=COLOUR[::-1])
  432. ##################################################################
  433. # do frequency domain workflow
  434. # fold to DC
  435. ND = np.append( DATASTACKZPF[ie,:][SPLIT::], DATASTACKZPF[ie,:][0:SPLIT] )
  436. #X = 1e-2*((self.RL/2.)/self.NR) * np.fft.fft(ND, n=self.NRS) # Why 1e-2?
  437. X = 1e4*(( (self.DT*self.NRS) /2.)/self.NRS) * np.fft.fft(ND, n=self.NRS) # Why 1e-2?
  438. #X = (self.DT) * np.fft.fft(ND, n=self.NRS) # Why 1e-2?
  439. if (self.NS > 1):
  440. NND = np.append( NOISEZPF[ie,:][SPLIT::], NOISEZPF[ie,:][0:SPLIT] )
  441. #XN = 1e-2*((self.RL/2.)/self.NR) * np.fft.fft(NND, n=self.NRS) # Why 1e-2?
  442. XN = 1e4*(( (self.DT*self.NRS) /2.)/self.NRS) * np.fft.fft(NND, n=self.NRS) # Why 1e-2?
  443. self.T2N[ie] = XN[0] #complex( np.real(X[0]), np.imag(X[0]) )
  444. ##################################################################
  445. # Save T2 records
  446. self.T2D[ie] = X[0] #complex( np.real(X[0]), np.imag(X[0]) )
  447. self.T2T[ie] = (1.+ie)*(self.TAUE)
  448. self.emit(SIGNAL("updateProgress(int)"), (int)(1e2*ie/self.NE))
  449. self.computeSTD()
  450. self.emit(SIGNAL("plotENV()"))
  451. self.emit(SIGNAL("enableDSP()"))
  452. self.emit(SIGNAL("enableINV()"))
  453. self.emit(SIGNAL("doneStatus()"))
  454. def CorrectedAmplitude(self, joe=0, frank=0):
  455. """ Phase corrects the record, joe and Frank are empty (ignored) and are workarounds for threading.
  456. """
  457. #[E0,df,phi,T2] = quadratureDetect(Q, I, tt)
  458. # mask first few echoes, just for detection
  459. NM = 2
  460. #[conv,E0,df,phi,T2] = quadratureDetect(np.imag(self.T2D[NM::]), np.real(self.T2D[NM::]), self.T2T[NM::], False, True)
  461. #[conv,E0,df,phi,T2] = quadratureDetect(np.imag(self.T2D[NM::]), np.real(self.T2D[NM::]), self.T2T[NM::], False, False, True)
  462. # #CorrectFreq=False, BiExp=False, CorrectDC=False)
  463. [conv,E0,df,phi,T2] = quadratureDetect2(np.imag(self.T2D[NM::]), np.real(self.T2D[NM::]), self.T2T[NM::])
  464. self.zeta1 = phi
  465. #if conv: # failed convergence
  466. # [conv,E0,df,phi,T2] = quadratureDetect(np.imag(self.T2D[NM::]), np.real(self.T2D[NM::]), self.T2T[NM::], False, False, False)
  467. self.T2D = self.RotateAmplitude(np.real(self.T2D), np.imag(self.T2D), phi, df, self.T2T)
  468. self.computeSTD()
  469. #print(str(conv) + "\t" + str(phi)) #, file = "phase.dat")
  470. if self.burst:
  471. #[conv,E0,df,phi,T2] = quadratureDetect(np.imag(self.T2Db), np.real(self.T2Db), self.T2Tb, False, False, False)
  472. [conv,E0,df,phi,T2] = quadratureDetect2(np.imag(self.T2Db), np.real(self.T2Db), self.T2Tb) #, False, False, False)
  473. #if conv: # failed convergence
  474. # [conv,E0,df,phi,T2] = quadratureDetect(np.imag(self.T2Db), np.real(self.T2Db), self.T2Tb, False, False, False)
  475. env = E0*np.exp(-np.array(self.T2Tb)/T2)
  476. self.T2Db = self.RotateAmplitude(np.real(self.T2Db), np.imag(self.T2Db), phi, df, self.T2Tb)
  477. self.computeSTDBurst()
  478. self.emit(SIGNAL("plotENV()"))
  479. self.emit(SIGNAL("enableDSP()"))
  480. self.emit(SIGNAL("enableINV()"))
  481. self.emit(SIGNAL("doneStatus()"))
  482. def RotateAmplitude(self, X, Y, zeta, df, t):
  483. self.zeta = zeta
  484. self.df = df
  485. V = X + 1j*Y
  486. return np.abs(V) * np.exp(1j * (np.angle(V) - zeta - 2.*np.pi*df*t))
  487. def MonoFit(self, mask=2, intercept="False"):
  488. component='imag'
  489. #print "MonoRes", component, mask, intercept
  490. a0 = 0
  491. if intercept=="True":
  492. if component == "imag":
  493. [a0,b0,rt20] = regressCurve(np.imag(self.T2D[mask::]), self.T2T[mask::], intercept=True)
  494. if component == "real":
  495. [a0,b0,rt20] = regressCurve(np.real(self.T2D[mask::]), self.T2T[mask::], intercept=True)
  496. rfit = r"$\tilde{T}_2$= %4d [ms]"%(1e3*rt20)
  497. else:
  498. if component == "imag":
  499. [b0,rt20] = regressCurve(np.imag(self.T2D[mask::]), self.T2T[mask::], intercept=False) #, intercept=True) #
  500. if component == "real":
  501. [b0,rt20] = regressCurve(np.real(self.T2D[mask::]), self.T2T[mask::], intercept=False) #, intercept=True) #
  502. rfit = r"$\tilde{T}_2$= %4d [ms]"%(1e3*rt20)
  503. extrapolate = False #False #True #False
  504. if extrapolate:
  505. Textr = 3
  506. nede = np.log10(Textr/self.T2T[-1]) #np.log10(self.T2T[-1]) - np.log10(self.T2T[-1])
  507. tex = np.logspace( np.log10(self.T2T[-1]), np.log10(Textr), (int)(self.gpd*nede)+1, base=10, endpoint=True)
  508. dex = 1j* (a0 + b0*np.exp(-np.array(tex)/rt20))
  509. self.T2T = np.concatenate( (self.T2T, tex) )
  510. self.T2D = np.concatenate( (self.T2D, dex) )
  511. self.sigma = np.concatenate( (self.sigma, self.sigma[-1]*np.ones(len(dex)) ) )
  512. #env = b0*np.exp(-np.array(self.T2T)/rt20)
  513. #self.MonoRes = { 'env':env, 'rfit':rfit, 'b0':b0, 'rt20' :rt20 } #[a0,b0,rt20] }
  514. env = a0 + b0*np.exp(-np.array(self.T2T)/rt20)
  515. self.MonoRes = { 'env':env, 'rfit':rfit, 'a0':a0, 'b0':b0, 'rt20' :rt20, 't':self.T2T } #[a0,b0,rt20] }
  516. self.emit(SIGNAL("plotMONO()"))
  517. self.emit(SIGNAL("enableDSP()"))
  518. self.emit(SIGNAL("enableINV()"))
  519. self.emit(SIGNAL("doneStatus()"))
  520. def BiFit(self, mask=2, intercept="False"):
  521. component='imag'
  522. #print "MonoRes", component, mask, intercept
  523. a0 = 0
  524. if intercept=="True":
  525. if component == "imag":
  526. [a0,b0,rt20,bb0,rrt20] = regressCurve2(np.imag(self.T2D[mask::]), self.T2T[mask::], sigma2=self.sigma[mask::], intercept=True)
  527. if component == "real":
  528. [a0,b0,rt20,bb0,rrt20] = regressCurve2(np.real(self.T2D[mask::]), self.T2T[mask::], sigma2=self.sigma[mask::], intercept=True)
  529. rfit = r"$\tilde{T}_2$= %4d [ms]"%(1e3*rt20)
  530. else:
  531. if component == "imag":
  532. [b0,rt20,bb0,rrt20] = regressCurve2(np.imag(self.T2D[mask::]), self.T2T[mask::], sigma2=self.sigma[mask::], intercept=False) #, intercept=True) #
  533. if component == "real":
  534. [b0,rt20,bb0,rrt20] = regressCurve2(np.real(self.T2D[mask::]), self.T2T[mask::], sigma2=self.sigma[mask::], intercept=False) #, intercept=True) #
  535. rfit = r"$\tilde{T}_2$= %4d [ms]"%(1e3*rt20)
  536. extrapolate = True # True #True
  537. if extrapolate:
  538. Textr = 3
  539. nede = np.log10(Textr/self.T2T[-1]) #np.log10(self.T2T[-1]) - np.log10(self.T2T[-1])
  540. tex = np.logspace( np.log10(self.T2T[-1]), np.log10(Textr), (int)(self.gpd*nede)+1, base=10, endpoint=True)
  541. dex = 1j* (a0 + b0*np.exp(-np.array(tex)/rt20) + bb0*np.exp(-np.array(tex)/rrt20) )
  542. self.T2T = np.concatenate( (self.T2T, tex) )
  543. self.T2D = np.concatenate( (self.T2D, dex) )
  544. self.sigma = np.concatenate( (self.sigma, self.sigma[-1]*np.ones(len(dex)) ) )
  545. #env = b0*np.exp(-np.array(self.T2T)/rt20)
  546. #self.MonoRes = { 'env':env, 'rfit':rfit, 'b0':b0, 'rt20' :rt20 } #[a0,b0,rt20] }
  547. env = a0 + b0*np.exp(-np.array(self.T2T)/rt20) + bb0*np.exp(-np.array(self.T2T)/rrt20)
  548. #print ("bi fits", a0, b0, rt20, bb0, rrt20)
  549. self.BiRes = { 'env':env, 'rfit':rfit, 'a0':a0, 'b0':b0, 'rt20' :rt20, 'bb0':bb0, 'rrt20':rrt20, 't':self.T2T } #[a0,b0,rt20] }
  550. self.emit(SIGNAL("plotBI()"))
  551. self.emit(SIGNAL("enableDSP()"))
  552. self.emit(SIGNAL("enableINV()"))
  553. self.emit(SIGNAL("doneStatus()"))
  554. def DistFit(self, dc=0, mask=0, nT2=30, lowT2=.005, hiT2=3.25, spacing="Log_10", Smooth="Smallest", betaScale=1):
  555. Time = pwcTime()
  556. Time.setT2(lowT2, hiT2, nT2, spacing)
  557. Time.setSampling( self.T2T[mask:] )
  558. Time.generateGenv()
  559. #self.burst = False
  560. #print ("MRProc.py ~~ DistFit self.burst override for publication ", self.burst)
  561. if self.burst:
  562. # fit burst data first
  563. Timeb = pwcTime()
  564. #Timeb.setT2(lowT2, hiT2, nT2, spacing)
  565. Timeb.T2Bins = np.copy(Time.T2Bins)
  566. #if len(Timeb.T2Bins) > 20:
  567. # Timeb.T2Bins = Timeb.T2Bins[0:20] # Subset
  568. Timeb.setSampling( self.T2Tb )
  569. Timeb.generateGenv()
  570. # invert burst data, always use smooth to encourage fast time inversion
  571. modelb = logBarrier(Timeb.Genv, np.imag(self.T2Db)-dc, Timeb.T2Bins, MAXITER=500, sigma=betaScale*self.sigmaBurst, alpha=1e10, smooth=Smooth)
  572. modelb2 = np.zeros(nT2)
  573. modelb2[0:len(modelb)] += modelb
  574. #model = modelb2 # TODO remove
  575. # invert regular data independently, sum inversions
  576. #model = logBarrier(Time.Genv, np.imag(self.T2D[mask:])-dc, MAXITER=500, x_0=modelb, sigma=betaScale*self.sigma[mask:], alpha=1e10, smooth=Smooth)
  577. #model += modelb
  578. # Pass burt result as reference model
  579. model = logBarrier(Time.Genv, np.imag(self.T2D[mask:])-dc, Time.T2Bins, MAXITER=500, xr=modelb2, sigma=betaScale*self.sigma[mask:], alpha=1e10, smooth=Smooth)
  580. else:
  581. model = logBarrier(Time.Genv, np.imag(self.T2D[mask:])-dc, Time.T2Bins, MAXITER=500, sigma=betaScale*self.sigma[mask:], alpha=1e10, smooth=Smooth) #, smooth=True) #
  582. # forward model
  583. env = np.dot(Time.Genv, model)
  584. # NOTE this is not thread safe or even thread aware. Need to use MPI style message passing
  585. self.DistRes = { 'env':env, 'mod':model, 'Time':Time } #[a0,b0,rt20] } no dice
  586. def DistFitMP(self, q, tag, dc=0, mask=0, nT2=30, lowT2=.005, hiT2=3.25, spacing="Log_10", Smooth="Smallest", betaScale=1):
  587. """ Multi-processor version of DistFit using Queue. Note that Queue doesn't
  588. tolerate large messages. So instead we write them to file, using pickle!
  589. It's semi-absurd. But seems to function fast enough.
  590. """
  591. import pickle
  592. self.DistFit(dc, mask, nT2, lowT2, hiT2, spacing, Smooth, betaScale)
  593. self.DistRes['tag'] = tag
  594. pickle.dump( self.DistRes, open( str(tag)+".p", "wb" ) )
  595. q.put( tag )
  596. if __name__ == "__main__":
  597. fileName, fileExtension = os.path.splitext(sys.argv[1])
  598. if fileExtension == ".ors":
  599. ORS = MRProc()
  600. ORS.loadORSFile(sys.argv[1])
  601. ORS.WindowAndStack()
  602. ORS.T2EnvelopeDetect()
  603. #ORS.ResampleEnvelopeData(200, Mask=2, GatesPD=20) # resample size
  604. #print (len(ORS.T2D))
  605. #exit()
  606. #mono, rfit, [a0,b0,mt2] = ORS.MonoFit()
  607. mono, rfit, [b0,mt2] = ORS.MonoFit()
  608. #ca, caEnv, CAS = ORS.CorrectedAmplitude()
  609. mymask = 2
  610. env, mod, Time = ORS.DistFit(0, mymask) #a0)
  611. # Subtract DC term before 2D fit? Kind of kludgy but I don't see a way around it right now.
  612. plt.figure()
  613. #plt.plot(ORS.T2T, np.imag(ca), label='imag ca')
  614. #plt.plot(ORS.T2T, np.real(ca), label='real ca')
  615. #if ORS.NS < 2:
  616. # plt.plot(ORS.T2T, np.imag(ORS.T2D), label='imag')
  617. # plt.plot(ORS.T2T, np.real(ORS.T2D), label='real')
  618. #else:
  619. plt.errorbar(ORS.T2T, np.imag(ORS.T2D), fmt='o', yerr=ORS.sigma, label="data imag")
  620. plt.errorbar(ORS.T2T, np.real(ORS.T2D), fmt='o', yerr=np.std(ORS.T2D.real), label="data real")
  621. if ORS.NS > 2:
  622. plt.errorbar(ORS.T2T, np.imag(ORS.T2N), fmt='o', yerr=ORS.sigma, label="noise imag")
  623. plt.plot(ORS.T2T, mono, color='cyan', linewidth=3, label=rfit)
  624. #plt.plot(ORS.T2T, caEnv, label="car")
  625. plt.plot(ORS.T2T[mymask::], env, color='magenta', linewidth=3, label="dist")
  626. #plt.plot(ORS.T2T, a0+env)
  627. plt.legend()
  628. plt.savefig(sys.argv[1].strip(".ors")+"_caenv.pdf", facecolor='white', edgecolor='white', dpi=200)
  629. # Report RMS error of two results
  630. print("RMS error mono-exponential ", np.linalg.norm(np.imag(ORS.T2D) - mono) )
  631. print("RMS error dist-fit ", np.linalg.norm(np.imag(ORS.T2D[mymask::]) - env) )
  632. print("RMS error real channel ", np.linalg.norm(np.real(ORS.T2D) ) )
  633. if ORS.NS > 2:
  634. print("RMS error noise channel ", np.linalg.norm(np.imag(ORS.T2N) ) )
  635. plt.figure()
  636. plt.plot( ORS.T2T[mymask::], np.imag(ORS.T2D[mymask::]) - env )
  637. plt.title("residuals")
  638. #plt.show()
  639. #exit()
  640. plt.figure()
  641. plt.plot(Time.T2Bins, mod, color='purple', linewidth=2, label="recovered")
  642. #plt.plot(Time.T2Bins, guess, color='red', linewidth=2, label="guess")
  643. axmine = plt.gca()
  644. axmine.set_xlabel(r"$T_2$ [s]", color='black')
  645. axmine.set_ylabel(r"$A_0$ [rku]", color='black')
  646. axmine2 = plt.twinx()
  647. axmine2.set_ylabel(r"$A_0$ [rku]", color='black')
  648. theta = np.sum(mod)
  649. LogMeanT2 = np.exp(np.sum( mod * np.log(Time.T2Bins) ) / theta )
  650. #axmine2.plot(mt2, a0+b0, 'o', markersize=8, label="mono")
  651. axmine2.plot(mt2, b0, 'o', markersize=8, label="mono")
  652. #axmine2.plot(Time.T2Bins, guess, '-',color=colour[ic], linewidth=2, label="total")
  653. axmine2.plot(LogMeanT2, theta, 's', markersize=8, label="$T_{2ML}$")
  654. axmine2.set_ylim([0, axmine2.get_ylim()[1]])
  655. leg = plt.legend()
  656. plt.savefig(sys.argv[1].strip(".ors")+"_2DT2.pdf", facecolor='white', edgecolor='white', dpi=200)
  657. plt.figure(23)
  658. stats.probplot( np.imag(ORS.T2D) - mono , dist="norm", plot=plt) #, label="mono resid")
  659. stats.probplot( np.imag(ORS.T2D[mymask::]) - env , dist="norm", plot=plt) #, label="dist resid")
  660. if ORS.NS > 2:
  661. stats.probplot( np.imag(ORS.T2N), dist="norm", plot=plt) #, label="noise" )
  662. plt.savefig(sys.argv[1].strip(".ors")+"_qq.pdf")
  663. plt.show()
  664. exit()