Repository: sail-sg/envpool (language: python)

Issue:
[Feature Request] Refactor mujoco_env.h with dm composer callback API
## Motivation

https://github.com/sail-sg/envpool/issues/59#issuecomment-1110289093

We are going to refactor the `MujocoEnv` class with deepmind composer API. It is more general with various kinds of robotics research.

## Resources

- https://arxiv.org/pdf/2006.12983.pdf page 16
- composer task: https://github.com/deepmind/dm_control/blob/main/dm_control/composer/task.py
- composer environment: https://github.com/deepmind/dm_control/blob/main/dm_control/composer/environment.py


Discussion / hints:

Fix patch:
diff --git a/docs/api/dm_control.rst b/docs/api/dm_control.rst
index 78d7c47c..dc056dc2 100644
--- a/docs/api/dm_control.rst
+++ b/docs/api/dm_control.rst
@@ -33,6 +33,26 @@ CheetahRun-v1
 - ``max_episode_steps``: 1000;
 
 
+FingerSpin-v1, FingerTurnEasy-v1, FingerTurnHard-v1
+---------------------------------------------------
+
+`dm_control suite finger source code
+<https://github.com/deepmind/dm_control/blob/main/dm_control/suite/finger.py>`_
+
+- Observation spec: a namedtuple with five keys: ``position (4)``,
+  ``velocity (3)``, ``touch (2)``, ``target_position (2)``,
+  ``dist_to_target ()``;
+- Action spec: ``(2)``, with range ``[-1, 1]``;
+- ``frame_skip``: 2;
+- ``max_episode_steps``: 1000;
+
+.. note ::
+
+    The observation keys ``target_position`` and ``dist_to_target`` are only
+    available in ``FingerTurnEasy-v1`` and ``FingerTurnHard-v1`` tasks. Their
+    values are meaningless in ``FingerSpin-v1``.
+
+
 HopperStand-v1, HopperHop-v1
 ----------------------------
 
diff --git a/envpool/mujoco/BUILD b/envpool/mujoco/BUILD
index 6d3fc718..9a50d854 100644
--- a/envpool/mujoco/BUILD
+++ b/envpool/mujoco/BUILD
@@ -64,6 +64,7 @@ cc_library(
     name = "mujoco_dmc_env",
     hdrs = [
         "dmc/cheetah.h",
+        "dmc/finger.h",
         "dmc/hopper.h",
         "dmc/mujoco_env.h",
         "dmc/reacher.h",
diff --git a/envpool/mujoco/__init__.py b/envpool/mujoco/__init__.py
index f0fcc877..896f13dd 100644
--- a/envpool/mujoco/__init__.py
+++ b/envpool/mujoco/__init__.py
@@ -19,6 +19,8 @@
 from .mujoco_dmc_envpool import (
   _DmcCheetahEnvPool,
   _DmcCheetahEnvSpec,
+  _DmcFingerEnvPool,
+  _DmcFingerEnvSpec,
   _DmcHopperEnvPool,
   _DmcHopperEnvSpec,
   _DmcReacherEnvPool,
@@ -57,6 +59,11 @@
 DmcCheetahEnvSpec, DmcCheetahDMEnvPool, DmcCheetahGymEnvPool = py_env(
   _DmcCheetahEnvSpec, _DmcCheetahEnvPool
 )
+
+DmcFingerEnvSpec, DmcFingerDMEnvPool, DmcFingerGymEnvPool = py_env(
+  _DmcFingerEnvSpec, _DmcFingerEnvPool
+)
+
 DmcHopperEnvSpec, DmcHopperDMEnvPool, DmcHopperGymEnvPool = py_env(
   _DmcHopperEnvSpec, _DmcHopperEnvPool
 )
diff --git a/envpool/mujoco/dmc/finger.h b/envpool/mujoco/dmc/finger.h
new file mode 100644
index 00000000..942b9204
--- /dev/null
+++ b/envpool/mujoco/dmc/finger.h
@@ -0,0 +1,250 @@
+/*
+ * Copyright 2022 Garena Online Private 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.
+ */
+// https://github.com/deepmind/dm_control/blob/1.0.2/dm_control/suite/finger.py
+
+#ifndef ENVPOOL_MUJOCO_DMC_FINGER_H_
+#define ENVPOOL_MUJOCO_DMC_FINGER_H_
+
+#include <algorithm>
+#include <cmath>
+#include <limits>
+#include <memory>
+#include <random>
+#include <string>
+
+#include "envpool/core/async_envpool.h"
+#include "envpool/core/env.h"
+#include "envpool/mujoco/dmc/mujoco_env.h"
+#include "envpool/mujoco/dmc/utils.h"
+
+namespace mujoco_dmc {
+
+std::string GetFingerXML(const std::string& base_path,
+                         const std::string& task_name_) {
+  return GetFileContent(base_path, "finger.xml");
+}
+
+class FingerEnvFns {
+ public:
+  static decltype(auto) DefaultConfig() {
+    return MakeDict("max_episode_steps"_.Bind(1000), "frame_skip"_.Bind(2),
+                    "task_name"_.Bind(std::string("spin")));
+  }
+  template <typename Config>
+  static decltype(auto) StateSpec(const Config& conf) {
+    return MakeDict("obs:position"_.Bind(Spec<mjtNum>({4})),
+                    "obs:velocity"_.Bind(Spec<mjtNum>({3})),
+                    "obs:touch"_.Bind(Spec<mjtNum>({2})),
+                    "obs:target_position"_.Bind(Spec<mjtNum>({2})),
+                    "obs:dist_to_target"_.Bind(Spec<mjtNum>({})),
+#ifdef ENVPOOL_TEST
+                    "info:qpos0"_.Bind(Spec<mjtNum>({3})),
+                    "info:target"_.Bind(Spec<mjtNum>({2})),
+#endif
+                    "discount"_.Bind(Spec<float>({-1}, {0.0, 1.0})));
+  }
+  template <typename Config>
+  static decltype(auto) ActionSpec(const Config& conf) {
+    return MakeDict("action"_.Bind(Spec<mjtNum>({-1, 2}, {-1.0, 1.0})));
+  }
+};
+
+using FingerEnvSpec = EnvSpec<FingerEnvFns>;
+
+class FingerEnv : public Env<FingerEnvSpec>, public MujocoEnv {
+ protected:
+  const mjtNum kEasyTargetSize = 0.07;
+  const mjtNum kHardTargetSize = 0.03;
+  const mjtNum kSpinVelocity = 15;
+  std::uniform_real_distribution<> dist_uniform_;
+  mjtNum target_radius_;
+  bool is_spin_;
+#ifdef ENVPOOL_TEST
+  std::array<mjtNum, 2> target_;
+#endif
+
+ public:
+  FingerEnv(const Spec& spec, int env_id)
+      : Env<FingerEnvSpec>(spec, env_id),
+        MujocoEnv(
+            spec.config["base_path"_],
+            GetFingerXML(spec.config["base_path"_], spec.config["task_name"_]),
+            spec.config["frame_skip"_], spec.config["max_episode_steps"_]),
+        dist_uniform_(-M_PI, M_PI),
+        is_spin_(spec.config["task_name"_] == "spin") {
+    const std::string& task_name = spec.config["task_name"_];
+    if (task_name == "turn_easy") {
+      target_radius_ = kEasyTargetSize;
+    } else if (task_name == "turn_hard") {
+      target_radius_ = kHardTargetSize;
+    } else if (task_name != "spin") {
+      throw std::runtime_error("Unknown task_name for dmc finger.");
+    }
+  }
+
+  void TaskInitializeEpisode() override {
+    if (is_spin_) {
+      model_->site_rgba[3 * 0 + 3] = 0;
+      model_->site_rgba[3 * 3 + 3] = 0;
+      model_->dof_damping[2] = 0.03;
+      SetRandomJointAngles();
+    } else {
+      mjtNum target_angle = dist_uniform_(gen_);
+      mjtNum hinge_x = data_->xanchor[2 * 3 + 0];
+      mjtNum hinge_z = data_->xanchor[2 * 3 + 2];
+      mjtNum radius = model_->geom_size[5 * 3 + 0] + model_->geom_size[5 * 1] +
+                      model_->geom_size[5 * 2];
+      mjtNum target_x = hinge_x + radius * std::sin(target_angle);
+      mjtNum target_z = hinge_z + radius * std::cos(target_angle);
+      model_->site_pos[0] = target_x;
+      model_->site_pos[2] = target_z;
+      model_->site_size[0] = target_radius_;
+#ifdef ENVPOOL_TEST
+      target_[0] = target_x;
+      target_[1] = target_z;
+#endif
+      SetRandomJointAngles();
+    }
+#ifdef ENVPOOL_TEST
+    std::memcpy(qpos0_.get(), data_->qpos, sizeof(mjtNum) * model_->nq);
+#endif
+  }
+
+  bool IsDone() override { return done_; }
+
+  void Reset() override {
+    ControlReset();
+    WriteState();
+  }
+
+  void Step(const Action& action) override {
+    mjtNum* act = static_cast<mjtNum*>(action["action"_].Data());
+    ControlStep(act);
+    WriteState();
+  }
+
+  float TaskGetReward() override {
+    if (is_spin_) {
+      return static_cast<float>(HingeVelocity() <= -kSpinVelocity);
+    }
+    return static_cast<float>(DistToTarget() <= 0);
+  }
+
+  bool TaskShouldTerminateEpisode() override { return false; }
+
+ private:
+  void WriteState() {
+    State state = Allocate();
+    state["reward"_] = reward_;
+    state["discount"_] = discount_;
+    // obs
+    const auto& bound_pos = BoundedPosition();
+    const auto& velocity = Velocity();
+    const auto& touch = Touch();
+    state["obs:position"_].Assign(bound_pos.begin(), bound_pos.size());
+    state["obs:velocity"_].Assign(velocity.begin(), velocity.size());
+    state["obs:touch"_].Assign(touch.begin(), touch.size());
+    if (!is_spin_) {
+      const auto& target_position = TargetPosition();
+      state["obs:target_position"_].Assign(target_position.begin(),
+                                           target_position.size());
+      state["obs:dist_to_target"_] = DistToTarget();
+    }
+    // info
+#ifdef ENVPOOL_TEST
+    state["info:qpos0"_].Assign(qpos0_.get(), model_->nq);
+    if (!is_spin_) {
+      state["info:target"_].Assign(target_.begin(), target_.size());
+    }
+#endif
+  }
+
+  void SetRandomJointAngles(int max_attempts = 1000) {
+    int i = 0;
+    for (int i = 0; i < max_attempts; i++) {
+      RandomizeLimitedAndRotationalJoints(&gen_);
+      PhysicsAfterReset();
+      if (data_->ncon == 0) {
+        break;
+      }
+    }
+    if (i == max_attempts) {
+      throw std::runtime_error(
+          "Could not find a collision-free state after max_attempts attempts");
+    }
+  }
+
+  mjtNum HingeVelocity() {
+    // return self.named.data.sensordata['hinge_velocity']
+    return data_->sensordata[4];
+  }
+  std::array<mjtNum, 4> BoundedPosition() {
+    // return np.hstack((self.named.data.sensordata[['proximal', 'distal']],
+    //                   self.tip_position()))
+    const auto& tip_position = TipPosition();
+    return {data_->sensordata[0], data_->sensordata[1], tip_position[0],
+            tip_position[1]};
+  }
+
+  std::array<mjtNum, 2> TipPosition() {
+    // return (self.named.data.sensordata['tip'][[0, 2]] -
+    //         self.named.data.sensordata['spinner'][[0, 2]])
+    return {data_->sensordata[5] - data_->sensordata[11],
+            data_->sensordata[7] - data_->sensordata[13]};
+  }
+
+  std::array<mjtNum, 2> TargetPosition() {
+    // return (self.named.data.sensordata['target'][[0, 2]] -
+    //         self.named.data.sensordata['spinner'][[0, 2]])
+    return {data_->sensordata[8] - data_->sensordata[11],
+            data_->sensordata[10] - data_->sensordata[13]};
+  }
+
+  std::array<mjtNum, 2> Touch() {
+    // return np.log1p(self.named.data.sensordata[['touchtop', 'touchbottom']])
+    return {std::log1p(data_->sensordata[14]),
+            std::log1p(data_->sensordata[15])};
+  }
+
+  std::array<mjtNum, 3> Velocity() {
+    // return self.named.data.sensordata[['proximal_velocity',
+    // 'distal_velocity', 'hinge_velocity']]
+    return {data_->sensordata[2], data_->sensordata[3], data_->sensordata[4]};
+  }
+
+  std::array<mjtNum, 2> ToTarget() {
+    // return self.target_position() - self.tip_position()
+    const auto& target_position = TargetPosition();
+    const auto& tip_position = TipPosition();
+    return {target_position[0] - tip_position[0],
+            target_position[1] - tip_position[1]};
+  }
+
+  mjtNum DistToTarget() {
+    // return (np.linalg.norm(self.to_target()) -
+    //         self.named.model.site_size['target', 0])
+    const auto& to_target = ToTarget();
+    return std::sqrt(to_target[0] * to_target[0] +
+                     to_target[1] * to_target[1]) -
+           model_->site_size[0];
+  }
+};
+
+using FingerEnvPool = AsyncEnvPool<FingerEnv>;
+
+}  // namespace mujoco_dmc
+
+#endif  // ENVPOOL_MUJOCO_DMC_FINGER_H_
diff --git a/envpool/mujoco/dmc/hopper.h b/envpool/mujoco/dmc/hopper.h
index 7ba74830..39b580b3 100644
--- a/envpool/mujoco/dmc/hopper.h
+++ b/envpool/mujoco/dmc/hopper.h
@@ -136,8 +136,7 @@ class HopperEnv : public Env<HopperEnvSpec>, public MujocoEnv {
 
   std::array<mjtNum, 2> Touch() {
     // return np.log1p(self.named.data.sensordata[['touch_toe', 'touch_heel']])
-    return std::array<mjtNum, 2>{std::log1p(data_->sensordata[3]),
-                                 std::log1p(data_->sensordata[4])};
+    return {std::log1p(data_->sensordata[3]), std::log1p(data_->sensordata[4])};
   }
 
   void WriteState() {
diff --git a/envpool/mujoco/dmc/mujoco_envpool.cc b/envpool/mujoco/dmc/mujoco_envpool.cc
index 687526f4..c86fca67 100644
--- a/envpool/mujoco/dmc/mujoco_envpool.cc
+++ b/envpool/mujoco/dmc/mujoco_envpool.cc
@@ -14,6 +14,7 @@
 
 #include "envpool/core/py_envpool.h"
 #include "envpool/mujoco/dmc/cheetah.h"
+#include "envpool/mujoco/dmc/finger.h"
 #include "envpool/mujoco/dmc/hopper.h"
 #include "envpool/mujoco/dmc/reacher.h"
 #include "envpool/mujoco/dmc/walker.h"
@@ -21,6 +22,9 @@
 using DmcCheetahEnvSpec = PyEnvSpec<mujoco_dmc::CheetahEnvSpec>;
 using DmcCheetahEnvPool = PyEnvPool<mujoco_dmc::CheetahEnvPool>;
 
+using DmcFingerEnvSpec = PyEnvSpec<mujoco_dmc::FingerEnvSpec>;
+using DmcFingerEnvPool = PyEnvPool<mujoco_dmc::FingerEnvPool>;
+
 using DmcHopperEnvSpec = PyEnvSpec<mujoco_dmc::HopperEnvSpec>;
 using DmcHopperEnvPool = PyEnvPool<mujoco_dmc::HopperEnvPool>;
 
@@ -32,6 +36,7 @@ using DmcWalkerEnvPool = PyEnvPool<mujoco_dmc::WalkerEnvPool>;
 
 PYBIND11_MODULE(mujoco_dmc_envpool, m) {
   REGISTER(m, DmcCheetahEnvSpec, DmcCheetahEnvPool)
+  REGISTER(m, DmcFingerEnvSpec, DmcFingerEnvPool)
   REGISTER(m, DmcHopperEnvSpec, DmcHopperEnvPool)
   REGISTER(m, DmcReacherEnvSpec, DmcReacherEnvPool)
   REGISTER(m, DmcWalkerEnvSpec, DmcWalkerEnvPool)
diff --git a/envpool/mujoco/dmc/reacher.h b/envpool/mujoco/dmc/reacher.h
index 0f594887..61eab7a4 100644
--- a/envpool/mujoco/dmc/reacher.h
+++ b/envpool/mujoco/dmc/reacher.h
@@ -129,7 +129,7 @@ class ReacherEnv : public Env<ReacherEnvSpec>, public MujocoEnv {
     state["discount"_] = discount_;
     // obs
     state["obs:position"_].Assign(data_->qpos, model_->nq);
-    auto finger = FingerToTarget();
+    const auto& finger = FingerToTarget();
     state["obs:to_target"_].Assign(finger.begin(), finger.size());
     state["obs:velocity"_].Assign(data_->qvel, model_->nv);
     // info for check alignment
@@ -140,13 +140,11 @@ class ReacherEnv : public Env<ReacherEnvSpec>, public MujocoEnv {
   }
 
   std::array<mjtNum, 2> FingerToTarget() {
-    std::array<mjtNum, 2> finger;
-    finger[0] = data_->geom_xpos[6 * 3 + 0] - data_->geom_xpos[9 * 3 + 0];
-    finger[1] = data_->geom_xpos[6 * 3 + 1] - data_->geom_xpos[9 * 3 + 1];
-    return finger;
+    return {data_->geom_xpos[6 * 3 + 0] - data_->geom_xpos[9 * 3 + 0],
+            data_->geom_xpos[6 * 3 + 1] - data_->geom_xpos[9 * 3 + 1]};
   }
   mjtNum FingerToTargetDist() {
-    std::array<mjtNum, 2> finger = FingerToTarget();
+    const auto& finger = FingerToTarget();
     return std::sqrt(finger[0] * finger[0] + finger[1] * finger[1]);
   }
 };
diff --git a/envpool/mujoco/dmc/walker.h b/envpool/mujoco/dmc/walker.h
index fc646008..1f765de7 100644
--- a/envpool/mujoco/dmc/walker.h
+++ b/envpool/mujoco/dmc/walker.h
@@ -67,7 +67,7 @@ class WalkerEnv : public Env<WalkerEnvSpec>, public MujocoEnv {
   // Horizontal speeds(meters / second) above which move reward is 1.
   const mjtNum kWalkSpeed = 1;
   const mjtNum kRunSpeed = 8;
-  float move_speed_;
+  mjtNum move_speed_;
 
  public:
   WalkerEnv(const Spec& spec, int env_id)
@@ -133,7 +133,7 @@ class WalkerEnv : public Env<WalkerEnvSpec>, public MujocoEnv {
     state["reward"_] = reward_;
     state["discount"_] = discount_;
     // obs
-    auto orient = Orientations();
+    const auto& orient = Orientations();
     state["obs:orientations"_].Assign(orient.begin(), orient.size());
     state["obs:height"_] = TorsoHeight();
     state["obs:velocity"_].Assign(data_->qvel, model_->nv);
diff --git a/envpool/mujoco/registration.py b/envpool/mujoco/registration.py
index cd575bcd..1351f6e3 100644
--- a/envpool/mujoco/registration.py
+++ b/envpool/mujoco/registration.py
@@ -22,6 +22,9 @@
 # from suite.BENCHMARKING
 dmc_mujoco_envs = [
   ("cheetah", "run"),
+  ("finger", "spin"),
+  ("finger", "turn_easy"),
+  ("finger", "turn_hard"),
   ("hopper", "hop"),
   ("hopper", "stand"),
   ("reacher", "easy"),


Test patch:
diff --git a/envpool/make_test.py b/envpool/make_test.py
index fbb76d50..153521ff 100644
--- a/envpool/make_test.py
+++ b/envpool/make_test.py
@@ -121,6 +121,9 @@ def test_make_mujoco_dmc(self) -> None:
     self.check_step(
       [
         "CheetahRun-v1",
+        "FingerSpin-v1",
+        "FingerTurnEasy-v1",
+        "FingerTurnHard-v1",
         "HopperHop-v1",
         "HopperStand-v1",
         "ReacherEasy-v1",
diff --git a/envpool/mujoco/dmc/mujoco_dmc_align_test.py b/envpool/mujoco/dmc/mujoco_dmc_align_test.py
index 0a5ed667..dc97a940 100644
--- a/envpool/mujoco/dmc/mujoco_dmc_align_test.py
+++ b/envpool/mujoco/dmc/mujoco_dmc_align_test.py
@@ -24,6 +24,8 @@
 from envpool.mujoco import (
   DmcCheetahDMEnvPool,
   DmcCheetahEnvSpec,
+  DmcFingerDMEnvPool,
+  DmcFingerEnvSpec,
   DmcHopperDMEnvPool,
   DmcHopperEnvSpec,
   DmcReacherDMEnvPool,
@@ -49,19 +51,22 @@ def run_space_check(self, env0: dm_env.Environment, env1: Any) -> None:
 
   @no_type_check
   def reset_state(
-    self, env: dm_env.Environment, ts: dm_env.TimeStep, task: str
+    self, env: dm_env.Environment, ts: dm_env.TimeStep, domain: str, task: str
   ) -> None:
     # manually reset, mimic initialize_episode
     with env.physics.reset_context():
       env.physics.data.qpos = ts.observation.qpos0[0]
-      if task == "cheetah":
+      if domain == "cheetah":
         for _ in range(200):
           env.physics.step()
         env.physics.data.time = 0
-      elif task == "reacher":
+      elif domain == "reacher":
         target = ts.observation.target[0]
         env.physics.named.model.geom_pos["target", "x"] = target[0]
         env.physics.named.model.geom_pos["target", "y"] = target[1]
+      elif domain == "finger" and task in ["turn_easy", "turn_hard"]:
+        obs = ts.observation
+        env.physics.named.model.site_pos["target", ["x", "z"]] = obs.target[0]
 
   def sample_action(self, action_spec: dm_env.specs.Array) -> np.ndarray:
     return np.random.uniform(
@@ -71,17 +76,17 @@ def sample_action(self, action_spec: dm_env.specs.Array) -> np.ndarray:
     )
 
   def run_align_check(
-    self, env0: dm_env.Environment, env1: Any, task: str
+    self, env0: dm_env.Environment, env1: Any, domain: str, task: str
   ) -> None:
-    logging.info(f"align check for {env1.__class__.__name__}")
+    logging.info(f"align check for {domain} {task}")
     obs_spec, action_spec = env0.observation_spec(), env0.action_spec()
     for i in range(5):
       np.random.seed(i)
       env0.reset()
       a = self.sample_action(action_spec)
       ts = env1.reset(np.array([0]))
-      self.reset_state(env0, ts, task)
-      logging.info(f'reset qpos {ts.observation.qpos0[0]}')
+      self.reset_state(env0, ts, domain, task)
+      logging.info(f"reset qpos {ts.observation.qpos0[0]}")
       cnt = 0
       done = False
       while not done:
@@ -105,13 +110,19 @@ def run_align_check_entry(
       env0 = suite.load(domain, task)
       env1 = envpool_cls(spec_cls(spec_cls.gen_config(task_name=task)))
       self.run_space_check(env0, env1)
-      self.run_align_check(env0, env1, domain)
+      self.run_align_check(env0, env1, domain, task)
 
   def test_cheetah(self) -> None:
     self.run_align_check_entry(
       "cheetah", ["run"], DmcCheetahEnvSpec, DmcCheetahDMEnvPool
     )
 
+  def test_finger(self) -> None:
+    self.run_align_check_entry(
+      "finger", ["spin", "turn_easy", "turn_hard"], DmcFingerEnvSpec,
+      DmcFingerDMEnvPool
+    )
+
   def test_hopper(self) -> None:
     self.run_align_check_entry(
       "hopper", ["hop", "stand"], DmcHopperEnvSpec, DmcHopperDMEnvPool
diff --git a/envpool/mujoco/dmc/mujoco_dmc_deterministic_test.py b/envpool/mujoco/dmc/mujoco_dmc_deterministic_test.py
index 8449d786..f9dece5b 100644
--- a/envpool/mujoco/dmc/mujoco_dmc_deterministic_test.py
+++ b/envpool/mujoco/dmc/mujoco_dmc_deterministic_test.py
@@ -22,6 +22,8 @@
 from envpool.mujoco import (
   DmcCheetahDMEnvPool,
   DmcCheetahEnvSpec,
+  DmcFingerDMEnvPool,
+  DmcFingerEnvSpec,
   DmcHopperDMEnvPool,
   DmcHopperEnvSpec,
   DmcReacherDMEnvPool,
@@ -76,6 +78,14 @@ def test_cheetah(self) -> None:
     for task in ["run"]:
       self.check(DmcCheetahEnvSpec, DmcCheetahDMEnvPool, task, obs_keys)
 
+  def test_finger(self) -> None:
+    obs_keys = ["position", "velocity", "touch"]
+    for task in ["spin"]:
+      self.check(DmcFingerEnvSpec, DmcFingerDMEnvPool, task, obs_keys)
+    obs_keys += ["target_position", "dist_to_target"]
+    for task in ["turn_easy", "turn_hard"]:
+      self.check(DmcFingerEnvSpec, DmcFingerDMEnvPool, task, obs_keys)
+
   def test_hopper(self) -> None:
     obs_keys = ["position", "velocity", "touch"]
     for task in ["stand", "hop"]: