openscvx 0.3.2.dev170__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.
Potentially problematic release.
This version of openscvx might be problematic. Click here for more details.
- openscvx/__init__.py +123 -0
- openscvx/_version.py +34 -0
- openscvx/algorithms/__init__.py +92 -0
- openscvx/algorithms/autotuning.py +24 -0
- openscvx/algorithms/base.py +351 -0
- openscvx/algorithms/optimization_results.py +215 -0
- openscvx/algorithms/penalized_trust_region.py +384 -0
- openscvx/config.py +437 -0
- openscvx/discretization/__init__.py +47 -0
- openscvx/discretization/discretization.py +236 -0
- openscvx/expert/__init__.py +23 -0
- openscvx/expert/byof.py +326 -0
- openscvx/expert/lowering.py +419 -0
- openscvx/expert/validation.py +357 -0
- openscvx/integrators/__init__.py +48 -0
- openscvx/integrators/runge_kutta.py +281 -0
- openscvx/lowered/__init__.py +30 -0
- openscvx/lowered/cvxpy_constraints.py +23 -0
- openscvx/lowered/cvxpy_variables.py +124 -0
- openscvx/lowered/dynamics.py +34 -0
- openscvx/lowered/jax_constraints.py +133 -0
- openscvx/lowered/parameters.py +54 -0
- openscvx/lowered/problem.py +70 -0
- openscvx/lowered/unified.py +718 -0
- openscvx/plotting/__init__.py +63 -0
- openscvx/plotting/plotting.py +756 -0
- openscvx/plotting/scp_iteration.py +299 -0
- openscvx/plotting/viser/__init__.py +126 -0
- openscvx/plotting/viser/animated.py +605 -0
- openscvx/plotting/viser/plotly_integration.py +333 -0
- openscvx/plotting/viser/primitives.py +355 -0
- openscvx/plotting/viser/scp.py +459 -0
- openscvx/plotting/viser/server.py +112 -0
- openscvx/problem.py +734 -0
- openscvx/propagation/__init__.py +60 -0
- openscvx/propagation/post_processing.py +104 -0
- openscvx/propagation/propagation.py +248 -0
- openscvx/solvers/__init__.py +51 -0
- openscvx/solvers/cvxpy.py +226 -0
- openscvx/symbolic/__init__.py +9 -0
- openscvx/symbolic/augmentation.py +630 -0
- openscvx/symbolic/builder.py +492 -0
- openscvx/symbolic/constraint_set.py +92 -0
- openscvx/symbolic/expr/__init__.py +222 -0
- openscvx/symbolic/expr/arithmetic.py +517 -0
- openscvx/symbolic/expr/array.py +632 -0
- openscvx/symbolic/expr/constraint.py +796 -0
- openscvx/symbolic/expr/control.py +135 -0
- openscvx/symbolic/expr/expr.py +720 -0
- openscvx/symbolic/expr/lie/__init__.py +87 -0
- openscvx/symbolic/expr/lie/adjoint.py +357 -0
- openscvx/symbolic/expr/lie/se3.py +172 -0
- openscvx/symbolic/expr/lie/so3.py +138 -0
- openscvx/symbolic/expr/linalg.py +279 -0
- openscvx/symbolic/expr/math.py +699 -0
- openscvx/symbolic/expr/spatial.py +209 -0
- openscvx/symbolic/expr/state.py +607 -0
- openscvx/symbolic/expr/stl.py +136 -0
- openscvx/symbolic/expr/variable.py +321 -0
- openscvx/symbolic/hashing.py +112 -0
- openscvx/symbolic/lower.py +760 -0
- openscvx/symbolic/lowerers/__init__.py +106 -0
- openscvx/symbolic/lowerers/cvxpy.py +1302 -0
- openscvx/symbolic/lowerers/jax.py +1382 -0
- openscvx/symbolic/preprocessing.py +757 -0
- openscvx/symbolic/problem.py +110 -0
- openscvx/symbolic/time.py +116 -0
- openscvx/symbolic/unified.py +420 -0
- openscvx/utils/__init__.py +20 -0
- openscvx/utils/cache.py +131 -0
- openscvx/utils/caching.py +210 -0
- openscvx/utils/printing.py +301 -0
- openscvx/utils/profiling.py +37 -0
- openscvx/utils/utils.py +100 -0
- openscvx-0.3.2.dev170.dist-info/METADATA +350 -0
- openscvx-0.3.2.dev170.dist-info/RECORD +79 -0
- openscvx-0.3.2.dev170.dist-info/WHEEL +5 -0
- openscvx-0.3.2.dev170.dist-info/licenses/LICENSE +201 -0
- openscvx-0.3.2.dev170.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,459 @@
|
|
|
1
|
+
"""SCP iteration visualization components for viser.
|
|
2
|
+
|
|
3
|
+
This module contains functions for visualizing the successive convex programming
|
|
4
|
+
(SCP) optimization process, showing how the solution evolves across iterations.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import threading
|
|
8
|
+
import time
|
|
9
|
+
from typing import Callable
|
|
10
|
+
|
|
11
|
+
import matplotlib.pyplot as plt
|
|
12
|
+
import numpy as np
|
|
13
|
+
import viser
|
|
14
|
+
|
|
15
|
+
# Type alias for update callbacks
|
|
16
|
+
UpdateCallback = Callable[[int], None]
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def add_scp_iteration_nodes(
|
|
20
|
+
server: viser.ViserServer,
|
|
21
|
+
positions: list[np.ndarray],
|
|
22
|
+
colors: list[tuple[int, int, int]] | None = None,
|
|
23
|
+
point_size: float = 0.3,
|
|
24
|
+
cmap_name: str = "viridis",
|
|
25
|
+
) -> tuple[list[viser.PointCloudHandle], UpdateCallback]:
|
|
26
|
+
"""Add animated optimization nodes that update per SCP iteration.
|
|
27
|
+
|
|
28
|
+
Pre-buffers point clouds for all iterations and toggles visibility for performance.
|
|
29
|
+
This avoids transmitting point data on every frame update.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
server: ViserServer instance
|
|
33
|
+
positions: List of position arrays per iteration, each shape (N, 3)
|
|
34
|
+
colors: Optional list of RGB colors per iteration. If None, uses viridis colormap.
|
|
35
|
+
point_size: Size of node markers
|
|
36
|
+
cmap_name: Matplotlib colormap name (default: "viridis")
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Tuple of (list of point_handles, update_callback)
|
|
40
|
+
"""
|
|
41
|
+
n_iterations = len(positions)
|
|
42
|
+
|
|
43
|
+
# Default: use viridis colormap
|
|
44
|
+
if colors is None:
|
|
45
|
+
cmap = plt.get_cmap(cmap_name)
|
|
46
|
+
colors = []
|
|
47
|
+
for i in range(n_iterations):
|
|
48
|
+
t = i / max(n_iterations - 1, 1)
|
|
49
|
+
rgb = cmap(t)[:3]
|
|
50
|
+
colors.append(tuple(int(c * 255) for c in rgb))
|
|
51
|
+
|
|
52
|
+
# Convert colors to numpy arrays for viser compatibility
|
|
53
|
+
colors_np = [np.array([c[0], c[1], c[2]], dtype=np.uint8) for c in colors]
|
|
54
|
+
|
|
55
|
+
# Pre-create point clouds for all iterations (only first visible initially)
|
|
56
|
+
handles = []
|
|
57
|
+
for i in range(n_iterations):
|
|
58
|
+
pos = np.asarray(positions[i], dtype=np.float32)
|
|
59
|
+
handle = server.scene.add_point_cloud(
|
|
60
|
+
f"/scp/nodes/iter_{i}",
|
|
61
|
+
points=pos,
|
|
62
|
+
colors=colors_np[i],
|
|
63
|
+
point_size=point_size,
|
|
64
|
+
visible=(i == 0),
|
|
65
|
+
)
|
|
66
|
+
handles.append(handle)
|
|
67
|
+
|
|
68
|
+
# Track current visible iteration to minimize visibility toggles
|
|
69
|
+
state = {"current_idx": 0}
|
|
70
|
+
|
|
71
|
+
def update(iter_idx: int) -> None:
|
|
72
|
+
idx = min(iter_idx, n_iterations - 1)
|
|
73
|
+
if idx != state["current_idx"]:
|
|
74
|
+
handles[state["current_idx"]].visible = False
|
|
75
|
+
handles[idx].visible = True
|
|
76
|
+
state["current_idx"] = idx
|
|
77
|
+
|
|
78
|
+
return handles, update
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def add_scp_iteration_attitudes(
|
|
82
|
+
server: viser.ViserServer,
|
|
83
|
+
positions: list[np.ndarray],
|
|
84
|
+
attitudes: list[np.ndarray] | None,
|
|
85
|
+
axes_length: float = 1.5,
|
|
86
|
+
axes_radius: float = 0.03,
|
|
87
|
+
stride: int = 1,
|
|
88
|
+
) -> tuple[list[viser.FrameHandle], UpdateCallback | None]:
|
|
89
|
+
"""Add animated attitude frames at each node that update per SCP iteration.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
server: ViserServer instance
|
|
93
|
+
positions: List of position arrays per iteration, each shape (N, 3)
|
|
94
|
+
attitudes: List of quaternion arrays per iteration, each shape (N, 4) in wxyz format.
|
|
95
|
+
If None, returns empty list and None callback.
|
|
96
|
+
axes_length: Length of coordinate frame axes
|
|
97
|
+
axes_radius: Radius of axes cylinders
|
|
98
|
+
stride: Show attitude frame every `stride` nodes (1 = all nodes)
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
Tuple of (list of frame handles, update_callback)
|
|
102
|
+
"""
|
|
103
|
+
if attitudes is None:
|
|
104
|
+
return [], None
|
|
105
|
+
|
|
106
|
+
n_iterations = len(positions)
|
|
107
|
+
n_nodes = len(positions[0])
|
|
108
|
+
|
|
109
|
+
# Create frame handles for nodes at stride intervals
|
|
110
|
+
node_indices = list(range(0, n_nodes, stride))
|
|
111
|
+
handles = []
|
|
112
|
+
|
|
113
|
+
for i, node_idx in enumerate(node_indices):
|
|
114
|
+
handle = server.scene.add_frame(
|
|
115
|
+
f"/scp/attitudes/frame_{i}",
|
|
116
|
+
wxyz=attitudes[0][node_idx],
|
|
117
|
+
position=positions[0][node_idx],
|
|
118
|
+
axes_length=axes_length,
|
|
119
|
+
axes_radius=axes_radius,
|
|
120
|
+
)
|
|
121
|
+
handles.append(handle)
|
|
122
|
+
|
|
123
|
+
def update(iter_idx: int) -> None:
|
|
124
|
+
idx = min(iter_idx, n_iterations - 1)
|
|
125
|
+
pos = positions[idx]
|
|
126
|
+
att = attitudes[idx]
|
|
127
|
+
|
|
128
|
+
for i, node_idx in enumerate(node_indices):
|
|
129
|
+
# Handle case where number of nodes changes between iterations
|
|
130
|
+
if node_idx < len(pos) and node_idx < len(att):
|
|
131
|
+
handles[i].position = pos[node_idx]
|
|
132
|
+
handles[i].wxyz = att[node_idx]
|
|
133
|
+
|
|
134
|
+
return handles, update
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def add_scp_ghost_iterations(
|
|
138
|
+
server: viser.ViserServer,
|
|
139
|
+
positions: list[np.ndarray],
|
|
140
|
+
point_size: float = 0.15,
|
|
141
|
+
cmap_name: str = "viridis",
|
|
142
|
+
) -> tuple[list[viser.PointCloudHandle], UpdateCallback]:
|
|
143
|
+
"""Add ghost trails showing all previous SCP iterations.
|
|
144
|
+
|
|
145
|
+
Pre-buffers point clouds for all iterations and toggles visibility for performance.
|
|
146
|
+
Shows all previous iterations with viridis coloring to visualize convergence.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
server: ViserServer instance
|
|
150
|
+
positions: List of position arrays per iteration, each shape (N, 3)
|
|
151
|
+
point_size: Size of ghost points
|
|
152
|
+
cmap_name: Matplotlib colormap name for ghost colors
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
Tuple of (list of handles, update_callback)
|
|
156
|
+
"""
|
|
157
|
+
n_iterations = len(positions)
|
|
158
|
+
cmap = plt.get_cmap(cmap_name)
|
|
159
|
+
|
|
160
|
+
# Pre-create point clouds for all iterations with their colors
|
|
161
|
+
# (all initially hidden, shown progressively as ghosts)
|
|
162
|
+
handles = []
|
|
163
|
+
for i in range(n_iterations):
|
|
164
|
+
t = i / max(n_iterations - 1, 1)
|
|
165
|
+
rgb = cmap(t)[:3]
|
|
166
|
+
color = np.array([int(c * 255) for c in rgb], dtype=np.uint8)
|
|
167
|
+
pos = np.asarray(positions[i], dtype=np.float32)
|
|
168
|
+
|
|
169
|
+
handle = server.scene.add_point_cloud(
|
|
170
|
+
f"/scp/ghosts/iter_{i}",
|
|
171
|
+
points=pos,
|
|
172
|
+
colors=color,
|
|
173
|
+
point_size=point_size,
|
|
174
|
+
visible=False, # All start hidden
|
|
175
|
+
)
|
|
176
|
+
handles.append(handle)
|
|
177
|
+
|
|
178
|
+
# Track which iterations are currently visible as ghosts
|
|
179
|
+
state = {"visible_up_to": -1}
|
|
180
|
+
|
|
181
|
+
def update(iter_idx: int) -> None:
|
|
182
|
+
idx = min(iter_idx, n_iterations - 1)
|
|
183
|
+
# Ghosts are iterations 0 through idx-1 (everything before current)
|
|
184
|
+
new_visible_up_to = idx - 1
|
|
185
|
+
|
|
186
|
+
if new_visible_up_to != state["visible_up_to"]:
|
|
187
|
+
# Show/hide only the iterations that changed
|
|
188
|
+
if new_visible_up_to > state["visible_up_to"]:
|
|
189
|
+
# Show newly visible ghosts
|
|
190
|
+
for i in range(state["visible_up_to"] + 1, new_visible_up_to + 1):
|
|
191
|
+
handles[i].visible = True
|
|
192
|
+
else:
|
|
193
|
+
# Hide ghosts that should no longer be visible
|
|
194
|
+
for i in range(new_visible_up_to + 1, state["visible_up_to"] + 1):
|
|
195
|
+
handles[i].visible = False
|
|
196
|
+
state["visible_up_to"] = new_visible_up_to
|
|
197
|
+
|
|
198
|
+
return handles, update
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def extract_propagation_positions(
|
|
202
|
+
discretization_history: list[np.ndarray],
|
|
203
|
+
n_x: int,
|
|
204
|
+
n_u: int,
|
|
205
|
+
position_slice: slice,
|
|
206
|
+
scene_scale: float = 1.0,
|
|
207
|
+
) -> list[list[np.ndarray]]:
|
|
208
|
+
"""Extract 3D position trajectories from discretization history.
|
|
209
|
+
|
|
210
|
+
The discretization history contains the multi-shot integration results.
|
|
211
|
+
Each V matrix has shape (flattened_size, n_timesteps) where:
|
|
212
|
+
- flattened_size = (N-1) * i4
|
|
213
|
+
- i4 = n_x + n_x*n_x + 2*n_x*n_u (state + STM + control influence matrices)
|
|
214
|
+
- n_timesteps = number of integration substeps
|
|
215
|
+
|
|
216
|
+
Args:
|
|
217
|
+
discretization_history: List of V matrices from each SCP iteration
|
|
218
|
+
n_x: Number of states
|
|
219
|
+
n_u: Number of controls
|
|
220
|
+
position_slice: Slice for extracting position from state vector
|
|
221
|
+
scene_scale: Divide positions by this factor for visualization
|
|
222
|
+
|
|
223
|
+
Returns:
|
|
224
|
+
List of propagation trajectories per iteration.
|
|
225
|
+
Each iteration contains a list of (n_substeps, 3) arrays, one per segment.
|
|
226
|
+
"""
|
|
227
|
+
if not discretization_history:
|
|
228
|
+
return []
|
|
229
|
+
|
|
230
|
+
i4 = n_x + n_x * n_x + 2 * n_x * n_u
|
|
231
|
+
propagations = []
|
|
232
|
+
|
|
233
|
+
for V in discretization_history:
|
|
234
|
+
# V shape: (flattened_size, n_timesteps)
|
|
235
|
+
n_timesteps = V.shape[1]
|
|
236
|
+
n_segments = V.shape[0] // i4 # N-1 segments
|
|
237
|
+
|
|
238
|
+
iteration_segments = []
|
|
239
|
+
for seg_idx in range(n_segments):
|
|
240
|
+
# Extract this segment's data across all timesteps
|
|
241
|
+
seg_start = seg_idx * i4
|
|
242
|
+
seg_end = seg_start + i4
|
|
243
|
+
|
|
244
|
+
# For each timestep, extract the position from the state
|
|
245
|
+
segment_positions = []
|
|
246
|
+
for t_idx in range(n_timesteps):
|
|
247
|
+
# Get full state at this segment and timestep
|
|
248
|
+
state = V[seg_start:seg_end, t_idx][:n_x]
|
|
249
|
+
# Extract position components
|
|
250
|
+
pos = state[position_slice] / scene_scale
|
|
251
|
+
segment_positions.append(pos)
|
|
252
|
+
|
|
253
|
+
iteration_segments.append(np.array(segment_positions, dtype=np.float32))
|
|
254
|
+
|
|
255
|
+
propagations.append(iteration_segments)
|
|
256
|
+
|
|
257
|
+
return propagations
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def add_scp_propagation_lines(
|
|
261
|
+
server: viser.ViserServer,
|
|
262
|
+
propagations: list[list[np.ndarray]],
|
|
263
|
+
line_width: float = 2.0,
|
|
264
|
+
cmap_name: str = "viridis",
|
|
265
|
+
) -> tuple[list, UpdateCallback]:
|
|
266
|
+
"""Add animated nonlinear propagation lines that update per SCP iteration.
|
|
267
|
+
|
|
268
|
+
Shows the actual integrated trajectory between optimization nodes,
|
|
269
|
+
revealing defects (gaps) in early iterations that close as SCP converges.
|
|
270
|
+
All iterations up to the current one are shown with viridis coloring,
|
|
271
|
+
similar to ghost iterations for nodes.
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
server: ViserServer instance
|
|
275
|
+
propagations: List of propagation trajectories per iteration from
|
|
276
|
+
extract_propagation_positions(). Each iteration contains a list
|
|
277
|
+
of (n_substeps, 3) position arrays, one per segment.
|
|
278
|
+
line_width: Width of propagation lines
|
|
279
|
+
cmap_name: Matplotlib colormap name (default: "viridis")
|
|
280
|
+
|
|
281
|
+
Returns:
|
|
282
|
+
Tuple of (list of line handles, update_callback)
|
|
283
|
+
"""
|
|
284
|
+
if not propagations:
|
|
285
|
+
return [], lambda _: None
|
|
286
|
+
|
|
287
|
+
n_iterations = len(propagations)
|
|
288
|
+
n_segments = len(propagations[0])
|
|
289
|
+
cmap = plt.get_cmap(cmap_name)
|
|
290
|
+
|
|
291
|
+
# Pre-compute colors for each iteration
|
|
292
|
+
iteration_colors = []
|
|
293
|
+
for i in range(n_iterations):
|
|
294
|
+
t = i / max(n_iterations - 1, 1)
|
|
295
|
+
rgb = cmap(t)[:3]
|
|
296
|
+
iteration_colors.append(np.array([int(c * 255) for c in rgb], dtype=np.uint8))
|
|
297
|
+
|
|
298
|
+
# Create line handles for each (iteration, segment) pair
|
|
299
|
+
# Structure: handles[iter_idx][seg_idx]
|
|
300
|
+
all_handles = []
|
|
301
|
+
|
|
302
|
+
for iter_idx in range(n_iterations):
|
|
303
|
+
iter_handles = []
|
|
304
|
+
color = iteration_colors[iter_idx]
|
|
305
|
+
|
|
306
|
+
for seg_idx in range(n_segments):
|
|
307
|
+
seg_pos = propagations[iter_idx][seg_idx] # Shape (n_substeps, 3)
|
|
308
|
+
|
|
309
|
+
if len(seg_pos) < 2:
|
|
310
|
+
iter_handles.append(None)
|
|
311
|
+
continue
|
|
312
|
+
|
|
313
|
+
# Create line segments connecting consecutive substeps
|
|
314
|
+
segments = np.array(
|
|
315
|
+
[[seg_pos[i], seg_pos[i + 1]] for i in range(len(seg_pos) - 1)],
|
|
316
|
+
dtype=np.float32,
|
|
317
|
+
)
|
|
318
|
+
|
|
319
|
+
handle = server.scene.add_line_segments(
|
|
320
|
+
f"/scp/propagation/iter_{iter_idx}/segment_{seg_idx}",
|
|
321
|
+
points=segments,
|
|
322
|
+
colors=color,
|
|
323
|
+
line_width=line_width,
|
|
324
|
+
visible=(iter_idx == 0), # Only first iteration visible initially
|
|
325
|
+
)
|
|
326
|
+
iter_handles.append(handle)
|
|
327
|
+
|
|
328
|
+
all_handles.append(iter_handles)
|
|
329
|
+
|
|
330
|
+
def update(iter_idx: int) -> None:
|
|
331
|
+
idx = min(iter_idx, n_iterations - 1)
|
|
332
|
+
|
|
333
|
+
# Show all iterations up to and including current, hide the rest
|
|
334
|
+
for i in range(n_iterations):
|
|
335
|
+
should_show = i <= idx
|
|
336
|
+
for handle in all_handles[i]:
|
|
337
|
+
if handle is not None:
|
|
338
|
+
handle.visible = should_show
|
|
339
|
+
|
|
340
|
+
return all_handles, update
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def add_scp_animation_controls(
|
|
344
|
+
server: viser.ViserServer,
|
|
345
|
+
n_iterations: int,
|
|
346
|
+
update_callbacks: list[UpdateCallback],
|
|
347
|
+
autoplay: bool = False,
|
|
348
|
+
frame_duration_ms: int = 500,
|
|
349
|
+
folder_name: str = "SCP Animation",
|
|
350
|
+
) -> None:
|
|
351
|
+
"""Add GUI controls for stepping through SCP iterations.
|
|
352
|
+
|
|
353
|
+
Creates play/pause button, step buttons, iteration slider, and speed control.
|
|
354
|
+
|
|
355
|
+
Args:
|
|
356
|
+
server: ViserServer instance
|
|
357
|
+
n_iterations: Total number of SCP iterations
|
|
358
|
+
update_callbacks: List of update functions to call each iteration
|
|
359
|
+
autoplay: Whether to start playing automatically
|
|
360
|
+
frame_duration_ms: Default milliseconds per iteration frame
|
|
361
|
+
folder_name: Name for the GUI folder
|
|
362
|
+
"""
|
|
363
|
+
# Filter out None callbacks
|
|
364
|
+
callbacks = [cb for cb in update_callbacks if cb is not None]
|
|
365
|
+
|
|
366
|
+
def update_all(iter_idx: int) -> None:
|
|
367
|
+
"""Update all visualization components."""
|
|
368
|
+
for callback in callbacks:
|
|
369
|
+
callback(iter_idx)
|
|
370
|
+
|
|
371
|
+
# --- GUI Controls ---
|
|
372
|
+
with server.gui.add_folder(folder_name):
|
|
373
|
+
play_button = server.gui.add_button("Play")
|
|
374
|
+
with server.gui.add_folder("Step Controls", expand_by_default=False):
|
|
375
|
+
prev_button = server.gui.add_button("< Previous")
|
|
376
|
+
next_button = server.gui.add_button("Next >")
|
|
377
|
+
iter_slider = server.gui.add_slider(
|
|
378
|
+
"Iteration",
|
|
379
|
+
min=0,
|
|
380
|
+
max=n_iterations - 1,
|
|
381
|
+
step=1,
|
|
382
|
+
initial_value=0,
|
|
383
|
+
)
|
|
384
|
+
speed_slider = server.gui.add_slider(
|
|
385
|
+
"Speed (ms/iter)",
|
|
386
|
+
min=50,
|
|
387
|
+
max=2000,
|
|
388
|
+
step=50,
|
|
389
|
+
initial_value=frame_duration_ms,
|
|
390
|
+
)
|
|
391
|
+
loop_checkbox = server.gui.add_checkbox("Loop", initial_value=True)
|
|
392
|
+
|
|
393
|
+
# Animation state
|
|
394
|
+
state = {"playing": autoplay, "iteration": 0, "needs_update": True}
|
|
395
|
+
|
|
396
|
+
@play_button.on_click
|
|
397
|
+
def _(_) -> None:
|
|
398
|
+
state["playing"] = not state["playing"]
|
|
399
|
+
state["needs_update"] = True # Trigger immediate update on play
|
|
400
|
+
play_button.name = "Pause" if state["playing"] else "Play"
|
|
401
|
+
|
|
402
|
+
@prev_button.on_click
|
|
403
|
+
def _(_) -> None:
|
|
404
|
+
if state["iteration"] > 0:
|
|
405
|
+
state["iteration"] -= 1
|
|
406
|
+
iter_slider.value = state["iteration"]
|
|
407
|
+
update_all(state["iteration"])
|
|
408
|
+
|
|
409
|
+
@next_button.on_click
|
|
410
|
+
def _(_) -> None:
|
|
411
|
+
if state["iteration"] < n_iterations - 1:
|
|
412
|
+
state["iteration"] += 1
|
|
413
|
+
iter_slider.value = state["iteration"]
|
|
414
|
+
update_all(state["iteration"])
|
|
415
|
+
|
|
416
|
+
@iter_slider.on_update
|
|
417
|
+
def _(_) -> None:
|
|
418
|
+
if not state["playing"]:
|
|
419
|
+
state["iteration"] = int(iter_slider.value)
|
|
420
|
+
update_all(state["iteration"])
|
|
421
|
+
|
|
422
|
+
def animation_loop() -> None:
|
|
423
|
+
"""Background thread for SCP iteration playback."""
|
|
424
|
+
last_update = time.time()
|
|
425
|
+
while True:
|
|
426
|
+
time.sleep(0.016) # ~60 fps check rate
|
|
427
|
+
|
|
428
|
+
# Handle immediate update requests (e.g., on play button click)
|
|
429
|
+
if state["needs_update"]:
|
|
430
|
+
state["needs_update"] = False
|
|
431
|
+
last_update = time.time()
|
|
432
|
+
update_all(state["iteration"])
|
|
433
|
+
continue
|
|
434
|
+
|
|
435
|
+
if state["playing"]:
|
|
436
|
+
current_time = time.time()
|
|
437
|
+
elapsed_ms = (current_time - last_update) * 1000
|
|
438
|
+
|
|
439
|
+
if elapsed_ms >= speed_slider.value:
|
|
440
|
+
last_update = current_time
|
|
441
|
+
state["iteration"] += 1
|
|
442
|
+
|
|
443
|
+
if state["iteration"] >= n_iterations:
|
|
444
|
+
if loop_checkbox.value:
|
|
445
|
+
state["iteration"] = 0
|
|
446
|
+
else:
|
|
447
|
+
state["iteration"] = n_iterations - 1
|
|
448
|
+
state["playing"] = False
|
|
449
|
+
play_button.name = "Play"
|
|
450
|
+
|
|
451
|
+
iter_slider.value = state["iteration"]
|
|
452
|
+
update_all(state["iteration"])
|
|
453
|
+
|
|
454
|
+
# Start animation thread
|
|
455
|
+
thread = threading.Thread(target=animation_loop, daemon=True)
|
|
456
|
+
thread.start()
|
|
457
|
+
|
|
458
|
+
# Initial update to ensure first frame is fully rendered
|
|
459
|
+
update_all(0)
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
"""Viser server setup utilities."""
|
|
2
|
+
|
|
3
|
+
import matplotlib.pyplot as plt
|
|
4
|
+
import numpy as np
|
|
5
|
+
import viser
|
|
6
|
+
from viser.theme import TitlebarButton, TitlebarConfig, TitlebarImage
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def compute_velocity_colors(vel: np.ndarray, cmap_name: str = "viridis") -> np.ndarray:
|
|
10
|
+
"""Compute RGB colors based on velocity magnitude.
|
|
11
|
+
|
|
12
|
+
Args:
|
|
13
|
+
vel: Velocity array of shape (N, 3)
|
|
14
|
+
cmap_name: Matplotlib colormap name
|
|
15
|
+
|
|
16
|
+
Returns:
|
|
17
|
+
Array of RGB colors with shape (N, 3), values in [0, 255]
|
|
18
|
+
"""
|
|
19
|
+
vel_norms = np.linalg.norm(vel, axis=1)
|
|
20
|
+
vel_range = vel_norms.max() - vel_norms.min()
|
|
21
|
+
if vel_range < 1e-8:
|
|
22
|
+
vel_normalized = np.zeros_like(vel_norms)
|
|
23
|
+
else:
|
|
24
|
+
vel_normalized = (vel_norms - vel_norms.min()) / vel_range
|
|
25
|
+
|
|
26
|
+
cmap = plt.get_cmap(cmap_name)
|
|
27
|
+
colors = np.array([[int(c * 255) for c in cmap(v)[:3]] for v in vel_normalized])
|
|
28
|
+
return colors
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def compute_grid_size(pos: np.ndarray, padding: float = 1.2) -> float:
|
|
32
|
+
"""Compute grid size based on trajectory extent.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
pos: Position array of shape (N, 3)
|
|
36
|
+
padding: Padding factor (1.2 = 20% padding)
|
|
37
|
+
|
|
38
|
+
Returns:
|
|
39
|
+
Grid size (width and height)
|
|
40
|
+
"""
|
|
41
|
+
max_x = np.abs(pos[:, 0]).max()
|
|
42
|
+
max_y = np.abs(pos[:, 1]).max()
|
|
43
|
+
return max(max_x, max_y) * 2 * padding
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def create_server(
|
|
47
|
+
pos: np.ndarray,
|
|
48
|
+
dark_mode: bool = True,
|
|
49
|
+
) -> viser.ViserServer:
|
|
50
|
+
"""Create a viser server with basic scene setup.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
pos: Position array for computing grid size
|
|
54
|
+
dark_mode: Whether to use dark theme
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
ViserServer instance with grid and origin frame
|
|
58
|
+
"""
|
|
59
|
+
server = viser.ViserServer()
|
|
60
|
+
|
|
61
|
+
# Configure theme with OpenSCvx branding
|
|
62
|
+
# TitlebarButton and TitlebarConfig are TypedDict classes (create as plain dicts)
|
|
63
|
+
buttons = (
|
|
64
|
+
TitlebarButton(
|
|
65
|
+
text="Getting Started",
|
|
66
|
+
icon="Description",
|
|
67
|
+
href="https://haynec.github.io/OpenSCvx/latest/getting-started/",
|
|
68
|
+
),
|
|
69
|
+
TitlebarButton(
|
|
70
|
+
text="Docs",
|
|
71
|
+
icon="Description",
|
|
72
|
+
href="https://haynec.github.io/OpenSCvx/",
|
|
73
|
+
),
|
|
74
|
+
TitlebarButton(
|
|
75
|
+
text="GitHub",
|
|
76
|
+
icon="GitHub",
|
|
77
|
+
href="https://github.com/haynec/OpenSCvx",
|
|
78
|
+
),
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
# Add OpenSCvx logo to titlebar (loaded from GitHub)
|
|
82
|
+
logo_url = (
|
|
83
|
+
"https://raw.githubusercontent.com/haynec/OpenSCvx/main/figures/openscvx_logo_square.png"
|
|
84
|
+
)
|
|
85
|
+
image = TitlebarImage(
|
|
86
|
+
image_url_light=logo_url,
|
|
87
|
+
image_url_dark=logo_url, # Use same logo for both themes
|
|
88
|
+
image_alt="OpenSCvx",
|
|
89
|
+
href="https://github.com/haynec/OpenSCvx",
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
titlebar_config = TitlebarConfig(buttons=buttons, image=image)
|
|
93
|
+
|
|
94
|
+
server.gui.configure_theme(
|
|
95
|
+
titlebar_content=titlebar_config,
|
|
96
|
+
dark_mode=dark_mode,
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
grid_size = compute_grid_size(pos)
|
|
100
|
+
server.scene.add_grid(
|
|
101
|
+
"/grid",
|
|
102
|
+
width=grid_size,
|
|
103
|
+
height=grid_size,
|
|
104
|
+
position=np.array([0.0, 0.0, 0.0]),
|
|
105
|
+
)
|
|
106
|
+
server.scene.add_frame(
|
|
107
|
+
"/origin",
|
|
108
|
+
wxyz=(1.0, 0.0, 0.0, 0.0),
|
|
109
|
+
position=(0.0, 0.0, 0.0),
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
return server
|