photoreflectance/prsctrl/prsctrl_interactive.py
JohannesDittloff 7f7561e4d9 rename prsctrl
2025-05-08 13:07:22 +02:00

330 lines
12 KiB
Python

"""
run this before using this library:
ipython -i prctrl_interactive.py
"""
version = "0.1"
import numpy as np
import matplotlib.pyplot as plt
from datetime import datetime as dtime
from os import path, makedirs
import threading as mt
import multiprocessing as mp
import argparse
if __name__ == "__main__":
import sys
if __package__ is None:
# make relative imports work as described here: https://peps.python.org/pep-0366/#proposed-change
__package__ = "prsctrl"
from os import path
filepath = path.realpath(path.abspath(__file__))
sys.path.insert(0, path.dirname(path.dirname(filepath)))
# import device modules
from .devices import shutter as mod_shutter
from .devices import lock_in as mod_lock_in
from .devices import lamp as mod_lamp
from .devices import monochromator as mod_monochromator
# import base classes
from .devices.lock_in import Lock_In_Amp
from .devices.shutter import Shutter
from .devices.lamp import Lamp
from .devices.monochromator import Monochromator
# from .measurement import measure as _measure
from .utility.data_collector import PrsDataCollector
from .utility.config_file import ConfigFile
from .utility.device_select import select_device_interactive, connect_device_from_config_or_interactive
from .update_funcs import Monitor
import logging
log = logging.getLogger(__name__)
# CONFIGURATION
_runtime_vars = {
"last-measurement": ""
}
# defaults, these may be overridden by a config file
settings = {
"datadir": path.expanduser("~/Desktop/PR/data"),
"name": "interactive-test",
"flush_after": 3000,
"use_buffer": False,
}
cfilename: str = "photoreflectance.yaml"
config_path: str = ""
config_file: ConfigFile = ConfigFile("")
test = False
# DEVICES
# global variable for the instrument/client returned by pyvisa/bleak
lockin: Lock_In_Amp|None = None
shutter: Shutter|None = None
lamp: Lamp|None = None
mcm: Monochromator|None = None
data_collector = PrsDataCollector(data_path=settings["datadir"], data_name="interactive", dirname="interactive_test", add_number_if_dir_exists=True)
t0 = 0
data = None
md = None
from .test_measurement import _measure_both_sim
def measure_both_sim(**kwargs):
return _measure_both_sim(mcm, lockin, shutter, **kwargs)
def monitor(script: str|int=0, interval: float|None=None, metadata:dict={}, flush_after: int|None=None, use_buffer: bool|None=None, max_measurements=None, stop_on_script_end: bool=False, max_points_shown=None):
"""
Monitor the voltage with matplotlib.
- Opens a matplotlib window and takes measurements depending on settings["interval"]
- Waits for the user to press a key
If use_buffer=False, uses python's time.sleep() for waiting the interval, which is not very precise.
With use_buffer=True, the timing of the voltage data readings will be very precise, however,
the led updates may deviate up to <interval>.
The data is automatically saved to "<date>_<time>_<name>" in the data directory.
Parameters
----------
script : str|int
Path to a led script file, or a constant value between 0 and 100 for the LED.
interval : float|None
Time between measurements.
If None, the value is taken from the settings.
metadata : dict
Metadata to append to the data header.
The set interval as well as the setting for 'name' and 'led' are automatically added.
flush_after : int|None
Flush the data to disk after <flush_after> readings
If None, the value is taken from the settings.
use_buffer : bool
If True, use the voltage measurement device's internal buffer for readings, which leads to more accurate timings.
If None, the value is taken from the settings.
max_points_shown : int|None
how many points should be shown at once. None means infinite
max_measurements : int|None
maximum number of measurements. None means infinite
stop_on_script_end : bool, optional
Stop measurement when the script end is reached
"""
global _runtime_vars, data_collector, dev, led
global data, md
_runtime_vars["last_measurement"] = dtime.now().isoformat()
if interval is None: interval = settings["interval"]
if flush_after is None: flush_after = settings["flush_after"]
if use_buffer is None: use_buffer = settings["use_buffer"]
# set metadata
metadata["interval"] = str(interval)
metadata["name"] = settings["name"]
metadata["led"] = settings["led"]
metadata["led_script"] = str(script)
print(f"Starting measurement with:\n\tinterval = {interval}s\nUse <C-c> to stop. Save the data using 'data.save_csv()' afterwards.")
plt.ion()
plt_monitor = Monitor(max_points_shown=max_points_shown)
led_script = LedScript(script=script, auto_update=True, verbose=True)
data_collector = PrsDataCollector(metadata=metadata, data_path=settings["datadir"], data_name=settings["name"])
# data_collector.clear()
data_queue = mp.Queue()
command_queue = mp.Queue()
# Argument order must match the definition
proc_measure = mt.Thread(target=_measure, args=(dev,
led,
led_script,
data_collector,
interval,
flush_after,
use_buffer,
max_measurements,
stop_on_script_end,
False, # verbose
command_queue,
data_queue
))
proc_measure.start()
try:
while proc_measure.is_alive():
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
plt_monitor.update(i, tval, vval, led_val)
except KeyboardInterrupt:
pass
command_queue.put("stop")
proc_measure.join()
print("Measurement stopped" + " "*50)
led_script.stop_updating() # stop watching for file updates (if enabled)
data_collector.save_csv(verbose=True)
data, metadata = data_collector.get_data()
fig = data_plot(data, CPD=True, LED=True)
plt.ioff()
fig_path = path.join(data_collector.path, data_collector.dirname + ".pdf")
fig.savefig(fig_path)
# DATA
def data_load(dirname:str) -> tuple[np.ndarray, dict]:
"""
Load data in directory <dirname> in the data directory as numpy array.
Sets the `data` and `md` variables
Parameters
----------
dirname : str
Absolute path to the directory containing the measurement or directory name in the data directory.
"""
global data, md
if path.isabs(dirname):
dirpath = dirname
else:
dirpath = path.join(settings["datadir"], dirname)
data, md = PrsDataCollector.load_data_from_dir(dirpath, verbose=True)
# SETTINGS
def set(setting, value):
global settings, config_path
if setting in settings:
if type(value) != type(settings[setting]):
print(f"set: setting '{setting}' currently holds a value of type '{type(settings[setting])}'")
return
settings[setting] = value
config_file.set(setting, value)
def name(s:str):
global settings
settings["name"] = s
def save_settings():
global settings
for k, v in settings.items():
config_file.set(k, v)
config_file.save()
def load_settings():
global settings, config_path
settings = config_file.get_values()
settings["datadir"] = path.expanduser(settings["datadir"]) # replace ~
def help(topic=None):
if topic == None:
print("""
Functions:
monitor - take measurements with live monitoring in a matplotlib window
data_load - load data from a directory
data_plot - plot a data array
Run 'help(function)' to see more information on a function
Available topics:
imports
settings
Run 'help("topic")' to see more information on a topic""")
elif topic in [settings, "settings"]:
print(f"""Settings:
name: str - name of the measurement, determines filename
led: str - name/model of the LED that is being used
datadir: str - output directory for the csv files
interval: int - interval (inverse frequency) of the measurements, in seconds
beep: bool - whether the device should beep or not
Functions:
name("<name>") - short for set("name", "<name>")
set("setting", value) - set a setting to a value
save_settings() - store the settings in the config file
load_settings() - load settings from a file
Upon startup, settings are loaded from the config file.
The global variable 'config_path' determines the path used by save/load_settings. Use -c '<path>' to set another path.
The search path is:
<working-dir>/{cfilename}
$XDG_CONFIG_HOME/{cfilename}
~/.config/photoreflectance/{cfilename}
The current file path is:
{config_path}
""")
elif topic == "imports":
print("""Imports:
numpy as np
pandas as pd
matplotlib.pyplot as plt
os.path """)
else:
print(topic.__doc__.strip(" ").strip("\n"))
def connect_devices():
global lockin, shutter, lamp, mcm
lockin = mod_lock_in.connect_device(*select_device_interactive(mod_lock_in.list_devices(), "Select Lock-In-Amplifier: "))
shutter = mod_shutter.connect_device(*select_device_interactive(mod_shutter.list_devices(), "Select Shutter: "))
lamp = mod_lamp.connect_device(*select_device_interactive(mod_lamp.list_devices(), "Select Lamp: "))
mcm = mod_monochromator.connect_device(*select_device_interactive(mod_monochromator.list_devices(), "Select Monochromator: "))
def init():
global lockin, shutter, lamp, mcm, settings, config_path, config_file
print(r""" __ .__
_____________ ______ _____/ |________| |
\____ \_ __ \/ ___// ___\ __\_ __ \ |
| |_> > | \/\___ \\ \___| | | | \/ |__
| __/|__| /____ >\___ >__| |__| |____/
|__| \/ \/ """ + f"""{version}
Interactive Shell for Photoreflectance measurements
---
Enter 'help()' for a list of commands""")
parser = argparse.ArgumentParser(
prog="prsctrl",
description="measure photoreflectance",
)
backend_group = parser.add_mutually_exclusive_group(required=False)
parser.add_argument("-c", "--config", action="store", help="alternate path to config file")
args = vars(parser.parse_args())
from os import environ
# Load config file
if path.isfile(cfilename):
config_path = cfilename
elif 'XDG_CONFIG_HOME' in environ.keys():
config_path = path.join(environ["XDG_CONFIG_HOME"], "prsctrl", cfilename)
else:
config_path = path.join(path.expanduser("~/.config/prsctrl"), cfilename)
if args["config"]:
config_path = args["config"]
config_file = ConfigFile(config_path, init_values=settings)
load_settings()
# setup logging
log_path = path.expanduser(config_file.get_or("path_log", "~/.cache/prsctrl-interactive.log"))
makedirs(path.dirname(log_path), exist_ok=True)
logging.basicConfig(
level=logging.WARN,
format="%(asctime)s [%(levelname)s] [%(name)s] %(message)s",
handlers=[
logging.FileHandler(log_path),
logging.StreamHandler()
]
)
if not path.isdir(settings["datadir"]):
makedirs(settings["datadir"])
# init the devices
shutter = connect_device_from_config_or_interactive(config_file, "shutter", "Shutter", mod_shutter, log=log)
lockin = connect_device_from_config_or_interactive(config_file, "lock-in", "Lock-In Amplifier", mod_lock_in, log=log)
lamp = connect_device_from_config_or_interactive(config_file, "lamp", "Lamp", mod_lamp, log=log)
mcm = connect_device_from_config_or_interactive(config_file, "monochromator", "Monochromator", mod_monochromator, log=log)
# atexit.register(_backend.exit, dev)
if __name__ == "__main__":
init()