Source code for denspp.offline.analog.adc.adc_basic

import numpy as np
from fractions import Fraction
from scipy.signal import square, resample_poly

from .adc_settings import SettingsADC
from denspp.offline.analog.common_func import CommonAnalogFunctions, CommonDigitalFunctions
from denspp.offline.analog.dev_noise import SettingsNoise, RecommendedSettingsNoise, ProcessNoise


[docs] class BasicADC(CommonAnalogFunctions, CommonDigitalFunctions): _settings: SettingsADC _handler_noise: ProcessNoise def __init__(self, settings_dev: SettingsADC, settings_noise: SettingsNoise = RecommendedSettingsNoise): """Basic class for applying an Analogue-Digital-Converter (ADC) on the raw data :param settings_dev: Configuration class for defining properties of ADC :param settings_noise: Configuration class for defining noise properties of device """ super().__init__() self.define_voltage_range(volt_low=settings_dev.vref[1], volt_hgh=settings_dev.vref[0]) self.define_limits(bit_signed=settings_dev.is_signed, total_bitwidth=settings_dev.Nadc, frac_bitwidth=0) self._handler_noise = ProcessNoise(settings_noise, settings_dev.fs_ana) self._settings = settings_dev # --- Internal characteristic self.noise_eff_out = 0.0 self.__dvrange = self._settings.vref[0] - self._settings.vref[1] # --- Resampling stuff (self.__p_ratio, self.__q_ratio) = ( Fraction(self._settings.fs_adc / self._settings.fs_ana) .limit_denominator(100) .as_integer_ratio() ) # --- Internal voltage values self.__input_snh = 0.0 @property def snr_ideal(self) -> float: """Getting the ideal Signal-to-Noise ratio""" return 10 * np.log10(4) * self._settings.Nadc + 10 * np.log10(3 / 2) def __do_snh_sample(self, uin: np.ndarray, do: bool | np.ndarray) -> np.ndarray: """Performing sample-and-hold (S&H) stage for buffering input value""" uout = uin if do: uout = self.__input_snh self.__input_snh = uin return uout
[docs] def do_snh_stream(self, uin: np.ndarray, f_snh: float) -> np.ndarray: """Performing sample-and-hold (S&H) stage for buffering input value""" t = np.arange(0, uin.size, 1) / self._settings.fs_ana clk_fsh = square(2 * np.pi * t * f_snh, duty=0.5) do_snh = np.where(np.diff(clk_fsh) >= 0.5) do_snh += 1 uout = np.zeros(shape=uin.shape) for idx, do_snh in enumerate(do_snh): uout[idx] = self.__do_snh_sample(uin[idx], do_snh) return uout
def _do_resample(self, uin: np.ndarray) -> np.ndarray: """Do resampling of input values""" if uin.size == 1: uout = uin else: uout = uin[0] + resample_poly(uin - uin[0], self.__p_ratio, self.__q_ratio) return uout def _gen_noise(self, size: int) -> np.ndarray: """Generate the transient input noise of the amplifier""" unoise = self._handler_noise.gen_noise_awgn_pwr( size = size, e_n=-self.snr_ideal ) return unoise
[docs] def adc_ideal(self, uin: np.ndarray) -> [np.ndarray, np.ndarray, np.ndarray]: """Using the ideal ADC Args: uin: Input voltage Returns: Tuple with three numpy arrays [x_out = Output digital value, u_out = Output digitized voltage, uerr = Quantization error] """ # Pre-Processing uin_adc = self.clamp_voltage(uin) uin0 = self._do_resample(uin_adc) uin0 += self._gen_noise(uin0.size) # ADC conversion xout = np.floor((uin0 - self._settings.vcm) / self._settings.lsb) xout = self.clamp_digital(xout) uout = self._settings.vref[1] + xout * self._settings.lsb # Calculating quantization error uerr = uin0 - uout return xout, uout, uerr
def _do_downsample(self, uin: np.ndarray) -> np.ndarray: """Performing a simple downsampling of the adc data stream""" (p_ratio, q_ratio) = ( Fraction(self._settings.fs_dig / self._settings.fs_adc) .limit_denominator(100) .as_integer_ratio() ) uout = uin[0] + resample_poly(uin - uin[0], p_ratio, q_ratio) return uout
[docs] def do_cic(self, uin: np.ndarray, num_stages: int=5) -> np.ndarray: """Performing the CIC filter at the output of oversampled ADC""" output_transient = list() gain = (self._settings.osr * 1) ** num_stages class integrator: def __init__(self): self.yn = 0 self.ynm = 0 def update(self, inp): self.ynm = self.yn self.yn = (self.ynm + inp) return (self.yn) class comb: def __init__(self): self.xn = 0 self.xnm = 0 def update(self, inp): self.xnm = self.xn self.xn = inp return (self.xn - self.xnm) ## Generate Integrator and Comb lists (Python list of objects) intes = [integrator() for a in range(num_stages)] combs = [comb() for a in range(num_stages)] ## Performing Decimation CIC Filter for (s, v) in enumerate(uin): z = v for i in range(num_stages): z = intes[i].update(z) if (s % self._settings.osr) == 0: # decimate is done here for c in range(num_stages): z = combs[c].update(z) j = z output_transient.append(j / gain) # normalise the gain return np.array(output_transient)
@staticmethod def _generate_sar_empty_data(shape) -> tuple[np.ndarray, np.ndarray, np.ndarray]: uout = np.zeros(shape=shape, dtype=np.float32) xout = np.zeros(shape=shape, dtype=np.int16) uerr = np.zeros(shape=shape, dtype=np.float32) return xout, uout, uerr @staticmethod def _generate_dsigma_empty_data(shape) -> tuple[np.ndarray, np.ndarray]: xout_hs = np.zeros(shape=shape, dtype=np.int32) xbit = np.zeros(shape=shape, dtype=np.int32) return xout_hs, xbit
[docs] @staticmethod def do_decimation_polyphase_order_one(uin: np.ndarray) -> np.ndarray: """Performing first order Non-Recursive Polyphase Decimation on input""" last_sample_hs = 0 uout = [] for idx, val in enumerate(uin): if idx % 2 == 1: uout.append(val + last_sample_hs) last_sample_hs = val uout = np.array(uout) return uout
[docs] @staticmethod def do_decimation_polyphase_order_two(uin: np.ndarray) -> np.ndarray: """Performing second order Non-Recursive Polyphase Decimation on input""" last_sample_hs = 0 last_sample_ls = 0 uout = [] for idx, val in enumerate(uin): if idx % 2 == 1: uout.append(val + last_sample_ls + 2 * last_sample_hs) last_sample_ls = val last_sample_hs = val uout = np.array(uout) return uout