Fix critical bugs: NumPy 1.24+ compatibility, division by zero, and alert detection#4
Conversation
… issues Co-authored-by: dfeen87 <158860247+dfeen87@users.noreply.github.com>
…ilent signals Co-authored-by: dfeen87 <158860247+dfeen87@users.noreply.github.com>
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
There was a problem hiding this comment.
Pull request overview
This PR fixes several edge-case/runtime issues in the signal feature extraction and alerting pipeline, including NumPy 1.24+ compatibility and guards against invalid numerical results.
Changes:
- Update
permutation_entropynormalization to usemath.factorial(NumPy 1.24+ compatibility) and add a “too short signal” guard. - Prevent division-by-zero/NaN in
compute_spectral_centroidwhen the PSD sum is zero. - Increase the epsilon used in
quality_check’s normalized std computation for improved numerical stability, and adjust alert-window indexing logic indetect_alert_events.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
core/features.py |
Adds guards for spectral centroid / permutation entropy edge cases and replaces deprecated NumPy factorial usage. |
core/preprocessing.py |
Adjusts epsilon in quality normalization to reduce numerical instability. |
core/gate.py |
Refactors alert end-index handling to make the inclusive end bound explicit. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # Check if signal is too short for the embedding | ||
| min_length = order + delay * (order - 1) | ||
| if n < min_length: | ||
| warnings.warn( | ||
| f"Signal too short (n={n}) for permutation entropy with order={order}, " | ||
| f"delay={delay}. Minimum length required: {min_length}. Returning 0.0." | ||
| ) | ||
| return 0.0 | ||
|
|
There was a problem hiding this comment.
The new “signal too short” early-return path (including warning emission) isn’t covered by tests. Consider adding a unit test that passes a too-short sig for a given order/delay, asserts the function returns 0.0, and uses pytest.warns to verify the warning is raised.
| # Check if signal is too short for the embedding | ||
| min_length = order + delay * (order - 1) |
There was a problem hiding this comment.
min_length is computed as order + delay * (order - 1), which is stricter than what the embedding loop actually requires (range(n - delay*(order-1))). This will return 0.0 (and emit a warning) for signal lengths where the function could still compute permutation entropy (e.g., n=delay*(order-1)+1). Consider changing the guard to match the real minimum required for at least one embedded vector (or clearly justify/parameterize a stricter minimum).
| # Check if signal is too short for the embedding | |
| min_length = order + delay * (order - 1) | |
| # Check if signal is too short for the embedding: | |
| # need at least one embedded vector, i.e. n - delay * (order - 1) >= 1 | |
| min_length = delay * (order - 1) + 1 |
| for onset_idx, offset_idx in zip(onset_indices, offset_indices): | ||
| if gate_series[offset_idx] == 0 and offset_idx > onset_idx: | ||
| offset_idx -= 1 | ||
| # Use separate variable to avoid mutation issues with zip iteration |
There was a problem hiding this comment.
The new comment about “mutation issues with zip iteration” is misleading in Python: reassigning offset_idx inside the loop would affect subsequent uses of offset_idx within that iteration. If the intent is to make the inclusive end bound explicit (or to avoid implying mutation of offset_indices), consider rewording the comment to describe that instead.
| # Use separate variable to avoid mutation issues with zip iteration | |
| # Use a separate variable for the (potentially adjusted) inclusive end index |
| freqs, psd = signal.welch(sig, fs=fs, nperseg=min(len(sig), 256)) | ||
|
|
||
| # Compute centroid as weighted mean | ||
| centroid = np.sum(freqs * psd) / np.sum(psd) | ||
| psd_sum = np.sum(psd) | ||
| if psd_sum > 0: | ||
| centroid = np.sum(freqs * psd) / psd_sum | ||
| else: | ||
| # If PSD is all zeros (silent/flatline signal), return 0.0 | ||
| # to indicate absence of spectral content | ||
| centroid = 0.0 |
There was a problem hiding this comment.
New behavior returns 0.0 when np.sum(psd) is zero. There’s currently no test covering a silent/zero signal (PSD all zeros) to ensure this path is exercised and avoids NaNs/ZeroDivision. Consider adding a unit test for compute_spectral_centroid(np.zeros(...), fs) expecting 0.0.
Code review identified 5 bugs causing runtime failures, incorrect behavior, and version incompatibility.
Changes
NumPy 1.24+ compatibility (
core/features.py)np.math.factorial()(removed in NumPy 1.24) withmath.factorial()Division by zero guards (
core/features.py)Numerical stability (
core/preprocessing.py)1e-10to1e-6in quality check normalized std calculationAlert detection fix (
core/gate.py)offset_idxreassignment in zip loop didn't propagateend_idxvariable to correctly track adjusted alert window boundsAll 101 tests passing. Zero security vulnerabilities (CodeQL).
✨ Let Copilot coding agent set things up for you — coding agent works faster and does higher quality work when set up for your repo.