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/report.py ADDED
@@ -0,0 +1,354 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ import tempfile
6
+ from dataclasses import dataclass, field
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ from .config import LayerSpec
11
+ from ._version import __version__
12
+ from .exact_oracle import ExactOracleResult, format_twice_area
13
+ from .policy import PolicyClassificationResult
14
+ from .semantics import CompareStatus, FailurePolicy, exit_code_for_status
15
+
16
+ REPORT_SCHEMA_VERSION = "gdsdiff-report-v1"
17
+ TOOL_VERSION = __version__
18
+
19
+
20
+ def _area_string(value: int | str) -> str:
21
+ if isinstance(value, int):
22
+ return str(value)
23
+ text = str(value)
24
+ if not text or text == "-":
25
+ raise ValueError("area string cannot be empty")
26
+ if text.startswith("-"):
27
+ body = text[1:]
28
+ else:
29
+ body = text
30
+ if body.endswith(".5"):
31
+ body = body[:-2]
32
+ if not body.isdigit():
33
+ raise ValueError(f"invalid exact area string: {value!r}")
34
+ return text
35
+
36
+
37
+ @dataclass(frozen=True)
38
+ class ComponentSummary:
39
+ direction: str
40
+ bbox: tuple[int, int, int, int]
41
+ area_dbu2: int | str
42
+ classification: str = "unclassified"
43
+
44
+ def to_json(self) -> dict[str, Any]:
45
+ return {
46
+ "direction": self.direction,
47
+ "bbox": list(self.bbox),
48
+ "area_dbu2": _area_string(self.area_dbu2),
49
+ "classification": self.classification,
50
+ }
51
+
52
+
53
+ @dataclass(frozen=True)
54
+ class LayerReport:
55
+ layer: LayerSpec
56
+ present_old: bool
57
+ present_new: bool
58
+ xor_area_dbu2: int | str = 0
59
+ old_minus_new_area_dbu2: int | str = 0
60
+ new_minus_old_area_dbu2: int | str = 0
61
+ expected_area_dbu2: int | str = 0
62
+ unexpected_area_dbu2: int | str = 0
63
+ polygon_count_old: int = 0
64
+ polygon_count_new: int = 0
65
+ candidate_tile_count: int = 0
66
+ exact_window_count: int = 0
67
+ exact_status: str = "not-run"
68
+ components: tuple[ComponentSummary, ...] = field(default_factory=tuple)
69
+ component_limit: int = 100
70
+
71
+ def to_json(self) -> dict[str, Any]:
72
+ shown = self.components[: self.component_limit]
73
+ return {
74
+ "layer": self.layer.layer,
75
+ "datatype": self.layer.datatype,
76
+ "present_old": self.present_old,
77
+ "present_new": self.present_new,
78
+ "polygon_count_old": self.polygon_count_old,
79
+ "polygon_count_new": self.polygon_count_new,
80
+ "candidate_tile_count": self.candidate_tile_count,
81
+ "exact_window_count": self.exact_window_count,
82
+ "xor_area_dbu2": _area_string(self.xor_area_dbu2),
83
+ "old_minus_new_area_dbu2": _area_string(self.old_minus_new_area_dbu2),
84
+ "new_minus_old_area_dbu2": _area_string(self.new_minus_old_area_dbu2),
85
+ "expected_area_dbu2": _area_string(self.expected_area_dbu2),
86
+ "unexpected_area_dbu2": _area_string(self.unexpected_area_dbu2),
87
+ "components": [component.to_json() for component in shown],
88
+ "truncated_component_count": max(0, len(self.components) - len(shown)),
89
+ "exact_status": self.exact_status,
90
+ }
91
+
92
+
93
+ @dataclass(frozen=True)
94
+ class CompareReport:
95
+ status: CompareStatus | str
96
+ exact_verification_complete: bool
97
+ old_input: dict[str, Any]
98
+ new_input: dict[str, Any]
99
+ semantics: dict[str, Any]
100
+ selection: dict[str, Any]
101
+ backend: dict[str, Any]
102
+ prefilter: dict[str, Any]
103
+ exact_fallback: dict[str, Any]
104
+ layers: tuple[LayerReport, ...] = field(default_factory=tuple)
105
+ allowed_policy: dict[str, Any] = field(default_factory=dict)
106
+ unexpected_summary: dict[str, Any] = field(default_factory=dict)
107
+ timings: dict[str, Any] = field(default_factory=dict)
108
+ memory: dict[str, Any] = field(default_factory=dict)
109
+ warnings: tuple[str, ...] = field(default_factory=tuple)
110
+ errors: tuple[str, ...] = field(default_factory=tuple)
111
+ artifacts: dict[str, Any] = field(default_factory=dict)
112
+ evidence_status: str | None = None
113
+ differences_found: bool | None = None
114
+ coverage_changed: bool | None = None
115
+ inventory_changed: bool | None = None
116
+ coverage_equal_but_inventory_changed: bool | None = None
117
+ coverage_component_count: int | None = None
118
+ inventory_delta_count: int | None = None
119
+ vertex_sidecar_path: str | None = None
120
+ unsupported_feature_count: int = 0
121
+ command: tuple[str, ...] = field(default_factory=tuple)
122
+ failure_policy: FailurePolicy | str = FailurePolicy.FAIL_ON_UNEXPECTED
123
+ tool_version: str = TOOL_VERSION
124
+
125
+ def to_json_dict(self) -> dict[str, Any]:
126
+ status = CompareStatus.coerce(self.status)
127
+ failure_policy = FailurePolicy.coerce(self.failure_policy)
128
+ coverage_changed = self.coverage_changed
129
+ if coverage_changed is None:
130
+ coverage_changed = any(
131
+ _area_string(layer.xor_area_dbu2) != "0"
132
+ for layer in self.layers
133
+ )
134
+ differences_found = self.differences_found
135
+ if differences_found is None:
136
+ differences_found = status in {
137
+ CompareStatus.DIFFERENCES_FOUND,
138
+ CompareStatus.EXPECTED_DIFFERENCES_ONLY,
139
+ CompareStatus.UNEXPECTED_DIFFERENCES,
140
+ }
141
+ report = {
142
+ "schema_version": REPORT_SCHEMA_VERSION,
143
+ "tool_version": self.tool_version,
144
+ "command": list(self.command),
145
+ "status": status.value,
146
+ "exit_code_category": exit_code_for_status(status, failure_policy),
147
+ "evidence_status": self.evidence_status or _default_evidence_status(status),
148
+ "differences_found": differences_found,
149
+ "coverage_changed": coverage_changed,
150
+ "inventory_changed": self.inventory_changed,
151
+ "coverage_equal_but_inventory_changed": self.coverage_equal_but_inventory_changed,
152
+ "coverage_component_count": self.coverage_component_count,
153
+ "inventory_delta_count": self.inventory_delta_count,
154
+ "vertex_sidecar_path": self.vertex_sidecar_path,
155
+ "unsupported_feature_count": self.unsupported_feature_count,
156
+ "exact_verification_complete": self.exact_verification_complete,
157
+ "old_input": self.old_input,
158
+ "new_input": self.new_input,
159
+ "semantics": self.semantics,
160
+ "selection": self.selection,
161
+ "backend": self.backend,
162
+ "prefilter": self.prefilter,
163
+ "exact_fallback": self.exact_fallback,
164
+ "layers": [layer.to_json() for layer in self.layers],
165
+ "allowed_policy": self.allowed_policy,
166
+ "unexpected_summary": self.unexpected_summary,
167
+ "timings": self.timings,
168
+ "memory": self.memory,
169
+ "warnings": list(self.warnings),
170
+ "errors": list(self.errors),
171
+ "artifacts": self.artifacts,
172
+ }
173
+ validate_report_dict(report)
174
+ return report
175
+
176
+ def to_json(self, *, indent: int | None = 2) -> str:
177
+ return json.dumps(self.to_json_dict(), indent=indent, sort_keys=True)
178
+
179
+ def write_json(self, path: str | Path, *, indent: int | None = 2) -> Path:
180
+ path = Path(path)
181
+ path.parent.mkdir(parents=True, exist_ok=True)
182
+ data = self.to_json(indent=indent)
183
+ fd, tmp_name = tempfile.mkstemp(prefix=f".{path.name}.", suffix=".tmp", dir=path.parent)
184
+ try:
185
+ with os.fdopen(fd, "w", encoding="utf-8") as handle:
186
+ handle.write(data)
187
+ handle.write("\n")
188
+ handle.flush()
189
+ os.fsync(handle.fileno())
190
+ os.replace(tmp_name, path)
191
+ except Exception:
192
+ raise
193
+ return path
194
+
195
+
196
+ def build_layer_reports(
197
+ exact_result: ExactOracleResult,
198
+ *,
199
+ policy_result: PolicyClassificationResult | None = None,
200
+ presence_by_layer: dict[LayerSpec, tuple[bool, bool]] | None = None,
201
+ polygon_counts_by_layer: dict[LayerSpec, tuple[int, int]] | None = None,
202
+ candidate_tile_counts_by_layer: dict[LayerSpec, int] | None = None,
203
+ exact_window_counts_by_layer: dict[LayerSpec, int] | None = None,
204
+ exact_status: str = "complete",
205
+ component_limit: int = 100,
206
+ ) -> tuple[LayerReport, ...]:
207
+ policy_by_layer = {layer.layer: layer for layer in policy_result.layers} if policy_result else {}
208
+ reports = []
209
+ for layer_diff in exact_result.layers:
210
+ layer = layer_diff.layer
211
+ present_old, present_new = (presence_by_layer or {}).get(layer, (False, False))
212
+ polygon_count_old, polygon_count_new = (polygon_counts_by_layer or {}).get(layer, (0, 0))
213
+ policy_layer = policy_by_layer.get(layer)
214
+ components = ()
215
+ expected_area = "0"
216
+ unexpected_area = "0"
217
+ if policy_layer is not None:
218
+ expected_area = format_twice_area(policy_layer.expected_twice_area)
219
+ unexpected_area = format_twice_area(policy_layer.unexpected_twice_area)
220
+ components = tuple(
221
+ ComponentSummary(
222
+ direction=component.direction.value,
223
+ bbox=component.bbox,
224
+ area_dbu2=component.area_dbu2_string,
225
+ classification=component.classification,
226
+ )
227
+ for component in policy_layer.components
228
+ )
229
+ reports.append(
230
+ LayerReport(
231
+ layer=layer,
232
+ present_old=present_old,
233
+ present_new=present_new,
234
+ xor_area_dbu2=format_twice_area(layer_diff.xor_twice_area),
235
+ old_minus_new_area_dbu2=format_twice_area(layer_diff.old_minus_new_twice_area),
236
+ new_minus_old_area_dbu2=format_twice_area(layer_diff.new_minus_old_twice_area),
237
+ expected_area_dbu2=expected_area,
238
+ unexpected_area_dbu2=unexpected_area,
239
+ polygon_count_old=polygon_count_old,
240
+ polygon_count_new=polygon_count_new,
241
+ candidate_tile_count=(candidate_tile_counts_by_layer or {}).get(layer, 0),
242
+ exact_window_count=(exact_window_counts_by_layer or {}).get(layer, 0),
243
+ exact_status=exact_status,
244
+ components=components,
245
+ component_limit=component_limit,
246
+ )
247
+ )
248
+ return tuple(sorted(reports, key=lambda report: report.layer))
249
+
250
+
251
+ def validate_report_dict(report: dict[str, Any]) -> None:
252
+ required = {
253
+ "schema_version",
254
+ "tool_version",
255
+ "status",
256
+ "exit_code_category",
257
+ "evidence_status",
258
+ "differences_found",
259
+ "coverage_changed",
260
+ "inventory_changed",
261
+ "coverage_equal_but_inventory_changed",
262
+ "coverage_component_count",
263
+ "inventory_delta_count",
264
+ "vertex_sidecar_path",
265
+ "unsupported_feature_count",
266
+ "exact_verification_complete",
267
+ "old_input",
268
+ "new_input",
269
+ "semantics",
270
+ "selection",
271
+ "backend",
272
+ "prefilter",
273
+ "exact_fallback",
274
+ "layers",
275
+ "allowed_policy",
276
+ "unexpected_summary",
277
+ "timings",
278
+ "memory",
279
+ "warnings",
280
+ "errors",
281
+ "artifacts",
282
+ }
283
+ missing = sorted(required - set(report))
284
+ if missing:
285
+ raise ValueError(f"report missing required fields: {', '.join(missing)}")
286
+ if report["schema_version"] != REPORT_SCHEMA_VERSION:
287
+ raise ValueError("unsupported report schema_version")
288
+ CompareStatus.coerce(report["status"])
289
+ if report["exit_code_category"] not in {0, 1, 2}:
290
+ raise ValueError("exit_code_category must be 0, 1, or 2")
291
+ if report["evidence_status"] not in {
292
+ "identical",
293
+ "differences-found",
294
+ "unsupported-input",
295
+ "incomplete-evidence",
296
+ "internal-mismatch",
297
+ }:
298
+ raise ValueError("invalid evidence_status")
299
+ for key in ("differences_found", "coverage_changed"):
300
+ if not isinstance(report[key], bool):
301
+ raise ValueError(f"{key} must be a boolean")
302
+ if not isinstance(report["exact_verification_complete"], bool):
303
+ raise ValueError("exact_verification_complete must be a boolean")
304
+ if not isinstance(report["layers"], list):
305
+ raise ValueError("layers must be a list")
306
+ for layer in report["layers"]:
307
+ _validate_layer_report(layer)
308
+
309
+
310
+ def _validate_layer_report(layer: dict[str, Any]) -> None:
311
+ required = {
312
+ "layer",
313
+ "datatype",
314
+ "present_old",
315
+ "present_new",
316
+ "xor_area_dbu2",
317
+ "old_minus_new_area_dbu2",
318
+ "new_minus_old_area_dbu2",
319
+ "expected_area_dbu2",
320
+ "unexpected_area_dbu2",
321
+ "exact_status",
322
+ }
323
+ missing = sorted(required - set(layer))
324
+ if missing:
325
+ raise ValueError(f"layer report missing required fields: {', '.join(missing)}")
326
+ LayerSpec(layer["layer"], layer["datatype"])
327
+ if not isinstance(layer["present_old"], bool) or not isinstance(layer["present_new"], bool):
328
+ raise ValueError("layer presence fields must be boolean")
329
+ for key in (
330
+ "xor_area_dbu2",
331
+ "old_minus_new_area_dbu2",
332
+ "new_minus_old_area_dbu2",
333
+ "expected_area_dbu2",
334
+ "unexpected_area_dbu2",
335
+ ):
336
+ _area_string(layer[key])
337
+
338
+
339
+ def _default_evidence_status(status: CompareStatus) -> str:
340
+ if status in {CompareStatus.IDENTICAL, CompareStatus.EQUIVALENT}:
341
+ return "identical"
342
+ if status in {
343
+ CompareStatus.DIFFERENCES_FOUND,
344
+ CompareStatus.EXPECTED_DIFFERENCES_ONLY,
345
+ CompareStatus.UNEXPECTED_DIFFERENCES,
346
+ }:
347
+ return "differences-found"
348
+ if status in {CompareStatus.INVALID_INPUT, CompareStatus.UNSUPPORTED_INPUT}:
349
+ return "unsupported-input"
350
+ if status in {CompareStatus.UNRESOLVED, CompareStatus.INCOMPLETE_EVIDENCE}:
351
+ return "incomplete-evidence"
352
+ if status == CompareStatus.INTERNAL_MISMATCH:
353
+ return "internal-mismatch"
354
+ return "incomplete-evidence"
@@ -0,0 +1,92 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "gdsdiff_report-v1.schema.json",
4
+ "title": "gdsdiff report v1",
5
+ "type": "object",
6
+ "required": [
7
+ "schema_version", "tool_version", "command", "status",
8
+ "exit_code_category", "evidence_status", "differences_found",
9
+ "coverage_changed", "inventory_changed",
10
+ "coverage_equal_but_inventory_changed", "coverage_component_count",
11
+ "inventory_delta_count", "vertex_sidecar_path",
12
+ "unsupported_feature_count", "exact_verification_complete", "old_input",
13
+ "new_input", "semantics", "selection", "backend", "prefilter",
14
+ "exact_fallback", "layers", "allowed_policy", "unexpected_summary",
15
+ "timings", "memory", "warnings", "errors", "artifacts"
16
+ ],
17
+ "properties": {
18
+ "schema_version": {"const": "gdsdiff-report-v1"},
19
+ "tool_version": {"type": "string"},
20
+ "command": {"type": "array", "items": {"type": "string"}},
21
+ "status": {
22
+ "enum": [
23
+ "identical", "differences-found", "unsupported-input",
24
+ "incomplete-evidence", "internal-mismatch", "equivalent",
25
+ "expected-differences-only", "unexpected-differences", "unresolved",
26
+ "invalid-input", "backend-error", "artifact-error"
27
+ ]
28
+ },
29
+ "exit_code_category": {"enum": [0, 1, 2]},
30
+ "evidence_status": {
31
+ "enum": ["identical", "differences-found", "unsupported-input", "incomplete-evidence", "internal-mismatch"]
32
+ },
33
+ "differences_found": {"type": "boolean"},
34
+ "coverage_changed": {"type": "boolean"},
35
+ "inventory_changed": {"type": ["boolean", "null"]},
36
+ "coverage_equal_but_inventory_changed": {"type": ["boolean", "null"]},
37
+ "coverage_component_count": {"type": ["integer", "null"], "minimum": 0},
38
+ "inventory_delta_count": {"type": ["integer", "null"], "minimum": 0},
39
+ "vertex_sidecar_path": {"type": ["string", "null"]},
40
+ "unsupported_feature_count": {"type": "integer", "minimum": 0},
41
+ "exact_verification_complete": {"type": "boolean"},
42
+ "old_input": {"type": "object"},
43
+ "new_input": {"type": "object"},
44
+ "semantics": {"type": "object"},
45
+ "selection": {"type": "object"},
46
+ "backend": {"type": "object"},
47
+ "prefilter": {"type": "object"},
48
+ "exact_fallback": {"type": "object"},
49
+ "layers": {
50
+ "type": "array",
51
+ "items": {
52
+ "type": "object",
53
+ "required": [
54
+ "layer", "datatype", "present_old", "present_new",
55
+ "polygon_count_old", "polygon_count_new", "candidate_tile_count",
56
+ "exact_window_count", "xor_area_dbu2", "old_minus_new_area_dbu2",
57
+ "new_minus_old_area_dbu2", "expected_area_dbu2",
58
+ "unexpected_area_dbu2", "components", "truncated_component_count",
59
+ "exact_status"
60
+ ],
61
+ "properties": {
62
+ "layer": {"type": "integer", "minimum": 0},
63
+ "datatype": {"type": "integer", "minimum": 0},
64
+ "present_old": {"type": "boolean"},
65
+ "present_new": {"type": "boolean"},
66
+ "polygon_count_old": {"type": "integer", "minimum": 0},
67
+ "polygon_count_new": {"type": "integer", "minimum": 0},
68
+ "candidate_tile_count": {"type": "integer", "minimum": 0},
69
+ "exact_window_count": {"type": "integer", "minimum": 0},
70
+ "xor_area_dbu2": {"$ref": "#/$defs/exactArea"},
71
+ "old_minus_new_area_dbu2": {"$ref": "#/$defs/exactArea"},
72
+ "new_minus_old_area_dbu2": {"$ref": "#/$defs/exactArea"},
73
+ "expected_area_dbu2": {"$ref": "#/$defs/exactArea"},
74
+ "unexpected_area_dbu2": {"$ref": "#/$defs/exactArea"},
75
+ "components": {"type": "array"},
76
+ "truncated_component_count": {"type": "integer", "minimum": 0},
77
+ "exact_status": {"type": "string"}
78
+ }
79
+ }
80
+ },
81
+ "allowed_policy": {"type": "object"},
82
+ "unexpected_summary": {"type": "object"},
83
+ "timings": {"type": "object"},
84
+ "memory": {"type": "object"},
85
+ "warnings": {"type": "array", "items": {"type": "string"}},
86
+ "errors": {"type": "array", "items": {"type": "string"}},
87
+ "artifacts": {"type": "object"}
88
+ },
89
+ "$defs": {
90
+ "exactArea": {"type": "string", "pattern": "^-?[0-9]+(\\.5)?$"}
91
+ }
92
+ }
gdsdiff/semantics.py ADDED
@@ -0,0 +1,75 @@
1
+ from __future__ import annotations
2
+
3
+ from enum import Enum
4
+
5
+
6
+ class _StringEnum(str, Enum):
7
+ @classmethod
8
+ def coerce(cls, value):
9
+ if isinstance(value, cls):
10
+ return value
11
+ try:
12
+ return cls(str(value))
13
+ except ValueError as exc:
14
+ allowed = ", ".join(item.value for item in cls)
15
+ raise ValueError(f"invalid {cls.__name__}: {value!r}; expected one of {allowed}") from exc
16
+
17
+
18
+ class Backend(_StringEnum):
19
+ AUTO = "auto"
20
+ CPU = "cpu"
21
+ CUDA = "cuda"
22
+ METAL = "metal"
23
+
24
+
25
+ class EvidenceEngine(_StringEnum):
26
+ AUTO = "auto"
27
+ GDSTK_ORACLE = "gdstk-oracle"
28
+ GPU_POLYGON = "gpu-polygon"
29
+
30
+
31
+ class PathPolicy(_StringEnum):
32
+ GDSTK = "gdstk"
33
+ REJECT = "reject"
34
+ REPORT_ONLY = "report-only"
35
+
36
+
37
+ class DifferenceDirection(_StringEnum):
38
+ BOTH = "both"
39
+ ADDITION = "addition"
40
+ REMOVAL = "removal"
41
+ XOR = "xor"
42
+
43
+
44
+ class FailurePolicy(_StringEnum):
45
+ FAIL_ON_UNEXPECTED = "fail-on-unexpected"
46
+ FAIL_ON_ANY_DIFF = "fail-on-any-diff"
47
+
48
+
49
+ class CompareStatus(_StringEnum):
50
+ IDENTICAL = "identical"
51
+ DIFFERENCES_FOUND = "differences-found"
52
+ UNSUPPORTED_INPUT = "unsupported-input"
53
+ INCOMPLETE_EVIDENCE = "incomplete-evidence"
54
+ INTERNAL_MISMATCH = "internal-mismatch"
55
+ EQUIVALENT = "equivalent"
56
+ EXPECTED_DIFFERENCES_ONLY = "expected-differences-only"
57
+ UNEXPECTED_DIFFERENCES = "unexpected-differences"
58
+ UNRESOLVED = "unresolved"
59
+ INVALID_INPUT = "invalid-input"
60
+ BACKEND_ERROR = "backend-error"
61
+ ARTIFACT_ERROR = "artifact-error"
62
+
63
+
64
+ def exit_code_for_status(status: CompareStatus | str, failure_policy: FailurePolicy | str = FailurePolicy.FAIL_ON_UNEXPECTED) -> int:
65
+ status = CompareStatus.coerce(status)
66
+ failure_policy = FailurePolicy.coerce(failure_policy)
67
+ if status in {CompareStatus.IDENTICAL, CompareStatus.EQUIVALENT}:
68
+ return 0
69
+ if status == CompareStatus.DIFFERENCES_FOUND:
70
+ return 1
71
+ if status == CompareStatus.EXPECTED_DIFFERENCES_ONLY:
72
+ return 1 if failure_policy == FailurePolicy.FAIL_ON_ANY_DIFF else 0
73
+ if status == CompareStatus.UNEXPECTED_DIFFERENCES:
74
+ return 1
75
+ return 2