Skip to content

Commit 764c0f0

Browse files
feat(tidy3d): FXC-4607-autograd-for-clip-operation
1 parent 88e9306 commit 764c0f0

File tree

10 files changed

+2324
-238
lines changed

10 files changed

+2324
-238
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
- Added `Grid.fine_mesh_info` property to identify and report locations where grid cell sizes are fine for understanding meshing hotspots.
1414
- Added visualization of finest grid regions in `Simulation.plot_grid()` with shaded regions highlighting areas of fine meshing.
1515
- Added autograd support for `Sphere`.
16+
- Added autograd support for `ClipOperation` geometries like unions or intersections of geometries.
1617

1718
### Breaking Changes
1819
- Added `structure_priority_mode` for `TerminalComponentModeler` and default to `"conductor"` to ensure metal structures override dielectrics regardless of structure order, preventing order-dependent results in RF simulations.

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)