From a5130729142ff61e45784ff505fa0cfcd1bf9cfc Mon Sep 17 00:00:00 2001 From: saesaemlee Date: Sun, 21 Jun 2026 19:22:02 +0900 Subject: [PATCH] fix(util): plot_shmoo.py is broken on modern matplotlib and mis-wires its ticks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `util/plot_shmoo.py` cannot currently render a Shmoo plot in any environment with matplotlib >= 3.5. Three independent issues were preventing it from working as documented. ## 1. Tick formatters use the old 1-arg lambda signature ax.xaxis.set_major_formatter(lambda val: f'{val:.0f}') ax.yaxis.set_major_formatter(lambda val: f'{val:1.2f}') Matplotlib 3.5+ calls callable formatters as `fmt(value, pos)`. The 1-arg lambdas raise `TypeError: () takes 1 positional argument but 2 were given` during the first `savefig`, before any PNG is produced. Reproduced with matplotlib 3.10 on Python 3.12. Fixed by widening the signature to `lambda val, _pos: ...`. ## 2. Tick locations were assigned to the wrong axis The pcolormesh call is `ax.pcolormesh(ydata, xdata, bar_data)` with `xdata = data['vs_v']` (voltages) and `ydata = data['fs_mhz']` (frequencies). Per matplotlib's `pcolormesh(X, Y, C)` contract, this means the plot's X axis is frequency and the Y axis is voltage — and the existing `set_xlabel('Frequency (MHz)')` / `set_ylabel('Core Voltage (V)')` confirms that intent. However, the surrounding tick wiring was: xticks = xdata[::5] # voltages (0.7..1.2 V) yticks = ydata[::6] # frequencies (40..200 MHz) plt.xticks(xticks, rotation=0) # voltage values placed on the freq axis ax.set_yticks(yticks) # frequency values placed on the volt axis Voltage tick positions (0.7..1.2) fall completely outside the frequency axis range (~40..200), and vice versa, so even after the formatter bug is fixed the axes come out with no usable tick marks. Renamed the local variables to `freq_mhz` / `volt_v` so the tick wiring cannot drift from the pcolormesh argument order, and changed the pcolormesh call to the explicit `(freq_mhz, volt_v, bar_data)` form. `bar_data` already has shape `(len(volt_v), len(freq_mhz))`, so no data reshaping was needed. ## 3. CLI cannot express "no measurement" without crashing The header comment documents calling the script with positional placeholders for the optional `pmeas`/`psupply`/`pchan`/`cmeas`/ `citer`/`ops` arguments. CLI invocations cannot pass real `None`, so those slots arrive as the literal string `"None"`, and the downstream `if pmeas is not None` checks are always true — `generate_data` then crashes with `KeyError: 'None'` while indexing `run["None"]`. Conversely, calling the script with no optional args (just `out_file runs_path`) makes `genargs[6]`, `genargs[4]`, `genargs[1]` raise `IndexError` because the original `main()` body indexes positions that may not exist. Fixed both cases: genargs = tuple(None if g in ('None', '') else g for g in genargs) ... if len(genargs) > 6 and genargs[6] is not None: ... so both `python plot_shmoo.py out.png runs.json` and `python plot_shmoo.py out.png runs.json None None None None None None` now produce a correct correctness-only Shmoo plot. ## Verification Synthetic runs JSON with Vmin(F) = 0.6 + 0.0025*F over a 26x17 (voltage × frequency) sweep produces the expected diagonal Vmin curve. Both the no-args and all-None-placeholder invocations now succeed and write a valid PNG; the original code crashes on both. Co-Authored-By: Claude Opus 4.7 --- util/plot_shmoo.py | 50 ++++++++++++++++++++++++++++++---------------- 1 file changed, 33 insertions(+), 17 deletions(-) diff --git a/util/plot_shmoo.py b/util/plot_shmoo.py index cdcc0f8..b5d8711 100755 --- a/util/plot_shmoo.py +++ b/util/plot_shmoo.py @@ -92,6 +92,13 @@ def generate_data( def main(out_file: str, *genargs) -> int: + # CLI invocations cannot pass real `None`; the documented usage in this + # file's header uses positional placeholders for the optional measurement + # arguments, which then arrive here as the literal string "None". Convert + # those (and empty strings) to actual `None` so the downstream + # `is not None` checks behave as intended. + genargs = tuple(None if g in ('None', '') else g for g in genargs) + # Generate data to be plotted data = generate_data(*genargs) @@ -99,13 +106,13 @@ def main(out_file: str, *genargs) -> int: bar_cmap = mpl.colormaps['viridis'] bar_cmap.set_under('white') bar_show = True - if genargs[6] is not None: + if len(genargs) > 6 and genargs[6] is not None: bar_legend = 'En. Eff. (MFLOP/s/W)' bar_data = data['effs_mflop_per_s_per_w'] - elif genargs[4] is not None: + elif len(genargs) > 4 and genargs[4] is not None: bar_legend = 'Energy (mJ)' bar_data = data['es_mj'] - elif genargs[1] is not None: + elif len(genargs) > 1 and genargs[1] is not None: bar_legend = 'Power (mW)' bar_data = data['ps_mw'] else: @@ -119,22 +126,31 @@ def main(out_file: str, *genargs) -> int: fig = plt.figure(figsize=(3.3*scale, 2.0*scale)) ax = plt.subplot(111) - # Style axes - xdata = data['vs_v'] - ydata = data['fs_mhz'] - xticks = xdata[::5] - yticks = ydata[::6] + # Style axes. The plotted axes are frequency on X (matching xlabel) and + # voltage on Y (matching ylabel); name the local variables accordingly so + # the tick wiring below cannot drift from the pcolormesh argument order. + freq_mhz = data['fs_mhz'] + volt_v = data['vs_v'] + freq_ticks = freq_mhz[::3] + volt_ticks = volt_v[::5] ax.set_xlabel('Frequency (MHz)', fontsize=10) ax.set_ylabel('Core Voltage (V)', fontsize=10) - plt.xticks(xticks, rotation=0) - ax.set_yticks(yticks) - ax.set_xticklabels(xticks) - ax.set_yticklabels(yticks) - ax.xaxis.set_major_formatter(lambda val: f'{val:.0f}') - ax.yaxis.set_major_formatter(lambda val: f'{val:1.2f}') - - # Plot the desired data and save - c = ax.pcolormesh(ydata, xdata, bar_data, cmap=bar_cmap, edgecolor='silver', linewidth=0.0) + ax.set_xticks(freq_ticks) + ax.set_yticks(volt_ticks) + ax.set_xticklabels([f'{v:.0f}' for v in freq_ticks]) + ax.set_yticklabels([f'{v:.2f}' for v in volt_ticks]) + # Matplotlib >= 3.5 calls the formatter as ``fmt(value, pos)``; the + # original 1-arg lambdas raised TypeError during draw, preventing the + # plot from ever being saved. + ax.xaxis.set_major_formatter(lambda val, _pos: f'{val:.0f}') + ax.yaxis.set_major_formatter(lambda val, _pos: f'{val:1.2f}') + + # Plot the desired data and save. pcolormesh(X, Y, C): X = frequency, + # Y = voltage. `bar_data` has shape (len(volt_v), len(freq_mhz)) so + # `bar_data[volt_idx][freq_idx]` lands at (freq_mhz[freq_idx], + # volt_v[volt_idx]). + c = ax.pcolormesh(freq_mhz, volt_v, bar_data, cmap=bar_cmap, + edgecolor='silver', linewidth=0.0) if bar_show: cbar = fig.colorbar(c, ax=ax) cbar.set_label(bar_legend, fontsize=10)