add buffer measurement mode

This commit is contained in:
CPD 2025-02-07 11:13:15 +01:00
parent 4782adbf6a
commit c5c016399b
5 changed files with 311 additions and 101 deletions

View File

@ -79,7 +79,7 @@ led: LedControlDevice|None = None
data = DataCollector(data_path=settings["datadir"], data_name="interactive", dirname="interactive_test", dir_exists_is_ok=True) data = DataCollector(data_path=settings["datadir"], data_name="interactive", dirname="interactive_test", dir_exists_is_ok=True)
t0 = 0 t0 = 0
def monitor(script: str|int=0, interval=None, flush_after=None, max_measurements=None, max_points_shown=None): def monitor(script: str|int=0, interval=None, flush_after=None, use_buffer=False, max_measurements=None, max_points_shown=None):
""" """
Monitor the voltage with matplotlib. Monitor the voltage with matplotlib.
@ -99,21 +99,32 @@ def monitor(script: str|int=0, interval=None, flush_after=None, max_measurements
plt_monitor = _Monitor(use_print=True, max_points_shown=max_points_shown) plt_monitor = _Monitor(use_print=True, max_points_shown=max_points_shown)
led_script = LedScript(script=script, auto_update=True, verbose=True) led_script = LedScript(script=script, auto_update=True, verbose=True)
data.clear() data.clear()
queue = mp.Queue() data_queue = mp.Queue()
pipe_send, pipe_recv = mp.Pipe() command_queue = mp.Queue()
# TODO: pass instruments # Argument order must match the definition
proc_measure = mt.Thread(target=_measure, args=(dev, led, led_script, data, interval, flush_after, max_measurements, False, pipe_recv, queue)) proc_measure = mt.Thread(target=_measure, args=(dev, led, led_script, data,
interval,
flush_after,
use_buffer,
max_measurements,
False, # verbose
command_queue,
data_queue
))
proc_measure.start() proc_measure.start()
try: try:
while True: while proc_measure.is_alive():
current_data = queue.get(block=True, timeout=30) while not data_queue.empty():
# print(data_queue.qsize(), "\n\n")
current_data = data_queue.get(block=False)
i, tval, vval, led_val = current_data i, tval, vval, led_val = current_data
plt_monitor.update(i, tval, vval, led_val) plt_monitor.update(i, tval, vval, led_val)
except KeyboardInterrupt: except KeyboardInterrupt:
pass pass
pipe_send.send("stop") command_queue.put("stop")
proc_measure.join() proc_measure.join()
print(data.metadata) led_script.stop_updating() # stop watching for file updates (if enabled)
data.save_csv(verbose=True) data.save_csv(verbose=True)
@ -231,12 +242,8 @@ Enter 'help()' for a list of commands""")
try: try:
pass pass
# dev = _volt.init("GPIB0::22::INSTR") dev = _volt.init("GPIB0::22::INSTR")
# TODO led = _led.LEDD1B()
# manager = BaseManager()
# manager.start()
# led = manager._led.LEDD1B()
# led = _led.LEDD1B()
except Exception as e: except Exception as e:
print(e) print(e)
exit(1) exit(1)

View File

@ -3,4 +3,3 @@ INIT:CONT OFF
' two readings per second ' two readings per second
TRIGger:SOURce TIMer TRIGger:SOURce TIMer
TRIGger:TIMer 0.5 TRIGger:TIMer 0.5

View File

@ -4,13 +4,21 @@ Created on Fri Jan 24 16:46:06 2025
@author: CPD @author: CPD
""" """
import os
import time import time
import re import re
import numpy as np import numpy as np
import watchdog
from watchdog.observers import Observer from watchdog.observers import Observer
from watchdog.events import LoggingEventHandler, FileSystemEventHandler from watchdog.events import FileSystemEventHandler
import os
class InvalidScript(Exception):
def __init__(self, lineNr, message, fix=""):
self.lineNr = lineNr
self.message = message
self.fix = fix
self.full_message = f"Line {lineNr}: {message} {fix}"
super().__init__(self.full_message)
class LedScriptUpdateHandler(FileSystemEventHandler): class LedScriptUpdateHandler(FileSystemEventHandler):
def __init__(self, led_script, verbose=False): def __init__(self, led_script, verbose=False):
@ -30,10 +38,10 @@ class LedScriptUpdateHandler(FileSystemEventHandler):
class LedScript: class LedScript:
ARRAY_DTYPE = [("dt", "f8"), ("dtsum", "f8"), ("led", "i4"), ("line", "i4")]
def __init__(self, script:np.ndarray|str|int=0, auto_update=False, verbose=False): def __init__(self, script:np.ndarray|str|int=0, auto_update=False, verbose=False):
""" """
Parameters Parameters
---------- ----------
script : np.ndarray|str|int script : np.ndarray|str|int
@ -45,48 +53,35 @@ class LedScript:
The <line> field is the line number if the step in a script and is optional. The <line> field is the line number if the step in a script and is optional.
If str: path to a led script file If str: path to a led script file
If int: constant led state value If int: constant led state value
constantValue : TYPE, optional
DESCRIPTION. The default is None.
auto_update: bool, optional auto_update: bool, optional
If True and script is a filepath, the script will automatically be reloaded when the file changes If True and script is a filepath, the script will automatically be reloaded when the file changes
verbose: bool, optional
If True, print messages when important operations occur
Returns Returns
------- -------
None. None.
""" """
self.verbose = verbose
self.t_start = 0 self.t_start = 0
self.auto_update = False self.auto_update = False
self.filepath = None self.filepath = None
if type(script) == int: if type(script) == int:
self.script = np.array([(0., 0., script)]) self.script = np.array([(0., 0., script, 0)], dtype=LedScript.ARRAY_DTYPE)
elif type(script) == np.ndarray: elif type(script) == np.ndarray:
self.script = script self.script = script
elif type(script) == str: elif type(script) == str:
self.script = LedScript.parse_script(script, ignore_errors=False) self.script = LedScript.parse_script(script, ignore_errors=False)
self.filepath = script self.filepath = script
self.auto_update = auto_update self.auto_update = auto_update
if self.auto_update:
# event_handler = LoggingEventHandler()
event_handler = LedScriptUpdateHandler(self, verbose=True)
self.observer = Observer()
dirname = os.path.dirname(os.path.abspath(self.filepath)) # directory of the file
self.observer.schedule(event_handler, dirname)
self.observer.start()
if verbose: print(f"Led script is watching for updates on '{self.filepath}'")
else:
self.observer = None self.observer = None
if self.auto_update:
self.start_updating()
self.current_dt = 0 self.current_dt = 0
assert(self.script.shape[0] > 0) assert(self.script.shape[0] > 0)
def __del__(self): def __del__(self):
self.stop() self.stop_updating()
def stop(self):
print("Led script stopped watching for updates")
if self.observer is not None:
self.observer.stop()
self.observer.join()
def start(self) -> int: def start(self) -> int:
@ -141,16 +136,71 @@ class LedScript:
return int(self.script["led"][idx]) return int(self.script["led"][idx])
@staticmethod @staticmethod
def _get_current_index(script, dt:float): def _get_current_index(script, dt:float) -> int:
if script.shape[0] == 1: if script.shape[0] == 1:
return 0 return 0
distance = script["dtsum"] - dt distance = script["dtsum"] - dt
idx = np.where(distance >= 0, distance, np.inf).argmin() idx = np.where(distance >= 0, distance, np.inf).argmin()
return idx return idx
def get_current_index(self, dt:float): def get_current_index(self, dt:float) -> int:
"""
Get the index into self.script at `dt`
Parameters
----------
dt : float
Time relative to script start.
Returns
-------
int
Index into self.script at relative time dt.
"""
return LedScript._get_current_index(self.script, dt) return LedScript._get_current_index(self.script, dt)
def start_updating(self):
"""
Start watching for updates to the script file.
Raises
------
ValueError
If the LedScript object was initialized with a filepath or if already watching for script updates.
Returns
-------
None.
"""
if self.observer is not None:
raise ValueError("Already watching for updates")
if self.filepath is None:
raise ValueError("Can not watch for updates if the LedScript was not initialized with a file path")
# event_handler = LoggingEventHandler()
event_handler = LedScriptUpdateHandler(self) #, verbose=True)
self.observer = Observer()
dirname = os.path.dirname(os.path.abspath(self.filepath)) # directory of the file
self.observer.schedule(event_handler, dirname)
self.observer.start()
if self.verbose: print(f"Led script is watching for updates on '{self.filepath}'")
def stop_updating(self):
"""
Stop watching for updates to the script file.
Does nothing if not currently watching for file updates.
Returns
-------
None.
"""
if self.observer is not None:
self.observer.stop()
self.observer.join()
self.observer = None
if self.verbose: print("Led script stopped watching for updates")
def update(self, verbose=True): def update(self, verbose=True):
print(f"Updating led script from '{self.filepath}'") print(f"Updating led script from '{self.filepath}'")
newscript = LedScript.parse_script(self.filepath, ignore_errors=False) newscript = LedScript.parse_script(self.filepath, ignore_errors=False)
@ -167,7 +217,31 @@ class LedScript:
self.script = newscript self.script = newscript
@staticmethod @staticmethod
def parse_script(filepath, ignore_errors=False): def parse_script(filepath: str, ignore_errors:bool=False) -> np.ndarray|tuple[np.ndarray, list[InvalidScript]]:
"""
Parse a led script from a file into a structured array
Parameters
----------
filepath : str
Path to the led script file.
ignore_errors : bool, optional
If True, does not throw an exception upon script errors.
Instead, ignores errornous lines and additionally returns all exceptions in a list.
The default is False.
Raises
------
InvalidScript
If encountering invalid lines in the script.
Returns
-------
np.ndarray or tuple[np.ndarray, list[InvalidScript]]
Returns the script as structured array. For the format, see the
docstring of the LedString constructor.
If ignore_errors=True, additionally returns a list of all errors.
"""
with open(filepath, "r") as file: with open(filepath, "r") as file:
lines = file.readlines() lines = file.readlines()
@ -255,15 +329,8 @@ class LedScript:
cum_duration = states[-1][1] + duration cum_duration = states[-1][1] + duration
# 6) append # 6) append
states.append((duration, cum_duration, state, i+1)) states.append((duration, cum_duration, state, i+1))
states = np.array(states, dtype=[("dt", "f8"), ("dtsum", "f8"), ("led", "i4"), ("line", "i4")]) states = np.array(states, dtype=LedScript.ARRAY_DTYPE)
if ignore_errors: if ignore_errors:
return states, errors return states, errors
return states return states
class InvalidScript(Exception):
def __init__(self, lineNr, message, fix=""):
self.lineNr = lineNr
self.message = message
self.fix = fix
self.full_message = f"Line {lineNr}: {message} {fix}"
super().__init__(self.full_message)

View File

@ -24,51 +24,107 @@ def measure(
data: DataCollector, data: DataCollector,
delta_t: float=0.1, delta_t: float=0.1,
flush_after:int|None=None, flush_after:int|None=None,
use_buffer=False,
max_measurements: int=None, max_measurements: int=None,
verbose: bool=False, verbose: bool=False,
pipe: None|Connection=None, command_queue: None|Queue=None,
queue: None|Queue=None data_queue: None|Queue=None
): ):
# TODO: find a way to move inherited objects into a process """
if led_dev is None: Perform a measurement
led_dev = LEDD1B()
if vm_dev is None: Parameters
vm_dev = init("GPIB0::22::INSTR") ----------
vm_dev : VoltageMeasurementDevice
DESCRIPTION.
led_dev : LedControlDevice
DESCRIPTION.
led_script : LedScript
DESCRIPTION.
data : DataCollector
DESCRIPTION.
delta_t : float, optional
Target interval between measurements and led updates. The default is 0.1.
flush_after : int|None, optional
If int, flush values to disk after <flush_after>. The default is None.
use_buffer : TYPE, optional
If True, use the buffer measurement mode. The default is False.
max_measurements : int, optional
Number of measurements to perform before returning.
Note: If use_buffer=True, a few more than max_measurements might be performed
The default is None.
verbose : bool, optional
If True, print some messages. The default is False.
command_queue : None|Connection, optional
A queue to receive to commands from.
Commands may be:
"stop" -> stops the measurement
("led_script", <LedScript object>) a new led script to use
The default is None.
data_queue : None|Queue, optional
A queue to put data in. The default is None.
Returns
-------
None.
"""
# old hack when using multiprocessing instead of mulithreading:
# devices are not pickleable and thus cant be moved to / shared with the measurement process
# if led_dev is None:
# led_dev = LEDD1B()
# if vm_dev is None:
# vm_dev = init("GPIB0::22::INSTR")
# if no "time" in metadata, set the current local time in ISO 8601 format # if no "time" in metadata, set the current local time in ISO 8601 format
# and without microseconds # and without microseconds
if not "time" in data.metadata: if not "time" in data.metadata:
data.metadata["time"] = datetime.datetime.now().replace(microsecond=0).isoformat() data.metadata["time"] = datetime.datetime.now().replace(microsecond=0).isoformat()
data.metadata["test"] = "TEST" data.metadata["test"] = "TEST"
vm_dev.reset(True) vm_dev.reset(True)
if use_buffer:
vm_dev.buffer_measure(delta_t, verbose=True)
try: try:
i = 0 i = 0
led_val = led_script.start() led_val = led_script.start()
t_iter_start = time.time() t_iter_start = time.time()
while max_measurements is None or i < max_measurements: while max_measurements is None or i < max_measurements:
# 1) read value # 1) read value(s)
tval, vval = vm_dev.read_value() if use_buffer:
try:
values = vm_dev.buffer_read_new_values()
except ValueError as e:
print(f"Error in buffer measurement {i}:", e)
values = []
else:
values = [vm_dev.read_value()]
# print(values)
# 2) process value(s)
for (tval, vval) in values:
if i == 0: if i == 0:
t0 = tval t0 = tval
tval -= t0 tval -= t0
current_data = (i, tval, vval, led_val) current_data = (i, tval, vval, led_val)
data.add_data(*current_data) data.add_data(*current_data)
# 2) write data # 3) write data
print(f"n = {i:6d}, t = {tval: .2f} s, U = {vval: .5f} V, LED = {led_val:03}%" + " "*10, end='\r') print(f"n = {i:6d}, t = {tval: .2f} s, U = {vval: .5f} V, LED = {led_val:03}%" + " "*10, end='\r')
if flush_after is not None and (i+1) % flush_after == 0: if flush_after is not None and (i+1) % flush_after == 0:
data.flush(verbose=verbose) data.flush(verbose=verbose)
# if a queue was given, put the data # if a queue was given, put the data
if queue is not None: if data_queue is not None:
queue.put(current_data) data_queue.put(current_data)
i += 1
# if a pipe was given, check for messages # if a pipe was given, check for messages
if pipe is not None and pipe.poll(0): if command_queue is not None and command_queue.qsize() > 0:
recv = pipe.recv() recv = command_queue.get(block=False)
if recv == "stop": if recv == "stop":
break break
elif type(recv) == tuple and recv[0] == "led_script": elif type(recv) == tuple and recv[0] == "led_script":
led_script = recv[1] led_script = recv[1]
else: else:
print(f"Received invalid message: '{recv}'") print(f"Received invalid message: '{recv}'")
# 3) sleep
# 4) sleep
# substract the execution time from the sleep time for a more # substract the execution time from the sleep time for a more
# acurate frequency # acurate frequency
dt_sleep = delta_t - (time.time() - t_iter_start) dt_sleep = delta_t - (time.time() - t_iter_start)
@ -76,7 +132,7 @@ def measure(
# print(f"Sleeping for {dt_sleep}") # print(f"Sleeping for {dt_sleep}")
time.sleep(dt_sleep) time.sleep(dt_sleep)
t_iter_start = time.time() t_iter_start = time.time()
# 4) update LED # 5) update LED
new_led_val = led_script.get_state() new_led_val = led_script.get_state()
if new_led_val != led_val: if new_led_val != led_val:
try: try:
@ -85,10 +141,12 @@ def measure(
except Exception as e: except Exception as e:
print(f"Error setting led to {new_led_val}%:") print(f"Error setting led to {new_led_val}%:")
print(e) print(e)
i += 1
except KeyboardInterrupt: except KeyboardInterrupt:
pass pass
data.flush(verbose=verbose) data.flush(verbose=verbose)
led_dev.off() led_dev.off()
print(data.metadata) print(data.metadata)
print("Measurement stopped" + " "*50) print("Measurement stopped" + " "*50)

View File

@ -52,6 +52,7 @@ class Keithley2700(VoltageMeasurementDevice):
self.instr = instr self.instr = instr
if check_front_switch: if check_front_switch:
self._check_front_input_selected() self._check_front_input_selected()
self.buffer_next_idx = None
def __del__(self): def __del__(self):
"""Properly close the instrument connection""" """Properly close the instrument connection"""
@ -75,6 +76,17 @@ class Keithley2700(VoltageMeasurementDevice):
raise Exception("The Keithley's INPUT switch must select the [F]ront inputs") raise Exception("The Keithley's INPUT switch must select the [F]ront inputs")
return switch return switch
def query(self, query):
try:
return self.instr.query(query).strip("\n")
except pyvisa.VisaIOError as e:
print(f"VisaIOError raised during query: '{query}'")
raise e
def query_int(self, query):
return int(float(self.query(query)))
# RUN COMMANDS ON THE DEVICE # RUN COMMANDS ON THE DEVICE
def run(self, code, verbose=False): def run(self, code, verbose=False):
""" """
@ -85,10 +97,18 @@ class Keithley2700(VoltageMeasurementDevice):
code : str code : str
SCPI commands SCPI commands
""" """
script = '\n'.join([l.strip(" ") for l in code.strip(" ").strip("\n").split("\n") if len(l) > 0 and l[0] not in "#'"]) script = ''
for line in code.strip(" ").split("\n"):
l = line.strip(" ")
if len(l) == 0 or l[0] in "#'": continue
script += l + "\n"
if verbose: if verbose:
print(f"Running code:\n{script}") print(f"Running code:\n{script}")
try:
self.instr.write(script) self.instr.write(script)
except pyvisa.VisaIOError as e:
print(f"VisaIOError raised while writing command(s):\n'{script}'\n")
raise e
def run_script(self, script_path, verbose=False): def run_script(self, script_path, verbose=False):
""" """
@ -118,42 +138,101 @@ class Keithley2700(VoltageMeasurementDevice):
def reset(self, verbose=False): def reset(self, verbose=False):
""" """
Reset smua and its buffers Reset the device
@param instr : pyvisa instrument
""" """
self.run_script(scripts["instrument_reset"], verbose=verbose)
self.buffer_reset() reset_script = """
VOLT:DC:RANGe:AUTO ON
' DC Voltage measurement
SENSe:FUNC 'VOLT:DC'
' Set voltage divider if required
' SENSE:VOLT:DC:IDIVider OFF
' Disable continuous initiation
INIT:CONT OFF
' Disable Buffer and trigger
TRACe:FEED NONE
TRACe:FEED:CONTrol NEVer
TRIGger:SOURce IMMediate
' Set timestamp format to relative
SYSTem:TSTamp:TYPE RELative
"""
self.run(reset_script)
# self.run_script(scripts["instrument_reset"], verbose=verbose)
self.buffer_clear()
# INTERACT WITH DEVICE BUFFER # INTERACT WITH DEVICE BUFFER
# might not be needed # might not be needed
def buffer_reset(self): def buffer_clear(self):
buffer_reset = """ buffer_reset_script = """
TRACe:CLEar TRACe:CLEar
TRACe:CLEar:AUTO ON TRACe:CLEar:AUTO ON
SYSTem:TSTamp:TYPE RELative
""" """
self.run(buffer_reset) self.run(buffer_reset_script)
def buffer_get_size(self, buffer_nr=1): def buffer_get_size(self):
n = self.instr.query("TRACe:POINts?").strip("\n") return self.query_int("TRACe:POINts?")
return int(float(n))
def buffer_set_size(self, s): def buffer_set_size(self, s):
if not type(s) == int or s < 2 or s > 55000: if not type(s) == int or s < 2 or s > 55000:
raise ValueError(f"Invalid buffer size: {s}. Must be int and between 2 and 55000") raise ValueError(f"Invalid buffer size: {s}. Must be int and between 2 and 55000")
self.instr.write(f"TRACe:POINts {s}") self.instr.write(f"TRACe:POINts {s}")
def buffer_measure(self, interval: float=0.5, verbose=False):
if interval < 0.001 or interval > 999999.999:
raise ValueError("Interval must be between 0.001 and 999999.999")
self.run(f"""
TRIGger:TIMer {interval}
TRIGger:COUNt INFinity
TRIGger:SOURce TIMer
""")
self.buffer_clear()
self.run("""
TRACe:FEED SENSe
' write continuously
TRACe:FEED:CONTrol ALWays
INITiate:CONTinuous ON
""")
self.buffer_next_idx = 0
self.buffer_size = self.buffer_get_size()
if verbose: print("Started buffer measurement")
def buffer_read_new_values(self):
if self.buffer_next_idx is None:
raise ValueError("You must first start a buffer measurement by calling buffer_measure()")
# TRACe:NEXT? returns the next index that will be written to
new_next_idx = self.query_int("TRACe:NEXT?")
if new_next_idx == self.buffer_next_idx:
raise ValueError(f"No new value or buffer has been filled completely (next reading at {self.buffer_next_idx})")
# if reached end of buffer, first read to end and then from beginning
vals = ""
if new_next_idx < self.buffer_next_idx:
count = self.buffer_size - self.buffer_next_idx
# print(f"start={self.buffer_next_idx}, stop={new_next_idx}, count={count}")
vals += self.query(f"TRACe:DATA:SELected? {self.buffer_next_idx}, {count}").strip("\n")
self.buffer_next_idx = 0
count = new_next_idx - self.buffer_next_idx
if count > 0:
# print(f"start={self.buffer_next_idx}, stop={new_next_idx}, count={count}")
vals += self.query(f"TRACe:DATA:SELected? {self.buffer_next_idx}, {count}").strip("\n")
self.buffer_next_idx = new_next_idx
processed_vals = [self.process_reading(val) for val in vals.strip("#").split("#")]
return processed_vals
# MEASUREMENT # MEASUREMENT
def process_reading(self, reading: str): @staticmethod
def process_reading(reading: str):
""" """
process a reading. Only works with VDC and relative time stamps right now! process a reading. Only works with VDC and relative time stamps right now!
'-1.19655066E+01VDC,+9627.275SECS,+64993RDNG#\n' '-1.19655066E+01VDC,+9627.275SECS,+64993RDNG\n'
May have trailing commas and trailing '#' characters
Returns Returns
------- -------
[timestamp, voltage] [timestamp, voltage]
""" """
parts = reading.split(",") parts = reading.strip("#").strip(",").split(",")
if len(parts) != 3: if len(parts) != 3:
raise ValueError(f"Invalid reading: '{reading}'") raise ValueError(f"Invalid reading: '{reading}'")
vdc = float(parts[0][:-3]) vdc = float(parts[0][:-3])