Open ended stub antenna#

An open-ended stub antenna is a simple radiating element consisting of a transmission line (typically microstrip or coaxial cable) with one end open-circuited, allowing electromagnetic energy to radiate directly into free space.

import gmsh
import math

from palacetoolkit.viz import view_mesh
from palacetoolkit.mesh import (
    Entity, 
    run_entity_pipeline, 
    generate_3d_mesh, 
    create_graded_mesh
)
from palacetoolkit.simulation import Simulation

Paramters#

  • 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

  • h : Patch height along z-axis, specified as a scalar in meters.

  • strip_line_length : Notch length along x-axis, specified as a scalar in meters.

  • strip_lined_width: Notch width along y-axis, specified as a scalar in meters.

  • air_height : Air-domain height reference for sizing the enclosing sphere, specified as a scalar in meters.

  • air_margin : Air-domain margin along x and y axes used for sphere sizing, 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
strip_line_length: float = 0.04
strip_line_width: float = 0.002
h: float = 0.0013
air_height: float = 0.025    
air_margin: float = 0.025    
freq: float = 3.3
filename: str = "open_ended_antenna.msh"

wavelength = 3e8 / (freq * 1e9)

Model initialization#

gmsh.initialize()
gmsh.model.add("patch_antenna")
kernel = gmsh.model.occ

Geometry construction#

# Total domain bounds
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

# Substrate box
substrate = kernel.addBox(-l1/2, -w1/2, 0, l1, w1, h)

# Ground plane
ground_plane = kernel.addRectangle(-l1/2, -w1/2, 0, l1, w1)

# Top conductor
strip_line_1 = kernel.addRectangle(-l1/2, -strip_line_width/2, h, strip_line_length, strip_line_width)

# gap bewteen the ground plane and the bottom of the lumped port.
gap = 0

# lumped port
lumped_port = kernel.addRectangle(-l1/2 + gap, -strip_line_width/2, 0, h - gap, strip_line_width)
kernel.rotate([(2, lumped_port)], -l1/2, 0, 0, 0, 1, 0, -math.pi/2)

# Air sphere
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)

# Synchronize everything!
kernel.synchronize()
# Define the entities which later will become the physical groups.
entities = [
    Entity("air_sphere", dim=3, btype="dielectric", mesh_order=2, tags=[air_sphere], eps_r=1.0, mu_r=1.0, loss_tan=0.0),
    Entity("substrate", dim=3, btype="dielectric", mesh_order=1, tags=[substrate], eps_r=2.2, mu_r=1.0, loss_tan=0.0009),
    Entity("top_conductor", dim=2, btype="pec", mesh_order=1, tags=[strip_line_1]),
    Entity("ground_plane", dim=2, btype="pec", mesh_order=1, tags=[ground_plane]),
    Entity("lumped_port", dim=2, btype="lumped_port", mesh_order=0, tags=[lumped_port], R=50.0, direction="+Z")
]

# Boolean operations to guarantee a nice mesh, algo it returns the
# physical group map.
pg_map = run_entity_pipeline(entities)

# Refine near the top conductor and locally the lumped port
create_graded_mesh(  wavelength, 
                     ppw_near=50, 
                     ppw_far=30, 
                     set_as_background=True,
                     local_refinements = {entities[-1].dimtags[0]: 150})
# Mesh sizes
mesh_sizes = {
    "substrate": wavelength / 12,
    "air_sphere": wavelength / 4,
    "lumped_port": wavelength / 150,
    "ground_plane" : wavelength / 10,
    "top_conductor": wavelength / 50
}

# Generate the 3d mesh.
generate_3d_mesh(entities, mesh_sizes, filename, optimize = True)
  Physical group 'air_sphere' (dim=3): pg=1, tags=[2]
  Physical group 'substrate' (dim=3): pg=2, tags=[1]
  Physical group 'top_conductor' (dim=2): pg=3, tags=[8]
  Physical group 'ground_plane' (dim=2): pg=4, tags=[7]
  Physical group 'lumped_port' (dim=2): pg=5, tags=[9]
  Physical group 'air_sphere__None' (dim=2): pg=6, tags=[16]
  Physical group 'air_sphere__substrate' (dim=2): pg=7, tags=[10, 11, 13, 15, 14, 12]
  ignoring 3 curves from {'air_sphere__None'}
  global: 21 curves, SizeMin=0.0018
  ppw_near=50  ppw_far=30
  SizeMax=0.0030  transition=0.0227
  local: dimtag=(2, 9), ppw=150, SizeMin=0.0006
Mesh saved to open_ended_antenna.msh
  Nodes: 30128
  Elements: 178626

Mesh visualization.#

# Render the physical groups. The air sphere is rendered transparent.
view_mesh(filename, transparent_groups= "air_sphere__None")
Loading mesh file: open_ended_antenna.msh
Groups to render transparent: air_sphere__None
Mesh loaded successfully with 2 cell blocks
Found 16412 triangles total
Physical group tags in mesh: {3: 'top_conductor', 4: 'ground_plane', 5: 'lumped_port', 6: 'air_sphere__None', 7: 'air_sphere__substrate'}
../_images/a78d710ddbc7967ac108a0d3c07696871986e9a59a6f5750af697327d5dcc301.png

Generate palace JSON config#

config_filename: str = "op_spalace.conf"
freq_min: float = 3.0
freq_max: float = 3.5
freq_step: float = 0.005
eps_r: float = 2.2
loss_tan: float = 0.0009
port_impedance: float = 50.0
solver_order: int = 2
sim = Simulation()

def attr(name):
    return [pg_map[name]] if name in pg_map else []

sim.set_mesh_file(filename)
sim.set_config_option("Problem.Output", "postpro/open_ended_antenna/")

sim.set_config_option("Domains.Materials", [
    {
        "Attributes": attr("substrate"),
        "Permittivity": eps_r,
        "Permeability": 1.0,
        "LossTan": loss_tan
    },
    {
        "Attributes": attr("air"),
        "Permittivity": 1.0,
        "Permeability": 1.0
    }
])

sim.set_config_option("Boundaries.PEC", {
    "Attributes": attr("ground_plane") + attr("patch")
})

sim.set_config_option("Boundaries.LumpedPort", [
    {
        "Index": 1,
        "Attributes": attr("lumped_port"),
        "R": port_impedance,
        "Excitation": True,
        "Direction": "+Z"
    }
])
sim.set_config_option("Boundaries.Absorbing", {
    "Attributes": attr("farfield"),
    "Order": 1
})

sim.set_config_option("Solver.Order", solver_order)
sim.set_config_option("Solver.Driven.MinFreq", freq_min)
sim.set_config_option("Solver.Driven.MaxFreq", freq_max)
sim.set_config_option("Solver.Driven.FreqStep", freq_step)
sim.set_config_option("Solver.Driven.AdaptiveTol", 0.001)

config_path = sim.write_config(config_filename)
print(f"Wrote {config_path}")
Palace config written to op_spalace.conf
Wrote op_spalace.conf