Esempio n. 1
0
 def __init__(self, name, x, y, z, milling_angle, streams=None):
     """
     :param name: (string) the feature name
     :param x: (float) the X axis of the feature position
     :param y: (float) the Y axis of the feature position
     :param z: (float) the Z axis of the feature position
     :param milling_angle: (float)  angle used for milling (angle between the sample and the ion-beam, similar to the
     one in the chamber tab, not the actual Rx)
     :param streams: (List of StaticStream) list of acquired streams on this feature
     """
     self.name = model.StringVA(name)
     # The 3D position of an interesting point in the site (Typically, the milling should happen around that
     # volume, never touching it.)
     self.pos = model.TupleContinuous(
         (x, y, z),
         range=((-1, -1, -1), (1, 1, 1)),
         cls=(int, float),
     )
     # TODO: Check if negative milling angle is allowed
     if milling_angle <= 0:
         milling_angle = DEFAULT_MILLING_ANGLE
         logging.warning(
             f"Given milling angle {milling_angle} is negative, setting it to default {DEFAULT_MILLING_ANGLE}"
         )
     self.milling_angle = model.FloatVA(milling_angle)
     self.status = model.StringVA(FEATURE_ACTIVE)
     # TODO: Handle acquired files
     self.streams = streams if streams is not None else model.ListVA()
Esempio n. 2
0
    def __init__(self, name, role, parent, **kwargs):
        '''
        parent (symphotime.Controller): a symphotime server parent object
        '''
        # we will fill the set of children with Components later in ._children
        model.Emitter.__init__(self, name, role, parent=parent, **kwargs)
        self._shape = (2048, 2048) # Max resolution

        # Define VA's as references to the parent.
        self.filename = model.StringVA(setter=self._setFilename)
        self.directory = model.StringVA(setter=self._setDirectory)
        self.resolution = model.ResolutionVA((64, 64), ((1, 1), (2048, 2048)))
        self.bidirectional = model.BooleanVA(value=False)
        self.dwellTime = model.FloatContinuous(value=10e-6, range=DWELLTIME_RNG, unit="s")
Esempio n. 3
0
    def __init__(self, microscope, main_app):
        super(MonoScanPlugin, self).__init__(microscope, main_app)

        # Can only be used on a Sparc with a monochromator
        if not microscope:
            return
        try:
            self.ebeam = model.getComponent(role="e-beam")
            self.mchr = model.getComponent(role="monochromator")
            self.sgrh = model.getComponent(role="spectrograph")
        except LookupError:
            logging.debug("No mochromator and spectrograph found, cannot use the plugin")
            return

        self.addMenu("Acquisition/Monochromator scan...", self.start)

        # the SEM survey stream (will be updated when showing the window)
        self._survey_s = None

        # Create a stream for monochromator scan
        self._mchr_s = MonochromatorScanStream("Spectrum", self.mchr, self.ebeam, self.sgrh)

        # The settings to be displayed in the dialog
        # Trick: we use the same VAs as the stream, so they are directly synchronised
        self.startWavelength = self._mchr_s.startWavelength
        self.endWavelength = self._mchr_s.endWavelength
        self.numberOfPixels = self._mchr_s.numberOfPixels
        self.dwellTime = self._mchr_s.dwellTime

        self.filename = model.StringVA("a.h5")
        self.expectedDuration = model.VigilantAttribute(1, unit="s", readonly=True)

        # Update the expected duration when values change
        self.dwellTime.subscribe(self._update_exp_dur)
        self.numberOfPixels.subscribe(self._update_exp_dur)
Esempio n. 4
0
    def __init__(self, tab_data, main_frame, settings_controller, roa, vas):
        """
        tab_data (MicroscopyGUIData): the representation of the microscope GUI
        main_frame: (wx.Frame): the frame which contains the 4 viewports
        settings_controller (SettingsController)
        roa (VA): VA of the ROA
        vas (list of VAs): all the VAs which might affect acquisition (time)
        """
        self._tab_data_model = tab_data
        self._main_data_model = tab_data.main
        self._main_frame = main_frame
        self._roa = roa
        self._vas = vas

        # For file selection
        self.conf = conf.get_acqui_conf()

        # for saving/restoring the settings
        self._settings_controller = settings_controller
        self._orig_settings = {}  # Entry -> value to restore

        # TODO: this should be the date at which the user presses the acquire
        # button (or when the last settings were changed)!
        # At least, we must ensure it's a new date after the acquisition
        # is done.
        # Filename to save the acquisition
        self.filename = model.StringVA(self._get_default_filename())
        self.filename.subscribe(self._onFilename, init=True)

        # For acquisition
        # a ProgressiveFuture if the acquisition is going on
        self.btn_acquire = self._main_frame.btn_sparc_acquire
        self.btn_change_file = self._main_frame.btn_sparc_change_file
        self.btn_cancel = self._main_frame.btn_sparc_cancel
        self.acq_future = None
        self.gauge_acq = self._main_frame.gauge_sparc_acq
        self.lbl_acqestimate = self._main_frame.lbl_sparc_acq_estimate
        self._acq_future_connector = None

        # TODO: share an executor with the whole GUI.
        self._executor = futures.ThreadPoolExecutor(max_workers=2)

        # Link buttons
        self.btn_acquire.Bind(wx.EVT_BUTTON, self.on_acquisition)
        self.btn_change_file.Bind(wx.EVT_BUTTON, self.on_change_file)
        self.btn_cancel.Bind(wx.EVT_BUTTON, self.on_cancel)

        self.gauge_acq.Hide()
        self._main_frame.Layout()

        # TODO: we need to be informed if the user closes suddenly the window
        # self.Bind(wx.EVT_CLOSE, self.on_close)

        # Event binding
        pub.subscribe(self.on_setting_change, 'setting.changed')
        # TODO We should also listen to repetition, in case it's modified after we've
        # received the new ROI. Or maybe always compute acquisition time a bit delayed?
        for va in vas:
            va.subscribe(self.onAnyVA)
        roa.subscribe(self.onROA, init=True)
Esempio n. 5
0
    def __init__(self, name, detector, emitter, spectrograph, opm=None):
        """
        name (string): user-friendly name of this stream
        detector (Detector): the monochromator
        emitter (Emitter): the emitter (eg: ebeam scanner)
        spectrograph (Actuator): the spectrograph
        """
        self.name = model.StringVA(name)

        # Hardware Components
        self._detector = detector
        self._emitter = emitter
        self._sgr = spectrograph
        self._opm = opm

        self.is_active = model.BooleanVA(False)

        wlr = spectrograph.axes["wavelength"].range
        self.startWavelength = model.FloatContinuous(400e-9, wlr, unit="m")
        self.endWavelength = model.FloatContinuous(500e-9, wlr, unit="m")
        self.numberOfPixels = model.IntContinuous(51, (2, 10001), unit="px")
        # TODO: could be a local attribute?
        self.dwellTime = model.FloatContinuous(
            1e-3, range=self._emitter.dwellTime.range, unit="s")
        self.emtTranslation = model.TupleContinuous(
            (0, 0),
            range=self._emitter.translation.range,
            cls=(int, long, float),
            unit="px")

        # For acquisition
        self._pt_acq = threading.Event()
        self._data = []
        self._md = {}
Esempio n. 6
0
    def __init__(self, microscope, main_app):
        super(CLAcqPlugin, self).__init__(microscope, main_app)
        # Can only be used with a microscope
        if not microscope:
            return
        else:
            # Check which stream the microscope supports
            main_data = self.main_app.main_data
            if not (main_data.ccd and main_data.ebeam):
                return

        self.exposureTime = main_data.ccd.exposureTime
        self.binning = main_data.ccd.binning
        # Trick to pass the component (ccd to binning_1d_from_2d())
        self.vaconf["binning"]["choices"] = (
            lambda cp, va, cf: gui.conf.util.binning_1d_from_2d(
                main_data.ccd, va, cf))
        self.xres = model.IntContinuous(10, (1, 1000), unit="px")
        self.yres = model.IntContinuous(10, (1, 1000), unit="px")
        self.stepsize = model.FloatVA(1e-6, unit="m")  # Just to show
        # Maximum margin is half the CCD FoV
        ccd_rect = get_ccd_fov(main_data.ccd)
        max_margin = max(ccd_rect[2] - ccd_rect[0],
                         ccd_rect[3] - ccd_rect[1]) / 2
        self.roi_margin = model.FloatContinuous(0, (0, max_margin), unit="m")
        self.filename = model.StringVA("a.tiff")

        self.xres.subscribe(self._update_stepsize)
        self.yres.subscribe(self._update_stepsize)

        self.addMenu("Acquisition/CL acquisition...", self.start)
Esempio n. 7
0
    def __init__(self, microscope, main_app):
        super(TimelapsePlugin, self).__init__(microscope, main_app)
        # Can only be used with a microscope
        if not microscope:
            return

        self.period = model.FloatContinuous(10, (1e-3, 10000),
                                            unit="s",
                                            setter=self._setPeriod)
        # TODO: prevent period < acquisition time of all streams
        self.numberOfAcquisitions = model.IntContinuous(100, (2, 100000))
        self.semOnlyOnLast = model.BooleanVA(False)
        self.filename = model.StringVA("a.h5")
        self.expectedDuration = model.VigilantAttribute(1,
                                                        unit="s",
                                                        readonly=True)

        self.period.subscribe(self._update_exp_dur)
        self.numberOfAcquisitions.subscribe(self._update_exp_dur)

        # On SECOM/DELPHI, propose to only acquire the SEM at the end
        if microscope.role in ("secom", "delphi"):
            self.vaconf["semOnlyOnLast"][
                "control_type"] = odemis.gui.CONTROL_CHECK

        self._dlg = None
        self.addMenu("Acquisition/Timelapse...\tCtrl+T", self.start)

        self._to_store = queue.Queue(
        )  # queue of tuples (str, [DataArray]) for saving data
        self._sthreads = []  # the saving threads
        self._exporter = None  # dataio exporter to use
Esempio n. 8
0
    def __init__(self, microscope, main_app):
        super(ZStackPlugin, self).__init__(microscope, main_app)
        # Can only be used with a microscope
        main_data = self.main_app.main_data
        
        if not microscope or main_data.focus is None:
            return

        self.focus = main_data.focus
        self._zrange = self.focus.axes['z'].range
        zunit = self.focus.axes['z'].unit
        self._old_pos = self.focus.position.value
        z = max(self._zrange[0], min(self._old_pos['z'], self._zrange[1]))
        self.zstart = model.FloatContinuous(z, range=self._zrange, unit=zunit)
        self.zstep = model.FloatContinuous(1e-6, range=(-1e-5, 1e-5), unit=zunit, setter=self._setZStep)
        self.numberOfAcquisitions = model.IntContinuous(3, (2, 999), setter=self._setNumberOfAcquisitions)

        self.filename = model.StringVA("a.h5")
        self.expectedDuration = model.VigilantAttribute(1, unit="s", readonly=True)

        self.zstep.subscribe(self._update_exp_dur)
        self.numberOfAcquisitions.subscribe(self._update_exp_dur)
        
        # Two acquisition order possible:
        # * for each Z, all the streams (aka intertwined): Z exactly the same for each stream
        # * for each stream, the whole Z stack: Might be faster (if filter wheel used for changing wavelength)
        self._streams_intertwined = True
        if main_data.light_filter and len(main_data.light_filter.axes["band"].choices) > 1:
            logging.info("Filter-wheel detected, Z-stack will be acquired stream-per-stream")
            self._streams_intertwined = False

        self._acq_streams = None  # previously folded streams, for optimisation
        self._dlg = None
        self.addMenu("Acquisition/ZStack...\tCtrl+B", self.start)
Esempio n. 9
0
    def __init__(self, name, ccd, emitter, emd):
        """
        name (string): user-friendly name of this stream
        ccd (Camera): the ccd
        emitter (Emitter): the emitter (eg: ebeam scanner)
        emd (Detector): the SEM detector (eg: SED)
        """
        self.name = model.StringVA(name)

        # Hardware Components
        self._detector = emd
        self._emitter = emitter
        self._ccd = ccd

        # 0.1s is a bit small, but the algorithm will automaticaly try with
        # longer dwell times if no spot is visible first.
        self.dwellTime = model.FloatContinuous(0.1,
                                               range=emitter.dwellTime.range,
                                               unit="s")
        # The number of points in the grid
        self.repetition = model.ResolutionVA(
            (4, 4),  # good default
            range=((2, 2), (16, 16)))

        # Future generated by find_overlay
        self._overlay_future = None
Esempio n. 10
0
    def __init__(self, microscope, main_app):
        super(ZStackPlugin, self).__init__(microscope, main_app)
        # Can only be used with a microscope
        main_data = self.main_app.main_data

        if not microscope or main_data.focus is None:
            return

        self.focus = main_data.focus
        self._zrange = self.focus.axes['z'].range
        zunit = self.focus.axes['z'].unit
        self.old_pos = self.focus.position.value
        self.zstart = model.FloatContinuous(self.old_pos['z'],
                                            range=self._zrange,
                                            unit=zunit)
        self.zstep = model.FloatContinuous(1e-6,
                                           range=(-1e-5, 1e-5),
                                           unit=zunit,
                                           setter=self._setZStep)
        self.numberofAcquisitions = model.IntContinuous(
            3, (2, 999), setter=self._setNumberOfAcquisitions)

        self.filename = model.StringVA("a.h5")
        self.expectedDuration = model.VigilantAttribute(1,
                                                        unit="s",
                                                        readonly=True)

        self.zstep.subscribe(self._update_exp_dur)
        self.numberofAcquisitions.subscribe(self._update_exp_dur)

        self._acq_streams = None  # previously folded streams, for optimisation
        self._dlg = None
        self.addMenu("Acquisition/ZStack...\tCtrl+B", self.start)
Esempio n. 11
0
    def __init__(self, name, coordinates, roc, asm, multibeam, descanner,
                 detector):
        """
        :param name: (str) Name of the region of acquisition (ROA). It is the name of the megafield (id) as stored on
                     the external storage.
        :param coordinates: (float, float, float, float) left, top, right, bottom, Bounding box
                            coordinates of the ROA in [m]. The coordinates are in the sample carrier coordinate
                            system, which corresponds to the component with role='stage'.
        :param roc: (FastEMROC) Corresponding region of calibration (ROC).
        :param asm: (technolution.AcquisitionServer) The acquisition server module component.
        :param multibeam: (technolution.EBeamScanner) The multibeam scanner component of the acquisition server module.
        :param descanner: (technolution.MirrorDescanner) The mirror descanner component of the acquisition server module.
        :param detector: (technolution.MPPC) The detector object to be used for collecting the image data.
        """
        self.name = model.StringVA(name)
        self.coordinates = model.TupleContinuous(coordinates,
                                                 range=((-1, -1, -1, -1),
                                                        (1, 1, 1, 1)),
                                                 cls=(int, float),
                                                 unit='m')
        self.roc = model.VigilantAttribute(roc)
        self._asm = asm
        self._multibeam = multibeam
        self._descanner = descanner
        self._detector = detector

        # List of tuples(int, int) containing the position indices of each field to be acquired.
        # Automatically updated when the coordinates change.
        self.field_indices = []
        self.coordinates.subscribe(self.on_coordinates, init=True)
Esempio n. 12
0
    def __init__(self, microscope, main_app):
        super(AveragePlugin, self).__init__(microscope, main_app)
        # Can only be used with a microscope
        if not microscope:
            return

        # Check which stream the microscope supports
        main_data = self.main_app.main_data
        if not main_data.ebeam:
            return

        self.addMenu("Acquisition/Averaged frame...", self.start)

        dt = main_data.ebeam.dwellTime
        dtrg = (dt.range[0], min(dt.range[1], 1))
        self.dwellTime = model.FloatContinuous(dt.value,
                                               range=dtrg,
                                               unit=dt.unit)
        self.scale = main_data.ebeam.scale
        # Trick to pass the component (ebeam to binning_1d_from_2d())
        self.vaconf["scale"]["choices"] = (
            lambda cp, va, cf: odemis.gui.conf.util.binning_1d_from_2d(
                self.main_app.main_data.ebeam, va, cf))
        self.resolution = main_data.ebeam.resolution  # Just for info
        self.accumulations = model.IntContinuous(10, (1, 10000))
        self.filename = model.StringVA("a.h5")
        self.expectedDuration = model.VigilantAttribute(1,
                                                        unit="s",
                                                        readonly=True)

        self.dwellTime.subscribe(self._update_exp_dur)
        self.accumulations.subscribe(self._update_exp_dur)
        self.scale.subscribe(self._update_exp_dur)
Esempio n. 13
0
    def __init__(self, name, streams):
        """
        :param streams: ([Stream]) The streams to acquire.
        """
        super(SECOMCLSEMMDStream, self).__init__(name, streams)

        self.filename = model.StringVA("a.tiff")
        self.firstOptImg = None  # save the first optical image for display in analysis tab
Esempio n. 14
0
    def __init__(self, name, detector, dataflow, emitter):
        self.name = model.StringVA(name)

        # Hardware Components
        self._detector = detector  # the spectrometer
        self._emitter = emitter  # the e-beam
        # To acquire simultaneously other detector (ex: SEM secondary electrons)
        # a separate stream must be used, and the acquisition manager will take
        # care of doing both at the same time

        # data-flow of the spectrometer
        self._dataflow = dataflow

        self.raw = []  # to contain data during acquisition (from MD streams)

        # all the information needed to acquire an image (in addition to the
        # hardware component settings which can be directly set).

        # ROI + repetition is sufficient, but pixel size is nicer for the user
        # and allow us to ensure each pixel is square. (Non-square pixels are
        # not a problem for the hardware, but annoying to display data in normal
        # software).

        # We ensure in the setters that all the data is always consistent:
        # roi set: roi + pxs → repetition + roi + pxs
        # pxs set: roi + pxs → repetition + roi (small changes)
        # repetition set: repetition + roi + pxs → repetition + pxs + roi (small changes)

        # Region of interest as left, top, right, bottom (in ratio from the
        # whole area of the emitter => between 0 and 1)
        self.roi = model.TupleContinuous((0, 0, 1, 1),
                                         range=[(0, 0, 0, 0), (1, 1, 1, 1)],
                                         cls=(int, long, float),
                                         setter=self._setROI)
        # the number of pixels acquired in each dimension
        # it will be assigned to the resolution of the emitter (but cannot be
        # directly set, as one might want to use the emitter while configuring
        # the stream).
        self.repetition = model.ResolutionVA(emitter.resolution.value,
                                             emitter.resolution.range,
                                             setter=self._setRepetition)

        # the size of the pixel, horizontally and vertically
        # actual range is dynamic, as it changes with the magnification
        self.pixelSize = model.FloatContinuous(emitter.pixelSize.value[0],
                                               range=[0, 1],
                                               unit="m",
                                               setter=self._setPixelSize)

        # exposure time of each pixel is the exposure time of the detector,
        # the dwell time of the emitter will be adapted before acquisition.

        # Update the pixel size whenever SEM magnification changes
        # This allows to keep the ROI at the same place in the SEM FoV.
        # Note: this is to be done only if the user needs to manually update the
        # magnification.
        self._prev_mag = emitter.magnification.value
        emitter.magnification.subscribe(self._onMagnification)
Esempio n. 15
0
    def __init__(self, name, detector, sed, emitter, opm=None):
        """
        name (string): user-friendly name of this stream
        detector (Detector): the monochromator
        sed (Detector): the se-detector
        emitter (Emitter): the emitter (eg: ebeam scanner)
        spectrograph (Actuator): the spectrograph
        """
        self.name = model.StringVA(name)

        # Hardware Components, detector is the correlator, sed is the secondary electron image and the emitter is the electron beam
        self._detector = detector
        self._sed = sed
        self._emitter = emitter
        self._opm = opm

        self.is_active = model.BooleanVA(False)

        #dwell time and exposure time are the same thing in this case
        self.dwellTime = model.FloatContinuous(1, range=self._emitter.dwellTime.range,
                                               unit="s")
        # pixelDuration of correlator, this can be shortened once implemented as choices.
        self.pixelDuration = model.FloatEnumerated(512e-12,
                                choices={4e-12, 8e-12, 16e-12, 32e-12, 64e-12, 128e-12, 256e-12, 512e-12},
                                unit="s",
                                )
        #Sync Offset time correlator
        self.syncOffset = self._detector.syncOffset
        #Sync Divider time correlator
        self.syncDiv = self._detector.syncDiv 
        
        # Distance between the center of each pixel
        self.stepsize = model.FloatContinuous(1e-6, (1e-9, 1e-4), unit="m")

        # Region of acquisition. ROI form is LEFT Top RIGHT Bottom, relative to full field size
        self.roi = model.TupleContinuous((0, 0, 1, 1),
                                         range=((0, 0, 0, 0), (1, 1, 1, 1)),
                                         cls=(int, long, float))

        # Cropvalue that  can be used to crop the data for better visualization in odemis
        self.cropvalue = model.IntContinuous(1024, (1, 65536), unit="px")

        # For drift correction
        self.dcRegion = model.TupleContinuous(UNDEFINED_ROI,
                                              range=((0, 0, 0, 0), (1, 1, 1, 1)),
                                              cls=(int, long, float))
        self.dcDwellTime = model.FloatContinuous(emitter.dwellTime.range[0],
                                                 range=emitter.dwellTime.range, unit="s")
        #number of drift corrections per scanning pixel
        self.nDC = model.IntContinuous(1, (1, 20))

        # For acquisition
        self.tc_data = None
        self.tc_data_received = threading.Event()
        self.sem_data = []
        self.sem_data_received = threading.Event()
        self._hw_settings = None
Esempio n. 16
0
    def __init__(self, microscope, main_app):
        # Called when the plugin is loaed (ie, at GUI initialisation)
        super(Correlator2D, self).__init__(microscope, main_app)
        if not microscope or microscope.role not in ("sparc", "sparc2"):
            return
        try:
            self.ebeam = model.getComponent(role="e-beam")
            self.correlator = model.getComponent(role="time-correlator")
            self.sed = model.getComponent(role="se-detector")

        except LookupError:
            logging.debug("Hardware not found, cannot use the plugin")
            return

        # the SEM survey stream (will be updated when showing the window)
        self._survey_s = None

        # Create a stream for correlator spectral measurement
        self._correlator_s = CorrelatorScanStream("Correlator data",
                                                  self.correlator, self.sed,
                                                  self.ebeam,
                                                  main_app.main_data.opm)

        # For reading the ROA and anchor ROI
        self._acqui_tab = main_app.main_data.getTabByName(
            "sparc_acqui").tab_data_model

        self.dwellTime = self._correlator_s.dwellTime
        self.pixelDuration = self._correlator_s.pixelDuration
        self.syncOffset = self._correlator_s.syncOffset
        self.syncDiv = self._correlator_s.syncDiv

        # The scanning positions are defined by ROI (as selected in the acquisition tab) + stepsize
        # Based on these values, the scanning resolution is computed
        self.roi = self._correlator_s.roi
        self.stepsize = self._correlator_s.stepsize
        self.cropvalue = self._correlator_s.cropvalue
        self.xres = model.IntVA(1, unit="px")
        self.yres = model.IntVA(1, unit="px")

        self.nDC = self._correlator_s.nDC

        self.filename = model.StringVA("a.h5")
        self.expectedDuration = model.VigilantAttribute(1,
                                                        unit="s",
                                                        readonly=True)

        # Update the expected duration when values change, depends both dwell time and # of pixels
        self.dwellTime.subscribe(self._update_exp_dur)
        self.stepsize.subscribe(self._update_exp_dur)
        self.nDC.subscribe(self._update_exp_dur)

        # subscribe to update X/Y res
        self.stepsize.subscribe(self._update_res)
        self.roi.subscribe(self._update_res)

        self.addMenu("Acquisition/CL time correlator scan...", self.start)
Esempio n. 17
0
    def __init__(self, name):
        self.name = model.StringVA(name)

        # a thumbnail version of what is displayed
        self.thumbnail = VigilantAttribute(None)  # contains a wx.Image

        # Last time the image of the view was changed. It's actually mostly
        # a trick to allow other parts of the GUI to know when the (theoretical)
        # composited image has changed.
        self.lastUpdate = model.FloatVA(time.time(), unit="s")
Esempio n. 18
0
    def __init__(self, microscope, main_app):
        super(RGBCLIntensity, self).__init__(microscope, main_app)

        # Can only be used on a SPARC with a CL-intensity detector
        if not microscope:
            return
        try:
            self.ebeam = model.getComponent(role="e-beam")
            self.cldetector = model.getComponent(role="cl-detector")
            self.filterwheel = model.getComponent(role="cl-filter")
            self.sed = model.getComponent(role="se-detector")
            # We could also check the filter wheel has at least 3 filters, but
            # let's not be too picky, if the user has installed the plugin, he
            # probably wants to use it anyway.
        except LookupError:
            logging.info("Hardware not found, cannot use the RGB CL plugin")
            return

        # The SEM survey and CLi stream (will be updated when showing the window)
        self._survey_s = None
        self._cl_int_s = None
        self._acqui_tab = main_app.main_data.getTabByName(
            "sparc_acqui").tab_data_model

        # The settings to be displayed in the dialog
        # TODO: pick better default filters than first 3 filters
        # => based on the wavelengths fitting best RGB, or the names (eg, "Blue"),
        # and avoid "pass-through".
        fbchoices = self.filterwheel.axes["band"].choices
        if isinstance(fbchoices, dict):
            fbvalues = sorted(fbchoices.keys())
        else:
            fbvalues = fbchoices
        # FloatEnumerated because filter positions can be in rad (ie, not int positions)
        self.filter1 = model.FloatEnumerated(fbvalues[0], choices=fbchoices)
        self.filter2 = model.FloatEnumerated(fbvalues[min(
            1,
            len(fbvalues) - 1)],
                                             choices=fbchoices)
        self.filter3 = model.FloatEnumerated(fbvalues[min(
            2,
            len(fbvalues) - 1)],
                                             choices=fbchoices)

        self._filters = [self.filter1, self.filter2, self.filter3]
        self._colours = [(0, 0, 255), (0, 255, 0), (255, 0, 0)]  # B, G, R

        self.filename = model.StringVA("a.tiff")
        self.expectedDuration = model.VigilantAttribute(1,
                                                        unit="s",
                                                        readonly=True)

        self.addMenu("Acquisition/RGB CL intensity...", self.start)
Esempio n. 19
0
 def __init__(self, name, coordinates):
     """
     :param name: (str) name of the ROC
     :param coordinates: (float, float, float, float) l, t, r, b coordinates in m
     """
     self.name = model.StringVA(name)
     self.coordinates = model.TupleContinuous(coordinates,
                                              range=((-1, -1, -1, -1),
                                                     (1, 1, 1, 1)),
                                              cls=(int, float),
                                              unit='m')
     self.parameters = None  # calibration object with all relevant parameters
Esempio n. 20
0
    def __init__(self, microscope, main_app):
        super(SimplePlugin, self).__init__(microscope, main_app)
        # if not microscope:
        #     return

        self.main_data = self.main_app.main_data
        #         if not self.main_data.ccd:
        #             return

        self.addMenu("Acquisition/Fancy acquisition...", self.start)
        self.exposureTime = model.FloatContinuous(2, (0, 10), unit="s")
        self.filename = model.StringVA("boo.h5")
Esempio n. 21
0
 def __init__(self, name, coordinates, roc):
     """
     :param name: (str) name of the ROA
     :param coordinates: (float, float, float, float) l, t, r, b coordinates in m
     :param roc: (FastEMROC) corresponding region of calibration
     """
     self.name = model.StringVA(name)
     self.coordinates = model.TupleContinuous(coordinates,
                                              range=((-1, -1, -1, -1),
                                                     (1, 1, 1, 1)),
                                              cls=(int, float),
                                              unit='m')
     self.roc = model.VigilantAttribute(roc)
Esempio n. 22
0
    def __init__(self, microscope, main_app):
        super(MergeChannelsPlugin, self).__init__(microscope, main_app)

        self.filenameR = model.StringVA(" ")
        self.filenameG = model.StringVA(" ")
        self.filenameB = model.StringVA(" ")
        self.redShiftX = model.FloatContinuous(0, range=(-500, 500), unit="px")
        self.redShiftY = model.FloatContinuous(0, range=(-500, 500), unit="px")
        self.greenShiftX = model.FloatContinuous(0,
                                                 range=(-500, 500),
                                                 unit="px")
        self.greenShiftY = model.FloatContinuous(0,
                                                 range=(-500, 500),
                                                 unit="px")
        self.blueShiftX = model.FloatContinuous(0,
                                                range=(-500, 500),
                                                unit="px")
        self.blueShiftY = model.FloatContinuous(0,
                                                range=(-500, 500),
                                                unit="px")
        self.cropBottom = model.IntContinuous(0, range=(0, 200), unit="px")

        analysis_tab = self.main_app.main_data.getTabByName('analysis')
        analysis_tab.stream_bar_controller.add_action("Add RGB channels...",
                                                      self.start)

        self.filenameR.subscribe(self._filenameR)
        self.filenameG.subscribe(self._filenameG)
        self.filenameB.subscribe(self._filenameB)
        self.cropBottom.subscribe(self._cropBottom)

        self._subscribers = []
        self._dlg = None
        self._stream_red = None
        self._stream_green = None
        self._stream_blue = None
        self._raw_orig = {
        }  # dictionary (Stream -> DataArray) to handle the (un)cropping
Esempio n. 23
0
    def __init__(self,
                 name,
                 coordinates,
                 roc_2,
                 roc_3,
                 asm,
                 multibeam,
                 descanner,
                 detector,
                 overlap=0,
                 pre_calibrate=False):
        """
        :param name: (str) Name of the region of acquisition (ROA). It is the name of the megafield (id) as stored on
                     the external storage.
        :param coordinates: (float, float, float, float) xmin, ymin, xmax, ymax
                            Bounding box coordinates of the ROA in [m]. The coordinates are in the sample carrier
                            coordinate system, which corresponds to the component with role='stage'.
        :param roc_2: (FastEMROC) Corresponding region of calibration (ROC). Used for dark offset and digital
                      gain calibration.
        :param roc_3: (FastEMROC) Corresponding region of calibration (ROC). Used for the final scanner rotation,
                      final scanning amplitude and cell translation (cell stitching) calibration (field corrections).
        :param asm: (technolution.AcquisitionServer) The acquisition server module component.
        :param multibeam: (technolution.EBeamScanner) The multibeam scanner component of the acquisition server module.
        :param descanner: (technolution.MirrorDescanner) The mirror descanner component of the acquisition server module.
        :param detector: (technolution.MPPC) The detector object to be used for collecting the image data.
        :param overlap: (float), optional
            The amount of overlap required between single fields. An overlap of 0.2 means that two neighboring fields
            overlap by 20%. By default, the overlap is 0, this means there is no overlap and one field is exactly next
            to the neighboring field.
        :param pre_calibrate: (bool) If True run pre-calibrations before each ROA acquisition.
        """
        self.name = model.StringVA(name)
        self.coordinates = model.TupleContinuous(coordinates,
                                                 range=((-1, -1, -1, -1),
                                                        (1, 1, 1, 1)),
                                                 cls=(int, float),
                                                 unit='m')
        self.roc_2 = model.VigilantAttribute(roc_2)
        self.roc_3 = model.VigilantAttribute(roc_3)
        self._asm = asm
        self._multibeam = multibeam
        self._descanner = descanner
        self._detector = detector

        # List of tuples(int, int) containing the position indices of each field to be acquired.
        # Automatically updated when the coordinates change.
        self.field_indices = []
        self.overlap = overlap
        self.pre_calibrate = pre_calibrate
        self.coordinates.subscribe(self.on_coordinates, init=True)
Esempio n. 24
0
 def __init__(self, name, coordinates):
     """
     :param name: (str) Name of the region of calibration (ROC). It is the name of the megafield (id) as stored on
                  the external storage.
     :param coordinates: (float, float, float, float) left, top, right, bottom, Bounding box coordinates of the
                         ROC in [m]. The coordinates are in the sample carrier coordinate system, which
                         corresponds to the component with role='stage'.
     """
     self.name = model.StringVA(name)
     self.coordinates = model.TupleContinuous(coordinates,
                                              range=((-1, -1, -1, -1),
                                                     (1, 1, 1, 1)),
                                              cls=(int, float),
                                              unit='m')
     self.parameters = None  # calibration object with all relevant parameters
Esempio n. 25
0
    def __init__(self, microscope, main_app):
        super(TileAcqPlugin, self).__init__(microscope, main_app)

        self._dlg = None
        self._tab = None  # the acquisition tab
        self.ft = model.InstantaneousFuture()  # acquisition future
        self.microscope = microscope

        # Can only be used with a microscope
        if not microscope:
            return
        else:
            # Check if microscope supports tiling (= has a sample stage)
            main_data = self.main_app.main_data
            if main_data.stage:
                self.addMenu("Acquisition/Tile...\tCtrl+G", self.show_dlg)
            else:
                logging.info(
                    "Tile acquisition not available as no stage present")
                return

        self._ovrl_stream = None  # stream for fine alignment

        self.nx = model.IntContinuous(5, (1, 1000), setter=self._set_nx)
        self.ny = model.IntContinuous(5, (1, 1000), setter=self._set_ny)
        self.overlap = model.FloatContinuous(20, (-80, 80), unit="%")
        self.angle = model.FloatContinuous(0, (-90, 90), unit=u"°")
        self.filename = model.StringVA("a.ome.tiff")
        self.expectedDuration = model.VigilantAttribute(1,
                                                        unit="s",
                                                        readonly=True)
        self.totalArea = model.TupleVA((1, 1), unit="m", readonly=True)
        self.stitch = model.BooleanVA(True)
        self.fineAlign = model.BooleanVA(False)
        # TODO: manage focus (eg, autofocus or ask to manual focus on the corners
        # of the ROI and linearly interpolate)

        self.nx.subscribe(self._update_exp_dur)
        self.ny.subscribe(self._update_exp_dur)
        self.fineAlign.subscribe(self._update_exp_dur)
        self.nx.subscribe(self._update_total_area)
        self.ny.subscribe(self._update_total_area)
        self.overlap.subscribe(self._update_total_area)

        # Warn if memory will be exhausted
        self.nx.subscribe(self._memory_check)
        self.ny.subscribe(self._memory_check)
        self.stitch.subscribe(self._memory_check)
Esempio n. 26
0
    def __init__(self, microscope, main_app):
        super(TimelapsePlugin, self).__init__(microscope, main_app)
        # Can only be used with a microscope
        if not microscope:
            return
        else:
            # Check which stream the microscope supports
            main_data = self.main_app.main_data
            # TODO: also support backscatter detector
            if main_data.sed and main_data.ebeam:
                self.addMenu("Acquisition/Timelapse SEM...", self.start_sem)
            if main_data.ccd and main_data.light:
                self.addMenu("Acquisition/Timelapse Fluorescence...",
                             self.start_fluo)

        self.period = model.FloatContinuous(10, (1e-3, 10000), unit="s")
        self.numberOfAcquisitions = model.IntContinuous(100, (2, 1000))
        self.filename = model.StringVA("a.h5")

        self._stream = None
Esempio n. 27
0
    def __init__(self, microscope, main_app):
        super(CLAcqPlugin, self).__init__(microscope, main_app)
        # Can only be used with a microscope
        if not microscope:
            return
        else:
            # Check which stream the microscope supports
            main_data = self.main_app.main_data
            if not (main_data.ccd and main_data.ebeam):
                return

        self.exposureTime = main_data.ccd.exposureTime
        self.binning = main_data.ccd.binning
        self.xres = model.IntContinuous(10, (1, 1000), unit="px")
        self.yres = model.IntContinuous(10, (1, 1000), unit="px")
        self.stepsize = model.FloatVA(1e-6, unit="m")  # Just to show
        self.filename = model.StringVA("a.tiff")

        self.xres.subscribe(self._update_stepsize)
        self.yres.subscribe(self._update_stepsize)

        self.addMenu("Acquisition/CL acquisition...", self.start)
Esempio n. 28
0
    def __init__(self, name, detector, dataflow, emitter, focuser=None, opm=None,
                 hwdetvas=None, hwemtvas=None, detvas=None, emtvas=None, raw=None,
                 acq_type=None):
        """
        name (string): user-friendly name of this stream
        detector (Detector): the detector which has the dataflow
        dataflow (Dataflow): the dataflow from which to get the data
        emitter (Emitter): the emitter
        opm (OpticalPathManager): the optical path manager
        focuser (Actuator or None): an actuator with a 'z' axis that allows to change
          the focus
        hwdetvas (None or set of str): names of all detector hardware VAs to be controlled by this
            Stream
        hwemtvas (None or set of str): names of all emitter hardware VAs to be controlled by this
            Stream
        detvas (None or set of str): names of all the detector VigilantAttributes
          (VAs) to be duplicated on the stream. They will be named .detOriginalName
        emtvas (None or set of str): names of all the emitter VAs to be
          duplicated on the stream. They will be named .emtOriginalName
        raw (None or list of DataArrays or DataArrayShadow): raw data to be used
          at initialisation. By default, it will contain no data.
        acq_type (MD_AT_*): acquisition type associated with this stream (as in model._metadata)
        """
        self.name = model.StringVA(name)
        self.acquisitionType = model.VigilantAttribute(acq_type)  # MD_ACQ_TYPE or None
        # for identification of the acquisition type associated with the stream

        # Hardware Components
        self._detector = detector
        self._emitter = emitter
        self._focuser = focuser
        self._opm = opm

        # Dataflow (Live image stream with meta data)
        # Note: A Detector can have multiple dataflows, so that's why a Stream
        # has a separate attribute.
        self._dataflow = dataflow

        # TODO: We need to reorganise everything so that the
        # image display is done via a dataflow (in a separate thread), instead
        # of a VA.
        self._im_needs_recompute = threading.Event()
        self._init_thread()

        # list of DataArray(Shadow) received and used to generate the image
        # every time it's modified, image is also modified
        if raw is None:
            self.raw = []
        else:
            self.raw = raw

        # initialize the projected tiles cache
        self._projectedTilesCache = {}
        # initialize the raw tiles cache
        self._rawTilesCache = {}

        # TODO: should better be based on a BufferedDataFlow: subscribing starts
        # acquisition and sends (raw) data to whoever is interested. .get()
        # returns the previous or next image acquired.

        # indicating if stream has already been prepared
        self._prepared = False
        # TODO: should_update is a GUI stuff => move away from stream
        # should_update has no effect direct effect, it's just a flag to
        # indicate the user would like to have the stream updated (live)
        self.should_update = model.BooleanVA(False)
        # is_active set to True will keep the acquisition going on
        self.is_active = model.BooleanVA(False, setter=self._is_active_setter)

        # Leech to use during acquisition.
        # Note: for now only some streams actually use them (MDStreams*)
        self.leeches = []

        # Hardware VA that the stream is directly linked to
        self.hw_vas = {}
        self.hw_vas.update(self._getVAs(detector, hwdetvas or set()))
        self.hw_vas.update(self._getVAs(emitter, hwemtvas or set()))

        # Duplicate VA if requested
        self._hwvas = {}  # str (name of the proxied VA) -> original Hw VA
        self._hwvasetters = {}  # str (name of the proxied VA) -> setter
        self._lvaupdaters = {}  # str (name of the proxied VA) -> listener

        self._det_vas = self._duplicateVAs(detector, "det", detvas or set())
        self._emt_vas = self._duplicateVAs(emitter, "emt", emtvas or set())

        self._dRangeLock = threading.Lock()
        self._drange = None  # min/max data range, or None if unknown
        self._drange_unreliable = True  # if current values are a rough guess (based on detector)

        # drange_raw is the smaller (less zoomed) image of an pyramidal image. It is used
        # instead of the full image because it would be too slow or even impossible to read
        # the full data from the image to the memory. It is also not the tiles from the tiled
        # image, so the code for pyramidal and non-pyramidal images
        # that reads drange_raw is the same.
        # The drawback of not using the full image, is that some of the pixels are lost, so
        # maybe the max/min of the smaller image is different from the min/max of the full image.
        # And the histogram of both images will probably be a bit different also.
        if raw and isinstance(raw[0], model.DataArrayShadow):
            # if the image is pyramidal, use the smaller image
            drange_raw = self._getMergedRawImage(raw[0], raw[0].maxzoom)
        else:
            drange_raw = None

        # TODO: move to the DataProjection class
        self.auto_bc = model.BooleanVA(True)
        self.auto_bc.subscribe(self._onAutoBC)

        # % of values considered outliers discarded in auto BC detection
        # Note: 1/256th is a nice value because on RGB, it means in degenerated
        # cases (like flat histogram), you still loose only one value on each
        # side.
        self.auto_bc_outliers = model.FloatContinuous(100 / 256, range=(0, 40))
        self.auto_bc_outliers.subscribe(self._onOutliers)

        self.tint = model.ListVA((255, 255, 255), unit="RGB")  # 3-int R,G,B

        # Used if auto_bc is False
        # min/max ratio of the whole intensity level which are mapped to
        # black/white. Its range is ._drange (will be updated by _updateDRange)
        self.intensityRange = model.TupleContinuous((0, 0),
                                                    range=((0, 0), (1, 1)),
                                                    cls=(int, long, float),
                                                    setter=self._setIntensityRange)
        # Make it so that the value gets clipped when its range is updated and
        # the value is outside of it.
        self.intensityRange.clip_on_range = True
        self._updateDRange(drange_raw)  # sets intensityRange
        self._init_projection_vas()

        # Histogram of the current image _or_ slightly older image.
        # Note it's an ndarray. Use .tolist() to get a python list.
        self.histogram = model.VigilantAttribute(numpy.empty(0), readonly=True)
        self.histogram._full_hist = numpy.ndarray(0) # for finding the outliers
        self.histogram._edges = None

        # Tuple of (int, str) or (None, None): loglevel and message
        self.status = model.VigilantAttribute((None, None), readonly=True)

        # Background data, to be subtracted from the acquisition data before
        # projection. It should be the same shape and dtype as the acquisition
        # data, otherwise no subtraction will be performed. If None, nothing is
        # subtracted is applied.
        self.background = model.VigilantAttribute(None, setter=self._setBackground)
        self.background.subscribe(self._onBackground)

        # if there is already some data, update image with it
        # TODO: have this done by the child class, if needed.
        if self.raw:
            self._updateHistogram(drange_raw)
            self._onNewData(None, self.raw[0])
Esempio n. 29
0
    def __init__(self, parent, orig_tab_data):
        xrcfr_acq.__init__(self, parent)

        self.conf = get_acqui_conf()

        for n in presets:
            self.cmb_presets.Append(n)
        # TODO: record and reuse the preset used?
        self.cmb_presets.Select(0)

        self.filename = model.StringVA(create_filename(self.conf.last_path, self.conf.fn_ptn,
                                                       self.conf.last_extension, self.conf.fn_count))
        self.filename.subscribe(self._onFilename, init=True)

        # The name of the last file that got written to disk (used for auto viewing on close)
        self.last_saved_file = None
        
        # True when acquisition occurs
        self.acquiring = False

        # a ProgressiveFuture if the acquisition is going on
        self.acq_future = None
        self._acq_future_connector = None

        self._main_data_model = orig_tab_data.main

        # duplicate the interface, but with only one view
        self._tab_data_model = self.duplicate_tab_data_model(orig_tab_data)

        # Create a new settings controller for the acquisition dialog
        self._settings_controller = SecomSettingsController(
            self,
            self._tab_data_model,
            highlight_change=True  # also adds a "Reset" context menu
        )

        orig_view = orig_tab_data.focussedView.value
        self._view = self._tab_data_model.focussedView.value
        self._hidden_view = StreamView("Plugin View Hidden")

        self.streambar_controller = StreamBarController(self._tab_data_model,
                                                        self.pnl_secom_streams,
                                                        static=True,
                                                        ignore_view=True)
        # The streams currently displayed are the one visible
        self.add_all_streams()

        # The list of streams ready for acquisition (just used as a cache)
        self._acq_streams = {}

        # FIXME: pass the fold_panels

        # Compute the preset values for each preset
        self._preset_values = {}  # dict string -> dict (SettingEntries -> value)
        self._orig_entries = get_global_settings_entries(self._settings_controller)
        for sc in self.streambar_controller.stream_controllers:
            self._orig_entries += get_local_settings_entries(sc)
        self._orig_settings = preset_as_is(self._orig_entries)  # to detect changes
        for n, preset in presets.items():
            self._preset_values[n] = preset(self._orig_entries)
        # Presets which have been confirmed on the hardware
        self._presets_confirmed = set() # (string)

        self.start_listening_to_va()

        # If it could be possible to do fine alignment, allow the user to choose
        if self._can_fine_align(self._tab_data_model.streams.value):
            self.chkbox_fine_align.Show()
            # Set to True to make it the default, but will be automatically
            # disabled later if the current visible streams don't allow it.
            self.chkbox_fine_align.Value = True

            for s in self._tab_data_model.streams.value:
                if isinstance(s, EMStream):
                    em_det = s.detector
                    em_emt = s.emitter
                elif isinstance(s, OpticalStream) and not isinstance(s, ScannedFluoStream):
                    opt_det = s.detector
            self._ovrl_stream = stream.OverlayStream("Fine alignment", opt_det, em_emt, em_det,
                                                     opm=self._main_data_model.opm)
            self._ovrl_stream.dwellTime.value = self._main_data_model.fineAlignDwellTime.value
        else:
            self.chkbox_fine_align.Show(False)
            self.chkbox_fine_align.Value = False

        self._prev_fine_align = self.chkbox_fine_align.Value

        # make sure the view displays the same thing as the one we are
        # duplicating
        self._view.view_pos.value = orig_view.view_pos.value
        self._view.mpp.value = orig_view.mpp.value
        self._view.merge_ratio.value = orig_view.merge_ratio.value

        # attach the view to the viewport
        self.pnl_view_acq.canvas.fit_view_to_next_image = False
        self.pnl_view_acq.setView(self._view, self._tab_data_model)

        # The TOOL_ROA is not present because we don't allow the user to change
        # the ROA), so we need to explicitly request the canvas to show the ROA.
        if hasattr(self._tab_data_model, "roa") and self._tab_data_model.roa is not None:
            cnvs = self.pnl_view_acq.canvas
            self.roa_overlay = RepetitionSelectOverlay(cnvs, self._tab_data_model.roa,
                                                             self._tab_data_model.fovComp)
            cnvs.add_world_overlay(self.roa_overlay)

        self.Bind(wx.EVT_CHAR_HOOK, self.on_key)

        self.btn_cancel.Bind(wx.EVT_BUTTON, self.on_close)
        self.btn_change_file.Bind(wx.EVT_BUTTON, self.on_change_file)
        self.btn_secom_acquire.Bind(wx.EVT_BUTTON, self.on_acquire)
        self.cmb_presets.Bind(wx.EVT_COMBOBOX, self.on_preset)
        self.Bind(wx.EVT_CLOSE, self.on_close)
        # on_streams_changed is compatible because it doesn't use the args
        self.chkbox_fine_align.Bind(wx.EVT_CHECKBOX, self.on_streams_changed)

        self.on_preset(None) # will force setting the current preset

        # To update the estimated time when streams are removed/added
        self._view.stream_tree.flat.subscribe(self.on_streams_changed)
        self._hidden_view.stream_tree.flat.subscribe(self.on_streams_changed)
Esempio n. 30
0
    def __init__(self, microscope, main_app):
        super(QuickCLPlugin, self).__init__(microscope, main_app)
        # Can only be used with a SPARC with CL detector (or monochromator)
        if not microscope:
            return
        main_data = self.main_app.main_data
        if not main_data.ebeam or not (main_data.cld
                                       or main_data.monochromator):
            return

        self.conf = get_acqui_conf()
        self.filename = model.StringVA("")
        self.filename.subscribe(self._on_filename)

        self.expectedDuration = model.VigilantAttribute(1,
                                                        unit="s",
                                                        readonly=True)

        self.hasDatabar = model.BooleanVA(False)

        # Only put the VAs that do directly define the image as local, everything
        # else should be global. The advantage is double: the global VAs will
        # set the hardware even if another stream (also using the e-beam) is
        # currently playing, and if the VAs are changed externally, the settings
        # will be displayed correctly (and not reset the values on next play).
        emtvas = set()
        hwemtvas = set()
        for vaname in get_local_vas(main_data.ebeam,
                                    main_data.hw_settings_config):
            if vaname in ("resolution", "dwellTime", "scale"):
                emtvas.add(vaname)
            else:
                hwemtvas.add(vaname)

        self._sem_stream = stream.SEMStream(
            "Secondary electrons",
            main_data.sed,
            main_data.sed.data,
            main_data.ebeam,
            focuser=main_data.ebeam_focus,
            hwemtvas=hwemtvas,
            hwdetvas=None,
            emtvas=emtvas,
            detvas=get_local_vas(main_data.sed, main_data.hw_settings_config),
        )

        # This stream is used both for rendering and acquisition.
        # LiveCLStream is more or less like a SEMStream, but ensures the icon in
        # the merge slider is correct, and provide a few extra.
        if main_data.cld:
            self._cl_stream = LiveCLStream(
                "CL intensity",
                main_data.cld,
                main_data.cld.data,
                main_data.ebeam,
                focuser=main_data.ebeam_focus,
                emtvas=emtvas,
                detvas=get_local_vas(main_data.cld,
                                     main_data.hw_settings_config),
                opm=main_data.opm,
            )
            # TODO: allow to type in the resolution of the CL?
            # TODO: add the cl-filter axis (or reset it to pass-through?)
            self.logScale = self._cl_stream.logScale

            if hasattr(self._cl_stream, "detGain"):
                self._cl_stream.detGain.subscribe(self._on_cl_gain)

            # Update the acquisition time when it might change (ie, the scan settings
            # change)
            self._cl_stream.emtDwellTime.subscribe(self._update_exp_dur)
            self._cl_stream.emtResolution.subscribe(self._update_exp_dur)

        # Note: for now we don't really support SPARC with BOTH CL-detector and
        # monochromator.
        if main_data.monochromator:
            self._mn_stream = LiveCLStream(
                "Monochromator",
                main_data.monochromator,
                main_data.monochromator.data,
                main_data.ebeam,
                focuser=main_data.ebeam_focus,
                emtvas=emtvas,
                detvas=get_local_vas(main_data.monochromator,
                                     main_data.hw_settings_config),
                opm=main_data.opm,
            )
            self._mn_stream.emtDwellTime.subscribe(self._update_exp_dur)
            self._mn_stream.emtResolution.subscribe(self._update_exp_dur)

            # spg = self._getAffectingSpectrograph(main_data.spectrometer)
            # TODO: show axes

        self._dlg = None

        self.addMenu("Acquisition/Quick CL...\tF2", self.start)