class RNGGui(GUIBase): _modclass = 'rnggui' _modtype = 'gui' # declare connectors rnglogic = Connector(interface='RNGLogic') # Signals # sigStart = QtCore.Signal() # sigStop = QtCore.Signal() def on_activate(self): # Connect to logic self._rng_logic = self.rnglogic() # instantiate MainWindow self._mw = RNGMainWindow() # Signal connection to logic module self._mw.startButton.clicked.connect(self._rng_logic.start_monitoring) self._mw.stopButton.clicked.connect(self._rng_logic.stop_monitoring) self._mw.mean_box.valueChanged.connect(self.mean_changed) self._mw.noise_box.valueChanged.connect(self.noise_changed) self._mw.update_rate_box.valueChanged.connect(self.update_rate_changed) self._rng_logic.repeat_sig.connect(self.new_random_value_received) def on_deactivate(self): pass # self._mw.startButton.clicked.disconnect() # self._mw.startButton.clicked.disconnect() # self._mw.mean_box.valueChanged.disconnect() # self._mw.noise_box.valueChanged.disconnect() # self._mw.update_rate_box.valueChanged.disconnect() # self._rng_logic._monitor_thread.value_updated_sig.disconnect() def show(self): """Make window visible and put it above all other windows. """ QtWidgets.QMainWindow.show(self._mw) self._mw.activateWindow() self._mw.raise_() def mean_changed(self, new_mean): self._rng_logic.set_rng_params(mean=new_mean) def noise_changed(self, new_noise): self._rng_logic.set_rng_params(noise=new_noise) def update_rate_changed(self, new_update_rate): self._rng_logic.set_monitor_params(update_rate=new_update_rate) def new_random_value_received(self): try: new_value = self._rng_logic.get_current_value()[0] self._mw.display.display(new_value) except: return
class SimpleDataLogic(GenericLogic): """ Logic module agreggating multiple hardware switches. """ _modclass = 'smple_data' _modtype = 'logic' simpledata = Connector(interface='SimpleDataInterface') sigRepeat = QtCore.Signal() def on_activate(self): """ Prepare logic module for work. """ self._data_logic = self.get_connector('simpledata') self.stopRequest = False self.bufferLength = 10000 self.sigRepeat.connect(self.measureLoop, QtCore.Qt.QueuedConnection) def on_deactivate(self): """ Deactivate modeule. """ self.stopMeasure() def startMeasure(self): """ Start measurement: zero the buffer and call loop function.""" self.window_len = 50 self.buf = np.zeros( (self.bufferLength, self._data_logic.getChannels())) self.smooth = np.zeros((self.bufferLength + self.window_len - 1, self._data_logic.getChannels())) self.lock() self.sigRepeat.emit() def stopMeasure(self): """ Ask the measurement loop to stop. """ self.stopRequest = True def measureLoop(self): """ Measure 10 values, add them to buffer and remove the 10 oldest values. """ if self.stopRequest: self.stopRequest = False self.unlock() return data = np.zeros((100, self._data_logic.getChannels())) data[:, 0] = np.array([self._data_logic.getData() for i in range(100)]) self.buf = np.roll(self.buf, -100, axis=0) self.buf[-101:-1] = data w = np.hanning(self.window_len) s = np.r_[self.buf[self.window_len - 1:0:-1], self.buf, self.buf[-1:-self.window_len:-1]] for channel in range(self._data_logic.getChannels()): convolved = np.convolve(w / w.sum(), s[:, channel], mode='valid') self.smooth[:, channel] = convolved self.sigRepeat.emit()
class AutomationLogic(GenericLogic): """ Logic module agreggating multiple hardware switches. """ _modclass = 'AutomationLogic' _modtype = 'logic' taskrunner = Connector(interface='TaskRunner') sigRepeat = QtCore.Signal() def on_activate(self): """ Prepare logic module for work. """ self._taskrunner = self.taskrunner() #stuff = "a\txyz\n b\tx\n c\ty\n d\tw\ne\tm\n" #tr = OrderedDict([ # ('a', OrderedDict([ # ('f', OrderedDict([ # ('g', 5) # ])), # ('h', 'letrole'), # ])), # ('b', 1), # ('c', 2), # ('d', 3), # ('e', 4) #]) self.model = TreeModel() #self.model.loadExecTree(tr) self.loadAutomation('auto.cfg') def on_deactivate(self): """ Deactivate modeule. """ print(self.model.recursiveSave(self.model.rootItem)) def loadAutomation(self, path): """ Load automation config into model. @param path str: file path """ if os.path.isfile(path): configdict = configfile.readConfigFile(path) self.model.loadExecTree(configdict)
class AutomationGui(GUIBase): """ Graphical interface for arranging tasks without using Python code. """ _modclass = 'AutomationGui' _modtype = 'gui' ## declare connectors automationlogic = Connector(interface='AutomationLogic') sigRunTaskFromList = QtCore.Signal(object) sigPauseTaskFromList = QtCore.Signal(object) sigStopTaskFromList = QtCore.Signal(object) def on_activate(self): """Create all UI objects and show the window. """ self._mw = AutomationMainWindow() self.restoreWindowPos(self._mw) self.logic = self.automationlogic() self._mw.autoTreeView.setModel(self.logic.model) self._mw.taskTableView.clicked.connect(self.setRunToolState) self._mw.actionStart_Task.triggered.connect(self.manualStart) self._mw.actionPause_Task.triggered.connect(self.manualPause) self._mw.actionStop_Task.triggered.connect(self.manualStop) self.sigRunTaskFromList.connect(self.logic.startTaskByIndex) self.sigPauseTaskFromList.connect(self.logic.pauseTaskByIndex) self.sigStopTaskFromList.connect(self.logic.stopTaskByIndex) self.logic.model.dataChanged.connect( lambda i1, i2: self.setRunToolState(None, i1)) self.show() def show(self): """Make sure that the window is visible and at the top. """ self._mw.show() def on_deactivate(self): """ Hide window and stop ipython console. """ self.saveWindowPos(self._mw) self._mw.close()
class SwitchGui(GUIBase): """ A grephical interface to mofe switches by hand and change their calibration. """ _modclass = 'SwitchGui' _modtype = 'gui' ## declare connectors switchlogic = Connector(interface='SwitchLogic') def on_activate(self): """Create all UI objects and show the window. """ self._mw = SwitchMainWindow() lsw = self.switchlogic() # For each switch that the logic has, add a widget to the GUI to show its state for hw in lsw.switches: frame = QtWidgets.QGroupBox(hw, self._mw.scrollAreaWidgetContents) frame.setAlignment(QtCore.Qt.AlignLeft) frame.setFlat(False) self._mw.layout.addWidget(frame) layout = QtWidgets.QVBoxLayout(frame) for switch in lsw.switches[hw]: swidget = SwitchWidget(switch, lsw.switches[hw][switch]) layout.addWidget(swidget) self.restoreWindowPos(self._mw) self.show() def show(self): """Make sure that the window is visible and at the top. """ self._mw.show() def on_deactivate(self): """ Hide window and stop ipython console. """ self.saveWindowPos(self._mw) self._mw.close()
class SoftPIDController(GenericLogic, PIDControllerInterface): """ Control a process via software PID. """ _modclass = 'pidlogic' _modtype = 'logic' ## declare connectors process = Connector(interface='ProcessInterface') control = Connector(interface='ProcessControlInterface') # config opt timestep = ConfigOption(default=100) # status vars kP = StatusVar(default=1) kI = StatusVar(default=1) kD = StatusVar(default=1) setpoint = StatusVar(default=273.15) manualvalue = StatusVar(default=0) sigNewValue = QtCore.Signal(float) def __init__(self, config, **kwargs): super().__init__(config=config, **kwargs) self.log.debug('The following configuration was found.') # checking for the right configuration for key in config.keys(): self.log.debug('{0}: {1}'.format(key, config[key])) #number of lines in the matrix plot self.NumberOfSecondsLog = 100 self.threadlock = Mutex() def on_activate(self): """ Initialisation performed during activation of the module. """ self._process = self.get_connector('process') self._control = self.get_connector('control') self.previousdelta = 0 self.cv = self._control.getControlValue() self.timer = QtCore.QTimer() self.timer.setSingleShot(True) self.timer.setInterval(self.timestep) self.timer.timeout.connect(self._calcNextStep, QtCore.Qt.QueuedConnection) self.sigNewValue.connect(self._control.setControlValue) self.history = np.zeros([3, 5]) self.savingState = False self.enable = False self.integrated = 0 self.countdown = 2 self.timer.start(self.timestep) def on_deactivate(self): """ Perform required deactivation. """ pass def _calcNextStep(self): """ This function implements the Takahashi Type C PID controller: the P and D term are no longer dependent on the set-point, only on PV (which is Thlt). The D term is NOT low-pass filtered. This function should be called once every TS seconds. """ self.pv = self._process.getProcessValue() if self.countdown > 0: self.countdown -= 1 self.previousdelta = self.setpoint - self.pv print('Countdown: ', self.countdown) elif self.countdown == 0: self.countdown = -1 self.integrated = 0 self.enable = True if (self.enable): delta = self.setpoint - self.pv self.integrated += delta ## Calculate PID controller: self.P = self.kP * delta self.I = self.kI * self.timestep * self.integrated self.D = self.kD / self.timestep * (delta - self.previousdelta) self.cv += self.P + self.I + self.D self.previousdelta = delta ## limit contol output to maximum permissible limits limits = self._control.getControlLimits() if (self.cv > limits[1]): self.cv = limits[1] if (self.cv < limits[0]): self.cv = limits[0] self.history = np.roll(self.history, -1, axis=1) self.history[0, -1] = self.pv self.history[1, -1] = self.cv self.history[2, -1] = self.setpoint self.sigNewValue.emit(self.cv) else: self.cv = self.manualvalue limits = self._control.getControlLimits() if (self.cv > limits[1]): self.cv = limits[1] if (self.cv < limits[0]): self.cv = limits[0] self.sigNewValue.emit(self.cv) self.timer.start(self.timestep) def startLoop(self): """ Start the control loop. """ self.countdown = 2 def stopLoop(self): """ Stop the control loop. """ self.countdown = -1 self.enable = False def getSavingState(self): """ Find out if we are keeping data for saving later. @return bool: whether module is saving process and control data """ return self.savingState def startSaving(self): """ Start saving process and control data. Does not do anything right now. """ pass def saveData(self): """ Write process and control data to file. Does not do anything right now. """ pass def get_kp(self): """ Return the proportional constant. @return float: proportional constant of PID controller """ return self.kP def set_kp(self, kp): """ Set the proportional constant of the PID controller. @prarm float kp: proportional constant of PID controller """ self.kP = kp def get_ki(self): """ Get the integration constant of the PID controller @return float: integration constant of the PID controller """ return self.kI def set_ki(self, ki): """ Set the integration constant of the PID controller. @param float ki: integration constant of the PID controller """ self.kI = ki def get_kd(self): """ Get the derivative constant of the PID controller @return float: the derivative constant of the PID controller """ return self.kD def set_kd(self, kd): """ Set the derivative constant of the PID controller @param float kd: the derivative constant of the PID controller """ self.kD = kd def get_setpoint(self): """ Get the current setpoint of the PID controller. @return float: current set point of the PID controller """ return self.setpoint def set_setpoint(self, setpoint): """ Set the current setpoint of the PID controller. @param float setpoint: new set point of the PID controller """ self.setpoint = setpoint def get_manual_value(self): """ Return the control value for manual mode. @return float: control value for manual mode """ return self.manualvalue def set_manual_value(self, manualvalue): """ Set the control value for manual mode. @param float manualvalue: control value for manual mode of controller """ self.manualvalue = manualvalue limits = self._control.getControlLimits() if (self.manualvalue > limits[1]): self.manualvalue = limits[1] if (self.manualvalue < limits[0]): self.manualvalue = limits[0] def get_enabled(self): """ See if the PID controller is controlling a process. @return bool: whether the PID controller is preparing to or conreolling a process """ return self.enable or self.countdown >= 0 def set_enabled(self, enabled): """ Set the state of the PID controller. @param bool enabled: desired state of PID controller """ if enabled and not self.enable and self.countdown == -1: self.startLoop() if not enabled and self.enable: self.stopLoop() def get_control_limits(self): """ Get the minimum and maximum value of the control actuator. @return list(float): (minimum, maximum) values of the control actuator """ return self._control.getControlLimits() def set_control_limits(self, limits): """ Set the minimum and maximum value of the control actuator. @param list(float) limits: (minimum, maximum) values of the control actuator This function does nothing, control limits are handled by the control module """ pass def get_control_value(self): """ Get current control output value. @return float: control output value """ return self.cv def get_process_value(self): """ Get current process input value. @return float: current process input value """ return self.pv def get_extra(self): """ Extra information about the controller state. @return dict: extra informatin about internal controller state Do not depend on the output of this function, not every field exists for every PID controller. """ return {'P': self.P, 'I': self.I, 'D': self.D}
class ODMRCounterMicrowaveInterfuse(GenericLogic, ODMRCounterInterface, MicrowaveInterface): """ Interfuse to enable a software trigger of the microwave source but still having a hardware timed counter. This interfuse connects the ODMR logic with a slowcounter and a microwave device. """ _modclass = 'ODMRCounterMicrowaveInterfuse' _modtype = 'interfuse' slowcounter = Connector(interface='SlowCounterInterface') microwave = Connector(interface='MicrowaveInterface') def __init__(self, config, **kwargs): super().__init__(config=config, **kwargs) self._pulse_out_channel = 'dummy' self._lock_in_active = False self._oversampling = 10 self._odmr_length = 100 def on_activate(self): """ Initialisation performed during activation of the module.""" self._mw_device = self.microwave() self._sc_device = self.slowcounter() # slow counter device pass def on_deactivate(self): pass ### ODMR counter interface commands def set_up_odmr_clock(self, clock_frequency=None, clock_channel=None): """ Configures the hardware clock of the NiDAQ card to give the timing. @param float clock_frequency: if defined, this sets the frequency of the clock @param str clock_channel: if defined, this is the physical channel of the clock @return int: error code (0:OK, -1:error) """ return self._sc_device.set_up_clock(clock_frequency=clock_frequency, clock_channel=clock_channel) def set_up_odmr(self, counter_channel=None, photon_source=None, clock_channel=None, odmr_trigger_channel=None): """ Configures the actual counter with a given clock. @param str counter_channel: if defined, this is the physical channel of the counter @param str photon_source: if defined, this is the physical channel where the photons are to count from @param str clock_channel: if defined, this specifies the clock for the counter @param str odmr_trigger_channel: if defined, this specifies the trigger output for the microwave @return int: error code (0:OK, -1:error) """ return self._sc_device.set_up_counter(counter_channels=counter_channel, sources=photon_source, clock_channel=clock_channel, counter_buffer=None) def set_odmr_length(self, length=100): """Set up the trigger sequence for the ODMR and the triggered microwave. @param int length: length of microwave sweep in pixel @return int: error code (0:OK, -1:error) """ self._odmr_length = length return 0 def count_odmr(self, length=100): """ Sweeps the microwave and returns the counts on that sweep. @param int length: length of microwave sweep in pixel @return float[]: the photon counts per second """ counts = np.zeros((len(self.get_odmr_channels()), length)) # self.trigger() for i in range(length): self.trigger() counts[:, i] = self._sc_device.get_counter(samples=1)[0] self.trigger() return False, counts def close_odmr(self): """ Close the odmr and clean up afterwards. @return int: error code (0:OK, -1:error) """ return self._sc_device.close_counter() def close_odmr_clock(self): """ Close the odmr and clean up afterwards. @return int: error code (0:OK, -1:error) """ return self._sc_device.close_clock() def get_odmr_channels(self): """ Return a list of channel names. @return list(str): channels recorded during ODMR measurement """ return self._sc_device.get_counter_channels() ### ----------- Microwave interface commands ----------- def trigger(self): return self._mw_device.trigger() def off(self): """ Switches off any microwave output. Must return AFTER the device is actually stopped. @return int: error code (0:OK, -1:error) """ return self._mw_device.off() def get_status(self): """ Gets the current status of the MW source, i.e. the mode (cw, list or sweep) and the output state (stopped, running) @return str, bool: mode ['cw', 'list', 'sweep'], is_running [True, False] """ return self._mw_device.get_status() def get_power(self): """ Gets the microwave output power for the currently active mode. @return float: the output power in dBm """ return self._mw_device.get_power() def get_frequency(self): """ Gets the frequency of the microwave output. Returns single float value if the device is in cw mode. Returns list like [start, stop, step] if the device is in sweep mode. Returns list of frequencies if the device is in list mode. @return [float, list]: frequency(s) currently set for this device in Hz """ return self._mw_device.get_frequency() def cw_on(self): """ Switches on cw microwave output. Must return AFTER the device is actually running. @return int: error code (0:OK, -1:error) """ return self._mw_device.cw_on() def set_cw(self, frequency=None, power=None): """ Configures the device for cw-mode and optionally sets frequency and/or power @param float frequency: frequency to set in Hz @param float power: power to set in dBm @return tuple(float, float, str): with the relation current frequency in Hz, current power in dBm, current mode """ return self._mw_device.set_cw(frequency=frequency, power=power) def list_on(self): """ Switches on the list mode microwave output. Must return AFTER the device is actually running. @return int: error code (0:OK, -1:error) """ return self._mw_device.list_on() def set_list(self, frequency=None, power=None): """ Configures the device for list-mode and optionally sets frequencies and/or power @param list frequency: list of frequencies in Hz @param float power: MW power of the frequency list in dBm @return list, float, str: current frequencies in Hz, current power in dBm, current mode """ return self._mw_device.set_list(frequency=frequency, power=power) def reset_listpos(self): """ Reset of MW list mode position to start (first frequency step) @return int: error code (0:OK, -1:error) """ return self._mw_device.reset_listpos() def sweep_on(self): """ Switches on the sweep mode. @return int: error code (0:OK, -1:error) """ return self._mw_device.sweep_on() def set_sweep(self, start=None, stop=None, step=None, power=None): """ Configures the device for sweep-mode and optionally sets frequency start/stop/step and/or power @return float, float, float, float, str: current start frequency in Hz, current stop frequency in Hz, current frequency step in Hz, current power in dBm, current mode """ return self._mw_device.set_sweep(start=start, stop=stop, step=step, power=power) def reset_sweeppos(self): """ Reset of MW sweep mode position to start (start frequency) @return int: error code (0:OK, -1:error) """ return self._mw_device.reset_sweeppos() def set_ext_trigger(self, pol, timing): """ Set the external trigger for this device with proper polarization. @param TriggerEdge pol: polarisation of the trigger (basically rising edge or falling edge) @param timing: estimated time between triggers @return object: current trigger polarity [TriggerEdge.RISING, TriggerEdge.FALLING] """ return self._mw_device.set_ext_trigger(pol=pol, timing=timing) def get_limits(self): """ Return the device-specific limits in a nested dictionary. @return MicrowaveLimits: Microwave limits object """ return self._mw_device.get_limits() @property def oversampling(self): return self._oversampling @oversampling.setter def oversampling(self, val): if not isinstance(val, (int, float)): self.log.error('oversampling has to be int of float.') else: self._oversampling = int(val) @property def lock_in_active(self): return self._lock_in_active @lock_in_active.setter def lock_in_active(self, val): if not isinstance(val, bool): self.log.error('lock_in_active has to be boolean.') else: self._lock_in_active = val if self._lock_in_active: self.log.warn('Lock-In is not implemented')
class PIDLogic(GenericLogic): """ Control a process via software PID. """ _modclass = 'pidlogic' _modtype = 'logic' ## declare connectors controller = Connector(interface='PIDControllerInterface') savelogic = Connector(interface='SaveLogic') # status vars bufferLength = StatusVar('bufferlength', 1000) timestep = StatusVar(default=100) # signals sigUpdateDisplay = QtCore.Signal() def __init__(self, config, **kwargs): super().__init__(config=config, **kwargs) self.log.debug('The following configuration was found.') #number of lines in the matrix plot self.NumberOfSecondsLog = 100 self.threadlock = Mutex() def on_activate(self): """ Initialisation performed during activation of the module. """ self._controller = self.get_connector('controller') self._save_logic = self.get_connector('savelogic') self.history = np.zeros([3, self.bufferLength]) self.savingState = False self.enabled = False self.timer = QtCore.QTimer() self.timer.setSingleShot(True) self.timer.setInterval(self.timestep) self.timer.timeout.connect(self.loop) def on_deactivate(self): """ Perform required deactivation. """ pass def getBufferLength(self): """ Get the current data buffer length. """ return self.bufferLength def startLoop(self): """ Start the data recording loop. """ self.enabled = True self.timer.start(self.timestep) def stopLoop(self): """ Stop the data recording loop. """ self.enabled = False def loop(self): """ Execute step in the data recording loop: save one of each control and process values """ self.history = np.roll(self.history, -1, axis=1) self.history[0, -1] = self._controller.get_process_value() self.history[1, -1] = self._controller.get_control_value() self.history[2, -1] = self._controller.get_setpoint() self.sigUpdateDisplay.emit() if self.enabled: self.timer.start(self.timestep) def getSavingState(self): """ Return whether we are saving data @return bool: whether we are saving data right now """ return self.savingState def startSaving(self): """ Start saving data. Function does nothing right now. """ pass def saveData(self): """ Stop saving data and write data to file. Function does nothing right now. """ pass def setBufferLength(self, newBufferLength): """ Change buffer length to new value. @param int newBufferLength: new buffer length """ self.bufferLength = newBufferLength self.history = np.zeros([3, self.bufferLength]) def get_kp(self): """ Return the proportional constant. @return float: proportional constant of PID controller """ return self._controller.get_kp() def set_kp(self, kp): """ Set the proportional constant of the PID controller. @prarm float kp: proportional constant of PID controller """ return self._controller.set_kp(kp) def get_ki(self): """ Get the integration constant of the PID controller @return float: integration constant of the PID controller """ return self._controller.get_ki() def set_ki(self, ki): """ Set the integration constant of the PID controller. @param float ki: integration constant of the PID controller """ return self._controller.set_ki(ki) def get_kd(self): """ Get the derivative constant of the PID controller @return float: the derivative constant of the PID controller """ return self._controller.get_kd() def set_kd(self, kd): """ Set the derivative constant of the PID controller @param float kd: the derivative constant of the PID controller """ return self._controller.set_kd(kd) def get_setpoint(self): """ Get the current setpoint of the PID controller. @return float: current set point of the PID controller """ return self.history[2, -1] def set_setpoint(self, setpoint): """ Set the current setpoint of the PID controller. @param float setpoint: new set point of the PID controller """ self._controller.set_setpoint(setpoint) def get_manual_value(self): """ Return the control value for manual mode. @return float: control value for manual mode """ return self._controller.get_manual_value() def set_manual_value(self, manualvalue): """ Set the control value for manual mode. @param float manualvalue: control value for manual mode of controller """ return self._controller.set_manual_value(manualvalue) def get_enabled(self): """ See if the PID controller is controlling a process. @return bool: whether the PID controller is preparing to or conreolling a process """ return self.enabled def set_enabled(self, enabled): """ Set the state of the PID controller. @param bool enabled: desired state of PID controller """ if enabled and not self.enabled: self.startLoop() if not enabled and self.enabled: self.stopLoop() def get_control_limits(self): """ Get the minimum and maximum value of the control actuator. @return list(float): (minimum, maximum) values of the control actuator """ return self._controller.get_control_limits() def set_control_limits(self, limits): """ Set the minimum and maximum value of the control actuator. @param list(float) limits: (minimum, maximum) values of the control actuator This function does nothing, control limits are handled by the control module """ return self._controller.set_control_limits(limits) def get_pv(self): """ Get current process input value. @return float: current process input value """ return self.history[0, -1] def get_cv(self): """ Get current control output value. @return float: control output value """ return self.history[1, -1]
class WavemeterLogGui(GUIBase): _modclass = 'WavemeterLogGui' _modtype = 'gui' ## declare connectors wavemeterloggerlogic1 = Connector(interface='WavemeterLoggerLogic') savelogic = Connector(interface='SaveLogic') sigStartCounter = QtCore.Signal() sigStopCounter = QtCore.Signal() sigFitChanged = QtCore.Signal(str) sigDoFit = QtCore.Signal() def __init__(self, config, **kwargs): super().__init__(config=config, **kwargs) self.log.debug('The following configuration was found.') # checking for the right configuration for key in config.keys(): self.log.info('{0}: {1}'.format(key, config[key])) def on_activate(self): """ Definition and initialisation of the GUI. """ self._wm_logger_logic = self.get_connector('wavemeterloggerlogic1') self._save_logic = self.get_connector('savelogic') # setting up the window self._mw = WavemeterLogWindow() ## giving the plots names allows us to link their axes together self._pw = self._mw.plotWidget # pg.PlotWidget(name='Counter1') self._plot_item = self._pw.plotItem ## create a new ViewBox, link the right axis to its coordinate system self._right_axis = pg.ViewBox() self._plot_item.showAxis('right') self._plot_item.scene().addItem(self._right_axis) self._plot_item.getAxis('right').linkToView(self._right_axis) self._right_axis.setXLink(self._plot_item) ## create a new ViewBox, link the right axis to its coordinate system self._top_axis = pg.ViewBox() self._plot_item.showAxis('top') self._plot_item.scene().addItem(self._top_axis) self._plot_item.getAxis('top').linkToView(self._top_axis) self._top_axis.setYLink(self._plot_item) self._top_axis.invertX(b=True) # handle resizing of any of the elements self._update_plot_views() self._plot_item.vb.sigResized.connect(self._update_plot_views) self._pw.setLabel('left', 'Fluorescence', units='counts/s') self._pw.setLabel('right', 'Number of Points', units='#') self._pw.setLabel('bottom', 'Wavelength', units='nm') self._pw.setLabel('top', 'Relative Frequency', units='Hz') self._mw.actionStop_resume_scan.triggered.connect( self.stop_resume_clicked) self._mw.actionSave_histogram.triggered.connect(self.save_clicked) self._mw.actionStart_scan.triggered.connect(self.start_clicked) self._mw.actionAuto_range.triggered.connect(self.set_auto_range) # defining the parameters to edit self._mw.binSpinBox.setValue(self._wm_logger_logic.get_bins()) self._mw.binSpinBox.editingFinished.connect(self.recalculate_histogram) self._mw.minDoubleSpinBox.setValue( self._wm_logger_logic.get_min_wavelength()) self._mw.minDoubleSpinBox.editingFinished.connect( self.recalculate_histogram) self._mw.maxDoubleSpinBox.setValue( self._wm_logger_logic.get_max_wavelength()) self._mw.maxDoubleSpinBox.editingFinished.connect( self.recalculate_histogram) self._mw.show() ## Create an empty plot curve to be filled later, set its pen self.curve_data_points = pg.PlotDataItem(pen=pg.mkPen(palette.c1), symbol=None) self.curve_nm_counts = pg.PlotDataItem(pen=pg.mkPen( palette.c2, style=QtCore.Qt.DotLine), symbol=None) self.curve_hz_counts = pg.PlotDataItem(pen=pg.mkPen( palette.c6, style=QtCore.Qt.DotLine), symbol=None) self.curve_envelope = pg.PlotDataItem(pen=pg.mkPen( palette.c3, style=QtCore.Qt.DotLine), symbol=None) self.curve_fit = pg.PlotDataItem(pen=pg.mkPen(palette.c2, width=3), symbol=None) self._pw.addItem(self.curve_data_points) self._pw.addItem(self.curve_envelope) self._right_axis.addItem(self.curve_nm_counts) self._top_axis.addItem(self.curve_hz_counts) # scatter plot for time series self._spw = self._mw.scatterPlotWidget self._spi = self._spw.plotItem self._spw.setLabel('bottom', 'Wavelength', units='nm') self._spw.setLabel('left', 'Time', units='s') self._scatterplot = pg.ScatterPlotItem(size=10, pen=pg.mkPen(None), brush=pg.mkBrush( 255, 255, 255, 20)) self._spw.addItem(self._scatterplot) self._spw.setXLink(self._plot_item) self._wm_logger_logic.sig_new_data_point.connect(self.add_data_point) self._wm_logger_logic.sig_data_updated.connect(self.updateData) # fit settings self._fsd = FitSettingsDialog(self._wm_logger_logic.fc) self._fsd.sigFitsUpdated.connect( self._mw.fit_methods_ComboBox.setFitFunctions) self._fsd.applySettings() self._mw.actionFit_settings.triggered.connect(self._fsd.show) self._mw.do_fit_PushButton.clicked.connect(self.doFit) self.sigDoFit.connect(self._wm_logger_logic.do_fit) self.sigFitChanged.connect(self._wm_logger_logic.fc.set_current_fit) self._wm_logger_logic.sig_fit_updated.connect(self.updateFit) def on_deactivate(self): """ Deactivate the module properly. """ self._mw.close() def show(self): """ Make window visible and put it above all other windows. """ QtWidgets.QMainWindow.show(self._mw) self._mw.activateWindow() self._mw.raise_() def updateData(self): """ The function that grabs the data and sends it to the plot. """ self._mw.wavelengthLabel.setText('{0:,.6f} nm '.format( self._wm_logger_logic.current_wavelength)) self._mw.autoMinLabel.setText('Minimum: {0:3.6f} (nm) '.format( self._wm_logger_logic.intern_xmin)) self._mw.autoMaxLabel.setText('Maximum: {0:3.6f} (nm) '.format( self._wm_logger_logic.intern_xmax)) x_axis = self._wm_logger_logic.histogram_axis x_axis_hz = (3.0e17 / (x_axis) - 6.0e17 / (self._wm_logger_logic.get_max_wavelength() + self._wm_logger_logic.get_min_wavelength())) plotdata = np.array(self._wm_logger_logic.counts_with_wavelength) if len(plotdata.shape) > 1 and plotdata.shape[1] == 3: self.curve_data_points.setData(plotdata[:, 2:0:-1]) self.curve_nm_counts.setData(x=x_axis, y=self._wm_logger_logic.histogram) self.curve_hz_counts.setData(x=x_axis_hz, y=self._wm_logger_logic.histogram) self.curve_envelope.setData(x=x_axis, y=self._wm_logger_logic.envelope_histogram) @QtCore.Slot() def doFit(self): self.sigFitChanged.emit( self._mw.fit_methods_ComboBox.getCurrentFit()[0]) self.sigDoFit.emit() @QtCore.Slot() def updateFit(self): """ Do the configured fit and show it in the plot """ fit_name = self._wm_logger_logic.fc.current_fit fit_result = self._wm_logger_logic.fc.current_fit_result fit_param = self._wm_logger_logic.fc.current_fit_param if fit_result is not None: # display results as formatted text self._mw.fit_results_DisplayWidget.clear() try: formated_results = units.create_formatted_output( fit_result.result_str_dict) except: formated_results = 'this fit does not return formatted results' self._mw.fit_results_DisplayWidget.setPlainText(formated_results) if fit_name is not None: self._mw.fit_methods_ComboBox.setCurrentFit(fit_name) # check which fit method is used and show the curve in the plot accordingly if fit_name != 'No Fit': self.curve_fit.setData(x=self._wm_logger_logic.wlog_fit_x, y=self._wm_logger_logic.wlog_fit_y) if self.curve_fit not in self._mw.plotWidget.listDataItems(): self._mw.plotWidget.addItem(self.curve_fit) else: if self.curve_fit in self._mw.plotWidget.listDataItems(): self._mw.plotWidget.removeItem(self.curve_fit) def add_data_point(self, point): if len(point) >= 3: spts = [{ 'pos': (point[0], point[1]), 'size': 5, 'brush': pg.intColor(point[2] / 100, 255) }] self._scatterplot.addPoints(spts) def stop_resume_clicked(self): """ Handling the Start button to stop and restart the counter. """ # If running, then we stop the measurement and enable inputs again if self._wm_logger_logic.module_state() == 'running': self._mw.actionStop_resume_scan.setText('Resume') self._wm_logger_logic.stop_scanning() self._mw.actionStop_resume_scan.setEnabled(True) self._mw.actionStart_scan.setEnabled(True) self._mw.binSpinBox.setEnabled(True) # Otherwise, we start a measurement and disable some inputs. else: self._mw.actionStop_resume_scan.setText('Stop') self._wm_logger_logic.start_scanning(resume=True) self._mw.actionStart_scan.setEnabled(False) self._mw.binSpinBox.setEnabled(False) def start_clicked(self): """ Handling resume of the scanning without resetting the data. """ if self._wm_logger_logic.module_state() == 'idle': self._scatterplot.clear() self._wm_logger_logic.start_scanning() # Enable the stop button once a scan starts. self._mw.actionStop_resume_scan.setText('Stop') self._mw.actionStop_resume_scan.setEnabled(True) self._mw.actionStart_scan.setEnabled(False) self._mw.binSpinBox.setEnabled(False) self.recalculate_histogram() else: self.log.error('Cannot scan, since a scan is alredy running.') def save_clicked(self): """ Handling the save button to save the data into a file. """ timestamp = datetime.datetime.now() filepath = self._save_logic.get_path_for_module( module_name='WavemeterLogger') filename = os.path.join( filepath, timestamp.strftime('%Y%m%d-%H%M-%S_wavemeter_log_thumbnail')) exporter = pg.exporters.SVGExporter(self._pw.plotItem) exporter.export(filename + '.svg') self._wm_logger_logic.save_data(timestamp=timestamp) def recalculate_histogram(self): self._wm_logger_logic.recalculate_histogram( bins=self._mw.binSpinBox.value(), xmin=self._mw.minDoubleSpinBox.value(), xmax=self._mw.maxDoubleSpinBox.value()) def set_auto_range(self): self._mw.minDoubleSpinBox.setValue(self._wm_logger_logic.intern_xmin) self._mw.maxDoubleSpinBox.setValue(self._wm_logger_logic.intern_xmax) self.recalculate_histogram() ## Handle view resizing def _update_plot_views(self): ## view has resized; update auxiliary views to match self._right_axis.setGeometry(self._plot_item.vb.sceneBoundingRect()) self._top_axis.setGeometry(self._plot_item.vb.sceneBoundingRect()) ## need to re-update linked axes since this was called ## incorrectly while views had different shapes. ## (probably this should be handled in ViewBox.resizeEvent) self._right_axis.linkedViewChanged(self._plot_item.vb, self._right_axis.XAxis) self._top_axis.linkedViewChanged(self._plot_item.vb, self._top_axis.YAxis)
class WavemeterLoggerLogic(GenericLogic): """This logic module gathers data from wavemeter and the counter logic. """ sig_data_updated = QtCore.Signal() sig_update_histogram_next = QtCore.Signal(bool) sig_handle_timer = QtCore.Signal(bool) sig_new_data_point = QtCore.Signal(list) sig_fit_updated = QtCore.Signal() _modclass = 'laserscanninglogic' _modtype = 'logic' # declare connectors wavemeter1 = Connector(interface='WavemeterInterface') counterlogic = Connector(interface='CounterLogic') savelogic = Connector(interface='SaveLogic') fitlogic = Connector(interface='FitLogic') # config opts _logic_acquisition_timing = ConfigOption('logic_acquisition_timing', 20.0, missing='warn') _logic_update_timing = ConfigOption('logic_update_timing', 100.0, missing='warn') def __init__(self, config, **kwargs): """ Create WavemeterLoggerLogic object with connectors. @param dict config: module configuration @param dict kwargs: optional parameters """ super().__init__(config=config, **kwargs) # locking for thread safety self.threadlock = Mutex() self._acqusition_start_time = 0 self._bins = 200 self._data_index = 0 self._recent_wavelength_window = [0, 0] self.counts_with_wavelength = [] self._xmin = 650 self._xmax = 750 # internal min and max wavelength determined by the measured wavelength self.intern_xmax = -1.0 self.intern_xmin = 1.0e10 self.current_wavelength = 0 def on_activate(self): """ Initialisation performed during activation of the module. """ self._wavelength_data = [] self.stopRequested = False self._wavemeter_device = self.get_connector('wavemeter1') # print("Counting device is", self._counting_device) self._save_logic = self.get_connector('savelogic') self._counter_logic = self.get_connector('counterlogic') self._fit_logic = self.get_connector('fitlogic') self.fc = self._fit_logic.make_fit_container('Wavemeter counts', '1d') self.fc.set_units(['Hz', 'c/s']) if 'fits' in self._statusVariables and isinstance( self._statusVariables['fits'], dict): self.fc.load_from_dict(self._statusVariables['fits']) else: d1 = OrderedDict() d1['Lorentzian peak'] = { 'fit_function': 'lorentzian', 'estimator': 'peak' } d1['Two Lorentzian peaks'] = { 'fit_function': 'lorentziandouble', 'estimator': 'peak' } d1['Two Gaussian peaks'] = { 'fit_function': 'gaussiandouble', 'estimator': 'peak' } default_fits = OrderedDict() default_fits['1d'] = d1 self.fc.load_from_dict(default_fits) # create a new x axis from xmin to xmax with bins points self.histogram_axis = np.arange(self._xmin, self._xmax, (self._xmax - self._xmin) / self._bins) self.histogram = np.zeros(self.histogram_axis.shape) self.envelope_histogram = np.zeros(self.histogram_axis.shape) self.sig_update_histogram_next.connect( self._attach_counts_to_wavelength, QtCore.Qt.QueuedConnection) # fit data self.wlog_fit_x = np.linspace(self._xmin, self._xmax, self._bins * 5) self.wlog_fit_y = np.zeros(self.wlog_fit_x.shape) # create an indepentent thread for the hardware communication self.hardware_thread = QtCore.QThread() # create an object for the hardware communication and let it live on the new thread self._hardware_pull = HardwarePull(self) self._hardware_pull.moveToThread(self.hardware_thread) # connect the signals in and out of the threaded object self.sig_handle_timer.connect(self._hardware_pull.handle_timer) # start the event loop for the hardware self.hardware_thread.start() self.last_point_time = time.time() def on_deactivate(self): """ Deinitialisation performed during deactivation of the module. """ if self.module_state() != 'idle' and self.module_state( ) != 'deactivated': self.stop_scanning() self.hardware_thread.quit() self.sig_handle_timer.disconnect() if len(self.fc.fit_list) > 0: self._statusVariables['fits'] = self.fc.save_to_dict() def get_max_wavelength(self): """ Current maximum wavelength of the scan. @return float: current maximum wavelength """ return self._xmax def get_min_wavelength(self): """ Current minimum wavelength of the scan. @return float: current minimum wavelength """ return self._xmin def get_bins(self): """ Current number of bins in the spectrum. @return int: current number of bins in the scan """ return self._bins def recalculate_histogram(self, bins=None, xmin=None, xmax=None): """ Recalculate the current spectrum from raw data. @praram int bins: new number of bins @param float xmin: new minimum wavelength @param float xmax: new maximum wavelength """ if bins is not None: self._bins = bins if xmin is not None: self._xmin = xmin if xmax is not None: self._xmax = xmax # create a new x axis from xmin to xmax with bins points self.rawhisto = np.zeros(self._bins) self.envelope_histogram = np.zeros(self._bins) self.sumhisto = np.ones(self._bins) * 1.0e-10 self.histogram_axis = np.linspace(self._xmin, self._xmax, self._bins) self.sig_update_histogram_next.emit(True) def get_fit_functions(self): """ Return the names of all ocnfigured fit functions. @return list(str): list of fit function names """ return self.fc.fit_list.keys() def do_fit(self): """ Execute the currently configured fit """ self.wlog_fit_x, self.wlog_fit_y, result = self.fc.do_fit( self.histogram_axis, self.histogram) self.sig_fit_updated.emit() self.sig_data_updated.emit() def start_scanning(self, resume=False): """ Prepare to start counting: zero variables, change state and start counting "loop" @param bool resume: whether to resume measurement """ self.module_state.run() if self._counter_logic.module_state() == 'idle': self._counter_logic.startCount() if self._counter_logic.get_saving_state(): self._counter_logic.save_data() self._wavemeter_device.start_acqusition() self._counter_logic.start_saving(resume=resume) if not resume: self._acqusition_start_time = self._counter_logic._saving_start_time self._wavelength_data = [] self.data_index = 0 self._recent_wavelength_window = [0, 0] self.counts_with_wavelength = [] self.rawhisto = np.zeros(self._bins) self.sumhisto = np.ones(self._bins) * 1.0e-10 self.intern_xmax = -1.0 self.intern_xmin = 1.0e10 self.recent_avg = [0, 0, 0] self.recent_count = 0 # start the measuring thread self.sig_handle_timer.emit(True) self._complete_histogram = True self.sig_update_histogram_next.emit(False) return 0 def stop_scanning(self): """ Set a flag to request stopping counting. """ if not self.module_state() == 'idle': # self._wavemeter_device.stop_acqusition() # stop the measurement thread self.sig_handle_timer.emit(False) # set status to idle again self.module_state.stop() if self._counter_logic.get_saving_state(): self._counter_logic.save_data(to_file=False) return 0 def _attach_counts_to_wavelength(self, complete_histogram): """ Interpolate a wavelength value for each photon count value. This process assumes that the wavelength is varying smoothly and fairly continuously, which is sensible for most measurement conditions. Recent count values are those recorded AFTER the previous stitch operation, but BEFORE the most recent wavelength value (do not extrapolate beyond the current wavelength information). """ # If there is not yet any wavelength data, then wait and signal next loop if len(self._wavelength_data) == 0: time.sleep(self._logic_update_timing * 1e-3) self.sig_data_updated.emit() return # The end of the recent_wavelength_window is the time of the latest wavelength data self._recent_wavelength_window[1] = self._wavelength_data[-1][0] # (speed-up) We only need to worry about "recent" counts, because as the count data gets # very long all the earlier points will already be attached to wavelength values. count_recentness = 100 # TODO: calculate this from count_freq and wavemeter refresh rate # TODO: Does this depend on things, or do we loop fast enough to get every wavelength value? wavelength_recentness = np.min([5, len(self._wavelength_data)]) recent_counts = np.array( self._counter_logic._data_to_save[-count_recentness:]) recent_wavelengths = np.array( self._wavelength_data[-wavelength_recentness:]) # The latest counts are those recorded during the recent_wavelength_window count_idx = [0, 0] count_idx[0] = np.searchsorted(recent_counts[:, 0], self._recent_wavelength_window[0]) count_idx[1] = np.searchsorted(recent_counts[:, 0], self._recent_wavelength_window[1]) latest_counts = recent_counts[count_idx[0]:count_idx[1]] # Interpolate to obtain wavelength values at the times of each count interpolated_wavelengths = np.interp(latest_counts[:, 0], xp=recent_wavelengths[:, 0], fp=recent_wavelengths[:, 1]) # Stitch interpolated wavelength into latest counts array latest_stitched_data = np.insert(latest_counts, 2, values=interpolated_wavelengths, axis=1) # Add this latest data to the list of counts vs wavelength self.counts_with_wavelength += latest_stitched_data.tolist() # The start of the recent data window for the next round will be the end of this one. self._recent_wavelength_window[0] = self._recent_wavelength_window[1] # Run the old update histogram method to keep duplicate data self._update_histogram(complete_histogram) # Signal that data has been updated self.sig_data_updated.emit() # Wait and repeat if measurement is ongoing time.sleep(self._logic_update_timing * 1e-3) if self.module_state() == 'running': self.sig_update_histogram_next.emit(False) def _update_histogram(self, complete_histogram): """ Calculate new points for the histogram. @param bool complete_histogram: should the complete histogram be recalculated, or just the most recent data? @return: """ # If things like num_of_bins have changed, then recalculate the complete histogram # Note: The histogram may be recalculated (bins changed, etc) from the stitched data. # There is no need to recompute the interpolation for the stitched data. if complete_histogram: count_window = len(self._counter_logic._data_to_save) self._data_index = 0 self.log.info('Recalcutating Laser Scanning Histogram for: ' '{0:d} counts and {1:d} wavelength.'.format( count_window, len(self._wavelength_data))) else: count_window = min(100, len(self._counter_logic._data_to_save)) if count_window < 2: time.sleep(self._logic_update_timing * 1e-3) self.sig_update_histogram_next.emit(False) return temp = np.array(self._counter_logic._data_to_save[-count_window:]) # only do something if there is wavelength data to work with if len(self._wavelength_data) > 0: for i in self._wavelength_data[self._data_index:]: self._data_index += 1 if i[1] < self._xmin or i[1] > self._xmax: continue # calculate the bin the new wavelength needs to go in newbin = np.digitize([i[1]], self.histogram_axis)[0] # if the bin make no sense, start from the beginning if newbin > len(self.rawhisto) - 1: continue # sum the counts in rawhisto and count the occurence of the bin in sumhisto interpolation = np.interp(i[0], xp=temp[:, 0], fp=temp[:, 1]) self.rawhisto[newbin] += interpolation self.sumhisto[newbin] += 1.0 self.envelope_histogram[newbin] = np.max( [interpolation, self.envelope_histogram[newbin]]) datapoint = [i[1], i[0], interpolation] if time.time() - self.last_point_time > 1: self.sig_new_data_point.emit(self.recent_avg) self.last_point_time = time.time() self.recent_count = 0 else: self.recent_count += 1 for j in range(3): self.recent_avg[ j] -= self.recent_avg[j] / self.recent_count self.recent_avg[j] += datapoint[j] / self.recent_count # the plot data is the summed counts divided by the occurence of the respective bins self.histogram = self.rawhisto / self.sumhisto def save_data(self, timestamp=None): """ Save the counter trace data and writes it to a file. @param datetime timestamp: timestamp passed from gui so that saved images match filenames of data. This will be removed when savelogic handles the image creation also. @return int: error code (0:OK, -1:error) """ self._saving_stop_time = time.time() filepath = self._save_logic.get_path_for_module( module_name='WavemeterLogger') filelabel = 'wavemeter_log_histogram' # Currently need to pass timestamp from gui so that the saved image matches saved data. # TODO: once the savelogic saves images, we can revert this to always getting timestamp here. if timestamp is None: timestamp = datetime.datetime.now() # prepare the data in a dict or in an OrderedDict: data = OrderedDict() data['Wavelength (nm)'] = np.array(self.histogram_axis) data['Signal (counts/s)'] = np.array(self.histogram) # write the parameters: parameters = OrderedDict() parameters['Bins (#)'] = self._bins parameters['Xmin (nm)'] = self._xmin parameters['XMax (nm)'] = self._xmax parameters['Start Time (s)'] = time.strftime( '%d.%m.%Y %Hh:%Mmin:%Ss', time.localtime(self._acqusition_start_time)) parameters['Stop Time (s)'] = time.strftime( '%d.%m.%Y %Hh:%Mmin:%Ss', time.localtime(self._saving_stop_time)) self._save_logic.save_data(data, filepath=filepath, parameters=parameters, filelabel=filelabel, timestamp=timestamp, fmt='%.12e') filelabel = 'wavemeter_log_wavelength' # prepare the data in a dict or in an OrderedDict: data = OrderedDict() data['Time (s), Wavelength (nm)'] = self._wavelength_data # write the parameters: parameters = OrderedDict() parameters['Acquisition Timing (ms)'] = self._logic_acquisition_timing parameters['Start Time (s)'] = time.strftime( '%d.%m.%Y %Hh:%Mmin:%Ss', time.localtime(self._acqusition_start_time)) parameters['Stop Time (s)'] = time.strftime( '%d.%m.%Y %Hh:%Mmin:%Ss', time.localtime(self._saving_stop_time)) self._save_logic.save_data(data, filepath=filepath, parameters=parameters, filelabel=filelabel, timestamp=timestamp, fmt='%.12e') filelabel = 'wavemeter_log_counts' # prepare the data in a dict or in an OrderedDict: data = OrderedDict() data['Time (s),Signal (counts/s)'] = self._counter_logic._data_to_save # write the parameters: parameters = OrderedDict() parameters['Start counting time (s)'] = time.strftime( '%d.%m.%Y %Hh:%Mmin:%Ss', time.localtime(self._counter_logic._saving_start_time)) parameters['Stop counting time (s)'] = time.strftime( '%d.%m.%Y %Hh:%Mmin:%Ss', time.localtime(self._saving_stop_time)) parameters[ 'Length of counter window (# of events)'] = self._counter_logic._count_length parameters[ 'Count frequency (Hz)'] = self._counter_logic._count_frequency parameters[ 'Oversampling (Samples)'] = self._counter_logic._counting_samples parameters[ 'Smooth Window Length (# of events)'] = self._counter_logic._smooth_window_length self._save_logic.save_data(data, filepath=filepath, parameters=parameters, filelabel=filelabel, timestamp=timestamp, fmt='%.12e') self.log.debug('Laser Scan saved to:\n{0}'.format(filepath)) filelabel = 'wavemeter_log_counts_with_wavelength' # prepare the data in a dict or in an OrderedDict: data = OrderedDict() data[ 'Measurement Time (s), Signal (counts/s), Interpolated Wavelength (nm)'] = np.array( self.counts_with_wavelength) fig = self.draw_figure() # write the parameters: parameters = OrderedDict() parameters['Start Time (s)'] = time.strftime( '%d.%m.%Y %Hh:%Mmin:%Ss', time.localtime(self._acqusition_start_time)) parameters['Stop Time (s)'] = time.strftime( '%d.%m.%Y %Hh:%Mmin:%Ss', time.localtime(self._saving_stop_time)) self._save_logic.save_data(data, filepath=filepath, parameters=parameters, filelabel=filelabel, timestamp=timestamp, plotfig=fig, fmt='%.12e') plt.close(fig) return 0 def draw_figure(self): """ Draw figure to save with data file. @return: fig fig: a matplotlib figure object to be saved to file. """ # TODO: Draw plot for second APD if it is connected wavelength_data = [entry[2] for entry in self.counts_with_wavelength] count_data = np.array( [entry[1] for entry in self.counts_with_wavelength]) # Index of max counts, to use to position "0" of frequency-shift axis count_max_index = count_data.argmax() # Scale count values using SI prefix prefix = ['', 'k', 'M', 'G'] prefix_index = 0 while np.max(count_data) > 1000: count_data = count_data / 1000 prefix_index = prefix_index + 1 counts_prefix = prefix[prefix_index] # Use qudi style plt.style.use(self._save_logic.mpl_qd_style) # Create figure fig, ax = plt.subplots() ax.plot(wavelength_data, count_data, linestyle=':', linewidth=0.5) ax.set_xlabel('wavelength (nm)') ax.set_ylabel('Fluorescence (' + counts_prefix + 'c/s)') x_formatter = mpl.ticker.ScalarFormatter(useOffset=False) ax.xaxis.set_major_formatter(x_formatter) ax2 = ax.twiny() nm_xlim = ax.get_xlim() ghz_at_max_counts = self.nm_to_ghz(wavelength_data[count_max_index]) ghz_min = self.nm_to_ghz(nm_xlim[0]) - ghz_at_max_counts ghz_max = self.nm_to_ghz(nm_xlim[1]) - ghz_at_max_counts ax2.set_xlim(ghz_min, ghz_max) ax2.set_xlabel('Shift (GHz)') return fig def nm_to_ghz(self, wavelength): """ Convert wavelength to frequency. @param float wavelength: vacuum wavelength @return float: freequency """ return 3e8 / wavelength
class SpectrumLogic(GenericLogic): """This logic module gathers data from the spectrometer. """ sig_specdata_updated = QtCore.Signal() sig_next_diff_loop = QtCore.Signal() _modclass = 'spectrumlogic' _modtype = 'logic' # declare connectors spectrometer = Connector(interface='SpectrometerInterface') odmrlogic1 = Connector(interface='ODMRLogic') savelogic = Connector(interface='SaveLogic') def __init__(self, **kwargs): """ Create SpectrometerLogic object with connectors. @param dict kwargs: optional parameters """ super().__init__(**kwargs) # locking for thread safety self.threadlock = Mutex() def on_activate(self): """ Initialisation performed during activation of the module. """ self.spectrum_data = np.array([]) self.diff_spec_data_mod_on = np.array([]) self.diff_spec_data_mod_off = np.array([]) self.repetition_count = 0 # count loops for differential spectrum self._spectrometer_device = self.spectrometer() self._odmr_logic = self.odmrlogic1() self._save_logic = self.savelogic() self.sig_next_diff_loop.connect(self._loop_differential_spectrum) def on_deactivate(self): """ Deinitialisation performed during deactivation of the module. """ if self.module_state() != 'idle' and self.module_state( ) != 'deactivated': pass def get_single_spectrum(self): """ Record a single spectrum from the spectrometer. """ self.spectrum_data = netobtain( self._spectrometer_device.recordSpectrum()) # Clearing the differential spectra data arrays so that they do not get # saved with this single spectrum. self.diff_spec_data_mod_on = np.array([]) self.diff_spec_data_mod_off = np.array([]) self.sig_specdata_updated.emit() def save_raw_spectrometer_file(self, path='', postfix=''): """Ask the hardware device to save its own raw file. """ # TODO: sanity check the passed parameters. self._spectrometer_device.saveSpectrum(path, postfix=postfix) def start_differential_spectrum(self): """Start a differential spectrum acquisition. An initial spectrum is recorded to initialise the data arrays to the right size. """ self._continue_differential = True # Taking a demo spectrum gives us the wavelength values and the length of the spectrum data. demo_data = netobtain(self._spectrometer_device.recordSpectrum()) wavelengths = demo_data[0, :] empty_signal = np.zeros(len(wavelengths)) # Using this information to initialise the differential spectrum data arrays. self.spectrum_data = np.array([wavelengths, empty_signal]) self.diff_spec_data_mod_on = np.array([wavelengths, empty_signal]) self.diff_spec_data_mod_off = np.array([wavelengths, empty_signal]) self.repetition_count = 0 # Starting the measurement loop self._loop_differential_spectrum() def resume_differential_spectrum(self): """Resume a differential spectrum acquisition. """ self._continue_differential = True # Starting the measurement loop self._loop_differential_spectrum() def _loop_differential_spectrum(self): """ This loop toggles the modulation and iteratively records a differential spectrum. """ # If the loop should not continue, then return immediately without # emitting any signal to repeat. if not self._continue_differential: return # Otherwise, we make a measurement and then emit a signal to repeat this loop. # Toggle on, take spectrum and add data to the mod_on data self.toggle_modulation(on=True) these_data = netobtain(self._spectrometer_device.recordSpectrum()) self.diff_spec_data_mod_on[1, :] += these_data[1, :] # Toggle off, take spectrum and add data to the mod_off data self.toggle_modulation(on=False) these_data = netobtain(self._spectrometer_device.recordSpectrum()) self.diff_spec_data_mod_off[1, :] += these_data[1, :] self.repetition_count += 1 # increment the loop count # Calculate the differential spectrum self.spectrum_data[1, :] = self.diff_spec_data_mod_on[ 1, :] - self.diff_spec_data_mod_off[1, :] self.sig_specdata_updated.emit() self.sig_next_diff_loop.emit() def stop_differential_spectrum(self): """Stop an ongoing differential spectrum acquisition """ self._continue_differential = False def toggle_modulation(self, on): """ Toggle the modulation. """ if on: self._odmr_logic.MW_on() elif not on: self._odmr_logic.MW_off() else: print("Parameter 'on' needs to be boolean") def save_spectrum_data(self): """ Saves the current spectrum data to a file. """ filepath = self._save_logic.get_path_for_module(module_name='spectra') filelabel = 'spectrum' # write experimental parameters parameters = OrderedDict() parameters[ 'Spectrometer acquisition repetitions'] = self.repetition_count # prepare the data in an OrderedDict: data = OrderedDict() data['wavelength'] = self.spectrum_data[0, :] # If the differential spectra arrays are not empty, save them as raw data if len(self.diff_spec_data_mod_on) != 0 and len( self.diff_spec_data_mod_off) != 0: data['signal_mod_on'] = self.diff_spec_data_mod_on[1, :] data['signal_mod_off'] = self.diff_spec_data_mod_off[1, :] data['differential'] = self.spectrum_data[1, :] else: data['signal'] = self.spectrum_data[1, :] # Prepare the figure to save as a "data thumbnail" plt.style.use(self._save_logic.mpl_qd_style) fig, ax1 = plt.subplots() ax1.plot(data['wavelength'], data['signal']) ax1.set_xlabel('Wavelength (nm)') ax1.set_ylabel('Signal (arb. u.)') fig.tight_layout() # Save to file self._save_logic.save_data(data, filepath=filepath, parameters=parameters, filelabel=filelabel, plotfig=fig) self.log.debug('Spectrum saved to:\n{0}'.format(filepath))
class ConfocalScannerDummy(Base, ConfocalScannerInterface): """ Dummy confocal scanner. Produces a picture with several gaussian spots. """ _modclass = 'ConfocalScannerDummy' _modtype = 'hardware' # connectors fitlogic = Connector(interface='FitLogic') # config _clock_frequency = ConfigOption('clock_frequency', 100, missing='warn') def __init__(self, config, **kwargs): super().__init__(config=config, **kwargs) # Internal parameters self._line_length = None self._voltage_range = [-10, 10] self._position_range = [[0, 100e-6], [0, 100e-6], [0, 100e-6], [0, 1e-6]] self._current_position = [0, 0, 0, 0][0:len(self.get_scanner_axes())] self._num_points = 500 def on_activate(self): """ Initialisation performed during activation of the module. """ self._fit_logic = self.fitlogic() # put randomly distributed NVs in the scanner, first the x,y scan self._points = np.empty([self._num_points, 7]) # amplitude self._points[:, 0] = np.random.normal(4e5, 1e5, self._num_points) # x_zero self._points[:, 1] = np.random.uniform(self._position_range[0][0], self._position_range[0][1], self._num_points) # y_zero self._points[:, 2] = np.random.uniform(self._position_range[1][0], self._position_range[1][1], self._num_points) # sigma_x self._points[:, 3] = np.random.normal(0.7e-6, 0.1e-6, self._num_points) # sigma_y self._points[:, 4] = np.random.normal(0.7e-6, 0.1e-6, self._num_points) # theta self._points[:, 5] = 10 # offset self._points[:, 6] = 0 # now also the z-position # gaussian_function(self,x_data=None,amplitude=None, x_zero=None, sigma=None, offset=None): self._points_z = np.empty([self._num_points, 4]) # amplitude self._points_z[:, 0] = np.random.normal(1, 0.05, self._num_points) # x_zero self._points_z[:, 1] = np.random.uniform(45e-6, 55e-6, self._num_points) # sigma self._points_z[:, 2] = np.random.normal(0.5e-6, 0.1e-6, self._num_points) # offset self._points_z[:, 3] = 0 def on_deactivate(self): """ Deactivate properly the confocal scanner dummy. """ self.reset_hardware() def reset_hardware(self): """ Resets the hardware, so the connection is lost and other programs can access it. @return int: error code (0:OK, -1:error) """ self.log.warning('Scanning Device will be reset.') return 0 def get_position_range(self): """ Returns the physical range of the scanner. @return float [4][2]: array of 4 ranges with an array containing lower and upper limit """ return self._position_range def set_position_range(self, myrange=None): """ Sets the physical range of the scanner. @param float [4][2] myrange: array of 4 ranges with an array containing lower and upper limit @return int: error code (0:OK, -1:error) """ if myrange is None: myrange = [[0, 1e-6], [0, 1e-6], [0, 1e-6], [0, 1e-6]] if not isinstance(myrange, ( frozenset, list, set, tuple, np.ndarray, )): self.log.error('Given range is no array type.') return -1 if len(myrange) != 4: self.log.error('Given range should have dimension 4, but has ' '{0:d} instead.'.format(len(myrange))) return -1 for pos in myrange: if len(pos) != 2: self.log.error('Given range limit {1:d} should have ' 'dimension 2, but has {0:d} instead.'.format( len(pos), pos)) return -1 if pos[0] > pos[1]: self.log.error('Given range limit {0:d} has the wrong ' 'order.'.format(pos)) return -1 self._position_range = myrange return 0 def set_voltage_range(self, myrange=None): """ Sets the voltage range of the NI Card. @param float [2] myrange: array containing lower and upper limit @return int: error code (0:OK, -1:error) """ if myrange is None: myrange = [-10., 10.] if not isinstance(myrange, ( frozenset, list, set, tuple, np.ndarray, )): self.log.error('Given range is no array type.') return -1 if len(myrange) != 2: self.log.error('Given range should have dimension 2, but has ' '{0:d} instead.'.format(len(myrange))) return -1 if myrange[0] > myrange[1]: self.log.error('Given range limit {0:d} has the wrong ' 'order.'.format(myrange)) return -1 if self.module_state() == 'locked': self.log.error('A Scanner is already running, close this one ' 'first.') return -1 self._voltage_range = myrange return 0 def get_scanner_axes(self): """ Dummy scanner is always 3D cartesian. """ return ['x', 'y', 'z', 'a'] def get_scanner_count_channels(self): """ 3 counting channels in dummy confocal: normal, negative and a ramp.""" return ['Norm', 'Neg', 'Ramp'] def set_up_scanner_clock(self, clock_frequency=None, clock_channel=None): """ Configures the hardware clock of the NiDAQ card to give the timing. @param float clock_frequency: if defined, this sets the frequency of the clock @param str clock_channel: if defined, this is the physical channel of the clock @return int: error code (0:OK, -1:error) """ if clock_frequency is not None: self._clock_frequency = float(clock_frequency) self.log.debug('ConfocalScannerDummy>set_up_scanner_clock') time.sleep(0.2) return 0 def set_up_scanner(self, counter_channels=None, sources=None, clock_channel=None, scanner_ao_channels=None): """ Configures the actual scanner with a given clock. @param str counter_channel: if defined, this is the physical channel of the counter @param str photon_source: if defined, this is the physical channel where the photons are to count from @param str clock_channel: if defined, this specifies the clock for the counter @param str scanner_ao_channels: if defined, this specifies the analoque output channels @return int: error code (0:OK, -1:error) """ self.log.debug('ConfocalScannerDummy>set_up_scanner') time.sleep(0.2) return 0 def scanner_set_position(self, x=None, y=None, z=None, a=None): """Move stage to x, y, z, a (where a is the fourth voltage channel). @param float x: postion in x-direction (volts) @param float y: postion in y-direction (volts) @param float z: postion in z-direction (volts) @param float a: postion in a-direction (volts) @return int: error code (0:OK, -1:error) """ if self.module_state() == 'locked': self.log.error( 'A Scanner is already running, close this one first.') return -1 time.sleep(0.01) self._current_position = [x, y, z, a][0:len(self.get_scanner_axes())] return 0 def get_scanner_position(self): """ Get the current position of the scanner hardware. @return float[]: current position in (x, y, z, a). """ return self._current_position[0:len(self.get_scanner_axes())] def _set_up_line(self, length=100): """ Sets up the analoque output for scanning a line. @param int length: length of the line in pixel @return int: error code (0:OK, -1:error) """ self._line_length = length # self.log.debug('ConfocalScannerInterfaceDummy>set_up_line') return 0 def scan_line(self, line_path=None, pixel_clock=False): """ Scans a line and returns the counts on that line. @param float[][4] line_path: array of 4-part tuples defining the voltage points @param bool pixel_clock: whether we need to output a pixel clock for this line @return float[]: the photon counts per second """ if not isinstance(line_path, ( frozenset, list, set, tuple, np.ndarray, )): self.log.error('Given voltage list is no array type.') return np.array([[-1.]]) if np.shape(line_path)[1] != self._line_length: self._set_up_line(np.shape(line_path)[1]) count_data = np.random.uniform(0, 2e4, self._line_length) z_data = line_path[2, :] #TODO: Change the gaussian function here to the one from fitlogic and delete the local modules to calculate #the gaussian functions x_data = np.array(line_path[0, :]) y_data = np.array(line_path[1, :]) for i in range(self._num_points): count_data += self.twoD_gaussian_function( (x_data, y_data), *(self._points[i])) * self.gaussian_function( np.array(z_data), *(self._points_z[i])) time.sleep(self._line_length * 1. / self._clock_frequency) time.sleep(self._line_length * 1. / self._clock_frequency) # update the scanner position instance variable self._current_position = list(line_path[:, -1]) return np.array([ count_data, 5e5 - count_data, np.ones(count_data.shape) * line_path[1, 0] * 100 ]).transpose() def close_scanner(self): """ Closes the scanner and cleans up afterwards. @return int: error code (0:OK, -1:error) """ self.log.debug('ConfocalScannerDummy>close_scanner') return 0 def close_scanner_clock(self, power=0): """ Closes the clock and cleans up afterwards. @return int: error code (0:OK, -1:error) """ self.log.debug('ConfocalScannerDummy>close_scanner_clock') return 0 ############################################################################ # # # the following two functions are needed to fluorescence signal # # of the dummy NVs # # # ############################################################################ def twoD_gaussian_function(self, x_data_tuple=None, amplitude=None, x_zero=None, y_zero=None, sigma_x=None, sigma_y=None, theta=None, offset=None): #FIXME: x_data_tuple: dimension of arrays """ This method provides a two dimensional gaussian function. @param (k,M)-shaped array x_data_tuple: x and y values @param float or int amplitude: Amplitude of gaussian @param float or int x_zero: x value of maximum @param float or int y_zero: y value of maximum @param float or int sigma_x: standard deviation in x direction @param float or int sigma_y: standard deviation in y direction @param float or int theta: angle for eliptical gaussians @param float or int offset: offset @return callable function: returns the function """ # check if parameters make sense #FIXME: Check for 2D matrix if not isinstance(x_data_tuple, (frozenset, list, set, tuple, np.ndarray)): self.log.error('Given range of axes is no array type.') parameters = [ amplitude, x_zero, y_zero, sigma_x, sigma_y, theta, offset ] for var in parameters: if not isinstance(var, (float, int)): self.log.error('Given range of parameter is no float or int.') (x, y) = x_data_tuple x_zero = float(x_zero) y_zero = float(y_zero) a = (np.cos(theta)**2) / (2 * sigma_x**2) + (np.sin(theta)** 2) / (2 * sigma_y**2) b = -(np.sin(2 * theta)) / (4 * sigma_x**2) + (np.sin( 2 * theta)) / (4 * sigma_y**2) c = (np.sin(theta)**2) / (2 * sigma_x**2) + (np.cos(theta)** 2) / (2 * sigma_y**2) g = offset + amplitude * np.exp(-(a * ((x - x_zero)**2) + 2 * b * (x - x_zero) * (y - y_zero) + c * ((y - y_zero)**2))) return g.ravel() def gaussian_function(self, x_data=None, amplitude=None, x_zero=None, sigma=None, offset=None): """ This method provides a one dimensional gaussian function. @param array x_data: x values @param float or int amplitude: Amplitude of gaussian @param float or int x_zero: x value of maximum @param float or int sigma: standard deviation @param float or int offset: offset @return callable function: returns a 1D Gaussian function """ # check if parameters make sense if not isinstance(x_data, (frozenset, list, set, tuple, np.ndarray)): self.log.error('Given range of axis is no array type.') parameters = [amplitude, x_zero, sigma, offset] for var in parameters: if not isinstance(var, (float, int)): print('error', var) self.log.error('Given range of parameter is no float or int.') gaussian = amplitude * np.exp(-(x_data - x_zero)**2 / (2 * sigma**2)) + offset return gaussian
class QdplotLogic(GenericLogic): """ This logic module helps display user data in plots, and makes it easy to save. @signal sigCounterUpdate: there is new counting data available @signal sigCountContinuousNext: used to simulate a loop in which the data acquisition runs. @sigmal sigCountGatedNext: ??? """ sigPlotDataUpdated = QtCore.Signal() sigPlotParamsUpdated = QtCore.Signal() _modclass = 'QdplotLogic' _modtype = 'logic' # declare connectors savelogic = Connector(interface='SaveLogic') def __init__(self, **kwargs): """ Create QdplotLogic object with connectors. @param dict kwargs: optional parameters """ super().__init__(**kwargs) # locking for thread safety self.threadlock = Mutex() def on_activate(self): """ Initialisation performed during activation of the module. """ self.indep_vals = np.zeros((10, )) self.depen_vals = np.zeros((10, )) self.plot_domain = [0, 1] self.plot_range = [0, 1] self.set_hlabel() self.set_vlabel() self._save_logic = self.get_connector('savelogic') def on_deactivate(self): """ Deinitialisation performed during deactivation of the module. """ return def set_data(self, x=None, y=None, clear_old=True): """Set the data to plot @param np.ndarray/list or list of np.ndarrays/lists x: data of independents variable(s) @param np.ndarray/list or list of np.ndarrays/lists y: data of dependent variable(s) @param bool clear_old: clear old plots in GUI if True """ if x is None: self.log.error('No x-values provided, cannot set plot data.') return -1 if y is None: self.log.error('No y-values provided, cannot set plot data.') return -1 self.clear_old = clear_old # check if input is only an array (single plot) or a list of arrays (several plots) if len(x) == 1: self.indep_vals = [x] self.depen_vals = [y] else: self.indep_vals = x self.depen_vals = y self.sigPlotDataUpdated.emit() self.sigPlotParamsUpdated.emit() self.set_domain() self.set_range() return def set_domain(self, newdomain=None): """Set the plot domain, to match the data (default) or to a specified new domain. @param float newdomain: 2-element list containing min and max x-values """ # TODO: This needs to check that newdomain is a 2-element list with numerical values. if newdomain is not None: self.plot_domain = newdomain else: domain_min = np.min([np.min(values) for values in self.indep_vals]) domain_max = np.max([np.max(values) for values in self.indep_vals]) domain_range = domain_max - domain_min self.plot_domain = [ domain_min - 0.02 * domain_range, domain_max + 0.02 * domain_range ] self.sigPlotParamsUpdated.emit() return 0 def set_range(self, newrange=None): """Set the plot range, to match the data (default) or to a specified new range @param float newrange: 2-element list containing min and max y-values """ # TODO: This needs to check that newdomain is a 2-element list with numerical values. if newrange is not None: self.plot_range = newrange else: range_min = np.min([np.min(values) for values in self.depen_vals]) range_max = np.max([np.max(values) for values in self.depen_vals]) range_range = range_max - range_min self.plot_range = [ range_min - 0.02 * range_range, range_max + 0.02 * range_range ] self.sigPlotParamsUpdated.emit() return 0 def set_hlabel(self, label='Independent variable', units='arb. units'): """Set the horizontal axis label and specify units. @param string label: name of axis @param string units: symbol for units """ print('label_in_sethlabel', label) self.h_label = label self.h_units = units self.sigPlotParamsUpdated.emit() return 0 def set_vlabel(self, label='Dependent variable', units='arb. units'): """Set the vertical axis label and specify units. @param string label: name of axis @param string units: symbol for units """ print('label_in_setvlabel', label) self.v_label = label self.v_units = units self.sigPlotParamsUpdated.emit() return 0 def get_domain(self): return self.plot_domain def get_range(self): return self.plot_range def save_data(self, postfix=''): """ Save the data to a file. @param bool to_file: indicate, whether data have to be saved to file @param str postfix: an additional tag, which will be added to the filename upon save @return np.array([2 or 3][X]), OrderedDict: array with the """ # Set the parameters: parameters = OrderedDict() parameters['User-selected display domain'] = self.plot_domain parameters['User-selected display range'] = self.plot_range # If there is a postfix then add separating underscore if postfix == '': filelabel = 'qdplot' else: filelabel = postfix # Data labels indep_label = self.h_label + ' (' + self.h_units + ')' depen_label = self.v_label + ' (' + self.v_units + ')' # prepare the data in a dict or in an OrderedDict: data = OrderedDict() for ii in range(len(self.indep_vals)): data['indep_label' + str(ii + 1)] = self.indep_vals[ii] data['depen_label' + str(ii + 1)] = self.depen_vals[ii] # Prepare the figure to save as a "data thumbnail" plt.style.use(self._save_logic.mpl_qd_style) fig, ax1 = plt.subplots() for ii in range(len(self.indep_vals)): ax1.plot(self.indep_vals[ii], self.depen_vals[ii], linestyle=':', linewidth=1) ax1.set_xlabel(indep_label) ax1.set_ylabel(depen_label) ax1.set_xlim(self.plot_domain) ax1.set_ylim(self.plot_range) fig.tight_layout() filepath = self._save_logic.get_path_for_module(module_name='qdplot') # Call save logic to write everything to file self._save_logic.save_data(data, filepath=filepath, parameters=parameters, filelabel=filelabel, plotfig=fig, delimiter='\t') plt.close(fig) self.log.debug('Data saved to:\n{0}'.format(filepath))
class PoiManagerLogic(GenericLogic): """ This is the Logic class for mapping and tracking bright features in the confocal scan. """ _modclass = 'poimanagerlogic' _modtype = 'logic' # declare connectors optimiserlogic = Connector(interface='OptimizerLogic') scannerlogic = Connector(interface='ConfocalLogic') savelogic = Connector(interface='SaveLogic') # status vars _roi = StatusVar(default=dict()) # Notice constructor and representer further below _refocus_period = StatusVar(default=120) _active_poi = StatusVar(default=None) _move_scanner_after_optimization = StatusVar(default=True) # Signals for connecting modules sigRefocusStateUpdated = QtCore.Signal(bool) # is_active sigRefocusTimerUpdated = QtCore.Signal(bool, float, float) # is_active, period, remaining_time sigPoiUpdated = QtCore.Signal(str, str, np.ndarray) # old_name, new_name, current_position sigActivePoiUpdated = QtCore.Signal(str) sigRoiUpdated = QtCore.Signal(dict) # Dict containing ROI parameters to update # Internal signals __sigStartPeriodicRefocus = QtCore.Signal() __sigStopPeriodicRefocus = QtCore.Signal() def __init__(self, config, **kwargs): super().__init__(config=config, **kwargs) # timer for the periodic refocus self.__timer = None self._last_refocus = 0 self._periodic_refocus_poi = None # threading self._threadlock = Mutex() return def on_activate(self): """ Initialisation performed during activation of the module. """ self.__timer = QtCore.QTimer() self.__timer.setSingleShot(False) self._last_refocus = 0 self._periodic_refocus_poi = None # Connect callback for a finished refocus self.optimiserlogic().sigRefocusFinished.connect( self._optimisation_callback, QtCore.Qt.QueuedConnection) # Connect internal start/stop signals to decouple QTimer from other threads self.__sigStartPeriodicRefocus.connect( self.start_periodic_refocus, QtCore.Qt.QueuedConnection) self.__sigStopPeriodicRefocus.connect( self.stop_periodic_refocus, QtCore.Qt.QueuedConnection) # Initialise the ROI scan image (xy confocal image) if not present if self._roi.scan_image is None: self.set_scan_image(False) self.sigRoiUpdated.emit({'name': self.roi_name, 'poi_nametag': self.poi_nametag, 'pois': self.poi_positions, 'history': self.roi_pos_history, 'scan_image': self.roi_scan_image, 'scan_image_extent': self.roi_scan_image_extent}) self.sigActivePoiUpdated.emit('' if self.active_poi is None else self.active_poi) self.update_poi_tag_in_savelogic() return def on_deactivate(self): # Stop active processes/loops self.stop_periodic_refocus() # Disconnect signals self.optimiserlogic().sigRefocusFinished.disconnect() self.__sigStartPeriodicRefocus.disconnect() self.__sigStopPeriodicRefocus.disconnect() return @property def data_directory(self): return self.savelogic().data_dir @property def optimise_xy_size(self): return float(self.optimiserlogic().refocus_XY_size) @property def active_poi(self): return self._active_poi @active_poi.setter def active_poi(self, name): self.set_active_poi(name) return @property def poi_names(self): return self._roi.poi_names @property def poi_positions(self): return self._roi.poi_positions @property def poi_anchors(self): return self._roi.poi_anchors @property def roi_name(self): return self._roi.name @roi_name.setter def roi_name(self, name): self.rename_roi(new_name=name) @property def poi_nametag(self): return self._roi.poi_nametag @poi_nametag.setter def poi_nametag(self, tag): self.set_poi_nametag(tag) return @property def roi_origin(self): return self._roi.origin @property def roi_creation_time(self): return self._roi.creation_time @property def roi_creation_time_as_str(self): return self._roi.creation_time_as_str @property def roi_pos_history(self): return self._roi.pos_history @property def roi_scan_image(self): return self._roi.scan_image @property def roi_scan_image_extent(self): return self._roi.scan_image_extent @property def refocus_period(self): return float(self._refocus_period) @refocus_period.setter def refocus_period(self, period): self.set_refocus_period(period) return @property def time_until_refocus(self): if not self.__timer.isActive(): return -1 return max(0., self._refocus_period - (time.time() - self._last_refocus)) @property def scanner_position(self): return self.scannerlogic().get_position()[:3] @property def move_scanner_after_optimise(self): return bool(self._move_scanner_after_optimization) @move_scanner_after_optimise.setter def move_scanner_after_optimise(self, move): self.set_move_scanner_after_optimise(move) return @QtCore.Slot(int) @QtCore.Slot(bool) def set_move_scanner_after_optimise(self, move): with self._threadlock: self._move_scanner_after_optimization = bool(move) return @QtCore.Slot(str) def set_poi_nametag(self, tag): if tag is None or isinstance(tag, str): if tag == '': tag = None self._roi.poi_nametag = tag self.sigRoiUpdated.emit({'poi_nametag': self.poi_nametag}) else: self.log.error('POI name tag must be str or None.') return @QtCore.Slot() @QtCore.Slot(np.ndarray) def add_poi(self, position=None, name=None, emit_change=True): """ Creates a new POI and adds it to the current ROI. POI can be optionally initialized with position and name. @param str name: Name for the POI (must be unique within ROI). None (default) will create generic name. @param scalar[3] position: Iterable of length 3 representing the (x, y, z) position with respect to the ROI origin. None (default) causes the current scanner crosshair position to be used. @param bool emit_change: Flag indicating if the changed POI set should be signaled. """ # Get current scanner position from scannerlogic if no position is provided. if position is None: position = self.scanner_position current_poi_set = set(self.poi_names) # Add POI to current ROI self._roi.add_poi(position=position, name=name) # Get newly added POI name from comparing POI names before and after addition of new POI poi_name = set(self.poi_names).difference(current_poi_set).pop() # Notify about a changed set of POIs if necessary if emit_change: self.sigPoiUpdated.emit('', poi_name, self.get_poi_position(poi_name)) # Set newly created POI as active poi self.set_active_poi(poi_name) return @QtCore.Slot() def delete_poi(self, name=None): """ Deletes the given poi from the ROI. @param str name: Name of the POI to delete. If None (default) delete active POI. @param bool emit_change: Flag indicating if the changed POI set should be signaled. """ if len(self.poi_names) == 0: self.log.warning('Can not delete POI. No POI present in ROI.') return if name is None: if self.active_poi is None: self.log.error('No POI name to delete and no active POI set.') return else: name = self.active_poi self._roi.delete_poi(name) if self.active_poi == name: if len(self.poi_names) > 0: self.set_active_poi(self.poi_names[0]) else: self.set_active_poi(None) # Notify about a changed set of POIs if necessary self.sigPoiUpdated.emit(name, '', np.zeros(3)) return @QtCore.Slot(str) @QtCore.Slot(str, str) def rename_poi(self, new_name, name=None): """ @param str name: @param str new_name: """ if not isinstance(new_name, str) or not new_name: self.log.error('POI name to set must be str of length > 0.') return if name is None: if self.active_poi is None: self.log.error('Unable to rename POI. No POI name given and no active POI set.') return else: name = self.active_poi self._roi.rename_poi(name=name, new_name=new_name) self.sigPoiUpdated.emit(name, new_name, self.get_poi_position(new_name)) if self.active_poi == name: self.set_active_poi(new_name) return @QtCore.Slot(str) def set_active_poi(self, name=None): """ Set the name of the currently active POI @param name: """ if not isinstance(name, str) and name is not None: self.log.error('POI name must be of type str or None.') elif name is None or name == '': self._active_poi = None elif name in self.poi_names: self._active_poi = str(name) else: self.log.error('No POI with name "{0}" found in POI list.'.format(name)) self.sigActivePoiUpdated.emit('' if self.active_poi is None else self.active_poi) self.update_poi_tag_in_savelogic() return def get_poi_position(self, name=None): """ Returns the POI position of the specified POI or the active POI if none is given. @param str name: Name of the POI to return the position for. If None (default) the active POI position is returned. @return float[3]: Coordinates of the desired POI (x,y,z) """ if name is None: name = self.active_poi return self._roi.get_poi_position(name) def get_poi_anchor(self, name=None): """ Returns the POI anchor position (excluding sample movement) of the specified POI or the active POI if none is given. @param str name: Name of the POI to return the position for. If None (default) the active POI position is returned. @return float[3]: Coordinates of the desired POI anchor (x,y,z) """ if name is None: name = self.active_poi return self._roi.get_poi_anchor(name) @QtCore.Slot() def move_roi_from_poi_position(self, name=None, position=None): if position is None: position = self.scanner_position if name is None: if self.active_poi is None: self.log.error('Unable to set POI position. ' 'No POI name given and no active POI set.') return else: name = self.active_poi if len(position) != 3: self.log.error('POI position must be iterable of length 3.') return if not isinstance(name, str): self.log.error('POI name must be of type str.') shift = position - self.get_poi_position(name) self.add_roi_position(self.roi_origin + shift) return @QtCore.Slot() def set_poi_anchor_from_position(self, name=None, position=None): if position is None: position = self.scanner_position if name is None: if self.active_poi is None: self.log.error('Unable to set POI position. ' 'No POI name given and no active POI set.') return else: name = self.active_poi if len(position) != 3: self.log.error('POI position must be iterable of length 3.') return if not isinstance(name, str): self.log.error('POI name must be of type str.') shift = position - self.get_poi_position(name) self._roi.set_poi_anchor(name, self.get_poi_anchor(name) + shift) self.sigPoiUpdated.emit(name, name, self.get_poi_position(name)) return @QtCore.Slot(str) def rename_roi(self, new_name): if not isinstance(new_name, str) or new_name == '': self.log.error('ROI name to set must be str of length > 0.') return self._roi.name = new_name self.sigRoiUpdated.emit({'name': self.roi_name}) return @QtCore.Slot(np.ndarray) def add_roi_position(self, position): self._roi.add_history_entry(position) self.sigRoiUpdated.emit({'pois': self.poi_positions, 'history': self.roi_pos_history, 'scan_image': self.roi_scan_image, 'scan_image_extent': self.roi_scan_image_extent}) return @QtCore.Slot() @QtCore.Slot(int) def delete_history_entry(self, history_index=-1): """ Delete an entry in the ROI history. Deletes the last position by default. @param int|slice history_index: List index for history entry """ old_roi_origin = self.roi_origin self._roi.delete_history_entry(history_index) if np.any(old_roi_origin != self.roi_origin): self.sigRoiUpdated.emit({'pois': self.poi_positions, 'history': self.roi_pos_history, 'scan_image': self.roi_scan_image, 'scan_image_extent': self.roi_scan_image_extent}) else: self.sigRoiUpdated.emit({'history': self.roi_pos_history}) return @QtCore.Slot() def go_to_poi(self, name=None): """ Move crosshair to the given poi. @param str name: the name of the POI """ if name is None: name = self.active_poi if not isinstance(name, str): self.log.error('POI name to move to must be of type str.') return self.move_scanner(self.get_poi_position(name)) return def move_scanner(self, position): if len(position) != 3: self.log.error('Scanner position to set must be iterable of length 3.') return self.scannerlogic().set_position('poimanager', x=position[0], y=position[1], z=position[2]) return @QtCore.Slot() def set_scan_image(self, emit_change=True): """ Get the current xy scan data and set as scan_image of ROI. """ self._roi.set_scan_image( self.scannerlogic().xy_image[:, :, 3], (tuple(self.scannerlogic().image_x_range), tuple(self.scannerlogic().image_y_range))) if emit_change: self.sigRoiUpdated.emit({'scan_image': self.roi_scan_image, 'scan_image_extent': self.roi_scan_image_extent}) return @QtCore.Slot() def reset_roi(self): self.stop_periodic_refocus() self._roi = RegionOfInterest() self.set_scan_image(False) self.sigRoiUpdated.emit({'name': self.roi_name, 'poi_nametag': self.poi_nametag, 'pois': self.poi_positions, 'history': self.roi_pos_history, 'scan_image': self.roi_scan_image, 'scan_image_extent': self.roi_scan_image_extent}) self.set_active_poi(None) return @QtCore.Slot(int) @QtCore.Slot(float) def set_refocus_period(self, period): """ Change the duration of the periodic optimise timer during active periodic refocusing. @param float period: The time between optimisation procedures. """ if period < 0: self.log.error('Refocus period must be a value > 0. Unable to set period of "{0}".' ''.format(period)) return # Acquire thread lock in order to change the period during a running periodic refocus with self._threadlock: self._refocus_period = float(period) if self.__timer.isActive(): self.sigRefocusTimerUpdated.emit(True, self.refocus_period, self.time_until_refocus) else: self.sigRefocusTimerUpdated.emit(False, self.refocus_period, self.refocus_period) return def start_periodic_refocus(self, name=None): """ Starts periodic refocusing of the POI <name>. @param str name: The name of the POI to be refocused periodically. If None (default) perform periodic refocus on active POI. """ if name is None: if self.active_poi is None: self.log.error('Unable to start periodic refocus. No POI name given and no active ' 'POI set.') return else: name = self.active_poi if name not in self.poi_names: self.log.error('No POI with name "{0}" found in POI list.\n' 'Unable to start periodic refocus.') return with self._threadlock: if self.__timer.isActive(): self.log.error('Periodic refocus already running. Unable to start a new one.') return self.module_state.lock() self._periodic_refocus_poi = name self.optimise_poi_position(name=name) self._last_refocus = time.time() self.__timer.timeout.connect(self._periodic_refocus_loop) self.__timer.start(500) self.sigRefocusTimerUpdated.emit(True, self.refocus_period, self.refocus_period) return def stop_periodic_refocus(self): """ Stops the periodic refocusing of the POI. """ with self._threadlock: if self.__timer.isActive(): self.__timer.stop() self.__timer.timeout.disconnect() self._periodic_refocus_poi = None self.module_state.unlock() self.sigRefocusTimerUpdated.emit(False, self.refocus_period, self.refocus_period) return @QtCore.Slot(bool) def toggle_periodic_refocus(self, switch_on): """ @param switch_on: """ if switch_on: self.__sigStartPeriodicRefocus.emit() else: self.__sigStopPeriodicRefocus.emit() return @QtCore.Slot() def _periodic_refocus_loop(self): """ This is the looped function that does the actual periodic refocus. If the time has run out, it refocuses the current poi. Otherwise it just updates the time that is left. """ with self._threadlock: if self.__timer.isActive(): remaining_time = self.time_until_refocus self.sigRefocusTimerUpdated.emit(True, self.refocus_period, remaining_time) if remaining_time <= 0 and self.optimiserlogic().module_state() == 'idle': self.optimise_poi_position(self._periodic_refocus_poi) self._last_refocus = time.time() return @QtCore.Slot() def optimise_poi_position(self, name=None, update_roi_position=True): """ Triggers the optimisation procedure for the given poi using the optimiserlogic. The difference between old and new position can be used to update the ROI position. This function will return immediately. The function "_optimisation_callback" will handle the aftermath of the optimisation. @param str name: Name of the POI for which to optimise the position. @param bool update_roi_position: Flag indicating if the ROI should be shifted accordingly. """ if name is None: if self.active_poi is None: self.log.error('Unable to optimize POI position. ' 'No POI name given and not active POI set.') return else: name = self.active_poi if update_roi_position: tag = 'poimanagermoveroi_{0}'.format(name) else: tag = 'poimanager_{0}'.format(name) if self.optimiserlogic().module_state() == 'idle': self.optimiserlogic().start_refocus(initial_pos=self.get_poi_position(name), caller_tag=tag) self.sigRefocusStateUpdated.emit(True) else: self.log.warning('Unable to start POI refocus procedure. ' 'OptimizerLogic module is still locked.') return def _optimisation_callback(self, caller_tag, optimal_pos): """ Callback function for a finished position optimisation. If desired the relative shift of the optimised POI can be used to update the ROI position. The scanner is moved to the optimised POI if desired. @param caller_tag: @param optimal_pos: """ # If the refocus was initiated by poimanager, update POI and ROI position if caller_tag.startswith('poimanager_') or caller_tag.startswith('poimanagermoveroi_'): shift_roi = caller_tag.startswith('poimanagermoveroi_') poi_name = caller_tag.split('_', 1)[1] if poi_name in self.poi_names: # We only need x, y, z optimal_pos = np.array(optimal_pos[:3], dtype=float) if shift_roi: self.move_roi_from_poi_position(name=poi_name, position=optimal_pos) else: self.set_poi_anchor_from_position(name=poi_name, position=optimal_pos) if self._move_scanner_after_optimization: self.move_scanner(position=optimal_pos) self.sigRefocusStateUpdated.emit(False) return def update_poi_tag_in_savelogic(self): if not self._active_poi: self.savelogic().remove_additional_parameter('Active POI') else: self.savelogic().update_additional_parameters({'Active POI': self._active_poi}) def save_roi(self): """ Save all current absolute POI coordinates to a file. Save ROI history to a second file. Save ROI scan image (if present) to a third file (binary numpy .npy-format). """ # File path and names filepath = self.savelogic().get_path_for_module(module_name='ROIs') roi_name_no_blanks = self.roi_name.replace(' ', '_') timestamp = datetime.now() pois_filename = '{0}_poi_list'.format(roi_name_no_blanks) roi_history_filename = '{0}_{1}_history.npy'.format( timestamp.strftime('%Y%m%d-%H%M-%S'), roi_name_no_blanks) roi_image_filename = '{0}_{1}_scan_image.npy'.format( timestamp.strftime('%Y%m%d-%H%M-%S'), roi_name_no_blanks) # Metadata to save in both file headers x_extent, y_extent = self.roi_scan_image_extent parameters = OrderedDict() parameters['roi_name'] = self.roi_name parameters['poi_nametag'] = '' if self.poi_nametag is None else self.poi_nametag parameters['roi_creation_time'] = self.roi_creation_time_as_str parameters['scan_image_x_extent'] = '{0:.9e},{1:.9e}'.format(*x_extent) parameters['scan_image_y_extent'] = '{0:.9e},{1:.9e}'.format(*y_extent) ################################## # Save POI positions to first file ################################## poi_dict = self.poi_positions poi_positions = np.array(tuple(poi_dict.values()), dtype=float) data = OrderedDict() # Save POI names in the first column data['name'] = np.array(tuple(poi_dict), dtype=str) # Save x,y,z coordinates in the following 3 columns data['X (m)'] = poi_positions[:, 0] data['Y (m)'] = poi_positions[:, 1] data['Z (m)'] = poi_positions[:, 2] self.savelogic().save_data(data, timestamp=timestamp, filepath=filepath, parameters=parameters, filelabel=pois_filename, fmt=['%s', '%.6e', '%.6e', '%.6e']) ############################################ # Save ROI history to second file (binary) if present ############################################ if len(self.roi_pos_history) > 1: np.save(os.path.join(filepath, roi_history_filename), self.roi_pos_history) ####################################################### # Save ROI scan image to third file (binary) if present ####################################################### if self.roi_scan_image is not None: np.save(os.path.join(filepath, roi_image_filename), self.roi_scan_image) return def load_roi(self, complete_path=None): if complete_path is None: return filepath, filename = os.path.split(complete_path) # Try to detect legacy file format is_legacy_format = False if not complete_path.endswith('_poi_list.dat'): self.log.info('Trying to read ROI from legacy file format...') with open(complete_path, 'r') as file: for line in file.readlines(): if line.strip() == '#POI Name\tPOI Key\tX\tY\tZ': is_legacy_format = True elif not line.startswith('#'): break if not is_legacy_format: self.log.error('Unable to load ROI from file. File format not understood.') return if is_legacy_format: filetag = filename.split('_', 1)[1].rsplit('.dat', 1)[0] else: filetag = filename.rsplit('_poi_list.dat', 1)[0] # Read POI data as well as roi metadata from textfile poi_names = np.loadtxt(complete_path, delimiter='\t', usecols=0, dtype=str) if is_legacy_format: poi_coords = np.loadtxt(complete_path, delimiter='\t', usecols=(2, 3, 4), dtype=float) else: poi_coords = np.loadtxt(complete_path, delimiter='\t', usecols=(1, 2, 3), dtype=float) # Create list of POI instances poi_list = [PointOfInterest(pos, poi_names[i]) for i, pos in enumerate(poi_coords)] roi_name = None poi_nametag = None roi_creation_time = None scan_extent = None if is_legacy_format: roi_name = filetag else: with open(complete_path, 'r') as file: for line in file.readlines(): if not line.startswith('#'): break if line.startswith('#roi_name:'): roi_name = line.split('#roi_name:', 1)[1].strip() elif line.startswith('#poi_nametag:'): poi_nametag = line.split('#poi_nametag:', 1)[1].strip() elif line.startswith('#roi_creation_time:'): roi_creation_time = line.split('#roi_creation_time:', 1)[1].strip() elif line.startswith('#scan_image_x_extent:'): scan_x_extent = line.split('#scan_image_x_extent:', 1)[1].strip().split(',') elif line.startswith('#scan_image_y_extent:'): scan_y_extent = line.split('#scan_image_y_extent:', 1)[1].strip().split(',') scan_extent = ((float(scan_x_extent[0]), float(scan_x_extent[1])), (float(scan_y_extent[0]), float(scan_y_extent[1]))) poi_nametag = None if not poi_nametag else poi_nametag # Read ROI position history from binary file history_filename = os.path.join(filepath, '{0}_history.npy'.format(filetag)) try: roi_history = np.load(history_filename) except FileNotFoundError: roi_history = None # Read ROI scan image from binary file image_filename = os.path.join(filepath, '{0}_scan_image.npy'.format(filetag)) try: roi_scan_image = np.load(image_filename) except FileNotFoundError: roi_scan_image = None # Reset current ROI and initialize new one from loaded data self.reset_roi() self._roi = RegionOfInterest(name=roi_name, creation_time=roi_creation_time, history=roi_history, scan_image=roi_scan_image, scan_image_extent=scan_extent, poi_list=poi_list, poi_nametag=poi_nametag) print(poi_nametag, self.poi_nametag) self.sigRoiUpdated.emit({'name': self.roi_name, 'poi_nametag': self.poi_nametag, 'pois': self.poi_positions, 'history': self.roi_pos_history, 'scan_image': self.roi_scan_image, 'scan_image_extent': self.roi_scan_image_extent}) self.set_active_poi(None if len(poi_names) == 0 else poi_names[0]) return @_roi.constructor def dict_to_roi(self, roi_dict): return RegionOfInterest.from_dict(roi_dict) @_roi.representer def roi_to_dict(self, roi): return roi.to_dict() def transform_roi(self, transform_matrix): # TODO: Implement this if transform_matrix.shape != (3, 3): self.log.error('Tranformation matrix must be numpy array of shape (3, 3).') return self.log.error('Tranformation of all POI positions not implemented yet.') return
class MagnetControlLogic(GenericLogic): """This is the Logic class for ODMR.""" _modclass = 'magnetcontrollogic' _modtype = 'logic' # declare connectors fitlogic = Connector(interface='FitLogic') savelogic = Connector(interface='SaveLogic') magnetstage = Connector(interface='magnet_control_interface') counter = Connector(interface='CounterLogic') curr_x_pos = StatusVar('curr_x_pos', 0.0000) curr_y_pos = StatusVar('curr_y_pos', 0.0000) curr_z_pos = StatusVar('curr_z_pos', 0.0000) set_x_pos = StatusVar('set_x_pos', 0.0000) set_y_pos = StatusVar('set_y_pos', 0.0000) set_z_pos = StatusVar('set_z_pos', 0.0000) N_AF_points = StatusVar('N_AF_points', 10) x_start = StatusVar('x_start', 9.4600) x_end = StatusVar('x_end', 9.9600) step_x = StatusVar('step_x', 0.0300) n_x_points = StatusVar('n_x_points', 0.0) y_start = StatusVar('y_start', 9.4600) y_end = StatusVar('y_end', 9.9600) step_y = StatusVar('step_y', 0.0300) n_y_points = StatusVar('n_y_points', 0.0) Xmax = StatusVar('Xmax', 0.0000) Ymax = StatusVar('Ymax', 0.0000) x_scan_fit_x = StatusVar('x_scan_fit_x', 0.0000) x_scan_fit_y = StatusVar('x_scan_fit_x', 0.0000) y_scan_fit_x = StatusVar('x_scan_fit_x', 0.0000) y_scan_fit_y = StatusVar('x_scan_fit_x', 0.0000) fc = StatusVar('fits', None) i = StatusVar('i', 0) motion_time = StatusVar('motion_time', 0.0000) fluorescence_integration_time = StatusVar('fluorescence_integration_time', 0.5) # Update signals, e.g. for GUI module sigPlotXUpdated = QtCore.Signal(np.ndarray, np.ndarray) sigPlotYUpdated = QtCore.Signal(np.ndarray, np.ndarray) sigFitXUpdated = QtCore.Signal(np.ndarray, np.ndarray, dict, str) sigFitYUpdated = QtCore.Signal(np.ndarray, np.ndarray, dict, str) sigPositionUpdated = QtCore.Signal() sigNextXPoint = QtCore.Signal() sigNextYPoint = QtCore.Signal() signal_stop_scanning = QtCore.Signal() def __init__(self, config, **kwargs): super().__init__(config=config, **kwargs) self.threadlock = Mutex() def on_activate(self): """ Initialisation performed during activation of the module. """ # Get connectors self._magnetstage = self.get_connector('magnetstage') self._fit_logic = self.get_connector('fitlogic') self._counter = self.get_connector('counter') self._save_logic = self.get_connector('savelogic') # Set flags # for stopping a measurement self.stopRequested = False # Initalize the ODMR data arrays (mean signal and sweep matrix) self._initialize_plots() # Connect signals self.sigNextXPoint.connect(self._next_x_point, QtCore.Qt.QueuedConnection) self.sigNextYPoint.connect(self._next_y_point, QtCore.Qt.QueuedConnection) return def on_deactivate(self): """ Deinitialisation performed during deactivation of the module. """ return @fc.constructor def sv_set_fits(self, val): # Setup fit container fc = self.fitlogic().make_fit_container('length', '1d') fc.set_units(['mm', 'c/s']) if isinstance(val, dict) and len(val) > 0: fc.load_from_dict(val) else: d1 = OrderedDict() d1['Gaussian peak'] = { 'fit_function': 'gaussian', 'estimator': 'peak' } d1['Lorentzian peak'] = { 'fit_function': 'lorentzian', 'estimator': 'peak' } d1['Two Lorentzian dips'] = { 'fit_function': 'lorentziandouble', 'estimator': 'dip' } d1['N14'] = { 'fit_function': 'lorentziantriple', 'estimator': 'N14' } d1['N15'] = { 'fit_function': 'lorentziandouble', 'estimator': 'N15' } default_fits = OrderedDict() default_fits['1d'] = d1['Gaussian peak'] fc.load_from_dict(default_fits) return fc @fc.representer def sv_get_fits(self, val): """ save configured fits """ if len(val.fit_list) > 0: return val.save_to_dict() else: return None def _initialize_plots(self): """ Initializing the ODMR plots (line and matrix). """ self.fluor_plot_x = np.arange(self.x_start, self.x_end, self.step_x) self.fluor_plot_y = np.zeros(self.fluor_plot_x.size) self.x_scan_fit_x = np.arange(self.x_start, self.x_end, self.step_x) self.x_scan_fit_y = np.zeros(self.fluor_plot_x.size) self.yfluor_plot_x = np.arange(self.y_start, self.y_end, self.step_y) self.yfluor_plot_y = np.zeros(self.yfluor_plot_x.size) self.y_scan_fit_x = np.arange(self.y_start, self.y_end, self.step_y) self.y_scan_fit_y = np.zeros(self.yfluor_plot_x.size) self.sigPlotXUpdated.emit(self.fluor_plot_x, self.fluor_plot_y) self.sigPlotYUpdated.emit(self.yfluor_plot_x, self.yfluor_plot_y) current_x_fit = self.fc.current_fit self.sigFitXUpdated.emit(self.x_scan_fit_x, self.x_scan_fit_y, {}, current_x_fit) current_y_fit = self.fc.current_fit self.sigFitXUpdated.emit(self.y_scan_fit_x, self.y_scan_fit_y, {}, current_y_fit) return def get_current_position(self): try: self.curr_x_pos = float( self._magnetstage.get_current_position(1)[3:-2]) except pyvisa.errors.VisaIOError: print('visa error') time.sleep(0.05) try: self.curr_x_pos = float( self._magnetstage.get_current_position(1)[3:-2]) except pyvisa.errors.VisaIOError: print('visa error') time.sleep(0.05) self.curr_x_pos = float( self._magnetstage.get_current_position(1)[3:-2]) time.sleep(0.05) try: self.curr_y_pos = float( self._magnetstage.get_current_position(2)[3:-2]) except pyvisa.errors.VisaIOError: print('visa error') time.sleep(0.05) self.curr_y_pos = float( self._magnetstage.get_current_position(2)[3:-2]) self.curr_z_pos = float( self._magnetstage.get_current_position(3)[3:-2]) return def set_position(self): self._magnetstage.move_absolute(1, self.set_x_pos) self._magnetstage.move_absolute(2, self.set_y_pos) self._magnetstage.move_absolute(3, self.set_z_pos) return def start_x_scanning(self, tag='logic'): """Starts scanning """ with self.threadlock: if self.module_state() == 'locked': self.log.error( 'Can not start fluorescence scan. Logic is already locked.' ) return -1 self.module_state.lock() self.stopRequested = False self.step_x = int(self.step_x / 2e-4) * 2e-4 self.fluor_plot_x = np.arange(self.x_start, self.x_end, self.step_x) self.fluor_plot_y = np.zeros((len(self.fluor_plot_x))) self.curr_x_pos = float( self._magnetstage.get_current_position(1)[3:-2]) time.sleep(0.1) self.motion_time = float( self._magnetstage.get_motiontime_relativemove( 1, self.step_x)[3:-2]) + 0.05 if self.x_start != self.curr_x_pos: t = float( self._magnetstage.get_motiontime_relativemove( 1, np.abs(self.x_start - self.curr_x_pos))[3:-2]) self._magnetstage.move_absolute(1, self.x_start) time.sleep(t + 1) self.get_current_position() self.i = 0 self.sigNextXPoint.emit() return 0 def _next_x_point(self): with self.threadlock: # If the odmr measurement is not running do nothing if self.module_state() != 'locked': return # Stop measurement if stop has been requested if self.stopRequested: self.stopRequested = False self._magnetstage.stop_motion(1) self.signal_stop_scanning.emit() self.module_state.unlock() return # Move the magnet self._magnetstage.move_relative(1, self.step_x) time.sleep(self.motion_time) self.curr_x_pos = float( self._magnetstage.get_current_position(1)[3:-2]) time.sleep(0.1) if self.curr_x_pos > (self.x_end + self.step_x): self.stopRequested = False self._magnetstage.stop_motion(1) self.module_state.unlock() self.signal_stop_scanning.emit() return # Acquire count data if self.i <= (self.n_x_points - 1): self.fluor_plot_y[ self.i] = self._perform_fluorescence_measure()[0] self.i = self.i + 1 else: self.module_state.unlock() self.signal_stop_scanning.emit() return # Fire update signals self.sigPlotXUpdated.emit(self.fluor_plot_x, self.fluor_plot_y) self.sigPositionUpdated.emit() self.sigNextXPoint.emit() return def start_y_scanning(self, tag='logic'): """Starts scanning """ with self.threadlock: if self.module_state() == 'locked': self.log.error( 'Can not start ODMR scan. Logic is already locked.') return -1 self.module_state.lock() self.stopRequested = False self.step_y = int(self.step_y / 2e-4) * 2e-4 self.yfluor_plot_x = np.arange(self.y_start, self.y_end, self.step_y) self.yfluor_plot_y = np.zeros((len(self.yfluor_plot_x))) self.curr_y_pos = float( self._magnetstage.get_current_position(1)[3:-2]) time.sleep(0.1) self.motion_time = float( self._magnetstage.get_motiontime_relativemove( 2, self.step_y)[3:-2]) + 0.05 if self.y_start != self.curr_y_pos: t = float( self._magnetstage.get_motiontime_relativemove( 2, np.abs(self.y_start - self.curr_y_pos))[3:-2]) self._magnetstage.move_absolute(2, self.y_start) time.sleep(t + 1) self.get_current_position() self.i = 0 self.sigNextYPoint.emit() return 0 def _next_y_point(self): with self.threadlock: # If the odmr measurement is not running do nothing if self.module_state() != 'locked': return # Stop measurement if stop has been requested if self.stopRequested: self.stopRequested = False self._magnetstage.stop_motion(2) self.signal_stop_scanning.emit() self.module_state.unlock() return # Move the magnet self._magnetstage.move_relative(2, self.step_y) time.sleep(self.motion_time + 0.1) self.curr_y_pos = float( self._magnetstage.get_current_position(2)[3:-2]) time.sleep(0.1) if self.curr_y_pos > (self.y_end + self.step_y): self.stopRequested = False self._magnetstage.stop_motion(2) self.module_state.unlock() self.signal_stop_scanning.emit() return # Acquire count data if self.i <= (self.n_y_points - 1): self.yfluor_plot_y[ self.i] = self._perform_fluorescence_measure()[0] self.i = self.i + 1 else: self.module_state.unlock() self.signal_stop_scanning.emit() return # Fire update signals self.sigPlotYUpdated.emit(self.yfluor_plot_x, self.yfluor_plot_y) self.sigPositionUpdated.emit() self.sigNextYPoint.emit() return def stop_scanning(self): """Stops the scan @return int: error code (0:OK, -1:error) """ with self.threadlock: if self.module_state() == 'locked': self.stopRequested = True self.signal_stop_scanning.emit() return 0 def _perform_fluorescence_measure(self): #FIXME: that should be run through the TaskRunner! Implement the call # by not using this connection! if self._counter.get_counting_mode() != 0: self._counter.set_counting_mode(mode='CONTINUOUS') self._counter.start_saving() time.sleep(self.fluorescence_integration_time) self._counter.stopCount() data_array, parameters = self._counter.save_data(to_file=False) data_array = np.array(data_array)[:, 1] return data_array.mean(), parameters def get_fit_x_functions(self): """ Return the hardware constraints/limits @return list(str): list of fit function names """ return list(self.fc.fit_list) def get_fit_y_functions(self): """ Return the hardware constraints/limits @return list(str): list of fit function names """ return list(self.fc.fit_list) def do_x_fit(self, fit_function=None, x_data=None, y_data=None): """ Execute the currently configured fit on the measurement data. Optionally on passed data """ if (x_data is None) or (y_data is None): x_data = self.fluor_plot_x y_data = self.fluor_plot_y if fit_function is not None and isinstance(fit_function, str): if fit_function in self.get_fit_x_functions(): self.fc.set_current_fit(fit_function) else: self.fc.set_current_fit('No Fit') if fit_function != 'No Fit': self.log.warning( 'Fit function "{0}" not available in ODMRLogic fit container.' ''.format(fit_function)) self.x_scan_fit_x, self.x_scan_fit_y, result = self.fc.do_fit( x_data, y_data) if result is None: result_str_dict = {} else: result_str_dict = result.result_str_dict # print(result.result_str_dict) self.sigFitXUpdated.emit(self.x_scan_fit_x, self.x_scan_fit_y, result_str_dict, self.fc.current_fit) return def do_y_fit(self, fit_function=None, x_data=None, y_data=None): """ Execute the currently configured fit on the measurement data. Optionally on passed data """ if (x_data is None) or (y_data is None): x_data = self.yfluor_plot_x y_data = self.yfluor_plot_y if fit_function is not None and isinstance(fit_function, str): if fit_function in self.get_fit_y_functions(): self.fc.set_current_fit(fit_function) else: self.fc.set_current_fit('No Fit') if fit_function != 'No Fit': self.log.warning( 'Fit function "{0}" not available in ODMRLogic fit container.' ''.format(fit_function)) self.y_scan_fit_x, self.y_scan_fit_y, result = self.fc.do_fit( x_data, y_data) if result is None: result_str_dict = {} else: result_str_dict = result.result_str_dict self.sigFitYUpdated.emit(self.y_scan_fit_x, self.y_scan_fit_y, result_str_dict, self.fc.current_fit) return def save_data(self, tag=None, colorscale_range=None, percentile_range=None): """ Saves the current data to a file.""" if tag is None: tag = '' # two paths to save the raw data and the odmr scan data. filepath = self._save_logic.get_path_for_module(module_name='MAGNET') filepath2 = self._save_logic.get_path_for_module(module_name='MAGNET') timestamp = datetime.datetime.now() if len(tag) > 0: filelabel = tag + '_ODMR_data' filelabel2 = tag + '_ODMR_data_raw' else: filelabel = 'ODMR_data' filelabel2 = 'ODMR_data_raw' # prepare the data in a dict or in an OrderedDict: data = OrderedDict() data2 = OrderedDict() data['frequency (Hz)'] = self.odmr_plot_x data['count data (counts/s)'] = self.odmr_plot_y data2['count data (counts/s)'] = self.odmr_raw_data[:self. elapsed_sweeps, :] parameters = OrderedDict() parameters['Microwave CW Power (dBm)'] = self.cw_mw_power parameters['Microwave Sweep Power (dBm)'] = self.sweep_mw_power parameters['Run Time (s)'] = self.run_time parameters['Number of frequency sweeps (#)'] = self.elapsed_sweeps parameters['Start Frequency (Hz)'] = self.mw_start parameters['Stop Frequency (Hz)'] = self.mw_stop parameters['Step size (Hz)'] = self.mw_step parameters['Clock Frequency (Hz)'] = self.clock_frequency if self.fc.current_fit != 'No Fit': parameters['Fit function'] = self.fc.current_fit # add all fit parameter to the saved data: for name, param in self.fc.current_fit_param.items(): parameters[name] = str(param) fig = self.draw_figure(cbar_range=colorscale_range, percentile_range=percentile_range) self._save_logic.save_data(data, filepath=filepath, parameters=parameters, filelabel=filelabel, fmt='%.6e', delimiter='\t', timestamp=timestamp, plotfig=fig) self._save_logic.save_data(data2, filepath=filepath2, parameters=parameters, filelabel=filelabel2, fmt='%.6e', delimiter='\t', timestamp=timestamp) self.log.info('ODMR data saved to:\n{0}'.format(filepath)) return def draw_figure(self, cbar_range=None, percentile_range=None): """ Draw the summary figure to save with the data. @param: list cbar_range: (optional) [color_scale_min, color_scale_max]. If not supplied then a default of data_min to data_max will be used. @param: list percentile_range: (optional) Percentile range of the chosen cbar_range. @return: fig fig: a matplotlib figure object to be saved to file. """ freq_data = self.odmr_plot_x count_data = self.odmr_plot_y fit_freq_vals = self.odmr_fit_x fit_count_vals = self.odmr_fit_y matrix_data = self.odmr_plot_xy # If no colorbar range was given, take full range of data if cbar_range is None: cbar_range = np.array([np.min(matrix_data), np.max(matrix_data)]) else: cbar_range = np.array(cbar_range) prefix = ['', 'k', 'M', 'G', 'T'] prefix_index = 0 # Rescale counts data with SI prefix while np.max(count_data) > 1000: count_data = count_data / 1000 fit_count_vals = fit_count_vals / 1000 prefix_index = prefix_index + 1 counts_prefix = prefix[prefix_index] # Rescale frequency data with SI prefix prefix_index = 0 while np.max(freq_data) > 1000: freq_data = freq_data / 1000 fit_freq_vals = fit_freq_vals / 1000 prefix_index = prefix_index + 1 mw_prefix = prefix[prefix_index] # Rescale matrix counts data with SI prefix prefix_index = 0 while np.max(matrix_data) > 1000: matrix_data = matrix_data / 1000 cbar_range = cbar_range / 1000 prefix_index = prefix_index + 1 cbar_prefix = prefix[prefix_index] # Use qudi style plt.style.use(self._save_logic.mpl_qd_style) # Create figure fig, (ax_mean, ax_matrix) = plt.subplots(nrows=2, ncols=1) ax_mean.plot(freq_data, count_data, linestyle=':', linewidth=0.5) # Do not include fit curve if there is no fit calculated. if max(fit_count_vals) > 0: ax_mean.plot(fit_freq_vals, fit_count_vals, marker='None') ax_mean.set_ylabel('Fluorescence (' + counts_prefix + 'c/s)') ax_mean.set_xlim(np.min(freq_data), np.max(freq_data)) matrixplot = ax_matrix.imshow( matrix_data, cmap=plt.get_cmap('inferno'), # reference the right place in qd origin='lower', vmin=cbar_range[0], vmax=cbar_range[1], extent=[ np.min(freq_data), np.max(freq_data), 0, self.number_of_lines ], aspect='auto', interpolation='nearest') ax_matrix.set_xlabel('Frequency (' + mw_prefix + 'Hz)') ax_matrix.set_ylabel('Scan #') # Adjust subplots to make room for colorbar fig.subplots_adjust(right=0.8) # Add colorbar axis to figure cbar_ax = fig.add_axes([0.85, 0.15, 0.02, 0.7]) # Draw colorbar cbar = fig.colorbar(matrixplot, cax=cbar_ax) cbar.set_label('Fluorescence (' + cbar_prefix + 'c/s)') # remove ticks from colorbar for cleaner image cbar.ax.tick_params(which=u'both', length=0) # If we have percentile information, draw that to the figure if percentile_range is not None: cbar.ax.annotate(str(percentile_range[0]), xy=(-0.3, 0.0), xycoords='axes fraction', horizontalalignment='right', verticalalignment='center', rotation=90) cbar.ax.annotate(str(percentile_range[1]), xy=(-0.3, 1.0), xycoords='axes fraction', horizontalalignment='right', verticalalignment='center', rotation=90) cbar.ax.annotate('(percentile)', xy=(-0.3, 0.5), xycoords='axes fraction', horizontalalignment='right', verticalalignment='center', rotation=90) return fig
class SingleShotLogic(GenericLogic): """ This class brings raw data coming from fastcounter measurements (gated or ungated) into trace form processable by the trace_analysis_logic. """ _modclass = 'SingleShotLogic' _modtype = 'logic' # declare connectors savelogic = Connector(interface='SaveLogic') fitlogic = Connector(interface='FitLogic') fastcounter = Connector(interface='FastCounterInterface') pulseextractionlogic = Connector(interface='PulseExtractionLogic') pulsedmeasurementlogic = Connector(interface='PulsedMeasurementLogic') traceanalysislogic1 = Connector(interface='TraceAnalysisLogic') pulsegenerator = Connector(interface='PulserInterface') scannerlogic = Connector(interface='ScannerLogic') optimizerlogic = Connector(interface='OptimizerLogic') pulsedmasterlogic = Connector(interface='PulsedMasterLogic') odmrlogic = Connector(interface='ODMRLogic') # add possible signals here sigHistogramUpdated = QtCore.Signal() sigMeasurementFinished = QtCore.Signal() sigTraceUpdated = QtCore.Signal() def __init__(self, config, **kwargs): """ Create CounterLogic object with connectors. @param dict config: module configuration @param dict kwargs: optional parameters """ super().__init__(config=config, **kwargs) self.log.debug('The following configuration was found.') # checking for the right configuration for key in config.keys(): self.log.debug('{0}: {1}'.format(key, config[key])) # initalize internal variables here self.hist_data = None self._hist_num_bins = None self.data_dict = None def on_activate(self): """ Initialisation performed during activation of the module. """ self._fast_counter_device = self.get_connector('fastcounter') self._pulse_generator_device = self.get_connector('pulsegenerator') self._save_logic = self.get_connector('savelogic') self._fit_logic = self.get_connector('fitlogic') self._traceanalysis_logic = self.get_connector('traceanalysislogic1') self._pe_logic = self.get_connector('pulseextractionlogic') self._pm_logic = self.get_connector('pulsedmeasurementlogic') self._odmr_logic = self.get_connector('odmrlogic') self._pulsed_master_logic = self.get_connector('pulsedmasterlogic') self._confocal_logic = self.get_connector('scannerlogic') self._optimizer_logic = self.get_connector('optimizerlogic') self.hist_data = None self.trace = None self.sigMeasurementFinished.connect(self.ssr_measurement_analysis) def on_deactivate(self): """ Deinitialisation performed during deactivation of the module. @param object e: Event class object from Fysom. A more detailed explanation can be found in method activation. """ return # ========================================================================= # Raw Data Analysis # ========================================================================= def get_data(self, fastcounter='fastcomtec'): """ get the singleshot data from the fastcounter along with its shape @param: optional string fastcounter: Determines how the data is extracted from the fastcounter @return: dictionary containing shape of the data as well as the raw data coming from fastcounter and possible additional data to calculate dt ( the time between two singleshot measurements ) later. """ return_dict = OrderedDict() if not self._fast_counter_device.is_gated(): if fastcounter == 'fastcomtec': settings = self._fast_counter_device.get_settings() # check if settings object is coming from a remote connection settings = netobtain(settings) n_rows = settings.cycles # looks like this is in ns, but I'm not completely sure n_columns = settings.range reps_per_row = settings.swpreset raw_data = netobtain( self._fast_counter_device.get_data_trace(sweep_reset=True)) return_dict['n_rows'] = n_rows return_dict['n_columns'] = n_columns return_dict['reps_per_row'] = reps_per_row return_dict['raw_data'] = raw_data # needed to internally calculate the measurement time, unless the columns are # always in ns ? return_dict[ 'bin_width'] = self._fast_counter_device.get_binwidth() else: self.log.warning( 'other ungated counters are not implemented at the moment') else: self.log.warning('using gated counter not implemented yet') self.data_dict = return_dict return 0 def find_laser(self, smoothing=10.0, n_laserpulses=2): """ returns the start and stop indices of laserpulses @param smoothing: smoothing data to improve flank detection @param n_laserpulses: the number of laserpulses expected in the data @return: list containing tupels of start and stop values of individual laser pulses """ data = self.data_dict['raw_data'] n_rows = self.data_dict['n_rows'] # we want to add up the pulses along the cycles axis shape = data.shape if shape[0] == n_rows: axis = 0 elif shape[1] == n_rows: axis = 1 else: self.log.debug( 'something went wrong in identifying the correct axis of data in find_laser ' 'of singleshot_logic') summed_pulses = np.sum(data, axis) # TODO make the type of pulsed extraction adjustable self._pe_logic.number_of_lasers = n_laserpulses self._pe_logic.conv_std_dev = smoothing return_dict = self._pe_logic.ungated_extraction_methods['conv_deriv']( summed_pulses) rising_ind = return_dict['laser_indices_rising'] falling_ind = return_dict['laser_indices_falling'] start_stop_tupel_list = [] for jj, rising in enumerate(rising_ind): start_stop_tupel_list.append((rising, falling_ind[jj])) return start_stop_tupel_list def sum_laserpulse(self, smoothing=10.0, n_laserpulses=2): """ First find the laserpulses, then go on to add up the individual laserpulses. After that the data depending on the sequence may need normalization and higher binwidths may be calculated ( add to gather 2 up to floor( n_rows // 2) values ). @param float smoothing: If pulse detection doesn't work, change this value @return numpy array: dimensionality is n_rows x n_laserpulses """ sum_single_pulses = [] start_stop_tupel_list = self.find_laser(smoothing=smoothing, n_laserpulses=n_laserpulses) if self.data_dict: data = self.data_dict['raw_data'] for row in data: laser_pulses = [ np.sum(row[jj[0]:jj[1]]) for jj in start_stop_tupel_list ] sum_single_pulses.append(laser_pulses) else: self.log.error( 'Pull data from fastcounting device using get_data function before trying to sum_laserpulse.' ) return np.array(sum_single_pulses) def get_normalized_signal(self, smoothing=10.0): """ given the raw data this function will calculate the normalized signal. It assumes that the pulse sequence used has 2 laserpulses ( for normalization purposes ) @param float smoothing: If pulse detection doesn't work, change this value @return numpy array: 1D array containing the normalized signal """ sum_single_pulses = self.sum_laserpulse() if sum_single_pulses.shape[1] == 2: normalized_signal = np.array([(ii[0] - ii[1]) / (ii[0] + ii[1]) for ii in sum_single_pulses]) else: self.log.warning( 'could not perform normalisation. Wrong number of laserpulses.' ) return normalized_signal def calc_all_binnings(self, num_bins=100): """ calculate reasonable binnings of the signal @param int num_bins: minimal number the binnings can have @return list bin_list: Contains the arrays with the binned data. Data is structured as follows: bin_list[0] is the initial binning given by the measurement and then going up. """ if self.data_dict: data = self.data_dict else: self.log.error( 'Pull data from fastcounting device using get_data function ' 'before trying to calc_all_binnings.') NN = data['n_rows'] # this is just a guess value, at some point it doesn't make # sense anymore to further decrease the number of bins max_bin = NN // num_bins count_var = 1 bin_list = [] temp_list = [] signal = self.sum_laserpulse() while count_var <= max_bin: # check if the the first run through loop was done if temp_list: app_arr = np.array(temp_list) bin_list.append(app_arr) temp_list = [] jj = 0 while jj < NN: sum_ind = np.linspace(jj, jj + count_var - 1, count_var, dtype=np.int) # make sure we don't try to adress not reserved memory jj += count_var if sum_ind[-1] < NN: # normalize temp_list.append( np.array([ np.sum(signal[sum_ind, 0]), np.sum(signal[sum_ind, 1]) ])) else: jj = NN count_var += 1 return np.array(bin_list) def calc_all_binnings_normalized(self, num_bins=100): """ Calculate all normalized binnings from singleshot data @param integer num_bins: Tells how many data points should still remain ( in this sense restricts the maximum number of data points added up together ) @return list normalized_bin_list: The entries are numpy arrays that represent different binnings ( 1 to n values) """ bin_list = self.calc_all_binnings(num_bins=num_bins) normalized_bin_list = [] for binning in bin_list: normalized_binning = (binning[:, 0] - binning[:, 1]) / ( binning[:, 0] + binning[:, 1]) normalized_bin_list.append(normalized_binning) return np.array(normalized_bin_list) def get_timetrace(self): """ This function will help to find the optimal binning in the fastcomtec data, for now under development, as we decided for now to manually pick the right binning. Therefore the function visualize_bin_list is there to help with that task. It will make a plot of all binnings and show which shows the best features. @return: """ # what needs to be done here now is the basic evaluation steps like fit, threshold # readout fidelity bin_list = self.calc_all_binnings(self, num_bins=100) param_dict_list = [] fidelity_list = [] for ii in bin_list: # what is a good estimate for the number of bins ? hist_y_val, hist_x_val = np.histogram(ii, bins=50) hist_data = np.array([hist_x_val, hist_y_val]) threshold_fit, fidelity, \ param_dict = self._traceanalysis_logic.calculate_threshold(hist_data=hist_data, distr='gaussian_normalized') param_dict_list.append(param_dict) fidelity_list.append(fidelity) # now get the maximum fidelity, not really working up till now. The fidelity alone is not a good indicator # because the fit can still be bad. Need somehow a mixed measure of this. Will look for some heuristic. ind = np.argmax(np.array(fidelity_list)) timetrace = bin_list[ind] return timetrace # ========================================================================= # Single Shot measurements # ========================================================================= # TODO make more general for other devices def do_singleshot(self, mw_dict=None, refocus=True, laser_wfm='LaserOn', singleshot_wfm='SSR_normalise_2MW', normalized=True): """ For additional microwave usage this assumes an external signal generator. Could be also done with an additional pulser channel though. """ use_mw = False if mw_dict: use_mw = True if mw_dict['freq']: mw_freq = mw_dict['freq'] self._odmr_logic.set_frequency(frequency=mw_freq) if mw_dict['power']: mw_power = mw_dict['power'] self._odmr_logic.set_power(mw_power) # set mw power # self._odmr_logic.set_power(mw_power) # turn on laser to refocus self._pulse_generator_device.load_asset(laser_wfm) self._pulse_generator_device.pulser_on() # TODO make this with queue connections if refocus: self._do_optimize_pos() # load sequence for SSR measurement self._pulse_generator_device.load_asset(singleshot_wfm) self._pulse_generator_device.pulser_on() # set mw frequency and turn it on if use_mw: self._odmr_logic.MW_on() self._fast_counter_device.start_measure() time.sleep(10) # try to do this differently tmp_var1 = self._fast_counter_device.get_status() while (tmp_var1 - 1): time.sleep(5) tmp_var1 = self._fast_counter_device.get_status() # pull data. This will also update the variable self.data_dict self.get_data() if normalized: bin_list = self.calc_all_binnings() else: bin_list = self.calc_all_binnings_normalized() return bin_list # I would very much like to have this function here, both in respect to the magnet logic, which will # need such a function for the nuclear alignment as well as for other measurements such as rf odmr and so on # therefore I'm going to include it here. # TODO include focusing on a single peak here # TODO refocus replaced through refocus frequency def do_pulsed_odmr(self, measurement_time, controlled_vals_start, controlled_vals_incr, num_of_lasers, sequence_length_s, refocus=True, pulsedODMR_wfm='PulsedODMR', save_tag=''): """ A function to do pulsed odmr. Important as exact transition frequencies are important. @param measurement_time: @param controlled_vals_start: @param controlled_vals_incr: @param num_of_lasers: @param sequence_length_s: @param refocus: @param pulsedODMR_wfm: @param save_tag: @return: """ laser_ignore_list = [] # TODO maybe this data is also differently available or units can be set within the logic alternating = False controlled_vals = np.arange( controlled_vals_start, controlled_vals_start + (controlled_vals_incr * num_of_lasers) - (controlled_vals_incr / 2), controlled_vals_incr) self._pulsed_master_logic.measurement_sequence_settings_changed( controlled_vals, num_of_lasers, sequence_length_s, laser_ignore_list, alternating) self._pm_logic._initialize_plots() self._pulse_generator_device.load_asset(pulsedODMR_wfm) self._pulsed_master_logic.start_measurement() if refocus: self._do_optimize_pos() time.sleep(measurement_time) self._pulsed_master_logic.stop_measurement() freqs = self._pm_logic.signal_plot_x signal = self._pm_logic.signal_plot_y # now everything is saved, lets do the fitting results = self._fit_logic.make_N14_fit(freqs, signal) freq_peaks = np.array([ results.params['l0_center'].value, results.params['l1_center'].value, results.params['l2_center'].value ]) if save_tag: controlled_val_unit = 'Hz' self._pulsed_master_logic.save_measurement_data( controlled_val_unit, save_tag) return freq_peaks def save_singleshot(self, tag=None, normalized=True, visualize=True): """ When called this will save the attribute data_dict of class savelogic to file. The raw data will be postprocessed to bin lists as well as normalized bin lists ( containing all the possible binnings of the data. Additionally the meta_data will be saved. @return: """ filepath = self._save_logic.get_path_for_module( module_name='SingleShot') timestamp = datetime.datetime.now() timestamp_str = timestamp.strftime('%Y%m%d-%H%M-%S') if normalized: if tag is not None and len(tag) > 0: filelabel2 = tag + '_' + timestamp_str + '_normalized_bin_list' else: filelabel2 = timestamp_str + '_normalized_bin_list' normalized_bin_list = self.calc_all_binnings_normalized() save_path2 = os.path.join(filepath, filelabel2) np.save(save_path2, normalized_bin_list) if visualize: visualize_path = os.path.join( filepath, timestamp_str + '_visualize_bins') os.mkdir(visualize_path) self.visualize_bin_list(normalized_bin_list, visualize_path) else: if tag is not None and len(tag) > 0: filelabel1 = tag + '_' + timestamp_str + '_bin_list' else: filelabel1 = timestamp_str + '_bin_list' bin_list = self.calc_all_binnings() save_path1 = os.path.join(filepath, filelabel1) np.save(save_path1, bin_list) meta_data_dict = copy.deepcopy(self.data_dict) meta_data_dict.pop('raw_data') meta_path = os.path.join(filepath, timestamp_str + '_meta_data') np.save(meta_path, meta_data_dict) for key in meta_data_dict: meta_data_dict[key] = [meta_data_dict[key]] self._save_logic.save_data(meta_data_dict, filepath=filepath, filelabel='meta_data') return # Helper methods def _do_optimize_pos(self): curr_pos = self._confocal_logic.get_position() self._optimizer_logic.start_refocus(curr_pos, caller_tag='singleshot_logic') # check just the state of the optimizer while self._optimizer_logic.getState() != 'idle': time.sleep(0.5) # use the position to move the scanner self._confocal_logic.set_position('magnet_logic', self._optimizer_logic.optim_pos_x, self._optimizer_logic.optim_pos_y, self._optimizer_logic.optim_pos_z) def visualize_bin_list(self, bin_list, path): """ Will create a histogram of all bin_list entries and save it to the specified path """ # TODO use savelogic here for jj, bin_entry in enumerate(bin_list): hist_x, hist_y = self._traceanalysis_logic.calculate_histogram( bin_entry, num_bins=50) pb.plot(hist_x[0:len(hist_y)], hist_y) fname = 'bin_' + str(jj) + '.png' savepath = os.path.join(path, fname) pb.savefig(savepath) pb.close() # ========================================================================= # Connecting to GUI # ========================================================================= # absolutely not working at the moment. def ssr_measurement_analysis(self, record_length): """ Gets executed when a single shot measurment has finished. This function will update GUI elements @param record_length: @return: """ normalized_bin_list = self.calc_all_binnings_normalized(self, num_bins=100) # for now take only the initial binning data = normalized_bin_list[0] measurement = self.data_dict # also only the initial binning, needs to be adjusted then time_axis = np.linspace( record_length * measurement['reps_per_row'], record_length * (measurement['reps_per_row'] + 1), measurement['n_rows']) # update the histogram in the gui self.do_calculate_histogram(data) # update the trace in the gui self._do_calculate_trace(time_axis, data) def do_calculate_histogram(self, data): """ Passes all the needed parameters to the appropriated methods. @return: """ self.hist_data = self._traceanalysis_logic.calculate_histogram( data, self._traceanalysis_logic._hist_num_bins) self.sigHistogramUpdated.emit() def do_calculate_trace(self, time_axis, data): self.trace = np.array([time_axis, data]) self.sigTraceUpdated.emit()
class PolarizationDependenceSim(Base, SlowCounterInterface, MotorInterface): """ This class wraps the slow-counter dummy and adds polarisation angle dependence in order to simulate dipole polarisation measurements. """ _modclass = 'polarizationdepsim' _modtype = 'hardware' # Connectors counter1 = Connector(interface='SlowCounterInterface') _move_signal = QtCore.Signal() def on_activate(self): """ Activation of the class """ # name connected modules self._counter_hw = self.get_connector('counter1') # Required class variables to pretend to be the counter hardware self._photon_source2 = None # initialize class variables self.hwp_angle = 0 self.dipole_angle = random.uniform(0, 360) self.velocity = 10 self.clock_frequency = 50 self.forwards_motion = True self.moving = False # Signals self._move_signal.connect(self._move_step, QtCore.Qt.QueuedConnection) def on_deactivate(self): self._counter_hw.close_counter() self._counter_hw.close_clock() # Wrapping the slow counter methods def set_up_clock(self, clock_frequency=None, clock_channel=None): """ Direct pass-through to the counter hardware module """ self.clock_frequency = clock_frequency return self._counter_hw.set_up_clock(clock_frequency=clock_frequency, clock_channel=clock_channel) def get_constraints(self): """ Pass through counter constraints. """ return self._counter_hw.get_constraints() def set_up_counter(self, counter_channel=None, photon_source=None, counter_channel2=None, photon_source2=None, clock_channel=None): """ Direct pass-through to the counter hardware module """ return self._counter_hw.set_up_counter( counter_channel=counter_channel, photon_source=photon_source, counter_channel2=counter_channel2, photon_source2=photon_source2, clock_channel=clock_channel) def get_counter(self, samples=None): """ Direct pass-through to the counter hardware module """ raw_count = self._counter_hw.get_counter(samples=samples) # modulate the counts with a polarisation dependence angle = np.radians(self.hwp_angle - self.dipole_angle) count = raw_count * np.sin(angle) * np.sin(angle) + random.uniform( -0.1, 0.1) return count def close_counter(self): """ Direct pass-through to the counter hardware module """ return self._counter_hw.close_counter() def close_clock(self, power=0): """ Direct pass-through to the counter hardware module """ return self._counter_hw.close_clock(power=power) # Satisfy the motor interface def move_rel(self, axis=None, distance=None): """ Move the polarisation angle by relative degrees """ if distance is None: #TODO warning pass self.destination = self.hwp_angle + distance # Keep track of the motion direction so we will know when we are past the destination if distance > 0: self.forwards_motion = True else: self.forwards_motion = False self.moving = True self._move_signal.emit() return 0 def move_abs(self, axis=None, position=None): """ Move the polarisation angle to absolute degrees """ if position is None: #TODO warning pass self.destination = position # Keep track of the motion direction so we will know when we are past the destination if position > self.hwp_angle: self.forwards_motion = True else: self.forwards_motion = False self.moving = True self._move_signal.emit() return 0 def _move_step(self): """Make movement steps in a threaded loop """ # if abort is requested, then stop moving if not self.moving: return # If we have reached the destination then stop the movement if self.forwards_motion: if self.hwp_angle > self.destination: return else: if self.hwp_angle < self.destination: return # Otherwise make a movement step step_size = self.velocity / self.clock_frequency if self.forwards_motion: self.hwp_angle += step_size else: self.hwp_angle -= step_size time.sleep(1. / self.clock_frequency) self._move_signal.emit() def abort(self): self.moving = False return 0 def get_pos(self, axis=None): return self.hwp_angle def get_status(self): return 0 def calibrate(self, axis=None): self.hwp_angle = 0 return 0 def get_velocity(self, axis=None): return self.velocity def set_velocity(self, axis=None, velocity=None): self.velocity = velocity return 0
class TraceAnalysisLogic(GenericLogic): """ Perform a gated counting measurement with the hardware. """ _modclass = 'TraceAnalysisLogic' _modtype = 'logic' # declare connectors counterlogic1 = Connector(interface='CounterLogic') savelogic = Connector(interface='SaveLogic') fitlogic = Connector(interface='FitLogic') sigHistogramUpdated = QtCore.Signal() def __init__(self, config, **kwargs): """ Create CounterLogic object with connectors. @param dict config: module configuration @param dict kwargs: optional parameters """ super().__init__(config=config, **kwargs) self.log.debug('The following configuration was found.') # checking for the right configuration for key in config.keys(): self.log.debug('{0}: {1}'.format(key, config[key])) self.hist_data = None self._hist_num_bins = None def on_activate(self): """ Initialisation performed during activation of the module. """ self._counter_logic = self.get_connector('counterlogic1') self._save_logic = self.get_connector('savelogic') self._fit_logic = self.get_connector('fitlogic') self._counter_logic.sigGatedCounterFinished.connect( self.do_calculate_histogram) self.current_fit_function = 'No Fit' def on_deactivate(self): """ Deinitialisation performed during deactivation of the module. """ return def set_num_bins_histogram(self, num_bins, update=True): """ Set the number of bins @param int num_bins: number of bins for the histogram @param bool update: if the change of bins should evoke a recalculation of the histogram. """ self._hist_num_bins = num_bins if update: self.do_calculate_histogram() def do_calculate_histogram(self, mode='normal'): """ Passes all the needed parameters to the appropriated methods. @return: """ if mode == 'normal': self.hist_data = self.calculate_histogram( self._counter_logic.countdata[0], self._hist_num_bins) if mode == 'fastcomtec': self.sigHistogramUpdated.emit() def calculate_histogram(self, trace, num_bins=None, custom_bin_arr=None): """ Calculate the histogram of a given trace. @param np.array trace: a 1D trace @param int num_bins: number of bins between the minimal and maximal value of the trace. That must be an integer greater than or equal to 1. @param np.array custom_bin_arr: optional, 1D array. If a specific, non-uniform binning array is desired then it can be passed to the numpy routine. Then the parameter num_bins is ignored. Otherwise a uniform binning is applied by default. @return: np.array: a 2D array, where first entry are the x_values and second entry are the count values. The length of the array is normally determined by the num_bins parameter. Usually the bins for the histogram are taken to be equally spaced, ranging from the minimal to the maximal value of the input trace array. """ if custom_bin_arr is not None: hist_y_val, hist_x_val = np.histogram(trace, custom_bin_arr, density=False) else: # analyze the trace, and check whether all values are the same difference = trace.max() - trace.min() # if all values are the same, run at least the method with an zero # array. That will ensure at least an output: if np.isclose(0, difference) and num_bins is None: # numpy can handle an array of zeros num_bins = 50 hist_y_val, hist_x_val = np.histogram(trace, num_bins) # if no number of bins are passed, then take the integer difference # between the counts, that will prevent strange histogram artifacts: elif not np.isclose(0, difference) and num_bins is None: hist_y_val, hist_x_val = np.histogram(trace, int(difference)) # a histogram with self defined number of bins else: hist_y_val, hist_x_val = np.histogram(trace, num_bins) return hist_x_val, hist_y_val def analyze_flip_prob(self, trace, num_bins=None, threshold=None): """General method, which analysis how often a value was changed from one data point to another in relation to a certain threshold. @param np.array trace: 1D trace of data @param int num_bins: optional, if a specific size for the histogram is desired, which is used to calculate the threshold. @param float threshold: optional, if a specific threshold is going to be used, otherwise the threshold is calculated from the data. @return tuple(flip_prop, param): float flip_prop: the actual flip probability int num_of_flips: the total number of flips float fidelity: the fidelity float threshold: the calculated or passed threshold float lifetime_dark: the lifetime in the dark state in s float lifetime_bright: lifetime in the bright state in s """ hist_data = self.calculate_histogram(trace=trace, num_bins=num_bins) threshold_fit, fidelity, fit_param = self.calculate_threshold( hist_data) bin_trace = self.calculate_binary_trace(trace, threshold_fit) # here the index_arr contain all indices where the state is above # threshold, indicating the bright state. index_arr, filtered_arr = self.extract_filtered_values(trace, threshold_fit, below=False) # by shifting the index_arr one value further, one will investigate # basically the next state, where a change has happened. next_index_arr = index_arr + 1 # Just for safety neglect the last value in the index_arr so that one # will not go beyond the array. next_filtered_bin_arr = bin_trace[next_index_arr[:-1]] # calculate how many darkstates are present in the array, remember # filtered_arr contains all the bright states. num_dark_state = len(trace) - len(filtered_arr) num_bright_state = len(filtered_arr) # extract the number of state, which has been flipped to dark state # (True) started in the bright state (=False) num_flip_to_dark = len(np.where(next_filtered_bin_arr == True)[0]) # flip probability: # In the array filtered_bin_arr all states are in bright state meaning # that if you would perform for # filtered_bin_arr = bin_trace[index_arr] # the mean value with filtered_bin_arr.mean() then you should get 0.0 # since every entry in that array is False. By looking at the next index # it might be that some entries turn to True, i.e. a flip from bright to # dark occurred. Then you get a different mean value, which would # indicate how many states are flipped from bright (False) to dark (True). # If all the next states would be dark (True), then you would perform a # perfect flip into the dark state, meaning a flip probability of 1. flip_prob = next_filtered_bin_arr.mean() # put all the calculated parameters in a proper dict: param = OrderedDict() param['num_dark_state'] = num_dark_state # Number of Dark States param['num_bright_state'] = num_bright_state # Number of Bright States param[ 'num_flip_to_dark'] = num_flip_to_dark # Number of flips from bright to dark param['fidelity'] = fidelity # Fidelity of Double Poissonian Fit param['threshold'] = threshold_fit # Threshold # add the fit parameter to the output parameter: param.update(fit_param) return flip_prob, param def analyze_flip_prob_postselect(self): """ Post select the data trace so that the flip probability is only calculated from a jump from below a threshold value to an value above threshold. @return: """ pass def get_fit_functions(self): """ Return all fit functions, which are currently implemented for that module. @return list: with string entries denoting the name of the fit. """ return [ 'No Fit', 'Gaussian', 'Double Gaussian', 'Poisson', 'Double Poisson' ] def do_fit(self, fit_function=None): """ Makes the a fit of the current fit function. @param str fit_function: name of the chosen fit function. @return tuple(x_val, y_val, fit_results): x_val: a 1D numpy array containing the x values y_val: a 1D numpy array containing the y values fit_results: a string containing the information of the fit results. You can obtain with get_fit_methods all implemented fit methods. """ if self.hist_data is None: hist_fit_x = [] hist_fit_y = [] param_dict = OrderedDict() fit_result = None return hist_fit_x, hist_fit_y, param_dict, fit_result else: # self.log.debug((self.calculate_threshold(self.hist_data))) # shift x axis to middle of bin axis = self.hist_data[0][:-1] + (self.hist_data[0][1] - self.hist_data[0][0]) / 2. data = self.hist_data[1] if fit_function == 'No Fit': hist_fit_x, hist_fit_y, fit_param_dict, fit_result = self.do_no_fit( ) return hist_fit_x, hist_fit_y, fit_param_dict, fit_result elif fit_function == 'Gaussian': hist_fit_x, hist_fit_y, fit_param_dict, fit_result = self.do_gaussian_fit( axis, data) return hist_fit_x, hist_fit_y, fit_param_dict, fit_result elif fit_function == 'Double Gaussian': hist_fit_x, hist_fit_y, fit_param_dict, fit_result = self.do_doublegaussian_fit( axis, data) return hist_fit_x, hist_fit_y, fit_param_dict, fit_result elif fit_function == 'Poisson': hist_fit_x, hist_fit_y, fit_param_dict, fit_result = self.do_possonian_fit( axis, data) return hist_fit_x, hist_fit_y, fit_param_dict, fit_result elif fit_function == 'Double Poisson': hist_fit_x, hist_fit_y, fit_param_dict, fit_result = self.do_doublepossonian_fit( axis, data) return hist_fit_x, hist_fit_y, fit_param_dict, fit_result def do_no_fit(self): """ Perform no fit, basically return an empty array. @return tuple(x_val, y_val, fit_results): x_val: a 1D numpy array containing the x values y_val: a 1D numpy array containing the y values fit_results: a string containing the information of the fit results. """ hist_fit_x = [] hist_fit_y = [] param_dict = {} fit_result = None return hist_fit_x, hist_fit_y, param_dict, fit_result def analyze_lifetime(self, trace, dt, method='postselect', distr='gaussian_normalized', state='|-1>', num_bins=50): """ Perform an lifetime analysis of a 1D time trace. The analysis is based on the method provided ( for now only post select is implemented ). @param numpy array trace: 1 D array @param string method: The method used for the lifetime analysis @param string distr: distribution used for analysis @param string state: State that the mw was applied to @param int num_bins: number of bins used in the histogram to determine the threshold before digitalisation of data @return: dictionary containing the lifetimes of the different states |0>, |1>, |-1> in the case of the HMM method For the postselect method only lifetime for bright and darkstate is returned, keys are 'bright_state' and 'dark_state' """ lifetime_dict = {} if method == 'postselect': if distr == 'gaussian_normalized': hist_y_val, hist_x_val = np.histogram(trace, num_bins) hist_data = np.array([hist_x_val, hist_y_val]) threshold_fit, fidelity, param_dict = self.calculate_threshold( hist_data=hist_data, distr='gaussian_normalized') threshold = threshold_fit # helper functions to get and analyze the timetrace def analog_digitial_converter(cut_off, data): digital_trace = [] for data_point in data: if data_point >= cut_off: digital_trace.append(1) else: digital_trace.append(0) return digital_trace def time_in_high_low(digital_trace, dt): """ What I need this function to do is to get all consecutive {1, ... , n} 1s or 0s and add them up and put into a list to later make a histogram from them. """ occurances = [] index = 0 index2 = 0 while (index < len(digital_trace)): occurances.append(0) # start following the consecutive 1s while (digital_trace[index] == 1): occurances[index2] += 1 if index == (len(digital_trace) - 1): occurances = np.array(occurances) return occurances * dt else: index += 1 if digital_trace[index - 1] == 1: index2 += 1 occurances.append(0) # start following the consecutive 0s while (digital_trace[index] == 0): occurances[index2] -= 1 if index == (len(digital_trace) - 1): occurances = np.array(occurances) return occurances * dt else: index += 1 index2 += 1 digital_trace = analog_digitial_converter(threshold, trace) time_array = time_in_high_low(digital_trace, dt) # now we need to make a histogram as well as a fit # what would be a good estimate for the number of bins # longest = np.max(np.array(occurances)) # number of steps in between, rather not use that for now # est_bins = np.int(longest/dt) time_array_high = np.array( [ii for ii in filter(lambda x: x > 0, time_array)]) time_array_low = np.array( [ii for ii in filter(lambda x: x < 0, time_array)]) # get lifetime of bright state time_hist_high = np.histogram(time_array_high, bins=num_bins) vals = [ i for i in filter(lambda x: x[1] > 0, enumerate(time_hist_high[0][0:num_bins])) ] indices = np.array([val[0] for val in vals]) indices = np.array([np.int(indice) for indice in indices]) self.log.debug('threshold {0}'.format(threshold)) self.log.debug('time_array:{0}'.format(time_array)) self.log.debug('time_array_high:{0}'.format(time_array_high)) self.log.debug('time_hist_high:{0}'.format(time_hist_high)) self.log.debug('indices: {0}'.format(indices)) self.debug_lifetime_x = time_hist_high[1][indices] self.debug_lifetime_y = time_hist_high[0][indices] para = dict() para['offset'] = {"value": 0.0, "vary": False} result = self._fit_logic.make_decayexponential_fit( time_hist_high[1][indices], time_hist_high[0][indices], self._fit_logic.estimate_decayexponential, add_params=para) bright_liftime = result.params['lifetime'] # for debug purposes give also the results back of the fits for now lifetime_dict['result_bright'] = result # also give back the data used for the fit lifetime_dict['bright_raw'] = np.array( [time_hist_high[1][indices], time_hist_high[0][indices]]) # get lifetime of dark state time_hist_low = np.histogram(time_array_low, bins=num_bins) vals = [ i for i in filter(lambda x: x[1] > 0, enumerate(time_hist_low[0][0:num_bins])) ] indices = np.array([val[0] for val in vals]) indices = np.array([np.int(indice) for indice in indices]) values = np.array([val[1] for val in vals]) # positive axis mirror_axis = -time_hist_low[1][indices] result = self._fit_logic.make_decayexponential_fit( mirror_axis, values, self._fit_logic.estimate_decayexponential, add_params=para) dark_liftime = result.params['lifetime'] lifetime_dict['result_dark'] = result lifetime_dict['bright_state'] = bright_liftime.value lifetime_dict['dark_state'] = dark_liftime.value # also give back the data used for the fit lifetime_dict['dark_raw'] = np.array([mirror_axis, values]) return lifetime_dict def do_gaussian_fit(self, axis, data): """ Perform a gaussian fit. @param axis: @param data: @return: """ model, params = self._fit_logic.make_gaussian_model() if len(axis) < len(params): self.log.warning('Fit could not be performed because number of ' 'parameters is smaller than data points.') return self.do_no_fit() else: parameters_to_substitute = dict() update_dict = dict() #TODO: move this to "gated counter" estimator in fitlogic # make the filter an extra function shared and usable for other # functions gauss = gaussian(10, 10) data_smooth = filters.convolve1d(data, gauss / gauss.sum(), mode='mirror') # integral of data corresponds to sqrt(2) * Amplitude * Sigma function = InterpolatedUnivariateSpline(axis, data_smooth, k=1) Integral = function.integral(axis[0], axis[-1]) amp = data_smooth.max() sigma = Integral / amp / np.sqrt(2 * np.pi) amplitude = amp * sigma * np.sqrt(2 * np.pi) update_dict['offset'] = { 'min': 0, 'max': data.max(), 'value': 0, 'vary': False } update_dict['center'] = { 'min': axis.min(), 'max': axis.max(), 'value': axis[np.argmax(data)] } update_dict['sigma'] = { 'min': -np.inf, 'max': np.inf, 'value': sigma } update_dict['amplitude'] = { 'min': 0, 'max': np.inf, 'value': amplitude } result = self._fit_logic.make_gaussian_fit( x_axis=axis, data=data, estimator=self._fit_logic.estimate_gaussian_peak, units=None, # TODO add_params=update_dict) # 1000 points in x axis for smooth fit data hist_fit_x = np.linspace(axis[0], axis[-1], 1000) hist_fit_y = model.eval(x=hist_fit_x, params=result.params) param_dict = OrderedDict() # create the proper param_dict with the values: param_dict['sigma_0'] = { 'value': result.params['sigma'].value, 'error': result.params['sigma'].stderr, 'unit': 'Occurrences' } param_dict['FWHM'] = { 'value': result.params['fwhm'].value, 'error': result.params['fwhm'].stderr, 'unit': 'Counts/s' } param_dict['Center'] = { 'value': result.params['center'].value, 'error': result.params['center'].stderr, 'unit': 'Counts/s' } param_dict['Amplitude'] = { 'value': result.params['amplitude'].value, 'error': result.params['amplitude'].stderr, 'unit': 'Occurrences' } param_dict['chi_sqr'] = {'value': result.chisqr, 'unit': ''} return hist_fit_x, hist_fit_y, param_dict, result def do_doublegaussian_fit(self, axis, data): model, params = self._fit_logic.make_gaussiandouble_model() if len(axis) < len(params): self.log.warning('Fit could not be performed because number of ' 'parameters is smaller than data points') return self.do_no_fit() else: result = self._fit_logic.make_gaussiandouble_fit( axis, data, self._fit_logic.estimate_gaussiandouble_peak) # 1000 points in x axis for smooth fit data hist_fit_x = np.linspace(axis[0], axis[-1], 1000) hist_fit_y = model.eval(x=hist_fit_x, params=result.params) # this dict will be passed to the formatting method param_dict = OrderedDict() # create the proper param_dict with the values: param_dict['sigma_0'] = { 'value': result.params['g0_sigma'].value, 'error': result.params['g0_sigma'].stderr, 'unit': 'Counts/s' } param_dict['FWHM_0'] = { 'value': result.params['g0_fwhm'].value, 'error': result.params['g0_fwhm'].stderr, 'unit': 'Counts/s' } param_dict['Center_0'] = { 'value': result.params['g0_center'].value, 'error': result.params['g0_center'].stderr, 'unit': 'Counts/s' } param_dict['Amplitude_0'] = { 'value': result.params['g0_amplitude'].value, 'error': result.params['g0_amplitude'].stderr, 'unit': 'Occurrences' } param_dict['sigma_1'] = { 'value': result.params['g1_sigma'].value, 'error': result.params['g1_sigma'].stderr, 'unit': 'Counts/s' } param_dict['FWHM_1'] = { 'value': result.params['g1_fwhm'].value, 'error': result.params['g1_fwhm'].stderr, 'unit': 'Counts/s' } param_dict['Center_1'] = { 'value': result.params['g1_center'].value, 'error': result.params['g1_center'].stderr, 'unit': 'Counts/s' } param_dict['Amplitude_1'] = { 'value': result.params['g1_amplitude'].value, 'error': result.params['g1_amplitude'].stderr, 'unit': 'Occurrences' } param_dict['chi_sqr'] = {'value': result.chisqr, 'unit': ''} return hist_fit_x, hist_fit_y, param_dict, result def do_doublepossonian_fit(self, axis, data): model, params = self._fit_logic.make_multiplepoissonian_model( no_of_functions=2) if len(axis) < len(params): self.log.warning('Fit could not be performed because number of ' 'parameters is smaller than data points') return self.do_no_fit() else: result = self._fit_logic.make_doublepoissonian_fit(x_axis=axis, data=data, add_params=None) # 1000 points in x axis for smooth fit data hist_fit_x = np.linspace(axis[0], axis[-1], 1000) hist_fit_y = model.eval(x=hist_fit_x, params=result.params) # this dict will be passed to the formatting method param_dict = OrderedDict() # create the proper param_dict with the values: param_dict['lambda_0'] = { 'value': result.params['p0_mu'].value, 'error': result.params['p0_mu'].stderr, 'unit': 'Counts/s' } param_dict['Amplitude_0'] = { 'value': result.params['p0_amplitude'].value, 'error': result.params['p0_amplitude'].stderr, 'unit': 'Occurrences' } param_dict['lambda_1'] = { 'value': result.params['p1_mu'].value, 'error': result.params['p1_mu'].stderr, 'unit': 'Counts/s' } param_dict['Amplitude_1'] = { 'value': result.params['p1_amplitude'].value, 'error': result.params['p1_amplitude'].stderr, 'unit': 'Occurrences' } param_dict['chi_sqr'] = {'value': result.chisqr, 'unit': ''} # removed last return value <<result>> here, because function calculate_threshold only expected # three return values return hist_fit_x, hist_fit_y, param_dict def do_possonian_fit(self, axis, data): model, params = self._fit_logic.make_poissonian_model() if len(axis) < len(params): self.log.error('Fit could not be performed because number of ' 'parameters is smaller than data points') return self.do_no_fit() else: result = self._fit_logic.make_poissonian_fit(x_axis=axis, data=data, add_params=None) # 1000 points in x axis for smooth fit data hist_fit_x = np.linspace(axis[0], axis[-1], 1000) hist_fit_y = model.eval(x=hist_fit_x, params=result.params) # this dict will be passed to the formatting method param_dict = OrderedDict() # create the proper param_dict with the values: param_dict['lambda'] = { 'value': result.params['mu'].value, 'error': result.params['mu'].stderr, 'unit': 'Counts/s' } param_dict['chi_sqr'] = {'value': result.chisqr, 'unit': ''} return hist_fit_x, hist_fit_y, param_dict, result def get_poissonian(self, x_val, mu, amplitude): """ Calculate, bases on the passed values a poisson distribution. @param float mu: expected value of poisson distribution @param float amplitude: Amplitude to which is multiplied on distribution @param int,float or np.array x_val: x values for poisson distribution, also works for numbers (int or float) @return np.array: a 1D array with the calculated poisson distribution, corresponding to given parameters/ x values Calculate a Poisson distribution according to: P(k) = mu^k * exp(-mu) / k! """ model, params = self._fit_logic.make_poissonian_model() return model.eval(x=np.array(x_val), poissonian_mu=mu, poissonian_amplitude=amplitude) def guess_threshold(self, hist_val=None, trace=None, max_ratio_value=0.1): """ Assume a distribution between two values and try to guess the threshold. @param np.array hist_val: 1D array whitch represent the y values of a histogram of a trace. Optional, if None is passed here, the passed trace will be used for calculations. @param np.array trace: optional, 1D array containing the y values of a meausured counter trace. If None is passed to hist_y_val then the threshold will be calculated from the trace. @param float max_ration_value: the ratio how strong the lower y values will be cut off. For max_ratio_value=0.1 all the data which are 10% or less in amptitude compared to the maximal value are neglected. The guess procedure tries to find all values, which are max_ratio_value * maximum value of the histogram of the trace and selects those by indices. Then taking the first an the last might and assuming that the threshold is in the middle, gives a first estimate of the threshold value. FIXME: That guessing procedure can be improved! @return float: a guessed threshold """ if hist_val is None and trace is not None: hist_val = self.calculate_histogram(trace) hist_val = np.array(hist_val) # just to be sure to have a np.array indices_arr = np.where( hist_val[1] > hist_val[1].max() * max_ratio_value)[0] guessed_threshold = hist_val[0][int( (indices_arr[-1] + indices_arr[0]) / 2)] return guessed_threshold def calculate_threshold(self, hist_data=None, distr='poissonian'): """ Calculate the threshold by minimizing its overlap with the poissonian fits. @param np.array hist_data: 2D array whitch represent the x and y values of a histogram of a trace. string distr: tells the function on what distribution it should calculate the threshold ( Added because it might happen that one normalizes data between (-1,1) and then a poissonian distribution won't work anymore. @return tuple(float, float): threshold: the calculated threshold between two overlapping poissonian distributed peaks. fidelity: the measure how good the two peaks are resolved according to the calculated threshold The calculation of the threshold relies on fitting two poissonian distributions to the count histogram and minimize a threshold with respect to the overlap area: """ # in any case calculate the hist data x_axis = hist_data[0][:-1] + (hist_data[0][1] - hist_data[0][0]) / 2. y_data = hist_data[1] if distr == 'poissonian': # perform the fit hist_fit_x, hist_fit_y, param_dict = self.do_doublepossonian_fit( x_axis, y_data) if param_dict.get('lambda_0') is None: self.log.error( 'The double poissonian fit does not work! Take at ' 'least a dummy value, in order not to break the ' 'routine.') amp0 = 1 amp1 = 1 param_dict['Amplitude_0'] = { 'value': amp0, 'unit': 'occurences' } param_dict['Amplitude_1'] = { 'value': amp0, 'unit': 'occurences' } # make them a bit different so that fit works. mu0 = hist_data[0][:].mean() - 0.1 mu1 = hist_data[0][:].mean() + 0.1 param_dict['lambda_0'] = {'value': mu0, 'unit': 'counts'} param_dict['lambda_1'] = {'value': mu1, 'unit': 'counts'} else: mu0 = param_dict['lambda_0']['value'] mu1 = param_dict['lambda_1']['value'] amp0 = param_dict['Amplitude_0']['value'] amp1 = param_dict['Amplitude_1']['value'] if mu0 < mu1: first_dist = self.get_poissonian(x_val=hist_data[0], mu=mu0, amplitude=amp0) sec_dist = self.get_poissonian(x_val=hist_data[0], mu=mu1, amplitude=amp1) else: first_dist = self.get_poissonian(x_val=hist_data[0], mu=mu1, amplitude=amp1) sec_dist = self.get_poissonian(x_val=hist_data[0], mu=mu0, amplitude=amp0) # create a two poissonian array, where the second poissonian # distribution is add as negative values. Now the transition from # positive to negative values will get the threshold: difference_poissonian = first_dist - sec_dist trans_index = 0 for i in range(len(difference_poissonian) - 1): # go through the combined histogram array and the point which # changes the sign. The transition from positive to negative values # will get the threshold: if difference_poissonian[i] < 0 and difference_poissonian[ i + 1] >= 0: trans_index = i break elif difference_poissonian[i] > 0 and difference_poissonian[ i + 1] <= 0: trans_index = i break threshold_fit = hist_data[0][trans_index] # Calculate also the readout fidelity, i.e. sum the area under the # first peak before the threshold of the first and second distribution # and take the ratio of that area. Do the same thing after the threshold # (of course with a reversed choice of the distribution). If the overlap # in both cases is very small, then the fidelity is good, if the overlap # is identical, then fidelity indicates a poor separation of the peaks. if mu0 < mu1: area0_low = self.get_poissonian(hist_data[0][0:trans_index], mu0, 1).sum() area0_high = self.get_poissonian(hist_data[0][trans_index:], mu0, 1).sum() area1_low = self.get_poissonian(hist_data[0][0:trans_index], mu1, 1).sum() area1_high = self.get_poissonian(hist_data[0][trans_index:], mu1, 1).sum() area0_low_amp = self.get_poissonian( hist_data[0][0:trans_index], mu0, amp0).sum() area0_high_amp = self.get_poissonian( hist_data[0][trans_index:], mu0, amp0).sum() area1_low_amp = self.get_poissonian( hist_data[0][0:trans_index], mu1, amp1).sum() area1_high_amp = self.get_poissonian( hist_data[0][trans_index:], mu1, amp1).sum() else: area1_low = self.get_poissonian(hist_data[0][0:trans_index], mu0, 1).sum() area1_high = self.get_poissonian(hist_data[0][trans_index:], mu0, 1).sum() area0_low = self.get_poissonian(hist_data[0][0:trans_index], mu1, 1).sum() area0_high = self.get_poissonian(hist_data[0][trans_index:], mu1, 1).sum() area1_low_amp = self.get_poissonian( hist_data[0][0:trans_index], mu0, amp0).sum() area1_high_amp = self.get_poissonian( hist_data[0][trans_index:], mu0, amp0).sum() area0_low_amp = self.get_poissonian( hist_data[0][0:trans_index], mu1, amp1).sum() area0_high_amp = self.get_poissonian( hist_data[0][trans_index:], mu1, amp1).sum() # Now calculate how big is the overlap relative to the sum of the other # part of the area, that will give the normalized fidelity: fidelity = 1 - (area1_low / area0_low + area0_high / area1_high) / 2 area0 = self.get_poissonian(hist_data[0][:], mu0, amp0).sum() area1 = self.get_poissonian(hist_data[0][:], mu1, amp1).sum() # try this new measure for the fidelity fidelity2 = 1 - ((area1_low_amp / area1) / (area0_low_amp / area0) + (area0_high_amp / area0) / (area1_high_amp / area1)) / 2 param_dict['normalized_fidelity'] = fidelity2 return threshold_fit, fidelity, param_dict # this works if your data is normalized to the interval (-1,1) if distr == 'gaussian_normalized': # first some helper functions def two_gaussian_intersect(m1, m2, std1, std2, amp1, amp2): """ function to calculate intersection of two gaussians """ a = 1 / (2 * std1**2) - 1 / (2 * std2**2) b = m2 / (std2**2) - m1 / (std1**2) c = m1**2 / (2 * std1**2) - m2**2 / (2 * std2**2) - np.log( amp2 / amp1) return np.roots([a, b, c]) def gaussian(counts, amp, stdv, mean): return amp * np.exp( -(counts - mean)**2 / (2 * stdv**2)) / (stdv * np.sqrt(2 * np.pi)) try: result = self._fit_logic.make_gaussiandouble_fit( x_axis, y_data, self._fit_logic.estimate_gaussiandouble_peak) # calculating the threshold # NOTE the threshold is taken as the intersection of the two gaussians, while this should give # a good approximation I doubt it is mathematical exact. mu0 = result.params['g0_center'].value mu1 = result.params['g1_center'].value sigma0 = result.params['g0_sigma'].value sigma1 = result.params['g1_sigma'].value amp0 = result.params['g0_amplitude'].value / ( sigma0 * np.sqrt(2 * np.pi)) amp1 = result.params['g1_amplitude'].value / ( sigma1 * np.sqrt(2 * np.pi)) candidates = two_gaussian_intersect(mu0, mu1, sigma0, sigma1, amp0, amp1) # we want to get the intersection that lies between the two peaks if mu0 < mu1: threshold = [ i for i in filter(lambda x: (x > mu0) & (x < mu1), candidates) ] else: threshold = [ i for i in filter(lambda x: (x < mu0) & (x > mu1), candidates) ] threshold = threshold[0] # now we want to get the readout fidelity # of the bigger peak ( most likely the two states that aren't driven by the mw pi pulse ) if mu0 < mu1: gc0 = integrate.quad( lambda counts: gaussian(counts, amp1, sigma1, mu1), -1, 1) gp0 = integrate.quad( lambda counts: gaussian(counts, amp1, sigma1, mu1), -1, threshold) else: gc0 = integrate.quad( lambda counts: gaussian(counts, amp0, sigma0, mu0), -1, 1) gp0 = integrate.quad( lambda counts: gaussian(counts, amp0, sigma0, mu0), -1, threshold) # and then the same for the other peak ] if mu0 > mu1: gc1 = integrate.quad( lambda counts: gaussian(counts, amp1, sigma1, mu1), -1, 1) gp1 = integrate.quad( lambda counts: gaussian(counts, amp1, sigma1, mu1), threshold, 1) else: gc1 = integrate.quad( lambda counts: gaussian(counts, amp0, sigma0, mu0), -1, 1) gp1 = integrate.quad( lambda counts: gaussian(counts, amp0, sigma0, mu0), threshold, 1) param_dict = {} fidelity = 1 - (gp0[0] / gc0[0] + gp1[0] / gc1[0]) / 2 fidelity1 = 1 - (gp0[0] / gc0[0]) fidelity2 = 1 - gp1[0] / gc1[0] threshold_fit = threshold # if the fit worked, add also the result to the param_dict, which might be useful for debugging param_dict['result'] = result except: self.log.error('could not fit the data') error = True fidelity = 0 threshold_fit = 0 param_dict = {} new_dict = {} new_dict['value'] = np.inf param_dict['chi_sqr'] = new_dict return threshold_fit, fidelity, param_dict def calculate_binary_trace(self, trace, threshold): """ Calculate for a given threshold all the trace values und output a binary array, where True = Below or equal Threshold False = Above Threshold. @param np.array trace: a 1D array containing the y data, e.g. ccunts @param float threshold: value to decide whether a point in the trace is below/equal (True) or above threshold (False). @return np.array: 1D trace of the length(trace) but now with boolean entries """ return trace <= threshold def extract_filtered_values(self, trace, threshold, below=True): """ Extract only those values, which are below or equal a certain Threshold. @param np.array trace: @param float threshold: @return tuple(index_array, filtered_array): np.array index_array: 1D integer array containing the indices of the passed trace array which are equal or below the threshold np.array filtered_array: the actual values of the trace, which are equal or below threshold """ if below: index_array = np.where(trace <= threshold)[0] else: index_array = np.where(trace > threshold)[0] filtered_array = trace[index_array] return index_array, filtered_array
class ConfocalScanner_PI_Swabian_Interfuse(Base, FastCounterInterface, PulserInterface): """This is the Interface class to define the controls for the simple microwave hardware. """ _modclass = 'Swabian I/O Interfuse' _modtype = 'hardware' # connectors fitlogic = Connector(interface='FitLogic') pulsestreamer = Connector(interface='PulserInterface') timetagger = Connector(interface='FastCounterInterface') # config options _clock_frequency = ConfigOption('clock_frequency', 100, missing='warn') def __init__(self, config, **kwargs): super().__init__(config=config, **kwargs) # Internal parameters def on_activate(self): """ Initialisation performed during activation of the module. """ self._fit_logic = self.fitlogic() self._output_hw = self.pulsestreamer() self._input_hw = self.timetagger() self._input_hw.on_activate() self._output_hw.on_activate() # configure(self, bin_width_s, record_length_s, number_of_gates=0) self._input_hw.configure(self._output_hw._channel_detect) def on_deactivate(self): self._input_hw.on_deactivate() self._output_hw.on_deactivate() # === timetagger / input functionality === def get_constraints(self): """ Retrieve the hardware constrains from the Fast counting device. @return dict: dict with keys being the constraint names as string and items are the definition for the constaints. """ self._input_hw.get_constraints() def configure(self, bin_width_s, record_length_s, number_of_gates=0): """ Configuration of the fast counter. @param float bin_width_s: Length of a single time bin in the time trace histogram in seconds. @param float record_length_s: Total length of the timetrace/each single gate in seconds. @param int number_of_gates: optional, number of gates in the pulse sequence. Ignore for not gated counter. @return tuple(binwidth_s, record_length_s, number_of_gates): binwidth_s: float the actual set binwidth in seconds gate_length_s: the actual record length in seconds number_of_gates: the number of gated, which are accepted, None if not-gated """ self._input_hw.configure(bin_width_s, record_length_s, number_of_gates) def get_status(self): """ Receives the current status of the Fast Counter and outputs it as return value. 0 = unconfigured 1 = idle 2 = running 3 = paused -1 = error state """ self._input_hw.get_status() def start_measure(self): """ Start the fast counter. """ self._input_hw.start_measure() def stop_measure(self): """ Stop the fast counter. """ self._input_hw.stop_measure() def pause_measure(self): """ Pauses the current measurement. Fast counter must be initially in the run state to make it pause. """ self._input_hw.pause_measure() def continue_measure(self): """ Continues the current measurement. If fast counter is in pause state, then fast counter will be continued. """ self._input_hw.continue_measure() def is_gated(self): """ Check the gated counting possibility. @return bool: Boolean value indicates if the fast counter is a gated counter (TRUE) or not (FALSE). """ self._input_hw.is_gated() def get_binwidth(self): """ Returns the width of a single timebin in the timetrace in seconds. @return float: current length of a single bin in seconds (seconds/bin) """ self._input_hw.get_binwidth() def get_data_trace(self): """ Polls the current timetrace data from the fast counter. Return value is a numpy array (dtype = int64). The binning, specified by calling configure() in forehand, must be taken care of in this hardware class. A possible overflow of the histogram bins must be caught here and taken care of. If the counter is NOT GATED it will return a 1D-numpy-array with returnarray[timebin_index] If the counter is GATED it will return a 2D-numpy-array with returnarray[gate_index, timebin_index] """ self._input_hw.get_data_trace() # === PulseStreamer / output === def get_constraints(self): """ Retrieve the hardware constrains from the Pulsing device. @return constraints object: object with pulser constraints as attributes. """ self._output_hw.get_constraints() def pulser_on(self): """ Switches the pulsing device on. @return int: error code (0:OK, -1:error) """ self._output_hw.pulser_on() def pulser_off(self): """ Switches the pulsing device off. @return int: error code (0:OK, -1:error) """ self._output_hw.pulser_off() def upload_asset(self, asset_name=None): """ Upload an already hardware conform file to the device mass memory. Also loads these files into the device workspace if present. Does NOT load waveforms/sequences/patterns into channels. @param asset_name: string, name of the ensemble/sequence to be uploaded @return int: error code (0:OK, -1:error) """ self._output_hw.upload_asset(asset_name) def load_asset(self, asset_name, load_dict=None): """ Loads a sequence or waveform to the specified channel of the pulsing device. For devices that have a workspace (i.e. AWG) this will load the asset from the device workspace into the channel. For a device without mass memory this will transfer the waveform/sequence/pattern data directly to the device so that it is ready to play. @param str asset_name: The name of the asset to be loaded @param dict load_dict: a dictionary with keys being one of the available channel numbers and items being the name of the already sampled waveform/sequence files. Examples: {1: rabi_Ch1, 2: rabi_Ch2} {1: rabi_Ch2, 2: rabi_Ch1} This parameter is optional. If none is given then the channel association is invoked from the file name, i.e. the appendix (_ch1, _ch2 etc.) @return int: error code (0:OK, -1:error) """ self._output_hw.load_asset(asset_name, load_dict) def get_loaded_asset(self): """ Retrieve the currently loaded asset name of the device. @return str: Name of the current asset ready to play. (no filename) """ self._output_hw.get_loaded_asset() def clear_all(self): """ Clears all loaded waveforms from the pulse generators RAM/workspace. @return int: error code (0:OK, -1:error) """ self._output_hw.clear_all() def get_status(self): """ Retrieves the status of the pulsing hardware @return (int, dict): tuple with an interger value of the current status and a corresponding dictionary containing status description for all the possible status variables of the pulse generator hardware. """ self._output_hw.get_status() def get_sample_rate(self): """ Get the sample rate of the pulse generator hardware @return float: The current sample rate of the device (in Hz) """ self._output_hw.get_sample_rate() def set_sample_rate(self, sample_rate): """ Set the sample rate of the pulse generator hardware. @param float sample_rate: The sampling rate to be set (in Hz) @return float: the sample rate returned from the device (in Hz). """ self._output_hw.set_sample_rate(sample_rate) def get_analog_level(self, amplitude=None, offset=None): """ Retrieve the analog amplitude and offset of the provided channels. @param list amplitude: optional, if the amplitude value (in Volt peak to peak, i.e. the full amplitude) of a specific channel is desired. @param list offset: optional, if the offset value (in Volt) of a specific channel is desired. @return: (dict, dict): tuple of two dicts, with keys being the channel descriptor string (i.e. 'a_ch1') and items being the values for those channels. Amplitude is always denoted in Volt-peak-to-peak and Offset in volts. """ self._output_hw.get_analog_level(amplitude, offset) def set_analog_level(self, amplitude=None, offset=None): """ Set amplitude and/or offset value of the provided analog channel(s). @param dict amplitude: dictionary, with key being the channel descriptor string (i.e. 'a_ch1', 'a_ch2') and items being the amplitude values (in Volt peak to peak, i.e. the full amplitude) for the desired channel. @param dict offset: dictionary, with key being the channel descriptor string (i.e. 'a_ch1', 'a_ch2') and items being the offset values (in absolute volt) for the desired channel. @return (dict, dict): tuple of two dicts with the actual set values for amplitude and offset for ALL channels. """ self._output_hw.set_analog_level(amplitude, offset) def get_digital_level(self, low=None, high=None): """ Retrieve the digital low and high level of the provided/all channels. @param list low: optional, if the low value (in Volt) of a specific channel is desired. @param list high: optional, if the high value (in Volt) of a specific channel is desired. @return: (dict, dict): tuple of two dicts, with keys being the channel descriptor strings (i.e. 'd_ch1', 'd_ch2') and items being the values for those channels. Both low and high value of a channel is denoted in volts. """ self._output_hw.get_digital_level(low, high) def set_digital_level(self, low=None, high=None): """ Set low and/or high value of the provided digital channel. @param dict low: dictionary, with key being the channel descriptor string (i.e. 'd_ch1', 'd_ch2') and items being the low values (in volt) for the desired channel. @param dict high: dictionary, with key being the channel descriptor string (i.e. 'd_ch1', 'd_ch2') and items being the high values (in volt) for the desired channel. @return (dict, dict): tuple of two dicts where first dict denotes the current low value and the second dict the high value for ALL digital channels. Keys are the channel descriptor strings (i.e. 'd_ch1', 'd_ch2') """ self._output_hw.set_digital_level(low, high) def get_active_channels(self, ch=None): """ Get the active channels of the pulse generator hardware. @param list ch: optional, if specific analog or digital channels are needed to be asked without obtaining all the channels. @return dict: where keys denoting the channel string and items boolean expressions whether channel are active or not. """ self._output_hw.get_active_channels(ch) def set_active_channels(self, ch=None): """ Set the active channels for the pulse generator hardware. @param dict ch: dictionary with keys being the analog or digital string generic names for the channels (i.e. 'd_ch1', 'a_ch2') with items being a boolean value. True: Activate channel, False: Deactivate channel @return dict: with the actual set values for ALL active analog and digital channels """ self._output_hw.set_active_channels(ch) def get_uploaded_asset_names(self): """ Retrieve the names of all uploaded assets on the device. @return list: List of all uploaded asset name strings in the current device directory. This is no list of the file names. Unused for pulse generators without sequence storage capability (PulseBlaster, FPGA). """ self._output_hw.get_uploaded_asset_names() def get_saved_asset_names(self): """ Retrieve the names of all sampled and saved assets on the host PC. This is no list of the file names. @return list: List of all saved asset name strings in the current directory of the host PC. """ self._output_hw.get_saved_asset_names() def delete_asset(self, asset_name): """ Delete all files associated with an asset with the passed asset_name from the device memory (mass storage as well as i.e. awg workspace/channels). @param str asset_name: The name of the asset to be deleted Optionally a list of asset names can be passed. @return list: a list with strings of the files which were deleted. Unused for pulse generators without sequence storage capability (PulseBlaster, FPGA). """ self._output_hw.delete_asset(asset_name) def set_asset_dir_on_device(self, dir_path): """ Change the directory where the assets are stored on the device. @param str dir_path: The target directory @return int: error code (0:OK, -1:error) Unused for pulse generators without changeable file structure (PulseBlaster, FPGA). """ self._output_hw.set_asset_dir_on_device(dir_path) def get_asset_dir_on_device(self): """ Ask for the directory where the hardware conform files are stored on the device. @return str: The current file directory Unused for pulse generators without changeable file structure (i.e. PulseBlaster, FPGA). """ self._output_hw.get_asset_dir_on_device() def get_interleave(self): """ Check whether Interleave is ON or OFF in AWG. @return bool: True: ON, False: OFF Will always return False for pulse generator hardware without interleave. """ self._output_hw.get_interleave() def set_interleave(self, state=False): """ Turns the interleave of an AWG on or off. @param bool state: The state the interleave should be set to (True: ON, False: OFF) @return bool: actual interleave status (True: ON, False: OFF) Note: After setting the interleave of the device, retrieve the interleave again and use that information for further processing. Unused for pulse generator hardware other than an AWG. """ self._output_hw.set_interleave(state) def tell(self, command): """ Sends a command string to the device. @param string command: string containing the command @return int: error code (0:OK, -1:error) """ self._output_hw.tell(command) def ask(self, question): """ Asks the device a 'question' and receive and return an answer from it. @param string question: string containing the command @return string: the answer of the device to the 'question' in a string """ self._output_hw.ask(question) def reset(self): """ Reset the device. @return int: error code (0:OK, -1:error) """ self._output_hw.reset() def has_sequence_mode(self): """ Asks the pulse generator whether sequence mode exists. @return: bool, True for yes, False for no. """ self._output_hw.has_sequence_mode()
class PoiManagerGui(GUIBase): """ This is the GUI Class for PoiManager """ _modclass = 'PoiManagerGui' _modtype = 'gui' # declare connectors poimanagerlogic1 = Connector(interface='PoiManagerLogic') confocallogic1 = Connector(interface='ConfocalLogic') def __init__(self, config, **kwargs): super().__init__(config=config, **kwargs) def on_activate(self): """ Initializes the overall GUI, and establishes the connectors. This method executes the init methods for each of the GUIs. """ # Connectors self._poi_manager_logic = self.poimanagerlogic1() self._confocal_logic = self.confocallogic1() self.log.debug("POI Manager logic is {0}".format( self._poi_manager_logic)) self.log.debug("Confocal logic is {0}".format(self._confocal_logic)) # Initializing the GUIs self.initMainUI() self.initReorientRoiDialogUI() # There could be POIs created in the logic already, so update lists and map self.populate_poi_list() self._redraw_sample_shift() self._redraw_poi_markers() def mouseMoved(self, event): """ Handles any mouse movements inside the image. @param event: Event that signals the new mouse movement. This should be of type QPointF. Gets the mouse position, converts it to a position scaled to the image axis and than calculates and updated the position to the current POI. """ # converts the absolute mouse position to a position relative to the axis mouse_point = self._mw.roi_map_ViewWidget.getPlotItem().getViewBox( ).mapSceneToView(event.toPoint()) #self.log.debug("Mouse at x = {0:0.2e}, y = {1:0.2e}".format(mouse_point.x(), mouse_point.y())) # only calculate distance, if a POI is selected if self._poi_manager_logic.active_poi is not None: cur_poi_pos = self._poi_manager_logic.get_poi_position( poikey=self._poi_manager_logic.active_poi.get_key()) dx = ScaledFloat(mouse_point.x() - cur_poi_pos[0]) dy = ScaledFloat(mouse_point.y() - cur_poi_pos[1]) d_total = ScaledFloat( np.sqrt((mouse_point.x() - cur_poi_pos[0])**2 + (mouse_point.y() - cur_poi_pos[1])**2)) self._mw.poi_distance_label.setText( '{0:.2r}m ({1:.2r}m, {2:.2r}m)'.format(d_total, dx, dy)) def initMainUI(self): """ Definition, configuration and initialisation of the POI Manager GUI. This init connects all the graphic modules, which were created in the *.ui file and configures the event handling between the modules. """ # Use the inherited class 'Ui_PoiManagerGuiTemplate' to create now the # GUI element: self._mw = PoiManagerMainWindow() ##################### # Configuring the dock widgets ##################### # All our gui elements are dockable, and so there should be no "central" widget. self._mw.centralwidget.hide() self._mw.setDockNestingEnabled(True) ##################### # Setting up display of ROI map xy image ##################### # Get the image for the display from the logic: self.roi_xy_image_data = self._poi_manager_logic.roi_map_data[:, :, 3] # Load the image in the display: self.roi_map_image = pg.ImageItem(image=self.roi_xy_image_data, axisOrder='row-major') self.roi_map_image.setRect( QtCore.QRectF( self._confocal_logic.image_x_range[0], self._confocal_logic.image_y_range[0], self._confocal_logic.image_x_range[1] - self._confocal_logic.image_x_range[0], self._confocal_logic.image_y_range[1] - self._confocal_logic.image_y_range[0])) # Add the display item to the roi map ViewWidget defined in the UI file self._mw.roi_map_ViewWidget.addItem(self.roi_map_image) self._mw.roi_map_ViewWidget.setLabel('bottom', 'X position', units='m') self._mw.roi_map_ViewWidget.setLabel('left', 'Y position', units='m') # Set to fixed 1.0 aspect ratio, since the metaphor is a "map" of the sample self._mw.roi_map_ViewWidget.setAspectLocked(lock=True, ratio=1.0) # Get the colorscales and set LUT my_colors = ColorScaleInferno() self.roi_map_image.setLookupTable(my_colors.lut) # Add color bar: self.roi_cb = ColorBar(my_colors.cmap_normed, 100, 0, 100000) self._mw.roi_cb_ViewWidget.addItem(self.roi_cb) self._mw.roi_cb_ViewWidget.hideAxis('bottom') self._mw.roi_cb_ViewWidget.setLabel('left', 'Fluorescence', units='c/s') self._mw.roi_cb_ViewWidget.setMouseEnabled(x=False, y=False) ##################### # Setting up display of sample shift plot ##################### # Load image in the display self.x_shift_plot = pg.PlotDataItem([0], [0], pen=pg.mkPen( palette.c1, style=QtCore.Qt.DotLine), symbol='o', symbolPen=palette.c1, symbolBrush=palette.c1, symbolSize=5, name='x') self.y_shift_plot = pg.PlotDataItem([0], [0], pen=pg.mkPen( palette.c2, style=QtCore.Qt.DotLine), symbol='s', symbolPen=palette.c2, symbolBrush=palette.c2, symbolSize=5, name='y') self.z_shift_plot = pg.PlotDataItem([0], [0], pen=pg.mkPen( palette.c3, style=QtCore.Qt.DotLine), symbol='t', symbolPen=palette.c3, symbolBrush=palette.c3, symbolSize=5, name='z') self._mw.sample_shift_ViewWidget.addLegend() # Add the plot to the ViewWidget defined in the UI file self._mw.sample_shift_ViewWidget.addItem(self.x_shift_plot) self._mw.sample_shift_ViewWidget.addItem(self.y_shift_plot) self._mw.sample_shift_ViewWidget.addItem(self.z_shift_plot) # Label axes self._mw.sample_shift_ViewWidget.setLabel('bottom', 'Time', units='s') self._mw.sample_shift_ViewWidget.setLabel('left', 'Sample shift', units='m') ##################### # Connect signals ##################### # Distance Measurement: # Introducing a SignalProxy will limit the rate of signals that get fired. # Otherwise we will run into a heap of unhandled function calls. proxy = pg.SignalProxy(self.roi_map_image.scene().sigMouseMoved, rateLimit=60, slot=self.mouseMoved) # Connecting a Mouse Signal to trace to mouse movement function. self.roi_map_image.scene().sigMouseMoved.connect(self.mouseMoved) # Toolbar actions self._mw.new_roi_Action.triggered.connect(self.make_new_roi) self._mw.save_roi_Action.triggered.connect(self.save_roi) self._mw.load_roi_Action.triggered.connect(self.load_roi) self._mw.reorient_roi_Action.triggered.connect( self.open_reorient_roi_dialog) self._mw.autofind_pois_Action.triggered.connect( self.do_autofind_poi_procedure) self._mw.optimize_roi_Action.triggered.connect(self.optimize_roi) self._mw.new_poi_Action.triggered.connect(self.set_new_poi) self._mw.goto_poi_Action.triggered.connect(self.goto_poi) self._mw.refind_poi_Action.triggered.connect(self.update_poi_pos) self._mw.track_poi_Action.triggered.connect(self.toggle_tracking) # Interface controls self._mw.get_confocal_image_PushButton.clicked.connect( self.get_confocal_image) self._mw.set_poi_PushButton.clicked.connect(self.set_new_poi) self._mw.delete_last_pos_Button.clicked.connect(self.delete_last_point) self._mw.manual_update_poi_PushButton.clicked.connect( self.manual_update_poi) self._mw.move_poi_PushButton.clicked.connect(self.move_poi) self._mw.poi_name_LineEdit.returnPressed.connect(self.change_poi_name) self._mw.roi_name_LineEdit.editingFinished.connect(self.set_roi_name) self._mw.delete_poi_PushButton.clicked.connect(self.delete_poi) self._mw.goto_poi_after_update_checkBox.toggled.connect( self.toggle_follow) # This needs to be activated so that it only listens to user input, and ignores # algorithmic index changes self._mw.active_poi_ComboBox.activated.connect( self.handle_active_poi_ComboBox_index_change) self._mw.refind_method_ComboBox.currentIndexChanged.connect( self.change_refind_method) # Connect the buttons and inputs for the colorbar self._mw.roi_cb_centiles_RadioButton.toggled.connect( self.refresh_roi_colorscale) self._mw.roi_cb_manual_RadioButton.toggled.connect( self.refresh_roi_colorscale) self._mw.roi_cb_min_SpinBox.valueChanged.connect( self.shortcut_to_roi_cb_manual) self._mw.roi_cb_max_SpinBox.valueChanged.connect( self.shortcut_to_roi_cb_manual) self._mw.roi_cb_low_percentile_DoubleSpinBox.valueChanged.connect( self.shortcut_to_roi_cb_centiles) self._mw.roi_cb_high_percentile_DoubleSpinBox.valueChanged.connect( self.shortcut_to_roi_cb_centiles) self._mw.display_shift_vs_duration_RadioButton.toggled.connect( self._redraw_sample_shift) self._mw.display_shift_vs_clocktime_RadioButton.toggled.connect( self._redraw_sample_shift) self._markers = dict() # Signal at end of refocus self._poi_manager_logic.signal_timer_updated.connect( self._update_timer, QtCore.Qt.QueuedConnection) self._poi_manager_logic.signal_poi_updated.connect( self._redraw_sample_shift, QtCore.Qt.QueuedConnection) self._poi_manager_logic.signal_poi_updated.connect( self.populate_poi_list, QtCore.Qt.QueuedConnection) self._poi_manager_logic.signal_poi_updated.connect( self._redraw_poi_markers, QtCore.Qt.QueuedConnection) self._poi_manager_logic.signal_poi_deleted.connect( self._remove_poi_marker) self._poi_manager_logic.signal_confocal_image_updated.connect( self._redraw_roi_image) self._poi_manager_logic.signal_periodic_opt_duration_changed.connect( self._track_period_changed) self._poi_manager_logic.signal_periodic_opt_started.connect( self._tracking_started) self._poi_manager_logic.signal_periodic_opt_stopped.connect( self._tracking_stopped) # Connect track period after setting the GUI value from the logic initial_period = self._poi_manager_logic.timer_duration self._mw.track_period_SpinBox.setValue(initial_period) self._mw.time_till_next_update_ProgressBar.setMaximum(initial_period) self._mw.time_till_next_update_ProgressBar.setValue(initial_period) self._mw.track_period_SpinBox.valueChanged.connect( self.set_track_period) # Redraw the sample_shift axes if the range changes self._mw.sample_shift_ViewWidget.plotItem.sigRangeChanged.connect( self._redraw_sample_shift) self._mw.show() def initReorientRoiDialogUI(self): """ Definition, configuration and initialization fo the Reorient ROI Dialog GUI. This init connects all the graphic modules which were created in the *.ui file and configures event handling. """ # Create the Reorient ROI Dialog window self._rrd = ReorientRoiDialog() # Connect the QDialog buttons to methods in the GUI self._rrd.accepted.connect(self.do_roi_reorientation) self._rrd.rejected.connect(self.reset_reorientation_dialog) # Connect the at_crosshair buttons self._rrd.ref_a_at_crosshair_PushButton.clicked.connect( self.ref_a_at_crosshair) self._rrd.ref_b_at_crosshair_PushButton.clicked.connect( self.ref_b_at_crosshair) self._rrd.ref_c_at_crosshair_PushButton.clicked.connect( self.ref_c_at_crosshair) # Connect input value changes to update the sanity-check values self._rrd.ref_a_poi_ComboBox.activated.connect( self.reorientation_sanity_check) self._rrd.ref_b_poi_ComboBox.activated.connect( self.reorientation_sanity_check) self._rrd.ref_c_poi_ComboBox.activated.connect( self.reorientation_sanity_check) self._rrd.ref_a_x_pos_DoubleSpinBox.valueChanged.connect( self.reorientation_sanity_check) self._rrd.ref_a_y_pos_DoubleSpinBox.valueChanged.connect( self.reorientation_sanity_check) self._rrd.ref_a_z_pos_DoubleSpinBox.valueChanged.connect( self.reorientation_sanity_check) self._rrd.ref_b_x_pos_DoubleSpinBox.valueChanged.connect( self.reorientation_sanity_check) self._rrd.ref_b_y_pos_DoubleSpinBox.valueChanged.connect( self.reorientation_sanity_check) self._rrd.ref_b_z_pos_DoubleSpinBox.valueChanged.connect( self.reorientation_sanity_check) self._rrd.ref_c_x_pos_DoubleSpinBox.valueChanged.connect( self.reorientation_sanity_check) self._rrd.ref_c_y_pos_DoubleSpinBox.valueChanged.connect( self.reorientation_sanity_check) self._rrd.ref_c_z_pos_DoubleSpinBox.valueChanged.connect( self.reorientation_sanity_check) def on_deactivate(self): """ Deinitialisation performed during deactivation of the module. """ self._mw.close() def show(self): """Make main window visible and put it above all other windows. """ QtWidgets.QMainWindow.show(self._mw) self._mw.activateWindow() self._mw.raise_() def get_confocal_image(self): """ Update the roi_map_data in poi manager logic, and use this updated data to redraw an image of the ROI. """ # Make poi manager logic get the confocal data self._poi_manager_logic.get_confocal_image_data() def _redraw_roi_image(self): # the image data is the fluorescence part self.roi_xy_image_data = self._poi_manager_logic.roi_map_data[:, :, 3] # Also get the x and y range limits and hold them locally self.roi_map_xmin = np.min(self._poi_manager_logic.roi_map_data[:, :, 0]) self.roi_map_xmax = np.max(self._poi_manager_logic.roi_map_data[:, :, 0]) self.roi_map_ymin = np.min(self._poi_manager_logic.roi_map_data[:, :, 1]) self.roi_map_ymax = np.max(self._poi_manager_logic.roi_map_data[:, :, 1]) self.roi_map_image.getViewBox().enableAutoRange() self.roi_map_image.setRect( QtCore.QRectF(self.roi_map_xmin, self.roi_map_ymin, self.roi_map_xmax - self.roi_map_xmin, self.roi_map_ymax - self.roi_map_ymin)) self.roi_map_image.setImage(image=self.roi_xy_image_data, autoLevels=True) def shortcut_to_roi_cb_manual(self): self._mw.roi_cb_manual_RadioButton.setChecked(True) self.refresh_roi_colorscale() def shortcut_to_roi_cb_centiles(self): self._mw.roi_cb_centiles_RadioButton.setChecked(True) self.refresh_roi_colorscale() def refresh_roi_colorscale(self): """ Adjust the colorbar in the ROI xy image, and update the image with the new color scale. Calls the refresh method from colorbar, which takes either the lowest and higherst value in the image or predefined ranges. Note that you can invert the colorbar if the lower border is bigger then the higher one. """ cb_min, cb_max = self.determine_cb_range() self.roi_map_image.setImage(image=self.roi_xy_image_data, levels=(cb_min, cb_max)) self.roi_cb.refresh_colorbar(cb_min, cb_max) self._mw.roi_cb_ViewWidget.update() def determine_cb_range(self): """ Process UI input to determine color bar range""" # If "Centiles" is checked, adjust colour scaling automatically to centiles. # Otherwise, take user-defined values. if self._mw.roi_cb_centiles_RadioButton.isChecked(): low_centile = self._mw.roi_cb_low_percentile_DoubleSpinBox.value() high_centile = self._mw.roi_cb_high_percentile_DoubleSpinBox.value( ) cb_min = np.percentile(self.roi_xy_image_data, low_centile) cb_max = np.percentile(self.roi_xy_image_data, high_centile) else: cb_min = self._mw.roi_cb_min_SpinBox.value() cb_max = self._mw.roi_cb_max_SpinBox.value() return cb_min, cb_max def set_new_poi(self): """ This method sets a new poi from the current crosshair position.""" key = self._poi_manager_logic.add_poi() def delete_last_point(self): """ Delete the last track position of a chosen poi. """ if self._poi_manager_logic.active_poi is None: self.log.warning("No POI selected. No datapoint can be deleted") else: self._poi_manager_logic.delete_last_position( poikey=self._poi_manager_logic.active_poi.get_key()) def delete_poi(self): """ Delete the active poi from the list of managed points. """ if self._poi_manager_logic.active_poi is None: self.log.warning("No POI selected.") else: key = self._poi_manager_logic.active_poi.get_key() # todo: this needs to handle the case where the logic deletes a POI. self._poi_manager_logic.delete_poi(poikey=key) def _remove_poi_marker(self, poikey): """ Remove the POI marker for a POI that was deleted. """ self._markers[poikey].delete_from_viewwidget() del self._markers[poikey] def manual_update_poi(self): """ Manually adds a point to the trace of a given poi without refocussing, and uses that information to update sample position. """ if self._poi_manager_logic.active_poi is None: self.log.warning("No POI selected.") else: self._poi_manager_logic.set_new_position( poikey=self._poi_manager_logic.active_poi.get_key()) def move_poi(self): """Manually move a POI to a new location in the sample map, but WITHOUT changing the sample position. This moves a POI relative to all the others. """ if self._poi_manager_logic.active_poi is None: self.log.warning("No POI selected.") else: self._poi_manager_logic.move_coords( poikey=self._poi_manager_logic.active_poi.get_key()) def toggle_tracking(self): if self._poi_manager_logic.active_poi is None: self.log.warning("No POI selected.") else: if self._poi_manager_logic.timer is None: self._poi_manager_logic.start_periodic_refocus( poikey=self._poi_manager_logic.active_poi.get_key()) else: self._poi_manager_logic.stop_periodic_refocus() def _tracking_started(self): self._mw.track_poi_Action.setChecked(True) def _tracking_stopped(self): self._mw.track_poi_Action.setChecked(False) def goto_poi(self, key): """ Go to the last known position of poi <key>.""" if self._poi_manager_logic.active_poi is None: self.log.warning("No POI selected.") else: self._poi_manager_logic.go_to_poi( poikey=self._poi_manager_logic.active_poi.get_key()) def populate_poi_list(self): """ Populate the dropdown box for selecting a poi. """ self.log.debug('started populate_poi_list at {0}'.format(time.time())) self._mw.active_poi_ComboBox.clear() self._mw.offset_anchor_ComboBox.clear() self._rrd.ref_a_poi_ComboBox.clear() self._rrd.ref_b_poi_ComboBox.clear() self._rrd.ref_c_poi_ComboBox.clear() for key in self._poi_manager_logic.get_all_pois(abc_sort=True): if key is not 'crosshair' and key is not 'sample': poi_list_empty = False self._mw.active_poi_ComboBox.addItem( self._poi_manager_logic.poi_list[key].get_name(), key) self._mw.offset_anchor_ComboBox.addItem( self._poi_manager_logic.poi_list[key].get_name(), key) self._rrd.ref_a_poi_ComboBox.addItem( self._poi_manager_logic.poi_list[key].get_name(), key) self._rrd.ref_b_poi_ComboBox.addItem( self._poi_manager_logic.poi_list[key].get_name(), key) self._rrd.ref_c_poi_ComboBox.addItem( self._poi_manager_logic.poi_list[key].get_name(), key) # If there is no active POI, set the combobox to nothing (-1) if self._poi_manager_logic.active_poi is None: self._mw.active_poi_ComboBox.setCurrentIndex(-1) # Otherwise, set it to the active POI else: self._mw.active_poi_ComboBox.setCurrentIndex( self._mw.active_poi_ComboBox.findData( self._poi_manager_logic.active_poi.get_key())) self.log.debug('finished populating at '.format(time.time())) def change_refind_method(self): """ Make appropriate changes in the GUI to reflect the newly chosen refind method.""" if self._mw.refind_method_ComboBox.currentText( ) == 'position optimisation': self._mw.offset_anchor_ComboBox.setEnabled(False) elif self._mw.refind_method_ComboBox.currentText() == 'offset anchor': self.log.error( "Anchor method not fully implemented yet. " "Feel free to fix this method. Using position optimisation instead." ) self._mw.offset_anchor_ComboBox.setEnabled(True) else: # TODO: throw an error self.log.debug('error 123') def set_roi_name(self): """ Set the name of a ROI (useful when saving).""" self._poi_manager_logic.roi_name = self._mw.roi_name_LineEdit.text( ).replace(" ", "_") def change_poi_name(self): """ Change the name of a poi.""" newname = self._mw.poi_name_LineEdit.text() self._poi_manager_logic.rename_poi( poikey=self._poi_manager_logic.active_poi.get_key(), name=newname) # After POI name is changed, empty name field self._mw.poi_name_LineEdit.setText('') def handle_active_poi_ComboBox_index_change(self): """ Handle the change of index in the active POI combobox.""" key = self._mw.active_poi_ComboBox.itemData( self._mw.active_poi_ComboBox.currentIndex()) self._poi_manager_logic.set_active_poi(poikey=key) self._redraw_poi_markers( ) # todo when line 660 signal in logic is done, this is not necessary def select_poi_from_marker(self, poikey=None): """ Process the selection of a POI from click on POImark.""" # Keep track of selected POI self._poi_manager_logic.set_active_poi(poikey=poikey) # # Set the selected POI in the combobox # self._mw.active_poi_ComboBox.setCurrentIndex(self._mw.active_poi_ComboBox.findData(poikey)) # self._redraw_poi_markers() def update_poi_pos(self): if self._poi_manager_logic.active_poi is None: self.log.warning("No POI selected.") else: if self._mw.refind_method_ComboBox.currentText( ) == 'position optimisation': self._poi_manager_logic.optimise_poi( poikey=self._poi_manager_logic.active_poi.get_key()) elif self._mw.refind_method_ComboBox.currentText( ) == 'offset anchor': anchor_key = self._mw.offset_anchor_ComboBox.itemData( self._mw.offset_anchor_ComboBox.currentIndex()) self._poi_manager_logic.optimise_poi( poikey=self._poi_manager_logic.active_poi.get_key(), anchorkey=anchor_key) def toggle_follow(self): if self._mw.goto_poi_after_update_checkBox.isChecked(): self._poi_manager_logic.go_to_crosshair_after_refocus = False else: self._poi_manager_logic.go_to_crosshair_after_refocus = True def _update_timer(self): self._mw.time_till_next_update_ProgressBar.setValue( self._poi_manager_logic.time_left) def set_track_period(self): """ Change the progress bar and update the timer duration.""" new_track_period = self._mw.track_period_SpinBox.value() self._poi_manager_logic.set_periodic_optimize_duration( duration=new_track_period) def _track_period_changed(self): """ Reflect the changed track period in the GUI elements. """ new_track_period = self._poi_manager_logic.timer_duration # Set the new maximum for the progress bar self._mw.time_till_next_update_ProgressBar.setMaximum(new_track_period) # If the tracker is not active, then set the value of the progress bar to the # new maximum if not self._mw.track_poi_Action.isChecked(): self._mw.time_till_next_update_ProgressBar.setValue( new_track_period) def _redraw_clocktime_ticks(self): """If duration is displayed, reset ticks to default. Otherwise, create and update custom date/time ticks to the new axis range. """ myAxisItem = self._mw.sample_shift_ViewWidget.plotItem.axes['bottom'][ 'item'] # if duration display, reset to default ticks if self._mw.display_shift_vs_duration_RadioButton.isChecked(): myAxisItem.setTicks(None) # otherwise, convert tick strings to clock format else: # determine size of the sample shift bottom axis item in pixels bounds = myAxisItem.mapRectFromParent(myAxisItem.geometry()) span = (bounds.topLeft(), bounds.topRight()) lengthInPixels = (span[1] - span[0]).manhattanLength() if lengthInPixels == 0: return -1 if myAxisItem.range[0] < 0: return -1 default_ticks = myAxisItem.tickValues(myAxisItem.range[0], myAxisItem.range[1], lengthInPixels) newticks = [] for i, tick_level in enumerate(default_ticks): newticks_this_level = [] ticks = tick_level[1] for ii, tick in enumerate(ticks): # For major ticks, include date if i == 0: string = time.strftime("%H:%M (%d.%m.)", time.localtime(tick * 3600)) # (the axis is plotted in hours to get naturally better placed ticks.) # for middle and minor ticks, just display clock time else: string = time.strftime("%H:%M", time.localtime(tick * 3600)) newticks_this_level.append((tick, string)) newticks.append(newticks_this_level) myAxisItem.setTicks(newticks) return 0 def _redraw_sample_shift(self): # Get trace data and calculate shifts in x,y,z poi_trace = self._poi_manager_logic.poi_list[ 'sample'].get_position_history() # If duration display is checked, subtract initial time and convert to # mins or hours as appropriate if self._mw.display_shift_vs_duration_RadioButton.isChecked(): time_shift_data = (poi_trace[:, 0] - poi_trace[0, 0]) if np.max(time_shift_data) < 300: self._mw.sample_shift_ViewWidget.setLabel('bottom', 'Time elapsed', units='s') elif np.max(time_shift_data) < 7200: time_shift_data = time_shift_data / 60.0 self._mw.sample_shift_ViewWidget.setLabel('bottom', 'Time elapsed', units='min') else: time_shift_data = time_shift_data / 3600.0 self._mw.sample_shift_ViewWidget.setLabel('bottom', 'Time elapsed', units='hr') # Otherwise, take the actual time but divide by 3600 so that tickmarks # automatically fall on whole hours else: time_shift_data = poi_trace[:, 0] / 3600.0 self._mw.sample_shift_ViewWidget.setLabel('bottom', 'Time', units='') # Subtract initial position to get shifts x_shift_data = (poi_trace[:, 1] - poi_trace[0, 1]) y_shift_data = (poi_trace[:, 2] - poi_trace[0, 2]) z_shift_data = (poi_trace[:, 3] - poi_trace[0, 3]) # Plot data self.x_shift_plot.setData(time_shift_data, x_shift_data) self.y_shift_plot.setData(time_shift_data, y_shift_data) self.z_shift_plot.setData(time_shift_data, z_shift_data) self._redraw_clocktime_ticks() def _redraw_poi_markers(self): self.log.debug('starting redraw_poi_markers {0}'.format(time.time())) for key in self._poi_manager_logic.get_all_pois(): if key is not 'crosshair' and key is not 'sample': position = self._poi_manager_logic.get_poi_position(poikey=key) position = position[:2] if key in self._markers.keys(): self._markers[key].set_position(position) self._markers[key].deselect() else: # Create Region of Interest as marker: marker = PoiMark(position, poi=self._poi_manager_logic.poi_list[key], click_action=self.select_poi_from_marker, movable=False, scaleSnap=False, snapSize=1.0e-6) # Add to the Map Widget marker.add_to_viewwidget(self._mw.roi_map_ViewWidget) self._markers[key] = marker if self._poi_manager_logic.active_poi is not None: active_poi_key = self._poi_manager_logic.active_poi.get_key() self._markers[active_poi_key].select() cur_poi_pos = self._poi_manager_logic.get_poi_position( poikey=active_poi_key) self._mw.poi_coords_label.setText( '({0:.2r}m, {1:.2r}m, {2:.2r}m)'.format( ScaledFloat(cur_poi_pos[0]), ScaledFloat(cur_poi_pos[1]), ScaledFloat(cur_poi_pos[2]))) self.log.debug('finished redraw at {0}'.format(time.time())) def make_new_roi(self): """ Start new ROI by removing all POIs and resetting the sample history.""" for key in self._poi_manager_logic.get_all_pois(): if key is not 'crosshair' and key is not 'sample': self._markers[key].delete_from_viewwidget() del self._markers self._markers = dict() self._poi_manager_logic.reset_roi() self.populate_poi_list() def save_roi(self): """ Save ROI to file.""" self._poi_manager_logic.save_poi_map_as_roi() def load_roi(self): """ Load a saved ROI from file.""" this_file = QtWidgets.QFileDialog.getOpenFileName( self._mw, str("Open ROI"), None, str("Data files (*.dat)"))[0] self._poi_manager_logic.load_roi_from_file(filename=this_file) self.populate_poi_list() def open_reorient_roi_dialog(self): """ Open the dialog for reorienting the ROI. """ self._rrd.show() def ref_a_at_crosshair(self): """ Set the newpos for ref A from the current crosshair position. """ # TODO: get the range for these spinboxes from the hardware scanner range! self._rrd.ref_a_x_pos_DoubleSpinBox.setValue( self._confocal_logic.get_position()[0]) self._rrd.ref_a_y_pos_DoubleSpinBox.setValue( self._confocal_logic.get_position()[1]) self._rrd.ref_a_z_pos_DoubleSpinBox.setValue( self._confocal_logic.get_position()[2]) def ref_b_at_crosshair(self): """ Set the newpos for ref B from the current crosshair position. """ self._rrd.ref_b_x_pos_DoubleSpinBox.setValue( self._confocal_logic.get_position()[0]) self._rrd.ref_b_y_pos_DoubleSpinBox.setValue( self._confocal_logic.get_position()[1]) self._rrd.ref_b_z_pos_DoubleSpinBox.setValue( self._confocal_logic.get_position()[2]) def ref_c_at_crosshair(self): """ Set the newpos for ref C from the current crosshair position. """ self._rrd.ref_c_x_pos_DoubleSpinBox.setValue( self._confocal_logic.get_position()[0]) self._rrd.ref_c_y_pos_DoubleSpinBox.setValue( self._confocal_logic.get_position()[1]) self._rrd.ref_c_z_pos_DoubleSpinBox.setValue( self._confocal_logic.get_position()[2]) def do_roi_reorientation(self): """Pass the old and new positions of refs A, B, C to PoiManager Logic to reorient every POI in the ROI. """ ref_a_coords, ref_b_coords, ref_c_coords, ref_a_newpos, ref_b_newpos, ref_c_newpos = self._read_reorient_roi_dialog_values( ) self._poi_manager_logic.reorient_roi(ref_a_coords, ref_b_coords, ref_c_coords, ref_a_newpos, ref_b_newpos, ref_c_newpos) # Clear the values in the Reorient Roi Dialog in case it is needed again self.reset_reorientation_dialog() def _read_reorient_roi_dialog_values(self): """ This reads the values from reorient ROI Dialog, and returns them. """ # Get POI keys for the chosen ref points ref_a_key = self._rrd.ref_a_poi_ComboBox.itemData( self._rrd.ref_a_poi_ComboBox.currentIndex()) ref_b_key = self._rrd.ref_b_poi_ComboBox.itemData( self._rrd.ref_b_poi_ComboBox.currentIndex()) ref_c_key = self._rrd.ref_c_poi_ComboBox.itemData( self._rrd.ref_c_poi_ComboBox.currentIndex()) # Get the old coords for these refs ref_a_coords = np.array( self._poi_manager_logic.poi_list[ref_a_key].get_coords_in_sample()) ref_b_coords = np.array( self._poi_manager_logic.poi_list[ref_b_key].get_coords_in_sample()) ref_c_coords = np.array( self._poi_manager_logic.poi_list[ref_c_key].get_coords_in_sample()) ref_a_newpos = np.array([ self._rrd.ref_a_x_pos_DoubleSpinBox.value(), self._rrd.ref_a_y_pos_DoubleSpinBox.value(), self._rrd.ref_a_z_pos_DoubleSpinBox.value() ]) ref_b_newpos = np.array([ self._rrd.ref_b_x_pos_DoubleSpinBox.value(), self._rrd.ref_b_y_pos_DoubleSpinBox.value(), self._rrd.ref_b_z_pos_DoubleSpinBox.value() ]) ref_c_newpos = np.array([ self._rrd.ref_c_x_pos_DoubleSpinBox.value(), self._rrd.ref_c_y_pos_DoubleSpinBox.value(), self._rrd.ref_c_z_pos_DoubleSpinBox.value() ]) return ref_a_coords, ref_b_coords, ref_c_coords, ref_a_newpos * 1e-6, ref_b_newpos * 1e-6, ref_c_newpos * 1e-6 def reset_reorientation_dialog(self): """ Reset all the values in the reorient roi dialog. """ self._rrd.ref_a_x_pos_DoubleSpinBox.setValue(0) self._rrd.ref_a_y_pos_DoubleSpinBox.setValue(0) self._rrd.ref_a_z_pos_DoubleSpinBox.setValue(0) self._rrd.ref_b_x_pos_DoubleSpinBox.setValue(0) self._rrd.ref_b_y_pos_DoubleSpinBox.setValue(0) self._rrd.ref_b_z_pos_DoubleSpinBox.setValue(0) self._rrd.ref_c_x_pos_DoubleSpinBox.setValue(0) self._rrd.ref_c_y_pos_DoubleSpinBox.setValue(0) self._rrd.ref_c_z_pos_DoubleSpinBox.setValue(0) def reorientation_sanity_check(self): """ Calculate the difference in length between edges of old triangle defined by refs A, B, C and the new triangle. """ # Get set of positions from GUI ref_a_coords, ref_b_coords, ref_c_coords, ref_a_newpos, ref_b_newpos, ref_c_newpos = self._read_reorient_roi_dialog_values( ) # Calculate the difference in side lengths AB, BC, CA between the old triangle and the new triangle delta_ab = np.linalg.norm(ref_b_coords - ref_a_coords) - np.linalg.norm(ref_b_newpos - ref_a_newpos) delta_bc = np.linalg.norm(ref_c_coords - ref_b_coords) - np.linalg.norm(ref_c_newpos - ref_b_newpos) delta_ca = np.linalg.norm(ref_a_coords - ref_c_coords) - np.linalg.norm(ref_a_newpos - ref_c_newpos) # Write to the GUI self._rrd.length_difference_ab_Label.setText(str(delta_ab)) self._rrd.length_difference_bc_Label.setText(str(delta_bc)) self._rrd.length_difference_ca_Label.setText(str(delta_ca)) def do_autofind_poi_procedure(self): """Run the autofind_pois procedure in the POI Manager Logic to get all the POIs in the current ROI image.""" #Fixme: Add here the appropriate functionality self.log.error("Has to be implemented properly. Feel free to do it.") # # Get the thresholds from the user-chosen color bar range # cb_min, cb_max = self.determine_cb_range() # # this_min_threshold = cb_min + 0.3 * (cb_max - cb_min) # this_max_threshold = cb_max # # self._poi_manager_logic.autofind_pois(neighborhood_size=1, min_threshold=this_min_threshold, max_threshold=this_max_threshold) def optimize_roi(self): """Run the autofind_pois procedure in the POI Manager Logic to get all the POIs in the current ROI image.""" #Fixme: Add here the appropriate functionality self.log.error("Not implemented yet. Feel free to help!")
class PulsedMasterLogic(GenericLogic): """ This logic module combines the functionality of two modules. It can be used to generate pulse sequences/waveforms and to control the settings for the pulse generator via SequenceGeneratorLogic. Essentially this part controls what is played on the pulse generator. Furthermore it can be used to set up a pulsed measurement with an already set-up pulse generator together with a fast counting device via PulsedMeasurementLogic. The main purpose for this module is to provide a single interface while maintaining a modular structure for complex pulsed measurements. Each of the sub-modules can be used without this module but more care has to be taken in that case. Automatic transfer of information from one sub-module to the other for convenience is also handled here. Another important aspect is the use of this module in scripts (e.g. jupyter notebooks). All calls to sub-module setter functions (PulsedMeasurementLogic and SequenceGeneratorLogic) are decoupled from the calling thread via Qt queued connections. This ensures a more intuitive and less error prone use of scripting. """ _modclass = 'pulsedmasterlogic' _modtype = 'logic' # declare connectors pulsedmeasurementlogic = Connector(interface='PulsedMeasurementLogic') sequencegeneratorlogic = Connector(interface='SequenceGeneratorLogic') # PulsedMeasurementLogic control signals sigDoFit = QtCore.Signal(str) sigToggleMeasurement = QtCore.Signal(bool, str) sigToggleMeasurementPause = QtCore.Signal(bool) sigTogglePulser = QtCore.Signal(bool) sigToggleExtMicrowave = QtCore.Signal(bool) sigFastCounterSettingsChanged = QtCore.Signal(dict) sigMeasurementSettingsChanged = QtCore.Signal(dict) sigExtMicrowaveSettingsChanged = QtCore.Signal(dict) sigAnalysisSettingsChanged = QtCore.Signal(dict) sigExtractionSettingsChanged = QtCore.Signal(dict) sigTimerIntervalChanged = QtCore.Signal(float) sigAlternativeDataTypeChanged = QtCore.Signal(str) sigManuallyPullData = QtCore.Signal() # signals for master module (i.e. GUI) coming from PulsedMeasurementLogic sigMeasurementDataUpdated = QtCore.Signal() sigTimerUpdated = QtCore.Signal(float, int, float) sigFitUpdated = QtCore.Signal(str, np.ndarray, object) sigMeasurementStatusUpdated = QtCore.Signal(bool, bool) sigPulserRunningUpdated = QtCore.Signal(bool) sigExtMicrowaveRunningUpdated = QtCore.Signal(bool) sigExtMicrowaveSettingsUpdated = QtCore.Signal(dict) sigFastCounterSettingsUpdated = QtCore.Signal(dict) sigMeasurementSettingsUpdated = QtCore.Signal(dict) sigAnalysisSettingsUpdated = QtCore.Signal(dict) sigExtractionSettingsUpdated = QtCore.Signal(dict) # SequenceGeneratorLogic control signals sigSavePulseBlock = QtCore.Signal(object) sigSaveBlockEnsemble = QtCore.Signal(object) sigSaveSequence = QtCore.Signal(object) sigDeletePulseBlock = QtCore.Signal(str) sigDeleteBlockEnsemble = QtCore.Signal(str) sigDeleteSequence = QtCore.Signal(str) sigLoadBlockEnsemble = QtCore.Signal(str) sigLoadSequence = QtCore.Signal(str) sigSampleBlockEnsemble = QtCore.Signal(str) sigSampleSequence = QtCore.Signal(str) sigClearPulseGenerator = QtCore.Signal() sigGeneratorSettingsChanged = QtCore.Signal(dict) sigSamplingSettingsChanged = QtCore.Signal(dict) sigGeneratePredefinedSequence = QtCore.Signal(str, dict) # signals for master module (i.e. GUI) coming from SequenceGeneratorLogic sigBlockDictUpdated = QtCore.Signal(dict) sigEnsembleDictUpdated = QtCore.Signal(dict) sigSequenceDictUpdated = QtCore.Signal(dict) sigAvailableWaveformsUpdated = QtCore.Signal(list) sigAvailableSequencesUpdated = QtCore.Signal(list) sigSampleEnsembleComplete = QtCore.Signal(object) sigSampleSequenceComplete = QtCore.Signal(object) sigLoadedAssetUpdated = QtCore.Signal(str, str) sigGeneratorSettingsUpdated = QtCore.Signal(dict) sigSamplingSettingsUpdated = QtCore.Signal(dict) sigPredefinedSequenceGenerated = QtCore.Signal(object) def __init__(self, config, **kwargs): """ Create PulsedMasterLogic object with connectors. @param dict kwargs: optional parameters """ super().__init__(config=config, **kwargs) # Dictionary servings as status register self.status_dict = dict() return def on_activate(self): """ Initialisation performed during activation of the module. """ # Initialize status register self.status_dict = { 'sampling_ensemble_busy': False, 'sampling_sequence_busy': False, 'sampload_busy': False, 'loading_busy': False, 'pulser_running': False, 'measurement_running': False, 'microwave_running': False, 'predefined_generation_busy': False, 'fitting_busy': False } # Connect signals controlling PulsedMeasurementLogic self.sigDoFit.connect(self.pulsedmeasurementlogic().do_fit, QtCore.Qt.QueuedConnection) self.sigToggleMeasurement.connect( self.pulsedmeasurementlogic().toggle_pulsed_measurement, QtCore.Qt.QueuedConnection) self.sigToggleMeasurementPause.connect( self.pulsedmeasurementlogic().toggle_measurement_pause, QtCore.Qt.QueuedConnection) self.sigTogglePulser.connect( self.pulsedmeasurementlogic().toggle_pulse_generator, QtCore.Qt.QueuedConnection) self.sigToggleExtMicrowave.connect( self.pulsedmeasurementlogic().toggle_microwave, QtCore.Qt.QueuedConnection) self.sigFastCounterSettingsChanged.connect( self.pulsedmeasurementlogic().set_fast_counter_settings, QtCore.Qt.QueuedConnection) self.sigMeasurementSettingsChanged.connect( self.pulsedmeasurementlogic().set_measurement_settings, QtCore.Qt.QueuedConnection) self.sigExtMicrowaveSettingsChanged.connect( self.pulsedmeasurementlogic().set_microwave_settings, QtCore.Qt.QueuedConnection) self.sigAnalysisSettingsChanged.connect( self.pulsedmeasurementlogic().set_analysis_settings, QtCore.Qt.QueuedConnection) self.sigExtractionSettingsChanged.connect( self.pulsedmeasurementlogic().set_extraction_settings, QtCore.Qt.QueuedConnection) self.sigTimerIntervalChanged.connect( self.pulsedmeasurementlogic().set_timer_interval, QtCore.Qt.QueuedConnection) self.sigAlternativeDataTypeChanged.connect( self.pulsedmeasurementlogic().set_alternative_data_type, QtCore.Qt.QueuedConnection) self.sigManuallyPullData.connect( self.pulsedmeasurementlogic().manually_pull_data, QtCore.Qt.QueuedConnection) # Connect signals coming from PulsedMeasurementLogic self.pulsedmeasurementlogic().sigMeasurementDataUpdated.connect( self.sigMeasurementDataUpdated, QtCore.Qt.QueuedConnection) self.pulsedmeasurementlogic().sigTimerUpdated.connect( self.sigTimerUpdated, QtCore.Qt.QueuedConnection) self.pulsedmeasurementlogic().sigFitUpdated.connect( self.fit_updated, QtCore.Qt.QueuedConnection) self.pulsedmeasurementlogic().sigMeasurementStatusUpdated.connect( self.measurement_status_updated, QtCore.Qt.QueuedConnection) self.pulsedmeasurementlogic().sigPulserRunningUpdated.connect( self.pulser_running_updated, QtCore.Qt.QueuedConnection) self.pulsedmeasurementlogic().sigExtMicrowaveRunningUpdated.connect( self.ext_microwave_running_updated, QtCore.Qt.QueuedConnection) self.pulsedmeasurementlogic().sigExtMicrowaveSettingsUpdated.connect( self.sigExtMicrowaveSettingsUpdated, QtCore.Qt.QueuedConnection) self.pulsedmeasurementlogic().sigFastCounterSettingsUpdated.connect( self.sigFastCounterSettingsUpdated, QtCore.Qt.QueuedConnection) self.pulsedmeasurementlogic().sigMeasurementSettingsUpdated.connect( self.sigMeasurementSettingsUpdated, QtCore.Qt.QueuedConnection) self.pulsedmeasurementlogic().sigAnalysisSettingsUpdated.connect( self.sigAnalysisSettingsUpdated, QtCore.Qt.QueuedConnection) self.pulsedmeasurementlogic().sigExtractionSettingsUpdated.connect( self.sigExtractionSettingsUpdated, QtCore.Qt.QueuedConnection) # Connect signals controlling SequenceGeneratorLogic self.sigSavePulseBlock.connect( self.sequencegeneratorlogic().save_block, QtCore.Qt.QueuedConnection) self.sigSaveBlockEnsemble.connect( self.sequencegeneratorlogic().save_ensemble, QtCore.Qt.QueuedConnection) self.sigSaveSequence.connect( self.sequencegeneratorlogic().save_sequence, QtCore.Qt.QueuedConnection) self.sigDeletePulseBlock.connect( self.sequencegeneratorlogic().delete_block, QtCore.Qt.QueuedConnection) self.sigDeleteBlockEnsemble.connect( self.sequencegeneratorlogic().delete_ensemble, QtCore.Qt.QueuedConnection) self.sigDeleteSequence.connect( self.sequencegeneratorlogic().delete_sequence, QtCore.Qt.QueuedConnection) self.sigLoadBlockEnsemble.connect( self.sequencegeneratorlogic().load_ensemble, QtCore.Qt.QueuedConnection) self.sigLoadSequence.connect( self.sequencegeneratorlogic().load_sequence, QtCore.Qt.QueuedConnection) self.sigSampleBlockEnsemble.connect( self.sequencegeneratorlogic().sample_pulse_block_ensemble, QtCore.Qt.QueuedConnection) self.sigSampleSequence.connect( self.sequencegeneratorlogic().sample_pulse_sequence, QtCore.Qt.QueuedConnection) self.sigClearPulseGenerator.connect( self.sequencegeneratorlogic().clear_pulser, QtCore.Qt.QueuedConnection) self.sigGeneratorSettingsChanged.connect( self.sequencegeneratorlogic().set_pulse_generator_settings, QtCore.Qt.QueuedConnection) self.sigSamplingSettingsChanged.connect( self.sequencegeneratorlogic().set_generation_parameters, QtCore.Qt.QueuedConnection) self.sigGeneratePredefinedSequence.connect( self.sequencegeneratorlogic().generate_predefined_sequence, QtCore.Qt.QueuedConnection) # Connect signals coming from SequenceGeneratorLogic self.sequencegeneratorlogic().sigBlockDictUpdated.connect( self.sigBlockDictUpdated, QtCore.Qt.QueuedConnection) self.sequencegeneratorlogic().sigEnsembleDictUpdated.connect( self.sigEnsembleDictUpdated, QtCore.Qt.QueuedConnection) self.sequencegeneratorlogic().sigSequenceDictUpdated.connect( self.sigSequenceDictUpdated, QtCore.Qt.QueuedConnection) self.sequencegeneratorlogic().sigAvailableWaveformsUpdated.connect( self.sigAvailableWaveformsUpdated, QtCore.Qt.QueuedConnection) self.sequencegeneratorlogic().sigAvailableSequencesUpdated.connect( self.sigAvailableSequencesUpdated, QtCore.Qt.QueuedConnection) self.sequencegeneratorlogic().sigGeneratorSettingsUpdated.connect( self.sigGeneratorSettingsUpdated, QtCore.Qt.QueuedConnection) self.sequencegeneratorlogic().sigSamplingSettingsUpdated.connect( self.sigSamplingSettingsUpdated, QtCore.Qt.QueuedConnection) self.sequencegeneratorlogic().sigPredefinedSequenceGenerated.connect( self.predefined_sequence_generated, QtCore.Qt.QueuedConnection) self.sequencegeneratorlogic().sigSampleEnsembleComplete.connect( self.sample_ensemble_finished, QtCore.Qt.QueuedConnection) self.sequencegeneratorlogic().sigSampleSequenceComplete.connect( self.sample_sequence_finished, QtCore.Qt.QueuedConnection) self.sequencegeneratorlogic().sigLoadedAssetUpdated.connect( self.loaded_asset_updated, QtCore.Qt.QueuedConnection) return def on_deactivate(self): """ @return: """ # Disconnect all signals # Disconnect signals controlling PulsedMeasurementLogic self.sigDoFit.disconnect() self.sigToggleMeasurement.disconnect() self.sigToggleMeasurementPause.disconnect() self.sigTogglePulser.disconnect() self.sigToggleExtMicrowave.disconnect() self.sigFastCounterSettingsChanged.disconnect() self.sigMeasurementSettingsChanged.disconnect() self.sigExtMicrowaveSettingsChanged.disconnect() self.sigAnalysisSettingsChanged.disconnect() self.sigExtractionSettingsChanged.disconnect() self.sigTimerIntervalChanged.disconnect() self.sigAlternativeDataTypeChanged.disconnect() self.sigManuallyPullData.disconnect() # Disconnect signals coming from PulsedMeasurementLogic self.pulsedmeasurementlogic().sigMeasurementDataUpdated.disconnect() self.pulsedmeasurementlogic().sigTimerUpdated.disconnect() self.pulsedmeasurementlogic().sigFitUpdated.disconnect() self.pulsedmeasurementlogic().sigMeasurementStatusUpdated.disconnect() self.pulsedmeasurementlogic().sigPulserRunningUpdated.disconnect() self.pulsedmeasurementlogic().sigExtMicrowaveRunningUpdated.disconnect( ) self.pulsedmeasurementlogic( ).sigExtMicrowaveSettingsUpdated.disconnect() self.pulsedmeasurementlogic().sigFastCounterSettingsUpdated.disconnect( ) self.pulsedmeasurementlogic().sigMeasurementSettingsUpdated.disconnect( ) self.pulsedmeasurementlogic().sigAnalysisSettingsUpdated.disconnect() self.pulsedmeasurementlogic().sigExtractionSettingsUpdated.disconnect() # Disconnect signals controlling SequenceGeneratorLogic self.sigSavePulseBlock.disconnect() self.sigSaveBlockEnsemble.disconnect() self.sigSaveSequence.disconnect() self.sigDeletePulseBlock.disconnect() self.sigDeleteBlockEnsemble.disconnect() self.sigDeleteSequence.disconnect() self.sigLoadBlockEnsemble.disconnect() self.sigLoadSequence.disconnect() self.sigSampleBlockEnsemble.disconnect() self.sigSampleSequence.disconnect() self.sigClearPulseGenerator.disconnect() self.sigGeneratorSettingsChanged.disconnect() self.sigSamplingSettingsChanged.disconnect() self.sigGeneratePredefinedSequence.disconnect() # Disconnect signals coming from SequenceGeneratorLogic self.sequencegeneratorlogic().sigBlockDictUpdated.disconnect() self.sequencegeneratorlogic().sigEnsembleDictUpdated.disconnect() self.sequencegeneratorlogic().sigSequenceDictUpdated.disconnect() self.sequencegeneratorlogic().sigAvailableWaveformsUpdated.disconnect() self.sequencegeneratorlogic().sigAvailableSequencesUpdated.disconnect() self.sequencegeneratorlogic().sigGeneratorSettingsUpdated.disconnect() self.sequencegeneratorlogic().sigSamplingSettingsUpdated.disconnect() self.sequencegeneratorlogic( ).sigPredefinedSequenceGenerated.disconnect() self.sequencegeneratorlogic().sigSampleEnsembleComplete.disconnect() self.sequencegeneratorlogic().sigSampleSequenceComplete.disconnect() self.sequencegeneratorlogic().sigLoadedAssetUpdated.disconnect() return ####################################################################### ### Pulsed measurement properties ### ####################################################################### @property def fast_counter_constraints(self): return self.pulsedmeasurementlogic().fastcounter_constraints @property def fast_counter_settings(self): return self.pulsedmeasurementlogic().fast_counter_settings @property def ext_microwave_constraints(self): return self.pulsedmeasurementlogic().ext_microwave_constraints @property def ext_microwave_settings(self): return self.pulsedmeasurementlogic().ext_microwave_settings @property def measurement_settings(self): return self.pulsedmeasurementlogic().measurement_settings @property def timer_interval(self): return self.pulsedmeasurementlogic().timer_interval @property def analysis_methods(self): return self.pulsedmeasurementlogic().analysis_methods @property def extraction_methods(self): return self.pulsedmeasurementlogic().extraction_methods @property def analysis_settings(self): return self.pulsedmeasurementlogic().analysis_settings @property def extraction_settings(self): return self.pulsedmeasurementlogic().extraction_settings @property def signal_data(self): return self.pulsedmeasurementlogic().signal_data @property def signal_alt_data(self): return self.pulsedmeasurementlogic().signal_alt_data @property def measurement_error(self): return self.pulsedmeasurementlogic().measurement_error @property def raw_data(self): return self.pulsedmeasurementlogic().raw_data @property def laser_data(self): return self.pulsedmeasurementlogic().laser_data @property def alternative_data_type(self): return self.pulsedmeasurementlogic().alternative_data_type @property def fit_container(self): return self.pulsedmeasurementlogic().fc ####################################################################### ### Pulsed measurement methods ### ####################################################################### @QtCore.Slot(dict) def set_measurement_settings(self, settings_dict=None, **kwargs): """ @param settings_dict: @param kwargs: """ if isinstance(settings_dict, dict): self.sigMeasurementSettingsChanged.emit(settings_dict) else: self.sigMeasurementSettingsChanged.emit(kwargs) return @QtCore.Slot(dict) def set_fast_counter_settings(self, settings_dict=None, **kwargs): """ @param settings_dict: @param kwargs: """ if isinstance(settings_dict, dict): self.sigFastCounterSettingsChanged.emit(settings_dict) else: self.sigFastCounterSettingsChanged.emit(kwargs) return @QtCore.Slot(dict) def set_ext_microwave_settings(self, settings_dict=None, **kwargs): """ @param settings_dict: @param kwargs: """ if isinstance(settings_dict, dict): self.sigExtMicrowaveSettingsChanged.emit(settings_dict) else: self.sigExtMicrowaveSettingsChanged.emit(kwargs) return @QtCore.Slot(dict) def set_analysis_settings(self, settings_dict=None, **kwargs): """ @param settings_dict: @param kwargs: """ if isinstance(settings_dict, dict): self.sigAnalysisSettingsChanged.emit(settings_dict) else: self.sigAnalysisSettingsChanged.emit(kwargs) return @QtCore.Slot(dict) def set_extraction_settings(self, settings_dict=None, **kwargs): """ @param settings_dict: @param kwargs: """ if isinstance(settings_dict, dict): self.sigExtractionSettingsChanged.emit(settings_dict) else: self.sigExtractionSettingsChanged.emit(kwargs) return @QtCore.Slot(int) @QtCore.Slot(float) def set_timer_interval(self, interval): """ @param int|float interval: The timer interval to set in seconds. """ if isinstance(interval, (int, float)): self.sigTimerIntervalChanged.emit(interval) return @QtCore.Slot(str) def set_alternative_data_type(self, alt_data_type): """ @param alt_data_type: @return: """ if isinstance(alt_data_type, str): self.sigAlternativeDataTypeChanged.emit(alt_data_type) return @QtCore.Slot() def manually_pull_data(self): """ """ self.sigManuallyPullData.emit() return @QtCore.Slot(bool) def toggle_ext_microwave(self, switch_on): """ @param switch_on: """ if isinstance(switch_on, bool): self.sigToggleExtMicrowave.emit(switch_on) return @QtCore.Slot(bool) def ext_microwave_running_updated(self, is_running): """ @param is_running: """ if isinstance(is_running, bool): self.status_dict['microwave_running'] = is_running self.sigExtMicrowaveRunningUpdated.emit(is_running) return @QtCore.Slot(bool) def toggle_pulse_generator(self, switch_on): """ @param switch_on: """ if isinstance(switch_on, bool): self.sigTogglePulser.emit(switch_on) return @QtCore.Slot(bool) def pulser_running_updated(self, is_running): """ @param is_running: """ if isinstance(is_running, bool): self.status_dict['pulser_running'] = is_running self.sigPulserRunningUpdated.emit(is_running) return @QtCore.Slot(bool) @QtCore.Slot(bool, str) def toggle_pulsed_measurement(self, start, stash_raw_data_tag=''): """ @param bool start: @param str stash_raw_data_tag: """ if isinstance(start, bool) and isinstance(stash_raw_data_tag, str): self.sigToggleMeasurement.emit(start, stash_raw_data_tag) return @QtCore.Slot(bool) def toggle_pulsed_measurement_pause(self, pause): """ @param pause: """ if isinstance(pause, bool): self.sigToggleMeasurementPause.emit(pause) return @QtCore.Slot(bool, bool) def measurement_status_updated(self, is_running, is_paused): """ @param is_running: @param is_paused: """ if isinstance(is_running, bool) and isinstance(is_paused, bool): self.status_dict['measurement_running'] = is_running self.sigMeasurementStatusUpdated.emit(is_running, is_paused) return @QtCore.Slot(str) def do_fit(self, fit_function): """ @param fit_function: """ if isinstance(fit_function, str): self.status_dict['fitting_busy'] = True self.sigDoFit.emit(fit_function) return @QtCore.Slot(str, np.ndarray, object) def fit_updated(self, fit_name, fit_data, fit_result): """ @return: """ self.status_dict['fitting_busy'] = False self.sigFitUpdated.emit(fit_name, fit_data, fit_result) return def save_measurement_data(self, tag, with_error): """ Prepare data to be saved and create a proper plot of the data. This is just handed over to the measurement logic. @param str tag: a filetag which will be included in the filename @param bool with_error: select whether errors should be saved/plotted """ self.pulsedmeasurementlogic().save_measurement_data(tag, with_error) return ####################################################################### ### Sequence generator properties ### ####################################################################### @property def pulse_generator_constraints(self): return self.sequencegeneratorlogic().pulse_generator_constraints @property def pulse_generator_settings(self): return self.sequencegeneratorlogic().pulse_generator_settings @property def generation_parameters(self): return self.sequencegeneratorlogic().generation_parameters @property def analog_channels(self): return self.sequencegeneratorlogic().analog_channels @property def digital_channels(self): return self.sequencegeneratorlogic().digital_channels @property def saved_pulse_blocks(self): return self.sequencegeneratorlogic().saved_pulse_blocks @property def saved_pulse_block_ensembles(self): return self.sequencegeneratorlogic().saved_pulse_block_ensembles @property def saved_pulse_sequences(self): return self.sequencegeneratorlogic().saved_pulse_sequences @property def sampled_waveforms(self): return self.sequencegeneratorlogic().sampled_waveforms @property def sampled_sequences(self): return self.sequencegeneratorlogic().sampled_sequences @property def loaded_asset(self): return self.sequencegeneratorlogic().loaded_asset @property def generate_methods(self): return self.sequencegeneratorlogic().generate_methods @property def generate_method_params(self): return self.sequencegeneratorlogic().generate_method_params ####################################################################### ### Sequence generator methods ### ####################################################################### @QtCore.Slot() def clear_pulse_generator(self): still_busy = self.status_dict[ 'sampling_ensemble_busy'] or self.status_dict[ 'sampling_sequence_busy'] or self.status_dict[ 'loading_busy'] or self.status_dict['sampload_busy'] if still_busy: self.log.error( 'Can not clear pulse generator. Sampling/Loading still in progress.' ) else: self.sigClearPulseGenerator.emit() return @QtCore.Slot(str) @QtCore.Slot(str, bool) def sample_ensemble(self, ensemble_name, with_load=False): already_busy = self.status_dict[ 'sampling_ensemble_busy'] or self.status_dict[ 'sampling_sequence_busy'] or self.sequencegeneratorlogic( ).module_state() == 'locked' if already_busy: self.log.error( 'Sampling of a different asset already in progress.\n' 'PulseBlockEnsemble "{0}" not sampled!'.format(ensemble_name)) else: if with_load: self.status_dict['sampload_busy'] = True self.status_dict['sampling_ensemble_busy'] = True self.sigSampleBlockEnsemble.emit(ensemble_name) return @QtCore.Slot(object) def sample_ensemble_finished(self, ensemble): self.status_dict['sampling_ensemble_busy'] = False self.sigSampleEnsembleComplete.emit(ensemble) if self.status_dict['sampload_busy'] and not self.status_dict[ 'sampling_sequence_busy']: if ensemble is None: self.status_dict['sampload_busy'] = False self.sigLoadedAssetUpdated.emit(*self.loaded_asset) else: self.load_ensemble(ensemble.name) return @QtCore.Slot(str) @QtCore.Slot(str, bool) def sample_sequence(self, sequence_name, with_load=False): already_busy = self.status_dict[ 'sampling_ensemble_busy'] or self.status_dict[ 'sampling_sequence_busy'] or self.sequencegeneratorlogic( ).module_state() == 'locked' if already_busy: self.log.error( 'Sampling of a different asset already in progress.\n' 'PulseSequence "{0}" not sampled!'.format(sequence_name)) else: if with_load: self.status_dict['sampload_busy'] = True self.status_dict['sampling_sequence_busy'] = True self.sigSampleSequence.emit(sequence_name) return @QtCore.Slot(object) def sample_sequence_finished(self, sequence): self.status_dict['sampling_sequence_busy'] = False self.sigSampleSequenceComplete.emit(sequence) if self.status_dict['sampload_busy']: if sequence is None: self.status_dict['sampload_busy'] = False self.sigLoadedAssetUpdated.emit(*self.loaded_asset) else: self.load_sequence(sequence.name) return @QtCore.Slot(str) def load_ensemble(self, ensemble_name): if self.status_dict['loading_busy']: self.log.error( 'Loading of a different asset already in progress.\n' 'PulseBlockEnsemble "{0}" not loaded!'.format(ensemble_name)) else: self.status_dict['loading_busy'] = True self.sigLoadBlockEnsemble.emit(ensemble_name) return @QtCore.Slot(str) def load_sequence(self, sequence_name): if self.status_dict['loading_busy']: self.log.error( 'Loading of a different asset already in progress.\n' 'PulseSequence "{0}" not loaded!'.format(sequence_name)) else: self.status_dict['loading_busy'] = True self.sigLoadSequence.emit(sequence_name) return @QtCore.Slot(str, str) def loaded_asset_updated(self, asset_name, asset_type): """ @param asset_name: @param asset_type: @return: """ self.status_dict['sampload_busy'] = False self.status_dict['loading_busy'] = False self.sigLoadedAssetUpdated.emit(asset_name, asset_type) # Transfer sequence information from PulseBlockEnsemble or PulseSequence to # PulsedMeasurementLogic to be able to invoke measurement settings from them if not asset_type: # If no asset loaded or asset type unknown, clear sequence_information dict object_instance = None elif asset_type == 'PulseBlockEnsemble': object_instance = self.saved_pulse_block_ensembles.get(asset_name) elif asset_type == 'PulseSequence': object_instance = self.saved_pulse_sequences.get(asset_name) else: object_instance = None if object_instance is None: self.pulsedmeasurementlogic().sampling_information = dict() self.pulsedmeasurementlogic().measurement_information = dict() else: self.pulsedmeasurementlogic( ).sampling_information = object_instance.sampling_information self.pulsedmeasurementlogic( ).measurement_information = object_instance.measurement_information return @QtCore.Slot(object) def save_pulse_block(self, block_instance): """ @param block_instance: @return: """ self.sigSavePulseBlock.emit(block_instance) return @QtCore.Slot(object) def save_block_ensemble(self, ensemble_instance): """ @param ensemble_instance: @return: """ self.sigSaveBlockEnsemble.emit(ensemble_instance) return @QtCore.Slot(object) def save_sequence(self, sequence_instance): """ @param sequence_instance: @return: """ self.sigSaveSequence.emit(sequence_instance) return @QtCore.Slot(str) def delete_pulse_block(self, block_name): """ @param block_name: @return: """ self.sigDeletePulseBlock.emit(block_name) return @QtCore.Slot(str) def delete_block_ensemble(self, ensemble_name): """ @param ensemble_name: @return: """ self.sigDeleteBlockEnsemble.emit(ensemble_name) return @QtCore.Slot(str) def delete_sequence(self, sequence_name): """ @param sequence_name: @return: """ self.sigDeleteSequence.emit(sequence_name) return @QtCore.Slot(dict) def set_pulse_generator_settings(self, settings_dict=None, **kwargs): """ Either accept a settings dictionary as positional argument or keyword arguments. If both are present both are being used by updating the settings_dict with kwargs. The keyword arguments take precedence over the items in settings_dict if there are conflicting names. @param settings_dict: @param kwargs: @return: """ if not isinstance(settings_dict, dict): settings_dict = kwargs else: settings_dict.update(kwargs) self.sigGeneratorSettingsChanged.emit(settings_dict) return @QtCore.Slot(dict) def set_generation_parameters(self, settings_dict=None, **kwargs): """ Either accept a settings dictionary as positional argument or keyword arguments. If both are present both are being used by updating the settings_dict with kwargs. The keyword arguments take precedence over the items in settings_dict if there are conflicting names. @param settings_dict: @param kwargs: @return: """ if not isinstance(settings_dict, dict): settings_dict = kwargs else: settings_dict.update(kwargs) # Force empty gate channel if fast counter is not gated if 'gate_channel' in settings_dict and not self.fast_counter_settings.get( 'is_gated'): settings_dict['gate_channel'] = '' self.sigSamplingSettingsChanged.emit(settings_dict) return @QtCore.Slot(str) @QtCore.Slot(str, dict) def generate_predefined_sequence(self, generator_method_name, kwarg_dict=None): """ @param generator_method_name: @param kwarg_dict: @return: """ if not isinstance(kwarg_dict, dict): kwarg_dict = dict() self.status_dict['predefined_generation_busy'] = True self.sigGeneratePredefinedSequence.emit(generator_method_name, kwarg_dict) return @QtCore.Slot(object) def predefined_sequence_generated(self, generated_name): self.status_dict['predefined_generation_busy'] = False self.sigPredefinedSequenceGenerated.emit(generated_name) return def get_ensemble_info(self, ensemble): """ """ return self.sequencegeneratorlogic().get_ensemble_info( ensemble=ensemble) def get_sequence_info(self, sequence): """ """ return self.sequencegeneratorlogic().get_sequence_info( sequence=sequence)
class PowerStabilizationLogic(GenericLogic): """This is the Interface class to define the controls for the simple microwave hardware. """ _modclass = 'power_stabilization_logic' _modtype = 'logic' # connectors # TiS_camera_hardware = Connector(interface='EmptyInterface') # arduino_hardware = Connector(interface='EmptyInterface') # power_meter_hardware = Connector(interface='EmptyInterface') powermeter1 = Connector(interface='SimpleDataInterface') confocalscanner1 = Connector(interface='ConfocalScannerInterface') sigPowerUpdated = QtCore.Signal() sigPowerDataNext = QtCore.Signal() def on_activate(self): """ Initialisation performed during activation of the module. """ self._powermeter = self.powermeter1() self._daq_card = self.confocalscanner1() #### activate the NI card AO channel self.z_range = self._daq_card.get_position_range()[2] # Initialise the current position of all four scanner channels. self.current_position = self._daq_card.get_scanner_position() # initialise the range for scanning # self.scan_range = [0.0, 1.0] # self.set_scan_range(self.scan_range) self._static_v = 0. # Keep track of the current static voltage even while a scan may cause the real-time # voltage to change. self.goto_voltage(self._static_v) # set power meter self._powermeter.set_wavelength(600.0) self._powermeter.set_autorange(1) # PID self.pid_status = False self.ramp_status = False self.setpoint = 0. self.polarity = 1 self.kp, self.ki, self.kd = 500, 1, 0 self.min_pid_out, self.max_pid_out = 0., 0.4 self.min_volt, self.max_volt = 0., 0.8 self.ramping_factor = 0.01 self.offset = 0 self.error_p_prev = 0. self.power_prev = 0. self.error_p = 0 self.error_i = 0 self.error_d = 0 self.output = 0 self.time_loop = [] self.power = None self.error = None # Thread self.threadlock = Mutex() self.sigPowerDataNext.connect(self.set_power, QtCore.Qt.QueuedConnection) return def on_deactivate(self): """ Performed during deactivation of the module. """ # self._TiS_camera_hardware.on_deactivate() # self.set_power(0) # self._arduino_hardware.on_deactivate() self.sigPowerDataNext.disconnect() self.goto_voltage(1) return #### NI card scanner @QtCore.Slot(float) def goto_voltage(self, volts=None): """Forwarding the desired output voltage to the scanning ♣. @param float volts: desired voltage (volts) @return int: error code (0:OK, -1:error) """ # print(tag, x, y, z) # Changes the respective value if volts is not None: self._static_v = volts ch_array = ['z'] pos_array = [self._static_v] pos_dict = {} pos_dict[ch_array[0]] = pos_array[0] self._daq_card.scanner_set_position(**pos_dict) def set_voltage(self, volts): """ Set the channel idle voltage """ self._static_v = np.clip(volts, self.z_range[0], self.z_range[1]) self.goto_voltage(self._static_v) def get_current_voltage(self): """returns current voltage of hardware device(atm NIDAQ 3rd output)""" return self._daq_card.get_scanner_position()[2] #### PID controler def set_setpoint(self, setpoint): """ Set the power setpoint of the laser. """ self.setpoint = setpoint return def get_setpoint(self): """ Get the power setpoint of the laser. """ return self.setpoint def set_pid_status(self, boolean): """ Set the PID to True or False. """ self.pid_status = boolean return def is_pid_status(self): """ Get the status of the PID (boolean). """ return self.pid_status def set_kp(self, value): """ Set the proportional term of the PID. """ self.kp = value return def set_ki(self, value): """ Set the integral term of the PID. """ self.ki = value return def set_kd(self, value): """ Set the derivative term of the PID. """ self.kd = value return def clear_integral(self): """ Clear the integral term of the PID. """ self.error_i = 0 return def set_ramp_status(self, boolean): """ Set the ramp status (True ot False). Used in order to increase the changes of laser power""" self.ramp_status = boolean return def set_power(self): """Set the voltage with or without PID""" # if not self.laser_on: # # Switch console from remote to local mode and exit # self._power_meter_hardware.power_meter._inst.control_ren(6) # return # measure power and the time self.power = self.get_power() self.time_loop.append(time.time()) # We delete the useless data in order to not saturate the memory if len(self.time_loop) > 2: del self.time_loop[0] if self.time_loop[-1] == self.time_loop[-2]: # If the time is the same for two loops then we call the function again pass else: # We update the power on the GUI self.sigPowerUpdated.emit() # get error self.error = self.get_setpoint() - self.power #### The ramp is there to speed up the process to fo the the setpoint value #### At the moment, the use of the ramp is disable by setting a high value #### TODO, delete the ramp which is more a source of trouble than anything if self.ramp_status: if abs(self.error) > 10e-7: self.offset = self.output self.output += np.sign(self.error) * 1e-4 #### set the output value self.goto_voltage(self.output) else: self.ramp_status = False self.clear_integral() self.pid_status = True elif self.pid_status: delta_t = self.time_loop[-1] - self.time_loop[-2] self.error_p = self.error self.error_i += self.error * delta_t self.error_d = (self.error - self.error_p_prev) / delta_t p = self.kp * self.error_p i = self.ki * self.error_i d = self.kd * self.error_d pid_out = self.polarity * (p + i + d / 100) #### ramp up/down until setpoint is reached? correction = self.offset + pid_out k_factor = 1e3 if correction >= self.max_pid_out: self.output = self.max_pid_out elif self.get_current_voltage( ) + correction * k_factor <= self.min_pid_out: self.output = self.min_pid_out else: self.output = correction err = self.output - self.power_prev #### What to do with the correction, is it the right way the correction signal? Medidate on that if abs(err) > 1e-10: if self.get_current_voltage( ) + correction * k_factor >= self.max_volt: self.goto_voltage(self.get_current_voltage()) print("maximum voltage reached") elif self.get_current_voltage( ) + correction * k_factor <= self.min_volt: self.goto_voltage(self.get_current_voltage()) print("minimum voltage reached") else: self.goto_voltage(self.get_current_voltage() + correction * k_factor) self.power_prev = self.output self.error_p_prev = self.error_p else: self.goto_voltage(self._static_v) self.sigPowerDataNext.emit() return #### Power Meter def get_power(self): """ Get the optical power measured by the power meter. """ return self._powermeter.get_power() def get_wavelength(self): """ Get the wavelength value of the PM100 powermeter """ return self._powermeter.get_wavelength() def set_wavelength(self, wavelength): """ Set the wavelength value to the PM100 powermeter """ self._powermeter.set_wavelength(wavelength)
class SpectrometerInterfaceDummy(Base, SpectrometerInterface): """ Dummy spectrometer module. Shows a silicon vacancy spectrum at liquid helium temperatures. """ fitlogic = Connector(interface='FitLogic') def on_activate(self): """ Activate module. """ self._fitLogic = self.fitlogic() self.exposure = 0.1 def on_deactivate(self): """ Deactivate module. """ pass def recordSpectrum(self): """ Record a dummy spectrum. @return ndarray: 1024-value ndarray containing wavelength and intensity of simulated spectrum """ length = 1024 data = np.empty((2, length), dtype=np.double) data[0] = np.arange(730, 750, 20 / length) data[1] = np.random.uniform(0, 2000, length) lorentz, params = self._fitLogic.make_multiplelorentzian_model( no_of_functions=4) sigma = 0.05 params.add('l0_amplitude', value=2000) params.add('l0_center', value=736.46) params.add('l0_sigma', value=1.5 * sigma) params.add('l1_amplitude', value=5800) params.add('l1_center', value=736.545) params.add('l1_sigma', value=sigma) params.add('l2_amplitude', value=7500) params.add('l2_center', value=736.923) params.add('l2_sigma', value=sigma) params.add('l3_amplitude', value=1000) params.add('l3_center', value=736.99) params.add('l3_sigma', value=1.5 * sigma) params.add('offset', value=50000.) data[1] += lorentz.eval(x=data[0], params=params) time.sleep(self.exposure) return data def saveSpectrum(self, path, postfix=''): """ Dummy save function. @param str path: path of saved spectrum @param str postfix: postfix of saved spectrum file """ timestr = strftime("%Y%m%d-%H%M-%S_", localtime()) print('Dummy would save to: ' + str(path) + timestr + str(postfix) + ".spe") def getExposure(self): """ Get exposure time. @return float: exposure time """ return self.exposure def setExposure(self, exposureTime): """ Set exposure time. @param float exposureTime: exposure time """ self.exposure = exposureTime
class HbtLogic(GenericLogic): """ This is the logic for running HBT experiments """ _modclass = 'hbtlogic' _modtype = 'logic' _channel_apd_0 = ConfigOption('timetagger_channel_apd_0', missing='error') _channel_apd_1 = ConfigOption('timetagger_channel_apd_1', missing='error') _bin_width = ConfigOption('bin_width', 500, missing='info') _n_bins = ConfigOption('bins', 2000, missing='info') savelogic = Connector(interface='SaveLogic') hbt_updated = QtCore.Signal() hbt_fit_updated = QtCore.Signal() hbt_saved = QtCore.Signal() sigStart = QtCore.Signal() sigStop = QtCore.Signal() def __init__(self, config, **kwargs): super().__init__(config=config, **kwargs) self.fit_times = [] self.bin_times = [] self.fit_g2 = [] self.g2_data = [] self.g2_data_normalised = [] self.hbt_available = False self._setup_measurement() self._close_measurement() def on_activate(self): """ Connect and configure the access to the FPGA. """ self._save_logic = self.get_connector('savelogic') self._number_of_gates = int(100) self.g2_data = np.zeros_like(self.bin_times) self.g2_data_normalised = np.zeros_like(self.bin_times) self.fit_times = self.bin_times self.fit_g2 = np.zeros_like(self.fit_times) self.timer = QtCore.QTimer() self.timer.timeout.connect(self.update) self.sigStart.connect(self._start_hbt) self.sigStop.connect(self._stop_hbt) def _setup_measurement(self): self._tagger = tt.createTimeTagger() self.coin = tt.Correlation(self._tagger, self._channel_apd_0, self._channel_apd_1, binwidth=self._bin_width, n_bins=self._n_bins) self.bin_times = self.coin.getIndex() def _close_measurement(self): self.coin.stop() self.coin = None self._tagger = None def start_hbt(self): self.sigStart.emit() def stop_hbt(self): self.sigStop.emit() def _start_hbt(self): self._setup_measurement() self.coin.clear() self.coin.start() self.timer.start(500) # 0.5s def update(self): self.bin_times = self.coin.getIndex() self.g2_data = self.coin.getData() self.hbt_available = True lvl = np.mean(self.g2_data[0:100]) if lvl > 0: self.g2_data_normalised = self.g2_data / lvl else: self.g2_data_normalised = np.zeros_like(self.g2_data) self.hbt_updated.emit() def pause_hbt(self): if self.coin is not None: self.coin.stop() def continue_hbt(self): if self.coin is not None: self.coin.start() def _stop_hbt(self): if self.coin is not None: self._close_measurement() self.timer.stop() def fit_data(self): pass # model, param = self.fitlogic.make_hyperbolicsaturation_model() # param['I_sat'].min = 0 # param['I_sat'].max = 1e7 # param['I_sat'].value = max(self.psat_data) * .7 # param['P_sat'].max = 100.0 # param['P_sat'].min = 0.0 # param['P_sat'].value = 1.0 # param['slope'].min = 0.0 # param['slope'].value = 1e3 # param['offset'].min = 0.0 # fit = self.fitlogic.make_hyperbolicsaturation_fit(x_axis=self.psat_powers, data=self.psat_data, # estimator=self.fitlogic.estimate_hyperbolicsaturation, # add_params=param) # self.fit = fit # self.fitted_Psat = fit.best_values['P_sat'] # self.fitted_Isat = fit.best_values['I_sat'] def save_hbt(self): # File path and name filepath = self._save_logic.get_path_for_module(module_name='HBT') # We will fill the data OrderedDict to send to savelogic data = OrderedDict() data['Time (ns)'] = np.array(self.bin_times) data['g2(t)'] = np.array(self.g2_data) data['g2(t) normalised'] = np.array(self.g2_data_normalised) self._save_logic.save_data(data, filepath=filepath, filelabel='g2data', fmt=['%.6e', '%.6e', '%.6e']) self.log.debug('HBT data saved to:\n{0}'.format(filepath)) self.hbt_saved.emit() return 0 def on_deactivate(self): """ Reverse steps of activation @return int: error code (0:OK, -1:error) """ return 0
class CavityLogic(GenericLogic): """ This is the Logic class for cavity scanning. """ _modclass = 'confocallogic' _modtype = 'logic' # declare connectors nicard = Connector(interface='ConfocalScannerInterface') scope = Connector(interface='scopeinterface') savelogic = Connector(interface='SaveLogic') sigFullSweepPlotUpdated = QtCore.Signal(np.ndarray, np.ndarray) sigLinewidthPlotUpdated = QtCore.Signal(np.ndarray, np.ndarray) sigResonancesUpdated = QtCore.Signal(np.ndarray) sigSweepNumberUpdated = QtCore.Signal(int) sigTargetModeNumberUpdated = QtCore.Signal(int) def __init__(self, config, **kwargs): super().__init__(config=config, **kwargs) #locking for thread safety self.threadlock = Mutex() self._full_sweep_freq = 2 / 3 self.RampUp_time = np.linspace(0, 1, 100) self.RampUp_signalR = np.linspace(1, 1, 100) self._full_sweep_start = 0.0 self._full_sweep_stop = -3.75 self._acqusition_time = 2.0 self.reflection_channel = 0 self.ramp_channel = 1 self.position_channel = 3 self.velocity_channel = 2 self.SG_scale = 10 # V self.lamb = 637e-9 # wavelenght self.current_mode_number = 10 self.current_sweep_number = 1 self.first_sweep = None self.first_corrected_resonances = None self.last_sweep = None self.last_corrected_resonances = None self.mode_shift_list = [0] self._current_filepath = r'C:\BittorrentSyncDrive\Personal - Rasmus\Rasmus notes\Measurements\test' def on_activate(self): """ Initialisation performed during activation of the module. """ self._ni = self.get_connector('nicard') self._scope = self.get_connector('scope') self._save_logic = self.get_connector('savelogic') self.cavity_range = self._ni._cavity_position_range[ 1] - self._ni._cavity_position_range[0] def on_deactivate(self): """ Reverse steps of activation @return int: error code (0:OK, -1:error) """ # ############################################ DATA AQUISITION START ######################################### def _trim_data(self, times, volts): ''' Trims data to the ramp :return: ''' total_trace = times[-1] - times[0] # sec ramp_period = 1.0 / self._full_sweep_freq # sec period_index = len(times) * ramp_period / total_trace ramp_mid = np.argmin(volts[self.ramp_channel]) low_index = ramp_mid - int(period_index / 2) high_index = ramp_mid + int(period_index / 2) volts_trim = volts[:, low_index:high_index + 1] time_trim = times[low_index:high_index + 1] return time_trim, volts_trim def _data_split_up(self): [self.RampUp_time, self.RampDown_time] = np.array_split(self.time_trim, 2) [self.RampUp_signalR, self.RampDown_signalR ] = np.array_split(self.volts_trim[self.reflection_channel], 2) [self.RampUp_signalNI, self.RampDown_signalNI ] = np.array_split(self.volts_trim[self.ramp_channel], 2) [self.RampUp_signalSG, self.RampDown_signalSG ] = np.array_split(self.volts_trim[self.position_channel], 2) [self.RampUp_signalSG_v, self.RampDown_signalSG_v ] = np.array_split(self.volts_trim[self.velocity_channel], 2) return 0 def _get_ramp_up_signgals(self): self.time_trim, self.volts_trim = self._trim_data( self.time, self.volts) self._data_split_up() return 0 # ############################################ DATA AQUSITION END ########################################## # ############################################ FITTING START ############################################### def _polyfit_SG(self, xdata, ydata, order=3, plot=False): xdata_trim = xdata[::9] ydata_trim = ydata[::9] p_fit = np.poly1d(np.polyfit(xdata_trim, ydata_trim, order)) if plot is True: plt.plot(xdata, ydata, '-', xdata, p_fit(xdata), '--') plt.show() return p_fit(xdata) def _fit_ramp(self, xdata, ydata): # Fitting setup parameter_guess = [ self._full_sweep_start, self._full_sweep_stop, self._full_sweep_freq, self.time_trim[0] ] func = self._ni.sweep_function # Actual fitting popt, pcov = curve_fit(func, xdata, ydata, parameter_guess) return popt # ############################################ FITTING END ############################################## # ################################## SAVE AND LOAD DATA START ########################################################## def _load_full_sweep(self, filepath=None, filename=None): """ Loads data from full sweep :param filepath: :param filename: :return: """ delimiter = '\t' if filepath is None: filepath = self._current_filepath if filename is None: filename = self._current_filename with open(os.path.join(filepath, filename), 'rb') as file: data = np.loadtxt(file, delimiter=delimiter) self.time = data[:, 0].transpose() self.volts = data[:, 1:5].transpose() def _save_raw_data(self, label=''): date = datetime.datetime.fromtimestamp( time()).strftime('%Y-%m-%d_%H%M%S') self._current_filename = date + label + '_full_sweep_data.dat' data = np.vstack([self.time, self.volts]) fmt = ['%.8e', '%.3e', '%.3e', '%.3e', '%.3e'] header = '' delimiter = '\t' comments = '#' with open(os.path.join(self._current_filepath, self._current_filename), 'wb') as file: np.savetxt(file, data.transpose(), fmt=fmt, delimiter=delimiter, header=header, comments=comments) def _save_linewidth_data(self, label, data): date = datetime.datetime.fromtimestamp( time()).strftime('%Y-%m-%d_%H%M%S') self._current_filename = date + label + '_linewidth_data.dat' fmt = ['%.8e'] for i in range(np.shape(data)[0] - 1): fmt.append('%.3e') header = '' delimiter = '\t' comments = '#' with open(os.path.join(self._current_filepath, self._current_filename), 'wb') as file: np.savetxt(file, data.transpose(), fmt=fmt, delimiter=delimiter, header=header, comments=comments) # ############################# Save and load data end ############################################################# # ################################### FULL SWEEPS START ################################################## def _get_scope_data(self): """ Get scope data for all four channels This is loaded into self.volts and self.time :return: """ times, volts = self._scope.aquire_data() volts = volts.reshape(4, int(len(volts) / 4)) times = times.reshape(4, int(len(times) / 4)) time = times[0] # First point hare not good! Scope outputs 0 or maybe a header self.volts = volts[:, 2000:] self.time = time[2000:] def setup_scope_for_full_sweep(self): self._scope.set_record_lenght(linewidth=False) self._scope.set_acquisition_time(self._acqusition_time) self._scope.set_data_composition_to_env() # HARD CODED!!!!! self._scope.set_vertical_scale(2, 500E-3) self._scope.set_vertical_position(2, 3500E-3) self._scope.set_vertical_scale(3.0, 5E-3) self._scope.set_vertical_position(3.0, 0) self._scope.set_vertical_scale(4.0, 2) self._scope.set_vertical_position(4.0, -2.5) sleep(1) def start_full_sweep(self): """ Starts a single full sweep 1. set up the scope for a full sweep 2. sets up a single ramp 3. executes the sweep 4. get_data 4. closed sweep :return: """ self._ni.cavity_set_voltage(0.0) sleep(1.0) # Set up scope for full sweep self.setup_scope_for_full_sweep() # set up ni card for full sweep # One full sweep RepOfSweep = 1 self._ni.set_up_sweep(self._full_sweep_start, self._full_sweep_stop, self._full_sweep_freq, RepOfSweep) # start sweep self._scope.run_single() # HARD CODED!!!!! sleep(0.5) self._ni.start_sweep() # stop sweep # HARD CODED!!!!! sleep(self._acqusition_time) self._ni.close_sweep() self._get_scope_data() return 0 def get_nth_full_sweep(self, sweep_number=None, save=True): """ :param sweep_number: :return: """ if sweep_number is None: sweep_number = self.current_sweep_number if sweep_number > 1: self.last_sweep = self.RampUp_signalR self.last_corrected_resonances = self.current_resonances try_num = 1 while True: try: self.start_full_sweep() self._get_ramp_up_signgals() self.RampUp_signalSG_polyfit = self._polyfit_SG( xdata=self.RampUp_time, ydata=self.RampUp_signalSG, order=3, plot=False) resonances = self._peak_search(self.RampUp_signalR) corrected_resonances = self._find_missing_resonances( resonances) if len(resonances) < 3: continue else: break except: # Did not get the full sweep if try_num < 3: try_num += 1 continue else: return -1 if sweep_number == 1: self.first_sweep = self.RampUp_signalR self.first_corrected_resonances = corrected_resonances self.first_RampUp_signalSG_polyfit = self.RampUp_signalSG_polyfit plt.plot(self.first_RampUp_signalSG_polyfit, self.first_sweep) plt.plot(self.first_RampUp_signalSG_polyfit[corrected_resonances], self.first_sweep[corrected_resonances], 'o', color='r') plt.grid() plt.show() self.current_sweep_number += 1 self.current_resonances = corrected_resonances self.sigFullSweepPlotUpdated.emit(self.RampUp_time, self.RampUp_signalR) self.sigResonancesUpdated.emit(self.current_resonances) if save is True: self._save_raw_data(label='_{}'.format(sweep_number)) return 0 # #################################### FULL SWEEPS STOP ################################################## # #################################### TARGET MODE START ################################################### def find_phase_difference(self, signal_a, signal_b, show=False): # regularize datasets by subtracting mean and dividing by s.d. mod_signal_a = signal_a - signal_a.mean() mod_signal_a = -mod_signal_a / mod_signal_a.std() mod_signal_b = signal_b - signal_b.mean() mod_signal_b = -mod_signal_b / mod_signal_b.std() # Calculate cross correlation function https://en.wikipedia.org/wiki/Cross-correlation xcorr = np.correlate(mod_signal_a, mod_signal_b, 'same') nsamples = mod_signal_a.size dt = np.arange(-nsamples / 2, nsamples / 2, dtype=int) # Add penalty penalty = -0.25 * np.max(xcorr) * np.abs(dt) xcorr = xcorr + penalty mode_delay = dt[xcorr.argmax()] if np.abs(mode_delay) > 3: return None if show is True: plt.plot(dt, xcorr) plt.grid() plt.savefig( self._current_filepath + r'\correlation{}.png'.format(self.current_sweep_number), dpi=200) plt.show() return int(mode_delay) def get_target_mode(self, resonances, low_mode=None, high_mode=None, plot=False): # Find phase difference if low_mode is None: low_mode = 0 if high_mode is None: high_mode = np.min( [len(self.last_corrected_resonances), len(resonances)]) if self.last_sweep is None: self.last_sweep = self.first_sweep self.last_corrected_resonances = self.first_corrected_resonances #mode_shift = self.find_phase_difference(self.last_sweep[self.last_corrected_resonances[low_mode:high_mode]], # self.RampUp_signalR[resonances[low_mode:high_mode]], show=True) closet_old_mode = np.argmin( np.abs(self.target_position - self.RampUp_signalSG_polyfit[resonances])) #if mode_shift == None: # return None # store mode shifts #self.mode_shift_list.append(mode_shift+self.mode_shift_list[-1]) # Find closets mode target_mode = closet_old_mode - 1 if plot is True: index = self.first_corrected_resonances[self.current_mode_number] new_index = resonances[target_mode] plt.plot(self.first_RampUp_signalSG_polyfit, self.first_sweep) plt.plot(self.first_RampUp_signalSG_polyfit[index], self.first_sweep[index], 'o', markersize=10, color='r') plt.plot(self.RampUp_signalSG_polyfit, self.RampUp_signalR) plt.plot(self.RampUp_signalSG_polyfit[new_index], self.RampUp_signalR[new_index], 'x', markersize=20, color='r') plt.grid() plt.savefig( self._current_filepath + r'\Target_mode_plot_{}.png'.format(self.current_sweep_number), dpi=200) plt.show() return target_mode # #################################### TARGET MODE END ################################################### # ############################# LINEWIDTH MEASUREMENT #################################################### def read_position_from_strain_gauge(self): """ This read the strain gauge voltage from the ni card :return: """ rawdata = self._ni.read_position() position = np.average(rawdata) return position def _move_closer_to_resonance(self, current_offset, position_error): """ :param current_offset: :param position_error: :return: """ # Approximate correction response = ( -3.75 / 20 ) * 2 # (expansion of pzt) V/um / (position in volt) 20 um/ 10 V = 2.0 correction = -response * position_error new_offset = current_offset + correction self._ni.cavity_set_voltage(new_offset) return new_offset def _find_resonance_position_from_strain_gauge(self, current_offset, target_position, threshold_pos): """ :return: offset for mode """ self.log.info('Target position = {:0.3f}'.format(2 * target_position)) i = 0 while i < 10: self._ni.cavity_set_voltage(current_offset) sleep(3.0) try: position_in_volt = self.read_position_from_strain_gauge() position_error = position_in_volt - target_position self.log.info( 'Current position = {:0.3f}, Distance from target {:0.3f}'. format(2 * position_in_volt, position_error * 2)) if np.abs(position_error ) < threshold_pos / 2.0: # Convert from volt to nm break else: current_offset = self._move_closer_to_resonance( current_offset, position_error) i += 1 continue except: self.log.error('could not find resonance position') if i > 10: self.log.warning('Did not find a position') return current_offset def setup_scope_for_linewidth(self, trigger_level, acquisition_time): """ :param trigger_level: :param acquisition_time: :param position: :param scale: :return: """ # Adjust ramp channel: self._scope.set_data_composition_to_yt() self._scope.set_acquisition_time(acquisition_time) self._scope.set_record_lenght(linewidth=True) self._scope.set_egde_trigger(channel=1, level=trigger_level) # FIXME: Adjust position and velocity # self._scope.set_vertical_scale(channel=4, scale=1.0) def _linewidth_get_data(self): """ Get data from scope :return: """ linewidth_times, self.linewidth_volts = self._scope.aquire_data() linewidth_times = linewidth_times.reshape( 4, int(len(linewidth_times) / 4)) self.linewidth_time = linewidth_times[0] def linewidth_measurement(self, modes, target_mode, repeat, freq=40): """ 1. sets up scope for linewidth measurement 2. start a ramp around the target mode 3. gets data if triggered 4. closes ramp and saves data :param modes: List of NI_card voltages for each resonances :param target_mode: :param repeat: number of linewidth measurements :param freq: :return: """ # Setup scope for linewidth measurements with trigger on ramp signal contrast = np.abs(self.RampUp_signalR.min() - np.median(self.RampUp_signalR)) trigger_level = np.median(self.RampUp_signalR) - contrast self.setup_scope_for_linewidth(trigger_level=trigger_level, acquisition_time=40e-6) # start continues ramp #Two first cases are for end modes # FIXME: End modes still does not work because of bound of 0, -3.75 if target_mode >= np.size(modes): amplitude = 2 * abs(modes[target_mode - 1] - modes[target_mode]) / 2.0 elif target_mode == 0: amplitude = 2 * abs(modes[target_mode] - modes[target_mode + 1]) / 2.0 else: amplitude = abs(modes[target_mode - 1] - modes[target_mode + 1]) / 2.0 self.linewidth_time_list = np.array([]) self.linewidth_volts_list = np.array([]) self.target_position = self.RampUp_signalSG_polyfit[ self.current_resonances[target_mode]] # Get data from scope i = 0 while i < repeat: k = 1.0 # Refind correct position offset = self._find_resonance_position_from_strain_gauge( current_offset=modes[target_mode], target_position=self.RampUp_signalSG_polyfit[ self.current_resonances[target_mode]], threshold_pos=0.025) # 25 nm # Start new ramp self._ni.set_up_ramp_output(amplitude, offset, freq) self._ni.start_ramp() trigger_level = np.median(self.RampUp_signalR) - k * contrast self._scope.set_egde_trigger(channel=1, level=trigger_level) self._scope.run_single() while True: if k < 0.2: print('did not find resonance {} {}'.format( self.current_mode_number, i)) self.linewidth_time = np.zeros_like(self.linewidth_time) self.linewidth_volts = np.zeros_like(self.linewidth_volts) ret_str = 0 break # if triggered then get data self._scope.scope.write('*OPC?') sleep(0.1) try: ret_str = self._scope.scope.read() break except: k -= 0.02 trigger_level = np.median( self.RampUp_signalR) - k * contrast self._scope.set_egde_trigger(channel=1, level=trigger_level) continue if ret_str == r'1': self._linewidth_get_data() i += 1 self.linewidth_time_list = np.concatenate( [self.linewidth_time_list, self.linewidth_time]) self.linewidth_volts_list = np.concatenate( [self.linewidth_volts_list, self.linewidth_volts]) self.sigLinewidthPlotUpdated.emit( self.linewidth_time, self.linewidth_volts[0:int(len(self.linewidth_volts) / 4)]) # Make sure we are still at the right position self._ni.stop_ramp() self._ni.close_ramp() #Update plot in gui data = self.linewidth_volts_list data = data.reshape(4 * repeat, int(len(data) / (4 * repeat))) data = np.vstack([self.linewidth_time, data]) # close ramp self._ni.cavity_set_position(20.0e-6) self._save_linewidth_data(label='_{}'.format(self.current_mode_number), data=data) return 0 # ############################### PEAK DETECTION START ################################################# def _detect_peaks(self, x, y=None, mph=None, mpd=1, threshold=0, edge='rising', kpsh=False, valley=False, show=False, ax=None): """ Detect peaks in data based on their amplitude and other features. Parameters ---------- x : 1D array_like data. mph : {None, number}, optional (default = None) detect peaks that are greater than minimum peak height. mpd : positive integer, optional (default = 1) detect peaks that are at least separated by minimum peak distance (in number of data). threshold : positive number, optional (default = 0) detect peaks (valleys) that are greater (smaller) than `threshold` in relation to their immediate neighbors. edge : {None, 'rising', 'falling', 'both'}, optional (default = 'rising') for a flat peak, keep only the rising edge ('rising'), only the falling edge ('falling'), both edges ('both'), or don't detect a flat peak (None). kpsh : bool, optional (default = False) keep peaks with same height even if they are closer than `mpd`. valley : bool, optional (default = False) if True (1), detect valleys (local minima) instead of peaks. show : bool, optional (default = False) if True (1), plot data in matplotlib figure. ax : a matplotlib.axes.Axes instance, optional (default = None). Returns ------- ind : 1D array_like indeces of the peaks in `x`. Notes ----- The detection of valleys instead of peaks is performed internally by simply negating the data: `ind_valleys = detect_peaks(-x)` The function can handle NaN's See this IPython Notebook [1]_. References ---------- .. [1] http://nbviewer.ipython.org/github/demotu/BMC/blob/master/notebooks/DetectPeaks.ipynb """ x = np.atleast_1d(x).astype('float64') if x.size < 3: return np.array([], dtype=int) if valley: x = -x # find indices of all peaks dx = x[1:] - x[:-1] # handle NaN's indnan = np.where(np.isnan(x))[0] if indnan.size: x[indnan] = np.inf dx[np.where(np.isnan(dx))[0]] = np.inf ine, ire, ife = np.array([[], [], []], dtype=int) if not edge: ine = np.where((np.hstack((dx, 0)) < 0) & (np.hstack((0, dx)) > 0))[0] else: if edge.lower() in ['rising', 'both']: ire = np.where((np.hstack((dx, 0)) <= 0) & (np.hstack((0, dx)) > 0))[0] if edge.lower() in ['falling', 'both']: ife = np.where((np.hstack((dx, 0)) < 0) & (np.hstack((0, dx)) >= 0))[0] ind = np.unique(np.hstack((ine, ire, ife))) # handle NaN's if ind.size and indnan.size: # NaN's and values close to NaN's cannot be peaks ind = ind[np.in1d(ind, np.unique( np.hstack((indnan, indnan - 1, indnan + 1))), invert=True)] # first and last values of x cannot be peaks if ind.size and ind[0] == 0: ind = ind[1:] if ind.size and ind[-1] == x.size - 1: ind = ind[:-1] # remove peaks < minimum peak height if ind.size and mph is not None: ind = ind[x[ind] >= mph] # remove peaks - neighbors < threshold if ind.size and threshold > 0: dx = np.min(np.vstack([x[ind] - x[ind - 1], x[ind] - x[ind + 1]]), axis=0) ind = np.delete(ind, np.where(dx < threshold)[0]) # detect small peaks closer than minimum peak distance if ind.size and mpd > 0: ind = ind[np.argsort(x[ind])][::-1] # sort ind by peak height idel = np.zeros(ind.size, dtype=bool) for i in range(ind.size): if not idel[i]: # keep peaks with the same height if kpsh is True if y is not None: idel = idel | (y[ind] >= y[ind[i]] - mpd) & (y[ind] <= y[ind[i]] + mpd) \ & (x[ind[i]] > x[ind] if kpsh else True) else: idel = idel | (ind >= ind[i] - mpd) & (ind <= ind[i] + mpd) \ & (x[ind[i]] > x[ind] if kpsh else True) idel[i] = 0 # Keep current peak # remove the small peaks and sort back the indices by their occurrence ind = np.sort(ind[~idel]) if show: if indnan.size: x[indnan] = np.nan if valley: x = -x self._plot(x, mph, mpd, threshold, edge, valley, ax, ind) return ind def _plot(self, x, mph, mpd, threshold, edge, valley, ax, ind): """Plot results of the detect_peaks function, see its help.""" try: import matplotlib.pyplot as plt except ImportError: print('matplotlib is not available.') else: if ax is None: _, ax = plt.subplots(1, 1, figsize=(8, 4)) ax.plot(x, 'b', lw=1) if ind.size: label = 'valley' if valley else 'peak' label = label + 's' if ind.size > 1 else label ax.plot(ind, x[ind], '+', mfc=None, mec='r', mew=2, ms=8, label='%d %s' % (ind.size, label)) ax.legend(loc='best', framealpha=.5, numpoints=1) ax.set_xlim(-.02 * x.size, x.size * 1.02 - 1) ymin, ymax = x[np.isfinite(x)].min(), x[np.isfinite(x)].max() yrange = ymax - ymin if ymax > ymin else 1 ax.set_ylim(ymin - 0.1 * yrange, ymax + 0.1 * yrange) ax.set_xlabel('Data #', fontsize=14) ax.set_ylabel('Amplitude', fontsize=14) mode = 'Valley detection' if valley else 'Peak detection' ax.set_title("%s (mph=%s, mpd=%d, threshold=%s, edge='%s')" % (mode, str(mph), mpd, str(threshold), edge)) # plt.grid() plt.show() def _check_for_outliers(self, peaks, outlier_cutoff=1.5): """ Finds the distances between between resonaces and locates where the is a missing resoances. The is when the distances is larger than 1.5 fsr. :param peaks: list with resonances :param outlier_cutoff: the distance in the units of fsr :return: """ # Expected fsr in voltage one_fsr = self.SG_scale / self.cavity_range * (self.lamb / 2.0 ) # in Volt # Distance between resonances delta_peaks = self.RampUp_signalSG_polyfit[ peaks[1:]] - self.RampUp_signalSG_polyfit[peaks[:-1]] # Find where the distance between resonances is to large compared to the cutoff outliers = np.where(delta_peaks > outlier_cutoff * one_fsr)[0] if outliers.size > 0: return outliers else: return np.array([]) def _find_missing_resonances(self, resonances, outlier_cutoff=1.5): """ Inserts a index for a missing resonance if there is more that 1.5 fsr between two resonances :param resonances: :param outlier_cutoff: :return: """ corrected_resonances = resonances i = 0 while i < int(1 / 4 * len(resonances)): outliers = self._check_for_outliers(resonances, outlier_cutoff) i += 1 if len(outliers) > 0: outlier = outliers[0] delta_peaks = self.RampUp_signalSG_polyfit[resonances[ 1:]] - self.RampUp_signalSG_polyfit[resonances[:-1]] value = self.RampUp_signalSG_polyfit[ resonances[outlier]] + np.median(delta_peaks) # insert new peak in new corrected array corrected_resonances = np.insert( corrected_resonances, outlier + 1, np.abs(self.RampUp_signalSG_polyfit - value).argmin()) else: # found no new peaks break return corrected_resonances def _peak_search(self, signal, outlier_cutoff=1.5, show=False): """ This uses the function peak detect with a few different parameters to find the parameter with gives the least amount of outliers (in terms of distance between resonances) :param signal: :param outlier_cutoff: :param show: :return: """ # minimum peak height mph = -(signal.mean() - np.abs(signal.max() - signal.mean())) # minimum peak distance one_fsr = self.SG_scale / self.cavity_range * (self.lamb / 2.0 ) # in Volt MaxNumPeak = int( (self.RampUp_signalSG.max() - self.RampUp_signalSG.min()) / one_fsr) + 10 contrast = np.abs(signal.min() - signal.mean()) errors = [0.75, 0.8, 0.85, 0.9, 0.95] constants = np.linspace(0.0, 0.1, 10) OutlierList = [] ErrorList = [] ConstantList = [] for error, constant in product(errors, constants): # Search for the parameters with least outliers mpd = error * one_fsr threshold = constant * contrast resonances = self._detect_peaks(signal, y=self.RampUp_signalSG_polyfit, mph=mph, mpd=mpd, threshold=threshold, valley=True, show=False) outliers = self._check_for_outliers(resonances, outlier_cutoff=outlier_cutoff) # Check to see if there is too many resonances if len(resonances) < MaxNumPeak: OutlierList.append(len(outliers)) ErrorList.append(error) ConstantList.append(constant) #Optimal parameters for peak search OptimalError = ErrorList[np.argmin(OutlierList)] OptimalConstant = ConstantList[np.argmin(OutlierList)] mpd = OptimalError * one_fsr threshold = OptimalConstant * contrast resonances = self._detect_peaks(signal, y=self.RampUp_signalSG_polyfit, mph=mph, mpd=mpd, threshold=threshold, valley=True, show=show) return resonances # ############################################# PEAK DETECTION End ###################################################### def get_hw_constraints(self): """ Return the names of all ocnfigured fit functions. @return object: Hardware constraints object """ # FIXME: Should be from hardware constraints_dict = { 'min_position': 0, 'max_position': 20e-6, 'min_speed': 0, 'max_speed': 100, 'min_temperature': -100, 'max_temperature': 30, 'min_averages': 1, 'max_averages': 1000, 'min_exposure': 0, 'max_exposure': 100 } return constraints_dict def start_ramp(self, amplitude, offset, freq): self._ni.set_up_ramp_output(amplitude, offset, freq) self._ni.start_ramp() def stop_ramp(self): self._ni.stop_ramp() self._ni.close_ramp() def start_finesse_measurement(self, repeat=10, freq=40): """ Starts the finesse measurement @param repeat: @param freq: @return: """ self.finesse_measure_cont = True ret_val = self.get_nth_full_sweep(sweep_number=1, save=True) if ret_val != 0: self.log.error('Did not get first sweep!') self.ramp_popt = self._fit_ramp( xdata=self.time_trim[::9], ydata=self.volts_trim[self.ramp_channel, ::9]) Modes = self._ni.sweep_function( self.RampUp_time[self.first_corrected_resonances], *self.ramp_popt) self.current_mode_number = len(self.first_corrected_resonances) - 2 self.linewidth_measurement(Modes, target_mode=self.current_mode_number, repeat=repeat, freq=freq) high_mode = len(self.first_corrected_resonances) - 2 low_mode = 0 for i in range(15): if self.finesse_measure_cont is True: self.current_mode_number -= 1 ret_val = self.get_nth_full_sweep(sweep_number=2 + i) target_mode = self.get_target_mode(self.current_resonances, low_mode=low_mode, high_mode=high_mode, plot=True) if target_mode == None: print('Moved more that 5 modes') self.ramp_popt = self._fit_ramp( xdata=self.time_trim[::9], ydata=self.volts_trim[self.ramp_channel, ::9]) Modes = self._ni.sweep_function( self.RampUp_time[self.current_resonances], *self.ramp_popt) self.linewidth_measurement(Modes, target_mode=target_mode, repeat=repeat, freq=freq) else: break def continue_finesse_measurements(self): self.finesse_measure_cont = False pass def stop_finesse_measurement(self): pass def set_cavity_position(self, position): self._ni.cavity_set_position(position)
class SimpleDataGui(GUIBase): """ FIXME: Please document """ _modclass = 'simplegui' _modtype = 'gui' ## declare connectors simplelogic = Connector(interface='SimpleDataLogic') sigStart = QtCore.Signal() sigStop = QtCore.Signal() def __init__(self, config, **kwargs): super().__init__(config=config, **kwargs) self.log.debug('The following configuration was found.') # checking for the right configuration for key in config.keys(): self.log.info('{0}: {1}'.format(key, config[key])) def on_activate(self): """ Definition and initialisation of the GUI. """ self._simple_logic = self.simplelogic() ##################### # Configuring the dock widgets # Use the inherited class 'CounterMainWindow' to create the GUI window self._mw = SimpleMainWindow() # Setup dock widgets self._mw.centralwidget.hide() self._mw.setDockNestingEnabled(True) # Plot labels. self._pw = self._mw.trace_PlotWidget self.plot1 = self._pw.plotItem self.plot1.setLabel('left', 'Some Value', units='some unit', color='#00ff00') self.plot1.setLabel('bottom', 'Number of values', units='some unit') self.plot2 = self._pw.plotItem self.plot2.setLabel('right', 'Smooth Value', units='some unit', color='#ff0000') self.curvearr = [] self.smootharr = [] colorlist = (palette.c1, palette.c2, palette.c3, palette.c4, palette.c5, palette.c6) ## Create an empty plot curve to be filled later, set its pen for i in range(self._simple_logic._data_logic.getChannels()): self.curvearr.append(self.plot1.plot()) self.curvearr[-1].setPen(colorlist[(2 * i) % len(colorlist)]) self.smootharr.append(self.plot2.plot()) self.smootharr[-1].setPen(colorlist[(2 * i + 1) % len(colorlist)], width=2) # make correct button state self._mw.startAction.setChecked(False) ##################### # Connecting user interactions self._mw.startAction.triggered.connect(self.start_clicked) self._mw.recordAction.triggered.connect(self.save_clicked) ##################### # starting the physical measurement self.sigStart.connect(self._simple_logic.startMeasure) self.sigStop.connect(self._simple_logic.stopMeasure) self._simple_logic.sigRepeat.connect(self.updateData) def show(self): """Make window visible and put it above all other windows. """ QtWidgets.QMainWindow.show(self._mw) self._mw.activateWindow() self._mw.raise_() def on_deactivate(self): """ Deactivate the module properly. """ # FIXME: ! self._mw.close() def updateData(self): """ The function that grabs the data and sends it to the plot. """ for i in range(self._simple_logic._data_logic.getChannels()): self.curvearr[i].setData(y=self._simple_logic.buf[0:-11, i], x=np.arange( 0, len(self._simple_logic.buf[0:-11]))) self.smootharr[i].setData( y=self._simple_logic.smooth[24:-25 - 10, i], x=np.arange(0, len(self._simple_logic.smooth[24:-25 - 10]))) if self._simple_logic.module_state() == 'locked': self._mw.startAction.setText('Stop') else: self._mw.startAction.setText('Start') def start_clicked(self): """ Handling the Start button to stop and restart the counter. """ if self._simple_logic.module_state() == 'locked': self._mw.startAction.setText('Start') self.sigStop.emit() else: self._mw.startAction.setText('Stop') self.sigStart.emit() def save_clicked(self): """ Handling the save button to save the data into a file. """ return
class NuclearOperationsGui(GUIBase): """ This is the main GUI Class for Nuclear Operations. """ _modclass = 'NuclearOperationsGui' _modtype = 'gui' # declare connectors nuclearoperationslogic = Connector(interface='NuclearOperationsLogic') savelogic = Connector(interface='SaveLogic') def on_activate(self): """ This init connects all the graphic modules, which were created in the *.ui file and configures the event handling between the modules. """ self._no_logic = self.nuclearoperationslogic() self._save_logic = self.savelogic() # Create the MainWindow to display the GUI self._mw = NuclearOperationsMainWindow() # Add save file tag input box self._mw.save_tag_LineEdit = QtWidgets.QLineEdit(self._mw) self._mw.save_tag_LineEdit.setMaximumWidth(200) self._mw.save_tag_LineEdit.setToolTip('Enter a nametag which will be\n' 'added to the filename.') self._mw.save_ToolBar.addWidget(self._mw.save_tag_LineEdit) # Set the values from the logic to the GUI: # Set the pulser parameter: self._mw.electron_rabi_periode_DSpinBox.setValue(self._no_logic.electron_rabi_periode*1e9) self._mw.pulser_mw_freq_DSpinBox.setValue(self._no_logic.pulser_mw_freq/1e6) self._mw.pulser_mw_amp_DSpinBox.setValue(self._no_logic.pulser_mw_amp) self._mw.pulser_mw_ch_SpinBox.setValue(self._no_logic.pulser_mw_ch) self._mw.nuclear_rabi_period0_DSpinBox.setValue(self._no_logic.nuclear_rabi_period0*1e6) self._mw.pulser_rf_freq0_DSpinBox.setValue(self._no_logic.pulser_rf_freq0/1e6) self._mw.pulser_rf_amp0_DSpinBox.setValue(self._no_logic.pulser_rf_amp0) self._mw.nuclear_rabi_period1_DSpinBox.setValue(self._no_logic.nuclear_rabi_period1*1e6) self._mw.pulser_rf_freq1_DSpinBox.setValue(self._no_logic.pulser_rf_freq1/1e6) self._mw.pulser_rf_amp1_DSpinBox.setValue(self._no_logic.pulser_rf_amp1) self._mw.pulser_rf_ch_SpinBox.setValue(self._no_logic.pulser_rf_ch) self._mw.pulser_laser_length_DSpinBox.setValue(self._no_logic.pulser_laser_length*1e9) self._mw.pulser_laser_amp_DSpinBox.setValue(self._no_logic.pulser_laser_amp) self._mw.pulser_laser_ch_SpinBox.setValue(self._no_logic.pulser_laser_ch) self._mw.num_singleshot_readout_SpinBox.setValue(self._no_logic.num_singleshot_readout) self._mw.pulser_idle_time_DSpinBox.setValue(self._no_logic.pulser_idle_time*1e9) self._mw.pulser_detect_ch_SpinBox.setValue(self._no_logic.pulser_detect_ch) # set the measurement parameter: self._mw.current_meas_asset_name_ComboBox.clear() self._mw.current_meas_asset_name_ComboBox.addItems(self._no_logic.get_meas_type_list()) if self._no_logic.current_meas_asset_name != '': index = self._mw.current_meas_asset_name_ComboBox.findText(self._no_logic.current_meas_asset_name, QtCore.Qt.MatchFixedString) if index >= 0: self._mw.current_meas_asset_name_ComboBox.setCurrentIndex(index) if self._no_logic.current_meas_asset_name == 'Nuclear_Frequency_Scan': self._mw.x_axis_start_DSpinBox.setValue(self._no_logic.x_axis_start/1e6) self._mw.x_axis_step_DSpinBox.setValue(self._no_logic.x_axis_step/1e6) elif self._no_logic.current_meas_asset_name in ['Nuclear_Rabi','QSD_-_Artificial_Drive', 'QSD_-_SWAP_FID','QSD_-_Entanglement_FID']: self._mw.x_axis_start_DSpinBox.setValue(self._no_logic.x_axis_start*1e6) self._mw.x_axis_step_DSpinBox.setValue(self._no_logic.x_axis_step*1e6) self._mw.x_axis_num_points_SpinBox.setValue(self._no_logic.x_axis_num_points) self._mw.num_of_meas_runs_SpinBox.setValue(self._no_logic.num_of_meas_runs) # set the optimize parameters: self._mw.optimize_period_odmr_SpinBox.setValue(self._no_logic.optimize_period_odmr) self._mw.optimize_period_confocal_SpinBox.setValue(self._no_logic.optimize_period_confocal) self._mw.odmr_meas_freq0_DSpinBox.setValue(self._no_logic.odmr_meas_freq0/1e6) self._mw.odmr_meas_freq1_DSpinBox.setValue(self._no_logic.odmr_meas_freq1/1e6) self._mw.odmr_meas_freq2_DSpinBox.setValue(self._no_logic.odmr_meas_freq2/1e6) self._mw.odmr_meas_runtime_DSpinBox.setValue(self._no_logic.odmr_meas_runtime) self._mw.odmr_meas_freq_range_DSpinBox.setValue(self._no_logic.odmr_meas_freq_range/1e6) self._mw.odmr_meas_step_DSpinBox.setValue(self._no_logic.odmr_meas_step/1e6) self._mw.odmr_meas_power_DSpinBox.setValue(self._no_logic.odmr_meas_power) # set the mw parameters for measurement self._mw.mw_cw_freq_DSpinBox.setValue(self._no_logic.mw_cw_freq/1e6) self._mw.mw_cw_power_DSpinBox.setValue(self._no_logic.mw_cw_power) self._mw.mw_on_odmr_peak_ComboBox.clear() # convert on the fly the integer entries to str entries: self._mw.mw_on_odmr_peak_ComboBox.addItems([str(elem) for elem in self._no_logic.get_available_odmr_peaks()]) # set gated counter parameters: self._mw.gc_number_of_samples_SpinBox.setValue(self._no_logic.gc_number_of_samples) self._mw.gc_samples_per_readout_SpinBox.setValue(self._no_logic.gc_samples_per_readout) # Create the graphic display for the measurement: self.nuclear_ops_graph = pg.PlotDataItem(self._no_logic.x_axis_list, self._no_logic.y_axis_list, pen=QtGui.QPen(QtGui.QColor(212, 85, 0, 255))) self._mw.nulcear_ops_GraphicsView.addItem(self.nuclear_ops_graph) # Set the proper initial display: self.current_meas_asset_name_changed() # Connect the signals: self._mw.current_meas_asset_name_ComboBox.currentIndexChanged.connect(self.current_meas_asset_name_changed) # adapt the unit according to the # Connect the start and stop signals: self._mw.action_run_stop.toggled.connect(self.start_stop_measurement) self._mw.action_continue.toggled.connect(self.continue_stop_measurement) self._mw.action_save.triggered.connect(self.save_measurement) self._no_logic.sigMeasurementStopped.connect(self._update_display_meas_stopped) # Connect graphic update: self._no_logic.sigCurrMeasPointUpdated.connect(self.update_meas_graph) self._no_logic.sigCurrMeasPointUpdated.connect(self.update_meas_parameter) def on_deactivate(self): """ Reverse steps of activation @return int: error code (0:OK, -1:error) """ self._mw.close() def show(self): """Make window visible and put it above all other windows. """ QtWidgets.QMainWindow.show(self._mw) self._mw.activateWindow() self._mw.raise_() def start_stop_measurement(self, is_checked): """ Manages what happens if nuclear operations are started/stopped. @param bool ischecked: If true measurement is started, if false measurement stops. """ if is_checked: # change the axes appearance according to input values: self._no_logic.stop_nuclear_meas() self.update_all_logic_parameter() self._no_logic.start_nuclear_meas() self._mw.action_continue.setEnabled(False) else: self._no_logic.stop_nuclear_meas() self._mw.action_continue.setEnabled(True) def continue_stop_measurement(self, is_checked): """ Manages what happens if nuclear operations are continued/stopped. @param bool ischecked: If true measurement is continued, if false measurement stops. """ if is_checked: # self._no_logic.stop_nuclear_meas() self._no_logic.start_nuclear_meas(continue_meas=True) self._mw.action_run_stop.setEnabled(False) else: self._no_logic.stop_nuclear_meas() self._mw.action_run_stop.setEnabled(True) def _update_display_meas_stopped(self): """ Update all the displays of the current measurement state and set them to stop. """ self.start_stop_measurement(is_checked=False) self.continue_stop_measurement(is_checked=False) def current_meas_asset_name_changed(self): """ Adapt the input widget to the current measurement sequence. """ name = self._mw.current_meas_asset_name_ComboBox.currentText() if name == 'Nuclear_Rabi': self._mw.nuclear_rabi_period0_DSpinBox.setVisible(False) self._mw.nuclear_rabi_period0_Label.setVisible(False) self._mw.nuclear_rabi_period1_DSpinBox.setVisible(False) self._mw.nuclear_rabi_period1_Label.setVisible(False) self._mw.pulser_rf_freq1_DSpinBox.setVisible(False) self._mw.pulser_rf_freq1_Label.setVisible(False) self._mw.pulser_rf_amp1_DSpinBox.setVisible(False) self._mw.pulser_rf_amp1_Label.setVisible(False) self._mw.pulser_rf_freq0_DSpinBox.setVisible(True) self._mw.pulser_rf_freq0_Label.setVisible(True) self._mw.nulcear_ops_GraphicsView.setLabel(axis='bottom', text='RF pulse length', units='s') self._mw.nulcear_ops_GraphicsView.setLabel(axis='left', text='Flip probability') self._mw.x_axis_start_Label.setText('x start (u\u00B5s)') self._mw.x_axis_step_Label.setText('x step (u\u00B5s)') self._mw.current_meas_point_Label.setText('Curr meas point (u\u00B5s)') elif name == 'Nuclear_Frequency_Scan': self._mw.pulser_rf_freq0_DSpinBox.setVisible(False) self._mw.pulser_rf_freq0_Label.setVisible(False) self._mw.nuclear_rabi_period1_DSpinBox.setVisible(False) self._mw.nuclear_rabi_period1_Label.setVisible(False) self._mw.pulser_rf_freq1_DSpinBox.setVisible(False) self._mw.pulser_rf_freq1_Label.setVisible(False) self._mw.pulser_rf_amp1_DSpinBox.setVisible(False) self._mw.pulser_rf_amp1_Label.setVisible(False) self._mw.nuclear_rabi_period0_DSpinBox.setVisible(True) self._mw.nuclear_rabi_period0_Label.setVisible(True) self._mw.nulcear_ops_GraphicsView.setLabel(axis='bottom', text='RF pulse Frequency', units='Hz') self._mw.nulcear_ops_GraphicsView.setLabel(axis='left', text='Flip probability') self._mw.x_axis_start_Label.setText('x start (MHz)') self._mw.x_axis_step_Label.setText('x step (MHz)') self._mw.current_meas_point_Label.setText('Curr meas point (MHz)') elif name in ['QSD_-_Artificial_Drive', 'QSD_-_SWAP_FID','QSD_-_Entanglement_FID']: self._mw.nuclear_rabi_period0_DSpinBox.setVisible(True) self._mw.nuclear_rabi_period0_Label.setVisible(True) self._mw.nuclear_rabi_period1_DSpinBox.setVisible(True) self._mw.nuclear_rabi_period1_Label.setVisible(True) self._mw.pulser_rf_freq1_DSpinBox.setVisible(True) self._mw.pulser_rf_freq1_Label.setVisible(True) self._mw.pulser_rf_amp1_DSpinBox.setVisible(True) self._mw.pulser_rf_amp1_Label.setVisible(True) self._mw.pulser_rf_freq0_DSpinBox.setVisible(True) self._mw.pulser_rf_freq0_Label.setVisible(True) self._mw.nulcear_ops_GraphicsView.setLabel(axis='bottom', text='Pulse length', units='s') self._mw.nulcear_ops_GraphicsView.setLabel(axis='left', text='Flip probability') self._mw.x_axis_start_Label.setText('x start (\u00B5s)') self._mw.x_axis_step_Label.setText('x step (\u00B5s)') self._mw.current_meas_point_Label.setText('Curr meas point (u\u00B5s)') def update_all_logic_parameter(self): """ If the measurement is started, update all parameters in the logic. """ # pulser parameter: self._no_logic.electron_rabi_periode = self._mw.electron_rabi_periode_DSpinBox.value()/1e9 self._no_logic.pulser_mw_freq = self._mw.pulser_mw_freq_DSpinBox.value()*1e6 self._no_logic.pulser_mw_amp = self._mw.pulser_mw_amp_DSpinBox.value() self._no_logic.pulser_mw_ch = self._mw.pulser_mw_ch_SpinBox.value() self._no_logic.nuclear_rabi_period0 = self._mw.nuclear_rabi_period0_DSpinBox.value()/1e6 self._no_logic.pulser_rf_freq0 = self._mw.pulser_rf_freq0_DSpinBox.value()*1e6 self._no_logic.pulser_rf_amp0 = self._mw.pulser_rf_amp0_DSpinBox.value() self._no_logic.nuclear_rabi_period1 = self._mw.nuclear_rabi_period1_DSpinBox.value()/1e6 self._no_logic.pulser_rf_freq1 = self._mw.pulser_rf_freq1_DSpinBox.value()*1e6 self._no_logic.pulser_rf_amp1 = self._mw.pulser_rf_amp1_DSpinBox.value() self._no_logic.pulser_rf_ch = self._mw.pulser_rf_ch_SpinBox.value() self._no_logic.pulser_laser_length = self._mw.pulser_laser_length_DSpinBox.value()/1e9 self._no_logic.pulser_laser_amp = self._mw.pulser_laser_amp_DSpinBox.value() self._no_logic.pulser_laser_ch = self._mw.pulser_laser_ch_SpinBox.value() self._no_logic.num_singleshot_readout = self._mw.num_singleshot_readout_SpinBox.value() self._no_logic.pulser_idle_time = self._mw.pulser_idle_time_DSpinBox.value()/1e9 self._no_logic.pulser_detect_ch = self._mw.pulser_detect_ch_SpinBox.value() # measurement parameter curr_meas_name = self._mw.current_meas_asset_name_ComboBox.currentText() self._no_logic.current_meas_asset_name = curr_meas_name if curr_meas_name in ['Nuclear_Rabi','QSD_-_Artificial_Drive', 'QSD_-_SWAP_FID','QSD_-_Entanglement_FID']: self._no_logic.x_axis_start = self._mw.x_axis_start_DSpinBox.value()/1e6 self._no_logic.x_axis_step = self._mw.x_axis_step_DSpinBox.value()/1e6 elif curr_meas_name in ['Nuclear_Frequency_Scan']: self._no_logic.x_axis_start = self._mw.x_axis_start_DSpinBox.value()*1e6 self._no_logic.x_axis_step = self._mw.x_axis_step_DSpinBox.value()*1e6 else: self.log.error('This measurement does not have any units associated to it!') self._no_logic.x_axis_num_points = self._mw.x_axis_num_points_SpinBox.value() self._no_logic.num_of_meas_runs = self._mw.num_of_meas_runs_SpinBox.value() # Optimization measurements: self._no_logic.optimize_period_odmr = self._mw.optimize_period_odmr_SpinBox.value() self._no_logic.optimize_period_confocal = self._mw.optimize_period_confocal_SpinBox.value() # Optimization parameters: self._no_logic.odmr_meas_freq0 = self._mw.odmr_meas_freq0_DSpinBox.value()*1e6 self._no_logic.odmr_meas_freq1 = self._mw.odmr_meas_freq1_DSpinBox.value()*1e6 self._no_logic.odmr_meas_freq2 = self._mw.odmr_meas_freq2_DSpinBox.value()*1e6 self._no_logic.odmr_meas_runtime = self._mw.odmr_meas_runtime_DSpinBox.value() self._no_logic.odmr_meas_freq_range = self._mw.odmr_meas_freq_range_DSpinBox.value()*1e6 self._no_logic.odmr_meas_step = self._mw.odmr_meas_step_DSpinBox.value()*1e6 self._no_logic.odmr_meas_power = self._mw.odmr_meas_power_DSpinBox.value() # mw parameters for measurement self._no_logic.mw_cw_freq = self._mw.mw_cw_freq_DSpinBox.value()*1e6 self._no_logic.mw_cw_power = self._mw.mw_cw_power_DSpinBox.value() self._no_logic.mw_on_odmr_peak = int(self._mw.mw_on_odmr_peak_ComboBox.currentText()) # gated counter self._no_logic.gc_number_of_samples = self._mw.gc_number_of_samples_SpinBox.value() self._no_logic.gc_samples_per_readout = self._mw.gc_samples_per_readout_SpinBox.value() def save_measurement(self): """ Save the current measurement. @return: """ timestamp = datetime.datetime.now() filetag = self._mw.save_tag_LineEdit.text() filepath = self._save_logic.get_path_for_module(module_name='NuclearOperations') if len(filetag) > 0: filename = os.path.join(filepath, '{0}_{1}_NuclearOps'.format(timestamp.strftime('%Y%m%d-%H%M-%S'), filetag)) else: filename = os.path.join(filepath, '{0}_NuclearOps'.format(timestamp.strftime('%Y%m%d-%H%M-%S'),)) exporter_graph = pg.exporters.SVGExporter(self._mw.nulcear_ops_GraphicsView.plotItem.scene()) #exporter_graph = pg.exporters.ImageExporter(self._mw.odmr_PlotWidget.plotItem) exporter_graph.export(filename + '.svg') # self._save_logic. self._no_logic.save_nuclear_operation_measurement(name_tag=filetag, timestamp=timestamp) def update_meas_graph(self): """ Retrieve from the logic the current x and y values and display them in the graph. """ self.nuclear_ops_graph.setData(self._no_logic.x_axis_list, self._no_logic.y_axis_list) def update_meas_parameter(self): """ Update the display parameter close to the graph. """ self._mw.current_meas_index_SpinBox.setValue(self._no_logic.current_meas_index) self._mw.elapsed_time_DSpinBox.setValue(self._no_logic.elapsed_time) self._mw.num_of_current_meas_runs_SpinBox.setValue(self._no_logic.num_of_current_meas_runs) measurement_name = self._no_logic.current_meas_asset_name if measurement_name in ['Nuclear_Rabi','QSD_-_Artificial_Drive', 'QSD_-_SWAP_FID','QSD_-_Entanglement_FID']: self._mw.current_meas_point_DSpinBox.setValue(self._no_logic.current_meas_point*1e6) elif measurement_name == 'Nuclear_Frequency_Scan': self._mw.current_meas_point_DSpinBox.setValue(self._no_logic.current_meas_point/1e6) else: pass
class SpectrometerGui(GUIBase): _modclass = 'SpectrometerGui' _modtype = 'gui' # declare connectors spectrumlogic = Connector(interface='SpectrumLogic') def __init__(self, config, **kwargs): super().__init__(config=config, **kwargs) def on_activate(self): """ Definition and initialisation of the GUI. """ self._spectrum_logic = self.spectrumlogic() # setting up the window self._mw = SpectrometerWindow() self._mw.stop_diff_spec_Action.setEnabled(False) self._mw.resume_diff_spec_Action.setEnabled(False) self._mw.correct_background_Action.setChecked( self._spectrum_logic.background_correction) # giving the plots names allows us to link their axes together self._pw = self._mw.plotWidget # pg.PlotWidget(name='Counter1') self._plot_item = self._pw.plotItem # create a new ViewBox, link the right axis to its coordinate system self._right_axis = pg.ViewBox() self._plot_item.showAxis('right') self._plot_item.scene().addItem(self._right_axis) self._plot_item.getAxis('right').linkToView(self._right_axis) self._right_axis.setXLink(self._plot_item) # create a new ViewBox, link the right axis to its coordinate system self._top_axis = pg.ViewBox() self._plot_item.showAxis('top') self._plot_item.scene().addItem(self._top_axis) self._plot_item.getAxis('top').linkToView(self._top_axis) self._top_axis.setYLink(self._plot_item) self._top_axis.invertX(b=True) # handle resizing of any of the elements self._pw.setLabel('left', 'Fluorescence', units='counts/s') self._pw.setLabel('right', 'Number of Points', units='#') self._pw.setLabel('bottom', 'Wavelength', units='m') self._pw.setLabel('top', 'Relative Frequency', units='Hz') # Create an empty plot curve to be filled later, set its pen self._curve1 = self._pw.plot() self._curve1.setPen(palette.c1, width=2) self._curve2 = self._pw.plot() self._curve2.setPen(palette.c2, width=2) self.update_data() # Connect singals self._mw.rec_single_spectrum_Action.triggered.connect( self.record_single_spectrum) self._mw.start_diff_spec_Action.triggered.connect( self.start_differential_measurement) self._mw.stop_diff_spec_Action.triggered.connect( self.stop_differential_measurement) self._mw.resume_diff_spec_Action.triggered.connect( self.resume_differential_measurement) self._mw.save_spectrum_Action.triggered.connect( self.save_spectrum_data) self._mw.correct_background_Action.triggered.connect( self.correct_background) self._mw.acquire_background_Action.triggered.connect( self.acquire_background) self._mw.save_background_Action.triggered.connect( self.save_background_data) self._mw.restore_default_view_Action.triggered.connect( self.restore_default_view) self._spectrum_logic.sig_specdata_updated.connect(self.update_data) self._spectrum_logic.spectrum_fit_updated_Signal.connect( self.update_fit) self._spectrum_logic.fit_domain_updated_Signal.connect( self.update_fit_domain) self._mw.show() self._save_PNG = True # Internal user input changed signals self._mw.fit_domain_min_doubleSpinBox.valueChanged.connect( self.set_fit_domain) self._mw.fit_domain_max_doubleSpinBox.valueChanged.connect( self.set_fit_domain) # Internal trigger signals self._mw.do_fit_PushButton.clicked.connect(self.do_fit) self._mw.fit_domain_all_data_pushButton.clicked.connect( self.reset_fit_domain_all_data) # fit settings self._fsd = FitSettingsDialog(self._spectrum_logic.fc) self._fsd.sigFitsUpdated.connect( self._mw.fit_methods_ComboBox.setFitFunctions) self._fsd.applySettings() self._mw.action_FitSettings.triggered.connect(self._fsd.show) def on_deactivate(self): """ Deinitialisation performed during deactivation of the module. """ # disconnect signals self._fsd.sigFitsUpdated.disconnect() self._mw.close() def show(self): """Make window visible and put it above all other windows. """ QtWidgets.QMainWindow.show(self._mw) self._mw.activateWindow() self._mw.raise_() def update_data(self): """ The function that grabs the data and sends it to the plot. """ data = self._spectrum_logic.spectrum_data # erase previous fit line self._curve2.setData(x=[], y=[]) # draw new data self._curve1.setData(x=data[0, :], y=data[1, :]) def update_fit(self, fit_data, result_str_dict, current_fit): """ Update the drawn fit curve and displayed fit results. """ if current_fit != 'No Fit': # display results as formatted text self._mw.spectrum_fit_results_DisplayWidget.clear() try: formated_results = units.create_formatted_output( result_str_dict) except: formated_results = 'this fit does not return formatted results' self._mw.spectrum_fit_results_DisplayWidget.setPlainText( formated_results) # redraw the fit curve in the GUI plot. self._curve2.setData(x=fit_data[0, :], y=fit_data[1, :]) def record_single_spectrum(self): """ Handle resume of the scanning without resetting the data. """ self._spectrum_logic.get_single_spectrum() def start_differential_measurement(self): # Change enabling of GUI actions self._mw.stop_diff_spec_Action.setEnabled(True) self._mw.start_diff_spec_Action.setEnabled(False) self._mw.rec_single_spectrum_Action.setEnabled(False) self._mw.resume_diff_spec_Action.setEnabled(False) self._spectrum_logic.start_differential_spectrum() def stop_differential_measurement(self): self._spectrum_logic.stop_differential_spectrum() # Change enabling of GUI actions self._mw.stop_diff_spec_Action.setEnabled(False) self._mw.start_diff_spec_Action.setEnabled(True) self._mw.rec_single_spectrum_Action.setEnabled(True) self._mw.resume_diff_spec_Action.setEnabled(True) def resume_differential_measurement(self): self._spectrum_logic.resume_differential_spectrum() # Change enabling of GUI actions self._mw.stop_diff_spec_Action.setEnabled(True) self._mw.start_diff_spec_Action.setEnabled(False) self._mw.rec_single_spectrum_Action.setEnabled(False) self._mw.resume_diff_spec_Action.setEnabled(False) def save_spectrum_data(self): self._spectrum_logic.save_spectrum_data() def correct_background(self): self._spectrum_logic.background_correction = self._mw.correct_background_Action.isChecked( ) def acquire_background(self): self._spectrum_logic.get_single_spectrum(background=True) def save_background_data(self): self._spectrum_logic.save_spectrum_data(background=True) def do_fit(self): """ Command spectrum logic to do the fit with the chosen fit function. """ fit_function = self._mw.fit_methods_ComboBox.getCurrentFit()[0] self._spectrum_logic.do_fit(fit_function) def set_fit_domain(self): """ Set the fit domain in the spectrum logic to values given by the GUI spinboxes. """ lambda_min = self._mw.fit_domain_min_doubleSpinBox.value() lambda_max = self._mw.fit_domain_max_doubleSpinBox.value() new_fit_domain = np.array([lambda_min, lambda_max]) self._spectrum_logic.set_fit_domain(new_fit_domain) def reset_fit_domain_all_data(self): """ Reset the fit domain to match the full data set. """ self._spectrum_logic.set_fit_domain() def update_fit_domain(self, domain): """ Update the displayed fit domain to new values (set elsewhere). """ self._mw.fit_domain_min_doubleSpinBox.setValue(domain[0]) self._mw.fit_domain_max_doubleSpinBox.setValue(domain[1]) def restore_default_view(self): """ Restore the arrangement of DockWidgets to the default """ # Show any hidden dock widgets self._mw.spectrum_fit_dockWidget.show() # re-dock any floating dock widgets self._mw.spectrum_fit_dockWidget.setFloating(False) # Arrange docks widgets self._mw.addDockWidget( QtCore.Qt.DockWidgetArea(QtCore.Qt.TopDockWidgetArea), self._mw.spectrum_fit_dockWidget) # Set the toolbar to its initial top area self._mw.addToolBar(QtCore.Qt.TopToolBarArea, self._mw.measure_ToolBar) self._mw.addToolBar(QtCore.Qt.TopToolBarArea, self._mw.background_ToolBar) self._mw.addToolBar(QtCore.Qt.TopToolBarArea, self._mw.differential_ToolBar) return 0
class PoiManagerLogic(GenericLogic): """ This is the Logic class for mapping and tracking bright features in the confocal scan. """ _modclass = 'poimanagerlogic' _modtype = 'logic' # declare connectors optimizer1 = Connector(interface='OptimizerLogic') scannerlogic = Connector(interface='ConfocalLogic') savelogic = Connector(interface='SaveLogic') # status vars poi_list = StatusVar(default=OrderedDict()) roi_name = StatusVar(default='') active_poi = StatusVar(default=None) signal_timer_updated = QtCore.Signal() signal_poi_updated = QtCore.Signal() signal_poi_deleted = QtCore.Signal(str) signal_confocal_image_updated = QtCore.Signal() signal_periodic_opt_started = QtCore.Signal() signal_periodic_opt_duration_changed = QtCore.Signal() signal_periodic_opt_stopped = QtCore.Signal() def __init__(self, config, **kwargs): super().__init__(config=config, **kwargs) self._current_poi_key = None self.go_to_crosshair_after_refocus = False # default value # timer and its handling for the periodic refocus self.timer = None self.time_left = 0 self.timer_step = 0 self.timer_duration = 300 # locking for thread safety self.threadlock = Mutex() def on_activate(self): """ Initialisation performed during activation of the module. """ self._optimizer_logic = self.optimizer1() self._confocal_logic = self.scannerlogic() self._save_logic = self.savelogic() # listen for the refocus to finish self._optimizer_logic.sigRefocusFinished.connect(self._refocus_done) # listen for the deactivation of a POI caused by moving to a different position self._confocal_logic.signal_change_position.connect(self.user_move_deactivates_poi) # Initialise the roi_map_data (xy confocal image) self.roi_map_data = self._confocal_logic.xy_image def on_deactivate(self): return def user_move_deactivates_poi(self, tag): """ Deactivate the active POI if the confocal microscope scanner position is moved by anything other than the optimizer """ pass def add_poi(self, position=None, key=None, emit_change=True): """ Creates a new poi and adds it to the list. @return int: key of this new poi A position can be provided (such as during re-loading a saved ROI). If no position is provided, then the current crosshair position is used. """ # If there are only 2 POIs (sample and crosshair) then the newly added POI needs to start the sample drift logging. if len(self.poi_list) == 2: self.poi_list['sample']._creation_time = time.time() # When the poimanager is activated the 'sample' poi is created because it is needed # from the beginning for various functionalities. If the tracking of the sample is started it has # to be reset such that this first point is deleted here # Probably this can be solved a lot nicer. self.poi_list['sample'].delete_last_position(empty_array_completely=True) self.poi_list['sample'].add_position_to_history(position=[0, 0, 0]) self.poi_list['sample'].set_coords_in_sample(coords=[0, 0, 0]) if position is None: position = self._confocal_logic.get_position()[:3] if len(position) != 3: self.log.error('Given position is not 3-dimensional.' 'Please pass POIManager a 3-dimensional position to set a POI.') return new_poi = PoI(pos=position, key=key) self.poi_list[new_poi.get_key()] = new_poi # The POI coordinates are set relative to the last known sample position most_recent_sample_pos = self.poi_list['sample'].get_position_history()[-1, :][1:4] this_poi_coords = position - most_recent_sample_pos new_poi.set_coords_in_sample(coords=this_poi_coords) # Since POI was created at current scanner position, it automatically # becomes the active POI. self.set_active_poi(poikey=new_poi.get_key()) if emit_change: self.signal_poi_updated.emit() return new_poi.get_key() def get_confocal_image_data(self): """ Get the current confocal xy scan data to hold as image of ROI""" # get the roi_map_data (xy confocal image) self.roi_map_data = self._confocal_logic.xy_image self.signal_confocal_image_updated.emit() def get_all_pois(self, abc_sort=False): """ Returns a list of the names of all existing POIs. @return string[]: List of names of the POIs Also crosshair and sample are included. """ if abc_sort is False: return sorted(self.poi_list.keys()) elif abc_sort is True: # First create a dictionary with poikeys indexed against names poinames = [''] * len(self.poi_list.keys()) for i, poikey in enumerate(self.poi_list.keys()): poiname = self.poi_list[poikey].get_name() poinames[i] = [poiname, poikey] # Sort names in the way that humans expect (site1, site2, site11, etc) # Regular expressions to make sorting key convert = lambda text: int(text) if text.isdigit() else text alphanum_key = lambda key: [convert(c) for c in re.split('([0-9]+)', key[0])] # Now we can sort poinames by name and return keys in that order return [key for [name, key] in sorted(poinames, key=alphanum_key)] else: # TODO: produce sensible error about unknown value of abc_sort. self.log.debug('fix TODO!') # TODO: Find a way to return a list of POI keys sorted in order of the POI names. def delete_last_position(self, poikey=None): """ Delete the last position in the history. @param string poikey: the key of the poi @return int: error code (0:OK, -1:error) """ if poikey is not None and poikey in self.poi_list.keys(): self.poi_list[poikey].delete_last_position() self.poi_list['sample'].delete_last_position() self.signal_poi_updated.emit() return 0 else: self.log.error('The last position of given POI ({0}) could not be deleted.'.format( poikey)) return -1 def delete_poi(self, poikey=None): """ Completely deletes the whole given poi. @param string poikey: the key of the poi @return int: error code (0:OK, -1:error) Does not delete the crosshair and sample. """ if poikey is not None and poikey in self.poi_list.keys(): if poikey is 'crosshair' or poikey is 'sample': self.log.warning('You cannot delete the crosshair or sample.') return -1 del self.poi_list[poikey] # If the active poi was deleted, there is no way to automatically choose # another active POI, so we deactivate POI if self.active_poi is not None and poikey == self.active_poi.get_key(): self._deactivate_poi() self.signal_poi_updated.emit() self.signal_poi_deleted.emit(poikey) return 0 elif poikey is None: self.log.warning('No POI for deletion specified.') else: self.log.error('X. The given POI ({0}) does not exist.'.format( poikey)) return -1 def optimise_poi(self, poikey=None): """ Starts the optimisation procedure for the given poi. @param string poikey: the key of the poi @return int: error code (0:OK, -1:error) This is threaded, so it returns directly. The function _refocus_done handles the data when the optimisation returns. """ if poikey is not None and poikey in self.poi_list.keys(): self.poi_list['crosshair'].add_position_to_history(position=self._confocal_logic.get_position()[:3]) self._current_poi_key = poikey self._optimizer_logic.start_refocus( initial_pos=self.get_poi_position(poikey=poikey), caller_tag='poimanager') return 0 else: self.log.error( 'Z. The given POI ({0}) does not exist.'.format(poikey)) return -1 def go_to_poi(self, poikey=None): """ Goes to the given poi and saves it as the current one. @param string poikey: the key of the poi @return int: error code (0:OK, -1:error) """ if poikey is not None and poikey in self.poi_list.keys(): self._current_poi_key = poikey x, y, z = self.get_poi_position(poikey=poikey) self._confocal_logic.set_position('poimanager', x=x, y=y, z=z) else: self.log.error('The given POI ({0}) does not exist.'.format( poikey)) return -1 # This is now the active POI to send to save logic for naming in any saved filenames. self.set_active_poi(poikey) #Fixme: After pressing the Go to Poi button the active poi is empty and the following lines do fix this # The time.sleep is somehow needed if not active_poi can not be set time.sleep(0.001) self.active_poi = self.poi_list[poikey] self.signal_poi_updated.emit() def get_poi_position(self, poikey=None): """ Returns the current position of the given poi, calculated from the POI coords in sample and the current sample position. @param string poikey: the key of the poi @return """ if poikey is not None and poikey in self.poi_list.keys(): poi_coords = self.poi_list[poikey].get_coords_in_sample() sample_pos = self.poi_list['sample'].get_position_history()[-1, :][1:4] return sample_pos + poi_coords else: self.log.error('G. The given POI ({0}) does not exist.'.format( poikey)) return [-1., -1., -1.] def set_new_position(self, poikey=None, newpos=None): """ Moves the given POI to a new position, and uses this information to update the sample position. @param string poikey: the key of the poi @param float[3] newpos: coordinates of the new position @return int: error code (0:OK, -1:error) """ # If no new position is given, take the current confocal crosshair position if newpos is None: newpos = self._confocal_logic.get_position()[:3] if poikey is not None and poikey in self.poi_list.keys(): if len(newpos) != 3: self.log.error('Length of set poi is not 3.') return -1 # Add new position to trace of POI self.poi_list[poikey].add_position_to_history(position=newpos) # Calculate sample shift and add it to the trace of 'sample' POI sample_shift = newpos - self.get_poi_position(poikey=poikey) sample_shift += self.poi_list['sample'].get_position_history()[-1, :][1:4] self.poi_list['sample'].add_position_to_history(position=sample_shift) # signal POI has been updated (this will cause GUI to redraw) if (poikey is not 'crosshair') and (poikey is not 'sample'): self.signal_poi_updated.emit() return 0 self.log.error('J. The given POI ({0}) does not exist.'.format(poikey)) return -1 def move_coords(self, poikey=None, newpos=None): """Updates the coords of a given POI, and adds a position to the POI history, but DOES NOT update the sample position. """ if newpos is None: newpos = self._confocal_logic.get_position()[:3] if poikey is not None and poikey in self.poi_list.keys(): if len(newpos) != 3: self.log.error('Length of set poi is not 3.') return -1 this_poi = self.poi_list[poikey] return_val = this_poi.add_position_to_history(position=newpos) sample_pos = self.poi_list['sample'].get_position_history()[-1, :][1:4] new_coords = newpos - sample_pos this_poi.set_coords_in_sample(new_coords) self.signal_poi_updated.emit() return return_val self.log.error('JJ. The given POI ({0}) does not exist.'.format(poikey)) return -1 def rename_poi(self, poikey=None, name=None, emit_change=True): """ Sets the name of the given poi. @param string poikey: the key of the poi @param string name: name of the poi to be set @return int: error code (0:OK, -1:error) """ if poikey is not None and name is not None and poikey in self.poi_list.keys(): success = self.poi_list[poikey].set_name(name=name) # if this is the active POI then we need to update poi tag in savelogic if self.poi_list[poikey] == self.active_poi: self.update_poi_tag_in_savelogic() if emit_change: self.signal_poi_updated.emit() return success else: self.log.error('AAAThe given POI ({0}) does not exist.'.format( poikey)) return -1 def start_periodic_refocus(self, poikey=None): """ Starts the perodic refocussing of the poi. @param float duration: (optional) the time between periodic optimization @param string poikey: (optional) the key of the poi to be set and refocussed on. @return int: error code (0:OK, -1:error) """ if poikey is not None and poikey in self.poi_list.keys(): self._current_poi_key = poikey else: # Todo: warning message that active POI used by default self._current_poi_key = self.active_poi.get_key() self.log.info('Periodic refocus on {0}.'.format(self._current_poi_key)) self.timer_step = 0 self.timer = QtCore.QTimer() self.timer.setSingleShot(False) self.timer.timeout.connect(self._periodic_refocus_loop) self.timer.start(300) self.signal_periodic_opt_started.emit() return 0 def set_periodic_optimize_duration(self, duration=None): """ Change the duration of the periodic optimize timer during active periodic refocussing. @param float duration: (optional) the time between periodic optimization. """ if duration is not None: self.timer_duration = duration else: self.log.warning('No timer duration given, using {0} s.'.format( self.timer_duration)) self.signal_periodic_opt_duration_changed.emit() def _periodic_refocus_loop(self): """ This is the looped function that does the actual periodic refocus. If the time has run out, it refocussed the current poi. Otherwise it just updates the time that is left. """ self.time_left = self.timer_step - time.time() + self.timer_duration self.signal_timer_updated.emit() if self.time_left <= 0: self.timer_step = time.time() self.optimise_poi(poikey=self._current_poi_key) def stop_periodic_refocus(self): """ Stops the perodic refocussing of the poi. @return int: error code (0:OK, -1:error) """ if self.timer is None: self.log.warning('No timer to stop.') return -1 self.timer.stop() self.timer = None self.signal_periodic_opt_stopped.emit() return 0 def _refocus_done(self, caller_tag, optimal_pos): """ Gets called automatically after the refocus is done and saves the new position to the poi history. Also it tracks the sample and may go back to the crosshair. @return int: error code (0:OK, -1:error) """ # We only need x, y, z optimized_position = optimal_pos[0:3] # If the refocus was on the crosshair, then only update crosshair POI and don't # do anything with sample position. caller_tags = ['confocalgui', 'magnet_logic', 'singleshot_logic'] if caller_tag in caller_tags: self.poi_list['crosshair'].add_position_to_history(position=optimized_position) # If the refocus was initiated here by poimanager, then update POI and sample elif caller_tag == 'poimanager': if self._current_poi_key is not None and self._current_poi_key in self.poi_list.keys(): self.set_new_position(poikey=self._current_poi_key, newpos=optimized_position) if self.go_to_crosshair_after_refocus: temp_key = self._current_poi_key self.go_to_poi(poikey='crosshair') self._current_poi_key = temp_key else: self.go_to_poi(poikey=self._current_poi_key) return 0 else: self.log.error('The given POI ({0}) does not exist.'.format( self._current_poi_key)) return -1 else: self.log.warning("Unknown caller_tag for the optimizer. POI " "Manager does not know what to do with optimized " "position, and has done nothing.") def reset_roi(self): del self.poi_list self.poi_list = dict() self.active_poi = None self.roi_name = '' # initally add crosshair to the pois crosshair = PoI(pos=[0, 0, 0], name='crosshair') crosshair._key = 'crosshair' self.poi_list[crosshair._key] = crosshair # Re-initialise sample in the poi list sample = PoI(pos=[0, 0, 0], name='sample') sample._key = 'sample' self.poi_list[sample._key] = sample self.signal_poi_updated.emit() def set_active_poi(self, poikey=None): """ Set the active POI object. """ if poikey is None: # If poikey is none and no active poi is set, then do nothing if self.active_poi is None: return else: self.active_poi = None elif poikey in self.get_all_pois(): # If poikey is the current active POI then do nothing if self.poi_list[poikey] == self.active_poi: return else: self.active_poi = self.poi_list[poikey] else: # todo: error poikey unknown return -1 self.update_poi_tag_in_savelogic() self.signal_poi_updated.emit() # todo: this breaks the emit_change = false case def _deactivate_poi(self): self.set_active_poi(poikey=None) def update_poi_tag_in_savelogic(self): if self.active_poi is not None: self._save_logic.active_poi_name = self.active_poi.get_name() else: self._save_logic.active_poi_name = '' def save_poi_map_as_roi(self): """ Save a list of POIs with their coordinates to a file. """ # File path and name filepath = self._save_logic.get_path_for_module(module_name='ROIs') # We will fill the data OderedDict to send to savelogic data = OrderedDict() # Lists for each column of the output file poinames = [] poikeys = [] x_coords = [] y_coords = [] z_coords = [] for poikey in self.get_all_pois(abc_sort=True): if poikey is not 'sample' and poikey is not 'crosshair': thispoi = self.poi_list[poikey] poinames.append(thispoi.get_name()) poikeys.append(poikey) x_coords.append(thispoi.get_coords_in_sample()[0]) y_coords.append(thispoi.get_coords_in_sample()[1]) z_coords.append(thispoi.get_coords_in_sample()[2]) data['POI Name'] = np.array(poinames) data['POI Key'] = np.array(poikeys) data['X'] = np.array(x_coords) data['Y'] = np.array(y_coords) data['Z'] = np.array(z_coords) self._save_logic.save_data( data, filepath=filepath, filelabel=self.roi_name, fmt=['%s', '%s', '%.6e', '%.6e', '%.6e'] ) self.log.debug('ROI saved to:\n{0}'.format(filepath)) return 0 def load_roi_from_file(self, filename=None): if filename is None: return -1 with open(filename, 'r') as roifile: for line in roifile: if line[0] != '#' and line.split()[0] != 'NaN': saved_poi_name = line.split()[0] saved_poi_key = line.split()[1] saved_poi_coords = [ float(line.split()[2]), float(line.split()[3]), float(line.split()[4])] this_poi_key = self.add_poi( position=saved_poi_coords, key=saved_poi_key, emit_change=False) self.rename_poi(poikey=this_poi_key, name=saved_poi_name, emit_change=False) # Now that all the POIs are created, emit the signal for other things (ie gui) to update self.signal_poi_updated.emit() return 0 @poi_list.constructor def dict_to_poi_list(self, val): pdict = {} # initially add crosshair to the pois crosshair = PoI(pos=[0, 0, 0], name='crosshair') crosshair._key = 'crosshair' pdict[crosshair._key] = crosshair # initally add sample to the pois sample = PoI(pos=[0, 0, 0], name='sample') sample._key = 'sample' pdict[sample._key] = sample if isinstance(val, dict): for key, poidict in val.items(): try: if len(poidict['pos']) >= 3: newpoi = PoI(name=poidict['name'], key=poidict['key']) newpoi.set_coords_in_sample(poidict['pos']) newpoi._creation_time = poidict['time'] newpoi._position_time_trace = poidict['history'] pdict[key] = newpoi except Exception as e: self.log.exception('Could not load PoI {0}: {1}'.format(key, poidict)) return pdict @poi_list.representer def poi_list_to_dict(self, val): pdict = { key: poi.to_dict() for key, poi in val.items() } return pdict @active_poi.representer def active_poi_to_dict(self, val): if isinstance(val, PoI): return val.to_dict() return None @active_poi.constructor def dict_to_active_poi(self, val): try: if isinstance(val, dict): if len(val['pos']) >= 3: newpoi = PoI(pos=val['pos'], name=val['name'], key=val['key']) newpoi._creation_time = val['time'] newpoi._position_time_trace = val['history'] return newpoi except Exception as e: self.log.exception('Could not load active poi {0}'.format(val)) return None def triangulate(self, r, a1, b1, c1, a2, b2, c2): """ Reorients a coordinate r that is known relative to reference points a1, b1, c1 to produce a new vector rnew that has exactly the same relation to rotated/shifted/tilted reference positions a2, b2, c2. @param np.array r: position to be remapped. @param np.array a1: initial location of ref1. @param np.array a2: final location of ref1. @param np.array b1, b2, c1, c2: similar for ref2 and ref3 """ ab_old = b1 - a1 ac_old = c1 - a1 ab_new = b2 - a2 ac_new = c2 - a2 # Firstly, find the angle to rotate ab_old onto ab_new. This rotation must be done in # the plane that contains these two vectors, which means rotating about an axis # perpendicular to both of them (the cross product). axis1 = np.cross(ab_old, ab_new) # Only works if ab_old and ab_new are not parallel axis1length = np.sqrt((axis1 * axis1).sum()) if axis1length == 0: ab_olddif = ab_old + np.array([100, 0, 0]) axis1 = np.cross(ab_old, ab_olddif) # normalising the axis1 vector axis1 = axis1 / np.sqrt((axis1 * axis1).sum()) # The dot product gives the angle between ab_old and ab_new dot = np.dot(ab_old, ab_new) x_modulus = np.sqrt((ab_old * ab_old).sum()) y_modulus = np.sqrt((ab_new * ab_new).sum()) # float errors can cause the division to be slightly above 1 for 90 degree rotations, which # will confuse arccos. cos_angle = min(dot / x_modulus / y_modulus, 1) angle1 = np.arccos(cos_angle) # angle in radians # Construct a rotational matrix for axis1 n1 = axis1[0] n2 = axis1[1] n3 = axis1[2] m1 = np.matrix(((((n1 * n1) * (1 - np.cos(angle1)) + np.cos(angle1)), ((n1 * n2) * (1 - np.cos(angle1)) - n3 * np.sin(angle1)), ((n1 * n3) * (1 - np.cos(angle1)) + n2 * np.sin(angle1)) ), (((n2 * n1) * (1 - np.cos(angle1)) + n3 * np.sin(angle1)), ((n2 * n2) * (1 - np.cos(angle1)) + np.cos(angle1)), ((n2 * n3) * (1 - np.cos(angle1)) - n1 * np.sin(angle1)) ), (((n3 * n1) * (1 - np.cos(angle1)) - n2 * np.sin(angle1)), ((n3 * n2) * (1 - np.cos(angle1)) + n1 * np.sin(angle1)), ((n3 * n3) * (1 - np.cos(angle1)) + np.cos(angle1)) ) ) ) # Now that ab_old can be rotated to overlap with ab_new, we need to rotate in another # axis to fix "tilt". By choosing ab_new as the rotation axis we ensure that the # ab vectors stay where they need to be. # ac_old_rot is the rotated ac_old (around axis1). We need to find the angle to rotate # ac_old_rot around ab_new to get ac_new. ac_old_rot = np.array(np.dot(m1, ac_old))[0] axis2 = -ab_new # TODO: check maths to find why this negative sign is necessary. Empirically it is now working. axis2 = axis2 / np.sqrt((axis2 * axis2).sum()) # To get the angle of rotation it is most convenient to work in the plane for which axis2 is the normal. # We must project vectors ac_old_rot and ac_new into this plane. a = ac_old_rot - np.dot(ac_old_rot, axis2) * axis2 # projection of ac_old_rot in the plane of rotation about axis2 b = ac_new - np.dot(ac_new, axis2) * axis2 # projection of ac_new in the plane of rotation about axis2 # The dot product gives the angle of rotation around axis2 dot = np.dot(a, b) x_modulus = np.sqrt((a * a).sum()) y_modulus = np.sqrt((b * b).sum()) cos_angle = min(dot / x_modulus / y_modulus, 1) # float errors can cause the division to be slightly above 1 for 90 degree rotations, which will confuse arccos. angle2 = np.arccos(cos_angle) # angle in radians # Construct a rotation matrix around axis2 n1 = axis2[0] n2 = axis2[1] n3 = axis2[2] m2 = np.matrix(((((n1 * n1) * (1 - np.cos(angle2)) + np.cos(angle2)), ((n1 * n2) * (1 - np.cos(angle2)) - n3 * np.sin(angle2)), ((n1 * n3) * (1 - np.cos(angle2)) + n2 * np.sin(angle2)) ), (((n2 * n1) * (1 - np.cos(angle2)) + n3 * np.sin(angle2)), ((n2 * n2) * (1 - np.cos(angle2)) + np.cos(angle2)), ((n2 * n3) * (1 - np.cos(angle2)) - n1 * np.sin(angle2)) ), (((n3 * n1) * (1 - np.cos(angle2)) - n2 * np.sin(angle2)), ((n3 * n2) * (1 - np.cos(angle2)) + n1 * np.sin(angle2)), ((n3 * n3) * (1 - np.cos(angle2)) + np.cos(angle2)) ) ) ) # To find the new position of r, displace by (a2 - a1) and do the rotations a1r = r - a1 rnew = a2 + np.array(np.dot(m2, np.array(np.dot(m1, a1r))[0]))[0] return rnew def reorient_roi(self, ref1_coords, ref2_coords, ref3_coords, ref1_newpos, ref2_newpos, ref3_newpos): """ Move and rotate the ROI to a new position specified by the newpos of 3 reference POIs from the saved ROI. @param ref1_coords: coordinates (from ROI save file) of reference 1. @param ref2_coords: similar, ref2. @param ref3_coords: similar, ref3. @param ref1_newpos: the new (current) position of POI reference 1. @param ref2_newpos: similar, ref2. @param ref3_newpos: similar, ref3. """ for poikey in self.get_all_pois(abc_sort=True): if poikey is not 'sample' and poikey is not 'crosshair': thispoi = self.poi_list[poikey] old_coords = thispoi.get_coords_in_sample() new_coords = self.triangulate(old_coords, ref1_coords, ref2_coords, ref3_coords, ref1_newpos, ref2_newpos, ref3_newpos) self.move_coords(poikey=poikey, newpos=new_coords) def autofind_pois(self, neighborhood_size=1, min_threshold=10000, max_threshold=1e6): """Automatically search the xy scan image for POIs. @param neighborhood_size: size in microns. Only the brightest POI per neighborhood will be found. @param min_threshold: POIs must have c/s above this threshold. @param max_threshold: POIs must have c/s below this threshold. """ # Calculate the neighborhood size in pixels from the image range and resolution x_range_microns = np.max(self.roi_map_data[:, :, 0]) - np.min(self.roi_map_data[:, :, 0]) y_range_microns = np.max(self.roi_map_data[:, :, 1]) - np.min(self.roi_map_data[:, :, 1]) y_pixels = len(self.roi_map_data) x_pixels = len(self.roi_map_data[1, :]) pixels_per_micron = np.max([x_pixels, y_pixels]) / np.max([x_range_microns, y_range_microns]) # The neighborhood in pixels is nbhd_size * pixels_per_um, but it must be 1 or greater neighborhood_pix = int(np.max([math.ceil(pixels_per_micron * neighborhood_size), 1])) data = self.roi_map_data[:, :, 3] data_max = filters.maximum_filter(data, neighborhood_pix) maxima = (data == data_max) data_min = filters.minimum_filter(data, 3 * neighborhood_pix) diff = ((data_max - data_min) > min_threshold) maxima[diff is False] = 0 labeled, num_objects = ndimage.label(maxima) xy = np.array(ndimage.center_of_mass(data, labeled, range(1, num_objects + 1))) for count, pix_pos in enumerate(xy): poi_pos = self.roi_map_data[pix_pos[0], pix_pos[1], :][0:3] this_poi_key = self.add_poi(position=poi_pos, emit_change=False) self.rename_poi(poikey=this_poi_key, name='spot' + str(count), emit_change=False) # Now that all the POIs are created, emit the signal for other things (ie gui) to update self.signal_poi_updated.emit()
class MagnetMotorInterfuse(GenericLogic, MagnetInterface): _modclass = 'MagnetMotorInterfuse' _modtype = 'interfuse' # declare connectors, here you can see the interfuse action: the in # connector will cope a motor hardware, that means a motor device can # connect to the in connector of the logic. motorstage = Connector(interface='MotorInterface') def __init__(self, **kwargs): super().__init__(**kwargs) # save the idle state in this class variable, since that is not present # in the actual motor hardware device. Use this variable to decide # whether movement commands are passed to the hardware. self._magnet_idle = False def on_activate(self): """ Initialisation performed during activation of the module. """ self._motor_device = self.motorstage() def on_deactivate(self): """ Deinitialisation performed during deactivation of the module. """ pass def get_constraints(self): """ Retrieve the hardware constrains from the magnet driving device. @return dict: dict with constraints for the magnet hardware. These constraints will be passed via the logic to the GUI so that proper display elements with boundary conditions could be made. """ return self._motor_device.get_constraints() def move_rel(self, param_dict): """ Moves stage in given direction (relative movement) @param dict param_dict: dictionary, which passes all the relevant parameters, which should be changed. Usage: {'axis_label': <the-abs-pos-value>}. 'axis_label' must correspond to a label given to one of the axis. A smart idea would be to ask the position after the movement. """ if not self._magnet_idle: self._motor_device.move_rel(param_dict) else: self.log.warning( 'Motor Device is in Idle state and cannot ' 'perform "move_rel" commands. Couple the Motor to ' 'control via the command "set_magnet_idle_state(False)" ' 'to have control over its movement.') return param_dict def move_abs(self, param_dict): """ Moves stage to absolute position (absolute movement) @param dict param_dict: dictionary, which passes all the relevant parameters, which should be changed. Usage: {'axis_label': <the-abs-pos-value>}. 'axis_label' must correspond to a label given to one of the axis. """ if not self._magnet_idle: self._motor_device.move_abs(param_dict) else: self.log.warning( 'Motor Device is in Idle state and cannot ' 'perform "move_abs" commands. Couple the Motor to ' 'control via the command "set_magnet_idle_state (False)" ' 'to have control over its movement.') return param_dict def abort(self): """ Stops movement of the stage @return int: error code (0:OK, -1:error) """ self._motor_device.abort() return 0 def get_pos(self, param_list=None): """ Gets current position of the stage @param list param_list: optional, if a specific position of an axis is desired, then the labels of the needed axis should be passed in the param_list. If nothing is passed, then from each axis the position is asked. @return dict: with keys being the axis labels and item the current position. """ return self._motor_device.get_pos(param_list) def get_status(self, param_list=None): """ Get the status of the position @param list param_list: optional, if a specific status of an axis is desired, then the labels of the needed axis should be passed in the param_list. If nothing is passed, then from each axis the status is asked. @return dict: with the axis label as key and the status number as item. """ return self._motor_device.get_status(param_list) def calibrate(self, param_list=None): """ Calibrates the stage. @param dict param_list: param_list: optional, if a specific calibration of an axis is desired, then the labels of the needed axis should be passed in the param_list. If nothing is passed, then all connected axis will be calibrated. @return int: error code (0:OK, -1:error) After calibration the stage moves to home position which will be the zero point for the passed axis. The calibration procedure will be different for each stage. """ if not self._magnet_idle: self._motor_device.calibrate(param_list) else: self.log.warning( 'Motor Device is in Idle state and cannot ' 'perform "calibrate" commands. Couple the Motor to ' 'control via the command "set_magnet_idle_state(False)" ' 'to have control over its movement.') def get_velocity(self, param_list=None): """ Gets the current velocity for all connected axes. @param dict param_list: optional, if a specific velocity of an axis is desired, then the labels of the needed axis should be passed as the param_list. If nothing is passed, then from each axis the velocity is asked. @return dict: with the axis label as key and the velocity as item. """ return self._motor_device.get_velocity(param_list) def set_velocity(self, param_dict=None): """ Write new value for velocity. @param dict param_dict: dictionary, which passes all the relevant parameters, which should be changed. Usage: {'axis_label': <the-velocity-value>}. 'axis_label' must correspond to a label given to one of the axis. @return int: error code (0:OK, -1:error) """ if not self._magnet_idle: self._motor_device.set_velocity(param_dict) else: self.log.warning( 'Motor Device is in Idle state and cannot ' 'perform "set_velocity" commands. Couple the Motor to ' 'control via the command "set_magnet_idle_state(False)" ' 'to have control over its movement.') return param_dict def tell(self, param_dict=None): """ Send a command to the magnet. @param dict param_dict: dictionary, which passes all the relevant parameters, which should be changed. Usage: {'axis_label': <the command string>}. 'axis_label' must correspond to a label given to one of the axis. @return int: error code (0:OK, -1:error) """ self.log.info('You can tell the motor dummy as much as you want, it ' 'has always an open ear for you. But do not expect an ' 'answer, it is very shy!') return param_dict def ask(self, param_dict=None): """ Ask the magnet a question. @param dict param_dict: dictionary, which passes all the relevant parameters, which should be changed. Usage: {'axis_label': <the question string>}. 'axis_label' must correspond to a label given to one of the axis. @return dict: contains the answer to the specific axis coming from the magnet. Keywords are the axis names, item names are the string answers of the axis. """ self.log.info( 'The Motor Hardware does not support an "ask" command ' 'and is not be able to answer the questions "{0}" to the ' 'axis "{1}"! If you want to talk to someone ask Siri, maybe ' 'she will listen to you and answer your questions ' ':P.'.format(list(param_dict.values()), list(param_dict))) return_val = {} for entry in param_dict: return_val[entry] = 'Nothing to say, Motor is quite.' return return_val def initialize(self): """ Acts as a switch. When all coils of the superconducting magnet are heated it cools them, else the coils get heated. @return int: (0: Ok, -1:error) """ self.log.info('Motor Hardware does not need initialization for ' 'starting or ending a movement. Nothing will happen.') return 0 def set_magnet_idle_state(self, magnet_idle=True): """ Set the magnet to couple/decouple to/from the control. @param bool magnet_idle: if True then magnet will be set to idle and each movement command will be ignored from the hardware file. If False the magnet will react on movement changes of any kind. @return bool: the actual state which was set in the magnet hardware. True = idle, decoupled from control False = Not Idle, coupled to control """ self._magnet_idle = magnet_idle return self._magnet_idle def get_magnet_idle_state(self): """ Retrieve the current state of the magnet, whether it is idle or not. @return bool: the actual state which was set in the magnet hardware. True = idle, decoupled from control False = Not Idle, coupled to control """ return self._magnet_idle