Running Palace

Running Palace#

[1]:
import numpy as np
from math import pi
import yaml

from matplotlib import pyplot as plt

%matplotlib inline
%config InlineBackend.figure_format='retina'
[2]:
from zeroheliumkit import Rectangle, Anchor, Structure, Skeletone, Layer
from zeroheliumkit.src.settings import *
from zeroheliumkit.src.geometries import Meander, Rectangle, ArcLine
from zeroheliumkit.helpers.resonator_calc import CPW_params

from zeroheliumkit.fem import GMSHmaker, ExtrudeSettings, SurfaceSettings, PECSettings, MeshSettings, BoxFieldMeshSettings, DistanceFieldMeshSettings
[3]:
um = 1e-6
GHz = 1e9
plot_config = {
    "wafer": LIGHTGRAY,
    "gnd": BLUE,
    "open": ORANGE,
    "temp": WHITE,
    "island": YELLOW2,
    "ports": RED,
    "anchors": RED
}
[4]:
def construct_resonator_skeletone(params: dict) -> LineString:
    g_params = params["geometry"]
    d_a = g_params['coupling_length']
    d_w = g_params['distance_from_waveguide']
    d_l = g_params['openend_length']
    d_R = g_params['radius']
    n = g_params['num']
    w = g_params["w"]
    g = g_params["g"]

    cpw_params = CPW_params(params["eps"], w * um, g * um, params["substrate_h"] * um, 0)
    cpw_params.resonator_length(params["frequency"] * GHz, params["type"], 0)
    length = cpw_params.length/um

    d_b = (length - (d_l + d_a + d_w + d_R * (3 * pi/2 + 2 * pi * n + 1)))/(2 * n + 1)
    if d_b < 0:
        raise Exception("Incorrect geometry input. Choose different 'bending_radius', 'coupling_length', 'openend_length' or 'num_wiggles'")

    sk = Skeletone()
    sk.add(LineString([(0, 0), (0, d_l)]))
    sk.add(ArcLine(-d_R, 0, d_R, 0, 90, 6))
    sk.add(LineString([(0, 0), (-d_a, 0)]))
    sk.add(ArcLine(0, -d_R, d_R, 90, 180, 6))
    sk.add(LineString([(0, 0), (0, -d_w)]))
    sk.add(ArcLine(d_R, 0, d_R, 180, 270, 6))
    sk.add(LineString([(0, 0), (d_b/2, 0)]), ignore_crossing=True)
    for _ in range(n):
        sk.add(Meander(d_b, d_R, direction=-90, num_segments=12), ignore_crossing=True)
    sk.add(LineString([(0, 0), (d_b/2 + d_R, 0)]))

    return sk
[5]:
### Creating Geometry
[6]:
with open('params.yaml', 'r') as file:
    params = yaml.safe_load(file)

gp = params["resonator"]["geometry"]
fc = params["feedline"]

d = Structure()
d.skeletone = construct_resonator_skeletone(params["resonator"])
d.add(Layer("op", d.skeletone.buffer(gp["w"]/2 + gp["g"], cap_style="square", join_style="mitre")))
# d.buffer_line("op", gp["w"]/2 + gp["g"], cap_style="square", join_style="mitre")
d.skeletone.add(LineString([(0, 0), (10, 0)]))
d.add(Layer("temp", d.skeletone.buffer(gp["w"]/2, cap_style="square", join_style="mitre")))
# d.buffer_line("temp", gp["w"]/2, cap_style="square", join_style="mitre")
d.anchors.add([Anchor((0,0), -90, "end")])

island = Structure()
island.add(Layer("isl", Rectangle(100, 20, (0,0))))
island.isl.add(Rectangle(20, 100, (0,0)))
island.add(Layer("op", island.isl.buffer(5, join_style='mitre')))
island.anchors.add([Anchor((0, 57.5), -90, "i1")])


d.append(island, anchoring=("end", "i1"))
d.op.add(Rectangle(60, 40, (0, 45 - 57.5)))
d.op.cut(Rectangle(15, 30, (20, 45 - 57.5)))
d.op.cut(Rectangle(15, 30, (-20, 45 - 57.5)))
d.op.cut(Rectangle(55, 8, (0, 0)))
d.op.cut(d.temp.polygons)
d.remove("temp")
d.anchors.remove("isl", "end")

d.add(Layer("wafer", Rectangle(700, 1600, (-200, 600))))
d.add(Layer("gnd", Rectangle(700, 1600, (-200, 600))))
d.gnd.cut(d.op.polygons)
d.remove("op")

bbox = d.skeletone.lines.bounds
y_offset = 30
x_offset = 130
y_feedline = bbox[3] + y_offset
fdl = Structure()
fdl.skeletone.add(LineString([(bbox[0] - x_offset, y_feedline), (bbox[2] + x_offset, y_feedline)]))
fdl.add(Layer("gnd", fdl.skeletone.buffer(fc["w"]/2 + fc["g"], cap_style="square", join_style="mitre")))
fdl.gnd.cut(fdl.skeletone.buffer(fc["w"]/2, cap_style="square", join_style="mitre"))


d.gnd.cut(fdl.gnd.polygons)
d.add(Layer("ports", Rectangle(4, 200, (bbox[0] - x_offset, y_feedline))))
d.ports.add(Rectangle(4, 200, (bbox[2] + x_offset, y_feedline)))
d.ports.cut(d.gnd.polygons)

d.gnd.add(d.isl.polygons)

bbox_island = d.isl.polygons.bounds
# adding gnd to the island
d.gnd.add(Rectangle(5, 10, (bbox_island[0]/2 + bbox_island[2]/2, bbox_island[1])))

d.remove("isl")

d.skeletone.add(fdl.skeletone.lines, chaining=False)
msh_lines = d.skeletone
d.gnd.remove_holes(cut_position=-80)


d.quickplot(color_config=plot_config, show_idx=True)
-------  ----------  ---------  -------  ---  -------  --------------  -------  -------
f0, GHz  length, mm  width, um  gap, um  eps  eps_eff  impedance, Ohm  L, nH/m  C, pF/m
7.7      3.974       8.0        4.5      11   6.0      52.57           429.52   155.423
-------  ----------  ---------  -------  ---  -------  --------------  -------  -------
[6]:
<Axes: >
../_images/notebooks_palace_6_2.png
[7]:
### Creating Mesh
[8]:
d_wafer     = 200
d_vac       = 200
d_metal     = 100

msh_filename = 'cpw'
save_dir = 'postpro/'
[9]:
Volumes = {
    "wafer": ExtrudeSettings(d.wafer.polygons, -d_wafer, d_wafer, "DIELECTRIC"),
    # "top": ExtrudeSettings(d.top, 0, d_metal, "METAL"),
    "vacuum": ExtrudeSettings(d.wafer.polygons, 0, d_vac, "VACUUM")
}

Surfaces = {
    "s0": SurfaceSettings(d.gnd.polygons, 0),
    "s1": SurfaceSettings(d.ports.polygons, 0)
}

PECs = {
    "conductor": PECSettings(d.gnd.polygons, [0,1,2,3], z=0),
    "port1+": PECSettings(d.ports.polygons, [0], z=0),
    "port1-": PECSettings(d.ports.polygons, [1], z=0),
    "port2+": PECSettings(d.ports.polygons, [2], z=0),
    "port2-": PECSettings(d.ports.polygons, [3], z=0)
}

mesh = MeshSettings(
    dim = 3,
    fields = {"Box": [BoxFieldMeshSettings(Thickness=400, VIn=1000, VOut=2000, box=[-10000, 10000, -10000, 10000, -1000, 1000])],
              "Distance": [DistanceFieldMeshSettings(geometry=msh_lines.lines, base_z=0, sampling=100, SizeMin=10, SizeMax=80, DistMin=10, DistMax=30)]},
)
[10]:
mshmkr = GMSHmaker(
    extrude = Volumes,
    surfaces = Surfaces,
    pecs = PECs,
    mesh = mesh,
    save = {"dir": save_dir, "filename": msh_filename},
    open_gmsh = False,
    debug_mode = False
)
on 0: mesh is constructed
on 0: mesh saved
Gmsh generation  |███| 1/1 [100%] in 1.5s (0.66/s)
[11]:
mshmkr.print_physical()
Volume        ID
----------  ----
VACUUM         1
DIELECTRIC     2

 #-----------------------------------

Surface      ID
---------  ----
conductor     3
port1+        4
port1-        5
port2+        6
port2-        7

Below is an example of created geometry and a mesh using GMSH.

04ef77f7c9014b2d894df69fdb449165

[ ]:
### Configuring Palace Simulation
[12]:
from zeroheliumkit.fem.palacer import (PalaceRunner, PalaceConfig, ProblemConfig, ModelConfig,
                                       MaterialsConfig, PostProEnergyConfig, PostProProbeConfig,
                                       DomainConfig, ElementConfig, LumpedPortConfig, BoundaryConfig,
                                       DrivenConfig, EigenConfig, SolverConfig)
[ ]:
### Frequency sweep
[13]:
problem_cfg = ProblemConfig(Type="Driven", Verbose=2, Output="postpro/")
# model_cfg = ModelConfig(Mesh="postpro/geo/cpw.msh", L0=1.0e-6, Refinement={"Tol": 1.0e-2, "MaxIts": 10, "MaxSize": 1e6})
model_cfg = ModelConfig(Mesh="postpro/geo/cpw.msh", L0=1.0e-6, Refinement={})

air = MaterialsConfig(Attributes=[2], Permeability=1.0, Permittivity=1.0, LossTan=0.0)
silicon = MaterialsConfig(Attributes=[1], Permeability=1.0, Permittivity=11, LossTan=0.0)

postpro = {"Energy": [PostProEnergyConfig(Index=1, Attributes=[1])],
          }

domain_cfg = DomainConfig(Materials=[air, silicon], Postprocessing=postpro)

lp1_cfg = LumpedPortConfig(Index=1, R=50.0, Excitation=1, Elements=[ElementConfig([4],"+Y"), ElementConfig([5],"-Y")])
lp2_cfg = LumpedPortConfig(Index=2, R=50.0, Excitation=0, Elements=[ElementConfig([6],"+Y"), ElementConfig([7],"-Y")])

boundary_cfg = BoundaryConfig(PEC={"Attributes": [3]},
                              LumpedPort=[lp1_cfg, lp2_cfg])


f1 = 7.0
f2 = 7.4
df = (f2 - f1)/100

solver_cfg = SolverConfig(Order=2, Device="CPU", Driven=DrivenConfig(MinFreq=f1, MaxFreq=f2, FreqStep=df, Save=[f1,f2], AdaptiveTol=1.0e-3))
[14]:
palace_app_path = "/Users/helium/spack/opt/spack/darwin-m1/palace-0.14.0-2v4weqz6drx675yhbuoyu2nmnqolzf5n/bin/palace"

palace_cfg = PalaceConfig(Problem=problem_cfg,
                          Model=model_cfg,
                          Domains=domain_cfg,
                          Boundaries=boundary_cfg,
                          Solver=solver_cfg)

pal = PalaceRunner(json_name="fsweep", config=palace_cfg, exec_path=palace_app_path)
[15]:
# this has been tested only on macs, windows may have issues with multiprocessing
# json config file is already created, refer to AWS Palace website for more information

pal.run(multicore=8)
tab 1 of window id 13378
[16]:
data = np.loadtxt('postpro/port-S.csv', skiprows=1, delimiter=',')

plt.figure(figsize=(8,3))

ax1 = plt.subplot(121)
ax1.plot(data[:,0], data[:,3], '-o')
ax1.set_xlabel('Frequency (GHz)')
ax1.set_ylabel('S21 Magnitude')

ax2 = plt.subplot(122)
ax2.plot(data[:,0], data[:,4], '-o')
ax2.set_xlabel('Frequency (GHz)')
ax2.set_ylabel('S21 Phase')


plt.tight_layout()
plt.show()
../_images/notebooks_palace_19_0.png
[ ]:
### Eigenfrequency calculations
[36]:
problem_cfg = ProblemConfig(Type="Eigenmode", Verbose=2, Output="postpro/")
model_cfg = ModelConfig(Mesh="postpro/cpw.msh2", L0=1.0e-6, Refinement={"Tol": 1.0e-2, "MaxIts": 10, "MaxSize": 1e6})

air = MaterialsConfig(Attributes=[1], Permeability=1.0, Permittivity=1.0, LossTan=0.0)
silicon = MaterialsConfig(Attributes=[2], Permeability=1.0, Permittivity=11, LossTan=0.0)

postpro = {"Energy": [PostProEnergyConfig(Index=1, Attributes=[1])],
          }

domain_cfg = DomainConfig(Materials=[air, silicon], Postprocessing=postpro)

boundary_cfg = BoundaryConfig(PEC={"Attributes": [3]},
                              LumpedPort=[])


solver_cfg = SolverConfig(Order=2, Device="CPU", Eigenmode=EigenConfig(N=2, Tol=1e-6, Target=6.5, Save=2))
[37]:
palace_cfg = PalaceConfig(Problem=problem_cfg,
                          Model=model_cfg,
                          Domains=domain_cfg,
                          Boundaries=boundary_cfg,
                          Solver=solver_cfg)

pal = PalaceRunner(json_name="eigen", config=palace_cfg, exec_path=palace_app_path)
[6]:
eigen = np.loadtxt('postpro/eig.csv', skiprows=1, delimiter=',')
print("Eigenfrequencies (GHz):")
print(eigen[:,1])
Eigenfrequencies (GHz):
[ 7.35171928 16.86640487 22.08328345 32.6005022  36.81162642]
[38]:
pal.run(multicore=8)
tab 1 of window id 49926

Statistics

Electric Field (first mode at 7.35171928 GHz)

e1bea09237254e88a7b7dbf813f55bc9

000bcd1d8a9a431fb79c4174337f3420