[学習記録.py]最新計算ツール

#main.py
from viewer import WaveformViewer

if __name__ == "__main__":

    app = WaveformViewer()

    app.mainloop()

#folder_loader.py
import numpy as np
import pandas as pd
import tkinter as tk
import os
from tkinter import messagebox
from data_loader import load_signal
from scipy.interpolate import interp1d
from signal_processing import *

def drop_folders(self, event):
    
    print("CALLED")

    paths = self.tk.splitlist(event.data)

    self.datasets = []

    csv_all = []

    for path in paths:

        if os.path.isdir(path):

            csv_files = [
                os.path.join(path, f)
                for f in os.listdir(path)
                if f.lower().endswith(".csv")
            ]

            csv_all.extend(csv_files)

    # 共通時間範囲を取得
    start_times = []
    end_times = []

    for fullpath in csv_all:

        ts = pd.read_csv(
            fullpath,
            header=None,
            usecols=[1]
        )

        timestamp = pd.to_numeric(
            ts.iloc[:, 0],
            errors="coerce"
        ).fillna(0).values

        start_times.append(timestamp[0])
        end_times.append(timestamp[-1])

    common_start = max(start_times)
    common_end = min(end_times)
    self.time_aligned = True

    #タイムスタンプの時間がラップしていない場合、位相を非表示
    if common_start >= common_end:
        common_start = None
        common_end = None
        self.time_aligned = False
    

    if len(csv_all) == 0:
        messagebox.showerror("Error", "No CSV files")
        return

    if len(csv_all) > 100:
        csv_all = csv_all[:100]
        
    
    for fullpath in csv_all:

        filename = os.path.basename(fullpath)

        raw_x, raw_y, raw_z, x, y, z, timestamp, fs, sync_edges, has_sync = load_signal(
            fullpath,
            common_start,
            common_end
        )

        dataset = {
            "fullpath": fullpath,
            "folder": os.path.basename(os.path.dirname(fullpath)),

            "timestamp": timestamp,
            "raw_x": raw_x,
            "raw_y": raw_y,
            "raw_z": raw_z,
            "x": x,
            "y": y,
            "z": z,
            "fs": fs,
            "synthesize": np.sqrt(x**2 + y**2 + z**2),
            "t": np.arange(len(x)) / fs,
            "sync_edges": sync_edges,
            "has_sync": has_sync,

            "fft_x": calc_fft(x, fs),
            "fft_y": calc_fft(y, fs),
            "fft_z": calc_fft(z, fs),
            "fft_syn": synthesize_fft(x, y, z, fs),
            "lockin_cache": {}

        }
        #同期信号のデバッグ用表示
        print(
            filename,
            "signal events =",
            len(sync_edges),
            sync_edges[:10]
        )
        self.datasets.append(dataset)

        fs_set = {d["fs"] for d in self.datasets}

        #fsが異なるデータを同時に取り込んだ際にエラーを出す
        if len(fs_set) > 1:
            self.datasets.clear()

            messagebox.showerror(
                "Error",
                f"Sampling frequency mismatch: {sorted(fs_set)}"
            )
            return

    # 外部同期信号がある場合時間ワープ
    use_sync = all(
        d["has_sync"]
        for d in self.datasets
    )

    if use_sync and len(self.datasets) >= 2:

        print("External Sync Found -> Time Warp ON")

        ref_edges = self.datasets[0]["sync_edges"]

        if len(ref_edges) >= 2:

            ref_len = len(self.datasets[0]["x"])

            for i in range(1, len(self.datasets)):

                dataset = self.datasets[i]

                src_edges = dataset["sync_edges"]

                if len(src_edges) < 2:
                    continue

                n = min(
                    len(ref_edges),
                    len(src_edges)
                )

                ref = ref_edges[:n]
                src = src_edges[:n]

                try:

                    warp = interp1d(
                        src,
                        ref,
                        kind="linear",
                        fill_value="extrapolate"
                    )

                    src_idx = np.arange(
                        len(dataset["x"])
                    )

                    warped_idx = warp(src_idx)

                    dataset["x"] = np.interp(
                        np.arange(ref_len),
                        warped_idx,
                        dataset["x"]
                    )

                    dataset["y"] = np.interp(
                        np.arange(ref_len),
                        warped_idx,
                        dataset["y"]
                    )

                    dataset["z"] = np.interp(
                        np.arange(ref_len),
                        warped_idx,
                        dataset["z"]
                    )

                    dataset["raw_x"] = np.interp(
                        np.arange(ref_len),
                        warped_idx,
                        dataset["raw_x"]
                    )

                    dataset["raw_y"] = np.interp(
                        np.arange(ref_len),
                        warped_idx,
                        dataset["raw_y"]
                    )

                    dataset["raw_z"] = np.interp(
                        np.arange(ref_len),
                        warped_idx,
                        dataset["raw_z"]
                    )

                    dataset["synthesize"] = np.sqrt(
                        dataset["x"]**2
                        +
                        dataset["y"]**2
                        +
                        dataset["z"]**2
                    )

                    dataset["t"] = (
                        np.arange(ref_len)
                        /
                        dataset["fs"]
                    )

                    dataset["fft_x"] = calc_fft(
                        dataset["x"],
                        dataset["fs"]
                    )

                    dataset["fft_y"] = calc_fft(
                        dataset["y"],
                        dataset["fs"]
                    )

                    dataset["fft_z"] = calc_fft(
                        dataset["z"],
                        dataset["fs"]
                    )

                    dataset["fft_syn"] = synthesize_fft(
                        dataset["x"],
                        dataset["y"],
                        dataset["z"],
                        dataset["fs"]
                    )

                    print(
                        dataset["folder"],
                        "time warp applied"
                    )

                except Exception as e:

                    print(
                        dataset["folder"],
                        "warp failed:",
                        e
                    )

    else:

        print(
            "External Sync Not Found -> Timestamp Sync Only"
        )

    # 最初のデータ表示
    self.current_dataset = self.datasets[0]

    self.raw_x = self.current_dataset["raw_x"]
    self.raw_y = self.current_dataset["raw_y"]
    self.raw_z = self.current_dataset["raw_z"]

    self.x = self.current_dataset["x"]
    self.y = self.current_dataset["y"]
    self.z = self.current_dataset["z"]

    self.synthesize = self.current_dataset["synthesize"]
    self.t_full = self.current_dataset["t"]
    
    self.fs_entry.config(state="normal")
    self.fs_entry.delete(0, tk.END)
    self.fs_entry.insert(0, str(fs))
    self.fs_entry.config(state="readonly")

    # コンボボックス(=プルダウン)更新
    names = [d["folder"] for d in self.datasets]

    self.data1_combo["values"] = names
    self.data2_combo["values"] = names

    if len(names) >= 1:
        self.data1_combo.current(0)

    if len(names) >= 2:
        self.data2_combo.current(1)
    else:
        self.data2_combo.current(0)

    self.plot_xyz()
    self.update_analysis_plot()   
    self.update_grip_fft()
    

    messagebox.showinfo(
        "Loaded",
        f"{len(self.datasets)} files loaded"
    )

#viwer.py
import tkinter as tk
from tkinter import *
from tkinter import ttk
from tkinter import filedialog
from tkinter import messagebox

from tkinterdnd2 import *

from openpyxl import load_workbook
from openpyxl.drawing.image import Image as XLImage


import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

from matplotlib.backends.backend_tkagg import (
    FigureCanvasTkAgg,
    NavigationToolbar2Tk
)

from pathlib import Path
import os

from matplotlib.widgets import SpanSelector
from signal_processing import *
from follder_loader import *

#フォントを設定
plt.rcParams["font.family"] = "Meiryo"

#---------------定数---------------
FREQ_DEFAULT = 240
LPF_DEFAULT = 3
AVG_START = 0.5
AVG_END = 22

#-----------------------------------

class WaveformViewer(TkinterDnD.Tk):     
    #==========================================
    #初期化
    #===========================================
    # 初期化処理
    def __init__(self):
        super().__init__()
        self.title("Waveform Viewer XYZ")
        self.geometry("800x600")

        self.protocol("WM_DELETE_WINDOW", self.on_close)

        self.x = self.y = self.z = None
        self.t_full = None
        self.save_path = None
        self.datasets = []

        self._build_controls()
        self._build_plot()

        self.update_lpf_display()

        self.current_button = 1
        self.canvas.mpl_connect("button_press_event", self.on_mouse_press)
        # self.canvas.get_tk_widget().bind("<Button-3>", lambda e: "break")

        self.colors = {
            "X": "tab:red",
            "Y": "tab:green",
            "Z": "tab:blue",
            "synthesize": "tab:purple"
        }

        self.display_idx1 = tk.IntVar(value=0)
        self.display_idx2 = tk.IntVar(value=0)

        self.time_aligned = True

    #確実に終了させる
    def on_close(self):  
        self.quit()
        self.destroy()
    
    #==========================================
    #GUI入力値取得
    #===========================================
    def get_freq(self):
        return float(self.freq_entry.get())
    
    # def get_lpf_cutoff(self):
    #     return float(self.lpf_entry.get())
    def get_lpf_cutoff(self):

        if self.use_auto_lpf.get():
            freq = self.get_freq()
            lpf = min(max(freq * 0.03, 1.0), 20.0)
        else:
            lpf = float(self.lpf_entry.get())

        return lpf

    
    drop_folders = drop_folders

    def update_lpf_display(self):
        if self.use_auto_lpf.get():
            freq = self.get_freq()
            lpf = min(max(freq * 0.03, 1.0), 20.0)

            self.lpf_entry.config(state="normal")
            self.lpf_entry.delete(0, tk.END)
            self.lpf_entry.insert(0, f"{lpf:.2f}")
            self.lpf_entry.config(state="disabled")


    def toggle_lpf_mode(self):
        if self.use_auto_lpf.get():
            self.lpf_entry.config(state="disabled")
        else:
            self.lpf_entry.config(state="normal")
        self.update_lpf_display()

        self.plot_xyz()
        self.update_analysis_plot()
        self.update_grip_fft()

    #==========================================
    #GUI構築
    #===========================================
    def _build_controls(self):
        frame1 = ttk.Frame(self)
        frame1.pack(side="top", fill="x", pady=5)
        
        frame2 = ttk.Frame(self)
        frame2.pack(side="top", fill="x", pady=5)

        ttk.Label(frame1, text="fs [Hz]").pack(side="left")
        self.fs_entry = ttk.Entry(frame1, width=8)
        self.fs_entry.insert(0, 1000)
        self.fs_entry.config(state="disabled")
        self.fs_entry.pack(side="left", padx=2)

        ttk.Label(frame1, text="Demod Freq [Hz]").pack(side="left")
        self.freq_entry = ttk.Entry(frame1, width=8)
        self.freq_entry.insert(0, str(FREQ_DEFAULT))
        self.freq_entry.pack(side="left", padx=2)

        ttk.Label(frame1, text="LPF Cutoff [Hz]").pack(side="left")
        self.lpf_entry = ttk.Entry(frame1, width=8)
        self.lpf_entry.insert(0, str(LPF_DEFAULT))
        self.lpf_entry.pack(side="left", padx=2)


        ttk.Label(frame1, text="Data1").pack(side="left")

        self.data1_combo = ttk.Combobox(
            frame1,
            width=15,
            state="readonly"
        )
        self.data1_combo.pack(side="left", padx=2)
        self.data1_combo.bind(
            "<<ComboboxSelected>>",
            lambda e: (
                self.plot_xyz(),
                self.update_analysis_plot(),
                self.update_grip_fft(),
            )
        )

        ttk.Label(frame1, text="Data2").pack(side="left")

        self.data2_combo = ttk.Combobox(
            frame1,
            width=15,
            state="readonly"
        )
        self.data2_combo.pack(side="left", padx=2)
        self.data2_combo.bind(
            "<<ComboboxSelected>>",
            lambda e: (
                self.plot_xyz(),
                self.update_analysis_plot(),
                self.update_grip_fft(),
            )
        )

        #-------表示する軸----------
        self.show_x = tk.BooleanVar(value=True)
        self.show_y = tk.BooleanVar(value=True)
        self.show_z = tk.BooleanVar(value=True)
        self.show_synthesize = tk.BooleanVar(value=True)
        self.show_unwrap = tk.BooleanVar(value=False)

        ttk.Checkbutton(
            frame1,
            text="X",
            variable=self.show_x,
            command=lambda: (
                self.plot_xyz(),
                self.update_analysis_plot(),
                self.update_grip_fft()
            )
        ).pack(side="left")

        ttk.Checkbutton(
            frame1,
            text="Y",
            variable=self.show_y,
            command=lambda: (
                self.plot_xyz(),
                self.update_analysis_plot(),
                self.update_grip_fft()
            )
        ).pack(side="left")

        ttk.Checkbutton(
            frame1,
            text="Z",
            variable=self.show_z,
            command=lambda: (
                self.plot_xyz(),
                self.update_analysis_plot(),
                self.update_grip_fft()
            )
        ).pack(side="left")

        ttk.Checkbutton(
            frame1,
            text="synthesize",
            variable=self.show_synthesize,
            command=lambda: (
                self.plot_xyz(),
                self.update_analysis_plot(),
                self.update_grip_fft()
            )
        ).pack(side="left")

        ttk.Label(frame1,text=" | ").pack(side="left")

        ttk.Checkbutton(
            frame1,
            text="unwrap",
            variable=self.show_unwrap,
            command=lambda: (
                self.update_analysis_plot(),
            )
        ).pack(side="left")

        ttk.Label(frame1,text=" | ").pack(side="left")
        self.show_peak_time = tk.BooleanVar(value=False)

        ttk.Checkbutton(
            frame1,
            text="Peak Time",
            variable=self.show_peak_time,
            command=self.update_analysis_plot
        ).pack(side="left")

        ttk.Label(frame1,text=" | ").pack(side="left")
        self.use_auto_lpf = tk.BooleanVar(value=False)

        ttk.Checkbutton(
            frame1,
            text="auto_lpf",
            variable=self.use_auto_lpf,
            command=self.toggle_lpf_mode
        ).pack(side="left")


        if self.use_auto_lpf.get():
            self.lpf_entry.config(state="disabled")

        self.freq_entry.bind(
            "<Return>",
            lambda e: (
                self.update_lpf_display(), 
                self.plot_xyz(),
                self.update_analysis_plot()
            )
        )

        self.lpf_entry.bind(
            "<Return>",
            lambda e: (
                self.plot_xyz(),
                self.update_analysis_plot()
            )
        )


        ttk.Button(frame1, text="Select Excel File", command=self.select_excel_file).pack(side="left", padx=5)

        ttk.Button(frame1, text="Save_file", command=self.save_file).pack(side="left", padx=5)
        ttk.Button(frame1, text="Save_wave_pic", command=self.save_picture).pack(side="left", padx=5)
        ttk.Button(frame1,text="Save_grip_Pic",command=self.save_grip_pic).pack(side="left", padx=5)
        
        ttk.Label(frame2,text=" Data1 | ").pack(side="left")
        ttk.Label(frame2, text="Free Start1").pack(side="left")
        self.free_start1_entry = ttk.Entry(frame2, width=6)
        self.free_start1_entry.insert(0, "0")
        self.free_start1_entry.pack(side="left")

        ttk.Label(frame2, text="Free End1").pack(side="left")

        self.free_end1_entry = ttk.Entry(frame2, width=6)
        self.free_end1_entry.insert(0, "5")
        self.free_end1_entry.pack(side="left")

        ttk.Label(frame2,text=" | ").pack(side="left")
        ttk.Label(frame2, text="Grip Start1").pack(side="left")

        self.grip_start1_entry = ttk.Entry(frame2, width=6)
        self.grip_start1_entry.insert(0, "5")
        self.grip_start1_entry.pack(side="left")

        ttk.Label(frame2, text="Grip End1").pack(side="left")

        self.grip_end1_entry = ttk.Entry(frame2, width=6)
        self.grip_end1_entry.insert(0, "10")
        self.grip_end1_entry.pack(side="left")

        ttk.Label(frame2, text="", width=5).pack(side="left")
        ttk.Label(frame2,text=" Data2 | ").pack(side="left")
        ttk.Label(frame2, text="Free Start2").pack(side="left")

        self.free_start2_entry = ttk.Entry(frame2, width=6)
        self.free_start2_entry.insert(0, "0")
        self.free_start2_entry.pack(side="left")

        ttk.Label(frame2, text="Free End2").pack(side="left")

        self.free_end2_entry = ttk.Entry(frame2, width=6)
        self.free_end2_entry.insert(0, "5")
        self.free_end2_entry.pack(side="left")

        ttk.Label(frame2,text=" | ").pack(side="left")
        ttk.Label(frame2, text="Grip Start2").pack(side="left")

        self.grip_start2_entry = ttk.Entry(frame2, width=6)
        self.grip_start2_entry.insert(0, "5")
        self.grip_start2_entry.pack(side="left")

        ttk.Label(frame2, text="Grip End2").pack(side="left")

        self.grip_end2_entry = ttk.Entry(frame2, width=6)
        self.grip_end2_entry.insert(0, "10")
        self.grip_end2_entry.pack(side="left")

        ttk.Button(frame2,text="Update Grip FFT",command=self.update_grip_fft).pack(side="left", padx=8)
        
        
        ttk.Label(frame2, text="FFT Ymax").pack(side="left")

        self.fft_ymax_entry = ttk.Entry(frame2, width=6)
        self.fft_ymax_entry.insert(0, "")
        self.fft_ymax_entry.pack(side="left")

        ttk.Label(frame2, text="Amp Ymax").pack(side="left")

        self.amp_ymax_entry = ttk.Entry(frame2, width=6)
        self.amp_ymax_entry.insert(0, "")
        self.amp_ymax_entry.pack(side="left")

        self.fft_ymax_entry.bind(
            "<Return>",
            lambda e: (
                self.plot_xyz(),
                self.update_analysis_plot()
            )
        )

        self.amp_ymax_entry.bind(
            "<Return>",
            lambda e: (
                self.plot_xyz(),
                self.update_analysis_plot()
            )
        )

        ttk.Label(frame2, text="FFT Xmax").pack(side="left")

        self.fft_xmax_entry = ttk.Entry(frame2, width=6)
        self.fft_xmax_entry.insert(0, "")
        self.fft_xmax_entry.pack(side="left")

        self.fft_xmax_entry.bind(
            "<Return>",
            lambda e: (
                self.plot_xyz(),
                self.update_analysis_plot()
            )
        )

        # ---- Drag & Drop ----
        self.drop_label = tk.Label(
            self,
            text="Drag & Drop Folder Here",
            relief="groove",
            height=3
        )

        self.drop_label.pack(fill="x", padx=10, pady=5)

        self.drop_label.drop_target_register(DND_FILES)
        self.drop_label.dnd_bind("<<Drop>>", self.drop_folders)

        entries = [
            self.free_start1_entry,
            self.free_end1_entry,
            self.grip_start1_entry,
            self.grip_end1_entry,
            self.free_start2_entry,
            self.free_end2_entry,
            self.grip_start2_entry,
            self.grip_end2_entry,
        ]

        for e in entries:
            e.bind("<Return>", lambda ev: self.update_span_visual())
    
    def get_fft_xmax(self):
        txt = self.fft_xmax_entry.get().strip()

        try:
            return float(txt)
        except ValueError:
            return None
    
    def get_fft_ymax(self):
        txt = self.fft_ymax_entry.get().strip()

        try:
            return float(txt)
            
        except ValueError:
            return None


    def get_amp_ymax(self):
        txt = self.amp_ymax_entry.get().strip()

        try:
            return float(txt)
        
        except ValueError:
            return None
        
    def _build_plot(self):

        # =========================================================
        # Notebook
        # =========================================================

        self.notebook = ttk.Notebook(self)
        self.notebook.pack(fill="both", expand=True)

        # =========================================================
        # Tab1 : Waveform
        # =========================================================

        self.wave_tab = ttk.Frame(self.notebook)
        self.notebook.add(self.wave_tab, text="Waveform")

        # =========================================================
        # Tab2 : Analysis
        # =========================================================

        self.analysis_tab = ttk.Frame(self.notebook)
        self.notebook.add(self.analysis_tab, text="Analysis")


        # =========================================================
        # Tab3 : Grip FFT
        # =========================================================

        
        self.grip_tab1 = ttk.Frame(self.notebook)
        self.grip_tab2 = ttk.Frame(self.notebook)

        self.notebook.add(self.grip_tab1, text="Grip Data1")
        self.notebook.add(self.grip_tab2, text="Grip Data2")


        # =========================================================
        # Waveform TAB
        # =========================================================

        wave_toolbar_frame = ttk.Frame(self.wave_tab)
        wave_toolbar_frame.pack(side="top", fill="x")

        wave_canvas_frame = ttk.Frame(self.wave_tab)
        wave_canvas_frame.pack(side="top", fill="both", expand=True)

        self.fig = plt.Figure(figsize=(12, 8))

        self.ax1 = self.fig.add_subplot(221)
        self.ax2 = self.fig.add_subplot(223)

        self.ax3 = self.fig.add_subplot(
            222,
            sharex=self.ax1,
            sharey=self.ax1
        )

        self.ax4 = self.fig.add_subplot(
            224,
            sharex=self.ax2,
            sharey=self.ax2
        )

        self.canvas = FigureCanvasTkAgg(
            self.fig,
            master=wave_canvas_frame
        )

        self.canvas.draw_idle()

        self.canvas.get_tk_widget().pack(
            fill="both",
            expand=True
        )

        self.toolbar = NavigationToolbar2Tk(
            self.canvas,
            wave_toolbar_frame
        )

        self.toolbar.update()

        # =========================================================
        # Analysis TAB
        # =========================================================

        analysis_toolbar_frame = ttk.Frame(self.analysis_tab)
        analysis_toolbar_frame.pack(side="top", fill="x")

        analysis_canvas_frame = ttk.Frame(self.analysis_tab)
        analysis_canvas_frame.pack(side="top", fill="both", expand=True)

        self.analysis_fig = plt.Figure(figsize=(12, 8))

        self.axa1 = self.analysis_fig.add_subplot(221)
        self.axa2 = self.analysis_fig.add_subplot(223)

        self.axa3 = self.analysis_fig.add_subplot(222,sharex=self.axa1,sharey=self.axa1)
        self.axa4 = self.analysis_fig.add_subplot(224,sharex=self.axa2,sharey=self.axa2)

        self.analysis_canvas = FigureCanvasTkAgg(
            self.analysis_fig,
            master=analysis_canvas_frame
        )

        self.analysis_canvas.draw_idle()

        self.analysis_canvas.get_tk_widget().pack(
            fill="both",
            expand=True
        )

        self.analysis_toolbar = NavigationToolbar2Tk(
            self.analysis_canvas,
            analysis_toolbar_frame
        )

        self.analysis_toolbar.update()

        # =========================================================
        # Grip FFT TAB
        # =========================================================

        grip_tab1_toolbar_frame = ttk.Frame(self.grip_tab1)
        grip_tab1_toolbar_frame.pack(side="top", fill="x")

        # Data1用     
        self.label_grip1 = ttk.Label(self.grip_tab1, text="Data1", font=("Meiryo", 14))
        self.label_grip1.pack(pady=5)
        
        self.grip_fig1 = plt.Figure(figsize=(10,8))
        self.grip_ax1_1 = self.grip_fig1.add_subplot(121)
        self.grip_ax1_2 = self.grip_fig1.add_subplot(122,sharey=self.grip_ax1_1)

        self.grip_canvas1 = FigureCanvasTkAgg(
            self.grip_fig1,
            master=self.grip_tab1
        )
        self.grip_canvas1.get_tk_widget().pack(fill="both", expand=True)

        self.grip_toolbar1 = NavigationToolbar2Tk(
            self.grip_canvas1,
            grip_tab1_toolbar_frame
        )
        self.grip_toolbar1.update()



        grip_tab2_toolbar_frame = ttk.Frame(self.grip_tab2)
        grip_tab2_toolbar_frame.pack(side="top", fill="x")
        # Data2用
        self.label_grip2 = ttk.Label(self.grip_tab2, text="Data2", font=("Meiryo", 14))
        self.label_grip2.pack(pady=5)

        self.grip_fig2 = plt.Figure(figsize=(10,8))
        self.grip_ax2_1 = self.grip_fig2.add_subplot(121)
        self.grip_ax2_2 = self.grip_fig2.add_subplot(122,sharey=self.grip_ax2_1)

        self.grip_canvas2 = FigureCanvasTkAgg(
            self.grip_fig2,
            master=self.grip_tab2
        )
        self.grip_canvas2.get_tk_widget().pack(fill="both", expand=True)

        self.grip_toolbar2 = NavigationToolbar2Tk(
            self.grip_canvas2,
            grip_tab2_toolbar_frame
        )
        self.grip_toolbar2.update()
    
    def select_excel_file(self):

        path = filedialog.asksaveasfilename(
            defaultextension=".xlsx",
            filetypes=[("Excel files", "*.xlsx")]
        )

        if path:
            self.excel_path = path

            # 初回は新規作成
            from openpyxl import Workbook

            # Excel読み込み
            if not os.path.exists(path):
                wb = Workbook()
            else:
                wb = load_workbook(path)

            freq = int(self.get_freq())
            lpf = int(self.get_lpf_cutoff())
            sheet_name = f"{freq}Hz_LPF{lpf}"

            if sheet_name not in wb.sheetnames:
                ws = wb.create_sheet(sheet_name)
            else:
                ws = wb[sheet_name]
            
            ws.save(path)

 
    def update_grip_fft(self):

        if len(self.datasets) == 0:
            return

        idx1 = self.data1_combo.current()
        idx2 = self.data2_combo.current()

        dataset1 = self.datasets[idx1]
        dataset2 = self.datasets[idx2]

        self.label_grip1.config(text=f"Data1 : {dataset1['folder']}")
        self.label_grip2.config(text=f"Data2 : {dataset2['folder']}")

        # ---- Data1 ----
        self._draw_grip(dataset1, self.grip_ax1_1, self.grip_ax1_2, mode="1")
        self.grip_canvas1.draw_idle()

        # ---- Data2 ----
        self._draw_grip(dataset2, self.grip_ax2_1, self.grip_ax2_2, mode="2")
        self.grip_canvas2.draw_idle()



    def _draw_grip(self, dataset, ax1, ax2, mode="1"):

        ax1.clear()
        ax2.clear()

        fs = dataset["fs"]

        if mode == "1":
            free_start = float(self.free_start1_entry.get())
            free_end   = float(self.free_end1_entry.get())
            grip_start = float(self.grip_start1_entry.get())
            grip_end   = float(self.grip_end1_entry.get())
        else:
            free_start = float(self.free_start2_entry.get())
            free_end   = float(self.free_end2_entry.get())
            grip_start = float(self.grip_start2_entry.get())
            grip_end   = float(self.grip_end2_entry.get())

        axes = [
            ("X", dataset["x"]),
            ("Y", dataset["y"]),
            ("Z", dataset["z"]),
        ]

        # ---- 各軸 ----
        for name, sig in axes:

            if name == "X" and not self.show_x.get():
                continue
            if name == "Y" and not self.show_y.get():
                continue
            if name == "Z" and not self.show_z.get():
                continue

            freq, amp = calc_fft(sig, fs, free_start, free_end)
            if freq is not None:
                peak_f, peak_a = find_fft_peak(freq, amp)
                ax1.plot(freq, amp, label=f"{name} (peak {peak_f:.1f} Hz, {peak_a:.4f} G)", color=self.colors[name])
                ax1.plot(peak_f,peak_a,"o",mfc="none",mec=self.colors[name],mew=2,ms=8)
       

            freq, amp = calc_fft(sig, fs, grip_start, grip_end)
            if freq is not None:
                peak_f, peak_a = find_fft_peak(freq, amp)
                ax2.plot(freq, amp, label=f"{name} (peak {peak_f:.1f} Hz, {peak_a:.4f} G)", color=self.colors[name])
                ax2.plot(peak_f,peak_a,"o",mfc="none",mec=self.colors[name],mew=2,ms=8)
        # -------------------
        # synthesize(合成)
        # -------------------
        if self.show_synthesize.get():

            # ---- free ----
            freq, amp = synthesize_fft(
                dataset["x"],
                dataset["y"],
                dataset["z"],
                fs,
                free_start,
                free_end
            )

            if freq is not None:
                peak_f, peak_a = find_fft_peak(freq, amp)

                ax1.plot(
                    freq, amp,
                    label=f"syn (peak {peak_f:.1f} Hz, {peak_a:.4f} G)",
                    color=self.colors["synthesize"]
                )

                ax1.plot(
                    peak_f, peak_a,
                    "o",
                    mfc="none",
                    mec=self.colors["synthesize"],
                    mew=2,
                    ms=8
                )

            # ---- grip ----
            freq, amp = synthesize_fft(
                dataset["x"],
                dataset["y"],
                dataset["z"],
                fs,
                grip_start,
                grip_end
            )

            if freq is not None:
                peak_f, peak_a = find_fft_peak(freq, amp)

                ax2.plot(
                    freq, amp,
                    label=f"syn (peak {peak_f:.1f} Hz, {peak_a:.4f} G)",
                    color=self.colors["synthesize"]
                )

                ax2.plot(
                    peak_f, peak_a,
                    "o",
                    mfc="none",
                    mec=self.colors["synthesize"],
                    mew=2,
                    ms=8
                )
    
        ax1.set_title("非把持")
        ax2.set_title("把持")

        ax1.set_xlabel("周波数[Hz]")
        ax2.set_xlabel("周波数[Hz]")
        ax1.set_ylabel("振幅[G]")
        ax2.set_ylabel("振幅[G]")

        if ax1.has_data():
            ax1.legend(loc="best")
        if ax2.has_data():
            ax2.legend(loc="best")
        # ------------------------
        # Avg / RMS テキスト表示
        # ------------------------

        # 合成振幅を作る
        freq = self.get_freq()
        lpf = self.get_lpf_cutoff()

        mag_x, _ = self.get_lockin(dataset, "X")
        mag_y, _ = self.get_lockin(dataset, "Y")
        mag_z, _ = self.get_lockin(dataset, "Z")

        sig = np.sqrt(mag_x**2 + mag_y**2 + mag_z**2)

        # 区間ごとに計算
        free_stats = self.calc_interval_stats(sig, fs, free_start, free_end)
        grip_stats = self.calc_interval_stats(sig, fs, grip_start, grip_end)

        if free_stats is not None and grip_stats is not None:
            free_mean, free_rms = free_stats
            grip_mean, grip_rms = grip_stats

            ax1.text(
                0.12, 0.98,
                f"Free Avg:{free_mean:.3f} RMS:{free_rms:.3f}",
                transform=ax1.transAxes,
                ha="left",
                va="top",
                fontsize=10
            )

            ax2.text(
                0.12, 0.98,
                f"Grip Avg:{grip_mean:.3f} RMS:{grip_rms:.3f}",
                transform=ax2.transAxes,
                ha="left",
                va="top",
                fontsize=10
            )

        fft_xmax = self.get_fft_xmax()

        if fft_xmax is not None:
            self.ax1.set_xlim(0, fft_xmax)
            self.ax2.set_xlim(0, fft_xmax)

    def calc_interval_stats(self, sig, fs, start, end):

        s = int(start * fs)
        e = int(end * fs)

        seg = sig[s:e]

        if len(seg) == 0:
            return None
        
        mean = np.mean(seg)
        rms = np.sqrt(np.mean(seg**2))
        
        return mean,rms        
    
    def draw_analysis_stats_text(self, ax, dataset):

        fs = dataset["fs"]

        # 固定範囲(定数)
        start = AVG_START
        end   = AVG_END

        freq = self.get_freq()
        lpf = self.get_lpf_cutoff()

        mag_x, _ = self.get_lockin(dataset, "X")
        mag_y, _ = self.get_lockin(dataset, "Y")
        mag_z, _ = self.get_lockin(dataset, "Z")

        mag_syn = np.sqrt(mag_x**2 + mag_y**2 + mag_z**2)

        # 各軸の統計
        stats_x = self.calc_interval_stats(mag_x, fs, start, end)
        stats_y = self.calc_interval_stats(mag_y, fs, start, end)
        stats_z = self.calc_interval_stats(mag_z, fs, start, end)
        stats_s = self.calc_interval_stats(mag_syn, fs, start, end)

        if None in (stats_x, stats_y, stats_z, stats_s):
            return

        mean_x, rms_x = stats_x
        mean_y, rms_y = stats_y
        mean_z, rms_z = stats_z
        mean_s, rms_s = stats_s

        # 表示テキスト
        text = (
            f"[{start:.1f}-{end:.1f}s]\n"
            f"X : Avg={mean_x:.3f}  RMS={rms_x:.3f}\n"
            f"Y : Avg={mean_y:.3f}  RMS={rms_y:.3f}\n"
            f"Z : Avg={mean_z:.3f}  RMS={rms_z:.3f}\n"
            f"SYN: Avg={mean_s:.3f}  RMS={rms_s:.3f}"
        )

        ax.text(
            -0.02, 1.05,
            text,
            transform=ax.transAxes,
            ha="left",
            va="bottom",
            fontsize=10,
            color="black"
        )

    
    #----------データ取り込み------------
    def select_save_folder(self):
        self.save_folder = filedialog.askdirectory()


    def draw_analysis_dataset(self, dataset, ax_top, ax_bottom):

        x = dataset["x"]
        y = dataset["y"]
        z = dataset["z"]

        t = dataset["t"]

        freq = self.get_freq()
        lpf = self.get_lpf_cutoff()

        mag_x, phase_x = self.get_lockin(dataset, "X")
        mag_y, phase_y = self.get_lockin(dataset, "Y")
        mag_z, phase_z = self.get_lockin(dataset, "Z")

        if not self.show_unwrap.get():
            phase_x = np.degrees(phase_x)
            phase_y = np.degrees(phase_y)
            phase_z = np.degrees(phase_z)
        else:
            phase_x = np.degrees(np.unwrap(phase_x))
            phase_y = np.degrees(np.unwrap(phase_y))
            phase_z = np.degrees(np.unwrap(phase_z))

        
        synthesize = np.sqrt(mag_x**2 +mag_y**2 +mag_z**2)

        fs = dataset["fs"]
        
        ignore_sec = 1.0
        start_idx = int(ignore_sec * fs)

        candidates = [
            ("X",   mag_x, self.colors["X"]),
            ("Y",   mag_y, self.colors["Y"]),
            ("Z",   mag_z, self.colors["Z"]),
            ("SYN", synthesize, self.colors["synthesize"])
        ]

        best_step_idx = 0
        best_diff = 0
        step_color = "black"
        step_axis = ""

        for axis_name, sig, color in candidates:

            smooth = pd.Series(sig).rolling(
                int(fs * 0.2),
                center=True,
                min_periods=1
            ).mean()

            diff = np.abs(np.diff(smooth))

            local_idx = (
                np.argmax(diff[start_idx:])
                + start_idx
            )

            local_diff = diff[local_idx]

            if local_diff > best_diff:
                best_diff = local_diff
                best_step_idx = local_idx
                step_color = color
                step_axis = axis_name

        step_idx = best_step_idx
        step_t = t[step_idx]


        # 段差位置を基準に
        # -3.5s ~ -0.5s
        # +0.5s ~ +3.5s

        pre_start = max(
            0,
            step_idx - int(fs * 3.5)
        )

        pre_end = max(
            0,
            step_idx - int(fs * 0.5)
        )

        post_start = min(
            len(synthesize),
            step_idx + int(fs * 0.5)
        )

        post_end = min(
            len(synthesize),
            step_idx + int(fs * 3.5)
        )

        before = synthesize[
            pre_start:pre_end
        ]

        after = synthesize[
            post_start:post_end
        ]

        before_mean = np.mean(before)
        after_mean = np.mean(after)

        delta = after_mean - before_mean

        ratio = (
            delta / before_mean * 100
            if before_mean != 0
            else 0
        )


        # ------------------------
        # Amplitude
        # ------------------------

        if self.show_x.get():
            ax_top.plot(
                t,
                mag_x,
                label="X",
                color=self.colors["X"]
            )

        if self.show_y.get():
            ax_top.plot(
                t,
                mag_y,
                label="Y",
                color=self.colors["Y"]
            )

        if self.show_z.get():
            ax_top.plot(
                t,
                mag_z,
                label="Z",
                color=self.colors["Z"]
            )

        if self.show_synthesize.get():
            ax_top.plot(
                t,
                synthesize,
                label="syn",
                color=self.colors["synthesize"]
            )
        if ax_top.has_data():
            ax_top.legend(loc="best")
        ax_top.set_title(f"振幅 : {dataset['folder']}", y=1.18)

        ax_top.set_xlabel("時間 [s]")
        ax_top.set_ylabel("振幅 [G]")

        ax_top.axvline(
            step_t,
            color=step_color,
            linestyle="--",
            linewidth=1
        )
        ax_bottom.axvline(
            step_t,
            color=step_color,
            linestyle="--",
            linewidth=1
        )

        ax_top.text(
            0.02,
            0.92,
            (
                f"Step={step_t:.2f}s\n"
                f"Before={before_mean:.3f}G\n"
                f"After={after_mean:.3f}G\n"
                f"Δ={delta:.3f}G\n"
                f"{ratio:.1f}%"
            ),
            transform=ax_top.transAxes,
            va="top"
        )

        if  self.show_peak_time.get():

            peak_idx = np.argmax(synthesize)

            peak_t = t[peak_idx]
            peak_amp = synthesize[peak_idx]

            ax_top.plot(
                peak_t, peak_amp,
                "o",
                mfc="none",
                mec=self.colors["synthesize"],
                mew=2,
                ms=8
            )

            ax_top.annotate(
                f"{peak_t:.2f} s",
                xy=(peak_t, peak_amp),
                xytext=(10, 10),
                textcoords="offset points",
                arrowprops=dict(arrowstyle="->")
            )


        self.draw_analysis_stats_text(ax_top, dataset)

        # ------------------------
        # Phase
        # ------------------------

        if not self.time_aligned:
            ax_bottom.text(
                0.02,
                0.98,
                "Warning: time not aligned",
                transform=ax_bottom.transAxes,
                va="top",
                color="red"
            )

        if self.show_x.get():
            ax_bottom.plot(
                t,
                phase_x,
                label="X",
                color=self.colors["X"]
            )

        if self.show_y.get():
            ax_bottom.plot(
                t,
                phase_y,
                label="Y",
                color=self.colors["Y"]
            )

        if self.show_z.get():
            ax_bottom.plot(
                t,
                phase_z,
                label="Z",
                color=self.colors["Z"]
            )

        ax_bottom.set_title(
            f"位相 : {dataset['folder']}"
        )

        ax_bottom.set_xlabel("時間 [s]")
        ax_bottom.set_ylabel("位相 [deg]")
        if ax_bottom.has_data():
            ax_bottom.legend(loc="best")
       
        amp_ymax = self.get_amp_ymax()

        if amp_ymax is not None:
            ax_top.set_ylim(0, amp_ymax)

    def plot_xyz(self):

        # ---------------------------
        # データ数チェック
        # ---------------------------
        single_mode = len(self.datasets) == 1

        if len(self.datasets) == 0:
            return

        # ---------------------------
        # 選択データ取得
        # ---------------------------

        idx1 = self.data1_combo.current()
        idx2 = self.data2_combo.current()

        if idx1 < 0:
            return

        dataset1 = self.datasets[idx1]

        if idx2 < 0:
            dataset2 = dataset1
        else:
            dataset2 = self.datasets[idx2]

        # ---------------------------
        # clear
        # ---------------------------

        self.ax1.clear()
        self.ax2.clear()
        self.ax3.clear()
        self.ax4.clear()

        #-----------表示分岐----------
        if single_mode:
            self.ax3.set_visible(False)
            self.ax4.set_visible(False)
        else:
            self.ax3.set_visible(True)
            self.ax4.set_visible(True)


        # ==================================================
        # Data1
        # ==================================================

        raw_x = dataset1["raw_x"]
        raw_y = dataset1["raw_y"]
        raw_z = dataset1["raw_z"]

        x = dataset1["x"]
        y = dataset1["y"]
        z = dataset1["z"]

        t = dataset1["t"]

        # ---- 時間波形 ----

        if self.show_x.get():
            self.ax1.plot(
                t,
                raw_x,
                label="X",
                color=self.colors["X"]
            )

        if self.show_y.get():
            self.ax1.plot(
                t,
                raw_y,
                label="Y",
                color=self.colors["Y"]
            )

        if self.show_z.get():
            self.ax1.plot(
                t,
                raw_z,
                label="Z",
                color=self.colors["Z"]
            )

        if self.show_synthesize.get():
            self.ax1.plot(
                t,
                np.sqrt(raw_x**2 + raw_y**2 + raw_z**2),
                label="syn",
                color=self.colors["synthesize"]
            )
                
        self.ax1.set_title(f"時間波形 : {dataset1['folder']}", y=1.18)
        self.ax1.set_xlabel("時間 [s]")
        self.ax1.set_ylabel("加速度 [G]")
        if self.ax1.has_data():
            self.ax1.legend(
                loc="upper left",
                bbox_to_anchor=(1.18, 1),
                borderaxespad=0
            )

        # ---- FFT ----
        fft_freq_x1, fft_amp_x1 = dataset1["fft_x"]
        fft_freq_y1, fft_amp_y1 = dataset1["fft_y"]
        fft_freq_z1, fft_amp_z1 = dataset1["fft_z"]
        fft_freq_synthesize, fft_amp_synthesize = dataset1["fft_syn"]

        peak_freq_x1, peak_amp_x1 = find_fft_peak(
            fft_freq_x1,
            fft_amp_x1
        )

        peak_freq_y1, peak_amp_y1 = find_fft_peak(
            fft_freq_y1,
            fft_amp_y1
        )
        
        peak_freq_z1, peak_amp_z1 = find_fft_peak(
            fft_freq_z1,
            fft_amp_z1   
        )

        peak_freq_syn1, peak_amp_syn1 = find_fft_peak(
            fft_freq_synthesize,
            fft_amp_synthesize
        )

        if self.show_x.get():
            self.ax2.plot(fft_freq_x1, fft_amp_x1,label=f"X (peak {peak_freq_x1:.1f} Hz, {peak_amp_x1:.4f} G)",color=self.colors["X"])

        if self.show_y.get():
            self.ax2.plot(fft_freq_y1, fft_amp_y1,label=f"Y (peak {peak_freq_y1:.1f} Hz, {peak_amp_y1:.4f} G)",color=self.colors["Y"])

        if self.show_z.get():
            self.ax2.plot(fft_freq_z1, fft_amp_z1,label=f"Z (peak {peak_freq_z1:.1f} Hz, {peak_amp_z1:.4f} G)",color=self.colors["Z"])

        if self.show_synthesize.get():
            self.ax2.plot(fft_freq_synthesize, fft_amp_synthesize,label=f"syn (peak {peak_freq_syn1:.1f} Hz, {peak_amp_syn1:.4f} G)",color=self.colors["synthesize"])

        if self.show_x.get():
            self.ax2.plot(peak_freq_x1,peak_amp_x1,"o",mfc="none",mec=self.colors["X"],mew=2,ms=8)

        if self.show_y.get():
            self.ax2.plot(peak_freq_y1,peak_amp_y1,"o",mfc="none",mec=self.colors["Y"],mew=2,ms=8)

        if self.show_z.get():
            self.ax2.plot(peak_freq_z1,peak_amp_z1,"o",mfc="none",mec=self.colors["Z"],mew=2,ms=8)


        if self.show_synthesize.get():
            self.ax2.plot(peak_freq_syn1,peak_amp_syn1,"o",mfc="none",mec=self.colors["synthesize"],mew=2,ms=8)
              

        self.ax2.set_title(f"FFT : {dataset1['folder']}")
        self.ax2.set_xlabel("周波数 [Hz]")
        self.ax2.set_ylabel("振幅 [G]")
        if self.ax2.has_data():
            self.ax2.legend(loc="best")
        
        fft_ymax = self.get_fft_ymax()

        if fft_ymax is not None:
            self.ax2.set_ylim(0, fft_ymax)


        # ==================================================
        # Data2
        # ==================================================

        raw_x = dataset2["raw_x"]
        raw_y = dataset2["raw_y"]
        raw_z = dataset2["raw_z"]

        x = dataset2["x"]
        y = dataset2["y"]
        z = dataset2["z"]

        t = dataset2["t"]

        # ---- 時間波形 ----
        if self.show_x.get():
            self.ax3.plot(
                t,
                raw_x,
                color=self.colors["X"],
            )

        if self.show_y.get():
            self.ax3.plot(
                t,
                raw_y,
                color=self.colors["Y"],
            )

        if self.show_z.get():
            self.ax3.plot(
                t,
                raw_z,
                color=self.colors["Z"],
            )

        if self.show_synthesize.get():
            self.ax3.plot(
                t,
                np.sqrt(raw_x**2 + raw_y**2 + raw_z**2),
                label="syn",
                color=self.colors["synthesize"]
            )

        self.ax3.set_title(f"時間波形 : {dataset2['folder']}", y=1.18)
        self.ax3.set_xlabel("時間 [s]")
        self.ax3.set_ylabel("加速度 [G]")

        # ---- FFT ----
        fft_freq_x2, fft_amp_x2 = dataset2["fft_x"]
        fft_freq_y2, fft_amp_y2 = dataset2["fft_y"]
        fft_freq_z2, fft_amp_z2 = dataset2["fft_z"]
        fft_freq_syn2, fft_amp_syn2 = dataset2["fft_syn"]

        peak_freq_x2, peak_amp_x2 = find_fft_peak(
            fft_freq_x2,
            fft_amp_x2
        )

        peak_freq_y2, peak_amp_y2 = find_fft_peak(
            fft_freq_y2,
            fft_amp_y2
        )
        
        peak_freq_z2, peak_amp_z2 = find_fft_peak(
            fft_freq_z2,
            fft_amp_z2
        )       

        peak_freq_syn2, peak_amp_syn2 = find_fft_peak(
            fft_freq_syn2,
            fft_amp_syn2
        )  

        if self.show_x.get():
            self.ax4.plot(fft_freq_x2, fft_amp_x2,label=f"X (peak {peak_freq_x2:.1f} Hz, {peak_amp_x2:.4f} G)",color=self.colors["X"])

        if self.show_y.get():
            self.ax4.plot(fft_freq_y2, fft_amp_y2,label=f"Y (peak {peak_freq_y2:.1f} Hz, {peak_amp_y2:.4f} G)",color=self.colors["Y"])

        if self.show_z.get():
            self.ax4.plot(fft_freq_z2, fft_amp_z2,label=f"Z (peak {peak_freq_z2:.1f} Hz, {peak_amp_z2:.4f} G)",color=self.colors["Z"])
        
        if self.show_synthesize.get():
            self.ax4.plot(fft_freq_syn2, fft_amp_syn2,label=f"syn (peak {peak_freq_syn2:.1f} Hz, {peak_amp_syn2:.4f} G)",color=self.colors["synthesize"])


        #---------ピーク位置--------
        if self.show_x.get():
            self.ax4.plot(peak_freq_x2,peak_amp_x2,"o",mfc="none",mec=self.colors["X"],mew=2,ms=8)

        if self.show_y.get():
            self.ax4.plot(peak_freq_y2,peak_amp_y2,"o",mfc="none",mec=self.colors["Y"],mew=2,ms=8)

        
        if self.show_z.get():
            self.ax4.plot(peak_freq_z2,peak_amp_z2,"o",mfc="none",mec=self.colors["Z"],mew=2,ms=8)
        
        if self.show_synthesize.get():
            self.ax4.plot(peak_freq_syn2,peak_amp_syn2,"o",mfc="none",mec=self.colors["synthesize"],mew=2,ms=8)
       
        self.ax4.set_title(f"FFT : {dataset2['folder']}")
        self.ax4.set_xlabel("周波数 [Hz]")
        self.ax4.set_ylabel("振幅 [G]")
        if self.ax4.has_data():
            self.ax4.legend(loc="best")

        if fft_ymax is not None:
            self.ax4.set_ylim(0, fft_ymax)

        self.fig.subplots_adjust(wspace=0.35, hspace=0.3, top=0.85)

        self.canvas.draw_idle()
        self.update_span_visual()   # ←追加
        
        
        # Data1用
        if hasattr(self, "span1"):
            self.span1.disconnect_events()
            del self.span1

        self.span1 = SpanSelector(
            self.ax1,
            lambda xmin, xmax: self.onselect(xmin, xmax, target="data1"),
            'horizontal',
            useblit=True,
            interactive=False,
            button=[1, 3],
            props=dict(alpha=0.3, facecolor="green")
        )

        # Data2用
        if hasattr(self, "span2"):
            self.span2.disconnect_events()
            del self.span2

        self.span2 = SpanSelector(
            self.ax3,
            lambda xmin, xmax: self.onselect(xmin, xmax, target="data2"),
            'horizontal',
            useblit=True,
            interactive=False,
            button=[1, 3],
            props=dict(alpha=0.3, facecolor="green")
        )
            
        fft_xmax = self.get_fft_xmax()

        if fft_xmax is not None:
            self.ax2.set_xlim(0, fft_xmax)
            self.ax4.set_xlim(0, fft_xmax)

    def get_lockin(self, dataset, axis):

        freq = self.get_freq()
        lpf = self.get_lpf_cutoff()

        key = (axis, freq, lpf)

        if key not in dataset["lockin_cache"]:

            sig = dataset[axis.lower()]

            dataset["lockin_cache"][key] = quadrature(
                sig,
                freq,
                dataset["fs"],
                lpf
            )

        return dataset["lockin_cache"][key]

    
    def update_span_visual(self):

        # ---- Data1 ----
        # 既存の塗りつぶしを消す
        for p in list(self.ax1.patches):          
            if hasattr(p, "_from_span_visual"):
                p.remove()


        fs = float(self.free_start1_entry.get())
        fe = float(self.free_end1_entry.get())
        gs = float(self.grip_start1_entry.get())
        ge = float(self.grip_end1_entry.get())

        p1 = self.ax1.axvspan(fs, fe, color="red", alpha=0.1)
        p1._from_span_visual = True

        p2 = self.ax1.axvspan(gs, ge, color="blue", alpha=0.1)
        p2._from_span_visual = True

        # ---- Data2 ----
        for p in list(self.ax3.patches):          
            if hasattr(p, "_from_span_visual"):
                p.remove()


        fs2 = float(self.free_start2_entry.get())
        fe2 = float(self.free_end2_entry.get())
        gs2 = float(self.grip_start2_entry.get())
        ge2 = float(self.grip_end2_entry.get())

        p3 = self.ax3.axvspan(fs2, fe2, color="red", alpha=0.1)
        p3._from_span_visual = True

        p4 = self.ax3.axvspan(gs2, ge2, color="blue", alpha=0.1)
        p4._from_span_visual = True

        self.canvas.draw_idle()


        
    def onselect(self, xmin, xmax, target="data1"):

        is_grip = (self.current_button == 3)  # 右クリック

        if target == "data1":

            if not is_grip:
                # Free
                self.free_start1_entry.delete(0, tk.END)
                self.free_start1_entry.insert(0, f"{xmin:.2f}")
                self.free_end1_entry.delete(0, tk.END)
                self.free_end1_entry.insert(0, f"{xmax:.2f}")
            else:
                # Grip
                self.grip_start1_entry.delete(0, tk.END)
                self.grip_start1_entry.insert(0, f"{xmin:.2f}")
                self.grip_end1_entry.delete(0, tk.END)
                self.grip_end1_entry.insert(0, f"{xmax:.2f}")

        elif target == "data2":

            if not is_grip:
                self.free_start2_entry.delete(0, tk.END)
                self.free_start2_entry.insert(0, f"{xmin:.2f}")
                self.free_end2_entry.delete(0, tk.END)
                self.free_end2_entry.insert(0, f"{xmax:.2f}")
            else:
                self.grip_start2_entry.delete(0, tk.END)
                self.grip_start2_entry.insert(0, f"{xmin:.2f}")
                self.grip_end2_entry.delete(0, tk.END)
                self.grip_end2_entry.insert(0, f"{xmax:.2f}")

        self.update_grip_fft()
        self.update_span_visual()
        self.plot_xyz()
    # ---------------------- 解析共通 ----------------------

    def update_analysis_plot(self):


        single_mode = len(self.datasets) == 1
        if len(self.datasets) == 0:
            return

        idx1 = self.data1_combo.current()
        idx2 = self.data2_combo.current()

        if idx1 < 0:
            return

        dataset1 = self.datasets[idx1]

        if idx2 < 0:
            dataset2 = dataset1
        else:
            dataset2 = self.datasets[idx2]

        # clear
        self.axa1.clear()
        self.axa2.clear()
        self.axa3.clear()
        self.axa4.clear()    

        #-----------表示分岐------------
        if single_mode:
            self.axa3.set_visible(False)
            self.axa4.set_visible(False)
        else:
            self.axa3.set_visible(True)
            self.axa4.set_visible(True)


        # Data1
        self.draw_analysis_dataset(
            dataset1,
            self.axa1,
            self.axa2
        )

        # Data2
        self.draw_analysis_dataset(
            dataset2,
            self.axa3,
            self.axa4
        )

        self.analysis_fig.subplots_adjust(wspace=0.35,hspace=0.3, top=0.85)

        self.analysis_canvas.draw_idle()
        self.analysis_ylim = self.axa1.get_ylim()

    def on_mouse_press(self, event):
        if event.button is not None:
            self.current_button = event.button
    

    def save_file(self):

        if len(self.datasets) == 0:
            return

        for dataset in self.datasets:

            foldername = dataset["folder"]
            download_folder = str(Path.home() / "Downloads")

            savepath = os.path.join(
                download_folder,
                foldername + ".xlsx"
            )

            x = dataset["x"]
            y = dataset["y"]
            z = dataset["z"]

            raw_x = dataset["raw_x"]
            raw_y = dataset["raw_y"]
            raw_z = dataset["raw_z"]

            t_full = dataset["t"]
            fs = dataset["fs"]


            # ロックイン
            mag_x, phase_x = self.get_lockin(dataset, "X")
            mag_y, phase_y = self.get_lockin(dataset, "Y")
            mag_z, phase_z = self.get_lockin(dataset, "Z")

            phase_x = np.degrees(phase_x)
            phase_y = np.degrees(phase_y)
            phase_z = np.degrees(phase_z)

            synthesize = np.sqrt(mag_x**2 + mag_y**2 + mag_z**2)

            mag_x_scalar = 2 * np.sqrt(np.mean(mag_x**2))
            mag_y_scalar = 2 * np.sqrt(np.mean(mag_y**2))
            mag_z_scalar = 2 * np.sqrt(np.mean(mag_z**2))

            # ----------------------------
            # FFT
            # ----------------------------
            freq_x, amp_x = calc_fft(x, fs)
            _, amp_y = calc_fft(y, fs)
            _, amp_z = calc_fft(z, fs)

            # ----------------------------
            # DataFrame 作成
            # ----------------------------
            df_time = pd.DataFrame({
                "Time": t_full,
                "raw_X": raw_x,
                "raw_Y": raw_y,
                "raw_Z": raw_z,
                "X": x,
                "Y": y,
                "Z": z,
                "Lockin_X": mag_x,
                "Lockin_Y": mag_y,
                "Lockin_Z": mag_z,
                "Amp_synthesize": synthesize
            })

            df_fft = pd.DataFrame({
                "freq": freq_x,
                "amp_X": amp_x,
                "amp_Y": amp_y,
                "amp_Z": amp_z
            })

            df_summary = pd.DataFrame({
                "Axis": ["X", "Y", "Z"],
                "Lockin_mag": [mag_x_scalar, mag_y_scalar, mag_z_scalar]
            })

            # ----------------------------
            # Excel保存(複数シート)
            # ----------------------------
            with pd.ExcelWriter(savepath) as writer:
                df_time.to_excel(writer, sheet_name="TimeData", index=False)
                df_fft.to_excel(writer, sheet_name="FFT", index=False)
                df_summary.to_excel(writer, sheet_name="Summary", index=False)

        messagebox.showinfo(
            "Save Complete",
            f"{len(self.datasets)} files saved"
        )

    # ---------------------- 画面の保存 ----------------------
    def save_picture(self):

        import uuid

        if len(self.datasets) == 0:
            return

        if not hasattr(self, "excel_path"):
            messagebox.showwarning("Warning", "Excelファイルを選択してください")
            return

        # Excel読み込み
        wb = load_workbook(self.excel_path)

        freq = int(self.get_freq())
        lpf = int(self.get_lpf_cutoff())
        base_name = f"{freq}Hz_LPF{lpf}"
        sheet_name = base_name

        i = 1
        while sheet_name in wb.sheetnames:
            sheet_name = f"{base_name}.{i}"
            i += 1

        ws = wb.create_sheet(sheet_name)
        self.excel_row = 1

      
        # --- 元の状態を保存 ---
        org_x = self.show_x.get()
        org_y = self.show_y.get()
        org_z = self.show_z.get()
        org_syn = self.show_synthesize.get()

        org_idx1 = self.data1_combo.current()
        org_idx2 = self.data2_combo.current()

        # 既存画像数(位置決め用)
        if not hasattr(self, "excel_row"):
            self.excel_row = 1

        row = self.excel_row


        # 一時保存フォルダ
        temp_dir = os.path.join(Path.home(), "AppData", "Local", "Temp")
        os.makedirs(temp_dir, exist_ok=True)
        

        pairs = []
        for i in range(0, len(self.datasets), 2):
            idx1 = i
            idx2 = i+1 if i+1 < len(self.datasets) else i
            pairs.append((idx1, idx2))

        count = 0

        for axis in ["SYN"]:

            self.show_x.set(axis == "X")
            self.show_y.set(axis == "Y")
            self.show_z.set(axis == "Z")
            self.show_synthesize.set(axis == "SYN")

            for idx1, idx2 in pairs:

                self.data1_combo.current(idx1)
                self.data2_combo.current(idx2)

                self.plot_xyz()
                self.update_analysis_plot()
                self.update_idletasks()

                # ---------- 一時画像保存 ----------
    
                wave_path = os.path.join(temp_dir, f"wave_{uuid.uuid4().hex}.png")
                anl_path  = os.path.join(temp_dir, f"analysis_{uuid.uuid4().hex}.png")


                self.fig.savefig(wave_path, dpi=200)
                self.analysis_fig.savefig(anl_path, dpi=200)

                # ---------- Excelに貼る ----------
                img1 = XLImage(wave_path)
                img1.width = 1200
                img1.height = 600

                img2 = XLImage(anl_path)
                img2.width = 1000
                img2.height = 600

                # 位置配置
                ws.add_image(img1, f"A{row}")
                ws.add_image(img2, f"A{row+35}")

                row += 70
                count += 1

        wb.save(self.excel_path)

        #元の状態に戻す
        self.show_x.set(org_x)
        self.show_y.set(org_y)
        self.show_z.set(org_z)
        self.show_synthesize.set(org_syn)

        self.data1_combo.current(org_idx1)
        self.data2_combo.current(org_idx2)

        # 再描画
        self.plot_xyz()
        self.update_analysis_plot()
        self.update_grip_fft()

        messagebox.showinfo("Complete", "Excelに画像追加しました")

    def save_grip_pic(self):

        save_dir = str(Path.home() / "Downloads")

        idx1 = self.data1_combo.current()
        idx2 = self.data2_combo.current()

        name1 = self.datasets[idx1]["folder"]
        name2 = self.datasets[idx2]["folder"]

        # 元の状態を保存
        org_x = self.show_x.get()
        org_y = self.show_y.get()
        org_z = self.show_z.get()
        org_syn = self.show_synthesize.get()

        # 軸ループ
        for axis in ["X", "Y", "Z", "SYN"]:

            # 一旦全部OFF
            self.show_x.set(False)
            self.show_y.set(False)
            self.show_z.set(False)
            self.show_synthesize.set(False)

            # 個別ON
            if axis == "X":
                self.show_x.set(True)
            elif axis == "Y":
                self.show_y.set(True)
            elif axis == "Z":
                self.show_z.set(True)
            elif axis == "SYN":
                self.show_synthesize.set(True)

            # 再描画
            self.update_grip_fft()
            self.update_idletasks()

            # 保存 Data1
            self.grip_fig1.savefig(
                os.path.join(save_dir, f"GripData1_{axis}_{name1}.png"),
                dpi=300,
                bbox_inches="tight"
            )

            # 保存 Data2
            self.grip_fig2.savefig(
                os.path.join(save_dir, f"GripData2_{axis}_{name2}.png"),
                dpi=300,
                bbox_inches="tight"
            )

        # 元に戻す
        self.show_x.set(org_x)
        self.show_y.set(org_y)
        self.show_z.set(org_z)
        self.show_synthesize.set(org_syn)

        self.update_grip_fft()

        messagebox.showinfo("Complete", "Grip FFT(軸別)保存完了")  

# data_loader.py

import pandas as pd
import numpy as np


#---------同期信号の立上り検出-------
#sync:同期信号,threshold : HIGH判定閾値,min_width : HIGH継続サンプル数
def detect_sync_edges(sync,threshold,min_width):

    #trueかfalseを代入
    high = sync > threshold
    edges = []

    i = 0

    while i < len(high):

        if high[i]:

            start = i
            while i < len(high) and high[i]:
                i += 1

            width = i - start

            if width >= min_width:
                edges.append(start)

        else:
            i += 1

    return np.array(edges)


#------データ読み込み--------
def load_signal(
    csv_path,
    common_start=None,
    common_end=None
):
    # 外部同期信号有無チェック(0列目に"ext data"があるかを確認)
    check_df = pd.read_csv(csv_path,header=None,usecols=[0],dtype=str)

    has_sync = check_df.iloc[:, 0].astype(str).str.contains("ext data",case=False,na=False).any()

    # -----CSV全体読込(agsとext dataを分離するため)-----
    full_df = pd.read_csv(csv_path,header=None)

    # ags行を抽出
    ags_df = full_df[
        full_df.iloc[:, 0]
        .astype(str)
        .str.startswith("ags")
    ].reset_index(drop=True)

    # ext data行を抽出
    ext_df = full_df[
        full_df.iloc[:, 0]
        .astype(str)
        .str.startswith("ext data")
    ].reset_index(drop=True)

    #---fs判定(0~3行までのC列を見て判定)---
    ch_vals = pd.to_numeric(
        ags_df.iloc[:4, 2],
        errors="coerce"
    ).fillna(0).tolist()
    
    if (ch_vals == [50, 0, 50, 0] or ch_vals == [0, 50, 0, 50]):
        print("2000")
        fs = 2000

    elif (ch_vals == [25, 50, 75, 0] or ch_vals == [50, 75, 0, 25] or ch_vals == [75, 0, 25, 50]):
        print("4000")
        fs = 4000

    else:
        print("1000")
        fs = 1000

    # ----データ形式(fs)に応じてどの行を読み込むかを決める----
    if fs == 1000:
        x = pd.to_numeric(
            ags_df.iloc[:, 2],
            errors="coerce"
        ).fillna(0).values / 10000

        y = pd.to_numeric(
            ags_df.iloc[:, 3],
            errors="coerce"
        ).fillna(0).values / 10000

        z = pd.to_numeric(
            ags_df.iloc[:, 4],
            errors="coerce"
        ).fillna(0).values / 10000
    else:
        x = pd.to_numeric(
            ags_df.iloc[:, 3],
            errors="coerce"
        ).fillna(0).values / 10000

        y = pd.to_numeric(
            ags_df.iloc[:, 4],
            errors="coerce"
        ).fillna(0).values / 10000

        z = pd.to_numeric(
            ags_df.iloc[:, 5],
            errors="coerce"
        ).fillna(0).values / 10000
    
    # ---タイムスタンプを読み込む----
    timestamp = pd.to_numeric(
        ags_df.iloc[:, 1],
        errors="coerce"
    ).fillna(0).values

    # ---同期信号(ext data の G列)の処理----
    if not ext_df.empty:
        
        #文字列等が入っていた場合数値に変換する(安全用)
        sync = pd.to_numeric(
            ext_df.iloc[:, 6],
            errors="coerce"
        ).fillna(0).values
        #閾値3800で5回連続したら、信号と判断
        sync_edges = detect_sync_edges(
            sync,
            threshold=3800,
            min_width=5
        )

    else:
        #同期信号がない場合、ゼロで埋める
        sync = np.zeros(len(timestamp))
        sync_edges = np.array([])

    #---タイムスタンプ同期---
    if common_start is not None:

        mask = (
            (timestamp >= common_start)
            &
            (timestamp <= common_end)
        )

        timestamp = timestamp[mask]

        x = x[mask]
        y = y[mask]
        z = z[mask]

    # ---生データ保存---
    raw_x = x.copy()
    raw_y = y.copy()
    raw_z = z.copy()

    # ---DC除去---
    x -= np.mean(x)
    y -= np.mean(y)
    z -= np.mean(z)

    return (
        raw_x,
        raw_y,
        raw_z,
        x,
        y,
        z,
        timestamp,
        fs,
        sync_edges,
        has_sync
    )

#signal_processing
import numpy as np
from scipy.signal import butter, filtfilt


def lowpass(x, cutoff, fs, order=4):

    if cutoff <= 0 or cutoff >= fs / 2:
        raise ValueError("LPF cutoff must satisfy 0 < cutoff < fs/2")
    
    b, a = butter(
        order,
        cutoff / (fs / 2),
        btype="low"
    )

    return filtfilt(b, a, x)

#-------直交検波----------
def quadrature(signal, freq, fs, lpf_cutoff=5):

    t = np.arange(len(signal)) / fs

    I = signal * np.cos(2 * np.pi * freq * t)
    Q = signal * np.sin(2 * np.pi * freq * t)

    I_f = lowpass(I, lpf_cutoff, fs)
    Q_f = lowpass(Q, lpf_cutoff, fs)

    mag = 2 * np.sqrt(I_f**2 + Q_f**2)

    phase = np.arctan2(Q_f,I_f)

    return mag, phase

#-------fft(振幅スペクトラム)----------
def calc_fft(signal, fs, start_sec=None, end_sec=None):
    
    signal = np.asarray(signal).flatten()


    # 区間切り出し(secがNoneなら無視)
    if start_sec is not None and end_sec is not None:
        start_idx = int(start_sec * fs)
        end_idx = int(end_sec * fs)
        signal = signal[start_idx:end_idx]

    #データ数が少ない場合、計算しない(非表示)
    if len(signal) < 10:
        return None, None

    n = len(signal)
    
    # DC除去
    signal = signal - np.mean(signal)
    # 窓関数
    w = np.hanning(n)
    signal = signal * w

    # FFT
    fft = np.fft.rfft(signal)
    amp = np.abs(fft)
    freq = np.fft.rfftfreq(n, d=1/fs)

    # 正規化
    amp = amp / np.sum(w) * 2

    return freq, amp


#--------先にFFT あとから合成------
def synthesize_fft(x, y, z, fs, start=None, end=None):

    #区間切り出し get.entryとかがないと入らない
    if start is not None and end is not None:
        s = int(start * fs)
        e = int(end * fs)
        x = x[s:e]
        y = y[s:e]
        z = z[s:e]

    freq, X = calc_fft(x, fs)
    _, Y = calc_fft(y, fs)
    _, Z = calc_fft(z, fs)

    XYZ = np.sqrt(X**2 + Y**2 + Z**2)

    return freq, XYZ


def find_fft_peak(freq,amp,fmin=20,fmax=1000):

    mask = np.ones_like(
        freq,
        dtype=bool
    )
    if fmin is not None:
        mask &= freq >= fmin

    if fmax is not None:
        mask &= freq <= fmax

    freq_sel = freq[mask]
    amp_sel = amp[mask]

    idx = np.argmax(amp_sel)

    return (
        freq_sel[idx],
        amp_sel[idx]
    )


コメント