hilbertplot 0.1.0__tar.gz → 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. hilbertplot-0.2.0/CHANGELOG.md +98 -0
  2. hilbertplot-0.2.0/PKG-INFO +157 -0
  3. hilbertplot-0.2.0/README.md +124 -0
  4. {hilbertplot-0.1.0 → hilbertplot-0.2.0}/examples/plot_catalog.py +1 -1
  5. hilbertplot-0.2.0/examples/readme_figures.py +128 -0
  6. {hilbertplot-0.1.0 → hilbertplot-0.2.0}/examples/show_curve.py +1 -1
  7. {hilbertplot-0.1.0 → hilbertplot-0.2.0}/pyproject.toml +26 -16
  8. hilbertplot-0.2.0/src/hilbertplot/__init__.py +102 -0
  9. hilbertplot-0.2.0/src/hilbertplot/_core/arbitrary.py +332 -0
  10. {hilbertplot-0.1.0 → hilbertplot-0.2.0}/src/hilbertplot/_core/catalog.py +47 -12
  11. {hilbertplot-0.1.0 → hilbertplot-0.2.0}/src/hilbertplot/_core/curve.py +351 -40
  12. {hilbertplot-0.1.0 → hilbertplot-0.2.0}/src/hilbertplot/_core/generation.py +25 -4
  13. {hilbertplot-0.1.0 → hilbertplot-0.2.0}/src/hilbertplot/_core/geometry.py +64 -21
  14. hilbertplot-0.2.0/src/hilbertplot/_core/indexing.py +146 -0
  15. hilbertplot-0.2.0/src/hilbertplot/_core/limits.py +100 -0
  16. {hilbertplot-0.1.0 → hilbertplot-0.2.0}/src/hilbertplot/_core/symmetry.py +9 -17
  17. {hilbertplot-0.1.0 → hilbertplot-0.2.0}/src/hilbertplot/_data/mapping.py +101 -28
  18. {hilbertplot-0.1.0 → hilbertplot-0.2.0}/src/hilbertplot/_viz/hilbert_plot.py +229 -38
  19. {hilbertplot-0.1.0 → hilbertplot-0.2.0}/src/hilbertplot/_viz/render.py +103 -32
  20. hilbertplot-0.2.0/tests/conftest.py +40 -0
  21. hilbertplot-0.2.0/tests/test_arbitrary.py +143 -0
  22. {hilbertplot-0.1.0 → hilbertplot-0.2.0}/tests/test_catalog.py +13 -8
  23. {hilbertplot-0.1.0 → hilbertplot-0.2.0}/tests/test_data.py +10 -9
  24. {hilbertplot-0.1.0 → hilbertplot-0.2.0}/tests/test_geometry.py +21 -4
  25. hilbertplot-0.2.0/tests/test_indexing.py +161 -0
  26. hilbertplot-0.2.0/tests/test_performance.py +108 -0
  27. hilbertplot-0.2.0/tests/test_plotting.py +282 -0
  28. hilbertplot-0.2.0/tests/test_readme.py +59 -0
  29. hilbertplot-0.2.0/tests/test_robustness.py +590 -0
  30. {hilbertplot-0.1.0 → hilbertplot-0.2.0}/tests/test_symmetry.py +1 -1
  31. {hilbertplot-0.1.0 → hilbertplot-0.2.0}/tests/test_words.py +5 -5
  32. hilbertplot-0.1.0/PKG-INFO +0 -285
  33. hilbertplot-0.1.0/README.md +0 -257
  34. hilbertplot-0.1.0/src/hilbertplot/__init__.py +0 -69
  35. hilbertplot-0.1.0/src/hilbertplot/_core/arbitrary.py +0 -114
  36. hilbertplot-0.1.0/tests/test_arbitrary.py +0 -95
  37. hilbertplot-0.1.0/tests/test_plotting.py +0 -161
  38. {hilbertplot-0.1.0 → hilbertplot-0.2.0}/.gitignore +0 -0
  39. {hilbertplot-0.1.0 → hilbertplot-0.2.0}/LICENSE +0 -0
  40. {hilbertplot-0.1.0 → hilbertplot-0.2.0}/examples/groups.py +0 -0
  41. {hilbertplot-0.1.0 → hilbertplot-0.2.0}/examples/plot_data.py +0 -0
  42. {hilbertplot-0.1.0 → hilbertplot-0.2.0}/src/hilbertplot/_core/__init__.py +0 -0
  43. {hilbertplot-0.1.0 → hilbertplot-0.2.0}/src/hilbertplot/_data/__init__.py +0 -0
  44. {hilbertplot-0.1.0 → hilbertplot-0.2.0}/src/hilbertplot/_viz/__init__.py +0 -0
  45. {hilbertplot-0.1.0 → hilbertplot-0.2.0}/src/hilbertplot/py.typed +0 -0
@@ -0,0 +1,98 @@
1
+ # Changelog
2
+
3
+ All notable changes to **hilbertplot** are documented here. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/), and the project aims to adhere to
5
+ [Semantic Versioning](https://semver.org/).
6
+
7
+ Released versions below are generated automatically by python-semantic-release;
8
+ new entries are inserted at the marker.
9
+
10
+ ## Migrating from 0.1.0
11
+
12
+ `hilbertplot` is on `0.x`, where [semver §4](https://semver.org/#spec-item-4) allows the
13
+ interface to change in a minor release. Two spellings changed; both fail loudly rather
14
+ than silently, and nothing else in the 0.1.0 surface was removed.
15
+
16
+ | 0.1.0 | now | why |
17
+ | --- | --- | --- |
18
+ | `curve.plot(order)` | `curve.draw(order)` | one verb for "render onto axes", shared with `HilbertPlot.draw` |
19
+ | `curve.save(order, path)` | `curve.save(path, order)` | matches `HilbertPlot.save(path, ...)`; the old order raises a `TypeError` naming the fix |
20
+ | `repr(curve)` → `'Hilbert'` | `repr(curve)` → `'<Curve 0H Hilbert (proper)>'` | `repr` is now unambiguous; `str(curve)` is still `'Hilbert'` |
21
+
22
+ Behaviour that was buggy and is now stricter: `curve(True)` and `curve(3.0)` raise
23
+ `TypeError` instead of silently meaning `curve(1)` / `curve(3)`; an out-of-range index
24
+ raises `IndexError` rather than `KeyError` (both are `LookupError`); `plot.data` is
25
+ genuinely immutable rather than merely flagged; and `image(order=..., fit=...)` now
26
+ validates `fit` instead of ignoring it.
27
+
28
+ <!-- version list -->
29
+
30
+ ## v0.2.0 (2026-07-25)
31
+
32
+ ### Bug Fixes
33
+
34
+ - Close the gaps found by the second review round
35
+ ([`9df64f4`](https://github.com/El3ssar/hilbertplot/commit/9df64f463b2619d0d19ca7d3d818a16e2c25d7d8))
36
+
37
+ - Keep the new guards from costing more than they save
38
+ ([`bbaa464`](https://github.com/El3ssar/hilbertplot/commit/bbaa4646af8ca401aa27174277cbae22910cd34f))
39
+
40
+ ### Continuous Integration
41
+
42
+ - Bump actions, exempt dependabot from title check, curb ruff bumps
43
+ ([`aa74d78`](https://github.com/El3ssar/hilbertplot/commit/aa74d7810fa542e259dfcee77c23ad9aa491374a))
44
+
45
+ - Stop Dependabot from bumping ruff
46
+ ([`f639fc0`](https://github.com/El3ssar/hilbertplot/commit/f639fc0aa5ab67c615bf31470cfd1bd10f5248b5))
47
+
48
+ - Stop the floors job re-resolving the pins it exists to test
49
+ ([`106b08f`](https://github.com/El3ssar/hilbertplot/commit/106b08f8bedcf0d6c2c0ff023a60c2ff020265e4))
50
+
51
+ ### Documentation
52
+
53
+ - Add status badges and tidy the 0.1.0 changelog entry
54
+ ([`7d967ca`](https://github.com/El3ssar/hilbertplot/commit/7d967caae998903d0885a81c4801b50b974aef62))
55
+
56
+ - Illustrate the README with generated figures
57
+ ([`0f245b9`](https://github.com/El3ssar/hilbertplot/commit/0f245b941fbb933141b10d60ee16157c463b8501))
58
+
59
+ - Streamline the README to install + two examples
60
+ ([`fb53eab`](https://github.com/El3ssar/hilbertplot/commit/fb53eab38129a8e60c42160d5bfa3959677a37db))
61
+
62
+ ### Features
63
+
64
+ - Make the arbitrary-square path fast, harden the public surface, add encode/decode
65
+ ([`a6ce0da`](https://github.com/El3ssar/hilbertplot/commit/a6ce0da3463acaacc6c69d349a1ef06804a817f4))
66
+
67
+
68
+ ## v0.1.0 (2026-07-25)
69
+
70
+ First public release — the complete set of 40 two-dimensional Hilbert curves,
71
+ with fast numpy generation and data-visualisation tooling.
72
+
73
+ ### Added
74
+ - The complete set of **40** two-dimensional Hilbert curves (Estevez-Rams et al., 2017):
75
+ `curve(i)` by index, name or symbol; `points(order)`, `word(order)`; family groups
76
+ `homogeneous` / `inhomogeneous` / `proper` / `improper` / `closed` / `by_kernels`,
77
+ returned as a `CurveList` that prints as a table.
78
+ - Fast numpy stroke-code generation (no per-stroke Python loop); order-10 curve (~1M
79
+ points) in tens of milliseconds.
80
+ - Curve rendering: `curve.show/save/plot(order)` with automatic line→fill mode by order,
81
+ colour-graded traversal, and `gallery()` for grids of curves.
82
+ - **Data visualisation** ("Hilbert plots", Estevez-Rams et al., 2015): `hilbert_plot`
83
+ / `curve.plot_data` return a `HilbertPlot` that maps a 1-D vector onto 2-D on demand,
84
+ with colormap-by-name or colour-list interpolation, the `l`-granularity transform,
85
+ and `pad` / `truncate` fits.
86
+ - **Difference map** overlay (`difference=True`, tunable threshold) marking where a curve
87
+ breaks locality (Pérez-Demydenko et al., 2014); `curve.difference_map(order)`.
88
+ - **2-D Fourier map** (`fourier=True`; `HilbertPlot.fourier()`) revealing hidden periodic
89
+ and self-similar structure.
90
+ - **`unroll`**: read a 2-D square array back into a 1-D vector in curve order.
91
+ - **Arbitrary `n x n` squares** for the eight curves that support it
92
+ (`curve.grid(n)`, `curve.generalizes`, `hilbertplot.generalizing()`); the data mapping
93
+ square-fits to a tight `ceil(sqrt(N))` grid by default for those curves. The full
94
+ feasibility classification and impossibility proofs are in `research/quasisquares/`.
95
+
96
+ ### Notes
97
+ - Requires Python 3.10+. `numpy` is the only runtime dependency; `matplotlib` is the
98
+ optional `[plot]` extra.
@@ -0,0 +1,157 @@
1
+ Metadata-Version: 2.4
2
+ Name: hilbertplot
3
+ Version: 0.2.0
4
+ Summary: The complete set of 40 two-dimensional Hilbert curves, and data plots on them.
5
+ Project-URL: Homepage, https://github.com/El3ssar/hilbertplot
6
+ Project-URL: Source, https://github.com/El3ssar/hilbertplot
7
+ Project-URL: Issues, https://github.com/El3ssar/hilbertplot/issues
8
+ Project-URL: Changelog, https://github.com/El3ssar/hilbertplot/blob/main/CHANGELOG.md
9
+ Author-email: Daniel Estevez <kemossabee@gmail.com>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Keywords: dimensionality-reduction,hilbert-curve,locality,moore-curve,space-filling-curve,visualization
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Programming Language :: Python :: 3.13
20
+ Classifier: Programming Language :: Python :: 3.14
21
+ Classifier: Topic :: Scientific/Engineering :: Mathematics
22
+ Classifier: Topic :: Scientific/Engineering :: Visualization
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.10
25
+ Requires-Dist: numpy>=1.21
26
+ Provides-Extra: dev
27
+ Requires-Dist: matplotlib>=3.5; extra == 'dev'
28
+ Requires-Dist: pytest>=7.0; extra == 'dev'
29
+ Requires-Dist: ruff==0.15.13; extra == 'dev'
30
+ Provides-Extra: plot
31
+ Requires-Dist: matplotlib>=3.5; extra == 'plot'
32
+ Description-Content-Type: text/markdown
33
+
34
+ # hilbertplot
35
+
36
+ [![PyPI version](https://img.shields.io/pypi/v/hilbertplot.svg)](https://pypi.org/project/hilbertplot/)
37
+ [![CI](https://github.com/El3ssar/hilbertplot/actions/workflows/ci.yml/badge.svg)](https://github.com/El3ssar/hilbertplot/actions/workflows/ci.yml)
38
+ [![Python versions](https://img.shields.io/pypi/pyversions/hilbertplot.svg)](https://pypi.org/project/hilbertplot/)
39
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/El3ssar/hilbertplot/blob/main/LICENSE)
40
+
41
+ The **complete set of 40 two-dimensional Hilbert curves** — and a fast way to plot long
42
+ 1-D data on any of them.
43
+
44
+ Most software knows one Hilbert curve. In fact there are **forty** distinct space-filling
45
+ curves of Hilbert type in two dimensions (up to rotation, reflection and reversion), as
46
+ proved by Estevez-Rams et al., *"Hilbert curves in two dimensions"*, Rev. Cub. Fís. **34**,
47
+ 9 (2017). `hilbertplot` implements all forty with pure-numpy generation and uses them to lay
48
+ long 1-D vectors onto 2-D images.
49
+
50
+ <p align="center">
51
+ <img src="https://raw.githubusercontent.com/El3ssar/hilbertplot/main/assets/curve.png"
52
+ width="440" alt="The Hilbert curve at order 4, drawn as a colour-graded path">
53
+ </p>
54
+
55
+ ## Install
56
+
57
+ ```bash
58
+ pip install "hilbertplot[plot]" # drop [plot] for a numpy-only core (no matplotlib)
59
+ ```
60
+
61
+ ## Draw a curve
62
+
63
+ ```python
64
+ import hilbertplot
65
+
66
+ c = hilbertplot.curve(0) # by index 0–39, by name ("Moore"), or by symbol
67
+ c.show(4) # draw order 4 in a window
68
+ pts = c.points(4) # (256, 2) integer lattice points, in visit order
69
+ ```
70
+
71
+ The forty curves come in named groups — `hilbertplot.proper()`, `.improper()`,
72
+ `.homogeneous()`, `.inhomogeneous()`, `.generalizing()` — each of which prints as a table,
73
+ and which compose:
74
+
75
+ ```python
76
+ import hilbertplot
77
+
78
+ print(hilbertplot.proper()) # a table of the six proper curves
79
+ hilbertplot.catalog().closed().names # ['Moore', 'Liu1', 'Improper1', 'Improper4']
80
+ hilbertplot.by_kernels(3, 5).gallery(order=3) # draw a group as a grid of panels
81
+ ```
82
+
83
+ In a Jupyter notebook a `Curve` renders itself — put `hilbertplot.curve("Moore")` in a cell
84
+ and you get the picture, not a `repr`.
85
+
86
+ ## One cell at a time
87
+
88
+ `points(order)` builds all `4**order` cells. When you only need *where step `i` lands*,
89
+ `encode` answers in `O(order)` — so it works at orders no machine could materialise, and it
90
+ does so for **all forty** curves, not just the classic one:
91
+
92
+ ```python
93
+ import hilbertplot
94
+
95
+ c = hilbertplot.curve(0)
96
+ c.encode(5, 2) # (0, 3) — the cell visited at step 5, order 2
97
+ c.decode(0, 3, 2) # 5 — and back again
98
+ c.encode(10**18, 40) # (751054336, 346100736) — a 2**80-cell curve
99
+ ```
100
+
101
+ ## Plot data
102
+
103
+ Walk a curve and drop `data[i]` on the *i*-th cell it visits: a locality-preserving
104
+ 1-D → 2-D map where nearby values stay nearby.
105
+
106
+ ```python
107
+ import numpy as np, hilbertplot
108
+
109
+ plot = hilbertplot.hilbert_plot(0, np.arange(1, 257))
110
+ plot.show("viridis") # a matplotlib colormap name, or a list of colours to blend
111
+ ```
112
+
113
+ ![The same data laid on two different curves](https://raw.githubusercontent.com/El3ssar/hilbertplot/main/assets/data_two_curves.png)
114
+
115
+ *The same numbers on curve 0 and curve 32 — one reason to have all forty.*
116
+
117
+ ## Seeing more than the data
118
+
119
+ **Coarse-grain it.** `plot.show(granularity=4)` replaces each block of values by its mean.
120
+ Below, a binary sequence interleaves stretches of two periodic patterns with identical
121
+ density — invisible in the faithful plot, obvious once averaged.
122
+
123
+ ![Periodic regions revealed by coarse-graining](https://raw.githubusercontent.com/El3ssar/hilbertplot/main/assets/granularity.png)
124
+
125
+ **Find where locality breaks.** `plot.show(difference=True)` marks the cells that are
126
+ neighbours in the plane but far apart along the curve. A higher threshold keeps only the
127
+ worst offenders.
128
+
129
+ ![The difference map at two thresholds](https://raw.githubusercontent.com/El3ssar/hilbertplot/main/assets/difference.png)
130
+
131
+ **Transform it.** `plot.show(fourier=True)` renders the 2-D Fourier map, exposing periodic
132
+ and self-similar structure — here, a Thue–Morse sequence.
133
+
134
+ ![A Thue–Morse sequence and its Fourier map](https://raw.githubusercontent.com/El3ssar/hilbertplot/main/assets/thue_morse.png)
135
+
136
+ ## Also
137
+
138
+ - `curve.unroll(img)` — read a 2-D array back into 1-D, in curve order
139
+ - `curve.grid(n)` — the eight `generalizing()` curves tile **any** `n×n` square, not just `2ᵏ`
140
+ - `curve.label_map(order)` — the grid of visit indices, as an image
141
+ - `curve.difference_map(order)` — where the curve breaks locality, as a field
142
+ - `plot.draw(colorbar=True)`, or `norm=LogNorm()` for heavy-tailed data
143
+ - `hilbertplot.is_space_filling` / `canonical_form` — check a path yourself; the same tools
144
+ the test suite uses to prove all forty curves are distinct Hamiltonian paths
145
+ - `hilbertplot.clear_cache()` / `cache_info()` — generated curves are memoised; this frees them
146
+ - `hilbertplot.set_cell_limit(n)` — raise the guard that refuses absurdly large orders
147
+
148
+ Every figure above is reproducible: `python examples/readme_figures.py`. More runnable
149
+ demos are in
150
+ [`examples/`](https://github.com/El3ssar/hilbertplot/tree/main/examples);
151
+ the arbitrary-square classification and its impossibility proofs are in the repository's
152
+ `research/quasisquares/`.
153
+
154
+ ## License
155
+
156
+ MIT © Daniel Estevez —
157
+ [LICENSE](https://github.com/El3ssar/hilbertplot/blob/main/LICENSE)
@@ -0,0 +1,124 @@
1
+ # hilbertplot
2
+
3
+ [![PyPI version](https://img.shields.io/pypi/v/hilbertplot.svg)](https://pypi.org/project/hilbertplot/)
4
+ [![CI](https://github.com/El3ssar/hilbertplot/actions/workflows/ci.yml/badge.svg)](https://github.com/El3ssar/hilbertplot/actions/workflows/ci.yml)
5
+ [![Python versions](https://img.shields.io/pypi/pyversions/hilbertplot.svg)](https://pypi.org/project/hilbertplot/)
6
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/El3ssar/hilbertplot/blob/main/LICENSE)
7
+
8
+ The **complete set of 40 two-dimensional Hilbert curves** — and a fast way to plot long
9
+ 1-D data on any of them.
10
+
11
+ Most software knows one Hilbert curve. In fact there are **forty** distinct space-filling
12
+ curves of Hilbert type in two dimensions (up to rotation, reflection and reversion), as
13
+ proved by Estevez-Rams et al., *"Hilbert curves in two dimensions"*, Rev. Cub. Fís. **34**,
14
+ 9 (2017). `hilbertplot` implements all forty with pure-numpy generation and uses them to lay
15
+ long 1-D vectors onto 2-D images.
16
+
17
+ <p align="center">
18
+ <img src="https://raw.githubusercontent.com/El3ssar/hilbertplot/main/assets/curve.png"
19
+ width="440" alt="The Hilbert curve at order 4, drawn as a colour-graded path">
20
+ </p>
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ pip install "hilbertplot[plot]" # drop [plot] for a numpy-only core (no matplotlib)
26
+ ```
27
+
28
+ ## Draw a curve
29
+
30
+ ```python
31
+ import hilbertplot
32
+
33
+ c = hilbertplot.curve(0) # by index 0–39, by name ("Moore"), or by symbol
34
+ c.show(4) # draw order 4 in a window
35
+ pts = c.points(4) # (256, 2) integer lattice points, in visit order
36
+ ```
37
+
38
+ The forty curves come in named groups — `hilbertplot.proper()`, `.improper()`,
39
+ `.homogeneous()`, `.inhomogeneous()`, `.generalizing()` — each of which prints as a table,
40
+ and which compose:
41
+
42
+ ```python
43
+ import hilbertplot
44
+
45
+ print(hilbertplot.proper()) # a table of the six proper curves
46
+ hilbertplot.catalog().closed().names # ['Moore', 'Liu1', 'Improper1', 'Improper4']
47
+ hilbertplot.by_kernels(3, 5).gallery(order=3) # draw a group as a grid of panels
48
+ ```
49
+
50
+ In a Jupyter notebook a `Curve` renders itself — put `hilbertplot.curve("Moore")` in a cell
51
+ and you get the picture, not a `repr`.
52
+
53
+ ## One cell at a time
54
+
55
+ `points(order)` builds all `4**order` cells. When you only need *where step `i` lands*,
56
+ `encode` answers in `O(order)` — so it works at orders no machine could materialise, and it
57
+ does so for **all forty** curves, not just the classic one:
58
+
59
+ ```python
60
+ import hilbertplot
61
+
62
+ c = hilbertplot.curve(0)
63
+ c.encode(5, 2) # (0, 3) — the cell visited at step 5, order 2
64
+ c.decode(0, 3, 2) # 5 — and back again
65
+ c.encode(10**18, 40) # (751054336, 346100736) — a 2**80-cell curve
66
+ ```
67
+
68
+ ## Plot data
69
+
70
+ Walk a curve and drop `data[i]` on the *i*-th cell it visits: a locality-preserving
71
+ 1-D → 2-D map where nearby values stay nearby.
72
+
73
+ ```python
74
+ import numpy as np, hilbertplot
75
+
76
+ plot = hilbertplot.hilbert_plot(0, np.arange(1, 257))
77
+ plot.show("viridis") # a matplotlib colormap name, or a list of colours to blend
78
+ ```
79
+
80
+ ![The same data laid on two different curves](https://raw.githubusercontent.com/El3ssar/hilbertplot/main/assets/data_two_curves.png)
81
+
82
+ *The same numbers on curve 0 and curve 32 — one reason to have all forty.*
83
+
84
+ ## Seeing more than the data
85
+
86
+ **Coarse-grain it.** `plot.show(granularity=4)` replaces each block of values by its mean.
87
+ Below, a binary sequence interleaves stretches of two periodic patterns with identical
88
+ density — invisible in the faithful plot, obvious once averaged.
89
+
90
+ ![Periodic regions revealed by coarse-graining](https://raw.githubusercontent.com/El3ssar/hilbertplot/main/assets/granularity.png)
91
+
92
+ **Find where locality breaks.** `plot.show(difference=True)` marks the cells that are
93
+ neighbours in the plane but far apart along the curve. A higher threshold keeps only the
94
+ worst offenders.
95
+
96
+ ![The difference map at two thresholds](https://raw.githubusercontent.com/El3ssar/hilbertplot/main/assets/difference.png)
97
+
98
+ **Transform it.** `plot.show(fourier=True)` renders the 2-D Fourier map, exposing periodic
99
+ and self-similar structure — here, a Thue–Morse sequence.
100
+
101
+ ![A Thue–Morse sequence and its Fourier map](https://raw.githubusercontent.com/El3ssar/hilbertplot/main/assets/thue_morse.png)
102
+
103
+ ## Also
104
+
105
+ - `curve.unroll(img)` — read a 2-D array back into 1-D, in curve order
106
+ - `curve.grid(n)` — the eight `generalizing()` curves tile **any** `n×n` square, not just `2ᵏ`
107
+ - `curve.label_map(order)` — the grid of visit indices, as an image
108
+ - `curve.difference_map(order)` — where the curve breaks locality, as a field
109
+ - `plot.draw(colorbar=True)`, or `norm=LogNorm()` for heavy-tailed data
110
+ - `hilbertplot.is_space_filling` / `canonical_form` — check a path yourself; the same tools
111
+ the test suite uses to prove all forty curves are distinct Hamiltonian paths
112
+ - `hilbertplot.clear_cache()` / `cache_info()` — generated curves are memoised; this frees them
113
+ - `hilbertplot.set_cell_limit(n)` — raise the guard that refuses absurdly large orders
114
+
115
+ Every figure above is reproducible: `python examples/readme_figures.py`. More runnable
116
+ demos are in
117
+ [`examples/`](https://github.com/El3ssar/hilbertplot/tree/main/examples);
118
+ the arbitrary-square classification and its impossibility proofs are in the repository's
119
+ `research/quasisquares/`.
120
+
121
+ ## License
122
+
123
+ MIT © Daniel Estevez —
124
+ [LICENSE](https://github.com/El3ssar/hilbertplot/blob/main/LICENSE)
@@ -35,7 +35,7 @@ def main() -> None:
35
35
  title="All 40 two-dimensional Hilbert curves (order 3)",
36
36
  save=os.path.join(OUT, "all40.png"),
37
37
  )
38
- hp.curve(0).save(6, os.path.join(OUT, "hilbert_order6.png"))
38
+ hp.curve(0).save(os.path.join(OUT, "hilbert_order6.png"), 6)
39
39
  print(f"wrote plots to {OUT}")
40
40
 
41
41
 
@@ -0,0 +1,128 @@
1
+ """Regenerate the figures shown in the README.
2
+
3
+ python examples/readme_figures.py
4
+
5
+ Writes to assets/:
6
+ * curve.png -- curve 0 at order 4, drawn as a colour-graded path
7
+ * data_two_curves.png -- the numbers 1-256 laid on curve 0 and on curve 32
8
+ * difference.png -- the difference-map overlay at two thresholds
9
+ * thue_morse.png -- a Thue-Morse sequence and its 2-D Fourier map
10
+ * granularity.png -- periodic regions that only appear once coarse-grained
11
+ """
12
+
13
+ import os
14
+
15
+ import numpy as np
16
+
17
+ import hilbertplot as hp
18
+
19
+ OUT = os.path.join(os.path.dirname(__file__), "..", "assets")
20
+
21
+
22
+ def thue_morse(n):
23
+ """The first ``n`` terms of the Thue-Morse sequence."""
24
+ s = [0]
25
+ while len(s) < n:
26
+ s = s + [1 - x for x in s]
27
+ return np.array(s[:n], dtype=float)
28
+
29
+
30
+ def interleaved_periodic(n, seed=3):
31
+ """A random interleaving of two periodic patterns of *equal* density.
32
+
33
+ ``.1001.`` (period 4) and ``.110100.`` (period 6) both have density 1/2, so no
34
+ stretch is brighter than another and the faithful plot is featureless noise. Every
35
+ chunk length is a multiple of 12, which keeps chunk starts aligned to the
36
+ granularity-4 block grid -- so 4-granularity collapses the period-4 stretches to a
37
+ constant (solid blocks) while the period-6 ones keep varying.
38
+ """
39
+ p4, p6 = [1, 0, 0, 1], [1, 1, 0, 1, 0, 0]
40
+ rng = np.random.default_rng(seed)
41
+ seq = []
42
+ while len(seq) < n:
43
+ p = p4 if rng.integers(2) else p6
44
+ length = 12 * int(rng.integers(16, 110))
45
+ seq += (p * (-(-length // len(p))))[:length]
46
+ return np.array(seq[:n], dtype=float)
47
+
48
+
49
+ def main() -> None:
50
+ import matplotlib
51
+
52
+ matplotlib.use("Agg")
53
+ import matplotlib.pyplot as plt
54
+
55
+ os.makedirs(OUT, exist_ok=True)
56
+ plt.rcParams.update({"figure.facecolor": "white", "savefig.facecolor": "white"})
57
+
58
+ def save(fig, name, dpi=140):
59
+ fig.savefig(os.path.join(OUT, name), dpi=dpi, bbox_inches="tight")
60
+ plt.close(fig)
61
+
62
+ # 1) the curve itself, colour-graded along the traversal
63
+ fig, ax = plt.subplots(figsize=(5.2, 5.2))
64
+ hp.curve(0).draw(4, ax=ax, cmap="viridis", title="The Hilbert curve (curve 0), order 4")
65
+ save(fig, "curve.png")
66
+
67
+ # 2) the same data laid on two different curves
68
+ data = np.arange(1, 257)
69
+ fig, axes = plt.subplots(1, 2, figsize=(9.2, 4.9))
70
+ hp.hilbert_plot(0, data).draw("viridis", ax=axes[0], title="curve 0 — Hilbert")
71
+ hp.hilbert_plot(32, data).draw("viridis", ax=axes[1], title="curve 32 — Moore+Liu4 #1")
72
+ fig.suptitle("The same data (1–256) laid on two different curves", fontsize=13, y=1.02)
73
+ save(fig, "data_two_curves.png")
74
+
75
+ # 3) where the curve breaks locality, at two thresholds
76
+ plot = hp.hilbert_plot(0, np.arange(4**5))
77
+ fig, axes = plt.subplots(1, 3, figsize=(12.6, 4.6))
78
+ plot.draw("gray", ax=axes[0], title="Hilbert plot")
79
+ for ax, thr in ((axes[1], 1.5), (axes[2], 0.5)):
80
+ plot.draw(
81
+ "gray",
82
+ ax=ax,
83
+ difference=True,
84
+ difference_threshold=thr,
85
+ difference_color="#ff3b30",
86
+ title=f"difference map (threshold {thr})",
87
+ )
88
+ fig.suptitle(
89
+ "Where the curve breaks locality — a higher threshold marks fewer, worse sites",
90
+ fontsize=13,
91
+ y=1.02,
92
+ )
93
+ save(fig, "difference.png")
94
+
95
+ # 4) a Thue-Morse sequence and its Fourier map (paper-style: light ground)
96
+ plot = hp.hilbert_plot(0, thue_morse(4**8))
97
+ fig, axes = plt.subplots(1, 2, figsize=(9.2, 4.9))
98
+ plot.draw("gray", ax=axes[0], title="Thue–Morse sequence")
99
+ lo, hi = np.percentile(plot.fourier(window=True), [2.0, 99.5])
100
+ plot.draw(
101
+ "gray_r",
102
+ ax=axes[1],
103
+ fourier=True,
104
+ window=True,
105
+ vmin=lo,
106
+ vmax=hi,
107
+ title="its 2-D Fourier map",
108
+ )
109
+ fig.suptitle("A Thue–Morse sequence and its self-similar Fourier map", fontsize=13, y=1.02)
110
+ save(fig, "thue_morse.png")
111
+
112
+ # 5) periodic regions that only show up once you coarse-grain
113
+ plot = hp.hilbert_plot(0, interleaved_periodic(4**7))
114
+ fig, axes = plt.subplots(1, 2, figsize=(9.2, 4.9))
115
+ plot.draw("gray", ax=axes[0], title="faithful — featureless noise")
116
+ plot.draw("gray", ax=axes[1], granularity=4, title="granularity 4 — regions appear")
117
+ fig.suptitle(
118
+ "Periodic regions hidden in a binary sequence, exposed by coarse-graining",
119
+ fontsize=13,
120
+ y=1.02,
121
+ )
122
+ save(fig, "granularity.png")
123
+
124
+ print(f"wrote README figures to {os.path.normpath(OUT)}")
125
+
126
+
127
+ if __name__ == "__main__":
128
+ main()
@@ -38,7 +38,7 @@ def main() -> None:
38
38
  )
39
39
 
40
40
  if args.save:
41
- c.save(args.order, args.save, **style)
41
+ c.save(args.save, args.order, **style)
42
42
  print(f"saved -> {args.save}")
43
43
  else:
44
44
  c.show(args.order, **style)
@@ -4,11 +4,14 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "hilbertplot"
7
- version = "0.1.0"
7
+ version = "0.2.0"
8
8
  description = "The complete set of 40 two-dimensional Hilbert curves, and data plots on them."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
11
- license = { text = "MIT" }
11
+ # PEP 639: a license expression plus the file, instead of the deprecated
12
+ # `license = { text = ... }` + `License ::` classifier pair.
13
+ license = "MIT"
14
+ license-files = ["LICENSE"]
12
15
  authors = [{ name = "Daniel Estevez", email = "kemossabee@gmail.com" }]
13
16
  keywords = [
14
17
  "hilbert-curve",
@@ -19,10 +22,14 @@ keywords = [
19
22
  "visualization",
20
23
  ]
21
24
  classifiers = [
22
- "Development Status :: 3 - Alpha",
25
+ "Development Status :: 4 - Beta",
23
26
  "Intended Audience :: Science/Research",
24
- "License :: OSI Approved :: MIT License",
25
27
  "Programming Language :: Python :: 3",
28
+ "Programming Language :: Python :: 3.10",
29
+ "Programming Language :: Python :: 3.11",
30
+ "Programming Language :: Python :: 3.12",
31
+ "Programming Language :: Python :: 3.13",
32
+ "Programming Language :: Python :: 3.14",
26
33
  "Topic :: Scientific/Engineering :: Mathematics",
27
34
  "Topic :: Scientific/Engineering :: Visualization",
28
35
  "Typing :: Typed",
@@ -31,12 +38,14 @@ dependencies = ["numpy>=1.21"]
31
38
 
32
39
  [project.optional-dependencies]
33
40
  plot = ["matplotlib>=3.5"]
34
- dev = ["matplotlib>=3.5", "pytest>=7.0", "ruff==0.15.13"]
41
+ # dev composes [plot] rather than restating matplotlib, so the two cannot drift.
42
+ dev = ["hilbertplot[plot]", "pytest>=7.0", "ruff==0.15.13"]
35
43
 
36
44
  [project.urls]
37
45
  Homepage = "https://github.com/El3ssar/hilbertplot"
38
46
  Source = "https://github.com/El3ssar/hilbertplot"
39
47
  Issues = "https://github.com/El3ssar/hilbertplot/issues"
48
+ Changelog = "https://github.com/El3ssar/hilbertplot/blob/main/CHANGELOG.md"
40
49
 
41
50
  [tool.hatch.build.targets.wheel]
42
51
  packages = ["src/hilbertplot"]
@@ -49,11 +58,16 @@ include = [
49
58
  "/tests",
50
59
  "/examples/*.py",
51
60
  "/README.md",
61
+ "/CHANGELOG.md",
52
62
  "/LICENSE",
53
63
  ]
54
64
 
55
65
  [tool.pytest.ini_options]
56
66
  testpaths = ["tests"]
67
+ # `-W error::RuntimeWarning` turns any numpy RuntimeWarning raised during the suite into a
68
+ # failure: an "invalid value encountered in divide" during ordinary use is a defect, not
69
+ # noise. (The README's own snippets are executed by tests/test_readme.py.)
70
+ addopts = "-W error::RuntimeWarning"
57
71
 
58
72
  [tool.ruff]
59
73
  line-length = 100
@@ -62,19 +76,15 @@ line-length = 100
62
76
  extend-exclude = ["research/"]
63
77
 
64
78
  [tool.ruff.lint]
65
- select = ["E", "F", "I", "UP", "B", "W"]
79
+ # RUF/SIM/C4/RET/PERF/NPY/PT are all near-zero-noise on this codebase and catch the
80
+ # classes of drift that actually bit us: unsorted __all__, stale noqa, over-broad
81
+ # pytest.raises, and numpy-specific footguns. ANN/D are deliberately NOT enabled --
82
+ # they would flag 390 sites without improving anything.
83
+ select = ["E", "F", "I", "UP", "B", "W", "RUF", "SIM", "C4", "RET", "PERF", "NPY", "PT"]
66
84
 
67
85
  [tool.ruff.lint.per-file-ignores]
68
- # intentional compact hot-loop: allow one-line compound statements
69
- "src/hilbertplot/_core/arbitrary.py" = ["E701", "E702"]
70
- # the paper's parameter name `l` (ambiguous-variable-name)
71
- "src/hilbertplot/_data/mapping.py" = ["E741"]
72
-
73
- [tool.ruff.format]
74
- # arbitrary.py's `_fill` is a deliberately dense, hand-tuned hot loop; keep it
75
- # linted (with E701/E702 ignored above) but exempt from the formatter so its
76
- # compact one-liners survive.
77
- exclude = ["src/hilbertplot/_core/arbitrary.py"]
86
+ # examples use en dashes in figure titles on purpose (RUF001 ambiguous-unicode)
87
+ "examples/*.py" = ["RUF001"]
78
88
 
79
89
  [tool.semantic_release]
80
90
  # pyproject keeps a STATIC version = "0.1.0"; PSR stamps BOTH files in sync on