Dynamic MuJoCo Model and Simulation


This page documents how the ArcFold inchworm-inspired foldable robot is converted from the Assignment 1 kinematic concept into a dynamic MuJoCo model with compliance, actuators, and performance tracking, as required for Project Assignment 2. The same model is used to study how gait timing (period) influences the achievable speed and robustness of the Sarrus‑linkage inchworm crawler, in parallel with experiments on the physical prototype.


1.1 Robot Overview and Objectives

The ArcFold robot is a planar Sarrus‑linkage inchworm crawler with two friction‑modulated pads and a central body that retracts and extends to realize a lift–drag gait. The simulated geometry is consistent with the laminated triangular body and two‑pad layout described in the Design section of the project.


1.2 Model Structure: Bodies, Joints, and Contacts

The MuJoCo model represents the ArcFold mechanism as a small set of rigid bodies connected by hinge joints and constrained into a Sarrus‑like spatial linkage. The key elements are:

A concise summary of the main model elements is given in Table 1.

Element typeName(s)Description
bodyfirst_left, first_right, second_left, second_rightCentral body segments
bodyfoot1Front pad body
bodyfoot2Rear pad body
jointmid_left, mid_rightMain arching hinges (compliant)
jointfoot1Front pad pivot
jointfoot2Rear pad pivot
bodyend_effectorCOM / performance tracking site
geomgroundGround plane

1.3 Compliance and Actuator Modeling

The dynamic model includes both structural compliance at the laminated flexure joints and simplified actuator dynamics for the servo‑driven joints.


1.4 Sensing and Performance Metric Definition

The MuJoCo model includes a dedicated body end_effector near the front of the robot. A small spherical geom is attached to this body and used purely for sensing and visualization.


1.5 MuJoCo XML Template

The MuJoCo model is instantiated from a Python string template. Placeholder fields such as {k_joint}, {b_joint}, {mu_tape}, {mu_base}, {mass}, {k_motor}, and {b_motor} are filled in at run time from experimentally identified parameters or design choices. This approach allows the same XML structure to be reused while sweeping over friction, compliance, actuator gains, or mass properties.

Structurally, the template defines:

The Python and XML template used in the notebook are shown below.


import os
import mujoco
import pandas
import numpy as np
import math
import mediapy as media
import matplotlib.pyplot as plt
import imageio_ffmpeg
from scipy import signal

# ---------------------------------------------------------------------------
# MuJoCo XML template for the ArcFold Sarrus-linkage inchworm crawler
#
# This template is filled from Python with:
#   mu_base : friction coefficient for low-friction base/structural contacts
#   mu_tape : friction coefficient for high-friction pads (anchor contacts)
#   mass    : link mass for each Sarrus linkage segment
#   k_joint : torsional stiffness of flexure joints (structural compliance)
#   b_joint : torsional damping of flexure joints
#   k_motor : position actuator proportional gain (servo stiffness)
#   b_motor : position actuator derivative gain (servo damping)
#
# Units:
#   length  -> meters
#   mass    -> kilograms
#   time    -> seconds
#   gravity -> m/s^2
#
# The model consists of:
#   - A bottom base body with an attached front pad (foot1)
#   - Left and right Sarrus-linkage chains that create the arching motion
#   - A rear base body with an attached rear pad (foot2)
#   - Weld constraints tying the left/right chains together (parallel mechanism)
#   - Position actuators driving the central hinge and both feet
# ---------------------------------------------------------------------------

xml_template = '''
<mujoco>
    <default>
        <!-- Simple lighting and camera defaults -->
        <light castshadow="false" diffuse="1 1 1"/>
        <camera fovy="45"/>
    </default>

    <!--
    Global simulation options:
      - RK4 integrator for improved stability
      - Small timestep for contact-rich inchworm dynamics
      - Standard Earth gravity along -z
    -->
    <!--<option><flag contact="disable"/></option>-->
    <option integrator="RK4"/>
    <option timestep="1e-4"/>
    <option gravity="0 0 -9.81"/>

    <worldbody>
        <!-- Overhead lights for even illumination along the crawl direction -->
        <light name="top"  pos="0 0 2"/>
        <light name="top1" pos="1 0 2"/>
        <light name="top2" pos="2 0 2"/>
        <light name="top3" pos="3 0 2"/>
        <light name="top4" pos="4 0 2"/>

        <!-- Camera views used for debug videos and qualitative inspection -->
        <camera name="triangle" pos="7 0 .75"  euler="90 90 0"/>
        <camera name="triangle2" pos="-2 0 .05" euler="90 -90 0"/>
        <camera name="iso"    pos="-2 -3 3"    euler="45 -45 -30"/>
        <camera name="iso2"   pos="10 -6 6"    euler="45 45 30"/>

        <!-- Flat ground plane; contype/conaffinity define contact bitmasks -->
        <geom name="ground" type="plane" pos="0 0 -.25" size="15 15 .05"
              rgba=".5 .5 .5 1" contype="2" conaffinity="1"/>

        <!--
        FRONT MODULE (base_bot):
          - Free-flying base box that carries the front pad (foot1)
          - Uses low friction (mu_base) when the body itself contacts the ground
        -->
        <body name="base_bot" pos="0 0 0">
            <!-- 6-DOF base joint (translation + rotation) -->
            <joint type="free"/>

            <!-- Front base body -->
            <geom type="box" size=".5 .5 .05" pos=".5 0 0"
                  rgba="1 0 0 .25" mass=".1"
                  contype="1" conaffinity="1"
                  friction="{mu_base} 0.0 0.0"/>

            <!-- Front pad (foot1) providing high-friction anchoring -->
            <body name="foot1" pos="0 0 0">
                 <geom type="box" size=".1 .25 .05" pos=".15 0 0"
                       rgba="0 1 0 1" mass=".1"
                       contype="1" conaffinity="1"
                       friction="{mu_tape} 0.0 0.0"/>
                 <!-- Hinge joint for front pad pitch (used by foot actuator) -->
                 <joint name="foot1" type="hinge" axis="1 0 0" pos=".25 0 0"/>
            </body>
        </body>

        <!--
        LEFT SARRUS CHAIN:
          - base_left, first_left, second_left, tail_left form one side of the Sarrus linkage
          - Each hinge has torsional stiffness/damping representing flexure joints
        -->
        <body name="base_left" pos="0 .25 .433" quat=".866 -.5 0 0">
            <joint type="free"/>

            <!-- Left front base segment -->
            <geom type="box" size=".5 .5 .05" pos=".5 0 0"
                  rgba="1 0 0 .15" mass="{mass}"
                  contype="1" conaffinity="1"/>

            <!-- First left link -->
            <body name="first_left" pos="1 0 0" axisangle="0 1 0 -45">
                <!-- Flexure hinge at first left joint -->
                <joint name="first_left" type="hinge" axis="0 1 0" pos="0 0 0"
                       range="-45 45" stiffness="{k_joint}" damping="{b_joint}"/>
                <geom type="box" size=".5 .5 .05" pos=".5 0 0"
                      rgba="1 1 1 .25" mass="{mass}"
                      contype="1" conaffinity="1"/>

                <!-- Second left link -->
                <body name="second_left" pos="1 0 0" axisangle="0 1 0 90">
                    <!-- Central Sarrus hinge on left side (arched motion) -->
                    <joint name="mid_left" type="hinge" axis="0 1 0" pos="0 0 0"
                           range="-90 90" stiffness="{k_joint}" damping="{b_joint}"/>
                    <geom type="box" size=".5 .5 .05" pos=".5 0 0"
                          rgba=".5 .5 .5 .25" mass="{mass}"
                          contype="1" conaffinity="1"/>

                    <!-- Tail left link -->
                    <body name="tail_left" pos="1 0 0" axisangle="0 1 0 -45">
                        <!-- Rear flexure hinge on left side -->
                        <joint name="last_left" type="hinge" axis="0 1 0" pos="0 0 0"
                               range="-45 45" stiffness="{k_joint}" damping="{b_joint}"/>
                        <geom type="box" size=".5 .5 .05" pos=".5 0 0"
                              rgba="1 0 0 .25" mass="{mass}"
                              contype="1" conaffinity="1"/>

                        <!--
                        REAR MODULE (tail_bot) & rear pad (foot2):
                          - tail_bot: rear base box, low friction (mu_base)
                          - foot2: rear pad, high friction (mu_tape)
                        -->
                        <body name="tail_bot" pos="0 .25 -.433">
                            <geom type="box" size=".5 .5 .05" pos=".5 0 0"
                                  rgba="1 0 0 .25" mass=".1"
                                  quat=".866 .5 0 0"
                                  friction="{mu_base} 0.0 0.0"
                                  contype="1" conaffinity="1"/>

                            <!-- Site near the rear "head" used as end-effector tracking point -->
                            <body name="end_effector" pos="1 -.375 .2165">
                                <geom type="sphere" size=".1" pos="0 0 0"
                                      rgba="1 1 1 1" mass=".00001"
                                      contype="1" conaffinity="1"/>
                            </body>

                            <!-- Rear pad (foot2) with its own hinge and high friction -->
                            <body name="foot2" pos="0 -.25 -.433" quat=".866 .5 0 0">
                                <!-- Hinge joint for rear pad pitch (used by foot actuator) -->
                                <joint name="foot2" type="hinge" axis="1 0 0" pos=".55 .5 0"/>
                                <geom type="box" size=".1 .25 .05" pos=".75 .5 0"
                                      rgba="0 1 0 1" mass=".1"
                                      contype="1" conaffinity="1"
                                      friction="{mu_tape} 0.0 0.0"/>
                            </body>
                        </body>
                    </body>
                </body>
            </body>
        </body>

        <!--
        RIGHT SARRUS CHAIN:
          - Mirrors the left chain; welded together in the equality section to form a 3D Sarrus linkage
        -->
        <body name="base_right" pos="0 -.25 .433" quat=".866 .5 0 0">
            <joint type="free"/>

            <geom type="box" size=".5 .5 .05" pos=".5 0 0"
                  rgba="1 0 0 .25" mass="{mass}"
                  contype="1" conaffinity="1"/>

            <body name="first_right" pos="1 0 0" axisangle="0 1 0 -45">
                <joint name="first_right" type="hinge" axis="0 1 0" pos="0 0 0"
                       range="-45 45" stiffness="{k_joint}" damping="{b_joint}"/>
                <geom type="box" size=".5 .5 .05" pos=".5 0 0"
                      rgba="1 1 1 .25" mass="{mass}"
                      contype="1" conaffinity="1"/>

                <body name="second_right" pos="1 0 0" axisangle="0 1 0 90">
                    <joint name="mid_right" type="hinge" axis="0 1 0" pos="0 0 0"
                           range="-90 90" stiffness="{k_joint}" damping="{b_joint}"/>
                    <geom type="box" size=".5 .5 .05" pos=".5 0 0"
                          rgba=".5 .5 .5 .25" mass="{mass}"
                          contype="1" conaffinity="1"/>

                    <body name="tail_right" pos="1 0 0" axisangle="0 1 0 -45">
                        <joint name="last_right" type="hinge" axis="0 1 0" pos="0 0 0"
                               range="-45 45" stiffness="{k_joint}" damping="{b_joint}"/>
                        <geom type="box" size=".5 .5 .05" pos=".5 0 0"
                              rgba="1 0 0 .25" mass="{mass}"
                              contype="1" conaffinity="1"/>
                    </body>
                </body>
            </body>
        </body>
    </worldbody>

    <!--
    Equality constraints:
      - Weld left/right bases and tails so the two chains act as a single Sarrus linkage.
      - This enforces the parallel-mechanism behavior used in the ArcFold robot.
    -->
    <equality>
        <weld body1="tail_bot"  body2="tail_right"/>
        <weld body1="tail_left" body2="tail_right"/>
        <weld body1="base_bot"  body2="base_left"/>
        <weld body1="base_bot"  body2="base_right"/>
        <weld body1="base_left" body2="base_right"/>
    </equality>

    <!--
    Actuators:
      - mid_act : drives the central Sarrus hinge (mid_left) to create arching/extension
      - foot1   : drives the front pad hinge
      - foot2   : drives the rear pad hinge
      All are modeled as position actuators with gains (k_motor, b_motor).
    -->
    <actuator>
        <position name="mid_act" joint="mid_left" kp="{k_motor}" kv="{b_motor}"/>
        <position name="foot1"   joint="foot1"    kp="{k_motor}" kv="{b_motor}"/>
        <position name="foot2"   joint="foot2"    kp="{k_motor}" kv="{b_motor}"/>
    </actuator>
</mujoco>
'''


1.6 Python: Loading and Running a Single Simulation

The Python script below creates a MuJoCo simulation instance from the XML template, applies a periodic gait defined by square‑wave joint targets, and records the motion of the end_effector for a given gait period. The example shown uses a 4 s gait period; identical code is reused for the 2 s, 1 s, and 0.5 s cases with only the frequency freq changed.

# This block is the creation of the simulation for the Mujoco model, at a specified period of 4 seconds. 
# It is repeated for all periods tested, but the repeat code will not be shown for brevity. 
# A .gif of the simulation will play at the bottom.

################## Period 4 seconds #########################
xml = xml_template.format(k_motor =15.42, # found through trial and error
                          k_joint = 2.392e-3, # from assignment 5
                          b_motor = 4.084e-6, # from RC servo data collection, most likely way off
                          b_joint = 3.28e-6, # from assignment 5
                          mu_tape = .856, # from assignment 5
                          mu_base = .434, # from assignment 5
                          mass = .005) # from assignment 5

model = mujoco.MjModel.from_xml_string(xml)
data = mujoco.MjData(model)
mujoco.mj_resetData(model, data)

theta = 45 # Initial middle hinge value, 0 = middle

data.qpos[15] = -math.pi*theta/180 # Initial conditions on hinge joints only
data.qpos[16] = 2*math.pi*theta/180
data.qpos[17] = -math.pi*theta/180
data.qpos[26] = -math.pi*theta/180
data.qpos[27] = 2*math.pi*theta/180
data.qpos[28] = -math.pi*theta/180

renderer = mujoco.Renderer(model)
      
wormAmp = np.deg2rad(90)
footAmp = -np.deg2rad(90)
freq = .25
period = 1/freq

frames = []
duration = 25 # (seconds)
framerate = 20 # (Hz)

eepos4 = []
time = []

while data.time < duration:

    data.ctrl[0] = wormAmp*(signal.square(freq*2*math.pi*(data.time+(period/4)), duty=.5)+1)/2
    data.ctrl[1] = footAmp*(signal.square(freq*2*math.pi*data.time, duty=.5)+1)/2
    data.ctrl[2] = footAmp*(signal.square(freq*2*math.pi*(data.time+(period/2)), duty=.5)+1)/2
    
    mujoco.mj_step(model, data)
    
    if len(frames) < data.time * framerate:
        renderer.update_scene(data, "iso2")
        pixels = renderer.render()
        frames.append(pixels)
        eepos4.append(data.body('end_effector').xpos[0])
        time.append(data.time)
        #print(100*len(frames)/(framerate*duration),"%")

media.set_ffmpeg(imageio_ffmpeg.get_ffmpeg_exe())
media.show_video(frames, fps = framerate)

Figure 1. Simulated ArcFold inchworm crawling with a 4 s gait period (single gait cycle repeated over the 25 s simulation).

Period of 2 seconds

Figure 2. Simulated ArcFold inchworm crawling with a 2 s gait period.

Period of 1 second

Figure 3. Simulated ArcFold inchworm crawling with a 1 s gait period.

Period of .5 seconds

Figure 4. Simulated ArcFold inchworm crawling with a 0.5 s gait period.

Sweeping the gait period in this way exposes the trade‑off between step quality and cycle frequency in the model. Longer periods produce well‑separated, clearly arching steps, whereas shorter periods compress the stance and swing phases and eventually lead to degraded motion. These simulations use the same timing structure as the ESP32 control code on the physical robot, enabling direct comparison between simulated and experimental trajectories.


1.7 Plotting Simulation Results

The code fragment below illustrates how the end‑effector x‑position is plotted as a function of time for a single gait period case (here, \(T_\text{gait} = 4\) s). Identical plotting calls are used for the 2 s, 1 s, and 0.5 s simulations after updating the underlying trajectory data.

plt.plot(time,eepos4)
plt.xlabel("Time [s]")
plt.ylabel("X position of end effector")
plt.title("Tracking End Effector Position over time. Period 4s")
plt.grid(True)

Period of 4 seconds

Figure 5. Simulated end‑effector x‑position versus time for a 4 s gait period.

Period of 2 seconds

Figure 6. Simulated end‑effector x‑position versus time for a 2 s gait period.

Period of 1 second

Figure 7. Simulated end‑effector x‑position versus time for a 1 s gait period.

Period of .5 seconds

Figure 8. Simulated end‑effector x‑position versus time for a 0.5 s gait period.

Combination plot of all periods

Figure 9. Simulated end‑effector trajectories for all gait periods overlaid, used to compare average forward speed and step quality as a function of gait period.


1.8 Sources of Error and Model Limitations

This section summarizes likely sources of discrepancy between the MuJoCo simulations and the experimental results, with emphasis on how modeling choices affect the timing study.

These limitations explain why the simulations tend to be more repeatable and slightly optimistic in speed compared with the hardware results, particularly at slower gait periods where real‑world slip, compliance losses, and servo saturation are more pronounced.

Here is a link to the .ipynb: