Skip to content

Commit ad907b5

Browse files
committed
Mode solver symmetries reorganized to come from simulation symmetries
1 parent c145f37 commit ad907b5

File tree

8 files changed

+280
-139
lines changed

8 files changed

+280
-139
lines changed

tests/test_components.py

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ def test_sim():
5959
sources=[
6060
VolumeSource(
6161
size=(0, 0, 0),
62-
center=(0, -50.5, 0),
62+
center=(0, -0.5, 0),
6363
polarization="Hx",
6464
source_time=GaussianPulse(
6565
freq0=1e14,
@@ -819,3 +819,57 @@ def surface_monitor_helper(center, size, monitor_test):
819819
# z+ surface
820820
assert monitor_surfaces[5].center == (center[0], center[1], center[2] + size[2] / 2.0)
821821
assert monitor_surfaces[5].size == (size[0], size[1], 0.0)
822+
823+
824+
""" modes """
825+
826+
827+
def test_mode_object_syms():
828+
"""Test that errors are raised if a mode object is not placed right in the presence of syms."""
829+
g = GaussianPulse(freq0=1, fwidth=0.1)
830+
831+
# wrong mode source
832+
with pytest.raises(SetupError) as e_info:
833+
sim = Simulation(
834+
center=(1.0, -1.0, 0.5),
835+
size=(2.0, 2.0, 2.0),
836+
grid_size=(0.01, 0.01, 0.01),
837+
run_time=1e-12,
838+
symmetry=(1, -1, 0),
839+
sources=[ModeSource(size=(2, 2, 0), direction="+", source_time=g)],
840+
)
841+
842+
# wrong mode monitor
843+
with pytest.raises(SetupError) as e_info:
844+
sim = Simulation(
845+
center=(1.0, -1.0, 0.5),
846+
size=(2.0, 2.0, 2.0),
847+
grid_size=(0.01, 0.01, 0.01),
848+
run_time=1e-12,
849+
symmetry=(1, -1, 0),
850+
monitors=[ModeMonitor(size=(2, 2, 0), name="mnt", freqs=[2], mode_spec=ModeSpec())],
851+
)
852+
853+
# right mode source (centered on the symmetry)
854+
sim = Simulation(
855+
center=(1.0, -1.0, 0.5),
856+
size=(2.0, 2.0, 2.0),
857+
grid_size=(0.01, 0.01, 0.01),
858+
run_time=1e-12,
859+
symmetry=(1, -1, 0),
860+
sources=[ModeSource(center=(1, -1, 1), size=(2, 2, 0), direction="+", source_time=g)],
861+
)
862+
863+
# right mode monitor (entirely in the main quadrant)
864+
sim = Simulation(
865+
center=(1.0, -1.0, 0.5),
866+
size=(2.0, 2.0, 2.0),
867+
grid_size=(0.01, 0.01, 0.01),
868+
run_time=1e-12,
869+
symmetry=(1, -1, 0),
870+
monitors=[
871+
ModeMonitor(
872+
center=(2, 0, 1), size=(2, 2, 0), name="mnt", freqs=[2], mode_spec=ModeSpec()
873+
)
874+
],
875+
)

tidy3d/components/data.py

Lines changed: 108 additions & 106 deletions
Original file line numberDiff line numberDiff line change
@@ -302,110 +302,6 @@ def ensure_member_exists(self, member_name: str):
302302

303303
""" Subclasses of MonitorData and CollectionData """
304304

305-
class FreqData(MonitorData, ABC):
306-
"""Stores frequency-domain data using an ``f`` dimension for frequency in Hz."""
307-
308-
f: Array[float]
309-
310-
@abstractmethod
311-
def normalize(self, source_freq_amps: Array[complex]) -> None:
312-
"""Normalize values of frequency-domain data by source amplitude spectrum."""
313-
314-
315-
class TimeData(MonitorData, ABC):
316-
"""Stores time-domain data using a ``t`` attribute for time in seconds."""
317-
318-
t: Array[float]
319-
320-
321-
class AbstractScalarFieldData(MonitorData, ABC):
322-
"""Stores a single, scalar field as a function of spatial coordinates x,y,z."""
323-
324-
x: Array[float]
325-
y: Array[float]
326-
z: Array[float]
327-
328-
329-
class PlanarData(MonitorData, ABC):
330-
"""Stores data that must be found via a planar monitor."""
331-
332-
333-
class AbstractFluxData(PlanarData, ABC):
334-
"""Stores electromagnetic flux through a plane."""
335-
336-
337-
""" usable monitors """
338-
339-
340-
class ScalarFieldData(AbstractScalarFieldData, FreqData):
341-
"""Stores a single scalar field in frequency-domain.
342-
343-
Parameters
344-
----------
345-
x : numpy.ndarray
346-
Data coordinates in x direction (um).
347-
y : numpy.ndarray
348-
Data coordinates in y direction (um).
349-
z : numpy.ndarray
350-
Data coordinates in z direction (um).
351-
f : numpy.ndarray
352-
Frequency coordinates (Hz).
353-
values : numpy.ndarray
354-
Complex-valued array of shape ``(len(x), len(y), len(z), len(f))`` storing field values.
355-
356-
Example
357-
-------
358-
>>> f = np.linspace(1e14, 2e14, 1001)
359-
>>> x = np.linspace(-1, 1, 10)
360-
>>> y = np.linspace(-2, 2, 20)
361-
>>> z = np.linspace(0, 0, 1)
362-
>>> values = (1+1j) * np.random.random((len(x), len(y), len(z), len(f)))
363-
>>> data = ScalarFieldData(values=values, x=x, y=y, z=z, f=f)
364-
"""
365-
366-
values: Array[complex]
367-
data_attrs: Dict[str, str] = None # {'units': '[E] = V/um, [H] = A/um'}
368-
type: Literal["ScalarFieldData"] = "ScalarFieldData"
369-
370-
_dims = ("x", "y", "z", "f")
371-
372-
def normalize(self, source_freq_amps: Array[complex]) -> None:
373-
"""normalize the values by the amplitude of the source."""
374-
self.values /= 1j * source_freq_amps # pylint: disable=no-member
375-
376-
377-
class ScalarFieldTimeData(AbstractScalarFieldData, TimeData):
378-
"""stores a single scalar field in time domain
379-
380-
Parameters
381-
----------
382-
x : numpy.ndarray
383-
Data coordinates in x direction (um).
384-
y : numpy.ndarray
385-
Data coordinates in y direction (um).
386-
z : numpy.ndarray
387-
Data coordinates in z direction (um).
388-
t : numpy.ndarray
389-
Time coordinates (sec).
390-
values : numpy.ndarray
391-
Real-valued array of shape ``(len(x), len(y), len(z), len(t))`` storing field values.
392-
393-
Example
394-
-------
395-
>>> t = np.linspace(0, 1e-12, 1001)
396-
>>> x = np.linspace(-1, 1, 10)
397-
>>> y = np.linspace(-2, 2, 20)
398-
>>> z = np.linspace(0, 0, 1)
399-
>>> values = np.random.random((len(x), len(y), len(z), len(t)))
400-
>>> data = ScalarFieldTimeData(values=values, x=x, y=y, z=z, t=t)
401-
"""
402-
403-
values: Array[float]
404-
data_attrs: Dict[str, str] = None # {'units': '[E] = V/m, [H] = A/m'}
405-
type: Literal["ScalarFieldTimeData"] = "ScalarFieldTimeData"
406-
407-
_dims = ("x", "y", "z", "t")
408-
409305

410306
class AbstractFieldData(CollectionData, ABC):
411307
"""Sores a collection of EM fields either in freq or time domain."""
@@ -502,6 +398,7 @@ def colocate(self, x, y, z) -> xr.Dataset:
502398
# import pdb; pdb.set_trace()
503399
return xr.Dataset(centered_data_dict)
504400

401+
# pylint:disable=too-many-locals
505402
def apply_syms(self, new_grid: YeeGrid, sym_center: Coordinate, symmetry: Symmetry):
506403
"""Create a new AbstractFieldData subclass by interpolating on the supplied ``new_grid``,
507404
using symmetries as defined by ``sym_center`` and ``symmetry``."""
@@ -520,7 +417,7 @@ def apply_syms(self, new_grid: YeeGrid, sym_center: Coordinate, symmetry: Symmet
520417

521418
for field, scalar_data in self.data_dict.items():
522419
new_data = scalar_data.data
523-
420+
524421
# Get new grid locations
525422
yee_coords = yee_grid_dict[field].to_list
526423

@@ -544,11 +441,116 @@ def apply_syms(self, new_grid: YeeGrid, sym_center: Coordinate, symmetry: Symmet
544441
# Apply the correct +/-1 for the field component
545442
new_data[{dim_name: flip_inds}] *= sym * component_sym_dict[field][dim]
546443

547-
new_data_dict[field] = ScalarFieldData(values=new_data.values, **new_data.coords)
444+
new_data_dict[field] = type(scalar_data)(values=new_data.values, **new_data.coords)
548445

549446
return type(self)(data_dict=new_data_dict)
550447

551448

449+
class FreqData(MonitorData, ABC):
450+
"""Stores frequency-domain data using an ``f`` dimension for frequency in Hz."""
451+
452+
f: Array[float]
453+
454+
@abstractmethod
455+
def normalize(self, source_freq_amps: Array[complex]) -> None:
456+
"""Normalize values of frequency-domain data by source amplitude spectrum."""
457+
458+
459+
class TimeData(MonitorData, ABC):
460+
"""Stores time-domain data using a ``t`` attribute for time in seconds."""
461+
462+
t: Array[float]
463+
464+
465+
class AbstractScalarFieldData(MonitorData, ABC):
466+
"""Stores a single, scalar field as a function of spatial coordinates x,y,z."""
467+
468+
x: Array[float]
469+
y: Array[float]
470+
z: Array[float]
471+
472+
473+
class PlanarData(MonitorData, ABC):
474+
"""Stores data that must be found via a planar monitor."""
475+
476+
477+
class AbstractFluxData(PlanarData, ABC):
478+
"""Stores electromagnetic flux through a plane."""
479+
480+
481+
""" usable monitors """
482+
483+
484+
class ScalarFieldData(AbstractScalarFieldData, FreqData):
485+
"""Stores a single scalar field in frequency-domain.
486+
487+
Parameters
488+
----------
489+
x : numpy.ndarray
490+
Data coordinates in x direction (um).
491+
y : numpy.ndarray
492+
Data coordinates in y direction (um).
493+
z : numpy.ndarray
494+
Data coordinates in z direction (um).
495+
f : numpy.ndarray
496+
Frequency coordinates (Hz).
497+
values : numpy.ndarray
498+
Complex-valued array of shape ``(len(x), len(y), len(z), len(f))`` storing field values.
499+
500+
Example
501+
-------
502+
>>> f = np.linspace(1e14, 2e14, 1001)
503+
>>> x = np.linspace(-1, 1, 10)
504+
>>> y = np.linspace(-2, 2, 20)
505+
>>> z = np.linspace(0, 0, 1)
506+
>>> values = (1+1j) * np.random.random((len(x), len(y), len(z), len(f)))
507+
>>> data = ScalarFieldData(values=values, x=x, y=y, z=z, f=f)
508+
"""
509+
510+
values: Array[complex]
511+
data_attrs: Dict[str, str] = None # {'units': '[E] = V/um, [H] = A/um'}
512+
type: Literal["ScalarFieldData"] = "ScalarFieldData"
513+
514+
_dims = ("x", "y", "z", "f")
515+
516+
def normalize(self, source_freq_amps: Array[complex]) -> None:
517+
"""normalize the values by the amplitude of the source."""
518+
self.values /= 1j * source_freq_amps # pylint: disable=no-member
519+
520+
521+
class ScalarFieldTimeData(AbstractScalarFieldData, TimeData):
522+
"""stores a single scalar field in time domain
523+
524+
Parameters
525+
----------
526+
x : numpy.ndarray
527+
Data coordinates in x direction (um).
528+
y : numpy.ndarray
529+
Data coordinates in y direction (um).
530+
z : numpy.ndarray
531+
Data coordinates in z direction (um).
532+
t : numpy.ndarray
533+
Time coordinates (sec).
534+
values : numpy.ndarray
535+
Real-valued array of shape ``(len(x), len(y), len(z), len(t))`` storing field values.
536+
537+
Example
538+
-------
539+
>>> t = np.linspace(0, 1e-12, 1001)
540+
>>> x = np.linspace(-1, 1, 10)
541+
>>> y = np.linspace(-2, 2, 20)
542+
>>> z = np.linspace(0, 0, 1)
543+
>>> values = np.random.random((len(x), len(y), len(z), len(t)))
544+
>>> data = ScalarFieldTimeData(values=values, x=x, y=y, z=z, t=t)
545+
"""
546+
547+
values: Array[float]
548+
data_attrs: Dict[str, str] = None # {'units': '[E] = V/m, [H] = A/m'}
549+
type: Literal["ScalarFieldTimeData"] = "ScalarFieldTimeData"
550+
551+
_dims = ("x", "y", "z", "t")
552+
553+
552554
class FieldData(AbstractFieldData):
553555
"""Stores a collection of scalar fields in the frequency domain from a :class:`FieldMonitor`.
554556

tidy3d/components/geometry.py

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
from .types import Bound, Size, Coordinate, Axis, Coordinate2D, ArrayLike
1515
from .types import Vertices, Ax, Shapely
1616
from .viz import add_ax_if_none, equal_aspect
17-
from ..log import Tidy3dKeyError, SetupError, ValidationError
17+
from ..log import Tidy3dKeyError, SetupError, ValidationError, Tidy3dError
1818
from ..constants import MICROMETER, LARGE_NUMBER
1919

2020
# add this around extents of plots
@@ -626,6 +626,36 @@ def geometry(self):
626626
"""
627627
return Box(center=self.center, size=self.size)
628628

629+
def pop_axis(
630+
self, coord: Tuple[Any, Any, Any], axis: int = None
631+
) -> Tuple[Any, Tuple[Any, Any]]:
632+
"""Overwrites ``geometry.pop_axis`` to allow ``axis==None``, in which case the first
633+
axis along which ``self.size==0`` is taken."""
634+
if axis is None:
635+
try:
636+
axis = self.size.index(0.0)
637+
except ValueError as e:
638+
raise Tidy3dError(
639+
"Box must have at least one zero-sized dimension or ``axis`` "
640+
"must be provided to ``pop_axis``."
641+
) from e
642+
return super().pop_axis(coord, axis)
643+
644+
def unpop_axis(
645+
self, ax_coord: Any, plane_coords: Tuple[Any, Any], axis: int = None
646+
) -> Tuple[Any, Any, Any]:
647+
"""Overwrites ``geometry.unpop_axis`` to allow ``axis==None``, in which case the first
648+
axis along which ``self.size==0`` is taken."""
649+
if axis is None:
650+
try:
651+
axis = self.size.index(0.0)
652+
except ValueError as e:
653+
raise Tidy3dError(
654+
"Box must have at least one zero-sized dimension or ``axis`` "
655+
"must be provided to ``unpop_axis``."
656+
) from e
657+
return super().unpop_axis(ax_coord, plane_coords, axis)
658+
629659

630660
class Sphere(Circular):
631661
"""Spherical geometry.

tidy3d/components/grid.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,7 @@ class YeeGrid(Tidy3dBaseModel):
100100

101101
@property
102102
def grid_dict(self):
103+
"""The Yee grid coordinates associated to various field components as a dictionary."""
103104
yee_grid_dict = {
104105
"Ex": self.E.x,
105106
"Ey": self.E.y,
@@ -110,6 +111,7 @@ def grid_dict(self):
110111
}
111112
return yee_grid_dict
112113

114+
113115
class Grid(Tidy3dBaseModel):
114116
"""Contains all information about the spatial positions of the FDTD grid.
115117

tidy3d/components/mode.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from ..constants import MICROMETER
88
from .base import Tidy3dBaseModel
9-
from .types import Symmetry, Axis2D
9+
from .types import Axis2D
1010
from ..log import SetupError
1111

1212

@@ -17,7 +17,7 @@ class ModeSpec(Tidy3dBaseModel):
1717
1818
Example
1919
-------
20-
>>> mode_spec = ModeSpec(num_modes=3, target_neff=1.5, symmetries=(1, -1))
20+
>>> mode_spec = ModeSpec(num_modes=3, target_neff=1.5)
2121
"""
2222

2323
num_modes: pd.PositiveInt = pd.Field(

0 commit comments

Comments
 (0)