Coverage for qpdk / helper.py: 96%

83 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-04-02 17:50 +0000

1"""Helper functions for the qpdk package.""" 

2 

3from __future__ import annotations 

4 

5import re 

6import warnings 

7from collections.abc import Callable, Sequence 

8from functools import wraps 

9from typing import TYPE_CHECKING, Any, cast 

10 

11if TYPE_CHECKING: 

12 import pandas as pd 

13 import polars as pl 

14 

15from gdsfactory import Component, ComponentAllAngle, LayerEnum, get_component 

16from gdsfactory.technology import LayerViews 

17from gdsfactory.typings import ComponentAllAngleSpec, ComponentSpec, Layer 

18 

19 

20def deprecated(msg: str | Callable | None = None) -> Any: 

21 """Decorator to mark functions as deprecated. 

22 

23 Can be used as @deprecated or @deprecated("custom message"). 

24 

25 Returns: 

26 A decorator function or the decorated function itself. 

27 """ 

28 

29 def decorator(func: Callable) -> Callable: 

30 @wraps(func) 

31 def wrapper(*args: Any, **kwargs: Any) -> Any: 

32 m = ( 

33 msg 

34 if isinstance(msg, str) 

35 else f"{func.__name__} is deprecated and will be removed in a future version." 

36 ) 

37 warnings.warn(m, category=DeprecationWarning, stacklevel=2) 

38 return func(*args, **kwargs) 

39 

40 return wrapper 

41 

42 if callable(msg): 

43 f = msg 

44 msg = None 

45 return decorator(f) 

46 return decorator 

47 

48 

49def denest_layerviews_to_layer_tuples( 

50 layer_views: LayerViews, 

51) -> dict[str, tuple[int, int]]: 

52 """De-nest LayerViews into a flat dictionary of layer names to layer tuples. 

53 

54 Args: 

55 layer_views: LayerViews object containing the layer views. 

56 

57 Returns: 

58 Dictionary mapping layer names to their corresponding (layer, datatype) tuples. 

59 """ 

60 

61 def denest_layer_dict_recursive(items: dict) -> dict: 

62 """Recursively denest layer views to any depth. 

63 

64 Args: 

65 items: Dictionary of layer view items to process 

66 

67 Returns: 

68 Dictionary mapping layer names to layer objects 

69 """ 

70 layers = {} 

71 

72 for key, value in items.items(): 

73 if value.group_members: 

74 # Recursively process nested group members and merge results 

75 nested_layers = denest_layer_dict_recursive(value.group_members) 

76 layers.update(nested_layers) 

77 # Base case: add the layer to our dictionary 

78 elif hasattr(value, "layer"): 

79 layers[key] = value.layer 

80 

81 return layers 

82 

83 # Start the recursive denesting process and return the result 

84 return denest_layer_dict_recursive(layer_views.layer_views) 

85 

86 

87def show_components( 

88 *args: ComponentSpec | ComponentAllAngleSpec, 

89 spacing: int = 200, 

90) -> Sequence[Component]: 

91 """Show sequence of components in a single layout in a line. 

92 

93 The components are spaced based on the maximum width and height of the components. 

94 

95 Args: 

96 *args: Component specifications to show. 

97 spacing: Extra spacing between components. 

98 

99 Returns: 

100 Components after :func:`gdsfactory.get_component`. 

101 """ 

102 from qpdk import PDK # noqa: PLC0415 

103 

104 PDK.activate() 

105 

106 components = [get_component(component_spec) for component_spec in args] 

107 any_all_angle = any( 

108 isinstance(component, ComponentAllAngle) for component in components 

109 ) 

110 

111 c = ComponentAllAngle() if any_all_angle else Component() 

112 

113 max_component_width = max(component.size_info.width for component in components) 

114 max_component_height = max(component.size_info.height for component in components) 

115 if max_component_width > max_component_height: 

116 shift = (0, max_component_height + spacing) 

117 else: 

118 shift = (max_component_width + spacing, 0) 

119 

120 for i, component in enumerate(components): 

121 (c << component).move(( 

122 shift[0] * i, 

123 shift[1] * i, 

124 )) 

125 label_offset = ( 

126 shift[0] * i + (component.size_info.width / 2), 

127 shift[1] * i + (component.size_info.height / 2), 

128 ) 

129 label_text = component.name if hasattr(component, "name") else f"component_{i}" 

130 c.add_label( 

131 text=label_text, 

132 position=label_offset, 

133 layer=cast(LayerEnum, PDK.layers).TEXT, 

134 ) 

135 c.show() 

136 

137 return components 

138 

139 

140def layerenum_to_tuple(layerenum: LayerEnum) -> Layer: 

141 """Convert a LayerEnum object to a tuple containing layer and datatype values. 

142 

143 Args: 

144 layerenum: The LayerEnum object to convert. 

145 

146 Returns: 

147 The (layer, datatype) tuple. 

148 """ 

149 return layerenum.layer, layerenum.datatype 

150 

151 

152_GREEK_MAP: dict[str, str] = { 

153 r"\varepsilon": "ε", 

154 r"\varphi": "φ", 

155 r"\alpha": "α", 

156 r"\beta": "β", 

157 r"\gamma": "γ", 

158 r"\delta": "δ", 

159 r"\epsilon": "ε", 

160 r"\zeta": "ζ", 

161 r"\eta": "η", 

162 r"\theta": "θ", 

163 r"\iota": "ι", 

164 r"\kappa": "κ", 

165 r"\lambda": "λ", 

166 r"\mu": "μ", 

167 r"\nu": "ν", 

168 r"\xi": "ξ", 

169 r"\pi": "π", 

170 r"\rho": "ρ", 

171 r"\sigma": "σ", 

172 r"\tau": "τ", 

173 r"\upsilon": "υ", 

174 r"\phi": "φ", 

175 r"\chi": "χ", 

176 r"\psi": "ψ", 

177 r"\omega": "ω", 

178 r"\Gamma": "Γ", 

179 r"\Delta": "Δ", 

180 r"\Theta": "Θ", 

181 r"\Lambda": "Λ", 

182 r"\Xi": "Ξ", 

183 r"\Pi": "Π", 

184 r"\Sigma": "Σ", 

185 r"\Phi": "Φ", 

186 r"\Psi": "Ψ", 

187 r"\Omega": "Ω", 

188} 

189 

190 

191def _latex_math_to_html(expr: str) -> str: 

192 r"""Convert a single LaTeX math expression (without ``$`` delimiters) to HTML. 

193 

194 Handles ``\mathrm``/``\text`` commands, Greek letters, and sub-/superscripts. 

195 

196 Returns: 

197 HTML string representation of the LaTeX expression. 

198 """ 

199 # Strip \mathrm{...} and \text{...} wrappers, keeping their content 

200 expr = re.sub(r"\\(?:mathrm|text|textrm)\{([^}]+)\}", r"\1", expr) 

201 

202 # Replace Greek letter commands with Unicode (longest names first) 

203 for latex, char in sorted(_GREEK_MAP.items(), key=lambda x: -len(x[0])): 

204 expr = expr.replace(latex, char) 

205 

206 # Subscripts: _{...} then _X 

207 expr = re.sub(r"_\{([^}]+)\}", r"<sub>\1</sub>", expr) 

208 expr = re.sub(r"_(\w)", r"<sub>\1</sub>", expr) 

209 

210 # Superscripts: ^{...} then ^X 

211 expr = re.sub(r"\^\{([^}]+)\}", r"<sup>\1</sup>", expr) 

212 return re.sub(r"\^(\w)", r"<sup>\1</sup>", expr) 

213 

214 

215def _latex_to_html(text: str) -> str: 

216 r"""Convert ``$...$`` delimited LaTeX math in *text* to HTML. 

217 

218 Non-math text is returned unchanged. 

219 

220 Returns: 

221 String with LaTeX math replaced by HTML. 

222 """ 

223 return re.sub(r"\$([^$]+)\$", lambda m: _latex_math_to_html(m.group(1)), text) 

224 

225 

226def display_dataframe(df: pd.DataFrame | pl.DataFrame) -> None: 

227 """Display a DataFrame with both HTML and LaTeX representations. 

228 

229 Wraps a polars or pandas :class:`~pandas.DataFrame` in an object that 

230 provides both ``_repr_html_`` (styled, index-hidden) and 

231 ``_repr_latex_`` representations so that Jupyter Book renders a proper 

232 table in both HTML and PDF outputs. 

233 

234 Cell values may contain ``$...$`` delimited LaTeX math. The HTML 

235 representation converts these to Unicode/HTML (subscripts, Greek 

236 letters, etc.) while the LaTeX representation passes them through as 

237 native math. 

238 

239 Args: 

240 df: A polars or pandas DataFrame to display. 

241 """ 

242 import pandas as pd # noqa: PLC0415 

243 from IPython.display import display # noqa: PLC0415 

244 

245 # Convert polars DataFrame to pandas if needed 

246 pdf: pd.DataFrame = df.to_pandas() if hasattr(df, "to_pandas") else df 

247 

248 class _DualFormatTable: 

249 """Table object providing both HTML and LaTeX representations.""" 

250 

251 @staticmethod 

252 def _repr_html_() -> str: # noqa: PLW3201 

253 html_df = pdf.copy() 

254 for col in html_df.columns: 

255 if pd.api.types.is_string_dtype(html_df[col]): 

256 html_df[col] = html_df[col].map( 

257 lambda x: _latex_to_html(x) if isinstance(x, str) else x 

258 ) 

259 return html_df.style.hide(axis="index")._repr_html_() 

260 

261 @staticmethod 

262 def _repr_latex_() -> str: # noqa: PLW3201 

263 return pdf.to_latex(index=False, escape=False) 

264 

265 display(_DualFormatTable())