L Antenna
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.
In [1]:
Copied!
import gmsh
import os
import json
from palacetoolkit.viz import run_with_scrollable_output, view_mesh
from palacetoolkit.mesh import (
Entity,
run_meshing_pipeline,
generate_3d_mesh,
refine_near_surfaces
)
import gmsh
import os
import json
from palacetoolkit.viz import run_with_scrollable_output, 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.
In [2]:
Copied!
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
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¶
In [3]:
Copied!
gmsh.initialize()
gmsh.model.add("patch_antenna")
kernel = gmsh.model.occ
gmsh.initialize()
gmsh.model.add("patch_antenna")
kernel = gmsh.model.occ
Geometry Construction¶
In [4]:
Copied!
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)
air_box = kernel.addBox(total_xmin, total_ymin, 0,
total_xmax - total_xmin,
total_ymax - total_ymin,
total_zmax)
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, mesh_order = 1, tags = [substrate]),
Entity("air_box", dim = 3, mesh_order = 2, tags = [air_box]),
Entity("top_conductor", dim = 2, mesh_order = 1, tags = [top_conductor[0][1]]),
Entity("ground_plane", dim = 2, mesh_order = 1, tags = [ground_plane]),
Entity("lumped_port_1", dim = 2, mesh_order = 0, tags = [lumped_port_1]),
Entity("lumped_port_2", dim = 2, mesh_order = 0, tags = [lumped_port_2])
]
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)
air_box = kernel.addBox(total_xmin, total_ymin, 0,
total_xmax - total_xmin,
total_ymax - total_ymin,
total_zmax)
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, mesh_order = 1, tags = [substrate]),
Entity("air_box", dim = 3, mesh_order = 2, tags = [air_box]),
Entity("top_conductor", dim = 2, mesh_order = 1, tags = [top_conductor[0][1]]),
Entity("ground_plane", dim = 2, mesh_order = 1, tags = [ground_plane]),
Entity("lumped_port_1", dim = 2, mesh_order = 0, tags = [lumped_port_1]),
Entity("lumped_port_2", dim = 2, mesh_order = 0, tags = [lumped_port_2])
]
Info : [ 0%] Union Info : [ 10%] Union Info : [ 20%] Union - Performing Vertex-Face intersection Info : [ 30%] Union Info : [ 40%] Union Info : [ 50%] Union Info : [ 60%] Union Info : [ 70%] Union - Filling splits of vertices Info : [ 80%] Union - Splitting faces Info : Cannot bind existing OpenCASCADE surface 14 to second tag 15 Info : Could not preserve tag of 2D object 15 (->14)
In [5]:
Copied!
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_box": wavelength / 4,
"lumped_port_1": wavelength / 18,
"lumped_port_2": wavelength / 18,
"top_conductor": wavelength /10
}
def _generate_l_antenna_mesh():
generate_3d_mesh(entities, mesh_sizes, mesh_file, optimize=True)
gmsh.option.setNumber("Mesh.MshFileVersion", 2.2)
gmsh.write(mesh_file)
run_with_scrollable_output(_generate_l_antenna_mesh, title="L antenna mesh generation", max_lines=10)
gmsh.finalize()
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_box": wavelength / 4,
"lumped_port_1": wavelength / 18,
"lumped_port_2": wavelength / 18,
"top_conductor": wavelength /10
}
def _generate_l_antenna_mesh():
generate_3d_mesh(entities, mesh_sizes, mesh_file, optimize=True)
gmsh.option.setNumber("Mesh.MshFileVersion", 2.2)
gmsh.write(mesh_file)
run_with_scrollable_output(_generate_l_antenna_mesh, title="L antenna mesh generation", max_lines=10)
gmsh.finalize()
Info : [ 0%] Difference Info : [ 10%] Difference Info : [ 20%] Difference Info : [ 30%] Difference Info : [ 40%] Difference Info : [ 50%] Difference Info : [ 60%] Difference Info : [ 70%] Difference - Filling splits of edges Info : [ 80%] Difference - Adding holes Info : [ 90%] Difference Info : [ 0%] Difference Info : [ 10%] Difference - Performing intersection of shapes Info : [ 80%] Difference - Building splits of containers Info : [ 0%] Difference Info : [ 10%] Difference Info : [ 20%] Difference Info : [ 30%] Difference Info : [ 40%] Difference Info : [ 50%] Difference Info : [ 60%] Difference Info : [ 70%] Difference - Filling splits of vertices Info : [ 80%] Difference - Splitting faces Info : [ 0%] Difference Info : [ 10%] Difference Info : [ 20%] Difference - Performing Vertex-Face intersection Info : [ 30%] Difference Info : [ 40%] Difference Info : [ 50%] Difference Info : [ 60%] Difference Info : [ 70%] Difference Info : [ 80%] Difference - Splitting faces Info : [ 0%] Fragments Info : [ 10%] Fragments Info : [ 20%] Fragments Info : [ 30%] Fragments Info : [ 40%] Fragments Info : [ 50%] Fragments Info : [ 60%] Fragments Info : [ 70%] Fragments - Filling splits of vertices Info : [ 80%] Fragments Info : [ 90%] Fragments - Looking for internal shapes Physical group 'substrate' (dim=3): pg=1, tags=[1] Physical group 'air_box' (dim=3): pg=2, tags=[2] Physical group 'top_conductor' (dim=2): pg=3, tags=[14] Physical group 'ground_plane' (dim=2): pg=4, tags=[13] Physical group 'lumped_port_1' (dim=2): pg=5, tags=[15] Physical group 'lumped_port_2' (dim=2): pg=6, tags=[16] Physical group 'air_box__substrate' (dim=2): pg=7, tags=[17, 18, 19, 20, 21, 22, 23, 24] Physical group 'air_box__None' (dim=2): pg=8, tags=[25, 26, 27, 28, 29, 30] ppw_near=100 ppw_far=15 SizeMax=0.0061 transition=0.0227 global: 6 curves, SizeMin=0.0009 local (2, 16): 4 curves, SizeMin=0.0009 local (2, 15): 4 curves, SizeMin=0.0009 Merged 3 fields with Min → field 7
Mesh saved to l_antenna.msh Nodes: 4815 Elements: 28619
In [6]:
Copied!
view_mesh(mesh_file, transparent_groups= "air_box__None")
view_mesh(mesh_file, transparent_groups= "air_box__None")
Loading mesh file: l_antenna.msh
Groups to render transparent: air_box__None
Mesh loaded successfully with 2 cell blocks
Found 5730 triangles total
Physical group tags in mesh: {3: 'top_conductor', 4: 'ground_plane', 5: 'lumped_port_1', 6: 'lumped_port_2', 7: 'air_box__substrate', 8: 'air_box__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)
In [7]:
Copied!
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
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.
In [8]:
Copied!
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}")
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
In [9]:
Copied!
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}")
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