Skip to content
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

Adding a degree of freedom (dof) flag (--n-independent-echos) to allow users to set the dof used in fstat calculations #1177

Open
wants to merge 19 commits into
base: main
Choose a base branch
from

Conversation

katielamar
Copy link

@katielamar katielamar commented Feb 25, 2025

Hi everyone,
I've updated the tedana code to address issue #1168. I've made this a draft PR since this is my first time contributing to Tedana and I'm a bit new to submitting pull requests. Let me know if you have any suggestions on how I can improve the documentation/code.

Closes #1168.

Summary of Updates:

  • User can now set the degree of freedom that will be used for the fstat calculation. More info: No BOLD components detected error #1168.
  • Tedana from CMD Line: Use --n-independent-echos to set the degrees of freedom when running tedana workflow from the command line
  • Tedana from Python: Set the variable echo_dof when calling the tedana_workflow function

@katielamar katielamar marked this pull request as ready for review February 25, 2025 18:52
Copy link
Member

@handwerkerd handwerkerd left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Besides the one ongoing discussion about a warning message, the rest of this looks good.

We should definitely include some tests. I can help with those, if useful.

@katielamar
Copy link
Author

@handwerkerd The tests/changes you made are in the latest commit. I also made a change to fix the fstat map test you added for echo_dof.

assert np.min(f_t2_maps_orig - f_t2_maps) >= 0
assert np.min(f_s0_maps_orig - f_s0_maps) >= 0

One of the asserts is still failing in fstat map test. Its caused by the following failing. Should this test be strictly greater than 0? I think maybe the f_max may be preventing that. Can you take a look?

assert np.min(f_t2_maps_orig[adaptive_mask == 5] - f_t2_maps[adaptive_mask == 5]) > 0
assert np.min(f_s0_maps_orig[adaptive_mask == 5] - f_s0_maps[adaptive_mask == 5]) > 0

@handwerkerd
Copy link
Member

Since the tests were passing on my local computer & failing here, I decided to push some commits to see if I could just solve it. I think the key problem was we have a maximum F value set to 500. That means, for a test >500, the F is a constant 500 and the difference between DOF==3 vs 5 is 0. Since the goal of the test was to make sure the change in DOF always changed the F stats, I needed to mask out the F==500 tests for both f_t2 and f_s0.

@handwerkerd
Copy link
Member

@tsalo Do you want to take another look or is this good to merge?

Copy link
Member

@tsalo tsalo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a few small requested changes. The main thing I'd like to see changed is to consistently refer to these numbers as either the number of independent echoes or the degrees of freedom. AFAIK the latter is one less than the former, but the terms are used interchangeably in this PR.

I'm also not clear on how the adaptive mask is used when echo_dof is defined. It's not used in the F-statistic estimation, but is it still used to calculate T2* maps and optimally combine data across echoes?

Comment on lines +233 to +236
if echo_dof is None:
f_thresh, _, _ = getfbounds(len(tes))
else:
f_thresh, _, _ = getfbounds(echo_dof)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be simpler to just set echo_dof as len(tes) if it's not provided, at the top of this function.

However, I am concerned that setting the degrees of freedom based on the number of echoes is already a bug (see #811). It just doesn't account for the fact that the degrees of freedom varies across voxels due to the adaptive mask.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree on this change.
Addressing #811 might be useful, but seems beyond the scope of this PR. If we're clarifying which inputs are supposed to be DOF, this might be easier to address.

Comment on lines +723 to +724
# DOF is number of echoes if not otherwise specified
echo_dof = selector.cross_component_metrics_["n_echos"]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to clarify- DOF wouldn't be the number of echoes, right? It would be the number of echoes minus 1.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ug. I'm fairly sure in all calculations that use echo_dof, it's echo_dof - 1. The math is correct, but the terminology is wrong. This also brings up an issue for @katielamar in that, if echo_dof is the true DOF, should we run calculations with echo_dof and not echo_dof - 1?

I think a terminology-consistent solution should be to make echo_dof the DOF, when we calculate this based on the number of echoes, echo_dof = n_echoes - 1 and we'd then need to update all calculations that use these values to not subtract another -1.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You comment below of changing echo_dof to n_independent_sources is another solution that would not require changing formulas.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm yeah I'm not sure. Im going to check with Tom on this and Ill get back to you.

Comment on lines +66 to +69
if echo_dof is None:
fmin, fmid, fmax = getfbounds(n_echos)
else:
fmin, fmid, fmax = getfbounds(echo_dof)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if echo_dof is None:
fmin, fmid, fmax = getfbounds(n_echos)
else:
fmin, fmid, fmax = getfbounds(echo_dof)
echo_dof = echo_dof or n_echos
fmin, fmid, fmax = getfbounds(n_echos)

Comment on lines +31 to +33
f05 = stats.f.ppf(q=(1 - 0.05), dfn=1, dfd=(echo_dof - 1))
f025 = stats.f.ppf(q=(1 - 0.025), dfn=1, dfd=(echo_dof - 1))
f01 = stats.f.ppf(q=(1 - 0.01), dfn=1, dfd=(echo_dof - 1))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it's misleading to call the parameter dof and then subtract 1, unless I'm misunderstanding the math. We should change the function to use the degrees of freedom and pass in n_echos - 1 as needed.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or just change the echo_dof variable to n_independent_sources/n_independent_echos.

Comment on lines +237 to +238
# For EPTI sequences, the way we use adaptive mask thresholding fails
# because sequential echoes have overlapping information.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# For EPTI sequences, the way we use adaptive mask thresholding fails
# because sequential echoes have overlapping information.
# For EPTI sequences, the way we use adaptive mask thresholding fails
# because sequential echoes have overlapping information and
# there is no clear mapping between the independent sources and the echoes.

If we knew how much each independent source contributed to each echo we could figure this out.

f"{n_3dof_voxels} voxels ({np.round(perc_3dof_voxels, decimals=1)}%) have fewer "
f"than {np.round(threshold_3dof)} "
"good voxels. These voxels will be used in all analyses, "
"but might not include 3 independant echo measurements."
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"but might not include 3 independant echo measurements."
"but might not include 3 independent echo measurements."

f"{n_dof_voxels} voxels ({np.round(perc_dof_voxels, decimals=1)}%) have fewer "
f"than {np.round(threshold_dof)} good voxels. "
f"The degrees of freedom for fits across echoes will remain {echo_dof} even if "
"there might be fewer independant echo measurements."
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"there might be fewer independant echo measurements."
"there might be fewer independent echo measurements."

"good voxels. These voxels will be used in all analyses, "
"but might not include 3 independant echo measurements."
)
# There's a separate warning about DOF if it's possible there's a DOF reduction.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# There's a separate warning about DOF if it's possible there's a DOF reduction.
# There's a separate warning about DOF if it's possible there's a DOF reduction.

# For EPTI sequences, the way we use adaptive mask thresholding fails
# because sequential echoes have overlapping information.
# Since EPTI has less dropout, it is unclear how often this will cause issues.
# To track this, we are flagging voxels that might mark less independant signal.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# To track this, we are flagging voxels that might mark less independant signal.
# To track this, we are flagging voxels that might mark less independent signal.

Comment on lines +251 to +252
f"than {np.round(threshold_3dof)} "
"good voxels. These voxels will be used in all analyses, "
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
f"than {np.round(threshold_3dof)} "
"good voxels. These voxels will be used in all analyses, "
f"than {np.round(threshold_3dof)} good voxels. "
"These voxels will be used in all analyses, "

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

No BOLD components detected error
3 participants