Repository: ME-ICA/tedana

Issue:
No BOLD components detected error
Hi – I am attempting to run Tedana v24.0.2 on EPTI data (https://martinos.org/~fw089/publications.html; 122 TEs, spanning 4.405 ms to 63.695 ms, 3mm isotropic voxels, TR = 1.7 seconds, MB factor = 2, ~15 minutes total). When I view the raw data, there are no obvious artifacts or indications that something went wrong with the acquisition, looks good

Hoewever, Tedana fails to classify any of the components as BOLD-like, despite there being obvious signals of interest  (see an example below; by the way, Tedana interpreted the TR in the header file as 1700 seconds, not 1700 milliseconds, which is why the FFT and time axis looks so strange, I have since changed the TR in the headers to be 1.7, which solved that issue). 

WARNING  tedana:tedana_workflow:834 No BOLD components found, but maximum number of restarts reached.
WARNING  tedana:tedana_workflow:913 No BOLD components detected! Please check data and results!

![Image](https://github.com/user-attachments/assets/5f9d917f-6d30-49e2-83aa-4ae1c2df12c0)

One other thing to note is that in the course of my troubleshooting, I found that if I use 3 TEs only (the first, middle, and last TE), Tedana performs as expected, the signal classifications look fine, 

Does anyone have any ideas about what might be the issue here, or suggestions for things to troubleshoot when encountering this particular error? I can share the data if it would be helpful, 

Thank you!
Chuck

Discussion:
@CesarCaballeroGaudes: Hi Charles (@cjl2007), let me suggest that it would be nice to see the slope of the kappa & rho parameters. As far as this component is concerned, the kappa value is much larger than rho. Therefore, it is likely BOLD. Btw, thanks for testing EPTI. Looking forward to hearing your opinion about this promising sequence. @cjl2007: Hi Cesar (@CesarCaballeroGaudes) --- Thank you for the quick response. I pasted a screenshot of the kappa and rho value distributions below, definitely looks different... not the usual "L" shape in the kappa vs. rho scatter I like to see, and the Rho Rank has this weird sigmoid like look to it that is not typical, right? 

![Image](https://github.com/user-attachments/assets/758191e2-e1e8-49df-8a55-b0d92e5694a5)



@CesarCaballeroGaudes: Hi @cjl2007, thanks for the kappa & rho plot. The fact that no components where labelled as BOLD related is what I find most surprising. There are some components with a very high kappa and low rho that should have been accepted according to the TE-dependent principles. My intuition makes me think that the elbow for the kappa curve should not be the one computed, but in the elbow in the kappa around 40 instead. In addition, can you share the command? or more specifically, which decision tree are you using?
@eurunuela: > In addition, can you share the command? or more specifically, which decision tree are you using?

You can find the command at the bottom of the html report.@cjl2007: I have tried a variety of options, different decision trees, different pca options, with / without explicit mask, etc. 

Here is an example command,

tedana -d /home/charleslynch/Desktop/ME06/func/rest/session_1/run_1/Rest_E001_acpc.nii.gz /home/charleslynch/Desktop/ME06/func/rest/session_1/run_1/Rest_E002_acpc.nii.gz /home/charleslynch/Desktop/ME06/func/rest/session_1/run_1/Rest_E003_acpc.nii.gz /home/charleslynch/Desktop/ME06/func/rest/session_1/run_1/Rest_E004_acpc.nii.gz /home/charleslynch/Desktop/ME06/func/rest/session_1/run_1/Rest_E005_acpc.nii.gz /home/charleslynch/Desktop/ME06/func/rest/session_1/run_1/Rest_E006_acpc.nii.gz /home/charleslynch/Desktop/ME06/func/rest/session_1/run_1/Rest_E007_acpc.nii.gz /home/charleslynch/Desktop/ME06/func/rest/session_1/run_1/Rest_E008_acpc.nii.gz /home/charleslynch/Desktop/ME06/func/rest/session_1/run_1/Rest_E009_acpc.nii.gz /home/charleslynch/Desktop/ME06/func/rest/session_1/run_1/Rest_E010_acpc.nii.gz /home/charleslynch/Desktop/ME06/func/rest/session_1/run_1/Rest_E011_acpc.nii.gz -e 7.1 12.98 18.86 24.74 30.62 36.5 42.38 48.26 54.14 60.02 63.45 --out-dir /home/charleslynch/Desktop/ME06/func/rest/session_1/run_1/Tedana/ --tedpca kundu --fittype curvefit --mask /home/charleslynch/Desktop/ME06/func/rest/session_1/run_1/brain_mask.nii.gz --maxit 500 --maxrestart 5 --seed 42

Note: I am not running all 122 TEs in this case for the purposes of speeding up my debugging, @handwerkerd: I'll try to respond more later, but I wanted to tag @katielamar. She's working on exactly this with Tom Liu at UCSD. The one core known issue (which we still need to integrate into the main tedana) is that F stats are calculated for T2* and S0 fits as part of the calculations of kappa and rho. Since EPITI has many overlapping echoes but still only 3-4 independent degrees of freedom, this skews the F stats. The fix is to set the degrees of freedom in a way that not the total number of echoes.@katielamar: Hi, yeah we had the same issue. The DOF should be changed to the number of bases set in the EPTI reconstruction. The default is 3 or 4 for most of the EPTI scripts provided by MGH. Currently Tedana sets the DOF to the number of echoes (as Dan mentioned). Once we changed that, Tedana worked well with EPTI data (even with 122 echoes). Let me know if you have any further questions.@tsalo: Is the number of bases recorded in the DICOMs/BIDS sidecar at all?@katielamar: For EPTI, the number of bases is set in the recon script. I don’t think they make note of it in any of the recon outputs. For most of the 3T scripts it is set at line 110 except for the GESE (p3/p6) where it’s set at line 125. @tsalo: On tedana's end, we could maybe add a command-line argument to override the degrees of freedom (`--n-independent-echos`?).

From a dataset curation perspective, I think we should probably add a metadata field that indicates the number of bases to the BIDS specification. Optimally, fMRIPost-tedana will just infer that information from the sidecar.@katielamar: Yeah that would be great. I ended up cloning this repo and adding that flag myself so I can run it easily from the command line. Definitely, makes it easier to have the flag since I’m often comparing the EPTI results to ME-EPI where the number of echoes is the DOF.@tsalo: @handwerkerd we calculate the F-statistics for ascending numbers of echoes, based on the adaptive mask. Is there some link between the echo times used and the number of independent echoes that we can use for that? Something like a mapping between echo time and the base(s) it's derived from?@cjl2007: Ah, this all makes sense, thank you, @katielamar. I see  bases variable definition (K = 3) in the recon Matlab code now. 

This also explains why Tedana was performing as expected in my hands when I tried running it using 3 of the 122 TEs only. 

@katielamar - If you have time and are willing to share, I would be interested in trying your modified version with the additional flag controlling the DOF. 

@katielamar: Sure. More than happy to share. Would you like to meet over a video call? I can also just send the code with a quick explanation of what I changed if that is easier. I'm a little busy over the next few days and I would need to clean my code up a bit before sending, but I could probably get that to you by next week. @eurunuela: @katielamar if you don't mind and feel comfortable with it, I think it would be very useful to have your changes in a pull request, so we can incorporate them to tedana at some point.@handwerkerd: Sorry for the delayed response from me. If @katielamar can also share your existing code with me or @eurunuela we can add it to the main tedana. 

To @tsalo's  Q, I agree there is a separate issue with adaptive mask. We only want to use a voxel with at least 3 good echoes for the ICA step. The trouble with this is that the first three echoes in EPITI are nearly identical data. We want to make sure the echoes are good up through the point where there's at least parts of 3 independent measurements. I haven't fully dug into EPITI reconstruction to  figure out what the shortest number of echoes that sample from at least 3 bases (the math is fancier, but it's roughly a sliding window reconstruction). By just treating it like a sliding window, the conservative answer would be, for bases set=3, (2/3)*(total # of echoes)+1. I suspect the actual recon method would include info from all 3 bases at a shorter echo. The problem is, with 100 echoes going to >100ms, I doubt the first good echo is at last as 67ms. This might be overly conservative. I haven't personally worked with EPITI data yet. Maybe Katie knows more?@handwerkerd: I also agree it would be good to get the number of bases used for reconstruction either directly into the NIFTI header or into a BIDS JSON. This is an important parameter that would be nearly impossible to estimate post hoc, since the point point of multi-echo is that there is dependence across echoes, and we're trying to measure dependence between independent measurements.

As issue can be opened for BIDS here, but I'm slightly traumatized by my last attempt. https://github.com/bids-standard/bids-specification/issues The specification thoughts I have are:
- Should EPITI be it's own [Pulse Sequence Type](https://bids-specification.readthedocs.io/en/latest/glossary.html#objects.metadata.PulseSequenceType)? It's not clear if there is a standard list of pulse sequence types for that key.
- In [timing parameters](https://bids-specification.readthedocs.io/en/latest/modality-specific-files/magnetic-resonance-imaging-data.html#timing-parameters) add something like `EPITIBasisSet` that expects a number
- Overall, since each echo 4D volume has it's own JSON, I guess this means there are now ~100 JSON files that will be identical except for the [EchoTime](https://bids-specification.readthedocs.io/en/latest/glossary.html#objects.metadata.EchoTime). This sounds like a mess, but I'm not sure how to fix it without higher-level changes in BIDS.@katielamar: Sure. I'm more than happy to share my code. @handwerkerd, I will reach out to you via email. I think before I share the code it needs be cleaned up a little bit. The way I had to pass the DOF variable into the calc_kappa_elbow and calc_rho_elbow functions was a bit hacky. I've occasionally run into bugs due to it and although I have a workaround myself, there is definitely a better way to do this. Let me get that cleaned up (maybe with some help from Dan) before I make a pull request. 

Solution patch:
diff --git a/.circleci/config.yml b/.circleci/config.yml
index 65c8f4f..69aec07 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -4,7 +4,7 @@
 #
 version: 2.1
 orbs:
-  codecov: codecov/codecov@5.3.0
+  codecov: codecov/codecov@5.2.1
 jobs:
   makeenv_38:
     docker:
@@ -374,17 +374,18 @@ jobs:
           name: Merge coverage files
           command: |
             apt-get update
-            apt-get install -yqq curl
+            apt-get install -yqq curl gnupg
             source activate tedana_py38  # depends on makeenv38
             cd /tmp/src/coverage/
             coverage combine
             coverage xml
+            mv coverage.xml /tmp/src/tedana/
       - store_artifacts:
           path: /tmp/src/coverage
+      - codecov/upload
 
 workflows:
-  version: 2.1
-  build_test:
+  upload-to-codecov:
     jobs:
       - makeenv_38
       - unittest_38:
diff --git a/README.md b/README.md
index eb12e5f..f7a1a32 100644
--- a/README.md
+++ b/README.md
@@ -181,6 +181,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
       <td align="center" valign="top" width="20%"><a href="https://github.com/mvdoc"><img src="https://avatars.githubusercontent.com/u/6150554?v=4?s=100" width="100px;" alt="Matteo Visconti di Oleggio Castello"/><br /><sub><b>Matteo Visconti di Oleggio Castello</b></sub></a><br /><a href="https://github.com/ME-ICA/tedana/commits?author=mvdoc" title="Code">💻</a> <a href="https://github.com/ME-ICA/tedana/issues?q=author%3Amvdoc" title="Bug reports">🐛</a> <a href="https://github.com/ME-ICA/tedana/commits?author=mvdoc" title="Documentation">📖</a></td>
       <td align="center" valign="top" width="20%"><a href="http://www.mrtrix.org"><img src="https://avatars.githubusercontent.com/u/5637955?v=4?s=100" width="100px;" alt="Robert Smith"/><br /><sub><b>Robert Smith</b></sub></a><br /><a href="https://github.com/ME-ICA/tedana/commits?author=Lestropie" title="Code">💻</a> <a href="#design-Lestropie" title="Design">🎨</a> <a href="#ideas-Lestropie" title="Ideas, Planning, & Feedback">🤔</a> <a href="https://github.com/ME-ICA/tedana/pulls?q=is%3Apr+reviewed-by%3ALestropie" title="Reviewed Pull Requests">👀</a></td>
       <td align="center" valign="top" width="20%"><a href="https://github.com/BahmanTahayori"><img src="https://avatars.githubusercontent.com/u/19523962?v=4?s=100" width="100px;" alt="Bahman "/><br /><sub><b>Bahman </b></sub></a><br /><a href="https://github.com/ME-ICA/tedana/commits?author=BahmanTahayori" title="Code">💻</a> <a href="#design-BahmanTahayori" title="Design">🎨</a> <a href="#ideas-BahmanTahayori" title="Ideas, Planning, & Feedback">🤔</a></td>
+      <td align="center" valign="top" width="20%"><a href="https://katielamar.github.io/"><img src="https://avatars.githubusercontent.com/u/65981856?v=4?s=100" width="100px;" alt="Katie Lamar"/><br /><sub><b>Katie Lamar</b></sub></a><br /><a href="https://github.com/ME-ICA/tedana/commits?author=katielamar" title="Code">💻</a> <a href="#design-katielamar" title="Design">🎨</a> <a href="#ideas-katielamar" title="Ideas, Planning, & Feedback">🤔</a></td>
     </tr>
   </tbody>
 </table>
diff --git a/docs/faq.rst b/docs/faq.rst
index bcb81cd..c3fecde 100644
--- a/docs/faq.rst
+++ b/docs/faq.rst
@@ -125,7 +125,7 @@ The clear failure cases are extreme. That is getting less than 1/5 the number of
 compared to time points or having nearly as many components as time points.
 We are working on identifying why this happens and adding better solutions.
 Our current guess is that most of the above methods assume data are
-independant and identically distributed (IID),
+independent and identically distributed (IID),
 and signal leakage from in-slice and multi-slice accelleration may violate this assumption.
 
 We have one option that is generally useful and is also a partial solution.
diff --git a/tedana/decomposition/pca.py b/tedana/decomposition/pca.py
index fd90478..4669569 100644
--- a/tedana/decomposition/pca.py
+++ b/tedana/decomposition/pca.py
@@ -55,6 +55,7 @@ def tedpca(
     adaptive_mask,
     io_generator,
     tes,
+    n_independent_echos=None,
     algorithm="aic",
     kdaw=10.0,
     rdaw=1.0,
@@ -79,6 +80,10 @@ def tedpca(
         The output generation object for this workflow
     tes : :obj:`list`
         List of echo times associated with `data_cat`, in milliseconds
+    n_independent_echos : :obj:`int`, optional
+        Number of independent echoes to use in goodness of fit metrics (fstat).
+        Primarily used for EPTI acquisitions.
+        If None, number of echoes will be used. Default is None.
     algorithm : {'kundu', 'kundu-stabilize', 'mdl', 'aic', 'kic', float}, optional
         Method with which to select components in TEDPCA. PCA
         decomposition with the mdl, kic and aic options are based on a Moving Average
@@ -355,6 +360,7 @@ def tedpca(
         mixing=comp_ts,
         adaptive_mask=adaptive_mask,
         tes=tes,
+        n_independent_echos=n_independent_echos,
         io_generator=io_generator,
         label="PCA",
         external_regressors=None,
@@ -377,6 +383,7 @@ def tedpca(
         component_table, metric_metadata = kundu_tedpca(
             component_table,
             n_echos,
+            n_independent_echos,
             kdaw,
             rdaw,
             stabilize=False,
@@ -385,6 +392,7 @@ def tedpca(
         component_table, metric_metadata = kundu_tedpca(
             component_table,
             n_echos,
+            n_independent_echos,
             kdaw,
             rdaw,
             stabilize=True,
diff --git a/tedana/metrics/collect.py b/tedana/metrics/collect.py
index c3892f1..0365752 100644
--- a/tedana/metrics/collect.py
+++ b/tedana/metrics/collect.py
@@ -29,6 +29,7 @@ def generate_metrics(
     mixing: npt.NDArray,
     adaptive_mask: npt.NDArray,
     tes: Union[List[int], List[float], npt.NDArray],
+    n_independent_echos: int = None,
     io_generator: io.OutputGenerator,
     label: str,
     external_regressors: Union[pd.DataFrame, None] = None,
@@ -53,6 +54,10 @@ def generate_metrics(
         For more information on thresholding, see `make_adaptive_mask`.
     tes : list
         List of echo times associated with `data_cat`, in milliseconds
+    n_independent_echos : int
+        Number of independent echoes to use in goodness of fit metrics (fstat).
+        Primarily used for EPTI acquisitions.
+        If None, number of echoes will be used. Default is None.
     io_generator : tedana.io.OutputGenerator
         The output generator object for this workflow
     label : str in ['ICA', 'PCA']
@@ -122,6 +127,10 @@ def generate_metrics(
     # Ensure that echo times are in an array, rather than a list
     tes = np.asarray(tes)
 
+    # use either the inputted number of indie echoes or the total number of echoes
+    # to calculate the threshold for f tests
+    f_thresh, _, _ = getfbounds(n_independent_echos or len(tes))
+
     # Get reference image from io_generator
     ref_img = io_generator.reference_img
 
@@ -196,6 +205,7 @@ def generate_metrics(
             mixing=mixing,
             adaptive_mask=adaptive_mask,
             tes=tes,
+            n_independent_echos=n_independent_echos,
         )
         metric_maps["map FT2"] = m_t2
         metric_maps["map FS0"] = m_s0
@@ -224,7 +234,7 @@ def generate_metrics(
 
     if "map FT2 clusterized" in required_metrics:
         LGR.info("Calculating T2* F-statistic maps")
-        f_thresh, _, _ = getfbounds(len(tes))
+
         metric_maps["map FT2 clusterized"] = dependence.threshold_map(
             maps=metric_maps["map FT2"],
             mask=mask,
@@ -234,7 +244,7 @@ def generate_metrics(
 
     if "map FS0 clusterized" in required_metrics:
         LGR.info("Calculating S0 F-statistic maps")
-        f_thresh, _, _ = getfbounds(len(tes))
+
         metric_maps["map FS0 clusterized"] = dependence.threshold_map(
             maps=metric_maps["map FS0"],
             mask=mask,
diff --git a/tedana/metrics/dependence.py b/tedana/metrics/dependence.py
index b7c6d09..c9271c6 100644
--- a/tedana/metrics/dependence.py
+++ b/tedana/metrics/dependence.py
@@ -136,6 +136,7 @@ def calculate_f_maps(
     mixing: np.ndarray,
     adaptive_mask: np.ndarray,
     tes: np.ndarray,
+    n_independent_echos=None,
     f_max: float = 500,
 ) -> typing.Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
     """Calculate pseudo-F-statistic maps for TE-dependence and -independence models.
@@ -153,6 +154,10 @@ def calculate_f_maps(
         "good signal". Limited to masked voxels.
     tes : (E) array_like
         Echo times in milliseconds, in the same order as the echoes in data_cat.
+    n_independent_echos : int
+        Number of independent echoes to use in goodness of fit metrics (fstat).
+        Primarily used for EPTI acquisitions.
+        If None, number of echoes will be used. Default is None.
     f_max : float, optional
         Maximum F-statistic, used to crop extreme values. Values in the
         F-statistic maps greater than this value are set to it.
@@ -201,7 +206,10 @@ def calculate_f_maps(
             pred_s0 = x1[:j_echo, :] * np.tile(coeffs_s0, (j_echo, 1))
             sse_s0 = (comp_betas[:j_echo] - pred_s0) ** 2
             sse_s0 = sse_s0.sum(axis=0)  # (S,) prediction error map
-            f_s0 = (alpha - sse_s0) * (j_echo - 1) / (sse_s0)
+            if n_independent_echos is None or n_independent_echos >= j_echo:
+                f_s0 = (alpha - sse_s0) * (j_echo - 1) / (sse_s0)
+            else:
+                f_s0 = (alpha - sse_s0) * (n_independent_echos - 1) / (sse_s0)
             f_s0[f_s0 > f_max] = f_max
             f_s0_maps[mask_idx, i_comp] = f_s0[mask_idx]
 
@@ -212,7 +220,10 @@ def calculate_f_maps(
             pred_t2 = x2[:j_echo] * np.tile(coeffs_t2, (j_echo, 1))
             sse_t2 = (comp_betas[:j_echo] - pred_t2) ** 2
             sse_t2 = sse_t2.sum(axis=0)
-            f_t2 = (alpha - sse_t2) * (j_echo - 1) / (sse_t2)
+            if n_independent_echos is None or n_independent_echos >= j_echo:
+                f_t2 = (alpha - sse_t2) * (j_echo - 1) / (sse_t2)
+            else:
+                f_t2 = (alpha - sse_t2) * (n_independent_echos - 1) / (sse_t2)
             f_t2[f_t2 > f_max] = f_max
             f_t2_maps[mask_idx, i_comp] = f_t2[mask_idx]
 
diff --git a/tedana/selection/component_selector.py b/tedana/selection/component_selector.py
index d9b6eb2..99242ff 100644
--- a/tedana/selection/component_selector.py
+++ b/tedana/selection/component_selector.py
@@ -505,6 +505,7 @@ class ComponentSelector:
                 f"Step {self.current_node_idx_}: Running function {node['functionname']} "
                 f"with parameters: {all_params}"
             )
+
             # run the decision node function
             self = fcn(self, **params, **kwargs)
 
diff --git a/tedana/selection/selection_nodes.py b/tedana/selection/selection_nodes.py
index 97b59ca..7a8423f 100644
--- a/tedana/selection/selection_nodes.py
+++ b/tedana/selection/selection_nodes.py
@@ -714,10 +714,20 @@ def calc_kappa_elbow(
     This also means the kappa elbow should be calculated before those two other functions
     are called
     """
+    if (
+        "n_independent_echos" in selector.cross_component_metrics_.keys()
+        and selector.cross_component_metrics_["n_independent_echos"]
+    ):
+        n_independent_echos = selector.cross_component_metrics_["n_independent_echos"]
+    else:
+        # Number of independent echoes is the number of echoes if not otherwise specified
+        n_independent_echos = selector.cross_component_metrics_["n_echos"]
+
     outputs = {
         "decision_node_idx": selector.current_node_idx_,
         "node_label": None,
         "n_echos": selector.cross_component_metrics_["n_echos"],
+        "n_independent_echos": n_independent_echos,
         "used_metrics": {"kappa"},
         "calc_cross_comp_metrics": [
             "kappa_elbow_kundu",
@@ -777,7 +787,7 @@ def calc_kappa_elbow(
             outputs["varex_upper_p"],
         ) = kappa_elbow_kundu(
             selector.component_table_,
-            selector.cross_component_metrics_["n_echos"],
+            n_independent_echos,
             comps2use=comps2use,
         )
         selector.cross_component_metrics_["kappa_elbow_kundu"] = outputs["kappa_elbow_kundu"]
@@ -846,10 +856,20 @@ def calc_rho_elbow(
             f"It is {rho_elbow_type} "
         )
 
+    if (
+        "n_independent_echos" in selector.cross_component_metrics_.keys()
+        and selector.cross_component_metrics_["n_independent_echos"]
+    ):
+        n_independent_echos = selector.cross_component_metrics_["n_independent_echos"]
+    else:
+        # DOF is number of echoes if not otherwise specified
+        n_independent_echos = selector.cross_component_metrics_["n_echos"]
+
     outputs = {
         "decision_node_idx": selector.current_node_idx_,
         "node_label": None,
         "n_echos": selector.cross_component_metrics_["n_echos"],
+        "n_independent_echos": n_independent_echos,
         "calc_cross_comp_metrics": [
             elbow_name,
             "rho_allcomps_elbow",
@@ -904,7 +924,7 @@ def calc_rho_elbow(
             outputs["elbow_f05"],
         ) = rho_elbow_kundu_liberal(
             selector.component_table_,
-            selector.cross_component_metrics_["n_echos"],
+            n_independent_echos,
             rho_elbow_type=rho_elbow_type,
             comps2use=comps2use,
             subset_comps2use=subset_comps2use,
diff --git a/tedana/selection/selection_utils.py b/tedana/selection/selection_utils.py
index 6389d07..49110b3 100644
--- a/tedana/selection/selection_utils.py
+++ b/tedana/selection/selection_utils.py
@@ -580,7 +580,7 @@ def getelbow(arr, return_val=False):
         return k_min_ind
 
 
-def kappa_elbow_kundu(component_table, n_echos, comps2use=None):
+def kappa_elbow_kundu(component_table, n_independent_echos, comps2use=None):
     """
     Calculate an elbow for kappa.
 
@@ -592,8 +592,10 @@ def kappa_elbow_kundu(component_table, n_echos, comps2use=None):
         Component metric table. One row for each component, with a column for
         each metric. The index should be the component number.
         Only the 'kappa' column is used in this function
-    n_echos : :obj:`int`
-        The number of echos in the multi-echo data
+    n_independent_echos : :obj:`int`
+        Number of independent echoes to use in goodness of fit metrics (fstat).
+        Typically the number of echos in the multi-echo data
+        May be a lower value for EPTI acquisitions.
     comps2use : :obj:`list[int]`
         A list of component indices used to calculate the elbow
         default=None which means use all components
@@ -633,7 +635,7 @@ def kappa_elbow_kundu(component_table, n_echos, comps2use=None):
     kappas2use = component_table.loc[comps2use, "kappa"].to_numpy()
 
     # low kappa threshold
-    _, _, f01 = getfbounds(n_echos)
+    _, _, f01 = getfbounds(n_independent_echos)
     # get kappa values for components below a significance threshold
     kappas_nonsig = kappas2use[kappas2use < f01]
 
@@ -670,7 +672,11 @@ def kappa_elbow_kundu(component_table, n_echos, comps2use=None):
 
 
 def rho_elbow_kundu_liberal(
-    component_table, n_echos, rho_elbow_type="kundu", comps2use=None, subset_comps2use=-1
+    component_table,
+    n_independent_echos,
+    rho_elbow_type="kundu",
+    comps2use=None,
+    subset_comps2use=-1,
 ):
     """
     Calculate an elbow for rho.
@@ -684,8 +690,10 @@ def rho_elbow_kundu_liberal(
         Component metric table. One row for each component, with a column for
         each metric. The index should be the component number.
         Only the 'kappa' column is used in this function
-    n_echos : :obj:`int`
-        The number of echos in the multi-echo data
+    n_independent_echos : :obj:`int`
+        Number of independent echoes to use in goodness of fit metrics (fstat).
+        Typically the number of echos in the multi-echo data
+        May be a lower value for EPTI acquisitions.
     rho_elbow_type : :obj:`str`
         The algorithm used to calculate the rho elbow. Current options are
         'kundu' and 'liberal'.
@@ -753,8 +761,7 @@ def rho_elbow_kundu_liberal(
         ].tolist()
 
     # One rho elbow threshold set just on the number of echoes
-    elbow_f05, _, _ = getfbounds(n_echos)
-
+    elbow_f05, _, _ = getfbounds(n_independent_echos)
     # One rho elbow threshold set using all componets in comps2use
     rhos_comps2use = component_table.loc[comps2use, "rho"].to_numpy()
     rho_allcomps_elbow = getelbow(rhos_comps2use, return_val=True)
diff --git a/tedana/selection/tedpca.py b/tedana/selection/tedpca.py
index 3765d0a..c00d4d6 100644
--- a/tedana/selection/tedpca.py
+++ b/tedana/selection/tedpca.py
@@ -15,7 +15,9 @@ RepLGR = logging.getLogger("REPORT")
 F_MAX = 500
 
 
-def kundu_tedpca(component_table, n_echos, kdaw=10.0, rdaw=1.0, stabilize=False):
+def kundu_tedpca(
+    component_table, n_echos, n_independent_echos=None, kdaw=10.0, rdaw=1.0, stabilize=False
+):
     """Select PCA components using Kundu's decision tree approach.
 
     Parameters
@@ -25,6 +27,10 @@ def kundu_tedpca(component_table, n_echos, kdaw=10.0, rdaw=1.0, stabilize=False)
         variance explained. Component number should be the index.
     n_echos : :obj:`int`
         Number of echoes in dataset.
+    n_independent_echos : int
+        Number of independent echoes to use in goodness of fit metrics (fstat).
+        Primarily used for EPTI acquisitions.
+        If None, number of echoes will be used. Default is None.
     kdaw : :obj:`float`, optional
         Kappa dimensionality augmentation weight. Must be a non-negative float,
         or -1 (a special value). Default is 10.
@@ -59,8 +65,9 @@ def kundu_tedpca(component_table, n_echos, kdaw=10.0, rdaw=1.0, stabilize=False)
         + 1
     ]
     varex_norm_cum = np.cumsum(component_table["normalized variance explained"])
+    n_independent_echos = n_independent_echos or n_echos
+    fmin, fmid, fmax = getfbounds(n_independent_echos)
 
-    fmin, fmid, fmax = getfbounds(n_echos)
     if int(kdaw) == -1:
         lim_idx = (
             utils.andb([component_table["kappa"] < fmid, component_table["kappa"] > fmin]) == 2
diff --git a/tedana/stats.py b/tedana/stats.py
index 18be493..c5ef13d 100644
--- a/tedana/stats.py
+++ b/tedana/stats.py
@@ -11,14 +11,16 @@ LGR = logging.getLogger("GENERAL")
 RepLGR = logging.getLogger("REPORT")
 
 
-def getfbounds(n_echos):
+def getfbounds(n_independent_sources):
     """
     Get F-statistic boundaries based on number of echos.
 
     Parameters
     ----------
-    n_echos : :obj:`int`
-        Number of echoes
+    n_independent_sources : :obj:`int`
+        The number of independent sources to calculate DOF for goodness of fit metrics (fstat).
+        Typically the number of echos in the multi-echo data
+        May be a lower value for EPTI acquisitions.
 
     Returns
     -------
@@ -26,9 +28,9 @@ def getfbounds(n_echos):
         F-statistic thresholds for alphas of 0.05, 0.025, and 0.01,
         respectively.
     """
-    f05 = stats.f.ppf(q=(1 - 0.05), dfn=1, dfd=(n_echos - 1))
-    f025 = stats.f.ppf(q=(1 - 0.025), dfn=1, dfd=(n_echos - 1))
-    f01 = stats.f.ppf(q=(1 - 0.01), dfn=1, dfd=(n_echos - 1))
+    f05 = stats.f.ppf(q=(1 - 0.05), dfn=1, dfd=(n_independent_sources - 1))
+    f025 = stats.f.ppf(q=(1 - 0.025), dfn=1, dfd=(n_independent_sources - 1))
+    f01 = stats.f.ppf(q=(1 - 0.01), dfn=1, dfd=(n_independent_sources - 1))
     return f05, f025, f01
 
 
diff --git a/tedana/utils.py b/tedana/utils.py
index f66044e..b246e2c 100644
--- a/tedana/utils.py
+++ b/tedana/utils.py
@@ -54,7 +54,7 @@ def reshape_niimg(data):
     return fdata
 
 
-def make_adaptive_mask(data, mask, threshold=1, methods=["dropout"]):
+def make_adaptive_mask(data, mask, n_independent_echos=None, threshold=1, methods=["dropout"]):
     """Make map of `data` specifying longest echo a voxel can be sampled with.
 
     Parameters
@@ -66,6 +66,10 @@ def make_adaptive_mask(data, mask, threshold=1, methods=["dropout"]):
         This must be provided, as the mask is used to identify exemplar voxels.
         Without a mask limiting the voxels to consider,
         the adaptive mask will generally select voxels outside the brain as exemplars.
+    n_independent_echos : :obj:`int`, optional
+        Number of independent echoes to use in goodness of fit metrics (fstat).
+        Primarily used for EPTI acquisitions.
+        If None, number of echoes will be used. Default is None.
     threshold : :obj:`int`, optional
         Minimum echo count to retain in the mask.
         Default is 1, which is equivalent to not thresholding.
@@ -230,8 +234,43 @@ def make_adaptive_mask(data, mask, threshold=1, methods=["dropout"]):
         )
         adaptive_mask[adaptive_mask < threshold] = 0
 
-    modified_mask = adaptive_mask.astype(bool)
+    if isinstance(n_independent_echos, int):
+        # 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.
+        # 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 independent signal.
+        # If such voxels appear often, this would show we might need to alter how the mask is used.
+        # The thresh where there might not be 3 independent sources of data within the good echoes
+        # For n_echos=100 & n_independent_echos=3, threshold_dof=66.
+        # For n_echos=100 & n_independent_echos=4, threshold_dof=50
+        threshold_3dof = np.floor(2 * n_echos / n_independent_echos)
+        n_3dof_voxels = np.sum(np.logical_and(adaptive_mask < threshold_3dof, adaptive_mask >= 1))
+        perc_3dof_voxels = 100 * n_3dof_voxels / np.sum(adaptive_mask >= 1)
+        if perc_3dof_voxels > 0:
+            LGR.warning(
+                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 independent echo measurements."
+            )
 
+        # There's a separate warning about DOF if it's possible there's a DOF reduction.
+        if n_independent_echos > 3:
+            # The threshold where the loss of good echoes might affect the DOF
+            # For n_echos=100 & n_independent_echos=4, threshold_dof=75
+            threshold_dof = np.floor((n_independent_echos - 1) * n_echos / n_independent_echos)
+            n_dof_voxels = np.sum(
+                np.logical_and(adaptive_mask < threshold_dof, adaptive_mask >= 1)
+            )
+            perc_dof_voxels = 100 * n_dof_voxels / np.sum(adaptive_mask >= 1)
+            LGR.warning(
+                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 {n_independent_echos} "
+                f"even if there might be fewer independent echo measurements."
+            )
+    modified_mask = adaptive_mask.astype(bool)
     adaptive_mask = unmask(adaptive_mask, mask)
     modified_mask = unmask(modified_mask, mask)
 
diff --git a/tedana/workflows/t2smap.py b/tedana/workflows/t2smap.py
index fdeb119..ef03307 100644
--- a/tedana/workflows/t2smap.py
+++ b/tedana/workflows/t2smap.py
@@ -132,6 +132,18 @@ def _get_parser():
         help=("Combination scheme for TEs: t2s (Posse 1999), paid (Poser)"),
         default="t2s",
     )
+    optional.add_argument(
+        "--n-independent-echos",
+        dest="n_independent_echos",
+        metavar="INT",
+        type=int,
+        help=(
+            "Number of independent echoes to use in goodness of fit metrics (fstat)."
+            "Primarily used for EPTI acquisitions."
+            "If not provided, number of echoes will be used."
+        ),
+        default=None,
+    )
     optional.add_argument(
         "--n-threads",
         dest="n_threads",
@@ -174,6 +186,7 @@ def _get_parser():
 def t2smap_workflow(
     data,
     tes,
+    n_independent_echos=None,
     out_dir=".",
     mask=None,
     prefix="",
@@ -200,6 +213,10 @@ def t2smap_workflow(
         list of echo-specific files, in ascending order.
     tes : :obj:`list`
         List of echo times associated with data in milliseconds.
+    n_independent_echos : :obj:`int`, optional
+        Number of independent echoes to use in goodness of fit metrics (fstat).
+        Primarily used for EPTI acquisitions.
+        If None, number of echoes will be used. Default is None.
     out_dir : :obj:`str`, optional
         Output directory.
     mask : :obj:`str`, optional
@@ -227,7 +244,6 @@ def t2smap_workflow(
     overwrite : :obj:`bool`, optional
         If True, force overwriting of files. Default is False.
 
-
     Other Parameters
     ----------------
     debug : :obj:`bool`, optional
@@ -323,6 +339,7 @@ def t2smap_workflow(
     mask, masksum = utils.make_adaptive_mask(
         data_cat,
         mask=mask,
+        n_independent_echos=n_independent_echos,
         threshold=1,
         methods=masktype,
     )
diff --git a/tedana/workflows/tedana.py b/tedana/workflows/tedana.py
index 1f54361..2c28bbb 100644
--- a/tedana/workflows/tedana.py
+++ b/tedana/workflows/tedana.py
@@ -369,6 +369,19 @@ def _get_parser():
         help="Force overwriting of files.",
         default=False,
     )
+
+    optional.add_argument(
+        "--n-independent-echos",
+        dest="n_independent_echos",
+        metavar="INT",
+        type=int,
+        help=(
+            "Number of independent echoes to use in goodness of fit metrics (fstat)."
+            "Primarily used for EPTI acquisitions."
+            "If not provided, number of echoes will be used."
+        ),
+        default=None,
+    )
     optional.add_argument("-v", "--version", action="version", version=verstr)
     parser._action_groups.append(optional)
 
@@ -385,6 +398,7 @@ def tedana_workflow(
     masktype=["dropout"],
     fittype="loglin",
     combmode="t2s",
+    n_independent_echos=None,
     tree="tedana_orig",
     external_regressors=None,
     ica_method=DEFAULT_ICA_METHOD,
@@ -442,6 +456,10 @@ def tedana_workflow(
         Default is 'loglin'.
     combmode : {'t2s'}, optional
         Combination scheme for TEs: 't2s' (Posse 1999, default).
+    n_independent_echos : :obj:`int`, optional
+        Number of independent echoes to use in goodness of fit metrics (fstat).
+        Primarily used for EPTI acquisitions.
+        If None, number of echoes will be used. Default is None.
     tree : {'tedana_orig', 'meica', 'minimal', 'json file'}, optional
         Decision tree to use for component selection. Can be a
         packaged tree (tedana_orig, meica, minimal) or a user-supplied JSON file that
@@ -696,6 +714,7 @@ def tedana_workflow(
     mask_denoise, masksum_denoise = utils.make_adaptive_mask(
         data_cat,
         mask=mask,
+        n_independent_echos=n_independent_echos,
         threshold=1,
         methods=masktype,
     )
@@ -775,6 +794,7 @@ def tedana_workflow(
             masksum_clf,
             io_generator,
             tes=tes,
+            n_independent_echos=n_independent_echos,
             algorithm=tedpca,
             kdaw=10.0,
             rdaw=1.0,
@@ -815,6 +835,7 @@ def tedana_workflow(
                 mixing=mixing,
                 adaptive_mask=masksum_clf,
                 tes=tes,
+                n_independent_echos=n_independent_echos,
                 io_generator=io_generator,
                 label="ICA",
                 metrics=necessary_metrics,
@@ -827,6 +848,7 @@ def tedana_workflow(
                 selector,
                 n_echos=n_echos,
                 n_vols=n_vols,
+                n_independent_echos=n_independent_echos,
             )
             n_likely_bold_comps = selector.n_likely_bold_comps_
             LGR.info("Selecting components from ICA results")
@@ -835,6 +857,7 @@ def tedana_workflow(
                 selector,
                 n_echos=n_echos,
                 n_vols=n_vols,
+                n_independent_echos=n_independent_echos,
             )
             n_likely_bold_comps = selector.n_likely_bold_comps_
             if (n_restarts < maxrestart) and (n_likely_bold_comps == 0):
@@ -876,6 +899,7 @@ def tedana_workflow(
             mixing=mixing,
             adaptive_mask=masksum_clf,
             tes=tes,
+            n_independent_echos=n_independent_echos,
             io_generator=io_generator,
             label="ICA",
             metrics=necessary_metrics,
@@ -883,7 +907,11 @@ def tedana_workflow(
             external_regressor_config=selector.tree["external_regressor_config"],
         )
         selector = selection.automatic_selection(
-            component_table, selector, n_echos=n_echos, n_vols=n_vols
+            component_table,
+            selector,
+            n_echos=n_echos,
+            n_vols=n_vols,
+            n_independent_echos=n_independent_echos,
         )
 
     # TODO The ICA mixing matrix should be written out after it is created


Test patch:
diff --git a/tedana/tests/test_integration.py b/tedana/tests/test_integration.py
index 359fdcc..1825091 100644
--- a/tedana/tests/test_integration.py
+++ b/tedana/tests/test_integration.py
@@ -126,11 +126,13 @@ def test_integration_five_echo(skip_integration):
     suffix = ".sm.nii.gz"
     datalist = [prepend + str(i + 1) + suffix for i in range(5)]
     echo_times = [15.4, 29.7, 44.0, 58.3, 72.6]
+    # adding n_independent_echos=4 to test workflow code using n_independent_echos is executed
     tedana_cli.tedana_workflow(
         data=datalist,
         tes=echo_times,
         ica_method="robustica",
         n_robust_runs=4,
+        n_independent_echos=4,
         out_dir=out_dir,
         tedpca=0.95,
         fittype="curvefit",
@@ -632,6 +634,7 @@ def test_integration_t2smap(skip_integration):
         + [str(te) for te in echo_times]
         + ["--out-dir", out_dir, "--fittype", "curvefit"]
         + ["--masktype", "dropout", "decay"]
+        + ["--n-independent-echos", "4"]
     )
     t2smap_cli._main(args)
 
diff --git a/tedana/tests/test_metrics.py b/tedana/tests/test_metrics.py
index 7f97a4d..0356ca7 100644
--- a/tedana/tests/test_metrics.py
+++ b/tedana/tests/test_metrics.py
@@ -205,17 +205,67 @@ def test_smoke_calculate_f_maps():
     data_cat = np.random.random((n_voxels, n_echos, n_volumes))
     z_maps = np.random.normal(size=(n_voxels, n_components))
     mixing = np.random.random((n_volumes, n_components))
-    adaptive_mask = np.random.randint(1, n_echos + 1, size=n_voxels)
+    # The ordering is random, but make sure the adaptive mask always includes values of 1-5
+    adaptive_mask = np.random.permutation(
+        np.concatenate(
+            (
+                np.tile(1, int(np.round(n_voxels * 0.05))),
+                np.tile(2, int(np.round(n_voxels * 0.1))),
+                np.tile(3, int(np.round(n_voxels * 0.4))),
+                np.tile(4, int(np.round(n_voxels * 0.2))),
+                np.tile(5, int(np.round(n_voxels * 0.25))),
+            )
+        )
+    )
     tes = np.array([15, 25, 35, 45, 55])
+    f_t2_maps_orig, f_s0_maps_orig, _, _ = dependence.calculate_f_maps(
+        data_cat=data_cat,
+        z_maps=z_maps,
+        mixing=mixing,
+        adaptive_mask=adaptive_mask,
+        tes=tes,
+        f_max=500,
+    )
+    assert f_t2_maps_orig.shape == f_s0_maps_orig.shape == (n_voxels, n_components)
+
+    # rerunning with n_independent_echos=3
     f_t2_maps, f_s0_maps, _, _ = dependence.calculate_f_maps(
         data_cat=data_cat,
         z_maps=z_maps,
         mixing=mixing,
         adaptive_mask=adaptive_mask,
         tes=tes,
+        n_independent_echos=3,
         f_max=500,
     )
     assert f_t2_maps.shape == f_s0_maps.shape == (n_voxels, n_components)
+    # exclude voxels f==0 and f==f_max since the >0 clause for 5 echoes wouldn't be true
+    noextreme_f_mask = np.logical_and(
+        np.logical_and(
+            np.logical_and(f_t2_maps_orig > 0.0, f_s0_maps_orig > 0.0), f_t2_maps_orig < 500
+        ),
+        f_s0_maps_orig < 500,
+    )
+    # When n_independent_echos == the number of echoes (3),
+    # then f_maps_orig should equal f_maps
+    echo3_mask = np.logical_and(np.tile(adaptive_mask == 3, (50, 1)).T, noextreme_f_mask)
+    assert np.round(
+        np.min(f_t2_maps_orig[echo3_mask] - f_t2_maps[echo3_mask]), decimals=3
+    ) == np.round(0.0, decimals=3)
+    assert np.round(
+        np.min(f_s0_maps_orig[echo3_mask] - f_s0_maps[echo3_mask]), decimals=3
+    ) == np.round(0.0, decimals=3)
+    assert np.round(
+        np.max(f_t2_maps_orig[echo3_mask] - f_t2_maps[echo3_mask]), decimals=3
+    ) == np.round(0.0, decimals=3)
+    assert np.round(
+        np.max(f_s0_maps_orig[echo3_mask] - f_s0_maps[echo3_mask]), decimals=3
+    ) == np.round(0.0, decimals=3)
+    # When n_independent_echos==3, there are 5 good echoes,
+    # then f_maps_orig should always be larger than f_maps with fewer DOF
+    echo5_mask = np.logical_and(np.tile(adaptive_mask == 5, (50, 1)).T, noextreme_f_mask)
+    assert np.min(f_t2_maps_orig[echo5_mask] - f_t2_maps[echo5_mask]) > 0.0
+    assert np.min(f_s0_maps_orig[echo5_mask] - f_s0_maps[echo5_mask]) > 0.0
 
 
 def test_smoke_calculate_varex():
diff --git a/tedana/tests/test_selection_utils.py b/tedana/tests/test_selection_utils.py
index 89d3b06..3c05720 100644
--- a/tedana/tests/test_selection_utils.py
+++ b/tedana/tests/test_selection_utils.py
@@ -388,7 +388,7 @@ def test_kappa_elbow_kundu_smoke():
         kappa_allcomps_elbow,
         kappa_nonsig_elbow,
         varex_upper_p,
-    ) = selection_utils.kappa_elbow_kundu(component_table, n_echos=5)
+    ) = selection_utils.kappa_elbow_kundu(component_table, n_independent_echos=5)
     assert isinstance(kappa_elbow_kundu, float)
     assert isinstance(kappa_allcomps_elbow, float)
     assert isinstance(kappa_nonsig_elbow, float)
@@ -401,7 +401,7 @@ def test_kappa_elbow_kundu_smoke():
         kappa_allcomps_elbow,
         kappa_nonsig_elbow,
         varex_upper_p,
-    ) = selection_utils.kappa_elbow_kundu(component_table, n_echos=6)
+    ) = selection_utils.kappa_elbow_kundu(component_table, n_independent_echos=6)
     assert isinstance(kappa_elbow_kundu, float)
     assert isinstance(kappa_allcomps_elbow, float)
     assert isinstance(kappa_nonsig_elbow, type(None))
@@ -415,7 +415,7 @@ def test_kappa_elbow_kundu_smoke():
         varex_upper_p,
     ) = selection_utils.kappa_elbow_kundu(
         component_table,
-        n_echos=5,
+        n_independent_echos=5,
         comps2use=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 14, 15, 17, 18, 20],
     )
     assert isinstance(kappa_elbow_kundu, float)
@@ -434,7 +434,7 @@ def test_rho_elbow_kundu_liberal_smoke():
         rho_allcomps_elbow,
         rho_unclassified_elbow,
         elbow_f05,
-    ) = selection_utils.rho_elbow_kundu_liberal(component_table, n_echos=3)
+    ) = selection_utils.rho_elbow_kundu_liberal(component_table, n_independent_echos=3)
     assert isinstance(rho_elbow_kundu, float)
     assert isinstance(rho_allcomps_elbow, float)
     assert isinstance(rho_unclassified_elbow, float)
@@ -447,7 +447,7 @@ def test_rho_elbow_kundu_liberal_smoke():
         rho_unclassified_elbow,
         elbow_f05,
     ) = selection_utils.rho_elbow_kundu_liberal(
-        component_table, n_echos=3, rho_elbow_type="liberal"
+        component_table, n_independent_echos=3, rho_elbow_type="liberal"
     )
     assert isinstance(rho_elbow_kundu, float)
     assert isinstance(rho_allcomps_elbow, float)
@@ -462,7 +462,7 @@ def test_rho_elbow_kundu_liberal_smoke():
         elbow_f05,
     ) = selection_utils.rho_elbow_kundu_liberal(
         component_table,
-        n_echos=3,
+        n_independent_echos=3,
         rho_elbow_type="kundu",
         comps2use=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 14, 15, 17, 18, 20],
         subset_comps2use=[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 13, 14, 18, 20],
@@ -479,7 +479,7 @@ def test_rho_elbow_kundu_liberal_smoke():
         rho_allcomps_elbow,
         rho_unclassified_elbow,
         elbow_f05,
-    ) = selection_utils.rho_elbow_kundu_liberal(component_table, n_echos=3)
+    ) = selection_utils.rho_elbow_kundu_liberal(component_table, n_independent_echos=3)
     assert isinstance(rho_elbow_kundu, float)
     assert isinstance(rho_allcomps_elbow, float)
     assert isinstance(rho_unclassified_elbow, type(None))
@@ -487,7 +487,7 @@ def test_rho_elbow_kundu_liberal_smoke():
 
     with pytest.raises(ValueError):
         selection_utils.rho_elbow_kundu_liberal(
-            component_table, n_echos=3, rho_elbow_type="perfect"
+            component_table, n_independent_echos=3, rho_elbow_type="perfect"
         )
 
 
diff --git a/tedana/tests/test_utils.py b/tedana/tests/test_utils.py
index cbbdcc9..0bf4212 100644
--- a/tedana/tests/test_utils.py
+++ b/tedana/tests/test_utils.py
@@ -74,7 +74,7 @@ def test_reshape_niimg():
     assert utils.reshape_niimg(fimg.get_fdata()).shape == exp_shape
 
 
-def test_make_adaptive_mask():
+def test_make_adaptive_mask(caplog):
     """Test tedana.utils.make_adaptive_mask with different methods."""
     # load data make masks
     mask_file = pjoin(datadir, "mask.nii.gz")
@@ -102,6 +102,16 @@ def test_make_adaptive_mask():
     # Decay: good good good (3)
     data[idx + 5, :, :] = np.array([1, 0.9, -1])[:, None]
 
+    # Simulating 5 echo data to test the n_independent_echos parameter
+    data5 = np.concatenate(
+        (
+            data,
+            0.95 * np.expand_dims(data[:, 2, :], axis=1),
+            0.9 * np.expand_dims(data[:, 2, :], axis=1),
+        ),
+        axis=1,
+    )
+
     # Just dropout method
     mask, adaptive_mask = utils.make_adaptive_mask(
         data,
@@ -122,6 +132,7 @@ def test_make_adaptive_mask():
     vals, counts = np.unique(adaptive_mask, return_counts=True)
     assert np.allclose(vals, np.array([0, 1, 2, 3]))
     assert np.allclose(counts, np.array([14976, 1817, 4427, 43130]))
+    assert "voxels in user-defined mask do not have good signal" in caplog.text
 
     # Just decay method
     mask, adaptive_mask = utils.make_adaptive_mask(
@@ -206,6 +217,43 @@ def test_make_adaptive_mask():
     vals, counts = np.unique(adaptive_mask, return_counts=True)
     assert np.allclose(vals, np.array([0, 1, 2, 3]))
     assert np.allclose(counts, np.array([3365, 1412, 1195, 58378]))
+    assert "No methods provided for adaptive mask generation." in caplog.text
+
+    # testing n_independent_echos
+    # This should match "decay" from above, except all voxels with 3 good echoes should now have 5
+    # since two echoes were added that should not have caused more decay
+    mask, adaptive_mask = utils.make_adaptive_mask(
+        data5, mask=mask_file, threshold=1, methods=["decay"], n_independent_echos=3
+    )
+
+    assert mask.shape == adaptive_mask.shape == (64350,)
+    assert np.allclose(mask, (adaptive_mask >= 1).astype(bool))
+    assert adaptive_mask[idx] == 5
+    assert adaptive_mask[idx + 1] == 2
+    assert adaptive_mask[idx + 2] == 2
+    assert adaptive_mask[idx + 3] == 1
+    assert adaptive_mask[idx + 4] == 5
+    assert adaptive_mask[idx + 5] == 2
+    assert mask.sum() == 60985  # This method can't flag first echo as bad
+    vals, counts = np.unique(adaptive_mask, return_counts=True)
+    assert np.allclose(vals, np.array([0, 1, 2, 5]))
+    assert np.allclose(counts, np.array([3365, 4366, 5973, 50646]))
+    # 4366 + 5973 = 10399 (i.e. voxels with 1 or 2 good echoes are flagged here)
+    assert (
+        "10339 voxels (17.0%) have fewer than 3.0 good voxels. "
+        "These voxels will be used in all analyses, "
+        "but might not include 3 independent echo measurements."
+    ) in caplog.text
+
+    mask, adaptive_mask = utils.make_adaptive_mask(
+        data5, mask=mask_file, threshold=1, methods=["decay"], n_independent_echos=4
+    )
+
+    assert (
+        "10339 voxels (17.0%) have fewer than 3.0 good voxels. "
+        "The degrees of freedom for fits across echoes will remain 4 even if "
+        "there might be fewer independent echo measurements."
+    ) in caplog.text
 
 
 # SMOKE TESTS