hilbertplot 0.0.0__py3-none-any.whl
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.
- hilbertplot/__init__.py +69 -0
- hilbertplot/_core/__init__.py +1 -0
- hilbertplot/_core/arbitrary.py +114 -0
- hilbertplot/_core/catalog.py +451 -0
- hilbertplot/_core/curve.py +604 -0
- hilbertplot/_core/generation.py +102 -0
- hilbertplot/_core/geometry.py +142 -0
- hilbertplot/_core/symmetry.py +186 -0
- hilbertplot/_data/__init__.py +1 -0
- hilbertplot/_data/mapping.py +255 -0
- hilbertplot/_viz/__init__.py +1 -0
- hilbertplot/_viz/hilbert_plot.py +413 -0
- hilbertplot/_viz/render.py +193 -0
- hilbertplot/py.typed +0 -0
- hilbertplot-0.0.0.dist-info/METADATA +285 -0
- hilbertplot-0.0.0.dist-info/RECORD +18 -0
- hilbertplot-0.0.0.dist-info/WHEEL +4 -0
- hilbertplot-0.0.0.dist-info/licenses/LICENSE +21 -0
hilbertplot/__init__.py
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""hilbertplot -- the complete set of 40 two-dimensional Hilbert curves, and data plots on them.
|
|
2
|
+
|
|
3
|
+
``hilbertplot`` implements every two-dimensional Hilbert-type space-filling curve proved to
|
|
4
|
+
exist by Estevez-Rams *et al.*, "Hilbert curves in two dimensions", *Rev. Cub. Fis.*
|
|
5
|
+
**34**, 9 (2017): twelve homogeneous (six proper, six improper) and twenty-eight
|
|
6
|
+
inhomogeneous -- forty in all -- and uses them to lay long 1-D data vectors onto 2-D
|
|
7
|
+
images (Estevez-Rams *et al.*, *Comput. Phys. Commun.* **197** (2015) 118).
|
|
8
|
+
|
|
9
|
+
Curves
|
|
10
|
+
------
|
|
11
|
+
>>> import hilbertplot
|
|
12
|
+
>>> hilbertplot.curve("Moore").show(6) # doctest: +SKIP
|
|
13
|
+
>>> hilbertplot.curve(0).points(3).shape
|
|
14
|
+
(64, 2)
|
|
15
|
+
>>> print(hilbertplot.proper()) # doctest: +SKIP
|
|
16
|
+
# name kind closed
|
|
17
|
+
...
|
|
18
|
+
|
|
19
|
+
Data plots
|
|
20
|
+
----------
|
|
21
|
+
``plot_data`` / ``hilbert_plot`` return a ``HilbertPlot`` that holds the data and draws
|
|
22
|
+
nothing until you call ``.show`` / ``.save`` -- where you pick the colormap (a name or a
|
|
23
|
+
list of colours to interpolate), the granularity, and how to fit a length that is not a
|
|
24
|
+
power of four:
|
|
25
|
+
|
|
26
|
+
>>> plot = hilbertplot.hilbert_plot(0, range(1024)) # curve 0, the Hilbert curve
|
|
27
|
+
>>> plot.show(["red", "green", "tab:blue"]) # doctest: +SKIP
|
|
28
|
+
>>> plot.show("viridis", granularity=4) # doctest: +SKIP
|
|
29
|
+
|
|
30
|
+
Round trip
|
|
31
|
+
----------
|
|
32
|
+
``curve.unroll(image)`` reads a ``2**k`` by ``2**k`` array back into a 1-D vector in
|
|
33
|
+
curve order -- the inverse of the data mapping.
|
|
34
|
+
|
|
35
|
+
The public API is exactly the names in ``hilbertplot.__all__``; everything else is internal.
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
from __future__ import annotations
|
|
39
|
+
|
|
40
|
+
from ._core.curve import (
|
|
41
|
+
by_kernels,
|
|
42
|
+
catalog,
|
|
43
|
+
closed,
|
|
44
|
+
curve,
|
|
45
|
+
generalizing,
|
|
46
|
+
homogeneous,
|
|
47
|
+
improper,
|
|
48
|
+
inhomogeneous,
|
|
49
|
+
proper,
|
|
50
|
+
)
|
|
51
|
+
from ._viz.hilbert_plot import hilbert_plot
|
|
52
|
+
from ._viz.render import gallery
|
|
53
|
+
|
|
54
|
+
__version__ = "0.0.0"
|
|
55
|
+
|
|
56
|
+
__all__ = [
|
|
57
|
+
"__version__",
|
|
58
|
+
"curve",
|
|
59
|
+
"catalog",
|
|
60
|
+
"homogeneous",
|
|
61
|
+
"inhomogeneous",
|
|
62
|
+
"proper",
|
|
63
|
+
"improper",
|
|
64
|
+
"closed",
|
|
65
|
+
"generalizing",
|
|
66
|
+
"by_kernels",
|
|
67
|
+
"hilbert_plot",
|
|
68
|
+
"gallery",
|
|
69
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Internal subpackage (not part of the public API)."""
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Arbitrary-size (non-power-of-two) square generation for the curves that support it.
|
|
2
|
+
|
|
3
|
+
Only eight of the forty curves can be laid on an arbitrary ``n x n`` square as a single
|
|
4
|
+
connected space-filling path (the rest are provably obstructed -- see
|
|
5
|
+
``research/quasisquares/``). This module implements the approximately-even-partition
|
|
6
|
+
(AEP) construction of Wu & Chang, generalized to those eight curves via the catalog
|
|
7
|
+
quadrant rules and the full dihedral group ``D4``, with a per-curve *swap rule* that
|
|
8
|
+
keeps the path connected at odd sizes.
|
|
9
|
+
|
|
10
|
+
Public surface is the :meth:`hilbertplot.Curve.grid` method and the :func:`hilbertplot.generalizing`
|
|
11
|
+
group; this module is the engine behind them. ``grid(2**k)`` reproduces the standard
|
|
12
|
+
``points(k)`` curve exactly for all eight, so the two are interchangeable.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import numpy as np
|
|
18
|
+
|
|
19
|
+
from .catalog import SPECS
|
|
20
|
+
from .symmetry import MORPHISMS
|
|
21
|
+
|
|
22
|
+
#: The eight curves that tile arbitrary squares (proved in research/quasisquares/).
|
|
23
|
+
FEASIBLE: frozenset[int] = frozenset({0, 3, 4, 5, 24, 29, 30, 31})
|
|
24
|
+
|
|
25
|
+
#: The size-independent swap rule for each feasible curve: (width-split, height-split),
|
|
26
|
+
#: each in {"first" = force low half even, "second" = high half even, "none"}.
|
|
27
|
+
_SWAP: dict[int, tuple[str, str]] = {
|
|
28
|
+
0: ("first", "first"), 3: ("first", "second"), 4: ("second", "first"),
|
|
29
|
+
5: ("first", "second"), 24: ("first", "second"), 29: ("second", "first"),
|
|
30
|
+
30: ("second", "first"), 31: ("first", "second"),
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
#: For each infeasible curve, a small square size at which it provably fails (for messages).
|
|
34
|
+
WITNESS: dict[int, int] = {
|
|
35
|
+
1: 7, 2: 7, 6: 3, 7: 9, 8: 3, 9: 5, 10: 5, 11: 5, 12: 3, 13: 5, 14: 3, 15: 5,
|
|
36
|
+
16: 7, 17: 5, 18: 13, 19: 7, 20: 7, 21: 13, 22: 7, 23: 9, 25: 13, 26: 7, 27: 7,
|
|
37
|
+
28: 13, 32: 5, 33: 5, 34: 5, 35: 5, 36: 5, 37: 3, 38: 7, 39: 14,
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
# D4 matrices (act on column (x, y): new = M @ (x, y)) and catalog rules.
|
|
41
|
+
_MAT = {code: (tuple(s.matrix[0]), tuple(s.matrix[1])) for code, s in MORPHISMS.items()}
|
|
42
|
+
_IDENT = _MAT["e"]
|
|
43
|
+
_RULES = {s.index: [(q.base, _MAT[q.morphism], q.reverse) for q in s.quadrants] for s in SPECS}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _matmul(A, B):
|
|
47
|
+
return ((A[0][0]*B[0][0]+A[0][1]*B[1][0], A[0][0]*B[0][1]+A[0][1]*B[1][1]),
|
|
48
|
+
(A[1][0]*B[0][0]+A[1][1]*B[1][0], A[1][0]*B[0][1]+A[1][1]*B[1][1]))
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
# Canonical base-tile visit orders (Wc, Hc) -> cells; the 2x2 is orientation A's U.
|
|
52
|
+
_BASE = {(1, 1): ((0, 0),), (2, 1): ((0, 0), (1, 0)), (1, 2): ((0, 0), (0, 1)),
|
|
53
|
+
(2, 2): ((0, 0), (0, 1), (1, 1), (1, 0))}
|
|
54
|
+
_ORDER = (0, 1, 2, 3)
|
|
55
|
+
_ORDER_REV = (3, 2, 1, 0)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _fill(index, x0, y0, w, h, M, rev, out):
|
|
59
|
+
"""Append the (x, y) cells of curve `index` on region (x0,y0,w,h), orientation `M`,
|
|
60
|
+
to the list `out` (reversed if `rev`). Hot path -- all affine maths is inlined."""
|
|
61
|
+
(a, b), (c, d) = M
|
|
62
|
+
Wc, Hc = (h, w) if a == 0 else (w, h)
|
|
63
|
+
# translate so the canonical box maps into the region's own frame (D4 matrices only)
|
|
64
|
+
ox = x0 - ((a * (Wc - 1) if a < 0 else 0) + (b * (Hc - 1) if b < 0 else 0))
|
|
65
|
+
oy = y0 - ((c * (Wc - 1) if c < 0 else 0) + (d * (Hc - 1) if d < 0 else 0))
|
|
66
|
+
if w <= 2 and h <= 2:
|
|
67
|
+
cells = _BASE[(Wc, Hc)]
|
|
68
|
+
mapped = [(ox + a * cx + b * cy, oy + c * cx + d * cy) for (cx, cy) in cells]
|
|
69
|
+
out.extend(reversed(mapped) if rev else mapped)
|
|
70
|
+
return
|
|
71
|
+
sw, sh = _SWAP.get(index, ("first", "first"))
|
|
72
|
+
Wc1 = Wc // 2; Wc2 = Wc - Wc1
|
|
73
|
+
if sw == "first" and Wc1 & 1: Wc1, Wc2 = Wc2, Wc1
|
|
74
|
+
elif sw == "second" and Wc2 & 1: Wc1, Wc2 = Wc2, Wc1
|
|
75
|
+
Hc1 = Hc // 2; Hc2 = Hc - Hc1
|
|
76
|
+
if sh == "first" and Hc1 & 1: Hc1, Hc2 = Hc2, Hc1
|
|
77
|
+
elif sh == "second" and Hc2 & 1: Hc1, Hc2 = Hc2, Hc1
|
|
78
|
+
boxes = ((0, 0, Wc1, Hc1), (0, Hc1, Wc1, Hc2), (Wc1, Hc1, Wc2, Hc2), (Wc1, 0, Wc2, Hc1))
|
|
79
|
+
rules = _RULES[index]
|
|
80
|
+
for i in (_ORDER_REV if rev else _ORDER):
|
|
81
|
+
base_i, morph_i, revi = rules[i]
|
|
82
|
+
cx0, cy0, cw, ch = boxes[i]
|
|
83
|
+
x1 = cx0 + cw - 1; y1 = cy0 + ch - 1
|
|
84
|
+
# absolute bounding box of the four transformed sub-box corners
|
|
85
|
+
u0 = ox + a * cx0 + b * cy0; u1 = ox + a * x1 + b * cy0
|
|
86
|
+
u2 = ox + a * cx0 + b * y1; u3 = ox + a * x1 + b * y1
|
|
87
|
+
v0 = oy + c * cx0 + d * cy0; v1 = oy + c * x1 + d * cy0
|
|
88
|
+
v2 = oy + c * cx0 + d * y1; v3 = oy + c * x1 + d * y1
|
|
89
|
+
axmin = u0 if u0 < u1 else u1
|
|
90
|
+
if u2 < axmin: axmin = u2
|
|
91
|
+
if u3 < axmin: axmin = u3
|
|
92
|
+
axmax = u0 if u0 > u1 else u1
|
|
93
|
+
if u2 > axmax: axmax = u2
|
|
94
|
+
if u3 > axmax: axmax = u3
|
|
95
|
+
aymin = v0 if v0 < v1 else v1
|
|
96
|
+
if v2 < aymin: aymin = v2
|
|
97
|
+
if v3 < aymin: aymin = v3
|
|
98
|
+
aymax = v0 if v0 > v1 else v1
|
|
99
|
+
if v2 > aymax: aymax = v2
|
|
100
|
+
if v3 > aymax: aymax = v3
|
|
101
|
+
_fill(base_i, axmin, aymin, axmax - axmin + 1, aymax - aymin + 1,
|
|
102
|
+
_matmul(M, morph_i), revi ^ rev, out)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def square_points(index: int, n: int) -> np.ndarray:
|
|
106
|
+
"""Return the ``(n*n, 2)`` cell path of feasible curve ``index`` on an ``n x n`` grid.
|
|
107
|
+
|
|
108
|
+
The caller is responsible for checking ``index in FEASIBLE`` and ``n >= 1``.
|
|
109
|
+
"""
|
|
110
|
+
if n == 1:
|
|
111
|
+
return np.zeros((1, 2), dtype=np.int64)
|
|
112
|
+
out: list = []
|
|
113
|
+
_fill(index, 0, 0, n, n, _IDENT, False, out)
|
|
114
|
+
return np.array(out, dtype=np.int64)
|
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
"""Declarative specification of the forty two-dimensional Hilbert curves.
|
|
2
|
+
|
|
3
|
+
This module is pure data plus the small dataclasses that give it structure. It is
|
|
4
|
+
the single source of truth for the whole library and maps one-to-one onto the tag
|
|
5
|
+
systems of Estevez-Rams *et al.*, "Hilbert curves in two dimensions",
|
|
6
|
+
*Rev. Cub. Fis.* **34**, 9 (2017).
|
|
7
|
+
|
|
8
|
+
The construction
|
|
9
|
+
----------------
|
|
10
|
+
Every order ``k`` curve is assembled from four sub-curves of order ``k - 1`` -- one
|
|
11
|
+
per quadrant -- joined by the fixed connectors ``u``, ``r``, ``d``::
|
|
12
|
+
|
|
13
|
+
word_k(nu) = Q0 . 'u' . Q1 . 'r' . Q2 . 'd' . Q3
|
|
14
|
+
|
|
15
|
+
The quadrants are numbered as in Fig. 1 of the paper, clockwise from the lower
|
|
16
|
+
left: ``0`` = bottom-left, ``1`` = top-left, ``2`` = top-right, ``3`` =
|
|
17
|
+
bottom-right. The connectors therefore trace ``0 -(up)-> 1 -(right)-> 2 -(down)->
|
|
18
|
+
3``.
|
|
19
|
+
|
|
20
|
+
Each quadrant ``Qi`` is obtained from a *kernel* curve ``base`` (also of order
|
|
21
|
+
``k - 1``) by applying a morphism and, optionally, a reversion:
|
|
22
|
+
|
|
23
|
+
Qi = [reversion?] ( morphism ( word_{k-1}(base) ) )
|
|
24
|
+
|
|
25
|
+
The classification the paper draws -- and that the user asked us to reflect -- falls
|
|
26
|
+
straight out of the kernels:
|
|
27
|
+
|
|
28
|
+
* **homogeneous** curves use a single kernel type in all four quadrants
|
|
29
|
+
(Definition III.1: "built from one and only one Hilbert curve of order k-1");
|
|
30
|
+
* **inhomogeneous** curves mix at least two kernel types (Definition VI.1).
|
|
31
|
+
|
|
32
|
+
Homogeneous curves split further into *proper* (kernel = the Hilbert curve ``0H``)
|
|
33
|
+
and *improper* (kernel = the Liu-4 curve ``5H``).
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
from __future__ import annotations
|
|
37
|
+
|
|
38
|
+
import enum
|
|
39
|
+
from collections.abc import Mapping
|
|
40
|
+
from dataclasses import dataclass, field, replace
|
|
41
|
+
|
|
42
|
+
__all__ = [
|
|
43
|
+
"Family",
|
|
44
|
+
"QuadrantRule",
|
|
45
|
+
"CurveSpec",
|
|
46
|
+
"SPECS",
|
|
47
|
+
"spec_by_index",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class Family(enum.Enum):
|
|
52
|
+
"""The three families the forty curves fall into."""
|
|
53
|
+
|
|
54
|
+
HOMOGENEOUS_PROPER = "homogeneous-proper"
|
|
55
|
+
HOMOGENEOUS_IMPROPER = "homogeneous-improper"
|
|
56
|
+
INHOMOGENEOUS = "inhomogeneous"
|
|
57
|
+
|
|
58
|
+
@property
|
|
59
|
+
def is_homogeneous(self) -> bool:
|
|
60
|
+
return self is not Family.INHOMOGENEOUS
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
@dataclass(frozen=True)
|
|
64
|
+
class QuadrantRule:
|
|
65
|
+
"""How one quadrant of an order ``k`` curve is built from an order ``k-1`` curve.
|
|
66
|
+
|
|
67
|
+
Parameters
|
|
68
|
+
----------
|
|
69
|
+
base:
|
|
70
|
+
Index of the *kernel* curve supplying this quadrant's sub-curve. Only the
|
|
71
|
+
homogeneous building blocks ``{0, 1, 3, 4, 5}`` (and never ``2``) ever
|
|
72
|
+
appear here, exactly as proved in the paper.
|
|
73
|
+
morphism:
|
|
74
|
+
Single-letter morphism code (see :data:`hilbertplot._core.symmetry.MORPHISMS`).
|
|
75
|
+
reverse:
|
|
76
|
+
Whether the reversion :math:`\\bar p` is applied after the morphism.
|
|
77
|
+
"""
|
|
78
|
+
|
|
79
|
+
base: int
|
|
80
|
+
morphism: str
|
|
81
|
+
reverse: bool = False
|
|
82
|
+
|
|
83
|
+
def __str__(self) -> str:
|
|
84
|
+
inner = f"{self.base}H"
|
|
85
|
+
if self.morphism != "e":
|
|
86
|
+
inner = f"d_{self.morphism}({inner})"
|
|
87
|
+
return f"rev({inner})" if self.reverse else inner
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass(frozen=True)
|
|
91
|
+
class CurveSpec:
|
|
92
|
+
"""Immutable description of one of the forty curves.
|
|
93
|
+
|
|
94
|
+
Attributes
|
|
95
|
+
----------
|
|
96
|
+
index:
|
|
97
|
+
The curve number ``nu`` in ``0..39`` (the paper's :math:`{}_\\nu H`).
|
|
98
|
+
name:
|
|
99
|
+
Human-readable name (``"Hilbert"``, ``"Moore"``, ``"Liu1"``, ..., ``"I1"``,
|
|
100
|
+
``"IH12"`` ...).
|
|
101
|
+
family:
|
|
102
|
+
Which :class:`Family` the curve belongs to.
|
|
103
|
+
quadrants:
|
|
104
|
+
The four :class:`QuadrantRule` s, in quadrant order ``0, 1, 2, 3``.
|
|
105
|
+
combination:
|
|
106
|
+
Short label of the kernel mix (e.g. ``"0H"``, ``"5H"``, ``"3H+5H"``), matching
|
|
107
|
+
the paper's grouping of the inhomogeneous curves.
|
|
108
|
+
closed:
|
|
109
|
+
``True`` if the curve's entry and exit lie in adjacent sub-squares so that the
|
|
110
|
+
infinite curve closes into a loop (Moore, Liu1, I1, I4; Table 3 of the paper).
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
index: int
|
|
114
|
+
name: str
|
|
115
|
+
family: Family
|
|
116
|
+
quadrants: tuple[QuadrantRule, QuadrantRule, QuadrantRule, QuadrantRule]
|
|
117
|
+
combination: str
|
|
118
|
+
closed: bool = False
|
|
119
|
+
#: Optional free-form geometric notes lifted from Table 3 of the paper.
|
|
120
|
+
notes: str = field(default="", compare=False)
|
|
121
|
+
|
|
122
|
+
@property
|
|
123
|
+
def kernels(self) -> frozenset[int]:
|
|
124
|
+
"""The set of distinct kernel curve indices used across the four quadrants."""
|
|
125
|
+
return frozenset(q.base for q in self.quadrants)
|
|
126
|
+
|
|
127
|
+
@property
|
|
128
|
+
def is_homogeneous(self) -> bool:
|
|
129
|
+
return self.family.is_homogeneous
|
|
130
|
+
|
|
131
|
+
def rule_str(self) -> str:
|
|
132
|
+
"""Reconstruct the paper's tag-system line for this curve."""
|
|
133
|
+
q0, q1, q2, q3 = self.quadrants
|
|
134
|
+
return f"{self.index}h(k) = {q0} u {q1} r {q2} d {q3}"
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _q(base: int, morphism: str = "e", reverse: bool = False) -> QuadrantRule:
|
|
138
|
+
return QuadrantRule(base, morphism, reverse)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
# ---------------------------------------------------------------------------
|
|
142
|
+
# The forty curves.
|
|
143
|
+
#
|
|
144
|
+
# Transcribed directly from the tag systems in sections V and VII of the paper.
|
|
145
|
+
# Homogeneous proper curves are built over the Hilbert kernel 0H; improper curves
|
|
146
|
+
# over the Liu-4 kernel 5H; inhomogeneous curves mix kernels. A bar over a term in
|
|
147
|
+
# the paper (reversion) is recorded as ``reverse=True``.
|
|
148
|
+
# ---------------------------------------------------------------------------
|
|
149
|
+
|
|
150
|
+
_PROPER: tuple[CurveSpec, ...] = (
|
|
151
|
+
CurveSpec(
|
|
152
|
+
0,
|
|
153
|
+
"Hilbert",
|
|
154
|
+
Family.HOMOGENEOUS_PROPER,
|
|
155
|
+
(_q(0, "o"), _q(0), _q(0), _q(0, "a")),
|
|
156
|
+
"0H",
|
|
157
|
+
notes="corner-corner; the original Hilbert curve",
|
|
158
|
+
),
|
|
159
|
+
CurveSpec(
|
|
160
|
+
1,
|
|
161
|
+
"Moore",
|
|
162
|
+
Family.HOMOGENEOUS_PROPER,
|
|
163
|
+
(_q(0, "g"), _q(0, "g"), _q(0, "x"), _q(0, "x")),
|
|
164
|
+
"0H",
|
|
165
|
+
closed=True,
|
|
166
|
+
notes="edge-edge shared; Moore's closed curve",
|
|
167
|
+
),
|
|
168
|
+
CurveSpec(
|
|
169
|
+
2,
|
|
170
|
+
"Liu1",
|
|
171
|
+
Family.HOMOGENEOUS_PROPER,
|
|
172
|
+
(_q(0, "f"), _q(0), _q(0), _q(0, "f")),
|
|
173
|
+
"0H",
|
|
174
|
+
closed=True,
|
|
175
|
+
notes="interior-interior; cannot serve as a building block",
|
|
176
|
+
),
|
|
177
|
+
CurveSpec(
|
|
178
|
+
3,
|
|
179
|
+
"Liu2",
|
|
180
|
+
Family.HOMOGENEOUS_PROPER,
|
|
181
|
+
(_q(0, "m"), _q(0, "g"), _q(0, "x"), _q(0, "m")),
|
|
182
|
+
"0H",
|
|
183
|
+
notes="edge-edge opposed",
|
|
184
|
+
),
|
|
185
|
+
CurveSpec(
|
|
186
|
+
4,
|
|
187
|
+
"Liu3",
|
|
188
|
+
Family.HOMOGENEOUS_PROPER,
|
|
189
|
+
(_q(0, "o"), _q(0), _q(0), _q(0, "f")),
|
|
190
|
+
"0H",
|
|
191
|
+
notes="corner-interior",
|
|
192
|
+
),
|
|
193
|
+
CurveSpec(
|
|
194
|
+
5,
|
|
195
|
+
"Liu4",
|
|
196
|
+
Family.HOMOGENEOUS_PROPER,
|
|
197
|
+
(_q(0, "m"), _q(0, "g"), _q(0, "x"), _q(0, "x")),
|
|
198
|
+
"0H",
|
|
199
|
+
notes="edge-edge adjacent; kernel of every improper curve",
|
|
200
|
+
),
|
|
201
|
+
)
|
|
202
|
+
|
|
203
|
+
_IMPROPER: tuple[CurveSpec, ...] = (
|
|
204
|
+
CurveSpec(
|
|
205
|
+
6,
|
|
206
|
+
"I1",
|
|
207
|
+
Family.HOMOGENEOUS_IMPROPER,
|
|
208
|
+
(_q(5, "f"), _q(5, "y", True), _q(5), _q(5, "m", True)),
|
|
209
|
+
"5H",
|
|
210
|
+
closed=True,
|
|
211
|
+
notes="interior-interior",
|
|
212
|
+
),
|
|
213
|
+
CurveSpec(
|
|
214
|
+
7,
|
|
215
|
+
"I2",
|
|
216
|
+
Family.HOMOGENEOUS_IMPROPER,
|
|
217
|
+
(_q(5, "f"), _q(5, "y", True), _q(5), _q(5, "a")),
|
|
218
|
+
"5H",
|
|
219
|
+
notes="interior-edge",
|
|
220
|
+
),
|
|
221
|
+
CurveSpec(
|
|
222
|
+
8,
|
|
223
|
+
"I3",
|
|
224
|
+
Family.HOMOGENEOUS_IMPROPER,
|
|
225
|
+
(_q(5, "x", True), _q(5, "y", True), _q(5), _q(5, "a")),
|
|
226
|
+
"5H",
|
|
227
|
+
notes="edge-edge opposed",
|
|
228
|
+
),
|
|
229
|
+
CurveSpec(
|
|
230
|
+
9,
|
|
231
|
+
"I4",
|
|
232
|
+
Family.HOMOGENEOUS_IMPROPER,
|
|
233
|
+
(_q(5, "a", True), _q(5, "g"), _q(5, "o", True), _q(5, "x")),
|
|
234
|
+
"5H",
|
|
235
|
+
closed=True,
|
|
236
|
+
notes="interior-interior",
|
|
237
|
+
),
|
|
238
|
+
CurveSpec(
|
|
239
|
+
10,
|
|
240
|
+
"I5",
|
|
241
|
+
Family.HOMOGENEOUS_IMPROPER,
|
|
242
|
+
(_q(5, "m"), _q(5, "g"), _q(5, "o", True), _q(5, "f", True)),
|
|
243
|
+
"5H",
|
|
244
|
+
notes="edge-edge opposed",
|
|
245
|
+
),
|
|
246
|
+
CurveSpec(
|
|
247
|
+
11,
|
|
248
|
+
"I6",
|
|
249
|
+
Family.HOMOGENEOUS_IMPROPER,
|
|
250
|
+
(_q(5, "m"), _q(5, "g"), _q(5, "o", True), _q(5, "x")),
|
|
251
|
+
"5H",
|
|
252
|
+
notes="edge-interior",
|
|
253
|
+
),
|
|
254
|
+
)
|
|
255
|
+
|
|
256
|
+
# Inhomogeneous curves 12-39. Grouped exactly as in the paper (Figures 6-9).
|
|
257
|
+
_INHOMOGENEOUS: tuple[CurveSpec, ...] = (
|
|
258
|
+
# 3H + 5H (Figure 6): two 3H + two 5H, then one 3H + three 5H.
|
|
259
|
+
CurveSpec(
|
|
260
|
+
12, "IH12", Family.INHOMOGENEOUS, (_q(3, "o"), _q(5, "y", True), _q(5), _q(3, "a")), "3H+5H"
|
|
261
|
+
),
|
|
262
|
+
CurveSpec(
|
|
263
|
+
13,
|
|
264
|
+
"IH13",
|
|
265
|
+
Family.INHOMOGENEOUS,
|
|
266
|
+
(_q(3, "g"), _q(5, "g"), _q(5, "o", True), _q(3, "x")),
|
|
267
|
+
"3H+5H",
|
|
268
|
+
),
|
|
269
|
+
CurveSpec(
|
|
270
|
+
14, "IH14", Family.INHOMOGENEOUS, (_q(3, "o"), _q(5, "y", True), _q(5), _q(5, "a")), "3H+5H"
|
|
271
|
+
),
|
|
272
|
+
CurveSpec(
|
|
273
|
+
15,
|
|
274
|
+
"IH15",
|
|
275
|
+
Family.INHOMOGENEOUS,
|
|
276
|
+
(_q(3, "g"), _q(5, "g"), _q(5, "o", True), _q(5, "f", True)),
|
|
277
|
+
"3H+5H",
|
|
278
|
+
),
|
|
279
|
+
CurveSpec(
|
|
280
|
+
16,
|
|
281
|
+
"IH16",
|
|
282
|
+
Family.INHOMOGENEOUS,
|
|
283
|
+
(_q(3, "o"), _q(5, "y", True), _q(5), _q(5, "m", True)),
|
|
284
|
+
"3H+5H",
|
|
285
|
+
),
|
|
286
|
+
CurveSpec(
|
|
287
|
+
17,
|
|
288
|
+
"IH17",
|
|
289
|
+
Family.INHOMOGENEOUS,
|
|
290
|
+
(_q(3, "g"), _q(5, "g"), _q(5, "o", True), _q(5, "x")),
|
|
291
|
+
"3H+5H",
|
|
292
|
+
),
|
|
293
|
+
# 0H + 4H (Figure 7): fourteen curves.
|
|
294
|
+
CurveSpec(
|
|
295
|
+
18, "IH18", Family.INHOMOGENEOUS, (_q(4, "x", True), _q(0), _q(0), _q(4, "a")), "0H+4H"
|
|
296
|
+
),
|
|
297
|
+
CurveSpec(
|
|
298
|
+
19, "IH19", Family.INHOMOGENEOUS, (_q(4, "m", True), _q(0), _q(0), _q(4, "f")), "0H+4H"
|
|
299
|
+
),
|
|
300
|
+
CurveSpec(
|
|
301
|
+
20, "IH20", Family.INHOMOGENEOUS, (_q(4, "x", True), _q(0), _q(0), _q(4, "f")), "0H+4H"
|
|
302
|
+
),
|
|
303
|
+
CurveSpec(
|
|
304
|
+
21,
|
|
305
|
+
"IH21",
|
|
306
|
+
Family.INHOMOGENEOUS,
|
|
307
|
+
(_q(4, "f", True), _q(0, "g"), _q(0, "x"), _q(4, "m")),
|
|
308
|
+
"0H+4H",
|
|
309
|
+
),
|
|
310
|
+
CurveSpec(
|
|
311
|
+
22,
|
|
312
|
+
"IH22",
|
|
313
|
+
Family.INHOMOGENEOUS,
|
|
314
|
+
(_q(4, "a", True), _q(0, "g"), _q(0, "x"), _q(4, "x")),
|
|
315
|
+
"0H+4H",
|
|
316
|
+
),
|
|
317
|
+
CurveSpec(
|
|
318
|
+
23,
|
|
319
|
+
"IH23",
|
|
320
|
+
Family.INHOMOGENEOUS,
|
|
321
|
+
(_q(4, "a", True), _q(0, "g"), _q(0, "x"), _q(4, "m")),
|
|
322
|
+
"0H+4H",
|
|
323
|
+
),
|
|
324
|
+
CurveSpec(
|
|
325
|
+
24, "IH24", Family.INHOMOGENEOUS, (_q(0, "m"), _q(0, "g"), _q(0, "x"), _q(4, "x")), "0H+4H"
|
|
326
|
+
),
|
|
327
|
+
CurveSpec(
|
|
328
|
+
25, "IH25", Family.INHOMOGENEOUS, (_q(0, "g"), _q(0, "g"), _q(0, "x"), _q(4, "m")), "0H+4H"
|
|
329
|
+
),
|
|
330
|
+
CurveSpec(
|
|
331
|
+
26, "IH26", Family.INHOMOGENEOUS, (_q(0, "g"), _q(0, "g"), _q(0, "x"), _q(4, "x")), "0H+4H"
|
|
332
|
+
),
|
|
333
|
+
CurveSpec(27, "IH27", Family.INHOMOGENEOUS, (_q(0, "f"), _q(0), _q(0), _q(4, "f")), "0H+4H"),
|
|
334
|
+
CurveSpec(28, "IH28", Family.INHOMOGENEOUS, (_q(0, "f"), _q(0), _q(0), _q(4, "a")), "0H+4H"),
|
|
335
|
+
CurveSpec(29, "IH29", Family.INHOMOGENEOUS, (_q(0, "o"), _q(0), _q(0), _q(4, "f")), "0H+4H"),
|
|
336
|
+
CurveSpec(30, "IH30", Family.INHOMOGENEOUS, (_q(0, "o"), _q(0), _q(0), _q(4, "a")), "0H+4H"),
|
|
337
|
+
CurveSpec(
|
|
338
|
+
31, "IH31", Family.INHOMOGENEOUS, (_q(0, "m"), _q(0, "g"), _q(0, "x"), _q(4, "m")), "0H+4H"
|
|
339
|
+
),
|
|
340
|
+
# 1H + 5H (Figure 8): two 1H + two 5H, then one 1H + three 5H.
|
|
341
|
+
CurveSpec(
|
|
342
|
+
32, "IH32", Family.INHOMOGENEOUS, (_q(1, "f"), _q(5, "y", True), _q(5), _q(1, "f")), "1H+5H"
|
|
343
|
+
),
|
|
344
|
+
CurveSpec(
|
|
345
|
+
33,
|
|
346
|
+
"IH33",
|
|
347
|
+
Family.INHOMOGENEOUS,
|
|
348
|
+
(_q(1, "m"), _q(5, "g"), _q(5, "o", True), _q(1, "m")),
|
|
349
|
+
"1H+5H",
|
|
350
|
+
),
|
|
351
|
+
CurveSpec(
|
|
352
|
+
34, "IH34", Family.INHOMOGENEOUS, (_q(5, "f"), _q(5, "y", True), _q(5), _q(1, "f")), "1H+5H"
|
|
353
|
+
),
|
|
354
|
+
CurveSpec(
|
|
355
|
+
35,
|
|
356
|
+
"IH35",
|
|
357
|
+
Family.INHOMOGENEOUS,
|
|
358
|
+
(_q(5, "a", True), _q(5, "g"), _q(5, "o", True), _q(1, "m")),
|
|
359
|
+
"1H+5H",
|
|
360
|
+
),
|
|
361
|
+
CurveSpec(
|
|
362
|
+
36,
|
|
363
|
+
"IH36",
|
|
364
|
+
Family.INHOMOGENEOUS,
|
|
365
|
+
(_q(5, "x", True), _q(5, "y", True), _q(5), _q(1, "f")),
|
|
366
|
+
"1H+5H",
|
|
367
|
+
),
|
|
368
|
+
CurveSpec(
|
|
369
|
+
37,
|
|
370
|
+
"IH37",
|
|
371
|
+
Family.INHOMOGENEOUS,
|
|
372
|
+
(_q(5, "m"), _q(5, "g"), _q(5, "o", True), _q(1, "m")),
|
|
373
|
+
"1H+5H",
|
|
374
|
+
),
|
|
375
|
+
# 1H + 3H + 5H (Figure 9): two curves.
|
|
376
|
+
CurveSpec(
|
|
377
|
+
38,
|
|
378
|
+
"IH38",
|
|
379
|
+
Family.INHOMOGENEOUS,
|
|
380
|
+
(_q(3, "o"), _q(5, "y", True), _q(5), _q(1, "f")),
|
|
381
|
+
"1H+3H+5H",
|
|
382
|
+
),
|
|
383
|
+
CurveSpec(
|
|
384
|
+
39,
|
|
385
|
+
"IH39",
|
|
386
|
+
Family.INHOMOGENEOUS,
|
|
387
|
+
(_q(3, "g"), _q(5, "g"), _q(5, "o", True), _q(1, "m")),
|
|
388
|
+
"1H+3H+5H",
|
|
389
|
+
),
|
|
390
|
+
)
|
|
391
|
+
|
|
392
|
+
# Friendly, human-readable names. Proper curves keep their established names
|
|
393
|
+
# (Hilbert, Moore, Liu1-4); improper curves become Improper1-6; and each
|
|
394
|
+
# inhomogeneous curve is named by the curves it mixes plus an index within the
|
|
395
|
+
# group, e.g. "Liu2+Liu4 #1" -- so the name states the recipe.
|
|
396
|
+
_PROPER_NAME = {0: "Hilbert", 1: "Moore", 2: "Liu1", 3: "Liu2", 4: "Liu3", 5: "Liu4"}
|
|
397
|
+
|
|
398
|
+
|
|
399
|
+
def _with_friendly_names(specs: tuple[CurveSpec, ...]) -> tuple[CurveSpec, ...]:
|
|
400
|
+
counters: dict[str, int] = {}
|
|
401
|
+
out = []
|
|
402
|
+
for s in specs:
|
|
403
|
+
if s.family is Family.HOMOGENEOUS_IMPROPER:
|
|
404
|
+
s = replace(s, name=f"Improper{s.index - 5}")
|
|
405
|
+
elif s.family is Family.INHOMOGENEOUS:
|
|
406
|
+
recipe = "+".join(_PROPER_NAME[k] for k in sorted(s.kernels))
|
|
407
|
+
counters[recipe] = counters.get(recipe, 0) + 1
|
|
408
|
+
s = replace(s, name=f"{recipe} #{counters[recipe]}")
|
|
409
|
+
out.append(s)
|
|
410
|
+
return tuple(out)
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
#: All forty specifications, ordered by index.
|
|
414
|
+
SPECS: tuple[CurveSpec, ...] = _with_friendly_names(_PROPER + _IMPROPER + _INHOMOGENEOUS)
|
|
415
|
+
|
|
416
|
+
_BY_INDEX: Mapping[int, CurveSpec] = {s.index: s for s in SPECS}
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def spec_by_index(index: int) -> CurveSpec:
|
|
420
|
+
"""Return the :class:`CurveSpec` for curve number ``index`` (0..39)."""
|
|
421
|
+
try:
|
|
422
|
+
return _BY_INDEX[index]
|
|
423
|
+
except KeyError:
|
|
424
|
+
raise KeyError(
|
|
425
|
+
f"curve index {index!r} out of range; expected an integer in 0..39"
|
|
426
|
+
) from None
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
# --- internal consistency checks (cheap, run once at import) ----------------
|
|
430
|
+
# These guard against typos in the table above: the declared family must agree
|
|
431
|
+
# with what the kernels imply, and only valid building blocks may appear.
|
|
432
|
+
def _self_check() -> None:
|
|
433
|
+
assert len(SPECS) == 40, f"expected 40 curves, found {len(SPECS)}"
|
|
434
|
+
assert [s.index for s in SPECS] == list(range(40)), "curve indices must be 0..39"
|
|
435
|
+
valid_kernels = {0, 1, 3, 4, 5} # never 2 (Liu1 cannot connect quadrants)
|
|
436
|
+
for s in SPECS:
|
|
437
|
+
assert len(s.quadrants) == 4, f"curve {s.index} needs four quadrants"
|
|
438
|
+
assert s.kernels <= valid_kernels, (
|
|
439
|
+
f"curve {s.index} uses invalid kernel(s) {s.kernels - valid_kernels}"
|
|
440
|
+
)
|
|
441
|
+
implied_homogeneous = len(s.kernels) == 1
|
|
442
|
+
assert implied_homogeneous == s.is_homogeneous, (
|
|
443
|
+
f"curve {s.index}: family {s.family} disagrees with kernels {set(s.kernels)}"
|
|
444
|
+
)
|
|
445
|
+
if s.family is Family.HOMOGENEOUS_PROPER:
|
|
446
|
+
assert s.kernels == {0}, f"proper curve {s.index} must use kernel 0H"
|
|
447
|
+
elif s.family is Family.HOMOGENEOUS_IMPROPER:
|
|
448
|
+
assert s.kernels == {5}, f"improper curve {s.index} must use kernel 5H"
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
_self_check()
|