Coverage for qpdk / simulation / aedt_base.py: 50%
131 statements
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-02 17:50 +0000
« prev ^ index » next coverage.py v7.13.5, created at 2026-04-02 17:50 +0000
1"""Base AEDT simulation utilities using PyAEDT.
3This module provides shared helper functions and a base class for AEDT
4simulations (HFSS, Q3D, Q2D) from gdsfactory components.
5"""
7from __future__ import annotations
9import re
10import tempfile
11from collections.abc import Generator
12from contextlib import contextmanager
13from pathlib import Path
14from typing import TYPE_CHECKING, Any
16import gdsfactory as gf
17from gdsfactory.technology.layer_stack import LayerLevel
19from qpdk import LAYER_STACK
20from qpdk.cells.helpers import (
21 add_margin_to_layer,
22 apply_additive_metals,
23 invert_mask_polarity,
24 remove_metadata_layers,
25)
26from qpdk.tech import LAYER, material_properties
28if TYPE_CHECKING:
29 from ansys.aedt.core import Hfss, Q2d
30 from ansys.aedt.core.q3d import Q3d
31 from gdsfactory.component import Component
32 from gdsfactory.technology import LayerStack
35def _get_layer_number_from_level(layer_level: LayerLevel) -> int | None:
36 """Extract layer number from a LayerLevel's layer definition.
38 Returns:
39 The GDS layer number if available, else None.
40 """
41 if hasattr(layer_level, "derived_layer") and layer_level.derived_layer is not None:
42 derived = layer_level.derived_layer
43 if hasattr(derived, "layer"):
44 inner = derived.layer
45 if hasattr(inner, "layer"):
46 val = inner.layer
47 if isinstance(val, tuple) and len(val) >= 1:
48 return int(val[0])
49 return int(val)
50 if isinstance(inner, tuple) and len(inner) >= 1:
51 return int(inner[0])
53 layer = layer_level.layer
54 if isinstance(layer, tuple) and len(layer) >= 1:
55 return int(layer[0])
56 if hasattr(layer, "layer"):
57 inner = layer.layer
58 if isinstance(inner, tuple) and len(inner) >= 1:
59 return int(inner[0])
60 if hasattr(inner, "layer"):
61 val = inner.layer
62 if isinstance(val, tuple) and len(val) >= 1:
63 return int(val[0])
64 return int(val)
65 return int(inner)
66 return None
69def layer_stack_to_gds_mapping(
70 layer_stack: LayerStack | None = None,
71 thickness_override: float | None = None,
72) -> dict[int, tuple[float, float]]:
73 """Convert a LayerStack to HFSS/Q3D GDS import mapping dictionary.
75 Returns:
76 Dictionary mapping layer number to (thickness, elevation) tuple.
77 """
78 if layer_stack is None:
79 layer_stack = LAYER_STACK
81 mapping: dict[int, tuple[float, float]] = {}
83 for layer_level in layer_stack.layers.values():
84 layer_number = _get_layer_number_from_level(layer_level)
85 if layer_number is None:
86 continue
88 elevation = layer_level.zmin if layer_level.zmin is not None else 0.0
89 thickness = (
90 thickness_override
91 if thickness_override is not None
92 else (layer_level.thickness or 0.0)
93 )
94 mapping[layer_number] = (elevation, thickness)
96 return mapping
99def prepare_component_for_aedt(
100 component: Component,
101 margin_draw: float = 0.0,
102 margin_etch: float = 0.0,
103) -> Component:
104 """Prepare a component for AEDT simulation export.
106 Returns:
107 A copy of the component prepared for simulation.
108 """
109 c = gf.Component(name=f"{component.name}_aedt")
110 c << component.copy()
111 if margin_etch > 0.0:
112 c = add_margin_to_layer(
113 c,
114 layer_margins=[
115 (LAYER.M1_ETCH, margin_etch),
116 (LAYER.M2_ETCH, margin_etch),
117 ],
118 )
119 c = apply_additive_metals(c)
120 c = invert_mask_polarity(c)
121 if margin_draw > 0.0:
122 c = add_margin_to_layer(
123 c,
124 layer_margins=[
125 (LAYER.M1_DRAW, margin_draw),
126 (LAYER.M2_DRAW, margin_draw),
127 ],
128 )
129 c = c.remove_layers(layer for layer in LAYER if str(layer).endswith("_ETCH"))
130 c = remove_metadata_layers(c)
131 c.add_ports(component.ports)
132 return c
135@contextmanager
136def export_component_to_gds_temp(
137 component: gf.Component,
138 gds_path: str | Path | None = None,
139 prefix: str = "qpdk_aedt_",
140) -> Generator[Path, None, None]:
141 """Context manager for exporting a component to a temporary GDS file.
143 Yields:
144 Path to the exported GDS file.
145 """
146 if gds_path is not None:
147 path = Path(gds_path)
148 component.write_gds(str(path))
149 yield path
150 else:
151 with tempfile.TemporaryDirectory(prefix=prefix) as temp_dir:
152 path = Path(temp_dir) / "component.gds"
153 component.write_gds(str(path))
154 yield path
157def rename_imported_objects(
158 app: Any, new_objects: list[str], layer_stack: LayerStack
159) -> list[str]:
160 """Rename imported GDS objects based on the layer stack.
162 Returns:
163 List of renamed object names.
164 """
165 num_to_name = {}
166 for name, level in layer_stack.layers.items():
167 layer_num = _get_layer_number_from_level(level)
168 if layer_num is not None and layer_num not in num_to_name:
169 num_to_name[layer_num] = name
171 renamed_objects = []
172 for obj_name in new_objects:
173 match = re.match(r"^signal(\d+)(_.*)?$", obj_name)
174 new_name = obj_name
175 if match:
176 layer_num = int(match.group(1))
177 suffix = match.group(2) or ""
178 if layer_num in num_to_name:
179 layer_name = num_to_name[layer_num]
180 new_name = f"{layer_name}{suffix}"
181 try:
182 app.modeler[obj_name].name = new_name
183 except Exception:
184 new_name = obj_name
185 renamed_objects.append(new_name)
187 return renamed_objects
190def add_materials_to_aedt(app: Hfss | Q2d | Q3d) -> None:
191 """Add QPDK materials to the PyAEDT application."""
192 for name, props in material_properties.items():
193 if app.materials.exists_material(name):
194 continue
196 mat = app.materials.add_material(name)
198 for prop_name, prop_value in props.items():
199 if prop_value == float("inf"):
200 if prop_name == "relative_permittivity":
201 mat.conductivity = 1e30
202 continue
204 if prop_name == "relative_permittivity":
205 mat.permittivity = prop_value
206 elif prop_name == "conductivity":
207 mat.conductivity = prop_value
210class AEDTBase:
211 """Base class for AEDT simulations."""
213 def __init__(self, app: Hfss | Q2d | Q3d):
214 """Initialize the AEDT base class.
216 Args:
217 app: The PyAEDT application instance.
218 """
219 self.app = app
221 @property
222 def modeler(self):
223 """Return the AEDT modeler instance."""
224 return self.app.modeler
226 def add_materials(self) -> None:
227 """Add QPDK materials to the AEDT project."""
228 add_materials_to_aedt(self.app)
230 def add_substrate(
231 self,
232 component: Component,
233 thickness: float = 500.0,
234 material: str = "silicon",
235 name: str = "Substrate",
236 ) -> str:
237 """Add a substrate box below the component geometry.
239 Returns:
240 Name of the created substrate object.
241 """
242 bounds = component.bbox()
243 x_min, y_min = bounds.p1.x, bounds.p1.y
244 dx, dy = bounds.p2.x - x_min, bounds.p2.y - y_min
246 substrate = self.modeler.create_box(
247 origin=[x_min, y_min, -thickness],
248 sizes=[dx, dy, thickness],
249 name=name,
250 material=material,
251 )
252 substrate.mesh_order = 4
253 return substrate.name
255 def save(self) -> None:
256 """Save the AEDT project."""
257 self.app.save_project()