Skip to content

Commit 04e212a

Browse files
committed
maint[autograd]: raise error when differentiating server-side field projection
1 parent 1000bda commit 04e212a

File tree

4 files changed

+150
-7
lines changed

4 files changed

+150
-7
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Changed
11+
- Attempting to differentiate server-side field projections now raises a clear error instead of silently failing.
12+
1013
## [2.8.3] - 2025-04-24
1114

1215
### Added

tests/test_components/test_autograd.py

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1260,7 +1260,7 @@ def objective(args):
12601260
class TestFieldProjection:
12611261
@staticmethod
12621262
def setup(far_field_approx, projection_type, sim_2d):
1263-
if sim_2d and not far_field_approx:
1263+
if (sim_2d or not IS_3D) and not far_field_approx:
12641264
pytest.skip("Exact field projection not implemented for 2d simulations")
12651265

12661266
r_proj = 50 * WVL
@@ -1274,39 +1274,36 @@ def setup(far_field_approx, projection_type, sim_2d):
12741274

12751275
if projection_type == "angular":
12761276
theta_proj = np.linspace(np.pi / 10, np.pi - np.pi / 10, 2)
1277-
phi_proj = np.linspace(np.pi / 10, np.pi - np.pi / 10, 3)
12781277
monitor_far = td.FieldProjectionAngleMonitor(
12791278
center=monitor.center,
12801279
size=monitor.size,
12811280
freqs=monitor.freqs,
1282-
phi=tuple(phi_proj),
1281+
phi=(np.pi / 2, 3 * np.pi / 2),
12831282
theta=tuple(theta_proj),
12841283
proj_distance=r_proj,
12851284
far_field_approx=far_field_approx,
12861285
name="far_field",
12871286
)
12881287
elif projection_type == "cartesian":
1289-
x_proj = np.linspace(-10, 10, 2)
12901288
y_proj = np.linspace(-10, 10, 3)
12911289
monitor_far = td.FieldProjectionCartesianMonitor(
12921290
center=monitor.center,
12931291
size=monitor.size,
12941292
freqs=monitor.freqs,
1295-
x=x_proj,
1293+
x=[0],
12961294
y=y_proj,
12971295
proj_axis=1,
12981296
proj_distance=r_proj,
12991297
far_field_approx=far_field_approx,
13001298
name="far_field",
13011299
)
13021300
elif projection_type == "kspace":
1303-
ux = np.linspace(-0.7, 0.7, 2)
13041301
uy = np.linspace(-0.7, 0.7, 3)
13051302
monitor_far = td.FieldProjectionKSpaceMonitor(
13061303
center=monitor.center,
13071304
size=monitor.size,
13081305
freqs=monitor.freqs,
1309-
ux=ux,
1306+
ux=[0],
13101307
uy=uy,
13111308
proj_axis=1,
13121309
proj_distance=r_proj,
@@ -1370,6 +1367,26 @@ def objective(x0):
13701367

13711368
check_grads(objective, modes=["rev"], order=1)(1.0)
13721369

1370+
def test_error_if_server_side_projection(
1371+
self, use_emulated_run, far_field_approx, projection_type, sim_2d
1372+
):
1373+
"""Using a far field monitor directly should error"""
1374+
# build a projection‐only monitor sim
1375+
sim_base, monitor_far = self.setup(far_field_approx, projection_type, sim_2d)
1376+
sim_base = sim_base.updated_copy(monitors=[monitor_far])
1377+
1378+
def objective(args):
1379+
structures_traced_dict = make_structures(args)
1380+
structures = list(SIM_BASE.structures)
1381+
for structure_key in structure_keys_:
1382+
structures.append(structures_traced_dict[structure_key])
1383+
sim = sim_base.updated_copy(structures=structures)
1384+
sim_data = run(sim, task_name="field_projection_test")
1385+
return sim_data["far_field"].power.sum().item()
1386+
1387+
with pytest.raises(NotImplementedError):
1388+
ag.grad(objective)(params0)
1389+
13731390

13741391
def test_autograd_deepcopy():
13751392
"""make sure deepcopy works as expected in autograd."""

tests/utils.py

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1092,6 +1092,112 @@ def make_directivity_data(monitor: td.DirectivityMonitor) -> td.DirectivityData:
10921092
projection_surfaces=monitor.projection_surfaces,
10931093
)
10941094

1095+
def make_field_projection_angle_data(
1096+
monitor: td.FieldProjectionAngleMonitor,
1097+
) -> td.FieldProjectionAngleData:
1098+
"""Random FieldProjectionAngleData from a FieldProjectionAngleMonitor."""
1099+
f = list(monitor.freqs)
1100+
r = np.atleast_1d(getattr(monitor, "proj_distance", 1.0))
1101+
theta = list(monitor.theta)
1102+
phi = list(monitor.phi)
1103+
1104+
coords = dict(r=r, theta=theta, phi=phi, f=f)
1105+
scalar_field = make_data(
1106+
coords=coords,
1107+
data_array_type=td.FieldProjectionAngleDataArray,
1108+
is_complex=True,
1109+
)
1110+
1111+
return td.FieldProjectionAngleData(
1112+
monitor=monitor,
1113+
Er=scalar_field,
1114+
Etheta=scalar_field,
1115+
Ephi=scalar_field,
1116+
Hr=scalar_field,
1117+
Htheta=scalar_field,
1118+
Hphi=scalar_field,
1119+
projection_surfaces=monitor.projection_surfaces,
1120+
)
1121+
1122+
def make_field_projection_cartesian_data(
1123+
monitor: td.FieldProjectionCartesianMonitor,
1124+
) -> td.FieldProjectionCartesianData:
1125+
"""Random FieldProjectionCartesianData from a FieldProjectionCartesianMonitor."""
1126+
1127+
f = list(monitor.freqs)
1128+
proj_distance = getattr(monitor, "proj_distance", 1.0)
1129+
1130+
# in-plane grids always come from monitor.x and monitor.y
1131+
x_plane = list(monitor.x)
1132+
y_plane = list(monitor.y)
1133+
1134+
# map the two planes to global (x, y, z) depending on the normal axis
1135+
if monitor.proj_axis == 0: # (y, z)
1136+
coords = dict(
1137+
x=np.atleast_1d(proj_distance),
1138+
y=x_plane,
1139+
z=y_plane,
1140+
f=f,
1141+
)
1142+
elif monitor.proj_axis == 1: # (x, z)
1143+
coords = dict(
1144+
x=x_plane,
1145+
y=np.atleast_1d(proj_distance),
1146+
z=y_plane,
1147+
f=f,
1148+
)
1149+
else: # (x, y)
1150+
coords = dict(
1151+
x=x_plane,
1152+
y=y_plane,
1153+
z=np.atleast_1d(proj_distance),
1154+
f=f,
1155+
)
1156+
1157+
scalar_field = make_data(
1158+
coords=coords,
1159+
data_array_type=td.FieldProjectionCartesianDataArray,
1160+
is_complex=True,
1161+
)
1162+
1163+
return td.FieldProjectionCartesianData(
1164+
monitor=monitor,
1165+
Er=scalar_field,
1166+
Etheta=scalar_field,
1167+
Ephi=scalar_field,
1168+
Hr=scalar_field,
1169+
Htheta=scalar_field,
1170+
Hphi=scalar_field,
1171+
projection_surfaces=monitor.projection_surfaces,
1172+
)
1173+
1174+
def make_field_projection_kspace_data(
1175+
monitor: td.FieldProjectionKSpaceMonitor,
1176+
) -> td.FieldProjectionKSpaceData:
1177+
"""Random FieldProjectionKSpaceData from a FieldProjectionKSpaceMonitor."""
1178+
f = list(monitor.freqs)
1179+
r = np.atleast_1d(getattr(monitor, "proj_distance", 1.0))
1180+
ux = list(monitor.ux)
1181+
uy = list(monitor.uy)
1182+
1183+
coords = dict(ux=ux, uy=uy, r=r, f=f)
1184+
scalar_field = make_data(
1185+
coords=coords,
1186+
data_array_type=td.FieldProjectionKSpaceDataArray,
1187+
is_complex=True,
1188+
)
1189+
1190+
return td.FieldProjectionKSpaceData(
1191+
monitor=monitor,
1192+
Er=scalar_field,
1193+
Etheta=scalar_field,
1194+
Ephi=scalar_field,
1195+
Hr=scalar_field,
1196+
Htheta=scalar_field,
1197+
Hphi=scalar_field,
1198+
projection_surfaces=monitor.projection_surfaces,
1199+
)
1200+
10951201
MONITOR_MAKER_MAP = {
10961202
td.FieldMonitor: make_field_data,
10971203
td.FieldTimeMonitor: make_field_time_data,
@@ -1101,6 +1207,9 @@ def make_directivity_data(monitor: td.DirectivityMonitor) -> td.DirectivityData:
11011207
td.DiffractionMonitor: make_diff_data,
11021208
td.FluxMonitor: make_flux_data,
11031209
td.DirectivityMonitor: make_directivity_data,
1210+
td.FieldProjectionAngleMonitor: make_field_projection_angle_data,
1211+
td.FieldProjectionCartesianMonitor: make_field_projection_cartesian_data,
1212+
td.FieldProjectionKSpaceMonitor: make_field_projection_kspace_data,
11041213
}
11051214

11061215
data = [MONITOR_MAKER_MAP[type(mnt)](mnt) for mnt in simulation.monitors]

tidy3d/components/data/monitor_data.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2433,6 +2433,20 @@ def radar_cross_section(self) -> DataArray:
24332433

24342434
return self.make_data_array(data=rcs_data)
24352435

2436+
def make_adjoint_sources(
2437+
self, dataset_names: list[str], fwidth: float
2438+
) -> List[Union[CustomCurrentSource, PointDipole]]:
2439+
"""Error if server-side field projection is used for autograd"""
2440+
2441+
raise NotImplementedError(
2442+
"Adjoint is currently not implemented for server-side field projections. "
2443+
"To compute derivatives with respect to field projection data, please use a 'FieldMonitor' "
2444+
"and use a local projection in your objective function via 'FieldProjector.from_near_field_monitors'. "
2445+
"Using field projection monitors directly is not supported as the full field information is required "
2446+
"to construct the adjoint source for this problem. The field projection data does not contain the "
2447+
"information necessary for gradient computation."
2448+
)
2449+
24362450

24372451
class FieldProjectionAngleData(AbstractFieldProjectionData):
24382452
"""Data associated with a :class:`.FieldProjectionAngleMonitor`: components of projected fields.

0 commit comments

Comments
 (0)