The first task is to develop models whose actuation we can then optimize for the flip maneuver. We walk through the code in modeling.cpp, which becomes an executable that creates two cat models. The first model is simply two segments connected by a 2-degree-of-freedom (no twist) joint. It exhibits the counter-rotation mechanism of flipping. The second builds on top of the first with actuate-able legs, and a twist degree of freedom between the model's two halves. This model exhibits both the counter-rotation and variable- inertia mechanisms of flipping. Here's a skeleton of modeling.cpp:
#include <OpenSim/OpenSim.h> int main(int argc, char *argv[]) { // First model: exhibiting the counter-rotation mechanism of flipping // ======================================================================== // ********** PART A ********** // Define anterior and posterior halves of the cat // ------------------------------------------------------------------------ // ********** PART A.1 ********** // Define joints between bodies (ground and two halves) // ------------------------------------------------------------------------ // ********** PART A.2 ********** // Display geometry // ------------------------------------------------------------------------ // ********** PART A.3 ********** // Actuation // ------------------------------------------------------------------------ // ********** PART A.4 ********** // Second model: adding legs for the variable-inertia mechanism of flipping // ======================================================================== // ********** PART B ********** return EXIT_SUCCESS; };
A: First model: exhibiting the counter-rotation mechanism of flipping
This first model has 2 degrees of freedom between its halves, hunch and wag, as show in the image to the left TODO. These degrees of freedom are sufficient for allowing the cat to execute a flip using counter-rotation. Mass properties come from Kane and Scher [2] and inspection of cats.
// Properties // ------------------------------------------------------------------------ double segmentalLength = 0.175; // m double segmentalDiam = 0.15; // m double segmentalMass = 1; // kg double segmentalTransverseMomentOfInertia = 1; // kg-m^2 // Ratio of transverse to axial moment of inertia (Kane and Scher, 1969): double JIratio = 0.25; // For actuators: double maxTorque = 40.0; // N-m // Basics // ------------------------------------------------------------------------ // Create the cat model. OpenSim::Model cat; // Name the cat after the founder of Stanford University. // This model will be able to hunch and wag (see the joints below). cat.setName("Leland_hunch_wag"); // 'Turn off' gravity so it's easier to watch an animation of the flip. using SimTK::Vec3; cat.setGravity(Vec3(0, 0, 0));
A.1: Define anterior and posterior halves of the cat
Both halves of the cat have the same inertial properties (mass, mass center, moments of inertia).
// Prepare inertia properties for the 2 primary segments of the cat. double segmentalAxialMomentOfInertia = JIratio * segmentalTransverseMomentOfInertia; double Ixx = segmentalAxialMomentOfInertia; double Iyy = segmentalTransverseMomentOfInertia; double Izz = segmentalTransverseMomentOfInertia; double Ixy = 0; double Ixz = 0; double Iyz = 0; using SimTK::Inertia; Inertia segmentalInertia = Inertia(Ixx, Iyy, Izz, Ixz, Ixz, Iyz); using OpenSim::Body; Body * anteriorBody = new Body(); Body * posteriorBody = new Body(); anteriorBody->setName("anteriorBody"); posteriorBody->setName("posteriorBody"); anteriorBody->setMass(segmentalMass); posteriorBody->setMass(segmentalMass); anteriorBody->setInertia(segmentalInertia); posteriorBody->setInertia(segmentalInertia);
Nonetheless, we specify different values to setMassCenter() below. For this reason, among others, we hope to clarify what this property means in OpenSim. Each body comes with a reference frame in which the body is fixed. This reference frame has an orientation and a position, or equivalently, an origin. Now, as modelers, we know where the mass center of each body is, independent of any reference frames. However, the setMassCenter() function requires that, when we communicate the mass center to OpenSim, we express the mass center location from the body's origin, and in the basis (orientation) of the reference frame. In doing so, we actually define the reference frame (that is, its origin and orientation in the body). The mass center is fixed regardless of how OpenSim internally represents it; it is the definition of the reference frame that is at our discretion when we use setMassCenter(). We could specify the mass center to be (0, 0, 0): this would just mean that the origin of the body's frame is at its mass center; it would not affect the inertial properties on its own. What it does affect is how we subsequently express the position and orientation of joints and display geometry in the body's frame (which, again, we defined at our discretion).
So, below, we provide different values to setMassCenter() for the 2 bodies, despite the fact that they have same mass center (that is, the same distribution of mass). What does differ between the 2 bodies is the location of their origin. (Note: We've presented one perspective of the situation. An alternative perspective is that we, the modelers, are given a frame, and must place our body in this frame. We can choose where we place the body in this frame, and we do so via the values we give to setMassCenter() and setIinertia().)
// By choosing the following as the mass center, we choose the origin of // the anteriorBody frame to be at the body's positive-X extent. That is, // the anterior body sits to the -X direction from its origin. anteriorBody->setMassCenter(Vec3(-0.5 * segmentalLength, 0, 0)); // Posterior body sits to the +X direction from its origin. posteriorBody->setMassCenter(Vec3(0.5 * segmentalLength, 0, 0));
A.2: Define joints between bodies using CustomJoint's
We now connect the bodies, and thus define how they can move with respect to one another. The cat is free to move about in space, and we represent this using a 6-degree-of-freedom joint between the ground (the inertial frame) and the cat's anterior body. We could use a FreeJoint, which is simpler to define, but we instead employ a CustomJoint. In our case we do so because we want to specify the type of Euler angles that describe the orientation of the cat's anterior segment. Although the FreeJoint also uses Euler angles, it does not allow the modeler to choose which type of Euler angles are used.
We choose YZX Euler angles, named yaw, pitch, and roll, respectively. We specify this via a SpatialTransform object, which we'll use to define our CustomJoint. A spatial transform has 6 transform axes. The first 3 are rotations, defined about the axes of our choosing. The remaining 3 are are translations, for which we can also set the axes. We choose the translation axes to be the X, Y, and Z directions of the ground's frame, which are also the default (YZX Euler angles are not the default, though; XYZ Euler angles is the default).
Body & ground = cat.getGroundBody(); using OpenSim::CustomJoint; using OpenSim::Array; OpenSim::SpatialTransform groundAnteriorST; groundAnteriorST.updTransformAxis(0).setCoordinateNames( Array<std::string>("yaw", 1)); groundAnteriorST.updTransformAxis(0).setAxis(Vec3(0, 1, 0)); groundAnteriorST.updTransformAxis(1).setCoordinateNames( Array<std::string>("pitch", 1)); groundAnteriorST.updTransformAxis(1).setAxis(Vec3(0, 0, 1)); groundAnteriorST.updTransformAxis(2).setCoordinateNames( Array<std::string>("roll", 1)); groundAnteriorST.updTransformAxis(2).setAxis(Vec3(1, 0, 0)); groundAnteriorST.updTransformAxis(3).setCoordinateNames( Array<std::string>("tx", 1)); groundAnteriorST.updTransformAxis(3).setAxis(Vec3(1, 0, 0)); groundAnteriorST.updTransformAxis(4).setCoordinateNames( Array<std::string>("ty", 1)); groundAnteriorST.updTransformAxis(4).setAxis(Vec3(0, 1, 0)); groundAnteriorST.updTransformAxis(5).setCoordinateNames( Array<std::string>("tz", 1)); groundAnteriorST.updTransformAxis(5).setAxis(Vec3(0, 0, 1));
Vec3 locGAInGround(0); Vec3 orientGAInGround(0); Vec3 locGAInAnterior(0); Vec3 orientGAInAnterior(0); CustomJoint * groundAnterior = new CustomJoint("ground_anterior", ground, locGAInGround, orientGAInGround, *anteriorBody, locGAInAnterior, orientGAInAnterior, groundAnteriorST);
// Edit the Coordinate's created by the CustomJoint. The 6 coordinates // correspond to the TransformAxis's we set above. using OpenSim::CoordinateSet; using SimTK::convertDegreesToRadians; using SimTK::Pi; CoordinateSet & groundAnteriorCS = groundAnterior->upd_CoordinateSet(); // yaw // As is, the range only affects how one can vary this coordinate in the // GUI. The range is not a joint limit, and does not affect dynamics. double groundAnteriorCS0range[2] = {-Pi, Pi}; groundAnteriorCS[0].setRange(groundAnteriorCS0range); groundAnteriorCS[0].setDefaultValue(0); groundAnteriorCS[0].setDefaultLocked(false); // pitch double groundAnteriorCS1range[2] = {-Pi, Pi}; groundAnteriorCS[1].setRange(groundAnteriorCS1range); groundAnteriorCS[1].setDefaultValue(convertDegreesToRadians(-15)); groundAnteriorCS[1].setDefaultLocked(false); // roll double groundAnteriorCS2range[2] = {-Pi, Pi}; groundAnteriorCS[2].setRange(groundAnteriorCS2range); groundAnteriorCS[2].setDefaultValue(0); groundAnteriorCS[2].setDefaultLocked(false); // tx double groundAnteriorCS3range[2] = {-1, 1}; groundAnteriorCS[3].setRange(groundAnteriorCS3range); groundAnteriorCS[3].setDefaultValue(0); groundAnteriorCS[3].setDefaultLocked(false); // ty double groundAnteriorCS4range[2] = {-1, 5}; groundAnteriorCS[4].setRange(groundAnteriorCS4range); groundAnteriorCS[4].setDefaultValue(0); groundAnteriorCS[4].setDefaultLocked(false); // tz double groundAnteriorCS5range[2] = {-1, 1}; groundAnteriorCS[5].setRange(groundAnteriorCS5range); groundAnteriorCS[5].setDefaultValue(0); groundAnteriorCS[5].setDefaultLocked(false);
// Anterior to posterior body via a CustomJoint // ```````````````````````````````````````````` // Rotation is defined via ZYX Euler angles. OpenSim::SpatialTransform anteriorPosteriorST; anteriorPosteriorST.updTransformAxis(0).setCoordinateNames( Array<std::string>("hunch", 1)); anteriorPosteriorST.updTransformAxis(0).setAxis(Vec3(0, 0, 1)); anteriorPosteriorST.updTransformAxis(1).setCoordinateNames( Array<std::string>("wag", 1)); anteriorPosteriorST.updTransformAxis(1).setAxis(Vec3(0, 1, 0)); anteriorPosteriorST.updTransformAxis(2).setCoordinateNames( Array<std::string>("twist", 1)); anteriorPosteriorST.updTransformAxis(2).setAxis(Vec3(1, 0, 0)); // There is no translation between the segments, and so we do not name the // remaining 3 TransformAxis's in the SpatialTransform. Vec3 locAPInAnterior(0); Vec3 orientAPInAnterior(0); Vec3 locAPInPosterior(0); Vec3 orientAPInPosterior(0); CustomJoint * anteriorPosterior = new CustomJoint("anterior_posterior", *anteriorBody, locAPInAnterior, orientAPInAnterior, *posteriorBody, locAPInPosterior, orientAPInPosterior, anteriorPosteriorST); // Set coordinate limits and default values from empirical data (i.e., // photos & video). CoordinateSet & anteriorPosteriorCS = anteriorPosterior->upd_CoordinateSet(); // hunch: [-20, +90] degrees double anteriorPosteriorCS0range[2] = {convertDegreesToRadians(-20), convertDegreesToRadians(90)}; anteriorPosteriorCS[0].setRange(anteriorPosteriorCS0range); anteriorPosteriorCS[0].setDefaultValue(convertDegreesToRadians(30)); anteriorPosteriorCS[0].setDefaultLocked(false); // wag: [-45, 45] degrees double anteriorPosteriorCS1range[2] = {-0.25 * Pi, 0.25 * Pi}; anteriorPosteriorCS[1].setRange(anteriorPosteriorCS1range); anteriorPosteriorCS[1].setDefaultValue(0); anteriorPosteriorCS[1].setDefaultLocked(false); // twist: [-80, 80] degrees double anteriorPosteriorCS2range[2] = {convertDegreesToRadians(-80), convertDegreesToRadians(80)}; anteriorPosteriorCS[2].setRange(anteriorPosteriorCS2range); anteriorPosteriorCS[2].setDefaultValue(0); // This first model can't twist; we'll unlock this for the next model. anteriorPosteriorCS[2].setDefaultLocked(true);
TODO after we've defined the joints.
cat.addBody(anteriorBody); cat.addBody(posteriorBody);
A.3: Display geometry
using OpenSim::DisplayGeometry; // So that we can see what the cat's up to. // By default, the cylinder has a diameter of 1 meter, height of 1 meter, // its centroid is at (0, 0, 0) (its origin), and its axis of symmetry is // its Y axis. // Anterior body // ````````````` // 'cylinder.vtp' is in the Geometry folder of an OpenSim installation. DisplayGeometry * anteriorDisplay = new DisplayGeometry("cylinder.vtp"); anteriorDisplay->setOpacity(0.5); anteriorDisplay->setColor(Vec3(0.5, 0.5, 0.5)); // We want the centroid to be at (-0.5 * segmentalLength, 0, 0), and for // its axis of symmetry to be its body's (i.e., the body it's helping us // to visualize) X axis. SimTK::Rotation rot; // Rotate the cylinder's symmetry (Y) axis to align with the body's X axis: rot.setRotationFromAngleAboutZ(0.5 * Pi); // Tranform combines rotation and translation: anteriorDisplay->setTransform( SimTK::Transform(rot, Vec3(-0.5 * segmentalLength, 0, 0))); anteriorDisplay->setScaleFactors( Vec3(segmentalDiam, segmentalLength, segmentalDiam)); anteriorBody->updDisplayer()->updGeometrySet().adoptAndAppend(anteriorDisplay); anteriorBody->updDisplayer()->setShowAxes(true); // Posterior body // `````````````` DisplayGeometry * posteriorDisplay = new DisplayGeometry("cylinder.vtp"); posteriorDisplay->setOpacity(0.5); posteriorDisplay->setColor(Vec3(0.7, 0.7, 0.7)); posteriorDisplay->setTransform( SimTK::Transform(rot, Vec3(0.5 * segmentalLength, 0, 0))); posteriorDisplay->setScaleFactors( Vec3(segmentalDiam, segmentalLength, segmentalDiam)); posteriorBody->updDisplayer()->updGeometrySet().adoptAndAppend(posteriorDisplay); posteriorBody->updDisplayer()->setShowAxes(true);
A.4: Actuation
// Since the coordinates between the segments are angles, the actuators are // effectively torque actuators. The reason to use a CoordinateActuator // instead of a TorqueActuator is that for a CoordinateActuator we needn't // specify the axis of the actuation, or the bodies on which it acts. using OpenSim::CoordinateActuator; // hunch CoordinateActuator * hunchAct = new CoordinateActuator("hunch"); hunchAct->setName("hunch_actuator"); hunchAct->setMinControl(-maxTorque); hunchAct->setMaxControl(maxTorque); cat.addForce(hunchAct); // wag CoordinateActuator * wagAct = new CoordinateActuator("wag"); wagAct->setName("wag_actuator"); wagAct->setMinControl(-maxTorque); wagAct->setMaxControl(maxTorque); cat.addForce(wagAct);
cat.print("flippinfelines_hunch_wag.osim");
B: Second model: adding legs for the variable-inertia mechanism of flipping
// First model: exhibiting the counter-rotation mechanism of flipping // ======================================================================== // ********** PART A ********** // Second model: adding legs for the variable-inertia mechanism of flipping // ======================================================================== // ********** PART B ********** // Define leg bodies // ------------------------------------------------------------------------ // ********** PART B.1 ********** // Define leg joints (i.e., between legs and two halves of cat) // ------------------------------------------------------------------------ // ********** PART B.2 ********** // Display geometry // ------------------------------------------------------------------------ // ********** PART B.3 ********** // Actuation // ------------------------------------------------------------------------ // ********** PART B.4 ********** // Enforce joint limits on the legs // ------------------------------------------------------------------------ // ********** PART B.5 **********
// NOTE: Most of the set-up for this model has already been done. Many of // ---- the variables defined above are simply updated or renamed below. // This model will additionally be able to twist, and has legs. cat.setName("Leland_hunch_wag_twist_legs"); // Allow twist. anteriorPosteriorCS[2].setDefaultLocked(false); CoordinateActuator * twistAct = new CoordinateActuator("twist"); twistAct->setName("twist_actuator"); twistAct->setMinControl(-maxTorque); twistAct->setMaxControl(maxTorque); cat.addForce(twistAct); // Leg properties // ------------------------------------------------------------------------ double legsLength = 0.125; // m double legsDiam = 0.1 * legsLength; // m // Sum of both legs (60% distance across the belly): double legsWidth = 0.6 * segmentalDiam; // m double legsMass = 0.2; // kg
B.1: Define leg bodies
// Scale the segmental inertia. Inertia legsInertia = (legsMass/segmentalMass) * segmentalInertia; // Anterior and posterior legs. Body * anteriorLegs = new Body(); anteriorLegs->setName("anteriorLegs"); anteriorLegs->setMass(legsMass); anteriorLegs->setMassCenter(Vec3(0.5 * legsLength, 0, 0)); anteriorLegs->setInertia(legsInertia); Body * posteriorLegs = new Body(); posteriorLegs->setName("posteriorLegs"); posteriorLegs->setMass(legsMass); posteriorLegs->setMassCenter(Vec3(0.5 * legsLength, 0, 0)); posteriorLegs->setInertia(legsInertia);
B.2: Define leg joints (i.e., between legs and two halves of cat)
using OpenSim::PinJoint; // Anterior leg // ```````````` Vec3 locALegsInAnterior(-0.75 * segmentalLength, 0.5 * segmentalDiam, 0); Vec3 orientALegsInAnterior(0); Vec3 locALegsInLegs(0); // Rotate the leg about what will become the pin-joint axis (the leg's // Z axis) so that it points straight out from the belly when leg angle // is 0. In other words, position the leg's long (X) axis normal to the // half of the cat. Vec3 orientALegsInLegs(0, 0, -0.5 * Pi); PinJoint * anteriorToLegs = new PinJoint("anterior_legs", *anteriorBody, locALegsInAnterior, orientALegsInAnterior, *anteriorLegs, locALegsInLegs, orientALegsInLegs); CoordinateSet & anteriorToLegsCS = anteriorToLegs->upd_CoordinateSet(); anteriorToLegsCS[0].setName("frontLegs"); double anteriorToLegsCS0range[2] = {-0.5 * Pi, 0.5 * Pi}; anteriorToLegsCS[0].setRange(anteriorToLegsCS0range); // So that the legs are directed strictly upwards initially: double pitch = groundAnteriorCS[1].getDefaultValue(); anteriorToLegsCS[0].setDefaultValue(-pitch); anteriorToLegsCS[0].setDefaultLocked(false);
// Posterior leg // ````````````` Vec3 locPLegsInPosterior(0.75 * segmentalLength, 0.5 * segmentalDiam, 0); Vec3 orientPLegsInPosterior(0, Pi, 0); Vec3 locPLegsInLegs(0); Vec3 orientPLegsInLegs(0, 0, -0.5 * Pi); PinJoint * posteriorToLegs = new PinJoint("posterior_legs", *posteriorBody, locPLegsInPosterior, orientPLegsInPosterior, *posteriorLegs, locPLegsInLegs, orientPLegsInLegs); CoordinateSet & posteriorToLegsCS = posteriorToLegs->upd_CoordinateSet(); posteriorToLegsCS[0].setName("backLegs"); double posteriorToLegsCS0range[2] = {-0.5 * Pi, 0.5 * Pi}; posteriorToLegsCS[0].setRange(posteriorToLegsCS0range); posteriorToLegsCS[0].setDefaultValue(-pitch); posteriorToLegsCS[0].setDefaultLocked(false);
cat.addBody(anteriorLegs); cat.addBody(posteriorLegs);
B.3: Display geometry
// Both legs have the same display geometry. // 'box.vtp' is in the Geometry folder of an OpenSim installation. DisplayGeometry legsDisplay = DisplayGeometry("box.vtp"); legsDisplay.setOpacity(0.5); legsDisplay.setColor(Vec3(0.7, 0.7, 0.7)); legsDisplay.setTransform(Transform(Vec3(0.3 * legsLength, 0, 0))); legsDisplay.setScaleFactors(Vec3(legsLength, legsDiam, legsWidth)); anteriorLegs->updDisplayer()->updGeometrySet().cloneAndAppend(legsDisplay); anteriorLegs->updDisplayer()->setShowAxes(true); posteriorLegs->updDisplayer()->updGeometrySet().cloneAndAppend(legsDisplay); posteriorLegs->updDisplayer()->setShowAxes(true);
B.4: Actuation
// front legs. CoordinateActuator * frontLegsAct = new CoordinateActuator("frontLegs"); frontLegsAct->setName("frontLegs_actuator"); frontLegsAct->setMinControl(-maxTorque); frontLegsAct->setMaxControl(maxTorque); cat.addForce(frontLegsAct); // back legs. CoordinateActuator * backLegsAct = new CoordinateActuator("backLegs"); backLegsAct->setName("backLegs_actuator"); backLegsAct->setMinControl(-maxTorque); backLegsAct->setMaxControl(maxTorque); cat.addForce(backLegsAct);
B.5: Enforce joint limits on the legs
using OpenSim::CoordinateLimitForce; CoordinateLimitForce * frontLegsLimitForce = new CoordinateLimitForce( "frontLegs", 90, 1.0E2, -90, 1.0E2, 1.0E1, 2.0, false); cat.addForce(frontLegsLimitForce); CoordinateLimitForce * backLegsLimitForce = new CoordinateLimitForce( "backLegs", 90, 1.0E2, -90, 1.0E2, 1.0E1, 2.0, false); cat.addForce(backLegsLimitForce);
cat.print("flippinfelines_hunch_wag_twist_legs.osim");