From 7fc868d7046e08029ab788faf6ed28035db49ac4 Mon Sep 17 00:00:00 2001 From: Aditya Singh Date: Wed, 5 Aug 2026 05:01:09 -0700 Subject: [PATCH 1/3] Fix flat line detection and peak precision in get_railed_percentage The straight line check compared abs (raw_data[i - 1]) against the signed raw_data[i] because the closing parenthesis was placed after the abs call instead of around the difference. That made the test sign dependent, so a channel stuck at a negative value was never reported as railed and a monotonically rising positive signal was always reported as 100 percent railed. The running maximum was also stored in an int, which discarded the fractional part of every peak and collapsed sub microvolt peaks to zero. Store the maximum as a double and take the absolute value of the difference between consecutive samples, matching the pattern already used by detect_peaks_z_score in the same file. --- src/data_handler/data_handler.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/data_handler/data_handler.cpp b/src/data_handler/data_handler.cpp index 106c614dd..6ba7c63e4 100644 --- a/src/data_handler/data_handler.cpp +++ b/src/data_handler/data_handler.cpp @@ -1438,7 +1438,7 @@ int get_railed_percentage (double *raw_data, int data_len, int gain, double *out double scaler = (4.5 / (pow (2, 23) - 1) / gain * 1000000.); double max_val = scaler * pow (2, 23); - int cur_max = abs (raw_data[0]); + double cur_max = abs (raw_data[0]); bool is_straight_line = true; for (int i = 1; i < data_len; i++) { @@ -1446,7 +1446,7 @@ int get_railed_percentage (double *raw_data, int data_len, int gain, double *out { cur_max = abs (raw_data[i]); } - if (((abs (raw_data[i - 1]) - raw_data[i]) > 0.00001) && (abs (raw_data[i]) > 0.00001)) + if ((abs (raw_data[i - 1] - raw_data[i]) > 0.00001) && (abs (raw_data[i]) > 0.00001)) { is_straight_line = false; } From d411140afe4743a9469edb434f5955ac585fae8a Mon Sep 17 00:00:00 2001 From: Aditya Singh Date: Thu, 6 Aug 2026 20:34:55 -0700 Subject: [PATCH 2/3] Add railed percentage regression test Covers positive and negative flat signals, ascending and descending ramps, fractional amplitudes below 1, and signals crossing or ending at zero. The zero cases pin the existing semantics rather than changing them: a step into zero is suppressed, a step back out of zero is not. --- .../examples/tests/railed_percentage.py | 67 +++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 python_package/examples/tests/railed_percentage.py diff --git a/python_package/examples/tests/railed_percentage.py b/python_package/examples/tests/railed_percentage.py new file mode 100644 index 000000000..78b6506f1 --- /dev/null +++ b/python_package/examples/tests/railed_percentage.py @@ -0,0 +1,67 @@ +import numpy as np + +from brainflow.data_filter import DataFilter + +GAIN = 24 +# Same full-scale value get_railed_percentage derives internally, so the expected +# percentages below are readable rather than magic numbers. +SCALER = 4.5 / (2 ** 23 - 1) / GAIN * 1000000.0 +MAX_VAL = SCALER * (2 ** 23) + +STRAIGHT_LINE = 100.0 + + +def expected_percentage(peak): + return peak / MAX_VAL * 100.0 + + +def railed(values): + return DataFilter.get_railed_percentage(np.array(values, dtype=np.float64), GAIN) + + +def main(): + # A flat line is fully railed regardless of sign. The comparison used to be + # abs(previous) - current, so a negative flat line produced a large positive + # difference and was reported as varying signal. + assert railed([5.0] * 8) == STRAIGHT_LINE + assert railed([-5.0] * 8) == STRAIGHT_LINE + + # A monotonic ramp is not a flat line. With the old comparison an increasing ramp + # always gave a negative difference, so it never tripped the check and every ramp + # was reported as 100 percent railed. + ramp = [float(i) for i in range(8)] + assert railed(ramp) != STRAIGHT_LINE + assert np.isclose(railed(ramp), expected_percentage(7.0)) + + # A descending ramp was already handled, so this pins that the fix did not lose it. + assert np.isclose(railed(ramp[::-1]), expected_percentage(7.0)) + + # Fractional amplitudes below 1. The running maximum was an int, so every peak + # under 1 truncated to 0 and the reported percentage was exactly 0. + fractional = [0.1, 0.5, 0.25, 0.75, 0.3, 0.6, 0.2, 0.9] + assert railed(fractional) > 0.0 + assert np.isclose(railed(fractional), expected_percentage(0.9)) + + # Signals that cross zero or end at zero. These pin the existing zero-transition + # semantics rather than changing them: the second half of the straight-line test is + # abs(current) > 0.00001, so a step *into* zero is ignored while the step back *out* + # of zero is counted. + crossing = [-3.0, -1.0, 0.0, 1.0, 3.0, 1.0, 0.0, -1.0] + assert railed(crossing) != STRAIGHT_LINE + assert np.isclose(railed(crossing), expected_percentage(3.0)) + + # Flat and then a single step down to zero at the end. The step is suppressed because + # the current sample is zero, so this still reads as a straight line. + assert railed([5.0, 5.0, 5.0, 0.0]) == STRAIGHT_LINE + assert railed([-5.0, -5.0, -5.0, 0.0]) == STRAIGHT_LINE + assert railed([0.0, 0.0, 0.0, 0.0]) == STRAIGHT_LINE + + # A zero in the middle of an otherwise flat line does break it, because the step from + # zero back up to 5 has a non-zero current sample. + assert railed([5.0, 5.0, 0.0, 5.0, 5.0]) != STRAIGHT_LINE + + print('railed percentage regression passed') + + +if __name__ == '__main__': + main() From 11b808af796afcf5e93d232dba142ee1b46451bb Mon Sep 17 00:00:00 2001 From: Aditya Singh Date: Thu, 6 Aug 2026 20:37:27 -0700 Subject: [PATCH 3/3] Run the new regression test in unix CI --- .github/workflows/run_unix.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/run_unix.yml b/.github/workflows/run_unix.yml index 557001dae..d1ae364bd 100644 --- a/.github/workflows/run_unix.yml +++ b/.github/workflows/run_unix.yml @@ -354,6 +354,8 @@ jobs: run: sudo -H python3 $GITHUB_WORKSPACE/python_package/examples/tests/transforms.py - name: Downsampling Python run: sudo -H python3 $GITHUB_WORKSPACE/python_package/examples/tests/downsampling.py + - name: Railed Percentage Python + run: sudo -H python3 $GITHUB_WORKSPACE/python_package/examples/tests/railed_percentage.py - name: ICA Python run: sudo -H python3 $GITHUB_WORKSPACE/python_package/examples/tests/ica.py - name: CSP Python