Skip to content

Commit ad744f5

Browse files
rebase changes
1 parent 5f20cf3 commit ad744f5

10 files changed

Lines changed: 253 additions & 78 deletions

File tree

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
3636
- 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.
3737
- Added validation to `GaussianDoping` to ensure `ref_con < concentration`, validate `source` face identifier, and warn the user when the box size is not sufficient for the specified transition width.
3838
- Local caching is now enabled by default (set `td.config.local_cache.enabled=False` to opt out).
39+
- Reduced computation time of `adaptive_vjp_spacing` for `GeometryGroup` by allowing permittivity based spacing value to be cached.
3940

4041
### Fixed
4142
- Fixed intermittent "API key not found" errors in parallel job launches by making configuration directory detection race-safe.
@@ -45,6 +46,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4546
- Fixed `CustomMedium` gradient calculation when field coordinates exactly align with boundaries.
4647
- Fixed adjoint simulation `grid_spec` to align exactly with forward simulation for correct `FieldData` adjoint source power.
4748
- Fixed redundant logging when `Batch.download()` skips existing files, and added `replace_existing` to `Batch.run()` so overwrite behavior can be controlled directly.
49+
- Fixed redundant server lookups when loading simulation results.
50+
- Updated docstrings for `DerivativeInfo` to more accurately reflect dataclass fields.
4851

4952
## [2.10.2] - 2026-01-21
5053

tests/test_components/autograd/test_autograd.py

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import cProfile
66
import typing
77
import warnings
8+
from contextlib import nullcontext
89
from dataclasses import dataclass
910
from importlib import reload
1011
from os.path import join
@@ -1967,6 +1968,96 @@ def test_adaptive_spacing(eps_real):
19671968
assert np.isclose(expected_vjp_spacing, vjp_spacing), "Unexpected adaptive vjp spacing!"
19681969

19691970

1971+
def test_adaptive_spacing_cache(rng, redirect_stdout_to_stderr, monkeypatch):
1972+
"""Ensure the cache is not affecting `GeometryGroup` results or persisting after derivative computation."""
1973+
x_geom = [-0.5, 0.5]
1974+
y_geom = [-0.5, 0.5]
1975+
1976+
radius = 0.3
1977+
1978+
geometries = []
1979+
for x_center in x_geom:
1980+
for y_center in y_geom:
1981+
geometries.append(
1982+
td.Cylinder(center=(x_center, y_center, 0.0), length=0.5, radius=radius)
1983+
)
1984+
1985+
field_paths = []
1986+
for idx in range(len(geometries)):
1987+
field_paths.append(("geometries", idx, "radius"))
1988+
1989+
geometry = td.GeometryGroup(geometries=geometries)
1990+
1991+
eps_keys = ["eps_xx", "eps_yy", "eps_zz"]
1992+
1993+
N = 20
1994+
xcoord = np.linspace(-1, 1, N)
1995+
ycoord = np.linspace(-1, 1, N)
1996+
zcoord = np.linspace(-1, 1, N)
1997+
1998+
eps_out_data = rng.uniform(1, 2, (N, N, N, 1))
1999+
eps_in_data = rng.uniform(1, 2, (N, N, N, 1))
2000+
2001+
freq = 1.94e14
2002+
2003+
def random_scalar_data_array():
2004+
return td.ScalarFieldDataArray(
2005+
rng.uniform(1, 2, (N, N, N, 1)),
2006+
coords={"x": xcoord, "y": ycoord, "z": zcoord, "f": [freq]},
2007+
)
2008+
2009+
E_fwd = {key: random_scalar_data_array() for key in ["Ex", "Ey", "Ez"]}
2010+
E_adj = {key: random_scalar_data_array() for key in ["Ex", "Ey", "Ez"]}
2011+
D_fwd = {key: random_scalar_data_array() for key in ["Ex", "Ey", "Ez"]}
2012+
D_adj = {key: random_scalar_data_array() for key in ["Ex", "Ey", "Ez"]}
2013+
E_der_map = {key: random_scalar_data_array() for key in ["Ex", "Ey", "Ez"]}
2014+
D_der_map = {key: random_scalar_data_array() for key in ["Ex", "Ey", "Ez"]}
2015+
2016+
derivative_info = DerivativeInfo(
2017+
paths=tuple(field_paths),
2018+
E_der_map=E_der_map,
2019+
D_der_map=D_der_map,
2020+
E_fwd=E_fwd,
2021+
D_fwd=D_fwd,
2022+
E_adj=E_adj,
2023+
D_adj=D_adj,
2024+
eps_data={
2025+
key: td.ScalarFieldDataArray(
2026+
rng.uniform(1, 2, (N, N, N, 1)),
2027+
coords={"x": xcoord, "y": ycoord, "z": zcoord, "f": [freq]},
2028+
)
2029+
for key in eps_keys
2030+
},
2031+
frequencies=[freq],
2032+
bounds=((-1, -1, -1), (1, 1, 1)),
2033+
eps_out=td.ScalarFieldDataArray(
2034+
eps_out_data, coords={"x": xcoord, "y": ycoord, "z": zcoord, "f": [freq]}
2035+
),
2036+
eps_in=td.ScalarFieldDataArray(
2037+
eps_in_data, coords={"x": xcoord, "y": ycoord, "z": zcoord, "f": [freq]}
2038+
),
2039+
bounds_intersect=((-1, -1, -1), (1, 1, 1)),
2040+
simulation_bounds=((-2, -2, -2), (2, 2, 2)),
2041+
)
2042+
2043+
vjp_with_cache = geometry._compute_derivatives(derivative_info)
2044+
2045+
assert derivative_info.cached_min_spacing_from_permittivity is None, (
2046+
"Unexpected cached variable persistence."
2047+
)
2048+
2049+
monkeypatch.setattr(
2050+
derivative_info, "cache_min_spacing_from_permittivity", lambda: nullcontext()
2051+
)
2052+
2053+
vjp_without_cache = geometry._compute_derivatives(derivative_info)
2054+
2055+
for k, v in vjp_with_cache.items():
2056+
assert v == vjp_without_cache[k], (
2057+
"Geometry group computation changed when running with cache."
2058+
)
2059+
2060+
19702061
@pytest.mark.parametrize("eps_real", [1e6, -1e8])
19712062
def test_cylinder_discretization(eps_real):
19722063
freq = 5e9
@@ -3284,6 +3375,18 @@ class SimpleDerivativeInfo:
32843375
bounds_intersect: tuple
32853376
simulation_bounds: tuple
32863377
interpolators: dict | None = None
3378+
cached_min_spacing_from_permittivity: float | None = None
3379+
eps_data = {
3380+
key: td.ScalarFieldDataArray(
3381+
[[[[2.0]]]], coords={"x": [0], "y": [0], "z": [0], "f": [200e12]}
3382+
)
3383+
for key in ["eps_xx", "eps_yy", "eps_zz"]
3384+
}
3385+
frequencies = [200e12]
3386+
3387+
cache_min_spacing_from_permittivity = DerivativeInfo.cache_min_spacing_from_permittivity
3388+
min_spacing_from_permittivity = DerivativeInfo.min_spacing_from_permittivity
3389+
wavelength_min = property(lambda self: DerivativeInfo.wavelength_min.fget(self))
32873390

32883391
def create_interpolators(self, dtype: float = float):
32893392
return self.interpolators or {}
@@ -3295,6 +3398,7 @@ def updated_copy(self, **kwargs):
32953398
"bounds_intersect": self.bounds_intersect,
32963399
"simulation_bounds": self.simulation_bounds,
32973400
"interpolators": self.interpolators,
3401+
"cached_min_spacing_from_permittivity": self.cached_min_spacing_from_permittivity,
32983402
}
32993403
data.update({k: v for k, v in kwargs.items() if k in data})
33003404
return SimpleDerivativeInfo(**data)

tests/test_components/autograd/test_autograd_polyslab.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,9 @@ def __init__(
132132
else ((-2e3, -2e3, -2e3), (2e3, 2e3, 2e3))
133133
)
134134

135+
self.cached_min_spacing_from_permittivity = None
136+
137+
min_spacing_from_permittivity = DerivativeInfo.min_spacing_from_permittivity
135138
adaptive_vjp_spacing = DerivativeInfo.adaptive_vjp_spacing
136139
wavelength_min = property(lambda self: DerivativeInfo.wavelength_min.fget(self))
137140
wavelength_max = property(lambda self: DerivativeInfo.wavelength_max.fget(self))

tests/test_web/test_webapi.py

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
from tidy3d.exceptions import SetupError
2929
from tidy3d.web import common
3030
from tidy3d.web.api.asynchronous import run_async
31-
from tidy3d.web.api.container import Batch, Job, WebContainer
31+
from tidy3d.web.api.container import Batch, BatchData, Job, WebContainer
3232
from tidy3d.web.api.run import _collect_by_hash, run
3333
from tidy3d.web.api.tidy3d_stub import Tidy3dStubData, task_type_name_of
3434
from tidy3d.web.api.webapi import (
@@ -435,6 +435,30 @@ def mock_download(*args, **kwargs):
435435
load(TASK_ID, str(tmp_path / "monitor_data.hdf5"))
436436

437437

438+
def test_batch_load_sim_data_skips_task_lookup(monkeypatch, tmp_path):
439+
data_path = tmp_path / "batch_results.hdf5"
440+
data_path.write_text("stub")
441+
batch_data = BatchData(
442+
task_paths={"task_1": str(data_path)},
443+
task_ids={"task_1": TASK_ID},
444+
cached_tasks={"task_1": False},
445+
is_downloaded=True,
446+
)
447+
448+
def _raise(*args, **kwargs):
449+
raise AssertionError("Unexpected web lookup during batch load.")
450+
451+
monkeypatch.setattr(f"{api_path}.get_info", _raise)
452+
monkeypatch.setattr(f"{task_core_path}.TaskFactory.get", _raise)
453+
monkeypatch.setattr(f"{task_core_path}.TaskFactory.get_kind", _raise)
454+
monkeypatch.setattr(f"{api_path}.resolve_local_cache", lambda: None)
455+
monkeypatch.setattr(
456+
f"{api_path}.Tidy3dStubData.postprocess", lambda *args, **kwargs: "stub_data"
457+
)
458+
459+
assert batch_data.load_sim_data("task_1") == "stub_data"
460+
461+
438462
@responses.activate
439463
def test_delete(set_api_key, mock_get_info):
440464
responses.add(

tidy3d/_common/components/autograd/derivative_utils.py

Lines changed: 66 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
from contextlib import contextmanager
56
from dataclasses import dataclass, field, replace
67
from functools import reduce
78
from typing import TYPE_CHECKING, Any, Optional
@@ -20,6 +21,7 @@
2021
from .utils import get_static
2122

2223
if TYPE_CHECKING:
24+
from collections.abc import Iterator
2325
from typing import Callable, Union
2426

2527
from tidy3d._common.compat import Self
@@ -98,15 +100,18 @@ class DerivativeInfo:
98100
Dataset of relative permittivity values along all three dimensions.
99101
Used for automatically computing permittivity inside or outside of a simple geometry."""
100102

101-
eps_in: EpsType
103+
eps_in: EpsType | None
102104
"""Permittivity inside the Structure.
103-
Typically computed from Structure.medium.eps_model.
104-
Used when it cannot be computed from eps_data or when eps_approx=True."""
105+
Computed only when structure.medium.is_custom is False. Contains the simulation
106+
permittivity inside the structure when the simulation background medium is set to
107+
the structure medium and all structures after the current structure are kept. Should
108+
be used as the inside permittivity for shape derivative computations."""
105109

106110
eps_out: EpsType
107111
"""Permittivity outside the Structure.
108-
Typically computed from Simulation.medium.eps_model.
109-
Used when it cannot be computed from eps_data or when eps_approx=True."""
112+
Contains the simulation permittivity outside the structure when the current structure
113+
is removed from the structure list. Should be used as the outside permittivity for
114+
shape derivative computations."""
110115

111116
bounds: Bound
112117
"""Geometry bounds.
@@ -161,6 +166,10 @@ class DerivativeInfo:
161166
sharing the same field data. This significantly improves performance for
162167
GeometryGroup processing."""
163168

169+
cached_min_spacing_from_permittivity: Optional[float] = None
170+
"""Cached `min_spacing_from_permittivity` to be used for objects like GeometryGroup
171+
to avoid recomputing this value multiple times in `adaptive_vjp_spacing`."""
172+
164173
# private cache for interpolators
165174
_interpolators_cache: dict = field(default_factory=dict, init=False, repr=False)
166175

@@ -215,7 +224,8 @@ def create_interpolators(self, dtype: Optional[np.dtype[Any]] = None) -> dict[st
215224
"""Create interpolators for field components and permittivity data.
216225
217226
Creates and caches ``RegularGridInterpolator`` objects for all field components
218-
(E_fwd, E_adj, D_fwd, D_adj) and permittivity data (eps_inf, eps_no).
227+
(E_fwd, E_adj, D_fwd, D_adj) and permittivity data (eps_in, eps_out, eps_data).
228+
Contains (H_fwd, H_adj) field components when relevant for certain material types.
219229
This caching strategy significantly improves performance by avoiding
220230
repeated interpolator construction in gradient evaluation loops.
221231
@@ -230,7 +240,7 @@ def create_interpolators(self, dtype: Optional[np.dtype[Any]] = None) -> dict[st
230240
dict
231241
Nested dictionary structure:
232242
- Field data: {"E_fwd": {"Ex": interpolator, ...}, ...}
233-
- Permittivity: {"eps_inf": interpolator, "eps_no": interpolator}
243+
- Permittivity: {"eps_in": interpolator, "eps_out": interpolator, "eps_data": interpolator}
234244
"""
235245
from scipy.interpolate import RegularGridInterpolator
236246

@@ -941,6 +951,54 @@ def project_der_map_to_axis(
941951
projected[key] = field_map[key]
942952
return projected
943953

954+
@property
955+
def min_spacing_from_permittivity(self) -> float:
956+
if self.cached_min_spacing_from_permittivity is not None:
957+
return self.cached_min_spacing_from_permittivity
958+
959+
def spacing_by_permittivity(eps_array: ScalarFieldDataArray) -> float:
960+
eps_real = np.asarray(eps_array.values, dtype=np.complex128).real
961+
962+
dx_candidates = []
963+
max_frequency = np.max(self.frequencies)
964+
965+
# wavelength-based sampling for dielectrics
966+
if np.any(eps_real > 0):
967+
eps_max = eps_real[eps_real > 0].max()
968+
lambda_min = self.wavelength_min / np.sqrt(eps_max)
969+
dx_candidates.append(lambda_min)
970+
971+
# skin depth sampling for metals
972+
if np.any(eps_real <= 0):
973+
omega = 2 * np.pi * max_frequency
974+
eps_neg = eps_real[eps_real <= 0]
975+
delta_min = C_0 / (omega * np.sqrt(np.abs(eps_neg).max()))
976+
dx_candidates.append(delta_min)
977+
978+
computed_spacing = min(dx_candidates)
979+
980+
return computed_spacing
981+
982+
eps_spacings = [
983+
spacing_by_permittivity(eps_array) for _, eps_array in self.eps_data.items()
984+
]
985+
min_spacing = np.min(eps_spacings)
986+
987+
return min_spacing
988+
989+
@contextmanager
990+
def cache_min_spacing_from_permittivity(self) -> Iterator[None]:
991+
"""
992+
Cache min_spacing_from_permittivity for the duration of the block. Cache
993+
is always cleared on exit.
994+
"""
995+
996+
self.cached_min_spacing_from_permittivity = self.min_spacing_from_permittivity
997+
try:
998+
yield
999+
finally:
1000+
self.cached_min_spacing_from_permittivity = None
1001+
9441002
def adaptive_vjp_spacing(
9451003
self,
9461004
wl_fraction: Optional[float] = None,
@@ -974,33 +1032,7 @@ def adaptive_vjp_spacing(
9741032
if min_allowed_spacing_fraction is None:
9751033
min_allowed_spacing_fraction = config.adjoint.minimum_spacing_fraction
9761034

977-
def spacing_by_permittivity(eps_array: ScalarFieldDataArray) -> float:
978-
eps_real = np.asarray(eps_array.values, dtype=np.complex128).real
979-
980-
dx_candidates = []
981-
max_frequency = np.max(self.frequencies)
982-
983-
# wavelength-based sampling for dielectrics
984-
if np.any(eps_real > 0):
985-
eps_max = eps_real[eps_real > 0].max()
986-
lambda_min = self.wavelength_min / np.sqrt(eps_max)
987-
dx_candidates.append(wl_fraction * lambda_min)
988-
989-
# skin depth sampling for metals
990-
if np.any(eps_real <= 0):
991-
omega = 2 * np.pi * max_frequency
992-
eps_neg = eps_real[eps_real <= 0]
993-
delta_min = C_0 / (omega * np.sqrt(np.abs(eps_neg).max()))
994-
dx_candidates.append(wl_fraction * delta_min)
995-
996-
computed_spacing = min(dx_candidates)
997-
998-
return computed_spacing
999-
1000-
eps_spacings = [
1001-
spacing_by_permittivity(eps_array) for _, eps_array in self.eps_data.items()
1002-
]
1003-
computed_spacing = np.min(eps_spacings)
1035+
computed_spacing = wl_fraction * self.min_spacing_from_permittivity
10041036

10051037
min_allowed_spacing = self.wavelength_min * min_allowed_spacing_fraction
10061038

0 commit comments

Comments
 (0)