class MADResultsManager(TreeManager): class Data(Enum): NAME, LABEL, ENERGY, EDGE, WAVELENGTH, FPP, FP, DIRECTORY = list( range(8)) Types = [str, str, float, str, float, float, float, str] Columns = ColumnSpec( (Data.LABEL, 'Label', ColumnType.TEXT, '{}', True), (Data.ENERGY, 'Energy', ColumnType.NUMBER, '{:0.3f}', True), (Data.WAVELENGTH, "\u03BB", ColumnType.NUMBER, '{:0.4f}', True), (Data.FP, "f'", ColumnType.NUMBER, '{:0.1f}', True), (Data.FPP, 'f"', ColumnType.NUMBER, '{:0.1f}', True), ) parent = Data.NAME run_info = Property(type=object) run_name = Property(type=str, default='') directory = Property(type=str, default='') def selection_changed(self, model, itr): if itr: self.props.directory = model[itr][self.Data.DIRECTORY.value] self.props.run_name = model[itr][self.Data.NAME.value] self.props.run_info = self.get_items(itr) else: self.props.directory = '' self.props.run_name = '' self.props.run_info = None def make_parent(self, row): parent_row = super(MADResultsManager, self).make_parent(row) parent_row[self.Data.ENERGY.value] = row[self.Data.EDGE.value] return parent_row
class Message(Object): user = Property(type=str, default='') message = Property(type=str, default='') date = Property(type=str, default='') avatar = Property(type=int, default=0) def __init__(self, user, avatar, message, date): super().__init__() self.props.user = user self.props.avatar = avatar self.props.message = message self.props.date = date def update(self, user, avatar, message, date): self.props.user = user self.props.avatar = avatar self.props.message = message self.props.date = date def get_info(self): return { 'user': self.user, 'avatar': self.avatar, 'message': self.message, 'date': self.date, }
class Diagnostic(Object): """ Base class for diagnostics. """ class State(Enum): GOOD, WARN, BAD, UNKNOWN, DISABLED = list(range(5)) state = Property(type=object) message = Property(type=str, default='') def __init__(self, descr): super(Diagnostic, self).__init__() self.description = descr self.props.state = self.State.UNKNOWN Registry.subscribe(IDiagnostic, self) def __repr__(self): return "<{}:'{}', status:{}>".format(self.__class__.__name__, self.description, self.state[0].name) def update_status(self, status, msg): if status != self.state or msg != self.message: self.props.state = status self.props.message = msg if status != self.State.GOOD: logger.warning("{}: {}".format(self.description, msg))
class XRFResultsManager(TreeManager): class Data(Enum): SELECTED, SYMBOL, NAME, PERCENT, DIRECTORY = list(range(5)) Types = [bool, str, str, float, str] Columns = ColumnSpec( (Data.SYMBOL, 'Symbol', ColumnType.TEXT, '{}', True), (Data.NAME, 'Element', ColumnType.TEXT, '{}', True), (Data.PERCENT, 'Amount', ColumnType.NUMBER, '{:0.1f} %', True), (Data.SELECTED, '', ColumnType.TOGGLE, '{}', False), ) flat = True directory = Property(type=str, default='') def format_cell(self, column, renderer, model, itr, spec): super(XRFResultsManager, self).format_cell(column, renderer, model, itr, spec) index = model.get_path(itr)[0] renderer.set_property("foreground", colors.Category.GOOG20[index % 20]) def selection_changed(self, model, itr): if itr: self.props.directory = model[itr][self.Data.DIRECTORY.value] else: self.props.directory = ''
class GroupItem(Object): selected = Property(type=bool, default=False) name = Property(type=str, default="") changed = Property(type=object) def __init__(self, name, sample_model, items=()): super().__init__() self.props.name = name self.sample_model = sample_model self.items = {path: False for path in items} self.uuid = str(uuid.uuid4()) self.notify_id = self.connect('notify::selected', self.on_selected) self.propagate = True def update_item(self, key, value): """propagate sample selection to whole group preventing cyclic propagation""" if key in self.items and self.items[key] != value: self.items[key] = value if self.propagate: selected = all(self.items.values()) if self.props.selected != selected: self.handler_block( self.notify_id ) # do not propagate if group was set/unset from item self.props.selected = selected self.handler_unblock(self.notify_id) def on_selected(self, obj, param): """Propagate group selection to individual samples preventing cyclic propagation""" changed = set() for sample in self.sample_model: can_change = ( sample[SampleStore.Data.GROUP] == self.props.name and sample[SampleStore.Data.SELECTED] != self.props.selected) if can_change: self.items[sample[SampleStore.Data.UUID]] = self.props.selected valid_ports = [Port.GOOD, Port.UNKNOWN, Port.MOUNTED] if sample[SampleStore.Data.PORT] and sample[ SampleStore.Data.STATE] in valid_ports: sample[SampleStore.Data.SELECTED] = self.props.selected sample[ SampleStore.Data.PROGRESS] = SampleStore.Progress.NONE changed.add(sample[SampleStore.Data.DATA]['id']) self.props.changed = changed def __str__(self): return '<Group: {}|{}>'.format(self.props.name, self.props.selected)
class BaseStorageRing(Device): """ Base class for storage ring devices Signals: - **ready**: arg_types=(bool,), beam available state Properties: - **current**: float, stored current - **mode**: int, operational mode - **state**: int, storage ring state - **message**: str, storage ring message """ class Signals: ready = Signal("ready", arg_types=(bool, )) # Properties current = Property(type=float, default=0.0) mode = Property(type=int, default=0) state = Property(type=int, default=0) message = Property(type=str, default='') def __init__(self): super().__init__() def check_ready(self, *args, **kwargs): return True def beam_available(self): """ Check beam availability """ return self.get_state('ready') def wait_for_beam(self, timeout=60): """ Wait for beam to become available :param timeout: timeout period """ while not self.get_state('ready') and timeout > 0: time.sleep(0.05) timeout -= 0.05 logger.warn('Timed out waiting for beam!')
class CryostatBase(Device): """ Base class for all cryostat devices. A cryostat maintains low temperatures at the sample position. Signals: - temp (float,): Sample temperature - level (float,): Cryogen level - sample (float,): Cryogen flow-rate - shield (float,): Shield flow-rate """ class Positions(Enum): IN, OUT = range(2) class Signals: temp = Signal('temp', arg_types=(float, )) level = Signal('level', arg_types=(float, )) sample = Signal('sample', arg_types=(float, )) shield = Signal('shield', arg_types=(float, )) pos = Signal('position', arg_types=(object, )) # Properties temperature = Property(type=float, default=0.0) shield = Property(type=float, default=0.0) sample = Property(type=float, default=0.0) level = Property(type=float, default=0.0) def configure(self, temp=None, sample=None, shield=None, position=None): """ Configure the Cryostat. :param temp: Set the target sample temperature :param sample: Set the sample flow rate :param shield: Set the shield flow rate :param position: If the cryostat set the position. Should be one of Positions.IN, Positions.OUT """ def stop(self): """ Stop the cryostat """ def start(self): """
class Setting(Object): name = Property(type=str, default='') key = Property(type=str, default='') icon = Property(type=str, default='') info = Property(type=object) value = Property(type=str, default='') def __init__(self, key, icon, validator, kind='string'): super().__init__() self.props.icon = icon self.props.key = key self.kind = kind self.props.name = key.replace('-', ' ').title() self.props.info = settings.get_setting_properties(key) self.validator = validator self.props.value = settings.get_string(key) settings.Settings.bind(key, self, 'value', 0) def validate(self, value): return self.validator(value)
class XASResultsManager(TreeManager): class Data(Enum): NAME, EDGE, SCAN, TIME, X_PEAK, Y_PEAK, DIRECTORY = list(range(7)) Types = [str, str, int, str, float, float, str] Columns = ColumnSpec( (Data.SCAN, 'Scan', ColumnType.TEXT, '{}', True), (Data.TIME, 'TIME', ColumnType.TEXT, '{}', True), (Data.X_PEAK, 'X-Peak', ColumnType.NUMBER, '{:0.3f}', True), (Data.Y_PEAK, 'Y-Peak', ColumnType.NUMBER, '{:0.1f}', True), ) parent = Data.NAME directory = Property(type=str, default='') def make_parent(self, row): parent_row = super(XASResultsManager, self).make_parent(row) parent_row[self.Data.TIME.value] = row[self.Data.EDGE.value] return parent_row def selection_changed(self, model, itr): if itr: self.props.directory = model[itr][self.Data.DIRECTORY.value] else: self.props.directory = ''
class RasterController(Object): class StateType: READY, ACTIVE, PAUSED = list(range(3)) Fields = ( FieldSpec('exposure', 'entry', '{:0.3g}', Validator.Float(0.001, 720, 0.5)), FieldSpec('resolution', 'entry','{:0.2g}', Validator.Float(0.5, 50, 2.0)), FieldSpec('width', 'entry', '{:0.0f}', Validator.Float(5., 500., 200.)), FieldSpec('height', 'entry', '{:0.0f}', Validator.Float(5., 500., 200.)), FieldSpec('frames', 'entry', '{:d}', Validator.Int(1, 100, 10)), FieldSpec('lines', 'entry', '{:d}', Validator.Int(1, 100, 10)), FieldSpec('angle', 'entry', '{:0.3g}', Validator.Float(-1e6, 1e6, 0.0)), FieldSpec('aperture', 'entry', '{:0.0f}', Validator.Float(-1e6, 1e6, 50.)), ) state = Property(type=int, default=StateType.READY) def __init__(self, view, widget): super().__init__() self.view = view self.widget = widget # housekeeping self.start_time = 0 self.pause_dialog = None self.results = {} self.form = RasterForm( widget, fields=self.Fields, prefix='raster', persist=True, disabled=('lines', 'frames') ) self.beamline = Registry.get_utility(IBeamline) self.microscope = Registry.get_utility(IMicroscope) self.sample_store = Registry.get_utility(ISampleStore) self.collector = RasterCollector() self.manager = RasterResultsManager(self.view, colormap=self.microscope.props.grid_cmap) # signals self.microscope.connect('notify::grid-params', self.on_grid_changed) self.connect('notify::state', self.on_state_changed) self.beamline.aperture.connect('changed', self.on_aperture) self.beamline.goniometer.omega.connect('changed', self.on_angle) self.collector.connect('done', self.on_done) self.collector.connect('paused', self.on_pause) self.collector.connect('stopped', self.on_stopped) self.collector.connect('progress', self.on_progress) self.collector.connect('started', self.on_started) self.collector.connect('result', self.on_results) self.setup() def setup(self): self.view.props.activate_on_single_click = False self.widget.raster_start_btn.connect('clicked', self.start_raster) self.widget.raster_stop_btn.connect('clicked', self.stop_raster) self.view.connect('row-activated', self.on_result_activated) def start_raster(self, *args, **kwargs): if self.props.state == self.StateType.ACTIVE: self.widget.raster_progress_lbl.set_text("Pausing raster ...") self.collector.pause() elif self.props.state == self.StateType.PAUSED: self.widget.raster_progress_lbl.set_text("Resuming raster ...") self.collector.resume() elif self.props.state == self.StateType.READY: self.widget.raster_progress_lbl.set_text("Starting raster ...") params = { 'name': datetime.now().strftime('%y%m%d-%H%M'), 'uuid': str(uuid.uuid4()), 'activity': 'raster', 'energy': self.beamline.energy.get_position(), 'delta': RASTER_DELTA, 'attenuation': self.beamline.attenuator.get(), } params.update(self.microscope.grid_params) params.update(self.form.get_values()) params.update({ 'distance': resol_to_dist(params['resolution'], self.beamline.detector.mm_size, params['energy']), 'origin': self.beamline.goniometer.stage.get_xyz(), }) params = datatools.update_for_sample(params, self.sample_store.get_current()) self.collector.configure(params) self.collector.start() def stop_raster(self, *args, **kwargs): self.widget.raster_progress_lbl.set_text("Stopping raster ...") self.collector.stop() def on_aperture(self, aperture, value): self.form.set_value('aperture', value, propagate=True) def on_angle(self, omega, value): self.form.set_value('angle', value) def on_state_changed(self, obj, param): if self.props.state == self.StateType.ACTIVE: self.widget.raster_start_icon.set_from_icon_name( "media-playback-pause-symbolic", Gtk.IconSize.LARGE_TOOLBAR ) self.widget.raster_stop_btn.set_sensitive(True) self.widget.raster_start_btn.set_sensitive(True) self.widget.raster_config_box.set_sensitive(False) self.view.set_sensitive(False) elif self.props.state == self.StateType.PAUSED: self.widget.raster_progress_lbl.set_text("Rastering paused!") self.widget.raster_start_icon.set_from_icon_name( "media-playback-start-symbolic", Gtk.IconSize.LARGE_TOOLBAR ) self.widget.raster_stop_btn.set_sensitive(True) self.widget.raster_start_btn.set_sensitive(True) self.widget.raster_config_box.set_sensitive(False) self.view.set_sensitive(True) else: self.widget.raster_start_icon.set_from_icon_name( "media-playback-start-symbolic", Gtk.IconSize.LARGE_TOOLBAR ) self.widget.raster_config_box.set_sensitive(True) self.widget.raster_start_btn.set_sensitive(True) self.widget.raster_stop_btn.set_sensitive(False) self.view.set_sensitive(True) def on_started(self, collector, config): self.start_time = time.time() self.props.state = self.StateType.ACTIVE logger.info("Rastering Started.") self.microscope.props.grid_xyz = config['grid'] self.results[config['uuid']] = { 'config': copy.deepcopy(config), 'scores': {} } directory = config['directory'] home_dir = misc.get_project_home() current_dir = directory.replace(home_dir, '~') self.widget.dsets_dir_fbk.set_text(current_dir) def on_done(self, obj, data): self.props.state = self.StateType.READY self.widget.raster_progress_lbl.set_text("Rastering Completed.") self.widget.raster_eta.set_text('--:--') self.widget.raster_pbar.set_fraction(1.0) def on_pause(self, obj, paused, reason): if paused: self.props.state = self.StateType.PAUSED if reason: # Build the dialog message self.pause_dialog = dialogs.make_dialog( Gtk.MessageType.WARNING, 'Rastering Paused', reason, buttons=(('OK', Gtk.ResponseType.OK),) ) self.pause_dialog.run() self.pause_dialog.destroy() self.pause_dialog = None else: self.props.state = self.StateType.ACTIVE if self.pause_dialog: self.pause_dialog.destroy() self.pause_dialog = None def on_error(self, obj, reason): error_dialog = dialogs.make_dialog( Gtk.MessageType.WARNING, 'Rastering Error!', reason, buttons=(('OK', Gtk.ResponseType.OK),) ) error_dialog.run() error_dialog.destroy() def on_stopped(self, obj, data): self.props.state = self.StateType.READY self.widget.raster_progress_lbl.set_text("Rastering Stopped.") self.widget.raster_eta.set_text('--:--') def on_progress(self, obj, fraction, message): used_time = time.time() - self.start_time remaining_time = (1 - fraction) * used_time / fraction eta_time = remaining_time self.widget.raster_eta.set_text('{:0>2.0f}:{:0>2.0f} ETA'.format(*divmod(eta_time, 60))) self.widget.raster_pbar.set_fraction(fraction) self.widget.raster_progress_lbl.set_text(message) def on_results(self, collector, frame, results): index = frame - 1 # frames are 1-based counting config = collector.config['params'] try: score = misc.frame_score(results) except (KeyError, ValueError, AttributeError): score = 0.0 self.microscope.add_grid_score(frame, score) x, y, z = self.microscope.props.grid_xyz[index] # frames parent, child = self.manager.add_item({ 'name': config['name'], 'angle': config['angle'], 'cell': frame, 'x_pos': x, 'y_pos': y, 'z_pos': z, 'score': score, 'color': score, 'filename': results['filename'], 'uuid': config['uuid'], }) self.results[config['uuid']]['scores'][frame] = score if parent: self.view.expand_row(parent, False) self.view.scroll_to_cell(child, None, True, 0.5, 0.5) self.microscope.update_grid() def on_result_activated(self, view, path, column=None): itr = self.manager.model.get_iter(path) item = self.manager.get_item(itr) if self.manager.model.iter_has_child(itr): self.beamline.goniometer.omega.move_to(item['angle'], wait=True) grid = self.results[item['uuid']] self.microscope.load_grid( grid['grid'], {'origin': grid['config']['origin'], 'angle': grid['config']['angle']}, grid['scores'] ) self.widget.dsets_dir_fbk.set_text(grid['config']['directory']) else: image_viewer = Registry.get_utility(IImageViewer) self.beamline.goniometer.stage.move_xyz(item['x_pos'], item['y_pos'], item['z_pos']) image_viewer.open_frame(item['filename']) def on_grid_changed(self, obj, param): params = self.microscope.grid_params if params is not None and 'width' in params and 'height' in params: self.form.set_values({'width': params['width'], 'height': params['height']}, propagate=True)
class SampleStore(Object): class Data(object): (SELECTED, NAME, GROUP, CONTAINER, PORT, LOCATION, BARCODE, MISMATCHED, PRIORITY, COMMENTS, STATE, CONTAINER_TYPE, PROGRESS, UUID, SORT_NAME, DATA) = list(range(16)) TYPES = (bool, str, str, str, str, str, str, bool, int, str, int, str, int, str, object, object) class Progress(object): NONE, PENDING, ACTIVE, DONE = list(range(4)) Column = OrderedDict([ (Data.SELECTED, ''), (Data.STATE, ''), (Data.NAME, 'Name'), (Data.GROUP, 'Group'), (Data.PORT, 'Port'), (Data.LOCATION, 'Container'), (Data.PRIORITY, 'Priority'), ]) class Signals: updated = Signal("updated", arg_types=()) # properties cache = Property(type=object) current_sample = Property(type=object) next_sample = Property(type=object) ports = Property(type=object) containers = Property(type=object) def __init__(self, view, widget): super().__init__() self.model = Gtk.ListStore(*self.Data.TYPES) self.search_model = self.model.filter_new() self.search_model.set_visible_func(self.search_data) self.sort_model = Gtk.TreeModelSort(model=self.search_model) self.group_model = Gio.ListStore(item_type=GroupItem) self.group_registry = {} self.prefetch_pending = False self.initializing = True # initialize properties self.props.next_sample = {} self.props.current_sample = {} self.props.ports = set() self.props.containers = set() cache = load_cache('samples') self.props.cache = set() if not cache else set(cache) self.filter_text = '' self.view = view self.widget = widget self.view.set_model(self.sort_model) self.beamline = Registry.get_utility(IBeamline) self.setup() self.model.connect('row-changed', self.on_sample_row_changed) self.widget.samples_selectall_btn.connect('clicked', self.on_select_all) self.widget.samples_selectnone_btn.connect('clicked', self.on_unselect_all) self.view.connect('key-press-event', self.on_key_press) self.widget.samples_reload_btn.connect('clicked', lambda x: self.import_mxlive()) self.beamline.automounter.connect('sample', self.on_sample_mounted) self.beamline.automounter.connect('next-port', self.on_prefetched) self.beamline.automounter.connect('ports', self.update_states) self.widget.samples_mount_btn.connect('clicked', lambda x: self.mount_action()) self.widget.samples_dismount_btn.connect( 'clicked', lambda x: self.dismount_action()) self.widget.samples_search_entry.connect('search-changed', self.on_search) self.widget.mxdc_main.connect('realize', self.import_mxlive) self.connect('notify::cache', self.on_cache) self.connect('notify::current-sample', self.on_cur_changed) self.connect('notify::next-sample', self.on_next_changed) Registry.add_utility(ISampleStore, self) def get_current(self): return self.current_sample def setup(self): # Selected Column for data, title in list(self.Column.items()): if data == self.Data.SELECTED: renderer = Gtk.CellRendererToggle(activatable=True) renderer.connect('toggled', self.on_row_toggled, self.sort_model) column = Gtk.TreeViewColumn(title=title, cell_renderer=renderer, active=data) column.set_fixed_width(30) elif data == self.Data.STATE: renderer = Gtk.CellRendererText(text="\u25c9") column = Gtk.TreeViewColumn(title=title, cell_renderer=renderer) column.props.sizing = Gtk.TreeViewColumnSizing.FIXED column.set_fixed_width(20) column.set_cell_data_func(renderer, self.format_state) else: renderer = Gtk.CellRendererText() column = Gtk.TreeViewColumn(title=title, cell_renderer=renderer, text=data) column.props.sizing = Gtk.TreeViewColumnSizing.FIXED column.set_expand(True) if data in [ self.Data.NAME, self.Data.PORT, self.Data.GROUP, self.Data.CONTAINER ]: self.sort_model.set_sort_func(data, human_name_sort, data) column.set_sort_column_id(data) column.set_cell_data_func(renderer, self.format_progress) if data in [self.Data.NAME, self.Data.GROUP]: column.set_resizable(True) column.set_clickable(True) column.props.sizing = Gtk.TreeViewColumnSizing.FIXED self.view.append_column(column) self.selection = self.view.get_selection() self.selection.set_mode(Gtk.SelectionMode.MULTIPLE) self.view.props.activate_on_single_click = False self.view.connect('row-activated', self.on_row_activated) self.view.set_enable_search(True) self.view.set_search_entry(self.widget.samples_search_entry) self.view.set_search_column(self.Data.NAME) self.view.set_tooltip_column(self.Data.COMMENTS) self.view.set_search_column(self.Data.NAME) self.sort_model.set_sort_column_id(self.Data.PRIORITY, Gtk.SortType.ASCENDING) self.sort_model.connect('sort-column-changed', lambda x: self.roll_next_sample()) self.roll_next_sample() self.widget.auto_groups_box.bind_model(self.group_model, self.create_group_selector) self.sample_dewar = DewarController(self.widget, self) self.sample_dewar.connect('selected', self.on_dewar_selected) def load_data(self, data): self.clear() groups = defaultdict(list) for item in data: key = self.add_item(item) groups[item['group']].append(key) for name, samples in list(groups.items()): group_item = GroupItem(name, self.model, items=samples) group_item.connect('notify::changed', self.on_group_changed) self.group_model.append(group_item) self.group_registry[name] = group_item self.emit('updated') self.props.containers = self.containers def add_item(self, item): item['uuid'] = str(uuid.uuid4()) if not (item.get('port') and self.beamline.automounter.is_valid(item.get('port'))): item['port'] = '' state = Port.UNKNOWN else: ports = self.beamline.automounter.get_state('ports') state = ports.get(item['port'], Port.UNKNOWN) state = state if state in [Port.BAD, Port.MOUNTED, Port.EMPTY ] else Port.GOOD self.model.append([ item['id'] in self.cache, item.get('name', 'unknown'), item.get('group', ''), item.get('container', ''), item.get('port', ''), item.get('container'), item.get('barcode', ''), False, # not mismatched item.get('priority', 0), item.get('comments', ''), state, item.get('container_type', ''), self.Progress.NONE, item['uuid'], re.split(r'(\d+)', item.get('name', 'unknown')), item ]) if item.get('port'): self.props.ports.add(item['port']) container_location = item['port'].rsplit(item['location'], 1)[0] self.props.containers.add(container_location) return item['uuid'] def create_group_selector(self, item): btn = Gtk.CheckButton(item.props.name) btn.set_active(item.selected) btn.connect('toggled', self.on_group_btn_toggled, item) item.connect('notify::selected', self.on_group_item_toggled, btn) return btn @async_call def schedule_prefetch(self): port = self.next_sample.get('port') cur = self.current_sample.get('port') if port and not self.prefetch_pending and port != cur: self.prefetch_pending = True self.beamline.automounter.prefetch(port, wait=True) self.prefetch_pending = False def on_group_btn_toggled(self, btn, item): if item.props.selected != btn.get_active(): item.props.selected = btn.get_active() def on_group_item_toggled(self, item, param, btn): if item.props.selected != btn.get_active(): btn.set_active(item.props.selected) def on_group_changed(self, item, *args, **kwargs): if item.selected: cache = self.cache | item.changed else: cache = self.cache - item.changed self.props.cache = cache def on_search(self, obj): self.filter_text = obj.get_text() self.search_model.refilter() def on_cache(self, *args, **kwargs): save_cache(list(self.props.cache), 'samples') def on_next_changed(self, *args, **kwargs): self.widget.samples_next_sample.set_text( self.next_sample.get('name', '...')) port = self.next_sample.get('port', '...') or '<manual>' self.widget.samples_next_port.set_text(port) self.widget.samples_mount_btn.set_sensitive(bool(self.next_sample)) # Only prefetch if there is a sample currently mounted if port not in ['...', '<manual>', None ] and self.beamline.automounter.is_mounted(): self.schedule_prefetch() def on_prefetched(self, obj, port): name_style = self.widget.samples_next_sample.get_style_context() port_style = self.widget.samples_next_port.get_style_context() if port: name_style.add_class('prefetched') port_style.add_class('prefetched') row = self.find_by_port(port) if row: self.props.next_sample = row[self.Data.DATA] else: self.props.next_sample = {'port': port} else: name_style.remove_class('prefetched') port_style.remove_class('prefetched') def on_cur_changed(self, *args, **kwargs): self.widget.samples_cur_sample.set_text( self.current_sample.get('name', '...')) port = self.current_sample.get('port', '...') or '<manual>' self.widget.samples_cur_port.set_text(port) self.widget.samples_dismount_btn.set_sensitive( bool(self.current_sample)) self.emit('updated') def search_data(self, model, itr, dat): """Test if the row is visible""" row = model[itr] search_text = " ".join([ str(row[col]) for col in [ self.Data.NAME, self.Data.GROUP, self.Data.CONTAINER, self.Data.PORT, self.Data.COMMENTS, self.Data.BARCODE ] ]) return (not self.filter_text) or (self.filter_text in search_text) def import_mxlive(self, *args, **kwargs): data = self.beamline.lims.get_samples(self.beamline.name) if not 'error' in data: self.widget.notifier.notify( '{} Samples Imported from MxLIVE'.format(len(data))) self.load_data(data) else: self.widget.notifier.notify(data['error']) def format_state(self, column, cell, model, itr, data): value = model[itr][self.Data.STATE] loaded = model[itr][self.Data.PORT] mismatched = model[itr][self.Data.MISMATCHED] if not loaded: cell.set_property("text", "") elif mismatched: col = Gdk.RGBA(red=0.5, green=0.5, blue=0.0, alpha=1.0) cell.set_property("foreground-rgba", col) cell.set_property("text", "\u2b24") elif value in [Port.EMPTY]: col = Gdk.RGBA(red=0.0, green=0.0, blue=0.0, alpha=0.5) cell.set_property("foreground-rgba", col) cell.set_property("text", "\u2b24") elif value in [Port.UNKNOWN]: col = Gdk.RGBA(red=0.0, green=0.0, blue=0.0, alpha=1.0) cell.set_property("foreground-rgba", col) cell.set_property("text", "\u25ef") else: col = Gdk.RGBA(**PortColors[value]) cell.set_property("foreground-rgba", col) cell.set_property("text", "\u2b24") def format_progress(self, column, cell, model, itr, data): value = model.get_value(itr, self.Data.PROGRESS) if value == self.Progress.DONE: cell.set_property("style", Pango.Style.ITALIC) else: cell.set_property("style", Pango.Style.NORMAL) def roll_next_sample(self): items = self.get_selected() if items: self.props.next_sample = items[0] else: self.props.next_sample = {} def get_next(self): itr = self.sort_model.get_iter_first() while itr and not self.sort_model.get_value(itr, self.Data.SELECTED): itr = self.sort_model.iter_next(itr) if itr: return Gtk.TreeRowReference.new(self.sort_model, self.sort_model.get_path(itr)) def find_by_port(self, port): for row in self.model: if row[self.Data.PORT] == port: return row def find_by_id(self, sample_id): for row in self.model: if row[self.Data.DATA].get('id') == sample_id: return row def get_name(self, port): row = self.find_by_port(port) if row: return row[self.Data.NAME] else: return '...' def get_selected(self): return [ row[self.Data.DATA] for row in self.sort_model if row[self.Data.SELECTED] and row[self.Data.STATE] not in [Port.BAD, Port.EMPTY] ] def select_all(self, option=True): # toggle all selected rows otherwise toggle the whole list changed = set() for row in self.model: if row[self.Data.SELECTED] != option: row[self.Data.SELECTED] = option changed.add(row[self.Data.DATA]['id']) cache = self.props.cache if changed: if option: cache |= changed else: cache -= changed self.props.cache = cache def clear(self): self.props.ports = set() self.props.containers = set() self.model.clear() self.group_model.remove_all() self.group_registry = {} self.emit('updated') def toggle_row(self, path): path = self.sort_model.convert_path_to_child_path(path) row = self.search_model[path] if row[self.Data.STATE] not in [Port.BAD, Port.EMPTY]: selected = not row[self.Data.SELECTED] row[self.Data.SELECTED] = selected cache = self.cache if selected: row[self.Data.PROGRESS] = self.Progress.NONE cache.add(row[self.Data.DATA]['id']) else: cache.remove(row[self.Data.DATA]['id']) self.props.cache = cache def update_states(self, obj, ports): for row in self.model: port = row[self.Data.PORT] if port: state = ports.get(port, Port.UNKNOWN) state = state if state in [Port.BAD, Port.MOUNTED, Port.EMPTY ] else Port.GOOD row[self.Data.STATE] = state def on_sample_row_changed(self, model, path, itr): if self.group_registry: val = model[path][self.Data.SELECTED] group = model[path][self.Data.GROUP] key = model[path][self.Data.UUID] self.group_registry[group].update_item(key, val) def on_dewar_selected(self, obj, port): row = self.find_by_port(port) if row: self.props.next_sample = row[self.Data.DATA] else: self.props.next_sample = {'port': port} def on_select_all(self, obj, *args): try: self.select_all(True) except ValueError: pass def on_unselect_all(self, obj, *args): try: self.select_all(False) except ValueError: pass def on_sample_mounted(self, obj, sample): if sample: port = sample.get('port', '') row = self.find_by_port(port) if row: self.props.current_sample = row[self.Data.DATA] row[self.Data.SELECTED] = False self.widget.samples_dismount_btn.set_sensitive(True) if self.props.current_sample['barcode'] != sample.get( 'barcode'): row[self.Data.MISMATCHED] = True else: row[self.Data.MISMATCHED] = False elif self.beamline.is_admin(): self.props.current_sample = { 'port': port, } self.widget.samples_dismount_btn.set_sensitive(True) else: self.props.current_sample = { 'port': port, } self.widget.samples_dismount_btn.set_sensitive(False) else: self.props.current_sample = {} self.widget.spinner.stop() if not self.initializing: self.roll_next_sample() self.initializing = False def on_key_press(self, obj, event): return self.widget.samples_search_entry.handle_event(event) def on_row_activated(self, cell, path, column): path = self.sort_model.convert_path_to_child_path(path) row = self.search_model[path] self.props.next_sample = row[self.Data.DATA] def on_row_toggled(self, cell, path, model): path = Gtk.TreePath.new_from_string(path) self.toggle_row(path) def mount_action(self): if not self.next_sample.get('port'): if self.current_sample: self.dismount_action() self.widget.notifier.notify( 'Switching from Automounter to Manual. Try again after ' 'current sample is done dismounting!') else: self.widget.notifier.notify( 'Manual Mode: Please mount it manually before proceeding') self.props.current_sample = self.next_sample self.props.next_sample = {} elif self.next_sample and self.beamline.automounter.is_mountable( self.next_sample['port']): if self.current_sample and not self.current_sample.get('port'): self.widget.notifier.notify( 'Switching from Manual to Autmounter. Try again after ' 'current sample is has been dismounted manually!') else: self.widget.spinner.start() auto.auto_mount(self.beamline, self.next_sample['port']) def dismount_action(self): if not self.current_sample.get('port'): self.widget.notifier.notify( 'Sample was mounted manually. Please dismount it manually') item = self.find_by_id(self.current_sample.get('id')) item[self.Data.SELECTED] = False self.props.current_sample = {} self.roll_next_sample() elif self.current_sample and self.beamline.automounter.is_mounted( self.current_sample['port']): self.widget.spinner.start() auto.auto_dismount(self.beamline)
class CryoJetBase(Device): """ Cryogenic Nozzle Jet Device """ temperature = Property(type=float, default=0.0) shield = Property(type=float, default=0.0) sample = Property(type=float, default=0.0) level = Property(type=float, default=0.0) def __init__(self, *args, **kwargs): super().__init__() self.name = 'Cryojet' self._previous_flow = 7.0 self.setup(*args, **kwargs) def setup(self, *args, **kwargs): pass def stop_flow(self): """ Stop the flow of the cold nitrogen stream. The current setting for flow rate is saved. """ self._previous_flow = self.sample_fbk.get() self.sample_sp.put(0.0) def resume_flow(self): """ Restores the flow rate to the previously saved setting. """ self.sample_sp.put(self._previous_flow) def on_temp(self, obj, val): if val < 110: self.set_state(health=(0, 'temp', '')) elif val < 115: self.set_state(health=(2, 'temp', 'Temp. high!')) else: self.set_state(health=(4, 'temp', 'Temp. too high!')) self.set_property('temperature', val) def on_sample(self, obj, val): if val > 5: self.set_state(health=(0, 'sample', '')) elif val > 4: self.set_state(health=(2, 'sample', 'Sample Flow Low!')) else: self.set_state(health=(4, 'sample', 'Sample Flow Too Low!')) self.set_property('sample', val) def on_shield(self, obj, val): if val > 5: self.set_state(health=(0, 'shield', '')) elif val > 4: self.set_state(health=(2, 'shield', 'Shield Flow Low!')) else: self.set_state(health=(4, 'shield', 'Shield Flow Too Low!')) self.set_property('shield', val) def on_level(self, obj, val): if val < 15: self.set_state(health=(4, 'cryo', 'Cryogen too low!')) elif val < 20: self.set_state(health=(2, 'cryo', 'Cryogen low!')) else: self.set_state(health=(0, 'cryo', '')) self.set_property('level', val) def on_nozzle(self, obj, val): if val: self.set_state(health=(1, 'nozzle', 'Retracted!')) else: self.set_state(health=(0, 'nozzle', 'Restored'))
class HutchSamplesController(Object): ports = Property(type=object) containers = Property(type=object) def __init__(self, widget): super().__init__() self.widget = widget self.props.ports = {} self.props.containers = {} self.beamline = Registry.get_utility(IBeamline) self.microscope = microscope.Microscope(self.widget) self.cryo_tool = cryo.CryoController(self.widget) self.sample_dewar = automounter.DewarController(self.widget, self) self.sample_dewar.connect('selected', self.on_dewar_selected) self.beamline.automounter.connect('sample', self.on_sample_mounted) self.setup() def setup(self): # create and pack devices into settings frame self.image_viewer = imageviewer.ImageViewer() self.widget.datasets_viewer_box.add(self.image_viewer) if self.beamline.is_admin(): self.beamline.detector.connect('new-image', self.on_new_image) def on_new_image(self, obj, dataset): self.image_viewer.show_frame(dataset) def on_dewar_selected(self, obj, port): logger.info('Sample Selected: {}'.format(port)) row = self.find_by_port(port) if row: self.next_sample = row[self.Data.DATA] elif port: self.next_sample = {'port': port} else: self.next_sample = None if self.next_sample: self.widget.samples_mount_btn.set_sensitive(True) self.widget.samples_next_port.set_text(port) else: self.widget.samples_mount_btn.set_sensitive(False) self.widget.samples_next_port.set_text("...") self.widget.samples_next_sample.set_text('...') def get_name(self, port): return '...' def find_by_port(self, port): return None def find_by_id(self, port): return None def on_sample_mounted(self, obj, sample): self.microscope.clear_objects() if sample and self.beamline.is_admin(): self.widget.samples_cur_sample.set_text('...') port = sample.get('port') if port: self.widget.samples_cur_port.set_text(port) self.widget.samples_dismount_btn.set_sensitive(True)
class CryoController(Object): anneal_active = Property(type=bool, default=False) anneal_time = Property(type=float, default=0.0) def __init__(self, widget): super().__init__() self.widget = widget self.beamline = Registry.get_utility(IBeamline) self.cryojet = self.beamline.cryojet self.stopped = True self.labels = {} self.limits = {} self.formats = {} self.setup() self._animation = GdkPixbuf.PixbufAnimation.new_from_resource("/org/mxdc/data/active_stop.gif") def setup(self): self.labels = { 'temperature': self.widget.cryo_temp_fbk, 'level': self.widget.cryo_level_fbk, 'sample': self.widget.cryo_sample_fbk, 'shield': self.widget.cryo_shield_fbk } self.formats = { 'temperature': '{:0.0f}', 'level': '{:0.0f}', 'sample': '{:0.1f}', 'shield': '{:0.1f}', } self.limits = { 'temperature': (105, 110), 'level': (25, 15), 'sample': (5, 4), 'shield': (5, 4), } self.cryojet.connect('notify', self.on_parameter_changed) self.nozzle_monitor = common.BoolMonitor( self.cryojet.nozzle, self.widget.cryo_nozzle_fbk, {True: 'OUT', False: 'IN'}, ) self.connect('notify::anneal-time', self.on_anneal_time) self.connect('notify::anneal-active', self.on_anneal_state) self.widget.anneal_btn.connect('clicked', self.on_anneal_action) def on_anneal_action(self, btn): if self.anneal_active: self.stop_annealing() else: val = self.widget.anneal_entry.get_text() try: t = float(val) except ValueError: pass else: self.props.anneal_time = t self.start_annealing() def on_parameter_changed(self, obj, param): if param.name in self.labels: val = obj.get_property(param.name) txt = self.formats[param.name].format(val) col = common.value_class(val, *self.limits[param.name]) self.labels[param.name].set_text(txt) style = self.labels[param.name].get_style_context() for name in ['dev-error', 'dev-warning']: if col == name: style.add_class(name) else: style.remove_class(name) def on_anneal_state(self, *args, **kwargs): if self.props.anneal_active: self.widget.anneal_icon.set_from_animation(self._animation) self.widget.anneal_entry.set_sensitive(False) else: self.widget.anneal_icon.set_from_icon_name("media-playback-start-symbolic", Gtk.IconSize.BUTTON) self.widget.anneal_entry.set_sensitive(True) def on_anneal_time(self, *args, **kwargs): self.widget.anneal_entry.set_text('{:0.0f}'.format(self.props.anneal_time)) def start_annealing(self): if self.props.anneal_time > 0: message = ( "This procedure will stop the cold cryogen " "stream, \nwarming up your sample for {:0.0f} " "seconds. Are you sure?" ).format(self.props.anneal_time) response = dialogs.warning('Annealing may damage your sample!', message, buttons=(('Cancel', Gtk.ButtonsType.CANCEL), ('Proceed', Gtk.ButtonsType.OK))) if response == Gtk.ButtonsType.OK: self.stopped = False self.props.anneal_active = True duration = 100 GLib.timeout_add(duration, self.monitor_annealing, duration) self.cryojet.stop_flow() def stop_annealing(self): self.cryojet.resume_flow() self.props.anneal_active = False self.stopped = True def monitor_annealing(self, dur): self.props.anneal_time = max(self.props.anneal_time - dur / 1000., 0) if self.anneal_time <= 0.0 or self.stopped: self.stop_annealing() return False return True
class AutomationController(Object): class StateType: STOPPED, PAUSED, ACTIVE, PENDING = list(range(4)) state = Property(type=int, default=0) def __init__(self, widget): super().__init__() self.widget = widget self.beamline = Registry.get_utility(IBeamline) self.image_viewer = Registry.get_utility(IImageViewer) self.run_dialog = datawidget.DataDialog() self.widget.auto_edit_acq_btn.set_popover(self.run_dialog.popover) self.automation_queue = SampleQueue(self.widget.auto_queue) self.automator = Automator() self.config = datawidget.RunItem() self.config_display = ConfigDisplay(self.config, self.widget, 'auto') self.start_time = 0 self.pause_info = None self.automator.connect('done', self.on_done) self.automator.connect('paused', self.on_pause) self.automator.connect('stopped', self.on_stopped) self.automator.connect('progress', self.on_progress) self.automator.connect('sample-done', self.on_sample_done) self.automator.connect('sample-started', self.on_sample_started) self.automator.connect('started', self.on_started) self.automator.connect('error', self.on_error) self.connect('notify::state', self.on_state_changed) # default params = self.run_dialog.get_default(strategy_type=datawidget.StrategyType.SINGLE) params.update({ 'resolution': converter.dist_to_resol( 250, self.beamline.detector.mm_size, self.beamline.energy.get_position() ), 'energy': self.beamline.energy.get_position(), }) self.run_dialog.configure(params) self.config.props.info = self.run_dialog.get_parameters() # btn, type, options method self.tasks = { 'mount': self.widget.mount_task_btn, 'center': self.widget.center_task_btn, 'pause1': self.widget.pause1_task_btn, 'acquire': self.widget.acquire_task_btn, 'analyse': self.widget.analyse_task_btn, 'pause2': self.widget.pause2_task_btn, } self.task_templates = [ (self.widget.mount_task_btn, Automator.Task.MOUNT), (self.widget.center_task_btn, Automator.Task.CENTER), (self.widget.pause1_task_btn, Automator.Task.PAUSE), (self.widget.acquire_task_btn, Automator.Task.ACQUIRE), (self.widget.analyse_task_btn, Automator.Task.ANALYSE), (self.widget.pause2_task_btn, Automator.Task.PAUSE) ] self.options = { 'capillary': self.widget.center_cap_option, 'loop': self.widget.center_loop_option, 'screen': self.widget.analyse_screen_option, 'process': self.widget.analyse_process_option, 'anomalous': self.widget.analyse_anom_option, 'powder': self.widget.analyse_powder_option, 'analyse': self.widget.analyse_task_btn } self.setup() def import_from_cache(self): config = load_cache('auto') if config: self.config.info = config['info'] for name, btn in list(self.tasks.items()): if name in config: btn.set_active(config[name]) for name, option in list(self.options.items()): if name in config: option.set_active(config[name]) def get_options(self, task_type): if task_type == Automator.Task.CENTER: for name in ['loop', 'crystal', 'raster', 'capillary']: if name in self.options and self.options[name].get_active(): return {'method': name} elif task_type == Automator.Task.ACQUIRE: options = {} if self.options['analyse'].get_active(): for name in ['screen', 'process', 'powder']: if self.options[name].get_active(): options = {'analysis': name, 'anomalous': self.options['anomalous'].get_active()} break options.update(self.config.props.info) options['energy'] = self.beamline.energy.get_position() # use current beamline energy return options return {} def get_task_list(self): return [ {'type': kind, 'options': self.get_options(kind)} for btn, kind in self.task_templates if btn.get_active() ] def get_sample_list(self): return self.automation_queue.get_samples() def on_state_changed(self, obj, param): if self.props.state == self.StateType.ACTIVE: self.widget.auto_collect_icon.set_from_icon_name( "media-playback-pause-symbolic", Gtk.IconSize.SMALL_TOOLBAR ) self.widget.auto_stop_btn.set_sensitive(True) self.widget.auto_collect_btn.set_sensitive(True) self.widget.auto_sequence_box.set_sensitive(False) self.widget.auto_groups_btn.set_sensitive(False) elif self.props.state == self.StateType.PAUSED: self.widget.auto_progress_lbl.set_text("Automation paused!") self.widget.auto_collect_icon.set_from_icon_name( "media-playback-start-symbolic", Gtk.IconSize.SMALL_TOOLBAR ) self.widget.auto_stop_btn.set_sensitive(True) self.widget.auto_collect_btn.set_sensitive(True) self.widget.auto_sequence_box.set_sensitive(False) self.widget.auto_groups_btn.set_sensitive(False) elif self.props.state == self.StateType.PENDING: self.widget.auto_collect_icon.set_from_icon_name( "media-playback-start-symbolic", Gtk.IconSize.SMALL_TOOLBAR ) self.widget.auto_sequence_box.set_sensitive(False) self.widget.auto_collect_btn.set_sensitive(False) self.widget.auto_stop_btn.set_sensitive(False) self.widget.auto_groups_btn.set_sensitive(False) else: self.widget.auto_collect_icon.set_from_icon_name( "media-playback-start-symbolic", Gtk.IconSize.SMALL_TOOLBAR ) self.widget.auto_stop_btn.set_sensitive(False) self.widget.auto_collect_btn.set_sensitive(True) self.widget.auto_sequence_box.set_sensitive(True) self.widget.auto_groups_btn.set_sensitive(True) def setup(self): self.import_from_cache() self.widget.auto_edit_acq_btn.connect('clicked', self.on_edit_acquisition) self.run_dialog.data_save_btn.connect('clicked', self.on_save_acquisition) for btn in list(self.tasks.values()): btn.connect('toggled', self.on_save_acquisition) self.widget.auto_collect_btn.connect('clicked', self.on_start_automation) self.widget.auto_stop_btn.connect('clicked', self.on_stop_automation) self.widget.auto_clean_btn.connect('clicked', self.on_clean_automation) self.widget.auto_groups_btn.set_popover(self.widget.auto_groups_pop) self.widget.center_task_btn.bind_property('active', self.widget.center_options_box, 'sensitive') self.widget.analyse_task_btn.bind_property('active', self.widget.analyse_options_box, 'sensitive') self.widget.acquire_task_btn.bind_property('active', self.widget.acquire_options_box, 'sensitive') def on_edit_acquisition(self, obj): info = self.config.info info['energy'] = self.beamline.energy.get_position() self.run_dialog.configure(info) def on_save_acquisition(self, obj): self.config.props.info = self.run_dialog.get_parameters() cache = { 'info': self.config.info, } for name, btn in list(self.tasks.items()): cache[name] = btn.get_active() for name, option in list(self.options.items()): cache[name] = option.get_active() save_cache(cache, 'auto') def on_progress(self, obj, fraction, message): used_time = time.time() - self.start_time remaining_time = 0 if not fraction else (1 - fraction) * used_time / fraction eta_time = remaining_time self.widget.auto_eta.set_text('{:0>2.0f}:{:0>2.0f} ETA'.format(*divmod(eta_time, 60))) self.widget.auto_pbar.set_fraction(fraction) self.widget.auto_progress_lbl.set_text(message) def on_sample_done(self, obj, uuid): self.automation_queue.mark_progress(uuid, SampleStore.Progress.DONE) def on_sample_started(self, obj, uuid): self.automation_queue.mark_progress(uuid, SampleStore.Progress.ACTIVE) def on_done(self, obj, data): self.props.state = self.StateType.STOPPED self.widget.auto_progress_lbl.set_text("Automation Completed.") self.widget.auto_eta.set_text('--:--') self.widget.auto_pbar.set_fraction(1.0) def on_stopped(self, obj, data): self.props.state = self.StateType.STOPPED self.widget.auto_progress_lbl.set_text("Automation Stopped.") self.widget.auto_eta.set_text('--:--') def on_pause(self, obj, paused, reason): if paused: self.props.state = self.StateType.PAUSED if reason: # Build the dialog message self.pause_info = dialogs.make_dialog( Gtk.MessageType.WARNING, 'Automation Paused', reason, buttons=(('OK', Gtk.ResponseType.OK),) ) self.pause_info.run() if self.pause_info: self.pause_info.destroy() self.pause_info = None else: self.props.state = self.StateType.ACTIVE if self.pause_info: self.pause_info.destroy() self.pause_info = None def on_error(self, obj, reason): # Build the dialog message error_dialog = dialogs.make_dialog( Gtk.MessageType.WARNING, 'Automation Error!', reason, buttons=(('OK', Gtk.ResponseType.OK),) ) error_dialog.run() error_dialog.destroy() def on_started(self, obj, data): self.start_time = time.time() self.props.state = self.StateType.ACTIVE logger.info("Automation Started.") def on_stop_automation(self, obj): self.automator.stop() def on_clean_automation(self, obj): self.automation_queue.clean() def on_start_automation(self, obj): if self.props.state == self.StateType.ACTIVE: self.widget.auto_progress_lbl.set_text("Pausing automation ...") self.automator.pause() elif self.props.state == self.StateType.PAUSED: self.widget.auto_progress_lbl.set_text("Resuming automation ...") self.automator.resume() elif self.props.state == self.StateType.STOPPED: tasks = self.get_task_list() samples = self.get_sample_list() if not samples: msg1 = 'Queue is empty!' msg2 = 'Please add samples and try again.' dialogs.warning(msg1, msg2) else: self.widget.auto_progress_lbl.set_text("Starting automation ...") self.props.state = self.StateType.PENDING self.automator.configure(samples, tasks) self.widget.auto_pbar.set_fraction(0) self.automator.start() self.image_viewer.set_collect_mode(True)
class ScanController(Object): class StateType: READY, ACTIVE, PAUSED = list(range(3)) state = Property(type=int, default=StateType.READY) config = Property(type=object) desc = 'MAD Scan' result_class = None ConfigSpec = None prefix = 'mad' Fields = () disabled = () def __init__(self, scanner, plotter, widget, edge_selector): super().__init__() self.widget = widget self.plotter = plotter self.scanner = scanner self.form = FormManager(self.widget, fields=self.Fields, prefix=self.prefix, persist=True, disabled=self.disabled) self.edge_selector = edge_selector self.sample_store = Registry.get_utility(ISampleStore) self.pause_dialog = None self.start_time = 0 self.scan = None self.scan_links = [] self.results = self.result_class(self.results_view) self.setup() def setup(self): self.scanner.connect('started', self.on_started) self.scanner.connect('new-point', self.on_new_point) self.scanner.connect('progress', self.on_progress) self.scanner.connect('paused', self.on_paused) self.scanner.connect('stopped', self.on_stopped) self.scanner.connect('error', self.on_error) self.scanner.connect('done', self.on_done) self.start_btn.connect('clicked', self.start) self.stop_btn.connect('clicked', self.stop) self.edge_btn.set_popover(self.widget.scans_ptable_pop) self.edge_btn.connect('toggled', self.prepare_ptable, self.edge_entry) self.edge_entry.connect('changed', self.hide_ptable) self.edge_entry.connect('changed', self.on_edge_changed) self.connect('notify::state', self.on_state_changed) def update_directory(self, directory): home = misc.get_project_home() dir_text = directory.replace(home, '~') self.widget.scans_dir_fbk.set_text(dir_text) self.widget.scans_dir_fbk.set_tooltip_text(directory) def prepare_ptable(self, btn, entry): if btn.get_active(): self.edge_selector.set_entry(entry) def hide_ptable(self, *args, **kwargs): self.widget.scans_ptable_pop.popdown() def start(self, *args, **kwargs): if self.props.state == self.StateType.ACTIVE: self.progress_lbl.set_text("Pausing {} ...".format(self.desc)) self.scanner.pause() elif self.props.state == self.StateType.PAUSED: self.progress_lbl.set_text("Resuming {} ...".format(self.desc)) self.scanner.resume() elif self.props.state == self.StateType.READY: self.progress_lbl.set_text("Starting {} ...".format(self.desc)) params = self.form.get_values() params['uuid'] = str(uuid.uuid4()) params['name'] = datetime.now().strftime('%y%m%d-%H%M') params['activity'] = '{}-scan'.format(self.prefix) params = datatools.update_for_sample( params, self.sample_store.get_current()) self.props.config = params self.scanner.configure(**self.props.config) self.scanner.start() def stop(self, *args, **kwargs): self.progress_lbl.set_text("Stopping {} ...".format(self.desc)) self.scanner.stop() def on_edge_changed(self, entry): edge = entry.get_text() if edge: absorption, emission = self.edge_selector.get_edge_specs(edge) self.absorption_entry.set_text('{:0.4f}'.format(absorption)) self.emission_entry.set_text('{:0.4f}'.format(emission)) def on_state_changed(self, *args, **kwargs): if self.props.state == self.StateType.ACTIVE: self.start_icon.set_from_icon_name("media-playback-pause-symbolic", Gtk.IconSize.BUTTON) self.stop_btn.set_sensitive(True) self.start_btn.set_sensitive(True) self.config_box.set_sensitive(False) elif self.props.state == self.StateType.PAUSED: self.progress_lbl.set_text("{} paused!".format(self.desc)) self.start_icon.set_from_icon_name("media-playback-start-symbolic", Gtk.IconSize.BUTTON) self.stop_btn.set_sensitive(True) self.start_btn.set_sensitive(True) self.config_box.set_sensitive(False) else: self.start_icon.set_from_icon_name("media-playback-start-symbolic", Gtk.IconSize.BUTTON) self.config_box.set_sensitive(True) self.start_btn.set_sensitive(True) self.stop_btn.set_sensitive(False) def on_new_point(self, scanner, data): self.plotter.add_point(data) def on_progress(self, scanner, fraction, message): if fraction > 0.0: used_time = time.time() - self.start_time remaining_time = (1 - fraction) * used_time / fraction eta_time = remaining_time self.eta.set_text( '{:0>2.0f}:{:0>2.0f} ETA'.format(*divmod(eta_time, 60))) self.pbar.set_fraction(fraction) self.progress_lbl.set_text(message) def on_paused(self, scanner, paused, reason): if paused: self.props.state = self.StateType.PAUSED if reason: # Build the dialog message self.pause_dialog = dialogs.make_dialog( Gtk.MessageType.WARNING, '{} Paused'.format(self.desc), reason, buttons=(('OK', Gtk.ResponseType.OK), )) self.pause_dialog.run() self.pause_dialog.destroy() self.pause_dialog = None else: self.props.state = self.StateType.ACTIVE if self.pause_dialog: self.pause_dialog.destroy() self.pause_dialog = None def on_started(self, scan, specs): """ Clear Scan and setup based on contents of data dictionary. """ self.plotter.clear(specs) self.start_time = time.time() self.props.state = self.StateType.ACTIVE logger.info("{} Started.".format(self.desc)) self.update_directory(scan.config.directory) x_name = specs['data_type']['names'][0] x_unit = specs['units'].get(x_name, '').strip() self.plotter.set_labels( title=specs['scan_type'], x_label='{}{}'.format(x_name, ' ({})'.format(x_unit) if x_unit else ''), ) def on_stopped(self, scanner, data): self.props.state = self.StateType.READY self.progress_lbl.set_text("{} Stopped.".format(self.desc)) self.eta.set_text('--:--') def on_error(self, scanner, message): error_dialog = dialogs.make_dialog(Gtk.MessageType.WARNING, '{} Error!'.format(self.desc), message, buttons=(('OK', Gtk.ResponseType.OK), )) error_dialog.run() error_dialog.destroy() def on_done(self, scan, data): self.props.state = self.StateType.READY self.progress_lbl.set_text("{} Completed.".format(self.desc)) self.eta.set_text('--:--') self.pbar.set_fraction(1.0) def __getattr__(self, item): try: return getattr(self.widget, '{}_{}'.format(self.prefix, item)) except AttributeError: raise AttributeError('{} does not have attribute: {}'.format( self, item))
class ReportManager(TreeManager): class Data(Enum): NAME, GROUP, ACTIVITY, TYPE, SCORE, TITLE, STATE, UUID, SAMPLE, DIRECTORY, DATA, REPORT, ERROR = list( range(13)) Types = [ str, str, str, str, float, str, int, str, object, str, object, object, object ] class State: PENDING, ACTIVE, SUCCESS, FAILED = list(range(4)) Columns = ColumnSpec( (Data.NAME, 'Name', ColumnType.TEXT, '{}', True), (Data.TITLE, "Title", ColumnType.TEXT, '{}', True), (Data.ACTIVITY, "Type", ColumnType.TEXT, '{}', False), (Data.SCORE, 'Score', ColumnType.NUMBER, '{:0.2f}', False), (Data.STATE, "", ColumnType.ICON, '{}', False), ) Icons = { State.PENDING: ('content-loading-symbolic', colors.Category.CAT20[14]), State.ACTIVE: ('emblem-synchronizing-symbolic', colors.Category.CAT20[2]), State.SUCCESS: ('object-select-symbolic', colors.Category.CAT20[4]), State.FAILED: ('computer-fail-symbolic', colors.Category.CAT20[6]), } # # tooltips = Data.TITLE parent = Data.NAME flat = False select_multiple = True single_click = True directory = Property(type=str, default='') sample = Property(type=object) strategy = Property(type=object) def update_item(self, item_id, report=None, error=None, title='????'): itr = self.model.get_iter_first() row = None while itr and not row: if self.model[itr][self.Data.UUID.value] == item_id: row = self.model[itr] break elif self.model.iter_has_child(itr): child_itr = self.model.iter_children(itr) while child_itr: if self.model[child_itr][self.Data.UUID.value] == item_id: row = self.model[child_itr] break child_itr = self.model.iter_next(child_itr) itr = self.model.iter_next(itr) if row: if report: row[self.Data.REPORT.value] = report row[self.Data.SCORE.value] = report.get('score', 0.0) row[self.Data.STATE.value] = self.State.SUCCESS elif error: row[self.Data.ERROR.value] = error row[self.Data.STATE.value] = self.State.FAILED row[self.Data.TITLE.value] = title def row_activated(self, view, path, column): model = view.get_model() itr = model.get_iter(path) item = self.row_to_dict(model[itr]) report = item['report'] or {} self.props.strategy = report.get('strategy') self.props.directory = item['directory'] self.props.sample = item['sample'] def format_cell(self, column, renderer, model, itr, spec): super(ReportManager, self).format_cell(column, renderer, model, itr, spec) if not model.iter_has_child(itr): row = model[itr] state = row[self.Data.STATE.value] if state == self.State.PENDING: renderer.set_property( "foreground-rgba", Gdk.RGBA(red=0.0, green=0.0, blue=0.0, alpha=0.7)) elif state == self.State.FAILED: renderer.set_property( "foreground-rgba", Gdk.RGBA(red=0.35, green=0.0, blue=0.0, alpha=1.0)) elif state == self.State.SUCCESS: renderer.set_property( "foreground-rgba", Gdk.RGBA(red=0.0, green=0.35, blue=0.0, alpha=1.0)) else: renderer.set_property( "foreground-rgba", Gdk.RGBA(red=0.35, green=0.35, blue=0.0, alpha=1.0)) else: renderer.set_property("foreground-rgba", None)
class BaseManager(Device): """ Base Mode Manager. A device to manage beamline modes Signals: - **mode**: beamline mode Properties: - **mode**: beamline mode """ class ModeType(Enum): MOUNT, CENTER, COLLECT, ALIGN, BUSY, UNKNOWN = list(range(6)) class Signals: mode = Signal("mode", arg_types=(object, )) # Properties mode = Property(type=object) def __init__(self, name='Beamline Modes'): super().__init__() self.name = name self.mode = self.ModeType.UNKNOWN def wait(self, *modes, start=True, stop=True, timeout=30): """ Wait for the one of specified modes. :param modes: a list of Mode ENUMS or strings to wait for :param start: (bool), Wait for the manager to become busy. :param stop: (bool), Wait for the manager to become idle. :param timeout: maximum time in seconds to wait before failing. :return: (bool), False if wait timed-out """ mode_set = { m if isinstance(m, self.ModeType) else self.ModeType[m] for m in modes } if self.mode in mode_set: logger.debug('Already in requested mode') return True poll = 0.05 time_left = 2 if start: logger.debug('Waiting for mode manager to start') while not self.is_busy() and time_left > 0: time.sleep(poll) time_left -= poll if time_left <= 0: logger.warn('Timed out waiting for mode manager to start') if stop: time_left = timeout if mode_set: logger.debug('Waiting for {}: {}'.format(self.name, mode_set)) while time_left > 0 and (not self.mode in mode_set) and self.is_busy(): time_left -= poll time.sleep(poll) else: logger.debug('Waiting for {} to stop moving'.format(self.name)) while time_left > 0 and self.is_busy(): time_left -= poll time.sleep(poll) if time_left <= 0: logger.warning('Timed out waiting for {}'.format(self.name)) return False return True def mount(self, wait=False): """ Switch to Mount mode :param wait: wait for switch to complete """ raise NotImplementedError('Sub-classes must implement "mount"') def center(self, wait=False): """ Switch to Center mode :param wait: wait for switch to complete """ raise NotImplementedError('Sub-classes must implement "center"') def collect(self, wait=False): """ Switch to Collect mode :param wait: wait for switch to complete """ raise NotImplementedError('Sub-classes must implement "collect"') def align(self, wait=False): """ Switch to Align mode :param wait: wait for switch to complete """ raise NotImplementedError('Sub-classes must implement "align"') def get_mode(self): """ Return the current mode """ return self.props.mode
class DewarController(Object): class Signals: selected = Signal("selected", arg_types=(str, )) layout = Property(type=object) ports = Property(type=object) containers = Property(type=object) def __init__(self, widget, store): super().__init__() self.widget = widget self.store = store self.beamline = Registry.get_utility(IBeamline) self.setup() self.failure_dialog = None self.messages = [(None, "")] self.props.layout = {} self.props.ports = {} self.props.containers = [] self.beamline.automounter.connect('status', self.on_state_changed) self.beamline.automounter.connect('ports', self.on_layout_changed) self.beamline.automounter.connect('layout', self.on_layout_changed) self.beamline.automounter.connect('containers', self.on_layout_changed) self.beamline.automounter.connect('sample', self.on_layout_changed) self.beamline.automounter.connect('failure', self.on_failure_changed) self.store.connect('notify::ports', self.on_layout_changed) self.store.connect('notify::containers', self.on_layout_changed) self.beamline.automounter.connect('active', self.on_state_changed) self.beamline.automounter.connect('busy', self.on_state_changed) self.beamline.automounter.connect('health', self.on_state_changed) self.beamline.automounter.connect('message', self.on_messages) def setup(self): self.widget.sample_dewar_area.connect('draw', self.draw_dewar) self.widget.sample_dewar_area.set_events( Gdk.EventMask.EXPOSURE_MASK | Gdk.EventMask.LEAVE_NOTIFY_MASK | Gdk.EventMask.BUTTON_PRESS_MASK | Gdk.EventMask.VISIBILITY_NOTIFY_MASK) self.widget.sample_dewar_area.connect('query-tooltip', self.on_query_tooltip) self.widget.sample_dewar_area.connect('button-press-event', self.on_press_event) def get_port_state(self, port): robot_ports = self.beamline.automounter.get_state('ports') user_ports = self.store.ports state = robot_ports.get(port, Port.UNKNOWN) if port in user_ports: if state not in [Port.BAD, Port.EMPTY, Port.MOUNTED]: state = Port.GOOD return state def on_layout_changed(self, obj, *args): self.props.layout = self.beamline.automounter.get_state('layout') robot_ports = self.beamline.automounter.get_state('ports') robot_containers = self.beamline.automounter.get_state('containers') user_ports = self.store.ports user_containers = self.store.containers self.props.ports = { port: self.get_port_state(port) for port in list(robot_ports.keys()) if (port in user_ports or self.beamline.is_admin()) } self.props.containers = { container for container in robot_containers if container in user_containers or self.beamline.is_admin() } self.widget.sample_dewar_area.queue_draw() def draw_dewar(self, widget, cr): # background alloc = widget.get_allocation() cr.save() cr.scale(alloc.width, alloc.height) if self.layout: for loc, container in list(self.layout.items()): container.draw(cr, self.ports, self.containers) else: xscale, yscale = cr.device_to_user_distance(1, 1) cr.set_font_size(14 * xscale) cr.set_line_width(1 * xscale) text = 'Layout Not available!' cr.set_source_rgb(0.5, 0.35, 0) xb, yb, w, h = cr.text_extents(text)[:4] cr.move_to(0.5 - w / 2.0, 0.5 - h / 2.0 - yb) cr.show_text(text) cr.stroke() cr.restore() def find_port(self, x, y): for loc, container in list(self.layout.items()): port = container.get_port(x, y) if port: return loc, port return None, None def on_query_tooltip(self, widget, x, y, keyboard, tooltip): if keyboard: return False alloc = widget.get_allocation() xp = x / alloc.width yp = y / alloc.height loc, port = self.find_port(xp, yp) if loc and port and self.allow_port(loc, port): label = self.store.get_name(port) widget.get_window().set_cursor(Gdk.Cursor.new( Gdk.CursorType.HAND2)) tooltip.set_markup('<small><b>{}</b>\n{}</small>'.format( port, label)) return True else: widget.get_window().set_cursor(None) return False def allow_port(self, container, port): if self.beamline.is_admin() or ((container and port) and (port in self.ports)): return self.get_port_state(port) not in [Port.EMPTY, Port.BAD] return False def on_press_event(self, widget, event): alloc = widget.get_allocation() x = event.x / alloc.width y = event.y / alloc.height loc, port = self.find_port(x, y) if self.allow_port(loc, port): self.emit('selected', port) def on_state_changed(self, obj, *args): status = self.beamline.automounter.get_state('status') if status.name in [ 'IDLE', ]: self.widget.automounter_command_box.set_sensitive(True) self.widget.automounter_status_fbk.set_text(status.name) failure = self.beamline.automounter.get_state('failure') if status.name == 'IDLE' and failure and self.failure_dialog: self.failure_dialog.destroy() self.failure_dialog = None def failure_callback(self, dialog, response, context): if self.failure_dialog: self.failure_dialog.destroy() self.failure_dialog = None if response == Gtk.ButtonsType.OK: self.beamline.automounter.recover(context) message = ("Recovery is in progress. This operation may take\n" "a few minutes. The automounter will be usable again\n" "when it reaches the IDLE state") dialogs.info('Automounter Recovery', message, modal=False) def on_failure_changed(self, obj, failure): failure_context = failure if failure_context: failure_type, message = failure_context self.failure_dialog = dialogs.make_dialog( Gtk.MessageType.QUESTION, 'Automounter Failed: {}'.format( failure_type.replace('-', ' ').title()), message, buttons=(('Cancel', Gtk.ButtonsType.CANCEL), ('Recover', Gtk.ButtonsType.OK)), modal=False) self.failure_dialog.connect('response', self.failure_callback, failure_context) self.failure_dialog.show_all() else: if self.failure_dialog: self.failure_dialog.destroy() self.failure_dialog = None def on_messages(self, obj, message): if message: prev_time, prev_msg = self.messages[-1] if message == prev_msg: return self.messages.append((datetime.now(), message)) if len(self.messages) > 3: self.messages = self.messages[-3:] text = '\n'.join([ "<small><tt>{} - </tt>{}</small>".format( dt.now().strftime('%H:%M:%S'), msg.strip()) for dt, msg in self.messages if dt ]) self.widget.automounter_message_fbk.set_markup(text)
class Microscope(Object): class GridState: PENDING, COMPLETE = list(range(2)) class ToolState(object): DEFAULT, CENTERING, GRID, MEASUREMENT = list(range(4)) grid = Property(type=object) grid_xyz = Property(type=object) grid_state = Property(type=int, default=GridState.PENDING) grid_params = Property(type=object) grid_scores = Property(type=object) grid_cmap = Property(type=object) grid_bbox = Property(type=object) points = Property(type=object) tool = Property(type=int, default=ToolState.DEFAULT) mode = Property(type=object) def __init__(self, widget): super().__init__() self.timeout_id = None self.max_fps = 20 self.fps_update = 0 self.video_ready = False self.queue_overlay() self.overlay_surface = None self.overlay_ctx = None self.props.grid = None self.props.grid_xyz = None self.props.points = [] self.props.grid_bbox = [] self.props.grid_scores = {} self.props.grid_params = {} self.props.grid_state = self.GridState.PENDING self.props.grid_cmap = colors.ColorMapper(min_val=0, max_val=100) self.props.tool = self.ToolState.DEFAULT self.prev_tool = self.tool self.tool_cursors = { self.ToolState.DEFAULT: None, self.ToolState.CENTERING: Gdk.Cursor.new_from_name(Gdk.Display.get_default(), 'pointer'), self.ToolState.GRID: Gdk.Cursor.new_from_name(Gdk.Display.get_default(), 'cell'), self.ToolState.MEASUREMENT: Gdk.Cursor.new_from_name(Gdk.Display.get_default(), 'crosshair'), } self.ruler_box = numpy.array([[0, 0], [0, 0]]) self.widget = widget self.beamline = Registry.get_utility(IBeamline) self.camera = self.beamline.sample_video self.centering = centering.Centering() self.setup() Registry.add_utility(IMicroscope, self) self.load_from_cache() def setup(self): # zoom low, med, high = self.beamline.config['zoom_levels'] self.widget.microscope_zoomout_btn.connect('clicked', self.on_zoom, low) self.widget.microscope_zoom100_btn.connect('clicked', self.on_zoom, med) self.widget.microscope_zoomin_btn.connect('clicked', self.on_zoom, high) # rotate sample self.widget.microscope_ccw90_btn.connect('clicked', self.on_rotate, -90) self.widget.microscope_cw90_btn.connect('clicked', self.on_rotate, 90) self.widget.microscope_rot180_btn.connect('clicked', self.on_rotate, 180) # centering self.widget.microscope_loop_btn.connect('clicked', self.on_auto_center, 'loop') self.widget.microscope_capillary_btn.connect('clicked', self.on_auto_center, 'capillary') self.widget.microscope_diff_btn.connect('clicked', self.on_auto_center, 'diffraction') self.widget.microscope_ai_btn.connect('clicked', self.on_auto_center, 'external') self.beamline.manager.connect('mode', self.on_gonio_mode) self.beamline.goniometer.stage.connect('changed', self.update_grid) self.beamline.sample_zoom.connect('changed', self.update_grid) self.beamline.aperture.connect('changed', self.on_aperture) # Video Area self.video = VideoWidget(self.camera) self.beamline.camera_scale.connect('changed', self.on_camera_scale) self.widget.microscope_video_frame.add(self.video) # status, save, etc self.widget.microscope_save_btn.connect('clicked', self.on_save) self.widget.microscope_grid_btn.connect('toggled', self.toggle_grid_mode) self.widget.microscope_colorize_tbtn.connect('toggled', self.colorize) self.widget.microscope_point_btn.connect('clicked', self.on_save_point) self.widget.microscope_clear_btn.connect('clicked', self.clear_objects) # disable centering buttons on click self.centering.connect('started', self.on_scripts_started) self.centering.connect('done', self.on_scripts_done) aicenter = Registry.get_utility(ICenter) self.widget.microscope_ai_btn.set_sensitive(False) if aicenter: aicenter.connect( 'active', lambda obj, state: self.widget.microscope_ai_btn. set_sensitive(state)) # lighting monitors self.monitors = [] for key in ['backlight', 'frontlight', 'uvlight']: light = getattr(self.beamline, 'sample_{}'.format(key), None) scale = getattr(self.widget, 'microscope_{}_scale'.format(key), None) box = getattr(self.widget, '{}_box'.format(key), None) if all([light, scale, box]): scale.set_adjustment( Gtk.Adjustment(0, 0.0, 100.0, 1.0, 1.0, 10)) self.monitors.append(common.ScaleMonitor(scale, light), ) box.set_sensitive(True) else: box.destroy() if key == 'uvlight': color = Gdk.RGBA() color.parse("#9B59B6") box.override_color(Gtk.StateFlags.NORMAL, color) self.video.connect('motion-notify-event', self.on_mouse_motion) self.video.connect('scroll-event', self.on_mouse_scroll) self.video.connect('button-press-event', self.on_mouse_press) self.video.connect('button-release-event', self.on_mouse_release) self.video.set_overlay_func(self.overlay_function) self.video.connect('configure-event', self.setup_grid) self.scripts = get_scripts() # Connect Grid signals self.connect('notify::grid-xyz', self.update_grid) self.connect('notify::tool', self.on_tool_changed) def change_tool(self, tool=None): if tool is None: self.props.tool, self.prev_tool = self.prev_tool, self.tool elif self.props.tool != tool: self.props.tool, self.prev_tool = tool, self.tool def setup_grid(self, *args, **kwargs): if not self.video_ready: self.video_ready = True for param in ['grid-xyz', 'points', 'grid-params']: self.connect('notify::{}'.format(param), self.save_to_cache) self.update_grid() def save_to_cache(self, *args, **kwargs): cache = { 'points': self.props.points, 'grid-xyz': None if self.props.grid_xyz is None else self.props.grid_xyz.tolist(), 'grid-params': self.props.grid_params, 'grid-scores': self.props.grid_scores, 'grid-state': self.props.grid_state, } save_cache(cache, 'microscope') def load_from_cache(self): cache = load_cache('microscope') if cache and isinstance(cache, dict): for name, value in list(cache.items()): if name == 'grid-xyz': value = None if not isinstance( value, list) else numpy.array(value) if name == 'points': value = [tuple(point) for point in value] self.set_property(name, value) def save_image(self, filename): self.video.save_image(filename) def draw_beam(self, cr): radius = 0.5e-3 * self.beamline.aperture.get() / self.video.mm_scale() tick_in = radius * 0.8 tick_out = radius * 1.2 center = numpy.array(self.video.get_size()) / 2 cr.set_source_rgba(1.0, 0.25, 0.0, 0.5) cr.set_line_width(2.0) # beam circle cr.arc(center[0], center[1], radius, 0, 2.0 * 3.14) cr.stroke() # beam target ticks cr.move_to(center[0], center[1] - tick_in) cr.line_to(center[0], center[1] - tick_out) cr.stroke() cr.move_to(center[0], center[1] + tick_in) cr.line_to(center[0], center[1] + tick_out) cr.stroke() cr.move_to(center[0] - tick_in, center[1]) cr.line_to(center[0] - tick_out, center[1]) cr.stroke() cr.move_to(center[0] + tick_in, center[1]) cr.line_to(center[0] + tick_out, center[1]) cr.stroke() def draw_measurement(self, cr): if self.tool == self.ToolState.MEASUREMENT: cr.set_font_size(10) (x1, y1), (x2, y2) = self.ruler_box dist = 1000 * self.video.mm_scale() * math.sqrt((x2 - x1)**2.0 + (y2 - y1)**2.0) cr.set_source_rgba(0.0, 0.5, 1.0, 1.0) cr.set_line_width(1.0) cr.move_to(x1, y1) cr.line_to(x2, y2) cr.stroke() label = '{:0.0f} µm'.format(dist) xb, yb, w, h = cr.text_extents(label)[:4] cr.move_to(x1 - w * (int(x2 > x1) + xb / w), y1 - h * (int(y2 > y1) + yb / h)) cr.show_text(label) def draw_bbox(self, cr): if self.tool == self.ToolState.GRID and len(self.props.grid_bbox): cr.set_font_size(10) cr.set_line_width(1.0) cr.set_source_rgba(0.0, 0.5, 1.0, 1.0) # rectangle (x1, y1), (x2, y2) = self.props.grid_bbox cr.rectangle(x1, y1, x2 - x1, y2 - y1) cr.stroke() # center cx = (x1 + x2) / 2 cy = (y1 + y2) / 2 # width width = 1000 * self.video.mm_scale() * abs(x2 - x1) w_label = '{:0.0f} µm'.format(width) xb, yb, w, h = cr.text_extents(w_label)[:4] cr.move_to(cx - w / 2, y1 - h / 2) cr.show_text(w_label) # height height = 1000 * self.video.mm_scale() * abs(y2 - y1) h_label = '{:0.0f} µm'.format(height) cr.move_to(x2 + h / 2, cy) cr.show_text(h_label) def draw_grid(self, cr): if self.props.grid is not None: radius = 0.5e-3 * self.beamline.aperture.get( ) / self.video.mm_scale() cr.set_line_width(1.0) cr.set_font_size(8) for i, (x, y, z) in enumerate(self.props.grid): if i + 1 in self.props.grid_scores: col = self.props.grid_cmap.rgba_values( self.props.grid_scores[i + 1], alpha=0.5) cr.set_source_rgba(*col) cr.arc(x, y, radius, 0, 2.0 * 3.14) cr.fill() cr.set_source_rgba(0.0, 0.5, 1.0, 1.0) cr.arc(x, y, radius, 0, 2.0 * 3.14) cr.stroke() name = '{}'.format(i + 1) xb, yb, w, h = cr.text_extents(name)[:4] cr.move_to(x - w / 2. - xb, y - h / 2. - yb) cr.show_text(name) cr.stroke() def draw_points(self, cr): if self.props.points: cr.save() mm_scale = self.video.mm_scale() radius = 0.5e-3 * self.beamline.aperture.get() / (16 * mm_scale) cur_point = numpy.array(self.beamline.goniometer.stage.get_xyz()) center = numpy.array(self.video.get_size()) / 2 points = numpy.array(self.props.points) - cur_point xyz = numpy.zeros_like(points) xyz[:, 0], xyz[:, 1], xyz[:, 2] = self.beamline.goniometer.stage.xyz_to_screen( points[:, 0], points[:, 1], points[:, 2]) xyz /= mm_scale radii = (4.0 - (xyz[:, 2] / (center[1] * 0.25))) * radius xyz[:, :2] += center cr.set_source_rgba(1.0, 0.25, 0.75, 0.5) for i, (x, y, z) in enumerate(xyz): cr.arc(x, y, radii[i], 0, 2.0 * 3.14) cr.fill() cr.move_to(x + 6, y) cr.show_text('P{}'.format(i + 1)) cr.stroke() cr.restore() def clear_objects(self, *args, **kwargs): self.props.grid = None self.props.grid_xyz = None self.props.grid_scores = {} self.props.points = [] self.props.grid_bbox = [] if self.tool == self.ToolState.GRID: self.change_tool(self.ToolState.CENTERING) self.widget.microscope_grid_btn.set_active(False) self.queue_overlay() def toggle_grid_mode(self, *args, **kwargs): if self.widget.microscope_grid_btn.get_active(): self.change_tool(self.ToolState.GRID) self.props.grid_bbox = [] else: self.widget.microscope_grid_btn.set_active(False) self.change_tool() self.queue_overlay() def auto_grid(self, *args, **kwargs): img = self.camera.get_frame() polygon = imgproc.find_profile(img, scale=0.25) points = numpy.array(polygon) self.props.grid_bbox = numpy.array( [points.min(axis=0), points.max(axis=0)]) def add_point(self, point): self.props.points = self.props.points + [point] self.queue_overlay() def make_grid(self, bbox=None, points=None, scaled=True, center=True): if points is not None: points = numpy.array(points) bbox = numpy.array([points.min(axis=0), points.max(axis=0)]) elif bbox is None: bbox = self.props.grid_bbox if not isinstance(bbox, numpy.ndarray): bbox = numpy.array(bbox) factor = 1.0 if scaled else self.video.scale step_size = 1e-3 * self.beamline.aperture.get() / self.video.mm_scale() w, h = 1000 * numpy.abs( numpy.diff(bbox, axis=0).ravel() * self.video.mm_scale()).round(4) # grid too small exit and clear if max(w, h) < 2 * step_size: self.props.grid_bbox = [] self.queue_overlay() self.props.grid_params = {} return bounds = bbox * factor grid = misc.grid_from_bounds(bounds, step_size, tight=False) dx, dy = self.video.screen_to_mm(*bounds.mean(axis=0))[2:] angle = self.beamline.goniometer.omega.get_position() ox, oy, oz = self.beamline.goniometer.stage.get_xyz() xmm, ymm = self.video.screen_to_mm(grid[:, 0], grid[:, 1])[2:] gx, gy, gz = self.beamline.goniometer.stage.xvw_to_xyz( -xmm, -ymm, numpy.radians(angle)) grid_xyz = numpy.dstack([gx + ox, gy + oy, gz + oz])[0] properties = { 'grid_state': self.GridState.PENDING, 'grid_xyz': grid_xyz.round(4), 'grid_bbox': [], 'grid_params': { 'width': w, 'height': h, 'angle': angle, }, 'grid_scores': {} } for k, v in properties.items(): self.set_property(k, v) if center: self.beamline.goniometer.stage.move_screen_by(-dx, -dy, 0.0) def add_grid_score(self, position, score): self.props.grid_scores[position] = score self.props.grid_cmap.autoscale(list(self.props.grid_scores.values())) self.props.grid_state = self.GridState.COMPLETE def load_grid(self, grid_xyz, params, scores): self.props.grid_xyz = grid_xyz self.props.grid_scores = scores self.props.grid_params = params self.props.grid_state = self.GridState.COMPLETE self.props.grid_cmap.autoscale(list(self.props.grid_scores.values())) @async_call def center_pixel(self, x, y, force=False): if self.tool == self.ToolState.CENTERING or force: ix, iy, xmm, ymm = self.video.screen_to_mm(x, y) if not self.beamline.goniometer.stage.is_busy(): self.beamline.goniometer.stage.move_screen_by(-xmm, -ymm, 0.0) def create_overlay_surface(self): self.overlay_surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, self.video.display_width, self.video.display_height) self.overlay_ctx = cairo.Context(self.overlay_surface) def overlay_function(self, cr): self.update_overlay() cr.set_source_surface(self.overlay_surface, 0, 0) cr.paint() def queue_overlay(self): self.overlay_dirty = True def update_overlay(self): if self.overlay_dirty or self.overlay_surface is None: self.create_overlay_surface() self.draw_beam(self.overlay_ctx) self.draw_grid(self.overlay_ctx) self.draw_bbox(self.overlay_ctx) self.draw_points(self.overlay_ctx) self.draw_measurement(self.overlay_ctx) self.overlay_dirty = False # callbacks def update_grid(self, *args, **kwargs): if self.props.grid_xyz is not None: center = numpy.array(self.video.get_size()) * 0.5 points = self.grid_xyz - numpy.array( self.beamline.goniometer.stage.get_xyz()) xyz = numpy.empty_like(points) xyz[:, 0], xyz[:, 1], xyz[:, 2] = self.beamline.goniometer.stage.xyz_to_screen( points[:, 0], points[:, 1], points[:, 2]) xyz /= self.video.mm_scale() xyz[:, :2] += center self.props.grid = xyz else: self.props.grid = None self.queue_overlay() def colorize(self, button): self.video.set_colorize(state=button.get_active()) def on_save_point(self, *args, **kwargs): self.add_point(self.beamline.goniometer.stage.get_xyz()) self.save_to_cache() def on_tool_changed(self, *args, **kwargs): window = self.widget.microscope_bkg.get_window() if window: window.set_cursor(self.tool_cursors[self.props.tool]) def on_camera_scale(self, obj, value): self.queue_overlay() self.video.set_pixel_size(value) def on_gonio_mode(self, obj, mode): self.props.mode = mode centering_tool = self.mode.name in ['CENTER', 'ALIGN'] self.widget.microscope_centering_box.set_sensitive(centering_tool) self.widget.microscope_grid_box.set_sensitive(centering_tool) if centering_tool: self.change_tool(self.ToolState.CENTERING) else: self.change_tool(self.ToolState.DEFAULT) if self.mode.name == 'ALIGN': self.widget.microscope_colorize_tbtn.set_active(True) elif self.mode.name not in ['BUSY', 'UNKNOWN']: self.widget.microscope_colorize_tbtn.set_active(False) def on_scripts_started(self, obj, event=None): self.widget.microscope_toolbar.set_sensitive(False) def on_scripts_done(self, obj, event=None): self.widget.microscope_toolbar.set_sensitive(True) def on_save(self, obj=None, arg=None): img_filename, _ = dialogs.select_save_file('Save Video Snapshot', formats=[ ('PNG Image', 'png'), ('JPEG Image', 'jpg') ]) if not img_filename: return if os.access(os.path.split(img_filename)[0], os.W_OK): self.save_image(img_filename) def on_auto_center(self, widget, method='loop'): self.centering.configure(method=method) self.centering.start() return True def on_zoom(self, widget, position): self.camera.zoom(position) def on_aperture(self, obj, value): if self.grid_xyz is not None: center = numpy.array(self.video.get_size()) * 0.5 points = self.grid_xyz - numpy.array( self.beamline.goniometer.stage.get_xyz()) xyz = numpy.empty_like(points) xyz[:, 0], xyz[:, 1], xyz[:, 2] = self.beamline.goniometer.stage.xyz_to_screen( points[:, 0], points[:, 1], points[:, 2]) xyz /= self.video.mm_scale() xyz[:, :2] += center self.make_grid(points=xyz[:, :2], center=False) else: self.queue_overlay() def on_rotate(self, widget, angle): cur_omega = int(self.beamline.goniometer.omega.get_position()) target = (cur_omega + angle) target = (target > 360) and (target % 360) or target self.beamline.goniometer.omega.move_to(target) def on_mouse_scroll(self, widget, event): if 'GDK_CONTROL_MASK' in event.get_state( ).value_names and self.mode.name in ['CENTER', 'ALIGN']: if event.direction == Gdk.ScrollDirection.UP: self.on_rotate(widget, 45) elif event.direction == Gdk.ScrollDirection.DOWN: self.on_rotate(widget, -45) def on_mouse_motion(self, widget, event): if event.is_hint: _, x, y, state = event.window.get_pointer() else: x, y = event.x, event.y ix, iy, xmm, ymm = self.video.screen_to_mm(x, y) self.widget.microscope_pos_lbl.set_markup( f"<small><tt>X:{ix:5.0f} {xmm:6.3f} mm\nY:{iy:5.0f} {ymm:6.3f} mm</tt></small>" ) if Gdk.ModifierType.BUTTON2_MASK & event.state: self.ruler_box[1] = (x, y) self.queue_overlay() elif Gdk.ModifierType.CONTROL_MASK & event.state and self.mode.name in [ 'COLLECT' ]: self.change_tool(tool=self.ToolState.CENTERING) elif self.tool == self.ToolState.GRID and len(self.props.grid_bbox): if Gdk.ModifierType.BUTTON1_MASK & event.state: self.props.grid_bbox[-1] = (x, y) self.queue_overlay() elif self.tool == self.ToolState.MEASUREMENT: self.change_tool() self.queue_overlay() elif self.tool == self.ToolState.CENTERING and self.mode.name in [ 'COLLECT' ]: self.change_tool() def on_mouse_press(self, widget, event): if event.button == 1: if self.tool == self.ToolState.GRID: self.props.grid_bbox = [(event.x, event.y), (event.x, event.y)] self.queue_overlay() else: self.center_pixel(event.x, event.y) elif event.button == 2: self.change_tool(self.ToolState.MEASUREMENT) self.ruler_box[0] = (event.x, event.y) self.ruler_box[1] = (event.x, event.y) self.queue_overlay() def on_mouse_release(self, widget, event): if event.button == 1: if self.tool == self.ToolState.GRID and self.grid_bbox: self.make_grid() self.widget.microscope_grid_btn.set_active(False)
class RunItem(Object): class StateType: (ADD, DRAFT, ACTIVE, PAUSED, ERROR, COMPLETE) = range(6) state = Property(type=int, default=StateType.DRAFT) position = Property(type=int, default=0) size = Property(type=int, default=0) info = Property(type=object) uuid = Property(type=str, default="") progress = Property(type=float, default=0.0) warning = Property(type=str, default="") title = Property(type=str, default="Add run ...") subtitle = Property(type=str, default="") created = Property(type=float, default=0.0) def __init__(self, info=None, state=StateType.DRAFT, uid=None, created=None): super().__init__() self.connect('notify::info', self.info_changed) self.props.created = created if created else time.time() self.props.uuid = uid if uid else str(uuid.uuid4()) self.props.state = state self.props.info = info def info_changed(self, *args, **kwargs): if self.props.info: self.props.size = datatools.count_frames(self.props.info) self.props.title = '{}_{:04d}, ...'.format(self.info['name'], self.info['first']) self.props.subtitle = '{}f {:0.4g}°/{:0.2g}s @ {:0.5g} keV {}'.format( self.props.size, self.props.info.get('delta'), self.props.info.get('exposure'), self.props.info.get('energy'), '[INV]' if self.props.info.get('inverse') else '' ) def set_progress(self, progress): state = self.props.state if state == RunItem.StateType.ADD: return False self.props.progress = progress if progress >= 0.95: self.props.state = RunItem.StateType.COMPLETE return state != self.props.state # return True if state changed @staticmethod def sorter(a_pointer, b_pointer): # if objects correctly translated do not translate again if isinstance(a_pointer, RunItem): a = a_pointer b = b_pointer else: a = glibref.capi.to_object(a_pointer) b = glibref.capi.to_object(b_pointer) if a.props.state == b.props.state == RunItem.StateType.ADD: return 0 elif a.props.state == RunItem.StateType.ADD: return 1 elif b.props.state == RunItem.StateType.ADD: return -1 else: if a.props.created > b.props.created: return 1 elif a.props.created < b.props.created: return -1 else: return 0 def get_color(self): return Gdk.RGBA(*STATE_COLORS[self.state]) def __getitem__(self, item): if self.props.info: return self.props.info[item] def __str__(self): return '<Run Item: {} - {}|{}>'.format(self.props.position, self.props.title, self.props.subtitle)