Add shade profiles, 2D V×I sweep, meter format toggle, ON/OFF indicators, and GUI console
- Shade profile: CSV-driven irradiance/voltage sequences with load control (bench.run_shade_profile, CLI shade-profile command, GUI profile panel) - 2D sweep: voltage × load current efficiency map with live graph updates (bench.sweep_vi, CLI sweep-vi command, GUI sweep panel with background thread) - GUI: meter format selector (scientific/normal), supply/load ON/OFF indicators, console log with stdout redirect and color-coded messages - Sample profiles: cloud_pass, partial_shade, intermittent_clouds Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -417,6 +417,251 @@ class MPPTTestbench:
|
||||
|
||||
return results
|
||||
|
||||
# ── Load value helper ─────────────────────────────────────────────
|
||||
|
||||
def _apply_load_value(self, mode: str, value: float) -> None:
|
||||
"""Set the load setpoint for the given mode."""
|
||||
if mode == "CC":
|
||||
self.load.set_cc_current(value)
|
||||
elif mode == "CR":
|
||||
self.load.set_cr_resistance(value)
|
||||
elif mode == "CV":
|
||||
self.load.set_cv_voltage(value)
|
||||
elif mode == "CP":
|
||||
self.load.set_cp_power(value)
|
||||
|
||||
# ── Shade Profile ────────────────────────────────────────────────
|
||||
|
||||
@staticmethod
|
||||
def load_shade_profile(path: str) -> list[dict]:
|
||||
"""Load a shade profile CSV.
|
||||
|
||||
Required columns: time, voltage, current_limit
|
||||
Optional columns: load_mode, load_value
|
||||
|
||||
Returns:
|
||||
Sorted list of step dicts.
|
||||
"""
|
||||
import csv as _csv
|
||||
|
||||
steps = []
|
||||
with open(path, newline="") as f:
|
||||
reader = _csv.DictReader(f)
|
||||
for row in reader:
|
||||
step = {
|
||||
"time": float(row["time"]),
|
||||
"voltage": float(row["voltage"]),
|
||||
"current_limit": float(row["current_limit"]),
|
||||
}
|
||||
if "load_mode" in row and row["load_mode"].strip():
|
||||
step["load_mode"] = row["load_mode"].strip().upper()
|
||||
if "load_value" in row and row["load_value"].strip():
|
||||
step["load_value"] = float(row["load_value"])
|
||||
steps.append(step)
|
||||
steps.sort(key=lambda s: s["time"])
|
||||
return steps
|
||||
|
||||
def run_shade_profile(
|
||||
self,
|
||||
steps: list[dict],
|
||||
settle_time: float = 2.0,
|
||||
) -> list[SweepPoint]:
|
||||
"""Run a shade / irradiance profile sequence.
|
||||
|
||||
Steps through the profile, applying voltage/current/load settings
|
||||
at the scheduled times, recording a measurement at each step.
|
||||
|
||||
Args:
|
||||
steps: Profile steps from :meth:`load_shade_profile`.
|
||||
settle_time: Seconds to wait after applying each step before
|
||||
recording a measurement.
|
||||
|
||||
Returns:
|
||||
List of SweepPoint measurements, one per step.
|
||||
"""
|
||||
if not steps:
|
||||
return []
|
||||
|
||||
# Apply first step and turn outputs on
|
||||
first = steps[0]
|
||||
self.supply.set_current(first["current_limit"])
|
||||
self.supply.set_voltage(first["voltage"])
|
||||
self.supply.output_on()
|
||||
|
||||
if "load_mode" in first:
|
||||
self.load.set_mode(first["load_mode"])
|
||||
if "load_value" in first:
|
||||
self._apply_load_value(
|
||||
first.get("load_mode", "CC"), first["load_value"]
|
||||
)
|
||||
self.load.load_on()
|
||||
|
||||
results: list[SweepPoint] = []
|
||||
t0 = time.time()
|
||||
|
||||
try:
|
||||
for i, step in enumerate(steps):
|
||||
# Wait until scheduled time, keepalive supply while waiting
|
||||
target = t0 + step["time"]
|
||||
while True:
|
||||
remaining = target - time.time()
|
||||
if remaining <= 0:
|
||||
break
|
||||
if remaining > 2.0:
|
||||
try:
|
||||
self.supply.measure_voltage()
|
||||
except Exception:
|
||||
pass
|
||||
time.sleep(min(2.0, remaining))
|
||||
else:
|
||||
time.sleep(remaining)
|
||||
|
||||
# Apply settings
|
||||
self.supply.set_current(step["current_limit"])
|
||||
self.supply.set_voltage(step["voltage"])
|
||||
|
||||
if "load_mode" in step:
|
||||
self.load.set_mode(step["load_mode"])
|
||||
if "load_value" in step:
|
||||
self._apply_load_value(
|
||||
step.get("load_mode", "CC"), step["load_value"]
|
||||
)
|
||||
|
||||
time.sleep(settle_time)
|
||||
|
||||
# Record
|
||||
load_val = step.get("load_value", 0.0) or 0.0
|
||||
point = self._record_point(
|
||||
step["voltage"], step["current_limit"], load_val
|
||||
)
|
||||
results.append(point)
|
||||
elapsed = time.time() - t0
|
||||
print(
|
||||
f" [{i + 1}/{len(steps)}] t={elapsed:6.1f}s "
|
||||
f"V={step['voltage']:.1f}V I_lim={step['current_limit']:.1f}A "
|
||||
f"P_in={point.input_power:8.2f}W "
|
||||
f"P_out={point.output_power:8.2f}W "
|
||||
f"EFF={point.efficiency:6.2f}%"
|
||||
)
|
||||
finally:
|
||||
self.load.load_off()
|
||||
self.supply.set_voltage(IDLE_VOLTAGE)
|
||||
print(
|
||||
f"\n Profile complete. Load OFF. "
|
||||
f"Supply returning to {IDLE_VOLTAGE:.0f}V (output stays ON)"
|
||||
)
|
||||
|
||||
return results
|
||||
|
||||
# ── 2D Voltage × Current Sweep ─────────────────────────────────────
|
||||
|
||||
def sweep_vi(
|
||||
self,
|
||||
v_start: float,
|
||||
v_stop: float,
|
||||
v_step: float,
|
||||
i_start: float,
|
||||
i_stop: float,
|
||||
i_step: float,
|
||||
current_limit: float,
|
||||
settle_time: float = 2.0,
|
||||
) -> list[SweepPoint]:
|
||||
"""2D sweep: voltage (outer) × load current (inner).
|
||||
|
||||
At each supply voltage, sweeps the load current through the full
|
||||
range, recording efficiency at every (V, I) combination. Produces
|
||||
a complete efficiency map of the DUT.
|
||||
|
||||
After the sweep the load turns OFF and the supply returns to
|
||||
IDLE_VOLTAGE (75 V, output stays ON).
|
||||
|
||||
Args:
|
||||
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.
|
||||
current_limit: Supply current limit (A) for the entire sweep.
|
||||
settle_time: Seconds to wait after each setpoint change.
|
||||
"""
|
||||
if v_step == 0:
|
||||
raise ValueError("v_step cannot be zero")
|
||||
if i_step == 0:
|
||||
raise ValueError("i_step cannot be zero")
|
||||
|
||||
# Auto-correct step 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
|
||||
|
||||
# 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")
|
||||
|
||||
self.supply.set_current(current_limit)
|
||||
self.supply.output_on()
|
||||
self.load.set_mode("CC")
|
||||
self.load.set_cc_current(i_start)
|
||||
self.load.load_on()
|
||||
|
||||
results: list[SweepPoint] = []
|
||||
n = 0
|
||||
v = v_start
|
||||
|
||||
try:
|
||||
while True:
|
||||
if v_step > 0 and v > v_stop + v_step / 2:
|
||||
break
|
||||
if v_step < 0 and v < v_stop + v_step / 2:
|
||||
break
|
||||
|
||||
self.supply.set_voltage(v)
|
||||
i = i_start
|
||||
|
||||
while True:
|
||||
if i_step > 0 and i > i_stop + i_step / 2:
|
||||
break
|
||||
if i_step < 0 and i < i_stop + i_step / 2:
|
||||
break
|
||||
|
||||
self.load.set_cc_current(i)
|
||||
time.sleep(settle_time)
|
||||
|
||||
point = self._record_point(v, current_limit, load_setpoint=i)
|
||||
results.append(point)
|
||||
n += 1
|
||||
|
||||
print(
|
||||
f" [{n:>4d}/{total}] "
|
||||
f"V={v:6.1f}V I_load={i:6.2f}A "
|
||||
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
|
||||
|
||||
v += v_step
|
||||
|
||||
finally:
|
||||
self.load.load_off()
|
||||
self.supply.set_voltage(IDLE_VOLTAGE)
|
||||
print(
|
||||
f"\n Load OFF. Supply returning to {IDLE_VOLTAGE:.0f}V "
|
||||
f"(output stays ON)"
|
||||
)
|
||||
|
||||
return results
|
||||
|
||||
# ── Efficiency at fixed operating point ───────────────────────────
|
||||
|
||||
def measure_efficiency(
|
||||
|
||||
Reference in New Issue
Block a user