"""Bit-weight radix and effective-resolution analysis.
Visualizes absolute ADC bit weights with radix annotations, and reports a
weight-list effective resolution from the significant reconstructed weights.
"""
import numpy as np
import matplotlib.pyplot as plt
[docs]
def analyze_weight_radix(
weights: np.ndarray,
create_plot: bool = True,
ax=None,
title: str | None = None
) -> dict:
"""
Analyze absolute bit weights, radix ratios, and weight-list resolution.
Pure binary: radix = 2.00. Sub-radix/redundancy: radix < 2.00.
``effres`` is a theoretical resolution estimate from the supplied weight
dynamic range, not a missing-code/DNL proof.
Parameters
----------
weights : np.ndarray
Bit weights (1D array), nominally from MSB to LSB for radix plotting.
Effective-resolution analysis uses sorted absolute magnitudes, so
negative trim weights count by magnitude.
create_plot : bool, default=True
If True, create line plot with radix annotations
ax : plt.Axes, optional
Axes to plot on. If None, uses current axes (plt.gca())
title : str, optional
Title for the plot. If None, uses default title
Returns
-------
dict
``radix``:
Radix between consecutive input-order weights,
``abs(weight[i-1]) / abs(weight[i])``. The first entry is ``NaN``.
``wgtsca``:
Scale factor that maps the significant absolute weights closest to
integer LSB units.
``effres``:
Effective resolution in bits,
``log2(sum(abs_w_sig) / min(abs_w_sig) + 1)``.
Notes
-----
The significant set ``abs_w_sig`` is formed by sorting ``abs(weights)`` in
descending order, then dropping the tail after the first adjacent ratio
``>= 3``. This excludes very small trim/noise weights from ``effres``.
``effres`` is useful for questions like "how many bits does this SAR
weight list span?" It does not verify SAR decision reachability, code
monotonicity, missing codes, DNL/INL, comparator noise, or sampling noise.
What to look for in radix values:
- Radix = 2.00: Binary scaling (SAR, pure binary)
- Radix < 2.00: Redundancy or sub-radix (e.g., 1.5-bit/stage → ~1.90)
- Radix > 2.00: Unusual, may indicate calibration error
- Consistent pattern: Expected architecture behavior
- Random jumps: Calibration errors or bit mismatch
"""
weights = np.asarray(weights, dtype=float)
n_bits = len(weights)
is_negative = weights < 0
abs_weights = np.abs(weights)
# Calculate radix between consecutive bits (|weight[i-1]| / |weight[i]|)
radix = np.zeros(n_bits)
radix[0] = np.nan # No radix for first bit
for i in range(1, n_bits):
radix[i] = abs_weights[i-1] / abs_weights[i]
# --- Compute wgtsca and effres ---
# Step 1: Sort absolute weights descending
sort_idx = np.argsort(abs_weights)[::-1]
abs_w_sorted = abs_weights[sort_idx]
# Step 2: Calculate ratios between adjacent sorted weights
ratios = abs_w_sorted[:-1] / abs_w_sorted[1:]
# Step 3: Find significance threshold (first ratio >= 3.0)
sig_break = np.where(ratios >= 3.0)[0]
if len(sig_break) == 0:
k = len(ratios) # all bits significant
else:
k = sig_break[0] # k ratios are < 3, so k+1 bits significant
# Track MSB/LSB indices (in original array order)
msb_idx = sort_idx[0] # index of largest weight
lsb_idx = sort_idx[k] # index of smallest significant weight
# Significant weights (sorted descending)
abs_w_sig = abs_w_sorted[:k + 1]
# Step 4: Initial wgtsca - normalize smallest significant weight to 1
wgtsca = 1.0 / abs_w_sig[-1]
# Step 5: Refine wgtsca by searching integer MSB values
w_err = np.sqrt(np.mean((abs_w_sig * wgtsca - np.round(abs_w_sig * wgtsca)) ** 2))
wmsb_init = round(abs_w_sorted[0] * wgtsca)
wmsb_min = max(1, round(wmsb_init * 0.5))
wmsb_max = max(wmsb_min, round(wmsb_init * 1.5))
for wmsb in range(wmsb_min, wmsb_max + 1):
w_refine = wmsb / abs_w_sorted[0]
w_err_ref = np.sqrt(np.mean((abs_w_sig * w_refine - np.round(abs_w_sig * w_refine)) ** 2))
if w_err_ref < w_err:
w_err = w_err_ref
wgtsca = w_refine
# Step 6: Effective resolution
effres = np.log2(np.sum(abs_w_sig) / abs_w_sig[-1] + 1)
if create_plot:
if ax is None:
ax = plt.gca()
# Plot connecting line (black)
ax.plot(range(1, n_bits + 1), abs_weights, '-', linewidth=2,
color=[0, 0, 0], label='_nolegend_')
# Positive weight markers (blue)
pos_idx = np.where(~is_negative)[0]
h_pos = None
if len(pos_idx) > 0:
h_pos = ax.plot(pos_idx + 1, abs_weights[pos_idx], 'o', markersize=8,
markerfacecolor=(0.3, 0.6, 0.8), color=(0.3, 0.6, 0.8),
linewidth=2, label='Positive')[0]
# Negative weight markers (red)
neg_idx = np.where(is_negative)[0]
h_neg = None
if len(neg_idx) > 0:
h_neg = ax.plot(neg_idx + 1, abs_weights[neg_idx], 'o', markersize=8,
markerfacecolor=(0.9, 0.3, 0.3), color=(0.9, 0.3, 0.3),
linewidth=2, label='Negative')[0]
ax.set_xlabel('Bit Index', fontsize=14)
ax.set_ylabel('Absolute Weight', fontsize=14)
if title is not None:
ax.set_title(title, fontsize=16)
else:
ax.set_title('Bit Weights with Radix', fontsize=16)
ax.grid(True)
ax.set_xlim([0.5, n_bits + 0.5])
ax.tick_params(labelsize=14)
ax.set_yscale('log')
# MSB label
ax.text(msb_idx + 1, abs_weights[msb_idx] / 1.5, 'MSB',
ha='center', va='top', fontsize=10,
color=(0.8, 0.1, 0.1), fontweight='bold')
# LSB label (only if different from MSB)
if lsb_idx != msb_idx:
ax.text(lsb_idx + 1, abs_weights[lsb_idx] / 1.5, 'LSB',
ha='center', va='top', fontsize=10,
color=(0.1, 0.5, 0.1), fontweight='bold')
# Effective resolution annotation (upper-right)
ax.text(0.98, 0.88, f'Eff. Res: {effres:.2f} bits',
transform=ax.transAxes, ha='right', va='top',
fontweight='bold', color='black',
bbox=dict(boxstyle='round,pad=0.3', facecolor='white',
edgecolor=(0.5, 0.5, 0.5)))
# Legend when negative weights present
if h_pos is not None and h_neg is not None:
ax.legend(loc='best')
elif h_neg is not None:
ax.legend(loc='best')
# Annotate radix on top of each data point (except first bit)
for b in range(1, n_bits):
y_pos = abs_weights[b] * 1.5
ax.text(b + 1, y_pos, f'/{radix[b]:.2f}',
ha='center', fontsize=10, color=[0.2, 0.2, 0.2], fontweight='bold')
return {'radix': radix, 'wgtsca': wgtsca, 'effres': effres}