Skip to content

Commit db6e9b1

Browse files
committed
move dotfile and showgraph into baserobot
1 parent 1c4bdf3 commit db6e9b1

File tree

2 files changed

+489
-244
lines changed

2 files changed

+489
-244
lines changed

roboticstoolbox/robot/BaseRobot.py

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
from copy import deepcopy
1111
from functools import lru_cache
1212
from typing import (
13+
IO,
1314
TYPE_CHECKING,
1415
Any,
1516
Callable,
@@ -49,6 +50,7 @@
4950
from roboticstoolbox.backends.PyPlot import PyPlot, PyPlot2
5051
from roboticstoolbox.backends.PyPlot.EllipsePlot import EllipsePlot
5152

53+
5254
if TYPE_CHECKING:
5355
from matplotlib.cm import Color # pragma nocover
5456
else:
@@ -2838,3 +2840,247 @@ def teach(
28382840
return env
28392841

28402842
# --------------------------------------------------------------------- #
2843+
2844+
# --------------------------------------------------------------------- #
2845+
# --------- Utility Methods ------------------------------------------- #
2846+
# --------------------------------------------------------------------- #
2847+
2848+
def showgraph(self, display_graph: bool = True, **kwargs) -> Union[None, str]:
2849+
"""
2850+
Display a link transform graph in browser
2851+
2852+
``robot.showgraph()`` displays a graph of the robot's link frames
2853+
and the ETS between them. It uses GraphViz dot.
2854+
2855+
The nodes are:
2856+
- Base is shown as a grey square. This is the world frame origin,
2857+
but can be changed using the ``base`` attribute of the robot.
2858+
- Link frames are indicated by circles
2859+
- ETS transforms are indicated by rounded boxes
2860+
2861+
The edges are:
2862+
- an arrow if `jtype` is False or the joint is fixed
2863+
- an arrow with a round head if `jtype` is True and the joint is
2864+
revolute
2865+
- an arrow with a box head if `jtype` is True and the joint is
2866+
prismatic
2867+
2868+
Edge labels or nodes in blue have a fixed transformation to the
2869+
preceding link.
2870+
2871+
Parameters
2872+
----------
2873+
display_graph
2874+
Open the graph in a browser if True. Otherwise will return the
2875+
file path
2876+
etsbox
2877+
Put the link ETS in a box, otherwise an edge label
2878+
jtype
2879+
Arrowhead to node indicates revolute or prismatic type
2880+
static
2881+
Show static joints in blue and bold
2882+
2883+
Examples
2884+
--------
2885+
>>> import roboticstoolbox as rtb
2886+
>>> panda = rtb.models.URDF.Panda()
2887+
>>> panda.showgraph()
2888+
2889+
.. image:: ../figs/panda-graph.svg
2890+
:width: 600
2891+
2892+
See Also
2893+
--------
2894+
:func:`dotfile`
2895+
2896+
"""
2897+
2898+
# Lazy import
2899+
import tempfile
2900+
import subprocess
2901+
import webbrowser
2902+
2903+
# create the temporary dotfile
2904+
dotfile = tempfile.TemporaryFile(mode="w")
2905+
self.dotfile(dotfile, **kwargs)
2906+
2907+
# rewind the dot file, create PDF file in the filesystem, run dot
2908+
dotfile.seek(0)
2909+
pdffile = tempfile.NamedTemporaryFile(suffix=".pdf", delete=False)
2910+
subprocess.run("dot -Tpdf", shell=True, stdin=dotfile, stdout=pdffile)
2911+
2912+
# open the PDF file in browser (hopefully portable), then cleanup
2913+
if display_graph: # pragma nocover
2914+
webbrowser.open(f"file://{pdffile.name}")
2915+
else:
2916+
return pdffile.name
2917+
2918+
def dotfile(
2919+
self,
2920+
filename: Union[str, IO[str]],
2921+
etsbox: bool = False,
2922+
ets: L["full", "brief"] = "full",
2923+
jtype: bool = False,
2924+
static: bool = True,
2925+
):
2926+
"""
2927+
Write a link transform graph as a GraphViz dot file
2928+
2929+
The file can be processed using dot:
2930+
% dot -Tpng -o out.png dotfile.dot
2931+
2932+
The nodes are:
2933+
- Base is shown as a grey square. This is the world frame origin,
2934+
but can be changed using the ``base`` attribute of the robot.
2935+
- Link frames are indicated by circles
2936+
- ETS transforms are indicated by rounded boxes
2937+
2938+
The edges are:
2939+
- an arrow if `jtype` is False or the joint is fixed
2940+
- an arrow with a round head if `jtype` is True and the joint is
2941+
revolute
2942+
- an arrow with a box head if `jtype` is True and the joint is
2943+
prismatic
2944+
2945+
Edge labels or nodes in blue have a fixed transformation to the
2946+
preceding link.
2947+
2948+
Note
2949+
----
2950+
If ``filename`` is a file object then the file will *not*
2951+
be closed after the GraphViz model is written.
2952+
2953+
Parameters
2954+
----------
2955+
file
2956+
Name of file to write to
2957+
etsbox
2958+
Put the link ETS in a box, otherwise an edge label
2959+
ets
2960+
Display the full ets with "full" or a brief version with "brief"
2961+
jtype
2962+
Arrowhead to node indicates revolute or prismatic type
2963+
static
2964+
Show static joints in blue and bold
2965+
2966+
See Also
2967+
--------
2968+
:func:`showgraph`
2969+
2970+
"""
2971+
2972+
if isinstance(filename, str):
2973+
file = open(filename, "w")
2974+
else:
2975+
file = filename
2976+
2977+
header = r"""digraph G {
2978+
graph [rankdir=LR];
2979+
"""
2980+
2981+
def draw_edge(link, etsbox, jtype, static):
2982+
# draw the edge
2983+
if jtype:
2984+
if link.isprismatic:
2985+
edge_options = 'arrowhead="box", arrowtail="inv", dir="both"'
2986+
elif link.isrevolute:
2987+
edge_options = 'arrowhead="dot", arrowtail="inv", dir="both"'
2988+
else:
2989+
edge_options = 'arrowhead="normal"'
2990+
else:
2991+
edge_options = 'arrowhead="normal"'
2992+
2993+
if link.parent is None:
2994+
parent = "BASE"
2995+
else:
2996+
parent = link.parent.name
2997+
2998+
if etsbox:
2999+
# put the ets fragment in a box
3000+
if not link.isjoint and static:
3001+
node_options = ', fontcolor="blue"'
3002+
else:
3003+
node_options = ""
3004+
3005+
try:
3006+
file.write(
3007+
' {}_ets [shape=box, style=rounded, label="{}"{}];\n'.format(
3008+
link.name,
3009+
link.ets.__str__(q=f"q{link.jindex}"),
3010+
node_options,
3011+
)
3012+
)
3013+
except UnicodeEncodeError: # pragma nocover
3014+
file.write(
3015+
' {}_ets [shape=box, style=rounded, label="{}"{}];\n'.format(
3016+
link.name,
3017+
link.ets.__str__(q=f"q{link.jindex}")
3018+
.encode("ascii", "ignore")
3019+
.decode("ascii"),
3020+
node_options,
3021+
)
3022+
)
3023+
3024+
file.write(" {} -> {}_ets;\n".format(parent, link.name))
3025+
file.write(
3026+
" {}_ets -> {} [{}];\n".format(link.name, link.name, edge_options)
3027+
)
3028+
else:
3029+
# put the ets fragment as an edge label
3030+
if not link.isjoint and static:
3031+
edge_options += 'fontcolor="blue"'
3032+
if ets == "full":
3033+
estr = link.ets.__str__(q=f"q{link.jindex}")
3034+
elif ets == "brief":
3035+
if link.jindex is None:
3036+
estr = ""
3037+
else:
3038+
estr = f"...q{link.jindex}"
3039+
else:
3040+
return
3041+
try:
3042+
file.write(
3043+
' {} -> {} [label="{}", {}];\n'.format(
3044+
parent,
3045+
link.name,
3046+
estr,
3047+
edge_options,
3048+
)
3049+
)
3050+
except UnicodeEncodeError: # pragma nocover
3051+
file.write(
3052+
' {} -> {} [label="{}", {}];\n'.format(
3053+
parent,
3054+
link.name,
3055+
estr.encode("ascii", "ignore").decode("ascii"),
3056+
edge_options,
3057+
)
3058+
)
3059+
3060+
file.write(header)
3061+
3062+
# add the base link
3063+
file.write(" BASE [shape=square, style=filled, fillcolor=gray]\n")
3064+
3065+
# add the links
3066+
for link in self:
3067+
# draw the link frame node (circle) or ee node (doublecircle)
3068+
if link in self.ee_links:
3069+
# end-effector
3070+
node_options = 'shape="doublecircle", color="blue", fontcolor="blue"'
3071+
else:
3072+
node_options = 'shape="circle"'
3073+
3074+
file.write(" {} [{}];\n".format(link.name, node_options))
3075+
3076+
draw_edge(link, etsbox, jtype, static)
3077+
3078+
for gripper in self.grippers:
3079+
for link in gripper.links:
3080+
file.write(" {} [shape=cds];\n".format(link.name))
3081+
draw_edge(link, etsbox, jtype, static)
3082+
3083+
file.write("}\n")
3084+
3085+
if isinstance(filename, str):
3086+
file.close() # noqa

0 commit comments

Comments
 (0)