Source code for adctoolbox.dout.analyze_overflow

"""Overflow detection for sub-radix SAR ADC via residue distribution analysis.

Analyzes residue distribution at each bit position to detect overflow conditions.
This is useful for sub-radix-2 SAR ADC calibration and redundancy analysis.

Ported from MATLAB: overflowChk.m
"""

import numpy as np
import matplotlib.pyplot as plt

[docs] def analyze_overflow( raw_code: np.ndarray, weight: np.ndarray, ofb: int | None = None, create_plot: bool = True, ax=None, title: str | None = None ) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: """ Analyze residue distribution at each bit position for overflow detection. Calculates normalized residue (remaining bits weighted sum) and detects overflow conditions where residue exceeds [0, 1] range. Parameters ---------- raw_code : np.ndarray Digital codes array, shape (N, M) where N=samples, M=bits (MSB first) weight : np.ndarray Weight array for each bit, shape (M,) ofb : int, optional Overflow bit position for overflow detection. Default is M (check at MSB, MATLAB convention: 1=LSB, M=MSB) create_plot : bool, default=True If True, generate residue distribution visualization ax : plt.Axes, optional Axes to plot on. If None, uses current axes (plt.gca()) title : str, optional Title for the plot. If None, no title is set Returns ------- tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray] - range_min: Minimum normalized residue per bit (shape M,) - range_max: Maximum normalized residue per bit (shape M,) - ovf_percent_zero: Underflow percentage per bit (shape M,) - ovf_percent_one: Overflow percentage per bit (shape M,) Notes ----- - A bit segment is the sub-code formed from one bit to the LSB - Residue is normalized by dividing by the sum of weights in the segment - Matches MATLAB ovfchk.m behavior exactly """ raw_code = np.asarray(raw_code) weight = np.asarray(weight) if raw_code.ndim == 1: raw_code = raw_code.reshape(-1, 1) N, M = raw_code.shape if len(weight) != M: raise ValueError(f"Weight length ({len(weight)}) must match number of bits ({M})") # Default ofb is M (LSB) if ofb is None: ofb = M data_decom = np.zeros((N, M)) range_min = np.zeros(M) range_max = np.zeros(M) ovf_percent_zero = np.zeros(M) ovf_percent_one = np.zeros(M) # Calculate normalized residue at each bit position for ii in range(M): # Weighted sum of remaining bits (from current bit to LSB) tmp = raw_code[:, ii:] @ weight[ii:] # Normalize by sum of remaining weights sum_weight = np.sum(weight[ii:]) data_decom[:, ii] = tmp / sum_weight range_min[ii] = np.min(tmp) / sum_weight range_max[ii] = np.max(tmp) / sum_weight # Calculate overflow percentages ovf_percent_zero[ii] = np.sum(data_decom[:, ii] <= 0) / N * 100 ovf_percent_one[ii] = np.sum(data_decom[:, ii] >= 1) / N * 100 # Detect overflow at specified bit position # MATLAB: ovf_zero = (data_decom(:,M-chkpos+1) <= 0); # Python 0-indexed: M-ofb+1-1 = M-ofb ovf_zero = data_decom[:, M - ofb] <= 0 ovf_one = data_decom[:, M - ofb] >= 1 non_ovf = ~(ovf_zero | ovf_one) # Only plot if display requested if create_plot: if ax is None: ax = plt.gca() # Reference lines at 0 and 1 (matching MATLAB) ax.plot([0, M + 1], [1, 1], '-k', linewidth=0.5) ax.plot([0, M + 1], [0, 0], '-k', linewidth=0.5) # Plot min/max range envelope (matching MATLAB) bit_positions = np.arange(1, M + 1) ax.plot(bit_positions, range_min, '-r', linewidth=1.5) ax.plot(bit_positions, range_max, '-r', linewidth=1.5) # Scatter plot for each bit position (matching MATLAB style) for ii in range(M): # Normal samples (blue) alpha = min(max(10 / N, 0.01), 1) if np.sum(non_ovf) > 0: ax.scatter( np.ones(np.sum(non_ovf)) * (ii + 1), data_decom[non_ovf, ii], s=36, facecolors='b', edgecolors='b', alpha=alpha, linewidths=0.5 ) # Overflow high samples (red, shifted left) if np.sum(ovf_one) > 0: ax.scatter( np.ones(np.sum(ovf_one)) * (ii + 1) - 0.2, data_decom[ovf_one, ii], s=36, facecolors='r', edgecolors='r', alpha=alpha, linewidths=0.5 ) # Overflow low samples (yellow, shifted right) if np.sum(ovf_zero) > 0: ax.scatter( np.ones(np.sum(ovf_zero)) * (ii + 1) + 0.2, data_decom[ovf_zero, ii], s=36, facecolors='y', edgecolors='y', alpha=alpha, linewidths=0.5 ) # Percentage labels (matching MATLAB format) ax.text(ii + 1, -0.05, f'{ovf_percent_zero[ii]:.1f}%', ha='center', va='top', fontsize=10) ax.text(ii + 1, 1.05, f'{ovf_percent_one[ii]:.1f}%', ha='center', va='bottom', fontsize=10) # Set axis limits and labels (matching MATLAB) ax.set_xlim([0, M + 1]) ax.set_ylim([-0.1, 1.1]) # X-axis ticks: bit positions from MSB to LSB ax.set_xticks(bit_positions) ax.set_xticklabels([str(M - i) for i in range(M)]) ax.set_xlabel('bit') ax.set_ylabel('Residue Distribution') # Set title if provided if title is not None: ax.set_title(title) return range_min, range_max, ovf_percent_zero, ovf_percent_one