Add mjs_makeFlex to the MuJoCo C and Python APIs.

- Add mjs_makeFlex C API that wraps mjCFlexcomp::Make(), creating
  a flex with auto-generated bodies, joints, and equality constraints
  from parameters (type, dim, dof, count, cellcount, spacing, scale,
  radius, mass, inertiabox, equality, rigid, flatskin, elastic2d,
  pos, quat, origin, file, vfs).
- Add Python binding body.make_flex() in specs.cc with keyword args.
- Refactor flex_spec_provision.py to use make_flex() instead of XML
  string templating + separate spec + attach. Eliminates textwrap
  dependency.
- Merge user_flexcomp into user_model BUILD target to resolve
  circular dependency.

PiperOrigin-RevId: 922725304
Change-Id: Ice13787c45f3173407fec659f9590a6453bc65dd
This commit is contained in:
Alessio Quaglino
2026-05-28 05:58:47 -07:00
committed by Copybara-Service
parent b9fb817af8
commit 4c381635e1
11 changed files with 506 additions and 2 deletions
+70
View File
@@ -2032,5 +2032,75 @@ class SpecsTest(absltest.TestCase):
with self.assertRaises(mujoco.FatalError):
spec.encode(filename, model)
def test_make_flex_grid(self):
# Create a spec with a flexcomp grid.
spec = mujoco.MjSpec()
body = spec.worldbody.add_body(name='flex_body')
flex = body.make_flex(
name='test_flex',
type='grid',
dim=3,
count=[4, 4, 4],
spacing=[0.05, 0.05, 0.05],
mass=0.5,
equality=1,
)
self.assertIsNotNone(flex)
model = spec.compile()
self.assertIsNotNone(model)
self.assertGreater(model.nflex, 0)
# Verify elastic2d is forwarded to Make() and produces shell-mode
# strain constraints (equality=3 + elastic2d=2 triggers shell path).
spec2 = mujoco.MjSpec()
body2 = spec2.worldbody.add_body(name='shell_body')
flex2 = body2.make_flex(
name='shell_flex',
type='grid',
dim=3,
count=[3, 3, 3],
spacing=[0.1, 0.1, 0.1],
dof='trilinear',
cellcount=[2, 2, 1],
mass=0.5,
equality=3, # strain
elastic2d=2, # bend
)
flex2.young = 1e3
flex2.thickness = 0.01
flex2.selfcollide = mujoco.mjtFlexSelf.mjFLEXSELF_NONE
self.assertIsNotNone(flex2)
model2 = spec2.compile()
self.assertIsNotNone(model2)
# Shell mode creates face-based constraints; verify they exist.
self.assertGreater(model2.neq, 0)
def test_make_flex_defaults(self):
# Create a spec with minimal flexcomp args (defaults).
spec = mujoco.MjSpec()
body = spec.worldbody.add_body(name='flex_body')
flex = body.make_flex(name='default_flex', equality=1)
self.assertIsNotNone(flex)
model = spec.compile()
self.assertIsNotNone(model)
def test_make_flex_with_pos_quat(self):
# Create a spec with flexcomp that has a pose.
spec = mujoco.MjSpec()
body = spec.worldbody.add_body(name='flex_body')
flex = body.make_flex(
name='posed_flex',
type='grid',
dim=2,
count=[3, 3, 1],
spacing=[0.1, 0.1, 0.1],
pos=[1.0, 2.0, 3.0],
quat=[1.0, 0.0, 0.0, 0.0],
equality=1,
)
self.assertIsNotNone(flex)
model = spec.compile()
self.assertIsNotNone(model)
if __name__ == '__main__':
absltest.main()