Skip to content

Commit 058786e

Browse files
committed
Add variant reduce_accumulate2
This adds a new variant reduce_accumulate2 using the scan with an associative operator that is slower in a single-threaded context but can take full advantage of parallel scan implementations.
1 parent 1e9930a commit 058786e

4 files changed

Lines changed: 55 additions & 8 deletions

File tree

README.md

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ More precisely,
1818
1. [`branchless`](fast_frechet/branchless.py): A variant w/o branches.
1919
1. [`linear_memory`](fast_frechet/linear_memory.py): This formulation reduces the quadratic memory footprint to a linear one.
2020
1. [`accumulate`](fast_frechet/accumulate.py): Formulation using a scan operation.
21-
1. [`reduce_accumulate`](fast_frechet/reduce_accumulate.py): Formulation using a fold operation.
21+
1. [`reduce_accumulate`](fast_frechet/reduce_accumulate.py): Formulation using scan and fold operations.
22+
1. [`reduce_accumulate2`](fast_frechet/reduce_accumulate2.py): Alternative formulation using the scan with an associative operator that is slower in a single-threaded context but can take full advantage of parallel scan implementations.
2223
1. [`compiled`](fast_frechet/compiled.py): Variant of [`reduce_accumulate`](fast_frechet/reduce_accumulate.py) using the [Numba library](https://numba.pydata.org/) for JIT compilation of the innermost loop.
2324

2425
Implementations of all these variants can be found under [`fast_frechet/`](fast_frechet/) or by simply clicking on the listed names above.
@@ -40,6 +41,7 @@ $ pre-commit install
4041
The snippet below estimates the Fréchet distance between the polygonal curves `p` and `q` using the Euclidean distance as a metric to measure distances between points:
4142

4243
```python
44+
>>> import numpy as np
4345
>>> from fast_frechet.linear_memory import frechet_distance
4446

4547
>>> p = np.array([[1, 2], [3, 4]])
@@ -55,13 +57,14 @@ For invoking the [benchmark script](fast_frechet/__main__.py), run:
5557
$ python fast_frechet
5658
Length of trajectory = 1024
5759

58-
no_recursion: 2303 ms
59-
vectorized: 553 ms
60-
branchless: 508 ms
61-
linear_memory: 348 ms
62-
accumulate: 286 ms
63-
reduce_accumulate: 282 ms
64-
compiled: 11 ms
60+
no_recursion: 1915 ms
61+
vectorized: 495 ms
62+
branchless: 466 ms
63+
linear_memory: 294 ms
64+
accumulate: 258 ms
65+
reduce_accumulate: 249 ms
66+
reduce_accumulate2: 360 ms
67+
compiled: 9 ms
6568
```
6669
(Note that we don't even try to benchmark the [`vanilla`](fast_frechet/vanilla.py) version here, as it already crashes for polygonal curves with a few hundred points due to its recursive nature.)
6770

fast_frechet/__main__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
linear_memory,
1111
no_recursion,
1212
reduce_accumulate,
13+
reduce_accumulate2,
1314
vectorized,
1415
)
1516

@@ -43,6 +44,7 @@ def main(*, n=1024, seed=42):
4344
linear_memory,
4445
accumulate,
4546
reduce_accumulate,
47+
reduce_accumulate2,
4648
compiled,
4749
]:
4850
f = partial(v.frechet_distance, metric=metric)

fast_frechet/reduce_accumulate2.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from functools import partial, reduce
2+
from itertools import accumulate
3+
4+
import numpy as np
5+
6+
7+
def frechet_combine(a, b):
8+
v1, d1 = a
9+
v2, d2 = b
10+
v, d = min(v1, v2), max(d1, d2)
11+
return d if d2 > v1 else v, v if d1 > v2 else d
12+
13+
14+
def frechet_next(v, d):
15+
v[1:] = np.minimum(v[:-1], v[1:])
16+
v = np.maximum(v, d)
17+
18+
d[0] = v[0]
19+
return [d for _, d in accumulate(zip(v, d), frechet_combine)]
20+
21+
22+
def frechet_distance(p, q, metric):
23+
d = map(partial(metric, q), p)
24+
init = np.maximum.accumulate(next(d))
25+
return reduce(frechet_next, d, init)[-1]

tests/test_frechet.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
linear_memory,
99
no_recursion,
1010
reduce_accumulate,
11+
reduce_accumulate2,
1112
vanilla,
1213
vectorized,
1314
)
@@ -41,6 +42,7 @@ def generate_trajectory(n, *, dim, rng):
4142
linear_memory,
4243
accumulate,
4344
reduce_accumulate,
45+
reduce_accumulate2,
4446
compiled,
4547
],
4648
)
@@ -63,6 +65,7 @@ def test_simple_example(variant):
6365
linear_memory,
6466
accumulate,
6567
reduce_accumulate,
68+
reduce_accumulate2,
6669
compiled,
6770
],
6871
)
@@ -83,3 +86,17 @@ def test_frechet(variant, P, Q, dim, seed):
8386
dqp = variant.frechet_distance(q, p, metric=f)
8487
assert dpq == d_exp
8588
assert dqp == d_exp
89+
90+
91+
@pytest.mark.parametrize("seed", range(1_000))
92+
def test_frechet_combine_associativity(seed):
93+
rng = np.random.default_rng(seed)
94+
95+
vd1, vd2, vd3 = rng.integers(0, 6, size=(3, 2))
96+
97+
vd1 = np.sort(vd1)[::-1]
98+
vd2 = np.sort(vd2)[::-1]
99+
vd3 = np.sort(vd3)[::-1]
100+
101+
f = reduce_accumulate2.frechet_combine
102+
assert f(f(vd1, vd2), vd3) == f(vd1, f(vd2, vd3))

0 commit comments

Comments
 (0)