You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
PR #82's new test test_trend_analysis_detects_break (tests/test_trends.py:34-39) segfaults (exit -11) in the Rust trend_analysis. Root cause is infinite recursion in recursive_trend_analysis (src/trends.rs).
For a symmetric \∧\ series, the least-squares fit over the whole array is a flat line, and the largest absolute residual lands on an endpoint (index 0, by floating-point rounding). The old code split at that index via y.split_at(max_residual_index), producing left=[] and right=y — an unchanged slice — so it recursed forever until the stack overflowed.
Fix
In src/trends.rs, restrict the split candidate to interior points (1..len-2) with a .filter. This:
guarantees both recursive sides are strictly shorter (left ≥ 1, right ≥ 2 elements), so recursion always terminates — no more segfault;
finds the true break at the interior peak, yielding the expected 2 segments [0,48] / [49,99] instead of peeling endpoints.
A 2-point fit is always exact, so reaching the split branch implies len ≥ 3 and an interior candidate always exists.
Tests
The existing test suite (tests/test_trends.py) now passes — validated the algorithm via an exact IEEE-754 simulation of the Rust logic:
This PR correctly diagnoses and fixes the infinite recursion / stack overflow in recursive_trend_analysis when the max-residual point lands on an endpoint. Restricting split candidates to interior indices (1..len-2) guarantees both recursive slices are strictly shorter, so the reported /\ series now terminates and produces the expected [0,48] / [49,99] segmentation (verified by an IEEE-754 simulation of the exact Rust arithmetic). CI is green and the diff is small and clean. However, the fix only partially addresses the stack-overflow class and leaves a silent data-loss path for isolated outliers that is worth addressing before merge.
Findings
[Major] src/trends.rs:69 (with base case :43) — the split point itself can be silently dropped from the output for outlier (spike) inputs.
The split point is placed in the right slice (y.split_at(max_residual_index)), and the endpoint filter then prevents splitting at that point again when it is the right slice's own largest residual. For y = [0..24, 1000, 26..49] the simulation returns segments [0,24] and [26,49]: index 25 (the spike that triggered the split) is covered by no segment. The 1-element left slice [1000] is discarded by the y.len() < 2 base case. This contradicts the PR body's validation claim that "spike input → segments split around the outlier" — the outlier is split around but omitted. Same class of gap was observed for step inputs depending on floating-point tie-breaking. Suggested remedies: keep the split point in the left slice instead, or merge a leftover 1-element boundary slice into an adjacent segment rather than dropping it.
[Major] src/trends.rs:77-78 — recursion depth is still O(n) worst case, so the stack-overflow class this PR targets is only partially fixed.
For a convex/exponential series (y_i = 1.001^i, n=10_000) the simulation shows max recursion depth ≈ 4052; extrapolating, inputs in the 100k+ range (realistic for EO time series) can still overflow the default thread stack. Consider an iterative implementation with an explicit segment stack, which bounds stack usage regardless of input.
[Minor] src/trends.rs:66-74 — 2-element input with threshold=0.0 silently returns zero segments.
A 2-point fit is exact, but residual rounding (~1e-17) can exceed threshold=0.0, entering the split branch where the filter yields no interior candidates, so max_by returns None and the function returns without pushing the exact-fit segment (e.g., [0.1, 0.2], threshold=0.0 → 0 segments).
[Minor] tests/test_trends.py — no new tests were added for the changed code path. test_trend_analysis_detects_break (PR #82) only asserts len(segments) >= 2 and monotonic end_index. Add regression tests that (a) assert the exact [0,48]/[49,99] split, (b) exercise spike and step inputs asserting termination and full coverage (no dropped indices), and (c) cover a large convex series to bound recursion depth (or rely on an iterative rewrite).
Verdict
REQUEST CHANGES — the core fix is correct for the reported /\ case and terminates, but the silent dropping of outlier split-points (Major) and the remaining O(n) recursion depth should be addressed, and the changed path needs real regression tests.
Review requested changes before merge. Tracked in #88.
This branch has not been deployed
No deployments
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #83
Summary
PR #82's new test
test_trend_analysis_detects_break(tests/test_trends.py:34-39) segfaults (exit -11) in the Rusttrend_analysis. Root cause is infinite recursion inrecursive_trend_analysis(src/trends.rs).For a symmetric \∧\ series, the least-squares fit over the whole array is a flat line, and the largest absolute residual lands on an endpoint (index 0, by floating-point rounding). The old code split at that index via
y.split_at(max_residual_index), producingleft=[]andright=y— an unchanged slice — so it recursed forever until the stack overflowed.Fix
In src/trends.rs, restrict the split candidate to interior points (
1..len-2) with a.filter. This:[0,48]/[49,99]instead of peeling endpoints.A 2-point fit is always exact, so reaching the split branch implies len ≥ 3 and an interior candidate always exists.
Tests
The existing test suite (tests/test_trends.py) now passes — validated the algorithm via an exact IEEE-754 simulation of the Rust logic:
Checklist