Source code for photonforge.parametric_utils

import base64
import functools
import hashlib
import warnings
from typing import Any, Callable, Optional

from .extension import (
    Component,
    Expression,
    PoleResidueMatrix,
    Technology,
    _component_registry,
    _technology_registry,
)

_warnings_cache: set = set()

_gdsii_safe_chars: set[str] = set(
    "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_?$"
)


def _safe_hash(b: bytes) -> str:
    # Remove 4 bytes of padding at the end and use a case-insensitive alphabet
    return base64.b32encode(hashlib.sha256(b).digest())[:-4].decode("utf-8")


# Tidy3D limits the path name to 100 characters, but the ui also appends the timestamp
def _filename_cleanup(s: str, strict: bool = True, max_length: int = 64) -> str:
    if strict:
        allowed = set("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ()-._~")
    else:
        allowed = set(
            "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!#$%&'()+,-.;=@[]^_`{}~ "
        )
    result = ("".join(c if c in allowed else "" for c in s)).strip()
    if max_length > 0:
        result = result[:max_length]
    return result


def _make_str(x: Any) -> str:
    return (
        f"{x.__name__}${hash(x)}"
        if callable(x) and not isinstance(x, (PoleResidueMatrix, Expression))
        else str(x)
    )


def _suffix_from_args(*args: Any, **kwargs: Any) -> str:
    suffix = ""
    args_suffix = "_".join(_make_str(x) for x in args)
    if len(args_suffix) > 0:
        suffix += "_" + args_suffix
    kwargs_suffix = "_".join(f"{k}={_make_str(kwargs[k])}" for k in sorted(kwargs))
    if len(kwargs_suffix) > 0:
        suffix += "_" + kwargs_suffix
    if len(suffix) > 53:  # 1 + length of _safe_hash return value
        return "_" + _safe_hash(suffix[1:].encode("utf-8"))
    return suffix


[docs] def parametric_component( decorated_function: Optional[Callable] = None, name_prefix: Optional[str] = None, gdsii_safe_name: bool = True, use_parametric_cache_default: bool = True, ) -> Callable: """Decorator to create parametric components from functions. If the name of the created component is empty, this decorator sets it with name prefix and the values of the function arguments when called. Components can be cached to avoid duplication. They are cached based on the calling arguments (specifically, argument ``id``). Regardless of the default setting, each component can use or skip caching by setting the ``bool`` keyword argument ``use_parametric_cache`` in the decorated function call. Args: decorated_function: Function that returns a Component. name_prefix: Prefix for the component name. If ``None``, the decorated function name is used. gdsii_safe_name: If set, only use GDSII-safe characters in the name (``name_prefix`` is not modified by this flag). use_parametric_cache_default: Controls the default caching behavior for the decorated function. Examples: >>> @parametric_component ... def straight(*, length, port_spec_name, technology): ... port_spec = technology.ports[port_spec_name] ... c = Component(technology=technology) ... for layer, path in port_spec.get_paths((0, 0)): ... c.add(layer, path.segment((length, 0))) ... c.add_port(Port(center=(0, 0), input_direction=0, spec=port_spec)) ... c.add_port(Port(center=(length, 0), input_direction=180, spec=port_spec)) ... c.add_model(Tidy3DModel(port_symmetries=[(1, 0)])) ... return c ... >>> technology = basic_technology() >>> component = straight(length=5, port_spec_name="Strip", technology=technology) >>> print(component.name) straight_10_Strip_Basic_Technology_1.0 Caching behavior: >>> component1 = straight(length=2, port_spec_name="Strip", technology=technology) >>> component2 = straight(length=2, port_spec_name="Strip", technology=technology) >>> component3 = straight( ... length=2, port_spec_name="Strip", technology=technology, use_parametric_cache=False ... ) >>> component2 == component1 True >>> component2 is component1 True >>> component3 == component1 True >>> component3 is component1 False Note: It is generally a good idea to force parametric components to accept only keyword arguments (by using the ``*`` as first argument in the argument list), because those are stored for future updates of the created component with :func:`Component.update`. See also: `Custom Parametric Components <../guides/Custom_Parametric_Components.ipynb>`__ """ def _decorator(component_func): _cache = {} prefix = component_func.__name__ if name_prefix is None else name_prefix full_name = f"{component_func.__module__}.{component_func.__qualname__}" if full_name in _component_registry: warnings.warn( f"Component function '{full_name}' previously registered will be overwritten.", RuntimeWarning, 2, ) @functools.wraps(component_func) def _component_func(*args, **kwargs): if len(args) > 0: warning_key = ("Parametric component with args", full_name) if warning_key not in _warnings_cache: try: var_names = component_func.__code__.co_varnames assert len(var_names) > 0 except Exception: var_names = ("argument1",) _warnings_cache.add(warning_key) warnings.warn( f"Parametric component '{full_name}' called with positional arguments. " f"Positional arguments are not remembered in parametric updates. Please " f"use keyword arguments, e.g., '{component_func.__qualname__}" f"({var_names[0]}={args[0]!r}, ...)'", RuntimeWarning, 2, ) use_parametric_cache = kwargs.pop("use_parametric_cache", use_parametric_cache_default) c = component_func(*args, **kwargs) if not isinstance(c, Component): raise TypeError( f"Updated object returned by parametric function '{full_name}' is not a " "'Component' instance." ) c.parametric_function = full_name final_kwargs = ( dict(component_func.__kwdefaults__) if hasattr(component_func, "__kwdefaults__") and component_func.__kwdefaults__ else {} ) final_kwargs.update(kwargs) c.parametric_kwargs = final_kwargs if not c.name: suffix = _suffix_from_args(*args, **final_kwargs) if gdsii_safe_name: suffix = "".join(x if x in _gdsii_safe_chars else "_" for x in suffix) c.name = prefix + suffix if use_parametric_cache: key = c.as_bytes if key in _cache: cached = _cache[key] if cached.as_bytes == key: c = cached else: _cache[key] = c else: _cache[key] = c return c _component_registry[full_name] = _component_func return _component_func if decorated_function: return _decorator(decorated_function) return _decorator
[docs] def parametric_technology( decorated_function: Optional[Callable] = None, name_prefix: Optional[str] = None, use_parametric_cache_default: bool = True, ) -> Callable: """Decorator to create parametric technologies from functions. If the name of the created technology is empty, this decorator sets it with name prefix and the values of the function arguments when called. Technologies can be cached to avoid duplication. They are cached based on the calling arguments (specifically, argument ``id``). Regardless of the default setting, each technology can use or skip caching by setting the ``bool`` keyword argument ``use_parametric_cache`` in the decorated function call. Args: decorated_function: Function that returns a Technology. name_prefix: Prefix for the technology name. If ``None``, the decorated function name is used. use_parametric_cache_default: Controls the default caching behavior for the decorated function. Example: >>> @parametric_technology ... def demo_technology(*, thickness=0.250, sidewall_angle=0): ... layers = { ... "Si": LayerSpec( ... (1, 0), "Silicon layer", "#d2132e18", "//" ... ) ... } ... extrusion_specs = [ ... ExtrusionSpec( ... MaskSpec((1, 0)), ... td.Medium(permittivity=3.48**2), ... (0, thickness), ... sidewall_angle=sidewall_angle, ... ) ... ] ... port_specs = { ... "STE": PortSpec( ... "Single mode strip", ... 1.5, ... (-0.5, thickness + 0.5), ... target_neff=3.48, ... path_profiles=[(0.45, 0, (1, 0))], ... ) ... } ... technology = Technology( ... "Demo technology", ... "1.0", ... layers, ... extrusion_specs, ... port_specs, ... td.Medium(permittivity=1.45**2), ... ) ... # Add random variables to facilitate Monte Carlo runs: ... technology.random_variables = [ ... monte_carlo.RandomVariable( ... "sidewall_angle", value=sidewall_angle, stdev=2 ... ), ... monte_carlo.RandomVariable( ... "thickness", ... value_range=[thickness - 0.01, thickness + 0.01], ... ), ... ] ... return technology >>> technology = demo_technology(sidewall_angle=10, thickness=0.3) >>> technology.random_variables [RandomVariable('sidewall_angle', **{'value': 10, 'stdev': 2}), RandomVariable('thickness', **{'value_range': (0.29, 0.31)})] Note: It is generally a good idea to force parametric technologies to accept only keyword arguments (by using the ``*`` as first argument in the argument list), because those are stored for future updates of the created technology with :func:`Technology.update`. """ def _decorator(technology_func): _cache = {} prefix = technology_func.__name__ if name_prefix is None else name_prefix full_name = f"{technology_func.__module__}.{technology_func.__qualname__}" if full_name in _technology_registry: warnings.warn( f"Technology function '{full_name}' previously registered will be overwritten.", RuntimeWarning, 2, ) @functools.wraps(technology_func) def _technology_func(*args, **kwargs): if len(args) > 0: warning_key = ("Parametric technology with args", full_name) if warning_key not in _warnings_cache: _warnings_cache.add(warning_key) warnings.warn( f"Parametric technology '{full_name}' called with positional arguments. " "Positional arguments are not remembered in parametric updates.", RuntimeWarning, 2, ) use_parametric_cache = kwargs.pop("use_parametric_cache", use_parametric_cache_default) t = technology_func(*args, **kwargs) if not isinstance(t, Technology): raise TypeError( f"Updated object returned by parametric function '{full_name}' is not a " "'Technology' instance." ) if not t.name: t.name = prefix + _suffix_from_args(*args, **kwargs) t.parametric_function = full_name final_kwargs = ( dict(technology_func.__kwdefaults__) if hasattr(technology_func, "__kwdefaults__") and technology_func.__kwdefaults__ else {} ) final_kwargs.update(kwargs) t.parametric_kwargs = final_kwargs if use_parametric_cache: key = t.as_bytes if key in _cache: cached = _cache[key] if cached.as_bytes == key: t = cached else: _cache[key] = t else: _cache[key] = t return t _technology_registry[full_name] = _technology_func return _technology_func if decorated_function: return _decorator(decorated_function) return _decorator