Advanced model composition¶
The previous tutorial demonstrated that you can compose "standard" scenes using the flygym.compose module. What if you need to customize it more? By design, FlyGym does not expose all available options in MuJoCo (that'd just be reinventing the wheel). Instead, the dm_control.mjcf module can be used to adjust these settings. Essentially, the entire model being simulated in MuJoCo is fully specified in an MJCF file (which follows the XML format). Libraries like dm_control.mjcf and flygym.compose are simply "parsers and writers" that generate and modify these XML files in a human-friendly way.
In this tutorial, we will demonstrate how you can use dm_control.mjcf to customize your model beyond what's natively supported by FlyGym. For all details, refer to the official dm_control.mjcf documentation.
Modifying MJCF model directly through dm_control.mjcf¶
Let's compose a Fly object as we did in the previous tutorial:
from flygym.compose import Fly, KinematicPosePreset
from flygym.anatomy import AxisOrder, JointPreset, Skeleton, ActuatedDOFPreset
from flygym.compose import KinematicPosePreset
fly = Fly(name="my_fly_model")
axis_order = AxisOrder.ROLL_PITCH_YAW
joint_preset = JointPreset.ALL_BIOLOGICAL
skeleton = Skeleton(joint_preset=joint_preset, axis_order=axis_order)
neutral_pose = KinematicPosePreset.NEUTRAL
joints = fly.add_joints(skeleton, neutral_pose=neutral_pose)
actuation_preset = ActuatedDOFPreset.LEGS_ACTIVE_ONLY
actuator_gain = 50
actuated_jointdofs = skeleton.get_actuated_dofs_from_preset(actuation_preset)
actuators = fly.add_actuators(
actuated_jointdofs,
actuator_type="position",
neutral_input=neutral_pose,
kp=actuator_gain,
)
fly.colorize()
camera = fly.add_tracking_camera()
But what do these translate to when we build the MJCF model specification? Let's export the model inspect the MJCF XML file:
from pathlib import Path
export_dir = Path("./demo_output/fly_model/")
fly.save_xml_with_assets(export_dir)
When we export the model to a directory, the MJCF file (in XML format) as well as all asset files (e.g. body geometric meshes) are saved under that directory:
!ls ./demo_output/fly_model/
c_abdomen12-a8d3b2092c0571e951c358e8d9536526eeecfadb.stl c_abdomen3-1f0954c337602ed3433f170cfeeb165233459e93.stl c_abdomen4-9eadc4169cd302e24703574f41608d390e1b3210.stl c_abdomen5-11f947c4a9f3d24975aa1ceb8786b1b4c448f0c7.stl c_abdomen6-b284da8f3035ebb1155e510b235ec455b2dceec9.stl c_haustellum-3dfc3c1da359ef1fed4cc8dd8e575e3166da97e8.stl c_head-7227f5a259368a94b9c4daa38115e125baa8b5e4.stl c_rostrum-5e39518fecaefb180162b126e850c42e65d7d24f.stl c_thorax-bd37f753b689721e52b3f11e7b7e53056d58c176.stl l_arista-ca5a94f3b4156aad813ba22162c061ec202b9408.stl l_eye-37af822ee1b2e87ad52e4bbfaa7e53f3cf82537e.stl lf_coxa-1321c57273cb04d3ffed05c96e37110a354a690a.stl lf_tarsus1-1d147b1716e7cac4d306a3674bf7ff78d920b433.stl lf_tarsus2-b70ade6a1b7d78fcc09e5f00442efb7713b8d741.stl lf_tarsus3-ed4a9553f2f81f9db0aca50a2c71673297de512d.stl lf_tarsus4-ae38a2f510fa46e3398a9026698c2fa9c5da1892.stl lf_tarsus5-49613537901312348802346b32762812e5f25d33.stl lf_tibia-bc9473c6217db75eba20d8643322fdf77d21fd18.stl lf_trochanterfemur-241f81c512fae0bc04fa5ed6b6ae0793f0a67c5a.stl l_funiculus-c1035b2ae2f250d031c1ae124c8779821087f76e.stl l_haltere-5ce9659a6f1f6b997b459d1cc91812189ac785b4.stl lh_coxa-32afbdd3e3d23227f7e8791d459f370f3e514a39.stl lh_tarsus1-611bb8735c9e036775ad304668305a3b3ca90c73.stl lh_tarsus2-d09375a6d0bf5d1d6b73f42b227dd4b952842459.stl lh_tarsus3-4f8b67478664dfd54acdce2b6f4fa30d19263ece.stl lh_tarsus4-e41c7f2e6fa0cc7e102807cf115c94bfc3fe6005.stl lh_tarsus5-08cf1d51c63e82183edcc0a4ba324adea0785a89.stl lh_tibia-2c0ce68948ec97b9fd8128e5e050a330f347d7b9.stl lh_trochanterfemur-0a494293ecc8c9a4da7204bf099fad714e625e23.stl lm_coxa-20287d4dd0b1e39d7ba5595942b6415576630c25.stl lm_tarsus1-5396934c3d3c1510dd67698dca637cf5117a843f.stl lm_tarsus2-5b9cc095b7cbad69a6483e110b2732f3c24ee94c.stl lm_tarsus3-926037bff9d9c5257d1e640aef23ac464d132c01.stl lm_tarsus4-9835eec5f3a691e4aaa2af172dad1abfe2beb84f.stl lm_tarsus5-a30e84c23a5a95f4bc8ad4d2c0164c21e8941639.stl lm_tibia-5b1c0ce0184c5b8801a3e7f20fb608ac633f35aa.stl lm_trochanterfemur-79abf8f6fb0906078e3bbfaed664639e7fe0e0f5.stl l_pedicel-1040a34bd5b5a9957cf9ca4c488b4bf9d185ba42.stl l_wing-4b059e8ef720d55ed2e83368728f667478334ba7.stl my_fly_model.xml
Please check the content of the actual XML file by downloading and opening the path below.
Before we start,
- If you are not familiar with the XML file format, see this short explanation from IBM.
- All possible XML elements and their full specifications can be found in the XML reference of the official MuJoCo documentation.
- Anything that is not explicitly defined in the XML file take the default value mentioned in the XML reference above.
xml_path = export_dir / f"{fly.name}.xml" # path to the actual XML
print(f"View XML file at this path: {xml_path}")
View XML file at this path: demo_output/fly_model/my_fly_model.xml
The first thing that you will notice is that the whole model is under a <mujoco model="my_fly_model"> element, where "my_fly_model" is the name we've assigned to the fly when we called fly = Fly(name="my_fly_model").
The next few sections define global settings for the simulation:
The
<compiler ...>element defines settings such as lower bounds on the mass and inertia of simulated bodies, the native angle unit, and the axis order for Euler angles:<compiler autolimits="true" boundmass="1e-06" boundinertia="1e-12" angle="radian" eulerseq="XYZ" fusestatic="true"/>
The
<option ...>element defines core simulation settings such as timestep, integrator, solver, and contact handling. In particular,gravity="0 0 -9810"represents Earth gravity in the model's length unit. Because this model uses millimeters, gravity is written as -9810 mm/s⁻² (equivalent to -9.81 m/s⁻²) (see discussion below).<option timestep="0.0001" gravity="0 0 -9810" integrator="Euler" solver="Newton" iterations="100" noslip_iterations="5"> <flag energy="enable" multiccd="enable"/> </option>
MuJoCo itself is unitless, so keeping units consistent across the model is the user's responsibility. When performing real-number arithmetics with discrete binary bits on a computer, keeping numbers closer in their orders of magnitude generally helps reduce round-off errors caused by floating point error. Therefore, since the fly is small, we use millimeter and gram as base units for length and mass instead of their SI counterparts, miter and kilogram. This means that the forces that we read out from the simulation are in g·mm·s⁻² (i.e., micronewton, μN), instead of kg·m·s⁻² (i.e., Newton). This helps us bring numerically computed numbers about 6 orders of magnitude closer (compared to length, which is around 1 mm).
The
<visual>element controls rendering-only parameters. Here,<headlight ...>sets scene lighting, and<map ...>sets scaling factors used to visualize forces and torques in the interactive viewer.<visual> <headlight ambient="0.5 0.5 0.5" diffuse="0.6 0.6 0.6" specular="0.3 0.3 0.3"/> <map stiffness="1000000" stiffnessrot="5000000" force="1e-05" torque="0.0002"/> </visual>
Next, we have the <asset> section. It contains reusable resources such as mesh files, textures, and materials that are referenced by bodies and geometries elsewhere in the model.
<asset>
<!-- Note that the meshes are scaled up 1000x in x, y, and z. This is because we -->
<!-- use mm as the base unit for length in the simulation, but mesh geometries are -->
<!-- specified in SI units (m). -->
<mesh
name="c_thorax"
class="/"
file="c_thorax-bd37f753b689721e52b3f11e7b7e53056d58c176.stl"
scale="1000 1000 1000"/>
<mesh
name="c_head"
class="/"
file="c_head-7227f5a259368a94b9c4daa38115e125baa8b5e4.stl"
scale="1000 1000 1000"/>
...
<material
name="headthorax"
class="/"
texture="headthorax"
specular="0.2"
shininess="0.2"
rgba="1 1 1 1"/>
<texture
name="headthorax"
builtin="flat"
rgb1="0.59 0.39 0.12"
rgb2="0.59 0.39 0.12"
mark="random"
markrgb="0.7 0.49 0.2"
random="0.3"
width="50"
height="50"/>
...
</asset>
The next section is very important, as it defines the kinematic tree of our animal model. The top-level element is <worldbody>, which encompasses all bodies in the model and the joints that connect them. In MuJoCo, body objects are abstract containers that hold "things" such as geometries, joints, and sites. The actual shape of a body is defined by its child geom elements, which can be primitive (spheres, cylinders, boxes, or cylinders with hemispherical ends, called "capsules") or mesh-based. In our case, body-segment meshes from the <asset> section are used for geometry. This section is very long and repetitive, so we include only a simplified, reordered snippet for demonstration.
<worldbody>
<!-- The root of our fly kinematic tree is defined as the thorax -->
<!-- We define its body as an empty container, and a geom using the mesh file asset -->
<body name="c_thorax" pos="0.496 0 1.3" quat="1 0 0 0">
<geom name="c_thorax" class="/" type="mesh" contype="0" conaffinity="0"
material="headthorax" mass="0.000307" mesh="c_thorax"/>
<!-- Under the thorax body, we can add body segments that extend from it -->
<!-- The order in which elements are defined does not matter, but the level -->
<!-- of the element is important (e.g., you can define the geom first and then -->
<!-- the child bodies, or vice versa). -->
<!-- For example, here we define the thorax-head-proboscis kinematic chain -->
<body name="c_head" ... >
<geom name="c_head" ... />
<!-- Under the "head" body, we can add joints that connect it to its parent -->
<!-- in the kinematic tree (thorax). Note that in NeuroMechFly, all joints -->
<!-- within the fly body are "hinge" joints, which means they define rotations -->
<!-- along only one rotational axis. In other words, they correspond to the -->
<!-- JointDOF class in FlyGym, not AnatomicalJoint (which can contain >1 DoFs. -->
<!-- Here, the thorax-head joint is a 3-DoF joint, so we define 3 joints. -->
<!-- Joints can have biophysical properties. For example, their passive -->
<!-- biomechanics are approximated as a spring-damper system (passive as in -->
<!-- the spring-damper system can dynamically respond to forces even in the -->
<!-- absence of active actuators like motors or muscles). The "springref" -->
<!-- parameter defines the neutral position. -->
<joint name="c_thorax-c_head-roll"
type="hinge" axis="0 0 1"
stiffness="10" damping="0.5" springref="0" ... />
<joint name="c_thorax-c_head-pitch" type="hinge" axis="0 1 0"
stiffness="10" damping="0.5" springref="0" ... />
<joint name="c_thorax-c_head-yaw" type="hinge" axis="1 0 0"
stiffness="10" damping="0.5" springref="0" ... />
<!-- Now we have further body segments extending from the head -->
<body name="c_rostrum" ... > <!-- rostrum: proximal part of the proboscis -->
<geom name="c_rostrum" ... />
<!-- From here on, we omit joint type, axis, spring-damper parameters, etc. -->
<joint name="c_head-c_rostrum-roll" ... />
<joint name="c_head-c_rostrum-pitch" ... />
<joint name="c_head-c_rostrum-yaw" ... />
<!-- ... and so forth -->
<body name="c_haustellum" ... > <!-- rostrum: distal part of the proboscis -->
<geom name="c_haustellum" ... />
<joint name="c_rostrum-c_haustellum-roll" ... />
<joint name="c_rostrum-c_haustellum-pitch" ... />
<joint name="c_rostrum-c_haustellum-yaw" ... />
</body>
</body>
<!-- The kinematic tree can branch out. For example, here we define the left -->
<!-- antenna. Note that the base link of the antenna (pedicel) is defined -->
<!-- immediately under the head (i.e., at the same level as the rostrum). -->
<!-- In other words, we have two branches in the kinematic tree: -->
<!-- thorax-head-rostrum-haustellum and thorax-head-l_pedicel-... -->
<body name="l_pedicel" ... >
<geom name="l_pedicel"... />
<joint name="c_head-l_pedicel-roll" ... />
<joint name="c_head-l_pedicel-pitch" ... />
<joint name="c_head-l_pedicel-yaw" ... />
<body name="l_funiculus" ... >
<geom name="l_funiculus" ... />
<joint name="l_pedicel-l_funiculus-roll" ... />
<joint name="l_pedicel-l_funiculus-pitch" ... />
<joint name="l_pedicel-l_funiculus-yaw" ... />
<body name="l_arista" ... >
<geom name="l_arista" ... />
<joint name="l_funiculus-l_arista-roll" ... />
<joint name="l_funiculus-l_arista-pitch" ... />
<joint name="l_funiculus-l_arista-yaw" ... />
</body>
</body>
</body>
</body>
<!-- Similarly, at the same level as the head, other kinematic chains can extend -->
<!-- from the thorax. Here we have the left-front leg's coxa, which rotates about -->
<!-- all 3 axes of rotation at its joint with the thorax -->
<body name="lf_coxa" ... >
<geom name="lf_coxa" ... />
<joint name="c_thorax-lf_coxa-roll" ... />
<joint name="c_thorax-lf_coxa-pitch" ... />
<joint name="c_thorax-lf_coxa-yaw" ... />
<!-- The second link of the kinematic chain is the fused trochanter-femur -->
<!-- segment, which has 2 axes of rotation at its joint with the coxa -->
<body name="lf_trochanterfemur" ... >
<geom name="lf_trochanterfemur" ... />
<joint name="lf_coxa-lf_trochanterfemur-roll" ... />
<joint name="lf_coxa-lf_trochanterfemur-pitch" ... />
<!-- Now the tibia, which only has one axis of rotation relative to -->
<!-- trochanter-femur (pitch) -->
<body name="lf_tibia" ... >
<geom name="lf_tibia" ... />
<joint name="lf_trochanterfemur-lf_tibia-pitch" ... />
<!-- The tarsus is compliant (soft) and has 5 segments. In NeuroMechFly, -->
<!-- each tarsal segment is approximated as a rigid body, and their -->
<!-- connections are approximated as passive, non-rigid joints (i.e., with -->
<!-- stiffness and damping) -->
<body name="lf_tarsus1" ... >
<geom name="lf_tarsus1" ... />
<joint name="lf_tibia-lf_tarsus1-pitch" ...
stiffness="10" damping="0.5" springref="-0.17" ... />
<body name="lf_tarsus2" ... >
<geom name="lf_tarsus2" ... />
<joint name="lf_tarsus1-lf_tarsus2-pitch" ...
stiffness="10" damping="0.5" springref="-0.09" ... />
<body name="lf_tarsus3" ... >
<geom name="lf_tarsus3" ... />
<joint name="lf_tarsus2-lf_tarsus3-pitch" ...
stiffness="10" damping="0.5" springref="-0.09" ... />
<body name="lf_tarsus4" ... >
<geom name="lf_tarsus4" ... />
<joint name="lf_tarsus3-lf_tarsus4-pitch" ...
stiffness="10" damping="0.5" springref="-0.09" ... />
<body name="lf_tarsus5" ... >
<geom name="lf_tarsus5" ... />
<joint name="lf_tarsus4-lf_tarsus5-pitch" ...
stiffness="10" damping="0.5" springref="-0.09" ... />
</body>
</body>
</body>
</body>
</body>
</body>
</body>
</body>
<!-- Other legs are defined in parallel to the left front leg -->
<body name="lm_coxa" ... >
...
</body>
...
<!-- End of the model -->
</worldbody>
The next section defines actuators:
<actuator>
<!-- The "position" tag defines position actuators. Velocity, torque, muscle, and -->
<!-- other actuators can be defined using "velocity", "motor", and "muscle" tags, etc. -->
<position
name="c_thorax-lf_coxa-roll-position"
class="/"
forcelimited="true"
forcerange="-30 30"
joint="c_thorax-lf_coxa-roll"
kp="50"/>
<position
name="c_thorax-lf_coxa-pitch-position"
class="/" forcelimited="true"
forcerange="-30 30"
joint="c_thorax-lf_coxa-pitch"
kp="50"/>
<position
name="c_thorax-lf_coxa-yaw-position"
class="/"
forcelimited="true"
forcerange="-30 30"
joint="c_thorax-lf_coxa-yaw"
kp="50"/>
...
</actuator>
There are additional sections that can be defined. For these, see the MuJoCo XML reference for details.
Now, you might have realized that flygym.compose is simply a tool to generate these MJCF specification strings. There is literally no physics simulation involved at all—just string manipulation and formatting that result in this text-only XML file (in fact, so is dm_control.mjcf). If you want to customize certain things in ways that are not supported by flygym.compose? You can quite literally modify the appropriate parameters in the XML file after composing your model using flygym.compose.
However, we do not recommend that you literally edit the XML file directly. We have learned through our experience that custom edits to XML files are hard to reproduce and hard to document. In a year's time, you might have mysterious semi-manually edited XML files passed among collaborators that no one really understands how they were made. For this reason, we strongly recommend using dm_control.mjcf to edit the model procedurally (i.e., with human-readable code).
As a concrete example, let's say you want to change the stiffness of all tarsal joints (tarsus1-2, 2-3, 3-4, 4-5) from 10 to 5. With flygym.compose, you can use a non-default stiffness value (fly.add_joints(stiffness=...)), but this would apply to all joints. Therefore, the best way to selectively modify tarsal joints is to use dm_control.mjcf. We will briefly demonstrate how you can do this, but for all details about the dm_control.mjcf API, please refer to its official documentation.
The MJCF root element is exposed through FlyGym as fly.mjcf_root (or world.mjcf_root on objects of World classes):
fly.mjcf_root
MJCF Element: <mujoco model="my_fly_model">...</mujoco>
We can access its element trees, for example the worldbody:
fly.mjcf_root.worldbody
MJCF Element: <worldbody>...</worldbody>
We can use .find_all(tag_name) to find all elements of that type under any element. In this case, let's find all joints:
all_joints = fly.mjcf_root.worldbody.find_all("joint")
print(f"Found {len(all_joints)} joints in the worldbody (printing first 5):")
for i, joint in enumerate(all_joints[:5]):
print(joint)
Found 126 joints in the worldbody (printing first 5): <joint name="c_rostrum-c_haustellum-roll" class="/" type="hinge" axis="0 0 1" stiffness="10" springref="0" armature="9.9999999999999995e-07" damping="0.5"/> <joint name="c_rostrum-c_haustellum-pitch" class="/" type="hinge" axis="0 1 0" stiffness="10" springref="0" armature="9.9999999999999995e-07" damping="0.5"/> <joint name="c_rostrum-c_haustellum-yaw" class="/" type="hinge" axis="1 0 0" stiffness="10" springref="0" armature="9.9999999999999995e-07" damping="0.5"/> <joint name="c_head-c_rostrum-roll" class="/" type="hinge" axis="0 0 1" stiffness="10" springref="0" armature="9.9999999999999995e-07" damping="0.5"/> <joint name="c_head-c_rostrum-pitch" class="/" type="hinge" axis="0 1 0" stiffness="10" springref="0" armature="9.9999999999999995e-07" damping="0.5"/>
By filtering by name, we can find all tarsal joints. Recall from the previous tutorial that flygym.anatomy offers handy Python class representations like JointDOF. In this case, you can parse MuJoCo <joint> names into JointDOF objects to simplify parsing. For example:
from flygym.anatomy import JointDOF
dof = JointDOF.from_name("c_thorax-lf_coxa-roll")
print("DoF:", dof.name)
print("parent:", dof.parent)
print("child:", dof.child)
print("child.is_leg:", dof.child.is_leg())
print("child.pos:", dof.parent.pos)
print("child.link:", dof.child.link)
DoF: c_thorax-lf_coxa-roll parent: BodySegment(name='c_thorax') child: BodySegment(name='lf_coxa') child.is_leg: True child.pos: c child.link: coxa
With this, we can modify tarsal segments as follows:
for joint_element in fly.mjcf_root.worldbody.find_all("joint"):
dof = JointDOF.from_name(joint_element.name)
if dof.child.link.startswith("tarsus") and dof.child.link != "tarsus1":
old_stiffness = joint_element.stiffness
joint_element.stiffness = 5
print(
f"Joint {joint_element.name}: "
f"changed stiffness from {old_stiffness} to {joint_element.stiffness}"
)
Joint lf_tarsus4-lf_tarsus5-pitch: changed stiffness from 10.0 to 5.0 Joint lf_tarsus3-lf_tarsus4-pitch: changed stiffness from 10.0 to 5.0 Joint lf_tarsus2-lf_tarsus3-pitch: changed stiffness from 10.0 to 5.0 Joint lf_tarsus1-lf_tarsus2-pitch: changed stiffness from 10.0 to 5.0 Joint lm_tarsus4-lm_tarsus5-pitch: changed stiffness from 10.0 to 5.0 Joint lm_tarsus3-lm_tarsus4-pitch: changed stiffness from 10.0 to 5.0 Joint lm_tarsus2-lm_tarsus3-pitch: changed stiffness from 10.0 to 5.0 Joint lm_tarsus1-lm_tarsus2-pitch: changed stiffness from 10.0 to 5.0 Joint lh_tarsus4-lh_tarsus5-pitch: changed stiffness from 10.0 to 5.0 Joint lh_tarsus3-lh_tarsus4-pitch: changed stiffness from 10.0 to 5.0 Joint lh_tarsus2-lh_tarsus3-pitch: changed stiffness from 10.0 to 5.0 Joint lh_tarsus1-lh_tarsus2-pitch: changed stiffness from 10.0 to 5.0 Joint rf_tarsus4-rf_tarsus5-pitch: changed stiffness from 10.0 to 5.0 Joint rf_tarsus3-rf_tarsus4-pitch: changed stiffness from 10.0 to 5.0 Joint rf_tarsus2-rf_tarsus3-pitch: changed stiffness from 10.0 to 5.0 Joint rf_tarsus1-rf_tarsus2-pitch: changed stiffness from 10.0 to 5.0 Joint rm_tarsus4-rm_tarsus5-pitch: changed stiffness from 10.0 to 5.0 Joint rm_tarsus3-rm_tarsus4-pitch: changed stiffness from 10.0 to 5.0 Joint rm_tarsus2-rm_tarsus3-pitch: changed stiffness from 10.0 to 5.0 Joint rm_tarsus1-rm_tarsus2-pitch: changed stiffness from 10.0 to 5.0 Joint rh_tarsus4-rh_tarsus5-pitch: changed stiffness from 10.0 to 5.0 Joint rh_tarsus3-rh_tarsus4-pitch: changed stiffness from 10.0 to 5.0 Joint rh_tarsus2-rh_tarsus3-pitch: changed stiffness from 10.0 to 5.0 Joint rh_tarsus1-rh_tarsus2-pitch: changed stiffness from 10.0 to 5.0
As you can see, these few lines of code are much more readable, version-trackable (e.g., with Git), and maintainable that someone changing these numbers manually in a 500-line XML file and passing it on to someone else.
We can also add new elements with dm_control.mjfc. Purely for the sake of demonstration, we will add a red sphere at the tibia-tarsus joint of every leg, and remove it specifically for the right front leg:
from flygym.anatomy import BodySegment
for body_element in fly.mjcf_root.worldbody.find_all("body"):
body_segment = BodySegment(body_element.name)
if body_segment.link == "tarsus1":
leg_name = body_segment.pos
# Add a sphere under the tarusus1 body at position (0, 0, 0), which, by
# definition, is the joint with its parent (tibia)
sphere_geom = body_element.add(
"geom",
name=f"visualization_sphere_{leg_name}",
type="sphere",
size=[0.1],
rgba=[1, 0, 0, 1],
pos=[0, 0, 0],
density=0, # important! Make the sphere is massless and visualization-only
)
print(f"Added sphere {sphere_geom.name} to body {body_element.name}")
Added sphere visualization_sphere_lf to body lf_tarsus1 Added sphere visualization_sphere_lm to body lm_tarsus1 Added sphere visualization_sphere_lh to body lh_tarsus1 Added sphere visualization_sphere_rf to body rf_tarsus1 Added sphere visualization_sphere_rm to body rm_tarsus1 Added sphere visualization_sphere_rh to body rh_tarsus1
Now, we can find the sphere for the left front leg:
fly.mjcf_root.worldbody.find("geom", "visualization_sphere_rf")
MJCF Element: <geom name="visualization_sphere_rf" class="/" type="sphere" size="0.10000000000000001" rgba="1 0 0 1" density="0" pos="0 0 0"/>
... and remove it:
fly.mjcf_root.worldbody.find("geom", "visualization_sphere_rf").remove()
Now, if we visualize the fly, we will notice the new sphere objects (except for the left front leg).
from flygym.rendering import preview_model
mj_model, mj_data = fly.compile()
preview_model(mj_model, mj_data, camera, show_in_notebook=True)
trackcam |
Modifying physics parameters¶
Beyond the body kinematic tree, you can also modify global simulation parameters by modifying attributes in the appropriate sections. For example, to change the simulation timestep from 0.1 ms to 1 ms (which makes the simulation 10x faster but less stable):
print(f"Old timestep:", fly.mjcf_root.option.timestep)
fly.mjcf_root.option.timestep = 0.001
print(f"New timestep:", fly.mjcf_root.option.timestep)
Old timestep: 0.0001 New timestep: 0.001
Or, to make background lighting less bright:
print("Old headlight ambience:", fly.mjcf_root.visual.headlight.ambient)
fly.mjcf_root.visual.headlight.ambient = [0.1, 0.1, 0.1] # in RGB
print("New headlight ambience:", fly.mjcf_root.visual.headlight.ambient)
Old headlight ambience: [0.5 0.5 0.5] New headlight ambience: [0.1 0.1 0.1]
mj_model, mj_data = fly.compile()
preview_model(mj_model, mj_data, camera, show_in_notebook=True)
trackcam |
Where does flygym.compose gets its data from?¶
Defaults used by flygym.compose are defined in these files:
from flygym import assets_dir
model_dir = (assets_dir / "model").relative_to(Path.cwd(), walk_up=True)
for path in sorted(model_dir.glob("*")):
print(path)
../src/flygym/assets/model/legacy ../src/flygym/assets/model/meshes ../src/flygym/assets/model/mujoco_globals.yaml ../src/flygym/assets/model/pose ../src/flygym/assets/model/rigging.yaml ../src/flygym/assets/model/visuals.yaml
Here,
- The
legacy/folder can be ignored. - The
meshes/folder contains geometric meshes for body parts. mujoco_globals.yamldefines parameters in the<compiler>,<option>,<size>and<visual>sections. Anything left unset defaults to its MuJoCo default.- The
pose/folder contains built-in kinematic poses (e.g., thepose/neutral/subfolder contains joint angles for the default pose in different joint axis orders). rigging.yamldefines the kinematic tree (which body parts are connected by which joints).visuals.yamldefines the appearance of different body parts.
For reasons similar to our arguments against manually modifying XML files, we recommend that you don't modify these files yourself and use dm_control.mjcf to chagne them instead. An exception is that you might add additional kinematic poses under the pose/ folder.