Introduces a new "strain" equality mode (flex_edgeequality = 3) for flexcomps with trilinear interpolation. This mode enforces constraints on the three invariants of the Green-Lagrange strain tensor at 8 Gauss quadrature points within each flex element, resulting in 24 equality constraints per flexcomp.

Also fixes a bug when attaching a flex with constraints.

PiperOrigin-RevId: 881349765
Change-Id: I8e6df8239488bfdc87d7e5b473568975e38c737e
This commit is contained in:
Alessio Quaglino
2026-03-10 04:25:19 -07:00
committed by Copybara-Service
parent fe6c81ed93
commit 713b5524bc
23 changed files with 899 additions and 48 deletions
+42 -9
View File
@@ -3614,7 +3614,9 @@ saving the XML:
for the entire flex, independent of the number of vertices. The positions of the vertices are updated using
quadratic interpolation over the bounding box. While this option requires more degrees of freedom than trilinear
flexes, it enables curved deformation modes, while the only modes achievable for trilinear flexes are
strech/compression and shear.
strech/compression and shear. To understand the difference between the two parametrizations, see `a trilinear cube
<https://github.com/google-deepmind/mujoco/blob/main/model/flex/trilinear.xml>`__ and `a quadratic cube
<https://github.com/google-deepmind/mujoco/blob/main/model/flex/quadratic.xml>`__.
Note that a higher interpolation order generally requires a smaller time step for stability, although usually not as
large as with the "full" option and a fine mesh.
@@ -3820,10 +3822,12 @@ element is used to adjust the properties of all edges in the flex.
.. _flexcomp-edge-equality:
:at:`equality`: :at-val:`[false, true, vert], "false"`
The type of equality constraint applied to this edge. If **false**, no equality constraint is applied. If **true**,
then edge constraints are enforced. If **vert**, an averaged constraint is used, see
:ref:`flexvert<equality-flexvert>`.
:at:`equality`: :at-val:`[false, true, vert, strain], "false"`
The type of equality constraint applied to this edge. If :at-val:`false`, no equality constraint is applied. If
:at-val:`true`, then edge constraints are enforced. If :at-val:`vert`, an averaged constraint is used, see
:ref:`flexvert<equality-flexvert>`. if :at-val:`strain`, then a constraint is added to enforce that the invariants of
the strain tensor do not change; this is only equality constraint type supported for trilinear and quadratic
:ref:`dofs<body-flexcomp-dof>` elements and :ref:`here<equality-flexstrain>`.
.. _flexcomp-edge-solref:
.. _flexcomp-edge-solimp:
@@ -4254,7 +4258,8 @@ The elasticity model is a `Saint Venant-Kirchhoff
<https://en.wikipedia.org/wiki/Hyperelastic_material#Saint_Venant%E2%80%93Kirchhoff_model>`__ model discretized with
piecewise linear finite elements, intended to simulate the compression or elongation of hyperelastic materials subjected
to large displacements (finite rotations) and small strains, since it uses a nonlinear strain-displacement but a linear
stress-strain relationship.. See also :ref:`deformable <CDeformable>` objects.
stress-strain relationship. See also :ref:`deformable <CDeformable>` objects and `this model
<https://github.com/google-deepmind/mujoco/blob/main/model/flex/floppy.xml>`__.
.. _flex-elasticity-young:
@@ -4787,7 +4792,8 @@ This element constrains the length of one tendon to be a quartic polynomial of a
This element constrains the lengths of all edges of a specified flex to their respective lengths in the initial model
configuration. In this way the edges are used to maintain the shape of the deformable entity. Note that all other
equality constraint types add a fixed number of scalar constraints, while this element adds as many scalar constraints
as there are edges in the specified flex.
as there are edges in the specified flex. See `this model
<https://github.com/google-deepmind/mujoco/blob/main/model/flex/plate.xml>`__ for an example.
.. _equality-flex-name:
.. _equality-flex-class:
@@ -4811,8 +4817,9 @@ as there are edges in the specified flex.
This element constrains the trace and the derminant of the strain tensor to that of the identity matrix as in Chen, Kry,
and Vouga, "Locking-free Simulation of Isometric Thin Plates", 2019. The strain tensor is computed per triangle and
averaged over all triangles adjacent to a vertex. This reduces the number of constraints from 2T to 2V, freeing
V degrees of freedom to avoid locking. It is only supported for dimension 2, i.e., cloth-like flexes.
averaged over all triangles adjacent to a vertex. This reduces the number of constraints from 2T to 2V, freeing V
degrees of freedom to avoid locking. It is only supported for dimension 2, i.e., cloth-like flexes. See `this model
<https://github.com/google-deepmind/mujoco/blob/main/model/flex/poncho.xml>`__ for an example.
.. _equality-flexvert-name:
.. _equality-flexvert-class:
@@ -4829,6 +4836,32 @@ V degrees of freedom to avoid locking. It is only supported for dimension 2, i.e
Name of the flex whose vertices are being constrained.
.. _equality-flexstrain:
:el-prefix:`equality/` |-| **flexstrain** |*|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
This element constrains the strain invariants of a trilinear or quadratic flex to their initial values. Specifically, it
enforces that the trace and determinant of the deformation gradient remain constant, preserving volume and preventing
excessive stretching. This constraint type is only supported for dimension 3 trilinear flexes (i.e., volumetric
deformable bodies using trilinear interpolation). See `this model
<https://github.com/google-deepmind/mujoco/blob/main/model/flex/strain.xml>`__ for an example.
.. _equality-flexstrain-name:
.. _equality-flexstrain-class:
.. _equality-flexstrain-active:
.. _equality-flexstrain-solref:
.. _equality-flexstrain-solimp:
:at:`name`, :at:`class`, :at:`active`, :at:`solref`, :at:`solimp`
Same as in :ref:`connect <equality-connect>` element.
.. _equality-flexstrain-flex:
:at:`flex`: :at-val:`string, required`
Name of the flex whose strain invariants are being constrained.
.. _equality-distance:
:el-prefix:`equality/` |-| **distance** |*|
+24
View File
@@ -2045,6 +2045,30 @@
:ref:`solimp<equality-flexvert-solimp>`
.. dropdown:: :ref:`flexstrain<equality-flexstrain>` |*|
.. grid:: 2 3 4 4
:gutter: 0
.. grid-item::
:ref:`name<equality-flexstrain-name>`
.. grid-item::
:ref:`class<equality-flexstrain-class>`
.. grid-item::
:ref:`flex<equality-flexstrain-flex>`
.. grid-item::
:ref:`active<equality-flexstrain-active>`
.. grid-item::
:ref:`solref<equality-flexstrain-solref>`
.. grid-item::
:ref:`solimp<equality-flexstrain-solimp>`
.. dropdown:: :ref:`tendon<tendon>` |*|
+2
View File
@@ -15,6 +15,8 @@ General
``ten_J_colind`` have been moved from :ref:`mjData` to :ref:`mjModel` and are no longer computed at run time by
``mj_tendon`` but at compile time.
- Added a new ``strain`` :ref:`equality constraint<flexcomp-edge-equality>` type for trilinear and quadratic
:ref:`dofs<body-flexcomp-dof>`.
- Flexes now support collisions with SDF geoms.
- Improved memory requirements for ``ten_J`` and ``ten_J_colind`` by reducing the upper bound for the number
of non-zeros ``nJten``.
+2 -1
View File
@@ -609,6 +609,7 @@ typedef enum mjtEq_ { // type of equality constraint
mjEQ_TENDON, // couple the lengths of two tendons with cubic
mjEQ_FLEX, // fix all edge lengths of a flex
mjEQ_FLEXVERT, // fix all vertex lengths of a flex
mjEQ_FLEXSTRAIN, // fix strain invariants of a trilinear flex
mjEQ_DISTANCE // unsupported, will cause an error if used
} mjtEq;
typedef enum mjtWrap_ { // type of tendon wrap object
@@ -1350,7 +1351,7 @@ struct mjModel_ {
mjtNum* flex_damping; // Rayleigh's damping coefficient (nflex x 1)
mjtNum* flex_edgestiffness; // edge stiffness (nflex x 1)
mjtNum* flex_edgedamping; // edge damping (nflex x 1)
int* flex_edgeequality; // 0: none, 1: edges, 2: vertices (nflex x 1)
int* flex_edgeequality; // 0:none, 1:edges, 2:vertices, 3:strain (nflex x 1)
mjtByte* flex_rigid; // are all vertices in the same body (nflex x 1)
mjtByte* flexedge_rigid; // are both edge vertices in same body (nflexedge x 1)
mjtByte* flex_centered; // are all vertex coordinates (0,0,0) (nflex x 1)
+2 -1
View File
@@ -211,6 +211,7 @@ typedef enum mjtEq_ { // type of equality constraint
mjEQ_TENDON, // couple the lengths of two tendons with cubic
mjEQ_FLEX, // fix all edge lengths of a flex
mjEQ_FLEXVERT, // fix all vertex lengths of a flex
mjEQ_FLEXSTRAIN, // fix strain invariants of a trilinear flex
mjEQ_DISTANCE // unsupported, will cause an error if used
} mjtEq;
@@ -1011,7 +1012,7 @@ struct mjModel_ {
mjtNum* flex_damping; // Rayleigh's damping coefficient (nflex x 1)
mjtNum* flex_edgestiffness; // edge stiffness (nflex x 1)
mjtNum* flex_edgedamping; // edge damping (nflex x 1)
int* flex_edgeequality; // 0: none, 1: edges, 2: vertices (nflex x 1)
int* flex_edgeequality; // 0:none, 1:edges, 2:vertices, 3:strain (nflex x 1)
mjtByte* flex_rigid; // are all vertices in the same body (nflex x 1)
mjtByte* flexedge_rigid; // are both edge vertices in same body (nflexedge x 1)
mjtByte* flex_centered; // are all vertex coordinates (0,0,0) (nflex x 1)
+64
View File
@@ -0,0 +1,64 @@
<!-- Copyright 2026 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="Gripper">
<include file="scene.xml"/>
<option cone="elliptic" impratio="10" integrator="implicitfast"/>
<worldbody>
<body>
<freejoint/>
<geom type="box" size=".46 .5 .4" pos="0 0 1.5" rgba=".5 .5 0 1" condim="6"/>
</body>
<body name="column_00" pos=".7 .4 .5">
<geom type="box" size=".1 .1 .5" rgba=".2 .2 .2 1"/>
<flexcomp type="box" pos="-.24 0 .1" dim="3" spacing=".03 .01 .01"
radius=".001" rgba="0 .7 .7 1" mass=".5" name="left" dof="trilinear">
<edge equality="strain"/>
<contact selfcollide="none" internal="false" friction="3"/>
<pin id="4 5 6 7"/>
</flexcomp>
</body>
<body name="column_01" pos=".7 -.4 .5">
<geom type="box" size=".1 .1 .5" rgba=".2 .2 .2 1"/>
<flexcomp type="box" pos="-.24 0 .1" dim="3" spacing=".03 .01 .01"
radius=".001" rgba="0 .7 .7 1" mass=".5" name="right" dof="trilinear">
<edge equality="strain"/>
<contact selfcollide="none" internal="false" friction="3"/>
<pin id="4 5 6 7"/>
</flexcomp>
</body>
<body name="column_02" pos=" -.7 .4 .5">
<geom type="box" size=".1 .1 .5" rgba=".2 .2 .2 1"/>
<flexcomp type="box" pos=".24 0 .1" dim="3" spacing=".03 .01 .01"
radius=".001" rgba="0 .7 .7 1" mass=".5" name="bottom" dof="trilinear">
<edge equality="strain"/>
<contact selfcollide="none" internal="false" friction="3"/>
<pin id="0 1 2 3"/>
</flexcomp>
</body>
<body name="column_03" pos="-.7 -.4 .5">
<geom type="box" size=".1 .1 .5" rgba=".2 .2 .2 1"/>
<flexcomp type="box" pos=".24 0 .1" dim="3" spacing=".03 .01 .01"
radius=".001" rgba="0 .7 .7 1" mass=".5" name="top" dof="trilinear">
<edge equality="strain"/>
<contact selfcollide="none" internal="false" friction="3"/>
<pin id="0 1 2 3"/>
</flexcomp>
</body>
</worldbody>
</mujoco>
+2 -1
View File
@@ -222,7 +222,8 @@ ENUMS: Mapping[str, EnumDecl] = dict([
('mjEQ_TENDON', 3),
('mjEQ_FLEX', 4),
('mjEQ_FLEXVERT', 5),
('mjEQ_DISTANCE', 6),
('mjEQ_FLEXSTRAIN', 6),
('mjEQ_DISTANCE', 7),
]),
)),
('mjtWrap',
+1 -1
View File
@@ -2980,7 +2980,7 @@ STRUCTS: Mapping[str, StructDecl] = dict([
type=PointerType(
inner_type=ValueType(name='int'),
),
doc='0: none, 1: edges, 2: vertices',
doc='0:none, 1:edges, 2:vertices, 3:strain',
array_extent=('nflex',),
),
StructFieldDecl(
+391 -25
View File
@@ -47,6 +47,26 @@
//-------------------------- utility functions -----------------------------------------------------
// basis functions for flex strain constraints
static void basis(int order, int i, mjtNum p, mjtNum* phi, mjtNum* dphi) {
if (order == 1) {
*phi = (i == 0 ? 1 - p : p);
*dphi = (i == 0 ? -1 : 1);
} else {
if (i == 0) {
*phi = 2 * p * p - 3 * p + 1;
*dphi = 4 * p - 3;
} else if (i == 1) {
*phi = 4 * (p - p * p);
*dphi = 4 * (1 - 2 * p);
} else {
*phi = 2 * p * p - p;
*dphi = 4 * p - 1;
}
}
}
// allocate efc arrays on arena, return 1 on success, 0 on failure
static int arenaAllocEfc(const mjModel* m, mjData* d) {
#undef MJ_M
@@ -612,36 +632,336 @@ void mj_instantiateEquality(const mjModel* m, mjData* d) {
size = 1;
break;
case mjEQ_FLEX:
flex_edgeadr = m->flex_edgeadr[id[0]];
flex_edgenum = m->flex_edgenum[id[0]];
// add one constraint per non-rigid edge
for (int e=flex_edgeadr; e < flex_edgeadr+flex_edgenum; e++) {
// skip rigid
if (m->flexedge_rigid[e]) {
continue;
}
case mjEQ_FLEXSTRAIN: {
// strain constraint mode: add 24 constraints (3 invariants at 8 Gauss points)
int f = id[0];
int nodenum = m->flex_nodenum[f];
int* bodyid = m->flex_nodebodyid + m->flex_nodeadr[f];
int order = m->flex_interp[f];
// position error
cpos[0] = d->flexedge_length[e] - m->flexedge_length0[e];
// skip if not interpolated (order == 0 or no nodes)
if (!order || !nodenum) {
break;
}
// add constraint: sparse or dense
if (issparse) {
mj_addConstraint(m, d, d->flexedge_J+m->flexedge_J_rowadr[e], cpos, 0, 0,
1, mjCNSTR_EQUALITY, i,
m->flexedge_J_rownnz[e],
m->flexedge_J_colind+m->flexedge_J_rowadr[e]);
// allocate stack for node positions and Jacobians
mj_markStack(d);
mjtNum* xpos = mjSTACKALLOC(d, 3*nodenum, mjtNum);
mjtNum* node_jac = mjSTACKALLOC(d, 3*nodenum*nv, mjtNum);
int* chain_col = mjSTACKALLOC(d, nv, int);
mjtNum* strain_jac = mjSTACKALLOC(d, nv, mjtNum);
// compute global node positions from body states
// Green-Lagrange strain E = ½(F'F - I) is rotationally invariant,
// so we don't need corotational decomposition - use global positions directly
int nstart = m->flex_nodeadr[f];
for (int n = 0; n < nodenum; n++) {
if (m->flex_centered[f]) {
mju_copy3(xpos + 3*n, d->xpos + 3*bodyid[n]);
} else {
mju_zero(jac[0], nv); // reuse first row of jac[0]
int rowadr = m->flexedge_J_rowadr[e];
int rownnz = m->flexedge_J_rownnz[e];
for (int k=0; k<rownnz; k++) {
jac[0][m->flexedge_J_colind[rowadr+k]] = d->flexedge_J[rowadr+k];
}
mj_addConstraint(m, d, jac[0], cpos, 0, 0, 1, mjCNSTR_EQUALITY, i, 0, NULL);
mju_mulMatVec3(xpos + 3*n, d->xmat + 9*bodyid[n], m->flex_node + 3*(n + nstart));
mju_addTo3(xpos + 3*n, d->xpos + 3*bodyid[n]);
}
}
// compute node Jacobians in global frame (3*nodenum rows x nv cols)
mjtNum* blk_jac = mjSTACKALLOC(d, 3*nv, mjtNum);
mju_zero(node_jac, 3*nodenum*nv);
for (int n = 0; n < nodenum; n++) {
int chain_nnz = mj_bodyChain(m, bodyid[n], chain_col);
mju_zero(blk_jac, 3*nv);
mj_jacSparse(m, d, blk_jac, NULL, xpos + 3*n, bodyid[n], chain_nnz, chain_col);
// expand sparse Jacobian to dense row format
for (int r = 0; r < 3; r++) {
for (int k = 0; k < chain_nnz; k++) {
node_jac[(3*n + r)*nv + chain_col[k]] = blk_jac[r*chain_nnz + k];
}
}
}
// compute combined chain for all nodes (for sparse mode)
int* combined_chain = mjSTACKALLOC(d, nv, int);
int combined_nnz = 0;
if (issparse) {
// mark which DOFs are used by any node body
int* dof_used = mjSTACKALLOC(d, nv, int);
mju_zeroInt(dof_used, nv);
for (int n = 0; n < nodenum; n++) {
int temp_chain[200]; // max DOFs per body
int temp_nnz = mj_bodyChain(m, bodyid[n], temp_chain);
for (int k = 0; k < temp_nnz; k++) {
dof_used[temp_chain[k]] = 1;
}
}
// build combined chain from marked DOFs
for (int q = 0; q < nv; q++) {
if (dof_used[q]) {
combined_chain[combined_nnz++] = q;
}
}
}
// Gauss-Legendre quadrature points in [0,1]^3
// order=1: 2x2x2=8 points, order=2: 3x3x3=27 points
int nquad = order + 1; // quadrature order per axis
int ngauss = nquad * nquad * nquad; // total Gauss points
// 1D Gauss-Legendre points and weights on [0,1]
mjtNum gp1d[3]; // max 3 points for order=2
if (nquad == 2) {
gp1d[0] = 0.5 - 0.5/mju_sqrt(3.0); // ~ 0.211
gp1d[1] = 0.5 + 0.5/mju_sqrt(3.0); // ~ 0.789
} else { // nquad == 3
gp1d[0] = 0.5 - 0.5*mju_sqrt(0.6); // ~ 0.113
gp1d[1] = 0.5; // 0.5
gp1d[2] = 0.5 + 0.5*mju_sqrt(0.6); // ~ 0.887
}
// build 3D Gauss points array (max 27 points)
mjtNum gauss[27][3];
for (int gi = 0; gi < nquad; gi++) {
for (int gj = 0; gj < nquad; gj++) {
for (int gk = 0; gk < nquad; gk++) {
int idx = gi*nquad*nquad + gj*nquad + gk;
gauss[idx][0] = gp1d[gi];
gauss[idx][1] = gp1d[gj];
gauss[idx][2] = gp1d[gk];
}
}
}
// loop over Gauss points
// get reference positions from m->flex_node0 (Cartesian positions at qpos0)
mjtNum* refpos = mjSTACKALLOC(d, 3*nodenum, mjtNum);
for (int n = 0; n < nodenum; n++) {
mju_copy3(refpos + 3*n, m->flex_node0 + 3*(n + nstart));
}
for (int g = 0; g < ngauss; g++) {
mjtNum* p = gauss[g];
// compute current position gradient dxcur/dxi
mjtNum Fcur[9];
mju_defGradient(Fcur, p, xpos, order);
// compute reference position gradient dxref/dxi
mjtNum Fref[9];
mju_defGradient(Fref, p, refpos, order);
// compute true deformation gradient F = Fcur * inv(Fref)
// F maps from reference to current configuration
mjtNum Fref_inv[9];
mju_transpose(Fref_inv, Fref, 3, 3); // start with transpose for cofactor
mjtNum det = Fref[0]*(Fref[4]*Fref[8] - Fref[5]*Fref[7]) -
Fref[1]*(Fref[3]*Fref[8] - Fref[5]*Fref[6]) +
Fref[2]*(Fref[3]*Fref[7] - Fref[4]*Fref[6]);
// compute adjugate (cofactor transposed)
Fref_inv[0] = (Fref[4]*Fref[8] - Fref[5]*Fref[7]) / det;
Fref_inv[1] = -(Fref[1]*Fref[8] - Fref[2]*Fref[7]) / det;
Fref_inv[2] = (Fref[1]*Fref[5] - Fref[2]*Fref[4]) / det;
Fref_inv[3] = -(Fref[3]*Fref[8] - Fref[5]*Fref[6]) / det;
Fref_inv[4] = (Fref[0]*Fref[8] - Fref[2]*Fref[6]) / det;
Fref_inv[5] = -(Fref[0]*Fref[5] - Fref[2]*Fref[3]) / det;
Fref_inv[6] = (Fref[3]*Fref[7] - Fref[4]*Fref[6]) / det;
Fref_inv[7] = -(Fref[0]*Fref[7] - Fref[1]*Fref[6]) / det;
Fref_inv[8] = (Fref[0]*Fref[4] - Fref[1]*Fref[3]) / det;
// F = Fcur * Fref_inv
mjtNum F[9];
mju_mulMatMat(F, Fcur, Fref_inv, 3, 3, 3);
// compute C = F'*F (right Cauchy-Green tensor)
mjtNum C[9];
mju_mulMatTMat3(C, F, F);
// compute Green-Lagrange strain E = 0.5*(C - I)
mjtNum E[9];
for (int j = 0; j < 9; j++) {
E[j] = 0.5 * C[j];
}
E[0] -= 0.5;
E[4] -= 0.5;
E[8] -= 0.5;
// compute 3 invariants of E
// I1 = tr(E) = E[0] + E[4] + E[8]
mjtNum I1 = E[0] + E[4] + E[8];
// I2 = 0.5*(tr(E)^2 - tr(E^2))
mjtNum trE2 = E[0]*E[0] + E[1]*E[3] + E[2]*E[6] +
E[3]*E[1] + E[4]*E[4] + E[5]*E[7] +
E[6]*E[2] + E[7]*E[5] + E[8]*E[8];
mjtNum I2 = 0.5 * (I1*I1 - trE2);
// I3 = det(E)
mjtNum I3 = E[0]*(E[4]*E[8] - E[5]*E[7]) -
E[1]*(E[3]*E[8] - E[5]*E[6]) +
E[2]*(E[3]*E[7] - E[4]*E[6]);
// compute shape function gradients at this Gauss point
// grad[n][k] = d(basis_n)/d(p_k), for n in [0, nodenum), k in [0,3)
// indexing matches mju_defGradient: idx = i*(order+1)^2 + j*(order+1) + k
mjtNum grad[27][3]; // max 27 nodes for order=2
int npoint = (order + 1) * (order + 1) * (order + 1);
for (int n = 0; n < npoint; n++) {
int stride = order + 1;
int ix = n / (stride * stride);
int iy = (n / stride) % stride;
int iz = n % stride;
mjtNum phi_x, phi_y, phi_z, dphi_x, dphi_y, dphi_z;
basis(order, ix, p[0], &phi_x, &dphi_x);
basis(order, iy, p[1], &phi_y, &dphi_y);
basis(order, iz, p[2], &phi_z, &dphi_z);
grad[n][0] = dphi_x * phi_y * phi_z;
grad[n][1] = phi_x * dphi_y * phi_z;
grad[n][2] = phi_x * phi_y * dphi_z;
}
// compute Jacobian for each invariant
mjtNum invariants[3] = {I1, I2, I3};
for (int inv = 0; inv < 3; inv++) {
cpos[0] = invariants[inv];
// compute dI/dE (3x3 symmetric, stored as 9)
mjtNum dIdE[9];
if (inv == 0) {
// dI1/dE = I (identity)
mju_zero(dIdE, 9);
dIdE[0] = dIdE[4] = dIdE[8] = 1.0;
} else if (inv == 1) {
// dI2/dE = tr(E)*I - E
mju_zero(dIdE, 9);
dIdE[0] = I1 - E[0];
dIdE[4] = I1 - E[4];
dIdE[8] = I1 - E[8];
dIdE[1] = -E[1]; dIdE[3] = -E[3];
dIdE[2] = -E[2]; dIdE[6] = -E[6];
dIdE[5] = -E[5]; dIdE[7] = -E[7];
} else {
// dI3/dE = cofactor(E) = det(E) * E^{-T} for invertible E
// For small strain, use adjugate directly
dIdE[0] = E[4]*E[8] - E[5]*E[7];
dIdE[1] = -(E[1]*E[8] - E[2]*E[7]);
dIdE[2] = E[1]*E[5] - E[2]*E[4];
dIdE[3] = -(E[3]*E[8] - E[5]*E[6]);
dIdE[4] = E[0]*E[8] - E[2]*E[6];
dIdE[5] = -(E[0]*E[5] - E[2]*E[3]);
dIdE[6] = E[3]*E[7] - E[4]*E[6];
dIdE[7] = -(E[0]*E[7] - E[1]*E[6]);
dIdE[8] = E[0]*E[4] - E[1]*E[3];
}
// dI/dx_n = sum over i,j: dI/dE_ij * dE_ij/dx_n
// where dE_ij/dx_n = 0.5 * d(F'F)_ij/dx_n
// d(F'F)_ij/dx_n = sum_k (dF_ki/dx_n * F_kj + F_ki * dF_kj/dx_n)
//
// With F = Fcur * Fref_inv:
// dF_ab/d(x_n)_c = sum_k (dFcur_ak/d(x_n)_c) * Fref_inv[kb]
// = sum_k delta_{ac} * grad[n][k] * Fref_inv[kb]
// So only row a=c contributes
// compute dI/dx for all nodes (3*nodenum values)
mjtNum* dIdx = mjSTACKALLOC(d, 3*nodenum, mjtNum);
mju_zero(dIdx, 3*nodenum);
for (int n = 0; n < nodenum; n++) {
for (int c = 0; c < 3; c++) { // component of x_n
mjtNum dI = 0;
for (int ij = 0; ij < 9; ij++) {
int ii = ij / 3; // row of E/C
int jj = ij % 3; // col of E/C
// dF_ab/d(x_n)_c = sum_k grad[n][k] * Fref_inv[kb] (only for a=c)
// dC_ij/d(x_n)_c = dF_ci * F_cj + F_ci * dF_cj
// = (sum_k grad[n][k]*Fref_inv[ki]) * F_cj
// + F_ci * (sum_k grad[n][k]*Fref_inv[kj])
// compute dF_ci = sum_k grad[n][k] * Fref_inv[ki]
mjtNum dF_ci = 0;
for (int k = 0; k < 3; k++) {
dF_ci += grad[n][k] * Fref_inv[k*3 + ii];
}
// compute dF_cj = sum_k grad[n][k] * Fref_inv[kj]
mjtNum dF_cj = 0;
for (int k = 0; k < 3; k++) {
dF_cj += grad[n][k] * Fref_inv[k*3 + jj];
}
mjtNum dC_ij = dF_ci * F[c*3 + jj] + F[c*3 + ii] * dF_cj;
// dE_ij = 0.5 * dC_ij
dI += dIdE[ij] * 0.5 * dC_ij;
}
dIdx[3*n + c] = dI;
}
}
// strain_jac[q] = sum_n sum_c dIdx[3*n+c] * node_jac[(3*n+c)*nv + q]
mju_zero(strain_jac, nv);
for (int n = 0; n < nodenum; n++) {
for (int c = 0; c < 3; c++) {
int row = 3*n + c;
for (int q = 0; q < nv; q++) {
strain_jac[q] += dIdx[row] * node_jac[row*nv + q];
}
}
}
// add constraint: sparse or dense
if (issparse) {
// convert dense strain_jac to sparse format
mjtNum* sparse_jac = mjSTACKALLOC(d, combined_nnz, mjtNum);
for (int k = 0; k < combined_nnz; k++) {
sparse_jac[k] = strain_jac[combined_chain[k]];
}
mj_addConstraint(m, d, sparse_jac, cpos, 0, 0, 1, mjCNSTR_EQUALITY, i,
combined_nnz, combined_chain);
} else {
mj_addConstraint(m, d, strain_jac, cpos, 0, 0, 1, mjCNSTR_EQUALITY, i, 0, NULL);
}
}
}
mj_freeStack(d);
break;
}
case mjEQ_FLEX:
// edge constraint mode: add one constraint per non-rigid edge
flex_edgeadr = m->flex_edgeadr[id[0]];
flex_edgenum = m->flex_edgenum[id[0]];
for (int e=flex_edgeadr; e < flex_edgeadr+flex_edgenum; e++) {
// skip rigid
if (m->flexedge_rigid[e]) {
continue;
}
// position error
cpos[0] = d->flexedge_length[e] - m->flexedge_length0[e];
// add constraint: sparse or dense
if (issparse) {
mj_addConstraint(m, d, d->flexedge_J+m->flexedge_J_rowadr[e], cpos, 0, 0,
1, mjCNSTR_EQUALITY, i,
m->flexedge_J_rownnz[e],
m->flexedge_J_colind+m->flexedge_J_rowadr[e]);
} else {
mju_zero(jac[0], nv); // reuse first row of jac[0]
int rowadr = m->flexedge_J_rowadr[e];
int rownnz = m->flexedge_J_rownnz[e];
for (int k=0; k<rownnz; k++) {
jac[0][m->flexedge_J_colind[rowadr+k]] = d->flexedge_J[rowadr+k];
}
mj_addConstraint(m, d, jac[0], cpos, 0, 0, 1, mjCNSTR_EQUALITY, i, 0, NULL);
}
}
break;
case mjEQ_FLEXVERT:
// add two constraints per vertex
@@ -1186,6 +1506,26 @@ void mj_diagApprox(const mjModel* m, mjData* d) {
i--;
break;
case mjEQ_FLEXSTRAIN: {
// strain constraints: 24 constraints, use average node inv weight
int flex_id = m->eq_obj1id[id];
int nodenum = m->flex_nodenum[flex_id];
int nstart = m->flex_nodeadr[flex_id];
mjtNum avg_invweight = 0;
for (int n = 0; n < nodenum; n++) {
int bodyid = m->flex_nodebodyid[nstart + n];
avg_invweight += m->body_invweight0[2*bodyid];
}
avg_invweight /= nodenum;
for (int c = 0; c < 24; c++) {
dA[i++] = avg_invweight;
}
// adjust constraint counter
i--;
break;
}
default:
mjERROR("unknown constraint type %d", d->efc_type[i]); // SHOULD NOT OCCUR
}
@@ -1788,6 +2128,31 @@ static int mj_ne(const mjModel* m, mjData* d, int* nnz) {
}
break;
case mjEQ_FLEXSTRAIN: {
// strain constraints: 3 invariants × ngauss Gauss points
// skip if not interpolated (order == 0 or no nodes)
int order = m->flex_interp[id[0]];
int nodenum = m->flex_nodenum[id[0]];
if (!order || !nodenum) {
break;
}
int nquad = order + 1; // 2 for order=1, 3 for order=2
int ngauss = nquad * nquad * nquad; // 8 or 27
size = 3 * ngauss; // 24 or 81
if (nnz) {
// NV is sum of all node Jacobians
int nstart = m->flex_nodeadr[id[0]];
for (int n = 0; n < nodenum; n++) {
int bodyid = m->flex_nodebodyid[nstart + n];
NV += mj_bodyChain(m, bodyid, chain);
}
// each constraint row shares this NV
NV = size * NV;
}
break;
}
default:
// might occur in case of the now-removed distance equality constraint
mjERROR("unknown constraint type %d", m->eq_type[i]); // SHOULD NOT OCCUR
@@ -1795,7 +2160,8 @@ static int mj_ne(const mjModel* m, mjData* d, int* nnz) {
// accumulate counts; flex NV already accumulated
ne += mj_addConstraintCount(m, size, NV);
if (m->eq_type[i] == mjEQ_FLEX || m->eq_type[i] == mjEQ_FLEXVERT) {
if (m->eq_type[i] == mjEQ_FLEX || m->eq_type[i] == mjEQ_FLEXVERT ||
m->eq_type[i] == mjEQ_FLEXSTRAIN) {
nnze += NV;
} else {
nnze += size*NV;
+13
View File
@@ -2573,6 +2573,19 @@ void mj_rnePostConstraint(const mjModel* m, mjData* d) {
i += 2*m->flex_vertnum[k];
break;
case mjEQ_FLEXSTRAIN: {
// increment with 3 invariants × ngauss Gauss points
k = m->eq_obj1id[id];
int order = m->flex_interp[k];
int nodenum = m->flex_nodenum[k];
if (order && nodenum) {
int nquad = order + 1;
int ngauss = nquad * nquad * nquad;
i += 3 * ngauss;
}
break;
}
default:
mjERROR("unknown constraint type type %d", m->eq_type[id]); // SHOULD NOT OCCUR
}
+1
View File
@@ -1958,6 +1958,7 @@ const char* mj_validateReferences(const mjModel* m) {
case mjEQ_FLEX:
case mjEQ_FLEXVERT:
case mjEQ_FLEXSTRAIN:
if (obj1id >= m->nflex || obj1id < 0) {
return "Invalid model: eq_obj1id out of bounds.";
}
+8 -1
View File
@@ -644,7 +644,14 @@ bool mjCFlexcomp::Make(mjsBody* body, char* error, int error_sz, const mjVFS* vf
if (equality) {
mjsEquality* pe = mjs_addEquality(&model->spec, &def.spec);
mjs_setDefault(pe->element, &model->Default()->spec);
pe->type = equality == 1 ? mjEQ_FLEX : mjEQ_FLEXVERT;
// equality 1=edge(mjEQ_FLEX), 2=vert(mjEQ_FLEXVERT), 3=strain(mjEQ_FLEXSTRAIN)
if (equality == 1) {
pe->type = mjEQ_FLEX;
} else if (equality == 2) {
pe->type = mjEQ_FLEXVERT;
} else if (equality == 3) {
pe->type = mjEQ_FLEXSTRAIN;
}
pe->active = true;
mjs_setString(pe->name1, name.c_str());
}
+1 -1
View File
@@ -83,7 +83,7 @@ class mjCFlexcomp {
double origin[3]; // origin for generating a 3D mesh from a convex 2D mesh
double mass; // total mass of auto-generated bodies
double inertiabox; // size of inertia box for each body
int equality; // create equality constraint, 0:none, 1:edge, 2:vert
int equality; // create equality constraint, 0:none, 1:edge, 2:vert, 3:strain
std::string file; // mesh/gmsh file name
mjtDof doftype; // dof type, all vertices or trilinear interpolation
+1
View File
@@ -3876,6 +3876,7 @@ void mjCFlex::PointToLocal() {
void mjCFlex::NameSpace(const mjCModel* m) {
mjCBase::NameSpace(m);
for (auto& name : spec_vertbody_) {
name = m->prefix + name + m->suffix;
}
+4
View File
@@ -3533,6 +3533,10 @@ void mjCModel::CopyObjects(mjModel* m) {
m->flex_edgeequality[i] = 2;
break;
}
if (equalities_[k]->type == mjEQ_FLEXSTRAIN) {
m->flex_edgeequality[i] = 3;
break;
}
}
}
+3 -2
View File
@@ -6292,7 +6292,7 @@ void mjCEquality::ResolveReferences(const mjCModel* m) {
object_type = mjOBJ_JOINT;
} else if (type == mjEQ_TENDON) {
object_type = mjOBJ_TENDON;
} else if (type == mjEQ_FLEX || type == mjEQ_FLEXVERT) {
} else if (type == mjEQ_FLEX || type == mjEQ_FLEXVERT || type == mjEQ_FLEXSTRAIN) {
object_type = mjOBJ_FLEX;
} else {
throw mjCError(this, "invalid type in equality constraint");
@@ -6349,7 +6349,8 @@ void mjCEquality::Compile(void) {
ResolveReferences(model);
// make sure flex is not rigid
if ((type == mjEQ_FLEX || type == mjEQ_FLEXVERT) && model->Flexes()[obj1id]->rigid) {
if ((type == mjEQ_FLEX || type == mjEQ_FLEXVERT || type == mjEQ_FLEXSTRAIN) &&
model->Flexes()[obj1id]->rigid) {
throw mjCError(this, "rigid flex '%s' in equality constraint %d", name1_.c_str(), id);
}
}
+11 -3
View File
@@ -366,6 +366,8 @@ std::vector<const char*> MJCF[nMJCF] = {
"active", "solref", "solimp"},
{"flexvert", "*", "name", "class", "flex",
"active", "solref", "solimp"},
{"flexstrain", "*", "name", "class", "flex",
"active", "solref", "solimp"},
{">"},
{"tendon", "*"},
@@ -669,7 +671,7 @@ const mjMap solver_map[solver_sz] = {
// constraint type
const int equality_sz = 7;
const int equality_sz = 8;
const mjMap equality_map[equality_sz] = {
{"connect", mjEQ_CONNECT},
{"weld", mjEQ_WELD},
@@ -677,6 +679,7 @@ const mjMap equality_map[equality_sz] = {
{"tendon", mjEQ_TENDON},
{"flex", mjEQ_FLEX},
{"flexvert", mjEQ_FLEXVERT},
{"flexstrain", mjEQ_FLEXSTRAIN},
{"distance", mjEQ_DISTANCE}
};
@@ -930,10 +933,11 @@ const mjMap elastic2d_map[5] = {
// flex equality type
const mjMap flexeq_map[3] = {
const mjMap flexeq_map[4] = {
{"false", 0},
{"true", 1},
{"vert", 2},
{"strain", 3},
};
@@ -2207,6 +2211,7 @@ void mjXReader::OneEquality(XMLElement* elem, mjsEquality* equality) {
case mjEQ_FLEX:
case mjEQ_FLEXVERT:
case mjEQ_FLEXSTRAIN:
ReadAttrTxt(elem, "flex", name1, true);
break;
@@ -2768,7 +2773,7 @@ void mjXReader::OneFlexcomp(XMLElement* elem, mjsBody* body, const mjVFS* vfs) {
// edge
XMLElement* edge = FirstChildElement(elem, "edge");
if (edge) {
MapValue(edge, "equality", &fcomp.equality, flexeq_map, 3);
MapValue(edge, "equality", &fcomp.equality, flexeq_map, 4);
ReadAttr(edge, "solref", mjNREF, fcomp.def.spec.equality->solref, text, false, false);
ReadAttr(edge, "solimp", mjNIMP, fcomp.def.spec.equality->solimp, text, false, false);
ReadAttr(edge, "stiffness", 1, &dflex.edgestiffness, text);
@@ -2789,6 +2794,9 @@ void mjXReader::OneFlexcomp(XMLElement* elem, mjsBody* body, const mjVFS* vfs) {
if (dflex.elastic2d >= 2 && fcomp.equality) {
throw mjXError(elem, "elasticity and edge constraints cannot both be present");
}
if (fcomp.equality == 3 && dflex.young > 0) {
throw mjXError(elem, "strain constraint and elasticity (young) cannot both be present");
}
// contact
XMLElement* cont = FirstChildElement(elem, "contact");
+1 -1
View File
@@ -102,7 +102,7 @@ class mjXReader : public mjXBase {
};
// MJCF schema
#define nMJCF 245
#define nMJCF 246
extern std::vector<const char*> MJCF[nMJCF];
#endif // MUJOCO_SRC_XML_XML_NATIVE_READER_H_
+1
View File
@@ -711,6 +711,7 @@ void mjXWriter::OneEquality(XMLElement* elem, const mjCEquality* equality, mjCDe
case mjEQ_FLEX:
case mjEQ_FLEXVERT:
case mjEQ_FLEXSTRAIN:
WriteAttrTxt(elem, "flex", mjs_getString(equality->name1));
break;
+254
View File
@@ -14,6 +14,7 @@
// Tests for engine/engine_core_constraint.c.
#include <array>
#include <cstddef>
#include <cstring>
#include <string>
@@ -489,5 +490,258 @@ TEST_F(CoreConstraintTest, FlexvertEquality) {
mj_deleteModel(model);
}
// Test flex strain constraint with pinned nodes attached to freejoint parent
TEST_F(CoreConstraintTest, BoxShellPinnedParentWithFreejoint) {
static constexpr char xml[] = R"(
<mujoco>
<option integrator="implicitfast" jacobian="dense" gravity="0 0 0"/>
<worldbody>
<geom type="plane" size="10 10 1" pos="0 0 -.1"/>
<body>
<joint type="free"/>
<geom type="box" size="0.13 0.18 0.036"/>
<body name="parent">
<flexcomp name="test" type="box"
spacing=".1 .02 .1" radius="0.001"
pos="0 0 .2" dof="trilinear" xyaxes="0 1 0 0 0 1" mass="1" dim="3">
<contact selfcollide="none"/>
<edge equality="strain"/>
<pin id="0 2 4 6"/>
</flexcomp>
</body>
</body>
</worldbody>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* m = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(m, NotNull()) << error.data();
mjData* d = mj_makeData(m);
mj_resetData(m, d);
mj_forward(m, d);
// Check that we have constraints
EXPECT_GT(d->nefc, 0) << "No constraints generated";
EXPECT_GT(d->ne, 0) << "Expected some strain constraints";
// Check qacc and forces at rest with gravity=0
EXPECT_NEAR(d->qacc_smooth[6], 0, 1e-6) << "qacc_smooth should be 0 at rest";
EXPECT_NEAR(d->qacc[6], 0, 1e-6) << "qacc should be 0 at rest";
// Check initial constraint values (efc_pos)
bool has_bad_constraint = false;
for (int i = 0; i < d->nefc; i++) {
if (d->efc_type[i] == mjCNSTR_EQUALITY) {
if (mju_abs(d->efc_pos[i]) > 1.0) {
has_bad_constraint = true;
}
}
}
EXPECT_FALSE(has_bad_constraint)
<< "Some constraint values are too large at rest";
// Check Jacobian values - look for NaN or huge values
int nv = m->nv;
bool has_bad_jacobian = false;
for (int i = 0; i < d->nefc; i++) {
if (d->efc_type[i] == mjCNSTR_EQUALITY) {
for (int j = 0; j < nv; j++) {
mjtNum val = d->efc_J[i*nv + j];
if (mju_isBad(val) || mju_abs(val) > 1e10) {
has_bad_jacobian = true;
}
}
}
}
EXPECT_FALSE(has_bad_jacobian) << "Jacobian contains NaN or huge values";
// Verify Jacobian with finite differences for first few constraints
mjtNum eps = 1e-6;
std::vector<mjtNum> qpos0(m->nq);
mju_copy(qpos0.data(), d->qpos, m->nq);
// Store original constraint values
std::vector<mjtNum> efc_pos0(d->nefc);
mju_copy(efc_pos0.data(), d->efc_pos, d->nefc);
int num_constraints_to_check = mju_min(3, d->ne);
bool has_jacobian_mismatch = false;
for (int j = 0; j < nv && j < 6; j++) {
mju_copy(d->qpos, qpos0.data(), m->nq);
mjtNum dqpos[100] = {0};
dqpos[j] = eps;
mj_integratePos(m, d->qpos, dqpos, 1);
mj_forward(m, d);
for (int i = 0; i < num_constraints_to_check; i++) {
mjtNum fd = (d->efc_pos[i] - efc_pos0[i]) / eps;
mjtNum analytic = d->efc_J[i*nv + j];
// Use relative tolerance with absolute floor to handle near-zero values
mjtNum tol = mju_max(1e-8, 0.1 * (mju_abs(fd) + mju_abs(analytic)));
if (mju_abs(fd - analytic) > tol) {
has_jacobian_mismatch = true;
}
}
}
EXPECT_FALSE(has_jacobian_mismatch)
<< "Jacobian FD mismatch at initial config";
// Test rotation invariance: rotate via freejoint quaternion
mju_copy(d->qpos, qpos0.data(), m->nq);
mjtNum angle = 0.785398; // 45 degrees
d->qpos[3] = mju_cos(angle/2); // w
d->qpos[4] = 0;
d->qpos[5] = 0;
d->qpos[6] = mju_sin(angle/2); // z
mj_forward(m, d);
mjtNum max_strain_rotated = 0;
for (int i = 0; i < d->ne; i++) {
if (mju_abs(d->efc_pos[i]) > max_strain_rotated) {
max_strain_rotated = mju_abs(d->efc_pos[i]);
}
}
EXPECT_LT(max_strain_rotated, 1e-6)
<< "Strain should remain ~0 after rigid rotation";
// Check Jacobian in rotated configuration via FD
std::vector<mjtNum> qpos_rot(m->nq);
mju_copy(qpos_rot.data(), d->qpos, m->nq);
std::vector<mjtNum> efc_pos_rot(d->nefc);
mju_copy(efc_pos_rot.data(), d->efc_pos, d->nefc);
bool has_rotated_jacobian_mismatch = false;
for (int j = 0; j < nv; j++) {
mju_copy(d->qpos, qpos_rot.data(), m->nq);
mjtNum dqpos[100] = {0};
dqpos[j] = eps;
mj_integratePos(m, d->qpos, dqpos, 1);
mj_forward(m, d);
mjtNum fd = (d->efc_pos[0] - efc_pos_rot[0]) / eps;
mjtNum analytic = d->efc_J[0*nv + j];
mjtNum tol = 0.1 * (mju_abs(fd) + mju_abs(analytic) + 1e-8);
if ((mju_abs(fd) > 1e-8 || mju_abs(analytic) > 1e-8) &&
mju_abs(fd - analytic) > tol) {
has_rotated_jacobian_mismatch = true;
}
}
EXPECT_FALSE(has_rotated_jacobian_mismatch)
<< "Jacobian FD mismatch in rotated config";
// Reset for simulation
mju_copy(d->qpos, qpos0.data(), m->nq);
mj_forward(m, d);
// Run simulation only if checks pass
if (!has_bad_constraint && !has_bad_jacobian) {
for (int i = 0; i < 2000; i++) {
mj_step(m, d);
ASSERT_FALSE(mju_isBad(d->qpos[0]))
<< "Simulation became unstable at step " << i;
ASSERT_FALSE(mju_isBad(d->qvel[0]))
<< "Velocity became unstable at step " << i;
for (int j = 0; j < m->nv; j++) {
ASSERT_LT(mju_abs(d->qvel[j]), 1000.0)
<< "Velocity exploded at step " << i << ", qvel[" << j
<< "]=" << d->qvel[j];
}
}
}
mj_deleteData(d);
mj_deleteModel(m);
}
// Test flex strain constraint WITHOUT pinned nodes (simpler case)
TEST_F(CoreConstraintTest, StrainConstraintNoPinning) {
static constexpr char xml[] = R"(
<mujoco>
<option integrator="implicitfast" jacobian="dense"/>
<worldbody>
<body name="parent">
<joint type="free"/>
<geom type="box" size=".01 .01 .01" mass=".1"/>
<flexcomp name="test" type="box"
spacing=".1 .1 .1" radius="0.001"
pos="0 0 .5" dof="trilinear" mass="1" dim="3">
<contact selfcollide="none"/>
<edge equality="strain"/>
</flexcomp>
</body>
</worldbody>
</mujoco>
)";
std::array<char, 1024> error;
mjModel* m = LoadModelFromString(xml, error.data(), error.size());
ASSERT_THAT(m, NotNull()) << error.data();
mjData* d = mj_makeData(m);
mj_resetData(m, d);
mj_forward(m, d);
// Check constraints
EXPECT_GT(d->ne, 0) << "Expected strain constraints";
// Check no contacts
EXPECT_EQ(d->ncon, 0);
// Check that initial strain is ~0
mjtNum max_pos = 0;
for (int i = 0; i < d->ne; i++) {
if (mju_abs(d->efc_pos[i]) > max_pos) {
max_pos = mju_abs(d->efc_pos[i]);
}
}
EXPECT_LT(max_pos, 1e-6) << "Initial strain should be ~0";
// Check Jacobian for NaN
int nv = m->nv;
bool has_bad_jacobian = false;
for (int i = 0; i < d->ne; i++) {
for (int j = 0; j < nv; j++) {
if (mju_isBad(d->efc_J[i*nv + j])) {
has_bad_jacobian = true;
}
}
}
EXPECT_FALSE(has_bad_jacobian) << "Jacobian has NaN";
// Test rigid rotation: rotate flex and check strain still ~0
std::vector<mjtNum> qpos0(m->nq);
mju_copy(qpos0.data(), d->qpos, m->nq);
// Rotate by 45 degrees around Z axis via quaternion
mjtNum angle = 0.785398; // 45 degrees
d->qpos[3] = mju_cos(angle/2); // w
d->qpos[4] = 0; // x
d->qpos[5] = 0; // y
d->qpos[6] = mju_sin(angle/2); // z
mj_forward(m, d);
mjtNum max_strain_rotated = 0;
for (int i = 0; i < d->ne; i++) {
if (mju_abs(d->efc_pos[i]) > max_strain_rotated) {
max_strain_rotated = mju_abs(d->efc_pos[i]);
}
}
EXPECT_LT(max_strain_rotated, 1e-6)
<< "Strain should remain ~0 after rigid rotation";
// Run simulation for a few steps to check stability
mju_copy(d->qpos, qpos0.data(), m->nq);
mj_forward(m, d);
for (int i = 0; i < 100; i++) {
mj_step(m, d);
ASSERT_FALSE(mju_isBad(d->qpos[0])) << "Simulation unstable at step " << i;
}
mj_deleteData(d);
mj_deleteModel(m);
}
} // namespace
} // namespace mujoco
+68 -1
View File
@@ -15,7 +15,7 @@
// Tests for user/user_model.cc.
#include <array>
#include <cstdio>
#include <memory>
#include <string>
#include <gmock/gmock.h>
@@ -919,5 +919,72 @@ TEST_F(UserFlexTest, FlexcompMeshLoadsFromVFS) {
mj_deleteVFS(&vfs);
}
// Test that flex constraints are preserved when attaching a model
TEST_F(UserFlexTest, FlexAttachConstraintPreserved) {
// Child model with flex and strain constraint
static constexpr char flex_xml[] = R"(
<mujoco>
<worldbody>
<body name="flex_parent">
<flexcomp name="test" type="box"
spacing=".1 .1 .1" radius="0.001"
dof="trilinear" mass="1" dim="3">
<contact selfcollide="none"/>
<edge equality="strain"/>
</flexcomp>
</body>
</worldbody>
</mujoco>
)";
// Parent model that attaches the flex model
static constexpr char parent_xml[] = R"(
<mujoco>
<asset>
<model name="flex" file="flex.xml"/>
</asset>
<worldbody>
<frame pos="0 0 0.3">
<attach model="flex" prefix="flex_"/>
</frame>
</worldbody>
</mujoco>
)";
// Set up VFS with both XML files
auto vfs = std::make_unique<mjVFS>();
mj_defaultVFS(vfs.get());
mj_addBufferVFS(vfs.get(), "flex.xml", flex_xml, sizeof(flex_xml));
// First verify the standalone flex model has constraints
std::array<char, 1024> error;
mjModel* m_standalone =
LoadModelFromString(flex_xml, error.data(), error.size(), vfs.get());
ASSERT_THAT(m_standalone, NotNull()) << error.data();
mjData* d_standalone = mj_makeData(m_standalone);
mj_forward(m_standalone, d_standalone);
int standalone_neq = m_standalone->neq;
EXPECT_GT(standalone_neq, 0) << "Standalone flex should have constraints";
mj_deleteData(d_standalone);
mj_deleteModel(m_standalone);
// Now load the parent model which attaches the flex
mjModel* m_attached =
LoadModelFromString(parent_xml, error.data(), error.size(), vfs.get());
ASSERT_THAT(m_attached, NotNull()) << error.data();
mjData* d_attached = mj_makeData(m_attached);
mj_forward(m_attached, d_attached);
// THE BUG: flex constraints disappear when attached
EXPECT_GT(m_attached->neq, 0)
<< "Attached flex should preserve strain constraints";
EXPECT_EQ(m_attached->neq, standalone_neq)
<< "Attached flex should have same number of constraints as standalone";
mj_deleteData(d_attached);
mj_deleteModel(m_attached);
mj_deleteVFS(vfs.get());
}
} // namespace
} // namespace mujoco
+2 -1
View File
@@ -288,7 +288,8 @@ public enum mjtEq : int{
mjEQ_TENDON = 3,
mjEQ_FLEX = 4,
mjEQ_FLEXVERT = 5,
mjEQ_DISTANCE = 6,
mjEQ_FLEXSTRAIN = 6,
mjEQ_DISTANCE = 7,
}
public enum mjtWrap : int{
mjWRAP_NONE = 0,
+1
View File
@@ -10868,6 +10868,7 @@ EMSCRIPTEN_BINDINGS(mujoco_bindings) {
.value("mjEQ_TENDON", mjEQ_TENDON)
.value("mjEQ_FLEX", mjEQ_FLEX)
.value("mjEQ_FLEXVERT", mjEQ_FLEXVERT)
.value("mjEQ_FLEXSTRAIN", mjEQ_FLEXSTRAIN)
.value("mjEQ_DISTANCE", mjEQ_DISTANCE);
enum_<mjtEvent>("mjtEvent")
.value("mjEVENT_NONE", mjEVENT_NONE)