adctoolbox.spectrum.plot_spectrum_virtuoso 源代码

"""
Cadence Virtuoso / ADE Explorer style spectrum plot.

Dark canvas, red stem bars per FFT bin, fine dotted grid.  Same set of
annotations as the analyzer-style plotter (fundamental marker, max-spur
diamond, harmonic squares, metrics text block, NSD line) but recolored
for the dark theme:

    - stems      : red          ('1 stem = 1 bin' Virtuoso convention)
    - fundamental: yellow dot + "Sig = X dB"
    - max spur   : yellow diamond + "MaxSpur"
    - harmonics  : cyan squares + cyan order numbers (2, 3, ...)
    - metric box : white text
    - NSD line   : yellow dashed
    - OSR line   : dim white dashed

Mirrors `plot_spectrum.py`'s structure so the two stay in lock-step
when the underlying metrics / plot_data layout evolves.
"""

import numpy as np
import matplotlib.pyplot as plt

from adctoolbox.spectrum._bin_ranges import rfft_inband_bin_count
from adctoolbox.spectrum.plot_spectrum import (
    _attach_max_spur_annotation,
    _lobe_bounds,
    _noise_floor_axis_min,
    _should_label_harmonic,
)


# Color palette — single place to retune the dark theme
_C_STEM   = '#ff3030'   # red, slightly brighter than 'r' for visibility on black
_C_FUND   = '#ffd000'   # warm yellow — fundamental + spur (loud-and-clear)
_C_HARM   = '#00e5ff'   # cyan — harmonics (distinct from fundamental)
_C_METRIC = 'white'     # metric text block
_C_NSD    = '#ffd000'   # NSD line — match fundamental color
_C_OSR    = '#999999'   # dim white — OSR cutoff line
_C_COH    = '#ffe680'   # light yellow — coherent gain note
_C_WARN   = '#ffa500'   # orange — collision warning


[文档] def plot_spectrum_virtuoso(compute_results, show_title=True, show_label=True, plot_harmonics_up_to=3, ax=None, baseline_db=None): """ Virtuoso/ADE-Explorer style spectrum plot with annotations. Parameters ---------- compute_results : dict Output of `compute_spectrum`. show_title : bool Show auto-generated plot title. show_label : bool Add fundamental marker, spur marker, harmonics markers, and the metric text block. Set False for a bare-stem look. plot_harmonics_up_to : int Highlight harmonics up to this order (HD2..HDk). ax : matplotlib.axes.Axes, optional Target axes. If None, uses current axes. Axes + figure facecolor will be set to black. baseline_db : float, optional Bottom of stem bars in dB. None → adaptive (min(spec) - 5). """ metrics = compute_results['metrics'] plot_data = compute_results['plot_data'] spec_db = plot_data['power_spectrum_db_plot'] freq = plot_data['freq'] fundamental_bin = plot_data['fundamental_bin'] side_bin = int(plot_data.get('side_bin', 0)) spur_bin_idx = plot_data['spur_bin_idx'] spur_db = spec_db[spur_bin_idx] is_coherent = plot_data.get('is_coherent', False) collided_harmonics = plot_data.get('collided_harmonics', []) harmonic_bins = plot_data.get('harmonic_bins', []) harmonics_dbc = metrics.get('harmonics_dbc', []) N = compute_results['N'] M = compute_results['M'] fs = compute_results['fs'] osr = compute_results['osr'] n_inband = rfft_inband_bin_count(N, osr) spur_bin_start, spur_bin_end = _lobe_bounds(spur_bin_idx, side_bin, n_inband) nf_line_level = metrics['noise_floor_dbfs'] - 10 * np.log10(N / (2 * osr)) if ax is None: ax = plt.gca() fig = ax.figure # ---- Dark theme: black canvas, white annotations ----------------- fig.set_facecolor('black') ax.set_facecolor('black') for spine in ax.spines.values(): spine.set_color('white') ax.tick_params(axis='both', which='both', colors='white') ax.xaxis.label.set_color('white') ax.yaxis.label.set_color('white') ax.title.set_color('white') # ---- Axis floor + stem bars -------------------------------------- sndr_floor_level = metrics['sig_pwr_dbfs'] - metrics['sndr_dbc'] minx = _noise_floor_axis_min(nf_line_level, fallback_level=sndr_floor_level) if baseline_db is None: baseline_db = minx else: minx = min(minx, baseline_db) ax.vlines(freq, baseline_db, spec_db, colors=_C_STEM, linewidth=0.8) # ---- Grid -------------------------------------------------------- ax.minorticks_on() ax.grid(True, which='major', linestyle=':', color='white', alpha=0.35, linewidth=0.6) ax.grid(True, which='minor', linestyle=':', color='white', alpha=0.15, linewidth=0.4) # ---- Y-axis follows the plotted NSD/bin line --------------------- x_min = fs / N x_max = fs / 2 ax.set_xlim(x_min, x_max) ax.set_ylim(minx, 0) # ---- Labels ------------------------------------------------------ ax.set_xlabel('freq (Hz)') ax.set_ylabel('(dB)') if show_title: ax.set_title('Power Spectrum') if not show_label: return # ---- Fundamental marker + "Sig" text ----------------------------- ax.plot(freq[fundamental_bin], spec_db[fundamental_bin], 'o', color=_C_FUND, markersize=8) # ---- Harmonic markers -------------------------------------------- if plot_harmonics_up_to > 0 and len(harmonic_bins) and len(harmonics_dbc): for idx in range(len(harmonics_dbc)): order = idx + 2 # HD2, HD3, ... if order > plot_harmonics_up_to or order in collided_harmonics: continue bin_center = harmonic_bins[idx] if not _should_label_harmonic(spec_db[bin_center], nf_line_level): continue ax.plot(bin_center * fs / N, spec_db[bin_center], 's', color=_C_HARM, markersize=5) ax.text(bin_center * fs / N, spec_db[bin_center] + 3, str(order), color=_C_HARM, fontsize=11, ha='center', clip_on=True) # ---- Max-spur diamond + "MaxSpur" text --------------------------- ax.plot( freq[spur_bin_start:spur_bin_end], spec_db[spur_bin_start:spur_bin_end], color=_C_FUND, linestyle='--', linewidth=1.0, label='_max_spur_lobe', ) max_spur_marker, = ax.plot(spur_bin_idx / N * fs, spur_db, 'd', color=_C_FUND, markersize=5) max_spur_label = ax.text(spur_bin_idx / N * fs, spur_db + 10, 'MaxSpur', color=_C_FUND, fontsize=10, ha='center', clip_on=True) _attach_max_spur_annotation(ax, max_spur_marker, max_spur_label, spur_db) # ---- Text-block positioning (mirrors plot_spectrum.py) ----------- # Axes-relative metric text stays fixed if callers change y-limits. metric_x = 0.02 if osr > 1 or fundamental_bin / N >= 0.2 else 0.60 metric_y_start = 0.94 metric_y_step = 0.06 def _fmt_freq(f): if f >= 1e9: return f'{f/1e9:.1f}G' if f >= 1e6: return f'{f/1e6:.1f}M' if f >= 1e3: return f'{f/1e3:.1f}K' return f'{f:.1f}' Fin = fundamental_bin / N * fs ax.text(freq[fundamental_bin], 0.98, f'Sig = {metrics["sig_pwr_dbfs"]:.2f} dB', transform=ax.get_xaxis_transform(), color=_C_FUND, fontsize=10, va='top') snr_text = f'{metrics["snr_dbc"]:.2f} dB' if np.isfinite(metrics["snr_dbc"]) else 'N/A' noise_floor_text = ( f'{metrics["noise_floor_dbfs"]:.2f} dB' if np.isfinite(metrics["noise_floor_dbfs"]) else 'N/A' ) nsd_text = ( f'{metrics["nsd_dbfs_hz"]:.2f} dBFS/Hz' if np.isfinite(metrics["nsd_dbfs_hz"]) else 'N/A' ) metric_lines = [ f'Fin/fs = {_fmt_freq(Fin)} / {_fmt_freq(fs)} Hz', f'ENoB = {metrics["enob"]:.2f}', f'SNDR = {metrics["sndr_dbc"]:.2f} dB', f'SFDR = {metrics["sfdr_dbc"]:.2f} dB', f'THD = {metrics["thd_dbc"]:.2f} dB', f'SNR = {snr_text}', f'Noise Floor = {noise_floor_text}', f'NSD = {nsd_text}', ] # ---- NSD baseline line ------------------------------------------- if not np.isfinite(nf_line_level): pass elif osr > 1: ax.semilogx([fs / N, fs / 2 / osr], [nf_line_level, nf_line_level], '--', color=_C_NSD, linewidth=1) metric_lines.append(f'OSR = {osr:.2f}') ax.plot([fs / 2 / osr, fs / 2 / osr], [0, -1000], '--', color=_C_OSR, linewidth=1) else: ax.plot([0, fs / 2], [nf_line_level, nf_line_level], '--', color=_C_NSD, linewidth=1) # ---- Optional gain / collision notes ----------------------------- warning_lines = [] if is_coherent and M > 1: coh_gain_db = 10 * np.log10(M) warning_lines.append((f'*Coherent Gain = {coh_gain_db:.2f} dB', _C_COH)) if collided_harmonics: collision_str = ', '.join(f'HD{h}' for h in sorted(collided_harmonics)) warning_lines.append((f'*Collided with fundamental: {collision_str}', _C_WARN)) metric_rows = [(line, _C_METRIC) for line in metric_lines] + warning_lines for row, (line, color) in enumerate(metric_rows): ax.text( metric_x, metric_y_start - metric_y_step * row, line, transform=ax.transAxes, color=color, fontsize=10, ha='left', va='top', )