Skip to content

allow wavelength to be used in source and monitor creation #2245

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: pre/2.8
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- 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.
- 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.
- 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.
- Sources and monitors can be alternatively initialized with wavelengths instead of frequencies.

### Changed
- The coordinate of snapping points in `GridSpec` can take value `None`, so that mesh can be selectively snapped only along certain dimensions.
Expand Down
22 changes: 19 additions & 3 deletions tests/test_components/test_monitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -398,9 +398,9 @@ def test_monitor_plane():
td.DiffractionMonitor(size=size, freqs=FREQS, name="de")


def _test_freqs_nonempty():
with pytest.raises(ValidationError):
td.FieldMonitor(size=(1, 1, 1), freqs=[])
def test_freqs_nonempty():
with pytest.raises(pydantic.ValidationError):
td.FieldMonitor(size=(1, 1, 1), freqs=[], name="no_freq_monitor")


def test_monitor_surfaces_from_volume():
Expand Down Expand Up @@ -440,3 +440,19 @@ def test_monitor_surfaces_from_volume():
# z+ surface
assert monitor_surfaces[5].center == (center[0], center[1], center[2] + size[2] / 2.0)
assert monitor_surfaces[5].size == (size[0], size[1], 0.0)


def test_monitor_wavelength_spec(rng):
N = 15

wavelengths = 1e-6 * (1 + rng.random(N))
expected_freqs = td.C_0 / wavelengths

field_monitor = td.FieldMonitor(size=(1, 1, 1), lambdas=wavelengths, name="wl_spec")

assert np.allclose(field_monitor.freqs, expected_freqs)

with pytest.raises(ValidationError):
field_monitor_overspec = td.FieldMonitor(
size=(1, 1, 1), freqs=expected_freqs, lambdas=wavelengths, name="freq_wl_spec"
)
85 changes: 84 additions & 1 deletion tests/test_components/test_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import pytest
import tidy3d as td
from tidy3d.components.source.field import CHEB_GRID_WIDTH, DirectionalSource
from tidy3d.exceptions import SetupError
from tidy3d.exceptions import SetupError, ValidationError

from ..utils import AssertLogLevel

Expand Down Expand Up @@ -415,3 +415,86 @@ def test_fixed_angle_source():
)

assert not plane_wave._is_fixed_angle


def test_source_wavelength_spec():
"""Test the ability to specify either wavelength or frequency for the source."""
freq0 = 1e14
fwidth = 0.2 * freq0

wl_low = td.C_0 / (freq0 + 0.5 * fwidth)
wl_high = td.C_0 / (freq0 - 0.5 * fwidth)

lamb0 = 0.5 * (wl_low + wl_high)
lamb_width = wl_high - wl_low
# set by wavelength and make sure the frequencies are correct
source_time = td.GaussianPulse(lamb0=lamb0, lamb_width=lamb_width)

assert np.isclose(source_time.freq0, freq0)
assert np.isclose(source_time.fwidth, fwidth)

# allow customize_source_bandwidth to be used
customize_source_bandwidth = 2.0
source_time = td.GaussianPulse(
lamb0=lamb0, lamb_width=lamb_width, customize_source_bandwidth=customize_source_bandwidth
)

assert np.isclose(source_time.freq0, freq0)
assert np.isclose(source_time.fwidth, customize_source_bandwidth * fwidth)

# ensure conflicting wavelength and frequency specifications are not allowed
with pytest.raises(ValidationError):
source_time = td.GaussianPulse(
lamb0=lamb0, lamb_width=lamb_width, freq0=freq0, fwidth=fwidth
)

with pytest.raises(ValidationError):
source_time = td.GaussianPulse(lamb0=lamb0, lamb_width=lamb_width, freq0=freq0)

with pytest.raises(ValidationError):
source_time = td.GaussianPulse(lamb0=lamb0, lamb_width=lamb_width, fwidth=fwidth)

with pytest.raises(ValidationError):
source_time = td.GaussianPulse(lamb0=lamb0, freq0=freq0, fwidth=fwidth)

with pytest.raises(ValidationError):
source_time = td.GaussianPulse(lamb_width=lamb_width, freq0=freq0, fwidth=fwidth)

with pytest.raises(ValidationError):
source_time = td.GaussianPulse(lamb_width=lamb_width, freq0=freq0)

with pytest.raises(ValidationError):
source_time = td.GaussianPulse(lamb0=lamb0, fwidth=fwidth)

with pytest.raises(ValidationError):
source_time = td.GaussianPulse(lamb0=lamb0, freq0=freq0)

with pytest.raises(ValidationError):
source_time = td.GaussianPulse(lamb_width=lamb_width, fwidth=fwidth)

# ensure enough information is provided
with pytest.raises(ValidationError):
source_time = td.GaussianPulse(fwidth=fwidth)

with pytest.raises(ValidationError):
source_time = td.GaussianPulse(freq0=freq0)

with pytest.raises(ValidationError):
source_time = td.GaussianPulse(lamb_width=lamb_width)

with pytest.raises(ValidationError):
source_time = td.GaussianPulse(lamb0=lamb0)

wl_low = td.C_0 / (freq0 + 0.5 * fwidth)
wl_high = td.C_0 / (freq0 - 0.5 * fwidth)

lamb0 = 0.5 * (wl_low + wl_high)
lamb_width = 2 * lamb0

# ensure wavelength specification does not lead to divide by zero error
with pytest.raises(ValidationError):
source_time = td.GaussianPulse(
lamb0=lamb0,
lamb_width=lamb_width,
customize_source_bandwidth=customize_source_bandwidth,
)
18 changes: 17 additions & 1 deletion tidy3d/components/monitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import numpy as np
import pydantic.v1 as pydantic

from ..constants import HERTZ, MICROMETER, RADIAN, SECOND, inf
from ..constants import C_0, HERTZ, MICROMETER, RADIAN, SECOND, inf
from ..exceptions import SetupError, ValidationError
from ..log import log
from .apodization import ApodizationSpec
Expand All @@ -28,6 +28,7 @@
Literal,
ObsGridArray,
Size,
WavelengthArray,
)
from .validators import assert_plane, validate_freqs_min, validate_freqs_not_empty
from .viz import ARROW_ALPHA, ARROW_COLOR_MONITOR
Expand Down Expand Up @@ -99,6 +100,21 @@ class FreqMonitor(Monitor, ABC):
"affects the normalization of the frequency-domain fields.",
)

def __init__(self, freqs: FreqArray = None, lambdas: WavelengthArray = None, **kwargs):
freq_specified = freqs is not None
lambda_specified = lambdas is not None
if freq_specified and lambda_specified:
raise ValidationError("Both wavelengths and freqs should not be specified")
elif not (freq_specified or lambda_specified):
raise ValidationError("At least one of wavelengths or freqs should be specified")
elif lambda_specified:
if not (np.count_nonzero(lambdas > 0) == len(lambdas)):
raise ValidationError("Wavelengths should be strictly positive")
freqs_from_free_space_wavelength = C_0 / lambdas
super().__init__(freqs=freqs_from_free_space_wavelength, **kwargs)
else:
super().__init__(freqs=freqs, **kwargs)

_freqs_not_empty = validate_freqs_not_empty()
_freqs_lower_bound = validate_freqs_min()

Expand Down
44 changes: 43 additions & 1 deletion tidy3d/components/source/time.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import numpy as np
import pydantic.v1 as pydantic

from ...constants import HERTZ
from ...constants import C_0, HERTZ
from ...exceptions import ValidationError
from ..data.data_array import TimeDataArray
from ..data.dataset import TimeDataset
Expand Down Expand Up @@ -95,6 +95,48 @@ class Pulse(SourceTime, ABC):
ge=2.5,
)

def __init__(
self,
freq0: float = None,
fwidth: float = None,
lamb0: float = None,
lamb_width: float = None,
customize_source_bandwidth: float = 1.0,
**kwargs,
):
freq_specified = (freq0 is not None) and (fwidth is not None)
lambda_specified = (lamb0 is not None) and (lamb_width is not None)

partial_freq_specified = (freq0 is not None) or (fwidth is not None)
partial_lambda_specified = (lamb0 is not None) or (lamb_width is not None)

if (freq_specified and partial_lambda_specified) or (
partial_freq_specified and lambda_specified
):
raise ValidationError("Frequency and wavelength specification are conflicting")

if not (freq_specified or lambda_specified):
raise ValidationError("Either frequency or wavelength should be specified")

if lambda_specified:
lambda_bottom = lamb0 - 0.5 * lamb_width
lambda_top = lamb0 + 0.5 * lamb_width

if (lambda_bottom <= 0.0) or (lambda_top <= 0.0):
raise ValidationError("Wavelength bounds should be strictly positive")

freq_bottom = C_0 / lambda_top
freq_top = C_0 / lambda_bottom

freq_mid = 0.5 * (freq_bottom + freq_top)
freq_width = freq_top - freq_bottom

super().__init__(
freq0=freq_mid, fwidth=customize_source_bandwidth * freq_width, **kwargs
)
else:
super().__init__(freq0=freq0, fwidth=customize_source_bandwidth * fwidth, **kwargs)

@property
def twidth(self) -> float:
"""Width of pulse in seconds."""
Expand Down
1 change: 1 addition & 0 deletions tidy3d/components/types.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,7 @@ def __modify_schema__(cls, field_schema):
EMField = Literal["Ex", "Ey", "Ez", "Hx", "Hy", "Hz"]
FieldType = Literal["Ex", "Ey", "Ez", "Hx", "Hy", "Hz"]
FreqArray = Union[Tuple[float, ...], ArrayFloat1D]
WavelengthArray = Union[Tuple[float, ...], ArrayFloat1D]
ObsGridArray = Union[Tuple[float, ...], ArrayFloat1D]

""" plotting """
Expand Down