To get our models to flip, we want to add actuation control to them, and tweak the control until we obtain our desired flip motion. We have an optimization algorithm take care of the tweaking for us. The algorithm will tweak the control in some intelligent way, then we'll run a forward dynamic simulation with the given controls, and we'll see if the cat flips. The algorithm continues to tweak the control until we decide that the cat model has achieved a flip.
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 example), 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 getting one's feet wet with dynamic optimization.
Organization of code
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 shows only 4 lines from this file, the most important lines, that show how we connect the various pieces of the optimization. Look at the next code block, a skeleton of the second file, FlippinFelinesOptimizerSystem.h, to see where we define the FlippinFelinesOptimizerTool and FlippinFelinesOptimizerSystem classes.
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. (TODO so we'll focus on the first 2).
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 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 an 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.
#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.
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 little. We set the maximum number of iterations to be large just because we don't want the optimizer to quit early.
// 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 the space of the parameters. 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 this between optimizations easily. So it's actually something we can specify 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).
// 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
Although this part of the code is not vital for setting up optimizations, we discuss it now, since the rest of the code depends on it. Below is a skeleton of the FlippinFelinesOptimizerTool definition. There are three main parts to the definition, which we'll get to shortly.
This class operates similarly to OpenSim's tools (e.g., for the Forward Tool), which can be run via the command line by providing an XML setup file. The way we'd like to use our optimizer is via the command like via commands like optimize optimize_setup_file.xml
. This requires the ability to parse that XML file. That's what this FlippinFelinesOptimizerTool 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 that optimize_setup_file.xml
input file to set settings within 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.
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 value. These macros define getters/setters for us; we'll see the usage of the getters in other parts of the code.
// 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().
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.
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(""); }
A sample input file
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 we do not discuss it in this tutorial.
<?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
/** * 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. * * Parameters are ordered by actuator, then by spline point index for a * given actuator. Parameters are nondimensionalized by the min or max * control values for the associated actuator. * */ 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 (i.e., update spline points) // -------------------------------------------------------------------- // ********** PART C.5 ********** // Run a forward dynamic simulation // -------------------------------------------------------------------- // ********** PART C.6 ********** // Construct the objective function, term by term // -------------------------------------------------------------------- // ********** PART C.7 ********** // Update the log and outputs with the objective function value // -------------------------------------------------------------------- // ********** PART C.8 ********** } // Miscellaneous functions (used, but uninteresting) // ------------------------------------------------------------------------ // ********** PART C.10 ********** private: // Objective function terms // ------------------------------------------------------------------------ // ********** PART C.7 ********** // Member variables // ------------------------------------------------------------------------ // ********** PART C.9 ********** };
TODO why you would look at the member variables section.
C.1: Parse inputs and prepare output
C.2: Add a controller to the model to control its actuators
C.3: Set parameter limits/bounds for the optimization
C.4: Initial parameters
C.5: Unpack parameters into the model
C.6: Run a forward dynamic simulation
TODO MUTABLES
TODO talk about controls
TODO talk about how the parameters work.
C.2 is the part that would be custom between different pplz
CAN BE ANYTHING
iterations is not number of objective function calls.
There's an additional file, optimizer_tool_template.cpp, whose code we do not show here. It generates a template XML file TODO .... TODO paste a version of it into this page.
Getting started with optimization and reproducing our results
We've given you some setup/input files to the optimization to get you started and to reproduce our results. Enter a terminal or command prompt, navigate to a 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. TODO. You can do the same 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 amount of time to complete.