Skip to content

Commit 3e3526a

Browse files
perf(tidy3d): FXC-4377-speed-up-klayout-results-loader
1 parent 25ecca3 commit 3e3526a

File tree

5 files changed

+169
-20
lines changed

5 files changed

+169
-20
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313
### Changed
1414
- Removed validator that would warn if `PerturbationMedium` values could become numerically unstable, since an error will anyway be raised if this actually happens when the medium is converted using actual perturbation data.
1515
- Improved performance of adjoint gradient computation for `PolySlab` and `Cylinder` geometries through vectorized sidewall patch collection (~4x speedup).
16+
- Added optional `max_results` handling to the kLayout `DRCLoader` and `DRCRunner` and speeded up parsing of big result sets.
1617

1718
### Fixed
1819

tests/test_plugins/klayout/drc/test_drc.py

Lines changed: 87 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,66 @@ def _basic_drc_config_kwargs(tmp_path: Path) -> dict[str, Path | bool]:
3535
}
3636

3737

38+
def _write_results_file(
39+
tmp_path: Path,
40+
*,
41+
category: str = "min_width",
42+
num_items: int = 1,
43+
filename: str = "many_results.lyrdb",
44+
) -> Path:
45+
"""Write a simple DRC results file with the requested number of items."""
46+
47+
template_header = f"""\
48+
<?xml version=\"1.0\" encoding=\"utf-8\"?>
49+
<report-database>
50+
<categories>
51+
<category>
52+
<name>{category}</name>
53+
<description>auto</description>
54+
<categories></categories>
55+
</category>
56+
</categories>
57+
<cells></cells>
58+
<items>
59+
"""
60+
template_footer = """\
61+
</items>
62+
</report-database>
63+
"""
64+
item = """\
65+
<item>
66+
<tags/>
67+
<category>{category}</category>
68+
<cell>TOP</cell>
69+
<visited>false</visited>
70+
<multiplicity>1</multiplicity>
71+
<comment/>
72+
<image/>
73+
<values>
74+
<value>edge: (0.0,0.0;1.0,1.0)</value>
75+
</values>
76+
</item>
77+
"""
78+
contents = [template_header]
79+
contents.extend(item.format(category=category) for _ in range(num_items))
80+
contents.append(template_footer)
81+
path = tmp_path / filename
82+
path.write_text("".join(contents))
83+
return path
84+
85+
86+
def _capture_log_warnings(monkeypatch):
87+
"""Capture calls to td.log.warning."""
88+
89+
messages = []
90+
91+
def fake_warning(message, *args, **kwargs):
92+
messages.append(message % args if args else message)
93+
94+
monkeypatch.setattr(td.log, "warning", fake_warning)
95+
return messages
96+
97+
3898
def test_check_klayout_not_installed(monkeypatch):
3999
"""check_installation raises when KLayout is not on PATH.
40100
@@ -62,7 +122,7 @@ def test_runner_passes_drc_args_to_config(monkeypatch, tmp_path):
62122
resultsfile = tmp_path / "results.lyrdb"
63123
captured_config = {}
64124

65-
def mock_run_drc_on_gds(config):
125+
def mock_run_drc_on_gds(config, **_kwargs):
66126
captured_config["config"] = config
67127
return DRCResults.load(filepath / "drc_results.lyrdb")
68128

@@ -101,7 +161,7 @@ def fake_run(cmd, capture_output):
101161
monkeypatch.setattr(f"{KLAYOUT_PLUGIN_PATH}.drc.drc.run", fake_run)
102162
monkeypatch.setattr(
103163
f"{KLAYOUT_PLUGIN_PATH}.drc.drc.DRCResults.load",
104-
lambda resultsfile: DRCResults(violations_by_category={}),
164+
lambda resultsfile, **_kwargs: DRCResults(violations_by_category={}),
105165
)
106166

107167
config = DRCConfig(
@@ -276,7 +336,7 @@ def run(
276336
"""Calls DRCRunner.run with dummy run_drc_on_gds()"""
277337

278338
# monkeypatch run_drc_on_gds() since the test machines do not have KLayout installed
279-
def mock_run_drc_on_gds(config):
339+
def mock_run_drc_on_gds(config, **_kwargs):
280340
return DRCResults.load(filepath / "drc_results.lyrdb")
281341

282342
monkeypatch.setattr(f"{KLAYOUT_PLUGIN_PATH}.drc.drc.run_drc_on_gds", mock_run_drc_on_gds)
@@ -608,3 +668,27 @@ def test_parse_violation_value_unknown_type(self):
608668
"""Test parsing unknown violation type."""
609669
with pytest.raises(ValueError):
610670
parse_violation_value("unknown: (1.0,2.0)")
671+
672+
def test_results_warn_without_limit(self, monkeypatch, tmp_path):
673+
"""Warn when no limit is set and a category exceeds the threshold."""
674+
675+
warnings = _capture_log_warnings(monkeypatch)
676+
monkeypatch.setattr(
677+
f"{KLAYOUT_PLUGIN_PATH}.drc.results.UNLIMITED_VIOLATION_WARNING_COUNT",
678+
3,
679+
)
680+
results_path = _write_results_file(tmp_path, num_items=4)
681+
results = DRCResults.load(results_path)
682+
assert results["min_width"].count == 4
683+
assert len(warnings) == 1
684+
assert "many markers (4)" in warnings[0]
685+
686+
def test_results_warn_when_limit_truncates(self, monkeypatch, tmp_path):
687+
"""Warn when the global limit removes markers."""
688+
689+
warnings = _capture_log_warnings(monkeypatch)
690+
results_path = _write_results_file(tmp_path, category="overflow", num_items=4)
691+
results = DRCResults.load(results_path, max_results=2)
692+
assert results["overflow"].count == 2
693+
assert len(warnings) == 1
694+
assert "only the first 2" in warnings[0]

tidy3d/plugins/klayout/drc/README.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ For a full quickstart example, please see [this quickstart notebook](https://git
1010

1111
- Run DRC on GDS files or Tidy3D objects ([Geometry](https://docs.flexcompute.com/projects/tidy3d/en/latest/api/_autosummary/tidy3d.Geometry.html), [Structure](https://docs.flexcompute.com/projects/tidy3d/en/latest/api/_autosummary/tidy3d.Structure.html#tidy3d.Structure), or [Simulation](https://docs.flexcompute.com/projects/tidy3d/en/latest/api/_autosummary/tidy3d.Simulation.html#tidy3d.Simulation)) with `DRCRunner.run()`.
1212
- Load DRC results into a `DRCResults` data structure with `DRCResults.load()`.
13+
- Limit how many violation markers are loaded by passing `max_results` to `DRCRunner.run()`,
14+
`run_drc_on_gds()`, or `DRCResults.load()`.
1315

1416
## Prerequisites
1517

@@ -131,4 +133,13 @@ Results can also be loaded from a KLayout DRC database file with `DRCResults.loa
131133
from tidy3d.plugins.klayout.drc import DRCResults
132134

133135
print(DRCResults.load("drc_results.lyrdb"))
134-
```
136+
```
137+
138+
### Limiting Loaded Results
139+
140+
Large designs can generate an enormous number of violations. Pass the optional `max_results`
141+
argument to `DRCRunner.run()`, `run_drc_on_gds()`, or `DRCResults.load()` to retain only the first
142+
`N` markers across all categories. When the option is not set, a warning is emitted if more than
143+
100,000 markers are present so you can set an appropriate limit. When the option is set and more
144+
total violations are present than the limit allows, a warning indicates that the results were
145+
truncated before parsing individual markers.

tidy3d/plugins/klayout/drc/drc.py

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -160,6 +160,7 @@ def run(
160160
td_object_gds_savefile: Path = DEFAULT_GDSFILE,
161161
resultsfile: Path = DEFAULT_RESULTSFILE,
162162
drc_args: Optional[dict[str, str]] = None,
163+
max_results: Optional[int] = None,
163164
**to_gds_file_kwargs: Any,
164165
) -> DRCResults:
165166
"""Runs KLayout's DRC on a GDS file or a Tidy3D object. The Tidy3D object can be a :class:`.Geometry`, :class:`.Structure`, or :class:`.Simulation`.
@@ -174,6 +175,9 @@ def run(
174175
The path to save the KLayout DRC results file to. Defaults to ``"drc_results.lyrdb"``.
175176
drc_args : Optional[dict[str, str]] = None
176177
Additional key/value pairs passed through to KLayout as ``-rd key=value`` CLI arguments.
178+
max_results : Optional[int]
179+
Maximum number of markers to load from the results file. ``None`` (default) loads all
180+
markers.
177181
**to_gds_file_kwargs
178182
Additional keyword arguments to pass to the Tidy3D object-specific ``to_gds_file()`` method.
179183
@@ -215,16 +219,22 @@ def run(
215219
verbose=self.verbose,
216220
drc_args={} if drc_args is None else drc_args,
217221
)
218-
return run_drc_on_gds(config=config)
222+
return run_drc_on_gds(
223+
config=config,
224+
max_results=max_results,
225+
)
219226

220227

221-
def run_drc_on_gds(config: DRCConfig) -> DRCResults:
228+
def run_drc_on_gds(config: DRCConfig, max_results: Optional[int] = None) -> DRCResults:
222229
"""Runs KLayout's DRC on a GDS file.
223230
224231
Parameters
225232
----------
226233
config : :class:`.DRCConfig`
227234
The configuration for the DRC run.
235+
max_results : Optional[int]
236+
Maximum number of markers to load from the results file. ``None`` (default) loads all
237+
markers.
228238
229239
Returns
230240
-------
@@ -267,4 +277,7 @@ def run_drc_on_gds(config: DRCConfig) -> DRCResults:
267277
if config.verbose:
268278
console.log("KLayout DRC completed successfully.")
269279

270-
return DRCResults.load(resultsfile=config.resultsfile)
280+
return DRCResults.load(
281+
resultsfile=config.resultsfile,
282+
max_results=max_results,
283+
)

tidy3d/plugins/klayout/drc/results.py

Lines changed: 53 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -5,20 +5,23 @@
55
import re
66
import xml.etree.ElementTree as ET
77
from pathlib import Path
8-
from typing import Union
8+
from typing import Optional, Union
99

1010
import pydantic.v1 as pd
1111

1212
from tidy3d.components.base import Tidy3dBaseModel, cached_property
1313
from tidy3d.components.types import Coordinate2D
1414
from tidy3d.exceptions import FileError
15+
from tidy3d.log import log
1516

1617
# Types for DRC markers
1718
DRCEdge = tuple[Coordinate2D, Coordinate2D]
1819
DRCEdgePair = tuple[DRCEdge, DRCEdge]
1920
DRCPolygon = tuple[Coordinate2D, ...]
2021
DRCMultiPolygon = tuple[DRCPolygon, ...]
2122

23+
UNLIMITED_VIOLATION_WARNING_COUNT = 100_000
24+
2225

2326
def parse_edge(value: str) -> EdgeMarker:
2427
"""
@@ -290,13 +293,20 @@ def __str__(self) -> str:
290293
return summary
291294

292295
@classmethod
293-
def load(cls, resultsfile: Union[str, Path]) -> DRCResults:
296+
def load(
297+
cls,
298+
resultsfile: Union[str, Path],
299+
max_results: Optional[int] = None,
300+
) -> DRCResults:
294301
"""Create a :class:`.DRCResults` instance from a results file.
295302
296303
Parameters
297304
----------
298305
resultsfile : Union[str, Path]
299306
Path to the KLayout DRC results file.
307+
max_results : Optional[int]
308+
Maximum number of markers to load from the file. If ``None`` (default), all markers are
309+
loaded.
300310
301311
Returns
302312
-------
@@ -316,16 +326,26 @@ def load(cls, resultsfile: Union[str, Path]) -> DRCResults:
316326
>>> results = DRCResults.load(resultsfile="drc_results.lyrdb") # doctest: +SKIP
317327
>>> print(results) # doctest: +SKIP
318328
"""
319-
return cls(violations_by_category=violations_from_file(resultsfile=resultsfile))
329+
violations = violations_from_file(
330+
resultsfile=resultsfile,
331+
max_results=max_results,
332+
)
333+
return cls.construct(violations_by_category=violations)
320334

321335

322-
def violations_from_file(resultsfile: Union[str, Path]) -> dict[str, DRCViolation]:
336+
def violations_from_file(
337+
resultsfile: Union[str, Path],
338+
max_results: Optional[int] = None,
339+
) -> dict[str, DRCViolation]:
323340
"""Loads a KLayout DRC results file and returns the results as a dictionary of :class:`.DRCViolation` objects.
324341
325342
Parameters
326343
----------
327344
resultsfile : Union[str, Path]
328345
Path to the KLayout DRC results file.
346+
max_results : Optional[int]
347+
Maximum number of markers to load from the file. If ``None`` (default), all markers are
348+
loaded.
329349
330350
Returns
331351
-------
@@ -339,6 +359,9 @@ def violations_from_file(resultsfile: Union[str, Path]) -> dict[str, DRCViolatio
339359
ET.ParseError
340360
If the DRC result file is not a valid XML file.
341361
"""
362+
if max_results is not None and max_results <= 0:
363+
raise ValueError("'max_results' must be a positive integer.")
364+
342365
# Parse the results file
343366
try:
344367
xmltree = ET.parse(resultsfile)
@@ -354,20 +377,37 @@ def violations_from_file(resultsfile: Union[str, Path]) -> dict[str, DRCViolatio
354377
if category_name is None:
355378
raise FileError("Encountered DRC category without a name in results file.")
356379
category_name = category_name.strip().strip("'\"")
357-
violations[category_name] = DRCViolation(category=category_name, markers=())
380+
violations[category_name] = []
381+
382+
# Prepare the items and warn if necessary
383+
items = list(xmltree.getroot().findall(".//item"))
384+
total_markers = len(items)
385+
if max_results is None and total_markers > UNLIMITED_VIOLATION_WARNING_COUNT:
386+
log.warning(
387+
f"DRC result file contains many markers ({total_markers}), "
388+
"which can affect loading performance. "
389+
"Pass 'max_results' to limit loaded results."
390+
)
391+
elif max_results is not None and total_markers > max_results:
392+
log.warning(
393+
f"DRC result file contains {total_markers} markers; "
394+
f"only the first {max_results} were loaded due to 'max_results'."
395+
)
358396

359397
# Parse markers
360-
for item in xmltree.getroot().findall(".//item"):
398+
for idx, item in enumerate(items):
399+
if max_results is not None and idx >= max_results:
400+
break
361401
category_el = item.find("category")
362402
if category_el is None or category_el.text is None:
363403
raise FileError("Encountered DRC item without a category in results file.")
364404
category = category_el.text.strip().strip("'\"")
365405
value = item.find("values/value").text
366406
marker = parse_violation_value(value)
367-
if category not in violations:
368-
violations[category] = DRCViolation(category=category, markers=())
369-
violations[category] = DRCViolation(
370-
category=category,
371-
markers=(*violations[category].markers, marker),
372-
)
373-
return violations
407+
markers = violations.setdefault(category, [])
408+
markers.append(marker)
409+
410+
return {
411+
category: DRCViolation(category=category, markers=tuple(markers))
412+
for category, markers in violations.items()
413+
}

0 commit comments

Comments
 (0)