-
Notifications
You must be signed in to change notification settings - Fork 7
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
Fixing fft guess #915
Fixing fft guess #915
Conversation
Codecov ReportAll modified and coverable lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@
## main #915 +/- ##
==========================================
+ Coverage 97.41% 97.46% +0.04%
==========================================
Files 117 117
Lines 8905 8874 -31
==========================================
- Hits 8675 8649 -26
+ Misses 230 225 -5
Flags with carried forward coverage won't be shown. Click here to find out more.
|
Sorry, this PR closes which issue? #651 seems just an old PR. |
Thanks @rodolfocarobene I linked the wrong issue. |
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.
Apart to the line I don't really understand, it looks good to me
src/qibocal/protocols/rabi/utils.py
Outdated
fft = np.fft.rfft(y) | ||
fft_freqs = np.fft.rfftfreq(len(y), d=(x[1] - x[0])) | ||
mags = abs(fft) | ||
mags[0] = 0 |
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.
Could you explain this?
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.
Yes, when you run fft you expect to see a peak corresponding to the frequency of your signal.
However, you will always find a peak in the first element of the array which corresponds to the dc
component, which can be linked to the sample average.
main
was currently broken because we were not removing this first point (in this case I'm just setting in to zero).
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 see, thanks for the explanation!
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 for reviewing!
I requested you for a review since you recently edited the Rabi protocols #857. I just wanted someone to double check that I didn't break anything :)
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.
Why not fft[1:]
, instead of setting it to 0
?
Ok, it's stupid, but it's just more readable (for me):
mags = abs(fft[1:])
(obviously cutting also fft_freqs
)
It's not truly changing anything, so it definitely personal preference...
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 @andrea-pasquale ! You should propagate it also in other protocols like Ramsey
src/qibocal/protocols/rabi/utils.py
Outdated
# 0.5 hardcoded guess for less than one oscillation | ||
return x[index] / (x[1] - x[0]) if index is not None else 0.5 | ||
return 1 / dominant_freq if dominant_freq != 0 else 0.5 |
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 it is better to return 1 in stead of 0.5 when you acquire less than a period
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.
0.5 is the Nyquist frequency, so it's the maximum value returned by np.fft.rfftfreq
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.
First of all, it is returning a period so it should be 2. I am not fully familiar with this concepts, but the numpy function is returning that maximum value because it is expecting you chose the parameters wisely to completely reconstruct the function but in general this is not the case. Of course, if the frequency is zero and we are expecting an oscillation we are out of the Nyquist theorem, so we can return whatever value (or raise an error).
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.
To be fair: this is fully redundant, because we could deal with this in the above conditional, instead of reflecting it here.
I.e. instead of
qibocal/src/qibocal/protocols/rabi/utils.py
Lines 281 to 286 in 26a43b5
if len(local_maxima) > 0: | |
dominant_freq = fft_freqs[np.argmax(mags)] | |
else: | |
dominant_freq = 0 # Default if no peaks are found | |
# 0.5 hardcoded guess for less than one oscillation | |
return 1 / dominant_freq if dominant_freq != 0 else 0.5 |
use:
if len(local_maxima) > 0:
return 1 / fft_freqs[np.argmax(mags)]
return 2.
Even returning that value when no maximum is found is quite arbitrary. In principle, we could just fail. But in this way you leave your optimizer a chance of finding on its own (in principle, we could even return 4.
, instead, as it would be at half the frequency range [0, 0.5]
, but it's just a random guess)
First of all, it is returning a period so it should be 2.
Correct.
I am not fully familiar with this concepts, but the numpy function is returning that maximum value
The frequency is half the sampling frequency, because you want to make sure to have more than one point per period (approximate explanation). If you take only one point per period, exactly spaced by one period, it would appear a constant. If your frequency is slightly less, you might see as a much wider oscillation (because if you move by 2pi - delta
, it would appear as if you were moving by just -delta
, think about when you look at the fan or helicopter blades, and they seem to move slowly in the opposite direction).
The Nyquist-Shannon theorem proves formally that this "stops" when you take at least two points per period, because you realize there has to be an oscillations in that time range.
So, yes, the period saturates to 2.
it is expecting you chose the parameters wisely to completely reconstruct the function but in general this is not the case.
The problem is that, above the Nyquist frequency, you simply don't know the frequency, if you're sampling.
You can be use a response function to modulate your signal, and move to a different Nyquist zone. But this is inductive bias about you knowing already something about your frequency, so you can distinguish aliases (@rodolfocarobene has also a nice plot to show how this work when you used during synthesis)
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.
Even returning that value when no maximum is found is quite arbitrary. In principle, we could just fail. But in this way you leave your optimizer a chance of finding on its own (in principle, we could even return
4.
, instead, as it would be at half the frequency range[0, 0.5]
, but it's just a random guess)
Ok, but I don't think we can trust the parameters of the fit (otherwise this is against the Nyquist theorem).
The problem is that, above the Nyquist frequency, you simply don't know the frequency, if you're sampling.
Yes, so if the dominant frequency
is zero, this means that your real frequency is above the Nyquist one, but you cannot easily infer which one, so 2 is as good as 3,4,5. For this reason I will suggest to raise an error.
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 was not taking into account the noisy case, usually (especially with probabilities) it is not so common, but also the case above the Nyquist frequency is not, so it is better to take it into account and return a guess. Maybe I suggest to have a warning just to be aware of the situation.
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 usual, I'm not a huge fun of warnings: they are ignored in programs (since side-effects), and then ignored also by users.
In case, much better to return None
, and leave the choice to the caller. If this information should be called, it can be further propagated, and then made part of the Results
.
(i.e. always prefer returning over warnings - otherwise use exceptions)
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.
Actually by putting None
I believe scipy will be put 1
instead.
Shall we stick to 2 or 4 as an initial guess?
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.
Well, there are two considerations here:
- if you return 2 or 4, it is indistinguishable from an actual outcome, so you'll never know (out of this function) whether the maximum was found or not - moreover, this function's sake is to give the best estimate of the sine frequency, not to provide an initial value for a parameter in a fit (a function should never know how it is used, it's definitely anti-modular)
- if you return
None
, you will have to provide a fallback in every place the function is used - it's not terrible, since the "optimal" fallback value may depend on the usage, but if it's happening a lot could be pretty redundant
A modular design could be something like:
fallback_period(guess_period())
with the outer function to be something like lambda x: x if x is not None else 4
. If the function is not going to be used many times, you can just inline it.
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 I implemented the fallback
function in b3891dc
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 didn't test, but it should be mostly fine.
This PR is already doing more than one thing, since it started by fixing the FFT guess, and then deduplicated this guess in many places. Which is great, but it could be another PR, before or after this.
I believe there are more things related to sinusoidal fits to be deduplicated, but let's keep them for another PR.
Co-authored-by: Alessandro Candido <[email protected]>
Co-authored-by: Edoardo Pedicillo <[email protected]>
Closes #897.
Checklist:
master
main
main