{ "cells": [ { "cell_type": "markdown", "id": "d827dcd0", "metadata": {}, "source": [ "# COMSOL transmon capacitance and field\n", "\n", "::::{admonition} Required extras\n", ":class: tip\n", "\n", "This notebook needs the `comsol` extra (`uv sync --extra comsol`). It installs `MPh`, the Python client for\n", "COMSOL, and nothing else: not COMSOL and not a license, so the build and solve cells need a local\n", "installation and cannot run on Colab. See the {ref}`extras reference `.\n", "::::\n", "\n", "This tutorial walks the path end to end: build a QPDK double-pad\n", "transmon, copy it without the Josephson junction layers, extract the metal layout, run a stationary\n", "**Electrostatics** solve on a COMSOL sheet model, and read the capacitance and the potential and field the\n", "solve stores. The figures are **saved cell outputs** of licensed solves; without a license the COMSOL cells\n", "are skipped and the result cells say so.\n", "\n", "## What is modelled, and what is solved\n", "\n", "The device is a QPDK {py:func}`~qpdk.cells.transmon.double_pad_transmon_with_bbox`: two pads joined\n", "**only** through the Josephson junction, with a SQUID loop at the centre, so the qubit frequency follows\n", "from $E_\\text{J}$ and $E_\\text{C} = e^2 / 2C$ {cite:p}`kochChargeinsensitiveQubitDesign2007a`. The sheet\n", "model cannot represent the junction overlap or its barrier, so the extraction uses an **EM-only copy** with\n", "the junction layers removed: the SQUID loop and its leads are absent from the solved geometry.\n", "\n", "The solve adds **Electrostatics**, not electromagnetic waves, and returns `es.C11`, the driven pad's\n", "capacitance to the grounded rest of the chip, plus `es.intWe`, which agree through\n", "$2 W_\\text{e} / V^2 = C_{11}$ {cite:p}`m.pozarMicrowaveEngineering2012`. It is a **quasi-static extraction**,\n", "with no Josephson inductance and no resonance solved.\n", "\n", "::::{only} html\n", "```{mermaid}\n", "flowchart LR\n", " A[\"Transmon cell\"] --> B[\"EM-only copy\"] --> C[\"Extracted layout\"] --> D[\"Sheet model\"] --> E[\"Electrostatics\"] --> F[\"Mesh, stationary solve\"] --> G[\"C11, intWe, V, normE\"]\n", "```\n", "::::\n", "\n", "::::{only} typst or typstpdf\n", "The pipeline: transmon cell, EM-only copy with the junction layers removed, extracted layout of pads and\n", "ground plane, sheet model with air over silicon, Electrostatics solve driving the left pad, mesh, and the\n", "capacitance, energy, and field results.\n", "::::\n", "\n", "::::{admonition} Reading the numbers on this page\n", ":class: warning\n", "\n", "The values come from a licensed electrostatic solve, but they are not a validated device prediction: the\n", "SQUID loop and leads are absent, and $C_{11}$ is a **one-terminal** value, the driven pad against the\n", "grounded chip, not the two-pad differential capacitance that sets $E_\\text{C}$\n", "{cite:p}`blaisCircuitQuantumElectrodynamics2021`. Nothing here is a transmon eigenmode or an $f_{01}$.\n", "::::\n", "\n", "**References:**\n", "- QPDK transmon physics and the double-pad cell: {py:func}`~qpdk.cells.transmon.double_pad_transmon_with_bbox`\n", "- [COMSOL Electrostatics interface](https://doc.comsol.com/6.3/doc/com.comsol.help.acdc/acdc_ug_electric_fields.07.002.html)\n", "- [COMSOL capacitance example](https://doc.comsol.com/6.3/doc/com.comsol.help.models.acdc.capacitor_dc/capacitor_dc.html)\n", "- [MPh tutorial](https://mph.readthedocs.io/en/stable/tutorial.html)" ] }, { "cell_type": "markdown", "id": "a875df33", "metadata": {}, "source": [ "## Setup and imports" ] }, { "cell_type": "code", "execution_count": 1, "id": "0a91fb01", "metadata": { "execution": { "iopub.execute_input": "2026-09-30T09:16:17.277221Z", "iopub.status.busy": "2026-09-30T09:16:17.277174Z", "iopub.status.idle": "2026-09-30T09:16:17.279325Z", "shell.execute_reply": "2026-09-30T09:16:17.279091Z" }, "tags": [ "hide-input", "hide-output" ] }, "outputs": [], "source": [ "import sys\n", "\n", "if \"google.colab\" in sys.modules:\n", " import subprocess\n", "\n", " print(\"Running in Google Colab. Installing QPDK...\")\n", " subprocess.check_call([\n", " sys.executable,\n", " \"-m\",\n", " \"pip\",\n", " \"install\",\n", " \"-q\",\n", " \"qpdk[comsol] @ git+https://github.com/gdsfactory/quantum-rf-pdk.git\",\n", " ])\n", " print(\n", " \"Note: this installs the Python client only. COMSOL itself and its \"\n", " \"license are not pip-installable, so the build and solve cells below \"\n", " \"cannot run in Colab, and the result cells will report that no exported \"\n", " \"results are present.\"\n", " )" ] }, { "cell_type": "code", "execution_count": 2, "id": "927d7bda", "metadata": { "execution": { "iopub.execute_input": "2026-09-30T09:16:17.280171Z", "iopub.status.busy": "2026-09-30T09:16:17.280121Z", "iopub.status.idle": "2026-09-30T09:16:19.388117Z", "shell.execute_reply": "2026-09-30T09:16:19.387622Z" }, "tags": [ "hide-input", "hide-output" ] }, "outputs": [], "source": [ "import json\n", "import os\n", "from pathlib import Path\n", "from typing import Any\n", "\n", "import matplotlib.pyplot as plt\n", "import matplotlib.tri as mtri\n", "import numpy as np\n", "from matplotlib import axes as mpl_axes, font_manager\n", "from matplotlib.colors import LogNorm\n", "\n", "from qpdk import PDK\n", "from qpdk.cells.transmon import double_pad_transmon_with_bbox\n", "from qpdk.config import PATH\n", "from qpdk.simulation import prepare_comsol_layout\n", "from qpdk.simulation.comsol.plotting import draw_layout_polygons\n", "from qpdk.simulation.comsol.results import (\n", " explain_missing_results,\n", " result_file,\n", " write_json_atomically,\n", ")\n", "from qpdk.tech import LAYER, LAYER_STACK, get_layer_material_properties\n", "\n", "try:\n", " import mph\n", "\n", " from qpdk.simulation import COMSOL\n", "except ImportError:\n", " mph = None\n", " COMSOL = None\n", "\n", "PDK.activate()\n", "\n", "MPH_AVAILABLE = mph is not None\n", "\n", "# The checkout stylesheet is absent from installed wheels.\n", "for _style in (PATH.docs / \"qpdk.mplstyle\", \"qpdk\"):\n", " try:\n", " plt.style.use(_style)\n", " except OSError:\n", " continue\n", " break\n", "\n", "# Re-running setup must not wrap Axes.set_title again.\n", "if \"Outfit\" in {font.name for font in font_manager.fontManager.ttflist} and not getattr(\n", " mpl_axes.Axes, \"_qpdk_outfit_titles\", False\n", "):\n", " _original_set_title = mpl_axes.Axes.set_title\n", "\n", " def _qpdk_set_title(self: mpl_axes.Axes, *args: Any, **kwargs: Any) -> Any:\n", " kwargs.setdefault(\"fontfamily\", \"Outfit\")\n", " kwargs.setdefault(\"fontweight\", \"bold\")\n", " return _original_set_title(self, *args, **kwargs)\n", "\n", " mpl_axes.Axes.set_title = _qpdk_set_title\n", " mpl_axes.Axes._qpdk_outfit_titles = True\n", "\n", "# Keep glyphs in the SVG for viewers without the documentation fonts.\n", "plt.rcParams[\"svg.fonttype\"] = \"path\"\n", "try:\n", " from matplotlib_inline.backend_inline import set_matplotlib_formats\n", "except ImportError:\n", " pass\n", "else:\n", " set_matplotlib_formats(\"svg\", \"png\")" ] }, { "cell_type": "markdown", "id": "19a81ac3", "metadata": {}, "source": [ "## Build the transmon cell and an EM-only copy\n", "\n", "`prepare_comsol_layout` rejects geometry on layers it does not model, the junction layers among them. The\n", "cell is copied and `JJ_AREA` and `JJ_PATCH` removed, leaving `M1_DRAW` and the `M1_ETCH` mask." ] }, { "cell_type": "code", "execution_count": 3, "id": "1c4948b6", "metadata": { "execution": { "iopub.execute_input": "2026-09-30T09:16:19.389393Z", "iopub.status.busy": "2026-09-30T09:16:19.389262Z", "iopub.status.idle": "2026-09-30T09:16:19.404720Z", "shell.execute_reply": "2026-09-30T09:16:19.404279Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Component: double_pad_transmon_with_bbox_BE200_PS250_400_PG15_JSsq_c3fdad3b\n", "Bounding box (µm): (-457.5,-400;457.5,400)\n", "Junction site: center=(0.0, 0.0) µm\n", "Layers kept: [(1, 0), (1, 1)]\n", "JJ_AREA/JJ_PATCH removed; M1_DRAW and M1_ETCH remain.\n" ] } ], "source": [ "component = double_pad_transmon_with_bbox(bbox_extension=200.0)\n", "\n", "junction_port = component.ports[\"junction\"]\n", "JUNCTION_CENTER_UM = tuple(junction_port.center)\n", "\n", "em_component = component.copy()\n", "em_component.remove_layers(layers=[LAYER.JJ_AREA, LAYER.JJ_PATCH])\n", "\n", "print(f\"Component: {component.name}\")\n", "print(f\"Bounding box (µm): {component.bbox()}\")\n", "print(f\"Junction site: center={JUNCTION_CENTER_UM} µm\")\n", "print(f\"Layers kept: {sorted(em_component.layers)}\")\n", "print(\"JJ_AREA/JJ_PATCH removed; M1_DRAW and M1_ETCH remain.\")" ] }, { "cell_type": "markdown", "id": "9c219b0d", "metadata": {}, "source": [ "## Extract the COMSOL layout\n", "\n", "The pads are not transmission-line ports, so no feed ports are requested. The etched moat is a *hole* in\n", "the ground-plane polygon; the pads sit inside it as separate polygons." ] }, { "cell_type": "code", "execution_count": 4, "id": "a8c21bd8", "metadata": { "execution": { "iopub.execute_input": "2026-09-30T09:16:19.405759Z", "iopub.status.busy": "2026-09-30T09:16:19.405708Z", "iopub.status.idle": "2026-09-30T09:16:19.409289Z", "shell.execute_reply": "2026-09-30T09:16:19.408981Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "Metal polygons: 3\n", "Feed ports: ()\n", "Prepared bbox (µm): ComsolBoundingBox(xmin=-557.5, ymin=-500.0, xmax=557.5, ymax=500.0)\n" ] } ], "source": [ "layout = prepare_comsol_layout(em_component, feed_ports=None, ground_margin=100.0)\n", "\n", "print(f\"Metal polygons: {len(layout.polygons)}\")\n", "print(f\"Feed ports: {layout.feed_ports}\")\n", "print(f\"Prepared bbox (µm): {layout.bbox}\")\n", "\n", "# Three conductor faces: the two pads and the ground plane. Each point below\n", "# sits well inside its metal region so the face selection resolves to exactly\n", "# one face, which the study builder checks before adding the terminals.\n", "LEFT_PAD_POINT = (-132.5, 0.0)\n", "RIGHT_PAD_POINT = (132.5, 0.0)\n", "GROUND_POINT = (-500.0, 0.0)\n", "\n", "# The study names one face selection per conductor after these tags, so the mesh\n", "# sizes and the terminal and ground arguments below can name a face by tag.\n", "PAD_L_SELECTION = \"pad_l\"\n", "PAD_R_SELECTION = \"pad_r\"\n", "GROUND_SELECTION = \"gnd\"\n", "CONDUCTORS = (\n", " (PAD_L_SELECTION, LEFT_PAD_POINT),\n", " (PAD_R_SELECTION, RIGHT_PAD_POINT),\n", " (GROUND_SELECTION, GROUND_POINT),\n", ")" ] }, { "cell_type": "code", "execution_count": 5, "id": "49012594", "metadata": { "execution": { "iopub.execute_input": "2026-09-30T09:16:19.410327Z", "iopub.status.busy": "2026-09-30T09:16:19.410260Z", "iopub.status.idle": "2026-09-30T09:16:19.629026Z", "shell.execute_reply": "2026-09-30T09:16:19.628644Z" } }, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ "findfont: Failed to find font weight bold, now using 400.\n" ] }, { "data": { "image/svg+xml": [ "\n", "\n", "\n", " \n", " \n", " \n", " \n", " 2026-09-30T12:16:19.510074\n", " image/svg+xml\n", " \n", " \n", " Matplotlib v3.11.1, https://matplotlib.org/\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "\n" ], "text/plain": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "fig, ax = plt.subplots(figsize=(6, 6))\n", "draw_layout_polygons(ax, layout.polygons)\n", "for point, label, offset in (\n", " (LEFT_PAD_POINT, \"left pad\", (-35, 10)),\n", " (RIGHT_PAD_POINT, \"right pad\", (8, 10)),\n", " (GROUND_POINT, \"ground\", (8, 8)),\n", " (JUNCTION_CENTER_UM, \"junction site\", (8, -15)),\n", "):\n", " ax.plot(*point, marker=\"x\", color=\"crimson\", markersize=7, zorder=4)\n", " ax.annotate(\n", " label,\n", " point,\n", " textcoords=\"offset points\",\n", " xytext=offset,\n", " fontsize=9,\n", " color=\"crimson\",\n", " )\n", "ax.set_aspect(\"equal\")\n", "ax.set_xlabel(\"x (µm)\")\n", "ax.set_ylabel(\"y (µm)\")\n", "ax.set_title(\"Pads, ground plane, and the conductor points\")\n", "plt.tight_layout()\n", "plt.show()" ] }, { "cell_type": "markdown", "id": "4c678afc", "metadata": {}, "source": [ "## Build the COMSOL model and capacitance study\n", "\n", "Three calls configure the model: {py:meth}`~qpdk.simulation.comsol.model.COMSOL.create_sheet` makes the\n", "air and silicon blocks meeting at $z = 0$, imprints the layout metal on that interface as faces, and\n", "assigns materials;\n", "{py:meth}`~qpdk.simulation.comsol.model.COMSOL.add_capacitance_study` picks the pad and ground faces from\n", "the points above, drives the left pad, grounds the right pad and the ground plane, and adds the mesh and a\n", "stationary study; and {py:meth}`~qpdk.simulation.comsol.model.COMSOL.pin_absolute_mesh_sizes` reruns that\n", "mesh with absolute element sizes in micrometres, so the near-metal resolution does not move with the\n", "domain.\n", "\n", "The silicon thickness and permittivity are read from the `Substrate` level of the PDK layer stack and its\n", "material in `qpdk.tech.material_properties`, so they match every other QPDK simulation. The metal is a\n", "zero-thickness sheet, so the M1 film thickness does not enter. The air height and lateral margin only size\n", "the simulation domain.\n", "\n", "`RUN_COMSOL` is `False` by default, so a documentation build without a license skips all of this and the\n", "result cells below read saved exports instead. Set it to `True` on a licensed machine to solve and export.\n", "The constants are the configuration the saved exports were produced at." ] }, { "cell_type": "code", "execution_count": 6, "id": "2545cd6e", "metadata": { "execution": { "iopub.execute_input": "2026-09-30T09:16:19.629908Z", "iopub.status.busy": "2026-09-30T09:16:19.629854Z", "iopub.status.idle": "2026-09-30T09:16:19.632308Z", "shell.execute_reply": "2026-09-30T09:16:19.631959Z" } }, "outputs": [], "source": [ "RUN_COMSOL = False\n", "MODEL_DIR = Path.home() / \"comsol_models\"\n", "RESULTS_DIR_ENV = \"QPDK_COMSOL_RESULTS_DIR\"\n", "# The environment variable wins when it is set and non-empty, so a run without a\n", "# license can still read a licensed run's exports.\n", "_results_from_environment = os.environ.get(RESULTS_DIR_ENV)\n", "RESULTS_DIR: Path | None = (\n", " Path(_results_from_environment).expanduser()\n", " if _results_from_environment\n", " else MODEL_DIR\n", " if RUN_COMSOL\n", " else None\n", ")\n", "\n", "MODEL_PATH = MODEL_DIR / \"comsol_qubit_capacitance.mph\"\n", "METRICS_JSON = \"comsol_qubit_metrics.json\"\n", "FIELD_TXT = \"comsol_qubit_field.txt\"\n", "CORES = 4\n", "VOLTAGE_V = 1.0\n", "\n", "# The substrate thickness and permittivity come from the PDK layer stack; the air\n", "# height and lateral margin size the simulation domain, in µm.\n", "SUBSTRATE_THICKNESS_UM = LAYER_STACK.layers[\"Substrate\"].thickness\n", "SILICON_RELATIVE_PERMITTIVITY = get_layer_material_properties(\"Substrate\")[\n", " \"relative_permittivity\"\n", "]\n", "AIR_HEIGHT_UM = 1600.0\n", "LATERAL_MARGIN_UM = 8000.0\n", "# Element size COMSOL's physics-controlled build starts from. The pinned sizes\n", "# below replace it, so it only sets the sizing the sequence is materialised with.\n", "BASE_MESH_SIZE = 2\n", "GLOBAL_HMAX_UM = 1000.0\n", "GLOBAL_HMIN_UM = 2.0\n", "HGRAD = 1.4\n", "HCURVE = 0.5\n", "HNARROW = 0.7\n", "# Near-metal element sizes (hmax, hmin) in µm, one per conductor face. Each hmin\n", "# is a tenth of its hmax.\n", "NEAR_METAL = \"0.625/1.25\"\n", "PAD_HMAX_UM, PAD_HMIN_UM = 0.625, 0.0625\n", "GROUND_HMAX_UM, GROUND_HMIN_UM = 1.25, 0.125\n", "\n", "if RUN_COMSOL and not MPH_AVAILABLE:\n", " raise RuntimeError(\"RUN_COMSOL needs MPh and a licensed COMSOL installation\")" ] }, { "cell_type": "markdown", "id": "7e83f80e", "metadata": {}, "source": [ "### Solve, save, and export\n", "\n", "One licensed cell: build, mesh, and solve, then save the model, write `comsol_qubit_metrics.json`, and\n", "export $V$ and `es.normE` on a cut plane at $z = 1\\,\\text{µm}$ into `comsol_qubit_field.txt` through\n", "COMSOL's Java Data export." ] }, { "cell_type": "code", "execution_count": 7, "id": "93d1d16d", "metadata": { "execution": { "iopub.execute_input": "2026-09-30T09:16:19.633182Z", "iopub.status.busy": "2026-09-30T09:16:19.633135Z", "iopub.status.idle": "2026-09-30T09:16:19.636185Z", "shell.execute_reply": "2026-09-30T09:16:19.635922Z" } }, "outputs": [], "source": [ "model = None\n", "\n", "if RUN_COMSOL and MPH_AVAILABLE:\n", " MODEL_DIR.mkdir(parents=True, exist_ok=True)\n", " client = mph.start(cores=CORES)\n", " model = COMSOL.create_sheet(\n", " client,\n", " layout,\n", " name=\"QPDK Double-Pad Transmon\",\n", " substrate_thickness_um=SUBSTRATE_THICKNESS_UM,\n", " air_height_um=AIR_HEIGHT_UM,\n", " lateral_margin_um=LATERAL_MARGIN_UM,\n", " silicon_relative_permittivity=SILICON_RELATIVE_PERMITTIVITY,\n", " ).add_capacitance_study(\n", " conductors=CONDUCTORS,\n", " terminal=PAD_L_SELECTION,\n", " grounds=(PAD_R_SELECTION, GROUND_SELECTION),\n", " voltage_v=VOLTAGE_V,\n", " mesh_size=BASE_MESH_SIZE,\n", " )\n", " element_count = model.pin_absolute_mesh_sizes(\n", " global_hmax_um=GLOBAL_HMAX_UM,\n", " global_hmin_um=GLOBAL_HMIN_UM,\n", " face_sizes={\n", " PAD_L_SELECTION: (PAD_HMAX_UM, PAD_HMIN_UM),\n", " PAD_R_SELECTION: (PAD_HMAX_UM, PAD_HMIN_UM),\n", " GROUND_SELECTION: (GROUND_HMAX_UM, GROUND_HMIN_UM),\n", " },\n", " hgrad=HGRAD,\n", " hcurve=HCURVE,\n", " hnarrow=HNARROW,\n", " )\n", " model.java.study(\"std1\").run()\n", " for problem in model.problems():\n", " print(f\"The solve reports: {problem}\")\n", "\n", " (MODEL_DIR / FIELD_TXT).unlink(missing_ok=True)\n", " model.save(MODEL_PATH)\n", "\n", " capacitance_f = float(model.evaluate(\"es.C11\"))\n", " stored_energy_j = float(model.evaluate(\"es.intWe\"))\n", " print(f\"es.C11 = {capacitance_f:.6e} F\")\n", " print(f\"es.intWe = {stored_energy_j:.6e} J\")\n", " print(f\"2*intWe/V^2 = {2.0 * stored_energy_j / VOLTAGE_V**2:.6e} F\")\n", " print(f\"Mesh elements = {element_count}\")\n", " print(f\"Saved model to {MODEL_PATH}\")\n", "\n", " write_json_atomically(\n", " MODEL_DIR / METRICS_JSON,\n", " {\n", " \"capacitance_f\": capacitance_f,\n", " \"stored_energy_j\": stored_energy_j,\n", " \"voltage_v\": VOLTAGE_V,\n", " \"solver\": \"COMSOL Multiphysics\",\n", " \"study\": \"Electrostatics stationary\",\n", " \"mesh\": \"absolute element sizes, physics-controlled sizing replaced\",\n", " \"element_count\": element_count,\n", " \"near_metal\": NEAR_METAL,\n", " \"lateral_margin_um\": LATERAL_MARGIN_UM,\n", " \"substrate_thickness_um\": SUBSTRATE_THICKNESS_UM,\n", " \"air_height_um\": AIR_HEIGHT_UM,\n", " \"silicon_relative_permittivity\": SILICON_RELATIVE_PERMITTIVITY,\n", " },\n", " )\n", "\n", " result = model.java.result()\n", " # Rerunning this cell must not collide with the nodes a previous run left, so\n", " # an existing tag is reused and every setting is written again before export.\n", " datasets = result.dataset()\n", " plane = (\n", " result.dataset(\"cutplane\")\n", " if datasets.hasTag(\"cutplane\")\n", " else datasets.create(\"cutplane\", \"CutPlane\")\n", " )\n", " plane.set(\"planetype\", \"quick\")\n", " plane.set(\"quickplane\", \"xy\")\n", " plane.set(\"quickz\", \"1[um]\")\n", " plane.set(\"data\", \"dset1\")\n", "\n", " exports = result.export()\n", " field_export = (\n", " result.export(\"field\")\n", " if exports.hasTag(\"field\")\n", " else exports.create(\"field\", \"Data\")\n", " )\n", " field_export.set(\"data\", \"cutplane\")\n", " field_export.set(\"expr\", [\"V\", \"es.normE\"])\n", " field_path = MODEL_DIR / FIELD_TXT\n", " field_export.set(\"filename\", str(field_path))\n", " field_export.run()\n", " print(f\"Exported V and es.normE to {field_path}\")" ] }, { "cell_type": "markdown", "id": "f284f9f6", "metadata": {}, "source": [ "## Saved capacitance result\n", "\n", "This cell reads `comsol_qubit_metrics.json` and checks $C_{11}$ against the stored energy through\n", "$2 W_\\text{e} / V^2$, the two agreeing to the solver's own precision." ] }, { "cell_type": "code", "execution_count": 8, "id": "f736a5d2", "metadata": { "execution": { "iopub.execute_input": "2026-09-30T09:16:19.637002Z", "iopub.status.busy": "2026-09-30T09:16:19.636962Z", "iopub.status.idle": "2026-09-30T09:16:19.639556Z", "shell.execute_reply": "2026-09-30T09:16:19.639270Z" } }, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "C11 = 122.2183 fF at V = 1 V\n", "2 * We / V^2 = 122.2183 fF (relative difference 3.74e-14)\n", "mesh = 30,901,244 elements, 0.625/1.25 near-metal sizes\n", "box = margin 8000 µm, silicon 500 µm, air 1600 µm\n" ] } ], "source": [ "metrics_file = result_file(RESULTS_DIR, METRICS_JSON)\n", "\n", "capacitance_f: float | None = None\n", "stored_energy_j: float | None = None\n", "voltage_v: float | None = None\n", "\n", "if metrics_file is None:\n", " print(explain_missing_results(RESULTS_DIR, METRICS_JSON))\n", "else:\n", " metrics = json.loads(metrics_file.read_text())\n", " capacitance_f = float(metrics[\"capacitance_f\"])\n", " stored_energy_j = float(metrics[\"stored_energy_j\"])\n", " voltage_v = float(metrics[\"voltage_v\"])\n", "\n", " capacitance_from_energy_f = 2.0 * stored_energy_j / voltage_v**2\n", " print(\n", " f\"C11 = {capacitance_f * 1e15:.4f} fF at V = {voltage_v:g} V\"\n", " )\n", " print(\n", " f\"2 * We / V^2 = {capacitance_from_energy_f * 1e15:.4f} fF \"\n", " f\"(relative difference \"\n", " f\"{abs(capacitance_from_energy_f - capacitance_f) / capacitance_f:.2e})\"\n", " )\n", " print(\n", " f\"mesh = {int(metrics['element_count']):,} elements, \"\n", " f\"{metrics['near_metal']} near-metal sizes\"\n", " )\n", " print(\n", " f\"box = margin {float(metrics['lateral_margin_um']):g} µm, \"\n", " f\"silicon {float(metrics['substrate_thickness_um']):g} µm, \"\n", " f\"air {float(metrics['air_height_um']):g} µm\"\n", " )" ] }, { "cell_type": "markdown", "id": "673aa6a5", "metadata": {}, "source": [ "## Saved potential and field map\n", "\n", "The exported field is $V$ and the field norm `es.normE` on the $z = 1\\,\\text{µm}$ plane just above the\n", "metal sheet. The export spans the whole box, where the pads are a dot, so the map is a **close-up**:\n", "`FIELD_LIMIT_X_UM` and `FIELD_LIMIT_Y_UM` frame both pads, and the remaining nodes are resampled onto a\n", "display grid (**display only**: it reads the solved field without re-solving it)." ] }, { "cell_type": "code", "execution_count": 9, "id": "696a424c", "metadata": { "execution": { "iopub.execute_input": "2026-09-30T09:16:19.640436Z", "iopub.status.busy": "2026-09-30T09:16:19.640389Z", "iopub.status.idle": "2026-09-30T09:16:49.639181Z", "shell.execute_reply": "2026-09-30T09:16:49.638846Z" } }, "outputs": [ { "data": { "image/svg+xml": [ "\n", "\n", "\n", " \n", " \n", " \n", " \n", " 2026-09-30T12:16:49.529411\n", " image/svg+xml\n", " \n", " \n", " Matplotlib v3.11.1, https://matplotlib.org/\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "\n" ], "text/plain": [ "
" ] }, "metadata": {}, "output_type": "display_data" } ], "source": [ "FIELD_LIMIT_X_UM = 300.0\n", "FIELD_LIMIT_Y_UM = 250.0\n", "FIELD_GRID_X = 300\n", "FIELD_GRID_Y = 250\n", "FIELD_LEVELS = 20\n", "\n", "field_file = result_file(RESULTS_DIR, FIELD_TXT)\n", "\n", "if field_file is None:\n", " print(explain_missing_results(RESULTS_DIR, FIELD_TXT))\n", "else:\n", " field = np.loadtxt(field_file, comments=\"%\")\n", " field_x, field_y = field[:, 0], field[:, 1]\n", " in_view = (np.abs(field_x) <= FIELD_LIMIT_X_UM) & (\n", " np.abs(field_y) <= FIELD_LIMIT_Y_UM\n", " )\n", " if in_view.sum() < 3:\n", " raise ValueError(\n", " f\"No field nodes lie within ±{FIELD_LIMIT_X_UM:g} by \"\n", " f\"±{FIELD_LIMIT_Y_UM:g} µm, so there is nothing to draw a close-up from\"\n", " )\n", " triangulation = mtri.Triangulation(field_x[in_view], field_y[in_view])\n", " grid_x = np.linspace(-FIELD_LIMIT_X_UM, FIELD_LIMIT_X_UM, FIELD_GRID_X)\n", " grid_y = np.linspace(-FIELD_LIMIT_Y_UM, FIELD_LIMIT_Y_UM, FIELD_GRID_Y)\n", " grid_xx, grid_yy = np.meshgrid(grid_x, grid_y)\n", " field_v = mtri.LinearTriInterpolator(triangulation, field[in_view, 3])(\n", " grid_xx, grid_yy\n", " ).filled(np.nan)\n", " field_e = mtri.LinearTriInterpolator(triangulation, field[in_view, 4])(\n", " grid_xx, grid_yy\n", " ).filled(np.nan)\n", " field_e_max = float(np.nanmax(field_e))\n", "\n", " voltage_label = f\"at V = {voltage_v:g} V, \" if voltage_v is not None else \"\"\n", "\n", " fig, axes = plt.subplots(1, 2, figsize=(12, 4))\n", " potential = axes[0].contourf(\n", " grid_x, grid_y, field_v, levels=FIELD_LEVELS, cmap=\"viridis\"\n", " )\n", " axes[0].set_title(f\"Electric potential {voltage_label}z = 1 µm\")\n", " fig.colorbar(potential, ax=axes[0], label=r\"$V$ (V)\")\n", "\n", " norm_e = axes[1].contourf(\n", " grid_x,\n", " grid_y,\n", " field_e,\n", " levels=np.geomspace(1.0, field_e_max, FIELD_LEVELS),\n", " norm=LogNorm(vmin=1.0, vmax=field_e_max),\n", " cmap=\"inferno\",\n", " extend=\"min\",\n", " )\n", " axes[1].set_title(f\"Electric field norm {voltage_label}z = 1 µm\")\n", " fig.colorbar(norm_e, ax=axes[1], label=r\"$|\\mathbf{E}|$ (V/m)\")\n", "\n", " for axis in axes:\n", " axis.set_aspect(\"equal\")\n", " axis.set_xlabel(\"x (µm)\")\n", " axis.set_ylabel(\"y (µm)\")\n", " plt.tight_layout()\n", " plt.show()" ] }, { "cell_type": "markdown", "id": "64871d61", "metadata": {}, "source": [ "The potential holds across the driven pad and falls through the gap to the grounded pad; the field\n", "concentrates in that gap and at the pad edges, where the pad capacitance mainly lives." ] }, { "cell_type": "markdown", "id": "552e0157", "metadata": {}, "source": [ "## Limitations and next steps\n", "\n", "The solve is electrostatic and the EM-only copy omits the SQUID loop and its leads, so $C_{11}$ is a\n", "**one-terminal** capacitance to the grounded chip rather than the two-pad charging capacitance, and it is\n", "not a transmon eigenfrequency or $f_{01}$. The finite zero-charge outer walls of the box act at any mesh,\n", "so the value is not a converged or validated device number. The natural next step is the full two-pad\n", "capacitance matrix feeding the QPDK Hamiltonian workflow\n", "({doc}`/notebooks/scqubits_parameter_calculation`).\n", "\n", "## References\n", "\n", "```{bibliography}\n", ":filter: docname in docnames\n", "```" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "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.14.7" } }, "nbformat": 4, "nbformat_minor": 5 }