11. Plot library

This python notebook shows how to use the plot package of the Unified Planning.

Open In GitHub Open In Colab

11.1. Setup

We start by downloading (from github) the unified planning library.

[1]:
!apt install graphviz graphviz-dev
!pip install --pre unified-planning[plot]
E: Could not open lock file /var/lib/dpkg/lock-frontend - open (13: Permission denied)
E: Unable to acquire the dpkg frontend lock (/var/lib/dpkg/lock-frontend), are you root?
Collecting unified-planning[plot]
  Downloading unified_planning-0.6.0.273.dev1-py3-none-any.whl (635 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 635.7/635.7 kB 13.4 MB/s eta 0:00:00
Requirement already satisfied: pyparsing in /home/docs/checkouts/readthedocs.org/user_builds/unified-planning/envs/v1.0.0/lib/python3.10/site-packages (from unified-planning[plot]) (3.1.0)
Requirement already satisfied: networkx in /home/docs/checkouts/readthedocs.org/user_builds/unified-planning/envs/v1.0.0/lib/python3.10/site-packages (from unified-planning[plot]) (3.1)
Collecting plotly (from unified-planning[plot])
  Downloading plotly-5.15.0-py2.py3-none-any.whl (15.5 MB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 15.5/15.5 MB 72.3 MB/s eta 0:00:00
Collecting matplotlib (from unified-planning[plot])
  Downloading matplotlib-3.7.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (11.6 MB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 11.6/11.6 MB 74.6 MB/s eta 0:00:00
Collecting kaleido (from unified-planning[plot])
  Downloading kaleido-0.2.1-py2.py3-none-manylinux1_x86_64.whl (79.9 MB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 79.9/79.9 MB 23.2 MB/s eta 0:00:00
Collecting pygraphviz (from unified-planning[plot])
  Downloading pygraphviz-1.11.zip (120 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 120.8/120.8 kB 25.1 MB/s eta 0:00:00
  Preparing metadata (setup.py) ... - done
Collecting pandas (from unified-planning[plot])
  Downloading pandas-2.0.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (12.3 MB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 12.3/12.3 MB 73.6 MB/s eta 0:00:00
Collecting contourpy>=1.0.1 (from matplotlib->unified-planning[plot])
  Downloading contourpy-1.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (300 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 300.7/300.7 kB 43.4 MB/s eta 0:00:00
Collecting cycler>=0.10 (from matplotlib->unified-planning[plot])
  Downloading cycler-0.11.0-py3-none-any.whl (6.4 kB)
Collecting fonttools>=4.22.0 (from matplotlib->unified-planning[plot])
  Downloading fonttools-4.40.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (4.2 MB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 4.2/4.2 MB 99.5 MB/s eta 0:00:00
Collecting kiwisolver>=1.0.1 (from matplotlib->unified-planning[plot])
  Downloading kiwisolver-1.4.4-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl (1.6 MB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 1.6/1.6 MB 90.0 MB/s eta 0:00:00
Collecting numpy>=1.20 (from matplotlib->unified-planning[plot])
  Downloading numpy-1.25.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (17.6 MB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 17.6/17.6 MB 69.5 MB/s eta 0:00:00
Requirement already satisfied: packaging>=20.0 in /home/docs/checkouts/readthedocs.org/user_builds/unified-planning/envs/v1.0.0/lib/python3.10/site-packages (from matplotlib->unified-planning[plot]) (23.1)
Requirement already satisfied: pillow>=6.2.0 in /home/docs/checkouts/readthedocs.org/user_builds/unified-planning/envs/v1.0.0/lib/python3.10/site-packages (from matplotlib->unified-planning[plot]) (9.5.0)
Requirement already satisfied: python-dateutil>=2.7 in /home/docs/checkouts/readthedocs.org/user_builds/unified-planning/envs/v1.0.0/lib/python3.10/site-packages (from matplotlib->unified-planning[plot]) (2.8.2)
Collecting pytz>=2020.1 (from pandas->unified-planning[plot])
  Downloading pytz-2023.3-py2.py3-none-any.whl (502 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 502.3/502.3 kB 59.5 MB/s eta 0:00:00
Collecting tzdata>=2022.1 (from pandas->unified-planning[plot])
  Downloading tzdata-2023.3-py2.py3-none-any.whl (341 kB)
     ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ 341.8/341.8 kB 44.2 MB/s eta 0:00:00
Collecting tenacity>=6.2.0 (from plotly->unified-planning[plot])
  Downloading tenacity-8.2.2-py3-none-any.whl (24 kB)
Requirement already satisfied: six>=1.5 in /home/docs/checkouts/readthedocs.org/user_builds/unified-planning/envs/v1.0.0/lib/python3.10/site-packages (from python-dateutil>=2.7->matplotlib->unified-planning[plot]) (1.16.0)
Building wheels for collected packages: pygraphviz
  Building wheel for pygraphviz (setup.py) ... - \ | done
  Created wheel for pygraphviz: filename=pygraphviz-1.11-cp310-cp310-linux_x86_64.whl size=220189 sha256=bef02c74284d3b867abf27e79a26efdab1d05866c7aa6b3a482855f567417f2c
  Stored in directory: /home/docs/.cache/pip/wheels/5b/ee/36/f47a0d35664fbe1a2b5a433ae33c6ad636b00bb231f68a9aaa
Successfully built pygraphviz
Installing collected packages: pytz, kaleido, unified-planning, tzdata, tenacity, pygraphviz, numpy, kiwisolver, fonttools, cycler, plotly, pandas, contourpy, matplotlib
Successfully installed contourpy-1.1.0 cycler-0.11.0 fonttools-4.40.0 kaleido-0.2.1 kiwisolver-1.4.4 matplotlib-3.7.1 numpy-1.25.0 pandas-2.0.3 plotly-5.15.0 pygraphviz-1.11 pytz-2023.3 tenacity-8.2.2 tzdata-2023.3 unified-planning-0.6.0.273.dev1

We are now ready to plot some plans using the Unified-Planning library!

11.2. Imports

We need to import all the types of plan we want to plot, the plot functions and the actions.

[2]:
import unified_planning
from unified_planning.plans import (
    SequentialPlan,
    TimeTriggeredPlan,
    PartialOrderPlan,
    ContingentPlanNode,
    ContingentPlan,
    STNPlanNode,
    STNPlan,
)
from unified_planning.plot import (
    plot_plan, # plot_plan plots all the types of plans, but is not customizable, while specific methods
    plot_sequential_plan,
    plot_time_triggered_plan,
    plot_partial_order_plan,
    plot_contingent_plan,
    plot_stn_plan,
    plot_causal_graph,
)


from unified_planning.model import InstantaneousAction, DurativeAction, TimepointKind, Fluent, Problem, Object
from unified_planning.shortcuts import BoolType, UserType, IntType, RealType

Ignore the code below, it’s used to make this notebook also runnable in the Countinuous Intergation.

[3]:
# Redefine the plot package methods imported above to print the plot to a temp file
# if the exception "could not locate runnable browser" is raised. This usually happens
# in the Continuous Integration.

from inspect import getmembers, isfunction
from unified_planning import plot
from functools import partial
import os, uuid, tempfile as tf

# Define the function that will be executed instead
def _function(original_function, *args, **kwargs):
    try:
        original_function(*args, **kwargs)
    except Exception as e:
        if "could not locate runnable browser" in str(e):
            original_function(*args, **kwargs,
                filename=f"{os.path.join(tf.gettempdir(), str(uuid.uuid1()))}.png"
            )
        else:
            raise e

# Iterate over all the functions of the plot package
for function_name, function in getmembers(plot, isfunction):
    # Override the original function with the new one
    globals()[function_name] = partial(_function, function)

11.3. Sequential Plan

A SequentialPlan is simply a sequence of actions.

[4]:
actions = list((InstantaneousAction(f"a{i}") for i in range(1, 5)))
sequential_plan = SequentialPlan([a() for a in actions])

plot_sequential_plan(sequential_plan, font_size=14)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
File ~/checkouts/readthedocs.org/user_builds/unified-planning/envs/v1.0.0/lib/python3.10/site-packages/pygraphviz/agraph.py:1361, in AGraph._get_prog(self, prog)
   1360 try:  # user must pick one of the graphviz programs...
-> 1361     runprog = self._which(prog)
   1362 except:

File ~/checkouts/readthedocs.org/user_builds/unified-planning/envs/v1.0.0/lib/python3.10/site-packages/pygraphviz/agraph.py:1816, in AGraph._which(self, name)
   1815         return match[0]
-> 1816 raise ValueError(f"No prog {name} in path.")

ValueError: No prog dot in path.

During handling of the above exception, another exception occurred:

ValueError                                Traceback (most recent call last)
Cell In[4], line 4
      1 actions = list((InstantaneousAction(f"a{i}") for i in range(1, 5)))
      2 sequential_plan = SequentialPlan([a() for a in actions])
----> 4 plot_sequential_plan(sequential_plan, font_size=14)

Cell In[3], line 20, in _function(original_function, *args, **kwargs)
     16     original_function(*args, **kwargs,
     17         filename=f"{os.path.join(tf.gettempdir(), str(uuid.uuid1()))}.png"
     18     )
     19 else:
---> 20     raise e

Cell In[3], line 13, in _function(original_function, *args, **kwargs)
     11 def _function(original_function, *args, **kwargs):
     12     try:
---> 13         original_function(*args, **kwargs)
     14     except Exception as e:
     15         if "could not locate runnable browser" in str(e):

File ~/checkouts/readthedocs.org/user_builds/unified-planning/envs/v1.0.0/lib/python3.10/site-packages/unified_planning/plot/plan_plot.py:179, in plot_sequential_plan(plan, problem, expression_or_expressions, metric_or_metrics, filename, figsize, top_bottom, generate_node_label, arrowsize, node_size, node_color, edge_color, font_size, font_color, draw_networkx_kwargs)
    177         graph.add_edge(current_ai, next_ai)
    178         current_ai = next_ai
--> 179 fig, _, _ = draw_base_graph(
    180     graph,
    181     figsize=figsize,
    182     top_bottom=top_bottom,
    183     generate_node_label=generate_node_label,
    184     arrowsize=arrowsize,
    185     node_size=node_size,
    186     node_color=node_color,
    187     edge_color=edge_color,
    188     font_size=font_size,
    189     font_color=font_color,
    190     draw_networkx_kwargs=draw_networkx_kwargs,
    191 )
    192 if filename is None:
    193     plt.show()

File ~/checkouts/readthedocs.org/user_builds/unified-planning/envs/v1.0.0/lib/python3.10/site-packages/unified_planning/plot/utils.py:95, in draw_base_graph(graph, figsize, top_bottom, generate_node_label, arrowsize, node_size, node_color, edge_color, font_size, font_color, draw_networkx_kwargs)
     92 fig = plt.figure(figsize=figsize)
     93 ax = fig.add_subplot()
---> 95 pos = _generate_positions(graph, prog="dot", top_bottom=top_bottom)
     97 nx.draw_networkx(
     98     graph,
     99     pos,
   (...)
    110     **draw_networkx_kwargs,
    111 )
    112 return fig, ax, pos

File ~/checkouts/readthedocs.org/user_builds/unified-planning/envs/v1.0.0/lib/python3.10/site-packages/unified_planning/plot/utils.py:139, in _generate_positions(graph, prog, top_bottom)
    136 else:
    137     new_graph.graph.setdefault("graph", {})["rankdir"] = "LR"
--> 139 new_pos = nx.nx_agraph.graphviz_layout(new_graph, prog=prog)
    141 pos = {id_to_node[i]: value for i, value in new_pos.items()}
    143 return pos

File ~/checkouts/readthedocs.org/user_builds/unified-planning/envs/v1.0.0/lib/python3.10/site-packages/networkx/drawing/nx_agraph.py:251, in graphviz_layout(G, prog, root, args)
    220 def graphviz_layout(G, prog="neato", root=None, args=""):
    221     """Create node positions for G using Graphviz.
    222
    223     Parameters
   (...)
    249     see https://gitlab.com/graphviz/graphviz/-/issues/1767 for more info.
    250     """
--> 251     return pygraphviz_layout(G, prog=prog, root=root, args=args)

File ~/checkouts/readthedocs.org/user_builds/unified-planning/envs/v1.0.0/lib/python3.10/site-packages/networkx/drawing/nx_agraph.py:303, in pygraphviz_layout(G, prog, root, args)
    301     args += f"-Groot={root}"
    302 A = to_agraph(G)
--> 303 A.layout(prog=prog, args=args)
    304 node_pos = {}
    305 for n in G:

File ~/checkouts/readthedocs.org/user_builds/unified-planning/envs/v1.0.0/lib/python3.10/site-packages/pygraphviz/agraph.py:1483, in AGraph.layout(self, prog, args)
   1466 """Assign positions to nodes in graph.
   1467
   1468 Optional prog=['neato'|'dot'|'twopi'|'circo'|'fdp'|'nop']
   (...)
   1480
   1481 """
   1482 output_fmt = "dot"
-> 1483 data = self._run_prog(prog, " ".join([args, "-T", output_fmt]))
   1484 self.from_string(data)
   1485 self.has_layout = True

File ~/checkouts/readthedocs.org/user_builds/unified-planning/envs/v1.0.0/lib/python3.10/site-packages/pygraphviz/agraph.py:1377, in AGraph._run_prog(self, prog, args)
   1367 def _run_prog(self, prog="nop", args=""):
   1368     """Apply graphviz program to graph and return the result as a string.
   1369
   1370     >>> import pygraphviz as pgv
   (...)
   1375     Use keyword args to add additional arguments to graphviz programs.
   1376     """
-> 1377     runprog = r'"%s"' % self._get_prog(prog)
   1378     cmd = " ".join([runprog, args])
   1379     dotargs = shlex.split(cmd)

File ~/checkouts/readthedocs.org/user_builds/unified-planning/envs/v1.0.0/lib/python3.10/site-packages/pygraphviz/agraph.py:1363, in AGraph._get_prog(self, prog)
   1361     runprog = self._which(prog)
   1362 except:
-> 1363     raise ValueError(f"Program {prog} not found in path.")
   1365 return runprog

ValueError: Program dot not found in path.
../_images/notebooks_11-plot_9_1.png

11.3.1. Plotting fluents in SequentialPlan

On a SequentialPlan, if the UPSequentialSimulator (in the up.engines.sequential_simulator.py) supports the Problem, the value of some arbitrary expressions or the value of some quality metrics can be plotted. This shows how the value changes during the plan.

Only numeric or boolean expressions can be plotted.

11.3.1.1. Define the problem

The problem defined is a robot that moves from l1 to l4, passing through l2 and l3.

While moving, the battery decreases based on the distance from the locations and the total distance accumulates.

Note: the problem has specified only the parts interesting for this simulation. It’s not a complete problem to give to a planner.

[5]:
# Define the UserType
Location = UserType("Location")

# Define fluents
robot_at = Fluent("robot_at", BoolType(), position=Location)
battery_charge = Fluent("battery_charge", RealType(0, 100))
distance = Fluent("distance", IntType(), l_from=Location, l_to=Location)
total_distance = Fluent("total_distance", IntType())

# Define the move action
move = InstantaneousAction("move", l_from=Location, l_to=Location)
l_from = move.parameter("l_from")
l_to = move.parameter("l_to")
move.add_precondition(robot_at(l_from))
move.add_effect(robot_at(l_from), False)
move.add_effect(robot_at(l_to), True)
move.add_decrease_effect(battery_charge, distance(l_from, l_to)/2 + 5)
move.add_increase_effect(total_distance, distance(l_from, l_to))

# Define the Location Objects
objects = list((Object(f"l{i}", Location) for i in range(1, 5)))
l1, l2, l3, l4 = objects

# Create the problem, add fluents, the move action and the objects
problem = Problem("moving robot")
problem.add_fluent(robot_at, default_initial_value=False)
problem.add_fluent(battery_charge, default_initial_value=100)
problem.add_fluent(distance, default_initial_value=0)
problem.add_fluent(total_distance, default_initial_value=0)
problem.add_action(move)
problem.add_objects(objects)

# Set the initial values different from the defaults
problem.set_initial_value(robot_at(l1), True)
problem.set_initial_value(distance(l1, l2), 26)
problem.set_initial_value(distance(l2, l3), 50)
problem.set_initial_value(distance(l3, l4), 10)

# Create the plan to simulate
plan = SequentialPlan([
    move(l1, l2),
    move(l2, l3),
    move(l3, l4),
])
[6]:
plot_sequential_plan(plan, problem, [battery_charge, total_distance])

11.4. TimeTriggeredPlan

A TimeTriggeredPlan is a sequence of 3 items. Every items is composed by an action, the time in which the action starts, and, if the action is a DurativeAction, the action duration.

[7]:
a1, a2, a3, a4 = (DurativeAction(f"a{i}") for i in range(1, 5))
time_triggered_plan = TimeTriggeredPlan([
    (0, a1(), 3),
    (1, a2(), 3),
    (2, a3(), 2),
    (2, a4(), 1),
])

plot_plan(time_triggered_plan)

11.5. PartialOrderPlan

A PartialOrderPlan is a directed graph where the nodes are ActionInstances and the edges create an ordering between 2 actions.

In the unified_planning, a ParitalOrderPlan is created with the adjacency list.

Generally speaking, a SequentialPlan is a specific case of a PartialOrderPlan where every action has only one possible action that must be done before and one action that must be done later.

[8]:
ai1, ai2, ai3, ai4, ai5, ai6, ai7, ai8 = (InstantaneousAction(f"a{i}")() for i in range(1, 9))
partial_order_plan = PartialOrderPlan({
    ai1: [ai2],
    ai2: [ai4],
    ai3: [ai4],
    ai4: [ai5, ai6],
    ai5: [ai7],
    ai6: [ai8],
    ai7: [ai8],
})

plot_partial_order_plan(partial_order_plan, font_size=16)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
File ~/checkouts/readthedocs.org/user_builds/unified-planning/envs/v1.0.0/lib/python3.10/site-packages/pygraphviz/agraph.py:1361, in AGraph._get_prog(self, prog)
   1360 try:  # user must pick one of the graphviz programs...
-> 1361     runprog = self._which(prog)
   1362 except:

File ~/checkouts/readthedocs.org/user_builds/unified-planning/envs/v1.0.0/lib/python3.10/site-packages/pygraphviz/agraph.py:1816, in AGraph._which(self, name)
   1815         return match[0]
-> 1816 raise ValueError(f"No prog {name} in path.")

ValueError: No prog dot in path.

During handling of the above exception, another exception occurred:

ValueError                                Traceback (most recent call last)
Cell In[8], line 12
      1 ai1, ai2, ai3, ai4, ai5, ai6, ai7, ai8 = (InstantaneousAction(f"a{i}")() for i in range(1, 9))
      2 partial_order_plan = PartialOrderPlan({
      3     ai1: [ai2],
      4     ai2: [ai4],
   (...)
      9     ai7: [ai8],
     10 })
---> 12 plot_partial_order_plan(partial_order_plan, font_size=16)

Cell In[3], line 20, in _function(original_function, *args, **kwargs)
     16     original_function(*args, **kwargs,
     17         filename=f"{os.path.join(tf.gettempdir(), str(uuid.uuid1()))}.png"
     18     )
     19 else:
---> 20     raise e

Cell In[3], line 13, in _function(original_function, *args, **kwargs)
     11 def _function(original_function, *args, **kwargs):
     12     try:
---> 13         original_function(*args, **kwargs)
     14     except Exception as e:
     15         if "could not locate runnable browser" in str(e):

File ~/checkouts/readthedocs.org/user_builds/unified-planning/envs/v1.0.0/lib/python3.10/site-packages/unified_planning/plot/plan_plot.py:587, in plot_partial_order_plan(plan, filename, figsize, top_bottom, generate_node_label, arrowsize, node_size, node_color, edge_color, font_size, font_color, draw_networkx_kwargs)
    547 """
    548 This method plots the Contingent plan as a directed graph where the edges are
    549 labeled with the temporal constraints contained in the given ``plan``.
   (...)
    583     maintained in any way and it might be removed or modified at any moment.
    584 """
    585 import matplotlib.pyplot as plt  # type: ignore[import]
--> 587 fig, _, _ = draw_base_graph(
    588     plan._graph,
    589     figsize=figsize,
    590     top_bottom=top_bottom,
    591     generate_node_label=generate_node_label,
    592     arrowsize=arrowsize,
    593     node_size=node_size,
    594     node_color=node_color,
    595     edge_color=edge_color,
    596     font_size=font_size,
    597     font_color=font_color,
    598     draw_networkx_kwargs=draw_networkx_kwargs,
    599 )
    600 if filename is None:
    601     plt.show()

File ~/checkouts/readthedocs.org/user_builds/unified-planning/envs/v1.0.0/lib/python3.10/site-packages/unified_planning/plot/utils.py:95, in draw_base_graph(graph, figsize, top_bottom, generate_node_label, arrowsize, node_size, node_color, edge_color, font_size, font_color, draw_networkx_kwargs)
     92 fig = plt.figure(figsize=figsize)
     93 ax = fig.add_subplot()
---> 95 pos = _generate_positions(graph, prog="dot", top_bottom=top_bottom)
     97 nx.draw_networkx(
     98     graph,
     99     pos,
   (...)
    110     **draw_networkx_kwargs,
    111 )
    112 return fig, ax, pos

File ~/checkouts/readthedocs.org/user_builds/unified-planning/envs/v1.0.0/lib/python3.10/site-packages/unified_planning/plot/utils.py:139, in _generate_positions(graph, prog, top_bottom)
    136 else:
    137     new_graph.graph.setdefault("graph", {})["rankdir"] = "LR"
--> 139 new_pos = nx.nx_agraph.graphviz_layout(new_graph, prog=prog)
    141 pos = {id_to_node[i]: value for i, value in new_pos.items()}
    143 return pos

File ~/checkouts/readthedocs.org/user_builds/unified-planning/envs/v1.0.0/lib/python3.10/site-packages/networkx/drawing/nx_agraph.py:251, in graphviz_layout(G, prog, root, args)
    220 def graphviz_layout(G, prog="neato", root=None, args=""):
    221     """Create node positions for G using Graphviz.
    222
    223     Parameters
   (...)
    249     see https://gitlab.com/graphviz/graphviz/-/issues/1767 for more info.
    250     """
--> 251     return pygraphviz_layout(G, prog=prog, root=root, args=args)

File ~/checkouts/readthedocs.org/user_builds/unified-planning/envs/v1.0.0/lib/python3.10/site-packages/networkx/drawing/nx_agraph.py:303, in pygraphviz_layout(G, prog, root, args)
    301     args += f"-Groot={root}"
    302 A = to_agraph(G)
--> 303 A.layout(prog=prog, args=args)
    304 node_pos = {}
    305 for n in G:

File ~/checkouts/readthedocs.org/user_builds/unified-planning/envs/v1.0.0/lib/python3.10/site-packages/pygraphviz/agraph.py:1483, in AGraph.layout(self, prog, args)
   1466 """Assign positions to nodes in graph.
   1467
   1468 Optional prog=['neato'|'dot'|'twopi'|'circo'|'fdp'|'nop']
   (...)
   1480
   1481 """
   1482 output_fmt = "dot"
-> 1483 data = self._run_prog(prog, " ".join([args, "-T", output_fmt]))
   1484 self.from_string(data)
   1485 self.has_layout = True

File ~/checkouts/readthedocs.org/user_builds/unified-planning/envs/v1.0.0/lib/python3.10/site-packages/pygraphviz/agraph.py:1377, in AGraph._run_prog(self, prog, args)
   1367 def _run_prog(self, prog="nop", args=""):
   1368     """Apply graphviz program to graph and return the result as a string.
   1369
   1370     >>> import pygraphviz as pgv
   (...)
   1375     Use keyword args to add additional arguments to graphviz programs.
   1376     """
-> 1377     runprog = r'"%s"' % self._get_prog(prog)
   1378     cmd = " ".join([runprog, args])
   1379     dotargs = shlex.split(cmd)

File ~/checkouts/readthedocs.org/user_builds/unified-planning/envs/v1.0.0/lib/python3.10/site-packages/pygraphviz/agraph.py:1363, in AGraph._get_prog(self, prog)
   1361     runprog = self._which(prog)
   1362 except:
-> 1363     raise ValueError(f"Program {prog} not found in path.")
   1365 return runprog

ValueError: Program dot not found in path.
../_images/notebooks_11-plot_17_1.png

11.6. STNPlan

An STNPlan represents the temporal constraints from the events that must be performed during the plan.

An event is either the start of the plan, the end of the plan, the start of an action or the end of an action.

It is represented as a directed graph, where the nodes are the events.

There is an edge from the node N to the node M if N and M have a relative temporal constraint. The edge is labeled with 2 numbers, the lower bound and the upper bound to the time that can elapse from N to M.

For example, N –[0, 7]–> M means that M must happen after -or at the same time, since 0 is included- N, but no more than 7 time units later.

[9]:
ai1, ai2, ai3, ai4 = (InstantaneousAction(f"a{i}")() for i in range(1, 5))
start_a1 = STNPlanNode(TimepointKind.START, ai1)
end_a1 = STNPlanNode(TimepointKind.END, ai1)
start_a2 = STNPlanNode(TimepointKind.START, ai2)
end_a2 = STNPlanNode(TimepointKind.END, ai2)
start_a3 = STNPlanNode(TimepointKind.START, ai3)
end_a3 = STNPlanNode(TimepointKind.END, ai3)
start_a4 = STNPlanNode(TimepointKind.START, ai4)
end_a4 = STNPlanNode(TimepointKind.END, ai4)
stn_plan = STNPlan([
    (start_a1, 1, 1, end_a1), # Link start to end actions
    (start_a2, 0, 3, end_a2),
    (start_a3, 0, 3, end_a3),
    (start_a4, 0, 3, end_a4),
    (end_a1, 0, None, start_a2), # Action 1 must finish before action 2 start (or in the same moment)
    (end_a3, None, 1, end_a4), # Action 3 can finish AT most 1 after the end of Action4
])

plot_stn_plan(stn_plan)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
File ~/checkouts/readthedocs.org/user_builds/unified-planning/envs/v1.0.0/lib/python3.10/site-packages/pygraphviz/agraph.py:1361, in AGraph._get_prog(self, prog)
   1360 try:  # user must pick one of the graphviz programs...
-> 1361     runprog = self._which(prog)
   1362 except:

File ~/checkouts/readthedocs.org/user_builds/unified-planning/envs/v1.0.0/lib/python3.10/site-packages/pygraphviz/agraph.py:1816, in AGraph._which(self, name)
   1815         return match[0]
-> 1816 raise ValueError(f"No prog {name} in path.")

ValueError: No prog dot in path.

During handling of the above exception, another exception occurred:

ValueError                                Traceback (most recent call last)
Cell In[9], line 19
      9 end_a4 = STNPlanNode(TimepointKind.END, ai4)
     10 stn_plan = STNPlan([
     11     (start_a1, 1, 1, end_a1), # Link start to end actions
     12     (start_a2, 0, 3, end_a2),
   (...)
     16     (end_a3, None, 1, end_a4), # Action 3 can finish AT most 1 after the end of Action4
     17 ])
---> 19 plot_stn_plan(stn_plan)

Cell In[3], line 20, in _function(original_function, *args, **kwargs)
     16     original_function(*args, **kwargs,
     17         filename=f"{os.path.join(tf.gettempdir(), str(uuid.uuid1()))}.png"
     18     )
     19 else:
---> 20     raise e

Cell In[3], line 13, in _function(original_function, *args, **kwargs)
     11 def _function(original_function, *args, **kwargs):
     12     try:
---> 13         original_function(*args, **kwargs)
     14     except Exception as e:
     15         if "could not locate runnable browser" in str(e):

File ~/checkouts/readthedocs.org/user_builds/unified-planning/envs/v1.0.0/lib/python3.10/site-packages/unified_planning/plot/plan_plot.py:398, in plot_stn_plan(plan, filename, figsize, top_bottom, generate_node_label, arrowsize, node_size, node_color, edge_color, font_size, font_color, generate_edge_label, edge_font_size, edge_font_color, draw_networkx_kwargs, draw_networkx_edge_labels_kwargs)
    393         graph.add_edge(left_node, right_node)
    394         edge_labels[(left_node, right_node)] = edge_label_function(
    395             lower_bound, upper_bound
    396         )
--> 398 fig, ax, pos = draw_base_graph(
    399     graph,
    400     figsize=figsize,
    401     top_bottom=top_bottom,
    402     generate_node_label=generate_node_label,
    403     arrowsize=arrowsize,
    404     node_size=node_size,
    405     node_color=node_color,
    406     edge_color=edge_color,
    407     font_size=font_size,
    408     font_color=font_color,
    409     draw_networkx_kwargs=draw_networkx_kwargs,
    410 )
    411 nx.draw_networkx_edge_labels(
    412     graph,
    413     pos,
   (...)
    418     **draw_networkx_edge_labels_kwargs,
    419 )
    420 if filename is None:

File ~/checkouts/readthedocs.org/user_builds/unified-planning/envs/v1.0.0/lib/python3.10/site-packages/unified_planning/plot/utils.py:95, in draw_base_graph(graph, figsize, top_bottom, generate_node_label, arrowsize, node_size, node_color, edge_color, font_size, font_color, draw_networkx_kwargs)
     92 fig = plt.figure(figsize=figsize)
     93 ax = fig.add_subplot()
---> 95 pos = _generate_positions(graph, prog="dot", top_bottom=top_bottom)
     97 nx.draw_networkx(
     98     graph,
     99     pos,
   (...)
    110     **draw_networkx_kwargs,
    111 )
    112 return fig, ax, pos

File ~/checkouts/readthedocs.org/user_builds/unified-planning/envs/v1.0.0/lib/python3.10/site-packages/unified_planning/plot/utils.py:139, in _generate_positions(graph, prog, top_bottom)
    136 else:
    137     new_graph.graph.setdefault("graph", {})["rankdir"] = "LR"
--> 139 new_pos = nx.nx_agraph.graphviz_layout(new_graph, prog=prog)
    141 pos = {id_to_node[i]: value for i, value in new_pos.items()}
    143 return pos

File ~/checkouts/readthedocs.org/user_builds/unified-planning/envs/v1.0.0/lib/python3.10/site-packages/networkx/drawing/nx_agraph.py:251, in graphviz_layout(G, prog, root, args)
    220 def graphviz_layout(G, prog="neato", root=None, args=""):
    221     """Create node positions for G using Graphviz.
    222
    223     Parameters
   (...)
    249     see https://gitlab.com/graphviz/graphviz/-/issues/1767 for more info.
    250     """
--> 251     return pygraphviz_layout(G, prog=prog, root=root, args=args)

File ~/checkouts/readthedocs.org/user_builds/unified-planning/envs/v1.0.0/lib/python3.10/site-packages/networkx/drawing/nx_agraph.py:303, in pygraphviz_layout(G, prog, root, args)
    301     args += f"-Groot={root}"
    302 A = to_agraph(G)
--> 303 A.layout(prog=prog, args=args)
    304 node_pos = {}
    305 for n in G:

File ~/checkouts/readthedocs.org/user_builds/unified-planning/envs/v1.0.0/lib/python3.10/site-packages/pygraphviz/agraph.py:1483, in AGraph.layout(self, prog, args)
   1466 """Assign positions to nodes in graph.
   1467
   1468 Optional prog=['neato'|'dot'|'twopi'|'circo'|'fdp'|'nop']
   (...)
   1480
   1481 """
   1482 output_fmt = "dot"
-> 1483 data = self._run_prog(prog, " ".join([args, "-T", output_fmt]))
   1484 self.from_string(data)
   1485 self.has_layout = True

File ~/checkouts/readthedocs.org/user_builds/unified-planning/envs/v1.0.0/lib/python3.10/site-packages/pygraphviz/agraph.py:1377, in AGraph._run_prog(self, prog, args)
   1367 def _run_prog(self, prog="nop", args=""):
   1368     """Apply graphviz program to graph and return the result as a string.
   1369
   1370     >>> import pygraphviz as pgv
   (...)
   1375     Use keyword args to add additional arguments to graphviz programs.
   1376     """
-> 1377     runprog = r'"%s"' % self._get_prog(prog)
   1378     cmd = " ".join([runprog, args])
   1379     dotargs = shlex.split(cmd)

File ~/checkouts/readthedocs.org/user_builds/unified-planning/envs/v1.0.0/lib/python3.10/site-packages/pygraphviz/agraph.py:1363, in AGraph._get_prog(self, prog)
   1361     runprog = self._which(prog)
   1362 except:
-> 1363     raise ValueError(f"Program {prog} not found in path.")
   1365 return runprog

ValueError: Program dot not found in path.
../_images/notebooks_11-plot_19_1.png

11.7. ContingentPlan

A ContingentPlan is represented as a decision tree. Some actions can sense the initially unknown value of one or more fluents and decide which action to perform next based on the sensed values.

It is represented as a directed graph, with the nodes containing the action to perform and the edges labeled as the expression that must be True in order to take that branch.

[10]:
from unified_planning.shortcuts import *
ai1, ai2, ai3, ai4, ai5 = (InstantaneousAction(f"a{i}")() for i in range(1, 6))
node_1 = ContingentPlanNode(ai1)
node_2 = ContingentPlanNode(ai2)
node_3 = ContingentPlanNode(ai3)
node_4 = ContingentPlanNode(ai4)
node_5 = ContingentPlanNode(ai5)
x, y = Fluent("x"), Fluent("y")
node_1.add_child({x(): TRUE()}, node_2)
node_1.add_child({x(): FALSE()}, node_3)
node_3.add_child({y(): TRUE()}, node_4)
node_3.add_child({y(): FALSE()}, node_5)
contingent_plan = ContingentPlan(node_1)

plot_contingent_plan(contingent_plan, font_size = 14, edge_font_size = 12)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
File ~/checkouts/readthedocs.org/user_builds/unified-planning/envs/v1.0.0/lib/python3.10/site-packages/pygraphviz/agraph.py:1361, in AGraph._get_prog(self, prog)
   1360 try:  # user must pick one of the graphviz programs...
-> 1361     runprog = self._which(prog)
   1362 except:

File ~/checkouts/readthedocs.org/user_builds/unified-planning/envs/v1.0.0/lib/python3.10/site-packages/pygraphviz/agraph.py:1816, in AGraph._which(self, name)
   1815         return match[0]
-> 1816 raise ValueError(f"No prog {name} in path.")

ValueError: No prog dot in path.

During handling of the above exception, another exception occurred:

ValueError                                Traceback (most recent call last)
Cell In[10], line 15
     12 node_3.add_child({y(): FALSE()}, node_5)
     13 contingent_plan = ContingentPlan(node_1)
---> 15 plot_contingent_plan(contingent_plan, font_size = 14, edge_font_size = 12)

Cell In[3], line 20, in _function(original_function, *args, **kwargs)
     16     original_function(*args, **kwargs,
     17         filename=f"{os.path.join(tf.gettempdir(), str(uuid.uuid1()))}.png"
     18     )
     19 else:
---> 20     raise e

Cell In[3], line 13, in _function(original_function, *args, **kwargs)
     11 def _function(original_function, *args, **kwargs):
     12     try:
---> 13         original_function(*args, **kwargs)
     14     except Exception as e:
     15         if "could not locate runnable browser" in str(e):

File ~/checkouts/readthedocs.org/user_builds/unified-planning/envs/v1.0.0/lib/python3.10/site-packages/unified_planning/plot/plan_plot.py:504, in plot_contingent_plan(plan, filename, figsize, top_bottom, generate_node_label, arrowsize, node_size, node_color, edge_color, font_size, font_color, generate_edge_label, edge_font_size, edge_font_color, draw_networkx_kwargs, draw_networkx_edge_labels_kwargs)
    501         graph.add_edge(node, child)
    502         edge_labels[(node, child)] = edge_label_function(fluents)
--> 504 fig, ax, pos = draw_base_graph(
    505     graph,
    506     figsize=figsize,
    507     top_bottom=top_bottom,
    508     generate_node_label=generate_node_label,
    509     arrowsize=arrowsize,
    510     node_size=node_size,
    511     node_color=node_color,
    512     edge_color=edge_color,
    513     font_size=font_size,
    514     font_color=font_color,
    515     draw_networkx_kwargs=draw_networkx_kwargs,
    516 )
    517 nx.draw_networkx_edge_labels(
    518     graph,
    519     pos,
   (...)
    524     **draw_networkx_edge_labels_kwargs,
    525 )
    526 if filename is None:

File ~/checkouts/readthedocs.org/user_builds/unified-planning/envs/v1.0.0/lib/python3.10/site-packages/unified_planning/plot/utils.py:95, in draw_base_graph(graph, figsize, top_bottom, generate_node_label, arrowsize, node_size, node_color, edge_color, font_size, font_color, draw_networkx_kwargs)
     92 fig = plt.figure(figsize=figsize)
     93 ax = fig.add_subplot()
---> 95 pos = _generate_positions(graph, prog="dot", top_bottom=top_bottom)
     97 nx.draw_networkx(
     98     graph,
     99     pos,
   (...)
    110     **draw_networkx_kwargs,
    111 )
    112 return fig, ax, pos

File ~/checkouts/readthedocs.org/user_builds/unified-planning/envs/v1.0.0/lib/python3.10/site-packages/unified_planning/plot/utils.py:139, in _generate_positions(graph, prog, top_bottom)
    136 else:
    137     new_graph.graph.setdefault("graph", {})["rankdir"] = "LR"
--> 139 new_pos = nx.nx_agraph.graphviz_layout(new_graph, prog=prog)
    141 pos = {id_to_node[i]: value for i, value in new_pos.items()}
    143 return pos

File ~/checkouts/readthedocs.org/user_builds/unified-planning/envs/v1.0.0/lib/python3.10/site-packages/networkx/drawing/nx_agraph.py:251, in graphviz_layout(G, prog, root, args)
    220 def graphviz_layout(G, prog="neato", root=None, args=""):
    221     """Create node positions for G using Graphviz.
    222
    223     Parameters
   (...)
    249     see https://gitlab.com/graphviz/graphviz/-/issues/1767 for more info.
    250     """
--> 251     return pygraphviz_layout(G, prog=prog, root=root, args=args)

File ~/checkouts/readthedocs.org/user_builds/unified-planning/envs/v1.0.0/lib/python3.10/site-packages/networkx/drawing/nx_agraph.py:303, in pygraphviz_layout(G, prog, root, args)
    301     args += f"-Groot={root}"
    302 A = to_agraph(G)
--> 303 A.layout(prog=prog, args=args)
    304 node_pos = {}
    305 for n in G:

File ~/checkouts/readthedocs.org/user_builds/unified-planning/envs/v1.0.0/lib/python3.10/site-packages/pygraphviz/agraph.py:1483, in AGraph.layout(self, prog, args)
   1466 """Assign positions to nodes in graph.
   1467
   1468 Optional prog=['neato'|'dot'|'twopi'|'circo'|'fdp'|'nop']
   (...)
   1480
   1481 """
   1482 output_fmt = "dot"
-> 1483 data = self._run_prog(prog, " ".join([args, "-T", output_fmt]))
   1484 self.from_string(data)
   1485 self.has_layout = True

File ~/checkouts/readthedocs.org/user_builds/unified-planning/envs/v1.0.0/lib/python3.10/site-packages/pygraphviz/agraph.py:1377, in AGraph._run_prog(self, prog, args)
   1367 def _run_prog(self, prog="nop", args=""):
   1368     """Apply graphviz program to graph and return the result as a string.
   1369
   1370     >>> import pygraphviz as pgv
   (...)
   1375     Use keyword args to add additional arguments to graphviz programs.
   1376     """
-> 1377     runprog = r'"%s"' % self._get_prog(prog)
   1378     cmd = " ".join([runprog, args])
   1379     dotargs = shlex.split(cmd)

File ~/checkouts/readthedocs.org/user_builds/unified-planning/envs/v1.0.0/lib/python3.10/site-packages/pygraphviz/agraph.py:1363, in AGraph._get_prog(self, prog)
   1361     runprog = self._which(prog)
   1362 except:
-> 1363     raise ValueError(f"Program {prog} not found in path.")
   1365 return runprog

ValueError: Program dot not found in path.
../_images/notebooks_11-plot_21_1.png

11.8. Causal Graph

The causal graph of a problem shows how the different (grounded) fluents of the problem are interwined by the actions.

Every node of the graph is a fluent and there is an arc from F1 to F2 if there is an action in the problem that reads F1 and writes F2. If an action writes both F1 and F2 the arc from F1 to F2 will be bidirectional.

The edge labels are the actions that use both fluents.

11.8.1. Define the problem

The problem has a cargo at Location l2 that must be moved by a robot to a location l1.

[11]:
# Define UserTypes
Location = UserType("Location")

# Define Fluents
robot_at = Fluent("robot_at", BoolType(), position=Location)
cargo_at = Fluent("cargo_at", BoolType(), position=Location)
cargo_mounted = Fluent("cargo_mounted")

# Define move action
move = InstantaneousAction("move", l_from=Location, l_to=Location)
l_from = move.parameter("l_from")
l_to = move.parameter("l_to")
move.add_precondition(robot_at(l_from))
move.add_precondition(Not(robot_at(l_to)))
move.add_effect(robot_at(l_from), False)
move.add_effect(robot_at(l_to), True)

# Define load action
load = InstantaneousAction("load", loc=Location)
loc = load.parameter("loc")
load.add_precondition(cargo_at(loc))
load.add_precondition(robot_at(loc))
load.add_effect(cargo_at(loc), False)
load.add_effect(cargo_mounted, True)

# Define unload action
unload = InstantaneousAction("unload", loc=Location)
loc = unload.parameter("loc")
unload.add_precondition(robot_at(loc))
unload.add_precondition(cargo_mounted)
unload.add_effect(cargo_at(loc), True)
unload.add_effect(cargo_mounted, False)

# Define objects
l1 = Object("l1", Location)
l2 = Object("l2", Location)

# Create the problem and add fluents, actions and objects
problem = Problem("robot_loader")
problem.add_fluent(robot_at, default_initial_value=False)
problem.add_fluent(cargo_at, default_initial_value=False)
problem.add_fluent(cargo_mounted, default_initial_value=False)
problem.add_action(move)
problem.add_action(load)
problem.add_action(unload)
problem.add_objects((l1, l2))

# Set initial value and goals
problem.set_initial_value(robot_at(l1), True)
problem.set_initial_value(cargo_at(l2), True)
problem.add_goal(cargo_at(l1))

Plot the causal graph

[12]:
plot_causal_graph(problem)
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
File ~/checkouts/readthedocs.org/user_builds/unified-planning/envs/v1.0.0/lib/python3.10/site-packages/pygraphviz/agraph.py:1361, in AGraph._get_prog(self, prog)
   1360 try:  # user must pick one of the graphviz programs...
-> 1361     runprog = self._which(prog)
   1362 except:

File ~/checkouts/readthedocs.org/user_builds/unified-planning/envs/v1.0.0/lib/python3.10/site-packages/pygraphviz/agraph.py:1816, in AGraph._which(self, name)
   1815         return match[0]
-> 1816 raise ValueError(f"No prog {name} in path.")

ValueError: No prog dot in path.

During handling of the above exception, another exception occurred:

ValueError                                Traceback (most recent call last)
Cell In[12], line 1
----> 1 plot_causal_graph(problem)

Cell In[3], line 20, in _function(original_function, *args, **kwargs)
     16     original_function(*args, **kwargs,
     17         filename=f"{os.path.join(tf.gettempdir(), str(uuid.uuid1()))}.png"
     18     )
     19 else:
---> 20     raise e

Cell In[3], line 13, in _function(original_function, *args, **kwargs)
     11 def _function(original_function, *args, **kwargs):
     12     try:
---> 13         original_function(*args, **kwargs)
     14     except Exception as e:
     15         if "could not locate runnable browser" in str(e):

File ~/checkouts/readthedocs.org/user_builds/unified-planning/envs/v1.0.0/lib/python3.10/site-packages/unified_planning/plot/causal_graph_plot.py:234, in plot_causal_graph(problem, filename, figsize, top_bottom, generate_node_label, arrowsize, node_size, node_color, edge_color, font_size, font_color, generate_edge_label, edge_font_size, edge_font_color, draw_networkx_kwargs, draw_networkx_edge_labels_kwargs)
    225                 label.add(
    226                     edge_label_function(
    227                         *actions_mapping.get(ln_action, (ln_action, tuple()))
    228                     )
    229                 )
    230 edge_labels: Dict[Tuple[FNode, FNode], str] = {
    231     edge: ", ".join(labels) for edge, labels in edge_labels_set.items() if labels
    232 }
--> 234 fig, ax, pos = draw_base_graph(
    235     graph,
    236     figsize=figsize,
    237     top_bottom=top_bottom,
    238     generate_node_label=generate_node_label,
    239     arrowsize=arrowsize,
    240     node_size=node_size,
    241     node_color=node_color,
    242     edge_color=edge_color,
    243     font_size=font_size,
    244     font_color=font_color,
    245     draw_networkx_kwargs=draw_networkx_kwargs,
    246 )
    247 nx.draw_networkx_edge_labels(
    248     graph,
    249     pos,
   (...)
    254     **draw_networkx_edge_labels_kwargs,
    255 )
    256 if filename is None:

File ~/checkouts/readthedocs.org/user_builds/unified-planning/envs/v1.0.0/lib/python3.10/site-packages/unified_planning/plot/utils.py:95, in draw_base_graph(graph, figsize, top_bottom, generate_node_label, arrowsize, node_size, node_color, edge_color, font_size, font_color, draw_networkx_kwargs)
     92 fig = plt.figure(figsize=figsize)
     93 ax = fig.add_subplot()
---> 95 pos = _generate_positions(graph, prog="dot", top_bottom=top_bottom)
     97 nx.draw_networkx(
     98     graph,
     99     pos,
   (...)
    110     **draw_networkx_kwargs,
    111 )
    112 return fig, ax, pos

File ~/checkouts/readthedocs.org/user_builds/unified-planning/envs/v1.0.0/lib/python3.10/site-packages/unified_planning/plot/utils.py:139, in _generate_positions(graph, prog, top_bottom)
    136 else:
    137     new_graph.graph.setdefault("graph", {})["rankdir"] = "LR"
--> 139 new_pos = nx.nx_agraph.graphviz_layout(new_graph, prog=prog)
    141 pos = {id_to_node[i]: value for i, value in new_pos.items()}
    143 return pos

File ~/checkouts/readthedocs.org/user_builds/unified-planning/envs/v1.0.0/lib/python3.10/site-packages/networkx/drawing/nx_agraph.py:251, in graphviz_layout(G, prog, root, args)
    220 def graphviz_layout(G, prog="neato", root=None, args=""):
    221     """Create node positions for G using Graphviz.
    222
    223     Parameters
   (...)
    249     see https://gitlab.com/graphviz/graphviz/-/issues/1767 for more info.
    250     """
--> 251     return pygraphviz_layout(G, prog=prog, root=root, args=args)

File ~/checkouts/readthedocs.org/user_builds/unified-planning/envs/v1.0.0/lib/python3.10/site-packages/networkx/drawing/nx_agraph.py:303, in pygraphviz_layout(G, prog, root, args)
    301     args += f"-Groot={root}"
    302 A = to_agraph(G)
--> 303 A.layout(prog=prog, args=args)
    304 node_pos = {}
    305 for n in G:

File ~/checkouts/readthedocs.org/user_builds/unified-planning/envs/v1.0.0/lib/python3.10/site-packages/pygraphviz/agraph.py:1483, in AGraph.layout(self, prog, args)
   1466 """Assign positions to nodes in graph.
   1467
   1468 Optional prog=['neato'|'dot'|'twopi'|'circo'|'fdp'|'nop']
   (...)
   1480
   1481 """
   1482 output_fmt = "dot"
-> 1483 data = self._run_prog(prog, " ".join([args, "-T", output_fmt]))
   1484 self.from_string(data)
   1485 self.has_layout = True

File ~/checkouts/readthedocs.org/user_builds/unified-planning/envs/v1.0.0/lib/python3.10/site-packages/pygraphviz/agraph.py:1377, in AGraph._run_prog(self, prog, args)
   1367 def _run_prog(self, prog="nop", args=""):
   1368     """Apply graphviz program to graph and return the result as a string.
   1369
   1370     >>> import pygraphviz as pgv
   (...)
   1375     Use keyword args to add additional arguments to graphviz programs.
   1376     """
-> 1377     runprog = r'"%s"' % self._get_prog(prog)
   1378     cmd = " ".join([runprog, args])
   1379     dotargs = shlex.split(cmd)

File ~/checkouts/readthedocs.org/user_builds/unified-planning/envs/v1.0.0/lib/python3.10/site-packages/pygraphviz/agraph.py:1363, in AGraph._get_prog(self, prog)
   1361     runprog = self._which(prog)
   1362 except:
-> 1363     raise ValueError(f"Program {prog} not found in path.")
   1365 return runprog

ValueError: Program dot not found in path.
../_images/notebooks_11-plot_26_1.png