Skip to content

Commit e124a1e

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

File tree

10 files changed

+1805
-38
lines changed

10 files changed

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

0 commit comments

Comments
 (0)