vhdl-sincos-gen/tools/eval_sine_quality.py

154 lines
3.8 KiB
Python
Raw Permalink Normal View History

#!/usr/bin/python
"""
Evaluate quality of generated sine/cosine waveform.
Reads output file from VHDL testbench and reports
key figures for the quality of the sine wave.
Usage:
python eval_sine_quality.py datafile.dat
"""
from __future__ import print_function
import sys
import numpy
def read_data(fname):
n = 0
data = numpy.zeros((1024, 2), dtype=numpy.int64)
with open(fname, 'r') as f:
for s in f:
if n == data.shape[0]:
data = numpy.resize(data, (2*n, 2))
w = s.split()
assert len(w) == 2
if len(w) != 2:
raise ValueError("Expecting two columns in file")
data[n,0] = int(w[0])
data[n,1] = int(w[1])
n += 1
return data[:n,:]
def eval_sine_quality(data):
assert len(data.shape) == 2
assert data.shape[1] == 2
n = data.shape[0]
assert n >= 4 and n % 4 == 0
# Extract sine and cosine colums.
dsin = numpy.copy(data[:,0])
dcos = data[:,1]
del data
# Check that cosine is an exact phase-shifted version of sine.
dcos[0:3*n//4] -= dsin[n//4:]
dcos[3*n//4:] -= dsin[0:n//4]
tmin = numpy.amin(dcos)
tmax = numpy.amax(dcos)
del dcos
if tmin == 0 and tmax == 0:
print('cos(x) == sin(x+pi/2) exactly')
else:
print('cos(x) == sin(x+pi/2) + (%d .. %d)' % (tmin, tmax))
# Check that 180 degree phase shift is equivalent to sign negation.
t = dsin[0:n//2] + dsin[n//2:]
tmin = numpy.amin(t)
tmax = numpy.amax(t)
del t
if tmin == 0 and tmax == 0:
print('sin(x) == - sin(x+pi) exactly')
else:
print('sin(x) == - sin(x+pi) + (%d .. %d)' % (tmin, tmax))
print()
# Determine offset (mean value of waveform).
offs = numpy.mean(dsin)
print('offset = %20.12f lsb' % offs)
# Determine amplitude and phase.
tref = numpy.sin(2 * numpy.pi / n * numpy.arange(n))
asin = numpy.sum(dsin * tref) * 2.0 / n
del tref
tref = numpy.cos(2 * numpy.pi / n * numpy.arange(n))
acos = numpy.sum(dsin * tref) * 2.0 / n
del tref
ampl = numpy.sqrt(asin**2 + acos**2)
phase = numpy.arctan2(acos, asin)
print('amplitude = %20.12f lsb' % ampl)
print('phase offset = %20.12f rad' % phase)
print()
# Determine peak and rms deviation.
tref = ampl * numpy.sin(2 * numpy.pi / n * numpy.arange(n))
terr = dsin - tref
del tref
peakerr = numpy.amax(numpy.abs(terr))
rmserr = numpy.std(terr)
del terr
print('peak error = %20.12f lsb' % peakerr)
print('rms error = %20.12f lsb rms' % rmserr)
# Calculate SNR and effective number of bits.
sinad = 20 * numpy.log10(ampl * numpy.sqrt(0.5) / rmserr)
print('SINAD = %12.4f dB' % sinad)
print('ENOB = %12.4f bits' % ((sinad - 1.76) / 6.02))
# Determine spurious-free dynamic range.
q = numpy.fft.rfft(dsin)
tampl = numpy.abs(q[1])
tspur = numpy.amax(numpy.abs(q[2:]))
del q
sfdr = 20 * numpy.log10(tampl / tspur)
print('SFDR = %12.4f dB' % sfdr)
print()
def main():
if len(sys.argv) != 2:
print(__doc__, file=sys.stderr)
print("ERROR: Invalid/missing command line arguments", file=sys.stderr)
sys.exit(1)
fname = sys.argv[1]
# Read data from file.
print("reading", fname, "...")
data = read_data(fname)
print("got array", data.shape)
print()
# Check array shape.
if len(data.shape) != 2 or data.shape[1] != 2:
print("ERROR: Expected array of shape (N, 2)", file=sys.stderr)
sys.exit(1)
# Check number of samples.
if data.shape[0] < 4 or (data.shape[0] & (data.shape[0] - 1)) != 0:
print("ERROR: Expected power-of-two record length", file=sys.stderr)
sys.exit(1)
eval_sine_quality(data)
if __name__ == '__main__':
main()