Coverage for qpdk / simulation / aedt_base.py: 50%

132 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-03-14 10:27 +0000

1"""Base AEDT simulation utilities using PyAEDT. 

2 

3This module provides shared helper functions and a base class for AEDT 

4simulations (HFSS, Q3D, Q2D) from gdsfactory components. 

5""" 

6 

7from __future__ import annotations 

8 

9import contextlib 

10import re 

11import tempfile 

12from collections.abc import Generator 

13from pathlib import Path 

14from typing import TYPE_CHECKING, Any 

15 

16import gdsfactory as gf 

17from gdsfactory.technology.layer_stack import LayerLevel 

18 

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 

27 

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 

33 

34 

35def _get_layer_number_from_level(layer_level: LayerLevel) -> int | None: 

36 """Extract layer number from a LayerLevel's layer definition.""" 

37 if hasattr(layer_level, "derived_layer") and layer_level.derived_layer is not None: 

38 derived = layer_level.derived_layer 

39 if hasattr(derived, "layer"): 

40 inner = derived.layer 

41 if hasattr(inner, "layer"): 

42 val = inner.layer 

43 if isinstance(val, tuple) and len(val) >= 1: 

44 return int(val[0]) 

45 return int(val) 

46 if isinstance(inner, tuple) and len(inner) >= 1: 

47 return int(inner[0]) 

48 

49 layer = layer_level.layer 

50 if isinstance(layer, tuple) and len(layer) >= 1: 

51 return int(layer[0]) 

52 if hasattr(layer, "layer"): 

53 inner = layer.layer 

54 if isinstance(inner, tuple) and len(inner) >= 1: 

55 return int(inner[0]) 

56 if hasattr(inner, "layer"): 

57 val = inner.layer 

58 if isinstance(val, tuple) and len(val) >= 1: 

59 return int(val[0]) 

60 return int(val) 

61 return int(inner) 

62 return None 

63 

64 

65def layer_stack_to_gds_mapping( 

66 layer_stack: LayerStack | None = None, 

67 thickness_override: float | None = None, 

68) -> dict[int, tuple[float, float]]: 

69 """Convert a LayerStack to HFSS/Q3D GDS import mapping dictionary.""" 

70 if layer_stack is None: 

71 layer_stack = LAYER_STACK 

72 

73 mapping: dict[int, tuple[float, float]] = {} 

74 

75 for layer_level in layer_stack.layers.values(): 

76 layer_number = _get_layer_number_from_level(layer_level) 

77 if layer_number is None: 

78 continue 

79 

80 elevation = layer_level.zmin if layer_level.zmin is not None else 0.0 

81 thickness = ( 

82 thickness_override 

83 if thickness_override is not None 

84 else (layer_level.thickness if layer_level.thickness else 0.0) 

85 ) 

86 mapping[layer_number] = (elevation, thickness) 

87 

88 return mapping 

89 

90 

91def prepare_component_for_aedt( 

92 component: Component, 

93 margin_draw: float = 0.0, 

94 margin_etch: float = 0.0, 

95) -> Component: 

96 """Prepare a component for AEDT simulation export.""" 

97 c = gf.Component(name=f"{component.name}_aedt") 

98 c << component.copy() 

99 if margin_etch > 0.0: 

100 c = add_margin_to_layer( 

101 c, 

102 layer_margins=[ 

103 (LAYER.M1_ETCH, margin_etch), 

104 (LAYER.M2_ETCH, margin_etch), 

105 ], 

106 ) 

107 c = apply_additive_metals(c) 

108 c = invert_mask_polarity(c) 

109 if margin_draw > 0.0: 

110 c = add_margin_to_layer( 

111 c, 

112 layer_margins=[ 

113 (LAYER.M1_DRAW, margin_draw), 

114 (LAYER.M2_DRAW, margin_draw), 

115 ], 

116 ) 

117 c = c.remove_layers(layer for layer in LAYER if str(layer).endswith("_ETCH")) 

118 c = remove_metadata_layers(c) 

119 c.add_ports(component.ports) 

120 return c 

121 

122 

123@contextlib.contextmanager 

124def export_component_to_gds_temp( 

125 component: Component, 

126 gds_path: str | Path | None = None, 

127 prefix: str = "qpdk_aedt_", 

128) -> Generator[Path, None, None]: 

129 """Context manager for exporting a component to a temporary GDS file.""" 

130 if gds_path is not None: 

131 path = Path(gds_path) 

132 component.write_gds(str(path)) 

133 yield path 

134 else: 

135 with tempfile.TemporaryDirectory(prefix=prefix) as temp_dir: 

136 path = Path(temp_dir) / "component.gds" 

137 component.write_gds(str(path)) 

138 yield path 

139 

140 

141def rename_imported_objects( 

142 app: Any, new_objects: list[str], layer_stack: LayerStack 

143) -> list[str]: 

144 """Rename imported GDS objects based on the layer stack.""" 

145 num_to_name = {} 

146 for name, level in layer_stack.layers.items(): 

147 layer_num = _get_layer_number_from_level(level) 

148 if layer_num is not None and layer_num not in num_to_name: 

149 num_to_name[layer_num] = name 

150 

151 renamed_objects = [] 

152 for obj_name in new_objects: 

153 match = re.match(r"^signal(\d+)(_.*)?$", obj_name) 

154 new_name = obj_name 

155 if match: 

156 layer_num = int(match.group(1)) 

157 suffix = match.group(2) or "" 

158 if layer_num in num_to_name: 

159 layer_name = num_to_name[layer_num] 

160 new_name = f"{layer_name}{suffix}" 

161 try: 

162 app.modeler[obj_name].name = new_name 

163 except Exception: 

164 new_name = obj_name 

165 renamed_objects.append(new_name) 

166 

167 return renamed_objects 

168 

169 

170def add_materials_to_aedt(app: Hfss | Q2d | Q3d) -> None: 

171 """Add QPDK materials to the PyAEDT application.""" 

172 from qpdk.tech import material_properties 

173 

174 for name, props in material_properties.items(): 

175 if app.materials.exists_material(name): 

176 continue 

177 

178 mat = app.materials.add_material(name) 

179 

180 for prop_name, prop_value in props.items(): 

181 if prop_value == float("inf"): 

182 if prop_name == "relative_permittivity": 

183 mat.conductivity = 1e30 

184 continue 

185 

186 if prop_name == "relative_permittivity": 

187 mat.permittivity = prop_value 

188 elif prop_name == "conductivity": 

189 mat.conductivity = prop_value 

190 

191 

192class AEDTBase: 

193 """Base class for AEDT simulations.""" 

194 

195 def __init__(self, app: Hfss | Q2d | Q3d): 

196 """Initialize the AEDT base class. 

197 

198 Args: 

199 app: The PyAEDT application instance. 

200 """ 

201 self.app = app 

202 

203 @property 

204 def modeler(self): 

205 """Return the AEDT modeler instance.""" 

206 return self.app.modeler 

207 

208 def add_materials(self) -> None: 

209 """Add QPDK materials to the AEDT project.""" 

210 add_materials_to_aedt(self.app) 

211 

212 def add_substrate( 

213 self, 

214 component: Component, 

215 thickness: float = 500.0, 

216 material: str = "silicon", 

217 name: str = "Substrate", 

218 ) -> str: 

219 """Add a substrate box below the component geometry.""" 

220 bounds = component.bbox() 

221 x_min, y_min = bounds.p1.x, bounds.p1.y 

222 dx, dy = bounds.p2.x - x_min, bounds.p2.y - y_min 

223 

224 substrate = self.modeler.create_box( 

225 origin=[x_min, y_min, -thickness], 

226 sizes=[dx, dy, thickness], 

227 name=name, 

228 material=material, 

229 ) 

230 substrate.mesh_order = 4 

231 return substrate.name 

232 

233 def save(self) -> None: 

234 """Save the AEDT project.""" 

235 self.app.save_project()