Source code for gdsfactory.technology.layer_views

"""A GDS layer is a tuple of two integers.

You can maintain LayerViews in YAML (.yaml) or Klayout XML file (.lyp)

"""

from __future__ import annotations

import builtins
import pathlib
import re
import warnings
import xml.etree.ElementTree as ET
from collections.abc import Mapping
from typing import TYPE_CHECKING, Any, TypeAlias

import numpy as np
import yaml
from kfactory import logger
from kfactory.layer import LayerEnum
from pydantic import BaseModel, ConfigDict, Field, field_validator
from pydantic.color import ColorType
from pydantic_extra_types.color import Color

from gdsfactory.name import clean_name
from gdsfactory.technology.color_utils import ensure_six_digit_hex_color
from gdsfactory.technology.xml_utils import make_pretty_xml
from gdsfactory.technology.yaml_utils import TechnologyDumper

if TYPE_CHECKING:
    from gdsfactory.component import Component

PathLike = pathlib.Path | str
Layer = tuple[int, int]
IncEx: TypeAlias = (
    set[int] | set[str] | Mapping[int, "IncEx | bool"] | Mapping[str, "IncEx | bool"]
)

_klayout_line_styles = {
    "solid": "",
    "dotted": "*.",
    "dashed": "**..**",
    "dash-dotted": "***..**..***",
    "short dashed": "*..*",
    "short dash-dotted": "**.*.*",
    "long dashed": "*****..*****",
    "dash-double-dotted": "***..*.*..**",
}
_klayout_dither_patterns = {
    "solid": "*",
    "hollow": ".",
    "dotted": "*.\n.*",
    "coarsely dotted": "*...\n....\n..*.\n....",
    "left-hatched": "*...\n.*..\n..*.\n...*",
    "lightly left-hatched": "*.......\n"
    ".*......\n"
    "..*.....\n"
    "...*....\n"
    "....*...\n"
    ".....*..\n"
    "......*.\n"
    ".......*",
    "strongly left-hatched dense": "**..\n.**.\n..**\n*..*",
    "strongly left-hatched sparse": "**......\n"
    ".**.....\n"
    "..**....\n"
    "...**...\n"
    "....**..\n"
    ".....**.\n"
    "......**\n"
    "*......*",
    "right-hatched": "*...\n...*\n..*.\n.*..",
    "lightly right-hatched": "*.......\n"
    ".......*\n"
    "......*.\n"
    ".....*..\n"
    "....*...\n"
    "...*....\n"
    "..*.....\n"
    ".*......",
    "strongly right-hatched dense": "**..\n*..*\n..**\n.**.",
    "strongly right-hatched sparse": "**......\n"
    "*......*\n"
    "......**\n"
    ".....**.\n"
    "....**..\n"
    "...**...\n"
    "..**....\n"
    ".**.....",
    "cross-hatched": "*...\n.*.*\n..*.\n.*.*",
    "lightly cross-hatched": "*.......\n"
    ".*.....*\n"
    "..*...*.\n"
    "...*.*..\n"
    "....*...\n"
    "...*.*..\n"
    "..*...*.\n"
    ".*.....*",
    "checkerboard 2px": "**..\n**..\n..**\n..**",
    "strongly cross-hatched sparse": "**......\n"
    "***....*\n"
    "..**..**\n"
    "...****.\n"
    "....**..\n"
    "...****.\n"
    "..**..**\n"
    "***....*",
    "heavy checkerboard": "****....\n"
    "****....\n"
    "****....\n"
    "****....\n"
    "....****\n"
    "....****\n"
    "....****\n"
    "....****",
    "hollow bubbles": ".*...*..\n"
    "*.*.....\n"
    ".*...*..\n"
    "....*.*.\n"
    ".*...*..\n"
    "*.*.....\n"
    ".*...*..\n"
    "....*.*.",
    "solid bubbles": ".*...*..\n"
    "***.....\n"
    ".*...*..\n"
    "....***.\n"
    ".*...*..\n"
    "***.....\n"
    ".*...*..\n"
    "....***.",
    "pyramids": ".*......\n"
    "*.*.....\n"
    "****...*\n"
    "........\n"
    "....*...\n"
    "...*.*..\n"
    "..*****.\n"
    "........",
    "turned pyramids": "****...*\n"
    "*.*.....\n"
    ".*......\n"
    "........\n"
    "..*****.\n"
    "...*.*..\n"
    "....*...\n"
    "........",
    "plus": "..*...*.\n"
    "..*.....\n"
    "*****...\n"
    "..*.....\n"
    "..*...*.\n"
    "......*.\n"
    "*...****\n"
    "......*.",
    "minus": "........\n"
    "........\n"
    "*****...\n"
    "........\n"
    "........\n"
    "........\n"
    "*...****\n"
    "........",
    "22.5 degree down": "*......*\n"
    ".**.....\n"
    "...**...\n"
    ".....**.\n"
    "*......*\n"
    ".**.....\n"
    "...**...\n"
    ".....**.",
    "22.5 degree up": "*......*\n"
    ".....**.\n"
    "...**...\n"
    ".**.....\n"
    "*......*\n"
    ".....**.\n"
    "...**...\n"
    ".**.....",
    "67.5 degree down": "*...*...\n"
    ".*...*..\n"
    ".*...*..\n"
    "..*...*.\n"
    "..*...*.\n"
    "...*...*\n"
    "...*...*\n"
    "*...*...",
    "67.5 degree up": "...*...*\n"
    "..*...*.\n"
    "..*...*.\n"
    ".*...*..\n"
    ".*...*..\n"
    "*...*...\n"
    "*...*...\n"
    "...*...*",
    "22.5 degree cross hatched": "*......*\n"
    ".**..**.\n"
    "...**...\n"
    ".**..**.\n"
    "*......*\n"
    ".**..**.\n"
    "...**...\n"
    ".**..**.",
    "zig zag": "..*...*.\n"
    ".*.*.*.*\n"
    "*...*...\n"
    "........\n"
    "..*...*.\n"
    ".*.*.*.*\n"
    "*...*...\n"
    "........",
    "sine": "..***...\n"
    ".*...*..\n"
    "*.....**\n"
    "........\n"
    "..***...\n"
    ".*...*..\n"
    "*.....**\n"
    "........",
    "heavy unordered": "****.*.*\n"
    "**.****.\n"
    "*.**.***\n"
    "*****.*.\n"
    ".**.****\n"
    "**.***.*\n"
    ".****.**\n"
    "*.*.****",
    "light unordered": "....*.*.\n"
    "..*....*\n"
    ".*..*...\n"
    ".....*.*\n"
    "*..*....\n"
    "..*...*.\n"
    "*....*..\n"
    ".*.*....",
    "vertical dense": "*.\n*.\n",
    "vertical": ".*..\n.*..\n.*..\n.*..\n",
    "vertical thick": ".**.\n.**.\n.**.\n.**.\n",
    "vertical sparse": "...*....\n...*....\n...*....\n...*....\n",
    "vertical sparse, thick": "...**...\n...**...\n...**...\n...**...\n",
    "horizontal dense": "**\n..\n",
    "horizontal": "....\n****\n....\n....\n",
    "horizontal thick": "....\n****\n****\n....\n",
    "horizontal sparse": "........\n"
    "........\n"
    "........\n"
    "********\n"
    "........\n"
    "........\n"
    "........\n"
    "........\n",
    "horizontal sparse, thick": "........\n"
    "........\n"
    "........\n"
    "********\n"
    "********\n"
    "........\n"
    "........\n"
    "........\n",
    "grid dense": "**\n*.\n",
    "grid": ".*..\n****\n.*..\n.*..\n",
    "grid thick": ".**.\n****\n****\n.**.\n",
    "grid sparse": "...*....\n"
    "...*....\n"
    "...*....\n"
    "********\n"
    "...*....\n"
    "...*....\n"
    "...*....\n"
    "...*....\n",
    "grid sparse, thick": "...**...\n"
    "...**...\n"
    "...**...\n"
    "********\n"
    "********\n"
    "...**...\n"
    "...**...\n"
    "...**...\n",
}


class HatchPattern(BaseModel):
    """Custom dither pattern. See KLayout documentation for more info.

    Attributes:
        name: Name of the pattern.
        order: Order of pattern.
        custom_pattern: Pattern defining custom shape.
    """

    name: str | None = Field(default=None, exclude=True)
    order: int | None = None
    custom_pattern: str | None = None

    @field_validator("custom_pattern")
    @classmethod
    def check_pattern_klayout(cls, pattern: str | None) -> str | None:
        if pattern is None:
            return None
        # Optimization: Use len(line) directly without converting to list
        if any(len(line) > 32 for line in pattern.splitlines()):
            raise ValueError(f"Custom pattern {pattern} has more than 32 characters.")
        return pattern

    def to_klayout_xml(self) -> ET.Element:
        if self.custom_pattern is None:
            raise KeyError(
                f"Cannot write custom hatch/dither pattern {self.name} to KLayout xml format because no pattern is present."
            )

        el = ET.Element("custom-dither-pattern")

        subel = ET.SubElement(el, "pattern")
        lines = self.custom_pattern.splitlines()
        if len(lines) == 1:
            subel.text = lines[0]
        else:
            for line in lines:
                ET.SubElement(subel, "line").text = line

        ET.SubElement(el, "order").text = str(self.order)
        ET.SubElement(el, "name").text = self.name
        return el


class LineStyle(BaseModel):
    """Custom line style. See KLayout documentation for more info.

    Attributes:
        name: Name of the line style.
        order: Order of line style.
        custom_style: Line style to use.
    """

    name: str | None = Field(default=None, exclude=True)
    order: int | None = None
    custom_style: str | None = None

    @field_validator("custom_style")
    @classmethod
    def check_pattern(cls, pattern: str | None) -> str | None:
        if pattern is None:
            return None

        # Check length first (it's faster)
        if len(pattern) > 32:
            raise ValueError(
                f"Custom line pattern {pattern} must consist of '*' and '.' characters and be no more than 32 characters long."
            )

        valid_chars = {"*", "."}
        for char in pattern:
            if char not in valid_chars:
                raise ValueError(
                    f"Custom line pattern {pattern} must consist of '*' and '.' characters and be no more than 32 characters long."
                )

        return pattern

    def to_klayout_xml(self) -> ET.Element:
        if self.custom_style is None:
            raise KeyError(
                f"Cannot write custom line style {self.name} to KLayout xml format because no pattern is present."
            )

        el = ET.Element("custom-line-pattern")

        ET.SubElement(el, "pattern").text = str(self.custom_style)
        ET.SubElement(el, "order").text = str(self.order)
        ET.SubElement(el, "name").text = self.name
        return el


[docs] class LayerView(BaseModel): """KLayout layer properties. Docstrings adapted from KLayout documentation: https://www.klayout.de/lyp_format.html Attributes: name: Layer name. info: Extra info to include in the LayerView. layer: GDSII layer. layer_in_name: Whether to display the name as 'name layer/datatype' rather than just the layer. width: This is the line width of the frame in pixels (or empty for the default which is 1). line_style: This is the number of the line style used to draw the shape boundaries. An empty string is "solid line". The values are "Ix" for one of the built-in styles where "I0" is "solid", "I1" is "dotted" etc. hatch_pattern: This is the number of the dither pattern used to fill the shapes. The values are "Ix" for one of the built-in pattern where "I0" is "solid" and "I1" is "clear". fill_color: Display color of the layer fill. frame_color: Display color of the layer frame. Accepts Pydantic Color types. See: https://docs.pydantic.dev/usage/types/#color-type for more info. fill_brightness: Brightness of the fill. frame_brightness: Brightness of the frame. animation: This is a value indicating the animation mode. 0 is "none", 1 is "scrolling", 2 is "blinking" and 3 is "inverse blinking". (Only applies to KLayout layer properties) xfill: Whether boxes are drawn with a diagonal cross. (Only applies to KLayout layer properties) marked: Whether the entry is marked (drawn with small crosses). (Only applies to KLayout layer properties) transparent: Whether the entry is transparent. visible: Whether the entry is visible. valid: Whether the entry is valid. Invalid layers are drawn but you can't select shapes on those layers. (Only applies to KLayout layer properties) group_members: Add a list of group members to the LayerView. """ name: str | None = Field(default=None, exclude=True) info: str | None = Field(default=None) layer: Layer | None = None layer_in_name: bool = False frame_color: Color | None = None fill_color: Color | None = None frame_brightness: int = 0 fill_brightness: int = 0 hatch_pattern: str | HatchPattern | None = None line_style: str | LineStyle | None = None valid: bool = True visible: bool = True transparent: bool = False width: int | None = None marked: bool = False xfill: bool = False animation: int = 0 group_members: builtins.dict[str, LayerView] = Field(default_factory=builtins.dict)
[docs] def __init__( self, gds_layer: int | None = None, gds_datatype: int | None = None, color: ColorType | None = None, brightness: int | None = None, **data: Any, ) -> None: """Initialize LayerView object.""" if (gds_layer is not None) and (gds_datatype is not None): if "layer" in data and data["layer"] is not None: raise KeyError( "Specify either 'layer' or both 'gds_layer' and 'gds_datatype'." ) data["layer"] = (gds_layer, gds_datatype) if color is not None: if "fill_color" in data or "frame_color" in data: raise KeyError( "Specify either a single 'color' or both 'frame_color' and 'fill_color'." ) data["fill_color"] = data["frame_color"] = color if brightness is not None: if "fill_brightness" in data or "frame_brightness" in data: raise KeyError( "Specify either a single 'brightness' or both 'frame_brightness' and 'fill_brightness'." ) data["fill_brightness"] = data["frame_brightness"] = brightness super().__init__(**data)
def dict( self, *, include: IncEx | None = None, exclude: IncEx | None = None, by_alias: bool = False, exclude_unset: bool = False, exclude_defaults: bool = False, exclude_none: bool = False, simplify: bool = True, ) -> dict[str, Any]: """Generate a dictionary representation of the model, optionally specifying which fields to include or exclude. Specify "simplify" to consolidate fill and frame color/brightness if they are the same. Args: mode: The mode in which `to_python` should run. If mode is 'json', the dictionary will only contain JSON serializable types. If mode is 'python', the dictionary may contain any Python objects. include: A list of fields to include in the output. exclude: A list of fields to exclude from the output. by_alias: Whether to use the field's alias in the dictionary key if defined. exclude_unset: Whether to exclude fields that are unset or None from the output. exclude_defaults: Whether to exclude fields that are set to their default value from the output. exclude_none: Whether to exclude fields that have a value of `None` from the output. simplify: Whether to consolidate fill and frame color/brightness if they are the same. Returns: A dictionary representation of the model. """ _dict = super().model_dump( include=include, exclude=exclude, by_alias=by_alias, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, exclude_none=exclude_none, ) if simplify: replace_keys = ["color", "brightness"] for key in replace_keys: fill = f"fill_{key}" frame = f"frame_{key}" if (fill in _dict and frame in _dict) and (_dict[fill] == _dict[frame]): _dict[key] = _dict.pop(f"frame_{key}") _dict.pop(f"fill_{key}") return _dict def __str__(self) -> str: """Returns a formatted view of properties and their values.""" return "LayerView:\n\t" + "\n\t".join( [f"{k}: {v}" for k, v in self.model_dump().items()] ) def __repr__(self) -> str: """Returns a formatted view of properties and their values.""" return self.__str__() def get_alpha(self) -> float: if not self.visible: return 0.0 if not self.transparent: dither_name = getattr(self.hatch_pattern, "name", self.hatch_pattern) if dither_name in ["I0", "solid"]: return 0.6 if dither_name in ["I1", "hollow"]: return 0.1 return 0.4 return 0.3 def get_color_dict(self) -> builtins.dict[str, str | None]: if self.fill_color is not None and self.frame_color is not None: return { "fill_color": ensure_six_digit_hex_color(self.fill_color.as_hex()), "frame_color": ensure_six_digit_hex_color(self.frame_color.as_hex()), } # Colors generated from here: http://phrogz.net/css/distinct-colors.html layer_colors = [ "#3dcc5c", "#2b0fff", "#cc3d3d", "#e5dd45", "#7b3dcc", "#cc860c", "#73ff0f", "#2dccb4", "#ff0fa3", "#0ec2e6", "#3d87cc", "#e5520e", ] if self.layer is not None: color = layer_colors[int(np.mod(self.layer[0], len(layer_colors)))] else: color = None return {"fill_color": color, "frame_color": color} def _build_klayout_xml_element( self, tag: str, name: str, custom_hatch_patterns: builtins.dict[str, HatchPattern], custom_line_styles: builtins.dict[str, LineStyle], ) -> ET.Element: """Get XML Element from attributes.""" # If hatch pattern name matches a named (built-in) KLayout pattern, use 'I<idx>' notation hatch_name = getattr(self.hatch_pattern, "name", self.hatch_pattern) if hatch_name is None: dither_pattern = None elif hatch_name in _klayout_dither_patterns: dither_pattern = f"I{list(_klayout_dither_patterns).index(str(hatch_name))}" elif hatch_name in custom_hatch_patterns: dither_pattern = f"C{list(custom_hatch_patterns).index(str(hatch_name))}" else: warnings.warn( f"Dither pattern {hatch_name!r} does not correspond to any KLayout built-in or custom pattern! Using 'I3' instead.", stacklevel=3, ) dither_pattern = "I3" # If line style name matches a named (built-in) KLayout pattern, use 'I<idx>' notation ls_name = getattr(self.line_style, "name", self.line_style) if ls_name is None: line_style = None elif ls_name in _klayout_line_styles: line_style = f"I{list(_klayout_line_styles).index(str(ls_name))}" elif ls_name in custom_line_styles: line_style = f"C{list(custom_line_styles).index(str(ls_name))}" else: warnings.warn( f"Line style {ls_name!r} does not correspond to any KLayout built-in or custom pattern! Using 'I3' instead.", stacklevel=3, ) line_style = "I3" frame_color = ( ensure_six_digit_hex_color(self.frame_color.as_hex()) if isinstance(self.frame_color, Color) else self.frame_color ) fill_color = ( ensure_six_digit_hex_color(self.fill_color.as_hex()) if isinstance(self.fill_color, Color) else self.fill_color ) prop_dict = { "frame-color": frame_color, "fill-color": fill_color, "frame-brightness": self.frame_brightness, "fill-brightness": self.fill_brightness, "dither-pattern": dither_pattern, "line-style": line_style, "valid": str(self.valid).lower(), "visible": str(self.visible).lower(), "transparent": str(self.transparent).lower(), "width": self.width, "marked": str(self.marked).lower(), "xfill": str(self.xfill).lower(), "animation": self.animation, "name": ( f"{name} {self.layer[0]}/{self.layer[1]}" if self.layer_in_name and self.layer is not None else name ), "source": ( f"{self.layer[0]}/{self.layer[1]}@1" if self.layer is not None else "*/*@*" ), } el = ET.Element(tag) for key, value in prop_dict.items(): subel = ET.SubElement(el, key) if value is None: continue if isinstance(value, bool): value = str(value).lower() subel.text = str(value) return el def to_klayout_xml( self, custom_hatch_patterns: builtins.dict[str, HatchPattern], custom_line_styles: builtins.dict[str, LineStyle], ) -> ET.Element: """Return an XML representation of the LayerView.""" props = self._build_klayout_xml_element( "properties", name=self.name or "", custom_hatch_patterns=custom_hatch_patterns, custom_line_styles=custom_line_styles, ) for member_name, member in self.group_members.items(): props.append( member._build_klayout_xml_element( "group-members", name=member_name, custom_hatch_patterns=custom_hatch_patterns, custom_line_styles=custom_line_styles, ) ) return props @staticmethod def _process_name( name: str, layer_pattern: str | re.Pattern[str], source: str | None = None ) -> tuple[str | None, bool | None]: """Strip layer info from name if it exists. Args: name: XML-formatted name entry. layer_pattern: Regex pattern to match layers with. source: Source field from XML that may contain layer name. """ # If name is empty but source exists, try to extract name from source if not name and source: # Extract name from source like "WG_COR 37/4@1" match = re.search(layer_pattern, source) if match: # Get the part before the layer pattern name = source[: match.start()].strip() if name: return clean_name(name, remove_dots=True), False return None, None if not name: return None, None layer_in_name = False match = re.search(layer_pattern, name) if match: name = (name[: match.start()] + name[match.end() :]).strip() layer_in_name = True return clean_name(name, remove_dots=True), layer_in_name @staticmethod def _process_layer( layer: str, layer_pattern: str | re.Pattern[str] ) -> Layer | None: """Convert .lyp XML layer entry to a Layer. Args: layer: XML-formatted layer entry. layer_pattern: Regex pattern to match layers with. """ match = re.search(layer_pattern, layer) if not match: raise OSError(f"Could not read layer {layer}!") v = match.group().split("/") return None if "*" in v else (int(v[0]), int(v[1])) @classmethod def from_xml_element( cls, element: ET.Element, layer_pattern: str | re.Pattern[str] ) -> LayerView | None: """Read properties from .lyp XML and generate LayerViews from them. Args: element: XML Element to iterate over. layer_pattern: Regex pattern to match layers with. """ element_name = element.find("name") element_source = element.find("source") source_text = element_source.text if element_source is not None else None name, layer_in_name = cls._process_name( element_name.text or "" if element_name is not None else "", layer_pattern, source_text, ) if name is None: return None hatch_pattern = element.find("dither-pattern").text # type: ignore[union-attr] # Translate KLayout index to hatch name if hatch_pattern and re.match(r"I\d+", hatch_pattern): hatch_pattern = list(_klayout_dither_patterns.keys())[ int(hatch_pattern[1:]) ] # Translate KLayout index to line style name line_style = element.find("line-style") if ( line_style is not None and line_style.text is not None and re.match(r"I\d+", line_style.text) ): line_style = list(_klayout_line_styles.keys())[int(line_style.text[1:])] # type: ignore[assignment] lv = LayerView( name=name, layer=cls._process_layer(element.find("source").text, layer_pattern), # type: ignore[union-attr,arg-type] fill_color=getattr(element.find("fill-color"), "text", None), frame_color=getattr(element.find("frame-color"), "text", None), fill_brightness=element.find("fill-brightness").text or 0, # type: ignore[union-attr] frame_brightness=element.find("frame-brightness").text or 0, # type: ignore[union-attr] hatch_pattern=hatch_pattern or None, line_style=line_style if line_style is not None and len(line_style) > 0 else None, valid=getattr(element.find("valid"), "text", True), visible=getattr(element.find("visible"), "text", True), transparent=getattr(element.find("transparent"), "text", False), width=getattr(element.find("width"), "text", None), marked=getattr(element.find("marked"), "text", False), xfill=getattr(element.find("xfill"), "text", False), animation=getattr(element.find("animation"), "text", False), layer_in_name=layer_in_name, ) # Add only if needed, so we can filter by defaults when dumping to yaml group_members: builtins.dict[str, LayerView] = {} for member in element.iterfind("group-members"): member_lv = cls.from_xml_element(member, layer_pattern) if member_lv and member_lv.name is not None: group_members[member_lv.name] = member_lv if group_members != {}: lv.group_members = group_members return lv
[docs] class LayerViews(BaseModel): """A container for layer properties for KLayout layer property (.lyp) files. Attributes: layer_views: Dictionary of LayerViews describing how to display gds layers. custom_dither_patterns: Custom dither patterns. custom_line_styles: Custom line styles. layers: Specify a layer_map to get the layer tuple based on the name of the LayerView, rather than the 'layer' argument. """ layer_views: dict[str, LayerView] = Field(default_factory=dict) custom_dither_patterns: dict[str, HatchPattern] = Field(default_factory=dict) custom_line_styles: dict[str, LineStyle] = Field(default_factory=dict) layers: type[LayerEnum] | None = None model_config = ConfigDict(extra="forbid", frozen=True)
[docs] def __init__( self, filepath: PathLike | None = None, layers: type[LayerEnum] | None = None, **data: Any, ) -> None: """Initialize LayerViews object. Args: filepath: can be YAML or LYP. layers: Optional layermap. data: Additional data to add to the LayerViews object. """ if filepath is not None: filepath = pathlib.Path(filepath) if filepath.suffix == ".lyp": lvs = LayerViews.from_lyp(filepath=filepath) logger.debug( f"Importing LayerViews from KLayout layer properties file: {str(filepath)!r}." ) elif filepath.suffix in {".yaml", ".yml"}: lvs = LayerViews.from_yaml(layer_file=filepath) logger.debug(f"Importing LayerViews from YAML file: {str(filepath)!r}.") else: raise ValueError(f"Unable to load LayerViews from {str(filepath)!r}.") data["layer_views"] = lvs.layer_views data["custom_line_styles"] = lvs.custom_line_styles data["custom_dither_patterns"] = lvs.custom_dither_patterns layer_names: builtins.dict[str, LayerEnum] | None = None if layers: layer_names = {layer.name: layer for layer in layers if layer is not None} # type: ignore[attr-defined] else: layer_names = None super().__init__(**data) for name in self.model_dump(): lv = getattr(self, name) if isinstance(lv, LayerView): # Auto-populate group_members from LayerView subclass fields if type(lv) is not LayerView and not lv.group_members: for field_name in type(lv).model_fields: field_val = getattr(lv, field_name) if isinstance(field_val, LayerView): lv.group_members[field_name] = field_val if ( layers is not None and layer_names is not None and name in layer_names ): lv_dict = lv.dict(exclude={"layer", "name"}) lv = LayerView(layer=layer_names[name], name=name, **lv_dict) self.add_layer_view(name=name, layer_view=lv)
def add_layer_view( self, name: str, layer_view: LayerView | None = None, **kwargs: Any ) -> None: """Adds a layer to LayerViews. Args: name: Name of the LayerView. layer_view: LayerView to add. kwargs: Additional arguments to pass to LayerView. """ if name in self.layer_views: raise ValueError( f"Adding {name!r} already defined {list(self.layer_views.keys())}" ) if layer_view is None: layer_view = LayerView(name=name, **kwargs) if layer_view.name is None: layer_view.name = name self.layer_views[name] = layer_view # If the dither pattern is a CustomDitherPattern, add it to custom_patterns dither_pattern = layer_view.hatch_pattern if isinstance(dither_pattern, HatchPattern) and ( dither_pattern.name is not None and dither_pattern.name not in self.custom_dither_patterns ): self.custom_dither_patterns[dither_pattern.name] = dither_pattern # If hatch_pattern is the name of a custom pattern, replace string with the CustomDitherPattern elif ( isinstance(dither_pattern, str) and dither_pattern in self.custom_dither_patterns ): layer_view.hatch_pattern = self.custom_dither_patterns[dither_pattern] line_style = layer_view.line_style if isinstance(line_style, LineStyle) and ( line_style.name is not None and line_style.name not in self.custom_line_styles ): self.custom_line_styles[line_style.name] = line_style elif isinstance(line_style, str) and line_style in self.custom_line_styles: layer_view.line_style = self.custom_line_styles[line_style] def get_layer_views(self, exclude_groups: bool = False) -> dict[str, LayerView]: """Return all LayerViews. Args: exclude_groups: Whether to exclude LayerViews that contain other LayerViews. """ layers: dict[str, LayerView] = {} for name, view in self.layer_views.items(): if view.group_members and not exclude_groups: layers.update(view.group_members.items()) layers[name] = view return layers def get_layer_view_groups(self) -> dict[str, LayerView]: """Return the LayerViews that contain other LayerViews.""" return {name: lv for name, lv in self.layer_views.items() if lv.group_members} def __str__(self) -> str: """Prints the number of LayerView objects in the LayerViews object.""" lvs = self.get_layer_views() groups = self.get_layer_view_groups() return ( f"LayerViews: {len(lvs)} layers ({len(groups)} groups)\n" f"\tCustomDitherPatterns: {list(self.custom_dither_patterns.keys())}\n" f"\tCustomLineStyles: {list(self.custom_line_styles.keys())}\n" ) def get(self, name: str) -> LayerView: """Returns Layer from name. Args: name: Name of layer. """ if name not in self.layer_views: raise ValueError(f"Layer {name!r} not in {list(self.layer_views.keys())}") return self.layer_views[name] def __getitem__(self, val: str) -> LayerView: """Allows accessing to the layer names like ls['gold2']. Args: val: Layer name to access within the LayerViews. Returns: self.layers[val]: LayerView in the LayerViews. """ try: return self.get_layer_views()[val] except Exception as error: raise ValueError( f"LayerView {val!r} not in LayerViews {list(self.layer_views.keys())}" ) from error def get_from_tuple(self, layer_tuple: tuple[int, int]) -> LayerView: """Returns LayerView from layer tuple. Args: layer_tuple: Tuple of (gds_layer, gds_datatype). Returns: LayerView. """ tuple_to_name = {v.layer: k for k, v in self.get_layer_views().items()} if layer_tuple not in tuple_to_name: raise ValueError( f"LayerView {layer_tuple} not in {list(tuple_to_name.keys())}" ) name = tuple_to_name[layer_tuple] return self.get_layer_views()[name] def get_layer_tuples(self) -> set[Layer]: """Returns a tuple for each layer.""" return { layer.layer for layer in self.get_layer_views().values() if layer.layer is not None } def clear(self) -> None: """Deletes all layers in the LayerViews.""" self.layer_views = {} def preview_layerset( self, size: float = 100.0, spacing: float = 100.0 ) -> Component: """Generates a Component with all the layers. Args: size: square size in um. spacing: spacing between each square in um. """ import gdsfactory as gf component = gf.Component() scale = size / 100 num_layers = len(self.get_layer_views()) matrix_size = int(np.ceil(np.sqrt(num_layers))) layer_views = self.get_layer_views() non_empty_layers = [v for v in layer_views.values() if v.layer is not None] sorted_layers = sorted( non_empty_layers, key=lambda x: (x.layer[0], x.layer[1]), # type: ignore[index] ) for n, layer in enumerate(sorted_layers): layer_tuple = layer.layer if layer_tuple is None: continue rectangle = gf.components.rectangle( size=(100 * scale, 100 * scale), layer=layer_tuple ) text = gf.components.text( text=f"{layer.name}\n{layer_tuple[0]} / {layer_tuple[1]}" if layer_tuple is not None else layer.name, size=20 * scale, position=(50 * scale, -20 * scale), justify="center", layer=layer_tuple, ) xloc = n % matrix_size yloc = int(n // matrix_size) ref = component.add_ref(rectangle) ref.movex((100 + spacing) * xloc * scale) ref.movey(-(100 + spacing) * yloc * scale) ref = component.add_ref(text) ref.movex((100 + spacing) * xloc * scale) ref.movey(-(100 + spacing) * yloc * scale) return component def to_lyp( self, filepath: str | pathlib.Path, overwrite: bool = True ) -> pathlib.Path: """Write all layer properties to a KLayout .lyp file. Args: filepath: to write the .lyp file to (appends .lyp extension if not present). overwrite: Whether to overwrite an existing file located at the filepath. """ filepath = pathlib.Path(filepath) dirpath = filepath.parent dirpath.mkdir(exist_ok=True, parents=True) if filepath.exists() and not overwrite: raise OSError(f"File {str(filepath)!r} exists, cannot write.") root = ET.Element("layer-properties") for lv in self.layer_views.values(): root.append( lv.to_klayout_xml( custom_hatch_patterns=self.custom_dither_patterns, custom_line_styles=self.custom_line_styles, ) ) for dp in self.custom_dither_patterns.values(): root.append(dp.to_klayout_xml()) for ls in self.custom_line_styles.values(): root.append(ls.to_klayout_xml()) filepath.write_bytes(make_pretty_xml(root)) return filepath @staticmethod def from_lyp( filepath: str | pathlib.Path, layer_pattern: str | re.Pattern[str] | None = None, ) -> LayerViews: r"""Write all layer properties to a KLayout .lyp file. Args: filepath: to write the .lyp file to (appends .lyp extension if not present). layer_pattern: Regex pattern to match layers with. Defaults to r'(\d+|\*)/(\d+|\*)'. """ layer_pattern = re.compile(layer_pattern or r"(\d+|\*)/(\d+|\*)") filepath = pathlib.Path(filepath) if not filepath.exists(): raise FileNotFoundError( f"File {str(filepath)!r} does not exist, cannot read." ) tree = ET.parse(filepath) root = tree.getroot() if root.tag != "layer-properties": raise OSError("Layer properties file incorrectly formatted, cannot read.") dither_patterns: dict[str, HatchPattern] = {} pattern_counter = 0 for dither_block in root.iter("custom-dither-pattern"): name_element = dither_block.find("name") name = name_element.text if name_element is not None else None order_element = dither_block.find("order") order = order_element.text if order_element is not None else None if order is None: continue # Generate a name if none exists if not name: name = f"custom_pattern_{pattern_counter}" pattern_counter += 1 assert name is not None # Type assertion for mypy pattern = "\n".join( [line.text for line in dither_block.find("pattern").iter()] # type: ignore[misc,union-attr] ) if name in dither_patterns: warnings.warn( f"Dither pattern named {name!r} already exists. Keeping only the first defined.", stacklevel=3, ) continue dither_patterns[name] = HatchPattern( name=name, order=int(order), custom_pattern=pattern.lstrip(), ) line_styles: dict[str, LineStyle] = {} style_counter = 0 for line_block in root.iter("custom-line-style"): name_element = line_block.find("name") name = name_element.text if name_element is not None else None order_element = line_block.find("order") order = order_element.text if order_element is not None else None if order is None: continue # Generate a name if none exists if not name: name = f"custom_style_{style_counter}" style_counter += 1 assert name is not None # Type assertion for mypy if name in line_styles: warnings.warn( f"Line style named {name!r} already exists. Keeping only the first defined.", stacklevel=3, ) continue pattern_element = line_block.find("pattern") line_pattern = pattern_element.text if pattern_element is not None else None line_styles[name] = LineStyle( name=name, order=int(order), custom_style=line_pattern, ) layer_views = {} for properties_element in root.iter("properties"): lv = LayerView.from_xml_element( properties_element, layer_pattern=layer_pattern ) if lv: hp = lv.hatch_pattern if isinstance(hp, str) and re.match(r"C\d+", hp): lv.hatch_pattern = list(dither_patterns.keys())[int(hp[1:])] layer_views[lv.name] = lv return LayerViews( layer_views=layer_views, custom_dither_patterns=dither_patterns, custom_line_styles=line_styles, ) def to_yaml(self, layer_file: str | pathlib.Path) -> None: """Export layer properties to a YAML file. Args: layer_file: Name of the file to write LayerViews to. """ lf_path = pathlib.Path(layer_file) dirpath = lf_path.parent dirpath.mkdir(exist_ok=True, parents=True) lvs = { name: lv.dict(exclude_none=True, exclude_defaults=True, exclude_unset=True) for name, lv in self.layer_views.items() } out_dict = {"LayerViews": lvs} if self.custom_dither_patterns: out_dict["CustomDitherPatterns"] = { name: dp.model_dump( exclude_none=True, exclude_defaults=True, exclude_unset=True ) for name, dp in self.custom_dither_patterns.items() } if self.custom_line_styles: out_dict["CustomLineStyles"] = { name: ls.model_dump( exclude_none=True, exclude_defaults=True, exclude_unset=True ) for name, ls in self.custom_line_styles.items() } lf_path.write_bytes( yaml.dump( out_dict, indent=2, sort_keys=False, default_flow_style=False, encoding="utf-8", Dumper=TechnologyDumper, ) ) @staticmethod def from_yaml(layer_file: str | pathlib.Path) -> LayerViews: """Import layer properties from two yaml files. Args: layer_file: Name of the file to read LayerViews, CustomDitherPatterns, and CustomLineStyles from. """ layer_file = pathlib.Path(layer_file) properties = yaml.safe_load(layer_file.read_text()) lvs = {} for name, lv in properties["LayerViews"].items(): if "group_members" in lv: lv["group_members"] = { member_name: LayerView(name=member_name, **member_view) for member_name, member_view in lv["group_members"].items() } lvs[name] = LayerView(name=name, **lv) custom_dither_patterns = ( { name: HatchPattern(name=name, **dp) for name, dp in properties["CustomDitherPatterns"].items() } if "CustomDitherPatterns" in properties else {} ) custom_line_styles = ( { name: LineStyle(name=name, **ls) for name, ls in properties["CustomLineStyles"].items() } if "CustomLineStyles" in properties else {} ) return LayerViews( layer_views=lvs, custom_dither_patterns=custom_dither_patterns, custom_line_styles=custom_line_styles, )
def test_load_lyp() -> None: """Test loading a KLayout layer properties.""" from gdsfactory.config import PATH lys = LayerViews(PATH.klayout_lyp) assert len(lys.layer_views) > 10, len(lys.layer_views)