def __init__(self, name, stage=None, focus0=None, focus1=None, stream_classes=None): """ :param name (string): user-friendly name of the view :param stage (Actuator): actuator with two axes: x and y :param focus0 (Actuator): actuator with one axis: z. Can be None :param focus1 (Actuator): actuator with one axis: z. Can be None Focuses 0 and 1 are modified when changing focus respectively along the X and Y axis. :param stream_classes (None, or tuple of classes): all subclasses that the streams in this view is allowed to show. """ super(MicroscopeView, self).__init__(name) if stream_classes is None: self.stream_classes = (Stream,) else: self.stream_classes = stream_classes self._stage = stage self._focus = [focus0, focus1] # The real stage position, to be modified via moveStageToView() # it's a direct access from the stage, so looks like a dict of axes if stage: self.stage_pos = stage.position # stage.position.subscribe(self.onStagePos) # the current center of the view, which might be different from # the stage # TODO: we might need to have it on the MicroscopeModel, if all the # viewports must display the same location pos = self.stage_pos.value view_pos_init = (pos["x"], pos["y"]) else: view_pos_init = (0, 0) self.view_pos = model.ListVA(view_pos_init, unit="m") # current density (meter per pixel, ~ scale/zoom level) # 10µm/px => ~large view of the sample self.mpp = FloatContinuous(10e-6, range=(10e-12, 1e-3), unit="m/px") # How much one image is displayed on the other one. Value used by # StreamTree self.merge_ratio = FloatContinuous(0.3, range=[0, 1], unit="") self.merge_ratio.subscribe(self._onMergeRatio) # Streams to display (can be considered an implementation detail in most # cases) # Note: use addStream/removeStream for simple modifications self.stream_tree = StreamTree(merge=self.merge_ratio.value) # Only modify with this lock acquired: # TODO: Is this the source of the intermittent locking of the GUI when # Streams are active? If so, is there another/better way? self._streams_lock = threading.Lock() # TODO: list of annotations to display self.show_crosshair = model.BooleanVA(True)
def test_reshape(self): darray = generate_img_data(100, 100, 4, 100) self.app.test_frame.SetSize((500, 500)) self.app.test_frame.Center() self.app.test_frame.Layout() tab = self.create_simple_tab_model() mpp = FloatContinuous(2, range=(0, 1), unit="m/px") tab.focussedView.value.mpp = mpp view = tab.focussedView.value cnvs = miccanvas.DblMicroscopeCanvas(self.panel) cnvs.setView(view, tab)
def xtest_reshape(self): darray = generate_img_data(100, 100, 4, 100) self.app.test_frame.SetSize((500, 500)) self.app.test_frame.Center() self.app.test_frame.Layout() mmodel = test.FakeMicroscopeModel() mpp = FloatContinuous(2, range=(0, 1), unit="m/px") mmodel.focussedView.value.mpp = mpp view = mmodel.focussedView.value canvas = miccanvas.DblMicroscopeCanvas(self.panel) canvas.setView(view, mmodel)
def test_nanana(self): self.app.test_frame.SetSize((500, 500)) self.app.test_frame.Center() self.app.test_frame.Layout() # old_canvas = DraggableCanvas(self.panel) tab = self.create_simple_tab_model() mpp = FloatContinuous(10e-6, range=(1e-3, 1), unit="m/px") tab.focussedView.value.mpp = mpp view = tab.focussedView.value canvas = miccanvas.DblMicroscopeCanvas(self.panel) shape = (5, 5, 4) rgb = numpy.empty(shape, dtype=numpy.uint8) rgb[::2, ...] = [ [255, 0, 0, 255], [0, 255, 0, 255], [255, 255, 0, 255], [255, 0, 255, 255], [0, 0, 255, 255] ][:shape[1]] rgb[1::2, ...] = [ [127, 0, 0, 255], [0, 127, 0, 255], [127, 127, 0, 255], [127, 0, 127, 255], [0, 0, 127, 255] ][:shape[1]] rgb[..., [0, 1, 2, 3]] = rgb[..., [2, 1, 0, 3]] darray = DataArray(rgb) canvas.setView(view, tab) self.add_control(canvas, flags=wx.EXPAND, proportion=1) test.gui_loop() # Set the mpp again, because the on_size handler will have recalculated it view.mpp.value = 1 images = [(darray, (0.0, 0.0), (2, 2), True, None, None, None, None, "nanana")] canvas.set_images(images) canvas.scale = 1 canvas.update_drawing() test.gui_loop(0.1)
def __init__(self, main): assert main.microscope is not None MicroscopyGUIData.__init__(self, main) # Step size name -> val, range, actuator, axes (None if all) # str -> float, [float, float], str, (str, ...) ss_def = { "stage": (1e-6, [100e-9, 1e-3], "stage", None), # "focus": (100e-9, [10e-9, 1e-4], "focus", None), "aligner": (1e-6, [100e-9, 1e-4], "aligner", None), # Mirror is a bit more complicated as it has 4 axes and Y # usually needs to be 10x bigger than X "mirror_x": (1e-6, [100e-9, 1e-3], "mirror", ("x", )), "mirror_y": (10e-6, [100e-9, 1e-3], "mirror", ("y", )), "mirror_r": (10e-6, [100e-9, 1e-3], "mirror", ("ry", "rz")) } # str -> VA: name (as the name of the attribute) -> step size (m) self.stepsizes = {} # This allow the UI code to mention axes only as role/axis name. # str -> (str, str): # role/axis ("mirror/x") -> (actuator ("mirror"), stepsize ("mirror_r")) self._axis_to_act_ss = {} # remove the ones that don't have an actuator for ss, (v, r, an, axn) in ss_def.items(): if getattr(main, an) is not None: self.stepsizes[ss] = FloatContinuous(v, r) if axn is None: axn = getattr(main, an).axes for a in axn: self._axis_to_act_ss[(an, a)] = (an, ss) logging.debug("Add axis %s/%s to stepsize %s", an, a, ss) # set of (str, str): actuator name, axis name self.axes = frozenset(self._axis_to_act_ss.keys()) # Tools are for lens alignment (mirror alignment actually needs none) tools = {TOOL_NONE, TOOL_DICHO, TOOL_SPOT} self.tool = IntEnumerated(TOOL_NONE, choices=tools) # For dichotomic mode self.dicho_seq = model.ListVA() # list of 4 enumerated for each corner
def __init__(self, name, role, port, sensor_input='b', output_channel=2, **kwargs): """ A driver for the Lakeshore 310 temperature controller. name: (str) role: (str) port: (str) port name. Can be a pattern, in which case all the ports fitting the pattern will be tried. Use /dev/fake for a simulator sensor_input (str): The sensor input to use, typically 'a' or 'b' output_channel: (int): The channel output to control, typically 1 or 2 """ # Connect to serial port self._ser_access = threading.Lock() self._serial = None self._file = None # Check input and output channels self._sensor_input = sensor_input.upper() if not self._sensor_input in ('A', 'B'): raise ValueError("Sensor input must be either 'A' or 'B'") self._output_channel = output_channel if not self._output_channel in (1, 2): raise ValueError( "Invalid output channel. Should be an int of 1 or 2") self._port = self._findDevice(port) # sets ._serial and ._file logging.info("Found Lakeshore 335 device on port %s", self._port) manufacturer, md, serialn, firmware = self.GetIdentifier() self._hwVersion = "%s %s S/N: %s" % (manufacturer, md, serialn) self._swVersion = "Firmware: %s" % (firmware, ) # Clear errors at start try: self.checkError() except LakeshoreError as ex: logging.warning("Discarding initial error status: %s", ex) # Vigilant attributes of the controller. self.temperature = FloatVA(unit=u"°C", value=self.GetSensorTemperature(), readonly=True) self.targetTemperature = FloatContinuous( value=self.GetSetpoint(), unit=u"°C", range=[-273, 50], setter=self._set_targetTemperature) self.heating = IntEnumerated(value=self.GetHeaterRange(), choices={ 0: "Off", 1: "Low", 2: "Medium", 3: "High" }, setter=self._set_heating) self._poll_timer = RepeatingTimer(POLL_INTERVAL, self._poll, "Lakeshore temperature update") self._poll_timer.start() # lock the keypad self.LockKeypad(True)
def __init__(self, name, stage=None, focus0=None, focus1=None, stream_classes=None): """ :param name (string): user-friendly name of the view :param stage (Actuator): actuator with two axes: x and y :param focus0 (Actuator): actuator with one axis: z. Can be None :param focus1 (Actuator): actuator with one axis: z. Can be None Focuses 0 and 1 are modified when changing focus respectively along the X and Y axis. :param stream_classes (None, or tuple of classes): all subclasses that the streams in this view is allowed to show. """ super(MicroscopeView, self).__init__(name) if stream_classes is None: self.stream_classes = (Stream, ) else: self.stream_classes = stream_classes self._stage = stage self._focus = [focus0, focus1] # The real stage position, to be modified via moveStageToView() # it's a direct access from the stage, so looks like a dict of axes if stage: self.stage_pos = stage.position # stage.position.subscribe(self.onStagePos) # the current center of the view, which might be different from # the stage # TODO: we might need to have it on the MicroscopeModel, if all the # viewports must display the same location pos = self.stage_pos.value view_pos_init = (pos["x"], pos["y"]) else: view_pos_init = (0, 0) self.view_pos = model.ListVA(view_pos_init, unit="m") # current density (meter per pixel, ~ scale/zoom level) # 10µm/px => ~large view of the sample self.mpp = FloatContinuous(10e-6, range=(10e-12, 1e-3), unit="m/px") # How much one image is displayed on the other one. Value used by # StreamTree self.merge_ratio = FloatContinuous(0.3, range=[0, 1], unit="") self.merge_ratio.subscribe(self._onMergeRatio) # Streams to display (can be considered an implementation detail in most # cases) # Note: use addStream/removeStream for simple modifications self.stream_tree = StreamTree(merge=self.merge_ratio.value) # Only modify with this lock acquired: # TODO: Is this the source of the intermittent locking of the GUI when # Streams are active? If so, is there another/better way? self._streams_lock = threading.Lock() # TODO: list of annotations to display self.show_crosshair = model.BooleanVA(True)
class MicroscopeView(View): """ Represents a view from a microscope and ways to alter it. Basically, its "input" is a StreamTree and it can request stage and focus move. It never computes the composited image from all the streams itself. It's up to other objects (e.g., the canvas) to ask the StreamTree for its latest image (the main goal of this scheme is to avoid computation when not needed). Similarly, the thumbnail is never automatically recomputed, but other objects can update it. """ def __init__(self, name, stage=None, focus0=None, focus1=None, stream_classes=None): """ :param name (string): user-friendly name of the view :param stage (Actuator): actuator with two axes: x and y :param focus0 (Actuator): actuator with one axis: z. Can be None :param focus1 (Actuator): actuator with one axis: z. Can be None Focuses 0 and 1 are modified when changing focus respectively along the X and Y axis. :param stream_classes (None, or tuple of classes): all subclasses that the streams in this view is allowed to show. """ super(MicroscopeView, self).__init__(name) if stream_classes is None: self.stream_classes = (Stream, ) else: self.stream_classes = stream_classes self._stage = stage self._focus = [focus0, focus1] # The real stage position, to be modified via moveStageToView() # it's a direct access from the stage, so looks like a dict of axes if stage: self.stage_pos = stage.position # stage.position.subscribe(self.onStagePos) # the current center of the view, which might be different from # the stage # TODO: we might need to have it on the MicroscopeModel, if all the # viewports must display the same location pos = self.stage_pos.value view_pos_init = (pos["x"], pos["y"]) else: view_pos_init = (0, 0) self.view_pos = model.ListVA(view_pos_init, unit="m") # current density (meter per pixel, ~ scale/zoom level) # 10µm/px => ~large view of the sample self.mpp = FloatContinuous(10e-6, range=(10e-12, 1e-3), unit="m/px") # How much one image is displayed on the other one. Value used by # StreamTree self.merge_ratio = FloatContinuous(0.3, range=[0, 1], unit="") self.merge_ratio.subscribe(self._onMergeRatio) # Streams to display (can be considered an implementation detail in most # cases) # Note: use addStream/removeStream for simple modifications self.stream_tree = StreamTree(merge=self.merge_ratio.value) # Only modify with this lock acquired: # TODO: Is this the source of the intermittent locking of the GUI when # Streams are active? If so, is there another/better way? self._streams_lock = threading.Lock() # TODO: list of annotations to display self.show_crosshair = model.BooleanVA(True) def get_focus(self, i): return self._focus[i] def get_focus_count(self): """ Get the number of available focus actuators """ return len([a for a in self._focus if a]) def has_stage(self): return self._stage is not None def moveStageToView(self): """ Move the stage to the current view_pos :return (None or Future): a future (that allows to know when the move is finished) Note: once the move is finished stage_pos will be updated (by the back-end) """ if not self._stage: return # TODO: a way to know if it can do absolute move? => .capabilities! # if hasattr(self.stage, "moveAbs"): # # absolute # move = {"x": pos[0], "y": pos[1]} # self._stage.moveAbs(move) # else: view_pos = self.view_pos.value # relative prev_pos = self.stage_pos.value move = { "x": view_pos[0] - prev_pos["x"], "y": view_pos[1] - prev_pos["y"] } if abs(move["x"]) < 1e-12 and abs(move["y"]) < 1e-12: logging.debug("skipping move request of 0") return # Check it makes sense (=> not too big) distance = math.sqrt(sum([v**2 for v in move.values()])) if distance > MAX_SAFE_MOVE_DISTANCE: logging.error( "Cancelling request to move by %f m (because > %f m)", distance, MAX_SAFE_MOVE_DISTANCE) return logging.debug("Sending move request of %s", move) return self._stage.moveRel(move) # def onStagePos(self, pos): # # we want to recenter the viewports whenever the stage moves # # Not sure whether that's really the right way to do it though... # # TODO: avoid it to move the view when the user is dragging the view # # => might require cleverness # # self.view_pos = model.ListVA((pos["x"], pos["y"]), unit="m") def getStreams(self): """ :return: [Stream] list of streams that are displayed in the view Do not modify directly, use addStream(), and removeStream(). Note: use .stream_tree for getting the raw StreamTree """ return self.stream_tree.getStreams() def addStream(self, stream): """ Add a stream to the view. It takes care of updating the StreamTree according to the type of stream. stream (acq.stream.Stream): stream to add If the stream is already present, nothing happens """ # check if the stream is already present if stream in self.stream_tree.getStreams(): return if not isinstance(stream, self.stream_classes): msg = "Adding incompatible stream '%s' to view '%s'. %s needed" logging.warning(msg, stream.name.value, self.name.value, self.stream_classes) # Find out where the stream should go in the streamTree # FIXME: manage sub-trees, with different merge operations # For now we just add it to the list of streams, with the only merge # operation possible with self._streams_lock: self.stream_tree.add_stream(stream) # subscribe to the stream's image if hasattr(stream, "image"): stream.image.subscribe(self._onNewImage) # if the stream already has an image, update now if stream.image.value is not None: self._onNewImage(stream.image.value) else: logging.debug("No image found for stream %s", type(stream)) def removeStream(self, stream): """ Remove a stream from the view. It takes care of updating the StreamTree. stream (Stream): stream to remove If the stream is not present, nothing happens """ # Stop listening to the stream changes if hasattr(stream, "image"): stream.image.unsubscribe(self._onNewImage) with self._streams_lock: # check if the stream is already removed if not stream in self.stream_tree.getStreams(): return # remove stream from the StreamTree() # TODO handle more complex trees self.stream_tree.remove_stream(stream) # let everyone know that the view has changed self.lastUpdate.value = time.time() def _onNewImage(self, im): """ Called when one stream has its image updated im (DataArray) """ # just let everyone know that the composited image has changed self.lastUpdate.value = time.time() def _onMergeRatio(self, ratio): """ Called when the merge ratio is modified """ # This actually modifies the root operator of the stream tree # It has effect only if the operator can do something with the "merge" # argument with self._streams_lock: self.stream_tree.kwargs["merge"] = ratio # just let everyone that the composited image has changed self.lastUpdate.value = time.time() def is_compatible(self, stream_cls): """ Check if the given stream class is compatible with this view. """ return issubclass(stream_cls, self.stream_classes)
class MicroscopeView(View): """ Represents a view from a microscope and ways to alter it. Basically, its "input" is a StreamTree and it can request stage and focus move. It never computes the composited image from all the streams itself. It's up to other objects (e.g., the canvas) to ask the StreamTree for its latest image (the main goal of this scheme is to avoid computation when not needed). Similarly, the thumbnail is never automatically recomputed, but other objects can update it. """ def __init__(self, name, stage=None, focus0=None, focus1=None, stream_classes=None): """ :param name (string): user-friendly name of the view :param stage (Actuator): actuator with two axes: x and y :param focus0 (Actuator): actuator with one axis: z. Can be None :param focus1 (Actuator): actuator with one axis: z. Can be None Focuses 0 and 1 are modified when changing focus respectively along the X and Y axis. :param stream_classes (None, or tuple of classes): all subclasses that the streams in this view is allowed to show. """ super(MicroscopeView, self).__init__(name) if stream_classes is None: self.stream_classes = (Stream,) else: self.stream_classes = stream_classes self._stage = stage self._focus = [focus0, focus1] # The real stage position, to be modified via moveStageToView() # it's a direct access from the stage, so looks like a dict of axes if stage: self.stage_pos = stage.position # stage.position.subscribe(self.onStagePos) # the current center of the view, which might be different from # the stage # TODO: we might need to have it on the MicroscopeModel, if all the # viewports must display the same location pos = self.stage_pos.value view_pos_init = (pos["x"], pos["y"]) else: view_pos_init = (0, 0) self.view_pos = model.ListVA(view_pos_init, unit="m") # current density (meter per pixel, ~ scale/zoom level) # 10µm/px => ~large view of the sample self.mpp = FloatContinuous(10e-6, range=(10e-12, 1e-3), unit="m/px") # How much one image is displayed on the other one. Value used by # StreamTree self.merge_ratio = FloatContinuous(0.3, range=[0, 1], unit="") self.merge_ratio.subscribe(self._onMergeRatio) # Streams to display (can be considered an implementation detail in most # cases) # Note: use addStream/removeStream for simple modifications self.stream_tree = StreamTree(merge=self.merge_ratio.value) # Only modify with this lock acquired: # TODO: Is this the source of the intermittent locking of the GUI when # Streams are active? If so, is there another/better way? self._streams_lock = threading.Lock() # TODO: list of annotations to display self.show_crosshair = model.BooleanVA(True) def get_focus(self, i): return self._focus[i] def get_focus_count(self): """ Get the number of available focus actuators """ return len([a for a in self._focus if a]) def has_stage(self): return self._stage is not None def moveStageToView(self): """ Move the stage to the current view_pos :return (None or Future): a future (that allows to know when the move is finished) Note: once the move is finished stage_pos will be updated (by the back-end) """ if not self._stage: return # TODO: a way to know if it can do absolute move? => .capabilities! # if hasattr(self.stage, "moveAbs"): # # absolute # move = {"x": pos[0], "y": pos[1]} # self._stage.moveAbs(move) # else: view_pos = self.view_pos.value # relative prev_pos = self.stage_pos.value move = { "x": view_pos[0] - prev_pos["x"], "y": view_pos[1] - prev_pos["y"] } if abs(move["x"]) < 1e-12 and abs(move["y"]) < 1e-12: logging.debug("skipping move request of 0") return # Check it makes sense (=> not too big) distance = math.sqrt(sum([v ** 2 for v in move.values()])) if distance > MAX_SAFE_MOVE_DISTANCE: logging.error("Cancelling request to move by %f m (because > %f m)", distance, MAX_SAFE_MOVE_DISTANCE) return logging.debug("Sending move request of %s", move) return self._stage.moveRel(move) # def onStagePos(self, pos): # # we want to recenter the viewports whenever the stage moves # # Not sure whether that's really the right way to do it though... # # TODO: avoid it to move the view when the user is dragging the view # # => might require cleverness # # self.view_pos = model.ListVA((pos["x"], pos["y"]), unit="m") def getStreams(self): """ :return: [Stream] list of streams that are displayed in the view Do not modify directly, use addStream(), and removeStream(). Note: use .stream_tree for getting the raw StreamTree """ return self.stream_tree.getStreams() def addStream(self, stream): """ Add a stream to the view. It takes care of updating the StreamTree according to the type of stream. stream (acq.stream.Stream): stream to add If the stream is already present, nothing happens """ # check if the stream is already present if stream in self.stream_tree.getStreams(): return if not isinstance(stream, self.stream_classes): msg = "Adding incompatible stream '%s' to view '%s'. %s needed" logging.warning(msg, stream.name.value, self.name.value, self.stream_classes) # Find out where the stream should go in the streamTree # FIXME: manage sub-trees, with different merge operations # For now we just add it to the list of streams, with the only merge # operation possible with self._streams_lock: self.stream_tree.add_stream(stream) # subscribe to the stream's image if hasattr(stream, "image"): stream.image.subscribe(self._onNewImage) # if the stream already has an image, update now if stream.image.value is not None: self._onNewImage(stream.image.value) else: logging.debug("No image found for stream %s", type(stream)) def removeStream(self, stream): """ Remove a stream from the view. It takes care of updating the StreamTree. stream (Stream): stream to remove If the stream is not present, nothing happens """ # Stop listening to the stream changes if hasattr(stream, "image"): stream.image.unsubscribe(self._onNewImage) with self._streams_lock: # check if the stream is already removed if not stream in self.stream_tree.getStreams(): return # remove stream from the StreamTree() # TODO handle more complex trees self.stream_tree.remove_stream(stream) # let everyone know that the view has changed self.lastUpdate.value = time.time() def _onNewImage(self, im): """ Called when one stream has its image updated im (DataArray) """ # just let everyone know that the composited image has changed self.lastUpdate.value = time.time() def _onMergeRatio(self, ratio): """ Called when the merge ratio is modified """ # This actually modifies the root operator of the stream tree # It has effect only if the operator can do something with the "merge" # argument with self._streams_lock: self.stream_tree.kwargs["merge"] = ratio # just let everyone that the composited image has changed self.lastUpdate.value = time.time() def is_compatible(self, stream_cls): """ Check if the given stream class is compatible with this view. """ return issubclass(stream_cls, self.stream_classes)
def __init__(self, microscope): """ :param microscope: (model.Microscope or None): the root of the HwComponent tree provided by the back-end. If None, it means the interface is not connected to a microscope (and displays a recorded acquisition). """ self.microscope = microscope self.role = None # The following attributes are either HwComponents or None (if not available) self.ccd = None self.stage = None self.focus = None # actuator to change the camera focus self.aligner = None # actuator to align ebeam/ccd self.mirror = None # actuator to change the mirror position (on SPARC) self.light = None self.light_filter = None # emission light filter for SECOM/output filter for SPARC self.lens = None self.ebeam = None self.ebeam_focus = None # change the e-beam focus self.sed = None # secondary electron detector self.bsd = None # backscattered electron detector self.spectrometer = None # spectrometer self.spectrograph = None # actuator to change the wavelength self.ar_spec_sel = None # actuator to select AR/Spectrometer (SPARC) self.lens_switch = None # actuator to (de)activate the lens (SPARC) self.chamber = None # actuator to control the chamber (has vacuum, pumping etc.) self.chamber_ccd = None # view of inside the chamber self.chamber_light = None # Light illuminating the chamber self.overview_ccd = None # global view from above the sample self.overview_focus = None # focus of the overview CCD self.overview_light = None # light of the overview CCD # Indicates whether the microscope is acquiring a high quality image self.is_acquiring = model.BooleanVA(False) # The microscope object will be probed for common detectors, actuators, emitters etc. if microscope: self.role = microscope.role for c in microscope.children.value: if c.role == "ccd": self.ccd = c elif c.role == "se-detector": self.sed = c elif c.role == "bs-detector": self.bsd = c elif c.role == "spectrometer": self.spectrometer = c elif c.role == "chamber-ccd": self.chamber_ccd = c elif c.role == "overview-ccd": self.overview_ccd = c elif c.role == "stage": self.stage = c # most views move this actuator when moving elif c.role == "focus": self.focus = c elif c.role == "ebeam-focus": self.ebeam_focus = c elif c.role == "overview-focus": self.overview_focus = c elif c.role == "mirror": self.mirror = c elif c.role == "align": self.aligner = c elif c.role == "lens-switch": self.lens_switch = c elif c.role == "ar-spec-selector": self.ar_spec_sel = c elif c.role == "chamber": self.chamber = c elif c.role == "light": self.light = c elif c.role == "filter": self.light_filter = c elif c.role == "lens": self.lens = c elif c.role == "e-beam": self.ebeam = c elif c.role == "chamber-light": self.chamber_light = c elif c.role == "overview-light": self.overview_light = c # Spectrograph is not directly an actuator, but a sub-comp of spectrometer if self.spectrometer: for child in self.spectrometer.children.value: if child.role == "spectrograph": self.spectrograph = child # Check that the components that can be expected to be present on an actual microscope # have been correctly detected. if not any((self.ccd, self.sed, self.bsd, self.spectrometer)): raise KeyError("No detector found in the microscope") if not self.light and not self.ebeam: raise KeyError("No emitter found in the microscope") # Chamber is complex so we provide a "simplified state" # It's managed by the ChamberController. Setting to PUMPING or VENTING # state will request a pressure change. chamber_states = { CHAMBER_UNKNOWN, CHAMBER_VENTED, CHAMBER_PUMPING, CHAMBER_VACUUM, CHAMBER_VENTING } self.chamberState = model.IntEnumerated(CHAMBER_UNKNOWN, chamber_states) # Used when doing fine alignment, based on the value used by the user # when doing manual alignment. 0.1s is not too bad value if the user # hasn't specified anything (yet). self.fineAlignDwellTime = FloatContinuous(0.1, range=[1e-9, 100], unit="s") # TODO: should we put also the configuration related stuff? # Like path/file format # Set to True to request debug info to be displayed self.debug = model.BooleanVA(False) # Current tab (+ all available tabs in choices as a dict tab -> name) # Fully set and managed later by the TabBarController. # Not very beautiful because Tab is not part of the model. # MicroscopyGUIData would be better in theory, but is less convenient # do directly access additional GUI information. self.tab = model.VAEnumerated(None, choices={None: ""})
def __init__(self, name, stage=None, focus=None, stream_classes=None): """ :param name (string): user-friendly name of the view :param stage (Actuator): actuator with two axes: x and y :param focus (Actuator): actuator with one axis: z. Can be None :param stream_classes (None, or tuple of classes): all subclasses that the streams in this view is allowed to show. """ super(StreamView, self).__init__(name) if stream_classes is None: self.stream_classes = (Stream, ) else: self.stream_classes = stream_classes self._stage = stage # TODO: allow to have multiple focus, one per stream class self.focus = focus if focus is not None: self._focus_queue = Queue.Queue() self._focus_thread = threading.Thread(target=self._moveFocus, name="Focus mover view %s" % name) # TODO: way to detect the view is not used and so we need to stop the thread? (cf __del__?) self._focus_thread.daemon = True self._focus_thread.start() # The real stage position, to be modified via moveStageToView() # it's a direct access from the stage, so looks like a dict of axes if stage: self.stage_pos = stage.position # the current center of the view, which might be different from # the stage pos = self.stage_pos.value view_pos_init = (pos["x"], pos["y"]) else: view_pos_init = (0, 0) self.view_pos = model.ListVA(view_pos_init, unit="m") # current density (meter per pixel, ~ scale/zoom level) # 1µm/px => ~large view of the sample (view width ~= 1000 px) self.mpp = FloatContinuous(1e-6, range=(10e-12, 50e-6), unit="m/px") # How much one image is displayed on the other one. Value used by # StreamTree self.merge_ratio = FloatContinuous(0.3, range=[0, 1], unit="") self.merge_ratio.subscribe(self._onMergeRatio) # Streams to display (can be considered an implementation detail in most # cases) # Note: use addStream/removeStream for simple modifications self.stream_tree = StreamTree(merge=self.merge_ratio.value) # Only modify with this lock acquired: # TODO: Is this the source of the intermittent locking of the GUI when # Streams are active? If so, is there another/better way? self._streams_lock = threading.Lock() # TODO: list of annotations to display self.show_crosshair = model.BooleanVA(True)
class StreamView(View): """ An abstract class that is common for every view which display layers of streams and might have also actuators such as a stage and a focus. Basically, its "input" is a StreamTree and it can request stage and focus move. It never computes the composited image from all the streams itself. It's up to other objects (e.g., the canvas) to ask the StreamTree for its latest image (the main goal of this scheme is to avoid computation when not needed). Similarly, the thumbnail is never automatically recomputed, but other objects can update it. """ def __init__(self, name, stage=None, focus=None, stream_classes=None): """ :param name (string): user-friendly name of the view :param stage (Actuator): actuator with two axes: x and y :param focus (Actuator): actuator with one axis: z. Can be None :param stream_classes (None, or tuple of classes): all subclasses that the streams in this view is allowed to show. """ super(StreamView, self).__init__(name) if stream_classes is None: self.stream_classes = (Stream, ) else: self.stream_classes = stream_classes self._stage = stage # TODO: allow to have multiple focus, one per stream class self.focus = focus if focus is not None: self._focus_queue = Queue.Queue() self._focus_thread = threading.Thread(target=self._moveFocus, name="Focus mover view %s" % name) # TODO: way to detect the view is not used and so we need to stop the thread? (cf __del__?) self._focus_thread.daemon = True self._focus_thread.start() # The real stage position, to be modified via moveStageToView() # it's a direct access from the stage, so looks like a dict of axes if stage: self.stage_pos = stage.position # the current center of the view, which might be different from # the stage pos = self.stage_pos.value view_pos_init = (pos["x"], pos["y"]) else: view_pos_init = (0, 0) self.view_pos = model.ListVA(view_pos_init, unit="m") # current density (meter per pixel, ~ scale/zoom level) # 1µm/px => ~large view of the sample (view width ~= 1000 px) self.mpp = FloatContinuous(1e-6, range=(10e-12, 50e-6), unit="m/px") # How much one image is displayed on the other one. Value used by # StreamTree self.merge_ratio = FloatContinuous(0.3, range=[0, 1], unit="") self.merge_ratio.subscribe(self._onMergeRatio) # Streams to display (can be considered an implementation detail in most # cases) # Note: use addStream/removeStream for simple modifications self.stream_tree = StreamTree(merge=self.merge_ratio.value) # Only modify with this lock acquired: # TODO: Is this the source of the intermittent locking of the GUI when # Streams are active? If so, is there another/better way? self._streams_lock = threading.Lock() # TODO: list of annotations to display self.show_crosshair = model.BooleanVA(True) def has_stage(self): return self._stage is not None def _moveFocus(self): time_last_move = 0 axis = self.focus.axes["z"] try: rng = axis.range except AttributeError: rng = None try: while True: # wait until there is something to do shift = self._focus_queue.get() pos = self.focus.position.value["z"] # rate limit to 20 Hz sleept = time_last_move + 0.05 - time.time() if sleept > 0: time.sleep(sleept) # Add more moves if there are already more try: while True: shift += self._focus_queue.get(block=False) except Queue.Empty: pass logging.debug("Moving focus by %f μm", shift * 1e6) # clip to the range if rng: new_pos = pos + shift new_pos = max(rng[0], min(new_pos, rng[1])) req_shift = shift shift = new_pos - pos if abs(shift - req_shift) > 1e-9: logging.info( "Restricting focus move to %f µm as it reached the end", shift * 1e6) f = self.focus.moveRel({"z": shift}) time_last_move = time.time() # wait until it's finished so that we don't accumulate requests, # but instead only do requests of size "big enough" try: f.result() except Exception: logging.info("Failed to apply focus move", exc_info=1) except Exception: logging.exception("Focus mover thread failed") def moveFocusRel(self, shift): # FIXME: "stop all axes" should also clear the queue self._focus_queue.put(shift) def moveStageToView(self): """ Move the stage to the current view_pos :return (None or Future): a future (that allows to know when the move is finished) Note: once the move is finished stage_pos will be updated (by the back-end) """ if not self._stage: return view_pos = self.view_pos.value # TODO: Use the max FoV of the streams to determine what's a big # distance (because on the overview cam a move can be much bigger than # on a SEM image at high mag). # relative prev_pos = self.stage_pos.value move = { "x": view_pos[0] - prev_pos["x"], "y": view_pos[1] - prev_pos["y"] } if abs(move["x"]) < 1e-12 and abs(move["y"]) < 1e-12: logging.debug("skipping move request of 0") return # Check it makes sense (=> not too big) distance = math.sqrt(sum([v**2 for v in move.values()])) if distance > MAX_SAFE_MOVE_DISTANCE: logging.error( "Cancelling request to move by %f m (because > %f m)", distance, MAX_SAFE_MOVE_DISTANCE) return logging.debug("Sending move request of %s", move) return self._stage.moveRel(move) def getStreams(self): """ :return: [Stream] list of streams that are displayed in the view Do not modify directly, use addStream(), and removeStream(). Note: use .stream_tree for getting the raw StreamTree """ return self.stream_tree.getStreams() def addStream(self, stream): """ Add a stream to the view. It takes care of updating the StreamTree according to the type of stream. stream (acq.stream.Stream): stream to add If the stream is already present, nothing happens """ # check if the stream is already present if stream in self.stream_tree.getStreams(): return if not isinstance(stream, self.stream_classes): msg = "Adding incompatible stream '%s' to view '%s'. %s needed" logging.warning(msg, stream.name.value, self.name.value, self.stream_classes) # Find out where the stream should go in the streamTree # FIXME: manage sub-trees, with different merge operations # For now we just add it to the list of streams, with the only merge # operation possible with self._streams_lock: self.stream_tree.add_stream(stream) # subscribe to the stream's image if hasattr(stream, "image"): stream.image.subscribe(self._onNewImage) # if the stream already has an image, update now if stream.image.value is not None: self._onNewImage(stream.image.value) else: logging.debug("No image found for stream %s", type(stream)) def removeStream(self, stream): """ Remove a stream from the view. It takes care of updating the StreamTree. stream (Stream): stream to remove If the stream is not present, nothing happens """ # Stop listening to the stream changes if hasattr(stream, "image"): stream.image.unsubscribe(self._onNewImage) with self._streams_lock: # check if the stream is already removed if not stream in self.stream_tree.getStreams(): return # remove stream from the StreamTree() # TODO handle more complex trees self.stream_tree.remove_stream(stream) # let everyone know that the view has changed self.lastUpdate.value = time.time() def _onNewImage(self, im): """ Called when one stream has its image updated im (DataArray) """ # just let everyone know that the composited image has changed self.lastUpdate.value = time.time() def _onMergeRatio(self, ratio): """ Called when the merge ratio is modified """ # This actually modifies the root operator of the stream tree # It has effect only if the operator can do something with the "merge" # argument with self._streams_lock: self.stream_tree.kwargs["merge"] = ratio # just let everyone that the composited image has changed self.lastUpdate.value = time.time() def is_compatible(self, stream_cls): """ Check if the given stream class is compatible with this view. """ return issubclass(stream_cls, self.stream_classes)
def __init__(self, name, stage=None, focus=None, stream_classes=None): """ :param name (string): user-friendly name of the view :param stage (Actuator): actuator with two axes: x and y :param focus (Actuator): actuator with one axis: z. Can be None :param stream_classes (None, or tuple of classes): all subclasses that the streams in this view is allowed to show. """ super(StreamView, self).__init__(name) if stream_classes is None: self.stream_classes = (Stream,) else: self.stream_classes = stream_classes self._stage = stage # TODO: allow to have multiple focus, one per stream class self.focus = focus if focus is not None: self._focus_queue = Queue.Queue() self._focus_thread = threading.Thread(target=self._moveFocus, name="Focus mover view %s" % name) # TODO: way to detect the view is not used and so we need to stop the thread? (cf __del__?) self._focus_thread.daemon = True self._focus_thread.start() # The real stage position, to be modified via moveStageToView() # it's a direct access from the stage, so looks like a dict of axes if stage: self.stage_pos = stage.position # the current center of the view, which might be different from # the stage pos = self.stage_pos.value view_pos_init = (pos["x"], pos["y"]) else: view_pos_init = (0, 0) self.view_pos = model.ListVA(view_pos_init, unit="m") # current density (meter per pixel, ~ scale/zoom level) # 1µm/px => ~large view of the sample (view width ~= 1000 px) self.mpp = FloatContinuous(1e-6, range=(10e-12, 50e-6), unit="m/px") # How much one image is displayed on the other one. Value used by # StreamTree self.merge_ratio = FloatContinuous(0.3, range=[0, 1], unit="") self.merge_ratio.subscribe(self._onMergeRatio) # Streams to display (can be considered an implementation detail in most # cases) # Note: use addStream/removeStream for simple modifications self.stream_tree = StreamTree(merge=self.merge_ratio.value) # Only modify with this lock acquired: # TODO: Is this the source of the intermittent locking of the GUI when # Streams are active? If so, is there another/better way? self._streams_lock = threading.Lock() # TODO: list of annotations to display self.show_crosshair = model.BooleanVA(True)
class StreamView(View): """ An abstract class that is common for every view which display layers of streams and might have also actuators such as a stage and a focus. Basically, its "input" is a StreamTree and it can request stage and focus move. It never computes the composited image from all the streams itself. It's up to other objects (e.g., the canvas) to ask the StreamTree for its latest image (the main goal of this scheme is to avoid computation when not needed). Similarly, the thumbnail is never automatically recomputed, but other objects can update it. """ def __init__(self, name, stage=None, focus=None, stream_classes=None): """ :param name (string): user-friendly name of the view :param stage (Actuator): actuator with two axes: x and y :param focus (Actuator): actuator with one axis: z. Can be None :param stream_classes (None, or tuple of classes): all subclasses that the streams in this view is allowed to show. """ super(StreamView, self).__init__(name) if stream_classes is None: self.stream_classes = (Stream,) else: self.stream_classes = stream_classes self._stage = stage # TODO: allow to have multiple focus, one per stream class self.focus = focus if focus is not None: self._focus_queue = Queue.Queue() self._focus_thread = threading.Thread(target=self._moveFocus, name="Focus mover view %s" % name) # TODO: way to detect the view is not used and so we need to stop the thread? (cf __del__?) self._focus_thread.daemon = True self._focus_thread.start() # The real stage position, to be modified via moveStageToView() # it's a direct access from the stage, so looks like a dict of axes if stage: self.stage_pos = stage.position # the current center of the view, which might be different from # the stage pos = self.stage_pos.value view_pos_init = (pos["x"], pos["y"]) else: view_pos_init = (0, 0) self.view_pos = model.ListVA(view_pos_init, unit="m") # current density (meter per pixel, ~ scale/zoom level) # 1µm/px => ~large view of the sample (view width ~= 1000 px) self.mpp = FloatContinuous(1e-6, range=(10e-12, 50e-6), unit="m/px") # How much one image is displayed on the other one. Value used by # StreamTree self.merge_ratio = FloatContinuous(0.3, range=[0, 1], unit="") self.merge_ratio.subscribe(self._onMergeRatio) # Streams to display (can be considered an implementation detail in most # cases) # Note: use addStream/removeStream for simple modifications self.stream_tree = StreamTree(merge=self.merge_ratio.value) # Only modify with this lock acquired: # TODO: Is this the source of the intermittent locking of the GUI when # Streams are active? If so, is there another/better way? self._streams_lock = threading.Lock() # TODO: list of annotations to display self.show_crosshair = model.BooleanVA(True) def has_stage(self): return self._stage is not None def _moveFocus(self): time_last_move = 0 axis = self.focus.axes["z"] try: rng = axis.range except AttributeError: rng = None try: while True: # wait until there is something to do shift = self._focus_queue.get() pos = self.focus.position.value["z"] # rate limit to 20 Hz sleept = time_last_move + 0.05 - time.time() if sleept > 0: time.sleep(sleept) # Add more moves if there are already more try: while True: shift += self._focus_queue.get(block=False) except Queue.Empty: pass logging.debug("Moving focus by %f μm", shift * 1e6) # clip to the range if rng: new_pos = pos + shift new_pos = max(rng[0], min(new_pos, rng[1])) req_shift = shift shift = new_pos - pos if abs(shift - req_shift) > 1e-9: logging.info("Restricting focus move to %f µm as it reached the end", shift * 1e6) f = self.focus.moveRel({"z": shift}) time_last_move = time.time() # wait until it's finished so that we don't accumulate requests, # but instead only do requests of size "big enough" try: f.result() except Exception: logging.info("Failed to apply focus move", exc_info=1) except Exception: logging.exception("Focus mover thread failed") def moveFocusRel(self, shift): # FIXME: "stop all axes" should also clear the queue self._focus_queue.put(shift) def moveStageToView(self): """ Move the stage to the current view_pos :return (None or Future): a future (that allows to know when the move is finished) Note: once the move is finished stage_pos will be updated (by the back-end) """ if not self._stage: return view_pos = self.view_pos.value # TODO: Use the max FoV of the streams to determine what's a big # distance (because on the overview cam a move can be much bigger than # on a SEM image at high mag). # relative prev_pos = self.stage_pos.value move = { "x": view_pos[0] - prev_pos["x"], "y": view_pos[1] - prev_pos["y"] } if abs(move["x"]) < 1e-12 and abs(move["y"]) < 1e-12: logging.debug("skipping move request of 0") return # Check it makes sense (=> not too big) distance = math.sqrt(sum([v ** 2 for v in move.values()])) if distance > MAX_SAFE_MOVE_DISTANCE: logging.error("Cancelling request to move by %f m (because > %f m)", distance, MAX_SAFE_MOVE_DISTANCE) return logging.debug("Sending move request of %s", move) return self._stage.moveRel(move) def getStreams(self): """ :return: [Stream] list of streams that are displayed in the view Do not modify directly, use addStream(), and removeStream(). Note: use .stream_tree for getting the raw StreamTree """ return self.stream_tree.getStreams() def addStream(self, stream): """ Add a stream to the view. It takes care of updating the StreamTree according to the type of stream. stream (acq.stream.Stream): stream to add If the stream is already present, nothing happens """ # check if the stream is already present if stream in self.stream_tree.getStreams(): return if not isinstance(stream, self.stream_classes): msg = "Adding incompatible stream '%s' to view '%s'. %s needed" logging.warning(msg, stream.name.value, self.name.value, self.stream_classes) # Find out where the stream should go in the streamTree # FIXME: manage sub-trees, with different merge operations # For now we just add it to the list of streams, with the only merge # operation possible with self._streams_lock: self.stream_tree.add_stream(stream) # subscribe to the stream's image if hasattr(stream, "image"): stream.image.subscribe(self._onNewImage) # if the stream already has an image, update now if stream.image.value is not None: self._onNewImage(stream.image.value) else: logging.debug("No image found for stream %s", type(stream)) def removeStream(self, stream): """ Remove a stream from the view. It takes care of updating the StreamTree. stream (Stream): stream to remove If the stream is not present, nothing happens """ # Stop listening to the stream changes if hasattr(stream, "image"): stream.image.unsubscribe(self._onNewImage) with self._streams_lock: # check if the stream is already removed if not stream in self.stream_tree.getStreams(): return # remove stream from the StreamTree() # TODO handle more complex trees self.stream_tree.remove_stream(stream) # let everyone know that the view has changed self.lastUpdate.value = time.time() def _onNewImage(self, im): """ Called when one stream has its image updated im (DataArray) """ # just let everyone know that the composited image has changed self.lastUpdate.value = time.time() def _onMergeRatio(self, ratio): """ Called when the merge ratio is modified """ # This actually modifies the root operator of the stream tree # It has effect only if the operator can do something with the "merge" # argument with self._streams_lock: self.stream_tree.kwargs["merge"] = ratio # just let everyone that the composited image has changed self.lastUpdate.value = time.time() def is_compatible(self, stream_cls): """ Check if the given stream class is compatible with this view. """ return issubclass(stream_cls, self.stream_classes)