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/cache.py ADDED
@@ -0,0 +1,544 @@
1
+ from __future__ import annotations
2
+
3
+ import hashlib
4
+ import json
5
+ import os
6
+ import tempfile
7
+ from dataclasses import dataclass
8
+ from decimal import Decimal
9
+ from pathlib import Path
10
+ from time import perf_counter
11
+ from typing import Iterable
12
+
13
+ import numpy as np
14
+
15
+ from .canonicalize import CanonicalRecord, InternResult, intern_polygons
16
+ from .config import CompareConfig, LayerSpec
17
+ from .backends.polygon_extract_ctypes import NativePolygonExtraction, extract_gds_polygons_native
18
+ from .exact_oracle import ExactOracleResult
19
+ from .extract import ExtractionResult, ExtractionStats, extract_gds_polygons
20
+ from .geometry import LayerTable, PolygonBuffer
21
+ from .native_cache import _exclusive_cache_lock
22
+ from .semantics import PathPolicy
23
+ from .spatial import SpatialIndex
24
+ from .tiling import CpuTilePrefilterResult
25
+ from .windows import CandidateWindow
26
+
27
+ CACHE_SCHEMA_VERSION = "gdsdiff-geometry-cache-v2"
28
+ NATIVE_POLYGON_CACHE_SCHEMA_VERSION = "gdsdiff-native-polygon-cache-v2"
29
+ PAIR_VERDICT_SCHEMA_VERSION = "gdsdiff-pair-verdict-cache-v3"
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class CachedGeometry:
34
+ extraction: ExtractionResult
35
+ intern: InternResult
36
+ spatial_index: SpatialIndex
37
+ cache_key: str
38
+ cache_hit: bool
39
+ load_s: float = 0.0
40
+ build_s: float = 0.0
41
+ write_s: float = 0.0
42
+
43
+ def to_timing_json(self, prefix: str) -> dict[str, float | bool | str]:
44
+ return {
45
+ f"{prefix}_geometry_cache_hit": self.cache_hit,
46
+ f"{prefix}_geometry_cache_load_s": self.load_s,
47
+ f"{prefix}_geometry_cache_build_s": self.build_s,
48
+ f"{prefix}_geometry_cache_write_s": self.write_s,
49
+ f"{prefix}_geometry_cache_key": self.cache_key,
50
+ }
51
+
52
+
53
+ @dataclass(frozen=True)
54
+ class CachedNativePolygonExtraction:
55
+ extraction: NativePolygonExtraction
56
+ cache_key: str
57
+ cache_hit: bool
58
+ load_s: float = 0.0
59
+ build_s: float = 0.0
60
+ write_s: float = 0.0
61
+
62
+ def to_timing_json(self, prefix: str) -> dict[str, float | bool | str]:
63
+ return {
64
+ f"{prefix}_native_polygon_cache_hit": self.cache_hit,
65
+ f"{prefix}_native_polygon_cache_load_s": self.load_s,
66
+ f"{prefix}_native_polygon_cache_build_s": self.build_s,
67
+ f"{prefix}_native_polygon_cache_write_s": self.write_s,
68
+ f"{prefix}_native_polygon_cache_key": self.cache_key,
69
+ }
70
+
71
+
72
+ def load_or_build_cached_geometry(
73
+ *,
74
+ cache_dir: str | Path,
75
+ path: str | Path,
76
+ top_name: str | None,
77
+ layers: str | LayerSpec | Iterable[str | tuple[int, int] | LayerSpec],
78
+ meters_per_dbu,
79
+ path_policy: PathPolicy | str,
80
+ ) -> CachedGeometry:
81
+ cache_dir = Path(cache_dir)
82
+ path = Path(path)
83
+ key = geometry_cache_key(
84
+ path=path,
85
+ top_name=top_name,
86
+ layers=layers,
87
+ meters_per_dbu=meters_per_dbu,
88
+ path_policy=path_policy,
89
+ )
90
+ manifest_path = cache_dir / f"{key}.json"
91
+ arrays_path = cache_dir / f"{key}.npz"
92
+ load_start = perf_counter()
93
+ cached = _read_array_cache(manifest_path, arrays_path, CACHE_SCHEMA_VERSION, key)
94
+ if cached is not None:
95
+ manifest, arrays = cached
96
+ try:
97
+ buffer = _buffer_from_cache(manifest, arrays)
98
+ extraction = ExtractionResult(
99
+ path=path,
100
+ top_name=str(manifest["top_name"]),
101
+ buffer=buffer,
102
+ stats=ExtractionStats(
103
+ raw_polygon_count=int(manifest["raw_polygon_count"]),
104
+ kept_polygon_count=int(manifest["kept_polygon_count"]),
105
+ vertex_count=int(manifest["vertex_count"]),
106
+ max_snap_error_dbu=Decimal(str(manifest["max_snap_error_dbu"])),
107
+ ),
108
+ )
109
+ intern = _intern_from_cache(
110
+ arrays,
111
+ duplicate_occurrence_count=int(manifest["duplicate_occurrence_count"]),
112
+ hash_collision_bucket_count=int(manifest["hash_collision_bucket_count"]),
113
+ )
114
+ spatial_index = _spatial_from_cache(buffer, arrays)
115
+ except (KeyError, TypeError, ValueError, IndexError):
116
+ cached = None
117
+ else:
118
+ load_s = perf_counter() - load_start
119
+ return CachedGeometry(
120
+ extraction=extraction,
121
+ intern=intern,
122
+ spatial_index=spatial_index,
123
+ cache_key=key,
124
+ cache_hit=True,
125
+ load_s=load_s,
126
+ )
127
+
128
+ build_start = perf_counter()
129
+ extraction = extract_gds_polygons(
130
+ path,
131
+ top_name=top_name,
132
+ layers=layers,
133
+ meters_per_dbu=meters_per_dbu,
134
+ path_policy=path_policy,
135
+ )
136
+ intern = intern_polygons(extraction.buffer)
137
+ spatial_index = SpatialIndex.from_buffer(extraction.buffer)
138
+ build_s = perf_counter() - build_start
139
+
140
+ write_start = perf_counter()
141
+ manifest = {
142
+ "schema_version": CACHE_SCHEMA_VERSION,
143
+ "cache_key": key,
144
+ "top_name": extraction.top_name,
145
+ "raw_polygon_count": extraction.stats.raw_polygon_count,
146
+ "kept_polygon_count": extraction.stats.kept_polygon_count,
147
+ "vertex_count": extraction.stats.vertex_count,
148
+ "max_snap_error_dbu": str(extraction.stats.max_snap_error_dbu),
149
+ "layers": extraction.buffer.layer_table.to_json(),
150
+ "polygon_hashes_present": extraction.buffer.polygon_hashes is not None,
151
+ "duplicate_occurrence_count": intern.duplicate_occurrence_count,
152
+ "hash_collision_bucket_count": intern.hash_collision_bucket_count,
153
+ }
154
+ arrays = _geometry_cache_arrays(extraction.buffer, intern)
155
+ _write_array_cache(manifest_path, arrays_path, manifest, arrays)
156
+ write_s = perf_counter() - write_start
157
+ return CachedGeometry(
158
+ extraction=extraction,
159
+ intern=intern,
160
+ spatial_index=spatial_index,
161
+ cache_key=key,
162
+ cache_hit=False,
163
+ build_s=build_s,
164
+ write_s=write_s,
165
+ )
166
+
167
+
168
+ def load_or_build_cached_native_polygons(
169
+ *,
170
+ cache_dir: str | Path,
171
+ path: str | Path,
172
+ top_name: str | None,
173
+ layers: str | LayerSpec | Iterable[str | tuple[int, int] | LayerSpec],
174
+ ) -> CachedNativePolygonExtraction:
175
+ cache_dir = Path(cache_dir)
176
+ path = Path(path)
177
+ key = native_polygon_cache_key(path=path, top_name=top_name, layers=layers)
178
+ manifest_path = cache_dir / "native_polygons" / f"{key}.json"
179
+ arrays_path = cache_dir / "native_polygons" / f"{key}.npz"
180
+ load_start = perf_counter()
181
+ cached = _read_array_cache(manifest_path, arrays_path, NATIVE_POLYGON_CACHE_SCHEMA_VERSION, key)
182
+ if cached is not None:
183
+ manifest, arrays = cached
184
+ try:
185
+ extraction = NativePolygonExtraction(
186
+ path=path,
187
+ top_name=manifest.get("top_name"),
188
+ buffer=_buffer_from_cache(manifest, arrays),
189
+ native_parse_s=manifest.get("native_parse_s"),
190
+ )
191
+ except (KeyError, TypeError, ValueError, IndexError):
192
+ cached = None
193
+ else:
194
+ load_s = perf_counter() - load_start
195
+ return CachedNativePolygonExtraction(
196
+ extraction=extraction,
197
+ cache_key=key,
198
+ cache_hit=True,
199
+ load_s=load_s,
200
+ )
201
+
202
+ build_start = perf_counter()
203
+ extraction = extract_gds_polygons_native(path, top_name=top_name, layers=layers)
204
+ build_s = perf_counter() - build_start
205
+
206
+ write_start = perf_counter()
207
+ manifest = {
208
+ "schema_version": NATIVE_POLYGON_CACHE_SCHEMA_VERSION,
209
+ "cache_key": key,
210
+ "top_name": extraction.top_name,
211
+ "native_parse_s": extraction.native_parse_s,
212
+ "layers": extraction.buffer.layer_table.to_json(),
213
+ "polygon_hashes_present": extraction.buffer.polygon_hashes is not None,
214
+ }
215
+ _write_array_cache(manifest_path, arrays_path, manifest, _buffer_cache_arrays(extraction.buffer))
216
+ write_s = perf_counter() - write_start
217
+ return CachedNativePolygonExtraction(
218
+ extraction=extraction,
219
+ cache_key=key,
220
+ cache_hit=False,
221
+ build_s=build_s,
222
+ write_s=write_s,
223
+ )
224
+
225
+
226
+ def geometry_cache_key(
227
+ *,
228
+ path: Path,
229
+ top_name: str | None,
230
+ layers,
231
+ meters_per_dbu,
232
+ path_policy,
233
+ ) -> str:
234
+ stat = path.stat()
235
+ digest = hashlib.sha256()
236
+ with path.open("rb") as handle:
237
+ for chunk in iter(lambda: handle.read(1024 * 1024), b""):
238
+ digest.update(chunk)
239
+ metadata = {
240
+ "schema_version": CACHE_SCHEMA_VERSION,
241
+ "path_sha256": digest.hexdigest(),
242
+ "path_size": stat.st_size,
243
+ "top_name": top_name,
244
+ "layers": _jsonable_layers(layers),
245
+ "meters_per_dbu": str(meters_per_dbu),
246
+ "path_policy": str(PathPolicy.coerce(path_policy).value),
247
+ }
248
+ return hashlib.sha256(json.dumps(metadata, sort_keys=True).encode("utf-8")).hexdigest()
249
+
250
+
251
+ def native_polygon_cache_key(
252
+ *,
253
+ path: Path,
254
+ top_name: str | None,
255
+ layers,
256
+ ) -> str:
257
+ stat = path.stat()
258
+ digest = hashlib.sha256()
259
+ with path.open("rb") as handle:
260
+ for chunk in iter(lambda: handle.read(1024 * 1024), b""):
261
+ digest.update(chunk)
262
+ metadata = {
263
+ "schema_version": NATIVE_POLYGON_CACHE_SCHEMA_VERSION,
264
+ "path_sha256": digest.hexdigest(),
265
+ "path_size": stat.st_size,
266
+ "top_name": top_name,
267
+ "layers": _jsonable_layers(layers),
268
+ }
269
+ return hashlib.sha256(json.dumps(metadata, sort_keys=True).encode("utf-8")).hexdigest()
270
+
271
+
272
+ def _jsonable_layers(layers):
273
+ if layers == "all":
274
+ return "all"
275
+ return [LayerSpec.parse(layer).to_json() for layer in layers]
276
+
277
+
278
+ @dataclass(frozen=True)
279
+ class CachedPairVerdict:
280
+ fast_status: str
281
+ exact_fallback: dict[str, object]
282
+ exact_verification_complete: bool
283
+ layer_exact_status: str
284
+ warnings: tuple[str, ...]
285
+ cache_key: str
286
+ load_s: float
287
+
288
+
289
+ def load_cached_pair_verdict(config: CompareConfig, *, old_top_name: str | None, new_top_name: str | None) -> CachedPairVerdict | None:
290
+ if config.geometry_cache_dir is None:
291
+ return None
292
+ key = pair_verdict_cache_key(config, old_top_name=old_top_name, new_top_name=new_top_name)
293
+ path = Path(config.geometry_cache_dir) / "pair_verdicts" / f"{key}.json"
294
+ if not path.exists():
295
+ return None
296
+ start = perf_counter()
297
+ try:
298
+ payload = json.loads(path.read_text(encoding="utf-8"))
299
+ except (OSError, UnicodeError, json.JSONDecodeError):
300
+ return None
301
+ load_s = perf_counter() - start
302
+ if payload.get("schema_version") != PAIR_VERDICT_SCHEMA_VERSION or payload.get("cache_key") != key:
303
+ return None
304
+ return CachedPairVerdict(
305
+ fast_status=str(payload["fast_status"]),
306
+ exact_fallback=payload["exact_fallback"],
307
+ exact_verification_complete=bool(payload["exact_verification_complete"]),
308
+ layer_exact_status=str(payload["layer_exact_status"]),
309
+ warnings=tuple(payload["warnings"]),
310
+ cache_key=key,
311
+ load_s=load_s,
312
+ )
313
+
314
+
315
+ def write_cached_pair_verdict(
316
+ config: CompareConfig,
317
+ *,
318
+ old_top_name: str | None,
319
+ new_top_name: str | None,
320
+ exact_result: ExactOracleResult,
321
+ prefilter: CpuTilePrefilterResult,
322
+ windows: tuple[CandidateWindow, ...],
323
+ exact_fallback: dict[str, object],
324
+ fast_status: str,
325
+ exact_verification_complete: bool,
326
+ layer_exact_status: str,
327
+ warnings: tuple[str, ...],
328
+ ) -> str | None:
329
+ if config.geometry_cache_dir is None:
330
+ return None
331
+ key = pair_verdict_cache_key(config, old_top_name=old_top_name, new_top_name=new_top_name)
332
+ cache_dir = Path(config.geometry_cache_dir) / "pair_verdicts"
333
+ cache_dir.mkdir(parents=True, exist_ok=True)
334
+ path = cache_dir / f"{key}.json"
335
+ _atomic_write_json(
336
+ path,
337
+ {
338
+ "schema_version": PAIR_VERDICT_SCHEMA_VERSION,
339
+ "cache_key": key,
340
+ "fast_status": fast_status,
341
+ "exact_fallback": exact_fallback,
342
+ "exact_verification_complete": exact_verification_complete,
343
+ "layer_exact_status": layer_exact_status,
344
+ "warnings": list(warnings),
345
+ },
346
+ )
347
+ return key
348
+
349
+
350
+ def pair_verdict_cache_key(config: CompareConfig, *, old_top_name: str | None, new_top_name: str | None) -> str:
351
+ old_key = geometry_cache_key(
352
+ path=Path(config.old_path),
353
+ top_name=old_top_name,
354
+ layers=config.layers,
355
+ meters_per_dbu=config.dbu,
356
+ path_policy=config.path_policy,
357
+ )
358
+ new_key = geometry_cache_key(
359
+ path=Path(config.new_path),
360
+ top_name=new_top_name,
361
+ layers=config.layers,
362
+ meters_per_dbu=config.dbu,
363
+ path_policy=config.path_policy,
364
+ )
365
+ metadata = {
366
+ "schema_version": PAIR_VERDICT_SCHEMA_VERSION,
367
+ "old_geometry_key": old_key,
368
+ "new_geometry_key": new_key,
369
+ "tile_size": config.tile_size,
370
+ "layers": _jsonable_layers(config.layers),
371
+ "allowed_regions": [region.to_json() for region in config.allowed_regions],
372
+ "failure_policy": config.failure_policy.value,
373
+ }
374
+ return hashlib.sha256(json.dumps(metadata, sort_keys=True).encode("utf-8")).hexdigest()
375
+
376
+
377
+ def _buffer_cache_arrays(buffer: PolygonBuffer) -> dict[str, np.ndarray]:
378
+ return {
379
+ "vertices_xy": buffer.vertices_xy,
380
+ "polygon_offsets": buffer.polygon_offsets,
381
+ "layer_ids": buffer.layer_ids,
382
+ "bboxes": buffer.bboxes,
383
+ "flags": buffer.flags,
384
+ "source_ids": buffer.source_ids,
385
+ "polygon_hashes": (
386
+ buffer.polygon_hashes if buffer.polygon_hashes is not None else np.empty((0, 2), dtype=np.uint64)
387
+ ),
388
+ }
389
+
390
+
391
+ def _geometry_cache_arrays(buffer: PolygonBuffer, intern: InternResult) -> dict[str, np.ndarray]:
392
+ arrays = _buffer_cache_arrays(buffer)
393
+ blob_offsets = [0]
394
+ blob_bytes = bytearray()
395
+ for blob in intern.unique_blobs:
396
+ blob_bytes.extend(blob)
397
+ blob_offsets.append(len(blob_bytes))
398
+ arrays.update(
399
+ {
400
+ "canonical_ids": intern.canonical_ids,
401
+ "unique_blob_bytes": np.frombuffer(bytes(blob_bytes), dtype=np.uint8),
402
+ "unique_blob_offsets": np.asarray(blob_offsets, dtype=np.uint64),
403
+ "record_polygon_indices": np.asarray([record.polygon_index for record in intern.records], dtype=np.uint64),
404
+ "record_digests": np.frombuffer(b"".join(record.digest128 for record in intern.records), dtype=np.uint8).reshape((-1, 16)),
405
+ "record_validity": np.asarray([record.validity_class for record in intern.records], dtype=np.uint8),
406
+ "spatial_order": np.lexsort(
407
+ (
408
+ np.arange(buffer.polygon_count, dtype=np.int64),
409
+ buffer.bboxes[:, 3],
410
+ buffer.bboxes[:, 2],
411
+ buffer.bboxes[:, 1],
412
+ buffer.bboxes[:, 0],
413
+ buffer.layer_ids,
414
+ )
415
+ ).astype(np.int64, copy=False),
416
+ }
417
+ )
418
+ return arrays
419
+
420
+
421
+ def _buffer_from_cache(manifest: dict[str, object], arrays: dict[str, np.ndarray]) -> PolygonBuffer:
422
+ layer_table = LayerTable(
423
+ tuple(LayerSpec(int(item["layer"]), int(item["datatype"])) for item in manifest["layers"])
424
+ )
425
+ polygon_hashes = arrays["polygon_hashes"] if manifest.get("polygon_hashes_present") else None
426
+ return PolygonBuffer(
427
+ vertices_xy=np.asarray(arrays["vertices_xy"], dtype=np.int64),
428
+ polygon_offsets=np.asarray(arrays["polygon_offsets"], dtype=np.uint64),
429
+ layer_ids=np.asarray(arrays["layer_ids"], dtype=np.uint32),
430
+ bboxes=np.asarray(arrays["bboxes"], dtype=np.int64),
431
+ flags=np.asarray(arrays["flags"], dtype=np.uint32),
432
+ source_ids=np.asarray(arrays["source_ids"], dtype=np.uint64),
433
+ layer_table=layer_table,
434
+ polygon_hashes=None if polygon_hashes is None else np.asarray(polygon_hashes, dtype=np.uint64),
435
+ )
436
+
437
+
438
+ def _intern_from_cache(
439
+ arrays: dict[str, np.ndarray],
440
+ *,
441
+ duplicate_occurrence_count: int,
442
+ hash_collision_bucket_count: int,
443
+ ) -> InternResult:
444
+ offsets = arrays["unique_blob_offsets"]
445
+ blob_data = arrays["unique_blob_bytes"].tobytes()
446
+ unique_blobs = tuple(blob_data[int(offsets[index]) : int(offsets[index + 1])] for index in range(len(offsets) - 1))
447
+ canonical_ids = np.asarray(arrays["canonical_ids"], dtype=np.uint64)
448
+ indices = np.asarray(arrays["record_polygon_indices"], dtype=np.uint64)
449
+ digests = np.asarray(arrays["record_digests"], dtype=np.uint8)
450
+ validity = np.asarray(arrays["record_validity"], dtype=np.uint8)
451
+ records = tuple(
452
+ CanonicalRecord(
453
+ int(index),
454
+ unique_blobs[int(canonical_ids[int(index)])],
455
+ bytes(digests[position]),
456
+ int(validity[position]),
457
+ )
458
+ for position, index in enumerate(indices)
459
+ )
460
+ return InternResult(
461
+ canonical_ids=canonical_ids,
462
+ unique_blobs=unique_blobs,
463
+ records=records,
464
+ duplicate_occurrence_count=duplicate_occurrence_count,
465
+ hash_collision_bucket_count=hash_collision_bucket_count,
466
+ )
467
+
468
+
469
+ def _spatial_from_cache(buffer: PolygonBuffer, arrays: dict[str, np.ndarray]) -> SpatialIndex:
470
+ order = np.asarray(arrays["spatial_order"], dtype=np.int64)
471
+ entries = {}
472
+ bbox_arrays = {}
473
+ index_arrays = {}
474
+ for layer_id, layer in enumerate(buffer.layer_table.layers):
475
+ indices = order[buffer.layer_ids[order] == layer_id]
476
+ bboxes = np.asarray(buffer.bboxes[indices], dtype=np.int64).reshape((-1, 4))
477
+ entries[layer] = tuple((tuple(int(value) for value in bbox), int(index)) for bbox, index in zip(bboxes, indices))
478
+ bbox_arrays[layer] = bboxes
479
+ index_arrays[layer] = indices
480
+ return SpatialIndex(entries, bbox_arrays_by_layer=bbox_arrays, index_arrays_by_layer=index_arrays)
481
+
482
+
483
+ def _read_array_cache(
484
+ manifest_path: Path,
485
+ arrays_path: Path,
486
+ schema_version: str,
487
+ cache_key: str,
488
+ ) -> tuple[dict[str, object], dict[str, np.ndarray]] | None:
489
+ try:
490
+ manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
491
+ if manifest.get("schema_version") != schema_version or manifest.get("cache_key") != cache_key:
492
+ return None
493
+ if _sha256_path(arrays_path) != manifest.get("arrays_sha256"):
494
+ return None
495
+ with np.load(arrays_path, allow_pickle=False) as archive:
496
+ arrays = {name: archive[name].copy() for name in archive.files}
497
+ except (OSError, UnicodeError, json.JSONDecodeError, ValueError, KeyError):
498
+ return None
499
+ return manifest, arrays
500
+
501
+
502
+ def _write_array_cache(
503
+ manifest_path: Path,
504
+ arrays_path: Path,
505
+ manifest: dict[str, object],
506
+ arrays: dict[str, np.ndarray],
507
+ ) -> None:
508
+ manifest_path.parent.mkdir(parents=True, exist_ok=True)
509
+ lock_path = manifest_path.with_name(f".{manifest_path.name}.lock")
510
+ with _exclusive_cache_lock(lock_path):
511
+ descriptor, temporary_name = tempfile.mkstemp(prefix=f".{arrays_path.name}.", suffix=".tmp", dir=arrays_path.parent)
512
+ try:
513
+ with os.fdopen(descriptor, "wb") as handle:
514
+ np.savez_compressed(handle, **arrays)
515
+ handle.flush()
516
+ os.fsync(handle.fileno())
517
+ temporary_path = Path(temporary_name)
518
+ committed_manifest = {**manifest, "arrays_sha256": _sha256_path(temporary_path)}
519
+ os.replace(temporary_path, arrays_path)
520
+ _atomic_write_json(manifest_path, committed_manifest)
521
+ finally:
522
+ Path(temporary_name).unlink(missing_ok=True)
523
+
524
+
525
+ def _atomic_write_json(path: Path, payload: dict[str, object]) -> None:
526
+ path.parent.mkdir(parents=True, exist_ok=True)
527
+ descriptor, temporary_name = tempfile.mkstemp(prefix=f".{path.name}.", suffix=".tmp", dir=path.parent)
528
+ try:
529
+ with os.fdopen(descriptor, "w", encoding="utf-8") as handle:
530
+ json.dump(payload, handle, indent=2, sort_keys=True)
531
+ handle.write("\n")
532
+ handle.flush()
533
+ os.fsync(handle.fileno())
534
+ os.replace(temporary_name, path)
535
+ finally:
536
+ Path(temporary_name).unlink(missing_ok=True)
537
+
538
+
539
+ def _sha256_path(path: Path) -> str:
540
+ digest = hashlib.sha256()
541
+ with path.open("rb") as handle:
542
+ for chunk in iter(lambda: handle.read(1024 * 1024), b""):
543
+ digest.update(chunk)
544
+ return digest.hexdigest()