Source code for tidy3d.plugins.klayout.drc.results

"""Methods for loading and parsing KLayout DRC results."""

from __future__ import annotations

import re
import xml.etree.ElementTree as ET
from pathlib import Path
from typing import Union

import pydantic.v1 as pd

from tidy3d.components.base import Tidy3dBaseModel, cached_property
from tidy3d.components.types import Coordinate2D
from tidy3d.exceptions import FileError

# Types for DRC markers
DRCEdge = tuple[Coordinate2D, Coordinate2D]
DRCEdgePair = tuple[DRCEdge, DRCEdge]
DRCPolygon = tuple[Coordinate2D, ...]
DRCMultiPolygon = tuple[DRCPolygon, ...]


def parse_edge(value: str) -> EdgeMarker:
    """
    Extract coordinates from edge format: ``(x1,y1;x2,y2)``.

    Parameters
    ----------
    value : str
        The edge value string from DRC result database, with format ``(x1,y1;x2,y2)``.

    Returns
    -------
    :class:`.EdgeMarker`
        :class:`.EdgeMarker` containing start and end points of the edge.

    Raises
    ------
    ValueError
        If the edge format is invalid.
    """
    # Extract coordinates from edge format: (x1,y1;x2,y2)
    pattern = r"\(([\d.-]+),([\d.-]+);([\d.-]+),([\d.-]+)\)"
    match = re.match(pattern, value)
    if match:
        coords = [float(x) for x in match.groups()]
        return EdgeMarker(edge=((coords[0], coords[1]), (coords[2], coords[3])))
    raise ValueError(f"Invalid edge format: '{value}'.")


def parse_edge_pair(value: str) -> EdgePairMarker:
    """
    Extract coordinates from edge-pair format: ``(x1,y1;x2,y2)|(x3,y3;x4,y4)``.

    Parameters
    ----------
    value : str
        The edge-pair value string from DRC result database, with format ``(x1,y1;x2,y2)|(x3,y3;x4,y4)``.

    Returns
    -------
    :class:`.EdgePairMarker`
        :class:`.EdgePairMarker` containing both edges' coordinates.

    Raises
    ------
    ValueError
        If the edge-pair format is invalid.
    """
    # Extract coordinates from edge-pair format: (x1,y1;x2,y2)|(x3,y3;x4,y4)
    pattern = (
        r"\(([\d.-]+),([\d.-]+);([\d.-]+),([\d.-]+)\)\|\(([\d.-]+),([\d.-]+);([\d.-]+),([\d.-]+)\)"
    )
    match = re.match(pattern, value)
    if match:
        coords = [float(x) for x in match.groups()]
        return EdgePairMarker(
            edge_pair=(
                ((coords[0], coords[1]), (coords[2], coords[3])),
                ((coords[4], coords[5]), (coords[6], coords[7])),
            )
        )
    raise ValueError(f"Invalid edge-pair format: '{value}'.")


def parse_polygon_coordinates(coords_str: str) -> DRCPolygon:
    """Parse coordinates for a single polygon into a tuple of (x, y) pairs.

    Parameters
    ----------
    coords_str : str
        The string of coordinates for a single polygon, with format ``x1,y1;x2,y2;...``.

    Returns
    -------
    DRCPolygon
        A tuple of (x, y) pairs for the polygon.

    Raises
    ------
    ValueError
        If the coordinate format is invalid.
    """
    coords_pattern = r"([\d.-]+),([\d.-]+)"  # match the coordinates

    # Split by semicolon to validate each coordinate pair
    coord_pairs = coords_str.split(";")

    # Each coordinate pair should match the pattern exactly
    coords = []
    for pair in coord_pairs:
        match = re.match(coords_pattern, pair)
        if not match:
            raise ValueError(f"Invalid coordinate pair in polygon: '{pair}'.")
        coords.append((float(match.group(1)), float(match.group(2))))

    return tuple(coords)


def parse_polygons(value: str) -> MultiPolygonMarker:
    """
    Extract coordinates from polygon format: ``(x1,y1;x2,y2;...)`` including multiple polygons separated by ``/``.

    Parameters
    ----------
    value : str
        The polygon value string from DRC result database, with format ``(x1,y1;x2,y2;...)``
        or multiple polygons separated by ``/`` like ``(x1,y1;.../x3,y3;...)``.

    Returns
    -------
    :class:`.MultiPolygonMarker`
        :class:`.MultiPolygonMarker` containing one or more polygon shapes.

    Raises
    ------
    ValueError
        If the polygon format is invalid or contains incomplete coordinate pairs.
    """
    # Extract the full content inside outer parentheses
    outer_pattern = r"\((.*)\)"
    match = re.match(outer_pattern, value)
    if not match:
        raise ValueError(f"Invalid polygon format: '{value}'.")

    coords_content = match.group(1)

    # Parse multiple polygons separated by '/'
    polygon_parts = coords_content.split("/")
    polygons = []
    for part in polygon_parts:
        polygons.append(parse_polygon_coordinates(part.strip()))

    return MultiPolygonMarker(polygons=tuple(polygons))


def parse_violation_value(value: str) -> Union[EdgeMarker, EdgePairMarker, MultiPolygonMarker]:
    """
    Parse a violation value based on its type (edge, edge-pair, or polygon).

    Parameters
    ----------
    value : str
        The value string from DRC result database.

    Returns
    -------
    Union[:class:`.EdgeMarker`, :class:`.EdgePairMarker`, :class:`.MultiPolygonMarker`]
        The parsed violation marker.

    Raises
    ------
    ValueError
        If the violation marker type is invalid.
    """
    if value.startswith("edge: "):
        return parse_edge(value=value.replace("edge: ", ""))
    elif value.startswith("edge-pair: "):
        return parse_edge_pair(value=value.replace("edge-pair: ", ""))
    elif value.startswith("polygon: "):
        return parse_polygons(value=value.replace("polygon: ", ""))
    raise ValueError(
        f"Invalid marker type (should start with 'edge:', 'edge-pair:', or 'polygon:'): '{value}'."
    )


class EdgeMarker(Tidy3dBaseModel):
    """A class for storing KLayout DRC edge marker results."""

    edge: DRCEdge = pd.Field(
        title="DRC Edge Marker",
        description="The edge marker of the DRC violation. The format is ((x1, y1), (x2, y2)).",
    )


class EdgePairMarker(Tidy3dBaseModel):
    """A class for storing KLayout DRC edge pair marker results."""

    edge_pair: DRCEdgePair = pd.Field(
        title="DRC Edge Pair Marker",
        description="The edge pair marker of the DRC violation. The format is (edge1, edge2), where an edge has format ((x1, y1), (x2, y2)).",
    )


class MultiPolygonMarker(Tidy3dBaseModel):
    """A class for storing KLayout DRC multi-polygon marker results."""

    polygons: DRCMultiPolygon = pd.Field(
        title="DRC Multi-Polygon Marker",
        description="The multi-polygon marker of the DRC violation. The format is (polygon1, polygon2, ...), where each polygon has format ((x1, y1), (x2, y2), ...).",
    )


DRCMarker = Union[EdgeMarker, EdgePairMarker, MultiPolygonMarker]


class DRCViolation(Tidy3dBaseModel):
    """A class for storing KLayout DRC violation results for a single category."""

    category: str = pd.Field(
        title="DRC Violation Category", description="The category of the DRC violation."
    )
    markers: tuple[DRCMarker, ...] = pd.Field(
        title="DRC Markers", description="Tuple of DRC markers in this category."
    )

    @cached_property
    def count(self) -> int:
        """The number of DRC markers in this category."""
        return len(self.markers)

    def __str__(self) -> str:
        """Get a nice string summary of the number of markers in this category."""
        return f"{self.category}: {self.count}"


[docs] class DRCResults(Tidy3dBaseModel): """A class for loading and storing KLayout DRC results.""" violations_by_category: dict[str, DRCViolation] = pd.Field( title="DRC Violations", description="Dictionary of DRC violations by category." ) @cached_property def is_clean(self) -> bool: """Whether the DRC is clean (no violations).""" return all(v.count == 0 for v in self.violations_by_category.values()) @cached_property def violation_counts(self) -> dict[str, int]: """Counts violations by category. Returns ------- dict[str, int] A dictionary of violation counts for each category. """ return { category: violation.count for category, violation in self.violations_by_category.items() } @cached_property def categories(self) -> tuple[str, ...]: """A tuple of all DRC categories.""" return tuple(self.violations_by_category.keys())
[docs] def __getitem__(self, category: str) -> DRCViolation: """Get DRC violation result by category. Parameters ---------- category : str The category of the DRC violation. Returns ------- :class:`.DRCViolation` The DRC violation result for the given category. """ return self.violations_by_category[category]
[docs] def __str__(self) -> str: """Get a nice string representation of the DRC results.""" summary = "DRC results summary\n" summary += "--------------------------------\n" summary += f"Total violations: {sum(violation.count for violation in self.violations_by_category.values())}\n\n" summary += "Violations by category:\n" for violation in self.violations_by_category.values(): summary += violation.__str__() + "\n" return summary
[docs] @classmethod def load(cls, resultsfile: Union[str, Path]) -> DRCResults: """Create a :class:`.DRCResults` instance from a results file. Parameters ---------- resultsfile : Union[str, Path] Path to the KLayout DRC results file. Returns ------- :class:`.DRCResults` A :class:`.DRCResults` object containing the DRC results. Raises ------ FileNotFoundError If the DRC result file is not found. ET.ParseError If the DRC result file is not a valid XML file. Example ------- >>> from tidy3d.plugins.klayout.drc import DRCResults >>> results = DRCResults.load(resultsfile="drc_results.lyrdb") # doctest: +SKIP >>> print(results) # doctest: +SKIP """ return cls(violations_by_category=violations_from_file(resultsfile=resultsfile))
def violations_from_file(resultsfile: Union[str, Path]) -> dict[str, DRCViolation]: """Loads a KLayout DRC results file and returns the results as a dictionary of :class:`.DRCViolation` objects. Parameters ---------- resultsfile : Union[str, Path] Path to the KLayout DRC results file. Returns ------- dict[str, :class:`.DRCViolation`] A dictionary of :class:`.DRCViolation` objects for each category. Raises ------ FileNotFoundError If the DRC result file is not found. ET.ParseError If the DRC result file is not a valid XML file. """ # Parse the results file try: xmltree = ET.parse(resultsfile) except FileNotFoundError as err: raise FileError(f"DRC result file not found: '{resultsfile}'.") from err except ET.ParseError as err: raise ET.ParseError(f"Invalid XML format in DRC result file: '{resultsfile}'.") from err # Initialize violations dict with all the categories violations = {} for category in xmltree.getroot().findall(".//categories/category/name"): violations[category.text] = DRCViolation(category=category.text, markers=()) # Parse markers for item in xmltree.getroot().findall(".//item"): category = item.find("category").text value = item.find("values/value").text marker = parse_violation_value(value) violations[category] = DRCViolation( category=category, markers=(*violations[category].markers, marker), ) return violations