AFFECTIVESILICON

Forward Dynamics Model of a 5 Degree-of-Freedom Robot Arm using DynaMechs

Revision 1.0 2022-09-14
Revision 2.0 2023-10-16

DynaMechs is a dynamics library which performs the physics for chains of rigid bodies. There are many physics engines, but this one in particular looks to have been around for a long time. It's not clear to us whether or not it has been abandoned, although the library is still available to download online. It is interesting to see the source code — it was developed at a time when CPU cycles were precious, and serious efforts were made to make it as optimised as possible. We decided to try to get it working, and our initial goal with it became, to try to use it to develop a forward model for a robot arm. This forward model would be inherently more complex than the kinematics models we normally use, and we would aim to try to use it in predictive control and motor planning, to see what extra demands it would make in such scenarios.

Building DynaMechs

DynaMechs is so venerable it no longer builds with the makefiles in its source distribution. However, this is relatively easy to fix. Here are our modifications to get DynaMechs to build on our target system, Linux x86_64. We aimed to touch the source files as little as possible:-

In make.platform.linux.i686 the CXXFLAGS line becomes:-

CXXFLAGS := -DDM_DOUBLE_PRECISION -pedantic -Wpointer-arith -Wconversion -O2 -s -D_BOOL -Wall -Wuninitialized -c -fPIC -D_POSIX_C_SOURCE=199506L -I/$(OPENGLINCDIR)

The following line in the dm.h, dmGL.h and Gait.hpp files is changed from:

#if defined(WIN32) || (defined(sgi) && defined(_STANDARD_C_PLUS_PLUS)) || (defined(__GNUC__) && (__GNUC__>=2) && (__GNUC_MINOR__>=91))

to:-

#if defined(WIN32) || (defined(sgi) && defined(_STANDARD_C_PLUS_PLUS)) || (defined(__GNUC__) && (((__GNUC__>=2) && (__GNUC_MINOR__>=91)) || (__GNUC__>=3)))

(We did this because g++ was tripped up by the __GNUC_MINOR__ preprocessor definition).

In svd_linpack.cpp change:-

#include <iomanip.h>

to:-

#include <iomanip>
#include <iostream>
using namespace std;

DynaMechs defines a new type "Float" (not to be confused with "float") to allow the library to be compiled in either single or double precision. We set it to compile with double precision, but in the code their are some instances where "float" seems to be used accidentally. In dm.h we changed this line:-

float norm = sqrt(v[0]*v[0] + v[1]*v[1] + v[2]*v[2]);

to:-

Float norm = sqrt(v[0]*v[0] + v[1]*v[1] + v[2]*v[2]);

There are some instances of this in aquarobot.cpp as well.

Arm Specification

Here we take the convention that the Z-axis is the vertical axis (positive up).

Modified Denavit-Hartenberg Parameters

Each rigid body in the chain is specified in its own coordinate system (describing the origin and axis of movement). DynaMechs uses Modified Denavit-Hartenberg (MDH) parameters to specify how the coordinate system of one link is transformed to become the coordinate system of the next link; thereby connecting the links together in a chain. This requires specifying four parameters: a, alpha, d, and theta. a and d are displacements and alpha and theta are rotation angles. When using rotational joint links, as we do in this project, the a, alpha and d parameters are held fixed and the theta parameter becomes variable, allowing the link to move around that degree of freedom. It can be tricky to work with the MDH parameters to specify a linkage system; fortunately, this page explains it, and has an example of how a Mitsubishi RV-6SDL can be described using the parameters. We decided to use the parameter settings as described on that page, with some slight modifications, and the result is a fictional but hopefully plausible robot arm assembly, using the following MDH parameters:-


Joint a (m) alpha (rad) d (m) initial theta (rad) min theta (rad) max theta (rad)
R1 0.0 0.0 0.35 0.0 -∞
R2 0.085 -π/2 0.0 -1.1 -1.2 0.7
R3 0.38 0.0 0.0 0.3 0.6
R4 0.0 -π/2 0.425 0.0 π
R5 0.0 π/2 0.0 0.0 -1.1 1.1

Although the theta parameter is the variable for these joints, an initial theta was specified to pose the robot arm, and maximum and minimum thetas were determined for joint limits. DynaMechs provides a mechanism to set joint limits, and attempts to move a joint past its limits will be opposed by a damped spring equation.

Mass Properties

As the model is a dynamics model, the mass properties of the rigid bodies in the linkage system must be specified. In DynaMechs, these properties are the overall mass of the rigid body, its centre of gravity, and its inertia tensor. To simplify inertia tensor calculation, we decided to model the rigid bodies as axes-aligned cylinders of uniform density (various equations for inertia tensors can be found here). In the cases where rigid body was offset from the origin of its coordinate system, the parallel axis theorem was used to modify the tensor components. The rigid bodies for joints R1 and R2 were modelled as amalgams of two cylinders, to model the large motors that would likely be attached to those joints. The parameters used are described below ('Axis' denotes the principal axis of the cylinder):-


Joint Axis Height (m) Radius (m) Mass (kg) Inertia Tensor (kg m2) Centre of Gravity (m)
R1 Z 0.3 0.085 3.0 $$\begin{bmatrix} 0.0279 & 0.0 & 0.0 \\ 0.0 & 0.0279 & 0.0 \\ 0.0 & 0.0 & 0.0108 \end{bmatrix}$$ $$[ 0.0, 0.0, 0.0 ]$$
Y 0.14 0.05 1.5 $$\begin{bmatrix} 0.0034 & 0.0 & 0.0 \\ 0.0 & 0.0127 & 0.0 \\ 0.0 & 0.0 & 0.0142 \end{bmatrix}$$ $$[ 0.085, 0.0, 0.0 ]$$
R2 X 0.38 0.05 1.0 $$\begin{bmatrix} 0.0013 & 0.0 & 0.0 \\ 0.0 & 0.0488 & 0.0 \\ 0.0 & 0.0 & 0.0488 \end{bmatrix}$$ $$[ 0.19, 0.0, 0.0 ]$$
Z 0.1 0.04 1.0 $$\begin{bmatrix} 0.0012 & 0.0 & 0.0 \\ 0.0 & 0.1456 & 0.0 \\ 0.0 & 0.0 & 0.1452 \end{bmatrix}$$ $$[ 0.38, 0.0, 0.0 ]$$
R3 Y 0.425 0.04 1.0 $$\begin{bmatrix} 0.0606 & 0.0 & 0.0 \\ 0.0 & 0.0008 & 0.0 \\ 0.0 & 0.0 & 0.0606 \end{bmatrix}$$ $$[ 0.0, 0.2125, 0.0 ]$$
R4 Y 0.1 0.02 0.3 $$\begin{bmatrix} 0.00028 & 0.0 & 0.0 \\ 0.0 & 0.00006 & 0.0 \\ 0.0 & 0.0 & 0.00028 \end{bmatrix}$$ $$[ 0.0, 0.0, 0.0 ]$$
R5 Y 0.085 0.01 0.2 $$\begin{bmatrix} 0.00049 & 0.0 & 0.0 \\ 0.0 & 0.00001 & 0.0 \\ 0.0 & 0.0 & 0.00049 \end{bmatrix}$$ $$[ 0.0, 0.0425, 0.0 ]$$

Motor Model

DynaMechs includes a DC motor model which can be used to drive the arm joints, converting voltage into torque. The model is quite complete, incorporating parameters for brush drop, armature resistance, friction etc. In these initial experiments we decided to use a simplified set of parameters, and assume the brush drop, rotor inertia and friction are negligable. The resulting motor equations used were:-

$$\displaystyle K_{e} = K_{p}$$

$$\displaystyle V_{s} = K_{e} \times \omega + I_{a} \times R_{a}$$

$$\displaystyle T = K_{p} \times I_{a}$$

Where \(K_{p}\) is the torque constant, \(K_{e}\) is the back EMF constant, \(V_{s}\) is the voltage applied to the motor, \(\omega\) is the angular velocity, \(I_{a}\) is the armature current, \(R_{a}\) is the armature resistance and \(T\) is the output torque.

Ultimately to simplify even further we set \(R_{a}\) to unity, requiring us to specify only \(K_{p}\) for the joint motors, which we did as follows:-


Motor for joint \(K_{p}\)
R1 10.0
R2 10.0
R3 10.0
R4 1.0
R5 1.0

When and if we come to construct a robot arm of this nature we would of course be able to supply the other parameters from the motor data sheets. Although the motor model is greatly simplified, it does present interesting effects, for example if no source voltage is supplied then the robot joints do not just immediately collapse under gravity as the resultant back EMF produces a torque opposing the motion.

Simulating the Arm

Although the model of the arm is itself mathematical, it is always useful to see what it looks like and how it operates in a simulation. To that end we produced a simulation of the robot in the same format as the other examples provided by DynaMechs, which use the GLUT library to handle display and to process mouse and keyboard input. The OpenGL visualisation was constructed using cylinders matching the MDH parameters and shape properties as described above. The below image is a screenshot from the program.

To prevent the robot arm from collapsing under gravity, simple PD controllers were put on the arm joints to provide motor voltages to keep the joints in their target poses.

Constructing the Forward Model

DynaMechs has several examples of simulated linkage systems in its testdm/ directory. However, to make the forward model, it's not quite enough just to be able to run a simulation loop of a robot; The forward model function has to be constructed so that any robot state and joint forces can be passed in, and the next state (after a specified timestep) be returned. The robot arm state and joint inputs have to be set, the integration performed, and the resultant arm state extracted to be returned to the caller.

State Definition

As the robot arm has five joints each of one degree of freedom, and the dynamics model is second order, the state vector has at least fifteen entries; one each of the position, velocity and acceleration for each of the five joints. We then define three additional states — the (x,y,z) coordinate of an end-effector location (we do not model an end-effector in this report). We do this since it is likely that it is the end-effector location that we ultimately will be wanting to control. DynaMechs provides a function to run the forward kinematics for a linkage system and thereby get the transformation matrix to calculate the end-effector location in the world frame (the location is show as a cross-hair in the image above).

The motors also have internal state information which has to be preserved for a complete representation of the system; specifically, whether the motor is in stiction, and to assist in computing that, the joint velocity at the previous time step. For this robot there are therefore ten extra states, two for each joint, specifying the stiction and the previous velocity. This results in a final state vector which is 28 doubles in size.

The joint input is a vector of five doubles, each specifying a voltage (positive or negative) to apply to one of the five joint motors.

Implementation

At this point, we had to modify the library slightly to extract internal state from DynaMechs. DynaMechs includes a function to return a joint state in terms of its position and velocity, but not its acceleration, which is required for the integration. This is probably because the accelerations are directly computable from the forces in the joints, however it was still much more convenient for us to pull that internal state out of the DynaMechs engine. To do that we added a new method 'AS_getState' to the dmMDHLink.cpp and dmMDHLink.h files.

The following declaration was added to the public section of the 'dmMDHLink' class in dmMDHLink.hpp:-

void AS_getState(Float q[], Float qd[], Float qdd[]) const;

And the body of the method was put in the dmMDHLink.cpp:-

void dmMDHLink::AS_getState(Float q[], Float qd[], Float qdd[]) const
{
   qdd[0] = m_qdd;
   qd[0] = m_qd;
   q[0] = getJointPos();
}

The 'AS_getState' method could then be called to retrieve the position, velocity and acceleration for a joint. Similar simple methods were added in order to retrieve and restore the motor state.

Finally, the implemented code was arranged to create a shared library so that the robot arm forward model could be accessed from other code. The code for this library is below.

Testing

When the forward model is looped with the next state output becoming the current state input, this should result in the same state sequence being generated as when performing a normal simulation of the robot arm, so long as the same motor voltages and timestep are used between the two. To confirm this, we implemented a 'recording' function in the robot simulation, which would store the start state and the voltages applied to the joint motors for a period of time thereafter; the forward model would then be run from the same start state and with the same voltages and timestep applied, and the final states from both modes compared. This proved at least that the forward model could replicate the usual DynaMechs simulation.

We were interested to make the forward model callable from a higher-level language so we could easily perform experiments and log results. We used the FFI::Platypus Perl module to load the library and call the forward model function from Perl. As an experiment, we decided to try to compute the P and I parameters for a PI controller to make the robot arm keep a pose, against gravity. The PI controller would work on the error in the difference from the target pose to calculate voltages to apply to the robot arm's (simulated) motors.

To do this we tried a model-based optimization approach which used the Nelder and Mead Simplex method on an objective function. The Math::Amoeba Perl module provides the Nelder and Mead Simplex, so with a bit of experimentation it was straightforward to put a script together to do this (see here). The objective function is a short simulation of the robot arm over a few seconds, which accumulates the absolute error (deviation) from the target pose. The objective is to minimize this error by selecting candidate parameters. The P parameters are determined first, then the I parameters.

The script executes fairly quickly on our computers, but would likely be prohibitively slow if the arm dynamics themselves were implemented in Perl. The parameters produced were plugged back into the main simulation and do indeed keep the arm stable for the given target pose, however if the target pose is changed significantly the arm does blow up, which is to be expected as the forces on the arm will be very different in different positions. A more comprehensive approach, or at the very least good D parameters, would be needed to control the arm in all situations.

Conclusions and Further Work

For this project we used DynaMechs to build a forward model of a five degree-of-freedom robot arm. We do not have a physical robot arm currently to model, so this robot arm is by necessity somewhat fictional, but hopefully not implausible. The dynamics calculations involved in the model are significantly more involved than with a kinematics model, so we are interested to see how well it can be used in different types of predictive controllers, and for motor planning.

Now that we have our own build of DynaMechs, it is tempting to continue to add to it. DynaMechs provides explicit Euler and some Runge-Kutta integrators. This current model uses explicit Euler, and therefore requires a relatively small timestep to prevent explosions — implementing a Verlet method might provide a good trade-off in terms of stability, speed and accuracy. There is also a contact model in DynaMechs for terrain heightmaps, but not for the rigid bodies themselves, although this should be relatively straightforward to add and potentially allow for the possibility of modelling a gripper or other type of end effector for the arm to manipulate objects.

Source Code - Library Header


/* robot arm dynamics model */

#ifndef AS_ROBOTARM_H
#define AS_ROBOTARM_H

#include <dm.h>
#include <dmEnvironment.hpp>
#include <dmObject.hpp>
#include <dmSystem.hpp>
#include <dmArticulation.hpp>
#include <dmLink.hpp>
#include <dmMDHLink.hpp>
#include <dmRevoluteLink.hpp>
#include <dmRevDCMotor.hpp>

class RobotArm
{

    public:

        RobotArm();
        ~RobotArm();

        int _size();        // necessary for FFI::Platypus

        void forward_model( Float cur_state[], Float motor_voltages[], Float next_state[], Float delta_t );

    	dmArticulation *Arm;
	    dmRevoluteLink *r1, *r2, *r3, *r4, *r5;
	
	    enum Links { R1, R2, R3, R4, R5, NUM_DOFS };

        struct MDH_params {
	        Float a;
	        Float alpha;
	        Float d;
	        Float theta;
	        Float min_theta;
	        Float max_theta;
        };

        // default joint parameters on construction
	    MDH_params MDH[NUM_DOFS] = { 	{ 0.0, 0.0, 0.35, 0.0, -99999999.99, 99999999.99 },		// R1
									    { 0.085, -M_PI/2.0, 0.0, -1.1, -1.2, 0.7 }, 			// R2
									    { 0.38, 0.0, 0.0, 0.3, -M_PI, 0.6 },					// R3
									    { 0.0, -M_PI/2.0, 0.425, 0.0, -M_PI, M_PI },			// R4
									    { 0.0, M_PI/2.0, 0.0, 0.0, -1.1, 1.1 } };				// R5

};

#endif
				

Source Code - Library Implementation


// Robot arm implementation
/*
    g++ -c robot_arm.cpp -I/usr/local/include/AS-DynaMechs/ -DDM_DOUBLE_PRECISION -D__sgi -pedantic -Wpointer-arith -O3 -s -D_BOOL -Wuninitialized -c -fPIC -D_POSIX_C_SOURCE=199506L
	Link:- g++ -shared -o robot_arm.so robot_arm.o -lASdm -fPIC -lm -ldl -lrt
*/

#include "robot_arm.hpp"

RobotArm::RobotArm()
{

	CartesianVector gravity = { 0.0, 0.0, (Float)-9.81};
	dmEnvironment *environment = new dmEnvironment();
	dmEnvironment::setEnvironment(environment);
	environment->setGravity(gravity);

    Arm = new dmArticulation;	// static base for arm
    
    CartesianVector pos = {0.0, 0.0, 0};
    Quaternion quat = {0.0, 0.0, 0.0, 1.0};
   
    Arm->setRefSystem(quat, pos);

	/**** R1 ******/
	r1 = new dmRevoluteLink();	

	// r1 beam approximated as Z cylinder, radius: 0.085m height: 0.3m 
	Float r1_beam_radius = 0.085;		// radius of cylinder
	Float r1_beam_height = 0.3;			// height of cylinder
	Float r1_beam_mass = 3.0; // mass of cylinder
	Float r1_beam_Ixx, r1_beam_Iyy, r1_beam_Izz; // calculate inertia tensor based off solid cylinder equations (z axis aligned)
	r1_beam_Ixx = r1_beam_Iyy = (r1_beam_mass / 12.0) * ((3*r1_beam_radius*r1_beam_radius) + (r1_beam_height*r1_beam_height));
	r1_beam_Izz = 0.5 * r1_beam_mass * r1_beam_radius * r1_beam_radius;  

	// motor for r2 approximated as another cylinder, Y axis aligned
	Float r1_motor_radius = 0.05;	// "r1_motor" is actually modelling the motor for moving r2
	Float r1_motor_height = 0.14;
	Float r1_motor_mass = 1.5;
	Float r1_motor_CoG_X = 0.085;
	Float r1_motor_Ixx, r1_motor_Iyy, r1_motor_Izz;
	r1_motor_Ixx = r1_motor_Izz = (r1_motor_mass / 12.0) * ((3*r1_motor_radius*r1_motor_radius) + (r1_motor_height*r1_motor_height));
	r1_motor_Iyy = 0.5 * r1_motor_mass * r1_motor_radius * r1_motor_radius;
	Float r1_motor_PAT = r1_motor_mass * r1_motor_CoG_X * r1_motor_CoG_X;	// calc addition according to parallel axis theorem
	r1_motor_Iyy += r1_motor_PAT;	// because motor cylinder is not centred on 0,0,0
	r1_motor_Izz += r1_motor_PAT;

	// combine the inertia parameters into the tensor
	CartesianTensor r1_combined_I_bar = {	{r1_beam_Ixx+r1_motor_Ixx,   0.0,			0.0},
			                   		        {0.0,          r1_beam_Iyy+r1_motor_Iyy,	0.0},
                            				{0.0,          0.0,			r1_beam_Izz+r1_motor_Izz}	};

	// combined mass
	Float r1_combined_mass = r1_beam_mass + r1_motor_mass;

	// calculate the combined centre of gravity
	Float r1_X_bar = (r1_beam_mass*0.0 + r1_motor_mass*r1_motor_CoG_X) / r1_combined_mass;
	CartesianVector r1_combined_CoG = { r1_X_bar, 0.0, 0.0 };

	// set all the parameters for the link
	r1->setInertiaParameters(r1_combined_mass, r1_combined_I_bar, r1_combined_CoG);
    r1->setMDHParameters(MDH[R1].a, MDH[R1].alpha, MDH[R1].d, MDH[R1].theta);
    r1->setJointFriction( 0.3 );
    r1->setJointLimits(MDH[R1].min_theta, MDH[R1].max_theta, 50, 5);

	dmRevDCMotor *actuator_1 = new dmRevDCMotor();
	actuator_1->setParameters(10.0, 10.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0);    // NOTE: have to enter non-zero values for armature resistance and half drop value to avoid div by zero error
	r1->setActuator(actuator_1);

    Arm->addLink(r1, NULL);

	/**** R2 ******/
	r2 = new dmRevoluteLink();

	// r2 beam approximated as X cylinder, radius: 0.05m height: 0.38m 
	Float r2_beam_radius = 0.05;
	Float r2_beam_height = 0.38;
	Float r2_beam_CoG_X = r2_beam_height / 2.0; // starts at x=0; so CoG is offset to halfway along length (height) (which is along X)
	Float r2_beam_mass = 1.0;
	Float r2_beam_Ixx, r2_beam_Iyy, r2_beam_Izz;		
	r2_beam_Iyy = r2_beam_Izz = (r2_beam_mass / 12.0) * ((3*r2_beam_radius*r2_beam_radius) + (r2_beam_height*r2_beam_height));
	r2_beam_Ixx = 0.5 * r2_beam_mass * r2_beam_radius * r2_beam_radius;
	Float r2_beam_PAT = r2_beam_mass * r2_beam_CoG_X * r2_beam_CoG_X;
	r2_beam_Iyy += r2_beam_PAT;	
	r2_beam_Izz += r2_beam_PAT;

	// motor for r3 approximated as Z cylinder
	Float r2_motor_radius = 0.04;
	Float r2_motor_height = 0.1;
	Float r2_motor_mass = 1.0;
	Float r2_motor_CoG_X = 0.38;	// motor for r3 is at the end of r2 beam 
	Float r2_motor_Ixx, r2_motor_Iyy, r2_motor_Izz;
	r2_motor_Ixx = r2_motor_Iyy = (r2_motor_mass / 12.0) * ((3*r2_motor_radius*r2_motor_radius) + (r2_motor_height*r2_motor_height));
	r2_motor_Izz = 0.5 * r2_motor_mass * r2_motor_radius * r2_motor_radius;
	Float r2_motor_PAT = r2_motor_mass * r2_motor_CoG_X * r2_motor_CoG_X;
	r2_motor_Iyy += r2_motor_PAT;
	r2_motor_Izz += r2_motor_PAT;

	CartesianTensor r2_combined_I_bar = {	{r2_beam_Ixx+r2_motor_Ixx,   0.0,			0.0},
			                   		        {0.0,          r2_beam_Iyy+r2_motor_Iyy,	0.0},
                            				{0.0,          0.0,			r2_beam_Izz+r2_motor_Izz}	};

	Float r2_combined_mass = r2_beam_mass + r2_motor_mass;

	Float r2_X_bar = (r2_beam_mass*r2_beam_CoG_X + r2_motor_mass*r2_motor_CoG_X) / r2_combined_mass;
	CartesianVector r2_combined_CoG = { r2_X_bar, 0.0, 0.0 };

    r2->setInertiaParameters(r2_combined_mass, r2_combined_I_bar, r2_combined_CoG);
    r2->setMDHParameters(MDH[R2].a, MDH[R2].alpha, MDH[R2].d, MDH[R2].theta);
    r2->setJointFriction( 0.3 );
    r2->setJointLimits(MDH[R2].min_theta, MDH[R2].max_theta, 50, 5);

	dmRevDCMotor *actuator_2 = new dmRevDCMotor();
	actuator_2->setParameters(10.0, 10.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0);
	r2->setActuator(actuator_2);

    Arm->addLink(r2, r1);

	/**** R3 ******/
	r3 = new dmRevoluteLink();

	// r3 beam approximated as Y cylinder, radius: 0.04m height: 0.425m 
	Float r3_beam_radius = 0.04;
	Float r3_beam_height = 0.425;
	Float r3_beam_CoG_Y = r3_beam_height / 2.0;
	Float r3_beam_mass = 1.0;
    CartesianVector r3_beam_CoG = { 0.0, r3_beam_CoG_Y, 0.0 };
	Float r3_beam_Ixx, r3_beam_Iyy, r3_beam_Izz;
	r3_beam_Ixx = r3_beam_Izz = (r3_beam_mass / 12.0) * ((3*r3_beam_radius*r3_beam_radius) + (r3_beam_height*r3_beam_height));
	r3_beam_Iyy = 0.5 * r3_beam_mass * r3_beam_radius * r3_beam_radius;
	Float r3_beam_PAT = r3_beam_mass * r3_beam_CoG_Y * r3_beam_CoG_Y;
	r3_beam_Ixx += r3_beam_PAT;
	r3_beam_Izz += r3_beam_PAT;
    CartesianTensor r3_beam_I_bar = {	{r3_beam_Ixx,   0.0,		0.0},
			                            {0.0,          r3_beam_Iyy,	0.0},
                            			{0.0,          0.0,			r3_beam_Izz}	};

    r3->setInertiaParameters(r3_beam_mass, r3_beam_I_bar, r3_beam_CoG);    
    r3->setMDHParameters(MDH[R3].a, MDH[R3].alpha, MDH[R3].d, MDH[R3].theta);
    r3->setJointFriction( 0.3 );
    r3->setJointLimits(MDH[R3].min_theta, MDH[R3].max_theta, 50, 5);

	dmRevDCMotor *actuator_3 = new dmRevDCMotor();
	actuator_3->setParameters(10.0, 10.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0);
	r3->setActuator(actuator_3);

    Arm->addLink(r3, r2);

	/**** R4 ******/
	r4 = new dmRevoluteLink();

	// r4 beam approximated as Y cylinder, radius: 0.02m height: 0.1m 
	Float r4_beam_radius = 0.02;
	Float r4_beam_height = 0.1;
	Float r4_beam_mass = 0.3;
    CartesianVector r4_beam_CoG = { 0.0, 0.0, 0.0 };
	Float r4_beam_Ixx, r4_beam_Iyy, r4_beam_Izz;
	r4_beam_Ixx = r4_beam_Izz = (r4_beam_mass / 12.0) * ((3*r4_beam_radius*r4_beam_radius) + (r4_beam_height*r4_beam_height));
	r4_beam_Iyy = 0.5 * r4_beam_mass * r4_beam_radius * r4_beam_radius;
    CartesianTensor r4_beam_I_bar = {	{r4_beam_Ixx,   0.0,		0.0},
			                            {0.0,          r4_beam_Iyy,	0.0},
                            			{0.0,          0.0,			r4_beam_Izz}	};

    r4->setInertiaParameters(r4_beam_mass, r4_beam_I_bar, r4_beam_CoG);
    r4->setMDHParameters(MDH[R4].a, MDH[R4].alpha, MDH[R4].d, MDH[R4].theta);
    r4->setJointFriction( 0.3 );
    r4->setJointLimits(MDH[R4].min_theta, MDH[R4].max_theta, 50, 5);

	dmRevDCMotor *actuator_4 = new dmRevDCMotor();
	actuator_4->setParameters(1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0);
	r4->setActuator(actuator_4);

    Arm->addLink(r4, r3);

	/**** R5 ******/
	r5 = new dmRevoluteLink();

	// r5 beam approximated as Y cylinder, radius: 0.01m height: 0.085m 
	Float r5_beam_radius = 0.01;
	Float r5_beam_height = 0.085;
	Float r5_beam_CoG_Y = r5_beam_height / 2.0;
	Float r5_beam_mass = 0.2;
    CartesianVector r5_beam_CoG = { 0.0, r5_beam_CoG_Y, 0.0 };
	Float r5_beam_Ixx, r5_beam_Iyy, r5_beam_Izz;
	r5_beam_Ixx = r5_beam_Izz = (r5_beam_mass / 12.0) * ((3*r5_beam_radius*r5_beam_radius) + (r5_beam_height*r5_beam_height));
	r5_beam_Iyy = 0.5 * r5_beam_mass * r5_beam_radius * r5_beam_radius;
	Float r5_beam_PAT = r5_beam_mass * r5_beam_CoG_Y * r5_beam_CoG_Y;
	r5_beam_Ixx += r5_beam_PAT;
	r5_beam_Izz += r5_beam_PAT;
    CartesianTensor r5_beam_I_bar = {	{r5_beam_Ixx,   0.0,		0.0},
			                            {0.0,          r5_beam_Iyy,	0.0},
                            			{0.0,          0.0,			r5_beam_Izz}	};

    r5->setInertiaParameters(r5_beam_mass, r5_beam_I_bar, r5_beam_CoG);    
    r5->setMDHParameters(MDH[R5].a, MDH[R5].alpha, MDH[R5].d, MDH[R5].theta);
    r5->setJointFriction( 0.3 );

	dmRevDCMotor *actuator_5 = new dmRevDCMotor();
	actuator_5->setParameters(1.0, 1.0, 1.0, 0.0, 0.0, 0.0, 0.0, 1.0);
	r5->setActuator(actuator_5);

    Arm->addLink(r5, r4);

    // done arm construction
}

RobotArm::~RobotArm()
{
}

/* 
Forward Model - get the next state from a current state after a timestep delta_t 
Do not use if trying to run the same instance of the robot as a simulation elsewhere as this completely borks the internal state.
state vector:- 5 Floats for joint position (R1 to R5), 5 Floats for joint velocity, 5 Floats for the acceleration, 3 Floats for the world coordinates of the "end effector".
Then in addition for motor internal states, 5 pairs of Floats for stiction flag and previous velocity. 
motor_voltages is 5 Floats for the voltages on the motors in the joints R1 to R5.
*/
void RobotArm::forward_model( Float cur_state[], Float motor_voltages[], Float next_state[], Float delta_t ) {

	unsigned int i;
	Float m_qy[2*NUM_DOFS], m_qdy[2*NUM_DOFS];

	// 1. copy input state vector into integrator vars
	for (i = 0; i < 2*NUM_DOFS; i++) {
		m_qy[i] = cur_state[i];				// position and velocity
		m_qdy[i] = cur_state[i+NUM_DOFS];	// velocity and acceleration
	}

	// 2. Set the state of the arm from these vars
    Arm->setState( &m_qy[0], &m_qy[NUM_DOFS] );

    // 2.5 Also the motor state
    r1->getActuator()->setStictionFlag( (bool) cur_state[18] );
    r1->getActuator()->setPrevVel( cur_state[19] );
    r2->getActuator()->setStictionFlag( (bool) cur_state[20] );
    r2->getActuator()->setPrevVel( cur_state[21] );
    r3->getActuator()->setStictionFlag( (bool) cur_state[22] );
    r3->getActuator()->setPrevVel( cur_state[23] );
    r4->getActuator()->setStictionFlag( (bool) cur_state[24] );
    r4->getActuator()->setPrevVel( cur_state[25] );
    r5->getActuator()->setStictionFlag( (bool) cur_state[26] );
    r5->getActuator()->setPrevVel( cur_state[27] );

	// 3. Set the joint inputs
	r1->setJointInput( &motor_voltages[R1] );
	r2->setJointInput( &motor_voltages[R2] );
	r3->setJointInput( &motor_voltages[R3] );
	r4->setJointInput( &motor_voltages[R4] );
	r5->setJointInput( &motor_voltages[R5] );

	// 4. Integrate. Explicit Euler
	for (i = 0; i < 2*NUM_DOFS; i++) {
		m_qy[i] += delta_t*m_qdy[i];
	}

	// 5. Run the dynamics to update states
    Arm->dynamics( m_qy, m_qdy );

	// 6. Copy to the next state vector
	// position and velocity
	for (i = 0; i < 2*NUM_DOFS; i++) {
		next_state[i] = m_qy[i];
	}
	// acceleration
	for (i = 0; i < NUM_DOFS; i++) {
		next_state[i+(2*NUM_DOFS)] = m_qdy[i+NUM_DOFS];
	}

	// 7. Compute the new end effector location
	HomogeneousTransformationMatrix mm;
	Arm->forwardKinematics(R5, mm);	

	Float p[4] = { 0.0, 0.1, 0.0, 1.0 };
	Float ee[4];

	ee[0] = mm[0][0]*p[0] + mm[0][1]*p[1] + mm[0][2]*p[2] + mm[0][3]*p[3];
    ee[1] = mm[1][0]*p[0] + mm[1][1]*p[1] + mm[1][2]*p[2] + mm[1][3]*p[3];
    ee[2] = mm[2][0]*p[0] + mm[2][1]*p[1] + mm[2][2]*p[2] + mm[2][3]*p[3];
    ee[3] = mm[3][0]*p[0] + mm[3][1]*p[1] + mm[3][2]*p[2] + mm[3][3]*p[3];

    next_state[15] = ee[0];
    next_state[16] = ee[1];
    next_state[17] = ee[2];

    // 8. Also get the new motor state
    next_state[18+0] = (Float) r1->getActuator()->getStictionFlag();
    next_state[18+1] = r1->getActuator()->getPrevVel();
    next_state[18+2] = (Float) r2->getActuator()->getStictionFlag();
    next_state[18+3] = r2->getActuator()->getPrevVel();
    next_state[18+4] = (Float) r3->getActuator()->getStictionFlag();
    next_state[18+5] = r3->getActuator()->getPrevVel();
    next_state[18+6] = (Float) r4->getActuator()->getStictionFlag();
    next_state[18+7] = r4->getActuator()->getPrevVel();
    next_state[18+8] = (Float) r5->getActuator()->getStictionFlag();
    next_state[18+9] = r5->getActuator()->getPrevVel();

}

// this little function needed for FFI::Platypus
int RobotArm::_size()
{
    return sizeof(RobotArm);
}

				

Source Code - Perl FFI


# Attempt to discover good parameters for a PI controller for the robot arm using model-based optimisation with the Nelder and Mead Simplex
use v5.16;

package RobotArm;
# Use FFI::Platypus to access the robot arm constructor and forward model in the shared library

use FFI::Platypus 2.00;
use FFI::Platypus::Memory qw( malloc free );

my $ffi = FFI::Platypus->new( api => 2 );
$ffi->lang('CPP');
$ffi->lib('./robot_arm.so');

$ffi->custom_type( RobotArm => {
    native_type => 'opaque',
    perl_to_native => sub { ${ $_[0] } },
    native_to_perl => sub { bless \$_[0], 'RobotArm' },
});

$ffi->attach( [ 'RobotArm::RobotArm()'     => '_new'     ] => ['RobotArm']  => 'void' );
$ffi->attach( [ 'RobotArm::~RobotArm()'    => '_DESTROY' ] => ['RobotArm']  => 'void' );
# Use FFI::ExtractSymbols to discover symbol names in the library if necessary
$ffi->attach( [ '_ZN8RobotArm13forward_modelEPdS0_S0_d' => 'forward_model' ] => ['RobotArm', 'double[]', 'double[]', 'double[]', 'double'] => 'void' );

my $size = $ffi->function('RobotArm::_size()' => [] => 'int')->call;

sub new
{
    my($class) = @_;
    my $ptr = malloc $size;
    my $self = bless \$ptr, $class;
    _new($self);
    $self;
}

sub DESTROY
{
    my($self) = @_;
    _DESTROY($self);
    free($$self);
}

package main;
# Use Perl's list functions to avoid cluttering up with lots of inner loops
use List::Util qw(any sum0);
use List::MoreUtils qw(pairwise);
use Math::Amoeba qw(MinimiseND);

# construct the robot arm
my $RobotArm = RobotArm->new;

# in this we are only going to target joints R2, R3, and R5; as gravity will not affect R1 and R4.
# however this vector is the correct initial pose for all the five joints
my @target_pose = (0.0, -1.1, 0.3, 0.0, 0.0);   

# First define and run the function for determining the proportional parameters for the controller.
# Accumulates and returns the absolute error across joints R2, R3 and R5 for a period of simulation time.
# The idea is to find P parameters that minimize that error sum
sub ObjectiveFunctionP {

    my @p_params = @_;      # the candidate coefficients for the proportional part of the controller

    # initialise the arrays to be passed to the forward model for simulation
    my @cur_state = (0.0) x 28; 
    @cur_state[0..4] = @target_pose; # setup initial pose, we want to maintain this
    my @motor_command = (0.0) x 5;
    my @next_state = (0.0) x 28;
    my $dt = 0.0001;                # timestep for simulation (s)
    my $num_iterations = 3.0 / $dt; # number of iterations is duration of sim (s) over timestep
    my $sum_abs_error = 0;          # accumulate absolute error across all joints in question over the course of the simulation

    foreach my $step (1..$num_iterations) {

        # call the forward model in the shared library
        $RobotArm->forward_model( \@cur_state, \@motor_command, \@next_state, $dt );

        # accumulate the absolute error across joints R2, R3 and R5
        my @diffs = map { $target_pose[$_] - $next_state[$_] } (1,2,4);
        my $step_error = sum0 map { abs($_) } @diffs;
        return 9999999 if $step_error > 100;    # generated forces are so great they have blown up the arm, no point continuing

        $sum_abs_error += $step_error;

        # execute proportional controllers to generate motor voltages for joints R2, R3 and R5
        @motor_command[1,2,4] = pairwise { $a * $b } @p_params, @diffs;

        # copy next state to current state to run simulation forward in the loop        
        $cur_state[$_] = $next_state[$_] for (0..27);        
    }    

    return $sum_abs_error;
}

# setup and run the simplex on the above function to find "good" P parameters
my @p_guess = (6.0, 5.0, 1.0);   # initial guesses at the P coefficients [turn out not to be great]
my @p_scale = (0.1, 0.1, 0.1);
my ($p, $y1) = MinimiseND(\@p_guess, \@p_scale, \&ObjectiveFunctionP, 0.0001, 250);    # execute the simplex

say "P = (", join(',', @{$p}), ") = $y1";   # display the result

my @p_coeff = @{$p};    # assume we have found some decent P params, set them and proceed to finding I params

# Define the function for discovering I parameters. Makes use of the P parameters previously determined
sub ObjectiveFunctionI {

    my @i_params = @_;      # the candidate coefficients for the integral part of the controller

    # initialise the arrays to be passed to the forward model
    my @cur_state = (0.0) x 28; 
    @cur_state[0..4] = @target_pose;
    my @motor_command = (0.0) x 5;
    my @next_state = (0.0) x 28;
    my $dt = 0.0001;
    my $num_iterations = 3.0 / $dt;
    my $sum_abs_error = 0;
    my @cumulative = (0.0) x 3;

    foreach my $step (1..$num_iterations) {

        # call the forward model in the shared library
        $RobotArm->forward_model( \@cur_state, \@motor_command, \@next_state, $dt );

        # errors in positions for joints R2, R3, and R5
        my @diffs = map { $target_pose[$_] - $next_state[$_] } (1,2,4);

        # sum absolute error, break out of function if arm blown up
        my $step_error = sum0 map { abs($_) } @diffs;
        return 9999999 if $step_error > 100;
        $sum_abs_error += $step_error;

        # for integral component must keep running total of the diffs
        $cumulative[$_] += $diffs[$_] for (0..2);

        my @p_c = pairwise { $a * $b } @p_coeff, @diffs;            # execute proportional component of control (determined in previous simplex)
        my @i_c = pairwise { $a * $b } @i_params, @cumulative;      # execute integral component (being worked on in this function)
        @motor_command[1,2,4] = pairwise { $a + $b } @p_c, @i_c;    # sum them for final motor voltage

        # copy next state to current state to run simulation forward in the loop        
        $cur_state[$_] = $next_state[$_] for (0..27);        
    }    

    return $sum_abs_error;
}

# setup and run simplex to find "good" I parameters. These are often much smaller than the P paramters (if the P are any good)
my @i_guess = (0.1, 0.1, 0.1);   # initial guesses at the I coefficients
my @i_scale = (0.01, 0.01, 0.01);
my ($i, $y2) = MinimiseND(\@i_guess, \@i_scale, \&ObjectiveFunctionI, 0.00001, 250);

say "I = (", join(',', @{$i}), ") = $y2";

# Output here is:
# P = (1688.61035631972,5529.51939064412,5578.2466209947) = 16.3888758427019
# I = (11.7143467458887,4.57248153380727,0.451883276796821) = 0.305710199012247

exit;