Repository navigation
Commit 92e2545
authored
Add support for reading and writing .wfdb archive files (#541)
As discussed in wfdb/wfdb-spec#21 and
#540 we would like to allow
WFDB Records to be saved to a single file `.wfdb`.
This pull request introduces support for reading and writing `.wfdb`
archive files: a ZIP file that holds the header, signal and annotation
files of a single record.
## Implementation
The pull request has three commits:
1. **Reading.** `rdrecord`, `rdsamp`, `rdheader` and `rdann` read from
an archive when given its path, including the `.wfdb` extension (for
example `wfdb.rdrecord("100.wfdb")`).
- The header is the file named after the archive (`100.hea` in
`100.wfdb`), or the only header in the archive. Signal and annotation
files are read by the names that follow from the header, and a missing
file is an error.
- `rdrecord` reads the header and signals from a single open archive.
Archives are opened and closed within each call, so nothing is cached
between calls.
- Headers larger than 1 MB are rejected, as a guard against damaged or
malicious archives.
2. **Writing.** `wrsamp` and `Record.wrsamp` take a `wfdb_archive`
argument: a path, or an open `WFDBArchive`.
- A new archive is written to a temporary file and moved into place only
when writing succeeds. A failed write leaves no partial archive, and an
existing archive at the same path is either fully replaced or left
unchanged.
- New archives get the permissions of the archive they replace, or the
default permissions for new files.
- `create_archive(file_paths, archive_path)` puts the files of an
existing record into an archive.
3. **Docs.** A section on archives in the I/O docs, and a changelog
entry.
Format details:
- **One record per archive.** `WFDBArchive` refuses to write a second
header file, so it's always clear which record an archive holds.
Annotation and other files can be added alongside the record.
- **Compression.** Header files are compressed. Signal files are stored
without compression, so that reading a range of samples doesn't require
decompressing the whole file.
Not supported yet: multi-segment records, and archives in cloud storage
or on PhysioNet. These raise a clear error. Multi-segment support would
extend the one-record rule to "the main header plus the segment headers
it lists", and is planned as a follow-up.
## Example usage
```python
import numpy as np
import wfdb
from wfdb.io.archive import WFDBArchive, create_archive
# Write a record straight into an archive
record = wfdb.rdrecord("sample-data/test01_00s", physical=False)
record.wrsamp(wfdb_archive="test01_00s.wfdb")
# Read it back, in full or in part
archive_record = wfdb.rdrecord("test01_00s.wfdb", physical=False)
np.testing.assert_array_equal(archive_record.d_signal, record.d_signal)
part = wfdb.rdrecord("test01_00s.wfdb", sampfrom=100, sampto=200, channels=[0, 2])
signals, fields = wfdb.rdsamp("test01_00s.wfdb")
header = wfdb.rdheader("test01_00s.wfdb")
# Or with the wrsamp function
wfdb.wrsamp(
"ecg",
fs=250,
units=["mV", "mV"],
sig_name=["I", "II"],
p_signal=np.random.default_rng(0).standard_normal((1000, 2)),
fmt=["16", "16"],
wfdb_archive="ecg.wfdb",
)
# Put an existing record and its annotations into an archive
create_archive(
["sample-data/100.hea", "sample-data/100.dat", "sample-data/100.atr"],
"100.wfdb",
)
annotation = wfdb.rdann("100.wfdb", "atr")
# Add an annotation file while writing a record
with WFDBArchive("100_copy.wfdb", mode="w") as archive:
wfdb.rdrecord("sample-data/100", physical=False).wrsamp(wfdb_archive=archive)
archive.add_file("sample-data/100.atr")
```
Note: `Record.wrsamp` writes the digital signal, so the record is read
with `physical=False` above. Calling it on a record read with physical
signals fails with an unclear `TypeError`. That's existing behaviour,
not specific to archives, and could be improved separately.
## Tests
`tests/test_archive.py` compares records read from archives with the
same records read from separate files, including FLAC formats, multiple
samples per frame and sample ranges. It also covers writing,
annotations, the one-record rule, permissions, failed writes, and error
cases such as missing files and unsupported multi-segment records.8 files changed
Lines changed: 1531 additions & 49 deletions
File tree
- docs
- src/wfdb/io
- tests
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
74 | 74 | | |
75 | 75 | | |
76 | 76 | | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
77 | 80 | | |
78 | 81 | | |
79 | 82 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
34 | 34 | | |
35 | 35 | | |
36 | 36 | | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
37 | 85 | | |
38 | 86 | | |
39 | 87 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
290 | 290 | | |
291 | 291 | | |
292 | 292 | | |
293 | | - | |
| 293 | + | |
294 | 294 | | |
295 | 295 | | |
296 | 296 | | |
| |||
307 | 307 | | |
308 | 308 | | |
309 | 309 | | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
310 | 313 | | |
311 | 314 | | |
312 | 315 | | |
| |||
338 | 341 | | |
339 | 342 | | |
340 | 343 | | |
341 | | - | |
| 344 | + | |
342 | 345 | | |
343 | 346 | | |
344 | 347 | | |
| |||
514 | 517 | | |
515 | 518 | | |
516 | 519 | | |
517 | | - | |
| 520 | + | |
| 521 | + | |
| 522 | + | |
518 | 523 | | |
519 | 524 | | |
520 | 525 | | |
| |||
528 | 533 | | |
529 | 534 | | |
530 | 535 | | |
| 536 | + | |
| 537 | + | |
| 538 | + | |
531 | 539 | | |
532 | 540 | | |
533 | 541 | | |
| |||
572 | 580 | | |
573 | 581 | | |
574 | 582 | | |
575 | | - | |
| 583 | + | |
| 584 | + | |
| 585 | + | |
| 586 | + | |
| 587 | + | |
576 | 588 | | |
577 | 589 | | |
578 | 590 | | |
| |||
0 commit comments