hilbertplot 0.2.0__tar.gz → 1.0.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.
Files changed (56) hide show
  1. {hilbertplot-0.2.0 → hilbertplot-1.0.1}/CHANGELOG.md +64 -0
  2. hilbertplot-1.0.1/PKG-INFO +255 -0
  3. hilbertplot-1.0.1/README.md +222 -0
  4. hilbertplot-1.0.1/examples/groups.py +14 -0
  5. {hilbertplot-0.2.0 → hilbertplot-1.0.1}/examples/plot_catalog.py +20 -10
  6. hilbertplot-1.0.1/examples/plot_data.py +34 -0
  7. {hilbertplot-0.2.0 → hilbertplot-1.0.1}/examples/readme_figures.py +29 -30
  8. hilbertplot-1.0.1/examples/show_curve.py +30 -0
  9. hilbertplot-1.0.1/examples/try_api.py +135 -0
  10. {hilbertplot-0.2.0 → hilbertplot-1.0.1}/pyproject.toml +9 -7
  11. hilbertplot-1.0.1/scripts/build_release.sh +17 -0
  12. hilbertplot-1.0.1/scripts/check_release.py +102 -0
  13. hilbertplot-1.0.1/src/hilbertplot/__init__.py +37 -0
  14. hilbertplot-1.0.1/src/hilbertplot/_api.py +404 -0
  15. {hilbertplot-0.2.0 → hilbertplot-1.0.1}/src/hilbertplot/_core/arbitrary.py +49 -3
  16. {hilbertplot-0.2.0 → hilbertplot-1.0.1}/src/hilbertplot/_core/curve.py +53 -53
  17. {hilbertplot-0.2.0 → hilbertplot-1.0.1}/src/hilbertplot/_core/geometry.py +8 -8
  18. {hilbertplot-0.2.0 → hilbertplot-1.0.1}/src/hilbertplot/_core/indexing.py +2 -2
  19. {hilbertplot-0.2.0 → hilbertplot-1.0.1}/src/hilbertplot/_core/limits.py +5 -5
  20. hilbertplot-1.0.1/src/hilbertplot/_data/conversion.py +165 -0
  21. {hilbertplot-0.2.0 → hilbertplot-1.0.1}/src/hilbertplot/_data/mapping.py +6 -2
  22. {hilbertplot-0.2.0 → hilbertplot-1.0.1}/src/hilbertplot/_viz/hilbert_plot.py +55 -79
  23. {hilbertplot-0.2.0 → hilbertplot-1.0.1}/src/hilbertplot/_viz/render.py +31 -16
  24. hilbertplot-1.0.1/src/hilbertplot/config.py +10 -0
  25. hilbertplot-1.0.1/src/hilbertplot/curves.py +53 -0
  26. hilbertplot-1.0.1/src/hilbertplot/data.py +15 -0
  27. hilbertplot-1.0.1/tests/test_api.py +243 -0
  28. {hilbertplot-0.2.0 → hilbertplot-1.0.1}/tests/test_arbitrary.py +21 -19
  29. {hilbertplot-0.2.0 → hilbertplot-1.0.1}/tests/test_catalog.py +42 -42
  30. hilbertplot-1.0.1/tests/test_conversion.py +161 -0
  31. hilbertplot-1.0.1/tests/test_curve_shapes.py +86 -0
  32. {hilbertplot-0.2.0 → hilbertplot-1.0.1}/tests/test_data.py +33 -32
  33. {hilbertplot-0.2.0 → hilbertplot-1.0.1}/tests/test_geometry.py +7 -7
  34. {hilbertplot-0.2.0 → hilbertplot-1.0.1}/tests/test_indexing.py +21 -20
  35. {hilbertplot-0.2.0 → hilbertplot-1.0.1}/tests/test_performance.py +13 -12
  36. {hilbertplot-0.2.0 → hilbertplot-1.0.1}/tests/test_plotting.py +43 -44
  37. {hilbertplot-0.2.0 → hilbertplot-1.0.1}/tests/test_readme.py +4 -1
  38. {hilbertplot-0.2.0 → hilbertplot-1.0.1}/tests/test_robustness.py +101 -90
  39. {hilbertplot-0.2.0 → hilbertplot-1.0.1}/tests/test_words.py +5 -5
  40. hilbertplot-0.2.0/PKG-INFO +0 -157
  41. hilbertplot-0.2.0/README.md +0 -124
  42. hilbertplot-0.2.0/examples/groups.py +0 -29
  43. hilbertplot-0.2.0/examples/plot_data.py +0 -119
  44. hilbertplot-0.2.0/examples/show_curve.py +0 -48
  45. hilbertplot-0.2.0/src/hilbertplot/__init__.py +0 -102
  46. {hilbertplot-0.2.0 → hilbertplot-1.0.1}/.gitignore +0 -0
  47. {hilbertplot-0.2.0 → hilbertplot-1.0.1}/LICENSE +0 -0
  48. {hilbertplot-0.2.0 → hilbertplot-1.0.1}/src/hilbertplot/_core/__init__.py +0 -0
  49. {hilbertplot-0.2.0 → hilbertplot-1.0.1}/src/hilbertplot/_core/catalog.py +0 -0
  50. {hilbertplot-0.2.0 → hilbertplot-1.0.1}/src/hilbertplot/_core/generation.py +0 -0
  51. {hilbertplot-0.2.0 → hilbertplot-1.0.1}/src/hilbertplot/_core/symmetry.py +0 -0
  52. {hilbertplot-0.2.0 → hilbertplot-1.0.1}/src/hilbertplot/_data/__init__.py +0 -0
  53. {hilbertplot-0.2.0 → hilbertplot-1.0.1}/src/hilbertplot/_viz/__init__.py +0 -0
  54. {hilbertplot-0.2.0 → hilbertplot-1.0.1}/src/hilbertplot/py.typed +0 -0
  55. {hilbertplot-0.2.0 → hilbertplot-1.0.1}/tests/conftest.py +0 -0
  56. {hilbertplot-0.2.0 → hilbertplot-1.0.1}/tests/test_symmetry.py +0 -0
@@ -7,6 +7,42 @@ All notable changes to **hilbertplot** are documented here. The format follows
7
7
  Released versions below are generated automatically by python-semantic-release;
8
8
  new entries are inserted at the marker.
9
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
+
10
46
  ## Migrating from 0.1.0
11
47
 
12
48
  `hilbertplot` is on `0.x`, where [semver §4](https://semver.org/#spec-item-4) allows the
@@ -27,6 +63,34 @@ validates `fit` instead of ignoring it.
27
63
 
28
64
  <!-- version list -->
29
65
 
66
+ ## v1.0.1 (2026-11-01)
67
+
68
+ ### Bug Fixes
69
+
70
+ - Provide citation metadata for software archives
71
+ ([`be2d92b`](https://github.com/El3ssar/hilbertplot/commit/be2d92b00b5d8ce083eec3c6c9ec5a2d9215028c))
72
+
73
+ ### Testing
74
+
75
+ - Measure construction allocations without interpreter overhead
76
+ ([`0b95704`](https://github.com/El3ssar/hilbertplot/commit/0b9570438681c47d0736415c1e74fb8522d2f04e))
77
+
78
+
79
+ ## v1.0.0 (2026-11-01)
80
+
81
+ ### Features
82
+
83
+ - Simplify plotting and add lossless curve conversions
84
+ ([`f45f2a6`](https://github.com/El3ssar/hilbertplot/commit/f45f2a6219bfc1f6259d72a64d01c8b19ae7d28e))
85
+
86
+ ### Breaking Changes
87
+
88
+ - Replace public curve/plot objects and top-level numerical helpers with function-based plotting and
89
+ namespaced to_grid/from_grid/index_to_xy/xy_to_index operations. Data plots default to
90
+ power-of-two padding; difference=True renders the full locality-loss field. General rectangles are
91
+ intentionally unsupported.
92
+
93
+
30
94
  ## v0.2.0 (2026-07-25)
31
95
 
32
96
  ### Bug Fixes
@@ -0,0 +1,255 @@
1
+ Metadata-Version: 2.5
2
+ Name: hilbertplot
3
+ Version: 1.0.1
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()
@@ -7,6 +7,11 @@ Writes to examples/output/.
7
7
 
8
8
  import os
9
9
 
10
+ import matplotlib
11
+
12
+ matplotlib.use("Agg")
13
+ import matplotlib.pyplot as plt
14
+
10
15
  import hilbertplot as hp
11
16
 
12
17
  OUT = os.path.join(os.path.dirname(__file__), "output")
@@ -14,28 +19,33 @@ os.makedirs(OUT, exist_ok=True)
14
19
 
15
20
 
16
21
  def main() -> None:
17
- hp.gallery(
18
- hp.homogeneous(),
22
+ fig = hp.gallery(
23
+ hp.curves.homogeneous(),
19
24
  order=4,
20
25
  ncols=6,
21
26
  title="The 12 homogeneous Hilbert curves (order 4)",
22
- save=os.path.join(OUT, "homogeneous.png"),
23
27
  )
24
- hp.gallery(
25
- hp.inhomogeneous(),
28
+ fig.savefig(os.path.join(OUT, "homogeneous.png"), dpi=140)
29
+ plt.close(fig)
30
+ fig = hp.gallery(
31
+ hp.curves.inhomogeneous(),
26
32
  order=3,
27
33
  ncols=7,
28
34
  title="The 28 inhomogeneous Hilbert curves (order 3)",
29
- save=os.path.join(OUT, "inhomogeneous.png"),
30
35
  )
31
- hp.gallery(
32
- hp.catalog(),
36
+ fig.savefig(os.path.join(OUT, "inhomogeneous.png"), dpi=140)
37
+ plt.close(fig)
38
+ fig = hp.gallery(
39
+ hp.curves.catalog(),
33
40
  order=3,
34
41
  ncols=8,
35
42
  title="All 40 two-dimensional Hilbert curves (order 3)",
36
- save=os.path.join(OUT, "all40.png"),
37
43
  )
38
- hp.curve(0).save(os.path.join(OUT, "hilbert_order6.png"), 6)
44
+ fig.savefig(os.path.join(OUT, "all40.png"), dpi=140)
45
+ plt.close(fig)
46
+ fig = hp.plot_curve(0, order=6)
47
+ fig.savefig(os.path.join(OUT, "hilbert_order6.png"), dpi=140)
48
+ plt.close(fig)
39
49
  print(f"wrote plots to {OUT}")
40
50
 
41
51
 
@@ -0,0 +1,34 @@
1
+ """One call, one plot: python examples/plot_data.py. Writes examples/output/."""
2
+
3
+ from pathlib import Path
4
+
5
+ import matplotlib
6
+ import numpy as np
7
+
8
+ matplotlib.use("Agg")
9
+ import matplotlib.pyplot as plt
10
+
11
+ import hilbertplot as hp
12
+
13
+
14
+ def main():
15
+ out = Path(__file__).parent / "output"
16
+ out.mkdir(exist_ok=True)
17
+ t = np.arange(1000)
18
+ data = np.sin(2 * np.pi * t / 97) + 0.4 * np.cos(2 * np.pi * t / 31)
19
+ for name, options in (
20
+ ("data", {}),
21
+ ("granularity", {"granularity": 16}),
22
+ ("fourier", {"fourier": True, "window": True}),
23
+ ("difference", {"difference": True}),
24
+ ):
25
+ fig = hp.plot(data, curve=12, **options)
26
+ fig.savefig(out / f"{name}.png", dpi=160)
27
+ plt.close(fig)
28
+ fig = hp.plot(data, curve="Moore", cmap=["navy", "white", "firebrick"])
29
+ fig.savefig(out / "colours.png", dpi=160)
30
+ plt.close(fig)
31
+
32
+
33
+ if __name__ == "__main__":
34
+ main()