def DAQ_function(): # Query the Arduino for its state success, tmp_state = ard.query_ascii_values("?", delimiter="\t") if not (success): dprint("'%s' reports IOError" % ard.name) return False # Parse readings into separate state variables try: state.time, state.reading_1 = tmp_state state.time /= 1000 except Exception as err: pft(err, 3) dprint("'%s' reports IOError" % ard.name) return False # Use Arduino time or PC time? USE_PC_TIME = True now = time.perf_counter() if USE_PC_TIME else state.time if qdev_ard.update_counter_DAQ == 1: state.time_0 = now state.time = 0 else: state.time = now - state.time_0 # For demo purposes: Quit automatically after N updates if qdev_ard.update_counter_DAQ > 1000: app.quit() return True
def DAQ_function(self): DEBUG_local = False if DEBUG_local: tick = time.perf_counter() if not self.dev.query_V_meas(): return False if not self.dev.query_I_meas(): return False if not self.dev.query_LSR(): return False if not self.dev.query_ENA_output(): return False # Explicitly force the output state to off when the output got disabled # on a hardware level by a triggered protection or fault. if self.dev.state.ENA_output and self.dev.state.LSR_is_tripped: self.dev.state.ENA_output = False self.dev.set_ENA_output(False) if DEBUG_local: tock = time.perf_counter() dprint("%s: done in %i" % (self.dev.name, tock - tick)) return True
def DAQ_function(): # Date-time keeping str_cur_date, str_cur_time, str_cur_datetime = get_current_date_time() # Query the Arduino for its state success, tmp_state = ard.query_ascii_values("?", delimiter="\t") if not (success): dprint("'%s' reports IOError @ %s %s" % (ard.name, str_cur_date, str_cur_time)) return False # Parse readings into separate state variables try: state.time, state.reading_1 = tmp_state state.time /= 1000 except Exception as err: pft(err, 3) dprint("'%s' reports IOError @ %s %s" % (ard.name, str_cur_date, str_cur_time)) return False if USE_PC_TIME: state.time = time.perf_counter() # Add readings to chart history window.history_chart_curve.appendData(state.time, state.reading_1) # Logging to file log.update(filepath=str_cur_datetime + ".txt") # Return success return True
def test_Worker_jobs__start_without_create(): print_title("Worker_jobs - start without create") import pytest qdev = QDeviceIO(FakeDevice()) with pytest.raises(SystemExit) as pytest_wrapped_e: qdev.start_worker_jobs() assert pytest_wrapped_e.type == SystemExit dprint("Exit code: %i" % pytest_wrapped_e.value.code) assert pytest_wrapped_e.value.code == 404
def test_Worker_jobs__no_device_attached(): print_title("Worker_jobs - no device attached") import pytest qdev = QDeviceIO() with pytest.raises(SystemExit) as pytest_wrapped_e: qdev.create_worker_jobs() assert pytest_wrapped_e.type == SystemExit dprint("Exit code: %i" % pytest_wrapped_e.value.code) assert pytest_wrapped_e.value.code == 99
def test_attach_device_twice(): print_title("Attach device twice") import pytest qdev = QDeviceIO(FakeDevice()) with pytest.raises(SystemExit) as pytest_wrapped_e: qdev.attach_device(FakeDevice()) assert pytest_wrapped_e.type == SystemExit dprint("Exit code: %i" % pytest_wrapped_e.value.code) assert pytest_wrapped_e.value.code == 22
def DAQ_function(): # Date-time keeping str_cur_date, str_cur_time, str_cur_datetime = get_current_date_time() # Query the Arduino for its state success, tmp_state = ard.query_ascii_values("?", delimiter="\t") if not (success): dprint( "'%s' reports IOError @ %s %s" % (ard.name, str_cur_date, str_cur_time) ) return False # Parse readings into separate state variables try: ( state.time, state.ds_temp, state.bme_temp, state.bme_humi, state.bme_pres, ) = tmp_state state.time /= 1000 # Arduino time, [msec] to [s] state.bme_pres /= 100 # [Pa] to [mbar] except Exception as err: pft(err, 3) dprint( "'%s' reports IOError @ %s %s" % (ard.name, str_cur_date, str_cur_time) ) return False # Catch very intermittent DS18B20 sensor errors if state.ds_temp <= -127.0: state.ds_temp = np.nan # We will use PC time instead state.time = time.perf_counter() # Add readings to chart histories window.tscurve_julabo_setp.appendData(state.time, julabo.state.setpoint) window.tscurve_julabo_bath.appendData(state.time, julabo.state.bath_temp) window.tscurve_ds_temp.appendData(state.time, state.ds_temp) window.tscurve_bme_temp.appendData(state.time, state.bme_temp) window.tscurve_bme_humi.appendData(state.time, state.bme_humi) window.tscurve_bme_pres.appendData(state.time, state.bme_pres) # Logging to file log.update(filepath=str_cur_datetime + ".txt", mode="w") # Return success return True
def DAQ_function(): # Date-time keeping str_cur_date, str_cur_time, str_cur_datetime = get_current_date_time() state.update_counter_DAQ += 1 # Keep track of the obtained DAQ rate if not state.QET_rate.isValid(): state.QET_rate.start() else: # Obtained DAQ rate state.rate_accumulator += 1 dT = state.QET_rate.elapsed() if dT >= 1000: # Evaluate every N elapsed milliseconds. Hard-coded. state.QET_rate.restart() try: state.obtained_DAQ_rate_Hz = state.rate_accumulator / dT * 1e3 except ZeroDivisionError: state.obtained_DAQ_rate_Hz = np.nan state.rate_accumulator = 0 # Query the Arduino for its state success, tmp_state = ard.query_ascii_values("?", delimiter="\t") if not (success): dprint("'%s' reports IOError @ %s %s" % (ard.name, str_cur_date, str_cur_time)) sys.exit(0) # Parse readings into separate state variables try: state.time, state.reading_1 = tmp_state state.time /= 1000 except Exception as err: pft(err, 3) dprint("'%s' reports IOError @ %s %s" % (ard.name, str_cur_date, str_cur_time)) sys.exit(0) if USE_PC_TIME: state.time = time.perf_counter() # Add readings to chart histories window.history_chart_curve.appendData(state.time, state.reading_1) # Logging to file log.update(filepath=str_cur_datetime + ".txt") # We update the GUI right now because this is a singlethread demo window.update_GUI()
def DAQ_function(): # Date-time keeping str_cur_date, str_cur_time, str_cur_datetime = get_current_date_time() # Query the Arduino for its state success, tmp_state = ard.query_ascii_values("?", delimiter="\t") if not (success): dprint("'%s' reports IOError @ %s %s" % (ard.name, str_cur_date, str_cur_time)) return False # Parse readings into separate state variables try: ( state.time, state.dht22_temp, state.dht22_humi, ds18b20_temp, ) = tmp_state state.time /= 1000 # Arduino time, [msec] to [s] except Exception as err: pft(err, 3) dprint("'%s' reports IOError @ %s %s" % (ard.name, str_cur_date, str_cur_time)) return False # Optional extra sensor to register the heater surface temperature # print("%.2f" % ds18b20_temp) # We will use PC time instead state.time = time.perf_counter() # PID control pid.set_mode( mode=(psu.state.ENA_output and state.pid_enabled and not np.isnan(state.dht22_temp)), current_input=state.dht22_temp, current_output=psu.state.V_source, ) if pid.compute(current_input=state.dht22_temp): # New PID output got computed -> send new voltage to PSU qdev_psu.send(qdev_psu.dev.set_V_source, pid.output) # Print debug info to the terminal dprint("Tp=%7.3f Ti=%7.3f outp=%7.3f" % (pid.pTerm, pid.iTerm, pid.output)) # Add readings to chart histories window.tscurve_dht22_temp.appendData(state.time, state.dht22_temp) window.tscurve_dht22_humi.appendData(state.time, state.dht22_humi) window.tscurve_power.appendData(state.time, psu.state.P_meas) # Logging to file log.update(filepath=str_cur_datetime + ".txt", mode="w") # Return success return True
def initialize(self, current_input, current_output): """Does all the things that need to happen to ensure a bumpless transfer from manual to automatic mode. """ self.iTerm = current_output self.last_input = current_input if self.debug: dprint("PID init") if self.iTerm < self.output_limit_min: dprint("@PID init: iTerm < output_limit_min: integral windup") elif self.iTerm > self.output_limit_max: dprint("@PID init: iTerm > output_limit_max: integral windup") self.iTerm = np.clip(self.iTerm, self.output_limit_min, self.output_limit_max)
def scan_ports(self, verbose: bool = True) -> bool: """Scan over all serial ports and try to establish a connection. See further the description at :meth:`connect_at_port`. Args: verbose (:obj:`bool`, optional): Print a `"Scanning ports for: "`-message to the terminal? Default: :const:`True` Returns: True if successful, False otherwise. """ if verbose: _print_hrule(True) if (self._ID_validation_query is None or self._valid_ID_specific is None): dprint(" Scanning ports for: %s" % self.long_name, ANSI.YELLOW) else: dprint( " Scanning ports for: %s `%s`" % (self.long_name, self._valid_ID_specific), ANSI.YELLOW, ) _print_hrule() # Ports is a list of tuples ports = list(serial.tools.list_ports.comports()) for p in ports: port = p[0] if self.connect_at_port(port, verbose=False): return True else: continue # Scanned over all the ports without finding a match dprint(" Error: Device not found.\n", ANSI.RED) return False
def test_dprint_in_red(): with mock.patch("sys.stdout", new=io.StringIO()) as fake_stdout: dprint("In red", ANSI.RED) assert fake_stdout.getvalue() == "\x1b[1;31mIn red\x1b[1;37m\n"
def trigger_update_psus(): if DEBUG: dprint("timer_psus: wake up all DAQ") for psu_qdev in psus_qdev: psu_qdev.worker_DAQ.wake_up()
def DAQ_function(self): tick = get_tick() # Clear input and output buffers of the device. Seems to resolve # intermittent communication time-outs. self.dev.device.clear() success = True if self.is_MUX_scanning: success &= self.dev.init_scan() # Init scan if success: self.dev.wait_for_OPC() # Wait for OPC if self.worker_DAQ.debug: tock = get_tick() dprint("opc? in: %i" % (tock - tick)) tick = tock success &= self.dev.fetch_scan() # Fetch scan if self.worker_DAQ.debug: tock = get_tick() dprint("fetc in: %i" % (tock - tick)) tick = tock if success: self.dev.wait_for_OPC() # Wait for OPC if self.worker_DAQ.debug: tock = get_tick() dprint("opc? in: %i" % (tock - tick)) tick = tock if success: # Do not throw additional timeout exceptions when .init_scan() # might have already failed. Hence this check for no success. self.dev.query_all_errors_in_queue() # Query errors if self.worker_DAQ.debug: tock = get_tick() dprint("err? in: %i" % (tock - tick)) tick = tock # The next statement seems to trigger timeout, but very # intermittently (~once per 20 minutes). After this timeout, # everything times out. # self.dev.wait_for_OPC() # if self.worker_DAQ.debug: # tock = get_tick() # dprint("opc? in: %i" % (tock - tick)) # tick = tock # NOTE: Another work-around to intermittent time-outs might # be sending self.dev.clear() every iter to clear the input and # output buffers. This is now done at the start of this function. # Optional user-supplied function to run. You can use this function to, # e.g., parse out the scan readings into separate variables and # post-process this data or log it. if self.DAQ_postprocess_MUX_scan_function is not None: self.DAQ_postprocess_MUX_scan_function() if self.worker_DAQ.debug: tock = get_tick() dprint("extf in: %i" % (tock - tick)) tick = tock return success
def DAQ_function(): # Must return True when successful, False otherwise reply = dev.fake_query_1() dprint(" " * 50 + "%.1f Hz" % qdev.obtained_DAQ_rate_Hz) return reply[-4:] == "0101"
def _print_hrule(leading_newline=False): dprint(("\n" if leading_newline else "") + "-" * 60, ANSI.YELLOW)
def print_success(success_str: str): dprint(success_str, ANSI.GREEN) dprint(" " * 16 + "--> %s\n" % self.name, ANSI.GREEN)
def connect_at_port(self, port: str, verbose: bool = True) -> bool: """Open the serial port at address ``port`` and try to establish a connection. If the connection is successful and :meth:`set_ID_validation_query` was not set, then this function will return :const:`True`. If :meth:`set_ID_validation_query` was set, then this function will return :const:`True` or :const:`False` depending on the validation scheme as explained in :meth:`set_ID_validation_query`. Args: port (:obj:`str`): Serial port address to open. verbose (:obj:`bool`, optional): Print a `"Connecting to: "`-message to the terminal? Default: :const:`True` Returns: True if successful, False otherwise. """ def print_success(success_str: str): dprint(success_str, ANSI.GREEN) dprint(" " * 16 + "--> %s\n" % self.name, ANSI.GREEN) if verbose: _print_hrule(True) if (self._ID_validation_query is None or self._valid_ID_specific is None): dprint(" Connecting to: %s" % self.long_name, ANSI.YELLOW) else: dprint( " Connecting to: %s `%s`" % (self.long_name, self._valid_ID_specific), ANSI.YELLOW, ) _print_hrule() print(" @ %-11s " % port, end="") try: # Open the serial port self.ser = serial.Serial(port=port, **self.serial_settings) except serial.SerialException: print("Could not open port.") return False # --> leaving except Exception as err: pft(err, 3) sys.exit(0) # --> leaving if self._ID_validation_query is None: # Found any device print_success("Any Success!") self.is_alive = True return True # --> leaving # Optional validation query try: self._force_query_to_raise_on_timeout = True self.is_alive = True # We must assume communication is possible reply_broad, reply_specific = self._ID_validation_query() except: print("Wrong or no device.") self.close(ignore_exceptions=True) return False # --> leaving finally: self._force_query_to_raise_on_timeout = False if reply_broad == self._valid_ID_broad: if reply_specific is not None: print("Found `%s`: " % reply_specific, end="") if self._valid_ID_specific is None: # Found a matching device in a broad sense print_success("Broad Success!") self.is_alive = True return True # --> leaving elif reply_specific == self._valid_ID_specific: # Found a matching device in a specific sense print_success("Specific Success!") self.is_alive = True return True # --> leaving print("Wrong device.") self.close(ignore_exceptions=True) return False
def print_title(title): dprint("\n%s" % title, ANSI.PURPLE) dprint("-" * 50, ANSI.PURPLE)
def compute(self, current_input): """Compute new PID output. This function should be called repeatedly, preferably at a fixed time interval. Returns True when the output is computed, false when nothing has been done. """ now = time.perf_counter() # [s] time_step = now - self.last_time if (not self.in_auto) or np.isnan(self.setpoint): self.last_time = now return False _input = current_input error = self.setpoint - _input # Proportional term self.pTerm = self.kp * error # Integral term # self.iTerm = self.iTerm + (self.ki * error) self.iTerm = self.iTerm + (self.ki * time_step * error) if self.debug: if self.iTerm < self.output_limit_min: dprint("iTerm < output_limit_min: integral windup") elif self.iTerm > self.output_limit_max: dprint("iTerm > output_limit_max: integral windup") # Prevent integral windup self.iTerm = np.clip(self.iTerm, self.output_limit_min, self.output_limit_max) # Derivative term # Prevent derivative kick: really good to do! # self.dTerm = -self.kd * (_input - self.last_input) self.dTerm = -self.kd / time_step * (_input - self.last_input) # Compute PID Output self.output = self.pTerm + self.iTerm + self.dTerm if self.debug: dprint("%i" % (time_step * 1000)) dprint("%.1f %.1f %.1f" % (self.pTerm, self.iTerm, self.dTerm)) dprint((" " * 14 + "%.2f") % self.output) if self.output < self.output_limit_min: dprint("output < output_limit_min: output clamped") elif self.output > self.output_limit_max: dprint("output > output_limit_max: output clamped") # Clamp the output to its limits self.output = np.clip(self.output, self.output_limit_min, self.output_limit_max) # Remember some variables for next time self.last_input = _input self.last_time = now return True
def DAQ_function(self): DEBUG_local = False if DEBUG_local: tick = get_tick() # Clear input and output buffers of the device. Seems to resolve # intermittent communication time-outs. self.dev.device.clear() # Finish all operations at the device first if not self.dev.wait_for_OPC(): return False if not self.dev.query_V_meas(): return False if not self.dev.query_I_meas(): return False # -------------------- # Heater power PID # -------------------- # PID controllers work best when the process and control variables have # a linear relationship. # Here: # Process var: V (voltage) # Control var: P (power) # Relation : P = R / V^2 # # Hence, we transform P into P_star # Control var: P_star = sqrt(P) # Relation : P_star = sqrt(R) / V # When we assume R remains constant (which is not the case as the # resistance is a function of the heater temperature, but the dependence # is expected to be insignificant in our small temperature range of 20 # to 100 deg C), we now have linearized the PID feedback relation. self.dev.PID_power.set_mode( (self.dev.state.ENA_output and self.dev.state.ENA_PID), self.dev.state.P_meas, self.dev.state.V_source, ) self.dev.PID_power.setpoint = np.sqrt(self.dev.state.P_source) if self.dev.PID_power.compute(np.sqrt(self.dev.state.P_meas)): # New PID output got computed -> send new voltage to PSU if self.dev.PID_power.output < 1: # PSU does not regulate well below 1 V, hence clamp to 0 self.dev.PID_power.output = 0 if not self.dev.set_V_source(self.dev.PID_power.output): return False # Wait for the set_V_source operation to finish. # Takes ~ 300 ms to complete with wait_for_OPC. if not self.dev.wait_for_OPC(): return False if not self.dev.query_ENA_OCP(): return False if not self.dev.query_status_OC(): return False if not self.dev.query_status_QC(): return False if not self.dev.query_ENA_output(): return False # Explicitly force the output state to off when the output got disabled # on a hardware level by a triggered protection or fault. if self.dev.state.ENA_output & ( self.dev.state.status_QC_OV | self.dev.state.status_QC_OC | self.dev.state.status_QC_PF | self.dev.state.status_QC_OT | self.dev.state.status_QC_INH ): self.dev.state.ENA_output = False self.dev.set_ENA_output(False) if DEBUG_local: tock = get_tick() dprint("%s: done in %i" % (self.dev.name, tock - tick)) # Check if there are errors in the device queue and retrieve all # if any and append these to 'dev.state.all_errors'. if DEBUG_local: dprint("%s: query errors" % self.dev.name) tick = get_tick() self.dev.query_all_errors_in_queue() if DEBUG_local: tock = get_tick() dprint("%s: stb done in %i" % (self.dev.name, tock - tick)) return True
def tprint_tab(str_msg, ANSI_color=None): dprint(" " * 60 + "%.4f %s" % (time.perf_counter(), str_msg), ANSI_color)
def test_dprint(): with mock.patch("sys.stdout", new=io.StringIO()) as fake_stdout: dprint("No color") assert fake_stdout.getvalue() == "No color\n"
def listen_to_lockin_amp( self, ) -> Tuple[bool, Union[int, float], np.ndarray, np.ndarray, np.ndarray, np.ndarray]: """Reads incoming data packets coming from the lock-in amp. This method is blocking until it receives an EOM (end-of-message) sentinel or until it times out. Returns: Tuple ( success: bool counter: int | numpy.nan time : numpy.ndarray, units [us] ref_X : numpy.ndarray, units [non-dim] ref_Y : numpy.ndarray, units [non-dim] sig_I : numpy.ndarray, units [V] ) """ failed = False, None, [np.nan], [np.nan], [np.nan], [np.nan] c = self.config # Shorthand alias ans_bytes = self.read_until_EOM() # dprint("EOM found with %i bytes and..." % len(ans_bytes)) if not ans_bytes[:c.N_BYTES_SOM] == c.SOM: dprint("'%s' I/O ERROR: No SOM found" % self.name) return failed # dprint("SOM okay") if not len(ans_bytes) == c.N_BYTES_TX_BUFFER: dprint("'%s' I/O ERROR: Expected %i bytes but received %i" % (self.name, c.N_BYTES_TX_BUFFER, len(ans_bytes))) return failed if c.mcu_firmware == "ALIA v0.2.0 VSCODE": # --------------------------- # Legacy firmware # --------------------------- try: counter = struct.unpack(c.binfrmt_counter, ans_bytes[c.byte_slice_counter]) millis = struct.unpack(c.binfrmt_millis, ans_bytes[c.byte_slice_millis]) micros = struct.unpack(c.binfrmt_micros, ans_bytes[c.byte_slice_micros]) idx_phase = struct.unpack( c.binfrmt_idx_phase.format(c.BLOCK_SIZE), ans_bytes[c.byte_slice_idx_phase], ) sig_I = struct.unpack( c.binfrmt_sig_I.format(c.BLOCK_SIZE), ans_bytes[c.byte_slice_sig_I], ) except: dprint("'%s' I/O ERROR: Can't unpack bytes" % self.name) return failed # fmt: off counter = counter[0] millis = millis[0] micros = micros[0] idx_phase = np.array(idx_phase, dtype=int, order="C") sig_I = np.array(sig_I, dtype=float, order="C") # fmt: on # dprint("%i %i" % (millis, micros)) t0 = millis * 1000 + micros time = t0 + np.arange(0, c.BLOCK_SIZE) * c.SAMPLING_PERIOD * 1e6 time = np.asarray(time, dtype=float, order="C") phi = 2 * np.pi * idx_phase / c.N_LUT # DEBUG test: Add artificial phase delay between ref_X/Y and sig_I if 0: # pylint: disable=using-constant-test phase_offset_deg = 50 phi = np.unwrap(phi + phase_offset_deg / 180 * np.pi) # Reconstruct `ref_X` and `ref_Y` """ if c.ref_waveform == Waveform.Sine: # OVERRIDE: Only `Sine` allowed, because `Square` and `Triangle` # can not be garantueed deterministic on both Arduino and Python # side due to rounding differences and the problem of computing the # correct 90 degrees quadrant `ref_Y`. """ c.ref_RMS_factor = np.sqrt(2) ref_X = np.sin(phi) ref_Y = np.cos(phi) """ if c.ref_waveform == Waveform.Square: c.ref_RMS_factor = 1 ref_X.fill(-1) ref_X[ref_X_phase < (c.N_LUT / 4.0)] = 1.0 ref_X[ref_X_phase >= (c.N_LUT / 4.0 * 3.0)] = 1.0 ref_Y.fill(-1) ref_Y[ref_X_phase < (c.N_LUT / 2.0)] = 1.0 ref_Y[ref_X_phase == (c.N_LUT / 2.0)] = 0.0 if c.ref_waveform == Waveform.Triangle: c.ref_RMS_factor = np.sqrt(3) ref_X = np.arcsin(ref_X) / np.pi * 2 ref_Y = np.arcsin(ref_Y) / np.pi * 2 """ # Scale the LUTs [-1, 1] with the RMS factor np.multiply(ref_X, c.ref_RMS_factor, out=ref_X) np.multiply(ref_Y, c.ref_RMS_factor, out=ref_Y) # Transform `sig_I` from [bits] to [V] sig_I = np.multiply(sig_I, c.ADC_BITS_TO_V, out=sig_I) else: # --------------------------- # Modern firmware # --------------------------- try: counter = struct.unpack(c.binfrmt_counter, ans_bytes[c.byte_slice_counter]) millis = struct.unpack(c.binfrmt_millis, ans_bytes[c.byte_slice_millis]) micros = struct.unpack(c.binfrmt_micros, ans_bytes[c.byte_slice_micros]) idx_phase = struct.unpack(c.binfrmt_idx_phase, ans_bytes[c.byte_slice_idx_phase]) sig_I = struct.unpack( c.binfrmt_sig_I.format(c.BLOCK_SIZE), ans_bytes[c.byte_slice_sig_I], ) except: dprint("'%s' I/O ERROR: Can't unpack bytes" % self.name) return failed # fmt: off counter = counter[0] millis = millis[0] micros = micros[0] idx_phase = idx_phase[0] sig_I = np.array(sig_I, dtype=float, order="C") # fmt: on # dprint("%i %i" % (millis, micros)) t0 = millis * 1000 + micros time = t0 + np.arange(0, c.BLOCK_SIZE) * c.SAMPLING_PERIOD * 1e6 time = np.asarray(time, dtype=float, order="C") # DEBUG test: Add artificial phase delay between ref_X/Y and sig_I if 0: # pylint: disable=using-constant-test phase_offset_deg = 10 idx_phase += int(np.floor(phase_offset_deg / 360 * c.N_LUT)) # Construct `ref_X` and `ref_Y` LUT_X = np.roll(c.LUT_X, -idx_phase) LUT_Y = np.roll(c.LUT_Y, -idx_phase) ref_X_tiled = np.tile(LUT_X, int(np.ceil(c.BLOCK_SIZE / c.N_LUT))) ref_Y_tiled = np.tile(LUT_Y, int(np.ceil(c.BLOCK_SIZE / c.N_LUT))) ref_X = np.asarray( ref_X_tiled[:c.BLOCK_SIZE], dtype=float, order="C", ) ref_Y = np.asarray( ref_Y_tiled[:c.BLOCK_SIZE], dtype=float, order="C", ) # Transform `sig_I` from [bits] to [V] sig_I = np.multiply(sig_I, c.ADC_BITS_TO_V, out=sig_I) return True, counter, time, ref_X, ref_Y, sig_I
def lockin_DAQ_update(): """Listen for new data blocks send by the lock-in amplifier and perform the main mathematical operations for signal processing. This function will run in a dedicated thread (i.e. `worker_DAQ`), separated from the main program thread that handles the GUI. NOTE: NO GUI OPERATIONS ARE ALLOWED HERE. Otherwise it may affect the `worker_DAQ` thread negatively, resulting in lost blocks of data. """ # Shorthands c: Alia.Config = alia.config state: Alia_qdev.State = alia_qdev.state # Prevent throwings errors if just paused if alia.lockin_paused: return False if DEBUG_TIMING: tock = Time.perf_counter() print("%.2f _DAQ" % (tock - alia.tick)) alia.tick = tock # Listen for data buffers send by the lock-in ( success, _counter, state.time, state.ref_X, state.ref_Y, state.sig_I, ) = alia.listen_to_lockin_amp() if not success: dprint("@ %s %s" % current_date_time_strings()) return False # Detect dropped blocks # --------------------- # TODO: Rethink this procedure. Might be easier done with the index of the # block that also gets send by the Arduino. We either receive a full block, # or we don't. There are no partial blocks that can be received. alia_qdev.state.blocks_received += 1 last_time = state.rb_time[-1] if state.blocks_received > 1 else np.nan dT = (state.time[0] - last_time) / 1e6 # [usec] to [sec] if dT > c.SAMPLING_PERIOD * 1e6 * 1.10: # Allow a little clock jitter N_dropped_samples = int(round(dT / c.SAMPLING_PERIOD) - 1) dprint("Dropped samples: %i" % N_dropped_samples) dprint("@ %s %s" % current_date_time_strings()) # Replace dropped samples with np.nan samples. # As a result, the filter output will contain a continuous series of # np.nan values in the output for up to `RingBuffer_FIR_Filter. # T_settle_filter` seconds long after the occurrence of the last dropped # sample. state.rb_time.extend(last_time + np.arange(1, N_dropped_samples + 1) * c.SAMPLING_PERIOD * 1e6) state.rb_ref_X.extend(np.full(N_dropped_samples, np.nan)) state.rb_ref_Y.extend(np.full(N_dropped_samples, np.nan)) state.rb_sig_I.extend(np.full(N_dropped_samples, np.nan)) # Stage 0 # ------- state.sig_I_min = np.min(state.sig_I) state.sig_I_max = np.max(state.sig_I) state.sig_I_avg = np.mean(state.sig_I) state.sig_I_std = np.std(state.sig_I) state.rb_time.extend(state.time) state.rb_ref_X.extend(state.ref_X) state.rb_ref_Y.extend(state.ref_Y) state.rb_sig_I.extend(state.sig_I) # Note: `ref_X` [non-dim] is transformed to `ref_X*` [V] # Note: `ref_Y` [non-dim] is transformed to `ref_Y*` [V] window.hcc_ref_X.extendData( state.time, np.multiply(state.ref_X, c.ref_V_ampl_RMS) + c.ref_V_offset) window.hcc_ref_Y.extendData( state.time, np.multiply(state.ref_Y, c.ref_V_ampl_RMS) + c.ref_V_offset) window.hcc_sig_I.extendData(state.time, state.sig_I) # Stage 1 # ------- # fmt: off # Apply filter 1 to sig_I state.filt_I = alia_qdev.firf_1_sig_I.apply_filter(state.rb_sig_I) if alia_qdev.firf_1_sig_I.filter_has_settled: # Retrieve the block of original data from the past that aligns with # the current filter output valid_slice = alia_qdev.firf_1_sig_I.rb_valid_slice state.time_1 = state.rb_time[valid_slice] old_sig_I = state.rb_sig_I[valid_slice] old_ref_X = state.rb_ref_X[valid_slice] old_ref_Y = state.rb_ref_Y[valid_slice] # Heterodyne mixing np.multiply(state.filt_I, old_ref_X, out=state.mix_X) np.multiply(state.filt_I, old_ref_Y, out=state.mix_Y) else: state.time_1.fill(np.nan) old_sig_I = np.full(c.BLOCK_SIZE, np.nan) state.mix_X.fill(np.nan) state.mix_Y.fill(np.nan) state.filt_I_min = np.min(state.filt_I) state.filt_I_max = np.max(state.filt_I) state.filt_I_avg = np.mean(state.filt_I) state.filt_I_std = np.std(state.filt_I) state.rb_time_1.extend(state.time_1) state.rb_filt_I.extend(state.filt_I) state.rb_mix_X.extend(state.mix_X) state.rb_mix_Y.extend(state.mix_Y) window.hcc_filt_1_in.extendData(state.time_1, old_sig_I) window.hcc_filt_1_out.extendData(state.time_1, state.filt_I) window.hcc_mix_X.extendData(state.time_1, state.mix_X) window.hcc_mix_Y.extendData(state.time_1, state.mix_Y) # fmt: on # Stage 2 # ------- # Apply filter 2 to the mixer output state.X = alia_qdev.firf_2_mix_X.apply_filter(state.rb_mix_X) state.Y = alia_qdev.firf_2_mix_Y.apply_filter(state.rb_mix_Y) if alia_qdev.firf_2_mix_X.filter_has_settled: # Retrieve the block of time data from the past that aligns with # the current filter output valid_slice = alia_qdev.firf_1_sig_I.rb_valid_slice state.time_2 = state.rb_time_1[valid_slice] # Signal amplitude: R np.sqrt(np.add(np.square(state.X), np.square(state.Y)), out=state.R) # Signal phase: Theta np.arctan2(state.Y, state.X, out=state.T) np.multiply(state.T, 180 / np.pi, out=state.T) # [rad] to [deg] else: state.time_2.fill(np.nan) state.R.fill(np.nan) state.T.fill(np.nan) state.X_avg = np.mean(state.X) state.Y_avg = np.mean(state.Y) state.R_avg = np.mean(state.R) state.T_avg = np.mean(state.T) state.rb_time_2.extend(state.time_2) state.rb_X.extend(state.X) state.rb_Y.extend(state.Y) state.rb_R.extend(state.R) state.rb_T.extend(state.T) window.hcc_LIA_XR.extendData( state.time_2, state.X if window.qrbt_XR_X.isChecked() else state.R) window.hcc_LIA_YT.extendData( state.time_2, state.Y if window.qrbt_YT_Y.isChecked() else state.T) # Check if memory address of underlying buffer is still unchanged # pylint: disable=pointless-string-statement """ test = np.asarray(state.rb_X) print("%6i, mem: %i, cont?: %i, rb buf mem: %i, full? %i" % ( state.blocks_received, test.__array_interface__['data'][0], test.flags['C_CONTIGUOUS'], state.rb_X._unwrap_buffer.__array_interface__['data'][0], state.rb_X.is_full)) """ # Power spectra # ------------- calculate_PS_sig_I() calculate_PS_filt_I() calculate_PS_mix_X() calculate_PS_mix_Y() calculate_PS_R() # Logging to file logger.update(mode="w") # Return success return True