Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Development into master #581

Merged
merged 2 commits into from
Oct 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions codetools/mypy/mypy.ini
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,9 @@ ignore_missing_imports = True
[mypy-scipy.*]
ignore_missing_imports = True

[mypy-sklearn.*]
ignore_missing_imports = True

[mypy-cairo.*]
ignore_missing_imports = True

Expand Down
2 changes: 2 additions & 0 deletions docs/source/physical_robot_core_setup/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ On the RPi adjust the config in `/boot/config.txt` or on newer systems `/boot/fi
------------------
Setting up the RPi
------------------
**Note**: For students in the CI Group, the RPi is already set up. All RPis are flashed with the same image, so the following steps are not necessary. Additionally, there should be an IP address on the head, allowing you to SSH into it. However, be aware that there will always be ongoing development changes in the revolve2-modular-robot_physical and revolve2-robohat packages, so make sure to pip install the latest version in your virtual environment.

This step is the same for all types of hardware.

#. Flash the SD card with Raspberry Pi OS (previously Raspbian). Some Important notes:
Expand Down
2 changes: 2 additions & 0 deletions examples/3_experiment_foundations/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ Here you will learn about a few things that are important in almost all experime
- In `3b_evaluate_single_robot` we will see how you can evaluate robots in ane experiment.
- Since evaluating one robot is not very interesting in `3c_evaluate_multiple_isolated_robots` you will see how this evaluation can be done for a population of robots.
- Alternatively if you want multiple robots interacting with each other look into example `3d_evaluate_multiple_interacting_robots`.


Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
In this example you will learn how to create N random robots, starting from their genotypes and the process of mapping them into
phenotypes and finally robots in order to asses the "quality" of your initial population and individual morphological traits.

You learn:
- How to generate N random genotypes (where N is set in config.py).
- How to develop the genotypes into phenotypes to form robots.
- How to visualize such robots to asses their morphologies
- How to use the "MorphologicalMeasures" class to calculate different morpholoical traits of individuals.
- How to use such measures to compute population metrics, such as diversity.
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"""Configuration parameters for this example."""

DATABASE_FILE = "database.sqlite"
NUM_SIMULATORS = 8
POPULATION_SIZE = 2 # Setting this to 1 will result in warnings and faulty diversity measures, as you need more than 1 individual for that.
EVALUATE = True
VISUALIZE_MAP = True # Be careful when setting this to true when POPULATION_size > 1, as you will get plots for each individual.
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
"""Evaluator class."""

from genotype import Genotype

from revolve2.experimentation.evolution.abstract_elements import Evaluator as Eval
from revolve2.modular_robot_simulation import (
ModularRobotScene,
Terrain,
simulate_scenes,
)
from revolve2.simulators.mujoco_simulator import LocalSimulator
from revolve2.standards import fitness_functions, terrains
from revolve2.standards.simulation_parameters import make_standard_batch_parameters


class Evaluator(Eval):
"""Provides evaluation of robots."""

_simulator: LocalSimulator
_terrain: Terrain

def __init__(
self,
headless: bool,
num_simulators: int,
) -> None:
"""
Initialize this object.

:param headless: `headless` parameter for the physics simulator.
:param num_simulators: `num_simulators` parameter for the physics simulator.
"""
self._simulator = LocalSimulator(
headless=headless, num_simulators=num_simulators
)
self._terrain = terrains.flat()

def evaluate(
self,
population: list[Genotype],
) -> list[float]:
"""
Evaluate multiple robots.

Fitness is the distance traveled on the xy plane.

:param population: The robots to simulate.
:returns: Fitnesses of the robots.
"""
robots = [genotype.develop() for genotype in population]
# Create the scenes.
scenes = []
for robot in robots:
scene = ModularRobotScene(terrain=self._terrain)
scene.add_robot(robot)
scenes.append(scene)

# Simulate all scenes.
scene_states = simulate_scenes(
simulator=self._simulator,
batch_parameters=make_standard_batch_parameters(),
scenes=scenes,
)

# Calculate the xy displacements.
xy_displacements = [
fitness_functions.xy_displacement(
states[0].get_modular_robot_simulation_state(robot),
states[-1].get_modular_robot_simulation_state(robot),
)
for robot, states in zip(robots, scene_states)
]

return xy_displacements
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
"""Genotype class."""

from __future__ import annotations

from dataclasses import dataclass

import multineat
import numpy as np

from revolve2.modular_robot import ModularRobot
from revolve2.standards.genotypes.cppnwin.modular_robot import BrainGenotypeCpg
from revolve2.standards.genotypes.cppnwin.modular_robot.v2 import BodyGenotypeV2


@dataclass
class Genotype(BodyGenotypeV2, BrainGenotypeCpg):
"""A genotype for a body and brain using CPPN."""

@classmethod
def random(
cls,
innov_db_body: multineat.InnovationDatabase,
innov_db_brain: multineat.InnovationDatabase,
rng: np.random.Generator,
) -> Genotype:
"""
Create a random genotype.

:param innov_db_body: Multineat innovation database for the body. See Multineat library.
:param innov_db_brain: Multineat innovation database for the brain. See Multineat library.
:param rng: Random number generator.
:returns: The created genotype.
"""
body = cls.random_body(innov_db_body, rng)
brain = cls.random_brain(innov_db_brain, rng)

return Genotype(body=body.body, brain=brain.brain)

def develop(self, visualize: bool = False) -> ModularRobot:
"""
Develop the genotype into a modular robot.

:param visualize: Wether to plot the mapping from genotype to phenotype.
:returns: The created robot.
"""
body = self.develop_body(visualize=visualize)
brain = self.develop_brain(body=body)
return ModularRobot(body=body, brain=brain)
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
"""Individual class."""

from dataclasses import dataclass

from genotype import Genotype


@dataclass
class Individual:
"""An individual in a population."""

genotype: Genotype
fitness: float
Original file line number Diff line number Diff line change
@@ -0,0 +1,194 @@
"""Main script for the example."""

import logging
import random

import config
import multineat
import numpy as np
from evaluator import Evaluator
from genotype import Genotype
from individual import Individual
from pyrr import Vector3
from scipy.spatial import distance
from sklearn.neighbors import KDTree

from revolve2.experimentation.logging import setup_logging
from revolve2.experimentation.rng import make_rng_time_seed
from revolve2.modular_robot import ModularRobot
from revolve2.modular_robot_simulation import ModularRobotScene, simulate_scenes
from revolve2.simulation.scene import Pose
from revolve2.simulators.mujoco_simulator import LocalSimulator
from revolve2.standards import terrains
from revolve2.standards.morphological_measures import MorphologicalMeasures
from revolve2.standards.simulation_parameters import make_standard_batch_parameters


def calculate_morphological_features(robot: ModularRobot) -> dict[str, float]:
"""
Calculate the morphological features for a given robot using the MorphologicalMeasures class.

:param robot: A ModularRobot object.
:returns: A dictionary with the calculated morphological features.
"""
measures: MorphologicalMeasures[np.generic] = MorphologicalMeasures(
robot.body
) # Explore this class and the referenced paper to learn more about the traits
features = {
"symmetry": measures.symmetry,
"proportion": (
measures.proportion_2d if measures.is_2d else 0
), # Use 0 for 3D robots
"coverage": measures.coverage,
"extremities_prop": measures.limbs,
"extensiveness_prop": measures.length_of_limbs,
"hinge_prop": (
measures.num_active_hinges / measures.num_modules
if measures.num_modules > 0
else 0
),
"hinge_ratio": (
measures.num_active_hinges / measures.num_bricks
if measures.num_bricks > 0
else 0
),
"branching_prop": measures.branching,
}
return features


def calculate_euclidean_diversity(
morphological_features: list[dict[str, float]]
) -> float:
"""
Calculate the Euclidean diversity of the population using morphological features.

:param morphological_features: List of morphological features for each robot in the population.
:returns: The average Euclidean diversity of the population.
"""
n = len(morphological_features)
total_distance = 0
count = 0

# Convert feature dictionaries to feature vectors
feature_vectors = [list(features.values()) for features in morphological_features]

# Calculate pairwise Euclidean distances
for i in range(n):
for j in range(i + 1, n):
dist = distance.euclidean(feature_vectors[i], feature_vectors[j])
total_distance += dist
count += 1

# Average Euclidean distance across all pairs
avg_euclidean_diversity = total_distance / count if count > 0 else 0
return avg_euclidean_diversity


def calculate_kdtree_diversity(morphological_features: list[dict[str, float]]) -> float:
"""
Calculate the diversity of the population using KDTree for nearest neighbors.

:param morphological_features: List of morphological features for each robot in the population.
:returns: The average KDTree-based diversity of the population.
"""
# Convert feature dictionaries to feature vectors and buld a Kdtree
feature_vectors = [list(features.values()) for features in morphological_features]
kdtree = KDTree(feature_vectors, leaf_size=30, metric="euclidean")

# Compute the distances of each robot to its k nearest neighbors
k = (
len(morphological_features) - 1
) # Using k-1 because the nearest neighbor is the point itself
distances, _ = kdtree.query(feature_vectors, k=k)

# Calculate the average diversity as the mean distance to nearest neighbors
avg_kdtree_diversity: float = np.mean([np.mean(dist) for dist in distances])

return avg_kdtree_diversity


def main() -> None:
"""Run the simulation."""
# Set up logging.
setup_logging()

# Set up the random number generator and databases.
rng = make_rng_time_seed()
innov_db_body = multineat.InnovationDatabase()
innov_db_brain = multineat.InnovationDatabase()

# Create an initial population.
logging.info("Generating initial population.")
initial_genotypes = [
Genotype.random(
innov_db_body=innov_db_body,
innov_db_brain=innov_db_brain,
rng=rng,
)
for _ in range(config.POPULATION_SIZE)
]

# You can choose to not evaluate the robots if all you want is to visualize the morphologies or compute diversity to save time
if config.EVALUATE:
logging.info("Evaluating initial population.")
initial_fitnesses = Evaluator(
headless=True, num_simulators=config.NUM_SIMULATORS
).evaluate(initial_genotypes)
else:
initial_fitnesses = [
random.uniform(0.0, 1.0) for _ in range(len(initial_genotypes))
]

# Create a population of individuals, combining genotype with fitness.
population = [
Individual(genotype, fitness)
for genotype, fitness in zip(initial_genotypes, initial_fitnesses, strict=True)
]

# Create the robot bodies from the genotypes of the population
robots = [
individual.genotype.develop(config.VISUALIZE_MAP) for individual in population
]

# Calculate the morphological features for each robot in the population
morphological_features = []
for robot in robots:
features = calculate_morphological_features(robot)
morphological_features.append(features)

# Calculate the Euclidean-based morphological diversity of the population
euclidean_diversity = calculate_euclidean_diversity(morphological_features)
print(
f"Euclidean-based morphological diversity of the population: {euclidean_diversity}"
)

# Calculate the KDTree-based morphological diversity of the population
kdtree_diversity = calculate_kdtree_diversity(morphological_features)
print(f"KDTree-based morphological diversity of the population: {kdtree_diversity}")

# Now we can create a scene and add the robots by mapping the genotypes to phenotypes
scene = ModularRobotScene(terrain=terrains.flat())
i = 0
for individual in robots:
# By changing the value of "VISUALIZE_MAP" to true you can plot the body creation process
scene.add_robot(individual, pose=Pose(Vector3([i, 0.0, 0.0])))
i += 1

# Declare the simulators and simulation parameters.
simulator = LocalSimulator(viewer_type="custom", headless=False)
batch_parameters = make_standard_batch_parameters()
batch_parameters.simulation_time = 1000
batch_parameters.simulation_timestep = 0.01
batch_parameters.sampling_frequency = 10

# Finally simulate the scenes to inspect the resulting morphologies
simulate_scenes(
simulator=simulator,
batch_parameters=batch_parameters,
scenes=scene,
)


if __name__ == "__main__":
main()
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
scikit-learn>=1.5.2
1 change: 1 addition & 0 deletions examples/4_example_experiment_setups/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,5 @@ Additionally you will learn hwo to use the evolution abstraction layer in Revolv
- `4d_robot_bodybrain_ea_database` is the same example, with the addition of databases to allow for data storage.
- If you want to add learning into your experiments look at `4e_robot_brain_cmaes`, in which we add learning to a **single** robot.
- `4f_robot_brain_cmaes_database` does the same thing as the previous example with the addition of a database.
- Finally you can learn about the exploration of the initial population and their morphological features in `4g_explore_initial_population`

Loading
Loading