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.

Files changed (79) hide show
  1. openscvx/__init__.py +123 -0
  2. openscvx/_version.py +34 -0
  3. openscvx/algorithms/__init__.py +92 -0
  4. openscvx/algorithms/autotuning.py +24 -0
  5. openscvx/algorithms/base.py +351 -0
  6. openscvx/algorithms/optimization_results.py +215 -0
  7. openscvx/algorithms/penalized_trust_region.py +384 -0
  8. openscvx/config.py +437 -0
  9. openscvx/discretization/__init__.py +47 -0
  10. openscvx/discretization/discretization.py +236 -0
  11. openscvx/expert/__init__.py +23 -0
  12. openscvx/expert/byof.py +326 -0
  13. openscvx/expert/lowering.py +419 -0
  14. openscvx/expert/validation.py +357 -0
  15. openscvx/integrators/__init__.py +48 -0
  16. openscvx/integrators/runge_kutta.py +281 -0
  17. openscvx/lowered/__init__.py +30 -0
  18. openscvx/lowered/cvxpy_constraints.py +23 -0
  19. openscvx/lowered/cvxpy_variables.py +124 -0
  20. openscvx/lowered/dynamics.py +34 -0
  21. openscvx/lowered/jax_constraints.py +133 -0
  22. openscvx/lowered/parameters.py +54 -0
  23. openscvx/lowered/problem.py +70 -0
  24. openscvx/lowered/unified.py +718 -0
  25. openscvx/plotting/__init__.py +63 -0
  26. openscvx/plotting/plotting.py +756 -0
  27. openscvx/plotting/scp_iteration.py +299 -0
  28. openscvx/plotting/viser/__init__.py +126 -0
  29. openscvx/plotting/viser/animated.py +605 -0
  30. openscvx/plotting/viser/plotly_integration.py +333 -0
  31. openscvx/plotting/viser/primitives.py +355 -0
  32. openscvx/plotting/viser/scp.py +459 -0
  33. openscvx/plotting/viser/server.py +112 -0
  34. openscvx/problem.py +734 -0
  35. openscvx/propagation/__init__.py +60 -0
  36. openscvx/propagation/post_processing.py +104 -0
  37. openscvx/propagation/propagation.py +248 -0
  38. openscvx/solvers/__init__.py +51 -0
  39. openscvx/solvers/cvxpy.py +226 -0
  40. openscvx/symbolic/__init__.py +9 -0
  41. openscvx/symbolic/augmentation.py +630 -0
  42. openscvx/symbolic/builder.py +492 -0
  43. openscvx/symbolic/constraint_set.py +92 -0
  44. openscvx/symbolic/expr/__init__.py +222 -0
  45. openscvx/symbolic/expr/arithmetic.py +517 -0
  46. openscvx/symbolic/expr/array.py +632 -0
  47. openscvx/symbolic/expr/constraint.py +796 -0
  48. openscvx/symbolic/expr/control.py +135 -0
  49. openscvx/symbolic/expr/expr.py +720 -0
  50. openscvx/symbolic/expr/lie/__init__.py +87 -0
  51. openscvx/symbolic/expr/lie/adjoint.py +357 -0
  52. openscvx/symbolic/expr/lie/se3.py +172 -0
  53. openscvx/symbolic/expr/lie/so3.py +138 -0
  54. openscvx/symbolic/expr/linalg.py +279 -0
  55. openscvx/symbolic/expr/math.py +699 -0
  56. openscvx/symbolic/expr/spatial.py +209 -0
  57. openscvx/symbolic/expr/state.py +607 -0
  58. openscvx/symbolic/expr/stl.py +136 -0
  59. openscvx/symbolic/expr/variable.py +321 -0
  60. openscvx/symbolic/hashing.py +112 -0
  61. openscvx/symbolic/lower.py +760 -0
  62. openscvx/symbolic/lowerers/__init__.py +106 -0
  63. openscvx/symbolic/lowerers/cvxpy.py +1302 -0
  64. openscvx/symbolic/lowerers/jax.py +1382 -0
  65. openscvx/symbolic/preprocessing.py +757 -0
  66. openscvx/symbolic/problem.py +110 -0
  67. openscvx/symbolic/time.py +116 -0
  68. openscvx/symbolic/unified.py +420 -0
  69. openscvx/utils/__init__.py +20 -0
  70. openscvx/utils/cache.py +131 -0
  71. openscvx/utils/caching.py +210 -0
  72. openscvx/utils/printing.py +301 -0
  73. openscvx/utils/profiling.py +37 -0
  74. openscvx/utils/utils.py +100 -0
  75. openscvx-0.3.2.dev170.dist-info/METADATA +350 -0
  76. openscvx-0.3.2.dev170.dist-info/RECORD +79 -0
  77. openscvx-0.3.2.dev170.dist-info/WHEEL +5 -0
  78. openscvx-0.3.2.dev170.dist-info/licenses/LICENSE +201 -0
  79. 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