Skip to content

Commit d111194

Browse files
authored
Handle tiny SDF inertial data and improve warnings (#2284)
1 parent d11fa33 commit d111194

File tree

2 files changed

+201
-0
lines changed

2 files changed

+201
-0
lines changed

dart/utils/sdf/SdfParser.cpp

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1081,6 +1081,7 @@ SDFBodyNode readBodyNode(
10811081
// Name attribute
10821082
std::string name = getAttributeString(bodyNodeElement, "name");
10831083
properties.mName = name;
1084+
const std::string bodyName = name;
10841085

10851086
//--------------------------------------------------------------------------
10861087
// gravity
@@ -1108,13 +1109,33 @@ SDFBodyNode readBodyNode(
11081109

11091110
//--------------------------------------------------------------------------
11101111
// inertia
1112+
constexpr double kMinReasonableMass = 1e-9; // 1 microgram
1113+
bool massSpecified = false;
11111114
if (hasElement(bodyNodeElement, "inertial")) {
11121115
const ElementPtr& inertiaElement = getElement(bodyNodeElement, "inertial");
11131116

11141117
// mass
11151118
if (hasElement(inertiaElement, "mass")) {
11161119
double mass = getValueDouble(inertiaElement, "mass");
1120+
if (mass <= 0.0) {
1121+
DART_WARN(
1122+
"[SdfParser] Link [{}] has non-positive mass [{}]. Clamping to {} "
1123+
"to continue parsing.",
1124+
bodyName,
1125+
mass,
1126+
kMinReasonableMass);
1127+
mass = kMinReasonableMass;
1128+
} else if (mass < kMinReasonableMass) {
1129+
DART_WARN(
1130+
"[SdfParser] Link [{}] has a very small mass [{} kg]; clamping to "
1131+
"{} to avoid numerical issues.",
1132+
bodyName,
1133+
mass,
1134+
kMinReasonableMass);
1135+
mass = kMinReasonableMass;
1136+
}
11171137
properties.mInertia.setMass(mass);
1138+
massSpecified = true;
11181139
}
11191140

11201141
// offset
@@ -1137,7 +1158,32 @@ SDFBodyNode readBodyNode(
11371158
double iyz = getValueDouble(moiElement, "iyz");
11381159

11391160
properties.mInertia.setMoment(ixx, iyy, izz, ixy, ixz, iyz);
1161+
} else if (massSpecified) {
1162+
// Keep the inertia physically meaningful by matching the moment scale
1163+
// to the specified mass; geometry is unknown, so use an isotropic guess.
1164+
const double mass = properties.mInertia.getMass();
1165+
properties.mInertia.setMoment(Eigen::Matrix3d::Identity() * mass);
1166+
DART_WARN(
1167+
"[SdfParser] Link [{}] defines <mass> but no <inertia>; using an "
1168+
"isotropic inertia tensor (mass * I). Provide <inertia> for "
1169+
"physically correct behavior.",
1170+
bodyName);
11401171
}
1172+
1173+
if (!massSpecified) {
1174+
DART_WARN(
1175+
"[SdfParser] Link [{}] is missing <mass>; using default mass of 1 "
1176+
"kg. "
1177+
"Specify <inertial><mass> to avoid unstable simulation.",
1178+
bodyName);
1179+
}
1180+
} else {
1181+
DART_WARN(
1182+
"[SdfParser] Link [{}] is missing <inertial>; using default "
1183+
"mass/inertia "
1184+
"(1 kg, unit inertia). Specify <inertial> to avoid unstable "
1185+
"simulation.",
1186+
bodyName);
11411187
}
11421188

11431189
SDFBodyNode sdfBodyNode;

tests/integration/io/test_SdfParser.cpp

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
#include "dart/common/Resource.hpp"
3636
#include "dart/common/ResourceRetriever.hpp"
3737
#include "dart/common/Uri.hpp"
38+
#include "dart/config.hpp"
3839
#include "dart/dynamics/FreeJoint.hpp"
3940
#include "dart/dynamics/MeshShape.hpp"
4041
#include "dart/dynamics/PlanarJoint.hpp"
@@ -52,9 +53,16 @@
5253
#include <filesystem>
5354
#include <iostream>
5455
#include <map>
56+
#include <sstream>
57+
#include <string>
5558

5659
#include <cstring>
5760

61+
#if DART_HAVE_spdlog
62+
#include <spdlog/sinks/ostream_sink.h>
63+
#include <spdlog/spdlog.h>
64+
#endif
65+
5866
using namespace dart;
5967
using namespace dart::dynamics;
6068
using namespace dart::test;
@@ -140,6 +148,65 @@ class MemoryResourceRetriever final : public common::ResourceRetriever
140148
std::map<std::string, std::string> mFiles;
141149
};
142150

151+
class LogCapture
152+
{
153+
public:
154+
LogCapture()
155+
{
156+
#if DART_HAVE_spdlog
157+
mStream = std::make_shared<std::ostringstream>();
158+
mSink = std::make_shared<spdlog::sinks::ostream_sink_mt>(*mStream);
159+
mPreviousLogger = spdlog::default_logger();
160+
mLogger = std::make_shared<spdlog::logger>("sdf-parser-log-capture", mSink);
161+
mLogger->set_level(spdlog::level::trace);
162+
mLogger->flush_on(spdlog::level::trace);
163+
spdlog::set_default_logger(mLogger);
164+
#else
165+
mOldCout = std::cout.rdbuf(mStream.rdbuf());
166+
mOldCerr = std::cerr.rdbuf(mStream.rdbuf());
167+
#endif
168+
}
169+
170+
~LogCapture()
171+
{
172+
#if DART_HAVE_spdlog
173+
if (mLogger)
174+
mLogger->flush();
175+
spdlog::set_default_logger(mPreviousLogger);
176+
if (mLogger)
177+
spdlog::drop(mLogger->name());
178+
#else
179+
std::cout.rdbuf(mOldCout);
180+
std::cerr.rdbuf(mOldCerr);
181+
#endif
182+
}
183+
184+
std::string contents()
185+
{
186+
#if DART_HAVE_spdlog
187+
if (mLogger)
188+
mLogger->flush();
189+
if (mStream)
190+
return mStream->str();
191+
return {};
192+
#else
193+
return mStream.str();
194+
#endif
195+
}
196+
197+
private:
198+
#if DART_HAVE_spdlog
199+
std::shared_ptr<std::ostringstream> mStream;
200+
std::shared_ptr<spdlog::sinks::ostream_sink_mt> mSink;
201+
std::shared_ptr<spdlog::logger> mPreviousLogger;
202+
std::shared_ptr<spdlog::logger> mLogger;
203+
#else
204+
std::ostringstream mStream;
205+
std::streambuf* mOldCout;
206+
std::streambuf* mOldCerr;
207+
#endif
208+
};
209+
143210
} // namespace
144211

145212
//==============================================================================
@@ -480,3 +547,91 @@ f 1 3 4
480547
});
481548
EXPECT_TRUE(foundMesh);
482549
}
550+
551+
//==============================================================================
552+
TEST(SdfParser, WarnsOnMissingInertialBlock)
553+
{
554+
auto retriever = std::make_shared<MemoryResourceRetriever>();
555+
const std::string modelUri = "memory://pkg/models/missing_inertial/model.sdf";
556+
557+
const std::string modelSdf = R"(
558+
<?xml version="1.0" ?>
559+
<sdf version="1.7">
560+
<model name="no_inertial">
561+
<link name="link_without_inertial">
562+
<visual name="v">
563+
<geometry>
564+
<box><size>0.1 0.1 0.1</size></box>
565+
</geometry>
566+
</visual>
567+
</link>
568+
</model>
569+
</sdf>
570+
)";
571+
572+
retriever->add(modelUri, modelSdf);
573+
574+
LogCapture capture;
575+
SdfParser::Options options;
576+
options.mResourceRetriever = retriever;
577+
auto skeleton = SdfParser::readSkeleton(modelUri, options);
578+
579+
ASSERT_TRUE(skeleton);
580+
ASSERT_EQ(skeleton->getNumBodyNodes(), 1u);
581+
const auto* body = skeleton->getBodyNode(0);
582+
EXPECT_DOUBLE_EQ(body->getMass(), 1.0);
583+
584+
const auto logs = capture.contents();
585+
EXPECT_NE(logs.find("missing <inertial>"), std::string::npos)
586+
<< "Expected warning about missing <inertial> block in logs: " << logs;
587+
}
588+
589+
//==============================================================================
590+
TEST(SdfParser, WarnsOnTinyMassAndDefaultsInertia)
591+
{
592+
auto retriever = std::make_shared<MemoryResourceRetriever>();
593+
const std::string modelUri = "memory://pkg/models/tiny_mass/model.sdf";
594+
const double tinyMass = 1e-14;
595+
const double clampedMass = 1e-9; // matches parser clamp
596+
597+
const std::string modelSdf = std::string(R"(
598+
<?xml version="1.0" ?>
599+
<sdf version="1.7">
600+
<model name="tiny_mass_model">
601+
<link name="link_with_mass_only">
602+
<inertial>
603+
<mass>)") + std::to_string(tinyMass)
604+
+ R"(</mass>
605+
</inertial>
606+
<collision name="c">
607+
<geometry>
608+
<box><size>0.1 0.1 0.1</size></box>
609+
</geometry>
610+
</collision>
611+
</link>
612+
</model>
613+
</sdf>
614+
)";
615+
616+
retriever->add(modelUri, modelSdf);
617+
618+
LogCapture capture;
619+
SdfParser::Options options;
620+
options.mResourceRetriever = retriever;
621+
auto skeleton = SdfParser::readSkeleton(modelUri, options);
622+
623+
ASSERT_TRUE(skeleton);
624+
ASSERT_EQ(skeleton->getNumBodyNodes(), 1u);
625+
const auto* body = skeleton->getBodyNode(0);
626+
const auto inertia = body->getInertia();
627+
EXPECT_DOUBLE_EQ(inertia.getMass(), clampedMass);
628+
const Eigen::Matrix3d expectedMoment
629+
= Eigen::Matrix3d::Identity() * clampedMass;
630+
EXPECT_TRUE(inertia.getMoment().isApprox(expectedMoment));
631+
632+
const auto logs = capture.contents();
633+
EXPECT_NE(logs.find("Clamping to"), std::string::npos)
634+
<< "Expected warning about tiny mass clamping in logs: " << logs;
635+
EXPECT_NE(logs.find("defines <mass> but no <inertia>"), std::string::npos)
636+
<< "Expected warning about missing inertia tensor in logs: " << logs;
637+
}

0 commit comments

Comments
 (0)