diff --git a/CHANGELOG.md b/CHANGELOG.md index c1dd63d8d..02390610b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Improved autograd tracer handling in `DataArray`, resulting in significant speedups for differentiation involving large monitors. - Triangulation of `PolySlab` polygons now supports polygons with collinear vertices. +- Frequency and wavelength utilities under `tidy3d.frequencies` and `tidy3d.wavelengths`. ### Fixed - Minor gradient direction and normalization fixes for polyslab, field monitors, and diffraction monitors in autograd. diff --git a/tests/test_components/test_frequencies.py b/tests/test_components/test_frequencies.py new file mode 100644 index 000000000..30c8f2571 --- /dev/null +++ b/tests/test_components/test_frequencies.py @@ -0,0 +1,17 @@ +import numpy as np +import pytest +import tidy3d as td + + +def test_classification(): + assert td.frequencies.classification(1) == ("near static",) + assert td.wavelengths.classification(td.C_0) == ("near static",) + assert td.frequencies.classification(td.C_0 / 1.55) == ("infrared", "NIR") + assert td.wavelengths.classification(1.55) == ("infrared", "NIR") + + +@pytest.mark.parametrize("band", ["O", "E", "S", "C", "L", "U"]) +def test_bands(band): + freqs = getattr(td.frequencies, band.lower() + "_band")() + ldas = getattr(td.wavelengths, band.lower() + "_band")() + assert np.allclose(freqs, td.C_0 / np.array(ldas)) diff --git a/tidy3d/__init__.py b/tidy3d/__init__.py index 85825ab60..6642b2c95 100644 --- a/tidy3d/__init__.py +++ b/tidy3d/__init__.py @@ -105,7 +105,6 @@ EMEMonitor, ) -# EME # EME from .components.eme.simulation import EMESimulation from .components.eme.sweep import EMEFreqSweep, EMELengthSweep, EMEModeSweep @@ -113,6 +112,9 @@ # field projection from .components.field_projection import FieldProjector +# frequency conversion utilities +from .components.frequencies import frequencies, wavelengths + # geometry from .components.geometry.base import Box, ClipOperation, Geometry, GeometryGroup, Transformed from .components.geometry.mesh import TriangleMesh @@ -424,6 +426,8 @@ def set_logging_level(level: str) -> None: "Q_e", "K_B", "inf", + "frequencies", + "wavelengths", "material_library", "Graphene", "AbstractMedium", diff --git a/tidy3d/components/frequencies.py b/tidy3d/components/frequencies.py new file mode 100644 index 000000000..6a14fb5b6 --- /dev/null +++ b/tidy3d/components/frequencies.py @@ -0,0 +1,231 @@ +"""Frequency utilities.""" + +import numpy as np +import pydantic as pd + +from ..constants import C_0 +from .base import Tidy3dBaseModel + +O_BAND = (1.260, 1.360) +E_BAND = (1.360, 1.460) +S_BAND = (1.460, 1.530) +C_BAND = (1.530, 1.565) +L_BAND = (1.565, 1.625) +U_BAND = (1.625, 1.675) + + +class FrequencyUtils(Tidy3dBaseModel): + """Class for general frequency/wavelength utilities.""" + + use_wavelength: bool = pd.Field( + False, + title="Use wavelength", + description="Indicate whether to use wavelengths instead of frequencies for the return " + "values of functions and parameters.", + ) + + def classification(self, value: float) -> tuple[str]: + """Band classification for a given frequency/wavelength. + + Frequency values must be given in hertz (Hz). Wavelengths must be + given in micrometers (μm). + + Parameters + ---------- + value : float + Value to classify. + + Returns + ------- + tuple[str] + String tuple with classification. + """ + if self.use_wavelength: + value = C_0 / value + if value < 3: + return ("near static",) + elif value < 300e6: + if value < 30: + return ("radio wave", "ELF") + elif value < 300: + return ("radio wave", "SLF") + elif value < 3e3: + return ("radio wave", "ULF") + elif value < 30e3: + return ("radio wave", "VLF") + elif value < 300e3: + return ("radio wave", "LF") + elif value < 3e6: + return ("radio wave", "MF") + elif value < 30e6: + return ("radio wave", "HF") + return ("radio wave", "VHF") + elif value < 300e9: + if value < 3e9: + return ("microwave", "UHF") + elif value < 30e9: + return ("microwave", "SHF") + return ("microwave", "EHF") + elif value < 400e12: + if value < 6e12: + return ("infrared", "FIR") + elif value < 100e12: + return ("infrared", "MIR") + return ("infrared", "NIR") + elif value < 790e12: + if value < 480e12: + return ("visible", "red") + elif value < 510e12: + return ("visible", "orange") + elif value < 530e12: + return ("visible", "yellow") + elif value < 600e12: + return ("visible", "green") + elif value < 620e12: + return ("visible", "cyan") + elif value < 670e12: + return ("visible", "blue") + return ("visible", "violet") + elif value < 30e15: + if value < 1e15: + return ("ultraviolet", "NUV") + elif value < 1.5e15: + return ("ultraviolet", "MUV") + elif value < 2.47e15: + return ("ultraviolet", "FUV") + return ("ultraviolet", "EUV") + if value < 30e18: + if value < 3e18: + return ("X-ray", "soft X-ray") + return ("X-ray", "hard X-ray") + return ("γ-ray",) + + def o_band(self, n: int = 11) -> list[float]: + """ + Optical O band frequencies/wavelengths sorted by wavelength. + + The returned samples are equally spaced in wavelength. + + Parameters + ---------- + n : int + Desired number of samples. + + Returns + ------- + list[float] + Samples list. + """ + values = np.linspace(*O_BAND, n) + if not self.use_wavelength: + values = C_0 / values + return values.tolist() + + def e_band(self, n: int = 11) -> list[float]: + """ + Optical E band frequencies/wavelengths sorted by wavelength. + + The returned samples are equally spaced in wavelength. + + Parameters + ---------- + n : int + Desired number of samples. + + Returns + ------- + list[float] + Samples list. + """ + values = np.linspace(*E_BAND, n) + if not self.use_wavelength: + values = C_0 / values + return values.tolist() + + def s_band(self, n: int = 15) -> list[float]: + """ + Optical S band frequencies/wavelengths sorted by wavelength. + + The returned samples are equally spaced in wavelength. + + Parameters + ---------- + n : int + Desired number of samples. + + Returns + ------- + list[float] + Samples list. + """ + values = np.linspace(*S_BAND, n) + if not self.use_wavelength: + values = C_0 / values + return values.tolist() + + def c_band(self, n: int = 8) -> list[float]: + """ + Optical C band frequencies/wavelengths sorted by wavelength. + + The returned samples are equally spaced in wavelength. + + Parameters + ---------- + n : int + Desired number of samples. + + Returns + ------- + list[float] + Samples list. + """ + values = np.linspace(*C_BAND, n) + if not self.use_wavelength: + values = C_0 / values + return values.tolist() + + def l_band(self, n: int = 13) -> list[float]: + """ + Optical L band frequencies/wavelengths sorted by wavelength. + + The returned samples are equally spaced in wavelength. + + Parameters + ---------- + n : int + Desired number of samples. + + Returns + ------- + list[float] + Samples list. + """ + values = np.linspace(*L_BAND, n) + if not self.use_wavelength: + values = C_0 / values + return values.tolist() + + def u_band(self, n: int = 11) -> list[float]: + """ + Optical U band frequencies/wavelengths sorted by wavelength. + + The returned samples are equally spaced in wavelength. + + Parameters + ---------- + n : int + Desired number of samples. + + Returns + ------- + list[float] + Samples list. + """ + values = np.linspace(*U_BAND, n) + if not self.use_wavelength: + values = C_0 / values + return values.tolist() + + +frequencies = FrequencyUtils(use_wavelength=False) +wavelengths = FrequencyUtils(use_wavelength=True)