Add plot-sweep command, degauss buttons, sweep ramp-down, and time estimate

- Add offline `plot-sweep` CLI command: generates efficiency overlay,
  heatmap, and power loss plots from sweep CSV (no instruments needed)
- Add degauss buttons to CH5/CH6 in GUI (sends :DEMAg SCPI command)
- Gradual load ramp-down at end of 2D sweep to avoid transients
- Live sweep time estimate in GUI based on grid size × settle time
- Update HIOKI submodule to include degauss CLI command

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-12 16:31:33 +07:00
parent 5fe2ec4556
commit 3f65b5f2f2
5 changed files with 245 additions and 2 deletions

View File

@@ -410,6 +410,8 @@ class TestbenchGUI(tk.Tk):
self._ch5_i_range.pack(side=tk.LEFT, padx=2)
ttk.Button(rng5, text="Set", width=3,
command=lambda: self._set_range(5, "I", self._ch5_i_range)).pack(side=tk.LEFT)
ttk.Button(rng5, text="Degauss", width=7,
command=lambda: self._send(Cmd.DEGAUSS, [5])).pack(side=tk.LEFT, padx=(8, 0))
self._ch5_range_label = ttk.Label(input_frame, text="V: --- / I: ---", font=("Consolas", 9))
self._ch5_range_label.pack(anchor=tk.W)
@@ -437,6 +439,8 @@ class TestbenchGUI(tk.Tk):
self._ch6_i_range.pack(side=tk.LEFT, padx=2)
ttk.Button(rng6, text="Set", width=3,
command=lambda: self._set_range(6, "I", self._ch6_i_range)).pack(side=tk.LEFT)
ttk.Button(rng6, text="Degauss", width=7,
command=lambda: self._send(Cmd.DEGAUSS, [6])).pack(side=tk.LEFT, padx=(8, 0))
self._ch6_range_label = ttk.Label(output_frame, text="V: --- / I: ---", font=("Consolas", 9))
self._ch6_range_label.pack(anchor=tk.W)
@@ -570,6 +574,15 @@ class TestbenchGUI(tk.Tk):
self._svi_status = ttk.Label(frame, text="Idle", font=("Consolas", 9))
self._svi_status.pack(anchor=tk.W, pady=2)
self._svi_estimate = ttk.Label(frame, text="", font=("Consolas", 9))
self._svi_estimate.pack(anchor=tk.W)
# Bind entries to recalculate time estimate on change
for entry in (self._svi_v_start, self._svi_v_stop, self._svi_v_step,
self._svi_l_start, self._svi_l_stop, self._svi_l_step,
self._svi_settle):
entry.bind("<KeyRelease>", lambda _e: self._update_svi_estimate())
self._update_svi_estimate()
self._svi_thread = None
self._svi_stop_event = None
@@ -907,6 +920,37 @@ class TestbenchGUI(tk.Tk):
self._svi_l_label.config(text="I start:")
else:
self._svi_l_label.config(text="P start:")
self._update_svi_estimate()
def _update_svi_estimate(self) -> None:
"""Recalculate and display estimated sweep duration."""
try:
v_start = float(self._svi_v_start.get())
v_stop = float(self._svi_v_stop.get())
v_step = abs(float(self._svi_v_step.get()))
l_start = float(self._svi_l_start.get())
l_stop = float(self._svi_l_stop.get())
l_step = abs(float(self._svi_l_step.get()))
settle = float(self._svi_settle.get())
if v_step <= 0 or l_step <= 0:
raise ValueError
v_count = int(abs(v_stop - v_start) / v_step) + 1
l_count = int(abs(l_stop - l_start) / l_step) + 1
total = v_count * l_count
secs = total * settle
mins, s = divmod(int(secs), 60)
hrs, m = divmod(mins, 60)
if hrs:
time_str = f"{hrs}h {m:02d}m {s:02d}s"
elif m:
time_str = f"{m}m {s:02d}s"
else:
time_str = f"{s}s"
self._svi_estimate.config(
text=f"{total} points, ~{time_str} (settle only)"
)
except (ValueError, ZeroDivisionError):
self._svi_estimate.config(text="")
def _set_range(self, channel: int, vi: str, combo: ttk.Combobox) -> None:
"""Set voltage or current range for a HIOKI channel."""
@@ -1286,6 +1330,22 @@ class TestbenchGUI(tk.Tk):
ll += l_step
v += v_step
finally:
# Ramp load down gradually to avoid sudden transients
cur_load = ll - l_step if n > 0 else l_start
ramp_steps = max(int(abs(cur_load - l_start) / abs(l_step)), 1) if l_step != 0 else 1
ramp_steps = min(ramp_steps, 10) # cap at 10 steps
if abs(cur_load) > abs(l_start) and ramp_steps > 1:
decrement = (cur_load - l_start) / ramp_steps
print(f"Ramping load down from {cur_load:.1f} to "
f"{l_start:.1f}{unit} in {ramp_steps} steps...")
for i in range(1, ramp_steps + 1):
step_val = cur_load - decrement * i
bench._apply_load_value(load_mode, step_val)
time.sleep(0.5)
else:
bench._apply_load_value(load_mode, l_start)
time.sleep(0.5)
bench.load.load_off()
bench.supply.set_voltage(IDLE_VOLTAGE)