Using discopygal package

In this page it will be explained what the discopygal package contains and how to use it when writing scripts.

The main abilities of the package are:

  1. Create scenes and visualize them

  2. Create motion solving algorithms

  3. Environment to check and visualize the motion solving algorithms on actual scenes

1. How to use the package?

Once installed (in virtual environment or directly on regular environment) the package can be imported from anywhere as a regular package:

import discopygal

or with the “from” syntax for specific modules:

from discopygal... import ...

If the package was installed in virtual env don’t forget to activate it first:

pipenv shell

To deactivate:

exit

If working with an IDE it is also possible to run your scripts from there like any other script. Again, if using virtual env configure first the python interpreter in your IDE to be the right one from the virtual env. If using pipenv, the python interpreter of the virtual env will be found at:

/home/<User name>/.virtualenvs/<virtual env name>/bin/python

2. Components

As can be seen in these docs, the package consists of the following components:

3. Creating a solver

One of the main options discopygal offers is an opportunity to write your own motion planning algorithm which is referred as “Solver”.
Discopygal has a basic solver class discopygal.solvers.Solver which every solver must inherit from.
Therefore, a solver is simply a class that inherits from Solver and implements a motion solving algorithm.
In every solver class it is required to implement the following functions:
  • discopygal.solvers.Solver.load_scene() - The function that gets a scene object and does necessary preprocessing if needed. Call the base function to save the scene in scene

  • discopygal.solvers.Solver.solve() - The function that solves the loaded scene, plans the motion and returns the robots’ paths

  • discopygal.solvers.Solver.get_arguments() - Return a dict of all needed arguments (parameters) by the solver in the following form

    {
        argument_name: (description, default value, type),
        ...
    }
    
    • argument_name: The name of the solver’s attribute

      Important

      Note that the corresponding parameter in the __init__() function of the solver must also have also this name

    • description: The label that will be seen in the gui

    • default value: The default value that will be set for this argument

    • type: The type of the argument

  • discopygal.solvers.Solver.get_graph() (Optional) - Return all the constructed graph (can be viewed in solver_viewer)

  • discopygal.solvers.Solver.get_arrangement() (Optional) - Return an arrangement (can be viewed in solver_viewer)

After creating your solver class, you can initialize solver objects from it and use them directly to solve scenes or load them to Solver viewer and use them there.

Check the Solver class documentation for a list of all the functions it contains.

3.1. Examples

Here are some basic examples to understand how to build a solver class and use it:

3.1.1. Empty Solver

An empty solver that doesn’t do anything, it doesn’t return any paths and therefore of course doesn’t really solve the scene. It shows the minium functions that need to be implemented to create a valid solver

from discopygal.solvers import Solver


class EmptySolver(Solver):
    def solve(self):
        self.log("Solving...")
        return None

    def load_scene(self, scene):
        pass

    @staticmethod
    def get_arguments():
        return {}

3.1.2. Random Solver

This is an example for a solver that creates randomized paths by picking randomized valid points in the scene, creating a graph from them by picking random connections (the roadmap) and then searching for the shortest path for each robot (from it’s start point to it’s end point) Of course this also isn’t a good solver that produces valid paths (check with the ‘Verify paths’ button) but it is possible to draw the paths and run the animation

Solver Class code
RandomSolver.py
import random
import networkx as nx

from discopygal.solvers import Solver, Robot, Path, PathPoint, PathCollection
from discopygal.geometry_utils.bounding_boxes import calc_scene_bounding_box
from discopygal.geometry_utils.collision_detection import ObjectCollisionDetection
from discopygal.bindings import *


class RandomSolver(Solver):
    def __init__(self, num_landmarks, num_connections):
        super().__init__()
        self.num_landmarks = num_landmarks
        self.num_connections = num_connections
        self._collision_detection = {}
        self._start = None
        self._end = None
        self._roadmap = None

    def _sample_random_point(self):
        x = random.uniform(self._x_min, self._x_max)
        y = random.uniform(self._y_min, self._y_max)
        return Point_2(FT(x), FT(y))

    def _create_random_point(self, robots):
        point = self._sample_random_point()

        # Set a point that for all robots it won't collide with an obstacle
        is_valid_point = False
        while not is_valid_point:
            point = self._sample_random_point()
            is_valid_point = all([self._collision_detection[robot].is_point_valid(point) for robot in robots])

        return point

    def _create_random_roadmap(self, robots):
        roadmap = nx.Graph()
        # Add random points
        for _ in range(self.num_landmarks):
            point = self._create_random_point(robots)
            roadmap.add_node(point)

        # Add random connections
        for _ in range(self.num_connections):
            v, u = random.sample(roadmap.nodes, 2)
            roadmap.add_edge(v, u, weight=1)

        for robot in robots:
            roadmap.add_node(robot.start)  # Add starting point
            roadmap.add_edge(robot.start, *random.sample(roadmap.nodes, 1), weight=1) # Connect start to a random point
            roadmap.add_node(robot.end)  # Add ending point
            roadmap.add_edge(robot.end, *random.sample(roadmap.nodes, 1), weight=1) # Connect to end to a random point

        return roadmap


    def load_scene(self, scene):
        super().load_scene(scene)
        self._x_min, self._x_max, self._y_min, self._y_max = calc_scene_bounding_box(self.scene)

        # Build collision detection for each robot
        for robot in self.scene.robots:
            self._collision_detection[robot] = ObjectCollisionDetection(scene.obstacles, robot)

    def get_graph(self):
        return self._roadmap

    def solve(self):
        self.log("Solving...")
        self._roadmap = self._create_random_roadmap(self.scene.robots)

        path_collection = PathCollection()
        for i, robot in enumerate(self.scene.robots):
            self.log(f"Robot {i}")
            if not nx.algorithms.has_path(self._roadmap, robot.start, robot.end):
                self.log(f"No path found for robot {i}")
                return PathCollection()

            found_path = nx.algorithms.shortest_path(self._roadmap, robot.start, robot.end)
            points = [PathPoint(point) for point in found_path]
            path = Path(points)
            path_collection.add_robot_path(robot, path)

        self.log("Successfully found a path for all robots")
        return path_collection

    @staticmethod
    def get_arguments():
        return {
            'num_landmarks': ('Number of points', 5, int),
            'num_connections': ('Number of connections', 5, int),
        }
Use solver in script
random_solver_example.py
import json

from discopygal.solvers import Scene
from discopygal.solvers.verify_paths import verify_paths
from discopygal_tools.solver_viewer import start_gui

from RandomSolver import RandomSolver

with open('basic_scene.json', 'r') as fp:
    scene = Scene.from_dict(json.load(fp))

# "Solve" the scene (find paths for the robots)
solver = RandomSolver(num_landmarks=10, num_connections=10)
solver.load_scene(scene)
path_collection = solver.solve()  # Returns a PathCollection object

# Print the points of the paths
for i, (robot, path) in enumerate(path_collection.paths.items()):
    print("Path for robot {}:".format(i))
    for j, point in enumerate(path.points):
        print(f'\t Point {j:2}:  ', point.location)  # point is of type PathPoint, point.location is CGALPY.Ker.Point_2
    print()

result, reason = verify_paths(scene, path_collection)
print(f"Are paths valid: {result}\t{reason}")

# Optional - Open solver_viewer with our solver and scene

# Option 1 - Use solve object we made:
print("First gui")
start_gui(scene, solver)

# Option 2 - Use solver's class type to create a new one:
print("Second gui")
start_gui(scene, RandomSolver)

# Option 3 - Use solver's class name to create a new one (must pass solver's module):
print("Third gui")
start_gui(scene, "RandomSolver", "RandomSolver.py")

# Option 4 - Passing the path to the scene json file
print("Fourth gui")
start_gui("basic_scene.json", solver)
Invoke Solver viewer with solver from command line
solver_viewer -sl RandomSolver -sf RandomSolver.py -sc simple_motion_planning_scene.json

4. Creating a scene

Another main issue is to create a scene.
There are two ways to do this, the first is to create a json file interactively with Scene designer and load it and the second is to directly to create a discopygal.solvers.Scene object during the script and use it (as was shown in the example).
Check the discopygal.solvers.Scene class documentation to be familiar with the available methods and options.

5. Discopygal starter

Discopygal starter is exactly the same to the regular discopygal except it doesn’t contain any pre-written solver.
All other usage and tools are the same.
It has the basic abstract Solver class and it is required to implement by yourself solvers that will inherit from this class.
If using discopygal-starter and discopygal make sure to install them in two different virtual envs or otherwise uninstall the first before installing the second version.