Coverage for qpdk / helper.py: 96%
83 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"""Helper functions for the qpdk package."""
3from __future__ import annotations
5import re
6import warnings
7from collections.abc import Callable, Sequence
8from functools import wraps
9from typing import TYPE_CHECKING, Any, cast
11if TYPE_CHECKING:
12 import pandas as pd
13 import polars as pl
15from gdsfactory import Component, ComponentAllAngle, LayerEnum, get_component
16from gdsfactory.technology import LayerViews
17from gdsfactory.typings import ComponentAllAngleSpec, ComponentSpec, Layer
20def deprecated(msg: str | Callable | None = None) -> Any:
21 """Decorator to mark functions as deprecated.
23 Can be used as @deprecated or @deprecated("custom message").
25 Returns:
26 A decorator function or the decorated function itself.
27 """
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)
40 return wrapper
42 if callable(msg):
43 f = msg
44 msg = None
45 return decorator(f)
46 return decorator
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.
54 Args:
55 layer_views: LayerViews object containing the layer views.
57 Returns:
58 Dictionary mapping layer names to their corresponding (layer, datatype) tuples.
59 """
61 def denest_layer_dict_recursive(items: dict) -> dict:
62 """Recursively denest layer views to any depth.
64 Args:
65 items: Dictionary of layer view items to process
67 Returns:
68 Dictionary mapping layer names to layer objects
69 """
70 layers = {}
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
81 return layers
83 # Start the recursive denesting process and return the result
84 return denest_layer_dict_recursive(layer_views.layer_views)
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.
93 The components are spaced based on the maximum width and height of the components.
95 Args:
96 *args: Component specifications to show.
97 spacing: Extra spacing between components.
99 Returns:
100 Components after :func:`gdsfactory.get_component`.
101 """
102 from qpdk import PDK # noqa: PLC0415
104 PDK.activate()
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 )
111 c = ComponentAllAngle() if any_all_angle else Component()
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)
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()
137 return components
140def layerenum_to_tuple(layerenum: LayerEnum) -> Layer:
141 """Convert a LayerEnum object to a tuple containing layer and datatype values.
143 Args:
144 layerenum: The LayerEnum object to convert.
146 Returns:
147 The (layer, datatype) tuple.
148 """
149 return layerenum.layer, layerenum.datatype
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}
191def _latex_math_to_html(expr: str) -> str:
192 r"""Convert a single LaTeX math expression (without ``$`` delimiters) to HTML.
194 Handles ``\mathrm``/``\text`` commands, Greek letters, and sub-/superscripts.
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)
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)
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)
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)
215def _latex_to_html(text: str) -> str:
216 r"""Convert ``$...$`` delimited LaTeX math in *text* to HTML.
218 Non-math text is returned unchanged.
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)
226def display_dataframe(df: pd.DataFrame | pl.DataFrame) -> None:
227 """Display a DataFrame with both HTML and LaTeX representations.
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.
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.
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
245 # Convert polars DataFrame to pandas if needed
246 pdf: pd.DataFrame = df.to_pandas() if hasattr(df, "to_pandas") else df
248 class _DualFormatTable:
249 """Table object providing both HTML and LaTeX representations."""
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_()
261 @staticmethod
262 def _repr_latex_() -> str: # noqa: PLW3201
263 return pdf.to_latex(index=False, escape=False)
265 display(_DualFormatTable())