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/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 4d02c10af..265b2a499 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. +``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 e1abd5835..186f5ce6d 100644 --- a/python_package/brainflow/data_filter.py +++ b/python_package/brainflow/data_filter.py @@ -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,6 +1599,37 @@ def perform_ica(cls, data, num_components: int, channels=None, *, max_iterations return w, k, a, s + @classmethod + 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 ``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: writable float64 2d array with shape (channels, samples) + :type data: NDArray[Shape["*, *"], Float64] + :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 + """ + 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): """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..de1fd143d --- /dev/null +++ b/python_package/examples/tests/rereference_channels.py @@ -0,0 +1,83 @@ +"""Unit tests for DataFilter.rereference_channels.""" + +import unittest + +import numpy as np + +from brainflow.data_filter import DataFilter +from brainflow.exit_codes import BrainFlowError, BrainFlowExitCodes + + +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 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): + data = self.data.copy() + DataFilter.rereference_channels(data, [0, 1, 2, 3], [0, 1]) + reference = np.array([2.5, 3.5, 4.5]) + np.testing.assert_allclose(data, self.data - reference) + + def test_average_reference_across_three_channels(self): + data = self.data.copy() + DataFilter.rereference_channels(data, [0, 1, 2, 3], [0, 1, 2]) + reference = np.array([4.0, 5.0, 6.0]) + np.testing.assert_allclose(data, self.data - reference, atol=1e-12) + + def test_reference_to_single_channel(self): + 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(data, expected) + np.testing.assert_array_equal(data[2], self.data[2]) + np.testing.assert_array_equal(data[3], self.data[3]) + + 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): + 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): + 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_empty_channel_lists_raise(self): + self.assert_invalid(self.data.copy(), [], [0]) + self.assert_invalid(self.data.copy(), [0], []) + + 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): + self.assert_invalid(np.array([1.0, 2.0, 3.0], dtype=np.float64), [0], [0]) + + +if __name__ == '__main__': + unittest.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))