Skip to content

Commit e64e1f5

Browse files
feat(tidy3d): FXC-4607-autograd-for-clip-operation
1 parent 043030a commit e64e1f5

File tree

12 files changed

+2471
-248
lines changed

12 files changed

+2471
-248
lines changed

CHANGELOG.md

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212
## [2.11.0.dev0] - 2026-02-19
1313

1414
### Added
15+
- Added `ModeSortSpec.keep_modes` which can be set to `"all"` to keep all modes in the mode solver (the default), `"filtered"` to keep only modes passing the filter defined by the `ModeSortSpec`, or an integer `N` to keep only the top `N` modes after filtering and sorting.
16+
- Added `fill_fraction_box` as a new filtering and sorting key which computes the field-energy fill fraction within a specified bounding box (`ModeSortSpec.bounding_box`).
17+
- Added `Grid.fine_mesh_info` property to identify and report locations where grid cell sizes are fine for understanding meshing hotspots.
18+
- Added visualization of finest grid regions in `Simulation.plot_grid()` with shaded regions highlighting areas of fine meshing.
19+
- Added autograd support for `Sphere`.
20+
- Added validation warning in `HeatChargeSimulation` for very small `Cylinder` radii to help users avoid meshing and numerical issues.
21+
- Added `GaussianOverlapMonitor` and `AstigmaticGaussianOverlapMonitor` for decomposing electromagnetic fields onto Gaussian beam profiles.
22+
- Added `GaussianPort` and `AstigmaticGaussianPort` for S-matrix calculations using Gaussian beam sources and overlap monitors.
23+
- Added `symmetric_pseudo` option for `s_param_def` in `TerminalComponentModeler` which applies a scaling factor that ensures the S-matrix is symmetric in reciprocal systems.
24+
- Added deprecation warning for ``TemperatureMonitor`` and ``SteadyPotentialMonitor`` when ``unstructured`` parameter is not explicitly set. The default value of ``unstructured`` will change from ``False`` to ``True`` after the 2.11 release.
25+
- 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.
26+
- 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`.
27+
- Added in-memory caching for downloaded batch results, configurable via ``config.batch_data_cache``.
28+
- Added autograd support for `ClipOperation` geometries like unions or intersections of geometries.
1529

1630
- Added `ModeSortSpec.keep_modes` which can be set to `"all"` to keep all modes in the mode solver (the default), `"filtered"` to keep only modes passing the filter defined by the `ModeSortSpec`, or an integer `N` to keep only the top `N` modes after filtering and sorting.
1731
- Added `Grid.fine_mesh_info` property to identify and report locations where grid cell sizes are fine for understanding meshing hotspots.

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)