cockpit 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544
  1. #!/usr/bin/env python
  2. import os
  3. import re
  4. import json
  5. import glob
  6. import shlex
  7. import collections
  8. import datetime
  9. import logging
  10. import random
  11. import argparse
  12. import subprocess
  13. import curses
  14. def read_info_file(path):
  15. result = {}
  16. with open(path) as f:
  17. for line in f:
  18. key, value = line.split('=')
  19. result[key] = value.strip()
  20. return result
  21. def read_edf_id19_header(filename):
  22. result = {}
  23. with open(filename) as f:
  24. data = f.read(1024)
  25. pattern = re.compile(r'(.*) = (.*) ;')
  26. for line in data.split('\n'):
  27. m = pattern.match(line)
  28. if m:
  29. result[m.group(1).strip()] = m.group(2).strip()
  30. return result
  31. def extract_motor_positions(id19_header):
  32. names = id19_header['motor_mne'].split(' ')
  33. values = id19_header['motor_pos'].split(' ')
  34. return {k: float(v) for (k, v) in zip(names, values)}
  35. def have_flats(path):
  36. large = os.path.join(path, 'fc')
  37. small = os.path.join(path, 'fc-small')
  38. return (os.path.exists(large) and
  39. os.path.exists(small) and
  40. glob.glob(os.path.join(large, '*.tif')) and
  41. glob.glob(os.path.join(small, '*.tif')))
  42. def have_params(path):
  43. return os.path.exists(os.path.join(path, 'params.json'))
  44. class Configuration(object):
  45. def __init__(self, source, destination):
  46. self.source = os.path.abspath(source)
  47. self.destination = os.path.abspath(destination)
  48. class LineList(object):
  49. def __init__(self, window):
  50. self.window = window
  51. self.height, self.width = window.getmaxyx()
  52. self.current = 0
  53. self.lines = []
  54. def redraw(self):
  55. for y in range(len(self.lines)):
  56. line, attr = self.lines[y]
  57. self.window.addnstr(y, 0, line, self.width, attr)
  58. self.window.clrtoeol()
  59. self.window.refresh()
  60. def add_line(self, s, attr=0):
  61. self.lines.append((s, attr))
  62. if len(self.lines) > self.height - 1:
  63. self.lines = self.lines[1:]
  64. self.redraw()
  65. def update_last(self, line=None, attr=None):
  66. if not self.lines:
  67. return
  68. old_line, old_attr = self.lines[-1]
  69. self.lines[-1] = (line or old_line, attr or old_attr)
  70. self.redraw()
  71. class Colors(object):
  72. NORMAL = 1
  73. SUCCESS = 2
  74. WARN = 3
  75. ERROR = 4
  76. STATUS_BAR = 5
  77. HIGHLIGHT = 6
  78. def __init__(self):
  79. curses.init_pair(Colors.NORMAL, curses.COLOR_WHITE, curses.COLOR_BLACK)
  80. curses.init_pair(Colors.SUCCESS, curses.COLOR_GREEN, curses.COLOR_BLACK)
  81. curses.init_pair(Colors.WARN, curses.COLOR_YELLOW, curses.COLOR_BLACK)
  82. curses.init_pair(Colors.ERROR, curses.COLOR_RED, curses.COLOR_BLACK)
  83. curses.init_pair(Colors.STATUS_BAR, curses.COLOR_BLACK, curses.COLOR_YELLOW)
  84. curses.init_pair(Colors.HIGHLIGHT, curses.COLOR_WHITE, curses.COLOR_BLACK)
  85. self.attrs = {
  86. Colors.NORMAL: curses.A_NORMAL,
  87. Colors.SUCCESS: curses.A_NORMAL,
  88. Colors.WARN: curses.A_BOLD,
  89. Colors.ERROR: curses.A_BOLD,
  90. Colors.STATUS_BAR: curses.A_NORMAL,
  91. Colors.HIGHLIGHT: curses.A_BOLD,
  92. }
  93. def get(self, code):
  94. return curses.color_pair(code) | self.attrs[code]
  95. class StatusBar(object):
  96. def __init__(self, window, colors):
  97. self.window = window
  98. self.c = colors
  99. def update(self, s):
  100. self.window.bkgd(' ', self.c.get(Colors.STATUS_BAR))
  101. self.window.addstr(s, self.c.get(Colors.STATUS_BAR))
  102. class LogList(LineList):
  103. def __init__(self, window, colors):
  104. self.line_list = LineList(window)
  105. self.c = colors
  106. self.log_file = open('cockpit.log', 'a')
  107. def _log_time(self, s, attr):
  108. timestamp = datetime.datetime.now().strftime('%H:%M:%S')
  109. log = '[{}] {}'.format(timestamp, s)
  110. self.line_list.add_line(log, attr)
  111. self.log_file.write(log)
  112. if not log.endswith('\n'):
  113. self.log_file.write('\n')
  114. self.log_file.flush()
  115. os.fsync(self.log_file.fileno())
  116. def info(self, s):
  117. self._log_time(s, self.c.get(Colors.NORMAL))
  118. def highlight(self, s):
  119. self._log_time(s, self.c.get(Colors.HIGHLIGHT))
  120. def success(self, s):
  121. self._log_time(s, self.c.get(Colors.SUCCESS))
  122. def warn(self, s):
  123. self._log_time(s, self.c.get(Colors.WARN))
  124. def error(self, s):
  125. self._log_time(s, self.c.get(Colors.ERROR))
  126. class CommandList(LineList):
  127. def __init__(self, window, colors):
  128. self.line_list = LineList(window)
  129. self.normal = colors.get(Colors.NORMAL)
  130. self.highlight = colors.get(Colors.HIGHLIGHT)
  131. self.current = ''
  132. def add_character(self, c):
  133. self.current += c
  134. self.line_list.update_last(line='> {}'.format(self.current))
  135. def backspace(self):
  136. if self.current:
  137. self.current = self.current[:len(self.current) - 1]
  138. self.add_character('')
  139. def set_actions(self, actions):
  140. self.line_list.update_last(attr=self.normal)
  141. self.current = ''
  142. message = ' | '.join(('[{}] {}'.format(a.key, a.note) for a in actions.values()))
  143. self.line_list.add_line(message, self.highlight)
  144. class Action(object):
  145. def __init__(self, key, note, func, next_state):
  146. self.key = key
  147. self.note = note
  148. self.run = func
  149. self.next_state = next_state
  150. def __repr__(self):
  151. return '<Action:key={}>'.format(self.key)
  152. class StateMachine(object):
  153. START = 0
  154. QUIT = 1
  155. SYNC = 2
  156. CLEAN = 3
  157. FLATCORRECT = 4
  158. OPTIMIZE = 5
  159. QUICK_OPTIMIZE = 6
  160. RECONSTRUCT = 7
  161. def __init__(self):
  162. self.current = StateMachine.START
  163. self.transitions = {
  164. StateMachine.START: collections.OrderedDict(),
  165. StateMachine.CLEAN: collections.OrderedDict(),
  166. StateMachine.FLATCORRECT: collections.OrderedDict(),
  167. StateMachine.QUIT: collections.OrderedDict(),
  168. StateMachine.SYNC: collections.OrderedDict(),
  169. StateMachine.OPTIMIZE: collections.OrderedDict(),
  170. StateMachine.QUICK_OPTIMIZE: collections.OrderedDict(),
  171. StateMachine.RECONSTRUCT: collections.OrderedDict(),
  172. }
  173. def add_action(self, from_state, action):
  174. self.transitions[from_state][action.key] = action
  175. def transition(self, action):
  176. self.current = self.transitions[self.current][action.key].next_state
  177. @property
  178. def actions(self):
  179. return self.transitions[self.current]
  180. def is_valid_key(self, key):
  181. return key in (k for k in self.transitions[self.current].keys())
  182. class Application(object):
  183. def __init__(self, config):
  184. self.config = config
  185. self.running = True
  186. def run_command(self, cmd):
  187. try:
  188. self.log.info("Executing `{}`".format(cmd))
  189. p = subprocess.Popen(shlex.split(cmd), stdout=subprocess.PIPE, stderr=subprocess.PIPE)
  190. for line in iter(p.stdout.readline, ''):
  191. self.log.info(line)
  192. for line in iter(p.stderr.readline, ''):
  193. self.log.error(line)
  194. while p.poll() is None:
  195. pass
  196. if p.returncode == 0:
  197. self.log.success("done")
  198. return True
  199. self.log.error("Command returned {}".format(p.returncode))
  200. return False
  201. except Exception as e:
  202. self.log.error("{}".format(e))
  203. return False
  204. @property
  205. def prefix(self):
  206. return os.path.basename(self.config.destination)
  207. def read_info(self):
  208. info_file = os.path.join(self.config.destination, '{}.info'.format(self.prefix))
  209. return read_info_file(info_file)
  210. def optimize(self, resize):
  211. slices_per_device = 100
  212. half_range = 1.0
  213. info = self.read_info()
  214. axis = (float(info['Col_end']) + 1) / 2.0
  215. axis_step = 0.25
  216. fname = os.path.join(self.config.destination, '{}0000.edf'.format(self.prefix))
  217. header = read_edf_id19_header(fname)
  218. motor_pos = extract_motor_positions(header)
  219. inclination_angle = motor_pos['rytot']
  220. theta = 90.0 - inclination_angle
  221. angle_step = 0.025
  222. angle_start = theta - half_range
  223. angle_stop = theta + half_range
  224. x_region = 960
  225. y_region = 960
  226. fc_path = '{prefix}/fc'.format(prefix=self.prefix)
  227. if resize:
  228. x_region /= 2
  229. y_region /= 2
  230. axis /= 2
  231. axis_step /= 2
  232. fc_path = '{prefix}/fc-small'.format(prefix=self.prefix)
  233. axis_start = axis - slices_per_device * axis_step
  234. axis_stop = axis + slices_per_device * axis_step
  235. self.log.info(" Using theta = {}, inclination angle = {}".format(theta, inclination_angle))
  236. self.log.info(" Scanning lamino angle within [{}:{}:{}]".format(angle_start, angle_stop, angle_step))
  237. self.log.info(" Scanning x axis within [{}:{}:{}]".format(axis_start, axis_stop, axis_step))
  238. opt_params = ('--num-iterations 2'
  239. ' --axis-range={ax_start},{ax_stop},{ax_step}'
  240. ' --lamino-angle-range={an_start},{an_stop},{an_step}'
  241. ' --metric kurtosis --z-metric kurtosis'
  242. .format(ax_start=axis_start, ax_stop=axis_stop, ax_step=axis_step,
  243. an_start=angle_start, an_stop=angle_stop, an_step=angle_step))
  244. params = ('--x-region="-{x_region},{x_region},1"'
  245. ' --y-region="-{y_region},{y_region},1"'
  246. ' --overall-angle -360'
  247. ' --pixel-size {pixel_size}e-6'
  248. ' --roll-angle 0'
  249. ' --slices-per-device 100'
  250. .format(pixel_size=info['PixelSize'], x_region=x_region, y_region=y_region))
  251. cmd = ('optimize-parameters --verbose'
  252. ' {fc_path}'
  253. ' {opt_params}'
  254. ' --reco-params "{params}"'
  255. ' --params-filename {prefix}/params.json'
  256. .format(opt_params=opt_params, params=params, fc_path=fc_path, prefix=self.prefix))
  257. return self.run_command(cmd)
  258. def on_reconstruct(self):
  259. self.log.highlight("Reconstructing ...")
  260. with open(os.path.join(self.prefix, 'params.json')) as f:
  261. info = self.read_info()
  262. opt = json.load(f)
  263. lamino_angle = opt['lamino-angle']['value']
  264. axis = opt['x-center']['value']
  265. x_region = 960
  266. y_region = 960
  267. cmd = ('tofu lamino --verbose'
  268. ' --projections {prefix}/fc'
  269. ' --x-region="-{x_region},{x_region},1"'
  270. ' --y-region="-{y_region},{y_region},1"'
  271. ' --lamino-angle {lamino_angle}'
  272. ' --axis {x_axis},{y_axis}'
  273. ' --overall-angle -360'
  274. ' --pixel-size {pixel_size}e-6'
  275. ' --roll-angle 0'
  276. ' --slices-per-device 300'
  277. ' --output {prefix}/slices/slice'
  278. .format(pixel_size=info['PixelSize'],
  279. x_region=x_region, y_region=y_region,
  280. lamino_angle=lamino_angle,
  281. x_axis=axis, y_axis=float(info['Dim_2']) / 2,
  282. prefix=self.prefix))
  283. return self.run_command(cmd)
  284. def on_quit(self):
  285. self.running = False
  286. return True
  287. def on_sync(self):
  288. self.log.highlight("Syncing data ...")
  289. cmd = 'bash sync.sh {} {}'.format(self.config.source, self.config.destination)
  290. return self.run_command(cmd)
  291. def on_clean(self):
  292. self.log.highlight("Cleaning {}".format(self.config.destination))
  293. return True
  294. def on_flat_correct(self):
  295. self.log.highlight("Flat field correction ...")
  296. info = self.read_info()
  297. path = self.config.destination
  298. data = dict(path=path, prefix=self.prefix, num=info['TOMO_N'], step=1)
  299. cmd = ('tofu flatcorrect --verbose'
  300. ' --reduction-mode median'
  301. ' --projections "{path}/{prefix}*.edf"'
  302. ' --darks {path}/darkend0000.edf'
  303. ' --flats {path}/ref*_0000.edf'
  304. ' --flats2 {path}/ref*_{num}.edf'
  305. ' --number {num}'
  306. ' --step {step}'
  307. ' --absorptivity'
  308. ' --fix-nan-and-inf'.format(**data))
  309. large_cmd = cmd + ' --output {path}/fc/fc-%04i.tif'.format(path=path)
  310. small_cmd = cmd + ' --resize 2 --output {path}/fc-small/fc-%04i.tif'.format(path=path)
  311. if not self.run_command(small_cmd):
  312. self.log.error("Could not create small flat field corrected projections")
  313. return result
  314. return self.run_command(large_cmd)
  315. def on_quick_optimize(self):
  316. self.log.highlight("Quick optimization ...")
  317. if self.optimize(True):
  318. with open(os.path.join(self.prefix, 'params.json')) as f:
  319. opt = json.load(f)
  320. self.log.highlight(" Optimal axis: {}".format(opt['lamino-angle']['value']))
  321. self.log.highlight(" Optimal center: {}".format(opt['x-center']['value'] * 2.0))
  322. return True
  323. return False
  324. def on_optimize(self):
  325. self.log.highlight("Optimizing ...")
  326. if self.optimize(False):
  327. with open(os.path.join(self.prefix, 'params.json')) as f:
  328. opt = json.load(f)
  329. self.log.highlight(" Optimal axis: {}".format(opt['lamino-angle']['value']))
  330. self.log.highlight(" Optimal center: {}".format(opt['x-center']['value']))
  331. return True
  332. return False
  333. def do_nothing(self):
  334. return True
  335. def _run(self, screen):
  336. curses.curs_set(False)
  337. height, width = screen.getmaxyx()
  338. colors = Colors()
  339. top_pane = screen.subwin(1, width, 0, 0)
  340. bottom_pane = screen.subwin(height - 1, width, 1, 0)
  341. left_pane = bottom_pane.subwin(height - 1, width / 3, 1, 0)
  342. right_pane = bottom_pane.subwin(height - 1, 2 * width / 3, 1, width / 3)
  343. status_bar = StatusBar(top_pane, colors)
  344. status_bar.update('Cockpit')
  345. cmd_window = CommandList(left_pane, colors)
  346. machine = StateMachine()
  347. quit = Action('e', 'Exit', self.on_quit, machine.QUIT)
  348. sync = Action('s', 'Sync', self.on_sync, machine.SYNC)
  349. clean = Action('c', 'Clean', self.on_clean, machine.CLEAN)
  350. quick_optimize = Action('q', 'Quick', self.on_quick_optimize, machine.QUICK_OPTIMIZE)
  351. optimize = Action('o', 'Optimize', self.on_optimize, machine.OPTIMIZE)
  352. reconstruct = Action('r', 'Reconstruct', self.on_reconstruct, machine.RECONSTRUCT)
  353. machine.add_action(machine.START, sync)
  354. machine.add_action(machine.START, quick_optimize)
  355. machine.add_action(machine.START, optimize)
  356. machine.add_action(machine.START, quit)
  357. machine.add_action(machine.SYNC, quit)
  358. machine.add_action(machine.QUICK_OPTIMIZE, reconstruct)
  359. machine.add_action(machine.QUICK_OPTIMIZE, optimize)
  360. machine.add_action(machine.QUICK_OPTIMIZE, quit)
  361. machine.add_action(machine.OPTIMIZE, reconstruct)
  362. machine.add_action(machine.OPTIMIZE, quick_optimize)
  363. machine.add_action(machine.OPTIMIZE, quit)
  364. machine.add_action(machine.CLEAN, sync)
  365. machine.add_action(machine.CLEAN, quit)
  366. if have_params(self.config.destination):
  367. machine.add_action(machine.START, reconstruct)
  368. if not have_flats(self.config.destination):
  369. flatcorrect = Action('f', 'Flat correct', self.on_flat_correct, machine.FLATCORRECT)
  370. machine.add_action(machine.START, flatcorrect)
  371. machine.add_action(machine.SYNC, flatcorrect)
  372. machine.add_action(machine.FLATCORRECT, quick_optimize)
  373. machine.add_action(machine.FLATCORRECT, optimize)
  374. machine.add_action(machine.FLATCORRECT, quit)
  375. cmd_window.set_actions(machine.actions)
  376. self.log = LogList(right_pane, colors)
  377. self.log.info('Source dir set to {}'.format(self.config.source))
  378. self.log.info('Destination dir set to {}'.format(self.config.destination))
  379. while self.running:
  380. ci = screen.getch()
  381. cc = chr(ci) if ci < 256 else ''
  382. if machine.is_valid_key(cc):
  383. cmd_window.add_character(cc)
  384. action = machine.actions[cc]
  385. if action.run():
  386. machine.transition(action)
  387. cmd_window.set_actions(machine.actions)
  388. elif ci == curses.KEY_BACKSPACE:
  389. cmd_window.backspace()
  390. screen.refresh()
  391. def run(self):
  392. curses.wrapper(self._run)
  393. if __name__ == '__main__':
  394. parser = argparse.ArgumentParser()
  395. parser.add_argument('--source', help='Source directory', default='.', metavar='DIR')
  396. parser.add_argument('--destination', help='Destination directory', default='.', metavar='DIR')
  397. args = parser.parse_args()
  398. config = Configuration(args.source, args.destination)
  399. app = Application(config)
  400. app.run()