Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/changes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ Unreleased
**Fix record name and add write directory in csv2ann**
``csv2ann`` now takes the record name from the CSV file name without its directory and extension, as ``csv_to_wfdb`` does. Paths with a directory (such as ``data/x.csv``) used to fail when writing, and ``./x.csv`` gave an empty name. The annotation file is written to the new ``write_dir`` argument, which defaults to the current directory. ``csv2ann`` is now included in the API documentation.

**Speed up the digital range check when writing records**
``wrsamp`` now uses NumPy to check that digital samples are within the range of each format, instead of Python's built-in ``min`` and ``max``. Writing a 4-hour, 8-channel record at 500 Hz in format 16 now takes about 0.5 seconds instead of 3. The written files are unchanged.

**Fix parameter handling in csv_to_wfdb**
``csv_to_wfdb`` now builds the same ``Record`` whether it writes files or returns the record (``record_only=True``), so all header parameters (such as ``dat_file_name``, ``counter_freq``, ``base_counter``, ``skew``, ``byte_offset``, ``adc_res`` and ``adc_zero``) are written instead of being ignored. Single values of any numeric type (including NumPy scalars and floats) are used for all channels, and an explicit ``0`` is no longer replaced by the default. The initial value and checksum are computed from the digital samples, and the default ADC resolution now follows ``fmt`` in both modes. The ``samps_per_frame``, ``init_value`` and ``checksum`` arguments are deprecated and ignored. The parameter descriptions in the docstring have been corrected.

Expand Down
8 changes: 4 additions & 4 deletions src/wfdb/io/_signal.py
Original file line number Diff line number Diff line change
Expand Up @@ -201,8 +201,8 @@ def check_sig_cohesion(self, write_fields, expanded):
fmt = self.fmt[ch]
dmin, dmax = _digi_bounds(self.fmt[ch])

chmin = min(self.e_d_signal[ch])
chmax = max(self.e_d_signal[ch])
chmin = np.min(self.e_d_signal[ch])
chmax = np.max(self.e_d_signal[ch])
if (chmin < dmin) or (chmax > dmax):
raise IndexError(
"Channel "
Expand Down Expand Up @@ -256,8 +256,8 @@ def check_sig_cohesion(self, write_fields, expanded):
fmt = self.fmt[ch]
dmin, dmax = _digi_bounds(self.fmt[ch])

chmin = min(self.d_signal[:, ch])
chmax = max(self.d_signal[:, ch])
chmin = np.min(self.d_signal[:, ch])
chmax = np.max(self.d_signal[:, ch])
if (chmin < dmin) or (chmax > dmax):
raise IndexError(
"Channel "
Expand Down
51 changes: 51 additions & 0 deletions tests/test_record.py
Original file line number Diff line number Diff line change
Expand Up @@ -1384,6 +1384,57 @@ def test_adc_gain_min_boundary(self, temp_path):
np.testing.assert_allclose(record.adc_gain[0], expected_gain, rtol=1e-3)


class TestSigCohesion:
"""Tests for the digital range check in check_sig_cohesion."""

@staticmethod
def _record(d_signal=None, e_d_signal=None, samps_per_frame=(1, 1)):
sig_len = len(d_signal) if d_signal is not None else len(e_d_signal[1])
return wfdb.Record(
record_name="x",
n_sig=2,
fs=100,
sig_len=sig_len,
fmt=["16", "80"],
adc_gain=[200, 200],
baseline=[0, 0],
units=["mV", "mV"],
sig_name=["a", "b"],
samps_per_frame=list(samps_per_frame),
d_signal=d_signal,
e_d_signal=e_d_signal,
)

@pytest.mark.parametrize("value", [-32769, 32768])
def test_out_of_range(self, value):
"""A value outside the format's range is rejected, even at the end."""
d_signal = np.zeros((5, 2), dtype="int64")
d_signal[-1, 0] = value
record = self._record(d_signal=d_signal)

with pytest.raises(IndexError, match="Channel 0 contain values outside"):
record.check_sig_cohesion([], expanded=False)

@pytest.mark.parametrize("value", [-129, 128])
def test_out_of_range_expanded(self, value):
"""Expanded signals are checked channel by channel."""
e_d_signal = [np.zeros(10, dtype="int64"), np.zeros(5, dtype="int64")]
e_d_signal[1][-1] = value
record = self._record(e_d_signal=e_d_signal, samps_per_frame=(2, 1))

with pytest.raises(IndexError, match="Channel 1 contain values outside"):
record.check_sig_cohesion([], expanded=True)

def test_limits_allowed(self):
"""The smallest and largest values of each format are allowed."""
d_signal = np.array([[-32768, -128], [32767, 127]])
self._record(d_signal=d_signal).check_sig_cohesion([], expanded=False)

e_d_signal = [np.array([-32768, 0, 0, 32767]), np.array([-128, 127])]
record = self._record(e_d_signal=e_d_signal, samps_per_frame=(2, 1))
record.check_sig_cohesion([], expanded=True)


class TestDownload:
# Test that we can download records with no "dat" file
# Regression test for https://github.com/MIT-LCP/wfdb-python/issues/118
Expand Down
Loading