Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [2.11.0.dev0] - 2026-02-19

### Added
- 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.
- 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`).
- Added `Grid.fine_mesh_info` property to identify and report locations where grid cell sizes are fine for understanding meshing hotspots.
- Added visualization of finest grid regions in `Simulation.plot_grid()` with shaded regions highlighting areas of fine meshing.
- Added autograd support for `Sphere`.
- Added validation warning in `HeatChargeSimulation` for very small `Cylinder` radii to help users avoid meshing and numerical issues.
- Added `GaussianOverlapMonitor` and `AstigmaticGaussianOverlapMonitor` for decomposing electromagnetic fields onto Gaussian beam profiles.
- Added `GaussianPort` and `AstigmaticGaussianPort` for S-matrix calculations using Gaussian beam sources and overlap monitors.
- 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.
- 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.
- 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.
- 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`.
- Added in-memory caching for downloaded batch results, configurable via ``config.batch_data_cache``.
- Added autograd support for `ClipOperation` geometries like unions or intersections of geometries.

- 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.
- Added `Grid.fine_mesh_info` property to identify and report locations where grid cell sizes are fine for understanding meshing hotspots.
Expand Down
73 changes: 71 additions & 2 deletions tests/test_components/autograd/numerical/conftest.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from __future__ import annotations

import hashlib
import os
import re
from pathlib import Path
Expand All @@ -9,13 +10,35 @@
ARTIFACT_ENV_VAR = "TIDY3D_NUMERICAL_ARTIFACT_DIR"
DEFAULT_RELATIVE_DIR = Path("tests/tmp/autograd_numerical")

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


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


def _pathconf_limit(path: Path, key: str, fallback: int) -> int:
"""Best-effort os.pathconf lookup with a fallback."""
try:
return int(os.pathconf(str(path), key))
except (AttributeError, ValueError, OSError):
return fallback


def _artifact_name_max_override() -> int | None:
"""Optional user cap for artifact directory names (bytes)."""
raw = os.environ.get(ARTIFACT_NAME_MAX_ENV_VAR)
if not raw:
return None
try:
return max(1, int(raw))
except ValueError as e:
raise ValueError(f"{ARTIFACT_NAME_MAX_ENV_VAR} must be an integer (got {raw!r}).") from e


def _resolve_artifact_root() -> Path:
env_value = os.environ.get(ARTIFACT_ENV_VAR)
if env_value:
Expand All @@ -27,14 +50,60 @@ def _resolve_artifact_root() -> Path:
return root


def _case_dir_name(request, artifact_root: Path) -> str:
"""Return a filesystem-friendly per-test artifact directory name.

Uses ``request.node.name``. If truncation is needed to satisfy filesystem
path/name limits (and optional ``TIDY3D_NUMERICAL_ARTIFACT_NAME_MAX``), append a
short SHA1 digest of the full nodeid. Otherwise, no digest is added, so
uniqueness across files is not guaranteed.
"""
raw_nodeid = request.node.nodeid
base_name = _sanitize_segment(request.node.name) or "case"

# Use filesystem-encoded byte lengths to be conservative under multibyte encodings.
def _fslen(s: str) -> int:
return len(os.fsencode(s))

root_abs = artifact_root.resolve()

# Common Linux defaults: NAME_MAX ~255 bytes, PATH_MAX ~4096 bytes (may vary).
name_max = _pathconf_limit(root_abs, "PC_NAME_MAX", 255)
path_max = _pathconf_limit(root_abs, "PC_PATH_MAX", 4096)

# Leave room for: <root>/<name> plus NUL (pathconf is in bytes).
available = path_max - _fslen(str(root_abs)) - _fslen(os.sep) - 1

max_len = min(name_max, max(1, available))
override = _artifact_name_max_override()
if override is not None:
max_len = min(max_len, override)

if _fslen(base_name) <= max_len:
return base_name

digest = hashlib.sha1(raw_nodeid.encode("utf-8")).hexdigest()[:8]
suffix = f"-{digest}"
max_base = max_len - _fslen(suffix)
if max_base < 1:
max_base = 1

# Truncate by bytes (not chars): drop codepoints until it fits.
while base_name and _fslen(base_name) > max_base:
base_name = base_name[:-1]
if not base_name:
base_name = "c"

return f"{base_name}{suffix}"


@pytest.fixture(scope="session")
def numerical_artifact_root() -> Path:
return _resolve_artifact_root()


@pytest.fixture
def numerical_case_dir(request, numerical_artifact_root: Path) -> Path:
safe_nodeid = _sanitize_segment(request.node.nodeid.replace(os.sep, "_"))
case_dir = numerical_artifact_root / safe_nodeid
case_dir = numerical_artifact_root / _case_dir_name(request, numerical_artifact_root)
case_dir.mkdir(parents=True, exist_ok=True)
return case_dir
Loading
Loading