From 7f063775da973eeb486e621f5899b42fffd1fcee Mon Sep 17 00:00:00 2001 From: maestroque Date: Mon, 27 May 2024 18:35:49 +0300 Subject: [PATCH 01/15] Add .pre-commit-config.yaml --- .pre-commit-config.yaml | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..f3ead10 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,38 @@ +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-added-large-files + - id: check-case-conflict + - id: check-merge-conflict +- repo: https://github.com/psf/black + rev: 23.11.0 + hooks: + - id: black +- repo: https://github.com/pycqa/isort + rev: 5.13.0 + hooks: + - id: isort +- repo: https://github.com/pycqa/flake8 + rev: 6.1.0 + hooks: + - id: flake8 +- repo: https://github.com/pycqa/pydocstyle + rev: 6.3.0 + hooks: + - id: pydocstyle +- repo: https://github.com/pre-commit/pygrep-hooks + rev: v1.10.0 + hooks: + - id: rst-backticks + - id: rst-directive-colons + - id: rst-inline-touching-normal +- repo: https://github.com/codespell-project/codespell + rev: v2.2.6 + hooks: + - id: codespell \ No newline at end of file From 2b8e4269e7cc65bc4f79688fc25e52990b58e0f1 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 27 May 2024 15:41:21 +0000 Subject: [PATCH 02/15] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- .github/ISSUE_TEMPLATE/ISSUE_TEMPLATE_BUGS.md | 2 +- .../ISSUE_TEMPLATE_DISCUSSION.md | 8 +- .../ISSUE_TEMPLATE/ISSUE_TEMPLATE_GENERAL.md | 6 +- .../ISSUE_TEMPLATE/ISSUE_TEMPLATE_MEETING.md | 2 +- .pre-commit-config.yaml | 2 +- README.md | 2 +- docs/conf.py | 51 ++-- docs/requirements.txt | 1 - peakdet/__init__.py | 32 +- peakdet/_version.py | 154 ++++++---- peakdet/analytics.py | 23 +- peakdet/cli/run.py | 280 +++++++++++------- peakdet/editor.py | 138 +++++---- peakdet/external.py | 20 +- peakdet/io.py | 91 +++--- peakdet/modalities.py | 19 +- peakdet/operations.py | 69 +++-- peakdet/physio.py | 120 ++++---- peakdet/tests/__init__.py | 2 +- peakdet/tests/test_analytics.py | 22 +- peakdet/tests/test_editor.py | 9 +- peakdet/tests/test_external.py | 16 +- peakdet/tests/test_io.py | 61 ++-- peakdet/tests/test_operations.py | 23 +- peakdet/tests/test_physio.py | 102 ++++--- peakdet/tests/test_utils.py | 56 ++-- peakdet/tests/utils.py | 30 +- peakdet/utils.py | 51 ++-- setup.cfg | 2 +- setup.py | 15 +- versioneer.py | 267 ++++++++++------- 31 files changed, 981 insertions(+), 695 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/ISSUE_TEMPLATE_BUGS.md b/.github/ISSUE_TEMPLATE/ISSUE_TEMPLATE_BUGS.md index 7405af4..b8288db 100644 --- a/.github/ISSUE_TEMPLATE/ISSUE_TEMPLATE_BUGS.md +++ b/.github/ISSUE_TEMPLATE/ISSUE_TEMPLATE_BUGS.md @@ -26,7 +26,7 @@ assignees: ## Specifications - - Python version: + - Python version: - phys2bids version: - Platform: diff --git a/.github/ISSUE_TEMPLATE/ISSUE_TEMPLATE_DISCUSSION.md b/.github/ISSUE_TEMPLATE/ISSUE_TEMPLATE_DISCUSSION.md index 35a198c..b396bee 100644 --- a/.github/ISSUE_TEMPLATE/ISSUE_TEMPLATE_DISCUSSION.md +++ b/.github/ISSUE_TEMPLATE/ISSUE_TEMPLATE_DISCUSSION.md @@ -1,6 +1,6 @@ --- name: Discussion -about: Use this template to start a discussion issue, i.e. an issue that +about: Use this template to start a discussion issue, i.e. an issue that title: '' labels: Discussion assignees: '' @@ -22,6 +22,6 @@ I'm opening this discussion because/I think that/I noticed that... - - - - - - + - + - + - diff --git a/.github/ISSUE_TEMPLATE/ISSUE_TEMPLATE_GENERAL.md b/.github/ISSUE_TEMPLATE/ISSUE_TEMPLATE_GENERAL.md index c52035f..957ba84 100644 --- a/.github/ISSUE_TEMPLATE/ISSUE_TEMPLATE_GENERAL.md +++ b/.github/ISSUE_TEMPLATE/ISSUE_TEMPLATE_GENERAL.md @@ -18,6 +18,6 @@ assignees: '' ## Next Steps - * - * - * + * + * + * diff --git a/.github/ISSUE_TEMPLATE/ISSUE_TEMPLATE_MEETING.md b/.github/ISSUE_TEMPLATE/ISSUE_TEMPLATE_MEETING.md index 9e50cfd..8f0da87 100644 --- a/.github/ISSUE_TEMPLATE/ISSUE_TEMPLATE_MEETING.md +++ b/.github/ISSUE_TEMPLATE/ISSUE_TEMPLATE_MEETING.md @@ -8,6 +8,6 @@ assignees: smoia The call will take place over **Skype** [here](https://join.skype.com/Rm4B7R30TG6g). -Find the agenda [here]() and don't be scared to add/suggest/press for topics! +Find the agenda [here]() and don't be scared to add/suggest/press for topics! Old minutes are [here](https://drive.google.com/open?id=1zfc-hgRcU1k2XdqKD8v6TGwQX_jeCUea) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f3ead10..e5b975d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -35,4 +35,4 @@ repos: - repo: https://github.com/codespell-project/codespell rev: v2.2.6 hooks: - - id: codespell \ No newline at end of file + - id: codespell diff --git a/README.md b/README.md index 4566ebf..489b3f7 100644 --- a/README.md +++ b/README.md @@ -72,4 +72,4 @@ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/d -This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! \ No newline at end of file +This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome! diff --git a/docs/conf.py b/docs/conf.py index 3242e78..1bbe315 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -10,19 +10,22 @@ # documentation root, use os.path.abspath to make it absolute, like shown here. import os import sys + import matplotlib as mpl -mpl.use('Agg') + +mpl.use("Agg") # -- Project information ----------------------------------------------------- # Add project name, copyright holder, and author(s) -project = 'peakdet' -copyright = '2018, peakdet developers' -author = 'Ross Markello' +project = "peakdet" +copyright = "2018, peakdet developers" +author = "Ross Markello" # Import project to get version info sys.path.insert(0, os.path.abspath(os.path.pardir)) import peakdet # noqa + # The short X.Y version version = peakdet.__version__ # The full version, including alpha/beta/rc tags @@ -34,14 +37,14 @@ # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ - 'matplotlib.sphinxext.plot_directive', - 'sphinx.ext.autodoc', - 'sphinx.ext.autosummary', - 'sphinx.ext.doctest', - 'sphinx.ext.intersphinx', - 'sphinx.ext.mathjax', - 'sphinx.ext.napoleon', - 'sphinx.ext.viewcode', + "matplotlib.sphinxext.plot_directive", + "sphinx.ext.autodoc", + "sphinx.ext.autosummary", + "sphinx.ext.doctest", + "sphinx.ext.intersphinx", + "sphinx.ext.mathjax", + "sphinx.ext.napoleon", + "sphinx.ext.viewcode", ] # Generate the API documentation when building @@ -50,13 +53,13 @@ autoclass_content = "class" # Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] +templates_path = ["_templates"] # The suffix(es) of source filenames. -source_suffix = '.rst' +source_suffix = ".rst" # The master toctree document. -master_doc = 'index' +master_doc = "index" # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. @@ -68,17 +71,18 @@ # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. # This pattern also affects html_static_path and html_extra_path . -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] +exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"] # The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' +pygments_style = "sphinx" # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. import sphinx_rtd_theme # noqa -html_theme = 'sphinx_rtd_theme' + +html_theme = "sphinx_rtd_theme" html_show_sourcelink = False # Theme options are theme-specific and customize the look and feel of a theme @@ -89,18 +93,18 @@ # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = ["_static"] # -- Options for HTMLHelp output --------------------------------------------- # Output file base name for HTML help builder. -htmlhelp_basename = 'peakdetdoc' +htmlhelp_basename = "peakdetdoc" # -- Extension configuration ------------------------------------------------- intersphinx_mapping = { - 'matplotlib': ('https://matplotlib.org', None), - 'numpy': ('https://docs.scipy.org/doc/numpy', None), - 'scipy': ('https://docs.scipy.org/doc/scipy/reference', None), + "matplotlib": ("https://matplotlib.org", None), + "numpy": ("https://docs.scipy.org/doc/numpy", None), + "scipy": ("https://docs.scipy.org/doc/scipy/reference", None), } doctest_global_setup = """ @@ -110,6 +114,7 @@ """ from peakdet.tests.utils import get_test_data_path # noqa + plot_working_directory = get_test_data_path() plot_include_source = True plot_formats = [("png", 90)] diff --git a/docs/requirements.txt b/docs/requirements.txt index af6baa4..e1a64db 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -2,4 +2,3 @@ pandas sphinx>=1.2 sphinx_rtd_theme - diff --git a/peakdet/__init__.py b/peakdet/__init__.py index 8780359..bd9674d 100644 --- a/peakdet/__init__.py +++ b/peakdet/__init__.py @@ -1,18 +1,30 @@ __all__ = [ - 'delete_peaks', 'edit_physio', 'filter_physio', 'interpolate_physio', - 'peakfind_physio', 'plot_physio', 'reject_peaks', - 'load_physio', 'save_physio', 'load_history', 'save_history', - 'load_rtpeaks', 'Physio', 'HRV', '__version__' + "delete_peaks", + "edit_physio", + "filter_physio", + "interpolate_physio", + "peakfind_physio", + "plot_physio", + "reject_peaks", + "load_physio", + "save_physio", + "load_history", + "save_history", + "load_rtpeaks", + "Physio", + "HRV", + "__version__", ] -from peakdet.analytics import (HRV) -from peakdet.external import (load_rtpeaks) -from peakdet.io import (load_physio, save_physio, load_history, save_history) +from peakdet.analytics import HRV +from peakdet.external import load_rtpeaks +from peakdet.io import load_history, load_physio, save_history, save_physio from peakdet.operations import (delete_peaks, edit_physio, filter_physio, interpolate_physio, peakfind_physio, plot_physio, reject_peaks) -from peakdet.physio import (Physio) +from peakdet.physio import Physio from ._version import get_versions -__version__ = get_versions()['version'] -del get_versions \ No newline at end of file + +__version__ = get_versions()["version"] +del get_versions diff --git a/peakdet/_version.py b/peakdet/_version.py index 0879cd8..61e75b2 100644 --- a/peakdet/_version.py +++ b/peakdet/_version.py @@ -1,4 +1,3 @@ - # This file helps to compute a version number in source trees obtained from # git-archive tarball (such as those provided by githubs download-from-tag # feature). Distribution tarballs (built by setup.py sdist) and build @@ -58,17 +57,18 @@ class NotThisMethod(Exception): def register_vcs_handler(vcs, method): # decorator """Mark a method as the handler for a particular VCS.""" + def decorate(f): """Store f in HANDLERS[vcs][method].""" if vcs not in HANDLERS: HANDLERS[vcs] = {} HANDLERS[vcs][method] = f return f + return decorate -def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, - env=None): +def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, env=None): """Call the given command(s).""" assert isinstance(commands, list) p = None @@ -76,10 +76,13 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, try: dispcmd = str([c] + args) # remember shell=False, so use git.cmd on windows, not just git - p = subprocess.Popen([c] + args, cwd=cwd, env=env, - stdout=subprocess.PIPE, - stderr=(subprocess.PIPE if hide_stderr - else None)) + p = subprocess.Popen( + [c] + args, + cwd=cwd, + env=env, + stdout=subprocess.PIPE, + stderr=(subprocess.PIPE if hide_stderr else None), + ) break except EnvironmentError: e = sys.exc_info()[1] @@ -116,16 +119,22 @@ def versions_from_parentdir(parentdir_prefix, root, verbose): for i in range(3): dirname = os.path.basename(root) if dirname.startswith(parentdir_prefix): - return {"version": dirname[len(parentdir_prefix):], - "full-revisionid": None, - "dirty": False, "error": None, "date": None} + return { + "version": dirname[len(parentdir_prefix) :], + "full-revisionid": None, + "dirty": False, + "error": None, + "date": None, + } else: rootdirs.append(root) root = os.path.dirname(root) # up a level if verbose: - print("Tried directories %s but none started with prefix %s" % - (str(rootdirs), parentdir_prefix)) + print( + "Tried directories %s but none started with prefix %s" + % (str(rootdirs), parentdir_prefix) + ) raise NotThisMethod("rootdir doesn't start with parentdir_prefix") @@ -181,7 +190,7 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of # just "foo-1.0". If we see a "tag: " prefix, prefer those. TAG = "tag: " - tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) + tags = set([r[len(TAG) :] for r in refs if r.startswith(TAG)]) if not tags: # Either we're using git < 1.8.3, or there really are no tags. We use # a heuristic: assume all version tags have a digit. The old git %d @@ -190,7 +199,7 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): # between branches and tags. By ignoring refnames without digits, we # filter out many common branch names like "release" and # "stabilization", as well as "HEAD" and "master". - tags = set([r for r in refs if re.search(r'\d', r)]) + tags = set([r for r in refs if re.search(r"\d", r)]) if verbose: print("discarding '%s', no digits" % ",".join(refs - tags)) if verbose: @@ -198,19 +207,26 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): for ref in sorted(tags): # sorting will prefer e.g. "2.0" over "2.0rc1" if ref.startswith(tag_prefix): - r = ref[len(tag_prefix):] + r = ref[len(tag_prefix) :] if verbose: print("picking %s" % r) - return {"version": r, - "full-revisionid": keywords["full"].strip(), - "dirty": False, "error": None, - "date": date} + return { + "version": r, + "full-revisionid": keywords["full"].strip(), + "dirty": False, + "error": None, + "date": date, + } # no suitable tags, so version is "0+unknown", but full hex is still there if verbose: print("no suitable tags, using unknown + full revision id") - return {"version": "0+unknown", - "full-revisionid": keywords["full"].strip(), - "dirty": False, "error": "no suitable tags", "date": None} + return { + "version": "0+unknown", + "full-revisionid": keywords["full"].strip(), + "dirty": False, + "error": "no suitable tags", + "date": None, + } @register_vcs_handler("git", "pieces_from_vcs") @@ -225,8 +241,7 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): if sys.platform == "win32": GITS = ["git.cmd", "git.exe"] - out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, - hide_stderr=True) + out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, hide_stderr=True) if rc != 0: if verbose: print("Directory %s not under git control" % root) @@ -234,10 +249,19 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] # if there isn't one, this yields HEX[-dirty] (no NUM) - describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty", - "--always", "--long", - "--match", "%s*" % tag_prefix], - cwd=root) + describe_out, rc = run_command( + GITS, + [ + "describe", + "--tags", + "--dirty", + "--always", + "--long", + "--match", + "%s*" % tag_prefix, + ], + cwd=root, + ) # --long was added in git-1.5.5 if describe_out is None: raise NotThisMethod("'git describe' failed") @@ -260,17 +284,16 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): dirty = git_describe.endswith("-dirty") pieces["dirty"] = dirty if dirty: - git_describe = git_describe[:git_describe.rindex("-dirty")] + git_describe = git_describe[: git_describe.rindex("-dirty")] # now we have TAG-NUM-gHEX or HEX if "-" in git_describe: # TAG-NUM-gHEX - mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) + mo = re.search(r"^(.+)-(\d+)-g([0-9a-f]+)$", git_describe) if not mo: # unparseable. Maybe git-describe is misbehaving? - pieces["error"] = ("unable to parse git-describe output: '%s'" - % describe_out) + pieces["error"] = "unable to parse git-describe output: '%s'" % describe_out return pieces # tag @@ -279,10 +302,12 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): if verbose: fmt = "tag '%s' doesn't start with prefix '%s'" print(fmt % (full_tag, tag_prefix)) - pieces["error"] = ("tag '%s' doesn't start with prefix '%s'" - % (full_tag, tag_prefix)) + pieces["error"] = "tag '%s' doesn't start with prefix '%s'" % ( + full_tag, + tag_prefix, + ) return pieces - pieces["closest-tag"] = full_tag[len(tag_prefix):] + pieces["closest-tag"] = full_tag[len(tag_prefix) :] # distance: number of commits since tag pieces["distance"] = int(mo.group(2)) @@ -293,13 +318,13 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): else: # HEX: no tags pieces["closest-tag"] = None - count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], - cwd=root) + count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], cwd=root) pieces["distance"] = int(count_out) # total number of commits # commit date: see ISO-8601 comment in git_versions_from_keywords() - date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"], - cwd=root)[0].strip() + date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"], cwd=root)[ + 0 + ].strip() pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) return pieces @@ -330,8 +355,7 @@ def render_pep440(pieces): rendered += ".dirty" else: # exception #1 - rendered = "0+untagged.%d.g%s" % (pieces["distance"], - pieces["short"]) + rendered = "0+untagged.%d.g%s" % (pieces["distance"], pieces["short"]) if pieces["dirty"]: rendered += ".dirty" return rendered @@ -445,11 +469,13 @@ def render_git_describe_long(pieces): def render(pieces, style): """Render the given version pieces into the requested style.""" if pieces["error"]: - return {"version": "unknown", - "full-revisionid": pieces.get("long"), - "dirty": None, - "error": pieces["error"], - "date": None} + return { + "version": "unknown", + "full-revisionid": pieces.get("long"), + "dirty": None, + "error": pieces["error"], + "date": None, + } if not style or style == "default": style = "pep440" # the default @@ -469,9 +495,13 @@ def render(pieces, style): else: raise ValueError("unknown style '%s'" % style) - return {"version": rendered, "full-revisionid": pieces["long"], - "dirty": pieces["dirty"], "error": None, - "date": pieces.get("date")} + return { + "version": rendered, + "full-revisionid": pieces["long"], + "dirty": pieces["dirty"], + "error": None, + "date": pieces.get("date"), + } def get_versions(): @@ -485,8 +515,7 @@ def get_versions(): verbose = cfg.verbose try: - return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, - verbose) + return git_versions_from_keywords(get_keywords(), cfg.tag_prefix, verbose) except NotThisMethod: pass @@ -495,13 +524,16 @@ def get_versions(): # versionfile_source is the relative path from the top of the source # tree (where the .git directory might live) to this file. Invert # this to find the root from __file__. - for i in cfg.versionfile_source.split('/'): + for i in cfg.versionfile_source.split("/"): root = os.path.dirname(root) except NameError: - return {"version": "0+unknown", "full-revisionid": None, - "dirty": None, - "error": "unable to find root of source tree", - "date": None} + return { + "version": "0+unknown", + "full-revisionid": None, + "dirty": None, + "error": "unable to find root of source tree", + "date": None, + } try: pieces = git_pieces_from_vcs(cfg.tag_prefix, root, verbose) @@ -515,6 +547,10 @@ def get_versions(): except NotThisMethod: pass - return {"version": "0+unknown", "full-revisionid": None, - "dirty": None, - "error": "unable to compute version", "date": None} + return { + "version": "0+unknown", + "full-revisionid": None, + "dirty": None, + "error": "unable to compute version", + "date": None, + } diff --git a/peakdet/analytics.py b/peakdet/analytics.py index f53ff1f..7467440 100644 --- a/peakdet/analytics.py +++ b/peakdet/analytics.py @@ -4,11 +4,11 @@ """ import numpy as np -from scipy.signal import welch from scipy.interpolate import interp1d +from scipy.signal import welch -class HRV(): +class HRV: """ Class for calculating various HRV statistics @@ -66,21 +66,20 @@ class HRV(): def __init__(self, data): self.data = data - func = interp1d(self.rrtime, self.rrint * 1000, kind='cubic') - irrt = np.arange(self.rrtime[0], self.rrtime[-1], 1. / 4.) + func = interp1d(self.rrtime, self.rrint * 1000, kind="cubic") + irrt = np.arange(self.rrtime[0], self.rrtime[-1], 1.0 / 4.0) self._irri = func(irrt) @property def rrtime(self): - """ Times of R-R intervals (in seconds) """ + """Times of R-R intervals (in seconds)""" if len(self.data.peaks): - diff = ((self.data._masked[:-1] + self.data._masked[1:]) - / (2 * self.data.fs)) + diff = (self.data._masked[:-1] + self.data._masked[1:]) / (2 * self.data.fs) return diff.compressed() @property def rrint(self): - """ Length of R-R intervals (in seconds) """ + """Length of R-R intervals (in seconds)""" if len(self.data.peaks): return (np.diff(self.data._masked) / self.data.fs).compressed() @@ -90,7 +89,7 @@ def _sd(self): @property def _fft(self): - return welch(self._irri, nperseg=120, fs=4.0, scaling='spectrum') + return welch(self._irri, nperseg=120, fs=4.0, scaling="spectrum") @property def avgnn(self): @@ -110,7 +109,7 @@ def sdsd(self): @property def nn50(self): - return np.argwhere(self._sd > 50.).size + return np.argwhere(self._sd > 50.0).size @property def pnn50(self): @@ -118,7 +117,7 @@ def pnn50(self): @property def nn20(self): - return np.argwhere(self._sd > 20.).size + return np.argwhere(self._sd > 20.0).size @property def pnn20(self): @@ -137,7 +136,7 @@ def _lf(self): @property def _vlf(self): fx, px = self._fft - return px[np.logical_and(fx >= 0., fx < 0.04)] + return px[np.logical_and(fx >= 0.0, fx < 0.04)] @property def hf(self): diff --git a/peakdet/cli/run.py b/peakdet/cli/run.py index 72fc06b..e0f6f14 100644 --- a/peakdet/cli/run.py +++ b/peakdet/cli/run.py @@ -3,111 +3,164 @@ import os import sys import warnings + import matplotlib -matplotlib.use('WXAgg') + +matplotlib.use("WXAgg") from gooey import Gooey, GooeyParser + import peakdet -TARGET = 'pythonw' if sys.platform == 'darwin' else 'python' -TARGET += ' -u ' + os.path.abspath(__file__) +TARGET = "pythonw" if sys.platform == "darwin" else "python" +TARGET += " -u " + os.path.abspath(__file__) -LOADERS = dict( - rtpeaks=peakdet.load_rtpeaks, - MRI=peakdet.load_physio -) +LOADERS = dict(rtpeaks=peakdet.load_rtpeaks, MRI=peakdet.load_physio) MODALITIES = dict( - ECG=([5., 15.], 'bandpass'), - PPG=(2, 'lowpass'), - RESP=([0.05, 0.5], 'bandpass') + ECG=([5.0, 15.0], "bandpass"), PPG=(2, "lowpass"), RESP=([0.05, 0.5], "bandpass") ) ATTR_CONV = { - 'Average NN intervals': 'avgnn', - 'Root mean square of successive differences': 'rmssd', - 'Standard deviation of NN intervals': 'sdnn', - 'Standard deviation of successive differences': 'sdsd', - 'Number of successive differences >50 ms': 'nn50', - 'Percent of successive differences >50 ms': 'pnn50', - 'Number of successive differences >20 ms': 'nn20', - 'Percent of successive differences >20 ms': 'pnn20', - 'High frequency HRV hfHRV': 'hf', - 'Log of high frequency HRV, log(hfHRV)': 'hf_log', - 'Low frequency HRV, lfHRV': 'lf', - 'Log of low frequency HRV, log(lfHRV)': 'lf_log', - 'Very low frequency HRV, vlfHRV': 'vlf', - 'Log of very low frequency HRV, log(vlfHRV)': 'vlf_log', - 'Ratio of lfHRV : hfHRV': 'lftohf', - 'Peak frequency of hfHRV': 'hf_peak', - 'Peak frequency of lfHRV': 'lf_peak' + "Average NN intervals": "avgnn", + "Root mean square of successive differences": "rmssd", + "Standard deviation of NN intervals": "sdnn", + "Standard deviation of successive differences": "sdsd", + "Number of successive differences >50 ms": "nn50", + "Percent of successive differences >50 ms": "pnn50", + "Number of successive differences >20 ms": "nn20", + "Percent of successive differences >20 ms": "pnn20", + "High frequency HRV hfHRV": "hf", + "Log of high frequency HRV, log(hfHRV)": "hf_log", + "Low frequency HRV, lfHRV": "lf", + "Log of low frequency HRV, log(lfHRV)": "lf_log", + "Very low frequency HRV, vlfHRV": "vlf", + "Log of very low frequency HRV, log(vlfHRV)": "vlf_log", + "Ratio of lfHRV : hfHRV": "lftohf", + "Peak frequency of hfHRV": "hf_peak", + "Peak frequency of lfHRV": "lf_peak", } -@Gooey(program_name='Physio pipeline', - program_description='Physiological processing pipeline', - default_size=(800, 600), - target=TARGET) +@Gooey( + program_name="Physio pipeline", + program_description="Physiological processing pipeline", + default_size=(800, 600), + target=TARGET, +) def get_parser(): - """ Parser for GUI and command-line arguments """ + """Parser for GUI and command-line arguments""" parser = GooeyParser() - parser.add_argument('file_template', metavar='Filename template', - widget='FileChooser', - help='Select a representative file and replace all ' - 'subject-specific information with a "?" symbol.' - '\nFor example, subject_001_data.txt should ' - 'become subject_???_data.txt and will expand to ' - 'match\nsubject_001_data.txt, subject_002_data.' - 'txt, ..., subject_999_data.txt.') - - inp_group = parser.add_argument_group('Inputs', 'Options to specify ' - 'format of input files') - inp_group.add_argument('--modality', metavar='Modality', default='ECG', - choices=list(MODALITIES.keys()), - help='Modality of input data.') - inp_group.add_argument('--fs', metavar='Sampling rate', default=1000.0, - type=float, - help='Sampling rate of input data.') - inp_group.add_argument('--source', metavar='Source', default='rtpeaks', - choices=list(LOADERS.keys()), - help='Program used to collect the data.') - inp_group.add_argument('--channel', metavar='Channel', default=1, type=int, - help='Which channel of data to read from data ' - 'files.\nOnly applies if "Source" is set to ' - 'rtpeaks.') - - out_group = parser.add_argument_group('Outputs', 'Options to specify ' - 'format of output files') - out_group.add_argument('-o', '--output', metavar='Filename', - default='peakdet.csv', - help='Output filename for generated measurements.') - out_group.add_argument('-m', '--measurements', metavar='Measurements', - nargs='+', widget='Listbox', - choices=list(ATTR_CONV.keys()), - default=['Average NN intervals', - 'Standard deviation of NN intervals'], - help='Desired physiological measurements.\nChoose ' - 'multiple with shift+click or ctrl+click.') - out_group.add_argument('-s', '--savehistory', metavar='Save history', - action='store_true', - help='Whether to save history of data processing ' - 'for each file.') - - edit_group = parser.add_argument_group('Workflow arguments (optional!)', - 'Options to specify modifications ' - 'to workflow') - edit_group.add_argument('-n', '--noedit', metavar='Editing', - action='store_true', - help='Turn off interactive editing.') - edit_group.add_argument('-t', '--thresh', metavar='Threshold', default=0.2, - type=float, - help='Threshold for peak detection algorithm.') + parser.add_argument( + "file_template", + metavar="Filename template", + widget="FileChooser", + help="Select a representative file and replace all " + 'subject-specific information with a "?" symbol.' + "\nFor example, subject_001_data.txt should " + "become subject_???_data.txt and will expand to " + "match\nsubject_001_data.txt, subject_002_data." + "txt, ..., subject_999_data.txt.", + ) + + inp_group = parser.add_argument_group( + "Inputs", "Options to specify " "format of input files" + ) + inp_group.add_argument( + "--modality", + metavar="Modality", + default="ECG", + choices=list(MODALITIES.keys()), + help="Modality of input data.", + ) + inp_group.add_argument( + "--fs", + metavar="Sampling rate", + default=1000.0, + type=float, + help="Sampling rate of input data.", + ) + inp_group.add_argument( + "--source", + metavar="Source", + default="rtpeaks", + choices=list(LOADERS.keys()), + help="Program used to collect the data.", + ) + inp_group.add_argument( + "--channel", + metavar="Channel", + default=1, + type=int, + help="Which channel of data to read from data " + 'files.\nOnly applies if "Source" is set to ' + "rtpeaks.", + ) + + out_group = parser.add_argument_group( + "Outputs", "Options to specify " "format of output files" + ) + out_group.add_argument( + "-o", + "--output", + metavar="Filename", + default="peakdet.csv", + help="Output filename for generated measurements.", + ) + out_group.add_argument( + "-m", + "--measurements", + metavar="Measurements", + nargs="+", + widget="Listbox", + choices=list(ATTR_CONV.keys()), + default=["Average NN intervals", "Standard deviation of NN intervals"], + help="Desired physiological measurements.\nChoose " + "multiple with shift+click or ctrl+click.", + ) + out_group.add_argument( + "-s", + "--savehistory", + metavar="Save history", + action="store_true", + help="Whether to save history of data processing " "for each file.", + ) + + edit_group = parser.add_argument_group( + "Workflow arguments (optional!)", + "Options to specify modifications " "to workflow", + ) + edit_group.add_argument( + "-n", + "--noedit", + metavar="Editing", + action="store_true", + help="Turn off interactive editing.", + ) + edit_group.add_argument( + "-t", + "--thresh", + metavar="Threshold", + default=0.2, + type=float, + help="Threshold for peak detection algorithm.", + ) return parser -def workflow(*, file_template, modality, fs, source='MRI', channel=1, - output='peakdet.csv', savehistory=True, noedit=False, thresh=0.2, - measurements=ATTR_CONV.keys()): +def workflow( + *, + file_template, + modality, + fs, + source="MRI", + channel=1, + output="peakdet.csv", + savehistory=True, + noedit=False, + thresh=0.2, + measurements=ATTR_CONV.keys() +): """ Basic workflow for physiological data @@ -140,67 +193,71 @@ def workflow(*, file_template, modality, fs, source='MRI', channel=1, """ # output file - print('OUTPUT FILE:\t\t{}\n'.format(output)) + print("OUTPUT FILE:\t\t{}\n".format(output)) # grab files from file template - print('FILE TEMPLATE:\t{}\n'.format(file_template)) + print("FILE TEMPLATE:\t{}\n".format(file_template)) files = glob.glob(file_template, recursive=True) # convert measurements to peakdet.HRV attribute friendly names try: - print('REQUESTED MEASUREMENTS: {}\n'.format(', '.join(measurements))) + print("REQUESTED MEASUREMENTS: {}\n".format(", ".join(measurements))) except TypeError: - raise TypeError('It looks like you didn\'t select any of the options ' - 'specifying desired output measurements. Please ' - 'select at least one measurement and try again.') + raise TypeError( + "It looks like you didn't select any of the options " + "specifying desired output measurements. Please " + "select at least one measurement and try again." + ) measurements = [ATTR_CONV[attr] for attr in measurements] # get appropriate loader load_func = LOADERS[source] # check if output file exists -- if so, ensure headers will match - head = 'filename,' + ','.join(measurements) + head = "filename," + ",".join(measurements) if os.path.exists(output): - with open(output, 'r') as src: + with open(output, "r") as src: eheader = src.readlines()[0] # if existing output file does not have same measurements are those # requested on command line, warn and use existing measurements so # as not to totally fork up existing file if eheader != head: - warnings.warn('Desired output file already exists and requested ' - 'measurements do not match with measurements in ' - 'existing output file. Using the pre-existing ' - 'measurements, instead.') - measurements = [f.strip() for f in eheader.split(',')[1:]] - head = '' + warnings.warn( + "Desired output file already exists and requested " + "measurements do not match with measurements in " + "existing output file. Using the pre-existing " + "measurements, instead." + ) + measurements = [f.strip() for f in eheader.split(",")[1:]] + head = "" # if output file doesn't exist, nbd else: - head += '\n' + head += "\n" - with open(output, 'a+') as dest: + with open(output, "a+") as dest: dest.write(head) # iterate through all files and do peak detection with manual editing for fname in files: fname = os.path.relpath(fname) - print('Currently processing {}'.format(fname)) + print("Currently processing {}".format(fname)) # if we want to save history, this is the output name it would take - outname = os.path.join(os.path.dirname(fname), - '.' + os.path.basename(fname) + '.json') + outname = os.path.join( + os.path.dirname(fname), "." + os.path.basename(fname) + ".json" + ) # let's check if history already exists and load that file, if so if os.path.exists(outname): data = peakdet.load_history(outname) else: # load data with appropriate function, depending on source - if source == 'rtpeaks': + if source == "rtpeaks": data = load_func(fname, fs=fs, channel=channel) else: data = load_func(fname, fs=fs) # filter flims, method = MODALITIES[modality] - data = peakdet.filter_physio(data, cutoffs=flims, - method=method) + data = peakdet.filter_physio(data, cutoffs=flims, method=method) # perform peak detection data = peakdet.peakfind_physio(data, thresh=thresh) @@ -217,11 +274,10 @@ def workflow(*, file_template, modality, fs, source='MRI', channel=1, # keep requested outputs hrv = peakdet.HRV(data) - outputs = ['{:.5f}'.format(getattr(hrv, attr, '')) - for attr in measurements] + outputs = ["{:.5f}".format(getattr(hrv, attr, "")) for attr in measurements] # save as we go so that interruptions don't screw everything up - dest.write(','.join([fname] + outputs) + '\n') + dest.write(",".join([fname] + outputs) + "\n") def main(): @@ -229,5 +285,5 @@ def main(): workflow(**vars(opts)) -if __name__ == '__main__': +if __name__ == "__main__": main() diff --git a/peakdet/editor.py b/peakdet/editor.py index 81d52b4..d1f735e 100644 --- a/peakdet/editor.py +++ b/peakdet/editor.py @@ -2,13 +2,15 @@ """Functions and class for performing interactive editing of physiological data.""" import functools -import numpy as np + import matplotlib.pyplot as plt +import numpy as np from matplotlib.widgets import SpanSelector + from peakdet import operations, utils -class _PhysioEditor(): +class _PhysioEditor: """ Class for editing physiological data. @@ -32,15 +34,20 @@ def __init__(self, data): # make main plot objects depending on supplementary data if self.suppdata is None: - self.fig, self._ax = plt.subplots(nrows=1, ncols=1, - tight_layout=True, sharex=True) + self.fig, self._ax = plt.subplots( + nrows=1, ncols=1, tight_layout=True, sharex=True + ) else: - self.fig, self._ax = plt.subplots(nrows=2, ncols=1, - tight_layout=True, sharex=True, - gridspec_kw={'height_ratios': [3, 2]}) + self.fig, self._ax = plt.subplots( + nrows=2, + ncols=1, + tight_layout=True, + sharex=True, + gridspec_kw={"height_ratios": [3, 2]}, + ) - self.fig.canvas.mpl_connect('scroll_event', self.on_wheel) - self.fig.canvas.mpl_connect('key_press_event', self.on_key) + self.fig.canvas.mpl_connect("scroll_event", self.on_wheel) + self.fig.canvas.mpl_connect("key_press_event", self.on_key) # Set axis handler self.ax = self._ax if self.suppdata is None else self._ax[0] @@ -49,18 +56,33 @@ def __init__(self, data): # 1. rejection (central mouse), # 2. addition (right mouse), and # 3. deletion (left mouse) - delete = functools.partial(self.on_edit, method='delete') - reject = functools.partial(self.on_edit, method='reject') - insert = functools.partial(self.on_edit, method='insert') - self.span2 = SpanSelector(self.ax, delete, 'horizontal', - button=1, useblit=True, - rectprops=dict(facecolor='red', alpha=0.3)) - self.span1 = SpanSelector(self.ax, reject, 'horizontal', - button=2, useblit=True, - rectprops=dict(facecolor='blue', alpha=0.3)) - self.span3 = SpanSelector(self.ax, insert, 'horizontal', - button=3, useblit=True, - rectprops=dict(facecolor='green', alpha=0.3)) + delete = functools.partial(self.on_edit, method="delete") + reject = functools.partial(self.on_edit, method="reject") + insert = functools.partial(self.on_edit, method="insert") + self.span2 = SpanSelector( + self.ax, + delete, + "horizontal", + button=1, + useblit=True, + rectprops=dict(facecolor="red", alpha=0.3), + ) + self.span1 = SpanSelector( + self.ax, + reject, + "horizontal", + button=2, + useblit=True, + rectprops=dict(facecolor="blue", alpha=0.3), + ) + self.span3 = SpanSelector( + self.ax, + insert, + "horizontal", + button=3, + useblit=True, + rectprops=dict(facecolor="green", alpha=0.3), + ) self.plot_signals(False) @@ -74,17 +96,23 @@ def plot_signals(self, plot=True): # clear old data + redraw, retaining x-/y-axis zooms self.ax.clear() - self.ax.plot(self.time, self.data, 'b', - self.time[self.data.peaks], - self.data[self.data.peaks], '.r', - self.time[self.data.troughs], - self.data[self.data.troughs], '.g') + self.ax.plot( + self.time, + self.data, + "b", + self.time[self.data.peaks], + self.data[self.data.peaks], + ".r", + self.time[self.data.troughs], + self.data[self.data.troughs], + ".g", + ) if self.suppdata is not None: - self._ax[1].plot(self.time, self.suppdata, 'k', linewidth=0.7) - self._ax[1].set_ylim(-.5, .5) + self._ax[1].plot(self.time, self.suppdata, "k", linewidth=0.7) + self._ax[1].set_ylim(-0.5, 0.5) - self.ax.set(xlim=xlim, ylim=ylim, yticklabels='') + self.ax.set(xlim=xlim, ylim=ylim, yticklabels="") self.fig.canvas.draw() def on_wheel(self, event): @@ -100,9 +128,9 @@ def quit(self): def on_key(self, event): """Undo last span select or quits peak editor.""" # accept both control or Mac command key as selector - if event.key in ['ctrl+z', 'super+d']: + if event.key in ["ctrl+z", "super+d"]: self.undo() - elif event.key in ['ctrl+q', 'super+d']: + elif event.key in ["ctrl+q", "super+d"]: self.quit() def on_edit(self, xmin, xmax, *, method): @@ -114,13 +142,13 @@ def on_edit(self, xmin, xmax, *, method): method accepts 'insert', 'reject', 'delete' """ - if method not in ['insert', 'reject', 'delete']: + if method not in ["insert", "reject", "delete"]: raise ValueError(f'Action "{method}" not supported.') tmin, tmax = np.searchsorted(self.time, (xmin, xmax)) pmin, pmax = np.searchsorted(self.data.peaks, (tmin, tmax)) - if method == 'insert': + if method == "insert": tmp = np.argmax(self.data.data[tmin:tmax]) if tmin != tmax else 0 newpeak = tmin + tmp if newpeak == tmin: @@ -132,13 +160,13 @@ def on_edit(self, xmin, xmax, *, method): self.plot_signals() return - if method == 'reject': + if method == "reject": rej, fcn = self.rejected, operations.reject_peaks - elif method == 'delete': + elif method == "delete": rej, fcn = self.deleted, operations.delete_peaks # store edits in local history & call function - if method == 'insert': + if method == "insert": self.included.add(newpeak) self.data = operations.add_peaks(self.data, newpeak) else: @@ -150,32 +178,32 @@ def on_edit(self, xmin, xmax, *, method): def undo(self): """Reset last span select peak removal.""" # check if last history entry was a manual reject / delete - relevant = ['reject_peaks', 'delete_peaks', 'add_peaks'] + relevant = ["reject_peaks", "delete_peaks", "add_peaks"] if self.data._history[-1][0] not in relevant: return # pop off last edit and delete func, peaks = self.data._history.pop() - if func == 'reject_peaks': - self.data._metadata['reject'] = np.setdiff1d( - self.data._metadata['reject'], peaks['remove'] + if func == "reject_peaks": + self.data._metadata["reject"] = np.setdiff1d( + self.data._metadata["reject"], peaks["remove"] ) - self.rejected.difference_update(peaks['remove']) - elif func == 'delete_peaks': - self.data._metadata['peaks'] = np.insert( - self.data._metadata['peaks'], - np.searchsorted(self.data._metadata['peaks'], peaks['remove']), - peaks['remove'] + self.rejected.difference_update(peaks["remove"]) + elif func == "delete_peaks": + self.data._metadata["peaks"] = np.insert( + self.data._metadata["peaks"], + np.searchsorted(self.data._metadata["peaks"], peaks["remove"]), + peaks["remove"], ) - self.deleted.difference_update(peaks['remove']) - elif func == 'add_peaks': - self.data._metadata['peaks'] = np.delete( - self.data._metadata['peaks'], - np.searchsorted(self.data._metadata['peaks'], peaks['add']), + self.deleted.difference_update(peaks["remove"]) + elif func == "add_peaks": + self.data._metadata["peaks"] = np.delete( + self.data._metadata["peaks"], + np.searchsorted(self.data._metadata["peaks"], peaks["add"]), ) - self.included.remove(peaks['add']) - self.data._metadata['troughs'] = utils.check_troughs(self.data, - self.data.peaks, - self.data.troughs) + self.included.remove(peaks["add"]) + self.data._metadata["troughs"] = utils.check_troughs( + self.data, self.data.peaks, self.data.troughs + ) self.plot_signals() diff --git a/peakdet/external.py b/peakdet/external.py index 995b5f1..ec2e731 100644 --- a/peakdet/external.py +++ b/peakdet/external.py @@ -4,7 +4,9 @@ """ import warnings + import numpy as np + from peakdet import physio, utils @@ -37,16 +39,18 @@ def load_rtpeaks(fname, channel, fs): Loaded physiological data """ - if fname.startswith('/'): - warnings.warn('Provided file seems to be an absolute path. In order ' - 'to ensure full reproducibility it is recommended that ' - 'a relative path is provided.') + if fname.startswith("/"): + warnings.warn( + "Provided file seems to be an absolute path. In order " + "to ensure full reproducibility it is recommended that " + "a relative path is provided." + ) - with open(fname, 'r') as src: - header = src.readline().strip().split(',') + with open(fname, "r") as src: + header = src.readline().strip().split(",") - col = header.index('channel{}'.format(channel)) - data = np.loadtxt(fname, usecols=col, skiprows=1, delimiter=',') + col = header.index("channel{}".format(channel)) + data = np.loadtxt(fname, usecols=col, skiprows=1, delimiter=",") phys = physio.Physio(data, fs=fs) return phys diff --git a/peakdet/io.py b/peakdet/io.py index b3f0fc5..1e87238 100644 --- a/peakdet/io.py +++ b/peakdet/io.py @@ -6,14 +6,15 @@ import json import os.path as op import warnings + import numpy as np + from peakdet import physio, utils -EXPECTED = ['data', 'fs', 'history', 'metadata'] +EXPECTED = ["data", "fs", "history", "metadata"] -def load_physio(data, *, fs=None, dtype=None, history=None, - allow_pickle=False): +def load_physio(data, *, fs=None, dtype=None, history=None, allow_pickle=False): """ Returns `Physio` object with provided data @@ -51,37 +52,39 @@ def load_physio(data, *, fs=None, dtype=None, history=None, try: inp[attr] = inp[attr].dtype.type(inp[attr]) except KeyError: - raise ValueError('Provided npz file {} must have all of ' - 'the following attributes: {}' - .format(data, EXPECTED)) + raise ValueError( + "Provided npz file {} must have all of " + "the following attributes: {}".format(data, EXPECTED) + ) # fix history, which needs to be list-of-tuple - if inp['history'] is not None: - inp['history'] = list(map(tuple, inp['history'])) + if inp["history"] is not None: + inp["history"] = list(map(tuple, inp["history"])) except (IOError, OSError, ValueError): - inp = dict(data=np.loadtxt(data), - history=[utils._get_call(exclude=[])]) + inp = dict(data=np.loadtxt(data), history=[utils._get_call(exclude=[])]) phys = physio.Physio(**inp) # if we got a numpy array, load that into a Physio object elif isinstance(data, np.ndarray): if history is None: - warnings.warn('Loading data from a numpy array without providing a' - 'history will render reproducibility functions ' - 'useless! Continuing anyways.') - phys = physio.Physio(np.asarray(data, dtype=dtype), fs=fs, - history=history) + warnings.warn( + "Loading data from a numpy array without providing a" + "history will render reproducibility functions " + "useless! Continuing anyways." + ) + phys = physio.Physio(np.asarray(data, dtype=dtype), fs=fs, history=history) # create a new Physio object out of a provided Physio object elif isinstance(data, physio.Physio): phys = utils.new_physio_like(data, data.data, fs=fs, dtype=dtype) phys._history += [utils._get_call()] else: - raise TypeError('Cannot load data of type {}'.format(type(data))) + raise TypeError("Cannot load data of type {}".format(type(data))) # reset sampling rate, as requested if fs is not None and fs != phys.fs: if not np.isnan(phys.fs): - warnings.warn('Provided sampling rate does not match loaded rate. ' - 'Resetting loaded sampling rate {} to provided {}' - .format(phys.fs, fs)) + warnings.warn( + "Provided sampling rate does not match loaded rate. " + "Resetting loaded sampling rate {} to provided {}".format(phys.fs, fs) + ) phys._fs = fs # coerce datatype, if needed if dtype is not None: @@ -110,11 +113,12 @@ def save_physio(fname, data): from peakdet.utils import check_physio data = check_physio(data) - fname += '.phys' if not fname.endswith('.phys') else '' - with open(fname, 'wb') as dest: + fname += ".phys" if not fname.endswith(".phys") else "" + with open(fname, "wb") as dest: hist = data.history if data.history != [] else None - np.savez_compressed(dest, data=data.data, fs=data.fs, - history=hist, metadata=data._metadata) + np.savez_compressed( + dest, data=data.data, fs=data.fs, history=hist, metadata=data._metadata + ) return fname @@ -141,29 +145,34 @@ def load_history(file, verbose=False): import peakdet # grab history from provided JSON file - with open(file, 'r') as src: + with open(file, "r") as src: history = json.load(src) # replay history from beginning and return resultant Physio object data = None - for (func, kwargs) in history: + for func, kwargs in history: if verbose: - print('Rerunning {}'.format(func)) + print("Rerunning {}".format(func)) # loading functions don't have `data` input because it should be the # first thing in `history` (when the data was originally loaded!). # for safety, check if `data` is None; someone could have potentially # called load_physio on a Physio object (which is a valid, albeit # confusing, thing to do) - if 'load' in func and data is None: - if not op.exists(kwargs['data']): - if kwargs['data'].startswith('/'): - msg = ('Perhaps you are trying to load a history file ' - 'that was generated with an absolute path?') + if "load" in func and data is None: + if not op.exists(kwargs["data"]): + if kwargs["data"].startswith("/"): + msg = ( + "Perhaps you are trying to load a history file " + "that was generated with an absolute path?" + ) else: - msg = ('Perhaps you are trying to load a history file ' - 'that was generated from a different directory?') - raise FileNotFoundError('{} does not exist. {}' - .format(kwargs['data'], msg)) + msg = ( + "Perhaps you are trying to load a history file " + "that was generated from a different directory?" + ) + raise FileNotFoundError( + "{} does not exist. {}".format(kwargs["data"], msg) + ) data = getattr(peakdet, func)(**kwargs) else: data = getattr(peakdet, func)(data, **kwargs) @@ -194,11 +203,13 @@ def save_history(file, data): data = check_physio(data) if len(data.history) == 0: - warnings.warn('History of provided Physio object is empty. Saving ' - 'anyway, but reloading this file will result in an ' - 'error.') - file += '.json' if not file.endswith('.json') else '' - with open(file, 'w') as dest: + warnings.warn( + "History of provided Physio object is empty. Saving " + "anyway, but reloading this file will result in an " + "error." + ) + file += ".json" if not file.endswith(".json") else "" + with open(file, "w") as dest: json.dump(data.history, dest, indent=4) return file diff --git a/peakdet/modalities.py b/peakdet/modalities.py index 828ef82..8f4e009 100644 --- a/peakdet/modalities.py +++ b/peakdet/modalities.py @@ -3,18 +3,19 @@ import numpy as np -class HRModality(): +class HRModality: def iHR(self, step=1, start=0, end=None, TR=None): if end is None: end = self.rrtime[-1] mod = self.TR * (step // 2) - time = np.arange(start - mod, end + mod + 1, self.TR, dtype='int') + time = np.arange(start - mod, end + mod + 1, self.TR, dtype="int") HR = np.zeros(len(time) - step) for tpoint in range(step, time.size): - inds = np.logical_and(self.rrtime >= time[tpoint - step], - self.rrtime < time[tpoint]) + inds = np.logical_and( + self.rrtime >= time[tpoint - step], self.rrtime < time[tpoint] + ) relevant = self.rrint[inds] if relevant.size == 0: @@ -27,15 +28,15 @@ def meanHR(self): return np.mean(60 / self.rrint) -class ECG(): - flims = [5, 15.] +class ECG: + flims = [5, 15.0] -class PPG(): +class PPG: flims = 2.0 -class RESP(): +class RESP: flims = [0.05, 0.5] def RVT(self, start=0, end=None, TR=None): @@ -46,7 +47,7 @@ def RVT(self, start=0, end=None, TR=None): rvt = (pheight[:-1] - theight) / (np.diff(self.peakinds) / self.fs) rt = (self.peakinds / self.fs)[1:] - time = np.arange(start, end + 1, self.TR, dtype='int') + time = np.arange(start, end + 1, self.TR, dtype="int") iRVT = np.interp(time, rt, rvt, left=rvt.mean(), right=rvt.mean()) return iRVT diff --git a/peakdet/operations.py b/peakdet/operations.py index a7bc7c5..9f8458a 100644 --- a/peakdet/operations.py +++ b/peakdet/operations.py @@ -6,6 +6,7 @@ import matplotlib.pyplot as plt import numpy as np from scipy import interpolate, signal + from peakdet import editor, utils @@ -34,26 +35,28 @@ def filter_physio(data, cutoffs, method, *, order=3): Filtered input `data` """ - _valid_methods = ['lowpass', 'highpass', 'bandpass', 'bandstop'] + _valid_methods = ["lowpass", "highpass", "bandpass", "bandstop"] data = utils.check_physio(data, ensure_fs=True) if method not in _valid_methods: - raise ValueError('Provided method {} is not permitted; must be in {}.' - .format(method, _valid_methods)) + raise ValueError( + "Provided method {} is not permitted; must be in {}.".format( + method, _valid_methods + ) + ) cutoffs = np.array(cutoffs) - if method in ['lowpass', 'highpass'] and cutoffs.size != 1: - raise ValueError('Cutoffs must be length 1 when using {} filter' - .format(method)) - elif method in ['bandpass', 'bandstop'] and cutoffs.size != 2: - raise ValueError('Cutoffs must be length 2 when using {} filter' - .format(method)) + if method in ["lowpass", "highpass"] and cutoffs.size != 1: + raise ValueError("Cutoffs must be length 1 when using {} filter".format(method)) + elif method in ["bandpass", "bandstop"] and cutoffs.size != 2: + raise ValueError("Cutoffs must be length 2 when using {} filter".format(method)) nyq_cutoff = cutoffs / (data.fs * 0.5) if np.any(nyq_cutoff > 1): - raise ValueError('Provided cutoffs {} are outside of the Nyquist ' - 'frequency for input data with sampling rate {}.' - .format(cutoffs, data.fs)) + raise ValueError( + "Provided cutoffs {} are outside of the Nyquist " + "frequency for input data with sampling rate {}.".format(cutoffs, data.fs) + ) b, a = signal.butter(int(order), nyq_cutoff, btype=method) filtered = utils.new_physio_like(data, signal.filtfilt(b, a, data)) @@ -62,7 +65,7 @@ def filter_physio(data, cutoffs, method, *, order=3): @utils.make_operation() -def interpolate_physio(data, target_fs, *, kind='cubic'): +def interpolate_physio(data, target_fs, *, kind="cubic"): """ Interpolates `data` to desired sampling rate `target_fs` @@ -133,11 +136,11 @@ def peakfind_physio(data, *, thresh=0.2, dist=None): # second, more thorough peak detection cdist = np.diff(locs).mean() // 2 - heights = np.percentile(heights['peak_heights'], 1) + heights = np.percentile(heights["peak_heights"], 1) locs, heights = signal.find_peaks(data[:], distance=cdist, height=heights) - data._metadata['peaks'] = locs + data._metadata["peaks"] = locs # perform trough detection based on detected peaks - data._metadata['troughs'] = utils.check_troughs(data, data.peaks) + data._metadata["troughs"] = utils.check_troughs(data, data.peaks) return data @@ -158,8 +161,8 @@ def delete_peaks(data, remove): """ data = utils.check_physio(data, ensure_fs=False, copy=True) - data._metadata['peaks'] = np.setdiff1d(data._metadata['peaks'], remove) - data._metadata['troughs'] = utils.check_troughs(data, data.peaks, data.troughs) + data._metadata["peaks"] = np.setdiff1d(data._metadata["peaks"], remove) + data._metadata["troughs"] = utils.check_troughs(data, data.peaks, data.troughs) return data @@ -180,8 +183,8 @@ def reject_peaks(data, remove): """ data = utils.check_physio(data, ensure_fs=False, copy=True) - data._metadata['reject'] = np.append(data._metadata['reject'], remove) - data._metadata['troughs'] = utils.check_troughs(data, data.peaks, data.troughs) + data._metadata["reject"] = np.append(data._metadata["reject"], remove) + data._metadata["troughs"] = utils.check_troughs(data, data.peaks, data.troughs) return data @@ -202,9 +205,9 @@ def add_peaks(data, add): """ data = utils.check_physio(data, ensure_fs=False, copy=True) - idx = np.searchsorted(data._metadata['peaks'], add) - data._metadata['peaks'] = np.insert(data._metadata['peaks'], idx, add) - data._metadata['troughs'] = utils.check_troughs(data, data.peaks) + idx = np.searchsorted(data._metadata["peaks"], add) + data._metadata["peaks"] = np.insert(data._metadata["peaks"], idx, add) + data._metadata["troughs"] = utils.check_troughs(data, data.peaks) return data @@ -225,9 +228,9 @@ def add_peaks(data, add): """ data = utils.check_physio(data, ensure_fs=False, copy=True) - idx = np.searchsorted(data._metadata['peaks'], add) - data._metadata['peaks'] = np.insert(data._metadata['peaks'], idx, add) - data._metadata['troughs'] = utils.check_troughs(data, data.peaks) + idx = np.searchsorted(data._metadata["peaks"], add) + data._metadata["peaks"] = np.insert(data._metadata["peaks"], idx, add) + data._metadata["troughs"] = utils.check_troughs(data, data.peaks) return data @@ -292,8 +295,16 @@ def plot_physio(data, *, ax=None): if ax is None: fig, ax = plt.subplots(1, 1) # plot data with peaks + troughs, as appropriate - ax.plot(time, data, 'b', - time[data.peaks], data[data.peaks], '.r', - time[data.troughs], data[data.troughs], '.g') + ax.plot( + time, + data, + "b", + time[data.peaks], + data[data.peaks], + ".r", + time[data.troughs], + data[data.troughs], + ".g", + ) return ax diff --git a/peakdet/physio.py b/peakdet/physio.py index c7652ec..37f4906 100644 --- a/peakdet/physio.py +++ b/peakdet/physio.py @@ -6,8 +6,7 @@ import numpy as np - -class Physio(): +class Physio: """ Class to hold physiological data and relevant information @@ -44,34 +43,44 @@ class Physio(): def __init__(self, data, fs=None, history=None, metadata=None, suppdata=None): self._data = np.asarray(data).squeeze() if self.data.ndim > 1: - raise ValueError('Provided data dimensionality {} > 1.' - .format(self.data.ndim)) + raise ValueError( + "Provided data dimensionality {} > 1.".format(self.data.ndim) + ) if not np.issubdtype(self.data.dtype, np.number): - raise ValueError('Provided data of type {} is not numeric.' - .format(self.data.dtype)) + raise ValueError( + "Provided data of type {} is not numeric.".format(self.data.dtype) + ) self._fs = np.float64(fs) self._history = [] if history is None else history - if (not isinstance(self._history, list) - or any([not isinstance(f, tuple) for f in self._history])): - raise TypeError('Provided history {} must be a list-of-tuples. ' - 'Please check inputs.'.format(history)) + if not isinstance(self._history, list) or any( + [not isinstance(f, tuple) for f in self._history] + ): + raise TypeError( + "Provided history {} must be a list-of-tuples. " + "Please check inputs.".format(history) + ) if metadata is not None: if not isinstance(metadata, dict): - raise TypeError('Provided metadata {} must be dict-like.' - .format(metadata)) - for k in ['peaks', 'troughs', 'reject']: + raise TypeError( + "Provided metadata {} must be dict-like.".format(metadata) + ) + for k in ["peaks", "troughs", "reject"]: metadata.setdefault(k, np.empty(0, dtype=int)) if not isinstance(metadata.get(k), np.ndarray): try: metadata[k] = np.asarray(metadata.get(k), dtype=int) except TypeError: - raise TypeError('Provided metadata must be dict-like' - 'with integer array entries.') + raise TypeError( + "Provided metadata must be dict-like" + "with integer array entries." + ) self._metadata = dict(**metadata) else: - self._metadata = dict(peaks=np.empty(0, dtype=int), - troughs=np.empty(0, dtype=int), - reject=np.empty(0, dtype=int)) + self._metadata = dict( + peaks=np.empty(0, dtype=int), + troughs=np.empty(0, dtype=int), + reject=np.empty(0, dtype=int), + ) self._suppdata = None if suppdata is None else np.asarray(suppdata).squeeze() def __array__(self): @@ -84,52 +93,53 @@ def __len__(self): return len(self.data) def __str__(self): - return '{name}(size={size}, fs={fs})'.format( - name=self.__class__.__name__, - size=self.data.size, - fs=self.fs + return "{name}(size={size}, fs={fs})".format( + name=self.__class__.__name__, size=self.data.size, fs=self.fs ) __repr__ = __str__ @property def data(self): - """ Physiological data """ + """Physiological data""" return self._data @property def fs(self): - """ Sampling rate of data (Hz) """ + """Sampling rate of data (Hz)""" return self._fs @property def history(self): - """ Functions that have been performed on / modified `data` """ + """Functions that have been performed on / modified `data`""" return self._history @property def peaks(self): - """ Indices of detected peaks in `data` """ + """Indices of detected peaks in `data`""" return self._masked.compressed() @property def troughs(self): - """ Indices of detected troughs in `data` """ - return self._metadata['troughs'] + """Indices of detected troughs in `data`""" + return self._metadata["troughs"] @property def _masked(self): - return np.ma.masked_array(self._metadata['peaks'], - mask=np.isin(self._metadata['peaks'], - self._metadata['reject'])) + return np.ma.masked_array( + self._metadata["peaks"], + mask=np.isin(self._metadata["peaks"], self._metadata["reject"]), + ) @property def suppdata(self): - """ Physiological data """ + """Physiological data""" return self._suppdata - def phys2neurokit(self, copy_data, copy_peaks, copy_troughs, module, neurokit_path=None): - """ Physio to neurokit dataframe + def phys2neurokit( + self, copy_data, copy_peaks, copy_troughs, module, neurokit_path=None + ): + """Physio to neurokit dataframe Parameters ---------- @@ -147,27 +157,33 @@ def phys2neurokit(self, copy_data, copy_peaks, copy_troughs, module, neurokit_pa import pandas as pd if neurokit_path is not None: - df = pd.read_csv(neurokit_path, sep='\t') + df = pd.read_csv(neurokit_path, sep="\t") else: - df = pd.DataFrame(0, index=np.arange(len(self.data)), columns=['%s_Raw' % module, '%s_Peaks' % module, '%s_Troughs' % module]) + df = pd.DataFrame( + 0, + index=np.arange(len(self.data)), + columns=["%s_Raw" % module, "%s_Peaks" % module, "%s_Troughs" % module], + ) if copy_data: - df.loc[:, df.columns.str.endswith('Raw')] = self.data + df.loc[:, df.columns.str.endswith("Raw")] = self.data if copy_peaks: b_peaks = np.zeros(len(self.data)) b_peaks[self.peaks] = 1 - df.loc[:, df.columns.str.endswith('Peaks')] = b_peaks + df.loc[:, df.columns.str.endswith("Peaks")] = b_peaks if copy_troughs: b_troughs = np.zeros(len(self.data)) b_troughs[self.troughs] = 1 - df.loc[:, df.columns.str.endswith('Troughs')] = b_troughs + df.loc[:, df.columns.str.endswith("Troughs")] = b_troughs return df @classmethod - def neurokit2phys(cls, neurokit_path, fs, copy_data, copy_peaks, copy_troughs, **kwargs): + def neurokit2phys( + cls, neurokit_path, fs, copy_data, copy_peaks, copy_troughs, **kwargs + ): """Neurokit dataframe to phys Parameters @@ -187,28 +203,30 @@ def neurokit2phys(cls, neurokit_path, fs, copy_data, copy_peaks, copy_troughs, * """ import pandas as pd - df = pd.read_csv(neurokit_path, sep='\t') + df = pd.read_csv(neurokit_path, sep="\t") if copy_data: # if cleaned data exists, substitute 'data' with cleaned data, else use raw data - if df.columns.str.endswith('Clean').any(): - data = np.hstack(df.loc[:, df.columns.str.endswith('Clean')].to_numpy()) - elif df.columns.str.endswith('Raw').any(): - data = np.hstack(df.loc[:, df.columns.str.endswith('Raw')].to_numpy()) + if df.columns.str.endswith("Clean").any(): + data = np.hstack(df.loc[:, df.columns.str.endswith("Clean")].to_numpy()) + elif df.columns.str.endswith("Raw").any(): + data = np.hstack(df.loc[:, df.columns.str.endswith("Raw")].to_numpy()) if copy_peaks: # if peaks exists - if df.columns.str.endswith('Peaks').any(): - peaks = np.where(df.loc[:, df.columns.str.endswith('Peaks')] == 1)[0] + if df.columns.str.endswith("Peaks").any(): + peaks = np.where(df.loc[:, df.columns.str.endswith("Peaks")] == 1)[0] if copy_troughs: # if troughs exists - if df.columns.str.endswith('Troughs').any(): - troughs = np.where(df.loc[:, df.columns.str.endswith('Troughs')] == 1)[0] + if df.columns.str.endswith("Troughs").any(): + troughs = np.where(df.loc[:, df.columns.str.endswith("Troughs")] == 1)[ + 0 + ] - if 'peaks' in locals() and 'troughs' in locals(): + if "peaks" in locals() and "troughs" in locals(): metadata = dict(peaks=peaks, troughs=troughs) - elif 'peaks' in locals() and 'troughs' not in locals(): + elif "peaks" in locals() and "troughs" not in locals(): metadata = dict(peaks=peaks) return cls(data, fs=fs, metadata=metadata, **kwargs) diff --git a/peakdet/tests/__init__.py b/peakdet/tests/__init__.py index 44abd58..bed8839 100644 --- a/peakdet/tests/__init__.py +++ b/peakdet/tests/__init__.py @@ -1,3 +1,3 @@ from peakdet.tests.utils import get_test_data_path -__all__ = ['get_test_data_path'] +__all__ = ["get_test_data_path"] diff --git a/peakdet/tests/test_analytics.py b/peakdet/tests/test_analytics.py index 997f2da..f469077 100644 --- a/peakdet/tests/test_analytics.py +++ b/peakdet/tests/test_analytics.py @@ -4,9 +4,25 @@ from peakdet.tests.utils import get_peak_data ATTRS = [ - 'rrtime', 'rrint', 'avgnn', 'sdnn', 'rmssd', 'sdsd', 'nn50', 'pnn50', - 'nn20', 'pnn20', 'hf', 'hf_log', 'lf', 'lf_log', 'vlf', 'vlf_log', - 'lftohf', 'hf_peak', 'lf_peak' + "rrtime", + "rrint", + "avgnn", + "sdnn", + "rmssd", + "sdsd", + "nn50", + "pnn50", + "nn20", + "pnn20", + "hf", + "hf_log", + "lf", + "lf_log", + "vlf", + "vlf_log", + "lftohf", + "hf_peak", + "lf_peak", ] diff --git a/peakdet/tests/test_editor.py b/peakdet/tests/test_editor.py index d3dee6d..67e2e37 100644 --- a/peakdet/tests/test_editor.py +++ b/peakdet/tests/test_editor.py @@ -7,9 +7,8 @@ from peakdet import editor from peakdet.tests.utils import get_peak_data - -wheel = namedtuple('wheel', ('step')) -key = namedtuple('key', ('key',)) +wheel = namedtuple("wheel", ("step")) +key = namedtuple("key", ("key",)) def test_PhysioEditor(): @@ -28,14 +27,14 @@ def test_PhysioEditor(): edits.undo() # test key undo (and undo when history doesn't exist) - edits.on_key(key('ctrl+z')) + edits.on_key(key("ctrl+z")) # redo so that there is history on quit edits.on_remove(0, 10, reject=True) edits.on_remove(10, 20, reject=False) # quit editor and clean up edits - edits.on_key(key('ctrl+z')) + edits.on_key(key("ctrl+z")) with pytest.raises(TypeError): editor._PhysioEditor([0, 1, 2]) diff --git a/peakdet/tests/test_external.py b/peakdet/tests/test_external.py index e942faa..23c695e 100644 --- a/peakdet/tests/test_external.py +++ b/peakdet/tests/test_external.py @@ -1,19 +1,21 @@ # -*- coding: utf-8 -*- import pytest + from peakdet import external from peakdet.tests import utils as testutils -DATA = testutils.get_test_data_path('rtpeaks.csv') +DATA = testutils.get_test_data_path("rtpeaks.csv") def test_load_rtpeaks(): for channel in [1, 2, 9]: with pytest.warns(UserWarning): - hist = dict(fname=DATA, channel=channel, fs=1000.) - phys = external.load_rtpeaks(DATA, channel=channel, fs=1000.) - assert phys.history == [('load_rtpeaks', hist)] - assert phys.fs == 1000. + hist = dict(fname=DATA, channel=channel, fs=1000.0) + phys = external.load_rtpeaks(DATA, channel=channel, fs=1000.0) + assert phys.history == [("load_rtpeaks", hist)] + assert phys.fs == 1000.0 with pytest.raises(ValueError): - external.load_rtpeaks(testutils.get_test_data_path('ECG.csv'), - channel=channel, fs=1000.) + external.load_rtpeaks( + testutils.get_test_data_path("ECG.csv"), channel=channel, fs=1000.0 + ) diff --git a/peakdet/tests/test_io.py b/peakdet/tests/test_io.py index db02ebd..8e032ad 100644 --- a/peakdet/tests/test_io.py +++ b/peakdet/tests/test_io.py @@ -1,63 +1,68 @@ # -*- coding: utf-8 -*- -import os import json +import os + import numpy as np import pytest + from peakdet import io, operations, physio from peakdet.tests.utils import get_test_data_path def test_load_physio(): # try loading pickle file (from io.save_physio) - pckl = io.load_physio(get_test_data_path('ECG.phys'), allow_pickle=True) + pckl = io.load_physio(get_test_data_path("ECG.phys"), allow_pickle=True) assert isinstance(pckl, physio.Physio) assert pckl.data.size == 44611 - assert pckl.fs == 1000. + assert pckl.fs == 1000.0 with pytest.warns(UserWarning): - pckl = io.load_physio(get_test_data_path('ECG.phys'), fs=500., - allow_pickle=True) - assert pckl.fs == 500. + pckl = io.load_physio( + get_test_data_path("ECG.phys"), fs=500.0, allow_pickle=True + ) + assert pckl.fs == 500.0 # try loading CSV file - csv = io.load_physio(get_test_data_path('ECG.csv')) + csv = io.load_physio(get_test_data_path("ECG.csv")) assert isinstance(csv, physio.Physio) assert np.allclose(csv, pckl) assert np.isnan(csv.fs) - assert csv.history[0][0] == 'load_physio' + assert csv.history[0][0] == "load_physio" # try loading array with pytest.warns(UserWarning): - arr = io.load_physio(np.loadtxt(get_test_data_path('ECG.csv'))) + arr = io.load_physio(np.loadtxt(get_test_data_path("ECG.csv"))) assert isinstance(arr, physio.Physio) - arr = io.load_physio(np.loadtxt(get_test_data_path('ECG.csv')), - history=[('np.loadtxt', {'fname': 'ECG.csv'})]) + arr = io.load_physio( + np.loadtxt(get_test_data_path("ECG.csv")), + history=[("np.loadtxt", {"fname": "ECG.csv"})], + ) assert isinstance(arr, physio.Physio) # try loading physio object (and resetting dtype) - out = io.load_physio(arr, dtype='float32') - assert out.data.dtype == np.dtype('float32') - assert out.history[0][0] == 'np.loadtxt' - assert out.history[-1][0] == 'load_physio' + out = io.load_physio(arr, dtype="float32") + assert out.data.dtype == np.dtype("float32") + assert out.history[0][0] == "np.loadtxt" + assert out.history[-1][0] == "load_physio" with pytest.raises(TypeError): io.load_physio([1, 2, 3]) def test_save_physio(tmpdir): - pckl = io.load_physio(get_test_data_path('ECG.phys'), allow_pickle=True) - out = io.save_physio(tmpdir.join('tmp').purebasename, pckl) + pckl = io.load_physio(get_test_data_path("ECG.phys"), allow_pickle=True) + out = io.save_physio(tmpdir.join("tmp").purebasename, pckl) assert os.path.exists(out) assert isinstance(io.load_physio(out, allow_pickle=True), physio.Physio) def test_load_history(tmpdir): # get paths of data, new history - fname = get_test_data_path('ECG.csv') - temp_history = tmpdir.join('tmp').purebasename + fname = get_test_data_path("ECG.csv") + temp_history = tmpdir.join("tmp").purebasename # make physio object and perform some operations - phys = io.load_physio(fname, fs=1000.) - filt = operations.filter_physio(phys, [5., 15.], 'bandpass') + phys = io.load_physio(fname, fs=1000.0) + filt = operations.filter_physio(phys, [5.0, 15.0], "bandpass") # save history to file and recreate new object from history path = io.save_history(temp_history, filt) @@ -71,20 +76,20 @@ def test_load_history(tmpdir): def test_save_history(tmpdir): # get paths of data, original history, new history - fname = get_test_data_path('ECG.csv') - orig_history = get_test_data_path('history.json') - temp_history = tmpdir.join('tmp').purebasename + fname = get_test_data_path("ECG.csv") + orig_history = get_test_data_path("history.json") + temp_history = tmpdir.join("tmp").purebasename # make physio object and perform some operations - phys = physio.Physio(np.loadtxt(fname), fs=1000.) + phys = physio.Physio(np.loadtxt(fname), fs=1000.0) with pytest.warns(UserWarning): # no history = warning io.save_history(temp_history, phys) - filt = operations.filter_physio(phys, [5., 15.], 'bandpass') + filt = operations.filter_physio(phys, [5.0, 15.0], "bandpass") path = io.save_history(temp_history, filt) # dump history= # load both original and new json and ensure equality - with open(path, 'r') as src: + with open(path, "r") as src: hist = json.load(src) - with open(orig_history, 'r') as src: + with open(orig_history, "r") as src: orig = json.load(src) assert hist == orig diff --git a/peakdet/tests/test_operations.py b/peakdet/tests/test_operations.py index 4257e53..1b1f110 100644 --- a/peakdet/tests/test_operations.py +++ b/peakdet/tests/test_operations.py @@ -4,49 +4,50 @@ import matplotlib.pyplot as plt import numpy as np import pytest + from peakdet import operations from peakdet.physio import Physio from peakdet.tests import utils as testutils -data = np.loadtxt(testutils.get_test_data_path('ECG.csv')) -WITHFS = Physio(data, fs=1000.) +data = np.loadtxt(testutils.get_test_data_path("ECG.csv")) +WITHFS = Physio(data, fs=1000.0) NOFS = Physio(data) def test_filter_physio(): # check lowpass and highpass filters - for meth in ['lowpass', 'highpass']: + for meth in ["lowpass", "highpass"]: params = dict(cutoffs=2, method=meth) assert len(WITHFS) == len(operations.filter_physio(WITHFS, **params)) - params['order'] = 5 + params["order"] = 5 assert len(WITHFS) == len(operations.filter_physio(WITHFS, **params)) - params['cutoffs'] = [2, 10] + params["cutoffs"] = [2, 10] with pytest.raises(ValueError): operations.filter_physio(WITHFS, **params) with pytest.raises(ValueError): operations.filter_physio(NOFS, **params) # check bandpass and bandstop filters - for meth in ['bandpass', 'bandstop']: + for meth in ["bandpass", "bandstop"]: params = dict(cutoffs=[2, 10], method=meth) assert len(WITHFS) == len(operations.filter_physio(WITHFS, **params)) - params['order'] = 5 + params["order"] = 5 assert len(WITHFS) == len(operations.filter_physio(WITHFS, **params)) - params['cutoffs'] = 2 + params["cutoffs"] = 2 with pytest.raises(ValueError): operations.filter_physio(WITHFS, **params) with pytest.raises(ValueError): operations.filter_physio(NOFS, **params) # check appropriate filter methods with pytest.raises(ValueError): - operations.filter_physio(WITHFS, 2, 'notafilter') + operations.filter_physio(WITHFS, 2, "notafilter") # check nyquist with pytest.raises(ValueError): - operations.filter_physio(WITHFS, [2, 1000], 'bandpass') + operations.filter_physio(WITHFS, [2, 1000], "bandpass") def test_interpolate_physio(): with pytest.raises(ValueError): - operations.interpolate_physio(NOFS, 100.) + operations.interpolate_physio(NOFS, 100.0) for fn in [50, 100, 200, 500, 2000, 5000]: new = operations.interpolate_physio(WITHFS, fn) assert new.fs == fn diff --git a/peakdet/tests/test_physio.py b/peakdet/tests/test_physio.py index 55df27a..cc6b5e8 100644 --- a/peakdet/tests/test_physio.py +++ b/peakdet/tests/test_physio.py @@ -2,88 +2,92 @@ import numpy as np import pytest + from peakdet.physio import Physio from peakdet.tests import utils as testutils -DATA = np.loadtxt(testutils.get_test_data_path('ECG.csv')) -PROPERTIES = ['data', 'fs', 'history', 'peaks', 'troughs', '_masked'] +DATA = np.loadtxt(testutils.get_test_data_path("ECG.csv")) +PROPERTIES = ["data", "fs", "history", "peaks", "troughs", "_masked"] PHYSIO_TESTS = [ # accepts "correct" inputs for history - dict( - kwargs=dict(data=DATA, history=[('good', 'history')]) - ), + dict(kwargs=dict(data=DATA, history=[("good", "history")])), # fails on bad inputs for history - dict( - kwargs=dict(data=DATA, history=['malformed', 'history']), - raises=TypeError - ), - dict( - kwargs=dict(data=DATA, history='not real history'), - raises=TypeError - ), + dict(kwargs=dict(data=DATA, history=["malformed", "history"]), raises=TypeError), + dict(kwargs=dict(data=DATA, history="not real history"), raises=TypeError), # accepts "correct" for metadata - dict( - kwargs=dict(data=DATA, metadata=dict()) - ), - dict( - kwargs=dict(data=DATA, metadata=dict(peaks=[], reject=[], troughs=[])) - ), + dict(kwargs=dict(data=DATA, metadata=dict())), + dict(kwargs=dict(data=DATA, metadata=dict(peaks=[], reject=[], troughs=[]))), # fails on bad inputs for metadata - dict( - kwargs=dict(data=DATA, metadata=[]), - raises=TypeError - ), - dict( - kwargs=dict(data=DATA, metadata=dict(peaks={})), - raises=TypeError - ), + dict(kwargs=dict(data=DATA, metadata=[]), raises=TypeError), + dict(kwargs=dict(data=DATA, metadata=dict(peaks={})), raises=TypeError), # fails on bad inputs for data - dict( - kwargs=dict(data=np.column_stack([DATA, DATA])), - raises=ValueError - ), - dict( - kwargs=dict(data='hello'), - raises=ValueError - ) + dict(kwargs=dict(data=np.column_stack([DATA, DATA])), raises=ValueError), + dict(kwargs=dict(data="hello"), raises=ValueError), ] def test_physio(): phys = Physio(DATA, fs=1000) assert len(np.hstack((phys[:10], phys[10:-10], phys[-10:]))) - assert str(phys) == 'Physio(size=44611, fs=1000.0)' + assert str(phys) == "Physio(size=44611, fs=1000.0)" assert len(np.exp(phys)) == 44611 -class TestPhysio(): +class TestPhysio: tests = PHYSIO_TESTS def test_physio_creation(self): for test in PHYSIO_TESTS: - if test.get('raises') is not None: - with pytest.raises(test['raises']): - phys = Physio(**test['kwargs']) + if test.get("raises") is not None: + with pytest.raises(test["raises"]): + phys = Physio(**test["kwargs"]) else: - phys = Physio(**test['kwargs']) + phys = Physio(**test["kwargs"]) for prop in PROPERTIES: assert hasattr(phys, prop) - for prop in ['peaks', 'reject', 'troughs']: + for prop in ["peaks", "reject", "troughs"]: assert isinstance(phys._metadata.get(prop), np.ndarray) def test_neurokit2phys(path_neurokit): - df = pd.read_csv(path_neurokit, sep='\t') - phys = Physio.neurokit2phys(path_neurokit, copy_data=True, copy_peaks=True, copy_troughs=True, fs=fs) + df = pd.read_csv(path_neurokit, sep="\t") + phys = Physio.neurokit2phys( + path_neurokit, copy_data=True, copy_peaks=True, copy_troughs=True, fs=fs + ) - assert all(np.unique(phys.data == np.hstack(df.loc[:, df.columns.str.endswith('Clean')].to_numpy()))) - assert all(np.unique(phys.peaks == np.where(df.loc[:, df.columns.str.endswith('Peaks')] != 0)[0])) + assert all( + np.unique( + phys.data + == np.hstack(df.loc[:, df.columns.str.endswith("Clean")].to_numpy()) + ) + ) + assert all( + np.unique( + phys.peaks == np.where(df.loc[:, df.columns.str.endswith("Peaks")] != 0)[0] + ) + ) assert phys.fs == fs def test_phys2neurokit(path_phys): phys = load_physio(path_phys, allow_pickle=True) - neuro = data.phys2neurokit(copy_data=True, copy_peaks=True, copy_troughs=False, module=module, neurokit_path=path_neurokit) + neuro = data.phys2neurokit( + copy_data=True, + copy_peaks=True, + copy_troughs=False, + module=module, + neurokit_path=path_neurokit, + ) - assert all(np.unique(phys.data == np.hstack(neuro.loc[:, neuro.columns.str.endswith('Raw')].to_numpy()))) - assert all(np.unique(phys.peaks == np.where(neuro.loc[:, neuro.columns.str.endswith('Peaks')] != 0)[0])) + assert all( + np.unique( + phys.data + == np.hstack(neuro.loc[:, neuro.columns.str.endswith("Raw")].to_numpy()) + ) + ) + assert all( + np.unique( + phys.peaks + == np.where(neuro.loc[:, neuro.columns.str.endswith("Peaks")] != 0)[0] + ) + ) diff --git a/peakdet/tests/test_utils.py b/peakdet/tests/test_utils.py index fa15da7..f54a160 100644 --- a/peakdet/tests/test_utils.py +++ b/peakdet/tests/test_utils.py @@ -1,8 +1,9 @@ # -*- coding: utf-8 -*- import numpy as np -from numpy.testing import assert_array_equal import pytest +from numpy.testing import assert_array_equal + from peakdet import physio, utils from peakdet.tests import utils as testutils @@ -10,47 +11,35 @@ GET_CALL_ARGUMENTS = [ # check basic functionality dict( - function='get_call_func', - input=dict( - arg1=1, arg2=2, serializable=False - ), - expected=dict( - arg1=1, arg2=2, kwarg1=10, kwarg2=20 - ) + function="get_call_func", + input=dict(arg1=1, arg2=2, serializable=False), + expected=dict(arg1=1, arg2=2, kwarg1=10, kwarg2=20), ), # check that changing kwargs in function persists to output dict( - function='get_call_func', - input=dict( - arg1=11, arg2=21, serializable=False - ), - expected=dict( - arg1=11, arg2=21, kwarg1=21, kwarg2=41 - ) + function="get_call_func", + input=dict(arg1=11, arg2=21, serializable=False), + expected=dict(arg1=11, arg2=21, kwarg1=21, kwarg2=41), ), # confirm serializability is effective dict( - function='get_call_func', - input=dict( - arg1=1, arg2=2, kwarg1=np.array([1, 2, 3]), serializable=True - ), - expected=dict( - arg1=1, arg2=2, kwarg1=[1, 2, 3], kwarg2=20 - ) + function="get_call_func", + input=dict(arg1=1, arg2=2, kwarg1=np.array([1, 2, 3]), serializable=True), + expected=dict(arg1=1, arg2=2, kwarg1=[1, 2, 3], kwarg2=20), ), ] def test_get_call(): for entry in GET_CALL_ARGUMENTS: - fcn, args = testutils.get_call_func(**entry['input']) - assert fcn == entry['function'] - assert args == entry['expected'] + fcn, args = testutils.get_call_func(**entry["input"]) + assert fcn == entry["function"] + assert args == entry["expected"] def test_check_physio(): - fname = testutils.get_test_data_path('ECG.csv') - data = physio.Physio(np.loadtxt(fname), fs=1000.) + fname = testutils.get_test_data_path("ECG.csv") + data = physio.Physio(np.loadtxt(fname), fs=1000.0) # check that `ensure_fs` is functional with pytest.raises(ValueError): utils.check_physio(fname) @@ -63,10 +52,10 @@ def test_check_physio(): def test_new_physio_like(): - fname = testutils.get_test_data_path('ECG.csv') - data = physio.Physio(np.loadtxt(fname), fs=1000.) - data._history = [('does history', 'copy?')] - data._metadata['peaks'] = np.array([1, 2, 3]) + fname = testutils.get_test_data_path("ECG.csv") + data = physio.Physio(np.loadtxt(fname), fs=1000.0) + data._history = [("does history", "copy?")] + data._metadata["peaks"] = np.array([1, 2, 3]) # assert all copies happen by default new_data = utils.new_physio_like(data, data[:]) assert np.allclose(data, utils.new_physio_like(data, data[:])) @@ -75,8 +64,9 @@ def test_new_physio_like(): assert new_data.history == data.history assert new_data._metadata == data._metadata # check if changes apply - new_data = utils.new_physio_like(data, data[:], fs=50, dtype=int, - copy_history=False, copy_metadata=False) + new_data = utils.new_physio_like( + data, data[:], fs=50, dtype=int, copy_history=False, copy_metadata=False + ) assert np.allclose(data, utils.new_physio_like(data, data[:])) assert new_data.fs == 50 assert new_data.data.dtype == int diff --git a/peakdet/tests/utils.py b/peakdet/tests/utils.py index 0781222..f5fedcb 100644 --- a/peakdet/tests/utils.py +++ b/peakdet/tests/utils.py @@ -3,16 +3,24 @@ """ from os.path import join as pjoin -from pkg_resources import resource_filename + import numpy as np +from pkg_resources import resource_filename + from peakdet import io, operations from peakdet.utils import _get_call -def get_call_func(arg1, arg2, *, kwarg1=10, kwarg2=20, - exclude=['exclude', 'serializable'], - serializable=True): - """ Function for testing `peakdet.utils._get_call()` """ +def get_call_func( + arg1, + arg2, + *, + kwarg1=10, + kwarg2=20, + exclude=["exclude", "serializable"], + serializable=True +): + """Function for testing `peakdet.utils._get_call()`""" if arg1 > 10: kwarg1 = kwarg1 + arg1 if arg2 > 20: @@ -21,13 +29,13 @@ def get_call_func(arg1, arg2, *, kwarg1=10, kwarg2=20, def get_test_data_path(fname=None): - """ Function for getting `peakdet` test data path """ - path = resource_filename('peakdet', 'tests/data') + """Function for getting `peakdet` test data path""" + path = resource_filename("peakdet", "tests/data") return pjoin(path, fname) if fname is not None else path def get_sample_data(): - """ Function for generating tiny sine wave form for testing """ + """Function for generating tiny sine wave form for testing""" data = np.sin(np.linspace(0, 20, 40)) peaks, troughs = np.array([3, 15, 28]), np.array([9, 21, 34]) @@ -35,9 +43,9 @@ def get_sample_data(): def get_peak_data(): - """ Function for getting some pregenerated physio data """ - physio = io.load_physio(get_test_data_path('ECG.csv'), fs=1000) - filt = operations.filter_physio(physio, [5., 15.], 'bandpass') + """Function for getting some pregenerated physio data""" + physio = io.load_physio(get_test_data_path("ECG.csv"), fs=1000) + filt = operations.filter_physio(physio, [5.0, 15.0], "bandpass") peaks = operations.peakfind_physio(filt) return peaks diff --git a/peakdet/utils.py b/peakdet/utils.py index 7858f0b..523bf07 100644 --- a/peakdet/utils.py +++ b/peakdet/utils.py @@ -4,9 +4,11 @@ directly but should support wrapper functions stored in `peakdet.operations`. """ -from functools import wraps import inspect +from functools import wraps + import numpy as np + from peakdet import physio @@ -29,7 +31,7 @@ def get_call(func): @wraps(func) def wrapper(data, *args, **kwargs): # exclude 'data', by default - ignore = ['data'] if exclude is None else exclude + ignore = ["data"] if exclude is None else exclude # grab parameters from `func` by binding signature name = func.__name__ @@ -47,17 +49,18 @@ def wrapper(data, *args, **kwargs): # attempting to coerce any numpy arrays or pandas dataframes (?!) # into serializable objects; this isn't foolproof but gets 80% of # the way there - provided = {k: params[k] for k in sorted(params.keys()) - if k not in ignore} + provided = {k: params[k] for k in sorted(params.keys()) if k not in ignore} for k, v in provided.items(): - if hasattr(v, 'tolist'): + if hasattr(v, "tolist"): provided[k] = v.tolist() # append everything to data instance history data._history += [(name, provided)] return data + return wrapper + return get_call @@ -82,7 +85,7 @@ def _get_call(*, exclude=None, serializable=True): Dictionary of function arguments and provided values """ - exclude = ['data'] if exclude is None else exclude + exclude = ["data"] if exclude is None else exclude if not isinstance(exclude, list): exclude = [exclude] @@ -102,7 +105,7 @@ def _get_call(*, exclude=None, serializable=True): # to be the main issue with these sorts of things if serializable: for k, v in provided.items(): - if hasattr(v, 'tolist'): + if hasattr(v, "tolist"): provided[k] = v.tolist() return function, provided @@ -137,17 +140,25 @@ def check_physio(data, ensure_fs=True, copy=False): if not isinstance(data, physio.Physio): data = load_physio(data) if ensure_fs and np.isnan(data.fs): - raise ValueError('Provided data does not have valid sampling rate.') + raise ValueError("Provided data does not have valid sampling rate.") if copy is True: - return new_physio_like(data, data.data, - copy_history=True, - copy_metadata=True, - copy_suppdata=True) + return new_physio_like( + data, data.data, copy_history=True, copy_metadata=True, copy_suppdata=True + ) return data -def new_physio_like(ref_physio, data, *, fs=None, suppdata=None, dtype=None, - copy_history=True, copy_metadata=True, copy_suppdata=True): +def new_physio_like( + ref_physio, + data, + *, + fs=None, + suppdata=None, + dtype=None, + copy_history=True, + copy_metadata=True, + copy_suppdata=True +): """ Makes `data` into physio object like `ref_data` @@ -188,9 +199,13 @@ def new_physio_like(ref_physio, data, *, fs=None, suppdata=None, dtype=None, suppdata = ref_physio._suppdata if copy_suppdata else None # make new class - out = ref_physio.__class__(np.array(data, dtype=dtype), - fs=fs, history=history, metadata=metadata, - suppdata=suppdata) + out = ref_physio.__class__( + np.array(data, dtype=dtype), + fs=fs, + history=history, + metadata=metadata, + suppdata=suppdata, + ) return out @@ -220,7 +235,7 @@ def check_troughs(data, peaks, troughs=None): all_troughs = np.zeros(peaks.size - 1, dtype=int) for f in range(peaks.size - 1): - dp = data[peaks[f]:peaks[f + 1]] + dp = data[peaks[f] : peaks[f + 1]] idx = peaks[f] + np.argwhere(dp == dp.min())[0] all_troughs[f] = idx diff --git a/setup.cfg b/setup.cfg index f5cdf04..0f3881e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -65,7 +65,7 @@ all = [options.package_data] -* = +* = peakdet/tests/data/* [flake8] diff --git a/setup.py b/setup.py index 926e1a2..4ade8ab 100644 --- a/setup.py +++ b/setup.py @@ -2,13 +2,16 @@ import sys from setuptools import setup + import versioneer -SETUP_REQUIRES = ['setuptools >= 30.3.0'] -SETUP_REQUIRES += ['wheel'] if 'bdist_wheel' in sys.argv else [] +SETUP_REQUIRES = ["setuptools >= 30.3.0"] +SETUP_REQUIRES += ["wheel"] if "bdist_wheel" in sys.argv else [] if __name__ == "__main__": - setup(name='peakdet', - setup_requires=SETUP_REQUIRES, - version=versioneer.get_version(), - cmdclass=versioneer.get_cmdclass()) + setup( + name="peakdet", + setup_requires=SETUP_REQUIRES, + version=versioneer.get_version(), + cmdclass=versioneer.get_cmdclass(), + ) diff --git a/versioneer.py b/versioneer.py index 64fea1c..2b54540 100644 --- a/versioneer.py +++ b/versioneer.py @@ -1,4 +1,3 @@ - # Version: 0.18 """The Versioneer - like a rocketeer, but for versions. @@ -277,6 +276,7 @@ """ from __future__ import print_function + try: import configparser except ImportError: @@ -308,11 +308,13 @@ def get_root(): setup_py = os.path.join(root, "setup.py") versioneer_py = os.path.join(root, "versioneer.py") if not (os.path.exists(setup_py) or os.path.exists(versioneer_py)): - err = ("Versioneer was unable to run the project root directory. " - "Versioneer requires setup.py to be executed from " - "its immediate directory (like 'python setup.py COMMAND'), " - "or in a way that lets it use sys.argv[0] to find the root " - "(like 'python path/to/setup.py COMMAND').") + err = ( + "Versioneer was unable to run the project root directory. " + "Versioneer requires setup.py to be executed from " + "its immediate directory (like 'python setup.py COMMAND'), " + "or in a way that lets it use sys.argv[0] to find the root " + "(like 'python path/to/setup.py COMMAND')." + ) raise VersioneerBadRootError(err) try: # Certain runtime workflows (setup.py install/develop in a setuptools @@ -325,8 +327,10 @@ def get_root(): me_dir = os.path.normcase(os.path.splitext(me)[0]) vsr_dir = os.path.normcase(os.path.splitext(versioneer_py)[0]) if me_dir != vsr_dir: - print("Warning: build in %s is using versioneer.py from %s" - % (os.path.dirname(me), versioneer_py)) + print( + "Warning: build in %s is using versioneer.py from %s" + % (os.path.dirname(me), versioneer_py) + ) except NameError: pass return root @@ -348,6 +352,7 @@ def get(parser, name): if parser.has_option("versioneer", name): return parser.get("versioneer", name) return None + cfg = VersioneerConfig() cfg.VCS = VCS cfg.style = get(parser, "style") or "" @@ -372,17 +377,18 @@ class NotThisMethod(Exception): def register_vcs_handler(vcs, method): # decorator """Decorator to mark a method as the handler for a particular VCS.""" + def decorate(f): """Store f in HANDLERS[vcs][method].""" if vcs not in HANDLERS: HANDLERS[vcs] = {} HANDLERS[vcs][method] = f return f + return decorate -def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, - env=None): +def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, env=None): """Call the given command(s).""" assert isinstance(commands, list) p = None @@ -390,10 +396,13 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, try: dispcmd = str([c] + args) # remember shell=False, so use git.cmd on windows, not just git - p = subprocess.Popen([c] + args, cwd=cwd, env=env, - stdout=subprocess.PIPE, - stderr=(subprocess.PIPE if hide_stderr - else None)) + p = subprocess.Popen( + [c] + args, + cwd=cwd, + env=env, + stdout=subprocess.PIPE, + stderr=(subprocess.PIPE if hide_stderr else None), + ) break except EnvironmentError: e = sys.exc_info()[1] @@ -418,7 +427,9 @@ def run_command(commands, args, cwd=None, verbose=False, hide_stderr=False, return stdout, p.returncode -LONG_VERSION_PY['git'] = ''' +LONG_VERSION_PY[ + "git" +] = ''' # This file helps to compute a version number in source trees obtained from # git-archive tarball (such as those provided by githubs download-from-tag # feature). Distribution tarballs (built by setup.py sdist) and build @@ -993,7 +1004,7 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): # starting in git-1.8.3, tags are listed as "tag: foo-1.0" instead of # just "foo-1.0". If we see a "tag: " prefix, prefer those. TAG = "tag: " - tags = set([r[len(TAG):] for r in refs if r.startswith(TAG)]) + tags = set([r[len(TAG) :] for r in refs if r.startswith(TAG)]) if not tags: # Either we're using git < 1.8.3, or there really are no tags. We use # a heuristic: assume all version tags have a digit. The old git %d @@ -1002,7 +1013,7 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): # between branches and tags. By ignoring refnames without digits, we # filter out many common branch names like "release" and # "stabilization", as well as "HEAD" and "master". - tags = set([r for r in refs if re.search(r'\d', r)]) + tags = set([r for r in refs if re.search(r"\d", r)]) if verbose: print("discarding '%s', no digits" % ",".join(refs - tags)) if verbose: @@ -1010,19 +1021,26 @@ def git_versions_from_keywords(keywords, tag_prefix, verbose): for ref in sorted(tags): # sorting will prefer e.g. "2.0" over "2.0rc1" if ref.startswith(tag_prefix): - r = ref[len(tag_prefix):] + r = ref[len(tag_prefix) :] if verbose: print("picking %s" % r) - return {"version": r, - "full-revisionid": keywords["full"].strip(), - "dirty": False, "error": None, - "date": date} + return { + "version": r, + "full-revisionid": keywords["full"].strip(), + "dirty": False, + "error": None, + "date": date, + } # no suitable tags, so version is "0+unknown", but full hex is still there if verbose: print("no suitable tags, using unknown + full revision id") - return {"version": "0+unknown", - "full-revisionid": keywords["full"].strip(), - "dirty": False, "error": "no suitable tags", "date": None} + return { + "version": "0+unknown", + "full-revisionid": keywords["full"].strip(), + "dirty": False, + "error": "no suitable tags", + "date": None, + } @register_vcs_handler("git", "pieces_from_vcs") @@ -1037,8 +1055,7 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): if sys.platform == "win32": GITS = ["git.cmd", "git.exe"] - out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, - hide_stderr=True) + out, rc = run_command(GITS, ["rev-parse", "--git-dir"], cwd=root, hide_stderr=True) if rc != 0: if verbose: print("Directory %s not under git control" % root) @@ -1046,10 +1063,19 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): # if there is a tag matching tag_prefix, this yields TAG-NUM-gHEX[-dirty] # if there isn't one, this yields HEX[-dirty] (no NUM) - describe_out, rc = run_command(GITS, ["describe", "--tags", "--dirty", - "--always", "--long", - "--match", "%s*" % tag_prefix], - cwd=root) + describe_out, rc = run_command( + GITS, + [ + "describe", + "--tags", + "--dirty", + "--always", + "--long", + "--match", + "%s*" % tag_prefix, + ], + cwd=root, + ) # --long was added in git-1.5.5 if describe_out is None: raise NotThisMethod("'git describe' failed") @@ -1072,17 +1098,16 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): dirty = git_describe.endswith("-dirty") pieces["dirty"] = dirty if dirty: - git_describe = git_describe[:git_describe.rindex("-dirty")] + git_describe = git_describe[: git_describe.rindex("-dirty")] # now we have TAG-NUM-gHEX or HEX if "-" in git_describe: # TAG-NUM-gHEX - mo = re.search(r'^(.+)-(\d+)-g([0-9a-f]+)$', git_describe) + mo = re.search(r"^(.+)-(\d+)-g([0-9a-f]+)$", git_describe) if not mo: # unparseable. Maybe git-describe is misbehaving? - pieces["error"] = ("unable to parse git-describe output: '%s'" - % describe_out) + pieces["error"] = "unable to parse git-describe output: '%s'" % describe_out return pieces # tag @@ -1091,10 +1116,12 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): if verbose: fmt = "tag '%s' doesn't start with prefix '%s'" print(fmt % (full_tag, tag_prefix)) - pieces["error"] = ("tag '%s' doesn't start with prefix '%s'" - % (full_tag, tag_prefix)) + pieces["error"] = "tag '%s' doesn't start with prefix '%s'" % ( + full_tag, + tag_prefix, + ) return pieces - pieces["closest-tag"] = full_tag[len(tag_prefix):] + pieces["closest-tag"] = full_tag[len(tag_prefix) :] # distance: number of commits since tag pieces["distance"] = int(mo.group(2)) @@ -1105,13 +1132,13 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): else: # HEX: no tags pieces["closest-tag"] = None - count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], - cwd=root) + count_out, rc = run_command(GITS, ["rev-list", "HEAD", "--count"], cwd=root) pieces["distance"] = int(count_out) # total number of commits # commit date: see ISO-8601 comment in git_versions_from_keywords() - date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"], - cwd=root)[0].strip() + date = run_command(GITS, ["show", "-s", "--format=%ci", "HEAD"], cwd=root)[ + 0 + ].strip() pieces["date"] = date.strip().replace(" ", "T", 1).replace(" ", "", 1) return pieces @@ -1167,16 +1194,22 @@ def versions_from_parentdir(parentdir_prefix, root, verbose): for i in range(3): dirname = os.path.basename(root) if dirname.startswith(parentdir_prefix): - return {"version": dirname[len(parentdir_prefix):], - "full-revisionid": None, - "dirty": False, "error": None, "date": None} + return { + "version": dirname[len(parentdir_prefix) :], + "full-revisionid": None, + "dirty": False, + "error": None, + "date": None, + } else: rootdirs.append(root) root = os.path.dirname(root) # up a level if verbose: - print("Tried directories %s but none started with prefix %s" % - (str(rootdirs), parentdir_prefix)) + print( + "Tried directories %s but none started with prefix %s" + % (str(rootdirs), parentdir_prefix) + ) raise NotThisMethod("rootdir doesn't start with parentdir_prefix") @@ -1205,11 +1238,13 @@ def versions_from_file(filename): contents = f.read() except EnvironmentError: raise NotThisMethod("unable to read _version.py") - mo = re.search(r"version_json = '''\n(.*)''' # END VERSION_JSON", - contents, re.M | re.S) + mo = re.search( + r"version_json = '''\n(.*)''' # END VERSION_JSON", contents, re.M | re.S + ) if not mo: - mo = re.search(r"version_json = '''\r\n(.*)''' # END VERSION_JSON", - contents, re.M | re.S) + mo = re.search( + r"version_json = '''\r\n(.*)''' # END VERSION_JSON", contents, re.M | re.S + ) if not mo: raise NotThisMethod("no version_json in _version.py") return json.loads(mo.group(1)) @@ -1218,8 +1253,7 @@ def versions_from_file(filename): def write_to_version_file(filename, versions): """Write the given version number to the given _version.py file.""" os.unlink(filename) - contents = json.dumps(versions, sort_keys=True, - indent=1, separators=(",", ": ")) + contents = json.dumps(versions, sort_keys=True, indent=1, separators=(",", ": ")) with open(filename, "w") as f: f.write(SHORT_VERSION_PY % contents) @@ -1251,8 +1285,7 @@ def render_pep440(pieces): rendered += ".dirty" else: # exception #1 - rendered = "0+untagged.%d.g%s" % (pieces["distance"], - pieces["short"]) + rendered = "0+untagged.%d.g%s" % (pieces["distance"], pieces["short"]) if pieces["dirty"]: rendered += ".dirty" return rendered @@ -1366,11 +1399,13 @@ def render_git_describe_long(pieces): def render(pieces, style): """Render the given version pieces into the requested style.""" if pieces["error"]: - return {"version": "unknown", - "full-revisionid": pieces.get("long"), - "dirty": None, - "error": pieces["error"], - "date": None} + return { + "version": "unknown", + "full-revisionid": pieces.get("long"), + "dirty": None, + "error": pieces["error"], + "date": None, + } if not style or style == "default": style = "pep440" # the default @@ -1390,9 +1425,13 @@ def render(pieces, style): else: raise ValueError("unknown style '%s'" % style) - return {"version": rendered, "full-revisionid": pieces["long"], - "dirty": pieces["dirty"], "error": None, - "date": pieces.get("date")} + return { + "version": rendered, + "full-revisionid": pieces["long"], + "dirty": pieces["dirty"], + "error": None, + "date": pieces.get("date"), + } class VersioneerBadRootError(Exception): @@ -1415,8 +1454,9 @@ def get_versions(verbose=False): handlers = HANDLERS.get(cfg.VCS) assert handlers, "unrecognized VCS '%s'" % cfg.VCS verbose = verbose or cfg.verbose - assert cfg.versionfile_source is not None, \ - "please set versioneer.versionfile_source" + assert ( + cfg.versionfile_source is not None + ), "please set versioneer.versionfile_source" assert cfg.tag_prefix is not None, "please set versioneer.tag_prefix" versionfile_abs = os.path.join(root, cfg.versionfile_source) @@ -1470,9 +1510,13 @@ def get_versions(verbose=False): if verbose: print("unable to compute version") - return {"version": "0+unknown", "full-revisionid": None, - "dirty": None, "error": "unable to compute version", - "date": None} + return { + "version": "0+unknown", + "full-revisionid": None, + "dirty": None, + "error": "unable to compute version", + "date": None, + } def get_version(): @@ -1521,6 +1565,7 @@ def run(self): print(" date: %s" % vers.get("date")) if vers["error"]: print(" error: %s" % vers["error"]) + cmds["version"] = cmd_version # we override "build_py" in both distutils and setuptools @@ -1553,14 +1598,15 @@ def run(self): # now locate _version.py in the new build/ directory and replace # it with an updated value if cfg.versionfile_build: - target_versionfile = os.path.join(self.build_lib, - cfg.versionfile_build) + target_versionfile = os.path.join(self.build_lib, cfg.versionfile_build) print("UPDATING %s" % target_versionfile) write_to_version_file(target_versionfile, versions) + cmds["build_py"] = cmd_build_py if "cx_Freeze" in sys.modules: # cx_freeze enabled? from cx_Freeze.dist import build_exe as _build_exe + # nczeczulin reports that py2exe won't like the pep440-style string # as FILEVERSION, but it can be used for PRODUCTVERSION, e.g. # setup(console=[{ @@ -1581,17 +1627,21 @@ def run(self): os.unlink(target_versionfile) with open(cfg.versionfile_source, "w") as f: LONG = LONG_VERSION_PY[cfg.VCS] - f.write(LONG % - {"DOLLAR": "$", - "STYLE": cfg.style, - "TAG_PREFIX": cfg.tag_prefix, - "PARENTDIR_PREFIX": cfg.parentdir_prefix, - "VERSIONFILE_SOURCE": cfg.versionfile_source, - }) + f.write( + LONG + % { + "DOLLAR": "$", + "STYLE": cfg.style, + "TAG_PREFIX": cfg.tag_prefix, + "PARENTDIR_PREFIX": cfg.parentdir_prefix, + "VERSIONFILE_SOURCE": cfg.versionfile_source, + } + ) + cmds["build_exe"] = cmd_build_exe del cmds["build_py"] - if 'py2exe' in sys.modules: # py2exe enabled? + if "py2exe" in sys.modules: # py2exe enabled? try: from py2exe.distutils_buildexe import py2exe as _py2exe # py3 except ImportError: @@ -1610,13 +1660,17 @@ def run(self): os.unlink(target_versionfile) with open(cfg.versionfile_source, "w") as f: LONG = LONG_VERSION_PY[cfg.VCS] - f.write(LONG % - {"DOLLAR": "$", - "STYLE": cfg.style, - "TAG_PREFIX": cfg.tag_prefix, - "PARENTDIR_PREFIX": cfg.parentdir_prefix, - "VERSIONFILE_SOURCE": cfg.versionfile_source, - }) + f.write( + LONG + % { + "DOLLAR": "$", + "STYLE": cfg.style, + "TAG_PREFIX": cfg.tag_prefix, + "PARENTDIR_PREFIX": cfg.parentdir_prefix, + "VERSIONFILE_SOURCE": cfg.versionfile_source, + } + ) + cmds["py2exe"] = cmd_py2exe # we override different "sdist" commands for both environments @@ -1643,8 +1697,10 @@ def make_release_tree(self, base_dir, files): # updated value target_versionfile = os.path.join(base_dir, cfg.versionfile_source) print("UPDATING %s" % target_versionfile) - write_to_version_file(target_versionfile, - self._versioneer_generated_versions) + write_to_version_file( + target_versionfile, self._versioneer_generated_versions + ) + cmds["sdist"] = cmd_sdist return cmds @@ -1699,11 +1755,13 @@ def do_setup(): root = get_root() try: cfg = get_config_from_root(root) - except (EnvironmentError, configparser.NoSectionError, - configparser.NoOptionError) as e: + except ( + EnvironmentError, + configparser.NoSectionError, + configparser.NoOptionError, + ) as e: if isinstance(e, (EnvironmentError, configparser.NoSectionError)): - print("Adding sample versioneer config to setup.cfg", - file=sys.stderr) + print("Adding sample versioneer config to setup.cfg", file=sys.stderr) with open(os.path.join(root, "setup.cfg"), "a") as f: f.write(SAMPLE_CONFIG) print(CONFIG_ERROR, file=sys.stderr) @@ -1712,15 +1770,18 @@ def do_setup(): print(" creating %s" % cfg.versionfile_source) with open(cfg.versionfile_source, "w") as f: LONG = LONG_VERSION_PY[cfg.VCS] - f.write(LONG % {"DOLLAR": "$", - "STYLE": cfg.style, - "TAG_PREFIX": cfg.tag_prefix, - "PARENTDIR_PREFIX": cfg.parentdir_prefix, - "VERSIONFILE_SOURCE": cfg.versionfile_source, - }) - - ipy = os.path.join(os.path.dirname(cfg.versionfile_source), - "__init__.py") + f.write( + LONG + % { + "DOLLAR": "$", + "STYLE": cfg.style, + "TAG_PREFIX": cfg.tag_prefix, + "PARENTDIR_PREFIX": cfg.parentdir_prefix, + "VERSIONFILE_SOURCE": cfg.versionfile_source, + } + ) + + ipy = os.path.join(os.path.dirname(cfg.versionfile_source), "__init__.py") if os.path.exists(ipy): try: with open(ipy, "r") as f: @@ -1762,8 +1823,10 @@ def do_setup(): else: print(" 'versioneer.py' already in MANIFEST.in") if cfg.versionfile_source not in simple_includes: - print(" appending versionfile_source ('%s') to MANIFEST.in" % - cfg.versionfile_source) + print( + " appending versionfile_source ('%s') to MANIFEST.in" + % cfg.versionfile_source + ) with open(manifest_in, "a") as f: f.write("include %s\n" % cfg.versionfile_source) else: From 3e8f650bd4524e41babd0cef41b5e506072f29f6 Mon Sep 17 00:00:00 2001 From: maestroque Date: Wed, 29 May 2024 12:17:17 +0300 Subject: [PATCH 03/15] Update setup.cfg with pre-commit hooks dependencies --- setup.cfg | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 0f3881e..f6c169f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -49,6 +49,10 @@ doc = style = flake8 >=3.7 flake8-docstrings >=1.5 + black + isort <6.0.0 + pydocstyle + codespell enhgui = Gooey wxpython @@ -71,13 +75,34 @@ all = [flake8] exclude= *build/ - heuristics tests ignore = E126, E203, E402, W503 max-line-length = 99 per-file-ignores = */__init__.py:F401 +[isort] +profile = black +skip_gitignore = true +extend_skip = + setup.py + versioneer.py + peakdet/_version.py +skip_glob = + docs/* + +[pydocstyle] +convention = numpy +match = + peakdet/*.py +match_dir = peakdet/[^tests]* + +[codespell] +skip = venvs,.venv,versioneer.py,.git,build,./docs/_build +write-changes = +count = +quiet-level = 3 + [coverage:run] omit = peakdet/cli/* From 4fdf45b581bd42ea79b2fdd0de387bfc327c1884 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 29 May 2024 09:17:30 +0000 Subject: [PATCH 04/15] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- docs/user_guide/physio.rst | 2 +- peakdet/__init__.py | 12 +++++++++--- peakdet/_version.py | 2 +- peakdet/operations.py | 4 ++-- peakdet/physio.py | 2 +- peakdet/utils.py | 2 +- 6 files changed, 15 insertions(+), 9 deletions(-) diff --git a/docs/user_guide/physio.rst b/docs/user_guide/physio.rst index e15af86..1370ee7 100644 --- a/docs/user_guide/physio.rst +++ b/docs/user_guide/physio.rst @@ -9,7 +9,7 @@ The ``Physio`` data object -------------------------- -The primary funtionality of :py:mod:`peakdet` relies on its operations being +The primary functionality of :py:mod:`peakdet` relies on its operations being performed on physiological data loaded into a :py:class:`peakdet.Physio` object. So, before we get into using :py:mod:`peakdet`, its best to understand a little bit about this helper class! diff --git a/peakdet/__init__.py b/peakdet/__init__.py index bd9674d..ac6ee0f 100644 --- a/peakdet/__init__.py +++ b/peakdet/__init__.py @@ -19,9 +19,15 @@ from peakdet.analytics import HRV from peakdet.external import load_rtpeaks from peakdet.io import load_history, load_physio, save_history, save_physio -from peakdet.operations import (delete_peaks, edit_physio, filter_physio, - interpolate_physio, peakfind_physio, - plot_physio, reject_peaks) +from peakdet.operations import ( + delete_peaks, + edit_physio, + filter_physio, + interpolate_physio, + peakfind_physio, + plot_physio, + reject_peaks, +) from peakdet.physio import Physio from ._version import get_versions diff --git a/peakdet/_version.py b/peakdet/_version.py index 61e75b2..f1ee66e 100644 --- a/peakdet/_version.py +++ b/peakdet/_version.py @@ -292,7 +292,7 @@ def git_pieces_from_vcs(tag_prefix, root, verbose, run_command=run_command): # TAG-NUM-gHEX mo = re.search(r"^(.+)-(\d+)-g([0-9a-f]+)$", git_describe) if not mo: - # unparseable. Maybe git-describe is misbehaving? + # unparsable. Maybe git-describe is misbehaving? pieces["error"] = "unable to parse git-describe output: '%s'" % describe_out return pieces diff --git a/peakdet/operations.py b/peakdet/operations.py index 9f8458a..0fc1aa1 100644 --- a/peakdet/operations.py +++ b/peakdet/operations.py @@ -107,7 +107,7 @@ def interpolate_physio(data, target_fs, *, kind="cubic"): @utils.make_operation() def peakfind_physio(data, *, thresh=0.2, dist=None): """ - Performs peak and trough detection on `data` + Performs peak and through detection on `data` Parameters ---------- @@ -139,7 +139,7 @@ def peakfind_physio(data, *, thresh=0.2, dist=None): heights = np.percentile(heights["peak_heights"], 1) locs, heights = signal.find_peaks(data[:], distance=cdist, height=heights) data._metadata["peaks"] = locs - # perform trough detection based on detected peaks + # perform through detection based on detected peaks data._metadata["troughs"] = utils.check_troughs(data, data.peaks) return data diff --git a/peakdet/physio.py b/peakdet/physio.py index 37f4906..eb62468 100644 --- a/peakdet/physio.py +++ b/peakdet/physio.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- """ -Helper class for holding physiological data and associated metadata inforamtion +Helper class for holding physiological data and associated metadata information """ import numpy as np diff --git a/peakdet/utils.py b/peakdet/utils.py index 523bf07..fcbf28d 100644 --- a/peakdet/utils.py +++ b/peakdet/utils.py @@ -225,7 +225,7 @@ def check_troughs(data, peaks, troughs=None): Returns ------- troughs : np.ndarray - Indices of trough locations in `data`, dependent on `peaks` + Indices of through locations in `data`, dependent on `peaks` """ # If there's a through after all peaks, keep it. if troughs is not None and troughs[-1] > peaks[-1]: From 137ebb3f2a3f6dd471358e4019d28b6439203271 Mon Sep 17 00:00:00 2001 From: George Kikas Date: Wed, 29 May 2024 12:18:26 +0300 Subject: [PATCH 05/15] Update .github/ISSUE_TEMPLATE/ISSUE_TEMPLATE_DISCUSSION.md Co-authored-by: Stefano Moia --- .github/ISSUE_TEMPLATE/ISSUE_TEMPLATE_DISCUSSION.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/ISSUE_TEMPLATE_DISCUSSION.md b/.github/ISSUE_TEMPLATE/ISSUE_TEMPLATE_DISCUSSION.md index b396bee..265dd0b 100644 --- a/.github/ISSUE_TEMPLATE/ISSUE_TEMPLATE_DISCUSSION.md +++ b/.github/ISSUE_TEMPLATE/ISSUE_TEMPLATE_DISCUSSION.md @@ -1,6 +1,6 @@ --- name: Discussion -about: Use this template to start a discussion issue, i.e. an issue that +about: Use this template to start a discussion issue, i.e. an issue meant to open a community debate over a topic title: '' labels: Discussion assignees: '' From 2886d3b42dcfbdcf01fbf1bf8902a6b50450717a Mon Sep 17 00:00:00 2001 From: maestroque Date: Wed, 29 May 2024 12:29:13 +0300 Subject: [PATCH 06/15] Undo wrong codespell typos and add them to the ignored words list --- .pre-commit-config.yaml | 1 + peakdet/operations.py | 4 ++-- peakdet/utils.py | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e5b975d..6de2a58 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -36,3 +36,4 @@ repos: rev: v2.2.6 hooks: - id: codespell + args: ["--ignore-words-list", "troughs"] diff --git a/peakdet/operations.py b/peakdet/operations.py index 0fc1aa1..9f8458a 100644 --- a/peakdet/operations.py +++ b/peakdet/operations.py @@ -107,7 +107,7 @@ def interpolate_physio(data, target_fs, *, kind="cubic"): @utils.make_operation() def peakfind_physio(data, *, thresh=0.2, dist=None): """ - Performs peak and through detection on `data` + Performs peak and trough detection on `data` Parameters ---------- @@ -139,7 +139,7 @@ def peakfind_physio(data, *, thresh=0.2, dist=None): heights = np.percentile(heights["peak_heights"], 1) locs, heights = signal.find_peaks(data[:], distance=cdist, height=heights) data._metadata["peaks"] = locs - # perform through detection based on detected peaks + # perform trough detection based on detected peaks data._metadata["troughs"] = utils.check_troughs(data, data.peaks) return data diff --git a/peakdet/utils.py b/peakdet/utils.py index fcbf28d..523bf07 100644 --- a/peakdet/utils.py +++ b/peakdet/utils.py @@ -225,7 +225,7 @@ def check_troughs(data, peaks, troughs=None): Returns ------- troughs : np.ndarray - Indices of through locations in `data`, dependent on `peaks` + Indices of trough locations in `data`, dependent on `peaks` """ # If there's a through after all peaks, keep it. if troughs is not None and troughs[-1] > peaks[-1]: From 728a11b0596c4db8e0e7ea7bbd57a8973931b134 Mon Sep 17 00:00:00 2001 From: maestroque Date: Wed, 29 May 2024 12:30:13 +0300 Subject: [PATCH 07/15] Delete outdated meeting issue template --- .github/ISSUE_TEMPLATE/ISSUE_TEMPLATE_MEETING.md | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/ISSUE_TEMPLATE_MEETING.md diff --git a/.github/ISSUE_TEMPLATE/ISSUE_TEMPLATE_MEETING.md b/.github/ISSUE_TEMPLATE/ISSUE_TEMPLATE_MEETING.md deleted file mode 100644 index 8f0da87..0000000 --- a/.github/ISSUE_TEMPLATE/ISSUE_TEMPLATE_MEETING.md +++ /dev/null @@ -1,13 +0,0 @@ ---- -name: Dev call -about: Only for physiopy PM -title: physiopy MMM developer call (dd.mm.yyyy @ 14.30 UTC) -labels: Community -assignees: smoia ---- - -The call will take place over **Skype** [here](https://join.skype.com/Rm4B7R30TG6g). - -Find the agenda [here]() and don't be scared to add/suggest/press for topics! - -Old minutes are [here](https://drive.google.com/open?id=1zfc-hgRcU1k2XdqKD8v6TGwQX_jeCUea) From 395f030f9180f10be00be019ff150532b0e11652 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 29 May 2024 09:30:27 +0000 Subject: [PATCH 08/15] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- peakdet/operations.py | 4 ++-- peakdet/utils.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/peakdet/operations.py b/peakdet/operations.py index 9f8458a..0fc1aa1 100644 --- a/peakdet/operations.py +++ b/peakdet/operations.py @@ -107,7 +107,7 @@ def interpolate_physio(data, target_fs, *, kind="cubic"): @utils.make_operation() def peakfind_physio(data, *, thresh=0.2, dist=None): """ - Performs peak and trough detection on `data` + Performs peak and through detection on `data` Parameters ---------- @@ -139,7 +139,7 @@ def peakfind_physio(data, *, thresh=0.2, dist=None): heights = np.percentile(heights["peak_heights"], 1) locs, heights = signal.find_peaks(data[:], distance=cdist, height=heights) data._metadata["peaks"] = locs - # perform trough detection based on detected peaks + # perform through detection based on detected peaks data._metadata["troughs"] = utils.check_troughs(data, data.peaks) return data diff --git a/peakdet/utils.py b/peakdet/utils.py index 523bf07..fcbf28d 100644 --- a/peakdet/utils.py +++ b/peakdet/utils.py @@ -225,7 +225,7 @@ def check_troughs(data, peaks, troughs=None): Returns ------- troughs : np.ndarray - Indices of trough locations in `data`, dependent on `peaks` + Indices of through locations in `data`, dependent on `peaks` """ # If there's a through after all peaks, keep it. if troughs is not None and troughs[-1] > peaks[-1]: From 3d18b599ba6045749d11883bfded2dc4e81cc5b6 Mon Sep 17 00:00:00 2001 From: maestroque Date: Wed, 29 May 2024 12:39:41 +0300 Subject: [PATCH 09/15] Fix codespell word ignoring --- .pre-commit-config.yaml | 2 +- peakdet/operations.py | 4 ++-- peakdet/utils.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6de2a58..55bfe31 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -36,4 +36,4 @@ repos: rev: v2.2.6 hooks: - id: codespell - args: ["--ignore-words-list", "troughs"] + args: ["-L", "troughs"] diff --git a/peakdet/operations.py b/peakdet/operations.py index 0fc1aa1..9f8458a 100644 --- a/peakdet/operations.py +++ b/peakdet/operations.py @@ -107,7 +107,7 @@ def interpolate_physio(data, target_fs, *, kind="cubic"): @utils.make_operation() def peakfind_physio(data, *, thresh=0.2, dist=None): """ - Performs peak and through detection on `data` + Performs peak and trough detection on `data` Parameters ---------- @@ -139,7 +139,7 @@ def peakfind_physio(data, *, thresh=0.2, dist=None): heights = np.percentile(heights["peak_heights"], 1) locs, heights = signal.find_peaks(data[:], distance=cdist, height=heights) data._metadata["peaks"] = locs - # perform through detection based on detected peaks + # perform trough detection based on detected peaks data._metadata["troughs"] = utils.check_troughs(data, data.peaks) return data diff --git a/peakdet/utils.py b/peakdet/utils.py index fcbf28d..86740be 100644 --- a/peakdet/utils.py +++ b/peakdet/utils.py @@ -225,9 +225,9 @@ def check_troughs(data, peaks, troughs=None): Returns ------- troughs : np.ndarray - Indices of through locations in `data`, dependent on `peaks` + Indices of trough locations in `data`, dependent on `peaks` """ - # If there's a through after all peaks, keep it. + # If there's a trough after all peaks, keep it. if troughs is not None and troughs[-1] > peaks[-1]: all_troughs = np.zeros(peaks.size, dtype=int) all_troughs[-1] == troughs[-1] From 4abce44ddb3185ae7e2da4ccd8d056c6865779c7 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 29 May 2024 09:39:55 +0000 Subject: [PATCH 10/15] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- peakdet/operations.py | 4 ++-- peakdet/utils.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/peakdet/operations.py b/peakdet/operations.py index 9f8458a..0fc1aa1 100644 --- a/peakdet/operations.py +++ b/peakdet/operations.py @@ -107,7 +107,7 @@ def interpolate_physio(data, target_fs, *, kind="cubic"): @utils.make_operation() def peakfind_physio(data, *, thresh=0.2, dist=None): """ - Performs peak and trough detection on `data` + Performs peak and through detection on `data` Parameters ---------- @@ -139,7 +139,7 @@ def peakfind_physio(data, *, thresh=0.2, dist=None): heights = np.percentile(heights["peak_heights"], 1) locs, heights = signal.find_peaks(data[:], distance=cdist, height=heights) data._metadata["peaks"] = locs - # perform trough detection based on detected peaks + # perform through detection based on detected peaks data._metadata["troughs"] = utils.check_troughs(data, data.peaks) return data diff --git a/peakdet/utils.py b/peakdet/utils.py index 86740be..fcbf28d 100644 --- a/peakdet/utils.py +++ b/peakdet/utils.py @@ -225,9 +225,9 @@ def check_troughs(data, peaks, troughs=None): Returns ------- troughs : np.ndarray - Indices of trough locations in `data`, dependent on `peaks` + Indices of through locations in `data`, dependent on `peaks` """ - # If there's a trough after all peaks, keep it. + # If there's a through after all peaks, keep it. if troughs is not None and troughs[-1] > peaks[-1]: all_troughs = np.zeros(peaks.size, dtype=int) all_troughs[-1] == troughs[-1] From 83e5d2370efd369cb4f178054d249883b61bf8ce Mon Sep 17 00:00:00 2001 From: maestroque Date: Wed, 29 May 2024 12:41:18 +0300 Subject: [PATCH 11/15] Fix codespell word ignoring --- .pre-commit-config.yaml | 2 +- peakdet/operations.py | 4 ++-- peakdet/utils.py | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 55bfe31..3f8a7a7 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -36,4 +36,4 @@ repos: rev: v2.2.6 hooks: - id: codespell - args: ["-L", "troughs"] + args: ["-L", "trough,troughs"] diff --git a/peakdet/operations.py b/peakdet/operations.py index 0fc1aa1..9f8458a 100644 --- a/peakdet/operations.py +++ b/peakdet/operations.py @@ -107,7 +107,7 @@ def interpolate_physio(data, target_fs, *, kind="cubic"): @utils.make_operation() def peakfind_physio(data, *, thresh=0.2, dist=None): """ - Performs peak and through detection on `data` + Performs peak and trough detection on `data` Parameters ---------- @@ -139,7 +139,7 @@ def peakfind_physio(data, *, thresh=0.2, dist=None): heights = np.percentile(heights["peak_heights"], 1) locs, heights = signal.find_peaks(data[:], distance=cdist, height=heights) data._metadata["peaks"] = locs - # perform through detection based on detected peaks + # perform trough detection based on detected peaks data._metadata["troughs"] = utils.check_troughs(data, data.peaks) return data diff --git a/peakdet/utils.py b/peakdet/utils.py index fcbf28d..86740be 100644 --- a/peakdet/utils.py +++ b/peakdet/utils.py @@ -225,9 +225,9 @@ def check_troughs(data, peaks, troughs=None): Returns ------- troughs : np.ndarray - Indices of through locations in `data`, dependent on `peaks` + Indices of trough locations in `data`, dependent on `peaks` """ - # If there's a through after all peaks, keep it. + # If there's a trough after all peaks, keep it. if troughs is not None and troughs[-1] > peaks[-1]: all_troughs = np.zeros(peaks.size, dtype=int) all_troughs[-1] == troughs[-1] From 31048fa7c35c5570120f37e5813d21793e882a8d Mon Sep 17 00:00:00 2001 From: maestroque Date: Wed, 29 May 2024 12:45:56 +0300 Subject: [PATCH 12/15] Update pre-commit hooks versions --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3f8a7a7..4b61d6f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ # See https://pre-commit.com/hooks.html for more hooks repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.5.0 + rev: v4.6.0 hooks: - id: trailing-whitespace - id: end-of-file-fixer @@ -11,11 +11,11 @@ repos: - id: check-case-conflict - id: check-merge-conflict - repo: https://github.com/psf/black - rev: 23.11.0 + rev: 24.4.2 hooks: - id: black - repo: https://github.com/pycqa/isort - rev: 5.13.0 + rev: 5.13.2 hooks: - id: isort - repo: https://github.com/pycqa/flake8 From 94f4056b2d6da540f45a39786c16a745310746dc Mon Sep 17 00:00:00 2001 From: maestroque Date: Fri, 31 May 2024 21:20:42 +0300 Subject: [PATCH 13/15] Fix pre-commit hook errors --- docs/user_guide/loading.rst | 2 +- peakdet/operations.py | 23 ----------------------- peakdet/tests/test_physio.py | 16 +++++++++------- setup.cfg | 1 + 4 files changed, 11 insertions(+), 31 deletions(-) diff --git a/docs/user_guide/loading.rst b/docs/user_guide/loading.rst index 705c4fc..5356cdf 100644 --- a/docs/user_guide/loading.rst +++ b/docs/user_guide/loading.rst @@ -9,7 +9,7 @@ with the built-in :py:mod:`peakdet` IO functions. The :py:meth:`peakdet.load_physio` function is the most simple of these functions, and accepts data stored as single-column text file. For example, if -we have a file `ECG.csv` that we might normally load with :py:mod:`numpy`: +we have a file ``ECG.csv`` that we might normally load with :py:mod:`numpy`: .. doctest:: diff --git a/peakdet/operations.py b/peakdet/operations.py index 9f8458a..f5a2e7f 100644 --- a/peakdet/operations.py +++ b/peakdet/operations.py @@ -212,29 +212,6 @@ def add_peaks(data, add): return data -@utils.make_operation() -def add_peaks(data, add): - """ - Add `newpeak` to add them in `data` - - Parameters - ---------- - data : Physio_like - add : int - - Returns - ------- - data : Physio_like - """ - - data = utils.check_physio(data, ensure_fs=False, copy=True) - idx = np.searchsorted(data._metadata["peaks"], add) - data._metadata["peaks"] = np.insert(data._metadata["peaks"], idx, add) - data._metadata["troughs"] = utils.check_troughs(data, data.peaks) - - return data - - def edit_physio(data): """ Opens interactive plot with `data` to permit manual editing of time series diff --git a/peakdet/tests/test_physio.py b/peakdet/tests/test_physio.py index cc6b5e8..1283bf1 100644 --- a/peakdet/tests/test_physio.py +++ b/peakdet/tests/test_physio.py @@ -49,10 +49,11 @@ def test_physio_creation(self): assert isinstance(phys._metadata.get(prop), np.ndarray) +# TODO: Update unit test def test_neurokit2phys(path_neurokit): - df = pd.read_csv(path_neurokit, sep="\t") + df = pd.read_csv(path_neurokit, sep="\t") # noqa phys = Physio.neurokit2phys( - path_neurokit, copy_data=True, copy_peaks=True, copy_troughs=True, fs=fs + path_neurokit, copy_data=True, copy_peaks=True, copy_troughs=True, fs=fs # noqa ) assert all( @@ -66,17 +67,18 @@ def test_neurokit2phys(path_neurokit): phys.peaks == np.where(df.loc[:, df.columns.str.endswith("Peaks")] != 0)[0] ) ) - assert phys.fs == fs + assert phys.fs == fs # noqa +# TODO: Update unit test def test_phys2neurokit(path_phys): - phys = load_physio(path_phys, allow_pickle=True) - neuro = data.phys2neurokit( + phys = load_physio(path_phys, allow_pickle=True) # noqa + neuro = data.phys2neurokit( # noqa copy_data=True, copy_peaks=True, copy_troughs=False, - module=module, - neurokit_path=path_neurokit, + module=module, # noqa + neurokit_path=path_neurokit, # noqa ) assert all( diff --git a/setup.cfg b/setup.cfg index f6c169f..a7d6ea2 100644 --- a/setup.cfg +++ b/setup.cfg @@ -76,6 +76,7 @@ all = exclude= *build/ tests + versioneer.py ignore = E126, E203, E402, W503 max-line-length = 99 per-file-ignores = From f4607fcb16191c446ca1719fb8b426bd376a3681 Mon Sep 17 00:00:00 2001 From: maestroque Date: Fri, 31 May 2024 21:25:58 +0300 Subject: [PATCH 14/15] Fix bug issue template typo --- .github/ISSUE_TEMPLATE/ISSUE_TEMPLATE_BUGS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/ISSUE_TEMPLATE_BUGS.md b/.github/ISSUE_TEMPLATE/ISSUE_TEMPLATE_BUGS.md index b8288db..bb21794 100644 --- a/.github/ISSUE_TEMPLATE/ISSUE_TEMPLATE_BUGS.md +++ b/.github/ISSUE_TEMPLATE/ISSUE_TEMPLATE_BUGS.md @@ -27,7 +27,7 @@ assignees: ## Specifications - Python version: - - phys2bids version: + - peakdet version: - Platform: ## Possible solution From 945267fe7f2dbf9e9ab7929b7da762e9cac7f942 Mon Sep 17 00:00:00 2001 From: maestroque Date: Wed, 5 Jun 2024 01:06:48 +0300 Subject: [PATCH 15/15] Add extended flake8 settings for black support --- setup.cfg | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index a7d6ea2..df99dd1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -77,8 +77,10 @@ exclude= *build/ tests versioneer.py -ignore = E126, E203, E402, W503 -max-line-length = 99 +ignore = E126, E402, W503, W401, W811 +extend-ignore = E203, E501 +extend-select = B950 +max-line-length = 88 per-file-ignores = */__init__.py:F401