From d39ad930925db3578bfbeb23f83ea03d128babf2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 11 Sep 2023 10:14:58 +0200 Subject: [PATCH 01/22] Bump actions/checkout from 3 to 4 (#121) Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4. - [Release notes](https://github.com/actions/checkout/releases) - [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md) - [Commits](https://github.com/actions/checkout/compare/v3...v4) --- updated-dependencies: - dependency-name: actions/checkout dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/build.yml | 2 +- .github/workflows/code-style.yml | 2 +- .github/workflows/publish.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3fd56102..77e21482 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -26,7 +26,7 @@ jobs: shell: bash steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Python ${{ matrix.python-version }} uses: actions/setup-python@v4 with: diff --git a/.github/workflows/code-style.yml b/.github/workflows/code-style.yml index d5181af0..856892f6 100644 --- a/.github/workflows/code-style.yml +++ b/.github/workflows/code-style.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Python 3.9 uses: actions/setup-python@v4 with: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index a1ba3de5..1b48e48d 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup Python 3.9 uses: actions/setup-python@v4 with: From a8416ccd8585bb551c3c1ea2a23f4ac665ca4eeb Mon Sep 17 00:00:00 2001 From: Mathieu Scheltienne Date: Thu, 14 Sep 2023 11:50:57 +0200 Subject: [PATCH 02/22] MAINT: Future compatibility with MNE 1.6 (#120) * compatibility for meas_info * compatibility for fiff.py and ch_data.py * compatibility for test_meas_info * fix missed for _picks_to_idx * fix missed * fix style * trigger cis * fix cis MNE-main which should install the dependencies required by main instead of stable * trigger cis * install pre-release binaries for numpy and scipy when running MNE-main tests * try again to install pre-release binaries * try again * trigger cis * Update azure-pipelines.yml * increase timeout, upgrade instead of force-reinstall, and add matplotlib * fix numpy 1.25 deprecation * sort alphabetically * rm duplicate junitxml and use the set coverage config --- azure-pipelines.yml | 40 +++++------ pycrostates/cluster/_base.py | 7 +- pycrostates/cluster/tests/test_aahc.py | 7 +- pycrostates/cluster/tests/test_kmeans.py | 7 +- pycrostates/io/ch_data.py | 9 ++- pycrostates/io/fiff.py | 69 ++++++++++++------- pycrostates/io/meas_info.py | 31 ++++++--- pycrostates/io/tests/test_meas_info.py | 3 +- .../preprocessing/extract_gfp_peaks.py | 7 +- pycrostates/preprocessing/resample.py | 7 +- pycrostates/preprocessing/spatial_filter.py | 7 +- .../tests/test_extract_gfp_peaks.py | 7 +- .../preprocessing/tests/test_resample.py | 7 +- .../tests/test_spatial_filter.py | 7 +- pycrostates/utils/mixin.py | 10 ++- pycrostates/utils/tests/test_checks.py | 7 +- pycrostates/viz/cluster_centers.py | 2 +- pyproject.toml | 36 +++++----- 18 files changed, 182 insertions(+), 88 deletions(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index c6febb15..3ff83aea 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -48,67 +48,67 @@ stages: linux 3.8: VM_IMAGE: 'ubuntu-latest' PYTHON_VERSION: '3.8' - MNE_DEV: false + PRE: false CACHE_MNE_PATH: $(CACHE_PATH_MNE_LINUX) linux 3.9: VM_IMAGE: 'ubuntu-latest' PYTHON_VERSION: '3.9' - MNE_DEV: false + PRE: false CACHE_MNE_PATH: $(CACHE_PATH_MNE_LINUX) linux 3.10: VM_IMAGE: 'ubuntu-latest' PYTHON_VERSION: '3.10' - MNE_DEV: false + PRE: false CACHE_MNE_PATH: $(CACHE_PATH_MNE_LINUX) linux 3.11: VM_IMAGE: 'ubuntu-latest' PYTHON_VERSION: '3.11' - MNE_DEV: false + PRE: false CACHE_MNE_PATH: $(CACHE_PATH_MNE_LINUX) linux 3.10 MNE-dev: VM_IMAGE: 'ubuntu-latest' PYTHON_VERSION: '3.10' - MNE_DEV: true + PRE: true CACHE_MNE_PATH: $(CACHE_PATH_MNE_LINUX) macOS 3.8: VM_IMAGE: 'macOS-latest' PYTHON_VERSION: '3.8' - MNE_DEV: false + PRE: false CACHE_MNE_PATH: $(CACHE_PATH_MNE_MACOS) macOS 3.9: VM_IMAGE: 'macOS-latest' PYTHON_VERSION: '3.9' - MNE_DEV: false + PRE: false CACHE_MNE_PATH: $(CACHE_PATH_MNE_MACOS) macOS 3.10: VM_IMAGE: 'macOS-latest' PYTHON_VERSION: '3.10' - MNE_DEV: false + PRE: false CACHE_MNE_PATH: $(CACHE_PATH_MNE_MACOS) macOS 3.11: VM_IMAGE: 'macOS-latest' PYTHON_VERSION: '3.11' - MNE_DEV: false + PRE: false CACHE_MNE_PATH: $(CACHE_PATH_MNE_MACOS) windows 3.8: VM_IMAGE: 'windows-latest' PYTHON_VERSION: '3.8' - MNE_DEV: false + PRE: false CACHE_MNE_PATH: $(CACHE_PATH_MNE_WINDOWS) windows 3.9: VM_IMAGE: 'windows-latest' PYTHON_VERSION: '3.9' - MNE_DEV: false + PRE: false CACHE_MNE_PATH: $(CACHE_PATH_MNE_WINDOWS) windows 3.10: VM_IMAGE: 'windows-latest' PYTHON_VERSION: '3.10' - MNE_DEV: false + PRE: false CACHE_MNE_PATH: $(CACHE_PATH_MNE_WINDOWS) windows 3.11: VM_IMAGE: 'windows-latest' PYTHON_VERSION: '3.11' - MNE_DEV: false + PRE: false CACHE_MNE_PATH: $(CACHE_PATH_MNE_WINDOWS) pool: vmImage: $(VM_IMAGE) @@ -121,13 +121,13 @@ stages: displayName: 'Setup Python $(PYTHON_VERSION)' - script: | python -m pip install --progress-bar off --upgrade pip setuptools wheel - python -m pip install --progress-bar off .[test] - displayName: 'Install dependencies and MNE (stable)' + python -m pip install --progress-bar off --upgrade .[test] + displayName: 'Install package' - script: | - python -m pip uninstall -y mne - python -m pip install --progress-bar off --no-deps git+https://github.com/mne-tools/mne-python - condition: eq(variables.MNE_DEV, 'true') - displayName: 'Install MNE (main)' + python -m pip install --progress-bar off --force-reinstall git+https://github.com/mne-tools/mne-python + python -m pip install --progress-bar off --upgrade --pre --only-binary :all: -i https://pypi.anaconda.org/scientific-python-nightly-wheels/simple --timeout=180 numpy scipy scikit-learn matplotlib + condition: eq(variables.PRE, 'true') + displayName: 'Install pre-release dependencies' - script: mne sys_info -pd displayName: 'Display MNE config' - script: | @@ -142,7 +142,7 @@ stages: - script: python -c "import mne; mne.datasets.testing.data_path(verbose=True)" displayName: 'Get MNE testing datataset' condition: ne(variables.CACHE_MNE_RESTORED, 'true') - - script: pytest pycrostates --junitxml=junit/test-results.xml --cov=pycrostates --cov-report=xml + - script: pytest pycrostates --cov=pycrostates --cov-report=xml --cov-config=pyproject.toml displayName: 'Run unit tests' - task: PublishCodeCoverageResults@1 inputs: diff --git a/pycrostates/cluster/_base.py b/pycrostates/cluster/_base.py index 1e1cdf7a..187b7b83 100644 --- a/pycrostates/cluster/_base.py +++ b/pycrostates/cluster/_base.py @@ -9,10 +9,15 @@ from mne import BaseEpochs, pick_info from mne.annotations import _annotations_starts_stops from mne.io import BaseRaw -from mne.io.pick import _picks_to_idx +from mne.utils import check_version from numpy.typing import NDArray from scipy.signal import convolve2d +if check_version("mne", "1.6"): + from mne._fiff.pick import _picks_to_idx +else: + from mne.io.pick import _picks_to_idx + from .._typing import CHData, Cluster, Picks from ..segmentation import EpochsSegmentation, RawSegmentation from ..utils import _corr_vectors diff --git a/pycrostates/cluster/tests/test_aahc.py b/pycrostates/cluster/tests/test_aahc.py index cb1e71e3..a8cdfd8d 100644 --- a/pycrostates/cluster/tests/test_aahc.py +++ b/pycrostates/cluster/tests/test_aahc.py @@ -13,9 +13,14 @@ from mne.channels import DigMontage from mne.datasets import testing from mne.io import RawArray, read_raw_fif -from mne.io.pick import _picks_to_idx +from mne.utils import check_version from numpy.testing import assert_allclose +if check_version("mne", "1.6"): + from mne._fiff.pick import _picks_to_idx +else: + from mne.io.pick import _picks_to_idx + from pycrostates import __version__ from pycrostates.cluster import AAHCluster from pycrostates.io import ChData, ChInfo, read_cluster diff --git a/pycrostates/cluster/tests/test_kmeans.py b/pycrostates/cluster/tests/test_kmeans.py index db4be0c3..032f85da 100644 --- a/pycrostates/cluster/tests/test_kmeans.py +++ b/pycrostates/cluster/tests/test_kmeans.py @@ -13,9 +13,14 @@ from mne.channels import DigMontage from mne.datasets import testing from mne.io import RawArray, read_raw_fif -from mne.io.pick import _picks_to_idx +from mne.utils import check_version from numpy.testing import assert_allclose +if check_version("mne", "1.6"): + from mne._fiff.pick import _picks_to_idx +else: + from mne.io.pick import _picks_to_idx + from pycrostates import __version__ from pycrostates.cluster import ModKMeans from pycrostates.io import ChData, ChInfo, read_cluster diff --git a/pycrostates/io/ch_data.py b/pycrostates/io/ch_data.py index 11009cb2..c861435d 100644 --- a/pycrostates/io/ch_data.py +++ b/pycrostates/io/ch_data.py @@ -3,10 +3,15 @@ from typing import Any, Union import numpy as np -from mne.io import Info -from mne.io.pick import _picks_to_idx, pick_info +from mne import Info, pick_info +from mne.utils import check_version from numpy.typing import NDArray +if check_version("mne", "1.6"): + from mne._fiff.pick import _picks_to_idx +else: + from mne.io.pick import _picks_to_idx + from .._typing import CHData, CHInfo from ..utils._checks import _check_type from ..utils._docs import fill_doc diff --git a/pycrostates/io/fiff.py b/pycrostates/io/fiff.py index 77068300..bee0bc9e 100644 --- a/pycrostates/io/fiff.py +++ b/pycrostates/io/fiff.py @@ -8,30 +8,53 @@ from typing import List, Union import numpy as np -from mne.io import Info -from mne.io._digitization import _format_dig_points, _read_dig_fif +from mne import Info, Transform from mne.io.constants import FIFF -from mne.io.ctf_comp import _read_ctf_comp, write_ctf_comp -from mne.io.meas_info import _read_bad_channels, _write_ch_infos -from mne.io.open import fiff_open -from mne.io.proj import _read_proj, _write_proj -from mne.io.tag import read_tag -from mne.io.tree import dir_tree_find -from mne.io.write import ( - end_block, - start_and_end_file, - start_block, - write_coord_trans, - write_dig_points, - write_double_matrix, - write_id, - write_int, - write_name_list, - write_string, -) -from mne.transforms import Transform, invert_transform +from mne.transforms import invert_transform +from mne.utils import check_version from numpy.typing import NDArray +if check_version("mne", "1.6"): + from mne._fiff._digitization import _format_dig_points, _read_dig_fif + from mne._fiff.ctf_comp import _read_ctf_comp, write_ctf_comp + from mne._fiff.meas_info import _read_bad_channels, _write_ch_infos + from mne._fiff.open import fiff_open + from mne._fiff.proj import _read_proj, _write_proj + from mne._fiff.tag import read_tag + from mne._fiff.tree import dir_tree_find + from mne._fiff.write import ( + end_block, + start_and_end_file, + start_block, + write_coord_trans, + write_dig_points, + write_double_matrix, + write_id, + write_int, + write_name_list, + write_string, + ) +else: + from mne.io._digitization import _format_dig_points, _read_dig_fif + from mne.io.ctf_comp import _read_ctf_comp, write_ctf_comp + from mne.io.meas_info import _read_bad_channels, _write_ch_infos + from mne.io.open import fiff_open + from mne.io.proj import _read_proj, _write_proj + from mne.io.tag import read_tag + from mne.io.tree import dir_tree_find + from mne.io.write import ( + end_block, + start_and_end_file, + start_block, + write_coord_trans, + write_dig_points, + write_double_matrix, + write_id, + write_int, + write_name_list, + write_string, + ) + from .. import __version__ from .._typing import CHInfo from ..cluster import AAHCluster, ModKMeans @@ -521,13 +544,13 @@ def _read_meas_info(fid, tree): pos = meas_info["directory"][k].pos if kind == FIFF.FIFF_NCHAN: tag = read_tag(fid, pos) - nchan = int(tag.data) + nchan = int(tag.data.item()) elif kind == FIFF.FIFF_CH_INFO: tag = read_tag(fid, pos) chs.append(tag.data) elif kind == FIFF.FIFF_MNE_CUSTOM_REF: tag = read_tag(fid, pos) - custom_ref_applied = int(tag.data) + custom_ref_applied = int(tag.data.item()) elif kind == FIFF.FIFF_COORD_TRANS: tag = read_tag(fid, pos) cand = tag.data diff --git a/pycrostates/io/meas_info.py b/pycrostates/io/meas_info.py index e36b55fb..ca987dc6 100644 --- a/pycrostates/io/meas_info.py +++ b/pycrostates/io/meas_info.py @@ -5,20 +5,29 @@ from typing import List, Optional, Tuple, Union import numpy as np -from mne.io import Info +from mne import Info, Projection, Transform from mne.io.constants import FIFF -from mne.io.meas_info import ( - _check_bads, - _check_ch_keys, - _check_dev_head_t, - _unique_channel_names, -) -from mne.io.pick import get_channel_type_constants -from mne.io.proj import Projection -from mne.io.tag import _ch_coord_dict -from mne.transforms import Transform from mne.utils import check_version +if check_version("mne", "1.6"): + from mne._fiff.meas_info import ( + _check_bads, + _check_ch_keys, + _check_dev_head_t, + _unique_channel_names, + ) + from mne._fiff.pick import get_channel_type_constants + from mne._fiff.tag import _ch_coord_dict +else: + from mne.io.meas_info import ( + _check_bads, + _check_ch_keys, + _check_dev_head_t, + _unique_channel_names, + ) + from mne.io.pick import get_channel_type_constants + from mne.io.tag import _ch_coord_dict + from .._typing import CHInfo from ..utils._checks import _check_type, _IntLike from ..utils._logs import logger diff --git a/pycrostates/io/tests/test_meas_info.py b/pycrostates/io/tests/test_meas_info.py index f055271e..7f603f35 100644 --- a/pycrostates/io/tests/test_meas_info.py +++ b/pycrostates/io/tests/test_meas_info.py @@ -4,12 +4,11 @@ import numpy as np import pytest -from mne import create_info +from mne import Transform, create_info from mne.channels import DigMontage from mne.datasets import testing from mne.io import read_raw_fif from mne.io.constants import FIFF -from mne.transforms import Transform from mne.utils import check_version from numpy.testing import assert_allclose diff --git a/pycrostates/preprocessing/extract_gfp_peaks.py b/pycrostates/preprocessing/extract_gfp_peaks.py index f7b17a38..333f69e1 100644 --- a/pycrostates/preprocessing/extract_gfp_peaks.py +++ b/pycrostates/preprocessing/extract_gfp_peaks.py @@ -4,10 +4,15 @@ import numpy as np from mne import BaseEpochs, pick_info from mne.io import BaseRaw -from mne.io.pick import _picks_to_idx +from mne.utils import check_version from numpy.typing import NDArray from scipy.signal import find_peaks +if check_version("mne", "1.6"): + from mne._fiff.pick import _picks_to_idx +else: + from mne.io.pick import _picks_to_idx + from .._typing import CHData, Picks from ..utils._checks import ( _check_picks_uniqueness, diff --git a/pycrostates/preprocessing/resample.py b/pycrostates/preprocessing/resample.py index f0a92c25..7b2b6c54 100644 --- a/pycrostates/preprocessing/resample.py +++ b/pycrostates/preprocessing/resample.py @@ -5,7 +5,12 @@ import numpy as np from mne import BaseEpochs, pick_info from mne.io import BaseRaw -from mne.io.pick import _picks_to_idx +from mne.utils import check_version + +if check_version("mne", "1.6"): + from mne._fiff.pick import _picks_to_idx +else: + from mne.io.pick import _picks_to_idx from .._typing import CHData, Picks, RANDomState from ..utils._checks import ( diff --git a/pycrostates/preprocessing/spatial_filter.py b/pycrostates/preprocessing/spatial_filter.py index e9b2fa07..9978d04a 100644 --- a/pycrostates/preprocessing/spatial_filter.py +++ b/pycrostates/preprocessing/spatial_filter.py @@ -6,12 +6,17 @@ from mne.channels import find_ch_adjacency from mne.channels.interpolation import _make_interpolation_matrix from mne.io import BaseRaw -from mne.io.pick import _picks_by_type from mne.parallel import parallel_func +from mne.utils import check_version from mne.utils.check import _check_preload from numpy.typing import NDArray from scipy.sparse import csr_matrix +if check_version("mne", "1.6"): + from mne._fiff.pick import _picks_by_type +else: + from mne.io.pick import _picks_by_type + from .._typing import CHData from ..utils._checks import _check_n_jobs, _check_type, _check_value from ..utils._docs import fill_doc diff --git a/pycrostates/preprocessing/tests/test_extract_gfp_peaks.py b/pycrostates/preprocessing/tests/test_extract_gfp_peaks.py index 8979a36c..ee1ef2c9 100644 --- a/pycrostates/preprocessing/tests/test_extract_gfp_peaks.py +++ b/pycrostates/preprocessing/tests/test_extract_gfp_peaks.py @@ -2,7 +2,12 @@ import pytest from mne import pick_info from mne.datasets import testing -from mne.io.pick import _picks_to_idx +from mne.utils import check_version + +if check_version("mne", "1.6"): + from mne._fiff.pick import _picks_to_idx +else: + from mne.io.pick import _picks_to_idx from pycrostates.io import ChData from pycrostates.preprocessing import extract_gfp_peaks diff --git a/pycrostates/preprocessing/tests/test_resample.py b/pycrostates/preprocessing/tests/test_resample.py index 2fb49798..ecba3272 100644 --- a/pycrostates/preprocessing/tests/test_resample.py +++ b/pycrostates/preprocessing/tests/test_resample.py @@ -5,9 +5,14 @@ import pytest from mne import BaseEpochs from mne.datasets import testing -from mne.io.pick import _picks_to_idx +from mne.utils import check_version from numpy.testing import assert_allclose +if check_version("mne", "1.6"): + from mne._fiff.pick import _picks_to_idx +else: + from mne.io.pick import _picks_to_idx + from pycrostates.io import ChData from pycrostates.preprocessing import resample diff --git a/pycrostates/preprocessing/tests/test_spatial_filter.py b/pycrostates/preprocessing/tests/test_spatial_filter.py index 200b67cd..92119579 100644 --- a/pycrostates/preprocessing/tests/test_spatial_filter.py +++ b/pycrostates/preprocessing/tests/test_spatial_filter.py @@ -3,7 +3,12 @@ import pytest from mne.channels import find_ch_adjacency from mne.datasets import testing -from mne.io.pick import _picks_by_type +from mne.utils import check_version + +if check_version("mne", "1.6"): + from mne._fiff.pick import _picks_by_type +else: + from mne.io.pick import _picks_by_type from pycrostates.io import ChData from pycrostates.preprocessing import apply_spatial_filter diff --git a/pycrostates/utils/mixin.py b/pycrostates/utils/mixin.py index b44c0806..8cea73af 100644 --- a/pycrostates/utils/mixin.py +++ b/pycrostates/utils/mixin.py @@ -1,7 +1,13 @@ """Mixins for pycrostates data container with a .info attribute.""" -from mne.io.meas_info import ContainsMixin as MNEContainsMixin -from mne.io.meas_info import MontageMixin as MNEMontageMixin +from mne.utils import check_version + +if check_version("mne", "1.6"): + from mne._fiff.meas_info import ContainsMixin as MNEContainsMixin + from mne._fiff.meas_info import MontageMixin as MNEMontageMixin +else: + from mne.io.meas_info import ContainsMixin as MNEContainsMixin + from mne.io.meas_info import MontageMixin as MNEMontageMixin from ._docs import copy_doc diff --git a/pycrostates/utils/tests/test_checks.py b/pycrostates/utils/tests/test_checks.py index c557e30c..655fc4e9 100644 --- a/pycrostates/utils/tests/test_checks.py +++ b/pycrostates/utils/tests/test_checks.py @@ -9,10 +9,15 @@ from matplotlib import pyplot as plt from mne import EpochsArray, create_info from mne.io import RawArray -from mne.io.pick import _picks_to_idx +from mne.utils import check_version from numpy.random import PCG64, Generator from numpy.random.mtrand import RandomState +if check_version("mne", "1.6"): + from mne._fiff.pick import _picks_to_idx +else: + from mne.io.pick import _picks_to_idx + from pycrostates.utils._checks import ( _check_axes, _check_n_jobs, diff --git a/pycrostates/viz/cluster_centers.py b/pycrostates/viz/cluster_centers.py index 6fb323bd..072987b1 100644 --- a/pycrostates/viz/cluster_centers.py +++ b/pycrostates/viz/cluster_centers.py @@ -5,8 +5,8 @@ import numpy as np from matplotlib import pyplot as plt from matplotlib.axes import Axes +from mne import Info from mne.channels.layout import _find_topomap_coords -from mne.io import Info from mne.viz import plot_topomap from numpy.typing import NDArray diff --git a/pyproject.toml b/pyproject.toml index 533127c9..892c7a03 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,31 +25,31 @@ keywords = [ 'brain', ] classifiers = [ + 'Intended Audience :: Developers', + 'Intended Audience :: Science/Research', + 'License :: OSI Approved :: BSD License', + 'Natural Language :: English', + 'Operating System :: MacOS', 'Operating System :: Microsoft :: Windows', 'Operating System :: Unix', - 'Operating System :: MacOS', 'Programming Language :: Python :: 3 :: Only', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: 3.10', 'Programming Language :: Python :: 3.11', - 'Natural Language :: English', - 'License :: OSI Approved :: BSD License', - 'Intended Audience :: Science/Research', - 'Intended Audience :: Developers', 'Topic :: Scientific/Engineering', ] dependencies = [ - 'numpy>=1.21', - 'scipy', - 'mne>=1.1.0', + 'decorator', + 'importlib-resources; python_version < "3.9"', + 'jinja2', 'joblib', 'matplotlib', - 'scikit-learn', + 'mne>=1.2', + 'numpy>=1.21', 'pooch', - 'decorator', - 'jinja2', - 'importlib-resources; python_version < "3.9"', + 'scikit-learn', + 'scipy', ] [project.optional-dependencies] @@ -113,11 +113,12 @@ include = '\.pyi?$' extend-exclude = ''' ( __pycache__ - | \.github - | setup.py + | \.github/ | docs/ - | tutorials/ + | paper/ | pycrostates/html_templates/repr + | setup.py + | tutorials/ ) ''' @@ -127,10 +128,11 @@ multi_line_output = 3 line_length = 88 py_version = 39 extend_skip_glob = [ - 'setup.py', 'docs/*', - 'tutorials/*', + 'paper/', 'pycrostates/html_templates/repr/*', + 'setup.py', + 'tutorials/*', ] [tool.pydocstyle] From 45659facfe3205fbbd89184a354928eb82dabc22 Mon Sep 17 00:00:00 2001 From: Mathieu Scheltienne Date: Thu, 14 Sep 2023 15:31:19 +0200 Subject: [PATCH 03/22] Add sys_info utility to display dependencies version (#122) * add sys_info utility * fix import * fix style --- .github/workflows/build.yml | 6 +- azure-pipelines.yml | 2 + pycrostates/__init__.py | 2 + pycrostates/commands/__init__.py | 0 pycrostates/commands/sys_info.py | 18 ++++ pycrostates/utils/sys_info.py | 121 +++++++++++++++++++++++ pycrostates/utils/tests/test_sys_info.py | 36 +++++++ pyproject.toml | 5 + 8 files changed, 187 insertions(+), 3 deletions(-) create mode 100644 pycrostates/commands/__init__.py create mode 100644 pycrostates/commands/sys_info.py create mode 100644 pycrostates/utils/sys_info.py create mode 100644 pycrostates/utils/tests/test_sys_info.py diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 77e21482..6ffd79e6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -37,7 +37,7 @@ jobs: python -m pip install --progress-bar off --upgrade pip setuptools wheel python -m pip install --progress-bar off .[build] - name: Test package install - run: python -c "import pycrostates; print(pycrostates.__version__)" + run: pycrostates-sys_info - name: Remove package install run: python -m pip uninstall -y pycrostates - name: Build package @@ -45,12 +45,12 @@ jobs: - name: Install sdist run: pip install ./dist/*.tar.gz - name: Test sdist install - run: python -c "import pycrostates; print(pycrostates.__version__)" + run: pycrostates-sys_info - name: Remove sdist install run: python -m pip uninstall -y pycrostates - name: Install wheel run: pip install ./dist/*.whl - name: Test wheel install - run: python -c "import pycrostates; print(pycrostates.__version__)" + run: pycrostates-sys_info - name: Remove wheel install run: python -m pip uninstall -y pycrostates diff --git a/azure-pipelines.yml b/azure-pipelines.yml index 3ff83aea..5feb49d9 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -128,6 +128,8 @@ stages: python -m pip install --progress-bar off --upgrade --pre --only-binary :all: -i https://pypi.anaconda.org/scientific-python-nightly-wheels/simple --timeout=180 numpy scipy scikit-learn matplotlib condition: eq(variables.PRE, 'true') displayName: 'Install pre-release dependencies' + - script: pycrostates-sys_info --developer + displayName: 'Display Pycrostates config' - script: mne sys_info -pd displayName: 'Display MNE config' - script: | diff --git a/pycrostates/__init__.py b/pycrostates/__init__.py index e49f0477..999f2576 100644 --- a/pycrostates/__init__.py +++ b/pycrostates/__init__.py @@ -3,6 +3,7 @@ from . import cluster, datasets, metrics, preprocessing, utils, viz from ._version import __version__ # noqa: F401 from .utils._logs import set_log_level +from .utils.sys_info import sys_info # noqa: F401 __all__ = ( "cluster", @@ -12,4 +13,5 @@ "utils", "viz", "set_log_level", + "sys_info", ) diff --git a/pycrostates/commands/__init__.py b/pycrostates/commands/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/pycrostates/commands/sys_info.py b/pycrostates/commands/sys_info.py new file mode 100644 index 00000000..c1607cf3 --- /dev/null +++ b/pycrostates/commands/sys_info.py @@ -0,0 +1,18 @@ +import argparse + +from .. import sys_info + + +def run(): + """Run sys_info() command.""" + parser = argparse.ArgumentParser( + prog=f"{__package__.split('.')[0]}-sys_info", description="sys_info" + ) + parser.add_argument( + "--developer", + help="display information for optional dependencies", + action="store_true", + ) + args = parser.parse_args() + + sys_info(developer=args.developer) diff --git a/pycrostates/utils/sys_info.py b/pycrostates/utils/sys_info.py new file mode 100644 index 00000000..34ed5af9 --- /dev/null +++ b/pycrostates/utils/sys_info.py @@ -0,0 +1,121 @@ +import platform +import sys +from functools import partial +from importlib.metadata import requires, version +from typing import IO, Callable, List, Optional + +import psutil +from packaging.requirements import Requirement + +from ._checks import _check_type + + +def sys_info(fid: Optional[IO] = None, developer: bool = False): + """Print the system information for debugging. + + Parameters + ---------- + fid : file-like | None + The file to write to, passed to :func:`print`. Can be None to use + :data:`sys.stdout`. + developer : bool + If True, display information about optional dependencies. + """ + _check_type(developer, (bool,), "developer") + + ljust = 26 + out = partial(print, end="", file=fid) + package = __package__.split(".")[0] + + # OS information - requires python 3.8 or above + out("Platform:".ljust(ljust) + platform.platform() + "\n") + # python information + out("Python:".ljust(ljust) + sys.version.replace("\n", " ") + "\n") + out("Executable:".ljust(ljust) + sys.executable + "\n") + # CPU information + out("CPU:".ljust(ljust) + platform.processor() + "\n") + out("Physical cores:".ljust(ljust) + str(psutil.cpu_count(False)) + "\n") + out("Logical cores:".ljust(ljust) + str(psutil.cpu_count(True)) + "\n") + # memory information + out("RAM:".ljust(ljust)) + out(f"{psutil.virtual_memory().total / float(2 ** 30):0.1f} GB\n") + out("SWAP:".ljust(ljust)) + out(f"{psutil.swap_memory().total / float(2 ** 30):0.1f} GB\n") + # package information + out(f"{package}:".ljust(ljust) + version(package) + "\n") + + # dependencies + out("\nCore dependencies\n") + dependencies = [Requirement(elt) for elt in requires(package)] + core_dependencies = [dep for dep in dependencies if "extra" not in str(dep.marker)] + _list_dependencies_info(out, ljust, package, core_dependencies) + + # extras + if developer: + keys = ( + "build", + "docs", + "test", + "style", + ) + for key in keys: + extra_dependencies = [ + dep + for dep in dependencies + if all(elt in str(dep.marker) for elt in ("extra", key)) + ] + if len(extra_dependencies) == 0: + continue + out(f"\nOptional '{key}' dependencies\n") + _list_dependencies_info(out, ljust, package, extra_dependencies) + + +def _list_dependencies_info( + out: Callable, ljust: int, package: str, dependencies: List[Requirement] +): + """List dependencies names and versions.""" + unicode = sys.stdout.encoding.lower().startswith("utf") + if unicode: + ljust += 1 + + not_found: List[Requirement] = list() + for dep in dependencies: + if dep.name == package: + continue + try: + version_ = version(dep.name) + except Exception: + not_found.append(dep) + continue + + # build the output string step by step + output = f"✔︎ {dep.name}" if unicode else dep.name + # handle version specifiers + if len(dep.specifier) != 0: + output += f" ({str(dep.specifier)})" + output += ":" + output = output.ljust(ljust) + version_ + + # handle special dependencies with backends, C dep, .. + if dep.name in ("matplotlib", "seaborn") and version_ != "Not found.": + try: + from matplotlib import pyplot as plt + + backend = plt.get_backend() + except Exception: + backend = "Not found" + + output += f" (backend: {backend})" + out(output + "\n") + + if len(not_found) != 0: + not_found = [ + f"{dep.name} ({str(dep.specifier)})" + if len(dep.specifier) != 0 + else dep.name + for dep in not_found + ] + if unicode: + out(f"✘ Not installed: {', '.join(not_found)}\n") + else: + out(f"Not installed: {', '.join(not_found)}\n") diff --git a/pycrostates/utils/tests/test_sys_info.py b/pycrostates/utils/tests/test_sys_info.py new file mode 100644 index 00000000..fc8111f5 --- /dev/null +++ b/pycrostates/utils/tests/test_sys_info.py @@ -0,0 +1,36 @@ +"""Test config.py""" + +from io import StringIO + +from pycrostates.utils.sys_info import sys_info + + +def test_sys_info(): + """Test info-showing utility.""" + out = StringIO() + sys_info(fid=out) + value = out.getvalue() + out.close() + assert "Platform:" in value + assert "Executable:" in value + assert "CPU:" in value + assert "Physical cores:" in value + assert "Logical cores" in value + assert "RAM:" in value + assert "SWAP:" in value + + assert "numpy" in value + assert "psutil" in value + + assert "style" not in value + assert "test" not in value + + out = StringIO() + sys_info(fid=out, developer=True) + value = out.getvalue() + out.close() + + assert "build" in value + assert "docs" in value + assert "style" in value + assert "test" in value diff --git a/pyproject.toml b/pyproject.toml index 892c7a03..6dd34e7d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -47,7 +47,9 @@ dependencies = [ 'matplotlib', 'mne>=1.2', 'numpy>=1.21', + 'packaging', 'pooch', + 'psutil', 'scikit-learn', 'scipy', ] @@ -95,6 +97,9 @@ documentation = 'https://pycrostates.readthedocs.io/en/master/' source = 'https://github.com/vferat/pycrostates' tracker = 'https://github.com/vferat/pycrostates/issues' +[project.scripts] +pycrostates-sys_info = 'pycrostates.commands.sys_info:run' + [tool.setuptools] include-package-data = false From b4535cfe4c1626b7e55652de7910ce4c96f46c8c Mon Sep 17 00:00:00 2001 From: Mathieu Scheltienne Date: Wed, 18 Oct 2023 10:33:24 +0200 Subject: [PATCH 04/22] rm legacy setup.py (#123) --- pyproject.toml | 2 +- setup.py | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) delete mode 100644 setup.py diff --git a/pyproject.toml b/pyproject.toml index 6dd34e7d..0370dca4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ['setuptools >= 61.0.0'] +requires = ['setuptools >= 64.0.0'] build-backend = 'setuptools.build_meta' [project] diff --git a/setup.py b/setup.py deleted file mode 100644 index 60684932..00000000 --- a/setup.py +++ /dev/null @@ -1,3 +0,0 @@ -from setuptools import setup - -setup() From 350773dc8291e79400e5322f189b5c3a8ac8fbff Mon Sep 17 00:00:00 2001 From: Mathieu Scheltienne Date: Fri, 27 Oct 2023 16:31:41 +0200 Subject: [PATCH 05/22] nitpick and typos --- pycrostates/segmentation/entropy.py | 127 ++++++++---------- .../segmentation/tests/test_entropy.py | 3 - 2 files changed, 53 insertions(+), 77 deletions(-) diff --git a/pycrostates/segmentation/entropy.py b/pycrostates/segmentation/entropy.py index 8247d0c7..b683b5d6 100644 --- a/pycrostates/segmentation/entropy.py +++ b/pycrostates/segmentation/entropy.py @@ -18,10 +18,10 @@ from ..utils._docs import fill_doc -def _check_log_base(log_base): +def _check_log_base(log_base) -> float: _check_type(log_base, ("numeric", str), "log_base") if isinstance(log_base, str): - mapping = {"bits": 2, "natureal": np.e, "dits": 10} + mapping = {"bits": 2, "natural": np.e, "dits": 10} _check_value( log_base, mapping, @@ -37,7 +37,7 @@ def _check_log_base(log_base): return log_base -def _check_lags(lags): +def _check_lags(lags) -> NDArray[int]: _check_type(lags, ("int", "array-like"), "lags") if isinstance(lags, int): if lags < 1: @@ -60,7 +60,7 @@ def _joint_entropy( y: NDArray[int], state_to_ignore: Optional[int] = -1, log_base: Union[float, str] = 2, -): +) -> float: """Joint Shannon entropy of the symbolic sequences x, y. Parameters @@ -74,12 +74,14 @@ def _joint_entropy( Returns ------- - h: float + h : float joint Shannon entropy of (x, y). """ _check_type(x, (np.ndarray,), "x") _check_type(y, (np.ndarray,), "y") - if len(x) != len(y): + if any(elt.ndim != 1 for elt in (x, y)): + raise ValueError("Sequences must be 1D arrays.") + if x.size != y.size: raise ValueError("Sequences of different lengths.") if state_to_ignore is not None: state_to_ignore = _ensure_int(state_to_ignore, "state_to_ignore") @@ -90,34 +92,27 @@ def _joint_entropy( x_valid = x[valid_indices] y_valid = y[valid_indices] - # Compute the joint probability distribution + # compute the joint probability distribution n_clusters = np.max([x_valid, y_valid]) + 1 joint_prob = np.zeros((n_clusters, n_clusters)) for i in range(len(x_valid)): joint_prob[x_valid[i], y_valid[i]] += 1 joint_prob /= len(x_valid) - # Compute the joint entropy - joint_entropy = scipy.stats.entropy(joint_prob.flatten(), base=log_base) - return joint_entropy + return scipy.stats.entropy(joint_prob.flatten(), base=log_base) -def _check_labels(labels, item_name: str = "labels"): +def _check_labels(labels, item_name: str = "labels") -> None: _check_type(labels, (np.ndarray,), item_name) - if not labels.ndim == 1: + if labels.ndim != 1: raise ValueError(f"{item_name} must be a 1D array.") if not np.issubdtype(labels.dtype, np.integer): raise ValueError(f"{item_name} must be an array of integers.") -def _check_segmentation(segmentation, item_name: str = "segmentation"): - from ._base import _BaseSegmentation - - _check_type(segmentation, (_BaseSegmentation,), item_name) - labels = segmentation._labels - # reshape if epochs (returns a view) - labels = labels.reshape(-1) - return labels +def _check_segmentation(segmentation, item_name: str = "segmentation") -> NDArray[int]: + _check_type(segmentation, (Segmentation,), item_name) + return segmentation._labels.reshape(-1) # reshape if epochs (returns a view) @fill_doc @@ -126,7 +121,7 @@ def _joint_entropy_history( k: int, state_to_ignore: Optional[int] = -1, log_base: Union[float, str] = 2, -): +) -> float: r"""Compute the joint Shannon of k-histories x[t:t+k]. Compute the joint Shannon entropy of the k-histories x[t:t+k]. @@ -162,9 +157,8 @@ def _joint_entropy_history( history = tuple(labels[i : i + k]) if state_to_ignore not in history: joint_dist[history] += 1.0 - # Compute the joint entropy - _joint_entropy = scipy.stats.entropy(joint_dist.flatten(), base=log_base) - return _joint_entropy + + return scipy.stats.entropy(joint_dist.flatten(), base=log_base) @fill_doc @@ -172,7 +166,7 @@ def _entropy( labels: NDArray[int], state_to_ignore: Optional[int] = -1, log_base: Union[float, str] = 2, -): +) -> float: r"""Compute the Shannon entropy of the a symbolic sequence. Parameters @@ -191,11 +185,9 @@ def _entropy( if state_to_ignore is not None: _ensure_int(state_to_ignore, "state_to_ignore") log_base = _check_log_base(log_base) - - h = _joint_entropy_history( + return _joint_entropy_history( labels, k=1, state_to_ignore=state_to_ignore, log_base=log_base ) - return h @fill_doc @@ -203,7 +195,7 @@ def entropy( segmentation: Segmentation, ignore_repetitions: bool = False, log_base: Union[float, str] = 2, -): +) -> float: r"""Compute the Shannon entropy of a symbolic sequence. Compute the Shannon entropy\ :footcite:p:`shannon1948mathematical` of the microstate @@ -227,12 +219,9 @@ def entropy( labels = _check_segmentation(segmentation) _check_type(ignore_repetitions, (bool,), "ignore_repetitions") log_base = _check_log_base(log_base) - # ignore transition to itself (i.e. AAABBBBC -> ABC) - if ignore_repetitions: + if ignore_repetitions: # ignore transition to itself (i.e. AAABBBBC -> ABC) labels = np.array([s for s, _ in itertools.groupby(labels)]) - - h = _entropy(labels, state_to_ignore=-1, log_base=log_base) - return h + return _entropy(labels, state_to_ignore=-1, log_base=log_base) # -- excess entropy rate --------------------------------------------------------------- @@ -243,7 +232,7 @@ def _excess_entropy_rate( state_to_ignore: Optional[int] = -1, log_base: Union[float, str] = 2, n_jobs: int = 1, -): +) -> Tuple[float, float, float, NDArray[int], NDArray[float]]: """Estimate the entropy rate and the excess_entropy from a linear fit. Parameters @@ -257,15 +246,15 @@ def _excess_entropy_rate( Returns ------- - a: float + a : float Entropy rate (slope). - b: float + b : float Excess entropy (intercept). residual: float Sum of squared residuals of the least squares fit. - lags: array of shape (history_length,) + lags : array of shape (history_length,) Lag values in sample used for the fit. - joint_entropies: array of shape (history_length,) + joint_entropies : array of shape (history_length,) Joint entropy value for each lag. """ _check_labels(labels) @@ -289,7 +278,7 @@ def excess_entropy_rate( ignore_repetitions: bool = False, log_base: Union[float, str] = 2, n_jobs: int = 1, -): +) -> Tuple[float, float, float, NDArray[int], NDArray[float]]: r"""Estimate the entropy rate and the ``excess_entropy`` of the segmentation. The entropy rate and the ``excess_entropy`` are estimated from a linear fit: @@ -312,15 +301,15 @@ def excess_entropy_rate( Returns ------- - a: float + a : float Entropy rate (slope). - b: float + b : float Excess entropy (intercept). - residual: float + residual : float Sum of squared residuals of the least squares fit. - lags: array of shape (history_length,) + lags : array of shape (history_length,) Lag values in sample used for the fit. - joint_entropies: array of shape (history_length,) + joint_entropies : array of shape (history_length,) Joint entropy value for each lag. References @@ -332,11 +321,8 @@ def excess_entropy_rate( _check_type(ignore_repetitions, (bool,), "ignore_repetitions") log_base = _check_log_base(log_base) n_jobs = _check_n_jobs(n_jobs) - - # ignore transition to itself (i.e. 1 -> 1) - if ignore_repetitions: + if ignore_repetitions: # ignore transition to itself (i.e. 1 -> 1) labels = np.array([s for s, _ in itertools.groupby(labels)]) - return _excess_entropy_rate( labels, history_length, @@ -352,22 +338,21 @@ def _auto_information( k: int, state_to_ignore: Optional[int] = -1, log_base: Union[float, str] = 2, -): +) -> float: """Compute the Auto-information for lag k. Parameters ---------- %(labels_info)s - k: int + k : int Lag value in sample. %(state_to_ignore)s %(log_base)s Returns ------- - a: float + a : float time-lagged auto-information for lag k. - """ _check_labels(labels) k = _ensure_int(k, "k") @@ -404,10 +389,11 @@ def auto_information_function( ignore_repetitions: bool = False, log_base: Union[float, str] = 2, n_jobs: int = 1, -): +) -> Tuple[NDArray[int], NDArray[float]]: r"""Compute the Auto-information function (aif). - Compute the Auto-information function (aif) as described in\ :footcite:p:`von2018partial`: + Compute the Auto-information function (aif) as described + in :footcite:t:`von2018partial`: .. math:: @@ -421,7 +407,7 @@ def auto_information_function( %(segmentation)s lags : int | list | tuple | array of shape ``(n_lags,)`` Lags at which to compute the auto-information function. - If int, will use lags = np.arange(lags). + If int, will use ``lags = np.arange(lags)``. %(ignore_repetitions)s %(log_base)s %(n_jobs)s @@ -441,11 +427,8 @@ def auto_information_function( lags = _check_lags(lags) log_base = _check_log_base(log_base) n_jobs = _check_n_jobs(n_jobs) - - # ignore transition to itself (i.e. AAABBBBC -> ABC) - if ignore_repetitions: + if ignore_repetitions: # ignore transition to itself (i.e. AAABBBBC -> ABC) labels = np.array([s for s, _ in itertools.groupby(labels)]) - parallel, p_fun, _ = parallel_func(_auto_information, n_jobs, total=len(lags)) runs = parallel( p_fun(labels, k, state_to_ignore=-1, log_base=log_base) for k in lags @@ -454,27 +437,26 @@ def auto_information_function( return lags, ai -# Partial auto-information @fill_doc def _partial_auto_information( labels: NDArray[int], k: int, state_to_ignore: Optional[int] = -1, log_base: Union[float, str] = 2, -): +) -> float: """Compute the partial auto-information for lag k. Parameters ---------- %(labels_info)s - k: int + k : int Lag values in sample. %(state_to_ignore)s %(log_base)s Returns ------- - p: float + p : float Partial auto-information for lag k. """ _check_labels(labels) @@ -518,11 +500,11 @@ def partial_auto_information_function( ignore_repetitions: bool = False, log_base: Union[float, str] = 2, n_jobs: Optional[int] = 1, -): +) -> Tuple[NDArray[int], NDArray[float]]: r"""Compute the Partial auto-information function. - Compute the Partial auto-information function as described in - :footcite:t:`von2018partial`. + Compute the Partial auto-information function as described + in :footcite:t:`von2018partial`. .. math:: @@ -537,16 +519,16 @@ def partial_auto_information_function( %(segmentation)s lags : int | list, tuple, array of shape ``(n_lags,)`` The lags at which to compute the auto-information function. - If int, will use lags = np.arange(lags). + If int, will use ``lags = np.arange(lags)``. %(ignore_repetitions)s %(log_base)s %(n_jobs)s Returns ------- - lags: array of shape ``(n_lags,)`` + lags : array of shape ``(n_lags,)`` Lag values in sample. - pai: array of shape ``(n_lags,)`` + pai : array of shape ``(n_lags,)`` Partial auto-information array for each lag. References @@ -558,11 +540,8 @@ def partial_auto_information_function( _check_type(ignore_repetitions, (bool,), "ignore_repetitions") log_base = _check_log_base(log_base) n_jobs = _check_n_jobs(n_jobs) - - # ignore transition to itself (i.e. 1 -> 1) - if ignore_repetitions: + if ignore_repetitions: # ignore transition to itself (i.e. 1 -> 1) labels = np.array([s for s, _ in itertools.groupby(labels)]) - parallel, p_fun, _ = parallel_func( _partial_auto_information, n_jobs, total=len(lags) ) diff --git a/pycrostates/segmentation/tests/test_entropy.py b/pycrostates/segmentation/tests/test_entropy.py index 1f48105d..602e622c 100644 --- a/pycrostates/segmentation/tests/test_entropy.py +++ b/pycrostates/segmentation/tests/test_entropy.py @@ -32,14 +32,11 @@ raw = read_raw_fif(fname_raw_testing, preload=False) raw = raw.pick("eeg").crop(0, 10) raw = raw.load_data().filter(1, 40).apply_proj() - epochs = make_fixed_length_epochs(raw, 1, preload=True) - ModK_raw = ModKMeans(n_clusters=4, n_init=10, max_iter=100, tol=1e-4, random_state=1) ModK_epochs = ModKMeans(n_clusters=4, n_init=10, max_iter=100, tol=1e-4, random_state=1) ModK_raw.fit(raw, n_jobs=1) ModK_epochs.fit(epochs, n_jobs=1) - raw_segmentation = ModK_raw.predict(raw) epochs_segmentation = ModK_epochs.predict(epochs) From 213584d6c3ed6015eed7e631f4ad305d6898e3f5 Mon Sep 17 00:00:00 2001 From: Mathieu Scheltienne Date: Fri, 27 Oct 2023 16:47:19 +0200 Subject: [PATCH 06/22] better deprecation --- pycrostates/segmentation/_base.py | 27 ++++++++----------- .../segmentation/tests/test_transitions.py | 13 ++++----- pycrostates/segmentation/transitions.py | 24 ++++++----------- pycrostates/utils/_docs.py | 3 ++- pycrostates/utils/_fixes.py | 11 ++++++++ 5 files changed, 37 insertions(+), 41 deletions(-) diff --git a/pycrostates/segmentation/_base.py b/pycrostates/segmentation/_base.py index ddf4c465..3f9626b2 100644 --- a/pycrostates/segmentation/_base.py +++ b/pycrostates/segmentation/_base.py @@ -14,6 +14,7 @@ from ..utils import _corr_vectors from ..utils._checks import _check_type from ..utils._docs import fill_doc +from ..utils._fixes import deprecate from ..utils._logs import logger from ..viz import plot_cluster_centers from .entropy import entropy @@ -84,6 +85,13 @@ def _repr_html_(self, caption=None): def compute_parameters(self, norm_gfp: bool = True, return_dist: bool = False): """Compute microstate parameters. + .. warning:: + + When working with `~mne.Epochs`, this method will put together segments of + all epochs. This could lead to wrong interpretation especially on state + durations. To avoid this behaviour, make sure to set the ``reject_edges`` + parameter to ``True`` when creating the segmentation. + Parameters ---------- norm_gfp : bool @@ -118,13 +126,6 @@ def compute_parameters(self, norm_gfp: bool = True, return_dist: bool = False): * ``dist_durs`` (req. ``return_dist=True``): Distribution of durations of each segments assigned to a given state. Each value is expressed in seconds (s). - - warnings - -------- - When working with `~mne.Epochs`, this method will put together segments of all - epochs. This could lead to wrong interpretation especially on state durations. - To avoid this behaviour, make sure to set the ``reject_edges`` parameter to - ``True`` when creating the segmentation. """ _check_type(norm_gfp, (bool,), "norm_gfp") _check_type(return_dist, (bool,), "return_dist") @@ -240,10 +241,7 @@ def compute_transition_matrix( ) _check_type(ignore_repetitions, (bool,), "ignore_repetitions") if ignore_self is not None: - logger.warning( - "The 'ignore_self' parameter is deprecated and will be removed in \ - future versions. Please use the 'ignore_repetitions' parameter instead." - ) + deprecate("ignore_self", "ignore_repetitions") ignore_repetitions = ignore_self return _compute_transition_matrix( self._labels, @@ -285,10 +283,7 @@ def compute_expected_transition_matrix( ) _check_type(ignore_repetitions, (bool,), "ignore_repetitions") if ignore_self is not None: - logger.warning( - "The 'ignore_self' parameter is deprecated and will be removed in \ - future versions. Please use the 'ignore_repetitions' parameter instead." - ) + deprecate("ignore_self", "ignore_repetitions") ignore_repetitions = ignore_self return _compute_expected_transition_matrix( self._labels, @@ -390,7 +385,7 @@ def _check_predict_parameters(predict_parameters: dict): "reject_by_annotation", ) # Let the door open for custom prediction with different keys, so log - # a warninginstead of raising. + # a warning instead of raising. for key in predict_parameters.keys(): if key not in valid_keys: logger.warning( diff --git a/pycrostates/segmentation/tests/test_transitions.py b/pycrostates/segmentation/tests/test_transitions.py index c1a2afa8..563fdc9f 100644 --- a/pycrostates/segmentation/tests/test_transitions.py +++ b/pycrostates/segmentation/tests/test_transitions.py @@ -145,18 +145,15 @@ def test_check_labels_n_clusters(): _check_labels_n_clusters(np.random.randint(0, 8, size=100), 6) -def test_deprecated_ignore_self(caplog): +def test_deprecated_ignore_self(): labels = np.random.randint(0, 5, size=100) - log = "The 'ignore_self' parameter is deprecated" - caplog.clear() - M = compute_transition_matrix(labels, 5, ignore_self=True) - assert log in caplog.text + with pytest.warns(DeprecationWarning, match="is deprecated and will be removed"): + M = compute_transition_matrix(labels, 5, ignore_self=True) M_ = compute_transition_matrix(labels, 5, ignore_repetitions=True) assert_allclose(M, M_) - caplog.clear() - M = compute_expected_transition_matrix(labels, 5, ignore_self=True) - assert log in caplog.text + with pytest.warns(DeprecationWarning, match="is deprecated and will be removed"): + M = compute_expected_transition_matrix(labels, 5, ignore_self=True) M_ = compute_expected_transition_matrix(labels, 5, ignore_repetitions=True) assert_allclose(M, M_) diff --git a/pycrostates/segmentation/transitions.py b/pycrostates/segmentation/transitions.py index 24bb8744..8e5e7fe7 100644 --- a/pycrostates/segmentation/transitions.py +++ b/pycrostates/segmentation/transitions.py @@ -1,12 +1,12 @@ from itertools import groupby -from typing import Union +from typing import Optional import numpy as np from numpy.typing import NDArray from ..utils._checks import _check_type, _check_value from ..utils._docs import fill_doc -from ..utils._logs import logger +from ..utils._fixes import deprecate @fill_doc @@ -14,7 +14,7 @@ def compute_transition_matrix( labels: NDArray[int], n_clusters: int, stat: str = "probability", - ignore_self: Union[bool, None] = None, + ignore_self: Optional[bool] = None, ignore_repetitions: bool = True, ) -> NDArray[float]: """Compute the observed transition matrix. @@ -37,18 +37,12 @@ def compute_transition_matrix( _check_labels_n_clusters(labels, n_clusters) _check_type( ignore_self, - ( - bool, - None, - ), + (bool, None), "ignore_self", ) _check_type(ignore_repetitions, (bool,), "ignore_repetitions") if ignore_self is not None: - logger.warning( - "The 'ignore_self' parameter is deprecated and will be removed in \ - future versions. Please use the 'ignore_repetitions' parameter instead." - ) + deprecate("ignore_self", "ignore_repetitions") ignore_repetitions = ignore_self return _compute_transition_matrix( labels, @@ -98,7 +92,7 @@ def compute_expected_transition_matrix( labels: NDArray[int], n_clusters: int, stat: str = "probability", - ignore_self: Union[bool, None] = None, + ignore_self: Optional[bool] = None, ignore_repetitions: bool = True, ) -> NDArray[float]: """Compute the expected transition matrix. @@ -133,10 +127,7 @@ def compute_expected_transition_matrix( ) _check_type(ignore_repetitions, (bool,), "ignore_repetitions") if ignore_self is not None: - logger.warning( - "The 'ignore_self' parameter is deprecated and will be removed in \ - future versions. Please use the 'ignore_repetitions' parameter instead." - ) + deprecate("ignore_self", "ignore_repetitions") ignore_repetitions = ignore_self return _compute_expected_transition_matrix( labels, @@ -158,6 +149,7 @@ def _compute_expected_transition_matrix( """ # common error checking _check_value(stat, ("probability", "proportion", "percent"), "stat") + _check_type(ignore_repetitions, (bool,), "ignore_repetitions") # reshape if epochs (returns a view) labels = labels.reshape(-1) diff --git a/pycrostates/utils/_docs.py b/pycrostates/utils/_docs.py index da90b078..84d943ee 100644 --- a/pycrostates/utils/_docs.py +++ b/pycrostates/utils/_docs.py @@ -134,13 +134,14 @@ See ``ignore_repetitions`` instead. .. deprecated:: 0.3.0 + This parameter is deprecated and will be removed in future versions. Please use the ``ignore_repetitions`` parameter instead.""" docdict[ "ignore_repetitions" ] = """ ignore_repetitions : bool - If ``True``, ignores state repetitions. + If ``True``, ignores state repetitions. For example, the input sequence ``AAABBCCD`` will be transformed into ``ABCD`` before any calculation. This is equivalent to setting the duration of all states to 1 sample.""" diff --git a/pycrostates/utils/_fixes.py b/pycrostates/utils/_fixes.py index 579aebce..f5bd5635 100644 --- a/pycrostates/utils/_fixes.py +++ b/pycrostates/utils/_fixes.py @@ -1,6 +1,7 @@ """Temporary bug-fixes awaiting an upstream fix.""" import sys +from warnings import warn # https://github.com/sphinx-gallery/sphinx-gallery/issues/1112 @@ -18,3 +19,13 @@ def __getattr__(self, name): # noqa: D105 return getattr(sys.stdout, name) else: raise AttributeError(f"'file' object has not attribute '{name}'") + + +def deprecate(old: str, new: str) -> None: + """Warn about deprecation of an argument.""" + warn( + f"The '{old}' argument is deprecated and will be removed in future " + f"versions. Please use '{new}' instead.", + DeprecationWarning, + stacklevel=2, + ) From cd83f21dc2f93997c3bc7a621a5def6f999bde48 Mon Sep 17 00:00:00 2001 From: Mathieu Scheltienne Date: Fri, 27 Oct 2023 16:54:59 +0200 Subject: [PATCH 07/22] tutorial fixes --- tutorials/segmentation/10_entropy.py | 110 +++++++++++++++------------ 1 file changed, 61 insertions(+), 49 deletions(-) diff --git a/tutorials/segmentation/10_entropy.py b/tutorials/segmentation/10_entropy.py index f603b603..47c9f79b 100644 --- a/tutorials/segmentation/10_entropy.py +++ b/tutorials/segmentation/10_entropy.py @@ -2,35 +2,34 @@ Microstate Segmentation ======================= -This tutorial introduces . -""" +This tutorial introduces measures of entropy and excess entropy for microstate. -#%% -# .. include:: ../../../../links.inc +We start by fitting a modified K-means +(:class:`~pycrostates.cluster.ModKMeans`) with a sample dataset. For more +details about fitting a clustering algorithm, please refer to +:ref:`this tutorial `. -#%% -# Entropy -# ------------ -# -# We start by fitting a modified K-means -# (:class:`~pycrostates.cluster.ModKMeans`) with a sample dataset. For more -# details about fitting a clustering algorithm, please refer to -# :ref:`this tutorial `. -# -# .. note:: -# -# The lemon datasets used in this tutorial is composed of EEGLAB files. To -# use the MNE reader :func:`mne.io.read_raw_eeglab`, the ``pymatreader`` -# optional dependency is required. Use the following installation method -# appropriate for your environment: -# -# - ``pip install pymatreader`` -# - ``conda install -c conda-forge pymatreader`` -# -# Note that an environment created via the `MNE installers`_ includes -# ``pymatreader`` by default. +.. note:: + + The lemon datasets used in this tutorial is composed of EEGLAB files. To + use the MNE reader :func:`mne.io.read_raw_eeglab`, the ``pymatreader`` + optional dependency is required. Use the following installation method + appropriate for your environment: + + - ``pip install pymatreader`` + - ``conda install -c conda-forge pymatreader`` + + Note that an environment created via the `MNE installers`_ includes + ``pymatreader`` by default. + +.. include:: ../../../../links.inc +""" + +# %% # sphinx_gallery_thumbnail_number = 2 + +import numpy as np from matplotlib import pyplot as plt from mne.io import read_raw_eeglab @@ -49,8 +48,8 @@ ModK.rename_clusters(new_names=["A", "B", "C", "D", "F"]) ModK.plot() -#%% -# Once a set of cluster centers has been fitted, It can be used to predict the +# %% +# Once a set of cluster centers has been fitted, it can be used to predict the # microstate segmentation with the method # :meth:`pycrostates.cluster.ModKMeans.predict`. It returns either a # `~pycrostates.segmentation.RawSegmentation` or an @@ -66,58 +65,71 @@ reject_edges=True, ) -#%% +# %% # Entropy +# ------- # TODO: explain the concept of entropy and its application to MS analysis + h = segmentation.entropy(ignore_repetitions=False) -#%% -# We can also ignore state repetitions (i.e. self-transitions) by setting the ignore_repetitions to True. -# This is useful when you don't want to take state duration into account. +# %% +# We can also ignore state repetitions (i.e. self-transitions) by setting the +# ``ignore_repetitions`` to True. This is useful when you don't want to take state +# duration into account. + h = segmentation.entropy(ignore_repetitions=True) -#%% +# %% # Excess entropy +# -------------- # TODO: explain the concept of excess entropy and its application to MS analysis and parameters. + from pycrostates.segmentation import excess_entropy_rate -import matplotlib.pyplot as plt -a, b, residuals, lags, joint_entropies = excess_entropy_rate(segmentation, history_length=12, ignore_repetitions=False) +a, b, residuals, lags, joint_entropies = excess_entropy_rate( + segmentation, history_length=12, ignore_repetitions=False +) -plt.figure() -plt.plot(lags, joint_entropies, '-sk') -plt.plot(lags, a*lags+b, '-b') +plt.figure(layout="constrained") +plt.plot(lags, joint_entropies, "-sk") +plt.plot(lags, a * lags + b, "-b") plt.title("Entropy rate & excess entropy") plt.show() -#%% -# auto_information_function +# %% +# Auto information function (AIF) +# ------------------------------- # TODO: explain the auto_information_function and parameters. + from pycrostates.segmentation import auto_information_function -import numpy as np -lags, ai = auto_information_function(segmentation, lags=np.arange(1, 20), ignore_repetitions=False, n_jobs=2) +lags, ai = auto_information_function( + segmentation, lags=np.arange(1, 20), ignore_repetitions=False, n_jobs=2 +) -plt.figure() -plt.plot(lags, ai, '-sk') +plt.figure(layout="constrained") +plt.plot(lags, ai, "-sk") plt.title("Auto information function") plt.show() -#%% -# partial_auto_information_function +# %% +# Partial Auto information function (pAIF) +# ---------------------------------------- # TODO: explain the partial_auto_information_function and parameters. + from pycrostates.segmentation import partial_auto_information_function -import numpy as np -lags, pai = partial_auto_information_function(segmentation, lags=np.arange(1, 5), ignore_repetitions=False, n_jobs=1) +lags, pai = partial_auto_information_function( + segmentation, lags=np.arange(1, 5), ignore_repetitions=False, n_jobs=1 +) plt.figure() -plt.plot(lags, pai, '-sk') +plt.plot(lags, pai, "-sk") plt.title("Partial Auto information function") plt.show() -#%% +# %% # References # ---------- # .. footbibliography:: From 777f0c5fd68bbb54eeb60922fad7be4b64e77359 Mon Sep 17 00:00:00 2001 From: Mathieu Scheltienne Date: Fri, 27 Oct 2023 17:04:23 +0200 Subject: [PATCH 08/22] fix tutorial title --- tutorials/segmentation/10_entropy.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tutorials/segmentation/10_entropy.py b/tutorials/segmentation/10_entropy.py index 47c9f79b..c74a00a9 100644 --- a/tutorials/segmentation/10_entropy.py +++ b/tutorials/segmentation/10_entropy.py @@ -1,6 +1,6 @@ """ -Microstate Segmentation -======================= +Segmentation metrics +==================== This tutorial introduces measures of entropy and excess entropy for microstate. From e74a0f7f28704cdcb5f8d053c17942c494bf180c Mon Sep 17 00:00:00 2001 From: Mathieu Scheltienne Date: Mon, 30 Oct 2023 14:07:06 +0100 Subject: [PATCH 09/22] [MNT] Add pre-commit, drop python 3.8, add python 3.12, add yamllint, add toml-sort (#124) * use yaml format everywhere * use single quote in pyproject.toml * add codecov configuration * sort pyproject.toml * drop python 3.8, add 3.12 and sort pyproject.toml * add pre-commit and yamllint configuration * fix yaml style * fix more * better azp workflow * try different method * try again * fix doc * fix order * fix spelling * more fixes * rm check_version("mne", 1.2) * try patch for MNE 1.6 * fix type-hint * try again * fix style * fix doc build * turn warnings into errors * skip conftest in coverage and ignore pytest warn * add comment * try again * add ignore * improve test config * try github-actions for comparison * don't hard pin pytest version..... * sort pypyproject.toml * add cache to gh-action * remove plt.close("all") duplicate calls and set show to plt.isinteractive() * add cache for pip-pre * fix test * fix F401 * use constrained-layout in matplotlib * drop azp (temp, revert if needed!!) * Update pycrostates/conftest.py * Update pycrostates/conftest.py * Update pycrostates/conftest.py * fix import * Add pre-commit badge * ignore deprecation * fix joblib deprecation warning * one more --------- Co-authored-by: vferat --- .codecov.yaml | 25 ++ .github/{dependabot.yml => dependabot.yaml} | 0 .github/workflows/build.yml | 56 ---- .../{code-style.yml => code-style.yaml} | 14 +- .../workflows/{publish.yml => publish.yaml} | 4 +- .github/workflows/pytest.yaml | 109 ++++++++ .pre-commit-config.yaml | 55 ++++ .prospector.yaml | 2 +- .readthedocs.yml => .readthedocs.yaml | 21 +- .yamllint.yaml | 5 + README.md | 4 +- azure-pipelines.yml | 155 ----------- docs/source/conf.py | 8 +- pycrostates/cluster/tests/test_aahc.py | 4 +- pycrostates/cluster/tests/test_kmeans.py | 2 - pycrostates/conftest.py | 81 ++++++ pycrostates/io/meas_info.py | 62 +---- pycrostates/io/tests/test_meas_info.py | 8 +- .../segmentation/tests/test_segmentation.py | 7 - pycrostates/utils/_checks.py | 3 +- pycrostates/utils/tests/test_checks.py | 5 - pycrostates/viz/cluster_centers.py | 4 +- pycrostates/viz/segmentation.py | 4 +- pycrostates/viz/tests/test_cluster_centers.py | 20 +- pycrostates/viz/tests/test_segmentation.py | 10 - pyproject.toml | 261 +++++++++--------- setup.py | 3 + 27 files changed, 456 insertions(+), 476 deletions(-) create mode 100644 .codecov.yaml rename .github/{dependabot.yml => dependabot.yaml} (100%) delete mode 100644 .github/workflows/build.yml rename .github/workflows/{code-style.yml => code-style.yaml} (81%) rename .github/workflows/{publish.yml => publish.yaml} (93%) create mode 100644 .github/workflows/pytest.yaml create mode 100644 .pre-commit-config.yaml rename .readthedocs.yml => .readthedocs.yaml (64%) create mode 100644 .yamllint.yaml delete mode 100644 azure-pipelines.yml create mode 100644 pycrostates/conftest.py create mode 100644 setup.py diff --git a/.codecov.yaml b/.codecov.yaml new file mode 100644 index 00000000..408f379c --- /dev/null +++ b/.codecov.yaml @@ -0,0 +1,25 @@ +comment: false +github_checks: # too noisy, even though "a" interactively disables them + annotations: false + +codecov: + notify: + require_ci_to_pass: false + +coverage: + status: + patch: + default: + informational: true + target: 95% + if_no_uploads: error + if_not_found: success + if_ci_failed: failure + project: + default: false + library: + informational: true + target: 90% + if_no_uploads: error + if_not_found: success + if_ci_failed: failure diff --git a/.github/dependabot.yml b/.github/dependabot.yaml similarity index 100% rename from .github/dependabot.yml rename to .github/dependabot.yaml diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml deleted file mode 100644 index 6ffd79e6..00000000 --- a/.github/workflows/build.yml +++ /dev/null @@ -1,56 +0,0 @@ -name: build -# https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions#concurrency -# https://docs.github.com/en/developers/webhooks-and-events/events/github-event-types#pullrequestevent -# workflow name, PR number (empty on push), push ref (empty on PR) -concurrency: - group: ${{ github.workflow }}-${{ github.event.number }}-${{ github.event.ref }} - cancel-in-progress: true -on: - pull_request: - push: - branches: [main] - workflow_dispatch: - -jobs: - build: - timeout-minutes: 30 - strategy: - fail-fast: false - matrix: - os: [ubuntu, macos, windows] - python-version: [3.8, 3.9, "3.10", "3.11"] - name: ${{ matrix.os }} - ${{ matrix.python-version }} - runs-on: ${{ matrix.os }}-latest - defaults: - run: - shell: bash - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - name: Setup Python ${{ matrix.python-version }} - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - architecture: 'x64' - - name: Install dependencies - run: | - python -m pip install --progress-bar off --upgrade pip setuptools wheel - python -m pip install --progress-bar off .[build] - - name: Test package install - run: pycrostates-sys_info - - name: Remove package install - run: python -m pip uninstall -y pycrostates - - name: Build package - run: python -m build - - name: Install sdist - run: pip install ./dist/*.tar.gz - - name: Test sdist install - run: pycrostates-sys_info - - name: Remove sdist install - run: python -m pip uninstall -y pycrostates - - name: Install wheel - run: pip install ./dist/*.whl - - name: Test wheel install - run: pycrostates-sys_info - - name: Remove wheel install - run: python -m pip uninstall -y pycrostates diff --git a/.github/workflows/code-style.yml b/.github/workflows/code-style.yaml similarity index 81% rename from .github/workflows/code-style.yml rename to .github/workflows/code-style.yaml index 856892f6..2bcb7a0a 100644 --- a/.github/workflows/code-style.yml +++ b/.github/workflows/code-style.yaml @@ -2,7 +2,7 @@ name: style concurrency: group: ${{ github.workflow }}-${{ github.event.number }}-${{ github.event.ref }} cancel-in-progress: true -on: +on: # yamllint disable-line rule:truthy pull_request: push: branches: [main] @@ -22,24 +22,28 @@ jobs: architecture: 'x64' - name: Install dependencies run: | - python -m pip install --progress-bar off --upgrade pip setuptools wheel + python -m pip install --progress-bar off --upgrade pip setuptools python -m pip install --progress-bar off .[style] - - name: Run Ruff - run: ruff check pycrostates - name: Run isort uses: isort/isort-action@master - name: Run black uses: psf/black@stable with: options: "--check --verbose" + - name: Run Ruff + run: ruff check pycrostates - name: Run codespell uses: codespell-project/actions-codespell@master with: check_filenames: true check_hidden: true - skip: ./.git,./build,./.github,*.bib + skip: ./.git,./build,./.github,*.bib,./.mypy_cache,./.pytest_cache ignore_words_file: ./.codespellignore - name: Run pydocstyle run: pydocstyle . - name: Run bibclean run: bibclean-check docs/references.bib + - name: Run toml-sort + run: toml-sort pyproject.toml --check + - name: Run yamllint + run: yamllint . -c .yamllint.yaml --strict diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yaml similarity index 93% rename from .github/workflows/publish.yml rename to .github/workflows/publish.yaml index 1b48e48d..cf8a2cf7 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yaml @@ -1,6 +1,6 @@ name: publish -on: +on: # yamllint disable-line rule:truthy release: types: [published] workflow_dispatch: @@ -19,7 +19,7 @@ jobs: architecture: 'x64' - name: Install dependencies run: | - python -m pip install --progress-bar off --upgrade pip setuptools wheel + python -m pip install --progress-bar off --upgrade pip setuptools python -m pip install --progress-bar off .[build] - name: Build and publish env: diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml new file mode 100644 index 00000000..f306a318 --- /dev/null +++ b/.github/workflows/pytest.yaml @@ -0,0 +1,109 @@ +name: pytest +concurrency: + group: ${{ github.workflow }}-${{ github.event.number }}-${{ github.event.ref }} + cancel-in-progress: true +on: # yamllint disable-line rule:truthy + pull_request: + push: + branches: [main] + workflow_dispatch: + schedule: + - cron: '0 8 * * 1' + +jobs: + pytest: + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + os: [ubuntu, macos, windows] + python-version: [3.9, "3.10", "3.11", "3.12"] + name: ${{ matrix.os }} - py${{ matrix.python-version }} + runs-on: ${{ matrix.os }}-latest + defaults: + run: + shell: bash + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Setup Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + architecture: 'x64' + - name: Install package + run: | + python -m pip install --progress-bar off --upgrade pip setuptools + python -m pip install --progress-bar off .[test] + - name: Display system information + run: pycrostates-sys_info --developer + - name: Display MNE info + run: mne sys_info -pd + - name: Get testing dataset version + run: | + curl https://raw.githubusercontent.com/mne-tools/mne-testing-data/master/version.txt -o mne_testing_data_version.txt + - name: Cache testing dataset + uses: actions/cache@v3 + with: + key: mne-testing-${{ hashFiles('mne_testing_data_version.txt') }} + path: ~/mne_data + - name: Download testing dataset + run: python -c "import mne; mne.datasets.testing.data_path(verbose=True)" + - name: Run pytest + run: pytest pycrostates --cov=pycrostates --cov-report=xml --cov-config=pyproject.toml + - name: Upload to codecov + uses: codecov/codecov-action@v3 + with: + files: ./coverage.xml + flags: unittests # optional + name: codecov-umbrella # optional + verbose: true # optional (default = false) + + pytest-pip-pre: + timeout-minutes: 30 + strategy: + fail-fast: false + matrix: + python-version: ["3.11"] + name: pip pre-release - py${{ matrix.python-version }} + runs-on: ubuntu-latest + defaults: + run: + shell: bash + steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Setup Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + architecture: 'x64' + - name: Install dependencies + run: | + python -m pip install --progress-bar off --upgrade pip setuptools + python -m pip install --progress-bar off .[test] + python -m pip install --progress-bar off --upgrade git+https://github.com/mne-tools/mne-python + python -m pip install --progress-bar off --upgrade --pre --only-binary :all: -i https://pypi.anaconda.org/scientific-python-nightly-wheels/simple --timeout=180 numpy scipy scikit-learn matplotlib + - name: Display system information + run: pycrostates-sys_info --developer + - name: Display MNE info + run: mne sys_info -pd + - name: Get testing dataset version + run: | + curl https://raw.githubusercontent.com/mne-tools/mne-testing-data/master/version.txt -o mne_testing_data_version.txt + - name: Cache testing dataset + uses: actions/cache@v3 + with: + key: mne-testing-${{ hashFiles('mne_testing_data_version.txt') }} + path: ~/mne_data + - name: Download testing dataset + run: python -c "import mne; mne.datasets.testing.data_path(verbose=True)" + - name: Run pytest + run: pytest pycrostates --cov=pycrostates --cov-report=xml --cov-config=pyproject.toml + - name: Upload to codecov + uses: codecov/codecov-action@v3 + with: + files: ./coverage.xml + flags: unittests # optional + name: codecov-umbrella # optional + verbose: true # optional (default = false) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..1789a86a --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,55 @@ +ci: + skip: [codespell, pydocstyle, yamllint] + +repos: + - repo: https://github.com/pycqa/isort + rev: 5.12.0 + hooks: + - id: isort + files: pycrostates + + - repo: https://github.com/psf/black-pre-commit-mirror + rev: 23.10.1 + hooks: + - id: black + args: [--quiet] + files: pycrostates + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.1.0 + hooks: + - id: ruff + args: [--fix, --exit-non-zero-on-fix] + files: pycrostates + + - repo: https://github.com/codespell-project/codespell + rev: v2.2.6 + hooks: + - id: codespell + + - repo: https://github.com/pycqa/pydocstyle + rev: 6.3.0 + hooks: + - id: pydocstyle + files: pycrostates + additional_dependencies: [tomli] + + - repo: https://github.com/mscheltienne/bibclean + rev: 0.7.1 + hooks: + - id: bibclean-fix + files: docs/references.bib + args: [--exit-non-zero-on-fix] + + - repo: https://github.com/pappasam/toml-sort + rev: v0.23.1 + hooks: + - id: toml-sort-fix + files: pyproject.toml + + - repo: https://github.com/adrienverge/yamllint + rev: v1.32.0 + hooks: + - id: yamllint + args: [--strict, -c, .yamllint.yaml] + files: (.github/|.codecov.yaml|.pre-commit-config.yaml|.prospector.yaml|.readthedocs.yaml) diff --git a/.prospector.yaml b/.prospector.yaml index 111ac73d..3ef4bf62 100644 --- a/.prospector.yaml +++ b/.prospector.yaml @@ -28,4 +28,4 @@ pep257: pylint: disable: - - too-many-arguments \ No newline at end of file + - too-many-arguments diff --git a/.readthedocs.yml b/.readthedocs.yaml similarity index 64% rename from .readthedocs.yml rename to .readthedocs.yaml index 7f131480..9bdc832b 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yaml @@ -7,22 +7,19 @@ version: 2 # Build documentation in the docs/ directory with Sphinx sphinx: + builder: html configuration: docs/source/conf.py + fail_on_warning: true # Optionally set the version of Python and requirements required to build your docs python: install: - - method: pip - path: . - extra_requirements: - - docs + - method: pip + path: . + extra_requirements: + - docs build: - os: ubuntu-20.04 - tools: - python: "3.8" - -sphinx: - builder: html - configuration: docs/source/conf.py - fail_on_warning: true + os: ubuntu-20.04 + tools: + python: "3.10" diff --git a/.yamllint.yaml b/.yamllint.yaml new file mode 100644 index 00000000..669c8646 --- /dev/null +++ b/.yamllint.yaml @@ -0,0 +1,5 @@ +extends: default + +rules: + line-length: disable + document-start: disable diff --git a/README.md b/README.md index 8be607d7..66654433 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![PyPI version](https://badge.fury.io/py/pycrostates.svg)](https://badge.fury.io/py/pycrostates) [![Conda Version](https://img.shields.io/conda/vn/conda-forge/pycrostates.svg)](https://anaconda.org/conda-forge/pycrostates) [![Documentation Status](https://readthedocs.org/projects/pycrostates/badge/?version=latest)](https://pycrostates.readthedocs.io/en/latest/?badge=latest) -[![Build Status](https://dev.azure.com/vferat/pycrostates/_apis/build/status/vferat.pycrostates?branchName=main)](https://dev.azure.com/vferat/pycrostates/_build/latest?definitionId=1&branchName=main) +[![tests](https://github.com/vferat/pycrostates/actions/workflows/pytest.yaml/badge.svg?branch=main)](https://github.com/vferat/pycrostates/actions/workflows/pytest.yaml) [![codecov](https://codecov.io/gh/vferat/pycrostates/branch/master/graph/badge.svg?token=47COGGCGX8)](https://codecov.io/gh/vferat/pycrostates) [![Codacy Badge](https://app.codacy.com/project/badge/Grade/029e425f90614943b0a944e03922b637)](https://www.codacy.com/gh/vferat/pycrostates/dashboard?utm_source=github.com&utm_medium=referral&utm_content=vferat/pycrostates&utm_campaign=Badge_Grade) [![DOI](https://joss.theoj.org/papers/10.21105/joss.04564/status.svg)](https://doi.org/10.21105/joss.04564) @@ -38,7 +38,7 @@ To cite specific version numbers of the software, you can use the DOIs provided by [Zenodo](https://zenodo.org/record/7129852). ## Contributing - +[![pre-commit.ci status](https://results.pre-commit.ci/badge/github/vferat/pycrostates/main.svg)](https://results.pre-commit.ci/latest/github/vferat/pycrostates/main) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) [![Imports: isort](https://img.shields.io/badge/%20imports-isort-%231674b1?style=flat&labelColor=ef8336)](https://pycqa.github.io/isort/) diff --git a/azure-pipelines.yml b/azure-pipelines.yml deleted file mode 100644 index 5feb49d9..00000000 --- a/azure-pipelines.yml +++ /dev/null @@ -1,155 +0,0 @@ -trigger: - batch: false - branches: - include: - - 'main' - -pr: - branches: - include: - - '*' - -stages: -- stage: Check - jobs: - - job: Skip - pool: - vmImage: 'ubuntu-latest' - variables: - NO_SKIP: 'true' - BUILD_REASON: $(Build.Reason) - steps: - - bash: | - git_log=`git log --format=oneline -n 1 --skip=1` - echo "##vso[task.setvariable variable=log]$git_log" - displayName: Retrieve git log - - bash: echo "##vso[task.setvariable variable=NO_SKIP]false" - condition: and(eq(variables.BUILD_REASON, 'PullRequest'), or(contains(variables.log, '[skip azp]'), contains(variables.log, '[azp skip]'), contains(variables.log, '[skip ci]'), contains(variables.log, '[ci skip]'))) - displayName: Check for skip flags - - bash: echo "##vso[task.setvariable variable=start_main;isOutput=true]$NO_SKIP" - name: result - displayName: Store result in start_main - -- stage: Tests - condition: and(succeeded(), eq(dependencies.Check.outputs['Skip.result.start_main'], 'true')) - dependsOn: ['Check'] - jobs: - - job: pytest - timeoutInMinutes: 30 - variables: - CACHE_PATH_MNE_LINUX: /home/vsts/mne_data - CACHE_PATH_MNE_MACOS: /Users/runner/mne_data - CACHE_PATH_MNE_WINDOWS: C:\Users\VssAdministrator\mne_data - CODECOV_OS: Linux - CODECOV_PYTHON_VERSION: 3.9 - MPLBACKEND: agg - strategy: - matrix: - linux 3.8: - VM_IMAGE: 'ubuntu-latest' - PYTHON_VERSION: '3.8' - PRE: false - CACHE_MNE_PATH: $(CACHE_PATH_MNE_LINUX) - linux 3.9: - VM_IMAGE: 'ubuntu-latest' - PYTHON_VERSION: '3.9' - PRE: false - CACHE_MNE_PATH: $(CACHE_PATH_MNE_LINUX) - linux 3.10: - VM_IMAGE: 'ubuntu-latest' - PYTHON_VERSION: '3.10' - PRE: false - CACHE_MNE_PATH: $(CACHE_PATH_MNE_LINUX) - linux 3.11: - VM_IMAGE: 'ubuntu-latest' - PYTHON_VERSION: '3.11' - PRE: false - CACHE_MNE_PATH: $(CACHE_PATH_MNE_LINUX) - linux 3.10 MNE-dev: - VM_IMAGE: 'ubuntu-latest' - PYTHON_VERSION: '3.10' - PRE: true - CACHE_MNE_PATH: $(CACHE_PATH_MNE_LINUX) - macOS 3.8: - VM_IMAGE: 'macOS-latest' - PYTHON_VERSION: '3.8' - PRE: false - CACHE_MNE_PATH: $(CACHE_PATH_MNE_MACOS) - macOS 3.9: - VM_IMAGE: 'macOS-latest' - PYTHON_VERSION: '3.9' - PRE: false - CACHE_MNE_PATH: $(CACHE_PATH_MNE_MACOS) - macOS 3.10: - VM_IMAGE: 'macOS-latest' - PYTHON_VERSION: '3.10' - PRE: false - CACHE_MNE_PATH: $(CACHE_PATH_MNE_MACOS) - macOS 3.11: - VM_IMAGE: 'macOS-latest' - PYTHON_VERSION: '3.11' - PRE: false - CACHE_MNE_PATH: $(CACHE_PATH_MNE_MACOS) - windows 3.8: - VM_IMAGE: 'windows-latest' - PYTHON_VERSION: '3.8' - PRE: false - CACHE_MNE_PATH: $(CACHE_PATH_MNE_WINDOWS) - windows 3.9: - VM_IMAGE: 'windows-latest' - PYTHON_VERSION: '3.9' - PRE: false - CACHE_MNE_PATH: $(CACHE_PATH_MNE_WINDOWS) - windows 3.10: - VM_IMAGE: 'windows-latest' - PYTHON_VERSION: '3.10' - PRE: false - CACHE_MNE_PATH: $(CACHE_PATH_MNE_WINDOWS) - windows 3.11: - VM_IMAGE: 'windows-latest' - PYTHON_VERSION: '3.11' - PRE: false - CACHE_MNE_PATH: $(CACHE_PATH_MNE_WINDOWS) - pool: - vmImage: $(VM_IMAGE) - steps: - - task: UsePythonVersion@0 - inputs: - versionSpec: $(PYTHON_VERSION) - architecture: 'x64' - addToPath: true - displayName: 'Setup Python $(PYTHON_VERSION)' - - script: | - python -m pip install --progress-bar off --upgrade pip setuptools wheel - python -m pip install --progress-bar off --upgrade .[test] - displayName: 'Install package' - - script: | - python -m pip install --progress-bar off --force-reinstall git+https://github.com/mne-tools/mne-python - python -m pip install --progress-bar off --upgrade --pre --only-binary :all: -i https://pypi.anaconda.org/scientific-python-nightly-wheels/simple --timeout=180 numpy scipy scikit-learn matplotlib - condition: eq(variables.PRE, 'true') - displayName: 'Install pre-release dependencies' - - script: pycrostates-sys_info --developer - displayName: 'Display Pycrostates config' - - script: mne sys_info -pd - displayName: 'Display MNE config' - - script: | - curl https://raw.githubusercontent.com/mne-tools/mne-testing-data/master/version.txt -o mne_testing_data_version.txt - displayName: "Retrieve MNE testing dataset version" - - task: Cache@2 - inputs: - key: '"mne_testing_data" | mne_testing_data_version.txt' - path: $(CACHE_MNE_PATH) - cacheHitVar: CACHE_MNE_RESTORED - displayName: 'Cache MNE testing dataset' - - script: python -c "import mne; mne.datasets.testing.data_path(verbose=True)" - displayName: 'Get MNE testing datataset' - condition: ne(variables.CACHE_MNE_RESTORED, 'true') - - script: pytest pycrostates --cov=pycrostates --cov-report=xml --cov-config=pyproject.toml - displayName: 'Run unit tests' - - task: PublishCodeCoverageResults@1 - inputs: - codeCoverageTool: Cobertura - summaryFileLocation: '$(System.DefaultWorkingDirectory)/**/coverage.xml' - - bash: bash <(curl -s https://codecov.io/bash) - condition: and(eq(variables['Agent.OS'], variables.CODECOV_OS), eq(variables.PYTHON_VERSION, variables.CODECOV_PYTHON_VERSION)) - displayName: 'Upload to codecov.io' diff --git a/docs/source/conf.py b/docs/source/conf.py index 1ef7e93d..6a195c40 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -94,9 +94,8 @@ "icon": "fab fa-slack", }, ], - "external_links": [ - {"name": "MNE", "url": "https://mne.tools/stable/index.html"} - ], + "external_links": [{"name": "MNE", "url": "https://mne.tools/stable/index.html"}], + "navigation_with_keys": False, } # Add any paths that contain custom static files (such as style sheets) here, @@ -171,7 +170,7 @@ # Matplotlib "Axes": "matplotlib.axes.Axes", "Axes3D": "mpl_toolkits.mplot3d.axes3d.Axes3D", - "colormap": ":doc:`colormap `", + "colormap": ":ref:`colormap `", "Figure": "matplotlib.figure.Figure", # Scipy "csr_matrix": "scipy.sparse.csr_matrix", @@ -261,6 +260,7 @@ "within_subsection_order": FileNameSortKey, } + def append_attr_meth_examples(app, what, name, obj, options, lines): """Append SG examples backreferences to method and attr docstrings.""" # NumpyDoc nicely embeds method and attribute docstrings for us, but it diff --git a/pycrostates/cluster/tests/test_aahc.py b/pycrostates/cluster/tests/test_aahc.py index a8cdfd8d..be3cabb7 100644 --- a/pycrostates/cluster/tests/test_aahc.py +++ b/pycrostates/cluster/tests/test_aahc.py @@ -7,7 +7,6 @@ import numpy as np import pytest -from matplotlib import pyplot as plt from matplotlib.figure import Figure from mne import Annotations, Epochs, create_info, make_fixed_length_events from mne.channels import DigMontage @@ -73,7 +72,7 @@ # compute forward model A = np.sum((pos[None, ...] - sources[:, None, :3]) * sources[:, None, 3:], axis=2) A /= np.linalg.norm(A, axis=1, keepdims=True) -# simulate source actvities for 4 sources +# simulate source activities for 4 sources # with positive and negative polarity mapping = np.arange(sim_n_frames) % (sim_n_ms * 2) s = np.sign(mapping - sim_n_ms + 0.01) * np.eye(sim_n_ms)[:, mapping % sim_n_ms] @@ -282,7 +281,6 @@ def test_aahClusterMeans(): assert isinstance(f, Figure) with pytest.raises(RuntimeError, match="must be fitted before"): aahCluster2.plot(block=False) - plt.close("all") def test_invert_polarity(): diff --git a/pycrostates/cluster/tests/test_kmeans.py b/pycrostates/cluster/tests/test_kmeans.py index 032f85da..ada03a73 100644 --- a/pycrostates/cluster/tests/test_kmeans.py +++ b/pycrostates/cluster/tests/test_kmeans.py @@ -7,7 +7,6 @@ import numpy as np import pytest -from matplotlib import pyplot as plt from matplotlib.figure import Figure from mne import Annotations, Epochs, create_info, make_fixed_length_events from mne.channels import DigMontage @@ -193,7 +192,6 @@ def test_ModKMeans(): assert isinstance(f, Figure) with pytest.raises(RuntimeError, match="must be fitted before"): ModK2.plot(block=False) - plt.close("all") def test_invert_polarity(): diff --git a/pycrostates/conftest.py b/pycrostates/conftest.py new file mode 100644 index 00000000..953d89c1 --- /dev/null +++ b/pycrostates/conftest.py @@ -0,0 +1,81 @@ +import os +import warnings + +import pytest +from mne import set_log_level as set_log_level_mne + +from pycrostates import set_log_level +from pycrostates.utils._logs import logger + + +def pytest_configure(config): + """Configure pytest options.""" + config.addinivalue_line("usefixtures", "matplotlib_config") + + warnings_lines = r""" + error:: + # We use matplotlib agg backend to avoid any window to pop up during tests. + ignore:Matplotlib is currently using agg:UserWarning + # Pytest internals + ignore:Use setlocale.*instead:DeprecationWarning + ignore:datetime\.datetime\.utcnow.*is deprecated.*:DeprecationWarning + ignore:datetime\.datetime\.utcfromtimestamp.*is deprecated.*:DeprecationWarning + # Joblib + ignore:ast\.Num is deprecated.*:DeprecationWarning + ignore:Attribute n is deprecated.*:DeprecationWarning + """ + for warning_line in warnings_lines.split("\n"): + warning_line = warning_line.strip() + if warning_line and not warning_line.startswith("#"): + config.addinivalue_line("filterwarnings", warning_line) + + logger.propagate = True + set_log_level_mne("WARNING") + set_log_level("WARNING") + + +@pytest.fixture(scope="session") +def matplotlib_config(): + """Configure matplotlib for viz tests.""" + import matplotlib + from matplotlib import cbook + + # Allow for easy interactive debugging with a call like: + # + # $ PYCROSTATES_MPL_TESTING_BACKEND=Qt5Agg pytest mne/viz/tests/test_raw.py -k annotation -x --pdb # noqa: E501 + # + try: + want = os.environ["PYCROSTATES_MPL_TESTING_BACKEND"] + except KeyError: + want = "agg" # don't pop up windows + with warnings.catch_warnings(record=True): # ignore warning + warnings.filterwarnings("ignore") + matplotlib.use(want, force=True) + import matplotlib.pyplot as plt + + assert plt.get_backend() == want + # overwrite some params that can horribly slow down tests that + # users might have changed locally (but should not otherwise affect + # functionality) + plt.ioff() + plt.rcParams["figure.dpi"] = 100 + plt.rcParams["figure.raise_window"] = False + + # Make sure that we always reraise exceptions in handlers + orig = cbook.CallbackRegistry + + class CallbackRegistryReraise(orig): + def __init__(self, exception_handler=None, signals=None): + super(CallbackRegistryReraise, self).__init__(exception_handler) + + cbook.CallbackRegistry = CallbackRegistryReraise + + +@pytest.fixture(autouse=True) +def close_all(): + """Close all matplotlib plots, regardless of test status.""" + # This adds < 1 µS in local testing + import matplotlib.pyplot as plt + + yield + plt.close("all") diff --git a/pycrostates/io/meas_info.py b/pycrostates/io/meas_info.py index ca987dc6..41261a57 100644 --- a/pycrostates/io/meas_info.py +++ b/pycrostates/io/meas_info.py @@ -10,19 +10,12 @@ from mne.utils import check_version if check_version("mne", "1.6"): - from mne._fiff.meas_info import ( - _check_bads, - _check_ch_keys, - _check_dev_head_t, - _unique_channel_names, - ) + from mne._fiff.meas_info import _check_ch_keys, _unique_channel_names from mne._fiff.pick import get_channel_type_constants from mne._fiff.tag import _ch_coord_dict else: from mne.io.meas_info import ( - _check_bads, _check_ch_keys, - _check_dev_head_t, _unique_channel_names, ) from mne.io.pick import get_channel_type_constants @@ -156,34 +149,6 @@ class ChInfo(CHInfo, Info): The coordinate frame used, e.g. ``FIFFV_COORD_HEAD``. """ - # valid items - # fmt: off - _attributes = { - "bads": _check_bads, - "ch_names": "ch_names cannot be set directly. Please use methods " - "inst.add_channels(), inst.drop_channels(), inst.pick_channels(), " - "inst.rename_channels(), inst.reorder_channels() and " - "inst.set_channel_types() instead.", - "chs": "chs cannot be set directly. Please use methods inst.add_channels(), " - "inst.drop_channels(), inst.pick_channels(), inst.rename_channels(), " - "inst.reorder_channels() and inst.set_channel_types() instead.", - "comps": "comps cannot be set directly. " - "Please use method Raw.apply_gradient_compensation() instead.", - "ctf_head_t": "ctf_head_t cannot be set directly.", - "custom_ref_applied": "custom_ref_applied cannot be set directly. " - "Please use method inst.set_eeg_reference() instead.", - "dev_ctf_t": "dev_ctf_t cannot be set directly.", - "dev_head_t": _check_dev_head_t, - "dig": "dig cannot be set directly. Please use method inst.set_montage() " - "instead.", - "nchan": "nchan cannot be set directly. Please use methods " - "inst.add_channels(), inst.drop_channels(), and inst.pick_channels() " - "instead.", - "projs": "projs cannot be set directly. Please use methods inst.add_proj() and " - "inst.del_proj() instead.", - } - # fmt: on - def __init__( self, info: Optional[Info] = None, @@ -365,10 +330,9 @@ def __eq__(self, other): # compare projs, compatible with mne 1.2 and above if len(self["projs"]) != len(other["projs"]): return False - if check_version("mne", "1.2"): - for proj1, proj2 in zip(self["projs"], other["projs"]): - if proj1 != proj2: - return False + for proj1, proj2 in zip(self["projs"], other["projs"]): + if proj1 != proj2: + return False # TODO: compare compensation grades @@ -384,24 +348,6 @@ def __ne__(self, other): return not self.__eq__(other) # ------------------------------------------------------------------------ - def __setitem__(self, key, val): - """Setter for Dictionary item.""" - # During unpickling, the _unlocked attribute has not been set, so - # let __setstate__ do it later and act unlocked now - unlocked = getattr(self, "_unlocked", True) - if key in self._attributes: - if isinstance(self._attributes[key], str): - if not unlocked: - raise RuntimeError(self._attributes[key]) - else: - val = self._attributes[key](val) # attribute checker function - else: - raise RuntimeError( - f"Info does not support setting the key {repr(key)}. Supported keys " - f"are {', '.join(repr(k) for k in self._attributes)}" - ) - super().__setitem__(key, val) # calls the dict __setitem__ - def __deepcopy__(self, memodict): """Make a deepcopy.""" result = ChInfo.__new__(ChInfo) diff --git a/pycrostates/io/tests/test_meas_info.py b/pycrostates/io/tests/test_meas_info.py index 7f603f35..9bacd059 100644 --- a/pycrostates/io/tests/test_meas_info.py +++ b/pycrostates/io/tests/test_meas_info.py @@ -9,7 +9,6 @@ from mne.datasets import testing from mne.io import read_raw_fif from mne.io.constants import FIFF -from mne.utils import check_version from numpy.testing import assert_allclose from pycrostates.io import ChInfo @@ -317,7 +316,7 @@ def test_setting_invalid_keys(): chinfo = ChInfo(info=info) with pytest.raises( - RuntimeError, match="Supported keys are 'bads', 'ch_names', 'chs'" + RuntimeError, match="Info does not support directly setting the key 'test'" ): chinfo["test"] = 5 @@ -377,7 +376,4 @@ def test_comparison(caplog): assert chinfo1 == chinfo2 with chinfo1._unlock(): chinfo1["projs"] = [] - if check_version("mne", "1.2"): - assert chinfo1 != chinfo2 - else: - assert chinfo1 == chinfo2 + assert chinfo1 != chinfo2 diff --git a/pycrostates/segmentation/tests/test_segmentation.py b/pycrostates/segmentation/tests/test_segmentation.py index d5703cf9..4d6c73cd 100644 --- a/pycrostates/segmentation/tests/test_segmentation.py +++ b/pycrostates/segmentation/tests/test_segmentation.py @@ -122,12 +122,10 @@ def test_plot_cluster_centers(ModK, inst): # with raw segmentation = ModK.predict(inst) segmentation.plot_cluster_centers() - plt.close("all") # with axes provided f, ax = plt.subplots(2, 2) segmentation.plot_cluster_centers(axes=ax) - plt.close("all") @pytest.mark.parametrize( @@ -187,20 +185,15 @@ def test_plot_segmentation(ModK, inst): segmentation = ModK.predict(inst) segmentation.plot() - plt.close("all") segmentation.plot(cmap="plasma") - plt.close("all") f, ax = plt.subplots(1, 1) segmentation.plot(axes=ax) - plt.close("all") f, ax = plt.subplots(1, 1) segmentation.plot(cbar_axes=ax) - plt.close("all") # specific to raw if isinstance(inst, BaseRaw): segmentation.plot(tmin=0, tmax=5) - plt.close("all") @pytest.mark.parametrize( diff --git a/pycrostates/utils/_checks.py b/pycrostates/utils/_checks.py index 1394e0c4..5a6c8d2b 100644 --- a/pycrostates/utils/_checks.py +++ b/pycrostates/utils/_checks.py @@ -4,7 +4,6 @@ import multiprocessing as mp import operator import os -from collections.abc import Sequence from itertools import product from pathlib import Path from typing import Any @@ -70,7 +69,7 @@ def __instancecheck__(cls, other): "path-like": (str, Path, os.PathLike), "int": (_IntLike(),), "callable": (_Callable(),), - "array-like": (Sequence, np.ndarray), + "array-like": (list, tuple, set, np.ndarray), } diff --git a/pycrostates/utils/tests/test_checks.py b/pycrostates/utils/tests/test_checks.py index 655fc4e9..1ae202e9 100644 --- a/pycrostates/utils/tests/test_checks.py +++ b/pycrostates/utils/tests/test_checks.py @@ -122,24 +122,19 @@ def test_check_axes(): # test valid inputs _, ax = plt.subplots(1, 1) _check_axes(ax) - plt.close("all") _, ax = plt.subplots(1, 2) _check_axes(ax) - plt.close("all") _, ax = plt.subplots(2, 1) _check_axes(ax) - plt.close("all") # test invalid inputs f, ax = plt.subplots(1, 1) with pytest.raises(TypeError, match="must be an instance of"): _check_axes(f) - plt.close("all") _, ax = plt.subplots(10, 10) ax = ax.reshape((2, 5, 10)) with pytest.raises(ValueError, match="Argument 'axes' should be a"): _check_axes(ax) - plt.close("all") def test_check_reject_by_annotation(): diff --git a/pycrostates/viz/cluster_centers.py b/pycrostates/viz/cluster_centers.py index 072987b1..8a27c675 100644 --- a/pycrostates/viz/cluster_centers.py +++ b/pycrostates/viz/cluster_centers.py @@ -92,7 +92,7 @@ def plot_cluster_centers( # create axes if needed, and retrieve figure n_clusters = cluster_centers.shape[0] if axes is None: - f, axes = plt.subplots(1, n_clusters) + f, axes = plt.subplots(1, n_clusters, layout="constrained") if isinstance(axes, Axes): axes = np.array([axes]) # wrap in an array-like # sanity-check @@ -127,7 +127,7 @@ def plot_cluster_centers( _check_type(show, (bool,), "show") del kwargs["show"] else: - show = True + show = plt.isinteractive() # plot cluster centers for k, (center, name) in enumerate(zip(cluster_centers, cluster_names)): diff --git a/pycrostates/viz/segmentation.py b/pycrostates/viz/segmentation.py index 4ff65fd6..5e226278 100644 --- a/pycrostates/viz/segmentation.py +++ b/pycrostates/viz/segmentation.py @@ -232,7 +232,7 @@ def _plot_segmentation( ) if axes is None: - fig, axes = plt.subplots(1, 1) + fig, axes = plt.subplots(1, 1, layout="constrained") else: fig = axes.get_figure() @@ -242,7 +242,7 @@ def _plot_segmentation( _check_type(show, (bool,), "show") del kwargs["show"] else: - show = True + show = plt.isinteractive() # add color and linewidth if absent from kwargs if "color" not in kwargs: diff --git a/pycrostates/viz/tests/test_cluster_centers.py b/pycrostates/viz/tests/test_cluster_centers.py index b44293e4..c82ea1c0 100644 --- a/pycrostates/viz/tests/test_cluster_centers.py +++ b/pycrostates/viz/tests/test_cluster_centers.py @@ -22,29 +22,26 @@ def test_plot_cluster_centers(caplog): # plot with info plot_cluster_centers(cluster_centers, info) - plt.close("all") # plot with chinfo plot_cluster_centers(cluster_centers, chinfo) - plt.close("all") # provide cluster_names plot_cluster_centers(cluster_centers, info, ["A", "B"]) - plt.close("all") # provide ax f, ax = plt.subplots(1, 2) plot_cluster_centers(cluster_centers, chinfo, axes=ax) - plt.close("all") f, ax = plt.subplots(2, 1) plot_cluster_centers(cluster_centers, info, axes=ax) - plt.close("all") # provide show - plot_cluster_centers(cluster_centers, info, show=True) - plt.close("all") - plot_cluster_centers(cluster_centers, info, show=False) - plt.close("all") + # do not provide show=True for CIs with non-interactive backend + if plt.isinteractive(): + plot_cluster_centers(cluster_centers, info, show=True) + plot_cluster_centers(cluster_centers, info, show=False) + else: + plot_cluster_centers(cluster_centers, info, show=False) # invalid arguments cluster_centers_ = [[1.1, 1, 1.2], [0.4, 0.8, 0.7]] @@ -76,7 +73,6 @@ def test_plot_cluster_centers(caplog): f, ax = plt.subplots(1, 1) with pytest.raises(ValueError, match="Argument 'cluster_centers' and 'axes' must "): plot_cluster_centers(cluster_centers, info=chinfo, axes=ax) - plt.close("all") # invalid show with pytest.raises(TypeError, match="'show' must be an "): @@ -84,7 +80,6 @@ def test_plot_cluster_centers(caplog): # gradient plot_cluster_centers(cluster_centers, info, show_gradient=True) - plt.close("all") # gradient_kwargs plot_cluster_centers( @@ -93,7 +88,6 @@ def test_plot_cluster_centers(caplog): show_gradient=True, gradient_kwargs={"color": "red"}, ) - plt.close("all") caplog.clear() plot_cluster_centers( @@ -102,7 +96,6 @@ def test_plot_cluster_centers(caplog): show_gradient=False, gradient_kwargs={"color": "red"}, ) - plt.close("all") assert "argument 'gradient_kwargs' has not effect when" in caplog.text @@ -121,4 +114,3 @@ def test_with_grid_layout(): f, ax = plt.subplots(2, 2) plot_cluster_centers(cluster_centers, info, axes=ax) - plt.close("all") diff --git a/pycrostates/viz/tests/test_segmentation.py b/pycrostates/viz/tests/test_segmentation.py index afd0810e..40a98cc7 100644 --- a/pycrostates/viz/tests/test_segmentation.py +++ b/pycrostates/viz/tests/test_segmentation.py @@ -19,26 +19,21 @@ def test_plot_raw_segmentation(): labels = np.random.choice([-1, 0, 1, 2, 3], raw.times.size) plot_raw_segmentation(labels, raw, n_clusters) - plt.close("all") # provide ax f, ax = plt.subplots(1, 1) plot_raw_segmentation(labels, raw, n_clusters, axes=ax) - plt.close("all") # provide cbar_ax f, cbar_ax = plt.subplots(1, 1) plot_raw_segmentation(labels, raw, n_clusters, cbar_axes=cbar_ax) - plt.close("all") # provide ax and cbar_ax f, axes = plt.subplots(1, 2) plot_raw_segmentation(labels, raw, n_clusters, axes=axes[0], cbar_axes=axes[1]) - plt.close("all") # provide cmap plot_raw_segmentation(labels, raw, n_clusters, cmap="plasma") - plt.close("all") def test_plot_epoch_segmentation(): @@ -47,23 +42,18 @@ def test_plot_epoch_segmentation(): labels = np.random.choice([-1, 0, 1, 2, 3], (len(epochs), epochs.times.size)) plot_epoch_segmentation(labels, epochs, n_clusters) - plt.close("all") # provide ax f, ax = plt.subplots(1, 1) plot_epoch_segmentation(labels, epochs, n_clusters, axes=ax) - plt.close("all") # provide cbar_ax f, cbar_ax = plt.subplots(1, 1) plot_epoch_segmentation(labels, epochs, n_clusters, cbar_axes=cbar_ax) - plt.close("all") # provide ax and cbar_ax f, axes = plt.subplots(1, 2) plot_epoch_segmentation(labels, epochs, n_clusters, axes=axes[0], cbar_axes=axes[1]) - plt.close("all") # provide cmap plot_epoch_segmentation(labels, epochs, n_clusters, cmap="plasma") - plt.close("all") diff --git a/pyproject.toml b/pyproject.toml index 0370dca4..c089febf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,120 +1,107 @@ [build-system] -requires = ['setuptools >= 64.0.0'] build-backend = 'setuptools.build_meta' +requires = ['setuptools >= 64.0.0'] [project] -name = 'pycrostates' -version = '0.4.0.dev' -description = 'A simple open source Python package for EEG microstate segmentation.' -readme = 'README.md' -license = {file = 'LICENSE'} -requires-python = '>=3.8' authors = [ - {name = 'Victor Férat', email = 'victor.ferat@unige.ch'}, -] -maintainers = [ - {name = 'Victor Férat', email = 'victor.ferat@unige.ch'}, - {name = 'Mathieu Scheltienne', email = 'mathieu.scheltienne@fcbg.ch'}, -] -keywords = [ - 'python', - 'neuroscience', - 'neuroimaging', - 'eeg', - 'microstates', - 'brain', + {email = 'victor.ferat@unige.ch', name = 'Victor Férat'}, ] classifiers = [ - 'Intended Audience :: Developers', - 'Intended Audience :: Science/Research', - 'License :: OSI Approved :: BSD License', - 'Natural Language :: English', - 'Operating System :: MacOS', - 'Operating System :: Microsoft :: Windows', - 'Operating System :: Unix', - 'Programming Language :: Python :: 3 :: Only', - 'Programming Language :: Python :: 3.8', - 'Programming Language :: Python :: 3.9', - 'Programming Language :: Python :: 3.10', - 'Programming Language :: Python :: 3.11', - 'Topic :: Scientific/Engineering', + 'Intended Audience :: Developers', + 'Intended Audience :: Science/Research', + 'License :: OSI Approved :: BSD License', + 'Natural Language :: English', + 'Operating System :: MacOS', + 'Operating System :: Microsoft :: Windows', + 'Operating System :: Unix', + 'Programming Language :: Python :: 3 :: Only', + 'Programming Language :: Python :: 3.10', + 'Programming Language :: Python :: 3.11', + 'Programming Language :: Python :: 3.12', + 'Programming Language :: Python :: 3.9', + 'Topic :: Scientific/Engineering', ] dependencies = [ - 'decorator', - 'importlib-resources; python_version < "3.9"', - 'jinja2', - 'joblib', - 'matplotlib', - 'mne>=1.2', - 'numpy>=1.21', - 'packaging', - 'pooch', - 'psutil', - 'scikit-learn', - 'scipy', + 'decorator', + 'importlib-resources; python_version < "3.9"', + 'jinja2', + 'joblib', + 'matplotlib>=3.5', + 'mne>=1.2', + 'numpy>=1.21', + 'packaging', + 'pooch', + 'psutil', + 'scikit-learn', + 'scipy', ] +description = 'A simple open source Python package for EEG microstate segmentation.' +keywords = [ + 'brain', + 'eeg', + 'microstates', + 'neuroimaging', + 'neuroscience', + 'python', +] +license = {file = 'LICENSE'} +maintainers = [ + {email = 'mathieu.scheltienne@fcbg.ch', name = 'Mathieu Scheltienne'}, + {email = 'victor.ferat@unige.ch', name = 'Victor Férat'}, +] +name = 'pycrostates' +readme = 'README.md' +requires-python = '>=3.9' +version = '0.4.0.dev' [project.optional-dependencies] +all = [ + 'pycrostates[build]', + 'pycrostates[docs]', + 'pycrostates[style]', + 'pycrostates[test]', +] build = ['build', 'twine'] docs = [ - 'memory-profiler', - 'numpydoc', - 'pandas', - 'pydata-sphinx-theme', - 'pymatreader', - 'sphinx', - 'sphinxcontrib-bibtex', - 'sphinx-copybutton', - 'sphinx-design', - 'sphinx_gallery', - 'sphinx-issues', - 'seaborn', + 'memory-profiler', + 'numpydoc', + 'pandas', + 'pydata-sphinx-theme', + 'pymatreader', + 'seaborn', + 'sphinx', + 'sphinx-copybutton', + 'sphinx-design', + 'sphinx-issues', + 'sphinx_gallery', + 'sphinxcontrib-bibtex', ] style = [ - 'bibclean', - 'black', - 'codespell', - 'isort', - 'pydocstyle[toml]', - 'ruff', + 'bibclean', + 'black', + 'codespell', + 'isort', + 'pydocstyle[toml]', + 'ruff', + 'toml-sort', + 'yamllint', ] test = [ - 'pymatreader', - 'pytest==7.0.1', - 'pytest-azurepipelines', - 'pytest-cov', -] -all = [ - 'pycrostates[build]', - 'pycrostates[docs]', - 'pycrostates[style]', - 'pycrostates[test]', + 'pymatreader', + 'pytest', + 'pytest-cov', ] +[project.scripts] +pycrostates-sys_info = 'pycrostates.commands.sys_info:run' + [project.urls] -homepage = 'https://pycrostates.readthedocs.io/en/master/' documentation = 'https://pycrostates.readthedocs.io/en/master/' +homepage = 'https://pycrostates.readthedocs.io/en/master/' source = 'https://github.com/vferat/pycrostates' tracker = 'https://github.com/vferat/pycrostates/issues' -[project.scripts] -pycrostates-sys_info = 'pycrostates.commands.sys_info:run' - -[tool.setuptools] -include-package-data = false - -[tool.setuptools.packages.find] -include = ['pycrostates*'] -exclude = ['pycrostates*tests'] - -[tool.setuptools.package-data] -"pycrostates.datasets" = ["**/*.txt"] -"pycrostates.html_templates" = ["repr/*.jinja"] - [tool.black] -line-length = 88 -target-version = ['py39'] -include = '\.pyi?$' extend-exclude = ''' ( __pycache__ @@ -126,58 +113,76 @@ extend-exclude = ''' | tutorials/ ) ''' +include = '\.pyi?$' +line-length = 88 +target-version = ['py39'] + +[tool.coverage.report] +exclude_lines = [ + 'if __name__ == .__main__.:', + 'if TYPE_CHECKING:', + 'pragma: no cover', +] +precision = 2 + +[tool.coverage.run] +branch = true +cover_pylib = false +omit = [ + '**/__init__.py', + '**/pycrostates/_version.py', + '**/pycrostates/conftest.py', + '**/pycrostates/utils/_fixes.py', + '**/tests/**', +] [tool.isort] -profile = 'black' -multi_line_output = 3 -line_length = 88 -py_version = 39 extend_skip_glob = [ - 'docs/*', - 'paper/', - 'pycrostates/html_templates/repr/*', - 'setup.py', - 'tutorials/*', + 'docs/*', + 'paper/', + 'pycrostates/html_templates/repr/*', + 'setup.py', + 'tutorials/*', ] +line_length = 88 +multi_line_output = 3 +profile = 'black' +py_version = 39 [tool.pydocstyle] +add_ignore = 'D100,D104,D107' convention = 'numpy' -ignore-decorators= '(copy_doc|property|.*setter|.*getter|pyqtSlot|Slot)' +ignore-decorators = '(copy_doc|property|.*setter|.*getter|pyqtSlot|Slot)' match = '^(?!setup|__init__|test_).*\.py' match-dir = '^pycrostates.*' -add_ignore = 'D100,D104,D107' + +[tool.pytest.ini_options] +addopts = '--durations 20 --junit-xml=junit-results.xml -v --color=yes' +junit_family = 'xunit2' +minversion = '6.0' [tool.ruff] -line-length = 88 extend-exclude = [ - "doc", - "setup.py", + 'doc', + 'setup.py', ] +line-length = 88 [tool.ruff.per-file-ignores] -"__init__.py" = ["F401"] +'__init__.py' = ['F401'] -[tool.pytest.ini_options] -minversion = '6.0' -addopts = '--durations 20 --junit-xml=junit-results.xml --verbose' -filterwarnings = [ - "ignore:Matplotlib is currently using agg:UserWarning", -] +[tool.setuptools] +include-package-data = false -[tool.coverage.run] -branch = true -cover_pylib = false -omit = [ - '**/__init__.py', - '**/pycrostates/_version.py', - '**/pycrostates/utils/_fixes.py', - '**/tests/**', -] +[tool.setuptools.package-data] +'pycrostates.datasets' = ['**/*.txt'] +'pycrostates.html_templates' = ['repr/*.jinja'] -[tool.coverage.report] -exclude_lines = [ - 'pragma: no cover', - 'if __name__ == .__main__.:', - 'if TYPE_CHECKING:', -] -precision = 2 +[tool.setuptools.packages.find] +exclude = ['pycrostates*tests'] +include = ['pycrostates*'] + +[tool.tomlsort] +all = true +ignore_case = true +trailing_comma_inline_array = true diff --git a/setup.py b/setup.py new file mode 100644 index 00000000..60684932 --- /dev/null +++ b/setup.py @@ -0,0 +1,3 @@ +from setuptools import setup + +setup() From fde8e5125b149c4669594e3fdd4b47bbff4e6e76 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 30 Oct 2023 18:39:16 +0100 Subject: [PATCH 10/22] [pre-commit.ci] pre-commit autoupdate (#125) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/astral-sh/ruff-pre-commit: v0.1.0 → v0.1.3](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.0...v0.1.3) - [github.com/mscheltienne/bibclean: 0.7.1 → 0.7.0](https://github.com/mscheltienne/bibclean/compare/0.7.1...0.7.0) Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1789a86a..1a0eabd1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,7 +16,7 @@ repos: files: pycrostates - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.0 + rev: v0.1.3 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] @@ -35,7 +35,7 @@ repos: additional_dependencies: [tomli] - repo: https://github.com/mscheltienne/bibclean - rev: 0.7.1 + rev: 0.7.0 hooks: - id: bibclean-fix files: docs/references.bib From 35bd45f2f583c874781a65f2b99c07fb956b4bf0 Mon Sep 17 00:00:00 2001 From: Mathieu Scheltienne Date: Wed, 1 Nov 2023 10:30:29 +0100 Subject: [PATCH 11/22] skip bibclean-fix in CIs --- .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 1a0eabd1..be8b72e6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,5 @@ ci: - skip: [codespell, pydocstyle, yamllint] + skip: [codespell, pydocstyle, bibclean-fix, yamllint] repos: - repo: https://github.com/pycqa/isort From d6538cdcb1354cbd621a95cd5d03e94c1f6c7c08 Mon Sep 17 00:00:00 2001 From: Mathieu Scheltienne Date: Wed, 1 Nov 2023 10:33:14 +0100 Subject: [PATCH 12/22] pin correct version --- .pre-commit-config.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index be8b72e6..e01c547c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,5 +1,5 @@ ci: - skip: [codespell, pydocstyle, bibclean-fix, yamllint] + skip: [codespell, pydocstyle, yamllint] repos: - repo: https://github.com/pycqa/isort @@ -35,7 +35,7 @@ repos: additional_dependencies: [tomli] - repo: https://github.com/mscheltienne/bibclean - rev: 0.7.0 + rev: 0.7.1 hooks: - id: bibclean-fix files: docs/references.bib From 588c644eab08c83cd4a1a67b2e01f7adf475aee0 Mon Sep 17 00:00:00 2001 From: Mathieu Scheltienne Date: Thu, 2 Nov 2023 12:42:59 +0100 Subject: [PATCH 13/22] correctly ignore words in codespell --- .pre-commit-config.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e01c547c..d2a76ad9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -26,6 +26,7 @@ repos: rev: v2.2.6 hooks: - id: codespell + args: [--check-filenames, --ignore-words=.codespellignore] - repo: https://github.com/pycqa/pydocstyle rev: 6.3.0 From 264a7a5ba3f7902c9cf8498005b873e428e9f1ae Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 7 Nov 2023 11:12:35 +0100 Subject: [PATCH 14/22] [pre-commit.ci] pre-commit autoupdate (#127) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * [pre-commit.ci] pre-commit autoupdate updates: - [github.com/astral-sh/ruff-pre-commit: v0.1.3 → v0.1.4](https://github.com/astral-sh/ruff-pre-commit/compare/v0.1.3...v0.1.4) - [github.com/mscheltienne/bibclean: 0.7.1 → 0.7.0](https://github.com/mscheltienne/bibclean/compare/0.7.1...0.7.0) * Update .pre-commit-config.yaml --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Mathieu Scheltienne --- .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 d2a76ad9..3f1e59a5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,7 +16,7 @@ repos: files: pycrostates - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.1.3 + rev: v0.1.4 hooks: - id: ruff args: [--fix, --exit-non-zero-on-fix] From 54bd1e57c6281a666f0dbed62e330f98b6373dca Mon Sep 17 00:00:00 2001 From: Mathieu Scheltienne Date: Wed, 8 Nov 2023 14:44:52 +0100 Subject: [PATCH 15/22] [MNT] Release 0.4.0 (#128) * bump version and changelog * add deprecation * add tests --- .../dev/changes/{latest.rst => 0.4.0.rst} | 10 +-- docs/source/dev/changes/changelog.rst | 2 +- pycrostates/_typing.py | 6 ++ pycrostates/segmentation/_base.py | 61 ++++++++++++++----- .../segmentation/tests/test_segmentation.py | 8 +-- .../segmentation/tests/test_transitions.py | 28 ++++++--- pycrostates/segmentation/transitions.py | 51 +++++++++++++--- pycrostates/utils/_docs.py | 16 ++++- pycrostates/utils/_fixes.py | 11 ++++ pyproject.toml | 2 +- 10 files changed, 148 insertions(+), 47 deletions(-) rename docs/source/dev/changes/{latest.rst => 0.4.0.rst} (84%) diff --git a/docs/source/dev/changes/latest.rst b/docs/source/dev/changes/0.4.0.rst similarity index 84% rename from docs/source/dev/changes/latest.rst rename to docs/source/dev/changes/0.4.0.rst index 84f1d92a..bee5f9aa 100644 --- a/docs/source/dev/changes/latest.rst +++ b/docs/source/dev/changes/0.4.0.rst @@ -22,11 +22,5 @@ Current 0.4.0 ------------- -Enhancements -~~~~~~~~~~~~ - -Bugs -~~~~ - -API changes -~~~~~~~~~~~ +- Compatibility with ``MNE`` 1.6 and above (:pr:`120` and :pr:`124` by `Mathieu Scheltienne`_) +. Add ``show_gradient`` argument (:pr:`111` by `Victor Férat`_) diff --git a/docs/source/dev/changes/changelog.rst b/docs/source/dev/changes/changelog.rst index 51978ca2..04eb9120 100644 --- a/docs/source/dev/changes/changelog.rst +++ b/docs/source/dev/changes/changelog.rst @@ -6,6 +6,6 @@ Changelog .. toctree:: :titlesonly: - latest + 0.4.0 0.3.0 0.2.0 diff --git a/pycrostates/_typing.py b/pycrostates/_typing.py index 79786b17..b09b5cd3 100644 --- a/pycrostates/_typing.py +++ b/pycrostates/_typing.py @@ -29,5 +29,11 @@ class Cluster(ABC): pass +class Segmentation(ABC): + """Typing for a clustering class.""" + + pass + + RANDomState = Optional[Union[int, RandomState, Generator]] Picks = Optional[Union[str, NDArray[int]]] diff --git a/pycrostates/segmentation/_base.py b/pycrostates/segmentation/_base.py index 9de7d200..2de820fc 100644 --- a/pycrostates/segmentation/_base.py +++ b/pycrostates/segmentation/_base.py @@ -1,7 +1,7 @@ """Segmentation module for segmented data.""" import itertools -from abc import ABC, abstractmethod +from abc import abstractmethod from typing import List, Optional, Union import numpy as np @@ -10,16 +10,18 @@ from mne.io import BaseRaw from numpy.typing import NDArray +from .._typing import Segmentation from ..utils import _corr_vectors from ..utils._checks import _check_type from ..utils._docs import fill_doc +from ..utils._fixes import deprecate from ..utils._logs import logger from ..viz import plot_cluster_centers from .transitions import _compute_expected_transition_matrix, _compute_transition_matrix @fill_doc -class _BaseSegmentation(ABC): +class _BaseSegmentation(Segmentation): """Base class for a Microstates segmentation. Parameters @@ -82,6 +84,13 @@ def _repr_html_(self, caption=None): def compute_parameters(self, norm_gfp: bool = True, return_dist: bool = False): """Compute microstate parameters. + .. warning:: + + When working with `~mne.Epochs`, this method will put together segments of + all epochs. This could lead to wrong interpretation especially on state + durations. To avoid this behaviour, make sure to set the ``reject_edges`` + parameter to ``True`` when creating the segmentation. + Parameters ---------- norm_gfp : bool @@ -116,13 +125,6 @@ def compute_parameters(self, norm_gfp: bool = True, return_dist: bool = False): * ``dist_durs`` (req. ``return_dist=True``): Distribution of durations of each segments assigned to a given state. Each value is expressed in seconds (s). - - Warnings - -------- - When working with `~mne.Epochs`, this method will put together segments of all - epochs. This could lead to wrong interpretation especially on state durations. - To avoid this behaviour, make sure to set the ``reject_edges`` parameter to - ``True`` when creating the segmentation. """ _check_type(norm_gfp, (bool,), "norm_gfp") _check_type(return_dist, (bool,), "return_dist") @@ -202,7 +204,9 @@ def compute_parameters(self, norm_gfp: bool = True, return_dist: bool = False): params["unlabeled"] = len(np.argwhere(labels == -1)) / len(gfp) return params - def compute_transition_matrix(self, stat="probability", ignore_self=True): + def compute_transition_matrix( + self, stat="probability", ignore_self=None, ignore_repetitions=True + ): """Compute the observed transition matrix. Count the number of transitions from one state to another and aggregate the @@ -213,6 +217,7 @@ def compute_transition_matrix(self, stat="probability", ignore_self=True): ---------- %(stat_transition)s %(ignore_self)s + %(ignore_repetitions)s Returns ------- @@ -225,15 +230,29 @@ def compute_transition_matrix(self, stat="probability", ignore_self=True): with discontinuous data. To avoid this behaviour, make sure to set the ``reject_edges`` parameter to ``True`` when predicting the segmentation. """ + _check_type( + ignore_self, + ( + bool, + None, + ), + "ignore_self", + ) + _check_type(ignore_repetitions, (bool,), "ignore_repetitions") + if ignore_self is not None: + deprecate("ignore_self", "ignore_repetitions") + ignore_repetitions = ignore_self return _compute_transition_matrix( self._labels, self._cluster_centers_.shape[0], stat, - ignore_self, + ignore_repetitions, ) @fill_doc - def compute_expected_transition_matrix(self, stat="probability", ignore_self=True): + def compute_expected_transition_matrix( + self, stat="probability", ignore_self=None, ignore_repetitions=True + ): """Compute the expected transition matrix. Compute the theoretical transition matrix as if time course was ignored, but @@ -247,16 +266,30 @@ def compute_expected_transition_matrix(self, stat="probability", ignore_self=Tru ---------- %(stat_expected_transitions)s %(ignore_self)s + %(ignore_repetitions)s Returns ------- %(transition_matrix)s - """ # noqa: E501 + """ + _check_type( + ignore_self, + ( + bool, + None, + ), + "ignore_self", + ) + _check_type(ignore_repetitions, (bool,), "ignore_repetitions") + if ignore_self is not None: + deprecate("ignore_self", "ignore_repetitions") + ignore_repetitions = ignore_self + return _compute_expected_transition_matrix( self._labels, n_clusters=self._cluster_centers_.shape[0], stat=stat, - ignore_self=ignore_self, + ignore_repetitions=ignore_repetitions, ) @fill_doc diff --git a/pycrostates/segmentation/tests/test_segmentation.py b/pycrostates/segmentation/tests/test_segmentation.py index 4d6c73cd..f80d1385 100644 --- a/pycrostates/segmentation/tests/test_segmentation.py +++ b/pycrostates/segmentation/tests/test_segmentation.py @@ -266,13 +266,13 @@ def test_invalid_segmentation(Segmentation, inst, bad_inst, caplog): def test_compute_transition_matrix_Raw(): segmentation = ModK_raw.predict(raw) segmentation.compute_transition_matrix() - segmentation.compute_transition_matrix(ignore_self=False) + segmentation.compute_transition_matrix(ignore_repetitions=False) def test_compute_transition_matrix_Epochs(): segmentation = ModK_epochs.predict(epochs) segmentation.compute_transition_matrix() - segmentation.compute_transition_matrix(ignore_self=False) + segmentation.compute_transition_matrix(ignore_repetitions=False) @pytest.mark.parametrize("ModK, inst", [(ModK_raw, raw), (ModK_epochs, epochs)]) @@ -292,13 +292,13 @@ def test_compute_transition_matrix_stat(ModK, inst): def test_compute_expected_transition_matrix_Raw(): segmentation = ModK_raw.predict(raw) segmentation.compute_expected_transition_matrix() - segmentation.compute_expected_transition_matrix(ignore_self=False) + segmentation.compute_expected_transition_matrix(ignore_repetitions=False) def test_compute_expected_transition_matrix_Epochs(): segmentation = ModK_epochs.predict(epochs) segmentation.compute_expected_transition_matrix() - segmentation.compute_expected_transition_matrix(ignore_self=False) + segmentation.compute_expected_transition_matrix(ignore_repetitions=False) @pytest.mark.parametrize("ModK, inst", [(ModK_raw, raw), (ModK_epochs, epochs)]) diff --git a/pycrostates/segmentation/tests/test_transitions.py b/pycrostates/segmentation/tests/test_transitions.py index 5180f6e8..563fdc9f 100644 --- a/pycrostates/segmentation/tests/test_transitions.py +++ b/pycrostates/segmentation/tests/test_transitions.py @@ -14,7 +14,7 @@ @pytest.mark.parametrize( - "labels, ignore_self, T", + "labels, ignore_repetitions, T", [ # Raw ( @@ -72,12 +72,12 @@ ), ], ) -def test_compute_transition_matrix(labels, ignore_self, T): +def test_compute_transition_matrix(labels, ignore_repetitions, T): n_clusters = ( np.unique(labels).size - 1 if np.any(labels == -1) else np.unique(labels).size ) t = _compute_transition_matrix( - labels, n_clusters=n_clusters, ignore_self=ignore_self + labels, n_clusters=n_clusters, ignore_repetitions=ignore_repetitions ) assert isinstance(T, np.ndarray) assert t.shape == (n_clusters, n_clusters) @@ -93,12 +93,12 @@ def test_compute_expected_transition_matrix(): labels_ = labels.copy() np.random.shuffle(labels_) T = _compute_transition_matrix( - labels_, n_clusters, ignore_self=True, stat="probability" + labels_, n_clusters, ignore_repetitions=True, stat="probability" ) Ts.append(T) boostrap_T = np.array(Ts).mean(axis=0) expected_T = _compute_expected_transition_matrix( - labels, n_clusters, ignore_self=True, stat="probability" + labels, n_clusters, ignore_repetitions=True, stat="probability" ) assert_allclose(boostrap_T, expected_T, atol=1e-2) @@ -110,12 +110,12 @@ def test_compute_expected_transition_matrix(): labels_ = labels.copy() np.random.shuffle(labels_) T = _compute_transition_matrix( - labels_, n_clusters, ignore_self=True, stat="probability" + labels_, n_clusters, ignore_repetitions=True, stat="probability" ) Ts.append(T) boostrap_T = np.array(Ts).mean(axis=0) expected_T = _compute_expected_transition_matrix( - labels, n_clusters, ignore_self=True, stat="probability" + labels, n_clusters, ignore_repetitions=True, stat="probability" ) assert_allclose(boostrap_T, expected_T, atol=1e-2) @@ -143,3 +143,17 @@ def test_check_labels_n_clusters(): _check_labels_n_clusters(np.random.randint(0, 5, size=100).astype(float), 5) with pytest.raises(ValueError, match=re.escape("'[6 7]' are invalid")): _check_labels_n_clusters(np.random.randint(0, 8, size=100), 6) + + +def test_deprecated_ignore_self(): + labels = np.random.randint(0, 5, size=100) + + with pytest.warns(DeprecationWarning, match="is deprecated and will be removed"): + M = compute_transition_matrix(labels, 5, ignore_self=True) + M_ = compute_transition_matrix(labels, 5, ignore_repetitions=True) + assert_allclose(M, M_) + + with pytest.warns(DeprecationWarning, match="is deprecated and will be removed"): + M = compute_expected_transition_matrix(labels, 5, ignore_self=True) + M_ = compute_expected_transition_matrix(labels, 5, ignore_repetitions=True) + assert_allclose(M, M_) diff --git a/pycrostates/segmentation/transitions.py b/pycrostates/segmentation/transitions.py index b3c36252..a1d68b24 100644 --- a/pycrostates/segmentation/transitions.py +++ b/pycrostates/segmentation/transitions.py @@ -1,10 +1,12 @@ from itertools import groupby +from typing import Optional import numpy as np from numpy.typing import NDArray from ..utils._checks import _check_type, _check_value from ..utils._docs import fill_doc +from ..utils._fixes import deprecate @fill_doc @@ -12,7 +14,8 @@ def compute_transition_matrix( labels: NDArray[int], n_clusters: int, stat: str = "probability", - ignore_self: bool = True, + ignore_self: Optional[bool] = None, + ignore_repetitions: bool = True, ) -> NDArray[float]: """Compute the observed transition matrix. @@ -25,17 +28,29 @@ def compute_transition_matrix( %(n_clusters)s %(stat_transition)s %(ignore_self)s + %(ignore_repetitions)s Returns ------- %(transition_matrix)s """ _check_labels_n_clusters(labels, n_clusters) + + _check_type( + ignore_self, + (bool, None), + "ignore_self", + ) + _check_type(ignore_repetitions, (bool,), "ignore_repetitions") + if ignore_self is not None: + deprecate("ignore_self", "ignore_repetitions") + ignore_repetitions = ignore_self + return _compute_transition_matrix( labels, n_clusters, stat, - ignore_self, + ignore_repetitions, ) @@ -43,17 +58,17 @@ def _compute_transition_matrix( labels: NDArray[int], n_clusters: int, stat: str = "probability", - ignore_self: bool = True, + ignore_repetitions: bool = True, ) -> NDArray[float]: """Compute observed transition.""" # common error checking _check_value(stat, ("count", "probability", "proportion", "percent"), "stat") - _check_type(ignore_self, (bool,), "ignore_self") + _check_type(ignore_repetitions, (bool,), "ignore_repetitions") # reshape if epochs (returns a view) labels = labels.reshape(-1) # ignore transition to itself (i.e. 1 -> 1) - if ignore_self: + if ignore_repetitions: labels = [s for s, _ in groupby(labels)] T = np.zeros(shape=(n_clusters, n_clusters)) @@ -79,7 +94,8 @@ def compute_expected_transition_matrix( labels: NDArray[int], n_clusters: int, stat: str = "probability", - ignore_self: bool = True, + ignore_self: Optional[bool] = None, + ignore_repetitions: bool = True, ) -> NDArray[float]: """Compute the expected transition matrix. @@ -96,17 +112,32 @@ def compute_expected_transition_matrix( %(n_clusters)s %(stat_expected_transitions)s %(ignore_self)s + %(ignore_repetitions)s Returns ------- %(transition_matrix)s """ _check_labels_n_clusters(labels, n_clusters) + + _check_type( + ignore_self, + ( + bool, + None, + ), + "ignore_self", + ) + _check_type(ignore_repetitions, (bool,), "ignore_repetitions") + if ignore_self is not None: + deprecate("ignore_self", "ignore_repetitions") + ignore_repetitions = ignore_self + return _compute_expected_transition_matrix( labels, n_clusters, stat, - ignore_self, + ignore_repetitions, ) @@ -114,7 +145,7 @@ def _compute_expected_transition_matrix( labels: NDArray[int], n_clusters: int, stat: str = "probability", - ignore_self: bool = True, + ignore_repetitions: bool = True, ) -> NDArray[float]: """Compute theoretical transition matrix. @@ -122,7 +153,7 @@ def _compute_expected_transition_matrix( """ # common error checking _check_value(stat, ("probability", "proportion", "percent"), "stat") - _check_type(ignore_self, (bool,), "ignore_self") + _check_type(ignore_repetitions, (bool,), "ignore_repetitions") # reshape if epochs (returns a view) labels = labels.reshape(-1) @@ -143,7 +174,7 @@ def _compute_expected_transition_matrix( # ignore unlabeled T_expected = T_expected[:-1, :-1] - if ignore_self: + if ignore_repetitions: np.fill_diagonal(T_expected, 0) # transform to probability with np.errstate(divide="ignore", invalid="ignore"): diff --git a/pycrostates/utils/_docs.py b/pycrostates/utils/_docs.py index 7bf23960..956202f1 100644 --- a/pycrostates/utils/_docs.py +++ b/pycrostates/utils/_docs.py @@ -126,8 +126,20 @@ "ignore_self" ] = """ ignore_self : bool - If True, ignores the transition from one state to itself. This is equivalent to - setting the duration of all states to 1 sample.""" + See ``ignore_repetitions`` instead. + + .. deprecated:: 0.4.0 + + This parameter is deprecated and will be removed in future versions. + Please use the ``ignore_repetitions`` parameter instead.""" +docdict[ + "ignore_repetitions" +] = """ +ignore_repetitions : bool + If ``True``, ignores state repetitions. + For example, the input sequence ``AAABBCCD`` + will be transformed into ``ABCD`` before any calculation. + This is equivalent to setting the duration of all states to 1 sample.""" docdict[ "transition_matrix" ] = """ diff --git a/pycrostates/utils/_fixes.py b/pycrostates/utils/_fixes.py index 579aebce..f5bd5635 100644 --- a/pycrostates/utils/_fixes.py +++ b/pycrostates/utils/_fixes.py @@ -1,6 +1,7 @@ """Temporary bug-fixes awaiting an upstream fix.""" import sys +from warnings import warn # https://github.com/sphinx-gallery/sphinx-gallery/issues/1112 @@ -18,3 +19,13 @@ def __getattr__(self, name): # noqa: D105 return getattr(sys.stdout, name) else: raise AttributeError(f"'file' object has not attribute '{name}'") + + +def deprecate(old: str, new: str) -> None: + """Warn about deprecation of an argument.""" + warn( + f"The '{old}' argument is deprecated and will be removed in future " + f"versions. Please use '{new}' instead.", + DeprecationWarning, + stacklevel=2, + ) diff --git a/pyproject.toml b/pyproject.toml index c089febf..cbd98549 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,7 +52,7 @@ maintainers = [ name = 'pycrostates' readme = 'README.md' requires-python = '>=3.9' -version = '0.4.0.dev' +version = '0.4.0' [project.optional-dependencies] all = [ From 137a71025b657155cc999b1e37821400e1ba4e77 Mon Sep 17 00:00:00 2001 From: Mathieu Scheltienne Date: Wed, 8 Nov 2023 14:51:13 +0100 Subject: [PATCH 16/22] fix changelog and path to logo --- docs/source/conf.py | 4 ++-- docs/source/dev/changes/0.4.0.rst | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/source/conf.py b/docs/source/conf.py index 6a195c40..264a9043 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -79,8 +79,8 @@ html_theme = "pydata_sphinx_theme" html_theme_options = { "logo": { - "image_light": "../_static/logos/Pycrostates_logo_black.png", - "image_dark": "../_static/logos/Pycrostates_logo_white.png", + "image_light": "./_static/logos/Pycrostates_logo_black.png", + "image_dark": "./_static/logos/Pycrostates_logo_white.png", }, "icon_links": [ { diff --git a/docs/source/dev/changes/0.4.0.rst b/docs/source/dev/changes/0.4.0.rst index bee5f9aa..3f2cc9aa 100644 --- a/docs/source/dev/changes/0.4.0.rst +++ b/docs/source/dev/changes/0.4.0.rst @@ -23,4 +23,4 @@ Current 0.4.0 ------------- - Compatibility with ``MNE`` 1.6 and above (:pr:`120` and :pr:`124` by `Mathieu Scheltienne`_) -. Add ``show_gradient`` argument (:pr:`111` by `Victor Férat`_) +- Add ``show_gradient`` argument (:pr:`111` by `Victor Férat`_) From 81e37787e42b8d245c0e4e9c60c4b68de0d5b4e7 Mon Sep 17 00:00:00 2001 From: Mathieu Scheltienne Date: Wed, 8 Nov 2023 15:46:52 +0100 Subject: [PATCH 17/22] [MNT] 0.5.dev0 and remove deprecated arguments (#129) * version and changelog * rm deprecated argument * rm unused importlib_resources * fix fname in template changelog * revert for logos? --- docs/source/conf.py | 4 +-- docs/source/dev/changes/0.4.0.rst | 2 -- docs/source/dev/changes/changelog.rst | 1 + docs/source/dev/changes/latest.rst | 33 ++++++++++++++++++ docs/source/dev/changes/latest.rst.template | 2 +- pycrostates/datasets/lemon/lemon.py | 6 +--- pycrostates/segmentation/_base.py | 34 ++----------------- .../segmentation/tests/test_transitions.py | 14 -------- pycrostates/segmentation/transitions.py | 31 ----------------- pycrostates/utils/_docs.py | 10 ------ pycrostates/utils/_fixes.py | 11 ------ pyproject.toml | 3 +- 12 files changed, 41 insertions(+), 110 deletions(-) create mode 100644 docs/source/dev/changes/latest.rst diff --git a/docs/source/conf.py b/docs/source/conf.py index 264a9043..6a195c40 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -79,8 +79,8 @@ html_theme = "pydata_sphinx_theme" html_theme_options = { "logo": { - "image_light": "./_static/logos/Pycrostates_logo_black.png", - "image_dark": "./_static/logos/Pycrostates_logo_white.png", + "image_light": "../_static/logos/Pycrostates_logo_black.png", + "image_dark": "../_static/logos/Pycrostates_logo_white.png", }, "icon_links": [ { diff --git a/docs/source/dev/changes/0.4.0.rst b/docs/source/dev/changes/0.4.0.rst index 3f2cc9aa..f8144829 100644 --- a/docs/source/dev/changes/0.4.0.rst +++ b/docs/source/dev/changes/0.4.0.rst @@ -17,8 +17,6 @@ .. include:: ./names.inc -.. _latest: - Current 0.4.0 ------------- diff --git a/docs/source/dev/changes/changelog.rst b/docs/source/dev/changes/changelog.rst index 04eb9120..7782aebc 100644 --- a/docs/source/dev/changes/changelog.rst +++ b/docs/source/dev/changes/changelog.rst @@ -6,6 +6,7 @@ Changelog .. toctree:: :titlesonly: + latest.rst 0.4.0 0.3.0 0.2.0 diff --git a/docs/source/dev/changes/latest.rst b/docs/source/dev/changes/latest.rst new file mode 100644 index 00000000..e284008b --- /dev/null +++ b/docs/source/dev/changes/latest.rst @@ -0,0 +1,33 @@ +.. NOTE: we use cross-references to highlight new functions and classes. + Please follow the examples below, so the changelog page will have a link to + the function/class documentation. + +.. NOTE: there are 3 separate sections for changes, based on type: + - "Enhancements" for new features + - "Bugs" for bug fixes + - "API changes" for backward-incompatible changes + +.. include:: ./names.inc + +.. _latest: + +Version 0.5 +----------- + +Enhancements +~~~~~~~~~~~~ + +- xxx + +Bugs +~~~~ + +- xxx + +API and behavior changes +~~~~~~~~~~~~~~~~~~~~~~~~ + +- xxx + +Authors +~~~~~~~ diff --git a/docs/source/dev/changes/latest.rst.template b/docs/source/dev/changes/latest.rst.template index c61f1e66..9da8b15b 100644 --- a/docs/source/dev/changes/latest.rst.template +++ b/docs/source/dev/changes/latest.rst.template @@ -7,7 +7,7 @@ - "Bugs" for bug fixes - "API changes" for backward-incompatible changes -.. include:: ./authors.inc +.. include:: ./names.inc .. _latest: diff --git a/pycrostates/datasets/lemon/lemon.py b/pycrostates/datasets/lemon/lemon.py index 08cca02d..8b002dd3 100644 --- a/pycrostates/datasets/lemon/lemon.py +++ b/pycrostates/datasets/lemon/lemon.py @@ -1,10 +1,6 @@ """Functions to use the LEMON dataset.""" -try: - from importlib.resources import files # type: ignore -except ImportError: - from importlib_resources import files # type: ignore - +from importlib.resources import files from pathlib import Path import numpy as np diff --git a/pycrostates/segmentation/_base.py b/pycrostates/segmentation/_base.py index 2de820fc..553a0ecd 100644 --- a/pycrostates/segmentation/_base.py +++ b/pycrostates/segmentation/_base.py @@ -14,7 +14,6 @@ from ..utils import _corr_vectors from ..utils._checks import _check_type from ..utils._docs import fill_doc -from ..utils._fixes import deprecate from ..utils._logs import logger from ..viz import plot_cluster_centers from .transitions import _compute_expected_transition_matrix, _compute_transition_matrix @@ -204,9 +203,7 @@ def compute_parameters(self, norm_gfp: bool = True, return_dist: bool = False): params["unlabeled"] = len(np.argwhere(labels == -1)) / len(gfp) return params - def compute_transition_matrix( - self, stat="probability", ignore_self=None, ignore_repetitions=True - ): + def compute_transition_matrix(self, stat="probability", ignore_repetitions=True): """Compute the observed transition matrix. Count the number of transitions from one state to another and aggregate the @@ -216,7 +213,6 @@ def compute_transition_matrix( Parameters ---------- %(stat_transition)s - %(ignore_self)s %(ignore_repetitions)s Returns @@ -230,18 +226,6 @@ def compute_transition_matrix( with discontinuous data. To avoid this behaviour, make sure to set the ``reject_edges`` parameter to ``True`` when predicting the segmentation. """ - _check_type( - ignore_self, - ( - bool, - None, - ), - "ignore_self", - ) - _check_type(ignore_repetitions, (bool,), "ignore_repetitions") - if ignore_self is not None: - deprecate("ignore_self", "ignore_repetitions") - ignore_repetitions = ignore_self return _compute_transition_matrix( self._labels, self._cluster_centers_.shape[0], @@ -251,7 +235,7 @@ def compute_transition_matrix( @fill_doc def compute_expected_transition_matrix( - self, stat="probability", ignore_self=None, ignore_repetitions=True + self, stat="probability", ignore_repetitions=True ): """Compute the expected transition matrix. @@ -265,26 +249,12 @@ def compute_expected_transition_matrix( Parameters ---------- %(stat_expected_transitions)s - %(ignore_self)s %(ignore_repetitions)s Returns ------- %(transition_matrix)s """ - _check_type( - ignore_self, - ( - bool, - None, - ), - "ignore_self", - ) - _check_type(ignore_repetitions, (bool,), "ignore_repetitions") - if ignore_self is not None: - deprecate("ignore_self", "ignore_repetitions") - ignore_repetitions = ignore_self - return _compute_expected_transition_matrix( self._labels, n_clusters=self._cluster_centers_.shape[0], diff --git a/pycrostates/segmentation/tests/test_transitions.py b/pycrostates/segmentation/tests/test_transitions.py index 563fdc9f..ef607feb 100644 --- a/pycrostates/segmentation/tests/test_transitions.py +++ b/pycrostates/segmentation/tests/test_transitions.py @@ -143,17 +143,3 @@ def test_check_labels_n_clusters(): _check_labels_n_clusters(np.random.randint(0, 5, size=100).astype(float), 5) with pytest.raises(ValueError, match=re.escape("'[6 7]' are invalid")): _check_labels_n_clusters(np.random.randint(0, 8, size=100), 6) - - -def test_deprecated_ignore_self(): - labels = np.random.randint(0, 5, size=100) - - with pytest.warns(DeprecationWarning, match="is deprecated and will be removed"): - M = compute_transition_matrix(labels, 5, ignore_self=True) - M_ = compute_transition_matrix(labels, 5, ignore_repetitions=True) - assert_allclose(M, M_) - - with pytest.warns(DeprecationWarning, match="is deprecated and will be removed"): - M = compute_expected_transition_matrix(labels, 5, ignore_self=True) - M_ = compute_expected_transition_matrix(labels, 5, ignore_repetitions=True) - assert_allclose(M, M_) diff --git a/pycrostates/segmentation/transitions.py b/pycrostates/segmentation/transitions.py index a1d68b24..8026f1df 100644 --- a/pycrostates/segmentation/transitions.py +++ b/pycrostates/segmentation/transitions.py @@ -1,12 +1,10 @@ from itertools import groupby -from typing import Optional import numpy as np from numpy.typing import NDArray from ..utils._checks import _check_type, _check_value from ..utils._docs import fill_doc -from ..utils._fixes import deprecate @fill_doc @@ -14,7 +12,6 @@ def compute_transition_matrix( labels: NDArray[int], n_clusters: int, stat: str = "probability", - ignore_self: Optional[bool] = None, ignore_repetitions: bool = True, ) -> NDArray[float]: """Compute the observed transition matrix. @@ -27,7 +24,6 @@ def compute_transition_matrix( %(labels_transition)s %(n_clusters)s %(stat_transition)s - %(ignore_self)s %(ignore_repetitions)s Returns @@ -35,17 +31,6 @@ def compute_transition_matrix( %(transition_matrix)s """ _check_labels_n_clusters(labels, n_clusters) - - _check_type( - ignore_self, - (bool, None), - "ignore_self", - ) - _check_type(ignore_repetitions, (bool,), "ignore_repetitions") - if ignore_self is not None: - deprecate("ignore_self", "ignore_repetitions") - ignore_repetitions = ignore_self - return _compute_transition_matrix( labels, n_clusters, @@ -94,7 +79,6 @@ def compute_expected_transition_matrix( labels: NDArray[int], n_clusters: int, stat: str = "probability", - ignore_self: Optional[bool] = None, ignore_repetitions: bool = True, ) -> NDArray[float]: """Compute the expected transition matrix. @@ -111,7 +95,6 @@ def compute_expected_transition_matrix( %(labels_transition)s %(n_clusters)s %(stat_expected_transitions)s - %(ignore_self)s %(ignore_repetitions)s Returns @@ -119,20 +102,6 @@ def compute_expected_transition_matrix( %(transition_matrix)s """ _check_labels_n_clusters(labels, n_clusters) - - _check_type( - ignore_self, - ( - bool, - None, - ), - "ignore_self", - ) - _check_type(ignore_repetitions, (bool,), "ignore_repetitions") - if ignore_self is not None: - deprecate("ignore_self", "ignore_repetitions") - ignore_repetitions = ignore_self - return _compute_expected_transition_matrix( labels, n_clusters, diff --git a/pycrostates/utils/_docs.py b/pycrostates/utils/_docs.py index 956202f1..dc326f3f 100644 --- a/pycrostates/utils/_docs.py +++ b/pycrostates/utils/_docs.py @@ -122,16 +122,6 @@ the first axis is always equal to ``1``. * ``percent``: normalize count such as the probabilities along the first axis is always equal to ``100``.""" -docdict[ - "ignore_self" -] = """ -ignore_self : bool - See ``ignore_repetitions`` instead. - - .. deprecated:: 0.4.0 - - This parameter is deprecated and will be removed in future versions. - Please use the ``ignore_repetitions`` parameter instead.""" docdict[ "ignore_repetitions" ] = """ diff --git a/pycrostates/utils/_fixes.py b/pycrostates/utils/_fixes.py index f5bd5635..579aebce 100644 --- a/pycrostates/utils/_fixes.py +++ b/pycrostates/utils/_fixes.py @@ -1,7 +1,6 @@ """Temporary bug-fixes awaiting an upstream fix.""" import sys -from warnings import warn # https://github.com/sphinx-gallery/sphinx-gallery/issues/1112 @@ -19,13 +18,3 @@ def __getattr__(self, name): # noqa: D105 return getattr(sys.stdout, name) else: raise AttributeError(f"'file' object has not attribute '{name}'") - - -def deprecate(old: str, new: str) -> None: - """Warn about deprecation of an argument.""" - warn( - f"The '{old}' argument is deprecated and will be removed in future " - f"versions. Please use '{new}' instead.", - DeprecationWarning, - stacklevel=2, - ) diff --git a/pyproject.toml b/pyproject.toml index cbd98549..142b5fc5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -23,7 +23,6 @@ classifiers = [ ] dependencies = [ 'decorator', - 'importlib-resources; python_version < "3.9"', 'jinja2', 'joblib', 'matplotlib>=3.5', @@ -52,7 +51,7 @@ maintainers = [ name = 'pycrostates' readme = 'README.md' requires-python = '>=3.9' -version = '0.4.0' +version = '0.5.0.dev0' [project.optional-dependencies] all = [ From 9f5cc5f1190160d760748de4ffe74f4090159a7d Mon Sep 17 00:00:00 2001 From: Mathieu Scheltienne Date: Wed, 8 Nov 2023 15:53:48 +0100 Subject: [PATCH 18/22] fix more --- pycrostates/segmentation/_base.py | 14 +------------- pycrostates/segmentation/transitions.py | 22 ---------------------- 2 files changed, 1 insertion(+), 35 deletions(-) diff --git a/pycrostates/segmentation/_base.py b/pycrostates/segmentation/_base.py index a859cc34..de02edaa 100644 --- a/pycrostates/segmentation/_base.py +++ b/pycrostates/segmentation/_base.py @@ -228,18 +228,6 @@ def compute_transition_matrix(self, stat="probability", ignore_repetitions=True) with discontinuous data. To avoid this behaviour, make sure to set the ``reject_edges`` parameter to ``True`` when predicting the segmentation. """ - _check_type( - ignore_self, - ( - bool, - None, - ), - "ignore_self", - ) - _check_type(ignore_repetitions, (bool,), "ignore_repetitions") - if ignore_self is not None: - deprecate("ignore_self", "ignore_repetitions") - ignore_repetitions = ignore_self return _compute_transition_matrix( self._labels, self._cluster_centers_.shape[0], @@ -283,7 +271,7 @@ def entropy( ignore_repetitions: bool = False, log_base: Union[float, str] = 2, ): - """Compute the Shannon entropy of the segmentation. + r"""Compute the Shannon entropy of the segmentation. Compute the Shannon entropy\ :footcite:p:`shannon1948mathematical` of the microstate symbolic sequence. diff --git a/pycrostates/segmentation/transitions.py b/pycrostates/segmentation/transitions.py index f912e3b6..05ce0a1a 100644 --- a/pycrostates/segmentation/transitions.py +++ b/pycrostates/segmentation/transitions.py @@ -1,5 +1,4 @@ from itertools import groupby -from typing import Optional import numpy as np from numpy.typing import NDArray @@ -33,15 +32,6 @@ def compute_transition_matrix( %(transition_matrix)s """ _check_labels_n_clusters(labels, n_clusters) - _check_type( - ignore_self, - (bool, None), - "ignore_self", - ) - _check_type(ignore_repetitions, (bool,), "ignore_repetitions") - if ignore_self is not None: - deprecate("ignore_self", "ignore_repetitions") - ignore_repetitions = ignore_self return _compute_transition_matrix( labels, n_clusters, @@ -113,18 +103,6 @@ def compute_expected_transition_matrix( %(transition_matrix)s """ _check_labels_n_clusters(labels, n_clusters) - _check_type( - ignore_self, - ( - bool, - None, - ), - "ignore_self", - ) - _check_type(ignore_repetitions, (bool,), "ignore_repetitions") - if ignore_self is not None: - deprecate("ignore_self", "ignore_repetitions") - ignore_repetitions = ignore_self return _compute_expected_transition_matrix( labels, n_clusters, From 95cd822debb93acc94c5698d7268065a524d3855 Mon Sep 17 00:00:00 2001 From: Mathieu Scheltienne Date: Wed, 8 Nov 2023 15:54:11 +0100 Subject: [PATCH 19/22] fix ruff --- pycrostates/segmentation/_base.py | 1 - pycrostates/segmentation/transitions.py | 1 - 2 files changed, 2 deletions(-) diff --git a/pycrostates/segmentation/_base.py b/pycrostates/segmentation/_base.py index de02edaa..c7395bfc 100644 --- a/pycrostates/segmentation/_base.py +++ b/pycrostates/segmentation/_base.py @@ -14,7 +14,6 @@ from ..utils import _corr_vectors from ..utils._checks import _check_type from ..utils._docs import fill_doc -from ..utils._fixes import deprecate from ..utils._logs import logger from ..viz import plot_cluster_centers from .entropy import entropy diff --git a/pycrostates/segmentation/transitions.py b/pycrostates/segmentation/transitions.py index 05ce0a1a..8026f1df 100644 --- a/pycrostates/segmentation/transitions.py +++ b/pycrostates/segmentation/transitions.py @@ -5,7 +5,6 @@ from ..utils._checks import _check_type, _check_value from ..utils._docs import fill_doc -from ..utils._fixes import deprecate @fill_doc From bef91557e9793a620f6fc58f593fd37a0ce483c0 Mon Sep 17 00:00:00 2001 From: Mathieu Scheltienne Date: Wed, 8 Nov 2023 15:58:06 +0100 Subject: [PATCH 20/22] skip bib files in codespell --- .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 3f1e59a5..61def20e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -26,7 +26,7 @@ repos: rev: v2.2.6 hooks: - id: codespell - args: [--check-filenames, --ignore-words=.codespellignore] + args: [--check-filenames, --ignore-words=.codespellignore, --skip=*.bib] - repo: https://github.com/pycqa/pydocstyle rev: 6.3.0 From 3c72e6ff0e110229d14d5cffa3d5a1407d6e3831 Mon Sep 17 00:00:00 2001 From: Mathieu Scheltienne Date: Wed, 8 Nov 2023 16:04:51 +0100 Subject: [PATCH 21/22] rm deprecation test function --- pycrostates/segmentation/tests/test_transitions.py | 14 -------------- pycrostates/utils/_fixes.py | 11 ----------- 2 files changed, 25 deletions(-) diff --git a/pycrostates/segmentation/tests/test_transitions.py b/pycrostates/segmentation/tests/test_transitions.py index 563fdc9f..ef607feb 100644 --- a/pycrostates/segmentation/tests/test_transitions.py +++ b/pycrostates/segmentation/tests/test_transitions.py @@ -143,17 +143,3 @@ def test_check_labels_n_clusters(): _check_labels_n_clusters(np.random.randint(0, 5, size=100).astype(float), 5) with pytest.raises(ValueError, match=re.escape("'[6 7]' are invalid")): _check_labels_n_clusters(np.random.randint(0, 8, size=100), 6) - - -def test_deprecated_ignore_self(): - labels = np.random.randint(0, 5, size=100) - - with pytest.warns(DeprecationWarning, match="is deprecated and will be removed"): - M = compute_transition_matrix(labels, 5, ignore_self=True) - M_ = compute_transition_matrix(labels, 5, ignore_repetitions=True) - assert_allclose(M, M_) - - with pytest.warns(DeprecationWarning, match="is deprecated and will be removed"): - M = compute_expected_transition_matrix(labels, 5, ignore_self=True) - M_ = compute_expected_transition_matrix(labels, 5, ignore_repetitions=True) - assert_allclose(M, M_) diff --git a/pycrostates/utils/_fixes.py b/pycrostates/utils/_fixes.py index f5bd5635..579aebce 100644 --- a/pycrostates/utils/_fixes.py +++ b/pycrostates/utils/_fixes.py @@ -1,7 +1,6 @@ """Temporary bug-fixes awaiting an upstream fix.""" import sys -from warnings import warn # https://github.com/sphinx-gallery/sphinx-gallery/issues/1112 @@ -19,13 +18,3 @@ def __getattr__(self, name): # noqa: D105 return getattr(sys.stdout, name) else: raise AttributeError(f"'file' object has not attribute '{name}'") - - -def deprecate(old: str, new: str) -> None: - """Warn about deprecation of an argument.""" - warn( - f"The '{old}' argument is deprecated and will be removed in future " - f"versions. Please use '{new}' instead.", - DeprecationWarning, - stacklevel=2, - ) From deec3c80c3ee63af742fa6e6a9e6530ad3bca842 Mon Sep 17 00:00:00 2001 From: Mathieu Scheltienne Date: Fri, 10 Nov 2023 16:20:51 +0100 Subject: [PATCH 22/22] Remove deprecated type-hints (#130) * rm deprecated type-hints * fix FutureWarning mne 1.7 epochs view/copy * use a copy * Revert "use a copy" This reverts commit 10bc139aafce51c58990e27dae51b0af5490720b. * fix * fix variable name --- pycrostates/cluster/_base.py | 22 +++++++++++----------- pycrostates/cluster/aahc.py | 6 +++--- pycrostates/cluster/kmeans.py | 6 +++--- pycrostates/io/fiff.py | 8 ++++---- pycrostates/io/meas_info.py | 12 ++++++------ pycrostates/preprocessing/resample.py | 4 ++-- pycrostates/segmentation/_base.py | 12 +++++++----- pycrostates/utils/_docs.py | 10 +++++----- pycrostates/utils/sys_info.py | 6 +++--- pycrostates/viz/cluster_centers.py | 8 ++++---- pycrostates/viz/segmentation.py | 11 ++++++----- 11 files changed, 54 insertions(+), 51 deletions(-) diff --git a/pycrostates/cluster/_base.py b/pycrostates/cluster/_base.py index 187b7b83..aaf3dfac 100644 --- a/pycrostates/cluster/_base.py +++ b/pycrostates/cluster/_base.py @@ -2,7 +2,7 @@ from copy import copy, deepcopy from itertools import groupby from pathlib import Path -from typing import Any, Dict, List, Optional, Tuple, Union +from typing import Any, Optional, Union import numpy as np from matplotlib.axes import Axes @@ -277,11 +277,11 @@ def fit( def rename_clusters( self, - mapping: Optional[Dict[str, str]] = None, + mapping: Optional[dict[str, str]] = None, new_names: Optional[ Union[ - List[str], - Tuple[str, ...], + list[str], + tuple[str, ...], ] ] = None, ) -> None: @@ -341,11 +341,11 @@ def rename_clusters( def reorder_clusters( self, - mapping: Optional[Dict[int, int]] = None, + mapping: Optional[dict[int, int]] = None, order: Optional[ Union[ - List[int], - Tuple[int, ...], + list[int], + tuple[int, ...], NDArray[int], ] ] = None, @@ -458,8 +458,8 @@ def invert_polarity( self, invert: Union[ bool, - List[bool], - Tuple[bool, ...], + list[bool], + tuple[bool, ...], NDArray[bool], ], ) -> None: @@ -518,7 +518,7 @@ def plot( self, axes: Optional[Union[Axes, NDArray[Axes]]] = None, show_gradient: Optional[bool] = False, - gradient_kwargs: Dict[str, Any] = { + gradient_kwargs: dict[str, Any] = { "color": "black", "linestyle": "-", "marker": "P", @@ -1180,7 +1180,7 @@ def labels_(self) -> NDArray[int]: return self._labels_.copy() @property - def cluster_names(self) -> List[str]: + def cluster_names(self) -> list[str]: """Name of the clusters. :type: `list` diff --git a/pycrostates/cluster/aahc.py b/pycrostates/cluster/aahc.py index 87099b16..75df046e 100644 --- a/pycrostates/cluster/aahc.py +++ b/pycrostates/cluster/aahc.py @@ -1,7 +1,7 @@ """Atomize and Agglomerate Hierarchical Clustering (AAHC).""" from pathlib import Path -from typing import Any, Optional, Tuple, Union +from typing import Any, Optional, Union import numpy as np from mne import BaseEpochs @@ -183,7 +183,7 @@ def _aahc( n_clusters: int, ignore_polarity: bool, normalize_input: bool, - ) -> Tuple[float, NDArray[float], NDArray[int]]: + ) -> tuple[float, NDArray[float], NDArray[int]]: """Run the AAHC algorithm.""" gfp_sum_sq = np.sum(data**2) maps, segmentation = AAHCluster._compute_maps( @@ -200,7 +200,7 @@ def _compute_maps( n_clusters: int, ignore_polarity: bool, normalize_input: bool, - ) -> Tuple[NDArray[float], NDArray[int]]: + ) -> tuple[NDArray[float], NDArray[int]]: """Compute microstates maps.""" n_chan, n_frame = data.shape diff --git a/pycrostates/cluster/kmeans.py b/pycrostates/cluster/kmeans.py index c7de600c..b69d4081 100644 --- a/pycrostates/cluster/kmeans.py +++ b/pycrostates/cluster/kmeans.py @@ -1,7 +1,7 @@ """Class and functions to use modified Kmeans.""" from pathlib import Path -from typing import Any, Optional, Tuple, Union +from typing import Any, Optional, Union import numpy as np from mne import BaseEpochs @@ -263,7 +263,7 @@ def _kmeans( max_iter: int, random_state: Union[RandomState, Generator], tol: Union[int, float], - ) -> Tuple[float, NDArray[float], NDArray[int], bool]: + ) -> tuple[float, NDArray[float], NDArray[int], bool]: """Run the k-means algorithm.""" gfp_sum_sq = np.sum(data**2) maps, converged = ModKMeans._compute_maps( @@ -282,7 +282,7 @@ def _compute_maps( max_iter: int, random_state: Union[RandomState, Generator], tol: Union[int, float], - ) -> Tuple[NDArray[float], bool]: + ) -> tuple[NDArray[float], bool]: """Compute microstates maps. Based on mne_microstates by Marijn van Vliet diff --git a/pycrostates/io/fiff.py b/pycrostates/io/fiff.py index bee0bc9e..3b56dbfa 100644 --- a/pycrostates/io/fiff.py +++ b/pycrostates/io/fiff.py @@ -5,7 +5,7 @@ from functools import reduce from numbers import Integral from pathlib import Path -from typing import List, Union +from typing import Union import numpy as np from mne import Info, Transform @@ -98,7 +98,7 @@ def _write_cluster( cluster_centers_: NDArray[float], chinfo: Union[CHInfo, Info], algorithm: str, - cluster_names: List[str], + cluster_names: list[str], fitted_data: NDArray[float], labels_: NDArray[int], **kwargs, @@ -398,7 +398,7 @@ def _check_fit_parameters_and_variables( def _create_ModKMeans( cluster_centers_: NDArray[float], info: CHInfo, - cluster_names: List[str], + cluster_names: list[str], fitted_data: NDArray[float], labels_: NDArray[int], n_init: int, @@ -423,7 +423,7 @@ def _create_ModKMeans( def _create_AAHCluster( cluster_centers_: NDArray[float], info: CHInfo, - cluster_names: List[str], + cluster_names: list[str], fitted_data: NDArray[float], labels_: NDArray[int], ignore_polarity: bool, # pylint: disable=unused-argument diff --git a/pycrostates/io/meas_info.py b/pycrostates/io/meas_info.py index 41261a57..91a6ec54 100644 --- a/pycrostates/io/meas_info.py +++ b/pycrostates/io/meas_info.py @@ -2,7 +2,7 @@ from copy import deepcopy from numbers import Number -from typing import List, Optional, Tuple, Union +from typing import Optional, Union import numpy as np from mne import Info, Projection, Transform @@ -155,11 +155,11 @@ def __init__( ch_names: Optional[ Union[ int, - List[str], - Tuple[str, ...], + list[str], + tuple[str, ...], ] ] = None, - ch_types: Optional[Union[str, List[str], Tuple[str, ...]]] = None, + ch_types: Optional[Union[str, list[str], tuple[str, ...]]] = None, ): if all(arg is None for arg in (info, ch_names, ch_types)): raise RuntimeError( @@ -195,8 +195,8 @@ def _init_from_info(self, info: Info): def _init_from_channels( self, - ch_names: Union[int, List[str], Tuple[str, ...]], - ch_types: Union[str, List[str], Tuple[str, ...]], + ch_names: Union[int, list[str], tuple[str, ...]], + ch_types: Union[str, list[str], tuple[str, ...]], ): """Init instance from channel names and types.""" self._unlocked = True diff --git a/pycrostates/preprocessing/resample.py b/pycrostates/preprocessing/resample.py index 7b2b6c54..eadcba7a 100644 --- a/pycrostates/preprocessing/resample.py +++ b/pycrostates/preprocessing/resample.py @@ -1,6 +1,6 @@ """Preprocessing functions to create resamples from raw or epochs instances.""" -from typing import List, Optional, Union +from typing import Optional, Union import numpy as np from mne import BaseEpochs, pick_info @@ -37,7 +37,7 @@ def resample( replace: bool = True, random_state: RANDomState = None, verbose=None, -) -> List[CHData]: +) -> list[CHData]: """Resample a recording into epochs of random samples. Resample :class:`~mne.io.Raw`. :class:`~mne.Epochs` or diff --git a/pycrostates/segmentation/_base.py b/pycrostates/segmentation/_base.py index 553a0ecd..41ed5a37 100644 --- a/pycrostates/segmentation/_base.py +++ b/pycrostates/segmentation/_base.py @@ -2,12 +2,13 @@ import itertools from abc import abstractmethod -from typing import List, Optional, Union +from typing import Optional, Union import numpy as np from matplotlib.axes import Axes from mne import BaseEpochs from mne.io import BaseRaw +from mne.utils import check_version from numpy.typing import NDArray from .._typing import Segmentation @@ -40,7 +41,7 @@ def __init__( labels: NDArray[int], inst: Union[BaseRaw, BaseEpochs], cluster_centers_: NDArray[float], - cluster_names: Optional[List[str]] = None, + cluster_names: Optional[list[str]] = None, predict_parameters: Optional[dict] = None, ): # check input @@ -141,7 +142,8 @@ def compute_parameters(self, norm_gfp: bool = True, return_dist: bool = False): assert data.ndim == 2 assert labels.size == data.shape[1] elif isinstance(self._inst, BaseEpochs): - data = self._inst.get_data() + kwargs_epochs = dict(copy=False) if check_version("mne", "1.6") else dict() + data = self._inst.get_data(**kwargs_epochs) # sanity-checks assert labels.ndim == 2 assert data.ndim == 3 @@ -291,7 +293,7 @@ def plot_cluster_centers( # -------------------------------------------------------------------- @staticmethod def _check_cluster_names( - cluster_names: List[str], + cluster_names: list[str], cluster_centers_: NDArray[float], ): """Check that the argument 'cluster_names' is valid.""" @@ -367,7 +369,7 @@ def cluster_centers_(self) -> NDArray[float]: return self._cluster_centers_.copy() @property - def cluster_names(self) -> List[str]: + def cluster_names(self) -> list[str]: """Name of the cluster centers. :type: `list` diff --git a/pycrostates/utils/_docs.py b/pycrostates/utils/_docs.py index dc326f3f..7e080d96 100644 --- a/pycrostates/utils/_docs.py +++ b/pycrostates/utils/_docs.py @@ -5,15 +5,15 @@ Inspired from mne.utils.docs.py by Eric Larson """ import sys -from typing import Callable, Dict, List, Tuple +from typing import Callable from mne.utils.docs import docdict as docdict_mne # ------------------------- Documentation dictionary ------------------------- -docdict: Dict[str, str] = {} +docdict: dict[str, str] = {} # ---- Documentation to inc. from MNE ---- -keys: Tuple[str, ...] = ( +keys: tuple[str, ...] = ( "n_jobs", "picks_all", "random_state", @@ -169,7 +169,7 @@ axes.""" # ------------------------- Documentation functions -------------------------- -docdict_indented: Dict[int, Dict[str, str]] = {} +docdict_indented: dict[int, dict[str, str]] = {} def fill_doc(f: Callable) -> Callable: @@ -215,7 +215,7 @@ def fill_doc(f: Callable) -> Callable: return f -def _indentcount_lines(lines: List[str]) -> int: +def _indentcount_lines(lines: list[str]) -> int: """Minimum indent for all lines in line list. >>> lines = [' one', ' two', ' three'] diff --git a/pycrostates/utils/sys_info.py b/pycrostates/utils/sys_info.py index 34ed5af9..710d4e0d 100644 --- a/pycrostates/utils/sys_info.py +++ b/pycrostates/utils/sys_info.py @@ -2,7 +2,7 @@ import sys from functools import partial from importlib.metadata import requires, version -from typing import IO, Callable, List, Optional +from typing import IO, Callable, Optional import psutil from packaging.requirements import Requirement @@ -71,14 +71,14 @@ def sys_info(fid: Optional[IO] = None, developer: bool = False): def _list_dependencies_info( - out: Callable, ljust: int, package: str, dependencies: List[Requirement] + out: Callable, ljust: int, package: str, dependencies: list[Requirement] ): """List dependencies names and versions.""" unicode = sys.stdout.encoding.lower().startswith("utf") if unicode: ljust += 1 - not_found: List[Requirement] = list() + not_found: list[Requirement] = list() for dep in dependencies: if dep.name == package: continue diff --git a/pycrostates/viz/cluster_centers.py b/pycrostates/viz/cluster_centers.py index 8a27c675..9d833313 100644 --- a/pycrostates/viz/cluster_centers.py +++ b/pycrostates/viz/cluster_centers.py @@ -1,6 +1,6 @@ """Visualization module for plotting cluster centers.""" -from typing import Any, Dict, List, Optional, Union +from typing import Any, Optional, Union import numpy as np from matplotlib import pyplot as plt @@ -15,7 +15,7 @@ from ..utils._docs import fill_doc from ..utils._logs import logger, verbose -_GRADIENT_KWARGS_DEFAULTS: Dict[str, str] = { +_GRADIENT_KWARGS_DEFAULTS: dict[str, str] = { "color": "black", "linestyle": "-", "marker": "P", @@ -27,10 +27,10 @@ def plot_cluster_centers( cluster_centers: NDArray[float], info: Union[Info, CHInfo], - cluster_names: List[str] = None, + cluster_names: list[str] = None, axes: Optional[Union[Axes, NDArray[Axes]]] = None, show_gradient: Optional[bool] = False, - gradient_kwargs: Dict[str, Any] = _GRADIENT_KWARGS_DEFAULTS, + gradient_kwargs: dict[str, Any] = _GRADIENT_KWARGS_DEFAULTS, *, block: bool = False, verbose: Optional[str] = None, diff --git a/pycrostates/viz/segmentation.py b/pycrostates/viz/segmentation.py index 5e226278..1b8f7c48 100644 --- a/pycrostates/viz/segmentation.py +++ b/pycrostates/viz/segmentation.py @@ -1,6 +1,6 @@ """Visualisation module for plotting segmentations.""" -from typing import List, Optional, Union +from typing import Optional, Union import numpy as np from matplotlib import colormaps, colors @@ -21,7 +21,7 @@ def plot_raw_segmentation( labels: NDArray[int], raw: BaseRaw, n_clusters: int, - cluster_names: List[str] = None, + cluster_names: list[str] = None, tmin: Optional[Union[int, float]] = None, tmax: Optional[Union[int, float]] = None, cmap: Optional[str] = None, @@ -107,7 +107,7 @@ def plot_epoch_segmentation( labels: NDArray[int], epochs: BaseEpochs, n_clusters: int, - cluster_names: List[str] = None, + cluster_names: list[str] = None, cmap: Optional[str] = None, axes: Optional[Axes] = None, cbar_axes: Optional[Axes] = None, @@ -145,7 +145,8 @@ def plot_epoch_segmentation( _check_type(epochs, (BaseEpochs,), "epochs") _check_type(block, (bool,), "block") - data = epochs.get_data().swapaxes(0, 1) + kwargs_epochs = dict(copy=False) if check_version("mne", "1.6") else dict() + data = epochs.get_data(**kwargs_epochs).swapaxes(0, 1) data = data.reshape(data.shape[0], -1) gfp = np.std(data, axis=0) times = np.arange(0, data.shape[-1]) @@ -199,7 +200,7 @@ def _plot_segmentation( gfp: NDArray[float], times: NDArray[float], n_clusters: int, - cluster_names: List[str] = None, + cluster_names: list[str] = None, cmap: Optional[Union[str, colors.Colormap]] = None, axes: Optional[Axes] = None, cbar_axes: Optional[Axes] = None,