diff --git a/CHANGELOG.md b/CHANGELOG.md index 61d487171e..76c6ae21ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Added configurable local simulation result caching with checksum validation, eviction limits, and per-call overrides across `web.run`, `web.load`, and job workflows. - Added `DirectivityMonitorSpec` for automated creation and configuration of directivity radiation monitors in `TerminalComponentModeler`. - Added multimode support to `WavePort` in the smatrix plugin, allowing multiple modes to be analyzed per port. +- Added support for `.lydrc` files for design rule checking in the `klayout` plugin. ### Breaking Changes - Edge singularity correction at PEC and lossy metal edges defaults to `True`. diff --git a/tests/test_plugins/klayout/drc/test_drc.py b/tests/test_plugins/klayout/drc/test_drc.py index a3466a5534..c73b96a6a0 100644 --- a/tests/test_plugins/klayout/drc/test_drc.py +++ b/tests/test_plugins/klayout/drc/test_drc.py @@ -53,6 +53,25 @@ def good_drcrunset_content(): report("DRC results", $resultsfile) """ + @staticmethod + def wrap_drc_to_lydrc(body: str): + """Return the XML-wrapped .lydrc runset content.""" + xml = f"""\ + + + Test DRC runset + + drc + + + + {body} + + + """ + + return xml + @staticmethod @pytest.fixture(scope="class") def bad_drcrunset_content_source(): @@ -147,15 +166,26 @@ def mock_run_drc_on_gds(config): ) @pytest.mark.parametrize("verbose", [True, False]) + @pytest.mark.parametrize("drc_file_suffix", [".drc", ".lydrc"]) def test_valid_run_on_gds( - self, monkeypatch, tmp_path, verbose, geom, geom_to_gds_kwargs, good_drcrunset_content + self, + monkeypatch, + tmp_path, + verbose, + geom, + geom_to_gds_kwargs, + good_drcrunset_content, + drc_file_suffix, ): """Test that no error is raised when runs on a gds are valid""" geom.to_gds_file(tmp_path / "test.gds", **geom_to_gds_kwargs) - self.write_drcrunset(tmp_path, "good_drcfile.drc", good_drcrunset_content) + drc_content = good_drcrunset_content + if drc_file_suffix == ".lydrc": + drc_content = TestDRCRunner.wrap_drc_to_lydrc(drc_content) + self.write_drcrunset(tmp_path, f"good_drcfile{drc_file_suffix}", drc_content) self.run( monkeypatch=monkeypatch, - drc_runsetfile=tmp_path / "good_drcfile.drc", + drc_runsetfile=tmp_path / f"good_drcfile{drc_file_suffix}", verbose=verbose, source=tmp_path / "test.gds", td_object_gds_savefile=tmp_path / "test.gds", @@ -163,6 +193,7 @@ def test_valid_run_on_gds( ) @pytest.mark.parametrize("verbose", [True, False]) + @pytest.mark.parametrize("drc_file_suffix", [".drc", ".lydrc"]) @pytest.mark.parametrize( "td_object, obj_to_gds_kwargs", [ @@ -180,12 +211,16 @@ def test_valid_run_on_td_object( td_object, obj_to_gds_kwargs, good_drcrunset_content, + drc_file_suffix, ): """Test that no error is raised when runs on a Geometry, Structure, or Simulation are valid""" - self.write_drcrunset(tmp_path, "good_drcfile.drc", good_drcrunset_content) + drc_content = good_drcrunset_content + if drc_file_suffix == ".lydrc": + drc_content = TestDRCRunner.wrap_drc_to_lydrc(drc_content) + self.write_drcrunset(tmp_path, f"good_drcfile{drc_file_suffix}", drc_content) self.run( monkeypatch=monkeypatch, - drc_runsetfile=tmp_path / "good_drcfile.drc", + drc_runsetfile=tmp_path / f"good_drcfile{drc_file_suffix}", verbose=verbose, source=request.getfixturevalue(td_object), td_object_gds_savefile=tmp_path / "test.gds", @@ -196,18 +231,27 @@ def test_valid_run_on_td_object( @pytest.mark.parametrize( "bad_drcrunset_content", ["bad_drcrunset_content_source", "bad_drcrunset_content_report"] ) + @pytest.mark.parametrize("drc_file_suffix", [".drc", ".lydrc"]) def test_check_drcfile_format_invalid( - self, request, monkeypatch, tmp_path, geom, geom_to_gds_kwargs, bad_drcrunset_content + self, + request, + monkeypatch, + tmp_path, + geom, + geom_to_gds_kwargs, + bad_drcrunset_content, + drc_file_suffix, ): """Tests that ValidationError is raised when the drc file content is invalid""" geom.to_gds_file(tmp_path / "test.gds", **geom_to_gds_kwargs) - self.write_drcrunset( - tmp_path, "bad_drcrunset.drc", request.getfixturevalue(bad_drcrunset_content) - ) + drc_content = request.getfixturevalue(bad_drcrunset_content) + if drc_file_suffix == ".lydrc": + drc_content = TestDRCRunner.wrap_drc_to_lydrc(drc_content) + self.write_drcrunset(tmp_path, f"bad_drcrunset{drc_file_suffix}", drc_content) with pytest.raises(pd.ValidationError) as e: self.run( monkeypatch=monkeypatch, - drc_runsetfile=tmp_path / "bad_drcrunset.drc", + drc_runsetfile=tmp_path / f"bad_drcrunset{drc_file_suffix}", verbose=True, source=tmp_path / "test.gds", td_object_gds_savefile=None, diff --git a/tidy3d/plugins/klayout/drc/drc.py b/tidy3d/plugins/klayout/drc/drc.py index a174323e93..3296446b58 100644 --- a/tidy3d/plugins/klayout/drc/drc.py +++ b/tidy3d/plugins/klayout/drc/drc.py @@ -24,6 +24,8 @@ from tidy3d.plugins.klayout.drc.results import DRCResults from tidy3d.plugins.klayout.util import check_installation +SUPPORTED_DRC_SUFFIXES: frozenset[str] = frozenset({".drc", ".lydrc"}) + class DRCConfig(Tidy3dBaseModel): """Configuration for KLayout DRC.""" @@ -54,9 +56,11 @@ def _validate_gdsfile_filetype(cls, v: pd.FilePath) -> pd.FilePath: @validator("drc_runset") def _validate_drc_runset_filetype(cls, v: pd.FilePath) -> pd.FilePath: - """Check DRC runset filetype is ``.drc``.""" - if v.suffix != ".drc": - raise ValidationError(f"DRC runset file '{v}' must end with '.drc'.") + """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 @validator("drc_runset")