From 0eb787852e729e968c8cc43b6600945f3c50a80e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 27 Nov 2025 16:28:14 +0000 Subject: [PATCH 01/19] Initial plan From 192ac458dc8814398c4fa509cd75f6bc5de457ec Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 27 Nov 2025 16:33:19 +0000 Subject: [PATCH 02/19] Initial plan From bd1afb3acc2473a653e48f003aa0851bc48eac64 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 27 Nov 2025 16:37:40 +0000 Subject: [PATCH 03/19] Add arithmetic.py with sum and mean processors Co-authored-by: cVogl97 <53898318+cVogl97@users.noreply.github.com> --- src/dspeed/processors/__init__.py | 2 + src/dspeed/processors/arithmetic.py | 128 ++++++++++++++++++++++++++++ tests/processors/test_arithmetic.py | 96 +++++++++++++++++++++ 3 files changed, 226 insertions(+) create mode 100644 src/dspeed/processors/arithmetic.py create mode 100644 tests/processors/test_arithmetic.py diff --git a/src/dspeed/processors/__init__.py b/src/dspeed/processors/__init__.py index 8ae1b2a..55194cc 100644 --- a/src/dspeed/processors/__init__.py +++ b/src/dspeed/processors/__init__.py @@ -64,6 +64,8 @@ # Mapping from function to name of module in which it is defined # To add a new function to processors, it must be added here! _modules = { + "mean": "arithmetic", + "sum": "arithmetic", "bl_subtract": "bl_subtract", "convolve_damped_oscillator": "convolutions", "convolve_exp": "convolutions", diff --git a/src/dspeed/processors/arithmetic.py b/src/dspeed/processors/arithmetic.py new file mode 100644 index 0000000..8a6d23d --- /dev/null +++ b/src/dspeed/processors/arithmetic.py @@ -0,0 +1,128 @@ +from __future__ import annotations + +import numpy as np +from numba import guvectorize + +from ..utils import numba_defaults_kwargs as nb_kwargs + + +@guvectorize( + [ + "void(float32[:], float32, float32, float32[:])", + "void(float64[:], float64, float64, float64[:])", + ], + "(n),(),()->()", + **nb_kwargs, +) +def sum(w_in: np.ndarray, a: float, b: float, result: float) -> None: + """Sum the waveform values from index a to b. + + Parameters + ---------- + w_in + the input waveform. + a + the starting index (inclusive). If NaN, defaults to 0. + b + the ending index (exclusive). If NaN, defaults to len(w_in). + result + the sum of w_in[a:b]. + + YAML Configuration Example + -------------------------- + + .. code-block:: yaml + + wf_sum: + function: sum + module: dspeed.processors + args: + - waveform + - "np.nan" + - "np.nan" + - wf_sum + unit: + - ADC + """ + result[0] = np.nan + + if np.isnan(w_in).any(): + return + + start = 0 if np.isnan(a) else int(a) + end = len(w_in) if np.isnan(b) else int(b) + + if start < 0: + start = 0 + if end > len(w_in): + end = len(w_in) + if start >= end: + result[0] = 0.0 + return + + total = 0.0 + for i in range(start, end): + total += w_in[i] + + result[0] = total + + +@guvectorize( + [ + "void(float32[:], float32, float32, float32[:])", + "void(float64[:], float64, float64, float64[:])", + ], + "(n),(),()->()", + **nb_kwargs, +) +def mean(w_in: np.ndarray, a: float, b: float, result: float) -> None: + """Calculate the mean of waveform values from index a to b. + + Parameters + ---------- + w_in + the input waveform. + a + the starting index (inclusive). If NaN, defaults to 0. + b + the ending index (exclusive). If NaN, defaults to len(w_in). + result + the mean of w_in[a:b], which is sum(w_in[a:b]) / (b - a). + + YAML Configuration Example + -------------------------- + + .. code-block:: yaml + + wf_mean: + function: mean + module: dspeed.processors + args: + - waveform + - "np.nan" + - "np.nan" + - wf_mean + unit: + - ADC + """ + result[0] = np.nan + + if np.isnan(w_in).any(): + return + + start = 0 if np.isnan(a) else int(a) + end = len(w_in) if np.isnan(b) else int(b) + + if start < 0: + start = 0 + if end > len(w_in): + end = len(w_in) + if start >= end: + result[0] = np.nan + return + + total = 0.0 + for i in range(start, end): + total += w_in[i] + + result[0] = total / (end - start) diff --git a/tests/processors/test_arithmetic.py b/tests/processors/test_arithmetic.py new file mode 100644 index 0000000..6aa1439 --- /dev/null +++ b/tests/processors/test_arithmetic.py @@ -0,0 +1,96 @@ +import numpy as np +import pytest + +from dspeed.processors import mean, sum + + +@pytest.mark.filterwarnings("ignore:invalid value encountered:RuntimeWarning") +def test_sum_basic(compare_numba_vs_python): + """Test basic sum functionality.""" + # Test sum of entire array + w_in = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) + result = compare_numba_vs_python(sum, w_in, np.nan, np.nan) + assert np.isclose(result, 15.0) + + +def test_sum_with_range(compare_numba_vs_python): + """Test sum with specified range.""" + w_in = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) + # Sum from index 1 to 4 (exclusive) = 2 + 3 + 4 = 9 + result = compare_numba_vs_python(sum, w_in, 1.0, 4.0) + assert np.isclose(result, 9.0) + + +@pytest.mark.filterwarnings("ignore:invalid value encountered:RuntimeWarning") +def test_sum_with_nan_input(compare_numba_vs_python): + """Test sum returns nan if input contains nan.""" + w_in = np.array([1.0, 2.0, np.nan, 4.0, 5.0]) + result = compare_numba_vs_python(sum, w_in, np.nan, np.nan) + assert np.isnan(result) + + +def test_sum_empty_range(compare_numba_vs_python): + """Test sum returns 0 when start >= end.""" + w_in = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) + result = compare_numba_vs_python(sum, w_in, 3.0, 2.0) + assert np.isclose(result, 0.0) + + +@pytest.mark.filterwarnings("ignore:invalid value encountered:RuntimeWarning") +def test_mean_basic(compare_numba_vs_python): + """Test basic mean functionality.""" + # Mean of entire array: 15/5 = 3 + w_in = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) + result = compare_numba_vs_python(mean, w_in, np.nan, np.nan) + assert np.isclose(result, 3.0) + + +def test_mean_with_range(compare_numba_vs_python): + """Test mean with specified range.""" + w_in = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) + # Mean from index 1 to 4 (exclusive) = (2 + 3 + 4) / 3 = 3 + result = compare_numba_vs_python(mean, w_in, 1.0, 4.0) + assert np.isclose(result, 3.0) + + +@pytest.mark.filterwarnings("ignore:invalid value encountered:RuntimeWarning") +def test_mean_with_nan_input(compare_numba_vs_python): + """Test mean returns nan if input contains nan.""" + w_in = np.array([1.0, 2.0, np.nan, 4.0, 5.0]) + result = compare_numba_vs_python(mean, w_in, np.nan, np.nan) + assert np.isnan(result) + + +def test_mean_empty_range(compare_numba_vs_python): + """Test mean returns nan when start >= end.""" + w_in = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) + result = compare_numba_vs_python(mean, w_in, 3.0, 2.0) + assert np.isnan(result) + + +def test_sum_boundary_conditions(compare_numba_vs_python): + """Test sum with boundary conditions.""" + w_in = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) + # Test with negative start (should be clamped to 0) + result = compare_numba_vs_python(sum, w_in, -1.0, 3.0) + # Sum of indices 0, 1, 2 = 1 + 2 + 3 = 6 + assert np.isclose(result, 6.0) + + # Test with end past array length (should be clamped) + result = compare_numba_vs_python(sum, w_in, 2.0, 10.0) + # Sum of indices 2, 3, 4 = 3 + 4 + 5 = 12 + assert np.isclose(result, 12.0) + + +def test_mean_boundary_conditions(compare_numba_vs_python): + """Test mean with boundary conditions.""" + w_in = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) + # Test with negative start (should be clamped to 0) + result = compare_numba_vs_python(mean, w_in, -1.0, 3.0) + # Mean of indices 0, 1, 2 = (1 + 2 + 3) / 3 = 2 + assert np.isclose(result, 2.0) + + # Test with end past array length (should be clamped) + result = compare_numba_vs_python(mean, w_in, 2.0, 10.0) + # Mean of indices 2, 3, 4 = (3 + 4 + 5) / 3 = 4 + assert np.isclose(result, 4.0) From 97c3559ce05cfbc3e2f68a8db81ff66469cbed14 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 27 Nov 2025 16:42:31 +0000 Subject: [PATCH 04/19] Add sort_wf processor that returns a sorted array using numpy.sort Co-authored-by: cVogl97 <53898318+cVogl97@users.noreply.github.com> --- src/dspeed/processors/__init__.py | 1 + src/dspeed/processors/sort_wf.py | 41 +++++++++++++++++++++++++++++++ tests/processors/test_sort_wf.py | 31 +++++++++++++++++++++++ 3 files changed, 73 insertions(+) create mode 100644 src/dspeed/processors/sort_wf.py create mode 100644 tests/processors/test_sort_wf.py diff --git a/src/dspeed/processors/__init__.py b/src/dspeed/processors/__init__.py index 8ae1b2a..a4b3538 100644 --- a/src/dspeed/processors/__init__.py +++ b/src/dspeed/processors/__init__.py @@ -131,6 +131,7 @@ "saturation": "saturation", "soft_pileup_corr": "soft_pileup_corr", "soft_pileup_corr_bl": "soft_pileup_corr", + "sort_wf": "sort_wf", "svm_predict": "svm", "tf_model": "tf_model", "time_over_threshold": "time_over_threshold", diff --git a/src/dspeed/processors/sort_wf.py b/src/dspeed/processors/sort_wf.py new file mode 100644 index 0000000..c40983e --- /dev/null +++ b/src/dspeed/processors/sort_wf.py @@ -0,0 +1,41 @@ +from __future__ import annotations + +import numpy as np +from numba import guvectorize + +from ..utils import numba_defaults_kwargs as nb_kwargs + + +@guvectorize( + ["void(float32[:], float32[:])", "void(float64[:], float64[:])"], + "(n)->(n)", + **nb_kwargs, +) +def sort_wf(w_in: np.ndarray, w_out: np.ndarray) -> None: + """Return a sorted array using :func:`numpy.sort`. + + Parameters + ---------- + w_in + the input waveform. + w_out + the output sorted waveform. + + YAML Configuration Example + -------------------------- + + .. code-block:: yaml + + wf_sorted: + function: sort_wf + module: dspeed.processors + args: + - waveform + - wf_sorted + """ + w_out[:] = np.nan + + if np.isnan(w_in).any(): + return + + w_out[:] = np.sort(w_in) diff --git a/tests/processors/test_sort_wf.py b/tests/processors/test_sort_wf.py new file mode 100644 index 0000000..f78fa73 --- /dev/null +++ b/tests/processors/test_sort_wf.py @@ -0,0 +1,31 @@ +import numpy as np + +from dspeed.processors import sort_wf + + +def test_sort_wf(compare_numba_vs_python): + """Testing function for the sort_wf processor.""" + + # test basic sorting functionality + w_in = np.array([5.0, 2.0, 8.0, 1.0, 9.0, 3.0]) + w_out_expected = np.array([1.0, 2.0, 3.0, 5.0, 8.0, 9.0]) + assert np.allclose(compare_numba_vs_python(sort_wf, w_in), w_out_expected) + + # test with negative values + w_in = np.array([3.0, -1.0, 2.0, -5.0, 0.0]) + w_out_expected = np.array([-5.0, -1.0, 0.0, 2.0, 3.0]) + assert np.allclose(compare_numba_vs_python(sort_wf, w_in), w_out_expected) + + # test with already sorted array + w_in = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) + w_out_expected = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) + assert np.allclose(compare_numba_vs_python(sort_wf, w_in), w_out_expected) + + # test with reverse sorted array + w_in = np.array([5.0, 4.0, 3.0, 2.0, 1.0]) + w_out_expected = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) + assert np.allclose(compare_numba_vs_python(sort_wf, w_in), w_out_expected) + + # test that nan in w_in produces all nans in output + w_in = np.array([1.0, 2.0, np.nan, 4.0, 5.0]) + assert np.all(np.isnan(compare_numba_vs_python(sort_wf, w_in))) From 48b7617977f0faabcc36ef37e34d13e6098070b1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 28 Nov 2025 07:51:14 +0000 Subject: [PATCH 05/19] Make sum and mean consistent (both return NaN for empty range) and use inclusive end index Co-authored-by: cVogl97 <53898318+cVogl97@users.noreply.github.com> --- src/dspeed/processors/arithmetic.py | 32 ++++++++++++++--------------- tests/processors/test_arithmetic.py | 22 ++++++++++---------- 2 files changed, 26 insertions(+), 28 deletions(-) diff --git a/src/dspeed/processors/arithmetic.py b/src/dspeed/processors/arithmetic.py index 8a6d23d..d987fb4 100644 --- a/src/dspeed/processors/arithmetic.py +++ b/src/dspeed/processors/arithmetic.py @@ -24,9 +24,9 @@ def sum(w_in: np.ndarray, a: float, b: float, result: float) -> None: a the starting index (inclusive). If NaN, defaults to 0. b - the ending index (exclusive). If NaN, defaults to len(w_in). + the ending index (inclusive). If NaN, defaults to len(w_in) - 1. result - the sum of w_in[a:b]. + the sum of w_in[a:b+1]. YAML Configuration Example -------------------------- @@ -50,18 +50,17 @@ def sum(w_in: np.ndarray, a: float, b: float, result: float) -> None: return start = 0 if np.isnan(a) else int(a) - end = len(w_in) if np.isnan(b) else int(b) + end = len(w_in) - 1 if np.isnan(b) else int(b) if start < 0: start = 0 - if end > len(w_in): - end = len(w_in) - if start >= end: - result[0] = 0.0 + if end > len(w_in) - 1: + end = len(w_in) - 1 + if start > end: return total = 0.0 - for i in range(start, end): + for i in range(start, end + 1): total += w_in[i] result[0] = total @@ -85,9 +84,9 @@ def mean(w_in: np.ndarray, a: float, b: float, result: float) -> None: a the starting index (inclusive). If NaN, defaults to 0. b - the ending index (exclusive). If NaN, defaults to len(w_in). + the ending index (inclusive). If NaN, defaults to len(w_in) - 1. result - the mean of w_in[a:b], which is sum(w_in[a:b]) / (b - a). + the mean of w_in[a:b+1], which is sum(w_in[a:b+1]) / (b - a + 1). YAML Configuration Example -------------------------- @@ -111,18 +110,17 @@ def mean(w_in: np.ndarray, a: float, b: float, result: float) -> None: return start = 0 if np.isnan(a) else int(a) - end = len(w_in) if np.isnan(b) else int(b) + end = len(w_in) - 1 if np.isnan(b) else int(b) if start < 0: start = 0 - if end > len(w_in): - end = len(w_in) - if start >= end: - result[0] = np.nan + if end > len(w_in) - 1: + end = len(w_in) - 1 + if start > end: return total = 0.0 - for i in range(start, end): + for i in range(start, end + 1): total += w_in[i] - result[0] = total / (end - start) + result[0] = total / (end - start + 1) diff --git a/tests/processors/test_arithmetic.py b/tests/processors/test_arithmetic.py index 6aa1439..0fe0826 100644 --- a/tests/processors/test_arithmetic.py +++ b/tests/processors/test_arithmetic.py @@ -16,8 +16,8 @@ def test_sum_basic(compare_numba_vs_python): def test_sum_with_range(compare_numba_vs_python): """Test sum with specified range.""" w_in = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) - # Sum from index 1 to 4 (exclusive) = 2 + 3 + 4 = 9 - result = compare_numba_vs_python(sum, w_in, 1.0, 4.0) + # Sum from index 1 to 3 (inclusive) = 2 + 3 + 4 = 9 + result = compare_numba_vs_python(sum, w_in, 1.0, 3.0) assert np.isclose(result, 9.0) @@ -30,10 +30,10 @@ def test_sum_with_nan_input(compare_numba_vs_python): def test_sum_empty_range(compare_numba_vs_python): - """Test sum returns 0 when start >= end.""" + """Test sum returns nan when start > end.""" w_in = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) result = compare_numba_vs_python(sum, w_in, 3.0, 2.0) - assert np.isclose(result, 0.0) + assert np.isnan(result) @pytest.mark.filterwarnings("ignore:invalid value encountered:RuntimeWarning") @@ -48,8 +48,8 @@ def test_mean_basic(compare_numba_vs_python): def test_mean_with_range(compare_numba_vs_python): """Test mean with specified range.""" w_in = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) - # Mean from index 1 to 4 (exclusive) = (2 + 3 + 4) / 3 = 3 - result = compare_numba_vs_python(mean, w_in, 1.0, 4.0) + # Mean from index 1 to 3 (inclusive) = (2 + 3 + 4) / 3 = 3 + result = compare_numba_vs_python(mean, w_in, 1.0, 3.0) assert np.isclose(result, 3.0) @@ -62,7 +62,7 @@ def test_mean_with_nan_input(compare_numba_vs_python): def test_mean_empty_range(compare_numba_vs_python): - """Test mean returns nan when start >= end.""" + """Test mean returns nan when start > end.""" w_in = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) result = compare_numba_vs_python(mean, w_in, 3.0, 2.0) assert np.isnan(result) @@ -72,11 +72,11 @@ def test_sum_boundary_conditions(compare_numba_vs_python): """Test sum with boundary conditions.""" w_in = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) # Test with negative start (should be clamped to 0) - result = compare_numba_vs_python(sum, w_in, -1.0, 3.0) + result = compare_numba_vs_python(sum, w_in, -1.0, 2.0) # Sum of indices 0, 1, 2 = 1 + 2 + 3 = 6 assert np.isclose(result, 6.0) - # Test with end past array length (should be clamped) + # Test with end past array length (should be clamped to len-1) result = compare_numba_vs_python(sum, w_in, 2.0, 10.0) # Sum of indices 2, 3, 4 = 3 + 4 + 5 = 12 assert np.isclose(result, 12.0) @@ -86,11 +86,11 @@ def test_mean_boundary_conditions(compare_numba_vs_python): """Test mean with boundary conditions.""" w_in = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) # Test with negative start (should be clamped to 0) - result = compare_numba_vs_python(mean, w_in, -1.0, 3.0) + result = compare_numba_vs_python(mean, w_in, -1.0, 2.0) # Mean of indices 0, 1, 2 = (1 + 2 + 3) / 3 = 2 assert np.isclose(result, 2.0) - # Test with end past array length (should be clamped) + # Test with end past array length (should be clamped to len-1) result = compare_numba_vs_python(mean, w_in, 2.0, 10.0) # Mean of indices 2, 3, 4 = (3 + 4 + 5) / 3 = 4 assert np.isclose(result, 4.0) From 9a0fb334735004a6204fd8c15226d05acf0db593 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 28 Nov 2025 07:56:36 +0000 Subject: [PATCH 06/19] Rename sort_wf to sort Co-authored-by: cVogl97 <53898318+cVogl97@users.noreply.github.com> --- src/dspeed/processors/__init__.py | 2 +- src/dspeed/processors/{sort_wf.py => sort.py} | 4 ++-- .../processors/{test_sort_wf.py => test_sort.py} | 16 ++++++++-------- 3 files changed, 11 insertions(+), 11 deletions(-) rename src/dspeed/processors/{sort_wf.py => sort.py} (89%) rename tests/processors/{test_sort_wf.py => test_sort.py} (57%) diff --git a/src/dspeed/processors/__init__.py b/src/dspeed/processors/__init__.py index a4b3538..588699d 100644 --- a/src/dspeed/processors/__init__.py +++ b/src/dspeed/processors/__init__.py @@ -131,7 +131,7 @@ "saturation": "saturation", "soft_pileup_corr": "soft_pileup_corr", "soft_pileup_corr_bl": "soft_pileup_corr", - "sort_wf": "sort_wf", + "sort": "sort", "svm_predict": "svm", "tf_model": "tf_model", "time_over_threshold": "time_over_threshold", diff --git a/src/dspeed/processors/sort_wf.py b/src/dspeed/processors/sort.py similarity index 89% rename from src/dspeed/processors/sort_wf.py rename to src/dspeed/processors/sort.py index c40983e..802694c 100644 --- a/src/dspeed/processors/sort_wf.py +++ b/src/dspeed/processors/sort.py @@ -11,7 +11,7 @@ "(n)->(n)", **nb_kwargs, ) -def sort_wf(w_in: np.ndarray, w_out: np.ndarray) -> None: +def sort(w_in: np.ndarray, w_out: np.ndarray) -> None: """Return a sorted array using :func:`numpy.sort`. Parameters @@ -27,7 +27,7 @@ def sort_wf(w_in: np.ndarray, w_out: np.ndarray) -> None: .. code-block:: yaml wf_sorted: - function: sort_wf + function: sort module: dspeed.processors args: - waveform diff --git a/tests/processors/test_sort_wf.py b/tests/processors/test_sort.py similarity index 57% rename from tests/processors/test_sort_wf.py rename to tests/processors/test_sort.py index f78fa73..dcecbb2 100644 --- a/tests/processors/test_sort_wf.py +++ b/tests/processors/test_sort.py @@ -1,31 +1,31 @@ import numpy as np -from dspeed.processors import sort_wf +from dspeed.processors import sort -def test_sort_wf(compare_numba_vs_python): - """Testing function for the sort_wf processor.""" +def test_sort(compare_numba_vs_python): + """Testing function for the sort processor.""" # test basic sorting functionality w_in = np.array([5.0, 2.0, 8.0, 1.0, 9.0, 3.0]) w_out_expected = np.array([1.0, 2.0, 3.0, 5.0, 8.0, 9.0]) - assert np.allclose(compare_numba_vs_python(sort_wf, w_in), w_out_expected) + assert np.allclose(compare_numba_vs_python(sort, w_in), w_out_expected) # test with negative values w_in = np.array([3.0, -1.0, 2.0, -5.0, 0.0]) w_out_expected = np.array([-5.0, -1.0, 0.0, 2.0, 3.0]) - assert np.allclose(compare_numba_vs_python(sort_wf, w_in), w_out_expected) + assert np.allclose(compare_numba_vs_python(sort, w_in), w_out_expected) # test with already sorted array w_in = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) w_out_expected = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) - assert np.allclose(compare_numba_vs_python(sort_wf, w_in), w_out_expected) + assert np.allclose(compare_numba_vs_python(sort, w_in), w_out_expected) # test with reverse sorted array w_in = np.array([5.0, 4.0, 3.0, 2.0, 1.0]) w_out_expected = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) - assert np.allclose(compare_numba_vs_python(sort_wf, w_in), w_out_expected) + assert np.allclose(compare_numba_vs_python(sort, w_in), w_out_expected) # test that nan in w_in produces all nans in output w_in = np.array([1.0, 2.0, np.nan, 4.0, 5.0]) - assert np.all(np.isnan(compare_numba_vs_python(sort_wf, w_in))) + assert np.all(np.isnan(compare_numba_vs_python(sort, w_in))) From 149dd964590e15b9b959410ecc2bf738169674a7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 28 Nov 2025 09:08:48 +0000 Subject: [PATCH 07/19] Fix mean to divide by b-a as per original requirements Co-authored-by: cVogl97 <53898318+cVogl97@users.noreply.github.com> --- src/dspeed/processors/arithmetic.py | 4 ++-- tests/processors/test_arithmetic.py | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/dspeed/processors/arithmetic.py b/src/dspeed/processors/arithmetic.py index d987fb4..ccde955 100644 --- a/src/dspeed/processors/arithmetic.py +++ b/src/dspeed/processors/arithmetic.py @@ -86,7 +86,7 @@ def mean(w_in: np.ndarray, a: float, b: float, result: float) -> None: b the ending index (inclusive). If NaN, defaults to len(w_in) - 1. result - the mean of w_in[a:b+1], which is sum(w_in[a:b+1]) / (b - a + 1). + the mean of w_in[a:b+1], which is sum(w_in[a:b+1]) / (b - a). YAML Configuration Example -------------------------- @@ -123,4 +123,4 @@ def mean(w_in: np.ndarray, a: float, b: float, result: float) -> None: for i in range(start, end + 1): total += w_in[i] - result[0] = total / (end - start + 1) + result[0] = total / (end - start) diff --git a/tests/processors/test_arithmetic.py b/tests/processors/test_arithmetic.py index 0fe0826..000643a 100644 --- a/tests/processors/test_arithmetic.py +++ b/tests/processors/test_arithmetic.py @@ -39,18 +39,18 @@ def test_sum_empty_range(compare_numba_vs_python): @pytest.mark.filterwarnings("ignore:invalid value encountered:RuntimeWarning") def test_mean_basic(compare_numba_vs_python): """Test basic mean functionality.""" - # Mean of entire array: 15/5 = 3 + # Mean of entire array: 15/(5-1) = 15/4 = 3.75 w_in = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) result = compare_numba_vs_python(mean, w_in, np.nan, np.nan) - assert np.isclose(result, 3.0) + assert np.isclose(result, 3.75) def test_mean_with_range(compare_numba_vs_python): """Test mean with specified range.""" w_in = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) - # Mean from index 1 to 3 (inclusive) = (2 + 3 + 4) / 3 = 3 + # Mean from index 1 to 3 (inclusive) = (2 + 3 + 4) / (3-1) = 9/2 = 4.5 result = compare_numba_vs_python(mean, w_in, 1.0, 3.0) - assert np.isclose(result, 3.0) + assert np.isclose(result, 4.5) @pytest.mark.filterwarnings("ignore:invalid value encountered:RuntimeWarning") @@ -87,10 +87,10 @@ def test_mean_boundary_conditions(compare_numba_vs_python): w_in = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) # Test with negative start (should be clamped to 0) result = compare_numba_vs_python(mean, w_in, -1.0, 2.0) - # Mean of indices 0, 1, 2 = (1 + 2 + 3) / 3 = 2 - assert np.isclose(result, 2.0) + # Mean of indices 0, 1, 2 = (1 + 2 + 3) / (2-0) = 6/2 = 3 + assert np.isclose(result, 3.0) # Test with end past array length (should be clamped to len-1) result = compare_numba_vs_python(mean, w_in, 2.0, 10.0) - # Mean of indices 2, 3, 4 = (3 + 4 + 5) / 3 = 4 - assert np.isclose(result, 4.0) + # Mean of indices 2, 3, 4 = (3 + 4 + 5) / (4-2) = 12/2 = 6 + assert np.isclose(result, 6.0) From b66d4794e357c9bbf686ff76f19c6846fd1faa19 Mon Sep 17 00:00:00 2001 From: cVogl Date: Fri, 28 Nov 2025 10:31:29 +0100 Subject: [PATCH 08/19] updated docstring of time_point_thresh and added time_point_thresh_nopol, which does not do a polarity check --- src/dspeed/processors/time_point_thresh.py | 78 +++++++++++++++++++++- 1 file changed, 77 insertions(+), 1 deletion(-) diff --git a/src/dspeed/processors/time_point_thresh.py b/src/dspeed/processors/time_point_thresh.py index c6067a3..2dd7336 100644 --- a/src/dspeed/processors/time_point_thresh.py +++ b/src/dspeed/processors/time_point_thresh.py @@ -19,7 +19,9 @@ def time_point_thresh( w_in: np.ndarray, a_threshold: float, t_start: int, walk_forward: int, t_out: float ) -> None: """Find the index where the waveform value crosses the threshold, walking - either forward or backward from the starting index. + either forward or backward from the starting index, including a polarity check. + This means that it will only find crossings where the waveform is rising + through the threshold when moving forward in time. Parameters ---------- @@ -79,6 +81,80 @@ def time_point_thresh( if w_in[i - 1] < a_threshold <= w_in[i]: t_out[0] = i return + +@guvectorize( + [ + "void(float32[:], float32, float32, float32, float32[:])", + "void(float64[:], float64, float64, float64, float64[:])", + ], + "(n),(),(),()->()", + **nb_kwargs, +) +def time_point_thresh_nopol( + w_in: np.ndarray, a_threshold: float, t_start: int, walk_forward: int, t_out: float +) -> None: + """Find the index where the waveform value crosses the threshold, walking + either forward or backward from the starting index, without polarity check. + This means that it will find the first crossing of the threshold in the specified direction, regardless of whether the waveform is rising or falling. + + Parameters + ---------- + w_in + the input waveform. + a_threshold + the threshold value. + t_start + the starting index. + walk_forward + the backward (``0``) or forward (``1``) search direction. + t_out + the index where the waveform value crosses the threshold. + + YAML Configuration Example + -------------------------- + + .. code-block:: yaml + + tp_0: + function: time_point_thresh + module: dspeed.processors + args: + - wf_atrap + - bl_std + - tp_start + - 0 + - tp_0 + unit: ns + """ + t_out[0] = np.nan + + if ( + np.isnan(w_in).any() + or np.isnan(a_threshold) + or np.isnan(t_start) + or np.isnan(walk_forward) + ): + return + + if np.floor(t_start) != t_start: + raise DSPFatal("The starting index must be an integer") + + if np.floor(walk_forward) != walk_forward: + raise DSPFatal("The search direction must be an integer") + + if int(t_start) < 0 or int(t_start) >= len(w_in): + raise DSPFatal("The starting index is out of range") + + if int(walk_forward) == 1: + for i in range(int(t_start), len(w_in) - 1, 1): + if w_in[i] <= a_threshold: + t_out[0] = i + return + else: + for i in range(int(t_start), 0, -1): + if w_in[i - 1] < a_threshold: + t_out[0] = i + return @guvectorize( From 2ac0d0f9fde9b9e7ae8c4cdb6b1990b22ef4d007 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 28 Nov 2025 09:38:44 +0000 Subject: [PATCH 09/19] Fix mean to divide by end-start+1 (number of elements) Co-authored-by: cVogl97 <53898318+cVogl97@users.noreply.github.com> --- src/dspeed/processors/arithmetic.py | 4 ++-- tests/processors/test_arithmetic.py | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/dspeed/processors/arithmetic.py b/src/dspeed/processors/arithmetic.py index ccde955..d987fb4 100644 --- a/src/dspeed/processors/arithmetic.py +++ b/src/dspeed/processors/arithmetic.py @@ -86,7 +86,7 @@ def mean(w_in: np.ndarray, a: float, b: float, result: float) -> None: b the ending index (inclusive). If NaN, defaults to len(w_in) - 1. result - the mean of w_in[a:b+1], which is sum(w_in[a:b+1]) / (b - a). + the mean of w_in[a:b+1], which is sum(w_in[a:b+1]) / (b - a + 1). YAML Configuration Example -------------------------- @@ -123,4 +123,4 @@ def mean(w_in: np.ndarray, a: float, b: float, result: float) -> None: for i in range(start, end + 1): total += w_in[i] - result[0] = total / (end - start) + result[0] = total / (end - start + 1) diff --git a/tests/processors/test_arithmetic.py b/tests/processors/test_arithmetic.py index 000643a..0fe0826 100644 --- a/tests/processors/test_arithmetic.py +++ b/tests/processors/test_arithmetic.py @@ -39,18 +39,18 @@ def test_sum_empty_range(compare_numba_vs_python): @pytest.mark.filterwarnings("ignore:invalid value encountered:RuntimeWarning") def test_mean_basic(compare_numba_vs_python): """Test basic mean functionality.""" - # Mean of entire array: 15/(5-1) = 15/4 = 3.75 + # Mean of entire array: 15/5 = 3 w_in = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) result = compare_numba_vs_python(mean, w_in, np.nan, np.nan) - assert np.isclose(result, 3.75) + assert np.isclose(result, 3.0) def test_mean_with_range(compare_numba_vs_python): """Test mean with specified range.""" w_in = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) - # Mean from index 1 to 3 (inclusive) = (2 + 3 + 4) / (3-1) = 9/2 = 4.5 + # Mean from index 1 to 3 (inclusive) = (2 + 3 + 4) / 3 = 3 result = compare_numba_vs_python(mean, w_in, 1.0, 3.0) - assert np.isclose(result, 4.5) + assert np.isclose(result, 3.0) @pytest.mark.filterwarnings("ignore:invalid value encountered:RuntimeWarning") @@ -87,10 +87,10 @@ def test_mean_boundary_conditions(compare_numba_vs_python): w_in = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) # Test with negative start (should be clamped to 0) result = compare_numba_vs_python(mean, w_in, -1.0, 2.0) - # Mean of indices 0, 1, 2 = (1 + 2 + 3) / (2-0) = 6/2 = 3 - assert np.isclose(result, 3.0) + # Mean of indices 0, 1, 2 = (1 + 2 + 3) / 3 = 2 + assert np.isclose(result, 2.0) # Test with end past array length (should be clamped to len-1) result = compare_numba_vs_python(mean, w_in, 2.0, 10.0) - # Mean of indices 2, 3, 4 = (3 + 4 + 5) / (4-2) = 12/2 = 6 - assert np.isclose(result, 6.0) + # Mean of indices 2, 3, 4 = (3 + 4 + 5) / 3 = 4 + assert np.isclose(result, 4.0) From 79dc5cf8d339ab69ebfeabf9459c5e03412a11a2 Mon Sep 17 00:00:00 2001 From: cVogl Date: Fri, 28 Nov 2025 13:50:45 +0100 Subject: [PATCH 10/19] add time_point_thresh_nopol to init --- src/dspeed/processors/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/dspeed/processors/__init__.py b/src/dspeed/processors/__init__.py index 24d4ccc..fa236a8 100644 --- a/src/dspeed/processors/__init__.py +++ b/src/dspeed/processors/__init__.py @@ -141,6 +141,7 @@ "interpolated_time_point_thresh": "time_point_thresh", "multi_time_point_thresh": "time_point_thresh", "time_point_thresh": "time_point_thresh", + "time_point_thresh_nopol": "time_point_thresh", "asym_trap_filter": "trap_filters", "trap_filter": "trap_filters", "trap_norm": "trap_filters", From 4885af7707e2784088686beea021b7fa1f54a6a6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 28 Nov 2025 13:06:01 +0000 Subject: [PATCH 11/19] Initial plan From 7544fa2c4983653fc82feb40d34a8d1348974d6f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 28 Nov 2025 13:12:34 +0000 Subject: [PATCH 12/19] Add tests for time_point_thresh_nopol and differentiation tests Co-authored-by: cVogl97 <53898318+cVogl97@users.noreply.github.com> --- tests/processors/test_time_point_thresh.py | 137 +++++++++++++++++++++ 1 file changed, 137 insertions(+) diff --git a/tests/processors/test_time_point_thresh.py b/tests/processors/test_time_point_thresh.py index 0c783f5..615952e 100644 --- a/tests/processors/test_time_point_thresh.py +++ b/tests/processors/test_time_point_thresh.py @@ -7,6 +7,7 @@ interpolated_time_point_thresh, rc_cr2, time_point_thresh, + time_point_thresh_nopol, ) @@ -85,6 +86,142 @@ def test_time_point_thresh(compare_numba_vs_python): w_in = np.concatenate([np.arange(-1, 5, 1), np.arange(-1, 5, 1)], dtype="float") assert compare_numba_vs_python(time_point_thresh, w_in, 3, 0, 1) == 4.0 + # -------- Differentiation tests for time_point_thresh -------- + # These tests use waveforms where time_point_thresh (with polarity check) + # produces different results than time_point_thresh_nopol (without polarity check). + + # Test differentiation: waveform falls through threshold without rising back + # [5, 4, 3, 2, 1, 0, -1] - threshold 2.5 walking forward from 0 + # time_point_thresh looks for w_in[i] <= threshold < w_in[i+1] (rising through) + # This never happens here since waveform is falling, so returns nan + w_falling = np.array([5.0, 4.0, 3.0, 2.0, 1.0, 0.0, -1.0]) + assert np.isnan(compare_numba_vs_python(time_point_thresh, w_falling, 2.5, 0, 1)) + + # Test differentiation: waveform that starts below threshold and rises + # [0, 1, 2, 3, 4, 5] - threshold 2.5 walking forward from 0 + # time_point_thresh finds the rising crossing at index 2 (w_in[2]=2 <= 2.5 < w_in[3]=3) + w_rising = np.array([0.0, 1.0, 2.0, 3.0, 4.0, 5.0]) + assert compare_numba_vs_python(time_point_thresh, w_rising, 2.5, 0, 1) == 2.0 + + +def test_time_point_thresh_nopol(compare_numba_vs_python): + """Testing function for the time_point_thresh_nopol processor.""" + + # test for nan if w_in has a nan + w_in = np.concatenate([np.arange(-1, 5, 1), np.arange(-1, 5, 1)], dtype="float") + w_in[4] = np.nan + assert np.isnan( + compare_numba_vs_python( + time_point_thresh_nopol, + w_in, + 1, + 11, + 0, + ) + ) + + # test for nan if nan is passed to a_threshold + w_in = np.concatenate([np.arange(-1, 5, 1), np.arange(-1, 5, 1)], dtype="float") + assert np.isnan( + compare_numba_vs_python( + time_point_thresh_nopol, + w_in, + np.nan, + 11, + 0, + ) + ) + + # test for nan if nan is passed to t_start + w_in = np.concatenate([np.arange(-1, 5, 1), np.arange(-1, 5, 1)], dtype="float") + assert np.isnan( + compare_numba_vs_python( + time_point_thresh_nopol, + w_in, + 1, + np.nan, + 0, + ) + ) + + # test for nan if nan is passed to walk_forward + w_in = np.concatenate([np.arange(-1, 5, 1), np.arange(-1, 5, 1)], dtype="float") + assert np.isnan( + compare_numba_vs_python( + time_point_thresh_nopol, + w_in, + 1, + 11, + np.nan, + ) + ) + + # test for error if t_start non integer + with pytest.raises(DSPFatal): + w_in = np.concatenate([np.arange(-1, 5, 1), np.arange(-1, 5, 1)], dtype="float") + time_point_thresh_nopol(w_in, 1, 10.5, 0, np.array([0.0])) + + # test for error if walk_forward non integer + with pytest.raises(DSPFatal): + w_in = np.concatenate([np.arange(-1, 5, 1), np.arange(-1, 5, 1)], dtype="float") + time_point_thresh_nopol(w_in, 1, 11, 0.5, np.array([0.0])) + + # test for error if t_start out of range + with pytest.raises(DSPFatal): + w_in = np.concatenate([np.arange(-1, 5, 1), np.arange(-1, 5, 1)], dtype="float") + time_point_thresh_nopol(w_in, 1, 12, 0, np.array([0.0])) + + # test walk backward - finds first point where w_in[i-1] < threshold + w_in = np.concatenate([np.arange(-1, 5, 1), np.arange(-1, 5, 1)], dtype="float") + # w_in = [-1, 0, 1, 2, 3, 4, -1, 0, 1, 2, 3, 4], threshold=1, start=11 + # Walking backward from index 11, find first i where w_in[i-1] < 1 + # i=11: w_in[10]=3 < 1? No + # i=10: w_in[9]=2 < 1? No + # i=9: w_in[8]=1 < 1? No + # i=8: w_in[7]=0 < 1? Yes -> return 8 + assert compare_numba_vs_python(time_point_thresh_nopol, w_in, 1, 11, 0) == 8.0 + + # test walk forward - finds first point where w_in[i] <= threshold + w_in = np.concatenate([np.arange(-1, 5, 1), np.arange(-1, 5, 1)], dtype="float") + # w_in = [-1, 0, 1, 2, 3, 4, -1, 0, 1, 2, 3, 4], threshold=3, start=0 + # Walking forward from index 0, find first i where w_in[i] <= 3 + # i=0: w_in[0]=-1 <= 3? Yes -> return 0 + assert compare_numba_vs_python(time_point_thresh_nopol, w_in, 3, 0, 1) == 0.0 + + # -------- Differentiation tests for time_point_thresh_nopol -------- + # These tests use waveforms where time_point_thresh_nopol (without polarity check) + # produces different results than time_point_thresh (with polarity check). + + # Test differentiation: waveform falls through threshold without rising back + # [5, 4, 3, 2, 1, 0, -1] - threshold 2.5 walking forward from 0 + # time_point_thresh_nopol looks for w_in[i] <= threshold (first at or below) + # i=0: w_in[0]=5 <= 2.5? No + # i=1: w_in[1]=4 <= 2.5? No + # i=2: w_in[2]=3 <= 2.5? No + # i=3: w_in[3]=2 <= 2.5? Yes -> return 3 + w_falling = np.array([5.0, 4.0, 3.0, 2.0, 1.0, 0.0, -1.0]) + assert compare_numba_vs_python(time_point_thresh_nopol, w_falling, 2.5, 0, 1) == 3.0 + + # Test differentiation: waveform that starts below threshold and rises + # [0, 1, 2, 3, 4, 5] - threshold 2.5 walking forward from 0 + # time_point_thresh_nopol finds first w_in[i] <= 2.5 + # i=0: w_in[0]=0 <= 2.5? Yes -> return 0 + w_rising = np.array([0.0, 1.0, 2.0, 3.0, 4.0, 5.0]) + assert compare_numba_vs_python(time_point_thresh_nopol, w_rising, 2.5, 0, 1) == 0.0 + + # Test walk forward with no crossing - all values above threshold + # time_point_thresh_nopol returns nan when no point is at or below threshold + w_above = np.array([5.0, 6.0, 7.0, 8.0, 9.0]) + assert np.isnan( + compare_numba_vs_python(time_point_thresh_nopol, w_above, 2.5, 0, 1) + ) + + # Test walk backward with no crossing - all values above threshold + w_above = np.array([5.0, 6.0, 7.0, 8.0, 9.0]) + assert np.isnan( + compare_numba_vs_python(time_point_thresh_nopol, w_above, 2.5, 4, 0) + ) + def test_interpolated_time_point_thresh(compare_numba_vs_python): """Testing function for the interpolated_time_point_thresh processor.""" From c248087dca71e3e1e6d21398008e4b36def4d603 Mon Sep 17 00:00:00 2001 From: cVogl Date: Mon, 1 Dec 2025 09:27:06 +0100 Subject: [PATCH 13/19] Clarified documentation --- src/dspeed/processors/time_point_thresh.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/dspeed/processors/time_point_thresh.py b/src/dspeed/processors/time_point_thresh.py index 2dd7336..3159aa1 100644 --- a/src/dspeed/processors/time_point_thresh.py +++ b/src/dspeed/processors/time_point_thresh.py @@ -18,10 +18,11 @@ def time_point_thresh( w_in: np.ndarray, a_threshold: float, t_start: int, walk_forward: int, t_out: float ) -> None: - """Find the index where the waveform value crosses the threshold, walking - either forward or backward from the starting index, including a polarity check. - This means that it will only find crossings where the waveform is rising - through the threshold when moving forward in time. + """Find the index where the waveform value crosses above the threshold, walking + either forward or backward from the starting index. Find only crossings where the + waveform is rising through the threshold when moving forward in time (polarity check). + Return the waveform index just before the threshold crossing (i.e. below the threshold + when searching forward and above the threshold when searching backward). Parameters ---------- @@ -93,9 +94,10 @@ def time_point_thresh( def time_point_thresh_nopol( w_in: np.ndarray, a_threshold: float, t_start: int, walk_forward: int, t_out: float ) -> None: - """Find the index where the waveform value crosses the threshold, walking + """Find the index where the waveform value crosses below a threshold, walking either forward or backward from the starting index, without polarity check. - This means that it will find the first crossing of the threshold in the specified direction, regardless of whether the waveform is rising or falling. + I.e., find the first crossing in the specified direction, regardless of whether the waveform is rising or falling. + Return the waveform index just above the threshold crossing. Parameters ---------- From 6c88f1583c445c9bf750656801715edc71f0eadc Mon Sep 17 00:00:00 2001 From: cVogl Date: Mon, 1 Dec 2025 10:50:11 +0100 Subject: [PATCH 14/19] changed reported index when walking forward in time_point_thresh_nopol --- src/dspeed/processors/time_point_thresh.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/dspeed/processors/time_point_thresh.py b/src/dspeed/processors/time_point_thresh.py index 3159aa1..e9ddb18 100644 --- a/src/dspeed/processors/time_point_thresh.py +++ b/src/dspeed/processors/time_point_thresh.py @@ -19,7 +19,7 @@ def time_point_thresh( w_in: np.ndarray, a_threshold: float, t_start: int, walk_forward: int, t_out: float ) -> None: """Find the index where the waveform value crosses above the threshold, walking - either forward or backward from the starting index. Find only crossings where the + either forward or backward from the starting index. Find only crossings where the waveform is rising through the threshold when moving forward in time (polarity check). Return the waveform index just before the threshold crossing (i.e. below the threshold when searching forward and above the threshold when searching backward). @@ -82,7 +82,8 @@ def time_point_thresh( if w_in[i - 1] < a_threshold <= w_in[i]: t_out[0] = i return - + + @guvectorize( [ "void(float32[:], float32, float32, float32, float32[:])", @@ -148,8 +149,8 @@ def time_point_thresh_nopol( raise DSPFatal("The starting index is out of range") if int(walk_forward) == 1: - for i in range(int(t_start), len(w_in) - 1, 1): - if w_in[i] <= a_threshold: + for i in range(int(t_start), len(w_in) - 2, 1): + if w_in[i + 1] <= a_threshold: t_out[0] = i return else: From 99e295bc1089910b5a5bf75d2419725dcbd531cf Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 1 Dec 2025 10:02:57 +0000 Subject: [PATCH 15/19] Adapt time_point_thresh_nopol tests for new behavior returning one index earlier Co-authored-by: cVogl97 <53898318+cVogl97@users.noreply.github.com> --- src/dspeed/processors/time_point_thresh.py | 5 +++-- tests/processors/test_time_point_thresh.py | 21 ++++++++++----------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/dspeed/processors/time_point_thresh.py b/src/dspeed/processors/time_point_thresh.py index 2dd7336..c4234ea 100644 --- a/src/dspeed/processors/time_point_thresh.py +++ b/src/dspeed/processors/time_point_thresh.py @@ -81,7 +81,8 @@ def time_point_thresh( if w_in[i - 1] < a_threshold <= w_in[i]: t_out[0] = i return - + + @guvectorize( [ "void(float32[:], float32, float32, float32, float32[:])", @@ -147,7 +148,7 @@ def time_point_thresh_nopol( if int(walk_forward) == 1: for i in range(int(t_start), len(w_in) - 1, 1): - if w_in[i] <= a_threshold: + if w_in[i + 1] <= a_threshold: t_out[0] = i return else: diff --git a/tests/processors/test_time_point_thresh.py b/tests/processors/test_time_point_thresh.py index 615952e..4cfa1f9 100644 --- a/tests/processors/test_time_point_thresh.py +++ b/tests/processors/test_time_point_thresh.py @@ -181,11 +181,11 @@ def test_time_point_thresh_nopol(compare_numba_vs_python): # i=8: w_in[7]=0 < 1? Yes -> return 8 assert compare_numba_vs_python(time_point_thresh_nopol, w_in, 1, 11, 0) == 8.0 - # test walk forward - finds first point where w_in[i] <= threshold + # test walk forward - finds the index before the first point where w_in[i+1] <= threshold w_in = np.concatenate([np.arange(-1, 5, 1), np.arange(-1, 5, 1)], dtype="float") # w_in = [-1, 0, 1, 2, 3, 4, -1, 0, 1, 2, 3, 4], threshold=3, start=0 - # Walking forward from index 0, find first i where w_in[i] <= 3 - # i=0: w_in[0]=-1 <= 3? Yes -> return 0 + # Walking forward from index 0, find first i where w_in[i+1] <= 3 + # i=0: w_in[1]=0 <= 3? Yes -> return 0 assert compare_numba_vs_python(time_point_thresh_nopol, w_in, 3, 0, 1) == 0.0 # -------- Differentiation tests for time_point_thresh_nopol -------- @@ -194,18 +194,17 @@ def test_time_point_thresh_nopol(compare_numba_vs_python): # Test differentiation: waveform falls through threshold without rising back # [5, 4, 3, 2, 1, 0, -1] - threshold 2.5 walking forward from 0 - # time_point_thresh_nopol looks for w_in[i] <= threshold (first at or below) - # i=0: w_in[0]=5 <= 2.5? No - # i=1: w_in[1]=4 <= 2.5? No - # i=2: w_in[2]=3 <= 2.5? No - # i=3: w_in[3]=2 <= 2.5? Yes -> return 3 + # time_point_thresh_nopol looks for w_in[i+1] <= threshold (index before first at or below) + # i=0: w_in[1]=4 <= 2.5? No + # i=1: w_in[2]=3 <= 2.5? No + # i=2: w_in[3]=2 <= 2.5? Yes -> return 2 w_falling = np.array([5.0, 4.0, 3.0, 2.0, 1.0, 0.0, -1.0]) - assert compare_numba_vs_python(time_point_thresh_nopol, w_falling, 2.5, 0, 1) == 3.0 + assert compare_numba_vs_python(time_point_thresh_nopol, w_falling, 2.5, 0, 1) == 2.0 # Test differentiation: waveform that starts below threshold and rises # [0, 1, 2, 3, 4, 5] - threshold 2.5 walking forward from 0 - # time_point_thresh_nopol finds first w_in[i] <= 2.5 - # i=0: w_in[0]=0 <= 2.5? Yes -> return 0 + # time_point_thresh_nopol finds first i where w_in[i+1] <= 2.5 + # i=0: w_in[1]=1 <= 2.5? Yes -> return 0 w_rising = np.array([0.0, 1.0, 2.0, 3.0, 4.0, 5.0]) assert compare_numba_vs_python(time_point_thresh_nopol, w_rising, 2.5, 0, 1) == 0.0 From 4dca79b51405ceda24aba5125aec1c0eae9e46f8 Mon Sep 17 00:00:00 2001 From: cVogl Date: Tue, 2 Dec 2025 11:35:06 +0100 Subject: [PATCH 16/19] simplified mean and sum processors such that they don't take start and end indices anymore --- src/dspeed/processors/arithmetic.py | 44 +++++++++++------------------ 1 file changed, 16 insertions(+), 28 deletions(-) diff --git a/src/dspeed/processors/arithmetic.py b/src/dspeed/processors/arithmetic.py index d987fb4..6b47364 100644 --- a/src/dspeed/processors/arithmetic.py +++ b/src/dspeed/processors/arithmetic.py @@ -8,25 +8,21 @@ @guvectorize( [ - "void(float32[:], float32, float32, float32[:])", - "void(float64[:], float64, float64, float64[:])", + "void(float32[:], float32[:])", + "void(float64[:], float64[:])", ], - "(n),(),()->()", + "(n)->()", **nb_kwargs, ) -def sum(w_in: np.ndarray, a: float, b: float, result: float) -> None: +def sum(w_in: np.ndarray, result: float) -> None: """Sum the waveform values from index a to b. Parameters ---------- w_in - the input waveform. - a - the starting index (inclusive). If NaN, defaults to 0. - b - the ending index (inclusive). If NaN, defaults to len(w_in) - 1. + the input waveform result - the sum of w_in[a:b+1]. + the sum of all values in w_in. YAML Configuration Example -------------------------- @@ -38,8 +34,6 @@ def sum(w_in: np.ndarray, a: float, b: float, result: float) -> None: module: dspeed.processors args: - waveform - - "np.nan" - - "np.nan" - wf_sum unit: - ADC @@ -49,8 +43,8 @@ def sum(w_in: np.ndarray, a: float, b: float, result: float) -> None: if np.isnan(w_in).any(): return - start = 0 if np.isnan(a) else int(a) - end = len(w_in) - 1 if np.isnan(b) else int(b) + start = 0 + end = len(w_in) - 1 if start < 0: start = 0 @@ -68,25 +62,21 @@ def sum(w_in: np.ndarray, a: float, b: float, result: float) -> None: @guvectorize( [ - "void(float32[:], float32, float32, float32[:])", - "void(float64[:], float64, float64, float64[:])", + "void(float32[:], float32[:])", + "void(float64[:], float64[:])", ], - "(n),(),()->()", + "(n)->()", **nb_kwargs, ) -def mean(w_in: np.ndarray, a: float, b: float, result: float) -> None: - """Calculate the mean of waveform values from index a to b. +def mean(w_in: np.ndarray, result: float) -> None: + """Calculate the mean of waveform values. Parameters ---------- w_in the input waveform. - a - the starting index (inclusive). If NaN, defaults to 0. - b - the ending index (inclusive). If NaN, defaults to len(w_in) - 1. result - the mean of w_in[a:b+1], which is sum(w_in[a:b+1]) / (b - a + 1). + the mean of all values in w_in. YAML Configuration Example -------------------------- @@ -98,8 +88,6 @@ def mean(w_in: np.ndarray, a: float, b: float, result: float) -> None: module: dspeed.processors args: - waveform - - "np.nan" - - "np.nan" - wf_mean unit: - ADC @@ -109,8 +97,8 @@ def mean(w_in: np.ndarray, a: float, b: float, result: float) -> None: if np.isnan(w_in).any(): return - start = 0 if np.isnan(a) else int(a) - end = len(w_in) - 1 if np.isnan(b) else int(b) + start = 0 + end = len(w_in) - 1 if start < 0: start = 0 From 3a5e551d99f489d5ca571c0e635d2ae7995f7fe6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Dec 2025 10:37:42 +0000 Subject: [PATCH 17/19] Initial plan From 03c65438d353e450fb7ba03196ef4b50bdf833bc Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Dec 2025 10:43:46 +0000 Subject: [PATCH 18/19] Update arithmetic tests and add mean_below_threshold processor Co-authored-by: cVogl97 <53898318+cVogl97@users.noreply.github.com> --- src/dspeed/processors/__init__.py | 1 + src/dspeed/processors/arithmetic.py | 58 ++++++++++++++ tests/processors/test_arithmetic.py | 119 ++++++++++++++++------------ 3 files changed, 129 insertions(+), 49 deletions(-) diff --git a/src/dspeed/processors/__init__.py b/src/dspeed/processors/__init__.py index fa236a8..3ec9f08 100644 --- a/src/dspeed/processors/__init__.py +++ b/src/dspeed/processors/__init__.py @@ -65,6 +65,7 @@ # To add a new function to processors, it must be added here! _modules = { "mean": "arithmetic", + "mean_below_threshold": "arithmetic", "sum": "arithmetic", "bl_subtract": "bl_subtract", "convolve_damped_oscillator": "convolutions", diff --git a/src/dspeed/processors/arithmetic.py b/src/dspeed/processors/arithmetic.py index 6b47364..ab16701 100644 --- a/src/dspeed/processors/arithmetic.py +++ b/src/dspeed/processors/arithmetic.py @@ -112,3 +112,61 @@ def mean(w_in: np.ndarray, result: float) -> None: total += w_in[i] result[0] = total / (end - start + 1) + + +@guvectorize( + [ + "void(float32[:], float32, float32[:])", + "void(float64[:], float64, float64[:])", + ], + "(n),()->()", + **nb_kwargs, +) +def mean_below_threshold( + w_in: np.ndarray, threshold: float, result: float +) -> None: + """Calculate the mean of waveform values that are below a threshold. + + Parameters + ---------- + w_in + the input waveform. + threshold + the threshold value. Only waveform values below this threshold + are included in the mean calculation. + result + the mean of all values in w_in that are below the threshold. + Returns NaN if no values are below the threshold. + + YAML Configuration Example + -------------------------- + + .. code-block:: yaml + + wf_mean_below_threshold: + function: mean_below_threshold + module: dspeed.processors + args: + - waveform + - 100 + - wf_mean_below_threshold + unit: + - ADC + """ + result[0] = np.nan + + if np.isnan(w_in).any() or np.isnan(threshold): + return + + total = 0.0 + count = 0 + + for i in range(len(w_in)): + if w_in[i] < threshold: + total += w_in[i] + count += 1 + + if count == 0: + return + + result[0] = total / count diff --git a/tests/processors/test_arithmetic.py b/tests/processors/test_arithmetic.py index 0fe0826..6806477 100644 --- a/tests/processors/test_arithmetic.py +++ b/tests/processors/test_arithmetic.py @@ -1,7 +1,7 @@ import numpy as np import pytest -from dspeed.processors import mean, sum +from dspeed.processors import mean, mean_below_threshold, sum @pytest.mark.filterwarnings("ignore:invalid value encountered:RuntimeWarning") @@ -9,31 +9,23 @@ def test_sum_basic(compare_numba_vs_python): """Test basic sum functionality.""" # Test sum of entire array w_in = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) - result = compare_numba_vs_python(sum, w_in, np.nan, np.nan) + result = compare_numba_vs_python(sum, w_in) assert np.isclose(result, 15.0) -def test_sum_with_range(compare_numba_vs_python): - """Test sum with specified range.""" - w_in = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) - # Sum from index 1 to 3 (inclusive) = 2 + 3 + 4 = 9 - result = compare_numba_vs_python(sum, w_in, 1.0, 3.0) - assert np.isclose(result, 9.0) - - @pytest.mark.filterwarnings("ignore:invalid value encountered:RuntimeWarning") def test_sum_with_nan_input(compare_numba_vs_python): """Test sum returns nan if input contains nan.""" w_in = np.array([1.0, 2.0, np.nan, 4.0, 5.0]) - result = compare_numba_vs_python(sum, w_in, np.nan, np.nan) + result = compare_numba_vs_python(sum, w_in) assert np.isnan(result) -def test_sum_empty_range(compare_numba_vs_python): - """Test sum returns nan when start > end.""" - w_in = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) - result = compare_numba_vs_python(sum, w_in, 3.0, 2.0) - assert np.isnan(result) +def test_sum_single_element(compare_numba_vs_python): + """Test sum with single element array.""" + w_in = np.array([42.0]) + result = compare_numba_vs_python(sum, w_in) + assert np.isclose(result, 42.0) @pytest.mark.filterwarnings("ignore:invalid value encountered:RuntimeWarning") @@ -41,15 +33,7 @@ def test_mean_basic(compare_numba_vs_python): """Test basic mean functionality.""" # Mean of entire array: 15/5 = 3 w_in = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) - result = compare_numba_vs_python(mean, w_in, np.nan, np.nan) - assert np.isclose(result, 3.0) - - -def test_mean_with_range(compare_numba_vs_python): - """Test mean with specified range.""" - w_in = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) - # Mean from index 1 to 3 (inclusive) = (2 + 3 + 4) / 3 = 3 - result = compare_numba_vs_python(mean, w_in, 1.0, 3.0) + result = compare_numba_vs_python(mean, w_in) assert np.isclose(result, 3.0) @@ -57,40 +41,77 @@ def test_mean_with_range(compare_numba_vs_python): def test_mean_with_nan_input(compare_numba_vs_python): """Test mean returns nan if input contains nan.""" w_in = np.array([1.0, 2.0, np.nan, 4.0, 5.0]) - result = compare_numba_vs_python(mean, w_in, np.nan, np.nan) + result = compare_numba_vs_python(mean, w_in) assert np.isnan(result) -def test_mean_empty_range(compare_numba_vs_python): - """Test mean returns nan when start > end.""" +def test_mean_single_element(compare_numba_vs_python): + """Test mean with single element array.""" + w_in = np.array([42.0]) + result = compare_numba_vs_python(mean, w_in) + assert np.isclose(result, 42.0) + + +@pytest.mark.filterwarnings("ignore:invalid value encountered:RuntimeWarning") +def test_mean_below_threshold_basic(compare_numba_vs_python): + """Test basic mean_below_threshold functionality.""" + # Values below 4.0: 1, 2, 3. Mean = (1 + 2 + 3) / 3 = 2 w_in = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) - result = compare_numba_vs_python(mean, w_in, 3.0, 2.0) + result = compare_numba_vs_python(mean_below_threshold, w_in, 4.0) + assert np.isclose(result, 2.0) + + +@pytest.mark.filterwarnings("ignore:invalid value encountered:RuntimeWarning") +def test_mean_below_threshold_all_above(compare_numba_vs_python): + """Test mean_below_threshold when all values are above threshold.""" + # All values >= 10.0, should return NaN + w_in = np.array([10.0, 20.0, 30.0, 40.0, 50.0]) + result = compare_numba_vs_python(mean_below_threshold, w_in, 10.0) assert np.isnan(result) -def test_sum_boundary_conditions(compare_numba_vs_python): - """Test sum with boundary conditions.""" +@pytest.mark.filterwarnings("ignore:invalid value encountered:RuntimeWarning") +def test_mean_below_threshold_all_below(compare_numba_vs_python): + """Test mean_below_threshold when all values are below threshold.""" + # All values < 100.0. Mean = 15 / 5 = 3 w_in = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) - # Test with negative start (should be clamped to 0) - result = compare_numba_vs_python(sum, w_in, -1.0, 2.0) - # Sum of indices 0, 1, 2 = 1 + 2 + 3 = 6 - assert np.isclose(result, 6.0) + result = compare_numba_vs_python(mean_below_threshold, w_in, 100.0) + assert np.isclose(result, 3.0) - # Test with end past array length (should be clamped to len-1) - result = compare_numba_vs_python(sum, w_in, 2.0, 10.0) - # Sum of indices 2, 3, 4 = 3 + 4 + 5 = 12 - assert np.isclose(result, 12.0) + +@pytest.mark.filterwarnings("ignore:invalid value encountered:RuntimeWarning") +def test_mean_below_threshold_with_nan_input(compare_numba_vs_python): + """Test mean_below_threshold returns nan if input contains nan.""" + w_in = np.array([1.0, 2.0, np.nan, 4.0, 5.0]) + result = compare_numba_vs_python(mean_below_threshold, w_in, 4.0) + assert np.isnan(result) -def test_mean_boundary_conditions(compare_numba_vs_python): - """Test mean with boundary conditions.""" +@pytest.mark.filterwarnings("ignore:invalid value encountered:RuntimeWarning") +def test_mean_below_threshold_with_nan_threshold(compare_numba_vs_python): + """Test mean_below_threshold returns nan if threshold is nan.""" w_in = np.array([1.0, 2.0, 3.0, 4.0, 5.0]) - # Test with negative start (should be clamped to 0) - result = compare_numba_vs_python(mean, w_in, -1.0, 2.0) - # Mean of indices 0, 1, 2 = (1 + 2 + 3) / 3 = 2 - assert np.isclose(result, 2.0) + result = compare_numba_vs_python(mean_below_threshold, w_in, np.nan) + assert np.isnan(result) + + +def test_mean_below_threshold_negative_values(compare_numba_vs_python): + """Test mean_below_threshold with negative values.""" + # Values below 0.0: -2, -1. Mean = (-2 + -1) / 2 = -1.5 + w_in = np.array([-2.0, -1.0, 0.0, 1.0, 2.0]) + result = compare_numba_vs_python(mean_below_threshold, w_in, 0.0) + assert np.isclose(result, -1.5) + - # Test with end past array length (should be clamped to len-1) - result = compare_numba_vs_python(mean, w_in, 2.0, 10.0) - # Mean of indices 2, 3, 4 = (3 + 4 + 5) / 3 = 4 - assert np.isclose(result, 4.0) +def test_mean_below_threshold_single_element_below(compare_numba_vs_python): + """Test mean_below_threshold with single element below threshold.""" + w_in = np.array([42.0]) + result = compare_numba_vs_python(mean_below_threshold, w_in, 50.0) + assert np.isclose(result, 42.0) + + +def test_mean_below_threshold_single_element_above(compare_numba_vs_python): + """Test mean_below_threshold with single element above threshold.""" + w_in = np.array([42.0]) + result = compare_numba_vs_python(mean_below_threshold, w_in, 30.0) + assert np.isnan(result) From 16e2f422119c6fd718b21a32f76437e407f68887 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 2 Dec 2025 10:48:53 +0000 Subject: [PATCH 19/19] Apply black formatting Co-authored-by: cVogl97 <53898318+cVogl97@users.noreply.github.com> --- src/dspeed/processors/arithmetic.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/dspeed/processors/arithmetic.py b/src/dspeed/processors/arithmetic.py index ab16701..5fcb683 100644 --- a/src/dspeed/processors/arithmetic.py +++ b/src/dspeed/processors/arithmetic.py @@ -122,9 +122,7 @@ def mean(w_in: np.ndarray, result: float) -> None: "(n),()->()", **nb_kwargs, ) -def mean_below_threshold( - w_in: np.ndarray, threshold: float, result: float -) -> None: +def mean_below_threshold(w_in: np.ndarray, threshold: float, result: float) -> None: """Calculate the mean of waveform values that are below a threshold. Parameters