Trait Design

Context and Problem Statement

We want to enable the users to program a wide range of robots with varying capabilities using Python. To achieve this, this library will be designed with user-friendliness in mind.

Currently, we have a single OPC UA interface to interact with robot-control, but its behavior can vary across different robot types and may not support all features for each one. There is ongoing debate about whether a universal interface that fully accommodates all robot capabilities is achievable.

However, the goal is that the user should not need to worry about these technical details while programming a robot. The focus should be on solving the problem at hand. Therefore, the library should be intuitive and guide the user in understanding which features are available for each robot type. If a feature is supported, it should work consistently across all robots.

A key challenge in achieving ease of use is balancing the need for a general API that works across all robots while also accommodating the unique behaviors of specific robot models. For example, while all robots should support PTP, only certain models, like KUKA, might offer a specialized spline motion command.

Traits and how we treat them

A trait is a characteristic of implementation that any robot we support can fulfill. One example could be a PTP movement trait. Every robot that implements and supports this trait will offer the same methods and behavior of these methods in its interface.

        ---
title: Robot Arm API Simplified
---
classDiagram
WaitTrait <|-- WaitFanucImpl : implements
WaitTrait <|-- WaitYuImpl : implements
PtpTrait <|-- PtpFanucImpl : implements
PtpTrait <|-- PtpYuImpl : implements
PtpFanucImpl <|-- FanucRobotArm
WaitFanucImpl <|-- FanucRobotArm
PtpYuImpl <|-- YuRobotArm
WaitYuImpl <|-- YuRobotArm
class WaitTrait{
    <<interface>>
    + wait(time_s)*
}
class WaitFanucImpl{
    + wait(time_s)
}
class WaitYuImpl{
    + wait(time_s)
}
class PtpTrait{
    <<interface>>
    + ptp(joint_pose: JointPose, ptp_params)*
}
class PtpFanucImpl{
    + ptp(joint_pose: JointPose, ptp_params)
}
class PtpYuImpl{
    + ptp(joint_pose: JointPose, ptp_params)
}

    

Fig. 1 A simplified API to get the gist of the proposal.

The figure above shows a simplified example. The classes which are used by the end user are FanucRobotArm and YuRobotArm. Since traits are not a native language feature of Python (unlike Rust), both classes inherit a robot specific implementation of a trait. As a result, both FanucRobotArm and YuRobotArm conform to the PTP trait, have the same methods and behave the same, although their implementations are different under the hood.

In comparison to one general robot interface, that acts as a common denominator for all robots, this traits design allows for a much more granular differentiation, what a robot does support or does not.

        ---
title: Robot Arm API
---
classDiagram
WaitTrait <|-- WaitFanucImpl : implements
FanucDriver "1" <-- HasFanucDriver
YuDriver "1" <-- HasYuDriver
HasFanucDriver <|-- WaitFanucImpl
WaitTrait <|-- WaitYuImpl : implements
HasYuDriver <|-- WaitYuImpl
PtpTrait <|-- PtpFanucImpl : implements
HasFanucDriver <|-- PtpFanucImpl
PtpTrait <|-- PtpYuImpl : implements
HasYuDriver <|-- PtpYuImpl
ToolInteractionTrait <|-- ToolInteractionFanucImpl : implements
HasFanucDriver <|-- ToolInteractionFanucImpl
SpecialBehaviorFanucTrait <|-- SpecialBehaviorFanucImpl : implements
HasFanucDriver <|-- SpecialBehaviorFanucImpl
HasFanucDriver <|-- LifecycleFanucImpl
LifecycleTrait <|-- LifecycleFanucImpl : implements
PtpFanucImpl <|-- FanucRobotArm
SpecialBehaviorFanucImpl <|-- FanucRobotArm
ToolInteractionFanucImpl <|-- FanucRobotArm
WaitFanucImpl <|-- FanucRobotArm
LifecycleFanucImpl <|-- FanucRobotArm
PtpYuImpl <|-- YuRobotArm
WaitYuImpl <|-- YuRobotArm
LifecycleYuImpl <|-- YuRobotArm
HasYuDriver <|-- LifecycleYuImpl
LifecycleTrait <|-- LifecycleYuImpl : implements
class FanucDriver{
...
}
class YuDriver{
...
}
class WaitTrait{
<<interface>>
+ wait(time_s)*
}
class WaitFanucImpl{
+ wait(time_s)
}
class WaitYuImpl{
+ wait(time_s)
}
class PtpTrait{
<<interface>>
+ ptp(joint_pose: JointPose, ptp_params)*
}
class PtpFanucImpl{
+ ptp(joint_pose: JointPose, ptp_params)
}
class PtpYuImpl{
+ ptp(joint_pose: JointPose, ptp_params)
}
class ToolInteractionTrait{
<<interface>>
+ open_tool()*
+ tool_is_open() *
+ close_tool()*
+ tool_is_closed()*
+ set_weight()*
}
class ToolInteractionFanucImpl{
+ open_tool()
+ tool_is_open()
+ close_tool()
+ tool_is_closed()
+ set_weight()
}
class SpecialBehaviorFanucTrait{
<<interface>>
+ do_special_instruction()*
}
class SpecialBehaviorFanucImpl{
+ do_special_instruction()
}
class LifecycleTrait{
<<interface>>
+ connect(host: str, port: int)$ Self
+ start_up()*
+ shut_down()*
+ disconnect()*
+ __enter__()*
+ __exit__()*
}
class LifecycleFanucImpl{
+ connect(host: str, port: int)$ Self
+ start_up()
+ shut_down()
+ disconnect()
+ __enter__()
+ __exit__()
}
class LifecycleYuImpl{
+ connect(host: str, port: int)$ Self
+ start_up()
+ shut_down()
+ disconnect()
+ __enter__()
+ __exit__()
}

    

Fig. 2 A more complex example.

The figure above shows a more complex example of this design. Most notably are the following points:

  1. Each trait implementation has to have a robot specific driver, which is the actual implementation of what happens under the hood.

  2. If a robot does support a non generic, robot-specific interface, it is always possible to implement a custom trait for it (see SpecialBehaviorFanucTrait)

  3. The lifecycle trait may be the only trait all robots have to support.

  4. The Yu does not implement all available traits. If a program expects that the robot it interacts with supports the ToolInteractionTrait, it only works for the FANUC. Otherwise the program works with both robots.

In code, this implementation could be used as:

from voraus_robot_arm import FanucRobotArm, YuRobotArm
from voraus_robot_arm.traits import PtpTrait, ToolInteractionTrait, TraitIntersection

RequiredTraits = TraitIntersection([PtpTrait, ToolInteractionTrait])

def my_program(robot: RequiredTraits) -> None:
    with robot.connect("opc.tcp://127.0.0.1:48401/"):
            robot.ptp(HOME)  # robot supports the PTP trait
            robot.ptp(POSE_1)
            robot.ptp(POSE_2)

            if robot.tool_is_open():
                robot.close_tool()

robot = FanucRobotArm()
my_program(robot) # Works as expected

robot = YuRobotArm()
my_program(robot) # mypy error: YuRobotArm does not support ToolInteractionTrait + runtime error: Method not found

Remarks on first development steps, ease of change and impacts on testing

  • As noted above, in theory, each supported robot should have its own specific driver under the hood, since there is no assumption, which interface is used to talk to robot control. In reality, most supported robots will use the same driver for the voraus.core OPC UA interface.

  • Since there already exists a first version of a RobotArm class, which has the OPC UA communication already implemented, it will be used as the first driver under the hood. However, if changes are necessary down the line to this driver, the user will not be impacted.

  • At the start of development, we assume, that we do not know, how the methods of the traits will look and if our assumptions for some traits will hold down the line e.g. regarding used method arguments. However, this is fine. Since we can phase out traits with similar traits down the line.

  • It is valid, that only some robots have support for some traits while others do not.

  • The trait definition itself can only define the interface with methods and arguments to call. However, traits come with a behavior contract for the user. Behavior in this case is defined in regards to timing, order and end result of executed commands. As such, a robot shall only conform to and support a trait if and only if it passes a set of abstractly defined behavior integration tests. The tests are considered part of the trait definition.