Skip to content

Commit e9fc81b

Browse files
committed
add ability to project fields through a manually specified material
1 parent 065831a commit e9fc81b

File tree

3 files changed

+37
-15
lines changed

3 files changed

+37
-15
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1010
- Warning if nonlinear mediums are used in an `adjoint` simulation. In this case, the gradients will not be accurate, but may be approximately correct if the nonlinearity is weak.
1111
- Validator for surface field projection monitors that warns if projecting backwards relative to the monitor's normal direction.
1212
- Validator for field projection monitors when far field approximation is enabled but the projection distance is small relative to the near field domain.
13+
- Ability to manually specify a medium through which to project fields, when using field projection monitors.
1314

1415
### Changed
1516
- Credit cost for remote mode solver has been modified to be defined in advance based on the mode solver details. Previously, the cost was based on elapsed runtime. On average, there should be little difference in the cost.

tidy3d/components/field_projection.py

Lines changed: 25 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -341,6 +341,7 @@ def _far_fields_for_surface(
341341
phi: ArrayLikeN2F,
342342
surface: FieldProjectionSurface,
343343
currents: xr.Dataset,
344+
medium: MediumType,
344345
):
345346
"""Compute far fields at an angle in spherical coordinates
346347
for a given set of surface currents and observation angles.
@@ -358,13 +359,14 @@ def _far_fields_for_surface(
358359
:class:`FieldProjectionSurface` object to use as source of near field.
359360
currents : xarray.Dataset
360361
xarray Dataset containing surface currents associated with the surface monitor.
362+
medium : :class:`.MediumType`
363+
Background medium through which to project fields.
361364
362365
Returns
363366
-------
364367
tuple(numpy.ndarray[float], ...)
365368
``Er``, ``Etheta``, ``Ephi``, ``Hr``, ``Htheta``, ``Hphi`` for the given surface.
366369
"""
367-
368370
pts = [currents[name].values for name in ["x", "y", "z"]]
369371

370372
try:
@@ -393,7 +395,7 @@ def _far_fields_for_surface(
393395

394396
phase = [None] * 3
395397
propagation_factor = -1j * AbstractFieldProjectionData.wavenumber(
396-
medium=self.medium, frequency=frequency
398+
medium=medium, frequency=frequency
397399
)
398400

399401
def integrate_for_one_theta(i_th: int):
@@ -448,7 +450,7 @@ def integrate_for_one_theta(i_th: int):
448450
# Lphi (8.34b)
449451
Lphi = -M[0] * sin_phi[None, :] + M[1] * cos_phi[None, :]
450452

451-
eta = ETA_0 / np.sqrt(self.medium.eps_model(frequency))
453+
eta = ETA_0 / np.sqrt(medium.eps_model(frequency))
452454

453455
Etheta = -(Lphi + eta * Ntheta)
454456
Ephi = Ltheta - eta * Nphi
@@ -546,7 +548,8 @@ def _project_fields_angular(
546548
np.zeros((1, len(theta), len(phi), len(freqs)), dtype=complex) for _ in field_names
547549
]
548550

549-
k = AbstractFieldProjectionData.wavenumber(medium=self.medium, frequency=freqs)
551+
medium = monitor.medium if monitor.medium else self.medium
552+
k = AbstractFieldProjectionData.wavenumber(medium=medium, frequency=freqs)
550553
phase = np.atleast_1d(
551554
AbstractFieldProjectionData.propagation_phase(dist=monitor.proj_distance, k=k)
552555
)
@@ -564,6 +567,7 @@ def _project_fields_angular(
564567
phi=phi,
565568
surface=surface,
566569
currents=currents,
570+
medium=medium,
567571
)
568572
for field, _field in zip(fields, _fields):
569573
field[..., idx_f] += _field * phase[idx_f]
@@ -580,7 +584,7 @@ def _project_fields_angular(
580584
):
581585
_x, _y, _z = monitor.sph_2_car(monitor.proj_distance, _theta, _phi)
582586
_fields = self._fields_for_surface_exact(
583-
x=_x, y=_y, z=_z, surface=surface, currents=currents
587+
x=_x, y=_y, z=_z, surface=surface, currents=currents, medium=medium
584588
)
585589
for field, _field in zip(fields, _fields):
586590
field[0, i, j, :] += _field
@@ -591,7 +595,7 @@ def _project_fields_angular(
591595
for name, field in zip(field_names, fields)
592596
}
593597
return FieldProjectionAngleData(
594-
monitor=monitor, projection_surfaces=self.surfaces, medium=self.medium, **fields
598+
monitor=monitor, projection_surfaces=self.surfaces, medium=medium, **fields
595599
)
596600

597601
def _project_fields_cartesian(
@@ -622,7 +626,8 @@ def _project_fields_cartesian(
622626
np.zeros((len(x), len(y), len(z), len(freqs)), dtype=complex) for _ in field_names
623627
]
624628

625-
wavenumber = AbstractFieldProjectionData.wavenumber(medium=self.medium, frequency=freqs)
629+
medium = monitor.medium if monitor.medium else self.medium
630+
wavenumber = AbstractFieldProjectionData.wavenumber(medium=medium, frequency=freqs)
626631

627632
# Zip together all combinations of observation points for better progress tracking
628633
iter_coords = [
@@ -655,12 +660,13 @@ def _project_fields_cartesian(
655660
phi=phi,
656661
surface=surface,
657662
currents=currents,
663+
medium=medium,
658664
)
659665
for field, _field in zip(fields, _fields):
660666
field[i, j, k, idx_f] += _field * phase[idx_f]
661667
else:
662668
_fields = self._fields_for_surface_exact(
663-
x=_x, y=_y, z=_z, surface=surface, currents=currents
669+
x=_x, y=_y, z=_z, surface=surface, currents=currents, medium=medium
664670
)
665671
for field, _field in zip(fields, _fields):
666672
field[i, j, k, :] += _field
@@ -671,7 +677,7 @@ def _project_fields_cartesian(
671677
for name, field in zip(field_names, fields)
672678
}
673679
return FieldProjectionCartesianData(
674-
monitor=monitor, projection_surfaces=self.surfaces, medium=self.medium, **fields
680+
monitor=monitor, projection_surfaces=self.surfaces, medium=medium, **fields
675681
)
676682

677683
def _project_fields_kspace(
@@ -698,7 +704,8 @@ def _project_fields_kspace(
698704
field_names = ("Er", "Etheta", "Ephi", "Hr", "Htheta", "Hphi")
699705
fields = [np.zeros((len(ux), len(uy), 1, len(freqs)), dtype=complex) for _ in field_names]
700706

701-
k = AbstractFieldProjectionData.wavenumber(medium=self.medium, frequency=freqs)
707+
medium = monitor.medium if monitor.medium else self.medium
708+
k = AbstractFieldProjectionData.wavenumber(medium=medium, frequency=freqs)
702709
phase = np.atleast_1d(
703710
AbstractFieldProjectionData.propagation_phase(dist=monitor.proj_distance, k=k)
704711
)
@@ -726,14 +733,15 @@ def _project_fields_kspace(
726733
phi=phi,
727734
surface=surface,
728735
currents=currents,
736+
medium=medium,
729737
)
730738
for field, _field in zip(fields, _fields):
731739
field[i, j, 0, idx_f] += _field * phase[idx_f]
732740

733741
else:
734742
_x, _y, _z = monitor.sph_2_car(monitor.proj_distance, theta, phi)
735743
_fields = self._fields_for_surface_exact(
736-
x=_x, y=_y, z=_z, surface=surface, currents=currents
744+
x=_x, y=_y, z=_z, surface=surface, currents=currents, medium=medium
737745
)
738746
for field, _field in zip(fields, _fields):
739747
field[i, j, 0, :] += _field
@@ -749,7 +757,7 @@ def _project_fields_kspace(
749757
for name, field in zip(field_names, fields)
750758
}
751759
return FieldProjectionKSpaceData(
752-
monitor=monitor, projection_surfaces=self.surfaces, medium=self.medium, **fields
760+
monitor=monitor, projection_surfaces=self.surfaces, medium=medium, **fields
753761
)
754762

755763
"""Exact projections"""
@@ -761,6 +769,7 @@ def _fields_for_surface_exact(
761769
z: float,
762770
surface: FieldProjectionSurface,
763771
currents: xr.Dataset,
772+
medium: MediumType,
764773
):
765774
"""Compute projected fields in spherical coordinates at a given projection point on a
766775
Cartesian grid for a given set of surface currents using the exact homogeneous medium
@@ -778,20 +787,21 @@ def _fields_for_surface_exact(
778787
:class:`FieldProjectionSurface` object to use as source of near field.
779788
currents : xarray.Dataset
780789
xarray Dataset containing surface currents associated with the surface monitor.
790+
medium : :class:`.MediumType`
791+
Background medium through which to project fields.
781792
782793
Returns
783794
-------
784795
tuple(np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray, np.ndarray)
785796
``Er``, ``Etheta``, ``Ephi``, ``Hr``, ``Htheta``, ``Hphi`` projected fields for
786797
each frequency.
787798
"""
788-
789799
freqs = np.array(self.frequencies)
790800
i_omega = 1j * 2.0 * np.pi * freqs[None, None, None, :]
791-
wavenumber = AbstractFieldProjectionData.wavenumber(frequency=freqs, medium=self.medium)
801+
wavenumber = AbstractFieldProjectionData.wavenumber(frequency=freqs, medium=medium)
792802
wavenumber = wavenumber[None, None, None, :] # add space dimensions
793803

794-
eps_complex = self.medium.eps_model(frequency=freqs)
804+
eps_complex = medium.eps_model(frequency=freqs)
795805
epsilon = EPSILON_0 * eps_complex[None, None, None, :]
796806

797807
# source points

tidy3d/components/monitor.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from .base import cached_property, Tidy3dBaseModel
1212
from .mode import ModeSpec
1313
from .apodization import ApodizationSpec
14+
from .medium import MediumType
1415
from .viz import ARROW_COLOR_MONITOR, ARROW_ALPHA
1516
from ..constants import HERTZ, SECOND, MICROMETER, RADIAN, inf
1617
from ..exceptions import SetupError, ValidationError
@@ -685,6 +686,16 @@ class AbstractFieldProjectionMonitor(SurfaceIntegrationMonitor, FreqMonitor):
685686
"and otherwise must remain (0, 0).",
686687
)
687688

689+
medium: MediumType = pydantic.Field(
690+
None,
691+
title="Projection medium",
692+
description="Medium through which to project fields. Generally, the fields should be "
693+
"projected through the same medium as the one in which this monitor is placed, and "
694+
"this is the default behavior when ``medium=None``. A custom ``medium`` can be useful "
695+
"in some situations for advanced users, but we recommend trying to avoid using a "
696+
"non-default ``medium``.",
697+
)
698+
688699
@pydantic.validator("window_size", always=True)
689700
def window_size_for_surface(cls, val, values):
690701
"""Ensures that windowing is applied for surface monitors only."""

0 commit comments

Comments
 (0)