A Rust application that evolves neural network controllers for a simulated quadruped robot. The robot model is created in Blender, physics are simulated with Rapier3D, and controllers are optimized using Age-Fitness Pareto Optimization (AFPO). Visualization uses the Bevy game engine.
The evolutionary algorithm is based on Age-Fitness Pareto Optimization (AFPO) from:
Schmidt, M. & Lipson, H. (2011). "Age-Fitness Pareto Optimization." Genetic Programming Theory and Practice VIII, pp. 129–146. doi:10.1007/978-1-4419-7747-2_8
AFPO uses multi-objective Pareto selection on two axes — maximize fitness and minimize age — to maintain population diversity without explicit diversity metrics. Each generation, one fresh random individual (age 0) is injected, offspring inherit age 0, and all Pareto-dominated individuals are culled. This prevents premature convergence while still selecting for high fitness.
Install Rust via rustup:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | shgit clone https://github.com/your-username/robo.git
cd robo
cargo build --releaseAll modes are selected via the --mode (-m) flag. Run in release mode for best performance:
# Default: random joint actuation (robot flops around)
cargo run --release -- --mode flop
# Suspended robot with joints driven to random positions (demonstrates full range of motion)
cargo run --release -- --mode flail
# Run a randomly-initialized neural network controller
cargo run --release -- --mode random-controller
# Headless AFPO evolutionary training (no window, parallel evaluation)
cargo run --release -- --mode evolve
# Replay the best evolved controller (requires best_controller.json)
cargo run --release -- --mode run-best- WASD — Move camera
- Q/E — Move camera up/down
- Arrow keys — Rotate camera
The quadruped is modeled in Blender — the source file is glb/quadruped_2.blend — and exported to GLB format (glb/quadruped_2.glb).
- Body — Central torso (cuboid collider)
- 4 Legs — Front-left, front-right, back-left, back-right
- Each leg: hip joint → upper leg → knee joint → lower leg
| Joint | DOF | Axis Limits |
|---|---|---|
| Hip (×4) | 3 | X: [-45°, 90°], Y: [-45°, 45°], Z: [-45°, 45°] |
| Knee (×4) | 1 | X: [0°, 135°] |
Joint properties are stored as custom properties on Blender empty nodes and exported via the GLTF extras field:
{
"joint_type": "revolute",
"motor_enabled": true
}Joint type (hip vs knee) is determined by node name convention (_knee / knee_ in the name).
At startup, quadruped.rs parses the GLB file using the gltf crate:
- Node traversal — All GLTF nodes are loaded into a map with their local transforms (translation, rotation, scale)
- World transforms — A BFS from the scene root accumulates quaternion rotations and positions to compute each node's world-space pose
- Rigid bodies — Nodes with meshes become
PartDefinitions. Collider half-extents are derived from mesh bounding boxes scaled by the node's local scale - Joints — Nodes whose
extrascontain ajoint_typekey becomeJointDefinitions. Parent and child parts are found by walking the node hierarchy. Anchor points and local rotations are computed by transforming the joint's world position into each part's local coordinate space
The result is a QuadrupedDefinition containing all parts and joints, which is then used to spawn physics bodies in both the Bevy visualizer and the headless simulator.
The controller is a direct linear mapping (no hidden layer):
| Count | Description | |
|---|---|---|
| Inputs | 23 | 3 IMU (gravity in body-local frame) + 16 joint angles (4 hips × 3 axes + 4 knees × 1) + 4 foot contact sensors |
| Outputs | 16 | Motor velocity targets for each joint axis |
The genome is a flat vector of 384 floats (23×16 weights + 16 biases), directly manipulated by the evolutionary algorithm.
The AFPO training loop (--mode evolve) runs headless with parallel episode evaluation via Rayon:
| Parameter | Value |
|---|---|
| Population size | 256 |
| Mutation rate | 0.2 (per gene) |
| Mutation strength (σ) | 0.1 |
| Episode duration | 12 s |
| Selection | Age-Fitness Pareto (canonical AFPO) |
Horizontal distance traveled from the origin, plus a small bonus for staying upright.
| File | Description |
|---|---|
best_controller.json |
Serialized weights/biases of the best-ever controller. Saved every 5 generations during evolution; loaded by --mode run-best |
lineage_log.json |
Per-generation snapshots of the best individual in each lineage, for tracking diversity over time |
plot_lineage.py |
Matplotlib script that reads lineage_log.json and plots fitness vs generation for each lineage. Run: python plot_lineage.py |
- Multiple robot configurations

