Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion nbs/_palace_cpw.ipynb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion nbs/_palace_cpw.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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)
Expand Down
7 changes: 6 additions & 1 deletion nbs/palace_branch_line_coupler.ipynb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion nbs/palace_branch_line_coupler.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
2 changes: 1 addition & 1 deletion nbs/palace_cpw_lumped.ipynb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion nbs/palace_cpw_lumped.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
7 changes: 6 additions & 1 deletion nbs/palace_cpw_via.ipynb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion nbs/palace_cpw_via.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
7 changes: 6 additions & 1 deletion nbs/palace_microstrip.ipynb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 6 additions & 1 deletion nbs/palace_microstrip.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
6 changes: 5 additions & 1 deletion nbs/palace_width_sweep.ipynb

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion nbs/palace_width_sweep.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = []

Expand All @@ -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(
Expand Down
24 changes: 24 additions & 0 deletions src/gsim/palace/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
83 changes: 47 additions & 36 deletions src/gsim/palace/mesh/geometry.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down Expand Up @@ -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

Expand All @@ -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

Expand Down
Loading