3 from dataclasses 
import dataclass
 
    4 from typing 
import Any, Callable, Dict, List, Optional, Tuple
 
    8 import plotly.graph_objects 
as go
 
    9 from tqdm.notebook 
import tqdm  
 
   17     """Holds all data needed for a single frame of the SLAM animation.""" 
   22     graph_dot_string: Optional[str] = 
None   
   29     cx: float, cy: float, cov_matrix: np.ndarray, scale: float = 2.0, N: int = 60
 
   31     """Generates SVG path string for an ellipse from 2x2 covariance.""" 
   32     cov = cov_matrix[:2, :2] + np.eye(2) * 1e-9  
 
   34         eigvals, eigvecs = np.linalg.eigh(cov)
 
   35         eigvals = np.maximum(eigvals, 1e-9)  
 
   36         minor_std, major_std = np.sqrt(eigvals)  
 
   37         v_minor, v_major = eigvecs[:, 0], eigvecs[:, 1]
 
   38     except np.linalg.LinAlgError:
 
   41         t = np.linspace(0, 2 * np.pi, N)
 
   42         x_p = cx + radius * np.cos(t)
 
   43         y_p = cy + radius * np.sin(t)
 
   46         t = np.linspace(0, 2 * np.pi, N)
 
   47         cos_t, sin_t = np.cos(t), np.sin(t)
 
   49             major_std * cos_t * v_major[0] + minor_std * sin_t * v_minor[0]
 
   52             major_std * cos_t * v_major[1] + minor_std * sin_t * v_minor[1]
 
   57         f
"M {x_p[0]},{y_p[0]} " 
   58         + 
" ".join(f
"L{x_},{y_}" for x_, y_ 
in zip(x_p[1:], y_p[1:]))
 
   68     landmarks_gt: Optional[np.ndarray],
 
   69 ) -> Optional[go.Scatter]:
 
   70     """Creates scatter trace for ground truth landmarks.""" 
   71     if landmarks_gt 
is None or landmarks_gt.size == 0:
 
   77         marker=
dict(color=
"black", size=8, symbol=
"star"),
 
   83     """Creates line trace for ground truth path.""" 
   87         x=[p.x() 
for p 
in poses_gt],
 
   88         y=[p.y() 
for p 
in poses_gt],
 
   90         line=
dict(color=
"gray", width=1, dash=
"dash"),
 
   96     est_path_x: List[float], est_path_y: List[float]
 
   98     """Creates trace for the estimated path (all poses up to current).""" 
  102         mode=
"lines+markers",
 
  103         line=
dict(color=
"rgba(255, 0, 0, 0.3)", width=1),  
 
  104         marker=
dict(size=4, color=
"red"),  
 
  111 ) -> Optional[go.Scatter]:
 
  112     """Creates a single marker trace for the current estimated pose.""" 
  113     if current_pose 
is None:
 
  116         x=[current_pose.x()],
 
  117         y=[current_pose.y()],
 
  119         marker=
dict(color=
"magenta", size=10, symbol=
"cross"),
 
  125     est_landmarks_x: List[float], est_landmarks_y: List[float]
 
  126 ) -> Optional[go.Scatter]:
 
  127     """Creates trace for currently estimated landmarks.""" 
  128     if not est_landmarks_x:
 
  134         marker=
dict(color=
"blue", size=6, symbol=
"x"),
 
  135         name=
"Landmarks Est",
 
  140     cx: float, cy: float, cov: np.ndarray, scale: float, fillcolor: str, line_color: str
 
  142     """Helper: Creates dict for a Plotly ellipse shape from covariance.""" 
  150         line_color=line_color,
 
  156     pose_mean_xy: np.ndarray, pose_cov: np.ndarray, scale: float
 
  158     """Creates shape dict for a pose covariance ellipse.""" 
  164         fillcolor=
"rgba(255,0,255,0.2)",  
 
  165         line_color=
"rgba(255,0,255,0.5)",  
 
  170     lm_mean_xy: np.ndarray, lm_cov: np.ndarray, scale: float
 
  172     """Creates shape dict for a landmark covariance ellipse.""" 
  178         fillcolor=
"rgba(0,0,255,0.1)",  
 
  179         line_color=
"rgba(0,0,255,0.3)",  
 
  184     dot_string: Optional[str], engine: str = 
"neato" 
  186     """Renders a DOT string to a base64 encoded SVG using graphviz.""" 
  190         source = graphviz.Source(dot_string, engine=engine)
 
  191         svg_bytes = source.pipe(format=
"svg")
 
  192         encoded_string = base64.b64encode(svg_bytes).
decode(
"utf-8")
 
  193         return f
"data:image/svg+xml;base64,{encoded_string}" 
  194     except graphviz.backend.execute.CalledProcessError 
as e:
 
  195         print(f
"Graphviz rendering error ({engine}): {e}")
 
  197     except Exception 
as e:
 
  198         print(f
"Unexpected error during Graphviz SVG generation: {e}")
 
  204     frame_data: SlamFrameData,
 
  205     X: Callable[[int], int],
 
  206     L: Callable[[int], int],
 
  207     max_landmark_index: int,
 
  208     ellipse_scale: float = 2.0,
 
  209     graphviz_engine: str = 
"neato",
 
  210     verbose: bool = 
False,
 
  211 ) -> Tuple[List[go.Scatter], List[Dict[str, Any]], Optional[Dict[str, Any]]]:
 
  212     """Generates dynamic traces, shapes, and layout image for a single frame.""" 
  213     k = frame_data.step_index
 
  215     step_results = frame_data.results
 
  216     step_marginals = frame_data.marginals
 
  218     frame_dynamic_traces: List[go.Scatter] = []
 
  219     frame_shapes: List[Dict[str, Any]] = []
 
  220     layout_image: Optional[Dict[str, Any]] = 
None 
  225     current_pose_est = 
None 
  228     for i 
in range(k + 1):  
 
  230         if step_results.exists(pose_key):
 
  231             pose = step_results.atPose2(pose_key)
 
  232             est_path_x.append(pose.x())
 
  233             est_path_y.append(pose.y())
 
  235                 current_pose_est = pose
 
  239         frame_dynamic_traces.append(path_trace)
 
  243     if current_pose_trace:
 
  244         frame_dynamic_traces.append(current_pose_trace)
 
  247     est_landmarks_x, est_landmarks_y, landmark_keys = [], [], []
 
  248     for j 
in range(max_landmark_index + 1):
 
  251         if step_results.exists(lm_key):
 
  252             lm_point = step_results.atPoint2(lm_key)
 
  253             est_landmarks_x.append(lm_point[0])
 
  254             est_landmarks_y.append(lm_point[1])
 
  255             landmark_keys.append(lm_key)  
 
  259         frame_dynamic_traces.append(lm_trace)
 
  266         cov = step_marginals.marginalCovariance(pose_key)
 
  268         mean = step_results.atPose2(pose_key).
translation()
 
  272         for lm_key 
in landmark_keys:
 
  275                 cov = step_marginals.marginalCovariance(lm_key)
 
  277                 mean = step_results.atPoint2(lm_key)
 
  284                         f
"Warn: LM {gtsam.Symbol(lm_key).index()} cov not in marginals @ step {k}" 
  286             except Exception 
as e:
 
  289                         f
"Warn: LM {gtsam.Symbol(lm_key).index()} cov OTHER err @ step {k}: {e}" 
  294         frame_data.graph_dot_string, engine=graphviz_engine
 
  312     return frame_dynamic_traces, frame_shapes, layout_image
 
  322     initial_shapes: List[Dict[str, Any]],
 
  323     initial_image: Optional[Dict[str, Any]],
 
  325     """Configures Plotly figure layout for side-by-side display.""" 
  327     plot_domain = [0.52, 1.0]  
 
  332             currentvalue={
"prefix": 
"Step: "},
 
  342                             frame=
dict(duration=100, redraw=
True),
 
  343                             transition=
dict(duration=0),
 
  356             pad={
"r": 10, 
"t": 20},
 
  369                             frame=
dict(duration=100, redraw=
True),
 
  370                             transition=
dict(duration=0),
 
  382                             frame=
dict(duration=0, redraw=
False),
 
  383                             transition=
dict(duration=0),
 
  392         title=
"Factor Graph SLAM Animation (Graph Left, Results Right)",
 
  394             range=[-world_size / 2 - 2, world_size / 2 + 2],
 
  399             range=[-world_size / 2 - 2, world_size / 2 + 2],
 
  407         updatemenus=updatemenus,
 
  409         shapes=initial_shapes,  
 
  410         images=([initial_image] 
if initial_image 
else []),  
 
  415             bgcolor=
"rgba(255,255,255,0.5)",
 
  418         margin=
dict(l=0, r=0, t=50, b=50),
 
  426     history: List[SlamFrameData],
 
  427     X: Callable[[int], int],
 
  428     L: Callable[[int], int],
 
  429     max_landmark_index: int,
 
  430     landmarks_gt_array: Optional[np.ndarray] = 
None,
 
  432     world_size: float = 20.0,
 
  433     ellipse_scale: float = 2.0,
 
  434     graphviz_engine: str = 
"neato",
 
  435     verbose_cov_errors: bool = 
False,
 
  437     """Creates a side-by-side Plotly SLAM animation using a history of dataclasses.""" 
  439         raise ValueError(
"History cannot be empty.")
 
  440     print(
"Generating Plotly animation...")
 
  441     num_steps = history[-1].step_index
 
  448         gt_traces.append(gt_lm_trace)
 
  451         gt_traces.append(gt_path_trace)
 
  454     initial_frame_data = next((item 
for item 
in history 
if item.step_index == 0), 
None)
 
  455     if initial_frame_data 
is None:
 
  456         raise ValueError(
"History must contain data for step 0.")
 
  459         initial_dynamic_traces,
 
  473     for trace 
in gt_traces:
 
  475     for trace 
in initial_dynamic_traces:
 
  480     steps_iterable = 
range(num_steps + 1)
 
  481     steps_iterable = tqdm(steps_iterable, desc=
"Creating Frames")
 
  483     for k 
in steps_iterable:
 
  484         frame_data = next((item 
for item 
in history 
if item.step_index == k), 
None)
 
  502                 + frame_dynamic_traces,  
 
  507                         [layout_image] 
if layout_image 
else []
 
  514     fig.update(frames=frames)
 
  519     print(
"Plotly animation generated.")