---
title: |
  Fundamentals of Analog VLSI Design \
  Exercise 3 - Solution
subtitle: Design and Optimization of the Common-source Gain Stage
author: Christian Enz (christian.enz@epfl.ch)
affiliation: EPFL
date: 10-01-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 3]{Exercise 3}
        \lofoot[$\copyright$ C. Enz]{$\copyright$ C. Enz}
        \cofoot[Fundamentals of Analog VLSI Design]{Fundamentals of Analog VLSI Design}
        \rofoot[01.10.2025]{01.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 = "csamp"
simulationPath = "./Simulations/ngspice/"
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 {-}

![Common-source (CS) gain stage.](Figures/CS_schematic.png){#fig-CS_schematic}

In this exercise we want to apply the $G_m/I_D$ design methodology using the inversion coefficient $IC$ to the simple common source (CS) amplifier shown in @fig-CS_schematic for various specifications. For the design we assume a generic 180 nm 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. 

::: {.callout-important}
For this process, the transistor dimensions are rounded to 10nm.
:::

```{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}$"],
         ["$H_{dif}$",f'{Hdif/1e-9:.0f}',"$nm$"],
         ["$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'{KFp:.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")
))
```

# Problem 1: Imposing the gain-bandwidth product (unity gain frequency)

In this first example we will design the CS gain stage shown in @fig-CS_schematic for the specifications at room temperature given in @tbl-specs_problem1 by imposing the inversions coefficient and the load capacitance.

## Analysis

![Small-signal schematic of the common-source (CS).](Figures/CS_small_signal.png){#fig-CS_small_signal}

The small-signal schematic of the CS amplifier is shown in @fig-CS_small_signal. The transfer function is given by
\begin{equation}
  H(s) \triangleq \frac{\Delta V_{out}}{\Delta V_{in}} = \frac{A_{dc}}{1+\frac{s}{\omega_c}},
\end{equation}
where $A_{dc} = -G_m/G_{ds}$ is the dc gain and $\omega_c = G_{ds}/C_L$ the cut-off frequency.

The magnitude of the frequency response is then given by
\begin{equation}
  |H(\omega)| = \frac{|A_{dc}|}{\sqrt{1+\left(\frac{\omega}{\omega_c}\right)^2}}
  \cong \frac{\omega_u}{\omega}
\end{equation}
for $\omega_c \ll \omega$ where
\begin{equation}
  \omega_u = |A_{dc}| \cdot \omega_c = \frac{G_m}{C_L}
\end{equation}
is the unity-gain frequency or gain-bandwidth product.

## Specifications
The specifications for Problem 1 are given in @tbl-specs_problem1.

```{python}
#| label: tbl-specs_problem1
#| tbl-cap: Specifications for problem 1.

from specs_problem1 import *

L=1e-6
Leff=L+DLn

table = [["Gain-bandwidth product","$GBW$",f'{GBWspec/1e6:.0f}',"$MHz$"],
         ["Inversion coefficient","$IC$",f'{IC:.0f}',"-"],
         ["Load capacitance","$C_L$",f'{CL/1e-12:.0f}',"$pF$"]
        ]

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

## Long-channel case
### Design

```{python}
Gm=2*pi*GBWspec*CL
gms_over_id=gmsid_ic(IC)
Ib=Gm*n0n*UT/gms_over_id
Ispec=Ib/IC
WoverL=Ispec/Ispecsqn
Weff=WoverL*Leff
W=Weff-DWn
W=round(W*1e8)/1e8
Weff=W+DWn
Gds=Ib/(lambdan*Leff)
Adcthe=-Gm/Gds
AdcdBthe=20*log(abs(Adcthe))
AD=2*Hdif*Weff
PD=2*(2*Hdif+Weff)
CDBJ=AD*CJn+PD*CJSWn
CGDo=Weff*CGDOn
CGSo=Weff*CGSOn
CD=CGDo+CDBJ
CL0=CL-CD
```

For the long-channel case, we take $L =$ `{python} f'{L/1e-6:.0f}'` $\mu m$ which corresponds to an effective length $L_{eff} =$ `{python} f'{Leff/1e-9:.0f}'` $nm$.

The gain-badwidth product $GBW$ and the load capacitance $C_L$ set the required transconductance as $G_m =$ `{python} f'{Gm/1e-6:.3f}'` $\mu A/V$. Assuming a long-channel transistor we can neglect the effect of velocity saturation and calculate the current efficiency for the chosen inversion coefficient as $G_m\,n U_T/I_D =$ `{python} f'{gms_over_id:.3f}'`, from which we get $I_b =$ `{python} f'{Ib/1e-6:.3f}'` $\mu A$. From the bias current and the inversion coefficient we get the specific current as $I_{spec} =$ `{python} f'{Ispec/1e-6:.3f}'` $\mu A$ and the aspect ratio $W/L =$ `{python} f'{WoverL:.3f}'`. Knowing the length, we get the effective width $W_{eff} =$ `{python} f'{Weff/1e-6:.3f}'` $\mu m$ and drawn width $W =$ `{python} f'{W/1e-6:.2f}'` $\mu m$.

We can check what is the resulting dc gain by estimating the output conductance $G_{ds} = I_b/(\lambda_n\,L) =$ `{python} f'{Gds/1e-6:.3f}'` $\mu A/V$. The resulting DC gain is then $A_{dc} = G_m/G_{ds} =$ `{python} f'{Adcthe:.3f}'` or `{python} f'{AdcdBthe:.3f}'` dB.

We also calculate the area and perimeter of the drain to estimate the junction capacitance and overlap capacitance in order to correct $C_L$ to get the correct $GBW$. This results in $C_{DBJ} =$ `{python} f'{CDBJ/1e-15:.1f}'` $fF$, $C_{GD} =$ `{python} f'{CGDo/1e-15:.1f}'` $fF$, $C_D =$ `{python} f'{CD/1e-15:.1f}'` $fF$. To account for the parasitic capacitance at the drain $C_D =$ `{python} f'{CD/1e-15:.1f}'` $fF$, we need to reduce the load capacitance. The effective load capacitance that needs to be added is then $C_{L0} =$ `{python} f'{CL0/1e-12:.3f}'` $pF$ instead of $C_L =$ `{python} f'{CL/1e-12:.3f}'` $pF$.

### Simulations

![Schematic of the common-source (CS) gain stage used for simulation.](Figures/CS_sim_sch.png){#fig-CS_sim_sch}

We can now check whether we get the correct GBW and DC gain using ngspice simulations with the schematic shown in @fig-CS_sim_sch, where transistor $M_2$ is used to correctly bias the gate of the CS transistor $M_1$ so that it draws the desired bias current $I_b$.

The simulations will be performed with the following parameters:

```{python}
paramstr = '\n'.join((
  f'.param VDD={VDD:.1f} Ib={Ib/1e-6:.3f}u',
  f'.param CL={CL0/1e-15:.2f}f W={W/1e-6:.2f}u L={L/1e-6:.2f}u AD={AD:.2e} PD={PD:.2e}',
#  f'.param CL={CL0/1e-15:.2f}f W={Wopt/1e-6:.2f}u L={L/1e-6:.2f}u' 
  ))
print(paramstr)
with open(simulationPath+'size_bias.par', 'w') as f:
  f.write(paramstr)

Ndec=101
fminsim=1e3
fmaxsim=1e9

simpar = '\n'.join((
  f'.csparam Ndec={Ndec:.0f}',
  f'.csparam fmin={fminsim:.3e}',
  f'.csparam fmax={fmaxsim:.3e}'
))
simparFile = simulationPath + "simulation.ac.par"
with open(simparFile, 'w') as f:
  f.write(simpar)
```

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

```{python}
#| label: tbl-problem1_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"

result = subprocess.run(f"""ngspice_con -b -o "{simulationLog}" "{simulationFile}" """, shell=True, capture_output=True, text=True)

nodeList=["vdd","in","out","1","2"]
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))

Voutq=v_df.loc['out','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")
))
```

The operating information for each transistor can be extracted and are given in @tbl-problem1_op1 and @tbl-problem1_op2.

```{python}
#| label: tbl-problem1_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-problem1_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")
))
```

We see that the simulated transconductance of M~1~ $G_m =$ `{python} f'{ekv2p6mos_df.at["M1","Gm"]/1e-6:.3f}'` $\mu A/V$ is slightly lower than the theoretical estimation $G_m =$ `{python} f'{Gm/1e-6:.3f}'` $\mu A/V$.

We can now proceed with the simulation of the transfer function which is shown in @fig-cs_gbw_long_nozero.

```{python}
#| label: fig-cs_gbw_long_nozero
#| fig-cap: Simulated gain response compared to theoretical estimation for the long-channel case.

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

def Av2(s,Adc,wp,wz):
  num=num=Adc*(1-s/wz)
  den=1+s/wp
  return num/den

circuitName = "csamp"
simtype = ".ac"
fileName = circuitName + simtype
simulationFile = simulationPath + fileName + ".cir"
simulationLog = simulationPath + fileName + ".log"
simulationData = simulationPath + fileName + ".dat"

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]

Avphidegsim=Avphidegsim-180

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:
      fcsim=float(match.group(1))

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

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

fthe=fsim
GBWthe=GBWspec
fcthe=GBWthe/abs(Adcthe)
wcthe=2*pi*fcthe

AvmagdBthe1=np.zeros(Npts)
Avphidegthe1=np.zeros(Npts)
AvmagdBthe2=np.zeros(Npts)
Avphidegthe2=np.zeros(Npts)
for k in range(0,Npts):
  jw=1j*2*pi*fthe[k]
  AvmagdBthe1[k]=20*log(abs(Av1(jw,Adcthe,wcthe)))
  Avphidegthe1[k]=np.angle(Av1(jw,Adcthe,wcthe),deg=True)

Avphidegthe1=Avphidegthe1-360

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

axs[0].semilogx([fmin,GBWsim],[0,0],'k--', linewidth=lw)
axs[0].semilogx([GBWsim,GBWsim],[0,-60],'k--', linewidth=lw)
axs[0].semilogx(fsim,AvmagdBsim,'ro', markersize=4, markevery=15, label='Simulation')
axs[0].semilogx(fthe,AvmagdBthe1,'r-',label='Theory')
axs[0].set(xlim=(fmin,fmax), ylim=(-40,60), ylabel='$|A_{vd}|$ [dB]')
#axs[0].set_yticks([-25,-20,-15,-10,-5,0,5,10,15,20,25])
axs[0].legend(loc='lower left')
textstr1 = '\n'.join((
  f'$A_{{dc}} =$ {AdcdBthe:.1f} dB (theory)',
  f'$A_{{dc}} =$ {AdcdBsim:.1f} dB (sim)',
  f'$GBW =$ {GBWthe/1e6:.1f} MHz (theory)',
  f'$GBW =$ {GBWsim/1e6:.1f} MHz (sim.)',
  f'$f_c =$ {fcthe/1e3:.1f} kHz (theory)',
  f'$f_c =$ {fcsim/1e3:.1f} kHz (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,GBWsim],[PGBWsim,PGBWsim],'k--', linewidth=lw)
#axs[1].semilogx([GBWsim,GBWsim],[PGBWsim,0],'k--', linewidth=lw)
axs[1].semilogx(fsim,Avphidegsim,'bo', markersize=4, markevery=15, label='Simulation')
axs[1].semilogx(fthe,Avphidegthe1,'b-',label='Theory')
axs[1].set(xlim=(fmin,fmax), ylim=(-360,-180), xlabel='Frequency [Hz]', ylabel='$arg\\{A_{vd}\\}$ [degree]')
axs[1].set_xticks([1e3,1e4,1e5,1e6,1e7,1e8,1e9])
axs[1].set_yticks([-360,-315,-270,-225,-180])
axs[1].legend(loc='lower left')
textstr2 = '\n'.join((
  f'$L =$ {L/1e-6:.0f} $\\mu m$',
  f'$W =$ {W/1e-6:.2f} $\\mu m$',
  f'$IC =$ {IC:.0f}',
  f'$I_b =$ {Ib/1e-6:.3f} $\\mu A$',
  f'$C_{{L0}} =$ {CL0/1e-12:.3f} $pF$',
  f'$C_L =$ {CL/1e-12:.0f} $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-cs_gbw_long_nozero, we see that the gain-bandwidth product $GBW =$ `{python} f'{GBWsim/1e6:.1f}'` $MHz$ is slightly lower than the specifications $GBW =$ `{python} f'{GBWspec/1e6:.0f}'` $MHz$ despite we have accounted for the junction capacitance at the output. This comes from the fact that the simulated transconductance $G_m =$ `{python} f'{ekv2p6mos_df.at["M1","Gm"]/1e-6:.3f}'` $\mu A/V$ is slightly lower than the theoretical estimation $G_m =$ `{python} f'{Gm/1e-6:.3f}'` $\mu A/V$.

We also see that there is some discrepancy of the phase at high frequancy (above the $GBW$). This coming from the gate-to-drain capacitance (the overla capacitance $C_{GDo}$) which introduces a positive zero. We can add this zero in the theoretical transfer leading to the transfer function magnitude and phase are plotted in @fig-cs_gbw_long. We now see that we have a better fit at high frequency (dashed blue line). The gate-to-drain capacitance seems to be larger.

```{python}
#| label: fig-cs_gbw_long
#| fig-cap: Simulated gain response compared to theoretical estimation for the long-channel case.

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

def Av2(s,Adc,wp,wz):
  num=num=Adc*(1-s/wz)
  den=1+s/wp
  return num/den

circuitName = "csamp"
simtype = ".ac"
fileName = circuitName + simtype
simulationFile = simulationPath + fileName + ".cir"
simulationLog = simulationPath + fileName + ".log"
simulationData = simulationPath + fileName + ".dat"

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]

Avphidegsim=Avphidegsim-180

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:
      fcsim=float(match.group(1))

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

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

fthe=fsim
fcthe=GBWthe/abs(Adcthe)
wcthe=2*pi*fcthe
#CF=14e-15
CF=CGDo
wzthe=Gm/CF

AvmagdBthe1=np.zeros(Npts)
Avphidegthe1=np.zeros(Npts)
AvmagdBthe2=np.zeros(Npts)
Avphidegthe2=np.zeros(Npts)
for k in range(0,Npts):
  jw=1j*2*pi*fthe[k]
  AvmagdBthe1[k]=20*log(abs(Av1(jw,Adcthe,wcthe)))
  Avphidegthe1[k]=np.angle(Av1(jw,Adcthe,wcthe),deg=True)
  AvmagdBthe2[k]=20*log(abs(Av2(jw,Adcthe,wcthe,wzthe)))
  Avphidegthe2[k]=np.angle(Av2(jw,Adcthe,wcthe,wzthe),deg=True)

Avphidegthe1=Avphidegthe1-360
Avphidegthe2=Avphidegthe2-360

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

axs[0].semilogx([fmin,GBWsim],[0,0],'k--', linewidth=lw)
axs[0].semilogx([GBWsim,GBWsim],[0,-60],'k--', linewidth=lw)
axs[0].semilogx(fsim,AvmagdBsim,'ro', markersize=4, markevery=15, label='Simulation')
axs[0].semilogx(fthe,AvmagdBthe1,'r-',label='Theory')
axs[0].semilogx(fthe,AvmagdBthe2,'r--',label='Theory (with zero)')
axs[0].set(xlim=(fmin,fmax), ylim=(-40,60), ylabel='$|A_{vd}|$ [dB]')
#axs[0].set_yticks([-25,-20,-15,-10,-5,0,5,10,15,20,25])
axs[0].legend(loc='lower left')
textstr1 = '\n'.join((
  f'$A_{{dc}} =$ {AdcdBthe:.1f} dB (theory)',
  f'$A_{{dc}} =$ {AdcdBsim:.1f} dB (sim)',
  f'$GBW =$ {GBWthe/1e6:.1f} MHz (theory)',
  f'$GBW =$ {GBWsim/1e6:.1f} MHz (sim.)',
  f'$f_c =$ {fcthe/1e3:.1f} kHz (theory)',
  f'$f_c =$ {fcsim/1e3:.1f} kHz (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,GBWsim],[PGBWsim,PGBWsim],'k--', linewidth=lw)
#axs[1].semilogx([GBWsim,GBWsim],[PGBWsim,0],'k--', linewidth=lw)
axs[1].semilogx(fsim,Avphidegsim,'bo', markersize=4, markevery=15, label='Simulation')
axs[1].semilogx(fthe,Avphidegthe1,'b-',label='Theory')
axs[1].semilogx(fthe,Avphidegthe2,'b--',label='Theory (with zero)')
axs[1].set(xlim=(fmin,fmax), ylim=(-360,-180), xlabel='Frequency [Hz]', ylabel='$arg\\{A_{vd}\\}$ [degree]')
axs[1].set_xticks([1e3,1e4,1e5,1e6,1e7,1e8,1e9])
axs[1].set_yticks([-360,-315,-270,-225,-180])
axs[1].legend(loc='lower left')
textstr2 = '\n'.join((
  f'$L =$ {L/1e-6:.0f} $\\mu m$',
  f'$W =$ {W/1e-6:.2f} $\\mu m$',
  f'$IC =$ {IC:.0f}',
  f'$I_b =$ {Ib/1e-6:.3f} $\\mu A$',
  f'$C_{{L0}} =$ {CL0/1e-12:.3f} $pF$',
  f'$C_L =$ {CL/1e-12:.3f} $pF$',
  f'$C_{{GD}} =$ {CGDo/1e-15:.3f} $fF$'))
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)
```

```{python}
Ib=3.5e-6
```

If we want to meet the specifications we can slightly increase the bias current to $I_b =$ `{python} f'{Ib/1e-6:.1f}'` $\mu A$. The transfer function is now given in @fig-cs_gbw_long2. We see that we are now right on spec.

```{python}
#| label: fig-cs_gbw_long2
#| fig-cap: Simulated gain response compared to theoretical estimation for the long-channel case.

circuitName = "csamp"
simtype = ".ac"
fileName = circuitName + simtype
simulationFile = simulationPath + fileName + ".cir"
simulationLog = simulationPath + fileName + ".log"
simulationData = simulationPath + fileName + ".dat"

paramstr = '\n'.join((
  f'.param VDD={VDD:.1f} Ib={Ib/1e-6:.3f}u',
  f'.param CL={CL0/1e-15:.2f}f W={W/1e-6:.2f}u L={L/1e-6:.2f}u AD={AD:.2e} PD={PD:.2e}',
#  f'.param CL={CL0/1e-15:.2f}f W={Wopt/1e-6:.2f}u L={L/1e-6:.2f}u' 
  ))
#print(paramstr)
with open(simulationPath+'size_bias.par', 'w') as f:
  f.write(paramstr)

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]

Avphidegsim=Avphidegsim-180

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:
      fcsim=float(match.group(1))

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

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

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

axs[0].semilogx([fmin,GBWsim],[0,0],'k--', linewidth=lw)
axs[0].semilogx([GBWsim,GBWsim],[0,-60],'k--', linewidth=lw)
axs[0].semilogx(fsim,AvmagdBsim,'ro', markersize=4, markevery=15, label='Simulation')
axs[0].semilogx(fthe,AvmagdBthe1,'r-',label='Theory')
axs[0].semilogx(fthe,AvmagdBthe2,'r--',label='Theory (with zero)')
axs[0].set(xlim=(fmin,fmax), ylim=(-40,60), ylabel='$|A_{vd}|$ [dB]')
#axs[0].set_yticks([-25,-20,-15,-10,-5,0,5,10,15,20,25])
axs[0].legend(loc='lower left')
textstr1 = '\n'.join((
  f'$A_{{dc}} =$ {AdcdBthe:.1f} dB (theory)',
  f'$A_{{dc}} =$ {AdcdBsim:.1f} dB (sim)',
  f'$GBW =$ {GBWthe/1e6:.1f} MHz (theory)',
  f'$GBW =$ {GBWsim/1e6:.1f} MHz (sim.)',
  f'$f_c =$ {fcthe/1e3:.1f} kHz (theory)',
  f'$f_c =$ {fcsim/1e3:.1f} kHz (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,GBWsim],[PGBWsim,PGBWsim],'k--', linewidth=lw)
#axs[1].semilogx([GBWsim,GBWsim],[PGBWsim,0],'k--', linewidth=lw)
axs[1].semilogx(fsim,Avphidegsim,'bo', markersize=4, markevery=15, label='Simulation')
axs[1].semilogx(fthe,Avphidegthe1,'b-',label='Theory')
axs[1].semilogx(fthe,Avphidegthe2,'b--',label='Theory (with zero)')
axs[1].set(xlim=(fmin,fmax), ylim=(-360,-180), xlabel='Frequency [Hz]', ylabel='$arg\\{A_{vd}\\}$ [degree]')
axs[1].set_xticks([1e3,1e4,1e5,1e6,1e7,1e8,1e9])
axs[1].set_yticks([-360,-315,-270,-225,-180])
axs[1].legend(loc='lower left')
textstr2 = '\n'.join((
  f'$L =$ {L/1e-6:.0f} $\\mu m$',
  f'$W =$ {W/1e-6:.2f} $\\mu m$',
  f'$IC =$ {IC:.0f}',
  f'$I_b =$ {Ib/1e-6:.3f} $\\mu A$',
  f'$C_{{L0}} =$ {CL0/1e-12:.3f} $pF$',
  f'$C_L =$ {CL/1e-12:.3f} $pF$',
  f'$C_{{GD}} =$ {CGDo/1e-15:.3f} $fF$'))
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)
```

```{=latex}
\newpage
```

## Short-channel case (including velocity saturation)
### Design

```{python}
L=180e-9
Leff=L+DLn
lambdac=Lsatn/Leff
gms_over_id=gmsid_ic_short(IC,lambdac)
Ib=Gm*n0n*UT/gms_over_id
Ispec=Ib/IC
WoverL=Ispec/Ispecsqn
Weff=WoverL*Leff
W=Weff-DWn
W=round(W*1e8)/1e8
Weff=W+DWn
Gds=Ib/(lambdan*Leff)
Adcthe=-Gm/Gds
AdcdBthe=20*log(abs(Adcthe))
AD=2*Hdif*Weff
PD=2*(2*Hdif+Weff)
#PD=4*Hdif+Weff
CDBJ=AD*CJn+PD*CJSWn
CGDo=Weff*CGDOn
CGSo=Weff*CGSOn
CD=CGDo+CDBJ
CL0=CL-CD
```

The required transconductance hasn't changed because it is set by the desired gain-bandwidth product and load capacitance $G_m =$ `{python} f'{Gm/1e-6:.3f}'` $\mu A/V$.

For the short-channel case we will use the shortest channel length for this technology, namely $L =$ `{python} f'{L/1e-6:.2f}'` $\mu m$ corresponding to an effective length $L_{eff} =$ `{python} f'{Leff/1e-9:.0f}'` $nm$. The velocity parameter for the chosen length is then given by $\lambda_c = L_{satn}/L_{eff} =$ `{python} f'{lambdac:.3f}'`. The procedure is then the same: we first get the bias current from the current efficiency but including the effect of velocity saturation $G_m\,n U_T/I_D =$ `{python} f'{gms_over_id:.3f}'`. We see that the current efficiency $G_m\,n U_T/I_D$ is only slightly smaller than for the long-channel case due to velocity saturation. The bias current is then given by $I_b =$ `{python} f'{Ib/1e-6:.3f}'` $\mu A$. Despite we use a shorter channel-length, the required bias current is about the same than the one from the long-channel case. This is simply due to the fact the effect of velocity saturation is small in moderate and hence has little effect on the current efficiency.

From the bias current and the inversion coefficient we get the specific current as $I_{spec} =$ `{python} f'{Ispec/1e-6:.3f}'` $\mu A$ and the aspect ratio $W_{eff}/L_{eff} =$ `{python} f'{WoverL:.3f}'`. For the chosen length we get the effective width $W_{eff} =$ `{python} f'{Weff/1e-6:.3f}'` $\mu m$ and drawn width $W =$ `{python} f'{W/1e-6:.2f}'` $\mu m$.

We can check what is the resulting dc gain by estimating the output conductance $G_{ds} = I_b/(\lambda_n\,L_{eff}) =$ `{python} f'{Gds/1e-6:.3f}'` $\mu A/V$. The DC gain is now $A_{dc} =$ `{python} f'{Adcthe:.3f}'` or `{python} f'{AdcdBthe:.3f}'` $dB$, which, as expected, is significantly lower than for the long-channel case.

We also calculate the area and perimeter of the drain to estimate the junction capacitance and overlap capacitance in order to correct $C_L$ to get the correct $GBW$. This results in $C_{DBJ} =$ `{python} f'{CDBJ/1e-15:.0f}'` $fF$, $C_{GD} =$ `{python} f'{CGDo/1e-15:.3f}'` $fF$, $C_D =$ `{python} f'{CD/1e-15:.0f}'` $fF$. The effective load capacitance that needs to be added is then $C_{L0} =$ `{python} f'{CL0/1e-12:.3f}'` $pF$.

### Simulations
We can check whether we get the correct GBW and dc gain using simulations with the following values of the parameters:


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

```{python}
#| label: tbl-problem1_voltages_short
#| 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"

result = subprocess.run(f"""ngspice_con -b -o "{simulationLog}" "{simulationFile}" """, shell=True, capture_output=True, text=True)

nodeList=["vdd","in","out","1","2"]
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))

Voutq=v_df.loc['out','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")
))
```

The operating information for each transistor can be extracted and are given in @tbl-problem1_op1_short and @tbl-problem1_op2_short.

```{python}
#| label: tbl-problem1_op1_short
#| 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-problem1_op2_short
#| 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")
))
```

We see that the simulated transconductance of M~1~ $G_m =$ `{python} f'{ekv2p6mos_df.at["M1","Gm"]/1e-6:.3f}'` $\mu A/V$ is slightly larger than the theoretical estimation $G_m =$ `{python} f'{Gm/1e-6:.3f}'` $\mu A/V$.


```{python}
circuitName = "csamp"
simtype = ".op"
fileName = circuitName + simtype
simulationPath = "./Simulations/ngspice/"

paramstr = '\n'.join((
  f'.param VDD={VDD:.1f} Ib={Ib/1e-6:.3f}u',
  f'.param CL={CL0/1e-15:.0f}f W={W/1e-6:.2f}u L={L/1e-6:.2f}u AD={AD:.2e} PD={PD:.2e}',
#  f'.param CL={CL0/1e-15:.2f}f W={Wopt/1e-6:.2f}u L={L/1e-6:.2f}u' 
  ))
print(paramstr)
with open(simulationPath+'size_bias.par', 'w') as f:
  f.write(paramstr)

Ndec=101
fminsim=1e3
fmaxsim=1e9

simpar = '\n'.join((
  f'.csparam Ndec={Ndec:.0f}',
  f'.csparam fmin={fminsim:.3e}',
  f'.csparam fmax={fmaxsim:.3e}'
))
simparFile = simulationPath + "simulation.ac.par"
with open(simparFile, 'w') as f:
  f.write(simpar)
```

The transfer function magnitude and phase are plotted in @fig-cs_gbw_short, which shows a reasonable fit between the simulation and the theoretical estimation. The simulated gain-bandwidth product $GBW$ is slightly lower than target and the simulated DC gain is slightly larger than the theoretical estimation.

```{python}
#| label: fig-cs_gbw_short
#| fig-cap: Simulated gain response compared to theoretical estimation for the short-channel case.

circuitName = "csamp"
simtype = ".ac"
fileName = circuitName + simtype
simulationFile = simulationPath + fileName + ".cir"
simulationLog = simulationPath + fileName + ".log"
simulationData = simulationPath + fileName + ".dat"

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]

Avphidegsim=Avphidegsim-180

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:
      fcsim=float(match.group(1))

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

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

fthe=fsim
GBWthe=GBWspec
fcthe=GBWthe/abs(Adcthe)
wcthe=2*pi*fcthe

AvmagdBthe1=np.zeros(Npts)
Avphidegthe1=np.zeros(Npts)
AvmagdBthe2=np.zeros(Npts)
Avphidegthe2=np.zeros(Npts)
for k in range(0,Npts):
  jw=1j*2*pi*fthe[k]
  AvmagdBthe1[k]=20*log(abs(Av1(jw,Adcthe,wcthe)))
  Avphidegthe1[k]=np.angle(Av1(jw,Adcthe,wcthe),deg=True)

Avphidegthe1=Avphidegthe1-360

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

axs[0].semilogx([fmin,GBWsim],[0,0],'k--', linewidth=lw)
axs[0].semilogx([GBWsim,GBWsim],[0,-60],'k--', linewidth=lw)
axs[0].semilogx(fsim,AvmagdBsim,'ro', markersize=4, markevery=15, label='Simulation')
axs[0].semilogx(fthe,AvmagdBthe1,'r-',label='Theory')
axs[0].set(xlim=(fmin,fmax), ylim=(-40,40), ylabel='$|A_{vd}|$ [dB]')
#axs[0].set_yticks([-25,-20,-15,-10,-5,0,5,10,15,20,25])
axs[0].legend(loc='lower left')
textstr1 = '\n'.join((
  f'$A_{{dc}} =$ {AdcdBthe:.1f} dB (theory)',
  f'$A_{{dc}} =$ {AdcdBsim:.1f} dB (sim)',
  f'$GBW =$ {GBWthe/1e6:.1f} MHz (theory)',
  f'$GBW =$ {GBWsim/1e6:.1f} MHz (sim.)',
  f'$f_c =$ {fcthe/1e3:.1f} kHz (theory)',
  f'$f_c =$ {fcsim/1e3:.1f} kHz (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,GBWsim],[PGBWsim,PGBWsim],'k--', linewidth=lw)
#axs[1].semilogx([GBWsim,GBWsim],[PGBWsim,0],'k--', linewidth=lw)
axs[1].semilogx(fsim,Avphidegsim,'bo', markersize=4, markevery=15, label='Simulation')
axs[1].semilogx(fthe,Avphidegthe1,'b-',label='Theory')
axs[1].set(xlim=(fmin,fmax), ylim=(-270,-180), xlabel='Frequency [Hz]', ylabel='$arg\\{A_{vd}\\}$ [degree]')
axs[1].set_xticks([1e3,1e4,1e5,1e6,1e7,1e8,1e9])
axs[1].set_yticks([-270,-225,-180])
axs[1].legend(loc='lower left')
textstr2 = '\n'.join((
  f'$L =$ {L/1e-6:.0f} $\\mu m$',
  f'$W =$ {W/1e-6:.2f} $\\mu m$',
  f'$IC =$ {IC:.0f}',
  f'$I_b =$ {Ib/1e-6:.3f} $\\mu A$',
  f'$C_{{L0}} =$ {CL0/1e-12:.3f} $pF$',
  f'$C_L =$ {CL/1e-12:.0f} $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)
```

```{python}
Ib=4e-6
```

From @fig-cs_gbw_short, we see that the simulated DC gain is slightly larger than the theoretical estimation. We also see that the gain-bandwidth product specs is not met. If we want to meet the specifications we can slightly increase the bias current to $I_b =$ `{python} f'{Ib/1e-6:.1f}'` $\mu A$. The transfer function is now given in @fig-cs_gbw_short2. We see that we are now right on spec.

::: {.callout-note}
We also see that for the short-channel case, we don't see the zero which has moved to much higher frequency. This is due to the much smaller transistor width which significantly reduces the gate-to-drain capacitance.
:::

```{python}
#| label: fig-cs_gbw_short2
#| fig-cap: Simulated gain response compared to theoretical estimation for the short-channel case.

circuitName = "csamp"
simtype = ".ac"
fileName = circuitName + simtype
simulationFile = simulationPath + fileName + ".cir"
simulationLog = simulationPath + fileName + ".log"
simulationData = simulationPath + fileName + ".dat"

paramstr = '\n'.join((
  f'.param VDD={VDD:.1f} Ib={Ib/1e-6:.3f}u',
  f'.param CL={CL0/1e-15:.2f}f W={W/1e-6:.2f}u L={L/1e-6:.2f}u AD={AD:.2e} PD={PD:.2e}',
#  f'.param CL={CL0/1e-15:.2f}f W={Wopt/1e-6:.2f}u L={L/1e-6:.2f}u' 
  ))
#print(paramstr)
with open(simulationPath+'size_bias.par', 'w') as f:
  f.write(paramstr)

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]

Avphidegsim=Avphidegsim-180

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:
      fcsim=float(match.group(1))

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

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

fthe=fsim
GBWthe=GBWspec
fcthe=GBWthe/abs(Adcthe)
wcthe=2*pi*fcthe

AvmagdBthe1=np.zeros(Npts)
Avphidegthe1=np.zeros(Npts)
AvmagdBthe2=np.zeros(Npts)
Avphidegthe2=np.zeros(Npts)
for k in range(0,Npts):
  jw=1j*2*pi*fthe[k]
  AvmagdBthe1[k]=20*log(abs(Av1(jw,Adcthe,wcthe)))
  Avphidegthe1[k]=np.angle(Av1(jw,Adcthe,wcthe),deg=True)

Avphidegthe1=Avphidegthe1-360

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

axs[0].semilogx([fmin,GBWsim],[0,0],'k--', linewidth=lw)
axs[0].semilogx([GBWsim,GBWsim],[0,-60],'k--', linewidth=lw)
axs[0].semilogx(fsim,AvmagdBsim,'ro', markersize=4, markevery=15, label='Simulation')
axs[0].semilogx(fthe,AvmagdBthe1,'r-',label='Theory')
axs[0].set(xlim=(fmin,fmax), ylim=(-40,40), ylabel='$|A_{vd}|$ [dB]')
#axs[0].set_yticks([-25,-20,-15,-10,-5,0,5,10,15,20,25])
axs[0].legend(loc='lower left')
textstr1 = '\n'.join((
  f'$A_{{dc}} =$ {AdcdBthe:.1f} dB (theory)',
  f'$A_{{dc}} =$ {AdcdBsim:.1f} dB (sim)',
  f'$GBW =$ {GBWthe/1e6:.1f} MHz (theory)',
  f'$GBW =$ {GBWsim/1e6:.1f} MHz (sim.)',
  f'$f_c =$ {fcthe/1e3:.1f} kHz (theory)',
  f'$f_c =$ {fcsim/1e3:.1f} kHz (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,GBWsim],[PGBWsim,PGBWsim],'k--', linewidth=lw)
#axs[1].semilogx([GBWsim,GBWsim],[PGBWsim,0],'k--', linewidth=lw)
axs[1].semilogx(fsim,Avphidegsim,'bo', markersize=4, markevery=15, label='Simulation')
axs[1].semilogx(fthe,Avphidegthe1,'b-',label='Theory')
axs[1].set(xlim=(fmin,fmax), ylim=(-270,-180), xlabel='Frequency [Hz]', ylabel='$arg\\{A_{vd}\\}$ [degree]')
axs[1].set_xticks([1e3,1e4,1e5,1e6,1e7,1e8,1e9])
axs[1].set_yticks([-270,-225,-180])
axs[1].legend(loc='lower left')
textstr2 = '\n'.join((
  f'$L =$ {L/1e-6:.0f} $\\mu m$',
  f'$W =$ {W/1e-6:.2f} $\\mu m$',
  f'$IC =$ {IC:.0f}',
  f'$I_b =$ {Ib/1e-6:.3f} $\\mu A$',
  f'$C_{{L0}} =$ {CL0/1e-12:.3f} $pF$',
  f'$C_L =$ {CL/1e-12:.0f} $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)
```


# Problem 2: Imposing the power consumption
## Specifications
We now impose the power consumption, the inversion coefficient and the load capacitance according to the specifications given in @tbl-specs_problem2.

```{python}
#| label: tbl-specs_problem2
#| tbl-cap: Specifications for problem 2.

from specs_problem2 import *

L=1e-6
Leff=L+DLn

table = [["Power consumption","$P$",f'{P/1e-6:.0f}',"$\mu W$"],
         ["Inversion coefficient","$IC$",f'{IC:.0f}',"-"],
         ["Load capacitance","$C_L$",f'{CL/1e-12:.0f}',"$pF$"],
         ]

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

## Long-channel case
### Design

```{python}
Ib=P/VDD
Ispec=Ib/IC
gms=gms_ic(IC)
Gm=gms*Ispec/(n0n*UT)
GBWthe=Gm/(2*pi*CL)
WoverL=Ispec/Ispecsqn
Weff=WoverL*Leff
W=Weff-DWn
W=round(W*1e8)/1e8
Weff=W+DWn
Gds=Ib/(lambdan*Leff)
Adcthe=-Gm/Gds
AdcdBthe=20*log(abs(Adcthe))
AD=2*Hdif*Weff
PD=2*(2*Hdif+Weff)
CDBJ=AD*CJn+PD*CJSWn
CGDo=W*CGDOn
CGSo=W*CGSOn
CD=CGDo+CDBJ
CL0=CL-CD
```

For the long-channel case, we choose $L =$ `{python} f'{L/1e-6:.0f}'` $\mu m$ which corresponds to an effective width $L_{eff} =$ `{python} f'{Leff/1e-6:.3f}'` $\mu m$. The maximum available current is directly given by $I_b = P/V_{DD} =$ `{python} f'{Ib/1e-6:.3f}'` $\mu A$. Since the inversion coefficient is chosen as $IC =$ `{python} f'{IC:.0f}'`, we get the specific current as $I_{spec} =$ `{python} f'{Ispec/1e-6:.3f}'` $\mu A$. The normalized transconductance is the given by the chosen inversion coefficient as $g_{ms} = G_{ms}/G_{spec} =$ `{python} f'{gms:.3f}'`, from which we derive the transconductance $G_m =$ `{python} f'{Gm/1e-6:.3f}'` $\mu A/V$ and the gain-bandwidth product for the given load capacitance $GBW = G_m/C_L =$ `{python} f'{GBWthe/1e6:.3f}'` $MHz$. The aspect ratio is then given by $W_{eff}/L_{eff} =$ `{python} f'{WoverL:.3f}'`. The effective width $W_{eff} =$ `{python} f'{Weff/1e-6:.3f}'` $\mu m$ and drawn width $W =$ `{python} f'{W/1e-6:.2f}'` $\mu m$.

We can calculate the dc gain by first estimating the output conductance $G_{ds} = I_b/(\lambda_n\,L_{eff}) =$ `{python} f'{Gds/1e-6:.3f}'` $\mu A/V$. The dc gain is then given by $A_{dc} =G_m/G_{ds} =$ `{python} f'{Adcthe:.3f}'` or `{python} f'{AdcdBthe:.3f}'` $dB$.

We also calculate the area and perimeter of the drain to estimate the junction capacitance and overlap capacitance in order to correct $C_L$ to get the correct $GBW$. This results in $C_{DBJ} =$ `{python} f'{CDBJ/1e-15:.0f}'` $fF$, $C_{GD} =$ `{python} f'{CGDo/1e-15:.3f}'` $fF$, $C_D =$ `{python} f'{CD/1e-15:.0f}'` $fF$. The effective load capacitance that needs to be added is then $C_{L0} =$ `{python} f'{CL0/1e-12:.3f}'` $pF$.

### Simulations
We can now check whether we get the correct GBW and DC gain using ngspice simulations with the schematic shown in @fig-CS_sim_sch. The simulations will be performed with the following parameters:

```{python}
paramstr = '\n'.join((
  f'.param VDD={VDD:.1f} Ib={Ib/1e-6:.3f}u',
  f'.param CL={CL0/1e-15:.2f}f W={W/1e-6:.2f}u L={L/1e-6:.2f}u AD={AD:.2e} PD={PD:.2e}',
#  f'.param CL={CL0/1e-15:.2f}f W={Wopt/1e-6:.2f}u L={L/1e-6:.2f}u' 
  ))
print(paramstr)
with open(simulationPath+'size_bias.par', 'w') as f:
  f.write(paramstr)

Ndec=101
fminsim=1e4
fmaxsim=1e10

simpar = '\n'.join((
  f'.csparam Ndec={Ndec:.0f}',
  f'.csparam fmin={fminsim:.3e}',
  f'.csparam fmax={fmaxsim:.3e}'
))
simparFile = simulationPath + "simulation.ac.par"
with open(simparFile, 'w') as f:
  f.write(simpar)
```

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

```{python}
#| label: tbl-problem2_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"

result = subprocess.run(f"""ngspice_con -b -o "{simulationLog}" "{simulationFile}" """, shell=True, capture_output=True, text=True)

nodeList=["vdd","in","out","1","2"]
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))

Voutq=v_df.loc['out','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")
))
```

The operating information for each transistor can be extracted and are given in @tbl-problem1_op1 and @tbl-problem1_op2.

```{python}
#| label: tbl-problem2_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-problem2_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")
))
```

We see that the simulated transconductance of M~1~ $G_m =$ `{python} f'{ekv2p6mos_df.at["M1","Gm"]/1e-6:.3f}'` $\mu A/V$ is slightly lower than the theoretical estimation $G_m =$ `{python} f'{Gm/1e-6:.3f}'` $\mu A/V$.

The transfer function magnitude and phase are plotted in @fig-cs_gbw_long, which shows a good fit between the simulation and the theoretical estimation.

```{python}
#| label: fig-cs_power_long
#| fig-cap: Simulated gain response compared to theoretical estimation for the long-channel case.

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

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]

Avphidegsim=Avphidegsim-180

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:
      fcsim=float(match.group(1))

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

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

fthe=fsim
fcthe=GBWthe/abs(Adcthe)
wcthe=2*pi*fcthe
#CF=90e-15
CF=CGDo
wzthe=Gm/CF

AvmagdBthe1=np.zeros(Npts)
Avphidegthe1=np.zeros(Npts)
AvmagdBthe2=np.zeros(Npts)
Avphidegthe2=np.zeros(Npts)
for k in range(0,Npts):
  jw=1j*2*pi*fthe[k]
  AvmagdBthe1[k]=20*log(abs(Av1(jw,Adcthe,wcthe)))
  Avphidegthe1[k]=np.angle(Av1(jw,Adcthe,wcthe),deg=True)
  AvmagdBthe2[k]=20*log(abs(Av2(jw,Adcthe,wcthe,wzthe)))
  Avphidegthe2[k]=np.angle(Av2(jw,Adcthe,wcthe,wzthe),deg=True)

Avphidegthe1=Avphidegthe1-360
Avphidegthe2=Avphidegthe2-360

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

axs[0].semilogx([fmin,GBWsim],[0,0],'k--', linewidth=lw)
axs[0].semilogx([GBWsim,GBWsim],[0,-60],'k--', linewidth=lw)
axs[0].semilogx(fsim,AvmagdBsim,'ro', markersize=4, markevery=15, label='Simulation')
axs[0].semilogx(fthe,AvmagdBthe1,'r-',label='Theory')
axs[0].semilogx(fthe,AvmagdBthe2,'r--',label='Theory (with zero)')
axs[0].set(xlim=(fmin,fmax), ylim=(-40,60), ylabel='$|A_{vd}|$ [dB]')
#axs[0].set_yticks([-25,-20,-15,-10,-5,0,5,10,15,20,25])
axs[0].legend(loc='lower left')
textstr1 = '\n'.join((
  f'$A_{{dc}} =$ {AdcdBthe:.1f} dB (theory)',
  f'$A_{{dc}} =$ {AdcdBsim:.1f} dB (sim)',
  f'$GBW =$ {GBWthe/1e6:.1f} MHz (theory)',
  f'$GBW =$ {GBWsim/1e6:.1f} MHz (sim.)',
  f'$f_c =$ {fcthe/1e3:.1f} kHz (theory)',
  f'$f_c =$ {fcsim/1e3:.1f} kHz (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,GBWsim],[PGBWsim,PGBWsim],'k--', linewidth=lw)
#axs[1].semilogx([GBWsim,GBWsim],[PGBWsim,0],'k--', linewidth=lw)
axs[1].semilogx(fsim,Avphidegsim,'bo', markersize=4, markevery=15, label='Simulation')
axs[1].semilogx(fthe,Avphidegthe1,'b-',label='Theory')
axs[1].semilogx(fthe,Avphidegthe2,'b--',label='Theory (with zero)')
axs[1].set(xlim=(fmin,fmax), ylim=(-360,-180), xlabel='Frequency [Hz]', ylabel='$arg\\{A_{vd}\\}$ [degree]')
axs[1].set_xticks([1e4,1e5,1e6,1e7,1e8,1e9,1e10])
axs[1].set_yticks([-360,-315,-270,-225,-180])
axs[1].legend(loc='lower left')
textstr2 = '\n'.join((
  f'$L =$ {L/1e-6:.0f} $\\mu m$',
  f'$W =$ {W/1e-6:.2f} $\\mu m$',
  f'$IC =$ {IC:.0f}',
  f'$I_b =$ {Ib/1e-6:.3f} $\\mu A$',
  f'$C_{{L0}} =$ {CL0/1e-12:.3f} $pF$',
  f'$C_L =$ {CL/1e-12:.3f} $pF$',
  f'$C_{{GDo}} =$ {CGDo/1e-15:.3f} $fF$'))
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)
```

## Short-channel case (including velocity saturation)
### Design

```{python}
L=180e-9
Leff=L+DLn
lambdac=Lsatn/Leff
gms=gms_ic_short(IC,lambdac)
Ib=P/VDD
Ispec=Ib/IC
Gm=gms*Ispec/(n0n*UT)
GBWthe=Gm/(2*pi*CL)
WoverL=Ispec/Ispecsqn
Weff=WoverL*Leff
W=Weff-DWn
W=round(W*1e8)/1e8
Weff=W+DWn
Gds=Ib/(lambdan*Leff)
Adcthe=-Gm/Gds
AdcdBthe=20*log(abs(Adcthe))
AD=2*Hdif*Weff
PD=2*(2*Hdif+Weff)
CDBJ=AD*CJn+PD*CJSWn
CGDo=W*CGDOn
CGSo=W*CGSOn
CD=CGDo+CDBJ
CL0=CL-CD
```

For the short-channel case we will use the shortest channel length for this technology, namely $L =$ `{python} f'{L/1e-9:.0f}'` $nm$. The velocity parameter for the chosen length is then given by $\lambda_c = L_{satn}/L =$ `{python} f'{lambdac:.3f}'`. The procedure is basically the same except that we now need to account for velocity saturation in the estimation of the transconductance. Since the inversion coefficient is chosen as $IC =$ `{python} f'{IC:.0f}'`, we get the specific current as $I_{spec} =$ `{python} f'{Ispec/1e-6:.3f}'` $\mu A$. The normalized transconductance is the given by the chosen inversion coefficient as $g_{ms} = G_{ms}/G_{spec} =$ `{python} f'{gms:.3f}'`, from which we derive the transconductance $G_m =$ `{python} f'{Gm/1e-3:.3f}'` $mA/V$ and the gain-bandwidth product for the given load capacitance $GBW = G_m/C_L =$ `{python} f'{GBWthe/1e6:.3f}'` $MHz$. The aspect ratio is then given by $W_{eff}/L_{eff} =$ `{python} f'{WoverL:.3f}'`. The effective width $W_{eff} =$ `{python} f'{Weff/1e-6:.3f}'` $\mu m$ and drawn width $W =$ `{python} f'{W/1e-6:.2f}'` $\mu m$.

We can calculate the dc gain by first estimating the output conductance $G_{ds} = I_b/(\lambda_n\,L_{eff}) =$ `{python} f'{Gds/1e-6:.3f}'` $\mu A/V$. The dc gain is then given by $A_{dc} =G_m/G_{ds} =$ `{python} f'{Adcthe:.3f}'` or `{python} f'{AdcdBthe:.3f}'` $dB$.

We also calculate the area and perimeter of the drain to estimate the junction capacitance and overlap capacitance in order to correct $C_L$ to get the correct $GBW$. This results in $C_{DBJ} =$ `{python} f'{CDBJ/1e-15:.1f}'` $fF$, $C_{GD} =$ `{python} f'{CGDo/1e-15:.3f}'` $fF$, $C_D =$ `{python} f'{CD/1e-15:.1f}'` $fF$. The effective load capacitance that needs to be added is then $C_{L0} =$ `{python} f'{CL0/1e-12:.3f}'` $pF$.

### Simulations
We can check whether we get the correct GBW and dc gain using simulations with the following values of the parameters:

```{python}
paramstr = '\n'.join((
  f'.param VDD={VDD:.1f} Ib={Ib/1e-6:.3f}u',
  f'.param CL={CL0/1e-15:.2f}f W={W/1e-6:.2f}u L={L/1e-6:.2f}u AD={AD:.2e} PD={PD:.2e}',
#  f'.param CL={CL0/1e-15:.2f}f W={Wopt/1e-6:.2f}u L={L/1e-6:.2f}u' 
  ))
print(paramstr)
with open(simulationPath+'size_bias.par', 'w') as f:
  f.write(paramstr)

Ndec=101
fminsim=1e4
fmaxsim=1e10

simpar = '\n'.join((
  f'.csparam Ndec={Ndec:.0f}',
  f'.csparam fmin={fminsim:.3e}',
  f'.csparam fmax={fmaxsim:.3e}'
))
simparFile = simulationPath + "simulation.ac.par"
with open(simparFile, 'w') as f:
  f.write(simpar)
```

The transfer function magnitude and phase are plotted in @fig-cs_power_short, which shows a reasonable fit between the simulation and the theoretical estimation.


```{python}
#| label: fig-cs_power_short
#| fig-cap: Simulated gain response compared to theoretical estimation for the short-channel case.

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

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]

Avphidegsim=Avphidegsim-180

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:
      fcsim=float(match.group(1))

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

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

fthe=fsim
fcthe=GBWthe/abs(Adcthe)
wcthe=2*pi*fcthe
#CF=90e-15
CF=CGDo
wzthe=Gm/CF

AvmagdBthe1=np.zeros(Npts)
Avphidegthe1=np.zeros(Npts)
AvmagdBthe2=np.zeros(Npts)
Avphidegthe2=np.zeros(Npts)
for k in range(0,Npts):
  jw=1j*2*pi*fthe[k]
  AvmagdBthe1[k]=20*log(abs(Av1(jw,Adcthe,wcthe)))
  Avphidegthe1[k]=np.angle(Av1(jw,Adcthe,wcthe),deg=True)
  AvmagdBthe2[k]=20*log(abs(Av2(jw,Adcthe,wcthe,wzthe)))
  Avphidegthe2[k]=np.angle(Av2(jw,Adcthe,wcthe,wzthe),deg=True)

Avphidegthe1=Avphidegthe1-360
Avphidegthe2=Avphidegthe2-360

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

axs[0].semilogx([fmin,GBWsim],[0,0],'k--', linewidth=lw)
axs[0].semilogx([GBWsim,GBWsim],[0,-60],'k--', linewidth=lw)
axs[0].semilogx(fsim,AvmagdBsim,'ro', markersize=4, markevery=15, label='Simulation')
axs[0].semilogx(fthe,AvmagdBthe1,'r-',label='Theory')
axs[0].semilogx(fthe,AvmagdBthe2,'r--',label='Theory (with zero)')
axs[0].set(xlim=(fmin,fmax), ylim=(-40,40), ylabel='$|A_{vd}|$ [dB]')
#axs[0].set_yticks([-25,-20,-15,-10,-5,0,5,10,15,20,25])
axs[0].legend(loc='lower left')
textstr1 = '\n'.join((
  f'$A_{{dc}} =$ {AdcdBthe:.1f} dB (theory)',
  f'$A_{{dc}} =$ {AdcdBsim:.1f} dB (sim)',
  f'$GBW =$ {GBWthe/1e6:.1f} MHz (theory)',
  f'$GBW =$ {GBWsim/1e6:.1f} MHz (sim.)',
  f'$f_c =$ {fcthe/1e6:.1f} MHz (theory)',
  f'$f_c =$ {fcsim/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,GBWsim],[PGBWsim,PGBWsim],'k--', linewidth=lw)
#axs[1].semilogx([GBWsim,GBWsim],[PGBWsim,0],'k--', linewidth=lw)
axs[1].semilogx(fsim,Avphidegsim,'bo', markersize=4, markevery=15, label='Simulation')
axs[1].semilogx(fthe,Avphidegthe1,'b-',label='Theory')
axs[1].semilogx(fthe,Avphidegthe2,'b--',label='Theory (with zero)')
axs[1].set(xlim=(fmin,fmax), ylim=(-315,-180), xlabel='Frequency [Hz]', ylabel='$arg\\{A_{vd}\\}$ [degree]')
axs[1].set_xticks([1e4,1e5,1e6,1e7,1e8,1e9,1e10])
axs[1].set_yticks([-315,-270,-225,-180])
axs[1].legend(loc='lower left')
textstr2 = '\n'.join((
  f'$L =$ {L/1e-6:.2f} $\\mu m$',
  f'$W =$ {W/1e-6:.2f} $\\mu m$',
  f'$IC =$ {IC:.0f}',
  f'$I_b =$ {Ib/1e-6:.3f} $\\mu A$',
  f'$C_{{L0}} =$ {CL0/1e-12:.3f} $pF$',
  f'$C_L =$ {CL/1e-12:.3f} $pF$',
  f'$C_{{GD}} =$ {CF/1e-15:.3f} $fF$'))
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)
```

# Problem 3: Imposing the gain bandwidth product and the current budget
## Specifications
We now don't impose the inversion coefficient but the gain bandwidth product and the current budget. We keep the same load capacitance and consider a long channel. The specifications are now given in @tbl-specs_problem3.

```{python}
#| label: tbl-specs_problem3
#| tbl-cap: Specifications for problem 3.

from specs_problem3 import *

table = [["Gain-bandwidth product","$GBW$",f'{GBWthe/1e6:.0f}',"$MHz$"],
         ["Bias current","$I_b$",f'{Ib/1e-9:.0f}',"$nA$"],
         ["Load capacitance","$C_L$",f'{CL/1e-12:.0f}',"$pF$"],
         ["Transistor length","$L$",f'{L/1e-6:.0f}',"$\\mu m$"]
         ]

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

## Design

```{python}
L=1e-6
Leff=L+DLn
Gm=2*pi*GBWthe*CL
gms_over_id=Gm*n0n*UT/Ib
IC=ic_gmsid(gms_over_id)
Ispec=Ib/IC
WoverL=Ispec/Ispecsqn
Weff=WoverL*Leff
W=Weff-DWn
W=round(W*1e8)/1e8
Weff=W+DWn
Gds=Ib/(lambdan*Leff)
Adcthe=-Gm/Gds
AdcdBthe=20*log(abs(Adcthe))
AD=2*Hdif*Weff
PD=2*(2*Hdif+Weff)
CDBJ=AD*CJn+PD*CJSWn
CGDo=Weff*CGDOn
CGSo=Weff*CGSOn
CD=CGDo+CDBJ
CL0=CL-CD
```

We first get the transconductance required to achieve the target gain-bandwidth product as $G_m =$ `{python} f'{Gm/1e-6:.3f}'` $\mu A/V$. Knowing the bias current and the transconductance we can calculate the current efficiency as $G_m\,n U_T/I_D =$ `{python} f'{gms_over_id:.3f}'`, from which we can derive the corresponding inversion coefficient $IC =$ `{python} f'{IC:.3f}'`. We see that we are in the moderate inversion region. The specific current is then given by $I_{spec} =$ `{python} f'{Ispec/1e-9:.0f}'` $nA$, the aspect ratio by $W_{eff}/L_{eff} =$ `{python} f'{WoverL:.3f}'`, the effective width by $W_{eff} =$ `{python} f'{Weff/1e-9:.0f}'` $nm$ and finally the drawn width $W =$ `{python} f'{W/1e-9:.0f}'` $nm$.

We can calculate the dc gain by first estimating the output conductance $G_{ds} = I_b/(\lambda_n\,L_{eff}) =$ `{python} f'{Gds/1e-9:.3f}'` $nA/V$. The dc gain is then given by $A_{dc} =G_m/G_{ds} =$ `{python} f'{Adcthe:.3f}'` or `{python} f'{AdcdBthe:.3f}'` $dB$.

We also calculate the area and perimeter of the drain to estimate the junction capacitance and overlap capacitance in order to correct $C_L$ to get the correct $GBW$. This results in $C_{DBJ} =$ `{python} f'{CDBJ/1e-18:.1f}'` $aF$, $C_{GD} =$ `{python} f'{CGDo/1e-18:.3f}'` $aF$, $C_D =$ `{python} f'{CD/1e-18:.1f}'` $aF$. The effective load capacitance that needs to be added is then $C_{L0} =$ `{python} f'{CL0/1e-15:.3f}'` $fF$. We observe that the parasitic capacitance at the drain are negligible compared to the load capacitance.

## Simulations
We can now check whether we get the correct GBW and DC gain using ngspice simulations with the schematic shown in @fig-CS_sim_sch. The simulations will be performed with the following parameters:

```{python}
paramstr = '\n'.join((
  f'.param VDD={VDD:.1f} Ib={Ib/1e-6:.3f}u',
  f'.param CL={CL0/1e-15:.2f}f W={W/1e-6:.2f}u L={L/1e-6:.2f}u AD={AD:.2e} PD={PD:.2e}',
#  f'.param CL={CL0/1e-15:.2f}f W={Wopt/1e-6:.2f}u L={L/1e-6:.2f}u' 
  ))
print(paramstr)
with open(simulationPath+'size_bias.par', 'w') as f:
  f.write(paramstr)

Ndec=101
fminsim=1e1
fmaxsim=1e7

simpar = '\n'.join((
  f'.csparam Ndec={Ndec:.0f}',
  f'.csparam fmin={fminsim:.3e}',
  f'.csparam fmax={fmaxsim:.3e}'
))
simparFile = simulationPath + "simulation.ac.par"
with open(simparFile, 'w') as f:
  f.write(simpar)
```

The transfer function magnitude and phase are plotted in @fig-cs_gbw_long, which shows a good fit between the simulation and the theoretical estimation.

```{python}
#| label: fig-cs_current
#| fig-cap: Simulated gain response compared to theoretical estimation for the long-channel case.

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

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]

Avphidegsim=Avphidegsim-180

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:
      fcsim=float(match.group(1))

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

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

fthe=fsim
fcthe=GBWthe/abs(Adcthe)
wcthe=2*pi*fcthe

AvmagdBthe1=np.zeros(Npts)
Avphidegthe1=np.zeros(Npts)
AvmagdBthe2=np.zeros(Npts)
Avphidegthe2=np.zeros(Npts)
for k in range(0,Npts):
  jw=1j*2*pi*fthe[k]
  AvmagdBthe1[k]=20*log(abs(Av1(jw,Adcthe,wcthe)))
  Avphidegthe1[k]=np.angle(Av1(jw,Adcthe,wcthe),deg=True)

Avphidegthe1=Avphidegthe1-360

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

axs[0].semilogx([fmin,GBWsim],[0,0],'k--', linewidth=lw)
axs[0].semilogx([GBWsim,GBWsim],[0,-60],'k--', linewidth=lw)
axs[0].semilogx(fsim,AvmagdBsim,'ro', markersize=4, markevery=15, label='Simulation')
axs[0].semilogx(fthe,AvmagdBthe1,'r-',label='Theory')
axs[0].set(xlim=(fmin,fmax), ylim=(-40,60), ylabel='$|A_{vd}|$ [dB]')
#axs[0].set_yticks([-25,-20,-15,-10,-5,0,5,10,15,20,25])
axs[0].legend(loc='lower left')
textstr1 = '\n'.join((
  f'$A_{{dc}} =$ {AdcdBthe:.1f} dB (theory)',
  f'$A_{{dc}} =$ {AdcdBsim:.1f} dB (sim)',
  f'$GBW =$ {GBWthe/1e6:.1f} MHz (theory)',
  f'$GBW =$ {GBWsim/1e6:.1f} MHz (sim.)',
  f'$f_c =$ {fcthe/1e3:.1f} kHz (theory)',
  f'$f_c =$ {fcsim/1e3:.1f} kHz (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,GBWsim],[PGBWsim,PGBWsim],'k--', linewidth=lw)
#axs[1].semilogx([GBWsim,GBWsim],[PGBWsim,0],'k--', linewidth=lw)
axs[1].semilogx(fsim,Avphidegsim,'bo', markersize=4, markevery=15, label='Simulation')
axs[1].semilogx(fthe,Avphidegthe1,'b-',label='Theory')
axs[1].set(xlim=(fmin,fmax), ylim=(-270,-180), xlabel='Frequency [Hz]', ylabel='$arg\\{A_{vd}\\}$ [degree]')
axs[1].set_xticks([1e1,1e2,1e3,1e4,1e5,1e6,1e7])
axs[1].set_yticks([-270,-225,-180])
axs[1].legend(loc='lower left')
textstr2 = '\n'.join((
  f'$L =$ {L/1e-6:.0f} $\\mu m$',
  f'$W =$ {W/1e-9:.0f} $nm$',
  f'$IC =$ {IC:.3f}',
  f'$I_b =$ {Ib/1e-9:.0f} $nA$',
  f'$C_{{L0}} =$ {CL0/1e-15:.1f} $fF$',
  f'$C_L =$ {CL/1e-12:.0f} $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)
```

# Problem 4: Imposing the input-referred noise
## Specifications
In this example, we impose the input-referred noise by setting the white noise and the corner frequency. The specifications are given in @tbl-specs_problem4.

```{python}
#| label: tbl-specs_problem4
#| tbl-cap: Specifications for problem 4.

from specs_problem4 import *

table = [["Corner frequency","$f_k$",f'{fkthe/1e3:.0f}',"$kHz$"],
         ["Input referred white PSD","$\\sqrt{{S_{{n,th}}}}$",f'{Vninth/1e-9:.0f}',"$\\frac{{nV}}{{\\sqrt{{Hz}}}}$"],
         [" ","$10\\,\\log(S_{{n,th}})$",f'{20*log(Vninth):.2f}',"$\\frac{{dBv}}{{\\sqrt{{Hz}}}}$"],
         ["Inversion coefficient","$IC$",f'{IC:.0f}',"-"],
         ["Load capacitance","$C_L$",f'{CL/1e-12:.0f}',"$pF$"]
        ]

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


## Design

```{python}
L=1e-6
Leff=L+DLn
gamman=gamman_ic(IC,n0n)
Sninth=Vninth**2
Gm=4*kT*gamman/Sninth
gms_over_id=gmsid_ic(IC)
Ib=Gm*n0n*UT/gms_over_id
Ispec=Ib/IC
WoverL=Ispec/Ispecsqn
WL=KFn/(Cox*Sninth*fkthe)
Weff=sqrt(WoverL*WL)
Leff=Weff/WoverL
W=Weff-DWn
W=round(W*1e8)/1e8
L=Leff-DLn
L=round(L*1e8)/1e8
GBWthe=Gm/(2*pi*CL)
Rninth=gamman/Gm
Sninththe=4*kT*Rninth
Gds=Ib/(lambdan*Leff)
Adcthe=-Gm/Gds
AdcdBthe=20*log(abs(Adcthe))
AD=2*Hdif*Weff
PD=2*(2*Hdif+Weff)
CDBJ=AD*CJn+PD*CJSWn
CGDo=Weff*CGDOn
CGSo=Weff*CGSOn
CD=CGDo+CDBJ
CL0=CL-CD
```

We need the flicker noise parameter $K_f =$ `{python} f'{KFn:.3e}'` $J$ and the oxide capacitance per unit area $C_{ox} =$ `{python} f'{Cox*1e3:.3f}'` $fF/\mu m^2$.

Before the transconductance is derived from the thermal noise specification, we need to know the thermal noise excess factor $\gamma_n$ for the given $IC$ (we assume long-channel case) which is $\gamma_n =$ `{python} f'{gamman :.3f}'`. The transconductance is then given by $G_m = 4 kT \gamma_n/S_{nin,th} =$ `{python} f'{Gm/1e-6:.3f}'` $\mu A/V$, where $S_{nin,th} = V_{nin,th}^2$. Once we have the transconductance, the procedure is similar to the previous one.

Having imposed the inversion coefficient $IC =$ `{python} f'{IC:.0f}'`, we can deduce the current efficiency as $G_m\,n U_T/I_D =$ `{python} f'{gms_over_id:.3f}'`. By definition of the current efficiency we can get the corresponding bias current $I_b =$ `{python} f'{Ib/1e-9:.0f}'` $nA$. The specific current and aspect ratio are then given by $I_{spec} =$ `{python} f'{Ispec/1e-9:.0f}'` $nA$ and $W_{eff}/L_{eff} =$ `{python} f'{WoverL:.3f}'`.

The corner frequency will actually set the flicker noise which is inversely proportional to the gate are. We can therefore calculate the gate area from the flicker noise remembering that the corner frequency is the frequency at which the flicker noise becomes equal to the white noise. The gate area is obtained as $W_{eff}\,L_{eff} =$ `{python} f'{WL/1e-12:.3f}'` $\mu m^2$. Knowing the $W/L$ ratio we can calculate the effective width and length as $W_{eff} =$ `{python} f'{Weff/1e-6:.3f}'` $\mu m$ and $L_{eff} =$ `{python} f'{Leff/1e-6:.3f}'` $\mu m$. The drawn width and length follow as $W =$ `{python} f'{W/1e-6:.2f}'` $\mu m$ and $L =$ `{python} f'{L/1e-6:.2f}'` $\mu m$.

The gain-bandwidth product is given by $GBW =$ `{python} f'{GBWthe/1e6:.3f}'` $MHz$.

We can calculate the dc gain by first estimating the output conductance $G_{ds} = I_b/(\lambda_n\,L_{eff}) =$ `{python} f'{Gds/1e-6:.3f}'` $\mu A/V$. The dc gain is then given by $A_{dc} =G_m/G_{ds} =$ `{python} f'{Adcthe:.3f}'` or `{python} f'{AdcdBthe:.3f}'` $dB$.

We also calculate the area and perimeter of the drain to estimate the junction capacitance and overlap capacitance in order to correct $C_L$ to get the correct $GBW$. This results in $C_{DBJ} =$ `{python} f'{CDBJ/1e-15:.1f}'` $fF$, $C_{GD} =$ `{python} f'{CGDo/1e-15:.3f}'` $fF$, $C_D =$ `{python} f'{CD/1e-15:.1f}'` $fF$. The effective load capacitance that needs to be added is then $C_{L0} =$ `{python} f'{CL0/1e-12:.3f}'` $pF$.

## Simulations
We can now check whether we get the correct GBW and DC gain using ngspice simulations with the schematic shown in @fig-CS_sim_sch. The simulations will be performed with the following parameters:

```{python}
paramstr = '\n'.join((
  f'.param VDD={VDD:.1f} Ib={Ib/1e-9:.0f}n',
  f'.param CL={CL0/1e-15:.2f}f W={W/1e-6:.2f}u L={L/1e-6:.2f}u AD={AD:.2e} PD={PD:.2e}',
#  f'.param CL={CL0/1e-15:.2f}f W={Wopt/1e-6:.2f}u L={L/1e-6:.2f}u' 
  ))
print(paramstr)
with open(simulationPath+'size_bias.par', 'w') as f:
  f.write(paramstr)

Ndec=101
fminsim=1e1
fmaxsim=1e7

simpar = '\n'.join((
  f'.csparam Ndec={Ndec:.0f}',
  f'.csparam fmin={fminsim:.3e}',
  f'.csparam fmax={fmaxsim:.3e}'
))
simparFile = simulationPath + "simulation.ac.par"
with open(simparFile, 'w') as f:
  f.write(simpar)
```

The transfer function magnitude and phase are plotted in @fig-cs_noise, which shows a good fit between the simulation and the theoretical estimation.

```{python}
#| label: fig-cs_gain_noise
#| fig-cap: Simulated gain response compared to theoretical estimation for the long-channel case.

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

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]

Avphidegsim=Avphidegsim-180

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:
      fcsim=float(match.group(1))

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

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

fthe=fsim
fcthe=GBWthe/abs(Adcthe)
wcthe=2*pi*fcthe

AvmagdBthe1=np.zeros(Npts)
Avphidegthe1=np.zeros(Npts)
for k in range(0,Npts):
  jw=1j*2*pi*fthe[k]
  AvmagdBthe1[k]=20*log(abs(Av1(jw,Adcthe,wcthe)))
  Avphidegthe1[k]=np.angle(Av1(jw,Adcthe,wcthe),deg=True)

Avphidegthe1=Avphidegthe1-360

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

axs[0].semilogx([fmin,GBWsim],[0,0],'k--', linewidth=lw)
axs[0].semilogx([GBWsim,GBWsim],[0,-60],'k--', linewidth=lw)
axs[0].semilogx(fsim,AvmagdBsim,'ro', markersize=4, markevery=15, label='Simulation')
axs[0].semilogx(fthe,AvmagdBthe1,'r-',label='Theory')
axs[0].set(xlim=(fmin,fmax), ylim=(-20,80), ylabel='$|A_{vd}|$ [dB]')
#axs[0].set_yticks([-25,-20,-15,-10,-5,0,5,10,15,20,25])
axs[0].legend(loc='lower left')
textstr1 = '\n'.join((
  f'$A_{{dc}} =$ {AdcdBthe:.1f} dB (theory)',
  f'$A_{{dc}} =$ {AdcdBsim:.1f} dB (sim)',
  f'$GBW =$ {GBWthe/1e6:.1f} MHz (theory)',
  f'$GBW =$ {GBWsim/1e6:.1f} MHz (sim.)',
  f'$f_c =$ {fcthe/1e3:.1f} kHz (theory)',
  f'$f_c =$ {fcsim/1e3:.1f} kHz (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,GBWsim],[PGBWsim,PGBWsim],'k--', linewidth=lw)
#axs[1].semilogx([GBWsim,GBWsim],[PGBWsim,0],'k--', linewidth=lw)
axs[1].semilogx(fsim,Avphidegsim,'bo', markersize=4, markevery=15, label='Simulation')
axs[1].semilogx(fthe,Avphidegthe1,'b-',label='Theory')
axs[1].set(xlim=(fmin,fmax), ylim=(-315,-180), xlabel='Frequency [Hz]', ylabel='$arg\\{A_{vd}\\}$ [degree]')
axs[1].set_xticks([1e1,1e2,1e3,1e4,1e5,1e6,1e7])
axs[1].set_yticks([-315,-270,-225,-180])
axs[1].legend(loc='lower left')
textstr2 = '\n'.join((
  f'$L =$ {L/1e-6:.2f} $\\mu m$',
  f'$W =$ {W/1e-6:.2f} $\\mu m$',
  f'$IC =$ {IC:.0f}',
  f'$I_b =$ {Ib/1e-6:.3f} $\\mu A$',
  f'$C_{{L0}} =$ {CL0/1e-12:.3f} $pF$',
  f'$C_L =$ {CL/1e-12:.0f} $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)
```

We can now check the input-referred noise PSD which is plotted in @fig-cs_noise. We can see that simulation perfectly matches the theoretical estimation.

```{python}
#| label: fig-cs_noise
#| fig-cap: Simulated input-referred noise PSD compared to theoretical estimation.

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

Ndec=101
fminsim=1e1
fmaxsim=1e7

simpar = '\n'.join((
  f'.csparam Ndec={Ndec:.0f}',
  f'.csparam fmin={fminsim:.3e}',
  f'.csparam fmax={fmaxsim:.3e}'
))
simparFile = simulationPath + "simulation.nz.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_noise=pd.read_table(simulationRaw, sep=r'\s+', skiprows=(14), skip_blank_lines=True, header=None, dtype=np.float64, engine='python')

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))

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))

freq=np.zeros(Npts)
Vnin=np.zeros(Npts)
VnoutM1=np.zeros(Npts)
VnoutM1f=np.zeros(Npts)
VnoutM1w=np.zeros(Npts)
Vnout=np.zeros(Npts)
SnoutM1=np.zeros(Npts)
SnoutM1f=np.zeros(Npts)
SnoutM1w=np.zeros(Npts)
Snin=np.zeros(Npts)
SnindBv=np.zeros(Npts)
SninM1=np.zeros(Npts)
SninM1f=np.zeros(Npts)
SninM1w=np.zeros(Npts)
SninM1dBv=np.zeros(Npts)
SninM1fdBv=np.zeros(Npts)
SninM1wdBv=np.zeros(Npts)
SnoutdBv=np.zeros(Npts)
A=np.zeros(Npts)
SninM1wthe=np.zeros(Npts)
SninM1wdBvthe=np.zeros(Npts)
SninM1fthe=np.zeros(Npts)
SninM1fdBvthe=np.zeros(Npts)
SninM1the=np.zeros(Npts)
SninM1dBvthe=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]
  Vnout[k]=df_noise.iloc[Nvar*k+5,0]
  SnoutM1[k]=VnoutM1[k]**2
  SnoutM1f[k]=VnoutM1f[k]**2
  SnoutM1w[k]=VnoutM1w[k]**2
  A[k]=Vnout[k]/Vnin[k]
  Snin[k]=Vnin[k]**2
  SnindBv[k]=10*log(Snin[k])
  SninM1[k]=SnoutM1[k]/A[k]**2
  SninM1f[k]=SnoutM1f[k]/A[k]**2
  SninM1w[k]=SnoutM1w[k]/A[k]**2
  SninM1dBv[k]=10*log(SninM1[k])
  SninM1fdBv[k]=10*log(SninM1f[k])
  SninM1wdBv[k]=10*log(SninM1w[k])
  SninM1wthe[k]=4*kT*gamman/Gm
  SninM1wdBvthe[k]=10*log(SninM1wthe[k])
  SninM1fthe[k]=KFn/(Weff*Leff*Cox*freq[k])
  SninM1fdBvthe[k]=10*log(SninM1fthe[k])
  SninM1the[k]=SninM1wthe[k]+SninM1fthe[k]
  SninM1dBvthe[k]=10*log(SninM1the[k])

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

plt.semilogx(freq,SninM1wdBv,'bo', label='Thermal (sim.)', markevery=20, markersize=4)
plt.semilogx(freq,SninM1fdBv,'go', label='Flicker (sim.)', markevery=20, markersize=4)
plt.semilogx(freq,SninM1dBv,'ro', label='Total (sim.)', markevery=20, markersize=4)
plt.semilogx(freq,SninM1wdBvthe,'b-', label='Thermal (theory)')
plt.semilogx(freq,SninM1fdBvthe,'g-', label='Flicker (theory)')
plt.semilogx(freq,SninM1dBvthe,'r-', label='Total (sim.)')
plt.xlim(fmin,fmax)
plt.xticks([1e1,1e2,1e3,1e4,1e5,1e6,1e7])
plt.xlabel('Frequency [Hz]')
plt.ylim(-160,-110)
#plt.yticks(range(-170,-150,5))
plt.ylabel('Input Noise PSD $[dBv/\\sqrt{Hz}]$')
#plt.legend(loc='center left')
plt.legend(loc='center left', bbox_to_anchor=(1, 0.5))
textstr = '\n'.join((
  f'$S_{{nin,th}} =$ {SninM1wdBvthe[0]:.1f} $dBv/\\sqrt{{Hz}}$ (theory)',
  f'$S_{{nin,th}} =$ {SninM1wdBv[0]:.1f} $dBv/\\sqrt{{Hz}}$ (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_white_noise_sim')
plt.show()
```

# Problem 5: Minimum current for a given gain-bandwidth product with self-loading

![Schematic of the common-source (CS) illustrating the self-loading effect.](Figures/CS_self-loading.png){#fig-CS_self-loading}

In this problem we want to find the minimum current required to achieve a given gain-bandwidth product accounting for the effect of self-loading as illustrated in @fig-CS_self-loading.

The total transistor parasitic capacitance at the drain can be written as
\begin{equation}
  C_D = C_{D0} + C_{DW} \cdot W.
\end{equation}
with
\begin{align}
  C_{D0} &= 4 H_{dif} \cdot C_{JSW},\\
  C_{DW} &= 2(H_{dif} \cdot C_J + C_{JSW}) + C_{GDe},
\end{align}
where $C_{GDe}$ is the extrinsic capacitance per unit width which is made of the overlap capacitance $C_{GDo}$. In more advanced technologies we also need to add the fringing-field capacitance $C_{GDf}$. In this 180nm technology the fringing-field capacitance can be neglected so that $C_{GDe} \cong C_{GDo}$.

The part $C_{D0}$ of the total parasitic capacitance at the drain $C_D$ that doesn't scale with $W$ needs to be added to $C_{L0}$ so that the total load capacitance that does not scale with $W$ is given by
\begin{equation}
  C_L = C_{L0} + C_{D0}.
\end{equation}

It can be shown @bib:enz:github:2025 that when accounting for the self-loading effect, there is a minimum current for achieving a given gain-bandwidth product. The otptimum value of the inversion coefficient $IC_{opt}$ corresponding to this minimum bias current is given by
\begin{equation}\label{eqn:icopt}
  IC_{opt} = \left(\sqrt{\Theta \cdot (1+\Theta)} + \Theta + \frac{1}{2}\right)^2 - \frac{1}{4}
  = 2 \Theta \cdot (1+\Theta) + (1+2\Theta) \cdot \sqrt{\Theta \cdot (1+\Theta)},
\end{equation}
where
\begin{equation}
  \Theta \triangleq \frac{\omega_u}{\omega_W},
\end{equation}
with
\begin{align}
  \omega_u &\triangleq 2 \pi\,GBW,\\
  \omega_W &\triangleq \frac{I_{spec\Box}}{n U_T \cdot C_{DW} \cdot L}.
\end{align}

The parameter $\Theta$ is usually $\Theta \ll 1$ and therefore \eqref{eqn:icopt} reduces to
\begin{equation}
  IC_{opt} \cong 2 \Theta + \sqrt{\Theta}.
\end{equation}

We can define a normalized current and aspect ratio as
\begin{align}
  i_b &\triangleq \frac{I_b}{I_{pec\Box} \cdot \Omega},\\
  AR &\triangleq \frac{W/L}{\Omega},
\end{align}
where
\begin{align}
  \Omega &\triangleq \frac{\omega_u}{\omega_L},\\
  \omega_L &\triangleq \frac{I_{spec\Box}}{n U_T \cdot C_L}.
\end{align}

The minimum normalized current $i_{bopt}$ and aspect ratio $AR_{opt}$ only depend on $\Theta$ according to
\begin{align}
  i_{bopt} &\triangleq i_b(IC_{opt}) = \sqrt{4 IC_{opt} + 1} = 1 + 2\Theta +2\sqrt{\Theta \cdot (1+\Theta)},\\
  AR_{opt} &\triangleq AR(IC_{opt}) = \frac{\sqrt{4 IC_{opt} + 1}}{IC_{opt}} = \frac{1}{\sqrt{\Theta \cdot (1+\Theta)}}.
\end{align}

## Specifications
The specifications for Problem 5 are given in @tbl-specs_problem5.

```{python}
#| label: tbl-specs_problem5
#| tbl-cap: Specifications for problem 5.

from specs_problem5 import *
Leff=L+DLn

table = [["Gain-bandwidth product","$GBW$",f'{GBWthe/1e6:.0f}',"$MHz$"],
         ["Load capacitance","$C_{L0}$",f'{CL0/1e-15:.0f}',"$fF$"],
         ["Transistor length","$L$",f'{L/1e-6:.0f}',"$\\mu m$"]
        ]

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

## Design

```{python}
CD0=4*Hdif*CJSWn
CDW=2*Hdif*CJn+2*CJSWn+CGDOn
```

For accounting for the self-loading effect we need the capacitance per width $C_{DW} =$ `{python} f'{CDW*1e9:.3f}'` $fF/\mu m$. All the parameters are given in @tbl-parameters_problem5.

```{python}
#| label: tbl-parameters_problem5
#| tbl-cap: Parameters for problem 5.

def ICopt_theta(theta):
    return (sqrt(theta*(1+theta))+theta+0.5)**2-0.25

def ibopt_theta(theta):
    return 1+2*theta+2*sqrt(theta*(1+theta))

def ARopt_theta(theta):
    return 1/sqrt(theta*(1+theta))

Ispecsq=Ispecsqn
n=n0n
nUT=n*UT
wu=2*pi*GBWthe
CL=CL0+CD0
wL=Ispecsq/(nUT*CL)
wW=Ispecsq/(nUT*CDW*L)
Omega=wu/wL
theta=wu/wW
kappa=wW/wL
ICopt=ICopt_theta(theta)
ibopt=ibopt_theta(theta)
ARopt=ARopt_theta(theta)
Ibopt=ibopt*Ispecsq*Omega
WoverLopt=ARopt*Omega
Wopt=WoverLopt*Leff
W=Wopt-DWn
W=round(W*1e8)/1e8
Gmopt=Ispecsq/nUT*WoverLopt*gms_ic(ICopt)
CDopt=CDW*Wopt
AD=2*Hdif*Wopt
PD=2*(2*Hdif+Wopt)
CDBJ=AD*CJn+PD*CJSWn
CF=Wopt*CGDOn
Cout=CL+CDopt
wucheck=Gmopt/Cout
wz=Gmopt/CF
VM=lambdan*L
Gds=Ibopt/VM
Adcthe=Gmopt/Gds
AdcdBthe=20*log(Adcthe)

table = [["$C_{{DW}}$",f'{CDW/1e-9:.3f}',"$fF/\\mu m$"],
         ["$C_{{D0}}$",f'{CD0/1e-15:.3f}',"$fF$"],
         ["$C_{{L0}}$",f'{CL0/1e-15:.3f}',"$fF$"],
         ["$C_L$",f'{CL/1e-15:.3f}',"$fF$"],
         ["$f_L$",f'{wL/(2*pi*1e6):.3f}',"$MHz$"],
         ["$f_W$",f'{wW/(2*pi*1e9):.3f}',"$GHz$"],
         ["$\\Omega$",f'{Omega:.3f}',"-"],
         ["$\\Theta$",f'{theta:.3e}',"-"],
         ["$IC_{{opt}}$",f'{ICopt:.3f}',"-"],
         ["$i_{{b,opt}}$",f'{ibopt:.3f}',"-"],
         ["$AR_{{opt}}$",f'{ARopt:.3f}',"-"],
         ["$\\left(W/L\\right)_{{opt}}$",f'{WoverLopt:.3f}',"-"],
         ["$I_{{b,opt}}$",f'{Ibopt/1e-9:.0f}',"$nA$"],
         ["$W_{{opt}}$",f'{Wopt/1e-6:.2f}',"$\\mu m$"],
         ["$G_{{m,opt}}$",f'{Gmopt/1e-6:.3f}',"$\\mu A/V$"],
         ["$C_{{D,opt}}$",f'{CDopt/1e-15:.3f}',"$fF$"],
         ["$AD$",f'{AD/1e-12:.2f}',"$\\mu m^2$"],
         ["$PD$",f'{PD/1e-6:.3f}',"$\\mu m$"],
         ["$C_{{DBJ}}$",f'{CDBJ/1e-15:.3f}',"$fF$"],
         ["$C_F$",f'{CF/1e-15:.3f}',"$fF$"],
         ["$C_{{out}}$",f'{Cout/1e-15:.3f}',"$fF$"],
         ["$GBW$ (check)",f'{wu/(2*pi*1e6):.0f}',"$MHz$"],
         ["$f_z$",f'{wz/(2*pi*1e9):.3f}',"$GHz$"],
         ["$A_{{dc}}$",f'{AdcdBthe:.3f}',"$dB$"]
        ]

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

## Simulations
We can now check whether we get the correct GBW and DC gain using ngspice simulations with the schematic shown in @fig-CS_sim_sch. The simulations will be performed with the following parameters:

```{python}
paramstr = '\n'.join((
  f'.param VDD={VDD:.1f} Ib={Ibopt/1e-9:.0f}n',
  f'.param CL={CL0/1e-15:.2f}f W={W/1e-6:.2f}u L={L/1e-6:.2f}u AD={AD:.2e} PD={PD:.2e}',
#  f'.param CL={CL0/1e-15:.2f}f W={Wopt/1e-6:.2f}u L={L/1e-6:.2f}u' 
  ))
print(paramstr)
with open(simulationPath+'size_bias.par', 'w') as f:
  f.write(paramstr)

Ndec=101
fminsim=1e3
fmaxsim=1e10

simpar = '\n'.join((
  f'.csparam Ndec={Ndec:.0f}',
  f'.csparam fmin={fminsim:.3e}',
  f'.csparam fmax={fmaxsim:.3e}'
))
simparFile = simulationPath + "simulation.ac.par"
with open(simparFile, 'w') as f:
  f.write(simpar)
```

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

```{python}
#| label: tbl-problem5_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"

result = subprocess.run(f"""ngspice_con -b -o "{simulationLog}" "{simulationFile}" """, shell=True, capture_output=True, text=True)

nodeList=["vdd","in","out","1","2"]
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))

Voutq=v_df.loc['out','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")
))
```

The operating information for each transistor can be extracted and are given in @tbl-problem5_op1 and @tbl-problem5_op2.

```{python}
#| label: tbl-problem5_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-problem5_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")
))
```

We see that the simulated transconductance of M~1~ $G_m =$ `{python} f'{ekv2p6mos_df.at["M1","Gm"]/1e-6:.3f}'` $\mu A/V$ is close to the theoretical estimation $G_m =$ `{python} f'{Gmopt/1e-6:.3f}'` $\mu A/V$.

The transfer function maginitude and phase are plotted in @fig-cs_min_current, which shows a good fit between the simulation and the theoretical estimation. The gain-bandwidth product $GBW$ is right on target. However, the simulated DC gain is slightly larger than the theoretical estimation. We also see some discrepancy at higher frequency. This is due to the zero introduced by the gate-to-drain capacitance $C_{GD}$.

```{python}
#| label: fig-cs_min_current
#| fig-cap: Simulated gain response compared to theoretical estimation for the long-channel case.

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

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]

Avphidegsim=Avphidegsim-180

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:
      fcsim=float(match.group(1))

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

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

fthe=fsim
fcthe=GBWthe/abs(Adcthe)
wcthe=2*pi*fcthe
#CF=3.5e-15
CF=CGDo
wzthe=Gm/CF

AvmagdBthe1=np.zeros(Npts)
Avphidegthe1=np.zeros(Npts)
AvmagdBthe2=np.zeros(Npts)
Avphidegthe2=np.zeros(Npts)
for k in range(0,Npts):
  jw=1j*2*pi*fthe[k]
  AvmagdBthe1[k]=20*log(abs(Av1(jw,Adcthe,wcthe)))
  Avphidegthe1[k]=np.angle(Av1(jw,Adcthe,wcthe),deg=True)
  AvmagdBthe2[k]=20*log(abs(Av2(jw,Adcthe,wcthe,wzthe)))
  Avphidegthe2[k]=np.angle(Av2(jw,Adcthe,wcthe,wzthe),deg=True)

Avphidegthe1=Avphidegthe1-180
Avphidegthe2=Avphidegthe2-180

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

axs[0].semilogx([fmin,GBWsim],[0,0],'k--', linewidth=lw)
axs[0].semilogx([GBWsim,GBWsim],[0,-60],'k--', linewidth=lw)
axs[0].semilogx(fsim,AvmagdBsim,'ro', markersize=4, markevery=15, label='Simulation')
axs[0].semilogx(fthe,AvmagdBthe1,'r-',label='Theory')
axs[0].semilogx(fthe,AvmagdBthe2,'r--',label='Theory (with zero)')
axs[0].set(xlim=(fmin,fmax), ylim=(-40,60), ylabel='$|A_{vd}|$ [dB]')
#axs[0].set_yticks([-25,-20,-15,-10,-5,0,5,10,15,20,25])
axs[0].legend(loc='lower left')
textstr1 = '\n'.join((
  f'$A_{{dc}} =$ {AdcdBthe:.1f} dB (theory)',
  f'$A_{{dc}} =$ {AdcdBsim:.1f} dB (sim)',
  f'$GBW =$ {GBWthe/1e6:.1f} MHz (theory)',
  f'$GBW =$ {GBWsim/1e6:.1f} MHz (sim.)',
  f'$f_c =$ {fcthe/1e3:.1f} kHz (theory)',
  f'$f_c =$ {fcsim/1e3:.1f} kHz (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,GBWsim],[PGBWsim,PGBWsim],'k--', linewidth=lw)
#axs[1].semilogx([GBWsim,GBWsim],[PGBWsim,0],'k--', linewidth=lw)
axs[1].semilogx(fsim,Avphidegsim,'bo', markersize=4, markevery=15, label='Simulation')
axs[1].semilogx(fthe,Avphidegthe1,'b-',label='Theory')
axs[1].semilogx(fthe,Avphidegthe2,'b--',label='Theory (with zero)')
axs[1].set(xlim=(fmin,fmax), ylim=(-360,-180), xlabel='Frequency [Hz]', ylabel='$arg\\{A_{vd}\\}$ [degree]')
axs[1].set_xticks([1e3,1e4,1e5,1e6,1e7,1e8,1e9,1e10])
axs[1].set_yticks([-360,-315,-270,-225,-180])
axs[1].legend(loc='lower left')
textstr2 = '\n'.join((
  f'$L =$ {L/1e-6:.0f} $\\mu m$',
  f'$W =$ {W/1e-6:.2f} $\\mu m$',
  f'$IC_{{opt}} =$ {ICopt:.3f}',
  f'$I_b =$ {Ib/1e-9:.0f} $nA$',
  f'$C_{{L0}} =$ {CL0/1e-15:.0f} $fF$',
  f'$C_L =$ {CL/1e-15:.3f} $fF$',
  f'$C_{{GD}} =$ {CGDo/1e-15:.3f} $fF$'))
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-cs_min_current we see that accounting for the gate-to-drain capacitance which corresponds actually to the overlap capacitance $C_{GDo}$, still underestimates the loction of the zero. This is due to a larger feedback capacitance. This is actually coming from the fact that we have ignored the transcapacitance $C_m$ in the small-signal analysis. In the quasi-static (QS) circuit of the MOS transistor, the transconductance needs to be replaced by the a transadmittance $Y_m$ given by
\begin{equation}
  Y_m = G_m + s\,C_m
\end{equation}
where $C_m$ is the gate transcapacitance given by
\begin{equation}
  C_m = C_{OX} \cdot c_m
\end{equation}
where $C_{OX} \triangleq W\,L\,C_{ox}$ and $c_m$ is the normalized gate transcapacitance given by @bib:enz:book:2006
\begin{equation}
  c_m = \frac{q_s-q_d}{15}\;\frac{4 q_s^2+4 q_d^2+12 q_s q_d+10 q_s+10 q_d+5}{(q_s+q_d+q)^3},
\end{equation}
which in saturation ($q_d=0$) reduces to
\begin{equation}
  c_{m_{sat}} \cong \frac{4 q_s^2+10 q_s+5}{(q_s+1)^3}.
\end{equation}
For a long-channel transistor, $q_s$ is simply given by
\begin{equation}
  q_s = \frac{\sqrt{4 IC+1}-1}{2}.
\end{equation}

The corresponding transfer function including the effect of the gate transcapacitance $C_m$ is then given by
\begin{equation}
  A(s) \triangleq \frac{\Delta V_{out}}{\Delta V_{in}} = A_{dc} \cdot \frac{1-s/\omega_z}{1+s/\omega_p}
\end{equation}
where
\begin{align}
  A_{dc} &= -G_m \cdot R_{ds},\\
  \omega_z &= \frac{G_m}{C_F+C_m},\\
  \omega_p &= \frac{1}{R_{ds}\,C_{out}},
\end{align}
with $C_{out} = C_L+C_F$ the total load capacitance. We see that the transcapacitance $C_m$ directly adds to the feedback capacitance $C_F$, but does not add to the load capacitance $C_{out}$. Therefore, the expression of the pole remains unchanged. The DC gain and the gain-bandwidth hence also remain unaffected by the transcapacitance. 

The theoretical transfer function including the effect of the gate transcapacitance is shown in @fig-cs_min_current_with_cm. We now see that the theoretical estimation perfectly matches the simulation including at high frequency.

```{python}
#| label: fig-cs_min_current_with_cm
#| fig-cap: Simulated gain response compared to theoretical estimation for the long-channel case.

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

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]

Avphidegsim=Avphidegsim-180

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:
      fcsim=float(match.group(1))

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

AdcdBsim=AvmagdBsim[0]
Adcsim=pow(10,AdcdBsim/20)
CFsim=Gmopt/(2*pi*fzsim)

fthe=fsim
fcthe=GBWthe/abs(Adcthe)
wcthe=2*pi*fcthe
qs=q_ic(ICopt)
COX=Wopt*Leff*Cox
Cm=COX*cm_qs_qd(qs,0)
CGDo=Wopt*CGDOn
CF=CGDo+Cm
wzthe=Gmopt/CF
fzthe=wzthe/(2*pi)
Adcinf=CF/(CF+CL)
AdcinfdB=20*log(Adcinf)

AvmagdBthe1=np.zeros(Npts)
Avphidegthe1=np.zeros(Npts)
AvmagdBthe2=np.zeros(Npts)
Avphidegthe2=np.zeros(Npts)
for k in range(0,Npts):
  jw=1j*2*pi*fthe[k]
  AvmagdBthe1[k]=20*log(abs(Av1(jw,Adcthe,wcthe)))
  Avphidegthe1[k]=np.angle(Av1(jw,Adcthe,wcthe),deg=True)
  AvmagdBthe2[k]=20*log(abs(Av2(jw,Adcthe,wcthe,wzthe)))
  Avphidegthe2[k]=np.angle(Av2(jw,Adcthe,wcthe,wzthe),deg=True)

Avphidegthe1=Avphidegthe1-180
Avphidegthe2=Avphidegthe2-180

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

axs[0].semilogx([fmin,GBWsim],[0,0],'k--', linewidth=lw)
axs[0].semilogx([GBWsim,GBWsim],[0,-60],'k--', linewidth=lw)
axs[0].semilogx(fsim,AvmagdBsim,'ro', markersize=4, markevery=15, label='Simulation')
axs[0].semilogx(fthe,AvmagdBthe1,'r-',label='Theory (without zero)')
axs[0].semilogx(fthe,AvmagdBthe2,'r--',label='Theory (incl. $C_{GDo}$ and $C_m$)')
axs[0].set(xlim=(fmin,fmax), ylim=(-40,60), ylabel='$|A_{vd}|$ [dB]')
#axs[0].set_yticks([-25,-20,-15,-10,-5,0,5,10,15,20,25])
axs[0].legend(loc='lower left')
textstr1 = '\n'.join((
  f'$A_{{dc}} =$ {AdcdBthe:.1f} dB (theory)',
  f'$A_{{dc}} =$ {AdcdBsim:.1f} dB (sim)',
  f'$GBW =$ {GBWthe/1e6:.1f} MHz (theory)',
  f'$GBW =$ {GBWsim/1e6:.1f} MHz (sim.)',
  f'$f_c =$ {fcthe/1e3:.1f} kHz (theory)',
  f'$f_c =$ {fcsim/1e3:.1f} kHz (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,GBWsim],[PGBWsim,PGBWsim],'k--', linewidth=lw)
#axs[1].semilogx([GBWsim,GBWsim],[PGBWsim,0],'k--', linewidth=lw)
axs[1].semilogx(fsim,Avphidegsim,'bo', markersize=4, markevery=15, label='Simulation')
axs[1].semilogx(fthe,Avphidegthe1,'b-',label='Theory (without zero)')
axs[1].semilogx(fthe,Avphidegthe2,'b--',label='Theory (incl. $C_{GDo}$ and $C_m$)')
axs[1].set(xlim=(fmin,fmax), ylim=(-360,-180), xlabel='Frequency [Hz]', ylabel='$arg\\{A_{vd}\\}$ [degree]')
axs[1].set_xticks([1e3,1e4,1e5,1e6,1e7,1e8,1e9,1e10])
axs[1].set_yticks([-360,-315,-270,-225,-180])
axs[1].legend(loc='lower left')
textstr2 = '\n'.join((
  f'$L =$ {L/1e-6:.0f} $\\mu m$',
  f'$W =$ {W/1e-6:.2f} $\\mu m$',
  f'$IC_{{opt}} =$ {ICopt:.3f}',
  f'$I_b =$ {Ib/1e-9:.0f} $nA$',
  f'$C_{{L0}} =$ {CL0/1e-15:.0f} $fF$',
  f'$C_L =$ {CL/1e-15:.3f} $fF$',
  f'$C_{{GDo}} =$ {CGDo/1e-15:.3f} $fF$',
  f'$C_m =$ {Cm/1e-15:.3f} $fF$',
  f'$C_F =$ {CF/1e-15:.3f} $fF$ (theory)',
  f'$C_F =$ {CFsim/1e-15:.3f} $fF$ (simulation)'))
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)
```


# Problem 6: Minimum current for a given DC gain and gain-bandwidth product with self-loading
In this problem we want to find the minimum current required to achieve a given gain-bandwidth product and a given dc gain accounting for the effect of self-loading. We can actually use the additional degree of freedom, namely the transistor length $L$ (which has been arbitrarily set in the previous example), to set the DC gain. To this purpose we can use the simple output conductance model given by
\begin{equation}
  G_{ds} \cong \frac{I_D}{\lambda \cdot L}.
\end{equation}

It can be shown that there is also an optimum value of the inversion coefficient for which the bias current becomes minimum @bib:enz:github:2025
\begin{equation}
  IC_{opt} = \frac{\sqrt{\xi\;}}{\left(1-\sqrt{\xi\;}\right)^2},
\end{equation}
where parameter $\xi$ brings together the technology parameters and the specs according to
\begin{equation}
  \xi \triangleq \underbrace{A_{dc} \cdot \omega_u}_{\textsf{specifications}} \cdot \underbrace{\frac{C_{DW} \cdot (n U_T)^2}{I_{spec\Box} \cdot \lambda}}_{\textsf{technology parameters}}.
\end{equation}

It can be shown that parameter $\xi$ needs to satisfy the following inequality
\begin{equation*}
  \xi \triangleq A_{dc} \cdot \omega_u \cdot \frac{C_{DW} \cdot (n U_T)^2}{I_{spec\Box} \cdot \lambda} < 1.
\end{equation*}
This means that we can check whether the specifications on $A_{dc}$ and $\omega_u$ can be achieved for a given technology (i.e. for a set of $I_{spec\Box}$, $\lambda$ and $C_{DW}$). The closer $\xi$ gets to 1, the larger the required current and size.

We can define the normalized bias current $i_b$, transistor width $w$ and length $\ell$ according to
\begin{align}
  i_b &\triangleq \frac{I_b}{I_{norm}} = \frac{g_{ms} \cdot IC}{g_{ms}^2 - \xi \cdot IC}
  = \frac{g_{ms}/IC}{(g_{ms}/IC)^2 - \xi/IC},\\
  w &\triangleq \frac{W}{W_{norm}} = \frac{IC}{g_{ms}^2 - \xi \cdot IC},\\
  \ell &\triangleq \frac{L}{L_{norm}} = \frac{IC}{g_{ms}},
\end{align}
where
\begin{align}
  I_{norm} & \triangleq n\,U_T \cdot C_{L0} \cdot \omega_u,\\
  W_{norm} &\triangleq \frac{C_{L0} \cdot (n U_T)^2}{I_{spec\Box} \cdot \lambda} \cdot A_{dc} \cdot \omega_u,\\
  L_{norm} & \triangleq \frac{n U_T}{\lambda} \cdot A_{dc}.
\end{align}

The minimum normalized current is then given by
\begin{equation}
  i_{b,opt} \triangleq i_b(IC_{opt}) = \frac{1}{\left(1-\sqrt{\xi\;}\right)^2} = \frac{1+2 IC_{opt}+\sqrt{4 IC_{opt}+1}}{2}.
\end{equation}

The optimum normalized width, length and transconductance are given by
\begin{align}
  w_{opt} &\triangleq w(IC_{opt}) = \frac{1}{\sqrt{\xi\;}\;\left(1-\sqrt{\xi\;}\right)} = \frac{1+3IC_{opt}+(1+IC_{opt})\,\sqrt{4IC_{opt}+1}}{2IC_{opt}},\\
  \ell_{opt} &\triangleq \ell(IC_{opt}) = \frac{1}{1-\sqrt{\xi\;}} = \frac{1+\sqrt{4IC_{opt}+1}}{2},\\
  g_{m,opt} &\triangleq g_m(IC_{opt}) = \frac{1}{1-\sqrt{\xi\;}} = \frac{1+\sqrt{4IC_{opt}+1}}{2}.
\end{align}

Note that the optimum current, width, length and transconductance only depend on $\xi$.

## Specifications
The specifications for Problem 5 are given in @tbl-specs_problem6.

```{python}
#| label: tbl-specs_problem6
#| tbl-cap: Specifications for problem 6.

from specs_problem6 import *

table = [["DC gain","$A_{dc}$",f'{AdcdBspec:.0f}',"$dB$"],
         ["Gain-bandwidth product","$GBW$",f'{GBWspec/1e6:.0f}',"$MHz$"],
         ["Load capacitance","$C_{L0}$",f'{CL0/1e-15:.0f}',"$fF$"]
        ]

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

## Design

```{python}
def ICopt_xi(xi):
  sqrtxi=sqrt(xi)
  if xi<1:
    ICopt=sqrtxi/(1-sqrtxi)**2
  else:
    ICopt=float("nan")
  return ICopt

def ibopt_xi(xi):
  if xi<1:
    ibopt=1/(1-sqrt(xi))**2
  else:
    ibopt=float("nan")
  return ibopt

def wopt_xi(xi):
  sqrtxi=sqrt(xi)
  if xi<1:
    wopt=1/(sqrtxi*(1-sqrtxi))
  else:
    wopt=float("nan")
  return wopt

def lopt_xi(xi):
  if xi<1:
    lopt=1/(1-sqrt(xi))
  else:
    lopt=float("nan")
  return lopt

n=n0n
nUT=n*UT
Ispecsq=Ispecsqn
lambdaL=lambdan
fu=GBWspec
wu=2*pi*fu
Adc=pow(10,AdcdBspec/20)
xi=CDW*nUT**2/(Ispecsq*lambdaL)*Adc*wu
CL=CL0+CD0
Inorm=wu*CL*nUT
Wnorm=CL*nUT**2/(Ispecsq*lambdaL)*Adc*wu
Lnorm=nUT/lambdaL*Adc
Gmnorm=CL*wu
ICopt=ICopt_xi(xi)
ibopt=ibopt_xi(xi)
wopt=wopt_xi(xi)
lopt=lopt_xi(xi)
gmopt=lopt
Ibopt=ibopt*Inorm
Wopt=wopt*Wnorm
Lopt=lopt*Lnorm
Gmopt=gmopt*Gmnorm
CDopt=CDW*Wopt
AD=2*Hdif*Wopt
PD=2*(2*Hdif+Wopt)
CDBJ=AD*CJn+PD*CJSWn
CF=Wopt*CGDOn
Cout=CL+CDopt
wuthe=Gmopt/Cout
GBWthe=wuthe/(2*pi)
Gdsopt=Ibopt/(lambdan*Lopt)
Adcthe=Gmopt/Gdsopt
AdcdBthe=20*log(Adcthe)
wcthe=wuthe/Adcthe
fcthe=wcthe/(2*pi)
wzthe=Gmopt/CF
fzthe=wzthe/(2*pi)
```

We can start by checking that the specifications in @tbl-specs_problem6 are feasible for the given 180nm technology. For this we can check that
\begin{equation*}
  \xi \triangleq A_{dc} \cdot \omega_u \cdot \frac{C_{DW} \cdot (n U_T)^2}{I_{spec\Box} \cdot \lambda} < 1.
\end{equation*}
For the specifications in @tbl-specs_problem6 and the chosen 180nm technology we have $\xi =$ `{python} f'{xi:.3f}'` which is smaller than 1.

```{python}
#| label: tbl-parameters_problem6
#| tbl-cap: Parameters or problem 6.

table = [["$C_{{DW}}$",f'{CDW/1e-9:.3f}',"$fF/\\mu m$"],
         ["$C_{{D0}}$",f'{CD0/1e-15:.3f}',"$fF$"],
         ["$C_{{L0}}$",f'{CL0/1e-15:.3f}',"$fF$"],
         ["$C_L$",f'{CL/1e-15:.3f}',"$fF$"],
         ["$\\xi$",f'{xi:.3f}',"-"],
         ["$IC_{{opt}}$",f'{ICopt:.3f}',"-"],
         ["$i_{{b,opt}}$",f'{ibopt:.3f}',"-"],
         ["$w_{{opt}}$",f'{wopt:.3f}',"-"],
         ["$\\ell_{{opt}}$",f'{lopt:.3f}',"-"],
         ["$g_{{m,opt}}$",f'{gmopt:.3f}',"-"],
         ["$I_{{b,opt}}$",f'{Ibopt/1e-9:.0f}',"$nA$"],
         ["$\\left(W/L\\right)_{{opt}}$",f'{WoverLopt:.3f}',"-"],
         ["$I_{{norm}}$",f'{Inorm/1e-9:.0f}',"$nA$"],
         ["$W_{{norm}}$",f'{Wnorm/1e-9:.0f}',"$nm$"],
         ["$L_{{norm}}$",f'{Lnorm/1e-9:.0f}',"$nm$"],
         ["$G_{{m,norm}}$",f'{Gmnorm/1e-6:.3f}',"$\\mu A/V$"],
         ["$I_{{b,opt}}$",f'{Ibopt/1e-9:.3f}',"$nA$"],
         ["$W_{{opt}}$",f'{Wopt/1e-6:.2f}',"$\\mu m$"],
         ["$L_{{opt}}$",f'{Lopt/1e-9:.2f}',"$nm$"],
         ["$G_{{m,opt}}$",f'{Gmopt/1e-6:.3f}',"$\\mu A/V$"],
         ["$C_{{D,opt}}$",f'{CDopt/1e-15:.3f}',"$fF$"],
         ["$AD$",f'{AD/1e-12:.2f}',"$\\mu m^2$"],
         ["$PD$",f'{PD/1e-6:.3f}',"$\\mu m$"],
         ["$C_{{DBJ}}$",f'{CDBJ/1e-15:.3f}',"$fF$"],
         ["$C_F$",f'{CF/1e-15:.3f}',"$fF$"],
         ["$C_{{out}}$",f'{Cout/1e-15:.3f}',"$fF$"],
         ["$GBW$ (check)",f'{GBWthe/1e6:.0f}',"$MHz$"],
         ["$f_z$",f'{wz/(2*pi*1e9):.3f}',"$GHz$"],
         ["$A_{{dc}}$",f'{AdcdBthe:.3f}',"$dB$"]
        ]

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

## Simulations
We can now check whether we get the correct GBW and DC gain using ngspice simulations with the schematic shown in @fig-CS_sim_sch. The simulations will be performed with the following parameters:

```{python}
paramstr = '\n'.join((
  f'.param VDD={VDD:.1f} Ib={Ibopt/1e-9:.0f}n',
  f'.param CL={CL0/1e-15:.2f}f W={Wopt/1e-6:.2f}u L={Lopt/1e-6:.2f}u AD={AD:.2e} PD={PD:.2e}',
#  f'.param CL={CL0/1e-15:.2f}f W={Wopt/1e-6:.2f}u L={L/1e-6:.2f}u' 
  ))
print(paramstr)
with open(simulationPath+'size_bias.par', 'w') as f:
  f.write(paramstr)

Ndec=101
fminsim=1e3
fmaxsim=1e10

simpar = '\n'.join((
  f'.csparam Ndec={Ndec:.0f}',
  f'.csparam fmin={fminsim:.3e}',
  f'.csparam fmax={fmaxsim:.3e}'
))
simparFile = simulationPath + "simulation.ac.par"
with open(simparFile, 'w') as f:
  f.write(simpar)
```


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

```{python}
#| label: tbl-problem6_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"

result = subprocess.run(f"""ngspice_con -b -o "{simulationLog}" "{simulationFile}" """, shell=True, capture_output=True, text=True)

nodeList=["vdd","in","out","1","2"]
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))

Voutq=v_df.loc['out','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")
))
```

The operating information for each transistor can be extracted and are given in @tbl-problem6_op1 and @tbl-problem6_op2.

```{python}
#| label: tbl-problem6_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-problem6_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")
))
```

We see that the simulated transconductance of M~1~ $G_m =$ `{python} f'{ekv2p6mos_df.at["M1","Gm"]/1e-6:.3f}'` $\mu A/V$ is close to the theoretical estimation $G_m =$ `{python} f'{Gmopt/1e-6:.3f}'` $\mu A/V$.

The transfer function magnitude and phase are plotted in @fig-cs_adc_gbw, which shows an almost perfect fit between the simulation and the theoretical estimation. The DC gain and gain-bandwidth product are right on target. Even the high frequency behavior is well captured because of the good estimation of the total gate-to-drain capacitance including the contribution of the gate transcapacitance.

```{python}
#| label: fig-cs_adc_gbw
#| fig-cap: Simulated gain response compared to theoretical estimation for the long-channel case.

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

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]

Avphidegsim=Avphidegsim-180

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:
      fcsim=float(match.group(1))

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

AdcdBsim=AvmagdBsim[0]
Adcsim=pow(10,AdcdBsim/20)
CFsim=Gmopt/(2*pi*fzsim)

fthe=fsim
fcthe=GBWthe/abs(Adcthe)
wcthe=2*pi*fcthe
qs=q_ic(ICopt)
COX=Wopt*Leff*Cox
Cm=COX*cm_qs_qd(qs,0)
CGDo=Wopt*CGDOn
CF=CGDo+Cm
wzthe=Gmopt/CF
fzthe=wzthe/(2*pi)
Adcinf=CF/(CF+CL)
AdcinfdB=20*log(Adcinf)

AvmagdBthe1=np.zeros(Npts)
Avphidegthe1=np.zeros(Npts)
AvmagdBthe2=np.zeros(Npts)
Avphidegthe2=np.zeros(Npts)
for k in range(0,Npts):
  jw=1j*2*pi*fthe[k]
  AvmagdBthe1[k]=20*log(abs(Av1(jw,Adcthe,wcthe)))
  Avphidegthe1[k]=np.angle(Av1(jw,Adcthe,wcthe),deg=True)
  AvmagdBthe2[k]=20*log(abs(Av2(jw,Adcthe,wcthe,wzthe)))
  Avphidegthe2[k]=np.angle(Av2(jw,Adcthe,wcthe,wzthe),deg=True)

Avphidegthe1=Avphidegthe1-180
Avphidegthe2=Avphidegthe2-180

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

axs[0].semilogx([fmin,GBWsim],[0,0],'k--', linewidth=lw)
axs[0].semilogx([GBWsim,GBWsim],[0,-60],'k--', linewidth=lw)
axs[0].semilogx(fsim,AvmagdBsim,'ro', markersize=4, markevery=15, label='Simulation')
axs[0].semilogx(fthe,AvmagdBthe1,'r-',label='Theory (without zero)')
axs[0].semilogx(fthe,AvmagdBthe2,'r--',label='Theory (incl. $C_{GDo}$ and $C_m$)')
axs[0].set(xlim=(fmin,fmax), ylim=(-40,60), ylabel='$|A_{vd}|$ [dB]')
#axs[0].set_yticks([-25,-20,-15,-10,-5,0,5,10,15,20,25])
axs[0].legend(loc='lower left')
textstr1 = '\n'.join((
  f'$A_{{dc}} =$ {AdcdBthe:.1f} dB (theory)',
  f'$A_{{dc}} =$ {AdcdBsim:.1f} dB (sim)',
  f'$GBW =$ {GBWthe/1e6:.1f} MHz (theory)',
  f'$GBW =$ {GBWsim/1e6:.1f} MHz (sim.)',
  f'$f_c =$ {fcthe/1e3:.1f} kHz (theory)',
  f'$f_c =$ {fcsim/1e3:.1f} kHz (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,GBWsim],[PGBWsim,PGBWsim],'k--', linewidth=lw)
#axs[1].semilogx([GBWsim,GBWsim],[PGBWsim,0],'k--', linewidth=lw)
axs[1].semilogx(fsim,Avphidegsim,'bo', markersize=4, markevery=15, label='Simulation')
axs[1].semilogx(fthe,Avphidegthe1,'b-',label='Theory (without zero)')
axs[1].semilogx(fthe,Avphidegthe2,'b--',label='Theory (incl. $C_{GDo}$ and $C_m$)')
axs[1].set(xlim=(fmin,fmax), ylim=(-360,-180), xlabel='Frequency [Hz]', ylabel='$arg\\{A_{vd}\\}$ [degree]')
axs[1].set_xticks([1e3,1e4,1e5,1e6,1e7,1e8,1e9,1e10])
axs[1].set_yticks([-360,-315,-270,-225,-180])
axs[1].legend(loc='lower left')
textstr2 = '\n'.join((
  f'$L =$ {Lopt/1e-9:.0f} $nm$',
  f'$W =$ {Wopt/1e-6:.3f} $\mu m$',
  f'$IC_{{opt}} =$ {ICopt:.3f}',
  f'$I_b =$ {Ib/1e-9:.0f} $nA$',
  f'$C_{{L0}} =$ {CL0/1e-15:.0f} $fF$',
  f'$C_L =$ {CL/1e-15:.3f} $fF$',
  f'$C_{{GDo}} =$ {CGDo/1e-15:.3f} $fF$',
  f'$C_m =$ {Cm/1e-15:.3f} $fF$',
  f'$C_F =$ {CF/1e-15:.3f} $fF$ (theory)',
  f'$C_F =$ {CFsim/1e-15:.3f} $fF$ (simulation)'))
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)
```

# Conclusion
This exercise has shown various ways to design a single transistor setting its bias current and choosing its width and length for reaching different specifications. It has also illustrated the role of parasitic capacitances and how to estimate them. All the designs have been succesfully checked with simulations using ngspice and the EKV 2.6 compact model.


# References