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,333 @@
|
|
|
1
|
+
"""Plotly integration for viser - animated 2D plots synchronized with 3D visualization.
|
|
2
|
+
|
|
3
|
+
This module provides utilities for embedding plotly figures in viser's GUI with
|
|
4
|
+
animated markers that synchronize with the 3D animation timeline.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import plotly.graph_objects as go
|
|
9
|
+
import viser
|
|
10
|
+
|
|
11
|
+
from openscvx.algorithms import OptimizationResults
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def add_animated_plotly_vline(
|
|
15
|
+
server: viser.ViserServer,
|
|
16
|
+
fig: go.Figure,
|
|
17
|
+
time_array: np.ndarray,
|
|
18
|
+
use_trajectory_indexing: bool = True,
|
|
19
|
+
line_color: str = "red",
|
|
20
|
+
line_width: int = 2,
|
|
21
|
+
line_dash: str = "dash",
|
|
22
|
+
annotation_text: str = "Current",
|
|
23
|
+
annotation_position: str = "top",
|
|
24
|
+
folder_name: str | None = None,
|
|
25
|
+
aspect: float = 1.5,
|
|
26
|
+
) -> tuple:
|
|
27
|
+
"""Add a plotly figure to viser GUI with an animated vertical line.
|
|
28
|
+
|
|
29
|
+
This function takes any plotly figure and adds an animated vertical line that
|
|
30
|
+
synchronizes with viser's 3D animation timeline. The line shows the current
|
|
31
|
+
time position as the animation plays.
|
|
32
|
+
|
|
33
|
+
This is more generic than add_animated_plotly_marker() as it works for any
|
|
34
|
+
number of traces without needing to specify y-data for each.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
server: ViserServer instance
|
|
38
|
+
fig: Plotly figure to display
|
|
39
|
+
time_array: Time values corresponding to animation frames (N,).
|
|
40
|
+
This should match the time array passed to add_animation_controls().
|
|
41
|
+
use_trajectory_indexing: If True, frame_idx maps directly to time indices.
|
|
42
|
+
If False, searches for nearest time value (use for node-only data).
|
|
43
|
+
line_color: Color of the vertical line
|
|
44
|
+
line_width: Width of the vertical line in pixels
|
|
45
|
+
line_dash: Dash style - "solid", "dash", "dot", "dashdot"
|
|
46
|
+
annotation_text: Text to show on the line
|
|
47
|
+
annotation_position: Position of annotation - "top", "bottom", "top left", etc.
|
|
48
|
+
folder_name: Optional GUI folder name to organize plots
|
|
49
|
+
aspect: Aspect ratio for plot display (width/height)
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
Tuple of (plot_handle, update_callback)
|
|
53
|
+
|
|
54
|
+
Example::
|
|
55
|
+
|
|
56
|
+
from openscvx.plotting import plot_control, viser
|
|
57
|
+
|
|
58
|
+
# Create any plotly figure
|
|
59
|
+
fig = plot_control(results, "thrust_force")
|
|
60
|
+
|
|
61
|
+
# Add to viser with animated vertical line
|
|
62
|
+
_, update_plot = viser.add_animated_plotly_vline(
|
|
63
|
+
server, fig,
|
|
64
|
+
time_array=results.trajectory["time"].flatten(),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
# Add to animation callbacks
|
|
68
|
+
update_callbacks.append(update_plot)
|
|
69
|
+
"""
|
|
70
|
+
# Detect number of subplots in the figure
|
|
71
|
+
# Count unique xaxis/yaxis references in the layout
|
|
72
|
+
n_subplots = 1
|
|
73
|
+
if hasattr(fig, "_grid_ref") and fig._grid_ref is not None:
|
|
74
|
+
# Figure created with make_subplots - use grid dimensions
|
|
75
|
+
n_rows = len(fig._grid_ref)
|
|
76
|
+
n_cols = len(fig._grid_ref[0]) if n_rows > 0 else 1
|
|
77
|
+
n_subplots = n_rows * n_cols
|
|
78
|
+
|
|
79
|
+
# Track which shapes are our vlines (before adding new ones)
|
|
80
|
+
n_existing_shapes = len(fig.layout.shapes) if fig.layout.shapes else 0
|
|
81
|
+
|
|
82
|
+
# Add vertical line to each subplot
|
|
83
|
+
if n_subplots == 1:
|
|
84
|
+
# Single plot - add one vline
|
|
85
|
+
fig.add_vline(
|
|
86
|
+
x=time_array[0],
|
|
87
|
+
line_dash=line_dash,
|
|
88
|
+
line_color=line_color,
|
|
89
|
+
line_width=line_width,
|
|
90
|
+
annotation_text=annotation_text,
|
|
91
|
+
annotation_position=annotation_position,
|
|
92
|
+
)
|
|
93
|
+
else:
|
|
94
|
+
# Multiple subplots - add vline to each
|
|
95
|
+
# Determine grid layout
|
|
96
|
+
n_rows = len(fig._grid_ref)
|
|
97
|
+
n_cols = len(fig._grid_ref[0]) if n_rows > 0 else 1
|
|
98
|
+
|
|
99
|
+
for row_idx in range(n_rows):
|
|
100
|
+
for col_idx in range(n_cols):
|
|
101
|
+
# Add vline to this subplot
|
|
102
|
+
# Only add annotation to first subplot to avoid clutter
|
|
103
|
+
show_annotation = row_idx == 0 and col_idx == 0
|
|
104
|
+
fig.add_vline(
|
|
105
|
+
x=time_array[0],
|
|
106
|
+
line_dash=line_dash,
|
|
107
|
+
line_color=line_color,
|
|
108
|
+
line_width=line_width,
|
|
109
|
+
annotation_text=annotation_text if show_annotation else None,
|
|
110
|
+
annotation_position=annotation_position if show_annotation else None,
|
|
111
|
+
row=row_idx + 1,
|
|
112
|
+
col=col_idx + 1,
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
# Track indices of shapes we added
|
|
116
|
+
n_new_shapes = len(fig.layout.shapes) - n_existing_shapes
|
|
117
|
+
vline_shape_indices = list(range(n_existing_shapes, n_existing_shapes + n_new_shapes))
|
|
118
|
+
|
|
119
|
+
# Add to viser GUI
|
|
120
|
+
if folder_name:
|
|
121
|
+
with server.gui.add_folder(folder_name):
|
|
122
|
+
plot_handle = server.gui.add_plotly(figure=fig, aspect=aspect)
|
|
123
|
+
else:
|
|
124
|
+
plot_handle = server.gui.add_plotly(figure=fig, aspect=aspect)
|
|
125
|
+
|
|
126
|
+
# Create update callback
|
|
127
|
+
def update(frame_idx: int) -> None:
|
|
128
|
+
"""Update vertical line position based on current frame."""
|
|
129
|
+
if use_trajectory_indexing:
|
|
130
|
+
# Direct indexing: frame_idx corresponds to time index
|
|
131
|
+
idx = min(frame_idx, len(time_array) - 1)
|
|
132
|
+
else:
|
|
133
|
+
# Search for nearest time (for node-only data)
|
|
134
|
+
current_time = time_array[frame_idx]
|
|
135
|
+
idx = min(frame_idx, len(time_array) - 1)
|
|
136
|
+
|
|
137
|
+
# Update all vertical line positions
|
|
138
|
+
current_time = time_array[idx]
|
|
139
|
+
for shape_idx in vline_shape_indices:
|
|
140
|
+
fig.layout.shapes[shape_idx].x0 = current_time
|
|
141
|
+
fig.layout.shapes[shape_idx].x1 = current_time
|
|
142
|
+
|
|
143
|
+
# Trigger viser update
|
|
144
|
+
plot_handle.figure = fig
|
|
145
|
+
|
|
146
|
+
return plot_handle, update
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def add_animated_plotly_marker(
|
|
150
|
+
server: viser.ViserServer,
|
|
151
|
+
fig: go.Figure,
|
|
152
|
+
time_array: np.ndarray,
|
|
153
|
+
marker_x_data: np.ndarray,
|
|
154
|
+
marker_y_data: np.ndarray,
|
|
155
|
+
use_trajectory_indexing: bool = True,
|
|
156
|
+
marker_name: str = "Current",
|
|
157
|
+
marker_color: str = "red",
|
|
158
|
+
marker_size: int = 12,
|
|
159
|
+
folder_name: str | None = None,
|
|
160
|
+
aspect: float = 1.5,
|
|
161
|
+
) -> tuple:
|
|
162
|
+
"""Add a plotly figure to viser GUI with an animated time marker.
|
|
163
|
+
|
|
164
|
+
This function takes any plotly figure and adds an animated marker that
|
|
165
|
+
synchronizes with viser's 3D animation timeline. The marker shows the
|
|
166
|
+
current position on the plot as the animation plays.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
server: ViserServer instance
|
|
170
|
+
fig: Plotly figure to display
|
|
171
|
+
time_array: Time values corresponding to animation frames (N,).
|
|
172
|
+
This should match the time array passed to add_animation_controls().
|
|
173
|
+
marker_x_data: X-axis values for marker position (N,)
|
|
174
|
+
marker_y_data: Y-axis values for marker position (N,)
|
|
175
|
+
use_trajectory_indexing: If True, frame_idx maps directly to data indices.
|
|
176
|
+
If False, searches for nearest time value (use for node-only data).
|
|
177
|
+
marker_name: Legend name for the marker trace
|
|
178
|
+
marker_color: Color of the animated marker
|
|
179
|
+
marker_size: Size of the animated marker in points
|
|
180
|
+
folder_name: Optional GUI folder name to organize plots
|
|
181
|
+
aspect: Aspect ratio for plot display (width/height)
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
Tuple of (plot_handle, update_callback)
|
|
185
|
+
|
|
186
|
+
Example::
|
|
187
|
+
|
|
188
|
+
from openscvx.plotting import plot_vector_norm, viser
|
|
189
|
+
|
|
190
|
+
# Create any plotly figure
|
|
191
|
+
fig = plot_vector_norm(results, "thrust")
|
|
192
|
+
thrust_norms = np.linalg.norm(results.trajectory["thrust"], axis=1)
|
|
193
|
+
|
|
194
|
+
# Add to viser with animated marker
|
|
195
|
+
_, update_plot = viser.add_animated_plotly_marker(
|
|
196
|
+
server, fig,
|
|
197
|
+
time_array=results.trajectory["time"].flatten(),
|
|
198
|
+
marker_x_data=results.trajectory["time"].flatten(),
|
|
199
|
+
marker_y_data=thrust_norms,
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
# Add to animation callbacks
|
|
203
|
+
update_callbacks.append(update_plot)
|
|
204
|
+
"""
|
|
205
|
+
# Add marker trace to figure
|
|
206
|
+
marker_trace = go.Scatter(
|
|
207
|
+
x=[marker_x_data[0]],
|
|
208
|
+
y=[marker_y_data[0]],
|
|
209
|
+
mode="markers",
|
|
210
|
+
marker={"color": marker_color, "size": marker_size, "symbol": "circle"},
|
|
211
|
+
name=marker_name,
|
|
212
|
+
)
|
|
213
|
+
fig.add_trace(marker_trace)
|
|
214
|
+
marker_trace_idx = len(fig.data) - 1
|
|
215
|
+
|
|
216
|
+
# Add to viser GUI
|
|
217
|
+
if folder_name:
|
|
218
|
+
with server.gui.add_folder(folder_name):
|
|
219
|
+
plot_handle = server.gui.add_plotly(figure=fig, aspect=aspect)
|
|
220
|
+
else:
|
|
221
|
+
plot_handle = server.gui.add_plotly(figure=fig, aspect=aspect)
|
|
222
|
+
|
|
223
|
+
# Create update callback
|
|
224
|
+
def update(frame_idx: int) -> None:
|
|
225
|
+
"""Update marker position based on current frame."""
|
|
226
|
+
if use_trajectory_indexing:
|
|
227
|
+
# Direct indexing: frame_idx corresponds to data index
|
|
228
|
+
idx = min(frame_idx, len(marker_y_data) - 1)
|
|
229
|
+
else:
|
|
230
|
+
# Search for nearest time (for node-only data)
|
|
231
|
+
current_time = time_array[frame_idx]
|
|
232
|
+
idx = min(np.searchsorted(marker_x_data, current_time), len(marker_y_data) - 1)
|
|
233
|
+
|
|
234
|
+
# Update marker position
|
|
235
|
+
fig.data[marker_trace_idx].x = [marker_x_data[idx]]
|
|
236
|
+
fig.data[marker_trace_idx].y = [marker_y_data[idx]]
|
|
237
|
+
|
|
238
|
+
# Trigger viser update
|
|
239
|
+
plot_handle.figure = fig
|
|
240
|
+
|
|
241
|
+
return plot_handle, update
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def add_animated_vector_norm_plot(
|
|
245
|
+
server: viser.ViserServer,
|
|
246
|
+
results: OptimizationResults,
|
|
247
|
+
var_name: str,
|
|
248
|
+
bounds: tuple[float, float] | None = None,
|
|
249
|
+
title: str | None = None,
|
|
250
|
+
folder_name: str | None = None,
|
|
251
|
+
aspect: float = 1.5,
|
|
252
|
+
marker_color: str = "red",
|
|
253
|
+
marker_size: int = 12,
|
|
254
|
+
) -> tuple:
|
|
255
|
+
"""Add animated norm plot for a state or control variable.
|
|
256
|
+
|
|
257
|
+
Convenience wrapper around add_animated_plotly_marker() that uses
|
|
258
|
+
the existing plot_vector_norm() function to create the base plot.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
server: ViserServer instance
|
|
262
|
+
results: Optimization results containing variable data
|
|
263
|
+
var_name: Name of the state or control variable to plot
|
|
264
|
+
bounds: Optional (min, max) bounds to display on plot
|
|
265
|
+
title: Optional custom title for the plot (defaults to "‖{var_name}‖₂")
|
|
266
|
+
folder_name: Optional GUI folder name to organize plots
|
|
267
|
+
aspect: Aspect ratio for plot display (width/height)
|
|
268
|
+
marker_color: Color of the animated marker
|
|
269
|
+
marker_size: Size of the animated marker in points
|
|
270
|
+
|
|
271
|
+
Returns:
|
|
272
|
+
Tuple of (plot_handle, update_callback), or (None, None) if variable not found
|
|
273
|
+
|
|
274
|
+
Example::
|
|
275
|
+
|
|
276
|
+
from openscvx.plotting import viser
|
|
277
|
+
|
|
278
|
+
# Add animated thrust norm plot
|
|
279
|
+
_, update_thrust = viser.add_animated_vector_norm_plot(
|
|
280
|
+
server, results, "thrust",
|
|
281
|
+
title="Thrust Magnitude",
|
|
282
|
+
bounds=(0.0, max_thrust),
|
|
283
|
+
folder_name="Control Plots"
|
|
284
|
+
)
|
|
285
|
+
if update_thrust is not None:
|
|
286
|
+
update_callbacks.append(update_thrust)
|
|
287
|
+
"""
|
|
288
|
+
from openscvx.plotting import plot_vector_norm
|
|
289
|
+
|
|
290
|
+
# Check if variable exists in results
|
|
291
|
+
has_in_trajectory = bool(results.trajectory) and var_name in results.trajectory
|
|
292
|
+
has_in_nodes = var_name in results.nodes
|
|
293
|
+
|
|
294
|
+
if not (has_in_trajectory or has_in_nodes):
|
|
295
|
+
import warnings
|
|
296
|
+
|
|
297
|
+
warnings.warn(f"Variable '{var_name}' not found in results, skipping plot")
|
|
298
|
+
return None, None
|
|
299
|
+
|
|
300
|
+
# Create figure using existing plotting function
|
|
301
|
+
fig = plot_vector_norm(results, var_name, bounds=bounds)
|
|
302
|
+
|
|
303
|
+
# Update title if custom title provided
|
|
304
|
+
if title is not None:
|
|
305
|
+
fig.update_layout(title_text=title)
|
|
306
|
+
|
|
307
|
+
# Determine data source and compute norms
|
|
308
|
+
if has_in_trajectory:
|
|
309
|
+
time_data = results.trajectory["time"].flatten()
|
|
310
|
+
var_data = results.trajectory[var_name]
|
|
311
|
+
use_trajectory_indexing = True
|
|
312
|
+
else:
|
|
313
|
+
time_data = results.nodes["time"].flatten()
|
|
314
|
+
var_data = results.nodes[var_name]
|
|
315
|
+
use_trajectory_indexing = False
|
|
316
|
+
|
|
317
|
+
# Compute norms
|
|
318
|
+
norm_data = np.linalg.norm(var_data, axis=1) if var_data.ndim > 1 else np.abs(var_data)
|
|
319
|
+
|
|
320
|
+
# Add animated marker
|
|
321
|
+
return add_animated_plotly_marker(
|
|
322
|
+
server,
|
|
323
|
+
fig,
|
|
324
|
+
time_array=time_data,
|
|
325
|
+
marker_x_data=time_data,
|
|
326
|
+
marker_y_data=norm_data,
|
|
327
|
+
use_trajectory_indexing=use_trajectory_indexing,
|
|
328
|
+
marker_name="Current",
|
|
329
|
+
marker_color=marker_color,
|
|
330
|
+
marker_size=marker_size,
|
|
331
|
+
folder_name=folder_name,
|
|
332
|
+
aspect=aspect,
|
|
333
|
+
)
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
"""Static scene primitives for viser visualization.
|
|
2
|
+
|
|
3
|
+
Functions for adding non-animated elements: obstacles, gates, constraint cones,
|
|
4
|
+
ghost trajectories, etc. Called once during scene setup.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import numpy as np
|
|
8
|
+
import viser
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _generate_ellipsoid_mesh(
|
|
12
|
+
center: np.ndarray,
|
|
13
|
+
radii: np.ndarray,
|
|
14
|
+
axes: np.ndarray | None = None,
|
|
15
|
+
subdivisions: int = 2,
|
|
16
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
17
|
+
"""Generate ellipsoid mesh vertices and faces via icosphere subdivision.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
center: Center position (3,)
|
|
21
|
+
radii: Radii along each principal axis (3,)
|
|
22
|
+
axes: Rotation matrix (3, 3) defining principal axes. If None, uses identity.
|
|
23
|
+
subdivisions: Number of icosphere subdivisions (higher = smoother)
|
|
24
|
+
|
|
25
|
+
Returns:
|
|
26
|
+
Tuple of (vertices, faces) where vertices is (V, 3) and faces is (F, 3)
|
|
27
|
+
"""
|
|
28
|
+
# Start with icosahedron vertices
|
|
29
|
+
phi = (1.0 + np.sqrt(5.0)) / 2.0 # Golden ratio
|
|
30
|
+
icosahedron_verts = np.array(
|
|
31
|
+
[
|
|
32
|
+
[-1, phi, 0],
|
|
33
|
+
[1, phi, 0],
|
|
34
|
+
[-1, -phi, 0],
|
|
35
|
+
[1, -phi, 0],
|
|
36
|
+
[0, -1, phi],
|
|
37
|
+
[0, 1, phi],
|
|
38
|
+
[0, -1, -phi],
|
|
39
|
+
[0, 1, -phi],
|
|
40
|
+
[phi, 0, -1],
|
|
41
|
+
[phi, 0, 1],
|
|
42
|
+
[-phi, 0, -1],
|
|
43
|
+
[-phi, 0, 1],
|
|
44
|
+
],
|
|
45
|
+
dtype=np.float64,
|
|
46
|
+
)
|
|
47
|
+
# Normalize to unit sphere
|
|
48
|
+
icosahedron_verts /= np.linalg.norm(icosahedron_verts[0])
|
|
49
|
+
|
|
50
|
+
icosahedron_faces = np.array(
|
|
51
|
+
[
|
|
52
|
+
[0, 11, 5],
|
|
53
|
+
[0, 5, 1],
|
|
54
|
+
[0, 1, 7],
|
|
55
|
+
[0, 7, 10],
|
|
56
|
+
[0, 10, 11],
|
|
57
|
+
[1, 5, 9],
|
|
58
|
+
[5, 11, 4],
|
|
59
|
+
[11, 10, 2],
|
|
60
|
+
[10, 7, 6],
|
|
61
|
+
[7, 1, 8],
|
|
62
|
+
[3, 9, 4],
|
|
63
|
+
[3, 4, 2],
|
|
64
|
+
[3, 2, 6],
|
|
65
|
+
[3, 6, 8],
|
|
66
|
+
[3, 8, 9],
|
|
67
|
+
[4, 9, 5],
|
|
68
|
+
[2, 4, 11],
|
|
69
|
+
[6, 2, 10],
|
|
70
|
+
[8, 6, 7],
|
|
71
|
+
[9, 8, 1],
|
|
72
|
+
],
|
|
73
|
+
dtype=np.int32,
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
vertices = icosahedron_verts
|
|
77
|
+
faces = icosahedron_faces
|
|
78
|
+
|
|
79
|
+
# Subdivide faces
|
|
80
|
+
for _ in range(subdivisions):
|
|
81
|
+
new_faces = []
|
|
82
|
+
midpoint_cache = {}
|
|
83
|
+
|
|
84
|
+
def get_midpoint(i1: int, i2: int) -> int:
|
|
85
|
+
"""Get or create midpoint vertex between two vertices."""
|
|
86
|
+
key = (min(i1, i2), max(i1, i2))
|
|
87
|
+
if key in midpoint_cache:
|
|
88
|
+
return midpoint_cache[key]
|
|
89
|
+
|
|
90
|
+
nonlocal vertices
|
|
91
|
+
p1, p2 = vertices[i1], vertices[i2]
|
|
92
|
+
mid = (p1 + p2) / 2.0
|
|
93
|
+
mid = mid / np.linalg.norm(mid) # Project onto unit sphere
|
|
94
|
+
|
|
95
|
+
idx = len(vertices)
|
|
96
|
+
vertices = np.vstack([vertices, mid])
|
|
97
|
+
midpoint_cache[key] = idx
|
|
98
|
+
return idx
|
|
99
|
+
|
|
100
|
+
for tri in faces:
|
|
101
|
+
v0, v1, v2 = tri
|
|
102
|
+
a = get_midpoint(v0, v1)
|
|
103
|
+
b = get_midpoint(v1, v2)
|
|
104
|
+
c = get_midpoint(v2, v0)
|
|
105
|
+
new_faces.extend([[v0, a, c], [v1, b, a], [v2, c, b], [a, b, c]])
|
|
106
|
+
|
|
107
|
+
faces = np.array(new_faces, dtype=np.int32)
|
|
108
|
+
|
|
109
|
+
# Scale by radii to create ellipsoid
|
|
110
|
+
vertices = vertices / radii
|
|
111
|
+
|
|
112
|
+
# Rotate by principal axes if provided
|
|
113
|
+
if axes is not None:
|
|
114
|
+
vertices = vertices @ axes.T
|
|
115
|
+
|
|
116
|
+
# Translate to center
|
|
117
|
+
vertices = vertices + center
|
|
118
|
+
|
|
119
|
+
return vertices.astype(np.float32), faces
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def add_ellipsoid_obstacles(
|
|
123
|
+
server: viser.ViserServer,
|
|
124
|
+
centers: list[np.ndarray],
|
|
125
|
+
radii: list[np.ndarray],
|
|
126
|
+
axes: list[np.ndarray] | None = None,
|
|
127
|
+
color: tuple[int, int, int] = (255, 100, 100),
|
|
128
|
+
opacity: float = 0.6,
|
|
129
|
+
wireframe: bool = False,
|
|
130
|
+
subdivisions: int = 2,
|
|
131
|
+
) -> list:
|
|
132
|
+
"""Add ellipsoidal obstacles to the scene.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
server: ViserServer instance
|
|
136
|
+
centers: List of center positions, each shape (3,)
|
|
137
|
+
radii: List of radii along principal axes, each shape (3,)
|
|
138
|
+
axes: List of rotation matrices (3, 3) defining principal axes.
|
|
139
|
+
If None, ellipsoids are axis-aligned.
|
|
140
|
+
color: RGB color tuple
|
|
141
|
+
opacity: Opacity (0-1), only used when wireframe=False
|
|
142
|
+
wireframe: If True, render as wireframe instead of solid
|
|
143
|
+
subdivisions: Icosphere subdivisions (higher = smoother, 2 is usually good)
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
List of mesh handles
|
|
147
|
+
"""
|
|
148
|
+
handles = []
|
|
149
|
+
|
|
150
|
+
if axes is None:
|
|
151
|
+
axes = [None] * len(centers)
|
|
152
|
+
|
|
153
|
+
for i, (center, rad, ax) in enumerate(zip(centers, radii, axes)):
|
|
154
|
+
# Convert JAX arrays to numpy if needed
|
|
155
|
+
center = np.asarray(center, dtype=np.float64)
|
|
156
|
+
rad = np.asarray(rad, dtype=np.float64)
|
|
157
|
+
if ax is not None:
|
|
158
|
+
ax = np.asarray(ax, dtype=np.float64)
|
|
159
|
+
|
|
160
|
+
vertices, faces = _generate_ellipsoid_mesh(center, rad, ax, subdivisions)
|
|
161
|
+
|
|
162
|
+
handle = server.scene.add_mesh_simple(
|
|
163
|
+
f"/obstacles/ellipsoid_{i}",
|
|
164
|
+
vertices=vertices,
|
|
165
|
+
faces=faces,
|
|
166
|
+
color=color,
|
|
167
|
+
wireframe=wireframe,
|
|
168
|
+
opacity=opacity if not wireframe else 1.0,
|
|
169
|
+
)
|
|
170
|
+
handles.append(handle)
|
|
171
|
+
|
|
172
|
+
return handles
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def add_gates(
|
|
176
|
+
server: viser.ViserServer,
|
|
177
|
+
vertices: list,
|
|
178
|
+
color: tuple[int, int, int] = (255, 165, 0),
|
|
179
|
+
line_width: float = 3.0,
|
|
180
|
+
) -> None:
|
|
181
|
+
"""Add gate/obstacle wireframes to the scene.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
server: ViserServer instance
|
|
185
|
+
vertices: List of vertex arrays (4 vertices for planar gate, 8 for box)
|
|
186
|
+
color: RGB color tuple
|
|
187
|
+
line_width: Line width for wireframe
|
|
188
|
+
"""
|
|
189
|
+
for i, verts in enumerate(vertices):
|
|
190
|
+
verts = np.array(verts)
|
|
191
|
+
n_verts = len(verts)
|
|
192
|
+
|
|
193
|
+
if n_verts == 4:
|
|
194
|
+
# Planar gate: 4 vertices forming a closed loop
|
|
195
|
+
edges = [[0, 1], [1, 2], [2, 3], [3, 0]]
|
|
196
|
+
elif n_verts == 8:
|
|
197
|
+
# 3D box: 8 vertices
|
|
198
|
+
edges = [
|
|
199
|
+
[0, 1],
|
|
200
|
+
[1, 2],
|
|
201
|
+
[2, 3],
|
|
202
|
+
[3, 0], # front face
|
|
203
|
+
[4, 5],
|
|
204
|
+
[5, 6],
|
|
205
|
+
[6, 7],
|
|
206
|
+
[7, 4], # back face
|
|
207
|
+
[0, 4],
|
|
208
|
+
[1, 5],
|
|
209
|
+
[2, 6],
|
|
210
|
+
[3, 7], # connecting edges
|
|
211
|
+
]
|
|
212
|
+
else:
|
|
213
|
+
# Unknown format, skip
|
|
214
|
+
continue
|
|
215
|
+
|
|
216
|
+
# Shape (N, 2, 3) for N line segments
|
|
217
|
+
points = np.array([[verts[e[0]], verts[e[1]]] for e in edges])
|
|
218
|
+
server.scene.add_line_segments(
|
|
219
|
+
f"/gates/gate_{i}",
|
|
220
|
+
points=points,
|
|
221
|
+
colors=color,
|
|
222
|
+
line_width=line_width,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
|
|
226
|
+
def _generate_cone_mesh(
|
|
227
|
+
apex: np.ndarray,
|
|
228
|
+
height: float,
|
|
229
|
+
half_angle_deg: float,
|
|
230
|
+
n_segments: int = 32,
|
|
231
|
+
) -> tuple[np.ndarray, np.ndarray]:
|
|
232
|
+
"""Generate a cone mesh with apex at given position, opening upward.
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
apex: Apex position (3,) - the tip of the cone
|
|
236
|
+
height: Height of the cone (extends in +Z direction from apex)
|
|
237
|
+
half_angle_deg: Half-angle of the cone from the vertical axis in degrees
|
|
238
|
+
n_segments: Number of segments around the circumference
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
Tuple of (vertices, faces) where vertices is (V, 3) and faces is (F, 3)
|
|
242
|
+
"""
|
|
243
|
+
half_angle_rad = np.radians(half_angle_deg)
|
|
244
|
+
base_radius = height * np.tan(half_angle_rad)
|
|
245
|
+
|
|
246
|
+
# Vertices: apex + base circle points
|
|
247
|
+
vertices = [apex.copy()] # Apex at index 0
|
|
248
|
+
|
|
249
|
+
# Base circle vertices
|
|
250
|
+
for i in range(n_segments):
|
|
251
|
+
angle = 2 * np.pi * i / n_segments
|
|
252
|
+
x = apex[0] + base_radius * np.cos(angle)
|
|
253
|
+
y = apex[1] + base_radius * np.sin(angle)
|
|
254
|
+
z = apex[2] + height
|
|
255
|
+
vertices.append([x, y, z])
|
|
256
|
+
|
|
257
|
+
# Center of base for closing the bottom
|
|
258
|
+
base_center = apex.copy()
|
|
259
|
+
base_center[2] += height
|
|
260
|
+
vertices.append(base_center) # Index n_segments + 1
|
|
261
|
+
|
|
262
|
+
vertices = np.array(vertices, dtype=np.float32)
|
|
263
|
+
|
|
264
|
+
# Faces: triangles from apex to base edge pairs, plus base cap
|
|
265
|
+
faces = []
|
|
266
|
+
|
|
267
|
+
# Side faces (apex to each edge of base)
|
|
268
|
+
for i in range(n_segments):
|
|
269
|
+
next_i = (i + 1) % n_segments
|
|
270
|
+
# Triangle: apex, base[i], base[next_i]
|
|
271
|
+
faces.append([0, i + 1, next_i + 1])
|
|
272
|
+
|
|
273
|
+
# Base cap faces (center to each edge)
|
|
274
|
+
base_center_idx = n_segments + 1
|
|
275
|
+
for i in range(n_segments):
|
|
276
|
+
next_i = (i + 1) % n_segments
|
|
277
|
+
# Triangle: center, base[next_i], base[i] (reverse winding for outward normal)
|
|
278
|
+
faces.append([base_center_idx, next_i + 1, i + 1])
|
|
279
|
+
|
|
280
|
+
faces = np.array(faces, dtype=np.int32)
|
|
281
|
+
|
|
282
|
+
return vertices, faces
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
def add_glideslope_cone(
|
|
286
|
+
server: viser.ViserServer,
|
|
287
|
+
apex: np.ndarray | tuple = (0.0, 0.0, 0.0),
|
|
288
|
+
height: float = 2000.0,
|
|
289
|
+
glideslope_angle_deg: float = 86.0,
|
|
290
|
+
color: tuple[int, int, int] = (100, 200, 100),
|
|
291
|
+
opacity: float = 0.2,
|
|
292
|
+
wireframe: bool = False,
|
|
293
|
+
n_segments: int = 32,
|
|
294
|
+
) -> viser.MeshHandle:
|
|
295
|
+
"""Add a glideslope constraint cone to the scene.
|
|
296
|
+
|
|
297
|
+
The glideslope constraint typically has the form:
|
|
298
|
+
||position_xy|| <= tan(angle) * position_z
|
|
299
|
+
|
|
300
|
+
This creates a cone with apex at the landing site, opening upward.
|
|
301
|
+
|
|
302
|
+
Args:
|
|
303
|
+
server: ViserServer instance
|
|
304
|
+
apex: Apex position (landing site), default is origin
|
|
305
|
+
height: Height of the cone visualization
|
|
306
|
+
glideslope_angle_deg: Glideslope angle in degrees (measured from vertical).
|
|
307
|
+
For constraint ||r_xy|| <= tan(theta) * z, pass theta here.
|
|
308
|
+
Common values: 86 deg (very wide), 70 deg (moderate), 45 deg (steep)
|
|
309
|
+
color: RGB color tuple
|
|
310
|
+
opacity: Opacity (0-1)
|
|
311
|
+
wireframe: If True, render as wireframe
|
|
312
|
+
n_segments: Number of segments for cone smoothness
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
Mesh handle for the cone
|
|
316
|
+
"""
|
|
317
|
+
apex = np.asarray(apex, dtype=np.float32)
|
|
318
|
+
|
|
319
|
+
vertices, faces = _generate_cone_mesh(apex, height, glideslope_angle_deg, n_segments)
|
|
320
|
+
|
|
321
|
+
handle = server.scene.add_mesh_simple(
|
|
322
|
+
"/constraints/glideslope_cone",
|
|
323
|
+
vertices=vertices,
|
|
324
|
+
faces=faces,
|
|
325
|
+
color=color,
|
|
326
|
+
wireframe=wireframe,
|
|
327
|
+
opacity=opacity if not wireframe else 1.0,
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
return handle
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
def add_ghost_trajectory(
|
|
334
|
+
server: viser.ViserServer,
|
|
335
|
+
pos: np.ndarray,
|
|
336
|
+
colors: np.ndarray,
|
|
337
|
+
opacity: float = 0.3,
|
|
338
|
+
point_size: float = 0.05,
|
|
339
|
+
) -> None:
|
|
340
|
+
"""Add a faint ghost trajectory showing the full path.
|
|
341
|
+
|
|
342
|
+
Args:
|
|
343
|
+
server: ViserServer instance
|
|
344
|
+
pos: Position array of shape (N, 3)
|
|
345
|
+
colors: RGB color array of shape (N, 3)
|
|
346
|
+
opacity: Opacity factor (0-1) applied to colors
|
|
347
|
+
point_size: Size of trajectory points
|
|
348
|
+
"""
|
|
349
|
+
ghost_colors = (colors * opacity).astype(np.uint8)
|
|
350
|
+
server.scene.add_point_cloud(
|
|
351
|
+
"/ghost_traj",
|
|
352
|
+
points=pos,
|
|
353
|
+
colors=ghost_colors,
|
|
354
|
+
point_size=point_size,
|
|
355
|
+
)
|