Squat Jump
📋 How Does it Work
Below is a 3D visualizer window where you can see a musculoskeletal model.
In order to give it inputs, you can use the Edit Muscle Excitations button located on the right side of the viewer.
Muscles are seperated into functional groups (e.g. Knee Flexors, Plantar Flexors) for easier control.
Simply choose the group you want to enter your input and draw it using the editor. It will be automatically saved.
Once you are done with your inputs, close the editor and click "Run Simulation". Your inputs will be simulated using OpenSim in the backend
and the results will be displayed.
🎯 Goal
Goal of this challenge is to jump as high as possible from a squat position.
The jump height will be measured from the initial position to the peak of the jump. You can view your results below the
viewer once the simulation is completed.
Analyze your results using the 3D viewer by slowing down the animation and rotating the view.
Iterate on your muscle excitation strategy to improve your jump height.
Once you are satisfied with your result, reveal what the genetic algorithm came up with and compare your performance to it.
Points won't be deducted for broken bones
🏆 Leaderboard
After achieving a jump height you are satisfied with, don't forget to submit your score to the leaderboard using the "Submit Your Score" button in the leaderboard panel. If you are interested in how others achieved their results you can import their excitation patterns directly into the muscle editor and run the simulation again.
👨💻 Your Solution
📊 Results
🔒 Optimal Solution (Click to Reveal)
Solution Viewer
View and analyze the Genetic Algorithm's solution below, or compare it with your own approach. Use the dropdown in the top-left corner to select a solver and watch how its solution evolved across generations.
📊 Performance Comparison
👨💻 Your Results
🏆 Solver Results
📈 Your Performance
🧬 How Does the Genetic Algorithm Work?
Here's how it works in a nutshell:
In this application of the Genetic Algorithm each Jumper is represented with their own "genome". Each genome is simply a list of muscle excitation values between 0 and 1 that looks something like this:
[0.0, 0.5, 1.0, 0.75, 0.55, 0.0, 0.1, 0.23, 0.77, 1.0, 0.9, ...,]
It is configured so that each muscle group has 20 excitation values over 1 second, giving it a resolution of 0.05s per value. Meaning that the algorithm decides the excitation level of each muscle group every 0.05 seconds during the jump. Since we have 8 muscle groups each individual consist of a list with 160 values.
So how does the Genetic Algorithm use these genomes to evolve better jumpers over generations?
- Start Random: Generate 200 jumpers with completely random muscle excitation patterns. Most of them will be terrible at jumping
- Survival of the Fittest: Run each jumper through the simulation and measure how high they jump. The ones that jump higher get a better fitness score
- Create the Next Generation: After each generation, we have a pool of candidate jumpers that will pass their genomes on. Their probability of being selected is based on their fitness scores. After the selection process two parent jumpers create a child by randomly mixing their muscle excitation patterns. Each "gene" (muscle excitation value at a specific time) has a 50% chance of coming from either parent. This means successful strategies from different jumpers can combine - maybe Parent A has great timing for the Knee Extensors, while Parent B nails the ankle push-off. Their child might inherit the best of both worlds.
- Add Some Variation: Sometimes genes randomly mutate (a muscle excitation value changes randomly). This helps discover new strategies that neither parent had.
- Rinse and Repeat: The new generation of 200 jumpers competes, and the cycle continues. Each generation gets progressively better at jumping as successful strategies spread and combine.
After 50 generations, the population converges on near-optimal solutions. Without any explicit information about jumping, the algorithm figures out the necessary muscle coordination to maximize jump height.
You may also notice that there are a lot of unnecessary movement after the take-off. This is a result of how the fitness function is defined. Which I will get more into in the next challenges.
For those interested in the details, below is the function I used to create the new generation. The whole project is also available on my GitHub.
View the Crossover Function
@classmethod
def crossover(cls, population_count, jumpers, mutation_rate, overlap, random, gen_counter):
"""Create a new generation using fitness-proportional selection."""
# First sort the jumpers by fitness
jumpers = sorted(jumpers, key=lambda jumper: jumper.fitness, reverse=True)
fitness_scores = np.array([jumper.fitness for jumper in jumpers])
# Normalize fitness scores to probabilities
probabilities = fitness_scores / np.sum(fitness_scores)
# This is called the elitism. We keep the best jumpers as is for the next gen.
new_gen = [jumper for jumper in jumpers[:overlap]]
# Adding some amount of completely random new jumpers to maintain diversity
new_gen += [cls.generate_member() for _ in range(random)]
for i in range(population_count - overlap - random):
parents = np.random.choice(jumpers, size=2, p=probabilities) # Select 2 parents based on fitness
parent1, parent2 = parents[0], parents[1]
child_genom = []
# Crossover the genes of the parents to create a child
for p1_gene, p2_gene in zip(parent1.get_genom(), parent2.get_genom()):
roll = np.random.random()
child_param = p1_gene if roll <= 0.5 else p2_gene
if np.random.random() < mutation_rate: # Random mutation chance that changes the childs gene
child_param = np.random.uniform(cls.GENOM_RANGE[0], cls.GENOM_RANGE[1])
child_genom.append(child_param)
name = f"{cls.PREFIX}_gen{gen_counter}_{i + overlap}"
child = cls(child_genom, name=name)
new_gen.append(child)
return new_gen