diff --git a/dart/biomechanics/SubjectOnDisk.cpp b/dart/biomechanics/SubjectOnDisk.cpp index 3388b83d9..077ceab40 100644 --- a/dart/biomechanics/SubjectOnDisk.cpp +++ b/dart/biomechanics/SubjectOnDisk.cpp @@ -181,6 +181,118 @@ MissingGRFReason missingGRFReasonFromProto(proto::MissingGRFReason reason) return notMissingGRF; } +BasicTrialType basicTrialTypeFromProto(proto::BasicTrialType type) +{ + switch (type) + { + case proto::BasicTrialType::treadmill: + return treadmill; + case proto::BasicTrialType::overground: + return overground; + case proto::BasicTrialType::staticTrial: + return staticTrial; + case proto::BasicTrialType::other: + return other; + case proto::BasicTrialType_INT_MIN_SENTINEL_DO_NOT_USE_: + return other; + break; + case proto::BasicTrialType_INT_MAX_SENTINEL_DO_NOT_USE_: + return other; + break; + } + return other; +} + +proto::BasicTrialType basicTrialTypeToProto(BasicTrialType type) +{ + switch (type) + { + case treadmill: + return proto::BasicTrialType::treadmill; + case overground: + return proto::BasicTrialType::overground; + case staticTrial: + return proto::BasicTrialType::staticTrial; + case other: + return proto::BasicTrialType::other; + } + return proto::BasicTrialType::other; +} + +DataQuality dataQualityFromProto(proto::DataQuality quality) +{ + switch (quality) + { + case proto::DataQuality::pilotData: + return pilotData; + case proto::DataQuality::experimentalData: + return experimentalData; + case proto::DataQuality::internetData: + return internetData; + case proto::DataQuality_INT_MIN_SENTINEL_DO_NOT_USE_: + return internetData; + break; + case proto::DataQuality_INT_MAX_SENTINEL_DO_NOT_USE_: + return internetData; + break; + } + return internetData; +} + +proto::DataQuality dataQualityToProto(DataQuality quality) +{ + switch (quality) + { + case pilotData: + return proto::DataQuality::pilotData; + case experimentalData: + return proto::DataQuality::experimentalData; + case internetData: + return proto::DataQuality::internetData; + } + return proto::DataQuality::pilotData; +} + +DetectedTrialFeature detectedTrialFeatureFromProto( + proto::DetectedTrialFeature feature) +{ + switch (feature) + { + case proto::DetectedTrialFeature::walking: + return walking; + case proto::DetectedTrialFeature::running: + return running; + case proto::DetectedTrialFeature::unevenTerrain: + return unevenTerrain; + case proto::DetectedTrialFeature::flatTerrain: + return flatTerrain; + case proto::DetectedTrialFeature_INT_MIN_SENTINEL_DO_NOT_USE_: + return walking; + break; + case proto::DetectedTrialFeature_INT_MAX_SENTINEL_DO_NOT_USE_: + return walking; + break; + } + return walking; +} + +proto::DetectedTrialFeature detectedTrialFeatureToProto( + DetectedTrialFeature feature) +{ + switch (feature) + { + case walking: + return proto::DetectedTrialFeature::walking; + case running: + return proto::DetectedTrialFeature::running; + case unevenTerrain: + return proto::DetectedTrialFeature::unevenTerrain; + case flatTerrain: + return proto::DetectedTrialFeature::flatTerrain; + } + return proto::DetectedTrialFeature::walking; +} + SubjectOnDisk::SubjectOnDisk(const std::string& path) : mPath(path), mLoadedAllFrames(false) { @@ -1490,6 +1602,12 @@ std::vector SubjectOnDisk::getMissingGRF(int trial) return mHeader->mTrials[trial]->mMissingGRFReason; } +/// This returns the user supplied enum of type 'DataQuality' +DataQuality SubjectOnDisk::getQuality() +{ + return mHeader->getQuality(); +} + int SubjectOnDisk::getNumProcessingPasses() { return mHeader->mPasses.size(); @@ -3193,6 +3311,17 @@ void SubjectOnDiskTrial::setMissingGRFReason( mMissingGRFReason = missingGRFReason; } +std::vector SubjectOnDiskTrial::getHasManualGRFAnnotation() +{ + return mHasManualGRFAnnotation; +} + +void SubjectOnDiskTrial::setHasManualGRFAnnotation( + std::vector hasManualGRFAnnotation) +{ + mHasManualGRFAnnotation = hasManualGRFAnnotation; +} + void SubjectOnDiskTrial::setCustomValues( std::vector customValues) { @@ -3250,6 +3379,27 @@ std::vector SubjectOnDiskTrial::getForcePlates() return mForcePlates; } +void SubjectOnDiskTrial::setBasicTrialType(BasicTrialType type) +{ + mBasicTrialType = type; +} + +BasicTrialType SubjectOnDiskTrial::getBasicTrialType() +{ + return mBasicTrialType; +} + +void SubjectOnDiskTrial::setDetectedTrialFeatures( + std::vector features) +{ + mDetectedTrialFeatures = features; +} + +std::vector SubjectOnDiskTrial::getDetectedTrialFeatures() +{ + return mDetectedTrialFeatures; +} + std::shared_ptr SubjectOnDiskTrial::addPass() { mTrialPasses.push_back(std::make_shared()); @@ -3305,6 +3455,19 @@ void SubjectOnDiskTrial::read(const proto::SubjectOnDiskTrialHeader& proto) mMissingGRFReason.push_back( missingGRFReasonFromProto(proto.missing_grf_reason(i))); } + mHasManualGRFAnnotation.clear(); + for (int i = 0; i < proto.has_manual_grf_annotation_size(); i++) + { + mHasManualGRFAnnotation.push_back(proto.has_manual_grf_annotation(i)); + } + + mBasicTrialType = basicTrialTypeFromProto(proto.trial_type()); + mDetectedTrialFeatures.clear(); + for (int i = 0; i < proto.detected_trial_feature_size(); i++) + { + mDetectedTrialFeatures.push_back( + detectedTrialFeatureFromProto(proto.detected_trial_feature(i))); + } // /////////////////////////////////////////////////////////////////////////// // // Raw sensor observations, which are shared across processing passes @@ -3378,6 +3541,16 @@ void SubjectOnDiskTrial::write(proto::SubjectOnDiskTrialHeader* proto) proto->add_missing_grf_reason( missingGRFReasonToProto(mMissingGRFReason[i])); } + for (int i = 0; i < mHasManualGRFAnnotation.size(); i++) + { + proto->add_has_manual_grf_annotation(mHasManualGRFAnnotation[i]); + } + proto->set_trial_type(basicTrialTypeToProto(mBasicTrialType)); + for (int i = 0; i < mDetectedTrialFeatures.size(); i++) + { + proto->add_detected_trial_feature( + detectedTrialFeatureToProto(mDetectedTrialFeatures[i])); + } // /////////////////////////////////////////////////////////////////////////// // // Raw sensor observations, which are shared across processing passes @@ -3547,6 +3720,17 @@ SubjectOnDiskHeader& SubjectOnDiskHeader::setNotes(const std::string& notes) return *this; } +SubjectOnDiskHeader& SubjectOnDiskHeader::setQuality(DataQuality quality) +{ + mDataQuality = quality; + return *this; +} + +DataQuality SubjectOnDiskHeader::getQuality() +{ + return mDataQuality; +} + std::shared_ptr SubjectOnDiskHeader::addProcessingPass() { @@ -3782,6 +3966,7 @@ void SubjectOnDiskHeader::write(dart::proto::SubjectOnDiskHeader* header) { header->add_exo_dof_index(index); } + header->set_data_quality(dataQualityToProto(mDataQuality)); if (!header->IsInitialized()) { @@ -3926,6 +4111,8 @@ void SubjectOnDiskHeader::read(const dart::proto::SubjectOnDiskHeader& proto) { mExoDofIndices.push_back(proto.exo_dof_index(i)); } + + mDataQuality = dataQualityFromProto(proto.data_quality()); } void SubjectOnDiskHeader::writeSensorsFrame( diff --git a/dart/biomechanics/SubjectOnDisk.hpp b/dart/biomechanics/SubjectOnDisk.hpp index 0f4d0c177..3bcb4619b 100644 --- a/dart/biomechanics/SubjectOnDisk.hpp +++ b/dart/biomechanics/SubjectOnDisk.hpp @@ -347,6 +347,8 @@ class SubjectOnDiskTrial void setOriginalTrialEndTime(s_t endTime); std::vector getMissingGRFReason(); void setMissingGRFReason(std::vector missingGRFReason); + std::vector getHasManualGRFAnnotation(); + void setHasManualGRFAnnotation(std::vector hasManualGRFAnnotation); void setCustomValues(std::vector customValues); void setMarkerNamesGuessed(bool markersGuessed); std::vector> getMarkerObservations(); @@ -361,6 +363,10 @@ class SubjectOnDiskTrial void setExoTorques(std::map exoTorques); void setForcePlates(std::vector forcePlates); std::vector getForcePlates(); + void setBasicTrialType(BasicTrialType type); + BasicTrialType getBasicTrialType(); + void setDetectedTrialFeatures(std::vector features); + std::vector getDetectedTrialFeatures(); std::shared_ptr addPass(); std::vector> getPasses(); void read(const proto::SubjectOnDiskTrialHeader& proto); @@ -373,6 +379,7 @@ class SubjectOnDiskTrial std::vector mTrialTags; std::vector> mTrialPasses; std::vector mMissingGRFReason; + std::vector mHasManualGRFAnnotation; // This is true if we guessed the marker names, and false if we got them from // the uploaded user's file, which implies that they got them from human // observations. @@ -385,6 +392,9 @@ class SubjectOnDiskTrial s_t mOriginalTrialStartTime; s_t mOriginalTrialEndTime; + BasicTrialType mBasicTrialType; + std::vector mDetectedTrialFeatures; + /////////////////////////////////////////////////////////////////////////// // Recovered proto summaries, for incremental loading of Frames /////////////////////////////////////////////////////////////////////////// @@ -453,6 +463,8 @@ class SubjectOnDiskHeader SubjectOnDiskHeader& setSubjectTags(std::vector subjectTags); SubjectOnDiskHeader& setHref(const std::string& sourceHref); SubjectOnDiskHeader& setNotes(const std::string& notes); + SubjectOnDiskHeader& setQuality(DataQuality quality); + DataQuality getQuality(); std::shared_ptr addProcessingPass(); std::vector> getProcessingPasses(); std::shared_ptr addTrial(); @@ -512,6 +524,9 @@ class SubjectOnDiskHeader // This is exoskeleton data std::vector mExoDofIndices; + // This is the user supplied quality of the data + DataQuality mDataQuality; + friend class SubjectOnDisk; friend struct Frame; friend struct FramePass; @@ -627,6 +642,9 @@ class SubjectOnDisk /// include `notMissingGRF`. std::vector getMissingGRF(int trial); + /// This returns the user supplied enum of type 'DataQuality' + DataQuality getQuality(); + int getNumProcessingPasses(); ProcessingPassType getProcessingPassType(int processingPass); diff --git a/dart/biomechanics/enums.hpp b/dart/biomechanics/enums.hpp index 733880856..5b8ad9b47 100644 --- a/dart/biomechanics/enums.hpp +++ b/dart/biomechanics/enums.hpp @@ -29,6 +29,29 @@ enum MissingGRFReason extendedToNearestPeakForce }; +enum BasicTrialType +{ + treadmill, + overground, + staticTrial, + other +}; + +enum DetectedTrialFeature +{ + walking, + running, + unevenTerrain, + flatTerrain +}; + +enum DataQuality +{ + pilotData, + experimentalData, + internetData +}; + enum MissingGRFStatus { no = 0, // no will cast to `false` diff --git a/dart/proto/SubjectOnDisk.proto b/dart/proto/SubjectOnDisk.proto index 0ebd6f17d..80463e40b 100644 --- a/dart/proto/SubjectOnDisk.proto +++ b/dart/proto/SubjectOnDisk.proto @@ -30,6 +30,26 @@ enum ProcessingPassType { accMinimizingFilter = 3; }; +enum BasicTrialType { + treadmill = 0; + overground = 1; + staticTrial = 2; + other = 3; +}; + +enum DetectedTrialFeature { + walking = 0; + running = 1; + unevenTerrain = 2; + flatTerrain = 3; +} + +enum DataQuality { + pilotData = 0; + experimentalData = 1; + internetData = 2; +} + // Many of the ML tasks we want to support from SubjectOnDisk data include // effectively predicting the results of a downstream processing task from // an upstream processing task. Trivially, that's predicting physics from @@ -79,6 +99,7 @@ message SubjectOnDiskTrialHeader { // memory, but we really want to know this information when randomly picking // frames from the subject to sample. repeated MissingGRFReason missing_GRF_reason = 2; + repeated bool has_manual_GRF_annotation = 16; // This is how many frames are in this trial int32 trial_length = 3; // This is the timestep used in this trial (assumed constant throughout the trial) @@ -102,6 +123,10 @@ message SubjectOnDiskTrialHeader { int32 original_trial_end_frame = 13; float original_trial_start_time = 14; float original_trial_end_time = 15; + // This is the type of trial we're dealing with + BasicTrialType trial_type = 17; + // This is the detected features of this trial + repeated DetectedTrialFeature detected_trial_feature = 18; } message SubjectOnDiskPass { @@ -147,6 +172,8 @@ message SubjectOnDiskHeader { repeated int32 exo_dof_index = 22; // Details about the subject tags provided on the AddBiomechanics platform repeated string subject_tag = 23; + // This is what the user has tagged this subject as, in terms of data quality + DataQuality data_quality = 25; } message SubjectOnDiskProcessingPassFrame { diff --git a/python/_nimblephysics/biomechanics/SubjectOnDisk.cpp b/python/_nimblephysics/biomechanics/SubjectOnDisk.cpp index bb4e16e5a..6e13db8e8 100644 --- a/python/_nimblephysics/biomechanics/SubjectOnDisk.cpp +++ b/python/_nimblephysics/biomechanics/SubjectOnDisk.cpp @@ -50,6 +50,66 @@ void SubjectOnDisk(py::module& m) "This is the pass where we apply an acceleration minimizing " "filter to the kinematics and dynamics."); + auto basicTrialType + = ::py::enum_(m, "BasicTrialType") + .value( + "TREADMILL", + dart::biomechanics::BasicTrialType::treadmill, + "This is a trial where the subject is walking or " + "running on a treadmill.") + .value( + "OVERGROUND", + dart::biomechanics::BasicTrialType::overground, + "This is a trial where the subject is walking or " + "running overground.") + .value( + "STATIC_TRIAL", + dart::biomechanics::BasicTrialType::staticTrial, + "This is a trial where the subject is standing " + "still.") + .value( + "OTHER", + dart::biomechanics::BasicTrialType::other, + "This is a trial that doesn't fit into any of the " + "other categories."); + + auto dataQuality + = ::py::enum_(m, "DataQuality") + .value( + "PILOT_DATA", + dart::biomechanics::DataQuality::pilotData, + "This is data that was collected as part of a pilot study.") + .value( + "EXPERIMENTAL_DATA", + dart::biomechanics::DataQuality::experimentalData, + "This is data that was collected as part of an experiment.") + .value( + "INTERNET_DATA", + dart::biomechanics::DataQuality::internetData, + "This is data that was collected from the internet."); + + auto detectedTrialFeature + = ::py::enum_( + m, "DetectedTrialFeature") + .value( + "WALKING", + dart::biomechanics::DetectedTrialFeature::walking, + "This is a trial where the subject is walking.") + .value( + "RUNNING", + dart::biomechanics::DetectedTrialFeature::running, + "This is a trial where the subject is running.") + .value( + "UNEVEN_TERRAIN", + dart::biomechanics::DetectedTrialFeature::unevenTerrain, + "This is a trial where the subject is walking or " + "running on uneven terrain.") + .value( + "FLAT_TERRAIN", + dart::biomechanics::DetectedTrialFeature::flatTerrain, + "This is a trial where the subject is walking or " + "running on flat terrain."); + auto framePass = ::py::class_< dart::biomechanics::FramePass, @@ -891,6 +951,15 @@ Note that these are specified in the local body frame, acting on the body at its .def( "getMissingGRFReason", &dart::biomechanics::SubjectOnDiskTrial::getMissingGRFReason) + .def( + "setHasManualGRFAnnotation", + &dart::biomechanics::SubjectOnDiskTrial:: + setHasManualGRFAnnotation, + ::py::arg("hasManualGRFAnnotation")) + .def( + "getHasManualGRFAnnotation", + &dart::biomechanics::SubjectOnDiskTrial:: + getHasManualGRFAnnotation) .def( "setCustomValues", &dart::biomechanics::SubjectOnDiskTrial::setCustomValues, @@ -929,6 +998,22 @@ Note that these are specified in the local body frame, acting on the body at its .def( "getForcePlates", &dart::biomechanics::SubjectOnDiskTrial::getForcePlates) + .def( + "setBasicTrialType", + &dart::biomechanics::SubjectOnDiskTrial::setBasicTrialType, + ::py::arg("type")) + .def( + "getBasicTrialType", + &dart::biomechanics::SubjectOnDiskTrial::getBasicTrialType) + .def( + "setDetectedTrialFeatures", + &dart::biomechanics::SubjectOnDiskTrial:: + setDetectedTrialFeatures, + ::py::arg("features")) + .def( + "getDetectedTrialFeatures", + &dart::biomechanics::SubjectOnDiskTrial:: + getDetectedTrialFeatures) .def( "addPass", &dart::biomechanics::SubjectOnDiskTrial::addPass, @@ -1019,6 +1104,13 @@ Note that these are specified in the local body frame, acting on the body at its "setNotes", &dart::biomechanics::SubjectOnDiskHeader::setNotes, ::py::arg("notes")) + .def( + "setQuality", + &dart::biomechanics::SubjectOnDiskHeader::setQuality, + ::py::arg("quality")) + .def( + "getQuality", + &dart::biomechanics::SubjectOnDiskHeader::getQuality) .def( "addProcessingPass", &dart::biomechanics::SubjectOnDiskHeader::addProcessingPass) @@ -1253,6 +1345,11 @@ Note that these are specified in the local body frame, acting on the body at its This method is provided to give a cheaper way to filter out frames we want to ignore for training, without having to call the more expensive :code:`loadFrames()` and examine frames individually. )doc") + .def( + "getQuality", + &dart::biomechanics::SubjectOnDisk::getQuality, + "This returns the user-supplied quality of the data in this " + "subject") // int getNumProcessingPasses(); .def( "getNumProcessingPasses", diff --git a/python/_nimblephysics/dynamics/EulerJoint.cpp b/python/_nimblephysics/dynamics/EulerJoint.cpp index 964a942a7..d1d5f006d 100644 --- a/python/_nimblephysics/dynamics/EulerJoint.cpp +++ b/python/_nimblephysics/dynamics/EulerJoint.cpp @@ -44,17 +44,17 @@ namespace python { void EulerJoint(py::module& m) { - ::py::enum_(m, "AxisOrder") - .value("XYZ", dart::dynamics::EulerJoint::AxisOrder::XYZ) - .value("XZY", dart::dynamics::EulerJoint::AxisOrder::XZY) - .value("ZYX", dart::dynamics::EulerJoint::AxisOrder::ZYX) - .value("ZXY", dart::dynamics::EulerJoint::AxisOrder::ZXY); + ::py::enum_(m, "AxisOrder") + .value("XYZ", dart::dynamics::detail::AxisOrder::XYZ) + .value("XZY", dart::dynamics::detail::AxisOrder::XZY) + .value("ZYX", dart::dynamics::detail::AxisOrder::ZYX) + .value("ZXY", dart::dynamics::detail::AxisOrder::ZXY); ::py::class_( m, "EulerJointUniqueProperties") .def(::py::init<>()) .def( - ::py::init(), + ::py::init(), ::py::arg("axisOrder")); ::py::class_< @@ -162,21 +162,21 @@ void EulerJoint(py::module& m) .def( "setAxisOrder", +[](dart::dynamics::EulerJoint* self, - dart::dynamics::EulerJoint::AxisOrder _order) { + dart::dynamics::detail::AxisOrder _order) { self->setAxisOrder(_order); }, ::py::arg("order")) .def( "setAxisOrder", +[](dart::dynamics::EulerJoint* self, - dart::dynamics::EulerJoint::AxisOrder _order, + dart::dynamics::detail::AxisOrder _order, bool _renameDofs) { self->setAxisOrder(_order, _renameDofs); }, ::py::arg("order"), ::py::arg("renameDofs")) .def( "getAxisOrder", +[](const dart::dynamics::EulerJoint* self) - -> dart::dynamics::EulerJoint::AxisOrder { + -> dart::dynamics::detail::AxisOrder { return self->getAxisOrder(); }) .def( @@ -209,7 +209,7 @@ void EulerJoint(py::module& m) .def_static( "convertToTransformOf", +[](const Eigen::Vector3s& _positions, - dart::dynamics::EulerJoint::AxisOrder _ordering, + dart::dynamics::detail::AxisOrder _ordering, const Eigen::Vector3s& flipAxisMap = Eigen::Vector3s::Ones()) -> Eigen::Isometry3s { return dart::dynamics::EulerJoint::convertToTransform( @@ -221,7 +221,7 @@ void EulerJoint(py::module& m) .def_static( "convertToRotationOf", +[](const Eigen::Vector3s& _positions, - dart::dynamics::EulerJoint::AxisOrder _ordering, + dart::dynamics::detail::AxisOrder _ordering, const Eigen::Vector3s& flipAxisMap = Eigen::Vector3s::Ones()) -> Eigen::Matrix3s { return dart::dynamics::EulerJoint::convertToRotation( diff --git a/python/_nimblephysics/math/Geometry.cpp b/python/_nimblephysics/math/Geometry.cpp index 80d3e6e18..a74723ffa 100644 --- a/python/_nimblephysics/math/Geometry.cpp +++ b/python/_nimblephysics/math/Geometry.cpp @@ -301,5 +301,20 @@ void Geometry(py::module& m) .def("computeHalfExtents", &dart::math::BoundingBox::computeHalfExtents); } +void EulerGeometry(py::module& m) +{ + m.def( + "roundEulerAnglesToNearest", + +[](Eigen::Vector3s angle, + Eigen::Vector3s previousAngle, + dynamics::detail::AxisOrder axisOrder) -> Eigen::Vector3s { + return dart::math::roundEulerAnglesToNearest( + angle, previousAngle, axisOrder); + }, + ::py::arg("angle"), + ::py::arg("previousAngle"), + ::py::arg("axisOrder") = dart::dynamics::detail::AxisOrder::XYZ); +} + } // namespace python } // namespace dart diff --git a/python/_nimblephysics/math/module.cpp b/python/_nimblephysics/math/module.cpp index c8879ea23..03c284ccf 100644 --- a/python/_nimblephysics/math/module.cpp +++ b/python/_nimblephysics/math/module.cpp @@ -43,7 +43,7 @@ void MultivariateGaussian(py::module& sm); void GraphFlowDiscretizer(py::module& sm); void PolynomialFitter(py::module& sm); -void dart_math(py::module& m) +py::module dart_math(py::module& m) { auto sm = m.def_submodule("math"); @@ -52,6 +52,14 @@ void dart_math(py::module& m) MultivariateGaussian(sm); GraphFlowDiscretizer(sm); PolynomialFitter(sm); + + return sm; +} + +void EulerGeometry(py::module& sm); +void dart_euler_math(py::module& sm) +{ + EulerGeometry(sm); } } // namespace python diff --git a/python/_nimblephysics/nimblephysics.cpp b/python/_nimblephysics/nimblephysics.cpp index 923bb17cd..4a358001d 100644 --- a/python/_nimblephysics/nimblephysics.cpp +++ b/python/_nimblephysics/nimblephysics.cpp @@ -43,7 +43,8 @@ namespace python { void eigen_geometry(py::module& m); void dart_common(py::module& m); -void dart_math(py::module& m); +py::module dart_math(py::module& m); +void dart_euler_math(py::module& m); void dart_dynamics(py::module& m); void dart_collision(py::module& m); void dart_constraint(py::module& m); @@ -71,9 +72,10 @@ PYBIND11_MODULE(_nimblephysics, m) eigen_geometry(m); dart_common(m); - dart_math(m); + py::module math_module = dart_math(m); dart_performance(m); dart_dynamics(m); + dart_euler_math(math_module); dart_collision(m); dart_constraint(m); dart_simulation_and_neural(m, neural, withRespectTo); diff --git a/python/nimblephysics.egg-info/PKG-INFO b/python/nimblephysics.egg-info/PKG-INFO index fca4fcae1..6043c3909 100644 --- a/python/nimblephysics.egg-info/PKG-INFO +++ b/python/nimblephysics.egg-info/PKG-INFO @@ -1,6 +1,6 @@ Metadata-Version: 2.1 Name: nimblephysics -Version: 0.10.45 +Version: 0.10.48 Summary: A differentiable fully featured physics engine Author: Keenon Werling Author-email: keenonwerling@gmail.com