Skip to content

Add sig and hea stream parameters to rdrecord, rdheader and rdann - #491

Closed
Ivorforce wants to merge 2 commits into
MIT-LCP:mainfrom
Ivorforce:read-stream
Closed

Ivorforce wants to merge 2 commits into
MIT-LCP:mainfrom
Ivorforce:read-stream

Conversation

@Ivorforce

@Ivorforce Ivorforce commented Jun 27, 2024 •

Copy link
Copy Markdown
Contributor

… in case the file is not read from disk.

This is needed if, for example, files are streamed using a file upload dialog versus read from the file system. Personally, I ran into this limitation using Dash's Upload widget.

There are some basic ToDos:

  • Documentation
  • Unit Tests (?)

However, with this PR the code is starting to get a bit messy. I propose the following changes to make it cleaner again (happy to oblige within the PR if accepted):

  • Deprecate no_file field, instead use a None test against sig_data
  • Since all paths end up with a byte stream anyway, separate concerns between file parsing and stream generation. Parsers get streams from loaders, becoming agnostic to file systems and downloads. (Edit: Addressed in Streamline file reading to use io streams #498)

Let me know what you think.

@Ivorforce Ivorforce changed the title Add the ability to pass sig and hea streams to rdrecord and rdheader Add sig and hea stream parameters to rdrecord and rdheader Jun 28, 2024
… rdann, in case the file is not read from disk.
@Ivorforce Ivorforce changed the title Add sig and hea stream parameters to rdrecord and rdheader Add sig and hea stream parameters to rdrecord, rdheader and rdann Jul 1, 2024
@Ivorforce

Ivorforce commented Jul 1, 2024 •

Copy link
Copy Markdown
Contributor Author

Update: I have added the same cabability to rdann.

rdann seems to have an additional problem right now of simply not working. A simple .astype(np.int32) seems to fix the issue, but may not be what's actually needed.
Edit: The astype issue is discussed in #493. I have removed the temporary fix from this PR.)

@Ivorforce Ivorforce mentioned this pull request Jul 4, 2024
2 tasks done
@Ivorforce

Copy link
Copy Markdown
Contributor Author

Update 2: I have opened #498. That PR is a more comprehensive and streamlined rewrite, and thus supersedes it, in my opinion.

@tompollard

Copy link
Copy Markdown
Member

Thanks for this, and sorry it sat for so long. Reading records from memory (e.g. from an upload widget) is a good use case.

Since this PR was opened, the reading code has moved to fsspec, which changes the best way to do this. Rather than adding stream parameters, we could treat fsspec's in-memory filesystem like the other URI schemes:

import fsspec, wfdb
fs = fsspec.filesystem("memory")
fs.pipe("/upload/100.hea", hea_bytes)
fs.pipe("/upload/100.dat", dat_bytes)
record = wfdb.rdrecord("memory://upload/100")

I tried this locally after adding "memory://" to CLOUD_PROTOCOLS, and it read format 212, FLAC, multi-file records and annotations correctly. It also avoids two problems with a single stream: records with signals in more than one .dat file, and multi-segment records.

Would you be interested in opening a PR for that instead (with tests and a docs note)? If so, I think we can close this one and #498.

tompollard added a commit that referenced this pull request Nov 8, 2026
`proc_ann_bytes` and its helpers decode an annotation file by indexing
the Nx2 NumPy byte array one element at a time (`filebytes[bpi, 1]`,
`np.int64(...)`, `.astype("i1")`), and build each aux note with
`"".join(chr(c) for c in ...)`. Each of those makes a NumPy scalar,
which is slow in a Python loop.

This converts the byte pairs to a list once with `.tolist()` and does
the rest with plain ints. Signed `subtype`/`num` bytes are converted
with a small helper, and aux notes are decoded with
`bytes(...).decode("latin-1")`, which gives the same characters as
`chr()` for 0-255. Since no NumPy scalars are involved any more, the
decoding no longer depends on NumPy's scalar type promotion rules, which
caused the `OverflowError` in #493 and #522 (already worked around on
main with `np.int64`).

The output is unchanged: I compared every field (`sample`, `symbol`,
`subtype`, `chan`, `num`, `aux_note`, `fs`, `label_store`, custom
labels, and dtypes) for all 15 annotation files in `sample-data`, with
and without `sampto`, against main.

Benchmark (median, Apple Silicon, alternating runs of main and this
branch; the machine was busy, so the absolute numbers are noisy):

| file | main | this PR | speedup |
| --- | --- | --- | --- |
| `rdann` 100.atr | 4.10 ms | 2.01 ms | 2.0x |
| `rdann` 1003.atr | 6.24 ms | 3.31 ms | 1.9x |
| `rdann` 12726.wabp (many aux notes) | 16.5 ms | 6.4 ms | 2.6x |
| `rdann` 03700181.gqrsl | 2.99 ms | 1.64 ms | 1.8x |
| `rdann`, 100,000 beats with some aux notes (written with `wrann`) |
176 ms | 76 ms | 2.3x |
| `proc_ann_bytes` only, 12726.wabp | 22.4 ms | 7.6 ms | 2.9x |

Tests: added a round-trip test for `subtype`, `chan` and `num` values
(including negative subtypes). `uv run pytest -m "not network"` passes,
`tests/test_annotation.py` passes with network tests, `ruff
format`/`ruff check` are clean, and the docs build without warnings.
Added a changelog entry.

Open PRs #491 and #498 also touch `annotation.py`, but only how the file
is opened and read into bytes, not the decoding, so they shouldn't
conflict beyond nearby lines.
@Ivorforce

Copy link
Copy Markdown
Contributor Author

Thank you for getting back to this @tompollard. I'm happy to hear it's already almost working out of the box so my larger changes from here are no longer needed :)

Closing in favour of #629

@Ivorforce Ivorforce closed this Nov 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants