From b3709cab2a4e6a97a318053e55d8342e3516da84 Mon Sep 17 00:00:00 2001 From: Casper da Costa-Luis Date: Fri, 3 Feb 2023 13:45:45 +0000 Subject: [PATCH 01/18] fix matlab.engine incompatibility with functools.lru_cache --- spm12/utils.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/spm12/utils.py b/spm12/utils.py index f499994..6d22e8c 100644 --- a/spm12/utils.py +++ b/spm12/utils.py @@ -1,5 +1,5 @@ import logging -from functools import lru_cache, wraps +from functools import wraps from os import path from textwrap import dedent @@ -13,7 +13,6 @@ log = logging.getLogger(__name__) -@lru_cache() def get_matlab(name=None): eng = get_engine(name=name) log.debug("adding wrappers (%s) to MATLAB path", PATH_M) @@ -39,7 +38,6 @@ def spm_dir_eng(name=None, cache="~/.spm12", version=12): return path.dirname(eng.which("spm_jobman")) -@lru_cache() @wraps(get_matlab) def ensure_spm(name=None, cache="~/.spm12", version=12): eng = get_matlab(name) From 8d45f28847ccad8471653fbeca0d789724f9a083 Mon Sep 17 00:00:00 2001 From: Casper da Costa-Luis Date: Fri, 10 Mar 2023 22:09:31 +0000 Subject: [PATCH 02/18] drop py3.6, misc tidy --- .gitignore | 4 ++-- pyproject.toml | 2 +- setup.cfg | 20 +++++++------------- spm12/cli.py | 2 +- spm12/regseg.py | 25 +++++++++---------------- tests/test_regseg.py | 16 +++------------- 6 files changed, 23 insertions(+), 46 deletions(-) diff --git a/.gitignore b/.gitignore index 10a4f13..b6ac364 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,9 @@ *.py[cod] __pycache__/ /spm12/_dist_ver.py -/.eggs/ -/*.egg-info/ +/*.egg*/ /build/ /dist/ /.coverage* /coverage.xml +/.pytest_cache/ diff --git a/pyproject.toml b/pyproject.toml index 46d2769..a1ff7c7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,5 +7,5 @@ write_to = "spm12/_dist_ver.py" write_to_template = "__version__ = '{version}'\n" [tool.black] -target_version = ["py36", "py310"] +target_version = ["py37", "py311"] line_length = 99 diff --git a/setup.cfg b/setup.cfg index 925941b..1181622 100644 --- a/setup.cfg +++ b/setup.cfg @@ -3,12 +3,12 @@ name=spm12 description=Statistical Parametric Mapping long_description=file: README.rst long_description_content_type=text/x-rst -license=Apache 2.0 +license=Apache-2.0 license_file=LICENCE.md url=https://github.com/AMYPAD/SPM12 project_urls= - Changelog = https://github.com/AMYPAD/SPM12/releases - Documentation = https://github.com/AMYPAD/SPM12/#SPM12 + Changelog=https://github.com/AMYPAD/SPM12/releases + Documentation=https://github.com/AMYPAD/SPM12/#SPM12 Upstream Project=https://www.fil.ion.ucl.ac.uk/spm maintainer=Casper da Costa-Luis maintainer_email=casper.dcl@physics.org @@ -26,17 +26,17 @@ classifiers= Operating System :: POSIX :: Linux Programming Language :: Other Scripting Engines Programming Language :: Python :: 3 - Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 + Programming Language :: Python :: 3.11 Programming Language :: Python :: 3 :: Only Topic :: Scientific/Engineering :: Medical Science Apps. Topic :: Software Development :: Libraries Topic :: System :: Installation/Setup [options] -zip_safe = False +zip_safe=False setup_requires=setuptools>=42; wheel; setuptools_scm[toml]>=3.4 install_requires= argopt @@ -46,17 +46,13 @@ install_requires= scipy include_package_data=True packages=find: -python_requires=>=3.6 +python_requires=>=3.7 [options.extras_require] dev= - pre-commit - twine - wheel pytest pytest-cov pytest-timeout pytest-xdist - codecov demo= miutil[plot]>=0.3.0 matplotlib @@ -68,9 +64,6 @@ exclude=tests [options.package_data] *=*.md, *.rst, *.m -[bdist_wheel] -universal = 1 - [flake8] max_line_length=99 extend_ignore=E203,P1 @@ -79,6 +72,7 @@ exclude=.git,__pycache__,build,dist,.eggs [isort] profile=black line_length=99 +multi_line_output=4 known_first_party=spm12,tests [tool:pytest] diff --git a/spm12/cli.py b/spm12/cli.py index 5d3168d..d13e870 100644 --- a/spm12/cli.py +++ b/spm12/cli.py @@ -16,5 +16,5 @@ def main(argv=None): logging.basicConfig(level=logging.DEBUG, format="%(levelname)s:%(funcName)s:%(message)s") args = argopt(__doc__).parse_args(argv) ensure_spm(cache=args.cache, version=args.spm_version) - print("SPM{v} is successfully installed".format(v=args.spm_version)) + print(f"SPM{args.spm_version} is successfully installed") return 0 diff --git a/spm12/regseg.py b/spm12/regseg.py index 398526c..fa25583 100644 --- a/spm12/regseg.py +++ b/spm12/regseg.py @@ -47,7 +47,7 @@ def smoothim(fim, fwhm=4, fout=""): ) if not fout: f = nii.file_parts(fim) - fout = os.path.join(f[0], "{}_smo{}{}".format(f[1], str(fwhm).replace(".", "-"), f[2])) + fout = os.path.join(f[0], f"{f[1]}_smo{str(fwhm).replace('.', '-')}{f[2]}") nii.array2nii( imsmo, imd["affine"], @@ -173,10 +173,7 @@ def coreg_spm( # delete the previous version (non-smoothed) os.remove(imrefu) imrefu = smodct["fim"] - - log.info( - "smoothed the reference image with FWHM={} and saved to\n{}".format(fwhm_ref, imrefu) - ) + log.info("smoothed the reference image with FWHM=%r and saved to\n%r", fwhm_ref, imrefu) # floating if hasext(imflo, "gz"): @@ -197,9 +194,7 @@ def coreg_spm( imflou = smodct["fim"] - log.info( - "smoothed the floating image with FWHM={} and saved to\n{}".format(fwhm_flo, imflou) - ) + log.info("smoothed the floating image with FWHM=%r and saved to\n%r", fwhm_flo, imflou) # run the MATLAB SPM registration import matlab as ml @@ -296,10 +291,12 @@ def resample_spm( """\ ====================================================================== S P M inputs: - > ref:' {} - > flo:' {} + > ref:' %r + > flo:' %r ======================================================================""" - ).format(imref, imflo) + ), + imref, + imflo, ) eng = ensure_spm(matlab_eng_name) # get_matlab @@ -385,11 +382,7 @@ def resample_spm( if fwhm > 0: smodct = smoothim(fout, fwhm) - log.info( - "smoothed the resampled image with FWHM={} and saved to\n{}".format( - fwhm, smodct["fim"] - ) - ) + log.info("smoothed the resampled image with FWHM=%r and saved to\n%r", fwhm, smodct["fim"]) return fout diff --git a/tests/test_regseg.py b/tests/test_regseg.py index 6aa6714..948037b 100644 --- a/tests/test_regseg.py +++ b/tests/test_regseg.py @@ -28,20 +28,10 @@ def assert_equal_arrays(x, y, nmse_tol=0, denan=True): return raise ValueError( dedent( - """\ - Unequal arrays:x != y. min/mean/max(std): - x: {:.3g}/{:.3g}/{:.3g}({:.3g}) - y: {:.3g}/{:.3g}/{:.3g}({:.3g}) + f""" Unequal arrays:x != y. min/mean/max(std): + x: {x.min():.3g}/{x.mean():.3g}/{x.max():.3g}({x.std():.3g}) + y: {y.min():.3g}/{y.mean():.3g}/{y.max():.3g}({y.std():.3g}) """ - ).format( - x.min(), - x.mean(), - x.max(), - x.std(), - y.min(), - y.mean(), - y.max(), - y.std(), ) ) From 700a9ba05dc5911b810e212219ad656a2e0fffd9 Mon Sep 17 00:00:00 2001 From: Casper da Costa-Luis Date: Fri, 10 Mar 2023 22:09:54 +0000 Subject: [PATCH 03/18] ci: update workflows --- .github/workflows/test.yml | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a70e5bc..d281833 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,10 +1,12 @@ name: Test -on: [push, pull_request] +on: + push: + pull_request: + schedule: [{cron: '10 23 * * 6'}] # M H d m w (Sat at 23:10) jobs: check: if: github.event_name != 'pull_request' || !contains('OWNER,MEMBER,COLLABORATOR', github.event.pull_request.author_association) runs-on: ubuntu-latest - name: Check steps: - uses: actions/checkout@v3 - uses: actions/setup-python@v4 @@ -19,7 +21,7 @@ jobs: - name: dependencies run: pip install -U pre-commit - uses: reviewdog/action-setup@v1 - - if: github.event_name != 'schedule' + - if: github.event_name == 'push' || github.event_name == 'pull_request' name: comment run: | if [[ $EVENT == pull_request ]]; then @@ -35,11 +37,11 @@ jobs: - run: pre-commit run -a --show-diff-on-failure test: if: github.event_name != 'pull_request' || !contains('OWNER,MEMBER,COLLABORATOR', github.event.pull_request.author_association) + name: py${{ matrix.python }} runs-on: ubuntu-latest strategy: matrix: - python: [3.7, '3.10'] - name: Test py${{ matrix.python }} + python: [3.7, 3.11] steps: - uses: actions/checkout@v3 with: @@ -49,22 +51,25 @@ jobs: python-version: ${{ matrix.python }} - run: pip install -U .[dev] - run: pytest --durations-min=1 - - run: codecov + - uses: codecov/codecov-action@v3 matlab: if: github.event_name != 'pull_request' || !contains('OWNER,MEMBER,COLLABORATOR', github.event.pull_request.author_association) + name: MATLAB py${{ matrix.python }} runs-on: [self-hosted, python, matlab] - name: Test matlab + strategy: + matrix: + python: [3.7, 3.8] steps: - uses: actions/checkout@v3 with: fetch-depth: 0 - name: Run setup-python - run: setup-python -p3.7 + run: setup-python -p${{ matrix.python }} - run: pip install -U .[dev] - run: pytest --durations-min=1 - - run: codecov + - uses: codecov/codecov-action@v3 - name: Post Run setup-python - run: setup-python -p3.7 -Dr + run: setup-python -p${{ matrix.python }} -Dr if: ${{ always() }} deploy: needs: [check, test, matlab] @@ -75,6 +80,8 @@ jobs: with: fetch-depth: 0 - uses: actions/setup-python@v4 + with: + python-version: '3.x' - id: dist uses: casperdcl/deploy-pypi@v2 with: @@ -83,10 +90,10 @@ jobs: password: ${{ secrets.PYPI_TOKEN }} upload: ${{ github.event_name == 'push' && startsWith(github.ref, 'refs/tags') }} - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') - name: release + name: Release run: | changelog=$(git log --pretty='format:%d%n- %s%n%b---' $(git tag --sort=v:refname | tail -n2 | head -n1)..HEAD) tag="${GITHUB_REF#refs/tags/}" gh release create --title "spm12 $tag beta" --draft --notes "$changelog" "$tag" dist/${{ steps.dist.outputs.whl }} dist/${{ steps.dist.outputs.whl_asc }} env: - GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} + GH_TOKEN: ${{ github.token }} From bb529cac49a58bba63d18d92b1e9bdde24dfaa7e Mon Sep 17 00:00:00 2001 From: Casper da Costa-Luis Date: Fri, 10 Mar 2023 22:10:40 +0000 Subject: [PATCH 04/18] reintroduce lru_cache - partially reverts b3709ca --- spm12/utils.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/spm12/utils.py b/spm12/utils.py index 6d22e8c..3592a12 100644 --- a/spm12/utils.py +++ b/spm12/utils.py @@ -1,5 +1,5 @@ import logging -from functools import wraps +from functools import lru_cache, wraps from os import path from textwrap import dedent @@ -38,6 +38,7 @@ def spm_dir_eng(name=None, cache="~/.spm12", version=12): return path.dirname(eng.which("spm_jobman")) +@lru_cache() @wraps(get_matlab) def ensure_spm(name=None, cache="~/.spm12", version=12): eng = get_matlab(name) From e0241cd611dbedf34f581310a8d8f6925dc8b2c3 Mon Sep 17 00:00:00 2001 From: Casper da Costa-Luis Date: Fri, 10 Mar 2023 22:40:41 +0000 Subject: [PATCH 05/18] replace pkg_resources => importlib.resources --- setup.cfg | 2 +- spm12/utils.py | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/setup.cfg b/setup.cfg index 1181622..c90f337 100644 --- a/setup.cfg +++ b/setup.cfg @@ -40,8 +40,8 @@ zip_safe=False setup_requires=setuptools>=42; wheel; setuptools_scm[toml]>=3.4 install_requires= argopt + importlib_resources; python_version < "3.9" miutil[nii,web]>=0.7.2 - setuptools # pkg_resources numpy scipy include_package_data=True diff --git a/spm12/utils.py b/spm12/utils.py index 3592a12..822d6e9 100644 --- a/spm12/utils.py +++ b/spm12/utils.py @@ -1,15 +1,19 @@ import logging from functools import lru_cache, wraps -from os import path +from os import fspath, path from textwrap import dedent from miutil.fdio import extractall from miutil.mlab import get_engine from miutil.web import urlopen_cached -from pkg_resources import resource_filename + +try: # py<3.9 + import importlib_resources as resources +except ImportError: + from importlib import resources __all__ = ["ensure_spm", "get_matlab", "spm_dir", "spm_dir_eng"] -PATH_M = resource_filename(__name__, "") +PATH_M = fspath(resources.files("spm12").resolve()) log = logging.getLogger(__name__) From c45b5a08aaa64c4b6568da33f6d9f4addf36846c Mon Sep 17 00:00:00 2001 From: Pawel Date: Mon, 27 Mar 2023 00:26:25 +0100 Subject: [PATCH 06/18] changed vox size defiinition in spm normalisation writing --- spm12/amypad_normw.m | 2 +- spm12/regseg.py | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/spm12/amypad_normw.m b/spm12/amypad_normw.m index 60946e8..07f7def 100644 --- a/spm12/amypad_normw.m +++ b/spm12/amypad_normw.m @@ -3,7 +3,7 @@ job.subj.resample = flist4norm; %job.woptions.bb = [NaN, NaN, NaN; NaN, NaN, NaN]; job.woptions.bb = bbox; - job.woptions.vox = [voxsz, voxsz, voxsz]; + job.woptions.vox = voxsz; %[voxsz, voxsz, voxsz]; job.woptions.interp = intrp; job.woptions.prefix = 'w'; spm_run_norm(job); diff --git a/spm12/regseg.py b/spm12/regseg.py index fa25583..e51299e 100644 --- a/spm12/regseg.py +++ b/spm12/regseg.py @@ -472,6 +472,23 @@ def normw_spm(f_def, files4norm, voxsz=2, intrp=4, bbox=None, matlab_eng_name="" else: raise ValueError("unrecognised format for bounding box") + + if isinstance(voxsz, (float, int, np.float32, np.float64, np.int32)): + voxsz = ml.double( [voxsz,voxsz,voxsz] ) + elif isinstance(voxsz, np.ndarray): + if voxsz.size!=3: + raise ValueError('incorrect vector size for voxel size ') + else: + voxsz = ml.double(list(voxsz)) + elif isinstance(voxsz, list): + if len(voxsz)!=3: + raise ValueError('incorrect list size for voxel size ') + else: + voxsz = ml.double(voxsz) + else: + raise ValueError('unrecognised voxel size ') + + eng = ensure_spm(matlab_eng_name) # get_matlab eng.amypad_normw(f_def, files4norm, float(voxsz), float(intrp), bb) out = [] # output list From a6e19653d2d82ecad623de78123cda2b47f6a655 Mon Sep 17 00:00:00 2001 From: Pawel Date: Mon, 27 Mar 2023 00:37:38 +0100 Subject: [PATCH 07/18] minor bug fix --- spm12/regseg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spm12/regseg.py b/spm12/regseg.py index e51299e..a2afd82 100644 --- a/spm12/regseg.py +++ b/spm12/regseg.py @@ -490,7 +490,7 @@ def normw_spm(f_def, files4norm, voxsz=2, intrp=4, bbox=None, matlab_eng_name="" eng = ensure_spm(matlab_eng_name) # get_matlab - eng.amypad_normw(f_def, files4norm, float(voxsz), float(intrp), bb) + eng.amypad_normw(f_def, files4norm, voxsz, float(intrp), bb) out = [] # output list if outpath is not None: create_dir(outpath) From e1999b4c4e4864a290f5b3e838a59c916e26f89d Mon Sep 17 00:00:00 2001 From: Casper da Costa-Luis Date: Tue, 28 Mar 2023 19:44:25 +0100 Subject: [PATCH 08/18] minor tidy --- spm12/regseg.py | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/spm12/regseg.py b/spm12/regseg.py index a2afd82..0081cf1 100644 --- a/spm12/regseg.py +++ b/spm12/regseg.py @@ -3,6 +3,7 @@ import os import re import shutil +from numbers import Number from pathlib import PurePath from textwrap import dedent @@ -472,23 +473,16 @@ def normw_spm(f_def, files4norm, voxsz=2, intrp=4, bbox=None, matlab_eng_name="" else: raise ValueError("unrecognised format for bounding box") - - if isinstance(voxsz, (float, int, np.float32, np.float64, np.int32)): - voxsz = ml.double( [voxsz,voxsz,voxsz] ) - elif isinstance(voxsz, np.ndarray): - if voxsz.size!=3: - raise ValueError('incorrect vector size for voxel size ') - else: - voxsz = ml.double(list(voxsz)) - elif isinstance(voxsz, list): - if len(voxsz)!=3: - raise ValueError('incorrect list size for voxel size ') - else: - voxsz = ml.double(voxsz) + if isinstance(voxsz, Number): + voxsz = ml.double([voxsz]*3) + elif isinstance(voxsz, (np.ndarray, list)): + if len(voxsz) != 3: + raise ValueError(f"voxel size ({voxsz}) should be scalar or 3-vector") + voxsz = ml.double(list(voxsz)) else: - raise ValueError('unrecognised voxel size ') + raise ValueError(f"voxel size ({voxsz}) should be scalar or 3-vector") + - eng = ensure_spm(matlab_eng_name) # get_matlab eng.amypad_normw(f_def, files4norm, voxsz, float(intrp), bb) out = [] # output list From d8b33547406ccf7972ea8f5f4ccc659827d0d96d Mon Sep 17 00:00:00 2001 From: Casper da Costa-Luis Date: Tue, 28 Mar 2023 19:49:23 +0100 Subject: [PATCH 09/18] pre-commit: replace black => yapf --- .pre-commit-config.yaml | 8 +++++--- pyproject.toml | 4 ---- setup.cfg | 14 +++++++++++++- spm12/__main__.py | 2 +- 4 files changed, 19 insertions(+), 9 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 17e0773..fb0c654 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -37,10 +37,12 @@ repos: - flake8-debugger - flake8-isort - flake8-string-format -- repo: https://github.com/psf/black - rev: 22.12.0 +- repo: https://github.com/google/yapf + rev: v0.32.0 hooks: - - id: black + - id: yapf + args: [-i] + additional_dependencies: [toml] - repo: https://github.com/PyCQA/isort rev: 5.12.0 hooks: diff --git a/pyproject.toml b/pyproject.toml index a1ff7c7..f493c01 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,3 @@ build-backend = "setuptools.build_meta" [tool.setuptools_scm] write_to = "spm12/_dist_ver.py" write_to_template = "__version__ = '{version}'\n" - -[tool.black] -target_version = ["py37", "py311"] -line_length = 99 diff --git a/setup.cfg b/setup.cfg index c90f337..23eeaa5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -66,9 +66,21 @@ exclude=tests [flake8] max_line_length=99 -extend_ignore=E203,P1 +extend_ignore=E261 exclude=.git,__pycache__,build,dist,.eggs +[yapf] +spaces_before_comment=15, 20 +arithmetic_precedence_indication=true +allow_split_before_dict_value=false +coalesce_brackets=True +column_limit=99 +each_dict_entry_on_separate_line=False +space_between_ending_comma_and_closing_bracket=False +split_before_named_assigns=False +split_before_closing_bracket=False +blank_line_before_nested_class_or_def=False + [isort] profile=black line_length=99 diff --git a/spm12/__main__.py b/spm12/__main__.py index daf9fc5..178f37e 100755 --- a/spm12/__main__.py +++ b/spm12/__main__.py @@ -3,5 +3,5 @@ from .cli import main -if __name__ == "__main__": # pragma: no cover +if __name__ == "__main__": # pragma: no cover sys.exit(main(sys.argv[1:])) From 298226cabd9b8c6e72b96e0910c90437445caa5d Mon Sep 17 00:00:00 2001 From: Casper da Costa-Luis Date: Tue, 28 Mar 2023 19:49:37 +0100 Subject: [PATCH 10/18] lint --- spm12/__init__.py | 4 +-- spm12/regseg.py | 69 +++++++++++++++----------------------------- spm12/utils.py | 22 ++++++-------- tests/conftest.py | 16 +++------- tests/test_regseg.py | 20 ++++--------- 5 files changed, 45 insertions(+), 86 deletions(-) diff --git a/spm12/__init__.py b/spm12/__init__.py index 2755ed5..a84bd14 100644 --- a/spm12/__init__.py +++ b/spm12/__init__.py @@ -1,5 +1,5 @@ -from .regseg import * # NOQA -from .utils import * # NOQA +from .regseg import * # NOQA, yapf: disable +from .utils import * # NOQA, yapf: disable # version detector. Precedence: installed dist, git, 'UNKNOWN' try: diff --git a/spm12/regseg.py b/spm12/regseg.py index 0081cf1..ea155d6 100644 --- a/spm12/regseg.py +++ b/spm12/regseg.py @@ -35,7 +35,7 @@ def glob_match(pttrn, pth): def fwhm2sig(fwhm, voxsize=2.0): - return fwhm / (voxsize * (8 * np.log(2)) ** 0.5) + return fwhm / (voxsize * (8 * np.log(2))**0.5) def smoothim(fim, fwhm=4, fout=""): @@ -43,9 +43,8 @@ def smoothim(fim, fwhm=4, fout=""): Smooth image using Gaussian filter with FWHM given as an option. """ imd = nii.getnii(fim, output="all") - imsmo = ndi.filters.gaussian_filter( - imd["im"], fwhm2sig(fwhm, voxsize=imd["voxsize"]), mode="constant" - ) + imsmo = ndi.filters.gaussian_filter(imd["im"], fwhm2sig(fwhm, voxsize=imd["voxsize"]), + mode="constant") if not fout: f = nii.file_parts(fim) fout = os.path.join(f[0], f"{f[1]}_smo{str(fwhm).replace('.', '-')}{f[2]}") @@ -76,18 +75,9 @@ def get_bbox(fnii): raise ValueError("incorrect input NIfTI file/dictionary") dim = niidct["hdr"]["dim"] - corners = np.array( - [ - [1, 1, 1, 1], - [1, 1, dim[3], 1], - [1, dim[2], 1, 1], - [1, dim[2], dim[3], 1], - [dim[1], 1, 1, 1], - [dim[1], 1, dim[3], 1], - [dim[1], dim[2], 1, 1], - [dim[1], dim[2], dim[3], 1], - ] - ) + corners = np.array([[1, 1, 1, 1], [1, 1, dim[3], 1], [1, dim[2], 1, 1], [1, dim[2], dim[3], 1], + [dim[1], 1, 1, 1], [dim[1], 1, dim[3], 1], [dim[1], dim[2], 1, 1], + [dim[1], dim[2], dim[3], 1]]) XYZ = np.dot(niidct["affine"][:3, :], corners.T) @@ -135,23 +125,14 @@ def coreg_spm( """ out = {} # output dictionary sep = sep or [4, 2] + tol = tol or [ - 0.0200, - 0.0200, - 0.0200, - 0.0010, - 0.0010, - 0.0010, - 0.0100, - 0.0100, - 0.0100, - 0.0010, - 0.0010, - 0.0010, - ] + 0.0200, 0.0200, 0.0200, 0.0010, 0.0010, 0.0010, 0.0100, 0.0100, 0.0100, 0.0010, 0.0010, + 0.0010] + fwhm = fwhm or [7, 7] params = params or [0, 0, 0, 0, 0, 0] - eng = ensure_spm(matlab_eng_name) # get_matlab + eng = ensure_spm(matlab_eng_name) # get_matlab if not outpath and fname_aff and "/" in fname_aff: opth = os.path.dirname(fname_aff) or os.path.dirname(imflo) @@ -288,18 +269,16 @@ def resample_spm( del_out_uncmpr=False, ): log.debug( - dedent( - """\ + dedent("""\ ====================================================================== S P M inputs: > ref:' %r > flo:' %r - ======================================================================""" - ), + ======================================================================"""), imref, imflo, ) - eng = ensure_spm(matlab_eng_name) # get_matlab + eng = ensure_spm(matlab_eng_name) # get_matlab if not outpath and fimout: opth = os.path.dirname(fimout) or os.path.dirname(imflo) @@ -412,11 +391,11 @@ def seg_spm( sotre_fwd/inv: stores forward/inverse normalisation definitions visual: shows the Matlab window progress """ - out = {} # output dictionary - # get Matlab engine or use the provided one - eng = ensure_spm(matlab_eng_name) + out = {} # output dictionary + eng = ensure_spm(matlab_eng_name) # get Matlab engine or use the provided one if not spm_path: spm_path = spm_dir() + # run SPM normalisation/segmentation param, invdef, fordef = eng.amypad_seg( f_mri, @@ -434,7 +413,8 @@ def seg_spm( out["param"] = move_files(param, outpath) out["invdef"] = move_files(invdef, outpath) out["fordef"] = move_files(fordef, outpath) - # go through tissue types and move them to the output folder + + # move each tissue type to the output folder for c in glob_match(r"c\d*", os.path.dirname(param)): nm = os.path.basename(c)[:2] out[nm] = move_files(c, outpath) @@ -474,7 +454,7 @@ def normw_spm(f_def, files4norm, voxsz=2, intrp=4, bbox=None, matlab_eng_name="" raise ValueError("unrecognised format for bounding box") if isinstance(voxsz, Number): - voxsz = ml.double([voxsz]*3) + voxsz = ml.double([voxsz] * 3) elif isinstance(voxsz, (np.ndarray, list)): if len(voxsz) != 3: raise ValueError(f"voxel size ({voxsz}) should be scalar or 3-vector") @@ -482,10 +462,10 @@ def normw_spm(f_def, files4norm, voxsz=2, intrp=4, bbox=None, matlab_eng_name="" else: raise ValueError(f"voxel size ({voxsz}) should be scalar or 3-vector") - - eng = ensure_spm(matlab_eng_name) # get_matlab + eng = ensure_spm(matlab_eng_name) # get_matlab eng.amypad_normw(f_def, files4norm, voxsz, float(intrp), bb) - out = [] # output list + out = [] # output list + if outpath is not None: create_dir(outpath) for f in files4norm: @@ -494,8 +474,7 @@ def normw_spm(f_def, files4norm, voxsz=2, intrp=4, bbox=None, matlab_eng_name="" move_files( os.path.join(os.path.dirname(fpth), "w" + os.path.basename(fpth)), outpath, - ) - ) + )) else: out.append("w" + os.path.basename(f.split(",")[0])) return out diff --git a/spm12/utils.py b/spm12/utils.py index 822d6e9..9a688d6 100644 --- a/spm12/utils.py +++ b/spm12/utils.py @@ -7,7 +7,7 @@ from miutil.mlab import get_engine from miutil.web import urlopen_cached -try: # py<3.9 +try: # py<3.9 import importlib_resources as resources except ImportError: from importlib import resources @@ -55,29 +55,25 @@ def ensure_spm(name=None, cache="~/.spm12", version=12): try: log.info("Downloading to %s", cache) with urlopen_cached( - "https://www.fil.ion.ucl.ac.uk/spm/download/restricted/eldorado/spm12.zip", - cache, + "https://www.fil.ion.ucl.ac.uk/spm/download/restricted/eldorado/spm12.zip", + cache, ) as fd: extractall(fd, cache) eng.addpath(addpath) if not eng.exist("spm_jobman"): raise RuntimeError("MATLAB could not find SPM.") log.info("Installed") - except: # NOQA: E722,B001 + except: # NOQA: E722,B001 raise ImportError( - dedent( - """\ + dedent("""\ MATLAB could not find SPM. Please follow installation instructions at https://en.wikibooks.org/wiki/SPM/Download Make sure to add SPM to MATLAB's path using `startup.m` - """ - ) - ) + """)) + found = eng.which("spm_jobman") if path.realpath(path.dirname(found)) != path.realpath(addpath): - log.warning( - f"Internal ({addpath}) does not match detected ({found}) SPM12.\n" - "This means `spm_dir()` is likely to fail - use `spm_dir_eng()` instead." - ) + log.warning(f"Internal ({addpath}) does not match detected ({found}) SPM12.\n" + "This means `spm_dir()` is likely to fail - use `spm_dir_eng()` instead.") return eng diff --git a/tests/conftest.py b/tests/conftest.py index 8206804..e82ff35 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -12,14 +12,10 @@ def folder_in(): Ab_PET_mMR_test = HOME / "Ab_PET_mMR_test" if not Ab_PET_mMR_test.is_dir(): skip( - dedent( - """\ + dedent("""\ Cannot find Ab_PET_mMR_test in ${DATA_ROOT:-~} (%s). Try running `python -m tests` to download it. - """ - ) - % HOME - ) + """) % HOME) return Ab_PET_mMR_test @@ -28,15 +24,11 @@ def folder_ref(folder_in): Ab_PET_mMR_ref = folder_in / "testing_reference" / "Ab_PET_mMR_ref" if not Ab_PET_mMR_ref.is_dir(): skip( - dedent( - """\ + dedent("""\ Cannot find Ab_PET_mMR_ref in ${DATA_ROOT:-~}/testing_reference (%s/testing_reference). Try running `python -m tests` to download it. - """ - ) - % HOME - ) + """) % HOME) return Ab_PET_mMR_ref diff --git a/tests/test_regseg.py b/tests/test_regseg.py index 948037b..569f24f 100644 --- a/tests/test_regseg.py +++ b/tests/test_regseg.py @@ -6,14 +6,9 @@ from spm12 import regseg -MRI2PET = np.array( - [ - [0.99990508, 0.00800995, 0.01121016, -0.68164088], - [-0.00806219, 0.99995682, 0.00462244, -1.16235105], - [-0.01117265, -0.00471238, 0.99992648, -1.02167229], - [0.0, 0.0, 0.0, 1.0], - ] -) +MRI2PET = np.array([[0.99990508, 0.00800995, 0.01121016, -0.68164088], + [-0.00806219, 0.99995682, 0.00462244, -1.16235105], + [-0.01117265, -0.00471238, 0.99992648, -1.02167229], [0.0, 0.0, 0.0, 1.0]]) no_matlab_warn = mark.filterwarnings("ignore:.*collections.abc:DeprecationWarning") no_scipy_warn = mark.filterwarnings("ignore:numpy.ufunc size changed.*:RuntimeWarning") @@ -22,18 +17,15 @@ def assert_equal_arrays(x, y, nmse_tol=0, denan=True): if denan: x, y = map(np.nan_to_num, (x, y)) if nmse_tol: - if ((x - y) ** 2).mean() / (y**2).mean() < nmse_tol: + if ((x - y)**2).mean() / (y**2).mean() < nmse_tol: return elif (x == y).all(): return raise ValueError( - dedent( - f""" Unequal arrays:x != y. min/mean/max(std): + dedent(f""" Unequal arrays:x != y. min/mean/max(std): x: {x.min():.3g}/{x.mean():.3g}/{x.max():.3g}({x.std():.3g}) y: {y.min():.3g}/{y.mean():.3g}/{y.max():.3g}({y.std():.3g}) - """ - ) - ) + """)) @no_scipy_warn From b9e6d1907ba864779ee144f9a7c0607b67b708ca Mon Sep 17 00:00:00 2001 From: Casper da Costa-Luis Date: Tue, 28 Mar 2023 20:08:58 +0100 Subject: [PATCH 11/18] tests: ignore matlab engine PEP440 non-compliance - vis. https://github.com/pypa/setuptools/issues/3772 --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d281833..5d30704 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -65,7 +65,7 @@ jobs: fetch-depth: 0 - name: Run setup-python run: setup-python -p${{ matrix.python }} - - run: pip install -U .[dev] + - run: pip install -U .[dev] 'setuptools<66' # ignore matlab engine PEP440 non-compliance https://github.com/pypa/setuptools/issues/3772 - run: pytest --durations-min=1 - uses: codecov/codecov-action@v3 - name: Post Run setup-python From 3e3701f5adc30a6f3feb76a36bf2a6e260a09788 Mon Sep 17 00:00:00 2001 From: Pawel Date: Thu, 20 Apr 2023 14:14:38 +0100 Subject: [PATCH 12/18] minor fix for spm norm write --- spm12/amypad_normw.m | 2 ++ 1 file changed, 2 insertions(+) diff --git a/spm12/amypad_normw.m b/spm12/amypad_normw.m index 07f7def..9625ac2 100644 --- a/spm12/amypad_normw.m +++ b/spm12/amypad_normw.m @@ -6,6 +6,8 @@ job.woptions.vox = voxsz; %[voxsz, voxsz, voxsz]; job.woptions.interp = intrp; job.woptions.prefix = 'w'; + + addpath(fullfile(spm('Dir'),'config')); spm_run_norm(job); out=0; end From 07703307c3608b7c1c0bfd55fad1759ec5c91281 Mon Sep 17 00:00:00 2001 From: Pawel Date: Sat, 13 May 2023 09:45:57 +0100 Subject: [PATCH 13/18] corrected for converting arrays to python with newer versions of matlab --- spm12/regseg.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/spm12/regseg.py b/spm12/regseg.py index ea155d6..03359b5 100644 --- a/spm12/regseg.py +++ b/spm12/regseg.py @@ -200,11 +200,14 @@ def coreg_spm( out["freg"] = imflou_ # get the affine matrix - M = np.array(Mm._data.tolist()) - M = M.reshape(4, 4).T + M = np.array(Mm.tomemoryview().tolist()) + + # # OLD WAY: + # M = np.array(Mm._data.tolist()) + # M = M.reshape(4, 4).T # get the translation and rotation parameters in a vector - x = np.array(xm._data.tolist()) + x = np.array(xm.tomemoryview().tolist()) # delete the uncompressed files if del_uncmpr: From c173ac96ddc5d95b896aed0336362eadde2a18dc Mon Sep 17 00:00:00 2001 From: Casper da Costa-Luis Date: Sat, 13 May 2023 15:38:35 +0530 Subject: [PATCH 14/18] fix matlab again --- spm12/regseg.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/spm12/regseg.py b/spm12/regseg.py index 03359b5..06ca1fa 100644 --- a/spm12/regseg.py +++ b/spm12/regseg.py @@ -91,6 +91,12 @@ def get_bbox(fnii): return bbox +def mat2array(matlab_mat): + if hasattr(matlab_mat, '_data'): # matlab Date: Mon, 22 May 2023 11:15:35 +0100 Subject: [PATCH 15/18] fix matlab variable bug --- spm12/regseg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spm12/regseg.py b/spm12/regseg.py index 06ca1fa..39a6524 100644 --- a/spm12/regseg.py +++ b/spm12/regseg.py @@ -463,7 +463,7 @@ def normw_spm(f_def, files4norm, voxsz=2, intrp=4, bbox=None, matlab_eng_name="" elif isinstance(voxsz, (np.ndarray, list)): if len(voxsz) != 3: raise ValueError(f"voxel size ({voxsz}) should be scalar or 3-vector") - voxsz = ml.double(list(voxsz)) + voxsz = ml.double(np.float64(voxsz)) else: raise ValueError(f"voxel size ({voxsz}) should be scalar or 3-vector") From 8ccd75b749365618855b087262c6ca61ae8f513c Mon Sep 17 00:00:00 2001 From: Casper da Costa-Luis Date: Mon, 29 May 2023 11:24:10 +0530 Subject: [PATCH 16/18] tests: update pre-commit --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index fb0c654..56545ca 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -38,7 +38,7 @@ repos: - flake8-isort - flake8-string-format - repo: https://github.com/google/yapf - rev: v0.32.0 + rev: v0.33.0 hooks: - id: yapf args: [-i] From 75d7ce0a220d2cb9457b3ec54535e901b1a50cf0 Mon Sep 17 00:00:00 2001 From: Casper da Costa-Luis Date: Mon, 29 May 2023 12:01:10 +0530 Subject: [PATCH 17/18] build: migrate setup.cfg => pyproject.toml --- .pre-commit-config.yaml | 1 + LICENCE.md | 2 +- pyproject.toml | 82 +++++++++++++++++++++++++++++++++++ setup.cfg | 94 ----------------------------------------- setup.py | 3 -- 5 files changed, 84 insertions(+), 98 deletions(-) delete mode 100644 setup.cfg delete mode 100644 setup.py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 56545ca..c1ef057 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -36,6 +36,7 @@ repos: - flake8-comprehensions - flake8-debugger - flake8-isort + - flake8-pyproject - flake8-string-format - repo: https://github.com/google/yapf rev: v0.33.0 diff --git a/LICENCE.md b/LICENCE.md index ee3b9f9..3c0414b 100644 --- a/LICENCE.md +++ b/LICENCE.md @@ -1,4 +1,4 @@ -Copyright 2020 AMYPAD +Copyright 2020-23 AMYPAD Licensed under the Apache License, Version 2.0 (the "License"); you may not use this project except in compliance with the License. diff --git a/pyproject.toml b/pyproject.toml index f493c01..dfa1802 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,3 +5,85 @@ build-backend = "setuptools.build_meta" [tool.setuptools_scm] write_to = "spm12/_dist_ver.py" write_to_template = "__version__ = '{version}'\n" + +[tool.setuptools.packages.find] +exclude = ["tests"] + +[tool.setuptools.package-data] +"*" = ["*.md", "*.rst", "*.m"] + +[project.urls] +documentation = "https://github.com/AMYPAD/SPM12/#SPM12" +repository = "https://github.com/AMYPAD/SPM12" +changelog = "https://github.com/AMYPAD/SPM12/releases" +upstream-project = "https://www.fil.ion.ucl.ac.uk/spm" + +[project] +name = "spm12" +dynamic = ["version"] +maintainers = [{name = "Casper da Costa-Luis", email = "casper.dcl@physics.org"}] +description = "Statistical Parametric Mapping" +readme = "README.rst" +requires-python = ">=3.7" +keywords = ["fMRI", "PET", "SPECT", "EEG", "MEG"] +license = {text = "Apache-2.0"} +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Intended Audience :: Education", + "Intended Audience :: Healthcare Industry", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: Apache Software License", + "Operating System :: Microsoft :: Windows", + "Operating System :: POSIX :: Linux", + "Programming Language :: Other Scripting Engines", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3 :: Only", + "Topic :: Scientific/Engineering :: Medical Science Apps.", + "Topic :: Software Development :: Libraries", + "Topic :: System :: Installation/Setup"] +dependencies = ['importlib_resources; python_version < "3.9"', "argopt", "miutil[nii,web]>=0.7.2", "numpy", "scipy"] + +[project.optional-dependencies] +dev = ["pytest>=6", "pytest-cov", "pytest-timeout", "pytest-xdist"] +demo = ["miutil[plot]>=0.3.0", "matplotlib"] + +[project.scripts] +spm12 = "spm12.cli:main" + +[tool.flake8] +max_line_length = 99 +extend_ignore = ["E261"] +exclude = [".git", "__pycache__", "build", "dist", ".eggs"] + +[tool.yapf] +spaces_before_comment = [15, 20] +arithmetic_precedence_indication = true +allow_split_before_dict_value = false +coalesce_brackets = true +column_limit = 99 +each_dict_entry_on_separate_line = false +space_between_ending_comma_and_closing_bracket = false +split_before_named_assigns = false +split_before_closing_bracket = false +blank_line_before_nested_class_or_def = false + +[tool.isort] +profile = "black" +line_length = 99 +multi_line_output = 4 +known_first_party = ["spm12", "tests"] + +[tool.pytest.ini_options] +minversion = "6.0" +timeout = 300 +log_level = "INFO" +python_files = ["tests/test_*.py"] +testpaths = ["tests"] +addopts = "-v --tb=short -rxs -W=error --log-level=debug -n=auto --durations=0 --durations-min=1 --cov=spm12 --cov-report=term-missing --cov-report=xml" +filterwarnings = ["ignore:numpy.ufunc size changed.*:RuntimeWarning"] diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 23eeaa5..0000000 --- a/setup.cfg +++ /dev/null @@ -1,94 +0,0 @@ -[metadata] -name=spm12 -description=Statistical Parametric Mapping -long_description=file: README.rst -long_description_content_type=text/x-rst -license=Apache-2.0 -license_file=LICENCE.md -url=https://github.com/AMYPAD/SPM12 -project_urls= - Changelog=https://github.com/AMYPAD/SPM12/releases - Documentation=https://github.com/AMYPAD/SPM12/#SPM12 - Upstream Project=https://www.fil.ion.ucl.ac.uk/spm -maintainer=Casper da Costa-Luis -maintainer_email=casper.dcl@physics.org -keywords=fMRI, PET, SPECT, EEG, MEG -platforms=any -provides=spm12 -classifiers= - Development Status :: 4 - Beta - Intended Audience :: Developers - Intended Audience :: Education - Intended Audience :: Healthcare Industry - Intended Audience :: Science/Research - License :: OSI Approved :: Apache Software License - Operating System :: Microsoft :: Windows - Operating System :: POSIX :: Linux - Programming Language :: Other Scripting Engines - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - Programming Language :: Python :: 3.10 - Programming Language :: Python :: 3.11 - Programming Language :: Python :: 3 :: Only - Topic :: Scientific/Engineering :: Medical Science Apps. - Topic :: Software Development :: Libraries - Topic :: System :: Installation/Setup -[options] -zip_safe=False -setup_requires=setuptools>=42; wheel; setuptools_scm[toml]>=3.4 -install_requires= - argopt - importlib_resources; python_version < "3.9" - miutil[nii,web]>=0.7.2 - numpy - scipy -include_package_data=True -packages=find: -python_requires=>=3.7 -[options.extras_require] -dev= - pytest - pytest-cov - pytest-timeout - pytest-xdist -demo= - miutil[plot]>=0.3.0 - matplotlib -[options.entry_points] -console_scripts = - spm12=spm12.cli:main -[options.packages.find] -exclude=tests -[options.package_data] -*=*.md, *.rst, *.m - -[flake8] -max_line_length=99 -extend_ignore=E261 -exclude=.git,__pycache__,build,dist,.eggs - -[yapf] -spaces_before_comment=15, 20 -arithmetic_precedence_indication=true -allow_split_before_dict_value=false -coalesce_brackets=True -column_limit=99 -each_dict_entry_on_separate_line=False -space_between_ending_comma_and_closing_bracket=False -split_before_named_assigns=False -split_before_closing_bracket=False -blank_line_before_nested_class_or_def=False - -[isort] -profile=black -line_length=99 -multi_line_output=4 -known_first_party=spm12,tests - -[tool:pytest] -timeout=300 -addopts=-v --tb=short -rxs -W=error --log-level=debug -n=auto --durations=0 --cov=spm12 --cov-report=term-missing --cov-report=xml -filterwarnings= - ignore:numpy.ufunc size changed.*:RuntimeWarning diff --git a/setup.py b/setup.py deleted file mode 100644 index d5d43d7..0000000 --- a/setup.py +++ /dev/null @@ -1,3 +0,0 @@ -from setuptools import setup - -setup(use_scm_version=True) From 1e83fe293ef9cbf6fdea3be9de72230649190fdb Mon Sep 17 00:00:00 2001 From: Casper da Costa-Luis Date: Tue, 30 May 2023 11:12:00 +0530 Subject: [PATCH 18/18] deps: bump miutil.mlab --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index dfa1802..5e37a7c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,7 @@ classifiers = [ "Topic :: Scientific/Engineering :: Medical Science Apps.", "Topic :: Software Development :: Libraries", "Topic :: System :: Installation/Setup"] -dependencies = ['importlib_resources; python_version < "3.9"', "argopt", "miutil[nii,web]>=0.7.2", "numpy", "scipy"] +dependencies = ['importlib_resources; python_version < "3.9"', "argopt", "miutil[nii,web]>=0.12.0", "numpy", "scipy"] [project.optional-dependencies] dev = ["pytest>=6", "pytest-cov", "pytest-timeout", "pytest-xdist"]