L shaped antenna simulation.#
In this notebook we will generate the mesh of an L shaped antenna and generate a json for a transient simulation.
import gmsh
import os
import json
from palacetoolkit.viz import view_mesh
from palacetoolkit.mesh import (
Entity,
run_meshing_pipeline,
generate_3d_mesh,
refine_near_surfaces
)
Parameters:#
h : Patch height along z-axis, specified as a scalar in meters.
l1 : Ground plane length along x-axis, specified as a scalar in meters
w1 : Ground plane width along y-axis, specified as a scalar in meters
w3 : Strip line width along y-axis, specified as a scalar in meters.
air_height : Air box height along z-axis, specified as a scalar in meters.
air_margin : Air box margin along x and y axes, specified as a scalar in meters.
freq : Simulation frequency in GHz, specified as a scalar.
filename : Output mesh filename, specified as a string.
l1: float = 0.06
w1: float = 0.06
w3: float = 0.002
h: float = 0.0013
air_height: float = 0.025
air_margin: float = 0.025
freq: float = 3.3
mesh_file: str = "l_antenna.msh"
eps_r: float = 2.2
loss_tan: float = 0.0009
wavelength = 3e8 / (freq * 1e9)
mesh_size = wavelength / 15
Initialize the model#
gmsh.initialize()
gmsh.model.add("patch_antenna")
kernel = gmsh.model.occ
Geometry Construction#
total_xmin = -l1/2 - air_margin
total_xmax = l1/2 + air_margin
total_ymin = -w1/2 - air_margin
total_ymax = w1/2 + air_margin
total_zmax = h + air_height
# 1. Create volumes
substrate = kernel.addBox(-l1/2, -w1/2, 0, l1, w1, h)
airsphere_radius = max(abs(total_xmin), abs(total_xmax), abs(total_ymin), abs(total_ymax), total_zmax)
air_sphere = kernel.addSphere(0.0, 0.0, 0.0, airsphere_radius)
kernel.synchronize()
# 2. Create 2D surfaces (ground, patch, ports)
ground_plane = kernel.addRectangle(-l1/2, -w1/2, 0, l1, w1)
strip_length_x = l1/2
strip_length_y = w1/2 + w3/2
feed_line_x = kernel.addRectangle(-l1/2, -w3/2, h, strip_length_x, w3)
feed_line_y = kernel.addRectangle(0, -w3/2, h, w3, strip_length_y)
top_conductor, top_map = kernel.fuse(
[(2, feed_line_x)], [(2, feed_line_y)],
removeObject=True, removeTool=True
)
kernel.synchronize()
# Lumped ports - build directly in-plane, no rotation
# Port 1: at x = -l1/2, vertical face
p1 = kernel.addPoint(-l1/2, -w3/2, 0)
p2 = kernel.addPoint(-l1/2, w3/2, 0)
p3 = kernel.addPoint(-l1/2, w3/2, h)
p4 = kernel.addPoint(-l1/2, -w3/2, h)
lp1_a = kernel.addLine(p1, p2)
lp1_b = kernel.addLine(p2, p3)
lp1_c = kernel.addLine(p3, p4)
lp1_d = kernel.addLine(p4, p1)
loop1 = kernel.addCurveLoop([lp1_a, lp1_b, lp1_c, lp1_d])
lumped_port_1 = kernel.addPlaneSurface([loop1])
kernel.synchronize()
# Port 2: at y = w1/2, vertical face
p5 = kernel.addPoint(0, w1/2, 0)
p6 = kernel.addPoint(w3, w1/2, 0)
p7 = kernel.addPoint(w3, w1/2, h)
p8 = kernel.addPoint(0, w1/2, h)
lp2_a = kernel.addLine(p5, p6)
lp2_b = kernel.addLine(p6, p7)
lp2_c = kernel.addLine(p7, p8)
lp2_d = kernel.addLine(p8, p5)
loop2 = kernel.addCurveLoop([lp2_a, lp2_b, lp2_c, lp2_d])
lumped_port_2 = kernel.addPlaneSurface([loop2])
kernel.synchronize()
# Define the entities which will be the physical groups.
entities = [
Entity("substrate", dim=3, btype="dielectric", mesh_order=1, tags=[substrate], loss_tan=loss_tan, eps_r=eps_r, mu_r=1.0),
Entity("air_sphere", dim=3, btype="dielectric", mesh_order=2, tags=[air_sphere], loss_tan=0.0, eps_r=1.0, mu_r=1.0),
Entity("top_conductor", dim=2, btype="pec", mesh_order=1, tags=[top_conductor[0][1]]),
Entity("ground_plane", dim=2, btype="pec", mesh_order=1, tags=[ground_plane]),
Entity("lumped_port_1", dim=2, btype="lumped_port", mesh_order=0, tags=[lumped_port_1], R=50.0, direction="+Z", excitation=True),
Entity("lumped_port_2", dim=2, btype="lumped_port", mesh_order=0, tags=[lumped_port_2], R=50.0, direction="+Z", excitation=False),
]
Info : Cannot bind existing OpenCASCADE surface 9 to second tag 10
Info : Could not preserve tag of 2D object 10 (->9)
pg_map = run_meshing_pipeline(entities)
lumped_port_1 = entities[-1].dimtags[0]
lumped_port_2 = entities[-2].dimtags[0]
# Refine near the top conductor and also locally refine the ports.
refine_near_surfaces(entities[2].dimtags,
wavelength,
ppw_near=100,
ppw_far=15,
set_as_background=True,
local_refinements = {lumped_port_1: 100, lumped_port_2 : 100})
mesh_sizes = {
"substrate": wavelength / 12,
"air_sphere": wavelength / 4,
"lumped_port_1": wavelength / 18,
"lumped_port_2": wavelength / 18,
"top_conductor": wavelength /10
}
generate_3d_mesh(entities, mesh_sizes, mesh_file, optimize=True, verbose=False)
gmsh.finalize()
Physical group 'substrate' (dim=3): pg=1, tags=[1]
Physical group 'air_sphere' (dim=3): pg=2, tags=[2]
Physical group 'top_conductor' (dim=2): pg=3, tags=[9]
Physical group 'ground_plane' (dim=2): pg=4, tags=[8]
Physical group 'lumped_port_1' (dim=2): pg=5, tags=[10]
Physical group 'lumped_port_2' (dim=2): pg=6, tags=[11]
Physical group 'air_sphere__substrate' (dim=2): pg=7, tags=[12, 13, 14, 15, 16, 17, 18, 19]
Physical group 'air_sphere__None' (dim=2): pg=8, tags=[20]
ppw_near=100 ppw_far=15
SizeMax=0.0061 transition=0.0227
global: 6 curves, SizeMin=0.0009
local (2, 11): 4 curves, SizeMin=0.0009
local (2, 10): 4 curves, SizeMin=0.0009
Merged 3 fields with Min → field 7
view_mesh(mesh_file, transparent_groups= "air_sphere__None")
Loading mesh file: l_antenna.msh
Groups to render transparent: air_sphere__None
Mesh loaded successfully with 2 cell blocks
Found 5922 triangles total
Physical group tags in mesh: {3: 'top_conductor', 4: 'ground_plane', 5: 'lumped_port_1', 6: 'lumped_port_2', 7: 'air_sphere__substrate', 8: 'air_sphere__None'}
Simulation Configuration#
We define the key parameters for the electromagnetic simulation here. These settings control the frequency sweep range, material properties (dielectric constant and loss tangent for the substrate), and solver-specific configurations like port impedance and mesh order.
output_file : output filename for the configuration JSON file
freq : frequency for the simulation (GHz)
eps_r: relative permittivity of the substrate
loss_tan: loss tangent of the substrate
port_impedance: characteristic impedance of the lumped port (Ohms)
solver_order: order of the finite element basis functions for the simulation (e.g., 1 for linear, 2 for quadratic)
output_file_transient: str = "l_antenna_transient.json"
output_file_driven: str = "l_antenna_driven.json"
freq: float = 3.16
eps_r: float = 2.2
loss_tan: float = 0.0009
port_impedance: float = 50.0
solver_order: int = 2
import numpy as np
eps_0 = 8.8541878128e-12
sigma = 2 * np.pi * freq * eps_0 * eps_r * loss_tan
Generating the Palace Configuration File#
Finally, we assemble the simulation parameters into two JSON configuration. One for a transient simulation and one for a driven where we check the s-parameters.
def attr(name):
return [pg_map[name]] if name in pg_map else []
config = {
"Problem": {
"Type": "Transient",
"Verbose": 2,
"Output": "/work/results_transient/l_antenna/"
},
"Model": {
"Mesh": f"/work/{mesh_file}",
"L0": 1.0,
"Refinement": {}
},
"Domains": {
"Materials": [
{
"Attributes": attr("substrate"),
"Permittivity": eps_r,
"Permeability": 1.0,
"Conductivity": sigma # Replaced LossTan
},
{
"Attributes": attr("air"),
"Permittivity": 1.0,
"Permeability": 1.0
}
]
},
"Boundaries": {
"PEC": {
"Attributes": attr("ground_plane") + attr("patch")
},
"LumpedPort": [
{
"Index": 1,
"Attributes": attr("lumped_port_1"),
"R": port_impedance,
"Excitation": True,
"Direction": [0.0, 0.0, 1.0]
},
{
"Index": 2,
"Attributes": attr("lumped_port_2"),
"R": port_impedance,
"Excitation": False,
"Direction": [0.0, 0.0, 1.0]
}
],
"Absorbing": {
"Attributes": attr("farfield"),
"Order": 1
}
},
"Solver": {
"Order": solver_order,
"Device": "CPU",
"Transient": {
"Type": "GeneralizedAlpha",
"Excitation": "ModulatedGaussian",
"ExcitationFreq": freq,
"ExcitationWidth": 0.05,
"MaxTime": 1.0,
"TimeStep": 0.005,
"SaveStep": 10
},
"Linear": {
"Type": "AMS",
"KSPType": "CG",
"Tol": 1.0e-8,
"MaxIts": 100
}
}
}
script_dir = os.getcwd()
config_path = os.path.join(script_dir, output_file_transient)
with open(config_path, "w") as f:
json.dump(config, f, indent=2)
print(f"Palace config written to {config_path}")
Palace config written to /home/martin/Desktop/PalaceToolkit/docs/examples/l_antenna_transient.json
config = {
"Problem": {
"Type": "Driven",
"Verbose": 2,
"Output": "/work/results_driven/l_antenna/"
},
"Model": {
"Mesh": f"/work/{mesh_file}",
"L0": 1.0,
"Refinement": {}
},
"Domains": {
"Materials": [
{
"Attributes": attr("substrate"),
"Permittivity": eps_r,
"Permeability": 1.0,
"Conductivity": loss_tan
},
{
"Attributes": attr("air"),
"Permittivity": 1.0,
"Permeability": 1.0
}
]
},
"Boundaries": {
"PEC": {
"Attributes": attr("ground_plane") + attr("patch")
},
"LumpedPort": [
{
"Index": 1,
"Attributes": attr("lumped_port_1"),
"R": port_impedance,
"Excitation": True,
"Direction": [0.0, 0.0, 1.0]
},
{
"Index": 2,
"Attributes": attr("lumped_port_2"),
"R": port_impedance,
"Excitation": False,
"Direction": [0.0, 0.0, 1.0]
}
],
"Absorbing": {
"Attributes": attr("farfield"),
"Order": 1
}
},
"Solver": {
"Order": 2,
"Device": "CPU",
"Driven": {
"MinFreq": 3.0,
"MaxFreq": 3.5,
"FreqStep": 0.1,
"SaveStep": 1,
"AdaptiveTol": 0.0001
},
"Linear": {
"Type": "Default",
"KSPType": "GMRES",
"Tol": 1e-08,
"MaxIts": 300,
"MaxSize": 1000,
"ComplexCoarseSolve": True
}
}
}
script_dir = os.getcwd()
config_path = os.path.join(script_dir, output_file_driven)
with open(config_path, "w") as f:
json.dump(config, f, indent=2)
print(f"Palace config written to {config_path}")
Palace config written to /home/martin/Desktop/PalaceToolkit/docs/examples/l_antenna_driven.json