"""An MPh model with QPDK layout, study, and mesh methods.
:class:`COMSOL` keeps the extracted layout with the model while retaining MPh's
solve, save, and evaluation API. Import it from :mod:`qpdk.simulation.comsol` or
:mod:`qpdk.simulation` after installing the optional ``comsol`` extra.
"""
from __future__ import annotations
import importlib
from collections.abc import Mapping
from types import SimpleNamespace
from typing import TYPE_CHECKING, Self
from qpdk.simulation.comsol import (
capacitance,
mesh,
metal,
rf,
sheet,
)
if TYPE_CHECKING:
import mph
from qpdk.simulation.comsol.layout import ComsolBoundingBox, ComsolLayout, Point
else:
try:
mph = importlib.import_module("mph")
except ModuleNotFoundError as error:
if error.name != "mph":
raise
mph = SimpleNamespace(Model=object)
[docs]
class COMSOL(mph.Model):
"""An MPh model holding the QPDK layout its geometry was built from.
Use :meth:`create_sheet` or :meth:`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.
"""
def __init__(self, model: mph.Model, layout: ComsolLayout) -> None:
"""Wrap an already created MPh model and the layout behind it.
Args:
model: The model a builder returned, e.g.
:func:`~qpdk.simulation.comsol.sheet.build_comsol_sheet_model`.
Its Java handle is reused as-is.
layout: The layout that model was built from.
Raises:
ImportError: If the optional MPh dependency is not installed.
"""
if mph.Model is object:
raise ImportError("Install qpdk[comsol] to use the COMSOL model")
super().__init__(model)
self.layout = layout
[docs]
@classmethod
def create_sheet(
cls,
client: mph.Client,
layout: ComsolLayout,
name: str,
*,
substrate_thickness_um: float = 200.0,
air_height_um: float = 200.0,
lateral_margin_um: float = 0.0,
silicon_relative_permittivity: float = sheet.SILICON_RELATIVE_PERMITTIVITY,
) -> Self:
"""Build a sheet model, with the metal as faces on the z = 0 interface.
Args:
client: A connected :class:`mph.Client`.
layout: Extracted metal polygons and bounding box in µm.
name: Name of the COMSOL model.
substrate_thickness_um: Silicon thickness below the interface, in µm.
air_height_um: Air height above the interface, in µm.
lateral_margin_um: Margin around the layout bounding box for both
blocks, in µm.
silicon_relative_permittivity: 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 cls(
sheet.build_comsol_sheet_model(
client,
layout,
name,
substrate_thickness_um=substrate_thickness_um,
air_height_um=air_height_um,
lateral_margin_um=lateral_margin_um,
silicon_relative_permittivity=silicon_relative_permittivity,
),
layout,
)
[docs]
def add_cpw_rf_study(
self,
*,
cpw_gap_um: float,
frequency_ghz: float = 7.5,
mesh_size: int = 8,
effective_index_shift: float = 2.5,
) -> Self:
"""Add an unsolved CPW full-wave study.
Calls :func:`~qpdk.simulation.comsol.rf.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``.
Args:
cpw_gap_um: Width of the etch gap between the centre conductor and
the ground at the ports, in µm.
frequency_ghz: Boundary mode analysis reference and initial
frequency in GHz.
mesh_size: COMSOL mesh size, an integer from 1 (finest) to 9
(coarsest).
effective_index_shift: Effective-index shift both boundary mode
analyses search around, positive and finite.
Returns:
This model, so the call chains.
"""
rf.add_cpw_rf_study(
self,
self.layout,
cpw_gap_um=cpw_gap_um,
frequency_ghz=frequency_ghz,
mesh_size=mesh_size,
effective_index_shift=effective_index_shift,
)
return self
[docs]
def add_capacitance_study(
self,
*,
conductors: tuple[tuple[str, Point], ...],
terminal: str,
grounds: tuple[str, ...],
voltage_v: float = 1.0,
mesh_size: int = 7,
) -> Self:
"""Add an unsolved electrostatic capacitance study.
Calls
:func:`~qpdk.simulation.comsol.capacitance.add_capacitance_study` on
this model and its layout.
Args:
conductors: 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: Tag of the conductor to drive with the voltage terminal.
grounds: Tags of the conductors to ground. Together with ``terminal``
they name every conductor exactly once.
voltage_v: Terminal voltage in V, positive.
mesh_size: COMSOL mesh size, an integer from 1 (finest) to 9
(coarsest).
Returns:
This model, so the call chains.
"""
capacitance.add_capacitance_study(
self,
self.layout,
conductors=conductors,
terminal=terminal,
grounds=grounds,
voltage_v=voltage_v,
mesh_size=mesh_size,
)
return self
[docs]
def pin_absolute_mesh_sizes(
self,
*,
global_hmax_um: float,
global_hmin_um: float,
face_sizes: Mapping[str, tuple[float, float]],
hgrad: float | None = None,
hcurve: float | None = None,
hnarrow: float | None = None,
) -> int:
"""Mesh at absolute element sizes.
Calls :func:`~qpdk.simulation.comsol.mesh.pin_absolute_mesh_sizes` on
this model and its layout.
Args:
global_hmax_um: Largest element size in µm away from the metal.
global_hmin_um: Smallest element size in µm away from the metal.
face_sizes: ``(hmax, hmin)`` element sizes in µm keyed by the name of
a face selection on ``comp1``.
hgrad: Maximum element growth rate for the global sizes.
hcurve: Curvature resolution for the global sizes, in elements per
radian.
hnarrow: Narrow region resolution for the global sizes.
Returns:
The number of mesh elements the pinned sequence built.
"""
return mesh.pin_absolute_mesh_sizes(
self,
global_hmax_um=global_hmax_um,
global_hmin_um=global_hmin_um,
face_sizes=face_sizes,
hgrad=hgrad,
hcurve=hcurve,
hnarrow=hnarrow,
)
[docs]
def pin_absolute_edge_mesh_sizes(
self,
*,
edge_selection: str,
global_hmax_um: float,
global_hmin_um: float,
edge_hmax_um: float,
edge_hmin_um: float,
) -> int:
"""Mesh at absolute element sizes on a named edge selection.
Calls
:func:`~qpdk.simulation.comsol.mesh.pin_absolute_edge_mesh_sizes` on
this model and its layout.
Args:
edge_selection: Name of the edge selection to pin the local sizes on.
global_hmax_um: Largest element size in µm away from the edges.
global_hmin_um: Smallest element size in µm away from the edges.
edge_hmax_um: Largest element size in µm on the selected edges.
edge_hmin_um: Smallest element size in µm on the selected edges.
Returns:
The number of mesh elements the pinned sequence built.
"""
return mesh.pin_absolute_edge_mesh_sizes(
self,
edge_selection=edge_selection,
global_hmax_um=global_hmax_um,
global_hmin_um=global_hmin_um,
edge_hmax_um=edge_hmax_um,
edge_hmin_um=edge_hmin_um,
)