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.
@@ -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
@@ -0,0 +1,12 @@
1
+ .DS_Store
2
+ .pytest_cache/
3
+ .scikit-build/
4
+ .venv*/
5
+ build/
6
+ dist/
7
+ wheelhouse/
8
+
9
+ __pycache__/
10
+ *.py[cod]
11
+
12
+ examples/basic_usage_output.png
@@ -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.
@@ -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
+ ![megalap triptych showcase](assets/showcase_triptych_512.png)
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
@@ -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()