update of trace_signal_fitter.py and some doc
All checks were successful
CI-Build/Kettenoeler/pipeline/head This commit looks good

This commit is contained in:
2025-08-27 23:59:57 +02:00
parent 27993d72ee
commit a9053997a1
3 changed files with 780 additions and 230 deletions

View File

@@ -699,95 +699,197 @@ class TabTraceBatch(ttk.Frame):
def _append(self, s): self.txt.insert(tk.END, s); self.txt.see(tk.END)
# ---------------- Tab 4: Range-Fit (supervised + unsupervised) ----------------
# ---------------- Tab 4: Range-Fit (supervised + unsupervised, mit Physik-Constraints) ----------------
class TabRangeFit(ttk.Frame):
def __init__(self, master, state: AppState, header: Header):
super().__init__(master, padding=10)
self.state = state
self.header = header
self._last_outdir = None
self._build_ui()
def _build_ui(self):
self.columnconfigure(0, weight=1)
self.rowconfigure(2, weight=1)
self.rowconfigure(3, weight=1)
# unified trace panel (single-select)
self.trace_panel = TracePanel(self, self.state, title="Trace wählen (Single)", single_select=True, height=8)
self.trace_panel = TracePanel(self, self.state, title="Trace wählen (Single)", single_select=True, height=10)
self.trace_panel.grid(row=0, column=0, sticky="nsew", padx=5, pady=(5,10))
# Parameter
frm_params = ttk.LabelFrame(self, text="Range-/Unsupervised-Fit Parameter")
# Parameter Frames
frm_params = ttk.Frame(self)
frm_params.grid(row=1, column=0, sticky="nsew", padx=5, pady=5)
for c in (1,3,5):
for c in range(6):
frm_params.columnconfigure(c, weight=1)
# Range (leer lassen => unsupervised)
ttk.Label(frm_params, text="Range-Min (leer = unsupervised)").grid(row=0, column=0, sticky="e")
# --- Supervised (Range & Physik) ---
box_sup = ttk.LabelFrame(frm_params, text="Supervised (Range-Fit) lasse leer für Unsupervised")
box_sup.grid(row=0, column=0, columnspan=6, sticky="nsew", padx=5, pady=5)
for c in range(6):
box_sup.columnconfigure(c, weight=1)
ttk.Label(box_sup, text="Range-Min").grid(row=0, column=0, sticky="e")
self.rmin = tk.StringVar(value="")
ttk.Entry(frm_params, textvariable=self.rmin, width=12).grid(row=0, column=1, sticky="w", padx=5)
ttk.Entry(box_sup, textvariable=self.rmin, width=12).grid(row=0, column=1, sticky="w", padx=5)
ttk.Label(frm_params, text="Range-Max (leer = unsupervised)").grid(row=0, column=2, sticky="e")
ttk.Label(box_sup, text="Range-Max").grid(row=0, column=2, sticky="e")
self.rmax = tk.StringVar(value="")
ttk.Entry(frm_params, textvariable=self.rmax, width=12).grid(row=0, column=3, sticky="w", padx=5)
ttk.Entry(box_sup, textvariable=self.rmax, width=12).grid(row=0, column=3, sticky="w", padx=5)
ttk.Label(frm_params, text="Min. Hit-Ratio (Range-Fit)").grid(row=0, column=4, sticky="e")
ttk.Label(box_sup, text="Min. Hit-Ratio (0..1)").grid(row=0, column=4, sticky="e")
self.min_hit = tk.DoubleVar(value=0.5)
ttk.Entry(frm_params, textvariable=self.min_hit, width=10).grid(row=0, column=5, sticky="w", padx=5)
ttk.Entry(box_sup, textvariable=self.min_hit, width=10).grid(row=0, column=5, sticky="w", padx=5)
ttk.Label(frm_params, text="Min. Smoothness (Unsupervised)").grid(row=1, column=0, sticky="e")
self.allow_neg = tk.BooleanVar(value=False)
ttk.Checkbutton(box_sup, text="negative Scale erlauben", variable=self.allow_neg).grid(row=1, column=0, columnspan=2, sticky="w")
ttk.Label(box_sup, text="Rate-Min (Hz)").grid(row=1, column=2, sticky="e")
self.rate_min = tk.StringVar(value="")
ttk.Entry(box_sup, textvariable=self.rate_min, width=10).grid(row=1, column=3, sticky="w", padx=5)
ttk.Label(box_sup, text="Rate-Max (Hz)").grid(row=1, column=4, sticky="e")
self.rate_max = tk.StringVar(value="")
ttk.Entry(box_sup, textvariable=self.rate_max, width=10).grid(row=1, column=5, sticky="w", padx=5)
ttk.Label(box_sup, text="Jitter-Max (ms)").grid(row=2, column=0, sticky="e")
self.jitter_max = tk.StringVar(value="")
ttk.Entry(box_sup, textvariable=self.jitter_max, width=10).grid(row=2, column=1, sticky="w", padx=5)
ttk.Label(box_sup, text="Max-Slope-Abs (phys/s)").grid(row=2, column=2, sticky="e")
self.slope_abs = tk.StringVar(value="")
ttk.Entry(box_sup, textvariable=self.slope_abs, width=12).grid(row=2, column=3, sticky="w", padx=5)
ttk.Label(box_sup, text="Max-Slope-Frac (/s)").grid(row=2, column=4, sticky="e")
self.slope_frac = tk.StringVar(value="")
ttk.Entry(box_sup, textvariable=self.slope_frac, width=12).grid(row=2, column=5, sticky="w", padx=5)
ttk.Label(box_sup, text="Slope-Quantile").grid(row=3, column=0, sticky="e")
self.slope_q = tk.DoubleVar(value=0.95) # 0.95 oder 0.99
ttk.Entry(box_sup, textvariable=self.slope_q, width=10).grid(row=3, column=1, sticky="w", padx=5)
ttk.Label(box_sup, text="Min-Unique-Ratio").grid(row=3, column=2, sticky="e")
self.min_uniq = tk.StringVar(value="")
ttk.Entry(box_sup, textvariable=self.min_uniq, width=10).grid(row=3, column=3, sticky="w", padx=5)
# --- Unsupervised ---
box_uns = ttk.LabelFrame(frm_params, text="Unsupervised (ohne Range)")
box_uns.grid(row=1, column=0, columnspan=6, sticky="nsew", padx=5, pady=5)
for c in range(6):
box_uns.columnconfigure(c, weight=1)
ttk.Label(box_uns, text="Min. Smoothness (0..1)").grid(row=0, column=0, sticky="e")
self.min_smooth = tk.DoubleVar(value=0.2)
ttk.Entry(frm_params, textvariable=self.min_smooth, width=10).grid(row=1, column=1, sticky="w", padx=5)
ttk.Entry(box_uns, textvariable=self.min_smooth, width=12).grid(row=0, column=1, sticky="w", padx=5)
ttk.Label(box_uns, text="Max-Slope-Frac-RAW (/s)").grid(row=0, column=2, sticky="e")
self.max_slope_frac_raw = tk.StringVar(value="")
ttk.Entry(box_uns, textvariable=self.max_slope_frac_raw, width=12).grid(row=0, column=3, sticky="w", padx=5)
# --- Allgemein/Output ---
box_out = ttk.LabelFrame(frm_params, text="Allgemein & Output")
box_out.grid(row=2, column=0, columnspan=6, sticky="nsew", padx=5, pady=5)
for c in range(6):
box_out.columnconfigure(c, weight=1)
self.rx_only = tk.BooleanVar(value=False)
self.neg_scale = tk.BooleanVar(value=False) # nur bei Range-Fit
ttk.Checkbutton(frm_params, text="nur RX", variable=self.rx_only).grid(row=1, column=2, sticky="w")
ttk.Checkbutton(frm_params, text="negative Scale erlauben (Range-Fit)", variable=self.neg_scale).grid(row=1, column=3, sticky="w")
ttk.Checkbutton(box_out, text="nur RX", variable=self.rx_only).grid(row=0, column=0, sticky="w")
ttk.Label(frm_params, text="Plots Top-N").grid(row=1, column=4, sticky="e")
ttk.Label(box_out, text="Plots Top-N").grid(row=0, column=1, sticky="e")
self.plots_top = tk.IntVar(value=8)
ttk.Entry(frm_params, textvariable=self.plots_top, width=10).grid(row=1, column=5, sticky="w", padx=5)
ttk.Entry(box_out, textvariable=self.plots_top, width=10).grid(row=0, column=2, sticky="w", padx=5)
ttk.Label(frm_params, text="Output-Label").grid(row=2, column=0, sticky="e")
ttk.Label(box_out, text="Output-Label").grid(row=0, column=3, sticky="e")
self.out_label = tk.StringVar(value="rangefit")
ttk.Entry(frm_params, textvariable=self.out_label, width=16).grid(row=2, column=1, sticky="w", padx=5)
ttk.Entry(box_out, textvariable=self.out_label, width=18).grid(row=0, column=4, sticky="w", padx=5)
self.use_ts = tk.BooleanVar(value=True)
ttk.Checkbutton(frm_params, text="Zeitstempel-Unterordner", variable=self.use_ts).grid(row=2, column=2, sticky="w", padx=2)
ttk.Checkbutton(box_out, text="Zeitstempel-Unterordner", variable=self.use_ts).grid(row=0, column=5, sticky="w")
# Start + Konsole
# Start + Konsole + Aktionen
frm_run = ttk.Frame(self)
frm_run.grid(row=3, column=0, sticky="ew", padx=5, pady=5)
frm_run.grid(row=2, column=0, sticky="ew", padx=5, pady=5)
ttk.Button(frm_run, text="Start Range-/Unsupervised-Fit", command=self._on_run).pack(side="left", padx=5)
ttk.Button(frm_run, text="Report öffnen", command=self._open_last_report).pack(side="left", padx=5)
ttk.Button(frm_run, text="Output-Ordner öffnen", command=self._open_last_outdir).pack(side="left", padx=5)
frm_out = ttk.LabelFrame(self, text="Ausgabe")
frm_out.grid(row=4, column=0, sticky="nsew", padx=5, pady=5)
frm_out.grid(row=3, column=0, sticky="nsew", padx=5, pady=5)
frm_out.columnconfigure(0, weight=1); frm_out.rowconfigure(0, weight=1)
self.txt = tk.Text(frm_out, height=12); self.txt.grid(row=0, column=0, sticky="nsew")
self.txt = tk.Text(frm_out, height=14); self.txt.grid(row=0, column=0, sticky="nsew")
sbo = ttk.Scrollbar(frm_out, orient="vertical", command=self.txt.yview); sbo.grid(row=0, column=1, sticky="ns")
self.txt.configure(yscrollcommand=sbo.set)
# --- helpers ---
def _append(self, s):
self.txt.insert(tk.END, s); self.txt.see(tk.END)
def _on_run(self):
def _stamp(self):
import datetime as _dt
return _dt.datetime.now().strftime("%Y%m%d_%H%M%S")
def _build_outdir(self, supervised: bool) -> Path:
out_root = self.state.analyze_out_root()
label = (self.out_label.get().strip() or ("rangefit" if supervised else "unsupervised"))
stamp = f"{self._stamp()}_{label}" if self.use_ts.get() else label
outdir = out_root / stamp
outdir.mkdir(parents=True, exist_ok=True)
self._last_outdir = outdir
return outdir
def _selected_trace(self):
sel = self.trace_panel.get_selected()
if not sel:
messagebox.showwarning("Hinweis", "Bitte genau eine .trace-Datei auswählen.")
return
return None
if len(sel) != 1:
messagebox.showwarning("Hinweis", "Range-Fit benötigt genau eine .trace-Datei (Single-Select).")
return None
return sel[0]
def _maybe(self, val: str, flag: str, args: list):
v = (val or "").strip()
if v != "":
args += [flag, v]
def _open_path(self, p: Path):
try:
if sys.platform.startswith("darwin"):
subprocess.Popen(["open", str(p)])
elif os.name == "nt":
os.startfile(str(p)) # type: ignore
else:
subprocess.Popen(["xdg-open", str(p)])
except Exception as e:
messagebox.showwarning("Fehler", f"Konnte nicht öffnen:\n{p}\n{e}")
def _open_last_outdir(self):
if self._last_outdir and self._last_outdir.exists():
self._open_path(self._last_outdir)
else:
messagebox.showinfo("Hinweis", "Noch kein Output-Ordner vorhanden.")
def _open_last_report(self):
if not (self._last_outdir and self._last_outdir.exists()):
messagebox.showinfo("Hinweis", "Noch kein Report erzeugt.")
return
# versuche ein *_report.md im letzten Outdir zu finden
md = list(Path(self._last_outdir).glob("*_report.md"))
if not md:
messagebox.showinfo("Hinweis", "Kein Report gefunden.")
return
self._open_path(md[0])
def _on_run(self):
trace = self._selected_trace()
if not trace:
return
trace = sel[0]
# supervised?
rmin = self.rmin.get().strip()
rmax = self.rmax.get().strip()
supervised = bool(rmin) and bool(rmax)
out_root = self.state.analyze_out_root()
label = (self.out_label.get().strip() or ("rangefit" if supervised else "unsupervised"))
stamp = f"{now_stamp()}_{label}" if self.use_ts.get() else label
outdir = ensure_dir(out_root / stamp)
outdir = self._build_outdir(supervised)
cmd = [
sys.executable,
@@ -801,25 +903,38 @@ class TabRangeFit(ttk.Frame):
if supervised:
cmd += ["--rmin", rmin, "--rmax", rmax, "--min-hit", str(self.min_hit.get())]
if self.neg_scale.get():
if self.allow_neg.get():
cmd.append("--allow-neg-scale")
self._maybe(self.rate_min.get(), "--rate-min", cmd)
self._maybe(self.rate_max.get(), "--rate-max", cmd)
self._maybe(self.jitter_max.get(), "--jitter-max-ms", cmd)
self._maybe(self.slope_abs.get(), "--max-slope-abs", cmd)
self._maybe(self.slope_frac.get(), "--max-slope-frac", cmd)
cmd += ["--slope-quantile", str(self.slope_q.get())]
self._maybe(self.min_uniq.get(), "--min-uniq-ratio", cmd)
else:
# unsupervised
cmd += ["--min-smooth", str(self.min_smooth.get())]
self._maybe(self.max_slope_frac_raw.get(), "--max-slope-frac-raw", cmd)
cmd += ["--slope-quantile", str(self.slope_q.get())] # wird intern für p95/p99 gewählt
self._run_cmd(cmd)
self._append(f"\nDone. Output: {outdir}\n")
self._append(f"\n>>> RUN: {' '.join(cmd)}\n")
t = threading.Thread(target=self._run_cmd, args=(cmd,), daemon=True)
t.start()
def _run_cmd(self, cmd):
self._append(f"\n>>> RUN: {' '.join(cmd)}\n")
try:
with subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True) as proc:
for line in proc.stdout: self._append(line)
for line in proc.stdout:
self._append(line)
rc = proc.wait()
if rc != 0: self._append(f"[Exit-Code {rc}]\n")
if rc != 0:
self._append(f"[Exit-Code {rc}]\n")
else:
self._append(f"\nDone. Output: {self._last_outdir}\n")
except Exception as e:
self._append(f"[Fehler] {e}\n")
# ---------------- App Shell ----------------
class App(tk.Tk):
def __init__(self):