bin2path 0.1.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.
- bin2path/__init__.py +48 -0
- bin2path/animate.py +237 -0
- bin2path/batch.py +38 -0
- bin2path/compare.py +139 -0
- bin2path/decode.py +33 -0
- bin2path/encode.py +151 -0
- bin2path/features.py +155 -0
- bin2path/orient.py +107 -0
- bin2path/path.py +70 -0
- bin2path/serialize.py +97 -0
- bin2path/validate.py +101 -0
- bin2path/visualize.py +380 -0
- bin2path-0.1.0.dist-info/METADATA +234 -0
- bin2path-0.1.0.dist-info/RECORD +17 -0
- bin2path-0.1.0.dist-info/WHEEL +5 -0
- bin2path-0.1.0.dist-info/licenses/LICENSE +21 -0
- bin2path-0.1.0.dist-info/top_level.txt +1 -0
bin2path/__init__.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"""
|
|
2
|
+
bin2path - Transform numbers into 3D geometric paths.
|
|
3
|
+
|
|
4
|
+
A library for converting natural numbers into 3D geometric shapes (spatial
|
|
5
|
+
polyline on an integer lattice). Each bit of the binary representation
|
|
6
|
+
is mapped to a symbolic step sequence (L/R/U/D) using a cellular-automaton rule.
|
|
7
|
+
Symbols are then interpreted as local (orientation-dependent) 3D moves, so the
|
|
8
|
+
resulting path is not constrained to a plane.
|
|
9
|
+
|
|
10
|
+
Usage:
|
|
11
|
+
import bin2path
|
|
12
|
+
|
|
13
|
+
path = bin2path.encode(42)
|
|
14
|
+
number = bin2path.decode(path)
|
|
15
|
+
bin2path.visualize(path)
|
|
16
|
+
features = bin2path.features(path)
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from bin2path.path import Path3D, PathMetadata
|
|
20
|
+
from bin2path.encode import encode
|
|
21
|
+
from bin2path.decode import decode
|
|
22
|
+
from bin2path.features import features
|
|
23
|
+
from bin2path.visualize import visualize
|
|
24
|
+
from bin2path.serialize import serialize, deserialize, to_json, from_json
|
|
25
|
+
from bin2path.compare import compare
|
|
26
|
+
from bin2path.validate import validate, is_valid
|
|
27
|
+
from bin2path.batch import batch_encode, batch_decode
|
|
28
|
+
|
|
29
|
+
__version__ = "0.1.0"
|
|
30
|
+
__author__ = "bin2path"
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"Path3D",
|
|
34
|
+
"PathMetadata",
|
|
35
|
+
"encode",
|
|
36
|
+
"decode",
|
|
37
|
+
"features",
|
|
38
|
+
"visualize",
|
|
39
|
+
"serialize",
|
|
40
|
+
"deserialize",
|
|
41
|
+
"to_json",
|
|
42
|
+
"from_json",
|
|
43
|
+
"compare",
|
|
44
|
+
"validate",
|
|
45
|
+
"is_valid",
|
|
46
|
+
"batch_encode",
|
|
47
|
+
"batch_decode",
|
|
48
|
+
]
|
bin2path/animate.py
ADDED
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"""Simple animation utility for bin2path paths."""
|
|
2
|
+
|
|
3
|
+
from bin2path.path import Path3D
|
|
4
|
+
from bin2path.orient import infer_dirs_from_path
|
|
5
|
+
import matplotlib.pyplot as plt
|
|
6
|
+
import matplotlib.patches as mpatches
|
|
7
|
+
from mpl_toolkits.mplot3d import Axes3D # noqa: F401
|
|
8
|
+
import numpy as np
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def animate(
|
|
13
|
+
path: Path3D,
|
|
14
|
+
interval: int = 150,
|
|
15
|
+
title: Optional[str] = None,
|
|
16
|
+
) -> None:
|
|
17
|
+
"""Animate building the path step by step, coloring segments by step type (L/R/U/D)."""
|
|
18
|
+
raw_vertices = np.array(path.vertices, dtype=float)
|
|
19
|
+
vertices = raw_vertices.copy()
|
|
20
|
+
|
|
21
|
+
fig = plt.figure(figsize=(8, 6))
|
|
22
|
+
ax = fig.add_subplot(111, projection="3d")
|
|
23
|
+
|
|
24
|
+
# тёмный фон и более "объёмный" вид
|
|
25
|
+
fig.patch.set_facecolor("black")
|
|
26
|
+
ax.set_facecolor("black")
|
|
27
|
+
|
|
28
|
+
if title is None:
|
|
29
|
+
title = f"bin2path animation: {path.metadata.original_number}"
|
|
30
|
+
ax.set_title(title)
|
|
31
|
+
|
|
32
|
+
# limits
|
|
33
|
+
max_range = np.max(np.abs(vertices).max(axis=0)) * 1.1
|
|
34
|
+
ax.set_xlim([-max_range, max_range])
|
|
35
|
+
ax.set_ylim([-max_range, max_range])
|
|
36
|
+
ax.set_zlim([-max_range, max_range])
|
|
37
|
+
|
|
38
|
+
# базовый ракурс
|
|
39
|
+
ax.view_init(elev=30, azim=45)
|
|
40
|
+
|
|
41
|
+
# step colors by L/R/U/D
|
|
42
|
+
step_colors = {
|
|
43
|
+
"L": "#FF5733",
|
|
44
|
+
"R": "#2E86AB",
|
|
45
|
+
"U": "#28A745",
|
|
46
|
+
"D": "#FFC300",
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
# precompute per-edge colors и глубину по Z
|
|
50
|
+
dirs = infer_dirs_from_path(path.vertices, path.edges)
|
|
51
|
+
edge_colors = []
|
|
52
|
+
edge_depth = []
|
|
53
|
+
z_min = float(vertices[:, 2].min()) if len(vertices) > 0 else 0.0
|
|
54
|
+
z_max = float(vertices[:, 2].max()) if len(vertices) > 0 else 1.0
|
|
55
|
+
z_span = (z_max - z_min) or 1.0
|
|
56
|
+
for i, (from_idx, to_idx) in enumerate(path.edges):
|
|
57
|
+
sym = dirs[i] if i < len(dirs) else None
|
|
58
|
+
edge_colors.append(step_colors.get(sym or "", "#999999"))
|
|
59
|
+
# глубина относительно наблюдателя по Z
|
|
60
|
+
z_mid = 0.5 * (vertices[from_idx][2] + vertices[to_idx][2])
|
|
61
|
+
depth_factor = 0.45 + 0.55 * ((z_mid - z_min) / z_span)
|
|
62
|
+
edge_depth.append(depth_factor)
|
|
63
|
+
|
|
64
|
+
# line segments drawn progressively
|
|
65
|
+
lines = []
|
|
66
|
+
|
|
67
|
+
def update(i: int):
|
|
68
|
+
# draw segment i-1 -> i
|
|
69
|
+
if i > 0:
|
|
70
|
+
from_v = vertices[i - 1]
|
|
71
|
+
to_v = vertices[i]
|
|
72
|
+
color = edge_colors[i - 1]
|
|
73
|
+
depth_factor = edge_depth[i - 1]
|
|
74
|
+
# outline first (for better readability)
|
|
75
|
+
ax.plot(
|
|
76
|
+
[from_v[0], to_v[0]],
|
|
77
|
+
[from_v[1], to_v[1]],
|
|
78
|
+
[from_v[2], to_v[2]],
|
|
79
|
+
color="#FFFFFF",
|
|
80
|
+
linewidth=max(3.2, 4.6 * depth_factor),
|
|
81
|
+
alpha=0.85,
|
|
82
|
+
solid_capstyle="round",
|
|
83
|
+
solid_joinstyle="round",
|
|
84
|
+
antialiased=True,
|
|
85
|
+
)
|
|
86
|
+
line, = ax.plot(
|
|
87
|
+
[from_v[0], to_v[0]],
|
|
88
|
+
[from_v[1], to_v[1]],
|
|
89
|
+
[from_v[2], to_v[2]],
|
|
90
|
+
color=color,
|
|
91
|
+
linewidth=max(2.2, 3.2 * depth_factor),
|
|
92
|
+
alpha=0.7 + 0.3 * depth_factor,
|
|
93
|
+
solid_capstyle="round",
|
|
94
|
+
solid_joinstyle="round",
|
|
95
|
+
antialiased=True,
|
|
96
|
+
)
|
|
97
|
+
lines.append(line)
|
|
98
|
+
return lines
|
|
99
|
+
|
|
100
|
+
# Legend for step types
|
|
101
|
+
legend_handles = [
|
|
102
|
+
mpatches.Patch(color=step_colors["L"], label="L (Left)"),
|
|
103
|
+
mpatches.Patch(color=step_colors["R"], label="R (Right)"),
|
|
104
|
+
mpatches.Patch(color=step_colors["U"], label="U (Up)"),
|
|
105
|
+
mpatches.Patch(color=step_colors["D"], label="D (Down)"),
|
|
106
|
+
]
|
|
107
|
+
ax.legend(handles=legend_handles, loc="upper left")
|
|
108
|
+
|
|
109
|
+
from matplotlib.animation import FuncAnimation
|
|
110
|
+
|
|
111
|
+
# ВАЖНО: держим ссылку на объект анимации, чтобы его не уничтожил GC до show()
|
|
112
|
+
ani = FuncAnimation(
|
|
113
|
+
fig,
|
|
114
|
+
update,
|
|
115
|
+
frames=len(vertices),
|
|
116
|
+
interval=interval,
|
|
117
|
+
blit=False,
|
|
118
|
+
repeat=False,
|
|
119
|
+
)
|
|
120
|
+
# сохраняем на объекте фигуры, чтобы точно не пропал
|
|
121
|
+
fig._bin2path_animation = ani # type: ignore[attr-defined]
|
|
122
|
+
|
|
123
|
+
plt.show()
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def rotate_view(
|
|
127
|
+
path: Path3D,
|
|
128
|
+
frames: int = 120,
|
|
129
|
+
interval: int = 80,
|
|
130
|
+
title: Optional[str] = None,
|
|
131
|
+
) -> None:
|
|
132
|
+
"""Простая анимация вращения камеры вокруг уже построенного пути."""
|
|
133
|
+
raw_vertices = np.array(path.vertices, dtype=float)
|
|
134
|
+
vertices = raw_vertices.copy()
|
|
135
|
+
|
|
136
|
+
fig = plt.figure(figsize=(8, 6))
|
|
137
|
+
ax = fig.add_subplot(111, projection="3d")
|
|
138
|
+
|
|
139
|
+
fig.patch.set_facecolor("black")
|
|
140
|
+
ax.set_facecolor("black")
|
|
141
|
+
|
|
142
|
+
if title is None:
|
|
143
|
+
title = f"bin2path rotate: {path.metadata.original_number}"
|
|
144
|
+
ax.set_title(title, color="white")
|
|
145
|
+
|
|
146
|
+
max_range = np.max(np.abs(vertices).max(axis=0)) * 1.1
|
|
147
|
+
ax.set_xlim([-max_range, max_range])
|
|
148
|
+
ax.set_ylim([-max_range, max_range])
|
|
149
|
+
ax.set_zlim([-max_range, max_range])
|
|
150
|
+
|
|
151
|
+
# нарисуем весь путь как в визуализации (L/R/U/D цвета)
|
|
152
|
+
step_colors = {
|
|
153
|
+
"L": "#FF5733",
|
|
154
|
+
"R": "#2E86AB",
|
|
155
|
+
"U": "#28A745",
|
|
156
|
+
"D": "#FFC300",
|
|
157
|
+
}
|
|
158
|
+
dirs = infer_dirs_from_path(path.vertices, path.edges)
|
|
159
|
+
for i, (from_idx, to_idx) in enumerate(path.edges):
|
|
160
|
+
sym = dirs[i] if i < len(dirs) else None
|
|
161
|
+
color = step_colors.get(sym or "", "#999999")
|
|
162
|
+
p0 = vertices[from_idx]
|
|
163
|
+
p1 = vertices[to_idx]
|
|
164
|
+
# outline (white) underlay for contrast
|
|
165
|
+
ax.plot(
|
|
166
|
+
[p0[0], p1[0]],
|
|
167
|
+
[p0[1], p1[1]],
|
|
168
|
+
[p0[2], p1[2]],
|
|
169
|
+
color="#FFFFFF",
|
|
170
|
+
linewidth=3.6,
|
|
171
|
+
alpha=0.85,
|
|
172
|
+
solid_capstyle="round",
|
|
173
|
+
solid_joinstyle="round",
|
|
174
|
+
antialiased=True,
|
|
175
|
+
)
|
|
176
|
+
ax.plot(
|
|
177
|
+
[p0[0], p1[0]],
|
|
178
|
+
[p0[1], p1[1]],
|
|
179
|
+
[p0[2], p1[2]],
|
|
180
|
+
color=color,
|
|
181
|
+
linewidth=2.0,
|
|
182
|
+
alpha=0.9,
|
|
183
|
+
solid_capstyle="round",
|
|
184
|
+
solid_joinstyle="round",
|
|
185
|
+
antialiased=True,
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
# start/end точки
|
|
189
|
+
if len(vertices) > 0:
|
|
190
|
+
s = vertices[0]
|
|
191
|
+
ax.scatter(
|
|
192
|
+
[s[0]],
|
|
193
|
+
[s[1]],
|
|
194
|
+
[s[2]],
|
|
195
|
+
c="#FFFFFF",
|
|
196
|
+
s=80,
|
|
197
|
+
marker="o",
|
|
198
|
+
)
|
|
199
|
+
if len(vertices) > 1:
|
|
200
|
+
e = vertices[-1]
|
|
201
|
+
ax.scatter(
|
|
202
|
+
[e[0]],
|
|
203
|
+
[e[1]],
|
|
204
|
+
[e[2]],
|
|
205
|
+
c="#FF0000",
|
|
206
|
+
s=80,
|
|
207
|
+
marker="s",
|
|
208
|
+
)
|
|
209
|
+
|
|
210
|
+
# легенда направлений
|
|
211
|
+
legend_handles = [
|
|
212
|
+
mpatches.Patch(color=step_colors["L"], label="L (Left)"),
|
|
213
|
+
mpatches.Patch(color=step_colors["R"], label="R (Right)"),
|
|
214
|
+
mpatches.Patch(color=step_colors["U"], label="U (Up)"),
|
|
215
|
+
mpatches.Patch(color=step_colors["D"], label="D (Down)"),
|
|
216
|
+
]
|
|
217
|
+
ax.legend(handles=legend_handles, loc="upper left")
|
|
218
|
+
|
|
219
|
+
from matplotlib.animation import FuncAnimation
|
|
220
|
+
|
|
221
|
+
def update_view(i: int):
|
|
222
|
+
az = 360.0 * (i / frames)
|
|
223
|
+
ax.view_init(elev=30, azim=az)
|
|
224
|
+
return []
|
|
225
|
+
|
|
226
|
+
ani = FuncAnimation(
|
|
227
|
+
fig,
|
|
228
|
+
update_view,
|
|
229
|
+
frames=frames,
|
|
230
|
+
interval=interval,
|
|
231
|
+
blit=False,
|
|
232
|
+
repeat=True,
|
|
233
|
+
)
|
|
234
|
+
fig._bin2path_rotation = ani # type: ignore[attr-defined]
|
|
235
|
+
|
|
236
|
+
plt.show()
|
|
237
|
+
|
bin2path/batch.py
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"""Batch operations for encoding/decoding multiple items."""
|
|
2
|
+
|
|
3
|
+
from bin2path.path import Path3D
|
|
4
|
+
from bin2path.encode import encode
|
|
5
|
+
from bin2path.decode import decode
|
|
6
|
+
from typing import List
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def batch_encode(numbers: List[int]) -> List[Path3D]:
|
|
10
|
+
"""
|
|
11
|
+
Encode multiple numbers into paths.
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
numbers: List of non-negative integers.
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
List of Path3D objects.
|
|
18
|
+
|
|
19
|
+
Raises:
|
|
20
|
+
ValueError: If any number is invalid.
|
|
21
|
+
"""
|
|
22
|
+
return [encode(n) for n in numbers]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def batch_decode(paths: List[Path3D]) -> List[int]:
|
|
26
|
+
"""
|
|
27
|
+
Decode multiple paths back to numbers.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
paths: List of Path3D objects.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
List of original numbers.
|
|
34
|
+
|
|
35
|
+
Raises:
|
|
36
|
+
ValueError: If any path is invalid.
|
|
37
|
+
"""
|
|
38
|
+
return [decode(p) for p in paths]
|
bin2path/compare.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
"""Compare two paths for similarity."""
|
|
2
|
+
|
|
3
|
+
from bin2path.path import Path3D
|
|
4
|
+
from bin2path.features import features
|
|
5
|
+
import numpy as np
|
|
6
|
+
import math
|
|
7
|
+
from typing import Optional
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def compare(
|
|
11
|
+
path1: Path3D,
|
|
12
|
+
path2: Path3D,
|
|
13
|
+
method: str = "features",
|
|
14
|
+
) -> dict:
|
|
15
|
+
"""
|
|
16
|
+
Compare two paths and return similarity metrics.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
path1: First Path3D object.
|
|
20
|
+
path2: Second Path3D object.
|
|
21
|
+
method: Comparison method - "features", "hausdorff", or "dtw".
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
Dictionary with similarity metrics.
|
|
25
|
+
|
|
26
|
+
Raises:
|
|
27
|
+
ValueError: If method is unknown.
|
|
28
|
+
"""
|
|
29
|
+
if method == "features":
|
|
30
|
+
return _compare_features(path1, path2)
|
|
31
|
+
elif method == "hausdorff":
|
|
32
|
+
return _compare_hausdorff(path1, path2)
|
|
33
|
+
elif method == "dtw":
|
|
34
|
+
return _compare_dtw(path1, path2)
|
|
35
|
+
else:
|
|
36
|
+
raise ValueError(f"Unknown comparison method: {method}")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _compare_features(path1: Path3D, path2: Path3D) -> dict:
|
|
40
|
+
"""Compare paths using extracted features."""
|
|
41
|
+
f1 = features(path1)
|
|
42
|
+
f2 = features(path2)
|
|
43
|
+
|
|
44
|
+
# Numeric features to compare
|
|
45
|
+
numeric_keys = [
|
|
46
|
+
"path_length",
|
|
47
|
+
"turns",
|
|
48
|
+
"direct_distance",
|
|
49
|
+
"straightness",
|
|
50
|
+
"self_intersections",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
differences = {}
|
|
54
|
+
for key in numeric_keys:
|
|
55
|
+
diff = abs(f1.get(key, 0) - f2.get(key, 0))
|
|
56
|
+
differences[key] = diff
|
|
57
|
+
|
|
58
|
+
# Direction histogram comparison (L1 distance)
|
|
59
|
+
hist1 = f1.get("direction_histogram", {})
|
|
60
|
+
hist2 = f2.get("direction_histogram", {})
|
|
61
|
+
hist_diff = sum(abs(hist1.get(k, 0) - hist2.get(k, 0)) for k in set(hist1) | set(hist2))
|
|
62
|
+
|
|
63
|
+
# Combined similarity score (lower = more similar)
|
|
64
|
+
total_diff = sum(differences.values()) + hist_diff * 0.1
|
|
65
|
+
|
|
66
|
+
return {
|
|
67
|
+
"method": "features",
|
|
68
|
+
"feature_differences": differences,
|
|
69
|
+
"direction_histogram_diff": hist_diff,
|
|
70
|
+
"total_difference": total_diff,
|
|
71
|
+
"similarity_score": 1.0 / (1.0 + total_diff),
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _compare_hausdorff(path1: Path3D, path2: Path3D) -> dict:
|
|
76
|
+
"""Compute Hausdorff distance between path vertex sets."""
|
|
77
|
+
v1 = np.array(path1.vertices)
|
|
78
|
+
v2 = np.array(path2.vertices)
|
|
79
|
+
|
|
80
|
+
# Guard against empty vertex sets (defensive: Path3D normally enforces vertices exist)
|
|
81
|
+
if v1.size == 0 and v2.size == 0:
|
|
82
|
+
forward = 0.0
|
|
83
|
+
backward = 0.0
|
|
84
|
+
elif v1.size == 0:
|
|
85
|
+
forward = math.inf
|
|
86
|
+
backward = 0.0
|
|
87
|
+
elif v2.size == 0:
|
|
88
|
+
forward = 0.0
|
|
89
|
+
backward = math.inf
|
|
90
|
+
else:
|
|
91
|
+
# Forward Hausdorff: max distance from any point in v1 to nearest in v2
|
|
92
|
+
forward = max(np.linalg.norm(p - v2, axis=1).min() for p in v1)
|
|
93
|
+
|
|
94
|
+
# Backward: max distance from any point in v2 to nearest in v1
|
|
95
|
+
backward = max(np.linalg.norm(p - v1, axis=1).min() for p in v2)
|
|
96
|
+
|
|
97
|
+
hausdorff = max(forward, backward)
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
"method": "hausdorff",
|
|
101
|
+
"forward_hausdorff": float(forward),
|
|
102
|
+
"backward_hausdorff": float(backward),
|
|
103
|
+
"hausdorff_distance": float(hausdorff),
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _compare_dtw(path1: Path3D, path2: Path3D, max_cost: Optional[float] = None) -> dict:
|
|
108
|
+
"""Compute Dynamic Time Warping distance between paths."""
|
|
109
|
+
# For 3D paths, use vertex positions as time series
|
|
110
|
+
v1 = np.array(path1.vertices)
|
|
111
|
+
v2 = np.array(path2.vertices)
|
|
112
|
+
|
|
113
|
+
n, m = len(v1), len(v2)
|
|
114
|
+
|
|
115
|
+
# DTW matrix
|
|
116
|
+
dtw = np.full((n + 1, m + 1), float("inf"))
|
|
117
|
+
dtw[0, 0] = 0
|
|
118
|
+
|
|
119
|
+
for i in range(1, n + 1):
|
|
120
|
+
for j in range(1, m + 1):
|
|
121
|
+
cost = np.linalg.norm(v1[i - 1] - v2[j - 1])
|
|
122
|
+
dtw[i, j] = cost + min(
|
|
123
|
+
dtw[i - 1, j], # insertion
|
|
124
|
+
dtw[i, j - 1], # deletion
|
|
125
|
+
dtw[i - 1, j - 1], # match
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
dtw_distance = dtw[n, m]
|
|
129
|
+
|
|
130
|
+
# Normalize by path length
|
|
131
|
+
normalized = dtw_distance / ((n + m) / 2)
|
|
132
|
+
|
|
133
|
+
return {
|
|
134
|
+
"method": "dtw",
|
|
135
|
+
"dtw_distance": float(dtw_distance),
|
|
136
|
+
"normalized_dtw": float(normalized),
|
|
137
|
+
"path1_length": n,
|
|
138
|
+
"path2_length": m,
|
|
139
|
+
}
|
bin2path/decode.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"""Decode a 3D path back into a number (L/R/U/D scheme with local orientation)."""
|
|
2
|
+
|
|
3
|
+
from bin2path.path import Path3D
|
|
4
|
+
from bin2path.encode import _decode_bits_from_dirs # type: ignore
|
|
5
|
+
from bin2path.orient import infer_dirs_from_path
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def decode(path: Path3D) -> int:
|
|
9
|
+
"""
|
|
10
|
+
Decode a 3D path back into the original number.
|
|
11
|
+
|
|
12
|
+
Steps:
|
|
13
|
+
1) edges -> L/R/U/D using local-frame inference
|
|
14
|
+
2) dirs -> bits via _decode_bits_from_dirs
|
|
15
|
+
3) bits (MSB->LSB) -> integer
|
|
16
|
+
"""
|
|
17
|
+
if not path.vertices:
|
|
18
|
+
raise ValueError("Path must have at least one vertex")
|
|
19
|
+
|
|
20
|
+
if len(path.edges) == 0:
|
|
21
|
+
return 0
|
|
22
|
+
|
|
23
|
+
if len(path.edges) != len(path.vertices) - 1:
|
|
24
|
+
raise ValueError("Invalid path: edges count mismatch")
|
|
25
|
+
|
|
26
|
+
dirs = infer_dirs_from_path(path.vertices, path.edges)
|
|
27
|
+
|
|
28
|
+
bits = _decode_bits_from_dirs(dirs)
|
|
29
|
+
|
|
30
|
+
n = 0
|
|
31
|
+
for b in bits:
|
|
32
|
+
n = (n << 1) | b
|
|
33
|
+
return n
|
bin2path/encode.py
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
"""Encode a number into a 3D path using the L/R/U/D cellular-automaton scheme."""
|
|
2
|
+
|
|
3
|
+
from bin2path.path import Path3D, PathMetadata
|
|
4
|
+
from bin2path.orient import apply_step, Vec3
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _get_bits_msb(n: int) -> list[int]:
|
|
8
|
+
"""Return bits of n in MSB->LSB order."""
|
|
9
|
+
if n == 0:
|
|
10
|
+
return [0]
|
|
11
|
+
bits: list[int] = []
|
|
12
|
+
while n > 0:
|
|
13
|
+
bits.insert(0, n & 1)
|
|
14
|
+
n >>= 1
|
|
15
|
+
return bits
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
START_FORWARD: Vec3 = (0, 0, 1)
|
|
19
|
+
START_UP: Vec3 = (0, 1, 0)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def _encode_dirs(bits: list[int]) -> list[str]:
|
|
23
|
+
"""
|
|
24
|
+
Encode bit sequence (MSB first) into sequence of direction symbols L/R/U/D.
|
|
25
|
+
|
|
26
|
+
Rules:
|
|
27
|
+
- first bit:
|
|
28
|
+
0 -> L
|
|
29
|
+
1 -> R
|
|
30
|
+
- subsequent bits:
|
|
31
|
+
if bit == 0:
|
|
32
|
+
if previous step == L -> D
|
|
33
|
+
else -> L
|
|
34
|
+
if bit == 1:
|
|
35
|
+
if previous step == R -> U
|
|
36
|
+
else -> R
|
|
37
|
+
"""
|
|
38
|
+
if not bits:
|
|
39
|
+
return []
|
|
40
|
+
|
|
41
|
+
dirs: list[str] = []
|
|
42
|
+
|
|
43
|
+
# first bit
|
|
44
|
+
first = bits[0]
|
|
45
|
+
cur_dir = "L" if first == 0 else "R"
|
|
46
|
+
dirs.append(cur_dir)
|
|
47
|
+
|
|
48
|
+
for b in bits[1:]:
|
|
49
|
+
if b == 0:
|
|
50
|
+
if cur_dir == "L":
|
|
51
|
+
cur_dir = "D"
|
|
52
|
+
else:
|
|
53
|
+
cur_dir = "L"
|
|
54
|
+
else: # b == 1
|
|
55
|
+
if cur_dir == "R":
|
|
56
|
+
cur_dir = "U"
|
|
57
|
+
else:
|
|
58
|
+
cur_dir = "R"
|
|
59
|
+
dirs.append(cur_dir)
|
|
60
|
+
|
|
61
|
+
return dirs
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _decode_bits_from_dirs(dirs: list[str]) -> list[int]:
|
|
65
|
+
"""
|
|
66
|
+
Inverse of _encode_dirs with simplified rule:
|
|
67
|
+
- first step: L->0, R->1
|
|
68
|
+
- subsequent steps: (L,D)->0, (R,U)->1
|
|
69
|
+
"""
|
|
70
|
+
if not dirs:
|
|
71
|
+
return [0]
|
|
72
|
+
|
|
73
|
+
bits: list[int] = []
|
|
74
|
+
|
|
75
|
+
first = dirs[0]
|
|
76
|
+
if first == "L":
|
|
77
|
+
bits.append(0)
|
|
78
|
+
elif first == "R":
|
|
79
|
+
bits.append(1)
|
|
80
|
+
else:
|
|
81
|
+
raise ValueError(f"Invalid first direction {first}, expected 'L' or 'R'")
|
|
82
|
+
|
|
83
|
+
for cur in dirs[1:]:
|
|
84
|
+
if cur in ("L", "D"):
|
|
85
|
+
bits.append(0)
|
|
86
|
+
elif cur in ("R", "U"):
|
|
87
|
+
bits.append(1)
|
|
88
|
+
else:
|
|
89
|
+
raise ValueError(f"Invalid direction symbol {cur}")
|
|
90
|
+
|
|
91
|
+
return bits
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def encode(number: int) -> Path3D:
|
|
95
|
+
"""
|
|
96
|
+
Encode a natural number into a 3D path.
|
|
97
|
+
|
|
98
|
+
The binary representation (MSB first) is mapped to a sequence of symbols
|
|
99
|
+
L/R/U/D using the rules above. Symbols are then interpreted as *relative*
|
|
100
|
+
moves using a local orientation frame starting with forward=+Z and up=+Y.
|
|
101
|
+
"""
|
|
102
|
+
if not isinstance(number, int):
|
|
103
|
+
raise TypeError(f"Expected integer, got {type(number).__name__}")
|
|
104
|
+
if number < 0:
|
|
105
|
+
raise ValueError("Number must be non-negative")
|
|
106
|
+
|
|
107
|
+
bits = _get_bits_msb(number)
|
|
108
|
+
bits_length = len(bits)
|
|
109
|
+
|
|
110
|
+
# trivial path for 0: stay at origin
|
|
111
|
+
if number == 0:
|
|
112
|
+
metadata = PathMetadata(
|
|
113
|
+
original_number=0,
|
|
114
|
+
bits_length=1,
|
|
115
|
+
first_one_pos=0,
|
|
116
|
+
step_positions=[],
|
|
117
|
+
start_direction=START_FORWARD,
|
|
118
|
+
)
|
|
119
|
+
return Path3D(vertices=[(0, 0, 0)], edges=[], metadata=metadata)
|
|
120
|
+
|
|
121
|
+
dirs = _encode_dirs(bits)
|
|
122
|
+
|
|
123
|
+
current_point: Vec3 = (0, 0, 0)
|
|
124
|
+
vertices: list[tuple[int, int, int]] = [current_point]
|
|
125
|
+
edges: list[tuple[int, int]] = []
|
|
126
|
+
|
|
127
|
+
forward = START_FORWARD
|
|
128
|
+
up = START_UP
|
|
129
|
+
|
|
130
|
+
for d in dirs:
|
|
131
|
+
vec, forward, up = apply_step(d, forward, up)
|
|
132
|
+
new_point = (
|
|
133
|
+
current_point[0] + vec[0],
|
|
134
|
+
current_point[1] + vec[1],
|
|
135
|
+
current_point[2] + vec[2],
|
|
136
|
+
)
|
|
137
|
+
vertices.append(new_point)
|
|
138
|
+
edges.append((len(vertices) - 2, len(vertices) - 1))
|
|
139
|
+
current_point = new_point
|
|
140
|
+
|
|
141
|
+
step_positions = [i for i, b in enumerate(bits) if b == 1]
|
|
142
|
+
|
|
143
|
+
metadata = PathMetadata(
|
|
144
|
+
original_number=number,
|
|
145
|
+
bits_length=bits_length,
|
|
146
|
+
first_one_pos=step_positions[0] if step_positions else 0,
|
|
147
|
+
step_positions=step_positions,
|
|
148
|
+
start_direction=START_FORWARD,
|
|
149
|
+
)
|
|
150
|
+
|
|
151
|
+
return Path3D(vertices=vertices, edges=edges, metadata=metadata)
|