DiscoPygal Architecture Overview ================================ This document is a technical architecture manual for **DiscoPygal**: how the system is decomposed into layers, which module owns which responsibility, how a scene becomes a verified solution, and where to plug in when extending the framework. DiscoPygal is a Python framework for 2D single- and multi-robot motion planning. It is built on top of CGAL (through the CGALPY bindings), so exact rational arithmetic is available all the way from the geometric primitives up to the planners. Two distributions live in this repository: * ``src/discopygal`` — the installable library package. * ``tools/`` — standalone desktop applications (Solver Viewer, Scene Designer) that are *consumers* of the library, shipped alongside it rather than inside it. System Architecture & Layering Model ------------------------------------ The stack isolates low-level geometric arithmetic from planning algorithms, and both of those from the user interface. Each layer depends only on the layers beneath it. .. digraph:: system_architecture :caption: System Architecture & Layering Model rankdir=TB; node [shape=box, style="filled,rounded", fontname="Helvetica", fontsize=10, margin="0.2,0.1"]; edge [fontname="Helvetica", fontsize=9]; tools [label="Applications Layer (tools/)\nsolver_viewer · scene_designer", fillcolor="#E3F2FD", color="#1565C0"]; gui [label="GUI Toolkit Layer (discopygal.gui)\nGUI · GraphicsScenePlus · Worker · R* graphic items", fillcolor="#E1F5FE", color="#0277BD"]; exp [label="Experimentation Layer (discopygal.experiments)\nscenarios_runner · run_experiment", fillcolor="#F3E5F5", color="#7B1FA2"]; solvers [label="Concrete Solvers Layer (discopygal.solvers)\nrrt · prm · exact · staggered_grid · lattice_solver\nbottleneck_tree · revolving_areas · hgraphs", fillcolor="#FFF3E0", color="#E65100"]; infra [label="Solver Infrastructure Layer (discopygal.solvers_infra)\nScene/Robot/Obstacle/Path · Solver · SamplingSolver\nRoadmap · samplers · metrics · nearest_neighbors", fillcolor="#E8F5E9", color="#2E7D32"]; geom [label="Geometric Utilities Layer (discopygal.geometry_utils)\ncollision_detection · conversions · arrangement_operations\nbounding_boxes · transform", fillcolor="#FFFDE7", color="#F57F17"]; bindings [label="CGALPY Bindings Layer (discopygal.bindings)\nPoint_2 · Point_d · Segment_2 · Arrangement_2 · FT", fillcolor="#FFEBEE", color="#C62828"]; tools -> gui; tools -> solvers; exp -> solvers; gui -> infra; solvers -> infra; infra -> geom; geom -> bindings; .. note:: :mod:`discopygal.solvers_infra` imports :mod:`discopygal.gui.color` for the default robot and obstacle colors. This is the one place where the infrastructure layer reaches "sideways" into the GUI package, and it is a pure constants dependency — no Qt widgets are involved in loading a scene. Package Map & Inter-Module Dependencies --------------------------------------- .. list-table:: Subpackages and File Breakdown :header-rows: 1 :widths: 22 30 48 * - Subpackage - Core Files - Responsibilities & Dependencies * - ``discopygal.bindings`` - ``__init__.py`` - Imports two CGALPY builds and re-exports their kernel, arrangement, polygon and spatial-search types. Subclasses ``Ker.FT`` and ``Ker.Segment_2`` to add Python arithmetic and constructor conveniences. Depended on by every other layer. * - ``discopygal.geometry_utils`` - ``collision_detection.py``, ``conversions.py``, ``arrangement_operations.py``, ``bounding_boxes.py``, ``transform.py``, ``display_arrangement.py`` - Collision predicates, type conversions between CGAL/Python/tensor representations, arrangement construction and overlay, scene bounding boxes, and polygon offsetting. Depends on ``bindings``. * - ``discopygal.solvers_infra`` - ``__init__.py``, ``Solver.py``, ``SamplingSolver.py``, ``roadmap.py``, ``implicitroadmap.py``, ``samplers.py``, ``metrics.py``, ``nearest_neighbors.py``, ``search_algo.py``, ``verify_paths.py``, ``operations_counter.py``, ``heuristic_parser.py``, ``tensor_solver/`` - Scene data model, abstract solver contracts, roadmap graphs, and the pluggable sampler / metric / nearest-neighbor strategies. Depends on ``bindings``, ``geometry_utils`` and ``gui.color``. * - ``discopygal.solvers`` - ``rrt/``, ``prm/``, ``exact/``, ``staggered_grid/``, ``lattice_solver/``, ``bottleneck_tree/``, ``revolving_areas/``, ``hgraphs.py``, ``mapf.py`` - Concrete planners, plus the solver *registry* in ``__init__.py`` that discovers and loads solver classes by name. Depends on ``solvers_infra``. * - ``discopygal.experiments`` - ``scenarios_runner.py``, ``run_experiment.py``, ``__init__.py`` - Batch execution of solvers over scenario suites with per-run timeouts and process isolation; aggregates results into ``pandas`` frames. Depends on ``solvers`` and ``solvers_infra``. * - ``discopygal.gui`` - ``gui.py``, ``GraphicsScenePlus.py``, ``MainWindowsPlus.py``, ``Worker.py``, ``color.py``, ``logger.py``, ``RDisc.py``, ``RPolygon.py``, ``RSegment.py``, … - PySide6 building blocks: a base ``GUI`` window class, a zoomable graphics scene, a ``QRunnable`` worker, and one wrapper class per drawable CGAL shape. * - ``tools/`` *(top-level, not part of the package)* - ``solver_viewer/``, ``scene_designer/``, ``ui_helpers.py`` - End-user applications built on ``discopygal.gui``. Added to ``sys.path`` separately by ``docs/conf.py`` and by the entry-point scripts. Detailed File Outlines & Component Relations --------------------------------------------- 1. CGALPY Bindings Layer (``discopygal.bindings``) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ``__init__.py`` is the single bridge between the compiled CGALPY extension modules and the rest of the Python code. Its defining characteristic is that it loads **two** CGALPY builds and merges them into one namespace: .. list-table:: The two CGALPY builds :header-rows: 1 :widths: 18 38 44 * - Alias - Build - Role * - ``CGALPY_SEG`` - ``…_aos2SegVeHeFePl_bso2_ch2_ms2_pol2_ss2`` - Segment arrangement traits. The **default** everywhere no circular arc is involved. It is also the only build shipping the Minkowski sums module (``Ms2``). * - ``CGALPY_CS`` - ``…_aos2CsVeHeFePl_bso2_ch2_pol2_ss2`` - Circle-segment traits. The only build that can hold circular arcs (expanded discs, approximated offsets). Its types are exported with a ``_circle`` suffix. Both builds share the same kernel, polygon and spatial-search types, so ``Point_2``, ``Point_d`` and ``Polygon_2`` are unambiguous. The arrangement traits, however, **cannot be mixed** within a single arrangement or overlaid with one another — choosing the right flavour is a real design constraint for any solver that builds arrangements. * **Exported CGAL modules**: ``Ker`` (kernel), ``Aos2`` / ``As2`` (arrangements, segment and circle-segment traits), ``Pol2`` (polygons), ``Ss`` (spatial search), ``Bso2`` (boolean set operations), ``Ms2`` (Minkowski sums). * **Exported types**: ``Point_2``, ``Point_d``, ``Segment_2``, ``Line_2``, ``Circle_2``, ``Vector_2``, ``Polygon_2``, ``Polygon_with_holes_2``, ``Polygon_set_2``, ``Arrangement_2``, ``Arrangement_on_surface_2``, ``X_monotone_curve_2``, ``Curve_2``, ``TPoint``, ``Kd_tree``, ``K_neighbor_search``, ``Fuzzy_sphere``, ``Aff_transformation_2``, ``FT``, ``Gmpq``. * **Python-side subclasses**: * ``FT(Ker.FT)`` — overrides the arithmetic dunders (``__add__``, ``__sub__``, ``__mul__``, ``__truediv__`` and their in-place/reflected forms, generated by the ``create_method`` / ``create_imethod`` helpers) so exact field elements interoperate with plain Python ``int`` and ``float``. It also accepts ``str``, converting through ``float`` first. * ``Segment_2(Ker.Segment_2)`` — constructor conveniences over the raw binding. 2. Geometric Utilities Layer (``discopygal.geometry_utils``) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * **``collision_detection.py``** — the workhorse of validity checking. * **Classes**: ``ObjectCollisionDetection`` (built per robot from the scene's obstacles; exposes ``is_point_valid()`` and ``is_edge_valid()``), ``ArrTraits`` (a ``NamedTuple`` selecting the arrangement flavour for a given robot/obstacle combination, via ``scene_traits()``). * **Functions**: ``collide_two_robots()``, ``collide_disc_with_disc()``, ``collide_disc_with_polygon()``, ``collide_disc_with_rod()``. * **Relations**: :class:`~discopygal.solvers_infra.roadmap.Roadmap` holds one ``ObjectCollisionDetection`` per robot; ``verify_paths.py`` rebuilds them independently to re-validate a finished solution. * **``conversions.py``** — the bridge between single-robot 2D primitives and the composite configuration space. * **Functions**: ``Point_2_to_xy()`` / ``xy_to_Point_2()``, ``FT_to_float()`` / ``float_to_FT()`` / ``to_FT()``, ``Polygon_2_to_array_of_points()`` / ``array_of_points_to_Polygon_2()``, ``create_segment_2()``, ``to_TPoint()``, ``point_to_coordinate()``. * **Tensor multiplexing**: ``Point_2_list_to_Point_d()`` and ``Point_d_to_Point_2_list()`` (and the generalized ``Point_k_list_to_Point_d()`` / ``Point_d_to_Point_k_list()``) pack per-robot configurations into one ``Point_d`` and unpack them again. This pair is what makes multi-robot planning a single-graph search. * **Serialization**: ``graph_to_dict()`` / ``graph_from_dict()`` and ``arrangement_to_dict()`` / ``arrangement_from_dict()`` make roadmaps and arrangements picklable — required for sending results across process boundaries. * **``arrangement_operations.py``** — 2D arrangement construction and algebra. * **Constructors**: ``empty_arrangement()``, ``copy_arrangement()``, ``make_arrangement_polygon_from_points()``, ``make_rectangle_arrangement()``, ``make_ellipse_arrangement()``, ``make_conic_curve_arrangement()``. * **Algebra**: ``overlay_arrangements()``, ``union_arrangements()``, ``intersect_arrangements()``, ``intersect_arrangement_with_segment()``, ``add_segment_to_arrangement()``, ``clean_redundant_curves()``. * **Analysis**: ``project_curve_to_axis()`` / ``project_arrangement_to_axis()`` (with the ``Axis`` enum), ``maximum_point_of_curve()`` / ``minimum_point_of_curve()``, ``find_local_minimal_points()``, ``find_monotone_area_of_curve()`` / ``find_monotone_area_of_arrangement()``, ``ellipse_max_min_points()``, ``solve_quadratic()``, ``union_intervals()``, ``mark_true_all_bounded_faces()``. * **Relations**: the backbone of the exact solvers and of the bottleneck-tree / Fréchet family, which reason on free-space arrangements rather than on samples. * **``bounding_boxes.py``** — ``calc_scene_bounding_box(scene)`` returns the ``BoundingBox`` enclosing all robots and obstacles. :meth:`~discopygal.solvers_infra.Solver.Solver.load_scene` calls it and hands the result to the sampler, which is how sampling domains get scoped. * **``transform.py``** — ``offset_polygon(polygon, offset)``, used to grow obstacles by a robot's radius (configuration-space obstacles). * **``display_arrangement.py``** — debug visualizer. ``GUIArrangement`` plus ``display_arrangement()`` render an arrangement in a standalone Qt window, with per-curve-type converters (``segment_to_qt_args()``, ``circle_segment_to_qt_args()``, ``ellipse_segment_to_qt_args()``). 3. Solver Infrastructure Layer (``discopygal.solvers_infra``) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * **``__init__.py``** — the scene data model. This is where the domain objects live (a common misconception is that they live in ``Solver.py``). * **Robots**: ``Robot`` (abstract, carries ``start``, ``end``, ``data`` and a stable ``_obj_id`` that survives serialization), ``RobotDisc``, ``RobotPolygon``, ``RobotRod``. * **Obstacles**: ``Obstacle``, ``ObstacleDisc``, ``ObstaclePolygon``. * **Scene**: ``Scene`` — robots + obstacles + metadata, with a documented JSON serialization format (``__class__``-tagged dicts, resolved by ``load_object_from_dict()``). * **Solutions**: ``PathPoint``, ``Path`` (points + metric), ``PathCollection`` (``dict`` of robot → path; pads paths to equal length and computes time lengths). * **Rendering helper**: ``SceneDrawer``. * **``Solver.py``** — the abstract base contract. * **Classes**: ``Solver``; the ``Trait`` enum and ``add_traits()`` class decorator, which declare what a solver accepts (``ACCEPTS_MULTIPLE_ROBOTS``, ``ACCEPTS_DISC_ROBOT``, ``ACCEPTS_ROD_ROBOT``, …) so the GUI can filter solvers against the loaded scene. * **Lifecycle**: ``load_scene()`` stores the scene and computes the bounding box; ``_solve()`` is the abstract hook each planner implements; ``solve()`` is the non-overridable public entry point that loads (if given a scene), delegates to ``_solve()``, and optionally calls ``analyze_solution()``. * **Introspection**: ``get_arguments()`` returns ``{name: (label, default, type)}``; ``init_solver()`` / ``init_default_solver()`` build an instance from string arguments by casting through that table. This is what lets the Solver Viewer generate parameter forms with no solver-specific UI code. * **Bounding box policy**: ``bounding_margin_width_factor`` — ``0`` for a tight box, ``NO_BOUNDING_BOX`` (``-1``, the default) for none. * **``SamplingSolver.py``** — base class for sampling-based planners. * Holds the three injected strategies: ``nearest_neighbors_class`` (default ``NearestNeighbors_sklearn``), ``metric`` (default ``Metric_SumDist``) and ``sampler`` (default ``Sampler_Uniform``). * **Overrides ``load_scene()``** to also multiplex all robot starts/ends into ``self.start`` / ``self.end`` as ``Point_d``, and to call ``build_roadmap()``. * ``build_roadmap()`` is abstract; ``init_roadmap()`` is a convenience constructor; ``search_path_on_roadmap()`` runs a shortest-path query over the roadmap graph (via the ``networkx`` wrappers ``has_path()``, ``shortest_path()``, ``shortest_path_length()``) and de-multiplexes the tensor path back into a ``PathCollection``. * **``roadmap.py``** — ``Roadmap`` stores sampled configurations and motion edges in a ``networkx`` ``Graph`` (or ``DiGraph`` when ``is_directed``). All vertices are ``Point_d`` of dimension *(single-robot dimension) × (number of robots)*. It composes a ``Sampler``, a ``Metric``, a per-robot ``ObjectCollisionDetection``, and a ``NearestNeighbors`` backend wrapped in ``NearestNeighborsCached``. Key methods: ``is_point_valid()``, ``add_point()``, ``add_sampled_point()``, and collision-checked edge insertion. * **``implicitroadmap.py``** — ``ImplicitRoadmap``, for search spaces too large to materialize: neighbors are generated on demand rather than stored. * **``samplers.py``** — ``Sampler`` (abstract: ``set_scene()``, ``sample()``, ``update_params()``), ``Sampler_Uniform``, ``SamplerWithAngle`` (adds an orientation coordinate, for rod robots), ``Sampler_Elliptical`` (for informed / focused sampling). * **``metrics.py``** — ``Metric`` (abstract, with ``MetricNotImplemented`` for unsupported point types), ``Metric_Euclidean``, ``Metric_SumDist`` (sum of per-robot distances — the multi-robot default), ``RodDisplacementMetric``. Metrics expose both a distance function and a matching ``sklearn``/CGAL metric name so the nearest-neighbor backends stay consistent with edge weights. * **``nearest_neighbors.py``** — ``NearestNeighbors`` (interface: ``fit()``, ``k_nearest()``, ``neighbors_in_radius()``), ``NearestNeighbors_sklearn``, ``NearestNeighbors_CGAL`` (CGAL ``Kd_tree``), ``NeighborsFinder``, and ``NearestNeighborsCached`` — a decorator that batches insertions and defers the expensive ``fit()`` until a query actually needs it. * **``search_algo.py``** — ``GraphNode`` and ``Graph(ImplicitRoadmap)``, implementing multi-heuristic MRMH-A\* over an implicit multi-resolution roadmap, with one Fibonacci heap per heuristic and round-robin queue selection. Note that ordinary sampling solvers do **not** go through this module — they use the ``networkx`` shortest-path helpers in ``SamplingSolver.py``. * **``heuristic_parser.py``** — parses the heuristic specifications used by ``search_algo.Graph``. * **``verify_paths.py``** — ``verify_paths(scene, paths)`` returns ``(bool, reason)``. It re-checks every robot's edges against the obstacles, asserts all paths share a length, tests every pair of robots for collision at every time step, and confirms the endpoints match each robot's declared start/end. * **``operations_counter.py``** — ``OperationsCounter``, a class-level profiling registry with the ``count_calls()`` decorator and ``count_call_context()`` context manager. ``Solver.__init__`` clears it, so every solver run starts from zero. * **``tensor_solver/``** — ``TensorSolver(SamplingSolver)`` and ``TensorRoadmap``. The tensor approach builds one 2D roadmap **per robot**, then searches the tensor product of those roadmaps rather than a single composite roadmap. ``TensorSolver`` inverts the usual hooks: subclasses implement ``build_robot_roadmap(robot)`` and ``search_tensor_roadmap()`` instead of ``build_roadmap()``. 4. Concrete Solvers Layer (``discopygal.solvers``) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ``__init__.py`` is a **registry**, not just a re-export. ``DEFAULT_SOLVER_CLASSES`` maps module paths to the solver names they export; ``import_default_solvers()`` walks it through ``try_import()``, which tolerates missing optional dependencies (notably ``RevolvingAreas``, which needs the external ``RA`` package). ``get_available_solvers()`` lists every registered ``Solver`` subclass and ``get_solver_class(name)`` resolves one by name. ``import_solver_from_file(path)`` loads user-written solvers at runtime — this is how the Solver Viewer opens a solver that is not part of the distribution. * **``rrt/``** — ``rrt.py`` (``RRT``), ``rrt_star.py`` (``RRT_star``), ``birrt.py`` (``BiRRT``), ``lbt_rrt.py`` (``LBT_RRT``), ``informed_rrt_star.py`` (``Informed_RRT_star``), ``drrt.py`` (``dRRT``), ``drrt_star.py`` (``dRRT_star``), ``ao_drrt.py``, ``contact_guided_rrt.py`` (``Contact_Guided_RRT``). The ``dRRT`` family builds on ``TensorSolver``; the rest on ``SamplingSolver``. * **``prm/``** — ``prm.py`` (``PRM``, ``BasicRodPRM``), ``prm2.py`` (``PRM2``, ``PRM3``), ``prm_rod.py``. Classic build-then-query roadmaps. * **``exact/``** — ``exact_single.py`` (``ExactSingle``). Constructs the free-space arrangement directly with ``arrangement_operations`` and plans on its dual graph; no sampling, and complete. * **``staggered_grid/``** — ``staggered_grid.py`` (``StaggeredGrid``), ``staggered_grid_base.py``, ``staggered_grid_drrt.py`` (``dRRTStaggeredGrid``, ``dRRTStarStaggeredGrid``, ``StaggeredGridMinPath``). Deterministic staggered-grid sampling instead of random sampling. * **``lattice_solver/``** — ``latticebase.py``, ``anstar_solver.py`` (``anstar_lattice``), ``staggered_grid_solver.py`` (``staggered_lattice``). Lattice discretizations searched with the MRMH-A\* machinery of ``search_algo.py``. * **``bottleneck_tree/``** — ``bottleneck_tree.py``, ``frechet_matching.py`` (``FrechetMatching``), ``three_curve_frechet.py`` (``ThreeCurveFrechet``), ``exact_three_curve_frechet.py`` (``ExactThreeCurveFrechet``). Bottleneck / Fréchet curve-matching planners. * **``revolving_areas/``** — ``revolving_areas_solver.py`` (``RevolvingAreas``), ``revolving_areas_basic_solver.py``, plus path-combination strategies (``path_combiners.py``, ``path_combiners_factory.py``, ``path_combiner_mode.py``) and local utilities. **Optional**: requires the external ``RA`` package. * **``hgraphs.py``** — ``HGraph`` and ``MetaHGraphSolver``, the hierarchical-graph meta-solvers exported as ``HGraph_PRM`` and ``HGraph_RRT``. * **``mapf.py``** — ``GridMAPFSolver``, grid-based multi-agent pathfinding. 5. Experiments Layer (``discopygal.experiments``) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * **``scenarios_runner.py``** — the batch engine. * ``run_scenarios(scenarios, results_root_path, extra_result_handlers, resume_latest)`` is the top-level driver; ``repeat_scenario()`` handles repetitions and returns a ``pandas.DataFrame``; ``run_single_scenario()`` executes one configuration. * **Process isolation**: ``run_single_scenario_process()`` runs the solver in a separate ``multiprocessing`` process communicating over input/output queues, so a crashing or hanging solver cannot take down the batch. ``TimeoutException`` enforces per-run limits. * **Resumability**: ``get_latest_dir()`` and ``get_results_experiment_path()`` support picking up an interrupted experiment run. * ``run_solver(solver, scene)`` is the thin timing wrapper around ``solver.solve()``. * **``run_experiment.py``** — CLI entry point. ``load_scenarios_and_handlers()`` reads a scenario file, and ``get_chunk_slice(chunk_index, number_of_chunks, n)`` splits the suite so large experiments can be sharded across machines. * **``__init__.py``** — ``Experiment_CompareFunctions``, a helper for comparing implementations across a scenario suite. 6. GUI Toolkit Layer (``discopygal.gui``) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * **``gui.py``** — ``GUI``, the base class that applications subclass alongside their generated Qt ``Ui_*`` form. * **``GraphicsScenePlus.py``** — ``GraphicsScenePlus``, a ``QGraphicsScene`` extended with the drawing helpers DiscoPygal needs. * **``MainWindowsPlus.py``** — main-window scaffolding (zoom, pan, view management). * **``Worker.py``** — ``Worker(QRunnable)`` and ``WorkerSignals``, for running work off the Qt event loop within the same process. * **Drawable wrappers** — one class per CGAL shape rendered on the canvas: ``RDisc``, ``RDiscRobot``, ``RPolygon``, ``RPolygonWithHoles``, ``RSegment``, ``RSegment_angle``, ``RCircleSegment``, ``RText``. * **``color.py``** — the ``PREDEFINED_COLORS`` table and HSV serialization used by scenes. **``logger.py``** — the logging surface the GUI writes into. 7. Applications Layer (``tools/``) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ * **``solver_viewer/``** — the interactive solver runner. * ``solver_viewer_main.py`` — ``SolverViewerGUI`` (main window), ``SolverDialog`` (parameter form, generated from ``get_arguments()``), ``start_gui()`` and ``main()``. ``import_solver_file()`` loads external solver files. * ``solve_process()`` is the function executed in a separate ``multiprocessing.Process``. Results flow back over ``result_queue`` and log records over ``log_queue``, with a ``stop_event`` for cancellation. Arguments are pickle-checked before spawning so failures surface as a dialog rather than a crash in the child. * ``solver_viewer_gui.py`` (generated Qt form), ``serializers.py``, ``visuals.py``, ``theme.py``, ``logger_adapter.py`` (forwards child-process log records to the GUI), ``solver_viewer_headless.py`` (no-UI batch mode). * **``scene_designer/``** — the scene authoring tool: ``scene_designer_main.py``, ``scene_designer_gui.py``, ``object_drawer.py``, ``object_editor.py``, ``object_selector.py``, ``grid.py``, ``preferences_dialog.py``. Produces the JSON scene files consumed by :class:`~discopygal.solvers_infra.Scene`. Data Flow Lifecycle: From Scene to Rendered Solution ----------------------------------------------------- The diagram traces a sampling-based solve, which is the common case. .. digraph:: data_flow_lifecycle :caption: Operational Data Flow Lifecycle rankdir=TB; node [shape=box, style="filled,rounded", fontname="Helvetica", fontsize=10, margin="0.15,0.1"]; edge [fontname="Helvetica", fontsize=9]; input [label="Scene File (.json)\nauthored in scene_designer", fillcolor="#ECEFF1", color="#455A64"]; scene [label="solvers_infra.Scene\n(Robots, Obstacles, Metadata)", fillcolor="#E3F2FD", color="#1976D2"]; load [label="Solver.load_scene(scene)\ncalc_scene_bounding_box()", fillcolor="#BBDEFB", color="#1565C0"]; mux [label="SamplingSolver.load_scene()\nPoint_2_list_to_Point_d(starts / ends)", fillcolor="#90CAF9", color="#0D47A1"]; roadmap_build [label="build_roadmap()\nSampler.sample() -> Roadmap.add_point()\nObjectCollisionDetection + NearestNeighbors", fillcolor="#C8E6C9", color="#388E3C"]; roadmap [label="solvers_infra.roadmap.Roadmap\n(networkx graph over Point_d)", fillcolor="#A5D6A7", color="#2E7D32"]; search [label="search_path_on_roadmap()\nshortest_path(graph, start, end)", fillcolor="#FFE0B2", color="#F57C00"]; extract [label="Path de-multiplexing\nPoint_d_to_Point_2_list() per robot", fillcolor="#FFCC80", color="#EF6C00"]; paths [label="solvers_infra.PathCollection\n(padded, time lengths computed)", fillcolor="#CE93D8", color="#7B1FA2"]; verify [label="verify_paths(scene, paths)\n-> (bool, reason)", fillcolor="#E1BEE7", color="#8E24AA"]; gui_render [label="tools.solver_viewer\n(animated Qt graphics scene)", fillcolor="#F8BBD0", color="#C2185B"]; exp_log [label="discopygal.experiments\n(pandas DataFrame / results dir)", fillcolor="#D1C4E9", color="#512DA8"]; input -> scene; scene -> load; load -> mux; mux -> roadmap_build; roadmap_build -> roadmap; roadmap -> search; search -> extract; extract -> paths; paths -> verify; verify -> gui_render; verify -> exp_log; Exact solvers (``ExactSingle``) short-circuit the middle of this pipeline: instead of sampling into a ``Roadmap``, they build a free-space arrangement with ``arrangement_operations`` and search its dual graph, rejoining the flow at ``PathCollection``. Key Architectural Patterns -------------------------- 1. **Template Method** — :class:`~discopygal.solvers_infra.Solver.Solver` exposes a final ``solve()`` that loads the scene, delegates to the abstract ``_solve()``, and optionally analyzes the result. Solvers override ``_solve()``, never ``solve()``. :class:`~discopygal.solvers_infra.SamplingSolver.SamplingSolver` adds a second tier by overriding ``load_scene()`` to call the abstract ``build_roadmap()``, and ``TensorSolver`` adds a third by implementing ``build_roadmap()`` in terms of ``build_robot_roadmap()`` and ``search_tensor_roadmap()``. 2. **Strategy** — sampling (:class:`~discopygal.solvers_infra.samplers.Sampler`), distance (:class:`~discopygal.solvers_infra.metrics.Metric`) and spatial indexing (:class:`~discopygal.solvers_infra.nearest_neighbors.NearestNeighbors`) are injected through ``SamplingSolver.__init__`` and forwarded into ``Roadmap``. Swapping a sampling distribution or a distance function requires no change to planning logic. Note that the nearest-neighbor *class* is injected, not an instance — ``Roadmap`` instantiates one per roadmap. 3. **Composite Configuration Space** — a multi-robot state is one :class:`~discopygal.bindings.Point_d` of dimension :math:`d = \sum_i \dim(\text{Robot}_i)`. ``Point_2_list_to_Point_d`` and ``Point_d_to_Point_2_list`` in :mod:`discopygal.geometry_utils.conversions` are the multiplex/de-multiplex boundary, and they are the reason a multi-robot problem reduces to a single graph search. 4. **Registry / Plugin Discovery** — :mod:`discopygal.solvers` discovers solver classes by name through ``DEFAULT_SOLVER_CLASSES`` and ``get_solver_class()``, and loads third-party solvers at runtime via ``import_solver_from_file()``. Optional dependencies degrade to a warning rather than an import error. 5. **Introspective Parameters** — ``get_arguments()`` returns ``{name: (label, default, type)}``, and ``init_solver()`` casts raw strings through it. The Solver Viewer's parameter dialog and the experiment runner's scenario files are both generated from this one table, so a new solver parameter needs no UI or config-format change. 6. **Out-of-Process Execution** — both the Solver Viewer (``solve_process()``) and the experiment runner (``run_single_scenario_process()``) execute solvers in separate OS processes with queue-based IPC. This keeps heavy CGAL computation off the Qt event loop, allows hard cancellation and timeouts, and contains crashes. The cost is that everything crossing the boundary must pickle — which is why ``conversions.graph_to_dict()`` / ``arrangement_to_dict()`` exist and why robots carry a stable ``_obj_id`` for matching returned paths back to scene objects. 7. **Dual-Traits Geometry** — the two CGALPY builds (segment vs. circle-segment arrangement traits) are merged into one namespace but must not be mixed inside a single arrangement. ``collision_detection.scene_traits()`` selects the correct flavour for a given robot/obstacle pair. Extension Guidelines for Developers ----------------------------------- * **A new planning algorithm** — subclass :class:`~discopygal.solvers_infra.Solver.Solver` for a general algorithm, :class:`~discopygal.solvers_infra.SamplingSolver.SamplingSolver` for a roadmap/tree algorithm, or ``TensorSolver`` for a per-robot-roadmap algorithm. Implement ``_solve()`` (or ``build_roadmap()`` / ``build_robot_roadmap()`` respectively), declare parameters in ``get_arguments()`` — remembering to ``update()`` with ``super().get_arguments()`` — and annotate the class with ``@add_traits(...)`` so the GUI can match it against scenes. Register it in ``DEFAULT_SOLVER_CLASSES``, or just load the file from the Solver Viewer. * **A custom sampler** — subclass :class:`~discopygal.solvers_infra.samplers.Sampler` and implement ``sample()``; override ``set_scene()`` if you need scene-dependent precomputation. * **A custom metric** — subclass :class:`~discopygal.solvers_infra.metrics.Metric` and implement its distance function, raising ``MetricNotImplemented`` for point types you do not support. * **A nearest-neighbor backend** — implement the :class:`~discopygal.solvers_infra.nearest_neighbors.NearestNeighbors` interface (``fit()``, ``k_nearest()``, ``neighbors_in_radius()``). Pass the *class*, not an instance; ``Roadmap`` will wrap it in ``NearestNeighborsCached``. * **A new robot or obstacle type** — add the class to :mod:`discopygal.solvers_infra` with ``to_dict()`` / ``from_dict()``, extend ``collision_detection`` with the matching predicates, and add a drawable wrapper in :mod:`discopygal.gui`. Related Documentation --------------------- * :doc:`bindings` * :doc:`geometry_utils/geometry_utils` * :doc:`solvers_infra/solvers_infra` * :doc:`solvers/solvers` * :doc:`experiments/experiments` * :doc:`gui/gui` * :doc:`solver_viewer/solver_viewer` * :doc:`scene_designer/scene_designer`