.. _discopygal_solver_viewer_manual: ======================================================= DiscoPygal Solver Viewer: Developer Architecture Manual ======================================================= The **DiscoPygal Solver Viewer** is a PySide6-based graphical frontend for the ``discopygal`` Python framework. It provides interactive scene loading, dynamic solver configuration, process-isolated path planning execution, and multi-robot path animation. Because the backend relies on heavy geometric algorithms and C++ CGAL bindings, the viewer uses an out-of-process concurrency model with a specialized inter-process communication (IPC) serialization layer to keep the user interface responsive. .. contents:: Table of Contents :local: :depth: 2 ---------- System Architecture & Concurrency Model ========================================== Path planning on complex geometric arrangements is computationally intensive and can block the Qt event loop if run synchronously. To maintain UI responsiveness, the application isolates solver execution in a separate operating system process managed via Python's ``multiprocessing`` library. .. digraph:: solver_viewer_process_architecture :caption: Process-Isolated Concurrency Model & IPC Queues rankdir=TB; node [shape=box, style="filled,rounded", fontname="Helvetica", fontsize=10, margin="0.15,0.1"]; edge [fontname="Helvetica", fontsize=9]; subgraph cluster_main { label = "MAIN PROCESS (PySide6 UI Loop)"; style = "filled,dashed"; color = "#1565C0"; fillcolor = "#E3F2FD"; gui [label="SolverViewerGUI\n(Qt Window Event Loop)", fillcolor="#BBDEFB", color="#1976D2"]; drawer [label="SceneDrawer\n(Path Animation Canvas)", fillcolor="#BBDEFB", color="#1976D2"]; gui -> drawer [label="Renders Scene/Paths"]; } subgraph cluster_ipc { label = "IPC Queues (multiprocessing)"; style = "filled,dotted"; color = "#F57C00"; fillcolor = "#FFF3E0"; log_queue [label="log_queue\n(ChildWriter stdout proxy)", fillcolor="#FFE0B2", color="#E65100"]; res_queue [label="result_queue\n(Serialized payload / exceptions)", fillcolor="#FFE0B2", color="#E65100"]; } subgraph cluster_worker { label = "WORKER PROCESS (Python)"; style = "filled,dashed"; color = "#2E7D32"; fillcolor = "#E8F5E9"; worker [label="solve_process()\n(Isolated Process Routine)", fillcolor="#C8E6C9", color="#388E3C"]; solver [label="Solver.solve()\n(CGAL Geometric Operations)", fillcolor="#A5D6A7", color="#2E7D32"]; worker -> solver [label="Executes Planner"]; } gui -> worker [label="Spawns solve_process()\nTransfers serialized scene dict"]; solver -> log_queue [label="Logs messages"]; solver -> res_queue [label="Transfers result dict"]; log_queue -> gui [label="Log stream"]; res_queue -> gui [label="Path payload"]; Process Lifecycle & IPC Mechanism ---------------------------------- 1. **Initialization:** The GUI thread invokes ``SolverViewerGUI.solve_thread()``, which serializes the active scene into a dictionary (``scene_dict``) and gathers user-defined parameters via ``get_solver_args()``. 2. **Execution Isolation:** A ``multiprocessing.Process`` target is initialized with ``solve_process()``. 3. **Log Streaming:** Standard output inside the worker process is redirected to a ``ChildWriter`` proxy class, which pushes text log chunks into a thread-safe ``log_queue``. The GUI polls this queue asynchronously to update the log dock. 4. **Result Marshalling:** Upon completion of ``solver.solve()``, generated geometric paths, graphs, and planar arrangements are converted into primitive standard Python data structures and pushed onto ``result_queue``. ---------- Dynamic Solver Discovery & Trait Filtering ============================================= The viewer ensures structural compatibility between loaded geometric scenes and selected solvers prior to execution. Trait Extraction & Matching Logic --------------------------------- Scenes define operational constraints based on robot geometries and fleet size. Before displaying available solvers in the GUI dropdown, ``SolverDialog`` executes a compatibility check: 1. **Scene Analysis (``extract_scene_traits``):** * Inspects ``scene.robots`` to map concrete classes (``RobotDisc``, ``RobotPolygon``, ``RobotRod``) to trait capabilities (``Trait.ACCEPTS_DISC_ROBOT``, etc.). * Inspects ``len(scene.robots)`` to derive cardinality traits (``Trait.ACCEPTS_SINGLE_ROBOT``, ``Trait.ACCEPTS_MULTIPLE_ROBOTS``, etc.). 2. **Solver Trait Verification (``solver_matches_scene``):** * Inspects the ``_traits`` class attribute on candidate solvers via ``get_solver_class()``. * Verifies that the solver's traits cover all required scene capabilities. .. code-block:: text discopygal_scene ──► extract_scene_traits() ──► Required Trait Set │ Available Solvers ──► solver_matches_scene() ────────┴──► Active Dropdown List Dynamic Import Pipeline ----------------------- Custom solvers can be imported at runtime via ``import_solver_file()``. The module is loaded dynamically using ``importlib.import_module()``. ---------- Data Serialization & CGAL Marshalling Protocol ================================================= Because native C++ CGAL geometric objects (such as ``Point_2``, ``Polygon_2``, or arrangements) cannot be directly pickled across Python process boundaries, the viewer marshals all CGAL objects into standard Python dictionaries prior to transmission over ``multiprocessing.Queue``. .. code-block:: text ┌─────────────────────────────────┐ │ Worker Process Completion │ └────────────────┬────────────────┘ │ ┌───────────────────┴───────────────────┐ │ │ ▼ ▼ Path / Metadata Objects Graph & Arrangement │ │ serialize_paths() conversions.graph_to_dict() │ conversions.arrangement_to_dict() │ │ └───────────────────┬───────────────────┘ │ ▼ Serialized Result Tuple pushed to Queue Result Payload Contract ----------------------- The payload returned from the worker process via ``result_queue`` adheres to the following tuple signatures: * **Success Payload:** ``("RESULT", serialized_paths, serialized_graph, serialized_arrangement)`` * **Failure Payload:** ``("ERROR", exception_type, exception_value, formatted_traceback_string)`` ---------- Robot Identity Reconstruction ================================ When path dictionaries return to the main process, each path must be mapped back to its corresponding ``Robot`` object instance inside the PySide6 scene graph. .. code-block:: text ┌───────────────────────────────┐ │ Received Serialized Path │ └───────────────┬───────────────┘ │ Has 'obj_id' in Metadata? / \ Yes / \ No / \ ▼ ▼ Primary Match Geometric Fallback (Direct ID) (Coordinate Matching) Primary Identity Matching (``match_path_robot``) ------------------------------------------------ During serialization, ``serialize_paths()`` captures the robot's unique memory ID (``_obj_id``). The GUI matches this key against ``robot.data['_obj_id']`` for each robot active in ``discopygal_scene.robots``. Geometric Fallback Matching (``match_scene_robot``) --------------------------------------------------- If ``obj_id`` is absent (such as in external third-party path representations), ``match_scene_robot()`` performs a spatial boundary comparison using start :math:`(r_s, q_s)` and end :math:`(r_e, q_e)` path coordinates. Match validation applies an absolute tolerance threshold: .. math:: \Delta = |x_1 - x_2| < 10^{-6} \quad \text{and} \quad |y_1 - y_2| < 10^{-6} ---------- 5. Rendering & Parallel Animation Engine ======================================== The viewer includes an animation framework built on Qt's animation properties (``QPropertyAnimation``, ``QParallelAnimationGroup``) to simulate multi-robot trajectory execution simultaneously. Segment Animation Dispatch -------------------------- For each segment from path vertex :math:`i` to :math:`i+1`, the animation system calculates motion parameters and instantiates property drivers: 1. **Translation Segments:** Driven by ``linear_translation_animation()`` when coordinates update without orientation changes. 2. **Rotational Segments:** Driven by ``segment_angle_animation()`` when orientation (``theta``) keys are present in vertex metadata. 3. **Speed Evaluation:** Segment durations are calculated in milliseconds using Euclidean distances: .. math:: \text{duration} = \left\lfloor \frac{\text{distance}}{\text{animation\_speed} \times 0.0001 \times \text{speed}} \right\rfloor 4. **Multi-Robot Synchronization:** Individual segment animations for all robots are bundled into a ``QParallelAnimationGroup``. These groups are appended to a queue via ``queue_animation()`` and executed in sequence via ``play_queue()``. ---------- .. automodule:: discopygal_tools.solver_viewer.solver_viewer_main :members: :show-inheritance: