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.


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 { 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-Isolated Concurrency Model & IPC Queues

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.

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.

              ┌─────────────────────────────────┐
              │    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.

┌───────────────────────────────┐
│    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 (r_s, q_s) and end (r_e, q_e) path coordinates. Match validation applies an absolute tolerance threshold:

\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 i to 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:

\text{duration} = \left\lfloor \frac{\text{distance}}{\text{animation\_speed} \times 0.0001 \times \text{speed}} \right\rfloor

  1. 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().


discopygal_tools.solver_viewer.solver_viewer_main.start_gui(scene: Scene = None, solver: Solver = None, solver_file: str = None) None

Start solver_viewer tool

See also Solver Viewer - From script

Parameters:
  • scene (Scene) – scene to upload

  • solver (Solver or class or str) – solver to upload. May be a solver object (object of a class that inherits from Solver), class that inherits from Solver or a name of a solver’s class.