Skip to content

Commit d354c66

Browse files
committed
Add FreqRange utility class and unit tests
Includes methods for frequency/wavelength conversion, interval updates, and sampling. Adds test coverage for main methods.
1 parent c955b0e commit d354c66

File tree

3 files changed

+378
-0
lines changed

3 files changed

+378
-0
lines changed

CHANGELOG.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
- Implemented `FreqRange` utility class for frequency/wavelength handling; the default constructor and `from_interval()` class method assume, that the central frequency `freq0` is defined to be `freq0 = (fmin + fmax) / 2`; class methods `from_wavelength()` and `from_wavelegth_interval()` assume that the central wavelength `lda0` is defined to be `lda0 = (lda_min + lda_max) / 2`.
12+
- Added methods to initialize frequency data from intervals and sample values.
13+
- Included unit tests for constructor and edge-case input handling.
14+
15+
## [0.1.0] - 2025-06-03
16+
1017
### Added
1118

1219
- Fields `convex_resolution`, `concave_resolution`, and `mixed_resolution` in `CornerFinderSpec` can be used to take into account the dimensions of autodetected convex, concave, or mixed geometric features when `dl_min` is automatically inferred during automatic grid generation.

tests/test_FreqRange.py

Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import numpy as np
2+
3+
from tidy3d.freq_range import FreqRange, um_to_m
4+
from tidy3d.components.source.time import GaussianPulse
5+
import tidy3d.constants as td_const
6+
7+
8+
def test_constructor():
9+
10+
# set initial params
11+
freq0 = 1e9 # set central frequency
12+
fwidth = 1e8 # set one-side bandwidth
13+
14+
# construct instance of FreqRange class
15+
freq_range = FreqRange(freq0, fwidth)
16+
17+
# validated if class atributes were initialized correctly
18+
assert freq_range.fmin == freq0 - fwidth
19+
assert freq_range.fmax == freq0 + fwidth
20+
assert freq_range.lda0 == um_to_m(td_const.C_0 / freq0)
21+
assert freq_range.freq0 == freq0
22+
assert freq_range.fwidth == fwidth
23+
24+
def test_from_interval():
25+
26+
fmin = 1e10 # new min frequency
27+
fmax = 1e11 # new max frequency
28+
29+
# update object given new frequencies
30+
freq_range = FreqRange.from_interval(fmin=fmin, fmax=fmax)
31+
32+
# validate if frequencies were updated correctly
33+
assert freq_range.fmin == fmin
34+
assert freq_range.fmax == fmax
35+
assert freq_range.freq0 == (fmin + fmax) / 2
36+
assert freq_range.fwidth == (fmax - fmin) / 2
37+
assert freq_range.lda0 == um_to_m(2 * td_const.C_0 / (fmin + fmax))
38+
39+
def test_from_wavelength():
40+
41+
# set initial params
42+
wvl0 = 1e-6
43+
wvl_width = 1e-7
44+
45+
# get the shortest and the longest wavelengths
46+
wvl_min = wvl0 - wvl_width
47+
wvl_max = wvl0 + wvl_width
48+
49+
# define frequency range
50+
freq_range = FreqRange.from_wavelength(wvl0, wvl_width)
51+
52+
#
53+
assert freq_range.lda0 == wvl0
54+
assert freq_range.freq0 == um_to_m(td_const.C_0) / wvl0
55+
56+
57+
print(um_to_m(td_const.C_0) / wvl_max)
58+
print(um_to_m(td_const.C_0) / wvl_min)
59+
60+
assert freq_range.fmin == um_to_m(td_const.C_0 / (wvl_max))
61+
assert freq_range.fmax == um_to_m(td_const.C_0 / (wvl_min))
62+
assert freq_range.fwidth == 0.5 * (um_to_m(td_const.C_0 / wvl_min) - um_to_m(td_const.C_0 / wvl_max))
63+
64+
65+
def test_from_wavelegth_interval():
66+
67+
# set initial params
68+
freq0 = 1e9 # set central frequency
69+
fwidth = 1e8 # set one-side bandwidth
70+
wvl_min = 1e-7
71+
wvl_max = 1e-6
72+
73+
# freq_range = FreqRange(freq0, fwidth)
74+
75+
# update frequencies based on wavelength
76+
freq_range = FreqRange.from_wavelegth_interval(wvl_min, wvl_max)
77+
78+
# ensure that parameters are updated correctly
79+
assert freq_range.lda0 == (wvl_min + wvl_max) / 2
80+
assert freq_range.freq0 == um_to_m(2 * td_const.C_0 / (wvl_max + wvl_min))
81+
assert freq_range.fmax == um_to_m(td_const.C_0 / wvl_min)
82+
assert freq_range.fmin == um_to_m(td_const.C_0 / wvl_max)
83+
assert freq_range.fwidth == 0.5 * (um_to_m(td_const.C_0 / wvl_min) - um_to_m(td_const.C_0 / wvl_max))
84+
85+
def test_samples():
86+
87+
# set initial params
88+
freq0 = 1e9 # set central frequency
89+
fwidth = 1e8 # set one-side bandwidth
90+
num_points = 11
91+
92+
# construct instance of FreqRange class
93+
freq_range = FreqRange(freq0, fwidth)
94+
95+
# form sampling frequency points
96+
freqs = freq_range.samples(num_points)
97+
98+
# make sure
99+
assert np.array_equal(freqs,np.linspace(freq0 - fwidth, freq0 + fwidth, num_points))
100+
101+
# reset number of sampling points to 1
102+
num_points = 1
103+
freqs = freq_range.samples(num_points)
104+
105+
# check if freqs == freq0
106+
assert np.array_equal(freqs, np.array([freq0]))
107+
108+
# reset number of sampling points to 111
109+
num_points = 111
110+
freqs = freq_range.samples(num_points)
111+
112+
# check if sampling frequencies are updated correctly
113+
assert np.array_equal(freqs, np.linspace(freq0 - fwidth, freq0 + fwidth, num_points))
114+
115+
116+
117+
118+
def test_gaussian_pulse():
119+
120+
# set initial params
121+
freq0 = 1e9 # set central frequency
122+
fwidth = 1e8 # set one-side bandwidth
123+
124+
# construct instance of FreqRange class
125+
freq_range = FreqRange(freq0, fwidth)
126+
127+
pulse_exp = GaussianPulse(freq0 = freq0, fwidth = fwidth) # instantiate GaussianPulse explicitly
128+
pulse_imp = freq_range.gaussian_pulse() # get instance by calling a method gaussian_pulse()
129+
130+
assert pulse_exp == pulse_imp # compare two pulses
131+
132+
133+
134+

tidy3d/freq_range.py

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
import numpy as np
2+
3+
from tidy3d import constants as td_const
4+
from tidy3d.components.source.time import GaussianPulse
5+
6+
7+
8+
def um_to_m(val):
9+
"""um_to_m() is a simple function for convertion from [um] to [m]"""
10+
return val * 1e-6
11+
12+
13+
class FreqRange:
14+
"""
15+
Convenience class for handling frequency/wavelength conversion and assignment
16+
17+
Example
18+
-------
19+
>>> freq0 = 1e12
20+
>>> fwidth = 1e11
21+
>>> freq_range = FreqRange(freq0, fwidth)
22+
"""
23+
24+
# constructor
25+
def __init__(self, freq0, fwidth):
26+
"""
27+
FreqRange constructor accepts central frequency "freq0" and one-sided bandwidth "fwidth"
28+
as input arguments; this constructor makes an assumption that freq0 is a central frequency,
29+
i.e. freq0 = (fmin + fmax) / 2; this implies that lda0 != (lda_min + lda_max) / 2
30+
31+
when class methods with wavelength parameters are called,
32+
"fmin" and "fmax" have to be reassigned inside those methods, as there the assumption is:
33+
lda0 = (lda_min + lda_max) / 2; this implies that freq0 != (fmin + fmax) / 2
34+
35+
Parameters
36+
----------
37+
freq0 - central frequency [Hz]
38+
fwidth - one-sided bandwidth [Hz]
39+
"""
40+
41+
if not isinstance(freq0, (int, float)):
42+
raise TypeError("Central frequency freq0 must be a number (int or float)")
43+
if not isinstance(fwidth, (int, float)):
44+
raise TypeError("Frequency bandwidth fwidth must be a number (int or float)")
45+
46+
47+
# assign input args
48+
self.freq0 = freq0
49+
self.fwidth = fwidth
50+
51+
# infer frequency range from central frequency and one-side bandwidth
52+
self.fmin = self.freq0 - self.fwidth
53+
self.fmax = self.freq0 + self.fwidth
54+
55+
self.num_points = 1 # set default number of wavelengths/frequencies to 1
56+
self.freqs = np.array([self.freq0]) # until num_points is updated set freqs to carrier frequency/central frequency
57+
self.lda0 = um_to_m(td_const.C_0 / self.freq0) # set central wavelength
58+
self.ldas = np.array([self.lda0]) # set wavelength range to central wavelength
59+
60+
#-----------------------------------
61+
# class methods
62+
#-----------------------------------
63+
@classmethod
64+
def from_interval(clf, fmin, fmax):
65+
"""
66+
method from_interval() updated instance of class FreqRange by reassigning new
67+
frequency- and wavelength-related parameters; this method assumes, that the following
68+
definition is true:
69+
70+
freq0 = (fmin + fmax) / 2; it implies that lda0 != (lda_min + lda_max) / 2
71+
72+
Input Parameters
73+
----------------
74+
fmin - the lowest frequency [Hz]
75+
fmax - the highest frequency [Hz]
76+
77+
Returns
78+
-------
79+
updated instance of class FreqRange
80+
81+
Example
82+
-------
83+
>>> fmin = 1e12
84+
>>> fmax = 1e13
85+
>>> freq_range = FreqRange.from_interval(fmin, fmax)
86+
"""
87+
88+
if fmax < fmin:
89+
raise ValueError("fmax has to be higher than (or equal to) fmin")
90+
if fmin < 0:
91+
raise ValueError("fmin has to be a non-negative value")
92+
93+
# extract frequency-related info
94+
freq0 = 0.5 * (fmax + fmin) # extract central freq
95+
fwidth = 0.5 * (fmax - fmin) # extract bandwidth
96+
return clf(freq0, fwidth)
97+
98+
@classmethod
99+
def from_wavelength(clf, wvl0, wvl_width):
100+
"""
101+
method from_wavelength() updated instance of class FreqRange by reassigning new
102+
frequency- and wavelength-related parameters; this method assumes, that the following
103+
definition is true:
104+
105+
lda0 = (lda_min + lda_max) / 2; it implies that freq0 != (fmin + fmax) / 2
106+
107+
Input Parameters
108+
----------------
109+
wvl0 - central wavelength [m]
110+
wvl_width - wavelength range [m]
111+
112+
Returns
113+
-------
114+
updated instance of class FreqRange
115+
116+
Example
117+
-------
118+
>>> wvl0 = 1e-6
119+
>>> wvl_width = 1e-7
120+
>>> freq_range = FreqRange.from_wavelength(wvl0, wvl_width)
121+
"""
122+
if wvl0 < wvl_width:
123+
raise ValueError("negative wavelengths are not allowed")
124+
if wvl_width <= 0:
125+
raise ValueError("wavelength range has to be a positive value")
126+
127+
128+
freq0 = um_to_m(td_const.C_0) / wvl0
129+
130+
fmin = um_to_m(td_const.C_0) / (wvl0 + wvl_width)
131+
fmax = um_to_m(td_const.C_0) / (wvl0 - wvl_width)
132+
133+
fwidth = 0.5 * (fmax - fmin)
134+
135+
freq_range = clf(freq0, fwidth)
136+
137+
freq_range.fmin = fmin
138+
freq_range.fmax = fmax
139+
140+
return freq_range
141+
142+
@classmethod
143+
def from_wavelegth_interval(clf, wvl_min, wvl_max):
144+
"""
145+
method from_wavelegth_interval() updated instance of class FreqRange by reassigning new
146+
frequency- and wavelength-related parameters; this method assumes, that the following
147+
definition is true:
148+
149+
lda0 = (lda_min + lda_max) / 2; it implies that freq0 != (fmin + fmax) / 2
150+
151+
Input Parameters
152+
----------------
153+
wvl_min - the shortest wavelength [m]
154+
wvl_max - the longest wavelength [m]
155+
156+
Returns
157+
-------
158+
updated instance of class FreqRange
159+
160+
Example
161+
-------
162+
>>> wvl_min = 1e-7
163+
>>> wvl_max = 1e-6
164+
>>> freq_range = FreqRange.from_wavelength(wvl_min, wvl_max)
165+
"""
166+
167+
if wvl_max <= wvl_min:
168+
raise ValueError("longest wavelength must be greater than the shortest wavelength")
169+
if wvl_min <= 0:
170+
raise ValueError("wavelength has to be a positive value")
171+
172+
# convert wavelength intervals to freq. range
173+
lda0 = 0.5 * (wvl_min + wvl_max) # get central wavelength
174+
freq0 = um_to_m(td_const.C_0 / lda0) # get central frequency
175+
fmax = um_to_m(td_const.C_0 / wvl_min)
176+
fmin = um_to_m(td_const.C_0 / wvl_max)
177+
fwidth = 0.5 * (fmax - fmin) # define width of source frequency range
178+
179+
180+
freq_range = clf(freq0, fwidth)
181+
182+
freq_range.fmin = fmin
183+
freq_range.fmax = fmax
184+
return freq_range
185+
186+
187+
def samples(self, num_points):
188+
"""
189+
method samples() performes discretization of the frequency range by a
190+
given number of points "num_points"; frequency samples are uniformly
191+
distributed in ascending order (e.g. freqs = [3,4,...,100]Hz)
192+
193+
Parameters
194+
----------
195+
num_points - number of frequency points
196+
197+
Returns
198+
-------
199+
freqs - a numpy array of uniformly distributed frequency samples in ascending order
200+
"""
201+
202+
# update number of freq points
203+
self.num_points = num_points
204+
205+
if num_points == 1: # if one sample frequency point is selected
206+
207+
self.freqs = np.array([self.freq0])
208+
self.ldas = np.array([self.lda0])
209+
210+
else: # otherwise
211+
212+
if self.fmax <= self.fmin:
213+
raise ValueError("fmin has to be larger than fmin")
214+
if self.fmin <=0:
215+
raise ValueError("frequencies have to be positive values")
216+
217+
# calculate frequency points and corresponding wavelengths
218+
self.freqs = np.linspace(self.fmin, self.fmax, self.num_points)
219+
220+
# define shortest and longest wavelengths
221+
lmin = um_to_m(td_const.C_0 / self.fmax)
222+
lmax = um_to_m(td_const.C_0 / self.fmin)
223+
224+
# update array of wavelengths (wavelengths would be in descending order)
225+
self.ldas = um_to_m(td_const.C_0 / self.freqs)
226+
227+
return self.freqs
228+
229+
def gaussian_pulse(self):
230+
"""
231+
method gaussian_pulse() returns instance of class GaussianPulse
232+
with central frequency freq0 and width of the source frequency fwidth
233+
"""
234+
235+
# create an instance of GaussianPulse class with defined frequency params
236+
return GaussianPulse(freq0 = self.freq0, fwidth = self.fwidth)
237+

0 commit comments

Comments
 (0)