-
Notifications
You must be signed in to change notification settings - Fork 58
Add FreqRange utility class and unit tests (related issue #2529) #2542
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
base: develop
Are you sure you want to change the base?
Conversation
Includes methods for frequency/wavelength conversion, interval updates, and sampling. Adds test coverage for main methods.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hey, this is looking really good, and the tests are thorough!
I do have some comments which are mainly related to the way things work in Tidy3D specifically. You have used a more generic way to define things in python, while our components use pydantic
to define arguments, and inherit from Tidy3dBaseModel
.
Regarding using SI units: since everything in Tidy3D uses Tidy3D units, I think so should the FreqRange
and all its methods.
tidy3d/freq_range.py
Outdated
""" | ||
|
||
# create an instance of GaussianPulse class with defined frequency params | ||
return GaussianPulse(freq0 = self.freq0, fwidth = self.fwidth) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Better to use from_frequency_range here because this method ensures a good amount of power in the pulse signal across the frequency range.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for pointing this out! Fixed it
tidy3d/freq_range.py
Outdated
if not isinstance(freq0, (int, float)): | ||
raise TypeError("Central frequency freq0 must be a number (int or float)") | ||
if not isinstance(fwidth, (int, float)): | ||
raise TypeError("Frequency bandwidth fwidth must be a number (int or float)") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generally, Tidy3D follows a specific development pattern for all components. Everything is a subclass of Tidy3dBaseModel
, which is immutable and does not store an internal state. There is also no init method. The input fields are defined as pydantic
fields (which takes care of the type validation), and there are no private attributes. See e.g. here, but any component, really.
So here, you should decide what would be the fundamental input arguments: for example, freq0
and fwidth
, and everything else should be @property
rather than assigned in the __init__
method. Similarly there would either be no self.num_points
, or if there is one, then it should be static and should be removed as an argument in the samples
method.
I think one thing that is missing in the implementation is to be able to have the points either equally spaced in frequency, or equally spaced in wavelength. This could either be an argument passed to the FreqRange
, or, maybe better, just an argument to samples
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Rewrote class using pydantic. Looks cleaner now. Thank you for walking me through the development pattern, it was helpful!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @George-Guryev , looks good overall! A few things we can work on to make the design more in line with our existing design
tests/test_FreqRange.py
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We generally separate these tests into different sub directories. for this test, I suggest putting inside of tests/test_components/
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Makes sense, fixed it!
tests/test_FreqRange.py
Outdated
@@ -0,0 +1,134 @@ | |||
import numpy as np |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we also generally add a short module level docstring to describe what this file is testing, see other test files for examples.
CHANGELOG.md
Outdated
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 | |||
|
|||
## [Unreleased] | |||
|
|||
### Added | |||
- 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`. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think all three of these bullets could be combined into one, potentially. something like
- Utility class `FreqRange` for frequency and wavelength handling. With constructor methods x, y, and z to initialize frequency data.
I think the details about the arguments can be left out of the changelog for now since they are part of the API reference already. And we wont need to mention that we added unit tests in the changelog.
tidy3d/freq_range.py
Outdated
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add a module level docstring at the top of the file to briefly describe what is in this module.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done!
tidy3d/freq_range.py
Outdated
return val * 1e-6 | ||
|
||
|
||
class FreqRange: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
as Momchil mentioned, we use pydantic to define classes that store data, which also handles initialization. see this for an example
tidy3d/tidy3d/components/apodization.py
Line 16 in d354c66
class ApodizationSpec(Tidy3dBaseModel): |
tidy3d/freq_range.py
Outdated
# class methods | ||
#----------------------------------- | ||
@classmethod | ||
def from_interval(clf, fmin, fmax): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please be sure to add type hints to these and other function signatures, eg fmin: float
Also, in python we usually call the class cls
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for pointing these issues out. Fixed both
tidy3d/freq_range.py
Outdated
|
||
freq_range = clf(freq0, fwidth) | ||
|
||
freq_range.fmin = fmin |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
these are already set in the init method? or is there a need to overwrite their values here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There was an issue with an ambiguous definition of what "central wavelength" and "central frequency" were:
Naturally one interprets central frequency f0 = (fmin + fmax)/ 2 and central wavelength lda0 = (lda_min + lda_max)/2, i.e. both quantities are points in the middle of respective frequency and wavelength ranges.
If we assume that the dispersion equation holds: |k| = omega / c, then wavelength would be inverse-proportional to the corresponding frequency: lambda = c / f. In this case central wavelength does NOT map to the central frequency (and vice versa).
However, the first two lines in the ticket #2529 suggest the following definition:
lda0 = 0.8 # central wavelength
freq0 = td.C_0 / lda0 # central frequency
therefore the "central frequency" freq0 that corresponds to lda0 is not a true central frequency (i.e. it does not lie in the center of the frequency range [fmin, fmax].
Assumption in first commit
In first commit I decided to stick with the ambiguity in definition of "central frequency" and "central wavelength": if user constructs object with frequency args, freq0 is the true central frequency, while lda0 is defined as lda0 = c / freq0, but this "central wavelength" is not a true central wavelength; if user specifies wavelengths - reverse the argument.
To reflect that I had to modify fmin after constructor, as one could not sample frequencies with fwidth without reference frequency.
Assumption in second commit
I was not entirely happy with the solution and decided to change the assumption:
Instead I used the intuitive definitions for central wavelength and central frequency and did not enforce lda0 = c / freq0. This removes unnecessary ambiguity in definition and the need to reassign properties after object construction
tidy3d/freq_range.py
Outdated
raise ValueError("frequencies have to be positive values") | ||
|
||
# calculate frequency points and corresponding wavelengths | ||
self.freqs = np.linspace(self.fmin, self.fmax, self.num_points) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
is there a need to store self.freqs
and self.ldas
? if samples
is meant to simply return a numpy array of frequencies, perhaps we shouldn't be modifying self at all?
it might be worth considering a design where instead of samples(num_points)
we simply expose two methods FreqRange.freqs(num_points)
and FreqRange.wavelengths(num_points)
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a great suggestion! I've replaced samples(num_points)
with freqs(num_points)
and ldas(num_points)
. Looks cleaner now.
tidy3d/freq_range.py
Outdated
|
||
return self.freqs | ||
|
||
def gaussian_pulse(self): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what about if I want to pass other arguments that get passed to GaussianPulse
? such as the amplitude?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
please also be sure to add the FreqRange
class to tidy3d/__init__.py
, otherwise it wont be able to be imported from the tidy3d
namespace (eg import tidy3d as td; td.FreqRange
)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks George!
I'm going to leave my 2nd round of comments here just so they are recorded and I dont lose them. But for the purposes of this exercise, feel free to leave the PR as is (unless you feel compelled to make the changes), we can always pick this up later.
freq0 = 0.5 * (fmax + fmin) | ||
fwidth = 0.5 * (fmax - fmin) | ||
|
||
return cls(freq0=freq0, fwidth=fwidth) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I would prefer maybe to just call cls.from_interval
using fmin
and fmax
, to avoid duplicating operations
# class methods | ||
#----------------------------------- | ||
@classmethod | ||
def from_interval(cls, fmin: float, fmax: float) -> FreqRange: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we have from_interval
and from_wavelength
but both construct the FreqRange
out of intervals. Perhaps to unify the naming, we should call these from_freq_interval
and from_wvl_interval
or similar?
return cls(freq0=freq0, fwidth=fwidth) | ||
|
||
|
||
def freqs(self, num_points: int)->NDArray[np.float64]: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nice, I think this api looks a lot cleaner 👍
# generate array of wavelengths (in ascending order) | ||
return np.linspace(lmin, lmax, num_points) | ||
|
||
def gaussian_pulse(self, **kwargs)->GaussianPulse: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's call this to_gaussian_pulse()
(just because internally we tend to use to_ and from_ for these kinds of conversion methods).
with frequency-specific parameters defined in 'FreqRange' | ||
""" | ||
# create an instance of GaussianPulse class with defined frequency params | ||
return GaussianPulse.from_frequency_range(self.fmin, self.fmax, **kwargs) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
let's pass things using the keywords, eg (from_frequency_range(fmin=self.fmin, ...) for extra robustness.
@@ -7,6 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 | |||
|
|||
## [Unreleased] | |||
|
|||
### Added | |||
- Implemented `FreqRange` utility class for frequency/wavelength handling with constructor methods `from_interval()`, `from_wavelength()`, and `from_wavelength_interval()`. | |||
- Added methods `freqs(num_points)` and `ldas(num_points)` return numpy arrays of frequency and wavelength points uniformly sampled from the specified frequency range. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can just remove these second two bullets, the first should be sufficient. and users can check the API reference in the docs for more details.
1.0, title="Central frequency", description="Real-valued positive central frequency", units="Hz") | ||
|
||
fwidth: pydantic.PositiveFloat = pydantic.Field( | ||
1.0, title="Frequency bandwidth", description="Real-valued positive central frequency", units="Hz") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
description needs updating too.
|
||
Input Parameters | ||
---------------- | ||
wvl0 - central wavelength |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
we'll need to update the docstrings to be consistent with our formatting.
Summary
This pull request implements a new utility class
FreqRange
for convenient handling of frequency and wavelength conversions in simulations.Related issue
Convenience class for handling frequency/wavelength conversion and assignment #2529
What was implemented
Created FreqRange class to:
Added unit tests in tests/test_FreqRange.py to validate: