Skip to content

Commit b3eb6cb

Browse files
Gregory RobertsGregory Roberts
authored andcommitted
feature[adjoint]: pec gradient support for Box
1 parent 15d1342 commit b3eb6cb

File tree

8 files changed

+2135
-100
lines changed

8 files changed

+2135
-100
lines changed

tests/test_components/test_autograd_rf_box.py

Lines changed: 850 additions & 0 deletions
Large diffs are not rendered by default.

tests/test_components/test_autograd_rf_polyslab.py

Lines changed: 727 additions & 0 deletions
Large diffs are not rendered by default.

tidy3d/components/autograd/derivative_utils.py

Lines changed: 302 additions & 39 deletions
Large diffs are not rendered by default.

tidy3d/components/geometry/base.py

Lines changed: 145 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from tidy3d.components.autograd.constants import GRADIENT_DTYPE_FLOAT
2323
from tidy3d.components.autograd.derivative_utils import DerivativeInfo, integrate_within_bounds
2424
from tidy3d.components.base import Tidy3dBaseModel, cached_property
25+
from tidy3d.components.geometry.bound_ops import bounds_intersection, bounds_union
2526
from tidy3d.components.transformation import ReflectionFromPlane, RotationAroundAxis
2627
from tidy3d.components.types import (
2728
ArrayFloat2D,
@@ -51,7 +52,7 @@
5152
polygon_patch,
5253
set_default_labels_and_title,
5354
)
54-
from tidy3d.constants import LARGE_NUMBER, MICROMETER, RADIAN, fp_eps, inf
55+
from tidy3d.constants import EPSILON_0, LARGE_NUMBER, MICROMETER, MU_0, RADIAN, fp_eps, inf
5556
from tidy3d.exceptions import (
5657
SetupError,
5758
Tidy3dError,
@@ -62,8 +63,6 @@
6263
from tidy3d.log import log
6364
from tidy3d.packaging import verify_packages_import
6465

65-
from .bound_ops import bounds_intersection, bounds_union
66-
6766
POLY_GRID_SIZE = 1e-12
6867
POLY_TOLERANCE_RATIO = 1e-12
6968

@@ -2439,11 +2438,12 @@ def _derivative_face(
24392438

24402439
# normal and tangential dims
24412440
dim_normal, dims_perp = self.pop_axis("xyz", axis=axis_normal)
2442-
fld_normal, flds_perp = self.pop_axis(("Ex", "Ey", "Ez"), axis=axis_normal)
2441+
fld_E_normal, flds_E_perp = self.pop_axis(("Ex", "Ey", "Ez"), axis=axis_normal)
2442+
fld_H_normal, flds_H_perp = self.pop_axis(("Hx", "Hy", "Hz"), axis=axis_normal)
24432443

24442444
# fields and bounds
2445-
D_normal = derivative_info.D_der_map[fld_normal]
2446-
Es_perp = tuple(derivative_info.E_der_map[key] for key in flds_perp)
2445+
D_normal = derivative_info.D_der_map[fld_E_normal]
2446+
Es_perp = tuple(derivative_info.E_der_map[key] for key in flds_E_perp)
24472447
bounds_normal, bounds_perp = self.pop_axis(
24482448
np.array(derivative_info.bounds).T, axis=axis_normal
24492449
)
@@ -2497,33 +2497,159 @@ def _derivative_face(
24972497
delta_eps_perps = [eps_in - eps_out for eps_in, eps_out in zip(eps_in_perps, eps_out_perps)]
24982498
delta_eps_inv_normal = 1.0 / eps_in_normal - 1.0 / eps_out_normal
24992499

2500-
def integrate_face(arr: xr.DataArray) -> complex:
2500+
def integrate_face(
2501+
arr: xr.DataArray,
2502+
snap_coords_outside: bool = False,
2503+
singularity_correction=None,
2504+
integration_dims=dims_perp,
2505+
integration_bounds=bounds_perp,
2506+
) -> complex:
25012507
"""Interpolate and integrate a scalar field data over the face using bounds."""
25022508

2503-
arr_at_face = arr.interp(**{dim_normal: float(coord_normal_face)}, assume_sorted=True)
2509+
edge_correction = 1.0
2510+
2511+
interpolation_point = coord_normal_face
2512+
if snap_coords_outside:
2513+
snap_coords_values = arr.coords[dim_normal].values
2514+
2515+
index_face = np.argmin(np.abs(snap_coords_values - coord_normal_face))
2516+
2517+
if min_max_index == 0:
2518+
min_boundary_mapping = np.where(snap_coords_values > coord_normal_face)[0]
2519+
index_face = (
2520+
0
2521+
if (len(min_boundary_mapping) == 0)
2522+
else np.maximum(0, min_boundary_mapping[0] - 1)
2523+
)
2524+
2525+
if snap_coords_values[index_face] > coord_normal_face:
2526+
log.warning("Was not able to snap coordinates outside of min face.")
2527+
else:
2528+
max_boundary_mapping = np.where(snap_coords_values < coord_normal_face)[0]
2529+
index_face = (
2530+
0
2531+
if (len(max_boundary_mapping) == 0)
2532+
else np.minimum(len(snap_coords_values) - 1, max_boundary_mapping[-1] + 1)
2533+
)
2534+
2535+
if snap_coords_values[index_face] < coord_normal_face:
2536+
log.warning("Was not able to snap coordinates outside of max face.")
2537+
2538+
interpolation_point = snap_coords_values[index_face]
2539+
2540+
if singularity_correction:
2541+
edge_correction = singularity_correction(
2542+
np.abs(interpolation_point - coord_normal_face)
2543+
)
2544+
2545+
arr_at_face = (
2546+
arr
2547+
if (len(arr.coords[dim_normal]) == 1)
2548+
else arr.interp(**{dim_normal: float(interpolation_point)}, assume_sorted=True)
2549+
)
25042550

25052551
integral_result = integrate_within_bounds(
25062552
arr=arr_at_face,
2507-
dims=dims_perp,
2508-
bounds=bounds_perp,
2553+
dims=integration_dims,
2554+
bounds=integration_bounds,
25092555
)
25102556

2511-
return complex(integral_result.sum("f"))
2557+
return complex(edge_correction * integral_result.sum("f"))
25122558

25132559
# compute vjp from field integrals
25142560
vjp_value = 0.0
25152561

2516-
# perform D-normal integral
2517-
integrand_D = -delta_eps_inv_normal * D_normal
2518-
integral_D = integrate_face(integrand_D)
2519-
vjp_value += integral_D
2562+
if derivative_info.is_medium_pec:
2563+
Hs_perp = tuple(derivative_info.H_der_map[key] for key in flds_H_perp)
2564+
2565+
# detect whether the box is 2-dimensional
2566+
zero_size_map = [s == 0.0 for s in self.size]
2567+
2568+
dimension = 3 - np.sum(zero_size_map)
2569+
if dimension < 2:
2570+
log.error(
2571+
"Derivative of PEC material with less than 2 dimensions is unsupported. "
2572+
"Specified PEC box is {dimension}-dimesional"
2573+
)
2574+
2575+
do_singularity_correction = False
2576+
is_2d = np.any(zero_size_map)
2577+
if is_2d:
2578+
zero_dim_idx = np.where(zero_size_map)[0][0]
2579+
zero_dimension = "xyz"[zero_dim_idx]
2580+
2581+
# for singularity correction, need 2-dimensional box and that
2582+
# the face we are integrating over is the 1-dimensional (i.e. -
2583+
# the normal for the face is not the same as the flat dimension
2584+
do_singularity_correction = not (axis_normal == zero_dim_idx)
2585+
2586+
# apply singularity correction only when integrating along the line
2587+
singularity_correction = (
2588+
(lambda d: 0.5 * np.pi * d) if do_singularity_correction else (lambda d: 1.0)
2589+
)
2590+
2591+
integration_dims = dims_perp
2592+
integration_bounds = bounds_perp
2593+
# if we are correcting for singularity and integrating over the line, then adjust
2594+
# integration dimensions and bounds to exclude the flat dimension
2595+
if do_singularity_correction:
2596+
integration_dims = [dim for dim in dims_perp if (not (dim == zero_dimension))]
2597+
2598+
integration_bounds = []
2599+
zero_dim_idx = 0
2600+
# find the index into the bounds that corresponds to the flat dimension
2601+
for dim in dims_perp:
2602+
if dim == zero_dimension:
2603+
break
2604+
zero_dim_idx += 1
2605+
2606+
# trim the zero dimension from the integration bounds
2607+
for bound in bounds_perp:
2608+
new_bound = [b for b_idx, b in enumerate(bound) if b_idx != zero_dim_idx]
2609+
2610+
integration_bounds.append(new_bound)
2611+
2612+
integrand_E = D_normal / np.real(eps_out_normal)
2613+
2614+
integral_E = integrate_face(
2615+
integrand_E,
2616+
snap_coords_outside=True,
2617+
singularity_correction=singularity_correction,
2618+
integration_dims=integration_dims,
2619+
integration_bounds=integration_bounds,
2620+
)
25202621

2521-
# perform E-perpendicular integrals
2522-
for E_perp, delta_eps_perp in zip(Es_perp, delta_eps_perps):
2523-
integrand_E = E_perp * delta_eps_perp
2524-
integral_E = integrate_face(integrand_E)
25252622
vjp_value += integral_E
25262623

2624+
for H_perp_idx, H_perp in enumerate(Hs_perp):
2625+
integrand_H = MU_0 * H_perp / EPSILON_0
2626+
2627+
if (is_2d and not (flds_H_perp[H_perp_idx] == f"H{zero_dimension}")) and (
2628+
dim_normal != zero_dimension
2629+
):
2630+
continue
2631+
2632+
integral_H = integrate_face(
2633+
integrand_H,
2634+
snap_coords_outside=True,
2635+
singularity_correction=singularity_correction,
2636+
integration_dims=integration_dims,
2637+
integration_bounds=integration_bounds,
2638+
)
2639+
2640+
vjp_value += integral_H
2641+
2642+
else:
2643+
integrand_D = -delta_eps_inv_normal * D_normal
2644+
integral_D = integrate_face(integrand_D)
2645+
vjp_value += integral_D
2646+
2647+
# perform E-perpendicular integrals
2648+
for E_perp, delta_eps_perp in zip(Es_perp, delta_eps_perps):
2649+
integrand_E = E_perp * delta_eps_perp
2650+
integral_E = integrate_face(integrand_E)
2651+
vjp_value += integral_E
2652+
25272653
return np.real(vjp_value)
25282654

25292655

tidy3d/components/geometry/polyslab.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1447,13 +1447,15 @@ def _compute_derivatives(self, derivative_info: DerivativeInfo) -> AutogradField
14471447
"""
14481448
vjps: AutogradFieldMap = {}
14491449

1450-
sim_min, sim_max = map(np.asarray, derivative_info.bounds_intersect)
1451-
extents = sim_max - sim_min
1450+
intersect_min, intersect_max = map(np.asarray, derivative_info.bounds_intersect)
1451+
sim_min, sim_max = map(np.asarray, derivative_info.simulation_bounds)
1452+
1453+
extents = intersect_max - intersect_min
14521454
is_2d = np.isclose(extents[self.axis], 0.0)
14531455

14541456
# early return if polyslab is not in simulation domain
14551457
slab_min, slab_max = self.slab_bounds
1456-
if (slab_max <= sim_min[self.axis]) or (slab_min >= sim_max[self.axis]):
1458+
if (slab_max < sim_min[self.axis]) or (slab_min > sim_max[self.axis]):
14571459
log.warning(
14581460
"'PolySlab' lies completely outside the simulation domain.",
14591461
log_once=True,
@@ -1473,6 +1475,7 @@ def _compute_derivatives(self, derivative_info: DerivativeInfo) -> AutogradField
14731475
vjps[path] = self._compute_derivative_vertices(
14741476
derivative_info, sim_min, sim_max, is_2d, interpolators
14751477
)
1478+
14761479
elif path[0] == "slab_bounds":
14771480
idx = path[1]
14781481
face_coord = self.slab_bounds[idx]
@@ -1508,6 +1511,12 @@ def _compute_derivative_slab_bounds(
15081511
is split equally between the two vertices that bound the edge segment.
15091512
"""
15101513
# rmin/rmax over the geometry and simulation box
1514+
if np.isclose(self.slab_bounds[1] - self.slab_bounds[0], 0.0):
1515+
log.warning(
1516+
"Computing slab face derivatives for flat structures is not fully supported and "
1517+
"may give zero for the derivative. Try using a structure with a small, but nonzero"
1518+
"thickness for slab bound derivatives."
1519+
)
15111520
rmin, rmax = derivative_info.bounds_intersect
15121521
_, (r1_min, r2_min) = self.pop_axis(rmin, axis=self.axis)
15131522
_, (r1_max, r2_max) = self.pop_axis(rmax, axis=self.axis)

tidy3d/components/structure.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -321,11 +321,15 @@ def _make_adjoint_monitors(
321321
else:
322322
interval_space = AUTOGRAD_MONITOR_INTERVAL_SPACE_POLY
323323

324+
field_components_for_adjoint = [f"E{dim}" for dim in "xyz"]
325+
if self.medium.is_pec:
326+
field_components_for_adjoint += [f"H{dim}" for dim in "xyz"]
327+
324328
mnt_fld = FieldMonitor(
325329
size=size,
326330
center=center,
327331
freqs=freqs,
328-
fields=("Ex", "Ey", "Ez"),
332+
fields=field_components_for_adjoint,
329333
name=self._get_monitor_name(index=index, data_type="fld"),
330334
interval_space=interval_space,
331335
colocate=False,

0 commit comments

Comments
 (0)