Skip to content

Commit 421d868

Browse files
shnizzedybirajstha
andcommitted
♻️ Skip header when loading TSV into NumPy
Ref #2228 (comment), #2228 (comment) Co-authored-by: Biraj Shrestha <[email protected]>
1 parent 1a16e41 commit 421d868

File tree

4 files changed

+52
-7
lines changed

4 files changed

+52
-7
lines changed

CPAC/nuisance/nuisance.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright (C) 2012-2024 C-PAC Developers
1+
# Copyright (C) 2012-2025 C-PAC Developers
22

33
# This file is part of C-PAC.
44

@@ -14,6 +14,8 @@
1414

1515
# You should have received a copy of the GNU Lesser General Public
1616
# License along with C-PAC. If not, see <https://www.gnu.org/licenses/>.
17+
"""Nusiance regression."""
18+
1719
import os
1820
from typing import Literal
1921

@@ -29,6 +31,7 @@
2931
from CPAC.nuisance.utils import (
3032
find_offending_time_points,
3133
generate_summarize_tissue_mask,
34+
load_censor_tsv,
3235
temporal_variance_mask,
3336
)
3437
from CPAC.nuisance.utils.compcor import (
@@ -302,7 +305,7 @@ def gather_nuisance(
302305
raise ValueError(msg)
303306

304307
try:
305-
regressors = np.loadtxt(regressor_file)
308+
regressors = load_censor_tsv(regressor_file, regressor_length)
306309
except (OSError, TypeError, UnicodeDecodeError, ValueError) as error:
307310
msg = f"Could not read regressor {regressor_type} from {regressor_file}."
308311
raise OSError(msg) from error
@@ -382,7 +385,7 @@ def gather_nuisance(
382385
if custom_file_paths:
383386
for custom_file_path in custom_file_paths:
384387
try:
385-
custom_regressor = np.loadtxt(custom_file_path)
388+
custom_regressor = load_censor_tsv(custom_file_path, regressor_length)
386389
except:
387390
msg = "Could not read regressor {0} from {1}.".format(
388391
"Custom", custom_file_path
@@ -421,7 +424,7 @@ def gather_nuisance(
421424
censor_volumes = np.ones((regressor_length,), dtype=int)
422425
else:
423426
try:
424-
censor_volumes = np.loadtxt(regressor_file)
427+
censor_volumes = load_censor_tsv(regressor_file, regressor_length)
425428
except:
426429
msg = (
427430
f"Could not read regressor {regressor_type} from {regressor_file}."

CPAC/nuisance/tests/test_utils.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,18 @@
1818

1919
from importlib.resources import as_file, files
2020
import os
21+
from pathlib import Path
22+
from random import randint
2123
import tempfile
2224

2325
import numpy as np
2426
import pytest
2527

26-
from CPAC.nuisance.utils import calc_compcor_components, find_offending_time_points
28+
from CPAC.nuisance.utils import (
29+
calc_compcor_components,
30+
find_offending_time_points,
31+
load_censor_tsv,
32+
)
2733
from CPAC.utils.monitoring.custom_logging import getLogger
2834

2935
logger = getLogger("CPAC.nuisance.tests")
@@ -58,3 +64,20 @@ def test_calc_compcor_components():
5864
compcor_filename = calc_compcor_components(data_filename, 5, mask_filename)
5965
logger.info("compcor components written to %s", compcor_filename)
6066

67+
68+
@pytest.mark.parametrize("header", [True, False])
69+
def test_load_censor_tsv(header: bool, tmp_path: Path) -> None:
70+
"""Test loading of censor tsv files with and without headers."""
71+
expected_length = 3
72+
filepath = tmp_path / "censor.tsv"
73+
with filepath.open("w") as f:
74+
if header:
75+
f.write("censor\n")
76+
for i in range(expected_length):
77+
f.write(f"{randint(0, 1)}\n")
78+
censors = load_censor_tsv(str(filepath), expected_length)
79+
assert (
80+
censors.shape[0] == expected_length
81+
), "Length of censors does not match expected length"
82+
with pytest.raises(ValueError, match="expected length"):
83+
load_censor_tsv(str(filepath), expected_length + 1)

CPAC/nuisance/utils/__init__.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright (C) 2019-2024 C-PAC Developers
1+
# Copyright (C) 2019-2025 C-PAC Developers
22

33
# This file is part of C-PAC.
44

@@ -21,6 +21,7 @@
2121
from .utils import (
2222
find_offending_time_points,
2323
generate_summarize_tissue_mask,
24+
load_censor_tsv,
2425
NuisanceRegressor,
2526
temporal_variance_mask,
2627
)
@@ -30,6 +31,7 @@
3031
"compcor",
3132
"find_offending_time_points",
3233
"generate_summarize_tissue_mask",
34+
"load_censor_tsv",
3335
"NuisanceRegressor",
3436
"temporal_variance_mask",
3537
]

CPAC/nuisance/utils/utils.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright (C) 2019-2024 C-PAC Developers
1+
# Copyright (C) 2019-2025 C-PAC Developers
22

33
# This file is part of C-PAC.
44

@@ -21,6 +21,7 @@
2121
import re
2222
from typing import Optional
2323

24+
import numpy as np
2425
from nipype.interfaces import afni, ants, fsl
2526
import nipype.interfaces.utility as util
2627
from nipype.pipeline.engine import Workflow
@@ -860,3 +861,19 @@ def encode(selector: dict) -> str:
860861
def __repr__(self) -> str:
861862
"""Return a string representation of the nuisance regressor."""
862863
return NuisanceRegressor.encode(self.selector)
864+
865+
866+
def load_censor_tsv(filepath: str, expected_length: int) -> np.ndarray:
867+
"""Load censor TSV and verify length."""
868+
header = False
869+
censor = np.empty((0))
870+
try:
871+
censor = np.loadtxt(filepath)
872+
except ValueError:
873+
header = True
874+
if header or censor.shape[0] == expected_length + 1:
875+
censor = np.loadtxt(filepath, skiprows=1)
876+
if censor.shape[0] == expected_length:
877+
return censor
878+
msg = f"Censor file length ({censor.shape[0]}) does not match expected length ({expected_length})."
879+
raise ValueError(msg)

0 commit comments

Comments
 (0)