Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/source/arm_dh.rst
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ standard or modified. They can be listed by:
.. runblock:: pycon

>>> import roboticstoolbox as rtb
>>> rtb.models.list(mtype="DH")
>>> rtb.models.catalog(mtype="DH")

.. automodule:: roboticstoolbox.models.DH
:members:
Expand Down
4 changes: 2 additions & 2 deletions docs/source/arm_erobot.rst
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ They can be listed by:
.. runblock:: pycon

>>> import roboticstoolbox as rtb
>>> rtb.models.list(mtype="ETS")
>>> rtb.models.catalog(mtype="ETS")

.. automodule:: roboticstoolbox.models.ETS
:members:
Expand All @@ -65,7 +65,7 @@ standard or modified. They can be listed by:
.. runblock:: pycon

>>> import roboticstoolbox as rtb
>>> rtb.models.list(mtype="URDF")
>>> rtb.models.catalog(mtype="URDF")

.. automodule:: roboticstoolbox.models.URDF
:members:
Expand Down
5 changes: 5 additions & 0 deletions src/roboticstoolbox/models/URDF/LBR.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,9 +32,14 @@ class LBR(URDFRobot):

def __init__(self):

# The bundled file does $(find kuka_lbr_iiwa_support) to locate its
# own macro file, but rtb-data ships that content under the
# directory name "kuka_lbr_iiwa" (no "_support" suffix), so xacro's
# package lookup can't find it without this alias.
super().__init__(
"kuka_description/kuka_lbr_iiwa/urdf/lbr_iiwa_14_r820.xacro",
manufacturer="Kuka",
extra_packages={"kuka_lbr_iiwa_support": "kuka_description/kuka_lbr_iiwa"},
)

# self.qdlim = np.array([
Expand Down
19 changes: 18 additions & 1 deletion src/roboticstoolbox/models/URDF/URDFRobot.py
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,7 @@ def URDF_file(
file: "str | Path | TextIO",
model: "str | None" = None,
patch: "Callable[[str], str] | None" = None,
extra_packages: "dict[str, str] | None" = None,
) -> tuple:
"""Parse a URDF or xacro file, return (elinks, name).

Expand All @@ -306,13 +307,26 @@ def URDF_file(
the upstream repo or the bundled `rtb-data` copy. See ``Valkyrie.py``
and ``Fetch.py`` for real examples — each documents exactly which
upstream bug its patch works around.

``extra_packages``, if given, maps additional xacro package names to
paths relative to the bundled ``rtb-data`` xacro root. Use it when an
upstream file does ``$(find some_package)`` for a package name that
doesn't match any directory name rtb-data actually ships under — xacro's
package lookup only auto-discovers directories that are already known by
name, it doesn't fall back to searching by content. See ``LBR.py`` for a
real example.
"""
import rtbdata

xacro_root = Path(rtbdata.__file__).parent / "xacro"
pkg_map = {d.name: str(d) for d in xacro_root.iterdir() if d.is_dir()}
packages.update_package_cache(pkg_map)

if extra_packages is not None:
packages.update_package_cache(
{name: str(xacro_root / path) for name, path in extra_packages.items()}
)

xacro_args = None
if isinstance(file, str):
file = Path(file)
Expand Down Expand Up @@ -376,9 +390,12 @@ def __init__(
manufacturer: str = "",
gripper_link_index: "int | None" = None,
patch: "Callable[[str], str] | None" = None,
extra_packages: "dict[str, str] | None" = None,
**kwargs,
):
elinks, name, filepath = URDF_file(urdf_path, patch=patch)
elinks, name, filepath = URDF_file(
urdf_path, patch=patch, extra_packages=extra_packages
)
if gripper_link_index is not None:
kwargs["gripper_links"] = elinks[gripper_link_index]
super().__init__(elinks, name=name, manufacturer=manufacturer, **kwargs)
Expand Down
4 changes: 2 additions & 2 deletions src/roboticstoolbox/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
from roboticstoolbox.models.URDF import * # noqa
from roboticstoolbox.models import ETS # noqa
from roboticstoolbox.models import DH # noqa
from roboticstoolbox.models.list import list
from roboticstoolbox.models.catalog import catalog, list


__all__ = ["list", "ETS", "DH"]
__all__ = ["catalog", "list", "ETS", "DH"]
226 changes: 226 additions & 0 deletions src/roboticstoolbox/models/catalog.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,226 @@
import warnings
from roboticstoolbox.robot.Robot import Robot
from roboticstoolbox.tools import rtb_get_param
from roboticstoolbox.robot.ERobot import ERobot2
from roboticstoolbox.models.URDF.URDFRobot import _rd_link
from ansitable import ANSITable, Column
import inspect

# import importlib


def catalog(keywords=None, dof=None, mtype=None, sorton=None, border="thin"):
"""
Display all robot models in summary form

:param keywords: keywords to filter on, defaults to None
:type keywords: tuple of str, optional
:param dof: number of DoF to filter on, defaults to None
:type dof: int, optional
:param mtype: filter on model type: "DH", "ETS", "URDF", defaults to all types
:type mtype: str, optional
:param sorton: column to sort rows on: "name", "manufacturer" or "dof",
case insensitive, defaults to None (construction order)
:type sorton: str, optional

- ``catalog()`` displays a list of all models provided by the Toolbox.
It lists the name, manufacturer, model type, number of DoF, and
keywords. It also lists models available from
`robot_descriptions <https://github.com/robot-descriptions/robot_descriptions.py>`_
but not otherwise wrapped by the Toolbox: "robot_descriptions name"
is the string ``robot_descriptions`` itself calls the *name* (its own
``python -m robot_descriptions pull NAME`` CLI) -- pass it to
:class:`~roboticstoolbox.models.URDF.URDFRobot.URDFRobot` to load that
model, e.g. ``URDFRobot("ur5")``.

- ``catalog(mtype=MT)`` as above, but only displays Toolbox models of
type ``MT`` where ``MT`` is one of "DH", "ETS" or "URDF". The
robot_descriptions listing (all URDF) is shown for ``MT="URDF"`` or
unfiltered, and omitted for ``MT="DH"``/``MT="ETS"``.

- ``catalog(keywords=KW)`` as above, but only displays models that have
a keyword in the tuple ``KW``.

- ``catalog(dof=N)`` as above, but only display models that have ``N``
degrees of freedom.

- ``catalog(sorton=S)`` as above, but rows are sorted by column ``S``.
``name`` sorts by the robot's name, ``manufacturer`` sorts by the robot's
manufacturer, and ``dof`` sorts by the robot's number of degrees of freedom. Sorting is case insensitive.
Applies to both tables when both are shown. Rows with no ``DoF``
value (common in the robot_descriptions table) sort first.

The filters can be combined

- ``catalog(keywords=KW, dof=N)`` are those models that have a keyword
in ``KW`` and have ``N`` degrees of freedom.
"""

import roboticstoolbox.models as models

# module = importlib.import_module(
# '.' + os.path.splitext(file)[0], package='bdsim.blocks')

unicode = rtb_get_param("unicode")
if not unicode:
border = "ascii"

# sorton maps to (column-in-main-table, column-in-rd-table, key). The two
# tables spell the "robot's own name" column differently -- "name" here,
# "robot name" in the robot_descriptions table, which also has a second,
# distinct "robot_descriptions name" column (see make_rd_table) -- so
# sorton="name" resolves to a different literal header per table.
sort_col_main = None
sort_col_rd = None
sort_key = None
if sorton is not None:
sort_map = {
"name": ("robot name", "robot name", None),
"manufacturer": ("manufacturer", "manufacturer", None),
"dof": ("DoF", "DoF", lambda s: int(s) if s.strip() else -1),
}
try:
sort_col_main, sort_col_rd, sort_key = sort_map[sorton.lower()]
except KeyError:
raise ValueError(
f"sorton must be one of {sorted(sort_map)}, got {sorton!r}"
)

def make_table(border=None):
table = ANSITable(
Column("class", headalign="^", colalign="<"),
Column("robot name", headalign="^", colalign="<"),
Column("manufacturer", headalign="^", colalign="<"),
Column("model type", headalign="^", colalign="<"),
Column("DoF", colalign="<"),
Column("dims", colalign="<"),
Column("structure", colalign="<", width=16),
Column("dynamics", colalign="<"),
Column("geometry", colalign="<"),
Column("keywords", headalign="^", colalign="<"),
border=border,
)

if mtype is not None:
categories = [mtype]
else:
categories = ["DH", "URDF", "ETS"]
for category in categories:
# get all classes in this category
group = models.__dict__[category]
for cls in group.__dict__.values():
if inspect.isclass(cls) and issubclass(cls, Robot):
# we found a BaseRobot subclass, instantiate it
try:
robot = cls()
except Exception:
print(f"failed to load {cls}")
continue
try:
structure = robot.structure
except Exception: # pragma nocover
structure = ""

# apply filters
if keywords is not None:
if len(set(keywords) & set(robot.keywords)) == 0:
continue
if dof is not None and robot.n != dof:
continue # pragma nocover

dims = 0

if isinstance(robot, ERobot2):
dims = 2
else:
dims = 3
# add the row
table.row(
cls.__name__,
robot.name,
robot.manufacturer,
category,
robot.n,
f"{dims}d",
structure,
"Y" if robot._hasdynamics else "",
"Y" if robot._hasgeometry else "",
", ".join(robot.keywords),
)

if sort_col_main is not None:
table.sort(sort_col_main, key=sort_key)
table.print()

def make_rd_table(border=None):
from robot_descriptions import DESCRIPTIONS
from robot_descriptions._descriptions import Format

table = ANSITable(
Column("robot_descriptions name", headalign="^", colalign="<"),
Column("robot name", headalign="^", colalign="<"),
Column("manufacturer", headalign="^", colalign="<"),
Column("model type", headalign="^", colalign="<"),
Column("DoF", colalign="<"),
Column("keywords", headalign="^", colalign="<"),
border=border,
)

for key, description in sorted(DESCRIPTIONS.items()):
if Format.URDF not in description.formats:
continue

rd_name = key.removesuffix("_description").removesuffix("_official")
tags = sorted(description.tags)

if keywords is not None:
if len(set(keywords) & set(tags)) == 0:
continue
if dof is not None and description.dof != dof:
continue

table.row(
rd_name,
description.robot,
description.maker,
"URDF",
description.dof if description.dof is not None else "",
", ".join(tags),
)

print(f"\nImportable from {_rd_link()}:\n")
if sort_col_rd is not None:
table.sort(sort_col_rd, key=sort_key)
table.print()

make_table(border=border)
if mtype is None or mtype == "URDF":
make_rd_table(border=border)


def list( # pragma nocover
keywords=None, dof=None, mtype=None, sorton=None, border="thin"
):
"""
Display all robot models in summary form (deprecated)

:deprecated: 1.4.0
``list`` shadows the builtin and will be removed in a future
release. Use :func:`catalog` instead.
"""
warnings.warn(
"models.list() is deprecated and will be removed in a future "
"release, use models.catalog() instead",
FutureWarning,
stacklevel=2,
)
return catalog(
keywords=keywords, dof=dof, mtype=mtype, sorton=sorton, border=border
)


if __name__ == "__main__": # pragma nocover
catalog(border="ascii")
catalog(keywords=("dynamics",), border="thin")
catalog(dof=6)
catalog(keywords=("dynamics",), dof=6)
Loading