|
23 | 23 |
|
24 | 24 | #include <OpenSim/Actuators/ModelFactory.h> |
25 | 25 | #include <OpenSim/Actuators/Millard2012EquilibriumMuscle.h> |
| 26 | +#include <OpenSim/Common/CommonUtilities.h> |
26 | 27 | #include <OpenSim/Common/Constant.h> |
27 | 28 | #include <OpenSim/Simulation/Control/PrescribedController.h> |
28 | 29 | #include <OpenSim/Simulation/Manager/Manager.h> |
|
31 | 32 | #include <OpenSim/Simulation/Model/Scholz2015GeometryPath.h> |
32 | 33 | #include <OpenSim/Simulation/SimbodyEngine/SliderJoint.h> |
33 | 34 | #include <OpenSim/Simulation/SimbodyEngine/FreeJoint.h> |
| 35 | +#include <OpenSim/Simulation/SimbodyEngine/PinJoint.h> |
| 36 | +#include <OpenSim/Simulation/SimbodyEngine/WeldJoint.h> |
34 | 37 |
|
35 | 38 | #include <OpenSim/Simulation/VisualizerUtilities.h> |
36 | 39 |
|
@@ -146,6 +149,7 @@ TEST_CASE("Suspended pendulum") { |
146 | 149 |
|
147 | 150 | // Set the path's origin and insertion. |
148 | 151 | Scholz2015GeometryPath& path = actu->updPath<Scholz2015GeometryPath>(); |
| 152 | + path.setUseWarmStart(true); |
149 | 153 | path.appendPathPoint(model.getGround(), SimTK::Vec3(-0.1, 0, 0)); |
150 | 154 |
|
151 | 155 | // Check that pendulum is at rest with the expected length. |
@@ -596,6 +600,7 @@ TEST_CASE("Path with all surfaces and a via point") { |
596 | 600 |
|
597 | 601 | // Create the path object. |
598 | 602 | Scholz2015GeometryPath* path = new Scholz2015GeometryPath(); |
| 603 | + path->setUseWarmStart(true); |
599 | 604 | path->appendPathPoint(*originBody, SimTK::Vec3(0.)); |
600 | 605 |
|
601 | 606 | // Add the first torus obstacle. |
@@ -1014,3 +1019,126 @@ TEST_CASE("Scaling updates muscle properties") { |
1014 | 1019 | CHECK(scaledMuscle.get_optimal_fiber_length() == 0.123); |
1015 | 1020 | CHECK(scaledMuscle.get_tendon_slack_length() == 0.123); |
1016 | 1021 | } |
| 1022 | + |
| 1023 | +TEST_CASE("Disabling warm starts unifies moment arm calculations") { |
| 1024 | + using DataPoints = std::vector<std::pair<double, double>>; |
| 1025 | + |
| 1026 | + // A simple pendulum model with a path wrapping over an ellipsoid obstacle. |
| 1027 | + // The model is designed so that, depending on the choice of the default |
| 1028 | + // value for the 'angle' coordinate, moment arm calculations may differ when |
| 1029 | + // using the warm start feature of Scholz2015GeometryPath. |
| 1030 | + auto buildModel = [](bool warmStart, double defaultAngle) { |
| 1031 | + Model model; |
| 1032 | + model.setName("pendulum"); |
| 1033 | + |
| 1034 | + // Define the pendulum bodies: base and head. |
| 1035 | + auto* base = new Body("pendulum_base", 1.0, SimTK::Vec3(0), |
| 1036 | + SimTK::Inertia(1)); |
| 1037 | + model.addBody(base); |
| 1038 | + auto* head = new Body("pendulum_head", 1.0, SimTK::Vec3(0), |
| 1039 | + SimTK::Inertia(1)); |
| 1040 | + model.addBody(head); |
| 1041 | + |
| 1042 | + // Base welded 1.5 m above ground. |
| 1043 | + auto* weld = new WeldJoint("ground_base", |
| 1044 | + model.getGround(), SimTK::Vec3(0, 1.5, 0), SimTK::Vec3(0), |
| 1045 | + *base, SimTK::Vec3(0), SimTK::Vec3(0)); |
| 1046 | + model.addJoint(weld); |
| 1047 | + |
| 1048 | + // Add a PinJoint between the base and head driven by the coordinate |
| 1049 | + // 'angle'. |
| 1050 | + auto* pin = new PinJoint("base_to_head", |
| 1051 | + *base, SimTK::Vec3(0), SimTK::Vec3(0), |
| 1052 | + *head, SimTK::Vec3(0, 1, 0), SimTK::Vec3(0)); |
| 1053 | + auto& angle = pin->updCoordinate(); |
| 1054 | + angle.setName("angle"); |
| 1055 | + angle.setRangeMin(-0.5); |
| 1056 | + angle.setRangeMax(0.5); |
| 1057 | + angle.setDefaultValue(defaultAngle); |
| 1058 | + model.addJoint(pin); |
| 1059 | + |
| 1060 | + // Attach a contact ellipsoid to the base which the muscle path will |
| 1061 | + // wrap over. |
| 1062 | + auto* ellipsoid = new ContactEllipsoid( |
| 1063 | + SimTK::Vec3(0.1, 0.1, 0.5), |
| 1064 | + SimTK::Vec3(-0.01, -0.25, -0.035), |
| 1065 | + SimTK::Vec3(0.25, -0.29, 0.075), |
| 1066 | + *base); |
| 1067 | + ellipsoid->setName("ellipsoid"); |
| 1068 | + model.addComponent(ellipsoid); |
| 1069 | + |
| 1070 | + // Add muscle with a path between the head and base, wrapping over the |
| 1071 | + // ellipsoid. |
| 1072 | + auto* muscle = new Millard2012EquilibriumMuscle(); |
| 1073 | + muscle->setName("muscle"); |
| 1074 | + muscle->set_path(Scholz2015GeometryPath()); |
| 1075 | + model.addForce(muscle); |
| 1076 | + auto& path = muscle->updPath<Scholz2015GeometryPath>(); |
| 1077 | + path.appendPathPoint(*head, SimTK::Vec3(0)); |
| 1078 | + path.appendObstacle(*ellipsoid, SimTK::Vec3(0.10, 0, 0)); |
| 1079 | + path.appendPathPoint(*base, SimTK::Vec3(0)); |
| 1080 | + path.setUseWarmStart(warmStart); |
| 1081 | + return model; |
| 1082 | + }; |
| 1083 | + |
| 1084 | + // Sweep over the range of motion of the 'angle' coordinate, computing the |
| 1085 | + // moment arm of the muscle path at each point. This replicates the |
| 1086 | + // behavior of OpenSim Creator's moment arm plotting functionality, where |
| 1087 | + // the underlying bug was originally discovered. |
| 1088 | + auto computeDataPoints = [&](bool warmStart, double defaultAngle) |
| 1089 | + -> DataPoints { |
| 1090 | + Model model = buildModel(warmStart, defaultAngle); |
| 1091 | + SimTK::State state = model.initSystem(); |
| 1092 | + const Coordinate& coordinate = |
| 1093 | + model.getComponent<Coordinate>("/jointset/base_to_head/angle"); |
| 1094 | + const Muscle& muscle = model.getComponent<Muscle>("/forceset/muscle"); |
| 1095 | + const double start = coordinate.getRangeMin(); |
| 1096 | + const double end = coordinate.getRangeMax(); |
| 1097 | + SimTK::Vector x = createVectorLinspace(202, start, end); |
| 1098 | + DataPoints data_points; |
| 1099 | + for (int i = 0; i < x.size(); ++i) { |
| 1100 | + coordinate.setValue(state, x[i]); |
| 1101 | + model.equilibrateMuscles(state); |
| 1102 | + model.realizeReport(state); |
| 1103 | + const double y = muscle.getPath().computeMomentArm(state, |
| 1104 | + coordinate); |
| 1105 | + data_points.emplace_back(x[i], y); |
| 1106 | + } |
| 1107 | + return data_points; |
| 1108 | + }; |
| 1109 | + |
| 1110 | + // Compare two sets of data points sampled at the same coordinate values, |
| 1111 | + // checking that the moment arm agrees at every point across the range of |
| 1112 | + // motion. |
| 1113 | + auto compareDataPoints = [](const DataPoints& lhs, const DataPoints& rhs, |
| 1114 | + double tolerance, bool useWarmStart = false) { |
| 1115 | + REQUIRE(lhs.size() == rhs.size()); |
| 1116 | + SimTK::Vector error(static_cast<int>(lhs.size()), 0.0); |
| 1117 | + for (int i = 0; i < static_cast<int>(lhs.size()); ++i) { |
| 1118 | + error[i] = std::abs(lhs[i].second - rhs[i].second); |
| 1119 | + } |
| 1120 | + |
| 1121 | + // If using a warm start, we expect the error to be non-zero (i.e., the |
| 1122 | + // moment arm curves different) when using different default values for |
| 1123 | + // 'angle'. |
| 1124 | + if (useWarmStart) { |
| 1125 | + CHECK_THAT(error.norm(), |
| 1126 | + !Catch::Matchers::WithinAbs(0.0, tolerance)); |
| 1127 | + } else { |
| 1128 | + CHECK_THAT(error.norm(), |
| 1129 | + Catch::Matchers::WithinAbs(0.0, tolerance)); |
| 1130 | + } |
| 1131 | + |
| 1132 | + CAPTURE(error); |
| 1133 | + }; |
| 1134 | + |
| 1135 | + // Test that the moment arm curves are identical when using different |
| 1136 | + // default values for the 'angle' coordinate, when warm starts are disabled. |
| 1137 | + // Conversely, when warm starts are enabled, the moment arm curves should |
| 1138 | + // differ depending on the default value of 'angle'. |
| 1139 | + bool useWarmStart = GENERATE(false, true); |
| 1140 | + CAPTURE(useWarmStart); |
| 1141 | + auto lhs = computeDataPoints(useWarmStart, 0.0); |
| 1142 | + auto rhs = computeDataPoints(useWarmStart, 0.45); |
| 1143 | + compareDataPoints(lhs, rhs, 1e-6, useWarmStart); |
| 1144 | +} |
0 commit comments