Add plot-tune command for multi-voltage parameter sweep analysis
Overlays efficiency curves from multiple CSV files (one per voltage), finds per-voltage best and overall sweetspot (best average across all voltages), and generates efficiency overlay, heatmap, and loss plots. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
206
testbench/cli.py
206
testbench/cli.py
@@ -817,6 +817,203 @@ def cmd_plot_sweep(_args: argparse.Namespace) -> None:
|
||||
plt.show()
|
||||
|
||||
|
||||
def cmd_plot_tune(_args: argparse.Namespace) -> None:
|
||||
"""Analyze multiple tuning CSVs: overlay efficiency vs parameter, find sweetspot."""
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
from pathlib import Path
|
||||
|
||||
csv_files = _args.csv
|
||||
if not csv_files:
|
||||
print("No CSV files specified.")
|
||||
sys.exit(1)
|
||||
|
||||
# Load all CSVs
|
||||
all_rows = []
|
||||
for f in csv_files:
|
||||
p = Path(f)
|
||||
if not p.exists():
|
||||
print(f"File not found: {p}")
|
||||
sys.exit(1)
|
||||
with open(p) as fh:
|
||||
reader = csv.DictReader(fh)
|
||||
rows = list(reader)
|
||||
print(f" {p.name}: {len(rows)} points")
|
||||
all_rows.extend(rows)
|
||||
|
||||
if not all_rows:
|
||||
print("No data.")
|
||||
sys.exit(1)
|
||||
|
||||
param_name = all_rows[0]["param_name"]
|
||||
param_vals = np.array([float(r["param_value"]) for r in all_rows])
|
||||
voltages_arr = np.array([float(r["voltage_set"]) for r in all_rows])
|
||||
eff_hioki = np.array([float(r["meter_eff"]) for r in all_rows])
|
||||
eff_stm = np.array([float(r["stm_eff"]) for r in all_rows])
|
||||
temp = np.array([float(r["stm_etemp"]) for r in all_rows])
|
||||
pin = np.array([float(r["meter_pin"]) for r in all_rows])
|
||||
pout = np.array([float(r["meter_pout"]) for r in all_rows])
|
||||
|
||||
voltages = sorted(set(voltages_arr))
|
||||
valid = (eff_hioki > 0) & (eff_hioki < 110) & (pin > 1.0)
|
||||
|
||||
print(f"\nParameter: {param_name}")
|
||||
print(f"Voltages: {', '.join(f'{v:.0f}V' for v in voltages)}")
|
||||
print(f"Total: {len(all_rows)} points ({int(valid.sum())} valid)")
|
||||
|
||||
# Per-voltage best
|
||||
print(f"\n{'Voltage':>8} {'Best DT':>8} {'Efficiency':>12} {'Temp':>8} {'P_in':>8} {'P_out':>8}")
|
||||
print("-" * 62)
|
||||
per_voltage_best = {}
|
||||
for v in voltages:
|
||||
mask = valid & (voltages_arr == v)
|
||||
if not np.any(mask):
|
||||
continue
|
||||
idx = np.argmax(eff_hioki[mask])
|
||||
indices = np.where(mask)[0]
|
||||
best_i = indices[idx]
|
||||
per_voltage_best[v] = param_vals[best_i]
|
||||
print(f"{v:>7.0f}V {param_vals[best_i]:>8.0f} "
|
||||
f"{eff_hioki[best_i]:>11.2f}% {temp[best_i]:>7.0f}C "
|
||||
f"{pin[best_i]:>7.1f}W {pout[best_i]:>7.1f}W")
|
||||
|
||||
# Overall best
|
||||
valid_eff = eff_hioki.copy()
|
||||
valid_eff[~valid] = -1
|
||||
overall_best_i = np.argmax(valid_eff)
|
||||
print(f"\nOverall best: {param_name}={param_vals[overall_best_i]:.0f} "
|
||||
f"@ {voltages_arr[overall_best_i]:.0f}V ->{eff_hioki[overall_best_i]:.2f}%")
|
||||
|
||||
# Sweetspot: the value that maximizes average efficiency across all voltages
|
||||
unique_params = sorted(set(param_vals))
|
||||
avg_eff = []
|
||||
for pv in unique_params:
|
||||
mask = valid & (param_vals == pv)
|
||||
if np.any(mask):
|
||||
avg_eff.append(np.mean(eff_hioki[mask]))
|
||||
else:
|
||||
avg_eff.append(0.0)
|
||||
avg_eff = np.array(avg_eff)
|
||||
sweetspot_idx = np.argmax(avg_eff)
|
||||
sweetspot_val = unique_params[sweetspot_idx]
|
||||
sweetspot_eff = avg_eff[sweetspot_idx]
|
||||
print(f"Sweetspot (best avg across all voltages): "
|
||||
f"{param_name}={sweetspot_val:.0f} ->avg EFF={sweetspot_eff:.2f}%")
|
||||
|
||||
# Colour map
|
||||
cmap = plt.cm.viridis
|
||||
colours = {v: cmap(i / max(len(voltages) - 1, 1)) for i, v in enumerate(voltages)}
|
||||
|
||||
# ── Figure 1: Efficiency vs parameter, one line per voltage ──────
|
||||
fig1, (ax1, ax1b) = plt.subplots(2, 1, figsize=(14, 9), sharex=True,
|
||||
gridspec_kw={"height_ratios": [3, 1]})
|
||||
|
||||
for v in voltages:
|
||||
mask = valid & (voltages_arr == v)
|
||||
if not np.any(mask):
|
||||
continue
|
||||
x, y = param_vals[mask], eff_hioki[mask]
|
||||
order = np.argsort(x)
|
||||
ax1.plot(x[order], y[order], "o-", color=colours[v], markersize=3,
|
||||
label=f"{v:.0f}V (best={per_voltage_best.get(v, 0):.0f})")
|
||||
|
||||
# Average line
|
||||
ax1.plot(unique_params, avg_eff, "s--", color="black", markersize=4,
|
||||
linewidth=2, label=f"Average (sweetspot={sweetspot_val:.0f})", zorder=8)
|
||||
|
||||
# Sweetspot marker
|
||||
ax1.axvline(sweetspot_val, color="red", linestyle="--", alpha=0.6, linewidth=2)
|
||||
ax1.plot(sweetspot_val, sweetspot_eff, "*", color="red", markersize=20,
|
||||
zorder=10, label=f"Sweetspot: {sweetspot_val:.0f} ->{sweetspot_eff:.2f}%")
|
||||
|
||||
ax1.set_ylabel("Efficiency (%)", fontsize=12)
|
||||
ax1.set_title(f"Deadtime Tuning: {param_name} — All Voltages Overlaid", fontsize=14)
|
||||
ax1.legend(loc="lower right", fontsize=8, ncol=2)
|
||||
ax1.grid(True, alpha=0.3)
|
||||
|
||||
# Tighten y-axis to show detail
|
||||
valid_effs = eff_hioki[valid]
|
||||
if len(valid_effs) > 0:
|
||||
ax1.set_ylim(bottom=max(0, valid_effs.min() - 1),
|
||||
top=min(100.5, valid_effs.max() + 0.5))
|
||||
|
||||
# Temperature subplot
|
||||
for v in voltages:
|
||||
mask = valid & (voltages_arr == v)
|
||||
if not np.any(mask):
|
||||
continue
|
||||
x, y = param_vals[mask], temp[mask]
|
||||
order = np.argsort(x)
|
||||
ax1b.plot(x[order], y[order], "o-", color=colours[v], markersize=2)
|
||||
|
||||
ax1b.axvline(sweetspot_val, color="red", linestyle="--", alpha=0.6, linewidth=2)
|
||||
ax1b.set_xlabel(f"{param_name} (HRTIM ticks)", fontsize=12)
|
||||
ax1b.set_ylabel("Temp (°C)", fontsize=11)
|
||||
ax1b.grid(True, alpha=0.3)
|
||||
fig1.tight_layout()
|
||||
|
||||
# ── Figure 2: Heatmap (parameter × voltage) ─────────────────────
|
||||
fig2, ax2 = plt.subplots(figsize=(14, 7))
|
||||
p_grid = np.array(sorted(set(param_vals)))
|
||||
v_grid = np.array(voltages)
|
||||
eff_map = np.full((len(v_grid), len(p_grid)), np.nan)
|
||||
for r in all_rows:
|
||||
e = float(r["meter_eff"])
|
||||
if not (0 < e < 110 and float(r["meter_pin"]) > 1.0):
|
||||
continue
|
||||
vi = np.searchsorted(v_grid, float(r["voltage_set"]))
|
||||
pi = np.searchsorted(p_grid, float(r["param_value"]))
|
||||
if vi < len(v_grid) and pi < len(p_grid):
|
||||
eff_map[vi, pi] = e
|
||||
|
||||
im = ax2.pcolormesh(p_grid, v_grid, eff_map, cmap="RdYlGn", shading="nearest")
|
||||
fig2.colorbar(im, ax=ax2, label="Efficiency (%)")
|
||||
ax2.axvline(sweetspot_val, color="blue", linestyle="--", linewidth=2, alpha=0.7)
|
||||
ax2.set_xlabel(f"{param_name} (HRTIM ticks)", fontsize=12)
|
||||
ax2.set_ylabel("Supply Voltage (V)", fontsize=12)
|
||||
ax2.set_title(f"Efficiency Map: {param_name} × Voltage", fontsize=14)
|
||||
fig2.tight_layout()
|
||||
|
||||
# ── Figure 3: Power loss vs parameter ────────────────────────────
|
||||
fig3, ax3 = plt.subplots(figsize=(14, 7))
|
||||
for v in voltages:
|
||||
mask = valid & (voltages_arr == v)
|
||||
if not np.any(mask):
|
||||
continue
|
||||
x = param_vals[mask]
|
||||
loss = pin[mask] - pout[mask]
|
||||
order = np.argsort(x)
|
||||
ax3.plot(x[order], loss[order], "o-", color=colours[v], markersize=3,
|
||||
label=f"{v:.0f}V")
|
||||
|
||||
ax3.axvline(sweetspot_val, color="red", linestyle="--", alpha=0.6, linewidth=2)
|
||||
ax3.set_xlabel(f"{param_name} (HRTIM ticks)", fontsize=12)
|
||||
ax3.set_ylabel("Power Loss (W)", fontsize=12)
|
||||
ax3.set_title(f"Power Loss vs {param_name} — All Voltages", fontsize=14)
|
||||
ax3.legend(fontsize=8, ncol=2)
|
||||
ax3.grid(True, alpha=0.3)
|
||||
fig3.tight_layout()
|
||||
|
||||
# Save
|
||||
if _args.output_dir:
|
||||
out = Path(_args.output_dir)
|
||||
out.mkdir(parents=True, exist_ok=True)
|
||||
else:
|
||||
out = Path(csv_files[0]).parent
|
||||
|
||||
stem = f"tune_{param_name}"
|
||||
fig1.savefig(out / f"{stem}_efficiency.png", dpi=150)
|
||||
fig2.savefig(out / f"{stem}_heatmap.png", dpi=150)
|
||||
fig3.savefig(out / f"{stem}_loss.png", dpi=150)
|
||||
print(f"\nSaved plots to {out}:")
|
||||
print(f" {stem}_efficiency.png")
|
||||
print(f" {stem}_heatmap.png")
|
||||
print(f" {stem}_loss.png")
|
||||
|
||||
if not _args.no_show:
|
||||
plt.show()
|
||||
|
||||
|
||||
# ── Main ──────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -1015,12 +1212,21 @@ examples:
|
||||
p_plot.add_argument("-o", "--output-dir", help="Directory for saved plots (default: same as CSV)")
|
||||
p_plot.add_argument("--no-show", action="store_true", help="Save plots without displaying")
|
||||
|
||||
# plot-tune (offline, no instruments)
|
||||
p_pt = sub.add_parser("plot-tune", help="Analyze multiple tuning CSVs: overlay, find sweetspot (no instruments needed)")
|
||||
p_pt.add_argument("csv", nargs="+", help="Tuning CSV files (e.g. dt_0_3A_50V.csv dt_0_3A_60V.csv ...)")
|
||||
p_pt.add_argument("-o", "--output-dir", help="Directory for saved plots (default: same as first CSV)")
|
||||
p_pt.add_argument("--no-show", action="store_true", help="Save plots without displaying")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Offline commands (no instruments needed)
|
||||
if args.command == "plot-sweep":
|
||||
cmd_plot_sweep(args)
|
||||
return
|
||||
if args.command == "plot-tune":
|
||||
cmd_plot_tune(args)
|
||||
return
|
||||
|
||||
dispatch = {
|
||||
"identify": cmd_identify,
|
||||
|
||||
Reference in New Issue
Block a user