Skip to content

Commit 1a79a6c

Browse files
committed
Moved files and tests around for better org
1 parent 5509822 commit 1a79a6c

File tree

6 files changed

+428
-434
lines changed

6 files changed

+428
-434
lines changed

adc_eval/eval/calc.py

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
"""Spectral analysis helper module."""
2+
import numpy as np
3+
4+
5+
def db_to_pow(value, places=3):
6+
"""Convert dBW to W."""
7+
if isinstance(value, np.ndarray):
8+
return np.round(10 ** (0.1 * value), places)
9+
return round(10 ** (0.1 * value), places)
10+
11+
12+
def dBW(value, places=1):
13+
"""Convert to dBW."""
14+
if isinstance(value, np.ndarray):
15+
return np.round(10 * np.log10(value), places)
16+
return round(10 * np.log10(value), places)
17+
18+
19+
def enob(sndr, places=1):
20+
"""Return ENOB for given SNDR."""
21+
return round((sndr - 1.76) / 6.02, places)
22+
23+
24+
def sndr_sfdr(spectrum, freq, fs, nfft, leak, full_scale=0):
25+
"""Get SNDR and SFDR."""
26+
27+
# Zero the DC bin
28+
for i in range(0, leak + 1):
29+
spectrum[i] = 0
30+
bin_sig = np.argmax(spectrum)
31+
psig = sum(spectrum[i] for i in range(bin_sig - leak, bin_sig + leak + 1))
32+
spectrum_n = spectrum.copy()
33+
spectrum_n[bin_sig] = 0
34+
fbin = fs / nfft
35+
36+
for i in range(bin_sig - leak, bin_sig + leak + 1):
37+
spectrum_n[i] = 0
38+
39+
bin_spur = np.argmax(spectrum_n)
40+
pspur = spectrum[bin_spur]
41+
42+
noise_power = sum(spectrum_n)
43+
noise_floor = 2 * noise_power / nfft
44+
45+
stats = {}
46+
47+
stats["sig"] = {
48+
"freq": freq[bin_sig],
49+
"bin": bin_sig,
50+
"power": psig,
51+
"dB": dBW(psig),
52+
"dBFS": round(dBW(psig) - full_scale, 1),
53+
}
54+
55+
stats["spur"] = {
56+
"freq": freq[bin_spur],
57+
"bin": bin_spur,
58+
"power": pspur,
59+
"dB": dBW(pspur),
60+
"dBFS": round(dBW(pspur) - full_scale, 1),
61+
}
62+
stats["noise"] = {
63+
"floor": noise_floor,
64+
"power": noise_power,
65+
"rms": np.sqrt(noise_power),
66+
"dBHz": round(dBW(noise_floor, 3) - full_scale, 1),
67+
"NSD": round(dBW(noise_floor, 3) - full_scale - 2 * dBW(fbin, 3), 1),
68+
}
69+
stats["sndr"] = {
70+
"dBc": dBW(psig / noise_power),
71+
"dBFS": round(full_scale - dBW(noise_power), 1),
72+
}
73+
stats["sfdr"] = {
74+
"dBc": dBW(psig / pspur),
75+
"dBFS": round(full_scale - dBW(pspur), 1),
76+
}
77+
stats["enob"] = {"bits": enob(stats["sndr"]["dBFS"])}
78+
79+
return stats
80+
81+
82+
def find_harmonics(spectrum, freq, nfft, bin_sig, psig, harms=5, leak=20, fscale=1e6):
83+
"""Get the harmonic contents of the data."""
84+
harm_stats = {"harm": {}}
85+
harm_index = 2
86+
for harm in bin_sig * np.arange(2, harms + 1):
87+
harm_stats["harm"][harm_index] = {}
88+
zone = np.floor(harm / (nfft / 2)) + 1
89+
if zone % 2 == 0:
90+
bin_harm = int(nfft / 2 - (harm - (zone - 1) * nfft / 2))
91+
else:
92+
bin_harm = int(harm - (zone - 1) * nfft / 2)
93+
94+
# Make sure we pick the max bin where power is maximized; due to spectral leakage
95+
# if bin_harm == nfft/2, set to bin of 0
96+
if bin_harm == nfft / 2:
97+
bin_harm = 0
98+
pwr_max = spectrum[bin_harm]
99+
bin_harm_max = bin_harm
100+
for i in range(bin_harm - leak, bin_harm + leak + 1):
101+
try:
102+
pwr = spectrum[i]
103+
if pwr > pwr_max:
104+
bin_harm_max = i
105+
pwr_max = pwr
106+
except IndexError:
107+
# bin + leakage out of bounds, so stop looking
108+
break
109+
110+
harm_stats["harm"][harm_index]["bin"] = bin_harm_max
111+
harm_stats["harm"][harm_index]["power"] = pwr_max
112+
harm_stats["harm"][harm_index]["freq"] = round(freq[bin_harm] / fscale, 1)
113+
harm_stats["harm"][harm_index]["dBc"] = dBW(pwr_max / psig)
114+
harm_stats["harm"][harm_index]["dB"] = dBW(pwr_max)
115+
116+
harm_index = harm_index + 1
117+
118+
return harm_stats
119+
120+
121+
def get_plot_string(stats, full_scale, fs, nfft, window, xscale=1e6, fscale="MHz"):
122+
"""Generate plot string from stats dict."""
123+
124+
plt_str = "==== FFT ====\n"
125+
plt_str += f"NFFT = {nfft}\n"
126+
plt_str += f"fbin = {round(fs/nfft / 1e3, 2)} kHz\n"
127+
plt_str += f"window = {window}\n"
128+
plt_str += "\n"
129+
plt_str += "==== Signal ====\n"
130+
plt_str += f"FullScale = {full_scale} dB\n"
131+
plt_str += f"Psig = {stats['sig']['dBFS']} dBFS ({stats['sig']['dB']} dB)\n"
132+
plt_str += f"fsig = {round(stats['sig']['freq']/xscale, 2)} {fscale}\n"
133+
plt_str += f"fsamp = {round(fs/xscale, 2)} {fscale}\n"
134+
plt_str += "\n"
135+
plt_str += "==== SNDR/SFDR ====\n"
136+
plt_str += f"ENOB = {stats['enob']['bits']} bits\n"
137+
plt_str += f"SNDR = {stats['sndr']['dBFS']} dBFS ({stats['sndr']['dBc']} dBc)\n"
138+
plt_str += f"SFDR = {stats['sfdr']['dBFS']} dBFS ({stats['sfdr']['dBc']} dBc)\n"
139+
plt_str += f"Pspur = {stats['spur']['dBFS']} dBFS\n"
140+
plt_str += f"fspur = {round(stats['spur']['freq']/xscale, 2)} {fscale}\n"
141+
plt_str += f"Noise Floor = {stats['noise']['dBHz']} dBFS\n"
142+
plt_str += f"NSD = {stats['noise']['NSD']} dBFS\n"
143+
plt_str += "\n"
144+
plt_str += "==== Harmonics ====\n"
145+
146+
for hindex, hdata in stats["harm"].items():
147+
plt_str += f"HD{hindex} = {round(hdata['dB'] - full_scale, 1)} dBFS @ {hdata['freq']} {fscale}\n"
148+
149+
plt_str += "\n"
150+
151+
return plt_str

adc_eval/eval/spectrum.py

Lines changed: 9 additions & 157 deletions
Original file line numberDiff line numberDiff line change
@@ -2,122 +2,7 @@
22

33
import numpy as np
44
import matplotlib.pyplot as plt
5-
6-
7-
def db_to_pow(value, places=3):
8-
"""Convert dBW to W."""
9-
if isinstance(value, np.ndarray):
10-
return round(10 ** (0.1 * value), places)
11-
return round(10 ** (0.1 * value), places)
12-
13-
14-
def dBW(value, places=1):
15-
"""Convert to dBW."""
16-
if isinstance(value, np.ndarray):
17-
return round(10 * np.log10(value), places)
18-
return round(10 * np.log10(value), places)
19-
20-
21-
def enob(sndr, places=1):
22-
"""Return ENOB for given SNDR."""
23-
return round((sndr - 1.76) / 6.02, places)
24-
25-
26-
def sndr_sfdr(spectrum, freq, fs, nfft, leak, full_scale=0):
27-
"""Get SNDR and SFDR."""
28-
29-
# Zero the DC bin
30-
for i in range(0, leak + 1):
31-
spectrum[i] = 0
32-
bin_sig = np.argmax(spectrum)
33-
psig = sum(spectrum[i] for i in range(bin_sig - leak, bin_sig + leak + 1))
34-
spectrum_n = spectrum.copy()
35-
spectrum_n[bin_sig] = 0
36-
fbin = fs / nfft
37-
38-
for i in range(bin_sig - leak, bin_sig + leak + 1):
39-
spectrum_n[i] = 0
40-
41-
bin_spur = np.argmax(spectrum_n)
42-
pspur = spectrum[bin_spur]
43-
44-
noise_power = sum(spectrum_n)
45-
noise_floor = 2 * noise_power / nfft
46-
47-
stats = {}
48-
49-
stats["sig"] = {
50-
"freq": freq[bin_sig],
51-
"bin": bin_sig,
52-
"power": psig,
53-
"dB": dBW(psig),
54-
"dBFS": round(dBW(psig) - full_scale, 1),
55-
}
56-
57-
stats["spur"] = {
58-
"freq": freq[bin_spur],
59-
"bin": bin_spur,
60-
"power": pspur,
61-
"dB": dBW(pspur),
62-
"dBFS": round(dBW(pspur) - full_scale, 1),
63-
}
64-
stats["noise"] = {
65-
"floor": noise_floor,
66-
"power": noise_power,
67-
"rms": np.sqrt(noise_power),
68-
"dBHz": round(dBW(noise_floor, 3) - full_scale, 1),
69-
"NSD": round(dBW(noise_floor, 3) - full_scale - 2 * dBW(fbin, 3), 1),
70-
}
71-
stats["sndr"] = {
72-
"dBc": dBW(psig / noise_power),
73-
"dBFS": round(full_scale - dBW(noise_power), 1),
74-
}
75-
stats["sfdr"] = {
76-
"dBc": dBW(psig / pspur),
77-
"dBFS": round(full_scale - dBW(pspur), 1),
78-
}
79-
stats["enob"] = {"bits": enob(stats["sndr"]["dBFS"])}
80-
81-
return stats
82-
83-
84-
def find_harmonics(spectrum, freq, nfft, bin_sig, psig, harms=5, leak=20, fscale=1e6):
85-
"""Get the harmonic contents of the data."""
86-
harm_stats = {"harm": {}}
87-
harm_index = 2
88-
for harm in bin_sig * np.arange(2, harms + 1):
89-
harm_stats["harm"][harm_index] = {}
90-
zone = np.floor(harm / (nfft / 2)) + 1
91-
if zone % 2 == 0:
92-
bin_harm = int(nfft / 2 - (harm - (zone - 1) * nfft / 2))
93-
else:
94-
bin_harm = int(harm - (zone - 1) * nfft / 2)
95-
96-
# Make sure we pick the max bin where power is maximized; due to spectral leakage
97-
# if bin_harm == nfft/2, set to bin of 0
98-
if bin_harm == nfft / 2:
99-
bin_harm = 0
100-
pwr_max = spectrum[bin_harm]
101-
bin_harm_max = bin_harm
102-
for i in range(bin_harm - leak, bin_harm + leak + 1):
103-
try:
104-
pwr = spectrum[i]
105-
if pwr > pwr_max:
106-
bin_harm_max = i
107-
pwr_max = pwr
108-
except IndexError:
109-
# bin + leakage out of bounds, so stop looking
110-
break
111-
112-
harm_stats["harm"][harm_index]["bin"] = bin_harm_max
113-
harm_stats["harm"][harm_index]["power"] = pwr_max
114-
harm_stats["harm"][harm_index]["freq"] = round(freq[bin_harm] / fscale, 1)
115-
harm_stats["harm"][harm_index]["dBc"] = dBW(pwr_max / psig)
116-
harm_stats["harm"][harm_index]["dB"] = dBW(pwr_max)
117-
118-
harm_index = harm_index + 1
119-
120-
return harm_stats
5+
from adc_eval.eval import calc
1216

1227

1238
def calc_psd(data, fs, nfft=2**12):
@@ -197,12 +82,12 @@ def plot_spectrum(
19782
(freq, pwr) = get_spectrum(
19883
data * windows[window] * wscale, fs=fs, nfft=nfft, single_sided=single_sided
19984
)
200-
full_scale = dBW(dr**2 / 8)
85+
full_scale = calc.dBW(dr**2 / 8)
20186

20287
yaxis_lut = {
20388
"power": [0, "dB"],
204-
"fullscale": [dBW(dr**2 / 8), "dBFS"],
205-
"normalize": [max(dBW(pwr)), "dB Normalized"],
89+
"fullscale": [full_scale, "dBFS"],
90+
"normalize": [max(calc.dBW(pwr)), "dB Normalized"],
20691
"magnitude": [0, "W"],
20792
}
20893

@@ -217,7 +102,7 @@ def plot_spectrum(
217102
)
218103
print(" Defaulting to Hz.")
219104

220-
psd_out = 10 * np.log10(pwr) - scalar
105+
psd_out = calc.dBW(pwr, places=3) - scalar
221106
if lut_key in ["magnitude"]:
222107
psd_out = pwr
223108

@@ -229,9 +114,9 @@ def plot_spectrum(
229114
data * windows[window] * wscale, fs=fs, nfft=nfft, single_sided=True
230115
)
231116

232-
sndr_stats = sndr_sfdr(psd_ss, f_ss, fs, nfft, leak=leak, full_scale=full_scale)
117+
sndr_stats = calc.sndr_sfdr(psd_ss, f_ss, fs, nfft, leak=leak, full_scale=full_scale)
233118

234-
harm_stats = find_harmonics(
119+
harm_stats = calc.find_harmonics(
235120
psd_ss,
236121
f_ss,
237122
nfft,
@@ -247,7 +132,7 @@ def plot_spectrum(
247132
xmin = 0 if single_sided else -fs / 2e6
248133

249134
if not no_plot:
250-
plt_str = get_plot_string(stats, full_scale, fs, nfft, window, xscale, fscale)
135+
plt_str = calc.get_plot_string(stats, full_scale, fs, nfft, window, xscale, fscale)
251136
fig, ax = plt.subplots(figsize=(15, 8))
252137
ax.plot(freq / xscale, psd_out)
253138
ax.set_ylabel(f"Power Spectrum ({yunits})", fontsize=18)
@@ -316,39 +201,6 @@ def plot_spectrum(
316201
return (freq, psd_out, stats)
317202

318203

319-
def get_plot_string(stats, full_scale, fs, nfft, window, xscale=1e6, fscale="MHz"):
320-
"""Generate plot string from stats dict."""
321-
322-
plt_str = "==== FFT ====\n"
323-
plt_str += f"NFFT = {nfft}\n"
324-
plt_str += f"fbin = {round(fs/nfft / 1e3, 2)} kHz\n"
325-
plt_str += f"window = {window}\n"
326-
plt_str += "\n"
327-
plt_str += "==== Signal ====\n"
328-
plt_str += f"FullScale = {full_scale} dB\n"
329-
plt_str += f"Psig = {stats['sig']['dBFS']} dBFS ({stats['sig']['dB']} dB)\n"
330-
plt_str += f"fsig = {round(stats['sig']['freq']/xscale, 2)} {fscale}\n"
331-
plt_str += f"fsamp = {round(fs/xscale, 2)} {fscale}\n"
332-
plt_str += "\n"
333-
plt_str += "==== SNDR/SFDR ====\n"
334-
plt_str += f"ENOB = {stats['enob']['bits']} bits\n"
335-
plt_str += f"SNDR = {stats['sndr']['dBFS']} dBFS ({stats['sndr']['dBc']} dBc)\n"
336-
plt_str += f"SFDR = {stats['sfdr']['dBFS']} dBFS ({stats['sfdr']['dBc']} dBc)\n"
337-
plt_str += f"Pspur = {stats['spur']['dBFS']} dBFS\n"
338-
plt_str += f"fspur = {round(stats['spur']['freq']/xscale, 2)} {fscale}\n"
339-
plt_str += f"Noise Floor = {stats['noise']['dBHz']} dBFS\n"
340-
plt_str += f"NSD = {stats['noise']['NSD']} dBFS\n"
341-
plt_str += "\n"
342-
plt_str += "==== Harmonics ====\n"
343-
344-
for hindex, hdata in stats["harm"].items():
345-
plt_str += f"HD{hindex} = {round(hdata['dB'] - full_scale, 1)} dBFS @ {hdata['freq']} {fscale}\n"
346-
347-
plt_str += "\n"
348-
349-
return plt_str
350-
351-
352204
def analyze(
353205
data,
354206
nfft,
@@ -377,4 +229,4 @@ def analyze(
377229
fscale=fscale,
378230
)
379231

380-
return (freq, spectrum, stats)
232+
return (freq, spectrum, stats)

examples/basic_adc_simulation.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@
6565
no_plot=False,
6666
yaxis="fullscale",
6767
single_sided=True,
68+
fscale="MHz"
6869
)
6970
ax = plt.gca()
7071
ax.set_title("ADC Spectrum")

0 commit comments

Comments
 (0)