Part II: Dynamic Optimization of Control Strategy

To get our models to "right themselves", we control their actuation and tweak this control until we obtain a flipping motion. We have an optimization algorithm take care of the tweaking for us. During each step of the optimization, the algorithm tweaks the control in some intelligent way, we run a forward dynamic simulation with the tweaked controls, and we finally check if the cat has flipped. The tweaking continues until a flip is achieved.

This procedure of algorithmically tweaking control inputs, running a forward dynamic simulation, then judging how well the model/simulation achieved some objective is called dynamic optimization. It is the way that biomechanists would prefer to study human movement (see Sky Higher and Miller, 2012 for just two of many examples) and is commonly viewed as the alternative to tracking simulations. The models used for studying such motions are complex, and so it takes a long time to run a dynamic optimization (days to weeks). For cat flipping, however, an optimization takes only an hour or two. Thus, it's an ideal problem for first learning dynamic optimization.

Code Organization

First, we present an overview of the 2 source files we use for the optimization. The file optimize.cpp creates an executable that (mostly just) creates the necessary objects and runs the optimization. The code block below displays the four most important lines from this file, which show how we connect the various pieces of the optimization. Look at the next code block, a skeleton of the second file, FlippinFelinesOptimizerSystem.h. It is where we define the FlippinFelinesOptimizerTool and FlippinFelinesOptimizerSystem classes.

optimize.cpp (incomplete snippet)
int main(int argc, char * argv[])
{
    // ...

    // A class we made to read inputs to the optimization via an XML file.
    OpenSim::FlippinFelinesOptimizerTool tool(toolSetupFile);
    // ...

    // Our subclass of SimTK::OptimizerSystem; pass to it our inputs.
    FlippinFelinesOptimizerSystem sys(tool);
    // ...

    // The object that runs an optimization algorithm on the system we created.
    SimTK::Optimizer opt(sys, SimTK::BestAvailable);
    // ...

    // This function is what actually runs the optimizer.
    double f = opt.optimize(initParameters);

};

To run any optimization, it's necessary to use SimTK::Optimizer and a class like FlippinFelinesOptimizerSystem. However, a class like FlippinFelinesOptimizerTool is not necessary; it's just something we've done to make it easier to run these optimizations.

FlippinFelinesOptimizerSystem.h (incomplete snippet)
namespace OpenSim {

// Manages inputs to the optimization via OpenSim's XML abilities.
class FlippinFelinesOptimizerTool : public Object {
    // ********** PART B   **********
};

}  // end namespace OpenSim

// Finds a control input that achieves flip, as determined by objective function.
class FlippinFelinesOptimizerSystem : public SimTK::OptimizerSystem {
    // ...

    FlippinFelinesOptimizerSystem(OpenSim::FlippinFelinesOptimizerTool & tool) :
        _tool(tool), // ...
    {
	// ...
    }
    // ...

    // The meat of this class; this is called by the Optimizer that has this system.
    int objectiveFunc(const SimTK::Vector & parameters, bool new_parameters,
            SimTK::Real & f) const
    { // ...
        f = ...;
        // ...
    }
    // ...

    // Reference to the FlippinFelinesOptimizerTool.
    OpenSim::FlippinFelinesOptimizerTool & _tool;
    // ...

};

As you can see, our optimizer system must be a subclass of SimTK::OptimizerSystem. We override its objectiveFunc() function, which is then called by the SimTK::Optimizer as part of its algorithm. It's in objectiveFunc() that we (1) convert the optimizer's tweaks to optimization parameters into changes in the actuation control of the model, (2) run a forward dynamic simulation of the cat's fall, and (3) evaluate the simulation by assigning a value for our objective function, f, which is what we calledon the main Flippin' Felines page. See SimTK::OptimizerSystem reference and SimTK::Optimizer reference for more information.

A: optimize.cpp: the executable that ties the pieces together

Here, we show the complete optimize.cpp. The user must provide one command-line argument, which is the name of an XML file that defines a FlippinFelinesOptimizerTool object (we'll get to this in the next section). A serialization is a representation of an object or objects in data files (which, in our case, is a plain text file in XML format). See this page for more information about C++ command-line arguments.

optimize.cpp
#include <iostream>
#include <OpenSim/OpenSim.h>
#include "FlippinFelinesOptimizerSystem.h"

int main(int argc, char * argv[])
{
    // argc is the number of command line inputs, INCLUDING the name of the
    //      exectuable. Thus, it'll always be greater than/equal to 1.
    // argv is an array of space-delimited command line inputs, the first one
    //      necessarily being the name of the executable (e.g., "optimize
    //      optimize_input_template.xml").

    // Get the filename of the FlippinFelinesOptimizerTool serialization.
    if (argc == 2)
    { // Correct number of inputs.

        // Set-up
        // --------------------------------------------------------------------

        // Parse inputs using our Tool class.
        std::string toolSetupFile = argv[1];
        OpenSim::FlippinFelinesOptimizerTool tool(toolSetupFile);

One of the inputs provided in the FlippinFelinesOptimizerTool serialization is the name of a directory in which to save outputs of the optimization. We can grab it from the FlippinFelinesOptimizerTool as shown below. The only input our FlippinFelinesOptimizerSystem needs is this FlippinFelinesOptimizerTool object, which actually contains all our inputs.

optimize.cpp
        std::string name = tool.get_results_directory();

        // Use inputs to create the optimizer system.
        FlippinFelinesOptimizerSystem sys(tool);

We create an optimizer that will optimize our FlippinFelinesOptimizerSystem, and tell the optimizer to figure out the best algorithm to use for the optimization. We played around with the settings a bit. We set the maximum number of iterations to be large so that the optimizer does not quit early.

optimize.cpp
        // Create the optimizer with our system and the "Best Available"
        // algorithm.
        SimTK::Optimizer opt(sys, SimTK::BestAvailable);

        // Set optimizer settings.
        opt.setConvergenceTolerance(0.001);
        opt.useNumericalGradient(true);
        opt.setMaxIterations(100000);
        opt.setLimitedMemoryHistory(500);

When we call optimize(), we need to give the optimizer a place to start in parameter space. Typically, one would define these initial parameters right here, in the same function where we call optimize(). However, we did not want to hard-code our initial parameters in the source code; we wanted to change them between optimizations. So, the initial parameters are actually specified in the toolSetupFile. The FlippinFelinesOptimizerSystem parses that input and gives us the initial parameters in a Vector if we call FlippinFelinesOptimizerSystem::initialParameters(). We then pass this to the Optimizer.

The function optimize() returns when the optimization finishes. Once that happens, we print out the model (that should now be able to flip), as well as the actuation controls that produced the flip. In the case that anything goes wrong (an exception is thrown), we still want to see what the optimizer ended up finding (so as not to waste the failed optimization).

optimize.cpp
        // Initialize parameters for the optimization as those determined
        // by our OptimizerSystem.
        SimTK::Vector initParameters = sys.initialParameters();

	// And we're off! Running the optimization
        // --------------------------------------------------------------------
        try
        {
            double f = opt.optimize(initParameters);
            // Print the optimized model so we can explore the resulting motion.
            sys.printModel(name + "_optimized.osim");
            // Print the control splines so we can explore the resulting actuation.
            sys.printPrescribedControllerFunctionSet(
                    name + "_optimized_parameters.xml");
            
            // Print out the final value of the objective function.
            std::cout << "Done with " << name << "! f = " << f << std::endl;
        }
        catch (...)
        {
            // Print the last model/controls (not optimized) so we have something
            // to look at.
            sys.printModel(name + "_last.osim");
            sys.printPrescribedControllerFunctionSet(
                    name + "_last_parameters.xml");
            std::cout << "Exception thrown; optimization not achieved." << std::endl;
            // Don't want to give the appearance of normal operation.
            throw;
        }
    }
    else
    { // Too few/many inputs, etc.
        std::cout << "\nIncorrect input provided. "
            "Must specify the name of a FlippinFelinesOptimizerTool "
            "serialization (setup/input file).\n\nExamples:\n\t"
            "optimize optimize_input_template.xml\n" << std::endl;
        return 0;
    }
    return EXIT_SUCCESS;
};

B: FlippinFelinesOptimizerTool: optimization settings and serialization

Below is a skeleton of the FlippinFelinesOptimizerTool definition. There are three main parts to the definition; we'll address each individually. Although this code is not vital for setting up optimizations, we discuss it now since the rest of our code depends on it.

The FlippinFelinesOptimizerTool class operates similarly to OpenSim's tools (e.g., Forward Tool), which can be run via the command line by providing an XML setup file. We'd like to use our optimizer in a similar way: for example, optimize optimize_setup_file.xml. Of course, this requires the ability to parse the provided XML file. That's what the class is all about.

If we can define a class whose member variables are the settings for our optimization, and we can serialize this class into a text file, then we have a record of the settings we used for a given optimization. Furthermore, if we can deserialize a text file to create an instance of the class we originally serialized, then we have a really nice way to read the optimize_setup_file.xml input to set the settings for our optimization.

Fortunately, all OpenSim classes have the ability to serialize themselves and to deserialize a file into an instance of an OpenSim class. To create a class that has these features, all we need to do is subclass from OpenSim::Object and define our member variables with a specific syntax. OpenSim uses its ability to de/serialize all the time (all XML files produced by OpenSim or given to OpenSim are serializations done in this way), but we can use this feature for our own purposes as well. That's what we do here; we call our subclass FlippinFelinesOptimizerTool since the class functions similarly (but is different!) from OpenSim Tool and AbstractTool classes.

Note that the (unattractive) alternative to using a setup file would be to recompile our code every time we wanted to change a setting. In that case, we don't really have a record of the settings we used for previous runs, since we've changed the source code and erased our previous settings.

Whenever subclassing from an OpenSim class, we must use a macro such as OpenSim_DECLARE_CONCRETE_OBJECT. See here for specifics.

FlippinFelinesOptimizerSystem.h
namespace OpenSim {

class FlippinFelinesOptimizerTool : public Object {

OpenSim_DECLARE_CONCRETE_OBJECT(FlippinFelinesOptimizerTool, Object);

public:

    // Declare properties of the class.
    // ------------------------------------------------------------------------
    // ********** PART B.1 **********

    // Constructors
    // ------------------------------------------------------------------------
    // ********** PART B.2 **********

private:

    void setNull() { }

    void constructProperties()
    {
	// ********** PART B.3 **********
    }
};

B.1: Declare properties of the class

We create member variables using the OpenSim_DECLARE_PROPERTY and OpenSim_DECLARE_OPTIONAL_PROPERTY macros, which are described here. For simplicity, we've omitted many of the properties we have in the actual source code. For a member variable to be de/serializable, it must be delcared via this macro. Some of the properties help with general setup (e.g., results_directory, model_filename), while others modify the computation of the objective function (e.g., desired configurations and weightings). The macros define getters/setters for us; we'll see the usage of the getters in other parts of the code.

FlippinFelinesOptimizerSystem.h
// General properties.
OpenSim_DECLARE_PROPERTY(results_directory, std::string,
        "Directory in which to save optimization log and results");
OpenSim_DECLARE_PROPERTY(model_filename, std::string,
        "Specifies path to model file, WITH .osim extension");
OpenSim_DECLARE_PROPERTY(num_optim_spline_points, int,
        "Number of points being optimized in each spline function. "
        "Constant across all splines. If an initial_parameters_filename is "
        "provided, the functions specified in that file must have the "
        "correct number of points. We do not error-check for this.");
// Properties related to the objective function.
OpenSim_DECLARE_PROPERTY(anterior_legs_down_weight, double,
        "Adds terms to the objective to minimize final value of "
        "(roll - Pi) and related speeds");
OpenSim_DECLARE_PROPERTY(posterior_legs_down_weight, double,
        "Adds terms to the objective to minimize final value of "
        "(twist - 0) and related speeds");
OpenSim_DECLARE_PROPERTY(sagittal_symmetry_weight, double,
        "Adds a term to the objective to minimize final value of "
        "(hunch + 2 * pitch)");
// Modifying the model before optimizing.
OpenSim_DECLARE_PROPERTY(use_coordinate_limit_forces, bool,
        "TRUE: use coordinate limit forces, "
        "FALSE: ignore coordinate limit forces");
// Setting initial parameters for the optimization.
OpenSim_DECLARE_OPTIONAL_PROPERTY(initial_parameters_filename, std::string,
        "File containing FunctionSet of SimmSpline's used to initialize "
        "optimization parameters. If not provided, initial parameters are "
        "all 0.0, and this element must be DELETED from the XML file "
        "(cannot just leave it blank). The name of each function must be "
        "identical to that of the actuator it is for. x values are ignored. "
        "The time values that are actually used in the simulation are "
        "equally spaced from t = 0 to t = 1.0 s, and there should be "
        "as many points in each function as given by the num_optim_spline_points "
        "property. y values should be nondimensional and between -1 and 1 "
        "(negative values normalized by minControl if minControl is "
        "negative; otherwise the value is normalized by maxControl). "
        "NOTE that the output optimized splines are NOT NONDIMENSIONAL. "
        "Be careful, we do not do any error checking.");

B.2: Constructors

Classes are serialized by calling their print() method. To be able to deserialize a file, the associated class must have a constructor that takes in a filename and passes it to the constructor of Object. The properties in the class are overwritten by the values in the serialization by calling the method Object::updateFromXMLDocument().

FlippinFelinesOptimizerSystem.h
FlippinFelinesOptimizerTool() : Object()
{
    setNull();
    constructProperties();
}
// NOTE: This constructor allows for the de/serialization.
FlippinFelinesOptimizerTool(const std::string &aFileName, bool
        aUpdateFromXMLNode=true) : Object(aFileName, aUpdateFromXMLNode)
{
    setNull();
    constructProperties();
    // Must be called after constructProperties():
    updateFromXMLDocument();
}

Before we can do that though, we need to construct the properties we've declared.

B.3: Constructing the properties we've declared

These constructProperty functions, generated by the macros in part B.1, require an initial/default value for the property.

FlippinFelinesOptimizerSystem.h
void constructProperties()
{
    constructProperty_results_directory("results");
    constructProperty_model_filename("flippinfelines_*FILL THIS IN*.osim");
    constructProperty_num_optim_spline_points(20);
    constructProperty_anterior_legs_down_weight(1.0);
    constructProperty_posterior_legs_down_weight(1.0);
    constructProperty_sagittal_symmetry_weight(1.0);
    constructProperty_use_coordinate_limit_forces(true);
    constructProperty_initial_parameters_filename("");
}

Sample Input

Below is what the XML serialization of this class looks like. This is exactly what we meant by the optimizer_setup_file.xml above. This is a simplified version of what's generated by the executable optimize_input_template, which is generated from optimize_input_template.cpp. We've given you this file, but do not discuss it in the tutorial.

optimize_setup_file.xml
<?xml version="1.0" encoding="UTF-8" ?>
<OpenSimDocument Version="30000">
    <FlippinFelinesOptimizerTool>
        <!--Directory in which to save optimization log and results-->
        <results_directory>results</results_directory>
        <!--Specifies path to model file, WITH .osim extension-->
        <model_filename>flippinfelines_*FILL THIS IN*.osim</model_filename>
        <!--Number of points being optimized in each spline function. Constant across all splines. If an initial_parameters_filename is provided, the functions specified in that file must have the correct number of points. We do not error-check for this.-->
        <num_optim_spline_points>20</num_optim_spline_points>
        <!--Adds terms to the objective to minimize final value of (roll - Pi) and related speeds-->
        <anterior_legs_down_weight>1</anterior_legs_down_weight>
        <!--Adds terms to the objective to minimize final value of (twist - 0) and related speeds-->
        <posterior_legs_down_weight>1</posterior_legs_down_weight>
        <!--Adds a term to the objective to minimize final value of (hunch + 2 * pitch)-->
        <sagittal_symmetry_weight>1</sagittal_symmetry_weight>
        <!--TRUE: use coordinate limit forces, FALSE: ignore coordinate limit forces-->
        <use_coordinate_limit_forces>true</use_coordinate_limit_forces>
        <!--File containing FunctionSet of SimmSpline's used to initialize optimization parameters. If not provided, initial parameters are all 0.0, and this element must be DELETED from the XML file (cannot just leave it blank). The name of each function must be identical to that of the actuator it is for. x values are ignored. The time values that are actually used in the simulation are equally spaced from t = 0 to t = 1.0 s, and there should be as many points in each function as given by the num_optim_spline_points property. y values should be nondimensional and between -1 and 1 (negative values normalized by minControl if minControl is negative; otherwise the value is normalized by maxControl). NOTE that the output optimized splines are NOT NONDIMENSIONAL. Be careful, we do not do any error checking.-->
        <initial_parameters_filename>initial_parameters.xml</initial_parameters_filename>
    </FlippinFelinesOptimizerTool>
</OpenSimDocument> 

C: FlippinFelinesOptimizerSystem

Here's a skeleton of the FlippinFelinesOptimizerSystem class. The two functions that are the most important are the constructor and objectiveFunc(). We focus on these. The definition of an optimization problem consists primarily of (1) parameters/inputs, and (2) the objective function. The parts that deal with the parameters are C.2, C.3, C.4, and C.5. Parts C.6 and C.7 deal with the objective function.

FlippinFelinesOptimizerSystem.h
 /**
 * Finds a control input history that achieves certain desired features
 * of a cat's flipping maneuver, as determined by the objective function.
 * The control of the system is performed via a PrescribedController that
 * uses a spline for all actuators.
 * */
class FlippinFelinesOptimizerSystem : public SimTK::OptimizerSystem {
public:

    FlippinFelinesOptimizerSystem(OpenSim::FlippinFelinesOptimizerTool & tool) :
        _tool(tool),
        _duration(1.0),
        _objectiveCalls(0),
        _objectiveFcnValueBestYet(SimTK::Infinity),
    {
        // Parse inputs & prepare output
        // --------------------------------------------------------------------
	// ********** PART C.1 **********

        // Add a controller to the model to control its actuators
        // --------------------------------------------------------------------
        // ********** PART C.2 **********

	// Set parameter limits/bounds for the optimization.
        // --------------------------------------------------------------------
        // ********** PART C.3 **********

    }

    SimTK::Vector initialParameters()
    {
        // ********** PART C.4 **********
    }
	// ...

    /**
     * Defines desired features for the cat model's flip. Each call to this
     * function updates the model's control, runs a forward dynamic simulation
     * with this control, computes the objective value resulting from the
     * simulation, and saves the results to output.
     * */
    int objectiveFunc(const SimTK::Vector & parameters, bool new_parameters,
            SimTK::Real & f) const
    {
        // ...
        // Unpack parameters into the model
        // --------------------------------------------------------------------
        // ********** PART C.5 **********

	// Run a forward dynamic simulation
        // --------------------------------------------------------------------
        // ********** PART C.6 **********

        // Construct the objective function value
        // --------------------------------------------------------------------
        // ********** PART C.7 **********

        // Update the log and outputs
        // --------------------------------------------------------------------
        // ********** PART C.8 **********

    }
    // Miscellaneous functions (used, but uninteresting)
    // ------------------------------------------------------------------------
    // ********** PART C.9 **********

private:

    // Objective function terms
    // ------------------------------------------------------------------------
    // ********** PART C.7 **********

    // Member variables
    // ------------------------------------------------------------------------
    // ********** PART C.10 **********

};

Throughout, we'll be making use of member variables we've defined. To see their declarations, jump to part C.10.

Note that objectiveFunc() is necessarily a const function. This is because we don't want the state of the system changing as we optimize it. For example, we wouldn't want to change relative masses of the cat while trying to find the optimum control input; the solution would be a moving target.

C.1: Parse inputs & prepare output

Here's where we load the model. If the setup file specifies that we turn off the coordinate limit forces, we turn them off. We do this in the constructor because it's not something we would need to do during each call to the objective function.

FlippinFelinesOptimizerSystem.h
// Parse inputs
// --------------------------------------------------------------------
_name = _tool.get_results_directory();
_cat = OpenSim::Model(_tool.get_model_filename());

// Disable coordinate limit forces?
if (!_tool.get_use_coordinate_limit_forces())
{
    SimTK::State& initState = _cat.initSystem();
    // Loop over all forces in model.
    using OpenSim::CoordinateLimitForce;
    for (int iFor = 0; iFor < _cat.getForceSet().getSize(); iFor++)
    {
        CoordinateLimitForce * LF = dynamic_cast<CoordinateLimitForce *> (&_cat.updForceSet().get(iFor));
        if (LF)
        { // If it is a coordinate limit force, disable it.
            _cat.updForceSet().get(iFor).setDisabled(initState, true);
        }
    }
}

_numOptimSplinePoints = _tool.get_num_optim_spline_points();

We will continue to update a log file, and print out various other files (e.g., best-yet controls), as the optimization runs. Here's the code that creates a folder for our outputs and opens the log file.

FlippinFelinesOptimizerSystem.h
// Prepare output
// --------------------------------------------------------------------

// Create a results directory for all the output files.
#if defined(_WIN32)
    _mkdir(_name.c_str());
#else
    mkdir(_name.c_str(), 0777);
#endif

// Serialize what we just deserialized, in the results directory, to keep
// everything in one organized place.
_tool.print(_name + "/" + _name + "_setup.xml");

// Create a log for the objective function value and the separate terms
// that go into it.
_optLog.open((_name + "/" + _name + "_log.txt").c_str(), std::ofstream::out);
_optLog << "Flippin Felines optimization log: " << _name << std::endl;
time_t rawtime; time(&rawtime);
_optLog << ctime(&rawtime);
_optLog << "Model file name: " << _tool.get_model_filename() << std::endl;

C.2: Add a controller to the model to control its actuators

Right now, our models have CoordinateActuator's, but we have no way of controlling them. We must add a controller to the model to do this. In general, a controller can manage any number of the model's actuators. We choose to have one controller that controls all of the cat's actuators separately. More specifically, we want a controller that will specify beforehand what the actuator signal (torque, in our case) will be throughout the simulation (NOTE: one alternative would be to use a feedback law). Such a controller is a PrescribedController. With a PrescribedController, we specify for each actuator a function that is the torque that each CoordinateActuator applies as a function of simulation time. We choose to use splines (SimmSpline) as the functions.

We want our optimization to solve for the control input (prescribed control) that flips the cat. So, our optimization parameters need to be related to our SimmSpline's. The simplest way to relate them is by choosing the optimization parameters to be the Y-values of the spline points. As seen above, the number of spline points is specified in the setup file; the corresponding X-values (times) are preset as evenly-spaced over the simulation.

Parameters are ordered by actuator, then by spline point index for a given actuator. Parameters are nondimensionalized by the minimum or maximum control values for the associated actuator.

FlippinFelinesOptimizerSystem.h
// Compute the number of optimization parameters for the model.
_numActuators = _cat.getActuators().getSize();
int numParameters = _numActuators * _numOptimSplinePoints;
setNumParameters(numParameters);

// Size the vector of best-yet splines.
_splinesBestYet.resize(_numActuators);

// Add a PrescribedController to the model.
using OpenSim::PrescribedController;
PrescribedController * flipController = new PrescribedController();
flipController->setName("flip_controller");

// Set the PrescribedController to control all actuators.
flipController->setActuators(_cat.getActuators());

// Add the PrescribedController to the model.
_cat.addController(flipController);

// Create SimmSpline's for each actuator and push into vector.
double indexToTimeInSeconds =
    _duration / (double)(_numOptimSplinePoints - 1);
for (int i = 0; i < _numActuators; i++)
{
    // Create a spline function for the current actuator.
    _splines.push_back(new OpenSim::SimmSpline());
    // Name this spline with the name of the current actuator.
    _splines[i]->setName(_cat.getActuators().get(i).getName());
    // Add the correct number of points to this spline.
    for (int j = 0; j < _numOptimSplinePoints; j++)
    {
        // Scale the time points appropriately.
        double thisTime = (double)j * indexToTimeInSeconds;
        _splines[i]->addPoint(thisTime, 0.0);
    }
    // Tell the controller about this function.
    flipController->prescribeControlForActuator(i, _splines[i]);
} 

C.3: Set parameter limits/bounds for the optimization

We tell the optimizer that there are bounds on the values of the parameters. We've chosen to nondimensionalize our parameters, so our bounds are simple. The OptimizerSystem::setParameterLimits() function is public, so this function might be called in optimize.cpp instead.

FlippinFelinesOptimizerSystem.h
// Set (nondimensional) parameter limits/bounds for the actuators.
SimTK::Vector lowerLimits(getNumParameters(), -1.0);
SimTK::Vector upperLimits(getNumParameters(), 1.0);
setParameterLimits(lowerLimits, upperLimits);

C.4: Initial parameters

We wanted to make it really easy for the user to specify initial parameters for the optimization. Since these parameters end up as Y-coordinates of the spline points, we thought that SimmSpline's would be the easiest form of input. However, we need the initial parameters as a Vector. So, we wrote a method to convert the Y-coordinates from a FunctionSet of SimmSpline's into a Vector.

FlippinFelinesOptimizerSystem.h
SimTK::Vector initialParameters()
{
    SimTK::Vector initParams(getNumParameters(), 0.0);
    if (_tool.get_initial_parameters_filename() != "")
    { // A file is specified.
        // Deserialize the initial parameters XML file.
        OpenSim::FunctionSet initFcns(_tool.get_initial_parameters_filename());
        // Write a copy of the FunctionSet to the results directory.
        initFcns.print(_name + "/" + _name + "_initial_parameters.xml");
        OpenSim::Array<std::string> initNames;
        initFcns.getNames(initNames);
        
        // This loop is set up so that the initFcns do not need to be in the same
        // order as the optimization parameters. Also, the number of initFcns
        // specified by the initial parameters file can be greater than the
        // number of actuators in the model (assuming that the initial parameters
        // file AT LEAST contains the model's actuators).
        using OpenSim::SimmSpline;
        for (int iAct = 0; iAct < _numActuators; iAct++)
        {
            // Get index of the initFcn corresponding to the actuator.
            int iFcn = initFcns.getIndex(_cat.getActuators().get(iAct).getName());
            // Get access to the spline's methods.
            SimmSpline * fcn =
                dynamic_cast<SimmSpline *>(&initFcns.get(iFcn));
            for (int iPts = 0; iPts < _numOptimSplinePoints; iPts++)
            {
                // Find the right index in the optimization parameters.
                int paramIndex = iAct * _numOptimSplinePoints + iPts;
                // Transfer y value from input to initial parameters.
                initParams[paramIndex] = fcn->getY(iPts);
            }
        }
    }
    return initParams;
}

C.5: Unpack parameters into the model

At this point, the Optimizer has given us new parameters to try. We need to use these parameters to change the Y-values of the splines. We must first dimensionalize the parameters.

Note that the number of calls to the objective function (_objectiveCalls) is NOT equal to the number of iterations of the optimization algorithm.

FlippinFelinesOptimizerSystem.h
// Increment the count of calls to this function.
_objectiveCalls++;

// Unpack parameters into the model (i.e., update spline points)
// --------------------------------------------------------------------
for (int iAct = 0; iAct < _numActuators; iAct++)
{
    // Max/min for all spline points for the i-th actuator.
    double minControl = _cat.getActuators().get(iAct).getMinControl();
    double maxControl = _cat.getActuators().get(iAct).getMaxControl();

    // Visit each point in this spline.
    for (int iPts = 0; iPts < _numOptimSplinePoints; iPts++)
    {
        int paramIndex = iAct * _numOptimSplinePoints + iPts;

        // Dimensionalize the control value.
        double dimControlValue;
        if  (parameters[paramIndex] < 0 && minControl < 0)
        { // parameter and minControl are both neg; must negate again.
            dimControlValue = -minControl * parameters[paramIndex];
        }
        else
        {
            dimControlValue = maxControl * parameters[paramIndex];
        }

        // Edit the spline with the dimensional control value.
        _splines[iAct]->setY(iPts, dimControlValue);
    }
}

C.6: Run a forward dynamic simulation

Now that we've updated the controls, we can run a simulation and see how the cat ends up.

FlippinFelinesOptimizerSystem.h
SimTK::State& initState = _cat.initSystem();

// Construct an integrator.
SimTK::RungeKuttaMersonIntegrator integrator(_cat.getMultibodySystem());
integrator.setAccuracy(1.0e-6);

// Construct a manager to run the integration.
OpenSim::Manager manager(_cat, integrator);
_cat.getMultibodySystem().realize(initState, SimTK::Stage::Acceleration);

// Integrate from initial time to final time
manager.setInitialTime(0.0);
manager.setFinalTime(_duration);
manager.integrate(initState);

C.7: Construct the objective function value

Here's the brain of the optimization. We assign to f the value of the objective function. Here, we have 3 terms in our objective function, which are described on the main Flippin' Felines page. The source code contains a few more terms; we've cut them out for simplicity.

FlippinFelinesOptimizerSystem.h
// Will be writing to a log while constructing the objective function.
if (_objectiveCalls % _outputPeriod == 0)
    _optLog << _objectiveCalls;

// Copy the model's state. We need a consistent state to access the
// model's configuration and related speeds.
SimTK::State aState = initState;
_cat.getMultibodySystem().realize(aState, SimTK::Stage::Acceleration);
const OpenSim::CoordinateSet& coordinates = _cat.getCoordinateSet();
double roll = coordinates.get("roll").getValue(aState);
double rollRate = coordinates.get("roll").getSpeedValue(aState);
double rollAccel = coordinates.get("roll").getAccelerationValue(aState);
double twist = coordinates.get("twist").getValue(aState);
double twistRate = coordinates.get("twist").getSpeedValue(aState);
double twistAccel = coordinates.get("twist").getAccelerationValue(aState);
double hunch = coordinates.get("hunch").getValue(aState);
double pitch = coordinates.get("pitch").getValue(aState);
double pitchRate = coordinates.get("pitch").getSpeedValue(aState);
double pitchAccel = coordinates.get("pitch").getAccelerationValue(aState);

// --------------------------------------------------------------------
f = 0;
f += anteriorLegsDownTerm(roll, rollRate, rollAccel);
f += posteriorLegsDownTerm(twist, twistRate, twistAccel);
f += sagittalSymmetryTerm(hunch, pitch, pitchRate, pitchAccel);
// --------------------------------------------------------------------

We use some helper functions to keep this code clean. This first helper function is called by each of the "*Term()" functions above; it provides a convenient way to (in one step) update the log and weight a term in the function.

FlippinFelinesOptimizerSystem.h
/**
 * Helper function for writing data to the log.
 * */
double processObjTerm(std::string name, double weight, double term) const
{
    if (_objectiveCalls % _outputPeriod == 0)
        _optLog << " " << name << " " << weight * term;
    return weight * term;
}

The following functions must be const because objectiveFunc() is const; we cannot call member functions within a const member function unless those functions are also const. These functions are also defined inline (i.e., definition is inside the class definition) for efficiency: the compiler will 'paste' the function body into the place where the function is called.

FlippinFelinesOptimizerSystem.h
double anteriorLegsDownTerm(
        double roll, double rollRate, double rollAccel) const
{
    if (_tool.get_anterior_legs_down_weight() != 0.0)
    {
        return processObjTerm("anterior_legs_down", _tool.get_anterior_legs_down_weight(),
                pow(roll - SimTK::Pi, 2) + _relw * pow(rollRate, 2) + _relw * pow(rollAccel, 2));
    }
    return 0.0;
}
double posteriorLegsDownTerm(
        double twist, double twistRate, double twistAccel) const
{
    if (_tool.get_posterior_legs_down_weight() != 0.0)
    {
        return processObjTerm("posterior_legs_down", _tool.get_posterior_legs_down_weight(),
                pow(twist - 0.0, 2) + _relw * pow(twistRate, 2) + _relw * pow(twistAccel, 2));
    }
    return 0.0;
} 

double sagittalSymmetryTerm(
        double hunch, double pitch, double pitchRate, double pitchAccel) const
{
    if (_tool.get_sagittal_symmetry_weight() != 0.0)
    {
        return processObjTerm("sagittal_symmetry", _tool.get_sagittal_symmetry_weight(),
                pow(hunch + 2 * pitch, 2) + _relw * pow(pitchRate, 2) + _relw * pow(pitchAccel, 2));
    }
    return 0.0;
}

C.8: Update the log and outputs

We now return to objectiveFunc(), right after the code where we've assembled  f.  We update the log with the new objective function value and might print an updated model/controls.

FlippinFelinesOptimizerSystem.h
_lastCallWasBestYet = _thisCallIsBestYet;
_thisCallIsBestYet = f <= _objectiveFcnValueBestYet;
if (_thisCallIsBestYet) _objectiveFcnValueBestYet = f;
if (_objectiveCalls % _outputPeriod == 0)
    _optLog << " objfcn " << f << " objfcn_best_yet " << _objectiveFcnValueBestYet << std::endl;
// If this is the best yet (i.e., smallest objective function value),
// save a copy of the splines.
if (_thisCallIsBestYet)
{
    for (unsigned int iFcn = 0; iFcn < _splinesBestYet.size(); iFcn++)
        _splinesBestYet[iFcn] = *_splines[iFcn];
}
// If this is worse, print out the best-yet splines and current model
// (NOTE: current model is NOT "best yet", but the idea is that it will
// be close). Also print out nondimensionalized splines so that they can
// be used directly as input for a subsequent optimization.
if (_lastCallWasBestYet && !_thisCallIsBestYet)
{
    printBestYetPrescribedControllerFunctionSet(
            _name + "_best_yet_parameters.xml");
    printBestYetPrescribedControllerFunctionSet(
            _name + "_best_yet_parameters_nomdim.xml", true);
    printModel(_name + "_best_yet.osim");
}
// Print out to the terminal/console every so often.
if (_objectiveCalls % _outputPeriod == 0)
    std::cout << "Objective call # " << _objectiveCalls << std::endl;
return 0;

C.9: Miscellaneous functions

Here are some functions that we use, but that aren't too important for learning how optimization works. They are used to print either the model or control functions.

FlippinFelinesOptimizerSystem.h
/**
 * @brief Prints the current model to the given filename.  The file will
 * be located in the directory containing the log file for this
 * optimization run.
 * */
void printModel(std::string filename) const
{
    _cat.print(_name + "/" + filename);
}
/**
 * @brief Serializes the current set of functions used for the actuators.
 * The file will be located in the directory containing the log file
 * for this optimization run.
 * */
void printPrescribedControllerFunctionSet(std::string filename) const
{
    // Create the FunctionSet that we'll then serialize.
    OpenSim::FunctionSet fset;
    fset.setSize(_splines.size());
    for (unsigned int iFcn = 0; iFcn < _splines.size(); iFcn++)
    {
        fset.insert(iFcn, *_splines[iFcn]);
    }
    fset.print(_name + "/" + filename);
}
/**
 * @brief Serializes the set of functions associated with the best-yet
 * value of the objective function. The file will be located in the
 * directory containing the log file for this optimization run.
 *
 * @param (nondimensionalize) Divide spline values by minControl for the
 * actuator, if value and minControl are negative, and by maxControl, if
 * value is positive. Once nondimensionalized, this FunctionSet can be used
 * as initial parameters.
 * */
void printBestYetPrescribedControllerFunctionSet(std::string filename,
        bool nondimensionalize=false) const
{
    using OpenSim::SimmSpline;
    // Create the FunctionSet that we will then serialize.
    OpenSim::FunctionSet fset;
    fset.setSize(_splinesBestYet.size());
    for (unsigned int iFcn = 0; iFcn < _splinesBestYet.size(); iFcn++)
    {
        fset.insert(iFcn, _splinesBestYet[iFcn]);
        if (nondimensionalize)
        { // Go through each y-value and nondimensionalize based on its sign.
            // Max/min for all spline points for the i-th actuator.
            double minControl = _cat.getActuators().get(iFcn).getMinControl();
            double maxControl = _cat.getActuators().get(iFcn).getMaxControl();
            for (int iPts = 0; iPts < _numOptimSplinePoints; iPts++)
            {
                SimmSpline * fcn = dynamic_cast<SimmSpline *>(&fset.get(iFcn));
                double dimValue = fcn->getY(iPts);
                double nonDimValue = 0;
                if (dimValue < 0 && minControl < 0)
                {
                    nonDimValue = -dimValue / minControl;
                }
                else
                {
                    nonDimValue = dimValue / maxControl;
                }
                fcn->setY(iPts, nonDimValue);
            }
        }
    }
    fset.print(_name + "/" + filename);
}

C.10: Member variables

The mutable keyword lets us modify a member variable inside const member functions, such as objectiveFunc() above.

FlippinFelinesOptimizerSystem.h
// Reference to the FlippinFelinesOptimizerTool.
OpenSim::FlippinFelinesOptimizerTool & _tool;

// Simulation runtime (seconds).
double _duration;

// Model containing the attributes described in this class' description
mutable OpenSim::Model _cat;

// Number of actuators in the model.
int _numActuators;

// Number of points used to define splines for actuator control.
int _numOptimSplinePoints;

// Vector of splines used in the PrescribedController.
std::vector<OpenSim::SimmSpline *> _splines;

// Vector of splines that gave the best objective value yet.
mutable std::vector<OpenSim::SimmSpline> _splinesBestYet;

// Number of calls to the objective function.
mutable int _objectiveCalls;

// Name of results directory.
std::string _name;

// Log for recording data during optimization
mutable std::ofstream _optLog;

// Period for how often objective terms are printed to the log.
static const int _outputPeriod = 100;

// Best (lowest) value of the objective function, for logging.
mutable double _objectiveFcnValueBestYet;

// To aid with conservative printing of best yet actuation:
mutable bool _lastCallWasBestYet;
mutable bool _thisCallIsBestYet; 

Getting started with optimization and reproducing our results

We've given you some setup/input files for the optimization to get you started reproducing our results. Enter a terminal or command prompt, navigate to the directory containing the optimize executable and the counterrotation_setup.xml file. The following will start an optimization. It should take under 2 hours to complete.

 optimize counterrotation_setup.xml

Compare your results to those in the 'References' folder of the 'Tutorial code' directory. You can repeat this for the second optimization, if counterrotation_variableinertia_setup.xml and counterrotation_variableinertia_initial_parameters.xml are in the directory.

 optimize counterrotation_variableinertia_setup.xml

Remember that it's the setup file that refers to the initial parameters file. This second optimization should take about the same time to complete.

 

Enjoy playing around with the code. We hope you've learned something useful!

 

OpenSim is supported by the Mobilize Center , an NIH Biomedical Technology Resource Center (grant P41 EB027060); the Restore Center , an NIH-funded Medical Rehabilitation Research Resource Network Center (grant P2C HD101913); and the Wu Tsai Human Performance Alliance through the Joe and Clara Tsai Foundation. See the People page for a list of the many people who have contributed to the OpenSim project over the years. ©2010-2024 OpenSim. All rights reserved.