squarenet 0.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.
- squarenet-0.1.0/LICENSE.txt +19 -0
- squarenet-0.1.0/PKG-INFO +149 -0
- squarenet-0.1.0/README.md +134 -0
- squarenet-0.1.0/pyproject.toml +27 -0
- squarenet-0.1.0/setup.cfg +4 -0
- squarenet-0.1.0/src/squarenet/__init__.py +5 -0
- squarenet-0.1.0/src/squarenet/core.py +133 -0
- squarenet-0.1.0/src/squarenet/utils.py +148 -0
- squarenet-0.1.0/src/squarenet.egg-info/PKG-INFO +149 -0
- squarenet-0.1.0/src/squarenet.egg-info/SOURCES.txt +11 -0
- squarenet-0.1.0/src/squarenet.egg-info/dependency_links.txt +1 -0
- squarenet-0.1.0/src/squarenet.egg-info/requires.txt +6 -0
- squarenet-0.1.0/src/squarenet.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
Copyright (c) 2018 The Python Packaging Authority
|
|
2
|
+
|
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
4
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
5
|
+
in the Software without restriction, including without limitation the rights
|
|
6
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
7
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
8
|
+
furnished to do so, subject to the following conditions:
|
|
9
|
+
|
|
10
|
+
The above copyright notice and this permission notice shall be included in all
|
|
11
|
+
copies or substantial portions of the Software.
|
|
12
|
+
|
|
13
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
14
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
15
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
16
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
17
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
18
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
19
|
+
SOFTWARE.
|
squarenet-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: squarenet
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Sparse local operations for point clouds in any dimension.
|
|
5
|
+
Author-email: ArmanddeCacqueray <armanddecacqueray@sfr.fr>
|
|
6
|
+
Requires-Python: >=3.8
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE.txt
|
|
9
|
+
Requires-Dist: numpy
|
|
10
|
+
Requires-Dist: matplotlib
|
|
11
|
+
Provides-Extra: demo
|
|
12
|
+
Requires-Dist: geopandas; extra == "demo"
|
|
13
|
+
Requires-Dist: shapely; extra == "demo"
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
|
|
16
|
+
# SquareNet
|
|
17
|
+
|
|
18
|
+
**SquareNet** is a lightweight framework for mapping unstructured point clouds into structured N-dimensional grids, enabling fast and efficient local operations.
|
|
19
|
+
|
|
20
|
+
## β¨ Key Idea
|
|
21
|
+
|
|
22
|
+
SquareNet builds a bijective mapping between point indices and a structured grid:
|
|
23
|
+
|
|
24
|
+
* **Bijection**: each point maps to exactly one grid cell
|
|
25
|
+
* **Neighborhood preservation**: nearby points remain close in the grid
|
|
26
|
+
|
|
27
|
+
This allows replacing expensive spatial queries (k-NN, radius search) with simple **array slicing**.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## π Features
|
|
32
|
+
|
|
33
|
+
* Fast point cloud β grid mapping
|
|
34
|
+
* Invertible transformation
|
|
35
|
+
* Efficient local neighborhood operations
|
|
36
|
+
* Sparse Gram matrix computation via sliding windows
|
|
37
|
+
* NumPy-friendly (no heavy dependencies required)
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## π¦ Installation
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install squarenet
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## π§ Basic Usage
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from squarenet import SquareNet
|
|
53
|
+
|
|
54
|
+
sqnet = SquareNet(IJ=(L, W, H, ...), max_iter = 100)
|
|
55
|
+
|
|
56
|
+
# Original data: (N, D)
|
|
57
|
+
points = np.random.rand(N, D)
|
|
58
|
+
Sqnet.fit(points)
|
|
59
|
+
|
|
60
|
+
# Map (N, *) to the grid: (L, W, H, ..., *)
|
|
61
|
+
sqX = sqnet.map(X)
|
|
62
|
+
|
|
63
|
+
# Back to original ordering
|
|
64
|
+
X_rec = sqnet.invert_map(sqX)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## π¬ Local Processing Example
|
|
70
|
+
|
|
71
|
+
Compute a **local (sparse) Gram matrix** using a sliding window:
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
G = sqnet.gram(X, ws=(5, 5))
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Instead of computing a full `(N Γ N)` matrix, SquareNet only computes interactions within local neighborhoods.
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## πΊοΈ Demo Dataset
|
|
82
|
+
|
|
83
|
+
A small demo dataset (France map) is included:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
Sqnet.fit("france")
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## π§± Project Structure
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
src/
|
|
95
|
+
squarenet/
|
|
96
|
+
__init__.py
|
|
97
|
+
core.py
|
|
98
|
+
utils.py
|
|
99
|
+
data/france.geojson
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## π― Why SquareNet?
|
|
105
|
+
|
|
106
|
+
Traditional point cloud pipelines rely on:
|
|
107
|
+
|
|
108
|
+
* k-NN search (O(N log N))
|
|
109
|
+
* irregular memory access
|
|
110
|
+
* poor GPU utilization
|
|
111
|
+
|
|
112
|
+
SquareNet enables:
|
|
113
|
+
|
|
114
|
+
* **O(N)** local operations
|
|
115
|
+
* contiguous memory access
|
|
116
|
+
* vectorized operations on a sliding window
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## π Use Cases
|
|
121
|
+
|
|
122
|
+
* Point cloud processing
|
|
123
|
+
* Graph-to-grid transformations
|
|
124
|
+
* Fast kernel methods
|
|
125
|
+
* Local feature aggregation
|
|
126
|
+
* Deep learning preprocessing
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## π οΈ Development
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
git clone https://github.com/ArmanddeCacqueray/SquareNet
|
|
134
|
+
cd squarenet
|
|
135
|
+
pip install -e .
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## π License
|
|
141
|
+
|
|
142
|
+
MIT License
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## π€ Contributing
|
|
147
|
+
|
|
148
|
+
Contributions are welcome. Please open an issue or submit a pull request.
|
|
149
|
+
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# SquareNet
|
|
2
|
+
|
|
3
|
+
**SquareNet** is a lightweight framework for mapping unstructured point clouds into structured N-dimensional grids, enabling fast and efficient local operations.
|
|
4
|
+
|
|
5
|
+
## β¨ Key Idea
|
|
6
|
+
|
|
7
|
+
SquareNet builds a bijective mapping between point indices and a structured grid:
|
|
8
|
+
|
|
9
|
+
* **Bijection**: each point maps to exactly one grid cell
|
|
10
|
+
* **Neighborhood preservation**: nearby points remain close in the grid
|
|
11
|
+
|
|
12
|
+
This allows replacing expensive spatial queries (k-NN, radius search) with simple **array slicing**.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## π Features
|
|
17
|
+
|
|
18
|
+
* Fast point cloud β grid mapping
|
|
19
|
+
* Invertible transformation
|
|
20
|
+
* Efficient local neighborhood operations
|
|
21
|
+
* Sparse Gram matrix computation via sliding windows
|
|
22
|
+
* NumPy-friendly (no heavy dependencies required)
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## π¦ Installation
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install squarenet
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## π§ Basic Usage
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
from squarenet import SquareNet
|
|
38
|
+
|
|
39
|
+
sqnet = SquareNet(IJ=(L, W, H, ...), max_iter = 100)
|
|
40
|
+
|
|
41
|
+
# Original data: (N, D)
|
|
42
|
+
points = np.random.rand(N, D)
|
|
43
|
+
Sqnet.fit(points)
|
|
44
|
+
|
|
45
|
+
# Map (N, *) to the grid: (L, W, H, ..., *)
|
|
46
|
+
sqX = sqnet.map(X)
|
|
47
|
+
|
|
48
|
+
# Back to original ordering
|
|
49
|
+
X_rec = sqnet.invert_map(sqX)
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## π¬ Local Processing Example
|
|
55
|
+
|
|
56
|
+
Compute a **local (sparse) Gram matrix** using a sliding window:
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
G = sqnet.gram(X, ws=(5, 5))
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Instead of computing a full `(N Γ N)` matrix, SquareNet only computes interactions within local neighborhoods.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## πΊοΈ Demo Dataset
|
|
67
|
+
|
|
68
|
+
A small demo dataset (France map) is included:
|
|
69
|
+
|
|
70
|
+
```python
|
|
71
|
+
Sqnet.fit("france")
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## π§± Project Structure
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
src/
|
|
80
|
+
squarenet/
|
|
81
|
+
__init__.py
|
|
82
|
+
core.py
|
|
83
|
+
utils.py
|
|
84
|
+
data/france.geojson
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## π― Why SquareNet?
|
|
90
|
+
|
|
91
|
+
Traditional point cloud pipelines rely on:
|
|
92
|
+
|
|
93
|
+
* k-NN search (O(N log N))
|
|
94
|
+
* irregular memory access
|
|
95
|
+
* poor GPU utilization
|
|
96
|
+
|
|
97
|
+
SquareNet enables:
|
|
98
|
+
|
|
99
|
+
* **O(N)** local operations
|
|
100
|
+
* contiguous memory access
|
|
101
|
+
* vectorized operations on a sliding window
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## π Use Cases
|
|
106
|
+
|
|
107
|
+
* Point cloud processing
|
|
108
|
+
* Graph-to-grid transformations
|
|
109
|
+
* Fast kernel methods
|
|
110
|
+
* Local feature aggregation
|
|
111
|
+
* Deep learning preprocessing
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## π οΈ Development
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
git clone https://github.com/ArmanddeCacqueray/SquareNet
|
|
119
|
+
cd squarenet
|
|
120
|
+
pip install -e .
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## π License
|
|
126
|
+
|
|
127
|
+
MIT License
|
|
128
|
+
|
|
129
|
+
---
|
|
130
|
+
|
|
131
|
+
## π€ Contributing
|
|
132
|
+
|
|
133
|
+
Contributions are welcome. Please open an issue or submit a pull request.
|
|
134
|
+
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "squarenet"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Sparse local operations for point clouds in any dimension."
|
|
9
|
+
authors = [{ name = "ArmanddeCacqueray", email = "armanddecacqueray@sfr.fr" }]
|
|
10
|
+
readme = "README.md"
|
|
11
|
+
requires-python = ">=3.8"
|
|
12
|
+
dependencies = [
|
|
13
|
+
"numpy",
|
|
14
|
+
"matplotlib"
|
|
15
|
+
]
|
|
16
|
+
|
|
17
|
+
[project.optional-dependencies]
|
|
18
|
+
demo = [
|
|
19
|
+
"geopandas",
|
|
20
|
+
"shapely"
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[tool.setuptools.packages.find]
|
|
24
|
+
where = ["src"]
|
|
25
|
+
|
|
26
|
+
[tool.setuptools.package-data]
|
|
27
|
+
"squarenet" = ["data/*.json"]
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
from .utils import initpoint, grid_disorder, checkerboard, gram
|
|
3
|
+
|
|
4
|
+
class SquareNet:
|
|
5
|
+
"""
|
|
6
|
+
An iterative grid-straightening algorithm that untangles a D-dimensional mesh
|
|
7
|
+
by sorting nodes along each spatial axis to enforce a structured ordering.
|
|
8
|
+
|
|
9
|
+
The grid is represented as an (I, J, ..., D) array, where D is the number of spatial dimensions.
|
|
10
|
+
Each iteration attempts to minimize disorder along each dimension independently.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, IJ=(100, 100), max_iter=100):
|
|
14
|
+
"""
|
|
15
|
+
Initialize the SquareNet.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
IJ (tuple): Grid dimensions (Rows, Cols, ...). Supports any number of dimensions.
|
|
19
|
+
max_iter (int): Maximum number of straightening iterations.
|
|
20
|
+
"""
|
|
21
|
+
self.IJ = tuple(IJ)
|
|
22
|
+
self.D = len(self.IJ) # Spatial dimensions (x, y, z,...)
|
|
23
|
+
self.N = np.prod(IJ)
|
|
24
|
+
self.max_iter = max_iter
|
|
25
|
+
self.learning_curve = []
|
|
26
|
+
|
|
27
|
+
# Internal state
|
|
28
|
+
self.points = None # Points as given by the user
|
|
29
|
+
self.net = None # Grid with attached original indices
|
|
30
|
+
self.mapID = None # Permutation map after sorting
|
|
31
|
+
|
|
32
|
+
def map(self, X):
|
|
33
|
+
"""
|
|
34
|
+
Reorders any input array according to the optimized grid structure.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
X (np.ndarray): Array of shape (N, ...) to be remapped.
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
np.ndarray: Reshaped and reordered array of shape (*IJ, ...).
|
|
41
|
+
"""
|
|
42
|
+
return X[self.mapID].reshape(*self.IJ, *X.shape[1:])
|
|
43
|
+
|
|
44
|
+
def invert_map(self, X):
|
|
45
|
+
"""
|
|
46
|
+
Restores the original ordering from the grid representation.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
sqX (np.ndarray): Array of shape (*IJ, ...)
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
np.ndarray: Array of shape (N, ...)
|
|
53
|
+
"""
|
|
54
|
+
return (X.reshape(-1, *X.shape[len(self.IJ):]))[self.inv_mapID]
|
|
55
|
+
|
|
56
|
+
def fit(self, points):
|
|
57
|
+
"""
|
|
58
|
+
Fit the grid to a set of points in D dimensions.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
points (np.ndarray or str): Array of shape (N, D) or a method name supported
|
|
62
|
+
by .utils.initpoint.
|
|
63
|
+
"""
|
|
64
|
+
if isinstance(points, str):
|
|
65
|
+
points = initpoint(method=points, size=(self.N, self.D))
|
|
66
|
+
|
|
67
|
+
self.points = points
|
|
68
|
+
self.learning_curve = []
|
|
69
|
+
|
|
70
|
+
N, D = points.shape
|
|
71
|
+
assert N == self.N, f"Input points ({N}) must match grid size {self.N}"
|
|
72
|
+
assert D == self.D, f"Input points dimension ({D}) must match D={self.D}"
|
|
73
|
+
|
|
74
|
+
# Attach an ID to each point for tracking during swaps
|
|
75
|
+
indexed_points = np.concatenate([points, np.arange(N)[:, None]], axis=-1)
|
|
76
|
+
|
|
77
|
+
# Reshape into structured grid: (*IJ, D+1)
|
|
78
|
+
self.net = indexed_points.reshape(*self.IJ, self.D + 1)
|
|
79
|
+
|
|
80
|
+
# Iteratively sort along each spatial axis
|
|
81
|
+
for k in range(self.max_iter):
|
|
82
|
+
error = self.sort_increasing()
|
|
83
|
+
if error ==0:
|
|
84
|
+
print(f"succesfully sorted at step {k}")
|
|
85
|
+
break
|
|
86
|
+
|
|
87
|
+
# Extract permutation map and clean the coordinate grid
|
|
88
|
+
self.mapID = self.net[..., -1].astype(int).ravel()
|
|
89
|
+
self.inv_mapID = np.empty_like(self.mapID)
|
|
90
|
+
self.inv_mapID[self.mapID] = np.arange(len(self.mapID))
|
|
91
|
+
self.net = self.net[..., :-1]
|
|
92
|
+
|
|
93
|
+
def sort_increasing(self):
|
|
94
|
+
"""
|
|
95
|
+
Aligns the grid by sorting nodes along each spatial axis.
|
|
96
|
+
Ensures that the points are generally ordered along all dimensions.
|
|
97
|
+
"""
|
|
98
|
+
for axis in range(self.D):
|
|
99
|
+
# np.argsort along the current spatial axis
|
|
100
|
+
# Keep the last dimension (original index) attached
|
|
101
|
+
flat_axis = axis
|
|
102
|
+
idx = np.argsort(self.net[..., flat_axis], axis=axis)
|
|
103
|
+
# Expand idx to match last dimension for np.take_along_axis
|
|
104
|
+
idx_expanded = np.expand_dims(idx, axis=-1)
|
|
105
|
+
self.net = np.take_along_axis(self.net, idx_expanded.repeat(self.D + 1, axis=-1), axis=axis)
|
|
106
|
+
|
|
107
|
+
error = grid_disorder(self.net[..., :-1])
|
|
108
|
+
self.learning_curve.append(error)
|
|
109
|
+
return(error)
|
|
110
|
+
|
|
111
|
+
def checkerboard(self, scales= [2, 4, 8, 16]):
|
|
112
|
+
"""
|
|
113
|
+
plot the net as a chekerboard at different scales
|
|
114
|
+
"""
|
|
115
|
+
assert self.D == 2, "only valid in 2D"
|
|
116
|
+
checkerboard(self.net, scales)
|
|
117
|
+
|
|
118
|
+
def gram(self, X, ws=5):
|
|
119
|
+
"""
|
|
120
|
+
Compute a sparse Gram matrix using local neighborhoods within a window.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
X (np.ndarray): Input features of shape (N, C)
|
|
124
|
+
ws (int or sequence): Half window size per dimension.
|
|
125
|
+
If int β same for all dims
|
|
126
|
+
If sequence β one per grid dim
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
np.ndarray: Sparse Gram matrix of shape (N, K)
|
|
130
|
+
"""
|
|
131
|
+
|
|
132
|
+
return gram(self, X, ws)
|
|
133
|
+
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import matplotlib.pyplot as plt
|
|
3
|
+
from numpy.lib.stride_tricks import sliding_window_view
|
|
4
|
+
from importlib import resources
|
|
5
|
+
|
|
6
|
+
def checkerboard(grid, scales):
|
|
7
|
+
n = grid.shape[0]
|
|
8
|
+
I, J = np.arange(n)[:, None], np.arange(n)[None, :]
|
|
9
|
+
|
|
10
|
+
fig, axes = plt.subplots(2, 2, figsize=(8, 8)) # grille 2x2
|
|
11
|
+
|
|
12
|
+
for ax, i in zip(axes.flat, scales):
|
|
13
|
+
H = n // i
|
|
14
|
+
mask = (I // H) % 2 == (J // H) % 2
|
|
15
|
+
|
|
16
|
+
ax.scatter(grid[mask, 0], grid[mask, 1],
|
|
17
|
+
color="blue", s=3)
|
|
18
|
+
ax.set_aspect("equal")
|
|
19
|
+
ax.set_xticks([])
|
|
20
|
+
ax.set_yticks([])
|
|
21
|
+
for spine in ax.spines.values():
|
|
22
|
+
spine.set_visible(False)
|
|
23
|
+
|
|
24
|
+
plt.tight_layout()
|
|
25
|
+
plt.show()
|
|
26
|
+
|
|
27
|
+
def grid_disorder(net):
|
|
28
|
+
"""
|
|
29
|
+
Measures the "disorder" of a D-dimensional grid.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
net (np.ndarray): Grid of shape (*IJ, D), points already sorted with attached coordinates.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
int: Total number of consecutive inversions along all axes (lower is better).
|
|
36
|
+
"""
|
|
37
|
+
total_disorder = 0
|
|
38
|
+
D = net.shape[-1]
|
|
39
|
+
for axis in range(D):
|
|
40
|
+
# Move the sorting axis to the front
|
|
41
|
+
axes_order = (axis,) + tuple(i for i in range(net.ndim - 1) if i != axis) + (net.ndim - 1,)
|
|
42
|
+
net_perm = np.transpose(net, axes_order)
|
|
43
|
+
|
|
44
|
+
# Flatten all remaining axes except the sorting axis
|
|
45
|
+
shape = net_perm.shape
|
|
46
|
+
flat_net = net_perm.reshape(shape[0], -1, D)
|
|
47
|
+
|
|
48
|
+
# Count consecutive inversions along the axis
|
|
49
|
+
coords = flat_net[..., axis] # shape (axis_size, n_cols)
|
|
50
|
+
inv = np.sum(coords[:-1, :] > coords[1:, :]) # i > i+1
|
|
51
|
+
total_disorder += inv
|
|
52
|
+
return total_disorder
|
|
53
|
+
|
|
54
|
+
def initpoint(method, size):
|
|
55
|
+
N, D = size
|
|
56
|
+
if method == "test":
|
|
57
|
+
points = np.random.rand(N, D)
|
|
58
|
+
if method == "france":
|
|
59
|
+
import geopandas as gpd
|
|
60
|
+
from shapely.geometry import Point
|
|
61
|
+
|
|
62
|
+
# Charger le GeoJSON
|
|
63
|
+
with resources.files("squarenet.data").joinpath("france.geojson").open("r") as f:
|
|
64
|
+
gdf = gpd.read_file(f)
|
|
65
|
+
metropole = gdf[~gdf['nom'].isin([
|
|
66
|
+
'Guadeloupe', 'Martinique', 'Guyane', 'La RΓ©union', 'Mayotte', 'Corse'
|
|
67
|
+
])]
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
# Merge regions
|
|
71
|
+
france = metropole.union_all()
|
|
72
|
+
|
|
73
|
+
# Bounding box
|
|
74
|
+
minx, miny, maxx, maxy = france.bounds
|
|
75
|
+
|
|
76
|
+
def sample_points(polygon, n_points):
|
|
77
|
+
points = []
|
|
78
|
+
|
|
79
|
+
while len(points) < n_points:
|
|
80
|
+
x = np.random.uniform(minx, maxx)
|
|
81
|
+
y = np.random.uniform(miny, maxy)
|
|
82
|
+
p = Point(x, y)
|
|
83
|
+
|
|
84
|
+
if polygon.contains(p):
|
|
85
|
+
points.append(p)
|
|
86
|
+
|
|
87
|
+
return np.array([[p.x, p.y] for p in points])
|
|
88
|
+
|
|
89
|
+
# Generate sample
|
|
90
|
+
points = sample_points(france, N)
|
|
91
|
+
return points
|
|
92
|
+
|
|
93
|
+
def gram(self, X, ws):
|
|
94
|
+
"""
|
|
95
|
+
Compute a sparse Gram matrix using local neighborhoods within a window.
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
X (np.ndarray): Input features of shape (N, C)
|
|
99
|
+
ws (int or sequence): Half window size per dimension.
|
|
100
|
+
If int β same for all dims
|
|
101
|
+
If sequence β one per grid dim
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
np.ndarray: Sparse Gram matrix of shape (N, K)
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
# Map to grid β (*IJ, C)
|
|
108
|
+
Xg = self.map(X)
|
|
109
|
+
*grid_shape, C = Xg.shape
|
|
110
|
+
ndim = len(grid_shape)
|
|
111
|
+
d = ws
|
|
112
|
+
|
|
113
|
+
# Handle window size
|
|
114
|
+
if isinstance(d, int):
|
|
115
|
+
d = [d] * ndim
|
|
116
|
+
else:
|
|
117
|
+
assert len(d) == ndim, "d must match number of grid dimensions"
|
|
118
|
+
|
|
119
|
+
# Padding
|
|
120
|
+
pad_width = [(di, di) for di in d] + [(0, 0)]
|
|
121
|
+
X_padded = np.pad(Xg, pad_width, mode='constant')
|
|
122
|
+
|
|
123
|
+
# Window shape
|
|
124
|
+
window_shape = tuple(2 * di + 1 for di in d)
|
|
125
|
+
|
|
126
|
+
# Extract patches
|
|
127
|
+
patches = sliding_window_view(
|
|
128
|
+
X_padded,
|
|
129
|
+
window_shape=window_shape,
|
|
130
|
+
axis=tuple(range(ndim))
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# Compute K = number of neighbors
|
|
134
|
+
K = np.prod(window_shape)
|
|
135
|
+
|
|
136
|
+
# reshape β (*IJ, K, C)
|
|
137
|
+
patches = patches.reshape(*grid_shape, K, C)
|
|
138
|
+
|
|
139
|
+
# center β (*IJ, 1, C)
|
|
140
|
+
center = Xg[(...,) + (None, slice(None))]
|
|
141
|
+
|
|
142
|
+
# dot product β (*IJ, K)
|
|
143
|
+
G = np.sum(center * patches, axis=-1)
|
|
144
|
+
|
|
145
|
+
# back to (N, K)
|
|
146
|
+
G = self.invert_map(G)
|
|
147
|
+
|
|
148
|
+
return G
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: squarenet
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Sparse local operations for point clouds in any dimension.
|
|
5
|
+
Author-email: ArmanddeCacqueray <armanddecacqueray@sfr.fr>
|
|
6
|
+
Requires-Python: >=3.8
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE.txt
|
|
9
|
+
Requires-Dist: numpy
|
|
10
|
+
Requires-Dist: matplotlib
|
|
11
|
+
Provides-Extra: demo
|
|
12
|
+
Requires-Dist: geopandas; extra == "demo"
|
|
13
|
+
Requires-Dist: shapely; extra == "demo"
|
|
14
|
+
Dynamic: license-file
|
|
15
|
+
|
|
16
|
+
# SquareNet
|
|
17
|
+
|
|
18
|
+
**SquareNet** is a lightweight framework for mapping unstructured point clouds into structured N-dimensional grids, enabling fast and efficient local operations.
|
|
19
|
+
|
|
20
|
+
## β¨ Key Idea
|
|
21
|
+
|
|
22
|
+
SquareNet builds a bijective mapping between point indices and a structured grid:
|
|
23
|
+
|
|
24
|
+
* **Bijection**: each point maps to exactly one grid cell
|
|
25
|
+
* **Neighborhood preservation**: nearby points remain close in the grid
|
|
26
|
+
|
|
27
|
+
This allows replacing expensive spatial queries (k-NN, radius search) with simple **array slicing**.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## π Features
|
|
32
|
+
|
|
33
|
+
* Fast point cloud β grid mapping
|
|
34
|
+
* Invertible transformation
|
|
35
|
+
* Efficient local neighborhood operations
|
|
36
|
+
* Sparse Gram matrix computation via sliding windows
|
|
37
|
+
* NumPy-friendly (no heavy dependencies required)
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## π¦ Installation
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pip install squarenet
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## π§ Basic Usage
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from squarenet import SquareNet
|
|
53
|
+
|
|
54
|
+
sqnet = SquareNet(IJ=(L, W, H, ...), max_iter = 100)
|
|
55
|
+
|
|
56
|
+
# Original data: (N, D)
|
|
57
|
+
points = np.random.rand(N, D)
|
|
58
|
+
Sqnet.fit(points)
|
|
59
|
+
|
|
60
|
+
# Map (N, *) to the grid: (L, W, H, ..., *)
|
|
61
|
+
sqX = sqnet.map(X)
|
|
62
|
+
|
|
63
|
+
# Back to original ordering
|
|
64
|
+
X_rec = sqnet.invert_map(sqX)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## π¬ Local Processing Example
|
|
70
|
+
|
|
71
|
+
Compute a **local (sparse) Gram matrix** using a sliding window:
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
G = sqnet.gram(X, ws=(5, 5))
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Instead of computing a full `(N Γ N)` matrix, SquareNet only computes interactions within local neighborhoods.
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## πΊοΈ Demo Dataset
|
|
82
|
+
|
|
83
|
+
A small demo dataset (France map) is included:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
Sqnet.fit("france")
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
---
|
|
90
|
+
|
|
91
|
+
## π§± Project Structure
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
src/
|
|
95
|
+
squarenet/
|
|
96
|
+
__init__.py
|
|
97
|
+
core.py
|
|
98
|
+
utils.py
|
|
99
|
+
data/france.geojson
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## π― Why SquareNet?
|
|
105
|
+
|
|
106
|
+
Traditional point cloud pipelines rely on:
|
|
107
|
+
|
|
108
|
+
* k-NN search (O(N log N))
|
|
109
|
+
* irregular memory access
|
|
110
|
+
* poor GPU utilization
|
|
111
|
+
|
|
112
|
+
SquareNet enables:
|
|
113
|
+
|
|
114
|
+
* **O(N)** local operations
|
|
115
|
+
* contiguous memory access
|
|
116
|
+
* vectorized operations on a sliding window
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
## π Use Cases
|
|
121
|
+
|
|
122
|
+
* Point cloud processing
|
|
123
|
+
* Graph-to-grid transformations
|
|
124
|
+
* Fast kernel methods
|
|
125
|
+
* Local feature aggregation
|
|
126
|
+
* Deep learning preprocessing
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## π οΈ Development
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
git clone https://github.com/ArmanddeCacqueray/SquareNet
|
|
134
|
+
cd squarenet
|
|
135
|
+
pip install -e .
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## π License
|
|
141
|
+
|
|
142
|
+
MIT License
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## π€ Contributing
|
|
147
|
+
|
|
148
|
+
Contributions are welcome. Please open an issue or submit a pull request.
|
|
149
|
+
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
LICENSE.txt
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/squarenet/__init__.py
|
|
5
|
+
src/squarenet/core.py
|
|
6
|
+
src/squarenet/utils.py
|
|
7
|
+
src/squarenet.egg-info/PKG-INFO
|
|
8
|
+
src/squarenet.egg-info/SOURCES.txt
|
|
9
|
+
src/squarenet.egg-info/dependency_links.txt
|
|
10
|
+
src/squarenet.egg-info/requires.txt
|
|
11
|
+
src/squarenet.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
squarenet
|