From c661aa8b6a518c104e89995823b58b3d8b669487 Mon Sep 17 00:00:00 2001 From: Gregory Roberts Date: Wed, 12 Feb 2025 15:25:58 -0500 Subject: [PATCH 1/2] feature[frontend]: allow wavelength to be used in source and monitor creation --- tests/test_components/test_monitor.py | 22 ++++++- tests/test_components/test_source.py | 85 ++++++++++++++++++++++++++- tidy3d/components/monitor.py | 18 +++++- tidy3d/components/source/time.py | 44 +++++++++++++- tidy3d/components/types.py | 1 + 5 files changed, 164 insertions(+), 6 deletions(-) diff --git a/tests/test_components/test_monitor.py b/tests/test_components/test_monitor.py index a4563cc0c7..379a86669c 100644 --- a/tests/test_components/test_monitor.py +++ b/tests/test_components/test_monitor.py @@ -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(): @@ -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" + ) diff --git a/tests/test_components/test_source.py b/tests/test_components/test_source.py index a76a65c2bb..8f480e99ff 100644 --- a/tests/test_components/test_source.py +++ b/tests/test_components/test_source.py @@ -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 @@ -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, + ) diff --git a/tidy3d/components/monitor.py b/tidy3d/components/monitor.py index ac9e3d168d..a3fed865fd 100644 --- a/tidy3d/components/monitor.py +++ b/tidy3d/components/monitor.py @@ -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 @@ -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 @@ -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() diff --git a/tidy3d/components/source/time.py b/tidy3d/components/source/time.py index 2d919e74c1..2bc454f8db 100644 --- a/tidy3d/components/source/time.py +++ b/tidy3d/components/source/time.py @@ -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 @@ -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.""" diff --git a/tidy3d/components/types.py b/tidy3d/components/types.py index 884b904a18..b4d4a1ca7f 100644 --- a/tidy3d/components/types.py +++ b/tidy3d/components/types.py @@ -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 """ From 10b3abbf933a71ee0f441379a149626ddc13a288 Mon Sep 17 00:00:00 2001 From: Gregory Roberts Date: Wed, 12 Feb 2025 15:36:03 -0500 Subject: [PATCH 2/2] add changelog entry --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d8c5a0ca35..0d8b5d60ae 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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.