Files
Mujoco_WASM/test/engine/engine_plugin_test.cc
T
Yuval Tassa 5e99ca6cb8 Table-driven attribute writing: rebase the writer on the shared rows.
The writer consumes the same generated rows as the reader.
mjXWriter::WriteAttrTable drives the mechanical attributes of an
element from its mjXAttr rows: each bound field is compared against
the class default at the same offset -- the default object is the same
struct type, so the rows carry no comparison values -- and attributes
equal to their default are skipped. A null default object means the
element has no defaults, and every defined value is written.
Ranged-arity rows write with trailing-default trimming, which the
reader makes round-trip exact by refilling from the same default.
Call sites upcast to the private mjs base (the friend declarations
permit it; mjCMesh gains the friendship its siblings had); the
comparison object is def->X().spec, a freshly-defaulted struct for
the sections, or zero-initialized for size, whose spec defaults (-1,
auto) are resolved by compilation.

Converted: pair, geom, site, joint, camera, light, material, the
equality family, both tendon types (the fixed rows are the spatial
rows without appearance attributes -- exactly the tag difference), the
actuator, flex with its three sub-elements, mesh, skin, option, the
six visual sub-sections, statistic and size. The remnants keep names,
files, resolved reference strings (the mjC classes null their private
base's string pointers; resolved names live behind accessors), and the
writing=custom policies the schema declares: compile directives never
saved (fromto, springdamper, fitscale), type-dependent lengths and
attributes (sizes, joint pos/axis/limited, shellinertia), and
alternatives (mass/density, fovy-versus-intrinsics, the plugin-gated
gain/bias family). Compiler keeps its write-if-nonzero policy;
keyframe keeps its model-sized vectors.

Saved files are canonical: attributes follow schema declaration order
with remnants trailing, and sections follow the schema's dependency
order (statistic before visual, deformable before the contact and
equality sections that name flexes, tendon before the equality
constraints that name tendons, custom demoted to the data tail).
Uniform behavior fixes fall out: default-equal positionals are
dropped, dynprm is trimmed like every other ranged vector, and mesh
material -- read into the spec but never written -- now survives
save/load round trips. Changelog entries ride along.

Verified: full suite, doc_test, and the two-tier A/B harness --
saved XML reorders attributes, and every corpus model reloads to a
byte-identical binary.
PiperOrigin-RevId: 958255003
Change-Id: I5fe7346014450db88b2f3f8680a8f616f7d31266
2026-08-03 02:27:22 -07:00

717 lines
22 KiB
C++

// Copyright 2022 DeepMind Technologies Limited
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Tests for plugin-related functionalities.
// TODO(b/247110452) add more comments to this file, or add sample plugins
#include "src/engine/engine_plugin.h"
#include <array>
#include <cstdint>
#include <sstream>
#include <string>
#include <string_view>
#include <vector>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include <absl/strings/str_format.h>
#include <absl/strings/str_replace.h>
#include <mujoco/mujoco.h>
#include "test/fixture.h"
namespace mujoco {
namespace {
using ::testing::HasSubstr;
using ::testing::NotNull;
constexpr int kNumTruePlugins = 8;
constexpr int kNumFakePlugins = 30;
constexpr int kNumTestPlugins = 4;
class BaseTestPlugin {
public:
static constexpr int kDefaultStride = 1;
BaseTestPlugin(const mjModel* m, mjData* d, int instance)
: reset_count(d->plugin_state[m->plugin_stateadr[instance]]),
compute_count(d->plugin_state[m->plugin_stateadr[instance] + 1]),
advance_count(d->plugin_state[m->plugin_stateadr[instance] + 2]) {
{
const char* s = mj_getPluginConfig(m, instance, "stride");
if (*s) {
std::stringstream(s) >> stride;
} else {
stride = kDefaultStride;
}
}
reset_count = 0;
compute_count = 0;
advance_count = 0;
}
void Reset() {
reset_count += stride;
compute_count = 0;
advance_count = 0;
}
void Compute() { compute_count += stride; }
void Advance() { advance_count += stride; }
protected:
int stride;
mjtNum& reset_count;
mjtNum& compute_count;
mjtNum& advance_count;
};
class TestSensor : public BaseTestPlugin {
public:
TestSensor(const mjModel* m, mjData* d, int instance)
: BaseTestPlugin(m, d, instance) {
for (int i = 0; i < m->nsensor; ++i) {
if (m->sensor_type[i] == mjSENS_PLUGIN &&
m->sensor_plugin[i] == instance) {
sensors.push_back(
reinterpret_cast<mjtNum(*)[4]>(&d->sensordata[m->sensor_adr[i]]));
}
}
}
static int& InitCount() {
static int counter = 0;
return counter;
}
static int& DestroyCount() {
static int counter = 0;
return counter;
}
void Reset() {
BaseTestPlugin::Reset();
WriteSensorData();
}
void Compute() {
BaseTestPlugin::Compute();
WriteSensorData();
}
void Advance() {
BaseTestPlugin::Advance();
WriteSensorData();
}
private:
std::vector<mjtNum (*)[4]> sensors;
void WriteSensorData() {
for (auto* sensordata_ptr : sensors) {
auto& sensordata = *sensordata_ptr;
sensordata[0] = reset_count;
sensordata[1] = compute_count;
sensordata[2] = advance_count;
}
}
};
class TestActuator : public BaseTestPlugin {
public:
static constexpr mjtNum kDefaultMultiplier = 1.0;
static constexpr mjtNum kActDotValue = 13.0;
TestActuator(const mjModel* m, mjData* d, int instance)
: BaseTestPlugin(m, d, instance), instance_(instance) {
const char* s = mj_getPluginConfig(m, instance, "multiplier");
if (*s) {
std::stringstream(s) >> multiplier;
} else {
multiplier = kDefaultMultiplier;
}
for (int i = 0; i < m->nu; ++i) {
if (m->actuator_plugin[i] == instance) {
actuators.push_back(&d->actuator_force[i]);
}
}
}
static int& InitCount() {
static int counter = 0;
return counter;
}
static int& DestroyCount() {
static int counter = 0;
return counter;
}
void Reset() {
BaseTestPlugin::Reset();
WriteActuatorForce();
}
void Compute() {
BaseTestPlugin::Compute();
WriteActuatorForce();
}
void ActDot(const mjModel* m, mjData* d) {
for (int i = 0; i < m->nu; ++i) {
if (m->actuator_plugin[i] != instance_) {
continue;
}
// If the user specified an actdim, fill the act_dot values that belong
// to the plugin.
int actnum = m->actuator_actnum[i];
if (m->actuator_dyntype[i] != mjDYN_NONE) {
actnum--;
}
mju_fill(d->act_dot + m->actuator_actadr[i], kActDotValue, actnum);
}
}
private:
mjtNum multiplier;
std::vector<mjtNum*> actuators;
int instance_;
void WriteActuatorForce() {
for (mjtNum* actuator_force : actuators) {
*actuator_force = advance_count * multiplier;
}
}
};
class TestPassive {
public:
TestPassive(const mjModel* m, mjData* d, int instance) {}
void Reset() {}
void Compute() {}
void Advance() {}
};
int RegisterSensorPlugin() {
mjpPlugin plugin;
mjp_defaultPlugin(&plugin);
plugin.name = "mujoco.test.sensor";
const char* attributes[] = {"stride"};
plugin.nattribute = sizeof(attributes) / sizeof(*attributes);
plugin.attributes = attributes;
plugin.capabilityflags |= mjPLUGIN_SENSOR;
plugin.nstate = +[](const mjModel* m, int instance) { return 3; };
plugin.nsensordata =
+[](const mjModel* m, int instance, int sensor_id) { return 3; };
plugin.init = +[](const mjModel* m, mjData* d, int instance) {
auto* sensor = new TestSensor(m, d, instance);
d->plugin_data[instance] = reinterpret_cast<uintptr_t>(sensor);
TestSensor::InitCount()++;
return 0;
};
plugin.destroy = +[](mjData* d, int instance) {
delete reinterpret_cast<TestSensor*>(d->plugin_data[instance]);
d->plugin_data[instance] = 0;
TestSensor::DestroyCount()++;
};
plugin.reset = +[](const mjModel* m, mjtNum* plugin_state, void* plugin_data,
int instance) {
auto sensor = reinterpret_cast<TestSensor*>(plugin_data);
sensor->Reset();
};
plugin.compute = +[](const mjModel* m, mjData* d, int instance, int type) {
auto sensor = reinterpret_cast<TestSensor*>(d->plugin_data[instance]);
sensor->Compute();
};
plugin.advance = +[](const mjModel* m, mjData* d, int instance) {
auto sensor = reinterpret_cast<TestSensor*>(d->plugin_data[instance]);
sensor->Advance();
};
return mjp_registerPlugin(&plugin);
}
int RegisterActuatorPlugin() {
mjpPlugin plugin;
mjp_defaultPlugin(&plugin);
plugin.name = "mujoco.test.actuator";
const char* attributes[] = {"stride", "multiplier"};
plugin.nattribute = sizeof(attributes) / sizeof(*attributes);
plugin.attributes = attributes;
plugin.capabilityflags |= mjPLUGIN_ACTUATOR;
plugin.nstate = +[](const mjModel* m, int instance) { return 3; };
plugin.init = +[](const mjModel* m, mjData* d, int instance) {
auto* actuator = new TestActuator(m, d, instance);
d->plugin_data[instance] = reinterpret_cast<uintptr_t>(actuator);
TestActuator::InitCount()++;
return 0;
};
plugin.destroy = +[](mjData* d, int instance) {
delete reinterpret_cast<TestActuator*>(d->plugin_data[instance]);
d->plugin_data[instance] = 0;
TestActuator::DestroyCount()++;
};
plugin.reset = +[](const mjModel* m, mjtNum* plugin_state, void* plugin_data,
int instance) {
auto actuator = reinterpret_cast<TestActuator*>(plugin_data);
actuator->Reset();
};
plugin.compute = +[](const mjModel* m, mjData* d, int instance, int type) {
auto actuator = reinterpret_cast<TestActuator*>(d->plugin_data[instance]);
actuator->Compute();
};
plugin.advance = +[](const mjModel* m, mjData* d, int instance) {
auto actuator = reinterpret_cast<TestActuator*>(d->plugin_data[instance]);
actuator->Advance();
};
plugin.actuator_act_dot = +[](const mjModel* m, mjData* d, int instance) {
auto actuator = reinterpret_cast<TestActuator*>(d->plugin_data[instance]);
actuator->ActDot(m, d);
};
return mjp_registerPlugin(&plugin);
}
int RegisterPassivePlugin() {
mjpPlugin plugin;
mjp_defaultPlugin(&plugin);
plugin.name = "mujoco.test.passive";
const char* attributes[] = {"attribute"};
plugin.nattribute = sizeof(attributes) / sizeof(*attributes);
plugin.attributes = attributes;
plugin.capabilityflags |= mjPLUGIN_PASSIVE;
plugin.nstate = +[](const mjModel* m, int instance) { return 0; };
plugin.init = +[](const mjModel* m, mjData* d, int instance) {
auto* passive = new TestPassive(m, d, instance);
d->plugin_data[instance] = reinterpret_cast<uintptr_t>(passive);
return 0;
};
plugin.destroy = +[](mjData* d, int instance) {
delete reinterpret_cast<TestPassive*>(d->plugin_data[instance]);
d->plugin_data[instance] = 0;
};
plugin.reset = +[](const mjModel* m, mjtNum* plugin_state, void* plugin_data,
int instance) {
auto passive = reinterpret_cast<TestPassive*>(plugin_data);
passive->Reset();
};
plugin.compute = +[](const mjModel* m, mjData* d, int instance, int type) {
auto passive = reinterpret_cast<TestPassive*>(d->plugin_data[instance]);
passive->Compute();
};
return mjp_registerPlugin(&plugin);
}
int RegisterNoAttributePlugin() {
mjpPlugin plugin;
mjp_defaultPlugin(&plugin);
plugin.name = "mujoco.test.noattribute";
plugin.nattribute = 0;
plugin.attributes = nullptr;
plugin.capabilityflags |= mjPLUGIN_PASSIVE;
plugin.nstate = +[](const mjModel* m, int instance) { return 0; };
plugin.init = +[](const mjModel* m, mjData* d, int instance) {
auto* passive = new TestPassive(m, d, instance);
d->plugin_data[instance] = reinterpret_cast<uintptr_t>(passive);
return 0;
};
plugin.destroy = +[](mjData* d, int instance) {
delete reinterpret_cast<TestPassive*>(d->plugin_data[instance]);
d->plugin_data[instance] = 0;
};
plugin.reset = +[](const mjModel* m, mjtNum* plugin_state, void* plugin_data,
int instance) {
auto passive = reinterpret_cast<TestPassive*>(plugin_data);
passive->Reset();
};
plugin.compute = +[](const mjModel* m, mjData* d, int instance, int type) {
auto passive = reinterpret_cast<TestPassive*>(d->plugin_data[instance]);
passive->Compute();
};
return mjp_registerPlugin(&plugin);
}
class EnginePluginTest : public MujocoTest {
public:
// register all plugins
EnginePluginTest() : MujocoTest() {
RegisterSensorPlugin();
for (int i = 1; i <= kNumFakePlugins; ++i) {
mjpPlugin plugin;
mjp_defaultPlugin(&plugin);
std::string name = absl::StrFormat("mujoco.test.fake%u", i);
plugin.name = name.c_str();
mjp_registerPlugin(&plugin);
}
RegisterActuatorPlugin();
RegisterPassivePlugin();
RegisterNoAttributePlugin();
}
};
constexpr char xml[] = R"(
<mujoco>
<extension>
<plugin plugin="mujoco.test.sensor">
<instance name="twosensors"/>
<instance name="threesensors">
<config key="stride" value="3"/>
</instance>
</plugin>
<plugin plugin="mujoco.test.actuator">
<instance name="actuator2">
<config key="stride" value="2"/>
<config key="multiplier" value="0.125"/>
</instance>
</plugin>
<plugin plugin="mujoco.test.noattribute">
<instance name="noattribute"/>
</plugin>
<plugin plugin="mujoco.test.passive"/>
</extension>
<worldbody>
<body>
<plugin plugin="mujoco.test.passive">
<config key="attribute" value="0"/>
</plugin>
<geom type="capsule" size="0.1" fromto="-1 0 0 -1 0 -1"/>
<joint name="h1" type="hinge"/>
</body>
<body>
<geom type="capsule" size="0.1" fromto="1 0 0 1 0 -1"/>
<joint name="h2" type="hinge"/>
</body>
</worldbody>
<sensor>
<plugin instance="twosensors"/>
<plugin plugin="mujoco.test.sensor">
<config key="stride" value="5"/>
</plugin>
<plugin instance="threesensors"/>
<plugin instance="twosensors"/>
<plugin instance="threesensors"/>
<plugin instance="threesensors"/>
</sensor>
<actuator>
<plugin joint="h1" plugin="mujoco.test.actuator">
<config key="stride" value="4"/>
<config key="multiplier" value="0.03125"/>
</plugin>
<plugin joint="h2" actdim="3" instance="actuator2"/>
<plugin joint="h2" actdim="4" dyntype="filter" dynprm="0.9" instance="actuator2"/>
</actuator>
</mujoco>
)";
TEST_F(MujocoTest, EmptyPluginDisallowed) {
static constexpr char xml[] = R"(
<mujoco>
<worldbody>
<plugin/>
</worldbody>
</mujoco>
)";
std::array<char, 1024> error;
MjModelPtr m = LoadModelFromString(xml, error.data(), error.size());
EXPECT_THAT(m.get(), testing::IsNull()) << error.data();
EXPECT_THAT(
error.data(),
HasSubstr(
"neither 'plugin' nor 'instance' is specified for body 'world'"));
}
TEST_F(MujocoTest, FirstPartyPlugins) {
EXPECT_THAT(mjp_pluginCount(), kNumTruePlugins);
}
TEST_F(EnginePluginTest, NoAttributePlugin) {
static constexpr char xml[] = R"(
<mujoco>
<extension>
<plugin plugin="mujoco.test.noattribute">
<instance name="noattribute"/>
</plugin>
</extension>
</mujoco>
)";
std::array<char, 1024> error;
MjModelPtr m = LoadModelFromString(xml, error.data(), error.size());
EXPECT_THAT(m.get(), testing::NotNull()) << error.data();
}
TEST_F(EnginePluginTest, MultiplePluginTableBlocks) {
EXPECT_EQ(mjp_pluginCount(),
kNumTruePlugins + kNumFakePlugins + kNumTestPlugins);
const mjpPlugin* last_plugin = nullptr;
int table_count = 0;
for (int i = 1; i <= kNumFakePlugins; ++i) {
int slot;
std::string name = absl::StrFormat("mujoco.test.fake%u", i);
const mjpPlugin* plugin = mjp_getPlugin(name.c_str(), &slot);
EXPECT_EQ(slot, kNumTruePlugins + i);
EXPECT_THAT(plugin, NotNull());
if (plugin - last_plugin != 1) {
++table_count;
}
last_plugin = plugin;
}
// Make sure that there are enough fake plugins to fill multiple table blocks.
EXPECT_GT(table_count, 1);
// Make sure that a block contains multiple plugins.
EXPECT_LT(table_count, kNumFakePlugins);
}
TEST_F(EnginePluginTest, RegisterIdenticalPlugin) {
EXPECT_EQ(RegisterSensorPlugin(), kNumTruePlugins);
EXPECT_EQ(RegisterActuatorPlugin(), kNumTruePlugins + kNumFakePlugins + 1);
EXPECT_EQ(RegisterPassivePlugin(), kNumTruePlugins + kNumFakePlugins + 2);
EXPECT_EQ(mjp_pluginCount(),
kNumTruePlugins + kNumFakePlugins + kNumTestPlugins);
}
TEST_F(EnginePluginTest, SaveXml) {
char error[1024] = {0};
MjModelPtr m = LoadModelFromString(xml, error, sizeof(error));
ASSERT_THAT(m.get(), testing::NotNull()) << error;
std::string saved_xml = SaveAndReadXml(m.get());
std::string_view expected_xml(xml);
const std::string extension_open = "<extension>";
const std::string extension_close = "</extension>";
int extension_start = expected_xml.find(extension_open);
ASSERT_NE(extension_start, std::string::npos);
int extension_end =
expected_xml.find(extension_close) + extension_close.size();
ASSERT_NE(extension_end, std::string::npos);
ASSERT_LE(extension_start, extension_end);
auto expected_extension_section =
expected_xml.substr(extension_start, extension_end - extension_start);
const std::string sensor_open = "<sensor>";
const std::string sensor_close = "</sensor>";
int sensor_start = expected_xml.find(sensor_open);
ASSERT_NE(sensor_start, std::string::npos);
int sensor_end = expected_xml.find(sensor_close) + sensor_close.size();
ASSERT_NE(sensor_end, std::string::npos);
ASSERT_LE(sensor_start, sensor_end);
auto expected_sensor_section =
expected_xml.substr(sensor_start, sensor_end - sensor_start);
const std::string actuator_open = "<actuator>";
const std::string actuator_close = "</actuator>";
int actuator_start = expected_xml.find(actuator_open);
ASSERT_NE(actuator_start, std::string::npos);
int actuator_end = expected_xml.find(actuator_close) + actuator_close.size();
ASSERT_NE(actuator_end, std::string::npos);
ASSERT_LE(actuator_start, actuator_end);
// saved attributes follow schema order; dynprm round-trips as written
auto expected_actuator_section = absl::StrReplaceAll(
expected_xml.substr(actuator_start, actuator_end - actuator_start),
{{"actdim=\"4\" dyntype=\"filter\" dynprm=\"0.9\"",
"dyntype=\"filter\" dynprm=\"0.9\" actdim=\"4\""}});
EXPECT_THAT(saved_xml, HasSubstr(expected_extension_section));
EXPECT_THAT(saved_xml, HasSubstr(expected_sensor_section));
EXPECT_THAT(saved_xml, HasSubstr(expected_actuator_section));
// make sure that the saved XML can still be compiled
MjModelPtr m2 = LoadModelFromString(saved_xml, error, sizeof(error));
ASSERT_THAT(m.get(), testing::NotNull()) << error;
}
TEST_F(EnginePluginTest, SensorPlugin) {
int expected_init_count = TestSensor::InitCount();
int expected_destroy_count = TestSensor::DestroyCount();
EXPECT_EQ(expected_init_count, expected_destroy_count);
char error[1024] = {0};
MjModelPtr m = LoadModelFromString(xml, error, sizeof(error));
ASSERT_THAT(m.get(), testing::NotNull()) << error;
// mj_makeModel calls mj_makeData and mj_deleteData internally
expected_init_count += 3;
expected_destroy_count += 3;
EXPECT_EQ(TestSensor::InitCount(), expected_init_count);
EXPECT_EQ(TestSensor::DestroyCount(), expected_destroy_count);
EXPECT_EQ(m->nplugin, 7);
EXPECT_EQ(mj_name2id(m.get(), mjOBJ_PLUGIN, "twosensors"), 0);
EXPECT_EQ(mj_name2id(m.get(), mjOBJ_PLUGIN, "threesensors"), 1);
MjDataPtr d = MakeData(m);
expected_init_count += 3;
EXPECT_EQ(TestSensor::InitCount(), expected_init_count);
EXPECT_EQ(TestSensor::DestroyCount(), expected_destroy_count);
for (int i = 0; i < 5; ++i) {
for (int j = 0; j < 10; ++j) {
EXPECT_THAT(*reinterpret_cast<mjtNum(*)[3]>(d->plugin_state +
m->plugin_stateadr[0]),
testing::ElementsAreArray<int>({i + 1, 2 * j, j}));
EXPECT_THAT(*reinterpret_cast<mjtNum(*)[3]>(d->plugin_state +
m->plugin_stateadr[1]),
testing::ElementsAreArray<int>({3 * (i + 1), 6 * j, 3 * j}));
EXPECT_THAT(*reinterpret_cast<mjtNum(*)[3]>(d->plugin_state +
m->plugin_stateadr[5]),
testing::ElementsAreArray<int>({5 * (i + 1), 10 * j, 5 * j}));
EXPECT_THAT(*reinterpret_cast<mjtNum(*)[18]>(d->sensordata),
testing::ElementsAreArray<int>(
{i + 1, 2 * j, j, 5 * (i + 1), 10 * j, 5 * j, 3 * (i + 1),
6 * j, 3 * j, i + 1, 2 * j, j, 3 * (i + 1), 6 * j, 3 * j,
3 * (i + 1), 6 * j, 3 * j}));
mj_step(m.get(), d.get());
mj_forward(m.get(), d.get());
}
mj_resetData(m.get(), d.get());
}
d.reset();
m.reset();
expected_destroy_count += 3;
EXPECT_EQ(TestSensor::InitCount(), expected_init_count);
EXPECT_EQ(TestSensor::DestroyCount(), expected_destroy_count);
EXPECT_EQ(TestSensor::InitCount(), expected_init_count);
EXPECT_EQ(TestSensor::DestroyCount(), expected_destroy_count);
}
TEST_F(EnginePluginTest, ActuatorPlugin) {
int expected_init_count = TestActuator::InitCount();
int expected_destroy_count = TestActuator::DestroyCount();
EXPECT_EQ(expected_init_count, expected_destroy_count);
char error[1024] = {0};
MjModelPtr m = LoadModelFromString(xml, error, sizeof(error));
ASSERT_THAT(m.get(), testing::NotNull()) << error;
// mj_makeModel calls mj_makeData and mj_deleteData internally
expected_init_count += 2;
expected_destroy_count += 2;
EXPECT_EQ(TestActuator::InitCount(), expected_init_count);
EXPECT_EQ(TestActuator::DestroyCount(), expected_destroy_count);
EXPECT_EQ(m->nplugin, 7);
EXPECT_EQ(mj_name2id(m.get(), mjOBJ_PLUGIN, "actuator2"), 2);
MjDataPtr d = MakeData(m);
expected_init_count += 2;
EXPECT_EQ(TestActuator::InitCount(), expected_init_count);
EXPECT_EQ(TestActuator::DestroyCount(), expected_destroy_count);
for (int i = 0; i < 5; ++i) {
for (int j = 0; j < 10; ++j) {
EXPECT_THAT(*reinterpret_cast<mjtNum(*)[3]>(d->plugin_state +
m->plugin_stateadr[2]),
testing::ElementsAreArray<int>({2 * (i + 1), 4 * j, 2 * j}));
EXPECT_THAT(*reinterpret_cast<mjtNum(*)[3]>(d->plugin_state +
m->plugin_stateadr[3]),
testing::ElementsAreArray<int>({4 * (i + 1), 8 * j, 4 * j}));
EXPECT_THAT(*reinterpret_cast<mjtNum(*)[2]>(d->actuator_force),
testing::ElementsAreArray<mjtNum>(
{(mjtNum)0.125 * j, (mjtNum)0.25 * j}));
mj_step(m.get(), d.get());
mj_forward(m.get(), d.get());
}
mj_resetData(m.get(), d.get());
}
d.reset();
m.reset();
expected_destroy_count += 2;
EXPECT_EQ(TestActuator::InitCount(), expected_init_count);
EXPECT_EQ(TestActuator::DestroyCount(), expected_destroy_count);
EXPECT_EQ(TestActuator::InitCount(), expected_init_count);
EXPECT_EQ(TestActuator::DestroyCount(), expected_destroy_count);
}
TEST_F(EnginePluginTest, FilteredActuatorPlugin) {
char error[1024] = {0};
MjModelPtr m = LoadModelFromString(xml, error, sizeof(error));
ASSERT_THAT(m.get(), testing::NotNull()) << error;
// Expecting 7 actuator state variables: 3x2 from actuator2 instances, and 1
// from setting dyntype="filter" on one of the plugin actuators
ASSERT_EQ(m->na, 7);
EXPECT_EQ(m->actuator_actnum[0], 0);
EXPECT_EQ(m->actuator_actnum[1], 3);
EXPECT_EQ(m->actuator_actnum[2], 4);
EXPECT_EQ(m->actuator_actadr[0], -1);
EXPECT_EQ(m->actuator_actadr[1], 0);
EXPECT_EQ(m->actuator_actadr[2], 3);
MjDataPtr d = MakeData(m);
EXPECT_EQ(d->act[0], 0.0);
mju_fill(d->ctrl, 1, m->nu);
// start with nonzero act for the filter
d->act[6] = 0.5;
mj_step(m.get(), d.get());
for (int i = 0; i < 6; ++i) {
// act_dot should be computed by the plugin
mjtNum expected_act_dot = TestActuator::kActDotValue;
EXPECT_THAT(d->act_dot[i], MjNear(expected_act_dot, 1e-6, 1e-4));
// act_dot from the plugin should be Euler-integrated
mjtNum expected_act = expected_act_dot * m->opt.timestep;
EXPECT_THAT(d->act[i], MjNear(expected_act, 1e-6, 1e-4));
}
// actuator filter state should be updated outside the plugin for filter
// actuators.
mjtNum expected_act_dot = 0.5 / m->actuator_dynprm[mjNDYN * 2];
EXPECT_THAT(d->act_dot[6], MjNear(expected_act_dot, 1e-6, 1e-4));
EXPECT_THAT(d->act[6],
MjNear(0.5 + expected_act_dot * m->opt.timestep, 1e-6, 1e-4));
}
} // namespace
} // namespace mujoco