Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions .all-contributorsrc
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,17 @@
"infra"
]
},
{
"login": "beccaclements99",
"name": "Becca Clements",
"avatar_url": "https://avatars.githubusercontent.com/u/95257798?v=4",
"profile": "https://github.com/beccaclements99",
"contributions": [
"bug",
"code",
"userTesting"
]
},
{
"login": "avigotsky",
"name": "Andrew Vigotsky",
Expand All @@ -38,6 +49,16 @@
"code"
]
},
{
"login": "ccomellalue",
"name": "ccomellalue",
"avatar_url": "https://avatars.githubusercontent.com/u/209748720?v=4",
"profile": "https://github.com/ccomellalue",
"contributions": [
"code",
"ideas"
]
},
{
"login": "merelvdthiel",
"name": "merelvdthiel",
Expand Down
35 changes: 35 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,38 @@
# 0.28.3 (Thu Mar 12 2026)

:tada: This release contains work from a new contributor! :tada:

Thank you, Becca Clements ([@beccaclements99](https://github.com/beccaclements99)), for all your work!

#### 🐛 Bug Fix

- Fix usage of previously computed lag maps to run a (new) lagged regression [#154](https://github.com/smoia/phys2cvr/pull/154) ([@beccaclements99](https://github.com/beccaclements99) [@pre-commit-ci[bot]](https://github.com/pre-commit-ci[bot]) [@smoia](https://github.com/smoia))
- Give an error message if peaks are not provided AND comp_endtidal is True [#156](https://github.com/smoia/phys2cvr/pull/156) ([@beccaclements99](https://github.com/beccaclements99))

#### ⚠️ Pushed to `master`

- tests: Reorganise tests, remove or mark xfails the failing ones ([@smoia](https://github.com/smoia))
- docs: update licenses ([@smoia](https://github.com/smoia))
- docs: Fix logos ([@smoia](https://github.com/smoia))

#### ⚠️ Tests

- Fix workflow halting on missing peaks when they are not required (i.e. when skipping end-tidal convolution) [#153](https://github.com/smoia/phys2cvr/pull/153) ([@smoia](https://github.com/smoia) [@beccaclements99](https://github.com/beccaclements99) [@pre-commit-ci[bot]](https://github.com/pre-commit-ci[bot]))

#### 🏠 Internal

- [pre-commit.ci] pre-commit autoupdate [#152](https://github.com/smoia/phys2cvr/pull/152) ([@pre-commit-ci[bot]](https://github.com/pre-commit-ci[bot]))
- Bump actions/checkout from 5 to 6 [#151](https://github.com/smoia/phys2cvr/pull/151) ([@dependabot[bot]](https://github.com/dependabot[bot]))

#### Authors: 4

- [@dependabot[bot]](https://github.com/dependabot[bot])
- [@pre-commit-ci[bot]](https://github.com/pre-commit-ci[bot])
- Becca Clements ([@beccaclements99](https://github.com/beccaclements99))
- Stefano Moia ([@smoia](https://github.com/smoia))

---

# 0.28.2 (Wed Nov 19 2025)

#### 🐛 Bug Fix
Expand Down
8 changes: 6 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ phys2cvr
[![Supports python version](https://img.shields.io/pypi/pyversions/phys2cvr?style=shield&logo=python)](https://pypi.org/project/phys2cvr/)

<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
[![All Contributors](https://img.shields.io/badge/all_contributors-5-orange.svg?style=flat)](#contributors)
[![All Contributors](https://img.shields.io/badge/all_contributors-7-orange.svg?style=flat)](#contributors)
<!-- ALL-CONTRIBUTORS-BADGE:END -->

A python-based tool to generate regressor for and/or estimate CVR maps and their lag.
Expand Down Expand Up @@ -92,7 +92,11 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
<tr>
<td align="center" valign="top" width="20%"><a href="https://github.com/smoia"><img src="https://avatars3.githubusercontent.com/u/35300580?v=4?s=100" width="100px;" alt="Stefano Moia"/><br /><sub><b>Stefano Moia</b></sub></a><br /><a href="https://github.com/smoia/phys2cvr/commits?author=smoia" title="Code">💻</a> <a href="#ideas-smoia" title="Ideas, Planning, & Feedback">🤔</a> <a href="#infra-smoia" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a> <a href="#projectManagement-smoia" title="Project Management">📆</a></td>
<td align="center" valign="top" width="20%"><a href="https://github.com/kristinazvolanek"><img src="https://avatars3.githubusercontent.com/u/54590158?v=4?s=100" width="100px;" alt="Kristina Zvolanek"/><br /><sub><b>Kristina Zvolanek</b></sub></a><br /><a href="https://github.com/smoia/phys2cvr/commits?author=kristinazvolanek" title="Code">💻</a> <a href="https://github.com/smoia/phys2cvr/issues?q=author%3Akristinazvolanek" title="Bug reports">🐛</a> <a href="#infra-kristinazvolanek" title="Infrastructure (Hosting, Build-Tools, etc)">🚇</a></td>
<td align="center" valign="top" width="20%"><a href="https://github.com/beccaclements99"><img src="https://avatars.githubusercontent.com/u/95257798?v=4?s=100" width="100px;" alt="Becca Clements"/><br /><sub><b>Becca Clements</b></sub></a><br /><a href="https://github.com/smoia/phys2cvr/issues?q=author%3Abeccaclements99" title="Bug reports">🐛</a> <a href="https://github.com/smoia/phys2cvr/commits?author=beccaclements99" title="Code">💻</a> <a href="#userTesting-beccaclements99" title="User Testing">📓</a></td>
<td align="center" valign="top" width="20%"><a href="https://github.com/avigotsky"><img src="https://avatars.githubusercontent.com/u/904218?v=4?s=100" width="100px;" alt="Andrew Vigotsky"/><br /><sub><b>Andrew Vigotsky</b></sub></a><br /><a href="https://github.com/smoia/phys2cvr/commits?author=avigotsky" title="Code">💻</a></td>
<td align="center" valign="top" width="20%"><a href="https://github.com/ccomellalue"><img src="https://avatars.githubusercontent.com/u/209748720?v=4?s=100" width="100px;" alt="ccomellalue"/><br /><sub><b>ccomellalue</b></sub></a><br /><a href="https://github.com/smoia/phys2cvr/commits?author=ccomellalue" title="Code">💻</a> <a href="#ideas-ccomellalue" title="Ideas, Planning, & Feedback">🤔</a></td>
</tr>
<tr>
<td align="center" valign="top" width="20%"><a href="https://github.com/merelvdthiel"><img src="https://avatars.githubusercontent.com/u/72999546?v=4?s=100" width="100px;" alt="merelvdthiel"/><br /><sub><b>merelvdthiel</b></sub></a><br /><a href="https://github.com/smoia/phys2cvr/commits?author=merelvdthiel" title="Documentation">📖</a></td>
<td align="center" valign="top" width="20%"><a href="https://github.com/RazkinMalen"><img src="https://avatars.githubusercontent.com/u/147508933?v=4?s=100" width="100px;" alt="razkin"/><br /><sub><b>razkin</b></sub></a><br /><a href="#design-RazkinMalen" title="Design">🎨</a> <a href="https://github.com/smoia/phys2cvr/commits?author=RazkinMalen" title="Documentation">📖</a> <a href="https://github.com/smoia/phys2cvr/commits?author=RazkinMalen" title="Tests">⚠️</a></td>
</tr>
Expand All @@ -108,7 +112,7 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d
License
-------

Copyright 2021-2025, Stefano Moia & phys2cvr contributors.
Copyright 2021-2026, Stefano Moia & phys2cvr contributors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down
2 changes: 1 addition & 1 deletion phys2cvr/cli/run.py
Original file line number Diff line number Diff line change
Expand Up @@ -701,7 +701,7 @@ def _check_opt_conf(parser):


"""
Copyright 2021-2025, Stefano Moia & phys2cvr contributors.
Copyright 2021-2026, Stefano Moia & phys2cvr contributors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down
18 changes: 10 additions & 8 deletions phys2cvr/io.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ def load_txt(fname, shape=None):
'.txt': ' ',
'.1d': ' ',
'.par': ' ',
'': ' ',
}

mtx = np.genfromtxt(fname, delimiter=delimiter_map.get(ext))
Expand Down Expand Up @@ -235,10 +236,11 @@ def load_array(fname, shape=''):
"""
_, _, ext = utils.check_ext(EXT_ARRAY, fname, scan=True, remove=True)

if ext.lower() in EXT_1D:
return load_txt(fname, shape=shape)
if ext.lower() in EXT_MAT:
return load_mat(fname, shape=shape)
if ext:
if ext.lower() in EXT_1D:
return load_txt(fname, shape=shape)
if ext.lower() in EXT_MAT:
return load_mat(fname, shape=shape)

raise NotImplementedError(
f'{fname} file extension {ext} was not found or is not supported yet'
Expand Down Expand Up @@ -268,7 +270,7 @@ def load_physio(fname):


def export_regressor(
regressors_matrix, ntp, outname, suffix='petco2hrf', ext='.1D', axis=-1
regressors_matrix, ntp, outprefix, suffix='petco2hrf', ext='.1D', axis=-1
):
"""
Export generated regressors for fMRI analysis.
Expand All @@ -279,7 +281,7 @@ def export_regressor(
The regressors that needs to be exported, in its original sample
ntp : int
The number of fMRI timepoints
outname : str or path
outprefix : str or path
Prefix of the output file - can contain a path.
suffix : str, optional
The suffix of the output file.
Expand All @@ -301,7 +303,7 @@ def export_regressor(
regressors_demeaned = regressors_matrix - regressors_matrix.mean(
axis=axis, keepdims=True
)
np.savetxt(f'{outname}_{suffix}{ext}', regressors_demeaned, fmt='%.6f')
np.savetxt(f'{outprefix}_{suffix}{ext}', regressors_demeaned, fmt='%.6f')
return regressors_demeaned


Expand All @@ -324,7 +326,7 @@ def export_nifti(data, img, fname):


"""
Copyright 2021-2025, Stefano Moia & phys2cvr contributors.
Copyright 2021-2026, Stefano Moia & phys2cvr contributors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down
8 changes: 6 additions & 2 deletions phys2cvr/regressors.py
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ def compute_petco2hrf(
'Arrays with more than 2 dimensions are not supported.'
)

if comp_endtidal:
if comp_endtidal and pidx is not None:
petco2 = endtidal_interpolation(co2, pidx, axis=-1)

# Plot PetCO2 vs CO2
Expand All @@ -111,6 +111,10 @@ def compute_petco2hrf(
# Demean and export
petco2 = petco2 - petco2.mean()
np.savetxt(f'{outprefix}_petco2.1D', petco2, fmt='%.18f')
elif comp_endtidal and pidx is None:
raise ValueError(
'End-tidal interpolation was requested, but peaks were not provided.'
)
else:
LGR.info(
'Skipping End Tidal interpolation of PetCO2 trace (if you provided raw CO2 '
Expand Down Expand Up @@ -412,7 +416,7 @@ def create_physio_regressor(


"""
Copyright 2021-2025, Stefano Moia & phys2cvr contributors.
Copyright 2021-2026, Stefano Moia & phys2cvr contributors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down
4 changes: 2 additions & 2 deletions phys2cvr/signal.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ def create_hrf(freq=40):
u = np.arange(0, p[6] / dt + 1, 1) - p[5] / dt

a1 = p[0] / p[2]
b1 = 1 / p[3]
b1 = 1 / p[2]
a2 = p[1] / p[3]
b2 = 1 / p[3]

Expand Down Expand Up @@ -325,7 +325,7 @@ def resample_signal_freqs(ts, freq1, freq2, axis=-1):


"""
Copyright 2021-2025, Stefano Moia & phys2cvr contributors.
Copyright 2021-2026, Stefano Moia & phys2cvr contributors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down
2 changes: 1 addition & 1 deletion phys2cvr/stats.py
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,7 @@ def regression(


"""
Copyright 2021-2025, Stefano Moia & phys2cvr contributors.
Copyright 2021-2026, Stefano Moia & phys2cvr contributors.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
Expand Down
110 changes: 108 additions & 2 deletions phys2cvr/tests/test_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,14 @@
"""Tests for io."""

import os
import sys

import nibabel as nib
import numpy as np
import peakdet as pk
import pymatreader
import pytest
from scipy.io import savemat

from phys2cvr import io

Expand All @@ -27,8 +32,78 @@ def test_load_nifti_get_mask(nifti_data, nifti_mask):
assert isinstance(img, nib.nifti1.Nifti1Image)


@pytest.mark.parametrize(
'extension, delimiter',
[
['.csv', ','],
['.csv.gz', ','],
['.tsv', '\t'],
['.tsv.gz', '\t'],
['.txt', ' '],
['.1d', ' '],
['.par', ' '],
['', ' '],
],
)
def test_load_txt(extension, delimiter, testdir):
"""Test load_txt."""
a = np.random.randn(5)
n = os.join.path(testdir, f'zoe{extension}')
np.savetxt(n, a, delimiter=delimiter)
b = io.load_txt(n)

assert (a == b).all()
os.remove(n)


def test_load_mat():
"""Test load_mat."""
a = np.random.randn(5)
b = 'likealeaf'
n = 'inthewind'

savemat(n, {'data': a, 'other': b})

c = io.load_mat(n)

assert (a == c).all()
os.remove(n)


def test_load_array():
"""Test load_mat."""
a = np.random.randn(5)
b = 'likealeaf'
n = 'inthewind.mat'
m = 'zoe.txt'
np.savetxt(m, a)

savemat(n, {'data': a, 'other': b})

d = io.load_array(n)
e = io.load_array(m)

assert (a == d).all()
assert (a == e).all()
os.remove(n)
os.remove(m)


def test_load_physio():
d = pk.Physio(np.random.randn(40), fs=10)
d = pk.peakfind_physio(d)
n = 'kaylee.phys'
pk.save_physio(n, d)
p = io.load_physio(n)

assert (p[0] == d.data).all()
assert (p[1] == d.peaks).all()
assert p[2] == 10
os.remove(n)


def test_export_regressor(testdir):
petco2hrf_shift = np.random.rand(10)
petco2hrf_shift = np.random.randn(10)
ntp = 10
outname = os.path.join(testdir, 'test_regressor')
suffix = 'petco2hrf'
Expand All @@ -49,7 +124,7 @@ def test_export_regressor(testdir):

def test_export_nifti(testdir):
# create some test data
data = np.random.rand(10, 10, 10)
data = np.random.randn(10, 10, 10)
affine = np.eye(4)
header = nib.Nifti1Header()
img = nib.Nifti1Image(data, affine, header)
Expand All @@ -66,8 +141,39 @@ def test_export_nifti(testdir):
assert np.allclose(out_img.get_fdata(), data, atol=1e-6, rtol=0)
assert np.allclose(out_img.affine, affine, atol=1e-6, rtol=0)
assert out_img.header.__dict__.keys() == header.__dict__.keys()
os.remove(fname)
# Eventually check that headers have the same content (although they don't now
# for good reasons!)


# ## Break tests
def test_break_load_mat():
"""Break load_mat."""
sys.modules['pymatreader'] = None
with pytest.raises(ImportError) as errorinfo:
io.load_mat('simon')
assert 'is required' in str(errorinfo.value)
sys.modules['pymatreader'] = pymatreader

a = 'heart'
n = 'ofgold'
savemat(n, {'data': a})

with pytest.raises(EOFError) as errorinfo:
io.load_mat(n)
assert f'{n} does not seem' in str(errorinfo.value)
os.remove(n)


def test_break_load_xls():
"""Break load_xls."""
with pytest.raises(NotImplementedError) as errorinfo:
io.load_xls('firefly')
assert 'loading is not' in str(errorinfo.value)


def test_break_load_array():
"""Break load_xls."""
with pytest.raises(NotImplementedError) as errorinfo:
io.load_array('firefly.leaf')
assert 'file extension' in str(errorinfo.value)
Loading