Full-wave driven simulations with Palace¶
Warning
The full-wave driven plugin for Palace is experimental and currently supports only lumped ports that have to be placed manually as rectangles in the geometry.
Here, we show how Palace may be used to perform full-wave driven simulations in the frequency domain. See Palace – Crosstalk Between Coplanar Waveguides for more details.
For a given geometry, one needs to specify the terminals where to apply an excitation similar to Ansys HFSS. To this end, lumped ports (or wave ports) have to be added to the geometry to simulate. This effectively solves the scattering parameters for the terminals.
In this notebook, we the same interdigital capacitor as in {doc}palace_01_electrostatic.ipynb but add lumped ports to the geometry.
Afterwards, the capacitance matrix can be computed from the scattering parameters as described in Eq. {eq}s_to_y_to_c.
Installation¶
See Palace – Installation for installation or compilation instructions. Gplugins assumes palace is available in your PATH environment variable.
Alternatively, Singularity / Apptainer containers may be used. Instructions for building and an example definition file are found at Palace – Build using Singularity/Apptainer.
Afterwards, an easy install method is to add a script to ~/.local/bin (or elsewhere in PATH) calling the Singularity container. For example, one may create a palace file containing
Geometry, layer config and materials¶
import os
from math import inf
from pathlib import Path
import gdsfactory as gf
import numpy as np
import pyvista as pv
import skrf
from gdsfactory.components.interdigital_capacitor import interdigital_capacitor
from gdsfactory.gpdk import LAYER, get_generic_pdk
from gdsfactory.technology import LayerStack
from gdsfactory.technology.layer_stack import LayerLevel
from IPython.display import display
from matplotlib import pyplot as plt
from gplugins.common.types import RFMaterialSpec
from gplugins.palace import run_scattering_simulation_palace
gf.config.rich_output()
PDK = get_generic_pdk()
PDK.activate()
We employ an example LayerStack used in superconducting circuits similar to [@marxer_long-distance_2023].
layer_stack = LayerStack(
layers=dict(
substrate=LayerLevel(
layer=LAYER.WAFER,
thickness=500,
zmin=0,
material="Si",
mesh_order=99,
),
bw=LayerLevel(
layer=LAYER.WG,
thickness=200e-3,
zmin=500,
material="Nb",
mesh_order=2,
),
bw_port=LayerLevel(
layer=LAYER.PORT,
thickness=200e-3,
zmin=500,
material="Nb",
mesh_order=2,
),
)
)
material_spec: RFMaterialSpec = {
"Si": {"relative_permittivity": 11.45, "relative_permeability": 1},
"Nb": {"relative_permittivity": inf, "relative_permeability": 1},
"vacuum": {"relative_permittivity": 1, "relative_permeability": 1},
}
c = gf.Component()
cap = c << interdigital_capacitor()
# Add lumped port rectangles manually, see examples for https://awslabs.github.io/palace/stable/examples/cpw/
c.add_polygon([(-40, +11), (-46, +11), (-46, +5), (-40, +5)], layer=LAYER.PORT)
c.add_polygon([(40, -11), (46, -11), (46, -5), (40, -5)], layer=LAYER.WAFER)
c.add_port(
"o1_1", center=(-40, (11 + 5) / 2), layer=LAYER.PORT, width=1, orientation=180
)
c.add_port("o1_2", center=(+46, -11), layer=LAYER.PORT, width=1, orientation=0)
c.add_polygon([(-200, -200), (200, -200), (200, 200), (-200, 200)], layer=LAYER.WAFER)
c.draw_ports()
c.show()
c
Running the simulation¶
We use the function :func:`~run_scattering_simulation_palace`. This runs the simulation and returns an instance of :class:`~DrivenFullWaveResults` containing the capacitance matrix and a path to the mesh and the field solutions.
results = run_scattering_simulation_palace(
c,
layer_stack=layer_stack,
material_spec=material_spec,
only_one_port=True,
simulation_folder=Path(os.getcwd()) / "temporary",
driven_settings={
"MinFreq": 0.1,
"MaxFreq": 5,
"FreqStep": 5,
},
mesh_parameters=dict(
background_tag="vacuum",
background_padding=(0,) * 5 + (700,),
port_names=c.ports,
verbosity=1,
default_characteristic_length=200,
resolutions={
"bw": {
"resolution": 14,
},
"substrate": {
"resolution": 50,
},
"vacuum": {
"resolution": 120,
},
**{
f"bw_port{port}_vacuum": {
"resolution": 8,
"DistMax": 30,
"DistMin": 10,
"SizeMax": 14,
"SizeMin": 3,
}
for port in c.ports
},
},
),
)
display(results)
The capacitance matrix can be solved from the admittance matrix \(Y\) as
Math
:label: s_to_y_to_c C_{\text{i,j}} = \frac{\mathrm{Im}\,Y_{\text{i,j}}}{\mathrm{i} \omega} .
df = results.scattering_matrix
df.columns = df.columns.str.strip()
s_complex = 10 ** df["|S[2][1]| (dB)"].values * np.exp(
1j * skrf.degree_2_radian(df["arg(S[2][1]) (deg.)"].values)
)
ntw = skrf.Network(f=df["f (GHz)"].values, s=s_complex, z0=50)
cap = np.imag(ntw.y.flatten()) / (ntw.f * 2 * np.pi)
display(cap)
plt.plot(ntw.f, cap * 1e15)
plt.xlabel("Freq (GHz)")
plt.ylabel("C (fF)")
if results.field_file_locations:
pv.start_xvfb()
pv.set_jupyter_backend("trame")
field = pv.read(results.field_file_locations[0])
slice = field.slice_orthogonal(z=layer_stack.layers["bw"].zmin * 1e-6)
p = pv.Plotter()
p.add_mesh(slice, scalars="Ue", cmap="turbo")
p.show_grid()
p.camera_position = "xy"
p.enable_parallel_projection()
p.show()
Bibliography¶
Bibliography
:style: unsrt :filter: docname in docnames