cockpit 13 KB

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