squarenet 0.2.1__tar.gz → 0.2.2__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.
- squarenet-0.2.2/PKG-INFO +99 -0
- squarenet-0.2.2/README.md +80 -0
- {squarenet-0.2.1 → squarenet-0.2.2}/pyproject.toml +11 -2
- squarenet-0.2.2/src/squarenet/__init__.py +4 -0
- squarenet-0.2.2/src/squarenet/boards.py +71 -0
- squarenet-0.2.2/src/squarenet/core.py +154 -0
- squarenet-0.2.2/src/squarenet/squarenet.py +230 -0
- squarenet-0.2.2/src/squarenet/travel.py +58 -0
- squarenet-0.2.2/src/squarenet/utils.py +142 -0
- squarenet-0.2.2/src/squarenet/views.py +71 -0
- squarenet-0.2.2/src/squarenet.egg-info/PKG-INFO +99 -0
- {squarenet-0.2.1 → squarenet-0.2.2}/src/squarenet.egg-info/SOURCES.txt +4 -1
- {squarenet-0.2.1 → squarenet-0.2.2}/src/squarenet.egg-info/requires.txt +3 -0
- squarenet-0.2.1/PKG-INFO +0 -79
- squarenet-0.2.1/README.md +0 -65
- squarenet-0.2.1/src/squarenet/__init__.py +0 -6
- squarenet-0.2.1/src/squarenet/checkerboard.py +0 -119
- squarenet-0.2.1/src/squarenet/core.py +0 -138
- squarenet-0.2.1/src/squarenet/utils.py +0 -118
- squarenet-0.2.1/src/squarenet.egg-info/PKG-INFO +0 -79
- {squarenet-0.2.1 → squarenet-0.2.2}/LICENSE.txt +0 -0
- {squarenet-0.2.1 → squarenet-0.2.2}/setup.cfg +0 -0
- {squarenet-0.2.1 → squarenet-0.2.2}/src/squarenet/data/__init__.py +0 -0
- {squarenet-0.2.1 → squarenet-0.2.2}/src/squarenet/data/france.wkb +0 -0
- {squarenet-0.2.1 → squarenet-0.2.2}/src/squarenet/data/germany.wkb +0 -0
- {squarenet-0.2.1 → squarenet-0.2.2}/src/squarenet.egg-info/dependency_links.txt +0 -0
- {squarenet-0.2.1 → squarenet-0.2.2}/src/squarenet.egg-info/top_level.txt +0 -0
squarenet-0.2.2/PKG-INFO
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: squarenet
|
|
3
|
+
Version: 0.2.2
|
|
4
|
+
Summary: Sparse local operations for point clouds in any dimension.
|
|
5
|
+
Author-email: ArmanddeCacqueray <armanddecacqueray@sfr.fr>
|
|
6
|
+
Project-URL: Homepage, https://github.com/ArmanddeCacqueray/SquareNet
|
|
7
|
+
Project-URL: Documentation, https://squarenet.readthedocs.io
|
|
8
|
+
Project-URL: PyPI, https://pypi.org/project/squarenet/
|
|
9
|
+
Requires-Python: >=3.8
|
|
10
|
+
Description-Content-Type: text/markdown
|
|
11
|
+
License-File: LICENSE.txt
|
|
12
|
+
Requires-Dist: numpy
|
|
13
|
+
Requires-Dist: matplotlib
|
|
14
|
+
Provides-Extra: demo
|
|
15
|
+
Requires-Dist: shapely; extra == "demo"
|
|
16
|
+
Provides-Extra: zsquare
|
|
17
|
+
Requires-Dist: tensorly; extra == "zsquare"
|
|
18
|
+
Dynamic: license-file
|
|
19
|
+
|
|
20
|
+
-----
|
|
21
|
+
[](https://colab.research.google.com/github/ArmanddeCacqueray/SquareNet/blob/main/exemples/00_getting_started.ipynb)
|
|
22
|
+
[](https://pypi.org/project/squarenet/)
|
|
23
|
+
[](https://squarenet.readthedocs.io/en/latest/)
|
|
24
|
+
<p align="center">
|
|
25
|
+
<img src="plots/logo.png" width="200" alt="Project visualization">
|
|
26
|
+
</p>
|
|
27
|
+
|
|
28
|
+
❒ SquareNet
|
|
29
|
+
|
|
30
|
+
SquareNet maps unstructured **point clouds** to structured grids through a **bijective transformation**. It replaces expensive spatial queries (k-NN, radius search) with super fast **sliding window** operations. Think of it as a powerful alternative to kd-trees, voxelization, rasterization and neighborhood graphs.
|
|
31
|
+
✔ Works in any dimension
|
|
32
|
+
✔ Handles non-convex geometries
|
|
33
|
+
✔ Scales to millions of points (fast processing)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
-----
|
|
37
|
+
|
|
38
|
+
## 🚀 Why SquareNet?
|
|
39
|
+
|
|
40
|
+
* **Speed:** $O(N)$ local operations via vectorized sliding windows.
|
|
41
|
+
* **Memory:** Contiguous memory access instead of irregular spatial lookups.
|
|
42
|
+
* **Simplicity:** Pure NumPy-based logic, no heavy spatial dependencies.
|
|
43
|
+
|
|
44
|
+
## 📦 Installation
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
pip install squarenet
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## 🧠 Quick Start
|
|
51
|
+
-> exemples/00_getting_started.ipynb
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
from squarenet import SquareNet
|
|
55
|
+
import numpy as np
|
|
56
|
+
|
|
57
|
+
# Initialize and Fit
|
|
58
|
+
N = 5*11*7*13
|
|
59
|
+
d = 4
|
|
60
|
+
points = np.random.rand(N, d)
|
|
61
|
+
|
|
62
|
+
IJKL = (5, 11, 7, 13)
|
|
63
|
+
sqnet = SquareNet(IJ_=IJKL) # Define grid dimensions, here 4D
|
|
64
|
+
sqnet.fit(points)
|
|
65
|
+
|
|
66
|
+
# Map any property of the points to the grid e.g. the norm, could be anything else
|
|
67
|
+
Xpts = np.linalg.norm(points, axis = 1) #(N, *C)
|
|
68
|
+
Xmap = sqnet.map(Xpts) #(5, 11, 7, 13, *C)
|
|
69
|
+
Xrec = sqnet.invert_map(Xmap) #(N, *C)
|
|
70
|
+
```
|
|
71
|
+
## Compute Local Views
|
|
72
|
+
```python
|
|
73
|
+
# views enhance Xpts with a view in a rectangular neighborhood
|
|
74
|
+
# (in the grid) with radius *wr and size *ws = 2wr+1
|
|
75
|
+
Xview = sqnet.views(Xpts, wr=5, invert_map = True) #(N, *C, *ws)
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## 🗺️ Visualizing the Mapping
|
|
79
|
+
|
|
80
|
+
You can use the built-in checkerboard to verify neighborhood preservation:
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
sqnet = SquareNet(IJ_=(400, 400))
|
|
84
|
+
sqnet.fit("france") #require !pip install shapely
|
|
85
|
+
sqnet.checkerboard()
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## 📈 Key Applications
|
|
89
|
+
|
|
90
|
+
* **Point Cloud Processing:** Fast local feature aggregation.
|
|
91
|
+
* **Kernel Methods:** Efficient sparse approximation of large kernels.
|
|
92
|
+
* **Deep Learning:** Pre-structuring irregular data for CNN/Transformer inputs.
|
|
93
|
+
|
|
94
|
+
<p align="center">
|
|
95
|
+
<img src="plots/packing.png" width="200" alt="Packing">
|
|
96
|
+
</p>
|
|
97
|
+
-----
|
|
98
|
+
|
|
99
|
+
**License:** MIT | **Author:** [ArmanddeCacqueray](mailto:armanddecacqueray@sfr.fr)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
-----
|
|
2
|
+
[](https://colab.research.google.com/github/ArmanddeCacqueray/SquareNet/blob/main/exemples/00_getting_started.ipynb)
|
|
3
|
+
[](https://pypi.org/project/squarenet/)
|
|
4
|
+
[](https://squarenet.readthedocs.io/en/latest/)
|
|
5
|
+
<p align="center">
|
|
6
|
+
<img src="plots/logo.png" width="200" alt="Project visualization">
|
|
7
|
+
</p>
|
|
8
|
+
|
|
9
|
+
❒ SquareNet
|
|
10
|
+
|
|
11
|
+
SquareNet maps unstructured **point clouds** to structured grids through a **bijective transformation**. It replaces expensive spatial queries (k-NN, radius search) with super fast **sliding window** operations. Think of it as a powerful alternative to kd-trees, voxelization, rasterization and neighborhood graphs.
|
|
12
|
+
✔ Works in any dimension
|
|
13
|
+
✔ Handles non-convex geometries
|
|
14
|
+
✔ Scales to millions of points (fast processing)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
-----
|
|
18
|
+
|
|
19
|
+
## 🚀 Why SquareNet?
|
|
20
|
+
|
|
21
|
+
* **Speed:** $O(N)$ local operations via vectorized sliding windows.
|
|
22
|
+
* **Memory:** Contiguous memory access instead of irregular spatial lookups.
|
|
23
|
+
* **Simplicity:** Pure NumPy-based logic, no heavy spatial dependencies.
|
|
24
|
+
|
|
25
|
+
## 📦 Installation
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install squarenet
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## 🧠 Quick Start
|
|
32
|
+
-> exemples/00_getting_started.ipynb
|
|
33
|
+
|
|
34
|
+
```python
|
|
35
|
+
from squarenet import SquareNet
|
|
36
|
+
import numpy as np
|
|
37
|
+
|
|
38
|
+
# Initialize and Fit
|
|
39
|
+
N = 5*11*7*13
|
|
40
|
+
d = 4
|
|
41
|
+
points = np.random.rand(N, d)
|
|
42
|
+
|
|
43
|
+
IJKL = (5, 11, 7, 13)
|
|
44
|
+
sqnet = SquareNet(IJ_=IJKL) # Define grid dimensions, here 4D
|
|
45
|
+
sqnet.fit(points)
|
|
46
|
+
|
|
47
|
+
# Map any property of the points to the grid e.g. the norm, could be anything else
|
|
48
|
+
Xpts = np.linalg.norm(points, axis = 1) #(N, *C)
|
|
49
|
+
Xmap = sqnet.map(Xpts) #(5, 11, 7, 13, *C)
|
|
50
|
+
Xrec = sqnet.invert_map(Xmap) #(N, *C)
|
|
51
|
+
```
|
|
52
|
+
## Compute Local Views
|
|
53
|
+
```python
|
|
54
|
+
# views enhance Xpts with a view in a rectangular neighborhood
|
|
55
|
+
# (in the grid) with radius *wr and size *ws = 2wr+1
|
|
56
|
+
Xview = sqnet.views(Xpts, wr=5, invert_map = True) #(N, *C, *ws)
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
## 🗺️ Visualizing the Mapping
|
|
60
|
+
|
|
61
|
+
You can use the built-in checkerboard to verify neighborhood preservation:
|
|
62
|
+
|
|
63
|
+
```python
|
|
64
|
+
sqnet = SquareNet(IJ_=(400, 400))
|
|
65
|
+
sqnet.fit("france") #require !pip install shapely
|
|
66
|
+
sqnet.checkerboard()
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## 📈 Key Applications
|
|
70
|
+
|
|
71
|
+
* **Point Cloud Processing:** Fast local feature aggregation.
|
|
72
|
+
* **Kernel Methods:** Efficient sparse approximation of large kernels.
|
|
73
|
+
* **Deep Learning:** Pre-structuring irregular data for CNN/Transformer inputs.
|
|
74
|
+
|
|
75
|
+
<p align="center">
|
|
76
|
+
<img src="plots/packing.png" width="200" alt="Packing">
|
|
77
|
+
</p>
|
|
78
|
+
-----
|
|
79
|
+
|
|
80
|
+
**License:** MIT | **Author:** [ArmanddeCacqueray](mailto:armanddecacqueray@sfr.fr)
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "squarenet"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.2"
|
|
8
8
|
description = "Sparse local operations for point clouds in any dimension."
|
|
9
9
|
authors = [{ name = "ArmanddeCacqueray", email = "armanddecacqueray@sfr.fr" }]
|
|
10
10
|
readme = "README.md"
|
|
@@ -14,13 +14,22 @@ dependencies = [
|
|
|
14
14
|
"matplotlib"
|
|
15
15
|
]
|
|
16
16
|
|
|
17
|
+
[project.urls]
|
|
18
|
+
Homepage = "https://github.com/ArmanddeCacqueray/SquareNet"
|
|
19
|
+
Documentation = "https://squarenet.readthedocs.io"
|
|
20
|
+
PyPI = "https://pypi.org/project/squarenet/"
|
|
21
|
+
|
|
17
22
|
[project.optional-dependencies]
|
|
18
23
|
demo = [
|
|
19
24
|
"shapely"
|
|
20
25
|
]
|
|
26
|
+
Zsquare = [
|
|
27
|
+
"tensorly"
|
|
28
|
+
]
|
|
29
|
+
|
|
21
30
|
|
|
22
31
|
[tool.setuptools.packages.find]
|
|
23
32
|
where = ["src"]
|
|
24
33
|
|
|
25
34
|
[tool.setuptools.package-data]
|
|
26
|
-
"squarenet" = ["data/*.wkb"]
|
|
35
|
+
"squarenet" = ["data/*.wkb"]
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import matplotlib.pyplot as plt
|
|
3
|
+
from mpl_toolkits.mplot3d.art3d import Poly3DCollection
|
|
4
|
+
|
|
5
|
+
def checkerboard(grid, scale):
|
|
6
|
+
"""Split a grid into two sets of blocks following a checkerboard pattern."""
|
|
7
|
+
ni, nj = grid.shape[:2]
|
|
8
|
+
d = grid.shape[-1]
|
|
9
|
+
hi, hj = ni // scale, nj // scale
|
|
10
|
+
|
|
11
|
+
# Crop to ensure clean divisibility
|
|
12
|
+
grid = grid[:hi * scale, :hj * scale]
|
|
13
|
+
|
|
14
|
+
# Reshape and transpose into a block structure
|
|
15
|
+
blocks = grid.reshape(scale, hi, scale, hj, d)
|
|
16
|
+
blocks = blocks.transpose(0, 2, 1, 3, 4)
|
|
17
|
+
|
|
18
|
+
# Flatten into blocks for easy masking
|
|
19
|
+
flat_blocks = blocks.reshape(scale, scale, hi * hj, d)
|
|
20
|
+
|
|
21
|
+
# Generate checkerboard mask
|
|
22
|
+
ii, jj = np.indices((scale, scale))
|
|
23
|
+
mask = (ii % 2) == (jj % 2)
|
|
24
|
+
|
|
25
|
+
return flat_blocks[mask], flat_blocks[~mask]
|
|
26
|
+
|
|
27
|
+
def checkerboard2D(grid, scale=[2, 4, 8, 16], ax=None, colors=("lightgrey", "blue"), s=1, alpha=0.7):
|
|
28
|
+
"""Visualize different checkerboard scales on a 2D/3D point grid."""
|
|
29
|
+
# 1. Handle multiple scales
|
|
30
|
+
if isinstance(scale, list):
|
|
31
|
+
ns = int(np.sqrt(len(scale)))
|
|
32
|
+
fig, axes = plt.subplots(ns, ns, figsize=(10, 10))
|
|
33
|
+
for a, sc in zip(axes.flat, scale):
|
|
34
|
+
checkerboard2D(grid, scale=sc, ax=a, colors=colors, s=s, alpha=alpha)
|
|
35
|
+
return axes
|
|
36
|
+
|
|
37
|
+
# 2. Base case: single scale
|
|
38
|
+
if ax is None:
|
|
39
|
+
fig, ax = plt.subplots(figsize=(6, 6))
|
|
40
|
+
|
|
41
|
+
# Use the checkerboard function to get the two sets of points
|
|
42
|
+
set1, set2 = checkerboard(grid, scale)
|
|
43
|
+
|
|
44
|
+
# Plot both sets (set1 are all 'blue' blocks, set2 are all 'lightgrey' blocks)
|
|
45
|
+
for points, color in zip([set1, set2], colors):
|
|
46
|
+
# points shape: (num_blocks, points_per_block, dims) -> flat for scatter
|
|
47
|
+
pts_flat = points.reshape(-1, grid.shape[-1])
|
|
48
|
+
ax.scatter(*(pts_flat[:, i] for i in range(pts_flat.shape[-1])),
|
|
49
|
+
c=color, s=s, alpha=alpha, edgecolors='none')
|
|
50
|
+
|
|
51
|
+
ax.set_title(f"Scale: {scale}")
|
|
52
|
+
ax.axis("off")
|
|
53
|
+
return ax
|
|
54
|
+
|
|
55
|
+
def checkerboard3D(grid_3d, scale=8, ax = None, colors=("lightgrey", "blue"), s=3, alpha=1):
|
|
56
|
+
"""Visualize different checkerboard scales on the surface of a 3D point grid."""
|
|
57
|
+
if ax is None:
|
|
58
|
+
fig = plt.figure(figsize=(10, 10))
|
|
59
|
+
ax = fig.add_subplot(111, projection='3d')
|
|
60
|
+
|
|
61
|
+
faces = [
|
|
62
|
+
grid_3d[-1, :, :], #grid_3d[0, :, :],
|
|
63
|
+
grid_3d[:, 0, :], #grid_3d[:, -1, :],
|
|
64
|
+
grid_3d[:, :, -1], #grid_3d[:, :, 0]
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
for face in faces:
|
|
68
|
+
checkerboard2D(face, scale=scale, ax=ax, colors = colors, s=s, alpha=alpha)
|
|
69
|
+
|
|
70
|
+
ax.set_box_aspect([1, 1, 1])
|
|
71
|
+
plt.show()
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
""""
|
|
4
|
+
=============================================
|
|
5
|
+
=============================================
|
|
6
|
+
PSEUDO-CODE: convert unstructured point cloud (N, D)
|
|
7
|
+
to a grid (N1, ..., ND, D) by iteratively sorting
|
|
8
|
+
the d-est heuristic along the d-est axis
|
|
9
|
+
|
|
10
|
+
We can see the grid as D iterators on the points
|
|
11
|
+
such that axis-d iterator maps the point
|
|
12
|
+
P(n ~ n1...nd...nD) to the "next" point
|
|
13
|
+
Pnext(n ~ n1...nd+1...nD). Let call it Pnext(n, d)
|
|
14
|
+
|
|
15
|
+
We can select D euclidian heuristics
|
|
16
|
+
H(d): (x, y, z,...) -> value which we want to
|
|
17
|
+
be increasing along the d-est axis of the grid
|
|
18
|
+
It turns out that H(0) = x, H(1) = y,.... is
|
|
19
|
+
already a pretty good heuristic.
|
|
20
|
+
|
|
21
|
+
So the goal is simply to ensure that all
|
|
22
|
+
heuristics are sorted along the grid, in the
|
|
23
|
+
sense that for all point P(n) and axis d,
|
|
24
|
+
H(d)(P(n)) <= H(d)(Pnext(n, d))
|
|
25
|
+
|
|
26
|
+
We can compute a grid disorder parameter which is
|
|
27
|
+
just the counts of all P, Pnext which breaks this
|
|
28
|
+
inequality
|
|
29
|
+
=============================================
|
|
30
|
+
Sort_increasing is then pretty simple:
|
|
31
|
+
For learning step in (1, Max_iter = 100)
|
|
32
|
+
For d in (1, D):
|
|
33
|
+
sort heuristic d along axis d.
|
|
34
|
+
Check disorder
|
|
35
|
+
If disorder = 0, we are done !
|
|
36
|
+
=============================================
|
|
37
|
+
=============================================
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
def sort_increasing(gridmap, heuristics, max_iter=100):
|
|
41
|
+
"""
|
|
42
|
+
Args:
|
|
43
|
+
-gridmap (np.array of ints):
|
|
44
|
+
an initial gridmap such that cloud_features[gridmap]
|
|
45
|
+
write any feature (N, *C) of the point-cloud (N, D)
|
|
46
|
+
on a grid (N1, ..., ND, *C)
|
|
47
|
+
|
|
48
|
+
-max_iter (int):
|
|
49
|
+
last step after which algorithm shall stop
|
|
50
|
+
even if it hasn't converged yet
|
|
51
|
+
Returns:
|
|
52
|
+
-gridmap
|
|
53
|
+
sorted gridmap such that cloud_features[gridmap]
|
|
54
|
+
now write the feature on a spatially coherent grid
|
|
55
|
+
-learningcurve (list of values):
|
|
56
|
+
track the performance of the optimisation process.
|
|
57
|
+
should converge to 0
|
|
58
|
+
"""
|
|
59
|
+
g = gridmap.copy()
|
|
60
|
+
learning_curve = []
|
|
61
|
+
|
|
62
|
+
#loop[0]: index to heuristic 0
|
|
63
|
+
#loop[d+1]: heuristic d to heuristic d+1
|
|
64
|
+
#back_to_id: heuristic D-1 to index
|
|
65
|
+
loop, back_to_id = loop_boost(heuristics)
|
|
66
|
+
|
|
67
|
+
for _ in range(max_iter):
|
|
68
|
+
# --- 1. Check for convergence ---
|
|
69
|
+
disorder = 0
|
|
70
|
+
for d, heuristic in enumerate(loop):
|
|
71
|
+
g = heuristic[g]
|
|
72
|
+
|
|
73
|
+
# Efficient disorder check: H(d)(P) > H(d)(Pnext)
|
|
74
|
+
diff = np.diff(g, axis=d)
|
|
75
|
+
disorder += np.sum(diff < 0)
|
|
76
|
+
|
|
77
|
+
g = back_to_id[g]
|
|
78
|
+
|
|
79
|
+
learning_curve.append(disorder)
|
|
80
|
+
if disorder == 0:
|
|
81
|
+
break
|
|
82
|
+
|
|
83
|
+
# --- 2. Sorting Phase ---
|
|
84
|
+
for d, heuristic in enumerate(loop):
|
|
85
|
+
g = heuristic[g]
|
|
86
|
+
g.sort(axis = d)
|
|
87
|
+
|
|
88
|
+
g = back_to_id[g]
|
|
89
|
+
|
|
90
|
+
# last cleanup:
|
|
91
|
+
gridmap = np.ascontiguousarray(g)
|
|
92
|
+
return gridmap, learning_curve
|
|
93
|
+
|
|
94
|
+
# ============================================
|
|
95
|
+
# ============================================
|
|
96
|
+
# Heuristics
|
|
97
|
+
# ============================================
|
|
98
|
+
# ============================================
|
|
99
|
+
def carthesian_heuristics(points):
|
|
100
|
+
return points
|
|
101
|
+
|
|
102
|
+
# ============================================
|
|
103
|
+
# ============================================
|
|
104
|
+
# Boosters
|
|
105
|
+
# ============================================
|
|
106
|
+
# ============================================
|
|
107
|
+
def integer_boost(heuristics):
|
|
108
|
+
"""
|
|
109
|
+
Booster: Convert heuristics to integer
|
|
110
|
+
to boost sort_increasing function
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
- heuristics (np.ndarray) (N,D): heuristics computed on the point cloud
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
- h_int (list of np.uint32 arrays): heuristics as integers
|
|
117
|
+
"""
|
|
118
|
+
N, D = heuristics.shape
|
|
119
|
+
h_int = []
|
|
120
|
+
for d in range(D):
|
|
121
|
+
order = np.argsort(heuristics[:, d])
|
|
122
|
+
ranks = np.empty(N, dtype=np.int32)
|
|
123
|
+
ranks[order] = np.arange(N)
|
|
124
|
+
h_int.append(ranks)
|
|
125
|
+
return h_int
|
|
126
|
+
|
|
127
|
+
def loop_boost(heuristics):
|
|
128
|
+
"""
|
|
129
|
+
Booster: make looping over heuristics a bit faster
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
- heuristics (np.ndarray) (N,D): heuristics computed on the point cloud
|
|
133
|
+
Return:
|
|
134
|
+
- loop (...): list of permutations such that
|
|
135
|
+
loop[d](h_int[d][n]) = h_int[d+1][n]
|
|
136
|
+
- back_to_id (...): permutation such that
|
|
137
|
+
back_to_id(h_int[-1][n]) = n
|
|
138
|
+
"""
|
|
139
|
+
int_boost = integer_boost(heuristics)
|
|
140
|
+
N = len(int_boost[0])
|
|
141
|
+
identity = np.arange(N, dtype=np.int32)
|
|
142
|
+
#start the loop
|
|
143
|
+
h_int = [identity] + int_boost
|
|
144
|
+
#close the loop
|
|
145
|
+
h_int_plus = int_boost + [identity]
|
|
146
|
+
loop = []
|
|
147
|
+
|
|
148
|
+
for h, hplus in zip(h_int, h_int_plus):
|
|
149
|
+
sigma = np.zeros(N, dtype=np.int32)
|
|
150
|
+
sigma[h] = hplus
|
|
151
|
+
loop.append(np.ascontiguousarray(sigma))
|
|
152
|
+
|
|
153
|
+
loop, back_to_id = loop[:-1], loop[-1]
|
|
154
|
+
return loop, back_to_id
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from .core import sort_increasing, carthesian_heuristics
|
|
3
|
+
from .boards import checkerboard, checkerboard2D, checkerboard3D
|
|
4
|
+
from .views import localview, lazylocalview
|
|
5
|
+
from .utils import initpoint, dualgrid
|
|
6
|
+
from warnings import warn
|
|
7
|
+
|
|
8
|
+
class SquareNet:
|
|
9
|
+
"""
|
|
10
|
+
An iterative grid-straightening algorithm that untangles a D-dimensional mesh
|
|
11
|
+
by sorting nodes along each spatial axis to enforce a structured ordering.
|
|
12
|
+
|
|
13
|
+
The grid is represented as an (I, J, ..., D) array, where D is the number of spatial dimensions.
|
|
14
|
+
Each iteration attempts to minimize disorder along each dimension independently.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, IJ_=(100, 100), max_iter=100, warnings_=True):
|
|
18
|
+
"""
|
|
19
|
+
Initialize the SquareNet.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
IJ_ (tuple): Grid dimensions (Rows, Cols, ...). Supports any number of dimensions.
|
|
23
|
+
max_iter (int): Maximum number of straightening iterations.
|
|
24
|
+
warnings_ (bool): Flag to shut up all warnings if asked
|
|
25
|
+
"""
|
|
26
|
+
self.IJ_ = tuple(IJ_)
|
|
27
|
+
self.D = len(IJ_) # Spatial dimensions (x, y, z,...)
|
|
28
|
+
self.N = np.prod(IJ_)
|
|
29
|
+
self.max_iter = max_iter
|
|
30
|
+
self.learning_curve = []
|
|
31
|
+
self.warnings_ = warnings_
|
|
32
|
+
|
|
33
|
+
# Internal state
|
|
34
|
+
self.points = None # Points as given by the user
|
|
35
|
+
self.heuristics = None # Heuristics is computed on points
|
|
36
|
+
self.grid = np.arange(
|
|
37
|
+
self.N, dtype = np.int32
|
|
38
|
+
).reshape(IJ_) #grid to sort heuritics
|
|
39
|
+
self.invert_grid = dualgrid(self.grid) #for invertibility
|
|
40
|
+
self.packed = False #Flag for faster computations if possible
|
|
41
|
+
|
|
42
|
+
def fit(self, points):
|
|
43
|
+
"""
|
|
44
|
+
Fit the grid to a set of points in D dimensions.
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
points (np.ndarray or str): Array of shape (N, D) or a method name supported
|
|
48
|
+
by .utils.initpoint.
|
|
49
|
+
"""
|
|
50
|
+
if isinstance(points, str):
|
|
51
|
+
points = initpoint(method=points, size=(self.N, self.D))
|
|
52
|
+
|
|
53
|
+
N, D = points.shape
|
|
54
|
+
assert N == self.N, f"Input points ({N}) must match grid size {self.N}"
|
|
55
|
+
assert D == self.D, f"Input points dimension ({D}) must match D={self.D}"
|
|
56
|
+
|
|
57
|
+
self.points = points
|
|
58
|
+
self.heuristics = carthesian_heuristics(points)
|
|
59
|
+
|
|
60
|
+
grid, learning_curve = sort_increasing(
|
|
61
|
+
self.grid, self.heuristics, self.max_iter
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
last_iter = len(learning_curve) -1
|
|
65
|
+
last_error = learning_curve[-1]
|
|
66
|
+
|
|
67
|
+
if last_iter == 0:
|
|
68
|
+
#just tacking advantage of situation
|
|
69
|
+
#to boost map and invertmap function
|
|
70
|
+
self.autopack()
|
|
71
|
+
|
|
72
|
+
elif last_error == 0:
|
|
73
|
+
print(f"succesfully sorted at iteration {last_iter}")
|
|
74
|
+
|
|
75
|
+
else:
|
|
76
|
+
if self.warnings_:
|
|
77
|
+
warn(
|
|
78
|
+
"Disorder didn't converge to 0. "
|
|
79
|
+
"Check the learning curve and consider increasing the max_iter parameter.",
|
|
80
|
+
ConvergenceWarning,
|
|
81
|
+
stacklevel=2
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
# Save results
|
|
85
|
+
self.grid = grid
|
|
86
|
+
self.invert_grid = dualgrid(grid)
|
|
87
|
+
self.learning_curve = learning_curve
|
|
88
|
+
|
|
89
|
+
def map(self, features):
|
|
90
|
+
"""
|
|
91
|
+
Gather: cloud data (N, *C) -> grid data (N1, ..., ND, *C)
|
|
92
|
+
"""
|
|
93
|
+
if not self.packed:
|
|
94
|
+
return features[self.grid]
|
|
95
|
+
C = features.shape[1:]
|
|
96
|
+
return features.reshape(*self.IJ_, *C)
|
|
97
|
+
|
|
98
|
+
def invert_map(self, features):
|
|
99
|
+
"""
|
|
100
|
+
Restores: grid data (N1, ..., ND, *C) -> cloud data (N, *C)
|
|
101
|
+
"""
|
|
102
|
+
if not self.packed:
|
|
103
|
+
return features.reshape(
|
|
104
|
+
-1, *features.shape[self.D:]
|
|
105
|
+
)[self.invert_grid.flatten()]
|
|
106
|
+
|
|
107
|
+
C = features.shape[self.D:]
|
|
108
|
+
return features.reshape(-1, *C)
|
|
109
|
+
|
|
110
|
+
def mapidx(self, index):
|
|
111
|
+
"""
|
|
112
|
+
Convert: ONE cloud index -> ONE grid index
|
|
113
|
+
"""
|
|
114
|
+
return self.invert_grid[index]
|
|
115
|
+
|
|
116
|
+
def invert_mapidx(self, index):
|
|
117
|
+
"""
|
|
118
|
+
Convert: ONE grid index-> ONE cloud index
|
|
119
|
+
"""
|
|
120
|
+
return self.grid[tuple(index)]
|
|
121
|
+
|
|
122
|
+
def checkerboard(self, scale = [2, 4, 8, 16], toplot = True, **kwargs):
|
|
123
|
+
"""
|
|
124
|
+
alias for boards.checkerboard:
|
|
125
|
+
|
|
126
|
+
return the net as a checkerboard at required scale = number of cells per axe
|
|
127
|
+
make a nice plot if self.D = 2 or 3
|
|
128
|
+
|
|
129
|
+
**kwargs are extra visualization arguments (color, point size...)
|
|
130
|
+
"""
|
|
131
|
+
gpoints = np.ascontiguousarray(self.map(self.points))
|
|
132
|
+
#nice plot if possible
|
|
133
|
+
if toplot and (self.D == 2):
|
|
134
|
+
checkerboard2D(gpoints, scale = scale, **kwargs)
|
|
135
|
+
elif toplot and (self.D == 3):
|
|
136
|
+
checkerboard3D(gpoints, scale = 8, **kwargs)
|
|
137
|
+
else:
|
|
138
|
+
return checkerboard(gpoints, scale)
|
|
139
|
+
|
|
140
|
+
def views(self, X, wr, map=True, invert_map=False, select_lazy=None, **kwargs):
|
|
141
|
+
"""
|
|
142
|
+
Alias for views.localview
|
|
143
|
+
|
|
144
|
+
Compute local neighborhood views of the input array `X`
|
|
145
|
+
using rectangular windows of radius `wr`. The window size
|
|
146
|
+
is given by ``ws = 2 * wr + 1`` (per dimension).
|
|
147
|
+
|
|
148
|
+
Parameters
|
|
149
|
+
----------
|
|
150
|
+
X : np.ndarray
|
|
151
|
+
Input data of shape (N, *C) or (*G, *C).
|
|
152
|
+
wr : int or tuple (per dimension)
|
|
153
|
+
Radius of the window.
|
|
154
|
+
map : bool, default True
|
|
155
|
+
If True, `X` is assumed to be in (N, *C) space.
|
|
156
|
+
Otherwise, it is assumed to be in (*G, *C) space.
|
|
157
|
+
invert_map : bool, default False
|
|
158
|
+
If True, the output will stay in (*G, *C) space
|
|
159
|
+
Otherwise, it will be converted back to (N, *C)
|
|
160
|
+
select_lazy : None or IndexLike, default None
|
|
161
|
+
Subset of indices where the view is computed.
|
|
162
|
+
Must be grid indexes
|
|
163
|
+
**kwargs :
|
|
164
|
+
Additional keyword arguments passed to `np.pad`
|
|
165
|
+
(e.g., boundary conditions). See NumPy documentation.
|
|
166
|
+
|
|
167
|
+
Returns
|
|
168
|
+
-------
|
|
169
|
+
Xview : np.ndarray or WindowCollector
|
|
170
|
+
- If dense output:
|
|
171
|
+
Array of shape (N, *C, *ws) or (*G, *C, *ws).
|
|
172
|
+
- If `select_lazy` is used:
|
|
173
|
+
A mapping (e.g. dict-like) from selected indices
|
|
174
|
+
to local views of shape (*G[sel], *C, *ws).
|
|
175
|
+
"""
|
|
176
|
+
lazy = (select_lazy is not None)
|
|
177
|
+
|
|
178
|
+
Xmap = self.map(X) if map else X
|
|
179
|
+
Xmap = np.ascontiguousarray(Xmap)
|
|
180
|
+
|
|
181
|
+
args = (Xmap, wr, self.D)
|
|
182
|
+
|
|
183
|
+
Xview = (
|
|
184
|
+
localview(*args, **kwargs)
|
|
185
|
+
if not lazy
|
|
186
|
+
else lazylocalview(select_lazy, *args, **kwargs)
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
if invert_map:
|
|
190
|
+
if lazy:
|
|
191
|
+
Xview = {
|
|
192
|
+
self.invert_mapidx(key): value
|
|
193
|
+
for key, value in Xview.items()
|
|
194
|
+
}
|
|
195
|
+
else:
|
|
196
|
+
if self.warnings_:
|
|
197
|
+
warn(
|
|
198
|
+
"Using invert_map may be slow and memory-intensive. "
|
|
199
|
+
"Consider working with gridded data or enabling select_lazy "
|
|
200
|
+
"if you only need a subset of the views.",
|
|
201
|
+
PerformanceWarning,
|
|
202
|
+
stacklevel=2
|
|
203
|
+
)
|
|
204
|
+
Xview = self.invert_map(Xview)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
return Xview
|
|
208
|
+
|
|
209
|
+
def autopack(self):
|
|
210
|
+
print(
|
|
211
|
+
"Successfully sorted at iteration 0...\n"
|
|
212
|
+
"...Which means data are allready packed\n"
|
|
213
|
+
"Performing an autopack to boost performance."
|
|
214
|
+
)
|
|
215
|
+
self.packed = True
|
|
216
|
+
|
|
217
|
+
if self.warnings_:
|
|
218
|
+
if not self.points.flags.c_contiguous:
|
|
219
|
+
warn(
|
|
220
|
+
"Consider calling np.ascontiguousarray on your input arrays "
|
|
221
|
+
"(e.g., points and covariates) to improve performance.",
|
|
222
|
+
PerformanceWarning,
|
|
223
|
+
stacklevel=2
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
class ConvergenceWarning(UserWarning):
|
|
227
|
+
pass
|
|
228
|
+
|
|
229
|
+
class PerformanceWarning(UserWarning):
|
|
230
|
+
pass
|