Exemplo n.º 1
0
 def __init__(self):
     try:
         # Initialize VISA resource manager, connect to Matisse and wavemeter, clear any errors.
         self._instrument = ResourceManager().open_resource(
             cfg.get(cfg.MATISSE_DEVICE_ID))
         self.target_wavelength = None
         self._stabilization_thread = None
         self._lock_correction_thread = None
         self._plotting_processes = []
         self.exit_flag = False
         self._scan_attempts = 0
         self._force_large_scan = True
         self._restart_set_wavelength = False
         self.is_setting_wavelength = False
         self.is_scanning_bifi = False
         self.is_scanning_thin_etalon = False
         self.stabilization_auto_corrections = 0
         self.query('ERROR:CLEAR')  # start with a clean slate
         self.query('MOTORBIREFRINGENT:CLEAR')
         self.query('MOTORTHINETALON:CLEAR')
         self._wavemeter = WaveMaster(cfg.get(cfg.WAVEMETER_PORT))
     except VisaIOError as ioerr:
         raise IOError(
             "Can't reach Matisse. Make sure it's on and connected via USB."
         ) from ioerr
Exemplo n.º 2
0
    def set_wavelength_dialog(self, checked):
        """
        Open a dialog to set the wavelength of the Matisse. If the difference in wavleength is greater than or equal to
        the wavelength change threshold, display a warning to confirm the change.
        """
        current_wavelength = self.matisse.target_wavelength
        if current_wavelength is None:
            current_wavelength = self.matisse.wavemeter_wavelength()
        target_wavelength, success = QInputDialog.getDouble(
            self.window,
            'Set Wavelength',
            'Wavelength (nm): ',
            current_wavelength,
            decimals=3,
            min=cfg.get(cfg.WAVELENGTH_LOWER_LIMIT),
            max=cfg.get(cfg.WAVELENGTH_UPPER_LIMIT))
        if success:
            if abs(current_wavelength - target_wavelength
                   ) >= ControlApplication.CONFIRM_WAVELENGTH_CHANGE_THRESHOLD:
                answer = QMessageBox.warning(
                    self.window,
                    'Large Wavelength Change',
                    f"The desired wavelength, {target_wavelength} nm, is more than "
                    f"{ControlApplication.CONFIRM_WAVELENGTH_CHANGE_THRESHOLD} nm "
                    'away from the current wavelength. Are you sure?',
                    QMessageBox.Yes | QMessageBox.No,
                    defaultButton=QMessageBox.No)
                if answer == QMessageBox.No:
                    return

            print(f"Setting wavelength to {target_wavelength} nm...")
            self.run_matisse_task(self.matisse.set_wavelength,
                                  target_wavelength)
    def setup_form(self):
        form_layout = QFormLayout()
        data_file_selection_layout = QHBoxLayout()
        self.data_file_button = QPushButton('Select File')
        self.data_file_label = QLabel()
        data_file_selection_layout.addWidget(self.data_file_button)
        data_file_selection_layout.addWidget(self.data_file_label)
        form_layout.addRow('Existing Data: ', data_file_selection_layout)

        line = QFrame()
        line.setFrameShape(QFrame.HLine)
        line.setFrameShadow(QFrame.Sunken)
        form_layout.addRow(line)

        self.exposure_time_field = QDoubleSpinBox()
        self.exposure_time_field.setMinimum(0)
        self.exposure_time_field.setDecimals(4)
        form_layout.addRow('Exposure time (s): ', self.exposure_time_field)
        self.layout.addLayout(form_layout)

        # We need to make sure the Andor libraries are loaded to access the spectrometer
        ple.PLE.load_andor_libs()

        self.center_wavelength_field = QDoubleSpinBox()
        self.center_wavelength_field.setMinimum(cfg.get(cfg.WAVELENGTH_LOWER_LIMIT))
        self.center_wavelength_field.setMaximum(cfg.get(cfg.WAVELENGTH_UPPER_LIMIT))
        self.center_wavelength_field.setValue(ple.shamrock.get_center_wavelength())
        form_layout.addRow('Center wavelength (nm): ', self.center_wavelength_field)

        self.grating_grooves_field = QComboBox()
        for groove_num in ple.shamrock.gratings.keys():
            self.grating_grooves_field.addItem(f"{groove_num}", ple.shamrock.gratings[groove_num])
        self.grating_grooves_field.setCurrentText(str(ple.shamrock.get_grating_grooves()))
        form_layout.addRow('Grating grooves: ', self.grating_grooves_field)
    def run(self):
        while True:
            if self.messages.qsize() == 0:
                try:
                    bifi_pos = self.matisse.query('MOTBI:POS?',
                                                  numeric_result=True)
                    thin_eta_pos = self.matisse.query('MOTTE:POS?',
                                                      numeric_result=True)
                    refcell_pos, pz_eta_pos, slow_pz_pos = self.matisse.get_stabilizing_piezo_positions(
                    )
                    is_stabilizing = self.matisse.is_stabilizing()
                    is_scanning = self.matisse.is_scanning()
                    is_locked = self.matisse.laser_locked()
                    wavemeter_value = self.matisse.wavemeter_raw_value()

                    bifi_pos_text = f"BiFi:{bifi_pos}"
                    thin_eta_pos_text = f"Thin Eta:{thin_eta_pos}"
                    pz_eta_pos_text = f"Pz Eta:{pz_eta_pos:.3f}"
                    slow_pz_pos_text = f"Slow Pz:{slow_pz_pos:.3f}"
                    refcell_pos_text = f"RefCell:{refcell_pos:.3f}"
                    stabilizing_text = f"Stabilize:{green_text('ON') if is_stabilizing else red_text('OFF')}"
                    scanning_text = f"Scanning:{green_text('ON') if is_scanning else red_text('OFF')}"
                    locked_text = f"{green_text('LOCKED') if is_locked else red_text('NO LOCK')}"
                    wavemeter_text = f"Wavemeter:{wavemeter_value}"

                    limit_offset = cfg.get(cfg.COMPONENT_LIMIT_OFFSET)
                    refcell_at_limit = not matisse.REFERENCE_CELL_LOWER_LIMIT + limit_offset < refcell_pos < matisse.REFERENCE_CELL_UPPER_LIMIT - limit_offset
                    slow_pz_at_limit = not matisse.SLOW_PIEZO_LOWER_LIMIT + limit_offset < slow_pz_pos < matisse.SLOW_PIEZO_UPPER_LIMIT - limit_offset
                    pz_eta_at_limit = not matisse.PIEZO_ETALON_LOWER_LIMIT + limit_offset < pz_eta_pos < matisse.PIEZO_ETALON_UPPER_LIMIT - limit_offset
                    warn_offset = limit_offset * 2
                    refcell_near_limit = not matisse.REFERENCE_CELL_LOWER_LIMIT + warn_offset < refcell_pos < matisse.REFERENCE_CELL_UPPER_LIMIT - warn_offset
                    slow_pz_near_limit = not matisse.SLOW_PIEZO_LOWER_LIMIT + warn_offset < slow_pz_pos < matisse.SLOW_PIEZO_UPPER_LIMIT - warn_offset
                    pz_eta_near_limit = not matisse.PIEZO_ETALON_LOWER_LIMIT + warn_offset < pz_eta_pos < matisse.PIEZO_ETALON_UPPER_LIMIT - warn_offset

                    if refcell_at_limit:
                        refcell_pos_text = red_text(refcell_pos_text)
                    elif refcell_near_limit:
                        refcell_pos_text = orange_text(refcell_pos_text)

                    if slow_pz_at_limit:
                        slow_pz_pos_text = red_text(slow_pz_pos_text)
                    elif slow_pz_near_limit:
                        slow_pz_pos_text = orange_text(slow_pz_pos_text)

                    if pz_eta_at_limit:
                        pz_eta_pos_text = red_text(pz_eta_pos_text)
                    elif pz_eta_near_limit:
                        pz_eta_pos_text = orange_text(pz_eta_pos_text)

                    status = f"{bifi_pos_text} | {thin_eta_pos_text} | {pz_eta_pos_text} | {slow_pz_pos_text} | {refcell_pos_text} | {stabilizing_text} | {scanning_text} | {locked_text} | {wavemeter_text}"
                except Exception:
                    status = red_text(
                        'Error reading system status. Please restart if this issue persists.'
                    )
                self.status_read.emit(status)
                time.sleep(cfg.get(cfg.STATUS_MONITOR_DELAY))
            else:
                break
Exemplo n.º 5
0
    def run(self):
        with ControlLoopsOn(self.matisse):
            self.timer.start()
            while True:
                if self.messages.qsize() == 0:
                    if self.matisse.fast_piezo_locked():
                        self.timer.cancel()
                        if self.matisse.is_any_limit_reached():
                            print(
                                'WARNING: A component has hit a limit while the laser is locked. '
                                'Attempting automatic corrections.')
                            if cfg.get(cfg.REPORT_EVENTS):
                                current_wavelength = self.matisse.wavemeter_wavelength(
                                )
                                log_event(
                                    EventType.LOCK_CORRECTION, self.matisse,
                                    current_wavelength,
                                    'component hit a limit while laser was locked'
                                )
                            self.matisse.reset_stabilization_piezos()
                    else:
                        self.restart_timer()
                        if self.matisse.is_any_limit_reached():
                            print(
                                'WARNING: A component has hit a limit before the laser could lock. '
                                'Stopping control loops. ' +
                                LockCorrectionThread.UNABLE_TO_LOCK_MESSAGE)
                            self.timer.cancel()
                            break

                    time.sleep(1)
                else:
                    self.timer.cancel()
                    break
Exemplo n.º 6
0
    def set_recommended_fast_piezo_setpoint(self):
        """
        Analyze the data from the reference cell transmission spectrum, and set the fast piezo setpoint to a point
        about halfway between the min and max points on the spectrum. The recommended value is determined by averaging
        a number of scans given by cfg.FAST_PZ_SETPOINT_NUM_SCANS.

        If this doesn't help to lock the laser, use the 'RefCell Properties Measurement' feature in Matisse Commander
        to set the fast piezo setpoint instead.

        This stops auto-stabilization or any reference cell scans currently running and temporarily enables all
        component control loops.
        """
        if self.is_stabilizing():
            self.stabilize_off()
        self.stop_scan()

        with ControlLoopsOn(self):
            num_scans = cfg.get(cfg.FAST_PZ_SETPOINT_NUM_SCANS)
            total = 0
            for i in range(0, num_scans):
                positions, values = self.get_reference_cell_transmission_spectrum(
                )
                setpoint = (np.max(values) + np.min(values)) / 2
                total += setpoint
            recommended_setpoint = total / num_scans
            print(f"Setting fast piezo setpoint to {recommended_setpoint}")
            self.query(f"FASTPIEZO:CONTROLSETPOINT {recommended_setpoint}")
Exemplo n.º 7
0
 def lock_at_wavelength(self, wavelength: float):
     """Try to lock the Matisse at a given wavelength, waiting to return until we're within a small tolerance."""
     tolerance = 10**-cfg.get(cfg.WAVEMETER_PRECISION)
     self.matisse.set_wavelength(wavelength)
     while abs(wavelength - self.matisse.wavemeter_wavelength()) >= tolerance or \
             (self.matisse.is_setting_wavelength or self.matisse.is_scanning_bifi or self.matisse.is_scanning_thin_etalon):
         if self.ple_exit_flag:
             break
         time.sleep(3)
 def do_stabilization_correction(self, wavelength):
     """Reset the stabilization piezos and optionally log the correction event."""
     print('WARNING: A component has hit a limit while adjusting the RefCell. Attempting automatic corrections.')
     self._matisse.stop_scan()
     if cfg.get(cfg.REPORT_EVENTS):
         log_event(EventType.STABILIZATION_CORRECTION, self._matisse, wavelength,
                   'component hit a limit while auto-stabilization was on')
     self._matisse.reset_stabilization_piezos()
     self._matisse.stabilization_auto_corrections += 1
Exemplo n.º 9
0
    def set_bifi_wavelength(self, value: float):
        """
        Set the birefringent filter motor to the approximate position corresponding to the given wavelength. This
        position is determined by the Matisse.

        Parameters
        ----------
        value : float
            the desired wavelength
        """
        assert cfg.get(cfg.WAVELENGTH_LOWER_LIMIT) < value < cfg.get(cfg.WAVELENGTH_UPPER_LIMIT), \
            'Target wavelength out of range.'
        # Wait for motor to be ready to accept commands
        while not self.bifi_motor_status() == MOTOR_STATUS_IDLE:
            pass
        self.query(f"MOTBI:WAVELENGTH {value}")
        # Wait for motor to finish movement
        while not self.bifi_motor_status() == MOTOR_STATUS_IDLE:
            pass
    def run(self):
        """
        Try to keep the measured wavelength within the configured tolerance by scanning the reference cell.

        If a larger drift in wavelength occurs, we might have fallen into a dip on the power diode curve. To correct
        this, a small BiFi scan and a small thin etalon scan will be performed.

        Exit if anything is pushed to the message queue.
        """
        while True:
            if self.messages.qsize() == 0:
                current_wavelength = self._matisse.wavemeter_wavelength()
                drift = round(current_wavelength - self._matisse.target_wavelength, cfg.get(cfg.WAVEMETER_PRECISION))
                # TODO: This threshold is large, maybe add another config option for this condition
                if abs(drift) > cfg.get(cfg.LARGE_WAVELENGTH_DRIFT):
                    # TODO: Consider logging this event to the event report
                    print(f"WARNING: Wavelength drifted by {drift} nm during stabilization. Making corrections.")
                    self._matisse.stop_scan()
                    if self._matisse.is_lock_correction_on():
                        self._matisse.stop_laser_lock_correction()
                    # TODO: Skip BiFi scan if drift is small enough, kind of like in Matisse.set_wavelength
                    self._matisse.birefringent_filter_scan(scan_range=cfg.get(cfg.BIFI_SCAN_RANGE_SMALL))
                    self._matisse.thin_etalon_scan(scan_range=cfg.get(cfg.THIN_ETA_SCAN_RANGE_SMALL))
                    self._matisse.start_laser_lock_correction()
                elif abs(drift) > cfg.get(cfg.STABILIZATION_TOLERANCE):
                    if drift > 0:
                        # measured wavelength is too high
                        print(f"Wavelength too high, decreasing. Drift is {drift} nm. Refcell is at {self._matisse.query('SCAN:NOW?', numeric_result=True)}")
                        if not self._matisse.is_any_limit_reached():
                            if cfg.get(cfg.REPORT_EVENTS):
                                log_event(EventType.WAVELENGTH_DRIFT, self._matisse, current_wavelength,
                                          f"wavelength drifted by {drift} nm")
                            self._matisse.start_scan(matisse.SCAN_MODE_DOWN)
                        else:
                            self.do_stabilization_correction(current_wavelength)
                    else:
                        # measured wavelength is too low
                        print(f"Wavelength too low, increasing.  Drift is {drift} nm. Refcell is at {self._matisse.query('SCAN:NOW?', numeric_result=True)}")
                        if not self._matisse.is_any_limit_reached():
                            if cfg.get(cfg.REPORT_EVENTS):
                                log_event(EventType.WAVELENGTH_DRIFT, self._matisse, current_wavelength,
                                          f"wavelength drifted by {drift} nm")
                            self._matisse.start_scan(matisse.SCAN_MODE_UP)
                        else:
                            self.do_stabilization_correction(current_wavelength)
                else:
                    self._matisse.stop_scan()
                    # print(f"Within tolerance. Drift is {drift}")
                time.sleep(cfg.get(cfg.STABILIZATION_DELAY))
            else:
                self._matisse.stop_scan()
                break
Exemplo n.º 11
0
    def setup(self,
              exposure_time: float,
              acquisition_mode=ACQ_MODE_SINGLE,
              readout_mode=READ_MODE_FVB,
              temperature=-70,
              cool_down=True):
        """
        Perform setup procedures on CCD, like cooling down to a given temperature and setting acquisition parameters.

        Parameters
        ----------
        exposure_time
            the desired exposure time at which to configure the CCD
        acquisition_mode
            the desired acquisition mode at which to configure the CCD (default is accumulate)
        readout_mode
            the desired readout mode at which to configure the CCD (default is FVB)
        temperature
            the desired temperature in degrees centigrade at which to configure the CCD (default is -70)
        cool_down
            whether to cool down the CCD at all (sometimes we don't care, like when taking a single acquisition)
        """
        self.exit_flag = False
        if cool_down:
            min_temp, max_temp = c_int(), c_int()
            self.lib.GetTemperatureRange(pointer(min_temp), pointer(max_temp))
            min_temp, max_temp = min_temp.value, max_temp.value
            assert min_temp < temperature < max_temp, f"Temperature must be set between {min_temp} and {max_temp}"

            self.lib.SetTemperature(c_int(temperature))
            self.lib.CoolerON()
            # Cooler stops when temp is within 3 degrees of target, so wait until it's close
            # CCD normally takes a few minutes to fully cool down
            while not self.temperature_ok:
                if self.exit_flag:
                    return
                current_temp = self.get_temperature()
                print(
                    f"Cooling CCD. Current temperature is {round(current_temp, 2)} °C"
                )
                self.temperature_ok = current_temp < temperature + cfg.get(
                    cfg.PLE_TEMPERATURE_TOLERANCE)
                time.sleep(10)

        print('Configuring acquisition parameters.')
        self.lib.SetAcquisitionMode(c_int(acquisition_mode))
        self.lib.SetReadMode(c_int(readout_mode))
        self.lib.SetVSSpeed(c_int(1))
        self.lib.SetTriggerMode(c_int(TRIGGER_MODE_INTERNAL))
        self.lib.SetExposureTime(c_float(exposure_time))
        print('CCD ready for acquisition.')
Exemplo n.º 12
0
    def __init__(self):
        try:
            self.lib = load_lib(CCD.LIBRARY_NAME)
            self.lib.Initialize()
            self.lib.SetTemperature(c_int(cfg.get(cfg.PLE_TARGET_TEMPERATURE)))
            self.lib.CoolerON()
            self.temperature_ok = False
            self.exit_flag = False

            num_cameras = c_long()
            self.lib.GetAvailableCameras(pointer(num_cameras))
            assert num_cameras.value > 0, 'No CCD camera found.'
        except OSError as err:
            raise RuntimeError('Unable to initialize Andor CCD API.') from err
Exemplo n.º 13
0
    def get_reference_cell_transmission_spectrum(self):
        """
        Scan the reference cell from cfg.FAST_PZ_SETPOINT_SCAN_LOWER_LIMIT to cfg.FAST_PZ_SETPOINT_SCAN_UPPER_LIMIT,
        measuring the input to the fast piezo for each position. This creates a curve that represents the transmission
        spectrum of the reference cell.

        Returns
        -------
        (ndarray, ndarray)
            the positions and input values measured during the scan
        """
        positions = np.linspace(cfg.get(cfg.FAST_PZ_SETPOINT_SCAN_LOWER_LIMIT),
                                cfg.get(cfg.FAST_PZ_SETPOINT_SCAN_UPPER_LIMIT),
                                cfg.get(cfg.FAST_PZ_SETPOINT_NUM_POINTS))
        values = np.array([])
        old_refcell_pos = self.query(f"SCAN:NOW?", numeric_result=True)
        for pos in positions:
            self.query(f"SCAN:NOW {pos}")
            values = np.append(
                values, self.query('FASTPIEZO:INPUT?', numeric_result=True))
        self.query(f"SCAN:NOW {old_refcell_pos}")

        return positions, values
Exemplo n.º 14
0
 def is_any_limit_reached(self):
     """
     Returns
     -------
     bool
         whether any of the stabilization piezos are very close to their limits
     """
     refcell_pos, pz_eta_pos, slow_pz_pos = self.get_stabilizing_piezo_positions(
     )
     offset = cfg.get(cfg.COMPONENT_LIMIT_OFFSET)
     return not (REFERENCE_CELL_LOWER_LIMIT + offset < refcell_pos <
                 REFERENCE_CELL_UPPER_LIMIT - offset
                 and SLOW_PIEZO_LOWER_LIMIT + offset < slow_pz_pos <
                 SLOW_PIEZO_UPPER_LIMIT - offset
                 and PIEZO_ETALON_LOWER_LIMIT + offset < pz_eta_pos <
                 PIEZO_ETALON_UPPER_LIMIT - offset)
    def setup_form(self):
        form_layout = QFormLayout()

        self.analysis_name_field = QLineEdit()
        form_layout.addRow('Analysis name: ', self.analysis_name_field)

        data_file_selection_layout = QHBoxLayout()
        self.data_file_button = QPushButton('Select File')
        self.data_file_label = QLabel()
        data_file_selection_layout.addWidget(self.data_file_button)
        data_file_selection_layout.addWidget(self.data_file_label)
        form_layout.addRow('PLE Data: ', data_file_selection_layout)

        self.integration_start_field = QDoubleSpinBox()
        self.integration_start_field.setMinimum(
            cfg.get(cfg.WAVELENGTH_LOWER_LIMIT))
        self.integration_start_field.setMaximum(
            cfg.get(cfg.WAVELENGTH_UPPER_LIMIT))
        self.integration_start_field.setDecimals(
            cfg.get(cfg.WAVEMETER_PRECISION))
        self.integration_start_field.setSingleStep(
            10**-cfg.get(cfg.WAVEMETER_PRECISION))
        form_layout.addRow('Integration start (nm): ',
                           self.integration_start_field)
        self.integration_end_field = QDoubleSpinBox()
        self.integration_end_field.setMinimum(
            cfg.get(cfg.WAVELENGTH_LOWER_LIMIT))
        self.integration_end_field.setMaximum(
            cfg.get(cfg.WAVELENGTH_UPPER_LIMIT))
        self.integration_end_field.setDecimals(cfg.get(
            cfg.WAVEMETER_PRECISION))
        self.integration_end_field.setSingleStep(
            10**-cfg.get(cfg.WAVEMETER_PRECISION))
        form_layout.addRow('Integration end (nm): ',
                           self.integration_end_field)

        bkgd_file_selection_layout = QHBoxLayout()
        self.bkgd_file_button = QPushButton('Select File')
        self.bkgd_file_label = QLabel()
        bkgd_file_selection_layout.addWidget(self.bkgd_file_button)
        bkgd_file_selection_layout.addWidget(self.bkgd_file_label)
        form_layout.addRow('Subtract Background: ', bkgd_file_selection_layout)

        self.layout.addLayout(form_layout)
Exemplo n.º 16
0
    def start_laser_lock_correction(self):
        """
        Try to lock the laser, and make automatic corrections to the stabilization piezos if needed.

        If there is no target wavelength set, lock at the current wavelength.

        Starts a `matisse_controller.matisse.lock_correction_thread.LockCorrectionThread` as a daemon for this purpose.
        Call `Matisse.stop_laser_lock_correction` to disable lock.
        """
        if self.is_lock_correction_on():
            print('WARNING: Lock correction is already running.')
        else:
            print('Starting laser lock.')
            self._lock_correction_thread = LockCorrectionThread(
                self, cfg.get(cfg.LOCKING_TIMEOUT), queue.Queue(), daemon=True)
            if self.target_wavelength is None:
                self.target_wavelength = self.wavemeter_wavelength()
            self._lock_correction_thread.start()
 def __init__(self, matisse, messages: Queue, *args, **kwargs):
     """
     Parameters
     ----------
     matisse : matisse_controller.matisse.matisse.Matisse
         an instance of Matisse to be provided to the StatusUpdateThread
     messages
         a message queue to be given to the StatusUpdateThread
     *args
         args to pass to `QLabel.__init__`
     **kwargs
         kwargs to pass to `QLabel.__init__`
     """
     super().__init__(*args, **kwargs)
     self.messages = messages
     self.setFont(
         QFont('StyleNormal', cfg.get(cfg.STATUS_MONITOR_FONT_SIZE)))
     self.update_thread = StatusUpdateThread(matisse, messages, parent=self)
     self.update_thread.status_read.connect(self.setText)
     self.update_thread.start()
Exemplo n.º 18
0
    def reset_stabilization_piezos(self):
        """
        Reset the slow piezo to the center, and the RefCell and piezo etalon according to the following rules:

        - If RefCell is at upper limit, piezo etalon is likely near lower limit
        - If wavelength is still too low, move RefCell down lower than usual and piezo etalon higher than usual
        - If RefCell is at lower limit, piezo etalon is likely near upper limit
        - If wavelength is still too high, move RefCell up higher than usual and piezo etalon lower than usual
        - Else, move RefCell and piezo etalon to their center positions.

        A target wavelength must already be set in order to run this method.
        """
        current_refcell_pos, current_pz_eta_pos, current_slow_pz_pos = self.get_stabilizing_piezo_positions(
        )
        current_wavelength = self.wavemeter_wavelength()

        offset = cfg.get(cfg.COMPONENT_LIMIT_OFFSET)
        if (current_refcell_pos > REFERENCE_CELL_UPPER_LIMIT - offset
                and current_wavelength < self.target_wavelength):
            self.query(f"SCAN:NOW {cfg.get(cfg.REFCELL_LOWER_CORRECTION_POS)}")
            self.query(
                f"PIEZOETALON:BASELINE {cfg.get(cfg.PIEZO_ETA_UPPER_CORRECTION_POS)}"
            )
        elif (current_refcell_pos < REFERENCE_CELL_LOWER_LIMIT + offset
              and current_wavelength > self.target_wavelength):
            self.query(f"SCAN:NOW {cfg.get(cfg.REFCELL_UPPER_CORRECTION_POS)}")
            self.query(
                f"PIEZOETALON:BASELINE {cfg.get(cfg.PIEZO_ETA_LOWER_CORRECTION_POS)}"
            )
        else:
            self.query(f"SCAN:NOW {cfg.get(cfg.REFCELL_MID_CORRECTION_POS)}")
            self.query(
                f"PIEZOETALON:BASELINE {cfg.get(cfg.PIEZO_ETA_MID_CORRECTION_POS)}"
            )

        self.query(
            f"SLOWPIEZO:NOW {cfg.get(cfg.SLOW_PIEZO_MID_CORRECTION_POS)}")
Exemplo n.º 19
0
    def start_ple_scan(self,
                       scan_name: str,
                       scan_location: str,
                       initial_wavelength: float,
                       final_wavelength: float,
                       step: float,
                       center_wavelength: float,
                       grating_grooves: int,
                       *ccd_args,
                       plot_analysis=False,
                       integration_start=None,
                       integration_end=None,
                       **ccd_kwargs):
        """
        Perform a PLE scan using the Andor Shamrock spectrometer and Newton CCD.

        Generates text files with data from each spectrum taken during the scan, and pickles the Python dictionary of
        all data into {scan_name}.pickle.

        Parameters
        ----------
        scan_name
            a unique name to give the PLE measurement, which will be included in the name of all the data files
        scan_location
            the name of a folder to contain all relevant scan data
        initial_wavelength
            starting wavelength for the PLE scan
        final_wavelength
            ending wavelength for the PLE scan
        step
            the desired change in wavelength between each individual scan
        center_wavelength
            the wavelength at which to set the spectrometer
        grating_grooves
            the number of grooves to use for the spectrometer grating
        plot_analysis
            whether to plot the PLE analysis in real time
        integration_start : float
            the wavelength at which to start integration for real-time analysis plotting
        integration_end : float
            the wavelength at which to stop integration for real-time analysis plotting
        *ccd_args
            args to pass to `matisse_controller.shamrock_ple.ccd.CCD.setup`
        **ccd_kwargs
            kwargs to pass to `matisse_controller.shamrock_ple.ccd.CCD.setup`
        """
        self.ple_exit_flag = False

        if not scan_name:
            print('WARNING: Name of PLE scan is required.')
            return
        if not scan_location:
            print('WARNING: Location of PLE scan is required.')
            return

        data_file_name = os.path.join(scan_location, f"{scan_name}.pickle")

        if os.path.exists(data_file_name):
            print(
                f"WARNING: A PLE scan has already been run for '{scan_name}'. Choose a new name and try again."
            )
            return

        PLE.load_andor_libs()
        print(
            f"Setting spectrometer grating to {grating_grooves} grvs and center wavelength to {center_wavelength}..."
        )
        shamrock.set_grating_grooves(grating_grooves)
        shamrock.set_center_wavelength(center_wavelength)
        if self.ple_exit_flag:
            return
        ccd.setup(*ccd_args, **ccd_kwargs)
        wavelengths = np.append(
            np.arange(initial_wavelength, final_wavelength, step),
            final_wavelength)
        wavelength_range = abs(
            round(final_wavelength - initial_wavelength,
                  cfg.get(cfg.WAVEMETER_PRECISION)))
        counter = 1
        file_name = ''

        pl_pipe_in, pl_pipe_out = Pipe()
        pl_plot_process = SpectrumPlotProcess(pipe=pl_pipe_out, daemon=True)
        self.spectrum_plot_processes.append(pl_plot_process)
        pl_plot_process.start()

        if plot_analysis:
            analysis_pipe_in, analysis_pipe_out = Pipe()
            analysis_plot_process = PLEAnalysisPlotProcess(
                pipe=analysis_pipe_out, daemon=True)
            self.analysis_plot_processes.append(analysis_plot_process)
            analysis_plot_process.start()

        data = {
            'grating_grooves': grating_grooves,
            'center_wavelength': center_wavelength
        }
        for wavelength in wavelengths:
            print(f"Starting acquisition {counter}/{len(wavelengths)}.")
            wavelength = round(float(wavelength),
                               cfg.get(cfg.WAVEMETER_PRECISION))
            self.lock_at_wavelength(wavelength)
            if self.ple_exit_flag:
                print('Received exit signal, saving PLE data.')
                break
            acquisition_data = ccd.take_acquisition(
            )  # FVB mode bins into each column, so this only grabs points along width
            file_name = os.path.join(
                scan_location,
                f"{str(counter).zfill(3)}_{scan_name}_{wavelength}nm"
                f"_StepSize_{step}nm_Range_{wavelength_range}nm.txt")
            np.savetxt(file_name, acquisition_data)

            acq_wavelengths = self.pixels_to_wavelengths(
                range(len(acquisition_data)), center_wavelength,
                grating_grooves)
            pl_pipe_in.send((acq_wavelengths, acquisition_data))

            if plot_analysis:
                start_pixel, end_pixel = self.find_integration_endpoints(
                    integration_start, integration_end, center_wavelength,
                    grating_grooves)
                analysis_pipe_in.send(
                    (wavelength, sum(acquisition_data[start_pixel:end_pixel])))

            data[wavelength] = acquisition_data
            counter += 1

        pl_pipe_in.send(None)
        if file_name:
            self.plot_single_acquisition(center_wavelength,
                                         grating_grooves,
                                         data_file=file_name)

        with open(data_file_name, 'wb') as data_file:
            pickle.dump(data, data_file, pickle.HIGHEST_PROTOCOL)
        print('Finished PLE scan.')
    def set_current_values_from_config(self):
        self.matisse_device_id_field.setText(cfg.get(cfg.MATISSE_DEVICE_ID))
        self.wavemeter_port_field.setText(cfg.get(cfg.WAVEMETER_PORT))
        self.wavemeter_precision_field.setValue(
            cfg.get(cfg.WAVEMETER_PRECISION))
        self.wavemeter_measurement_delay_field.setValue(
            cfg.get(cfg.WAVEMETER_MEASUREMENT_DELAY))

        self.status_monitor_delay_field.setValue(
            cfg.get(cfg.STATUS_MONITOR_DELAY))
        self.status_monitor_font_size_field.setValue(
            cfg.get(cfg.STATUS_MONITOR_FONT_SIZE))

        self.component_limit_offset_field.setValue(
            cfg.get(cfg.COMPONENT_LIMIT_OFFSET))
        self.wavelength_lower_limit_field.setValue(
            cfg.get(cfg.WAVELENGTH_LOWER_LIMIT))
        self.wavelength_upper_limit_field.setValue(
            cfg.get(cfg.WAVELENGTH_UPPER_LIMIT))
        self.bifi_reset_pos_field.setValue(cfg.get(cfg.BIFI_RESET_POS))
        self.thin_eta_reset_pos_field.setValue(cfg.get(cfg.THIN_ETA_RESET_POS))
        self.thin_eta_rand_range_field.setValue(
            cfg.get(cfg.THIN_ETA_RAND_RANGE))

        self.report_events_field.setChecked(cfg.get(cfg.REPORT_EVENTS))

        self.scan_limit_field.setValue(cfg.get(cfg.SCAN_LIMIT))

        self.bifi_scan_range_field.setValue(cfg.get(cfg.BIFI_SCAN_RANGE))
        self.bifi_small_scan_range_field.setValue(
            cfg.get(cfg.BIFI_SCAN_RANGE_SMALL))
        self.bifi_scan_step_field.setValue(cfg.get(cfg.BIFI_SCAN_STEP))
        self.bifi_scan_show_plots_field.setChecked(
            cfg.get(cfg.BIFI_SCAN_SHOW_PLOTS))
        self.bifi_smoothing_window_field.setValue(
            cfg.get(cfg.BIFI_SMOOTHING_FILTER_WINDOW))
        self.bifi_smoothing_polyorder_field.setValue(
            cfg.get(cfg.BIFI_SMOOTHING_FILTER_POLYORDER))

        self.thin_eta_scan_range_field.setValue(
            cfg.get(cfg.THIN_ETA_SCAN_RANGE))
        self.thin_eta_small_scan_range_field.setValue(
            cfg.get(cfg.THIN_ETA_SCAN_RANGE_SMALL))
        self.thin_eta_scan_step_field.setValue(cfg.get(cfg.THIN_ETA_SCAN_STEP))
        self.thin_eta_nudge_field.setValue(cfg.get(cfg.THIN_ETA_NUDGE))
        self.thin_eta_scan_show_plots_field.setChecked(
            cfg.get(cfg.THIN_ETA_SHOW_PLOTS))
        self.thin_eta_smoothing_window_field.setValue(
            cfg.get(cfg.THIN_ETA_SMOOTHING_FILTER_WINDOW))
        self.thin_eta_smoothing_polyorder_field.setValue(
            cfg.get(cfg.THIN_ETA_SMOOTHING_FILTER_POLYORDER))
        self.thin_eta_max_allowed_stddev_field.setValue(
            cfg.get(cfg.THIN_ETA_MAX_ALLOWED_STDDEV))

        self.refcell_rising_speed_field.setValue(
            cfg.get(cfg.REFCELL_SCAN_RISING_SPEED))
        self.refcell_falling_speed_field.setValue(
            cfg.get(cfg.REFCELL_SCAN_FALLING_SPEED))

        self.large_wavelength_drift_field.setValue(
            cfg.get(cfg.LARGE_WAVELENGTH_DRIFT))
        self.medium_wavelength_drift_field.setValue(
            cfg.get(cfg.MEDIUM_WAVELENGTH_DRIFT))
        self.small_wavelength_drift_field.setValue(
            cfg.get(cfg.SMALL_WAVELENGTH_DRIFT))

        self.locking_timeout_field.setValue(cfg.get(cfg.LOCKING_TIMEOUT))
        self.fast_pz_setpoint_lower_limit_field.setValue(
            cfg.get(cfg.FAST_PZ_SETPOINT_SCAN_LOWER_LIMIT))
        self.fast_pz_setpoint_upper_limit_field.setValue(
            cfg.get(cfg.FAST_PZ_SETPOINT_SCAN_UPPER_LIMIT))
        self.fast_pz_setpoint_num_points_field.setValue(
            cfg.get(cfg.FAST_PZ_SETPOINT_NUM_POINTS))
        self.fast_pz_setpoint_num_scans_field.setValue(
            cfg.get(cfg.FAST_PZ_SETPOINT_NUM_SCANS))

        self.stabilization_rising_speed_field.setValue(
            cfg.get(cfg.STABILIZATION_RISING_SPEED))
        self.stabilization_falling_speed_field.setValue(
            cfg.get(cfg.STABILIZATION_FALLING_SPEED))
        self.stabilization_delay_field.setValue(
            cfg.get(cfg.STABILIZATION_DELAY))
        self.stabilization_tolerance_field.setValue(
            cfg.get(cfg.STABILIZATION_TOLERANCE))

        self.auto_correction_limit_field.setValue(cfg.get(
            cfg.CORRECTION_LIMIT))

        self.pz_eta_upper_correction_pos_field.setValue(
            cfg.get(cfg.PIEZO_ETA_UPPER_CORRECTION_POS))
        self.slow_pz_upper_correction_pos_field.setValue(
            cfg.get(cfg.SLOW_PIEZO_UPPER_CORRECTION_POS))
        self.refcell_upper_correction_pos_field.setValue(
            cfg.get(cfg.REFCELL_UPPER_CORRECTION_POS))
        self.pz_eta_mid_correction_pos_field.setValue(
            cfg.get(cfg.PIEZO_ETA_MID_CORRECTION_POS))
        self.slow_pz_mid_correction_pos_field.setValue(
            cfg.get(cfg.SLOW_PIEZO_MID_CORRECTION_POS))
        self.refcell_mid_correction_pos_field.setValue(
            cfg.get(cfg.REFCELL_MID_CORRECTION_POS))
        self.pz_eta_lower_correction_pos_field.setValue(
            cfg.get(cfg.PIEZO_ETA_LOWER_CORRECTION_POS))
        self.slow_pz_lower_correction_pos_field.setValue(
            cfg.get(cfg.SLOW_PIEZO_LOWER_CORRECTION_POS))
        self.refcell_lower_correction_pos_field.setValue(
            cfg.get(cfg.REFCELL_LOWER_CORRECTION_POS))

        self.target_temperature_field.setValue(
            cfg.get(cfg.PLE_TARGET_TEMPERATURE))
        self.temperature_tolerance_field.setValue(
            cfg.get(cfg.PLE_TEMPERATURE_TOLERANCE))
Exemplo n.º 21
0
    def birefringent_filter_scan(self, scan_range: int = None, repeat=False):
        """
        Initiate a scan of the birefringent filter, selecting the power maximum closest to the target wavelength.

        A configurable Savitzky-Golay filter is used to smooth the data for analysis.

        The position is not changed if the difference between the current position and the "best" position is less than
        1/6 of the average separation between peaks in the power diode curve.

        Additionally, plot the power data and motor position selection if plotting is enabled for this scan.

        Parameters
        ----------
        scan_range : int
            number of motor positions to scan left and right
        repeat : bool
            whether to repeat the scan until the wavelength difference is less than cfg.MEDIUM_WAVELENGTH_DRIFT
        """
        self.is_scanning_bifi = True
        if self.exit_flag or self._scan_attempts > cfg.get(
                cfg.SCAN_LIMIT) or self._restart_set_wavelength:
            self.is_scanning_bifi = False
            return
        if self.target_wavelength is None:
            self.target_wavelength = self.wavemeter_wavelength()
        if scan_range is None:
            scan_range = cfg.get(cfg.BIFI_SCAN_RANGE)

        self._scan_attempts += 1
        old_pos = int(self.query('MOTBI:POS?', numeric_result=True))
        lower_end = old_pos - scan_range
        upper_end = old_pos + scan_range
        assert (0 < lower_end < BIREFRINGENT_FILTER_UPPER_LIMIT
                and 0 < upper_end < BIREFRINGENT_FILTER_UPPER_LIMIT
                and lower_end < upper_end), 'Conditions for BiFi scan invalid. Motor position must be between ' + \
                                            f"{scan_range} and {BIREFRINGENT_FILTER_UPPER_LIMIT - scan_range}"
        positions = np.array(
            range(lower_end, upper_end, cfg.get(cfg.BIFI_SCAN_STEP)))
        voltages = np.array([])
        print('Starting BiFi scan... ')
        for pos in positions:
            self.set_bifi_motor_pos(pos)
            voltages = np.append(voltages,
                                 self.query('DPOW:DC?', numeric_result=True))
        self.set_bifi_motor_pos(
            old_pos
        )  # return back to where we started, just in case something goes wrong
        print('Done.')

        print('Analyzing scan data... ')
        # Smooth out the data and find extrema
        smoothed_data = savgol_filter(
            voltages,
            window_length=cfg.get(cfg.BIFI_SMOOTHING_FILTER_WINDOW),
            polyorder=cfg.get(cfg.BIFI_SMOOTHING_FILTER_POLYORDER))
        maxima = argrelextrema(smoothed_data, np.greater, order=5)

        # Find the position of the extremum closest to the target wavelength
        wavelength_differences = np.array([])
        for pos in positions[maxima]:
            self.set_bifi_motor_pos(pos)
            time.sleep(cfg.get(cfg.WAVEMETER_MEASUREMENT_DELAY))
            wavelength_differences = np.append(
                wavelength_differences,
                abs(self.wavemeter_wavelength() - self.target_wavelength))
        best_pos = positions[maxima][np.argmin(wavelength_differences)]

        # By default, let's assume we're using the new position.
        using_new_pos = True

        if len(positions[maxima]) > 1:
            difference_threshold = np.mean(np.diff(positions[maxima])) / 6
            if abs(old_pos - best_pos) > difference_threshold:
                self.set_bifi_motor_pos(best_pos)
            else:
                print(
                    'Current BiFi motor position is close enough, leaving it alone.'
                )
                self.set_bifi_motor_pos(old_pos)
                using_new_pos = False
        else:
            self.set_bifi_motor_pos(best_pos)
        print('Done.')
        self.is_scanning_bifi = False

        if cfg.get(cfg.BIFI_SCAN_SHOW_PLOTS):
            # TODO: Label wavelength at each peak
            plot_process = BirefringentFilterScanPlotProcess(positions,
                                                             voltages,
                                                             smoothed_data,
                                                             maxima,
                                                             old_pos,
                                                             best_pos,
                                                             using_new_pos,
                                                             daemon=True)
            self._plotting_processes.append(plot_process)
            plot_process.start()

        if repeat:
            new_diff = np.min(wavelength_differences)
            if abs(new_diff) > cfg.get(cfg.MEDIUM_WAVELENGTH_DRIFT):
                print(
                    'Wavelength still too far away from target value. Starting another scan.'
                )
                self.birefringent_filter_scan(scan_range, repeat=True)
Exemplo n.º 22
0
    def thin_etalon_scan(self, scan_range: int = None, repeat=False):
        """
        Initiate a scan of the thin etalon, selecting the reflex minimum closest to the target wavelength.

        A configurable Savitzky-Golay filter is used to smooth the data for analysis.

        The position is not changed if the difference between the current position and the "best" position is less than
        1/6 of the average separation between valleys in the reflex curve.

        If the thin etalon moves too far to one side and we end up in a valley of the power diode curve, the wavelength
        will make large jumps, so a small birefringent scan is performed to correct this.

        If the thin etalon moves into a region with too much noise (as determined by a normalized RMS deviation), quit
        early and perform a large scan next time set_wavelength is called.

        Nudges the motor position a little bit away from the minimum to ensure good locking later.
        Additionally, plot the reflex data and motor position selection.

        Parameters
        ----------
        scan_range : int
            number of motor positions to scan left and right
        repeat : bool
            whether to repeat the scan until the wavelength difference is less than cfg.SMALL_WAVELENGTH_DRIFT
        """
        self.is_scanning_thin_etalon = True
        if self.exit_flag or self._scan_attempts > cfg.get(
                cfg.SCAN_LIMIT) or self._restart_set_wavelength:
            self.is_scanning_thin_etalon = False
            return
        if self.target_wavelength is None:
            self.target_wavelength = self.wavemeter_wavelength()
        if scan_range is None:
            scan_range = cfg.get(cfg.THIN_ETA_SCAN_RANGE)

        self._scan_attempts += 1
        old_pos = int(self.query('MOTTE:POS?', numeric_result=True))
        lower_end, upper_end = self.limits_for_thin_etalon_scan(
            old_pos, scan_range)

        positions = np.array(
            range(lower_end, upper_end, cfg.get(cfg.THIN_ETA_SCAN_STEP)))
        voltages = np.array([])
        print('Starting thin etalon scan... ')
        for pos in positions:
            self.set_thin_etalon_motor_pos(pos)
            voltages = np.append(voltages,
                                 self.query('TE:DC?', numeric_result=True))
        self.set_thin_etalon_motor_pos(
            old_pos
        )  # return back to where we started, just in case something goes wrong
        print('Done.')

        print('Analyzing scan data... ')
        # Smooth out the data and find extrema
        smoothed_data = savgol_filter(
            voltages,
            window_length=cfg.get(cfg.THIN_ETA_SMOOTHING_FILTER_WINDOW),
            polyorder=cfg.get(cfg.THIN_ETA_SMOOTHING_FILTER_POLYORDER))

        normalized_std_dev = np.sqrt(
            np.sum(((smoothed_data - voltages) / smoothed_data)**2))
        print(
            f"Normalized standard deviation from smoothed data: {normalized_std_dev}"
        )
        # Example good value: 1.5, example bad value: 2.5
        if normalized_std_dev > cfg.get(cfg.THIN_ETA_MAX_ALLOWED_STDDEV):
            print(
                'Abnormal deviation from smoothed curve detected, the scan region might just contain noise.'
            )
            self._restart_set_wavelength = True
            self._force_large_scan = True
            self.is_scanning_thin_etalon = False
            return

        minima = argrelextrema(smoothed_data, np.less, order=5)

        # Find the position of the extremum closest to the target wavelength
        wavelength_differences = np.array([])
        for pos in positions[minima]:
            self.set_thin_etalon_motor_pos(pos)
            time.sleep(cfg.get(cfg.WAVEMETER_MEASUREMENT_DELAY))
            wavelength_differences = np.append(
                wavelength_differences,
                abs(self.wavemeter_wavelength() - self.target_wavelength))
        best_minimum_index = np.argmin(wavelength_differences)
        best_pos = positions[minima][best_minimum_index] + cfg.get(
            cfg.THIN_ETA_NUDGE)

        # By default, let's assume we're using the new position.
        using_new_pos = True

        if len(positions[minima]) > 1:
            difference_threshold = np.mean(np.diff(positions[minima])) / 6
            if abs(old_pos - best_pos) > difference_threshold:
                self.set_thin_etalon_motor_pos(best_pos)
            else:
                print(
                    'Current thin etalon motor position is close enough, leaving it alone.'
                )
                self.set_thin_etalon_motor_pos(old_pos)
                using_new_pos = False
        else:
            self.set_thin_etalon_motor_pos(best_pos)
        print('Done.')
        self.is_scanning_thin_etalon = False

        adjacent_differences = np.diff(wavelength_differences)
        left_too_large = (best_minimum_index >= 1
                          and adjacent_differences[best_minimum_index - 1] >
                          cfg.get(cfg.LARGE_WAVELENGTH_DRIFT))
        right_too_large = (best_minimum_index < len(wavelength_differences) - 1
                           and adjacent_differences[best_minimum_index] >
                           cfg.get(cfg.LARGE_WAVELENGTH_DRIFT))
        if left_too_large or right_too_large:
            print(
                'Large jump in wavelength detected, correcting birefringent filter position.'
            )
            self.birefringent_filter_scan(cfg.get(cfg.BIFI_SCAN_RANGE_SMALL),
                                          repeat=False)
            print('Returning to thin etalon scan.')

        if cfg.get(cfg.THIN_ETA_SHOW_PLOTS):
            plot_process = ThinEtalonScanPlotProcess(positions,
                                                     voltages,
                                                     smoothed_data,
                                                     minima,
                                                     old_pos,
                                                     best_pos,
                                                     using_new_pos,
                                                     daemon=True)
            self._plotting_processes.append(plot_process)
            plot_process.start()

        if repeat:
            new_diff = np.min(wavelength_differences)
            if new_diff > cfg.get(cfg.SMALL_WAVELENGTH_DRIFT):
                print(
                    'Wavelength still too far away from target value. Starting another scan.'
                )
                self.thin_etalon_scan(scan_range, repeat=True)
Exemplo n.º 23
0
    def set_wavelength(self, wavelength: float):
        """
        Configure the Matisse to output a given wavelength.

        If the laser is locked and/or stabilizing, pause those operations for the duration of the method.

        First I'll check the difference between the current wavelength and the target wavelength.

        - If this is the first time this is being run, do a large birefringent scan regardless of the difference.
        - If it's greater than cfg.LARGE_WAVELENGTH_DRIFT, do a large birefringent scan to choose a better peak.
        - If it's between about cfg.MEDIUM_WAVELENGTH_DRIFT and cfg.LARGE_WAVELENGTH_DRIFT, do a small birefringent scan
          to keep it on the peak.
        - If it's between cfg.SMALL_WAVELENGTH_DRIFT nm and cfg.MEDIUM_WAVELENGTH_DRIFT, skip the first birefringent
          scan and go right to the thin etalon scan.
        - If it's less than cfg.SMALL_WAVELENGTH_DRIFT, skip all BiFi and TE scans, and just do a RefCell scan.

        This is generally the process I'll follow:

        1. Decide whether to skip any scans, as described above.
        2. Set approx. wavelength using BiFi. This is supposed to be good to about +-1 nm but it's usually very far off.
        3. Scan the BiFi back and forth and measure the total laser power at each point.
        4. Find all local maxima, move the BiFi to the maximum that's closest to the desired wavelength.
        5. Move the thin etalon motor directly to a position close to the target wavelength.
        6. Scan the thin etalon back and forth and measure the thin etalon reflex at each point.
        7. Find all local minima. Move the TE to the minimum that's closest to the desired wavelength.
        8. Shift the thin etalon a over bit by cfg.THIN_ETA_NUDGE. We want to be on the "flank" of the chosen parabola.
        9. Do a small BiFi scan to make sure we're still on the location with maximum power. If the distance to the new
           motor location is very small, just leave the motor where it is.
        10. Do a small thin etalon scan to make sure we're still on the flank of the right parabola.
        11. Attempt to lock the laser, setting the fast piezo setpoint if needed.
        12. Enable RefCell stabilization, which scans the device up or down until the desired wavelength is reached.

        If more than cfg.SCAN_LIMIT scan attempts pass before stabilizing, restart the whole process over again.
        If, during stabilization, more than cfg.CORRECTION_LIMIT corrections are made, start with a large birefringent
        scan the next time this method is run.

        A scan may decide it needs to start the process over again for some other reason, like the thin etalon moving to
        a location with mostly noise.

        Parameters
        ----------
        wavelength : float
            the desired wavelength
        """
        self.is_setting_wavelength = True
        assert cfg.get(cfg.WAVELENGTH_LOWER_LIMIT) < wavelength < cfg.get(cfg.WAVELENGTH_UPPER_LIMIT), \
            'Target wavelength out of range.'

        self.target_wavelength = wavelength

        if self.is_lock_correction_on():
            self.stop_laser_lock_correction()
        # Disable all control loops. This is important if the laser is locked but lock correction isn't on.
        self.set_fast_piezo_control(False)
        self.set_piezo_etalon_control(False)
        self.set_thin_etalon_control(False)
        self.set_slow_piezo_control(False)
        if self.is_stabilizing():
            self.stabilize_off()

        while True:
            self._scan_attempts = 0
            diff = abs(wavelength - self.wavemeter_wavelength())

            if diff > cfg.get(
                    cfg.LARGE_WAVELENGTH_DRIFT) or self._force_large_scan:
                # Notice we randomize the position of the thin etalon a little bit to avoid returning to exactly the
                # same state each time. This is to further avoid the possibility of getting stuck in an endless loop.
                rand_offset = np.random.randint(
                    -cfg.get(cfg.THIN_ETA_RAND_RANGE),
                    cfg.get(cfg.THIN_ETA_RAND_RANGE) + 1)
                self.query(
                    f"MOTTE:POS {cfg.get(cfg.THIN_ETA_RESET_POS) + rand_offset}"
                )
                self.reset_stabilization_piezos()
                # Normal BiFi scan
                print(f"Setting BiFi to ~{wavelength} nm... ")
                self.set_bifi_wavelength(wavelength)
                time.sleep(cfg.get(cfg.WAVEMETER_MEASUREMENT_DELAY))
                print(
                    f"Done. Wavelength is now {self.wavemeter_wavelength()} nm. "
                    "(This is often very wrong, don't worry)")
                self.birefringent_filter_scan(repeat=True)
                self.thin_etalon_scan(repeat=True)
                self.birefringent_filter_scan(scan_range=cfg.get(
                    cfg.BIFI_SCAN_RANGE_SMALL),
                                              repeat=True)
                self.thin_etalon_scan(scan_range=cfg.get(
                    cfg.THIN_ETA_SCAN_RANGE_SMALL),
                                      repeat=True)
            elif cfg.get(cfg.MEDIUM_WAVELENGTH_DRIFT) < diff <= cfg.get(
                    cfg.LARGE_WAVELENGTH_DRIFT):
                # Small BiFi scan
                self.birefringent_filter_scan(scan_range=cfg.get(
                    cfg.BIFI_SCAN_RANGE_SMALL),
                                              repeat=True)
                self.thin_etalon_scan(repeat=True)
                self.birefringent_filter_scan(scan_range=cfg.get(
                    cfg.BIFI_SCAN_RANGE_SMALL),
                                              repeat=True)
                self.thin_etalon_scan(scan_range=cfg.get(
                    cfg.THIN_ETA_SCAN_RANGE_SMALL),
                                      repeat=True)
            elif cfg.get(cfg.SMALL_WAVELENGTH_DRIFT) < diff <= cfg.get(
                    cfg.MEDIUM_WAVELENGTH_DRIFT):
                # No BiFi scan, TE scan only
                self.thin_etalon_scan(repeat=True)
                self.birefringent_filter_scan(scan_range=cfg.get(
                    cfg.BIFI_SCAN_RANGE_SMALL),
                                              repeat=True)
                self.thin_etalon_scan(scan_range=cfg.get(
                    cfg.THIN_ETA_SCAN_RANGE_SMALL),
                                      repeat=True)
            else:
                # No BiFi, no TE. Scan device only.
                pass

            # Restart/exit conditions
            if self.exit_flag:
                self.is_setting_wavelength = False
                return
            if self._restart_set_wavelength:
                self._restart_set_wavelength = False
                print('Restarting wavelength-setting process.')
                continue
            elif self._scan_attempts > cfg.get(cfg.SCAN_LIMIT):
                print(
                    'WARNING: Number of scan attempts exceeded. Starting wavelength-setting process over again.'
                )
                log_event(
                    EventType.SCAN_LIMIT_EXCEEDED, self,
                    self.wavemeter_wavelength(),
                    f"too many scans, exceeded limit of {cfg.get(cfg.SCAN_LIMIT)}, restarting set_wavelength"
                )
                self._force_large_scan = True
                continue
            elif self.stabilization_auto_corrections > cfg.get(
                    cfg.CORRECTION_LIMIT):
                print(
                    'WARNING: Number of stabilization auto-corrections exceeded. Starting wavelength-setting process '
                    'over again.')
                log_event(
                    EventType.STABILIZATION_LIMIT_EXCEEDED, self,
                    self.wavemeter_wavelength(),
                    f"too many stabilization corrections, exceeded limit of {cfg.get(cfg.CORRECTION_LIMIT)}, "
                    'restarting set_wavelength')
                self.stabilization_auto_corrections = 0
                self._force_large_scan = True
                continue
            else:
                self._force_large_scan = False
                break

        self.start_laser_lock_correction()
        print('Attempting to lock laser...')
        while not self.laser_locked():
            if self.exit_flag:
                self.is_setting_wavelength = False
                return
            if not self.is_lock_correction_on():
                print('Lock failed, trying again.')
                self.set_recommended_fast_piezo_setpoint()
                self.start_laser_lock_correction()
            time.sleep(1)
        self.stabilize_on()

        self.is_setting_wavelength = False
    def setup_form(self):
        form_layout = QFormLayout()
        self.scan_name_field = QLineEdit()
        form_layout.addRow('Scan name: ', self.scan_name_field)

        scan_location_layout = QHBoxLayout()
        self.scan_location_button = QPushButton('Select Folder')
        self.scan_location_label = QLabel()
        scan_location_layout.addWidget(self.scan_location_button)
        scan_location_layout.addWidget(self.scan_location_label)
        form_layout.addRow('Scan Location: ', scan_location_layout)

        self.wavelength_start_field = QDoubleSpinBox()
        self.wavelength_start_field.setMinimum(
            cfg.get(cfg.WAVELENGTH_LOWER_LIMIT))
        self.wavelength_start_field.setMaximum(
            cfg.get(cfg.WAVELENGTH_UPPER_LIMIT))
        self.wavelength_start_field.setDecimals(
            cfg.get(cfg.WAVEMETER_PRECISION))
        self.wavelength_start_field.setSingleStep(
            10**-cfg.get(cfg.WAVEMETER_PRECISION))
        form_layout.addRow('Wavelength start (nm): ',
                           self.wavelength_start_field)
        self.wavelength_end_field = QDoubleSpinBox()
        self.wavelength_end_field.setMinimum(
            cfg.get(cfg.WAVELENGTH_LOWER_LIMIT))
        self.wavelength_end_field.setMaximum(
            cfg.get(cfg.WAVELENGTH_UPPER_LIMIT))
        self.wavelength_end_field.setDecimals(cfg.get(cfg.WAVEMETER_PRECISION))
        self.wavelength_end_field.setSingleStep(
            10**-cfg.get(cfg.WAVEMETER_PRECISION))
        form_layout.addRow('Wavelength end (nm): ', self.wavelength_end_field)
        self.wavelength_step_field = QDoubleSpinBox()
        self.wavelength_step_field.setMinimum(0)
        self.wavelength_step_field.setDecimals(cfg.get(
            cfg.WAVEMETER_PRECISION))
        self.wavelength_step_field.setSingleStep(
            10**-cfg.get(cfg.WAVEMETER_PRECISION))
        form_layout.addRow('Wavelength step (nm): ',
                           self.wavelength_step_field)
        self.exposure_time_field = QDoubleSpinBox()
        self.exposure_time_field.setMinimum(0)
        self.exposure_time_field.setDecimals(4)
        form_layout.addRow('Exposure time (s): ', self.exposure_time_field)
        self.layout.addLayout(form_layout)

        # We need to make sure the Andor libraries are loaded to access the spectrometer
        ple.PLE.load_andor_libs()

        self.center_wavelength_field = QDoubleSpinBox()
        self.center_wavelength_field.setMinimum(
            cfg.get(cfg.WAVELENGTH_LOWER_LIMIT))
        self.center_wavelength_field.setMaximum(
            cfg.get(cfg.WAVELENGTH_UPPER_LIMIT))
        self.center_wavelength_field.setValue(
            ple.shamrock.get_center_wavelength())
        form_layout.addRow('Center wavelength (nm): ',
                           self.center_wavelength_field)

        self.grating_grooves_field = QComboBox()
        for groove_num in ple.shamrock.gratings.keys():
            self.grating_grooves_field.addItem(
                f"{groove_num}", ple.shamrock.gratings[groove_num])
        self.grating_grooves_field.setCurrentText(
            str(ple.shamrock.get_grating_grooves()))
        form_layout.addRow('Grating grooves: ', self.grating_grooves_field)

        line = QFrame()
        line.setFrameShape(QFrame.HLine)
        line.setFrameShadow(QFrame.Sunken)
        form_layout.addRow(line)

        self.plot_analysis_field = QCheckBox()
        form_layout.addRow('Plot analysis in real time? ',
                           self.plot_analysis_field)
        self.integration_start_field = QDoubleSpinBox()
        self.integration_start_field.setMinimum(
            cfg.get(cfg.WAVELENGTH_LOWER_LIMIT))
        self.integration_start_field.setMaximum(
            cfg.get(cfg.WAVELENGTH_UPPER_LIMIT))
        self.integration_start_field.setDecimals(
            cfg.get(cfg.WAVEMETER_PRECISION))
        self.integration_start_field.setSingleStep(
            10**-cfg.get(cfg.WAVEMETER_PRECISION))
        self.integration_start_field.setEnabled(False)
        form_layout.addRow('Integration start (nm): ',
                           self.integration_start_field)
        self.integration_end_field = QDoubleSpinBox()
        self.integration_end_field.setMinimum(
            cfg.get(cfg.WAVELENGTH_LOWER_LIMIT))
        self.integration_end_field.setMaximum(
            cfg.get(cfg.WAVELENGTH_UPPER_LIMIT))
        self.integration_end_field.setDecimals(cfg.get(
            cfg.WAVEMETER_PRECISION))
        self.integration_end_field.setSingleStep(
            10**-cfg.get(cfg.WAVEMETER_PRECISION))
        self.integration_end_field.setEnabled(False)
        form_layout.addRow('Integration end (nm): ',
                           self.integration_end_field)