From 124fd7a288822745f944a3cdbb60e401d12a13d9 Mon Sep 17 00:00:00 2001 From: Clemens Brunner Date: Tue, 27 Oct 2026 16:48:22 +0100 Subject: [PATCH 1/6] Fix all Sphinx documentation warnings The docs build reported 94 warnings. Fix them and make the docs workflow fail on warnings (-W --keep-going) so they do not come back. - Mark repeated automodule directives and the classes that are documented both on the wfdb and io pages as :no-index:, so each object is indexed once (duplicate object descriptions). This option requires Sphinx 7.2, so raise the pinned Sphinx version of the docs group from 7.0.0 to 7.2.6. - Do not link the numpydoc method tables to autosummary stubs, which do not exist (numpydoc_class_members_toctree = False). - Fix too short title underlines. - Fix reStructuredText in docstrings: blank lines before lists, literal blocks for the EDF/MATLAB/WAV format descriptions, label and class tables, the rdedfann dictionary and the gqrs_detect pseudo-code, and "..." continuation lines in doctest examples (which also fixes a typo in a csv_to_wfdb example). - Rename the "Examples:" and "Example" docstring sections to the section name numpydoc knows ("Examples"). No code is changed. --- .github/workflows/docs.yml | 2 +- docs/conf.py | 4 + docs/io.rst | 7 +- docs/processing.rst | 5 ++ docs/wfdb.rst | 5 +- pyproject.toml | 2 +- wfdb/io/_signal.py | 4 +- wfdb/io/annotation.py | 8 +- wfdb/io/convert/csv.py | 11 +-- wfdb/io/convert/edf.py | 174 ++++++++++++++++++------------------ wfdb/io/convert/matlab.py | 26 +++--- wfdb/io/convert/wav.py | 64 ++++++------- wfdb/io/record.py | 12 ++- wfdb/processing/evaluate.py | 6 +- wfdb/processing/qrs.py | 3 +- 15 files changed, 179 insertions(+), 154 deletions(-) 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/docs/conf.py b/docs/conf.py index cdda06d7..0e9af116 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -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..f20459cc 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] diff --git a/wfdb/io/_signal.py b/wfdb/io/_signal.py index 19c22092..dfbeb394 100644 --- a/wfdb/io/_signal.py +++ b/wfdb/io/_signal.py @@ -509,8 +509,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() diff --git a/wfdb/io/annotation.py b/wfdb/io/annotation.py index 74588def..253295ac 100644 --- a/wfdb/io/annotation.py +++ b/wfdb/io/annotation.py @@ -27,11 +27,11 @@ 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) - where the associated values are: + where the associated values are:: ann_labels = [ AnnotationLabel(0, " ", 'NOTANN', 'Not an actual annotation'), @@ -76,11 +76,11 @@ class Annotation: 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), diff --git a/wfdb/io/convert/csv.py b/wfdb/io/convert/csv.py index 6f9e78f3..b41b6aba 100644 --- a/wfdb/io/convert/csv.py +++ b/wfdb/io/convert/csv.py @@ -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 diff --git a/wfdb/io/convert/edf.py b/wfdb/io/convert/edf.py index 887ca0bf..6d5b465c 100644 --- a/wfdb/io/convert/edf.py +++ b/wfdb/io/convert/edf.py @@ -75,53 +75,53 @@ 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: @@ -512,48 +512,48 @@ 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 -------- @@ -974,7 +974,8 @@ def rdedfann( """ 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: + 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 +984,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. diff --git a/wfdb/io/convert/matlab.py b/wfdb/io/convert/matlab.py index cd932927..d3a011a2 100644 --- a/wfdb/io/convert/matlab.py +++ b/wfdb/io/convert/matlab.py @@ -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/wav.py b/wfdb/io/convert/wav.py index a0091dc1..6f5819aa 100644 --- a/wfdb/io/convert/wav.py +++ b/wfdb/io/convert/wav.py @@ -175,38 +175,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/record.py b/wfdb/io/record.py index d232bcea..bd9ce612 100644 --- a/wfdb/io/record.py +++ b/wfdb/io/record.py @@ -250,6 +250,7 @@ def get_frame_number(self, time_value): 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 @@ -292,6 +293,7 @@ def get_elapsed_time(self, time_value): 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 @@ -318,6 +320,7 @@ def get_absolute_time(self, time_value): 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 @@ -2283,10 +2286,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 ----- diff --git a/wfdb/processing/evaluate.py b/wfdb/processing/evaluate.py index 7d49457c..f3c30a03 100644 --- a/wfdb/processing/evaluate.py +++ b/wfdb/processing/evaluate.py @@ -129,15 +129,15 @@ def compare(self): these ref/test scenarios: Parameters - ------- + ---------- N/A Returns ------- N/A - Example - ------- + Examples + -------- A: o----o---o---o x-------x----x diff --git a/wfdb/processing/qrs.py b/wfdb/processing/qrs.py index da0e3a1a..dc39f80c 100644 --- a/wfdb/processing/qrs.py +++ b/wfdb/processing/qrs.py @@ -1663,7 +1663,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 From fbe3d8048f4711fc09da647a8cf2bf4c2e4a8673 Mon Sep 17 00:00:00 2001 From: Clemens Brunner Date: Tue, 27 Oct 2026 16:52:01 +0100 Subject: [PATCH 2/6] Wrap docstrings and comments at 88 columns Docstrings and comments were wrapped inconsistently, with 86 lines over the 88 columns used for the code. Rewrap them and enforce the limit with ruff (W505, max-doc-length = 88). - Prose comments and docstrings are reflowed. - Long shell commands in the test docstrings, doctest examples, the EDF specification tables, the AnnotationLabel examples and commented-out code are split over several lines. - The EDF+ annotation table in the rdedfann docstring is now a literal block like the other format descriptions. - Fix a few typos in the lines that were touched ("thee", "mooth", "enfore"). No code is changed. --- pyproject.toml | 4 ++ tests/conftest.py | 2 +- tests/test_annotation.py | 7 ++-- tests/test_record.py | 41 ++++++++++++------- wfdb/io/_header.py | 15 ++++--- wfdb/io/_signal.py | 58 +++++++++++++++++---------- wfdb/io/annotation.py | 69 ++++++++++++++++++++++---------- wfdb/io/convert/edf.py | 86 ++++++++++++++++++++++++---------------- wfdb/io/convert/wav.py | 4 +- wfdb/io/header.py | 6 ++- wfdb/io/record.py | 24 +++++++---- wfdb/io/util.py | 8 ++-- 12 files changed, 211 insertions(+), 113 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f20459cc..549c4973 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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"] 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/test_annotation.py b/tests/test_annotation.py index 9ee80794..9e3aa019 100644 --- a/tests/test_annotation.py +++ b/tests/test_annotation.py @@ -26,9 +26,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: diff --git a/tests/test_record.py b/tests/test_record.py index 7eaac7b7..522cc30c 100644 --- a/tests/test_record.py +++ b/tests/test_record.py @@ -330,8 +330,9 @@ def test_read_write_flac_multifrequency(self, temp_path): 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 where 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( @@ -408,8 +409,9 @@ def test_unique_samps_per_frame_e_p_signal(self, temp_path): 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 where 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( @@ -640,7 +642,8 @@ def test_2d(self): """ 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) @@ -653,7 +656,8 @@ def test_2e(self): 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) @@ -697,7 +701,8 @@ def test_3b(self, source): 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", @@ -929,7 +934,8 @@ def test_multi_fixed_a(self): 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) @@ -943,7 +949,8 @@ def test_multi_fixed_b(self): 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) @@ -957,7 +964,8 @@ def test_multi_fixed_c(self): samples read from multiple 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", @@ -1020,7 +1028,8 @@ def test_multi_variable_a(self): from one segment only. 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", @@ -1038,7 +1047,8 @@ def test_multi_variable_b(self): from 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", @@ -1064,7 +1074,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 ``` @@ -1109,7 +1120,9 @@ def test_multi_variable_d(self, source): 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( diff --git a/wfdb/io/_header.py b/wfdb/io/_header.py index dabc6678..6a31ee72 100644 --- a/wfdb/io/_header.py +++ b/wfdb/io/_header.py @@ -189,7 +189,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 +204,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 ): @@ -493,7 +495,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: @@ -862,7 +865,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]. @@ -931,7 +935,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]. diff --git a/wfdb/io/_signal.py b/wfdb/io/_signal.py index dfbeb394..1f11dcc3 100644 --- a/wfdb/io/_signal.py +++ b/wfdb/io/_signal.py @@ -198,7 +198,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 +218,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 +253,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 +273,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() @@ -415,7 +419,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 +455,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 +464,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): @@ -891,7 +898,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 +909,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." @@ -999,8 +1008,8 @@ def smooth_frames(self, sigtype="physical"): 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 ------- @@ -1197,7 +1206,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 +1222,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]] @@ -1497,10 +1508,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) @@ -2388,7 +2403,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] @@ -2575,12 +2591,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/annotation.py b/wfdb/io/annotation.py index 253295ac..aa0e7ea6 100644 --- a/wfdb/io/annotation.py +++ b/wfdb/io/annotation.py @@ -29,7 +29,8 @@ class Annotation: 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:: @@ -43,7 +44,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,16 +67,24 @@ 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:: @@ -499,9 +510,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"]: @@ -613,9 +629,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 @@ -755,7 +771,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 ------- @@ -937,7 +954,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, @@ -1071,7 +1089,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 +1129,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] @@ -1317,7 +1342,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: @@ -1517,7 +1544,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] @@ -2205,8 +2233,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 +2244,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 diff --git a/wfdb/io/convert/edf.py b/wfdb/io/convert/edf.py index 6d5b465c..3d8d0c4b 100644 --- a/wfdb/io/convert/edf.py +++ b/wfdb/io/convert/edf.py @@ -79,16 +79,21 @@ def read_edf( 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) + 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 : 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 * 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) @@ -516,16 +521,21 @@ def wfdb_to_edf( 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) + 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 : 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 * 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) @@ -1029,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/wav.py b/wfdb/io/convert/wav.py index 6f5819aa..7d8b781a 100644 --- a/wfdb/io/convert/wav.py +++ b/wfdb/io/convert/wav.py @@ -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 ")) diff --git a/wfdb/io/header.py b/wfdb/io/header.py index f6ce180e..7b233271 100644 --- a/wfdb/io/header.py +++ b/wfdb/io/header.py @@ -14,7 +14,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 +32,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]*) diff --git a/wfdb/io/record.py b/wfdb/io/record.py index bd9ce612..205a31c5 100644 --- a/wfdb/io/record.py +++ b/wfdb/io/record.py @@ -378,7 +378,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 @@ -1179,7 +1180,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 != "~": @@ -1193,7 +1195,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" @@ -1802,12 +1805,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): @@ -2663,7 +2668,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] @@ -2954,7 +2960,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 = ( @@ -2976,7 +2983,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" diff --git a/wfdb/io/util.py b/wfdb/io/util.py index d45cbc49..958afcbf 100644 --- a/wfdb/io/util.py +++ b/wfdb/io/util.py @@ -127,8 +127,9 @@ 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, using `np.fromfile` + when possible. Falls back to manual reading for file-like objects that are not + compatible with np.fromfile. Parameters ---------- @@ -140,7 +141,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)) From 1594919c9ce5a8dcc4af83c253cc5411f2492df2 Mon Sep 17 00:00:00 2001 From: Clemens Brunner Date: Wed, 28 Oct 2026 21:46:43 +0100 Subject: [PATCH 3/6] Start docstrings on the opening quote line with a one-line summary Put the summary directly after the opening quotes instead of on the next line, and make it a single line. Summaries that were wrapped over several lines or were too long for one line are shortened, and the remaining text moves into the extended description below it. Lists in the first paragraph are now separated from the text by a blank line. Only docstrings change. Fix a few typos on the way ("suration", a doubled period) and make the descriptions of the CSV converters accurate. --- tests/io/test_convert.py | 20 +--- tests/test_annotation.py | 20 ++-- tests/test_multi_record.py | 4 +- tests/test_plot.py | 27 ++--- tests/test_processing.py | 21 +--- tests/test_record.py | 176 +++++++++++------------------- tests/test_url.py | 33 ++---- wfdb/io/_coreio.py | 3 +- wfdb/io/_header.py | 132 ++++++++++------------ wfdb/io/_signal.py | 133 +++++++++-------------- wfdb/io/_url.py | 57 ++++------ wfdb/io/annotation.py | 211 +++++++++++++++--------------------- wfdb/io/convert/csv.py | 12 +- wfdb/io/convert/edf.py | 20 ++-- wfdb/io/convert/matlab.py | 16 +-- wfdb/io/convert/tff.py | 15 +-- wfdb/io/convert/wav.py | 15 +-- wfdb/io/datasource.py | 15 +-- wfdb/io/download.py | 47 +++----- wfdb/io/header.py | 10 +- wfdb/io/record.py | 167 ++++++++++++---------------- wfdb/io/util.py | 33 +++--- wfdb/plot/__init__.py | 4 +- wfdb/plot/plot.py | 47 +++----- wfdb/processing/basic.py | 25 ++--- wfdb/processing/evaluate.py | 42 +++---- wfdb/processing/filter.py | 25 ++--- wfdb/processing/hr.py | 26 ++--- wfdb/processing/peaks.py | 20 ++-- wfdb/processing/qrs.py | 183 +++++++++++++------------------ 30 files changed, 610 insertions(+), 949 deletions(-) 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 9e3aa019..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 """ @@ -101,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 @@ -177,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 @@ -254,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 @@ -269,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 522cc30c..e177dec4 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- | @@ -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,10 +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( @@ -408,10 +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( @@ -491,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) @@ -518,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 | @@ -542,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 @@ -564,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 @@ -619,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 @@ -639,8 +620,8 @@ 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 @@ -652,8 +633,7 @@ 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- @@ -669,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 """ @@ -697,8 +677,7 @@ 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- @@ -730,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 @@ -764,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 @@ -804,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 @@ -850,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 @@ -870,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, @@ -898,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", @@ -920,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 @@ -930,8 +898,7 @@ 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- @@ -944,9 +911,7 @@ 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- @@ -959,9 +924,7 @@ 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 @@ -988,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 | @@ -1023,9 +985,7 @@ 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 @@ -1042,9 +1002,7 @@ 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 @@ -1061,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 @@ -1114,10 +1071,10 @@ 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 @@ -1162,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 @@ -1198,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 @@ -1227,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. """ @@ -1332,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 @@ -1382,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/io/_coreio.py b/wfdb/io/_coreio.py index 2835213b..74f76822 100644 --- a/wfdb/io/_coreio.py +++ b/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/wfdb/io/_header.py index 6a31ee72..255a2716 100644 --- a/wfdb/io/_header.py +++ b/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 ---------- @@ -237,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) @@ -256,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 ---------- @@ -266,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 ---------- @@ -297,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. @@ -346,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`. @@ -416,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 ---------- @@ -469,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 ---------- @@ -522,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 ---------- @@ -583,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 ---------- @@ -598,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 ---------- @@ -618,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. @@ -654,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 ---------- @@ -680,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 ---------- @@ -704,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 ---------- @@ -730,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 ---------- @@ -777,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 ---------- @@ -819,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 ---------- @@ -848,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 @@ -918,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 @@ -959,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. @@ -992,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 ---------- @@ -1060,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 ---------- @@ -1128,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/wfdb/io/_signal.py index 1f11dcc3..7bdc5b60 100644 --- a/wfdb/io/_signal.py +++ b/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 ---------- @@ -295,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 ---------- @@ -357,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 ---------- @@ -487,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. @@ -587,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. @@ -697,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 ---------- @@ -798,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 ---------- @@ -854,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 ---------- @@ -921,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 ---------- @@ -944,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 ---------- @@ -1001,9 +993,7 @@ 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 ---------- @@ -1105,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 ---------- @@ -1275,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 ---------- @@ -1468,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 ---------- @@ -1566,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 ---------- @@ -1615,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 @@ -1697,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 ---------- @@ -1823,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 ---------- @@ -1986,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 ---------- @@ -2053,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 ---------- @@ -2088,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 --------- @@ -2108,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 --------- @@ -2128,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 ---------- @@ -2184,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 ---------- @@ -2221,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 ---------- @@ -2252,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 ---------- @@ -2293,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 ---------- @@ -2523,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 ---------- @@ -2559,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 ---------- diff --git a/wfdb/io/_url.py b/wfdb/io/_url.py index adccbcf5..c0970119 100644 --- a/wfdb/io/_url.py +++ b/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/wfdb/io/annotation.py index aa0e7ea6..20f6b7b1 100644 --- a/wfdb/io/annotation.py +++ b/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`. @@ -191,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 ---------- @@ -236,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 ---------- @@ -262,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 ---------- @@ -321,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 ---------- @@ -342,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 ---------- @@ -362,8 +355,7 @@ def check_fields(self): return def check_field(self, field): - """ - Check a particular annotation field. + """Check a particular annotation field. Parameters ---------- @@ -597,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 ---------- @@ -652,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. @@ -737,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 ---------- @@ -754,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 @@ -845,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 ---------- @@ -887,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 @@ -917,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 ---------- @@ -969,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 ---------- @@ -1034,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 ---------- @@ -1143,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 ---------- @@ -1210,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 ---------- @@ -1258,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 ---------- @@ -1292,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 @@ -1385,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 ---------- @@ -1425,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 ---------- @@ -1447,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 ---------- @@ -1500,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 ---------- @@ -1529,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 ---------- @@ -1563,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 ---------- @@ -1586,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. @@ -1627,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 ---------- @@ -1720,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: @@ -1847,8 +1818,7 @@ def wrann( def show_ann_labels(): - """ - Display the standard WFDB annotation label mapping. + """Display the standard WFDB annotation label mapping. Parameters ---------- @@ -1867,8 +1837,7 @@ def show_ann_labels(): def show_ann_classes(): - """ - Display the standard WFDB annotation classes. + """Display the standard WFDB annotation classes. Parameters ---------- @@ -1897,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 ---------- @@ -2037,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 ---------- @@ -2079,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 ---------- @@ -2116,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 ---------- @@ -2209,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 ---------- @@ -2261,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 ---------- @@ -2348,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. @@ -2413,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 @@ -2458,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. @@ -2519,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 ---------- @@ -2545,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 ---------- @@ -2563,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 ---------- @@ -2596,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 ---------- @@ -2912,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 @@ -3002,8 +2963,7 @@ def format_ann_from_df(df_in): class AnnotationClass: - """ - Describes the annotations. + """Describes the annotations. Attributes ---------- @@ -3055,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/csv.py b/wfdb/io/convert/csv.py index b41b6aba..f095b06a 100644 --- a/wfdb/io/convert/csv.py +++ b/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 ---------- @@ -479,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/wfdb/io/convert/edf.py index 3d8d0c4b..6fe832dd 100644 --- a/wfdb/io/convert/edf.py +++ b/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 @@ -476,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 @@ -981,10 +981,10 @@ 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 diff --git a/wfdb/io/convert/matlab.py b/wfdb/io/convert/matlab.py index d3a011a2..fa24ae32 100644 --- a/wfdb/io/convert/matlab.py +++ b/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. diff --git a/wfdb/io/convert/tff.py b/wfdb/io/convert/tff.py index 312d8715..91c5c704 100644 --- a/wfdb/io/convert/tff.py +++ b/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/wfdb/io/convert/wav.py index 7d8b781a..ef3284c7 100644 --- a/wfdb/io/convert/wav.py +++ b/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 ---------- @@ -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 diff --git a/wfdb/io/datasource.py b/wfdb/io/datasource.py index 3ff4f152..5f1c09af 100644 --- a/wfdb/io/datasource.py +++ b/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/wfdb/io/download.py index 3f5123e4..ce84846f 100644 --- a/wfdb/io/download.py +++ b/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/wfdb/io/header.py index 7b233271..4cc7cbe6 100644 --- a/wfdb/io/header.py +++ b/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 @@ -65,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. @@ -100,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/wfdb/io/record.py index 205a31c5..107b0fc9 100644 --- a/wfdb/io/record.py +++ b/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,8 +245,7 @@ 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: @@ -289,8 +287,7 @@ 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: @@ -316,8 +313,7 @@ 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: @@ -348,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 ---------- @@ -550,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 ---------- @@ -623,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. @@ -655,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) @@ -828,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 ---------- @@ -880,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 ---------- @@ -916,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 ---------- @@ -974,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 @@ -1011,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) @@ -1136,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 ---------- @@ -1163,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 ---------- @@ -1206,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 ---------- @@ -1270,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 ---------- @@ -1328,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 ---------- @@ -1421,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 ---------- @@ -1658,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 ---------- @@ -1719,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 ---------- @@ -1764,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 ---------- @@ -1917,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 ---------- @@ -2240,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 ---------- @@ -2362,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 ---------- @@ -2414,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 ---------- @@ -2474,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 ---------- @@ -2600,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 ---------- @@ -2757,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 ---------- @@ -2804,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 ---------- @@ -2858,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 ---------- @@ -3053,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/wfdb/io/util.py index 958afcbf..1fbfb9a4 100644 --- a/wfdb/io/util.py +++ b/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,10 +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 ---------- diff --git a/wfdb/plot/__init__.py b/wfdb/plot/__init__.py index fb78fb36..de6734c0 100644 --- a/wfdb/plot/__init__.py +++ b/wfdb/plot/__init__.py @@ -1,5 +1,3 @@ -""" -The plot subpackage contains tools for plotting signals and annotations. -""" +"""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/wfdb/plot/plot.py index e7853e17..3fd6336a 100644 --- a/wfdb/plot/plot.py +++ b/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/basic.py b/wfdb/processing/basic.py index 0390b754..576ccbb3 100644 --- a/wfdb/processing/basic.py +++ b/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/wfdb/processing/evaluate.py index f3c30a03..e65a4257 100644 --- a/wfdb/processing/evaluate.py +++ b/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,9 +122,9 @@ 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 ---------- @@ -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/wfdb/processing/filter.py index 5b108987..67458fad 100644 --- a/wfdb/processing/filter.py +++ b/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/wfdb/processing/hr.py index b2ea28e0..76750677 100644 --- a/wfdb/processing/hr.py +++ b/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/wfdb/processing/peaks.py index 47d7ce48..7c618d91 100644 --- a/wfdb/processing/peaks.py +++ b/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/wfdb/processing/qrs.py index dc39f80c..f81ec65b 100644 --- a/wfdb/processing/qrs.py +++ b/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 ---------- @@ -1781,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: From ff94876bd861e440e28f8196e5d95b3b08e8c39f Mon Sep 17 00:00:00 2001 From: Clemens Brunner Date: Wed, 28 Oct 2026 21:49:49 +0100 Subject: [PATCH 4/6] Move the wfdb package to a src layout Move wfdb/ to src/wfdb/, which is the default module root of uv_build, so the module-root setting is no longer needed. The tests now always run against the installed package instead of the copy in the working directory, and the documentation build adds src/ to sys.path. --- docs/conf.py | 2 +- pyproject.toml | 3 --- {wfdb => src/wfdb}/__init__.py | 0 {wfdb => src/wfdb}/io/__init__.py | 0 {wfdb => src/wfdb}/io/_coreio.py | 0 {wfdb => src/wfdb}/io/_header.py | 0 {wfdb => src/wfdb}/io/_signal.py | 0 {wfdb => src/wfdb}/io/_url.py | 0 {wfdb => src/wfdb}/io/annotation.py | 0 {wfdb => src/wfdb}/io/convert/__init__.py | 0 {wfdb => src/wfdb}/io/convert/csv.py | 0 {wfdb => src/wfdb}/io/convert/edf.py | 0 {wfdb => src/wfdb}/io/convert/matlab.py | 0 {wfdb => src/wfdb}/io/convert/tff.py | 0 {wfdb => src/wfdb}/io/convert/wav.py | 0 {wfdb => src/wfdb}/io/datasource.py | 0 {wfdb => src/wfdb}/io/download.py | 0 {wfdb => src/wfdb}/io/header.py | 0 {wfdb => src/wfdb}/io/record.py | 0 {wfdb => src/wfdb}/io/util.py | 0 {wfdb => src/wfdb}/plot/__init__.py | 0 {wfdb => src/wfdb}/plot/plot.py | 0 {wfdb => src/wfdb}/processing/__init__.py | 0 {wfdb => src/wfdb}/processing/basic.py | 0 {wfdb => src/wfdb}/processing/evaluate.py | 0 {wfdb => src/wfdb}/processing/filter.py | 0 {wfdb => src/wfdb}/processing/hr.py | 0 {wfdb => src/wfdb}/processing/peaks.py | 0 {wfdb => src/wfdb}/processing/qrs.py | 0 {wfdb => src/wfdb}/version.py | 0 tests/test_record.py | 2 +- 31 files changed, 2 insertions(+), 5 deletions(-) rename {wfdb => src/wfdb}/__init__.py (100%) rename {wfdb => src/wfdb}/io/__init__.py (100%) rename {wfdb => src/wfdb}/io/_coreio.py (100%) rename {wfdb => src/wfdb}/io/_header.py (100%) rename {wfdb => src/wfdb}/io/_signal.py (100%) rename {wfdb => src/wfdb}/io/_url.py (100%) rename {wfdb => src/wfdb}/io/annotation.py (100%) rename {wfdb => src/wfdb}/io/convert/__init__.py (100%) rename {wfdb => src/wfdb}/io/convert/csv.py (100%) rename {wfdb => src/wfdb}/io/convert/edf.py (100%) rename {wfdb => src/wfdb}/io/convert/matlab.py (100%) rename {wfdb => src/wfdb}/io/convert/tff.py (100%) rename {wfdb => src/wfdb}/io/convert/wav.py (100%) rename {wfdb => src/wfdb}/io/datasource.py (100%) rename {wfdb => src/wfdb}/io/download.py (100%) rename {wfdb => src/wfdb}/io/header.py (100%) rename {wfdb => src/wfdb}/io/record.py (100%) rename {wfdb => src/wfdb}/io/util.py (100%) rename {wfdb => src/wfdb}/plot/__init__.py (100%) rename {wfdb => src/wfdb}/plot/plot.py (100%) rename {wfdb => src/wfdb}/processing/__init__.py (100%) rename {wfdb => src/wfdb}/processing/basic.py (100%) rename {wfdb => src/wfdb}/processing/evaluate.py (100%) rename {wfdb => src/wfdb}/processing/filter.py (100%) rename {wfdb => src/wfdb}/processing/hr.py (100%) rename {wfdb => src/wfdb}/processing/peaks.py (100%) rename {wfdb => src/wfdb}/processing/qrs.py (100%) rename {wfdb => src/wfdb}/version.py (100%) diff --git a/docs/conf.py b/docs/conf.py index 0e9af116..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 diff --git a/pyproject.toml b/pyproject.toml index 549c4973..95a9e5d1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -75,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 100% rename from wfdb/io/_coreio.py rename to src/wfdb/io/_coreio.py diff --git a/wfdb/io/_header.py b/src/wfdb/io/_header.py similarity index 100% rename from wfdb/io/_header.py rename to src/wfdb/io/_header.py diff --git a/wfdb/io/_signal.py b/src/wfdb/io/_signal.py similarity index 100% rename from wfdb/io/_signal.py rename to src/wfdb/io/_signal.py diff --git a/wfdb/io/_url.py b/src/wfdb/io/_url.py similarity index 100% rename from wfdb/io/_url.py rename to src/wfdb/io/_url.py diff --git a/wfdb/io/annotation.py b/src/wfdb/io/annotation.py similarity index 100% rename from wfdb/io/annotation.py rename to src/wfdb/io/annotation.py 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 100% rename from wfdb/io/convert/csv.py rename to src/wfdb/io/convert/csv.py diff --git a/wfdb/io/convert/edf.py b/src/wfdb/io/convert/edf.py similarity index 100% rename from wfdb/io/convert/edf.py rename to src/wfdb/io/convert/edf.py diff --git a/wfdb/io/convert/matlab.py b/src/wfdb/io/convert/matlab.py similarity index 100% rename from wfdb/io/convert/matlab.py rename to src/wfdb/io/convert/matlab.py diff --git a/wfdb/io/convert/tff.py b/src/wfdb/io/convert/tff.py similarity index 100% rename from wfdb/io/convert/tff.py rename to src/wfdb/io/convert/tff.py diff --git a/wfdb/io/convert/wav.py b/src/wfdb/io/convert/wav.py similarity index 100% rename from wfdb/io/convert/wav.py rename to src/wfdb/io/convert/wav.py diff --git a/wfdb/io/datasource.py b/src/wfdb/io/datasource.py similarity index 100% rename from wfdb/io/datasource.py rename to src/wfdb/io/datasource.py diff --git a/wfdb/io/download.py b/src/wfdb/io/download.py similarity index 100% rename from wfdb/io/download.py rename to src/wfdb/io/download.py diff --git a/wfdb/io/header.py b/src/wfdb/io/header.py similarity index 100% rename from wfdb/io/header.py rename to src/wfdb/io/header.py diff --git a/wfdb/io/record.py b/src/wfdb/io/record.py similarity index 100% rename from wfdb/io/record.py rename to src/wfdb/io/record.py diff --git a/wfdb/io/util.py b/src/wfdb/io/util.py similarity index 100% rename from wfdb/io/util.py rename to src/wfdb/io/util.py diff --git a/wfdb/plot/__init__.py b/src/wfdb/plot/__init__.py similarity index 100% rename from wfdb/plot/__init__.py rename to src/wfdb/plot/__init__.py diff --git a/wfdb/plot/plot.py b/src/wfdb/plot/plot.py similarity index 100% rename from wfdb/plot/plot.py rename to src/wfdb/plot/plot.py 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 100% rename from wfdb/processing/basic.py rename to src/wfdb/processing/basic.py diff --git a/wfdb/processing/evaluate.py b/src/wfdb/processing/evaluate.py similarity index 100% rename from wfdb/processing/evaluate.py rename to src/wfdb/processing/evaluate.py diff --git a/wfdb/processing/filter.py b/src/wfdb/processing/filter.py similarity index 100% rename from wfdb/processing/filter.py rename to src/wfdb/processing/filter.py diff --git a/wfdb/processing/hr.py b/src/wfdb/processing/hr.py similarity index 100% rename from wfdb/processing/hr.py rename to src/wfdb/processing/hr.py diff --git a/wfdb/processing/peaks.py b/src/wfdb/processing/peaks.py similarity index 100% rename from wfdb/processing/peaks.py rename to src/wfdb/processing/peaks.py diff --git a/wfdb/processing/qrs.py b/src/wfdb/processing/qrs.py similarity index 100% rename from wfdb/processing/qrs.py rename to src/wfdb/processing/qrs.py 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/test_record.py b/tests/test_record.py index e177dec4..2bb636ae 100644 --- a/tests/test_record.py +++ b/tests/test_record.py @@ -240,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): From b41b928fab73146af692983f4619166b11670621 Mon Sep 17 00:00:00 2001 From: Clemens Brunner Date: Wed, 28 Oct 2026 21:50:17 +0100 Subject: [PATCH 5/6] Ignore the docstring and comment reformatting commits in git blame --- .git-blame-ignore-revs | 6 ++++++ DEVELOPING.md | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) 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/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. From b1e454d5bda7cd265640411e8a1cfb6e190260f7 Mon Sep 17 00:00:00 2001 From: Clemens Brunner Date: Wed, 28 Oct 2026 21:52:53 +0100 Subject: [PATCH 6/6] Add AGENTS.md and CLAUDE.md --- AGENTS.md | 32 ++++++++++++++++++++++++++++++++ CLAUDE.md | 3 +++ 2 files changed, 35 insertions(+) create mode 100644 AGENTS.md create mode 100644 CLAUDE.md 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.