Skip to content

Commit

Permalink
feat: add support for motion setting (#23)
Browse files Browse the repository at this point in the history
  • Loading branch information
mahsumdemirwb authored Jan 16, 2025
1 parent 76cabf8 commit dbbe8a6
Show file tree
Hide file tree
Showing 5 changed files with 142 additions and 54 deletions.
6 changes: 5 additions & 1 deletion examples/02_plan_and_execute.py
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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)

Expand Down
2 changes: 1 addition & 1 deletion examples/03_move_and_set_ios.py
Original file line number Diff line number Diff line change
Expand Up @@ -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),
]
Expand Down
2 changes: 2 additions & 0 deletions nova/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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__ = [
Expand All @@ -13,4 +14,5 @@
"api",
"types",
"actions",
"MotionSettings",
]
90 changes: 38 additions & 52 deletions nova/actions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -17,7 +17,6 @@ def serialize_model(self):

class WriteAction(Action):
type: Literal["Write"] = "Write"
device_id: str
key: str
value: Any

Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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"
Expand All @@ -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(
Expand All @@ -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))
Expand All @@ -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"
Expand All @@ -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):
Expand All @@ -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))
Expand All @@ -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):
Expand Down Expand Up @@ -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))
Expand All @@ -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))
"""

Expand All @@ -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):
Expand All @@ -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)
"""
Expand All @@ -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")
Expand All @@ -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))
Expand Down Expand Up @@ -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 [
Expand Down
96 changes: 96 additions & 0 deletions nova/motion_settings.py
Original file line number Diff line number Diff line change
@@ -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"
)
)

0 comments on commit dbbe8a6

Please sign in to comment.