From 16275950c2abacae8ac2bc7f15c912182e7a1d26 Mon Sep 17 00:00:00 2001 From: Dmitry Sidorov-Biryukov <99192142+msbdd@users.noreply.github.com> Date: Mon, 30 Jun 2025 17:20:29 +0200 Subject: [PATCH 1/6] First GUI version; --- src/gui.py | 180 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 src/gui.py diff --git a/src/gui.py b/src/gui.py new file mode 100644 index 0000000..63a3e59 --- /dev/null +++ b/src/gui.py @@ -0,0 +1,180 @@ +import tkinter as tk +from tkinter import filedialog, messagebox +from tkinter import ttk +import yaml +from pathlib import Path +from obspy import read_inventory +from obspy import read +from obspy.signal import PPSD +from obspy.imaging.cm import pqlx +from PPSD_plotter import process_dataset, load_config, \ + parse_channel, find_miniseed +import matplotlib +matplotlib.use("TkAgg") + + +def plot_ppsd_interactive( + sampledata, channel, location, inv, npzfolder, + tw, plot_kwargs=None +): + if plot_kwargs is None: + plot_kwargs = {} + + st = read(sampledata) + if location: + matches = st.select(channel=channel, location=location) + else: + matches = st.select(channel=channel) + if not matches: + print(f"No matching trace for channel={channel} location={location}") + return + if location is None and len(matches) > 1: + print( + f"Warning: Multiple locations found for {channel}." + f"Using first: {matches[0].stats.location}" + ) + trace = matches[0] + ppsd = PPSD(trace.stats, inv, ppsd_length=tw) + + for file in Path(npzfolder).glob("*.npz"): + try: + ppsd.add_npz(str(file)) + except Exception as e: + print(f"Error loading {file}: {e}") + + cmap = plot_kwargs.pop("cmap", pqlx) + figsize = plot_kwargs.pop("figsize", (12, 6)) + + fig = ppsd.plot(cmap=cmap, show=False, **plot_kwargs) + fig.set_size_inches(*figsize) + fig.canvas.manager.set_window_title(f"PPSD Plot {trace.id}") + fig.show() + + +class DatasetFrame(ttk.LabelFrame): + def __init__(self, parent, dataset, index, tw): + super().__init__(parent, text=f"Dataset {index+1}") + self.dataset = dataset + self.tw = tw + self.figures = [] + self.build() + + def build(self): + ttk.Label( + self, text=f"Folder: {self.dataset['folder']}" + ).grid(row=0, column=0, sticky="w") + ttk.Label( + self, text=f"Channels: {', '.join(self.dataset['channels'])}" + ).grid(row=1, column=0, sticky="w") + ttk.Button( + self, text="Run Task", command=self.run_task + ).grid(row=2, column=0, sticky="w") + self.plot_canvas = tk.Canvas(self, height=200, width=400) + self.plot_canvas.grid(row=3, column=0, pady=10) + + def run_task(self): + try: + process_dataset(self.dataset, self.tw) + self.show_interactive_plots() + except Exception as e: + messagebox.showerror("Error", f"Processing failed: {e}") + + def show_interactive_plots(self): + folder = self.dataset["folder"] + inv = read_inventory(self.dataset["response"]) + for ch_str in self.dataset["channels"]: + loc_code, channel = parse_channel(ch_str) + if loc_code: + npzfolder = Path(folder) / f"npz_{loc_code}_{channel}" + else: + npzfolder = Path(folder) / f"npz_{channel}" + sample = find_miniseed(folder, channel, loc_code) + plot_ppsd_interactive( + sampledata=sample, + channel=channel, + location=loc_code, + inv=inv, + npzfolder=npzfolder, + tw=self.tw, + plot_kwargs={} + ) + + +class App(tk.Tk): + def __init__(self): + super().__init__() + self.title("PPSD Plotter GUI") + self.geometry("800x600") + self.datasets = [] + self.tw = 3600 # Default fallback + self.build_menu() + self.build_main() + + def build_menu(self): + menubar = tk.Menu(self) + filemenu = tk.Menu(menubar, tearoff=0) + filemenu.add_command(label="Load Config", command=self.load_config) + filemenu.add_command(label="Save Config", command=self.save_config) + filemenu.add_command(label="New Config", command=self.new_config) + filemenu.add_separator() + filemenu.add_command(label="Exit", command=self.quit) + menubar.add_cascade(label="File", menu=filemenu) + self.config(menu=menubar) + + def build_main(self): + self.container = ttk.Frame(self) + self.container.pack(fill="both", expand=True) + self.scroll = tk.Canvas(self.container) + self.scrollbar = ttk.Scrollbar( + self.container, orient="vertical", command=self.scroll.yview + ) + self.scroll_frame = ttk.Frame(self.scroll) + + self.scroll_frame.bind( + "", + lambda e: self.scroll.configure( + scrollregion=self.scroll.bbox("all") + ) + ) + self.scroll.create_window( + (0, 0), window=self.scroll_frame, anchor="nw" + ) + self.scroll.configure(yscrollcommand=self.scrollbar.set) + + self.scroll.pack(side="left", fill="both", expand=True) + self.scrollbar.pack(side="right", fill="y") + + def populate_datasets(self): + for widget in self.scroll_frame.winfo_children(): + widget.destroy() + for idx, ds in enumerate(self.datasets): + frame = DatasetFrame(self.scroll_frame, ds, idx, self.tw) + frame.pack(fill="x", padx=10, pady=5) + + def load_config(self): + filepath = filedialog.askopenfilename( + filetypes=[("YAML files", "*.yaml *.yml")] + ) + if not filepath: + return + config = load_config(filepath) + self.datasets = config.get("datasets", []) + self.tw = config.get("timewindow", 3600) + self.populate_datasets() + + def save_config(self): + filepath = filedialog.asksaveasfilename(defaultextension=".yaml") + if not filepath: + return + config = {"timewindow": self.tw, "datasets": self.datasets} + with open(filepath, "w") as f: + yaml.dump(config, f) + + def new_config(self): + self.datasets = [] + self.populate_datasets() + + +if __name__ == "__main__": + app = App() + app.mainloop() From 716b947f7c5bcbea4814ada2d743d1dbe1cf6283 Mon Sep 17 00:00:00 2001 From: Dmitry Sidorov-Biryukov <99192142+msbdd@users.noreply.github.com> Date: Tue, 1 Jul 2025 14:31:05 +0200 Subject: [PATCH 2/6] Added configurable parameters; switched to drop-down lists; added progress-bars; added nice namings; fixed plotting; --- src/gui.py | 744 ++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 678 insertions(+), 66 deletions(-) diff --git a/src/gui.py b/src/gui.py index 63a3e59..8b0976b 100644 --- a/src/gui.py +++ b/src/gui.py @@ -1,25 +1,336 @@ import tkinter as tk -from tkinter import filedialog, messagebox +from tqdm import tqdm +import numpy as np +from tkinter import filedialog from tkinter import ttk import yaml from pathlib import Path +from matplotlib import colormaps from obspy import read_inventory from obspy import read from obspy.signal import PPSD from obspy.imaging.cm import pqlx -from PPSD_plotter import process_dataset, load_config, \ - parse_channel, find_miniseed import matplotlib +from functools import partial + matplotlib.use("TkAgg") +CMAP_NAMES = ["pqlx"] + sorted(colormaps) + +PLOT_KWARGS = { + "show_coverage", + "show_percentiles", + "show_histogram", + "percentiles", + "show_noise_models", + "show_earthquakes", + "grid", + "max_percentage", + "period_lim", + "show_mode", + "show_mean", + "cmap", + "cumulative", + "cumulative_number_of_colors", + "xaxis_frequency", +} + +BOOLEAN_KEYS = { + "show_coverage", + "show_percentiles", + "show_histogram", + "show_noise_models", + "grid", + "show_mode", + "show_mean", + "cumulative", + "xaxis_frequency", +} + +PARAM_LABELS = { + "folder": "Data Folder", + "response": "Response File", + "channels": "Channels", + "show_coverage": "Show Coverage", + "show_percentiles": "Show Percentiles", + "show_histogram": "Show Histogram", + "percentiles": "Percentiles", + "show_noise_models": "Show Noise Models", + "show_earthquakes": "Show Earthquakes", + "grid": "Show Grid", + "max_percentage": "Max Percentage", + "period_lim": "Period Limits", + "show_mode": "Show Mode", + "show_mean": "Show Mean", + "cmap": "Colormap", + "cumulative": "Cumulative", + "cumulative_number_of_colors": "Cumulative Colors", + "xaxis_frequency": "X Axis Frequency", + "action": "Action", + "timewindow": "Timewindow", +} + +DEFAULT_PLOT_KWARGS = { + "show_coverage": True, + "show_histogram": True, + "show_percentiles": False, + "show_noise_models": True, + "grid": True, + "show_mode": False, + "show_mean": False, + "cmap": "pqlx", + "cumulative": False, + "xaxis_frequency": False, +} + +DEFAULT_DATASET = { + "folder": "", + "response": "", + "channels": [], + "action": "full", + "timewindow": 600, + "plot_kwargs": DEFAULT_PLOT_KWARGS.copy(), +} + + +PARAM_TOOLTIPS = { + "channels": "List of channels (e.g. BHZ or 00.BHZ, one per line).", + "action": "What to do: plot, calculate, full, or convert.", + "timewindow": "Window length in seconds for PPSD calculation.", + "show_coverage": "Show data coverage on the plot.", + "show_percentiles": "Show percentiles on the plot.", + "show_histogram": "Show histogram on the plot.", + "percentiles": "Percentile values to plot (comma-separated).", + "show_noise_models": "Show noise models (NLNM/NHNM).", + "show_earthquakes": "Show earthquakes on the plot.", + "grid": "Show grid lines.", + "max_percentage": "Maximum percentage for color scale.", + "period_lim": "Limits for period axis (e.g. 0.01, 179).", + "show_mode": "Show mode value.", + "show_mean": "Show mean value.", + "cmap": "Colormap for the plot.", + "cumulative": "Show cumulative distribution.", + "cumulative_number_of_colors": "Number of colors for cumulative plot.", + "xaxis_frequency": "Show frequency instead of period on x-axis.", +} + +ACTIONS = ["plot", "calculate", "full", "convert"] + + +def parse_channel(ch_str): + parts = ch_str.split(".", 1) + if len(parts) == 1: + return None, parts[0] + return parts[0], parts[1] + + +def find_miniseed_channels(folder): + + found = set() + for path in Path(folder).rglob("*.mseed"): + try: + st = read(str(path), headonly=True) + for tr in st: + found.add((tr.stats.location.strip(), tr.stats.channel.strip())) + except Exception: + continue + return sorted(found) + + +def find_miniseed(workdir, channel, location=None): + for file in Path(workdir).rglob("*"): + if file.suffix.lower() in [".msd", ".miniseed", ".mseed"]: + try: + st = read(str(file)) + for tr in st: + if location: + if ( + tr.stats.channel == channel + and tr.stats.location == location + ): + return str(file) + else: + if tr.stats.channel == channel: + return str(file) + except Exception as e: + print(f"Skipping {file} due to error: {e}") + return None + + +def calculate_ppsd(workdir, npzfolder, channel, location, inv, tw): + workdir = Path(workdir) + Path(npzfolder).mkdir(exist_ok=True) + + files = [ + f + for f in workdir.rglob("*") + if f.suffix.lower() in [".msd", ".miniseed", ".mseed"] + ] + + for file in tqdm( + files, desc=f"[{workdir.name} | {channel}] PSD files", unit="file" + ): + try: + st = read(str(file)) + st = st.select(channel=channel, location=location) + for trace in st: + ppsd = PPSD(trace.stats, metadata=inv, ppsd_length=tw) + ppsd.add(trace) + timestamp = trace.stats.starttime.strftime("%y-%m-%d_%H-%M-%S.%f") + outfile = npzfolder / f"{timestamp}.npz" + ppsd.save_npz(str(outfile)) + except Exception as e: + print( + f"Error processing {file} for channel={channel}" + f"location={location}: {e}" + ) + + +def convert_npz_to_text(npzdir): + npzdir = Path(npzdir) + outdir = npzdir.with_name(npzdir.name + "_text") + outdir.mkdir(exist_ok=True) + + psd_entries = [] + periods_struct = None + files = list(npzdir.glob("*.npz")) + + for file in tqdm(files, desc=f"[{npzdir.name}] Converting", unit="file"): + data = np.load(file, allow_pickle=True) + periods = np.asarray(data["_period_binning"]).flatten() + psd_values = np.asarray(data["_binned_psds"]).astype(float) + + if periods_struct is None: + periods_struct = periods + + for i, psd_row in enumerate(psd_values): + psd_entries.append((i, psd_row.flatten())) + + if psd_entries: + outcsv = outdir / "export.csv" + with open(outcsv, "w") as fo: + header = "Period binning," + ",".join( + f"{float(p):.6f}s" for p in periods_struct + ) + fo.write(header + "\n") + for time_window, row in psd_entries: + fo.write( + f"{time_window}," + ",".join(f"{float(v):.6f}" for v in row) + "\n" + ) + print(f"Saved CSV to {outcsv}") + else: + print("No PSD entries found.") + + +def make_yaml_safe(obj): + if isinstance(obj, dict): + return {k: make_yaml_safe(v) for k, v in obj.items()} + elif isinstance(obj, (list, tuple)): + return [make_yaml_safe(i) for i in obj] + else: + return obj + + +def normalize_plot_kwargs(plot_kwargs): + normalized = {} + for key, val in plot_kwargs.items(): + if key in BOOLEAN_KEYS: + normalized[key] = safe_bool(val) + elif key == "cmap" and isinstance(val, str): + normalized[key] = val + else: + normalized[key] = val + return normalized + + +def process_dataset_visual(ds, tw, progress_update_callback): + folder = ds.get("folder", "") + resp_file = ds.get("response", "") + channels = ds.get("channels", []) + action = str(ds.get("action", "full")) + inv = load_inventory(resp_file) + + if not inv: + progress_update_callback(0, "Failed: inventory load") + return + + plot_kwargs = normalize_plot_kwargs(ds.get("plot_kwargs", {})) + total = len(channels) + if total == 0: + progress_update_callback(0, "No channels defined") + return + + for i, ch_str in enumerate(channels): + progress = int((i / total) * 100) + progress_update_callback(progress, f"Processing {ch_str}") + + loc_code, channel = parse_channel(ch_str) + + if loc_code: + npzfolder = Path(folder) / f"npz_{loc_code}_{channel}" + else: + npzfolder = Path(folder) / f"npz_{channel}" + + if action in ["calculate", "full"]: + calculate_ppsd(folder, npzfolder, channel, loc_code, inv, tw) + + if action in ["plot", "full"]: + sample = find_miniseed(folder, channel, loc_code) + if sample: + plot_ppsd_interactive( + sample, channel, loc_code, inv, npzfolder, tw, plot_kwargs.copy() + ) + else: + progress_update_callback(progress, f"No data for {ch_str}") + + if action == "convert": + convert_npz_to_text(npzfolder) + + progress_update_callback(100, "Done") + + +def load_inventory(resp_file): + ext = Path(resp_file).suffix.lower() + + if ext in [".seed", ".dataless"]: + fmt = "SEED" + elif ext == ".xml": + fmt = "STATIONXML" + else: + fmt = None + + try: + if fmt: + inv = read_inventory(resp_file, format=fmt) + else: + inv = read_inventory(resp_file) + return inv + except Exception as e: + print(f"Failed to read inventory {resp_file}: {e}") + return + + +def format_plot_kwargs_for_display(plot_kwargs): + lines = [] + for key, value in plot_kwargs.items(): + label = PARAM_LABELS.get(key, key) + lines.append(f"{label}: {value}") + return "\n".join(lines) + def plot_ppsd_interactive( - sampledata, channel, location, inv, npzfolder, - tw, plot_kwargs=None + sampledata, channel, location, inv, npzfolder, tw, plot_kwargs=None ): if plot_kwargs is None: plot_kwargs = {} - + cmap_name_or_obj = plot_kwargs.pop("cmap", "pqlx") + if isinstance(cmap_name_or_obj, str): + if cmap_name_or_obj == "pqlx": + cmap = pqlx + else: + cmap = colormaps.get(cmap_name_or_obj, "viridis") # fallback + else: + cmap = cmap_name_or_obj # in case already a colormap object st = read(sampledata) if location: matches = st.select(channel=channel, location=location) @@ -32,7 +343,7 @@ def plot_ppsd_interactive( print( f"Warning: Multiple locations found for {channel}." f"Using first: {matches[0].stats.location}" - ) + ) trace = matches[0] ppsd = PPSD(trace.stats, inv, ppsd_length=tw) @@ -42,71 +353,308 @@ def plot_ppsd_interactive( except Exception as e: print(f"Error loading {file}: {e}") - cmap = plot_kwargs.pop("cmap", pqlx) - figsize = plot_kwargs.pop("figsize", (12, 6)) - fig = ppsd.plot(cmap=cmap, show=False, **plot_kwargs) - fig.set_size_inches(*figsize) + fig.canvas.manager.set_window_title(f"PPSD Plot {trace.id}") fig.show() +def safe_bool(val): + if isinstance(val, bool): + return val + if isinstance(val, str): + return val.lower() in ("true", "1", "yes", "on") + return bool(val) + + +class ToolTip: + def __init__(self, widget, text): + self.widget = widget + self.text = text + self.tipwindow = None + widget.bind("", self.show_tip) + widget.bind("", self.hide_tip) + + def show_tip(self, event=None): + if self.tipwindow or not self.text: + return + x, y, _, cy = ( + self.widget.bbox("insert") if hasattr(self.widget, "bbox") else (0, 0, 0, 0) + ) + x = x + self.widget.winfo_rootx() + 25 + y = y + cy + self.widget.winfo_rooty() + 20 + self.tipwindow = tw = tk.Toplevel(self.widget) + tw.wm_overrideredirect(True) + tw.wm_geometry(f"+{x}+{y}") + label = tk.Label( + tw, + text=self.text, + justify="left", + background="#ffffe0", + relief="solid", + borderwidth=1, + font=("tahoma", "8", "normal"), + ) + label.pack(ipadx=1) + + def hide_tip(self, event=None): + tw = self.tipwindow + self.tipwindow = None + if tw: + tw.destroy() + + class DatasetFrame(ttk.LabelFrame): - def __init__(self, parent, dataset, index, tw): - super().__init__(parent, text=f"Dataset {index+1}") + def __init__( + self, + parent, + dataset, + index, + run_callback=None, + delete_callback=None, + duplicate_callback=None, + ): + super().__init__(parent, text=f"Dataset {index+1}", padding=5) self.dataset = dataset - self.tw = tw - self.figures = [] + self.index = index + self.run_callback = run_callback + self.delete_callback = delete_callback + self.duplicate_callback = duplicate_callback + self.plot_kwargs_vars = {} + self.dataset.setdefault("plot_kwargs", {}) self.build() + def build_path_selector(self, label_text, key, row): + def select_path(): + path = ( + filedialog.askdirectory() + if "folder" in key + else filedialog.askopenfilename() + ) + if path: + self.dataset[key] = path + path_var.set(path) + if key == "folder": + channels = find_miniseed_channels(path) + if channels: + self.dataset["channels"] = [ + f"{loc}.{ch}" if loc else ch for loc, ch in channels + ] + self.channels_text.delete("1.0", tk.END) + self.channels_text.insert( + "1.0", "\n".join(self.dataset["channels"]) + ) + + path_var = tk.StringVar(value=self.dataset.get(key, "")) + label = ttk.Label(self, text=label_text) + label.grid(row=row, column=0, sticky="w") + # No tooltip for folders/files + ttk.Entry(self, textvariable=path_var, width=40).grid( + row=row, column=1, sticky="w" + ) + ttk.Button(self, text="Select", command=select_path).grid( + row=row, column=2, sticky="w" + ) + def build(self): - ttk.Label( - self, text=f"Folder: {self.dataset['folder']}" - ).grid(row=0, column=0, sticky="w") - ttk.Label( - self, text=f"Channels: {', '.join(self.dataset['channels'])}" - ).grid(row=1, column=0, sticky="w") + row = 0 + # Folders/files: no tooltip, plain label + self.build_path_selector(PARAM_LABELS.get("folder", "folder"), "folder", row) + row += 1 + self.build_path_selector( + PARAM_LABELS.get("response", "response"), "response", row + ) + row += 1 + # Action + label = ttk.Label(self, text=PARAM_LABELS.get("action", "Action") + ":") + label.grid(row=row, column=0, sticky="w") + ToolTip(label, PARAM_TOOLTIPS.get("action", "")) + self.action_var = tk.StringVar(value=self.dataset.get("action", "full")) + action_combo = ttk.Combobox( + self, + textvariable=self.action_var, + values=ACTIONS, + state="readonly", + width=15, + ) + action_combo.grid(row=row, column=1, sticky="w") + action_combo.bind("<>", self.update_action) + row += 1 + + # Timewindow + label = ttk.Label( + self, text=PARAM_LABELS.get("timewindow", "Timewindow") + " (s):" + ) + label.grid(row=row, column=0, sticky="w") + ToolTip(label, PARAM_TOOLTIPS.get("timewindow", "")) + self.tw_var = tk.StringVar(value=str(self.dataset.get("timewindow", 3600))) + tw_entry = ttk.Entry(self, textvariable=self.tw_var, width=10) + tw_entry.grid(row=row, column=1, sticky="w") + tw_entry.bind("", self.update_timewindow) + row += 1 + + # Channels + label = ttk.Label(self, text=PARAM_LABELS.get("channels", "channels")) + label.grid(row=row, column=0, sticky="nw") + ToolTip(label, PARAM_TOOLTIPS.get("channels", "")) + self.channels_text = tk.Text(self, height=3, width=40) + channels_val = self.dataset.get("channels", []) + if isinstance(channels_val, list): + self.channels_text.insert("1.0", "\n".join(channels_val)) + elif isinstance(channels_val, str): + self.channels_text.insert("1.0", channels_val) + self.channels_text.grid(row=row, column=1, columnspan=2, sticky="w") + self.channels_text.bind("", self.update_channels) + row += 1 + + # Plot options + label = ttk.Label(self, text="Plot Options:") + label.grid(row=row, column=0, sticky="w") + row += 1 + + for key in sorted(PLOT_KWARGS): + current_val = self.dataset["plot_kwargs"].get(key) + if key in BOOLEAN_KEYS: + var = tk.BooleanVar(value=safe_bool(current_val)) + cb = ttk.Checkbutton( + self, text=PARAM_LABELS.get(key, key), variable=var + ) + cb.grid(row=row, column=0, sticky="w") + ToolTip(cb, PARAM_TOOLTIPS.get(key, "")) + var.trace_add("write", partial(self.update_plot_kwargs, key, var)) + self.plot_kwargs_vars[key] = var + elif key == "cmap": + current_val = str(current_val) if current_val else "pqlx" + label = ttk.Label(self, text=PARAM_LABELS.get("cmap", "cmap") + ":") + label.grid(row=row, column=0, sticky="w") + ToolTip(label, PARAM_TOOLTIPS.get("cmap", "")) + var = tk.StringVar(value=current_val) + combo = ttk.Combobox( + self, + textvariable=var, + values=CMAP_NAMES, + state="readonly", + width=30, + ) + combo.grid(row=row, column=1, sticky="w") + combo.bind( + "<>", partial(self.update_plot_kwargs, key, var) + ) + self.plot_kwargs_vars[key] = var + else: + var = tk.StringVar( + value=str(current_val) if current_val is not None else "" + ) + label = ttk.Label(self, text=PARAM_LABELS.get(key, key) + ":") + label.grid(row=row, column=0, sticky="w") + ToolTip(label, PARAM_TOOLTIPS.get(key, "")) + ent = ttk.Entry(self, textvariable=var, width=30) + ent.grid(row=row, column=1, sticky="w") + ent.bind("", partial(self.update_plot_kwargs, key, var)) + self.plot_kwargs_vars[key] = var + row += 1 + + self.progress = ttk.Progressbar(self, maximum=100, mode="determinate") + self.progress.grid(row=row, column=0, columnspan=2, sticky="ew", pady=(5, 0)) + row += 1 + self.status_label = ttk.Label(self, text="", foreground="gray") + self.status_label.grid(row=row, column=0, columnspan=2, sticky="w") + row += 1 + # Group buttons in a frame + btn_frame = ttk.Frame(self) + btn_frame.grid(row=row, column=0, columnspan=3, sticky="w", pady=5) + ttk.Button(btn_frame, text="Run Dataset", command=self.run_this_dataset).pack( + side="left", padx=(0, 5) + ) + ttk.Button( + btn_frame, text="Delete Dataset", command=self.delete_this_dataset + ).pack(side="left", padx=(0, 5)) ttk.Button( - self, text="Run Task", command=self.run_task - ).grid(row=2, column=0, sticky="w") - self.plot_canvas = tk.Canvas(self, height=200, width=400) - self.plot_canvas.grid(row=3, column=0, pady=10) + btn_frame, text="Duplicate Dataset", command=self.duplicate_this_dataset + ).pack(side="left", padx=(0, 5)) - def run_task(self): + def update_timewindow(self, *_): try: - process_dataset(self.dataset, self.tw) - self.show_interactive_plots() - except Exception as e: - messagebox.showerror("Error", f"Processing failed: {e}") - - def show_interactive_plots(self): - folder = self.dataset["folder"] - inv = read_inventory(self.dataset["response"]) - for ch_str in self.dataset["channels"]: - loc_code, channel = parse_channel(ch_str) - if loc_code: - npzfolder = Path(folder) / f"npz_{loc_code}_{channel}" + self.dataset["timewindow"] = int(self.tw_var.get()) + except ValueError: + self.dataset["timewindow"] = 3600 + + def update_action(self, *_): + val = self.action_var.get() + if val in ACTIONS: + self.dataset["action"] = val + + def update_channels(self, event=None): + text = self.channels_text.get("1.0", "end").strip() + self.dataset["channels"] = [ + line.strip() for line in text.splitlines() if line.strip() + ] + + def update_plot_kwargs(self, key, var, *_): + val = var.get() + if key in BOOLEAN_KEYS: + self.dataset["plot_kwargs"][key] = bool(val) + elif key == "show_earthquakes": + val = val.strip() + if not val: + self.dataset["plot_kwargs"][key] = None else: - npzfolder = Path(folder) / f"npz_{channel}" - sample = find_miniseed(folder, channel, loc_code) - plot_ppsd_interactive( - sampledata=sample, - channel=channel, - location=loc_code, - inv=inv, - npzfolder=npzfolder, - tw=self.tw, - plot_kwargs={} - ) + try: + # Accept comma or space separated values + parts = [ + float(x.strip()) + for x in val.replace(" ", ",").split(",") + if x.strip() + ] + if len(parts) == 1: + self.dataset["plot_kwargs"][key] = (parts[0],) + elif len(parts) >= 2: + self.dataset["plot_kwargs"][key] = tuple(parts[:2]) + else: + self.dataset["plot_kwargs"][key] = None + except Exception: + self.dataset["plot_kwargs"][key] = None + else: + try: + if "," in val: + val = [float(x.strip()) for x in val.split(",")] + else: + val = float(val) + except ValueError: + pass # Keep as string + self.dataset["plot_kwargs"][key] = val + + def run_this_dataset(self): + self.status_label.config(text="Starting...", foreground="orange") + self.progress["value"] = 0 + + def update_progress(val, status): + self.progress["value"] = val + self.status_label.config(text=status) + self.update_idletasks() + + tw = self.dataset.get("timewindow", 3600) + try: + process_dataset_visual(self.dataset, tw, update_progress) + except Exception as e: + self.status_label.config(text=f"Error: {e}", foreground="red") + + def delete_this_dataset(self): + if self.delete_callback: + self.delete_callback(self.index) + + def duplicate_this_dataset(self): + if self.duplicate_callback: + self.duplicate_callback(self.index) class App(tk.Tk): def __init__(self): super().__init__() self.title("PPSD Plotter GUI") - self.geometry("800x600") + self.geometry("1000x800") self.datasets = [] - self.tw = 3600 # Default fallback + self.selected_dataset_index = None self.build_menu() self.build_main() @@ -127,46 +675,110 @@ def build_main(self): self.scroll = tk.Canvas(self.container) self.scrollbar = ttk.Scrollbar( self.container, orient="vertical", command=self.scroll.yview - ) + ) + self.hscrollbar = ttk.Scrollbar( + self.container, orient="horizontal", command=self.scroll.xview + ) self.scroll_frame = ttk.Frame(self.scroll) - self.scroll_frame.bind( "", - lambda e: self.scroll.configure( - scrollregion=self.scroll.bbox("all") - ) + lambda e: self.scroll.configure(scrollregion=self.scroll.bbox("all")), + ) + self.scroll.create_window((0, 0), window=self.scroll_frame, anchor="nw") + self.scroll.configure( + yscrollcommand=self.scrollbar.set, xscrollcommand=self.hscrollbar.set ) - self.scroll.create_window( - (0, 0), window=self.scroll_frame, anchor="nw" - ) - self.scroll.configure(yscrollcommand=self.scrollbar.set) self.scroll.pack(side="left", fill="both", expand=True) self.scrollbar.pack(side="right", fill="y") + self.hscrollbar.pack(side="bottom", fill="x") + + controls = ttk.Frame(self) + controls.pack(fill="x", padx=10, pady=5) + ttk.Button(controls, text="Add Dataset", command=self.add_dataset).pack( + side="left" + ) + + def select_dataset(self, index): + self.selected_dataset_index = index + self.populate_datasets() # Refresh to update selection highlight + + def add_dataset(self): + import copy + + self.datasets.append(copy.deepcopy(DEFAULT_DATASET)) + self.populate_datasets() + + def delete_selected_dataset(self): + if ( + self.selected_dataset_index is not None + and 0 <= self.selected_dataset_index < len(self.datasets) + ): + del self.datasets[self.selected_dataset_index] + self.selected_dataset_index = None + self.populate_datasets() def populate_datasets(self): for widget in self.scroll_frame.winfo_children(): widget.destroy() for idx, ds in enumerate(self.datasets): - frame = DatasetFrame(self.scroll_frame, ds, idx, self.tw) - frame.pack(fill="x", padx=10, pady=5) + ds.setdefault("plot_kwargs", {}) + frame = DatasetFrame( + self.scroll_frame, + ds, + idx, + run_callback=self.run_dataset, + delete_callback=self.delete_dataset, + duplicate_callback=self.duplicate_dataset, + ) + row, col = divmod(idx, 3) + frame.grid(row=row, column=col, padx=5, pady=5, sticky="nsew") + for i in range(3): + self.scroll_frame.columnconfigure(i, weight=1) + + def run_dataset(self, index): + ds = self.datasets[index] + process_dataset_visual(ds) # Only pass ds, not self + + def delete_dataset(self, index): + if 0 <= index < len(self.datasets): + del self.datasets[index] + self.populate_datasets() + + def duplicate_dataset(self, index): + if 0 <= index < len(self.datasets): + import copy + + self.datasets.insert(index + 1, copy.deepcopy(self.datasets[index])) + self.populate_datasets() def load_config(self): filepath = filedialog.askopenfilename( filetypes=[("YAML files", "*.yaml *.yml")] - ) + ) if not filepath: return - config = load_config(filepath) + with open(filepath) as f: + config = yaml.safe_load(f) self.datasets = config.get("datasets", []) - self.tw = config.get("timewindow", 3600) + for ds in self.datasets: + # Move top-level plot kwargs into plot_kwargs dict + plot_kwargs = ds.get("plot_kwargs", {}) + for key in list(ds.keys()): + if key in PLOT_KWARGS: + plot_kwargs[key] = ds.pop(key) + ds["plot_kwargs"] = normalize_plot_kwargs(plot_kwargs) + self.populate_datasets() def save_config(self): filepath = filedialog.asksaveasfilename(defaultextension=".yaml") if not filepath: return - config = {"timewindow": self.tw, "datasets": self.datasets} + for ds in self.datasets: + if ds.get("plot_kwargs", {}).get("cmap") == pqlx: + ds["plot_kwargs"]["cmap"] = "pqlx" + config = {"datasets": make_yaml_safe(self.datasets)} with open(filepath, "w") as f: yaml.dump(config, f) From cc09e8da7368d70c8825604a48e0d817fc18ad55 Mon Sep 17 00:00:00 2001 From: Dmitry Sidorov-Biryukov <99192142+msbdd@users.noreply.github.com> Date: Thu, 3 Jul 2025 22:26:06 +0200 Subject: [PATCH 3/6] Menu buttons reodered; Fixed default extension; Fixed waveform channel auto-finding; Added confirmation dialogs; --- src/gui.py | 49 +++++++++++++++++++++++++++++++++++-------------- 1 file changed, 35 insertions(+), 14 deletions(-) diff --git a/src/gui.py b/src/gui.py index 8b0976b..6fe5285 100644 --- a/src/gui.py +++ b/src/gui.py @@ -12,6 +12,9 @@ from obspy.imaging.cm import pqlx import matplotlib from functools import partial +import os +from tkinter import messagebox + matplotlib.use("TkAgg") @@ -125,16 +128,18 @@ def parse_channel(ch_str): def find_miniseed_channels(folder): - - found = set() - for path in Path(folder).rglob("*.mseed"): - try: - st = read(str(path), headonly=True) - for tr in st: - found.add((tr.stats.location.strip(), tr.stats.channel.strip())) - except Exception: - continue - return sorted(found) + extensions = [".mseed", ".msd", ".miniseed"] + for ext in extensions: + for path in Path(folder).rglob(f"*{ext}"): + try: + st = read(str(path), headonly=True) + return sorted( + (tr.stats.location.strip(), tr.stats.channel.strip()) + for tr in st + ) + except Exception: + continue + return [] def find_miniseed(workdir, channel, location=None): @@ -652,7 +657,11 @@ class App(tk.Tk): def __init__(self): super().__init__() self.title("PPSD Plotter GUI") - self.geometry("1000x800") + self.geometry("1000x700") + # Set custom icon + icon_path = os.path.join(os.path.dirname(__file__), "icon.ico") + if os.path.exists(icon_path): + self.iconbitmap(icon_path) self.datasets = [] self.selected_dataset_index = None self.build_menu() @@ -661,9 +670,9 @@ def __init__(self): def build_menu(self): menubar = tk.Menu(self) filemenu = tk.Menu(menubar, tearoff=0) + filemenu.add_command(label="New Config", command=self.new_config) filemenu.add_command(label="Load Config", command=self.load_config) filemenu.add_command(label="Save Config", command=self.save_config) - filemenu.add_command(label="New Config", command=self.new_config) filemenu.add_separator() filemenu.add_command(label="Exit", command=self.quit) menubar.add_cascade(label="File", menu=filemenu) @@ -772,7 +781,10 @@ def load_config(self): self.populate_datasets() def save_config(self): - filepath = filedialog.asksaveasfilename(defaultextension=".yaml") + filepath = filedialog.asksaveasfilename( + defaultextension=".yaml", + filetypes=[("YAML files", "*.yaml *.yml")], + ) if not filepath: return for ds in self.datasets: @@ -783,7 +795,16 @@ def save_config(self): yaml.dump(config, f) def new_config(self): - self.datasets = [] + if self.datasets: + confirm = messagebox.askyesno( + "New Configuration", "This will discard the current configuration.\nContinue?" + ) + else: + confirm = True + if not confirm: + return + import copy + self.datasets = [copy.deepcopy(DEFAULT_DATASET)] self.populate_datasets() From 3e563b7929587dfcfccdcd1fcaf01bb9c83fbe5b Mon Sep 17 00:00:00 2001 From: Dmitry Sidorov-Biryukov <99192142+msbdd@users.noreply.github.com> Date: Fri, 4 Jul 2025 10:57:16 +0200 Subject: [PATCH 4/6] Fixed channel auto-finding; Added a black dataset after a startup; Autopep8'ed the code; --- src/gui.py | 140 ++++++++++++++++++++++++++++++++++++----------------- 1 file changed, 95 insertions(+), 45 deletions(-) diff --git a/src/gui.py b/src/gui.py index 6fe5285..3ed5b9a 100644 --- a/src/gui.py +++ b/src/gui.py @@ -14,7 +14,7 @@ from functools import partial import os from tkinter import messagebox - +import copy matplotlib.use("TkAgg") @@ -129,14 +129,17 @@ def parse_channel(ch_str): def find_miniseed_channels(folder): extensions = [".mseed", ".msd", ".miniseed"] + seen = set() for ext in extensions: for path in Path(folder).rglob(f"*{ext}"): try: st = read(str(path), headonly=True) - return sorted( - (tr.stats.location.strip(), tr.stats.channel.strip()) - for tr in st - ) + for tr in st: + key = (tr.stats.location.strip(), tr.stats.channel.strip()) + if key not in seen: + seen.add(key) + if seen: + return sorted(seen) except Exception: continue return [] @@ -181,7 +184,8 @@ def calculate_ppsd(workdir, npzfolder, channel, location, inv, tw): for trace in st: ppsd = PPSD(trace.stats, metadata=inv, ppsd_length=tw) ppsd.add(trace) - timestamp = trace.stats.starttime.strftime("%y-%m-%d_%H-%M-%S.%f") + timestamp = trace.stats.starttime.strftime( + "%y-%m-%d_%H-%M-%S.%f") outfile = npzfolder / f"{timestamp}.npz" ppsd.save_npz(str(outfile)) except Exception as e: @@ -220,8 +224,10 @@ def convert_npz_to_text(npzdir): fo.write(header + "\n") for time_window, row in psd_entries: fo.write( - f"{time_window}," + ",".join(f"{float(v):.6f}" for v in row) + "\n" - ) + f"{time_window}," + + ",".join( + f"{float(v):.6f}" for v in row) + + "\n") print(f"Saved CSV to {outcsv}") else: print("No PSD entries found.") @@ -283,8 +289,13 @@ def process_dataset_visual(ds, tw, progress_update_callback): sample = find_miniseed(folder, channel, loc_code) if sample: plot_ppsd_interactive( - sample, channel, loc_code, inv, npzfolder, tw, plot_kwargs.copy() - ) + sample, + channel, + loc_code, + inv, + npzfolder, + tw, + plot_kwargs.copy()) else: progress_update_callback(progress, f"No data for {ch_str}") @@ -384,8 +395,9 @@ def show_tip(self, event=None): if self.tipwindow or not self.text: return x, y, _, cy = ( - self.widget.bbox("insert") if hasattr(self.widget, "bbox") else (0, 0, 0, 0) - ) + self.widget.bbox("insert") if hasattr( + self.widget, "bbox") else ( + 0, 0, 0, 0)) x = x + self.widget.winfo_rootx() + 25 y = y + cy + self.widget.winfo_rooty() + 20 self.tipwindow = tw = tk.Toplevel(self.widget) @@ -464,17 +476,26 @@ def select_path(): def build(self): row = 0 # Folders/files: no tooltip, plain label - self.build_path_selector(PARAM_LABELS.get("folder", "folder"), "folder", row) + self.build_path_selector( + PARAM_LABELS.get( + "folder", + "folder"), + "folder", + row) row += 1 self.build_path_selector( PARAM_LABELS.get("response", "response"), "response", row ) row += 1 # Action - label = ttk.Label(self, text=PARAM_LABELS.get("action", "Action") + ":") + label = ttk.Label( + self, text=PARAM_LABELS.get( + "action", "Action") + ":") label.grid(row=row, column=0, sticky="w") ToolTip(label, PARAM_TOOLTIPS.get("action", "")) - self.action_var = tk.StringVar(value=self.dataset.get("action", "full")) + self.action_var = tk.StringVar( + value=self.dataset.get( + "action", "full")) action_combo = ttk.Combobox( self, textvariable=self.action_var, @@ -492,7 +513,11 @@ def build(self): ) label.grid(row=row, column=0, sticky="w") ToolTip(label, PARAM_TOOLTIPS.get("timewindow", "")) - self.tw_var = tk.StringVar(value=str(self.dataset.get("timewindow", 3600))) + self.tw_var = tk.StringVar( + value=str( + self.dataset.get( + "timewindow", + 3600))) tw_entry = ttk.Entry(self, textvariable=self.tw_var, width=10) tw_entry.grid(row=row, column=1, sticky="w") tw_entry.bind("", self.update_timewindow) @@ -526,11 +551,15 @@ def build(self): ) cb.grid(row=row, column=0, sticky="w") ToolTip(cb, PARAM_TOOLTIPS.get(key, "")) - var.trace_add("write", partial(self.update_plot_kwargs, key, var)) + var.trace_add( + "write", partial( + self.update_plot_kwargs, key, var)) self.plot_kwargs_vars[key] = var elif key == "cmap": current_val = str(current_val) if current_val else "pqlx" - label = ttk.Label(self, text=PARAM_LABELS.get("cmap", "cmap") + ":") + label = ttk.Label( + self, text=PARAM_LABELS.get( + "cmap", "cmap") + ":") label.grid(row=row, column=0, sticky="w") ToolTip(label, PARAM_TOOLTIPS.get("cmap", "")) var = tk.StringVar(value=current_val) @@ -543,8 +572,8 @@ def build(self): ) combo.grid(row=row, column=1, sticky="w") combo.bind( - "<>", partial(self.update_plot_kwargs, key, var) - ) + "<>", partial( + self.update_plot_kwargs, key, var)) self.plot_kwargs_vars[key] = var else: var = tk.StringVar( @@ -555,12 +584,21 @@ def build(self): ToolTip(label, PARAM_TOOLTIPS.get(key, "")) ent = ttk.Entry(self, textvariable=var, width=30) ent.grid(row=row, column=1, sticky="w") - ent.bind("", partial(self.update_plot_kwargs, key, var)) + ent.bind( + "", partial( + self.update_plot_kwargs, key, var)) self.plot_kwargs_vars[key] = var row += 1 self.progress = ttk.Progressbar(self, maximum=100, mode="determinate") - self.progress.grid(row=row, column=0, columnspan=2, sticky="ew", pady=(5, 0)) + self.progress.grid( + row=row, + column=0, + columnspan=2, + sticky="ew", + pady=( + 5, + 0)) row += 1 self.status_label = ttk.Label(self, text="", foreground="gray") self.status_label.grid(row=row, column=0, columnspan=2, sticky="w") @@ -568,15 +606,25 @@ def build(self): # Group buttons in a frame btn_frame = ttk.Frame(self) btn_frame.grid(row=row, column=0, columnspan=3, sticky="w", pady=5) - ttk.Button(btn_frame, text="Run Dataset", command=self.run_this_dataset).pack( - side="left", padx=(0, 5) - ) + ttk.Button( + btn_frame, + text="Run Dataset", + command=self.run_this_dataset).pack( + side="left", + padx=( + 0, + 5)) ttk.Button( btn_frame, text="Delete Dataset", command=self.delete_this_dataset ).pack(side="left", padx=(0, 5)) ttk.Button( - btn_frame, text="Duplicate Dataset", command=self.duplicate_this_dataset - ).pack(side="left", padx=(0, 5)) + btn_frame, + text="Duplicate Dataset", + command=self.duplicate_this_dataset).pack( + side="left", + padx=( + 0, + 5)) def update_timewindow(self, *_): try: @@ -658,14 +706,14 @@ def __init__(self): super().__init__() self.title("PPSD Plotter GUI") self.geometry("1000x700") - # Set custom icon icon_path = os.path.join(os.path.dirname(__file__), "icon.ico") if os.path.exists(icon_path): self.iconbitmap(icon_path) - self.datasets = [] + self.datasets = [copy.deepcopy(DEFAULT_DATASET)] self.selected_dataset_index = None self.build_menu() self.build_main() + self.populate_datasets() def build_menu(self): menubar = tk.Menu(self) @@ -690,13 +738,13 @@ def build_main(self): ) self.scroll_frame = ttk.Frame(self.scroll) self.scroll_frame.bind( - "", - lambda e: self.scroll.configure(scrollregion=self.scroll.bbox("all")), - ) - self.scroll.create_window((0, 0), window=self.scroll_frame, anchor="nw") + "", lambda e: self.scroll.configure( + scrollregion=self.scroll.bbox("all")), ) + self.scroll.create_window( + (0, 0), window=self.scroll_frame, anchor="nw") self.scroll.configure( - yscrollcommand=self.scrollbar.set, xscrollcommand=self.hscrollbar.set - ) + yscrollcommand=self.scrollbar.set, + xscrollcommand=self.hscrollbar.set) self.scroll.pack(side="left", fill="both", expand=True) self.scrollbar.pack(side="right", fill="y") @@ -704,16 +752,17 @@ def build_main(self): controls = ttk.Frame(self) controls.pack(fill="x", padx=10, pady=5) - ttk.Button(controls, text="Add Dataset", command=self.add_dataset).pack( - side="left" - ) + ttk.Button( + controls, + text="Add Dataset", + command=self.add_dataset).pack( + side="left") def select_dataset(self, index): self.selected_dataset_index = index self.populate_datasets() # Refresh to update selection highlight def add_dataset(self): - import copy self.datasets.append(copy.deepcopy(DEFAULT_DATASET)) self.populate_datasets() @@ -756,9 +805,11 @@ def delete_dataset(self, index): def duplicate_dataset(self, index): if 0 <= index < len(self.datasets): - import copy - self.datasets.insert(index + 1, copy.deepcopy(self.datasets[index])) + self.datasets.insert( + index + 1, + copy.deepcopy( + self.datasets[index])) self.populate_datasets() def load_config(self): @@ -784,7 +835,7 @@ def save_config(self): filepath = filedialog.asksaveasfilename( defaultextension=".yaml", filetypes=[("YAML files", "*.yaml *.yml")], - ) + ) if not filepath: return for ds in self.datasets: @@ -797,13 +848,12 @@ def save_config(self): def new_config(self): if self.datasets: confirm = messagebox.askyesno( - "New Configuration", "This will discard the current configuration.\nContinue?" - ) + "New Configuration", + "This will discard the current configuration.\nContinue?") else: confirm = True if not confirm: return - import copy self.datasets = [copy.deepcopy(DEFAULT_DATASET)] self.populate_datasets() From 40d956db30897887fedf3921172ec7432c0d0486 Mon Sep 17 00:00:00 2001 From: Dmitry Sidorov-Biryukov <99192142+msbdd@users.noreply.github.com> Date: Fri, 4 Jul 2025 12:43:09 +0200 Subject: [PATCH 5/6] Added configuration to build GUI; Minor changes; Added GUI test config with the same dataset; --- .github/workflows/Distribute_Windows.yml | 33 +++++++++++++---- example/example_config.yaml | 4 +-- example/example_config_GUI.yaml | 39 ++++++++++++++++++++ setup_gui.py | 46 ++++++++++++++++++++++++ src/gui.py | 4 +-- 5 files changed, 115 insertions(+), 11 deletions(-) create mode 100644 example/example_config_GUI.yaml create mode 100644 setup_gui.py diff --git a/.github/workflows/Distribute_Windows.yml b/.github/workflows/Distribute_Windows.yml index 9c1faf4..737eea4 100644 --- a/.github/workflows/Distribute_Windows.yml +++ b/.github/workflows/Distribute_Windows.yml @@ -42,28 +42,47 @@ jobs: - name: Get version from tag run: echo "VERSION=${{ github.ref_name }}" | Out-File -FilePath $env:GITHUB_ENV -Append - - name: Debug VERSION - run: echo "VERSION=$env:VERSION" + # CLI build - name: Replace version in setup.py run: | (Get-Content setup.py) -replace '__VERSION__', "$env:VERSION" | Set-Content setup.py - - name: Build with cx_Freeze + - name: Build CLI with cx_Freeze run: | python setup.py build - - name: Rename build output + - name: Rename CLI build output run: | Move-Item build\PPSD_Plotter_Windows ("build\PPSD_Plotter_" + $env:VERSION + "_Windows") - - name: Create distribution archive + - name: Create CLI distribution archive run: | Compress-Archive -Path ("build\PPSD_Plotter_" + $env:VERSION + "_Windows") -DestinationPath ("PPSD_Plotter_" + $env:VERSION + "_Windows.zip") + + # Gui build + + - name: Replace version in setup_gui.py + run: | + (Get-Content setup_gui.py) -replace '__VERSION__', "$env:VERSION" | Set-Content setup_gui.py + + - name: Build GUI with cx_Freeze + run: | + python setup_gui.py build + + - name: Rename GUI build output + run: | + Move-Item build\PPSD_Plotter_Windows_GUI ("build\PPSD_Plotter_" + $env:VERSION + "_Windows_GUI") + + - name: Create GUI distribution archive + run: | + Compress-Archive -Path ("build\PPSD_Plotter_" + $env:VERSION + "_Windows_GUI") -DestinationPath ("PPSD_Plotter_" + $env:VERSION + "_Windows_GUI.zip") - name: Upload Release Asset uses: softprops/action-gh-release@v2 with: - files: PPSD_Plotter_${{ github.ref_name }}_Windows.zip + files: | + PPSD_Plotter_${{ github.ref_name }}_Windows.zip + PPSD_Plotter_${{ github.ref_name }}_Windows_GUI.zip env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/example/example_config.yaml b/example/example_config.yaml index cc138a8..4243e39 100644 --- a/example/example_config.yaml +++ b/example/example_config.yaml @@ -16,5 +16,5 @@ datasets: action: full output_folder: "example/result" figsize: [6, 6] - show_mean: yes - grid: no + show_mean: true + grid: true diff --git a/example/example_config_GUI.yaml b/example/example_config_GUI.yaml new file mode 100644 index 0000000..ac43113 --- /dev/null +++ b/example/example_config_GUI.yaml @@ -0,0 +1,39 @@ +datasets: +- action: full + channels: + - 00.BH1 + - 00.BH2 + - 00.BHZ + folder: example/IU.ANMO..D + plot_kwargs: + cmap: pqlx + cumulative: false + grid: true + show_coverage: true + show_histogram: true + show_mean: false + show_mode: false + show_noise_models: true + show_percentiles: false + xaxis_frequency: false + response: example/IU_ANMO_RESP.xml + timewindow: 600 +- action: full + channels: + - 00.BH1 + - 00.BH2 + - 00.BHZ + folder: example/IU.GRFO..D + plot_kwargs: + cmap: pqlx + cumulative: false + grid: true + show_coverage: true + show_histogram: true + show_mean: true + show_mode: false + show_noise_models: true + show_percentiles: false + xaxis_frequency: false + response: example/IU_GRFO_RESP.xml + timewindow: 600 diff --git a/setup_gui.py b/setup_gui.py new file mode 100644 index 0000000..10499b7 --- /dev/null +++ b/setup_gui.py @@ -0,0 +1,46 @@ +from cx_Freeze import setup, Executable +import sys +import os +from pathlib import Path +import matplotlib +import obspy + +mpl_data_path = matplotlib.get_data_path() +sys.setrecursionlimit(8000) + +obsipy_data_dir = os.path.join( + os.path.dirname(obspy.__file__), "imaging", "data" + ) + +site_packages = next(p for p in sys.path if 'site-packages' in p) +dist_info = next(Path(site_packages).glob("obspy-*.dist-info")) + +exe = Executable( + script=os.path.join("src", "gui.py"), + base=None if sys.platform == "win32" else None, + target_name="PPSD_Plot_GUI", + icon="src/icon.ico" +) + +setup( + name="PPSD_Plotter", + version="__VERSION__", + description="A simple PPSD plotting tool based on ObsPy", + options={ + "build_exe": { + "packages": ["obspy", "matplotlib", "yaml", "numpy", + "tqdm", "os", "pathlib"], + "excludes": [], + "include_files": [ + (str(dist_info), f"lib/{dist_info.name}"), + (mpl_data_path, "lib/matplotlib/mpl-data"), + (obsipy_data_dir, "lib/obspy/imaging/data"), + ("example", "example"), + ("LICENSE", "LICENSE"), + ("src/icon.ico", "src/icon.ico"), + ], + "build_exe": "build/PPSD_Plotter_Windows_GUI" + } + }, + executables=[exe], +) diff --git a/src/gui.py b/src/gui.py index 3ed5b9a..9a5e1ae 100644 --- a/src/gui.py +++ b/src/gui.py @@ -91,7 +91,7 @@ "response": "", "channels": [], "action": "full", - "timewindow": 600, + "timewindow": 3600, "plot_kwargs": DEFAULT_PLOT_KWARGS.copy(), } @@ -760,7 +760,7 @@ def build_main(self): def select_dataset(self, index): self.selected_dataset_index = index - self.populate_datasets() # Refresh to update selection highlight + self.populate_datasets() def add_dataset(self): From e173e8a1897363ba559e3aa9775dd85c73d4a973 Mon Sep 17 00:00:00 2001 From: Dmitry Sidorov-Biryukov <99192142+msbdd@users.noreply.github.com> Date: Fri, 4 Jul 2025 13:00:56 +0200 Subject: [PATCH 6/6] Added icon for the GUI; --- resources/icon.ico | Bin 0 -> 61872 bytes setup_gui.py | 4 ++-- src/gui.py | 14 ++++++++++++-- 3 files changed, 14 insertions(+), 4 deletions(-) create mode 100644 resources/icon.ico diff --git a/resources/icon.ico b/resources/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..f0c494126fbad3ff1168880640043d8ea5146ec7 GIT binary patch literal 61872 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