Skip to content

Commit 12bfb6b

Browse files
feat(tidy3d): FXC-4607-autograd-for-clip-operation
1 parent db8dbc3 commit 12bfb6b

File tree

12 files changed

+2459
-248
lines changed

12 files changed

+2459
-248
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2121
- Added flag `remove_fragments` to the base `UnstructuredGrid` to remove fragments in unstructured grids. This can ease meshing by eliminating internal boundaries in overlapping structures.
2222
- Added deprecation warning for `conformal` in TCAD heat/charge monitors when explicitly set; this option is ignored (treated as `False`) when meshing with `remove_fragments=True`.
2323
- Added in-memory caching for downloaded batch results, configurable via ``config.batch_data_cache``.
24+
- Added autograd support for `ClipOperation` geometries like unions or intersections of geometries.
2425

2526
### Breaking Changes
2627
- Added optional automatic extrusion of structures at the simulation boundaries into/through PML/Absorber layers via `extrude_structures` field in class `AbsorberSpec`.

tests/test_components/autograd/numerical/conftest.py

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from __future__ import annotations
22

3+
import hashlib
34
import os
45
import re
56
from pathlib import Path
@@ -9,13 +10,35 @@
910
ARTIFACT_ENV_VAR = "TIDY3D_NUMERICAL_ARTIFACT_DIR"
1011
DEFAULT_RELATIVE_DIR = Path("tests/tmp/autograd_numerical")
1112

13+
# Optional extra cap for the per-test directory name length (in bytes, after fs encoding).
14+
ARTIFACT_NAME_MAX_ENV_VAR = "TIDY3D_NUMERICAL_ARTIFACT_NAME_MAX"
15+
1216

1317
def _sanitize_segment(value: str) -> str:
1418
sanitized = re.sub(r"[^\w.-]+", "_", value)
1519
sanitized = sanitized.strip("_")
1620
return sanitized or "case"
1721

1822

23+
def _pathconf_limit(path: Path, key: str, fallback: int) -> int:
24+
"""Best-effort os.pathconf lookup with a fallback."""
25+
try:
26+
return int(os.pathconf(str(path), key))
27+
except (AttributeError, ValueError, OSError):
28+
return fallback
29+
30+
31+
def _artifact_name_max_override() -> int | None:
32+
"""Optional user cap for artifact directory names (bytes)."""
33+
raw = os.environ.get(ARTIFACT_NAME_MAX_ENV_VAR)
34+
if not raw:
35+
return None
36+
try:
37+
return max(1, int(raw))
38+
except ValueError as e:
39+
raise ValueError(f"{ARTIFACT_NAME_MAX_ENV_VAR} must be an integer (got {raw!r}).") from e
40+
41+
1942
def _resolve_artifact_root() -> Path:
2043
env_value = os.environ.get(ARTIFACT_ENV_VAR)
2144
if env_value:
@@ -27,14 +50,60 @@ def _resolve_artifact_root() -> Path:
2750
return root
2851

2952

53+
def _case_dir_name(request, artifact_root: Path) -> str:
54+
"""Return a filesystem-friendly per-test artifact directory name.
55+
56+
Uses ``request.node.name``. If truncation is needed to satisfy filesystem
57+
path/name limits (and optional ``TIDY3D_NUMERICAL_ARTIFACT_NAME_MAX``), append a
58+
short SHA1 digest of the full nodeid. Otherwise, no digest is added, so
59+
uniqueness across files is not guaranteed.
60+
"""
61+
raw_nodeid = request.node.nodeid
62+
base_name = _sanitize_segment(request.node.name) or "case"
63+
64+
# Use filesystem-encoded byte lengths to be conservative under multibyte encodings.
65+
def _fslen(s: str) -> int:
66+
return len(os.fsencode(s))
67+
68+
root_abs = artifact_root.resolve()
69+
70+
# Common Linux defaults: NAME_MAX ~255 bytes, PATH_MAX ~4096 bytes (may vary).
71+
name_max = _pathconf_limit(root_abs, "PC_NAME_MAX", 255)
72+
path_max = _pathconf_limit(root_abs, "PC_PATH_MAX", 4096)
73+
74+
# Leave room for: <root>/<name> plus NUL (pathconf is in bytes).
75+
available = path_max - _fslen(str(root_abs)) - _fslen(os.sep) - 1
76+
77+
max_len = min(name_max, max(1, available))
78+
override = _artifact_name_max_override()
79+
if override is not None:
80+
max_len = min(max_len, override)
81+
82+
if _fslen(base_name) <= max_len:
83+
return base_name
84+
85+
digest = hashlib.sha1(raw_nodeid.encode("utf-8")).hexdigest()[:8]
86+
suffix = f"-{digest}"
87+
max_base = max_len - _fslen(suffix)
88+
if max_base < 1:
89+
max_base = 1
90+
91+
# Truncate by bytes (not chars): drop codepoints until it fits.
92+
while base_name and _fslen(base_name) > max_base:
93+
base_name = base_name[:-1]
94+
if not base_name:
95+
base_name = "c"
96+
97+
return f"{base_name}{suffix}"
98+
99+
30100
@pytest.fixture(scope="session")
31101
def numerical_artifact_root() -> Path:
32102
return _resolve_artifact_root()
33103

34104

35105
@pytest.fixture
36106
def numerical_case_dir(request, numerical_artifact_root: Path) -> Path:
37-
safe_nodeid = _sanitize_segment(request.node.nodeid.replace(os.sep, "_"))
38-
case_dir = numerical_artifact_root / safe_nodeid
107+
case_dir = numerical_artifact_root / _case_dir_name(request, numerical_artifact_root)
39108
case_dir.mkdir(parents=True, exist_ok=True)
40109
return case_dir

0 commit comments

Comments
 (0)