IHP-AnalogAcademy/modules/module_0_foundations/scripting/gmid_commonsource.ipynb

535 lines
18 KiB
Plaintext

{
"cells": [
{
"cell_type": "markdown",
"id": "96c73c5c-d26f-4710-9dce-8b67c625438e",
"metadata": {},
"source": [
"# Simple setup for using gm/id with IHP open PDK"
]
},
{
"cell_type": "code",
"execution_count": 4,
"id": "93924bd4-4ba6-4275-8643-a10c59a209e7",
"metadata": {},
"outputs": [],
"source": [
"import numpy as np\n",
"#for windows\n",
"#import sys\n",
"#sys.path.append(r'C:\\Users\\....\\gmid') # path to gmid repository\n",
"# ------\n",
"import matplotlib.pyplot as plt\n",
"from mosplot import load_lookup_table, LoadMosfet # make sure that mosplot can be found in the python path\n",
"import ipywidgets as widgets\n",
"from ipywidgets import interactive\n",
"from ipywidgets import interactive_output, HBox, VBox\n",
"import matplotlib.ticker as ticker "
]
},
{
"cell_type": "code",
"execution_count": 5,
"id": "16b9f998-3107-436e-842d-c588dc4cbfee",
"metadata": {},
"outputs": [],
"source": [
"pmos_lv_path = '/home/pedersen/projects/IHP-AnalogAcademy/modules/module_0_foundations/gmid_sweeps/pmos_lv_sweep.npy'\n",
"nmos_lv_path ='/home/pedersen/projects/IHP-AnalogAcademy/modules/module_0_foundations/gmid_sweeps/nmos_lv_sweep.npy'\n",
"\n",
"lookup_table_pmos = load_lookup_table(pmos_lv_path)\n",
"lookup_table_nmos = load_lookup_table(nmos_lv_path)"
]
},
{
"cell_type": "code",
"execution_count": 6,
"id": "1eae8452-b1d5-4611-bb86-4f2f222b2929",
"metadata": {},
"outputs": [],
"source": [
"nmos = LoadMosfet(lookup_table=lookup_table_nmos, mos=\"nmos\", vsb=0.0, vds=0.4)\n",
"pmos = LoadMosfet(lookup_table=lookup_table_pmos, mos=\"pmos\", vsb=0, vds=-0.6, vgs=(-1.2, -0.1))"
]
},
{
"cell_type": "markdown",
"id": "576b86e1-caae-496f-a644-584ff4fb5dd6",
"metadata": {},
"source": [
"# Function Definitions"
]
},
{
"cell_type": "code",
"execution_count": 7,
"id": "7abd7d3e-cf3d-4643-b5b3-d35f2362f7b7",
"metadata": {
"jupyter": {
"source_hidden": true
}
},
"outputs": [],
"source": [
"def plot_data_vs_data(x_values, y_values, z_values, length, x_axis_name, y_axis_name='y', y_multiplier=1, log=False):\n",
" x_values_flat = np.array(x_values).flatten()\n",
" y_values_flat = np.array(y_values, dtype=np.float64).flatten()\n",
" z_values_flat = np.array(z_values, dtype=np.float64).flatten()\n",
" length_flat = np.array(length).flatten()\n",
"\n",
" # Ensure all inputs have the same length\n",
" if not (len(x_values_flat) == len(y_values_flat) == len(z_values_flat) == len(length_flat)):\n",
" raise ValueError(\"All input arrays (x_values, y_values, z_values, length) must have the same number of elements.\")\n",
"\n",
" unique_lengths = np.unique(length_flat)\n",
" unique_lengths_in_micro = unique_lengths * 1e6\n",
"\n",
" def update_plot(selected_length, x_value=None, y_value=None):\n",
" plt.figure(figsize=(8, 6)) # Ensure the plot is drawn fresh for each update\n",
"\n",
" if selected_length == \"Show All\":\n",
" mask = np.ones_like(length_flat, dtype=bool)\n",
" else:\n",
" selected_length_in_micro = float(selected_length.replace(' μm', ''))\n",
" tolerance = 0.1\n",
" # Recalculate the mask with matching shapes\n",
" mask = np.abs(length_flat * 1e6 - selected_length_in_micro) < tolerance\n",
"\n",
" x_values_for_length = x_values_flat[mask]\n",
" y_values_for_length = y_values_flat[mask] * y_multiplier\n",
" z_values_for_length = z_values_flat[mask]\n",
" length_for_length = length_flat[mask] * 1e6\n",
"\n",
" if selected_length == \"Show All\":\n",
" for length_value in np.unique(length_for_length):\n",
" mask_all = (length_for_length == length_value)\n",
" plt.plot(x_values_for_length[mask_all], y_values_for_length[mask_all])\n",
"\n",
" min_length = np.min(unique_lengths_in_micro)\n",
" max_length = np.max(unique_lengths_in_micro)\n",
" plt.title(f'{y_axis_name} vs {x_axis_name} (Length from {min_length:.2f} μm to {max_length:.2f} μm)')\n",
"\n",
" else:\n",
" plt.plot(x_values_for_length, y_values_for_length)\n",
" plt.title(f'{y_axis_name} vs {x_axis_name} for {selected_length}')\n",
"\n",
" plt.xlabel(f'{x_axis_name}')\n",
" plt.ylabel(f'{y_axis_name}')\n",
"\n",
" if log:\n",
" plt.yscale('log')\n",
" plt.gca().yaxis.set_major_locator(ticker.LogLocator(base=10, subs=[], numticks=10))\n",
" plt.gca().yaxis.set_major_formatter(ticker.FuncFormatter(lambda x, _: f'$10^{int(np.log10(x))}$'))\n",
" plt.ylabel(f'{y_axis_name} (Log Base 10)')\n",
"\n",
" if y_value is not None and x_value_widget.disabled:\n",
" closest_index = np.abs(y_values_for_length - y_value).argmin()\n",
" closest_x = x_values_for_length[closest_index]\n",
" closest_y = y_values_for_length[closest_index]\n",
" corresponding_z = z_values_for_length[closest_index]\n",
"\n",
" plt.scatter(closest_x, closest_y, color='blue', label=f'Point ({closest_x:.2f}, {closest_y:.2f})')\n",
" z_value_widget.value = corresponding_z\n",
" print(f\"The corresponding {x_axis_name} value for {y_axis_name} = {closest_y:.2f} is: {closest_x:.2f}\")\n",
" elif x_value is not None and y_value_widget.disabled:\n",
" closest_index = np.abs(x_values_for_length - x_value).argmin()\n",
" closest_x = x_values_for_length[closest_index]\n",
" closest_y = y_values_for_length[closest_index]\n",
" corresponding_z = z_values_for_length[closest_index]\n",
"\n",
" plt.scatter(closest_x, closest_y, color='red', label=f'Point ({closest_x:.2f}, {closest_y:.2f})')\n",
" z_value_widget.value = corresponding_z\n",
" print(f\"The corresponding {y_axis_name} value for {x_axis_name} = {closest_x:.2f} is: {closest_y:.2f}\")\n",
"\n",
" plt.grid(True)\n",
" plt.legend()\n",
" plt.show()\n",
"\n",
" dropdown_options = [\"Show All\"] + [f'{length:.2f} μm' for length in unique_lengths_in_micro]\n",
" length_widget = widgets.Dropdown(\n",
" options=dropdown_options,\n",
" value=dropdown_options[0],\n",
" description='Select Length:',\n",
" )\n",
"\n",
" x_value_widget = widgets.FloatText(\n",
" value=np.mean(x_values_flat),\n",
" description=f\"Select {x_axis_name}:\",\n",
" disabled=False\n",
" )\n",
"\n",
" y_value_widget = widgets.FloatText(\n",
" value=None,\n",
" description=f\"Set {y_axis_name}:\",\n",
" disabled=True\n",
" )\n",
"\n",
" z_value_widget = widgets.FloatText(\n",
" value=None,\n",
" description=f\"Corresponding z value:\",\n",
" disabled=True\n",
" )\n",
"\n",
" select_x_or_y_widget = widgets.Checkbox(\n",
" value=True,\n",
" description=f\"Select {x_axis_name} (uncheck for {y_axis_name})\",\n",
" )\n",
"\n",
" def toggle_x_or_y(change):\n",
" if change['new']:\n",
" x_value_widget.disabled = False\n",
" y_value_widget.disabled = True\n",
" else:\n",
" x_value_widget.disabled = True\n",
" y_value_widget.disabled = False\n",
"\n",
" select_x_or_y_widget.observe(toggle_x_or_y, names='value')\n",
"\n",
" output = interactive_output(update_plot, {\n",
" 'selected_length': length_widget,\n",
" 'x_value': x_value_widget,\n",
" 'y_value': y_value_widget\n",
" })\n",
"\n",
" display(VBox([length_widget, select_x_or_y_widget, HBox([x_value_widget, y_value_widget]), z_value_widget, output]))\n",
"\n",
"\n",
"def display_resistance(ro_value):\n",
" \"\"\"Determine the resistance value and its unit.\"\"\"\n",
" if ro_value < 1e3:\n",
" return ro_value, \"Ω\"\n",
" elif ro_value < 1e6:\n",
" return ro_value / 1e3, \"kΩ\"\n",
" elif ro_value < 1e9:\n",
" return ro_value / 1e6, \"MΩ\"\n",
" else:\n",
" return ro_value / 1e9, \"GΩ\"\n",
"\n",
"def display_current(Id_value):\n",
" \"\"\"Determine the current value and its unit.\"\"\"\n",
" if Id_value < 1e-6:\n",
" return Id_value * 1e9, \"nA\" # Convert to nA\n",
" elif Id_value < 1e-3:\n",
" return Id_value * 1e6, \"μA\" # Convert to μA\n",
" else:\n",
" return Id_value * 1e3, \"mA\" # Convert to mA\n",
" \n",
"def dB_to_linear(av_db):\n",
" return 10 ** (av_db / 20)\n",
"\n",
"\n",
"def determine_inversion_region(gm_id_value, device_type):\n",
" \"\"\"Determine the inversion region based on gm/id value for NMOS or PMOS.\"\"\"\n",
" if device_type == 'nmos':\n",
" if gm_id_value > 20:\n",
" return \"Weak Inversion\"\n",
" elif 10 < gm_id_value <= 20:\n",
" return \"Moderate Inversion\"\n",
" else:\n",
" return \"Strong Inversion\"\n",
" elif device_type == 'pmos':\n",
" if gm_id_value > 20:\n",
" return \"Weak Inversion\"\n",
" elif 10 < gm_id_value <= 20:\n",
" return \"Moderate Inversion\"\n",
" else:\n",
" return \"Strong Inversion\"\n",
" else:\n",
" raise ValueError(\"Invalid device type. Use 'nmos' or 'pmos'.\")\n",
" \n"
]
},
{
"cell_type": "markdown",
"id": "57bc7b38-a0e8-40c3-90c5-bd7c2bf03df4",
"metadata": {},
"source": [
"# Plotting examples\n",
"\n",
"Note: In my cases i will be using my own defined functions to plot more interatively. You can refer to the standard repository to see how to plot using the native functions in the repo..."
]
},
{
"cell_type": "code",
"execution_count": 8,
"id": "cdfafca1-e7c0-4491-8a38-5785f38c8d60",
"metadata": {},
"outputs": [
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "b02c4d3ead7440c2803417b6fcc005cf",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
"VBox(children=(Dropdown(description='Select Length:', options=('Show All', '0.13 μm', '0.26 μm', '0.39 μm', '0…"
]
},
"metadata": {},
"output_type": "display_data"
},
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "03c6dd08648e483ea151c95baae95aa4",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
"VBox(children=(Dropdown(description='Select Length:', options=('Show All', '0.13 μm', '0.26 μm', '0.39 μm', '0…"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"# Lets start by plotting the intrinsic gain of both pmos and nmos as a function of gm/id. We start by setting the data arrays\n",
"\n",
"id_values_nmos = nmos.extracted_table['id']\n",
"gm_values_nmos = nmos.extracted_table['gm']\n",
"gds_values_nmos = nmos.extracted_table['gds']\n",
"vgs_values_nmos = nmos.extracted_table['vgs']\n",
"\n",
"id_values_pmos = pmos.extracted_table['id']\n",
"gm_values_pmos = pmos.extracted_table['gm']\n",
"gds_values_pmos = pmos.extracted_table['gds']\n",
"vgs_values_pmos = pmos.extracted_table['vgs']\n",
"\n",
"\n",
"plot_data_vs_data(gm_values_nmos/id_values_nmos, gm_values_nmos/gds_values_nmos, vgs_values_nmos, nmos.extracted_table['lengths'], 'gm/id', 'gds') # plotting nmos data\n",
"plot_data_vs_data(gm_values_pmos/id_values_pmos, gm_values_pmos/gds_values_pmos, vgs_values_pmos, pmos.extracted_table['lengths'], 'gm/id', 'gds') # plotting pmos data"
]
},
{
"cell_type": "markdown",
"id": "1cd1cfe6-8260-4b9f-8166-f953758c0563",
"metadata": {},
"source": [
"# Verifying the models\n",
"To ensure that the gmid library is working properly we will simply set the dimensions for a nmos a varify in xschem"
]
},
{
"cell_type": "code",
"execution_count": 9,
"id": "7dd17dfa-abd2-4c76-9fd4-1f7824fdaa58",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"1.0 μA\n"
]
}
],
"source": [
"gmid = 18 #moderate inversion\n",
"id = 1e-6\n",
"gm = gmid*id\n",
"display_id, unit_id = display_current(id)\n",
"print(f'{display_id} {unit_id}')"
]
},
{
"cell_type": "markdown",
"id": "c5edba7b-f8ec-4405-a7b3-908489a7c47f",
"metadata": {},
"source": [
"# Choosing the channel length by quick overview"
]
},
{
"cell_type": "markdown",
"id": "5a570be2-c30c-456e-ad10-f18870d1d27e",
"metadata": {},
"source": [
"Here the focus is on getting a high intrinsic gain"
]
},
{
"cell_type": "code",
"execution_count": 10,
"id": "7fd6ceee-3d40-460f-82d6-6d672d0d337d",
"metadata": {},
"outputs": [
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "82d7c57bbc6f44a79891a51bcd92c82e",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
"VBox(children=(Dropdown(description='Select Length:', options=('Show All', '0.13 μm', '0.26 μm', '0.39 μm', '0…"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"plot_data_vs_data(gm_values_nmos/id_values_nmos, gm_values_nmos/gds_values_nmos, vgs_values_nmos, nmos.extracted_table['lengths'], 'gm/id', 'gds') # plotting nmos data"
]
},
{
"cell_type": "code",
"execution_count": 11,
"id": "3fa1a702-34dd-43de-a95e-a6654fea16eb",
"metadata": {},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"R_load = 2.25 MΩ\n"
]
}
],
"source": [
"# By sweeping through the lenghts for a fixed gm/id we see that the lengths is approximatly 3.25e-6\n",
"Lnmos = 3.25e-6\n",
"gmro = 40.44\n",
"ro = gmro/gm\n",
"display_ro, unit_ro = display_resistance(ro)\n",
"print(f'R_load = {display_ro:.2f} {unit_ro}')"
]
},
{
"cell_type": "markdown",
"id": "20cf0ad2-022a-45d6-8a1f-7c41fd2e26c7",
"metadata": {},
"source": [
"# Now we want to find the Corresponding width"
]
},
{
"cell_type": "code",
"execution_count": 12,
"id": "eb54a54a-491b-4c94-8d34-d14457474b25",
"metadata": {},
"outputs": [
{
"data": {
"application/vnd.jupyter.widget-view+json": {
"model_id": "9056d3790ad7459bbf57e878d655cce1",
"version_major": 2,
"version_minor": 0
},
"text/plain": [
"VBox(children=(Dropdown(description='Select Length:', options=('Show All', '0.13 μm', '0.26 μm', '0.39 μm', '0…"
]
},
"metadata": {},
"output_type": "display_data"
}
],
"source": [
"width_nmos = nmos.extracted_table['width']\n",
"plot_data_vs_data(gm_values_nmos/id_values_nmos, id_values_nmos/width_nmos, vgs_values_nmos, nmos.extracted_table['lengths'], 'gm/id', 'id/W', log=True) # plotting nmos data"
]
},
{
"cell_type": "code",
"execution_count": 13,
"id": "e6e2b885-e999-4e29-9a07-d9183db4c021",
"metadata": {},
"outputs": [],
"source": [
"# We see that for a gm/id of 18 and length of 3.25 our, id/W is given as 0.3\n",
"id_over_width_nmos = 0.3\n",
"Wnmos = id/id_over_width_nmos"
]
},
{
"cell_type": "code",
"execution_count": 14,
"id": "9ff5d012-44c7-4acb-b7e2-19ff15312594",
"metadata": {
"scrolled": true
},
"outputs": [
{
"name": "stdout",
"output_type": "stream",
"text": [
"\n",
"Width and Length for NMOS\n",
" W = 3.33 um\n",
" L = 3.25 um\n",
"Inversion Region for NMOS: Moderate Inversion\n",
"\n",
"Bias Current:\n",
" Id = 1.00μA\n",
"Parameters to check\n",
" gm/gds = 40.44\n",
" gm = 0.018 mS\n",
" ro = 2.25MΩ\n",
"\n",
"\n"
]
}
],
"source": [
"# Now we can summarize everything\n",
"\n",
"single_transistor_summary = f\"\"\"\n",
"Width and Length for NMOS\n",
" W = {Wnmos*1e6:.2f} um\n",
" L = {Lnmos*1e6:.2f} um\n",
"Inversion Region for NMOS: {determine_inversion_region(gmid, 'nmos')}\n",
"\n",
"Bias Current:\n",
" Id = {display_id:.2f}{unit_id}\n",
"Parameters to check\n",
" gm/gds = {gmro:.2f}\n",
" gm = {gm*1e3:.2} mS\n",
" ro = {display_ro:.2f}{unit_ro}\n",
"\n",
"\"\"\"\n",
"print(single_transistor_summary)"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "5a0c63c5-99f4-46ae-a827-bb7525950798",
"metadata": {},
"outputs": [],
"source": []
},
{
"cell_type": "code",
"execution_count": null,
"id": "16b8b4f2-807a-4f75-8bc1-cf12a7d1ec0f",
"metadata": {},
"outputs": [],
"source": []
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3 (ipykernel)",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.12"
}
},
"nbformat": 4,
"nbformat_minor": 5
}