-
Notifications
You must be signed in to change notification settings - Fork 141
Drift update #437
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
Drift update #437
Changes from all commits
a50bbef
c82e57f
13db940
0dbf242
9c1342b
79e7457
3ace12f
b3140a8
ab3297f
5d229b3
c632237
192a305
d2526d9
5ec1c63
e21342d
d80b89e
3109a19
79f4b9b
debb8b2
eb82c55
88ee76e
251e2bb
59346f1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
|
|
@@ -90,6 +90,7 @@ | |||
| from scipy import signal | ||||
| import scipy.ndimage as ndimage | ||||
| import copy | ||||
| from scipy import optimize | ||||
|
|
||||
| __all__ = [ | ||||
| "apply_signal", | ||||
|
|
@@ -1523,7 +1524,7 @@ def _generate_noise_temporal_task(stimfunction_tr, | |||
|
|
||||
| def _generate_noise_temporal_drift(trs, | ||||
| tr_duration, | ||||
| basis="discrete_cos", | ||||
| basis="cos_power_drop", | ||||
| period=150, | ||||
| ): | ||||
|
|
||||
|
|
@@ -1544,10 +1545,17 @@ def _generate_noise_temporal_drift(trs, | |||
| basis : str | ||||
| What is the basis function for the drift. Could be made of discrete | ||||
| cosines (for longer run durations, more basis functions are | ||||
| created) or a sine wave. | ||||
| created) that either have equal power ('discrete_cos') or the power | ||||
| diminishes such that 99% of the power is below a specified frequency | ||||
| ('cos_power_drop'). Alternatively, this drift could simply be a sine | ||||
| wave ('sine') | ||||
|
|
||||
| period : int | ||||
| How many seconds is the period of oscillation of the drift | ||||
| When the basis function is 'cos_power_drop' this is the period over | ||||
| which no power of the drift exceeds (i.e. the power of the drift | ||||
| asymptotes at this period). However for the other basis functions, | ||||
| this is simply how many seconds is the period of oscillation of the | ||||
| drift | ||||
|
|
||||
| Returns | ||||
| ---------- | ||||
|
|
@@ -1597,6 +1605,86 @@ def _generate_noise_temporal_drift(trs, | |||
| phase = (timepoints / (trs - 1) * cycles * 2 * np.pi) + phaseshift | ||||
| noise_drift = np.sin(phase) | ||||
|
|
||||
| elif basis == 'cos_power_drop': | ||||
|
|
||||
| # Make a vector counting each TR | ||||
| timepoints = np.linspace(0, trs - 1, trs) * tr_duration | ||||
|
|
||||
| # Specify the other timing information | ||||
| duration = trs * tr_duration | ||||
|
|
||||
| # How bases do you have? This is to adhere to Nyquist | ||||
| basis_funcs = int(trs) | ||||
|
|
||||
| noise_drift = np.zeros((timepoints.shape[0], basis_funcs)) | ||||
| for basis_counter in list(range(1, basis_funcs + 1)): | ||||
| # What steps do you want to take for this basis function | ||||
| random_phase = np.random.rand() * np.pi * 2 | ||||
|
|
||||
| timepoint_phase = (timepoints / duration * np.pi * basis_counter) | ||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Here,
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The reason for The reason for having the
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If this makes sense, is there something else I could add to the comments to clarify?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks. Now I think I may have got it. You are trying to make a power spectrum that decreases exponentially from the lowest frequency (inverse of double the duration of the scan, corresponding to when
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. But then I am confused with the description
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @lcnature is right that the units aren't right there. The reasoning for having the sum be up to L isn't based on the Nyquist freq (though it is related), it is because that is the number of basis functions in the DCT. For example see the top of pg 2 here - u indexes the timepoints, m indexes the basis functions. The twos are coming from the geometric series equation, since power is proportional to I believe the numerator is correct as is but the denominator needs to be put back into # of TRs units, e.g. This implies that the shortest possible period is tr_duration which also makes intuitive sense.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for this clarification @cbaldassano , it helped my understanding. Because of it I added, these changes, some documentation, an example and a check to ensure that brainiak/brainiak/utils/fmrisim.py Line 1679 in e21342d
basis_weights = r ** np.arange(basis_funcs)). This line is meant to find weights for each of the basis functions before they are all added together; however, based on my new understanding shouldn't this be: basis_weights = r ** (2 * np.arange(basis_funcs)) in order to make power proportional?
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No, the power spectrum is the square of the coefficients. So the coefficients themselves are 1, r, r^2, r^3... and the power spectrum of the frequencies is 1, r^2, r^4, r^6...
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Aha, okay thanks for that clarification. The code should then be ready to go, what do you think @lcnature ?
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Looks great. I am glad to approve it! |
||||
|
|
||||
| # In radians, what is the value for each time point | ||||
| timepoints_basis = timepoint_phase + random_phase | ||||
|
|
||||
| # Store the drift from this basis func | ||||
| noise_drift[:, basis_counter - 1] = np.cos(timepoints_basis) | ||||
|
|
||||
| def power_drop(r, L, F, tr_duration): | ||||
| # Function to return the drop rate for the power of basis functions | ||||
| # In other words, how much should the weight of each basis function | ||||
| # reduce in order to make the power you retain of the period's | ||||
| # frequency be 99% of the total power of the highest frequency, as | ||||
| # defined by the DCT. | ||||
| # For an example where there are 20 time points, there will be 20 | ||||
| # basis functions in the DCT. If the period of the signal you wish | ||||
| # to simulate is such that 99% of the power should drop off after | ||||
| # the equivalent of 5 of these basis functions, then the way this | ||||
| # code works is it finds the rate at which power must drop off for | ||||
| # all of the 20 basis functions such that by the 5th one, there is | ||||
| # only 1% of the power remaining. | ||||
| # r is the power reduction rate which should be between 0 and 1 | ||||
| # L is the duration of the run in seconds | ||||
| # F is period of the cycle in seconds It is assumed that this will | ||||
| # be greater than the tr_duration, or else this will not work | ||||
| # tr_duration is the duration of each TR in seconds | ||||
|
|
||||
| # Check the TR duration | ||||
| if F < tr_duration: | ||||
| msg = 'Period %0.0f > TR duration %0.0f' % ((F, tr_duration)) | ||||
| raise ValueError(msg) | ||||
|
|
||||
| percent_retained = 0.99 # What is the percentage of power retained | ||||
|
|
||||
| # Compare the power at the period frequency (in the numerator) with | ||||
| # the power at the frequency of the DCT, AKA the highest possible | ||||
| # frequency in the data (in the denominator) | ||||
| numerator = 1 - r ** (2 * L / F) # Power of this period | ||||
| denominator = 1 - r ** (2 * L / tr_duration) # Power of DCT freq. | ||||
|
|
||||
| # Calculate the retained power | ||||
| power_drop = abs((numerator / denominator) - percent_retained) | ||||
| return power_drop | ||||
|
|
||||
| # Solve for power reduction rate. | ||||
| # This assumes that r is between 0 and 1 | ||||
| # Takes the duration and period as arguments | ||||
| sol = optimize.minimize_scalar(power_drop, | ||||
| bounds=(0, 1), | ||||
| method='Bounded', | ||||
| args=(duration, period, tr_duration)) | ||||
|
|
||||
| # Pull out the solution | ||||
| r = sol.x | ||||
|
|
||||
| # Weight the basis functions based on the power drop off | ||||
| basis_weights = r ** np.arange(basis_funcs) | ||||
|
CameronTEllis marked this conversation as resolved.
|
||||
|
|
||||
| # Weigh the basis functions | ||||
| weighted_basis_funcs = np.multiply(noise_drift, basis_weights) | ||||
|
|
||||
| # Average the drift | ||||
| noise_drift = np.mean(weighted_basis_funcs, 1) | ||||
|
|
||||
| # Normalize so the sigma is 1 | ||||
| noise_drift = stats.zscore(noise_drift) | ||||
|
|
||||
|
|
@@ -2012,19 +2100,6 @@ def _generate_noise_temporal(stimfunction_tr, | |||
| # Preset the volume | ||||
| noise_volume = np.zeros((dimensions[0], dimensions[1], dimensions[2], trs)) | ||||
|
|
||||
| # Generate the drift noise | ||||
| if noise_dict['drift_sigma'] != 0: | ||||
| # Calculate the drift time course | ||||
| noise = _generate_noise_temporal_drift(trs, | ||||
| tr_duration, | ||||
| ) | ||||
| # Create a volume with the drift properties | ||||
| volume = np.ones(dimensions) | ||||
|
|
||||
| # Combine the volume and noise | ||||
| noise_volume += np.multiply.outer(volume, noise) * noise_dict[ | ||||
| 'drift_sigma'] | ||||
|
|
||||
| # Generate the physiological noise | ||||
| if noise_dict['physiological_sigma'] != 0: | ||||
|
|
||||
|
|
@@ -2285,6 +2360,7 @@ def _noise_dict_update(noise_dict): | |||
|
|
||||
| def _fit_spatial(noise, | ||||
| noise_temporal, | ||||
| drift_noise, | ||||
| mask, | ||||
| template, | ||||
| spatial_sd, | ||||
|
|
@@ -2306,6 +2382,9 @@ def _fit_spatial(noise, | |||
| noise_temporal : multidimensional array, float | ||||
| The temporal noise that was generated by _generate_temporal_noise | ||||
|
|
||||
| drift_noise : multidimensional array, float | ||||
| The drift noise generated by _generate_noise_temporal_drift | ||||
|
|
||||
| tr_duration : float | ||||
| What is the duration, in seconds, of each TR? | ||||
|
|
||||
|
|
@@ -2400,7 +2479,8 @@ def _fit_spatial(noise, | |||
| ) | ||||
|
|
||||
| # Sum up the noise of the brain | ||||
| noise = base + (noise_temporal * temporal_sd) + noise_system | ||||
| noise = base + drift_noise + noise_system | ||||
| noise += (noise_temporal * temporal_sd) # Add the brain specific noise | ||||
|
|
||||
| # Reject negative values (only happens outside of the brain) | ||||
| noise[noise < 0] = 0 | ||||
|
|
@@ -2423,6 +2503,7 @@ def _fit_temporal(noise, | |||
| spatial_sd, | ||||
| temporal_proportion, | ||||
| temporal_sd, | ||||
| drift_noise, | ||||
| noise_dict, | ||||
| fit_thresh, | ||||
| fit_delta, | ||||
|
|
@@ -2466,6 +2547,9 @@ def _fit_temporal(noise, | |||
| What is the standard deviation in time of the noise volume to be | ||||
| generated | ||||
|
|
||||
| drift_noise : multidimensional array, float | ||||
| The drift noise generated by _generate_noise_temporal_drift | ||||
|
|
||||
| noise_dict : dict | ||||
| A dictionary specifying the types of noise in this experiment. The | ||||
| noise types interact in important ways. First, all noise types | ||||
|
|
@@ -2581,7 +2665,8 @@ def _fit_temporal(noise, | |||
| ) | ||||
|
|
||||
| # Sum up the noise of the brain | ||||
| noise = base + (noise_temporal * temporal_sd) + noise_system | ||||
| noise = base + drift_noise + noise_system | ||||
| noise += (noise_temporal * temporal_sd) # Add the brain specific noise | ||||
|
|
||||
| # Reject negative values (only happens outside of the brain) | ||||
| noise[noise < 0] = 0 | ||||
|
|
@@ -2737,7 +2822,7 @@ def generate_noise(dimensions, | |||
| # What is the mean signal of the non masked voxels in this template? | ||||
| mean_signal = (base[mask > 0]).mean() | ||||
|
|
||||
| # Generate the noise | ||||
| # Generate the temporal noise | ||||
| noise_temporal = _generate_noise_temporal(stimfunction_tr=stimfunction_tr, | ||||
| tr_duration=tr_duration, | ||||
| dimensions=dimensions, | ||||
|
|
@@ -2746,6 +2831,22 @@ def generate_noise(dimensions, | |||
| noise_dict=noise_dict, | ||||
| ) | ||||
|
|
||||
| # Generate the drift noise | ||||
| if noise_dict['drift_sigma'] != 0: | ||||
| # Calculate the drift time course | ||||
| noise = _generate_noise_temporal_drift(len(stimfunction_tr), | ||||
| tr_duration, | ||||
| ) | ||||
| # Create a volume with the drift properties | ||||
| volume = np.ones(dimensions[:3]) | ||||
|
|
||||
| # Combine the volume and noise | ||||
| drift_noise = np.multiply.outer(volume, noise) * noise_dict[ | ||||
|
CameronTEllis marked this conversation as resolved.
|
||||
| 'drift_sigma'] | ||||
| else: | ||||
| # If there is no drift, then just make this zeros (in 4d) | ||||
| drift_noise = np.zeros(dimensions_tr) | ||||
|
|
||||
| # Convert SFNR into the size of the standard deviation of temporal | ||||
| # variability | ||||
| temporal_sd = (mean_signal / noise_dict['sfnr']) | ||||
|
|
@@ -2765,14 +2866,16 @@ def generate_noise(dimensions, | |||
| ) | ||||
|
|
||||
| # Sum up the noise of the brain | ||||
| noise = base + (noise_temporal * temporal_sd) + noise_system | ||||
| noise = base + drift_noise + noise_system | ||||
| noise += (noise_temporal * temporal_sd) # Add the brain specific noise | ||||
|
|
||||
| # Reject negative values (only happens outside of the brain) | ||||
| noise[noise < 0] = 0 | ||||
|
|
||||
| # Fit the SNR | ||||
| noise, spatial_sd = _fit_spatial(noise, | ||||
| noise_temporal, | ||||
| drift_noise, | ||||
| mask, | ||||
| template, | ||||
| spatial_sd, | ||||
|
|
@@ -2792,6 +2895,7 @@ def generate_noise(dimensions, | |||
| spatial_sd, | ||||
| temporal_proportion, | ||||
| temporal_sd, | ||||
| drift_noise, | ||||
| noise_dict, | ||||
| fit_thresh, | ||||
| fit_delta, | ||||
|
|
||||
Uh oh!
There was an error while loading. Please reload this page.