Add CP load mode to 2D sweep and supply sanity checks

- 2D sweep now supports CC (constant current) and CP (constant power)
  load modes via --load-mode flag (CLI) and combobox (GUI)
- Supply capability check before all sweeps: validates max voltage
  against supply range and prints V/I/P summary
- Renamed sweep-vi args from --i-start/stop/step to --l-start/stop/step
  to reflect that the load setpoint can be current or power
- GUI labels update dynamically based on selected load mode

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-11 15:57:44 +07:00
parent 05f23d6417
commit aced4f1e23
3 changed files with 146 additions and 69 deletions

View File

@@ -502,7 +502,7 @@ class TestbenchGUI(tk.Tk):
self._profile_after_id = None
def _build_sweep_vi_controls(self, parent) -> None:
frame = ttk.LabelFrame(parent, text="2D Sweep (V × I)", padding=8)
frame = ttk.LabelFrame(parent, text="2D Sweep (V × Load)", padding=8)
frame.pack(fill=tk.X, padx=4, pady=4)
# Voltage range
@@ -521,21 +521,31 @@ class TestbenchGUI(tk.Tk):
self._svi_v_step.insert(0, "5")
self._svi_v_step.pack(side=tk.LEFT, padx=2)
# Current range
# Load mode selector
row = ttk.Frame(frame)
row.pack(fill=tk.X, pady=1)
ttk.Label(row, text="I start:", width=10).pack(side=tk.LEFT)
self._svi_i_start = ttk.Entry(row, width=7)
self._svi_i_start.insert(0, "0.5")
self._svi_i_start.pack(side=tk.LEFT, padx=2)
ttk.Label(row, text="Load mode:", width=10).pack(side=tk.LEFT)
self._svi_load_mode = ttk.Combobox(row, values=["CC", "CP"], width=4, state="readonly")
self._svi_load_mode.set("CC")
self._svi_load_mode.pack(side=tk.LEFT, padx=2)
self._svi_load_mode.bind("<<ComboboxSelected>>", self._on_svi_mode_change)
# Load setpoint range
row = ttk.Frame(frame)
row.pack(fill=tk.X, pady=1)
self._svi_l_label = ttk.Label(row, text="I start:", width=10)
self._svi_l_label.pack(side=tk.LEFT)
self._svi_l_start = ttk.Entry(row, width=7)
self._svi_l_start.insert(0, "0.5")
self._svi_l_start.pack(side=tk.LEFT, padx=2)
ttk.Label(row, text="stop:").pack(side=tk.LEFT)
self._svi_i_stop = ttk.Entry(row, width=7)
self._svi_i_stop.insert(0, "30")
self._svi_i_stop.pack(side=tk.LEFT, padx=2)
self._svi_l_stop = ttk.Entry(row, width=7)
self._svi_l_stop.insert(0, "30")
self._svi_l_stop.pack(side=tk.LEFT, padx=2)
ttk.Label(row, text="step:").pack(side=tk.LEFT)
self._svi_i_step = ttk.Entry(row, width=5)
self._svi_i_step.insert(0, "1")
self._svi_i_step.pack(side=tk.LEFT, padx=2)
self._svi_l_step = ttk.Entry(row, width=5)
self._svi_l_step.insert(0, "1")
self._svi_l_step.pack(side=tk.LEFT, padx=2)
# Supply current limit + settle
row = ttk.Frame(frame)
@@ -890,6 +900,13 @@ class TestbenchGUI(tk.Tk):
return _fmt_eng(val)
return _fmt(val, decimals=4)
def _on_svi_mode_change(self, _event=None) -> None:
mode = self._svi_load_mode.get()
if mode == "CC":
self._svi_l_label.config(text="I start:")
else:
self._svi_l_label.config(text="P start:")
def _set_range(self, channel: int, vi: str, combo: ttk.Combobox) -> None:
"""Set voltage or current range for a HIOKI channel."""
val = combo.get()
@@ -1084,11 +1101,12 @@ class TestbenchGUI(tk.Tk):
"v_start": float(self._svi_v_start.get()),
"v_stop": float(self._svi_v_stop.get()),
"v_step": float(self._svi_v_step.get()),
"i_start": float(self._svi_i_start.get()),
"i_stop": float(self._svi_i_stop.get()),
"i_step": float(self._svi_i_step.get()),
"l_start": float(self._svi_l_start.get()),
"l_stop": float(self._svi_l_stop.get()),
"l_step": float(self._svi_l_step.get()),
"current_limit": float(self._svi_ilimit.get()),
"settle_time": float(self._svi_settle.get()),
"load_mode": self._svi_load_mode.get(),
}
except ValueError:
messagebox.showerror("Invalid Input", "Check sweep parameters.")
@@ -1104,9 +1122,11 @@ class TestbenchGUI(tk.Tk):
if not path:
return
mode = params["load_mode"]
unit = "A" if mode == "CC" else "W"
self._console(
f"2D sweep: V={params['v_start']}-{params['v_stop']}V "
f"I={params['i_start']}-{params['i_stop']}A",
f"{mode}={params['l_start']}-{params['l_stop']}{unit}",
"success",
)
@@ -1149,28 +1169,34 @@ class TestbenchGUI(tk.Tk):
def _sweep_vi_loop(self, p: dict, stop: threading.Event) -> list:
"""Run the 2D sweep on a background thread. Returns list of SweepPoint."""
from testbench.bench import SweepPoint, IDLE_VOLTAGE
from testbench.bench import IDLE_VOLTAGE
bench = self.bench
v_start, v_stop, v_step = p["v_start"], p["v_stop"], p["v_step"]
i_start, i_stop, i_step = p["i_start"], p["i_stop"], p["i_step"]
l_start, l_stop, l_step = p["l_start"], p["l_stop"], p["l_step"]
current_limit = p["current_limit"]
settle = p["settle_time"]
load_mode = p.get("load_mode", "CC")
unit = "A" if load_mode == "CC" else "W"
# Auto-correct directions
if v_start < v_stop and v_step < 0:
v_step = -v_step
elif v_start > v_stop and v_step > 0:
v_step = -v_step
if i_start < i_stop and i_step < 0:
i_step = -i_step
elif i_start > i_stop and i_step > 0:
i_step = -i_step
if l_start < l_stop and l_step < 0:
l_step = -l_step
elif l_start > l_stop and l_step > 0:
l_step = -l_step
# Sanity check
max_v = max(abs(v_start), abs(v_stop))
bench.check_supply_capability(max_v, current_limit)
bench.supply.set_current(current_limit)
bench.supply.output_on()
bench.load.set_mode("CC")
bench.load.set_cc_current(i_start)
bench.load.set_mode(load_mode)
bench._apply_load_value(load_mode, l_start)
bench.load.load_on()
results = []
@@ -1185,20 +1211,20 @@ class TestbenchGUI(tk.Tk):
break
bench.supply.set_voltage(v)
i = i_start
ll = l_start
while not stop.is_set():
if i_step > 0 and i > i_stop + i_step / 2:
if l_step > 0 and ll > l_stop + l_step / 2:
break
if i_step < 0 and i < i_stop + i_step / 2:
if l_step < 0 and ll < l_stop + l_step / 2:
break
bench.load.set_cc_current(i)
bench._apply_load_value(load_mode, ll)
time.sleep(settle)
if stop.is_set():
break
point = bench._record_point(v, current_limit, load_setpoint=i)
point = bench._record_point(v, current_limit, load_setpoint=ll)
results.append(point)
n += 1
@@ -1233,13 +1259,13 @@ class TestbenchGUI(tk.Tk):
self.after(
0,
lambda v=v, i=i, pt=point, n=n: self._svi_status.config(
text=f"[{n}] V={v:.1f}V I={i:.1f}A "
lambda v=v, ll=ll, pt=point, n=n: self._svi_status.config(
text=f"[{n}] V={v:.1f}V {load_mode}={ll:.1f}{unit} "
f"EFF={pt.efficiency:.1f}%"
),
)
i += i_step
ll += l_step
v += v_step
finally:
bench.load.load_off()