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 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)