diff --git a/bin/mule b/bin/mule index 52db84b..c625bf0 100755 --- a/bin/mule +++ b/bin/mule @@ -27,11 +27,12 @@ MULE pack runtime executable Use 'mule --help' for more information ======================================''', formatter_class=argparse.RawTextHelpFormatter) -parser.add_argument("pack", choices = ['acq','proc','tests','ana'], help = '''The pack implemented: +parser.add_argument("pack", choices = ['acq','proc','tests','ana', 'vis'], help = '''The pack implemented: acq - Acquisition of data using wavedump 1 proc - Processing of data tests - Testing directory (IGNORE) ana - Analysing data + vis - Visualise data ''') parser.add_argument("config", help = 'The config file provided to the pack, this differs based on which pack youre using') # acquire arguments diff --git a/packs/configs/vis_wd1_1channel.conf b/packs/configs/vis_wd1_1channel.conf new file mode 100644 index 0000000..8dc848c --- /dev/null +++ b/packs/configs/vis_wd1_1channel.conf @@ -0,0 +1,9 @@ +[required] + +visualise = 'waveform' # only 'waveform' as in single waveform so far +file_path = '/path/to/file.h5' +vis_params = { + 'baseline_sub' : 'median', + 'sidebands' : ((100, 300), (2900, 3100)), + 'negative' : False} + diff --git a/packs/vis/vis.py b/packs/vis/vis.py new file mode 100644 index 0000000..75717bf --- /dev/null +++ b/packs/vis/vis.py @@ -0,0 +1,30 @@ +import os +import sys +import traceback + +from packs.core.io import read_config_file +from packs.core.core_utils import check_test +from packs.vis.visualise_utils import visualise_waveform + +def vis(config_file): + print("Starting the visualisation pack...") + + # checks if test, if so ends run + if check_test(config_file): + return + + # take full path + full_path = os.path.expandvars(config_file) + + conf_dict = read_config_file(full_path) + # check the method implemented, currently just process + try: + match conf_dict.pop('visualise'): + case 'waveform': + visualise_waveform(**conf_dict) + case other: + raise RuntimeError(f"process {other} not currently implemented.") + except KeyError as e: + print(f"\nError in the configuration file, incorrect or missing argument: {e} \n") + traceback.print_exc() + sys.exit(2) \ No newline at end of file diff --git a/packs/vis/visualise_utils.py b/packs/vis/visualise_utils.py new file mode 100644 index 0000000..02e42bd --- /dev/null +++ b/packs/vis/visualise_utils.py @@ -0,0 +1,109 @@ +import numpy as np +import pandas as pd +import matplotlib.pyplot as plt + +from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg +import tkinter as tk +from tkinter import ttk + +from packs.core.io import load_evt_info, load_rwf_info +from packs.proc.calibration_utils import subtract_baseline, collect_sidebands + +def visualise_waveform(file_path : str, + vis_params : dict): + """ + Launch an interactive Tkinter GUI for browsing raw waveforms from a file. + + Parameters + ---------- + file_path : str + Path to the data file. Used to load event/waveform info. + vis_params : dict + Visualisation options: + - 'negative' (bool) – invert the waveform amplitude. + - 'baseline_sub' (str) – method for determining baseline, 'median' or 'mean' + """ + # supporting functions + # --------------------------------------------------------------------------------- + def plot_waveform(wf_num : int): + """Clear the axes and draw waveform wf_num with baseline subtraction applied.""" + ax.clear() + single_wf = wf_rwf['rwf'][wf_num] + if vis_params['negative']: + single_wf = -single_wf + + sideband_values = collect_sidebands(single_wf, time, vis_params) + single_wf = single_wf - subtract_baseline(sideband_values, sub_type = vis_params['baseline_sub']) + ax.plot(time, single_wf, + marker='o', markerfacecolor='None', linestyle='None', markersize=1) + ax.set_title(f'Waveform #{wf_num}') + ax.set_xlabel('Time (s)') + ax.set_ylabel('ADC') + canvas.draw() + + def on_entry(event : tk.Event | None = None): + """Check and apply the waveform index, fixing the diagram to the valid range.""" + try: + val = int(entry_var.get()) + val = max(0, min(val, max_wf)) + entry_var.set(val) + slider_var.set(val) + plot_waveform(val) + except ValueError: + pass + + def on_slider(value : str | None = None): + """Command for ttk.Scale. Sync the entry box to the slider position and redraw the selected waveform.""" + val = slider_var.get() + entry_var.set(str(val)) + plot_waveform(val) + # --------------------------------------------------------------------------------- + + filename = (file_path.rsplit('.')[1]).rsplit('/')[0] + + # load event + waveform info + wf_evt = load_evt_info(file_path) + samples = int(wf_evt.loc[0].samples) + sampling_period = float(wf_evt.loc[0].sampling_period) + wf_rwf = load_rwf_info(file_path, samples) + print(f'file: {file_path}\nsamples: {samples}\nsampling_period: {sampling_period}') + max_wf = len(wf_rwf['rwf']) - 1 + time = np.linspace(0,samples * sampling_period, num = samples) + + # init GUI + root = tk.Tk() + root.title(f"Waveform Viewer — {filename}") + + # generate plot + fig, ax = plt.subplots(layout='constrained', figsize=(8, 4)) + canvas = FigureCanvasTkAgg(fig, master=root) + canvas.get_tk_widget().pack(fill=tk.BOTH, expand=True) + + # controls frame + ctrl = ttk.Frame(root, padding=8) + ctrl.pack(fill=tk.X) + + ttk.Label(ctrl, text="Waveform #").pack(side=tk.LEFT) + + # number entry + entry_var = tk.StringVar(value="0") + + # controls entry + entry = ttk.Entry(ctrl, textvariable=entry_var, width=7) + entry.pack(side=tk.LEFT, padx=4) + entry.bind("", on_entry) + entry.bind("", on_entry) + + # slider + slider_var = tk.IntVar(value=0) + + # controls slider + slider = ttk.Scale(ctrl, from_=0, to=max_wf, orient=tk.HORIZONTAL, + variable=slider_var, command=on_slider, length=400) + slider.pack(side=tk.LEFT, padx=8, fill=tk.X, expand=True) + + ttk.Label(ctrl, text=f"(0 – {max_wf})").pack(side=tk.LEFT) + + # draw initial waveform + plot_waveform(0) + root.mainloop() \ No newline at end of file