adctoolbox 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. adctoolbox/__init__.py +42 -0
  2. adctoolbox/aout/__init__.py +23 -0
  3. adctoolbox/aout/err_auto_correlation.py +58 -0
  4. adctoolbox/aout/err_envelope_spectrum.py +33 -0
  5. adctoolbox/aout/err_hist_sine.py +274 -0
  6. adctoolbox/aout/err_pdf.py +74 -0
  7. adctoolbox/aout/inl_sine.py +111 -0
  8. adctoolbox/aout/spec_plot.py +274 -0
  9. adctoolbox/aout/spec_plot_2tone.py +264 -0
  10. adctoolbox/aout/spec_plot_phase.py +271 -0
  11. adctoolbox/aout/tom_decomp.py +218 -0
  12. adctoolbox/common/__init__.py +21 -0
  13. adctoolbox/common/alias.py +20 -0
  14. adctoolbox/common/bit_in_band.py +45 -0
  15. adctoolbox/common/cap2weight.py +46 -0
  16. adctoolbox/common/find_bin.py +100 -0
  17. adctoolbox/common/find_fin.py +21 -0
  18. adctoolbox/common/find_vinpp.py +94 -0
  19. adctoolbox/common/sine_fit.py +163 -0
  20. adctoolbox/data_generation/__init__.py +7 -0
  21. adctoolbox/data_generation/generate_jitter_data.py +68 -0
  22. adctoolbox/dout/__init__.py +13 -0
  23. adctoolbox/dout/fg_cal_sine.py +500 -0
  24. adctoolbox/dout/fg_cal_sine_2freq.py +198 -0
  25. adctoolbox/dout/fg_cal_sine_os.py +166 -0
  26. adctoolbox/dout/overflow_chk.py +147 -0
  27. adctoolbox/oversampling/__init__.py +7 -0
  28. adctoolbox/oversampling/ntf_analyzer.py +54 -0
  29. adctoolbox/utils/__init__.py +9 -0
  30. adctoolbox/utils/calculate_jitter.py +91 -0
  31. adctoolbox/utils/debug_jitter.py +101 -0
  32. adctoolbox/utils/multimodal_report.py +469 -0
  33. adctoolbox-0.1.0.dist-info/METADATA +263 -0
  34. adctoolbox-0.1.0.dist-info/RECORD +37 -0
  35. adctoolbox-0.1.0.dist-info/WHEEL +5 -0
  36. adctoolbox-0.1.0.dist-info/licenses/LICENSE +38 -0
  37. adctoolbox-0.1.0.dist-info/top_level.txt +1 -0
adctoolbox/__init__.py ADDED
@@ -0,0 +1,42 @@
1
+ """
2
+ ADCToolbox: A comprehensive toolbox for ADC testing and characterization.
3
+
4
+ This package provides tools for analyzing both analog and digital aspects of
5
+ Analog-to-Digital Converters, including spectrum analysis, error characterization,
6
+ calibration algorithms, and more.
7
+
8
+ Submodules:
9
+ -----------
10
+ - common: Common utility functions (alias, find_bin, find_fin, sine_fit, etc.)
11
+ - aout: Analog output / time-domain analysis (spectrum, error analysis, etc.)
12
+ - dout: Digital output / code-level analysis (calibration, overflow detection, etc.)
13
+ - oversampling: Oversampling and noise transfer function tools
14
+ - utils: Utility functions and reporting tools
15
+ - data_generation: Test data generation utilities
16
+
17
+ Usage:
18
+ ------
19
+ >>> from adctoolbox.aout import spec_plot
20
+ >>> from adctoolbox.common import sine_fit
21
+ >>> from adctoolbox.dout import fg_cal_sine
22
+ """
23
+
24
+ __version__ = '0.1.0'
25
+
26
+ # Expose submodules for convenience
27
+ from . import common
28
+ from . import aout
29
+ from . import dout
30
+ from . import oversampling
31
+ from . import utils
32
+ from . import data_generation
33
+
34
+ __all__ = [
35
+ 'common',
36
+ 'aout',
37
+ 'dout',
38
+ 'oversampling',
39
+ 'utils',
40
+ 'data_generation',
41
+ '__version__',
42
+ ]
@@ -0,0 +1,23 @@
1
+ """Analog output (time-domain) analysis tools."""
2
+
3
+ from .spec_plot import spec_plot
4
+ from .spec_plot_phase import spec_plot_phase
5
+ from .spec_plot_2tone import spec_plot_2tone
6
+ from .err_envelope_spectrum import err_envelope_spectrum
7
+ from .err_auto_correlation import err_auto_correlation
8
+ from .err_hist_sine import err_hist_sine
9
+ from .err_pdf import err_pdf
10
+ from .tom_decomp import tom_decomp
11
+ from .inl_sine import inl_sine
12
+
13
+ __all__ = [
14
+ 'spec_plot',
15
+ 'spec_plot_phase',
16
+ 'spec_plot_2tone',
17
+ 'err_envelope_spectrum',
18
+ 'err_auto_correlation',
19
+ 'err_hist_sine',
20
+ 'err_pdf',
21
+ 'tom_decomp',
22
+ 'inl_sine',
23
+ ]
@@ -0,0 +1,58 @@
1
+ import numpy as np
2
+ import matplotlib.pyplot as plt
3
+
4
+
5
+ def err_auto_correlation(err_data, MaxLag=100, Normalize=True):
6
+ """
7
+ Compute and plot autocorrelation function (ACF) of error signal.
8
+
9
+ Parameters:
10
+ err_data: Error signal (1D array)
11
+ MaxLag: Maximum lag in samples (default: 100)
12
+ Normalize: Normalize ACF so ACF[0] = 1 (default: True)
13
+
14
+ Returns:
15
+ acf: Autocorrelation values
16
+ lags: Lag indices (-MaxLag to +MaxLag)
17
+ """
18
+ fig = None
19
+ # Ensure column data
20
+ e = np.asarray(err_data).flatten()
21
+ N = len(e)
22
+
23
+ # Subtract mean
24
+ e = e - np.mean(e)
25
+
26
+ # Preallocate
27
+ lags = np.arange(-MaxLag, MaxLag + 1)
28
+ acf = np.zeros_like(lags, dtype=float)
29
+
30
+ # Compute autocorrelation manually (consistent with MATLAB implementation)
31
+ for k in range(len(lags)):
32
+ lag = lags[k]
33
+ if lag >= 0:
34
+ x1 = e[:N-lag] if lag > 0 else e
35
+ x2 = e[lag:N] if lag > 0 else e
36
+ else:
37
+ lag2 = -lag
38
+ x1 = e[lag2:N]
39
+ x2 = e[:N-lag2]
40
+ acf[k] = np.mean(x1 * x2)
41
+
42
+ # Normalize if required
43
+ if Normalize:
44
+ acf = acf / acf[lags == 0]
45
+
46
+ # Plot with larger fonts to match MATLAB
47
+ fig = plt.figure()
48
+ plt.plot(lags, acf, linewidth=2)
49
+ plt.grid(True)
50
+ plt.xlabel("Lag (samples)", fontsize=14)
51
+ plt.ylabel("Autocorrelation", fontsize=14)
52
+ plt.gca().tick_params(labelsize=14)
53
+
54
+ # Close figure to prevent memory leak
55
+ if fig is not None:
56
+ plt.close(fig)
57
+
58
+ return acf, lags
@@ -0,0 +1,33 @@
1
+ import numpy as np
2
+ import matplotlib.pyplot as plt
3
+ from scipy.signal import hilbert
4
+ from .spec_plot import spec_plot
5
+
6
+
7
+ def err_envelope_spectrum(err_data, Fs=1):
8
+ """
9
+ Compute envelope spectrum using Hilbert transform.
10
+
11
+ Parameters:
12
+ err_data: Error signal (1D array)
13
+ Fs: Sampling frequency (default: 1)
14
+
15
+ Returns:
16
+ None (generates plot via spec_plot)
17
+ """
18
+ # Ensure column data
19
+ e = np.asarray(err_data).flatten()
20
+
21
+ # Envelope extraction via Hilbert transform
22
+ env = np.abs(hilbert(e))
23
+
24
+ # Use spec_plot for spectrum analysis (spec_plot will handle closing its own figure)
25
+ spec_plot(env, Fs=Fs, label=0)
26
+
27
+ # Update labels with larger fonts to match MATLAB
28
+ plt.grid(True)
29
+ plt.xlabel("Frequency (Hz)", fontsize=14)
30
+ plt.ylabel("Envelope Spectrum (dB)", fontsize=14)
31
+ plt.gca().tick_params(labelsize=14)
32
+
33
+ # Note: spec_plot already closes its figure, so no need to close here
@@ -0,0 +1,274 @@
1
+ """
2
+ Error histogram analysis for ADC output.
3
+
4
+ Matches MATLAB errHistSine.m exactly for jitter detection.
5
+ """
6
+
7
+ import numpy as np
8
+ import matplotlib.pyplot as plt
9
+
10
+
11
+ def err_hist_sine(data, bin=100, fin=0, disp=1, mode=0, erange=None):
12
+ """
13
+ Error histogram analysis - matches MATLAB errHistSine.m exactly.
14
+
15
+ Args:
16
+ data: ADC output data (1D array)
17
+ bin: Number of bins (default: 100)
18
+ fin: Normalized frequency (0-1), 0 = auto detect (default: 0)
19
+ disp: Display plots (1=yes, 0=no) (default: 1)
20
+ mode: 0=phase domain, >=1=code domain (default: 0)
21
+ erange: Error range filter [min, max] (default: None)
22
+
23
+ Returns:
24
+ emean: Mean error per bin
25
+ erms: RMS error per bin
26
+ phase_code: Phase positions (deg) or code positions
27
+ anoi: Amplitude noise (reference noise)
28
+ pnoi: Phase noise (phase jitter in radians)
29
+ err: Raw error signal
30
+ xx: x-axis values corresponding to raw error
31
+ """
32
+ fig = None
33
+ # Ensure data is row vector
34
+ data = np.asarray(data).flatten()
35
+ N = len(data)
36
+
37
+ # Sine fit to get ideal signal and error
38
+ from ..common.sine_fit import sine_fit
39
+ if fin == 0:
40
+ data_fit, fin, mag, dc, phi = sine_fit(data)
41
+ else:
42
+ data_fit, _, mag, dc, phi = sine_fit(data, fin)
43
+
44
+ # MATLAB line 38: err = data_fit - data
45
+ err = data_fit - data
46
+
47
+ if mode >= 1:
48
+ # Code mode (not used for jitter detection)
49
+ xx = data
50
+ dat_min = np.min(data)
51
+ dat_max = np.max(data)
52
+ bin_wid = (dat_max - dat_min) / bin
53
+ phase_code = dat_min + np.arange(1, bin+1) * bin_wid - bin_wid/2
54
+
55
+ enum = np.zeros(bin)
56
+ esum = np.zeros(bin)
57
+ erms = np.zeros(bin)
58
+
59
+ for ii in range(N):
60
+ b = min(int(np.floor((data[ii] - dat_min) / bin_wid)), bin-1)
61
+ esum[b] += err[ii]
62
+ enum[b] += 1
63
+
64
+ enum = np.maximum(enum, 1)
65
+ emean = esum / enum
66
+
67
+ for ii in range(N):
68
+ b = min(int(np.floor((data[ii] - dat_min) / bin_wid)), bin-1)
69
+ erms[b] += (err[ii] - emean[b])**2
70
+
71
+ erms = np.sqrt(erms / enum)
72
+
73
+ anoi = np.nan
74
+ pnoi = np.nan
75
+
76
+ if erange is not None:
77
+ eid = (xx >= erange[0]) & (xx <= erange[1])
78
+ xx = xx[eid]
79
+ err = err[eid]
80
+
81
+ # Plotting for code mode
82
+ if disp:
83
+ fig = plt.figure(figsize=(10, 8))
84
+ ax1 = plt.subplot(2, 1, 1)
85
+ ax2 = plt.subplot(2, 1, 2)
86
+
87
+ ax1.plot(data, err, 'r.', markersize=2)
88
+ ax1.plot(phase_code, emean, 'b-', linewidth=2)
89
+ ax1.set_xlim([dat_min, dat_max])
90
+ ax1.set_ylim([np.min(err), np.max(err)])
91
+ ax1.set_ylabel('error')
92
+ ax1.set_xlabel('code')
93
+ ax1.grid(True, alpha=0.3)
94
+
95
+ if erange is not None:
96
+ ax1.plot(xx, err, 'm.', markersize=2)
97
+
98
+ ax2.bar(phase_code, erms, width=bin_wid*0.8, color='skyblue')
99
+ ax2.set_xlim([dat_min, dat_max])
100
+ ax2.set_ylim([0, np.max(erms)*1.1])
101
+ ax2.set_xlabel('code')
102
+ ax2.set_ylabel('RMS error')
103
+ ax2.grid(True, alpha=0.3)
104
+
105
+ plt.tight_layout()
106
+
107
+ # Close figure to prevent memory leak
108
+ if fig is not None:
109
+ plt.close(fig)
110
+
111
+ else:
112
+ # Phase mode (MATLAB lines 93-188) - THIS IS CRITICAL FOR JITTER DETECTION
113
+
114
+ # MATLAB line 94: xx = mod(phi/pi*180 + (0:length(data)-1)*fin*360,360);
115
+ xx = np.mod(phi/np.pi*180 + np.arange(N)*fin*360, 360)
116
+
117
+ # MATLAB line 95: phase_code = (0:bin-1)/bin*360;
118
+ phase_code = np.arange(bin) / bin * 360
119
+
120
+ enum = np.zeros(bin)
121
+ esum = np.zeros(bin)
122
+ erms = np.zeros(bin)
123
+
124
+ # MATLAB lines 101-105: binning
125
+ for ii in range(N):
126
+ # MATLAB line 102: b = mod(round(xx(ii)/360*bin),bin)+1;
127
+ b = int(np.mod(np.round(xx[ii]/360*bin), bin))
128
+ esum[b] += err[ii]
129
+ enum[b] += 1
130
+
131
+ # MATLAB line 106: emean = esum./enum (allows NaN for empty bins)
132
+ with np.errstate(divide='ignore', invalid='ignore'):
133
+ emean = esum / enum # Will create NaN for empty bins (enum=0)
134
+
135
+ # MATLAB lines 107-111: RMS calculation
136
+ for ii in range(N):
137
+ b = int(np.mod(np.round(xx[ii]/360*bin), bin))
138
+ erms[b] += (err[ii] - emean[b])**2
139
+
140
+ # MATLAB line 111: erms = sqrt(erms./enum)
141
+ with np.errstate(divide='ignore', invalid='ignore'):
142
+ erms = np.sqrt(erms / enum) # Will create NaN for empty bins
143
+
144
+ # ========== CRITICAL: Amplitude/Phase Noise Decomposition ==========
145
+ # MATLAB lines 114-147
146
+
147
+ # MATLAB line 114: asen = abs(cos(phase_code/360*2*pi)).^2;
148
+ asen = np.abs(np.cos(phase_code/360*2*np.pi))**2
149
+
150
+ # MATLAB line 115: psen = abs(sin(phase_code/360*2*pi)).^2;
151
+ psen = np.abs(np.sin(phase_code/360*2*np.pi))**2
152
+
153
+ # MATLAB line 117: tmp = linsolve([asen',psen',ones(bin,1)], erms'.^2);
154
+ # Filter out NaN values (empty bins) before least squares fit
155
+ valid_mask = ~np.isnan(erms)
156
+ A = np.column_stack([asen[valid_mask], psen[valid_mask], np.ones(np.sum(valid_mask))])
157
+ b_vec = erms[valid_mask]**2
158
+
159
+ # Use non-negative least squares to ensure non-negative variance components
160
+ # This avoids numerical issues with rank-deficient systems (asen + psen = 1)
161
+ try:
162
+ from scipy.optimize import nnls
163
+ tmp, _ = nnls(A, b_vec)
164
+ except ImportError:
165
+ # Fallback to pseudoinverse if scipy not available
166
+ tmp = np.linalg.pinv(A) @ b_vec
167
+
168
+ # MATLAB line 119-121
169
+ anoi = np.sqrt(tmp[0]) if tmp[0] >= 0 else 0
170
+ pnoi = np.sqrt(tmp[1]) / mag if tmp[1] >= 0 else 0
171
+ ermsbl = tmp[2]
172
+
173
+ # Handle edge cases where fit failed
174
+ if not np.isfinite(anoi):
175
+ anoi = 0
176
+ if not np.isfinite(pnoi):
177
+ pnoi = 0
178
+
179
+ # Filter error range if specified
180
+ if erange is not None:
181
+ eid = (xx >= erange[0]) & (xx <= erange[1])
182
+ xx = xx[eid]
183
+ err = err[eid]
184
+
185
+ # Plotting for phase mode
186
+ if disp:
187
+ fig = plt.figure(figsize=(10, 8))
188
+ ax1 = plt.subplot(2, 1, 1)
189
+ ax2 = plt.subplot(2, 1, 2)
190
+
191
+ # Top subplot: data and error vs phase
192
+ ax1_left = ax1
193
+ ax1_left.plot(xx, data, 'k.', markersize=2, label='data')
194
+ ax1_left.set_xlim([0, 360])
195
+ ax1_left.set_ylim([np.min(data), np.max(data)])
196
+ ax1_left.set_ylabel('data', color='k')
197
+ ax1_left.tick_params(axis='y', labelcolor='k')
198
+
199
+ ax1_right = ax1.twinx()
200
+ ax1_right.plot(xx, err, 'r.', markersize=2, alpha=0.5)
201
+ ax1_right.plot(phase_code, emean, 'b-', linewidth=2, label='error')
202
+ ax1_right.set_xlim([0, 360])
203
+ ax1_right.set_ylim([np.min(err), np.max(err)])
204
+ ax1_right.set_ylabel('error', color='r')
205
+ ax1_right.tick_params(axis='y', labelcolor='r')
206
+
207
+ ax1.legend(['data', 'error'], loc='upper right')
208
+ ax1.set_xlabel('phase(deg)')
209
+ ax1.grid(True, alpha=0.3)
210
+
211
+ if erange is not None:
212
+ ax1_right.plot(xx, err, 'm.', markersize=2)
213
+
214
+ # Bottom subplot: RMS error with fitted curves
215
+ ax2.bar(phase_code, erms, width=360/bin*0.8, color='skyblue', alpha=0.7)
216
+ ax2.plot(phase_code, np.sqrt(anoi**2 * asen + ermsbl), 'b-', linewidth=2)
217
+ ax2.plot(phase_code, np.sqrt(pnoi**2 * psen * mag**2 + ermsbl), 'r-', linewidth=2)
218
+ ax2.set_xlim([0, 360])
219
+ ax2.set_ylim([0, np.max(erms)*1.2])
220
+
221
+ # Add text labels (MATLAB lines 183-184)
222
+ ax2.text(10, np.max(erms)*1.15,
223
+ f'Normalized Amplitude Noise RMS = {anoi/mag:.2e}',
224
+ color='b', fontsize=10)
225
+ ax2.text(10, np.max(erms)*1.05,
226
+ f'Phase Noise RMS = {pnoi:.2e} rad',
227
+ color='r', fontsize=10)
228
+
229
+ ax2.set_xlabel('phase(deg)')
230
+ ax2.set_ylabel('RMS error')
231
+ ax2.grid(True, alpha=0.3)
232
+
233
+ plt.tight_layout()
234
+
235
+ # Close figure to prevent memory leak
236
+ if fig is not None:
237
+ plt.close(fig)
238
+
239
+ return emean, erms, phase_code, anoi, pnoi, err, xx
240
+
241
+
242
+ if __name__ == "__main__":
243
+ import os
244
+
245
+ # Test with jitter
246
+ N = 2**14
247
+ Fs = 10e9
248
+ Fin = 200e6
249
+ J = int(np.round(Fin / Fs * N))
250
+ fin_norm = J / N
251
+ Fin_actual = J / N * Fs
252
+
253
+ # Generate jittered signal
254
+ Tj = 100e-15 # 100 fs jitter
255
+ phase_noise_rms = 2 * np.pi * Fin_actual * Tj
256
+
257
+ Ts = 1 / Fs
258
+ theta = 2 * np.pi * Fin_actual * np.arange(N) * Ts
259
+ phase_jitter = np.random.randn(N) * phase_noise_rms
260
+
261
+ data = np.sin(theta + phase_jitter) * 0.49 + 0.5 + np.random.randn(N) * 0.00001
262
+
263
+ print(f"[Test] [Set jitter] = {Tj*1e15:.2f} fs")
264
+
265
+ # Test errHistSine
266
+ emean, erms, phase_code, anoi, pnoi, err, xx = errHistSine(data, bin=99, fin=fin_norm, disp=0)
267
+
268
+ # Convert pnoi to jitter (MATLAB line 47)
269
+ jitter_calc = pnoi / (2 * np.pi * Fin_actual)
270
+
271
+ print(f"[Test] [Calculated jitter] = {jitter_calc*1e15:.2f} fs")
272
+ print(f"[Test] [anoi] = {anoi:.6e}")
273
+ print(f"[Test] [pnoi] = {pnoi:.6e} rad")
274
+ print(f"[Test] [Ratio] = {jitter_calc/Tj:.2f}")
@@ -0,0 +1,74 @@
1
+ import numpy as np
2
+ import matplotlib.pyplot as plt
3
+
4
+
5
+ def err_pdf(err_data, Resolution=12, FullScale=1):
6
+ """
7
+ Compute error probability density function using Kernel Density Estimation (KDE).
8
+
9
+ Parameters:
10
+ err_data: Error signal (1D array)
11
+ Resolution: ADC resolution in bits (default: 12)
12
+ FullScale: Full-scale range (default: 1)
13
+
14
+ Returns:
15
+ noise_lsb: Error in LSB units (1D array)
16
+ mu: Mean of error distribution
17
+ sigma: Standard deviation of error distribution
18
+ KL_divergence: KL divergence from Gaussian distribution
19
+ x: Sample points for PDF
20
+ fx: KDE-estimated PDF values
21
+ gauss_pdf: Fitted Gaussian PDF values
22
+ """
23
+ fig = None
24
+ # Convert error to LSB units
25
+ lsb = FullScale / (2**Resolution)
26
+ noise_lsb = np.asarray(err_data).flatten() / lsb
27
+ n = noise_lsb
28
+ N = len(n)
29
+
30
+ # Silverman's rule for bandwidth
31
+ h = 1.06 * np.std(n) * N**(-1/5)
32
+
33
+ # Determine x-axis range
34
+ max_abs_noise = np.max(np.abs(n))
35
+ xlim_range = max(0.5, max_abs_noise)
36
+ x = np.linspace(-xlim_range, xlim_range, 200)
37
+ fx = np.zeros_like(x)
38
+
39
+ # KDE computation (manual implementation for consistency with MATLAB)
40
+ for i in range(len(x)):
41
+ u = (x[i] - n) / h
42
+ fx[i] = np.mean(np.exp(-0.5 * u**2)) / (h * np.sqrt(2*np.pi))
43
+
44
+ # Gaussian fit
45
+ mu = np.mean(n)
46
+ sigma = np.std(n)
47
+ gauss_pdf = (1/(sigma*np.sqrt(2*np.pi))) * np.exp(-(x - mu)**2 / (2*sigma**2))
48
+
49
+ # KL divergence calculation
50
+ dx = x[1] - x[0]
51
+ p = fx + np.finfo(float).eps
52
+ q = gauss_pdf + np.finfo(float).eps
53
+ KL_divergence = np.sum(p * np.log(p / q)) * dx
54
+
55
+ # Plotting with larger fonts to match MATLAB
56
+ fig = plt.figure()
57
+ plt.plot(x, fx, linewidth=2, label='KDE Estimate')
58
+ label_str = f"Gaussian Fit (KL = {KL_divergence:.4f}, μ={mu:.2f}, σ={sigma:.2f})"
59
+ plt.plot(x, gauss_pdf, '--r', linewidth=2, label=label_str)
60
+ plt.xlabel("Noise (LSB)", fontsize=18)
61
+ plt.ylabel("PDF", fontsize=18)
62
+ plt.legend(loc="upper left", fontsize=18)
63
+ plt.grid(True)
64
+
65
+ # Set y-axis limits
66
+ y_max = max(np.max(fx), np.max(gauss_pdf)) * 1.3
67
+ plt.ylim([0, y_max])
68
+ plt.gca().tick_params(labelsize=18)
69
+
70
+ # Close figure to prevent memory leak
71
+ if fig is not None:
72
+ plt.close(fig)
73
+
74
+ return noise_lsb, mu, sigma, KL_divergence, x, fx, gauss_pdf
@@ -0,0 +1,111 @@
1
+ import numpy as np
2
+
3
+ def inl_sine(data, clip=0.01):
4
+ """
5
+ Calculate ADC INL (Integral Nonlinearity) and DNL (Differential Nonlinearity) using sine histogram method
6
+
7
+ This function estimates INL and DNL by applying an arccosine transform to the histogram
8
+ of ADC output data. The basic principle: for an ideal sinusoidal input, the cumulative
9
+ distribution function (CDF) of ADC output codes should follow an arcsine function.
10
+ By applying an arccosine transform to the cumulative histogram, we obtain the ideal
11
+ linear code distribution, allowing calculation of nonlinearity errors.
12
+
13
+ Parameters
14
+ ----------
15
+ data : ndarray
16
+ ADC output data (1D array)
17
+ clip : float, optional
18
+ Clipping ratio to exclude outliers at data edges, default is 0.01 (1%)
19
+
20
+ Returns
21
+ -------
22
+ INL : ndarray
23
+ Integral nonlinearity error (in LSB units)
24
+ DNL : ndarray
25
+ Differential nonlinearity error (in LSB units)
26
+ code : ndarray
27
+ Corresponding output code values
28
+
29
+ Notes
30
+ -----
31
+ INL represents the maximum deviation of the actual transfer characteristic from the ideal line
32
+ DNL represents the deviation of adjacent code step sizes from the ideal step size (1 LSB)
33
+ """
34
+
35
+ # Ensure data is a column vector (transpose if needed)
36
+ data = np.asarray(data)
37
+ if data.ndim == 1:
38
+ data = data.reshape(-1, 1)
39
+
40
+ S = data.shape
41
+ if S[0] < S[1]: # If rows < columns, transpose
42
+ data = data.T
43
+
44
+ # Flatten to 1D array
45
+ data = data.flatten()
46
+
47
+ # Calculate data range and apply clipping
48
+ # Clipping excludes data points in the saturation region, which cause statistical bias
49
+ max_data = np.ceil(np.max(data))
50
+ min_data = np.floor(np.min(data))
51
+
52
+ # Clip both ends of the data range by clip ratio
53
+ max_data = round(max_data - clip * (max_data - min_data) / 2)
54
+ min_data = round(min_data + clip * (max_data - min_data) / 2)
55
+
56
+ # Generate code value range
57
+ code = np.arange(min_data, max_data + 1)
58
+
59
+ # Restrict data to clipped range
60
+ data = np.clip(data, min_data, max_data)
61
+
62
+ # Calculate histogram - count occurrences of each code
63
+ # DCC: Digital Code Count, count for each code
64
+ # MATLAB's hist(data, code) uses code values as bin centers
65
+ # For consecutive integers, bins are [code[i]-0.5, code[i]+0.5)
66
+ bins = np.append(code - 0.5, code[-1] + 0.5)
67
+ DCC, _ = np.histogram(data, bins=bins)
68
+
69
+ # Calculate cumulative distribution function (CDF) and apply arccosine transform
70
+ # Theory: for ideal sine wave, CDF should follow arcsine function
71
+ # Use -cos(π * CDF) transform to linearize it
72
+ # cumsum(DCC)/sum(DCC) calculates normalized CDF (range 0 to 1)
73
+ cumulative_prob = np.cumsum(DCC) / np.sum(DCC)
74
+ DCC = -np.cos(np.pi * cumulative_prob)
75
+
76
+ # Calculate differential: difference between adjacent codes
77
+ # DNL is the deviation of each code step size from ideal 1 LSB
78
+ DNL = DCC[1:] - DCC[:-1]
79
+
80
+ # Update code range (differential reduces by one point)
81
+ code = code[:-1]
82
+
83
+ # Apply edge clipping again to DNL and code values
84
+ # This excludes edge effects, as statistics at edges may be inaccurate
85
+ clip_points = int(np.floor(clip * (max_data - min_data + 1) / 2))
86
+
87
+ if clip_points > 0:
88
+ code = code[clip_points:-clip_points]
89
+ DNL = DNL[clip_points:-clip_points]
90
+
91
+ # DNL normalization processing
92
+ # 1. First normalize so sum equals 1 (probability normalization)
93
+ DNL = DNL / np.sum(DNL)
94
+
95
+ # 2. Scale to actual code range (in LSB units)
96
+ # Ideally, each code step size should be 1 LSB
97
+ num_codes = max_data - min_data - clip_points * 2 + 1
98
+ DNL = DNL * num_codes - 1
99
+
100
+ # 3. Remove mean (removes gain error influence)
101
+ # This way DNL reflects only nonlinearity, excluding overall gain error
102
+ DNL = DNL - np.mean(DNL)
103
+
104
+ # Calculate INL: cumulative sum of DNL
105
+ # INL represents deviation of each code point from ideal line
106
+ # Obtained by integrating (accumulating) DNL
107
+ INL = np.cumsum(DNL)
108
+
109
+ return INL, DNL, code
110
+
111
+