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.
- adctoolbox/__init__.py +42 -0
- adctoolbox/aout/__init__.py +23 -0
- adctoolbox/aout/err_auto_correlation.py +58 -0
- adctoolbox/aout/err_envelope_spectrum.py +33 -0
- adctoolbox/aout/err_hist_sine.py +274 -0
- adctoolbox/aout/err_pdf.py +74 -0
- adctoolbox/aout/inl_sine.py +111 -0
- adctoolbox/aout/spec_plot.py +274 -0
- adctoolbox/aout/spec_plot_2tone.py +264 -0
- adctoolbox/aout/spec_plot_phase.py +271 -0
- adctoolbox/aout/tom_decomp.py +218 -0
- adctoolbox/common/__init__.py +21 -0
- adctoolbox/common/alias.py +20 -0
- adctoolbox/common/bit_in_band.py +45 -0
- adctoolbox/common/cap2weight.py +46 -0
- adctoolbox/common/find_bin.py +100 -0
- adctoolbox/common/find_fin.py +21 -0
- adctoolbox/common/find_vinpp.py +94 -0
- adctoolbox/common/sine_fit.py +163 -0
- adctoolbox/data_generation/__init__.py +7 -0
- adctoolbox/data_generation/generate_jitter_data.py +68 -0
- adctoolbox/dout/__init__.py +13 -0
- adctoolbox/dout/fg_cal_sine.py +500 -0
- adctoolbox/dout/fg_cal_sine_2freq.py +198 -0
- adctoolbox/dout/fg_cal_sine_os.py +166 -0
- adctoolbox/dout/overflow_chk.py +147 -0
- adctoolbox/oversampling/__init__.py +7 -0
- adctoolbox/oversampling/ntf_analyzer.py +54 -0
- adctoolbox/utils/__init__.py +9 -0
- adctoolbox/utils/calculate_jitter.py +91 -0
- adctoolbox/utils/debug_jitter.py +101 -0
- adctoolbox/utils/multimodal_report.py +469 -0
- adctoolbox-0.1.0.dist-info/METADATA +263 -0
- adctoolbox-0.1.0.dist-info/RECORD +37 -0
- adctoolbox-0.1.0.dist-info/WHEEL +5 -0
- adctoolbox-0.1.0.dist-info/licenses/LICENSE +38 -0
- 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
|
+
|