hilbertplot 0.1.0__tar.gz → 1.0.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 (61) hide show
  1. hilbertplot-1.0.0/CHANGELOG.md +149 -0
  2. hilbertplot-1.0.0/PKG-INFO +255 -0
  3. hilbertplot-1.0.0/README.md +222 -0
  4. hilbertplot-1.0.0/examples/groups.py +14 -0
  5. {hilbertplot-0.1.0 → hilbertplot-1.0.0}/examples/plot_catalog.py +20 -10
  6. hilbertplot-1.0.0/examples/plot_data.py +34 -0
  7. hilbertplot-1.0.0/examples/readme_figures.py +127 -0
  8. hilbertplot-1.0.0/examples/show_curve.py +30 -0
  9. hilbertplot-1.0.0/examples/try_api.py +135 -0
  10. {hilbertplot-0.1.0 → hilbertplot-1.0.0}/pyproject.toml +34 -22
  11. hilbertplot-1.0.0/scripts/build_release.sh +17 -0
  12. hilbertplot-1.0.0/scripts/check_release.py +102 -0
  13. hilbertplot-1.0.0/src/hilbertplot/__init__.py +37 -0
  14. hilbertplot-1.0.0/src/hilbertplot/_api.py +404 -0
  15. hilbertplot-1.0.0/src/hilbertplot/_core/arbitrary.py +378 -0
  16. {hilbertplot-0.1.0 → hilbertplot-1.0.0}/src/hilbertplot/_core/catalog.py +47 -12
  17. hilbertplot-1.0.0/src/hilbertplot/_core/curve.py +915 -0
  18. {hilbertplot-0.1.0 → hilbertplot-1.0.0}/src/hilbertplot/_core/generation.py +25 -4
  19. {hilbertplot-0.1.0 → hilbertplot-1.0.0}/src/hilbertplot/_core/geometry.py +64 -21
  20. hilbertplot-1.0.0/src/hilbertplot/_core/indexing.py +146 -0
  21. hilbertplot-1.0.0/src/hilbertplot/_core/limits.py +100 -0
  22. {hilbertplot-0.1.0 → hilbertplot-1.0.0}/src/hilbertplot/_core/symmetry.py +9 -17
  23. hilbertplot-1.0.0/src/hilbertplot/_data/conversion.py +165 -0
  24. {hilbertplot-0.1.0 → hilbertplot-1.0.0}/src/hilbertplot/_data/mapping.py +106 -29
  25. hilbertplot-1.0.0/src/hilbertplot/_viz/hilbert_plot.py +580 -0
  26. {hilbertplot-0.1.0 → hilbertplot-1.0.0}/src/hilbertplot/_viz/render.py +127 -41
  27. hilbertplot-1.0.0/src/hilbertplot/config.py +10 -0
  28. hilbertplot-1.0.0/src/hilbertplot/curves.py +53 -0
  29. hilbertplot-1.0.0/src/hilbertplot/data.py +15 -0
  30. hilbertplot-1.0.0/tests/conftest.py +40 -0
  31. hilbertplot-1.0.0/tests/test_api.py +243 -0
  32. hilbertplot-1.0.0/tests/test_arbitrary.py +145 -0
  33. {hilbertplot-0.1.0 → hilbertplot-1.0.0}/tests/test_catalog.py +51 -46
  34. hilbertplot-1.0.0/tests/test_conversion.py +161 -0
  35. hilbertplot-1.0.0/tests/test_curve_shapes.py +86 -0
  36. {hilbertplot-0.1.0 → hilbertplot-1.0.0}/tests/test_data.py +43 -41
  37. {hilbertplot-0.1.0 → hilbertplot-1.0.0}/tests/test_geometry.py +25 -8
  38. hilbertplot-1.0.0/tests/test_indexing.py +162 -0
  39. hilbertplot-1.0.0/tests/test_performance.py +109 -0
  40. hilbertplot-1.0.0/tests/test_plotting.py +281 -0
  41. hilbertplot-1.0.0/tests/test_readme.py +62 -0
  42. hilbertplot-1.0.0/tests/test_robustness.py +597 -0
  43. {hilbertplot-0.1.0 → hilbertplot-1.0.0}/tests/test_symmetry.py +1 -1
  44. {hilbertplot-0.1.0 → hilbertplot-1.0.0}/tests/test_words.py +10 -10
  45. hilbertplot-0.1.0/PKG-INFO +0 -285
  46. hilbertplot-0.1.0/README.md +0 -257
  47. hilbertplot-0.1.0/examples/groups.py +0 -29
  48. hilbertplot-0.1.0/examples/plot_data.py +0 -119
  49. hilbertplot-0.1.0/examples/show_curve.py +0 -48
  50. hilbertplot-0.1.0/src/hilbertplot/__init__.py +0 -69
  51. hilbertplot-0.1.0/src/hilbertplot/_core/arbitrary.py +0 -114
  52. hilbertplot-0.1.0/src/hilbertplot/_core/curve.py +0 -604
  53. hilbertplot-0.1.0/src/hilbertplot/_viz/hilbert_plot.py +0 -413
  54. hilbertplot-0.1.0/tests/test_arbitrary.py +0 -95
  55. hilbertplot-0.1.0/tests/test_plotting.py +0 -161
  56. {hilbertplot-0.1.0 → hilbertplot-1.0.0}/.gitignore +0 -0
  57. {hilbertplot-0.1.0 → hilbertplot-1.0.0}/LICENSE +0 -0
  58. {hilbertplot-0.1.0 → hilbertplot-1.0.0}/src/hilbertplot/_core/__init__.py +0 -0
  59. {hilbertplot-0.1.0 → hilbertplot-1.0.0}/src/hilbertplot/_data/__init__.py +0 -0
  60. {hilbertplot-0.1.0 → hilbertplot-1.0.0}/src/hilbertplot/_viz/__init__.py +0 -0
  61. {hilbertplot-0.1.0 → hilbertplot-1.0.0}/src/hilbertplot/py.typed +0 -0
@@ -0,0 +1,149 @@
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
+ ## Local API redesign (unreleased)
11
+
12
+ - Make array conversion lossless for dtypes, large integers, complex values and
13
+ masks; keep padding masked where NaN would require lossy integer conversion.
14
+ - Convert supported square and quasisquare arrays to flat 1-D sequences,
15
+ retaining RGB/RGBA channel order; restore with `to_grid(..., shape=original.shape)`.
16
+ Reject general rectangles and unsupported curve/shape combinations without a
17
+ fallback. Add explicit `from_grid(..., length=...)` prefix handling.
18
+ - Run the release build hook after semantic version stamping to regenerate and
19
+ commit `uv.lock` automatically. Verify title-driven patch/minor/breaking releases
20
+ in disposable repositories in CI; versions remain semantic-release-owned.
21
+ - Allow `plot_curve(..., shape=(rows, columns))` to draw supported arbitrary
22
+ squares and quasisquares directly as lines. Select a connected quasisquare
23
+ orientation internally. `curves.grid` also accepts the same shape pair.
24
+ - Use `hp.data.to_grid` / `from_grid` and `hp.curves.index_to_xy` /
25
+ `xy_to_index` for explicit mapping directions. Remove the former `image`,
26
+ `unroll`, `encode`, and `decode` aliases from these namespaces.
27
+ - Keep top-level completion to `plot`, `plot_curve`, `gallery`, `curves`, `data`,
28
+ and `config`. Move metadata/geometry, array operations, and resource controls
29
+ into their respective module namespaces; remove their top-level aliases.
30
+ - Add `examples/try_api.py` to exercise the complete function-based interface.
31
+ - Add data-first `plot(data, curve=...)` and `plot_curve(curve, order=...)`.
32
+ Each call returns a standard Matplotlib Figure, without opening a window.
33
+ - Keep `fourier=False` and `difference=False` as mutually exclusive flags on
34
+ `plot`; difference mode displays the full geometric field instead of a threshold overlay.
35
+ - Keep custom classes internal. Remove the `Curve`, `CurveList`, `HilbertPlot`,
36
+ and `hilbert_plot` exports. Curve metadata and groups return plain dicts/lists.
37
+ - Expose array, coordinate, and indexing operations as functions; retain the
38
+ numpy-only computational path. See the README migration table for replacements.
39
+ - Default new data functions to power-of-two padding; require explicit truncation
40
+ when an order is too small. Reject ambiguous multidimensional/complex input,
41
+ handle masked integer data, and report invalid Fourier inputs without warnings.
42
+ - Normalize the memory regression test's RSS units on macOS as well as Linux.
43
+
44
+ These changes are under local review and have not been released.
45
+
46
+ ## Migrating from 0.1.0
47
+
48
+ `hilbertplot` is on `0.x`, where [semver §4](https://semver.org/#spec-item-4) allows the
49
+ interface to change in a minor release. Two spellings changed; both fail loudly rather
50
+ than silently, and nothing else in the 0.1.0 surface was removed.
51
+
52
+ | 0.1.0 | now | why |
53
+ | --- | --- | --- |
54
+ | `curve.plot(order)` | `curve.draw(order)` | one verb for "render onto axes", shared with `HilbertPlot.draw` |
55
+ | `curve.save(order, path)` | `curve.save(path, order)` | matches `HilbertPlot.save(path, ...)`; the old order raises a `TypeError` naming the fix |
56
+ | `repr(curve)` → `'Hilbert'` | `repr(curve)` → `'<Curve 0H Hilbert (proper)>'` | `repr` is now unambiguous; `str(curve)` is still `'Hilbert'` |
57
+
58
+ Behaviour that was buggy and is now stricter: `curve(True)` and `curve(3.0)` raise
59
+ `TypeError` instead of silently meaning `curve(1)` / `curve(3)`; an out-of-range index
60
+ raises `IndexError` rather than `KeyError` (both are `LookupError`); `plot.data` is
61
+ genuinely immutable rather than merely flagged; and `image(order=..., fit=...)` now
62
+ validates `fit` instead of ignoring it.
63
+
64
+ <!-- version list -->
65
+
66
+ ## v1.0.0 (2026-11-01)
67
+
68
+ ### Features
69
+
70
+ - Simplify plotting and add lossless curve conversions
71
+ ([`f45f2a6`](https://github.com/El3ssar/hilbertplot/commit/f45f2a6219bfc1f6259d72a64d01c8b19ae7d28e))
72
+
73
+ ### Breaking Changes
74
+
75
+ - Replace public curve/plot objects and top-level numerical helpers with function-based plotting and
76
+ namespaced to_grid/from_grid/index_to_xy/xy_to_index operations. Data plots default to
77
+ power-of-two padding; difference=True renders the full locality-loss field. General rectangles are
78
+ intentionally unsupported.
79
+
80
+
81
+ ## v0.2.0 (2026-07-25)
82
+
83
+ ### Bug Fixes
84
+
85
+ - Close the gaps found by the second review round
86
+ ([`9df64f4`](https://github.com/El3ssar/hilbertplot/commit/9df64f463b2619d0d19ca7d3d818a16e2c25d7d8))
87
+
88
+ - Keep the new guards from costing more than they save
89
+ ([`bbaa464`](https://github.com/El3ssar/hilbertplot/commit/bbaa4646af8ca401aa27174277cbae22910cd34f))
90
+
91
+ ### Continuous Integration
92
+
93
+ - Bump actions, exempt dependabot from title check, curb ruff bumps
94
+ ([`aa74d78`](https://github.com/El3ssar/hilbertplot/commit/aa74d7810fa542e259dfcee77c23ad9aa491374a))
95
+
96
+ - Stop Dependabot from bumping ruff
97
+ ([`f639fc0`](https://github.com/El3ssar/hilbertplot/commit/f639fc0aa5ab67c615bf31470cfd1bd10f5248b5))
98
+
99
+ - Stop the floors job re-resolving the pins it exists to test
100
+ ([`106b08f`](https://github.com/El3ssar/hilbertplot/commit/106b08f8bedcf0d6c2c0ff023a60c2ff020265e4))
101
+
102
+ ### Documentation
103
+
104
+ - Add status badges and tidy the 0.1.0 changelog entry
105
+ ([`7d967ca`](https://github.com/El3ssar/hilbertplot/commit/7d967caae998903d0885a81c4801b50b974aef62))
106
+
107
+ - Illustrate the README with generated figures
108
+ ([`0f245b9`](https://github.com/El3ssar/hilbertplot/commit/0f245b941fbb933141b10d60ee16157c463b8501))
109
+
110
+ - Streamline the README to install + two examples
111
+ ([`fb53eab`](https://github.com/El3ssar/hilbertplot/commit/fb53eab38129a8e60c42160d5bfa3959677a37db))
112
+
113
+ ### Features
114
+
115
+ - Make the arbitrary-square path fast, harden the public surface, add encode/decode
116
+ ([`a6ce0da`](https://github.com/El3ssar/hilbertplot/commit/a6ce0da3463acaacc6c69d349a1ef06804a817f4))
117
+
118
+
119
+ ## v0.1.0 (2026-07-25)
120
+
121
+ First public release — the complete set of 40 two-dimensional Hilbert curves,
122
+ with fast numpy generation and data-visualisation tooling.
123
+
124
+ ### Added
125
+ - The complete set of **40** two-dimensional Hilbert curves (Estevez-Rams et al., 2017):
126
+ `curve(i)` by index, name or symbol; `points(order)`, `word(order)`; family groups
127
+ `homogeneous` / `inhomogeneous` / `proper` / `improper` / `closed` / `by_kernels`,
128
+ returned as a `CurveList` that prints as a table.
129
+ - Fast numpy stroke-code generation (no per-stroke Python loop); order-10 curve (~1M
130
+ points) in tens of milliseconds.
131
+ - Curve rendering: `curve.show/save/plot(order)` with automatic line→fill mode by order,
132
+ colour-graded traversal, and `gallery()` for grids of curves.
133
+ - **Data visualisation** ("Hilbert plots", Estevez-Rams et al., 2015): `hilbert_plot`
134
+ / `curve.plot_data` return a `HilbertPlot` that maps a 1-D vector onto 2-D on demand,
135
+ with colormap-by-name or colour-list interpolation, the `l`-granularity transform,
136
+ and `pad` / `truncate` fits.
137
+ - **Difference map** overlay (`difference=True`, tunable threshold) marking where a curve
138
+ breaks locality (Pérez-Demydenko et al., 2014); `curve.difference_map(order)`.
139
+ - **2-D Fourier map** (`fourier=True`; `HilbertPlot.fourier()`) revealing hidden periodic
140
+ and self-similar structure.
141
+ - **`unroll`**: read a 2-D square array back into a 1-D vector in curve order.
142
+ - **Arbitrary `n x n` squares** for the eight curves that support it
143
+ (`curve.grid(n)`, `curve.generalizes`, `hilbertplot.generalizing()`); the data mapping
144
+ square-fits to a tight `ceil(sqrt(N))` grid by default for those curves. The full
145
+ feasibility classification and impossibility proofs are in `research/quasisquares/`.
146
+
147
+ ### Notes
148
+ - Requires Python 3.10+. `numpy` is the only runtime dependency; `matplotlib` is the
149
+ optional `[plot]` extra.
@@ -0,0 +1,255 @@
1
+ Metadata-Version: 2.5
2
+ Name: hilbertplot
3
+ Version: 1.0.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)](LICENSE)
40
+
41
+ Plot long numeric sequences on any of **40 two-dimensional Hilbert-type curves**.
42
+ Choose a curve by number, label, or paper symbol. The interface is functions returning
43
+ ordinary arrays, dictionaries, lists, and Matplotlib figures; custom classes stay internal.
44
+
45
+ Top-level completion has just six entries: `plot`, `plot_curve`, `gallery`,
46
+ `curves`, `data`, and `config`. Specialist functions live under those three namespaces:
47
+ `hp.curves` for the catalog and geometry, `hp.data` for arrays, and `hp.config` for
48
+ optional cache and allocation controls.
49
+
50
+ ## Install
51
+
52
+ ```bash
53
+ pip install "hilbertplot[plot]" # omit [plot] for numpy-only operations
54
+ ```
55
+
56
+ ## Plot your data
57
+
58
+ ```python
59
+ import numpy as np
60
+ import matplotlib.pyplot as plt
61
+ import hilbertplot as hp
62
+
63
+ data = np.sin(np.arange(1000) / 10)
64
+ fig = hp.plot(data) # standard Hilbert curve
65
+ fig = hp.plot(data, curve=12) # any curve index 0..39
66
+ fig = hp.plot(data, curve="Moore") # or a label; "12H" also works
67
+ fig.savefig("data.png", dpi=200)
68
+ plt.show() # display figures in a script
69
+ ```
70
+
71
+ Each call makes **one plot** and returns a Matplotlib `Figure`. No window is opened
72
+ implicitly. Use `ax=` to draw on your own Matplotlib axes. Figures remain open until
73
+ you close them with `plt.close(fig)`.
74
+
75
+ ## Fourier, difference, and granularity maps
76
+
77
+ ```python
78
+ fig = hp.plot(data, fourier=True) # centred 2-D Fourier magnitude
79
+ fig = hp.plot(data, difference=True) # full locality-loss field
80
+ fig = hp.plot(data, granularity=16) # average consecutive blocks of 16
81
+ fig = hp.plot(data, fourier=True, granularity=16, window=True)
82
+ ```
83
+
84
+ `fourier` and `difference` are boolean flags, both off by default (`False` / `0`).
85
+ They are **mutually exclusive**: setting both raises `ValueError`.
86
+
87
+ - **Fourier:** transforms the mapped 2-D image, not the original 1-D signal. Defaults
88
+ to `log1p(abs(FFT))` with the mean subtracted. Use `log=False` for raw magnitude,
89
+ `remove_dc=False` to retain the mean, or `window=True` for a Hann window.
90
+ - **Difference:** displays the mean absolute visit-index distance to each cell's up-to-eight
91
+ neighbours. It describes the curve's geometry, independently of data values.
92
+ - **Granularity:** replaces each consecutive sequence block by copies of its mean.
93
+ The sequence and image retain their sizes; this is not image downsampling. It affects
94
+ the data and its Fourier transform. Geometry-based difference maps stay unchanged.
95
+
96
+ ## Draw a curve
97
+
98
+ ```python
99
+ fig = hp.plot_curve(12, order=4)
100
+ fig = hp.plot_curve("Moore", order=4, color="steelblue")
101
+ fig = hp.plot_curve(0, order=8, mode="fill")
102
+
103
+ # Arbitrary square and quasisquare: draw the path itself, with no data.
104
+ fig = hp.plot_curve(3, shape=(15, 15), mode="line")
105
+ fig = hp.plot_curve(3, shape=(15, 16), mode="line")
106
+ fig = hp.plot_curve(3, shape=(16, 15), mode="line")
107
+ ```
108
+
109
+ `order=k` visits `4**k` cells in a `2**k` square. Curve drawing automatically switches
110
+ from coloured lines to a filled visit-order image at order 8. Explicit `mode="line"`
111
+ or `"fill"`, `linewidth`, and endpoint markers are also available.
112
+
113
+ `shape=(rows, columns)` selects explicit dimensions instead of an order. A
114
+ quasisquare's sides differ by one. Arbitrary squares and quasisquares require one
115
+ of curves **0, 3, 4, 5, 24, 29, 30, 31**. Quasisquare orientation is selected
116
+ automatically to keep every step connected. Use either `shape` or `order`, not both.
117
+ `hp.curves.grid((rows, columns), curve=...)` returns the same path as coordinates.
118
+
119
+ ## Colour and fitting
120
+
121
+ ```python
122
+ from matplotlib.colors import LogNorm
123
+
124
+ fig = hp.plot(np.arange(1, 1001), norm=LogNorm(), colorbar=True)
125
+ fig = hp.plot(data, cmap=["navy", "white", "firebrick"], vmin=-1, vmax=1)
126
+ compact = hp.data.to_grid(data, fit="square") # supported curves only
127
+ ```
128
+
129
+ `hp.plot()` and `hp.data.to_grid()` share these layout choices:
130
+
131
+ | `fit` | Behaviour |
132
+ | --- | --- |
133
+ | `"pad"` (default) | Smallest enclosing power-of-two square; works with every curve |
134
+ | `"square"` | Tight `ceil(sqrt(N))` square; only the eight generalizing curves |
135
+ | `"auto"` | Tight square where supported, otherwise power-of-two padding |
136
+ | `"truncate"` | Largest power-of-two square inside the data; explicitly discards the tail |
137
+
138
+ Padding is NaN and appears transparent. `order=` fixes the grid; if it is too small,
139
+ the call raises unless you explicitly choose `fit="truncate"`. Pass either `norm=`
140
+ or `vmin`/`vmax`, not both. Extra Matplotlib image options such as `alpha` are supported.
141
+
142
+ ## Arrays and index maths (no Matplotlib needed)
143
+
144
+ ```python
145
+ img = hp.data.to_grid(data, curve=12)
146
+ spectrum = hp.data.fourier(data, curve=12, window=True)
147
+ recovered = hp.data.from_grid(img, curve=12, length=len(data))
148
+ labels = hp.curves.label_map(12, side=img.shape[0])
149
+ differences = hp.curves.difference_map(12, side=img.shape[0])
150
+
151
+ pts = hp.curves.points(12, order=4) # (256, 2) array of (x, y) cells
152
+ word = hp.curves.word(12, order=4) # u/d/l/r strokes
153
+ arbitrary = hp.curves.grid(100, curve=0) # arbitrary square, supported curves only
154
+ xy = hp.curves.index_to_xy(10**18, 40, curve=12) # O(order), no full-grid allocation
155
+ assert hp.curves.xy_to_index(*xy, 40, curve=12) == 10**18
156
+ assert hp.curves.is_space_filling(pts, 16)
157
+ key = hp.curves.canonical_form(pts) # compare paths up to symmetry/reversal
158
+ ```
159
+
160
+ Images are indexed `[y, x]`; coordinates are `(x, y)`. Plotting and Fourier maps
161
+ accept real, nonempty scalar sequences; choose magnitude, phase, real or imaginary
162
+ values explicitly for complex data. Plotting rejects infinities. NaNs propagate
163
+ through granularity block means; Fourier maps mean-fill missing cells and require
164
+ at least one finite value.
165
+
166
+ Array conversion is separate from display: `to_grid()` / `from_grid()` preserve
167
+ numeric dtypes, complex values, large integers, and masks. With no padding or
168
+ averaging, conversion is exact. Default NaN padding keeps floating/complex dtypes;
169
+ integer and boolean padding uses a NumPy masked array to retain precision. Use
170
+ `fill=0` for ordinary integer padding. `length=` explicitly selects a prefix without
171
+ deleting legitimate NaNs or masked values.
172
+
173
+ ```python
174
+ # A square RGB image becomes a truly 1-D sequence. Each pixel's channels
175
+ # stay consecutive; use the full original shape when reconstructing.
176
+ rgb = np.arange(16 * 16 * 3, dtype=np.uint16).reshape(16, 16, 3)
177
+ sequence = hp.data.from_grid(rgb, curve=12)
178
+ assert sequence.ndim == 1 and sequence.size == rgb.size
179
+ restored = hp.data.to_grid(sequence, curve=12, shape=rgb.shape)
180
+ np.testing.assert_array_equal(restored, rgb)
181
+ assert restored.dtype == rgb.dtype
182
+
183
+ # The same functions use exact quasisquare paths for supported curves.
184
+ square_or_quasisquare = np.arange(15 * 16).reshape(15, 16)
185
+ sequence = hp.data.from_grid(square_or_quasisquare, curve=3)
186
+ restored = hp.data.to_grid(sequence, curve=3, shape=square_or_quasisquare.shape)
187
+ np.testing.assert_array_equal(restored, square_or_quasisquare)
188
+ ```
189
+
190
+ Conversion uses the same connected path as `plot_curve(shape=...)`. Spatial
191
+ shapes must be supported squares or quasisquares; general rectangles are not
192
+ supported. Unsupported shapes raise an error without padding or cropping them
193
+ into a different traversal. `shape=` specifies the output's original dimensions;
194
+ it cannot be combined with `order=`. Converting a grid with another curve gives a
195
+ reversible reordering; keep all entries, including padding, when reversing it.
196
+
197
+ ## Discover curves
198
+
199
+ ```python
200
+ info = hp.curves.info(12) # ordinary metadata dictionary
201
+ print(info["name"], info["index"], info["kernels"])
202
+ print([(c["index"], c["name"]) for c in hp.curves.catalog()])
203
+ print([c["index"] for c in hp.curves.generalizing()])
204
+ fig = hp.gallery(hp.curves.by_kernels(3, 5), order=3)
205
+ ```
206
+
207
+ Under `hp.curves`, `catalog()` lists all 40 curves: 6 `proper()`, 6 `improper()` (together 12
208
+ `homogeneous()`), and 28 `inhomogeneous()`. Groups return ordinary lists of metadata
209
+ dictionaries. `closed()` lists the four loops; `generalizing()` lists curves
210
+ 0, 3, 4, 5, 24, 29, 30, and 31, which support arbitrary square sizes.
211
+
212
+ Under `hp.config`, `clear_cache()` releases cached generation data, and `cache_info()`
213
+ reports its use.
214
+ `cell_limit()` / `set_cell_limit(n)` control the allocation guard. Grid size grows
215
+ exponentially with order; the default limit is a ceiling, not a memory reservation.
216
+
217
+ More runnable examples are in [examples/](examples/). The curve classification follows
218
+ Estevez-Rams et al., *Hilbert curves in two dimensions*, Rev. Cub. Fís. **34**, 9 (2017).
219
+ The arbitrary-square research is in [research/quasisquares/](research/quasisquares/).
220
+
221
+ ## Try all the features
222
+
223
+ The [complete runnable example](examples/try_api.py) draws separate figures for the
224
+ curve, data, coarse-grained data, Fourier and difference maps, colour options, fitting
225
+ modes, and a gallery. It also checks numeric round trips, geometry, indexing and groups.
226
+
227
+ ```bash
228
+ python examples/try_api.py
229
+ # Save figures and run all checks without opening windows:
230
+ MPLBACKEND=Agg python examples/try_api.py --save-only
231
+ ```
232
+
233
+ Figures are saved in `examples/output/try_api/`.
234
+
235
+ ## Local API migration
236
+
237
+ This working-tree redesign replaces the object-based 0.2.0 interface:
238
+
239
+ | Previous | New |
240
+ | --- | --- |
241
+ | `hilbert_plot(12, data).draw()` | `plot(data, curve=12)` |
242
+ | `curve(12).draw(4)` | `plot_curve(12, order=4)` |
243
+ | `plot.save(path)` | `fig = plot(data); fig.savefig(path)` |
244
+ | `curve(12).points(4)` | `hp.curves.points(12, order=4)` |
245
+ | `curve(12).unroll(img)` | `hp.data.from_grid(img, curve=12)` |
246
+ | `curve(12).name` | `hp.curves.info(12)["name"]` |
247
+ | `proper().names` | `[c["name"] for c in hp.curves.proper()]` |
248
+
249
+ The old `Curve`, `CurveList`, `HilbertPlot`, and `hilbert_plot` exports are removed.
250
+ `difference=True` now renders the complete difference map. Rendering defaults to
251
+ power-of-two padding. These are intentional interface changes under local review.
252
+
253
+ ## License
254
+
255
+ MIT © Daniel Estevez — [LICENSE](LICENSE).
@@ -0,0 +1,222 @@
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)](LICENSE)
7
+
8
+ Plot long numeric sequences on any of **40 two-dimensional Hilbert-type curves**.
9
+ Choose a curve by number, label, or paper symbol. The interface is functions returning
10
+ ordinary arrays, dictionaries, lists, and Matplotlib figures; custom classes stay internal.
11
+
12
+ Top-level completion has just six entries: `plot`, `plot_curve`, `gallery`,
13
+ `curves`, `data`, and `config`. Specialist functions live under those three namespaces:
14
+ `hp.curves` for the catalog and geometry, `hp.data` for arrays, and `hp.config` for
15
+ optional cache and allocation controls.
16
+
17
+ ## Install
18
+
19
+ ```bash
20
+ pip install "hilbertplot[plot]" # omit [plot] for numpy-only operations
21
+ ```
22
+
23
+ ## Plot your data
24
+
25
+ ```python
26
+ import numpy as np
27
+ import matplotlib.pyplot as plt
28
+ import hilbertplot as hp
29
+
30
+ data = np.sin(np.arange(1000) / 10)
31
+ fig = hp.plot(data) # standard Hilbert curve
32
+ fig = hp.plot(data, curve=12) # any curve index 0..39
33
+ fig = hp.plot(data, curve="Moore") # or a label; "12H" also works
34
+ fig.savefig("data.png", dpi=200)
35
+ plt.show() # display figures in a script
36
+ ```
37
+
38
+ Each call makes **one plot** and returns a Matplotlib `Figure`. No window is opened
39
+ implicitly. Use `ax=` to draw on your own Matplotlib axes. Figures remain open until
40
+ you close them with `plt.close(fig)`.
41
+
42
+ ## Fourier, difference, and granularity maps
43
+
44
+ ```python
45
+ fig = hp.plot(data, fourier=True) # centred 2-D Fourier magnitude
46
+ fig = hp.plot(data, difference=True) # full locality-loss field
47
+ fig = hp.plot(data, granularity=16) # average consecutive blocks of 16
48
+ fig = hp.plot(data, fourier=True, granularity=16, window=True)
49
+ ```
50
+
51
+ `fourier` and `difference` are boolean flags, both off by default (`False` / `0`).
52
+ They are **mutually exclusive**: setting both raises `ValueError`.
53
+
54
+ - **Fourier:** transforms the mapped 2-D image, not the original 1-D signal. Defaults
55
+ to `log1p(abs(FFT))` with the mean subtracted. Use `log=False` for raw magnitude,
56
+ `remove_dc=False` to retain the mean, or `window=True` for a Hann window.
57
+ - **Difference:** displays the mean absolute visit-index distance to each cell's up-to-eight
58
+ neighbours. It describes the curve's geometry, independently of data values.
59
+ - **Granularity:** replaces each consecutive sequence block by copies of its mean.
60
+ The sequence and image retain their sizes; this is not image downsampling. It affects
61
+ the data and its Fourier transform. Geometry-based difference maps stay unchanged.
62
+
63
+ ## Draw a curve
64
+
65
+ ```python
66
+ fig = hp.plot_curve(12, order=4)
67
+ fig = hp.plot_curve("Moore", order=4, color="steelblue")
68
+ fig = hp.plot_curve(0, order=8, mode="fill")
69
+
70
+ # Arbitrary square and quasisquare: draw the path itself, with no data.
71
+ fig = hp.plot_curve(3, shape=(15, 15), mode="line")
72
+ fig = hp.plot_curve(3, shape=(15, 16), mode="line")
73
+ fig = hp.plot_curve(3, shape=(16, 15), mode="line")
74
+ ```
75
+
76
+ `order=k` visits `4**k` cells in a `2**k` square. Curve drawing automatically switches
77
+ from coloured lines to a filled visit-order image at order 8. Explicit `mode="line"`
78
+ or `"fill"`, `linewidth`, and endpoint markers are also available.
79
+
80
+ `shape=(rows, columns)` selects explicit dimensions instead of an order. A
81
+ quasisquare's sides differ by one. Arbitrary squares and quasisquares require one
82
+ of curves **0, 3, 4, 5, 24, 29, 30, 31**. Quasisquare orientation is selected
83
+ automatically to keep every step connected. Use either `shape` or `order`, not both.
84
+ `hp.curves.grid((rows, columns), curve=...)` returns the same path as coordinates.
85
+
86
+ ## Colour and fitting
87
+
88
+ ```python
89
+ from matplotlib.colors import LogNorm
90
+
91
+ fig = hp.plot(np.arange(1, 1001), norm=LogNorm(), colorbar=True)
92
+ fig = hp.plot(data, cmap=["navy", "white", "firebrick"], vmin=-1, vmax=1)
93
+ compact = hp.data.to_grid(data, fit="square") # supported curves only
94
+ ```
95
+
96
+ `hp.plot()` and `hp.data.to_grid()` share these layout choices:
97
+
98
+ | `fit` | Behaviour |
99
+ | --- | --- |
100
+ | `"pad"` (default) | Smallest enclosing power-of-two square; works with every curve |
101
+ | `"square"` | Tight `ceil(sqrt(N))` square; only the eight generalizing curves |
102
+ | `"auto"` | Tight square where supported, otherwise power-of-two padding |
103
+ | `"truncate"` | Largest power-of-two square inside the data; explicitly discards the tail |
104
+
105
+ Padding is NaN and appears transparent. `order=` fixes the grid; if it is too small,
106
+ the call raises unless you explicitly choose `fit="truncate"`. Pass either `norm=`
107
+ or `vmin`/`vmax`, not both. Extra Matplotlib image options such as `alpha` are supported.
108
+
109
+ ## Arrays and index maths (no Matplotlib needed)
110
+
111
+ ```python
112
+ img = hp.data.to_grid(data, curve=12)
113
+ spectrum = hp.data.fourier(data, curve=12, window=True)
114
+ recovered = hp.data.from_grid(img, curve=12, length=len(data))
115
+ labels = hp.curves.label_map(12, side=img.shape[0])
116
+ differences = hp.curves.difference_map(12, side=img.shape[0])
117
+
118
+ pts = hp.curves.points(12, order=4) # (256, 2) array of (x, y) cells
119
+ word = hp.curves.word(12, order=4) # u/d/l/r strokes
120
+ arbitrary = hp.curves.grid(100, curve=0) # arbitrary square, supported curves only
121
+ xy = hp.curves.index_to_xy(10**18, 40, curve=12) # O(order), no full-grid allocation
122
+ assert hp.curves.xy_to_index(*xy, 40, curve=12) == 10**18
123
+ assert hp.curves.is_space_filling(pts, 16)
124
+ key = hp.curves.canonical_form(pts) # compare paths up to symmetry/reversal
125
+ ```
126
+
127
+ Images are indexed `[y, x]`; coordinates are `(x, y)`. Plotting and Fourier maps
128
+ accept real, nonempty scalar sequences; choose magnitude, phase, real or imaginary
129
+ values explicitly for complex data. Plotting rejects infinities. NaNs propagate
130
+ through granularity block means; Fourier maps mean-fill missing cells and require
131
+ at least one finite value.
132
+
133
+ Array conversion is separate from display: `to_grid()` / `from_grid()` preserve
134
+ numeric dtypes, complex values, large integers, and masks. With no padding or
135
+ averaging, conversion is exact. Default NaN padding keeps floating/complex dtypes;
136
+ integer and boolean padding uses a NumPy masked array to retain precision. Use
137
+ `fill=0` for ordinary integer padding. `length=` explicitly selects a prefix without
138
+ deleting legitimate NaNs or masked values.
139
+
140
+ ```python
141
+ # A square RGB image becomes a truly 1-D sequence. Each pixel's channels
142
+ # stay consecutive; use the full original shape when reconstructing.
143
+ rgb = np.arange(16 * 16 * 3, dtype=np.uint16).reshape(16, 16, 3)
144
+ sequence = hp.data.from_grid(rgb, curve=12)
145
+ assert sequence.ndim == 1 and sequence.size == rgb.size
146
+ restored = hp.data.to_grid(sequence, curve=12, shape=rgb.shape)
147
+ np.testing.assert_array_equal(restored, rgb)
148
+ assert restored.dtype == rgb.dtype
149
+
150
+ # The same functions use exact quasisquare paths for supported curves.
151
+ square_or_quasisquare = np.arange(15 * 16).reshape(15, 16)
152
+ sequence = hp.data.from_grid(square_or_quasisquare, curve=3)
153
+ restored = hp.data.to_grid(sequence, curve=3, shape=square_or_quasisquare.shape)
154
+ np.testing.assert_array_equal(restored, square_or_quasisquare)
155
+ ```
156
+
157
+ Conversion uses the same connected path as `plot_curve(shape=...)`. Spatial
158
+ shapes must be supported squares or quasisquares; general rectangles are not
159
+ supported. Unsupported shapes raise an error without padding or cropping them
160
+ into a different traversal. `shape=` specifies the output's original dimensions;
161
+ it cannot be combined with `order=`. Converting a grid with another curve gives a
162
+ reversible reordering; keep all entries, including padding, when reversing it.
163
+
164
+ ## Discover curves
165
+
166
+ ```python
167
+ info = hp.curves.info(12) # ordinary metadata dictionary
168
+ print(info["name"], info["index"], info["kernels"])
169
+ print([(c["index"], c["name"]) for c in hp.curves.catalog()])
170
+ print([c["index"] for c in hp.curves.generalizing()])
171
+ fig = hp.gallery(hp.curves.by_kernels(3, 5), order=3)
172
+ ```
173
+
174
+ Under `hp.curves`, `catalog()` lists all 40 curves: 6 `proper()`, 6 `improper()` (together 12
175
+ `homogeneous()`), and 28 `inhomogeneous()`. Groups return ordinary lists of metadata
176
+ dictionaries. `closed()` lists the four loops; `generalizing()` lists curves
177
+ 0, 3, 4, 5, 24, 29, 30, and 31, which support arbitrary square sizes.
178
+
179
+ Under `hp.config`, `clear_cache()` releases cached generation data, and `cache_info()`
180
+ reports its use.
181
+ `cell_limit()` / `set_cell_limit(n)` control the allocation guard. Grid size grows
182
+ exponentially with order; the default limit is a ceiling, not a memory reservation.
183
+
184
+ More runnable examples are in [examples/](examples/). The curve classification follows
185
+ Estevez-Rams et al., *Hilbert curves in two dimensions*, Rev. Cub. Fís. **34**, 9 (2017).
186
+ The arbitrary-square research is in [research/quasisquares/](research/quasisquares/).
187
+
188
+ ## Try all the features
189
+
190
+ The [complete runnable example](examples/try_api.py) draws separate figures for the
191
+ curve, data, coarse-grained data, Fourier and difference maps, colour options, fitting
192
+ modes, and a gallery. It also checks numeric round trips, geometry, indexing and groups.
193
+
194
+ ```bash
195
+ python examples/try_api.py
196
+ # Save figures and run all checks without opening windows:
197
+ MPLBACKEND=Agg python examples/try_api.py --save-only
198
+ ```
199
+
200
+ Figures are saved in `examples/output/try_api/`.
201
+
202
+ ## Local API migration
203
+
204
+ This working-tree redesign replaces the object-based 0.2.0 interface:
205
+
206
+ | Previous | New |
207
+ | --- | --- |
208
+ | `hilbert_plot(12, data).draw()` | `plot(data, curve=12)` |
209
+ | `curve(12).draw(4)` | `plot_curve(12, order=4)` |
210
+ | `plot.save(path)` | `fig = plot(data); fig.savefig(path)` |
211
+ | `curve(12).points(4)` | `hp.curves.points(12, order=4)` |
212
+ | `curve(12).unroll(img)` | `hp.data.from_grid(img, curve=12)` |
213
+ | `curve(12).name` | `hp.curves.info(12)["name"]` |
214
+ | `proper().names` | `[c["name"] for c in hp.curves.proper()]` |
215
+
216
+ The old `Curve`, `CurveList`, `HilbertPlot`, and `hilbert_plot` exports are removed.
217
+ `difference=True` now renders the complete difference map. Rendering defaults to
218
+ power-of-two padding. These are intentional interface changes under local review.
219
+
220
+ ## License
221
+
222
+ MIT © Daniel Estevez — [LICENSE](LICENSE).
@@ -0,0 +1,14 @@
1
+ """Print the curve groups using ordinary dictionaries: python examples/groups.py."""
2
+
3
+ import hilbertplot as hp
4
+
5
+
6
+ def main():
7
+ for group in (hp.curves.proper(), hp.curves.improper(), hp.curves.inhomogeneous()):
8
+ for c in group:
9
+ print(f"{c['index']:2} {c['name']:24} {c['kind']:15} kernels={c['kernels']}")
10
+ print()
11
+
12
+
13
+ if __name__ == "__main__":
14
+ main()