From 6dd3ce27cb88ccd288c4dc8a79971ad2dfd2c671 Mon Sep 17 00:00:00 2001 From: maestroque Date: Mon, 15 Apr 2024 13:10:19 +0300 Subject: [PATCH 01/30] Add loguru exception catching --- peakdet/cli/run.py | 2 ++ peakdet/external.py | 1 + peakdet/io.py | 3 +++ peakdet/operations.py | 3 ++- peakdet/physio.py | 4 ++-- peakdet/utils.py | 2 ++ 6 files changed, 12 insertions(+), 3 deletions(-) diff --git a/peakdet/cli/run.py b/peakdet/cli/run.py index 72fc06b..a2d25e9 100644 --- a/peakdet/cli/run.py +++ b/peakdet/cli/run.py @@ -7,6 +7,7 @@ matplotlib.use('WXAgg') from gooey import Gooey, GooeyParser import peakdet +from loguru import logger TARGET = 'pythonw' if sys.platform == 'darwin' else 'python' TARGET += ' -u ' + os.path.abspath(__file__) @@ -105,6 +106,7 @@ def get_parser(): return parser +@logger.catch def workflow(*, file_template, modality, fs, source='MRI', channel=1, output='peakdet.csv', savehistory=True, noedit=False, thresh=0.2, measurements=ATTR_CONV.keys()): diff --git a/peakdet/external.py b/peakdet/external.py index 995b5f1..2ccb1aa 100644 --- a/peakdet/external.py +++ b/peakdet/external.py @@ -6,6 +6,7 @@ import warnings import numpy as np from peakdet import physio, utils +from loguru import logger @utils.make_operation(exclude=[]) diff --git a/peakdet/io.py b/peakdet/io.py index b3f0fc5..439aac1 100644 --- a/peakdet/io.py +++ b/peakdet/io.py @@ -8,10 +8,12 @@ import warnings import numpy as np from peakdet import physio, utils +from loguru import logger EXPECTED = ['data', 'fs', 'history', 'metadata'] +@logger.catch def load_physio(data, *, fs=None, dtype=None, history=None, allow_pickle=False): """ @@ -119,6 +121,7 @@ def save_physio(fname, data): return fname +@logger.catch def load_history(file, verbose=False): """ Loads history from `file` and replays it, creating new Physio instance diff --git a/peakdet/operations.py b/peakdet/operations.py index a7bc7c5..1db0cee 100644 --- a/peakdet/operations.py +++ b/peakdet/operations.py @@ -7,8 +7,9 @@ import numpy as np from scipy import interpolate, signal from peakdet import editor, utils +from loguru import logger - +@logger.catch @utils.make_operation() def filter_physio(data, cutoffs, method, *, order=3): """ diff --git a/peakdet/physio.py b/peakdet/physio.py index c7652ec..9937ef3 100644 --- a/peakdet/physio.py +++ b/peakdet/physio.py @@ -4,7 +4,7 @@ """ import numpy as np - +from loguru import logger class Physio(): @@ -40,7 +40,7 @@ class Physio(): suppdata : :obj:`numpy.ndarray` Secondary physiological waveform """ - + @logger.catch def __init__(self, data, fs=None, history=None, metadata=None, suppdata=None): self._data = np.asarray(data).squeeze() if self.data.ndim > 1: diff --git a/peakdet/utils.py b/peakdet/utils.py index 7858f0b..fa782e8 100644 --- a/peakdet/utils.py +++ b/peakdet/utils.py @@ -8,6 +8,7 @@ import inspect import numpy as np from peakdet import physio +from loguru import logger def make_operation(*, exclude=None): @@ -108,6 +109,7 @@ def _get_call(*, exclude=None, serializable=True): return function, provided +@logger.catch def check_physio(data, ensure_fs=True, copy=False): """ Checks that `data` is in correct format (i.e., `peakdet.Physio`) From d33b5ac21d8c4ea5497120829765b21ba33799f0 Mon Sep 17 00:00:00 2001 From: maestroque Date: Mon, 15 Apr 2024 19:50:20 +0300 Subject: [PATCH 02/30] Integrate loguru for warning and info logs --- peakdet/cli/run.py | 16 ++++++++-------- peakdet/external.py | 6 +++--- peakdet/io.py | 16 ++++++++-------- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/peakdet/cli/run.py b/peakdet/cli/run.py index a2d25e9..0c16fe4 100644 --- a/peakdet/cli/run.py +++ b/peakdet/cli/run.py @@ -142,14 +142,14 @@ def workflow(*, file_template, modality, fs, source='MRI', channel=1, """ # output file - print('OUTPUT FILE:\t\t{}\n'.format(output)) + logger.info('OUTPUT FILE:\t\t{}\n'.format(output)) # grab files from file template - print('FILE TEMPLATE:\t{}\n'.format(file_template)) + logger.info('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))) + logger.info('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 ' @@ -168,10 +168,10 @@ def workflow(*, file_template, modality, fs, source='MRI', channel=1, # 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.') + logger.warning('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 @@ -183,7 +183,7 @@ def workflow(*, file_template, modality, fs, source='MRI', channel=1, # 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)) + logger.info('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), diff --git a/peakdet/external.py b/peakdet/external.py index 2ccb1aa..d0a274a 100644 --- a/peakdet/external.py +++ b/peakdet/external.py @@ -39,9 +39,9 @@ def load_rtpeaks(fname, channel, fs): """ 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.') + logger.warning('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(',') diff --git a/peakdet/io.py b/peakdet/io.py index 439aac1..68a0d00 100644 --- a/peakdet/io.py +++ b/peakdet/io.py @@ -66,9 +66,9 @@ def load_physio(data, *, fs=None, dtype=None, history=None, # 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.') + logger.warning('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 @@ -81,7 +81,7 @@ def load_physio(data, *, fs=None, dtype=None, history=None, # 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. ' + logger.warning('Provided sampling rate does not match loaded rate. ' 'Resetting loaded sampling rate {} to provided {}' .format(phys.fs, fs)) phys._fs = fs @@ -151,7 +151,7 @@ def load_history(file, verbose=False): data = None for (func, kwargs) in history: if verbose: - print('Rerunning {}'.format(func)) + logger.info('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 @@ -197,9 +197,9 @@ 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.') + logger.warning('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) From 72c427b0edeff9e4bfc997da7bd0ff51b5e1ec9b Mon Sep 17 00:00:00 2001 From: maestroque Date: Mon, 15 Apr 2024 19:53:18 +0300 Subject: [PATCH 03/30] Add loguru in requirements --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 74fa65e..0e8bceb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,4 @@ matplotlib numpy scipy +loguru From a9b0c18d417fe28a2c4fb15db0d487aa86a9ce95 Mon Sep 17 00:00:00 2001 From: maestroque Date: Mon, 13 May 2024 14:04:45 +0300 Subject: [PATCH 04/30] Add loguru to setup.cfg --- setup.cfg | 3 +++ 1 file changed, 3 insertions(+) diff --git a/setup.cfg b/setup.cfg index f5cdf04..363b5aa 100644 --- a/setup.cfg +++ b/setup.cfg @@ -57,11 +57,14 @@ test = pytest-cov %(style)s %(nk)s +logger = + loguru all = %(doc)s %(duecredit)s %(style)s %(test)s + %(logger)s [options.package_data] From acd31cd3fdf37c228c1cfcf2783b3f710bb73e3d Mon Sep 17 00:00:00 2001 From: maestroque Date: Thu, 23 May 2024 02:17:00 +0300 Subject: [PATCH 05/30] Disable loguru logs for library users --- peakdet/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/peakdet/__init__.py b/peakdet/__init__.py index 8780359..905c6ba 100644 --- a/peakdet/__init__.py +++ b/peakdet/__init__.py @@ -12,7 +12,10 @@ interpolate_physio, peakfind_physio, plot_physio, reject_peaks) from peakdet.physio import (Physio) +from loguru import logger from ._version import get_versions __version__ = get_versions()['version'] -del get_versions \ No newline at end of file +del get_versions + +logger.disable("peakdet") \ No newline at end of file From 537f48a092c8b1fcb3f4d51ec0f7024118b111f3 Mon Sep 17 00:00:00 2001 From: maestroque Date: Thu, 23 May 2024 02:23:47 +0300 Subject: [PATCH 06/30] Add verbose option in CLI --- peakdet/cli/run.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/peakdet/cli/run.py b/peakdet/cli/run.py index 0c16fe4..ac18775 100644 --- a/peakdet/cli/run.py +++ b/peakdet/cli/run.py @@ -109,7 +109,7 @@ def get_parser(): @logger.catch def workflow(*, file_template, modality, fs, source='MRI', channel=1, output='peakdet.csv', savehistory=True, noedit=False, thresh=0.2, - measurements=ATTR_CONV.keys()): + measurements=ATTR_CONV.keys(), verbose=True): """ Basic workflow for physiological data @@ -140,7 +140,8 @@ def workflow(*, file_template, modality, fs, source='MRI', channel=1, Which HRV-related measurements to save from data. See ``peakdet.HRV`` for available measurements. Default: all available measurements. """ - + logger.remove(0) + logger.add(sys.stderr, backtrace=verbose, diagnose=verbose) # output file logger.info('OUTPUT FILE:\t\t{}\n'.format(output)) # grab files from file template From bb7ce795ced49ff477adcde3ae5ed9d03f85a955 Mon Sep 17 00:00:00 2001 From: maestroque Date: Thu, 23 May 2024 03:11:18 +0300 Subject: [PATCH 07/30] Fix loguru disabling for library users --- peakdet/__init__.py | 3 ++- peakdet/cli/run.py | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/peakdet/__init__.py b/peakdet/__init__.py index 905c6ba..dd2c2e2 100644 --- a/peakdet/__init__.py +++ b/peakdet/__init__.py @@ -18,4 +18,5 @@ __version__ = get_versions()['version'] del get_versions -logger.disable("peakdet") \ No newline at end of file +# TODO: Loguru does not detect the module's name +logger.disable(None) \ No newline at end of file diff --git a/peakdet/cli/run.py b/peakdet/cli/run.py index ac18775..fea08d8 100644 --- a/peakdet/cli/run.py +++ b/peakdet/cli/run.py @@ -228,6 +228,7 @@ def workflow(*, file_template, modality, fs, source='MRI', channel=1, def main(): + logger.enable(None) opts = get_parser().parse_args() workflow(**vars(opts)) From 7070d70b1521fc180d8cca4ed47410822f37380a Mon Sep 17 00:00:00 2001 From: maestroque Date: Thu, 23 May 2024 03:59:58 +0300 Subject: [PATCH 08/30] Add logger enabling and configuring --- peakdet/__init__.py | 2 +- peakdet/cli/run.py | 2 +- peakdet/utils.py | 9 +++++++++ 3 files changed, 11 insertions(+), 2 deletions(-) diff --git a/peakdet/__init__.py b/peakdet/__init__.py index dd2c2e2..5b75bce 100644 --- a/peakdet/__init__.py +++ b/peakdet/__init__.py @@ -19,4 +19,4 @@ del get_versions # TODO: Loguru does not detect the module's name -logger.disable(None) \ No newline at end of file +logger.disable("") \ No newline at end of file diff --git a/peakdet/cli/run.py b/peakdet/cli/run.py index fea08d8..0267b5a 100644 --- a/peakdet/cli/run.py +++ b/peakdet/cli/run.py @@ -228,7 +228,7 @@ def workflow(*, file_template, modality, fs, source='MRI', channel=1, def main(): - logger.enable(None) + logger.enable("") opts = get_parser().parse_args() workflow(**vars(opts)) diff --git a/peakdet/utils.py b/peakdet/utils.py index fa782e8..7b291ab 100644 --- a/peakdet/utils.py +++ b/peakdet/utils.py @@ -9,6 +9,7 @@ import numpy as np from peakdet import physio from loguru import logger +import sys def make_operation(*, exclude=None): @@ -227,3 +228,11 @@ def check_troughs(data, peaks, troughs=None): all_troughs[f] = idx return all_troughs + +def enable_logger(diagnose=True, backtrace=True): + """ + Toggles the use of the module's logger and configures it + """ + logger.enable("") + logger.remove(0) + logger.add(sys.stderr, backtrace=backtrace, diagnose=diagnose) From 405254c3988f8e4697207118a54594936befd73b Mon Sep 17 00:00:00 2001 From: maestroque Date: Thu, 23 May 2024 15:03:30 +0300 Subject: [PATCH 09/30] Leave logger.catch decorator only for the CLI workflow function --- peakdet/io.py | 2 -- peakdet/operations.py | 1 - peakdet/physio.py | 1 - peakdet/utils.py | 1 - 4 files changed, 5 deletions(-) diff --git a/peakdet/io.py b/peakdet/io.py index 68a0d00..af6a212 100644 --- a/peakdet/io.py +++ b/peakdet/io.py @@ -13,7 +13,6 @@ EXPECTED = ['data', 'fs', 'history', 'metadata'] -@logger.catch def load_physio(data, *, fs=None, dtype=None, history=None, allow_pickle=False): """ @@ -121,7 +120,6 @@ def save_physio(fname, data): return fname -@logger.catch def load_history(file, verbose=False): """ Loads history from `file` and replays it, creating new Physio instance diff --git a/peakdet/operations.py b/peakdet/operations.py index 1db0cee..93afbf7 100644 --- a/peakdet/operations.py +++ b/peakdet/operations.py @@ -9,7 +9,6 @@ from peakdet import editor, utils from loguru import logger -@logger.catch @utils.make_operation() def filter_physio(data, cutoffs, method, *, order=3): """ diff --git a/peakdet/physio.py b/peakdet/physio.py index 9937ef3..e2f9127 100644 --- a/peakdet/physio.py +++ b/peakdet/physio.py @@ -40,7 +40,6 @@ class Physio(): suppdata : :obj:`numpy.ndarray` Secondary physiological waveform """ - @logger.catch def __init__(self, data, fs=None, history=None, metadata=None, suppdata=None): self._data = np.asarray(data).squeeze() if self.data.ndim > 1: diff --git a/peakdet/utils.py b/peakdet/utils.py index 7b291ab..7d49b76 100644 --- a/peakdet/utils.py +++ b/peakdet/utils.py @@ -110,7 +110,6 @@ def _get_call(*, exclude=None, serializable=True): return function, provided -@logger.catch def check_physio(data, ensure_fs=True, copy=False): """ Checks that `data` is in correct format (i.e., `peakdet.Physio`) From cbfc0139a7314efe9feefedf80f727eaa654dca9 Mon Sep 17 00:00:00 2001 From: maestroque Date: Thu, 23 May 2024 15:06:04 +0300 Subject: [PATCH 10/30] Remove unnecessary imports --- peakdet/cli/run.py | 1 - peakdet/external.py | 1 - peakdet/io.py | 1 - peakdet/operations.py | 1 - peakdet/physio.py | 1 - 5 files changed, 5 deletions(-) diff --git a/peakdet/cli/run.py b/peakdet/cli/run.py index 0267b5a..1aa9a48 100644 --- a/peakdet/cli/run.py +++ b/peakdet/cli/run.py @@ -2,7 +2,6 @@ import glob import os import sys -import warnings import matplotlib matplotlib.use('WXAgg') from gooey import Gooey, GooeyParser diff --git a/peakdet/external.py b/peakdet/external.py index d0a274a..5802d72 100644 --- a/peakdet/external.py +++ b/peakdet/external.py @@ -3,7 +3,6 @@ Functions for interacting with physiological data acquired by external packages """ -import warnings import numpy as np from peakdet import physio, utils from loguru import logger diff --git a/peakdet/io.py b/peakdet/io.py index af6a212..e505ccf 100644 --- a/peakdet/io.py +++ b/peakdet/io.py @@ -5,7 +5,6 @@ import json import os.path as op -import warnings import numpy as np from peakdet import physio, utils from loguru import logger diff --git a/peakdet/operations.py b/peakdet/operations.py index 93afbf7..96355fd 100644 --- a/peakdet/operations.py +++ b/peakdet/operations.py @@ -7,7 +7,6 @@ import numpy as np from scipy import interpolate, signal from peakdet import editor, utils -from loguru import logger @utils.make_operation() def filter_physio(data, cutoffs, method, *, order=3): diff --git a/peakdet/physio.py b/peakdet/physio.py index e2f9127..ce768c8 100644 --- a/peakdet/physio.py +++ b/peakdet/physio.py @@ -4,7 +4,6 @@ """ import numpy as np -from loguru import logger class Physio(): From 4d8ffa3bf55db648f508fbcd213bfe2c43fadd17 Mon Sep 17 00:00:00 2001 From: maestroque Date: Mon, 27 May 2024 17:36:34 +0300 Subject: [PATCH 11/30] Add loguru as a main dependency --- setup.cfg | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/setup.cfg b/setup.cfg index 363b5aa..2c4417b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -27,6 +27,7 @@ install_requires = matplotlib >=3.1.1, <=3.6.3, !=3.3.0rc1 numpy >=1.9.3 scipy + loguru tests_require = pytest >=5.3 pytest-cov @@ -57,14 +58,11 @@ test = pytest-cov %(style)s %(nk)s -logger = - loguru all = %(doc)s %(duecredit)s %(style)s %(test)s - %(logger)s [options.package_data] From 3724928df5703b5ac712ed6fb846cee636a07f73 Mon Sep 17 00:00:00 2001 From: maestroque Date: Mon, 27 May 2024 17:45:52 +0300 Subject: [PATCH 12/30] Reformat using isort and black --- peakdet/__init__.py | 35 ++-- peakdet/_version.py | 154 ++++++++++------- peakdet/analytics.py | 23 ++- peakdet/cli/run.py | 285 ++++++++++++++++++------------- peakdet/editor.py | 138 +++++++++------ peakdet/external.py | 21 ++- peakdet/io.py | 93 +++++----- peakdet/modalities.py | 19 ++- peakdet/operations.py | 70 ++++---- peakdet/physio.py | 126 ++++++++------ 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 | 56 +++--- 20 files changed, 784 insertions(+), 557 deletions(-) diff --git a/peakdet/__init__.py b/peakdet/__init__.py index 5b75bce..7a7f098 100644 --- a/peakdet/__init__.py +++ b/peakdet/__init__.py @@ -1,22 +1,35 @@ __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 loguru import logger + +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 loguru import logger +from peakdet.physio import Physio from ._version import get_versions -__version__ = get_versions()['version'] + +__version__ = get_versions()["version"] del get_versions # TODO: Loguru does not detect the module's name -logger.disable("") \ No newline at end of file +logger.disable("") 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 1aa9a48..b34915c 100644 --- a/peakdet/cli/run.py +++ b/peakdet/cli/run.py @@ -2,113 +2,167 @@ import glob import os import sys + import matplotlib -matplotlib.use('WXAgg') -from gooey import Gooey, GooeyParser -import peakdet + +matplotlib.use("WXAgg") from loguru import logger -TARGET = 'pythonw' if sys.platform == 'darwin' else 'python' -TARGET += ' -u ' + os.path.abspath(__file__) +# from gooey import Gooey, GooeyParser +import peakdet -LOADERS = dict( - rtpeaks=peakdet.load_rtpeaks, - MRI=peakdet.load_physio -) +TARGET = "pythonw" if sys.platform == "darwin" else "python" +TARGET += " -u " + os.path.abspath(__file__) + +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 @logger.catch -def workflow(*, file_template, modality, fs, source='MRI', channel=1, - output='peakdet.csv', savehistory=True, noedit=False, thresh=0.2, - measurements=ATTR_CONV.keys(), verbose=True): +def workflow( + *, + file_template, + modality, + fs, + source="MRI", + channel=1, + output="peakdet.csv", + savehistory=True, + noedit=False, + thresh=0.2, + measurements=ATTR_CONV.keys(), + verbose=True +): """ Basic workflow for physiological data @@ -142,67 +196,71 @@ def workflow(*, file_template, modality, fs, source='MRI', channel=1, logger.remove(0) logger.add(sys.stderr, backtrace=verbose, diagnose=verbose) # output file - logger.info('OUTPUT FILE:\t\t{}\n'.format(output)) + logger.info("OUTPUT FILE:\t\t{}\n".format(output)) # grab files from file template - logger.info('FILE TEMPLATE:\t{}\n'.format(file_template)) + logger.info("FILE TEMPLATE:\t{}\n".format(file_template)) files = glob.glob(file_template, recursive=True) # convert measurements to peakdet.HRV attribute friendly names try: - logger.info('REQUESTED MEASUREMENTS: {}\n'.format(', '.join(measurements))) + logger.info("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: - logger.warning('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 = '' + logger.warning( + "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) - logger.info('Currently processing {}'.format(fname)) + logger.info("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) @@ -219,11 +277,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(): @@ -232,5 +289,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 5802d72..26dab1b 100644 --- a/peakdet/external.py +++ b/peakdet/external.py @@ -4,9 +4,10 @@ """ import numpy as np -from peakdet import physio, utils from loguru import logger +from peakdet import physio, utils + @utils.make_operation(exclude=[]) def load_rtpeaks(fname, channel, fs): @@ -37,16 +38,18 @@ def load_rtpeaks(fname, channel, fs): Loaded physiological data """ - if fname.startswith('/'): - logger.warning('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("/"): + logger.warning( + "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 e505ccf..9218618 100644 --- a/peakdet/io.py +++ b/peakdet/io.py @@ -5,15 +5,16 @@ import json import os.path as op + import numpy as np -from peakdet import physio, utils from loguru import logger -EXPECTED = ['data', 'fs', 'history', 'metadata'] +from peakdet import physio, utils + +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: - logger.warning('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) + logger.warning( + "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): - logger.warning('Provided sampling rate does not match loaded rate. ' - 'Resetting loaded sampling rate {} to provided {}' - .format(phys.fs, fs)) + logger.warning( + "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: - logger.info('Rerunning {}'.format(func)) + logger.info("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: - logger.warning('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: + logger.warning( + "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 96355fd..9f8458a 100644 --- a/peakdet/operations.py +++ b/peakdet/operations.py @@ -6,8 +6,10 @@ import matplotlib.pyplot as plt import numpy as np from scipy import interpolate, signal + from peakdet import editor, utils + @utils.make_operation() def filter_physio(data, cutoffs, method, *, order=3): """ @@ -33,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)) @@ -61,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` @@ -132,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 @@ -157,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 @@ -179,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 @@ -201,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 @@ -224,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 @@ -291,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 ce768c8..d3c00a2 100644 --- a/peakdet/physio.py +++ b/peakdet/physio.py @@ -4,9 +4,10 @@ """ import numpy as np +from loguru import logger -class Physio(): +class Physio: """ Class to hold physiological data and relevant information @@ -39,37 +40,53 @@ class Physio(): suppdata : :obj:`numpy.ndarray` Secondary physiological waveform """ + 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)) + raise ValueError( + logger.exception( + "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): @@ -82,52 +99,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 ---------- @@ -145,27 +163,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 @@ -185,28 +209,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 7d49b76..2d1d3df 100644 --- a/peakdet/utils.py +++ b/peakdet/utils.py @@ -4,12 +4,14 @@ directly but should support wrapper functions stored in `peakdet.operations`. """ -from functools import wraps import inspect +import sys +from functools import wraps + import numpy as np -from peakdet import physio from loguru import logger -import sys + +from peakdet import physio def make_operation(*, exclude=None): @@ -31,7 +33,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__ @@ -49,17 +51,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 @@ -84,7 +87,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] @@ -104,7 +107,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 @@ -139,17 +142,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` @@ -190,9 +201,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 @@ -222,12 +237,13 @@ 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 return all_troughs + def enable_logger(diagnose=True, backtrace=True): """ Toggles the use of the module's logger and configures it From 333e94bddeab5ed37fae54e1c925165bb0d2a3df Mon Sep 17 00:00:00 2001 From: maestroque Date: Mon, 27 May 2024 18:21:26 +0300 Subject: [PATCH 13/30] CLI: minor fixes for verbose argument --- peakdet/cli/run.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/peakdet/cli/run.py b/peakdet/cli/run.py index b34915c..5802e53 100644 --- a/peakdet/cli/run.py +++ b/peakdet/cli/run.py @@ -161,7 +161,7 @@ def workflow( noedit=False, thresh=0.2, measurements=ATTR_CONV.keys(), - verbose=True + verbose=False ): """ Basic workflow for physiological data @@ -192,6 +192,8 @@ def workflow( measurements : list, optional Which HRV-related measurements to save from data. See ``peakdet.HRV`` for available measurements. Default: all available measurements. + verbose : bool, optional + Whether to include verbose logs when catching exceptions that include diagnostics """ logger.remove(0) logger.add(sys.stderr, backtrace=verbose, diagnose=verbose) From afcc380c4fd7c0bd6ef56e0017878074c4d1234c Mon Sep 17 00:00:00 2001 From: maestroque Date: Sat, 1 Jun 2024 23:07:05 +0300 Subject: [PATCH 14/30] Remove Gooey from draft CLI implementation --- peakdet/cli/run.py | 27 +++++---------------------- 1 file changed, 5 insertions(+), 22 deletions(-) diff --git a/peakdet/cli/run.py b/peakdet/cli/run.py index 5802e53..8dada1d 100644 --- a/peakdet/cli/run.py +++ b/peakdet/cli/run.py @@ -10,6 +10,7 @@ # from gooey import Gooey, GooeyParser import peakdet +import argparse TARGET = "pythonw" if sys.platform == "darwin" else "python" TARGET += " -u " + os.path.abspath(__file__) @@ -41,19 +42,11 @@ } -@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 = GooeyParser() + parser = argparse.ArgumentParser() 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 " @@ -67,28 +60,24 @@ def get_parser(): ) 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 " @@ -102,7 +91,6 @@ def get_parser(): out_group.add_argument( "-o", "--output", - metavar="Filename", default="peakdet.csv", help="Output filename for generated measurements.", ) @@ -111,16 +99,13 @@ def get_parser(): "--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.", + help="Desired physiological measurements", ) out_group.add_argument( "-s", "--savehistory", - metavar="Save history", action="store_true", help="Whether to save history of data processing " "for each file.", ) @@ -132,14 +117,12 @@ def get_parser(): 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.", @@ -198,9 +181,9 @@ def workflow( logger.remove(0) logger.add(sys.stderr, backtrace=verbose, diagnose=verbose) # output file - logger.info("OUTPUT FILE:\t\t{}\n".format(output)) + logger.info("OUTPUT FILE:\t\t{}".format(output)) # grab files from file template - logger.info("FILE TEMPLATE:\t{}\n".format(file_template)) + logger.info("FILE TEMPLATE:\t{}".format(file_template)) files = glob.glob(file_template, recursive=True) # convert measurements to peakdet.HRV attribute friendly names From be82040e87a1c8f72a6f3196965cdfc6e9910024 Mon Sep 17 00:00:00 2001 From: maestroque Date: Sat, 1 Jun 2024 23:10:31 +0300 Subject: [PATCH 15/30] Minor comment deletion and reformatting --- peakdet/__init__.py | 12 +++++++++--- peakdet/cli/run.py | 3 ++- peakdet/physio.py | 6 +----- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/peakdet/__init__.py b/peakdet/__init__.py index 7a7f098..795872a 100644 --- a/peakdet/__init__.py +++ b/peakdet/__init__.py @@ -21,9 +21,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/cli/run.py b/peakdet/cli/run.py index 8dada1d..7b70200 100644 --- a/peakdet/cli/run.py +++ b/peakdet/cli/run.py @@ -6,11 +6,12 @@ import matplotlib matplotlib.use("WXAgg") +import argparse + from loguru import logger # from gooey import Gooey, GooeyParser import peakdet -import argparse TARGET = "pythonw" if sys.platform == "darwin" else "python" TARGET += " -u " + os.path.abspath(__file__) diff --git a/peakdet/physio.py b/peakdet/physio.py index d3c00a2..a615e65 100644 --- a/peakdet/physio.py +++ b/peakdet/physio.py @@ -44,12 +44,8 @@ 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( - logger.exception( - "Provided data dimensionality {} > 1.".format(self.data.ndim) - ) + "Provided data dimensionality {} > 1.".format(self.data.ndim) ) if not np.issubdtype(self.data.dtype, np.number): From 7fcb3ab12906af0b55277ac4dcc8e37303f3d154 Mon Sep 17 00:00:00 2001 From: maestroque Date: Sat, 1 Jun 2024 23:26:47 +0300 Subject: [PATCH 16/30] Add log level choice option in CLI --- peakdet/cli/run.py | 28 ++++++++++++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/peakdet/cli/run.py b/peakdet/cli/run.py index 7b70200..cda8d74 100644 --- a/peakdet/cli/run.py +++ b/peakdet/cli/run.py @@ -128,6 +128,22 @@ def get_parser(): type=float, help="Threshold for peak detection algorithm.", ) + edit_group.add_argument( + "-debug", + "--debug", + dest="debug", + action="store_true", + help="Only print debugging info to log file. Default is False.", + default=False, + ) + edit_group.add_argument( + "-quiet", + "--quiet", + dest="quiet", + action="store_true", + help="Only print warnings to log file. Default is False.", + default=False, + ) return parser @@ -145,7 +161,9 @@ def workflow( noedit=False, thresh=0.2, measurements=ATTR_CONV.keys(), - verbose=False + verbose=False, + debug=False, + quiet=False ): """ Basic workflow for physiological data @@ -180,7 +198,13 @@ def workflow( Whether to include verbose logs when catching exceptions that include diagnostics """ logger.remove(0) - logger.add(sys.stderr, backtrace=verbose, diagnose=verbose) + if quiet: + logger.add(sys.stderr, level="WARNING", backtrace=verbose, diagnose=verbose) + elif debug: + logger.add(sys.stderr, level="DEBUG", backtrace=verbose, diagnose=verbose) + else: + logger.add(sys.stderr, level="INFO", backtrace=verbose, diagnose=verbose) + # output file logger.info("OUTPUT FILE:\t\t{}".format(output)) # grab files from file template From 3253566911fe4b5202dd5ed84ddcc140e9c849c7 Mon Sep 17 00:00:00 2001 From: maestroque Date: Sat, 1 Jun 2024 23:55:22 +0300 Subject: [PATCH 17/30] Add logfile sink --- peakdet/cli/run.py | 57 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 53 insertions(+), 4 deletions(-) diff --git a/peakdet/cli/run.py b/peakdet/cli/run.py index cda8d74..534cf0f 100644 --- a/peakdet/cli/run.py +++ b/peakdet/cli/run.py @@ -7,6 +7,7 @@ matplotlib.use("WXAgg") import argparse +import datetime from loguru import logger @@ -163,7 +164,7 @@ def workflow( measurements=ATTR_CONV.keys(), verbose=False, debug=False, - quiet=False + quiet=False, ): """ Basic workflow for physiological data @@ -197,13 +198,61 @@ def workflow( verbose : bool, optional Whether to include verbose logs when catching exceptions that include diagnostics """ + outdir = os.path.dirname(output) + logger.info(f"Current path is {outdir}") + + # Create logfile name + basename = "peakdet" + extension = "log" + isotime = datetime.datetime.now().strftime("%Y-%m-%dT%H%M%S") + logname = os.path.join(outdir, (basename + isotime + "." + extension)) + logger.remove(0) if quiet: - logger.add(sys.stderr, level="WARNING", backtrace=verbose, diagnose=verbose) + logger.add( + sys.stderr, + level="WARNING", + colorize=True, + backtrace=verbose, + diagnose=verbose, + ) + logger.add( + logname, + level="WARNING", + colorize=False, + backtrace=verbose, + diagnose=verbose, + ) elif debug: - logger.add(sys.stderr, level="DEBUG", backtrace=verbose, diagnose=verbose) + logger.add( + sys.stderr, + level="DEBUG", + colorize=True, + backtrace=verbose, + diagnose=verbose, + ) + logger.add( + logname, + level="DEBUG", + colorize=False, + backtrace=verbose, + diagnose=verbose, + ) else: - logger.add(sys.stderr, level="INFO", backtrace=verbose, diagnose=verbose) + logger.add( + sys.stderr, + level="INFO", + colorize=True, + backtrace=verbose, + diagnose=verbose, + ) + logger.add( + logname, + level="INFO", + colorize=False, + backtrace=verbose, + diagnose=verbose, + ) # output file logger.info("OUTPUT FILE:\t\t{}".format(output)) From 8dd3166895d2aaaf2167186803ad3299910df6d8 Mon Sep 17 00:00:00 2001 From: maestroque Date: Sat, 1 Jun 2024 23:59:13 +0300 Subject: [PATCH 18/30] Add --verbose flag in the CLI --- peakdet/cli/run.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/peakdet/cli/run.py b/peakdet/cli/run.py index 534cf0f..873fca6 100644 --- a/peakdet/cli/run.py +++ b/peakdet/cli/run.py @@ -145,6 +145,14 @@ def get_parser(): help="Only print warnings to log file. Default is False.", default=False, ) + edit_group.add_argument( + "-verbose", + "--verbose", + dest="verbose", + action="store_true", + help="Print verbose error logs with diagnostics", + default=False, + ) return parser From cd22a437e1199b918175d2f96978f0cb9c3aacae Mon Sep 17 00:00:00 2001 From: maestroque Date: Sun, 2 Jun 2024 01:31:20 +0300 Subject: [PATCH 19/30] Add log level choosing for module usage --- peakdet/utils.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/peakdet/utils.py b/peakdet/utils.py index 2d1d3df..0970743 100644 --- a/peakdet/utils.py +++ b/peakdet/utils.py @@ -244,10 +244,24 @@ def check_troughs(data, peaks, troughs=None): return all_troughs -def enable_logger(diagnose=True, backtrace=True): +def enable_logger(loglevel="INFO", diagnose=True, backtrace=True): """ Toggles the use of the module's logger and configures it + + Parameters + ---------- + loglevel : {'INFO', 'DEBUG', 'WARNING', 'ERROR'} + Logger log level. Default: "INFO" """ + _valid_loglevels = ["INFO", "DEBUG", "WARNING", "ERROR"] + + if loglevel not in _valid_loglevels: + raise ValueError( + "Provided log level {} is not permitted; must be in {}.".format( + loglevel, _valid_loglevels + ) + ) logger.enable("") logger.remove(0) - logger.add(sys.stderr, backtrace=backtrace, diagnose=diagnose) + logger.add(sys.stderr, level=loglevel, backtrace=backtrace, diagnose=diagnose) + logger.debug("Enabling logger") From 2ac91a3f38db89390bd063e0e50816918358d17d Mon Sep 17 00:00:00 2001 From: maestroque Date: Sun, 2 Jun 2024 02:52:31 +0300 Subject: [PATCH 20/30] Add info and debug logs physio.py and operations.py functions --- peakdet/analytics.py | 1 + peakdet/editor.py | 2 ++ peakdet/modalities.py | 1 + peakdet/operations.py | 41 +++++++++++++---------------------------- peakdet/physio.py | 1 + 5 files changed, 18 insertions(+), 28 deletions(-) diff --git a/peakdet/analytics.py b/peakdet/analytics.py index 7467440..825a879 100644 --- a/peakdet/analytics.py +++ b/peakdet/analytics.py @@ -6,6 +6,7 @@ import numpy as np from scipy.interpolate import interp1d from scipy.signal import welch +from loguru import logger class HRV: diff --git a/peakdet/editor.py b/peakdet/editor.py index d1f735e..85672ce 100644 --- a/peakdet/editor.py +++ b/peakdet/editor.py @@ -8,6 +8,7 @@ from matplotlib.widgets import SpanSelector from peakdet import operations, utils +from loguru import logger class _PhysioEditor: @@ -142,6 +143,7 @@ def on_edit(self, xmin, xmax, *, method): method accepts 'insert', 'reject', 'delete' """ + logger.debug("Edit") if method not in ["insert", "reject", "delete"]: raise ValueError(f'Action "{method}" not supported.') diff --git a/peakdet/modalities.py b/peakdet/modalities.py index 8f4e009..331106a 100644 --- a/peakdet/modalities.py +++ b/peakdet/modalities.py @@ -1,6 +1,7 @@ # -*- coding: utf-8 -*- import numpy as np +from loguru import logger class HRModality: diff --git a/peakdet/operations.py b/peakdet/operations.py index 9f8458a..0aa9453 100644 --- a/peakdet/operations.py +++ b/peakdet/operations.py @@ -3,6 +3,7 @@ Functions for processing and interpreting physiological data """ +from loguru import logger import matplotlib.pyplot as plt import numpy as np from scipy import interpolate, signal @@ -58,6 +59,11 @@ def filter_physio(data, cutoffs, method, *, order=3): "frequency for input data with sampling rate {}.".format(cutoffs, data.fs) ) + if method in ["lowpass", "highpass"]: + logger.info(f"Applying a {method} filter (order: {order}) to the signal, with cutoff frequency at {cutoffs} Hz") + elif method in ["bandpass", "bandstop"]: + logger.info(f"Applying a {method} filter (order: {order}) to the signal, with cutoff frequencies at {cutoffs[0]} and {cutoffs[1]} Hz") + b, a = signal.butter(int(order), nyq_cutoff, btype=method) filtered = utils.new_physio_like(data, signal.filtfilt(b, a, data)) @@ -99,6 +105,8 @@ def interpolate_physio(data, target_fs, *, kind="cubic"): suppinterp = None else: suppinterp = interpolate.interp1d(t_orig, data.suppdata, kind=kind)(t_new) + + logger.info(f"Interpolating the signal at {target_fs} Hz (Interpolation ratio: {factor}).") interp = utils.new_physio_like(data, interp, fs=target_fs, suppdata=suppinterp) return interp @@ -128,19 +136,21 @@ def peakfind_physio(data, *, thresh=0.2, dist=None): ensure_fs = True if dist is None else False data = utils.check_physio(data, ensure_fs=ensure_fs, copy=True) - # first pass peak detection to get approximate distance between peaks cdist = data.fs // 4 if dist is None else dist thresh = np.squeeze(np.diff(np.percentile(data, [5, 95]))) * thresh locs, heights = signal.find_peaks(data[:], distance=cdist, height=thresh) + logger.debug(f"First peak detection iteration. Acquiring approximate distance between peaks (Number of peaks: {len(locs)})") # second, more thorough peak detection cdist = np.diff(locs).mean() // 2 heights = np.percentile(heights["peak_heights"], 1) locs, heights = signal.find_peaks(data[:], distance=cdist, height=heights) data._metadata["peaks"] = locs + logger.debug(f"Second peak detection iteration. Acquiring more precise peak locations (Number of peaks: {len(locs)})") # perform trough detection based on detected peaks data._metadata["troughs"] = utils.check_troughs(data, data.peaks) + logger.debug(f"Trough detection based on detected peaks (Number of troughs: {len(data.troughs)})") return data @@ -159,7 +169,6 @@ def delete_peaks(data, remove): ------- data : Physio_like """ - 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) @@ -181,7 +190,6 @@ def reject_peaks(data, remove): ------- data : Physio_like """ - 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) @@ -203,30 +211,6 @@ def add_peaks(data, add): ------- 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 - - -@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) @@ -257,6 +241,7 @@ def edit_physio(data): return # perform manual editing + logger.info("Opening interactive peak editor") edits = editor._PhysioEditor(data) plt.show(block=True) @@ -288,7 +273,7 @@ def plot_physio(data, *, ax=None): ax : :class:`matplotlib.axes.Axes` Axis with plotted `data` """ - + logger.debug(f"Plotting {data}") # generate x-axis time series fs = 1 if np.isnan(data.fs) else data.fs time = np.arange(0, len(data) / fs, 1 / fs) diff --git a/peakdet/physio.py b/peakdet/physio.py index a615e65..dfb5497 100644 --- a/peakdet/physio.py +++ b/peakdet/physio.py @@ -42,6 +42,7 @@ class Physio: """ def __init__(self, data, fs=None, history=None, metadata=None, suppdata=None): + logger.debug("Initializing new Physio object") self._data = np.asarray(data).squeeze() if self.data.ndim > 1: raise ValueError( From 81750e34bc28fc420cc6eb59da1d20f673140d81 Mon Sep 17 00:00:00 2001 From: maestroque Date: Sun, 2 Jun 2024 14:06:47 +0200 Subject: [PATCH 21/30] Add info logs on editor.py and io.py --- peakdet/editor.py | 3 ++- peakdet/io.py | 6 ++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/peakdet/editor.py b/peakdet/editor.py index 85672ce..1cff31f 100644 --- a/peakdet/editor.py +++ b/peakdet/editor.py @@ -143,7 +143,7 @@ def on_edit(self, xmin, xmax, *, method): method accepts 'insert', 'reject', 'delete' """ - logger.debug("Edit") + logger.debug("Edited peaks with action: {}", method) if method not in ["insert", "reject", "delete"]: raise ValueError(f'Action "{method}" not supported.') @@ -186,6 +186,7 @@ def undo(self): # pop off last edit and delete func, peaks = self.data._history.pop() + logger.debug(f"Undo previous action: {func}") if func == "reject_peaks": self.data._metadata["reject"] = np.setdiff1d( diff --git a/peakdet/io.py b/peakdet/io.py index 9218618..07abe18 100644 --- a/peakdet/io.py +++ b/peakdet/io.py @@ -61,9 +61,11 @@ def load_physio(data, *, fs=None, dtype=None, history=None, allow_pickle=False): inp["history"] = list(map(tuple, inp["history"])) except (IOError, OSError, ValueError): inp = dict(data=np.loadtxt(data), history=[utils._get_call(exclude=[])]) + logger.debug("Instantiating Physio object from a file") phys = physio.Physio(**inp) # if we got a numpy array, load that into a Physio object elif isinstance(data, np.ndarray): + logger.debug("Instantiating Physio object from numpy array") if history is None: logger.warning( "Loading data from a numpy array without providing a" @@ -73,6 +75,7 @@ def load_physio(data, *, fs=None, dtype=None, history=None, allow_pickle=False): 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): + logger.debug("Instantiating a new Physio object from the provided Physio object") phys = utils.new_physio_like(data, data.data, fs=fs, dtype=dtype) phys._history += [utils._get_call()] else: @@ -119,6 +122,7 @@ def save_physio(fname, data): np.savez_compressed( dest, data=data.data, fs=data.fs, history=hist, metadata=data._metadata ) + logger.info(f"Saved {data} in {fname}") return fname @@ -149,6 +153,7 @@ def load_history(file, verbose=False): history = json.load(src) # replay history from beginning and return resultant Physio object + logger.info(f"Replaying history from {file}") data = None for func, kwargs in history: if verbose: @@ -211,5 +216,6 @@ def save_history(file, data): file += ".json" if not file.endswith(".json") else "" with open(file, "w") as dest: json.dump(data.history, dest, indent=4) + logger.info(f"Saved {data} history in {file}") return file From 7f8fd6a4ee4b680e3f6b091caad9cfd1c3cb7509 Mon Sep 17 00:00:00 2001 From: maestroque Date: Tue, 4 Jun 2024 22:56:08 +0300 Subject: [PATCH 22/30] Fix warning catching assertions in tests --- peakdet/tests/__init__.py | 3 +++ peakdet/tests/conftest.py | 15 +++++++++++++++ peakdet/tests/test_external.py | 22 +++++++++++----------- peakdet/tests/test_io.py | 20 ++++++++++---------- 4 files changed, 39 insertions(+), 21 deletions(-) create mode 100644 peakdet/tests/conftest.py diff --git a/peakdet/tests/__init__.py b/peakdet/tests/__init__.py index bed8839..0263b67 100644 --- a/peakdet/tests/__init__.py +++ b/peakdet/tests/__init__.py @@ -1,3 +1,6 @@ from peakdet.tests.utils import get_test_data_path +from peakdet.utils import enable_logger __all__ = ["get_test_data_path"] + +enable_logger("INFO", True, True) diff --git a/peakdet/tests/conftest.py b/peakdet/tests/conftest.py new file mode 100644 index 0000000..f1ebb0e --- /dev/null +++ b/peakdet/tests/conftest.py @@ -0,0 +1,15 @@ +import pytest +from loguru import logger +from _pytest.logging import LogCaptureFixture + +@pytest.fixture +def caplog(caplog: LogCaptureFixture): + handler_id = logger.add( + caplog.handler, + format="{message}", + level=0, + filter=lambda record: record["level"].no >= caplog.handler.level, + enqueue=False, + ) + yield caplog + logger.remove(handler_id) \ No newline at end of file diff --git a/peakdet/tests/test_external.py b/peakdet/tests/test_external.py index 23c695e..26bbf2c 100644 --- a/peakdet/tests/test_external.py +++ b/peakdet/tests/test_external.py @@ -8,14 +8,14 @@ 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.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.0 - ) +def test_load_rtpeaks(caplog): + for channel in [1, 2, 9]: + 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.0 + ) + assert "WARNING" in caplog.text diff --git a/peakdet/tests/test_io.py b/peakdet/tests/test_io.py index 8e032ad..4e8e2f7 100644 --- a/peakdet/tests/test_io.py +++ b/peakdet/tests/test_io.py @@ -10,16 +10,16 @@ from peakdet.tests.utils import get_test_data_path -def test_load_physio(): +def test_load_physio(caplog): # try loading pickle file (from io.save_physio) 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.0 - with pytest.warns(UserWarning): - pckl = io.load_physio( - get_test_data_path("ECG.phys"), fs=500.0, allow_pickle=True - ) + pckl = io.load_physio( + get_test_data_path("ECG.phys"), fs=500.0, allow_pickle=True + ) + assert "WARNING" in caplog.text assert pckl.fs == 500.0 # try loading CSV file @@ -30,8 +30,8 @@ def test_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 "WARNING" in caplog.text assert isinstance(arr, physio.Physio) arr = io.load_physio( np.loadtxt(get_test_data_path("ECG.csv")), @@ -74,7 +74,7 @@ def test_load_history(tmpdir): assert filt.fs == replayed.fs -def test_save_history(tmpdir): +def test_save_history(tmpdir, caplog): # get paths of data, original history, new history fname = get_test_data_path("ECG.csv") orig_history = get_test_data_path("history.json") @@ -82,8 +82,8 @@ def test_save_history(tmpdir): # make physio object and perform some operations phys = physio.Physio(np.loadtxt(fname), fs=1000.0) - with pytest.warns(UserWarning): # no history = warning - io.save_history(temp_history, phys) + io.save_history(temp_history, phys) + assert "WARNING" in caplog.text # no history = warning filt = operations.filter_physio(phys, [5.0, 15.0], "bandpass") path = io.save_history(temp_history, filt) # dump history= From 86adcf3a22ea54242293a1be6f3241ee7adb12c8 Mon Sep 17 00:00:00 2001 From: maestroque Date: Tue, 4 Jun 2024 23:36:15 +0300 Subject: [PATCH 23/30] Improve warning log assertions when there are multiple expected warnings per test --- peakdet/tests/__init__.py | 2 +- peakdet/tests/conftest.py | 2 +- peakdet/tests/test_external.py | 2 +- peakdet/tests/test_io.py | 6 +++--- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/peakdet/tests/__init__.py b/peakdet/tests/__init__.py index 0263b67..44aec0d 100644 --- a/peakdet/tests/__init__.py +++ b/peakdet/tests/__init__.py @@ -3,4 +3,4 @@ __all__ = ["get_test_data_path"] -enable_logger("INFO", True, True) +enable_logger("DEBUG", True, True) diff --git a/peakdet/tests/conftest.py b/peakdet/tests/conftest.py index f1ebb0e..d20e0bb 100644 --- a/peakdet/tests/conftest.py +++ b/peakdet/tests/conftest.py @@ -7,7 +7,7 @@ def caplog(caplog: LogCaptureFixture): handler_id = logger.add( caplog.handler, format="{message}", - level=0, + level=20, filter=lambda record: record["level"].no >= caplog.handler.level, enqueue=False, ) diff --git a/peakdet/tests/test_external.py b/peakdet/tests/test_external.py index 26bbf2c..2933b67 100644 --- a/peakdet/tests/test_external.py +++ b/peakdet/tests/test_external.py @@ -18,4 +18,4 @@ def test_load_rtpeaks(caplog): external.load_rtpeaks( testutils.get_test_data_path("ECG.csv"), channel=channel, fs=1000.0 ) - assert "WARNING" in caplog.text + assert caplog.text.count("WARNING") > 1 diff --git a/peakdet/tests/test_io.py b/peakdet/tests/test_io.py index 4e8e2f7..284f60d 100644 --- a/peakdet/tests/test_io.py +++ b/peakdet/tests/test_io.py @@ -19,7 +19,7 @@ def test_load_physio(caplog): pckl = io.load_physio( get_test_data_path("ECG.phys"), fs=500.0, allow_pickle=True ) - assert "WARNING" in caplog.text + assert caplog.text.count("WARNING") == 1 assert pckl.fs == 500.0 # try loading CSV file @@ -31,7 +31,7 @@ def test_load_physio(caplog): # try loading array arr = io.load_physio(np.loadtxt(get_test_data_path("ECG.csv"))) - assert "WARNING" in caplog.text + assert caplog.text.count("WARNING") == 2 assert isinstance(arr, physio.Physio) arr = io.load_physio( np.loadtxt(get_test_data_path("ECG.csv")), @@ -83,7 +83,7 @@ def test_save_history(tmpdir, caplog): # make physio object and perform some operations phys = physio.Physio(np.loadtxt(fname), fs=1000.0) io.save_history(temp_history, phys) - assert "WARNING" in caplog.text # no history = warning + assert caplog.text.count("WARNING") == 1 # no history = warning filt = operations.filter_physio(phys, [5.0, 15.0], "bandpass") path = io.save_history(temp_history, filt) # dump history= From b52d13a76f14f0c5895b4ceddbb42df7ed0208c1 Mon Sep 17 00:00:00 2001 From: maestroque Date: Tue, 4 Jun 2024 23:37:21 +0300 Subject: [PATCH 24/30] Style fix --- peakdet/analytics.py | 2 +- peakdet/editor.py | 2 +- peakdet/io.py | 4 +++- peakdet/operations.py | 28 ++++++++++++++++++++-------- peakdet/tests/conftest.py | 5 +++-- peakdet/tests/test_external.py | 2 +- peakdet/tests/test_io.py | 4 +--- 7 files changed, 30 insertions(+), 17 deletions(-) diff --git a/peakdet/analytics.py b/peakdet/analytics.py index 825a879..3f5f645 100644 --- a/peakdet/analytics.py +++ b/peakdet/analytics.py @@ -4,9 +4,9 @@ """ import numpy as np +from loguru import logger from scipy.interpolate import interp1d from scipy.signal import welch -from loguru import logger class HRV: diff --git a/peakdet/editor.py b/peakdet/editor.py index 1cff31f..c723ae4 100644 --- a/peakdet/editor.py +++ b/peakdet/editor.py @@ -5,10 +5,10 @@ import matplotlib.pyplot as plt import numpy as np +from loguru import logger from matplotlib.widgets import SpanSelector from peakdet import operations, utils -from loguru import logger class _PhysioEditor: diff --git a/peakdet/io.py b/peakdet/io.py index 07abe18..697f130 100644 --- a/peakdet/io.py +++ b/peakdet/io.py @@ -75,7 +75,9 @@ def load_physio(data, *, fs=None, dtype=None, history=None, allow_pickle=False): 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): - logger.debug("Instantiating a new Physio object from the provided Physio object") + logger.debug( + "Instantiating a new Physio object from the provided Physio object" + ) phys = utils.new_physio_like(data, data.data, fs=fs, dtype=dtype) phys._history += [utils._get_call()] else: diff --git a/peakdet/operations.py b/peakdet/operations.py index 0aa9453..480b0c0 100644 --- a/peakdet/operations.py +++ b/peakdet/operations.py @@ -3,9 +3,9 @@ Functions for processing and interpreting physiological data """ -from loguru import logger import matplotlib.pyplot as plt import numpy as np +from loguru import logger from scipy import interpolate, signal from peakdet import editor, utils @@ -60,10 +60,14 @@ def filter_physio(data, cutoffs, method, *, order=3): ) if method in ["lowpass", "highpass"]: - logger.info(f"Applying a {method} filter (order: {order}) to the signal, with cutoff frequency at {cutoffs} Hz") + logger.info( + f"Applying a {method} filter (order: {order}) to the signal, with cutoff frequency at {cutoffs} Hz" + ) elif method in ["bandpass", "bandstop"]: - logger.info(f"Applying a {method} filter (order: {order}) to the signal, with cutoff frequencies at {cutoffs[0]} and {cutoffs[1]} Hz") - + logger.info( + f"Applying a {method} filter (order: {order}) to the signal, with cutoff frequencies at {cutoffs[0]} and {cutoffs[1]} Hz" + ) + b, a = signal.butter(int(order), nyq_cutoff, btype=method) filtered = utils.new_physio_like(data, signal.filtfilt(b, a, data)) @@ -106,7 +110,9 @@ def interpolate_physio(data, target_fs, *, kind="cubic"): else: suppinterp = interpolate.interp1d(t_orig, data.suppdata, kind=kind)(t_new) - logger.info(f"Interpolating the signal at {target_fs} Hz (Interpolation ratio: {factor}).") + logger.info( + f"Interpolating the signal at {target_fs} Hz (Interpolation ratio: {factor})." + ) interp = utils.new_physio_like(data, interp, fs=target_fs, suppdata=suppinterp) return interp @@ -140,17 +146,23 @@ def peakfind_physio(data, *, thresh=0.2, dist=None): cdist = data.fs // 4 if dist is None else dist thresh = np.squeeze(np.diff(np.percentile(data, [5, 95]))) * thresh locs, heights = signal.find_peaks(data[:], distance=cdist, height=thresh) - logger.debug(f"First peak detection iteration. Acquiring approximate distance between peaks (Number of peaks: {len(locs)})") + logger.debug( + f"First peak detection iteration. Acquiring approximate distance between peaks (Number of peaks: {len(locs)})" + ) # second, more thorough peak detection cdist = np.diff(locs).mean() // 2 heights = np.percentile(heights["peak_heights"], 1) locs, heights = signal.find_peaks(data[:], distance=cdist, height=heights) data._metadata["peaks"] = locs - logger.debug(f"Second peak detection iteration. Acquiring more precise peak locations (Number of peaks: {len(locs)})") + logger.debug( + f"Second peak detection iteration. Acquiring more precise peak locations (Number of peaks: {len(locs)})" + ) # perform trough detection based on detected peaks data._metadata["troughs"] = utils.check_troughs(data, data.peaks) - logger.debug(f"Trough detection based on detected peaks (Number of troughs: {len(data.troughs)})") + logger.debug( + f"Trough detection based on detected peaks (Number of troughs: {len(data.troughs)})" + ) return data diff --git a/peakdet/tests/conftest.py b/peakdet/tests/conftest.py index d20e0bb..273030e 100644 --- a/peakdet/tests/conftest.py +++ b/peakdet/tests/conftest.py @@ -1,6 +1,7 @@ import pytest -from loguru import logger from _pytest.logging import LogCaptureFixture +from loguru import logger + @pytest.fixture def caplog(caplog: LogCaptureFixture): @@ -12,4 +13,4 @@ def caplog(caplog: LogCaptureFixture): enqueue=False, ) yield caplog - logger.remove(handler_id) \ No newline at end of file + logger.remove(handler_id) diff --git a/peakdet/tests/test_external.py b/peakdet/tests/test_external.py index 2933b67..4eb5efe 100644 --- a/peakdet/tests/test_external.py +++ b/peakdet/tests/test_external.py @@ -9,7 +9,7 @@ def test_load_rtpeaks(caplog): - for channel in [1, 2, 9]: + for channel in [1, 2, 9]: 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)] diff --git a/peakdet/tests/test_io.py b/peakdet/tests/test_io.py index 284f60d..0daa79a 100644 --- a/peakdet/tests/test_io.py +++ b/peakdet/tests/test_io.py @@ -16,9 +16,7 @@ def test_load_physio(caplog): assert isinstance(pckl, physio.Physio) assert pckl.data.size == 44611 assert pckl.fs == 1000.0 - pckl = io.load_physio( - get_test_data_path("ECG.phys"), fs=500.0, allow_pickle=True - ) + pckl = io.load_physio(get_test_data_path("ECG.phys"), fs=500.0, allow_pickle=True) assert caplog.text.count("WARNING") == 1 assert pckl.fs == 500.0 From c568cb2b5f517eb2659dfe6893a95883825310a3 Mon Sep 17 00:00:00 2001 From: maestroque Date: Thu, 6 Jun 2024 14:21:05 +0300 Subject: [PATCH 25/30] Remove unused imports --- peakdet/analytics.py | 1 - peakdet/modalities.py | 1 - 2 files changed, 2 deletions(-) diff --git a/peakdet/analytics.py b/peakdet/analytics.py index 3f5f645..7467440 100644 --- a/peakdet/analytics.py +++ b/peakdet/analytics.py @@ -4,7 +4,6 @@ """ import numpy as np -from loguru import logger from scipy.interpolate import interp1d from scipy.signal import welch diff --git a/peakdet/modalities.py b/peakdet/modalities.py index 331106a..8f4e009 100644 --- a/peakdet/modalities.py +++ b/peakdet/modalities.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- import numpy as np -from loguru import logger class HRModality: From a78544cae0053afc1a95c9cc1a8d565e664f7843 Mon Sep 17 00:00:00 2001 From: maestroque Date: Thu, 13 Jun 2024 17:33:38 +0300 Subject: [PATCH 26/30] Fix loguru not detecting module's name --- peakdet/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/peakdet/__init__.py b/peakdet/__init__.py index 795872a..324c738 100644 --- a/peakdet/__init__.py +++ b/peakdet/__init__.py @@ -37,5 +37,4 @@ __version__ = get_versions()["version"] del get_versions -# TODO: Loguru does not detect the module's name -logger.disable("") +logger.disable("peakdet") From a008c72f8911e35084a5e0e47055c8db75f39cf2 Mon Sep 17 00:00:00 2001 From: maestroque Date: Thu, 13 Jun 2024 20:06:49 +0300 Subject: [PATCH 27/30] Add more logger utilities. Fix multiple logger instance bug --- peakdet/utils.py | 71 ++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 66 insertions(+), 5 deletions(-) diff --git a/peakdet/utils.py b/peakdet/utils.py index 3af1606..b3041cf 100644 --- a/peakdet/utils.py +++ b/peakdet/utils.py @@ -159,7 +159,7 @@ def new_physio_like( dtype=None, copy_history=True, copy_metadata=True, - copy_suppdata=True + copy_suppdata=True, ): """ Makes `data` into physio object like `ref_data` @@ -261,7 +261,68 @@ def enable_logger(loglevel="INFO", diagnose=True, backtrace=True): loglevel, _valid_loglevels ) ) - logger.enable("") - logger.remove(0) - logger.add(sys.stderr, level=loglevel, backtrace=backtrace, diagnose=diagnose) - logger.debug("Enabling logger") + logger.enable("peakdet") + try: + logger.remove(0) + except ValueError: + logger.warning( + "The logger has been already enabled. If you want to" + "change the log level of an existing logger, please" + "refer to the change_loglevel() function. (Note: You can" + "find the log_handle either from the initial call of this" + "function, or the console logs)" + ) + return + log_handle = logger.add( + sys.stderr, level=loglevel, backtrace=backtrace, diagnose=diagnose + ) + logger.debug(f"Enabling logger with handle_id: {log_handle}") + return log_handle + + +def change_loglevel(log_handle, loglevel, diagnose=True, backtrace=True): + """ + Change the loguru logger's log level. The logger needs to + be already enabled by `enable_logger()` + + Parameters + ---------- + log_handle : Enabled logger's handle, returned by `enable_logger()` + loglevel : {'INFO', 'DEBUG', 'WARNING', 'ERROR'} + """ + _valid_loglevels = ["INFO", "DEBUG", "WARNING", "ERROR"] + + if loglevel not in _valid_loglevels: + raise ValueError( + "Provided log level {} is not permitted; must be in {}.".format( + loglevel, _valid_loglevels + ) + ) + logger.remove(log_handle) + new_log_handle = logger.add( + sys.stderr, level=loglevel, backtrace=backtrace, diagnose=diagnose + ) + logger.info( + f'Changing the logger log level to "{loglevel}" (New logger handle_id: {new_log_handle})' + ) + return new_log_handle + + +def disable_logger(log_handle=None): + """ + Change the loguru logger's log level. The logger needs to + be already enabled by `enable_logger()` + + Parameters + ---------- + log_handle : Enabled logger's handle, returned by `enable_logger()` + Default: None + If left as None, this function will disable all logger instances + """ + if log_handle is None: + logger.info("Disabling all logger instances") + logger.remove() + else: + logger.info(f"Disabling logger with handle_id: {log_handle}") + logger.remove(log_handle) + logger.disable("peakdet") From 8f6b9447e4fc82a3063df3b22378b3d77236700b Mon Sep 17 00:00:00 2001 From: maestroque Date: Fri, 14 Jun 2024 20:07:12 +0300 Subject: [PATCH 28/30] Remove commented out line --- peakdet/cli/run.py | 1 - 1 file changed, 1 deletion(-) diff --git a/peakdet/cli/run.py b/peakdet/cli/run.py index 873fca6..91b72e1 100644 --- a/peakdet/cli/run.py +++ b/peakdet/cli/run.py @@ -11,7 +11,6 @@ from loguru import logger -# from gooey import Gooey, GooeyParser import peakdet TARGET = "pythonw" if sys.platform == "darwin" else "python" From b86561771a2c7cce725fcc5624a923ee99d5cb54 Mon Sep 17 00:00:00 2001 From: maestroque Date: Fri, 14 Jun 2024 20:15:59 +0300 Subject: [PATCH 29/30] Fix imports in cli/run.py --- peakdet/cli/run.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/peakdet/cli/run.py b/peakdet/cli/run.py index 91b72e1..bdc7eb3 100644 --- a/peakdet/cli/run.py +++ b/peakdet/cli/run.py @@ -1,18 +1,17 @@ # -*- coding: utf-8 -*- +import argparse +import datetime import glob import os import sys import matplotlib - -matplotlib.use("WXAgg") -import argparse -import datetime - from loguru import logger import peakdet +matplotlib.use("WXAgg") + TARGET = "pythonw" if sys.platform == "darwin" else "python" TARGET += " -u " + os.path.abspath(__file__) From 145d48fa08f216e688eda9ffe6dba70c999c8d03 Mon Sep 17 00:00:00 2001 From: maestroque Date: Fri, 14 Jun 2024 20:48:06 +0300 Subject: [PATCH 30/30] Make --quiet and --debug cli options mutually exclusive, remove matplotlib and click.edit import from cli/run.py --- peakdet/cli/run.py | 48 ++++++++++++++++++++-------------------------- 1 file changed, 21 insertions(+), 27 deletions(-) diff --git a/peakdet/cli/run.py b/peakdet/cli/run.py index bdc7eb3..b08a7ca 100644 --- a/peakdet/cli/run.py +++ b/peakdet/cli/run.py @@ -5,13 +5,10 @@ import os import sys -import matplotlib from loguru import logger import peakdet -matplotlib.use("WXAgg") - TARGET = "pythonw" if sys.platform == "darwin" else "python" TARGET += " -u " + os.path.abspath(__file__) @@ -127,15 +124,21 @@ def get_parser(): type=float, help="Threshold for peak detection algorithm.", ) - edit_group.add_argument( + + log_style_group = parser.add_argument_group( + "Logging style arguments (optional and mutually exclusive)", + "Options to specify the logging style", + ) + log_style_group_exclusive = log_style_group.add_mutually_exclusive_group() + log_style_group_exclusive.add_argument( "-debug", "--debug", dest="debug", action="store_true", - help="Only print debugging info to log file. Default is False.", + help="Print additional debugging info and error diagnostics to log file. Default is False.", default=False, ) - edit_group.add_argument( + log_style_group_exclusive.add_argument( "-quiet", "--quiet", dest="quiet", @@ -143,14 +146,6 @@ def get_parser(): help="Only print warnings to log file. Default is False.", default=False, ) - edit_group.add_argument( - "-verbose", - "--verbose", - dest="verbose", - action="store_true", - help="Print verbose error logs with diagnostics", - default=False, - ) return parser @@ -168,7 +163,6 @@ def workflow( noedit=False, thresh=0.2, measurements=ATTR_CONV.keys(), - verbose=False, debug=False, quiet=False, ): @@ -219,45 +213,45 @@ def workflow( sys.stderr, level="WARNING", colorize=True, - backtrace=verbose, - diagnose=verbose, + backtrace=False, + diagnose=False, ) logger.add( logname, level="WARNING", colorize=False, - backtrace=verbose, - diagnose=verbose, + backtrace=False, + diagnose=False, ) elif debug: logger.add( sys.stderr, level="DEBUG", colorize=True, - backtrace=verbose, - diagnose=verbose, + backtrace=True, + diagnose=True, ) logger.add( logname, level="DEBUG", colorize=False, - backtrace=verbose, - diagnose=verbose, + backtrace=True, + diagnose=True, ) else: logger.add( sys.stderr, level="INFO", colorize=True, - backtrace=verbose, - diagnose=verbose, + backtrace=True, + diagnose=False, ) logger.add( logname, level="INFO", colorize=False, - backtrace=verbose, - diagnose=verbose, + backtrace=True, + diagnose=False, ) # output file