Skip to content

Add Palace eigenmode simulation notebook for transmon qubit with readout resonator#382

Draft
Copilot wants to merge 2 commits into
mainfrom
copilot/add-simulation-double-pads-qubit
Draft

Add Palace eigenmode simulation notebook for transmon qubit with readout resonator#382
Copilot wants to merge 2 commits into
mainfrom
copilot/add-simulation-double-pads-qubit

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 26, 2026

Adds a gsim/Palace eigenmode simulation example for a double-pad transmon coupled to a quarter-wave resonator read through a probeline, with analytical model comparison and an Optuna optimization loop.

New notebook: palace_eigenmode_qubit_resonator

  • Layout — Uses transmon_with_resonator_and_probeline with apply_additive_metals → etch-to-conductor conversion for Palace's 3D FEM mesh
  • Analytical baseline — Computes resonator_frequency() from qpdk/models/ as reference (f_r = v_p / 4L)
  • Eigenmode sim — Configures gsim.palace.EigenmodeSim with junction lumped port (10 nH), two CPW probeline ports, targets 5 modes near the analytical estimate
  • Comparison — Identifies closest eigenmode to analytical prediction, reports relative error, plots all modes vs. analytical estimate
  • Optuna optimization — Sweeps resonator_length ∈ [3000, 8000] µm to minimize (f_Palace − 6 GHz)² across Palace eigenmode solves

Docs

  • docs/notebooks.rst updated: new entry in FEM section and summary table

Key snippet

from gsim.palace import EigenmodeSim

sim = EigenmodeSim()
sim.set_geometry(etched)
sim.set_stack(substrate_thickness=500, air_above=500)
sim.add_port("junction", layer="SUPERCONDUCTOR", length=5.0, inductance=10e-9)
sim.add_cpw_port("coupling_o1", layer="SUPERCONDUCTOR", s_width=10.0, gap_width=6.0, length=5.0)
sim.add_cpw_port("coupling_o2", layer="SUPERCONDUCTOR", s_width=10.0, gap_width=6.0, length=5.0)
sim.set_eigenmode(target=analytical_freq, num_modes=5)
sim.mesh(preset="coarse")
results = sim.run()

The simulation block and optimization loop are gated behind if __name__ == "__main__" since they require a local Palace installation.

Summary by Sourcery

Add a Palace-based eigenmode simulation example for a transmon qubit coupled to a quarter-wave readout resonator, including analytical comparison and optimization of resonator length.

New Features:

  • Introduce a Jupyter notebook and corresponding script demonstrating a full Palace eigenmode workflow for a double-pad transmon with quarter-wave resonator and probeline readout.
  • Add an Optuna-based optimization loop that tunes the resonator length to reach a target readout frequency.

Enhancements:

  • Provide a reusable simulation component that wraps the transmon, resonator, probeline, and simulation area for eigenmode analysis.
  • Show conversion from qpdk subtractive etch layers to explicit conductor geometry suitable for gsim/Palace FEM meshing.

Documentation:

  • Document the new Palace eigenmode qubit–resonator notebook in the FEM notebooks section and summary table.

Copilot AI and others added 2 commits March 26, 2026 09:17
@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai Bot commented Mar 26, 2026

Reviewer's Guide

Adds a new Palace/gsim eigenmode simulation example (notebook + jupytext Python) for a double-pad transmon with quarter-wave resonator and probeline, including layout construction, etch-to-conductor conversion, analytical resonator estimate, Palace eigenmode setup/run, result comparison, and an Optuna optimization loop, plus docs entry wiring the notebook into the FEM examples list.

Sequence diagram for Optuna-driven Palace eigenmode optimization loop

sequenceDiagram
  actor User
  participant JupyterNotebook
  participant OptunaStudy
  participant EigenmodeObjective
  participant GDSFactory
  participant QPDK
  participant KLayout
  participant EigenmodeSim
  participant PalaceSolver
  participant Logger

  User->>JupyterNotebook: execute __main__ block
  JupyterNotebook->>OptunaStudy: create_study
  JupyterNotebook->>OptunaStudy: optimize(eigenmode_objective, n_trials)

  loop for each trial
    OptunaStudy->>EigenmodeObjective: call(trial)

    EigenmodeObjective->>OptunaStudy: suggest_float resonator_length

    EigenmodeObjective->>GDSFactory: qubit_resonator_sim_component(resonator_length)
    GDSFactory-->>EigenmodeObjective: Component

    EigenmodeObjective->>QPDK: apply_additive_metals(Component)
    QPDK-->>EigenmodeObjective: processed Component

    EigenmodeObjective->>KLayout: build Regions SIM_AREA, M1_ETCH
    KLayout-->>EigenmodeObjective: sim_region, etch_region

    EigenmodeObjective->>KLayout: decimate and compute conductor_region

    EigenmodeObjective->>GDSFactory: build etched Component with CPW_LAYERS
    GDSFactory-->>EigenmodeObjective: etched Component

    EigenmodeObjective->>EigenmodeSim: constructor
    EigenmodeObjective->>EigenmodeSim: set_geometry(etched)
    EigenmodeObjective->>EigenmodeSim: set_stack(substrate_thickness, air_above)
    EigenmodeObjective->>EigenmodeSim: add_port(junction, inductance)
    EigenmodeObjective->>EigenmodeSim: add_cpw_port(coupling_o1)
    EigenmodeObjective->>EigenmodeSim: add_cpw_port(coupling_o2)

    EigenmodeObjective->>QPDK: cpw_parameters(width, gap)
    QPDK-->>EigenmodeObjective: epsilon_eff
    EigenmodeObjective->>QPDK: resonator_frequency(resonator_length, epsilon_eff)
    QPDK-->>EigenmodeObjective: target_analytical

    EigenmodeObjective->>EigenmodeSim: set_eigenmode(target_analytical, num_modes)
    EigenmodeObjective->>EigenmodeSim: set_output_dir(trial-specific)
    EigenmodeObjective->>EigenmodeSim: mesh(preset="coarse")
    EigenmodeObjective->>PalaceSolver: run eigenmode solve
    PalaceSolver-->>EigenmodeObjective: opt_res (eigenvalues or error)

    alt opt_res.ok
      EigenmodeObjective->>EigenmodeObjective: select best_mode near target_analytical
      EigenmodeObjective->>OptunaStudy: set_user_attr(palace_freq_ghz, analytical_freq_ghz, resonator_length_um)
      EigenmodeObjective-->>OptunaStudy: return (best_mode - target_ghz)^2
    else failure
      EigenmodeObjective->>Logger: warn("Trial failed or simulation error")
      EigenmodeObjective-->>OptunaStudy: return large penalty
    end
  end

  OptunaStudy-->>JupyterNotebook: best_trial, trials
  JupyterNotebook->>Logger: log best params and frequencies
Loading

Flow diagram for Palace eigenmode simulation workflow in the new notebook

graph TD
  A_start["Start notebook<br/>Palace eigenmode qubit resonator"] --> B_layout["Create layout<br/>qubit_resonator_sim_component"]
  B_layout --> C_apply_additive["Apply additive metals<br/>apply_additive_metals"]
  C_apply_additive --> D_regions["Build KLayout regions<br/>SIM_AREA and M1_ETCH"]
  D_regions --> E_boolean["Boolean operation<br/>conductor_region = SIM_AREA − M1_ETCH"]
  E_boolean --> F_etched["Build etched component<br/>SUPERCONDUCTOR/SUBSTRATE/VACUUM layers"]
  F_etched --> G_cpw_params["Compute CPW parameters<br/>cpw_parameters, get_cpw_dimensions"]
  G_cpw_params --> H_analytical["Analytical resonator frequency<br/>resonator_frequency"]
  H_analytical --> I_config_sim["Configure EigenmodeSim<br/>geometry, stack, ports, eigenmode target"]
  I_config_sim --> J_mesh["Generate mesh<br/>sim.mesh"]
  J_mesh --> K_run["Run Palace eigenmode solve<br/>sim.run"]
  K_run --> L_check["Check results.ok"]
  L_check -->|ok| M_log_modes["Log eigenfrequencies and Q"]
  L_check -->|error| N_log_error["Log simulation error_msg"]
  M_log_modes --> O_compare["Compare closest eigenmode<br/>to analytical prediction"]
  O_compare --> P_plot["Plot eigenmode frequencies<br/>vs analytical estimate"]
  N_log_error --> Q_end["End (no comparison)"]
  P_plot --> R_ready["Notebook ready for<br/>interactive exploration"]
Loading

File-Level Changes

Change Details Files
Introduce a full Palace eigenmode simulation workflow for a transmon–resonator–probeline system, including layout creation, layer processing, analytical baseline, Palace configuration, and result visualization.
  • Define qubit_resonator_sim_component gf.cell to instantiate transmon_with_resonator_and_probeline, expose ports, and draw a SIM_AREA region around the device.
  • Convert QPDK subtractive etch layers to explicit conductor regions using apply_additive_metals, kdb.Region boolean operations, and decimate, then rebuild an etched component with CPW-style SUBSTRATE/SUPERCONDUCTOR/VACUUM layers and propagated ports.
  • Compute a CPW-based analytical resonator frequency using get_cpw_dimensions, cpw_parameters, and resonator_frequency, logging effective permittivity, impedance, and expected quarter-wave frequency.
  • Configure EigenmodeSim with the etched geometry, stack parameters, a lumped inductive junction port, two CPW feed ports, and eigenmode targeting around the analytical estimate; mesh (coarse) and run the Palace simulation; log eigenvalues and generate a bar-plot comparison against the analytical frequency.
notebooks/palace_eigenmode_qubit_resonator.ipynb
notebooks/src/palace_eigenmode_qubit_resonator.py
Add an Optuna-based optimization loop that sweeps resonator length to hit a target eigenmode frequency, gated behind a main-guard so it only runs in an appropriate environment.
  • Implement eigenmode_objective that, for each trial, rebuilds the layout and etched geometry at a suggested resonator_length, runs a shortened eigenmode simulation guided by an updated analytical estimate, and returns squared error to a 6 GHz target while recording trial metadata.
  • Add a __main__ block that creates an Optuna study, runs a small number of trials serially, and logs the best resonator length and corresponding Palace frequency, only executing when the script is run directly.
  • Document the optimization block in the notebook with a rendered code snippet and explanation that it requires a local Palace installation.
notebooks/palace_eigenmode_qubit_resonator.ipynb
notebooks/src/palace_eigenmode_qubit_resonator.py
Wire the new notebook into the documentation notebooks index under the FEM simulations section.
  • Add an entry for the Palace eigenmode qubit–resonator notebook to the FEM section and summary table so it appears in the rendered docs.
docs/notebooks.rst

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

component = qubit_resonator_sim_component()
_c = component.copy()
_c.draw_ports()
_c # noqa: B018
for port in processed.ports:
etched.add_port(name=port.name, port=port)

etched # noqa: B018
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new Palace/gsim eigenmode simulation example notebook for a double-pad transmon coupled to a quarter-wave readout resonator, including an analytical frequency comparison and an Optuna-based resonator-length tuning loop, and documents it in the notebooks index.

Changes:

  • Added a new Palace eigenmode simulation notebook (both .ipynb and jupytext .py source) for a transmon–resonator–probeline structure.
  • Included an analytical resonator-frequency baseline and post-processing/plotting to compare FEM modes vs. the analytical estimate.
  • Updated documentation (docs/notebooks.rst) to include the new notebook in the FEM section and summary table.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 7 comments.

File Description
notebooks/src/palace_eigenmode_qubit_resonator.py New jupytext Python notebook source implementing the full workflow (layout → etch conversion → analytical estimate → Palace eigenmode + Optuna).
notebooks/palace_eigenmode_qubit_resonator.ipynb Rendered notebook version of the same workflow intended for docs/notebook consumption.
docs/notebooks.rst Adds the new notebook to the FEM notebooks list and the summary table.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread docs/notebooks.rst
Comment on lines +121 to +124
- :doc:`notebooks/palace_eigenmode_qubit_resonator` — Eigenmode simulation of a
double-pad transmon qubit coupled to a quarter-wave readout resonator using `gsim
<https://gdsfactory.github.io/gsim/>`_ and Palace, including comparison with
semi-analytical frequency estimates and an Optuna optimization loop.
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding this notebook to the docs list will cause it to be executed during the Sphinx build (myst-nb is configured with execution enabled). As written, the notebook requires gsim (not currently in the repo lockfile) and a local Palace installation, so documentation builds will fail unless the notebook is excluded from execution (e.g., via nb_execution_excludepatterns) or solver-dependent cells are made non-executing in docs.

Suggested change
- :doc:`notebooks/palace_eigenmode_qubit_resonator` — Eigenmode simulation of a
double-pad transmon qubit coupled to a quarter-wave readout resonator using `gsim
<https://gdsfactory.github.io/gsim/>`_ and Palace, including comparison with
semi-analytical frequency estimates and an Optuna optimization loop.
- ``notebooks/palace_eigenmode_qubit_resonator.ipynb`` — Eigenmode simulation of a
double-pad transmon qubit coupled to a quarter-wave readout resonator using `gsim
<https://gdsfactory.github.io/gsim/>`_ and Palace, including comparison with
semi-analytical frequency estimates and an Optuna optimization loop. This notebook
requires external solver installations and is not executed as part of the documentation
build.

Copilot uses AI. Check for mistakes.
Comment on lines +243 to +256
sim.set_output_dir("./sim_palace_qubit_resonator")
sim.mesh(preset="coarse")
sim.plot_mesh()

# %%
results = sim.run()

if results.ok:
logger.info("Eigenvalues:")
logger.info(f"{'Mode':<6} {'Freq (GHz)':<16} {'Q':<16}")
for i, ev in enumerate(results.eigenvalues, start=1):
logger.info(f"{i:<6} {ev.freq:<16.4f} {ev.quality_factor:<16.4f}")
else:
logger.error(f"Simulation failed: {results.error_msg}")
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sim.mesh() / sim.run() are executed unconditionally in this notebook script. If this .py is ever run by automation (or imported), it will require a local Palace install and can be very expensive. Consider guarding the Palace execution behind if __name__ == "__main__" or converting it to a fenced example block so imports/docs can run without launching the solver.

Suggested change
sim.set_output_dir("./sim_palace_qubit_resonator")
sim.mesh(preset="coarse")
sim.plot_mesh()
# %%
results = sim.run()
if results.ok:
logger.info("Eigenvalues:")
logger.info(f"{'Mode':<6} {'Freq (GHz)':<16} {'Q':<16}")
for i, ev in enumerate(results.eigenvalues, start=1):
logger.info(f"{i:<6} {ev.freq:<16.4f} {ev.quality_factor:<16.4f}")
else:
logger.error(f"Simulation failed: {results.error_msg}")
if __name__ == "__main__":
sim.set_output_dir("./sim_palace_qubit_resonator")
sim.mesh(preset="coarse")
sim.plot_mesh()
# %%
results = sim.run()
if results.ok:
logger.info("Eigenvalues:")
logger.info(f"{'Mode':<6} {'Freq (GHz)':<16} {'Q':<16}")
for i, ev in enumerate(results.eigenvalues, start=1):
logger.info(f"{i:<6} {ev.freq:<16.4f} {ev.quality_factor:<16.4f}")
else:
logger.error(f"Simulation failed: {results.error_msg}")

Copilot uses AI. Check for mistakes.
"metadata": {},
"outputs": [],
"source": [
"from gsim.common.polygon_utils import decimate\n",
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This notebook imports gsim, but gsim is not present in this repo’s uv.lock. Because myst-nb executes notebooks during the docs build and this notebook is not excluded, the docs build will fail unless gsim is added to dependencies (and the lockfile updated) or these imports are moved into non-executed/fenced example blocks.

Suggested change
"from gsim.common.polygon_utils import decimate\n",
"try:\n",
" from gsim.common.polygon_utils import decimate\n",
"except Exception:\n",
" # Fallback: no-op decimation when gsim is unavailable (e.g. in docs builds)\n",
" def decimate(polygons):\n",
" return list(polygons)\n",

Copilot uses AI. Check for mistakes.
Comment on lines +311 to +313
"sim.set_output_dir(\"./sim_palace_qubit_resonator\")\n",
"sim.mesh(preset=\"coarse\")\n",
"sim.plot_mesh()"
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

sim.mesh(), sim.plot_mesh(), and sim.run() are executed unconditionally when this notebook runs. The docs build executes notebooks via myst-nb and Palace won’t be available in CI, so this will break documentation builds (and is also very expensive). Consider converting the entire “Mesh and run” + downstream result-processing cells into a fenced code block (as done in notebooks/optimize_capacitor_optuna.ipynb) or tagging/skipping these cells during docs execution.

Copilot uses AI. Check for mistakes.
Comment on lines +305 to +334
"cell_type": "code",
"execution_count": null,
"id": "11",
"metadata": {},
"outputs": [],
"source": [
"sim.set_output_dir(\"./sim_palace_qubit_resonator\")\n",
"sim.mesh(preset=\"coarse\")\n",
"sim.plot_mesh()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "12",
"metadata": {},
"outputs": [],
"source": [
"results = sim.run()\n",
"\n",
"if results.ok:\n",
" logger.info(\"Eigenvalues:\")\n",
" logger.info(f\"{'Mode':<6} {'Freq (GHz)':<16} {'Q':<16}\")\n",
" for i, ev in enumerate(results.eigenvalues, start=1):\n",
" logger.info(f\"{i:<6} {ev.freq:<16.4f} {ev.quality_factor:<16.4f}\")\n",
"else:\n",
" logger.error(f\"Simulation failed: {results.error_msg}\")"
]
},
{
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This call will execute Palace during notebook execution, which will fail in CI/doc builds where Palace isn’t installed (and nb_execution_allow_errors = False). Please gate Palace execution (and any downstream results usage) behind a non-executed block for docs (e.g., fenced code in markdown, or excluding this notebook from execution).

Suggested change
"cell_type": "code",
"execution_count": null,
"id": "11",
"metadata": {},
"outputs": [],
"source": [
"sim.set_output_dir(\"./sim_palace_qubit_resonator\")\n",
"sim.mesh(preset=\"coarse\")\n",
"sim.plot_mesh()"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "12",
"metadata": {},
"outputs": [],
"source": [
"results = sim.run()\n",
"\n",
"if results.ok:\n",
" logger.info(\"Eigenvalues:\")\n",
" logger.info(f\"{'Mode':<6} {'Freq (GHz)':<16} {'Q':<16}\")\n",
" for i, ev in enumerate(results.eigenvalues, start=1):\n",
" logger.info(f\"{i:<6} {ev.freq:<16.4f} {ev.quality_factor:<16.4f}\")\n",
"else:\n",
" logger.error(f\"Simulation failed: {results.error_msg}\")"
]
},
{
"cell_type": "markdown",
"execution_count": null,
"id": "11",
"metadata": {},
"outputs": [],
"source": [
"```python\n",
"sim.set_output_dir(\"./sim_palace_qubit_resonator\")\n",
"sim.mesh(preset=\"coarse\")\n",
"sim.plot_mesh()\n",
"```"
]
},
{
"cell_type": "markdown",
"execution_count": null,
"id": "12",
"metadata": {},
"outputs": [],
"source": [
"```python\n",
"results = sim.run()\n",
"\n",
"if results.ok:\n",
" logger.info(\"Eigenvalues:\")\n",
" logger.info(f\"{'Mode':<6} {'Freq (GHz)':<16} {'Q':<16}\")\n",
" for i, ev in enumerate(results.eigenvalues, start=1):\n",
" logger.info(f\"{i:<6} {ev.freq:<16.4f} {ev.quality_factor:<16.4f}\")\n",
"else:\n",
" logger.error(f\"Simulation failed: {results.error_msg}\")\n",
"```"
]
},
{

Copilot uses AI. Check for mistakes.
Comment on lines +549 to +550
"\n",
"ruff: enable[E402]"
Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks like a leftover Ruff directive, but it’s currently plain text in a markdown cell (and missing the leading #). It will render into the docs and doesn’t affect linting. Please remove it, or if you intended a Ruff directive, keep it as a comment in an executable code cell.

Suggested change
"\n",
"ruff: enable[E402]"
"\n"

Copilot uses AI. Check for mistakes.
Comment on lines +130 to +131
from gsim.common.polygon_utils import decimate

Copy link

Copilot AI Mar 29, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

gsim is not present in this repo’s uv.lock, so importing it at top-level will fail in environments that only install this repo’s locked dependencies (including the docs build unless gsim is added). Consider moving gsim imports into a fenced/non-executed example block or adding gsim to a documented extra and updating the lockfile.

Suggested change
from gsim.common.polygon_utils import decimate
try:
from gsim.common.polygon_utils import decimate
except ImportError: # pragma: no cover
def decimate(polygons, *args, **kwargs):
"""Fallback no-op decimation when `gsim` is unavailable.
Returns the input polygons unchanged.
"""
return polygons

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants