Coax to Waveguide Transition
Coax to waveguide¶
This notebook outlines the design and simulation workflow for a horn antenna fed by a standard WR-90 waveguide operating at a target frequency of 10 GHz (λ≈30mm)
Design Parameters¶
The antenna geometry and probe feed are defined based on the following physical constraints:
Waveguide (WR-90):
- 22.86 mm (width)
- 10.16 mm (height)
- 40 mm (length)
Probe Feed:
- Probe Length: 5.89 mm (≈λ/5) to ensure optimal impedance matching.
- Coaxial Geometry: 3.6 mm (outer diameter) and 1.28 mm (inner diameter).
These parameters serve as the foundation for our geometric construction and subsequent discretization, ensuring the antenna is tuned for its intended frequency of operation.
import gmsh
import math
import os
import json
from palacetoolkit.viz import run_with_scrollable_output, view_mesh
from palacetoolkit.geometry import extract_tag, xmin, xmax, ymin, ymax, zmin, zmax
from palacetoolkit.mesh import refine_near_surfaces
Parameters:¶
- inner_diameter_mm: Inner diameter of the coaxial cable in millimeters.
- outer_diameter_mm: Outer diameter of the coaxial cable in millimeters.
- probe_length_mm : Length of the probe in milimiters.
- length_mm: Length of the coaxial cable section in millimeters.
- wg_height: Height of the rectangular waveguide in millimeters.
- wg_length: Length of the rectangular waveguide in millimeters.
- wg_width: Width of the rectangular waveguide in millimeters.
- freq_ghz: Frequency of the wave in gigahertz.
- verbose: Verbosity level for Gmsh output
- filename: Name of the output mesh file.
inner_diameter_mm: float = 1.28
outer_diameter_mm: float = 3.6
probe_length_mm = 5.89
length_mm: float = 5
wg_height: float = 10.16
wg_length: float = 40
wg_width: float = 22.86
freq_ghz: float = 10.0
verbose: int = 2
filename: str = "coax_to_waveguide.msh"
mesh_order = 1
gui = False
order = 1
Initializing the Modeling Environment¶
gmsh.initialize()
gmsh.option.setNumber("General.Verbosity", 5)
gmsh.model.add("coax_to_waveguide")
kernel = gmsh.model.occ
Design Parameter Calculation¶
We calculate the operating wavelength (λ) and derive the critical dimensions for the coaxial probe and backshort position. By basing these values on the 10 GHz design frequency, we ensure the geometry is physically optimized for efficient power transfer within the waveguide structure.
c = 3e8
lamda = (c / (freq_ghz * 1e9)) * 1e3
inner_radius = inner_diameter_mm / 2
outer_radius = outer_diameter_mm / 2
# y_top is the y-coordinate of the center of the coax probe,
# which is typically placed at the center of the waveguide height
y_top = wg_height / 2
coax_y_max = y_top + length_mm
# backshort_z is the distance from the probe tip to the backshort plane,
# which is typically around λ/5 for good impedance matching
backshort_z = lamda / 5
print(f"Wavelength: {lamda:.2f} mm")
print(f"Probe length: {probe_length_mm:.2f} mm")
print(f"Backshort Z position: {backshort_z:.2f} mm")
Wavelength: 30.00 mm Probe length: 5.89 mm Backshort Z position: 6.00 mm
Geometry Construction and Boolean Operations¶
In this step, we construct the CAD model by defining the waveguide box and the coaxial feed components. We use Boolean cut operations to integrate the probe into the waveguide and define the dielectric region. Finally, we perform a global fragment operation to ensure all geometric volumes are properly connected, forming a unified topology ready for discretization.
# Waveguide box
waveguide = kernel.addBox(
-wg_width/2, -wg_height/2, 0,
wg_width, wg_height, wg_length
)
# Outer coax cylinder
outer_cyl = kernel.addCylinder(0, y_top, backshort_z,
0, length_mm, 0, outer_radius)
# Inner conductor
inner_cyl = kernel.addCylinder(0, coax_y_max, backshort_z,
0, -(length_mm + probe_length_mm), 0, inner_radius)
kernel.synchronize()
coax_dielectric, _ = kernel.cut([(3, outer_cyl)], [(3, inner_cyl)],
removeObject=True, removeTool=False)
waveguide_cut, _ = kernel.cut([(3, waveguide)], [(3, inner_cyl)],
removeObject=True, removeTool= True)
kernel.synchronize()
all_volumes = gmsh.model.getEntities(3)
kernel.fragment(all_volumes, [])
kernel.synchronize()
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 - Making faces Info : [ 90%] Difference - Classify solids Info : [ 0%] Difference Info : [ 10%] Difference Info : [ 20%] Difference Info : [ 30%] Difference Info : [ 70%] Difference Info : [ 80%] Difference Info : [ 90%] Difference Info : [ 0%] Fragments Info : [ 10%] Fragments Info : [ 20%] Fragments Info : [ 30%] Fragments Info : [ 40%] Fragments Info : [ 50%] Fragments Info : [ 60%] Fragments Info : [ 70%] Fragments Info : [ 80%] Fragments - Adding holes Info : [ 90%] Fragments
Geometric Entity Classification¶
Here, we categorize the fragmented CAD entities into functional groups based on their spatial coordinates. We define logical filters to isolate the waveguide walls, coaxial conductors, waveports, and internal volumes. This classification step is essential for mapping the geometry to the specific physical boundary conditions required by the Palace solver
all_2d_entities = gmsh.model.getEntities(2)
all_3d_entities = gmsh.model.getEntities(3)
def is_interface(x):
return (math.isclose(ymin(x), y_top, abs_tol=1e-4) and
math.isclose(ymax(x), y_top, abs_tol=1e-4) and
math.isclose(xmax(x), outer_radius, abs_tol=1e-4))
def is_coax_port(x):
return (math.isclose(ymin(x), coax_y_max, abs_tol=1e-4) and
math.isclose(ymax(x), coax_y_max, abs_tol=1e-4) and
math.isclose(xmax(x), outer_radius, abs_tol=1e-4))
def is_inner_conductor(x):
return (math.isclose(xmax(x), inner_radius, abs_tol=1e-4) and
not is_interface(x))
def is_outer_conductor(x):
return (math.isclose(xmax(x), outer_radius, abs_tol=1e-4) and
ymin(x) >= y_top - 1e-4 and
not is_coax_port(x) and
not is_interface(x))
def is_waveport(x):
return( math.isclose(zmax(x), wg_length, abs_tol=1e-4) and
math.isclose(zmin(x), wg_length, abs_tol=1e-4))
def is_waveguide_wall(x):
return (not is_coax_port(x) and
not is_inner_conductor(x) and
not is_outer_conductor(x) and
not is_waveport(x) and
not is_interface(x))
def is_coax_volume(x):
return ymin(x) >= y_top - 1e-4
def is_waveguide_volume(x):
return ymax(x) <= y_top + 1e-4
coax_port_surfs = [x for x in all_2d_entities if is_coax_port(x)]
inner_cond_surfs = [x for x in all_2d_entities if is_inner_conductor(x)]
outer_cond_surfs = [x for x in all_2d_entities if is_outer_conductor(x)]
waveguide_wall_surfs = [x for x in all_2d_entities if is_waveguide_wall(x)]
waveport_surfs = [x for x in all_2d_entities if is_waveport(x)]
coax_vols = [x for x in all_3d_entities if is_coax_volume(x)]
waveguide_vols = [x for x in all_3d_entities if is_waveguide_volume(x)]
Physical Group Assignment¶
In this block, we map the classified geometric entities to Physical Groups. Assigning these unique identifiers allows the Palace solver to distinguish between different materials and boundary conditions, such as the waveguide walls (PEC), the coaxial probe, and the internal air and dielectric volumes. Finally, we store these identifiers in a pg_map dictionary for streamlined reference during the simulation configuration.
pg_coax_port = gmsh.model.addPhysicalGroup(2, [x[1] for x in coax_port_surfs], -1, "coax_port")
pg_probe = gmsh.model.addPhysicalGroup(2, [x[1] for x in inner_cond_surfs], -1, "inner_conductor")
pg_outer_cyl = gmsh.model.addPhysicalGroup(2, [x[1] for x in outer_cond_surfs], -1, "outer_conductor")
pg_waveguide = gmsh.model.addPhysicalGroup(2, [x[1] for x in waveguide_wall_surfs], -1, "waveguide_walls")
pg_waveport = gmsh.model.addPhysicalGroup(2, [x[1] for x in waveport_surfs], -1, "waveport")
pg_dieletric = gmsh.model.addPhysicalGroup(3, [x[1] for x in coax_vols], -1, "coax_volume")
pg_air = gmsh.model.addPhysicalGroup(3, [x[1] for x in waveguide_vols], -1, "waveguide_volume")
pg_map = {
"coax_port": pg_coax_port,
"waveport": pg_waveport,
"waveguide_surface": pg_waveguide,
"waveguide_volume": pg_air,
"probe": pg_probe,
"outer_cyl": pg_outer_cyl,
"dielectric": pg_dieletric
}
Mesh Generation, Refinement, and Export¶
We conclude the workflow by applying a graded mesh refinement strategy, concentrating element density near the coaxial feed and waveport to accurately resolve the high-frequency electromagnetic field gradients.
def _generate_coax_to_waveguide_mesh():
refine_near_surfaces([(2, extract_tag(x)) for x in inner_cond_surfs + outer_cond_surfs + waveport_surfs + coax_port_surfs],
wavelength= lamda,
ppw_near = 50,
ppw_far = 20,
transition_distance= lamda / 8,
)
gmsh.model.mesh.generate(3)
gmsh.model.mesh.setOrder(order)
gmsh.model.mesh.optimize("Netgen")
gmsh.option.setNumber("Mesh.MshFileVersion", 2.2)
gmsh.option.setNumber("Mesh.Binary", 0)
script_dir = os.getcwd()
output_path = os.path.join(script_dir, filename)
gmsh.write(output_path)
run_with_scrollable_output(_generate_coax_to_waveguide_mesh, title="Coax-to-waveguide mesh generation", max_lines=10)
if gui:
gmsh.fltk.run()
gmsh.finalize()
ppw_near=50 ppw_far=20 SizeMax=1.5000 transition=3.7500 global: 12 curves, SizeMin=0.6000
import pyvista as pv
pv.set_jupyter_backend("trame")
# Visualize the mesh in the notebook.
if order == 1:
view_mesh(filename)
Loading mesh file: coax_to_waveguide.msh
Groups to render transparent: ['air_none', 'air_plastic_enclosure']
Mesh loaded successfully with 2 cell blocks
Found 4928 triangles total
Physical group tags in mesh: {1: 'coax_port', 2: 'inner_conductor', 3: 'outer_conductor', 4: 'waveguide_walls', 5: 'waveport'}
Generating the Palace Configuration File¶
Finally, we assemble the simulation parameters into a JSON configuration file. This file serves as the definitive input for Palace.
palace_config = {
"Problem": {
"Type": "Driven",
"Verbose": 2,
"Output": "/work/postpro/coax2waveguide"
},
"Model": {
"Mesh": f"/work/coax_to_waveguide.msh",
"L0": 1e-3,
"Refinement": {}
},
"Domains": {
"Materials": [
{
"Attributes": [pg_map["waveguide_volume"]],
"Permittivity": 1.0,
"Permeability": 1.0
},
{
"Attributes": [pg_map["dielectric"]],
"Permittivity": 2.2,
"Permeability": 1.0
}
]
},
"Boundaries": {
"PEC": {
"Attributes": [pg_map["waveguide_surface"]] + [pg_map["outer_cyl"]] + [pg_map["probe"]]
},
"WavePort": [
{
"Index": 2,
"Attributes": [pg_map["waveport"]],
"Mode": 1,
"Offset": 0.0
},
{
"Index": 1,
"Attributes": [pg_map["coax_port"]],
"Mode": 1,
"Offset": 0.0,
"Excitation": True
}
]
},
"Solver": {
"Order": 2,
"Device": "CPU",
"Driven": {
"MinFreq": 6.0,
"MaxFreq": 12.0,
"FreqStep": 0.1,
"SaveStep": 2,
"AdaptiveTol": 0.001
},
"Linear": {
"Type": "Default",
"KSPType": "GMRES",
"Tol": 1e-08,
"MaxIts": 200,
"ComplexCoarseSolve": True
}
}
}
with open("coax_to_waveguide.json", 'w') as json_file:
json.dump(palace_config, json_file, indent=4)
print(f"Palace config successfully generated: coax_to_waveguide.json")
Palace config successfully generated: coax_to_waveguide.json