diff --git a/nbs/_palace_cpw.ipynb b/nbs/_palace_cpw.ipynb index 215f16a3..69d64790 100644 --- a/nbs/_palace_cpw.ipynb +++ b/nbs/_palace_cpw.ipynb @@ -181,9 +181,14 @@ "sim_lumped.set_geometry(c)\n", "\n", "# Configure layer stack from active PDK\n", - "stack = get_stack(air_above=100.0, air_below=100.0) # auto-detects active PDK\n", + "stack = get_stack() # auto-detects active PDK\n", "sim_lumped.set_stack(stack)\n", "\n", + "# Surround the design with air. Without this the absorbing boundary sits\n", + "# directly on the passivation, microns above the metal, and adds large\n", + "# spurious loss and reflection.\n", + "sim_lumped.set_airbox(margin_x=50, margin_y=50, z_above=100, z_below=100)\n", + "\n", "# Configure left CPW port (single port at signal center)\n", "sim_lumped.add_cpw_port(\n", " \"o1\",\n", @@ -239,6 +244,7 @@ "\n", "# Reuse the same stack from active PDK\n", "sim_waveport.set_stack(stack)\n", + "sim_waveport.set_airbox(margin_x=50, margin_y=50, z_above=100, z_below=100)\n", "\n", "# Configure left CPW port (single port at signal center)\n", "sim_waveport.add_wave_port(\"o1\", layer=\"topmetal2\", max_size=True, mode=1, excited=True)\n", diff --git a/nbs/_palace_cpw.py b/nbs/_palace_cpw.py index 09ec0d9c..d258b3b9 100644 --- a/nbs/_palace_cpw.py +++ b/nbs/_palace_cpw.py @@ -108,9 +108,14 @@ def gsg_electrode( sim_lumped.set_geometry(c) # Configure layer stack from active PDK -stack = get_stack(air_above=100.0, air_below=100.0) # auto-detects active PDK +stack = get_stack() # auto-detects active PDK sim_lumped.set_stack(stack) +# Surround the design with air. Without this the absorbing boundary sits +# directly on the passivation, microns above the metal, and adds large +# spurious loss and reflection. +sim_lumped.set_airbox(margin_x=50, margin_y=50, z_above=100, z_below=100) + # Configure left CPW port (single port at signal center) sim_lumped.add_cpw_port( "o1", @@ -154,6 +159,7 @@ def gsg_electrode( # Reuse the same stack from active PDK sim_waveport.set_stack(stack) +sim_waveport.set_airbox(margin_x=50, margin_y=50, z_above=100, z_below=100) # Configure left CPW port (single port at signal center) sim_waveport.add_wave_port("o1", layer="topmetal2", max_size=True, mode=1, excited=True) diff --git a/nbs/palace_branch_line_coupler.ipynb b/nbs/palace_branch_line_coupler.ipynb index 075e233c..4ca2a730 100644 --- a/nbs/palace_branch_line_coupler.ipynb +++ b/nbs/palace_branch_line_coupler.ipynb @@ -317,9 +317,14 @@ "sim.set_geometry(c)\n", "\n", "# Configure layer stack from active PDK\n", - "stack = get_stack(air_above=300.0) # auto-detects active PDK\n", + "stack = get_stack() # auto-detects active PDK\n", "sim.set_stack(stack)\n", "\n", + "# Surround the design with air. Without this the absorbing boundary sits\n", + "# directly on the passivation, microns above the metal, and adds large\n", + "# spurious loss and reflection.\n", + "sim.set_airbox(margin_x=50, margin_y=50, z_above=100, z_below=100)\n", + "\n", "# Configure via ports (Metal3 ground plane to TopMetal2 signal)\n", "for port in c.ports:\n", " sim.add_port(port.name, from_layer=\"metal3\", to_layer=\"topmetal2\", geometry=\"via\")\n", diff --git a/nbs/palace_branch_line_coupler.py b/nbs/palace_branch_line_coupler.py index a1f76966..6069c028 100644 --- a/nbs/palace_branch_line_coupler.py +++ b/nbs/palace_branch_line_coupler.py @@ -233,9 +233,14 @@ def branch_line_coupler( sim.set_geometry(c) # Configure layer stack from active PDK -stack = get_stack(air_above=300.0) # auto-detects active PDK +stack = get_stack() # auto-detects active PDK sim.set_stack(stack) +# Surround the design with air. Without this the absorbing boundary sits +# directly on the passivation, microns above the metal, and adds large +# spurious loss and reflection. +sim.set_airbox(margin_x=50, margin_y=50, z_above=100, z_below=100) + # Configure via ports (Metal3 ground plane to TopMetal2 signal) for port in c.ports: sim.add_port(port.name, from_layer="metal3", to_layer="topmetal2", geometry="via") diff --git a/nbs/palace_cpw_lumped.ipynb b/nbs/palace_cpw_lumped.ipynb index 8d0e8f3d..c0ad37d8 100644 --- a/nbs/palace_cpw_lumped.ipynb +++ b/nbs/palace_cpw_lumped.ipynb @@ -189,7 +189,7 @@ "sim.set_output_dir(\"./palace-sim-cpw-lumped\")\n", "sim.set_geometry(c)\n", "\n", - "stack = get_stack(air_above=100.0, air_below=100.0) # auto-detects active PDK\n", + "stack = get_stack() # auto-detects active PDK; air comes from set_airbox() below\n", "sim.set_stack(stack)\n", "\n", "# CPW lumped ports — offset defaults to length/2 (flush with conductor edge)\n", diff --git a/nbs/palace_cpw_lumped.py b/nbs/palace_cpw_lumped.py index 72c4ce5d..2bae773d 100644 --- a/nbs/palace_cpw_lumped.py +++ b/nbs/palace_cpw_lumped.py @@ -98,7 +98,7 @@ def gsg_electrode( sim.set_output_dir("./palace-sim-cpw-lumped") sim.set_geometry(c) -stack = get_stack(air_above=100.0, air_below=100.0) # auto-detects active PDK +stack = get_stack() # auto-detects active PDK; air comes from set_airbox() below sim.set_stack(stack) # CPW lumped ports — offset defaults to length/2 (flush with conductor edge) diff --git a/nbs/palace_cpw_via.ipynb b/nbs/palace_cpw_via.ipynb index 3b9a2463..6b9c77e9 100644 --- a/nbs/palace_cpw_via.ipynb +++ b/nbs/palace_cpw_via.ipynb @@ -323,9 +323,14 @@ "sim.set_geometry(c)\n", "\n", "# Configure layer stack from active PDK\n", - "stack = get_stack(air_above=300.0) # auto-detects active PDK\n", + "stack = get_stack() # auto-detects active PDK\n", "sim.set_stack(stack)\n", "\n", + "# Surround the design with air. Without this the absorbing boundary sits\n", + "# directly on the passivation, microns above the metal, and adds large\n", + "# spurious loss and reflection.\n", + "sim.set_airbox(margin_x=50, margin_y=50, z_above=100, z_below=100)\n", + "\n", "# Configure left CPW port (single port at signal center)\n", "sim.add_cpw_port(\"o1\", layer=\"topmetal2\", s_width=20, gap_width=15)\n", "\n", diff --git a/nbs/palace_cpw_via.py b/nbs/palace_cpw_via.py index b429d1d4..f78425fa 100644 --- a/nbs/palace_cpw_via.py +++ b/nbs/palace_cpw_via.py @@ -239,9 +239,14 @@ def gsg_electrode_tm2_tm1_m5( sim.set_geometry(c) # Configure layer stack from active PDK -stack = get_stack(air_above=300.0) # auto-detects active PDK +stack = get_stack() # auto-detects active PDK sim.set_stack(stack) +# Surround the design with air. Without this the absorbing boundary sits +# directly on the passivation, microns above the metal, and adds large +# spurious loss and reflection. +sim.set_airbox(margin_x=50, margin_y=50, z_above=100, z_below=100) + # Configure left CPW port (single port at signal center) sim.add_cpw_port("o1", layer="topmetal2", s_width=20, gap_width=15) diff --git a/nbs/palace_microstrip.ipynb b/nbs/palace_microstrip.ipynb index 21a1d53f..85c057f6 100644 --- a/nbs/palace_microstrip.ipynb +++ b/nbs/palace_microstrip.ipynb @@ -146,9 +146,14 @@ "sim.set_geometry(c)\n", "\n", "# Configure layer stack from active PDK\n", - "stack = get_stack(air_above=300.0) # auto-detects active PDK\n", + "stack = get_stack() # auto-detects active PDK\n", "sim.set_stack(stack)\n", "\n", + "# Surround the design with air. Without this the absorbing boundary sits\n", + "# directly on the passivation, microns above the trace, and adds large\n", + "# spurious loss and reflection.\n", + "sim.set_airbox(margin_x=50, margin_y=50, z_above=100, z_below=100)\n", + "\n", "# Configure via ports (Metal1 ground plane to TopMetal2 signal)\n", "for port in c.ports:\n", " sim.add_port(port.name, from_layer=\"metal1\", to_layer=\"topmetal2\", geometry=\"via\")\n", diff --git a/nbs/palace_microstrip.py b/nbs/palace_microstrip.py index b55ac63f..f4624eb7 100644 --- a/nbs/palace_microstrip.py +++ b/nbs/palace_microstrip.py @@ -62,9 +62,14 @@ sim.set_geometry(c) # Configure layer stack from active PDK -stack = get_stack(air_above=300.0) # auto-detects active PDK +stack = get_stack() # auto-detects active PDK sim.set_stack(stack) +# Surround the design with air. Without this the absorbing boundary sits +# directly on the passivation, microns above the trace, and adds large +# spurious loss and reflection. +sim.set_airbox(margin_x=50, margin_y=50, z_above=100, z_below=100) + # Configure via ports (Metal1 ground plane to TopMetal2 signal) for port in c.ports: sim.add_port(port.name, from_layer="metal1", to_layer="topmetal2", geometry="via") diff --git a/nbs/palace_width_sweep.ipynb b/nbs/palace_width_sweep.ipynb index 47b13f5f..10153070 100644 --- a/nbs/palace_width_sweep.ipynb +++ b/nbs/palace_width_sweep.ipynb @@ -134,7 +134,7 @@ "\n", "PDK.activate()\n", "\n", - "stack = get_stack(air_above=300.0) # auto-detects active PDK\n", + "stack = get_stack() # auto-detects active PDK\n", "\n", "sims = []\n", "\n", @@ -153,6 +153,10 @@ " sim.set_output_dir(f\"./palace-sim-w{w:.1f}\")\n", " sim.set_geometry(c)\n", " sim.set_stack(stack)\n", + " # Surround the design with air. Without this the absorbing boundary sits\n", + " # directly on the passivation, microns above the trace, and adds large\n", + " # spurious loss and reflection.\n", + " sim.set_airbox(margin_x=50, margin_y=50, z_above=100, z_below=100)\n", "\n", " for port in c.ports:\n", " sim.add_port(\n", diff --git a/nbs/palace_width_sweep.py b/nbs/palace_width_sweep.py index 9658b87f..27a5bcb8 100644 --- a/nbs/palace_width_sweep.py +++ b/nbs/palace_width_sweep.py @@ -44,7 +44,7 @@ PDK.activate() -stack = get_stack(air_above=300.0) # auto-detects active PDK +stack = get_stack() # auto-detects active PDK sims = [] @@ -63,6 +63,10 @@ sim.set_output_dir(f"./palace-sim-w{w:.1f}") sim.set_geometry(c) sim.set_stack(stack) + # Surround the design with air. Without this the absorbing boundary sits + # directly on the passivation, microns above the trace, and adds large + # spurious loss and reflection. + sim.set_airbox(margin_x=50, margin_y=50, z_above=100, z_below=100) for port in c.ports: sim.add_port( diff --git a/src/gsim/palace/base.py b/src/gsim/palace/base.py index 760a2499..33bb6b2e 100644 --- a/src/gsim/palace/base.py +++ b/src/gsim/palace/base.py @@ -1328,6 +1328,30 @@ def mesh( gmsh_verbosity=verbosity, ) + # Without an airbox the absorbing boundary is applied straight to the + # outer dielectric surface, often microns from the metal, where it acts + # as a lossy sheet and fakes large insertion loss and reflection. Warn + # here so it surfaces before any run is submitted. + from gsim.palace.mesh.geometry import is_air_like_material + + mesh_result = getattr(self, "_mesh_result", None) or getattr( + self, "_last_mesh_result", None + ) + groups = getattr(mesh_result, "groups", None) or {} + volumes = groups.get("volumes", {}) + if groups.get("boundary_surfaces", {}).get("absorbing") and not any( + is_air_like_material(stack, name) for name in volumes + ): + logger.warning( + "Absorbing boundary is applied to solid dielectric surfaces " + "(%s) because the model has no air region, which produces " + "large spurious insertion loss and reflection. Add an airbox " + "via set_airbox(margin_x=..., margin_y=..., z_above=..., " + "z_below=...). Note that get_stack(air_above=...) is " + "deprecated and ignored.", + ", ".join(sorted(volumes)), + ) + # Post-mesh summary: nodes, tets, refined / max sizes (in um). stats = result.mesh_stats or {} node_count = stats.get("nodes") diff --git a/src/gsim/palace/mesh/geometry.py b/src/gsim/palace/mesh/geometry.py index 43006e81..2d1dc668 100644 --- a/src/gsim/palace/mesh/geometry.py +++ b/src/gsim/palace/mesh/geometry.py @@ -28,6 +28,50 @@ logger = logging.getLogger(__name__) +def _contains_air_token(name: str | None) -> bool: + """Return True when *name* reads as an air/vacuum region name.""" + if not name: + return False + + normalized = name.strip().lower().replace("-", "_") + if normalized in {"air", "vacuum"}: + return True + + tokens = [tok for tok in normalized.split("_") if tok] + return "air" in tokens or "vacuum" in tokens + + +def is_air_like_material( + stack: LayerStack, + material_name: str, + dielectric_name: str | None = None, +) -> bool: + """Return True when a stack material behaves as air/vacuum. + + A material qualifies if it is a dielectric with permittivity 1, or if + either the material or region name contains an ``air``/``vacuum`` token. + """ + mat = stack.materials.get(material_name) + if isinstance(mat, dict): + mat_type = str(mat.get("type", "")).strip().lower() + eps = mat.get("permittivity") + else: + mat_type = str(getattr(mat, "type", "")).strip().lower() + eps = getattr(mat, "permittivity", None) + + if mat_type == "dielectric": + try: + if eps is not None and abs(float(eps) - 1.0) <= 1e-9: + return True + except (TypeError, ValueError): + pass + + if _contains_air_token(material_name): + return True + + return _contains_air_token(dielectric_name) + + @dataclass class GeometryData: """Container for geometry data extracted from component.""" @@ -93,41 +137,6 @@ def resolve_dielectric_regions( xmax_air = xmax0 + margin_x ymax_air = ymax0 + margin_y - def _contains_air_token(name: str | None) -> bool: - if not name: - return False - - normalized = name.strip().lower().replace("-", "_") - if normalized in {"air", "vacuum"}: - return True - - tokens = [tok for tok in normalized.split("_") if tok] - return "air" in tokens or "vacuum" in tokens - - def _is_air_or_vacuum( - material_name: str, - dielectric_name: str | None = None, - ) -> bool: - mat = stack.materials.get(material_name) - if isinstance(mat, dict): - mat_type = str(mat.get("type", "")).strip().lower() - eps = mat.get("permittivity") - else: - mat_type = str(getattr(mat, "type", "")).strip().lower() - eps = getattr(mat, "permittivity", None) - - if mat_type == "dielectric": - try: - if eps is not None and abs(float(eps) - 1.0) <= 1e-9: - return True - except (TypeError, ValueError): - pass - - if _contains_air_token(material_name): - return True - - return _contains_air_token(dielectric_name) - z_min_all = math.inf z_max_all = -math.inf @@ -146,7 +155,9 @@ def _is_air_or_vacuum( dielectric_name = str(dielectric.get("name", "dielectric")) material = str(dielectric["material"]) - is_air_like = _is_air_or_vacuum(material, dielectric_name=dielectric_name) + is_air_like = is_air_like_material( + stack, material, dielectric_name=dielectric_name + ) if is_air_like and use_airbox: continue