-
Notifications
You must be signed in to change notification settings - Fork 65
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
[MNT] - Simulations refactors #290
Conversation
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 looked into np.vectorize to learn how to speed up my own code and noticed this was in the numpy doc notes:
The vectorize function is provided primarily for convenience, not for performance. The implementation is essentially a for loop.
I tested this out and a for loop was faster than using np.vectorize. It was also more memory efficient since the 2d cosines
isn't required in the loop I tested.
%load_ext line_profiler
import numpy as np
from neurodsp.sim.aperiodic import sim_knee
from neurodsp.utils.data import create_times, compute_nsamples
# Settings
n_seconds=10
fs=1000
chi1=-1
chi2=-2
knee=100
times = create_times(n_seconds, fs)
n_samples = compute_nsamples(n_seconds, fs)
# Create frequencies for the power spectrum and drop the DC component
# These frequencies are used to create the cosines to sum
freqs = np.linspace(0, fs / 2, num=int(n_samples // 2 + 1), endpoint=True)
freqs = freqs[1:]
%%timeit
np.random.seed(0)
def _coscoef(freq):
return np.sqrt(1 / (freq ** -chi1 * (freq ** (-chi2 - chi1) + knee)))
# Define & vectorize sub-function for computing the set of cosines, adding a random phase shift
def _create_cosines(freq):
return _coscoef(freq) * np.cos(2 * np.pi * freq * times + 2 * np.pi * np.random.rand())
vect_create_cosines = np.vectorize(_create_cosines, signature='()->(n)')
# Create the set of cosines
cosines = vect_create_cosines(freqs)
sig = np.sum(cosines, axis=0)
644 ms ± 3.45 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%%timeit
np.random.seed(0)
sig = np.zeros(n_samples)
for f in freqs:
sig += np.sqrt(1 / (f ** -chi1 * (f ** (-chi2 - chi1) + knee))) \
* np.cos(2 * np.pi * f * times + 2 * np.pi * np.random.rand())
581 ms ± 10.5 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
The latter method is also more memory efficient since it doesn't store a large 2d array of cosines. When I run the np.vectorize block with a 100 second signal and 1000 fs, I get:
MemoryError: Unable to allocate 37.3 GiB for an array with shape (50000, 100000) and data type float64
As for variable names, I do think we should choose either exponent
or chi
. I'm use to it as exponent
since that's what fooof used. But I don't have a strong opinion either way. I'll also give renaming sim_peak_oscillation
some thought. I'm not sure about a better name either.
@ryanhammonds - awesome deep dive! My original understanding was that vectorize makes functions able to be applied across arrays in effectively a loop (sorta like I had a quick poke around with what you suggested for sim_knee, and comparing all three implementations (for an intermediate, 15 second signal), I get:
Which means were getting to about a 100% speed up compared to original! Which is great! Thanks for digging further into this! Practically, I think the main benefit is simply removing extra loops (doing it all in one), which as you mention also has memory benefits! I think the code also ends up looking nice and clean. Next steps:
For naming: we're agreed on consolidating exponent - I think it makes sense to chose that over |
@TomDonoghue I updated I also converted the knee parameter to knee frequency in
from solving for the fwhm in the Lorenztian:
I think this should be the analogous to solving |
I'm going to tag in @rdgao for a quick consult here (hopefully he doesn't mind :)) @ Richard - we've been refactoring some of the simulation functions (most of which you can ignore). One update we would like to make is to update the sim_knee function to take in the "knee frequency" as opposed to the value of the knee parameter - meaning we need the conversion function between the knee parameter and the knee frequency, for the case with two exponents. So, the formulation of the overall spectrum should be: Could you check Ryan's comment above for how to convert the parameter in this case? |
wait just to make sure I understand correctly, this is for the case where there's two exponents? Did we (or I??) ever work out the sim equations for the 2-exponent case with knee? It's this: |
@rdgao - yes, this is for double exponent! You and/or Eric did figure out this equation a while ago! We do have a simulation function for this, from Eric Lybrand, which simulates a double-exponent + knee signal using a sum of sinusoids approach. I missed up the equation I wrote in my comment above (oops - not sure what happened there), but what you wrote is what we are doing (with an added square root?). The code for this equation is The thing we were trying to figure out here, is the mapping between knee parameter & knee frequency. In the single-exp Lorentzian, we convert knee-freq from knee parameter as: |
okay I somehow have zero memory of this, but at a quick glance, I think this might be a bit more complicated, though Ryan's derivations look good, at least in terms of the algebra. I'm a bit occupied in the next week or so for cosyne, but I'll take a look after I come back and maybe play around with some sims just to check (ofcourse you guys can do this as well). I'm actually not sure what the "right" answer for the knee_freq is when there's two slopes, bc it's not so easy to define where the tapering off happens when there's no flat plateau |
@rdgao - there's no particular rush here, this can definitely wait for after CoSyne! I played around with this, and the current candidate version, and it seemed close, but maybe not quite right - I need to dig in a little more. Practically speaking, I'm going to suggest we split out the knee conversion from the rest of this PR, which does other (mostly unrelated) refactors of the simulations. I've opened #301 to continue the knee discussion, from which we can coordinate what to do next for that. Otherwise, I've reverted a couple lines of code from this PR, so that the rest of the PR can be merged, and we can use a new PR for the knee stuff. @ryanhammonds - does this splitting up of updates make sense to you? If so, as far as I'm aware, this PR is now good to merge, right? Let me know if you have any other thoughts on what is currently in this PR - if it looks good to you, I'll merge it in! |
@TomDonoghue Splitting the knee freq update in a different PR sounds good to me, since that will need further review / checking. I went back through the current diff and everything makes sense / looks good! |
This PR does some refactors and tweaks of some of the newer simulation functions.
Naming
An open question (nothing changed yet) is if we want to do any naming updates.
exponent
(orexp
), and now in these newer functions,chi
to refer to the aperiodic exponent. I think I would vote we consolidate on using the termexponent
consistently for this. The main counterpoint I can think of is thatexponent
is a pretty generic name, and in some cases it could be a little unclear.sim_peak_oscillation
, which seems to imply a slightly different thing from this being a "combined" signal. I'm not sure I have a better suggestion though...Thoughts?
Optimizations
For
sim_knee
andsim_peak_oscillation
, there where a bunch of embedded loops that seemed optimizable - the updates here being to use sub-functions where they could be used and where appropriate, vectorize functions for applying across the vectors. Combining both also removed some interim computations of arrays. I also think the code might be a little clearer.Optimization updates:
sim_knee
: speedup of ~20-25% faster of shorter signals (10s), and ~100% faster for longer signals (60s)sim_peak_oscillation
: speedup of ~20-25% faster of shorter signals (10s), and ~100% faster for longer signals (60s)All the actually computations are the same code, and I validated that the new versions give the same results
@ryanhammonds - in terms of review, I'm fairly confident about the updates here, so they shouldn't need a wild amount of further testing for the code changes. Let me know if you have any thoughts on naming, and I'd also be curious to hear if you have any other optimization ideas.