Skip to content

Commit 63c1c3d

Browse files
caseyflexmomchil-flex
authored andcommitted
Add translated_copy function to ElectromagneticFieldData
1 parent c74d1e4 commit 63c1c3d

File tree

6 files changed

+249
-18
lines changed

6 files changed

+249
-18
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1111
- Advanced option `dist_type` that allows `LinearLumpedElement` to be distributed across grid cells in different ways. For example, restricting the network portion to a single cell and using PEC wires to connect to the desired location of the terminals.
1212
- New `layer_refinement_specs` field in `GridSpec` that takes a list of `LayerRefinementSpec` for automatic mesh refinement and snapping in layered structures. Structure corners on the cross section perpendicular to layer thickness direction can be automatically identified. Mesh is automatically snapped and refined around those corners.
1313
- New field `drop_outside_sim` in `MeshOverrideStructure` to specify whether to drop an override structure if it is outside the simulation domain, but it overlaps with the simulation domain when projected to an axis.
14+
- Function `translated_copy` in `ElectromagneticFieldData` to facilitate computing overlaps between field data at different locations.
1415

1516
### Changed
1617
- The coordinate of snapping points in `GridSpec` can take value `None`, so that mesh can be selectively snapped only along certain dimensions.

tests/test_data/test_data_arrays.py

Lines changed: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -120,20 +120,24 @@
120120
def get_xyz(
121121
monitor: td.components.monitor.MonitorType, grid_key: str, symmetry: bool
122122
) -> Tuple[List[float], List[float], List[float]]:
123-
if symmetry:
124-
grid = SIM_SYM.discretize_monitor(monitor)
123+
sim = SIM_SYM if symmetry else SIM
124+
grid = sim.discretize_monitor(monitor)
125+
if monitor.colocate:
126+
x, y, z = grid.boundaries.to_list
127+
else:
125128
x, y, z = grid[grid_key].to_list
129+
if symmetry:
126130
x = [_x for _x in x if _x >= 0]
127131
y = [_y for _y in y if _y >= 0]
128132
z = [_z for _z in z if _z >= 0]
129-
else:
130-
grid = SIM.discretize_monitor(monitor)
131-
x, y, z = grid[grid_key].to_list
132133
return x, y, z
133134

134135

135-
def make_scalar_field_data_array(grid_key: str, symmetry=True):
136-
XS, YS, ZS = get_xyz(FIELD_MONITOR, grid_key, symmetry)
136+
def make_scalar_field_data_array(grid_key: str, symmetry=True, colocate: bool = None):
137+
monitor = FIELD_MONITOR
138+
if colocate is not None:
139+
monitor = monitor.updated_copy(colocate=colocate)
140+
XS, YS, ZS = get_xyz(monitor, grid_key, symmetry)
137141
values = (1 + 1j) * np.random.random((len(XS), len(YS), len(ZS), len(FS)))
138142
return td.ScalarFieldDataArray(values, coords=dict(x=XS, y=YS, z=ZS, f=FS))
139143

@@ -144,8 +148,11 @@ def make_scalar_field_time_data_array(grid_key: str, symmetry=True):
144148
return td.ScalarFieldTimeDataArray(values, coords=dict(x=XS, y=YS, z=ZS, t=TS))
145149

146150

147-
def make_scalar_mode_field_data_array(grid_key: str, symmetry=True):
148-
XS, YS, ZS = get_xyz(MODE_MONITOR_WITH_FIELDS, grid_key, symmetry)
151+
def make_scalar_mode_field_data_array(grid_key: str, symmetry=True, colocate: bool = None):
152+
monitor = MODE_MONITOR_WITH_FIELDS
153+
if colocate is not None:
154+
monitor = monitor.updated_copy(colocate=colocate)
155+
XS, YS, ZS = get_xyz(monitor, grid_key, symmetry)
149156
values = (1 + 0.1j) * np.random.random((len(XS), 1, len(ZS), len(FS), len(MODE_INDICES)))
150157

151158
return td.ScalarModeFieldDataArray(

tests/test_data/test_monitor_data.py

Lines changed: 90 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -168,9 +168,9 @@ def make_permittivity_data(symmetry: bool = True):
168168
sim = SIM_SYM if symmetry else SIM
169169
return PermittivityData(
170170
monitor=PERMITTIVITY_MONITOR,
171-
eps_xx=make_scalar_field_data_array("Ex", symmetry),
172-
eps_yy=make_scalar_field_data_array("Ey", symmetry),
173-
eps_zz=make_scalar_field_data_array("Ez", symmetry),
171+
eps_xx=make_scalar_field_data_array("Ex", symmetry, colocate=False),
172+
eps_yy=make_scalar_field_data_array("Ey", symmetry, colocate=False),
173+
eps_zz=make_scalar_field_data_array("Ez", symmetry, colocate=False),
174174
symmetry=sim.symmetry,
175175
symmetry_center=sim.center,
176176
grid_expanded=sim.discretize_monitor(PERMITTIVITY_MONITOR),
@@ -234,7 +234,7 @@ def test_field_data():
234234
# Compute flux as dot product with itself
235235
flux2 = np.abs(data_2d.dot(data_2d))
236236
# Assert result is the same
237-
assert np.all(flux1 == flux2)
237+
assert np.allclose(flux1, flux2)
238238

239239

240240
def test_field_data_to_source():
@@ -267,7 +267,7 @@ def test_mode_solver_data():
267267
# Compute flux as dot product with itself
268268
flux2 = np.abs(data.dot(data))
269269
# Assert result is the same
270-
assert np.all(flux1 == flux2)
270+
assert np.allclose(flux1, flux2)
271271
# Compute dot product with a field data
272272
field_data = make_field_data_2d()
273273
dot = data.dot(field_data)
@@ -611,6 +611,91 @@ def isel(data, freqs):
611611
assert len(dot.f) == 2
612612

613613

614+
def test_translated_copy():
615+
mode_data = make_mode_solver_data()
616+
field_data = make_field_data_2d()
617+
618+
vector = (1, 0, 0)
619+
mode_data_translated = mode_data.translated_copy(vector=vector)
620+
field_data_translated = field_data.translated_copy(vector=vector)
621+
622+
field1 = mode_data.symmetry_expanded_copy.Ex.isel(mode_index=0, f=0)
623+
field2 = mode_data_translated.symmetry_expanded_copy.Ex.isel(mode_index=0, f=0)
624+
625+
atol = 1e-10
626+
627+
assert np.allclose(field1.data, field2.data)
628+
629+
assert np.allclose(
630+
mode_data.dot(mode_data), mode_data_translated.dot(mode_data_translated), atol=atol
631+
)
632+
assert np.allclose(
633+
mode_data.outer_dot(mode_data),
634+
mode_data_translated.outer_dot(mode_data_translated),
635+
atol=atol,
636+
)
637+
assert np.allclose(
638+
mode_data.dot(field_data), mode_data_translated.dot(field_data_translated), atol=atol
639+
)
640+
assert np.allclose(
641+
mode_data.outer_dot(field_data),
642+
mode_data_translated.outer_dot(field_data_translated),
643+
atol=atol,
644+
)
645+
assert np.allclose(
646+
field_data.dot(mode_data), field_data_translated.dot(mode_data_translated), atol=atol
647+
)
648+
assert np.allclose(
649+
field_data.outer_dot(mode_data),
650+
field_data_translated.outer_dot(mode_data_translated),
651+
atol=atol,
652+
)
653+
assert np.allclose(
654+
field_data.dot(field_data), field_data_translated.dot(field_data_translated), atol=atol
655+
)
656+
assert np.allclose(
657+
field_data.outer_dot(field_data),
658+
field_data_translated.outer_dot(field_data_translated),
659+
atol=atol,
660+
)
661+
662+
assert np.allclose(
663+
mode_data.dot(mode_data),
664+
mode_data_translated.translated_copy(vector=[-v for v in vector]).dot(mode_data),
665+
atol=atol,
666+
)
667+
668+
assert np.allclose(
669+
mode_data.outer_dot(mode_data),
670+
mode_data_translated.translated_copy(vector=[-v for v in vector]).outer_dot(mode_data),
671+
atol=atol,
672+
)
673+
674+
# test warning for mismatch between monitor and field colocation
675+
# monitor colocated, data colocated
676+
with AssertLogLevel(None):
677+
_ = mode_data.symmetry_expanded_copy
678+
monitor = mode_data.monitor.updated_copy(colocate=False)
679+
grid_expanded = SIM_SYM.discretize_monitor(monitor)
680+
mode_data_warn1 = mode_data.updated_copy(monitor=monitor, grid_expanded=grid_expanded)
681+
# monitor not colocated, data colocated
682+
with AssertLogLevel("WARNING", contains_str="Interpolating"):
683+
_ = mode_data_warn1.symmetry_expanded_copy
684+
field_kwargs = {}
685+
for key in mode_data.field_components.keys():
686+
field_kwargs[key] = make_scalar_mode_field_data_array(key, colocate=False)
687+
mode_data_warn2 = mode_data.updated_copy(**field_kwargs)
688+
# monitor colocated, data not colocated
689+
with AssertLogLevel("WARNING", contains_str="Interpolating"):
690+
_ = mode_data_warn2.symmetry_expanded_copy
691+
# neither colocated
692+
mode_data_uncolocated = mode_data_warn2.updated_copy(
693+
monitor=monitor, grid_expanded=grid_expanded
694+
)
695+
with AssertLogLevel(None):
696+
_ = mode_data_uncolocated.symmetry_expanded_copy
697+
698+
614699
@pytest.mark.parametrize("phase_shift", np.linspace(0, 2 * np.pi, 10))
615700
def test_field_data_phase(phase_shift):
616701
def get_combined_phase(data):

tests/test_plugins/test_mode_solver.py

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1202,3 +1202,45 @@ def test_gauge_robustness():
12021202

12031203
array[1, -1] = np.nan
12041204
assert ModeSolver._weighted_coord_max(array, ij, ij) == (0, 0)
1205+
1206+
1207+
def test_translated_dot():
1208+
sim_size = (5, 5, 5)
1209+
lambda0 = 1.55
1210+
freq0 = td.C_0 / lambda0
1211+
si = td.material_library["cSi"]["Li1993_293K"]
1212+
sio2 = td.material_library["SiO2"]["Horiba"]
1213+
wg = td.Structure(geometry=td.Box(size=(0.22, 0.5, td.inf)), medium=si)
1214+
mode_spec = td.ModeSpec(num_modes=3)
1215+
grid_spec = td.GridSpec.auto(wavelength=lambda0, min_steps_per_wvl=20)
1216+
1217+
sim = td.Simulation(
1218+
size=sim_size, medium=sio2, structures=[wg], grid_spec=grid_spec, run_time=1e-30
1219+
)
1220+
mode_plane = td.Box(size=(3, 3, 0))
1221+
mode_solver = ModeSolver(simulation=sim, plane=mode_plane, mode_spec=mode_spec, freqs=[freq0])
1222+
1223+
data = mode_solver.data_raw
1224+
1225+
# now create a translated copy
1226+
vector = (0.5, 0, 0)
1227+
mode_solver2 = mode_solver.updated_copy(center=vector, path="simulation/structures/0/geometry")
1228+
1229+
data2 = mode_solver2.data_raw
1230+
1231+
# self-overlaps are close to 1, others are close to 0
1232+
atol = 1e-2
1233+
1234+
# just make sure the mode overlaps in the translated waveguide are the same
1235+
assert np.allclose(data.dot(data), data2.dot(data2), atol=atol)
1236+
assert np.allclose(data.outer_dot(data), data2.outer_dot(data2), atol=atol)
1237+
1238+
# now translate the data, and check that its overlaps with the modes
1239+
# of the translated waveguide agree with the self-overlaps of those modes
1240+
data_translated = data.translated_copy(vector)
1241+
1242+
assert np.allclose(data2.dot(data_translated), data2.dot(data2), atol=atol)
1243+
assert np.allclose(data_translated.dot(data2), data2.dot(data2), atol=atol)
1244+
1245+
assert np.allclose(data2.outer_dot(data_translated), data2.outer_dot(data2), atol=atol)
1246+
assert np.allclose(data_translated.outer_dot(data2), data2.outer_dot(data2), atol=atol)

tidy3d/components/data/monitor_data.py

Lines changed: 89 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,7 @@ def _symmetry_update_dict(self) -> Dict:
246246
"""Dictionary of data fields to create data with expanded symmetry."""
247247

248248
update_dict = {}
249+
warn_interp = False
249250
for field_name, scalar_data in self.field_components.items():
250251
eigenval_fn = self.symmetry_eigenvalues[field_name]
251252

@@ -273,8 +274,44 @@ def _symmetry_update_dict(self) -> Dict:
273274
# Interpolate. There generally shouldn't be values out of bounds except potentially
274275
# when handling modes, in which case they should be at the boundary and close to 0.
275276

276-
scalar_data = scalar_data.sel(**{dim_name: coords_interp}, method="nearest")
277-
scalar_data = scalar_data.assign_coords({dim_name: coords})
277+
# using sel vs interp is faster, and should always be fine
278+
# if the data is set up correctly such that its colocation
279+
# matches the monitor colocation settings. If these do not match,
280+
# then we need to interpolate, which is slower.
281+
use_sel = (
282+
len(scalar_data.coords[dim_name]) == 1
283+
or coords[-1] in scalar_data.coords[dim_name]
284+
)
285+
if use_sel:
286+
scalar_data = scalar_data.sel(**{dim_name: coords_interp}, method="nearest")
287+
scalar_data = scalar_data.assign_coords({dim_name: coords})
288+
else:
289+
warn_interp = True
290+
no_flip_inds = np.where(coords >= sym_loc)[0]
291+
scalar_data_arrays = []
292+
if len(scalar_data.coords[dim_name]) == 1:
293+
scalar_data = scalar_data.sel(**{dim_name: coords_interp}, method="nearest")
294+
else:
295+
if len(flip_inds) > 0:
296+
scalar_data_flip = scalar_data.interp(
297+
**{dim_name: coords_interp[flip_inds][::-1]},
298+
method="linear",
299+
kwargs={"fill_value": "extrapolate"},
300+
assume_sorted=True,
301+
).isel({dim_name: slice(None, None, -1)})
302+
scalar_data_flip = scalar_data_flip.assign_coords(
303+
{dim_name: coords[flip_inds]}
304+
)
305+
scalar_data_arrays.append(scalar_data_flip)
306+
if len(no_flip_inds) > 0:
307+
scalar_data_no_flip = scalar_data.interp(
308+
**{dim_name: coords_interp[no_flip_inds]},
309+
method="linear",
310+
kwargs={"fill_value": "extrapolate"},
311+
assume_sorted=True,
312+
)
313+
scalar_data_arrays.append(scalar_data_no_flip)
314+
scalar_data = xr.concat(scalar_data_arrays, dim=dim_name)
278315

279316
# apply the symmetry eigenvalue (if defined) to the flipped values
280317
if eigenval_fn is not None:
@@ -288,6 +325,13 @@ def _symmetry_update_dict(self) -> Dict:
288325

289326
update_dict.update({"symmetry": (0, 0, 0), "symmetry_center": None})
290327

328+
if warn_interp:
329+
log.warning(
330+
"Interpolating 'ElectromagneticFieldData'. This may be due to "
331+
"mismatch between monitor colocation and data colocation, "
332+
"and can lead to performance issues."
333+
)
334+
291335
return update_dict
292336

293337
def at_coords(self, coords: Coords) -> xr.Dataset:
@@ -686,10 +730,11 @@ def dot(
686730
# Tangential fields for current and other field data
687731
fields_self = self._colocated_tangential_fields
688732

689-
fields_other = field_data._colocated_tangential_fields
690733
if conjugate:
691734
fields_self = {key: field.conj() for key, field in fields_self.items()}
692735

736+
fields_other = field_data._interpolated_tangential_fields(self._plane_grid_boundaries)
737+
693738
# Drop size-1 dimensions in the other data
694739
fields_other = {key: field.squeeze(drop=True) for key, field in fields_other.items()}
695740

@@ -703,6 +748,7 @@ def dot(
703748
# Integrate over plane
704749
d_area = self._diff_area
705750
integrand = (e_self_x_h_other - h_self_x_e_other) * d_area
751+
706752
return ModeAmpsDataArray(0.25 * integrand.sum(dim=d_area.dims))
707753

708754
def _interpolated_tangential_fields(self, coords: ArrayFloat2D) -> Dict[str, DataArray]:
@@ -941,6 +987,46 @@ def _check_fields_stored(self, components: list[str]):
941987
"the 'fields' argument of a field monitor to select which components are stored."
942988
)
943989

990+
def translated_copy(self, vector: Coordinate) -> ElectromagneticFieldData:
991+
"""Create a copy of the :class:`.ElectromagneticFieldData` with fields translated
992+
by the provided vector. Can be used together with ``dot`` or ``outer_dot``
993+
to compute overlaps between field data at different locations.
994+
995+
Parameters
996+
----------
997+
vector: :class:`.Coordinate`
998+
Translation vector to apply to the field data.
999+
1000+
Returns
1001+
-------
1002+
:class:`ElectromagneticFieldData`
1003+
A data object with the translated fields.
1004+
"""
1005+
field_kwargs = {}
1006+
for key, val in self.field_components.items():
1007+
coords = dict(val.coords)
1008+
coords["x"] = coords["x"] + vector[0]
1009+
coords["y"] = coords["y"] + vector[1]
1010+
coords["z"] = coords["z"] + vector[2]
1011+
field_kwargs[key] = val.assign_coords(coords)
1012+
1013+
symmetry_center = self.symmetry_center
1014+
if symmetry_center is not None:
1015+
symmetry_center = tuple([x + y for (x, y) in zip(symmetry_center, vector)])
1016+
grid_expanded = self.grid_expanded._translated_copy(vector=vector)
1017+
1018+
monitor_center = tuple([x + y for (x, y) in zip(self.monitor.center, vector)])
1019+
monitor = self.monitor.updated_copy(center=monitor_center)
1020+
1021+
return self.updated_copy(
1022+
monitor=monitor,
1023+
symmetry=self.symmetry,
1024+
symmetry_center=symmetry_center,
1025+
grid_expanded=grid_expanded,
1026+
**self._grid_correction_dict,
1027+
**field_kwargs,
1028+
)
1029+
9441030

9451031
class FieldData(FieldDataset, ElectromagneticFieldData):
9461032
"""

tidy3d/components/grid/grid.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from ..data.data_array import DataArray, ScalarFieldDataArray, SpatialDataArray
1313
from ..data.utils import UnstructuredGridDataset, UnstructuredGridDatasetType
1414
from ..geometry.base import Box
15-
from ..types import ArrayFloat1D, Axis, InterpMethod, Literal
15+
from ..types import ArrayFloat1D, Axis, Coordinate, InterpMethod, Literal
1616

1717
# data type of one dimensional coordinate array.
1818
Coords1D = ArrayFloat1D
@@ -625,3 +625,13 @@ def snap_to_box_zero_dim(self, box: Box):
625625
raise ValueError("Cannot snap grid to box center outside of grid domain.")
626626
boundary_dict[dim] = np.array([center, center])
627627
return self.updated_copy(boundaries=Coords(**boundary_dict))
628+
629+
def _translated_copy(self, vector: Coordinate) -> Grid:
630+
"""Translate the grid by a vector. Not officially supported as resulting
631+
grid may not be aligned with original Yee grid."""
632+
boundaries = Coords(
633+
x=self.boundaries.x + vector[0],
634+
y=self.boundaries.y + vector[1],
635+
z=self.boundaries.z + vector[2],
636+
)
637+
return self.updated_copy(boundaries=boundaries)

0 commit comments

Comments
 (0)