wfdb-x 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. wfdb_x-1.0.0.dist-info/METADATA +583 -0
  2. wfdb_x-1.0.0.dist-info/RECORD +65 -0
  3. wfdb_x-1.0.0.dist-info/WHEEL +5 -0
  4. wfdb_x-1.0.0.dist-info/entry_points.txt +2 -0
  5. wfdb_x-1.0.0.dist-info/licenses/LICENSE +674 -0
  6. wfdb_x-1.0.0.dist-info/top_level.txt +1 -0
  7. wfdbx/__init__.py +96 -0
  8. wfdbx/_annot.py +1718 -0
  9. wfdbx/_calib.py +229 -0
  10. wfdbx/_cstd.py +465 -0
  11. wfdbx/_ecgcodes.py +273 -0
  12. wfdbx/_flac.py +927 -0
  13. wfdbx/_signal.py +4444 -0
  14. wfdbx/_wfdbinit.py +76 -0
  15. wfdbx/_wfdbio.py +1373 -0
  16. wfdbx/apps/__init__.py +28 -0
  17. wfdbx/apps/_common.py +252 -0
  18. wfdbx/apps/a2m.py +269 -0
  19. wfdbx/apps/ad2m.py +309 -0
  20. wfdbx/apps/ahaecg2mit.py +285 -0
  21. wfdbx/apps/ann2rr.py +339 -0
  22. wfdbx/apps/edf2mit.py +381 -0
  23. wfdbx/apps/m2a.py +145 -0
  24. wfdbx/apps/makeid.py +108 -0
  25. wfdbx/apps/md2a.py +130 -0
  26. wfdbx/apps/mit2edf.py +354 -0
  27. wfdbx/apps/mit2wav.py +189 -0
  28. wfdbx/apps/parsescp.py +2029 -0
  29. wfdbx/apps/rdann.py +345 -0
  30. wfdbx/apps/rdedfann.py +228 -0
  31. wfdbx/apps/rdsamp.py +460 -0
  32. wfdbx/apps/readid.py +80 -0
  33. wfdbx/apps/revise.py +205 -0
  34. wfdbx/apps/rr2ann.py +201 -0
  35. wfdbx/apps/sampfreq.py +82 -0
  36. wfdbx/apps/sigamp.py +360 -0
  37. wfdbx/apps/signame.py +125 -0
  38. wfdbx/apps/signum.py +69 -0
  39. wfdbx/apps/snip.py +386 -0
  40. wfdbx/apps/sortann.py +197 -0
  41. wfdbx/apps/time2sec.py +75 -0
  42. wfdbx/apps/wav2mit.py +227 -0
  43. wfdbx/apps/wfdb2mat.py +379 -0
  44. wfdbx/apps/wfdbcat.py +70 -0
  45. wfdbx/apps/wfdbdesc.py +196 -0
  46. wfdbx/apps/wfdbtime.py +82 -0
  47. wfdbx/apps/wfdbwhich.py +88 -0
  48. wfdbx/apps/wrann.py +155 -0
  49. wfdbx/apps/wrsamp.py +590 -0
  50. wfdbx/apps/xform.py +816 -0
  51. wfdbx/check.py +859 -0
  52. wfdbx/cli.py +62 -0
  53. wfdbx/examples/01_getting_started.ipynb +196 -0
  54. wfdbx/examples/02_annotations_and_heart_rate.ipynb +149 -0
  55. wfdbx/examples/03_writing_records.ipynb +171 -0
  56. wfdbx/examples/04_command_line_tools.ipynb +164 -0
  57. wfdbx/examples/05_c_library_api.ipynb +155 -0
  58. wfdbx/examples/06_physionet_records.ipynb +118 -0
  59. wfdbx/examples/07_legacy_mode.ipynb +127 -0
  60. wfdbx/highlevel.py +1016 -0
  61. wfdbx/resources.py +67 -0
  62. wfdbx/sample_data/100s.atr +0 -0
  63. wfdbx/sample_data/100s.dat +12 -0
  64. wfdbx/sample_data/100s.hea +6 -0
  65. wfdbx/sample_data/README.md +11 -0
@@ -0,0 +1,583 @@
1
+ Metadata-Version: 2.4
2
+ Name: wfdb-x
3
+ Version: 1.0.0
4
+ Summary: A faithful, lightweight Python implementation of the WFDB library and command-line tools (WFDB 10.7.0)
5
+ Author-email: Reza Sameni <reza.sameni@gmail.com>
6
+ Maintainer-email: Reza Sameni <reza.sameni@gmail.com>
7
+ License-Expression: GPL-3.0-or-later
8
+ Project-URL: Homepage, https://github.com/alphanumericslab/wfdb-x
9
+ Project-URL: Source, https://github.com/alphanumericslab/wfdb-x
10
+ Project-URL: Documentation, https://github.com/alphanumericslab/wfdb-x/tree/main/python#readme
11
+ Project-URL: Issues, https://github.com/alphanumericslab/wfdb-x/issues
12
+ Keywords: WFDB,PhysioNet,physiologic signals,ECG,biomedical signal processing
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3 :: Only
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Environment :: Console
17
+ Classifier: Topic :: Scientific/Engineering :: Medical Science Apps.
18
+ Classifier: Intended Audience :: Science/Research
19
+ Classifier: Framework :: Jupyter
20
+ Requires-Python: >=3.9
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: numpy>=1.17
24
+ Provides-Extra: test
25
+ Requires-Dist: pytest; extra == "test"
26
+ Requires-Dist: nbformat; extra == "test"
27
+ Requires-Dist: nbclient; extra == "test"
28
+ Requires-Dist: ipykernel; extra == "test"
29
+ Provides-Extra: examples
30
+ Requires-Dist: matplotlib; extra == "examples"
31
+ Requires-Dist: jupyter; extra == "examples"
32
+ Dynamic: license-file
33
+
34
+ # WFDB-x for Python
35
+
36
+ `wfdbx` reads and writes physiological signal records in the WFDB format used
37
+ by [PhysioNet](https://physionet.org). It is a complete Python implementation
38
+ of the WFDB Software Package 10.7.0: the library and all of its command-line
39
+ tools. It reads and writes the same files as the original C library, and its
40
+ tools print the same output.
41
+
42
+ You can use it in three ways:
43
+
44
+ * **High-level functions** that read and write whole records and annotation
45
+ files as NumPy arrays: `read_record`, `read_header`, `read_annotations`,
46
+ `write_record`, `write_annotations`, and `check_record`, which checks a
47
+ record.
48
+ * **Command-line tools** such as `rdsamp`, `rdann` and `wrsamp`, run through
49
+ the `wfdbx` command.
50
+ * **The WFDB library functions** under their C names (`isigopen`, `getvec`,
51
+ `getann`, ...), for programs written against the C library.
52
+
53
+ The package is pure Python. It needs Python 3.9 or later and NumPy, and runs
54
+ on Linux, macOS and Windows.
55
+
56
+ WFDB-x is part of a project that provides the same package in seven languages
57
+ with identical behavior; see the
58
+ [project README](https://github.com/alphanumericslab/wfdb-x#readme).
59
+
60
+ ## Installation
61
+
62
+ ```sh
63
+ pip install wfdb-x # the package and the wfdbx command
64
+ pip install "wfdb-x[examples]" # also matplotlib and Jupyter, for the example notebooks
65
+ ```
66
+
67
+ The package is imported as `wfdbx`. To install from a copy of the
68
+ repository, run `pip install ./python`.
69
+
70
+ ## Quick start
71
+
72
+ The package includes a sample record, `100s`: the first minute of record 100
73
+ of the [MIT-BIH Arrhythmia Database](https://physionet.org/content/mitdb/),
74
+ with two ECG signals at 360 Hz and its beat annotations.
75
+
76
+ ```python
77
+ import wfdbx
78
+
79
+ # Read the sample record from the package's data directory.
80
+ wfdbx.wfdbx_set_input_dir(wfdbx.sample_data_dir())
81
+
82
+ rec = wfdbx.read_record("100s", physical=True) # signals in physical units
83
+ print(rec.fs, rec.names, rec.signals.shape) # 360.0 ['MLII', 'V5'] (21600, 2)
84
+
85
+ hdr = wfdbx.read_header("100s") # the header only, no samples
86
+ print(hdr.nsig, hdr.nsamples) # 2 21600
87
+
88
+ ann = wfdbx.read_annotations("100s", "atr") # beat annotations
89
+ print(len(ann), ann.symbols[:3], ann.samples[:3]) # 75 ['+', 'N', 'N'] [ 18 77 370]
90
+
91
+ # Write the first 10 seconds as a new record, "copy", in the current directory.
92
+ x = wfdbx.read_record("100s", sampto="0:10") # ADC units
93
+ wfdbx.write_record("copy", x.signals, fs=x.fs, fmt=16, gains=x.gains,
94
+ baselines=x.baselines, adcres=11, units="mV", names=x.names)
95
+ ```
96
+
97
+ ## Reading and writing records
98
+
99
+ The examples in this section read the sample record `100s` (the first minute
100
+ of record 100 of the MIT-BIH Arrhythmia Database, included with WFDB-x, in
101
+ the directory `wfdbx.sample_data_dir()`), and write records of their own in
102
+ the current directory.
103
+
104
+ ### Reading signals: `read_record`
105
+
106
+ ```python
107
+ wfdbx.wfdbx_set_input_dir(wfdbx.sample_data_dir()) # the sample record 100s
108
+ rec = wfdbx.read_record("100s", channels=["MLII"], sampfrom="0:10", sampto="0:20",
109
+ physical=True)
110
+ ```
111
+
112
+ | Argument | Meaning | Default |
113
+ |---|---|---|
114
+ | `record` | record name or path (see [Where files are read and written](#where-files-are-read-and-written)) | |
115
+ | `channels` | signals to read, by number or name (as in `names`) | all signals |
116
+ | `sampfrom`, `sampto` | first sample, and the sample after the last one: a sample number or a time string (see below) | the whole record |
117
+ | `physical` | return physical units (float64) instead of ADC units (int32) | `False` |
118
+ | `highres` | read a multi-frequency record at its highest sampling frequency | `False` |
119
+ | `pad` | replace invalid samples by the previous valid one | `False` |
120
+ | `checksum_warning` | warn when the data do not match the header's checksums | `True` |
121
+ | `input_dir`, `network` | input directory and network access for this call only | the current settings |
122
+ | `legacy` | behave as the original package (see [Legacy mode](#legacy-mode)) | `False` |
123
+
124
+ Times can be sample numbers or WFDB time strings: `"0:10"` (10 seconds),
125
+ `"1:02:03.5"`, `"s3600"` (sample 3600) or `"e"` (the end of the record).
126
+
127
+ The result is a `Record`:
128
+
129
+ | Field | Contents |
130
+ |---|---|
131
+ | `signals` | NumPy array, one row per sample and one column per signal: int32 ADC units, or float64 physical units with `physical=True` |
132
+ | `fs` | sampling frequency in Hz (0 if the header gives none) |
133
+ | `names`, `units`, `gains`, `baselines` | description (without white space at either end, which some headers add), physical units, gain (ADC units per physical unit; 0 means uncalibrated) and baseline of each signal |
134
+ | `siginfo` | the full signal specifications (`Siginfo` objects) |
135
+ | `sampfrom` | the number of the first sample returned |
136
+ | `base_time` | start time and date as text, to the second, such as `"[14:00:03 21/06/2024]"` |
137
+ | `base_time_ms` | start time in milliseconds after midnight, such as `50403500` for 14:00:03.500 |
138
+ | `base_date` | start date as `(year, month, day)` |
139
+ | `comments` | the info strings of the header |
140
+ | `checksum_ok` | `False` if the data do not match the header's checksums |
141
+
142
+ `base_time`, `base_time_ms` and `base_date` are `None` when the header does
143
+ not give them. Invalid samples are `wfdbx.WFDB_INVALID_SAMPLE`, or NaN in
144
+ physical units.
145
+
146
+ ### Reading a header: `read_header`
147
+
148
+ `read_header(record)` reads the header of a record without opening its
149
+ signal files. It returns a `Header` with these fields: `name`, `fs`,
150
+ `counter_freq`, `base_counter`, `nsamples` (0 if the header does not give
151
+ the length), `nsig`, `siginfo`, `base_time`, `base_time_ms`, `base_date` and
152
+ `comments`. They have the same meaning as in `Record`.
153
+
154
+ ```python
155
+ wfdbx.wfdbx_set_input_dir(wfdbx.sample_data_dir()) # the sample record 100s
156
+ hdr = wfdbx.read_header("100s")
157
+ for s in hdr.siginfo:
158
+ print(s.desc, s.fmt, s.gain, s.units)
159
+ ```
160
+
161
+ ### Reading annotations: `read_annotations`
162
+
163
+ ```python
164
+ wfdbx.wfdbx_set_input_dir(wfdbx.sample_data_dir()) # the sample record 100s
165
+ ann = wfdbx.read_annotations("100s", "atr", sampfrom="0:10", sampto="0:30")
166
+ ```
167
+
168
+ `annotator` is the file suffix (`"atr"` reads `100s.atr`). The optional `fs`
169
+ gives the sampling frequency when the record has no header, or its header
170
+ gives none. The result is an `Annotations` object, in file order:
171
+
172
+ | Field | Contents |
173
+ |---|---|
174
+ | `samples` | annotation times, in samples (int64) |
175
+ | `codes`, `symbols` | annotation codes (`wfdbx.NORMAL`, `wfdbx.PVC`, ...) and their mnemonics (`"N"`, `"V"`, ...) |
176
+ | `subtypes`, `chans`, `nums` | subtype, channel and annotator number of each annotation (int64) |
177
+ | `aux` | auxiliary text of each annotation, or `None` |
178
+ | `fs` | the frequency in which `samples` are counted (0 if unknown) |
179
+
180
+ ### Writing a record: `write_record`
181
+
182
+ ```python
183
+ import numpy as np
184
+ x = np.random.randn(3600, 2) # random numbers, written as a record "rand"
185
+ wfdbx.write_record("rand", x, fs=360, fmt=16, physical=True,
186
+ gains=1000, adcres=16, units="mV", names=["A", "B"])
187
+ ```
188
+
189
+ This writes the header `rand.hea` and the signal file `rand.dat`. You must
190
+ give the sampling frequency, the gain and ADC resolution of each signal, and
191
+ the units of each calibrated signal; there are no defaults. A gain of 0
192
+ marks a signal as uncalibrated, which is only allowed for samples given in
193
+ ADC units. Per-signal arguments take one value for all signals, or a list
194
+ with one value per signal.
195
+
196
+ | Argument | Meaning |
197
+ |---|---|
198
+ | `signals` | array with one row per sample and one column per signal; ADC units, or physical units with `physical=True` |
199
+ | `fs` | sampling frequency in Hz |
200
+ | `fmt` | storage format, such as 16, 212, 24 or 516 (FLAC); default 16 |
201
+ | `gains`, `adcres`, `units` | gain, ADC resolution in bits, and physical units |
202
+ | `baselines`, `adczeros`, `names` | optional signal specifications |
203
+ | `base_time`, `comments` | start time (`"hh:mm:ss dd/mm/yyyy"`) and info strings |
204
+ | `samps_per_frame` | samples per frame of each signal, for a multi-frequency record; `signals` then has one row per frame and `sum(samps_per_frame)` columns |
205
+ | `dir` | directory in which to create the files |
206
+
207
+ ### Writing annotations: `write_annotations`
208
+
209
+ This example writes the annotation file `rand.qrs` for the record `rand`
210
+ written above:
211
+
212
+ ```python
213
+ wfdbx.write_annotations("rand", "qrs", samples=[100, 460, 820], symbols=["N", "N", "V"])
214
+ ```
215
+
216
+ Give the annotation types as `symbols` or as `codes`. `subtypes`, `chans`,
217
+ `nums` and `aux` are optional. If the annotations are not in time order, the
218
+ file is sorted when it is written. `sort=False` keeps the order you gave
219
+ (without the library's advice to run `sortann`) and `sort=True` always
220
+ sorts, for that call only. Without `sort`, the `WFDBANNSORT` environment
221
+ variable decides (sorting is on unless it is `0`).
222
+
223
+ ### Errors and warnings
224
+
225
+ * A record, header or annotation file that cannot be read or written raises
226
+ `IOError`. This includes a file that cannot be written completely, for
227
+ example on a full disk: "write error on record REC", "write error on
228
+ annotator ANN for record REC". (`wfdbx_write_errors()` counts such errors
229
+ for the C-style functions, which report them only with a message, as in
230
+ C.)
231
+ * In `write_record`, `samps_per_frame` values that are not integers of at
232
+ least 1, and a number of columns of `signals` other than their sum, raise
233
+ `ValueError` before any file is created.
234
+ * A signal file that is shorter than its header says raises `IOError`:
235
+ "can't read signal data of record REC (getvec status -3)".
236
+ * An annotation file that ends before its end-of-file marker raises
237
+ `IOError`: "can't read annotation file ANN of record REC (getann status
238
+ -3)".
239
+ * Invalid arguments, and a value the operation needs but the record does not
240
+ give, raise `ValueError`. For example, a time string such as `"0:10"`
241
+ needs the sampling frequency, and `physical=True` needs the gain of each
242
+ signal.
243
+ * When the data do not match the checksums in the header, `read_record`
244
+ returns all the data and issues a `RuntimeWarning`: "checksum error in
245
+ record REC: the signal data do not match the checksums in its header".
246
+ `checksum_warning=False` turns the warning off; `rec.checksum_ok` is
247
+ `False` either way. Checksums are only checked when a read reaches the end
248
+ of the record.
249
+ * A tool, or a program that uses the WFDB library functions, never writes a
250
+ signal file over one of the signal files it is reading. Opening such an
251
+ output fails with "output file NAME is also an input file". `write_record`
252
+ reads nothing, so it overwrites any existing files of the same name.
253
+
254
+ The high-level functions call `wfdbquit()`, which closes anything opened
255
+ with the WFDB library functions.
256
+
257
+ ### Checking a record: `check_record`
258
+
259
+ ```python
260
+ wfdbx.wfdbx_set_input_dir(wfdbx.sample_data_dir()) # the sample record 100s
261
+ report = wfdbx.check_record("100s", ["atr"])
262
+ print(report)
263
+ ```
264
+
265
+ `check_record(record, annotators)` checks the header of a record, its signal
266
+ specifications, every sample of its signal files, and the annotation files
267
+ you name. It reports each problem it finds. It never raises an exception for
268
+ a problem in the record. It checks for problems similar to those found by
269
+ Benjamin Moody's [wfdbcheck](https://github.com/bemoody/wfdbcheck).
270
+
271
+ The result is a `CheckReport`. `report.ok` is `True` if there is no error.
272
+ `report.issues` lists the problems. Each has a `severity`, a `code` (such as
273
+ `"checksum"`), a `where` (`""` for the record, `"signal 1"`, `"segment s2"`,
274
+ `"annotator atr"`) and a `message`.
275
+
276
+ | Severity | Meaning |
277
+ |---|---|
278
+ | `error` | the record is damaged or does not follow the WFDB format: programs will read wrong data or fail |
279
+ | `warning` | the record can be read, but something is missing or unusual, and may give wrong results |
280
+ | `info` | a fact worth knowing, such as invalid (missing) samples |
281
+
282
+ Printed, the report has one line per problem, or the line
283
+ `no problems found`:
284
+
285
+ ```
286
+ warning: signal 0: the units are not specified
287
+ error: signal 1: the checksum does not match: the header gives 12, the samples sum to 13
288
+ error: annotator atr: 3 annotations are out of order; the first is at sample 50, after sample 100
289
+ ```
290
+
291
+ `report.lines()` returns these lines as a list. The lines are the same in
292
+ the seven languages of WFDB-x. The checks run in normal mode, even if legacy mode is on, and the library
293
+ prints no messages during the call.
294
+
295
+ What is checked:
296
+
297
+ * the header: that it can be read, the sampling frequency, the length, lines
298
+ of at most 255 characters, and the record name it gives;
299
+ * each signal: description, gain, units, ADC resolution and range, storage
300
+ format, and signal file name;
301
+ * the signal files: that they can be read, samples outside the ADC range,
302
+ invalid samples, checksums, and the length given in the header;
303
+ * each segment of a multi-segment record, and its signals against the
304
+ record's;
305
+ * the annotation files: that they can be read to the end, time order,
306
+ annotation codes, and annotations after the end of the record.
307
+
308
+ The sample record `100s` gives two warnings: its header gives no units.
309
+
310
+ ## Where files are read and written
311
+
312
+ WFDB-x reads and writes files exactly where you name them, and nowhere else:
313
+
314
+ | The name is | It is read from | It is written to |
315
+ |---|---|---|
316
+ | a full path: `/data/mitdb/100`, `C:\data\100` | that path | that path |
317
+ | a home path: `~/data/100` | your home directory | your home directory |
318
+ | a URL: `https://.../100` | that URL, if network access is on | (URLs cannot be written) |
319
+ | anything else: `100`, `mitdb/100`, `../data/100` | the **input directory** | the **output directory** |
320
+
321
+ In this table, `/data/mitdb` stands for a local copy of the MIT-BIH
322
+ Arrhythmia Database, and `100` for its record 100.
323
+
324
+ The input and output directories are both the current directory unless you
325
+ set them:
326
+
327
+ | Setting | Function | Option of the high-level functions | Environment variable |
328
+ |---|---|---|---|
329
+ | input directory: a local directory, `~/...` or a URL | `wfdbx_set_input_dir(d)` | `input_dir=` | `WFDBX_INPUT_DIR` |
330
+ | output directory | `wfdbx_set_output_dir(d)` | `dir=` | `WFDBX_OUTPUT_DIR` |
331
+ | network access (off by default) | `wfdbx_set_network(1)` | `network=True` | `WFDBX_NETWORK=1` |
332
+
333
+ A function call overrides the environment variable, and an option applies to
334
+ that one call. `wfdbx_get_input_dir()`, `wfdbx_get_output_dir()` and
335
+ `wfdbx_get_network()` return the current settings, and passing `None` to a
336
+ `set` function restores the default.
337
+
338
+ In this example, `/data/mitdb` stands for a local copy of the MIT-BIH
339
+ Arrhythmia Database, which contains record 100:
340
+
341
+ ```python
342
+ wfdbx.wfdbx_set_input_dir("/data/mitdb")
343
+ rec = wfdbx.read_record("100") # reads /data/mitdb/100.hea and 100.dat
344
+ wfdbx.write_annotations("100", "qrs", samples=[100, 460], symbols=["N", "N"],
345
+ dir="out") # writes out/100.qrs
346
+ ```
347
+
348
+ The signal files that a header names are read from the header's directory.
349
+
350
+ ### Record names
351
+
352
+ A record name is made of letters, digits, `_`, `-` and `~`. `/` separates
353
+ directories, and the names of directories may contain any characters, such
354
+ as dots: `my.data/rec` is a valid record name, `rec.1` is not. A name whose
355
+ last part contains a dot is the name of a file: `100s.hea` is read as the
356
+ header of record `100s`, and `100s.edf` as an EDF file.
357
+
358
+ Records can be written under any such name: `write_record("my.data/rec",
359
+ ...)` creates `my.data/rec.hea` and `my.data/rec.dat`. The header names the
360
+ record and its signal file without the directory (`rec`, `rec.dat`), so the
361
+ record can be read under any path that leads to it. Annotator names may
362
+ contain letters, digits, `_`, `-` and `~`.
363
+
364
+ ### Reading from PhysioNet
365
+
366
+ Set the input directory to the URL of the database and turn network access
367
+ on. The URL of a database is `https://physionet.org/files/<database>/<version>/`.
368
+ This example reads record 100 of the MIT-BIH Arrhythmia Database:
369
+
370
+ ```python
371
+ rec = wfdbx.read_record("100", input_dir="https://physionet.org/files/mitdb/1.0.0/",
372
+ network=True)
373
+ ```
374
+
375
+ Remote files are read once and kept for the rest of the session. A download
376
+ gives up when it cannot connect, or receives nothing, for 30 seconds.
377
+
378
+ ## Signal parameters
379
+
380
+ WFDB-x never assumes a sampling frequency, gain, ADC resolution or units
381
+ that a record does not give. Where one is needed, you must give it: in
382
+ `write_record`, as the `fs` argument of `read_annotations`, or as an option
383
+ of the tools (see below). A record whose header gives no frequency has
384
+ `fs = 0`. See
385
+ [DIVERGENCES.md](https://github.com/alphanumericslab/wfdb-x/blob/main/docs/DIVERGENCES.md)
386
+ for the reasons.
387
+
388
+ ## Command-line tools
389
+
390
+ Run a tool through the `wfdbx` command. These examples read the sample
391
+ record, so they first set the input directory to the package's data
392
+ directory:
393
+
394
+ ```sh
395
+ export WFDBX_INPUT_DIR=$(python -c "import wfdbx; print(wfdbx.sample_data_dir())")
396
+ wfdbx rdsamp -r 100s -t 10 -p # print the first 10 seconds, in physical units
397
+ wfdbx rdann -r 100s -a atr # print the annotations
398
+ wfdbx --list # list the tools
399
+ ```
400
+
401
+ The tools have the options and output of the original WFDB tools, which are
402
+ described in the *WFDB Applications Guide*
403
+ ([`third_party/wag.pdf`](https://github.com/alphanumericslab/wfdb-x/tree/main/third_party)):
404
+
405
+ * signals: `rdsamp`, `wrsamp`, `wfdbdesc`, `sigamp`, `snip`, `xform`,
406
+ `sampfreq`, `signame`, `signum`, `wfdbcat`, `wfdbwhich`
407
+ * times: `wfdbtime`, `time2sec`
408
+ * annotations: `rdann`, `wrann`, `sortann`, `ann2rr`, `rr2ann`
409
+ * converters: `edf2mit`, `mit2edf`, `rdedfann`, `wav2mit`, `mit2wav`,
410
+ `wfdb2mat`, `a2m`, `m2a`, `ad2m`, `md2a`, `ahaecg2mit`, `makeid`, `readid`,
411
+ `revise`, `parsescp`
412
+
413
+ When an output file or the standard output cannot be written (a full disk,
414
+ for example), a tool reports it and exits with status 1, as the standard
415
+ Unix tools do (the original tools exited with status 0).
416
+
417
+ The tools use the environment variables of the table above. To read record
418
+ 100 of the MIT-BIH Arrhythmia Database from PhysioNet:
419
+
420
+ ```sh
421
+ export WFDBX_INPUT_DIR=https://physionet.org/files/mitdb/1.0.0/
422
+ export WFDBX_NETWORK=1
423
+ wfdbx rdsamp -r 100 -t 0:05
424
+ ```
425
+
426
+ Tools that write signal parameters require them as options:
427
+
428
+ | Tool | Required options |
429
+ |---|---|
430
+ | `wrsamp` (with `-o`) | `-F` frequency, `-G` gain, `-R` ADC resolution, and `-U` units for calibrated signals |
431
+ | `wav2mit` | `-G` gain, and `-U` units unless the gain is 0 |
432
+ | `ad2m`, `ahaecg2mit` | `-F`, `-G`, `-R`, and `-U` unless the gain is 0 |
433
+ | `a2m`, `rdann`, `ann2rr`, `sortann`, `m2a` | `-F` frequency, when the record's header does not give one |
434
+
435
+ For example, `wfdbx wrsamp -F 250 -G 200 -R 16 -U mV -i samples.txt -o rec`
436
+ writes a record, `rec`, from the columns of numbers in the text file
437
+ `samples.txt`.
438
+
439
+ ## The WFDB library functions
440
+
441
+ Every function of the WFDB library is available under its C name, with the
442
+ same arguments and return values, so programs and documentation written for
443
+ the C library carry over directly. Where a C function fills an array, the
444
+ Python function fills a list that you pass. This example reads the sample
445
+ record `100s` (the first minute of record 100 of the MIT-BIH Arrhythmia
446
+ Database, included with WFDB-x):
447
+
448
+ ```python
449
+ import wfdbx as w
450
+
451
+ w.wfdbx_set_input_dir(w.sample_data_dir()) # the directory of 100s
452
+
453
+ si = []
454
+ nsig = w.isigopen("100s", si, 2) # open 2 signals; si receives their specifications
455
+ v = [0] * nsig
456
+ while w.getvec(v) > 0: # v receives one sample per signal
457
+ ...
458
+
459
+ a = w.Annotation()
460
+ w.annopen("100s", [w.Anninfo("atr", w.WFDB_READ)])
461
+ while w.getann(0, a) == 0:
462
+ print(a.time, w.annstr(a.anntyp), a.aux_string())
463
+ w.wfdbquit()
464
+ ```
465
+
466
+ Differences from C that come from the language:
467
+
468
+ * Strings are `str`. Text and binary data are handled as ISO-8859-1, so every
469
+ byte sequence round-trips unchanged.
470
+ * An annotation's `aux` is `None`, or `bytes` whose first byte is the length
471
+ of the text that follows, as in C. `Annotation.aux_string()` returns the
472
+ text.
473
+ * `getframes(n)` reads up to `n` frames at once, as `n` calls of `getvec`
474
+ would, but decoding whole blocks of the signal files (also for
475
+ multi-frequency and multi-segment records). It returns a NumPy array
476
+ with one row per frame, and the `getvec` status of the first frame it
477
+ could not read. If it read all `n` frames, the status is the number of
478
+ signals.
479
+ * `getseginfo()` returns a list of `Seginfo` objects.
480
+ * Errors are reported by return values, as in C, not by exceptions.
481
+ `wfdberror()` returns the last message, and `wfdbquiet()` stops the
482
+ messages from being printed.
483
+ * The library keeps the open record and annotators between calls, as in C.
484
+ `wfdbquit()` closes them; the directory and network settings are kept.
485
+
486
+ The annotation codes (`NORMAL`, `PVC`, ...) and the functions of
487
+ `<wfdb/ecgmap.h>` (`isqrs`, `map1`, `map2`, ...) are also provided. Use
488
+ `help()` on any function for its documentation.
489
+
490
+ ## Legacy mode
491
+
492
+ Legacy mode reproduces the original WFDB package exactly, including its
493
+ assumed signal parameters and its search for files. Use it only to reproduce
494
+ old results:
495
+
496
+ * tools: `--legacy` as the first argument, as in `wfdbx rdsamp --legacy -r 100s`,
497
+ run in the directory of the sample record `100s` (`wfdbx.sample_data_dir()`).
498
+ Legacy mode finds files through the WFDB path, which begins with the
499
+ current directory, and does not use the input directory;
500
+ * high-level functions: `legacy=True`;
501
+ * the whole library: `wfdbx_set_legacy(1)`.
502
+
503
+ [DIVERGENCES.md](https://github.com/alphanumericslab/wfdb-x/blob/main/docs/DIVERGENCES.md#legacy-mode)
504
+ describes what changes in legacy mode.
505
+
506
+ ## Example notebooks
507
+
508
+ The package includes seven Jupyter notebooks. Each runs without a network
509
+ connection.
510
+
511
+ | Notebook | Contents |
512
+ |---|---|
513
+ | `01_getting_started` | reading headers and signals; ADC and physical units |
514
+ | `02_annotations_and_heart_rate` | reading annotations; RR intervals and heart rate |
515
+ | `03_writing_records` | writing records and annotations |
516
+ | `04_command_line_tools` | running the tools |
517
+ | `05_c_library_api` | the WFDB library functions |
518
+ | `06_physionet_records` | reading a record from PhysioNet |
519
+ | `07_legacy_mode` | the original package's behavior, on request |
520
+
521
+ Copy them to a directory and open them with Jupyter:
522
+
523
+ ```sh
524
+ wfdbx --examples my-examples
525
+ cd my-examples
526
+ jupyter lab
527
+ ```
528
+
529
+ ## Notes
530
+
531
+ * **FLAC.** Formats 508, 516 and 524 (FLAC-compressed signals) are read and
532
+ written without any external library. The C library reads files written
533
+ by WFDB-x with identical sample values, although the compressed bytes
534
+ differ from those of libFLAC.
535
+ * **Speed.** Reading and writing whole records is vectorized with NumPy. On
536
+ a typical laptop, `read_record` reads 8 million samples in a few hundredths
537
+ of a second.
538
+
539
+ ## Testing
540
+
541
+ From the root of the repository:
542
+
543
+ ```sh
544
+ pip install -e "./python[test]"
545
+ pytest python/tests
546
+ ```
547
+
548
+ This runs the unit tests, the example notebooks and the cross-language
549
+ conformance suite (about 1000 command-line cases, a few minutes). Set
550
+ `WFDBX_CONFORMANCE=0` to skip the conformance suite, or, for example,
551
+ `WFDBX_CONFORMANCE=rdsamp,rdann` to run the cases of some tools only. The
552
+ conformance suite can also be run on its own, with more options (see
553
+ [conformance.md](https://github.com/alphanumericslab/wfdb-x/blob/main/docs/conformance.md)):
554
+
555
+ ```sh
556
+ python3 tests/conformance/harness.py check --impl python
557
+ ```
558
+
559
+ The tests that write to `/dev/full` (write errors) are skipped where it does
560
+ not exist, on macOS and Windows.
561
+
562
+ ## Authors and license
563
+
564
+ The WFDB library and tools were written by George B. Moody, with revisions by
565
+ Benjamin Moody and contributions from many others; `parsescp` was written by
566
+ George B. Moody and Edna S. Moody. Each module names the authors of the C
567
+ code it implements, and
568
+ [AUTHORS.md](https://github.com/alphanumericslab/wfdb-x/blob/main/docs/AUTHORS.md)
569
+ gives the details. The Python implementation was developed by Reza Sameni
570
+ (2026), through AI-assisted development, cross-language
571
+ verification and unit testing.
572
+
573
+ WFDB-x is an independent reimplementation. Its Python code is a
574
+ translation of the C code of the WFDB Software Package into Python,
575
+ written for WFDB-x; it is not code by the original WFDB developers. It
576
+ follows the original's logic and behavior, with the changes described
577
+ in [DIVERGENCES.md](https://github.com/alphanumericslab/wfdb-x/blob/main/docs/DIVERGENCES.md).
578
+ The authors of the original C code are credited in each file and in
579
+ [AUTHORS.md](https://github.com/alphanumericslab/wfdb-x/blob/main/docs/AUTHORS.md).
580
+
581
+ WFDB-x is distributed under the GNU General Public License, version 3 or
582
+ (at your option) any later version. See
583
+ [LICENSE](https://github.com/alphanumericslab/wfdb-x/blob/main/LICENSE).
@@ -0,0 +1,65 @@
1
+ wfdb_x-1.0.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
2
+ wfdbx/__init__.py,sha256=KoVRYNMkb2zGrSODAbodPPoPX7e683ofsDm9kVKhWZM,4673
3
+ wfdbx/_annot.py,sha256=E8TYGtoHGohE67fZTUeObe35UP0VK3XDRLsYdxckWjU,61333
4
+ wfdbx/_calib.py,sha256=lubvWuQL36J-00KEMT9JWiskx1qd29yCTLH6bATh3XI,7687
5
+ wfdbx/_cstd.py,sha256=GtmLrHeVob5_chaDJ9eGPbM2I9iLb-PR_zhyBfbig74,13125
6
+ wfdbx/_ecgcodes.py,sha256=A_cC4oAHu4L45A5MqlMKClM_oi186yzECasTViduqSg,9171
7
+ wfdbx/_flac.py,sha256=1oG7tsG7dnhrrvIoHoBi19D2SNwApYfDhXSwNe2Q0QM,31241
8
+ wfdbx/_signal.py,sha256=uGOxIQwBl7NW_1iuGhRm8Qv6rxpQ-OrorkH7Zo0-mE0,153042
9
+ wfdbx/_wfdbinit.py,sha256=GkD7bEnPBCgISaxXw-5RFBNtxMIfK4fiQy9BK9Itdy8,2985
10
+ wfdbx/_wfdbio.py,sha256=qYNvrfzu-8o0V7vtnnBTMS2W4sIZ6WZoIOmYurzW_NA,45511
11
+ wfdbx/check.py,sha256=dUnTXYSK5b2aDDoBH_apY2zRRHLM2RYJ0PHCEHU9220,32895
12
+ wfdbx/cli.py,sha256=hVLBvnYEiQQ00LuYIX6xThoqOesz_lmz6wkLbfKGlDo,2287
13
+ wfdbx/highlevel.py,sha256=qSix62aUrAPvBk1RJrD3VWpMbmgTpwMcHG2dbxSM5y0,44385
14
+ wfdbx/resources.py,sha256=b4Wu_p68yChF0UICPJG5wPaGQ5gBqKOn1Mezo_rqQ34,2227
15
+ wfdbx/apps/__init__.py,sha256=jjCNDllQvxczsI1APtIrNmiKlIU4gms4_riHhIVOaSk,1176
16
+ wfdbx/apps/_common.py,sha256=HBjTFhWXn9ltm34EKmaSX1GWi169RzhmT8trt3lCKN0,7588
17
+ wfdbx/apps/a2m.py,sha256=rAyj_yvI-EaeUhKCVUnk5_do5MMzusFx-kCQaxE4gwI,9851
18
+ wfdbx/apps/ad2m.py,sha256=YaBQ06cudGPqMMo7tuFz0_4Q0efCwGbJUA38LD8Gxrc,11704
19
+ wfdbx/apps/ahaecg2mit.py,sha256=SCCdMlVE4_O3bjG9UWOVIq0LGKE-wXwgxSaT2T4cwTE,11296
20
+ wfdbx/apps/ann2rr.py,sha256=JYjINPdvNACpog7xWIM_nFAOxLelZ2MCo7qXBL01YHg,12828
21
+ wfdbx/apps/edf2mit.py,sha256=HwxslfeFaAIyQtPNe5Ma9e5TxwKK3dmiyP8_xP9A-UU,12838
22
+ wfdbx/apps/m2a.py,sha256=dR3sH_1AqZ6nNg18NkJClko0NntQVJOBBm8ILzy0YjU,5205
23
+ wfdbx/apps/makeid.py,sha256=xfH-vgP1Dt3FBCOwoKNRZ47bGMJ0bMehaUzEkMOoiJs,3911
24
+ wfdbx/apps/md2a.py,sha256=ZaalL1kIvx2bpn8TytQc5INWssqZ1zelTjAqoe1DeEE,4152
25
+ wfdbx/apps/mit2edf.py,sha256=TTQH_dhwyKDDOExD5drbIwwdcDWu4rSlLJ8ohGV3GIU,13916
26
+ wfdbx/apps/mit2wav.py,sha256=8XciY_Pdn2jVuO_aubaW-cAsG9SOyugfIx_973ozexI,6360
27
+ wfdbx/apps/parsescp.py,sha256=cLcGK0p9nKAcXtnm1QxC6Vt-gSQ7yVWsWo2YHu78GkY,77588
28
+ wfdbx/apps/rdann.py,sha256=MJyI2A5RoIXX66s16q-lClDhty1tNCjzkOzHjHSnEno,11898
29
+ wfdbx/apps/rdedfann.py,sha256=XbjvIQ3i_gejKuMkh8dWuLaJEVAxGe0u2h-wgnIzS8I,7297
30
+ wfdbx/apps/rdsamp.py,sha256=bVWcJsqe3Rdymbr7nNEr9F3ZK7svnhuKZsvHIqdzkNY,17181
31
+ wfdbx/apps/readid.py,sha256=2MLpgDgOtQHMDmZqpu84ToMf0I9DQfluFn9z1AX3WtI,2707
32
+ wfdbx/apps/revise.py,sha256=h5daiv5y-mSZHNpac0UG0eePHfRGtzB1TFFpMiWN_bI,6706
33
+ wfdbx/apps/rr2ann.py,sha256=6xLkqkBXt5rE56lgZWmgL43DsqikbFPigdcPaoENsVs,7317
34
+ wfdbx/apps/sampfreq.py,sha256=dhhoBtTLNLjyoO6EmBIMLk1bfQ1BCb5r0B36SAuKqb8,2899
35
+ wfdbx/apps/sigamp.py,sha256=pS-8N9A9KjZaoYbtIHvQw3zhQVu2s3i9wb3CmOFZ_j0,12528
36
+ wfdbx/apps/signame.py,sha256=Tt0nUzK_eipKNdEfrCFY3_R8ZwiMA5MaX5IG1S7EaYU,4032
37
+ wfdbx/apps/signum.py,sha256=Y61uOaFqnq9re-xQE-w1sJccEILCVBfcU2kMKIfSVC8,2309
38
+ wfdbx/apps/snip.py,sha256=Ml3Ono-RlgycIzEWroIJyFo8RKg4HKZRrxhsAMFDMlw,13924
39
+ wfdbx/apps/sortann.py,sha256=h3XF1yP_bAs2yLwXTLpRZFNWg5t0eKQp7aGvD9ekUf0,7085
40
+ wfdbx/apps/time2sec.py,sha256=ok1t7fxvss8ZaPQYv5abB1JiD3I1fuofjPeHA4iKL_k,2678
41
+ wfdbx/apps/wav2mit.py,sha256=PquER7pHJAH3dSQnyj58KTfB8rle34u8N7luGKeXghk,7994
42
+ wfdbx/apps/wfdb2mat.py,sha256=T_are4xME8k9Kebc4VUVlg_ef8jsSyDoBZUd7lPR3q0,13362
43
+ wfdbx/apps/wfdbcat.py,sha256=Boi6e1yzgarwVIdjgoohc1voLP8SrnJ5z3JCMGhhSsA,2357
44
+ wfdbx/apps/wfdbdesc.py,sha256=jCOn1Zgm7ZXvKA7mP5RPBXOrUwDOTXuVywoCUNCnKsE,7783
45
+ wfdbx/apps/wfdbtime.py,sha256=twtiJG-Se38IJBFLveiWixymyynSyL6jKBFebTJX9Mc,2930
46
+ wfdbx/apps/wfdbwhich.py,sha256=9Os66RskKMBEwzLXEpRnSJ4Q64KCwO2DCiUxTn4nGT4,3105
47
+ wfdbx/apps/wrann.py,sha256=NxRaaJnCnYKRoJlIJy1Dwfd0DbfhMga3K6iA2kt7ywQ,5167
48
+ wfdbx/apps/wrsamp.py,sha256=t-XqQ9h08eTVI4RZTu1aaoXn_hAtixoV-OZ9Pdm7NjE,20895
49
+ wfdbx/apps/xform.py,sha256=wyCZkpb0ZOO6QSmgzVL00hCX9ciFCgQeGl8JpaWQ-OE,30892
50
+ wfdbx/examples/01_getting_started.ipynb,sha256=dCNseFwdqt6PEkY2FL4Ju5LcpmLNXPsQYnhZ2vKV5z8,6169
51
+ wfdbx/examples/02_annotations_and_heart_rate.ipynb,sha256=KaYqPnc-_davtI8zXBP4WPFguk8VwQKaE3IqPL0utd4,4287
52
+ wfdbx/examples/03_writing_records.ipynb,sha256=EOLes_22DwushFcz7O1jvmfCvjHiMrbIKvImcWGysRQ,4732
53
+ wfdbx/examples/04_command_line_tools.ipynb,sha256=UxIkOYwd6f7oSzSkW8fymFoCBpfHnORp3Hzzeetld_E,4577
54
+ wfdbx/examples/05_c_library_api.ipynb,sha256=p-7yjjf3lvo0OjS4ZKQNM589ZQsP7itNc52Q7tfwMw0,4006
55
+ wfdbx/examples/06_physionet_records.ipynb,sha256=Uqff3cTiBoOj_GzV2JLrR5Tyf-f5CdlrNH_KumOZqk8,3973
56
+ wfdbx/examples/07_legacy_mode.ipynb,sha256=_T9OcyfNLnq9pnvgqtepZWkZXTGsHUl4sDSOSkmjf6o,3757
57
+ wfdbx/sample_data/100s.atr,sha256=I3KZxKJBVa2viNsbNw5BSBcPYzD6reSSLPocRgRODWc,158
58
+ wfdbx/sample_data/100s.dat,sha256=lS_3fx-F8YUqRDVkHLDxC8GqjEhyJkL_s0MulYchHR8,64800
59
+ wfdbx/sample_data/100s.hea,sha256=iLCesUXM-b3R6tBSV9MSIwqbejLyJOesa0O6IrxixcI,198
60
+ wfdbx/sample_data/README.md,sha256=qQA0-FfjtDYRppjL8pq-fIgB9V4L55z_S7mNJc095A0,504
61
+ wfdb_x-1.0.0.dist-info/METADATA,sha256=ZfqXdtwT-rrSUbjVUouzj3P0ge-8Vy3CmW_FoI6Fvt8,26213
62
+ wfdb_x-1.0.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
63
+ wfdb_x-1.0.0.dist-info/entry_points.txt,sha256=sRvdoqU4uHKnRBunERCn3VEgP3ZOgRsQnFPe9JcoCcI,41
64
+ wfdb_x-1.0.0.dist-info/top_level.txt,sha256=hAJl1HDAasm67h7BWMgwSviNO6wCnzTnJa7jE4m9qE0,6
65
+ wfdb_x-1.0.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ wfdbx = wfdbx.cli:main