diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs index 85404fc6..b18e6336 100644 --- a/.git-blame-ignore-revs +++ b/.git-blame-ignore-revs @@ -1,2 +1,8 @@ # Apply ruff formatting and safe lint fixes 8790758451dfde249db2c31d4cc4ff13f5106fd2 + +# Wrap docstrings and comments at 88 columns +fbe3d8048f4711fc09da647a8cf2bf4c2e4a8673 + +# Start docstrings on the opening quote line with a one-line summary +1594919c9ce5a8dcc4af83c253cc5411f2492df2 diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 65cb590b..a35f9428 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -17,4 +17,4 @@ jobs: - name: Install Python run: uv python install 3.12 - name: Build documentation - run: uv run --python 3.12 --only-group docs make -C docs html + run: uv run --python 3.12 --only-group docs make -C docs html SPHINXOPTS="-W --keep-going" diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000..cb6fae2c --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,32 @@ +# AGENTS.md + +WFDB-Python is a library for reading, writing and processing physiologic signals and annotations in the WFDB format. See [DEVELOPING.md](DEVELOPING.md) for the full contributor guide. + +## Layout + +- `src/wfdb/`: the package (`io`, `plot`, `processing`) +- `tests/`: pytest tests, which read sample files from `sample-data/` relative to the repository root, so run them from there +- `docs/`: Sphinx documentation, including the changelog in `docs/changes.rst` + +## Commands + +The project uses [uv](https://docs.astral.sh/uv/). `uv run` installs the `dev` dependency group automatically. + +```sh +uv run pytest # all tests +uv run pytest -m "not network" # only tests that work offline +uv run pytest -n auto # in parallel +uv run ruff format . # format +uv run ruff check . # lint +uv run --only-group docs make -C docs html SPHINXOPTS="-W --keep-going" +``` + +## Conventions + +- Format and lint with Ruff before committing. Lines (including docstrings and comments) are at most 88 columns. +- Docstrings use the numpy style. Start the docstring directly after the opening quotes with a summary that fits on one line, followed by a blank line and the details (if any). +- The docs build treats warnings as errors, so check it after changing a docstring. +- Tests are plain pytest functions or classes (no `unittest`). Tests that need PhysioNet are marked `network` and are skipped if PhysioNet cannot be reached. Where possible, a test has a `local` variant and a `physionet` variant through the `source` fixture. +- Add a changelog entry to `docs/changes.rst` for user-visible changes. +- The version is set in `pyproject.toml` only. Do not bump it as part of a feature or fix. +- Commits that only reformat code are listed in `.git-blame-ignore-revs`. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..b465f32d --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,3 @@ +# CLAUDE.md + +See @AGENTS.md for guidance on working with this repository. diff --git a/DEVELOPING.md b/DEVELOPING.md index d22dd3a1..6a1e11ab 100644 --- a/DEVELOPING.md +++ b/DEVELOPING.md @@ -4,7 +4,7 @@ We welcome community contributions in the form of pull requests. When contributing code, please ensure: -- Documentation is provided. New functions and classes should have numpy/scipy style [docstrings](https://github.com/numpy/numpy/blob/master/doc/HOWTO_DOCUMENT.rst.txt). +- Documentation is provided. New functions and classes should have numpy/scipy style [docstrings](https://github.com/numpy/numpy/blob/master/doc/HOWTO_DOCUMENT.rst.txt). Start the docstring on the line of the opening quotes, with a summary that fits on a single line (the documentation build fails on warnings, so check it with `uv run --only-group docs make -C docs html SPHINXOPTS="-W --keep-going"`). - Unit tests are written for new features that are not covered by [existing tests](https://github.com/MIT-LCP/wfdb-python/tree/main/tests). - The code style is consistent with the project's formating standards. diff --git a/docs/conf.py b/docs/conf.py index cdda06d7..06cd929b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -20,7 +20,7 @@ import sys import tomllib -sys.path.insert(0, os.path.abspath("..")) +sys.path.insert(0, os.path.abspath("../src")) # For dependencies from unittest.mock import MagicMock @@ -49,6 +49,10 @@ def __getattr__(cls, name): "numpydoc", ] +# The class members are documented by the autoclass directives, so don't link the +# method tables to (non-existent) autosummary stubs. +numpydoc_class_members_toctree = False + # Add any paths that contain templates here, relative to this directory. templates_path = ["_templates"] diff --git a/docs/io.rst b/docs/io.rst index a67fbdf7..b47f94bc 100644 --- a/docs/io.rst +++ b/docs/io.rst @@ -12,21 +12,25 @@ WFDB Records :members: rdrecord, rdheader, rdsamp, wrsamp .. autoclass:: wfdb.io.Record + :no-index: :members: get_frame_number, get_elapsed_time, get_absolute_time, wrsamp, adc, dac, to_dataframe .. autoclass:: wfdb.io.MultiRecord + :no-index: :members: get_frame_number, get_elapsed_time, get_absolute_time, multi_to_single, contained_ranges, contained_combined_ranges WFDB Annotations ---------------- +---------------- .. automodule:: wfdb.io + :no-index: :members: rdann, wrann, show_ann_labels, show_ann_classes .. autoclass:: wfdb.io.Annotation + :no-index: :members: wrann @@ -86,4 +90,5 @@ Downloading ----------- .. automodule:: wfdb.io + :no-index: :members: dl_files, dl_database, get_dbs, get_record_list, set_db_index_url diff --git a/docs/processing.rst b/docs/processing.rst index 96c1a19a..988211cd 100644 --- a/docs/processing.rst +++ b/docs/processing.rst @@ -17,6 +17,7 @@ Heart Rate ---------- .. automodule:: wfdb.processing + :no-index: :members: compute_hr, calc_rr, calc_mean_hr, ann2rr, rr2ann @@ -24,18 +25,21 @@ Peaks ----- .. automodule:: wfdb.processing + :no-index: :members: find_peaks, find_local_peaks, correct_peaks Filters ------- .. automodule:: wfdb.processing + :no-index: :members: sigavg QRS Detectors ------------- .. automodule:: wfdb.processing + :no-index: :members: XQRS, xqrs_detect, gqrs_detect @@ -43,4 +47,5 @@ Annotation Evaluators --------------------- .. automodule:: wfdb.processing + :no-index: :members: Comparitor, compare_annotations, benchmark_mitdb diff --git a/docs/wfdb.rst b/docs/wfdb.rst index 62d3b7b2..bfda6b07 100644 --- a/docs/wfdb.rst +++ b/docs/wfdb.rst @@ -21,9 +21,10 @@ WFDB Records WFDB Annotations ---------------- +---------------- .. automodule:: wfdb + :no-index: :members: rdann, wrann, show_ann_labels, show_ann_classes .. autoclass:: wfdb.Annotation @@ -34,6 +35,7 @@ Downloading ----------- .. automodule:: wfdb + :no-index: :members: dl_files, dl_database, get_dbs, get_record_list, set_db_index_url @@ -41,4 +43,5 @@ Plotting -------- .. automodule:: wfdb + :no-index: :members: plot_items, plot_wfdb, plot_all_records diff --git a/pyproject.toml b/pyproject.toml index eea2a041..95a9e5d1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -37,7 +37,7 @@ dev = [ docs = [ "fsspec >= 2023.10.0", "numpydoc == 1.7.0", - "sphinx == 7.0.0", + "sphinx == 7.2.6", ] [tool.ruff] @@ -62,6 +62,10 @@ ignore = [ "B905", ] +[tool.ruff.lint.pycodestyle] +# Wrap docstrings and comments at the same width as the code +max-doc-length = 88 + [tool.ruff.lint.per-file-ignores] # Re-exports "__init__.py" = ["F401"] @@ -71,6 +75,3 @@ addopts = "-r s" markers = [ "network: requires access to PhysioNet (skipped if it cannot be reached)", ] - -[tool.uv.build-backend] -module-root = "" diff --git a/wfdb/__init__.py b/src/wfdb/__init__.py similarity index 100% rename from wfdb/__init__.py rename to src/wfdb/__init__.py diff --git a/wfdb/io/__init__.py b/src/wfdb/io/__init__.py similarity index 100% rename from wfdb/io/__init__.py rename to src/wfdb/io/__init__.py diff --git a/wfdb/io/_coreio.py b/src/wfdb/io/_coreio.py similarity index 97% rename from wfdb/io/_coreio.py rename to src/wfdb/io/_coreio.py index 2835213b..74f76822 100644 --- a/wfdb/io/_coreio.py +++ b/src/wfdb/io/_coreio.py @@ -20,8 +20,7 @@ def _open_file( newline=None, check_access=False, ): - """ - Open a data file as a random-access file object. + """Open a data file as a random-access file object. See the documentation of `open` and `wfdb.io._url.openurl` for details about the `mode`, `buffering`, `encoding`, `errors`, and `newline` diff --git a/wfdb/io/_header.py b/src/wfdb/io/_header.py similarity index 90% rename from wfdb/io/_header.py rename to src/wfdb/io/_header.py index dabc6678..255a2716 100644 --- a/wfdb/io/_header.py +++ b/src/wfdb/io/_header.py @@ -130,9 +130,9 @@ class BaseHeaderMixin: - """ - Mixin class with multi-segment header methods. Inherited by Record and - MultiRecord classes. + """Mixin class with multi-segment header methods. + + Inherited by Record and MultiRecord classes. Attributes ---------- @@ -141,11 +141,10 @@ class BaseHeaderMixin: """ def get_write_subset(self, spec_type): - """ - Get a set of fields used to write the header; either 'record' - or 'signal' specification fields. Helper function for - `get_write_fields`. Gets the default required fields, the user - defined fields, and their dependencies. + """Get the fields used to write a 'record' or 'signal' header. + + Helper function for `get_write_fields`. Gets the default required fields, the + user defined fields, and their dependencies. Parameters ---------- @@ -189,7 +188,8 @@ def get_write_subset(self, spec_type): if self.comments is not None: write_fields.append("comments") - # signal spec field. Need to return a potentially different list for each channel. + # signal spec field. Need to return a potentially different list for each + # channel. elif spec_type == "signal": # List of lists for each channel write_fields = [] @@ -203,7 +203,8 @@ def get_write_subset(self, spec_type): continue item = getattr(self, field) - # If the field is required by default or has been defined by the user + # If the field is required by default or has been defined by the + # user if signal_specs.loc[field, "write_required"] or ( item is not None and item[ch] is not None ): @@ -235,9 +236,7 @@ def get_write_subset(self, spec_type): return write_fields def preprocess_header(self, field): - """ - Get and process the fields as needed before writing - """ + """Get and process the fields as needed before writing""" original_field = getattr(self, field) string_field = str(original_field) @@ -254,8 +253,7 @@ def preprocess_header(self, field): class HeaderMixin(BaseHeaderMixin): - """ - Mixin class with single-segment header methods. Inherited by Record class. + """Mixin class with single-segment header methods. Inherited by Record class. Attributes ---------- @@ -264,9 +262,7 @@ class HeaderMixin(BaseHeaderMixin): """ def set_defaults(self): - """ - Set defaults for fields needed to write the header if they have - defaults. + """Set defaults for fields needed to write the header if they have defaults. Parameters ---------- @@ -295,9 +291,10 @@ def set_defaults(self): self.set_default(f) def wrheader(self, write_dir="", expanded=True): - """ - Write a WFDB header file. The signals are not used. Before - writing: + """Write a WFDB header file. + + The signals are not used. Before writing: + - Get the fields used to write the header for this instance. - Check each required field. - Check that the fields are cohesive with one another. @@ -344,10 +341,10 @@ def wrheader(self, write_dir="", expanded=True): self.wr_header_file(rec_write_fields, sig_write_fields, write_dir) def get_write_fields(self): - """ - Get the list of fields used to write the header, separating - record and signal specification fields. Returns the default - required fields, the user defined fields, and their dependencies. + """Get the header fields to write, split into record and signal fields. + + Returns the default required fields, the user defined fields, and their + dependencies. Does NOT include `d_signal` or `e_d_signal`. @@ -414,10 +411,9 @@ def _auto_signal_file_names(self): ] def set_default(self, field): - """ - Set the object's attribute to its default value if it is missing - and there is a default. Not responsible for initializing the - attribute. That is done by the constructor. + """Set a missing attribute to its default value, if it has one. + + Not responsible for initializing the attribute. That is done by the constructor. Parameters ---------- @@ -467,8 +463,7 @@ def set_default(self, field): ) def check_field_cohesion(self, rec_write_fields, sig_write_fields): - """ - Check the cohesion of fields used to write the header. + """Check the cohesion of fields used to write the header. Parameters ---------- @@ -493,7 +488,8 @@ def check_field_cohesion(self, rec_write_fields, sig_write_fields): "The length of field: " + f + " must match field n_sig." ) - # Each file_name must correspond to only one fmt, (and only one byte offset if defined). + # Each file_name must correspond to only one fmt, (and only one byte offset + # if defined). datfmts = {} for ch in range(self.n_sig): if self.file_name[ch] not in datfmts: @@ -519,9 +515,9 @@ def check_field_cohesion(self, rec_write_fields, sig_write_fields): ) def wr_header_file(self, rec_write_fields, sig_write_fields, write_dir): - """ - Write a header file using the specified fields. Converts Record - attributes into appropriate WFDB format strings. + """Write a header file using the specified fields. + + Converts Record attributes into appropriate WFDB format strings. Parameters ---------- @@ -580,9 +576,7 @@ def wr_header_file(self, rec_write_fields, sig_write_fields, write_dir): class MultiHeaderMixin(BaseHeaderMixin): - """ - Mixin class with multi-segment header methods. Inherited by - MultiRecord class. + """Mixin class with multi-segment header methods. Inherited by MultiRecord class. Attributes ---------- @@ -595,12 +589,12 @@ class MultiHeaderMixin(BaseHeaderMixin): segments: Sequence | None def set_defaults(self): - """ - Set defaults for fields needed to write the header if they have - defaults. This is NOT called by rdheader. It is only called by the - gateway wrsamp for convenience. It is also not called by wrheader since - it is supposed to be an explicit function. Not responsible for - initializing the attributes. That is done by the constructor. + """Set defaults for fields needed to write the header if they have defaults. + + This is NOT called by rdheader. It is only called by the gateway wrsamp for + convenience. It is also not called by wrheader since it is supposed to be an + explicit function. Not responsible for initializing the attributes. That is done + by the constructor. Parameters ---------- @@ -615,9 +609,10 @@ def set_defaults(self): self.set_default(field) def wrheader(self, write_dir=""): - """ - Write a multi-segment WFDB header file. The signals or segments are - not used. Before writing: + """Write a multi-segment WFDB header file. + + The signals or segments are not used. Before writing: + - Get the fields used to write the header for this instance. - Check each required field. - Check that the fields are cohesive with one another. @@ -651,8 +646,7 @@ def wrheader(self, write_dir=""): self.wr_header_file(write_fields, write_dir) def get_write_fields(self): - """ - Get the list of fields used to write the multi-segment header. + """Get the list of fields used to write the multi-segment header. Parameters ---------- @@ -677,8 +671,7 @@ def get_write_fields(self): return write_fields def set_default(self, field): - """ - Set a field to its default value if there is a default. + """Set a field to its default value if there is a default. Parameters ---------- @@ -701,8 +694,7 @@ def set_default(self, field): setattr(self, field, RECORD_SPECS[field].write_def) def check_field_cohesion(self): - """ - Check the cohesion of fields used to write the header. + """Check the cohesion of fields used to write the header. Parameters ---------- @@ -727,8 +719,7 @@ def check_field_cohesion(self): ) def wr_header_file(self, write_fields, write_dir): - """ - Write a header file using the specified fields. + """Write a header file using the specified fields. Parameters ---------- @@ -774,10 +765,10 @@ def wr_header_file(self, write_fields, write_dir): util.lines_to_file(self.record_name + ".hea", write_dir, header_lines) def get_sig_segments(self, sig_name=None): - """ - Get a list of the segment numbers that contain a particular signal - (or a dictionary of segment numbers for a list of signals). - Only works if information about the segments has been read in. + """Get the numbers of the segments that contain a signal. + + A list of signals returns a dictionary of segment numbers. Only works if + information about the segments has been read in. Parameters ---------- @@ -816,8 +807,7 @@ def get_sig_segments(self, sig_name=None): raise TypeError("sig_name must be a string or a list of strings") def get_sig_name(self): - """ - Get the signal names for the entire record. + """Get the signal names for the entire record. Parameters ---------- @@ -845,10 +835,10 @@ def get_sig_name(self): return sig_name def contained_ranges(self, sig_name: str) -> list[tuple[int, int]]: - """ - Given a signal name, return the sample ranges that contain signal values, - relative to the start of the full record. Does not account for NaNs/missing - values. + """Return the sample ranges that contain values of a signal. + + The ranges are relative to the start of the full record. Does not account for + NaNs/missing values. This function is mainly useful for variable layout records, but can also be used for fixed-layout records. Only works if the headers from the individual @@ -862,7 +852,8 @@ def contained_ranges(self, sig_name: str) -> list[tuple[int, int]]: Returns ------- ranges : List[Tuple[int, int]] - Tuple pairs which specify thee sample ranges in which the signal is contained. + Tuple pairs which specify the sample ranges in which the signal is + contained. The second value of each tuple pair will be one beyond the signal index. eg. A length 1000 signal would generate a tuple of: (0, 1000), allowing selection using signal[0:1000]. @@ -914,10 +905,10 @@ def contained_combined_ranges( self, sig_names: Sequence[str], ) -> list[tuple[int, int]]: - """ - Given a collection of signal name, return the sample ranges that - contain all of the specified signals, relative to the start of the - full record. Does not account for NaNs/missing values. + """Return the sample ranges that contain all of the given signals. + + The ranges are relative to the start of the full record. Does not account for + NaNs/missing values. This function is mainly useful for variable layout records, but can also be used for fixed-layout records. Only works if the headers from the individual @@ -931,7 +922,8 @@ def contained_combined_ranges( Returns ------- ranges : List[Tuple[int, int]] - Tuple pairs which specify thee sample ranges in which the signal is contained. + Tuple pairs which specify the sample ranges in which the signal is + contained. The second value of each tuple pair will be one beyond the signal index. eg. A length 1000 signal would generate a tuple of: (0, 1000), allowing selection using signal[0:1000]. @@ -954,9 +946,7 @@ def contained_combined_ranges( def wfdb_strptime(time_string: str) -> datetime.time: - """ - Given a time string in an acceptable WFDB format, return - a datetime.time object. + """Given a time string in an acceptable WFDB format, return a datetime.time object. Valid formats: SS, MM:SS, HH:MM:SS, all with and without microsec. @@ -987,8 +977,7 @@ def wfdb_strptime(time_string: str) -> datetime.time: def _parse_record_line(record_line: str) -> dict: - """ - Extract fields from a record line string into a dictionary. + """Extract fields from a record line string into a dictionary. Parameters ---------- @@ -1055,8 +1044,7 @@ def _parse_record_line(record_line: str) -> dict: def _parse_signal_lines(signal_lines): - """ - Extract fields from a list of signal line strings into a dictionary. + """Extract fields from a list of signal line strings into a dictionary. Parameters ---------- @@ -1123,8 +1111,7 @@ def _parse_signal_lines(signal_lines): def _read_segment_lines(segment_lines): - """ - Extract fields from segment line strings into a dictionary. + """Extract fields from segment line strings into a dictionary. Parameters ---------- diff --git a/wfdb/io/_signal.py b/src/wfdb/io/_signal.py similarity index 95% rename from wfdb/io/_signal.py rename to src/wfdb/io/_signal.py index 19c22092..7bdc5b60 100644 --- a/wfdb/io/_signal.py +++ b/src/wfdb/io/_signal.py @@ -110,8 +110,7 @@ class SignalMixin: - """ - Mixin class with signal methods. Inherited by Record class. + """Mixin class with signal methods. Inherited by Record class. Attributes ---------- @@ -121,9 +120,9 @@ class SignalMixin: """ def wr_dats(self, expanded, write_dir): - """ - Write all dat files associated with a record - expanded=True to use e_d_signal instead of d_signal. + """Write all dat files of a record. + + Use expanded=True to use e_d_signal instead of d_signal. Parameters ---------- @@ -163,9 +162,7 @@ def wr_dats(self, expanded, write_dir): self.wr_dat_files(expanded=expanded, write_dir=write_dir) def check_sig_cohesion(self, write_fields, expanded): - """ - Check the cohesion of the d_signal/e_d_signal field with the other - fields used to write the record. + """Check the cohesion of d_signal/e_d_signal with the other fields. Parameters ---------- @@ -198,7 +195,8 @@ def check_sig_cohesion(self, write_fields, expanded): f"samps_per_frame[{ch}]*sig_len" ) - # For each channel (if any), make sure the digital format has no values out of bounds + # For each channel (if any), make sure the digital format has no values out + # of bounds for ch in range(self.n_sig): fmt = self.fmt[ch] dmin, dmax = _digi_bounds(self.fmt[ch]) @@ -217,7 +215,8 @@ def check_sig_cohesion(self, write_fields, expanded): + str(fmt) ) - # Ensure that the checksums and initial value fields match the digital signal (if the fields are present) + # Ensure that the checksums and initial value fields match the digital + # signal (if the fields are present) if self.n_sig > 0: if "checksum" in write_fields: realchecksum = self.calc_checksum(expanded) @@ -251,7 +250,8 @@ def check_sig_cohesion(self, write_fields, expanded): print("d_signal.shape: ", self.d_signal.shape) raise ValueError("sig_len and n_sig do not match shape of d_signal") - # For each channel (if any), make sure the digital format has no values out of bounds + # For each channel (if any), make sure the digital format has no values out + # of bounds for ch in range(self.n_sig): fmt = self.fmt[ch] dmin, dmax = _digi_bounds(self.fmt[ch]) @@ -270,7 +270,8 @@ def check_sig_cohesion(self, write_fields, expanded): + str(fmt) ) - # Ensure that the checksums and initial value fields match the digital signal (if the fields are present) + # Ensure that the checksums and initial value fields match the digital + # signal (if the fields are present) if self.n_sig > 0: if "checksum" in write_fields: realchecksum = self.calc_checksum() @@ -291,9 +292,7 @@ def check_sig_cohesion(self, write_fields, expanded): ) def set_p_features(self, do_dac=False, expanded=False): - """ - Use properties of the physical signal field to set the following - features: n_sig, sig_len. + """Set n_sig and sig_len using properties of the physical signal. Parameters ---------- @@ -353,10 +352,10 @@ def set_p_features(self, do_dac=False, expanded=False): self.n_sig = self.p_signal.shape[1] def set_d_features(self, do_adc=False, single_fmt=True, expanded=False): - """ - Use properties of the digital signal field to set the following - features: n_sig, sig_len, init_value, checksum, and possibly - *(fmt, adc_gain, baseline). + """Set several features using properties of the digital signal field. + + The features are n_sig, sig_len, init_value, checksum, and possibly (fmt, + adc_gain, baseline). Parameters ---------- @@ -415,7 +414,8 @@ def set_d_features(self, do_adc=False, single_fmt=True, expanded=False): self.check_field("fmt", "all") # Neither field set if self.adc_gain is None and self.baseline is None: - # Calculate and set optimal gain and baseline values to convert physical signals + # Calculate and set optimal gain and baseline values to convert + # physical signals self.adc_gain, self.baseline = self.calc_adc_params() # Exactly one field set elif (self.adc_gain is None) ^ (self.baseline is None): @@ -450,7 +450,8 @@ def set_d_features(self, do_adc=False, single_fmt=True, expanded=False): # Choose appropriate fmts based on estimated signal resolutions. res = est_res(self.p_signal) self.fmt = _wfdb_fmt(res, single_fmt) - # Calculate and set optimal gain and baseline values to convert physical signals + # Calculate and set optimal gain and baseline values to convert + # physical signals self.adc_gain, self.baseline = self.calc_adc_params() # If there is a fmt set @@ -458,7 +459,8 @@ def set_d_features(self, do_adc=False, single_fmt=True, expanded=False): self.check_field("fmt", "all") # Neither field set if self.adc_gain is None and self.baseline is None: - # Calculate and set optimal gain and baseline values to convert physical signals + # Calculate and set optimal gain and baseline values to convert + # physical signals self.adc_gain, self.baseline = self.calc_adc_params() # Exactly one field set elif (self.adc_gain is None) ^ (self.baseline is None): @@ -480,9 +482,10 @@ def set_d_features(self, do_adc=False, single_fmt=True, expanded=False): self.checksum = self.calc_checksum() def adc(self, expanded=False, inplace=False): - """ - Performs analogue to digital conversion of the physical signal stored - in p_signal if expanded is False, or e_p_signal if expanded is True. + """Perform analogue to digital conversion of the physical signal. + + The signal is stored in p_signal if expanded is False, or in e_p_signal if + expanded is True. The p_signal/e_p_signal, fmt, gain, and baseline fields must all be valid. @@ -509,8 +512,8 @@ def adc(self, expanded=False, inplace=False): The digital conversion of the signal. Either a 2d numpy array or a list of 1d numpy arrays. - Examples: - --------- + Examples + -------- >>> import wfdb >>> record = wfdb.rdsamp('sample-data/100') >>> d_signal = record.adc() @@ -580,10 +583,10 @@ def adc_inplace(p_signal, adc_gain, baseline, d_nan): ) def dac(self, expanded=False, return_res=64, inplace=False): - """ - Performs the digital to analogue conversion of the signal stored - in `d_signal` if expanded is False, or `e_d_signal` if expanded - is True. + """Perform the digital to analogue conversion of the signal. + + The signal is stored in `d_signal` if expanded is False, or in `e_d_signal` if + expanded is True. The d_signal/e_d_signal, fmt, gain, and baseline fields must all be valid. @@ -690,8 +693,7 @@ def dac(self, expanded=False, return_res=64, inplace=False): return p_signal def calc_adc_gain_baseline(self, ch, minvals, maxvals): - """ - Compute adc_gain and baseline parameters for a given channel. + """Compute adc_gain and baseline parameters for a given channel. Parameters ---------- @@ -791,9 +793,9 @@ def calc_adc_gain_baseline(self, ch, minvals, maxvals): return adc_gain, baseline def calc_adc_params(self): - """ - Compute appropriate adc_gain and baseline parameters for adc - conversion, given the physical signal and the fmts. + """Compute adc_gain and baseline parameters for adc conversion. + + The parameters are computed from the physical signal and the fmts. Parameters ---------- @@ -847,8 +849,7 @@ def calc_adc_params(self): return (adc_gains, baselines) def convert_dtype(self, physical, return_res, smooth_frames): - """ - Convert the dtype of the signal. + """Convert the dtype of the signal. Parameters ---------- @@ -891,7 +892,8 @@ def convert_dtype(self, physical, return_res, smooth_frames): if smooth_frames: current_dtype = self.d_signal.dtype if current_dtype != return_dtype: - # Do not allow changing integer dtype to lower value due to over/underflow + # Do not allow changing integer dtype to lower value due to + # over/underflow if int(str(current_dtype)[3:]) > int(str(return_dtype)[3:]): raise Exception( "Cannot convert digital samples to lower dtype. Risk of overflow/underflow." @@ -901,7 +903,8 @@ def convert_dtype(self, physical, return_res, smooth_frames): for ch in range(self.n_sig): current_dtype = self.e_d_signal[ch].dtype if current_dtype != return_dtype: - # Do not allow changing integer dtype to lower value due to over/underflow + # Do not allow changing integer dtype to lower value due to + # over/underflow if int(str(current_dtype)[3:]) > int(str(return_dtype)[3:]): raise Exception( "Cannot convert digital samples to lower dtype. Risk of overflow/underflow." @@ -912,8 +915,7 @@ def convert_dtype(self, physical, return_res, smooth_frames): return def calc_checksum(self, expanded=False): - """ - Calculate the checksum(s) of the input signal. + """Calculate the checksum(s) of the input signal. Parameters ---------- @@ -935,8 +937,7 @@ def calc_checksum(self, expanded=False): return cs def wr_dat_files(self, expanded=False, write_dir=""): - """ - Write each of the specified dat files. + """Write each of the specified dat files. Parameters ---------- @@ -992,15 +993,13 @@ def wr_dat_files(self, expanded=False, write_dir=""): ) def smooth_frames(self, sigtype="physical"): - """ - Convert expanded signals with different samples/frame into - a uniform numpy array. + """Convert expanded signals with varying samples/frame to a uniform array. Parameters ---------- sigtype (default='physical') : str - Specifies whether to mooth the e_p_signal field ('physical'), or the e_d_signal - field ('digital'). + Specifies whether to smooth the e_p_signal field ('physical'), or the + e_d_signal field ('digital'). Returns ------- @@ -1096,9 +1095,7 @@ def _rd_segment( sig_data=None, return_res=64, ): - """ - Read the digital samples from a single segment record's associated - dat file(s). + """Read the digital samples from a single segment record's associated dat file(s). Parameters ---------- @@ -1197,7 +1194,8 @@ def _rd_segment( w_channel = {} # one list per dat file for fn in file_name: - # intersecting dat channels between the input channels and the channels of the file + # intersecting dat channels between the input channels and the channels of the + # file idc = [c for c in datchannel[fn] if c in channels] # There is at least one wanted channel in the dat file @@ -1212,7 +1210,8 @@ def _rd_segment( # Wanted dat channels, relative to the dat file itself r_w_channel = {} - # The channels in the final output array that correspond to the read channels in each dat file + # The channels in the final output array that correspond to the read channels in + # each dat file out_dat_channel = {} for fn in w_channel: r_w_channel[fn] = [c - min(datchannel[fn]) for c in w_channel[fn]] @@ -1264,8 +1263,7 @@ def _rd_dat_signals( no_file=False, sig_data=None, ): - """ - Read all signals from a WFDB dat file. + """Read all signals from a WFDB dat file. Parameters ---------- @@ -1457,9 +1455,9 @@ def _rd_dat_signals( def _dat_read_params( fmt, sig_len, byte_offset, skew, tsamps_per_frame, sampfrom, sampto ): - """ - Calculate the parameters used to read and process a dat file, given - its layout, and the desired sample range. + """Calculate the parameters to read and process a dat file. + + The parameters depend on the file layout and the desired sample range. Parameters ---------- @@ -1497,10 +1495,14 @@ def _dat_read_params( Examples -------- sig_len=100, t = 4 (total samples/frame), skew = [0, 2, 4, 5] - sampfrom=0, sampto=100 --> read_len = 100, n_sampread = 100*t, extralen = 5, nan_replace = [0, 2, 4, 5] - sampfrom=50, sampto=100 --> read_len = 50, n_sampread = 50*t, extralen = 5, nan_replace = [0, 2, 4, 5] - sampfrom=0, sampto=50 --> read_len = 50, n_sampread = 55*t, extralen = 0, nan_replace = [0, 0, 0, 0] - sampfrom=95, sampto=99 --> read_len = 4, n_sampread = 5*t, extralen = 4, nan_replace = [0, 1, 3, 4] + sampfrom=0, sampto=100 --> read_len = 100, n_sampread = 100*t, extralen = 5, + nan_replace = [0, 2, 4, 5] + sampfrom=50, sampto=100 --> read_len = 50, n_sampread = 50*t, extralen = 5, + nan_replace = [0, 2, 4, 5] + sampfrom=0, sampto=50 --> read_len = 50, n_sampread = 55*t, extralen = 0, + nan_replace = [0, 0, 0, 0] + sampfrom=95, sampto=99 --> read_len = 4, n_sampread = 5*t, extralen = 4, + nan_replace = [0, 1, 3, 4] """ # First flat sample number to read (if all channels were flattened) @@ -1551,9 +1553,9 @@ def _dat_read_params( def _required_byte_num(mode, fmt, n_samp): - """ - Determine how many signal bytes are needed to read or write a - number of desired samples from a dat file. + """Determine how many signal bytes to read or write for some samples. + + This applies to a dat file. Parameters ---------- @@ -1600,9 +1602,7 @@ def _required_byte_num(mode, fmt, n_samp): def _rd_dat_file(file_name, dir_name, pn_dir, fmt, start_byte, n_samp): - """ - Read data from a dat file, either local or remote, into a 1d numpy - array. + """Read data from a dat file, either local or remote, into a 1d numpy array. This is the lowest level dat reading function (along with `_stream_dat` which this function may call), and is called by @@ -1682,8 +1682,7 @@ def _rd_dat_file(file_name, dir_name, pn_dir, fmt, start_byte, n_samp): def _blocks_to_samples(sig_data, n_samp, fmt): - """ - Convert uint8 blocks into signal samples for unaligned dat formats. + """Convert uint8 blocks into signal samples for unaligned dat formats. Parameters ---------- @@ -1808,8 +1807,7 @@ def _rd_compressed_file( start_frame, end_frame, ): - """ - Read data from a compressed file into a 1D numpy array. + """Read data from a compressed file into a 1D numpy array. Parameters ---------- @@ -1971,8 +1969,7 @@ def _rd_compressed_file( def _skew_sig(sig, skew, n_sig, read_len, fmt, nan_replace, samps_per_frame=None): - """ - Skew the signal, insert nans, and shave off end of array if needed. + """Skew the signal, insert nans, and shave off end of array if needed. Parameters ---------- @@ -2038,8 +2035,7 @@ def _skew_sig(sig, skew, n_sig, read_len, fmt, nan_replace, samps_per_frame=None def _check_sig_dims(sig, read_len, n_sig, samps_per_frame): - """ - Integrity check of a signal's shape after reading. + """Integrity check of a signal's shape after reading. Parameters ---------- @@ -2073,8 +2069,7 @@ def _check_sig_dims(sig, read_len, n_sig, samps_per_frame): def _digi_bounds(fmt): - """ - Return min and max digital values for each format type. + """Return min and max digital values for each format type. Parmeters --------- @@ -2093,8 +2088,7 @@ def _digi_bounds(fmt): def _digi_nan(fmt): - """ - Return the WFDB digital value used to store NAN for the format type. + """Return the WFDB digital value used to store NAN for the format type. Parmeters --------- @@ -2113,9 +2107,9 @@ def _digi_nan(fmt): def est_res(signals): - """ - Estimate the resolution of each signal in a multi-channel signal in - bits. Maximum of 32 bits. + """Estimate the resolution of each signal in a multi-channel signal in bits. + + Maximum of 32 bits. Parameters ---------- @@ -2169,9 +2163,7 @@ def est_res(signals): def _wfdb_fmt(bit_res, single_fmt=True): - """ - Return the most suitable WFDB format(s) to use given signal - resolutions. + """Return the most suitable WFDB format(s) to use given signal resolutions. Parameters ---------- @@ -2206,9 +2198,9 @@ def _wfdb_fmt(bit_res, single_fmt=True): def _fmt_res(fmt, max_res=False): - """ - Return the resolution of the WFDB dat format(s). Uses the BIT_RES - dictionary, but accepts lists and other options. + """Return the resolution of the WFDB dat format(s). + + Uses the BIT_RES dictionary, but accepts lists and other options. Parameters ---------- @@ -2237,9 +2229,7 @@ def _fmt_res(fmt, max_res=False): def _np_dtype(bit_res, discrete): - """ - Given the bit resolution of a signal, return the minimum numpy dtype - used to store it. + """Return the minimum numpy dtype that can store a bit resolution. Parameters ---------- @@ -2278,9 +2268,7 @@ def wr_dat_file( samps_per_frame=None, write_dir="", ): - """ - Write a dat file. All bytes are written one at a time to avoid - endianness issues. + """Write a dat file. All bytes are written one at a time to avoid endianness issues. Parameters ---------- @@ -2388,7 +2376,8 @@ def wr_dat_file( # Triplet 3 from lowest 8 bits of sample 2 b_write[2::3] = d_signal[1::2] & 255 - # If we added an extra sample for byte calculation, remove the last byte (don't write) + # If we added an extra sample for byte calculation, remove the last byte (don't + # write) if n_samp % 2: b_write = b_write[:-1] @@ -2507,8 +2496,7 @@ def wr_dat_file( def describe_list_indices(full_list): - """ - Describe the indices of the given list. + """Describe the indices of the given list. Parameters ---------- @@ -2543,8 +2531,7 @@ def describe_list_indices(full_list): def _infer_sig_len( file_name, fmt, tsamps_per_frame, byte_offset, dir_name, pn_dir=None ): - """ - Infer the length of a signal from a dat file. + """Infer the length of a signal from a dat file. Parameters ---------- @@ -2575,12 +2562,14 @@ def _infer_sig_len( """ from wfdb.io.record import CLOUD_PROTOCOLS - # If this is a cloud path, use posixpath to construct the path and fsspec to open file + # If this is a cloud path, use posixpath to construct the path and fsspec to open + # file if any(dir_name.startswith(proto) for proto in CLOUD_PROTOCOLS): with fsspec.open(posixpath.join(dir_name, file_name), mode="rb") as f: file_size = f.seek(0, os.SEEK_END) - # If the PhysioNet database path is provided, construct the download path using the database version + # If the PhysioNet database path is provided, construct the download path using the + # database version elif pn_dir is not None: # check to make sure a cloud path isn't being passed under pn_dir if any(pn_dir.startswith(proto) for proto in CLOUD_PROTOCOLS): diff --git a/wfdb/io/_url.py b/src/wfdb/io/_url.py similarity index 95% rename from wfdb/io/_url.py rename to src/wfdb/io/_url.py index adccbcf5..c0970119 100644 --- a/wfdb/io/_url.py +++ b/src/wfdb/io/_url.py @@ -37,8 +37,7 @@ def _get_session(): - """ - Obtain a session object suitable for requesting remote files. + """Obtain a session object suitable for requesting remote files. Parameters ---------- @@ -103,8 +102,7 @@ class NetFilePermissionError(NetFileError, PermissionError): class RangeTransfer: - """ - A single HTTP transfer representing a range of bytes. + """A single HTTP transfer representing a range of bytes. Parameters ---------- @@ -176,8 +174,7 @@ def __init__(self, url, start, end): raise def _parse_headers(self, method, response): - """ - Parse the headers of the response object. + """Parse the headers of the response object. Parameters ---------- @@ -274,8 +271,7 @@ def _parse_headers(self, method, response): self.close() def close(self): - """ - Finish reading data from the response. + """Finish reading data from the response. Any leftover data in the response body will be discarded and the underlying HTTP connection will be returned to the pool. @@ -314,8 +310,7 @@ def __del__(self): response.close() def iter_chunks(self): - """ - Iterate over the response body as a sequence of chunks. + """Iterate over the response body as a sequence of chunks. Parameters ---------- @@ -338,8 +333,7 @@ def iter_chunks(self): self.file_size = self._current_pos def content(self): - """ - Read the complete response. + """Read the complete response. Parameters ---------- @@ -362,8 +356,7 @@ def content(self): class NetFile(io.BufferedIOBase): - """ - File object providing random access to a remote file over HTTP. + """File object providing random access to a remote file over HTTP. Attributes ---------- @@ -391,8 +384,7 @@ def __init__(self, url, buffering=-1): self._current_url = self.url def _read_buffered_range(self, start, end): - """ - Read a range of bytes from the internal buffer. + """Read a range of bytes from the internal buffer. Parameters ---------- @@ -415,8 +407,7 @@ def _read_buffered_range(self, start, end): return memoryview(b"") def _read_range(self, start, end): - """ - Read a range of bytes from the remote file. + """Read a range of bytes from the remote file. The result is returned as a sequence of chunks; the sizes of the individual chunks are unspecified. The total size may be @@ -513,8 +504,7 @@ def _read_range(self, start, end): self._file_size = xfer.file_size def _get_size(self): - """ - Determine the size of the remote file. + """Determine the size of the remote file. Parameters ---------- @@ -541,8 +531,7 @@ def _get_size(self): return size def readable(self): - """ - Determine whether the file supports read() and read1() operations. + """Determine whether the file supports read() and read1() operations. Parameters ---------- @@ -556,8 +545,7 @@ def readable(self): return True def read(self, size=-1): - """ - Read bytes from the file. + """Read bytes from the file. Parameters ---------- @@ -586,8 +574,7 @@ def read(self, size=-1): return result def read1(self, size=-1): - """ - Read bytes from the file. + """Read bytes from the file. Parameters ---------- @@ -605,8 +592,7 @@ def read1(self, size=-1): return self.read(size) def readinto(self, b): - """ - Read bytes from the file. + """Read bytes from the file. Parameters ---------- @@ -632,8 +618,7 @@ def readinto(self, b): return count def readinto1(self, b): - """ - Read bytes from the file. + """Read bytes from the file. Parameters ---------- @@ -651,8 +636,7 @@ def readinto1(self, b): return self.readinto(b) def seekable(self): - """ - Determine whether the file supports seek() and tell() operations. + """Determine whether the file supports seek() and tell() operations. Parameters ---------- @@ -666,8 +650,7 @@ def seekable(self): return True def seek(self, offset, whence=os.SEEK_SET): - """ - Set the current file position. + """Set the current file position. Parameters ---------- @@ -705,8 +688,7 @@ def seek(self, offset, whence=os.SEEK_SET): return pos def tell(self): - """ - Retrieve the current file position. + """Retrieve the current file position. Parameters ---------- @@ -731,8 +713,7 @@ def openurl( newline=None, check_access=False, ): - """ - Open a URL as a random-access file object. + """Open a URL as a random-access file object. Parameters ---------- diff --git a/wfdb/io/annotation.py b/src/wfdb/io/annotation.py similarity index 93% rename from wfdb/io/annotation.py rename to src/wfdb/io/annotation.py index 74588def..20f6b7b1 100644 --- a/wfdb/io/annotation.py +++ b/src/wfdb/io/annotation.py @@ -13,8 +13,7 @@ class Annotation: - """ - The class representing WFDB annotations. + """The class representing WFDB annotations. Annotation objects can be created using the initializer, or by reading a WFDB annotation file with `rdann`. @@ -27,11 +26,12 @@ class Annotation: text used to label annotations that are not one of these codes should go in the 'aux_note' field rather than the 'sym' field. - The current annotation values organized as such: + The current annotation values organized as such:: - AnnotationLabel(label_store (or subtype), symbol (or aux_note), short_description, description) + AnnotationLabel(label_store (or subtype), symbol (or aux_note), + short_description, description) - where the associated values are: + where the associated values are:: ann_labels = [ AnnotationLabel(0, " ", 'NOTANN', 'Not an actual annotation'), @@ -43,7 +43,9 @@ class Annotation: AnnotationLabel(6, "F", 'FUSION', 'Fusion of ventricular and normal beat'), AnnotationLabel(7, "J", 'NPC', 'Nodal (junctional) premature beat'), AnnotationLabel(8, "A", 'APC', 'Atrial premature contraction'), - AnnotationLabel(9, "S", 'SVPB', 'Premature or ectopic supraventricular beat'), + AnnotationLabel( + 9, "S", 'SVPB', 'Premature or ectopic supraventricular beat' + ), AnnotationLabel(10, "E", 'VESC', 'Ventricular escape beat'), AnnotationLabel(11, "j", 'NESC', 'Nodal (junctional) escape beat'), AnnotationLabel(12, "/", 'PACE', 'Paced beat'), @@ -64,23 +66,31 @@ class Annotation: AnnotationLabel(29, "u", 'UWAVE', 'U-wave peak'), AnnotationLabel(30, "?", 'LEARN', 'Learning'), AnnotationLabel(31, "!", 'FLWAV', 'Ventricular flutter wave'), - AnnotationLabel(32, "[", 'VFON', 'Start of ventricular flutter/fibrillation'), - AnnotationLabel(33, "]", 'VFOFF', 'End of ventricular flutter/fibrillation'), + AnnotationLabel( + 32, "[", 'VFON', 'Start of ventricular flutter/fibrillation' + ), + AnnotationLabel( + 33, "]", 'VFOFF', 'End of ventricular flutter/fibrillation' + ), AnnotationLabel(34, "e", 'AESC', 'Atrial escape beat'), AnnotationLabel(35, "n", 'SVESC', 'Supraventricular escape beat'), - AnnotationLabel(36, "@", 'LINK', 'Link to external data (aux_note contains URL)'), + AnnotationLabel( + 36, "@", 'LINK', 'Link to external data (aux_note contains URL)' + ), AnnotationLabel(37, "x", 'NAPC', 'Non-conducted P-wave (blocked APB)'), AnnotationLabel(38, "f", 'PFUS', 'Fusion of paced and normal beat'), AnnotationLabel(39, "(", 'WFON', 'Waveform onset'), AnnotationLabel(40, ")", 'WFOFF', 'Waveform end'), - AnnotationLabel(41, "r", 'RONT', 'R-on-T premature ventricular contraction') + AnnotationLabel( + 41, "r", 'RONT', 'R-on-T premature ventricular contraction' + ) ] - The current annotation classes are organized as such: + The current annotation classes are organized as such:: AnnotationClass(extension, description, human_reviewed) - where the associated values are: + where the associated values are:: ann_classes = [ AnnotationClass('atr', 'Reference ECG annotations', True), @@ -180,8 +190,7 @@ def __init__( # __label_map__: (storevalue, symbol, description) hidden attribute def __eq__(self, other): - """ - Equal comparison operator for objects of this type. + """Equal comparison operator for objects of this type. Parameters ---------- @@ -225,9 +234,7 @@ def __eq__(self, other): return True def apply_range(self, sampfrom=0, sampto=None): - """ - Filter the annotation attributes to keep only items between the - desired sample values. + """Keep only the annotation items between the desired sample values. Parameters ---------- @@ -251,8 +258,7 @@ def apply_range(self, sampfrom=0, sampto=None): self.ann_len = len(self.sample) def wrann(self, write_fs=False, write_dir=""): - """ - Write a WFDB annotation file from this object. + """Write a WFDB annotation file from this object. Parameters ---------- @@ -310,8 +316,7 @@ def wrann(self, write_fs=False, write_dir=""): return def get_label_fields(self): - """ - Get the present label fields in the object. + """Get the present label fields in the object. Parameters ---------- @@ -331,8 +336,7 @@ def get_label_fields(self): return present_label_fields def check_fields(self): - """ - Check the set fields of the annotation object. + """Check the set fields of the annotation object. Parameters ---------- @@ -351,8 +355,7 @@ def check_fields(self): return def check_field(self, field): - """ - Check a particular annotation field. + """Check a particular annotation field. Parameters ---------- @@ -499,9 +502,14 @@ def check_field(self, field): + " field must be strings" ) - # Would be good to enfore this but existing garbage annotations have tabs and newlines... + # Would be good to enforce this but existing garbage annotations have + # tabs and newlines... # if bool(re.search('[\t\n\r\f\v]', description[i])): - # raise ValueError('The description values of the '+field+' field must not contain tabs or newlines') + # raise ValueError( + # "The description values of the " + # + field + # + " field must not contain tabs or newlines" + # ) # The string fields elif field in ["symbol", "description", "aux_note"]: @@ -581,9 +589,7 @@ def check_field(self, field): return def check_field_cohesion(self, present_label_fields): - """ - Check that the content and structure of different fields are consistent - with one another. + """Check that the content and structure of the fields are consistent. Parameters ---------- @@ -613,9 +619,9 @@ def check_field_cohesion(self, present_label_fields): + "' fields do not match" ) - # Ensure all label fields are defined by the label map. This has to be checked because - # it is possible the user defined (or lack of) custom_labels does not capture all the - # labels present. + # Ensure all label fields are defined by the label map. This has to be checked + # because it is possible the user defined (or lack of) custom_labels does not + # capture all the labels present. for field in present_label_fields: defined_values = self.__label_map__[field].values @@ -636,9 +642,9 @@ def check_field_cohesion(self, present_label_fields): return def standardize_custom_labels(self): - """ - Set the custom_labels field of the object to a standardized format: - 3 column pandas df with ann_label_fields as columns. + """Set the custom_labels field to a standardized format. + + The format is a 3 column pandas df with ann_label_fields as columns. Does nothing if there are no custom labels defined. Does nothing if custom_labels is already a df with all 3 columns. @@ -721,9 +727,7 @@ def standardize_custom_labels(self): return def get_undefined_label_stores(self): - """ - Get the label_store values not defined in the - standard WFDB annotation labels. + """Get the label_store values not defined in the standard WFDB labels. Parameters ---------- @@ -738,9 +742,7 @@ def get_undefined_label_stores(self): return list(set(range(50)) - set(ann_label_table["label_store"])) def get_available_label_stores(self, usefield="tryall"): - """ - Get the label store values that may be used - for writing this annotation. + """Get the label store values that may be used for writing this annotation. Available store values include: - the undefined values in the standard WFDB labels @@ -755,7 +757,8 @@ def get_available_label_stores(self, usefield="tryall"): If 'usefield' is explicitly specified, the function will use that field to figure out available label stores. If 'usefield' is set to 'tryall', the function will choose one of the contained - attributes by checking availability in the order: label_store, symbol, description. + attributes by checking availability in the order: label_store, symbol, + description. Returns ------- @@ -828,10 +831,10 @@ def get_available_label_stores(self, usefield="tryall"): return available_label_stores def get_custom_label_attribute(self, attribute): - """ - Get a list of the custom_labels attribute i.e. label_store, - symbol, or description. The custom_labels variable could be in - a number of formats. + """Get a list of one custom_labels attribute. + + The attribute is label_store, symbol, or description. The custom_labels variable + could be in a number of formats. Parameters ---------- @@ -870,9 +873,9 @@ def get_custom_label_attribute(self, attribute): return a def create_label_map(self, inplace=True): - """ - Creates mapping df based on ann_label_table and self.custom_labels. Table - composed of entire WFDB standard annotation table, overwritten/appended + """Creates mapping df based on ann_label_table and self.custom_labels. + + Table composed of entire WFDB standard annotation table, overwritten/appended with custom_labels if any. Sets __label_map__ attribute, or returns value. Parameters @@ -900,9 +903,7 @@ def create_label_map(self, inplace=True): return label_map def wr_ann_file(self, write_fs, write_dir=""): - """ - Calculate the bytes used to encode an annotation set and - write them to an annotation file. + """Calculate the bytes to encode the annotations and write them to the file. Parameters ---------- @@ -937,7 +938,8 @@ def wr_ann_file(self, write_fs, write_dir=""): os.path.join(write_dir, self.record_name + "." + self.extension), "wb", ) as f: - # Combine all bytes to write: fs (if any), custom annotations (if any), main content, file terminator + # Combine all bytes to write: fs (if any), custom annotations (if any), main + # content, file terminator np.concatenate( ( fs_bytes, @@ -951,8 +953,7 @@ def wr_ann_file(self, write_fs, write_dir=""): return def calc_fs_bytes(self): - """ - Calculate the bytes written to the annotation file for the fs field. + """Calculate the bytes written to the annotation file for the fs field. Parameters ---------- @@ -1016,9 +1017,7 @@ def calc_fs_bytes(self): return np.array(data_bytes).astype("u1") def calc_cl_bytes(self): - """ - Calculate the bytes written to the annotation file for the - custom_labels field. + """Calculate the bytes written for the custom_labels field. Parameters ---------- @@ -1071,7 +1070,8 @@ def calc_cl_bytes(self): 115, ] - # The end wrapper: '0 NOTE length aux_note ## end of definitions' followed by SKIP -1, +1 + # The end wrapper: '0 NOTE length aux_note ## end of definitions' followed by + # SKIP -1, +1 tailbytes = [ 0, 88, @@ -1110,7 +1110,13 @@ def calc_cl_bytes(self): # writecontent = [] # for i in range(len(self.custom_labels)): - # writecontent.append([freenumbers[i],list(custom_labels.keys())[i],list(custom_labels.values())[i]]) + # writecontent.append( + # [ + # freenumbers[i], + # list(custom_labels.keys())[i], + # list(custom_labels.values())[i], + # ] + # ) # custombytes = [customcode2bytes(triplet) for triplet in writecontent] # custombytes = [item for sublist in custombytes for item in sublist] @@ -1118,8 +1124,7 @@ def calc_cl_bytes(self): return np.array(headbytes + custom_bytes + tailbytes).astype("u1") def calc_core_bytes(self): - """ - Convert all used annotation fields into bytes to write. + """Convert all used annotation fields into bytes to write. Parameters ---------- @@ -1185,9 +1190,7 @@ def calc_core_bytes(self): return data_bytes def compact_fields(self): - """ - Compact all of the object's fields so that the output - writing annotation file writes as few bytes as possible. + """Compact the fields to write as few bytes as possible. Parameters ---------- @@ -1233,8 +1236,7 @@ def compact_fields(self): self.aux_note = None def sym_to_aux(self): - """ - Move non-encoded symbol elements into the aux_note field. + """Move non-encoded symbol elements into the aux_note field. Parameters ---------- @@ -1267,10 +1269,10 @@ def sym_to_aux(self): return def get_contained_labels(self, inplace=True): - """ - Get the set of unique labels contained in this annotation. - Returns a pandas dataframe or sets the contained_labels - attribute of the object. Requires the label_store field to be set. + """Get the set of unique labels contained in this annotation. + + Returns a pandas dataframe or sets the contained_labels attribute of the object. + Requires the label_store field to be set. Function will try to use attributes contained in the order: 1. label_store @@ -1317,7 +1319,9 @@ def get_contained_labels(self, inplace=True): for i in custom_labels.index: label_map.loc[i] = custom_labels.loc[i] # This doesn't work... - # label_map.loc[custom_labels.index] = custom_labels.loc[custom_labels.index] + # label_map.loc[custom_labels.index] = custom_labels.loc[ + # custom_labels.index + # ] # Get the labels using one of the features if self.label_store is not None: @@ -1358,8 +1362,7 @@ def get_contained_labels(self, inplace=True): return contained_labels def set_label_elements(self, wanted_label_elements): - """ - Set one or more label elements based on at least one of the others. + """Set one or more label elements based on at least one of the others. Parameters ---------- @@ -1398,8 +1401,7 @@ def set_label_elements(self, wanted_label_elements): return def rm_attributes(self, attributes): - """ - Remove attributes from object. + """Remove attributes from object. Parameters ---------- @@ -1420,10 +1422,9 @@ def rm_attributes(self, attributes): def convert_label_attribute( self, source_field, target_field, inplace=True, overwrite=True ): - """ - Convert one label attribute (label_store, symbol, or description) to - another. Creates mapping df on the fly based on ann_label_table and - self.custom_labels. + """Convert one label attribute (label_store, symbol, or description) to another. + + Creates mapping df on the fly based on ann_label_table and self.custom_labels. Parameters ---------- @@ -1473,8 +1474,7 @@ def convert_label_attribute( def label_triplets_to_df(triplets): - """ - Get a pd dataframe from a tuple triplets used to define annotation labels. + """Get a pd dataframe from a tuple triplets used to define annotation labels. Parameters ---------- @@ -1502,9 +1502,9 @@ def label_triplets_to_df(triplets): def custom_triplet_bytes(custom_triplet): - """ - Convert triplet of [label_store, symbol, description] into bytes - for defining custom labels in the annotation file. + """Convert a [label_store, symbol, description] triplet into bytes. + + The bytes define custom labels in the annotation file. Parameters ---------- @@ -1517,7 +1517,8 @@ def custom_triplet_bytes(custom_triplet): All the bytes converted from the tiplet. """ - # Structure: 0, NOTE, len(aux_note), aux_note, codenumber, space, codesymbol, space, description, (0 null if necessary) + # Structure: 0, NOTE, len(aux_note), aux_note, codenumber, space, codesymbol, space, + # description, (0 null if necessary) # Remember, aux_note string includes 'number(s)'' annbytes = ( [0, 88, len(custom_triplet[2]) + 3 + len(str(custom_triplet[0])), 252] @@ -1535,8 +1536,7 @@ def custom_triplet_bytes(custom_triplet): def isblank(x): - """ - Tests whether the item is blank. + """Tests whether the item is blank. Parameters ---------- @@ -1558,9 +1558,10 @@ def isblank(x): def compact_carry_field(full_field): - """ - Return the compact list version of a list/array of an annotation - field that has previous values carried over (chan or num). + """Return the compact list version of an annotation field (chan or num). + + The field has previous values carried over. + - The first sample is 0 by default. Only set otherwise if necessary. - Only set fields if they are different from their prev field. @@ -1599,8 +1600,7 @@ def compact_carry_field(full_field): def field2bytes(field, value, typecodes): - """ - Convert an annotation field into bytes to write. + """Convert an annotation field into bytes to write. Parameters ---------- @@ -1692,8 +1692,7 @@ def wrann( custom_labels=None, write_dir="", ): - """ - Write a WFDB annotation file. + """Write a WFDB annotation file. Specify at least the following: @@ -1819,8 +1818,7 @@ def wrann( def show_ann_labels(): - """ - Display the standard WFDB annotation label mapping. + """Display the standard WFDB annotation label mapping. Parameters ---------- @@ -1839,8 +1837,7 @@ def show_ann_labels(): def show_ann_classes(): - """ - Display the standard WFDB annotation classes. + """Display the standard WFDB annotation classes. Parameters ---------- @@ -1869,9 +1866,7 @@ def rdann( return_label_elements=["symbol"], summarize_labels=False, ): - """ - Read a WFDB annotation file record_name.extension and return an - Annotation object. + """Read the annotation file record_name.extension as an Annotation. Parameters ---------- @@ -2009,8 +2004,7 @@ def rdann( def check_read_inputs(sampfrom, sampto, return_label_elements): - """ - Check if all the read inputs are valid. + """Check if all the read inputs are valid. Parameters ---------- @@ -2051,8 +2045,7 @@ def check_read_inputs(sampfrom, sampto, return_label_elements): def load_byte_pairs(record_name, extension, pn_dir): - """ - Load the annotation file 1 byte at a time and arrange in pairs. + """Load the annotation file 1 byte at a time and arrange in pairs. Parameters ---------- @@ -2088,8 +2081,7 @@ def load_byte_pairs(record_name, extension, pn_dir): def proc_ann_bytes(filebytes, sampto): - """ - Get regular annotation fields from the annotation bytes. + """Get regular annotation fields from the annotation bytes. Parameters ---------- @@ -2181,8 +2173,7 @@ def proc_ann_bytes(filebytes, sampto): def proc_core_fields(filebytes, bpi): - """ - Get the sample difference and store fields of the current annotation. + """Get the sample difference and store fields of the current annotation. Parameters ---------- @@ -2205,8 +2196,8 @@ def proc_core_fields(filebytes, bpi): """ sample_diff = 0 - # The current byte pair will contain either the actual d_sample + annotation store value, - # or 0 + SKIP. + # The current byte pair will contain either the actual d_sample + annotation store + # value, or 0 + SKIP. while int(filebytes[bpi, 1]) >> 2 == 59: # 4 bytes storing dt skip_diff = ( @@ -2216,7 +2207,8 @@ def proc_core_fields(filebytes, bpi): + (int(filebytes[bpi + 2, 1]) << 8) ) - # Data type is long integer (stored in two's complement). Range -2**31 to 2**31 - 1 + # Data type is long integer (stored in two's complement). + # Range -2**31 to 2**31 - 1 if skip_diff > 2147483647: skip_diff = skip_diff - 4294967296 @@ -2232,9 +2224,9 @@ def proc_core_fields(filebytes, bpi): def proc_extra_field(label_store, filebytes, bpi, subtype, chan, num, aux_note, update): - """ - Process extra fields belonging to the current annotation. Potential - updated fields: subtype, chan, num, aux_note. + """Process extra fields belonging to the current annotation. + + Potential updated fields: subtype, chan, num, aux_note. Parameters ---------- @@ -2319,9 +2311,7 @@ def proc_extra_field(label_store, filebytes, bpi, subtype, chan, num, aux_note, def update_extra_fields(subtype, chan, num, aux_note, update): - """ - Update the field if the current annotation did not - provide a value. + """Update the field if the current annotation did not provide a value. - aux_note and sub are set to default values if missing. - chan and num copy over previous value if missing. @@ -2384,9 +2374,10 @@ def update_extra_fields(subtype, chan, num, aux_note, update): def get_special_inds(sample, label_store, aux_note): - """ - Get the indices of annotations that hold definition information about - the entire annotation file, and other empty annotations to be removed. + """Get the indices of annotations to be removed from the annotation file. + + These are annotations that hold definition information about the entire annotation + file, and other empty annotations. Note: There is no need to deal with SKIP annotations (label_store=59) which were already dealt with in proc_core_fields and hence not @@ -2429,9 +2420,10 @@ def get_special_inds(sample, label_store, aux_note): def interpret_defintion_annotations(potential_definition_inds, aux_note): - """ - Try to extract annotation definition information from annotation notes. + """Try to extract annotation definition information from annotation notes. + Information that may be contained: + - fs - sample=0, label_state=22, aux_note='## time resolution: XXX'. - custom annotation label definitions. @@ -2490,8 +2482,7 @@ def interpret_defintion_annotations(potential_definition_inds, aux_note): def rm_empty_indices(*args): - """ - Remove unwanted list indices. + """Remove unwanted list indices. Parameters ---------- @@ -2516,8 +2507,7 @@ def rm_empty_indices(*args): def lists_to_int_arrays(*args): - """ - Convert lists to numpy int arrays. + """Convert lists to numpy int arrays. Parameters ---------- @@ -2534,8 +2524,7 @@ def lists_to_int_arrays(*args): def rm_last(*args): - """ - Remove the last index from each list. + """Remove the last index from each list. Parameters ---------- @@ -2567,20 +2556,20 @@ def mrgann( record_only=True, verbose=False, ): - """ - This function reads a pair of annotation files (specified by `ann_file1` - and `ann_file2`) for the specified record and writes a third annotation - file (specified by `out_file_name`) for the same record. The header (.hea) - file should be included in the same directory as each annotation file so - that the sampling rate can be read. Typical applications of `mrgann` - include combining annotation files that apply to different signals within - a multi-signal record, and replacing a segment of an annotation file with - annotations from another file. For example, setting 'merge_method' to - 'combine' will simply blindly merge the annotation files for the specified - 'start_ann' and 'end_ann' range while setting 'merge_method' to 'replace1' - will replace the contents of the first file with the second in that - specified range. Setting 'merge_method' to 'replace2' will replace the - contents of the second file with the first in that specified range. + """Merge two annotation files of a record into a third annotation file. + + This function reads a pair of annotation files (specified by `ann_file1` and + `ann_file2`) for the specified record and writes a third annotation file (specified + by `out_file_name`) for the same record. The header (.hea) file should be included + in the same directory as each annotation file so that the sampling rate can be read. + Typical applications of `mrgann` include combining annotation files that apply to + different signals within a multi-signal record, and replacing a segment of an + annotation file with annotations from another file. For example, setting + 'merge_method' to 'combine' will simply blindly merge the annotation files for the + specified 'start_ann' and 'end_ann' range while setting 'merge_method' to 'replace1' + will replace the contents of the first file with the second in that specified range. + Setting 'merge_method' to 'replace2' will replace the contents of the second file + with the first in that specified range. Parameters ---------- @@ -2883,7 +2872,8 @@ def mrgann( def format_ann_from_df(df_in): - """ + """Convert a dataframe of onsets, durations and descriptions to annotations. + Parameters ---------- df_in : Pandas dataframe @@ -2973,8 +2963,7 @@ def format_ann_from_df(df_in): class AnnotationClass: - """ - Describes the annotations. + """Describes the annotations. Attributes ---------- @@ -3026,8 +3015,7 @@ def __init__(self, extension, description, human_reviewed): # Individual annotation labels class AnnotationLabel: - """ - Describes the individual annotation labels. + """Describes the individual annotation labels. Attributes ---------- diff --git a/wfdb/io/convert/__init__.py b/src/wfdb/io/convert/__init__.py similarity index 100% rename from wfdb/io/convert/__init__.py rename to src/wfdb/io/convert/__init__.py diff --git a/wfdb/io/convert/csv.py b/src/wfdb/io/convert/csv.py similarity index 97% rename from wfdb/io/convert/csv.py rename to src/wfdb/io/convert/csv.py index 6f9e78f3..f095b06a 100644 --- a/wfdb/io/convert/csv.py +++ b/src/wfdb/io/convert/csv.py @@ -35,9 +35,9 @@ def csv_to_wfdb( verbose=False, write_dir="", ): - """ - Read a WFDB header file and return either a `Record` object with the - record descriptors as attributes or write a record and header file. + """Read a CSV file and return a `Record` object or write a record. + + A record and header file are written if `write_dir` is set. Parameters ---------- @@ -277,15 +277,16 @@ def csv_to_wfdb( Return just the record, note the use of lists to specify which values should be applied to each signal - >>> csv_record = csv_to_wfdb('sample-data/100.csv', fs=360, units=['mV','mV'], - fmt=['80',212'], adc_gain=[100,200], - baseline=[1024,512], record_only=True) + >>> csv_record = csv_to_wfdb('sample-data/100.csv', fs=360, + ... units=['mV', 'mV'], fmt=['80', '212'], + ... adc_gain=[100, 200], baseline=[1024, 512], + ... record_only=True) Return just the record, note the use of single strings and ints to specify when fields can be applied to all signals >>> csv_record = csv_to_wfdb('sample-data/100.csv', fs=360, units='mV', - fmt=['80','212'], adc_gain=200, baseline=1024, - record_only=True) + ... fmt=['80', '212'], adc_gain=200, baseline=1024, + ... record_only=True) """ # NOTE: No need to write input checks here since the Record class should @@ -478,9 +479,9 @@ def csv2ann( delimiter=",", verbose=False, ): - """ - Read a CSV/TSV/etc. file and return either an `Annotation` object with the - annotation descriptors as attributes or write an annotation file. + """Read a CSV file and return an `Annotation` object or write a file. + + The annotation descriptors are the attributes of the `Annotation` object. Parameters ---------- diff --git a/wfdb/io/convert/edf.py b/src/wfdb/io/convert/edf.py similarity index 84% rename from wfdb/io/convert/edf.py rename to src/wfdb/io/convert/edf.py index 887ca0bf..6fe832dd 100644 --- a/wfdb/io/convert/edf.py +++ b/src/wfdb/io/convert/edf.py @@ -21,8 +21,7 @@ def read_edf( rdedfann_flag=False, encoding="iso8859-1", ): - """ - Read a EDF format file into a WFDB Record. + """Read a EDF format file into a WFDB Record. Many EDF files contain signals at widely varying sampling frequencies. `read_edf` handles these properly, but the default behavior of most WFDB @@ -75,53 +74,58 @@ def read_edf( Notes ----- - The entire file is composed of (seen here: https://www.edfplus.info/specs/edf.html): - - HEADER RECORD (we suggest to also adopt the 12 simple additional EDF+ specs) - 8 ascii : version of this data format (0) - 80 ascii : local patient identification (mind item 3 of the additional EDF+ specs) - 80 ascii : local recording identification (mind item 4 of the additional EDF+ specs) - 8 ascii : startdate of recording (dd.mm.yy) (mind item 2 of the additional EDF+ specs) - 8 ascii : starttime of recording (hh.mm.ss) - 8 ascii : number of bytes in header record - 44 ascii : reserved - 8 ascii : number of data records (-1 if unknown, obey item 10 of the additional EDF+ specs) - 8 ascii : duration of a data record, in seconds - 4 ascii : number of signals (ns) in data record - ns * 16 ascii : ns * label (e.g. EEG Fpz-Cz or Body temp) (mind item 9 of the additional EDF+ specs) - ns * 80 ascii : ns * transducer type (e.g. AgAgCl electrode) - ns * 8 ascii : ns * physical dimension (e.g. uV or degreeC) - ns * 8 ascii : ns * physical minimum (e.g. -500 or 34) - ns * 8 ascii : ns * physical maximum (e.g. 500 or 40) - ns * 8 ascii : ns * digital minimum (e.g. -2048) - ns * 8 ascii : ns * digital maximum (e.g. 2047) - ns * 80 ascii : ns * prefiltering (e.g. HP:0.1Hz LP:75Hz) - ns * 8 ascii : ns * nr of samples in each data record - ns * 32 ascii : ns * reserved - - DATA RECORD - nr of samples[1] * integer : first signal in the data record - nr of samples[2] * integer : second signal - .. - .. - nr of samples[ns] * integer : last signal - - Bytes 0 - 127: descriptive text - Bytes 128 - 131: master tag (data type = matrix) - Bytes 132 - 135: master tag (data size) - Bytes 136 - 151: array flags (4 byte tag with data type, 4 byte - tag with subelement size, 8 bytes of content) - Bytes 152 - 167: array dimension (4 byte tag with data type, 4 - byte tag with subelement size, 8 bytes of content) - Bytes 168 - 183: array name (4 byte tag with data type, 4 byte - tag with subelement size, 8 bytes of content) - Bytes 184 - ...: array content (4 byte tag with data type, 4 byte - tag with subelement size, ... bytes of content) + The entire file is composed of (seen here: https://www.edfplus.info/specs/edf.html):: + + HEADER RECORD (we suggest to also adopt the 12 simple additional EDF+ specs) + 8 ascii : version of this data format (0) + 80 ascii : local patient identification + (mind item 3 of the additional EDF+ specs) + 80 ascii : local recording identification + (mind item 4 of the additional EDF+ specs) + 8 ascii : startdate of recording (dd.mm.yy) + (mind item 2 of the additional EDF+ specs) + 8 ascii : starttime of recording (hh.mm.ss) + 8 ascii : number of bytes in header record + 44 ascii : reserved + 8 ascii : number of data records + (-1 if unknown, obey item 10 of the additional EDF+ specs) + 8 ascii : duration of a data record, in seconds + 4 ascii : number of signals (ns) in data record + ns * 16 ascii : ns * label (e.g. EEG Fpz-Cz or Body temp) + (mind item 9 of the additional EDF+ specs) + ns * 80 ascii : ns * transducer type (e.g. AgAgCl electrode) + ns * 8 ascii : ns * physical dimension (e.g. uV or degreeC) + ns * 8 ascii : ns * physical minimum (e.g. -500 or 34) + ns * 8 ascii : ns * physical maximum (e.g. 500 or 40) + ns * 8 ascii : ns * digital minimum (e.g. -2048) + ns * 8 ascii : ns * digital maximum (e.g. 2047) + ns * 80 ascii : ns * prefiltering (e.g. HP:0.1Hz LP:75Hz) + ns * 8 ascii : ns * nr of samples in each data record + ns * 32 ascii : ns * reserved + + DATA RECORD + nr of samples[1] * integer : first signal in the data record + nr of samples[2] * integer : second signal + .. + .. + nr of samples[ns] * integer : last signal + + Bytes 0 - 127: descriptive text + Bytes 128 - 131: master tag (data type = matrix) + Bytes 132 - 135: master tag (data size) + Bytes 136 - 151: array flags (4 byte tag with data type, 4 byte + tag with subelement size, 8 bytes of content) + Bytes 152 - 167: array dimension (4 byte tag with data type, 4 + byte tag with subelement size, 8 bytes of content) + Bytes 168 - 183: array name (4 byte tag with data type, 4 byte + tag with subelement size, 8 bytes of content) + Bytes 184 - ...: array content (4 byte tag with data type, 4 byte + tag with subelement size, ... bytes of content) Examples -------- >>> record = read_edf('x001_FAROS.edf', - pn_dir='simultaneous-measurements/raw_data') + ... pn_dir='simultaneous-measurements/raw_data') """ if pn_dir is not None: @@ -471,10 +475,11 @@ def wfdb_to_edf( output_filename="", edf_plus=False, ): - """ - These programs convert EDF (European Data Format) files into - WFDB-compatible files (as used in PhysioNet) and vice versa. European - Data Format (EDF) was originally designed for storage of polysomnograms. + """Convert a WFDB record to an EDF file. + + These programs convert EDF (European Data Format) files into WFDB-compatible files + (as used in PhysioNet) and vice versa. European Data Format (EDF) was originally + designed for storage of polysomnograms. Note that WFDB format does not include a standard way to specify the transducer type or the prefiltering specification; these parameters are @@ -512,48 +517,53 @@ def wfdb_to_edf( Notes ----- - The entire file is composed of (seen here: https://www.edfplus.info/specs/edf.html): - - HEADER RECORD (we suggest to also adopt the 12 simple additional EDF+ specs) - 8 ascii : version of this data format (0) - 80 ascii : local patient identification (mind item 3 of the additional EDF+ specs) - 80 ascii : local recording identification (mind item 4 of the additional EDF+ specs) - 8 ascii : startdate of recording (dd.mm.yy) (mind item 2 of the additional EDF+ specs) - 8 ascii : starttime of recording (hh.mm.ss) - 8 ascii : number of bytes in header record - 44 ascii : reserved - 8 ascii : number of data records (-1 if unknown, obey item 10 of the additional EDF+ specs) - 8 ascii : duration of a data record, in seconds - 4 ascii : number of signals (ns) in data record - ns * 16 ascii : ns * label (e.g. EEG Fpz-Cz or Body temp) (mind item 9 of the additional EDF+ specs) - ns * 80 ascii : ns * transducer type (e.g. AgAgCl electrode) - ns * 8 ascii : ns * physical dimension (e.g. uV or degreeC) - ns * 8 ascii : ns * physical minimum (e.g. -500 or 34) - ns * 8 ascii : ns * physical maximum (e.g. 500 or 40) - ns * 8 ascii : ns * digital minimum (e.g. -2048) - ns * 8 ascii : ns * digital maximum (e.g. 2047) - ns * 80 ascii : ns * prefiltering (e.g. HP:0.1Hz LP:75Hz) - ns * 8 ascii : ns * nr of samples in each data record - ns * 32 ascii : ns * reserved - - DATA RECORD - nr of samples[1] * integer : first signal in the data record - nr of samples[2] * integer : second signal - .. - .. - nr of samples[ns] * integer : last signal - - Bytes 0 - 127: descriptive text - Bytes 128 - 131: master tag (data type = matrix) - Bytes 132 - 135: master tag (data size) - Bytes 136 - 151: array flags (4 byte tag with data type, 4 byte - tag with subelement size, 8 bytes of content) - Bytes 152 - 167: array dimension (4 byte tag with data type, 4 - byte tag with subelement size, 8 bytes of content) - Bytes 168 - 183: array name (4 byte tag with data type, 4 byte - tag with subelement size, 8 bytes of content) - Bytes 184 - ...: array content (4 byte tag with data type, 4 byte - tag with subelement size, ... bytes of content) + The entire file is composed of (seen here: https://www.edfplus.info/specs/edf.html):: + + HEADER RECORD (we suggest to also adopt the 12 simple additional EDF+ specs) + 8 ascii : version of this data format (0) + 80 ascii : local patient identification + (mind item 3 of the additional EDF+ specs) + 80 ascii : local recording identification + (mind item 4 of the additional EDF+ specs) + 8 ascii : startdate of recording (dd.mm.yy) + (mind item 2 of the additional EDF+ specs) + 8 ascii : starttime of recording (hh.mm.ss) + 8 ascii : number of bytes in header record + 44 ascii : reserved + 8 ascii : number of data records + (-1 if unknown, obey item 10 of the additional EDF+ specs) + 8 ascii : duration of a data record, in seconds + 4 ascii : number of signals (ns) in data record + ns * 16 ascii : ns * label (e.g. EEG Fpz-Cz or Body temp) + (mind item 9 of the additional EDF+ specs) + ns * 80 ascii : ns * transducer type (e.g. AgAgCl electrode) + ns * 8 ascii : ns * physical dimension (e.g. uV or degreeC) + ns * 8 ascii : ns * physical minimum (e.g. -500 or 34) + ns * 8 ascii : ns * physical maximum (e.g. 500 or 40) + ns * 8 ascii : ns * digital minimum (e.g. -2048) + ns * 8 ascii : ns * digital maximum (e.g. 2047) + ns * 80 ascii : ns * prefiltering (e.g. HP:0.1Hz LP:75Hz) + ns * 8 ascii : ns * nr of samples in each data record + ns * 32 ascii : ns * reserved + + DATA RECORD + nr of samples[1] * integer : first signal in the data record + nr of samples[2] * integer : second signal + .. + .. + nr of samples[ns] * integer : last signal + + Bytes 0 - 127: descriptive text + Bytes 128 - 131: master tag (data type = matrix) + Bytes 132 - 135: master tag (data size) + Bytes 136 - 151: array flags (4 byte tag with data type, 4 byte + tag with subelement size, 8 bytes of content) + Bytes 152 - 167: array dimension (4 byte tag with data type, 4 + byte tag with subelement size, 8 bytes of content) + Bytes 168 - 183: array name (4 byte tag with data type, 4 byte + tag with subelement size, 8 bytes of content) + Bytes 184 - ...: array content (4 byte tag with data type, 4 byte + tag with subelement size, ... bytes of content) Examples -------- @@ -971,10 +981,11 @@ def rdedfann( verbose=False, encoding="iso8859-1", ): - """ - This program returns the annotation information from an EDF+ file - containing annotations (with the signal name given as 'EDF Annotations'). - The information that is returned if `info_only` is set to True is: + """Return the annotation information from an EDF+ file. + + The file contains annotations (with the signal name given as 'EDF Annotations'). The + information that is returned if `info_only` is set to True is:: + { 'onset_time': list of %H:%M:%S.fff strings denoting the annotation onset times, @@ -983,6 +994,7 @@ def rdedfann( 'comment': list of comments (`aux_note`) for the annotations, 'duration': list of floats denoting the duration of the event } + Else, this function will return either the WFDB Annotation format of the information of the file if `record_only` is set to True, or nothing if neither are set to True though a WFDB Annotation file will be created. @@ -1027,31 +1039,37 @@ def rdedfann( Notes ----- The entire file is composed of (seen here: - https://www.edfplus.info/specs/edfplus.html#edfplusannotations): - - HEADER RECORD (we suggest to also adopt the 12 simple additional EDF+ specs) - 8 ascii : version of this data format (0) - 80 ascii : local patient identification (mind item 3 of the additional EDF+ specs) - 80 ascii : local recording identification (mind item 4 of the additional EDF+ specs) - 8 ascii : startdate of recording (dd.mm.yy) (mind item 2 of the additional EDF+ specs) - 8 ascii : starttime of recording (hh.mm.ss) - 8 ascii : number of bytes in header record - 44 ascii : reserved - 8 ascii : number of data records (-1 if unknown, obey item 10 of the additional EDF+ specs) - 8 ascii : duration of a data record, in seconds - 4 ascii : number of signals (ns) in data record - ns * 16 ascii : ns * label (must be 'EDF Annotations') - ns * 80 ascii : ns * transducer type (must be whitespace) - ns * 8 ascii : ns * physical dimension (must be whitespace) - ns * 8 ascii : ns * physical minimum (e.g. -500 or 34, different than physical maximum) - ns * 8 ascii : ns * physical maximum (e.g. 500 or 40, different than physical minimum) - ns * 8 ascii : ns * digital minimum (must be -32768) - ns * 8 ascii : ns * digital maximum (must be 32767) - ns * 80 ascii : ns * prefiltering (must be whitespace) - ns * 8 ascii : ns * nr of samples in each data record - ns * 32 ascii : ns * reserved - - ANNOTATION RECORD + https://www.edfplus.info/specs/edfplus.html#edfplusannotations):: + + HEADER RECORD (we suggest to also adopt the 12 simple additional EDF+ specs) + 8 ascii : version of this data format (0) + 80 ascii : local patient identification + (mind item 3 of the additional EDF+ specs) + 80 ascii : local recording identification + (mind item 4 of the additional EDF+ specs) + 8 ascii : startdate of recording (dd.mm.yy) + (mind item 2 of the additional EDF+ specs) + 8 ascii : starttime of recording (hh.mm.ss) + 8 ascii : number of bytes in header record + 44 ascii : reserved + 8 ascii : number of data records + (-1 if unknown, obey item 10 of the additional EDF+ specs) + 8 ascii : duration of a data record, in seconds + 4 ascii : number of signals (ns) in data record + ns * 16 ascii : ns * label (must be 'EDF Annotations') + ns * 80 ascii : ns * transducer type (must be whitespace) + ns * 8 ascii : ns * physical dimension (must be whitespace) + ns * 8 ascii : ns * physical minimum + (e.g. -500 or 34, different than physical maximum) + ns * 8 ascii : ns * physical maximum + (e.g. 500 or 40, different than physical minimum) + ns * 8 ascii : ns * digital minimum (must be -32768) + ns * 8 ascii : ns * digital maximum (must be 32767) + ns * 80 ascii : ns * prefiltering (must be whitespace) + ns * 8 ascii : ns * nr of samples in each data record + ns * 32 ascii : ns * reserved + + ANNOTATION RECORD Examples -------- diff --git a/wfdb/io/convert/matlab.py b/src/wfdb/io/convert/matlab.py similarity index 89% rename from wfdb/io/convert/matlab.py rename to src/wfdb/io/convert/matlab.py index cd932927..fa24ae32 100644 --- a/wfdb/io/convert/matlab.py +++ b/src/wfdb/io/convert/matlab.py @@ -8,14 +8,14 @@ def wfdb_to_mat(record_name, pn_dir=None, sampfrom=0, sampto=None, channels=None): - """ - This program converts the signals of any PhysioNet record (or one in any - compatible format) into a .mat file that can be read directly using any version - of Matlab, and a short text file containing information about the signals - (names, gains, baselines, units, sampling frequency, and start time/date if - known). If the input record name is REC, the output files are RECm.mat and - RECm.hea. The output files can also be read by any WFDB application as record - RECm. + """Convert a PhysioNet record to a Matlab .mat file. + + This program converts the signals of any PhysioNet record (or one in any compatible + format) into a .mat file that can be read directly using any version of Matlab, and + a short text file containing information about the signals (names, gains, baselines, + units, sampling frequency, and start time/date if known). If the input record name + is REC, the output files are RECm.mat and RECm.hea. The output files can also be + read by any WFDB application as record RECm. This program does not convert annotation files; for that task, 'rdann' is recommended. @@ -70,19 +70,19 @@ def wfdb_to_mat(record_name, pn_dir=None, sampfrom=0, sampto=None, channels=None Notes ----- - The entire file is composed of: - - Bytes 0 - 127: descriptive text - Bytes 128 - 131: master tag (data type = matrix) - Bytes 132 - 135: master tag (data size) - Bytes 136 - 151: array flags (4 byte tag with data type, 4 byte - tag with subelement size, 8 bytes of content) - Bytes 152 - 167: array dimension (4 byte tag with data type, 4 - byte tag with subelement size, 8 bytes of content) - Bytes 168 - 183: array name (4 byte tag with data type, 4 byte - tag with subelement size, 8 bytes of content) - Bytes 184 - ...: array content (4 byte tag with data type, 4 byte - tag with subelement size, ... bytes of content) + The entire file is composed of:: + + Bytes 0 - 127: descriptive text + Bytes 128 - 131: master tag (data type = matrix) + Bytes 132 - 135: master tag (data size) + Bytes 136 - 151: array flags (4 byte tag with data type, 4 byte + tag with subelement size, 8 bytes of content) + Bytes 152 - 167: array dimension (4 byte tag with data type, 4 + byte tag with subelement size, 8 bytes of content) + Bytes 168 - 183: array name (4 byte tag with data type, 4 byte + tag with subelement size, 8 bytes of content) + Bytes 184 - ...: array content (4 byte tag with data type, 4 byte + tag with subelement size, ... bytes of content) Examples -------- diff --git a/wfdb/io/convert/tff.py b/src/wfdb/io/convert/tff.py similarity index 97% rename from wfdb/io/convert/tff.py rename to src/wfdb/io/convert/tff.py index 312d8715..91c5c704 100644 --- a/wfdb/io/convert/tff.py +++ b/src/wfdb/io/convert/tff.py @@ -1,5 +1,4 @@ -""" -Module for reading ME6000 .tff format files. +"""Module for reading ME6000 .tff format files. http://www.biomation.com/kin/me6000.htm @@ -13,8 +12,7 @@ def rdtff(file_name, cut_end=False): - """ - Read values from a tff file. + """Read values from a tff file. Parameters ---------- @@ -62,8 +60,7 @@ def rdtff(file_name, cut_end=False): def _rdheader(fp): - """ - Read header info of the windaq file. + """Read header info of the windaq file. Parameters ---------- @@ -171,8 +168,7 @@ def _rdheader(fp): def _rdsignal(fp, file_size, header_size, n_sig, bit_width, is_signed, cut_end): - """ - Read the signal. + """Read the signal. Parameters ---------- @@ -254,8 +250,7 @@ def _rdsignal(fp, file_size, header_size, n_sig, bit_width, is_signed, cut_end): def _get_sample(fp, chunk, n_sig, dtype, signal, markers, triggers, sample_num): - """ - Get the total number of samples in the signal. + """Get the total number of samples in the signal. Parameters ---------- diff --git a/wfdb/io/convert/wav.py b/src/wfdb/io/convert/wav.py similarity index 83% rename from wfdb/io/convert/wav.py rename to src/wfdb/io/convert/wav.py index a0091dc1..ef3284c7 100644 --- a/wfdb/io/convert/wav.py +++ b/src/wfdb/io/convert/wav.py @@ -17,9 +17,9 @@ def wfdb_to_wav( output_filename="", write_header=False, ): - """ - This program converts a WFDB record into .wav format (format 16, multiplexed - signals, with embedded header information). + """Convert a WFDB record to .wav format. + + The .wav file has format 16, multiplexed signals, and embedded header information. Parameters ---------- @@ -86,8 +86,8 @@ def wfdb_to_wav( with open(output_filename, "wb") as f: # Write the WAV file identifier f.write(struct.pack(">4s", b"RIFF")) - # Write the number of bytes to follow in the file - # (num_samps*frame_length) sample bytes, and 36 more bytes of miscellaneous embedded header + # Write the number of bytes to follow in the file (num_samps*frame_length) + # sample bytes, and 36 more bytes of miscellaneous embedded header f.write(struct.pack("8s", b"WAVEfmt ")) @@ -140,10 +140,11 @@ def wfdb_to_wav( def read_wav(record_name, pn_dir=None, delete_file=True, record_only=False): - """ - Convert .wav (format 16, multiplexed signals, with embedded header - information) formatted files to WFDB format. See here for more details about - the formatting of a .wav file: http://soundfile.sapp.org/doc/WaveFormat/. + """Convert a .wav file to WFDB format. + + The .wav file has format 16, multiplexed signals, and embedded header information. + See here for more details about the formatting of a .wav file: + http://soundfile.sapp.org/doc/WaveFormat/. This process may not work with some .wav files that are encoded using variants of the original .wav format that are not WFDB-compatible. In @@ -175,38 +176,38 @@ def read_wav(record_name, pn_dir=None, delete_file=True, record_only=False): Files that can be processed successfully using `read_wav` always have exactly three chunks (a header chunk, a format chunk, and a data chunk). In .wav files, binary data are always written in little-endian format (least - significant byte first). The format of `read_wav`'s input files is as follows: - - [Header chunk] - Bytes 0 - 3: "RIFF" [4 ASCII characters] - Bytes 4 - 7: L-8 (number of bytes to follow in the file, excluding bytes 0-7) - Bytes 8 - 11: "WAVE" [4 ASCII characters] - - [Format chunk] - Bytes 12 - 15: "fmt " [4 ASCII characters, note trailing space] - Bytes 16 - 19: 16 (format chunk length in bytes, excluding bytes 12-19) - Bytes 20 - 35: format specification, consisting of: - Bytes 20 - 21: 1 (format tag, indicating no compression is used) - Bytes 22 - 23: number of signals (1 - 65535) - Bytes 24 - 27: sampling frequency in Hz (per signal) - Note that the sampling frequency in a .wav file must be an - integer multiple of 1 Hz, a restriction that is not imposed - by MIT (WFDB) format. - Bytes 28 - 31: bytes per second (sampling frequency * frame size in bytes) - Bytes 32 - 33: frame size in bytes - Bytes 34 - 35: bits per sample (ADC resolution in bits) - Note that the actual ADC resolution (e.g., 12) is written in - this field, although each output sample is right-padded to fill - a full (16-bit) word. (.wav format allows for 8, 16, 24, and - 32 bits per sample) - - [Data chunk] - Bytes 36 - 39: "data" [4 ASCII characters] - Bytes 40 - 43: L-44 (number of bytes to follow in the data chunk) - Bytes 44 - L-1: sample data, consisting of: - Bytes 44 - 45: sample 0, channel 0 - Bytes 46 - 47: sample 0, channel 1 - ... etc. (same order as in a multiplexed WFDB signal file) + significant byte first). The format of `read_wav`'s input files is as follows:: + + [Header chunk] + Bytes 0 - 3: "RIFF" [4 ASCII characters] + Bytes 4 - 7: L-8 (number of bytes to follow in the file, excluding bytes 0-7) + Bytes 8 - 11: "WAVE" [4 ASCII characters] + + [Format chunk] + Bytes 12 - 15: "fmt " [4 ASCII characters, note trailing space] + Bytes 16 - 19: 16 (format chunk length in bytes, excluding bytes 12-19) + Bytes 20 - 35: format specification, consisting of: + Bytes 20 - 21: 1 (format tag, indicating no compression is used) + Bytes 22 - 23: number of signals (1 - 65535) + Bytes 24 - 27: sampling frequency in Hz (per signal) + Note that the sampling frequency in a .wav file must be an + integer multiple of 1 Hz, a restriction that is not imposed + by MIT (WFDB) format. + Bytes 28 - 31: bytes per second (sampling frequency * frame size in bytes) + Bytes 32 - 33: frame size in bytes + Bytes 34 - 35: bits per sample (ADC resolution in bits) + Note that the actual ADC resolution (e.g., 12) is written in + this field, although each output sample is right-padded to fill + a full (16-bit) word. (.wav format allows for 8, 16, 24, and + 32 bits per sample) + + [Data chunk] + Bytes 36 - 39: "data" [4 ASCII characters] + Bytes 40 - 43: L-44 (number of bytes to follow in the data chunk) + Bytes 44 - L-1: sample data, consisting of: + Bytes 44 - 45: sample 0, channel 0 + Bytes 46 - 47: sample 0, channel 1 + ... etc. (same order as in a multiplexed WFDB signal file) Examples -------- diff --git a/wfdb/io/datasource.py b/src/wfdb/io/datasource.py similarity index 87% rename from wfdb/io/datasource.py rename to src/wfdb/io/datasource.py index 3ff4f152..5f1c09af 100644 --- a/wfdb/io/datasource.py +++ b/src/wfdb/io/datasource.py @@ -47,18 +47,14 @@ def uri(self, value: str): def show_data_sources(): - """ - Displays all configured data sources - """ + """Displays all configured data sources""" print("Data sources:") for _, ds in _data_sources.items(): print(ds) def add_data_source(ds: DataSource): - """ - Add a data source to the set of configured data sources - """ + """Add a data source to the set of configured data sources""" if ds.name in _data_sources: raise ValueError( f"There is already a configured data source with name: {ds.name}" @@ -68,15 +64,12 @@ def add_data_source(ds: DataSource): def remove_data_source(ds_name: str): - """ - Remove a data source from the set of configured data sources - """ + """Remove a data source from the set of configured data sources""" del _data_sources[ds_name] def reset_data_sources(keep_pn: bool = False): - """ - Reset all configured data sources + """Reset all configured data sources Parameters ---------- diff --git a/wfdb/io/download.py b/src/wfdb/io/download.py similarity index 93% rename from wfdb/io/download.py rename to src/wfdb/io/download.py index 3f5123e4..ce84846f 100644 --- a/wfdb/io/download.py +++ b/src/wfdb/io/download.py @@ -13,8 +13,7 @@ class Config: - """ - General class structure for the PhysioNet database. + """General class structure for the PhysioNet database. Attributes ---------- @@ -31,9 +30,7 @@ class Config: def set_db_index_url(db_index_url=PN_INDEX_URL): - """ - Set the database index url to a custom value, to stream remote - files from another location. + """Set the database index url to a custom location for streaming. Parameters ---------- @@ -50,8 +47,7 @@ def set_db_index_url(db_index_url=PN_INDEX_URL): def _remote_file_size(url=None, file_name=None, pn_dir=None): - """ - Get the remote file size in bytes. + """Get the remote file size in bytes. Parameters ---------- @@ -82,8 +78,7 @@ def _remote_file_size(url=None, file_name=None, pn_dir=None): def _stream_header(file_name: str, pn_dir: str) -> str: - """ - Stream the text of a remote header file. + """Stream the text of a remote header file. Parameters ---------- @@ -111,8 +106,7 @@ def _stream_header(file_name: str, pn_dir: str) -> str: def _stream_dat(file_name, pn_dir, byte_count, start_byte, dtype): - """ - Stream data from a remote dat file into a 1d numpy array. + """Stream data from a remote dat file into a 1d numpy array. Parameters ---------- @@ -148,8 +142,7 @@ def _stream_dat(file_name, pn_dir, byte_count, start_byte, dtype): def _stream_annotation(file_name, pn_dir): - """ - Stream an entire remote annotation file from Physionet. + """Stream an entire remote annotation file from Physionet. Parameters ---------- @@ -178,8 +171,7 @@ def _stream_annotation(file_name, pn_dir): def get_dbs(): - """ - Get a list of all the PhysioNet databases available. + """Get a list of all the PhysioNet databases available. Parameters ---------- @@ -215,8 +207,7 @@ def get_dbs(): def get_version(pn_dir): - """ - Get the version number of the desired project. + """Get the version number of the desired project. Parameters ---------- @@ -243,8 +234,7 @@ def get_version(pn_dir): def get_record_list(db_dir, records="all"): - """ - Get a list of records belonging to a database. + """Get a list of records belonging to a database. Parameters ---------- @@ -289,8 +279,7 @@ def get_record_list(db_dir, records="all"): def get_annotators(db_dir, annotators): - """ - Get a list of annotators belonging to a database. + """Get a list of annotators belonging to a database. Parameters ---------- @@ -350,8 +339,7 @@ def get_annotators(db_dir, annotators): def make_local_dirs(dl_dir, dl_inputs, keep_subdirs): - """ - Make any required local directories to prepare for downloading. + """Make any required local directories to prepare for downloading. Parameters ---------- @@ -385,9 +373,10 @@ def make_local_dirs(dl_dir, dl_inputs, keep_subdirs): def dl_pn_file(inputs): - """ - Download a file from Physionet. The input args are to be unpacked - for the use of multiprocessing map, because python2 doesn't have starmap. + """Download a file from Physionet. + + The input args are to be unpacked for the use of multiprocessing map, because + python2 doesn't have starmap. Parameters ---------- @@ -447,8 +436,7 @@ def dl_pn_file(inputs): def dl_full_file(url, save_file_name): - """ - Download a file. No checks are performed. + """Download a file. No checks are performed. Parameters ---------- @@ -471,8 +459,7 @@ def dl_full_file(url, save_file_name): def dl_files(db, dl_dir, files, keep_subdirs=True, overwrite=False): - """ - Download specified files from a PhysioNet database. + """Download specified files from a PhysioNet database. Parameters ---------- diff --git a/wfdb/io/header.py b/src/wfdb/io/header.py similarity index 91% rename from wfdb/io/header.py rename to src/wfdb/io/header.py index f6ce180e..4cc7cbe6 100644 --- a/wfdb/io/header.py +++ b/src/wfdb/io/header.py @@ -1,5 +1,4 @@ -""" -Module for parsing header files. +"""Module for parsing header files. This module will eventually replace _header.py @@ -14,7 +13,8 @@ class HeaderSyntaxError(ValueError): # Record line pattern. Format: -# RECORD_NAME/NUM_SEG NUM_SIG SAMP_FREQ/COUNT_FREQ(BASE_COUNT_VAL) SAMPS_PER_SIG BASE_TIME BASE_DATE +# RECORD_NAME/NUM_SEG NUM_SIG SAMP_FREQ/COUNT_FREQ(BASE_COUNT_VAL) SAMPS_PER_SIG +# BASE_TIME BASE_DATE rx_record = re.compile( r""" [ \t]* (?P[-\w]+) @@ -31,7 +31,8 @@ class HeaderSyntaxError(ValueError): ) # Signal line pattern. Format: -# FILE_NAME FORMATxSAMP_PER_FRAME:SKEW+BYTE_OFFSET ADC_GAIN(BASELINE)/UNITS ADC_RES ADC_ZERO CHECKSUM BLOCK_SIZE DESCRIPTION +# FILE_NAME FORMATxSAMP_PER_FRAME:SKEW+BYTE_OFFSET ADC_GAIN(BASELINE)/UNITS ADC_RES +# ADC_ZERO CHECKSUM BLOCK_SIZE DESCRIPTION rx_signal = re.compile( r""" [ \t]* (?P~?[-\w]*\.?[\w]*) @@ -63,9 +64,7 @@ class HeaderSyntaxError(ValueError): def wfdb_strptime(time_string: str) -> datetime.time: - """ - Given a time string in an acceptable WFDB format, return - a datetime.time object. + """Given a time string in an acceptable WFDB format, return a datetime.time object. Valid formats: SS, MM:SS, HH:MM:SS, all with and without microsec. @@ -98,8 +97,7 @@ def wfdb_strptime(time_string: str) -> datetime.time: def parse_header_content( header_content: str, ) -> tuple[list[str], list[str]]: - """ - Parse the text of a header file. + """Parse the text of a header file. Parameters ---------- diff --git a/wfdb/io/record.py b/src/wfdb/io/record.py similarity index 96% rename from wfdb/io/record.py rename to src/wfdb/io/record.py index d232bcea..107b0fc9 100644 --- a/wfdb/io/record.py +++ b/src/wfdb/io/record.py @@ -153,8 +153,7 @@ class BaseRecord: - """ - The base WFDB class extended by the Record and MultiRecord classes. + """The base WFDB class extended by the Record and MultiRecord classes. Attributes ---------- @@ -246,10 +245,10 @@ def base_datetime(self, value): raise TypeError(f"invalid base_datetime value: {value!r}") def get_frame_number(self, time_value): - """ - Convert a time value to a frame number. + """Convert a time value to a frame number. A time value may be specified as: + - An integer or floating-point number, representing the number of WFDB frames elapsed from the start of the record. - A `datetime.timedelta` object, representing elapsed time from the @@ -288,10 +287,10 @@ def get_frame_number(self, time_value): raise TypeError(f"invalid time value: {time_value!r}") def get_elapsed_time(self, time_value): - """ - Convert a time value to an elapsed time in seconds. + """Convert a time value to an elapsed time in seconds. A time value may be specified as: + - An integer or floating-point number, representing the number of WFDB frames elapsed from the start of the record. - A `datetime.timedelta` object, representing elapsed time from the @@ -314,10 +313,10 @@ def get_elapsed_time(self, time_value): return datetime.timedelta(seconds=time_value / self.fs) def get_absolute_time(self, time_value): - """ - Convert a time value to an absolute date and time. + """Convert a time value to an absolute date and time. A time value may be specified as: + - An integer or floating-point number, representing the number of WFDB frames elapsed from the start of the record. - A `datetime.timedelta` object, representing elapsed time from the @@ -345,9 +344,9 @@ def get_absolute_time(self, time_value): return time_value + self.base_datetime def check_field(self, field, required_channels="all"): - """ - Check whether a single field is valid in its basic form. Does - not check compatibility with other fields. + """Check whether a single field is valid in its basic form. + + Does not check compatibility with other fields. Parameters ---------- @@ -375,7 +374,8 @@ def check_field(self, field, required_channels="all"): # We should have a list specifying these automatically. - # Whether the item should be a list. Watch out for required_channels for `segments` + # Whether the item should be a list. Watch out for required_channels for + # `segments` expect_list = True if field in LIST_FIELDS else False # Check the type of the field (and of its elements if it should @@ -546,9 +546,7 @@ def check_field(self, field, required_channels="all"): def check_read_inputs( self, sampfrom, sampto, channels, physical, smooth_frames, return_res ): - """ - Ensure that input read parameters (from rdsamp) are valid for - the record. + """Ensure that input read parameters (from rdsamp) are valid for the record. Parameters ---------- @@ -619,8 +617,7 @@ def check_read_inputs( ) def _adjust_datetime(self, sampfrom: int): - """ - Adjust date and time fields to reflect user input if possible. + """Adjust date and time fields to reflect user input if possible. Helper function for the `_arrange_fields` of both Record and MultiRecord objects. @@ -651,8 +648,7 @@ def _adjust_datetime(self, sampfrom: int): class Record(BaseRecord, _header.HeaderMixin, _signal.SignalMixin): - """ - The class representing single segment WFDB records. + """The class representing single segment WFDB records. Record objects can be created using the initializer, by reading a WFDB header with `rdheader`, or a WFDB record (header and associated dat files) @@ -824,8 +820,7 @@ def __init__( # Equal comparison operator for objects of this type def __eq__(self, other, verbose=False): - """ - Equal comparison operator for objects of this type. + """Equal comparison operator for objects of this type. Parameters ---------- @@ -876,9 +871,7 @@ def __eq__(self, other, verbose=False): return True def wrsamp(self, expanded=False, write_dir=""): - """ - Write a WFDB header file and any associated dat files from this - object. + """Write a WFDB header file and any associated dat files from this object. Parameters ---------- @@ -912,9 +905,7 @@ def wrsamp(self, expanded=False, write_dir=""): self.wr_dats(expanded=expanded, write_dir=write_dir) def _arrange_fields(self, channels, sampfrom, smooth_frames): - """ - Arrange/edit object fields to reflect user channel and/or signal - range input. + """Arrange/edit object fields to reflect user channel and/or signal range input. Parameters ---------- @@ -970,8 +961,7 @@ def _arrange_fields(self, channels, sampfrom, smooth_frames): self._adjust_datetime(sampfrom=sampfrom) def to_dataframe(self) -> pd.DataFrame: - """ - Create a dataframe containing the data from this record. + """Create a dataframe containing the data from this record. Returns @@ -1007,8 +997,7 @@ def to_dataframe(self) -> pd.DataFrame: class MultiRecord(BaseRecord, _header.MultiHeaderMixin): - """ - The class representing multi-segment WFDB records. + """The class representing multi-segment WFDB records. MultiRecord objects can be created using the initializer, or by reading a multi-segment WFDB record using 'rdrecord' with the `m2s` (multi to single) @@ -1132,9 +1121,7 @@ def __init__( self.seg_len = [segment.sig_len for segment in segments] def wrsamp(self, expanded=False, write_dir=""): - """ - Write a multi-segment header, along with headers and dat files - for all segments, from this object. + """Write a multi-segment header and the headers and dat files of its segments. Parameters ---------- @@ -1159,9 +1146,7 @@ def wrsamp(self, expanded=False, write_dir=""): seg.wrsamp(expanded=expanded, write_dir=write_dir) def _check_segment_cohesion(self): - """ - Check the cohesion of the segments field with other fields used - to write the record. + """Check the cohesion of the segments field with the other fields. Parameters ---------- @@ -1176,7 +1161,8 @@ def _check_segment_cohesion(self): raise ValueError("Length of segments must match the 'n_seg' field") for seg_num, segment in enumerate(self.segments): - # If segment 0 is a layout specification record, check that its file names are all == '~'' + # If segment 0 is a layout specification record, check that its file names + # are all == '~'' if seg_num == 0 and self.seg_len[0] == 0: for file_name in segment.file_name: if file_name != "~": @@ -1190,7 +1176,8 @@ def _check_segment_cohesion(self): "The 'fs' in each segment must match the overall record's 'fs'" ) - # Check the signal length of the segment against the corresponding seg_len field + # Check the signal length of the segment against the corresponding seg_len + # field if segment.sig_len != self.seg_len[seg_num]: raise ValueError( f"The signal length of segment {seg_num} does not match the corresponding segment length" @@ -1200,9 +1187,9 @@ def _check_segment_cohesion(self): # Already effectively done it when checking sum(seg_len) against sig_len def _required_segments(self, sampfrom, sampto): - """ - Determine the segments and the samples within each segment in a - multi-segment record, that lie within a sample range. + """Determine the segments and samples of a sample range in a record. + + The record is a multi-segment record. Parameters ---------- @@ -1264,9 +1251,9 @@ def _required_segments(self, sampfrom, sampto): return (seg_numbers, readsamps) def _required_channels(self, seg_numbers, channels, dir_name, pn_dir): - """ - Get the channel numbers to be read from each specified segment, - given the channel numbers specified for the entire record. + """Get the channel numbers to be read from each specified segment. + + The channel numbers are specified for the entire record. Parameters ---------- @@ -1322,10 +1309,9 @@ def _required_channels(self, seg_numbers, channels, dir_name, pn_dir): def _arrange_fields( self, seg_numbers, seg_ranges, channels, sampfrom=0, force_channels=True ): - """ - Arrange/edit object fields to reflect user channel and/or - signal range inputs. Updates layout specification header if - necessary. + """Arrange the fields to reflect user channel and/or signal range inputs. + + Updates layout specification header if necessary. Parameters ---------- @@ -1415,11 +1401,11 @@ def _arrange_fields( self._adjust_datetime(sampfrom=sampfrom) def multi_to_single(self, physical, return_res=64, expanded=False): - """ - Create a Record object from the MultiRecord object. All signal - segments will be combined into the new object's `p_signal` or - `d_signal` field. For digital format, the signals must have - the same storage format, baseline, and adc_gain in all segments. + """Create a Record object from the MultiRecord object. + + All signal segments will be combined into the new object's `p_signal` or + `d_signal` field. For digital format, the signals must have the same storage + format, baseline, and adc_gain in all segments. Parameters ---------- @@ -1652,10 +1638,10 @@ def multi_to_single(self, physical, return_res=64, expanded=False): def _check_item_type( item, field_name, allowed_types, expect_list=False, required_channels="all" ): - """ - Check the item's type against a set of allowed types. - Vary the print message regarding whether the item can be None. - Helper to `BaseRecord.check_field`. + """Check the item's type against a set of allowed types. + + Vary the print message regarding whether the item can be None. Helper to + `BaseRecord.check_field`. Parameters ---------- @@ -1713,9 +1699,7 @@ def _check_item_type( def check_np_array(item, field_name, ndim, parent_class, channel_num=None): - """ - Check a numpy array's shape and dtype against required - specifications. + """Check a numpy array's shape and dtype against required specifications. Parameters ---------- @@ -1758,9 +1742,9 @@ def check_np_array(item, field_name, ndim, parent_class, channel_num=None): def rdheader(record_name, pn_dir=None, rd_segments=False): - """ - Read a WFDB header file and return a `Record` or `MultiRecord` - object with the record descriptors as attributes. + """Read a WFDB header file and return a `Record` or `MultiRecord`. + + The record descriptors are the attributes of the returned object. Parameters ---------- @@ -1799,12 +1783,14 @@ def rdheader(record_name, pn_dir=None, rd_segments=False): dir_name, base_record_name = os.path.split(record_name) file_name = f"{base_record_name}.hea" - # If this is a cloud path, use posixpath to construct the path and fsspec to open file + # If this is a cloud path, use posixpath to construct the path and fsspec to open + # file if any(dir_name.startswith(proto) for proto in CLOUD_PROTOCOLS): with fsspec.open(posixpath.join(dir_name, file_name), mode="r") as f: header_content = f.read() - # If the PhysioNet database path is provided, construct the download path using the database version + # If the PhysioNet database path is provided, construct the download path using the + # database version elif pn_dir is not None: # check to make sure a cloud path isn't being passed under pn_dir if any(pn_dir.startswith(proto) for proto in CLOUD_PROTOCOLS): @@ -1909,9 +1895,9 @@ def rdrecord( channel_names=None, warn_empty=False, ): - """ - Read a WFDB record and return the signal and record descriptors as - attributes in a Record or MultiRecord object. + """Read a WFDB record and return a `Record` or `MultiRecord` object. + + The signal and record descriptors are the attributes of the object. Parameters ---------- @@ -2232,9 +2218,9 @@ def rdsamp( warn_empty=False, return_res=64, ): - """ - Read a WFDB record, and return the physical signals and a few important - descriptor fields. + """Read a WFDB record and return the physical signals and key fields. + + The key fields are a few important descriptor fields. Parameters ---------- @@ -2283,10 +2269,11 @@ def rdsamp( fields : dict A dictionary containing several key attributes of the read record: - - fs: The sampling frequency of the record. - - units: The units for each channel. - - sig_name: The signal name for each channel. - - comments: Any comments written in the header. + + - fs: The sampling frequency of the record. + - units: The units for each channel. + - sig_name: The signal name for each channel. + - comments: Any comments written in the header. Notes ----- @@ -2353,9 +2340,7 @@ def rdsamp( def sampfreq(record_name, pn_dir=None): - """ - Read a WFDB header file and return the sampling frequency of - each of the signals in the record. + """Read a WFDB header file and return the sampling frequency of each signal. Parameters ---------- @@ -2405,8 +2390,7 @@ def sampfreq(record_name, pn_dir=None): def signame(record_name, pn_dir=None, sig_nums=[]): - """ - Read a WFDB record file and return the signal names. + """Read a WFDB record file and return the signal names. Parameters ---------- @@ -2465,9 +2449,7 @@ def signame(record_name, pn_dir=None, sig_nums=[]): def wfdbdesc(record_name, pn_dir=None): - """ - Reads specifications for the signals described in the header file for - a record. + """Reads specifications for the signals described in the header file for a record. Parameters ---------- @@ -2591,17 +2573,17 @@ def wfdbdesc(record_name, pn_dir=None): def wfdbtime(record_name, input_times, pn_dir=None): - """ - Use the specified record as a reference for determining the length of a - sample interval and the absolute time represented by sample number 0. This - program accepts one or more time arguments (in WFDB standard time format) - and produces one line on the standard output for each such argument. In - each output line, the corresponding time is written as a sample number (in - the form sNNN), as an elapsed time interval in hours, minutes, and seconds - from the beginning of the record (in the form hh:mm:ss.sss), and as an - absolute time and date (in the form [hh:mm:ss.sss DD/MM/YYYY]). If the - base time for the record is undefined, the absolute time cannot be - calculated, and in this case the elapsed time appears twice instead. + """Convert times to sample numbers, elapsed times and absolute times. + + Use the specified record as a reference for determining the length of a sample + interval and the absolute time represented by sample number 0. This program accepts + one or more time arguments (in WFDB standard time format) and produces one line on + the standard output for each such argument. In each output line, the corresponding + time is written as a sample number (in the form sNNN), as an elapsed time interval + in hours, minutes, and seconds from the beginning of the record (in the form + hh:mm:ss.sss), and as an absolute time and date (in the form [hh:mm:ss.sss + DD/MM/YYYY]). If the base time for the record is undefined, the absolute time cannot + be calculated, and in this case the elapsed time appears twice instead. Parameters ---------- @@ -2659,7 +2641,8 @@ def wfdbtime(record_name, input_times, pn_dir=None): * Note, the final argument results in the same date field as the argument but a different value for the elapsed time. - >>> wfdb.wfdbtime('sample-data/3000003_0003', ['s1','::5','e','19:46:34.981 01/01/0001']) + >>> wfdb.wfdbtime('sample-data/3000003_0003', + ... ['s1', '::5', 'e', '19:46:34.981 01/01/0001']) s1 00:00:00.008 [19:46:25.765000 01/01/0001] s625 00:00:05.000 [19:46:30.757000 01/01/0001] s1028 00:00:08.224 [19:46:33.981000 01/01/0001] @@ -2747,8 +2730,7 @@ def wfdbtime(record_name, input_times, pn_dir=None): def _get_date_from_time(start_time, hours, minutes, seconds, out_time): - """ - Convert a starting time to a date using the elapsed time. + """Convert a starting time to a date using the elapsed time. Parameters ---------- @@ -2794,9 +2776,10 @@ def _get_date_from_time(start_time, hours, minutes, seconds, out_time): def _get_wanted_channels(wanted_sig_names, record_sig_names, pad=False): - """ - Given some wanted signal names, and the signal names contained in a - record, return the indices of the record channels that intersect. + """Return the indices of the record channels that match wanted signals. + + Given some wanted signal names, and the signal names contained in a record, return + the indices of the record channels that intersect. Parameters ---------- @@ -2848,9 +2831,7 @@ def wrsamp( base_datetime=None, write_dir="", ): - """ - Write a single segment WFDB record, creating a WFDB header file and any - associated dat files. + """Write a single segment WFDB record with a header and any dat files. Parameters ---------- @@ -2950,7 +2931,8 @@ def wrsamp( "When passing e_p_signal or e_d_signal, you also need to specify samples per frame for each channel" ) - # If samps_per_frame is provided, check that it aligns as expected with the channels in the signal + # If samps_per_frame is provided, check that it aligns as expected with the channels + # in the signal if samps_per_frame: # Get the number of elements being passed in samps_per_frame samps_per_frame_length = ( @@ -2972,7 +2954,8 @@ def wrsamp( raise TypeError( "Unsupported signal format. Must be ndarray or list of lists." ) - # Check that the number of channels matches the number of samps_per_frame entries + # Check that the number of channels matches the number of samps_per_frame + # entries if num_sig_channels != samps_per_frame_length: raise Exception( "When passing samps_per_frame, it must have the same number of entries as the signal has channels" @@ -3041,10 +3024,10 @@ def dl_database( keep_subdirs=True, overwrite=False, ): - """ - Download WFDB record (and optionally annotation) files from a - PhysioNet database. The database must contain a 'RECORDS' file in - its base directory which lists its WFDB records. + """Download WFDB record (and optionally annotation) files from a PhysioNet database. + + The database must contain a 'RECORDS' file in its base directory which lists its + WFDB records. Parameters ---------- diff --git a/wfdb/io/util.py b/src/wfdb/io/util.py similarity index 83% rename from wfdb/io/util.py rename to src/wfdb/io/util.py index d45cbc49..1fbfb9a4 100644 --- a/wfdb/io/util.py +++ b/src/wfdb/io/util.py @@ -1,6 +1,4 @@ -""" -A module for general utility functions -""" +"""A module for general utility functions""" import io import math @@ -11,8 +9,7 @@ def lines_to_file(file_name: str, write_dir: str, lines: Sequence[str]): - """ - Write each line in a list of strings to a text file. + """Write each line in a list of strings to a text file. Parameters ---------- @@ -34,9 +31,9 @@ def lines_to_file(file_name: str, write_dir: str, lines: Sequence[str]): def is_monotonic(items: Sequence) -> bool: - """ - Determine whether elements in a list are monotonic. ie. unique - elements are clustered together. + """Determine whether the elements in a list are monotonic. + + That is, unique elements are clustered together. ie. [5,5,3,4] is, [5,3,5] is not. @@ -65,8 +62,7 @@ def is_monotonic(items: Sequence) -> bool: def downround(x, base): - """ - Round down to nearest . + """Round down to nearest . Parameters --------- @@ -85,8 +81,7 @@ def downround(x, base): def upround(x, base): - """ - Round up to nearest . + """Round up to nearest . Parameters --------- @@ -108,9 +103,9 @@ def overlapping_ranges( ranges_1: Sequence[tuple[int, int]], ranges_2: Sequence[tuple[int, int]], ) -> list[tuple[int, int]]: - """ - Given two collections of integer ranges, return a list of ranges - in which both input inputs overlap. + """Return the ranges in which two collections of ranges overlap. + + Both collections consist of integer ranges. From: https://stackoverflow.com/q/40367461 @@ -126,9 +121,10 @@ def overlapping_ranges( def fromfile(fileobj, dtype, count=-1): - """ - Read binary data from a file-like object into a NumPy array, using `np.fromfile` when possible. - Falls back to manual reading for file-like objects that are not compatible with np.fromfile. + """Read binary data from a file-like object into a NumPy array. + + Uses `np.fromfile` when possible. Falls back to manual reading for file-like objects + that are not compatible with np.fromfile. Parameters ---------- @@ -140,7 +136,8 @@ def fromfile(fileobj, dtype, count=-1): Number of elements or bytes to read depending on the format: - For most formats, this is the number of elements (e.g., samples) to read. - For formats "212", "310", "311", and "24", this is the number of bytes. - If set to -1 (default), reads until the end of the file and infers size from the stream + If set to -1 (default), reads until the end of the file and infers size from + the stream """ if isinstance(fileobj, io.FileIO) or ( isinstance(fileobj, (io.BufferedReader, io.BufferedRandom)) diff --git a/src/wfdb/plot/__init__.py b/src/wfdb/plot/__init__.py new file mode 100644 index 00000000..de6734c0 --- /dev/null +++ b/src/wfdb/plot/__init__.py @@ -0,0 +1,3 @@ +"""The plot subpackage contains tools for plotting signals and annotations.""" + +from wfdb.plot.plot import plot_all_records, plot_items, plot_wfdb diff --git a/wfdb/plot/plot.py b/src/wfdb/plot/plot.py similarity index 97% rename from wfdb/plot/plot.py rename to src/wfdb/plot/plot.py index e7853e17..3fd6336a 100644 --- a/wfdb/plot/plot.py +++ b/src/wfdb/plot/plot.py @@ -7,8 +7,7 @@ def _expand_channels(signal): - """ - Convert application-specified signal data to a list. + """Convert application-specified signal data to a list. Parameters ---------- @@ -44,8 +43,7 @@ def _expand_channels(signal): def _get_sampling_freq(sampling_freq, n_sig, frame_freq): - """ - Convert application-specified sampling frequency to a list. + """Convert application-specified sampling frequency to a list. Parameters ---------- @@ -78,8 +76,7 @@ def _get_sampling_freq(sampling_freq, n_sig, frame_freq): def _get_ann_freq(ann_freq, n_annot, frame_freq): - """ - Convert application-specified annotation frequency to a list. + """Convert application-specified annotation frequency to a list. Parameters ---------- @@ -132,8 +129,7 @@ def plot_items( sampling_freq=None, ann_freq=None, ): - """ - Subplot individual channels of signals and/or annotations. + """Subplot individual channels of signals and/or annotations. Parameters ---------- @@ -329,8 +325,7 @@ def plot_items( def _get_plot_dims(signal, ann_samp): - """ - Figure out the number of plot channels. + """Figure out the number of plot channels. Parameters ---------- @@ -388,8 +383,7 @@ def _get_plot_dims(signal, ann_samp): def _create_figure(n_subplots, sharex, sharey, figsize): - """ - Create the plot figure and subplot axes. + """Create the plot figure and subplot axes. Parameters ---------- @@ -422,8 +416,7 @@ def _create_figure(n_subplots, sharex, sharey, figsize): def _plot_signal( signal, sig_len, n_sig, fs, time_units, sig_style, axes, sampling_freq=None ): - """ - Plot signal channels. + """Plot signal channels. Parameters ---------- @@ -510,8 +503,8 @@ def _plot_annotation( sampling_freq=None, ann_freq=None, ): - """ - Plot annotations, possibly overlaid on signals. + """Plot annotations, possibly overlaid on signals. + ann_samp, n_annot, ann_sym, signal, n_sig, fs, time_units, ann_style, axes Parameters @@ -615,8 +608,7 @@ def _plot_annotation( def _plot_ecg_grids(ecg_grids, fs, units, time_units, axes, sampling_freq=None): - """ - Add ECG grids to the axes. + """Add ECG grids to the axes. Parameters ---------- @@ -692,8 +684,7 @@ def _plot_ecg_grids(ecg_grids, fs, units, time_units, axes, sampling_freq=None): def _calc_ecg_grids(minsig, maxsig, sig_units, fs, maxt, time_units): - """ - Calculate tick intervals for ECG grids. + """Calculate tick intervals for ECG grids. - 5mm 0.2s major grids, 0.04s minor grids. - 0.5mV major grids, 0.125 minor grids. @@ -771,8 +762,7 @@ def _calc_ecg_grids(minsig, maxsig, sig_units, fs, maxt, time_units): def _label_figure( axes, n_subplots, time_units, sig_name, sig_units, xlabel, ylabel, title ): - """ - Add title, and axes labels. + """Add title, and axes labels. Parameters ---------- @@ -852,8 +842,7 @@ def plot_wfdb( return_fig=False, sharex="auto", ): - """ - Subplot individual channels of a WFDB record and/or annotation. + """Subplot individual channels of a WFDB record and/or annotation. This function implements the base functionality of the `plot_items` function, while allowing direct input of WFDB objects. @@ -989,8 +978,7 @@ def plot_wfdb( def _get_wfdb_plot_items(record, annotation, plot_sym): - """ - Get items to plot from WFDB objects. + """Get items to plot from WFDB objects. Parameters ---------- @@ -1151,9 +1139,10 @@ def _get_wfdb_plot_items(record, annotation, plot_sym): def plot_all_records(directory=""): - """ - Plot all WFDB records in a directory (by finding header files), one at - a time, until the 'enter' key is pressed. + """Plot all WFDB records in a directory, one at a time. + + The records are found by their header files. The plots are shown until the 'enter' + key is pressed. Parameters ---------- diff --git a/wfdb/processing/__init__.py b/src/wfdb/processing/__init__.py similarity index 100% rename from wfdb/processing/__init__.py rename to src/wfdb/processing/__init__.py diff --git a/wfdb/processing/basic.py b/src/wfdb/processing/basic.py similarity index 91% rename from wfdb/processing/basic.py rename to src/wfdb/processing/basic.py index 0390b754..576ccbb3 100644 --- a/wfdb/processing/basic.py +++ b/src/wfdb/processing/basic.py @@ -6,8 +6,7 @@ def resample_ann(ann_sample, fs, fs_target): - """ - Compute the new annotation indices. + """Compute the new annotation indices. Parameters ---------- @@ -29,8 +28,7 @@ def resample_ann(ann_sample, fs, fs_target): def resample_sig(x, fs, fs_target): - """ - Resample a signal to a different frequency. + """Resample a signal to a different frequency. Parameters ---------- @@ -66,8 +64,7 @@ def resample_sig(x, fs, fs_target): def resample_singlechan(x, ann, fs, fs_target): - """ - Resample a single-channel signal with its annotations. + """Resample a single-channel signal with its annotations. Parameters ---------- @@ -107,8 +104,7 @@ def resample_singlechan(x, ann, fs, fs_target): def resample_multichan(xs, ann, fs, fs_target, resamp_ann_chan=0): - """ - Resample multiple channels with their annotations. + """Resample multiple channels with their annotations. Parameters ---------- @@ -156,8 +152,7 @@ def resample_multichan(xs, ann, fs, fs_target, resamp_ann_chan=0): def normalize_bound(sig, lb=0, ub=1): - """ - Normalize a signal between the lower and upper bound. + """Normalize a signal between the lower and upper bound. Parameters ---------- @@ -183,8 +178,7 @@ def normalize_bound(sig, lb=0, ub=1): def smooth(sig, window_size): - """ - Apply a uniform moving average filter to a signal. + """Apply a uniform moving average filter to a signal. Parameters ---------- @@ -204,9 +198,7 @@ def smooth(sig, window_size): def get_filter_gain(b, a, f_gain, fs): - """ - Given filter coefficients, return the gain at a particular - frequency. + """Given filter coefficients, return the gain at a particular frequency. Parameters ---------- @@ -236,8 +228,7 @@ def get_filter_gain(b, a, f_gain, fs): def normalize(X): - """ - Scale input vector to unit norm (vector length). + """Scale input vector to unit norm (vector length). Parameters ---------- diff --git a/wfdb/processing/evaluate.py b/src/wfdb/processing/evaluate.py similarity index 94% rename from wfdb/processing/evaluate.py rename to src/wfdb/processing/evaluate.py index 7d49457c..e65a4257 100644 --- a/wfdb/processing/evaluate.py +++ b/src/wfdb/processing/evaluate.py @@ -8,9 +8,9 @@ class Comparitor: - """ - The class to implement and hold comparisons between two sets of - annotations. See methods `compare`, `print_summary` and `plot`. + """The class to implement and hold comparisons between two sets of annotations. + + See methods `compare`, `print_summary` and `plot`. Attributes ---------- @@ -65,9 +65,7 @@ def __init__(self, ref_sample, test_sample, window_width, signal=None): # TODO: rdann return annotations.where def _calc_stats(self): - """ - Calculate performance statistics after the two sets of annotations - are compared. + """Calculate performance statistics after the annotation sets are compared. Parameters ---------- @@ -124,20 +122,20 @@ def _calc_stats(self): self.positive_predictivity = float(self.tp) / self.n_test def compare(self): - """ - Main comparison function. Note: Make sure to be able to handle - these ref/test scenarios: + """Main comparison function. + + Note: Make sure to be able to handle these ref/test scenarios: Parameters - ------- + ---------- N/A Returns ------- N/A - Example - ------- + Examples + -------- A: o----o---o---o x-------x----x @@ -224,9 +222,9 @@ def compare(self): self._calc_stats() def _get_closest_samp_num(self, ref_samp_num, start_test_samp_num): - """ - Return the closest testing sample number for the given reference - sample number. Limit the search from start_test_samp_num. + """Return the closest test sample number for a reference sample number. + + Limit the search from start_test_samp_num. Parameters ---------- @@ -274,8 +272,7 @@ def _get_closest_samp_num(self, ref_samp_num, start_test_samp_num): return closest_samp_num, smallest_samp_diff def print_summary(self): - """ - Print summary metrics of the annotation comparisons. + """Print summary metrics of the annotation comparisons. Parameters ---------- @@ -304,9 +301,7 @@ def print_summary(self): ) def plot(self, sig_style="", title=None, figsize=None, return_fig=False): - """ - Plot the comparison of two sets of annotations, possibly - overlaid on their original signal. + """Plot the comparison of two annotation sets, possibly overlaid on the signal. Parameters ---------- @@ -399,10 +394,9 @@ def plot(self, sig_style="", title=None, figsize=None, return_fig=False): def compare_annotations(ref_sample, test_sample, window_width, signal=None): - """ - Compare a set of reference annotation locations against a set of - test annotation locations. See the Comparitor class docstring - for more information. + """Compare reference annotation locations against test locations. + + See the Comparitor class docstring for more information. Parameters ---------- @@ -452,8 +446,7 @@ def compare_annotations(ref_sample, test_sample, window_width, signal=None): def benchmark_mitdb(detector, verbose=False, print_results=False): - """ - Benchmark a QRS detector against mitdb's records. + """Benchmark a QRS detector against mitdb's records. Parameters ---------- @@ -522,8 +515,7 @@ def benchmark_mitdb(detector, verbose=False, print_results=False): def benchmark_mitdb_record(rec, detector, verbose): - """ - Benchmark a single mitdb record. + """Benchmark a single mitdb record. Parameters ---------- diff --git a/wfdb/processing/filter.py b/src/wfdb/processing/filter.py similarity index 88% rename from wfdb/processing/filter.py rename to src/wfdb/processing/filter.py index 5b108987..67458fad 100644 --- a/wfdb/processing/filter.py +++ b/src/wfdb/processing/filter.py @@ -19,19 +19,18 @@ def sigavg( stop_time=-1, verbose=False, ): - """ - A common problem in signal processing is to determine the shape of a - recurring waveform in the presence of noise. If the waveform recurs - periodically (for example, once per second) the signal can be divided into - segments of an appropriate length (one second in this example), and the - segments can be averaged to reduce the amplitude of any noise that is - uncorrelated with the signal. Typically, noise is reduced by a factor of - the square root of the number of segments included in the average. For - physiologic signals, the waveforms of interest are usually not strictly - periodic, however. This function averages such waveforms by defining - segments (averaging windows) relative to the locations of waveform - annotations. By default, all QRS (beat) annotations for the specified - annotator are included. + """Average a recurring waveform in a signal to reduce the noise. + + A common problem in signal processing is to determine the shape of a recurring + waveform in the presence of noise. If the waveform recurs periodically (for example, + once per second) the signal can be divided into segments of an appropriate length + (one second in this example), and the segments can be averaged to reduce the + amplitude of any noise that is uncorrelated with the signal. Typically, noise is + reduced by a factor of the square root of the number of segments included in the + average. For physiologic signals, the waveforms of interest are usually not strictly + periodic, however. This function averages such waveforms by defining segments + (averaging windows) relative to the locations of waveform annotations. By default, + all QRS (beat) annotations for the specified annotator are included. Parameters ---------- diff --git a/wfdb/processing/hr.py b/src/wfdb/processing/hr.py similarity index 92% rename from wfdb/processing/hr.py rename to src/wfdb/processing/hr.py index b2ea28e0..76750677 100644 --- a/wfdb/processing/hr.py +++ b/src/wfdb/processing/hr.py @@ -7,8 +7,7 @@ def compute_hr(sig_len, qrs_inds, fs): - """ - Compute instantaneous heart rate from peak indices. + """Compute instantaneous heart rate from peak indices. Parameters ---------- @@ -53,9 +52,7 @@ def calc_rr( qrs_units="samples", rr_units="samples", ): - """ - Compute R-R intervals from QRS indices by extracting the time - differences. + """Compute R-R intervals from QRS indices by extracting the time differences. Parameters ---------- @@ -106,9 +103,9 @@ def calc_rr( def calc_mean_hr(rr, fs=None, min_rr=None, max_rr=None, rr_units="samples"): - """ - Compute mean heart rate in beats per minute, from a set of R-R - intervals. Returns 0 if rr is empty. + """Compute mean heart rate in beats per minute, from a set of R-R intervals. + + Returns 0 if rr is empty. Parameters ---------- @@ -162,8 +159,7 @@ def ann2rr( format=None, as_array=True, ): - """ - Obtain RR interval series from ECG annotation files. + """Obtain RR interval series from ECG annotation files. Parameters ---------- @@ -236,11 +232,11 @@ def ann2rr( def rr2ann(rr_array, record_name, extension, fs=250, as_time=False): - """ - Creates an annotation file from the standard input, which should usually - be a Numpy array of intervals in the format produced by `ann2rr`. (For - exceptions, see the `as_time` parameter below.). An optional second column - may be provided which gives the respective annotation mnemonic. + """Create an annotation file from an array of intervals. + + The intervals should usually be a Numpy array in the format produced by `ann2rr`. + (For exceptions, see the `as_time` parameter below.). An optional second column may + be provided which gives the respective annotation mnemonic. Parameters ---------- diff --git a/wfdb/processing/peaks.py b/src/wfdb/processing/peaks.py similarity index 92% rename from wfdb/processing/peaks.py rename to src/wfdb/processing/peaks.py index 47d7ce48..7c618d91 100644 --- a/wfdb/processing/peaks.py +++ b/src/wfdb/processing/peaks.py @@ -4,8 +4,7 @@ def find_peaks(sig): - r""" - Find hard peaks and soft peaks in a signal, defined as follows: + r"""Find hard peaks and soft peaks in a signal, defined as follows: - Hard peak: a peak that is either /\ or \/. - Soft peak: a peak that is either /-*\ or \-*/. @@ -56,10 +55,10 @@ def find_peaks(sig): def find_local_peaks(sig, radius): - """ - Find all local peaks in a signal. A sample is a local peak if it is - the largest value within the samples on its left and right. - In cases where it shares the max value with nearby samples, the + """Find all local peaks in a signal. + + A sample is a local peak if it is the largest value within the samples on + its left and right. In cases where it shares the max value with nearby samples, the middle sample is classified as the local peak. Parameters @@ -109,8 +108,7 @@ def find_local_peaks(sig, radius): def correct_peaks( sig, peak_inds, search_radius, smooth_window_size, peak_dir="compare" ): - """ - Adjust a set of detected peaks to coincide with local signal maxima. + """Adjust a set of detected peaks to coincide with local signal maxima. Parameters ---------- @@ -197,9 +195,9 @@ def correct_peaks( def shift_peaks(sig, peak_inds, search_radius, peak_up): - """ - Helper function for correct_peaks. Return the shifted peaks to local - maxima or minima within a radius. + """Helper function for correct_peaks. + + Return the shifted peaks to local maxima or minima within a radius. Parameters ---------- diff --git a/wfdb/processing/qrs.py b/src/wfdb/processing/qrs.py similarity index 92% rename from wfdb/processing/qrs.py rename to src/wfdb/processing/qrs.py index da0e3a1a..f81ec65b 100644 --- a/wfdb/processing/qrs.py +++ b/src/wfdb/processing/qrs.py @@ -9,10 +9,10 @@ class XQRS: - """ - The QRS detector class for the XQRS algorithm. The `XQRS.Conf` - class is the configuration class that stores initial parameters - for the detection. The `XQRS.detect` method runs the detection algorithm. + """The QRS detector class for the XQRS algorithm. + + The `XQRS.Conf` class is the configuration class that stores initial parameters for + the detection. The `XQRS.detect` method runs the detection algorithm. The process works as follows: @@ -76,8 +76,7 @@ def __init__(self, sig, fs, conf=None): self._set_conf() class Conf: - """ - Initial signal configuration object for this QRS detector. + """Initial signal configuration object for this QRS detector. Attributes ---------- @@ -138,10 +137,9 @@ def __init__( self.t_inspect_period = t_inspect_period def _set_conf(self): - """ - Set configuration parameters from the Conf object into the detector - object. Time values are converted to samples, and amplitude values - are in mV. + """Set configuration parameters from the Conf object into the detector object. + + Time values are converted to samples, and amplitude values are in mV. Parameters ---------- @@ -167,9 +165,7 @@ def _set_conf(self): self.t_inspect_period = int(self.conf.t_inspect_period * self.fs) def _bandpass(self, fc_low=5, fc_high=20): - """ - Apply a bandpass filter onto the signal, and save the filtered - signal. + """Apply a bandpass filter onto the signal, and save the filtered signal. Parameters ---------- @@ -200,10 +196,10 @@ def _bandpass(self, fc_low=5, fc_high=20): ) def _mwi(self): - """ - Apply moving wave integration (MWI) with a Ricker (Mexican hat) - wavelet onto the filtered signal, and save the square of the - integrated signal. The width of the hat is equal to the QRS width. + """Apply moving wave integration (MWI) with a Ricker (Mexican hat) wavelet. + + The wavelet is applied onto the filtered signal, and the square of the + integrated signal is saved. The width of the hat is equal to the QRS width. After integration, find all local peaks in the MWI signal. Parameters @@ -235,8 +231,10 @@ def _mwi(self): self.n_peaks_i = len(self.peak_inds_i) def _learn_init_params(self, n_calib_beats=8): - """ - Find a number of consecutive beats and use them to initialize: + """Find consecutive beats and use them to initialize the detector. + + The following parameters are initialized: + - recent QRS amplitude - recent noise amplitude - recent R-R interval @@ -365,8 +363,7 @@ def _learn_init_params(self, n_calib_beats=8): def _set_init_params( self, qrs_amp_recent, noise_amp_recent, rr_recent, last_qrs_ind ): - """ - Set initial online parameters. + """Set initial online parameters. Parameters ---------- @@ -399,8 +396,7 @@ def _set_init_params( self.last_qrs_peak_num = None def _set_default_init_params(self): - """ - Set initial running parameters using default values. + """Set initial running parameters using default values. The steady state equation is: `qrs_thr = 0.25*qrs_amp + 0.75*noise_amp` @@ -439,9 +435,10 @@ def _set_default_init_params(self): self.learned_init_params = False def _is_qrs(self, peak_num, backsearch=False): - """ - Check whether a peak is a QRS complex. It is classified as QRS - if it: + """Check whether a peak is a QRS complex. + + It is classified as QRS if it: + - Comes after the refractory period. - Passes QRS threshold. - Is not a T-wave (check it if the peak is close to the previous QRS). @@ -474,9 +471,9 @@ def _is_qrs(self, peak_num, backsearch=False): return False def _update_qrs(self, peak_num, backsearch=False): - """ - Update live QRS parameters. Adjust the recent R-R intervals and - QRS amplitudes, and the QRS threshold. + """Update live QRS parameters. + + Adjust the recent R-R intervals and QRS amplitudes, and the QRS threshold. Parameters ---------- @@ -519,9 +516,10 @@ def _update_qrs(self, peak_num, backsearch=False): return def _is_twave(self, peak_num): - """ - Check whether a segment is a T-wave. Compare the maximum gradient of - the filtered signal segment with that of the previous QRS segment. + """Check whether a segment is a T-wave. + + Compare the maximum gradient of the filtered signal segment with that of the + previous QRS segment. Parameters ---------- @@ -558,8 +556,7 @@ def _is_twave(self, peak_num): return False def _update_noise(self, peak_num): - """ - Update live noise parameters. + """Update live noise parameters. Parameters ---------- @@ -576,8 +573,7 @@ def _update_noise(self, peak_num): return def _require_backsearch(self): - """ - Determine whether a backsearch should be performed on prior peaks. + """Determine whether a backsearch should be performed on prior peaks. Parameters ---------- @@ -602,9 +598,7 @@ def _require_backsearch(self): return False def _backsearch(self): - """ - Inspect previous peaks from the last detected QRS peak (if any), - using a lower threshold. + """Inspect peaks since the last detected QRS (if any) using a lower threshold. Parameters ---------- @@ -623,9 +617,7 @@ def _backsearch(self): # noise. It would have already been updated. def _run_detection(self): - """ - Run the QRS detection after all signals and parameters have been - configured and set. + """Run the QRS detection after the signals and parameters have been set. Parameters ---------- @@ -666,8 +658,7 @@ def _run_detection(self): print("QRS detection complete.") def detect(self, sampfrom=0, sampto="end", learn=True, verbose=True): - """ - Detect QRS locations between two samples. + """Detect QRS locations between two samples. Parameters ---------- @@ -727,9 +718,9 @@ def detect(self, sampfrom=0, sampto="end", learn=True, verbose=True): def xqrs_detect(sig, fs, sampfrom=0, sampto="end", conf=None, learn=True, verbose=True): - """ - Run the 'xqrs' QRS detection algorithm on a signal. See the - docstring of the XQRS class for algorithm details. + """Run the 'xqrs' QRS detection algorithm on a signal. + + See the docstring of the XQRS class for algorithm details. Parameters ---------- @@ -774,8 +765,7 @@ def xqrs_detect(sig, fs, sampfrom=0, sampto="end", conf=None, learn=True, verbos def time_to_sample_number(seconds, frequency): - """ - Convert time to sample number. + """Convert time to sample number. Parameters ---------- @@ -794,8 +784,7 @@ def time_to_sample_number(seconds, frequency): class GQRS: - """ - GQRS detection class. + """GQRS detection class. Attributes ---------- @@ -804,8 +793,7 @@ class GQRS: """ class Conf: - """ - Initial signal configuration object for this QRS detector. + """Initial signal configuration object for this QRS detector. Attributes ---------- @@ -918,8 +906,7 @@ def __init__( self.smt0 = 0 + self.smdt class Peak: - """ - Holds all of the peak information for the QRS object. + """Holds all of the peak information for the QRS object. Attributes ---------- @@ -940,8 +927,7 @@ def __init__(self, peak_time, peak_amp, peak_type): self.prev_peak = None class Annotation: - """ - Holds all of the annotation information for the QRS object. + """Holds all of the annotation information for the QRS object. Attributes ---------- @@ -963,8 +949,7 @@ def __init__(self, ann_time, ann_type, ann_subtype, ann_num): self.num = ann_num def putann(self, annotation): - """ - Add an annotation to the object. + """Add an annotation to the object. Parameters ---------- @@ -979,8 +964,7 @@ def putann(self, annotation): self.annotations.append(copy.deepcopy(annotation)) def detect(self, x, conf, adc_zero): - """ - Run detection. + """Run detection. Parameters ---------- @@ -1053,8 +1037,7 @@ def detect(self, x, conf, adc_zero): return self.annotations def rewind_gqrs(self): - """ - Rewind the gqrs. + """Rewind the gqrs. Parameters ---------- @@ -1079,8 +1062,7 @@ def rewind_gqrs(self): p = p.next_peak def at(self, t): - """ - Determine the value of the sample at the specified time. + """Determine the value of the sample at the specified time. Parameters ---------- @@ -1102,8 +1084,7 @@ def at(self, t): return self.x[t] def smv_at(self, t): - """ - Determine the SMV value of the sample at the specified time. + """Determine the SMV value of the sample at the specified time. Parameters ---------- @@ -1118,8 +1099,7 @@ def smv_at(self, t): return self.smv[t & (self.c._BUFLN - 1)] def smv_put(self, t, v): - """ - Put the SMV value of the sample at the specified time. + """Put the SMV value of the sample at the specified time. Parameters ---------- @@ -1136,8 +1116,7 @@ def smv_put(self, t, v): self.smv[t & (self.c._BUFLN - 1)] = v def qfv_at(self, t): - """ - Determine the QFV value of the sample at the specified time. + """Determine the QFV value of the sample at the specified time. Parameters ---------- @@ -1152,8 +1131,7 @@ def qfv_at(self, t): return self.qfv[t & (self.c._BUFLN - 1)] def qfv_put(self, t, v): - """ - Put the QFV value of the sample at the specified time. + """Put the QFV value of the sample at the specified time. Parameters ---------- @@ -1170,11 +1148,11 @@ def qfv_put(self, t, v): self.qfv[t & (self.c._BUFLN - 1)] = v def sm(self, at_t): - """ - Implements a trapezoidal low pass (smoothing) filter (with a gain - of 4*smdt) applied to input signal sig before the QRS matched - filter qf(). Before attempting to 'rewind' by more than BUFLN-smdt - samples, reset smt and smt0. + """Apply a trapezoidal low pass (smoothing) filter with a gain of 4*smdt. + + The filter is applied to the input signal sig before the QRS matched filter + qf(). Before attempting to 'rewind' by more than BUFLN-smdt samples, reset smt + and smt0. Parameters ---------- @@ -1231,8 +1209,7 @@ def sm(self, at_t): return self.smv_at(at_t) def qf(self): - """ - Evaluate the QRS detector filter for the next sample. + """Evaluate the QRS detector filter for the next sample. Parameters ---------- @@ -1261,8 +1238,7 @@ def qf(self): self.SIG_QRS.append(v0**2) def gqrs(self, from_sample, to_sample): - """ - The GQRS algorithm. + """The GQRS algorithm. Parameters ---------- @@ -1297,8 +1273,7 @@ def gqrs(self, from_sample, to_sample): self.SIG_QRS = [] def add_peak(peak_time, peak_amp, peak_type): - """ - Add a peak. + """Add a peak. Parameters ---------- @@ -1322,19 +1297,17 @@ def add_peak(peak_time, peak_amp, peak_type): p.next_peak.amp = 0 def peaktype(p): - """ - The neighborhood consists of all other peaks within rrmin. - Normally, "most prominent" is equivalent to "largest in - amplitude", but this is not always true. For example, consider - three consecutive peaks a, b, c such that a and b share a - neighborhood, b and c share a neighborhood, but a and c do not; - and suppose that amp(a) > amp(b) > amp(c). In this case, if + """The neighborhood consists of all other peaks within rrmin. + + Normally, "most prominent" is equivalent to "largest in amplitude", but this + is not always true. For example, consider three consecutive peaks a, b, c + such that a and b share a neighborhood, b and c share a neighborhood, but a + and c do not; and suppose that amp(a) > amp(b) > amp(c). In this case, if there are no other peaks, a is the most prominent peak in the (a, b) - neighborhood. Since b is thus identified as a non-prominent peak, - c becomes the most prominent peak in the (b, c) neighborhood. - This is necessary to permit detection of low-amplitude beats that - closely precede or follow beats with large secondary peaks (as, - for example, in R-on-T PVCs). + neighborhood. Since b is thus identified as a non-prominent peak, c becomes + the most prominent peak in the (b, c) neighborhood. This is necessary to + permit detection of low-amplitude beats that closely precede or follow beats + with large secondary peaks (as, for example, in R-on-T PVCs). Parameters ---------- @@ -1382,8 +1355,7 @@ def peaktype(p): return p.type def find_missing(r, p): - """ - Find the missing peaks. + """Find the missing peaks. Parameters ---------- @@ -1580,12 +1552,12 @@ def gqrs_detect( QRSa=750, QRSamin=130, ): - """ - Detect QRS locations in a single channel ecg. Functionally, a direct port - of the GQRS algorithm from the original WFDB package. Accepts either a - physical signal, or a digital signal with known adc_gain and adc_zero. See - the notes below for a summary of the program. This algorithm is not being - developed/supported. + """Detect QRS locations in a single channel ecg. + + Functionally, a direct port of the GQRS algorithm from the original WFDB package. + Accepts either a physical signal, or a digital signal with known adc_gain and + adc_zero. See the notes below for a summary of the program. This algorithm is not + being developed/supported. Parameters ---------- @@ -1663,7 +1635,8 @@ def gqrs_detect( non-qrs peaks as in the main detection phase, without saving the QRS locations. During this phase, running parameters of recent intervals and peak/qrs thresholds are adjusted. - - Run the detection: + - Run the detection:: + if a sample is bigger than its immediate neighbors and larger than the peak detection threshold, it is a peak. if it is further than RRmin from the previous QRS, and is a @@ -1780,8 +1753,7 @@ def gqrs_detect( def ricker(points, a): - """ - Return a Ricker wavelet, also known as the "Mexican hat wavelet". + """Return a Ricker wavelet, also known as the "Mexican hat wavelet". It models the function: diff --git a/wfdb/version.py b/src/wfdb/version.py similarity index 100% rename from wfdb/version.py rename to src/wfdb/version.py diff --git a/tests/conftest.py b/tests/conftest.py index 65c3ccb3..75aca184 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -3,7 +3,7 @@ from wfdb.io._url import NetFileError -# Errors that indicate that PhysioNet is unreachable or too slow (and not a test failure) +# Errors that mean PhysioNet is unreachable or too slow (not that a test failed) NETWORK_ERRORS = ( requests.exceptions.ConnectionError, requests.exceptions.ChunkedEncodingError, diff --git a/tests/io/test_convert.py b/tests/io/test_convert.py index 6c5a042c..f233f4c8 100644 --- a/tests/io/test_convert.py +++ b/tests/io/test_convert.py @@ -8,14 +8,10 @@ class TestEdfToWfdb: - """ - Tests for the io.convert.edf module. - """ + """Tests for the io.convert.edf module.""" def test_edf_uniform(self): - """ - EDF format conversion to MIT for uniform sample rates. - """ + """EDF format conversion to MIT for uniform sample rates.""" # Uniform sample rates record_MIT = rdrecord("sample-data/n16").__dict__ record_EDF = read_edf("sample-data/n16.edf").__dict__ @@ -64,9 +60,7 @@ def test_edf_uniform(self): assert np.array_equal(test_results, target_results) def test_edf_non_uniform(self): - """ - EDF format conversion to MIT for non-uniform sample rates. - """ + """EDF format conversion to MIT for non-uniform sample rates.""" # Non-uniform sample rates record_MIT = rdrecord("sample-data/wave_4").__dict__ record_EDF = read_edf("sample-data/wave_4.edf").__dict__ @@ -116,14 +110,10 @@ def test_edf_non_uniform(self): class TestCsvToWfdb: - """ - Tests for the io.convert.csv module. - """ + """Tests for the io.convert.csv module.""" def test_write_dir(self, tmp_path): - """ - Call the function with the write_dir argument. - """ + """Call the function with the write_dir argument.""" test_dir = str(tmp_path) record_100_csv = "sample-data/100.csv" # Load 100.dat file for comparison to 100.csv file diff --git a/tests/test_annotation.py b/tests/test_annotation.py index 9ee80794..79c3dec8 100644 --- a/tests/test_annotation.py +++ b/tests/test_annotation.py @@ -8,16 +8,15 @@ class TestAnnotation: - """ - Testing read and write of WFDB annotations, including Physionet - streaming. + """Testing read and write of WFDB annotations, including Physionet streaming. Target files created using the original WFDB Software Package version 10.5.24 """ def test_1(self, source, temp_path): - """ + """Read a standard annotation file. + Target file created with: rdann -r sample-data/100 -a atr > ann-1 """ @@ -26,9 +25,10 @@ def test_1(self, source, temp_path): # This is not the fault of the script. The annotation file specifies a # length 3 annotation.aux_note[0] = "(N" - # aux_note field with a null written after '(N' which the script correctly picks up. I am just - # getting rid of the null in this unit test to compare with the regexp output below which has - # no null to detect in the output text file of rdann. + # aux_note field with a null written after '(N' which the script correctly picks + # up. I am just getting rid of the null in this unit test to compare with the + # regexp output below which has no null to detect in the output text file of + # rdann. # Target data from WFDB software package with open("tests/target-output/ann-1") as f: @@ -100,8 +100,7 @@ def test_1(self, source, temp_path): assert annotation.__eq__(write_annotation) def test_2(self, source, temp_path): - """ - Annotation file with many aux_note strings. + """Annotation file with many aux_note strings. Target file created with: rdann -r sample-data/100 -a atr > ann-2 @@ -176,8 +175,7 @@ def test_2(self, source, temp_path): assert annotation.__eq__(write_annotation) def test_3(self, source, temp_path): - """ - Annotation file with custom annotation types + """Annotation file with custom annotation types Target file created with: rdann -r sample-data/1003 -a atr > ann-3 @@ -253,8 +251,7 @@ def test_3(self, source, temp_path): assert annotation.__eq__(write_annotation) def test_4(self, temp_path): - """ - Read and write annotations with large time skips + """Read and write annotations with large time skips Annotation file created by: echo "xxxxxxxxx 10000000000 N 0 0 0" | wrann -r huge -a qrs @@ -268,9 +265,7 @@ def test_4(self, temp_path): assert annotation1 == annotation2 def test_5(self, temp_path): - """ - Write and read annotations with custom labels. - """ + """Write and read annotations with custom labels.""" ann_idx = np.array([1, 1000, 2000, 3000]) ann_chan = np.array([3, 1, 2, 3]) # write custom labels diff --git a/tests/test_multi_record.py b/tests/test_multi_record.py index 6c217820..fc86985c 100644 --- a/tests/test_multi_record.py +++ b/tests/test_multi_record.py @@ -2,9 +2,7 @@ class TestMultiRecordRanges: - """ - Test logic that deduces relevant segments/ranges for given signals. - """ + """Test logic that deduces relevant segments/ranges for given signals.""" def test_contained_ranges_simple_cases(self): record = wfdb.MultiRecord( diff --git a/tests/test_plot.py b/tests/test_plot.py index 97772b8d..22dc5f6f 100644 --- a/tests/test_plot.py +++ b/tests/test_plot.py @@ -6,8 +6,7 @@ def assert_axes_match_signal(axes, signal, t_divisor=1): - """ - Check that axis limits are reasonable for plotting a signal array. + """Check that axis limits are reasonable for plotting a signal array. Parameters ---------- @@ -44,14 +43,10 @@ def assert_axes_match_signal(axes, signal, t_divisor=1): class TestPlotWfdb: - """ - Tests for the wfdb.plot_wfdb function - """ + """Tests for the wfdb.plot_wfdb function""" def test_physical_smooth(self): - """ - Plot a record with physical, single-frequency data - """ + """Plot a record with physical, single-frequency data""" record = wfdb.rdrecord( "sample-data/100", sampto=1000, @@ -76,9 +71,7 @@ def test_physical_smooth(self): assert_axes_match_signal(fig.axes[ch], record.p_signal[:, ch]) def test_digital_smooth(self): - """ - Plot a record with digital, single-frequency data - """ + """Plot a record with digital, single-frequency data""" record = wfdb.rdrecord( "sample-data/drive02", sampto=1000, @@ -95,9 +88,7 @@ def test_digital_smooth(self): assert_axes_match_signal(fig.axes[ch], record.d_signal[:, ch], record.fs) def test_physical_multifrequency(self): - """ - Plot a record with physical, multi-frequency data - """ + """Plot a record with physical, multi-frequency data""" record = wfdb.rdrecord( "sample-data/wave_4", sampto=10, @@ -118,9 +109,7 @@ def test_physical_multifrequency(self): ) def test_digital_multifrequency(self): - """ - Plot a record with digital, multi-frequency data - """ + """Plot a record with digital, multi-frequency data""" record = wfdb.rdrecord( "sample-data/multi-segment/041s/041s", sampto=1000, @@ -142,9 +131,7 @@ def test_digital_multifrequency(self): class TestPlotInternal: - """ - Unit tests for internal wfdb.plot.plot functions - """ + """Unit tests for internal wfdb.plot.plot functions""" def test_get_plot_dims(self): sampfrom = 0 diff --git a/tests/test_processing.py b/tests/test_processing.py index 69f9e61b..1d5a6379 100644 --- a/tests/test_processing.py +++ b/tests/test_processing.py @@ -7,9 +7,7 @@ class TestProcessing: - """ - Test processing functions - """ + """Test processing functions""" def test_resample_single(self): sig, fields = wfdb.rdsamp("sample-data/100") @@ -177,14 +175,10 @@ def test_correct_peaks(self): class TestQrs: - """ - Testing QRS detectors - """ + """Testing QRS detectors""" def test_xqrs(self): - """ - Run XQRS detector on record 100 and compare to reference annotations - """ + """Run XQRS detector on record 100 and compare to reference annotations""" sig, fields = wfdb.rdsamp("sample-data/100", channels=[0]) ann_ref = wfdb.rdann("sample-data/100", "atr") @@ -200,15 +194,10 @@ def test_xqrs(self): class TestXQRSDetect: - """ - Tests for the xqrs_detect convenience wrapper. - """ + """Tests for the xqrs_detect convenience wrapper.""" def test_xqrs_detect_forwards_learn(self): - """ - xqrs_detect should forward its learn argument to XQRS.detect - rather than silently dropping it (issue #473). - """ + """xqrs_detect forwards its learn argument to XQRS.detect (issue #473).""" sig, fields = wfdb.rdsamp("sample-data/100", channels=[0]) def fake_detect(self, *args, **kwargs): diff --git a/tests/test_record.py b/tests/test_record.py index 7eaac7b7..2bb636ae 100644 --- a/tests/test_record.py +++ b/tests/test_record.py @@ -9,9 +9,7 @@ class TestRecord: - """ - Test read and write of single segment WFDB records, including - PhysioNet streaming. + """Test read and write of single segment WFDB records, including streaming. Target files created using the original WFDB Software Package version 10.5.24 @@ -40,8 +38,7 @@ class TestRecord: # ------------------------ 1. Basic Tests ------------------------ # def test_1a(self, source, temp_path): - """ - Format 16, entire signal, digital. + """Format 16, entire signal, digital. Target file created with: rdsamp -r sample-data/test01_00s | cut -f 2- > record-1a @@ -74,9 +71,7 @@ def test_1a(self, source, temp_path): assert record_2.__eq__(record_write) def test_1b(self, source): - """ - Format 16, byte offset, selected duration, selected channels, - physical. + """Format 16, byte offset, selected duration, selected channels, physical. Target file created with: rdsamp -r sample-data/a103l -f 50 -t 160 -s 2 0 -P | cut -f 2- > record-1b @@ -111,9 +106,7 @@ def test_1b(self, source): assert np.array_equal(sig, sig_named) and fields == fields_named def test_1c(self, source, temp_path): - """ - Format 16, byte offset, selected duration, selected channels, - digital, expanded format. + """Format 16, byte offset, selected duration and channels, digital, expanded. Target file created with: rdsamp -r sample-data/a103l -f 80 -s 0 1 | cut -f 2- > record-1c @@ -157,8 +150,7 @@ def test_1c(self, source, temp_path): assert record.__eq__(record_write) def test_1d(self, source): - """ - Format 80, selected duration, selected channels, physical + """Format 80, selected duration, selected channels, physical Target file created with: rdsamp -r sample-data/3000003_0003 -f 1 -t 8 -s 1 -P | cut -f 2- > record-1d @@ -185,8 +177,7 @@ def test_1d(self, source): assert np.array_equal(sig, sig_pn) and fields == fields_pn def test_1e(self, source, temp_path): - """ - Format 24, entire signal, digital. + """Format 24, entire signal, digital. Target file created with: rdsamp -r sample-data/n8_evoked_raw_95_F1_R9 | cut -f 2- | @@ -217,8 +208,7 @@ def test_1e(self, source, temp_path): assert record_2.__eq__(record_write) def test_1f(self, temp_path): - """ - All binary formats, multiple signal files in one record. + """All binary formats, multiple signal files in one record. Target file created with: rdsamp -r sample-data/binformats | cut -f 2- | @@ -250,7 +240,7 @@ def test_1f(self, temp_path): # Test writing all supported formats. (Currently not all signal # formats are supported for output; keep this list in sync with - # 'wr_dat_file' in wfdb/io/_signal.py.) + # 'wr_dat_file' in src/wfdb/io/_signal.py.) OUTPUT_FMTS = ["80", "212", "16", "24", "32"] channels = [] for i, fmt in enumerate(record.fmt): @@ -270,8 +260,7 @@ def test_1f(self, temp_path): assert partial_record == converted_record def test_read_write_flac(self, temp_path): - """ - All FLAC formats, multiple signal files in one record. + """All FLAC formats, multiple signal files in one record. Target file created with: rdsamp -r sample-data/flacformats | cut -f 2- | @@ -309,9 +298,7 @@ def test_read_write_flac(self, temp_path): assert record == record_write def test_read_write_flac_multifrequency(self, temp_path): - """ - Format 516 with multiple signal files and variable samples per frame. - """ + """Format 516 with multiple signal files and variable samples per frame.""" # Check that we can read a record and write it out again record = wfdb.rdrecord( "sample-data/mixedsignals", @@ -329,9 +316,10 @@ def test_read_write_flac_multifrequency(self, temp_path): assert record == record_write def test_unique_samps_per_frame_e_p_signal(self, temp_path): - """ - Test writing an e_p_signal with wfdb.io.wrsamp where the signals have different samples per frame. All other - parameters which overlap between a Record object and wfdb.io.wrsamp are also checked. + """Test writing an e_p_signal with wfdb.io.wrsamp. + + The signals have different samples per frame. All other parameters which overlap + between a Record object and wfdb.io.wrsamp are also checked. """ # Read in a record with different samples per frame record = wfdb.rdrecord( @@ -407,9 +395,10 @@ def test_unique_samps_per_frame_e_p_signal(self, temp_path): assert record_filtered == record_write_filtered def test_unique_samps_per_frame_e_d_signal(self, temp_path): - """ - Test writing an e_d_signal with wfdb.io.wrsamp where the signals have different samples per frame. All other - parameters which overlap between a Record object and wfdb.io.wrsamp are also checked. + """Test writing an e_d_signal with wfdb.io.wrsamp. + + The signals have different samples per frame. All other parameters which overlap + between a Record object and wfdb.io.wrsamp are also checked. """ # Read in a record with different samples per frame record = wfdb.rdrecord( @@ -489,9 +478,7 @@ def test_unique_samps_per_frame_e_d_signal(self, temp_path): assert record_filtered == record_write_filtered def test_read_write_flac_many_channels(self, temp_path): - """ - Check we can read and write to format 516 with more than 8 channels. - """ + """Check we can read and write to format 516 with more than 8 channels.""" # Read in a signal with 12 channels in format 16 record = wfdb.rdrecord("sample-data/s0010_re", physical=False) @@ -516,8 +503,7 @@ def test_read_write_flac_many_channels(self, temp_path): assert (record.d_signal == record_fmt516.d_signal).all() def test_read_flac_longduration(self): - """ - Three signals multiplexed in a FLAC file, over 2**24 samples. + """Three signals multiplexed in a FLAC file, over 2**24 samples. Input file created with: yes 25 50 75 | head -5600000 | @@ -540,8 +526,7 @@ def test_read_flac_longduration(self): # ------------------ 2. Special format records ------------------- # def test_2a(self, source): - """ - Format 212, entire signal, physical. + """Format 212, entire signal, physical. Target file created with: rdsamp -r sample-data/100 -P | cut -f 2- > record-2a @@ -562,8 +547,7 @@ def test_2a(self, source): assert np.array_equal(sig, sig_pn) and fields == fields_pn def test_2b(self, source, temp_path): - """ - Format 212, selected duration, selected channel, digital. + """Format 212, selected duration, selected channel, digital. Target file created with: rdsamp -r sample-data/100 -f 0.002 -t 30 -s 1 | cut -f 2- > record-2b @@ -617,8 +601,7 @@ def test_2b(self, source, temp_path): assert record.__eq__(record_write) def test_2c(self, temp_path): - """ - Format 212, entire signal, physical, odd sampled record. + """Format 212, entire signal, physical, odd sampled record. Target file created with: rdsamp -r sample-data/100_3chan -P | cut -f 2- > record-2c @@ -637,10 +620,11 @@ def test_2c(self, temp_path): assert record.__eq__(record_write) def test_2d(self): - """ - Format 310, selected duration, digital + """Format 310, selected duration, digital. + Target file created with: - rdsamp -r sample-data/3000003_0003 -f 0 -t 8.21 | cut -f 2- | wrsamp -o 310derive -O 310 + rdsamp -r sample-data/3000003_0003 -f 0 -t 8.21 | cut -f 2- + | wrsamp -o 310derive -O 310 rdsamp -r 310derive -f 0.007 | cut -f 2- > record-2d """ record = wfdb.rdrecord("sample-data/310derive", sampfrom=2, physical=False) @@ -649,11 +633,11 @@ def test_2d(self): assert np.array_equal(sig, sig_target) def test_2e(self): - """ - Format 311, selected duration, physical. + """Format 311, selected duration, physical. Target file created with: - rdsamp -r sample-data/3000003_0003 -f 0 -t 8.21 -s 1 | cut -f 2- | wrsamp -o 311derive -O 311 + rdsamp -r sample-data/3000003_0003 -f 0 -t 8.21 -s 1 | cut -f 2- + | wrsamp -o 311derive -O 311 rdsamp -r 311derive -f 0.005 -t 3.91 -P | cut -f 2- > record-2e """ sig, fields = wfdb.rdsamp("sample-data/311derive", sampfrom=1, sampto=978) @@ -665,8 +649,8 @@ def test_2e(self): # --------------------- 3. Multi-dat records --------------------- # def test_3a(self, source, temp_path): - """ - Multi-dat, entire signal, digital + """Multi-dat, entire signal, digital. + Target file created with: rdsamp -r sample-data/s0010_re | cut -f 2- > record-3a """ @@ -693,11 +677,11 @@ def test_3a(self, source, temp_path): assert record.__eq__(record_write) def test_3b(self, source): - """ - Multi-dat, selected duration, selected channels, physical. + """Multi-dat, selected duration, selected channels, physical. Target file created with: - rdsamp -r sample-data/s0010_re -f 5 -t 38 -P -s 13 0 4 8 3 | cut -f 2- > record-3b + rdsamp -r sample-data/s0010_re -f 5 -t 38 -P -s 13 0 4 8 3 | cut -f 2- + > record-3b """ sig, fields = wfdb.rdsamp( "sample-data/s0010_re", @@ -725,8 +709,7 @@ def test_3b(self, source): # -------------- 4. Skew and multiple samples/frame -------------- # def test_4a(self, temp_path): - """ - Format 16, multi-samples per frame, skew, digital. + """Format 16, multi-samples per frame, skew, digital. Target file created with: rdsamp -r sample-data/test01_00s_skewframe | cut -f 2- > record-4a @@ -759,8 +742,7 @@ def test_4a(self, temp_path): assert record.__eq__(record_write) def test_4b(self, source, temp_path): - """ - Format 12, multi-samples per frame, skew, entire signal, digital. + """Format 12, multi-samples per frame, skew, entire signal, digital. Target file created with: rdsamp -r sample-data/03700181 | cut -f 2- > record-4b @@ -799,9 +781,7 @@ def test_4b(self, source, temp_path): assert record.__eq__(record_write) def test_4c(self, source, temp_path): - """ - Format 12, multi-samples per frame, skew, selected suration, - selected channels, physical. + """Format 12, multi-samples/frame, skew, selected duration/channels, physical. Target file created with: rdsamp -r sample-data/03700181 -f 8 -t 128 -s 0 2 -P | cut -f 2- > record-4c @@ -845,8 +825,7 @@ def test_4c(self, source, temp_path): assert np.array_equal(sig, writesig) and fields == writefields def test_4d(self): - """ - Format 16, multi-samples per frame, skew, read expanded signals + """Format 16, multi-samples per frame, skew, read expanded signals Target file created with: rdsamp -r sample-data/test01_00s_skewframe -P -H | cut -f 2- > record-4d @@ -865,9 +844,7 @@ def test_4d(self): assert np.array_equal(sig_round, sig_target) def test_write_smoothed(self, temp_path): - """ - Test writing a record after reading with smooth_frames - """ + """Test writing a record after reading with smooth_frames""" record = wfdb.rdrecord( "sample-data/drive02", physical=False, @@ -893,9 +870,7 @@ def test_to_dataframe(self): assert np.array_equal(record.p_signal, df.values) def test_header_with_non_utf8(self): - """ - Ignores non-utf8 characters in the header part. - """ + """Ignores non-utf8 characters in the header part.""" record = wfdb.rdrecord("sample-data/test_generator_2") sig_units_target = [ "uV", @@ -915,9 +890,7 @@ def test_header_with_non_utf8(self): class TestMultiRecord: - """ - Test read and write of multi segment WFDB records, including - PhysioNet streaming. + """Test read and write of multi segment WFDB records, including PhysioNet streaming. Target files created using the original WFDB Software Package version 10.5.24 @@ -925,11 +898,11 @@ class TestMultiRecord: """ def test_multi_fixed_a(self): - """ - Multi-segment, fixed layout, read entire signal. + """Multi-segment, fixed layout, read entire signal. Target file created with: - rdsamp -r sample-data/multi-segment/fixed1/v102s -P | cut -f 2- > record-multi-fixed-a + rdsamp -r sample-data/multi-segment/fixed1/v102s -P | cut -f 2- + > record-multi-fixed-a """ record = wfdb.rdrecord("sample-data/multi-segment/fixed1/v102s") sig_round = np.round(record.p_signal, decimals=8) @@ -938,12 +911,11 @@ def test_multi_fixed_a(self): np.testing.assert_equal(sig_round, sig_target) def test_multi_fixed_b(self): - """ - Multi-segment, fixed layout, selected duration, samples read - from one segment. + """Multi-segment, fixed layout, selected duration, samples from one segment. Target file created with: - rdsamp -r sample-data/multi-segment/fixed1/v102s -t s75000 -P | cut -f 2- > record-multi-fixed-b + rdsamp -r sample-data/multi-segment/fixed1/v102s -t s75000 -P | cut -f 2- + > record-multi-fixed-b """ record = wfdb.rdrecord("sample-data/multi-segment/fixed1/v102s", sampto=75000) sig_round = np.round(record.p_signal, decimals=8) @@ -952,12 +924,11 @@ def test_multi_fixed_b(self): np.testing.assert_equal(sig_round, sig_target) def test_multi_fixed_c(self): - """ - Multi-segment, fixed layout, selected duration and channels, - samples read from multiple segments + """Multi-segment, fixed layout, selected duration and channels, many segments. Target file created with: - rdsamp -r sample-data/multi-segment/fixed1/v102s -f s70000 -t s80000 -s 1 0 3 -P | cut -f 2- > record-multi-fixed-c + rdsamp -r sample-data/multi-segment/fixed1/v102s -f s70000 -t s80000 + -s 1 0 3 -P | cut -f 2- > record-multi-fixed-c """ record = wfdb.rdrecord( "sample-data/multi-segment/fixed1/v102s", @@ -980,8 +951,7 @@ def test_multi_fixed_c(self): assert record.__eq__(record_named) def test_multi_fixed_d(self): - """ - Multi-segment, fixed layout, multi-frequency, selected channels + """Multi-segment, fixed layout, multi-frequency, selected channels Target file created with: rdsamp -r sample-data/multi-segment/041s/ -s 3 2 1 -H | @@ -1015,12 +985,11 @@ def test_multi_fixed_d(self): assert record == record_named def test_multi_variable_a(self): - """ - Multi-segment, variable layout, selected duration, samples read - from one segment only. + """Multi-segment, variable layout, selected duration, samples from one segment. Target file created with: - rdsamp -r sample-data/multi-segment/s00001/s00001-2896-10-10-00-31 -f s14428365 -t s14428375 -P | cut -f 2- > record-multi-variable-a + rdsamp -r sample-data/multi-segment/s00001/s00001-2896-10-10-00-31 + -f s14428365 -t s14428375 -P | cut -f 2- > record-multi-variable-a """ record = wfdb.rdrecord( "sample-data/multi-segment/s00001/s00001-2896-10-10-00-31", @@ -1033,12 +1002,11 @@ def test_multi_variable_a(self): np.testing.assert_equal(sig_round, sig_target) def test_multi_variable_b(self): - """ - Multi-segment, variable layout, selected duration, samples read - from several segments. + """Multi-segment, variable layout, selected duration, several segments. Target file created with: - rdsamp -r sample-data/multi-segment/s00001/s00001-2896-10-10-00-31 -f s14428364 -t s14428375 -P | cut -f 2- > record-multi-variable-b + rdsamp -r sample-data/multi-segment/s00001/s00001-2896-10-10-00-31 + -f s14428364 -t s14428375 -P | cut -f 2- > record-multi-variable-b """ record = wfdb.rdrecord( "sample-data/multi-segment/s00001/s00001-2896-10-10-00-31", @@ -1051,8 +1019,7 @@ def test_multi_variable_b(self): np.testing.assert_equal(sig_round, sig_target) def test_multi_variable_c(self): - """ - Multi-segment, variable layout, entire signal, physical, expanded + """Multi-segment, variable layout, entire signal, physical, expanded The reference signal creation cannot be made with rdsamp directly because the WFDB c package (10.5.24) applies the single @@ -1064,7 +1031,8 @@ def test_multi_variable_c(self): ``` for i in {01..18} do - rdsamp -r sample-data/multi-segment/s25047/3234460_00$i -P | cut -f 2- >> record-multi-variable-c + rdsamp -r sample-data/multi-segment/s25047/3234460_00$i -P | cut -f 2- + >> record-multi-variable-c done ``` @@ -1103,13 +1071,15 @@ def test_multi_variable_c(self): np.testing.assert_equal(sig_round, sig_target) def test_multi_variable_d(self, source): - """ - Multi-segment, variable layout, selected duration, selected - channels, digital. There are two channels: PLETH, and II. Their - fmt, adc_gain, and baseline do not change between the segments. + """Multi-segment, variable layout, selected duration and channels, digital. + + There are two channels: PLETH, and II. Their fmt, adc_gain, and baseline do not + change between the segments. Target file created with: - rdsamp -r sample-data/multi-segment/p000878/p000878-2137-10-26-16-57 -f s3550 -t s7500 -s 0 1 | cut -f 2- | perl -p -e 's/-32768/ -128/g;' > record-multi-variable-d + rdsamp -r sample-data/multi-segment/p000878/p000878-2137-10-26-16-57 + -f s3550 -t s7500 -s 0 1 | cut -f 2- | perl -p -e 's/-32768/ -128/g;' + > record-multi-variable-d """ record = wfdb.rdrecord( @@ -1149,13 +1119,10 @@ def test_multi_variable_d(self, source): class TestTimeConversion: - """ - Test cases for time conversion - """ + """Test cases for time conversion""" def test_single(self): - """ - Time conversion for a single-segment record + """Time conversion for a single-segment record This checks the get_frame_number, get_elapsed_time, and get_absolute_time methods for a Record object. The example record @@ -1185,8 +1152,7 @@ def test_single(self): header.get_absolute_time(t) def test_multisegment_with_date(self): - """ - Time conversion for a multi-segment record with base date + """Time conversion for a multi-segment record with base date This checks the get_frame_number, get_elapsed_time, and get_absolute_time methods for a MultiRecord object. The example @@ -1214,14 +1180,11 @@ def test_multisegment_with_date(self): class TestSignal: - """ - For lower level signal tests - - """ + """For lower level signal tests""" def test_infer_sig_len(self): - """ - Infer the signal length of a record without the sig_len header + """Infer the signal length of a record without the sig_len header. + Read two headers. The records should be the same. """ @@ -1319,11 +1282,12 @@ def test_physical_conversion(self, temp_path): ) def test_adc_gain_max_boundary(self, temp_path): - """ - Exercise the MAX_I32 clamp path: use a tiny range around a huge negative signal - so the computed baseline would exceed MAX_I32. Verify we hit that branch and - that a write/read round-trip preserves the physical signal within expected - quantization and the formula produces the expected result. + """Exercise the MAX_I32 clamp path. + + Use a tiny range around a huge negative signal so the computed baseline would + exceed MAX_I32. Verify we hit that branch and that a write/read round-trip + preserves the physical signal within expected quantization and the formula + produces the expected result. """ # Tiny range around a large negative value forces baseline > MAX_I32 base_value = -1e10 @@ -1369,11 +1333,12 @@ def test_adc_gain_max_boundary(self, temp_path): np.testing.assert_allclose(record.adc_gain[0], expected_gain, rtol=1e-3) def test_adc_gain_min_boundary(self, temp_path): - """ - Exercise the MIN_I32 clamp path: use a tiny range around a huge positive signal - so the computed baseline would drop below MIN_I32. Verify we hit that branch and - that a write/read round-trip preserves the physical signal within expected - quantization and the formula produces the expected result.. + """Exercise the MIN_I32 clamp path. + + Use a tiny range around a huge positive signal so the computed baseline would + drop below MIN_I32. Verify we hit that branch and that a write/read round-trip + preserves the physical signal within expected quantization and the formula + produces the expected result. """ # Tiny range around a large positive value forces baseline < MIN_I32 base_value = 1e10 diff --git a/tests/test_url.py b/tests/test_url.py index 680bf6a9..5fda1cc1 100644 --- a/tests/test_url.py +++ b/tests/test_url.py @@ -10,13 +10,10 @@ class TestNetFiles: - """ - Test accessing remote files. - """ + """Test accessing remote files.""" def test_timeout(self, monkeypatch): - """ - Test that a server that does not respond causes a timeout error. + """Test that a server that does not respond causes a timeout error. Without a timeout, the request would wait for the server forever. """ @@ -27,8 +24,7 @@ def test_timeout(self, monkeypatch): f.read() def test_requests(self): - """ - Test reading a remote file using various APIs. + """Test reading a remote file using various APIs. This tests that we can create a file object using wfdb.io._url.openurl(), and tests that the object implements @@ -74,8 +70,7 @@ def test_requests(self): self._test_binary(url, binary_data, buffering) def _test_text(self, url, content, buffering): - """ - Test reading a URL using text-mode file APIs. + """Test reading a URL using text-mode file APIs. Parameters ---------- @@ -122,8 +117,7 @@ def _test_text(self, url, content, buffering): assert result == content def _test_binary(self, url, content, buffering): - """ - Test reading a URL using binary-mode file APIs. + """Test reading a URL using binary-mode file APIs. Parameters ---------- @@ -212,13 +206,10 @@ def _test_binary(self, url, content, buffering): class TestRemoteFLACFiles: - """ - Test reading FLAC files over HTTP. - """ + """Test reading FLAC files over HTTP.""" def test_whole_file(self): - """ - Test reading a complete FLAC file using local and HTTP APIs. + """Test reading a complete FLAC file using local and HTTP APIs. This tests that we can read the file 'sample-data/flacformats.d2' (a 24-bit FLAC stream) using the soundfile library, first by @@ -268,8 +259,7 @@ def test_whole_file(self): class DummyHTTPServer(http.server.HTTPServer): - """ - HTTPServer used to simulate a web server for testing. + """HTTPServer used to simulate a web server for testing. The server may be used as a context manager (using "with"); during execution of the "with" block, a background thread runs that @@ -315,8 +305,7 @@ def __init__( self.delay = delay def url(self, path="/"): - """ - Generate a URL that points to a file on this server. + """Generate a URL that points to a file on this server. Parameters ---------- @@ -348,9 +337,7 @@ def __exit__(self, exc_type, exc_val, exc_tb): class DummyHTTPRequestHandler(http.server.BaseHTTPRequestHandler): - """ - HTTPRequestHandler used to simulate a web server for testing. - """ + """HTTPRequestHandler used to simulate a web server for testing.""" def do_HEAD(self): self.send_head() diff --git a/wfdb/plot/__init__.py b/wfdb/plot/__init__.py deleted file mode 100644 index fb78fb36..00000000 --- a/wfdb/plot/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -""" -The plot subpackage contains tools for plotting signals and annotations. -""" - -from wfdb.plot.plot import plot_all_records, plot_items, plot_wfdb