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:
2026-03-13 11:40:42 +07:00
parent 657ff27845
commit ce493204cb

View File

@@ -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,