gridpoints 1.0.6__tar.gz → 1.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (26) hide show
  1. gridpoints-1.1.0/PKG-INFO +116 -0
  2. gridpoints-1.1.0/README.md +100 -0
  3. {gridpoints-1.0.6 → gridpoints-1.1.0}/pyproject.toml +3 -3
  4. gridpoints-1.1.0/src/griddemo/__init__.py +10 -0
  5. {gridpoints-1.0.6 → gridpoints-1.1.0}/src/gridpoints/__init__.py +13 -11
  6. gridpoints-1.1.0/src/gridpoints/array_libs.py +183 -0
  7. gridpoints-1.1.0/src/gridpoints/diagonal_tricks.py +106 -0
  8. gridpoints-1.1.0/src/gridpoints/kdtree.py +176 -0
  9. gridpoints-1.1.0/src/gridpoints/mlg_sort.py +289 -0
  10. gridpoints-1.1.0/src/gridpoints/padding.py +48 -0
  11. gridpoints-1.1.0/src/gridpoints.egg-info/PKG-INFO +116 -0
  12. {gridpoints-1.0.6 → gridpoints-1.1.0}/src/gridpoints.egg-info/SOURCES.txt +1 -0
  13. gridpoints-1.0.6/PKG-INFO +0 -96
  14. gridpoints-1.0.6/README.md +0 -80
  15. gridpoints-1.0.6/src/griddemo/__init__.py +0 -10
  16. gridpoints-1.0.6/src/gridpoints/array_libs.py +0 -167
  17. gridpoints-1.0.6/src/gridpoints/diagonal_tricks.py +0 -96
  18. gridpoints-1.0.6/src/gridpoints/kdtree.py +0 -203
  19. gridpoints-1.0.6/src/gridpoints/mlg_sort.py +0 -266
  20. gridpoints-1.0.6/src/gridpoints.egg-info/PKG-INFO +0 -96
  21. {gridpoints-1.0.6 → gridpoints-1.1.0}/LICENSE +0 -0
  22. {gridpoints-1.0.6 → gridpoints-1.1.0}/setup.cfg +0 -0
  23. {gridpoints-1.0.6 → gridpoints-1.1.0}/src/griddemo/quick_demo.py +0 -0
  24. {gridpoints-1.0.6 → gridpoints-1.1.0}/src/gridpoints.egg-info/dependency_links.txt +0 -0
  25. {gridpoints-1.0.6 → gridpoints-1.1.0}/src/gridpoints.egg-info/requires.txt +0 -0
  26. {gridpoints-1.0.6 → gridpoints-1.1.0}/src/gridpoints.egg-info/top_level.txt +0 -0
@@ -0,0 +1,116 @@
1
+ Metadata-Version: 2.4
2
+ Name: gridpoints
3
+ Version: 1.1.0
4
+ Summary: fast multidimensional sorting tool for point clouds using the monotonic lagrangian grid
5
+ Author: Armand de Cacqueray
6
+ License: MIT
7
+ Project-URL: Homepage, https://neighborhood-grid.github.io/gridpoints/
8
+ Project-URL: Repository, https://github.com/Neighborhood-Grid/gridpoints
9
+ Keywords: sorting,point-clouds,multi-indexing,grid,neighborhood-grid,monotonic-lagrangian-grid,squarenet,nearest-neighbor
10
+ Requires-Python: >=3.9
11
+ Description-Content-Type: text/markdown
12
+ License-File: LICENSE
13
+ Provides-Extra: demo
14
+ Requires-Dist: squarenet; extra == "demo"
15
+ Dynamic: license-file
16
+
17
+ ## ❒ CubeNet: Multi dimensional sort for point clouds
18
+ # with the gridpoints python package
19
+
20
+ `Gridpoints` maps raw indexes of unstructured **point clouds** to structured grids multi-index through a **bijective transformation**:
21
+ One point, one cell,
22
+ ```text
23
+ n <-> [i,j,…](n).
24
+ ```
25
+ It replaces and enhances the [squarenet](https://github.com/Neighborhood-Grid/SquareNet) project with a more robust points sorting algorithm. The goal is
26
+ to build a spatialy coherent multi index to later store and query the point cloud efficiently, based on adaptative axes (lines, columns, and more in 3D+) as showed on following exemple :
27
+
28
+ <img src="https://raw.githubusercontent.com/Neighborhood-Grid/CubeNet/main/ballexemple.png">
29
+
30
+ What it does: Take raw point cloud `P(N, D)` and find a grid shape and an index permutation `order` such that `Pgrid(I, J, …, D)` = P[order].reshape(*gridshape, D) is sorted along every axis of the grid. E.g. in 3D, for Pgrid = (x, y, z):
31
+ ```text
32
+ x[i+1, j, k] >= x[i, j, k]
33
+ y[i, j+1, k] >= y[i, j, k]
34
+ z[i, j, k+1] >= z[i, j, k]
35
+ ```
36
+
37
+ → On the `Pgrid` view of `P`, neighbor queries become a simple stencil look-up
38
+ ```text
39
+ neighborhood(Pgrid[i, j, k]) = {Pgrid[i±di, j±dj, k±dk] | (di, dj, dk) ≤ R},
40
+ ````
41
+ where `R` is a radius cutoff to determine, allowing local operations in O(N) time.
42
+ → Geometric operations (convolution, clustering, interpolation …) can then be applied **in O(N) time** directly on the `Pgrid` view instead of relying on complex graph or point-cloud methods
43
+
44
+ `P` can be a NumPy, PyTorch or CuPy array of any dimension (N, D). To allow natural padding when the grid has more cells than they are points in the cloud, NaNs and Infs are supported in a consistent manner:
45
+ - nans → random position
46
+ - (+-) infs → border of the grid
47
+
48
+ This allow to gridsort prime or variable number of points, as long as one is ready to deal with void/special grid cells.
49
+
50
+ Expected runtime for sorting 1 million points, tested on google colab GPU T4 for 2D/3D point clouds:
51
+ - Numpy: 10s
52
+ - Torch (gpu): 500 ms
53
+ - Cupy (gpu): 1 s (after compilation e.g. not on the first cold call, which take 10 s)
54
+
55
+ `When not to use gridpoints ?`
56
+ - high dimension: the package is implemented to support arbitrary dimension, but sweetspot is really 2D/3D. dimensions 4-6 might still be reasonable depending on the task, but anything above 8D is generally too high dimensional for gridpoints.
57
+ - weird geometries. Supported datasets goes beyond smooth convex distributions: map of Indonesia, a sponge, a donuts, an elephant, an eggshell (by specifying a suitable gridshape e.g. (128,128,2) to `gridpoints.sort()`). You can see various exemples in the [plot](https://github.com/Neighborhood-Grid/CubeNet/blob/main/plots) folder But with some limits. Bad fits: a spider web, a wind turbine, same eggshell with a naive 3d sort (gridshape = (32,32,32)) ... the issue is not that gridpoints cannot sort these distributions, but that it will produce a poor representation of the geometry.
58
+ - small point clouds: beyond a few hundred points, local operations in O(N) time is not worth the overhead, because naive quadratic implementations will probably be simultaneously simpler and faster.
59
+
60
+ ---
61
+
62
+ ### Installation
63
+
64
+ ```bash
65
+ pip install gridpoints # core only
66
+ pip install gridpoints[demo] # for the demonstration notebook.
67
+ ```
68
+ Full Demo: [notebook.ipynb](https://github.com/Neighborhood-Grid/CubeNet/blob/main/notebook.ipynb)
69
+
70
+ ### Quickstart
71
+
72
+ ```python
73
+ import gridpoints as grid
74
+ import numpy as np
75
+
76
+ # Raw point cloud (numpy, pytorch or cupy)
77
+ A = np.random.rand(1_000_000, 3)
78
+
79
+ # Sorted view: place the points inside the grid
80
+ order = grid.argsort(A, gridshape=(100, 100, 100))
81
+ Bflat = A[order] #flat but sorted with a grid layout (C-order)
82
+ Bgrid = Bflat.reshape(100, 100, 100, 3) #reshaped as a grid
83
+
84
+ # Rest of your pipeline, working with grids
85
+ Cgrid = apply_something(Bgrid) #(100, 100, 100, *)
86
+
87
+ # Back to the original points indexing
88
+ Cflat = Cgrid.reshape(1_000_000, *)
89
+ orderinv = grid.invert_permutation(order)
90
+ C = Cflat[orderinv] # matches the initial points order
91
+ ```
92
+
93
+ ---
94
+
95
+ ### Note on rectangular gridshapes
96
+
97
+ The eggshell example illustrates the importance of selecting a suitable grid-shape decomposition. This can itself be challenging when the point cloud is assumed to follow a complex topological pattern, such as a thin surface rather than a full volumetric domain. While the precise decomposition is not particularly important (e.g. (18,20,16) vs. (16,15,24) will generally make little difference), the orders of magnitude of the different dimensions do matter and should be tuned to the dataset: (18,20,16) and (36,40,4) can lead to substantially different results. As mentioned, nan/inf padding can help accomodate integer-factorization constraints
98
+
99
+ Another possibility for smoothing out topology-specific effects is to perform the sort after randomly projecting the dataset onto a lower-dimensional subspace. Random projections are known to approximately preserve pairwise distances, as formalized by the Johnson–Lindenstrauss lemma.
100
+
101
+
102
+ ### Note on the cutoff radius R
103
+
104
+ There is no strict theoretical guarantee about what the cutof radius R should be for a given task. E.g the relative grid position between a point and its nearest neighbors can't be garanted to be in the exact adjacent grid cells. What is guaranteed from the sorted ordering is only **grid monotonicity**: *x* coordinates increase along rows, *y* coordinates along columns, and so on.
105
+
106
+ As an example, empirical results in 2-D show that `R = 5` is enough for ~99 % of the nearest neighbors; some outlier neighbors will sit further apart for complex geometries with pronounced peaks, holes or any non-smoothness. When a stricter neighborhood is required, or in high dimensional setting, the solution might be to build an assembly of grid experts, each working on a rotated / projected view of the points, as discussed in [this topic](https://github.com/glotzerlab/freud/discussions/1417). An other possibility would be to tile the grid: each tile is enhanced with location metadata (e.g. a bounding box), allowing for pruning pairs of tiles with a distance certified to be far enough for a given criterion.
107
+
108
+
109
+ ### Note on efficient stencil operations
110
+
111
+ The typical use-case of `Gridpoints` is to allow fast local operations on arbitrary point clouds using stencil kernels:
112
+
113
+ ```text
114
+ output(i, j, k) = f( Pgrid[i±di, j±dj, k±dk] | di, dj, dk in local window )
115
+ ```
116
+ To go beyond standard (slow) python loops, this kind of kernel computation can be accelerated with native grid convolution operations of standard libraries whenever possible, or with `pystencils` or `taichi` compilers for complex/non O(N) grid kernels. On GPU, tiling the grid and leveraging a custom `triton` kernel might be particularly efficient.
@@ -0,0 +1,100 @@
1
+ ## ❒ CubeNet: Multi dimensional sort for point clouds
2
+ # with the gridpoints python package
3
+
4
+ `Gridpoints` maps raw indexes of unstructured **point clouds** to structured grids multi-index through a **bijective transformation**:
5
+ One point, one cell,
6
+ ```text
7
+ n <-> [i,j,…](n).
8
+ ```
9
+ It replaces and enhances the [squarenet](https://github.com/Neighborhood-Grid/SquareNet) project with a more robust points sorting algorithm. The goal is
10
+ to build a spatialy coherent multi index to later store and query the point cloud efficiently, based on adaptative axes (lines, columns, and more in 3D+) as showed on following exemple :
11
+
12
+ <img src="https://raw.githubusercontent.com/Neighborhood-Grid/CubeNet/main/ballexemple.png">
13
+
14
+ What it does: Take raw point cloud `P(N, D)` and find a grid shape and an index permutation `order` such that `Pgrid(I, J, …, D)` = P[order].reshape(*gridshape, D) is sorted along every axis of the grid. E.g. in 3D, for Pgrid = (x, y, z):
15
+ ```text
16
+ x[i+1, j, k] >= x[i, j, k]
17
+ y[i, j+1, k] >= y[i, j, k]
18
+ z[i, j, k+1] >= z[i, j, k]
19
+ ```
20
+
21
+ → On the `Pgrid` view of `P`, neighbor queries become a simple stencil look-up
22
+ ```text
23
+ neighborhood(Pgrid[i, j, k]) = {Pgrid[i±di, j±dj, k±dk] | (di, dj, dk) ≤ R},
24
+ ````
25
+ where `R` is a radius cutoff to determine, allowing local operations in O(N) time.
26
+ → Geometric operations (convolution, clustering, interpolation …) can then be applied **in O(N) time** directly on the `Pgrid` view instead of relying on complex graph or point-cloud methods
27
+
28
+ `P` can be a NumPy, PyTorch or CuPy array of any dimension (N, D). To allow natural padding when the grid has more cells than they are points in the cloud, NaNs and Infs are supported in a consistent manner:
29
+ - nans → random position
30
+ - (+-) infs → border of the grid
31
+
32
+ This allow to gridsort prime or variable number of points, as long as one is ready to deal with void/special grid cells.
33
+
34
+ Expected runtime for sorting 1 million points, tested on google colab GPU T4 for 2D/3D point clouds:
35
+ - Numpy: 10s
36
+ - Torch (gpu): 500 ms
37
+ - Cupy (gpu): 1 s (after compilation e.g. not on the first cold call, which take 10 s)
38
+
39
+ `When not to use gridpoints ?`
40
+ - high dimension: the package is implemented to support arbitrary dimension, but sweetspot is really 2D/3D. dimensions 4-6 might still be reasonable depending on the task, but anything above 8D is generally too high dimensional for gridpoints.
41
+ - weird geometries. Supported datasets goes beyond smooth convex distributions: map of Indonesia, a sponge, a donuts, an elephant, an eggshell (by specifying a suitable gridshape e.g. (128,128,2) to `gridpoints.sort()`). You can see various exemples in the [plot](https://github.com/Neighborhood-Grid/CubeNet/blob/main/plots) folder But with some limits. Bad fits: a spider web, a wind turbine, same eggshell with a naive 3d sort (gridshape = (32,32,32)) ... the issue is not that gridpoints cannot sort these distributions, but that it will produce a poor representation of the geometry.
42
+ - small point clouds: beyond a few hundred points, local operations in O(N) time is not worth the overhead, because naive quadratic implementations will probably be simultaneously simpler and faster.
43
+
44
+ ---
45
+
46
+ ### Installation
47
+
48
+ ```bash
49
+ pip install gridpoints # core only
50
+ pip install gridpoints[demo] # for the demonstration notebook.
51
+ ```
52
+ Full Demo: [notebook.ipynb](https://github.com/Neighborhood-Grid/CubeNet/blob/main/notebook.ipynb)
53
+
54
+ ### Quickstart
55
+
56
+ ```python
57
+ import gridpoints as grid
58
+ import numpy as np
59
+
60
+ # Raw point cloud (numpy, pytorch or cupy)
61
+ A = np.random.rand(1_000_000, 3)
62
+
63
+ # Sorted view: place the points inside the grid
64
+ order = grid.argsort(A, gridshape=(100, 100, 100))
65
+ Bflat = A[order] #flat but sorted with a grid layout (C-order)
66
+ Bgrid = Bflat.reshape(100, 100, 100, 3) #reshaped as a grid
67
+
68
+ # Rest of your pipeline, working with grids
69
+ Cgrid = apply_something(Bgrid) #(100, 100, 100, *)
70
+
71
+ # Back to the original points indexing
72
+ Cflat = Cgrid.reshape(1_000_000, *)
73
+ orderinv = grid.invert_permutation(order)
74
+ C = Cflat[orderinv] # matches the initial points order
75
+ ```
76
+
77
+ ---
78
+
79
+ ### Note on rectangular gridshapes
80
+
81
+ The eggshell example illustrates the importance of selecting a suitable grid-shape decomposition. This can itself be challenging when the point cloud is assumed to follow a complex topological pattern, such as a thin surface rather than a full volumetric domain. While the precise decomposition is not particularly important (e.g. (18,20,16) vs. (16,15,24) will generally make little difference), the orders of magnitude of the different dimensions do matter and should be tuned to the dataset: (18,20,16) and (36,40,4) can lead to substantially different results. As mentioned, nan/inf padding can help accomodate integer-factorization constraints
82
+
83
+ Another possibility for smoothing out topology-specific effects is to perform the sort after randomly projecting the dataset onto a lower-dimensional subspace. Random projections are known to approximately preserve pairwise distances, as formalized by the Johnson–Lindenstrauss lemma.
84
+
85
+
86
+ ### Note on the cutoff radius R
87
+
88
+ There is no strict theoretical guarantee about what the cutof radius R should be for a given task. E.g the relative grid position between a point and its nearest neighbors can't be garanted to be in the exact adjacent grid cells. What is guaranteed from the sorted ordering is only **grid monotonicity**: *x* coordinates increase along rows, *y* coordinates along columns, and so on.
89
+
90
+ As an example, empirical results in 2-D show that `R = 5` is enough for ~99 % of the nearest neighbors; some outlier neighbors will sit further apart for complex geometries with pronounced peaks, holes or any non-smoothness. When a stricter neighborhood is required, or in high dimensional setting, the solution might be to build an assembly of grid experts, each working on a rotated / projected view of the points, as discussed in [this topic](https://github.com/glotzerlab/freud/discussions/1417). An other possibility would be to tile the grid: each tile is enhanced with location metadata (e.g. a bounding box), allowing for pruning pairs of tiles with a distance certified to be far enough for a given criterion.
91
+
92
+
93
+ ### Note on efficient stencil operations
94
+
95
+ The typical use-case of `Gridpoints` is to allow fast local operations on arbitrary point clouds using stencil kernels:
96
+
97
+ ```text
98
+ output(i, j, k) = f( Pgrid[i±di, j±dj, k±dk] | di, dj, dk in local window )
99
+ ```
100
+ To go beyond standard (slow) python loops, this kind of kernel computation can be accelerated with native grid convolution operations of standard libraries whenever possible, or with `pystencils` or `taichi` compilers for complex/non O(N) grid kernels. On GPU, tiling the grid and leveraging a custom `triton` kernel might be particularly efficient.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "gridpoints"
7
- version = "1.0.6"
7
+ version = "1.1.0"
8
8
  description = "fast multidimensional sorting tool for point clouds using the monotonic lagrangian grid "
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -26,8 +26,8 @@ demo = [
26
26
 
27
27
 
28
28
  [project.urls]
29
- Homepage = "https://github.com/Neighborhood-Grid/CubeNet"
30
- Repository = "https://github.com/Neighborhood-Grid/CubeNet"
29
+ Homepage = "https://neighborhood-grid.github.io/gridpoints/"
30
+ Repository = "https://github.com/Neighborhood-Grid/gridpoints"
31
31
 
32
32
  [tool.setuptools.packages.find]
33
33
  where = ["src"]
@@ -0,0 +1,10 @@
1
+
2
+ from .quick_demo import sample, show_result, print_zoom
3
+
4
+ __all__ = (
5
+ "sample",
6
+ "show_result",
7
+ "print_zoom",
8
+ )
9
+
10
+ __version__ = "1.0.8"
@@ -1,11 +1,13 @@
1
- """Monotonic Lagrangian sorting for NumPy, CuPy, and PyTorch arrays."""
2
-
3
- from .mlg_sort import argsort, sort, invert_permutation
4
-
5
- __all__ = (
6
- "argsort",
7
- "sort",
8
- "invert_permutation",
9
- )
10
-
11
- __version__ = "1.0.6"
1
+ """Monotonic Lagrangian sorting for NumPy, CuPy, and PyTorch arrays."""
2
+
3
+ from .mlg_sort import argsort, sort, invert_permutation
4
+ from .padding import pad
5
+
6
+ __all__ = (
7
+ "argsort",
8
+ "sort",
9
+ "invert_permutation",
10
+ "pad",
11
+ )
12
+
13
+ __version__ = "1.1.0"
@@ -0,0 +1,183 @@
1
+ """
2
+ Helpers:
3
+ Unified array syntax to support numpy, cupy and pytorch arrays
4
+
5
+ Here we just define basic primitives that we will need later
6
+ like arange, copy, shuffle etc.
7
+ """
8
+
9
+
10
+ def precision(N):
11
+ """Names of the (float, int) dtypes suited to point clouds of N points.
12
+
13
+ - float32 resolves the gaps (~1/N) between neighbouring points only up to
14
+ a few 10**4 points, float64 is used beyond.
15
+ - the diagonal sorts store values up to 6N: int32 is safe up to N ~ 3.5e8.
16
+ """
17
+ return ('float32' if N <= 2**16 else 'float64'), ('int32' if 6 * N < 2**31 else 'int64')
18
+
19
+
20
+ def get_dtype(dtp, xp):
21
+ if dtp == 'bool':
22
+ return getattr(xp, 'bool_', None) or xp.bool
23
+ return getattr(xp, dtp) if isinstance(dtp, str) else dtp
24
+
25
+
26
+ def get_backend(X):
27
+ module = type(X).__module__.split('.')[0]
28
+ if module == 'torch':
29
+ import torch
30
+ return torch, 'torch'
31
+ elif module == 'cupy':
32
+ import cupy
33
+ return cupy, 'cupy'
34
+ else:
35
+ import numpy
36
+ return numpy, 'numpy'
37
+
38
+ def astype(X, dtype_str, reference_array=None):
39
+ xp, backend_name = get_backend(X)
40
+ dtype = get_dtype(dtype_str, xp)
41
+ if backend_name == 'torch':
42
+ device = reference_array.device if reference_array is not None else X.device
43
+ return X.to(dtype=dtype, device=device)
44
+ return X.astype(dtype, copy=False)
45
+
46
+
47
+ def inplace_sort(X, axis):
48
+ xp, backend_name = get_backend(X)
49
+ if backend_name == 'torch':
50
+ X.copy_(X.sort(dim=axis)[0])
51
+ elif backend_name == "cupy":
52
+ X[...] = xp.sort(xp.ascontiguousarray(X), axis=axis)
53
+ else:
54
+ X.sort(axis=axis)
55
+
56
+
57
+ def sort(X, axis):
58
+ xp, backend_name = get_backend(X)
59
+ return X.sort(dim=axis)[0] if backend_name == 'torch' else xp.sort(X, axis=axis)
60
+
61
+
62
+ def _alloc(fn, shape, dtype_str, reference_array):
63
+ xp, backend_name = get_backend(reference_array)
64
+ dtype = get_dtype(dtype_str, xp)
65
+ if backend_name == 'torch':
66
+ return getattr(xp, fn)(shape, dtype=dtype, device=reference_array.device)
67
+ return getattr(xp, fn)(shape, dtype=dtype)
68
+
69
+ def empty(shape, dtype_str, reference_array):
70
+ return _alloc('empty', shape, dtype_str, reference_array)
71
+
72
+ def zeros(shape, dtype_str, reference_array):
73
+ return _alloc('zeros', shape, dtype_str, reference_array)
74
+
75
+ def ones(shape, dtype_str, reference_array):
76
+ return _alloc('ones', shape, dtype_str, reference_array)
77
+
78
+
79
+ def arange(N, dtype_str, reference_array):
80
+ xp, backend_name = get_backend(reference_array)
81
+ dtype = get_dtype(dtype_str, xp)
82
+ if backend_name == 'torch':
83
+ return xp.arange(N, dtype=dtype, device=reference_array.device)
84
+ return xp.arange(N, dtype=dtype)
85
+
86
+ def random(shape, dtype_str, reference_array):
87
+ """Uniform random numbers in [0, 1)."""
88
+ xp, backend_name = get_backend(reference_array)
89
+ dtype = get_dtype(dtype_str, xp)
90
+ if backend_name == 'torch':
91
+ return xp.rand(shape, dtype=dtype, device=reference_array.device)
92
+ elif backend_name == 'cupy':
93
+ return xp.random.rand(*shape).astype(dtype)
94
+ return xp.random.default_rng(42).random(shape, dtype=dtype)
95
+
96
+ def copy(arr):
97
+ _, backend_name = get_backend(arr)
98
+ if backend_name == 'torch':
99
+ return arr.clone()
100
+ return arr.copy()
101
+
102
+ def amax(X, axis):
103
+ return X.amax(axis) if get_backend(X)[1] == 'torch' else X.max(axis)
104
+
105
+ def amin(X, axis):
106
+ return X.amin(axis) if get_backend(X)[1] == 'torch' else X.min(axis)
107
+
108
+ def all_true(tensor):
109
+ return bool(tensor.all())
110
+
111
+ def any_true(tensor):
112
+ return bool(tensor.any())
113
+
114
+
115
+ def nan_infs_to_num_and_tiebreaker(arr):
116
+ """Make a batch of clouds (B, N, D) finite, and break ties with a tiny noise.
117
+
118
+ +inf / -inf are put just beyond the finite points of their cloud and axis
119
+ (they end up on the border of the grid), NaN are replaced by the value of a
120
+ random finite point of their cloud and axis (they end up anywhere).
121
+ """
122
+ xp, _ = get_backend(arr)
123
+ out = arr
124
+ finite = xp.isfinite(arr)
125
+ if not all_true(finite):
126
+ hi = amax(xp.where(finite, arr, -xp.inf), 1)[:, None, :]
127
+ lo = amin(xp.where(finite, arr, xp.inf), 1)[:, None, :]
128
+ out = xp.where(xp.isposinf(arr), hi + 1, arr)
129
+ out = xp.where(xp.isneginf(arr), lo - 1, out)
130
+ nan = xp.isnan(arr)
131
+ if any_true(nan):
132
+ finite_first = sort(xp.where(finite, arr, xp.inf), 1)
133
+ n_finite = finite.sum(1)[:, None, :]
134
+ pick = astype(random(arr.shape, arr.dtype, arr) * n_finite, 'int64')
135
+ out = xp.where(nan, take_along_axis(finite_first, pick, 1), out)
136
+ eps = 1e-5 if '32' in str(arr.dtype) else 1e-10
137
+ return out + eps * random(out.shape, arr.dtype, out)
138
+
139
+ def flip(X, axis=None):
140
+ xp, _ = get_backend(X)
141
+ if axis is None or axis == () or axis == []:
142
+ return X
143
+
144
+ if isinstance(axis, int):
145
+ axis = (axis,)
146
+ else:
147
+ axis = tuple(axis)
148
+
149
+ if hasattr(X, "flip") and callable(X.flip):
150
+ return X.flip(axis)
151
+
152
+ return xp.flip(X, axis=axis)
153
+
154
+ def argsort(X):
155
+ xp, backend_name = get_backend(X)
156
+ return xp.argsort(X, dim=-1) if backend_name == 'torch' else xp.argsort(X, axis=-1)
157
+
158
+ def argpartition(X, mid):
159
+ """Indices that put the `mid` smallest values of the last axis first."""
160
+ xp, backend_name = get_backend(X)
161
+ if backend_name == 'torch':
162
+ return xp.argsort(X, dim=-1)
163
+ else:
164
+ return xp.argpartition(X, mid - 1, axis=-1)
165
+
166
+ def take_along_axis(arr, idx, axis):
167
+ xp, backend_name = get_backend(arr)
168
+ if backend_name == 'torch':
169
+ return xp.take_along_dim(arr, idx.long(), dim=axis)
170
+ else:
171
+ return xp.take_along_axis(arr, idx, axis=axis)
172
+
173
+ def concatenate(arrays, axis = 0):
174
+ xp, name = get_backend(arrays[0])
175
+ if name == "torch":
176
+ return xp.cat(list(arrays), dim=axis)
177
+ return xp.concatenate(arrays, axis=axis)
178
+
179
+ def transpose(X, axes):
180
+ _, name = get_backend(X)
181
+ if name == "torch":
182
+ return X.permute(*axes)
183
+ return X.transpose(axes)
@@ -0,0 +1,106 @@
1
+ """
2
+ Helpers:
3
+ Diagonal trick is based on a tensor rotation that allow
4
+ to sort arrays efficiently along arbitrary diagonal fibers.
5
+
6
+ All functions work on a batch of grids X of shape (B, ...), and tell for each
7
+ grid of the batch whether it was already sorted.
8
+ """
9
+
10
+ import math
11
+
12
+ from .array_libs import (
13
+ get_backend,
14
+ arange,
15
+ inplace_sort,
16
+ )
17
+
18
+
19
+ def sorted_flags(X, axis):
20
+ """Bool array of shape (B,): is each grid of the batch X sorted along `axis`?"""
21
+ axis %= X.ndim
22
+ sl = lambda s: (slice(None),) * axis + (s,)
23
+ ok = X[sl(slice(1, None))] >= X[sl(slice(None, -1))]
24
+ return ok.reshape(len(X), math.prod(ok.shape[1:])).all(1)
25
+
26
+
27
+ def sort_diag_trick(X):
28
+ """
29
+ we rotate the tensor along the main diagonal
30
+ to place diagonals on the last axis, then
31
+ perform a standard sort(axis = -1), and
32
+ rotate back the diagonals
33
+ """
34
+ xp, _ = get_backend(X)
35
+ #bigM for safety, protect
36
+ #overlapping diagonals
37
+ bigM = (X.max() - X.min()) + 1
38
+
39
+ n, m = X.shape[-2:]
40
+
41
+ #move the smallest axis to last position
42
+ if m > n:
43
+ X = xp.moveaxis(X, [-1, -2], [-2, -1])
44
+ n, m = m, n #now n >= m
45
+
46
+ i = arange(n, X.dtype, X)[:, None]
47
+ j = arange(m, X.dtype, X)[None, :]
48
+
49
+ di = (i - j)
50
+ i_ = di % n
51
+ ires = di // n
52
+ groupkey = bigM * ires
53
+
54
+ X[..., i_, j] = X[..., i, j] - groupkey #Rotation
55
+ # ========================
56
+ # here is the trick: as we rotated the tensor
57
+ # sorting on the diagonal is just working on
58
+ # axis -1 of the rotated view
59
+ # ========================
60
+ was_sorted = sorted_flags(X, -1)
61
+ inplace_sort(X, axis=-1)
62
+ X[..., i, j] = X[..., i_, j] + groupkey
63
+
64
+ return was_sorted
65
+
66
+ def sort_tridiag_trick(X):
67
+ """
68
+ we rotate the tensor along the tridiagonal,
69
+ then perform a standard sort(axis = -1),
70
+ and rotate back the tridiagonals
71
+ """
72
+ xp, _ = get_backend(X)
73
+ #bigM for safety, protect
74
+ #overlapping diagonals
75
+ bigM = (X.max() - X.min()) + 1
76
+
77
+ #move the smallest axes to last positions
78
+ abc = X.shape
79
+ dst_axes = (-3, -2, -1)
80
+ src_axes = tuple(sorted(dst_axes, key=lambda ax: -abc[ax]))
81
+ X = xp.moveaxis(X, src_axes, dst_axes)
82
+ a, b, c = X.shape[-3:] # now a >= b >= c
83
+
84
+ i = arange(a, X.dtype, X)[:, None, None]
85
+ j = arange(b, X.dtype, X)[None, :, None]
86
+ k = arange(c, X.dtype, X)[None, None, :]
87
+
88
+ di = i - k
89
+ dj = j - k
90
+ di_ = di % a
91
+ dj_ = dj % b
92
+ dires = di // a
93
+ djres = dj // b
94
+ groupkey = bigM * (dires * 4 + djres)
95
+
96
+ X[..., di_, dj_, k] = X[..., i, j, k] - groupkey #Trirotation
97
+ # ========================
98
+ # Again, thanks to the rotation trick
99
+ # we just need to .sort(X, axis = -1)
100
+ # to sort the tridiagonal
101
+ # ========================
102
+ was_sorted = sorted_flags(X, -1)
103
+ inplace_sort(X, axis=-1)
104
+ X[..., i, j, k] = X[..., di_, dj_, k] + groupkey
105
+
106
+ return was_sorted