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