Source code for cbadc.fom

"""Figures of merit

This module provides tools to evaluate standard figures of merit as well as
provides an interface to Prof. Boris Murmann's famous `ADC Survey <https://web.stanford.edu/~murmann/adcsurvey.html>`_.
"""
import logging
import os
from typing import List, Tuple
import requests
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.cm
import matplotlib.colors

logger = logging.getLogger(__name__)

__twenty_log_2 = 20.0 * np.log10(2.0)
__quantization_noise_offset = 20.0 * np.log10(np.sqrt(6.0) / 2.0)


[docs]def snr_to_enob(SNR: float): """Compute the effective number of bits (ENOB). :math:`f(\\text{SNR}) \\approx \\frac{\\text{SNR} - 1.76}{6.02}` Parameters ---------- SNR: `float` the SNR expressed in dB. Returns : `float` the effective number of bits. """ return (SNR - __quantization_noise_offset) / __twenty_log_2
[docs]def snr_to_dB(snr: float): """Convert snr to dB :math:`f(\\text{snr}) = 10 \log(\\text{snr})` Parameters ---------- snr: `float` the snr not expressed in dB Returns ------- : `float` the SNR expressed in dB """ return 10.0 * np.log10(snr)
[docs]def snr_from_dB(snr: float): """Convert SNR from dB :math:`f(\\text{snr}) = 10^{ \\frac{\\text{snr}}{10}}` Parameters ---------- snr: `float` the snr expressed in dB Returns ------- : `float` the SNR not expressed in dB """ return 10 ** (snr / 10.0)
[docs]def enob_to_snr(ENOB: float): """Convert effective number of bits into SNR :math:`f(\\text{ENOB}) \\approx 6.02 \\cdot \\text{ENOB} + 1.76` Parameters ---------- ENOB: `float` effective number of bits Returns ------- : `float` The corresponding SNR expressed in dB. """ return ENOB * __twenty_log_2 + __quantization_noise_offset
[docs]def nyquist_frequency(fs: float): """The Nyquist frequency or bandwidth of a sampled signal. :math:`f(f_s)=f_s / 2` Parameters ---------- fs: `float` the sampling frequency Returns ------- : `float` the Nyquist frequency. """ return fs / 2.0
[docs]def OSR(fs, f_sig): """The oversampling ratio (OSR) :math:`f(f_s, f_{\\text{sig}}) = \\frac{f_s}{2 \\cdot f_{\\text{sig}}}` Parameters ---------- fs: `float` the sampling frequency f_sig: `float` the signal bandwidth. Returns ------- : `float` the oversampling ratio. """ return nyquist_frequency(fs) / f_sig
[docs]def FoM_W(P, fs, ENOB): """The Walden figure of merit (FoM) :math:`f(P, f_s, \\text{ENOB}) = \\frac{P}{f_s \\cdot 2 ^{ \\text{ENOB}}}` See `Walden 1999 <https://ieeexplore.ieee.org/document/761034>`_ Parameters ---------- P: `float` the power consumption. fs: `float` the sampling frequency. ENOB: `float` effective number of bits. Returns ------- The Walden figure of merit. """ return P / (fs * 2**ENOB)
[docs]def FoM_S(P, fs, SNR): """The Schreier figure fo merit. :math:`f(P, f_s, \\text{SNR}) = \\text{SNR} + 10 \log\\left(\\frac{f_s}{2 \cdot P}\\right)` From the book `Understanding Delta-Sigma Data Converters`. Parameters ---------- P: `float` power consumption. fs: `float` the sampling frequency. SNR: `float` the signal-to-noise (SNR) ratio. Returns ------- : `float` Schreier's figure of merit. """ return SNR + 10.0 * np.log10(nyquist_frequency(fs) / P)
[docs]class MurmannSurvey: """Data container for Murmann's ADC survey A convenience class for downloading and parsing the `Murmann ADC survey <https://web.stanford.edu/~murmann/adcsurvey.html>`_ into a pandas dataframe instance. Note that if the ADC survey excel file is not present in the working directory upon instantiating this class it is automatically downloaded. Attributes ---------- db: :py:class:`pandas.DataFrame` a pandas data frame containing the full Murmann survey. """ def __init__(self): self._version = [ "https://web.stanford.edu/~murmann/publications/ADCsurvey_rev20210628.xls" ] self._current_year = 2021 self._FoMW_hf_corner = 4.42e8 self._FoMW_hf_overall = 0.67 self._FoMS_hf_corner = 3.19e7 self._FoMS_hf_overall = 184.76 filename = os.path.basename(self._version[-1]) # Download from Standford if not present. if not os.path.isfile(filename): logging.info("Downloading Murmann Survey.") req = requests.get(self._version[-1], allow_redirects=True) with open(filename, 'wb') as f: f.write(req.content) else: logging.info(f"Found local version of {filename}") _temp = pd.read_excel(filename, sheet_name=['ISSCC', 'VLSI']) _temp['ISSCC']['CONFERENCE'] = 'ISSCC' _temp['VLSI']['CONFERENCE'] = 'VLSI' self.db = pd.concat(_temp, ignore_index=True) self._architecture = pd.unique(self.db['ARCHITECTURE']) self._color_map = matplotlib.cm.viridis self._color_list = [ matplotlib.colors.rgb2hex(self._color_map(i)) for i in np.linspace(0, 0.9, len(self._architecture)) ] # Fix some data problems self.db['AREA [mm^2]'] = pd.to_numeric(self.db['AREA [mm^2]'], errors='coerce')
[docs] def columns(self) -> List[str]: """Returns the columns of the dataframe Returns ------- :[str] all valid columns of the parsed survey. """ return self.db.columns
[docs] def architectures(self) -> List[str]: """Returns the present architectures of the survey. Returns ------- :[str] a list containing the architectures. """ return self._architecture
[docs] def energy(self): """Create the Murmann energy plot Creates a matplotlib scatter plot corresponding to the one found in the ADC survey. Returns ------- : :py:class:`matplotlib.axes.Axes` the figure axis. """ plt.figure() ax = plt.gca() # Plot from Murmann survey self._Murmann_style_data_and_legends('SNDR_hf [dB]', 'P/fsnyq [pJ]', ax) # Plot FoM lines _x = [40, 120] _y = [1e12 * self._FoMW_SNDR_to_p_fs(1e-15, x) for x in _x] ax.plot(_x, _y, '--', color='green', label='FoMW 1fJ/conv-step') _x = [40, 120] _y = [1e12 * self._FoMS_SNDR_to_p_fs(185, x) for x in _x] ax.plot(_x, _y, color='green', label='FoMS=185dB') # Estetics _ = ax.legend() ax.set_yscale('log') ax.set_title("Energy") ax.grid(True, which="both") ax.set_xlim((10, 120)) ax.set_ylim((1e-1, 1e7)) return ax
[docs] def aperture(self): """Create the Murmann aperture plot Creates a matplotlib scatter plot corresponding to the one found in the ADC survey. Returns ------- : :py:class:`matplotlib.axes.Axes` the figure axis. """ plt.figure() ax = plt.gca() # Plot from Murmann survey. self._Murmann_style_data_and_legends('SNDR_hf [dB]', 'fin_hf [Hz]', ax) # Jitter lines _y = [1e6, 1e11] _x = [self._f_sigma_to_jitter_sndr(1e-12, y) for y in _y] ax.plot(_x, _y, color='red', label='Jitter=1psrms') _y = [1e6, 1e11] _x = [self._f_sigma_to_jitter_sndr(1e-13, y) for y in _y] ax.plot(_x, _y, '--', color='red', label='Jitter=0.1psrms') # Estetics _ = ax.legend() ax.set_yscale('log') ax.set_title("Aperture") ax.grid(True, which="both") ax.set_xlim((10, 120)) ax.set_ylim((1e6, 1e11)) return ax
[docs] def walden_vs_speed(self): """Create the Murmann walden FoM vs speed plot Creates a matplotlib scatter plot corresponding to the one found in the ADC survey. Returns ------- : :py:class:`matplotlib.axes.Axes` the figure axis. """ plt.figure() ax = plt.gca() # Plot from Murmann Survey self._Murmann_style_data_and_legends('fsnyq [Hz]', 'FOMW_hf [fJ/conv-step]', ax) # Envelope _x = np.logspace(3, 12, 100) _y = [self._FoMW_envelope(x) for x in _x] ax.plot(_x, _y, '--', color='black', label='Envelope') # Estetics _ = ax.legend() ax.set_yscale('log') ax.set_xscale('log') ax.set_title("Walden's FoM vs Speed") ax.grid(True, which="both") ax.set_xlim((1e4, 5e11)) ax.set_ylim((2e-1, 2e4)) return ax
[docs] def schreier_vs_speed(self): """Create the Murmann Schreier FoM vs speed plot Creates a matplotlib scatter plot corresponding to the one found in the ADC survey. Returns ------- : :py:class:`matplotlib.axes.Axes` the figure axis. """ plt.figure() ax = plt.gca() # Plot from Murmann Survey self._Murmann_style_data_and_legends('fsnyq [Hz]', 'FOMS_hf [dB]', ax) # Envelope _x = np.logspace(2, 12, 100) _y = [self._FoMS_envelope(x) for x in _x] ax.plot(_x, _y, '--', color='black', label='Envelope') # Estetics _ = ax.legend() ax.set_xscale('log') ax.set_title("Schreier's FoM vs Speed") ax.grid(True, which="both") ax.set_xlim((1e2, 1e12)) ax.set_ylim((120, 190)) return ax
[docs] def select_bw_and_enob(self, BW: Tuple[float, float], ENOB: Tuple[float, float]): """Select publications with a specific bandwidth and ENOB range. Specifically, return all publications where BW[0] <= nyquist frequency < BW[1], ENOB[0] <= effective number of bits < ENOB[1]. Parameters ---------- BW: [float, float] a lower and upper bandwidth range. ENOB: [float, float] a lower and upper effective number of bits (ENOB) range. Returns : :py:class:`pandas.DataFrame` a new pandas dataframe with the selected subset. """ if BW[0] > BW[1]: raise Exception("BW must be a tuple with accsending values like (1e6, 1e8)") if ENOB[0] > ENOB[1]: raise Exception("ENOB must be a tuple with accsending values like (8, 10)") return self.db[ (self.db['fsnyq [Hz]'] >= BW[0]) & (self.db['fsnyq [Hz]'] < BW[1]) & (self.db['SNR [dB]'] >= enob_to_snr(ENOB[0])) & (self.db['SNR [dB]'] < enob_to_snr(ENOB[1])) ]
def _Murmann_style_data_and_legends(self, x, y, ax): self.db[ (self.db['CONFERENCE'] == 'ISSCC') & (self.db['YEAR'] == self._current_year) ].plot.scatter( x, y, label=f"ISSCC {self._current_year}", color='red', marker='s', ax=ax ) self.db[ (self.db['CONFERENCE'] == 'VLSI') & (self.db['YEAR'] == self._current_year) ].plot.scatter( x, y, label=f"VLSI {self._current_year}", color='blue', marker='D', ax=ax ) self.db[ (self.db['CONFERENCE'] == 'ISSCC') & (self.db['YEAR'] < self._current_year) ].plot.scatter( x, y, label=f"ISSCC 1997-{self._current_year - 1}", color='black', marker='o', ax=ax, ) self.db[ (self.db['CONFERENCE'] == 'VLSI') & (self.db['YEAR'] < self._current_year) ].plot.scatter( x, y, label=f"VLSI 1997-{self._current_year - 1}", color='black', marker='x', ax=ax, ) def _f_sigma_to_jitter_sndr(self, f, sigma): return -20.0 * np.log10(2 * np.pi * sigma * f) def _FoMW_SNDR_to_p_fs(self, FoMW, SNDR): ENOB = snr_to_enob(SNDR) return FoMW * 2**ENOB def _FoMS_SNDR_to_p_fs(self, FoMS, SNDR): return (10 ** (-(FoMS - SNDR) / 10.0)) / 2.0 def _FoMW_envelope(self, f): return self._FoMW_hf_overall * np.sqrt(1.0 + (f / self._FoMW_hf_corner) ** 2) def _FoMS_envelope(self, f): return self._FoMS_hf_overall - 10.0 * np.log10( np.sqrt(1.0 + (f / self._FoMS_hf_corner) ** 2) )