Tendon code examples
These examples show the C++ needed for individual tendon behaviors. Every block
below is included directly from the runnable tendon_recipes.cpp source.
The program also checks the resulting motion, forces, and exported state, so
these are executable examples with physical acceptance checks.
Download tendon_recipes.cpp
and its CMakeLists.txt into the
same directory, or use docs/code/tendons in the distribution. For the larger
interactive and TCP scenes, see Tendon examples and Rayrai. The recipes themselves run
headlessly and print measurements.
Build and run the recipes
From the raisim2Lib root on Linux:
cmake -S docs/code/tendons -B /tmp/raisim-tendon-recipes \
-DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_COMPILER=clang++-20 \
-DCMAKE_PREFIX_PATH="$PWD/raisim" \
-DRAISIM_RECIPE_ACTIVATION_KEY=/path/to/activation.raisim
cmake --build /tmp/raisim-tendon-recipes --parallel 12
/tmp/raisim-tendon-recipes/tendon_recipes limit /path/to/activation.raisim
ctest --test-dir /tmp/raisim-tendon-recipes -j 12 --output-on-failure
Set the prefix to the installed RaiSim package directory if its location differs.
On macOS and Windows, use the platform’s compiler setup and build directory;
omit the Linux compiler override. Multi-configuration generators also need
--config Release when building and -C Release for CTest. Their executable
normally resides in a configuration subdirectory such as Release. On Windows,
make the RaiSim package’s runtime DLLs discoverable as described in
Installation.
The command syntax is tendon_recipes CASE [ACTIVATION_KEY|-] [EXPORT_XML].
Omit the key argument or use - for normal license discovery. The CMake key
setting is optional and affects CTest. The export case also requires an
absolute output filename in an existing directory, for example:
/tmp/raisim-tendon-recipes/tendon_recipes export - /tmp/tendon-recipe.xml
Available cases are limit, lock, spring, force, servo,
wrap, fixed, coupling, export, and straight. Each has a CTest
entry; the straight case runs the three straight-tendon examples below. Export
writes the supplied file, so choose an output path you intend to replace.
The fragments below are function bodies. To use one in your own program,
include <raisim/World.hpp> and <iostream>, initialize your license before
creating the world, and provide this shared alias and stepping helper:
using Path = raisim::Tendon::PathElement;
void advance(raisim::World& world, int steps) {
for (int i = 0; i < steps; ++i) world.integrate();
}
Limit the maximum separation
Run tendon_recipes limit. A 1 kg load starts at 1 m with an outward velocity
of 2 m/s. It stops at the 1.2 m upper bound. No spring or actuator is needed for
a hard length limit.
raisim::World world;
world.setTimeStep(0.001);
world.setGravity({0., 0., 0.});
auto* load = world.addSphere(0.05, 1.0);
load->setPosition(1., 0., 0.);
load->setLinearVelocity({2., 0., 0.});
raisim::Tendon::Properties p;
p.upperLimit = 1.2; // Maximum distance from the world origin, in metres.
auto* cable = world.addSpatialTendon("stop", {
Path::via({nullptr, 0, {0., 0., 0.}}), Path::via({load})}, p);
advance(world, 1000);
cable->updateGeometry();
std::cout << "length=" << cable->getLength()
<< " speed=" << cable->getVelocity() << '\n';
For a minimum separation, set lowerLimit instead. To allow only an interval,
set both bounds, for example lowerLimit = 0.8 and upperLimit = 1.2.
limitMargin moves the unilateral activation boundaries inward.
Lock the current length
Run tendon_recipes lock. Refresh geometry and assign the current length to
both limits. The load can still swing: only its distance from the anchor is
constrained. This is the tendon equivalent of a bilateral straight wire.
raisim::World world;
world.setTimeStep(0.001);
world.setGravity({0., 0., -9.81});
auto* load = world.addSphere(0.05, 1.0);
load->setPosition(0., 0., 0.5);
auto* cable = world.addSpatialTendon("pendulum", {
Path::via({nullptr, 0, {0., 0., 2.}}), Path::via({load})});
cable->updateGeometry(); // Also do this after manually changing poses.
const double lockedLength = cable->getLength();
auto p = cable->getProperties();
p.lowerLimit = lockedLength;
p.upperLimit = lockedLength;
p.limitCompliance = 0.; // Hard bilateral length constraint.
p.positionCorrection = 0.2;
cable->setProperties(p);
load->setLinearVelocity({0.4, 0., 0.}); // The load may swing around the anchor.
advance(world, 2000);
cable->updateGeometry();
std::cout << "locked=" << lockedLength << " actual=" << cable->getLength()
<< " reaction=" << cable->getLimitForce() << '\n';
Equal bounds select a bilateral row; limitMargin is ignored for this lock.
positionCorrection controls error correction, so setting a target different
from the current length does not teleport the body. To soften the lock, use
positive limitCompliance; see Tendon physics for its discrete meaning.
To release the lock later, reset only its limits. Include <limits>:
auto released = cable->getProperties();
released.lowerLimit = -std::numeric_limits<double>::infinity();
released.upperLimit = std::numeric_limits<double>::infinity();
cable->setProperties(released); // Retain other properties and drive settings.
Springs, drives, and other configured effects remain active. The lock case
also checks that the load can move beyond the former bound after release.
Suspend a load with a slack spring
Run tendon_recipes spring. A vertical 1 kg load settles near
1 + 9.81 / 200 = 1.04905 m. Dry friction permits a small equilibrium band
around that value. Damping dissipates oscillation, while armature adds inertia
along the tendon coordinate.
raisim::World world;
world.setTimeStep(0.001);
world.setGravity({0., 0., -9.81});
auto* load = world.addSphere(0.05, 1.0);
load->setPosition(0., 0., 0.8);
raisim::Tendon::Properties p;
p.springLower = 0.;
p.springUpper = 1.0; // Spring is slack below 1 m.
p.stiffness = 200.; // N/m.
p.damping = 12.; // N s/m; remains active inside the slack interval.
p.frictionLoss = 0.05; // N of dry friction along the transmission.
p.armature = 0.1; // kg of added transmission inertia.
p.upperLimit = 1.8; // Independent safety bound.
auto* cable = world.addSpatialTendon("spring", {
Path::via({nullptr, 0, {0., 0., 2.}}), Path::via({load})}, p);
advance(world, 4000);
cable->updateGeometry();
std::cout << "length=" << cable->getLength()
<< " tension=" << cable->getTension()
<< " spring_energy=" << cable->getPotentialEnergy() << '\n';
For a spring that also resists compression, set both spring endpoints to the same rest length. The spring interval, hard limit, and actuator target are independent. A slack spring can still have damping or friction force.
Apply a bounded tension command
Run tendon_recipes force. The requested 5 N tension is limited to 4 N. In
zero gravity, the 1 kg load reaches -0.4 m/s after 0.1 s. Negative signed actuator
force pulls toward shorter length.
raisim::World world;
world.setTimeStep(0.001);
world.setGravity({0., 0., 0.});
auto* load = world.addSphere(0.05, 1.0);
load->setPosition(2., 0., 0.);
auto* cable = world.addSpatialTendon("motor", {
Path::via({nullptr, 0, {0., 0., 0.}}), Path::via({load})});
auto p = cable->getProperties();
p.actuationLower = -4.; // Up to 4 N pulling.
p.actuationUpper = 0.; // No pushing from the actuator.
cable->setProperties(p);
cable->setDrive(raisim::Tendon::Drive{}); // Clear any previous servo/filter.
cable->setTension(5.); // Requests F = -5 N; the actuator clamps this to -4 N.
advance(world, 100); // 0.1 s: a 1 kg load acquires -0.4 m/s.
cable->updateGeometry();
std::cout << "actuator_force=" << cable->getActuationForce()
<< " velocity_x=" << load->getLinearVelocity()[0] << '\n';
A fresh Drive{} clears previous gains, targets, and activation filtering.
Calling setTension() alone preserves those settings. The actuator bounds do
not clamp passive springs, friction, or limit/coupling reactions.
Control the length with a servo
Run tendon_recipes servo. A pull-only winch lifts the suspended load from a
1.2 m cable length toward 0.8 m. Feedforward compensates gravity in this vertical
configuration, while position/velocity feedback controls the transient.
raisim::World world;
world.setTimeStep(0.001);
world.setGravity({0., 0., -9.81});
auto* load = world.addSphere(0.05, 1.0);
load->setPosition(0., 0., 0.8);
raisim::Tendon::Properties p;
p.actuationLower = -30.;
p.actuationUpper = 0.;
auto* cable = world.addSpatialTendon("winch", {
Path::via({nullptr, 0, {0., 0., 2.}}), Path::via({load})}, p);
raisim::Tendon::Drive drive;
drive.targetLength = 0.8;
drive.targetVelocity = 0.;
drive.positionGain = 200.;
drive.velocityGain = 25.;
drive.force = -9.81; // Feedforward balances this 1 kg vertical load.
drive.activationTime = 0.05; // Filters feedforward; gains remain implicit.
cable->setDrive(drive);
advance(world, 3000);
cable->updateGeometry();
std::cout << "target=" << drive.targetLength
<< " actual=" << cable->getLength() << '\n';
For a moving target, copy getDrive(), update targetLength and optionally
targetVelocity, call setDrive(), and then integrate each step.
activationTime filters the feedforward force; it does not delay the target
or filter the feedback gains. The gravity-compensation value depends on the
actual routing and load and must be recomputed for other mechanisms.
Route over a cylinder and add a pulley branch
Run tendon_recipes wrap. The two main loads receive 4 N upward, and the
independent divisor-2 output branch receives 2 N. Their first-step vertical
velocities are approximately 0.004 and 0.002 m/s, respectively. Add <cmath>
when copying this fragment.
raisim::World world;
world.setTimeStep(0.001);
world.setGravity({0., 0., 0.});
auto* left = world.addSphere(0.05, 1.0);
auto* right = world.addSphere(0.05, 1.0);
auto* output = world.addSphere(0.05, 1.0);
left->setPosition(-0.5, 0., 1.);
right->setPosition(0.5, 0., 1.);
output->setPosition(3., 0., 1.);
// This static body makes the guide visible. Its collision masks are zero.
auto* wheel = world.addCylinder(0.5, 0.2, 1., "default", 0, 0);
wheel->setBodyType(raisim::BodyType::STATIC);
wheel->setPosition(0., 0., 2.);
wheel->setOrientation(std::sqrt(0.5), -std::sqrt(0.5), 0., 0.);
auto guide = Path::cylinder({wheel}, 0.5); // Local Z axis becomes world Y.
guide.withSideSite({nullptr, 0, {0., 0., 3.}}); // Route over the wheel.
raisim::Tendon::Properties p;
p.width = 0.01;
p.color = {1., 0.55, 0.05, 1.};
auto* cable = world.addSpatialTendon("pulley", {
Path::via({left}), guide, Path::via({right}),
Path::pulley(2.), // Begins an independent branch at half force.
Path::via({nullptr, 0, {3., 0., 3.}}), Path::via({output})}, p);
cable->setTension(4.); // Main branch: 4 N; output branch: 2 N.
world.integrate();
cable->updateGeometry(true);
std::cout << "left_vz=" << left->getLinearVelocity()[2]
<< " output_vz=" << output->getLinearVelocity()[2]
<< " drawing_segments=" << cable->getVisualSegments().size() << '\n';
{wheel} locates the routing guide at that body’s origin. The cylinder axis
is local +Z; the body’s orientation rotates it into world Y. The top side site
retains the overhead route. pulley(2) begins a separate branch rather than
creating a segment from right to the output anchor.
To wrap a sphere, use Path::sphere(centerSite, radius) with suitably placed
endpoints. The wrapping primitive is separate from collision geometry: the
static cylinder above exists to show the wheel. updateGeometry(true) exposes
the route for inspection; normal Rayrai rendering draws it automatically.
Drive a weighted joint coordinate
Run tendon_recipes fixed. This small model supplies two independent
prismatic joints with unit moving masses. It needs no external URDF file.
raisim::ArticulatedSystem* makeTwoSliders(raisim::World& world) {
// Two independent 1 kg sliders on a fixed base; no external model file.
raisim::Body body(1., raisim::Mat<3, 3>::getIdentity(), {0., 0., 0.});
raisim::Joint base;
base.type = raisim::Joint::FIXED;
base.name = "base";
raisim::Child root(body, base, "base");
raisim::Joint x({1., 0., 0.}, {0., 0., 0.},
raisim::Mat<3, 3>::getIdentity(), {-10., 10.}, raisim::Joint::PRISMATIC, "x");
raisim::Joint y({0., 1., 0.}, {0., 0., 0.},
raisim::Mat<3, 3>::getIdentity(), {-10., 10.}, raisim::Joint::PRISMATIC, "y");
root.addChild(raisim::Child(body, x, "link_x"));
root.addChild(raisim::Child(body, y, "link_y"));
return world.addArticulatedSystem(root);
}
The tendon drives x + 0.5*y toward 0.5. For these masses, zero initial state,
and no other forces, the resulting joint coordinates approach 0.4 and 0.2.
raisim::World world;
world.setTimeStep(0.001);
world.setGravity({0., 0., 0.});
auto* robot = makeTwoSliders(world);
robot->setGeneralizedCoordinate({0., 0.});
auto* transmission = world.addFixedTendon("differential", {
{robot, "x", 1.0}, {robot, "y", 0.5}});
raisim::Tendon::Drive drive;
drive.targetLength = 0.5; // x + 0.5*y = 0.5; individual joints remain free.
drive.positionGain = 200.;
drive.velocityGain = 20.;
transmission->setDrive(drive);
advance(world, 2000);
transmission->updateGeometry();
std::cout << "transmission=" << transmission->getLength()
<< " joints=" << robot->getGeneralizedCoordinate().e().transpose() << '\n';
Replace the model helper with your robot and use its revolute/prismatic joint names. The tendon constrains or drives the weighted sum, leaving other motion possible. Coefficients on revolute joints carry the chosen transmission units. A fixed tendon has no spatial cable line; Tendon examples and Rayrai includes a visible mechanism combining a fixed actuator with spatial cables.
Couple two visible cable lengths
Run tendon_recipes coupling. Tendon A extends by 0.2 m and tendon B shortens
by 0.13 m. Creation poses establish the references, so initialize the bodies
before adding the tendons.
raisim::World world;
world.setTimeStep(0.001);
world.setGravity({0., 0., 0.});
world.setContactSolverParam(1., 1., 1., 100, 1e-12);
auto* firstLoad = world.addSphere(0.05, 1.0);
auto* secondLoad = world.addSphere(0.05, 1.0);
firstLoad->setPosition(1., 0., 0.);
secondLoad->setPosition(1., 2., 0.);
auto* a = world.addSpatialTendon("A", {
Path::via({nullptr, 0, {0., 0., 0.}}), Path::via({firstLoad})});
auto* b = world.addSpatialTendon("B", {
Path::via({nullptr, 0, {0., 2., 0.}}), Path::via({secondLoad})});
raisim::TendonCoupling::Properties relation;
relation.coefficients = {0., -0.65, 0., 0., 0.};
world.addTendonCoupling("ratio", b, a, relation); // delta B = -0.65 * delta A.
raisim::Tendon::Drive drive;
drive.targetLength = a->getReferenceLength() + 0.2;
drive.positionGain = 100.;
drive.velocityGain = 20.;
a->setDrive(drive);
advance(world, 3000);
a->updateGeometry();
b->updateGeometry();
const double deltaA = a->getLength() - a->getReferenceLength();
const double deltaB = b->getLength() - b->getReferenceLength();
std::cout << "delta_A=" << deltaA << " delta_B=" << deltaB
<< " residual=" << deltaB + 0.65 * deltaA << '\n';
Argument order matters: the first tendon is the dependent coordinate. This
example passes b first and a second. The coefficient array is constant
term first; {0., 2., 0.1, 0., 0.} instead means
deltaB = 2*deltaA + 0.1*deltaA*deltaA. Passing nullptr as the second tendon
locks the first relative to its creation reference plus the constant term.
Inspect, style, export, and reload
Run the export case with an absolute output filename. The file preserves
attachment names, appearance, limits, and the current force command. Add
<stdexcept> when copying the fragment and supply exportPath as a string.
raisim::World world;
world.setTimeStep(0.001);
world.setGravity({0., 0., -9.81});
auto* load = world.addSphere(0.05, 1.0);
load->setName("load"); // Native XML resolves attachments by object name.
load->setPosition(0., 0., 0.8);
raisim::Tendon::Properties p;
p.upperLimit = 1.2;
auto* cable = world.addSpatialTendon("saved_cable", {
Path::via({nullptr, 0, {0., 0., 2.}}), Path::via({load})}, p);
cable->setTension(2.);
auto appearance = cable->getProperties();
appearance.width = 0.012; // Visual radius, not collision thickness.
appearance.color = {0.05, 0.9, 0.75, 1.};
cable->setProperties(appearance);
cable->updateGeometry(true);
for (const auto& segment : cable->getVisualSegments())
std::cout << "segment " << segment.start.e().transpose()
<< " -> " << segment.end.e().transpose() << '\n';
world.exportToXml(exportPath); // An absolute output filename supplied by main.
raisim::World restored(exportPath);
auto* restoredCable = restored.getTendon("saved_cable");
if (!restoredCable) throw std::runtime_error("missing tendon after reload");
advance(restored, 100);
restoredCable->updateGeometry();
std::cout << "reloaded_length=" << restoredCable->getLength()
<< " reloaded_force=" << restoredCable->getActuationForce() << '\n';
No custom drawing loop is required in Rayrai. Geometry refresh with true is
used here only to inspect segment coordinates. Alpha zero hides the cable while
retaining its physics; setEnabled(false) disables both its physics and its
drawing. Export preserves the current command, not a future C++ control schedule.
See Tendon API and model files for XML attributes and embedded-URDF sidecars.
Straight connections through the tendon API
Run tendon_recipes straight to execute these three independent worlds.
Each connection is a two-site spatial tendon. Here a static sphere supplies the
anchor; a null-object site can instead attach directly to the world frame.
Equal limits lock separation in both directions and stop the initial outward velocity:
raisim::World world;
world.setTimeStep(0.001);
world.setGravity({0., 0., 0.});
auto* anchor = world.addSphere(0.02, 1.0);
anchor->setBodyType(raisim::BodyType::STATIC);
anchor->setPosition(0., 0., 0.);
auto* load = world.addSphere(0.05, 1.0);
load->setPosition(1., 0., 0.);
load->setLinearVelocity({1., 0., 0.});
raisim::Tendon::Properties p;
p.lowerLimit = p.upperLimit = 1.0;
auto* tendon = world.addSpatialTendon("lock", {Path::via({anchor}), Path::via({load})}, p);
advance(world, 100);
tendon->updateGeometry();
std::cout << "target=" << tendon->getProperties().upperLimit
<< " current=" << tendon->getLength() << '\n';
getLength() is the current coordinate; getProperties() holds the bounds.
A pull-only spring is slack below its rest length. The implicit update with
0.1 m initial stretch and 100 N/m stiffness gives approximately 10 N of tension:
raisim::World world;
world.setTimeStep(0.001);
world.setGravity({0., 0., 0.});
auto* anchor = world.addSphere(0.02, 1.0);
anchor->setBodyType(raisim::BodyType::STATIC);
anchor->setPosition(0., 0., 0.);
auto* load = world.addSphere(0.05, 1.0);
load->setPosition(1.1, 0., 0.);
raisim::Tendon::Properties p;
p.springLower = 0.;
p.springUpper = 1.;
p.stiffness = 100.;
auto* tendon = world.addSpatialTendon("spring", {Path::via({anchor}), Path::via({load})}, p);
world.integrate(); // Implicit spring force is approximately 10 N initially.
std::cout << "tension=" << tendon->getTension() << '\n';
The same tendon type supports a commanded tension without a spring or limit:
raisim::World world;
world.setTimeStep(0.001);
world.setGravity({0., 0., 0.});
auto* anchor = world.addSphere(0.02, 1.0);
anchor->setBodyType(raisim::BodyType::STATIC);
anchor->setPosition(0., 0., 0.);
auto* load = world.addSphere(0.05, 1.0);
load->setPosition(1., 0., 0.);
auto* tendon = world.addSpatialTendon("actuator", {Path::via({anchor}), Path::via({load})});
tendon->setTension(4.); // Positive pulls the endpoints together.
advance(world, 100);
std::cout << "velocity_x=" << load->getLinearVelocity()[0] << '\n';
For a compression-only spring, set springLower = L and
springUpper = std::numeric_limits<double>::infinity(). For a bilateral
spring, set both endpoints to L. See Constraints for the complete
migration table from the former wire API.
Python uses the same model
raisimpy.Tendon exposes the same properties, routes, drives, and inspection
methods. Properties and drive getters return copies; apply edits with the
corresponding setter so validation and wake-up happen. The world owns tendons
and couplings; do not use their Python handles after explicit removal or after
removing an attached object.
import raisimpy as raisim
# Configure the RaiSim activation key before creating the world if needed.
world = raisim.World()
world.setTimeStep(0.001)
load = world.addSphere(0.05, 1.0)
load.setPosition(0.0, 0.0, 1.0)
Site = raisim.Tendon.Site
Path = raisim.Tendon.PathElement
properties = raisim.Tendon.Properties()
properties.upperLimit = 1.0
properties.width = 0.01
properties.color = [0.1, 0.9, 0.6, 1.0]
cable = world.addSpatialTendon("cable", [
Path.via(Site(position=[0.0, 0.0, 2.0])),
Path.via(Site(load)),
], properties)
cable.setTension(2.0)
for _ in range(1000):
world.integrate()
cable.updateGeometry()
print(cable.getLength(), cable.getTension())
properties = cable.getProperties()
properties.lowerLimit = properties.upperLimit = cable.getLength()
cable.setProperties(properties)
world.removeTendon(cable)