"""Methods for integrating KLayout's DRC with Tidy3D."""
from __future__ import annotations
import re
from collections.abc import Mapping
from pathlib import Path
from subprocess import run
from typing import TYPE_CHECKING, Any
from pydantic import Field, FilePath, field_validator
from tidy3d.components.base import Tidy3dBaseModel
from tidy3d.components.geometry.base import Geometry
from tidy3d.components.simulation import Simulation
from tidy3d.components.structure import Structure
from tidy3d.exceptions import ValidationError
from tidy3d.log import get_logging_console
from tidy3d.plugins.klayout.drc.defaults import (
DEFAULT_GDSFILE,
DEFAULT_RESULTSFILE,
DEFAULT_VERBOSE,
)
from tidy3d.plugins.klayout.drc.results import DRCResults
from tidy3d.plugins.klayout.util import check_installation
if TYPE_CHECKING:
from typing import Optional, Union
SUPPORTED_DRC_SUFFIXES: frozenset[str] = frozenset({".drc", ".lydrc"})
[docs]
class DRCConfig(Tidy3dBaseModel):
"""Configuration for KLayout DRC."""
gdsfile: FilePath = Field(
title="GDS File",
description="The path to the GDS file to write the Tidy3D object to.",
)
drc_runset: FilePath = Field(
title="DRC Runset file",
description="Path to the KLayout DRC runset file.",
)
resultsfile: Path = Field(
title="DRC Results File",
description="Path to the KLayout DRC results file.",
)
verbose: bool = Field(
title="Verbose",
description="Whether to print logging.",
)
drc_args: dict[str, str] = Field(
default_factory=dict,
title="DRC File Arguments",
description="Optional key/value pairs forwarded to KLayout as -rd <key>=<value> definitions.",
)
@field_validator("gdsfile")
@classmethod
def _validate_gdsfile_filetype(cls, v: FilePath) -> FilePath:
"""Check GDS filetype is ``.gds``."""
if v.suffix != ".gds":
raise ValidationError(f"GDS file '{v}' must end with '.gds'.")
return v
@field_validator("drc_runset")
@classmethod
def _validate_drc_runset_filetype(cls, v: FilePath) -> FilePath:
"""Check DRC runset filetype is ``.drc`` or ``.lydrc``."""
if v.suffix not in SUPPORTED_DRC_SUFFIXES:
raise ValidationError(
f"DRC runset file '{v}' must end with one of {', '.join(SUPPORTED_DRC_SUFFIXES)}."
)
return v
@field_validator("drc_runset")
@classmethod
def _validate_drc_runset_format(cls, v: FilePath) -> FilePath:
"""Check if the DRC runset file is formatted correctly.
The checks are:
1. The GDS source must be loaded with 'source($gdsfile)'.
2. The report must be defined as 'report("<your string>", $resultsfile)'.
"""
with Path(v).open("r") as f:
content = f.read()
if not re.search(r"source\(\s*\$gdsfile\s*\)", content):
raise ValidationError(
"DRC runset is not formatted correctly. The GDS source must be loaded with 'source($gdsfile)'. Please refer to the documentation at 'tidy3d/plugins/klayout/drc/README.md' for more details."
)
if not re.search(r"""report\(['"](.*?)['"],\s*\$resultsfile\)""", content):
raise ValidationError(
"DRC runset is not formatted correctly. The report must be defined as 'report(\"<your report name>\", $resultsfile)'. Please refer to the documentation at 'tidy3d/plugins/klayout/drc/README.md' for more details."
)
return v
@field_validator("drc_args", mode="before")
@classmethod
def _validate_drc_args_stringable(cls, v: Any) -> dict[str, str]:
"""Coerce all keys and values in drc_args to strings."""
if v is None:
return {}
if not isinstance(v, Mapping):
raise ValidationError("drc_args must be a mapping of keys to values.")
try:
v = {str(k): str(v) for k, v in v.items()}
except Exception as e:
raise ValidationError("Could not coerce keys and values of drc_args to strings.") from e
return v
@field_validator("drc_args")
@classmethod
def _validate_drc_args_reserved(cls, v: dict[str, str]) -> dict[str, str]:
"""Ensure user arguments do not override the reserved keys."""
reserved_keys = {"gdsfile", "resultsfile"}
conflicts = reserved_keys.intersection(v)
if conflicts:
conflict_str = ", ".join(sorted(conflicts))
raise ValidationError(
f"Invalid DRC argument key(s) {conflict_str}: these names are reserved and automatically "
"managed by Tidy3D."
)
return v
[docs]
class DRCRunner(Tidy3dBaseModel):
"""A class for running KLayout DRC. Can be used to run DRC on a Tidy3D object or a GDS file.
Parameters
----------
drc_runset : Path
The path to the KLayout DRC runset file.
verbose : bool
Whether to print logging. Default is ``True``.
Example
-------
>>> # Running DRC on a GDS file:
>>> from tidy3d.plugins.klayout.drc import DRCRunner
>>> runner = DRCRunner(drc_runset="my_drc_runset.drc", verbose=True) # doctest: +SKIP
>>> results = runner.run(source="my_layout.gds", resultsfile="drc_results.lyrdb") # doctest: +SKIP
>>> print(results) # doctest: +SKIP
>>> # Running DRC on a Tidy3D object:
>>> import tidy3d as td
>>> from tidy3d.plugins.klayout.drc import DRCRunner
>>> vertices = [(-2, 0), (-1, 1), (0, 0.5), (1, 1), (2, 0), (0, -1)]
>>> geom = td.PolySlab(vertices=vertices, slab_bounds=(0, 0.22), axis=2)
>>> runner = DRCRunner(drc_runset="my_drc_runset.drc", verbose=True) # doctest: +SKIP
>>> results = runner.run(source=geom, td_object_gds_savefile="geom.gds", resultsfile="drc_results.lyrdb", z=0.1, gds_layer=0, gds_dtype=0) # doctest: +SKIP
>>> print(results) # doctest: +SKIP
"""
drc_runset: FilePath = Field(
title="DRC Runset file",
description="Path to the KLayout DRC runset file.",
)
verbose: bool = Field(
default=DEFAULT_VERBOSE,
title="Verbose",
description="Whether to print logging.",
)
[docs]
def run(
self,
source: Union[Geometry, Structure, Simulation, Path],
td_object_gds_savefile: Path = DEFAULT_GDSFILE,
resultsfile: Path = DEFAULT_RESULTSFILE,
drc_args: Optional[dict[str, str]] = None,
max_results: Optional[int] = None,
**to_gds_file_kwargs: Any,
) -> DRCResults:
"""Runs KLayout's DRC on a GDS file or a Tidy3D object. The Tidy3D object can be a
:class:`~tidy3d.Geometry`, :class:`~tidy3d.Structure`, or
:class:`~tidy3d.Simulation`.
Parameters
----------
source : Union[:class:`~tidy3d.Geometry`, :class:`~tidy3d.Structure`, :class:`~tidy3d.Simulation`, Path]
The :class:`~tidy3d.Geometry`, :class:`~tidy3d.Structure`,
:class:`~tidy3d.Simulation`, or GDS file to run DRC on.
td_object_gds_savefile : Path
The path to save the Tidy3D object to. Defaults to ``"layout.gds"``.
resultsfile : Path
The path to save the KLayout DRC results file to. Defaults to ``"drc_results.lyrdb"``.
drc_args : Optional[dict[str, str]] = None
Additional key/value pairs passed through to KLayout as ``-rd key=value`` CLI arguments.
max_results : Optional[int]
Maximum number of markers to load from the results file. ``None`` (default) loads all
markers.
**to_gds_file_kwargs
Additional keyword arguments to pass to the Tidy3D object-specific ``to_gds_file()`` method.
Returns
-------
:class:`.DRCResults`
The DRC results object containing violations and status.
Example
-------
Running DRC on a GDS file:
>>> from tidy3d.plugins.klayout.drc import DRCRunner
>>> runner = DRCRunner(drc_runset="my_drc_runset.drc", verbose=True) # doctest: +SKIP
>>> results = runner.run(source="my_layout.gds") # doctest: +SKIP
>>> print(results) # doctest: +SKIP
Running DRC on a Tidy3D object:
>>> import tidy3d as td
>>> from tidy3d.plugins.klayout.drc import DRCRunner
>>> vertices = [(-2, 0), (-1, 1), (0, 0.5), (1, 1), (2, 0), (0, -1)]
>>> geom = td.PolySlab(vertices=vertices, slab_bounds=(0, 0.22), axis=2)
>>> runner = DRCRunner(drc_runset="my_drc_runset.drc", verbose=True) # doctest: +SKIP
>>> results = runner.run(source=geom, z=0.1, gds_layer=0, gds_dtype=0) # doctest: +SKIP
>>> print(results) # doctest: +SKIP
"""
if isinstance(source, (Geometry, Structure, Simulation)):
gdsfile = td_object_gds_savefile
if self.verbose:
console = get_logging_console()
console.log(f"Writing Tidy3D object to GDS file '{gdsfile}'.")
source.to_gds_file(fname=gdsfile, **to_gds_file_kwargs)
else:
gdsfile = source
config = DRCConfig(
gdsfile=gdsfile,
drc_runset=self.drc_runset,
resultsfile=resultsfile,
verbose=self.verbose,
drc_args={} if drc_args is None else drc_args,
)
return run_drc_on_gds(
config=config,
max_results=max_results,
)
[docs]
def run_drc_on_gds(config: DRCConfig, max_results: Optional[int] = None) -> DRCResults:
"""Runs KLayout's DRC on a GDS file.
Parameters
----------
config : :class:`.DRCConfig`
The configuration for the DRC run.
max_results : Optional[int]
Maximum number of markers to load from the results file. ``None`` (default) loads all
markers.
Returns
-------
:class:`.DRCResults`
The DRC results object containing violations and status.
Example
-------
>>> from tidy3d.plugins.klayout.drc import run_drc_on_gds, DRCConfig
>>> config = DRCConfig(gdsfile="geom.gds", drc_runset="my_drc_runset.drc", resultsfile="drc_results.lyrdb", verbose=True) # doctest: +SKIP
>>> results = run_drc_on_gds(config) # doctest: +SKIP
>>> print(results) # doctest: +SKIP
"""
klayout_cmd = check_installation(raise_error=True)
if config.verbose:
console = get_logging_console()
console.log(
f"Running KLayout DRC on GDS file '{config.gdsfile}' with runset '{config.drc_runset}' and saving results to '{config.resultsfile}'..."
)
# run klayout DRC as a subprocess
cmd = [
klayout_cmd,
"-b",
"-r",
config.drc_runset,
"-rd",
f"gdsfile={config.gdsfile}",
"-rd",
f"resultsfile={config.resultsfile}",
]
for key, value in config.drc_args.items():
cmd.extend(["-rd", f"{key}={value}"])
output = run(cmd, capture_output=True)
if output.returncode != 0:
msg = output.stderr.decode(errors="replace")
raise RuntimeError(f"KLayout DRC failed with error message: '{msg}'.")
if config.verbose:
console.log("KLayout DRC completed successfully.")
return DRCResults.load(
resultsfile=config.resultsfile,
max_results=max_results,
)