diff --git a/examples/02_plan_and_execute.py b/examples/02_plan_and_execute.py index 0fad11f3..998d8d34 100644 --- a/examples/02_plan_and_execute.py +++ b/examples/02_plan_and_execute.py @@ -1,6 +1,6 @@ import asyncio -from nova import Nova +from nova import Nova, MotionSettings from nova.actions import jnt, ptp from nova.types import Pose @@ -40,6 +40,10 @@ async def main(): jnt(home_joints), ] + # you can update the settings of the action + for action in actions: + action.settings = MotionSettings(tcp_velocity_limit=200) + joint_trajectory = await motion_group.plan(actions, tcp) await motion_group.execute(joint_trajectory, tcp, actions=actions) diff --git a/examples/03_move_and_set_ios.py b/examples/03_move_and_set_ios.py index 573dffd9..f98734a9 100644 --- a/examples/03_move_and_set_ios.py +++ b/examples/03_move_and_set_ios.py @@ -32,7 +32,7 @@ async def main(): target_pose = current_pose @ Pose((100, 0, 0, 0, 0, 0)) actions = [ jnt(home_joints), - WriteAction(device_id="ur", key="digital_out[0]", value=False), + WriteAction(key="digital_out[0]", value=False), ptp(target_pose), jnt(home_joints), ] diff --git a/nova/__init__.py b/nova/__init__.py index 64275a69..a2c79655 100644 --- a/nova/__init__.py +++ b/nova/__init__.py @@ -2,6 +2,7 @@ from nova.core.controller import Controller from nova.core.motion_group import MotionGroup from nova.core.movement_controller import speed_up as speed_up_movement_controller +from nova.motion_settings import MotionSettings from nova.core.nova import Cell, Nova __all__ = [ @@ -13,4 +14,5 @@ "api", "types", "actions", + "MotionSettings", ] diff --git a/nova/actions.py b/nova/actions.py index 27927aee..aea57955 100644 --- a/nova/actions.py +++ b/nova/actions.py @@ -3,7 +3,7 @@ import pydantic import wandelbots_api_client as wb - +from nova.motion_settings import MotionSettings from nova.types.collision_scene import CollisionScene from nova.types.pose import Pose @@ -17,7 +17,6 @@ def serialize_model(self): class WriteAction(Action): type: Literal["Write"] = "Write" - device_id: str key: str value: Any @@ -50,36 +49,6 @@ class CallAction(Action): arguments: list -class MotionSettings(pydantic.BaseModel): - """Settings to customize motions. - - Attributes: - velocity: the cartesian velocity of the TCP on the segment in mm/s - acceleration: the cartesian acceleration of the TCP on the segment in mm/s - orientation_velocity: the (tcp) orientation velocity on the segment in rad/s - orientation_acceleration: the tcp orientation acceleration on the segment in rad/s - joint_velocities: the joint velocities on the segment in rad/s - joint_accelerations: the joint accelerations on the segment in rad/s - blending: the blending radius to connect the previous segment to this one. If blending == 0, blending is - disabled. If blending == math.inf, blending is enabled and the radius is automatically determined - based on the robot velocity. - optimize_approach: define approach axis as DoF and optimize for executable path - """ - - velocity: float | None = pydantic.Field(default=None, ge=0) - acceleration: float | None = pydantic.Field(default=None, ge=0) - orientation_velocity: float | None = pydantic.Field(default=None, ge=0) - orientation_acceleration: float | None = pydantic.Field(default=None, ge=0) - joint_velocities: tuple[float, ...] | None = None - joint_accelerations: tuple[float, ...] | None = None - blending: float = pydantic.Field(default=0, ge=0) - optimize_approach: bool = False - - @classmethod - def field_to_varname(cls, field): - return f"__ms_{field}" - - MS = MotionSettings PoseOrVectorTuple = Union[ Pose, tuple[float, float, float, float, float, float], tuple[float, float, float] @@ -131,8 +100,9 @@ class Linear(Motion): """A linear motion Examples: - >>> Linear(target=Pose((1, 2, 3, 4, 5, 6)), settings=MotionSettings(velocity=10)) - Linear(type='linear', target=Pose(position=Vector3d(x=1, y=2, z=3), orientation=Vector3d(x=4, y=5, z=6)), settings=MotionSettings(velocity=10.0, acceleration=None, orientation_velocity=None, orientation_acceleration=None, joint_velocities=None, joint_accelerations=None, blending=0, optimize_approach=False)) + >>> Linear(target=Pose((1, 2, 3, 4, 5, 6)), settings=MotionSettings(tcp_velocity_limit=10)) + Linear(type='linear', target=Pose(position=Vector3d(x=1, y=2, z=3), orientation=Vector3d(x=4, y=5, z=6)), settings=MotionSettings(min_blending_velocity=None, position_zone_radius=None, joint_velocity_limits=None, joint_acceleration_limits=None, tcp_velocity_limit=10.0, tcp_acceleration_limit=None, tcp_orientation_velocity_limit=None, tcp_orientation_acceleration_limit=None)) + """ type: Literal["linear"] = "linear" @@ -142,7 +112,7 @@ def _to_api_model(self) -> wb.models.PathLine: """Serialize the model to the API model Examples: - >>> Linear(target=Pose((1, 2, 3, 4, 5, 6)), settings=MotionSettings(velocity=10))._to_api_model() + >>> Linear(target=Pose((1, 2, 3, 4, 5, 6)), settings=MotionSettings(tcp_velocity_limit=10))._to_api_model() PathLine(target_pose=Pose2(position=[1, 2, 3], orientation=[4, 5, 6]), path_definition_name='PathLine') """ return wb.models.PathLine( @@ -164,7 +134,7 @@ def lin(target: PoseOrVectorTuple, settings: MotionSettings = MotionSettings()) Returns: the linear motion Examples: - >>> ms = MotionSettings(velocity=10) + >>> ms = MotionSettings(tcp_velocity_limit=10) >>> assert lin((1, 2, 3, 4, 5, 6), settings=ms) == Linear(target=Pose((1, 2, 3, 4, 5, 6)), settings=ms) >>> assert lin((1, 2, 3)) == lin((1, 2, 3, 0, 0, 0)) @@ -177,8 +147,9 @@ class PTP(Motion): """A point-to-point motion Examples: - >>> PTP(target=Pose((1, 2, 3, 4, 5, 6)), settings=MotionSettings(velocity=30)) - PTP(type='ptp', target=Pose(position=Vector3d(x=1, y=2, z=3), orientation=Vector3d(x=4, y=5, z=6)), settings=MotionSettings(velocity=30.0, acceleration=None, orientation_velocity=None, orientation_acceleration=None, joint_velocities=None, joint_accelerations=None, blending=0, optimize_approach=False)) + >>> PTP(target=Pose((1, 2, 3, 4, 5, 6)), settings=MotionSettings(tcp_velocity_limit=30)) + PTP(type='ptp', target=Pose(position=Vector3d(x=1, y=2, z=3), orientation=Vector3d(x=4, y=5, z=6)), settings=MotionSettings(min_blending_velocity=None, position_zone_radius=None, joint_velocity_limits=None, joint_acceleration_limits=None, tcp_velocity_limit=30.0, tcp_acceleration_limit=None, tcp_orientation_velocity_limit=None, tcp_orientation_acceleration_limit=None)) + """ type: Literal["ptp"] = "ptp" @@ -187,7 +158,7 @@ def _to_api_model(self) -> wb.models.PathCartesianPTP: """Serialize the model to the API model Examples: - >>> PTP(target=Pose((1, 2, 3, 4, 5, 6)), settings=MotionSettings(velocity=30))._to_api_model() + >>> PTP(target=Pose((1, 2, 3, 4, 5, 6)), settings=MotionSettings(tcp_velocity_limit=30))._to_api_model() PathCartesianPTP(target_pose=Pose2(position=[1, 2, 3], orientation=[4, 5, 6]), path_definition_name='PathCartesianPTP') """ if not isinstance(self.target, Pose): @@ -212,7 +183,7 @@ def ptp(target: PoseOrVectorTuple, settings: MotionSettings = MotionSettings()) Returns: the point-to-point motion Examples: - >>> ms = MotionSettings(acceleration=10) + >>> ms = MotionSettings(tcp_acceleration_limit=10) >>> assert ptp((1, 2, 3, 4, 5, 6), settings=ms) == PTP(target=Pose((1, 2, 3, 4, 5, 6)), settings=ms) >>> assert ptp((1, 2, 3)) == ptp((1, 2, 3, 0, 0, 0)) @@ -239,7 +210,7 @@ def _to_api_model(self) -> wb.models.PathCircle: """Serialize the model to a dictionary Examples: - >>> Circular(target=Pose((1, 2, 3, 4, 5, 6)), intermediate=Pose((10, 20, 30, 40, 50, 60)), settings=MotionSettings(velocity=30))._to_api_model() + >>> Circular(target=Pose((1, 2, 3, 4, 5, 6)), intermediate=Pose((10, 20, 30, 40, 50, 60)), settings=MotionSettings(tcp_velocity_limit=30))._to_api_model() PathCircle(via_pose=Pose2(position=[10, 20, 30], orientation=[40, 50, 60]), target_pose=Pose2(position=[1, 2, 3], orientation=[4, 5, 6]), path_definition_name='PathCircle') """ if not isinstance(self.target, Pose): @@ -273,7 +244,7 @@ def cir( Returns: the circular motion Examples: - >>> ms = MotionSettings(acceleration=10) + >>> ms = MotionSettings(tcp_acceleration_limit=10) >>> assert cir((1, 2, 3, 4, 5, 6), (7, 8, 9, 10, 11, 12), settings=ms) == Circular(target=Pose((1, 2, 3, 4, 5, 6)), intermediate=Pose((7, 8, 9, 10, 11, 12)), settings=ms) >>> assert cir((1, 2, 3), (4, 5, 6)) == cir((1, 2, 3, 0, 0, 0), (4, 5, 6, 0, 0, 0)) @@ -287,8 +258,8 @@ class JointPTP(Motion): """A joint PTP motion Examples: - >>> JointPTP(target=(1, 2, 3, 4, 5, 6), settings=MotionSettings(velocity=30)) - JointPTP(type='joint_ptp', target=(1.0, 2.0, 3.0, 4.0, 5.0, 6.0), settings=MotionSettings(velocity=30.0, acceleration=None, orientation_velocity=None, orientation_acceleration=None, joint_velocities=None, joint_accelerations=None, blending=0, optimize_approach=False)) + >>> JointPTP(target=(1, 2, 3, 4, 5, 6), settings=MotionSettings(tcp_velocity_limit=30)) + JointPTP(type='joint_ptp', target=(1.0, 2.0, 3.0, 4.0, 5.0, 6.0), settings=MotionSettings(min_blending_velocity=None, position_zone_radius=None, joint_velocity_limits=None, joint_acceleration_limits=None, tcp_velocity_limit=30.0, tcp_acceleration_limit=None, tcp_orientation_velocity_limit=None, tcp_orientation_acceleration_limit=None)) """ @@ -298,7 +269,7 @@ def _to_api_model(self) -> wb.models.PathJointPTP: """Serialize the model to the API model Examples: - >>> JointPTP(target=(1, 2, 3, 4, 5, 6, 7), settings=MotionSettings(velocity=30))._to_api_model() + >>> JointPTP(target=(1, 2, 3, 4, 5, 6, 7), settings=MotionSettings(tcp_velocity_limit=30))._to_api_model() PathJointPTP(target_joint_position=[1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0], path_definition_name='PathJointPTP') """ if not isinstance(self.target, tuple): @@ -322,7 +293,7 @@ def jnt(target: tuple[float, ...], settings: MotionSettings = MotionSettings()) Returns: the joint PTP motion Examples: - >>> ms = MotionSettings(acceleration=10) + >>> ms = MotionSettings(tcp_acceleration_limit=10) >>> assert jnt((1, 2, 3, 4, 5, 6), settings=ms) == JointPTP(target=(1, 2, 3, 4, 5, 6), settings=ms) """ @@ -347,7 +318,7 @@ def serialize_model(self): """Serialize the model to a dictionary Examples: - >>> JointPTP(target=(1, 2, 3, 4, 5, 6, 7), settings=MotionSettings(velocity=30)).model_dump() + >>> JointPTP(target=(1, 2, 3, 4, 5, 6, 7), settings=MotionSettings(tcp_velocity_limit=30)).model_dump() {'target_joint_position': [1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0], 'path_definition_name': 'PathJointPTP'} """ raise NotImplementedError("Spline motion is not yet implemented") @@ -370,7 +341,7 @@ def spl( Returns: the spline motion Examples: - >>> ms = MotionSettings(acceleration=10) + >>> ms = MotionSettings(tcp_acceleration_limit=10) >>> assert spl((1, 2, 3, 4, 5, 6), settings=ms) == Spline(target=Pose((1, 2, 3, 4, 5, 6)), settings=ms) >>> assert spl((1, 2, 3)) == spl((1, 2, 3, 0, 0, 0)) @@ -496,10 +467,25 @@ def __add__(self, other: "CombinedActions") -> "CombinedActions": return CombinedActions(items=self.items + other.items) def to_motion_command(self) -> list[wb.models.MotionCommand]: - motions = [ - wb.models.MotionCommandPath.from_dict(motion.model_dump()) for motion in self.motions - ] - return [wb.models.MotionCommand(path=motion) for motion in motions] + motion_commands = [] + for motion in self.motions: + path = wb.models.MotionCommandPath.from_dict(motion.model_dump()) + blending = ( + motion.settings.as_blending_setting() + if motion.settings.has_blending_settings() + else None + ) + limits_override = ( + motion.settings.as_limits_settings() + if motion.settings.has_limits_override() + else None + ) + motion_commands.append( + wb.models.MotionCommand( + path=path, blending=blending, limits_override=limits_override + ) + ) + return motion_commands def to_set_io(self) -> list[wb.models.SetIO]: return [ diff --git a/nova/motion_settings.py b/nova/motion_settings.py new file mode 100644 index 00000000..4b60a5df --- /dev/null +++ b/nova/motion_settings.py @@ -0,0 +1,96 @@ +import pydantic +import wandelbots_api_client as wb + + +class MotionSettings(pydantic.BaseModel): + """ + Settings for an action. This is closely related to the `MotionCommand` in the API. + See planTrajectory.motion_commands for more information. + + Motion settings are immutable; if you need to change a setting, create a copy and update the new object. + + Attributes: + min_blending_velocity: + A minimum velocity for blending, in percent. Cannot be used if `blending` is set. + + position_zone_radius: + Defines the position zone radius. + + joint_velocity_limits: + Maximum joint velocities + + joint_acceleration_limits: + Maximum joint accelerations + + tcp_velocity_limit: + Maximum TCP velocity + + tcp_acceleration_limit: + Maximum TCP acceleration + + tcp_orientation_velocity_limit: + Maximum TCP orientation velocity + + tcp_orientation_acceleration_limit: + Maximum TCP orientation acceleration + """ + + min_blending_velocity: int | None = pydantic.Field(default=None) + position_zone_radius: float | None = pydantic.Field(default=None) + joint_velocity_limits: tuple[float, ...] | None = pydantic.Field(default=None) + joint_acceleration_limits: tuple[float, ...] | None = pydantic.Field(default=None) + tcp_velocity_limit: float | None = pydantic.Field(default=None) + tcp_acceleration_limit: float | None = pydantic.Field(default=None) + tcp_orientation_velocity_limit: float | None = pydantic.Field(default=None) + tcp_orientation_acceleration_limit: float | None = pydantic.Field(default=None) + + class Config: + frozen = True + + @pydantic.model_validator(mode="after") + def validate_blending_settings(self) -> "MotionSettings": + if self.min_blending_velocity and self.position_zone_radius: + raise ValueError("Can't set both min_blending_velocity and blending") + return self + + def has_blending_settings(self) -> bool: + return any([self.min_blending_velocity, self.position_zone_radius]) + + def has_limits_override(self) -> bool: + return any( + [ + self.tcp_velocity_limit, + self.tcp_acceleration_limit, + self.tcp_orientation_velocity_limit, + self.tcp_orientation_acceleration_limit, + self.joint_velocity_limits, + self.joint_acceleration_limits, + ] + ) + + def as_limits_settings(self) -> wb.models.LimitsOverride: + return wb.models.LimitsOverride( + joint_velocity_limits=wb.models.Joints(joints=self.joint_velocity_limits) # type: ignore + if self.joint_velocity_limits + else None, + joint_acceleration_limits=wb.models.Joints(joints=self.joint_acceleration_limits) # type: ignore + if self.joint_acceleration_limits + else None, + tcp_velocity_limit=self.tcp_velocity_limit, + tcp_acceleration_limit=self.tcp_acceleration_limit, + tcp_orientation_velocity_limit=self.tcp_orientation_velocity_limit, + tcp_orientation_acceleration_limit=self.tcp_orientation_acceleration_limit, + ) + + def as_blending_setting(self) -> wb.models.MotionCommandBlending: + if self.position_zone_radius: + return wb.models.MotionCommandBlending( + wb.models.BlendingPosition( + position_zone_radius=self.position_zone_radius, blending_name="BlendingPosition" + ) + ) + return wb.models.MotionCommandBlending( + wb.models.BlendingAuto( + min_velocity_in_percent=self.min_blending_velocity, blending_name="BlendingAuto" + ) + )