Source code for pyccapt.control.tdc_surface_concept.tdc_surface_concept

import multiprocessing as mp
import os
import time
from queue import Empty, Queue

import numpy as np

# local imports
from pyccapt.control.core import runtime as _runtime
from pyccapt.control.devices import initialize_devices
from pyccapt.control.tdc_surface_concept import scTDC

QUEUE_DATA = 0
QUEUE_ENDOFMEAS = 1
CHUNK_SIZE = 100_000  # Adjust the chunk size if needed

# Canonical on-disk dtype for each chunk stem, matching the HDF5 schema in
# core/hdf5_creator.py (chunk_mapping).  The integer stems are built from
# Python ints via list.extend(...), so a bare np.array(...) would infer the
# platform default int64.  hdf_creator._write_chunked_dataset declares these
# datasets as uint64/uint32 and (since the strict-dtype guard was added)
# refused to finalize when the chunk dtype did not match -- which broke every
# experiment longer than CHUNK_SIZE.  Pinning the dtype here keeps the writer
# and the HDF5 schema in agreement.  Stems absent from this map (e.g. the
# *_bin raw arrays, which hdf_creator does not consume) keep NumPy's inferred
# dtype.
CHUNK_DTYPES = {
    "x": np.float64,
    "y": np.float64,
    "t": np.float64,
    "voltage": np.float64,
    "voltage_pulse": np.float64,
    "laser_pulse": np.float64,
    "start_counter": np.uint64,
    "channel": np.uint32,
    "time": np.uint64,
    "tdc_start_counter": np.uint64,
    "voltage_tdc": np.float64,
    "voltage_pulse_tdc": np.float64,
    "laser_pulse_tdc": np.float64,
}
# Per-queue blocking-get timeout (s).  Two callbacks fire at different
# rates (DLD vs raw); using a small timeout instead of an indefinite
# wait avoids a deadlock when one queue is empty while the other is
# being drained.  Also lets us re-check stop_event between gets so
# STOP actually stops the TDC.
QUEUE_GET_TIMEOUT_S = 0.05


[docs] class BufDataCB4(scTDC.buffered_data_callbacks_pipe): """ The class inherits from python wrapper module scTDC and class: buffered_data_callbacks_pipe """ def __init__(self, lib, dev_desc, data_field_selection, dld_events, max_buffered_data_len=500_000): """ Initialize the base class: scTDC.buffered_data_callbacks_pipe Args: lib (scTDClib): A scTDClib object. dev_desc (int): Device descriptor as returned by sc_tdc_init_inifile(...). data_field_selection (int): A 'bitwise or' combination of SC_DATA_FIELD_xyz constants. dld_events (bool): True to receive DLD events, False to receive TDC events. max_buffered_data_len (int): Number of events buffered before invoking callbacks. """ super().__init__(lib, dev_desc, data_field_selection, max_buffered_data_len, dld_events) self.queue = Queue() self.end_of_meas = False
[docs] def on_data(self, d): """ This class method function: 1. Makes a deep copy of numpy arrays in d 2. Inserts basic values by simple assignment 3. Inserts numpy arrays using the copy method of the source array Args: d (dict): Data dictionary. Returns: None """ dcopy = {} for k in d.keys(): if isinstance(d[k], np.ndarray): dcopy[k] = d[k].copy() else: dcopy[k] = d[k] self.queue.put((QUEUE_DATA, dcopy)) if self.end_of_meas: self.end_of_meas = False self.queue.put((QUEUE_ENDOFMEAS, None))
[docs] def on_end_of_meas(self): """ This class method sets end_of_meas to True. Returns: True (bool) """ self.end_of_meas = True return True
[docs] def errorcheck(device, bufdatacb, bufdatacb_raw, retcode): """ This function checks return codes for errors and does cleanup. Args: retcode (int): Return code. bufdatacb (BufDataCB4): A BufDataCB4 object. bufdatacb_raw (BufDataCB4): A BufDataCB4 object. device (scTDC.Device): A scTDC.Device object. Returns: int: 0 if success return code or return code > 0, -1 if return code is error code or less than 0. """ if retcode < 0: print(device.lib.sc_get_err_msg(retcode)) bufdatacb.close() bufdatacb_raw.close() device.deinitialize() return -1 else: return 0
[docs] def save_chunk_worker(save_queue): while True: task = save_queue.get() if task is None: # Stop signal break chunk_id, path, chunk_data = task # Extract data try: for key, data in chunk_data.items(): target_dtype = CHUNK_DTYPES.get(key) if target_dtype is not None: arr = np.asarray(data, dtype=target_dtype) else: arr = np.array(data) np.save(os.path.join(path, f"chunks/{key}_chunk_{chunk_id}.npy"), arr) except Exception as e: print(f"Error saving chunk {chunk_id}: {e}")
# No artificial throttle: save_queue.get() already blocks when idle, # so the worker never busy-waits. A fixed per-chunk sleep would cap # throughput to ~1 chunk per sleep -- and because the raw/TDC stream # chunks far more often than DLD, that let the queue (and RAM) grow # without bound on high-raw-rate runs.
[docs] def load_and_concatenate_chunks(path, chunk_id): attr_names = [ "x_bin", "x", "y_bin", "y", "t_bin", "t", "voltage", "voltage_pulse", "laser_pulse", "start_counter", "channel", "time", "tdc_start_counter", "voltage_tdc", "voltage_pulse_tdc", "laser_pulse_tdc", ] all_data = {attr: [] for attr in attr_names} # Initialize storage for i in range(1, chunk_id + 1): for attr in attr_names: chunk_file = os.path.join(path, f"chunks/{attr}_chunk_{i}.npy") if os.path.exists(chunk_file): try: all_data[attr].append(np.load(chunk_file)) # Load NumPy array directly except Exception as e: print(f"Error loading {chunk_file}: {e}") else: print(f"Warning: File '{chunk_file}' not found.") # Convert lists of arrays to single NumPy arrays (faster processing) return tuple(np.concatenate(all_data[attr]) if all_data[attr] else np.array([]) for attr in attr_names)
[docs] def run_experiment_measure(variables, x_plot, y_plot, t_plot, main_v_dc_plot, stop_event): """ Measurement function: This function is called in a process to read data from the queue. Args: variables (share_variables.Variables): A share_variables.Variables object. x_plot (multiprocessing.Array): A multiprocessing.Array object. y_plot (multiprocessing.Array): A multiprocessing.Array object. t_plot (multiprocessing.Array): A multiprocessing.Array object. main_v_dc_plot (multiprocessing.Array): A multiprocessing.Array object. stop_event (multiprocessing.Event): A multiprocessing.Event object. Returns: int: Return code. """ # Calibration constants - all configurable via config.toml so # different rigs can be tuned without editing source. Defaults # match the values that were hard-coded here historically. try: _conf, _ = _runtime.load_project_config(change_cwd=False) except Exception: _conf = {} exposure_time = int(_conf.get("sc_exposure_time", 100)) TOFFACTOR = float(_conf.get("sc_tof_ns_per_bin", 27.432 / (1000 * 4))) DETBINS = int(_conf.get("sc_detector_bins", 4900)) BINNINGFAC = int(_conf.get("sc_detector_binning_factor", 2)) DET_WIDTH_MM = float(_conf.get("sc_detector_width_mm", 80)) XYFACTOR = DET_WIDTH_MM / DETBINS * BINNINGFAC # mm per bin XYBINSHIFT = DETBINS / BINNINGFAC / 2 # to center the detector device = scTDC.Device(autoinit=False) retcode, errmsg = device.initialize() if retcode < 0: print("Error during init:", retcode, errmsg) print( f"{initialize_devices.bcolors.FAIL}Error: Restart the TDC manually " f"(Turn it On and Off){initialize_devices.bcolors.ENDC}" ) variables.flag_finished_tdc = True if not getattr(variables, "access_override_enabled", False): variables.flag_tdc_failure = True return -1 print("Access Override is active. Continuing without a Surface Concept detector.") variables.flag_tdc_failure = False return 0 else: print("TDC is successfully initialized") variables.flag_tdc_failure = False DATA_FIELD_SEL = ( scTDC.SC_DATA_FIELD_DIF1 | scTDC.SC_DATA_FIELD_DIF2 | scTDC.SC_DATA_FIELD_TIME | scTDC.SC_DATA_FIELD_START_COUNTER ) DATA_FIELD_SEL_raw = scTDC.SC_DATA_FIELD_TIME | scTDC.SC_DATA_FIELD_CHANNEL | scTDC.SC_DATA_FIELD_START_COUNTER bufdatacb = BufDataCB4(device.lib, device.dev_desc, DATA_FIELD_SEL, dld_events=True) bufdatacb_raw = BufDataCB4(device.lib, device.dev_desc, DATA_FIELD_SEL_raw, dld_events=False) # DLD data xx = [] yy = [] tt = [] voltage_data = [] voltage_pulse_data = [] laser_pulse_data = [] start_counter = [] # The binning of DLD events xx_list_bin = [] yy_list_bin = [] tt_list_bin = [] # TDC data (Raw data) channel_data = [] time_data = [] tdc_start_counter = [] voltage_data_tdc = [] voltage_pulse_data_tdc = [] laser_pulse_data_tdc = [] retcode = bufdatacb.start_measurement(exposure_time) if errorcheck(device, bufdatacb, bufdatacb_raw, retcode) < 0: print("Error during read:", retcode, device.lib.sc_get_err_msg(retcode)) print( f"{initialize_devices.bcolors.FAIL}Error: Restart the TDC manually " f"(Turn it On and Off){initialize_devices.bcolors.ENDC}" ) variables.flag_finished_tdc = True if not getattr(variables, "access_override_enabled", False): variables.flag_tdc_failure = True return -1 print("Access Override is active. Continuing without a Surface Concept detector.") variables.flag_tdc_failure = False return 0 loop_time = 1 / variables.ex_freq events_detected = 0 events_detected_tmp = 0 raw_signal_detected = 0 start_time = time.time() pulse_frequency = variables.pulse_frequency * 1000 loop_counter = 0 loop_delay_counter = 0 # DLD events and raw/TDC signals are independent streams of DIFFERENT # length, so they are chunked independently -- each on its own # CHUNK_SIZE threshold and its own chunk-id sequence. This keeps the # faster-growing raw stream flushed to disk on time instead of # accumulating in RAM (OOM) and being truncated to the DLD count (data # loss). The two streams write disjoint chunk stems (e.g. x_chunk_* vs # channel_chunk_*), so reusing the same id counter values never collides. dld_chunk_id = 0 tdc_chunk_id = 0 save_queue = mp.Queue() save_process = mp.Process(target=save_chunk_worker, args=(save_queue,)) save_process.start() path = variables.path + "/temp_data/" # Create folder to save the data if not os.path.isdir(path): os.makedirs(path, mode=0o777, exist_ok=True) if not os.path.isdir(path + "chunks/"): os.makedirs(path + "chunks/", mode=0o777, exist_ok=True) _last_loop_warn = None # dedup transient inner-loop errors while not stop_event.is_set(): start_time_loop = time.time() # Two-queue gets used to be unconditional .get() calls back to # back. Because the DLD callback fires only on completed DLD # events while the raw callback fires on every TDC channel hit, # the queues have very different rates - whichever lagged would # block the loop indefinitely and starve stop_event polling. # Now: timed gets, no-data sentinels, stop_event re-checked. try: eventtype, data = bufdatacb.queue.get(timeout=QUEUE_GET_TIMEOUT_S) except Empty: eventtype, data = None, None if stop_event.is_set(): break try: eventtype_raw, data_raw = bufdatacb_raw.queue.get(timeout=QUEUE_GET_TIMEOUT_S) except Empty: eventtype_raw, data_raw = None, None if stop_event.is_set(): break try: specimen_voltage = variables.specimen_voltage voltage_pulse = variables.pulse_voltage laser_pulse = variables.laser_pulse_energy except Exception as exc: # Manager IPC can transiently fail under heavy load; skip # this iteration but keep the loop alive. if str(exc) != _last_loop_warn: _last_loop_warn = str(exc) print(f"TDC: variable read failed (non-fatal): {exc}") continue if eventtype == QUEUE_DATA: # correct for binning of surface concept xx_dif = data["dif1"] length = len(xx_dif) if length > 0: events_detected_tmp += length events_detected += length yy_dif = data["dif2"] tt_dif = data["time"] start_counter.extend(data["start_counter"].tolist()) xx_tmp = ((xx_dif - XYBINSHIFT) * XYFACTOR) * 0.1 # from mm to in cm by dividing by 10 yy_tmp = ((yy_dif - XYBINSHIFT) * XYFACTOR) * 0.1 # from mm to in cm by dividing by 10 tt_tmp = tt_dif * TOFFACTOR # in ns dc_voltage_tmp = np.tile(specimen_voltage, len(xx_tmp)) # Push into the shared-memory ring buffers (one per signal). # Append is non-blocking, bounded; the visualization # subprocess drains the rings on its own cadence. x_plot.write(xx_tmp) y_plot.write(yy_tmp) t_plot.write(tt_tmp) main_v_dc_plot.write(dc_voltage_tmp) # change to list xx_tmp = xx_tmp.tolist() yy_tmp = yy_tmp.tolist() tt_tmp = tt_tmp.tolist() # extend the main list with the new data xx.extend(xx_tmp) yy.extend(yy_tmp) tt.extend(tt_tmp) dc_voltage_tmp = dc_voltage_tmp.tolist() p_voltage_tmp = np.tile(voltage_pulse, len(xx_tmp)).tolist() p_laser_tmp = np.tile(laser_pulse, len(xx_tmp)).tolist() voltage_data.extend(dc_voltage_tmp) voltage_pulse_data.extend(p_voltage_tmp) laser_pulse_data.extend(p_laser_tmp) # The binning of DLD events xx_list_bin.extend(xx_dif.tolist()) yy_list_bin.extend(yy_dif.tolist()) tt_list_bin.extend(tt_dif.tolist()) if eventtype_raw == QUEUE_DATA: channel_data_tmp = data_raw["channel"].tolist() if len(channel_data_tmp) > 0: raw_signal_detected += len(channel_data_tmp) tdc_start_counter.extend(data_raw["start_counter"].tolist()) time_data.extend(data_raw["time"].tolist()) # raw data channel_data.extend(channel_data_tmp) voltage_data_tdc.extend((np.tile(specimen_voltage, len(channel_data_tmp))).tolist()) voltage_pulse_data_tdc.extend((np.tile(voltage_pulse, len(channel_data_tmp))).tolist()) laser_pulse_data_tdc.extend((np.tile(laser_pulse, len(channel_data_tmp))).tolist()) if eventtype == QUEUE_ENDOFMEAS: retcode = bufdatacb.start_measurement(exposure_time, retries=10) if retcode < 0: print("Error during read (error code: %s - error msg: %s):" % (retcode, device.lib.sc_get_err_msg(retcode))) variables.flag_tdc_failure = True # Clean teardown - the post-loop block at the end runs # bufdatacb.close() etc. We just break out here. break # Calculate the detection rate current_time = time.time() if current_time - start_time >= 0.5: # Re-read pulse_frequency every interval so the rate calc # stays correct if the user changes it mid-run. Guard # against zero (would divide by zero on first chunk after a # bad value). try: live_pulse_frequency = max(float(variables.pulse_frequency) * 1000.0, 1.0) except Exception: live_pulse_frequency = pulse_frequency pulse_frequency = live_pulse_frequency detection_rate = events_detected_tmp * 100 / pulse_frequency variables.detection_rate_current = detection_rate * 2 # rate per second variables.detection_rate_current_plot = detection_rate * 2 variables.total_ions = events_detected variables.total_raw_signals = raw_signal_detected events_detected_tmp = 0 start_time = current_time # Flush the DLD stream when it reaches a full chunk. We do NOT # mirror into the Manager here: the data is safe on disk under # temp_data/chunks/ and hdf_creator reads the chunk files directly. # (The old per-chunk extend_to shipped the whole growing list back # through the Manager every flush -- O(N) IPC -- which throttled # acquisition and OOM'd the Manager on large runs.) if len(xx) >= CHUNK_SIZE: dld_chunk_id += 1 dld_chunk = { "x_bin": xx_list_bin[:CHUNK_SIZE], "x": xx[:CHUNK_SIZE], "y_bin": yy_list_bin[:CHUNK_SIZE], "y": yy[:CHUNK_SIZE], "t_bin": tt_list_bin[:CHUNK_SIZE], "t": tt[:CHUNK_SIZE], "voltage": voltage_data[:CHUNK_SIZE], "voltage_pulse": voltage_pulse_data[:CHUNK_SIZE], "laser_pulse": laser_pulse_data[:CHUNK_SIZE], "start_counter": start_counter[:CHUNK_SIZE], } save_queue.put((dld_chunk_id, path, dld_chunk)) del xx[:CHUNK_SIZE], yy[:CHUNK_SIZE], tt[:CHUNK_SIZE] del xx_list_bin[:CHUNK_SIZE], yy_list_bin[:CHUNK_SIZE], tt_list_bin[:CHUNK_SIZE] del voltage_data[:CHUNK_SIZE], voltage_pulse_data[:CHUNK_SIZE], laser_pulse_data[:CHUNK_SIZE] del start_counter[:CHUNK_SIZE] # Flush the raw/TDC stream INDEPENDENTLY -- it grows much faster than # the DLD stream (many raw hits per DLD event), so it reaches # CHUNK_SIZE far more often. Gating this on len(xx) (the old code) # under-flushed the raw lists: they grew unbounded (OOM) and only # CHUNK_SIZE of the millions of raw rows were ever written per chunk. if len(channel_data) >= CHUNK_SIZE: tdc_chunk_id += 1 tdc_chunk = { "channel": channel_data[:CHUNK_SIZE], "time": time_data[:CHUNK_SIZE], "tdc_start_counter": tdc_start_counter[:CHUNK_SIZE], "voltage_tdc": voltage_data_tdc[:CHUNK_SIZE], "voltage_pulse_tdc": voltage_pulse_data_tdc[:CHUNK_SIZE], "laser_pulse_tdc": laser_pulse_data_tdc[:CHUNK_SIZE], } save_queue.put((tdc_chunk_id, path, tdc_chunk)) del channel_data[:CHUNK_SIZE], time_data[:CHUNK_SIZE], tdc_start_counter[:CHUNK_SIZE] del voltage_data_tdc[:CHUNK_SIZE], voltage_pulse_data_tdc[:CHUNK_SIZE] del laser_pulse_data_tdc[:CHUNK_SIZE] if time.time() - start_time_loop > loop_time: loop_delay_counter += 1 loop_counter += 1 print( "TDC process: for %s times loop time took longer than %s second" % (loop_delay_counter, loop_time), 'out of %s iterations' % loop_counter, ) variables.total_ions = events_detected variables.total_raw_signals = raw_signal_detected print("TDC Measurement stopped") # --- Teardown instrumentation ------------------------------------- # Lightweight stdout logging (captured + timestamped by the parent # process) so a slow or hanging shutdown can be diagnosed -- this is # where the TDC subprocess "won't join". It runs AFTER the # acquisition loop, so it never touches the live hot path. flush=True # so each line survives even if the process is later force-joined. def _tlog(msg): print("[TDC teardown] %s" % msg, flush=True) _teardown_t0 = time.time() _tlog( "begin: dld_chunk_id=%d (DLD residual=%d), tdc_chunk_id=%d (raw residual=%d), " "stop_event=%s, tdc_failure=%s" % (dld_chunk_id, len(xx), tdc_chunk_id, len(channel_data), stop_event.is_set(), getattr(variables, "flag_tdc_failure", "?")) ) # Final residual chunks -- whatever is still in the in-memory lists after # the last in-loop flush. Each stream is flushed independently (mirroring # the in-loop logic) and in FULL (no [:CHUNK_SIZE] truncation), so the # faster raw stream is saved completely. A stream that already produced # chunks during the run just appends its final partial chunk here. if dld_chunk_id > 0 and len(xx) > 0: dld_chunk_id += 1 save_queue.put((dld_chunk_id, path, { "x_bin": xx_list_bin, "x": xx, "y_bin": yy_list_bin, "y": yy, "t_bin": tt_list_bin, "t": tt, "voltage": voltage_data, "voltage_pulse": voltage_pulse_data, "laser_pulse": laser_pulse_data, "start_counter": start_counter, })) _tlog("queued residual DLD chunk #%d (%d rows)" % (dld_chunk_id, len(xx))) if tdc_chunk_id > 0 and len(channel_data) > 0: tdc_chunk_id += 1 save_queue.put((tdc_chunk_id, path, { "channel": channel_data, "time": time_data, "tdc_start_counter": tdc_start_counter, "voltage_tdc": voltage_data_tdc, "voltage_pulse_tdc": voltage_pulse_data_tdc, "laser_pulse_tdc": laser_pulse_data_tdc, })) _tlog("queued residual raw/TDC chunk #%d (%d rows)" % (tdc_chunk_id, len(channel_data))) _tlog("waiting for save worker to drain queue (save_process.join)...") _save_join_t = time.time() save_queue.put(None) save_process.join() _tlog("save worker joined in %.1fs" % (time.time() - _save_join_t)) # Per-stream fallback for a stream that NEVER reached a single full chunk # (its chunk-id is still 0): no chunk files exist for it, so hdf_creator # falls back to these in-memory lists -- mirror them into the Manager and # also drop a flat .npy as a manual-recovery copy. Done PER STREAM so a # run that chunked the fast raw stream but not the smaller DLD stream # (or vice versa) only mirrors the SMALL un-chunked stream, never the # large one already on disk -- avoiding the O(N) Manager blow-up. if dld_chunk_id == 0 and len(xx) > 0: _tlog("DLD fallback: mirroring %d rows into Manager + flat .npy ..." % len(xx)) _t = time.time() try: variables.extend_to('x', xx) variables.extend_to('y', yy) variables.extend_to('t', tt) variables.extend_to('dld_start_counter', start_counter) variables.extend_to('main_v_dc_dld', voltage_data) variables.extend_to('main_v_p_dld', voltage_pulse_data) variables.extend_to('main_l_p_dld', laser_pulse_data) except Exception as exc: print(f"TDC: DLD residual extend_to failed (non-fatal): {exc}") try: np.save(variables.path + "/temp_data/x.npy", np.array(xx)) np.save(variables.path + "/temp_data/y.npy", np.array(yy)) np.save(variables.path + "/temp_data/t.npy", np.array(tt)) np.save(variables.path + "/temp_data/voltage.npy", np.array(voltage_data)) np.save(variables.path + "/temp_data/voltage_pulse.npy", np.array(voltage_pulse_data)) np.save(variables.path + "/temp_data/laser_pulse.npy", np.array(laser_pulse_data)) np.save(variables.path + "/temp_data/start_counter.npy", np.array(start_counter)) np.save(variables.path + "/temp_data/x_bin.npy", np.array(xx_list_bin)) np.save(variables.path + "/temp_data/y_bin.npy", np.array(yy_list_bin)) np.save(variables.path + "/temp_data/t_bin.npy", np.array(tt_list_bin)) except Exception as exc: print(f"TDC: DLD single-shot .npy save failed: {exc}") _tlog("DLD fallback done in %.1fs" % (time.time() - _t)) if tdc_chunk_id == 0 and len(channel_data) > 0: _tlog("raw/TDC fallback: mirroring %d rows into Manager + flat .npy ..." % len(channel_data)) _t = time.time() try: variables.extend_to('channel', channel_data) variables.extend_to('time_data', time_data) variables.extend_to('tdc_start_counter', tdc_start_counter) variables.extend_to('main_v_dc_tdc', voltage_data_tdc) variables.extend_to('main_v_p_tdc', voltage_pulse_data_tdc) variables.extend_to('main_l_p_tdc', laser_pulse_data_tdc) except Exception as exc: print(f"TDC: raw residual extend_to failed (non-fatal): {exc}") try: np.save(variables.path + "/temp_data/channel.npy", np.array(channel_data)) np.save(variables.path + "/temp_data/time.npy", np.array(time_data)) # Canonical name matches the chunk stem (was the misnamed # "main_raw_counter.npy"; the recover script reads both). np.save(variables.path + "/temp_data/tdc_start_counter.npy", np.array(tdc_start_counter)) np.save(variables.path + "/temp_data/voltage_tdc.npy", np.array(voltage_data_tdc)) np.save(variables.path + "/temp_data/voltage_pulse_tdc.npy", np.array(voltage_pulse_data_tdc)) np.save(variables.path + "/temp_data/laser_pulse_tdc.npy", np.array(laser_pulse_data_tdc)) except Exception as exc: print(f"TDC: raw single-shot .npy save failed: {exc}") _tlog("raw/TDC fallback done in %.1fs" % (time.time() - _t)) print("data saved in share variables") time.sleep(0.1) _tlog("closing data-callback buffers + deinitializing TDC device...") _dev_t = time.time() bufdatacb.close() bufdatacb_raw.close() device.deinitialize() _tlog("device deinitialized in %.1fs" % (time.time() - _dev_t)) variables.flag_finished_tdc = True _tlog("flag_finished_tdc set; total teardown %.1fs" % (time.time() - _teardown_t0)) return 0
[docs] def experiment_measure(variables, x_plot, y_plot, t_plot, main_v_dc_plot, stop_event): run_experiment_measure(variables, x_plot, y_plot, t_plot, main_v_dc_plot, stop_event)