diff --git a/aeon/distances/elastic/_edr.py b/aeon/distances/elastic/_edr.py index aaf6d35e53..0f2409d7cb 100644 --- a/aeon/distances/elastic/_edr.py +++ b/aeon/distances/elastic/_edr.py @@ -186,10 +186,51 @@ def _edr_distance( bounding_matrix: np.ndarray, epsilon: float | None = None, ) -> float: - distance = _edr_cost_matrix(x, y, bounding_matrix, epsilon)[ - x.shape[1] - 1, y.shape[1] - 1 - ] - return float(distance / max(x.shape[1], y.shape[1])) + """Compute the EDR distance between two time series. + + This is optimized for memory usage by using a two-row buffer (O(M) space) + instead of allocating the full O(NM) cost matrix. The buffers are sized by + ``y`` (no swap) so the asymmetric EDR boundary is reproduced exactly. + """ + x_size = x.shape[1] + y_size = y.shape[1] + if epsilon is None: + epsilon = float(max(np.std(x), np.std(y))) / 4 + + prev = np.full(y_size + 1, np.inf) + curr = np.full(y_size + 1, np.inf) + + # Row 0 boundary (matches _edr_cost_matrix exactly, including the j == 0 case + # that reads bounding_matrix[0, -1]). + for j in range(y_size): + if bounding_matrix[0, j - 1]: + prev[j] = 0 + prev[0] = 0 + + for i in range(1, x_size + 1): + # Column 0 boundary: 0 if in bounds, else inf. + if bounding_matrix[i - 1, 0]: + curr[0] = 0 + else: + curr[0] = np.inf + for j in range(1, y_size + 1): + if bounding_matrix[i - 1, j - 1]: + if _univariate_euclidean_distance(x[:, i - 1], y[:, j - 1]) < epsilon: + cost = 0 + else: + cost = 1 + curr[j] = min( + prev[j - 1] + cost, + prev[j] + 1, + curr[j - 1] + 1, + ) + else: + curr[j] = np.inf + # Ping-pong the buffers instead of copying. + prev, curr = curr, prev + + distance = prev[y_size] + return float(distance / max(x_size, y_size)) @njit(cache=True, fastmath=True) diff --git a/aeon/distances/elastic/_lcss.py b/aeon/distances/elastic/_lcss.py index 03a542a074..7458581ce7 100644 --- a/aeon/distances/elastic/_lcss.py +++ b/aeon/distances/elastic/_lcss.py @@ -194,8 +194,34 @@ def lcss_cost_matrix( def _lcss_distance( x: np.ndarray, y: np.ndarray, bounding_matrix: np.ndarray, epsilon: float ) -> float: - distance = _lcss_cost_matrix(x, y, bounding_matrix, epsilon)[x.shape[1], y.shape[1]] - distance = 1 - (float(distance / min(x.shape[1], y.shape[1]))) + """Compute the LCSS distance between two time series. + + This is optimized for memory usage by using a two-row buffer (O(M) space) + instead of allocating the full O(NM) cost matrix. + """ + x_size = x.shape[1] + y_size = y.shape[1] + + # Row 0 and column 0 of the LCSS cost matrix are zero; excluded cells also + # stay zero, so zero-initialised buffers reproduce the matrix exactly. + prev = np.zeros(y_size + 1) + curr = np.zeros(y_size + 1) + + for i in range(1, x_size + 1): + curr[0] = 0.0 + for j in range(1, y_size + 1): + if bounding_matrix[i - 1, j - 1]: + if _univariate_euclidean_distance(x[:, i - 1], y[:, j - 1]) <= epsilon: + curr[j] = 1 + prev[j - 1] + else: + curr[j] = max(curr[j - 1], prev[j]) + else: + curr[j] = 0.0 + # Ping-pong the buffers instead of copying. + prev, curr = curr, prev + + distance = prev[y_size] + distance = 1 - (float(distance / min(x_size, y_size))) if distance < 0.0: return 0.0 return distance