Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 26 additions & 1 deletion python/tests/unit/aout/test_analyze_error_autocorrelation.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,23 @@
output_dir.mkdir(exist_ok=True)


def _assert_acf_panel(ax, result, title, max_lag, n_samples):
assert ax.get_title() == title
assert ax.get_xlabel() == 'Lag (samples)'
assert ax.get_ylabel() == 'ACF'
assert len(ax.collections) >= 1
assert len(ax.lines) >= 1

lags = result['lags']
acf = result['acf']
assert lags.shape == acf.shape == (2 * max_lag + 1,)
assert lags[0] == -max_lag
assert lags[-1] == max_lag
assert acf[lags == 0][0] == pytest.approx(1.0)
assert result['error_signal'].shape == (n_samples,)
assert np.all(np.isfinite(acf))


def test_analyze_error_autocorrelation_basic():
"""Test error autocorrelation analysis for different noise types."""
# Setup
Expand Down Expand Up @@ -59,14 +76,22 @@ def test_analyze_error_autocorrelation_basic():

fig_path = output_dir / 'test_analyze_error_autocorrelation.png'
plt.savefig(fig_path, dpi=150, bbox_inches='tight')
plt.close()

print(f"\n[Save fig] -> [{fig_path.resolve()}]\n")

# Verify file was created
assert fig_path.exists(), f"Figure file not created: {fig_path}"
assert fig_path.stat().st_size > 0, f"Figure file is empty: {fig_path}"

assert len(fig.axes) == 3
for ax, result, title in [
(axes[0], result1, 'Thermal Noise'),
(axes[1], result2, 'Memory Effect'),
(axes[2], result3, 'Drift'),
]:
_assert_acf_panel(ax, result, title, max_lag=100, n_samples=N)
plt.close(fig)


if __name__ == '__main__':
"""Run analyze_error_autocorrelation tests standalone"""
Expand Down
14 changes: 8 additions & 6 deletions python/tests/unit/aout/test_analyze_error_by_phase.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ def test_analyze_error_by_phase_pure_cases():
A = 0.49
DC = 0.5
phase_clean = 2 * np.pi * Fin * t
rng = np.random.default_rng(2026062210)

print(f"\n[Config] Fs={Fs/1e6:.0f} MHz, Fin={Fin/1e6:.2f} MHz, N={N}, A={A}")

Expand All @@ -38,9 +39,9 @@ def test_analyze_error_by_phase_pure_cases():

# Generate signals
for case in test_cases:
am_noise = np.random.randn(N) * case['am'] if case['am'] > 0 else 0
pm_noise = np.random.randn(N) * case['pm'] / A if case['pm'] > 0 else 0
th_noise = np.random.randn(N) * case['thermal'] if case['thermal'] > 0 else 0
am_noise = rng.standard_normal(N) * case['am'] if case['am'] > 0 else 0
pm_noise = rng.standard_normal(N) * case['pm'] / A if case['pm'] > 0 else 0
th_noise = rng.standard_normal(N) * case['thermal'] if case['thermal'] > 0 else 0
case['signal'] = (A + am_noise) * np.sin(phase_clean + pm_noise) + DC + th_noise

# Test both baseline modes
Expand Down Expand Up @@ -93,6 +94,7 @@ def test_analyze_error_by_phase_mixed_cases():
A = 0.49
DC = 0.5
phase_clean = 2 * np.pi * Fin * t
rng = np.random.default_rng(2026062211)

# Define mixed test cases
test_cases = [
Expand All @@ -106,9 +108,9 @@ def test_analyze_error_by_phase_mixed_cases():

# Generate signals
for case in test_cases:
am_noise = np.random.randn(N) * case['am'] if case['am'] > 0 else 0
pm_noise = np.random.randn(N) * case['pm'] / A if case['pm'] > 0 else 0
th_noise = np.random.randn(N) * case['thermal'] if case['thermal'] > 0 else 0
am_noise = rng.standard_normal(N) * case['am'] if case['am'] > 0 else 0
pm_noise = rng.standard_normal(N) * case['pm'] / A if case['pm'] > 0 else 0
th_noise = rng.standard_normal(N) * case['thermal'] if case['thermal'] > 0 else 0
case['signal'] = (A + am_noise) * np.sin(phase_clean + pm_noise) + DC + th_noise

fig, axes = plt.subplots(1, 3, figsize=(18, 8))
Expand Down
15 changes: 9 additions & 6 deletions python/tests/unit/aout/test_analyze_error_by_value.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,16 @@ def test_analyze_error_by_value_thermal_vs_nonlinearity():
A = 0.49
DC = 0.5
base_noise = 50e-6
rng = np.random.default_rng(2026062212)

print(f"\n[Config] Fs={Fs/1e6:.0f} MHz, Fin={Fin/1e6:.2f} MHz, N={N}")

# Case 1: Ideal ADC with Thermal Noise
sig_noise = A * np.sin(2 * np.pi * Fin * t) + DC + np.random.randn(N) * base_noise
sig_noise = A * np.sin(2 * np.pi * Fin * t) + DC + rng.standard_normal(N) * base_noise

# Case 2: ADC with 3rd Order Nonlinearity
k3 = 0.01
sig_nonlin = A * np.sin(2 * np.pi * Fin * t) + DC + k3 * (A * np.sin(2 * np.pi * Fin * t))**3 + np.random.randn(N) * base_noise
sig_nonlin = A * np.sin(2 * np.pi * Fin * t) + DC + k3 * (A * np.sin(2 * np.pi * Fin * t))**3 + rng.standard_normal(N) * base_noise

# Create figure with 3 subplots
fig, axes = plt.subplots(1, 3, figsize=(18, 8))
Expand Down Expand Up @@ -65,8 +66,9 @@ def test_analyze_error_by_value_bin_count(n_bins):
DC = 0.5
base_noise = 50e-6
k3 = 0.01
rng = np.random.default_rng(2026062213)

sig_nonlin = A * np.sin(2 * np.pi * Fin * t) + DC + k3 * (A * np.sin(2 * np.pi * Fin * t))**3 + np.random.randn(N) * base_noise
sig_nonlin = A * np.sin(2 * np.pi * Fin * t) + DC + k3 * (A * np.sin(2 * np.pi * Fin * t))**3 + rng.standard_normal(N) * base_noise

# Create a simple plot
fig, ax = plt.subplots(1, 1, figsize=(6, 5))
Expand All @@ -86,17 +88,18 @@ def test_analyze_error_by_value_different_nonlinearities():
A = 0.49
DC = 0.5
base_noise = 50e-6
rng = np.random.default_rng(2026062214)

# Case 1: Thermal only
sig_thermal = A * np.sin(2 * np.pi * Fin * t) + DC + np.random.randn(N) * base_noise
sig_thermal = A * np.sin(2 * np.pi * Fin * t) + DC + rng.standard_normal(N) * base_noise

# Case 2: 2nd order nonlinearity
k2 = 0.01
sig_k2 = A * np.sin(2 * np.pi * Fin * t) + DC + k2 * (A * np.sin(2 * np.pi * Fin * t))**2 + np.random.randn(N) * base_noise
sig_k2 = A * np.sin(2 * np.pi * Fin * t) + DC + k2 * (A * np.sin(2 * np.pi * Fin * t))**2 + rng.standard_normal(N) * base_noise

# Case 3: 3rd order nonlinearity
k3 = 0.01
sig_k3 = A * np.sin(2 * np.pi * Fin * t) + DC + k3 * (A * np.sin(2 * np.pi * Fin * t))**3 + np.random.randn(N) * base_noise
sig_k3 = A * np.sin(2 * np.pi * Fin * t) + DC + k3 * (A * np.sin(2 * np.pi * Fin * t))**3 + rng.standard_normal(N) * base_noise

# Create figure with 3 subplots
fig, axes = plt.subplots(1, 3, figsize=(18, 8))
Expand Down
29 changes: 25 additions & 4 deletions python/tests/unit/aout/test_analyze_error_envelope_spectrum.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,19 @@
output_dir.mkdir(exist_ok=True)


def _assert_envelope_spectrum_panel(ax, result, title, n_samples):
assert ax.get_title() == title
assert ax.get_xlabel() == 'Frequency (Hz)'
assert ax.get_ylabel() == 'Envelope Spectrum (dB)'
assert len(ax.lines) >= 1
assert len(ax.lines[0].get_xdata()) > 0
assert len(ax.lines[0].get_ydata()) > 0
assert result['error_signal'].shape == (n_samples,)
assert result['envelope'].shape == (n_samples,)
assert np.all(np.isfinite(result['envelope']))
assert np.all(result['envelope'] >= 0)


def test_analyze_error_envelope_spectrum_basic():
"""Test error envelope spectrum analysis for different noise types."""
# Setup
Expand All @@ -37,24 +50,32 @@ def test_analyze_error_envelope_spectrum_basic():
fig, axes = plt.subplots(1, 3, figsize=(18, 6))

# Analyze each case
analyze_error_envelope_spectrum(sig_thermal, fs=Fs, frequency=Fin/Fs, ax=axes[0], title='Thermal Noise')
analyze_error_envelope_spectrum(sig_am_noise, fs=Fs, frequency=Fin/Fs, ax=axes[1], title='AM Noise')
analyze_error_envelope_spectrum(sig_am_tone, fs=Fs, frequency=Fin/Fs, ax=axes[2], title='AM Tone')
result1 = analyze_error_envelope_spectrum(sig_thermal, fs=Fs, frequency=Fin/Fs, ax=axes[0], title='Thermal Noise')
result2 = analyze_error_envelope_spectrum(sig_am_noise, fs=Fs, frequency=Fin/Fs, ax=axes[1], title='AM Noise')
result3 = analyze_error_envelope_spectrum(sig_am_tone, fs=Fs, frequency=Fin/Fs, ax=axes[2], title='AM Tone')

fig.suptitle(f'Error Envelope Spectrum Analysis: ADC Non-idealities (Fs={Fs/1e6:.0f} MHz, Fin={Fin/1e6:.1f} MHz)',
fontsize=14, fontweight='bold')
plt.tight_layout()

fig_path = output_dir / 'test_analyze_error_envelope_spectrum.png'
plt.savefig(fig_path, dpi=150, bbox_inches='tight')
plt.close()

print(f"[Save fig] -> [{fig_path.resolve()}]\n")

# Verify file was created
assert fig_path.exists(), f"Figure file not created: {fig_path}"
assert fig_path.stat().st_size > 0, f"Figure file is empty: {fig_path}"

assert len(fig.axes) == 3
for ax, result, title in [
(axes[0], result1, 'Thermal Noise'),
(axes[1], result2, 'AM Noise'),
(axes[2], result3, 'AM Tone'),
]:
_assert_envelope_spectrum_panel(ax, result, title, N)
plt.close(fig)


if __name__ == '__main__':
"""Run analyze_error_envelope_spectrum tests standalone"""
Expand Down
28 changes: 27 additions & 1 deletion python/tests/unit/aout/test_analyze_error_pdf.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,24 @@
output_dir.mkdir(exist_ok=True)


def _assert_pdf_panel(ax, result, title, n_samples):
assert ax.get_title() == title
assert ax.get_xlabel() == 'Error (LSB)'
assert ax.get_ylabel() == 'Probability Density'
assert len(ax.lines) == 2
assert [line.get_label() for line in ax.lines] == ['Actual PDF (KDE)', 'Gaussian Fit']
assert ax.get_legend() is not None
assert len(ax.texts) == 1

assert result['err_lsb'].shape == (n_samples,)
assert result['x'].shape == result['pdf'].shape == result['gauss_pdf'].shape == (200,)
assert np.all(np.isfinite(result['pdf']))
assert np.all(np.isfinite(result['gauss_pdf']))
assert np.isfinite(result['mu'])
assert result['sigma'] > 0
assert result['kl_divergence'] >= 0


def test_analyze_error_pdf_basic():
"""Test error PDF analysis for thermal noise and nonlinearity."""
# Setup
Expand Down Expand Up @@ -52,7 +70,6 @@ def test_analyze_error_pdf_basic():

fig_path = output_dir / 'test_analyze_error_pdf.png'
plt.savefig(fig_path, dpi=150, bbox_inches='tight')
plt.close()

print(f"\n[Save fig] -> [{fig_path.resolve()}]\n")

Expand All @@ -65,6 +82,15 @@ def test_analyze_error_pdf_basic():
assert 'sigma' in result1, "Result should contain sigma"
assert 'kl_divergence' in result1, "Result should contain kl_divergence"

assert len(fig.axes) == 3
for ax, result, title in [
(axes[0], result1, 'Thermal Noise'),
(axes[1], result2, 'Quantization Noise'),
(axes[2], result3, 'Static Nonlinearity'),
]:
_assert_pdf_panel(ax, result, title, N)
plt.close(fig)


if __name__ == '__main__':
"""Run analyze_error_pdf tests standalone"""
Expand Down
26 changes: 25 additions & 1 deletion python/tests/unit/aout/test_analyze_error_phase_plane.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,22 @@
output_dir.mkdir(exist_ok=True)


def _assert_error_phase_plane_panel(ax, result, title, n_samples, polynomial_order):
assert ax.get_title() == title
assert ax.get_xlabel() == 'Signal Amplitude (V)'
assert ax.get_ylabel() == 'Error / Residual (uV)'
assert len(ax.collections) >= 1
assert ax.collections[0].get_offsets().shape == (n_samples, 2)
assert len(ax.lines) >= 1
assert any(text.get_text().startswith('RMS:') for text in ax.texts)

assert result['residual'].shape == (n_samples,)
assert result['fitted_sine'].shape == (n_samples,)
assert result['trend_coeffs'].shape == (polynomial_order + 1,)
assert result['hysteresis_gap'] >= 0
assert np.all(np.isfinite(result['residual']))


def test_analyze_error_phase_plane_basic():
"""Test residual phase plane analysis for different ADC non-idealities."""
# Setup
Expand Down Expand Up @@ -69,7 +85,6 @@ def test_analyze_error_phase_plane_basic():

fig_path = output_dir / 'test_analyze_error_phase_plane.png'
plt.savefig(fig_path, dpi=150, bbox_inches='tight')
plt.close()

print(f"\n[Save fig] -> [{fig_path.resolve()}]\n")

Expand All @@ -81,6 +96,15 @@ def test_analyze_error_phase_plane_basic():
assert 'residual' in result1, "Result should contain residual"
assert 'fitted_sine' in result1, "Result should contain fitted_sine"

assert len(fig.axes) == 3
for ax, result, title in [
(axes[0], result1, 'Thermal Noise'),
(axes[1], result2, 'Static HD2 (-80 dBc)'),
(axes[2], result3, 'Static HD3 (-70 dBc)'),
]:
_assert_error_phase_plane_panel(ax, result, title, N, polynomial_order=3)
plt.close(fig)


if __name__ == '__main__':
"""Run analyze_error_phase_plane tests standalone"""
Expand Down
27 changes: 23 additions & 4 deletions python/tests/unit/aout/test_analyze_error_spectrum.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,17 @@
output_dir.mkdir(exist_ok=True)


def _assert_error_spectrum_panel(ax, result, title, n_samples):
assert ax.get_title() == title
assert ax.get_xlabel() == 'Frequency (Hz)'
assert ax.get_ylabel() == 'Error Spectrum (dB)'
assert len(ax.lines) >= 1
assert len(ax.lines[0].get_xdata()) > 0
assert len(ax.lines[0].get_ydata()) > 0
assert result['error_signal'].shape == (n_samples,)
assert np.all(np.isfinite(result['error_signal']))


def test_analyze_error_spectrum_basic():
"""Test error spectrum analysis for thermal noise and nonlinearity."""
# Setup
Expand All @@ -37,24 +48,32 @@ def test_analyze_error_spectrum_basic():
fig, axes = plt.subplots(1, 3, figsize=(18, 6))

# Analyze each case
analyze_error_spectrum(sig_thermal, fs=Fs, ax=axes[0], title='Thermal Noise')
analyze_error_spectrum(sig_jitter, fs=Fs, ax=axes[1], title='Jitter Noise')
analyze_error_spectrum(sig_nonlin, fs=Fs, ax=axes[2], title='Static HD3')
result1 = analyze_error_spectrum(sig_thermal, fs=Fs, ax=axes[0], title='Thermal Noise')
result2 = analyze_error_spectrum(sig_jitter, fs=Fs, ax=axes[1], title='Jitter Noise')
result3 = analyze_error_spectrum(sig_nonlin, fs=Fs, ax=axes[2], title='Static HD3')

fig.suptitle(f'Error Spectrum Analysis: ADC Non-idealities (Fs={Fs/1e6:.0f} MHz, Fin={Fin/1e6:.1f} MHz)',
fontsize=14, fontweight='bold')
plt.tight_layout()

fig_path = output_dir / 'test_analyze_error_spectrum.png'
plt.savefig(fig_path, dpi=150, bbox_inches='tight')
plt.close()

print(f"[Save fig] -> [{fig_path.resolve()}]\n")

# Verify file was created
assert fig_path.exists(), f"Figure file not created: {fig_path}"
assert fig_path.stat().st_size > 0, f"Figure file is empty: {fig_path}"

assert len(fig.axes) == 3
for ax, result, title in [
(axes[0], result1, 'Thermal Noise'),
(axes[1], result2, 'Jitter Noise'),
(axes[2], result3, 'Static HD3'),
]:
_assert_error_spectrum_panel(ax, result, title, N)
plt.close(fig)


if __name__ == '__main__':
"""Run analyze_error_spectrum tests standalone"""
Expand Down
25 changes: 24 additions & 1 deletion python/tests/unit/aout/test_analyze_phase_plane.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,21 @@
output_dir.mkdir(exist_ok=True)


def _scatter_point_count(ax):
return sum(collection.get_offsets().shape[0] for collection in ax.collections)


def _assert_phase_plane_panel(ax, result, title, n_samples):
lag = result['lag']
assert ax.get_title() == title
assert ax.get_xlabel() == 'x[n]'
assert ax.get_ylabel() == f'x[n+{lag}]'
assert lag > 0
assert result['outliers'].ndim == 1
assert _scatter_point_count(ax) == n_samples - lag
assert any(text.get_text().startswith('Lag:') for text in ax.texts)


def test_analyze_phase_plane_basic():
"""Test phase plane analysis for different ADC non-idealities."""
# Setup
Expand Down Expand Up @@ -56,7 +71,6 @@ def test_analyze_phase_plane_basic():

fig_path = output_dir / 'test_analyze_phase_plane.png'
plt.savefig(fig_path, dpi=150, bbox_inches='tight')
plt.close()

print(f"\n[Save fig] -> [{fig_path.resolve()}]\n")

Expand All @@ -68,6 +82,15 @@ def test_analyze_phase_plane_basic():
assert 'lag' in result1, "Result should contain lag"
assert 'outliers' in result1, "Result should contain outliers"

assert len(fig.axes) == 3
for ax, result, title in [
(axes[0], result1, 'Thermal Noise'),
(axes[1], result2, 'Glitch'),
(axes[2], result3, 'Quantization Noise'),
]:
_assert_phase_plane_panel(ax, result, title, N)
plt.close(fig)


if __name__ == '__main__':
"""Run analyze_phase_plane tests standalone"""
Expand Down
Loading