Skip to content
Merged
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
4 changes: 2 additions & 2 deletions adc_eval/adcs/basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ def offset(self):
@offset.setter
def offset(self, values):
"""Set offset mean and stdev."""
self.err["offset"] = values[0] + values[1] * np.random.randn(1)
self.err["offset"] = np.random.normal(values[0], values[1])

@property
def gain_error(self):
Expand All @@ -121,7 +121,7 @@ def gain_error(self):
@gain_error.setter
def gain_error(self, values):
"""Set gain error mean and stdev."""
self.err["gain"] = values[0] + values[1] * np.random.randn(1)
self.err["gain"] = np.random.normal(values[0], values[1])

@property
def distortion(self):
Expand Down
128 changes: 128 additions & 0 deletions adc_eval/adcs/sar.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
"""SAR ADC models"""

import numpy as np
from adc_eval.adcs.basic import ADC


class SAR(ADC):
"""
SAR ADC Class.

Parameters
----------
nbits : int, optional
Number of bits for the ADC. The default is 8.
fs : float, optional
Sample rate for the ADC in Hz. The default is 1Hz.
vref : float, optional
Reference level of the ADC in Volts ([0, +vref] conversion range). The default is 1.
seed : int, optional
Seed for random variable generation. The default is 1.
**kwargs
Extra arguments.
weights : list, optional
List of weights for SAR capacitors. Must be >= nbits. Defaults to binary weights.
MSB weight should be in index 0.

Attributes
-------
vin : float
Sets or returns the current input voltage level. Assumed +/-vref/2 input
vlsb : float
LSB voltage of the converter. vref/2^nbits
noise : float, default=0
Sets or returns the stdev of the noise generated by the converter.
weights : list
Sets or returns the capacitor weighting of the array. Default is binary weighting.
mismatch : float
Sets or returns the stdev of the mismatch of the converter. Default is no mismatch.
comp_noise : float
Sets or returns the stdev of the comparator noise. Default is no noise.
offset : tuple of float
Sets the (mean, stdev) of the offset of the converter. Default is no offset.
gain_error : tuple of float
Sets the (mean, stdev) of the gain error of the converter. Default is no gain error.
distortion : list of float
Sets the harmonic distortion values with index=0 corresponding to HD1.
Example: For unity gain and only -30dB of HD3, input is [1, 0, 0.032]
dout : int
Digital output code for current vin value.

Methods
-------
run_step

"""

def __init__(self, nbits=8, fs=1, vref=1, seed=1, **kwargs):
"""Initialization function for Generic ADC."""
super().__init__(nbits, fs, vref, seed)

self._mismatch = None
self._comp_noise = 0

# Get keyword arguments
self._weights = kwargs.get("weights", None)

@property
def weights(self):
"""Returns capacitor unit weights."""
if self._weights is None:
self._weights = np.flip(2 ** np.linspace(0, self.nbits - 1, self.nbits))
return np.array(self._weights)

@weights.setter
def weights(self, values):
"""Sets the capacitor unit weights."""
self._weights = np.array(values)
if self._weights.size < self.nbits:
print(
f"WARNING: Capacitor weight array size is {self._weights.size} for {self.nbits}-bit ADC."
)
self.mismatch = self.err["mismatch"]

@property
def mismatch(self):
"""Return noise stdev."""
if self._mismatch is None:
self._mismatch = np.zeros(self.weights.size)
return self._mismatch

@mismatch.setter
def mismatch(self, stdev):
"""Sets mismatch stdev."""
self.err["mismatch"] = stdev
self._mismatch = np.random.normal(0, stdev, self.weights.size)
self._mismatch /= np.sqrt(self.weights)

@property
def comp_noise(self):
"""Returns the noise of the comparator."""
return self._comp_noise

@comp_noise.setter
def comp_noise(self, value):
"""Sets the noise of the comparator."""
self._comp_noise = value

def run_step(self):
"""Run a single ADC step."""
vinx = self.vin

cweights = self.weights * (1 + self.mismatch)
cdenom = sum(cweights) + 1

comp_noise = np.random.normal(0, self.comp_noise, cweights.size)

# Bit cycling
vdac = vinx
for n, _ in enumerate(cweights):
vcomp = vdac - self.vref / 2 + comp_noise[n]
compout = vcomp * 1e6
compout = -1 if compout <= 0 else 1
self.dbits[n] = max(0, compout)
vdac -= compout * self.vref / 2 * cweights[n] / cdenom

# Re-scale the data
scalar = 2**self.nbits / cdenom
self.dval = min(2**self.nbits - 1, scalar * sum(self.weights * self.dbits))
44 changes: 44 additions & 0 deletions adc_eval/signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,3 +89,47 @@ def impulse(nlen, mag=1):
data = np.zeros(nlen)
data[0] = mag
return data


def tones(nlen, bins, amps, offset=0, fs=1, nfft=None, phases=None):
"""
Generate a time-series of multiple tones.

Parameters
----------
nlen : int
Length of time-series array.
bins : list
List of signal bins to generate tones for.
amps : list
List of amplitudes for given bins.
offset : int, optional
Offset to apply to each signal (globally applied).
fs : float, optional
Sample rate of the signal in Hz. The default is 1Hz.
nfft : int, optional
Number of FFT samples, if different than length of signal. The default is None.
phases : list, optional
List of phase shifts for each bin. The default is None.

Returns
-------
tuple of ndarray
(time, signal)
Time-series and associated tone array.
"""
t = time(nlen, fs=fs)

signal = np.zeros(nlen)
if phases is None:
phases = np.zeros(nlen)
if nfft is None:
nfft = nlen

fbin = fs / nfft
for index, nbin in enumerate(bins):
signal += sin(
t, amp=amps[index], offset=offset, freq=nbin * fbin, ph0=phases[index]
)

return (t, signal)
8 changes: 3 additions & 5 deletions examples/basic_adc_simulation.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,15 @@
NLEN = 2**16 # Larger than NFFT to enable Bartlett method for PSD
NFFT = 2**12
vref = 1
fin_bin = NFFT / 4 - 31
fin = fin_bin * FS/NFFT
fbin = NFFT / 4 - 31
ftone = NFFT / 2 - 15
vin_amp = 0.707 * vref / 2


"""
VIN Generation
"""
t = signals.time(NLEN, FS)
vin = signals.sin(t, amp=vin_amp, offset=0, freq=fin)
vin += signals.sin(t, amp=vin_amp*0.2, offset=0, freq=(NFFT/2-15)*FS/NFFT) # Adds tone to show intermodulation
(t, vin) = signals.tones(NLEN, [fbin, ftone], [vin_amp, vin_amp*0.2], offset=0, fs=FS, nfft=NFFT)

"""
ADC Architecture Creation
Expand Down
1 change: 1 addition & 0 deletions pylintrc
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ disable=
duplicate-code,
implicit-str-concat,
too-many-arguments,
too-many-positional-arguments,
too-many-branches,
too-many-instance-attributes,
too-many-locals,
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ ignore = [
"PLR0912", # Too many branches ({branches} > {max_branches})
"PLR0913", # Too many arguments to function call ({c_args} > {max_args})
"PLR0915", # Too many statements ({statements} > {max_statements})
"PLR0917", # Too many positional arguments
"PLR2004", # Magic value used in comparison, consider replacing {value} with a constant variable
"PLW2901", # Outer {outer_kind} variable {name} overwritten by inner {inner_kind} target
"T201", # Allow print statements
Expand Down
36 changes: 35 additions & 1 deletion tests/test_signals.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@

RTOL = 0.01


@pytest.mark.parametrize("nlen", np.random.randint(4, 2**16, 3))
@pytest.mark.parametrize("fs", np.random.uniform(1, 1e9, 3))
def test_time(nlen, fs):
Expand Down Expand Up @@ -77,3 +76,38 @@ def test_impulse(nlen, mag):
assert data.size == nlen
assert data[0] == mag
assert data[1:].all() == 0


@pytest.mark.parametrize("nlen", np.random.randint(2, 2**12, 3))
def test_tones_no_nfft_arg(nlen):
"""Test tone generation with random length no nfft param."""
(t, data) = signals.tones(nlen, [0.5], [0.5])

assert t.size == nlen
assert t[0] == 0
assert t[-1] == nlen-1
assert data.size == nlen


@pytest.mark.parametrize("fs", np.random.uniform(100, 1e9, 3))
@pytest.mark.parametrize("nlen", np.random.randint(2, 2**12, 3))
def test_tones_with_fs_arg(fs, nlen):
"""Test tone generation with random length and fs given."""
(t, data) = signals.tones(nlen, [0.5], [0.5], fs=fs)

assert t.size == nlen
assert t[0] == 0
assert np.isclose(t[-1], (nlen-1) / fs, rtol=RTOL)
assert data.size == nlen


@pytest.mark.parametrize("nlen", np.random.randint(2, 2**12, 3))
def test_tones_with_empty_list( nlen):
"""Test tone generation with random length and fs given."""
(t, data) = signals.tones(nlen, [], [])

assert t.size == nlen
assert t[0] == 0
assert t[-1] == nlen-1
assert data.size == nlen
assert data.all() == np.zeros(nlen).all()