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
8 changes: 7 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Migrating

Three changes in this release alter results rather than the API. Code that compensated for the old behaviour needs the compensation removed.
One library factory is removed, and three changes alter results. Code that compensated for the old behaviour needs the compensation removed.

* **The bundled ABB IRB120 cell was removed.** `RobotCellLibrary.abb_irb120_3_58()` and its model assets no longer exist. Load an IRB120 from an external URDF/SRDF package if an application still needs it; the local library now concentrates on models with matching analytical solvers.
* **Tools from `ToolLibrary` mount along +Z.** If you attached one with an `attachment_frame` that rotated it to bridge the old +X convention — `state.set_tool_attached_to_group(tool_id, group, attachment_frame=Frame([0, 0, 0], [0, 0, 1], [1, 0, 0]))`, which is what the library's own cells used to do — drop that argument. The tool now attaches with an identity frame, and a leftover rotation rotates it twice. This is the one break that fails silently: nothing errors, the tool simply hangs off the flange sideways. Tools you authored yourself are untouched; if one was modelled along +X to match the old library convention, either re-express it with the new `base_frame` argument of `ToolModel` (`base_frame=Frame([0, 0, 0], [0, 1, 0], [0, 0, 1])`, the plane whose z+ runs along the tool's x+) or keep attaching it with the rotation you already use.
* **`TargetMode.TOOL` targets against library tools moved 90 degrees.** The TCF's Z axis now runs along the tool's working direction, where its X axis used to. A target frame that was correct before now points the tool sideways. Re-author such targets so their Z axis points where the tool should point — which is what the mode meant all along.
* **Analytical kinematics for UR robots returns different numbers.** Forward kinematics now reports the orientation of the `tool0` link (matching the robot model and PyBullet) instead of a version rotated by the never-applied `flange_frame`, and inverse kinematics interprets targets in that same corrected frame. Positions are unchanged; orientations are not. Frames and configurations recorded from the old solver — golden values in tests, targets tuned by trial and error, cached trajectories — have to be regenerated. Targets that were correct in `TargetMode.ROBOT` under PyBullet or MoveIt now work unchanged with the analytical planner too, which was the point.
Expand All @@ -20,15 +21,20 @@ Requires `compas_robots >= 1.1`.
### Added

* The `Tool From Mesh` Grasshopper component gained a `base_plane` input: where the robot's flange takes hold of the geometry, expressed in the coordinates the mesh was modelled in. Its Z axis points away from the robot, so a tool drawn reaching along world Z needs none, and a tool drawn along another axis is mounted by wiring a plane instead of redrawing the geometry. Backed by the new `base_frame` argument of `compas_robots.ToolModel`; nothing is baked into the mesh, so the plane can be re-wired at any time. The component also surfaces a remark when the TCP does not sit roughly on the tool's +Z, since that means the tool will point sideways once attached — the direction from the mount to the TCP is only a hint (it says nothing about roll), so it is reported rather than applied.
* `RobotCellLibrary` now includes UR3, UR3e, UR5e, UR10, UR16e, and Stäubli TX2-60L robot-only cells, completing the locally bundled models that have analytical kinematics solvers.
* Added `UR16eKinematics` and the correctly named `Staubli_TX2_60LKinematics` analytical solver classes. The previous Stäubli class and solver key remain as compatibility aliases.

### Changed

* Refreshed the bundled UR5 and UR10e descriptions and meshes from the official Universal Robots ROS 2 description package. All seven supported UR cells now share one `ur_description` mesh package instead of duplicating model assets per cell.
* Bundled UR and Panda visual meshes now use their original Collada files instead of derived OBJ copies, preserving source materials and eliminating redundant converted assets. The ROS package extraction script likewise keeps downloaded DAE files unchanged.
* The tools in `ToolLibrary` now mount along the +Z axis of their base frame instead of +X. Every planning group in `RobotCellLibrary` ends at a link whose +Z points away from the arm (`tool0` for the industrial robots, `panda_hand_tcp` for the Panda), so with this the same tool attaches to any of them with an identity attachment frame — previously each cell carried a rotation to bridge the two conventions, and a tool authored for one robot did not necessarily fit another. Their TCF states the tool's working direction with its own Z axis too, so a `TargetMode.TOOL` target aligns the tool along the target's Z — previously the TCF's X axis ran along the tool, which put every tool-mode target 90 degrees out. The tools are still modelled along +X internally and re-framed on the way out via `ToolModel.reframe_base`. The beams held by the gripper cells are authored in TCF coordinates and were re-authored to match, so they stay put. Poses are unchanged: the attached tools and workpieces of every cell land exactly where they did, only the tool's base frame is now the end effector link's frame rather than a rotated version of it. Requires the `reframe_base` support of `compas_robots >= 1.1`.
* `MoveItInverseKinematics` now sends the target pose with an explicit `ik_link_name` (the planning group's end effector link, overridable via the new `"link"` option) instead of relying on MoveIt's default tip link for the group. The two are the same link for the robots in this library, but nothing guarantees that for a planning group configured with a different tip, in which case MoveIt would interpret the target pose at the wrong frame. `PositionIKRequest` gained the corresponding `ik_link_name` field.
* `AnalyticalKinematicsPlanner` now honours the `flange_frame` of its kinematics solver, and the UR solvers (`OffsetWristKinematics` and its subclasses) declare one. Their DH chain ends at the robot's flange, whose x+ points away from the last link, while the planning group ends at `tool0`, whose z+ does — a fixed rotation apart. `flange_frame` was documented as the way to state that offset but was never applied, so the solver and the URDF model disagreed on the orientation of the end effector: every target was interpreted rotated and the robot never reached the frame it was asked for. Analytical forward kinematics now agrees with the robot model (and with PyBullet) to 1e-10, and a `TargetMode.TOOL` target is reached exactly.

### Removed

* Removed `RobotCellLibrary.abb_irb120_3_58()` and its bundled model assets. The model had no matching analytical solver and was not consistent with the analytical-cell focus of the local library.

## [2.0.1] 2026-06-18

Expand Down
2 changes: 1 addition & 1 deletion docs/concepts/02_robot_model/files/02_robot_from_disk.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@

# Also load geometry
support_package_name = ""
loader = LocalPackageMeshLoader(compas_fab.get("robot_library/ur10e_robot"), support_package_name)
loader = LocalPackageMeshLoader(compas_fab.get("robot_library"), support_package_name)
model.load_geometry(loader, precision=12)

print(model)
3 changes: 2 additions & 1 deletion docs/concepts/02_robot_model/files/02_robot_from_library.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
from compas_fab.robots import RobotCellLibrary

# Load robot from RobotCellLibrary
# RobotCellLibrary also contains .ur5(), .ur10e(), abb_irb120_3_58(), abb_irb4600_40_255(), .rfl(), .panda()
# RobotCellLibrary also contains .ur3(), .ur3e(), .ur5e(), .ur10(), .ur10e(),
# .ur16e(), .staubli_tx2_60l(), .abb_irb4600_40_255(), .rfl(), and .panda().
robot_cell, robot_cell_state = RobotCellLibrary.ur5()
robot_model = robot_cell.robot_model
robot_semantics = robot_cell.robot_semantics
Expand Down
3 changes: 2 additions & 1 deletion docs/concepts/02_robot_model/files/05_robot_model_chains.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@

from compas_fab.robots import RobotCellLibrary

# RobotCellLibrary also contains .ur5(), .ur10e(), abb_irb120_3_58(), abb_irb4600_40_255(), .rfl(), .panda()
# RobotCellLibrary also contains .ur3(), .ur3e(), .ur5(), .ur5e(), .ur10(),
# .ur10e(), .ur16e(), .staubli_tx2_60l(), .abb_irb4600_40_255(), .rfl(), and .panda().
robot_cell, robot_cell_state = RobotCellLibrary.panda(load_geometry=False)

model = robot_cell.robot_model
Expand Down
3 changes: 2 additions & 1 deletion docs/concepts/02_robot_model/files/05_robot_viewer.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,8 @@
viewer.ui.sidedock.show = True

# Load robot from RobotCellLibrary
# RobotCellLibrary also contains .ur5(), .ur10e(), abb_irb120_3_58(), abb_irb4600_40_255(), .rfl(), .panda()
# RobotCellLibrary also contains .ur3(), .ur3e(), .ur5e(), .ur10(), .ur10e(),
# .ur16e(), .staubli_tx2_60l(), .abb_irb4600_40_255(), .rfl(), and .panda().
robot_cell, robot_cell_state = RobotCellLibrary.ur5()
model = robot_cell.robot_model

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ optional-dependencies = { dev = { file = "requirements-dev.txt" } }
where = ["src"]

[tool.setuptools.package-data]
"*" = ["*.json", "*.yaml", "*.yml", "*.urdf", "*.srdf", "*.stl", "*.dae", "*.obj", "*.ghuser"]
"*" = ["*.json", "*.yaml", "*.yml", "*.urdf", "*.srdf", "*.stl", "*.dae", "*.obj", "*.mtl", "*.ghuser", "*.md", "LICENSE"]

[project.urls]
Homepage = "https://github.com/compas-dev/compas_fab"
Expand Down
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
compas >= 2.3, < 3
compas_robots @ git+https://github.com/compas-dev/compas_robots.git@tool-base-frame
compas_robots @ git+https://github.com/compas-dev/compas_robots.git@dae_import_fixes
roslibpy >=2
36 changes: 6 additions & 30 deletions scripts/extract_robot_package_from_ros.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,26 +20,24 @@
# The extracted SRDF is located in "\robot_packages\{robot_name}\robot_description_semantic.srdf"
# The extracted meshes are stored in paths relative to the package root and are defined in the URDF file.

import itertools
import os

from compas.datastructures import Mesh

from compas_fab.backends.ros.client import RosFileServerLoader
from compas_fab.backends import RosClient
from compas_fab.backends.ros.client import RosFileServerLoader

HERE = os.path.dirname(__file__)


with RosClient() as client:
# Standard way to load a robot from a MoveIt! instance.
robot_cell = client.load_robot_cell()
robot_model = robot_cell.robot_model
robot_cell.print_info()
print(robot_cell.robot_model.name)
print(robot_model.name)

# The RosFileServerLoader is used with a modified local_cache_directory argument
local_cache_directory = os.path.join(HERE, "robot_packages")
print("Saved to directory:", os.path.join(local_cache_directory, robot.name))
print("Saved to directory:", os.path.join(local_cache_directory, robot_model.name))
loader = RosFileServerLoader(client, True, local_cache_directory)

# The loader will retrieve URDF and SRDF from the ros server
Expand All @@ -48,30 +46,8 @@

# The meshes will be loaded from ROS to the local cache directory
mesh_precision = 12
robot.model.load_geometry(loader, precision=mesh_precision)

# Convert DAE meshes files to OBJ and update the URDF file
# The new OBJ files will be stored next to the original DAE files appended with ".obj" extension
for link in robot.model.links:
for element in itertools.chain(link.collision, link.visual):
shape = element.geometry.shape
if "filename" in dir(shape):
# Convert dae to stl
if shape.filename.endswith(".dae"):
filename_in_package = shape.filename.split("package://")[1]
local_path_to_stl = os.path.join(local_cache_directory, robot.name, filename_in_package + ".obj")
# Check if the STL file already exists (e.g. RFL have multiple links with the same mesh file)
if not os.path.exists(local_path_to_stl):
meshes = loader.load_meshes(shape.filename, precision=mesh_precision)
# Join all meshes into one for OBJ conversion (Note: Do not weld the vertices)
combined_mesh = Mesh()
for mesh in meshes:
combined_mesh.join(mesh, precision=mesh_precision)
combined_mesh.to_obj(local_path_to_stl, precision=mesh_precision)
print("> DAE Converted to OBJ file and stored in : ", local_path_to_stl)
# Change the filename in the URDF
shape.filename = shape.filename + ".obj"
robot_model.load_geometry(loader, precision=mesh_precision)

robot.model.to_urdf_file(loader._urdf_filename, prettify=True)
robot_model.to_urdf_file(loader._urdf_filename, prettify=True)

print("Extraction Complete.")
15 changes: 14 additions & 1 deletion scripts/generate_robot_cell_gltf_snapshots.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@
from compas.geometry import distance_point_point
from compas_robots import Configuration
from compas_robots import ToolModel
from compas_robots.model import Joint
from compas_robots.model import LinkGeometry

import compas_fab
Expand All @@ -71,8 +72,14 @@
from compas_fab.backends import PyBulletClient
from compas_fab.backends import PyBulletPlanner
from compas_fab.backends import RosClient
from compas_fab.backends import Staubli_TX2_60LKinematics
from compas_fab.backends import UR3eKinematics
from compas_fab.backends import UR3Kinematics
from compas_fab.backends import UR5eKinematics
from compas_fab.backends import UR5Kinematics
from compas_fab.backends import UR10eKinematics
from compas_fab.backends import UR10Kinematics
from compas_fab.backends import UR16eKinematics
from compas_fab.robots import FrameTarget
from compas_fab.robots import RobotCell
from compas_fab.robots import RobotCellLibrary
Expand Down Expand Up @@ -105,8 +112,14 @@ class LocalCellSpec:


LOCAL_CELL_SPECS = {
"ur3": LocalCellSpec("generated_cone", UR3Kinematics),
"ur3e": LocalCellSpec("generated_cone", UR3eKinematics),
"ur5": LocalCellSpec("generated_cone", UR5Kinematics),
"ur5e": LocalCellSpec("generated_cone", UR5eKinematics),
"ur10": LocalCellSpec("generated_cone", UR10Kinematics),
"ur10e": LocalCellSpec("generated_cone", UR10eKinematics),
"ur16e": LocalCellSpec("generated_cone", UR16eKinematics),
"staubli_tx2_60l": LocalCellSpec("generated_cone", Staubli_TX2_60LKinematics),
"ur5_cone_tool": LocalCellSpec("built_in_tool", UR5Kinematics),
"ur5_gripper_one_beam": LocalCellSpec("built_in_tool", UR5Kinematics),
"ur10e_gripper_one_beam": LocalCellSpec("built_in_tool", UR10eKinematics),
Expand Down Expand Up @@ -170,7 +183,7 @@ def reachable_target_frame(robot_cell: RobotCell, state: RobotCellState, target_

for index, joint in enumerate(joints):
value = float(reference_configuration[joint.name]) + REFERENCE_DELTAS[index % len(REFERENCE_DELTAS)]
if joint.limit:
if joint.limit and joint.type != Joint.CONTINUOUS:
span = float(joint.limit.upper) - float(joint.limit.lower)
margin = min(0.05, max(1e-6, span * 0.01))
value = max(float(joint.limit.lower) + margin, min(float(joint.limit.upper) - margin, value))
Expand Down
4 changes: 4 additions & 0 deletions src/compas_fab/backends/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,8 @@
UR5eKinematics,
UR10Kinematics,
UR10eKinematics,
UR16eKinematics,
Staubli_TX2_60LKinematics,
Staubli_TX260LKinematics,
ABB_IRB4600_40_255Kinematics,
)
Expand Down Expand Up @@ -127,6 +129,8 @@
"UR5eKinematics",
"UR10Kinematics",
"UR10eKinematics",
"UR16eKinematics",
"Staubli_TX2_60LKinematics",
"Staubli_TX260LKinematics",
"ABB_IRB4600_40_255Kinematics",
# PyBullet
Expand Down
4 changes: 4 additions & 0 deletions src/compas_fab/backends/kinematics/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
UR5eKinematics,
UR10Kinematics,
UR10eKinematics,
UR16eKinematics,
Staubli_TX2_60LKinematics,
Staubli_TX260LKinematics,
ABB_IRB4600_40_255Kinematics,
)
Expand All @@ -42,6 +44,8 @@
"UR5eKinematics",
"UR10Kinematics",
"UR10eKinematics",
"UR16eKinematics",
"Staubli_TX2_60LKinematics",
"Staubli_TX260LKinematics",
"ABB_IRB4600_40_255Kinematics",
]
2 changes: 2 additions & 0 deletions src/compas_fab/backends/kinematics/solvers/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
"ur5e": UR5eKinematics, # noqa: F405
"ur10": UR10Kinematics, # noqa: F405
"ur10e": UR10eKinematics, # noqa: F405
"ur16e": UR16eKinematics, # noqa: F405
"staubli_tx2_60l": Staubli_TX2_60LKinematics, # noqa: F405
"staubli_tx260l": Staubli_TX260LKinematics, # noqa: F405
"abb_irb4600_40_255": ABB_IRB4600_40_255Kinematics, # noqa: F405
}
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,15 @@
"d6": 0.0921,
}

UR16e_PARAMS = {
"d1": 0.1807,
"a2": -0.4784,
"a3": -0.36,
"d4": 0.17415,
"d5": 0.11985,
"d6": 0.11655,
}


class OffsetWristKinematics(AnalyticalKinematics):
""" """
Expand Down Expand Up @@ -117,7 +126,7 @@ def __init__(self):


class UR10Kinematics(OffsetWristKinematics):
"""Analytical IK solver for UR10 e-Series robots."""
"""Analytical IK solver for UR10 robots."""

def __init__(self):
params = [UR10_PARAMS[k] for k in ["d1", "a2", "a3", "d4", "d5", "d6"]]
Expand All @@ -130,3 +139,11 @@ class UR10eKinematics(OffsetWristKinematics):
def __init__(self):
params = [UR10e_PARAMS[k] for k in ["d1", "a2", "a3", "d4", "d5", "d6"]]
super(UR10eKinematics, self).__init__(params)


class UR16eKinematics(OffsetWristKinematics):
"""Analytical IK solver for UR16e robots."""

def __init__(self):
params = [UR16e_PARAMS[k] for k in ["d1", "a2", "a3", "d4", "d5", "d6"]]
super(UR16eKinematics, self).__init__(params)
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def _pre_process(self, joint_values: list[float]) -> list[float]:
return joint_values


class Staubli_TX260LKinematics(SphericalWristKinematics):
class Staubli_TX2_60LKinematics(SphericalWristKinematics):
"""Analytical IK solver for the Stäubli TX2 60L robot."""

def __init__(self):
Expand All @@ -42,7 +42,7 @@ def __init__(self):
Point(0.450, 0.020, 0.775),
Point(0.520, 0.020, 0.775),
]
super(Staubli_TX260LKinematics, self).__init__(points)
super(Staubli_TX2_60LKinematics, self).__init__(points)

def _pre_process(self, joint_values: list[float]) -> list[float]:
q1, q2, q3, q4, q5, q6 = joint_values
Expand All @@ -61,6 +61,11 @@ def _post_process(self, solutions: list[list[float]]) -> list[list[float]]:
yield [q1, q2, q3, q4, q5, q6]


# Backwards-compatible alias for the misspelled public name used before the
# TX2-60L model was bundled in the robot-cell library.
Staubli_TX260LKinematics = Staubli_TX2_60LKinematics


class ABB_IRB4600_40_255Kinematics(SphericalWristKinematics):
"""Analytical IK solver for the ABB IRB4600 40/255 robot."""

Expand Down
18 changes: 9 additions & 9 deletions src/compas_fab/backends/kinematics/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from typing import Optional

from compas_robots import Configuration
from compas_robots.model import Joint

from compas_fab.robots import RobotCell

Expand All @@ -26,19 +27,18 @@ def fit_within_bounds(angle, lower, upper):


def try_to_fit_configurations_between_bounds(robot_cell: RobotCell, configurations: list[Configuration], group: Optional[str] = None) -> list[Configuration]:
j1, j2, j3, j4, j5, j6 = robot_cell.get_configurable_joints(group=group)
joints = robot_cell.get_configurable_joints(group=group)
for i, c in enumerate(configurations):
if c is None:
continue
a1, a2, a3, a4, a5, a6 = c.values()
try:
a1 = fit_within_bounds(a1, j1.limit.lower, j1.limit.upper)
a2 = fit_within_bounds(a2, j2.limit.lower, j2.limit.upper)
a3 = fit_within_bounds(a3, j3.limit.lower, j3.limit.upper)
a4 = fit_within_bounds(a4, j4.limit.lower, j4.limit.upper)
a5 = fit_within_bounds(a5, j5.limit.lower, j5.limit.upper)
a6 = fit_within_bounds(a6, j6.limit.lower, j6.limit.upper)
configurations[i].joint_values = [a1, a2, a3, a4, a5, a6]
fitted = []
for angle, joint in zip(c.values(), joints):
if joint.type == Joint.CONTINUOUS:
fitted.append(get_smaller_angle(angle))
else:
fitted.append(fit_within_bounds(angle, joint.limit.lower, joint.limit.upper))
configurations[i].joint_values = fitted
except AssertionError:
configurations[i] = None
return configurations
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Loading