From 70ddfb1e2e98d5b92ee28ba0d313ede6d48ec6b2 Mon Sep 17 00:00:00 2001 From: Samer Zumot <54731842+samerzumot@users.noreply.github.com> Date: Sun, 23 Aug 2026 14:11:39 -0400 Subject: [PATCH 1/3] Add moving average option for ML classifiers (fixes #640) - Implement MovingAverageClassifier in C++ core supporting custom scores and built-in metric models - Add MOVING_AVERAGE_CLASSIFIER enum across Python, Java, C#, TypeScript, Rust, Swift, Julia, and MATLAB bindings - Add automated test moving_average_classifier.py --- .../brainflow/brainflow/ml_module_library.cs | 5 +- .../java/brainflow/BrainFlowClassifiers.java | 3 +- julia_package/brainflow/src/ml_model.jl | 1 + .../brainflow/BrainFlowClassifiers.m | 1 + nodejs_package/brainflow/brainflow.types.ts | 1 + python_package/brainflow/ml_model.py | 1 + .../tests/moving_average_classifier.py | 73 +++++++++ rust_package/brainflow/src/ffi/constants.rs | 1 + src/ml/build.cmake | 1 + src/ml/inc/moving_average_classifier.h | 28 ++++ src/ml/ml_module.cpp | 5 + src/ml/moving_average_classifier.cpp | 138 ++++++++++++++++++ src/utils/inc/brainflow_constants.h | 3 +- .../Sources/BrainFlow/BrainFlowEnums.swift | 1 + 14 files changed, 258 insertions(+), 4 deletions(-) create mode 100644 python_package/examples/tests/moving_average_classifier.py create mode 100644 src/ml/inc/moving_average_classifier.h create mode 100644 src/ml/moving_average_classifier.cpp diff --git a/csharp_package/brainflow/brainflow/ml_module_library.cs b/csharp_package/brainflow/brainflow/ml_module_library.cs index cb6de268c..bdfa6862c 100644 --- a/csharp_package/brainflow/brainflow/ml_module_library.cs +++ b/csharp_package/brainflow/brainflow/ml_module_library.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; @@ -19,7 +19,8 @@ public enum BrainFlowClassifiers { DEFAULT_CLASSIFIER = 0, DYN_LIB_CLASSIFIER = 1, - ONNX_CLASSIFIER = 2 + ONNX_CLASSIFIER = 2, + MOVING_AVERAGE_CLASSIFIER = 3 }; public static class MLModuleLibrary64 diff --git a/java_package/brainflow/src/main/java/brainflow/BrainFlowClassifiers.java b/java_package/brainflow/src/main/java/brainflow/BrainFlowClassifiers.java index e2233f7b5..9ed62c004 100644 --- a/java_package/brainflow/src/main/java/brainflow/BrainFlowClassifiers.java +++ b/java_package/brainflow/src/main/java/brainflow/BrainFlowClassifiers.java @@ -8,7 +8,8 @@ public enum BrainFlowClassifiers DEFAULT_CLASSIFIER (0), DYN_LIB_CLASSIFIER (1), - ONNX_CLASSIFIER (2); + ONNX_CLASSIFIER (2), + MOVING_AVERAGE_CLASSIFIER (3); private final int protocol; private static final Map cl_map = new HashMap (); diff --git a/julia_package/brainflow/src/ml_model.jl b/julia_package/brainflow/src/ml_model.jl index cc59e9849..e066e5156 100644 --- a/julia_package/brainflow/src/ml_model.jl +++ b/julia_package/brainflow/src/ml_model.jl @@ -16,6 +16,7 @@ MetricType = Union{BrainFlowMetrics, Integer} DEFAULT_CLASSIFIER = 0 DYN_LIB_CLASSIFIER = 1 ONNX_CLASSIFIER = 2 + MOVING_AVERAGE_CLASSIFIER = 3 end diff --git a/matlab_package/brainflow/BrainFlowClassifiers.m b/matlab_package/brainflow/BrainFlowClassifiers.m index ee4028d91..3aa1fab51 100644 --- a/matlab_package/brainflow/BrainFlowClassifiers.m +++ b/matlab_package/brainflow/BrainFlowClassifiers.m @@ -4,5 +4,6 @@ DEFAULT_CLASSIFIER(0) DYN_LIB_CLASSIFIER(1) ONNX_CLASSIFIER(2) + MOVING_AVERAGE_CLASSIFIER(3) end end \ No newline at end of file diff --git a/nodejs_package/brainflow/brainflow.types.ts b/nodejs_package/brainflow/brainflow.types.ts index 5aabeaed5..d1683f842 100644 --- a/nodejs_package/brainflow/brainflow.types.ts +++ b/nodejs_package/brainflow/brainflow.types.ts @@ -241,6 +241,7 @@ export enum BrainFlowClassifiers { DEFAULT_CLASSIFIER = 0, USER_DEFINED = 1, ONNX_CLASSIFIER = 2, + MOVING_AVERAGE_CLASSIFIER = 3, } export interface IBrainFlowInputParams { diff --git a/python_package/brainflow/ml_model.py b/python_package/brainflow/ml_model.py index fe9480e33..a20d40353 100644 --- a/python_package/brainflow/ml_model.py +++ b/python_package/brainflow/ml_model.py @@ -27,6 +27,7 @@ class BrainFlowClassifiers(enum.IntEnum): DEFAULT_CLASSIFIER = 0 #: DYN_LIB_CLASSIFIER = 1 #: ONNX_CLASSIFIER = 2 #: + MOVING_AVERAGE_CLASSIFIER = 3 #: class BrainFlowModelParams(object): diff --git a/python_package/examples/tests/moving_average_classifier.py b/python_package/examples/tests/moving_average_classifier.py new file mode 100644 index 000000000..270d5bbc8 --- /dev/null +++ b/python_package/examples/tests/moving_average_classifier.py @@ -0,0 +1,73 @@ +import numpy as np +import sys +import os + +# add python_package to sys.path +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))) + +from brainflow.ml_model import MLModel, BrainFlowMetrics, BrainFlowClassifiers, BrainFlowModelParams +from brainflow.exit_codes import BrainFlowError, BrainFlowExitCodes + + +def test_moving_average_classifier(): + print("Testing MovingAverageClassifier...") + + # 1. Test USER_DEFINED stream with window_len = 3 + params = BrainFlowModelParams( + BrainFlowMetrics.USER_DEFINED.value, + BrainFlowClassifiers.MOVING_AVERAGE_CLASSIFIER.value + ) + params.other_info = "3" + + model = MLModel(params) + model.prepare() + + # Step 1: Input 10.0 -> Avg: 10.0 + out1 = model.predict(np.array([10.0], dtype=np.float64)) + print(f"Step 1: In=10.0, Out={out1[0]}") + assert np.isclose(out1[0], 10.0) + + # Step 2: Input 20.0 -> Avg: (10 + 20) / 2 = 15.0 + out2 = model.predict(np.array([20.0], dtype=np.float64)) + print(f"Step 2: In=20.0, Out={out2[0]}") + assert np.isclose(out2[0], 15.0) + + # Step 3: Input 30.0 -> Avg: (10 + 20 + 30) / 3 = 20.0 + out3 = model.predict(np.array([30.0], dtype=np.float64)) + print(f"Step 3: In=30.0, Out={out3[0]}") + assert np.isclose(out3[0], 20.0) + + # Step 4: Input 40.0 -> Avg: (20 + 30 + 40) / 3 = 30.0 (oldest 10.0 dropped) + out4 = model.predict(np.array([40.0], dtype=np.float64)) + print(f"Step 4: In=40.0, Out={out4[0]}") + assert np.isclose(out4[0], 30.0) + + model.release() + + # 2. Test MINDFULNESS metric with MOVING_AVERAGE_CLASSIFIER + mf_params = BrainFlowModelParams( + BrainFlowMetrics.MINDFULNESS.value, + BrainFlowClassifiers.MOVING_AVERAGE_CLASSIFIER.value + ) + mf_params.other_info = '{"window_len": 4}' + + mf_model = MLModel(mf_params) + mf_model.prepare() + + # 5 band powers input + feature_vector = np.array([0.1, 0.2, 0.3, 0.2, 0.2], dtype=np.float64) + mf_out1 = mf_model.predict(feature_vector) + print(f"Mindfulness moving avg 1: {mf_out1[0]}") + assert 0.0 <= mf_out1[0] <= 1.0 + + mf_out2 = mf_model.predict(feature_vector) + print(f"Mindfulness moving avg 2: {mf_out2[0]}") + assert np.isclose(mf_out1[0], mf_out2[0]) + + mf_model.release() + + print("All MovingAverageClassifier tests passed successfully!") + + +if __name__ == '__main__': + test_moving_average_classifier() diff --git a/rust_package/brainflow/src/ffi/constants.rs b/rust_package/brainflow/src/ffi/constants.rs index 37d78ddfa..a9637ea53 100644 --- a/rust_package/brainflow/src/ffi/constants.rs +++ b/rust_package/brainflow/src/ffi/constants.rs @@ -157,6 +157,7 @@ pub enum BrainFlowClassifiers { DefaultClassifier = 0, DynLibClassifier = 1, OnnxClassifier = 2, + MovingAverageClassifier = 3, } #[repr(i32)] #[derive(FromPrimitive, ToPrimitive, Debug, Copy, Clone, Hash, PartialEq, Eq)] diff --git a/src/ml/build.cmake b/src/ml/build.cmake index bddbbe91c..e03054739 100644 --- a/src/ml/build.cmake +++ b/src/ml/build.cmake @@ -29,6 +29,7 @@ SET (ML_MODULE_SRC ${CMAKE_CURRENT_LIST_DIR}/base_classifier.cpp ${CMAKE_CURRENT_LIST_DIR}/mindfulness_classifier.cpp ${CMAKE_CURRENT_LIST_DIR}/generated/mindfulness_model.cpp + ${CMAKE_CURRENT_LIST_DIR}/moving_average_classifier.cpp ) add_library ( diff --git a/src/ml/inc/moving_average_classifier.h b/src/ml/inc/moving_average_classifier.h new file mode 100644 index 000000000..7c3c7fd0d --- /dev/null +++ b/src/ml/inc/moving_average_classifier.h @@ -0,0 +1,28 @@ +#pragma once + +#include +#include + +#include "base_classifier.h" +#include "brainflow_constants.h" +#include "brainflow_model_params.h" + + +class MovingAverageClassifier : public BaseClassifier +{ +protected: + int window_len; + std::deque buffer; + double sum; + std::shared_ptr base_classifier; + + int parse_window_len (); + +public: + MovingAverageClassifier (struct BrainFlowModelParams params); + ~MovingAverageClassifier (); + + int prepare () override; + int predict (double *data, int data_len, double *output, int *output_len) override; + int release () override; +}; diff --git a/src/ml/ml_module.cpp b/src/ml/ml_module.cpp index 55e8ebaaf..45f8c33c8 100644 --- a/src/ml/ml_module.cpp +++ b/src/ml/ml_module.cpp @@ -11,6 +11,7 @@ #include "dyn_lib_classifier.h" #include "mindfulness_classifier.h" #include "ml_module.h" +#include "moving_average_classifier.h" #include "onnx_classifier.h" #include "restfulness_classifier.h" @@ -62,6 +63,10 @@ int prepare (const char *json_params) { model = std::shared_ptr (new RestfulnessClassifier (key)); } + else if (key.classifier == (int)BrainFlowClassifiers::MOVING_AVERAGE_CLASSIFIER) + { + model = std::shared_ptr (new MovingAverageClassifier (key)); + } else { return (int)BrainFlowExitCodes::UNSUPPORTED_CLASSIFIER_AND_METRIC_COMBINATION_ERROR; diff --git a/src/ml/moving_average_classifier.cpp b/src/ml/moving_average_classifier.cpp new file mode 100644 index 000000000..535346891 --- /dev/null +++ b/src/ml/moving_average_classifier.cpp @@ -0,0 +1,138 @@ +#include +#include +#include + +#include "brainflow_constants.h" +#include "json.hpp" +#include "mindfulness_classifier.h" +#include "moving_average_classifier.h" +#include "restfulness_classifier.h" + +using json = nlohmann::json; + + +MovingAverageClassifier::MovingAverageClassifier (struct BrainFlowModelParams model_params) + : BaseClassifier (model_params) +{ + window_len = 5; + sum = 0.0; + base_classifier = NULL; + + if (params.metric == (int)BrainFlowMetrics::MINDFULNESS) + { + base_classifier = std::shared_ptr (new MindfulnessClassifier (params)); + } + else if (params.metric == (int)BrainFlowMetrics::RESTFULNESS) + { + base_classifier = std::shared_ptr (new RestfulnessClassifier (params)); + } +} + +MovingAverageClassifier::~MovingAverageClassifier () +{ + buffer.clear (); + sum = 0.0; + base_classifier = NULL; +} + +int MovingAverageClassifier::parse_window_len () +{ + int len = 5; + if (!params.other_info.empty ()) + { + try + { + if (params.other_info.find ("{") != std::string::npos) + { + json j = json::parse (params.other_info); + if (j.contains ("window_len")) + { + len = j["window_len"].get (); + } + else if (j.contains ("period")) + { + len = j["period"].get (); + } + } + else + { + len = std::stoi (params.other_info); + } + } + catch (...) + { + safe_logger (spdlog::level::warn, + "Unable to parse window_len from other_info: {}. Using default value of 5.", + params.other_info); + len = 5; + } + } + if (len <= 0) + { + len = 5; + } + return len; +} + +int MovingAverageClassifier::prepare () +{ + buffer.clear (); + sum = 0.0; + window_len = parse_window_len (); + + if (base_classifier != NULL) + { + return base_classifier->prepare (); + } + return (int)BrainFlowExitCodes::STATUS_OK; +} + +int MovingAverageClassifier::predict ( + double *data, int data_len, double *output, int *output_len) +{ + if ((data == NULL) || (output == NULL) || (data_len <= 0)) + { + safe_logger (spdlog::level::err, "Incorrect arguments for predict."); + return (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR; + } + + double raw_score = 0.0; + if (base_classifier != NULL) + { + double base_output = 0.0; + int base_output_len = 0; + int res = base_classifier->predict (data, data_len, &base_output, &base_output_len); + if (res != (int)BrainFlowExitCodes::STATUS_OK) + { + return res; + } + raw_score = base_output; + } + else + { + raw_score = data[0]; + } + + buffer.push_back (raw_score); + sum += raw_score; + if ((int)buffer.size () > window_len) + { + sum -= buffer.front (); + buffer.pop_front (); + } + + *output = sum / buffer.size (); + *output_len = 1; + return (int)BrainFlowExitCodes::STATUS_OK; +} + +int MovingAverageClassifier::release () +{ + buffer.clear (); + sum = 0.0; + if (base_classifier != NULL) + { + return base_classifier->release (); + } + return (int)BrainFlowExitCodes::STATUS_OK; +} diff --git a/src/utils/inc/brainflow_constants.h b/src/utils/inc/brainflow_constants.h index 8b31b0df2..de51574ee 100644 --- a/src/utils/inc/brainflow_constants.h +++ b/src/utils/inc/brainflow_constants.h @@ -152,7 +152,8 @@ enum class BrainFlowClassifiers : int { DEFAULT_CLASSIFIER = 0, DYN_LIB_CLASSIFIER = 1, - ONNX_CLASSIFIER = 2 + ONNX_CLASSIFIER = 2, + MOVING_AVERAGE_CLASSIFIER = 3 }; enum class BrainFlowPresets : int diff --git a/swift_package/Sources/BrainFlow/BrainFlowEnums.swift b/swift_package/Sources/BrainFlow/BrainFlowEnums.swift index 9e98fa9a0..f3031bdbd 100644 --- a/swift_package/Sources/BrainFlow/BrainFlowEnums.swift +++ b/swift_package/Sources/BrainFlow/BrainFlowEnums.swift @@ -126,6 +126,7 @@ public enum BrainFlowClassifiers: Int, CaseIterable, Sendable { case DEFAULT_CLASSIFIER = 0 case DYN_LIB_CLASSIFIER = 1 case ONNX_CLASSIFIER = 2 + case MOVING_AVERAGE_CLASSIFIER = 3 public var code: Int { rawValue } } From 9e9ad225be89ffb06192878c2c9c5b9e2f89c6c7 Mon Sep 17 00:00:00 2001 From: Samer Zumot <54731842+samerzumot@users.noreply.github.com> Date: Wed, 2 Sep 2026 17:30:44 -0400 Subject: [PATCH 2/3] Add moving average option to existing classifiers via other_info - Remove MOVING_AVERAGE_CLASSIFIER enum across all language bindings - Implement generic moving average smoothing in BaseClassifier using NVI pattern - Support moving average configuration via other_info (JSON, key-value, integer) - Support multi-channel smoothing with bounded window history - Update automated tests for existing classifiers with moving average --- .../brainflow/brainflow/ml_module_library.cs | 5 +- .../java/brainflow/BrainFlowClassifiers.java | 3 +- julia_package/brainflow/src/ml_model.jl | 1 - .../brainflow/BrainFlowClassifiers.m | 1 - nodejs_package/brainflow/brainflow.types.ts | 1 - python_package/brainflow/ml_model.py | 3 +- .../tests/moving_average_classifier.py | 167 ++++++++++---- rust_package/brainflow/src/ffi/constants.rs | 1 - src/ml/base_classifier.cpp | 217 ++++++++++++++++++ src/ml/build.cmake | 1 - src/ml/dyn_lib_classifier.cpp | 6 +- src/ml/inc/base_classifier.h | 35 ++- src/ml/inc/dyn_lib_classifier.h | 8 +- src/ml/inc/mindfulness_classifier.h | 5 +- src/ml/inc/moving_average_classifier.h | 28 --- src/ml/inc/restfulness_classifier.h | 5 +- src/ml/mindfulness_classifier.cpp | 13 +- src/ml/ml_module.cpp | 5 - src/ml/moving_average_classifier.cpp | 138 ----------- src/ml/onnx/inc/onnx_classifier.h | 7 +- src/ml/onnx/onnx_classifier.cpp | 6 +- src/utils/inc/brainflow_constants.h | 3 +- .../Sources/BrainFlow/BrainFlowEnums.swift | 1 - 23 files changed, 399 insertions(+), 261 deletions(-) delete mode 100644 src/ml/inc/moving_average_classifier.h delete mode 100644 src/ml/moving_average_classifier.cpp diff --git a/csharp_package/brainflow/brainflow/ml_module_library.cs b/csharp_package/brainflow/brainflow/ml_module_library.cs index bdfa6862c..cb6de268c 100644 --- a/csharp_package/brainflow/brainflow/ml_module_library.cs +++ b/csharp_package/brainflow/brainflow/ml_module_library.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; @@ -19,8 +19,7 @@ public enum BrainFlowClassifiers { DEFAULT_CLASSIFIER = 0, DYN_LIB_CLASSIFIER = 1, - ONNX_CLASSIFIER = 2, - MOVING_AVERAGE_CLASSIFIER = 3 + ONNX_CLASSIFIER = 2 }; public static class MLModuleLibrary64 diff --git a/java_package/brainflow/src/main/java/brainflow/BrainFlowClassifiers.java b/java_package/brainflow/src/main/java/brainflow/BrainFlowClassifiers.java index 9ed62c004..e2233f7b5 100644 --- a/java_package/brainflow/src/main/java/brainflow/BrainFlowClassifiers.java +++ b/java_package/brainflow/src/main/java/brainflow/BrainFlowClassifiers.java @@ -8,8 +8,7 @@ public enum BrainFlowClassifiers DEFAULT_CLASSIFIER (0), DYN_LIB_CLASSIFIER (1), - ONNX_CLASSIFIER (2), - MOVING_AVERAGE_CLASSIFIER (3); + ONNX_CLASSIFIER (2); private final int protocol; private static final Map cl_map = new HashMap (); diff --git a/julia_package/brainflow/src/ml_model.jl b/julia_package/brainflow/src/ml_model.jl index e066e5156..cc59e9849 100644 --- a/julia_package/brainflow/src/ml_model.jl +++ b/julia_package/brainflow/src/ml_model.jl @@ -16,7 +16,6 @@ MetricType = Union{BrainFlowMetrics, Integer} DEFAULT_CLASSIFIER = 0 DYN_LIB_CLASSIFIER = 1 ONNX_CLASSIFIER = 2 - MOVING_AVERAGE_CLASSIFIER = 3 end diff --git a/matlab_package/brainflow/BrainFlowClassifiers.m b/matlab_package/brainflow/BrainFlowClassifiers.m index 3aa1fab51..ee4028d91 100644 --- a/matlab_package/brainflow/BrainFlowClassifiers.m +++ b/matlab_package/brainflow/BrainFlowClassifiers.m @@ -4,6 +4,5 @@ DEFAULT_CLASSIFIER(0) DYN_LIB_CLASSIFIER(1) ONNX_CLASSIFIER(2) - MOVING_AVERAGE_CLASSIFIER(3) end end \ No newline at end of file diff --git a/nodejs_package/brainflow/brainflow.types.ts b/nodejs_package/brainflow/brainflow.types.ts index d1683f842..5aabeaed5 100644 --- a/nodejs_package/brainflow/brainflow.types.ts +++ b/nodejs_package/brainflow/brainflow.types.ts @@ -241,7 +241,6 @@ export enum BrainFlowClassifiers { DEFAULT_CLASSIFIER = 0, USER_DEFINED = 1, ONNX_CLASSIFIER = 2, - MOVING_AVERAGE_CLASSIFIER = 3, } export interface IBrainFlowInputParams { diff --git a/python_package/brainflow/ml_model.py b/python_package/brainflow/ml_model.py index a20d40353..c5b95bb53 100644 --- a/python_package/brainflow/ml_model.py +++ b/python_package/brainflow/ml_model.py @@ -27,7 +27,6 @@ class BrainFlowClassifiers(enum.IntEnum): DEFAULT_CLASSIFIER = 0 #: DYN_LIB_CLASSIFIER = 1 #: ONNX_CLASSIFIER = 2 #: - MOVING_AVERAGE_CLASSIFIER = 3 #: class BrainFlowModelParams(object): @@ -39,7 +38,7 @@ class BrainFlowModelParams(object): :type classifier: int :param file: file to load model :type file: str - :param other_info: additional information + :param other_info: additional information or configuration (e.g. moving average smoothing via `{"window_len": 5}` or `moving_average=5`) :type other_info: str :param output_name: output node name :type output_name: str diff --git a/python_package/examples/tests/moving_average_classifier.py b/python_package/examples/tests/moving_average_classifier.py index 270d5bbc8..ea33168bf 100644 --- a/python_package/examples/tests/moving_average_classifier.py +++ b/python_package/examples/tests/moving_average_classifier.py @@ -10,63 +10,144 @@ def test_moving_average_classifier(): - print("Testing MovingAverageClassifier...") + print("Testing moving average option on ML classifiers...") - # 1. Test USER_DEFINED stream with window_len = 3 - params = BrainFlowModelParams( - BrainFlowMetrics.USER_DEFINED.value, - BrainFlowClassifiers.MOVING_AVERAGE_CLASSIFIER.value + v1 = np.array([0.1, 0.2, 0.3, 0.2, 0.2], dtype=np.float64) + v2 = np.array([0.5, 0.1, 0.1, 0.1, 0.2], dtype=np.float64) + + # 1. Baseline: MINDFULNESS + DEFAULT_CLASSIFIER without moving average + raw_params = BrainFlowModelParams( + BrainFlowMetrics.MINDFULNESS.value, + BrainFlowClassifiers.DEFAULT_CLASSIFIER.value ) - params.other_info = "3" + raw_model = MLModel(raw_params) + raw_model.prepare() + raw_score_1 = raw_model.predict(v1)[0] + raw_score_2 = raw_model.predict(v2)[0] + raw_model.release() - model = MLModel(params) - model.prepare() + print(f"Raw scores: v1={raw_score_1:.6f}, v2={raw_score_2:.6f}") + assert raw_score_1 != raw_score_2 - # Step 1: Input 10.0 -> Avg: 10.0 - out1 = model.predict(np.array([10.0], dtype=np.float64)) - print(f"Step 1: In=10.0, Out={out1[0]}") - assert np.isclose(out1[0], 10.0) + # 2. Test JSON config with explicit window_len: '{"window_len": 3}' + mf_params = BrainFlowModelParams( + BrainFlowMetrics.MINDFULNESS.value, + BrainFlowClassifiers.DEFAULT_CLASSIFIER.value + ) + mf_params.other_info = '{"window_len": 3}' + mf_model = MLModel(mf_params) + mf_model.prepare() - # Step 2: Input 20.0 -> Avg: (10 + 20) / 2 = 15.0 - out2 = model.predict(np.array([20.0], dtype=np.float64)) - print(f"Step 2: In=20.0, Out={out2[0]}") - assert np.isclose(out2[0], 15.0) + # Step 1: In=v1 -> out1 = raw_score_1 + out1 = mf_model.predict(v1)[0] + print(f"Step 1 (v1): out={out1:.6f}, expected={raw_score_1:.6f}") + assert np.isclose(out1, raw_score_1) + + # Step 2: In=v2 -> out2 = (raw1 + raw2) / 2 + out2 = mf_model.predict(v2)[0] + expected_2 = (raw_score_1 + raw_score_2) / 2.0 + print(f"Step 2 (v2): out={out2:.6f}, expected={expected_2:.6f}") + assert np.isclose(out2, expected_2) + + # Step 3: In=v2 -> out3 = (raw1 + raw2 + raw2) / 3 + out3 = mf_model.predict(v2)[0] + expected_3 = (raw_score_1 + 2.0 * raw_score_2) / 3.0 + print(f"Step 3 (v2): out={out3:.6f}, expected={expected_3:.6f}") + assert np.isclose(out3, expected_3) + + # Step 4: In=v2 -> out4 = (raw2 + raw2 + raw2) / 3 = raw2 (oldest raw1 popped!) + out4 = mf_model.predict(v2)[0] + expected_4 = raw_score_2 + print(f"Step 4 (v2): out={out4:.6f}, expected={expected_4:.6f}") + assert np.isclose(out4, expected_4) - # Step 3: Input 30.0 -> Avg: (10 + 20 + 30) / 3 = 20.0 - out3 = model.predict(np.array([30.0], dtype=np.float64)) - print(f"Step 3: In=30.0, Out={out3[0]}") - assert np.isclose(out3[0], 20.0) + mf_model.release() - # Step 4: Input 40.0 -> Avg: (20 + 30 + 40) / 3 = 30.0 (oldest 10.0 dropped) - out4 = model.predict(np.array([40.0], dtype=np.float64)) - print(f"Step 4: In=40.0, Out={out4[0]}") - assert np.isclose(out4[0], 30.0) + # 3. Test RESTFULNESS metric with moving average: '{"moving_average": true, "window_len": 2}' + rf_params = BrainFlowModelParams( + BrainFlowMetrics.RESTFULNESS.value, + BrainFlowClassifiers.DEFAULT_CLASSIFIER.value + ) + rf_params.other_info = '{"moving_average": true, "window_len": 2}' + rf_model = MLModel(rf_params) + rf_model.prepare() - model.release() + raw_rf_1 = 1.0 - raw_score_1 + raw_rf_2 = 1.0 - raw_score_2 - # 2. Test MINDFULNESS metric with MOVING_AVERAGE_CLASSIFIER - mf_params = BrainFlowModelParams( - BrainFlowMetrics.MINDFULNESS.value, - BrainFlowClassifiers.MOVING_AVERAGE_CLASSIFIER.value - ) - mf_params.other_info = '{"window_len": 4}' + rf_out1 = rf_model.predict(v1)[0] + assert np.isclose(rf_out1, raw_rf_1) - mf_model = MLModel(mf_params) - mf_model.prepare() + rf_out2 = rf_model.predict(v2)[0] + assert np.isclose(rf_out2, (raw_rf_1 + raw_rf_2) / 2.0) + + rf_model.release() - # 5 band powers input - feature_vector = np.array([0.1, 0.2, 0.3, 0.2, 0.2], dtype=np.float64) - mf_out1 = mf_model.predict(feature_vector) - print(f"Mindfulness moving avg 1: {mf_out1[0]}") - assert 0.0 <= mf_out1[0] <= 1.0 + # 4. Test default window fallback: '{"moving_average": true}' (default window_len = 5) + def_params = BrainFlowModelParams( + BrainFlowMetrics.MINDFULNESS.value, + BrainFlowClassifiers.DEFAULT_CLASSIFIER.value + ) + def_params.other_info = '{"moving_average": true}' + def_model = MLModel(def_params) + def_model.prepare() - mf_out2 = mf_model.predict(feature_vector) - print(f"Mindfulness moving avg 2: {mf_out2[0]}") - assert np.isclose(mf_out1[0], mf_out2[0]) + # Feed 5 identical samples, then a 6th different sample + for _ in range(5): + def_model.predict(v1) + # The 6th prediction should be (4 * raw1 + 1 * raw2) / 5 + def_out6 = def_model.predict(v2)[0] + assert np.isclose(def_out6, (4.0 * raw_score_1 + raw_score_2) / 5.0) - mf_model.release() + def_model.release() - print("All MovingAverageClassifier tests passed successfully!") + # 5. Test key-value string format: 'moving_average=3' + kv_params = BrainFlowModelParams( + BrainFlowMetrics.MINDFULNESS.value, + BrainFlowClassifiers.DEFAULT_CLASSIFIER.value + ) + kv_params.other_info = "moving_average=3" + kv_model = MLModel(kv_params) + kv_model.prepare() + + kv_out1 = kv_model.predict(v1)[0] + assert np.isclose(kv_out1, raw_score_1) + kv_out2 = kv_model.predict(v2)[0] + assert np.isclose(kv_out2, (raw_score_1 + raw_score_2) / 2.0) + kv_model.release() + + # 6. Test integer string format: '3' + num_params = BrainFlowModelParams( + BrainFlowMetrics.MINDFULNESS.value, + BrainFlowClassifiers.DEFAULT_CLASSIFIER.value + ) + num_params.other_info = "3" + num_model = MLModel(num_params) + num_model.prepare() + + num_out1 = num_model.predict(v1)[0] + assert np.isclose(num_out1, raw_score_1) + num_out2 = num_model.predict(v2)[0] + assert np.isclose(num_out2, (raw_score_1 + raw_score_2) / 2.0) + num_model.release() + + # 7. Negative test: Unrelated other_info string does NOT activate moving average + neg_params = BrainFlowModelParams( + BrainFlowMetrics.MINDFULNESS.value, + BrainFlowClassifiers.DEFAULT_CLASSIFIER.value + ) + neg_params.other_info = '{"unrelated_key": "some_value"}' + neg_model = MLModel(neg_params) + neg_model.prepare() + + neg_out1 = neg_model.predict(v1)[0] + assert np.isclose(neg_out1, raw_score_1) + neg_out2 = neg_model.predict(v2)[0] + # Since moving average is NOT enabled, out2 should be raw_score_2, NOT an average + assert np.isclose(neg_out2, raw_score_2) + neg_model.release() + + print("All moving average classifier tests passed successfully!") if __name__ == '__main__': diff --git a/rust_package/brainflow/src/ffi/constants.rs b/rust_package/brainflow/src/ffi/constants.rs index a9637ea53..37d78ddfa 100644 --- a/rust_package/brainflow/src/ffi/constants.rs +++ b/rust_package/brainflow/src/ffi/constants.rs @@ -157,7 +157,6 @@ pub enum BrainFlowClassifiers { DefaultClassifier = 0, DynLibClassifier = 1, OnnxClassifier = 2, - MovingAverageClassifier = 3, } #[repr(i32)] #[derive(FromPrimitive, ToPrimitive, Debug, Copy, Clone, Hash, PartialEq, Eq)] diff --git a/src/ml/base_classifier.cpp b/src/ml/base_classifier.cpp index 7d227ac6d..8d7d4651f 100644 --- a/src/ml/base_classifier.cpp +++ b/src/ml/base_classifier.cpp @@ -1,7 +1,14 @@ +#include +#include +#include +#include + #include "base_classifier.h" #include "brainflow_constants.h" +#include "json.hpp" #include "spdlog/sinks/null_sink.h" +using json = nlohmann::json; #define LOGGER_NAME "ml_logger" @@ -60,3 +67,213 @@ int BaseClassifier::set_log_file (const char *log_file) return (int)BrainFlowExitCodes::STATUS_OK; #endif } + +void BaseClassifier::parse_moving_average_params () +{ + use_moving_average = false; + moving_average_window = 0; + + if (params.other_info.empty ()) + { + return; + } + + std::string info = params.other_info; + try + { + if (info.find ('{') != std::string::npos) + { + json j = json::parse (info); + if (j.contains ("moving_average")) + { + if (j["moving_average"].is_boolean ()) + { + use_moving_average = j["moving_average"].get (); + if (use_moving_average) + { + moving_average_window = DEFAULT_MOVING_AVERAGE_WINDOW; + } + } + else if (j["moving_average"].is_number_integer ()) + { + moving_average_window = j["moving_average"].get (); + use_moving_average = (moving_average_window > 0); + } + } + if (j.contains ("window_len")) + { + moving_average_window = j["window_len"].get (); + use_moving_average = true; + } + else if (j.contains ("period")) + { + moving_average_window = j["period"].get (); + use_moving_average = true; + } + } + else if (info.find ('=') != std::string::npos) + { + std::stringstream ss (info); + std::string token; + while (std::getline (ss, token, ';')) + { + size_t eq = token.find ('='); + if (eq != std::string::npos) + { + std::string k = token.substr (0, eq); + std::string v = token.substr (eq + 1); + k.erase (0, k.find_first_not_of (" \t\r\n")); + k.erase (k.find_last_not_of (" \t\r\n") + 1); + v.erase (0, v.find_first_not_of (" \t\r\n")); + v.erase (v.find_last_not_of (" \t\r\n") + 1); + std::transform (k.begin (), k.end (), k.begin (), ::tolower); + std::transform (v.begin (), v.end (), v.begin (), ::tolower); + + if (k == "moving_average") + { + if (v == "true" || v == "1") + { + use_moving_average = true; + if (moving_average_window <= 0) + { + moving_average_window = DEFAULT_MOVING_AVERAGE_WINDOW; + } + } + else if (v == "false" || v == "0") + { + use_moving_average = false; + } + else + { + try + { + moving_average_window = std::stoi (v); + use_moving_average = (moving_average_window > 0); + } + catch (...) + { + } + } + } + else if (k == "window_len" || k == "period") + { + try + { + moving_average_window = std::stoi (v); + use_moving_average = true; + } + catch (...) + { + } + } + } + } + } + else + { + try + { + size_t idx = 0; + int val = std::stoi (info, &idx); + if ((idx == info.length ()) && (val > 0)) + { + moving_average_window = val; + use_moving_average = true; + } + } + catch (...) + { + std::string lower_info = info; + lower_info.erase (0, lower_info.find_first_not_of (" \t\r\n")); + lower_info.erase (lower_info.find_last_not_of (" \t\r\n") + 1); + std::transform ( + lower_info.begin (), lower_info.end (), lower_info.begin (), ::tolower); + if ((lower_info == "moving_average") || (lower_info == "true")) + { + use_moving_average = true; + moving_average_window = DEFAULT_MOVING_AVERAGE_WINDOW; + } + } + } + } + catch (std::exception &e) + { + safe_logger (spdlog::level::warn, + "Failed to parse moving average from other_info: {}. Moving average disabled.", + e.what ()); + use_moving_average = false; + moving_average_window = 0; + } + + if (use_moving_average) + { + if (moving_average_window <= 0) + { + safe_logger (spdlog::level::warn, + "Invalid moving average window ({}), using default {}.", moving_average_window, + DEFAULT_MOVING_AVERAGE_WINDOW); + moving_average_window = DEFAULT_MOVING_AVERAGE_WINDOW; + } + } +} + +void BaseClassifier::reset_moving_average () +{ + window_data.clear (); +} + +void BaseClassifier::apply_moving_average (double *output, int *output_len) +{ + if ((output == NULL) || (output_len == NULL) || (*output_len <= 0)) + { + return; + } + + if (window_data.empty () || ((int)window_data.front ().size () != *output_len)) + { + window_data.clear (); + } + + window_data.push_back (std::vector (output, output + *output_len)); + while ((int)window_data.size () > moving_average_window) + { + window_data.pop_front (); + } + + for (int i = 0; i < *output_len; i++) + { + double sum = 0.0; + for (size_t w = 0; w < window_data.size (); w++) + { + sum += window_data[w][i]; + } + output[i] = sum / window_data.size (); + } +} + +int BaseClassifier::prepare () +{ + parse_moving_average_params (); + reset_moving_average (); + return prepare_classifier (); +} + +int BaseClassifier::predict (double *data, int data_len, double *output, int *output_len) +{ + int res = calculate (data, data_len, output, output_len); + if (res != (int)BrainFlowExitCodes::STATUS_OK) + { + return res; + } + if (use_moving_average) + { + apply_moving_average (output, output_len); + } + return (int)BrainFlowExitCodes::STATUS_OK; +} + +int BaseClassifier::release () +{ + reset_moving_average (); + return release_classifier (); +} diff --git a/src/ml/build.cmake b/src/ml/build.cmake index e03054739..bddbbe91c 100644 --- a/src/ml/build.cmake +++ b/src/ml/build.cmake @@ -29,7 +29,6 @@ SET (ML_MODULE_SRC ${CMAKE_CURRENT_LIST_DIR}/base_classifier.cpp ${CMAKE_CURRENT_LIST_DIR}/mindfulness_classifier.cpp ${CMAKE_CURRENT_LIST_DIR}/generated/mindfulness_model.cpp - ${CMAKE_CURRENT_LIST_DIR}/moving_average_classifier.cpp ) add_library ( diff --git a/src/ml/dyn_lib_classifier.cpp b/src/ml/dyn_lib_classifier.cpp index 269afbd0b..5578efd12 100644 --- a/src/ml/dyn_lib_classifier.cpp +++ b/src/ml/dyn_lib_classifier.cpp @@ -2,7 +2,7 @@ #include "brainflow_constants.h" -int DynLibClassifier::prepare () +int DynLibClassifier::prepare_classifier () { if (dll_loader != NULL) { @@ -33,7 +33,7 @@ int DynLibClassifier::prepare () return func ((void *)this, ¶ms); } -int DynLibClassifier::predict (double *data, int data_len, double *output, int *output_len) +int DynLibClassifier::calculate (double *data, int data_len, double *output, int *output_len) { if (dll_loader == NULL) { @@ -50,7 +50,7 @@ int DynLibClassifier::predict (double *data, int data_len, double *output, int * return func (data, data_len, output, output_len, ¶ms); } -int DynLibClassifier::release () +int DynLibClassifier::release_classifier () { if (dll_loader == NULL) { diff --git a/src/ml/inc/base_classifier.h b/src/ml/inc/base_classifier.h index ccd7abd33..bc138be1c 100644 --- a/src/ml/inc/base_classifier.h +++ b/src/ml/inc/base_classifier.h @@ -1,8 +1,15 @@ #pragma once +#include +#include +#include + +#include "brainflow_constants.h" #include "brainflow_model_params.h" #include "spdlog/spdlog.h" +#define DEFAULT_MOVING_AVERAGE_WINDOW 5 + class BaseClassifier { public: @@ -16,12 +23,15 @@ class BaseClassifier BaseClassifier (struct BrainFlowModelParams model_params) : params (model_params) { skip_logs = false; + use_moving_average = false; + moving_average_window = 0; } virtual ~BaseClassifier () { skip_logs = true; } + // Classifier ml_logger should not be called from destructors, to ensure that there are safe log // methods Classifierml_logger still available but should be used only outside destructors template @@ -45,7 +55,26 @@ class BaseClassifier } } - virtual int prepare () = 0; - virtual int predict (double *data, int data_len, double *output, int *output_len) = 0; - virtual int release () = 0; + virtual int prepare (); + virtual int predict (double *data, int data_len, double *output, int *output_len); + virtual int release (); + +protected: + bool use_moving_average; + int moving_average_window; + std::deque> window_data; + + void parse_moving_average_params (); + void reset_moving_average (); + void apply_moving_average (double *output, int *output_len); + + virtual int prepare_classifier () + { + return (int)BrainFlowExitCodes::STATUS_OK; + } + virtual int calculate (double *data, int data_len, double *output, int *output_len) = 0; + virtual int release_classifier () + { + return (int)BrainFlowExitCodes::STATUS_OK; + } }; diff --git a/src/ml/inc/dyn_lib_classifier.h b/src/ml/inc/dyn_lib_classifier.h index 28a075eff..a8df3cbd6 100644 --- a/src/ml/inc/dyn_lib_classifier.h +++ b/src/ml/inc/dyn_lib_classifier.h @@ -20,11 +20,11 @@ class DynLibClassifier : public BaseClassifier release (); } - int prepare () override; - int predict (double *data, int data_len, double *output, int *output_len) override; - int release () override; - protected: + int prepare_classifier () override; + int calculate (double *data, int data_len, double *output, int *output_len) override; + int release_classifier () override; + virtual std::string get_dyn_lib_path () { return params.file; diff --git a/src/ml/inc/mindfulness_classifier.h b/src/ml/inc/mindfulness_classifier.h index 0f6007be5..4958d09c8 100644 --- a/src/ml/inc/mindfulness_classifier.h +++ b/src/ml/inc/mindfulness_classifier.h @@ -16,7 +16,6 @@ class MindfulnessClassifier : public BaseClassifier release (); } - int prepare () override; - int predict (double *data, int data_len, double *output, int *output_len) override; - int release () override; +protected: + int calculate (double *data, int data_len, double *output, int *output_len) override; }; diff --git a/src/ml/inc/moving_average_classifier.h b/src/ml/inc/moving_average_classifier.h deleted file mode 100644 index 7c3c7fd0d..000000000 --- a/src/ml/inc/moving_average_classifier.h +++ /dev/null @@ -1,28 +0,0 @@ -#pragma once - -#include -#include - -#include "base_classifier.h" -#include "brainflow_constants.h" -#include "brainflow_model_params.h" - - -class MovingAverageClassifier : public BaseClassifier -{ -protected: - int window_len; - std::deque buffer; - double sum; - std::shared_ptr base_classifier; - - int parse_window_len (); - -public: - MovingAverageClassifier (struct BrainFlowModelParams params); - ~MovingAverageClassifier (); - - int prepare () override; - int predict (double *data, int data_len, double *output, int *output_len) override; - int release () override; -}; diff --git a/src/ml/inc/restfulness_classifier.h b/src/ml/inc/restfulness_classifier.h index dcfa9252f..6c97ce3b2 100644 --- a/src/ml/inc/restfulness_classifier.h +++ b/src/ml/inc/restfulness_classifier.h @@ -11,9 +11,10 @@ class RestfulnessClassifier : public MindfulnessClassifier { } - int predict (double *data, int data_len, double *output, int *output_len) override +protected: + int calculate (double *data, int data_len, double *output, int *output_len) override { - int res = MindfulnessClassifier::predict (data, data_len, output, output_len); + int res = MindfulnessClassifier::calculate (data, data_len, output, output_len); if (res != (int)BrainFlowExitCodes::STATUS_OK) { return res; diff --git a/src/ml/mindfulness_classifier.cpp b/src/ml/mindfulness_classifier.cpp index 62c52c611..dd4875dbb 100644 --- a/src/ml/mindfulness_classifier.cpp +++ b/src/ml/mindfulness_classifier.cpp @@ -7,12 +7,8 @@ #include "mindfulness_model.h" -int MindfulnessClassifier::prepare () -{ - return (int)BrainFlowExitCodes::STATUS_OK; -} - -int MindfulnessClassifier::predict (double *data, int data_len, double *output, int *output_len) +int MindfulnessClassifier::calculate ( + double *data, int data_len, double *output, int *output_len) { if ((data_len < 5) || (data == NULL) || (output == NULL)) { @@ -29,9 +25,4 @@ int MindfulnessClassifier::predict (double *data, int data_len, double *output, *output = mindfulness; *output_len = 1; return (int)BrainFlowExitCodes::STATUS_OK; -} - -int MindfulnessClassifier::release () -{ - return (int)BrainFlowExitCodes::STATUS_OK; } \ No newline at end of file diff --git a/src/ml/ml_module.cpp b/src/ml/ml_module.cpp index 45f8c33c8..55e8ebaaf 100644 --- a/src/ml/ml_module.cpp +++ b/src/ml/ml_module.cpp @@ -11,7 +11,6 @@ #include "dyn_lib_classifier.h" #include "mindfulness_classifier.h" #include "ml_module.h" -#include "moving_average_classifier.h" #include "onnx_classifier.h" #include "restfulness_classifier.h" @@ -63,10 +62,6 @@ int prepare (const char *json_params) { model = std::shared_ptr (new RestfulnessClassifier (key)); } - else if (key.classifier == (int)BrainFlowClassifiers::MOVING_AVERAGE_CLASSIFIER) - { - model = std::shared_ptr (new MovingAverageClassifier (key)); - } else { return (int)BrainFlowExitCodes::UNSUPPORTED_CLASSIFIER_AND_METRIC_COMBINATION_ERROR; diff --git a/src/ml/moving_average_classifier.cpp b/src/ml/moving_average_classifier.cpp deleted file mode 100644 index 535346891..000000000 --- a/src/ml/moving_average_classifier.cpp +++ /dev/null @@ -1,138 +0,0 @@ -#include -#include -#include - -#include "brainflow_constants.h" -#include "json.hpp" -#include "mindfulness_classifier.h" -#include "moving_average_classifier.h" -#include "restfulness_classifier.h" - -using json = nlohmann::json; - - -MovingAverageClassifier::MovingAverageClassifier (struct BrainFlowModelParams model_params) - : BaseClassifier (model_params) -{ - window_len = 5; - sum = 0.0; - base_classifier = NULL; - - if (params.metric == (int)BrainFlowMetrics::MINDFULNESS) - { - base_classifier = std::shared_ptr (new MindfulnessClassifier (params)); - } - else if (params.metric == (int)BrainFlowMetrics::RESTFULNESS) - { - base_classifier = std::shared_ptr (new RestfulnessClassifier (params)); - } -} - -MovingAverageClassifier::~MovingAverageClassifier () -{ - buffer.clear (); - sum = 0.0; - base_classifier = NULL; -} - -int MovingAverageClassifier::parse_window_len () -{ - int len = 5; - if (!params.other_info.empty ()) - { - try - { - if (params.other_info.find ("{") != std::string::npos) - { - json j = json::parse (params.other_info); - if (j.contains ("window_len")) - { - len = j["window_len"].get (); - } - else if (j.contains ("period")) - { - len = j["period"].get (); - } - } - else - { - len = std::stoi (params.other_info); - } - } - catch (...) - { - safe_logger (spdlog::level::warn, - "Unable to parse window_len from other_info: {}. Using default value of 5.", - params.other_info); - len = 5; - } - } - if (len <= 0) - { - len = 5; - } - return len; -} - -int MovingAverageClassifier::prepare () -{ - buffer.clear (); - sum = 0.0; - window_len = parse_window_len (); - - if (base_classifier != NULL) - { - return base_classifier->prepare (); - } - return (int)BrainFlowExitCodes::STATUS_OK; -} - -int MovingAverageClassifier::predict ( - double *data, int data_len, double *output, int *output_len) -{ - if ((data == NULL) || (output == NULL) || (data_len <= 0)) - { - safe_logger (spdlog::level::err, "Incorrect arguments for predict."); - return (int)BrainFlowExitCodes::INVALID_ARGUMENTS_ERROR; - } - - double raw_score = 0.0; - if (base_classifier != NULL) - { - double base_output = 0.0; - int base_output_len = 0; - int res = base_classifier->predict (data, data_len, &base_output, &base_output_len); - if (res != (int)BrainFlowExitCodes::STATUS_OK) - { - return res; - } - raw_score = base_output; - } - else - { - raw_score = data[0]; - } - - buffer.push_back (raw_score); - sum += raw_score; - if ((int)buffer.size () > window_len) - { - sum -= buffer.front (); - buffer.pop_front (); - } - - *output = sum / buffer.size (); - *output_len = 1; - return (int)BrainFlowExitCodes::STATUS_OK; -} - -int MovingAverageClassifier::release () -{ - buffer.clear (); - sum = 0.0; - if (base_classifier != NULL) - { - return base_classifier->release (); - } - return (int)BrainFlowExitCodes::STATUS_OK; -} diff --git a/src/ml/onnx/inc/onnx_classifier.h b/src/ml/onnx/inc/onnx_classifier.h index 18b8cc216..525769220 100644 --- a/src/ml/onnx/inc/onnx_classifier.h +++ b/src/ml/onnx/inc/onnx_classifier.h @@ -51,7 +51,8 @@ class OnnxClassifier : public BaseClassifier release (); } - int prepare () override; - int predict (double *data, int data_len, double *output, int *output_len) override; - int release () override; +protected: + int prepare_classifier () override; + int calculate (double *data, int data_len, double *output, int *output_len) override; + int release_classifier () override; }; diff --git a/src/ml/onnx/onnx_classifier.cpp b/src/ml/onnx/onnx_classifier.cpp index 02d99724b..eb2678eb5 100644 --- a/src/ml/onnx/onnx_classifier.cpp +++ b/src/ml/onnx/onnx_classifier.cpp @@ -14,7 +14,7 @@ void log_onnx_msg (void *param, OrtLoggingLevel severity, const char *category, } } -int OnnxClassifier::prepare () +int OnnxClassifier::prepare_classifier () { if (dll_loader != NULL) { @@ -62,7 +62,7 @@ int OnnxClassifier::prepare () } -int OnnxClassifier::predict (double *data, int data_len, double *output, int *output_len) +int OnnxClassifier::calculate (double *data, int data_len, double *output, int *output_len) { int res = (int)BrainFlowExitCodes::STATUS_OK; if (ort == NULL) @@ -390,7 +390,7 @@ int OnnxClassifier::predict (double *data, int data_len, double *output, int *ou return res; } -int OnnxClassifier::release () +int OnnxClassifier::release_classifier () { if ((allocator != NULL) && (ort != NULL)) { diff --git a/src/utils/inc/brainflow_constants.h b/src/utils/inc/brainflow_constants.h index de51574ee..8b31b0df2 100644 --- a/src/utils/inc/brainflow_constants.h +++ b/src/utils/inc/brainflow_constants.h @@ -152,8 +152,7 @@ enum class BrainFlowClassifiers : int { DEFAULT_CLASSIFIER = 0, DYN_LIB_CLASSIFIER = 1, - ONNX_CLASSIFIER = 2, - MOVING_AVERAGE_CLASSIFIER = 3 + ONNX_CLASSIFIER = 2 }; enum class BrainFlowPresets : int diff --git a/swift_package/Sources/BrainFlow/BrainFlowEnums.swift b/swift_package/Sources/BrainFlow/BrainFlowEnums.swift index f3031bdbd..9e98fa9a0 100644 --- a/swift_package/Sources/BrainFlow/BrainFlowEnums.swift +++ b/swift_package/Sources/BrainFlow/BrainFlowEnums.swift @@ -126,7 +126,6 @@ public enum BrainFlowClassifiers: Int, CaseIterable, Sendable { case DEFAULT_CLASSIFIER = 0 case DYN_LIB_CLASSIFIER = 1 case ONNX_CLASSIFIER = 2 - case MOVING_AVERAGE_CLASSIFIER = 3 public var code: Int { rawValue } } From d219dac5997e005d0cac3ae94810803f0af7d993 Mon Sep 17 00:00:00 2001 From: Samer Zumot <54731842+samerzumot@users.noreply.github.com> Date: Wed, 2 Sep 2026 17:39:03 -0400 Subject: [PATCH 3/3] Address review polish: NVI non-virtual methods, plugin docs, vector test, newline and BOM fixes - Make BaseClassifier public NVI methods non-virtual - Document other_info moving average behavior for DynLibClassifier plugin authors - Add multi-element vector output moving average test with DynLibClassifier - Fix missing trailing newlines in mindfulness_classifier.cpp and BrainFlowClassifiers.m - Remove BOM in C# library file to clean up diff --- .../brainflow/brainflow/ml_module_library.cs | 2 +- .../brainflow/BrainFlowClassifiers.m | 2 +- python_package/brainflow/ml_model.py | 2 +- .../tests/moving_average_classifier.py | 61 +++++++++++++++++++ src/ml/inc/base_classifier.h | 6 +- src/ml/inc/dyn_lib_classifier.h | 6 ++ src/ml/mindfulness_classifier.cpp | 2 +- 7 files changed, 74 insertions(+), 7 deletions(-) diff --git a/csharp_package/brainflow/brainflow/ml_module_library.cs b/csharp_package/brainflow/brainflow/ml_module_library.cs index cb6de268c..a0542397b 100644 --- a/csharp_package/brainflow/brainflow/ml_module_library.cs +++ b/csharp_package/brainflow/brainflow/ml_module_library.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Runtime.InteropServices; diff --git a/matlab_package/brainflow/BrainFlowClassifiers.m b/matlab_package/brainflow/BrainFlowClassifiers.m index ee4028d91..b70e4202a 100644 --- a/matlab_package/brainflow/BrainFlowClassifiers.m +++ b/matlab_package/brainflow/BrainFlowClassifiers.m @@ -5,4 +5,4 @@ DYN_LIB_CLASSIFIER(1) ONNX_CLASSIFIER(2) end -end \ No newline at end of file +end diff --git a/python_package/brainflow/ml_model.py b/python_package/brainflow/ml_model.py index c5b95bb53..0119f3de3 100644 --- a/python_package/brainflow/ml_model.py +++ b/python_package/brainflow/ml_model.py @@ -38,7 +38,7 @@ class BrainFlowModelParams(object): :type classifier: int :param file: file to load model :type file: str - :param other_info: additional information or configuration (e.g. moving average smoothing via `{"window_len": 5}` or `moving_average=5`) + :param other_info: additional information or configuration (e.g. moving average smoothing via `{"window_len": 5}` or `moving_average=5`). Note: moving average options in other_info are handled uniformly across all classifiers. :type other_info: str :param output_name: output node name :type output_name: str diff --git a/python_package/examples/tests/moving_average_classifier.py b/python_package/examples/tests/moving_average_classifier.py index ea33168bf..55aa30e98 100644 --- a/python_package/examples/tests/moving_average_classifier.py +++ b/python_package/examples/tests/moving_average_classifier.py @@ -147,8 +147,69 @@ def test_moving_average_classifier(): assert np.isclose(neg_out2, raw_score_2) neg_model.release() + # 8. Test multi-element (vector) output moving average with DYN_LIB_CLASSIFIER + import platform + import shutil + import subprocess + import tempfile + + cc = shutil.which('clang') or shutil.which('gcc') or shutil.which('cc') + if cc: + with tempfile.TemporaryDirectory() as tmpdir: + c_code = """ +#if defined(_WIN32) +#define EXPORT __declspec(dllexport) +#else +#define EXPORT __attribute__((visibility("default"))) +#endif + +EXPORT int prepare(void *cls, void *params) { return 0; } +EXPORT int predict(double *data, int data_len, double *output, int *output_len, void *params) { + output[0] = (data_len > 0) ? data[0] : 1.0; + output[1] = (data_len > 1) ? data[1] * 2.0 : 2.0; + output[2] = (data_len > 2) ? data[2] * 3.0 : 3.0; + *output_len = 3; + return 0; +} +EXPORT int release(void *params) { return 0; } +""" + c_file = os.path.join(tmpdir, 'plugin.c') + ext = '.dylib' if platform.system() == 'Darwin' else ('.dll' if platform.system() == 'Windows' else '.so') + so_file = os.path.join(tmpdir, 'libplugin' + ext) + with open(c_file, 'w') as f: + f.write(c_code) + cmd = [cc, '-shared', '-fPIC', c_file, '-o', so_file] + if subprocess.call(cmd) == 0: + vec_params = BrainFlowModelParams( + BrainFlowMetrics.USER_DEFINED.value, + BrainFlowClassifiers.DYN_LIB_CLASSIFIER.value + ) + vec_params.file = so_file + vec_params.other_info = '{"window_len": 2}' + vec_model = MLModel(vec_params) + vec_model.prepare() + + # Feed sample 1: [10, 10, 10] -> plugin returns [10, 20, 30] + vec_out1 = vec_model.predict(np.array([10.0, 10.0, 10.0], dtype=np.float64)) + assert len(vec_out1) == 3 + assert np.allclose(vec_out1, [10.0, 20.0, 30.0]) + + # Feed sample 2: [20, 20, 20] -> plugin returns [20, 40, 60] -> window avg = [15, 30, 45] + vec_out2 = vec_model.predict(np.array([20.0, 20.0, 20.0], dtype=np.float64)) + assert len(vec_out2) == 3 + assert np.allclose(vec_out2, [15.0, 30.0, 45.0]) + + # Feed sample 3: [20, 20, 20] -> window pops sample 1 -> avg = [20, 40, 60] + vec_out3 = vec_model.predict(np.array([20.0, 20.0, 20.0], dtype=np.float64)) + assert len(vec_out3) == 3 + assert np.allclose(vec_out3, [20.0, 40.0, 60.0]) + + vec_model.release() + print("Vector output moving average test passed successfully!") + print("All moving average classifier tests passed successfully!") if __name__ == '__main__': test_moving_average_classifier() + diff --git a/src/ml/inc/base_classifier.h b/src/ml/inc/base_classifier.h index bc138be1c..b287481ae 100644 --- a/src/ml/inc/base_classifier.h +++ b/src/ml/inc/base_classifier.h @@ -55,9 +55,9 @@ class BaseClassifier } } - virtual int prepare (); - virtual int predict (double *data, int data_len, double *output, int *output_len); - virtual int release (); + int prepare (); + int predict (double *data, int data_len, double *output, int *output_len); + int release (); protected: bool use_moving_average; diff --git a/src/ml/inc/dyn_lib_classifier.h b/src/ml/inc/dyn_lib_classifier.h index a8df3cbd6..c5fd95aa1 100644 --- a/src/ml/inc/dyn_lib_classifier.h +++ b/src/ml/inc/dyn_lib_classifier.h @@ -6,6 +6,12 @@ #include "runtime_dll_loader.h" +// DynLibClassifier loads user-provided shared libraries (.so / .dll / .dylib) exporting +// "prepare", "predict", and "release" C functions. +// Note for plugin authors: BaseClassifier inspects params.other_info for moving average +// configuration ("moving_average", "window_len", "period", or bare positive integer / "true"). +// If enabled, moving average smoothing is applied to the plugin's output. To avoid unintended +// smoothing, custom plugins using other_info should use unique JSON or key-value keys. class DynLibClassifier : public BaseClassifier { public: diff --git a/src/ml/mindfulness_classifier.cpp b/src/ml/mindfulness_classifier.cpp index dd4875dbb..6f1ea55ac 100644 --- a/src/ml/mindfulness_classifier.cpp +++ b/src/ml/mindfulness_classifier.cpp @@ -25,4 +25,4 @@ int MindfulnessClassifier::calculate ( *output = mindfulness; *output_len = 1; return (int)BrainFlowExitCodes::STATUS_OK; -} \ No newline at end of file +}