From 6e300747f060f15cb69f9fe6bf7281d9c2431545 Mon Sep 17 00:00:00 2001 From: antonogarkov Date: Mon, 2 Nov 2026 13:43:51 +0300 Subject: [PATCH 1/4] Add Python DataFilter.rereference_channels for EEG/EXG Implements #619 with a Python-only helper that subtracts the mean of selected reference channels from non-excluded channels, plus unit tests and CI coverage. Co-authored-by: Cursor --- .github/workflows/run_unix.yml | 1 + .github/workflows/run_windows.yml | 1 + docs/UserAPI.rst | 5 ++ python_package/brainflow/data_filter.py | 77 +++++++++++++++- .../examples/tests/rereference_channels.py | 90 +++++++++++++++++++ 5 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 python_package/examples/tests/rereference_channels.py diff --git a/.github/workflows/run_unix.yml b/.github/workflows/run_unix.yml index 6a9b02105..6db13acba 100644 --- a/.github/workflows/run_unix.yml +++ b/.github/workflows/run_unix.yml @@ -390,6 +390,7 @@ jobs: sudo -H python3 $GITHUB_WORKSPACE/python_package/examples/tests/decomposition_regression.py sudo -H python3 $GITHUB_WORKSPACE/python_package/examples/tests/signal_metrics_regression.py sudo -H python3 $GITHUB_WORKSPACE/python_package/examples/tests/binding_validation_regression.py + sudo -H python3 $GITHUB_WORKSPACE/python_package/examples/tests/rereference_channels.py - name: Signal Processing Regression Cpp run: $GITHUB_WORKSPACE/cpp_package/examples/signal_processing/build/signal_processing_regression env: diff --git a/.github/workflows/run_windows.yml b/.github/workflows/run_windows.yml index 9a7e4942b..eca737345 100644 --- a/.github/workflows/run_windows.yml +++ b/.github/workflows/run_windows.yml @@ -250,6 +250,7 @@ jobs: python python_package\examples\tests\decomposition_regression.py python python_package\examples\tests\signal_metrics_regression.py python python_package\examples\tests\binding_validation_regression.py + python python_package\examples\tests\rereference_channels.py shell: cmd - name: Signal Processing Regression Csharp run: dotnet run --project csharp_package\brainflow\examples\signal_processing_regression diff --git a/docs/UserAPI.rst b/docs/UserAPI.rst index 4d02c10af..d5ed67591 100644 --- a/docs/UserAPI.rst +++ b/docs/UserAPI.rst @@ -136,6 +136,11 @@ Data files accept rectangular tab- or comma-separated finite numeric values, inc a final line without a newline. Invalid tokens and insufficient destination capacity return errors. Writing uses enough decimal precision to round-trip doubles. +Python ``DataFilter.rereference_channels`` subtracts the element-wise mean of selected +reference channels from every non-excluded channel of a ``(channels, samples)`` array. +It returns a new array and does not mutate the input. Channel indices are zero-based. +This helper is currently implemented only in the Python binding. + Python API Reference ---------------------- diff --git a/python_package/brainflow/data_filter.py b/python_package/brainflow/data_filter.py index e1abd5835..5c871d183 100644 --- a/python_package/brainflow/data_filter.py +++ b/python_package/brainflow/data_filter.py @@ -3,7 +3,7 @@ import os import platform import struct -from typing import List, Tuple +from typing import List, Optional, Tuple, Union import numpy from importlib.resources import files @@ -1587,6 +1587,81 @@ def perform_ica(cls, data, num_components: int, channels=None, *, max_iterations return w, k, a, s + @staticmethod + def _parse_channel_indices(channels, num_channels: int, arg_name: str, + allow_empty: bool = False) -> numpy.ndarray: + if channels is None: + indices = numpy.asarray([], dtype=numpy.int64) + else: + indices = numpy.atleast_1d(numpy.asarray(channels)) + if indices.ndim != 1: + raise ValueError('%s must be a 1D sequence of channel indices' % arg_name) + if indices.size > 0 and not numpy.issubdtype(indices.dtype, numpy.integer): + raise ValueError('%s must contain integer channel indices' % arg_name) + indices = indices.astype(numpy.int64, copy=False) + + if indices.size == 0: + if allow_empty: + return indices + raise ValueError('%s must not be empty' % arg_name) + + if numpy.any(indices < 0) or numpy.any(indices >= num_channels): + raise ValueError('%s contains out-of-range channel index' % arg_name) + + return indices + + @classmethod + def rereference_channels(cls, data, reference: Union[List, Tuple, numpy.ndarray], + excluded_channels: Optional[Union[List, Tuple, numpy.ndarray]] = None): + """re-reference EEG/EXG channels by subtracting a shared reference signal + + Computes an element-wise mean across the selected reference channels and + subtracts that reference from every non-excluded channel. This is useful for + common average referencing and similar EEG/EXG re-referencing workflows. + Channel indices are zero-based. The input array is not modified. + + :param data: 2d array with shape (channels, samples) + :type data: NDArray[Shape["*, *"], Float64] + :param reference: zero-based indices of channels used to compute the reference + :type reference: list or tuple or ndarray + :param excluded_channels: zero-based indices of channels that remain unchanged, empty by default + :type excluded_channels: list or tuple or ndarray or None + :return: new 2d array with the same shape as ``data`` + :rtype: NDArray[Shape["*, *"], Float64] + :raises ValueError: if ``data`` is not a non-empty 2d array, ``reference`` is empty, + any channel index is out of range, reference indices are duplicated, or a channel + appears in both ``reference`` and ``excluded_channels`` + """ + data_arr = numpy.asarray(data) + if data_arr.ndim != 2: + raise ValueError('data must be a 2D array with shape (channels, samples)') + num_channels, num_samples = data_arr.shape + if num_channels == 0 or num_samples == 0: + raise ValueError('data must have at least one channel and one sample') + + ref_indices = cls._parse_channel_indices(reference, num_channels, 'reference') + if ref_indices.size != numpy.unique(ref_indices).size: + raise ValueError('reference must not contain duplicate indices') + + excl_indices = cls._parse_channel_indices( + excluded_channels, num_channels, 'excluded_channels', allow_empty=True) + if numpy.intersect1d(ref_indices, excl_indices).size > 0: + raise ValueError( + 'a channel cannot appear in both reference and excluded_channels') + + work = data_arr.astype(numpy.float64, copy=True) + reference_signal = numpy.mean(work[ref_indices], axis=0) + update_mask = numpy.ones(num_channels, dtype=bool) + update_mask[excl_indices] = False + work[update_mask] -= reference_signal + + if numpy.issubdtype(data_arr.dtype, numpy.floating): + result = work.astype(data_arr.dtype, copy=False) + if excl_indices.size > 0: + result[excl_indices] = data_arr[excl_indices] + return result + return work + @classmethod def perform_ifft(cls, data): """perform inverse fft diff --git a/python_package/examples/tests/rereference_channels.py b/python_package/examples/tests/rereference_channels.py new file mode 100644 index 000000000..034f03ee8 --- /dev/null +++ b/python_package/examples/tests/rereference_channels.py @@ -0,0 +1,90 @@ +"""Unit tests for DataFilter.rereference_channels.""" + +import unittest + +import numpy as np + +from brainflow.data_filter import DataFilter + + +class RereferenceChannelsTest(unittest.TestCase): + def setUp(self): + self.data = np.array([ + [1.0, 2.0, 3.0], + [4.0, 5.0, 6.0], + [7.0, 8.0, 9.0], + [10.0, 11.0, 12.0], + ], dtype=np.float64) + + def test_average_reference_across_two_channels(self): + result = DataFilter.rereference_channels(self.data, [0, 1]) + reference = np.array([2.5, 3.5, 4.5]) + expected = self.data - reference + np.testing.assert_allclose(result, expected) + + def test_average_reference_across_three_channels(self): + result = DataFilter.rereference_channels(self.data, [0, 1, 2]) + reference = np.array([4.0, 5.0, 6.0]) + expected = self.data - reference + np.testing.assert_allclose(result, expected) + + def test_reference_to_single_channel(self): + result = DataFilter.rereference_channels(self.data, [1]) + expected = self.data - self.data[1] + np.testing.assert_allclose(result, expected) + np.testing.assert_array_equal(result[1], np.zeros(3)) + + def test_multiple_reference_channels(self): + result = DataFilter.rereference_channels(self.data, [0, 2, 3]) + reference = np.mean(self.data[[0, 2, 3]], axis=0) + expected = self.data - reference + np.testing.assert_allclose(result, expected) + + def test_excluded_channels_remain_unchanged(self): + result = DataFilter.rereference_channels(self.data, [0, 1], excluded_channels=[2, 3]) + reference = np.array([2.5, 3.5, 4.5]) + expected = self.data.copy() + expected[0] -= reference + expected[1] -= reference + np.testing.assert_allclose(result, expected) + np.testing.assert_array_equal(result[2], self.data[2]) + np.testing.assert_array_equal(result[3], self.data[3]) + + def test_input_array_is_not_mutated(self): + original = self.data.copy() + DataFilter.rereference_channels(self.data, [0, 1], excluded_channels=[3]) + np.testing.assert_array_equal(self.data, original) + + def test_output_shape_equals_input_shape(self): + result = DataFilter.rereference_channels(self.data, [0, 1]) + self.assertEqual(result.shape, self.data.shape) + + def test_invalid_channel_index_raises(self): + with self.assertRaises(ValueError): + DataFilter.rereference_channels(self.data, [0, 4]) + with self.assertRaises(ValueError): + DataFilter.rereference_channels(self.data, [0], excluded_channels=[4]) + with self.assertRaises(ValueError): + DataFilter.rereference_channels(self.data, [-1]) + + def test_empty_reference_channels_raises(self): + with self.assertRaises(ValueError): + DataFilter.rereference_channels(self.data, []) + + def test_duplicate_reference_channels_raise(self): + with self.assertRaises(ValueError): + DataFilter.rereference_channels(self.data, [0, 1, 0]) + + def test_channel_cannot_be_both_reference_and_excluded(self): + with self.assertRaises(ValueError): + DataFilter.rereference_channels(self.data, [0, 1], excluded_channels=[1]) + + def test_non_2d_input_raises(self): + with self.assertRaises(ValueError): + DataFilter.rereference_channels(np.array([1.0, 2.0, 3.0]), [0]) + with self.assertRaises(ValueError): + DataFilter.rereference_channels(np.zeros((2, 2, 2)), [0]) + + +if __name__ == '__main__': + unittest.main() From f1957c1d5620b340b9aed1c9d5bc143e7eefd7af Mon Sep 17 00:00:00 2001 From: antonogarkov Date: Tue, 3 Nov 2026 17:13:42 +0300 Subject: [PATCH 2/4] Implement rereference_channels in DataHandler and bindings Move channel re-referencing from a Python-only helper into the native DataHandler C API with an in-place channels/reference_channels API, then expose it through C++ and the other language bindings per review on #619. Co-authored-by: Cursor --- .../src/signal_processing_regression.cpp | 36 ++++++ cpp_package/src/data_filter.cpp | 22 ++++ cpp_package/src/inc/data_filter.h | 10 ++ .../brainflow/brainflow/data_filter.cs | 28 +++++ .../brainflow/data_handler_library.cs | 17 +++ docs/UserAPI.rst | 8 +- .../src/main/java/brainflow/DataFilter.java | 38 ++++++ julia_package/brainflow/src/data_filter.jl | 20 ++++ matlab_package/brainflow/DataFilter.m | 16 +++ nodejs_package/brainflow/data_filter.ts | 26 +++++ nodejs_package/brainflow/functions.types.ts | 4 + python_package/brainflow/data_filter.py | 108 ++++++------------ .../examples/tests/rereference_channels.py | 89 +++++++-------- rust_package/brainflow/src/data_filter.rs | 40 +++++++ .../brainflow/src/ffi/data_handler.rs | 11 ++ src/data_handler/inc/data_handler.h | 4 + src/data_handler/signal_processing.cpp | 53 +++++++++ .../Sources/BrainFlow/DataFilter.swift | 43 +++++++ 18 files changed, 451 insertions(+), 122 deletions(-) diff --git a/cpp_package/examples/signal_processing/src/signal_processing_regression.cpp b/cpp_package/examples/signal_processing/src/signal_processing_regression.cpp index 5b882849c..ee233fd8c 100644 --- a/cpp_package/examples/signal_processing/src/signal_processing_regression.cpp +++ b/cpp_package/examples/signal_processing/src/signal_processing_regression.cpp @@ -71,5 +71,41 @@ int main () lengths[0]++; reject ( [&] { DataFilter::perform_inverse_wavelet_transform (coefficients, lengths, 256, 3, 3); }); + + BrainFlowArray reref_data (4, 3); + double values[12] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}; + for (int i = 0; i < 4; i++) + for (int j = 0; j < 3; j++) + reref_data.at (i, j) = values[i * 3 + j]; + DataFilter::rereference_channels (reref_data, {0, 1}, {0, 1}); + for (int j = 0; j < 3; j++) + if (std::abs (reref_data.at (0, j) + 1.5) > 1e-12 || + std::abs (reref_data.at (1, j) - 1.5) > 1e-12 || + std::abs (reref_data.at (2, j) - values[6 + j]) > 1e-12) + throw std::runtime_error ("Two-channel average reference failed"); + + for (int i = 0; i < 4; i++) + for (int j = 0; j < 3; j++) + reref_data.at (i, j) = values[i * 3 + j]; + DataFilter::rereference_channels (reref_data, {0, 1, 2, 3}, {1}); + for (int j = 0; j < 3; j++) + if (std::abs (reref_data.at (1, j)) > 1e-12 || + std::abs (reref_data.at (0, j) - (values[j] - values[3 + j])) > 1e-12) + throw std::runtime_error ("Single-channel reference failed"); + + for (int i = 0; i < 4; i++) + for (int j = 0; j < 3; j++) + reref_data.at (i, j) = values[i * 3 + j]; + DataFilter::rereference_channels (reref_data, {0, 1, 2, 3}, {0, 1, 2, 3}); + for (int j = 0; j < 3; j++) + { + double mean = (values[j] + values[3 + j] + values[6 + j] + values[9 + j]) / 4.0; + for (int i = 0; i < 4; i++) + if (std::abs (reref_data.at (i, j) - (values[i * 3 + j] - mean)) > 1e-12) + throw std::runtime_error ("Common average reference failed"); + } + reject ([&] { DataFilter::rereference_channels (reref_data, {0}, {4}); }); + reject ([&] { DataFilter::rereference_channels (reref_data, {0, 0}, {1}); }); + std::cout << "Signal-processing binding validation passed." << std::endl; } diff --git a/cpp_package/src/data_filter.cpp b/cpp_package/src/data_filter.cpp index aa673269a..8114f7e04 100644 --- a/cpp_package/src/data_filter.cpp +++ b/cpp_package/src/data_filter.cpp @@ -670,6 +670,28 @@ double *DataFilter::get_activity_index (const double *accel_x, const double *acc return output; } +void DataFilter::rereference_channels (BrainFlowArray &data, + const std::vector &channels, const std::vector &reference_channels) +{ + validate_channels (data, channels); + validate_channels (data, reference_channels); + if (channels.size () > (size_t)std::numeric_limits::max () || + reference_channels.size () > (size_t)std::numeric_limits::max ()) + { + throw BrainFlowException ( + "invalid channel dimensions", (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR); + } + std::vector channels_copy (channels.begin (), channels.end ()); + std::vector reference_copy (reference_channels.begin (), reference_channels.end ()); + int res = ::rereference_channels (data.get_raw_ptr (), data.get_size (0), data.get_size (1), + channels_copy.data (), (int)channels_copy.size (), reference_copy.data (), + (int)reference_copy.size ()); + if (res != (int)BrainFlowExitCodes::STATUS_OK) + { + throw BrainFlowException ("failed to rereference channels", res); + } +} + std::string DataFilter::get_version () { char version[64]; diff --git a/cpp_package/src/inc/data_filter.h b/cpp_package/src/inc/data_filter.h index 654cf4a61..c382c1c17 100644 --- a/cpp_package/src/inc/data_filter.h +++ b/cpp_package/src/inc/data_filter.h @@ -273,6 +273,16 @@ class DataFilter double noise_var_x = 0.0, double noise_var_y = 0.0, double noise_var_z = 0.0, int *output_len = NULL); + /** + * re-reference selected EEG/EXG channels in-place + * @param data 2d array with shape (channels, samples), modified in-place + * @param channels zero-based channel indices to update + * @param reference_channels zero-based channel indices used to compute the mean reference + * Overlap between channels and reference_channels is allowed (common average reference). + */ + static void rereference_channels (BrainFlowArray &data, + const std::vector &channels, const std::vector &reference_channels); + /// get brainflow version static std::string get_version (); }; diff --git a/csharp_package/brainflow/brainflow/data_filter.cs b/csharp_package/brainflow/brainflow/data_filter.cs index 7b8a419cb..8dd6630b5 100644 --- a/csharp_package/brainflow/brainflow/data_filter.cs +++ b/csharp_package/brainflow/brainflow/data_filter.cs @@ -794,6 +794,34 @@ public static Tuple perform_ica (dou return return_data; } + public static void rereference_channels (double[,] data, int[] channels, int[] reference_channels) + { + if (data == null || channels == null || reference_channels == null) + { + throw new BrainFlowError ((int)BrainFlowExitCodes.INVALID_ARGUMENTS_ERROR); + } + int rows = data.GetLength (0); + int cols = data.GetLength (1); + if (rows <= 0 || cols <= 0 || (long)rows * cols > int.MaxValue) + { + throw new BrainFlowError ((int)BrainFlowExitCodes.INVALID_ARGUMENTS_ERROR); + } + double[] data_1d = data.Flatten (); + int res = DataHandlerLibrary.rereference_channels (data_1d, rows, cols, channels, channels.Length, + reference_channels, reference_channels.Length); + if (res != (int)BrainFlowExitCodes.STATUS_OK) + { + throw new BrainFlowError (res); + } + for (int i = 0; i < rows; i++) + { + for (int j = 0; j < cols; j++) + { + data[i, j] = data_1d[i * cols + j]; + } + } + } + /// /// Calculate normalized mean powers for bands 2-4, 4-8, 8-13, 13-30, 30-45 Hz. /// Uses get_custom_band_powers preprocessing and minimum retained data length. diff --git a/csharp_package/brainflow/brainflow/data_handler_library.cs b/csharp_package/brainflow/brainflow/data_handler_library.cs index 134cd5fc8..ec5fbe956 100644 --- a/csharp_package/brainflow/brainflow/data_handler_library.cs +++ b/csharp_package/brainflow/brainflow/data_handler_library.cs @@ -190,6 +190,8 @@ public static extern int perform_wavelet_denoising (double[] data, int data_len, [DllImport ("DataHandler", SetLastError = true, CallingConvention = CallingConvention.Cdecl)] public static extern int perform_ica (double[] data, int rows, int cols, int num_components, double[] w, double[] k, double[] a, double[] s); [DllImport ("DataHandler", SetLastError = true, CallingConvention = CallingConvention.Cdecl)] + public static extern int rereference_channels (double[] data, int rows, int cols, int[] channels, int channels_len, int[] reference_channels, int reference_channels_len); + [DllImport ("DataHandler", SetLastError = true, CallingConvention = CallingConvention.Cdecl)] public static extern int get_activity_index (double[] accel_x, double[] accel_y, double[] accel_z, int data_len, int sampling_rate, int period, double noise_var_x, double noise_var_y, double noise_var_z, double[] output); // unsafe methods working with pointers [DllImport ("DataHandler", SetLastError = true, CallingConvention = CallingConvention.Cdecl)] @@ -304,6 +306,8 @@ public static extern int perform_wavelet_denoising (double[] data, int data_len, [DllImport ("DataHandler32", SetLastError = true, CallingConvention = CallingConvention.Cdecl)] public static extern int perform_ica (double[] data, int rows, int cols, int num_components, double[] w, double[] k, double[] a, double[] s); [DllImport ("DataHandler32", SetLastError = true, CallingConvention = CallingConvention.Cdecl)] + public static extern int rereference_channels (double[] data, int rows, int cols, int[] channels, int channels_len, int[] reference_channels, int reference_channels_len); + [DllImport ("DataHandler32", SetLastError = true, CallingConvention = CallingConvention.Cdecl)] public static extern int get_activity_index (double[] accel_x, double[] accel_y, double[] accel_z, int data_len, int sampling_rate, int period, double noise_var_x, double noise_var_y, double noise_var_z, double[] output); // unsafe methods working with pointers [DllImport ("DataHandler32", SetLastError = true, CallingConvention = CallingConvention.Cdecl)] @@ -397,6 +401,19 @@ public static int perform_ica (double[] data, int rows, int cols, int num_compon return (int)BrainFlowExitCodes.GENERAL_ERROR; } + public static int rereference_channels (double[] data, int rows, int cols, int[] channels, int channels_len, int[] reference_channels, int reference_channels_len) + { + switch (PlatformHelper.get_library_environment ()) + { + case LibraryEnvironment.x64: + return DataHandlerLibrary64.rereference_channels (data, rows, cols, channels, channels_len, reference_channels, reference_channels_len); + case LibraryEnvironment.x86: + return DataHandlerLibrary32.rereference_channels (data, rows, cols, channels, channels_len, reference_channels, reference_channels_len); + } + + return (int)BrainFlowExitCodes.GENERAL_ERROR; + } + public static int get_activity_index (double[] accel_x, double[] accel_y, double[] accel_z, int data_len, int sampling_rate, int period, double noise_var_x, double noise_var_y, double noise_var_z, double[] output) { switch (PlatformHelper.get_library_environment ()) diff --git a/docs/UserAPI.rst b/docs/UserAPI.rst index d5ed67591..265b2a499 100644 --- a/docs/UserAPI.rst +++ b/docs/UserAPI.rst @@ -136,10 +136,10 @@ Data files accept rectangular tab- or comma-separated finite numeric values, inc a final line without a newline. Invalid tokens and insufficient destination capacity return errors. Writing uses enough decimal precision to round-trip doubles. -Python ``DataFilter.rereference_channels`` subtracts the element-wise mean of selected -reference channels from every non-excluded channel of a ``(channels, samples)`` array. -It returns a new array and does not mutate the input. Channel indices are zero-based. -This helper is currently implemented only in the Python binding. +``DataFilter.rereference_channels`` subtracts the element-wise mean of +``reference_channels`` from the selected ``channels`` of a ``(channels, samples)`` +array in place. Channel indices are zero-based. Overlap between the two index lists +is allowed and supports common average referencing. Python API Reference ---------------------- diff --git a/java_package/brainflow/src/main/java/brainflow/DataFilter.java b/java_package/brainflow/src/main/java/brainflow/DataFilter.java index ffa6b227f..32e7968cb 100644 --- a/java_package/brainflow/src/main/java/brainflow/DataFilter.java +++ b/java_package/brainflow/src/main/java/brainflow/DataFilter.java @@ -137,6 +137,9 @@ int get_heart_rate (double[] ppg_ir, double[] ppg_red, int len, int sampling_rat int perform_ica (double[] data, int rows, int cols, int num_components, double[] w, double[] k, double[] a, double[] s); + int rereference_channels (double[] data, int rows, int cols, int[] channels, int channels_len, + int[] reference_channels, int reference_channels_len); + int get_activity_index (double[] accel_x, double[] accel_y, double[] accel_z, int data_len, int sampling_rate, int period, double noise_var_x, double noise_var_y, double noise_var_z, double[] output); @@ -988,6 +991,41 @@ public static List perform_ica (double[][] data, int num_components, return res; } + public static void rereference_channels (double[][] data, int[] channels, int[] reference_channels) + throws BrainFlowError + { + if (data == null || data.length == 0 || channels == null || reference_channels == null) + { + throw new BrainFlowError ("invalid args for rereference_channels", + BrainFlowExitCode.INVALID_ARGUMENTS_ERROR.get_code ()); + } + int cols = data[0].length; + for (int i = 0; i < data.length; i++) + { + if (data[i] == null || data[i].length != cols) + { + throw new BrainFlowError ("invalid args for rereference_channels", + BrainFlowExitCode.INVALID_ARGUMENTS_ERROR.get_code ()); + } + } + int rows = data.length; + double[] data_1d = new double[checked_product (rows, cols)]; + for (int i = 0; i < rows; i++) + { + System.arraycopy (data[i], 0, data_1d, i * cols, cols); + } + int ec = instance.rereference_channels (data_1d, rows, cols, channels, channels.length, + reference_channels, reference_channels.length); + if (ec != BrainFlowExitCode.STATUS_OK.get_code ()) + { + throw new BrainFlowError ("Failed to rereference_channels", ec); + } + for (int i = 0; i < rows; i++) + { + System.arraycopy (data_1d, i * cols, data[i], 0, cols); + } + } + /** * get PSD * diff --git a/julia_package/brainflow/src/data_filter.jl b/julia_package/brainflow/src/data_filter.jl index d2594c23e..fd76db4bd 100644 --- a/julia_package/brainflow/src/data_filter.jl +++ b/julia_package/brainflow/src/data_filter.jl @@ -540,6 +540,26 @@ function perform_ica(data, num_components::Integer) return perform_ica_select_channels(data, num_components, channels) end +@brainflow_rethrow function rereference_channels(data, channels, reference_channels) + if !(data isa StridedMatrix{Float64}) + throw(BrainFlowError("In-place processing requires a StridedMatrix{Float64}", Integer(INVALID_ARGUMENTS_ERROR))) + end + shape = size(data) + if !(0 < length(data) <= typemax(Cint)) || prod(shape) > typemax(Cint) + throw(BrainFlowError("Expected a nonempty real matrix", Integer(INVALID_ARGUMENTS_ERROR))) + end + channels = _dsp_channels(data, channels) + reference_channels = _dsp_channels(data, reference_channels) + channels_c = Int32.(channels .- 1) + reference_c = Int32.(reference_channels .- 1) + data_1d = copy(reshape(transpose(data), (1, prod(shape)))) + ccall((:rereference_channels, DATA_HANDLER_INTERFACE), Cint, + (Ptr{Float64}, Cint, Cint, Ptr{Int32}, Cint, Ptr{Int32}, Cint), + data_1d, Int32(shape[1]), Int32(shape[2]), channels_c, length(channels_c), reference_c, length(reference_c)) + copyto!(data, transpose(reshape(data_1d, shape[2], shape[1]))) + return +end + @brainflow_rethrow function get_psd(data, sampling_rate::Integer, window::WinType) data = _dsp_signal(data) diff --git a/matlab_package/brainflow/DataFilter.m b/matlab_package/brainflow/DataFilter.m index 4e8dc2345..71cbbb5f6 100644 --- a/matlab_package/brainflow/DataFilter.m +++ b/matlab_package/brainflow/DataFilter.m @@ -597,6 +597,22 @@ function write_file(data, file_name, file_mode) DataFilter.check_ec(exit_code, task_name); output = temp_output.Value; end + + function data = rereference_channels(data, channels, reference_channels) + % re-reference selected channels in-place + channels = DataFilter.checked_channels(data, channels); + reference_channels = DataFilter.checked_channels(data, reference_channels); + task_name = 'rereference_channels'; + transposed = transpose(data); + flatten = transpose(transposed(:)); + temp_input = libpointer('doublePtr', flatten); + temp_channels = libpointer('int32Ptr', int32(channels - 1)); + temp_reference = libpointer('int32Ptr', int32(reference_channels - 1)); + lib_name = DataFilter.load_lib(); + exit_code = calllib(lib_name, task_name, temp_input, size(data, 1), size(data, 2), temp_channels, numel(channels), temp_reference, numel(reference_channels)); + DataFilter.check_ec(exit_code, task_name); + data = reshape(temp_input.Value, [size(data, 2), size(data, 1)])'; + end end diff --git a/nodejs_package/brainflow/data_filter.ts b/nodejs_package/brainflow/data_filter.ts index a872308b5..ff8bc0798 100644 --- a/nodejs_package/brainflow/data_filter.ts +++ b/nodejs_package/brainflow/data_filter.ts @@ -66,6 +66,7 @@ class DataHandlerDLL extends DataHandlerFunctions this.detectPeaksZScore = this.lib.func(CLike.detect_peaks_z_score); this.performIca = this.lib.func(CLike.perform_ica); this.getActivityIndex = this.lib.func(CLike.get_activity_index); + this.rereferenceChannels = this.lib.func(CLike.rereference_channels); this.getCsp = this.lib.func(CLike.get_csp); this.detrend = this.lib.func(CLike.detrend); this.calcStddev = this.lib.func(CLike.calc_stddev); @@ -783,4 +784,29 @@ export class DataFilter } return output; } + + public static rereferenceChannels( + data: number[][], channels: number[], referenceChannels: number[]): void + { + const columns = DataFilter.checkMatrix(data, true); + DataFilter.checkChannels(data, channels, columns); + DataFilter.checkChannels(data, referenceChannels, columns); + const flat = data.flat(); + const res = DataHandlerDLL.getInstance().rereferenceChannels( + flat, data.length, columns, channels, channels.length, referenceChannels, + referenceChannels.length); + if (res !== BrainFlowExitCodes.STATUS_OK) + { + throw new BrainFlowError (res, 'Could not rereference channels'); + } + var i: number; + var j: number; + for (i = 0; i < data.length; i++) + { + for (j = 0; j < columns; j++) + { + data[i][j] = flat[i * columns + j]; + } + } + } } diff --git a/nodejs_package/brainflow/functions.types.ts b/nodejs_package/brainflow/functions.types.ts index 2a2c41d45..669005563 100644 --- a/nodejs_package/brainflow/functions.types.ts +++ b/nodejs_package/brainflow/functions.types.ts @@ -372,6 +372,8 @@ export enum DataHandlerCLikeFunctions { 'int perform_ica (double *data, int rows, int cols, int num_components, _Inout_ double *w_mat, _Inout_ double *k_mat, _Inout_ double *a_mat, _Inout_ double *s_mat)', get_activity_index = 'int get_activity_index (double *accel_x, double *accel_y, double *accel_z, int data_len, int sampling_rate, int period, double noise_var_x, double noise_var_y, double noise_var_z, _Inout_ double *activity_index)', + rereference_channels = + 'int rereference_channels (_Inout_ double *data, int rows, int cols, int *channels, int channels_len, int *reference_channels, int reference_channels_len)', get_csp = 'int get_csp (const double *data, const double *labels, int n_epochs, int n_channels, int n_times, _Inout_ double *output_w, _Inout_ double *output_d)', get_railed_percentage = @@ -450,6 +452,8 @@ export class DataHandlerFunctions getActivityIndex!: (accelX: number[], accelY: number[], accelZ: number[], dataLen: number, samplingRate: number, period: number, noiseVarX: number, noiseVarY: number, noiseVarZ: number, activityIndex: number[]) => BrainFlowExitCodes; + rereferenceChannels!: (data: number[], rows: number, cols: number, channels: number[], + channelsLen: number, referenceChannels: number[], referenceChannelsLen: number) => BrainFlowExitCodes; getCsp!: (data: number[], labels: number[], nEpochs: number, nChannels: number, nTimes: number, outputW: number[], outputD: number[]) => BrainFlowExitCodes; detrend!: (rawData: number[], dataLen: number, detrendOperation: number) => BrainFlowExitCodes; diff --git a/python_package/brainflow/data_filter.py b/python_package/brainflow/data_filter.py index 5c871d183..186f5ce6d 100644 --- a/python_package/brainflow/data_filter.py +++ b/python_package/brainflow/data_filter.py @@ -3,7 +3,7 @@ import os import platform import struct -from typing import List, Optional, Tuple, Union +from typing import List, Tuple import numpy from importlib.resources import files @@ -486,6 +486,18 @@ def __init__(self): self.perform_ica_with_options.argtypes = self.perform_ica.argtypes + [ ctypes.c_int, ctypes.c_double, ctypes.c_int] + self.rereference_channels = self.lib.rereference_channels + self.rereference_channels.restype = ctypes.c_int + self.rereference_channels.argtypes = [ + ndpointer(ctypes.c_double, flags=('C_CONTIGUOUS', 'WRITEABLE')), + ctypes.c_int, + ctypes.c_int, + ndpointer(ctypes.c_int32), + ctypes.c_int, + ndpointer(ctypes.c_int32), + ctypes.c_int, + ] + self.create_streaming_filter = self.lib.create_streaming_filter self.create_streaming_filter.restype = ctypes.c_int self.create_streaming_filter.argtypes = [ @@ -1587,80 +1599,36 @@ def perform_ica(cls, data, num_components: int, channels=None, *, max_iterations return w, k, a, s - @staticmethod - def _parse_channel_indices(channels, num_channels: int, arg_name: str, - allow_empty: bool = False) -> numpy.ndarray: - if channels is None: - indices = numpy.asarray([], dtype=numpy.int64) - else: - indices = numpy.atleast_1d(numpy.asarray(channels)) - if indices.ndim != 1: - raise ValueError('%s must be a 1D sequence of channel indices' % arg_name) - if indices.size > 0 and not numpy.issubdtype(indices.dtype, numpy.integer): - raise ValueError('%s must contain integer channel indices' % arg_name) - indices = indices.astype(numpy.int64, copy=False) - - if indices.size == 0: - if allow_empty: - return indices - raise ValueError('%s must not be empty' % arg_name) - - if numpy.any(indices < 0) or numpy.any(indices >= num_channels): - raise ValueError('%s contains out-of-range channel index' % arg_name) - - return indices - @classmethod - def rereference_channels(cls, data, reference: Union[List, Tuple, numpy.ndarray], - excluded_channels: Optional[Union[List, Tuple, numpy.ndarray]] = None): - """re-reference EEG/EXG channels by subtracting a shared reference signal + def rereference_channels(cls, data, channels: List, reference_channels: List) -> None: + """re-reference selected EEG/EXG channels in-place - Computes an element-wise mean across the selected reference channels and - subtracts that reference from every non-excluded channel. This is useful for - common average referencing and similar EEG/EXG re-referencing workflows. - Channel indices are zero-based. The input array is not modified. + Computes an element-wise mean across ``reference_channels`` and subtracts that + reference from each channel listed in ``channels``. Overlap between the two lists + is allowed and is the usual pattern for common average referencing. Channel indices + are zero-based. - :param data: 2d array with shape (channels, samples) + :param data: writable float64 2d array with shape (channels, samples) :type data: NDArray[Shape["*, *"], Float64] - :param reference: zero-based indices of channels used to compute the reference - :type reference: list or tuple or ndarray - :param excluded_channels: zero-based indices of channels that remain unchanged, empty by default - :type excluded_channels: list or tuple or ndarray or None - :return: new 2d array with the same shape as ``data`` - :rtype: NDArray[Shape["*, *"], Float64] - :raises ValueError: if ``data`` is not a non-empty 2d array, ``reference`` is empty, - any channel index is out of range, reference indices are duplicated, or a channel - appears in both ``reference`` and ``excluded_channels`` + :param channels: zero-based indices of channels to update + :type channels: List + :param reference_channels: zero-based indices used to compute the mean reference + :type reference_channels: List """ - data_arr = numpy.asarray(data) - if data_arr.ndim != 2: - raise ValueError('data must be a 2D array with shape (channels, samples)') - num_channels, num_samples = data_arr.shape - if num_channels == 0 or num_samples == 0: - raise ValueError('data must have at least one channel and one sample') - - ref_indices = cls._parse_channel_indices(reference, num_channels, 'reference') - if ref_indices.size != numpy.unique(ref_indices).size: - raise ValueError('reference must not contain duplicate indices') - - excl_indices = cls._parse_channel_indices( - excluded_channels, num_channels, 'excluded_channels', allow_empty=True) - if numpy.intersect1d(ref_indices, excl_indices).size > 0: - raise ValueError( - 'a channel cannot appear in both reference and excluded_channels') - - work = data_arr.astype(numpy.float64, copy=True) - reference_signal = numpy.mean(work[ref_indices], axis=0) - update_mask = numpy.ones(num_channels, dtype=bool) - update_mask[excl_indices] = False - work[update_mask] -= reference_signal - - if numpy.issubdtype(data_arr.dtype, numpy.floating): - result = work.astype(data_arr.dtype, copy=False) - if excl_indices.size > 0: - result[excl_indices] = data_arr[excl_indices] - return result - return work + check_memory_layout_row_major(data, 2) + if data.dtype != numpy.float64 or not data.flags.writeable: + raise BrainFlowError('data must be a writable float64 array', + BrainFlowExitCodes.INVALID_ARGUMENTS_ERROR.value) + channels_arr = numpy.asarray(channels, dtype=numpy.int32) + reference_arr = numpy.asarray(reference_channels, dtype=numpy.int32) + if channels_arr.ndim != 1 or reference_arr.ndim != 1: + raise BrainFlowError('channel index arrays must be 1d', + BrainFlowExitCodes.INVALID_ARGUMENTS_ERROR.value) + res = DataHandlerDLL.get_instance().rereference_channels( + data, data.shape[0], data.shape[1], channels_arr, channels_arr.size, + reference_arr, reference_arr.size) + if res != BrainFlowExitCodes.STATUS_OK.value: + raise BrainFlowError('unable to rereference channels', res) @classmethod def perform_ifft(cls, data): diff --git a/python_package/examples/tests/rereference_channels.py b/python_package/examples/tests/rereference_channels.py index 034f03ee8..de1fd143d 100644 --- a/python_package/examples/tests/rereference_channels.py +++ b/python_package/examples/tests/rereference_channels.py @@ -5,6 +5,7 @@ import numpy as np from brainflow.data_filter import DataFilter +from brainflow.exit_codes import BrainFlowError, BrainFlowExitCodes class RereferenceChannelsTest(unittest.TestCase): @@ -16,74 +17,66 @@ def setUp(self): [10.0, 11.0, 12.0], ], dtype=np.float64) + def assert_invalid(self, data, channels, reference_channels): + with self.assertRaises(BrainFlowError) as error: + DataFilter.rereference_channels(data, channels, reference_channels) + self.assertEqual(error.exception.exit_code, BrainFlowExitCodes.INVALID_ARGUMENTS_ERROR.value) + def test_average_reference_across_two_channels(self): - result = DataFilter.rereference_channels(self.data, [0, 1]) + data = self.data.copy() + DataFilter.rereference_channels(data, [0, 1, 2, 3], [0, 1]) reference = np.array([2.5, 3.5, 4.5]) - expected = self.data - reference - np.testing.assert_allclose(result, expected) + np.testing.assert_allclose(data, self.data - reference) def test_average_reference_across_three_channels(self): - result = DataFilter.rereference_channels(self.data, [0, 1, 2]) + data = self.data.copy() + DataFilter.rereference_channels(data, [0, 1, 2, 3], [0, 1, 2]) reference = np.array([4.0, 5.0, 6.0]) - expected = self.data - reference - np.testing.assert_allclose(result, expected) + np.testing.assert_allclose(data, self.data - reference, atol=1e-12) def test_reference_to_single_channel(self): - result = DataFilter.rereference_channels(self.data, [1]) - expected = self.data - self.data[1] - np.testing.assert_allclose(result, expected) - np.testing.assert_array_equal(result[1], np.zeros(3)) - - def test_multiple_reference_channels(self): - result = DataFilter.rereference_channels(self.data, [0, 2, 3]) - reference = np.mean(self.data[[0, 2, 3]], axis=0) - expected = self.data - reference - np.testing.assert_allclose(result, expected) - - def test_excluded_channels_remain_unchanged(self): - result = DataFilter.rereference_channels(self.data, [0, 1], excluded_channels=[2, 3]) + data = self.data.copy() + DataFilter.rereference_channels(data, [0, 1, 2, 3], [1]) + np.testing.assert_allclose(data, self.data - self.data[1]) + np.testing.assert_array_equal(data[1], np.zeros(3)) + + def test_selected_channels_only(self): + data = self.data.copy() + DataFilter.rereference_channels(data, [0, 1], [0, 1]) reference = np.array([2.5, 3.5, 4.5]) expected = self.data.copy() expected[0] -= reference expected[1] -= reference - np.testing.assert_allclose(result, expected) - np.testing.assert_array_equal(result[2], self.data[2]) - np.testing.assert_array_equal(result[3], self.data[3]) + np.testing.assert_allclose(data, expected) + np.testing.assert_array_equal(data[2], self.data[2]) + np.testing.assert_array_equal(data[3], self.data[3]) - def test_input_array_is_not_mutated(self): - original = self.data.copy() - DataFilter.rereference_channels(self.data, [0, 1], excluded_channels=[3]) - np.testing.assert_array_equal(self.data, original) + def test_common_average_reference_allows_overlap(self): + data = self.data.copy() + DataFilter.rereference_channels(data, [0, 1, 2, 3], [0, 1, 2, 3]) + reference = np.mean(self.data, axis=0) + np.testing.assert_allclose(data, self.data - reference) def test_output_shape_equals_input_shape(self): - result = DataFilter.rereference_channels(self.data, [0, 1]) - self.assertEqual(result.shape, self.data.shape) + data = self.data.copy() + DataFilter.rereference_channels(data, [0, 1], [0, 1]) + self.assertEqual(data.shape, self.data.shape) def test_invalid_channel_index_raises(self): - with self.assertRaises(ValueError): - DataFilter.rereference_channels(self.data, [0, 4]) - with self.assertRaises(ValueError): - DataFilter.rereference_channels(self.data, [0], excluded_channels=[4]) - with self.assertRaises(ValueError): - DataFilter.rereference_channels(self.data, [-1]) - - def test_empty_reference_channels_raises(self): - with self.assertRaises(ValueError): - DataFilter.rereference_channels(self.data, []) + self.assert_invalid(self.data.copy(), [0, 4], [0]) + self.assert_invalid(self.data.copy(), [0], [4]) + self.assert_invalid(self.data.copy(), [-1], [0]) - def test_duplicate_reference_channels_raise(self): - with self.assertRaises(ValueError): - DataFilter.rereference_channels(self.data, [0, 1, 0]) + def test_empty_channel_lists_raise(self): + self.assert_invalid(self.data.copy(), [], [0]) + self.assert_invalid(self.data.copy(), [0], []) - def test_channel_cannot_be_both_reference_and_excluded(self): - with self.assertRaises(ValueError): - DataFilter.rereference_channels(self.data, [0, 1], excluded_channels=[1]) + def test_duplicate_channel_indices_raise(self): + self.assert_invalid(self.data.copy(), [0, 1, 0], [1]) + self.assert_invalid(self.data.copy(), [0], [1, 1]) def test_non_2d_input_raises(self): - with self.assertRaises(ValueError): - DataFilter.rereference_channels(np.array([1.0, 2.0, 3.0]), [0]) - with self.assertRaises(ValueError): - DataFilter.rereference_channels(np.zeros((2, 2, 2)), [0]) + self.assert_invalid(np.array([1.0, 2.0, 3.0], dtype=np.float64), [0], [0]) if __name__ == '__main__': diff --git a/rust_package/brainflow/src/data_filter.rs b/rust_package/brainflow/src/data_filter.rs index 6d24621c2..195c5e0e1 100644 --- a/rust_package/brainflow/src/data_filter.rs +++ b/rust_package/brainflow/src/data_filter.rs @@ -1011,6 +1011,46 @@ pub fn get_activity_index( Ok(output) } +/// Re-reference selected channels in-place by subtracting the element-wise mean of +/// ``reference_channels`` from each channel listed in ``channels``. +pub fn rereference_channels( + data: &mut Array2, + channels: &[usize], + reference_channels: &[usize], +) -> Result<()> { + let shape = data.shape(); + let (rows, cols) = (shape[0], shape[1]); + for &ch in channels.iter().chain(reference_channels.iter()) { + if ch >= rows { + return Err(invalid_arguments()); + } + } + let channels_i32: Vec = channels + .iter() + .map(|&c| native_int(c)) + .collect::>>()?; + let reference_i32: Vec = reference_channels + .iter() + .map(|&c| native_int(c)) + .collect::>>()?; + let slice = data + .as_slice_mut() + .ok_or_else(invalid_arguments)?; + let res = unsafe { + data_handler::rereference_channels( + slice.as_mut_ptr() as *mut c_double, + native_int(rows)?, + native_int(cols)?, + channels_i32.as_ptr() as *mut c_int, + native_int(channels_i32.len())?, + reference_i32.as_ptr() as *mut c_int, + native_int(reference_i32.len())?, + ) + }; + check_brainflow_exit_code(res)?; + Ok(()) +} + /// Get DataFilter version. pub fn get_version() -> Result { const MAX_CHARS: usize = 64; diff --git a/rust_package/brainflow/src/ffi/data_handler.rs b/rust_package/brainflow/src/ffi/data_handler.rs index c62bec941..545b8744b 100644 --- a/rust_package/brainflow/src/ffi/data_handler.rs +++ b/rust_package/brainflow/src/ffi/data_handler.rs @@ -298,6 +298,17 @@ extern "C" { activity_index: *mut f64, ) -> ::std::os::raw::c_int; } +extern "C" { + pub fn rereference_channels( + data: *mut f64, + rows: ::std::os::raw::c_int, + cols: ::std::os::raw::c_int, + channels: *mut ::std::os::raw::c_int, + channels_len: ::std::os::raw::c_int, + reference_channels: *mut ::std::os::raw::c_int, + reference_channels_len: ::std::os::raw::c_int, + ) -> ::std::os::raw::c_int; +} extern "C" { pub fn set_log_level_data_handler(log_level: ::std::os::raw::c_int) -> ::std::os::raw::c_int; } diff --git a/src/data_handler/inc/data_handler.h b/src/data_handler/inc/data_handler.h index 600c964fc..d2b6a36ee 100644 --- a/src/data_handler/inc/data_handler.h +++ b/src/data_handler/inc/data_handler.h @@ -105,6 +105,10 @@ extern "C" SHARED_EXPORT int CALLING_CONVENTION get_activity_index (const double *accel_x, const double *accel_y, const double *accel_z, int data_len, int sampling_rate, int period, double noise_var_x, double noise_var_y, double noise_var_z, double *activity_index); + // Subtract mean of reference_channels from selected channels in-place. + // data is row-major (channels x samples). Overlap between lists is allowed (CAR). + SHARED_EXPORT int CALLING_CONVENTION rereference_channels (double *data, int rows, int cols, + int *channels, int channels_len, int *reference_channels, int reference_channels_len); // logging methods SHARED_EXPORT int CALLING_CONVENTION set_log_level_data_handler (int log_level); diff --git a/src/data_handler/signal_processing.cpp b/src/data_handler/signal_processing.cpp index fdaaa334f..4fdea8dd5 100644 --- a/src/data_handler/signal_processing.cpp +++ b/src/data_handler/signal_processing.cpp @@ -653,6 +653,59 @@ catch (...) return failure; } +int rereference_channels (double *data, int rows, int cols, int *channels, int channels_len, + int *reference_channels, int reference_channels_len) +try +{ + if (!data || !channels || !reference_channels || rows < 1 || cols < 1 || channels_len < 1 || + reference_channels_len < 1 || + (int64_t)rows * cols > std::numeric_limits::max ()) + return invalid; + + std::set seen_channels; + std::set seen_reference; + for (int i = 0; i < channels_len; i++) + { + int channel = channels[i]; + if (channel < 0 || channel >= rows || !seen_channels.insert (channel).second) + return invalid; + } + for (int i = 0; i < reference_channels_len; i++) + { + int channel = reference_channels[i]; + if (channel < 0 || channel >= rows || !seen_reference.insert (channel).second) + return invalid; + } + + for (int channel : seen_channels) + if (!finite_signal (data + (size_t)channel * cols, cols)) + return invalid; + for (int channel : seen_reference) + if (!seen_channels.count (channel) && + !finite_signal (data + (size_t)channel * cols, cols)) + return invalid; + + std::vector reference ((size_t)cols, 0.0); + const double scale = 1.0 / reference_channels_len; + for (int i = 0; i < reference_channels_len; i++) + { + const double *row = data + (size_t)reference_channels[i] * cols; + for (int t = 0; t < cols; t++) + reference[(size_t)t] += row[t] * scale; + } + for (int i = 0; i < channels_len; i++) + { + double *row = data + (size_t)channels[i] * cols; + for (int t = 0; t < cols; t++) + row[t] -= reference[(size_t)t]; + } + return ok; +} +catch (...) +{ + return failure; +} + int write_file (const double *data, int rows, int cols, const char *name, const char *mode) try { diff --git a/swift_package/Sources/BrainFlow/DataFilter.swift b/swift_package/Sources/BrainFlow/DataFilter.swift index 7a0b000b6..da6fd10f6 100644 --- a/swift_package/Sources/BrainFlow/DataFilter.swift +++ b/swift_package/Sources/BrainFlow/DataFilter.swift @@ -777,6 +777,47 @@ public enum DataFilter { return output } + public static func rereference_channels( + data: inout [[Double]], + channels: [Int], + reference_channels: [Int] + ) throws { + let (rows, cols) = try BrainFlowArray.validateRectangular(data) + guard !channels.isEmpty, !reference_channels.isEmpty else { + throw invalidArguments("channels and reference_channels must be non-empty") + } + guard channels.allSatisfy({ $0 >= 0 && $0 < rows }) else { + throw invalidArguments("Channel index is out of range") + } + guard reference_channels.allSatisfy({ $0 >= 0 && $0 < rows }) else { + throw invalidArguments("Reference channel index is out of range") + } + var linear = reshape_data_to_1d(num_rows: rows, num_cols: cols, buf: data) + var channelsNative = channels.map { CInt($0) } + var referenceNative = reference_channels.map { CInt($0) } + try linear.withUnsafeMutableBufferPointer { dataPtr in + try channelsNative.withUnsafeMutableBufferPointer { channelsPtr in + try referenceNative.withUnsafeMutableBufferPointer { referencePtr in + try DataFilterNative.withData { native in + try checkBrainFlowExitCode( + native.rereference_channels( + dataPtr.baseAddress, + checkedCInt(rows), + checkedCInt(cols), + channelsPtr.baseAddress, + checkedCInt(channels.count), + referencePtr.baseAddress, + checkedCInt(reference_channels.count) + ), + "Failed to rereference channels" + ) + } + } + } + } + data = BrainFlowArray.reshape_data_to_2d(num_rows: rows, num_cols: cols, linear_buffer: linear) + } + private static func checkedCInt(_ value: Int) throws -> CInt { guard let result = CInt(exactly: value) else { throw invalidArguments("Integer argument exceeds the native integer range") @@ -849,6 +890,7 @@ final class DataFilterNative { let detect_peaks_z_score: @convention(c) (UnsafeMutablePointer?, CInt, CInt, Double, Double, UnsafeMutablePointer?) -> CInt let perform_ica: @convention(c) (UnsafeMutablePointer?, CInt, CInt, CInt, UnsafeMutablePointer?, UnsafeMutablePointer?, UnsafeMutablePointer?, UnsafeMutablePointer?) -> CInt let get_activity_index: @convention(c) (UnsafePointer?, UnsafePointer?, UnsafePointer?, CInt, CInt, CInt, Double, Double, Double, UnsafeMutablePointer?) -> CInt + let rereference_channels: @convention(c) (UnsafeMutablePointer?, CInt, CInt, UnsafeMutablePointer?, CInt, UnsafeMutablePointer?, CInt) -> CInt let set_log_level_data_handler: @convention(c) (CInt) -> CInt let set_log_file_data_handler: @convention(c) (UnsafePointer?) -> CInt let log_message_data_handler: @convention(c) (CInt, UnsafeMutablePointer?) -> CInt @@ -902,6 +944,7 @@ final class DataFilterNative { detect_peaks_z_score = try library.symbol("detect_peaks_z_score", as: type(of: detect_peaks_z_score)) perform_ica = try library.symbol("perform_ica", as: type(of: perform_ica)) get_activity_index = try library.symbol("get_activity_index", as: type(of: get_activity_index)) + rereference_channels = try library.symbol("rereference_channels", as: type(of: rereference_channels)) set_log_level_data_handler = try library.symbol("set_log_level_data_handler", as: type(of: set_log_level_data_handler)) set_log_file_data_handler = try library.symbol("set_log_file_data_handler", as: type(of: set_log_file_data_handler)) log_message_data_handler = try library.symbol("log_message_data_handler", as: type(of: log_message_data_handler)) From 1d4f2d60b01053e59d62ec7bbf09181e29a9db47 Mon Sep 17 00:00:00 2001 From: antonogarkov Date: Mon, 9 Nov 2026 12:39:07 +0300 Subject: [PATCH 3/4] fix review issues --- docs/UserAPI.rst | 7 +- .../src/main/java/brainflow/DataFilter.java | 3 +- matlab_package/brainflow/DataFilter.m | 4 +- nodejs_package/brainflow/data_filter.ts | 2 +- .../tests/binding_validation_regression.ts | 7 + nodejs_package/tests/package-lock.json | 270 ++++++++++++++++++ nodejs_package/tests/package.json | 1 + python_package/brainflow/data_filter.py | 21 +- .../examples/tests/rereference_channels.py | 22 ++ 9 files changed, 324 insertions(+), 13 deletions(-) create mode 100644 nodejs_package/tests/package-lock.json diff --git a/docs/UserAPI.rst b/docs/UserAPI.rst index 265b2a499..59132b12c 100644 --- a/docs/UserAPI.rst +++ b/docs/UserAPI.rst @@ -138,8 +138,11 @@ return errors. Writing uses enough decimal precision to round-trip doubles. ``DataFilter.rereference_channels`` subtracts the element-wise mean of ``reference_channels`` from the selected ``channels`` of a ``(channels, samples)`` -array in place. Channel indices are zero-based. Overlap between the two index lists -is allowed and supports common average referencing. +array in place. Channel indices are zero-based in most bindings; MATLAB and Julia +use one-based indices and convert them before calling native code. Overlap between +the two index lists is allowed and supports common average referencing. MATLAB +returns the updated matrix, so callers must assign the result +(``data = DataFilter.rereference_channels(data, channels, reference_channels)``). Python API Reference ---------------------- diff --git a/java_package/brainflow/src/main/java/brainflow/DataFilter.java b/java_package/brainflow/src/main/java/brainflow/DataFilter.java index 32e7968cb..72af4df64 100644 --- a/java_package/brainflow/src/main/java/brainflow/DataFilter.java +++ b/java_package/brainflow/src/main/java/brainflow/DataFilter.java @@ -994,7 +994,8 @@ public static List perform_ica (double[][] data, int num_components, public static void rereference_channels (double[][] data, int[] channels, int[] reference_channels) throws BrainFlowError { - if (data == null || data.length == 0 || channels == null || reference_channels == null) + if (data == null || data.length == 0 || data[0] == null || channels == null || + reference_channels == null) { throw new BrainFlowError ("invalid args for rereference_channels", BrainFlowExitCode.INVALID_ARGUMENTS_ERROR.get_code ()); diff --git a/matlab_package/brainflow/DataFilter.m b/matlab_package/brainflow/DataFilter.m index 71cbbb5f6..91d038662 100644 --- a/matlab_package/brainflow/DataFilter.m +++ b/matlab_package/brainflow/DataFilter.m @@ -599,7 +599,7 @@ function write_file(data, file_name, file_mode) end function data = rereference_channels(data, channels, reference_channels) - % re-reference selected channels in-place + % re-reference selected channels; assign the returned matrix channels = DataFilter.checked_channels(data, channels); reference_channels = DataFilter.checked_channels(data, reference_channels); task_name = 'rereference_channels'; @@ -611,7 +611,7 @@ function write_file(data, file_name, file_mode) lib_name = DataFilter.load_lib(); exit_code = calllib(lib_name, task_name, temp_input, size(data, 1), size(data, 2), temp_channels, numel(channels), temp_reference, numel(reference_channels)); DataFilter.check_ec(exit_code, task_name); - data = reshape(temp_input.Value, [size(data, 2), size(data, 1)])'; + data = transpose(reshape(temp_input.Value, [size(data, 2), size(data, 1)])); end end diff --git a/nodejs_package/brainflow/data_filter.ts b/nodejs_package/brainflow/data_filter.ts index ff8bc0798..1323b1260 100644 --- a/nodejs_package/brainflow/data_filter.ts +++ b/nodejs_package/brainflow/data_filter.ts @@ -788,7 +788,7 @@ export class DataFilter public static rereferenceChannels( data: number[][], channels: number[], referenceChannels: number[]): void { - const columns = DataFilter.checkMatrix(data, true); + const columns = DataFilter.checkMatrix(data); DataFilter.checkChannels(data, channels, columns); DataFilter.checkChannels(data, referenceChannels, columns); const flat = data.flat(); diff --git a/nodejs_package/tests/binding_validation_regression.ts b/nodejs_package/tests/binding_validation_regression.ts index 6d77a1c97..3895d75b1 100644 --- a/nodejs_package/tests/binding_validation_regression.ts +++ b/nodejs_package/tests/binding_validation_regression.ts @@ -47,4 +47,11 @@ const restored = DataFilter.performInverseWaveletTransform(transformed, signal.l assert.ok(restored.every((value, i) => Math.abs(value - signal[i]) < 1e-10)); assert.throws(() => DataFilter.performInverseWaveletTransform( transformed, 64, WaveletTypes.DB3, 2, WaveletExtensionTypes.SYMMETRIC), BrainFlowError); + +const rerefData = [[1, 2], [3, 4], [Number.NaN, Number.NaN]]; +DataFilter.rereferenceChannels(rerefData, [0], [1]); +assert.deepStrictEqual(rerefData[0], [-2, -2]); +assert.deepStrictEqual(rerefData[1], [3, 4]); +assert.ok(Number.isNaN(rerefData[2][0]) && Number.isNaN(rerefData[2][1])); + console.log('Binding validation regressions passed'); diff --git a/nodejs_package/tests/package-lock.json b/nodejs_package/tests/package-lock.json new file mode 100644 index 000000000..539e76ce6 --- /dev/null +++ b/nodejs_package/tests/package-lock.json @@ -0,0 +1,270 @@ +{ + "name": "tests", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "tests", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "brainflow": "file:.." + }, + "devDependencies": { + "@types/node": "^20.6.0", + "ts-node": "^10.9.2", + "typescript": "^5.2.2" + } + }, + "..": { + "name": "brainflow", + "version": "0.0.0-development", + "license": "MIT", + "dependencies": { + "koffi": "2.5.7", + "lodash": "4.17.21" + }, + "devDependencies": { + "@types/jest": "29.5.3", + "@types/lodash": "4.14.195", + "@types/node": "20.4.2", + "@typescript-eslint/eslint-plugin": "^5.50.0", + "@typescript-eslint/parser": "^5.0.0", + "eslint": "^8.0.1", + "eslint-config-prettier": "8.8.0", + "eslint-config-standard-with-typescript": "36.1.0", + "eslint-plugin-import": "^2.25.2", + "eslint-plugin-n": "^15.0.0 || ^16.0.0 ", + "eslint-plugin-prettier": "5.0.0", + "eslint-plugin-promise": "6.0.0", + "jest": "29.6.1", + "prettier": "3.0.0", + "ts-jest": "29.1.1", + "typescript": "^5.1.6" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.13", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.13.tgz", + "integrity": "sha512-gcLdvR9HO1ZJBypsOGqaP6TFEzb6vIta0KSTLt9NAQ6pXQO3cRgSVyCN6pzYqI9DlJgY71XKO0dpDhCf08b3pg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.43", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.43.tgz", + "integrity": "sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/acorn": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.19.0.tgz", + "integrity": "sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/brainflow": { + "resolved": "..", + "link": true + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + } + } +} diff --git a/nodejs_package/tests/package.json b/nodejs_package/tests/package.json index 3697e0cf5..c25118e99 100644 --- a/nodejs_package/tests/package.json +++ b/nodejs_package/tests/package.json @@ -21,6 +21,7 @@ "license": "ISC", "devDependencies": { "@types/node": "^20.6.0", + "ts-node": "^10.9.2", "typescript": "^5.2.2" }, "dependencies": { diff --git a/python_package/brainflow/data_filter.py b/python_package/brainflow/data_filter.py index 186f5ce6d..5c1019f3b 100644 --- a/python_package/brainflow/data_filter.py +++ b/python_package/brainflow/data_filter.py @@ -492,9 +492,9 @@ def __init__(self): ndpointer(ctypes.c_double, flags=('C_CONTIGUOUS', 'WRITEABLE')), ctypes.c_int, ctypes.c_int, - ndpointer(ctypes.c_int32), + ndpointer(ctypes.c_int32, flags='C_CONTIGUOUS'), ctypes.c_int, - ndpointer(ctypes.c_int32), + ndpointer(ctypes.c_int32, flags='C_CONTIGUOUS'), ctypes.c_int, ] @@ -1619,17 +1619,24 @@ def rereference_channels(cls, data, channels: List, reference_channels: List) -> if data.dtype != numpy.float64 or not data.flags.writeable: raise BrainFlowError('data must be a writable float64 array', BrainFlowExitCodes.INVALID_ARGUMENTS_ERROR.value) - channels_arr = numpy.asarray(channels, dtype=numpy.int32) - reference_arr = numpy.asarray(reference_channels, dtype=numpy.int32) - if channels_arr.ndim != 1 or reference_arr.ndim != 1: - raise BrainFlowError('channel index arrays must be 1d', - BrainFlowExitCodes.INVALID_ARGUMENTS_ERROR.value) + channels_arr = cls._validated_channel_indices(channels, data.shape[0], 'channels') + reference_arr = cls._validated_channel_indices( + reference_channels, data.shape[0], 'reference_channels') res = DataHandlerDLL.get_instance().rereference_channels( data, data.shape[0], data.shape[1], channels_arr, channels_arr.size, reference_arr, reference_arr.size) if res != BrainFlowExitCodes.STATUS_OK.value: raise BrainFlowError('unable to rereference channels', res) + @staticmethod + def _validated_channel_indices(channels, num_channels: int, arg_name: str): + indices = numpy.asarray(channels) + if (indices.ndim != 1 or indices.size == 0 or indices.dtype.kind not in 'iu' or + numpy.any(indices < 0) or numpy.any(indices >= num_channels)): + raise BrainFlowError('invalid %s' % arg_name, + BrainFlowExitCodes.INVALID_ARGUMENTS_ERROR.value) + return numpy.ascontiguousarray(indices, dtype=numpy.int32) + @classmethod def perform_ifft(cls, data): """perform inverse fft diff --git a/python_package/examples/tests/rereference_channels.py b/python_package/examples/tests/rereference_channels.py index de1fd143d..c76cf6a5e 100644 --- a/python_package/examples/tests/rereference_channels.py +++ b/python_package/examples/tests/rereference_channels.py @@ -78,6 +78,28 @@ def test_duplicate_channel_indices_raise(self): def test_non_2d_input_raises(self): self.assert_invalid(np.array([1.0, 2.0, 3.0], dtype=np.float64), [0], [0]) + def test_sliced_channel_index_arrays(self): + data = self.data.copy() + channels = np.arange(4, dtype=np.int32)[::2] + DataFilter.rereference_channels(data, channels, [3]) + expected = self.data.copy() + expected[[0, 2]] -= self.data[3] + np.testing.assert_allclose(data, expected) + + data = self.data.copy() + reference = np.arange(4, dtype=np.int32)[::2] + DataFilter.rereference_channels(data, [0, 1, 2, 3], reference) + expected = self.data - np.mean(self.data[[0, 2]], axis=0) + np.testing.assert_allclose(data, expected) + + def test_non_integer_and_overflowing_indices_raise(self): + self.assert_invalid(self.data.copy(), [0.9], [0]) + self.assert_invalid(self.data.copy(), [-0.5], [0]) + self.assert_invalid(self.data.copy(), np.array([4294967296], dtype=np.int64), [0]) + self.assert_invalid(self.data.copy(), [0], [0.9]) + self.assert_invalid(self.data.copy(), [0], [-0.5]) + self.assert_invalid(self.data.copy(), [0], np.array([4294967296], dtype=np.int64)) + if __name__ == '__main__': unittest.main() From c171d6c33a4e58c7c597a5b63273ceb76a52a4c2 Mon Sep 17 00:00:00 2001 From: antonogarkov Date: Tue, 10 Nov 2026 19:00:09 +0300 Subject: [PATCH 4/4] fix format --- src/data_handler/signal_processing.cpp | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/data_handler/signal_processing.cpp b/src/data_handler/signal_processing.cpp index f09936abc..2f9017f9c 100644 --- a/src/data_handler/signal_processing.cpp +++ b/src/data_handler/signal_processing.cpp @@ -607,8 +607,7 @@ int rereference_channels (double *data, int rows, int cols, int *channels, int c try { if (!data || !channels || !reference_channels || rows < 1 || cols < 1 || channels_len < 1 || - reference_channels_len < 1 || - (int64_t)rows * cols > std::numeric_limits::max ()) + reference_channels_len < 1 || (int64_t)rows * cols > std::numeric_limits::max ()) return invalid; std::set seen_channels; @@ -630,8 +629,7 @@ try if (!finite_signal (data + (size_t)channel * cols, cols)) return invalid; for (int channel : seen_reference) - if (!seen_channels.count (channel) && - !finite_signal (data + (size_t)channel * cols, cols)) + if (!seen_channels.count (channel) && !finite_signal (data + (size_t)channel * cols, cols)) return invalid; std::vector reference ((size_t)cols, 0.0);