Skip to content

Commit bcf635f

Browse files
Add FreqRange utility class and unit tests (#2542)
- Implements FreqRange with pydantic validation - Adds frequency/wavelength conversion, interval sampling, and GaussianPulse generation - Includes comprehensive unit tests
1 parent e12cc67 commit bcf635f

File tree

4 files changed

+419
-0
lines changed

4 files changed

+419
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
88
## [Unreleased]
99

1010
### Added
11+
- Implemented `FreqRange` utility class for frequency/wavelength handling with constructor methods `from_freq_interval()`, `from_wavelength()`, and `from_wvl_interval()`.
1112
- Add support for `np.unwrap` in `tidy3d.plugins.autograd`.
1213
- Add Nunley variant to germanium material library based on Nunley et al. 2016 data.
1314
- Add `PointDipole.sources_from_angles()` that constructs a list of `PointDipole` objects needed to emulate a dipole oriented at a user-provided set of polar and azimuthal angles.
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
"""Test class ``FreqRange`` for frequency and wavelength handling."""
2+
3+
from __future__ import annotations
4+
5+
import numpy as np
6+
7+
import tidy3d.constants as td_const
8+
from tidy3d.components.source.freq_range import FreqRange
9+
from tidy3d.components.source.time import GaussianPulse
10+
11+
12+
def test_constructor():
13+
"""
14+
test construction of ``FreqRange`` from central frequency
15+
``freq0`` and bandwidth ``fwidth``.
16+
"""
17+
# set initial params
18+
freq0 = 1e9 # set central frequency
19+
fwidth = 1e8 # set one-side bandwidth
20+
21+
# construct instance of FreqRange class
22+
freq_range = FreqRange(freq0=freq0, fwidth=fwidth)
23+
24+
fmin = freq0 - fwidth
25+
fmax = freq0 + fwidth
26+
27+
lmin = td_const.C_0 / fmax
28+
lmax = td_const.C_0 / fmin
29+
30+
# validated if class atributes were initialized correctly
31+
assert freq_range.fmin == fmin
32+
assert freq_range.fmax == fmax
33+
assert freq_range.lda0 == (lmin + lmax) / 2
34+
assert freq_range.freq0 == freq0
35+
assert freq_range.fwidth == fwidth
36+
37+
38+
def test_from_freq_interval():
39+
"""
40+
test construction of ``FreqRange`` from frequency interval.
41+
"""
42+
43+
fmin = 1e10 # new min frequency
44+
fmax = 1e11 # new max frequency
45+
46+
lmin = td_const.C_0 / fmax
47+
lmax = td_const.C_0 / fmin
48+
49+
# update object given new frequencies
50+
freq_range = FreqRange.from_freq_interval(fmin=fmin, fmax=fmax)
51+
52+
# validate if frequencies were updated correctly
53+
assert freq_range.fmin == fmin
54+
assert freq_range.fmax == fmax
55+
assert np.isclose(freq_range.freq0, (fmin + fmax) / 2, rtol=1e-14)
56+
assert np.isclose(freq_range.fwidth, (fmax - fmin) / 2, rtol=1e-14)
57+
assert np.isclose(freq_range.lda0, (lmin + lmax) / 2, rtol=1e-14)
58+
59+
60+
def test_from_wavelength():
61+
"""
62+
test construction of ``FreqRange`` from a central wavelength and a wavelength.
63+
"""
64+
# set initial params
65+
wvl0 = 1
66+
wvl_width = 0.1
67+
68+
# get the shortest and the longest wavelengths
69+
wvl_min = wvl0 - wvl_width
70+
wvl_max = wvl0 + wvl_width
71+
72+
# define frequency range
73+
freq_range = FreqRange.from_wavelength(wvl0=wvl0, wvl_width=wvl_width)
74+
75+
#
76+
assert freq_range.lda0 == wvl0
77+
78+
fmin = td_const.C_0 / wvl_max
79+
fmax = td_const.C_0 / wvl_min
80+
81+
assert np.isclose(freq_range.fmin, fmin, rtol=1e-14)
82+
assert np.isclose(freq_range.fmax, fmax, rtol=1e-14)
83+
assert np.isclose(freq_range.freq0, 0.5 * (fmin + fmax), rtol=1e-14)
84+
assert np.isclose(freq_range.fwidth, 0.5 * (fmax - fmin), rtol=1e-14)
85+
86+
87+
def test_from_wvl_interval():
88+
"""
89+
test construction of ``FreqRange`` from an interval of wavelengths.
90+
"""
91+
# set initial params
92+
wvl_min = 0.1
93+
wvl_max = 1
94+
fmin = td_const.C_0 / wvl_max
95+
fmax = td_const.C_0 / wvl_min
96+
97+
# freq_range = FreqRange(freq0, fwidth)
98+
99+
# update frequencies based on wavelength
100+
freq_range = FreqRange.from_wvl_interval(wvl_min=wvl_min, wvl_max=wvl_max)
101+
102+
# ensure that parameters are updated correctly
103+
assert np.isclose(freq_range.lda0, (wvl_min + wvl_max) / 2, rtol=1e-14)
104+
assert np.isclose(freq_range.freq0, 0.5 * (fmin + fmax), rtol=1e-14)
105+
assert np.isclose(freq_range.fmax, fmax, rtol=1e-14)
106+
assert np.isclose(freq_range.fmin, fmin, rtol=1e-14)
107+
assert np.isclose(freq_range.fwidth, 0.5 * (fmax - fmin), rtol=1e-14)
108+
109+
110+
def test_freqs():
111+
"""
112+
test generation of uniformly distributed frequency samples
113+
from a given frequency interval.
114+
"""
115+
# set initial params
116+
freq0 = 1e9 # set central frequency
117+
fwidth = 1e8 # set one-side bandwidth
118+
num_points = 11
119+
120+
# construct instance of FreqRange class
121+
freq_range = FreqRange(freq0=freq0, fwidth=fwidth)
122+
123+
# form sampling frequency points
124+
freqs = freq_range.freqs(num_points=num_points)
125+
126+
# make sure
127+
assert np.array_equal(freqs, np.linspace(freq0 - fwidth, freq0 + fwidth, num_points))
128+
129+
# reset number of sampling points to 1
130+
num_points = 1
131+
freqs = freq_range.freqs(num_points=num_points)
132+
133+
# check if freqs == freq0
134+
assert np.array_equal(freqs, np.array([freq0]))
135+
136+
137+
def test_ldas():
138+
"""
139+
test generation of uniformly distributed wavelength samples
140+
from a given wavelength interval.
141+
"""
142+
# set initial params
143+
freq0 = 1e9 # set central frequency
144+
fwidth = 1e8 # set one-side bandwidth
145+
num_points = 11
146+
147+
lmin = td_const.C_0 / (freq0 + fwidth)
148+
lmax = td_const.C_0 / (freq0 - fwidth)
149+
lda0 = (lmin + lmax) / 2
150+
151+
# construct instance of FreqRange class
152+
freq_range = FreqRange(freq0=freq0, fwidth=fwidth)
153+
154+
# form sampling frequency points
155+
ldas = freq_range.ldas(num_points=num_points)
156+
157+
# make sure
158+
assert np.array_equal(ldas, np.linspace(lmin, lmax, num_points))
159+
160+
# reset number of sampling points to 1
161+
num_points = 1
162+
ldas = freq_range.ldas(num_points=num_points)
163+
164+
# check if freqs == freq0
165+
assert np.array_equal(ldas, np.array([lda0]))
166+
167+
168+
def test_gaussian_pulse():
169+
"""
170+
test generation of a ``GaussianPulse`` with frequency parameters
171+
defined in ``FreqRange``.
172+
"""
173+
174+
# set initial params
175+
freq0 = 1e9 # set central frequency
176+
fwidth = 1e8 # set one-side bandwidth
177+
fmin = freq0 - fwidth
178+
fmax = freq0 + fwidth
179+
180+
# construct instance of FreqRange class
181+
freq_range = FreqRange(freq0=freq0, fwidth=fwidth)
182+
183+
pulse_exp = GaussianPulse.from_frequency_range(
184+
fmin=fmin, fmax=fmax
185+
) # instantiate GaussianPulse explicitly
186+
pulse_imp = freq_range.to_gaussian_pulse() # get instance by calling a method gaussian_pulse()
187+
188+
assert pulse_exp == pulse_imp # compare two pulses

tidy3d/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,6 +337,7 @@
337337
ModeSource,
338338
PlaneWave,
339339
)
340+
from .components.source.freq_range import FreqRange
340341

341342
# sources
342343
from .components.source.time import (
@@ -540,6 +541,7 @@ def set_logging_level(level: str) -> None:
540541
"FluxTimeDataArray",
541542
"FluxTimeMonitor",
542543
"FossumCarrierLifetime",
544+
"FreqRange",
543545
"FullyAnisotropicMedium",
544546
"GaussianBeam",
545547
"GaussianBeamProfile",

0 commit comments

Comments
 (0)