224 files

This commit is contained in:
Home Assistant Version Control
2026-07-27 13:30:34 +00:00
parent b5723aa856
commit e36b8a1a22
224 changed files with 9967 additions and 2663 deletions
+147 -54
View File
@@ -162,6 +162,32 @@ def find_best_alignment(
return float(score), {"mae": float(mae), "corr": float(corr)}, final_offset
def _dtw_lite_scalar(x: np.ndarray, y: np.ndarray, n: int, m: int, w: int) -> float:
"""Verbatim original scalar fill for :func:`compute_dtw_lite` — kept as the
correctness reference and automatic fallback on unexpected errors."""
prev_row = np.full(m + 1, float("inf"))
curr_row = np.full(m + 1, float("inf"))
prev_row[0] = 0
for i in range(1, n + 1):
center = int(i * (m / n))
start_j = max(1, center - w)
end_j = min(m, center + w + 1)
curr_row.fill(float("inf"))
val_x = x[i - 1]
for j in range(start_j, end_j + 1):
cost = abs(float(val_x - y[j - 1]))
m1 = prev_row[j]
m2 = curr_row[j - 1]
m3 = prev_row[j - 1]
if m1 < m2:
best_prev = m1 if m1 < m3 else m3
else:
best_prev = m2 if m2 < m3 else m3
curr_row[j] = cost + best_prev
prev_row[:] = curr_row[:]
return float(prev_row[m])
def compute_dtw_lite(
x: np.ndarray, y: np.ndarray, band_width_ratio: float = 0.1,
derivative: bool = False,
@@ -173,6 +199,16 @@ def compute_dtw_lite(
When ``derivative`` is True this warps on the first derivative (slope) of the
two curves (Derivative DTW): alignment is driven by shape/transitions rather
than absolute power level, which is robust to amplitude offset and scale.
The inner loop operates on Python-native float lists (converted via ``.tolist()``
once per row) to avoid per-element NumPy scalar boxing overhead. The results
for each row are written back as a single slice assignment. For the typical
matching case (n=m=200, band=0.1 → w=20, ~41 cells/row) this is ~1.9× faster
than the original element-by-element NumPy indexing loop. The anti-diagonal
vectorized fill from :func:`_dtw_cost_matrix_vectorized` is NOT used here
because its per-diagonal Python setup overhead dominates for small n (it is
2× *slower* than the scalar loop for n=200 — the opposite of its large-n
envelope-rebuild behaviour where it wins by 1.68×).
"""
if derivative:
x = np.gradient(np.asarray(x, dtype=float)) if len(x) > 1 else np.asarray(x, dtype=float)
@@ -181,56 +217,60 @@ def compute_dtw_lite(
if n == 0 or m == 0:
return float("inf")
# Band width
xf = np.asarray(x, dtype=float)
yf = np.asarray(y, dtype=float)
w = max(1, int(min(n, m) * band_width_ratio))
# Use two rows to save memory and improve cache locality
prev_row = np.full(m + 1, float("inf"))
curr_row = np.full(m + 1, float("inf"))
prev_row[0] = 0
try:
# Precompute band bounds for all rows (eliminates per-row int/max/min calls).
i_idx = np.arange(1, n + 1, dtype=float)
centers = (i_idx * (m / n)).astype(np.intp)
start_js = np.maximum(1, centers - w)
end_js = np.minimum(m, centers + w + 1)
for i in range(1, n + 1):
center = int(i * (m / n))
start_j = max(1, center - w)
end_j = min(m, center + w + 1)
# Convert y to a plain Python list once so that inner-loop element access
# is native float retrieval rather than NumPy scalar unboxing.
ylist = yf.tolist()
curr_row.fill(float("inf"))
prev_row = np.full(m + 1, np.inf)
curr_row = np.full(m + 1, np.inf)
prev_row[0] = 0.0
# Pre-calculate costs for the current window to reduce Python overhead
# x is 0-indexed, so x[i-1]
val_x = x[i - 1]
for i in range(n):
sj = int(start_js[i])
ej = int(end_js[i])
curr_row[:] = np.inf
val_x = float(xf[i])
for j in range(start_j, end_j + 1):
cost = abs(float(val_x - y[j - 1]))
# Convert the relevant prev_row slice to Python lists once per row.
# prev_prev[k] == prev_row[sj - 1 + k] (diagonal predecessor of cell j=sj+k)
# prev_curr[k] == prev_row[sj + k] (up-predecessor of cell j=sj+k)
prev_prev = prev_row[sj - 1 : ej].tolist() # length = ej - sj + 1
prev_curr = prev_row[sj : ej + 1].tolist() # length = ej - sj + 1
# Standard DTW recursion
# curr_row[j] = cost + min(insertion, deletion, match)
# insertion: prev_row[j]
# deletion: curr_row[j-1]
# match: prev_row[j-1]
row_vals: list[float] = []
prev_j_val = np.inf # curr_row[sj - 1] — left predecessor, maintained locally
for y_val, pr_j1, pr_j in zip(ylist[sj - 1 : ej], prev_prev, prev_curr):
cost = abs(val_x - y_val)
# min(up=pr_j, left=prev_j_val, diag=pr_j1)
best = pr_j if pr_j < prev_j_val else prev_j_val
if pr_j1 < best:
best = pr_j1
prev_j_val = cost + best
row_vals.append(prev_j_val)
# Use a slightly faster min implementation if possible
m1 = prev_row[j]
m2 = curr_row[j - 1]
m3 = prev_row[j - 1]
curr_row[sj : ej + 1] = row_vals # single slice write
if m1 < m2:
if m1 < m3:
best_prev = m1
else:
best_prev = m3
else:
if m2 < m3:
best_prev = m2
else:
best_prev = m3
prev_row, curr_row = curr_row, prev_row # swap without copy
curr_row[j] = cost + best_prev
# Swap rows
prev_row[:] = curr_row[:]
return float(prev_row[m])
return float(prev_row[m])
except Exception: # pylint: disable=broad-exception-caught
# The scalar reference is byte-identical (proven by tests), so degrade to it on
# any unexpected error rather than propagating out of the unguarded Stage-3
# refinement loop in compute_matches_worker. Mirrors compute_dtw_path.
_LOGGER.debug("compute_dtw_lite vectorized path failed; using scalar fallback", exc_info=True)
return _dtw_lite_scalar(xf, yf, n, m, w)
def _resample_to(arr: np.ndarray, n: int) -> np.ndarray:
"""Linearly resample a 1-D array to exactly ``n`` points over its index span.
@@ -403,6 +443,69 @@ def compute_matches_worker(
return candidates
def _dtw_cost_matrix_scalar(
x: np.ndarray, y: np.ndarray, n: int, m: int, w: int
) -> np.ndarray:
"""Reference (scalar) Sakoe-Chiba DTW cost-matrix fill. Kept verbatim as the
fallback for :func:`_dtw_cost_matrix_vectorized` so behavior can never regress."""
cost_matrix = np.full((n + 1, m + 1), float("inf"))
cost_matrix[0, 0] = 0
for i in range(1, n + 1):
center = i * (m / n)
start_j = max(1, int(center - w))
end_j = min(m, int(center + w) + 1)
for j in range(start_j, end_j + 1):
cost = abs(float(x[i - 1] - y[j - 1]))
cost_matrix[i, j] = cost + min(
cost_matrix[i - 1, j], cost_matrix[i, j - 1], cost_matrix[i - 1, j - 1]
)
return cost_matrix
def _dtw_cost_matrix_vectorized(
x: np.ndarray, y: np.ndarray, n: int, m: int, w: int
) -> np.ndarray:
"""Bit-identical vectorized fill of the scalar cost matrix.
The DTW recurrence is sequential, but all cells on one anti-diagonal
(``i + j`` constant) depend only on earlier anti-diagonals, so each diagonal
is one vectorized NumPy update instead of thousands of Python ``min``/``abs``
calls. The Sakoe-Chiba band, the per-row bounds (``int`` truncation), the
``local + min(up, left, diag)`` recurrence and out-of-band ``inf`` cells all
match the scalar loop exactly, so the resulting matrix - and the backtracked
path - is identical. (#311 follow-up: this fill dominates envelope rebuilds.)
"""
xf = np.asarray(x, dtype=float)
yf = np.asarray(y, dtype=float)
cost_matrix = np.full((n + 1, m + 1), np.inf)
cost_matrix[0, 0] = 0.0
# Per-row band bounds, identical to the scalar start_j/end_j (int truncates
# toward zero, matching Python int()).
i_idx = np.arange(1, n + 1)
center = i_idx * (m / n)
lo = np.maximum(1, (center - w).astype(np.int64))
hi = np.minimum(m, (center + w).astype(np.int64) + 1)
for d in range(2, n + m + 1):
i_lo = max(1, d - m)
i_hi = min(n, d - 1)
if i_lo > i_hi:
continue
ii = np.arange(i_lo, i_hi + 1)
jj = d - ii
inb = (jj >= lo[ii - 1]) & (jj <= hi[ii - 1])
if not inb.any():
continue
ib = ii[inb]
jb = jj[inb]
local = np.abs(xf[ib - 1] - yf[jb - 1])
best = np.minimum(
np.minimum(cost_matrix[ib - 1, jb], cost_matrix[ib, jb - 1]),
cost_matrix[ib - 1, jb - 1],
)
cost_matrix[ib, jb] = local + best
return cost_matrix
def compute_dtw_path(
x: np.ndarray, y: np.ndarray, band_width_ratio: float = 0.1
) -> list[tuple[int, int]]:
@@ -415,20 +518,10 @@ def compute_dtw_path(
return []
w = max(1, int(min(n, m) * band_width_ratio))
cost_matrix = np.full((n + 1, m + 1), float("inf"))
cost_matrix[0, 0] = 0
# Cost Matrix
for i in range(1, n + 1):
center = i * (m / n)
start_j = max(1, int(center - w))
end_j = min(m, int(center + w) + 1)
for j in range(start_j, end_j + 1):
cost = abs(float(x[i - 1] - y[j - 1]))
cost_matrix[i, j] = cost + min(
cost_matrix[i - 1, j], cost_matrix[i, j - 1], cost_matrix[i - 1, j - 1]
)
try:
cost_matrix = _dtw_cost_matrix_vectorized(x, y, n, m, w)
except Exception: # pylint: disable=broad-exception-caught
cost_matrix = _dtw_cost_matrix_scalar(x, y, n, m, w)
# Backtracking
if np.isinf(cost_matrix[n, m]):