diff --git a/adc_eval/adcs/basic.py b/adc_eval/adcs/basic.py index eb4810d..c9235af 100644 --- a/adc_eval/adcs/basic.py +++ b/adc_eval/adcs/basic.py @@ -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): @@ -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): diff --git a/adc_eval/adcs/sar.py b/adc_eval/adcs/sar.py new file mode 100644 index 0000000..3d13567 --- /dev/null +++ b/adc_eval/adcs/sar.py @@ -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)) diff --git a/adc_eval/signals.py b/adc_eval/signals.py index 4c72f74..2d93d2c 100644 --- a/adc_eval/signals.py +++ b/adc_eval/signals.py @@ -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) diff --git a/examples/basic_adc_simulation.py b/examples/basic_adc_simulation.py index d5663c4..a82abb6 100644 --- a/examples/basic_adc_simulation.py +++ b/examples/basic_adc_simulation.py @@ -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 diff --git a/pylintrc b/pylintrc index e937446..e981f97 100644 --- a/pylintrc +++ b/pylintrc @@ -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, diff --git a/pyproject.toml b/pyproject.toml index 56bb465..26d7e19 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 diff --git a/tests/test_signals.py b/tests/test_signals.py index c9ccaa3..9464dc1 100644 --- a/tests/test_signals.py +++ b/tests/test_signals.py @@ -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): @@ -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() \ No newline at end of file