diff --git a/fastdtw/_fastdtw.pyx b/fastdtw/_fastdtw.pyx index 0adfccc..5b5206d 100644 --- a/fastdtw/_fastdtw.pyx +++ b/fastdtw/_fastdtw.pyx @@ -31,7 +31,8 @@ cdef struct PathElement: int x_idx, y_idx -def fastdtw(x, y, int radius=1, dist=None): +def fastdtw(x, y, int radius=1, dist=None, + b_partial_start=False, b_partial_end=False, radius_x=4): ''' return the approximate distance between 2 time series with O(N) time and memory complexity @@ -51,6 +52,17 @@ def fastdtw(x, y, int radius=1, dist=None): dist is an int of value p > 0, then the p-norm will be used. If dist is a function then dist(x[i], y[j]) will be used. If dist is None then abs(x[i] - y[j]) will be used. + b_partial_start: bool + If True, calculate a partial match where the start of path does + not point to the start of x. Otherwise, the start of path points + to the start of x. + b_partial_end: bool + If True, calculate a partial match where the end of path does not + point to the end of x. Otherwise, the end of path points to the + end of x. + radius_x: int + When b_partial_{start|end} is True, radius_x is used for x-axis + calculation instead of radius. Returns ------- @@ -75,7 +87,8 @@ def fastdtw(x, y, int radius=1, dist=None): subtype of np.float 2) The dist input is a positive integer or None ''' - x, y = __prep_inputs(x, y, dist) + x, y, radius_x = __prep_inputs(x, y, radius, dist, + b_partial_start, b_partial_end, radius_x) # passing by reference to recursive functions apparently doesn't work # so we are passing pointers @@ -83,7 +96,8 @@ def fastdtw(x, y, int radius=1, dist=None): PyMem_Malloc((len(x) + len(y) - 1) * sizeof(PathElement))) cdef int path_len = 0, i - cost = __fastdtw(x, y, radius, dist, path, path_len) + cost = __fastdtw(x, y, radius, dist, path, path_len, + b_partial_start, b_partial_end, radius_x) path_lst = [] if path != NULL: @@ -94,14 +108,20 @@ def fastdtw(x, y, int radius=1, dist=None): cdef double __fastdtw(x, y, int radius, dist, - PathElement *path, int &path_len) except? -1: + PathElement *path, int &path_len, + b_partial_start, b_partial_end, int radius_x) except? -1: cdef int min_time_size + cdef int min_time_size_x cdef double cost min_time_size = radius + 2 + min_time_size_x = radius_x + 2 if b_partial_start or b_partial_end \ + else min_time_size - if len(x) < min_time_size or len(y) < min_time_size: - cost, path_lst = dtw(x, y, dist=dist) + if len(x) < min_time_size_x or len(y) < min_time_size: + cost, path_lst = dtw(x, y, dist=dist, + b_partial_start=b_partial_start, + b_partial_end=b_partial_end) (&path_len)[0] = len(path_lst) for i in range((&path_len)[0]): path[i].x_idx = path_lst[i][0] @@ -111,14 +131,17 @@ cdef double __fastdtw(x, y, int radius, dist, x_shrinked = __reduce_by_half(x) y_shrinked = __reduce_by_half(y) - distance = __fastdtw(x_shrinked, y_shrinked, radius, dist, path, path_len) + distance = __fastdtw(x_shrinked, y_shrinked, radius, dist, path, path_len, + b_partial_start, b_partial_end, radius_x) cdef vector[WindowElement] window - __expand_window(path, path_len, x, y, radius, dist, window) - return __dtw(x, y, window, dist, path, path_len) + __expand_window(path, path_len, x, y, radius, dist, window, + b_partial_start, radius_x) + return __dtw(x, y, window, dist, path, path_len, + b_partial_start, b_partial_end) -def dtw(x, y, dist=None): +def dtw(x, y, dist=None, b_partial_start=False, b_partial_end=False): ''' return the distance between 2 time series without approximation Parameters @@ -132,6 +155,14 @@ def dtw(x, y, dist=None): dist is an int of value p > 0, then the p-norm will be used. If dist is a function then dist(x[i], y[j]) will be used. If dist is None then abs(x[i] - y[j]) will be used. + b_partial_start: bool + If True, calculate a partial match where the start of path does + not point to the start of x. Otherwise, the start of path points + to the start of x. + b_partial_end: bool + If True, calculate a partial match where the end of path does not + point to the end of x. Otherwise, the end of path points to the + end of x. Returns ------- @@ -159,7 +190,8 @@ def dtw(x, y, dist=None): cdef int len_x, len_y, x_idx, y_idx, idx cdef double cost - x, y = __prep_inputs(x, y, dist) + x, y, _ = __prep_inputs(x, y, -1, dist, + b_partial_start, b_partial_end, -1) len_x, len_y = len(x), len(y) idx = 0 cdef WindowElement we @@ -175,6 +207,10 @@ def dtw(x, y, dist=None): we.cost_idx_left = -1 we.cost_idx_up = -1 we.cost_idx_corner = 0 + elif b_partial_start and y_idx == 0: + we.cost_idx_left = -1 + we.cost_idx_up = -1 + we.cost_idx_corner = 0 else: we.cost_idx_left = -1 \ if y_idx == 0 and x_idx > 0 \ @@ -199,7 +235,8 @@ def dtw(x, y, dist=None): (len(x) + len(y) - 1) * sizeof(PathElement))) cdef int path_len = 0 - cost = __dtw(x, y, window, dist, path, path_len) + cost = __dtw(x, y, window, dist, path, path_len, + b_partial_start, b_partial_end) path_lst = [] if path != NULL: @@ -218,7 +255,8 @@ def __norm(p): return lambda a, b: np.linalg.norm(a - b, p) -def __prep_inputs(x, y, dist): +def __prep_inputs(x, y, int radius, dist, + b_partial_start, b_partial_end, int radius_x): x = np.asanyarray(x, dtype='float') y = np.asanyarray(y, dtype='float') @@ -227,11 +265,15 @@ def __prep_inputs(x, y, dist): if isinstance(dist, numbers.Number) and dist <= 0: raise ValueError('dist cannot be a negative integer') - return x, y + if not (b_partial_start or b_partial_end): + radius_x = radius + + return x, y, radius_x cdef double __dtw(x, y, vector[WindowElement] &window, dist, - PathElement *path, int &path_len) except? -1: + PathElement *path, int &path_len, + b_partial_start, b_partial_end) except? -1: ''' calculate the distance between 2 time series where the path between 2 time series can only be in window. ''' @@ -320,13 +362,24 @@ cdef double __dtw(x, y, vector[WindowElement] &window, dist, cost[idx + 1].prev_idx = we.cost_idx_corner # recreate the path - idx = cost_len - 1 + t_end = cost_len - 1 + if b_partial_end: + distance_min = INFINITY + finding_y = len(y) - 1 + for idx in range(1, cost_len): + if window[idx - 1].y_idx == finding_y \ + and distance_min > cost[idx].cost: + distance_min = cost[idx].cost + t_end = idx + idx = t_end (&path_len)[0] = 0 while idx != 0: we = window[idx - 1] path[path_len].x_idx = we.x_idx path[path_len].y_idx = we.y_idx (&path_len)[0] += 1 + if b_partial_start and we.y_idx == 0: + break idx = cost[idx].prev_idx # reverse path @@ -335,7 +388,7 @@ cdef double __dtw(x, y, vector[WindowElement] &window, dist, path[path_len - 1 - i] = path[i] path[i] = temp - return cost[cost_len - 1].cost + return cost[t_end].cost cdef __reduce_by_half(x): @@ -352,7 +405,8 @@ cdef __reduce_by_half(x): cdef __expand_window(PathElement *path, int path_len, x, y, int radius, dist, - vector[WindowElement] &window): + vector[WindowElement] &window, + b_partial_start, int radius_x): ''' calculate a window around a path where the expansion is of length radius in all directions (including diagonals). @@ -367,6 +421,7 @@ cdef __expand_window(PathElement *path, int path_len, ''' cdef int max_x_idx, max_y_idx, prv_y_idx, cur_x_idx, x_idx, y_idx, idx + cdef int min_x_idx, min_y_idx cdef int low_y_idx, hgh_y_idx cdef WindowElement we @@ -374,6 +429,8 @@ cdef __expand_window(PathElement *path, int path_len, len_x, len_y = len(x), len(y) # step 1 + min_x_idx = path[0].x_idx + min_y_idx = path[0].y_idx max_x_idx = path[path_len - 1].x_idx max_y_idx = path[path_len - 1].y_idx @@ -399,21 +456,34 @@ cdef __expand_window(PathElement *path, int path_len, ybounds1[max_x_idx].high = prv_y_idx # step 2 - cdef int len_ybounds2 = max_x_idx + radius + 1 + cdef int min_x_expanded_idx = max(0, min_x_idx - radius_x) + cdef int max_x_expanded_idx = max_x_idx + radius_x + + cdef int len_ybounds2 = max_x_expanded_idx + 1 cdef vector[LowHigh] ybounds2 ybounds2.resize(len_ybounds2) - for x_idx in range(max_x_idx + 1): - temp_min_x_idx = max(0, x_idx - radius) + for x_idx in range(min_x_expanded_idx, min_x_idx): + ybounds2[x_idx].low = 0 + + temp_max_x_idx = min(max_x_idx, x_idx + radius_x) + ybounds2[x_idx].high = \ + min(max_y_idx + radius, ybounds1[temp_max_x_idx].high + radius) + + for x_idx in range(min_x_idx, max_x_idx + 1): + temp_min_x_idx = max(min_x_idx, x_idx - radius_x) ybounds2[x_idx].low = \ max(0, ybounds1[temp_min_x_idx].low - radius) - temp_max_x_idx = min(max_x_idx, x_idx + radius) + temp_max_x_idx = min(max_x_idx, x_idx + radius_x) ybounds2[x_idx].high = \ min(max_y_idx + radius, ybounds1[temp_max_x_idx].high + radius) - for x_idx in range(max_x_idx + 1, max_x_idx + radius + 1): - ybounds2[x_idx].low = ybounds2[x_idx - 1].low + for x_idx in range(max_x_idx + 1, max_x_expanded_idx + 1): + temp_min_x_idx = max(min_x_idx, x_idx - radius_x) + ybounds2[x_idx].low = \ + max(0, ybounds1[temp_min_x_idx].low - radius) + ybounds2[x_idx].high = ybounds2[x_idx - 1].high # step 3 @@ -423,7 +493,7 @@ cdef __expand_window(PathElement *path, int path_len, cdef int window_size = 0 - for x_idx in range(max_x_idx + radius + 1): + for x_idx in range(min_x_expanded_idx, max_x_expanded_idx + 1): if 2 * x_idx < len_x: ybounds3[2 * x_idx].low = 2 * ybounds2[x_idx].low ybounds3[2 * x_idx].high = \ @@ -441,10 +511,13 @@ cdef __expand_window(PathElement *path, int path_len, ybounds3[2 * x_idx + 1].high - ybounds3[2 * x_idx + 1].low + 1 # step 4 + cdef int min_x_doubled_idx = 2 * min_x_expanded_idx + cdef int max_x_doubled_idx = min(len_ybounds3 - 1, + 2 * max_x_expanded_idx + 1) window.resize(window_size) idx = 0 - for x_idx in range(len_ybounds3): + for x_idx in range(min_x_doubled_idx, max_x_doubled_idx + 1): low_y_idx = ybounds3[x_idx].low hgh_y_idx = ybounds3[x_idx].high @@ -454,7 +527,7 @@ cdef __expand_window(PathElement *path, int path_len, we.x_idx = x_idx we.y_idx = y_idx - if x_idx == 0 and y_idx == 0: + if (b_partial_start or x_idx == 0) and y_idx == 0: we.cost_idx_up = -1 we.cost_idx_left = -1 we.cost_idx_corner = 0 diff --git a/fastdtw/fastdtw.py b/fastdtw/fastdtw.py index f010d3a..a0a42f4 100644 --- a/fastdtw/fastdtw.py +++ b/fastdtw/fastdtw.py @@ -12,7 +12,8 @@ pass -def fastdtw(x, y, radius=1, dist=None): +def fastdtw(x, y, radius=1, dist=None, + b_partial_start=False, b_partial_end=False, radius_x=4): ''' return the approximate distance between 2 time series with O(N) time and memory complexity @@ -32,6 +33,17 @@ def fastdtw(x, y, radius=1, dist=None): dist is an int of value p > 0, then the p-norm will be used. If dist is a function then dist(x[i], y[j]) will be used. If dist is None then abs(x[i] - y[j]) will be used. + b_partial_start: bool + If True, calculate a partial match where the start of path does + not point to the start of x. Otherwise, the start of path points + to the start of x. + b_partial_end: bool + If True, calculate a partial match where the end of path does not + point to the end of x. Otherwise, the end of path points to the + end of x. + radius_x: int + When b_partial_{start|end} is True, radius_x is used for x-axis + calculation instead of radius. Returns ------- @@ -49,8 +61,11 @@ def fastdtw(x, y, radius=1, dist=None): >>> fastdtw.fastdtw(x, y) (2.0, [(0, 0), (1, 0), (2, 1), (3, 2), (4, 2)]) ''' - x, y, dist = __prep_inputs(x, y, dist) - return __fastdtw(x, y, radius, dist) + x, y, dist, radius_x = __prep_inputs(x, y, radius, dist, + b_partial_start, b_partial_end, + radius_x) + return __fastdtw(x, y, radius, dist, + b_partial_start, b_partial_end, radius_x) def __difference(a, b): @@ -61,21 +76,32 @@ def __norm(p): return lambda a, b: np.linalg.norm(np.atleast_1d(a) - np.atleast_1d(b), p) -def __fastdtw(x, y, radius, dist): +def __fastdtw(x, y, radius, dist, + b_partial_start, b_partial_end, radius_x): min_time_size = radius + 2 + min_time_size_x = radius_x + 2 if b_partial_start or b_partial_end \ + else min_time_size - if len(x) < min_time_size or len(y) < min_time_size: - return dtw(x, y, dist=dist) + if len(x) < min_time_size_x or len(y) < min_time_size: + return dtw(x, y, dist=dist, + b_partial_start=b_partial_start, + b_partial_end=b_partial_end) x_shrinked = __reduce_by_half(x) y_shrinked = __reduce_by_half(y) distance, path = \ - __fastdtw(x_shrinked, y_shrinked, radius=radius, dist=dist) - window = __expand_window(path, len(x), len(y), radius) - return __dtw(x, y, window, dist=dist) - - -def __prep_inputs(x, y, dist): + __fastdtw(x_shrinked, y_shrinked, radius=radius, dist=dist, + b_partial_start=b_partial_start, + b_partial_end=b_partial_end, + radius_x=radius_x) + window = __expand_window(path, len(x), len(y), radius, radius_x) + return __dtw(x, y, window, dist=dist, + b_partial_start=b_partial_start, + b_partial_end=b_partial_end) + + +def __prep_inputs(x, y, radius, dist, + b_partial_start, b_partial_end, radius_x): x = np.asanyarray(x, dtype='float') y = np.asanyarray(y, dtype='float') @@ -92,10 +118,13 @@ def __prep_inputs(x, y, dist): elif isinstance(dist, numbers.Number): dist = __norm(p=dist) - return x, y, dist + if not (b_partial_start or b_partial_end): + radius_x = radius + + return x, y, dist, radius_x -def dtw(x, y, dist=None): +def dtw(x, y, dist=None, b_partial_start=False, b_partial_end=False): ''' return the distance between 2 time series without approximation Parameters @@ -109,6 +138,14 @@ def dtw(x, y, dist=None): dist is an int of value p > 0, then the p-norm will be used. If dist is a function then dist(x[i], y[j]) will be used. If dist is None then abs(x[i] - y[j]) will be used. + b_partial_start: bool + If True, calculate a partial match where the start of path does + not point to the start of x. Otherwise, the start of path points + to the start of x. + b_partial_end: bool + If True, calculate a partial match where the end of path does not + point to the end of x. Otherwise, the end of path points to the + end of x. Returns ------- @@ -126,39 +163,53 @@ def dtw(x, y, dist=None): >>> fastdtw.dtw(x, y) (2.0, [(0, 0), (1, 0), (2, 1), (3, 2), (4, 2)]) ''' - x, y, dist = __prep_inputs(x, y, dist) - return __dtw(x, y, None, dist) + x, y, dist, _ = __prep_inputs(x, y, None, dist, + b_partial_start, b_partial_end, + None) + return __dtw(x, y, None, dist, b_partial_start, b_partial_end) -def __dtw(x, y, window, dist): +def __dtw(x, y, window, dist, b_partial_start, b_partial_end): len_x, len_y = len(x), len(y) if window is None: window = [(i, j) for i in range(len_x) for j in range(len_y)] window = ((i + 1, j + 1) for i, j in window) D = defaultdict(lambda: (float('inf'),)) D[0, 0] = (0, 0, 0) + if b_partial_start: + for i in range(1, len_x+1): + D[i, 0] = (0, 0, 0) for i, j in window: dt = dist(x[i-1], y[j-1]) D[i, j] = min((D[i-1, j][0]+dt, i-1, j), (D[i, j-1][0]+dt, i, j-1), (D[i-1, j-1][0]+dt, i-1, j-1), key=lambda a: a[0]) path = [] - i, j = len_x, len_y + t_end = len_x + if b_partial_end: + distance_min = float('inf') + for i in range(1, len_x+1): + if distance_min > D[i, len_y][0]: + distance_min = D[i, len_y][0] + t_end = i + i, j = t_end, len_y while not (i == j == 0): path.append((i-1, j-1)) + if b_partial_start and j == 1: + break; i, j = D[i, j][1], D[i, j][2] path.reverse() - return (D[len_x, len_y][0], path) + return (D[t_end, len_y][0], path) def __reduce_by_half(x): return [(x[i] + x[1+i]) / 2 for i in range(0, len(x) - len(x) % 2, 2)] -def __expand_window(path, len_x, len_y, radius): +def __expand_window(path, len_x, len_y, radius, radius_x): path_ = set(path) for i, j in path: for a, b in ((i + a, j + b) - for a in range(-radius, radius+1) + for a in range(-radius_x, radius_x+1) for b in range(-radius, radius+1)): path_.add((a, b)) @@ -179,6 +230,6 @@ def __expand_window(path, len_x, len_y, radius): new_start_j = j elif new_start_j is not None: break - start_j = new_start_j + start_j = new_start_j if new_start_j is not None else 0 return window diff --git a/tests/test_fastdtw.py b/tests/test_fastdtw.py index 9bbdb5e..beffe14 100644 --- a/tests/test_fastdtw.py +++ b/tests/test_fastdtw.py @@ -21,23 +21,136 @@ def setUp(self): self.y_2d = np.array([[2, 2], [3, 3], [4, 4]]) self.dist_2d = lambda a, b: sum((a - b) ** 2) ** 0.5 + self.expected_path_full = [(0, 0), (1, 0), (2, 1), (3, 2), (4, 2)] + self.expected_path_partial_start = [(1, 0), (2, 1), (3, 2), (4, 2)] + self.expected_path_partial_end = [(0, 0), (1, 0), (2, 1), (3, 2)] + self.expected_path_partial_both = [(1, 0), (2, 1), (3, 2)] + def test_1d_fastdtw(self): - distance_c = fastdtw_c(self.x_1d, self.y_1d)[0] - distance_p = fastdtw_p(self.x_1d, self.y_1d)[0] + distance_c, path_c = fastdtw_c(self.x_1d, self.y_1d) + distance_p, path_p = fastdtw_p(self.x_1d, self.y_1d) self.assertEqual(distance_c, 2) self.assertEqual(distance_c, distance_p) + self.assertEqual(path_c, self.expected_path_full) + self.assertEqual(path_c, path_p) def test_1d_dtw(self): - distance_c = dtw_c(self.x_1d, self.y_1d)[0] - distance_p = dtw_p(self.x_1d, self.y_1d)[0] + distance_c, path_c = dtw_c(self.x_1d, self.y_1d) + distance_p, path_p = dtw_p(self.x_1d, self.y_1d) self.assertEqual(distance_c, 2) self.assertEqual(distance_c, distance_p) + self.assertEqual(path_c, self.expected_path_full) + self.assertEqual(path_c, path_p) + + def test_1d_fastdtw_partial_start(self): + distance_c, path_c = fastdtw_c(self.x_1d, self.y_1d, + b_partial_start=True) + distance_p, path_p = fastdtw_p(self.x_1d, self.y_1d, + b_partial_start=True) + self.assertEqual(distance_c, 1) + self.assertEqual(distance_c, distance_p) + self.assertEqual(path_c, self.expected_path_partial_start) + self.assertEqual(path_c, path_p) + + def test_1d_fastdtw_partial_end(self): + distance_c, path_c = fastdtw_c(self.x_1d, self.y_1d, + b_partial_end=True) + distance_p, path_p = fastdtw_p(self.x_1d, self.y_1d, + b_partial_end=True) + self.assertEqual(distance_c, 1) + self.assertEqual(distance_c, distance_p) + self.assertEqual(path_c, self.expected_path_partial_end) + self.assertEqual(path_c, path_p) + + def test_1d_fastdtw_partial_both(self): + distance_c, path_c = fastdtw_c(self.x_1d, self.y_1d, + b_partial_start=True, + b_partial_end=True) + distance_p, path_p = fastdtw_p(self.x_1d, self.y_1d, + b_partial_start=True, + b_partial_end=True) + self.assertEqual(distance_c, 0) + self.assertEqual(distance_c, distance_p) + self.assertEqual(path_c, self.expected_path_partial_both) + self.assertEqual(path_c, path_p) + + def test_1d_dtw_partial_start(self): + distance_c, path_c = dtw_c(self.x_1d, self.y_1d, + b_partial_start=True) + distance_p, path_p = dtw_p(self.x_1d, self.y_1d, + b_partial_start=True) + self.assertEqual(distance_c, 1) + self.assertEqual(distance_c, distance_p) + self.assertEqual(path_c, self.expected_path_partial_start) + self.assertEqual(path_c, path_p) + + def test_1d_dtw_partial_end(self): + distance_c, path_c = dtw_c(self.x_1d, self.y_1d, + b_partial_end=True) + distance_p, path_p = dtw_p(self.x_1d, self.y_1d, + b_partial_end=True) + self.assertEqual(distance_c, 1) + self.assertEqual(distance_c, distance_p) + self.assertEqual(path_c, self.expected_path_partial_end) + self.assertEqual(path_c, path_p) + + def test_1d_dtw_partial_both(self): + distance_c, path_c = dtw_c(self.x_1d, self.y_1d, + b_partial_start=True, + b_partial_end=True) + distance_p, path_p = dtw_p(self.x_1d, self.y_1d, + b_partial_start=True, + b_partial_end=True) + self.assertEqual(distance_c, 0) + self.assertEqual(distance_c, distance_p) + self.assertEqual(path_c, self.expected_path_partial_both) + self.assertEqual(path_c, path_p) def test_2d_fastdtw(self): - distance_c = fastdtw_c(self.x_2d, self.y_2d, dist=self.dist_2d)[0] - distance_p = fastdtw_p(self.x_2d, self.y_2d, dist=self.dist_2d)[0] + distance_c, path_c = fastdtw_c(self.x_2d, self.y_2d, dist=self.dist_2d) + distance_p, path_p = fastdtw_p(self.x_2d, self.y_2d, dist=self.dist_2d) self.assertAlmostEqual(distance_c, ((1+1)**0.5)*2) self.assertEqual(distance_c, distance_p) + self.assertEqual(path_c, self.expected_path_full) + self.assertEqual(path_c, path_p) + + def test_2d_fastdtw_partial_start(self): + distance_c, path_c = fastdtw_c(self.x_2d, self.y_2d, + dist=self.dist_2d, + b_partial_start=True) + distance_p, path_p = fastdtw_p(self.x_2d, self.y_2d, + dist=self.dist_2d, + b_partial_start=True) + self.assertAlmostEqual(distance_c, (1+1)**0.5) + self.assertAlmostEqual(distance_c, distance_p) + self.assertEqual(path_c, self.expected_path_partial_start) + self.assertEqual(path_c, path_p) + + def test_2d_fastdtw_partial_end(self): + distance_c, path_c = fastdtw_c(self.x_2d, self.y_2d, + dist=self.dist_2d, + b_partial_end=True) + distance_p, path_p = fastdtw_p(self.x_2d, self.y_2d, + dist=self.dist_2d, + b_partial_end=True) + self.assertAlmostEqual(distance_c, (1+1)**0.5) + self.assertAlmostEqual(distance_c, distance_p) + self.assertEqual(path_c, self.expected_path_partial_end) + self.assertEqual(path_c, path_p) + + def test_2d_fastdtw_partial_both(self): + distance_c, path_c = fastdtw_c(self.x_2d, self.y_2d, + dist=self.dist_2d, + b_partial_start=True, + b_partial_end=True) + distance_p, path_p = fastdtw_p(self.x_2d, self.y_2d, + dist=self.dist_2d, + b_partial_start=True, + b_partial_end=True) + self.assertAlmostEqual(distance_c, 0) + self.assertAlmostEqual(distance_c, distance_p) + self.assertEqual(path_c, self.expected_path_partial_both) + self.assertEqual(path_c, path_p) def test_2d_pnorm(self): distance_c = fastdtw_c(self.x_2d, self.y_2d, dist=2)[0]