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
co-authored by Claude Opus 4.6
parent 05f23d6417
commit aced4f1e23
3 changed files with 146 additions and 69 deletions
+73 -27
View File
@@ -316,6 +316,9 @@ class MPPTTestbench:
if v_step == 0:
raise ValueError("v_step cannot be zero")
max_v = max(abs(v_start), abs(v_stop))
self.check_supply_capability(max_v, current_limit)
# Auto-correct step direction
if v_start < v_stop and v_step < 0:
v_step = -v_step
@@ -378,6 +381,8 @@ class MPPTTestbench:
if i_step == 0:
raise ValueError("i_step cannot be zero")
self.check_supply_capability(voltage, current_limit)
# Auto-correct step direction
if i_start < i_stop and i_step < 0:
i_step = -i_step
@@ -553,6 +558,33 @@ class MPPTTestbench:
return results
# ── Sanity checks ────────────────────────────────────────────────
def check_supply_capability(
self,
max_voltage: float,
current_limit: float,
) -> None:
"""Check that the supply can deliver the requested V and I.
Queries the supply's voltage range (max V) and verifies the
requested parameters are within bounds. Raises ValueError if not.
"""
supply_max_v = self.supply.get_voltage_range()
if max_voltage > supply_max_v:
raise ValueError(
f"Requested voltage {max_voltage:.1f}V exceeds supply "
f"range {supply_max_v:.1f}V"
)
max_power = max_voltage * current_limit
print(
f" Supply check: V_max={max_voltage:.1f}V, "
f"I_limit={current_limit:.1f}A, "
f"P_max={max_power:.0f}W "
f"(range {supply_max_v:.0f}V)"
)
# ── 2D Voltage × Current Sweep ─────────────────────────────────────
def sweep_vi(
@@ -560,16 +592,17 @@ class MPPTTestbench:
v_start: float,
v_stop: float,
v_step: float,
i_start: float,
i_stop: float,
i_step: float,
l_start: float,
l_stop: float,
l_step: float,
current_limit: float,
settle_time: float = 2.0,
load_mode: str = "CC",
) -> list[SweepPoint]:
"""2D sweep: voltage (outer) × load current (inner).
"""2D sweep: voltage (outer) × load setpoint (inner).
At each supply voltage, sweeps the load current through the full
range, recording efficiency at every (V, I) combination. Produces
At each supply voltage, sweeps the load through the full
range, recording efficiency at every combination. Produces
a complete efficiency map of the DUT.
After the sweep the load turns OFF and the supply returns to
@@ -579,16 +612,25 @@ class MPPTTestbench:
v_start: Starting supply voltage (V).
v_stop: Final supply voltage (V).
v_step: Voltage step size (V). Sign is auto-corrected.
i_start: Starting load current (A).
i_stop: Final load current (A).
i_step: Current step size (A). Sign is auto-corrected.
l_start: Starting load setpoint (A for CC, W for CP).
l_stop: Final load setpoint.
l_step: Load step size. Sign is auto-corrected.
current_limit: Supply current limit (A) for the entire sweep.
settle_time: Seconds to wait after each setpoint change.
load_mode: Load mode — "CC" (constant current) or "CP"
(constant power).
"""
if v_step == 0:
raise ValueError("v_step cannot be zero")
if i_step == 0:
raise ValueError("i_step cannot be zero")
if l_step == 0:
raise ValueError("l_step cannot be zero")
load_mode = load_mode.upper()
if load_mode not in ("CC", "CP"):
raise ValueError(f"load_mode must be CC or CP, got {load_mode!r}")
# Sanity check
max_v = max(abs(v_start), abs(v_stop))
self.check_supply_capability(max_v, current_limit)
# Auto-correct step directions
if v_start < v_stop and v_step < 0:
@@ -596,21 +638,25 @@ class MPPTTestbench:
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
# Count steps for progress display
v_count = int(abs(v_stop - v_start) / abs(v_step)) + 1
i_count = int(abs(i_stop - i_start) / abs(i_step)) + 1
total = v_count * i_count
print(f" Grid: {v_count} voltage × {i_count} current = {total} points")
l_count = int(abs(l_stop - l_start) / abs(l_step)) + 1
total = v_count * l_count
unit = "A" if load_mode == "CC" else "W"
print(
f" Grid: {v_count} voltage × {l_count} {load_mode} "
f"= {total} points"
)
self.supply.set_current(current_limit)
self.supply.output_on()
self.load.set_mode("CC")
self.load.set_cc_current(i_start)
self.load.set_mode(load_mode)
self._apply_load_value(load_mode, l_start)
self.load.load_on()
results: list[SweepPoint] = []
@@ -625,30 +671,30 @@ class MPPTTestbench:
break
self.supply.set_voltage(v)
i = i_start
ll = l_start
while True:
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
self.load.set_cc_current(i)
self._apply_load_value(load_mode, ll)
time.sleep(settle_time)
point = self._record_point(v, current_limit, load_setpoint=i)
point = self._record_point(v, current_limit, load_setpoint=ll)
results.append(point)
n += 1
print(
f" [{n:>4d}/{total}] "
f"V={v:6.1f}V I_load={i:6.2f}A "
f"V={v:6.1f}V {load_mode}={ll:6.2f}{unit} "
f"P_in={point.input_power:8.2f}W "
f"P_out={point.output_power:8.2f}W "
f"EFF={point.efficiency:6.2f}%"
)
i += i_step
ll += l_step
v += v_step