gdsdiff 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (85) hide show
  1. gdsdiff/__init__.py +148 -0
  2. gdsdiff/__main__.py +7 -0
  3. gdsdiff/_environment.py +12 -0
  4. gdsdiff/_version.py +34 -0
  5. gdsdiff/acceptance_evidence.py +530 -0
  6. gdsdiff/api.py +2955 -0
  7. gdsdiff/backends/__init__.py +26 -0
  8. gdsdiff/backends/base.py +77 -0
  9. gdsdiff/backends/boundary_loop_ctypes.py +191 -0
  10. gdsdiff/backends/cpu.py +20 -0
  11. gdsdiff/backends/cpu_ctypes.py +255 -0
  12. gdsdiff/backends/cuda.py +40 -0
  13. gdsdiff/backends/cuda_ctypes.py +1297 -0
  14. gdsdiff/backends/exact.py +424 -0
  15. gdsdiff/backends/geometry_ctypes.py +252 -0
  16. gdsdiff/backends/identity.py +53 -0
  17. gdsdiff/backends/metal.py +198 -0
  18. gdsdiff/backends/metal_ctypes.py +866 -0
  19. gdsdiff/backends/polygon_extract_ctypes.py +233 -0
  20. gdsdiff/backends/structural_ctypes.py +130 -0
  21. gdsdiff/boundary_loops.py +616 -0
  22. gdsdiff/cache.py +544 -0
  23. gdsdiff/canonicalize.py +176 -0
  24. gdsdiff/cli.py +352 -0
  25. gdsdiff/config.py +257 -0
  26. gdsdiff/cuda_setup.py +174 -0
  27. gdsdiff/design_history.py +65 -0
  28. gdsdiff/diagnostics.py +42 -0
  29. gdsdiff/diff_gds.py +66 -0
  30. gdsdiff/exact_diff_markdown.py +979 -0
  31. gdsdiff/exact_oracle.py +649 -0
  32. gdsdiff/extract.py +376 -0
  33. gdsdiff/gds_metadata.py +43 -0
  34. gdsdiff/geometry.py +112 -0
  35. gdsdiff/grid.py +143 -0
  36. gdsdiff/hierarchy.py +215 -0
  37. gdsdiff/hierarchy_extract.py +263 -0
  38. gdsdiff/inventory.py +153 -0
  39. gdsdiff/multiprocessing_support.py +39 -0
  40. gdsdiff/native.py +135 -0
  41. gdsdiff/native_cache.py +265 -0
  42. gdsdiff/native_src/cpu_prefilter.cpp +349 -0
  43. gdsdiff/native_src/gds_boundary_loops.cpp +505 -0
  44. gdsdiff/native_src/gds_geometry_scan.cpp +601 -0
  45. gdsdiff/native_src/gds_polygon_extract.cpp +594 -0
  46. gdsdiff/native_src/gds_structural_scan.cpp +335 -0
  47. gdsdiff/native_src/gdsdiff/CMakeLists.txt +74 -0
  48. gdsdiff/native_src/gdsdiff/core.cpp +191 -0
  49. gdsdiff/native_src/gdsdiff/core.hpp +96 -0
  50. gdsdiff/native_src/gdsdiff/cuda_assignment.cu +304 -0
  51. gdsdiff/native_src/gdsdiff/cuda_assignment.cuh +33 -0
  52. gdsdiff/native_src/gdsdiff/cuda_candidates.cu +133 -0
  53. gdsdiff/native_src/gdsdiff/cuda_candidates.cuh +17 -0
  54. gdsdiff/native_src/gdsdiff/cuda_ctypes.cu +4528 -0
  55. gdsdiff/native_src/gdsdiff/cuda_memory.cu +194 -0
  56. gdsdiff/native_src/gdsdiff/cuda_memory.cuh +34 -0
  57. gdsdiff/native_src/gdsdiff/metal_ctypes.mm +2791 -0
  58. gdsdiff/native_src/gdsdiff/python_bindings.cpp +21 -0
  59. gdsdiff/native_src/gdsdiff/test_core.cpp +93 -0
  60. gdsdiff/native_src/gdsdiff/test_cuda_assignment.cu +109 -0
  61. gdsdiff/native_src/gdsdiff/test_cuda_candidates.cu +94 -0
  62. gdsdiff/native_src/gdsdiff/test_cuda_memory.cu +97 -0
  63. gdsdiff/policy.py +305 -0
  64. gdsdiff/polygon_components.py +860 -0
  65. gdsdiff/profiles.py +152 -0
  66. gdsdiff/py.typed +1 -0
  67. gdsdiff/rect_coverage.py +384 -0
  68. gdsdiff/report.py +354 -0
  69. gdsdiff/schemas/gdsdiff_report-v1.schema.json +92 -0
  70. gdsdiff/semantics.py +75 -0
  71. gdsdiff/source_edge_diff.py +1120 -0
  72. gdsdiff/spatial.py +71 -0
  73. gdsdiff/structural_guard.py +161 -0
  74. gdsdiff/surplus_coverage.py +255 -0
  75. gdsdiff/synthetic_suite.py +1643 -0
  76. gdsdiff/templates.py +709 -0
  77. gdsdiff/tiling.py +153 -0
  78. gdsdiff/windowed_exact.py +270 -0
  79. gdsdiff/windows.py +184 -0
  80. gdsdiff-0.1.0.dist-info/METADATA +135 -0
  81. gdsdiff-0.1.0.dist-info/RECORD +85 -0
  82. gdsdiff-0.1.0.dist-info/WHEEL +5 -0
  83. gdsdiff-0.1.0.dist-info/entry_points.txt +4 -0
  84. gdsdiff-0.1.0.dist-info/licenses/LICENSE +674 -0
  85. gdsdiff-0.1.0.dist-info/top_level.txt +1 -0
gdsdiff/policy.py ADDED
@@ -0,0 +1,305 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+
5
+ import gdstk
6
+
7
+ from .config import AllowedRegion, LayerSpec
8
+ from .exact_oracle import DiffFragment, ExactOracleResult, LayerExactDiff, format_twice_area
9
+ from .geometry import polygon_bbox, polygon_twice_area
10
+ from .grid import snap_to_grid
11
+ from .semantics import CompareStatus, DifferenceDirection, FailurePolicy
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class ClassifiedComponent:
16
+ layer: LayerSpec
17
+ direction: DifferenceDirection
18
+ classification: str
19
+ points: tuple[tuple[int, int], ...]
20
+ bbox: tuple[int, int, int, int]
21
+ twice_area: int
22
+
23
+ @property
24
+ def area_dbu2_string(self) -> str:
25
+ return format_twice_area(self.twice_area)
26
+
27
+ def to_json(self) -> dict[str, object]:
28
+ return {
29
+ "layer": self.layer.to_json(),
30
+ "direction": self.direction.value,
31
+ "classification": self.classification,
32
+ "bbox": list(self.bbox),
33
+ "area_dbu2": self.area_dbu2_string,
34
+ }
35
+
36
+
37
+ @dataclass(frozen=True)
38
+ class LayerPolicyResult:
39
+ layer: LayerSpec
40
+ expected_components: tuple[ClassifiedComponent, ...] = field(default_factory=tuple)
41
+ unexpected_components: tuple[ClassifiedComponent, ...] = field(default_factory=tuple)
42
+
43
+ @property
44
+ def expected_twice_area(self) -> int:
45
+ return sum(component.twice_area for component in self.expected_components)
46
+
47
+ @property
48
+ def unexpected_twice_area(self) -> int:
49
+ return sum(component.twice_area for component in self.unexpected_components)
50
+
51
+ @property
52
+ def components(self) -> tuple[ClassifiedComponent, ...]:
53
+ return tuple(sorted(self.expected_components + self.unexpected_components, key=_component_sort_key))
54
+
55
+
56
+ @dataclass(frozen=True)
57
+ class PolicyClassificationResult:
58
+ layers: tuple[LayerPolicyResult, ...]
59
+ effective_policy: dict[str, object]
60
+ failure_policy: FailurePolicy = FailurePolicy.FAIL_ON_UNEXPECTED
61
+
62
+ @property
63
+ def expected_twice_area(self) -> int:
64
+ return sum(layer.expected_twice_area for layer in self.layers)
65
+
66
+ @property
67
+ def unexpected_twice_area(self) -> int:
68
+ return sum(layer.unexpected_twice_area for layer in self.layers)
69
+
70
+ @property
71
+ def status(self) -> CompareStatus:
72
+ if self.unexpected_twice_area:
73
+ return CompareStatus.UNEXPECTED_DIFFERENCES
74
+ if self.expected_twice_area:
75
+ return CompareStatus.EXPECTED_DIFFERENCES_ONLY
76
+ return CompareStatus.EQUIVALENT
77
+
78
+ def to_json(self) -> dict[str, object]:
79
+ return {
80
+ "status": self.status.value,
81
+ "failure_policy": self.failure_policy.value,
82
+ "expected_area_dbu2": format_twice_area(self.expected_twice_area),
83
+ "unexpected_area_dbu2": format_twice_area(self.unexpected_twice_area),
84
+ "effective_policy": self.effective_policy,
85
+ "layers": [
86
+ {
87
+ "layer": layer.layer.to_json(),
88
+ "expected_area_dbu2": format_twice_area(layer.expected_twice_area),
89
+ "unexpected_area_dbu2": format_twice_area(layer.unexpected_twice_area),
90
+ "components": [component.to_json() for component in layer.components],
91
+ }
92
+ for layer in self.layers
93
+ ],
94
+ }
95
+
96
+
97
+ def classify_exact_differences(
98
+ exact_result: ExactOracleResult,
99
+ allowed_regions: tuple[AllowedRegion, ...] | list[AllowedRegion] = (),
100
+ *,
101
+ failure_policy: FailurePolicy | str = FailurePolicy.FAIL_ON_UNEXPECTED,
102
+ precision: float = 1.0,
103
+ ) -> PolicyClassificationResult:
104
+ failure_policy = FailurePolicy.coerce(failure_policy)
105
+ policy = _compile_policy(tuple(allowed_regions))
106
+ layer_results = []
107
+ for layer_diff in exact_result.layers:
108
+ expected: list[ClassifiedComponent] = []
109
+ unexpected: list[ClassifiedComponent] = []
110
+ _classify_fragments(
111
+ layer_diff.layer,
112
+ DifferenceDirection.REMOVAL,
113
+ layer_diff.old_minus_new,
114
+ policy,
115
+ expected,
116
+ unexpected,
117
+ precision=precision,
118
+ )
119
+ _classify_fragments(
120
+ layer_diff.layer,
121
+ DifferenceDirection.ADDITION,
122
+ layer_diff.new_minus_old,
123
+ policy,
124
+ expected,
125
+ unexpected,
126
+ precision=precision,
127
+ )
128
+ layer_results.append(
129
+ LayerPolicyResult(
130
+ layer=layer_diff.layer,
131
+ expected_components=tuple(sorted(expected, key=_component_sort_key)),
132
+ unexpected_components=tuple(sorted(unexpected, key=_component_sort_key)),
133
+ )
134
+ )
135
+ return PolicyClassificationResult(
136
+ layers=tuple(sorted(layer_results, key=lambda result: result.layer)),
137
+ effective_policy=_effective_policy_json(tuple(allowed_regions)),
138
+ failure_policy=failure_policy,
139
+ )
140
+
141
+
142
+ @dataclass(frozen=True)
143
+ class _CompiledPolicy:
144
+ bbox_regions: dict[tuple[LayerSpec, DifferenceDirection], tuple[AllowedRegion, ...]]
145
+ whole_layer: frozenset[tuple[LayerSpec, DifferenceDirection]]
146
+
147
+
148
+ def _compile_policy(allowed_regions: tuple[AllowedRegion, ...]) -> _CompiledPolicy:
149
+ bbox_regions: dict[tuple[LayerSpec, DifferenceDirection], list[AllowedRegion]] = {}
150
+ whole_layer: set[tuple[LayerSpec, DifferenceDirection]] = set()
151
+ for region in allowed_regions:
152
+ for layer in region.layers:
153
+ for direction in _region_directions(region.direction):
154
+ key = (layer, direction)
155
+ if region.whole_layer:
156
+ whole_layer.add(key)
157
+ else:
158
+ bbox_regions.setdefault(key, []).append(region)
159
+ return _CompiledPolicy(
160
+ bbox_regions={key: tuple(value) for key, value in bbox_regions.items()},
161
+ whole_layer=frozenset(whole_layer),
162
+ )
163
+
164
+
165
+ def _region_directions(direction: DifferenceDirection) -> tuple[DifferenceDirection, ...]:
166
+ if direction in {DifferenceDirection.BOTH, DifferenceDirection.XOR}:
167
+ return DifferenceDirection.ADDITION, DifferenceDirection.REMOVAL
168
+ if direction == DifferenceDirection.ADDITION:
169
+ return (DifferenceDirection.ADDITION,)
170
+ if direction == DifferenceDirection.REMOVAL:
171
+ return (DifferenceDirection.REMOVAL,)
172
+ raise ValueError(f"unsupported direction for allowed region: {direction}")
173
+
174
+
175
+ def _classify_fragments(
176
+ layer: LayerSpec,
177
+ direction: DifferenceDirection,
178
+ fragments: tuple[DiffFragment, ...],
179
+ policy: _CompiledPolicy,
180
+ expected: list[ClassifiedComponent],
181
+ unexpected: list[ClassifiedComponent],
182
+ *,
183
+ precision: float,
184
+ ) -> None:
185
+ if not fragments:
186
+ return
187
+ key = (layer, direction)
188
+ if key in policy.whole_layer:
189
+ expected.extend(_components_from_fragments(layer, direction, "expected", fragments))
190
+ return
191
+ regions = policy.bbox_regions.get(key, ())
192
+ for fragment in fragments:
193
+ if not regions:
194
+ unexpected.append(_component_from_fragment(layer, direction, "unexpected", fragment))
195
+ continue
196
+ fragment_expected, fragment_unexpected = _split_fragment_by_regions(
197
+ fragment,
198
+ regions,
199
+ layer,
200
+ precision=precision,
201
+ )
202
+ expected.extend(
203
+ _component_from_points(layer, direction, "expected", points)
204
+ for points in fragment_expected
205
+ )
206
+ unexpected.extend(
207
+ _component_from_points(layer, direction, "unexpected", points)
208
+ for points in fragment_unexpected
209
+ )
210
+
211
+
212
+ def _split_fragment_by_regions(
213
+ fragment: DiffFragment,
214
+ regions: tuple[AllowedRegion, ...],
215
+ layer: LayerSpec,
216
+ *,
217
+ precision: float,
218
+ ) -> tuple[tuple[tuple[tuple[int, int], ...], ...], tuple[tuple[tuple[int, int], ...], ...]]:
219
+ origin = fragment.bbox[0], fragment.bbox[1]
220
+ fragment_poly = _polygon_from_points(fragment.points, layer, origin)
221
+ masks = [_region_polygon(region, layer, origin) for region in regions]
222
+ allowed = gdstk.boolean([fragment_poly], masks, "and", precision=precision) or []
223
+ outside = gdstk.boolean([fragment_poly], masks, "not", precision=precision) or []
224
+ return _global_points_from_gdstk(allowed, origin), _global_points_from_gdstk(outside, origin)
225
+
226
+
227
+ def _polygon_from_points(points: tuple[tuple[int, int], ...], layer: LayerSpec, origin: tuple[int, int]) -> gdstk.Polygon:
228
+ ox, oy = origin
229
+ return gdstk.Polygon([(x - ox, y - oy) for x, y in points], layer=layer.layer, datatype=layer.datatype)
230
+
231
+
232
+ def _region_polygon(region: AllowedRegion, layer: LayerSpec, origin: tuple[int, int]) -> gdstk.Polygon:
233
+ if region.bbox is None:
234
+ raise ValueError("whole-layer regions should not be converted to bbox masks")
235
+ ox, oy = origin
236
+ x0, y0, x1, y1 = region.bbox
237
+ halo = region.halo_dbu
238
+ return gdstk.rectangle(
239
+ (x0 - halo - ox, y0 - halo - oy),
240
+ (x1 + halo - ox, y1 + halo - oy),
241
+ layer=layer.layer,
242
+ datatype=layer.datatype,
243
+ )
244
+
245
+
246
+ def _global_points_from_gdstk(polygons: list[gdstk.Polygon], origin: tuple[int, int]) -> tuple[tuple[tuple[int, int], ...], ...]:
247
+ ox, oy = origin
248
+ result = []
249
+ for polygon in polygons:
250
+ points = tuple((snap_to_grid(point[0], 1) + ox, snap_to_grid(point[1], 1) + oy) for point in polygon.points)
251
+ if len(points) >= 3 and polygon_twice_area(points):
252
+ result.append(points)
253
+ return tuple(result)
254
+
255
+
256
+ def _components_from_fragments(
257
+ layer: LayerSpec,
258
+ direction: DifferenceDirection,
259
+ classification: str,
260
+ fragments: tuple[DiffFragment, ...],
261
+ ) -> tuple[ClassifiedComponent, ...]:
262
+ return tuple(_component_from_fragment(layer, direction, classification, fragment) for fragment in fragments if fragment.twice_area)
263
+
264
+
265
+ def _component_from_fragment(
266
+ layer: LayerSpec,
267
+ direction: DifferenceDirection,
268
+ classification: str,
269
+ fragment: DiffFragment,
270
+ ) -> ClassifiedComponent:
271
+ return ClassifiedComponent(
272
+ layer=layer,
273
+ direction=direction,
274
+ classification=classification,
275
+ points=fragment.points,
276
+ bbox=fragment.bbox,
277
+ twice_area=fragment.twice_area,
278
+ )
279
+
280
+
281
+ def _component_from_points(
282
+ layer: LayerSpec,
283
+ direction: DifferenceDirection,
284
+ classification: str,
285
+ points: tuple[tuple[int, int], ...],
286
+ ) -> ClassifiedComponent:
287
+ return ClassifiedComponent(
288
+ layer=layer,
289
+ direction=direction,
290
+ classification=classification,
291
+ points=points,
292
+ bbox=polygon_bbox(points),
293
+ twice_area=polygon_twice_area(points),
294
+ )
295
+
296
+
297
+ def _component_sort_key(component: ClassifiedComponent) -> tuple[object, ...]:
298
+ return (component.layer, component.direction.value, component.classification, component.bbox, component.points)
299
+
300
+
301
+ def _effective_policy_json(allowed_regions: tuple[AllowedRegion, ...]) -> dict[str, object]:
302
+ return {
303
+ "allowed_region_count": len(allowed_regions),
304
+ "allowed_regions": [region.to_json() for region in allowed_regions],
305
+ }