Add contact sensor.
PiperOrigin-RevId: 783011982 Change-Id: Ica56fe9d520fa1d1ee7338e09148b1a55a049912
This commit is contained in:
committed by
Copybara-Service
parent
e441868dad
commit
d0e4771c8c
@@ -367,6 +367,16 @@ These are the possible sensor data types, used in ``mjData.sensor_datatype``.
|
||||
.. mujoco-include:: mjtDataType
|
||||
|
||||
|
||||
.. _mjtConDataField:
|
||||
|
||||
mjtConDataField
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
Types of data fields returned by contact sensors.
|
||||
|
||||
.. mujoco-include:: mjtConDataField
|
||||
|
||||
|
||||
.. _mjtSameFrame:
|
||||
|
||||
mjtSameFrame
|
||||
|
||||
+148
-3
@@ -7240,7 +7240,7 @@ See :ref:`collision-sensors` for more details about sensors of this type.
|
||||
|
||||
.. _sensor-distance-user:
|
||||
|
||||
:at:`name`, :at:`noise`, :at:`user`
|
||||
:at:`name`, :at:`noise`, :at:`user`:
|
||||
See :ref:`CSensor`.
|
||||
|
||||
|
||||
@@ -7287,7 +7287,7 @@ See :ref:`collision-sensors` for more details about sensors of this type.
|
||||
|
||||
.. _sensor-normal-user:
|
||||
|
||||
:at:`name`, :at:`noise`, :at:`user`
|
||||
:at:`name`, :at:`noise`, :at:`user`:
|
||||
See :ref:`CSensor`.
|
||||
|
||||
|
||||
@@ -7335,10 +7335,155 @@ See :ref:`collision-sensors` for more details about sensors of this type.
|
||||
|
||||
.. _sensor-fromto-user:
|
||||
|
||||
:at:`name`, :at:`noise`, :at:`user`
|
||||
:at:`name`, :at:`noise`, :at:`user`:
|
||||
See :ref:`CSensor`.
|
||||
|
||||
|
||||
.. _sensor-contact:
|
||||
|
||||
:el-prefix:`sensor/` |-| **contact** (*)
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
**Motivation:** The array of contacts which occur during the main dynamics pipeline is inherently variable-sized. The
|
||||
purpose of the contact sensor is to report contact-related information in a fixed-size array. This is useful as
|
||||
input to learning-based agents and in environment logic.
|
||||
|
||||
Unlike the purely geometric :ref:`collision-sensors` that act independently of the dynamics pipeline, the contact
|
||||
sensor reports information that was discovered during the collision and constraint steps, extracting data
|
||||
from ``mjData.{contact, efc_force}``, ignoring contacts that were filtered out by the :ref:`standard<coSelection>`
|
||||
mechanism and produce no force.
|
||||
|
||||
Contact sensor output involves three stages: **matching**, **reduction** and **extraction**.
|
||||
|
||||
Matching
|
||||
Selects a set of contacts from ``mjData.contact`` using criteria defined by :ref:`geom1<sensor-contact-geom1>`,
|
||||
:ref:`geom2<sensor-contact-geom2>`, :ref:`body1<sensor-contact-body1>`, :ref:`body2<sensor-contact-body2>`,
|
||||
:ref:`subtree1<sensor-contact-subtree1>`, :ref:`subtree2<sensor-contact-subtree2>` and
|
||||
:ref:`site<sensor-contact-site>`. Matching applies an intersection of criteria, for example setting
|
||||
:ref:`body1<sensor-contact-body1>` and :ref:`body2<sensor-contact-body2>` will match contacts that involve both
|
||||
bodies, while setting only :ref:`geom1<sensor-contact-geom1>` will match any contacts involving that geom. Setting
|
||||
:ref:`site<sensor-contact-site>` will match contacts that are inside the volume defined by the site; this matching
|
||||
criterion can be used with {geom2, body2, subtree2}. The subtree attributes take a body name and match all contacts
|
||||
involving the subtree where that body is located. Setting :ref:`subtree1<sensor-contact-subtree1>` and
|
||||
:ref:`subtree2<sensor-contact-subtree2>` to the same body or to two bodies in the same subtree will match
|
||||
self-collisions in the subtree. Specifying no matching criterion will match all contacts.
|
||||
|
||||
Reduction
|
||||
Reduces the number of matched contacts to exactly :ref:`num<sensor-contact-num>` sub-arrays, or "slots".
|
||||
If less than :at:`num` contacts match, the remaining slots are set to be identically zero. Note that the default,
|
||||
"unsorted" reduction criterion is potentitally non-deterministic. See :ref:`reduce<sensor-contact-reduce>` below.
|
||||
|
||||
Extraction
|
||||
Copies the set of fields specified by the user into each slot, see :ref:`data<sensor-contact-data>`.
|
||||
|
||||
.. _sensor-contact-geom1:
|
||||
.. _sensor-contact-geom2:
|
||||
|
||||
:at:`geom1`, :at:`geom2`: :at-val:`string, optional`
|
||||
Name of a geom participating in a contact. See **matching** :ref:`above <sensor-contact>`.
|
||||
|
||||
.. _sensor-contact-body1:
|
||||
.. _sensor-contact-body2:
|
||||
|
||||
:at:`body1`, :at:`body2`: :at-val:`string, optional`
|
||||
Name of a body participating in a contact. See **matching** :ref:`above <sensor-contact>`.
|
||||
|
||||
.. _sensor-contact-subtree1:
|
||||
.. _sensor-contact-subtree2:
|
||||
|
||||
:at:`subtree1`, :at:`subtree2`: :at-val:`string, optional`
|
||||
Name of a body whose subtree is participating in a contact. See **matching** :ref:`above <sensor-contact>`. Note
|
||||
currently only "entire" subtrees are supported, in the sense that the specified body must be a direct child of the
|
||||
world. General subtrees could be added in the future.
|
||||
|
||||
.. _sensor-contact-site:
|
||||
|
||||
:at:`site`: :at-val:`string, optional`
|
||||
Name of a site within whose volume the contact position must be found in order to match.
|
||||
See **matching** :ref:`above <sensor-contact>`.
|
||||
|
||||
.. _sensor-contact-num:
|
||||
|
||||
:at:`num`: :at-val:`int, "1"`
|
||||
Number of contacts to report. The sensor will always report :at:`num` sequential data arrays ("slots") per contact.
|
||||
The order in which contacts are reported depends on the :ref:`reduce<sensor-contact-reduce>` attribute.
|
||||
|
||||
.. _sensor-contact-data:
|
||||
|
||||
:at:`data`: :at-val:`[found, force, torque, dist, pos, normal, tangent], "found"`
|
||||
Specification of which data field(s) to report from the selected contacts.
|
||||
|
||||
- :at-val:`found` **real(1)**: This field serves two purposes. First, it indicates whether a contact was found in
|
||||
this slot, 0 means not found while a positive number means found. Second, the positive value equals the number of
|
||||
*matching* contacts. So if :at:`num = 3` contacts were requested but only 2 were matched, the :at-val:`found`
|
||||
fields will equal (2, 2, 0); if 6 were matched they will equal (6, 6, 6).
|
||||
- :at-val:`force` **real(3)**: The contact force, in the contact frame.
|
||||
- :at-val:`torque` **real(3)**: The contact torque, in the contact frame.
|
||||
- :at-val:`dist` **real(1)**: The penetration distance.
|
||||
- :at-val:`pos`: **real(3)**: The contact position, in the global frame.
|
||||
- :at-val:`normal`: **real(3)**: The contact normal direction, in the global frame.
|
||||
- :at-val:`tangent`: **real(3)**: The first tangent direction, in the global frame.
|
||||
In order to complete the full 3x3 contact frame, use tangent2 = cross(normal, tangent).
|
||||
|
||||
Importantly, the :at:`data` attribute can contain **multiple sequential data types**, as long as the relative
|
||||
order---as listed above---is maintained. For example, :at:`data` = :at-val:`"found force dist"` will return 5 numbers
|
||||
per contact (the concateneated values of [found, force, dist]), while :at:`data` = :at-val:`"force found dist"` is an
|
||||
error because :at-val:`found` must come before :at-val:`force`.
|
||||
|
||||
Missing contacts
|
||||
If less than :at:`num` contacts satisfy the matching criterion, the entire data slot is set to be identically
|
||||
zero. Because most data types can take 0 as a valid value, only the zero-ness of the :at-val:`normal` and
|
||||
:at-val:`tangent` unit vectors can be used to unambiguously detect an empty slot. For this reason, the
|
||||
:at-val:`found` data type is in place to allow for simple detection of missing contacts.
|
||||
|
||||
Size of sensordata block
|
||||
Unlike other sensors, the size of the corresponding sensordata block depends on the values of its attributes
|
||||
:ref:`num<sensor-contact-num>` and :ref:`data<sensor-contact-data>`. The total size of the output of a contact
|
||||
sensor is the product ``num x size(selected data fields)``. For example, requesting :at:`num = 6` contacts
|
||||
with :at:`data =` :at-val:`"force dist normal"` (3+1+3=7), will result in a sensordata block of 42 numbers (6
|
||||
consecutive slots x 7 numbers per slot).
|
||||
|
||||
Direction convention
|
||||
Because contacts create two equal-and-opposite forces between contacting bodies, there is freedom in the
|
||||
choice of which body impinges on which.
|
||||
|
||||
The sensor's convention is for "geom1/body1/subtree1" and "geom2/body2/subtree2" to determine the direction of
|
||||
the normal. The normal always points from the first to the second.
|
||||
|
||||
In the case that a direction cannot be determined, as when only a :at:`site` is used as the matching criterion, or
|
||||
when both subtrees are the same, the normal direction is the same as it is in ``mjData.contact``, where the normal
|
||||
points from the first to the second geom, and the two geoms are sorted according to their order in :ref:`mjtGeom`.
|
||||
|
||||
.. _sensor-contact-reduce:
|
||||
|
||||
:at:`reduce`: :at-val:`[none, mindist, maxforce, netforce], "none"`
|
||||
Reduction criterion to use. Also see **reduction** :ref:`above <sensor-contact>`.
|
||||
|
||||
- **none**: Returns the first :at:`num` contacts that satisfy the matching criterion, in the order that they appear
|
||||
in ``mjData.contact``. Note that while this is the fastest option, it is also potentially non-deterministic: future
|
||||
changes to collision detection code may cause the identity and order of matching contacts to change.
|
||||
- **mindist**: Returns :at:`num` contacts with the smallest penetration depth, ascending order.
|
||||
- **maxforce**: Returns :at:`num` contacts with the largest force norm, descending order.
|
||||
- **netforce**: This reduction criterion returns one new "synthetic" contact, located at the force-weighted centroid
|
||||
of all matched contacts. The frame of the contact is the global frame, so normal and tangent directions lose their
|
||||
natural semantic. The force and torque are computed such that a wrench applied at the computed position will have
|
||||
the same net effect as all the matching contacts combined. Note that this reduction criterion always returns
|
||||
exactly one contact.
|
||||
|
||||
.. _sensor-contact-cutoff:
|
||||
|
||||
:at:`cutoff`:
|
||||
This attribute is ignored.
|
||||
|
||||
.. _sensor-contact-name:
|
||||
|
||||
.. _sensor-contact-user:
|
||||
|
||||
.. _sensor-contact-noise:
|
||||
|
||||
:at:`name`, :at:`noise`, :at:`user`:
|
||||
See :ref:`CSensor`.
|
||||
|
||||
.. _sensor-e_potential:
|
||||
|
||||
:el-prefix:`sensor/` |-| **e_potential** (*)
|
||||
|
||||
@@ -1239,6 +1239,19 @@
|
||||
| | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ |
|
||||
+------------------------------------+----+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|
||||
| |_| sensor |br| |_| |L| | | .. table:: |
|
||||
| :ref:`contact | \* | :class: mjcf-attributes |
|
||||
| <sensor-contact>` | | |
|
||||
| | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ |
|
||||
| | | | :ref:`name<sensor-contact-name>` | :ref:`geom1<sensor-contact-geom1>` | :ref:`geom2<sensor-contact-geom2>` | :ref:`body1<sensor-contact-body1>` | |
|
||||
| | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ |
|
||||
| | | | :ref:`body2<sensor-contact-body2>` | :ref:`subtree1<sensor-contact-subtree1>` | :ref:`subtree2<sensor-contact-subtree2>` | :ref:`site<sensor-contact-site>` | |
|
||||
| | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ |
|
||||
| | | | :ref:`num<sensor-contact-num>` | :ref:`data<sensor-contact-data>` | :ref:`reduce<sensor-contact-reduce>` | :ref:`cutoff<sensor-contact-cutoff>` | |
|
||||
| | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ |
|
||||
| | | | :ref:`noise<sensor-contact-noise>` | :ref:`user<sensor-contact-user>` | | | |
|
||||
| | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ |
|
||||
+------------------------------------+----+------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
|
||||
| |_| sensor |br| |_| |L| | | .. table:: |
|
||||
| :ref:`e_potential | \* | :class: mjcf-attributes |
|
||||
| <sensor-e_potential>` | | |
|
||||
| | | +-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+-----------------------------------------------------------------+ |
|
||||
|
||||
@@ -9,6 +9,9 @@ General
|
||||
^^^^^^^
|
||||
- Added the :ref:`insidesite<sensor-insidesite>` sensor, for checking if an object is inside the volume of a site.
|
||||
It is useful for triggering events in surrounding environment logic.
|
||||
- Added the :ref:`contact<sensor-contact>` sensor, for reporting contact information according to user-defined criteria.
|
||||
The purpose of the :el:`contact` sensor is to report contact-related information in a fixed-size array. This is useful
|
||||
as input to learning-based agents and in environment logic.
|
||||
- Removed the SdfLib plugin and the dependency on `SdfLib <https://github.com/UPC-ViRVIG/SdfLib>`__. SDFs are now
|
||||
supported natively in mjModel.
|
||||
|
||||
|
||||
@@ -731,6 +731,9 @@ typedef enum mjtSensor_ { // type of sensor
|
||||
mjSENS_GEOMNORMAL, // normal direction between two geoms
|
||||
mjSENS_GEOMFROMTO, // segment between two geoms
|
||||
|
||||
// sensors for reporting contacts which occurred during the simulation
|
||||
mjSENS_CONTACT, // contacts which occurred during the simulation
|
||||
|
||||
// global sensors
|
||||
mjSENS_E_POTENTIAL, // potential energy
|
||||
mjSENS_E_KINETIC, // kinetic energy
|
||||
@@ -754,6 +757,17 @@ typedef enum mjtDataType_ { // data type for sensors
|
||||
mjDATATYPE_AXIS, // 3D unit vector
|
||||
mjDATATYPE_QUATERNION // unit quaternion
|
||||
} mjtDataType;
|
||||
typedef enum mjtConDataField_ { // data fields returned by contact sensors
|
||||
mjCONDATA_FOUND = 0, // whether a contact was found
|
||||
mjCONDATA_FORCE, // contact force
|
||||
mjCONDATA_TORQUE, // contact torque
|
||||
mjCONDATA_DIST, // contact penetration distance
|
||||
mjCONDATA_POS, // contact position
|
||||
mjCONDATA_NORMAL, // contact frame normal
|
||||
mjCONDATA_TANGENT, // contact frame first tangent
|
||||
|
||||
mjNCONDATA = 7 // number of contact sensor data fields
|
||||
} mjtConDataField;
|
||||
typedef enum mjtSameFrame_ { // frame alignment of bodies with their children
|
||||
mjSAMEFRAME_NONE = 0, // no alignment
|
||||
mjSAMEFRAME_BODY, // frame is same as body frame
|
||||
|
||||
@@ -369,6 +369,9 @@ typedef enum mjtSensor_ { // type of sensor
|
||||
mjSENS_GEOMNORMAL, // normal direction between two geoms
|
||||
mjSENS_GEOMFROMTO, // segment between two geoms
|
||||
|
||||
// sensors for reporting contacts which occurred during the simulation
|
||||
mjSENS_CONTACT, // contacts which occurred during the simulation
|
||||
|
||||
// global sensors
|
||||
mjSENS_E_POTENTIAL, // potential energy
|
||||
mjSENS_E_KINETIC, // kinetic energy
|
||||
@@ -398,6 +401,19 @@ typedef enum mjtDataType_ { // data type for sensors
|
||||
} mjtDataType;
|
||||
|
||||
|
||||
typedef enum mjtConDataField_ { // data fields returned by contact sensors
|
||||
mjCONDATA_FOUND = 0, // whether a contact was found
|
||||
mjCONDATA_FORCE, // contact force
|
||||
mjCONDATA_TORQUE, // contact torque
|
||||
mjCONDATA_DIST, // contact penetration distance
|
||||
mjCONDATA_POS, // contact position
|
||||
mjCONDATA_NORMAL, // contact frame normal
|
||||
mjCONDATA_TANGENT, // contact frame first tangent
|
||||
|
||||
mjNCONDATA = 7 // number of contact sensor data fields
|
||||
} mjtConDataField;
|
||||
|
||||
|
||||
typedef enum mjtSameFrame_ { // frame alignment of bodies with their children
|
||||
mjSAMEFRAME_NONE = 0, // no alignment
|
||||
mjSAMEFRAME_BODY, // frame is same as body frame
|
||||
|
||||
@@ -386,11 +386,12 @@ ENUMS: Mapping[str, EnumDecl] = dict([
|
||||
('mjSENS_GEOMDIST', 39),
|
||||
('mjSENS_GEOMNORMAL', 40),
|
||||
('mjSENS_GEOMFROMTO', 41),
|
||||
('mjSENS_E_POTENTIAL', 42),
|
||||
('mjSENS_E_KINETIC', 43),
|
||||
('mjSENS_CLOCK', 44),
|
||||
('mjSENS_PLUGIN', 45),
|
||||
('mjSENS_USER', 46),
|
||||
('mjSENS_CONTACT', 42),
|
||||
('mjSENS_E_POTENTIAL', 43),
|
||||
('mjSENS_E_KINETIC', 44),
|
||||
('mjSENS_CLOCK', 45),
|
||||
('mjSENS_PLUGIN', 46),
|
||||
('mjSENS_USER', 47),
|
||||
]),
|
||||
)),
|
||||
('mjtStage',
|
||||
@@ -415,6 +416,21 @@ ENUMS: Mapping[str, EnumDecl] = dict([
|
||||
('mjDATATYPE_QUATERNION', 3),
|
||||
]),
|
||||
)),
|
||||
('mjtConDataField',
|
||||
EnumDecl(
|
||||
name='mjtConDataField',
|
||||
declname='enum mjtConDataField_',
|
||||
values=dict([
|
||||
('mjCONDATA_FOUND', 0),
|
||||
('mjCONDATA_FORCE', 1),
|
||||
('mjCONDATA_TORQUE', 2),
|
||||
('mjCONDATA_DIST', 3),
|
||||
('mjCONDATA_POS', 4),
|
||||
('mjCONDATA_NORMAL', 5),
|
||||
('mjCONDATA_TANGENT', 6),
|
||||
('mjNCONDATA', 7),
|
||||
]),
|
||||
)),
|
||||
('mjtSameFrame',
|
||||
EnumDecl(
|
||||
name='mjtSameFrame',
|
||||
|
||||
@@ -2159,6 +2159,7 @@ static int sensorSize(mjtSensor sensor_type, int sensor_dim) {
|
||||
case mjSENS_FRAMEQUAT:
|
||||
return 4;
|
||||
|
||||
case mjSENS_CONTACT:
|
||||
case mjSENS_USER:
|
||||
return sensor_dim;
|
||||
|
||||
|
||||
+211
-1
@@ -26,6 +26,7 @@
|
||||
#include "engine/engine_io.h"
|
||||
#include "engine/engine_plugin.h"
|
||||
#include "engine/engine_ray.h"
|
||||
#include "engine/engine_sort.h"
|
||||
#include "engine/engine_support.h"
|
||||
#include "engine/engine_util_blas.h"
|
||||
#include "engine/engine_util_errmem.h"
|
||||
@@ -36,6 +37,26 @@
|
||||
|
||||
//-------------------------------- utility ---------------------------------------------------------
|
||||
|
||||
|
||||
typedef struct {
|
||||
mjtNum criterion; // criterion for partial sort
|
||||
int id; // index in d->contact
|
||||
int flip; // 0: don't flip the normal, 1: flip the normal
|
||||
} ContactInfo;
|
||||
|
||||
// define ContactSelect: find the k smallest elements of a ContactInfo array
|
||||
static int ContactInfoCompare(const ContactInfo* a, const ContactInfo* b, void* context) {
|
||||
if (a->criterion < b->criterion) return -1;
|
||||
if (a->criterion > b->criterion) return 1;
|
||||
|
||||
if (a->id < b->id) return -1;
|
||||
if (a->id > b->id) return 1;
|
||||
|
||||
return 0;
|
||||
}
|
||||
mjPARTIAL_SORT(ContactSelect, ContactInfo, ContactInfoCompare)
|
||||
|
||||
|
||||
// apply cutoff after each stage
|
||||
static void apply_cutoff(const mjModel* m, mjData* d, mjtStage stage) {
|
||||
// process sensors matching stage and having positive cutoff
|
||||
@@ -98,6 +119,8 @@ static void get_xpos_xmat(const mjData* d, mjtObj type, int id, int sensor_id,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
// get global quaternion of an object in mjData
|
||||
static void get_xquat(const mjModel* m, const mjData* d, mjtObj type, int id, int sensor_id,
|
||||
mjtNum *quat) {
|
||||
@@ -123,6 +146,7 @@ static void get_xquat(const mjModel* m, const mjData* d, mjtObj type, int id, in
|
||||
}
|
||||
|
||||
|
||||
|
||||
static void cam_project(mjtNum sensordata[2], const mjtNum target_xpos[3],
|
||||
const mjtNum cam_xpos[3], const mjtNum cam_xmat[9],
|
||||
const int cam_res[2], mjtNum cam_fovy,
|
||||
@@ -216,6 +240,117 @@ static void cam_project(mjtNum sensordata[2], const mjtNum target_xpos[3],
|
||||
|
||||
|
||||
|
||||
// check if a contact body/geom matches a sensor spec (type, id)
|
||||
static int checkMatch(const mjModel* m, int body, int geom, mjtObj type, int id) {
|
||||
if (type == mjOBJ_UNKNOWN) return 1;
|
||||
if (type == mjOBJ_SITE) return 1; // already passed site filter test
|
||||
if (type == mjOBJ_GEOM) return id == geom;
|
||||
if (type == mjOBJ_BODY) return id == body;
|
||||
if (type == mjOBJ_XBODY) return body >= 0 && m->body_rootid[id] == m->body_rootid[body];
|
||||
return 0;
|
||||
}
|
||||
|
||||
// 0: no match
|
||||
// 1: match, use contact normal
|
||||
// -1: match, flip contact normal
|
||||
static int matchContact(const mjModel* m, const mjData* d, int conid,
|
||||
mjtObj type1, int id1, mjtObj type2, int id2) {
|
||||
// no criterion: quick match
|
||||
if (type1 == mjOBJ_UNKNOWN && type2 == mjOBJ_UNKNOWN) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// site filter
|
||||
if (type1 == mjOBJ_SITE) {
|
||||
if (!mju_insideGeom(d->site_xpos + 3 * id1, d->site_xmat + 9 * id1,
|
||||
m->site_size + 3 * id1, m->site_type[id1], d->contact[conid].pos)) {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// get geom, body ids
|
||||
int geom1 = d->contact[conid].geom[0];
|
||||
int geom2 = d->contact[conid].geom[1];
|
||||
int body1 = geom1 >= 0 ? m->geom_bodyid[geom1] : -1;
|
||||
int body2 = geom2 >= 0 ? m->geom_bodyid[geom2] : -1;
|
||||
|
||||
// check match of sensor objects with contact objects
|
||||
int match11 = checkMatch(m, body1, geom1, type1, id1);
|
||||
int match12 = checkMatch(m, body2, geom2, type1, id1);
|
||||
int match21 = checkMatch(m, body1, geom1, type2, id2);
|
||||
int match22 = checkMatch(m, body2, geom2, type2, id2);
|
||||
|
||||
// if a sensor object is specified, it must be involved in the contact
|
||||
if (!match11 && !match12) return 0;
|
||||
if (!match21 && !match22) return 0;
|
||||
|
||||
// determine direction
|
||||
if (type1 != mjOBJ_UNKNOWN && type2 != mjOBJ_UNKNOWN) {
|
||||
// both obj1 and obj2 specified: direction depends on order
|
||||
int order_regular = match11 && match22;
|
||||
int order_reverse = match12 && match21;
|
||||
if (order_regular && !order_reverse) return 1;
|
||||
if (order_reverse && !order_regular) return -1;
|
||||
if (order_regular && order_reverse) return 1; // ambiguous, return 1
|
||||
} else if (type1 != mjOBJ_UNKNOWN) {
|
||||
// only obj1 specified: normal points away from obj1
|
||||
return match11 ? 1 : -1;
|
||||
} else if (type2 != mjOBJ_UNKNOWN) {
|
||||
// only obj2 specified: normal points towards obj2
|
||||
return match22 ? 1 : -1;
|
||||
}
|
||||
|
||||
// should not occur, all conditions are covered above
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// fill in output data for contact sensor for all fields
|
||||
// if flg_flip > 0, normal/tangent rotate 180 about frame[2]
|
||||
// force/torque flip-z s.t. force is equal-and-opposite in new contact frame
|
||||
static void copySensorData(const mjModel* m, const mjData* d,
|
||||
mjtNum* data[mjNCONDATA], int id, int flg_flip, int nfound) {
|
||||
// found flag
|
||||
if (data[mjCONDATA_FOUND]) *data[mjCONDATA_FOUND] = nfound;
|
||||
|
||||
// contact force and torque
|
||||
if (data[mjCONDATA_FORCE] || data[mjCONDATA_TORQUE]) {
|
||||
mjtNum forcetorque[6];
|
||||
mj_contactForce(m, d, id, forcetorque);
|
||||
if (data[mjCONDATA_FORCE]) {
|
||||
mju_copy3(data[mjCONDATA_FORCE], forcetorque);
|
||||
if (flg_flip) data[mjCONDATA_FORCE][2] *= -1;
|
||||
}
|
||||
if (data[mjCONDATA_TORQUE]) {
|
||||
mju_copy3(data[mjCONDATA_TORQUE], forcetorque+3);
|
||||
if (flg_flip) data[mjCONDATA_TORQUE][2] *= -1;
|
||||
}
|
||||
}
|
||||
|
||||
// contact penetration distance
|
||||
if (data[mjCONDATA_DIST]) {
|
||||
*data[mjCONDATA_DIST] = d->contact[id].dist;
|
||||
}
|
||||
|
||||
// contact position
|
||||
if (data[mjCONDATA_POS]) {
|
||||
mju_copy3(data[mjCONDATA_POS], d->contact[id].pos);
|
||||
}
|
||||
|
||||
// contact normal
|
||||
if (data[mjCONDATA_NORMAL]) {
|
||||
mju_copy3(data[mjCONDATA_NORMAL], d->contact[id].frame);
|
||||
if (flg_flip) mju_scl3(data[mjCONDATA_NORMAL], data[mjCONDATA_NORMAL], -1);
|
||||
}
|
||||
|
||||
// contact first tangent
|
||||
if (data[mjCONDATA_TANGENT]) {
|
||||
mju_copy3(data[mjCONDATA_TANGENT], d->contact[id].frame+3);
|
||||
if (flg_flip) mju_scl3(data[mjCONDATA_TANGENT], data[mjCONDATA_TANGENT], -1);
|
||||
}
|
||||
}
|
||||
|
||||
//-------------------------------- sensor ----------------------------------------------------------
|
||||
|
||||
// position-dependent sensors
|
||||
@@ -709,7 +844,7 @@ void mj_sensorAcc(const mjModel* m, mjData* d) {
|
||||
int rootid, bodyid, objtype, objid, adr, nusersensor = 0;
|
||||
int ne = d->ne, nf = d->nf, nefc = d->nefc, nu = m->nu;
|
||||
mjtNum tmp[6], conforce[6], conray[3], frc;
|
||||
mjContact* con;
|
||||
const mjContact* con;
|
||||
|
||||
// disabled sensors: return
|
||||
if (mjDISABLED(mjDSBL_SENSOR)) {
|
||||
@@ -792,6 +927,81 @@ void mj_sensorAcc(const mjModel* m, mjData* d) {
|
||||
}
|
||||
break;
|
||||
|
||||
case mjSENS_CONTACT: // contact
|
||||
{
|
||||
// prepare sizes and indices, check consistency
|
||||
int dataspec = m->sensor_intprm[i*mjNSENS];
|
||||
int size = mju_condataSize(dataspec); // size of each slot
|
||||
int dim = m->sensor_dim[i]; // total sensor array dimension
|
||||
int num = dim / size; // number of slots
|
||||
int reftype = m->sensor_reftype[i];
|
||||
int refid = m->sensor_refid[i];
|
||||
int reduce = m->sensor_intprm[i*mjNSENS+1];
|
||||
|
||||
// clear all outputs, prepare data pointers
|
||||
mjtNum* ptr = d->sensordata + adr;
|
||||
mju_zero(ptr, dim);
|
||||
mjtNum* data[mjNCONDATA] = {NULL};
|
||||
for (int j=0; j < mjNCONDATA; j++) {
|
||||
if (dataspec & (1 << j)) {
|
||||
data[j] = ptr;
|
||||
ptr += mjCONDATA_SIZE[j];
|
||||
}
|
||||
}
|
||||
|
||||
// prepare for matching loop
|
||||
int nmatch = 0;
|
||||
mj_markStack(d);
|
||||
ContactInfo *match = mjSTACKALLOC(d, d->ncon, ContactInfo);
|
||||
|
||||
// find matching contacts
|
||||
for (int j=0; j < d->ncon; j++) {
|
||||
// check match condition
|
||||
int match_j = matchContact(m, d, j, objtype, objid, reftype, refid);
|
||||
if (!match_j) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// save id and flip flag
|
||||
match[nmatch].id = j;
|
||||
match[nmatch].flip = match_j < 0;
|
||||
|
||||
// save sorting criterion, if required
|
||||
if (reduce) {
|
||||
if (reduce == 1) {
|
||||
match[nmatch].criterion = d->contact[j].dist;
|
||||
} else {
|
||||
mjtNum forcetorque[6];
|
||||
mj_contactForce(m, d, j, forcetorque);
|
||||
match[nmatch].criterion = -mju_dot3(forcetorque, forcetorque);
|
||||
}
|
||||
}
|
||||
|
||||
// increment number of matching contacts
|
||||
nmatch++;
|
||||
}
|
||||
|
||||
// number of slots to be filled
|
||||
int nslot = mjMIN(num, nmatch);
|
||||
|
||||
// partial sort to get bottom nslot contacts given reduction criterion
|
||||
if (reduce) {
|
||||
ContactInfo *heap = mjSTACKALLOC(d, nslot, ContactInfo);
|
||||
ContactSelect(match, heap, nmatch, nslot, NULL);
|
||||
}
|
||||
|
||||
// copy data into slots, increment pointers
|
||||
for (int j=0; j < nslot; j++) {
|
||||
copySensorData(m, d, data, match[j].id, match[j].flip, nmatch);
|
||||
for (int k=0; k < mjNCONDATA; k++) {
|
||||
if (data[k]) data[k] += size;
|
||||
}
|
||||
}
|
||||
|
||||
mj_freeStack(d);
|
||||
}
|
||||
break;
|
||||
|
||||
case mjSENS_ACCELEROMETER: // accelerometer
|
||||
// tmp = site acceleration, in site frame
|
||||
mj_objectAcceleration(m, d, mjOBJ_SITE, objid, tmp, 1);
|
||||
|
||||
@@ -97,6 +97,17 @@ const char* mjTIMERSTRING[mjNTIMER]= {
|
||||
};
|
||||
|
||||
|
||||
// size of contact data fields
|
||||
const int mjCONDATA_SIZE[mjNCONDATA] = {
|
||||
1, // mjCONDATA_FOUND
|
||||
3, // mjCONDATA_FORCE
|
||||
3, // mjCONDATA_TORQUE
|
||||
1, // mjCONDATA_DIST
|
||||
3, // mjCONDATA_POS
|
||||
3, // mjCONDATA_NORMAL
|
||||
3 // mjCONDATA_TANGENT
|
||||
};
|
||||
|
||||
|
||||
//-------------------------- get/set state ---------------------------------------------------------
|
||||
|
||||
@@ -1576,3 +1587,16 @@ const char* mj_versionString(void) {
|
||||
static const char versionstring[] = mjVERSIONSTRING;
|
||||
return versionstring;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// return total size of data in a contact sensor bitfield specification
|
||||
int mju_condataSize(int dataspec) {
|
||||
int size = 0;
|
||||
for (int i=0; i < mjNCONDATA; i++) {
|
||||
if (dataspec & (1 << i)) {
|
||||
size += mjCONDATA_SIZE[i];
|
||||
}
|
||||
}
|
||||
return size;
|
||||
}
|
||||
|
||||
@@ -29,6 +29,9 @@ MJAPI extern const char* mjDISABLESTRING[mjNDISABLE];
|
||||
MJAPI extern const char* mjENABLESTRING[mjNENABLE];
|
||||
MJAPI extern const char* mjTIMERSTRING[mjNTIMER];
|
||||
|
||||
// arrays
|
||||
MJAPI extern const int mjCONDATA_SIZE[mjNCONDATA]; // TODO(tassa): expose in public header?
|
||||
|
||||
|
||||
//-------------------------- get/set state ---------------------------------------------------------
|
||||
|
||||
@@ -199,6 +202,10 @@ MJAPI int mj_version(void);
|
||||
|
||||
// current version of MuJoCo as a null-terminated string
|
||||
MJAPI const char* mj_versionString(void);
|
||||
|
||||
// return total size of data fields in a contact sensor bitfield specification
|
||||
MJAPI int mju_condataSize(int dataSpec);
|
||||
|
||||
#ifdef __cplusplus
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -37,6 +37,7 @@
|
||||
#include "lodepng.h"
|
||||
#include "cc/array_safety.h"
|
||||
#include "engine/engine_passive.h"
|
||||
#include "engine/engine_support.h"
|
||||
#include <mujoco/mjspec.h>
|
||||
#include <mujoco/mujoco.h>
|
||||
#include "user/user_api.h"
|
||||
@@ -6686,6 +6687,7 @@ void mjCSensor::ResolveReferences(const mjCModel* m) {
|
||||
type != mjSENS_E_KINETIC &&
|
||||
type != mjSENS_CLOCK &&
|
||||
type != mjSENS_PLUGIN &&
|
||||
type != mjSENS_CONTACT &&
|
||||
type != mjSENS_USER) {
|
||||
throw mjCError(this, "invalid type in sensor");
|
||||
}
|
||||
@@ -7043,6 +7045,60 @@ void mjCSensor::Compile(void) {
|
||||
}
|
||||
break;
|
||||
|
||||
case mjSENS_CONTACT:
|
||||
// check first matching criterion
|
||||
if (objtype != mjOBJ_SITE &&
|
||||
objtype != mjOBJ_BODY &&
|
||||
objtype != mjOBJ_XBODY &&
|
||||
objtype != mjOBJ_GEOM &&
|
||||
objtype != mjOBJ_UNKNOWN) {
|
||||
throw mjCError(this, "first matching criterion: if set, must be (x)body, geom or site");
|
||||
}
|
||||
|
||||
// check that subtree1 is a full tree
|
||||
if (objtype == mjOBJ_XBODY && static_cast<mjCBody*>(obj)->GetParent()->id != 0) {
|
||||
throw mjCError(this, "subtree1 must be a child of the world");
|
||||
}
|
||||
|
||||
// check second matching criterion
|
||||
if (reftype != mjOBJ_BODY &&
|
||||
reftype != mjOBJ_XBODY &&
|
||||
reftype != mjOBJ_GEOM &&
|
||||
reftype != mjOBJ_UNKNOWN) {
|
||||
throw mjCError(this, "second matching criterion: if set, must be (x)body or geom");
|
||||
}
|
||||
|
||||
// check that subtree2 is a full tree
|
||||
if (reftype == mjOBJ_XBODY && static_cast<mjCBody*>(ref)->GetParent()->id != 0) {
|
||||
throw mjCError(this, "subtree2 must be a child of the world");
|
||||
}
|
||||
|
||||
// check for non-positive dim
|
||||
if (dim <= 0) {
|
||||
throw mjCError(this, "dim must be positive in sensor (got %d)", "", dim);
|
||||
}
|
||||
|
||||
// check for dim correctness
|
||||
if (dim % mju_condataSize(intprm[0]) != 0) {
|
||||
throw mjCError(this, "dim %d does not match data spec", "", dim);
|
||||
}
|
||||
|
||||
// check for reduce correctness
|
||||
if (intprm[1] < 0 || intprm[1] > 3) {
|
||||
throw mjCError(this, "unknown reduction criterion. got %d, "
|
||||
"expected one of {0, 1, 2, 3}", "", intprm[1]);
|
||||
}
|
||||
|
||||
// netforce not yet implemented
|
||||
if (intprm[1] == 3) {
|
||||
throw mjCError(this, "netforce reduction is not yet implemented\n"
|
||||
"please contact the developers if you need this feature");
|
||||
}
|
||||
|
||||
needstage = mjSTAGE_ACC;
|
||||
datatype = mjDATATYPE_REAL;
|
||||
break;
|
||||
|
||||
case mjSENS_E_POTENTIAL:
|
||||
case mjSENS_E_KINETIC:
|
||||
case mjSENS_CLOCK:
|
||||
@@ -7054,13 +7110,12 @@ void mjCSensor::Compile(void) {
|
||||
case mjSENS_USER:
|
||||
// check for negative dim
|
||||
if (dim < 0) {
|
||||
throw mjCError(this, "sensor dim must be positive in sensor");
|
||||
throw mjCError(this, "sensor dim must be non-negative in sensor");
|
||||
}
|
||||
|
||||
// make sure dim is consistent with datatype
|
||||
if (datatype == mjDATATYPE_AXIS && dim != 3) {
|
||||
throw mjCError(this,
|
||||
"datatype AXIS requires dim=3 in sensor");
|
||||
throw mjCError(this, "datatype AXIS requires dim=3 in sensor");
|
||||
}
|
||||
if (datatype == mjDATATYPE_QUATERNION && dim != 4) {
|
||||
throw mjCError(this, "datatype QUATERNION requires dim=4 in sensor");
|
||||
|
||||
@@ -43,6 +43,7 @@ extern const int gain_sz;
|
||||
extern const int bias_sz;
|
||||
extern const int stage_sz;
|
||||
extern const int datatype_sz;
|
||||
extern const int reduce_sz;
|
||||
extern const mjMap angle_map[];
|
||||
extern const mjMap enable_map[];
|
||||
extern const mjMap bool_map[];
|
||||
@@ -70,6 +71,8 @@ extern const mjMap gain_map[];
|
||||
extern const mjMap bias_map[];
|
||||
extern const mjMap stage_map[];
|
||||
extern const mjMap datatype_map[];
|
||||
extern const mjMap condata_map[];
|
||||
extern const mjMap reduce_map[];
|
||||
extern const mjMap meshtype_map[];
|
||||
extern const mjMap meshinertia_map[];
|
||||
extern const mjMap flexself_map[];
|
||||
|
||||
@@ -34,6 +34,7 @@
|
||||
#include <mujoco/mjtnum.h>
|
||||
#include <mujoco/mjvisualize.h>
|
||||
#include "engine/engine_plugin.h"
|
||||
#include "engine/engine_support.h"
|
||||
#include "engine/engine_util_errmem.h"
|
||||
#include "engine/engine_util_misc.h"
|
||||
#include <mujoco/mjspec.h>
|
||||
@@ -481,6 +482,8 @@ const char* MJCF[nMJCF][mjXATTRNUM] = {
|
||||
{"distance", "*", "8", "name", "geom1", "geom2", "body1", "body2", "cutoff", "noise", "user"},
|
||||
{"normal", "*", "8", "name", "geom1", "geom2", "body1", "body2", "cutoff", "noise", "user"},
|
||||
{"fromto", "*", "8", "name", "geom1", "geom2", "body1", "body2", "cutoff", "noise", "user"},
|
||||
{"contact", "*", "12", "name", "geom1", "geom2", "body1", "body2", "subtree1", "subtree2", "site",
|
||||
"num", "data", "reduce", "cutoff", "noise", "user"},
|
||||
{"e_potential", "*", "4", "name", "cutoff", "noise", "user"},
|
||||
{"e_kinetic", "*", "4", "name", "cutoff", "noise", "user"},
|
||||
{"clock", "*", "4", "name", "cutoff", "noise", "user"},
|
||||
@@ -606,6 +609,7 @@ const mjMap texrole_map[texrole_sz] = {
|
||||
{"orm", mjTEXROLE_ORM},
|
||||
};
|
||||
|
||||
|
||||
// integrator type
|
||||
const int integrator_sz = 4;
|
||||
const mjMap integrator_map[integrator_sz] = {
|
||||
@@ -615,6 +619,7 @@ const mjMap integrator_map[integrator_sz] = {
|
||||
{"implicitfast", mjINT_IMPLICITFAST}
|
||||
};
|
||||
|
||||
|
||||
// cone type
|
||||
const int cone_sz = 2;
|
||||
const mjMap cone_map[cone_sz] = {
|
||||
@@ -743,6 +748,28 @@ const mjMap datatype_map[datatype_sz] = {
|
||||
};
|
||||
|
||||
|
||||
// contact data type
|
||||
const mjMap condata_map[mjNCONDATA] = {
|
||||
{"found", mjCONDATA_FOUND},
|
||||
{"force", mjCONDATA_FORCE},
|
||||
{"torque", mjCONDATA_TORQUE},
|
||||
{"dist", mjCONDATA_DIST},
|
||||
{"pos", mjCONDATA_POS},
|
||||
{"normal", mjCONDATA_NORMAL},
|
||||
{"tangent", mjCONDATA_TANGENT}
|
||||
};
|
||||
|
||||
|
||||
// contact reduction type
|
||||
const int reduce_sz = 4;
|
||||
const mjMap reduce_map[reduce_sz] = {
|
||||
{"none", 0},
|
||||
{"mindist", 1},
|
||||
{"maxforce", 2},
|
||||
{"netforce", 3}
|
||||
};
|
||||
|
||||
|
||||
// LR mode
|
||||
const int lrmode_sz = 4;
|
||||
const mjMap lrmode_map[lrmode_sz] = {
|
||||
@@ -4132,6 +4159,74 @@ void mjXReader::Sensor(XMLElement* section) {
|
||||
}
|
||||
}
|
||||
|
||||
// sensor for contacts; attached to geoms or bodies or a site
|
||||
else if (type == "contact") {
|
||||
// first matching criterion
|
||||
bool has_site = ReadAttrTxt(elem, "site", objname);
|
||||
bool has_body1 = ReadAttrTxt(elem, "body1", objname);
|
||||
bool has_subtree1 = ReadAttrTxt(elem, "subtree1", objname);
|
||||
bool has_geom1 = ReadAttrTxt(elem, "geom1", objname);
|
||||
if (has_site + has_body1 + has_subtree1 + has_geom1 > 1) {
|
||||
throw mjXError(elem, "at most one of (geom1, body1, subtree1, site) can be specified");
|
||||
}
|
||||
if (has_site) { sensor->objtype = mjOBJ_SITE; }
|
||||
else if (has_body1) { sensor->objtype = mjOBJ_BODY; }
|
||||
else if (has_subtree1) { sensor->objtype = mjOBJ_XBODY; }
|
||||
else if (has_geom1) { sensor->objtype = mjOBJ_GEOM; }
|
||||
else { sensor->objtype = mjOBJ_UNKNOWN; }
|
||||
|
||||
// second matching criterion
|
||||
bool has_body2 = ReadAttrTxt(elem, "body2", refname);
|
||||
bool has_subtree2 = ReadAttrTxt(elem, "subtree2", refname);
|
||||
bool has_geom2 = ReadAttrTxt(elem, "geom2", refname);
|
||||
if (has_body2 + has_subtree2 + has_geom2 > 1) {
|
||||
throw mjXError(elem, "at most one of (geom2, body2, subtree2) can be specified");
|
||||
}
|
||||
if (has_body2) { sensor->reftype = mjOBJ_BODY; }
|
||||
else if (has_subtree2) { sensor->reftype = mjOBJ_XBODY; }
|
||||
else if (has_geom2) { sensor->reftype = mjOBJ_GEOM; }
|
||||
else { sensor->reftype = mjOBJ_UNKNOWN; }
|
||||
|
||||
// process data specification (intprm[0])
|
||||
int dataspec = 1 << mjCONDATA_FOUND;
|
||||
std::vector<int> condata(mjNCONDATA);
|
||||
int nkeys = MapValues(elem, "data", condata.data(), condata_map, mjNCONDATA);
|
||||
if (nkeys) {
|
||||
dataspec = 1 << condata[0];
|
||||
|
||||
// check ordering while adding bits to dataspec
|
||||
for (int i = 1; i < nkeys; ++i) {
|
||||
if (condata[i] <= condata[i-1]) {
|
||||
std::string correct_order;
|
||||
for (int j = 0; j < mjNCONDATA; ++j) {
|
||||
correct_order += condata_map[j].key;
|
||||
if (j < mjNCONDATA - 1) correct_order += ", ";
|
||||
}
|
||||
throw mjXError(elem, "data attributes must be in order: %s", correct_order.c_str());
|
||||
}
|
||||
dataspec |= 1 << condata[i];
|
||||
}
|
||||
}
|
||||
sensor->intprm[0] = dataspec;
|
||||
|
||||
// number of contacts, sensor dim
|
||||
sensor->dim = 1;
|
||||
ReadAttrInt(elem, "num", &sensor->dim);
|
||||
if (sensor->dim <= 0) {
|
||||
throw mjXError(elem, "'num' must be positive in sensor");
|
||||
}
|
||||
sensor->dim *= mju_condataSize(dataspec);
|
||||
|
||||
// reduction type (intprm[1])
|
||||
sensor->intprm[1] = 0;
|
||||
if (MapValue(elem, "reduce", &n, reduce_map, reduce_sz)) {
|
||||
sensor->intprm[1] = n;
|
||||
}
|
||||
|
||||
// sensor type
|
||||
sensor->type = mjSENS_CONTACT;
|
||||
}
|
||||
|
||||
// global sensors
|
||||
else if (type == "e_potential") {
|
||||
sensor->type = mjSENS_E_POTENTIAL;
|
||||
|
||||
@@ -101,7 +101,7 @@ class mjXReader : public mjXBase {
|
||||
};
|
||||
|
||||
// MJCF schema
|
||||
#define nMJCF 239
|
||||
#define nMJCF 240
|
||||
extern const char* MJCF[nMJCF][mjXATTRNUM];
|
||||
|
||||
#endif // MUJOCO_SRC_XML_XML_NATIVE_READER_H_
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#include <mujoco/mujoco.h>
|
||||
#include "engine/engine_io.h"
|
||||
#include "engine/engine_plugin.h"
|
||||
#include "engine/engine_support.h"
|
||||
#include "engine/engine_util_errmem.h"
|
||||
#include "engine/engine_util_misc.h"
|
||||
#include "user/user_model.h"
|
||||
@@ -76,7 +77,7 @@ static string WriteDoc(XMLDocument& doc, char *error, size_t error_sz) {
|
||||
|
||||
// top level sections
|
||||
std::array<string, 17> sections = {
|
||||
"<actuator", "<asset", "<compiler", "<contact", "<custom",
|
||||
"<actuator", "<asset", "<compiler", "<contact>", "<custom",
|
||||
"<default>", "<deformable", "<equality", "<extension", "<keyframe",
|
||||
"<option", "<sensor", "<size", "<statistic", "<tendon",
|
||||
"<visual", "<worldbody"};
|
||||
@@ -2205,7 +2206,38 @@ void mjXWriter::Sensor(XMLElement* root) {
|
||||
WriteAttrTxt(elem, sensor->objtype == mjOBJ_BODY ? "body1" : "geom1", sensor->get_objname());
|
||||
WriteAttrTxt(elem, sensor->reftype == mjOBJ_BODY ? "body2" : "geom2", sensor->get_refname());
|
||||
break;
|
||||
|
||||
case mjSENS_CONTACT:
|
||||
{
|
||||
elem = InsertEnd(section, "contact");
|
||||
if (sensor->objtype == mjOBJ_BODY) {
|
||||
WriteAttrTxt(elem, "body1", sensor->get_objname());
|
||||
} else if (sensor->objtype == mjOBJ_XBODY) {
|
||||
WriteAttrTxt(elem, "subtree1", sensor->get_objname());
|
||||
} else if (sensor->objtype == mjOBJ_GEOM) {
|
||||
WriteAttrTxt(elem, "geom1", sensor->get_objname());
|
||||
} else if (sensor->objtype == mjOBJ_SITE) {
|
||||
WriteAttrTxt(elem, "site", sensor->get_objname());
|
||||
}
|
||||
if (sensor->reftype == mjOBJ_BODY) {
|
||||
WriteAttrTxt(elem, "body2", sensor->get_refname());
|
||||
} else if (sensor->reftype == mjOBJ_XBODY) {
|
||||
WriteAttrTxt(elem, "subtree2", sensor->get_refname());
|
||||
} else if (sensor->reftype == mjOBJ_GEOM) {
|
||||
WriteAttrTxt(elem, "geom2", sensor->get_refname());
|
||||
}
|
||||
int dataspec = sensor->intprm[0];
|
||||
WriteAttrInt(elem, "num", sensor->dim / mju_condataSize(dataspec), 1);
|
||||
int data[mjNCONDATA];
|
||||
int ndata = 0;
|
||||
for (int i=0; i < mjNCONDATA; i++) {
|
||||
if (dataspec & (1 << i)) {
|
||||
data[ndata++] = i;
|
||||
}
|
||||
}
|
||||
WriteAttrKeys(elem, "data", condata_map, mjNCONDATA, data, ndata, 0);
|
||||
WriteAttrKey(elem, "reduce", reduce_map, reduce_sz, sensor->intprm[1], 0);
|
||||
}
|
||||
break;
|
||||
// global sensors
|
||||
case mjSENS_E_POTENTIAL:
|
||||
elem = InsertEnd(section, "potential");
|
||||
|
||||
+54
-1
@@ -792,7 +792,7 @@ XMLElement* mjXUtil::FindSubElem(XMLElement* elem, std::string name, bool requir
|
||||
|
||||
|
||||
|
||||
// find attribute, translate key, return int value
|
||||
// find attribute, translate key into data, return true if found
|
||||
bool mjXUtil::MapValue(XMLElement* elem, const char* attr, int* data,
|
||||
const mjMap* map, int mapSz, bool required) {
|
||||
// get attribute text
|
||||
@@ -814,6 +814,42 @@ bool mjXUtil::MapValue(XMLElement* elem, const char* attr, int* data,
|
||||
|
||||
|
||||
|
||||
// find attribute, translate unique space-separated keys to data, return number of keys found
|
||||
int mjXUtil::MapValues(XMLElement* elem, const char* attr, int* data,
|
||||
const mjMap* map, int mapSz, bool required) {
|
||||
// get attribute text
|
||||
auto maybe_text = ReadAttrStr(elem, attr, required);
|
||||
if (!maybe_text.has_value()) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::string text = maybe_text.value();
|
||||
std::istringstream strm(text);
|
||||
std::string key;
|
||||
std::set<std::string> found_keys;
|
||||
int count = 0;
|
||||
|
||||
while (strm >> key) {
|
||||
if (found_keys.count(key)) {
|
||||
throw mjXError(elem, "duplicate keyword: '%s'");
|
||||
return 0;
|
||||
}
|
||||
|
||||
int value = FindKey(map, mapSz, key);
|
||||
if (value == -1) {
|
||||
throw mjXError(elem, "invalid keyword: '%s'");
|
||||
return 0;
|
||||
}
|
||||
|
||||
found_keys.insert(key);
|
||||
data[count++] = value;
|
||||
}
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
|
||||
|
||||
//---------------------------------- write functions -----------------------------------------------
|
||||
|
||||
// check if double is int
|
||||
@@ -970,3 +1006,20 @@ void mjXUtil::WriteAttrKey(XMLElement* elem, std::string name,
|
||||
|
||||
WriteAttrTxt(elem, name, FindValue(map, mapsz, data));
|
||||
}
|
||||
|
||||
|
||||
// write attribute- space-separated keywords
|
||||
void mjXUtil::WriteAttrKeys(XMLElement* elem, std::string name, const mjMap* map,
|
||||
int mapsz, int* data, int ndata, int def) {
|
||||
// skip default
|
||||
if (ndata == 1 && data[0] == def) {
|
||||
return;
|
||||
}
|
||||
|
||||
std::string text = FindValue(map, mapsz, data[0]);
|
||||
for (int i = 1; i < ndata; ++i) {
|
||||
text += " " + FindValue(map, mapsz, data[i]);
|
||||
}
|
||||
|
||||
WriteAttrTxt(elem, name, text);
|
||||
}
|
||||
|
||||
@@ -183,6 +183,10 @@ class mjXUtil {
|
||||
static bool MapValue(tinyxml2::XMLElement* elem, const char* attr, int* data,
|
||||
const mjMap* map, int mapSz, bool required = false);
|
||||
|
||||
// find attribute, translate unique space-separated keys to data, return number of keys found
|
||||
static int MapValues(tinyxml2::XMLElement* elem, const char* attr, int* data,
|
||||
const mjMap* map, int mapSz, bool required = false);
|
||||
|
||||
// write attribute- any type
|
||||
template<typename T>
|
||||
static void WriteAttr(tinyxml2::XMLElement* elem, std::string name, int n, const T* data,
|
||||
@@ -204,6 +208,10 @@ class mjXUtil {
|
||||
static void WriteAttrKey(tinyxml2::XMLElement* elem, std::string name,
|
||||
const mjMap* map, int mapsz, int data, int def = -12345);
|
||||
|
||||
// write attribute- space-separated keywords
|
||||
static void WriteAttrKeys(XMLElement* elem, std::string name, const mjMap* map,
|
||||
int mapsz, int* data, int ndata, int def = -12345);
|
||||
|
||||
private:
|
||||
template<typename T>
|
||||
static bool ReadAttrValues(tinyxml2::XMLElement* elem, const char* attr,
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
|
||||
// Tests for engine/engine_sensor.c.
|
||||
|
||||
#include <cstddef>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
@@ -28,21 +30,39 @@
|
||||
namespace mujoco {
|
||||
namespace {
|
||||
|
||||
using ::std::string;
|
||||
using ::std::vector;
|
||||
|
||||
using ::testing::DoubleNear;
|
||||
using ::testing::ElementsAre;
|
||||
using ::testing::ElementsAreArray;
|
||||
using ::testing::HasSubstr;
|
||||
using ::testing::IsNull;
|
||||
using ::testing::Not;
|
||||
using ::testing::NotNull;
|
||||
using ::testing::Pointwise;
|
||||
using ::testing::SizeIs;
|
||||
using ::testing::StrEq;
|
||||
using ::testing::WhenSorted;
|
||||
|
||||
const mjtNum tol = 1e-14; // nearness tolerance for floating point numbers
|
||||
|
||||
// returns as a vector the measured values from sensor with index `id`
|
||||
static std::vector<mjtNum> GetSensor(const mjModel* model,
|
||||
const mjData* data,
|
||||
int id) {
|
||||
return std::vector<mjtNum>(
|
||||
static vector<mjtNum> GetSensor(const mjModel* model,
|
||||
const mjData* data, int id) {
|
||||
return vector<mjtNum>(
|
||||
data->sensordata + model->sensor_adr[id],
|
||||
data->sensordata + model->sensor_adr[id] + model->sensor_dim[id]);
|
||||
}
|
||||
|
||||
using ::testing::Pointwise;
|
||||
using ::testing::DoubleNear;
|
||||
using ::testing::NotNull;
|
||||
using ::testing::StrEq;
|
||||
// returns as a vector the measured values from sensor with name `name
|
||||
static vector<mjtNum> GetSensor(const mjModel* model,
|
||||
const mjData* data, const char* name) {
|
||||
int id = mj_name2id(model, mjOBJ_SENSOR, name);
|
||||
return vector<mjtNum>(
|
||||
data->sensordata + model->sensor_adr[id],
|
||||
data->sensordata + model->sensor_adr[id] + model->sensor_dim[id]);
|
||||
}
|
||||
|
||||
using SensorTest = MujocoTest;
|
||||
|
||||
@@ -112,13 +132,13 @@ TEST_F(RelativeFrameSensorTest, ReferencePosMat) {
|
||||
mj_forward(model, data);
|
||||
|
||||
// compare actual and expected values
|
||||
std::vector pos = GetSensor(model, data, 0);
|
||||
vector pos = GetSensor(model, data, 0);
|
||||
EXPECT_THAT(pos, Pointwise(DoubleNear(tol), {5, 5, 0}));
|
||||
|
||||
std::vector xaxis = GetSensor(model, data, 1);
|
||||
vector xaxis = GetSensor(model, data, 1);
|
||||
EXPECT_THAT(xaxis, Pointwise(DoubleNear(tol), {0, -1, 0}));
|
||||
|
||||
std::vector yaxis = GetSensor(model, data, 2);
|
||||
vector yaxis = GetSensor(model, data, 2);
|
||||
EXPECT_THAT(yaxis, Pointwise(DoubleNear(tol), {1, 0, 0}));
|
||||
|
||||
mj_deleteData(data);
|
||||
@@ -155,7 +175,7 @@ TEST_F(RelativeFrameSensorTest, ReferenceQuatMat) {
|
||||
mju_mat2Quat(converted_quat, mat);
|
||||
|
||||
// compare quaternion sensor and quat derived from orientation matrix
|
||||
std::vector quat = GetSensor(model, data, 3);
|
||||
vector quat = GetSensor(model, data, 3);
|
||||
EXPECT_THAT(quat, Pointwise(DoubleNear(tol), converted_quat));
|
||||
|
||||
mj_deleteData(data);
|
||||
@@ -199,7 +219,7 @@ TEST_F(RelativeFrameSensorTest, ReferencePosMatQuat) {
|
||||
|
||||
// call mj_forward, save global sensors (colocated with reference frame)
|
||||
mj_forward(model, data);
|
||||
std::vector expected_values(data->sensordata, data->sensordata+nsensordata/2);
|
||||
vector expected_values(data->sensordata, data->sensordata+nsensordata/2);
|
||||
|
||||
// set qpos to arbitrary values, call mj_forward
|
||||
for (int i=0; i < 7; i++) {
|
||||
@@ -208,7 +228,7 @@ TEST_F(RelativeFrameSensorTest, ReferencePosMatQuat) {
|
||||
mj_forward(model, data);
|
||||
|
||||
// get values from relative sensors after moving the object
|
||||
std::vector actual_values(data->sensordata+nsensordata/2,
|
||||
vector actual_values(data->sensordata+nsensordata/2,
|
||||
data->sensordata+nsensordata);
|
||||
|
||||
// object and reference have moved together, we expect values to not change
|
||||
@@ -245,7 +265,7 @@ TEST_F(RelativeFrameSensorTest, FrameVelLinearFixed) {
|
||||
mj_forward(model, data);
|
||||
|
||||
// compare to expected values
|
||||
std::vector linvel = GetSensor(model, data, 0);
|
||||
vector linvel = GetSensor(model, data, 0);
|
||||
const mjtNum expected_linvel[3] = {-mju_sqrt(0.5), mju_sqrt(0.5), 0};
|
||||
EXPECT_THAT(linvel, Pointwise(DoubleNear(tol), expected_linvel));
|
||||
|
||||
@@ -278,7 +298,7 @@ TEST_F(RelativeFrameSensorTest, FrameVelAngFixed) {
|
||||
mj_forward(model, data);
|
||||
|
||||
// obj and ref rotate together, relative angular velocities should be zero
|
||||
std::vector angvel = GetSensor(model, data, 0);
|
||||
vector angvel = GetSensor(model, data, 0);
|
||||
EXPECT_THAT(angvel, Pointwise(DoubleNear(tol), {0, 0, 0}));
|
||||
|
||||
mj_deleteData(data);
|
||||
@@ -314,7 +334,7 @@ TEST_F(RelativeFrameSensorTest, FrameVelAngOpposing) {
|
||||
mj_forward(model, data);
|
||||
|
||||
// obj and ref rotate on same axis, we can just difference the velocities
|
||||
std::vector angvel = GetSensor(model, data, 0);
|
||||
vector angvel = GetSensor(model, data, 0);
|
||||
const mjtNum expected_angvel[3] = {0, data->qvel[1]-data->qvel[0], 0};
|
||||
EXPECT_THAT(angvel, Pointwise(DoubleNear(tol), expected_angvel));
|
||||
|
||||
@@ -358,8 +378,8 @@ TEST_F(RelativeFrameSensorTest, FrameVelGeneral) {
|
||||
mj_forward(model, data);
|
||||
|
||||
// save measured linear and angular velocities as vectors
|
||||
std::vector linvel = GetSensor(model, data, 2);
|
||||
std::vector angvel = GetSensor(model, data, 3);
|
||||
vector linvel = GetSensor(model, data, 2);
|
||||
vector angvel = GetSensor(model, data, 3);
|
||||
|
||||
// save current position, quaternion as arrays
|
||||
mjtNum pos0[3], quat0[4];
|
||||
@@ -615,20 +635,20 @@ TEST_F(SensorTest, CollisionSequential) {
|
||||
mjtNum eps = 1e-14;
|
||||
|
||||
EXPECT_THAT(GetSensor(model, data, 3),
|
||||
Pointwise(DoubleNear(eps), std::vector<mjtNum>{0, 0, 1}));
|
||||
Pointwise(DoubleNear(eps), vector<mjtNum>{0, 0, 1}));
|
||||
EXPECT_THAT(GetSensor(model, data, 4),
|
||||
Pointwise(DoubleNear(eps), std::vector<mjtNum>{0, 0, -1}));
|
||||
Pointwise(DoubleNear(eps), vector<mjtNum>{0, 0, -1}));
|
||||
EXPECT_THAT(GetSensor(model, data, 5),
|
||||
Pointwise(DoubleNear(eps), std::vector<mjtNum>{1, 0, 0}));
|
||||
Pointwise(DoubleNear(eps), vector<mjtNum>{1, 0, 0}));
|
||||
EXPECT_THAT(GetSensor(model, data, 6),
|
||||
Pointwise(DoubleNear(eps),
|
||||
std::vector<mjtNum>{0, 0, 0, 0, 0, .8}));
|
||||
vector<mjtNum>{0, 0, 0, 0, 0, .8}));
|
||||
EXPECT_THAT(GetSensor(model, data, 7),
|
||||
Pointwise(DoubleNear(eps),
|
||||
std::vector<mjtNum>{1, 0, .7, 1, 0, 0}));
|
||||
vector<mjtNum>{1, 0, .7, 1, 0, 0}));
|
||||
EXPECT_THAT(GetSensor(model, data, 8),
|
||||
Pointwise(DoubleNear(eps),
|
||||
std::vector<mjtNum>{.2, 0, 1, .7, 0, 1}));
|
||||
vector<mjtNum>{.2, 0, 1, .7, 0, 1}));
|
||||
|
||||
EXPECT_THAT(GetSensor(model, data, 9),
|
||||
Pointwise(DoubleNear(eps), GetSensor(model, data, 0)));
|
||||
@@ -647,6 +667,202 @@ TEST_F(SensorTest, CollisionSequential) {
|
||||
mj_deleteModel(model);
|
||||
}
|
||||
|
||||
TEST_F(SensorTest, BadContact) {
|
||||
string xml_template = R"(
|
||||
<mujoco>
|
||||
<worldbody>
|
||||
<geom name="sphere1" pos="0 0 1" size="0.2"/>
|
||||
<body name="body">
|
||||
<freejoint/>
|
||||
<geom name="sphere2" pos="1 0 1" size="0.3"/>
|
||||
<site name="site" pos="1 0 1" size="0.3"/>
|
||||
<body name="non_root">
|
||||
<geom name="sphere3" pos="1 0 1" size="0.3"/>
|
||||
</body>
|
||||
</body>
|
||||
</worldbody>
|
||||
<sensor>
|
||||
<contact BAD_ATTR/>
|
||||
</sensor>
|
||||
</mujoco>
|
||||
)";
|
||||
|
||||
struct Case {
|
||||
string bad_attr;
|
||||
string expected_error;
|
||||
};
|
||||
|
||||
Case test_cases[] = {
|
||||
{"geom1='sphere1' geom2='sphere2' data='dist force normal'",
|
||||
"must be in order: found, force, torque, dist, pos, normal, tangent"},
|
||||
{"geom1='sphere1' geom2='sphere2' num='-3'",
|
||||
"'num' must be positive in sensor"},
|
||||
{"geom1='sphere1' geom2='sphere2' site='site'",
|
||||
"at most one of (geom1, body1, subtree1, site) can be specified"},
|
||||
{"geom2='sphere1' body2='body'",
|
||||
"at most one of (geom2, body2, subtree2) can be specified"},
|
||||
{"subtree1='non_root'",
|
||||
"must be a child of the world"}
|
||||
};
|
||||
|
||||
for (const auto& test : test_cases) {
|
||||
string xml = xml_template;
|
||||
size_t pos = xml.find("BAD_ATTR");
|
||||
ASSERT_NE(pos, string::npos);
|
||||
xml.replace(pos, 8, test.bad_attr);
|
||||
|
||||
char error[1024];
|
||||
mjModel* model = LoadModelFromString(xml.c_str(), error, sizeof(error));
|
||||
ASSERT_THAT(model, IsNull()) << "Test case: " << test.bad_attr;
|
||||
EXPECT_THAT(error, HasSubstr(test.expected_error))
|
||||
<< "Test case: " << test.bad_attr;
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(SensorTest, Contact) {
|
||||
const string xml_path =
|
||||
GetTestDataFilePath("engine/testdata/sensor/contact.xml");
|
||||
char error[1024];
|
||||
mjModel* model = mj_loadXML(xml_path.c_str(), nullptr, error, sizeof(error));
|
||||
ASSERT_THAT(model, NotNull()) << error;
|
||||
|
||||
mjData* data = mj_makeData(model);
|
||||
|
||||
for (mjtCone cone : {mjCONE_PYRAMIDAL, mjCONE_ELLIPTIC}) {
|
||||
model->opt.cone = cone;
|
||||
|
||||
mj_resetData(model, data);
|
||||
while (data->time < 2) {
|
||||
mj_step(model, data);
|
||||
}
|
||||
|
||||
vector all = GetSensor(model, data, "all");
|
||||
EXPECT_EQ(all, vector<mjtNum>{4});
|
||||
|
||||
vector world = GetSensor(model, data, "world");
|
||||
EXPECT_EQ(world, vector<mjtNum>{3});
|
||||
|
||||
vector b1 = GetSensor(model, data, "b1");
|
||||
EXPECT_EQ(b1, vector<mjtNum>{3});
|
||||
|
||||
vector g1 = GetSensor(model, data, "g1");
|
||||
EXPECT_EQ(g1, vector<mjtNum>{3});
|
||||
|
||||
vector b1g2 = GetSensor(model, data, "b1:g2");
|
||||
EXPECT_EQ(b1g2, vector<mjtNum>{1});
|
||||
|
||||
vector b1world = GetSensor(model, data, "b1:world");
|
||||
EXPECT_EQ(b1world, vector<mjtNum>{2});
|
||||
|
||||
vector site = GetSensor(model, data, "site");
|
||||
EXPECT_EQ(site, vector<mjtNum>{2});
|
||||
|
||||
vector sitewall = GetSensor(model, data, "site:wall");
|
||||
EXPECT_EQ(sitewall, vector<mjtNum>{1});
|
||||
|
||||
mjtNum tol = 1e-4;
|
||||
vector wall = GetSensor(model, data, "wall");
|
||||
EXPECT_THAT(wall, Pointwise(DoubleNear(tol), {1, 8, 0, 0, -1, 0, 0,
|
||||
0, 0, 0, 0, 0, 0, 0}));
|
||||
|
||||
// normals points *away* from b2 (towards floor / b1)
|
||||
vector b2 = GetSensor(model, data, "b2");
|
||||
EXPECT_THAT(b2, Pointwise(DoubleNear(tol), {3, 0, 0, 0, 0, -1,
|
||||
4, 0, 0, 1, 0, 0}));
|
||||
|
||||
// normal points *towards* b2
|
||||
vector b2f = GetSensor(model, data, "b2_flipped");
|
||||
EXPECT_THAT(b2f, Pointwise(DoubleNear(tol), {3, 0, 0, 0, 0, 1,
|
||||
4, 0, 0, -1, 0, 0}));
|
||||
|
||||
vector b2r = GetSensor(model, data, "b2_reduced");
|
||||
EXPECT_THAT(b2r, Pointwise(DoubleNear(tol), {4, 0, 0, -1, 0, 0}));
|
||||
}
|
||||
|
||||
mj_deleteData(data);
|
||||
mj_deleteModel(model);
|
||||
}
|
||||
|
||||
TEST_F(SensorTest, ContactSorted) {
|
||||
const string xml_path =
|
||||
GetTestDataFilePath("engine/testdata/sensor/contact_sorted.xml");
|
||||
char error[1024];
|
||||
mjModel* model = mj_loadXML(xml_path.c_str(), nullptr, error, sizeof(error));
|
||||
ASSERT_THAT(model, NotNull()) << error;
|
||||
|
||||
mjData* data = mj_makeData(model);
|
||||
while (data->time < .5) {
|
||||
mj_step(model, data);
|
||||
}
|
||||
|
||||
vector unsorted = GetSensor(model, data, "unsorted");
|
||||
EXPECT_THAT(unsorted, SizeIs(4));
|
||||
EXPECT_THAT(unsorted, Not(WhenSorted(ElementsAreArray(unsorted))));
|
||||
|
||||
vector sorted = GetSensor(model, data, "sorted dist");
|
||||
EXPECT_THAT(sorted, SizeIs(4));
|
||||
EXPECT_THAT(sorted, WhenSorted(ElementsAreArray(sorted)));
|
||||
|
||||
vector sorted_force = GetSensor(model, data, "sorted force");
|
||||
EXPECT_THAT(sorted_force, SizeIs(12));
|
||||
vector<mjtNum> nnorms;
|
||||
for (size_t i = 0; i < sorted_force.size(); i += 3) {
|
||||
nnorms.push_back(-sorted_force[i]*sorted_force[i] +
|
||||
-sorted_force[i+1]*sorted_force[i+1] +
|
||||
-sorted_force[i+2]*sorted_force[i+2]);
|
||||
}
|
||||
EXPECT_THAT(nnorms, WhenSorted(ElementsAreArray(nnorms)));
|
||||
|
||||
vector smallest = GetSensor(model, data, "smallest dist");
|
||||
EXPECT_THAT(smallest, SizeIs(1));
|
||||
EXPECT_EQ(smallest[0], sorted[0]);
|
||||
|
||||
vector largest = GetSensor(model, data, "largest force");
|
||||
EXPECT_THAT(largest, SizeIs(3));
|
||||
EXPECT_THAT(largest, ElementsAre(sorted_force[0],
|
||||
sorted_force[1],
|
||||
sorted_force[2]));
|
||||
|
||||
mj_deleteData(data);
|
||||
mj_deleteModel(model);
|
||||
}
|
||||
|
||||
TEST_F(SensorTest, ContactSubtree) {
|
||||
const string xml_path =
|
||||
GetTestDataFilePath("engine/testdata/sensor/contact_subtree.xml");
|
||||
char error[1024];
|
||||
mjModel* model = mj_loadXML(xml_path.c_str(), nullptr, error, sizeof(error));
|
||||
ASSERT_THAT(model, NotNull()) << error;
|
||||
|
||||
mjData* data = mj_makeData(model);
|
||||
|
||||
while (data->time < 0.2) {
|
||||
mj_step(model, data);
|
||||
|
||||
int all = GetSensor(model, data, "all")[0];
|
||||
int w_t1 = GetSensor(model, data, "w_t1")[0];
|
||||
int w_t2 = GetSensor(model, data, "w_t2")[0];
|
||||
int t1 = GetSensor(model, data, "t1")[0];
|
||||
int t2 = GetSensor(model, data, "t2")[0];
|
||||
int t1_t1 = GetSensor(model, data, "t1_t1")[0];
|
||||
int t2_t2 = GetSensor(model, data, "t2_t2")[0];
|
||||
int t1_t2 = GetSensor(model, data, "t1_t2")[0];
|
||||
int t2_t1 = GetSensor(model, data, "t2_t1")[0];
|
||||
|
||||
// compute the number of first tree contacts in two different ways
|
||||
EXPECT_EQ(t1, w_t1 + t1_t1 + t1_t2);
|
||||
|
||||
// compute the number of second tree contacts in two different ways
|
||||
EXPECT_EQ(t2, w_t2 + t2_t2 + t2_t1);
|
||||
|
||||
// compute the number of all contacts in two different ways
|
||||
EXPECT_EQ(all, w_t1 + w_t2 + t1_t1 + t2_t2 + t1_t2);
|
||||
}
|
||||
|
||||
mj_deleteData(data);
|
||||
mj_deleteModel(model);
|
||||
}
|
||||
|
||||
TEST_F(SensorTest, CameraProjection) {
|
||||
constexpr char xml[] = R"(
|
||||
<mujoco>
|
||||
@@ -725,7 +941,7 @@ TEST_F(SensorTest, InsideSite) {
|
||||
for (int i = 0; i < 5; i++) {
|
||||
data->qpos[0] = hpos[i];
|
||||
mj_forward(model, data);
|
||||
std::vector<mjtNum> expected(5, 0.0);
|
||||
vector<mjtNum> expected(5, 0.0);
|
||||
expected[i] = 1.0;
|
||||
EXPECT_EQ(AsVector(data->sensordata, model->nsensordata), expected);
|
||||
}
|
||||
|
||||
@@ -947,5 +947,15 @@ TEST_F(SupportTest, SetKeyframe) {
|
||||
mj_deleteModel(model);
|
||||
}
|
||||
|
||||
TEST_F(SupportTest, ContactSensorDim) {
|
||||
int dataSpec = 1 << mjCONDATA_FOUND |
|
||||
1 << mjCONDATA_FORCE |
|
||||
1 << mjCONDATA_DIST |
|
||||
1 << mjCONDATA_POS |
|
||||
1 << mjCONDATA_TANGENT;
|
||||
|
||||
EXPECT_EQ(mju_condataSize(dataSpec), 1+3+1+3+3);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
} // namespace mujoco
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
<mujoco model="contact">
|
||||
<option gravity="4 0 -3"/>
|
||||
|
||||
<worldbody>
|
||||
<light pos="0 0 3"/>
|
||||
<geom name="floor" type="plane" size=".5 1 .01"/>
|
||||
<geom name="wall" type="plane" size=".25 1 .05" zaxis="-1 0 0" pos=".5 0 .25"/>
|
||||
<site name="site" type="box" size=".15 .15 .15" pos=".5 0 0" rgba=".5 .7 .5 .3"/>
|
||||
|
||||
<body name="b1" pos="0 0 .2">
|
||||
<freejoint/>
|
||||
<geom name="g1" size=".1" rgba=".8 .2 .1 1" mass="1"/>
|
||||
</body>
|
||||
|
||||
<body name="b2" pos="-.3 0 .2">
|
||||
<freejoint/>
|
||||
<geom name="g2" size=".1" rgba=".8 .2 .1 1" mass="1"/>
|
||||
</body>
|
||||
</worldbody>
|
||||
|
||||
<sensor>
|
||||
<contact name="all"/>
|
||||
<contact name="world" body1="world"/>
|
||||
<contact name="b1" body1="b1"/>
|
||||
<contact name="g1" geom1="g1"/>
|
||||
<contact name="b1:g2" body1="b1" geom2="g2"/>
|
||||
<contact name="b1:world" body1="b1" body2="world"/>
|
||||
<contact name="site" site="site"/>
|
||||
<contact name="site:wall" site="site" geom2="wall"/>
|
||||
<contact name="wall" geom1="wall" num="2" data="found force normal"/>
|
||||
<contact name="b2" body1="b2" data="force normal" num="2"/>
|
||||
<contact name="b2_flipped" body2="b2" data="force normal" num="2"/>
|
||||
<contact name="b2_reduced" body2="b2" data="force normal" reduce="maxforce"/>
|
||||
</sensor>
|
||||
</mujoco>
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
<mujoco model="contact sorted">
|
||||
<worldbody>
|
||||
<light pos="0 0 3"/>
|
||||
<geom name="floor" type="plane" size=".5 1 .01"/>
|
||||
|
||||
<body name="spheres">
|
||||
<joint axis="0 -1 0"/>
|
||||
<geom size=".1" pos="-.1 0 0" rgba=".8 .2 .1 1"/>
|
||||
<geom size=".1" pos=".1 0 0" rgba=".8 .2 .1 1"/>
|
||||
<geom size=".1" pos="-.2 0 0" rgba=".8 .2 .1 1"/>
|
||||
<geom size=".1" pos=".3 0 0" rgba=".8 .2 .1 1"/>
|
||||
</body>
|
||||
</worldbody>
|
||||
|
||||
<sensor>
|
||||
<contact name="unsorted" body1="spheres" num="4" data="dist"/>
|
||||
<contact name="sorted dist" body1="spheres" num="4" data="dist" reduce="mindist"/>
|
||||
<contact name="sorted force" body1="spheres" num="4" data="force" reduce="maxforce"/>
|
||||
<contact name="smallest dist" body1="spheres" num="1" data="dist" reduce="mindist"/>
|
||||
<contact name="largest force" body1="spheres" num="1" data="force" reduce="maxforce"/>
|
||||
</sensor>
|
||||
</mujoco>
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
<mujoco model="contact subtree">
|
||||
<asset>
|
||||
<model file="humanoid.xml"/>
|
||||
</asset>
|
||||
|
||||
<worldbody>
|
||||
<light pos="0 0 5"/>
|
||||
<geom name="floor" type="plane" size="2 2 .01"/>
|
||||
|
||||
<!-- two horizontal humanoids, in the air, one above the other -->
|
||||
<frame pos="-.5 0 1.2" euler="0 90 0">
|
||||
<attach model="Humanoid" body="torso" prefix="1"/>
|
||||
</frame>
|
||||
<frame pos="-.5 0 0.5" euler="0 90 -90">
|
||||
<attach model="Humanoid" body="torso" prefix="2"/>
|
||||
</frame>
|
||||
</worldbody>
|
||||
|
||||
<sensor>
|
||||
<contact name="all"/>
|
||||
<contact name="w_t1" body1="world" subtree2="1torso"/>
|
||||
<contact name="w_t2" body1="world" subtree2="2torso"/>
|
||||
<contact name="t1" subtree1="1torso"/>
|
||||
<contact name="t2" subtree1="2torso"/>
|
||||
<contact name="t1_t1" subtree1="1torso" subtree2="1torso"/>
|
||||
<contact name="t2_t2" subtree1="2torso" subtree2="2torso"/>
|
||||
<contact name="t1_t2" subtree1="1torso" subtree2="2torso"/>
|
||||
<contact name="t2_t1" subtree1="2torso" subtree2="1torso"/>
|
||||
</sensor>
|
||||
</mujoco>
|
||||
+267
@@ -0,0 +1,267 @@
|
||||
<!-- Copyright 2021 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.
|
||||
-->
|
||||
|
||||
<mujoco model="Humanoid">
|
||||
<option timestep="0.005"/>
|
||||
|
||||
<visual>
|
||||
<map force="0.1" zfar="30"/>
|
||||
<rgba haze="0.15 0.25 0.35 1"/>
|
||||
<global offwidth="2560" offheight="1440" elevation="-20" azimuth="120"/>
|
||||
</visual>
|
||||
|
||||
<statistic center="0 0 0.7"/>
|
||||
|
||||
<asset>
|
||||
<texture type="skybox" builtin="gradient" rgb1=".3 .5 .7" rgb2="0 0 0" width="32" height="512"/>
|
||||
<texture name="body" type="cube" builtin="flat" mark="cross" width="128" height="128" rgb1="0.8 0.6 0.4" rgb2="0.8 0.6 0.4" markrgb="1 1 1"/>
|
||||
<material name="body" texture="body" texuniform="true" rgba="0.8 0.6 .4 1"/>
|
||||
<texture name="grid" type="2d" builtin="checker" width="512" height="512" rgb1=".1 .2 .3" rgb2=".2 .3 .4"/>
|
||||
<material name="grid" texture="grid" texrepeat="1 1" texuniform="true" reflectance=".2"/>
|
||||
</asset>
|
||||
|
||||
<default>
|
||||
<motor ctrlrange="-1 1" ctrllimited="true"/>
|
||||
<default class="body">
|
||||
|
||||
<!-- geoms -->
|
||||
<geom type="capsule" condim="1" friction=".7" solimp=".9 .99 .003" solref=".015 1" material="body" group="1"/>
|
||||
<default class="thigh">
|
||||
<geom size=".06"/>
|
||||
</default>
|
||||
<default class="shin">
|
||||
<geom fromto="0 0 0 0 0 -.3" size=".049"/>
|
||||
</default>
|
||||
<default class="foot">
|
||||
<geom size=".027"/>
|
||||
<default class="foot1">
|
||||
<geom fromto="-.07 -.01 0 .14 -.03 0"/>
|
||||
</default>
|
||||
<default class="foot2">
|
||||
<geom fromto="-.07 .01 0 .14 .03 0"/>
|
||||
</default>
|
||||
</default>
|
||||
<default class="arm_upper">
|
||||
<geom size=".04"/>
|
||||
</default>
|
||||
<default class="arm_lower">
|
||||
<geom size=".031"/>
|
||||
</default>
|
||||
<default class="hand">
|
||||
<geom type="sphere" size=".04"/>
|
||||
</default>
|
||||
|
||||
<!-- joints -->
|
||||
<joint type="hinge" damping=".2" stiffness="1" armature=".01" limited="true" solimplimit="0 .99 .01"/>
|
||||
<default class="joint_big">
|
||||
<joint damping="5" stiffness="10"/>
|
||||
<default class="hip_x">
|
||||
<joint range="-30 10"/>
|
||||
</default>
|
||||
<default class="hip_z">
|
||||
<joint range="-60 35"/>
|
||||
</default>
|
||||
<default class="hip_y">
|
||||
<joint axis="0 1 0" range="-150 20"/>
|
||||
</default>
|
||||
<default class="joint_big_stiff">
|
||||
<joint stiffness="20"/>
|
||||
</default>
|
||||
</default>
|
||||
<default class="knee">
|
||||
<joint pos="0 0 .02" axis="0 -1 0" range="-160 2"/>
|
||||
</default>
|
||||
<default class="ankle">
|
||||
<joint range="-50 50"/>
|
||||
<default class="ankle_y">
|
||||
<joint pos="0 0 .08" axis="0 1 0" stiffness="6"/>
|
||||
</default>
|
||||
<default class="ankle_x">
|
||||
<joint pos="0 0 .04" stiffness="3"/>
|
||||
</default>
|
||||
</default>
|
||||
<default class="shoulder">
|
||||
<joint range="-85 60"/>
|
||||
</default>
|
||||
<default class="elbow">
|
||||
<joint range="-100 50" stiffness="0"/>
|
||||
</default>
|
||||
</default>
|
||||
</default>
|
||||
|
||||
<worldbody>
|
||||
<geom name="floor" size="0 0 .05" type="plane" material="grid" condim="3"/>
|
||||
<light name="spotlight" mode="targetbodycom" target="torso" diffuse=".8 .8 .8" specular="0.3 0.3 0.3" pos="0 -6 4" cutoff="30"/>
|
||||
<light name="top" pos="0 0 2" mode="trackcom"/>
|
||||
<body name="torso" pos="0 0 1.282" childclass="body">
|
||||
<camera name="back" pos="-3 0 1" xyaxes="0 -1 0 1 0 2" mode="trackcom"/>
|
||||
<camera name="side" pos="0 -3 1" xyaxes="1 0 0 0 1 2" mode="trackcom"/>
|
||||
<freejoint name="root"/>
|
||||
<geom name="torso" fromto="0 -.07 0 0 .07 0" size=".07"/>
|
||||
<geom name="waist_upper" fromto="-.01 -.06 -.12 -.01 .06 -.12" size=".06"/>
|
||||
<body name="head" pos="0 0 .19">
|
||||
<geom name="head" type="sphere" size=".09"/>
|
||||
<camera name="egocentric" pos=".09 0 0" xyaxes="0 -1 0 .1 0 1" fovy="80"/>
|
||||
</body>
|
||||
<body name="waist_lower" pos="-.01 0 -.26">
|
||||
<geom name="waist_lower" fromto="0 -.06 0 0 .06 0" size=".06"/>
|
||||
<joint name="abdomen_z" pos="0 0 .065" axis="0 0 1" range="-45 45" class="joint_big_stiff"/>
|
||||
<joint name="abdomen_y" pos="0 0 .065" axis="0 1 0" range="-75 30" class="joint_big"/>
|
||||
<body name="pelvis" pos="0 0 -.165">
|
||||
<joint name="abdomen_x" pos="0 0 .1" axis="1 0 0" range="-35 35" class="joint_big"/>
|
||||
<geom name="butt" fromto="-.02 -.07 0 -.02 .07 0" size=".09"/>
|
||||
<body name="thigh_right" pos="0 -.1 -.04">
|
||||
<joint name="hip_x_right" axis="1 0 0" class="hip_x"/>
|
||||
<joint name="hip_z_right" axis="0 0 1" class="hip_z"/>
|
||||
<joint name="hip_y_right" class="hip_y"/>
|
||||
<geom name="thigh_right" fromto="0 0 0 0 .01 -.34" class="thigh"/>
|
||||
<body name="shin_right" pos="0 .01 -.4">
|
||||
<joint name="knee_right" class="knee"/>
|
||||
<geom name="shin_right" class="shin"/>
|
||||
<body name="foot_right" pos="0 0 -.39">
|
||||
<joint name="ankle_y_right" class="ankle_y"/>
|
||||
<joint name="ankle_x_right" class="ankle_x" axis="1 0 .5"/>
|
||||
<geom name="foot1_right" class="foot1"/>
|
||||
<geom name="foot2_right" class="foot2"/>
|
||||
</body>
|
||||
</body>
|
||||
</body>
|
||||
<body name="thigh_left" pos="0 .1 -.04">
|
||||
<joint name="hip_x_left" axis="-1 0 0" class="hip_x"/>
|
||||
<joint name="hip_z_left" axis="0 0 -1" class="hip_z"/>
|
||||
<joint name="hip_y_left" class="hip_y"/>
|
||||
<geom name="thigh_left" fromto="0 0 0 0 -.01 -.34" class="thigh"/>
|
||||
<body name="shin_left" pos="0 -.01 -.4">
|
||||
<joint name="knee_left" class="knee"/>
|
||||
<geom name="shin_left" fromto="0 0 0 0 0 -.3" class="shin"/>
|
||||
<body name="foot_left" pos="0 0 -.39">
|
||||
<joint name="ankle_y_left" class="ankle_y"/>
|
||||
<joint name="ankle_x_left" class="ankle_x" axis="-1 0 -.5"/>
|
||||
<geom name="foot1_left" class="foot1"/>
|
||||
<geom name="foot2_left" class="foot2"/>
|
||||
</body>
|
||||
</body>
|
||||
</body>
|
||||
</body>
|
||||
</body>
|
||||
<body name="upper_arm_right" pos="0 -.17 .06">
|
||||
<joint name="shoulder1_right" axis="2 1 1" class="shoulder"/>
|
||||
<joint name="shoulder2_right" axis="0 -1 1" class="shoulder"/>
|
||||
<geom name="upper_arm_right" fromto="0 0 0 .16 -.16 -.16" class="arm_upper"/>
|
||||
<body name="lower_arm_right" pos=".18 -.18 -.18">
|
||||
<joint name="elbow_right" axis="0 -1 1" class="elbow"/>
|
||||
<geom name="lower_arm_right" fromto=".01 .01 .01 .17 .17 .17" class="arm_lower"/>
|
||||
<body name="hand_right" pos=".18 .18 .18">
|
||||
<geom name="hand_right" zaxis="1 1 1" class="hand"/>
|
||||
</body>
|
||||
</body>
|
||||
</body>
|
||||
<body name="upper_arm_left" pos="0 .17 .06">
|
||||
<joint name="shoulder1_left" axis="-2 1 -1" class="shoulder"/>
|
||||
<joint name="shoulder2_left" axis="0 -1 -1" class="shoulder"/>
|
||||
<geom name="upper_arm_left" fromto="0 0 0 .16 .16 -.16" class="arm_upper"/>
|
||||
<body name="lower_arm_left" pos=".18 .18 -.18">
|
||||
<joint name="elbow_left" axis="0 -1 -1" class="elbow"/>
|
||||
<geom name="lower_arm_left" fromto=".01 -.01 .01 .17 -.17 .17" class="arm_lower"/>
|
||||
<body name="hand_left" pos=".18 -.18 .18">
|
||||
<geom name="hand_left" zaxis="1 -1 1" class="hand"/>
|
||||
</body>
|
||||
</body>
|
||||
</body>
|
||||
</body>
|
||||
</worldbody>
|
||||
|
||||
<contact>
|
||||
<exclude body1="waist_lower" body2="thigh_right"/>
|
||||
<exclude body1="waist_lower" body2="thigh_left"/>
|
||||
</contact>
|
||||
|
||||
<tendon>
|
||||
<fixed name="hamstring_right" limited="true" range="-0.3 2">
|
||||
<joint joint="hip_y_right" coef=".5"/>
|
||||
<joint joint="knee_right" coef="-.5"/>
|
||||
</fixed>
|
||||
<fixed name="hamstring_left" limited="true" range="-0.3 2">
|
||||
<joint joint="hip_y_left" coef=".5"/>
|
||||
<joint joint="knee_left" coef="-.5"/>
|
||||
</fixed>
|
||||
</tendon>
|
||||
|
||||
<actuator>
|
||||
<motor name="abdomen_z" gear="40" joint="abdomen_z"/>
|
||||
<motor name="abdomen_y" gear="40" joint="abdomen_y"/>
|
||||
<motor name="abdomen_x" gear="40" joint="abdomen_x"/>
|
||||
<motor name="hip_x_right" gear="40" joint="hip_x_right"/>
|
||||
<motor name="hip_z_right" gear="40" joint="hip_z_right"/>
|
||||
<motor name="hip_y_right" gear="120" joint="hip_y_right"/>
|
||||
<motor name="knee_right" gear="80" joint="knee_right"/>
|
||||
<motor name="ankle_y_right" gear="20" joint="ankle_y_right"/>
|
||||
<motor name="ankle_x_right" gear="20" joint="ankle_x_right"/>
|
||||
<motor name="hip_x_left" gear="40" joint="hip_x_left"/>
|
||||
<motor name="hip_z_left" gear="40" joint="hip_z_left"/>
|
||||
<motor name="hip_y_left" gear="120" joint="hip_y_left"/>
|
||||
<motor name="knee_left" gear="80" joint="knee_left"/>
|
||||
<motor name="ankle_y_left" gear="20" joint="ankle_y_left"/>
|
||||
<motor name="ankle_x_left" gear="20" joint="ankle_x_left"/>
|
||||
<motor name="shoulder1_right" gear="20" joint="shoulder1_right"/>
|
||||
<motor name="shoulder2_right" gear="20" joint="shoulder2_right"/>
|
||||
<motor name="elbow_right" gear="40" joint="elbow_right"/>
|
||||
<motor name="shoulder1_left" gear="20" joint="shoulder1_left"/>
|
||||
<motor name="shoulder2_left" gear="20" joint="shoulder2_left"/>
|
||||
<motor name="elbow_left" gear="40" joint="elbow_left"/>
|
||||
</actuator>
|
||||
|
||||
<keyframe>
|
||||
<!--
|
||||
The values below are split into rows for readibility:
|
||||
torso position
|
||||
torso orientation
|
||||
spinal
|
||||
right leg
|
||||
left leg
|
||||
arms
|
||||
-->
|
||||
<key name="squat"
|
||||
qpos="0 0 0.596
|
||||
0.988015 0 0.154359 0
|
||||
0 0.4 0
|
||||
-0.25 -0.5 -2.5 -2.65 -0.8 0.56
|
||||
-0.25 -0.5 -2.5 -2.65 -0.8 0.56
|
||||
0 0 0 0 0 0"/>
|
||||
<key name="stand_on_left_leg"
|
||||
qpos="0 0 1.21948
|
||||
0.971588 -0.179973 0.135318 -0.0729076
|
||||
-0.0516 -0.202 0.23
|
||||
-0.24 -0.007 -0.34 -1.76 -0.466 -0.0415
|
||||
-0.08 -0.01 -0.37 -0.685 -0.35 -0.09
|
||||
0.109 -0.067 -0.7 -0.05 0.12 0.16"/>
|
||||
<key name="prone"
|
||||
qpos="0.4 0 0.0757706
|
||||
0.7325 0 0.680767 0
|
||||
0 0.0729 0
|
||||
0.0077 0.0019 -0.026 -0.351 -0.27 0
|
||||
0.0077 0.0019 -0.026 -0.351 -0.27 0
|
||||
0.56 -0.62 -1.752
|
||||
0.56 -0.62 -1.752"/>
|
||||
<key name="supine"
|
||||
qpos="-0.4 0 0.08122
|
||||
0.722788 0 -0.69107 0
|
||||
0 -0.25 0
|
||||
0.0182 0.0142 0.3 0.042 -0.44 -0.02
|
||||
0.0182 0.0142 0.3 0.042 -0.44 -0.02
|
||||
0.186 -0.73 -1.73
|
||||
0.186 -0.73 -1.73"/>
|
||||
</keyframe>
|
||||
</mujoco>
|
||||
@@ -394,11 +394,12 @@ public enum mjtSensor : int{
|
||||
mjSENS_GEOMDIST = 39,
|
||||
mjSENS_GEOMNORMAL = 40,
|
||||
mjSENS_GEOMFROMTO = 41,
|
||||
mjSENS_E_POTENTIAL = 42,
|
||||
mjSENS_E_KINETIC = 43,
|
||||
mjSENS_CLOCK = 44,
|
||||
mjSENS_PLUGIN = 45,
|
||||
mjSENS_USER = 46,
|
||||
mjSENS_CONTACT = 42,
|
||||
mjSENS_E_POTENTIAL = 43,
|
||||
mjSENS_E_KINETIC = 44,
|
||||
mjSENS_CLOCK = 45,
|
||||
mjSENS_PLUGIN = 46,
|
||||
mjSENS_USER = 47,
|
||||
}
|
||||
public enum mjtStage : int{
|
||||
mjSTAGE_NONE = 0,
|
||||
@@ -412,6 +413,16 @@ public enum mjtDataType : int{
|
||||
mjDATATYPE_AXIS = 2,
|
||||
mjDATATYPE_QUATERNION = 3,
|
||||
}
|
||||
public enum mjtConDataField : int{
|
||||
mjCONDATA_FOUND = 0,
|
||||
mjCONDATA_FORCE = 1,
|
||||
mjCONDATA_TORQUE = 2,
|
||||
mjCONDATA_DIST = 3,
|
||||
mjCONDATA_POS = 4,
|
||||
mjCONDATA_NORMAL = 5,
|
||||
mjCONDATA_TANGENT = 6,
|
||||
mjNCONDATA = 7,
|
||||
}
|
||||
public enum mjtSameFrame : int{
|
||||
mjSAMEFRAME_NONE = 0,
|
||||
mjSAMEFRAME_BODY = 1,
|
||||
|
||||
Reference in New Issue
Block a user