"""Limited, offline teaching examples; these are not the NLSYS processing engine. Numeric elapsed time must be expressed in seconds. No function changes its input. Requires NumPy; optional smoothing examples require SciPy. """ from __future__ import annotations import numpy as np def _vector(x, name): a = np.asarray(x, dtype=float) if a.ndim != 1: raise ValueError(f"{name} must be one-dimensional") if np.isinf(a).any(): raise ValueError(f"{name} contains infinity") return a def short_gap_linear(t_seconds, values, segment_ids, *, max_gap_seconds): """Fill only bounded NaN runs within one contiguous segment and time budget. The time budget is the distance between the bounding observed samples. Duplicate/reset/unknown time is rejected; no resampling is performed. Returns (candidate_values, imputed_mask). Review changes before accepting. """ t, x = _vector(t_seconds, "time"), _vector(values, "values") segment = np.asarray(segment_ids) if segment.ndim != 1 or len(t) != len(x) or len(segment) != len(t): raise ValueError("time, values and segment IDs must have equal lengths") if not np.isfinite(t).all() or (np.diff(t) <= 0).any(): raise ValueError("Use finite, strictly increasing elapsed seconds in one record") if not np.isfinite(max_gap_seconds) or max_gap_seconds <= 0: raise ValueError("Set a positive maximum gap in seconds") out, filled = x.copy(), np.zeros(len(x), dtype=bool) i = 0 while i < len(x): if np.isfinite(x[i]): i += 1 continue first = i while i < len(x) and not np.isfinite(x[i]): i += 1 left, right = first - 1, i if left < 0 or right == len(x): continue if t[right] - t[left] > max_gap_seconds: continue # Every intervening boundary matters, not just equality at the endpoints. if not np.all(segment[left:right + 1] == segment[left]): continue out[first:right] = np.interp(t[first:right], t[[left, right]], x[[left, right]]) filled[first:right] = True return out, filled def hampel_flags(values, *, window=7, n_sigma=3.0): """Return candidate flags, window median, window MAD. Never replace a value. Full, finite, centered windows only; edges/missing-data windows remain unchecked. Call separately on each confirmed contiguous segment. """ x = _vector(values, "values") if not isinstance(window, int) or window < 3 or window % 2 != 1: raise ValueError("window must be an odd integer >= 3") if not np.isfinite(n_sigma) or n_sigma <= 0: raise ValueError("n_sigma must be positive") flags = np.zeros(len(x), dtype=bool) medians, mads = np.full(len(x), np.nan), np.full(len(x), np.nan) radius = window // 2 for i in range(radius, len(x) - radius): w = x[i - radius:i + radius + 1] if not np.isfinite(w).all(): continue m = np.median(w) mad = np.median(np.abs(w - m)) floor = np.finfo(float).eps * max(1.0, float(np.max(np.abs(w)))) * 128 medians[i], mads[i] = m, mad flags[i] = abs(x[i] - m) > max(n_sigma * 1.4826 * mad, floor) return flags, medians, mads def savgol_candidate(t_seconds, values, *, window=51, degree=3): """Candidate for one complete, uniformly sampled segment; no peak guarantee.""" from scipy.signal import savgol_filter t, x = _vector(t_seconds, "time"), _vector(values, "values") if len(t) != len(x) or len(x) < window: raise ValueError("Provide matching time/data arrays with at least window samples") if not isinstance(window, int) or window < 3 or window % 2 != 1: raise ValueError("window must be an odd integer >= 3") if not isinstance(degree, int) or not 0 <= degree < window: raise ValueError("degree must be an integer below the window length") if not np.isfinite(x).all() or not np.isfinite(t).all(): raise ValueError("This limited example requires finite values and times") dt = np.diff(t) if (dt <= 0).any() or not np.allclose(dt, np.median(dt), rtol=1e-6, atol=1e-12): raise ValueError("This example requires a uniformly sampled contiguous segment") return savgol_filter(x, window_length=window, polyorder=degree, mode="interp") if __name__ == "__main__": # Synthetic exercise, not a product benchmark or recommended physical threshold. t = np.array([0., 1., 10.]) x = np.array([0., np.nan, 10.]) candidate, mask = short_gap_linear(t, x, [0, 0, 0], max_gap_seconds=10) assert candidate[1] == 1 and mask.tolist() == [False, True, False] assert np.isnan(x[1]) print("Synthetic example passed; original data were not changed.")