"""Phase 4 — per-candidate metrics and basic rejection filters.
Given a raw :class:`~elbow_helper.types.KneeCandidate` (location + the
difference curve it came from), attach its prominence, local-noise estimate,
prominence-to-noise ratio and boundary distance, then decide whether it clears
the cheap structural filters before any expensive stability analysis.
Author
------
Warith Harchaoui, <warith.harchaoui@deraison.ai>
"""
from __future__ import annotations
import numpy as np
from .config import RobustKneeConfig
from .numerics import peak_prominence, robust_sigma_from_diffs
from .types import KneeCandidate, Reason
[docs]
def evaluate_candidate(
candidate: KneeCandidate,
y_difference: np.ndarray,
threshold_index: int,
y_scaled: np.ndarray,
) -> KneeCandidate:
"""Fill in a candidate's prominence, noise, ratio and boundary distance.
Parameters
----------
candidate : KneeCandidate
The candidate to annotate (mutated in place and returned).
y_difference : numpy.ndarray
The difference curve the candidate was found on.
threshold_index : int
Index of the generating peak on ``y_difference``.
y_scaled : numpy.ndarray
The (unsmoothed) scaled signal, for the robust noise estimate.
Returns
-------
KneeCandidate
The same object, with metric fields populated.
"""
candidate.prominence = peak_prominence(y_difference, threshold_index)
candidate.local_noise = robust_sigma_from_diffs(y_scaled)
denom = candidate.local_noise if candidate.local_noise > 1e-9 else 1e-9
candidate.noise_prominence_ratio = candidate.prominence / denom
candidate.boundary_distance = min(
candidate.knee_x_norm, 1.0 - candidate.knee_x_norm
)
return candidate
[docs]
def passes_basic_filters(
candidate: KneeCandidate, n: int, config: RobustKneeConfig
) -> bool:
"""Return ``True`` iff a candidate survives the cheap structural filters.
Sets ``candidate.rejected`` to a reason code when it fails. Rejections:
within the boundary margin, too few points on one side, within half a
smoothing window of an end, weak prominence, or weak prominence-to-noise.
Parameters
----------
candidate : KneeCandidate
The annotated candidate.
n : int
Number of samples in the curve.
config : RobustKneeConfig
Filter thresholds.
Returns
-------
bool
Whether the candidate passes.
"""
if candidate.boundary_distance < config.boundary_margin:
candidate.rejected = Reason.BOUNDARY_KNEE
return False
left = candidate.knee_index
right = n - 1 - candidate.knee_index
if left < config.min_side_points or right < config.min_side_points:
candidate.rejected = Reason.BOUNDARY_KNEE
return False
half_window = candidate.window // 2
if candidate.knee_index < half_window or candidate.knee_index > n - 1 - half_window:
candidate.rejected = Reason.BOUNDARY_KNEE
return False
if candidate.prominence < config.min_prominence:
candidate.rejected = Reason.ALL_CANDIDATES_WEAK
return False
if candidate.noise_prominence_ratio < config.min_noise_prominence_ratio:
candidate.rejected = Reason.ALL_CANDIDATES_WEAK
return False
return True