---
title: |
  Fundamentals of Analog VLSI Design \
  Exercise 6 - Solution
subtitle: The Fully Differential G~m~-R Gain Stage
author: Christian Enz (christian.enz@epfl.ch)
affiliation: EPFL
date: 10-29-2025
date-format: "DD.MM.YYYY"
execute:
  echo: false
  warning: false
toc: true
number-sections: true
number-depth: 3
highlight-style: pygments
bibliography: references.bib
csl: ieee.csl
format: 
  html:
    include-in-header: mathjax.html
    code-fold: true
    html-math-method: mathjax
    css: styles.css
  pdf:
    keep-tex: true
    pdf-engine: pdflatex
    documentclass: scrreprt
    papersize: a4
    geometry:
      - top=20mm
      - bottom=20mm
      - left=20mm
      - right=20mm
    include-in-header:
      text: |
        \usepackage{scrlayer-scrpage}
        \rohead[\thepage]{\thepage}
        \cohead[Exercise 6]{Exercise 6}
        \lofoot[$\copyright$ C. Enz]{$\copyright$ C. Enz}
        \cofoot[Fundamentals of Analog VLSI Design]{Fundamentals of Analog VLSI Design}
        \rofoot[29.09.2025]{29.10.2025}
---

```{python}
# Initialization

from ekv_functions import *

import pandas as pd
import matplotlib.pyplot as plt
import subprocess
import shutil
import os
import os.path as op
import sys
sys.path.append(".")
import re

from IPython.display import Markdown
from tabulate import tabulate

plt.rcParams['svg.fonttype'] = 'none'
plt.rcParams['pdf.fonttype'] = 42
plt.rcParams['font.family'] = 'Arial'
plt.rcParams['mathtext.fontset'] = 'custom'
plt.rcParams['mathtext.rm'] = 'Arial'
plt.rcParams['mathtext.it'] = 'Arial:italic'
plt.rcParams['mathtext.bf'] = 'Arial:bold'

inkscapePath = r"C:\\Program Files\\Inkscape\bin\\inkscape.exe"
savePlotPath = "./Plots/"
#savePlots=True
savePlots=False

if savePlots:
  lw=1
  curvelw=2
  msize=6
  textFontSize=12
  BodePlotParams = {'figure.figsize': (6,6)}
  pltStyle='plt_style_medium.mplstyle'
else:
  lw=1
  curvelw=1
  msize=5
  textFontSize=9
  BodePlotParams = {'figure.figsize': (5,5)}
  pltStyle='plt_style_small.mplstyle'

plt.style.use(pltStyle)

circuitName="diff_pair"
#newSim=True
newSim=False
refloat=r'.*?([+-]?\d+([.]\d*)?([eE][+-]?\d+)?|[.]\d+([eE][+-]?\d+)?)'

def saveFigures(savePath, plotName):
#  print("Patience, saving plot!")
  figFolder = savePath + r"\\{}.{}"
  pngFile = figFolder.format(plotName,"png")
  pdfFile = figFolder.format(plotName,"pdf")
  svgFile = figFolder.format(plotName,"svg")
  plt.savefig(pngFile, format="png", dpi=1200, bbox_inches="tight", transparent=True)
  plt.savefig(pdfFile, format="pdf", dpi=1200, bbox_inches="tight", transparent=True)
  plt.savefig(svgFile, format="svg", dpi=1200, bbox_inches="tight", transparent=True)
  cmd_emf = '"{}" "{}\\{}.svg" --export-filename="{}\\{}.emf" --export-overwrite'.format(inkscapePath, savePath, plotName, savePath, plotName)
#    cmd_emf = '"{}" "{}\{}.pdf" --export-filename="{}\{}.emf" --export-overwrite'.format(inkscapePath, savePath, plotName, savePath, plotName)
#    cmd_pdf = '"{}" "{}\{}.svg" --export-filename="{}\{}.pdf" --export-overwrite'.format(inkscapePath, savePath, plotName, savePath, plotName)
  subprocess.call(cmd_emf, shell=True)
#    subprocess.call(cmd_pdf, shell=True)
```

# Introduction

![Schematic of the differential pair with resistive load.](Figures/Diff_Pair_with_Rload.png){#fig-Diff_Pair_with_Rload}

@fig-Diff_Pair_with_Rload shows a differential pair composed of two NMOS transistors $M_1$ and $M_2$, loaded with resistors $R_1$ and $R_2$, respectively. Since there are two input terminals, the output current or voltage depends on both the input voltages $V_{i1}$ and $V_{i2}$. It is usually more interesting to express the output current or voltage in terms of the differential and common mode input voltages $V_{id}$ and $V_{ic}$ defined as
\begin{align*}
  V_{id} &\triangleq V_{i1} - V_{i2}\\
  V_{ic} &\triangleq \frac{V_{i1}+V_{i2}}{2}.
\end{align*}
The differential mode and common mode operations are defined for $V_{ic} = const.$ and for $V_{id} = 0$, respectively. The input terminals are set to an appropriate common mode voltage $V_{ic}$, to which a differential voltage $V_{id}$ is superimposed according to
\begin{align*}
  V_{i1} &= V_{ic} + \frac{V_{id}}{2}\\
  V_{i2} &= V_{ic} - \frac{V_{id}}{2}.
\end{align*}

# Small-signal analysis

![Small-signal schematic of the differential pair with resistive load.](Figures/small_signal_schematic.png){#fig-small_signal_schematic}

@fig-small_signal_schematic shows the small-signal schematic of the differential pair of @fig-Diff_Pair_with_Rload, where $Y_s$ corresponds to the admittance to ground at the common source node made of the output conductance of the bottom current source and the total capacitance between the common source node and ground.

If the transistors and resistances are perfectly matched, in small-signal operation (i.e. $G_{m1}=G_{m2}=G_m$ and $R_1=R_2=R$), an increase of the gate voltage of $M_1$ by $\Delta V_{id}/2$ combined with a decrease of the gate voltage of $M_2$ by the same amount keeps the common source node unchanged. This node can therefore be considered as a virtual AC ground (i.e. $\Delta V_S$). The circuit then reduces to a common-source stage loaded by a resistance $R$ which has a voltage gain $-G_m\,R$. The differential stage has therefore the same ideal differential voltage gain $A_{vd}=-G_m\,R$ as the common-source stage.

Of course we can find this results by solving the KCL equations of the equivalent small-signal circuit of @fig-small_signal_schematic. Writing the KCL equation for the three nodes leads to
\begin{align}
  G_{m1}\,\Delta V_{G1} + \Delta V_{o1}/R_1 &= 0,\\
  G_{m2}\,\Delta V_{G2} + \Delta V_{o2}/R_2 &= 0,\\
  G_{m1}\,\Delta V_{G1} + G_{m2}\,\Delta V_{G2} &= Y_s\,\Delta V_S.
\end{align}

Additionally, the incremental gate voltages are related to the input voltages according to
\begin{align}
  \Delta V_{G1} &= \Delta V_{i1} - \Delta V_S,\\
  \Delta V_{G2} &= \Delta V_{i2} - \Delta V_S.
\end{align}

Solving  together, we obtain the incremental output voltages as
\begin{align}
  \Delta V_{o1} &= -G_{m1}\,R_1 \cdot \frac{(G_{m2}+Y_s)\,\Delta V_{i1}-G_{m2}\Delta V_{i2}}{G_{m1}+G_{m2}+Y_s},\label{eqn:Vo1}\\
  \Delta V_{o2} &= -G_{m2}\,R_2 \cdot \frac{-G_{m1}\Delta V_{i1}+(G_{m1}+Y_s)\Delta V_{i2}}{G_{m1}+G_{m2}+Y_s}.\label{eqn:Vo2}
\end{align}

We now express the output voltages in terms of the differential and common mode voltages $\Delta V_{id}$ and $\Delta V_{ic}$ defined as
\begin{align}
  \Delta V_{id} &\triangleq \Delta V_{i1} - \Delta V_{i2},\\
  \Delta V_{ic} &\triangleq \frac{\Delta V_{i1} + \Delta V_{i2}}{2},
\end{align}
such that
\begin{align}
  \Delta V_{i1} &= \Delta V_{ic} + \frac{\Delta V_{id}}{2},\\
  \Delta V_{i2} &= \Delta V_{ic} - \frac{\Delta V_{id}}{2}.
\end{align}
The differential mode of operation is then defined for $\Delta V_{ic} = 0$, whereas the common mode operation corresponds to $\Delta V_{id} = 0$.

The *differential voltage gain*, is then defined as
\begin{equation}\label{eqn:Avd1}
  A_{vd} \triangleq \frac{\Delta V_{od}}{\Delta V_{id}} = \frac{\Delta V_{o1} - \Delta V_{o2}}{\Delta V_{id}} 
\end{equation}
and is obtained from \eqref{eqn:Vo1} and \eqref{eqn:Vo2} as
\begin{equation}\label{eqn:Avd2}
  A_{vd} = -\frac{G_{m1}\,G_{m2}\,(R_1+R_2)+(G_{m1}R_1+G_{m2}R_2)Y_s/2}{G_{m1}+G_{m2}+Y_s}.
\end{equation}
Assuming that the transistors and resistances are perfectly matched $G_{m1}=G_{m2}=G_m$ and $R_1=R_2=R$, the expression for the differential voltage gain simplifies to the expected result
\begin{equation}\label{eqn:Avd3}
  A_{vd} = -G_m\,R.
\end{equation}

One of the main feature of the differential pair is to reject the input common-mode voltage. In the case the transistors and resistances are perfectly matched, the common-mode to differential output voltage gain is ideally equal to zero because $\Delta V_{o1}=\Delta V_{o2}$. However, if there is a mismatch between the transistors or the resistances (or both), $\Delta V_{o1} \neq \Delta V_{o2}$ and therefore a differential output voltage is generated. The common-mode to differential voltage gain is given by
\begin{equation}\label{eqn:Avc}
  A_{vc} = Y_s \cdot \frac{G_{m2}\,R_2-G_{m1}\,R_1}{G_{m1}+G_{m2}+Y_s}
\end{equation}
From \eqref{eqn:Avc}, we see that for a perfect matching (i.e. $G_{m1}=G_{m2}$ and $R_1=R_2$) then $A_{vc}=0$ since $G_{m1}\,R_1 = G_{m2}\,R_2 = G_m \,R$. Note that at low frequency, $Y_s$ is equal to the output conductance $G_{ds}$ of the bottom current source. The common-mode to differential voltage gain is proportional to $G_{ds}$. We can account for the mismatch by replacing
\begin{align}
  G_{m1} &= G_m + \frac{\Delta G_m}{2},\\
  G_{m2} &= G_m - \frac{\Delta G_m}{2},\\
  R_1 &= R + \frac{\Delta R}{2},\\
  R_2 &= R - \frac{\Delta R}{2},
\end{align}
resulting in
\begin{equation}
  A_{vc} = -G_{ds} \cdot \frac{G_m\,R}{G_{ds}+2G_m} \cdot \left(\frac{\Delta G_m}{G_m} + \frac{\Delta R}{R}\right)
  \cong - \frac{G_{ds}\,R}{2} \left(\frac{\Delta G_m}{G_m} + \frac{\Delta R}{R}\right),
\end{equation}
since $G_m \gg G_{ds}$.

The ability of the differential pair to reject the differential voltage is measured by the common-mode rejection ratio or CMRR defined as
\begin{equation}\label{eqn:CMRR}
  CMRR \triangleq \frac{|A_{vd}|}{|A_{vc}|} \cong \frac{2 G_m/G_{ds}}{\frac{\Delta G_m}{G_m} + \frac{\Delta R}{R}}.
\end{equation}
From \eqref{eqn:CMRR}, we observe that the $CMRR$ is proportional to the transistor intrinsic gain (or self-gain) $G_m/G_{ds}$.

# Noise analysis

![Small-signal schematic of the differential pair with resistive load including the noise sources.](Figures/noise_schematic.png){#fig-noise_schematic}

The equivalent small-signal circuit of @fig-Diff_Pair_with_Rload including all the noise sources is shown in @fig-noise_schematic where $I_{nM1}$ and $I_{nM2}$ represent the noise of M~1~ and M~2~, whereas $I_{nR1}$ and $I_{nR2}$ represent the thermal noise of $R_1$ and $R_2$. For noise analysis the inputs are connected to the dc common-mode input voltage $V_{ic}$ so that $\Delta V_{i1} = \Delta V_{i2} = 0$ and $\Delta V_{G1} = \Delta V_{G2} = -\Delta V_S$. The output noise voltages are then simply given by
\begin{align}
  \Delta V_{no1} &= -R \cdot (I_{nM1} + I_{nR1}),\\
  \Delta V_{no2} &= -R \cdot (I_{nM2} + I_{nR2}).
\end{align}
and the differential output noise voltage is given by
\begin{equation}\label{eqn:Vnout}
  \Delta V_{nout} = -R \cdot (I_{nM1} + I_{nR1} - I_{nM2} - I_{nR2}).
\end{equation}
The PSD of the differential output noise voltage is then simply given by
\begin{equation}
  S_{nout} = R^2 \cdot (S_{I_{nM1}} + S_{I_{nR1}} + S_{I_{nM2}} + S_{I_{nR2}})
  = 2 R^2 \cdot (S_{I_{nM}} + S_{I_{nR}}).
\end{equation}
which can also be written in terms of the output noise resistance $R_{nout}$
\begin{equation}
  S_{nout} = 4 kT\,R_{nout}
\end{equation}
with
\begin{equation}
  R_{nout} = R^2 \cdot 2 (G_{nM} + G_{nR}).
\end{equation}
The noise conductances $G_{nM}$ and $G_{nR}$ are given by
\begin{align}
  G_{nM} &= \gamma_n\,G_m + \frac{\rho_n}{W\,L\,f},\\
  G_{nR} &= \frac{1}{R}.
\end{align}

The input-referred noise resistance at low-frequency is then given by
\begin{equation}
  R_{nin} = \frac{R_{nout}}{|A_{vd}|^2} = \frac{R_{nout}}{(G_m\,R)^2} = 2 \frac{G_{nM} + G_{nR}}{G_m^2}.
\end{equation}

The input-referred thermal noise resistance is then given by
\begin{equation}\label{eqn:20}
  R_{nin,th} = \frac{2 \gamma_n}{G_m} \cdot (1 + \eta_{th}),
\end{equation}
where $\eta_{th}$ represents the contribution of the resistances relative to the contribution of the differential pair
\begin{equation}\label{eqn:etath}
  \eta_{th} = \frac{1}{\gamma_n\,G_m\,R}.
\end{equation}
From \eqref{eqn:etath}, we see that the larger the differential gain, the lower the contribution of the resistances to the input-referred thermal noise resistance, which is consistent with the intuition that the larger the gain of the first stage the lower the contribution of the following stages to the input-referred noise.

The input-referred flicker noise resistance is only due to the transistors since the resistances only generate thermal noise
\begin{equation}
  R_{nin,fl}(f) = 2\frac{\rho_n}{W\,L\,f}.
\end{equation}

If we include a capacitance $C$ in parallel to the resistors $R$, the output thermal noise PSD is given by
\begin{equation}
  S_{nout,th} = \frac{S_0}{1 + (\omega/\omega_c)^2},
\end{equation}
with
\begin{equation}
  S_0 = 8 kT \cdot R \cdot (\gamma_n\,G_m\,R + 1)
\end{equation}
and $\omega_c=1/(R C)$ is the cut-off frequency. The output noise is therefore a 1^st^-order low-pass filtered white noise. The noise bandwidth is therefore given by
\begin{equation}
  B_n = \frac{\omega_c}{4} = \frac{1}{4 R C},
\end{equation}
The resulting variance of the output thermal noise voltage is then given by
\begin{equation}
  V_{nout,th}^2 = \frac{2kT}{C} \cdot (\gamma_n\,G_m\,R + 1)
  = \frac{2kT\,\gamma_n\,G_m\,R}{C} + \frac{2kT}{C}
  \cong \frac{2kT}{C} \cdot \gamma_n\,G_m\,R
\end{equation}
assuming that $G_m\,R \gg 1$. We see that the contribution of the resistance is simply $2kT/C$ because the noise level is proportional to $R$ while the cut-off frequency is inversely proportional to $R$. The contribution of the differential pair depends on $G_m$ because $G_m$ sets the noise level but the cut-off frequency is set by $R$ and does not depend on $G_m$.

The variance of the input-referred thermal noise voltage is then given by
\begin{equation}\label{eqn:Vninth2}
  V_{nin,th}^2 = \frac{V_{nout,th}^2}{(G_m\,R)^2} = \frac{2kT}{C\,G_m\,R}\,\left(\gamma_n + \frac{1}{G_m\,R}\right)
  \cong \frac{2 \gamma_n kT}{C\,G_m\,R}.
\end{equation}
From \eqref{eqn:Vninth2}, we see that the variance of the input-referred thermal noise for $G_m\,R \gg 1$ is inversely proportional to the voltage gain $G_m\,R$ and proportional to the transistor thermal noise excess factor $\gamma_n$.

::: {.callout-note}
Note that because the noise bandwidth is set by $R$ and is independent of $G_m$, we can set independently the bandwidth with $R$, the DC gain with $G_m$ and the thermal noise power with $C$.
:::

# Offset analysis
Because of the transistor and resistor mismatch, the output voltage is not equal to zero when the differential input voltage is zero. To calculate the offset voltage we can reuse the expression of the output noise voltage \eqref{eqn:Vnout} obtained in the noise analysis and replace
\begin{align}
  I_{nM1} &= +\frac{\Delta I_D}{2},\\
  I_{nM2} &= -\frac{\Delta I_D}{2},\\
  I_{nR1} &= +\frac{I_b}{R} \cdot \frac{\Delta R}{2},\\
  I_{nR2} &= -\frac{I_b}{R} \cdot \frac{\Delta R}{2}.
\end{align}

The resulting differential output offset voltage is then given by
\begin{equation}
  V_{os,out} = -R\,I_b \cdot \left(\frac{\Delta R}{R} + \frac{\Delta I_D}{I_b}\right)
\end{equation}
The dc input-referred offset voltage is then given by
\begin{equation}
  V_{os} = \frac{V_{os,out}}{-G_m\,R} = \frac{I_b}{G_m} \cdot \left(\frac{\Delta R}{R} + \frac{\Delta I_D}{I_b}\right).
\end{equation}
The variance of the input-referred offset voltage is then given by
\begin{equation}
  \sigma_{V_{os}}^2 = \left(\frac{I_b}{G_m}\right)^2 \cdot (\sigma_{\Delta R/R}^2 + \sigma_{\Delta I_D/I_D}^2).
\end{equation}
The variance $\sigma_{\Delta I_D/I_D}^2$ depends on the transistor $\beta$- and $V_T$-mismatch according to
\begin{equation}
  \sigma_{\Delta I_D/I_D}^2 = \sigma_{\Delta \beta/\beta}^2 + \left(\frac{G_m}{I_b}\right)^2 \cdot \sigma_{\Delta V_{T0}}^2,
\end{equation}
resulting in
\begin{equation}\label{eqn:sigmaVos2}
  \sigma_{V_{os}}^2 = \sigma_{\Delta V_{T0}}^2 + \left(\frac{I_b}{G_m}\right)^2 \cdot \left(\sigma_{\Delta R/R}^2 + \sigma_{\Delta \beta/\beta}^2\right)
\end{equation}
The variances $\sigma_{\Delta V_{T0}}^2$ and $\sigma_{\Delta \beta/\beta}^2$ can be expressed in terms of the transistor area according to
\begin{align}
  \sigma_{\Delta V_{T0}}^2 &= \frac{A_{\Delta V_{T0}}^2}{W\,L},\\
  \sigma_{\Delta \beta/\beta}^2 &= \frac{A_{\beta}^2}{W\,L}.
\end{align}
From \eqref{eqn:sigmaVos2}, we see that the contributions of the resistance mismatch and transistor $\beta$-mismatch to the input-referred offset voltage can be minimized by biasing the differential pair in weak inversion. The input-referred offset voltage then reduces to the transistor $V_{T0}$ mismatch.

# Common-mode input range analysis (CMIR) and differential-mode output range analysis (DMOR)
The minimum common-mode input voltage is limited by the saturation voltage of the bias current source
\begin{equation}
  V_{ic,min} = V_{GS} + V_{DSsat,I_b},
\end{equation}
where $V_{GS}$ is the gate-to-source voltage of M~1~-M~2~ and $V_{DSsat,I_b}$ the saturation voltage of the bias current source.

The maximum common-mode input voltage is limited by the voltage drop across the load resistance and the differential pair transistors M~1~-M~2~ going out of saturation. Indeed, when increasing the common-mode input voltage the common-source node voltage follows reducing the $V_{DS}$ voltage of M~1~-M~2~ until it becomes smaller than the saturation voltage $V_{DSsat}$. The maximum common-mode input voltage is therefore given by
\begin{equation}
  V_{ic,max} = V_{GS} - V_{DSsat} - R\,I_b + V_{DD},
\end{equation}
where $V_{GS}$ is the gate-to-source voltage of M~1~-M~2~ and $V_{DSsat}$ its saturation voltage.

The maximum output voltage is simply equal to $V_{DD}$. One of the output voltage saturates to $V_{DD}$ when the differential voltage is large enough for all the bias current to be steered into one side of the differential pair leaving no current flowing in the other branch making its voltage equal to $V_{DD}$.

The minimum output voltage corresponds to the same situation with all the current flowing into one branch. The voltage drop across the load resistor is then $R\,2I_b$ leading to
\begin{equation}
  V_{o,min} = V_{DD} - 2\,I_b\,R.
\end{equation}
The output voltage swing at each output node $V_{o1}$ and $V_{o2}$ is then given by
\begin{equation}
  \Delta V_{o,max} = V_{o,max} - V_{o,min} = 2\,I_b\,R
\end{equation}
so that the voltage swing of the differential output voltage $V_{od}$ is simply
\begin{equation}
  \Delta V_{od,max} = 2\,\Delta V_{o,max} = 4\,I_b\,R.
\end{equation}

# Example
We want to size the circuit of @fig-Diff_Pair_with_Rload for the specifications given in @tbl-specs. We need to find the minimum current and size the transistor to achieve this specs. We will design the amplifier for a generic 180nm bulk CMOS process. The physical parameters are given in @tbl-physics_param, the global process parameters in @tbl-process_param and finally the MOSFET parameters in @tbl-mos_param. 

```{python}
#| label: tbl-specs
#| tbl-cap: Specifications for the differential pair with resistive loads.

from specs import *

table = [["Input common mode voltage","$V_{{ic}}$",f'{Vic:.1f}',"$V$"],
         ["DC gain","$A_{{dc}}$",f'{AdcdBspec:.0f}',"$dB$"],
         ["DC gain","$A_{{dc}}$",f'{Adcspec:.0f}',"-"],
         ["Bandwidth","$B$",f'{Bspec/1e6:.0f}',"$MHz$"],
         ["Input-referred thermal noise resistance","$R_{{nin,th}}$",f'{Rninthspec/1e3:.0f}',"$k \Omega$"],
         ["Maximum input-referred offset voltage","$V_{{os}}$",f'{Vosmaxspec/1e-3:.0f}',"$mV$"]
         ]

Markdown(tabulate(
  table,
  headers=["Specification","Symbol","Value","Unit"],
  numalign="center",
  stralign="center",
  colalign=("left","center","center","center")
))
```

```{python}
#| label: tbl-physics_param
#| tbl-cap: Physical parameters

from tsmc018_sekv_ekv2p6 import *

table = [["$T$",f'{T:.0f}',"$K$"],
         ["$U_T$",f'{UT/1e-3:.3f}',"$mV$"]
         ]

Markdown(tabulate(
  table,
  headers=["Parameter","Value","Unit"],
  numalign="center",
  stralign="center",
  colalign=("center", "center", "center")
))
```

```{python}
#| label: tbl-process_param
#| tbl-cap: Global process parameters

table = [["$V_{DD}$",f'{VDD:.1f}',"$V$"],
         ["$C_{ox}$",f'{Cox*1e3:.3f}',"$\\frac{fF}{\\mu m^2}$"],
         ["$W_{min}$",f'{Wmin/1e-9:.0f}',"$nm$"],
         ["$L_{min}$",f'{Lmin/1e-9:.0f}',"$nm$"]
         ]

Markdown(tabulate(
  table,
  headers=["Parameter","Value","Unit"],
  numalign="center",
  stralign="center",
  colalign=("center", "center", "center")
))
```

```{python}
#| label: tbl-mos_param
#| tbl-cap: Transistor process parameters

table = [["sEKV parameters"],
         ["$n$",f'{n0n:.2f}',f'{n0p:.2f}',"-"],
         ["$I_{{spec\\Box}}$",f'{Ispecsqn/1e-9:.0f}',f'{Ispecsqp/1e-9:.0f}',"$nA$"],
         ["$V_{{T0}}$",f'{VT0n:.3f}',f'{VT0p:.3f}',"$V$"],
         ["$L_{{sat}}$",f'{Lsatn/1e-9:.0f}',f'{Lsatp/1e-9:.0f}',"$nm$"],
         ["$\\lambda$",f'{lambdan*1e-6:.0f}',f'{lambdap*1e-6:.0f}',"$\\frac{{V}}{{\\mu m}}$"],
         ["Overlap capacitances parameters"],
         ["$C_{{GDo}}$",f'{CGDOn*1e9:.3f}',f'{CGDOp*1e9:.3f}','$\\frac{{fF}}{{\\mu m}}$'],
         ["$C_{{GSo}}$",f'{CGSOn*1e9:.3f}',f'{CGSOp*1e9:.3f}','$\\frac{{fF}}{{\\mu m}}$'],
         ["$C_{{GBo}}$",f'{CGBOn*1e9:.3f}',f'{CGBOp*1e9:.3f}','$\\frac{{fF}}{{\\mu m}}$'],
         ["Junction capacitances parameters"],
         ["$C_J$",f'{CJn*1e3:.3f}',f'{CJp*1e3:.3f}','$\\frac{{fF}}{{\\mu m^2}}$'],
         ["$C_{{JSW}}$",f'{CJSWn*1e9:.3f}',f'{CJSWp*1e9:.3f}','$\\frac{{fF}}{{\\mu m}}$'],
         ["Flicker noise parameters"],
         ["$K_F$",f'{KFn:.1e}',f'{KFn:.1e}',"$J$"],
         ["$AF$",f'{AFn:.1f}',f'{AFp:.1f}',"-"],
         ["$\\rho$",f'{rhon:.3e}',f'{rhop:.3e}','$\\frac{{V \\cdot m^2}}{{A \\cdot s}}$'],
         ["Matching parameters"],
         ["$A_{{VT}}$",f'{AVTn*1e9:.0f}',f'{AVTp*1e9:.0f}',"$mV \\cdot \\mu m$"],
         ["$A_{{\\beta}}$",f'{Abetan*1e8:.0f}',f'{Abetap*1e8:.0f}',"$\\% \\cdot \\mu m$"],
         ["Source and drain sheet resistance parameter"],
         ["$R_{{sh}}$",f'{RSHn:.0f}',f'{RSHp:.0f}',"$\\frac{{\\Omega}}{{\\mu m}}$"],
         ["Width and length parameters"],
         ["$\\Delta W$",f'{DWn/1e-9:.0f}',f'{DWp/1e-9:.0f}',"$\\,nm$"],
         ["$\\Delta L$",f'{DLn/1e-9:.0f}',f'{DLp/1e-9:.0f}',"$\\,nm$"]
         ]

Markdown(tabulate(
  table,
  headers=["Parameter","NMOS","PMOS","Unit"],
  numalign="center",
  stralign="center",
  colalign=("center", "center", "center")
))
```

```{python}
IC=0.1
gms=gms_ic(IC)
n=n0n
gamman=gamman_ic(IC,n)
etath=1/(gamman*Adcspec)
Gm=2*gamman/Rninthspec*(1+etath)
nUT=n*UT
Ispec=Gm*nUT/gms
Ib=Ispec*IC
R=Adcspec/Gm
```

## Design
In order to minimize the input-referred offset voltage, the differential pair is biased in weak inversion with $IC =$ `{python} f'{IC:.1f}'`. The differential pair transconductance is set by the specification on the input-referred thermal noise resistance according to
\begin{equation}
  R_{nin,th} = \frac{2 \gamma_n}{G_m} \cdot (1 + \eta_{th}),
\end{equation}
with
\begin{equation}
  \eta_{th} = \frac{1}{\gamma_n\,A_{dc}}.
\end{equation}
The transconductance is then given by
\begin{equation}
  G_m = \frac{2 \gamma_n}{R_{nin,th}} \cdot (1+\eta_{th}).
\end{equation}
The DC gain is given by the spec, but we need the thermal noise excess factor $\gamma_n$ to compute $\eta_{th}$. Knowing $IC$ we can calculate $\gamma_n =$ `{python} f'{gamman:.3f}'` and the transconductance is then $G_m =$ `{python} f'{Gm/1e-6:.3f}'` $\mu A/V$. 

The load resistance is then set by the DC gain to $R =$ `{python} f'{R/1e3:.3f}'` $k \Omega$ which we round to a slightly higher value to have some margin on the gain.
```{python}
R=126e3
Adc=Gm*R
AdcdB=20*log(Adc)
```

Choosing $R =$ `{python} f'{R/1e3:.0f}'` $k \Omega$ we get $A_{dc} =$ `{python} f'{AdcdB:.3f}'` $dB$.

The transconductance also writes
\begin{equation}
  G_m = \frac{G_{spec}}{n}\,g_{ms} = \frac{I_{spec}}{n U_T}\,g_{ms},
\end{equation}
from which we get the specific current
\begin{equation}
  I_{spec} = \frac{G_m\,n U_T}{g_{ms}}
\end{equation}
Knowing $IC$ we can calculate $g_{ms} =$ `{python} f'{gms:.3f}'` and the specific current $I_{spec} =$ `{python} f'{Ispec/1e-6:.3f}'` $\mu A$. From $I_{spec}$ and $IC$ we get the bias current $I_b =$ `{python} f'{Ib/1e-6:.3f}'` $\mu A$.

```{python}
Ib=5.1e-6
Ispec=Ib/IC
WoverL=Ispec/Ispecsqn
WL=(AVTn/Vosmaxspec)**2
Wini=sqrt(WL*WoverL)
Lini=sqrt(WL/WoverL)
W=21.1e-6
L=300e-9
CL=1/(2*pi*Bspec*R)
```

We round the bias current to $I_b =$ `{python} f'{Ib/1e-6:.3f}'` $\mu A$. The $W/L$ is then given by $W/L =$ `{python} f'{WoverL:.3f}'`. To finally find $W$ and $L$ we use the specification on the maximum input-referred offset. If we neglect the $\beta$ mismatch and the mismatch of the load resistance, we have
\begin{equation}
  V_{os,max} = \frac{A_{\Delta V_{T0}}}{\sqrt{W\,L}}
\end{equation}
We finally get $W =$ `{python} f'{Wini/1e-6:.3f}'` $\mu m$ and $L =$ `{python} f'{Lini/1e-9:.3f}'` $nm$, which we round to $W =$ `{python} f'{W/1e-6:.1f}'` $\mu m$ and $L =$ `{python} f'{L/1e-9:.0f}'` $nm$.

Finally, the load capacitance is set by the bandwidth as $C_L =$ `{python} f'{CL/1e-12:.3f}'` $pF$.

The design is finalized and summarized in @tbl-design_summary.

```{python}
#| label: tbl-design_summary
#| tbl-cap: Summary of the design.

table = [["$I_b$",f'{Ib/1e-6:.1f}',"$\mu A$"],
         ["$R$",f'{R/1e3:.0f}',"$k \Omega$"],
         ["$C_L$",f'{CL/1e-12:.3f}',"$pF$"],
         ["$W$",f'{W/1e-6:.1f}',"$\mu m$"],
         ["$L$",f'{L/1e-9:.0f}',"$nm$"]
         ]

Markdown(tabulate(
  table,
  headers=["Parameter","Value","Unit"],
  numalign="center",
  stralign="center",
  colalign=("center", "center", "center")
))
```

## Simulation
We can now simulate the designed circuit with ngspice to check whether we meet the specifications.

### Operating point
Before running the AC simulation, we first need to check the quiescent voltages and currents and the operating points by running an .OP simulation. The node voltages are extracted from the .ic file and presented in @tbl-op_voltages.

```{python}
#| label: tbl-op_voltages
#| tbl-cap: Operating point information.

simtype = ".op"
fileName = circuitName + simtype
simulationPath = "./Simulations/ngspice/"
simulationFile = simulationPath + fileName + ".cir"
simulationLog = simulationPath + fileName + ".log"
simulationVoltages = simulationPath + fileName + ".ic"
simulationData = simulationPath + fileName + ".dat"

if newSim:
  paramstr = '\n'.join((
    f'.param R={R/1e3:.3}k CL={CL/1e-12:.3f}p W={W/1e-6:.2f}u L={L/1e-6:.2f}u',
    f'.param VDD={VDD:.1f} Vic={Vic:.1} Ib={Ib/1e-6:.1f}u'
    ))
  #print(paramstr)
  with open(simulationPath+'size_bias.par', 'w') as f:
    f.write(paramstr)
  #print('Starting ngspice simulation...\n')
  result = subprocess.run(f"""ngspice_con -b -o "{simulationLog}" "{simulationFile}" """, shell=True, capture_output=True, text=True)

nodeList=["vdd","in1","in2","out1","out2","1","ic","id"]
v_df=pd.DataFrame(index=nodeList)
Nnodes=len(nodeList)
for k in range(0,Nnodes):
  nodeName=nodeList[k]
  regexp = re.compile(r'v\(' + nodeName + r'\)' + refloat)
  with open(simulationVoltages) as f:
    for line in f:
      match = re.search(regexp, line)
      if match:
        v_df.at[nodeName, 'Voltage']=float(match.group(1))

Vout1q=v_df.loc['out1','Voltage']
#pd.set_option('display.float_format', '{:.2f}'.format)
#v_df

Markdown( 
  tabulate(v_df,
  showindex=True,
  headers=["Node","Voltage"],
  numalign="center",
  stralign="center",
  colalign=("center","center")
))
```

```{python}
#| label: tbl-ng_op1
#| tbl-cap: Operating point information extracted from ngspice .op file for each transistor.

ekv2p6mos_df=pd.read_table(simulationData, sep=r'\s+', skiprows=[2], dtype=np.float64, engine='python')
ekv2p6mos_df=ekv2p6mos_df.rename(columns={'@n1[id]': 'Transistor', '@N1[Id]': 'ID', '@N1[Ispec]': 'Ispec', '@N1[IC]': 'IC', '@N1[n0]': 'n', '@N1[Vdsat]': 'VDsat',
                            '@N1[gm]': 'Gm', '@N1[gms]': 'Gms', '@N1[gds]': 'Gds', '@N1[gmbs]': 'Gmb',
                            '@N1[Rn]': 'nn', '@N1[Vnth]': 'Vnth', '@N1[gamman]': 'gamman', '@N1[Vnfl]': 'Vnfl at 1Hz'})
ekv2p6mos_df['Transistor'] = ekv2p6mos_df['Transistor'].astype(str)

transistor_list=["M1","M2"]
Nmos=len(transistor_list)
for k in range(Nmos):
  ekv2p6mos_df.at[k, 'Transistor'] = transistor_list[k]

ekv2p6mos_df.set_index('Transistor', inplace=True)
ekv2p6mos_df.rename_axis(index=None, inplace=True)
pd.set_option('display.float_format', '{:.2E}'.format)
ekv2p6mos_df

feature_list=["ID","Ispec","IC","n","VDsat"]

Nfeature=len(feature_list)
table=[]
for mos in transistor_list:
  row=[mos]
  for feature in feature_list:
    row.append(ekv2p6mos_df.at[mos,feature])
  table.append(row)

for i in range(Nmos):
  for j in [1,2]:
    table[i][j]=table[i][j]/1e-6
  for j in [5]:
    table[i][j]=table[i][j]/1e-3

Markdown(tabulate(
  table,
  headers=["Transistor","$I_D\\;[\\mu A]$","$I_{spec}\\;[\\mu A]$","$IC$","$n$","$V_{Dsat}\\;[mV]$"],
  numalign="center",
  stralign="center",
  colalign=("center","center","center","center","center"),
  floatfmt=(".3f",".3f",".3f",".3f",".2f",".0f")
))
```

```{python}
#| label: tbl-ng_op2
#| tbl-cap: Small-signal operating point information extracted from ngspice .op file for each transistor.

feature_list=["n","Gms","Gm","Gmb","Gds"]

Nfeature=len(feature_list)
table=[]
for mos in transistor_list:
  row=[mos]
  for feature in feature_list:
    row.append(ekv2p6mos_df.at[mos,feature])
  table.append(row)

for i in range(Nmos):
  for j in [2,3,4,5]:
    table[i][j]=table[i][j]/1e-6
#  for j in [5]:
#    table[i][j]=table[i][j]/1e-9

Markdown(tabulate(
  table,
  headers=["Transistor","$n$","$G_{ms}\\;[\\mu A/V]$","$G_m\\;[\\mu A/V]$","$G_{mb}\\;[\\mu A/V]$","$G_{ds}\\;[\\mu A/V]$"],
  numalign="center",
  stralign="center",
  colalign=("center","center","center","center","center","center"),
  floatfmt=(".3f",".2f",".3f",".3f",".3f",".3f")
))
```

The large-signal transistor bias information and the small-signal parameters extracted from the simulation are given in @tbl-ng_op1 and @tbl-ng_op2, respectively.

### Large-signal differential transfer characteristic
We now simulate the large-signal DC input-output transfer characteristic. The simulation result is presented in @fig-ng_large_signal.

```{python}
#| label: fig-ng_large_signal
#| fig-cap: "Simulated large-signal input-output characteristic."

simtype = ".dc"
fileName = circuitName + simtype
simulationPath = "./Simulations/ngspice/"
simulationFile = simulationPath + fileName + ".cir"
simulationLog = simulationPath + fileName + ".log"
simulationData = simulationPath + fileName + ".dat"

Vidmin=-0.3
Vidmax=0.3
Npts=1001
dVid=(Vidmax-Vidmin)/(Npts-1)

if newSim:
  simpar = '\n'.join((
    f'.csparam Vidmin={Vidmin:.3e}',
    f'.csparam Vidmax={Vidmax:.3e}',
    f'.csparam dVid={dVid:.3e}'
  ))
  simparFile = simulationPath + "simulation.dc.par"
  with open(simparFile, 'w') as f:
    f.write(simpar)
  #print('Starting NGSpice simulation...\n')
  result = subprocess.run(f"""ngspice_con -b -o "{simulationLog}" "{simulationFile}" """, shell=True, capture_output=True, text=True)

df_sim=pd.read_table(simulationData, sep=' +', engine='python')

Vidsim=df_sim['v-sweep'].to_numpy()
Vodsim=df_sim['Vod'].to_numpy()

regexp = re.compile(r'vodmax' + refloat)
with open(simulationLog) as f:
  for line in f:
    match = re.search(regexp, line)
    if match:
      Vodmax=float(match.group(1))

regexp = re.compile(r'vodmin' + refloat)
with open(simulationLog) as f:
  for line in f:
    match = re.search(regexp, line)
    if match:
      Vodmin=float(match.group(1))

regexp = re.compile(r'vos' + refloat)
with open(simulationLog) as f:
  for line in f:
    match = re.search(regexp, line)
    if match:
      Vos=float(match.group(1))

Vodswing=Vodmax-Vodmin

plt.plot([Vidmin,Vidmax],[Vodmax,Vodmax],'k--', linewidth=lw)
plt.plot([Vidmin,Vidmax],[Vodmin,Vodmin],'k--', linewidth=lw)
plt.plot(Vidsim,Vodsim,'r')
plt.xlim(Vidmin,Vidmax)
#plt.xticks(np.arange(-0.4,0.5,0.1))
plt.xlabel('$V_{id}$ [$V$]')
plt.ylim(-1.5,1.5)
#plt.yticks(np.arange(0.6,1.5,0.1))
plt.ylabel('$V_{od}$ [V]')
#plt.legend(loc='best')
textstr = '\n'.join((
  f'$V_{{DD}} =$ {VDD:.1f} V',
  f'$V_{{ic}} =$ {Vic:.1f} V',
  f'$V_{{odmax}} \\cong$ {Vodmax:.3f} V',
  f'$V_{{odmin}} \\cong$ {Vodmin:.3f} V',
  f'$V_{{odswing}} \\cong$ {Vodswing:.3f} V'))
plt.text(0.63, 0.05, textstr, ha='left', va='bottom', size=textFontSize, transform=plt.gca().transAxes,
         bbox={'facecolor':'w', 'alpha':1.0, 'pad':5})
if savePlots: saveFigures(savePlotPath, otaName + '_transfer_characteristic_sim')
plt.show()
```

From @fig-ng_large_signal, we see that the differential output voltage swing is $V_{od,swing} =$ `{python} f'{Vodswing:.3f}'` $V$, which as expected corresponds to $4R\,I_b =$ `{python} f'{4*R*Ib:.3f}'` $V$.

### Transfer function
The simulated transfer function is shown in @fig-simulated_tf.

```{python}
#| label: fig-simulated_tf
#| fig-cap: "Simulated gain response compared to theoretical estimation."

def Av(s,Adc,wp):
  num=Adc
  den=1+s/wp
  return num/den

simtype = ".ac"
fileName = circuitName + simtype
simulationPath = "./Simulations/ngspice/"
simulationFile = simulationPath + fileName + ".cir"
simulationLog = simulationPath + fileName + ".log"
simulationData = simulationPath + fileName + ".dat"

if newSim:
  #print('Starting NGSpice simulation...\n')
  result = subprocess.run(f"""ngspice_con -b -o "{simulationLog}" "{simulationFile}" """, shell=True, capture_output=True, text=True)

df_sim=pd.read_table(simulationData, sep=' +', engine='python')
fsim=df_sim['frequency'].to_numpy()
AvmagdBsim=df_sim['AmagdB'].to_numpy()
Avphidegsim=df_sim['Aphdeg'].to_numpy()
Npts=len(fsim)
fmin=fsim[0]
fmax=fsim[Npts-1]

regexp = re.compile(r'adc' + refloat)
with open(simulationLog) as f:
  for line in f:
    match = re.match(regexp, line)
    if match:
      AdcdBsim=float(match.group(1))

regexp = re.compile(r'gbw' + refloat)
with open(simulationLog) as f:
  for line in f:
    match = re.match(regexp, line)
    if match:
      GBWsim=float(match.group(1))

regexp = re.compile(r'fc' + refloat)
with open(simulationLog) as f:
  for line in f:
    match = re.match(regexp, line)
    if match:
      Bsim=float(match.group(1))

AdcdBsim=AvmagdBsim[0]
Adcsim=pow(10,AdcdBsim/20)

Gmthe=ekv2p6mos_df.at["M1","Gm"]
Gdsthe=ekv2p6mos_df.at["M1","Gds"]
GL=Gdsthe+1/R
Adcthe=Gmthe/GL
AdcdBthe=20*log(Adcthe)
wpthe=GL/CL
Bthe=wpthe/(2*pi)
fthe=fsim
AvmagdBthe=np.zeros(Npts)
Avphidegthe=np.zeros(Npts)
AvmagdBthenocf=np.zeros(Npts)
Avphidegthenocf=np.zeros(Npts)

for k in range(0,Npts):
  jw=1j*2*pi*fthe[k]
  AvmagdBthe[k]=20*log(abs(Av(jw,Adcthe,wpthe)))
  Avphidegthe[k]=np.angle(Av(jw,Adcthe,wpthe),deg=True)

plt.rcParams.update(BodePlotParams)
fig, axs = plt.subplots(2, sharex=True)

axs[0].semilogx([fmin,Bspec],[AdcdBspec-3,AdcdBspec-3],'k--', linewidth=lw)
axs[0].semilogx([Bspec,Bspec],[-30,AdcdBspec-3],'k--', linewidth=lw)
axs[0].semilogx(fsim,AvmagdBsim,'ro', markersize=4, markevery=12, label='Simulation')
axs[0].semilogx(fthe,AvmagdBthe,'r-',label='Theory')
axs[0].set(xlim=(fmin,fmax), ylim=(-30,30), ylabel='$|A_{vd}|$ [dB]')
axs[0].set_yticks([-30,-20,-10,0,10,20,30])
axs[0].legend(loc='lower left')
textstr1 = '\n'.join((
  f'$A_{{dc}} =$ {AdcdBspec:.1f} dB (spec)',
  f'$A_{{dc}} =$ {AdcdBthe:.1f} dB (theory)',
  f'$A_{{dc}} =$ {AdcdBsim:.1f} dB (sim)',
  f'$B =$ {Bspec/1e6:.1f} MHz (theory)',
  f'$B =$ {Bthe/1e6:.1f} MHz (theory)',
  f'$B =$ {Bsim/1e6:.1f} MHz (sim.)'))
axs[0].text(1.05, 0.5, textstr1, ha='left', va='center', transform=axs[0].transAxes, size=textFontSize,
         bbox={'facecolor':'w', 'alpha':1.0, 'pad':5})

axs[1].semilogx([fmin,Bspec],[-45,-45],'k--', linewidth=lw)
axs[1].semilogx([Bspec,Bspec],[-45,0],'k--', linewidth=lw)
axs[1].semilogx(fsim,Avphidegsim,'bo', markersize=4, markevery=12, label='Simulation')
axs[1].semilogx(fthe,Avphidegthe,'b-',label='Theory')
axs[1].set(xlim=(fmin,fmax), ylim=(-135,0), xlabel='Frequency [Hz]', ylabel='$arg\\{A_{vd}\\}$ [degree]')
axs[1].set_xticks([1e3,1e4,1e5,1e6,1e7,1e8,1e9])
axs[1].set_yticks([-135,-90,-45,0])
axs[1].legend(loc='lower left')
textstr2 = '\n'.join((
  f'$W =$ {W/1e-6:.1f} $\\mu m$',
  f'$L =$ {L/1e-9:.0f} $nm$',
  f'$I_b =$ {Ib/1e-6:.1f} $\mu A$',
  f'$G_m =$ {Gm/1e-6:.0f} $\mu A/V$',
  f'$R =$ {R/1e3:.0f} $k \Omega$',
  f'$C_L =$ {CL/1e-12:.2f} $pF$'))
axs[1].text(1.05, 0.5, textstr2, ha='left', va='center', transform=axs[1].transAxes, size=textFontSize,
         bbox={'facecolor':'w', 'alpha':1.0, 'pad':5})
plt.subplots_adjust(hspace=0.1)
if savePlots: saveFigures(savePlotPath, 'CS_GBW_tf_long_sim')
plt.show()
plt.style.use(pltStyle)
```

From @fig-simulated_tf we see that the simulation matches the theoretical estimation. However, the DC gain is slightly smaller than the specification because of the transistor output conductance which has not been accounted for. On the other hand, the simulated bandwidth is slightly higher than the target because the DC gain is slightly smaller.

### Input-referred noise
We can compare the theoretical input-referred noise to that obtained from simulations. The simulation results are presented in @fig-ng_noise.

```{python}
#| label: fig-ng_noise
#| fig-cap: Simulated input-referred noise PSD.

simtype = ".nz"
fileName = circuitName + simtype
simulationPath = "./Simulations/ngspice/"
simulationFile = simulationPath + fileName + ".cir"
simulationLog = simulationPath + fileName + ".log"
simulationData = simulationPath + fileName + ".dat"
simulationRaw = simulationPath + fileName + ".raw"

if newSim:
  #print('Starting NGSpice simulation...\n')
  result = subprocess.run(f"""ngspice_con -b -o "{simulationLog}" "{simulationFile}" """, shell=True, capture_output=True, text=True)

df_sim=pd.read_table(simulationData, sep=' +', engine='python')

fsim=df_sim['frequency'].to_numpy()
Vnout=df_sim['onoise_spectrum'].to_numpy()
Vnin=df_sim['inoise_spectrum'].to_numpy()
SnoutdBvsim=20*log(Vnout)
SnindBvsim=20*log(Vnin)

Npts=len(fsim)
fmin=fsim[0]
fmax=fsim[Npts-1]

fthe=fsim
Ispec=Ispecsqn*W/L
Gm=Ispec/nUT*gms_ic(IC)
Adcthe=-Gm*R
gamman=gamman_ic(IC,n)
etath=1/(gamman*Adcthe)
Rninththe=2*gamman/Gm*(1+etath)
S0=4*kT*Rninththe
S0dBvthe=10*log(S0)
Weff=W+DWn
Leff=L+DLn
fkthe=2*rhon/(Weff*Leff*Rninththe)
Sninththe=np.zeros(Npts)
Sninflthe=np.zeros(Npts)
Sninthe=np.zeros(Npts)
SninthdBvthe=np.zeros(Npts)
SninfldBvthe=np.zeros(Npts)
SnindBvthe=np.zeros(Npts)

for k in range(0,Npts):
    Sninththe[k]=S0
    Sninflthe[k]=2*KFn/(Weff*Leff*Cox*fthe[k])
    Sninthe[k]=Sninththe[k]+Sninflthe[k]
    SninthdBvthe[k]=10*log(Sninththe[k])
    SninfldBvthe[k]=10*log(Sninflthe[k])
    SnindBvthe[k]=10*log(Sninthe[k])

#plt.semilogx([fkthe,fkthe],[-150,S0dBvthe],'k--',linewidth=lw)

plt.semilogx(fthe,SninthdBvthe,'b-', label='Thermal (theory)')
plt.semilogx(fthe,SninfldBvthe,'g-', label='Flicker (theory)')
plt.semilogx(fthe,SnindBvthe,'r-', label='Total (theory)')
plt.semilogx(fsim,SnindBvsim,'ro', label='Total (sim.)', markevery=20, markersize=4)
plt.xlim(fmin,fmax)
plt.xticks([1e0,1e1,1e2,1e3,1e4,1e5,1e6,1e7,1e8,1e9])
plt.xlabel('Frequency [Hz]')
plt.ylim(-160,-90)
plt.ylabel(r'Input Noise PSD $[dBv/\sqrt{Hz}]$')
#plt.legend(loc='best')
plt.legend(loc='center left', bbox_to_anchor=(1, 0.5))
#textstr = '\n'.join((
#    f'$S_{{nin,th}} =$ {S0dBvthe:.1f} $dBv/\\sqrt{{Hz}}$ (theory)',
#    f'$S_{{nin,th}} =$ {S0dBvsim:.1f} $dBv/\\sqrt{{Hz}}$ (sim.)',
#    f'$f_k =$ {fkthe/1e3:.1f} kHz (theory)'))
#    f'$f_k =$ {fksim/1e3:.1f} kHz (sim.)'))
#plt.text(0.35, 0.95, textstr, ha='left', va='top', size=textFontSize, transform=plt.gca().transAxes,
#         bbox={'facecolor':'w', 'alpha':1.0, 'pad':5})
if savePlots: saveFigures(savePlotPath, otaName + '_input_noise_sim')
plt.show()
```

We can check that the noise contribution of the load resistors is negligible. This is shown in @fig-ng_white_noise_contributions, which plots the input-referred thermal noise PSD for M~1~-M~2~ and R~1~-R~2~.

```{python}
#| label: fig-ng_white_noise_contributions
#| fig-cap: "Breakdown of the contributions to the simulated input-referred white noise PSD."

regexp = re.compile(r'No. Variables:' + refloat)
with open(simulationRaw) as f:
  for line in f:
    match = re.match(regexp, line)
    if match:
      Nvar=int(match.group(1))

#print(Nvar)

regexp = re.compile(r'No. Points:' + refloat)
with open(simulationRaw) as f:
  for line in f:
    match = re.match(regexp, line)
    if match:
      Npts=int(match.group(1))

#print(Npts)

df_noise=pd.read_table(simulationRaw, sep=r'\s+', skiprows=(19), skip_blank_lines=True, header=None, dtype=np.float64, engine='python')

freq=np.zeros(Npts)
Vnin=np.zeros(Npts)
VnoutM1=np.zeros(Npts)
VnoutM1f=np.zeros(Npts)
VnoutM1w=np.zeros(Npts)
VnoutM2=np.zeros(Npts)
VnoutM2f=np.zeros(Npts)
VnoutM2w=np.zeros(Npts)
VnoutR1=np.zeros(Npts)
VnoutR2=np.zeros(Npts)
Vnout=np.zeros(Npts)
SnoutM=np.zeros(Npts)
SnoutMf=np.zeros(Npts)
SnoutMw=np.zeros(Npts)
SnoutR=np.zeros(Npts)
SninM=np.zeros(Npts)
SninMf=np.zeros(Npts)
SninMw=np.zeros(Npts)
SninMdBv=np.zeros(Npts)
SninMfdBv=np.zeros(Npts)
SninMwdBv=np.zeros(Npts)
SninR=np.zeros(Npts)
SninRdBv=np.zeros(Npts)
SnoutdBv=np.zeros(Npts)
Snin=np.zeros(Npts)
SnindBv=np.zeros(Npts)
A=np.zeros(Npts)
Sninf=np.zeros(Npts)
SninfdBv=np.zeros(Npts)
Sninw=np.zeros(Npts)
SninwdBv=np.zeros(Npts)
SninMwthe=np.zeros(Npts)
SninMwthedBv=np.zeros(Npts)
SninRthe=np.zeros(Npts)
SninRthedBv=np.zeros(Npts)
Sninwthe=np.zeros(Npts)
SninwthedBv=np.zeros(Npts)

for k in range(0,Npts):
  freq[k]=df_noise.iloc[Nvar*k,1]
  Vnin[k]=df_noise.iloc[Nvar*k+1,0]
  VnoutM1[k]=df_noise.iloc[Nvar*k+2,0]
  VnoutM1f[k]=df_noise.iloc[Nvar*k+3,0]
  VnoutM1w[k]=df_noise.iloc[Nvar*k+4,0]
  VnoutM2[k]=df_noise.iloc[Nvar*k+5,0]
  VnoutM2f[k]=df_noise.iloc[Nvar*k+6,0]
  VnoutM2w[k]=df_noise.iloc[Nvar*k+7,0]
  VnoutR1[k]=df_noise.iloc[Nvar*k+8,0]
  VnoutR2[k]=df_noise.iloc[Nvar*k+9,0]
  Vnout[k]=df_noise.iloc[Nvar*k+10,0]
  SnoutM[k]=VnoutM1[k]**2+VnoutM2[k]**2
  SnoutMf[k]=VnoutM1f[k]**2+VnoutM2f[k]**2
  SnoutMw[k]=VnoutM1w[k]**2+VnoutM2w[k]**2
  SnoutR[k]=VnoutR1[k]**2+VnoutR2[k]**2
  A[k]=Vnout[k]/Vnin[k]
  Snin[k]=Vnin[k]**2
  SnindBv[k]=10*log(Snin[k])
  SninM[k]=SnoutM[k]/A[k]**2
  SninMf[k]=SnoutMf[k]/A[k]**2
  SninMw[k]=SnoutMw[k]/A[k]**2
  SninMdBv[k]=10*log(SninM[k])
  SninMfdBv[k]=10*log(SninMf[k])
  SninMwdBv[k]=10*log(SninMw[k])
  SninR[k]=SnoutR[k]/A[k]**2
  SninRdBv[k]=10*log(SninR[k])
  Sninf[k]=SninMf[k]
  Sninw[k]=SninMw[k]+SninR[k]
  SninfdBv[k]=10*log(Sninf[k])
  SninwdBv[k]=10*log(Sninw[k])
  SninMwthe[k]=8*kT*gamman/Gmthe
  SninMwthedBv[k]=10*log(SninMwthe[k])
  SninRthe[k]=8*kT/(Gmthe**2*R)
  SninRthedBv[k]=10*log(SninRthe[k])
  Sninwthe[k]=SninMwthe[k]+SninRthe[k]
  SninwthedBv[k]=10*log(Sninwthe[k])

fmin=freq[0]
fmax=freq[Npts-1]

plt.semilogx(freq,SninMwthedBv,'b-', label='M1-M2 (theory)')
plt.semilogx(freq,SninRthedBv,'g-', label='R1-R2 (theory)')
plt.semilogx(freq,SninwthedBv,'r-', label='Total (theory)')
plt.semilogx(freq,SninMwdBv,'bo', label='M1-M2 (sim.)', markevery=40, markersize=4)
plt.semilogx(freq,SninRdBv,'go', label='R1-R2 (sim.)', markevery=40, markersize=4)
plt.semilogx(freq,SninwdBv,'ro', label='Total (sim.)', markevery=40, markersize=4)
plt.xlim(fmin,fmax)
plt.xticks([1e0,1e1,1e2,1e3,1e4,1e5,1e6,1e7,1e8,1e9])
plt.xlabel('Frequency [Hz]')
plt.ylim(-170,-155)
plt.yticks(range(-170,-150,5))
plt.ylabel('Input Noise PSD $[dBv/\\sqrt{Hz}]$')
plt.legend(loc='center left')
#plt.legend(loc='upper left', bbox_to_anchor=(1, 1))
textstr = '\n'.join((
  f'$S_{{nin,th}} =$ {SninMwthedBv[0]:.1f} $dBv/\\sqrt{{Hz}}$ M1-M2 (theory)',
  f'$S_{{nin,th}} =$ {SninMwdBv[0]:.1f} $dBv/\\sqrt{{Hz}}$ M1-M2 (sim.)',
  f'$S_{{nin,th}} =$ {SninRthedBv[0]:.1f} $dBv/\\sqrt{{Hz}}$ R1-R2 (theory)',
  f'$S_{{nin,th}} =$ {SninRdBv[0]:.1f} $dBv/\\sqrt{{Hz}}$ R1-R2 (sim.)',
  f'$S_{{nin,th}} =$ {SninwthedBv[0]:.1f} $dBv/\\sqrt{{Hz}}$ total (theory)',
  f'$S_{{nin,th}} =$ {SninwdBv[0]:.1f} $dBv/\\sqrt{{Hz}}$ total (sim.)'))
plt.text(1.05, 0.5, textstr, ha='left', va='center', size=textFontSize, transform=plt.gca().transAxes,
         bbox={'facecolor':'w', 'alpha':1.0, 'pad':5})
if savePlots: saveFigures(savePlotPath, otaName + '_input_white_noise_sim')
plt.show()
```

We see that the contribution of R~1~-R~2~ is `{python} f'{SninMwdBv[0]-SninRdBv[0]:.3f}'` $dB$ smaller than the contribution of M~1~-M~2~.

## Reducing the supply voltage
If we reduce the supply voltage keeping the same bias current, the $RI$ drop across the load resistor will not change and remains equal to $V_{DD}-V_{out} =$ `{python} f'{v_df.at['vdd', 'Voltage']-v_df.at['out1', 'Voltage']:.3f}'` $V$. If we keep the same input common-mode voltage $V_{ic} =$ `{python} f'{Vic:.3f}'` $V$, then the voltage at node 1 remains the same $V(1) =$ `{python} f'{v_df.at['1', 'Voltage']:.3f}'` $V$. The $V_{DS}$ voltage of M~1~-M~2~ for $V_{DD} =$ `{python} f'{v_df.at['vdd', 'Voltage']:.3f}'` $V$ is $V_{DS} =$ `{python} f'{v_df.at['out1', 'Voltage']-v_df.at['1', 'Voltage']:.3f}'` $V$. If we reduce the supply voltage to $V_{DD}=1\,V$, the $V_{DS}$ voltage gets negative and the transistors M~1~-M~2~ go out of saturation!

We need to reduce the input common mode voltage but leaving enough voltage at node 1 to keep the bias transistor in saturation. The $V_{GS}$ voltage M~1~-M~2~ is `{python} f'{v_df.at['in1', 'Voltage']-v_df.at['1', 'Voltage']:.3f}'` $V$. We therefore could reduce $V_{ic}$ to 0.6 $V$ which would leave  `{python} f'{0.6-v_df.at['in1', 'Voltage']+v_df.at['1', 'Voltage']:.3f}'` $V$ for the bias current source and increase the $V_{DS}$ voltage of M~1~-M~2~ to `{python} f'{v_df.at['out1', 'Voltage']-v_df.at['1', 'Voltage']-0.6:.3f}'` $V$. This should be just enough to keep M~1~-M~2~ in saturation owing to the fact that they are biased in weak inversion.

What this illustrates is that the maximum gain of a $G_m\,R$ gain stage is directly related to the supply voltage. Indeed, the voltage gain is given by
\begin{equation}
  A_{dc} = G_m\,R = \frac{G_m}{I_b} \cdot R\,I_b = \frac{G_m\,n U_T}{I_b} \cdot \frac{V_{DD}-V_{out}}{n U_T}.
\end{equation}
If we assume that both the current source transistor and the differential pair are biased in weak inversion for maximum current efficiency and minimum saturation voltage, we then have $G_m\,n U_T/I_b \cong 1$ and $V_{out,min} = 2 V_{DSsat} \cong 8 U_T$. The voltage gain is then given by
\begin{equation}
  A_{dc} \cong \frac{V_{DD}-2 V_{DSsat}}{n U_T} = \frac{V_{DD}/U_T - 8}{n}.
\end{equation}
The minimum supply voltage for a given voltage gain including the peak-to-peak output signal swing $2\Delta V_{out}$ is given by
\begin{equation}\label{eqn:VDDmin}
  V_{DD,min} \cong n U_T \cdot A_{dc} + 2 V_{DSsat} + 2 \Delta V_{out}.
\end{equation}
The minimum supply voltage given by \eqref{eqn:VDDmin} is plotted versus $A_{dc}$ in @fig-vddmin.

```{python}
#| label: fig-vddmin
#| fig-cap: Minimum supply voltage versus voltage gain.

Npts=101
n=1.3
nUT=n*UT
VDSsat=4*UT
DVout=2*UT
A=np.linspace(0,50,Npts,endpoint=True)
VDDmin=np.zeros(Npts)

for k in range(0,Npts):
    VDDmin[k]=nUT*A[k]+2*VDSsat+2*DVout

VDDspec=nUT*Adcspec+2*VDSsat+2*DVout

plt.plot([0,Adcspec], [VDDspec,VDDspec], '--k', linewidth=lw)
plt.plot([Adcspec,Adcspec],[0,VDDspec], '--k', linewidth=lw)
plt.plot(A, VDDmin, '-r')
plt.xlim(0,50)
#plt.xticks(np.arange(-10,12,2))
plt.xlabel('$A_{dc}$')
plt.ylim(0,2)
#plt.yticks(np.arange(-10,12,2))
plt.ylabel('$V_{DD,min}$ [V]')
#plt.grid(False)
#plt.legend(loc='lower left')
textstr = '\n'.join((
    f'$n =$ {n:.1f}',
    f'$n\,U_T =$ {nUT/1e-3:.1f} mV',
    f'$V_{{DSsat}} = 4\,U_T =$ {VDSsat/1e-3:.0f} mV',
    f'$\Delta V_{{out}} = 2\,U_T =$ {DVout/1e-3:.0f} mV',
    f'$V_{{DD,min}} =$ {VDDspec:.1f} V'))
plt.text(0.5, 0.05, textstr, ha='left', va='bottom', size=textFontSize, transform=plt.gca().transAxes,
         bbox={'facecolor':'w', 'alpha':1.0, 'pad':5})
if savePlots: saveFigures(savePlotPath, 'CMOS_inverter_large_signal')
plt.show()
```

From @fig-vddmin, we see that the minimum supply voltage to achieve the desired voltage gain $A_{dc} =$ `{python} f'{Adcspec:.0f}'` is $V_{DD,min} =$ `{python} f'{VDDspec:.1f}'` $V$, so close to 1 V.