megalap 0.1.1__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.
- megalap-0.1.1/.github/workflows/ci.yml +64 -0
- megalap-0.1.1/.github/workflows/release.yml +84 -0
- megalap-0.1.1/.gitignore +12 -0
- megalap-0.1.1/CMakeLists.txt +10 -0
- megalap-0.1.1/LICENSE +21 -0
- megalap-0.1.1/PKG-INFO +111 -0
- megalap-0.1.1/PUBLISHING.md +60 -0
- megalap-0.1.1/README.md +157 -0
- megalap-0.1.1/README_PYPI.md +77 -0
- megalap-0.1.1/assets/showcase_triptych_512.png +0 -0
- megalap-0.1.1/examples/basic_usage.py +90 -0
- megalap-0.1.1/examples/benchmark_threads.py +61 -0
- megalap-0.1.1/examples/render_showcase.py +243 -0
- megalap-0.1.1/pyproject.toml +74 -0
- megalap-0.1.1/python/megalap/__init__.py +179 -0
- megalap-0.1.1/src/core.cpp +653 -0
- megalap-0.1.1/tests/test_smoke.py +71 -0
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: ["main"]
|
|
6
|
+
pull_request:
|
|
7
|
+
workflow_dispatch:
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
|
|
12
|
+
jobs:
|
|
13
|
+
tests:
|
|
14
|
+
name: Tests (${{ matrix.os }}, py${{ matrix.python-version }})
|
|
15
|
+
runs-on: ${{ matrix.os }}
|
|
16
|
+
strategy:
|
|
17
|
+
fail-fast: false
|
|
18
|
+
matrix:
|
|
19
|
+
os: [ubuntu-latest, windows-latest, macos-latest]
|
|
20
|
+
python-version: ["3.10", "3.12", "3.14"]
|
|
21
|
+
|
|
22
|
+
steps:
|
|
23
|
+
- uses: actions/checkout@v6
|
|
24
|
+
|
|
25
|
+
- uses: actions/setup-python@v6
|
|
26
|
+
with:
|
|
27
|
+
python-version: ${{ matrix.python-version }}
|
|
28
|
+
cache: pip
|
|
29
|
+
|
|
30
|
+
- name: Install package and test dependencies
|
|
31
|
+
run: python -m pip install -U pip && python -m pip install -e '.[test]'
|
|
32
|
+
|
|
33
|
+
- name: Run tests
|
|
34
|
+
run: python -m pytest -q
|
|
35
|
+
|
|
36
|
+
dist:
|
|
37
|
+
name: Build distributions
|
|
38
|
+
runs-on: ubuntu-latest
|
|
39
|
+
|
|
40
|
+
steps:
|
|
41
|
+
- uses: actions/checkout@v6
|
|
42
|
+
|
|
43
|
+
- uses: actions/setup-python@v6
|
|
44
|
+
with:
|
|
45
|
+
python-version: "3.14"
|
|
46
|
+
cache: pip
|
|
47
|
+
|
|
48
|
+
- name: Install release tooling
|
|
49
|
+
run: python -m pip install -U pip build twine pytest
|
|
50
|
+
|
|
51
|
+
- name: Build sdist and wheel
|
|
52
|
+
run: python -m build
|
|
53
|
+
|
|
54
|
+
- name: Check distribution metadata
|
|
55
|
+
run: python -m twine check dist/*
|
|
56
|
+
|
|
57
|
+
- name: Smoke test sdist install
|
|
58
|
+
run: python -m pip install --force-reinstall dist/*.tar.gz && python -m pytest -q tests/test_smoke.py
|
|
59
|
+
|
|
60
|
+
- uses: actions/upload-artifact@v6
|
|
61
|
+
with:
|
|
62
|
+
name: ci-dist
|
|
63
|
+
path: dist/*
|
|
64
|
+
if-no-files-found: error
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags: ["v*"]
|
|
6
|
+
workflow_dispatch:
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
contents: read
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
build-sdist:
|
|
13
|
+
name: Build sdist
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v6
|
|
18
|
+
|
|
19
|
+
- uses: actions/setup-python@v6
|
|
20
|
+
with:
|
|
21
|
+
python-version: "3.14"
|
|
22
|
+
cache: pip
|
|
23
|
+
|
|
24
|
+
- name: Install build tooling
|
|
25
|
+
run: python -m pip install -U pip build twine pytest
|
|
26
|
+
|
|
27
|
+
- name: Build sdist
|
|
28
|
+
run: python -m build --sdist
|
|
29
|
+
|
|
30
|
+
- name: Check sdist metadata
|
|
31
|
+
run: python -m twine check dist/*
|
|
32
|
+
|
|
33
|
+
- name: Smoke test sdist install
|
|
34
|
+
run: python -m pip install --force-reinstall dist/*.tar.gz && python -m pytest -q tests/test_smoke.py
|
|
35
|
+
|
|
36
|
+
- uses: actions/upload-artifact@v6
|
|
37
|
+
with:
|
|
38
|
+
name: dist-sdist
|
|
39
|
+
path: dist/*
|
|
40
|
+
if-no-files-found: error
|
|
41
|
+
|
|
42
|
+
build-wheels:
|
|
43
|
+
name: Build wheels (${{ matrix.os }})
|
|
44
|
+
runs-on: ${{ matrix.os }}
|
|
45
|
+
env:
|
|
46
|
+
CIBW_ENVIRONMENT_MACOS: MACOSX_DEPLOYMENT_TARGET=11.0
|
|
47
|
+
strategy:
|
|
48
|
+
fail-fast: false
|
|
49
|
+
matrix:
|
|
50
|
+
os: [ubuntu-latest, windows-latest, macos-15-intel, macos-14]
|
|
51
|
+
|
|
52
|
+
steps:
|
|
53
|
+
- uses: actions/checkout@v6
|
|
54
|
+
|
|
55
|
+
- uses: pypa/cibuildwheel@v3.3.0
|
|
56
|
+
with:
|
|
57
|
+
output-dir: wheelhouse
|
|
58
|
+
|
|
59
|
+
- uses: actions/upload-artifact@v6
|
|
60
|
+
with:
|
|
61
|
+
name: dist-${{ matrix.os }}
|
|
62
|
+
path: wheelhouse/*.whl
|
|
63
|
+
if-no-files-found: error
|
|
64
|
+
|
|
65
|
+
publish:
|
|
66
|
+
name: Publish to PyPI
|
|
67
|
+
runs-on: ubuntu-latest
|
|
68
|
+
needs: [build-sdist, build-wheels]
|
|
69
|
+
environment:
|
|
70
|
+
name: pypi
|
|
71
|
+
url: https://pypi.org/p/megalap
|
|
72
|
+
permissions:
|
|
73
|
+
contents: read
|
|
74
|
+
id-token: write
|
|
75
|
+
|
|
76
|
+
steps:
|
|
77
|
+
- uses: actions/download-artifact@v5
|
|
78
|
+
with:
|
|
79
|
+
pattern: dist-*
|
|
80
|
+
path: dist
|
|
81
|
+
merge-multiple: true
|
|
82
|
+
|
|
83
|
+
- name: Publish package distributions to PyPI
|
|
84
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
megalap-0.1.1/.gitignore
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
cmake_minimum_required(VERSION 3.18)
|
|
2
|
+
project(megalap LANGUAGES CXX)
|
|
3
|
+
|
|
4
|
+
find_package(Python REQUIRED COMPONENTS Interpreter Development.Module)
|
|
5
|
+
find_package(nanobind CONFIG REQUIRED)
|
|
6
|
+
|
|
7
|
+
nanobind_add_module(_core src/core.cpp)
|
|
8
|
+
target_compile_features(_core PRIVATE cxx_std_17)
|
|
9
|
+
|
|
10
|
+
install(TARGETS _core LIBRARY DESTINATION megalap)
|
megalap-0.1.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kyle McDonald
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
megalap-0.1.1/PKG-INFO
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: megalap
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Native dense LAP and windowed cleanup kernels for point-to-grid assignment.
|
|
5
|
+
Keywords: assignment,lap,point-cloud,grid,jv
|
|
6
|
+
Author: Kyle McDonald
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
16
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
17
|
+
Classifier: Programming Language :: C++
|
|
18
|
+
Classifier: Topic :: Scientific/Engineering
|
|
19
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
20
|
+
Project-URL: Homepage, https://github.com/kylemcdonald/megalap
|
|
21
|
+
Project-URL: Repository, https://github.com/kylemcdonald/megalap
|
|
22
|
+
Project-URL: Issues, https://github.com/kylemcdonald/megalap/issues
|
|
23
|
+
Requires-Python: >=3.10
|
|
24
|
+
Requires-Dist: numpy>=1.26
|
|
25
|
+
Provides-Extra: examples
|
|
26
|
+
Requires-Dist: matplotlib>=3.8; extra == "examples"
|
|
27
|
+
Provides-Extra: test
|
|
28
|
+
Requires-Dist: pytest>=8.3; extra == "test"
|
|
29
|
+
Provides-Extra: release
|
|
30
|
+
Requires-Dist: build>=1.2; extra == "release"
|
|
31
|
+
Requires-Dist: cibuildwheel>=3.0; extra == "release"
|
|
32
|
+
Requires-Dist: twine>=5.1; extra == "release"
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
# megalap
|
|
36
|
+
|
|
37
|
+
`megalap` is a Python package with a native C++ core and `nanobind` bindings for point-to-grid assignment.
|
|
38
|
+
|
|
39
|
+
The public API has three functions:
|
|
40
|
+
|
|
41
|
+
1. `linear_sum_assignment(cost_matrix)`
|
|
42
|
+
2. `window_cleanup(points, initial_assignment, rows, cols, budget_seconds, ...)`
|
|
43
|
+
3. `snap_to_grid(points, width=None, height=None, cleanup_seconds=30.0, ...)`
|
|
44
|
+
|
|
45
|
+
## Install
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
python -m pip install megalap
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
To run the matplotlib example from the source tree:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
python -m pip install -e '.[examples]'
|
|
55
|
+
python examples/basic_usage.py
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## API
|
|
59
|
+
|
|
60
|
+
### `linear_sum_assignment(cost_matrix)`
|
|
61
|
+
|
|
62
|
+
Solve a dense square linear assignment problem with the native C++ Jonker-Volgenant implementation.
|
|
63
|
+
|
|
64
|
+
Returns:
|
|
65
|
+
|
|
66
|
+
- `row_ind`: `int64` NumPy array of shape `(n,)`
|
|
67
|
+
- `col_ind`: `int64` NumPy array of shape `(n,)`
|
|
68
|
+
- `total_cost`: Python `float`
|
|
69
|
+
|
|
70
|
+
### `window_cleanup(points, initial_assignment, rows, cols, budget_seconds, ...)`
|
|
71
|
+
|
|
72
|
+
Run the overlapping-window cleanup kernel using native C++ threads.
|
|
73
|
+
|
|
74
|
+
Key options:
|
|
75
|
+
|
|
76
|
+
- `window_size=6`
|
|
77
|
+
- `num_threads=None` to use `std::thread::hardware_concurrency()`
|
|
78
|
+
- `num_threads=1` to force serial execution
|
|
79
|
+
- `fixed_suffix_count` to keep a suffix of target cells fixed
|
|
80
|
+
|
|
81
|
+
Returns a dict with:
|
|
82
|
+
|
|
83
|
+
- `assignment`
|
|
84
|
+
- `passes_completed`
|
|
85
|
+
- `elapsed_s`
|
|
86
|
+
- `final_cost`
|
|
87
|
+
|
|
88
|
+
### `snap_to_grid(points, width=None, height=None, cleanup_seconds=30.0, ...)`
|
|
89
|
+
|
|
90
|
+
High-level wrapper for snapping a 2D point cloud onto a destination grid.
|
|
91
|
+
|
|
92
|
+
Behavior:
|
|
93
|
+
|
|
94
|
+
- chooses a destination grid automatically when `width` and `height` are omitted
|
|
95
|
+
- prefers exact rectangular sizes with aspect ratio in `[1:1, 2:1]`
|
|
96
|
+
- falls back to a near-square enclosing grid in that same band when exact factors do not exist
|
|
97
|
+
- pads with edge ghost points when the chosen grid has more cells than real points
|
|
98
|
+
- runs the native LAP solver and `30s` of cleanup by default
|
|
99
|
+
- set `cleanup_seconds=0.0` to disable cleanup
|
|
100
|
+
|
|
101
|
+
Returns:
|
|
102
|
+
|
|
103
|
+
- `grid_points`: `(n, 2)` float64 NumPy array of assigned destination points in original point order
|
|
104
|
+
- `assignment`: `(n,)` int64 NumPy array of destination-grid indices in original point order
|
|
105
|
+
- `(width, height)`: destination-grid size tuple
|
|
106
|
+
|
|
107
|
+
## More
|
|
108
|
+
|
|
109
|
+
- Source repository: https://github.com/kylemcdonald/megalap
|
|
110
|
+
- Issue tracker: https://github.com/kylemcdonald/megalap/issues
|
|
111
|
+
- Example scripts: https://github.com/kylemcdonald/megalap/tree/main/examples
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# Publishing `megalap`
|
|
2
|
+
|
|
3
|
+
## Local validation
|
|
4
|
+
|
|
5
|
+
From the repository root:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
python -m pip install -U build twine pytest
|
|
9
|
+
python -m build
|
|
10
|
+
python -m twine check dist/*
|
|
11
|
+
python -m pip install --force-reinstall dist/*.whl
|
|
12
|
+
python -m pytest -q
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Continuous integration
|
|
16
|
+
|
|
17
|
+
The repository includes:
|
|
18
|
+
|
|
19
|
+
- [`.github/workflows/ci.yml`](.github/workflows/ci.yml) for editable-install tests and distribution checks
|
|
20
|
+
- [`.github/workflows/release.yml`](.github/workflows/release.yml) for tagged multi-platform releases
|
|
21
|
+
|
|
22
|
+
`release.yml` builds:
|
|
23
|
+
|
|
24
|
+
- one source distribution on Linux
|
|
25
|
+
- wheels on Linux, macOS Intel, macOS Apple Silicon, and Windows
|
|
26
|
+
|
|
27
|
+
## One-time PyPI setup
|
|
28
|
+
|
|
29
|
+
Before `release.yml` can publish to PyPI:
|
|
30
|
+
|
|
31
|
+
1. Create the `megalap` project on PyPI.
|
|
32
|
+
2. Configure PyPI trusted publishing for GitHub repository `kylemcdonald/megalap`.
|
|
33
|
+
3. Set the trusted publisher workflow file to `.github/workflows/release.yml`.
|
|
34
|
+
4. Add a GitHub `pypi` environment if you want environment protection on releases.
|
|
35
|
+
|
|
36
|
+
## Release steps
|
|
37
|
+
|
|
38
|
+
1. Update `version` in `pyproject.toml`.
|
|
39
|
+
2. Commit and push to `main`.
|
|
40
|
+
3. Wait for `.github/workflows/ci.yml` to pass.
|
|
41
|
+
4. Create and push a tag like `v0.1.0`.
|
|
42
|
+
5. Confirm the release workflow uploads the sdist and wheels, then publishes them to PyPI.
|
|
43
|
+
|
|
44
|
+
## Optional dry run
|
|
45
|
+
|
|
46
|
+
If you want to verify the package manually before a real release:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
python -m pip install -U pytest
|
|
50
|
+
python -m build
|
|
51
|
+
python -m pip install --force-reinstall dist/*.tar.gz
|
|
52
|
+
python -m pytest -q
|
|
53
|
+
python -m pip install --force-reinstall dist/*.whl
|
|
54
|
+
python -m pytest -q
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Notes
|
|
58
|
+
|
|
59
|
+
- The repository README keeps the hero image. PyPI uses `README_PYPI.md` as the package long description so the package page does not depend on local image assets.
|
|
60
|
+
- The release workflow uses `cibuildwheel` so PyPI receives Linux, macOS, and Windows wheels instead of a single local platform wheel.
|
megalap-0.1.1/README.md
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# megalap
|
|
2
|
+
|
|
3
|
+
`megalap` is a Python package with a native C++ core and `nanobind` bindings for point-to-grid assignment work.
|
|
4
|
+
|
|
5
|
+
The public API is intentionally small:
|
|
6
|
+
|
|
7
|
+
1. `linear_sum_assignment(cost_matrix)`
|
|
8
|
+
2. `window_cleanup(points, initial_assignment, rows, cols, budget_seconds, ...)`
|
|
9
|
+
3. `snap_to_grid(points, width=None, height=None, cleanup_seconds=30.0, ...)`
|
|
10
|
+
|
|
11
|
+
## Showcase
|
|
12
|
+
|
|
13
|
+
`512x512` meandering point cloud, recursive `8x8` seed, `30s` of native `6x6` cleanup, rendered as a three-panel hero image on a black background:
|
|
14
|
+
|
|
15
|
+
1. the initial point cloud
|
|
16
|
+
2. a `50%` interpolated view
|
|
17
|
+
3. the final grid
|
|
18
|
+
|
|
19
|
+
All three panels use the same Lab-derived coloring with source `x/y` mapped into `a/b`.
|
|
20
|
+
|
|
21
|
+

|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
From PyPI:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
python -m pip install megalap
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
From a local checkout:
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
python -m pip install -e .
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## API
|
|
38
|
+
|
|
39
|
+
### `linear_sum_assignment(cost_matrix)`
|
|
40
|
+
|
|
41
|
+
Solve a dense square LAP with the native C++ Jonker-Volgenant implementation.
|
|
42
|
+
|
|
43
|
+
Inputs:
|
|
44
|
+
|
|
45
|
+
- `cost_matrix`: `float64` array-like of shape `(n, n)`
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
|
|
49
|
+
- `row_ind`: `int64` NumPy array of shape `(n,)`
|
|
50
|
+
- `col_ind`: `int64` NumPy array of shape `(n,)`
|
|
51
|
+
- `total_cost`: Python `float`
|
|
52
|
+
|
|
53
|
+
### `window_cleanup(points, initial_assignment, rows, cols, budget_seconds, window_size=6, margin=0.03, num_threads=None, fixed_suffix_count=0)`
|
|
54
|
+
|
|
55
|
+
Run the native overlapping-window cleanup kernel.
|
|
56
|
+
|
|
57
|
+
Behavior:
|
|
58
|
+
|
|
59
|
+
- uses the C++ backend
|
|
60
|
+
- solves each phase as multiple independent small JV problems
|
|
61
|
+
- runs same-phase window solves in parallel with native C++ threads
|
|
62
|
+
- `num_threads=None` uses `std::thread::hardware_concurrency()`
|
|
63
|
+
- `num_threads=1` forces serial cleanup
|
|
64
|
+
- `fixed_suffix_count` can keep a suffix of target cells locked, which is useful for padded ghost points
|
|
65
|
+
|
|
66
|
+
Inputs:
|
|
67
|
+
|
|
68
|
+
- `points`: `float64` array-like of shape `(n, 2)`
|
|
69
|
+
- `initial_assignment`: `int64` array-like of shape `(n,)`
|
|
70
|
+
- `rows`, `cols`: target grid dimensions
|
|
71
|
+
- `budget_seconds`: cleanup wall-clock budget
|
|
72
|
+
- `window_size`: default `6`
|
|
73
|
+
- `margin`: normalized grid margin
|
|
74
|
+
- `num_threads`: optional thread count override
|
|
75
|
+
- `fixed_suffix_count`: number of trailing target cells to keep fixed during cleanup
|
|
76
|
+
|
|
77
|
+
Returns a Python `dict` with:
|
|
78
|
+
|
|
79
|
+
- `assignment`: final `int64` NumPy array
|
|
80
|
+
- `passes_completed`
|
|
81
|
+
- `elapsed_s`
|
|
82
|
+
- `final_cost`
|
|
83
|
+
|
|
84
|
+
### `snap_to_grid(points, width=None, height=None, cleanup_seconds=30.0, window_size=6, margin=0.03, num_threads=None)`
|
|
85
|
+
|
|
86
|
+
High-level point-cloud wrapper.
|
|
87
|
+
|
|
88
|
+
Behavior:
|
|
89
|
+
|
|
90
|
+
- chooses a destination grid automatically when `width` and `height` are omitted
|
|
91
|
+
- prefers exact rectangular sizes with aspect ratio in `[1:1, 2:1]`
|
|
92
|
+
- if no exact factorization exists in that range, chooses a near-square enclosing grid in that same band
|
|
93
|
+
- pads with edge ghost points when the grid has more cells than real points
|
|
94
|
+
- runs the native JV LAP
|
|
95
|
+
- runs `30s` of cleanup by default
|
|
96
|
+
- set `cleanup_seconds=0.0` to disable cleanup
|
|
97
|
+
- passes `num_threads` through to the native cleanup kernel
|
|
98
|
+
|
|
99
|
+
Returns three values:
|
|
100
|
+
|
|
101
|
+
- `grid_points`: `(n, 2)` float64 NumPy array of assigned destination-grid positions, in the original source-point order
|
|
102
|
+
- `assignment`: `(n,)` int64 NumPy array of destination-grid indices for the original source points
|
|
103
|
+
- `(width, height)`: destination-grid size tuple
|
|
104
|
+
|
|
105
|
+
## Example
|
|
106
|
+
|
|
107
|
+
See [examples/basic_usage.py](examples/basic_usage.py).
|
|
108
|
+
|
|
109
|
+
Run it after installation:
|
|
110
|
+
|
|
111
|
+
```bash
|
|
112
|
+
python -m pip install -e '.[examples]'
|
|
113
|
+
python examples/basic_usage.py
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
The showcase image above was generated with:
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
python examples/render_showcase.py \
|
|
120
|
+
--grid-width 512 \
|
|
121
|
+
--grid-height 512 \
|
|
122
|
+
--image-width 512 \
|
|
123
|
+
--image-height 512 \
|
|
124
|
+
--cleanup-seconds 30 \
|
|
125
|
+
--output assets/showcase_triptych_512.png
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
That renderer uses NumPy directly and writes the PNG without matplotlib.
|
|
129
|
+
|
|
130
|
+
For release instructions, see [PUBLISHING.md](PUBLISHING.md).
|
|
131
|
+
|
|
132
|
+
## Cleanup Threading Benchmark
|
|
133
|
+
|
|
134
|
+
There is a small reproducible threading benchmark at [examples/benchmark_threads.py](examples/benchmark_threads.py).
|
|
135
|
+
|
|
136
|
+
Run it with:
|
|
137
|
+
|
|
138
|
+
```bash
|
|
139
|
+
python examples/benchmark_threads.py
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
On this machine (`16` logical CPUs), using a `256x256` meandering point cloud, identity seed assignment, `window_size=6`, and a `1.0s` cleanup budget, the median of `3` runs was:
|
|
143
|
+
|
|
144
|
+
| mode | median elapsed | median passes | median passes/s |
|
|
145
|
+
|---|---:|---:|---:|
|
|
146
|
+
| `num_threads=1` | `1.028 s` | `3` | `2.92` |
|
|
147
|
+
| `num_threads=None` | `1.026 s` | `31` | `30.20` |
|
|
148
|
+
|
|
149
|
+
So the default threaded path improved cleanup throughput by about `10.3x` on that benchmark.
|
|
150
|
+
|
|
151
|
+
## Notes
|
|
152
|
+
|
|
153
|
+
- `linear_sum_assignment()` currently expects a square cost matrix.
|
|
154
|
+
- `snap_to_grid()` handles non-rectangular point counts by padding with visible ghost points along the trailing edge of the chosen destination grid.
|
|
155
|
+
- The cleanup kernel uses overlapping windows of at most `6x6`, so the native small-JV kernel is specialized for up to `36` points per window.
|
|
156
|
+
- The native cleanup kernel uses standard C++ threads and does not depend on OpenMP.
|
|
157
|
+
- GitHub Actions builds release artifacts for Linux, macOS, and Windows wheels, plus an sdist.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# megalap
|
|
2
|
+
|
|
3
|
+
`megalap` is a Python package with a native C++ core and `nanobind` bindings for point-to-grid assignment.
|
|
4
|
+
|
|
5
|
+
The public API has three functions:
|
|
6
|
+
|
|
7
|
+
1. `linear_sum_assignment(cost_matrix)`
|
|
8
|
+
2. `window_cleanup(points, initial_assignment, rows, cols, budget_seconds, ...)`
|
|
9
|
+
3. `snap_to_grid(points, width=None, height=None, cleanup_seconds=30.0, ...)`
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
python -m pip install megalap
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
To run the matplotlib example from the source tree:
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
python -m pip install -e '.[examples]'
|
|
21
|
+
python examples/basic_usage.py
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## API
|
|
25
|
+
|
|
26
|
+
### `linear_sum_assignment(cost_matrix)`
|
|
27
|
+
|
|
28
|
+
Solve a dense square linear assignment problem with the native C++ Jonker-Volgenant implementation.
|
|
29
|
+
|
|
30
|
+
Returns:
|
|
31
|
+
|
|
32
|
+
- `row_ind`: `int64` NumPy array of shape `(n,)`
|
|
33
|
+
- `col_ind`: `int64` NumPy array of shape `(n,)`
|
|
34
|
+
- `total_cost`: Python `float`
|
|
35
|
+
|
|
36
|
+
### `window_cleanup(points, initial_assignment, rows, cols, budget_seconds, ...)`
|
|
37
|
+
|
|
38
|
+
Run the overlapping-window cleanup kernel using native C++ threads.
|
|
39
|
+
|
|
40
|
+
Key options:
|
|
41
|
+
|
|
42
|
+
- `window_size=6`
|
|
43
|
+
- `num_threads=None` to use `std::thread::hardware_concurrency()`
|
|
44
|
+
- `num_threads=1` to force serial execution
|
|
45
|
+
- `fixed_suffix_count` to keep a suffix of target cells fixed
|
|
46
|
+
|
|
47
|
+
Returns a dict with:
|
|
48
|
+
|
|
49
|
+
- `assignment`
|
|
50
|
+
- `passes_completed`
|
|
51
|
+
- `elapsed_s`
|
|
52
|
+
- `final_cost`
|
|
53
|
+
|
|
54
|
+
### `snap_to_grid(points, width=None, height=None, cleanup_seconds=30.0, ...)`
|
|
55
|
+
|
|
56
|
+
High-level wrapper for snapping a 2D point cloud onto a destination grid.
|
|
57
|
+
|
|
58
|
+
Behavior:
|
|
59
|
+
|
|
60
|
+
- chooses a destination grid automatically when `width` and `height` are omitted
|
|
61
|
+
- prefers exact rectangular sizes with aspect ratio in `[1:1, 2:1]`
|
|
62
|
+
- falls back to a near-square enclosing grid in that same band when exact factors do not exist
|
|
63
|
+
- pads with edge ghost points when the chosen grid has more cells than real points
|
|
64
|
+
- runs the native LAP solver and `30s` of cleanup by default
|
|
65
|
+
- set `cleanup_seconds=0.0` to disable cleanup
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
|
|
69
|
+
- `grid_points`: `(n, 2)` float64 NumPy array of assigned destination points in original point order
|
|
70
|
+
- `assignment`: `(n,)` int64 NumPy array of destination-grid indices in original point order
|
|
71
|
+
- `(width, height)`: destination-grid size tuple
|
|
72
|
+
|
|
73
|
+
## More
|
|
74
|
+
|
|
75
|
+
- Source repository: https://github.com/kylemcdonald/megalap
|
|
76
|
+
- Issue tracker: https://github.com/kylemcdonald/megalap/issues
|
|
77
|
+
- Example scripts: https://github.com/kylemcdonald/megalap/tree/main/examples
|
|
Binary file
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import pathlib
|
|
4
|
+
|
|
5
|
+
import matplotlib.pyplot as plt
|
|
6
|
+
import numpy as np
|
|
7
|
+
|
|
8
|
+
import megalap
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def make_meandering_points(n: int, seed: int = 0, margin: float = 0.03) -> np.ndarray:
|
|
12
|
+
rng = np.random.default_rng(seed)
|
|
13
|
+
steps = rng.normal(loc=0.0, scale=1.0, size=(n, 2))
|
|
14
|
+
points = np.cumsum(steps, axis=0)
|
|
15
|
+
mins = points.min(axis=0)
|
|
16
|
+
maxs = points.max(axis=0)
|
|
17
|
+
span = np.maximum(maxs - mins, np.finfo(np.float64).eps)
|
|
18
|
+
points = (points - mins) / span
|
|
19
|
+
points = margin + (1.0 - 2.0 * margin) * points
|
|
20
|
+
return np.asarray(points, dtype=np.float64)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def lab_to_srgb(points: np.ndarray) -> np.ndarray:
|
|
24
|
+
l = np.full(points.shape[0], 72.0, dtype=np.float64)
|
|
25
|
+
a = (points[:, 0] * 2.0 - 1.0) * 80.0
|
|
26
|
+
b = (points[:, 1] * 2.0 - 1.0) * 80.0
|
|
27
|
+
|
|
28
|
+
fy = (l + 16.0) / 116.0
|
|
29
|
+
fx = fy + a / 500.0
|
|
30
|
+
fz = fy - b / 200.0
|
|
31
|
+
|
|
32
|
+
epsilon = 216.0 / 24389.0
|
|
33
|
+
kappa = 24389.0 / 27.0
|
|
34
|
+
|
|
35
|
+
def invf(t: np.ndarray) -> np.ndarray:
|
|
36
|
+
t3 = t * t * t
|
|
37
|
+
return np.where(t3 > epsilon, t3, (116.0 * t - 16.0) / kappa)
|
|
38
|
+
|
|
39
|
+
x = 0.95047 * invf(fx)
|
|
40
|
+
y = invf(fy)
|
|
41
|
+
z = 1.08883 * invf(fz)
|
|
42
|
+
|
|
43
|
+
r_lin = 3.2404542 * x - 1.5371385 * y - 0.4985314 * z
|
|
44
|
+
g_lin = -0.9692660 * x + 1.8760108 * y + 0.0415560 * z
|
|
45
|
+
b_lin = 0.0556434 * x - 0.2040259 * y + 1.0572252 * z
|
|
46
|
+
rgb_lin = np.clip(np.column_stack([r_lin, g_lin, b_lin]), 0.0, 1.0)
|
|
47
|
+
|
|
48
|
+
threshold = 0.0031308
|
|
49
|
+
rgb = np.where(
|
|
50
|
+
rgb_lin <= threshold,
|
|
51
|
+
12.92 * rgb_lin,
|
|
52
|
+
1.055 * np.power(rgb_lin, 1.0 / 2.4) - 0.055,
|
|
53
|
+
)
|
|
54
|
+
return np.clip(rgb, 0.0, 1.0)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def main() -> None:
|
|
58
|
+
points = make_meandering_points(32 * 32, seed=0)
|
|
59
|
+
grid_points, assignment, grid_size = megalap.snap_to_grid(
|
|
60
|
+
points,
|
|
61
|
+
width=32,
|
|
62
|
+
height=32,
|
|
63
|
+
cleanup_seconds=0.5,
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
interp = points + 0.8 * (grid_points - points)
|
|
67
|
+
colors = lab_to_srgb(points)
|
|
68
|
+
|
|
69
|
+
fig, ax = plt.subplots(figsize=(8, 8), dpi=150, facecolor="black")
|
|
70
|
+
ax.set_facecolor("black")
|
|
71
|
+
ax.scatter(interp[:, 0], interp[:, 1], s=1, c=colors, marker="s", linewidths=0)
|
|
72
|
+
ax.set_xlim(0.0, 1.0)
|
|
73
|
+
ax.set_ylim(0.0, 1.0)
|
|
74
|
+
ax.set_aspect("equal")
|
|
75
|
+
ax.set_xticks([])
|
|
76
|
+
ax.set_yticks([])
|
|
77
|
+
ax.set_title(f"megalap snap_to_grid · grid={grid_size[0]}x{grid_size[1]}", color="white")
|
|
78
|
+
fig.tight_layout()
|
|
79
|
+
output = pathlib.Path(__file__).with_name("basic_usage_output.png")
|
|
80
|
+
fig.savefig(output, facecolor=fig.get_facecolor(), bbox_inches="tight")
|
|
81
|
+
plt.close(fig)
|
|
82
|
+
|
|
83
|
+
print("grid_size:", grid_size)
|
|
84
|
+
print("assignment shape:", assignment.shape)
|
|
85
|
+
print("first five assigned indices:", assignment[:5])
|
|
86
|
+
print("wrote image:", output)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
if __name__ == "__main__":
|
|
90
|
+
main()
|