A call to find_peaks(x, distance=5, prominence=2) returns only peak index 12 for the signal examined here. Yet the same original signal contains another peak, at index 1, whose prominence is 5 and which is eleven samples away from index 12. If the application requirement is “first determine which peaks have prominence at least 2, then enforce five-sample spacing among those eligible peaks,” the expected result is therefore [1, 12].
The apparent contradiction is not a SciPy defect. It is a selection-policy mismatch.
SciPy 1.17.0 explicitly documents its condition order as plateau_size, height, threshold, distance, prominence, width. It also states that the distance rule removes smaller peaks first until the required separation is achieved. The versioned find_peaks reference therefore describes a distance-first contract, not an eligibility-first one.
This playbook audits that distinction using the supplied October 23, 2026 preparation fixture: CPython 3.13.5, NumPy 2.3.5, SciPy 1.17.0, Linux, CPU float64, one finite one-dimensional array, wlen=None, and integer sample spacing. No new execution is claimed here. The objective is narrower and more operational than a peak-detection tutorial: declare the selection policy, reconcile every candidate identity, and decide whether to accept SciPy's ordering, adopt a separately owned policy, repair an incorrect assumption, rerun qualification, or hold the release.
Declare which peak-selection policy the application owns
Before looking at a returned array, decide which operation the application actually means.
There are two legitimate policies in this review.
SciPy policy: identify local maxima, apply the configured conditions in SciPy's documented sequence, and therefore apply distance before prominence. For distance, SciPy's documentation says smaller peaks are removed until the remaining peak indices satisfy the required minimum separation. Prominence is evaluated later.
Application policy: identify all local maxima in the original finite signal, compute their global prominences with wlen=None, discard candidates below the prominence floor, and only then apply a greedy spacing rule to the eligible candidates. The spacing priority is descending raw height; equal heights are resolved by lower original index. That tie rule is application-owned. It is not asserted to be SciPy's undocumented behavior.
These policies answer different questions. Neither should be inferred from a parameter list.
For the authored signal, the required identities make the difference concrete:
Policy | Required result |
SciPy distance-first, then prominence | [12] |
Application prominence-eligible first, then spacing | [1, 12] |
The second policy is not a globally optimal peak-subset solver. It does not maximize peak count, total prominence, total height, or another objective over all feasible subsets. It is simply an explicitly declared greedy policy: establish prominence eligibility first, then consider eligible candidates by descending height.
That distinction is the core engineering control. A library function can behave exactly according to contract while a caller still implements the wrong application rule. The same discipline appears in non-mutating SciPy inspection: define the quantity and preservation policy before treating a plausible output as evidence of correctness.
For the requirement posed here (retain every prominence-eligible candidate that can survive the approved spacing procedure), a caller that assumes distance and prominence commute receives a REPAIR decision. SciPy itself does not.
Freeze the signal, sample spacing and runtime
A useful counterexample is small enough that every index can be inspected. The preparation fixture freezes this float64 signal:
index: 0 1 2 3 4 5 6 7 8 9 10 11 12 13
value: 0 5 0 8 9 10 9 10 11 12 13 14 15 0The supplied preparation run records CPython 3.13.5, NumPy 2.3.5, SciPy 1.17.0, Linux, and ordinary CPU float64 arrays. Those runtime labels matter because the release evidence is version-qualified. The fact that SciPy 1.17.0's documentation currently describes the relevant filter order does not authorize silently carrying an old verification result into a different package version.
The application helper also imposes stricter input admission than the generic SciPy API: its input must be one-dimensional and entirely finite; distance must be a positive integer and not a Boolean; and the prominence floor must be finite and nonnegative. Those checks are application policy, not a claim that SciPy itself requires exactly the same domain.
SciPy warns that find_peaks may produce unexpected results for data containing NaNs. Its prominence documentation gives a similar warning. The lab therefore does not “repair” a NaN by deleting it, interpolating it, replacing it, or compressing the coordinate system. It rejects the input.
That is important because deleting a sample is not merely a value edit when distance is defined in sample positions. It can change candidate indices and pairwise separations.
The scope is also intentionally narrower than SciPy's full API. There is no smoothing design, statistical significance test, irregular-time handling, streaming boundary state, GPU qualification, or plateau-edge-spacing policy here. Width is not an alternative proxy for the prominence requirement: SciPy documents peak_widths as a distinct peak property measured in samples at a relative evaluation height based on peak height and prominence. The SciPy 1.17.0 width reference keeps that concept separate.
Keep sample positions separate from elapsed time
distance=5 means five sample positions in this fixture. SciPy documents the parameter as a minimum horizontal distance in samples between neighboring peaks.
With a qualified uniform sampling interval Δt an application can separately map an index difference to elapsed time. But find_peaks does not receive timestamps in this call, and the present policy does not infer a sampling rate from the values.
That gives a simple admission rule: if the owner cannot state what one index step represents, do not translate distance=5 into milliseconds, meters, rotations, or another physical unit. The release decision is HOLD until sample geometry is known.
Likewise, irregular timestamps are outside this fixture. Five index positions in an irregular series do not imply one fixed elapsed-time separation. Converting that problem into the present one requires a separately qualified transformation or selection algorithm, not an undocumented assumption.
Build the candidate ledger before applying exclusions
The diagnostic starts with candidate identity, not the final count.
For ordinary non-flat local maxima in this fixture, a candidate must exceed its immediate neighbors. That gives three maxima independently by inspection:
Candidate index | Left value | Peak height | Right value | Local maximum? |
1 | 0 | 5 | 0 | Yes |
5 | 9 | 10 | 9 | Yes |
12 | 14 | 15 | 0 | Yes |
The candidate set is therefore [1, 5, 12].
Now compute prominence conceptually on the original signal. SciPy defines prominence as the vertical distance between a peak and its lowest contour line. Its documented procedure extends horizontally toward higher terrain, locates the minimum on each side within the applicable interval, and uses the higher of those two bases as the contour level. With wlen=None, this fixture does not intentionally restrict that search to a local window. The SciPy 1.17.0 peak_prominences reference describes those semantics and notes that setting wlen can intentionally produce a more local prominence by limiting the evaluated neighborhood.
At index 1, the peak height is 5. Its applicable contour reaches zero, so prominence is 5.
At index 5, the height is 10. On its right, the signal falls only to 9 before rising through the peak's level toward higher values. The controlling contour is therefore 9, making prominence only 1.
At index 12, the height is 15, the largest candidate height in the fixture. Its contour reaches zero, giving prominence 15.
The complete pre-filter ledger is consequently:
Candidate | Height | Global prominence | Meets prominence >= 2? |
1 | 5 | 5 | Yes |
5 | 10 | 1 | No |
12 | 15 | 15 | Yes |
The crucial asymmetry is already visible: candidate 5 ranks above candidate 1 by height, but below it by prominence. Any algorithm that uses one property for an early elimination and the other property later can therefore produce a different identity set from an algorithm that reverses those stages.
Use an oracle independent of the final call
Do not make this regression test circular by defining the expected result as whatever the combined find_peaks call returns.
The independent oracle consists of candidate identities, their authored signal heights, the analytically checked prominence values, the explicit prominence threshold, the spacing geometry, and the declared priority rule. The combined call is then an observation to reconcile against that oracle.
The complete preparation fixture is:
python -m pip install "numpy==2.3.5" "scipy==1.17.0"
python peaks_lab.py > peaks-evidence.jsonThe supplied preparation evidence reports the installed versions shown above; it does not establish that this package-install command itself was executed during preparation. The command is the reproduction recipe.
Save the following as peaks_lab.py:
import hashlib
import json
import platform
from pathlib import Path
import numpy as np
import scipy
from scipy.signal import find_peaks, peak_prominences
if not debug:
raise RuntimeError('Run without -O; diagnostic assertions are required.')
def eligible_then_space(values, min_prominence=2.0, distance=5):
"""Application policy: prominence eligibility, then height-first spacing."""
x = np.asarray(values, dtype=np.float64)
if x.ndim != 1 or not np.isfinite(x).all():
raise ValueError('Require a finite one-dimensional signal.')
if isinstance(distance, bool) or not isinstance(distance, int) or distance < 1:
raise ValueError('Require a positive integer distance in samples.')
if not np.isfinite(min_prominence) or min_prominence < 0:
raise ValueError('Require a finite nonnegative prominence floor.')
candidates, = findpeaks(x)
prominence = peak_prominences(x, candidates)[0]
eligible = candidates[prominence >= min_prominence].tolist()
selected = []
# This explicit tie policy belongs to this application, not to SciPy.
for index in sorted(eligible, key=lambda i: (-float(x[i]), i)):
if all(abs(index - kept) >= distance for kept in selected):
selected.append(index)
return sorted(selected)
def main():
x = np.array([0, 5, 0, 8, 9, 10, 9, 10, 11, 12, 13, 14, 15, 0], dtype=float)
frozen = x.copy()
rows = []
def check(case, observed, expected):
assert observed == expected, (case, observed, expected)
rows.append({'case': case, 'observed': observed, 'expected': expected})
p, = findpeaks(x)
check('P1 candidates and prominence',
[p.tolist(), peak_prominences(x, p)[0].tolist()],
[[1, 5, 12], [5.0, 1.0, 15.0]])
check('P2 distance first', find_peaks(x, distance=5)[0].tolist(), [5, 12])
check('P3 combined filters',
find_peaks(x, distance=5, prominence=2)[0].tolist(), [12])
check('P4 prominence only', find_peaks(x, prominence=2)[0].tolist(), [1, 12])
check('P5 declared application policy', eligible_then_space(x), [1, 12])
check('P6 exact distance boundary',
find_peaks(x, distance=4, prominence=2)[0].tolist(), [1, 12])
check('P7 explicit application tie rule',
eligible_then_space([0, 5, 0, 5, 0], 1, 3), [1])
check('P8 no local maxima', eligible_then_space([0, 1, 2, 3]), [])
for case, values, distance in [
('P9 nonfinite input rejected', [0, float('nan'), 0], 5),
('P10 zero distance rejected', x, 0),
]:
try:
eligible_then_space(values, distance=distance)
except ValueError:
check(case, 'ValueError', 'ValueError')
else:
raise AssertionError(case)
assert np.array_equal(x, frozen), 'The source signal changed.'
print(json.dumps({'python': platform.python_version(), 'numpy': np.__version__,
'scipy': scipy.__version__,
'source_sha256': hashlib.sha256(Path(__file__).read_bytes()).hexdigest(),
'cases': rows, 'status': 'PASS_10_CASES'}, indent=2))
if name == '__main__':
main()The supplied source SHA-256 is:
1078183fc741acc77059f927e06e3e65cf195f7d8cb861ecb1704aa00c3baef2That digest identifies the prepared fixture file. It is not an authenticated execution record and does not prove that the algorithm is correct. Similarly, np.array_equal(x, frozen) is the fixture's value-preservation check; it should not be generalized into a claim that an arbitrary array's storage identity, allocation, or every byte-level property has been audited.
Run distance alone and identify the discarded candidate
Now apply only the spacing condition:
find_peaks(x, distance=5)[0]The supplied preparation result is:
[5, 12]Why did index 1 disappear?
The three starting candidates are 1, 5, and 12. Candidate 1 and candidate 5 are four positions apart:
abs(5 - 1) = 4That violates distance=5. Candidate 5 has raw height 10; candidate 1 has raw height 5. SciPy documents that smaller peaks are removed first for the distance condition. The surviving pair 5 and 12 is seven positions apart, so it satisfies the requested separation.
The tagged SciPy 1.17.0 implementation corroborates the stage ordering. In find_peaks, the code first obtains local maxima, evaluates earlier configured conditions, then enters the distance block and calls the peak-distance selector with x[peaks] as its priority data. Only afterward does it calculate prominence when prominence or width is requested. The tagged SciPy v1.17.0 peakfinding.py shows those operations in that order.
That source reading is corroboration, not an excuse to broaden the application contract to undocumented implementation details. In particular, nothing in this playbook promises to reproduce SciPy's behavior when two conflicting peaks have exactly equal height. The fixture does not need that behavior to establish its main counterexample.
At this stage the ledger is:
start: [1, 5, 12]
distance=5: [ 5, 12]The key fact is not merely that one peak was removed. It is which peak was removed. Index 1 is already absent before the prominence condition gets a chance to establish that it is eligible.
Add prominence and observe the missing reconsideration
Now make the call that triggered the diagnosis:
find_peaks(x, distance=5, prominence=2)[0]The supplied preparation result is:
[12]A caller who reads the keyword arguments as a set of commuting predicates may reason incorrectly:
Peak 1 has prominence 5; peak 12 has prominence 15; they are eleven samples apart; therefore both satisfy the two requirements.
That reasoning describes prominence eligibility followed by spacing. It does not describe SciPy's documented condition order.
The actual stage ledger is:
local maxima: [1, 5, 12]
after distance=5: [5, 12]
after prominence >= 2: [12]Index 5 defeats index 1 during spacing because 10 > 5 in raw height. Then index 5 fails the later prominence threshold because its prominence is only 1. Index 1 is not put back into the candidate set.
That is the entire noncommutativity in one trace.
The distinction resembles another common numerical-software failure mode: two individually understandable rules can yield different results when their direction or ordering changes. The useful engineering habit is to make the policy explicit, as with explicit numerical reference policies, rather than inferring intent from a function that successfully returned a value.
This is also why a regression assertion that expects [12] can pass and still fail the application review. It proves that the observed SciPy call matches its recorded expectation; it does not prove that distance-first was the intended selection policy.
An eliminated candidate is not reconsidered later
It is useful to model the call as destructive filtering of a candidate set:
C0 = {1, 5, 12}
C1 = distance_filter(C0) = {5, 12}
C2 = prominence_filter(C1) = {12}The application policy instead evaluates:
C0 = {1, 5, 12}
E = prominence_filter(C0) = {1, 12}
A = spacing_filter(E) = {1, 12}There is no algebraic reason to assume those operations commute. In this signal, they demonstrably do not.
The difference is particularly easy to miss because candidate 5 acts as a temporary blocker. It matters during the first ordering even though it does not survive the final result. A final-output-only log erases precisely the evidence needed to explain why index 1 vanished.
For maintainable diagnostics, log identities at each exclusion stage, not only len(peaks) and not only the final array.
Separate raw height from contour-based prominence
The counterexample works because height and prominence are different properties.
Raw height is immediate:
x[1] = 5
x[5] = 10
x[12] = 15By height, the ranking is:
12 > 5 > 1Prominence asks a different question: how far does the peak stand above its controlling surrounding contour? SciPy defines it using the peak height and the higher of the two base levels that establish the lowest contour line.
For this fixture:
candidate 1: height 5, contour 0 -> prominence 5
candidate 5: height 10, contour 9 -> prominence 1
candidate 12: height 15, contour 0 -> prominence 15So the prominence ranking is:
12 > 1 > 5Index 5 is the intentionally troublesome candidate. It is taller than index 1, which gives it priority in the distance conflict, but it is only one unit above its relevant surrounding contour, so it fails the prominence floor.
That is why substituting “tall peak” for “prominent peak” would destroy the diagnostic. The terms are not stylistic variants.
The value of wlen is equally important. The preparation policy leaves it at None. SciPy documents that restricting wlen limits the area considered in prominence calculation and can result in a higher contour line and therefore a smaller calculated prominence when the true bases fall outside the window. A future decision to introduce wlen is consequently a signal-domain and policy change that requires RERUN, not a harmless performance tweak.
Nor should width be substituted for prominence because another property happens to correlate with the desired events. SciPy's width calculation explicitly uses an evaluation height derived from peak height, prominence, and rel_height; it is a separate measured characteristic with separate units and semantics.
The operational lesson is narrow: when one filter prioritizes candidates using height and another determines eligibility using prominence, inspect both columns in the candidate ledger. A count such as “one peak remained” cannot reveal which ranking caused the exclusion.
Implement eligibility before spacing as a named policy
When the approved requirement is prominence-first, do not try to persuade a combined SciPy call to mean something its documented ordering does not mean. Implement the application operation explicitly.
The helper in the fixture does four things in sequence.
First, it converts the source to a CPU float64 NumPy array and admits only one-dimensional finite data.
Second, it asks find_peaks(x) only for the local-maximum candidates, without distance or prominence filtering.
Third, it calls peak_prominences(x, candidates) on that original candidate set and retains only candidates whose prominence is at least the configured floor.
Fourth, it applies the application spacing policy over the eligible set:
for index in sorted(eligible, key=lambda i: (-float(x[i]), i)):
if all(abs(index - kept) >= distance for kept in selected):
selected.append(index)For the main signal, prominence eligibility produces:
eligible = [1, 12]Their separation is:
abs(12 - 1) = 11Both therefore survive distance=5, producing:
[1, 12]Notice that the helper still uses height priority during spacing. It is not prominence-priority spacing. Prominence is an eligibility gate; height orders the already eligible candidates. That distinction should appear in review material because an innocent-looking refactor such as sorting by prominence would create a third policy.
The output is sorted by original index only after selection so downstream consumers receive stable sample order. Selection priority and output presentation order are therefore separate concepts.
The helper also deliberately rejects a Boolean distance. In Python, bool is a subclass of int, so a check that merely accepts isinstance(distance, int) would allow True as a distance. The application contract says positive integer sample count, not truth value, so the stricter admission check is appropriate.
Again, these are application controls. They should not be described as hidden requirements of SciPy.
Specify the tie rule rather than guessing it
Equal-height conflicts deserve their own test because they are where maintainers are most tempted to reverse-engineer incidental library behavior.
The application fixture uses:
eligible_then_space([0, 5, 0, 5, 0], 1, 3)The two maxima occur at indices 1 and 3, both with height 5. Their distance is two samples, so they cannot both survive distance=3.
The application sort key is:
(-float(x[i]), i)Equal height therefore falls through to ascending index, and index 1 wins. P7 expects:
[1]This test establishes exactly one thing: the custom application's tie rule.
It does not establish a SciPy promise. The public find_peaks documentation states the general smaller-peak distance behavior and the condition order, but this article does not elevate any observed equal-height resolution mechanism into a supported API contract.
That boundary protects the regression suite from accidental dependence on an implementation detail that the application never needed to own.
Exercise exact-distance and equal-height boundaries
Minimum spacing has a boundary, and the fixture tests it.
P6 changes the distance from five to four:
find_peaks(x, distance=4, prominence=2)[0]The expected result is:
[1, 12]Why? Candidates 1 and 5 are exactly four positions apart. SciPy documents retained peak indices as being at least distance samples apart. Equality therefore satisfies the condition.
With distance=4, the distance stage need not remove index 1 merely because index 5 exists. Prominence can then reject index 5, leaving the eligible indices 1 and 12.
This is a useful boundary test because an incorrect application reimplementation might accidentally use:
abs(index - kept) > distancerather than:
abs(index - kept) >= distanceThose expressions disagree exactly at the threshold.
P7 covers a different boundary: equal height under the application-owned greedy policy. Its purpose is not to learn SciPy's undocumented tie semantics; it is to prevent the custom function's tie rule from drifting silently.
These are deterministic algorithmic boundaries, not tests of statistical significance. General statistical reasoning foundations remain useful when an analysis makes inferential claims, but this fixture asks a prior software question: given an already declared rule, which candidate identities does the implementation retain?
P8 supplies another negative control:
eligible_then_space([0, 1, 2, 3])The monotonically increasing finite sequence contains no interior local maximum under the peak definition relevant here, so the expected selection is empty. SciPy describes its basic peak detection in terms of comparison with direct neighbors, with separate handling for flat peaks.
Together, P6 through P8 protect three different assumptions: inclusion at exact spacing, explicit application tie ownership, and correct handling of a signal with no candidates.
Reject inputs whose geometry is not qualified
A reliable selector must know when not to manufacture an answer.
P9 passes:
[0, float('nan'), 0]The application helper raises ValueError.
SciPy's own documentation warns that NaNs can lead to unexpected peak and prominence results and suggests removing or replacing them when using those APIs. The application policy is intentionally stricter: this diagnostic does neither automatically.
That is not pedantry. Removing the NaN would shorten the array and change original sample indices. If event identity is an original telemetry position and spacing is measured in positions, coordinate compression can change both provenance and geometry. Replacing the NaN would make a different assumption about the missing value and could create or destroy a local maximum.
Both transformations may be legitimate in another pipeline. Neither is silently authorized here.
P10 similarly rejects distance=0. SciPy's public parameter contract requires distance >= 1, and the tagged implementation explicitly raises an error for values below one. The custom policy goes further by requiring a positive Python integer and rejecting Booleans.
The release policy should therefore issue HOLD when any of the following is unknown: whether the input is finite, whether sample positions correspond to qualified uniform spacing, or who owns the selection semantics.
“HOLD” is not an assertion that the data are unusable. It says the current evidence is insufficient to interpret a five-position exclusion as the intended physical separation.
A changed sampling rate deserves special attention. Even if the array remains finite and uniformly sampled, the same integer distance=5 can correspond to a different elapsed-time interval after resampling or acquisition changes. That is a signal-domain change and therefore triggers RERUN of the selection qualification.
Plateau handling is also outside this fixture. SciPy notes that while returned peak indices satisfy the distance rule, edges of flat peaks may be closer than the requested distance. Because this playbook deliberately excludes plateau-edge-spacing policy, introducing flat-top events into the admitted signal domain requires a new qualification rather than extrapolation from these three sharp maxima.
Reconcile every candidate and every exclusion
A release review should be able to account for every candidate by identity.
Here is the complete candidate disposition for the main fixture:
Candidate | 1 | 5 | 12 |
Height | 5 | 10 | 15 |
Prominence | 5 | 1 | 15 |
Prominence eligible? | Yes | No | Yes |
SciPy distance-first stage | Removed because index 5 is taller and only 4 samples away | Retained over 1 | Retained |
SciPy later prominence stage | Not reconsidered | Rejected: 1 < 2 | Passes |
SciPy combined result | Removed | Removed | Retained |
Application eligible-first spacing | Eligible; 11 samples from 12 | Excluded before spacing | Eligible and nonconflicting with 1 |
Application result | Retained | Removed | Retained |
This table exposes the suppressor explicitly. Index 5 does not need to be a final output to change the final identity set. Under the distance-first ordering, its temporary survival removes index 1 from later consideration.
The supplied preparation case ledger is equally important:
Case | Observation expected by preparation | Diagnostic purpose |
P1 | candidates [1, 5, 12], prominences [5.0, 1.0, 15.0] | Establish candidate/property ledger |
P2 | distance-only [5, 12] | Expose index 1 suppression |
P3 | combined filters [12] | Confirm distance-first counterexample |
P4 | prominence-only [1, 12] | Establish eligibility set |
P5 | application policy [1, 12] | Verify eligible-first rule |
P6 | distance 4 plus prominence gives [1, 12] | Test inclusive distance boundary |
P7 | equal-height application fixture gives [1] | Test owned tie rule |
P8 | monotonic signal gives [] | Negative candidate control |
P9 | NaN input raises ValueError | Enforce finite-input admission |
P10 | zero distance raises ValueError | Enforce positive integer spacing |
The supplied evidence records PASS_10_CASES. That status means the prepared observations matched the expected assertions. It does not, by itself, select the application policy.
The source array is also compared against a frozen copy after the checks. No part of the application policy requires editing the signal to obtain [1, 12]; the difference comes from candidate-selection order.
A correct counterexample is still a rejected policy
P3 is worth reading carefully:
observed: [12]
expected: [12]That is a passing diagnostic.
For a caller whose approved rule is prominence eligibility before spacing, it is simultaneously evidence of an unacceptable workflow.
There is no contradiction. Testing can answer, “Did the program execute the behavior we predicted?” Policy review answers, “Is that the behavior the application is authorized to use?”
A regression suite that merges those questions tends to encode mistakes permanently. Someone sees green tests, assumes the output is sanctioned, and later treats any change toward [1, 12] as a regression.
Keep three labels separate in release evidence: observed implementation behavior, expected diagnostic behavior, and approved selection policy.
Decide accept, repair, rerun or hold
The final decision vocabulary should be small enough that reviewers use it consistently.
Decision | Use it when | Required interpretation for this problem |
ACCEPT_SCIPY_POLICY | Distance-first selection is explicitly required and the returned identities reconcile with the documented contract | [12] is acceptable for the main fixture |
ACCEPT_APPLICATION_POLICY | The separately reviewed rule is prominence eligibility first, followed by height-priority greedy spacing with the lower-index equal-height tie rule | [1, 12] is acceptable |
REPAIR | A caller assumed distance and prominence commute or used the combined SciPy call while requiring eligible-first semantics | Replace the incorrect policy plumbing; do not call SciPy's behavior a bug |
RERUN | Package version, signal-domain assumptions, sampling rate, wlen, plateau policy, or other qualified selection semantics change | Regenerate the complete ledger under the new conditions |
HOLD | Sample spacing, finite-input admission, or policy ownership is unknown | Do not approve either identity set yet |
For the scenario that motivated this playbook, the immediate release decision is REPAIR: the caller expected every prominence-eligible peak to participate in the spacing decision, but the combined find_peaks(distance=5, prominence=2) call implements a different ordering.
After that repair, the custom rule can receive ACCEPT_APPLICATION_POLICY only if its ownership and exact semantics, including the height-first greedy step and lower-index tie rule, are separately approved.
Conversely, a system that genuinely wants SciPy's documented distance-first behavior should not be “fixed” merely because [1, 12] looks intuitively attractive. Its appropriate decision is ACCEPT_SCIPY_POLICY, provided the output ledger is reconciled and that ordering is explicitly the requirement.
This is ordinary pipeline governance: successful execution is not the same thing as satisfying the contract. Broader pipeline quality checks make the same general distinction between moving data successfully and validating that the delivered information has the required quality.
No decision here makes a clinical, safety-critical, or statistical interpretation of a detected event.
Recover analyses produced under the wrong policy
Once the caller is classified REPAIR, the next question is historical: which outputs may have been created under the wrong selection contract?
Do not begin by searching for records with “too few peaks.” Counts are insufficient. An old run can have the same count under both policies while retaining different identities.
Instead, locate revisions and configurations that satisfy both conditions: they used a distance-plus-prominence selection path equivalent to the combined call, and their intended application requirement was eligibility-first or otherwise assumed those predicates commute.
For each affected run where the original qualified signal is still available, rerun candidate selection under the approved policy while preserving the original sample coordinate system. Store both identity sets:
old_policy_peaks
new_policy_peaks
added_peak_indices
removed_peak_indices
unchanged_peak_indicesFor the fixture:
old_policy_peaks = [12]
new_policy_peaks = [1, 12]
added = [1]
removed = []
unchanged = [12]That diff is more useful than reporting “peak count changed from one to two.” It identifies the event whose downstream treatment may differ.
Recovery should also preserve the old output. Do not overwrite historical evidence and then lose the ability to explain why an earlier report, alert, feature vector, or derived aggregate differed.
The required rerun boundary is the earliest retained artifact from which the approved selection can be reproduced without inventing missing geometry. If only transformed data remain and the transformation changed indices, sample spacing, NaNs, or prominence contours, the historical case may instead require HOLD because exact reconstruction is no longer supported.
Do not replay downstream effects automatically
A corrected detection set does not automatically authorize repeating every downstream action that originally depended on it.
Separate recomputation from effect replay.
Recomputing a report, feature table, or internal analytical artifact can establish what the corrected selector would have produced. Sending notifications, modifying external state, changing operational classifications, or replaying other effects is a separate owner decision.
The selector's evidence should therefore stop at a clear handoff: “these candidate identities changed under the approved policy.” Downstream system owners can then determine what remediation is appropriate for their own contracts.
That separation also prevents a repair script from multiplying the impact of a historical policy error.
Release the policy with regression evidence and ownership
A reliable release package should make the selection contract reconstructable without requiring a future maintainer to infer intent from find_peaks keyword arguments.
Record the policy name, not merely the function call.
For the application rule, a useful release statement is:
Determine all local maxima in the original admitted finite one-dimensional signal. Compute global prominence with wlen=None. Keep candidates with prominence at least 2. Among eligible candidates, consider descending raw height and break equal-height ties by lower original index. Retain a candidate only when it is at least 5 sample positions from every already retained candidate. Return retained indices in ascending index order.
Also state explicitly that this is a height-priority greedy algorithm over prominence-eligible candidates and is not a globally optimal subset calculation.
The evidence bundle should retain the prepared source digest:
1078183fc741acc77059f927e06e3e65cf195f7d8cb861ecb1704aa00c3baef2alongside the runtime labels, all ten case observations and expectations, the approved candidate oracle, the policy revision, and the reviewer or owning role that approved it. A digest establishes which fixture bytes were intended; it does not authenticate who executed them or prove that a stored JSON result came from that source.
Version control is useful for tying code and configuration revisions to an analysis, which is why versioning a reproducible analysis belongs in the release workflow. But repository history is supporting provenance, not proof that a specific runtime contained the claimed packages or that an evidence file contains the correct observations.
The versioned public contract should remain part of review. SciPy 1.17.0 documents both the smaller-peak distance elimination and the condition ordering with distance before prominence. The tagged implementation corroborates that distance selection occurs before prominence calculation and filtering in that release. Documentation is the contract boundary; source inspection helps explain the qualified version without turning every implementation detail into an application dependency.
Define rerun triggers at release time. At minimum, RERUN after a SciPy or NumPy version change that affects the qualified environment; a change in sampling geometry or rate; introduction of wlen; expansion from finite to nonfinite inputs; a change in local-maximum or plateau requirements; a different prominence floor or minimum distance; or a change to candidate priority or tie ownership.
A new signal domain can require rerun even with identical code. Passing a synthetic fixture only says that this counterexample remains understood. It does not establish that new telemetry shares the assumptions under which the selection policy was approved.
Finally, make both acceptable outcomes testable. [12] belongs to the SciPy distance-first contract for this fixture. [1, 12] belongs to the separately declared application contract. A test suite should never treat those different answers as competing attempts to reproduce one unspecified notion of “the right peaks.”
Develop the analytical judgment behind reliable selection
The durable skill in this case is not memorizing that one SciPy parameter comes before another. It is learning to turn selection code into an auditable decision process.
Start from candidate identity. Separate raw height from contour-based prominence. Record exclusion stages. State sample geometry. Own tie rules instead of guessing undocumented ones. Preserve the original coordinate system. Distinguish a passing diagnostic from an approved policy. And when two valid implementations answer different questions, name those questions before choosing between their outputs.
For this fixture, that discipline makes the release decision unambiguous: SciPy 1.17.0's distance-first result [12] is consistent with its documented policy, while the separately approved eligibility-first rule produces [1, 12]. A caller that required the latter but assumed the filters commute needs REPAIR, not a bug report against SciPy.
For analysts developing the broader reasoning skills behind this kind of review, Refonte Learning's Data Analytics Program lists a three-month curriculum at 12–14 hours per week covering Python and R, SQL, exploratory analysis, visualization, practical project work, and an educationally mentored virtual-internship opportunity; the page also states its degree-study admission requirement and certificates offered on successful completion. It does not establish that this specific SciPy peak-selection laboratory is part of the curriculum.
