Skip to content

Commit 26bbd4e

Browse files
feat(tidy3d): FXC-3693-triangle-mesh-support-for-adjoint
1 parent 3037d0f commit 26bbd4e

File tree

10 files changed

+1839
-38
lines changed

10 files changed

+1839
-38
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
4242
- Added `smoothed_projection` for topology optimization of completely binarized designs.
4343
- Added more RF-specific mode characteristics to `MicrowaveModeData`, including propagation constants (alpha, beta, gamma), phase/group velocities, wave impedance, and automatic mode classification with configurable polarization thresholds in `MicrowaveModeSpec`.
4444
- Introduce `tidy3d.rf` namespace to consolidate all RF classes.
45+
- Added support of `TriangleMesh` for autograd.
4546

4647
### Breaking Changes
4748
- Edge singularity correction at PEC and lossy metal edges defaults to `True`.

tests/test_components/autograd/numerical/test_autograd_box_polyslab_numerical.py

Lines changed: 16 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,22 @@
4141
sys.stdout = sys.stderr
4242

4343

44+
def angled_overlap_deg(v1, v2):
45+
norm_v1 = np.linalg.norm(v1)
46+
norm_v2 = np.linalg.norm(v2)
47+
48+
if np.isclose(norm_v1, 0.0) or np.isclose(norm_v2, 0.0):
49+
if not (np.isclose(norm_v1, 0.0) and np.isclose(norm_v2, 0.0)):
50+
return np.inf
51+
52+
return 0.0
53+
54+
dot = np.minimum(1.0, np.sum((v1 / np.linalg.norm(v1)) * (v2 / np.linalg.norm(v2))))
55+
angle_deg = np.arccos(dot) * 180.0 / np.pi
56+
57+
return angle_deg
58+
59+
4460
def case_identifier(is_3d: bool, infinite_dim_2d: int | None, shift_box_center: bool) -> str:
4561
geometry_tag = "3d" if is_3d else f"2d_infinite_dim_{infinite_dim_2d}"
4662
shift_tag = "shifted" if shift_box_center else "centered"
@@ -422,21 +438,6 @@ def test_box_and_polyslab_gradients_match(
422438
)
423439
np.savez(npz_path, **test_data)
424440

425-
def angled_overlap_deg(v1, v2):
426-
norm_v1 = np.linalg.norm(v1)
427-
norm_v2 = np.linalg.norm(v2)
428-
429-
if np.isclose(norm_v1, 0.0) or np.isclose(norm_v2, 0.0):
430-
if not (np.isclose(norm_v1, 0.0) and np.isclose(norm_v2, 0.0)):
431-
return np.inf
432-
433-
return 0.0
434-
435-
dot = np.minimum(1.0, np.sum((v1 / np.linalg.norm(v1)) * (v2 / np.linalg.norm(v2))))
436-
angle_deg = np.arccos(dot) * 180.0 / np.pi
437-
438-
return angle_deg
439-
440441
box_polyslab_overlap_deg = angled_overlap_deg(box_grad_filtered, polyslab_grad_filtered)
441442
fd_overlap_deg = angled_overlap_deg(fd_box, fd_polyslab)
442443
box_fd_adj_overlap_deg = angled_overlap_deg(box_grad_filtered, fd_box)
Lines changed: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
1+
# Test autograd and compare to numerically computed finite difference gradients for
2+
# PolySlab and TriangleMesh geometries representing the same rectangular slab.
3+
from __future__ import annotations
4+
5+
import sys
6+
7+
import autograd.numpy as anp
8+
import numpy as np
9+
import pytest
10+
from autograd import value_and_grad
11+
12+
import tidy3d as td
13+
import tidy3d.web as web
14+
from tests.test_components.autograd.numerical.test_autograd_box_polyslab_numerical import (
15+
angled_overlap_deg,
16+
dimension_permutation,
17+
finite_difference,
18+
make_base_simulation,
19+
run_parameter_simulations,
20+
squeeze_dimension,
21+
)
22+
from tidy3d import config
23+
24+
config.local_cache.enabled = True
25+
WL_UM = 0.65
26+
FREQ0 = td.C_0 / WL_UM
27+
PERIODS_UM = (3 * WL_UM, 4 * WL_UM)
28+
INFINITE_DIM_SIZE_UM = 0.1
29+
SRC_OFFSET = -2.5
30+
MONITOR_OFFSET = 2.5
31+
PERMITTIVITY = 2.5**2
32+
MESH_SPACING_UM = WL_UM / 40.0
33+
FINITE_DIFFERENCE_STEP = MESH_SPACING_UM
34+
LOCAL_GRADIENT = True
35+
VERBOSE = False
36+
SHOW_PRINT_STATEMENTS = True
37+
PLOT_FD_ADJ_COMPARISON = False
38+
SAVE_OUTPUT_DATA = True
39+
COMPARE_TO_FINITE_DIFFERENCE = True
40+
COMPARE_TO_POLYSLAB = True
41+
42+
ANGLE_OVERLAP_THRESH_DEG = 10.0
43+
ANGLE_OVERLAP_FD_ADJ_THRESH_DEG = 10.0
44+
45+
VERTEX_SIGNS = np.array(
46+
[
47+
(-1.0, -1.0, -1.0),
48+
(-1.0, -1.0, 1.0),
49+
(-1.0, 1.0, -1.0),
50+
(-1.0, 1.0, 1.0),
51+
(1.0, -1.0, -1.0),
52+
(1.0, -1.0, 1.0),
53+
(1.0, 1.0, -1.0),
54+
(1.0, 1.0, 1.0),
55+
]
56+
)
57+
58+
TRIANGLE_FACE_VERTEX_IDS = np.array(
59+
[
60+
(1, 3, 0),
61+
(4, 1, 0),
62+
(0, 3, 2),
63+
(2, 4, 0),
64+
(1, 7, 3),
65+
(5, 1, 4),
66+
(5, 7, 1),
67+
(3, 7, 2),
68+
(6, 4, 2),
69+
(2, 7, 6),
70+
(6, 5, 4),
71+
(7, 5, 6),
72+
],
73+
dtype=int,
74+
)
75+
76+
if PLOT_FD_ADJ_COMPARISON:
77+
pytestmark = pytest.mark.usefixtures("mpl_config_interactive")
78+
else:
79+
pytestmark = pytest.mark.usefixtures("mpl_config_noninteractive")
80+
81+
if SHOW_PRINT_STATEMENTS:
82+
sys.stdout = sys.stderr
83+
84+
85+
def _triangles_from_params(params, box_center, xp):
86+
params_arr = xp.array(params)
87+
center_arr = xp.array(box_center)
88+
half_size = 0.5 * params_arr
89+
vertices = center_arr + xp.array(VERTEX_SIGNS) * half_size
90+
return vertices[xp.array(TRIANGLE_FACE_VERTEX_IDS)]
91+
92+
93+
def make_trianglemesh_geometry(params, box_center):
94+
triangles = _triangles_from_params(params, box_center, np)
95+
mesh = td.TriangleMesh.from_triangles(triangles)
96+
return mesh
97+
98+
99+
def make_polyslab_geometry(params, box_center, axis: int) -> td.PolySlab:
100+
half_size = 0.5 * params
101+
slab_bounds = (
102+
box_center[axis] - half_size[axis],
103+
box_center[axis] + half_size[axis],
104+
)
105+
plane_axes = [idx for idx in range(3) if idx != axis]
106+
107+
vertices = []
108+
for sign_0, sign_1 in ((-1, -1), (-1, 1), (1, 1), (1, -1)):
109+
coord_0 = box_center[plane_axes[0]] + sign_0 * half_size[plane_axes[0]]
110+
coord_1 = box_center[plane_axes[1]] + sign_1 * half_size[plane_axes[1]]
111+
vertices.append((coord_0, coord_1))
112+
113+
return td.PolySlab(vertices=tuple(vertices), slab_bounds=slab_bounds, axis=axis)
114+
115+
116+
def run_parameter_simulations2(
117+
parameter_sets: list[anp.ndarray],
118+
make_geometry,
119+
box_center,
120+
base_sim: td.Simulation,
121+
fom,
122+
tmp_path,
123+
*,
124+
local_gradient: bool,
125+
):
126+
simulations = []
127+
128+
for param_values in parameter_sets:
129+
geometry = make_geometry(param_values, box_center)
130+
structure = td.Structure(
131+
geometry=geometry,
132+
medium=td.Medium(permittivity=PERMITTIVITY),
133+
)
134+
sim = base_sim.updated_copy(structures=[structure], validate=False)
135+
simulations.append(sim)
136+
137+
sim_data = web.run(
138+
simulations,
139+
local_gradient=local_gradient,
140+
path=tmp_path,
141+
verbose=VERBOSE,
142+
)
143+
sim_fom = [fom(data) for data in sim_data]
144+
if len(sim_fom) == 1:
145+
sim_fom = sim_fom[0]
146+
return sim_fom
147+
148+
149+
def make_objective(
150+
make_geometry,
151+
box_center,
152+
tag: str,
153+
base_sim: td.Simulation,
154+
fom,
155+
tmp_path,
156+
*,
157+
local_gradient: bool,
158+
):
159+
def objective(parameters):
160+
results = run_parameter_simulations(
161+
parameters,
162+
make_geometry,
163+
box_center,
164+
tag,
165+
base_sim,
166+
fom,
167+
tmp_path,
168+
local_gradient=local_gradient,
169+
)
170+
171+
return results
172+
173+
return objective
174+
175+
176+
@pytest.mark.numerical
177+
@pytest.mark.parametrize(
178+
"is_3d, infinite_dim_2d",
179+
[
180+
(True, 2),
181+
(False, 0),
182+
(False, 1),
183+
(False, 2),
184+
],
185+
)
186+
@pytest.mark.parametrize("shift_box_center", (True, False))
187+
def test_polyslab_and_trianglemesh_gradients_match(
188+
is_3d, infinite_dim_2d, shift_box_center, tmp_path
189+
):
190+
"""Test that the triangle mesh and polyslab gradients match for rectangular slab geometries. Allow
191+
comparison as well to finite difference values."""
192+
193+
base_sim, fom = make_base_simulation(is_3d, infinite_dim_2d if not is_3d else None)
194+
195+
if shift_box_center:
196+
slab_init_size = [2.0 * WL_UM, 2.5 * WL_UM, 0.75 * WL_UM]
197+
else:
198+
slab_init_size = [1.0 * WL_UM, 1.25 * WL_UM, 0.75 * WL_UM]
199+
200+
initial_params = anp.array(slab_init_size)
201+
202+
polyslab_axis = 2 if is_3d else infinite_dim_2d
203+
204+
box_center = [0.0, 0.0, 0.0]
205+
if shift_box_center:
206+
# test what happens when part of the structure falls outside the simulation domain
207+
# but don't shift along source axis
208+
if is_3d:
209+
box_center[0:2] = [0.5 * p for p in PERIODS_UM]
210+
else:
211+
_, final_dim_2d = dimension_permutation(infinite_dim_2d)
212+
box_center[infinite_dim_2d] = 0.5 * INFINITE_DIM_SIZE_UM
213+
box_center[final_dim_2d] = 0.5 * PERIODS_UM[0]
214+
215+
triangle_objective = make_objective(
216+
make_trianglemesh_geometry,
217+
box_center,
218+
"trianglemesh",
219+
base_sim,
220+
fom,
221+
tmp_path,
222+
local_gradient=LOCAL_GRADIENT,
223+
)
224+
225+
polyslab_objective = make_objective(
226+
lambda p, box_center: make_polyslab_geometry(p, box_center, polyslab_axis),
227+
box_center,
228+
"polyslab",
229+
base_sim,
230+
fom,
231+
tmp_path,
232+
local_gradient=LOCAL_GRADIENT,
233+
)
234+
235+
triangle_objective_fd = make_objective(
236+
make_trianglemesh_geometry,
237+
box_center,
238+
"trianglemesh_fd",
239+
base_sim,
240+
fom,
241+
tmp_path,
242+
local_gradient=False,
243+
)
244+
245+
_triangle_value, triangle_grad = value_and_grad(triangle_objective)([initial_params])
246+
assert triangle_grad is not None
247+
if is_3d or infinite_dim_2d not in [1, 2]:
248+
grad_norm_triangle = np.linalg.norm(triangle_grad)
249+
assert grad_norm_triangle > 1e-6, (
250+
f"Assumed norm to be bigger than 1e-6, got {grad_norm_triangle}"
251+
)
252+
triangle_grad_filtered = squeeze_dimension(triangle_grad, is_3d, infinite_dim_2d)
253+
polyslab_grad_filtered = None
254+
if COMPARE_TO_POLYSLAB:
255+
_polyslab_value, polyslab_grad = value_and_grad(polyslab_objective)([initial_params])
256+
polyslab_grad_filtered = squeeze_dimension(polyslab_grad, is_3d, infinite_dim_2d)
257+
print(
258+
"polyslab_grad_filtered\t",
259+
polyslab_grad_filtered.tolist()
260+
if not isinstance(polyslab_grad_filtered, list)
261+
else polyslab_grad_filtered,
262+
)
263+
264+
fd_triangle = None
265+
if COMPARE_TO_FINITE_DIFFERENCE:
266+
fd_triangle = squeeze_dimension(
267+
finite_difference(triangle_objective_fd, initial_params, is_3d, infinite_dim_2d),
268+
is_3d,
269+
infinite_dim_2d,
270+
)
271+
272+
if SAVE_OUTPUT_DATA:
273+
test_data = {
274+
"fd trianglemesh": fd_triangle,
275+
"grad trianglemesh": triangle_grad_filtered,
276+
"grad polyslab": polyslab_grad_filtered,
277+
}
278+
np.savez(
279+
f"test_diff_triangle_poly_{'3' if is_3d else '2'}d_infinite_dim_{infinite_dim_2d}.npz",
280+
**test_data,
281+
)
282+
283+
if COMPARE_TO_POLYSLAB:
284+
triangle_polyslab_overlap_deg = angled_overlap_deg(
285+
triangle_grad_filtered, polyslab_grad_filtered
286+
)
287+
print(f"TriangleMesh FD vs. polyslab overlap: {triangle_polyslab_overlap_deg:.3f}° ")
288+
assert triangle_polyslab_overlap_deg < ANGLE_OVERLAP_THRESH_DEG, (
289+
f"[TriangleMesh vs. PolySlab] Autograd gradients disagree: "
290+
f"angle overlap = {triangle_polyslab_overlap_deg:.3f}° "
291+
f"(threshold = {ANGLE_OVERLAP_THRESH_DEG:.3f}°, "
292+
f"difference = {triangle_polyslab_overlap_deg - ANGLE_OVERLAP_THRESH_DEG:+.3f}°)"
293+
)
294+
295+
if COMPARE_TO_FINITE_DIFFERENCE:
296+
triangle_fd_adj_overlap_deg = angled_overlap_deg(triangle_grad_filtered, fd_triangle)
297+
print(f"TriangleMesh FD vs. Adjoint angle overlap: {triangle_fd_adj_overlap_deg:.3f}° ")
298+
assert triangle_fd_adj_overlap_deg < ANGLE_OVERLAP_FD_ADJ_THRESH_DEG, (
299+
f"Autograd and finite-difference gradients disagree: "
300+
f"angle overlap = {triangle_fd_adj_overlap_deg:.3f}° "
301+
f"(threshold = {ANGLE_OVERLAP_FD_ADJ_THRESH_DEG:.3f}°, "
302+
f"difference = {triangle_fd_adj_overlap_deg - ANGLE_OVERLAP_FD_ADJ_THRESH_DEG:+.3f}°)"
303+
)

0 commit comments

Comments
 (0)