Simulation#

Circuit-level model hierarchy#

SAX simulations expand unmodeled assemblies until they reach cells with declared models. Model declarations therefore define hierarchy boundaries instead of relying on a fixed depth.

Coupled resonators declare their own models so the coupling is preserved. Chip assemblies remain unmodeled and expand to these boundaries and other modeled primitives.

For new components, attach the compact model through schematic_function and keep its port_order consistent with the layout ports.

AEDT and COMSOL simulation utilities.

This package provides class-based interfaces for setting up HFSS simulations (eigenmode and driven modal) and Q3D Extractor parasitic extractions from gdsfactory components, plus MPh-based COMSOL geometry and study builders.

The AEDT wrappers use the PyAEDT library to interface with Ansys HFSS and Q3D Extractor.

HFSS workflow:

  1. Prepare a component with prepare_component_for_aedt()

  2. Export to GDS and import into HFSS with qpdk.simulation.hfss.HFSS.import_component()

  3. Configure simulation setup (e.g. Eigenmode or Driven) manually via PyAEDT

  4. Extract results with qpdk.simulation.hfss.HFSS.get_eigenmode_results() or qpdk.simulation.hfss.HFSS.get_sparameter_results()

Q3D Extractor workflow:

  1. Prepare a component with prepare_component_for_aedt()

  2. Export to GDS and import into Q3D with qpdk.simulation.q3d.Q3D.import_component()

  3. Assign signal nets with qpdk.simulation.q3d.Q3D.assign_nets_from_ports()

  4. Configure Q3D setup and analyze

  5. Extract capacitance matrix with qpdk.simulation.q3d.Q3D.get_capacitance_matrix()

COMSOL workflow:

  1. Extract metal polygons and optional feed ports with prepare_comsol_layout()

  2. Build a 3D air/silicon model with sheet metal via build_comsol_sheet_model()

  3. Add a CPW RF or qubit electrostatic study with add_cpw_rf_study() or add_capacitance_study()

  4. Mesh it, optionally refined at the metal plane, with refine_metal_plane_mesh(), or with absolute sizes at the metal via pin_absolute_mesh_sizes()

The steps above are also available as one chainable class: COMSOL builds a model through create_sheet() or create_metal(), holds the layout, and offers a method per study and mesh helper, so neither the model nor the layout has to be passed again. Solving, saving, and evaluating are MPh’s own methods.

Note

The AEDT wrappers require uv sync --extra hfss. The COMSOL builders require uv sync --extra comsol and a local COMSOL installation. Only COMSOL imports MPh, and it is exposed lazily, so the layout and helper modules and this package stay importable without it.

Example

>>> from ansys.aedt.core import Hfss
>>> from qpdk.simulation import HFSS, prepare_component_for_aedt
>>> from qpdk.cells import resonator
>>> comp = resonator(length=4000, meanders=4)
>>> prepared_comp = prepare_component_for_aedt(comp)
>>> hfss_app = Hfss(project="resonator_sim", solution_type="Eigenmode")
>>> hfss_sim = HFSS(hfss_app)
>>> hfss_sim.import_component(prepared_comp)

References

Common#

Neutral geometry preparation shared by the AEDT and COMSOL exporters.

This module is pure gdsfactory geometry: it turns a QPDK mask into the physical metal a field solver actually sees. It has no PyAEDT, MPh, or COMSOL imports so both exporters can depend on it.

qpdk.simulation.layout.prepare_metal_layout(component, margin_draw=0.0, margin_etch=0.0, *, name=None)[source]#

Fold a QPDK mask into positive metal geometry for a field solver.

Applies the additive metals to the etch layer, inverts the mask polarity around the component bounding box, grows the ground by margin_draw, drops the etch layers, and re-adds the original ports.

Parameters:
  • component (Component) – The component to prepare.

  • margin_draw (float) – Margin in µm added to the M1/M2 draw layers.

  • margin_etch (float) – Margin in µm added to the M1/M2 etch layers before inversion.

  • name (str | None) – Name for the initial preparation cell. Defaults to f"{component.name}_metal".

Returns:

A copy of the component prepared for simulation.

Return type:

Component

Base AEDT simulation utilities using PyAEDT.

This module provides shared helper functions and a base class for AEDT simulations (HFSS, Q3D, Q2D) from gdsfactory components.

class qpdk.simulation.aedt_base.AEDTBase(*args, **kwargs)[source]#

Bases: object

Base class for AEDT simulations.

Parameters:
Return type:

Any

add_materials()[source]#

Add QPDK materials to the AEDT project.

Return type:

None

add_substrate(component, thickness=500.0, material='silicon', name='Substrate')[source]#

Add a substrate box below the component geometry.

Returns:

Name of the created substrate object.

Parameters:
  • component (Component)

  • thickness (float)

  • material (str)

  • name (str)

Return type:

str

property modeler#

The AEDT modeler instance.

save()[source]#

Save the AEDT project.

Return type:

None

qpdk.simulation.aedt_base.add_materials_to_aedt(app)[source]#

Add QPDK materials to the PyAEDT application.

Parameters:

app (Hfss | Q2d | Q3d)

Return type:

None

qpdk.simulation.aedt_base.detach_desktop_logging(app)[source]#

Stop PyAEDT reading the AEDT desktop on every log message.

Logger._log_on_desktop tests the desktop before it tests settings.enable_desktop_logs, so the setting PyAEDT turns off itself in non-graphical mode does not prevent the read. The read goes through Desktop.odesktop, whose failure path releases the gRPC plugin’s AEDT handle, so a stray log message can end a session that AEDT is still running. Dropping the logger’s reference to the desktop is what Desktop.release_desktop does at the end of a session anyway.

PyAEDT has one logger per process and every desktop construction points it back at that desktop, so this is process-global and needs calling after each Hfss, Q3d or Q2d is built. Messages the logger reads out of the desktop, such as get_messages(aedt_messages=True), stop working afterwards.

Parameters:

app (Any) – The AEDT application (an Hfss, Q3d or Q2d instance).

Return type:

None

qpdk.simulation.aedt_base.export_component_to_gds_temp(component, gds_path=None, prefix='qpdk_aedt_')[source]#

Context manager for exporting a component to a temporary GDS file.

The GDS is written without the layout’s context info, where gdsfactory keeps its ports, cross-sections and settings. AEDT needs the geometry only, and on AEDT 2026 R1 the importer fails on a file that carries the info: the import returns success, the AEDT process is gone moments later, and every later call in the session reports a dead desktop. A consumer that reads the file back with gdsfactory rather than handing it to AEDT gets no ports from it.

Yields:

Path to the exported GDS file.

Parameters:
Return type:

Generator[Path, None, None]

qpdk.simulation.aedt_base.fit_view(app)[source]#

Frame the model in a graphical AEDT session, and do nothing when headless.

modeler.fit_all is a view operation. A non-graphical session has no view, and on AEDT 2026 R1 the failed call loses the gRPC channel: the geometry is fine, but every later call in the session reports a dead desktop.

The guard reads the flag the session was constructed with, not what the session itself reports, so attaching to an existing headless session still needs non_graphical set or PYAEDT_NON_GRAPHICAL exported. A released session has no desktop left and raises.

Parameters:

app (Any) – The AEDT application (an Hfss, Q3d or Q2d instance).

Return type:

None

qpdk.simulation.aedt_base.layer_stack_to_gds_mapping(layer_stack=None, thickness_override=None)[source]#

Convert a LayerStack to HFSS/Q3D GDS import mapping dictionary.

Vacuum levels (material == "vacuum") are skipped: the AEDT background region is already vacuum and any vacuum box is added explicitly with qpdk.simulation.hfss.HFSS.add_air_region(), so importing vacuum as GDS geometry is redundant. This also resolves the Substrate/Vacuum collision on LAYER.SIM_AREA (98, 0).

pyaedt’s import_gds_3d keys the mapping on GDS layer number only — the GDS datatype is not part of the key — so two levels sharing a layer number (e.g. LAYER.AB_DRAW = (10, 0) and LAYER.AB_VIA = (10, 1)) are merged into a single entry when they share a material and their z-spans join into one box; otherwise an error is raised rather than silently importing wrong geometry. Levels sharing a layer number are processed in ascending zmin order so the result does not depend on the layer stack’s insertion order.

Returns:

Dictionary mapping layer number to (elevation, thickness) tuple.

Raises:

ValueError – If two levels share a GDS layer number but cannot be merged into a single (elevation, thickness) entry because their materials differ (regardless of thickness_override) or, with thickness_override=None, their z-spans do not join into a single box.

Parameters:
  • layer_stack (LayerStack | None)

  • thickness_override (float | None)

Return type:

dict[int, tuple[float, float]]

qpdk.simulation.aedt_base.object_names_to_materials(object_names, layer_stack)[source]#

Map imported object names to AEDT material names based on the layer stack.

Resolves each object to its LayerLevel either by parsing the signal<layer_number> GDS import names (same convention as rename_imported_objects()) or by matching the (renamed) object name against layer stack names. Metal levels (materials with infinite relative permittivity in material_properties) map to "pec"; dielectric levels map to their layer stack material name.

Returns:

Dictionary mapping object names to AEDT material names.

Raises:

ValueError – If an object cannot be resolved to a layer stack level, or its level material is not registered in material_properties. Failing closed avoids silently leaving objects with the project default material, which would make extracted results wrong rather than failed.

Parameters:
  • object_names (list[str])

  • layer_stack (LayerStack)

Return type:

dict[str, str]

qpdk.simulation.aedt_base.prepare_component_for_aedt(component, margin_draw=0.0, margin_etch=0.0, *, name=None)[source]#

Prepare a component for AEDT simulation export.

Thin backward-compatible wrapper around qpdk.simulation.layout.prepare_metal_layout(), keeping the historical <component>_aedt name for the initial preparation cell.

Returns:

A copy of the component prepared for simulation.

Parameters:
  • component (Component)

  • margin_draw (float)

  • margin_etch (float)

  • name (str | None)

Return type:

Component

qpdk.simulation.aedt_base.rename_imported_objects(app, new_objects, layer_stack)[source]#

Rename imported GDS objects based on the layer stack.

Returns:

List of renamed object names.

Parameters:
  • app (Any)

  • new_objects (list[str])

  • layer_stack (LayerStack)

Return type:

list[str]

HFSS#

HFSS simulation utilities using PyAEDT.

class qpdk.simulation.hfss.HFSS(*args, **kwargs)[source]#

Bases: AEDTBase

HFSS simulation wrapper.

Provides high-level methods for importing components into HFSS, setting up simulation regions, and extracting results.

Parameters:
Return type:

Any

add_air_region(component, height=500.0, substrate_thickness=500.0, pec_boundary=False, name='AirRegion')[source]#

Add an air region (vacuum box) around the component.

Parameters:
  • component (Component) – The component to create air region around.

  • height (float) – Height above the component in micrometers.

  • substrate_thickness (float) – Depth below surface for the region.

  • pec_boundary (bool) – If True, assign PerfectE boundary conditions to outer faces.

  • name (str) – Name of the created region object.

Returns:

Name of the created region object.

Return type:

str

add_lumped_ports(ports, cpw_gap, cpw_width)[source]#

Add lumped ports to HFSS at given port locations.

Parameters:
  • ports (Ports) – Collection of gdsfactory ports defining signal locations.

  • cpw_gap (float) – The length of the port along the axis of propagation.

  • cpw_width (float) – The width of the port perpendicular to propagation.

Return type:

None

get_eigenmode_results(setup_name='EigenmodeSetup')[source]#

Extract eigenmode simulation results.

Parameters:

setup_name (str) – Name of the setup to get results from.

Returns:

  • frequencies: List of eigenmode frequencies in GHz

  • q_factors: List of Q factors for each mode

Return type:

Dictionary containing

get_sparameter_results(setup_name='DrivenSetup', sweep_name='FrequencySweep')[source]#

Extract S-parameter results from a driven simulation.

Parameters:
  • setup_name (str) – Name of the setup.

  • sweep_name (str) – Name of the frequency sweep.

Returns:

DataFrame containing a ‘frequency_ghz’ column and a column for each S-parameter trace (e.g., “S(1,1)”) containing complex values.

Return type:

DataFrame

import_component(component, layer_stack=None, *, import_as_sheets=False, units='um', gds_path=None)[source]#

Import a gdsfactory component into HFSS.

Parameters:
  • component (Component) – The gdsfactory component to import.

  • layer_stack (LayerStack | None) – LayerStack defining thickness and elevation for each layer. If None, uses QPDK’s default LAYER_STACK.

  • import_as_sheets (bool) – If True, imports metals as 2D sheets (zero thickness) and assigns PerfectE boundary to them. If False, imports as 3D objects with thickness from layer_stack and assigns PerfectE boundary to their surfaces.

  • units (str) – Length units for the geometry (default: “um” for micrometers).

  • gds_path (str | Path | None) – Optional path to write the GDS file. If None, uses a temporary file.

Returns:

True if import was successful, False otherwise.

Return type:

bool

class qpdk.simulation.hfss.LumpedPortConfig[source]#

Bases: TypedDict

Configuration for defining a lumped port rectangle in HFSS.

qpdk.simulation.hfss.lumped_port_rectangle_from_cpw(center, orientation, cpw_gap, cpw_width)[source]#

Calculates parameters for a lumped port based on its orientation.

Parameters:
  • center (tuple[float, float, float]) – [x, y, z] coordinates of the port face center.

  • orientation (float) – Angle in degrees (must be a multiple of 90).

  • cpw_gap (float) – The length of the port along the axis of propagation.

  • cpw_width (float) – The width of the port perpendicular to propagation.

Returns:

A dictionary containing ‘origin’, ‘sizes’, and ‘integration_line’ for HFSS.

Raises:

ValueError – If port orientation is not a multiple of 90 degrees.

Return type:

LumpedPortConfig

Q3D and Q2D#

Q3D and Q2D simulation utilities using PyAEDT.

class qpdk.simulation.q3d.Q2D(*args, **kwargs)[source]#

Bases: AEDTBase

Q2D simulation wrapper.

Provides methods for 2D cross-sectional impedance extraction.

Parameters:
Return type:

Any

create_2d_from_cross_section(cross_section, layer_stack=None, *, ground_width=None, units='um')[source]#

Create a 2D model from a CPW cross-section for impedance extraction.

Builds the cross-sectional geometry of a coplanar waveguide in Ansys Q2D (2D Extractor).

Parameters:
  • cross_section (CrossSectionSpec) – A gdsfactory cross-section specification describing the CPW geometry (width and gap).

  • layer_stack (LayerStack | None) – LayerStack defining substrate and conductor properties. If None, uses QPDK’s default LAYER_STACK.

  • ground_width (float | None) – Width of each coplanar ground plane in µm. If None, defaults to 10× the CPW gap.

  • units (str) – Length units for the Q2D geometry (default "um").

Returns:

Dictionary with keys "signal", "gnd_left", "gnd_right", "substrate" mapping to the created Q2D object names.

Raises:

ValueError – If cross-section mapping fails or dimensions are invalid.

Return type:

dict[str, str]

class qpdk.simulation.q3d.Q3D(*args, **kwargs)[source]#

Bases: AEDTBase

Q3D Extractor simulation wrapper.

Provides methods for importing components into Q3D and performing parasitic capacitance/inductance extraction.

Parameters:
Return type:

Any

assign_nets_from_ports(ports, conductor_objects)[source]#

Assign Q3D signal nets based on gdsfactory port locations.

For each gdsfactory port, finds the conductor object whose bounding-box center is nearest to the port center and assigns it as a Q3D signal net.

Parameters:
  • ports (Ports) – Collection of gdsfactory ports defining signal locations.

  • conductor_objects (list[str]) – List of conductor object names created by import_component().

Returns:

List of assigned signal net names (one per port).

Return type:

list[str]

get_capacitance_matrix(setup_name='Q3DSetup')[source]#

Extract the capacitance matrix from a Q3D Extractor simulation.

Retrieves all capacitance matrix entries (e.g. C(o1,o1), C(o1,o2)) from the solved Q3D setup.

Parameters:

setup_name (str) – Name of the analysis setup.

Returns:

DataFrame with one column per capacitance expression containing the extracted values in Farads.

Return type:

DataFrame

import_component(component, layer_stack=None, *, units='um', gds_path=None)[source]#

Import a gdsfactory component into Q3D Extractor.

Imports the component’s GDS geometry into a Q3D Extractor project, mapping each GDS layer to a 3D conductor at the appropriate elevation and thickness from the layer stack.

Parameters:
  • component (Component) – The gdsfactory component to import.

  • layer_stack (LayerStack | None) – LayerStack defining thickness and elevation for each layer. If None, uses QPDK’s default LAYER_STACK.

  • units (str) – Length units for the geometry (default: “um” for micrometers).

  • gds_path (str | Path | None) – Optional path to write the GDS file. If None, uses a temporary file.

Returns:

List of newly created conductor object names in Q3D.

Raises:

RuntimeError – If GDS import fails.

Return type:

list[str]

COMSOL#

The builders below return a plain MPh model. Study and local refinement helpers take that model and its layout; absolute mesh helpers use the model and named selections. COMSOL wraps the two in one object: it subclasses mph.Model, so MPh solves, saves, and evaluates it as usual, and it holds the layout, so a setup reads as a chain.

  1. Build with create_sheet() or create_metal()

  2. Add a study with add_cpw_rf_study() or add_capacitance_study()

  3. Mesh it with refine_metal_plane_mesh(), pin_absolute_mesh_sizes(), or pin_absolute_edge_mesh_sizes()

  4. Solve, save, and evaluate with MPh’s own solve, save, and evaluate

Constructing the class needs the comsol extra; geometry and result helpers remain importable without it.

An MPh model with QPDK layout, study, and mesh methods.

COMSOL keeps the extracted layout with the model while retaining MPh’s solve, save, and evaluation API. Import it from qpdk.simulation.comsol or qpdk.simulation after installing the optional comsol extra.

class qpdk.simulation.comsol.model.COMSOL(model, layout)[source]#

Bases: Model

An MPh model holding the QPDK layout its geometry was built from.

Use create_sheet() or create_metal() to build one. The study and mesh methods then use the stored layout. Study methods return the model for chaining; mesh methods return the resulting element count.

Parameters:
add_capacitance_study(*, conductors, terminal, grounds, voltage_v=1.0, mesh_size=7)[source]#

Add an unsolved electrostatic capacitance study.

Calls add_capacitance_study() on this model and its layout.

Parameters:
  • conductors (tuple[tuple[str, Point], ...]) – One (tag, point) pair per metal face. The tag names the face selection created on comp1, and the point in µm on the sheet plane sits well inside that face.

  • terminal (str) – Tag of the conductor to drive with the voltage terminal.

  • grounds (tuple[str, ...]) – Tags of the conductors to ground. Together with terminal they name every conductor exactly once.

  • voltage_v (float) – Terminal voltage in V, positive.

  • mesh_size (int) – COMSOL mesh size, an integer from 1 (finest) to 9 (coarsest).

Returns:

This model, so the call chains.

Return type:

Self

add_cpw_rf_study(*, cpw_gap_um, frequency_ghz=7.5, mesh_size=8, effective_index_shift=2.5)[source]#

Add an unsolved CPW full-wave study.

Calls add_cpw_rf_study() on this model and its layout. The model has to be a sheet model whose layout was extracted with crop_to_feed_ports=True.

Parameters:
  • cpw_gap_um (float) – Width of the etch gap between the centre conductor and the ground at the ports, in µm.

  • frequency_ghz (float) – Boundary mode analysis reference and initial frequency in GHz.

  • mesh_size (int) – COMSOL mesh size, an integer from 1 (finest) to 9 (coarsest).

  • effective_index_shift (float) – Effective-index shift both boundary mode analyses search around, positive and finite.

Returns:

This model, so the call chains.

Return type:

Self

classmethod create_metal(client, layout, *, metal_thickness_um=0.2, name='QPDK metal')[source]#

Build a metal model, with the polygons extruded to a thickness.

The layout-agnostic geometry milestone: no physics, materials, ports, or studies, and the feed ports are not used.

Parameters:
  • client (mph.Client) – A connected mph.Client.

  • layout (ComsolLayout) – Extracted metal polygons and feed ports in µm.

  • metal_thickness_um (float) – Extrusion height in µm, strictly positive.

  • name (str) – Name of the COMSOL model.

Returns:

The built model, holding the layout it was built from. The builder validates the thickness and refuses a layout with no polygons.

Return type:

Self

classmethod create_sheet(client, layout, name, *, substrate_thickness_um=200.0, air_height_um=200.0, lateral_margin_um=0.0, silicon_relative_permittivity=11.45)[source]#

Build a sheet model, with the metal as faces on the z = 0 interface.

Parameters:
  • client (mph.Client) – A connected mph.Client.

  • layout (ComsolLayout) – Extracted metal polygons and bounding box in µm.

  • name (str) – Name of the COMSOL model.

  • substrate_thickness_um (float) – Silicon thickness below the interface, in µm.

  • air_height_um (float) – Air height above the interface, in µm.

  • lateral_margin_um (float) – Margin around the layout bounding box for both blocks, in µm.

  • silicon_relative_permittivity (float) – Relative permittivity of the silicon block, positive and finite. Defaults to the QPDK technology value for Si.

Returns:

The built model, holding the layout it was built from. The builder validates the thicknesses, the margin, the permittivity, and the resulting geometry, and a layout with holes needs the Design Module licence.

Return type:

Self

pin_absolute_edge_mesh_sizes(*, edge_selection, global_hmax_um, global_hmin_um, edge_hmax_um, edge_hmin_um)[source]#

Mesh at absolute element sizes on a named edge selection.

Calls pin_absolute_edge_mesh_sizes() on this model and its layout.

Parameters:
  • edge_selection (str) – Name of the edge selection to pin the local sizes on.

  • global_hmax_um (float) – Largest element size in µm away from the edges.

  • global_hmin_um (float) – Smallest element size in µm away from the edges.

  • edge_hmax_um (float) – Largest element size in µm on the selected edges.

  • edge_hmin_um (float) – Smallest element size in µm on the selected edges.

Returns:

The number of mesh elements the pinned sequence built.

Return type:

int

pin_absolute_mesh_sizes(*, global_hmax_um, global_hmin_um, face_sizes, hgrad=None, hcurve=None, hnarrow=None)[source]#

Mesh at absolute element sizes.

Calls pin_absolute_mesh_sizes() on this model and its layout.

Parameters:
  • global_hmax_um (float) – Largest element size in µm away from the metal.

  • global_hmin_um (float) – Smallest element size in µm away from the metal.

  • face_sizes (Mapping[str, tuple[float, float]]) – (hmax, hmin) element sizes in µm keyed by the name of a face selection on comp1.

  • hgrad (float | None) – Maximum element growth rate for the global sizes.

  • hcurve (float | None) – Curvature resolution for the global sizes, in elements per radian.

  • hnarrow (float | None) – Narrow region resolution for the global sizes.

Returns:

The number of mesh elements the pinned sequence built.

Return type:

int

refine_metal_plane_mesh(passes, *, z_half_um=20.0, refine_box=None)[source]#

Mesh and refine around the metal plane.

Calls refine_metal_plane_mesh() on this model and its layout.

Parameters:
  • passes (int) – Number of refinement passes, a non-negative integer.

  • z_half_um (float) – Half-height in µm of the refine box above and below the metal plane.

  • refine_box (ComsolBoundingBox | None) – x/y bounds in µm to refine instead of the layout bounding box.

Returns:

The number of mesh elements after meshing.

Return type:

int

Extract a gdsfactory component into COMSOL-ready metal polygons and ports.

This module is pure geometry: it converts a QPDK M1_ETCH mask into physical M1_DRAW metal polygons (in micrometres) plus optional feed-port metadata so a separate COMSOL model builder can consume it. Layouts that are not fed (e.g. an eigenmode qubit cell) are extracted with feed_ports=None and carry no feed metadata. No COMSOL, MPh, or PyAEDT imports live here.

The negative-mask trick lives in the neutral prepare_metal_layout(): it folds the additive metal into the etch layer, then inverts the mask around the component bounding box and applies the ground margin. The result exchanged here has positive M1_DRAW metal, no etch layers, and the original ports re-added.

Optionally the prepared metal can be cropped at the two feed-port planes so a component whose CPW conductors and both etch-gap strips reach those planes ends in open CPW cross sections instead of solid ground.

class qpdk.simulation.comsol.layout.ComsolBoundingBox(*, xmin, ymin, xmax, ymax)[source]#

Bases: object

Axis-aligned bounding box of the prepared ground region, in µm.

Parameters:
property height: float#

Box height in µm.

property width: float#

Box width in µm.

class qpdk.simulation.comsol.layout.ComsolFeedPort(*, name, center, width, orientation)[source]#

Bases: object

A feed port selected for the COMSOL model, in µm and degrees.

Parameters:
class qpdk.simulation.comsol.layout.ComsolLayout(*, polygons, feed_ports, bbox)[source]#

Bases: object

Extracted COMSOL geometry and feed ports for one component.

All coordinates are in micrometres in the component’s own frame. feed_ports is empty for layouts extracted with feed_ports=None.

Note

The feed ports are reported as-is. Whether each one actually sits on the outer boundary of the prepared ground region is not asserted here: after the mask inversion the ground plane surrounds the feeds, so a port center can legitimately lie inside the bounding box. A model builder that needs the port to coincide with a ground boundary must check that against polygons and bbox itself. The exception is an extraction made with crop_to_feed_ports=True, where the two feed planes bound the metal and each feed center lies on a face of bbox.

Parameters:
class qpdk.simulation.comsol.layout.ComsolPolygon(*, outline, holes=())[source]#

Bases: object

One physical metal polygon in µm.

outline is the outer boundary (vertices in order, first vertex not repeated). holes are inner boundaries, i.e. etched negative space enclosed by metal.

A single simple COMSOL polygon point list cannot encode a hole, so a consumer that only accepts plain outlines must either subtract holes itself or reject polygons where holes is non-empty. outline alone is the complete shape only when holes is empty.

Parameters:
qpdk.simulation.comsol.layout.prepare_comsol_layout(component, feed_ports=('coupling_o1', 'coupling_o2'), ground_margin=100.0, *, crop_to_feed_ports=False)[source]#

Extract M1_DRAW metal polygons and optional feed ports from a component.

Parameters:
  • component (Component) – The gdsfactory component to extract. It must contain an M1_ETCH mask; positive M1_DRAW shapes alone do not define the gaps. The original ports are preserved.

  • feed_ports (tuple[str, str] | None) – Names of exactly two distinct ports to expose as feeds, or None for an unfed layout, in which case the result carries no feed ports. None fits layouts with no CPW feedline, e.g. an eigenmode qubit cell.

  • ground_margin (float) – Positive margin in µm added around the component bounding box to form the ground plane.

  • crop_to_feed_ports (bool) – When True, cut the prepared metal back to the two feed-port planes so each external face shows an open CPW cross section instead of the extended ground. Requires two feed ports that are axis-aligned, opposite, outward-facing, and share their transverse coordinate (left/right along x, or bottom/top along y). Cropping is refused unless every M1_DRAW and M1_ETCH polygon of the input component lies between the planes and the etch gaps reach both planes, so no resonator metal is cut and no face is shorted. The planes snap outwards to the database grid, so the returned bbox can exceed the port span by less than one database unit. Defaults to False, which leaves the prepared ground untouched.

Returns:

A ComsolLayout with metal polygons (holes preserved), the selected feed ports (empty when feed_ports is None), and the prepared bounding box, all in µm.

Raises:

ValueError – If ground_margin is not positive and finite, if feed_ports is supplied but does not name two distinct available ports, if a feed port is not cardinal or has non-finite coordinates or width, if the component has no M1_ETCH mask or carries geometry on unsupported fabrication layers, if no M1_DRAW metal remains, or if crop_to_feed_ports is set but the feeds are not a valid opposite pair or the component does not fit between the planes.

Return type:

ComsolLayout

Build unsolved extruded metal geometry from an extracted layout.

COMSOL extends MPh’s model with QPDK layout, study, and mesh methods and builds a model through this builder; the sheet builder for RF and electrostatic studies lives in qpdk.simulation.comsol.sheet.

qpdk.simulation.comsol.metal.build_comsol_metal_model(client, layout, *, metal_thickness_um=0.2, name='QPDK metal')[source]#

Create a COMSOL 3D model holding the layout’s metal as extruded polygons.

Works for any extracted layout, fed or not: the feed ports are not used here. Each polygon outline becomes a solid polygon on work plane wp1; each hole becomes a second polygon subtracted by its own Difference feature. The finished work plane is extruded to metal_thickness_um by ext1.

Parameters:
  • client (mph.Client) – A connected mph.Client.

  • layout (ComsolLayout) – Extracted metal polygons and feed ports in µm.

  • metal_thickness_um (float) – Extrusion height in µm, strictly positive.

  • name (str) – Name of the COMSOL model.

Returns:

no physics, materials, ports, or studies have been added, and the model has not been saved.

Return type:

The MPh model. Geometry only

Raises:

ValueError – If metal_thickness_um is not positive and finite, or the layout has no polygons to extrude.

Build air and silicon domains with QPDK metal sheets at their interface.

The metal polygons are faces at z = 0. Layouts without holes use a work plane and Form Union. Layouts with holes use the Design Module’s ProjectToFaces to imprint each outline and hole onto the interface; the builder checks that those faces survived. Air and silicon get named selections and materials. Physics and studies are added separately by add_cpw_rf_study() or add_capacitance_study().

qpdk.simulation.comsol.sheet.AIR_SELECTION = 'air'#

Domain selection tags, reused by the physics added on top of this model.

qpdk.simulation.comsol.sheet.SILICON_RELATIVE_PERMITTIVITY = 11.45#

Canonical relative permittivity of the silicon substrate and of the air above it, from the materials of the QPDK layer stack’s substrate and vacuum levels.

qpdk.simulation.comsol.sheet.build_comsol_sheet_model(client, layout, name, *, substrate_thickness_um=200.0, air_height_um=200.0, lateral_margin_um=0.0, silicon_relative_permittivity=11.45)[source]#

Create a COMSOL 3D model holding the layout’s metal as interface sheets.

The air and silicon blocks touch at z = 0 and span the layout bounding box grown by lateral_margin_um. A layout without holes puts the metal polygons on a work plane and lets Form Union leave one face per polygon. A layout with holes keeps the interface between the blocks and projects a construction work plane’s faces onto it, so the model ends up with two dielectric domains and one face per metal and per hole region.

A layout with holes is built with the Design Module’s ProjectToFaces feature and the CAD kernel geometry representation, so it needs the Design Module license; a layout without holes needs neither.

Parameters:
  • client (mph.Client) – A connected mph.Client.

  • layout (ComsolLayout) – Extracted metal polygons and bounding box in µm.

  • name (str) – Name of the COMSOL model.

  • substrate_thickness_um (float) – Silicon thickness below the interface, in µm.

  • air_height_um (float) – Air height above the interface, in µm.

  • lateral_margin_um (float) – Margin around the layout bounding box for both blocks, in µm.

  • silicon_relative_permittivity (float) – Relative permittivity of the silicon block, positive and finite. Defaults to the QPDK technology value for Si.

Returns:

The MPh model, with geometry, selections, and materials only.

Raises:

ValueError – If a thickness is not positive and finite, if the margin is negative or not finite, if the permittivity is not positive and finite, if the layout has no polygons, or, for a layout with holes, if the built geometry does not carry one face per metal polygon with the holes still cut out of the metal.

Return type:

mph.Model

Add an unsolved CPW full-wave study to a COMSOL model that uses metal sheets.

The model comes from build_comsol_sheet_model(): air and silicon meeting at z = 0 with the layout’s metal imprinted on that plane as faces. This module makes those faces perfect electric conductors, turns the two feed cross sections into numeric ports, and adds a mesh sequence and a frequency study. Nothing is meshed or solved here.

The port faces and the integration line are found from the feed ports and the bounding box of the ComsolLayout, never from hard-coded entity IDs, so a layout whose feeds do not sit on opposite bounding-box faces is refused instead of quietly porting the wrong plane.

What a solve gives is the S-parameters of the CPW section between the two planes: emw.S21dB is the through response at frequency_ghz, emw.S11dB the reflection. It is not an eigenmode solve, so it says nothing about a qubit hanging off the feedline.

qpdk.simulation.comsol.rf.INPUT_FACES_SELECTION = 'input_faces'#

Named selections for the two port cross sections, low side first.

qpdk.simulation.comsol.rf.INPUT_GAP_SELECTION = 'input_gap'#

Named selections for the integration line of each port, low side first.

qpdk.simulation.comsol.rf.METAL_FACES_SELECTION = 'metal_faces'#

Named selection holding every metal sheet face, for the PEC boundary.

qpdk.simulation.comsol.rf.add_cpw_rf_study(model, layout, *, cpw_gap_um, frequency_ghz=7.5, mesh_size=8, effective_index_shift=2.5)[source]#

Add an unsolved CPW full-wave study to a sheet model.

Parameters:
  • model (mph.Model) – The MPh model from build_comsol_sheet_model().

  • layout (ComsolLayout) – The same layout the model was built from, extracted with crop_to_feed_ports=True so both feed planes are open CPW cross sections on the bounding box.

  • cpw_gap_um (float) – Width of the etch gap between the centre conductor and the ground at the ports, in µm. The voltage integration line spans it.

  • frequency_ghz (float) – Boundary mode analysis reference and initial frequency in GHz.

  • mesh_size (int) – COMSOL mesh size, an integer from 1 (finest) to 9 (coarsest).

  • effective_index_shift (float) – Effective-index shift both boundary mode analyses search around, positive and finite. The default 2.5 suits the silicon/air CPW section and reproduces the saved examples.

Returns:

The same model, with the metal and port selections, the EMW interface, the mesh sequence, and the frequency study added. It has not been meshed or solved and has not been saved.

Raises:

ValueError – If the gap is not positive and finite, if the frequency is not positive and finite, if the mesh size is not an integer in 1 to 9, if the effective-index shift is not positive and finite, if the layout does not carry two opposite outward-facing feeds on the bounding box, or if a selection does not resolve to the faces or edges the layout implies.

Return type:

mph.Model

Add an electrostatic capacitance study to a COMSOL model that uses metal sheets.

The model comes from build_comsol_sheet_model(), where the metal is a set of faces on the silicon/air interface. This module picks those faces out with points inside them, drives one of them with a voltage terminal, grounds the others, and adds a mesh sequence and a stationary study. Nothing is solved here, and nothing here is specific to a device: the caller names the conductors, which one is driven, and which are grounded.

A solve gives electrostatic capacitance: es.C11 is the capacitance of the driven conductor to the grounded rest of the chip, and the stored energy agrees with it, since \(2 W_\text{e} / V^2\) equals es.C11. It is not an RF Josephson eigenmode, and turning it into a qubit frequency needs an \(L_\text{J}\) picked outside COMSOL: that LC estimate is not an eigensolve.

qpdk.simulation.comsol.capacitance.add_capacitance_study(model, layout, *, conductors, terminal, grounds, voltage_v=1.0, mesh_size=7)[source]#

Add an unsolved electrostatic capacitance study to a sheet model.

Parameters:
  • model (mph.Model) – The MPh model from build_comsol_sheet_model().

  • layout (ComsolLayout) – The metal polygons used to build that model. It must hold exactly one polygon per conductor; a metal polygon that no point names is refused rather than left as a dielectric interface.

  • conductors (tuple[tuple[str, Point], ...]) – One (tag, point) pair per metal face. The tag names the face selection created on comp1, and the point in µm on the sheet plane has to sit inside that face. It centres a small box, so it has to sit clear of the face’s edges.

  • terminal (str) – Tag of the conductor to drive with the voltage terminal.

  • grounds (tuple[str, ...]) – Tags of the conductors to ground, in the order their Ground features are tagged. Together with terminal they have to name every conductor exactly once.

  • voltage_v (float) – Terminal voltage in V, positive.

  • mesh_size (int) – COMSOL mesh size, an integer from 1 (finest) to 9 (coarsest).

Returns:

The same model, with the selections, physics, mesh, and study added and nothing solved. Once the caller runs them, es.C11 agrees with 2*es.intWe/voltage_v**2.

Raises:

ValueError – If the voltage is not positive and finite, if the mesh size is not an integer in 1 to 9, if a point is not a finite (x, y) pair, if the conductor tags do not name a terminal and a ground each, or if the layout, the polygons, or the points do not assign exactly one distinct metal face to each conductor.

Return type:

mph.Model

qpdk.simulation.comsol.capacitance.add_electrostatics(component, *, terminal, grounds, voltage_v)[source]#

Add Electrostatics with one voltage terminal and a ground per selection.

The interface covers every domain, so the default Free Space feature keeps the air, and a Charge Conservation feature on silicon uses that domain’s material permittivity through the default epsilonr_mat.

Parameters:
  • component (Any) – The Java component to add the physics to, usually comp1.

  • terminal (str) – Name of the face selection to drive with the voltage source.

  • grounds (tuple[str, ...]) – Names of the face selections to ground, one Ground feature each. The features are tagged gnd1, gnd2, and so on, in this order.

  • voltage_v (float) – Terminal voltage in V, positive.

Return type:

None

Mesh helpers for COMSOL sheet models.

Both COMSOL notebooks build a study on the same sheet geometry and then want one more thing from the mesh: the element count after a local refinement at the metal plane, to check that a solved quantity has stopped moving with mesh size. The sequence is the same in both, so it lives here once.

The study builders (add_cpw_rf_study() and add_capacitance_study()) leave mesh1 physics-controlled. Running it once realizes that sizing; a Refine feature then switches the sequence to user-controlled, and the second run meshes the refined box.

pin_absolute_mesh_sizes() is for the other kind of study, where the mesh must not follow the domain at all. A physics-controlled mesh scales its element sizes with the longest dimension of the domain, so a series that varies the distance to the boundary would vary the near-metal resolution with it.

pin_absolute_edge_mesh_sizes() is the same idea one dimension down: the local sizes go on a named edge selection instead of conductor faces, for a study whose resolution has to follow a trace outline rather than cover a whole face.

qpdk.simulation.comsol.mesh.DEFAULT_SIZE_TAG = 'size'#

Tag of the default Size feature a physics-controlled build leaves in the sequence, and the one feature pinning the global sizes requires to be there. The mesh chapter of the COMSOL programming reference warns that the contents of a physics-controlled sequence may change between versions, so it is checked at run time rather than assumed.

qpdk.simulation.comsol.mesh.EDGE_SIZE_TAG = 'size_edges'#

Tag of the Size feature pin_absolute_edge_mesh_sizes() creates for the named edge selection. It sits between the default size and the generator for the same reason every other size does.

qpdk.simulation.comsol.mesh.FREE_TET_TAG = 'ftet_fixed'#

Tag of the generator this module creates, the only one left in the sequence. A Size feature only affects the operation features after it, so this has to follow every size.

qpdk.simulation.comsol.mesh.GENERATED_FREE_TET_TAG = 'ftet1'#

FreeTet generator COMSOL generates for a physics-controlled sequence. It does not survive an edit of the sequence on a 6.3 build, but exactly when it goes is undocumented, so it is swept either way. Nothing may depend on it being there.

qpdk.simulation.comsol.mesh.pin_absolute_edge_mesh_sizes(model, *, edge_selection, global_hmax_um, global_hmin_um, edge_hmax_um, edge_hmin_um)[source]#

Mesh a sheet model with absolute element sizes on a named edge selection.

The edge counterpart of pin_absolute_mesh_sizes(), for the case where the resolution has to follow an outline instead of covering a face: the bulk stays at the global sizes and one Size feature pins the edge sizes on edge_selection, which the caller has already created on comp1. Both sizes are absolute in µm, so the edge resolution no longer moves with the domain. The sequence is left user-controlled, as there.

The sequence is built the same way and refused if it comes out any other way, including an edge_selection that resolves to no edge, which would leave the local size applying to nothing.

Parameters:
  • model (mph.Model) – A model carrying comp1/mesh1 from a study builder, with a named edge selection already created on comp1.

  • edge_selection (str) – Name of the edge selection to pin the local sizes on.

  • global_hmax_um (float) – Largest element size in µm away from the edges, positive and finite.

  • global_hmin_um (float) – Smallest element size in µm away from the edges.

  • edge_hmax_um (float) – Largest element size in µm on the selected edges.

  • edge_hmin_um (float) – Smallest element size in µm on the selected edges.

Returns:

The number of mesh elements the pinned sequence built.

Raises:
  • ValueError – If a size is not positive and finite, or if an hmin is larger than the hmax it belongs to.

  • RuntimeError – If the physics-controlled build left no default size feature, the edge selection resolved to no edge, the sequence came out in an order the sizes would not apply in, the sequence is still physics-controlled afterwards, or COMSOL reported no usable element count.

Return type:

int

qpdk.simulation.comsol.mesh.pin_absolute_mesh_sizes(model, *, global_hmax_um, global_hmin_um, face_sizes, hgrad=None, hcurve=None, hnarrow=None)[source]#

Mesh a sheet model with absolute element sizes at the metal.

A physics-controlled mesh scales its sizes with the longest dimension of the domain, so the near-metal resolution moves whenever the domain does. This helper runs that mesh once to materialise the sequence, then pins every size to an absolute value in µm: the default Size feature takes the global sizes, and one Size feature per entry of face_sizes takes the local sizes on the named face selection that entry is keyed by. The sequence is left user-controlled, so a later build or solve cannot re-derive the sizing.

A Size feature only affects the operation features after it, so the face sizes have to sit between the default size and the generator. The construction builds that order and refuses to mesh if the sequence comes out any other way, rather than silently meshing something else. The generated FreeTet of the physics-controlled sequence does not survive an edit, and when it goes is undocumented, so it is swept before and after the explicit generator is created, leaving exactly one generator. The Size feature properties are in the COMSOL 6.3 API reference, https://doc.comsol.com/6.3/doc/com.comsol.help.comsol/comsol_api_mesh.49.099.html

Parameters:
  • model (mph.Model) – A model carrying comp1/mesh1 from add_capacitance_study(), including the face selections that study creates.

  • global_hmax_um (float) – Largest element size in µm away from the metal, positive and finite.

  • global_hmin_um (float) – Smallest element size in µm away from the metal.

  • face_sizes (Mapping[str, tuple[float, float]]) – (hmax, hmin) element sizes in µm keyed by the name of a face selection on comp1, one Size feature each, tagged size_ followed by the selection name.

  • hgrad (float | None) – Maximum element growth rate for the global sizes.

  • hcurve (float | None) – Curvature resolution for the global sizes, in elements per radian.

  • hnarrow (float | None) – Narrow region resolution for the global sizes.

Returns:

The number of mesh elements the pinned sequence built.

Raises:
  • ValueError – If a mesh size is invalid or a minimum exceeds its maximum.

  • RuntimeError – If the physics-controlled build left no default size feature, the sequence came out in an order the sizes would not apply in, the sequence is still physics-controlled afterwards, or COMSOL reported no usable element count.

Return type:

int

qpdk.simulation.comsol.mesh.refine_metal_plane_mesh(model, layout, passes, *, z_half_um=20.0, refine_box=None)[source]#

Mesh a sheet model and refine the mesh around the metal plane.

The mesh is run once at the study builder’s physics-controlled size, then refined passes times inside a box: it spans refine_box in x and y and ±``z_half_um`` in z, so the metal plane and the fields just off it are refined while the bulk air and silicon stay coarse. Pass refine_box when the layout’s own box is much larger than the part that matters; a few refinement passes over a whole 3.5 mm ground box cost elements for nothing when the device is 1 mm across.

Parameters:
  • model (mph.Model) – A model carrying comp1/mesh1 from add_cpw_rf_study() or add_capacitance_study().

  • layout (ComsolLayout) – The layout the model was built from. Its bounding box sets the refine box unless refine_box is given.

  • passes (int) – Number of refinement passes, a non-negative integer. 0 meshes at the physics-controlled size without refining.

  • z_half_um (float) – Half-height in µm of the refine box above and below the metal plane, positive and finite.

  • refine_box (ComsolBoundingBox | None) – x/y bounds in µm to refine instead of the layout bounding box. None uses the layout’s own box.

Returns:

The number of mesh elements after meshing.

Raises:

ValueError – If passes is not a non-negative integer, or if z_half_um is not positive and finite.

Return type:

int