Repository: ci-group/revolve2 (language: python)

Issue:
Streamline Examples
- [x] Investigate ordering of examples based on progression
- [x] Make example structures less cluttered
- [x] Group examples into bigger groups?
- [ ] EA and db EA could be merged into one to avoid confusion
- [x] Improve documentation of examples
- [x] Make them use the new Evolution Abstraction !!! 


Discussion / hints:

Fix patch:
diff --git a/examples/simulate_single_robot/README.md b/examples/1_simulator_basics/1a_simulate_single_robot/README.md
similarity index 100%
rename from examples/simulate_single_robot/README.md
rename to examples/1_simulator_basics/1a_simulate_single_robot/README.md
diff --git a/examples/simulate_single_robot/main.py b/examples/1_simulator_basics/1a_simulate_single_robot/main.py
similarity index 67%
rename from examples/simulate_single_robot/main.py
rename to examples/1_simulator_basics/1a_simulate_single_robot/main.py
index 1e8927235..2d1be5601 100644
--- a/examples/simulate_single_robot/main.py
+++ b/examples/1_simulator_basics/1a_simulate_single_robot/main.py
@@ -1,6 +1,9 @@
 """Main script for the example."""
 
+from pyrr import Vector3
+
 from revolve2.ci_group import terrains
+from revolve2.ci_group.interactive_objects import Ball
 from revolve2.ci_group.simulation_parameters import make_standard_batch_parameters
 from revolve2.experimentation.logging import setup_logging
 from revolve2.experimentation.rng import make_rng_time_seed
@@ -9,6 +12,7 @@
 from revolve2.modular_robot.body.v2 import ActiveHingeV2, BodyV2, BrickV2
 from revolve2.modular_robot.brain.cpg import BrainCpgNetworkNeighborRandom
 from revolve2.modular_robot_simulation import ModularRobotScene, simulate_scenes
+from revolve2.simulation.scene import Pose
 from revolve2.simulators.mujoco_simulator import LocalSimulator
 
 
@@ -35,27 +39,41 @@ def make_body() -> BodyV2:
 
 def main() -> None:
     """Run the simulation."""
-    # Set up logging.
+    # Set up logging to give output of your simulation into the command line interface (CLI).
     setup_logging()
 
-    # Set up a random number generator, used later.
+    # Set up a random number generator, used later for the brain.
     rng = make_rng_time_seed()
 
     # Create a body for the robot.
     body = make_body()
-    # Create a brain for the robot.
-    # We choose a 'CPG' brain with random parameters (the exact working will not be explained here).
+
+    """
+    Here we create a brain for the robot.
+    We choose a 'CPG' brain with random parameters.
+    If you want to know more about CPGs checkout the Methods section in: https://doi.org/10.1038/s41598-023-48338-4. 
+    """
     brain = BrainCpgNetworkNeighborRandom(body=body, rng=rng)
-    # Combine the body and brain into a modular robot.
+
+    """Once we have a body and a brain we combine it into a ModularRobot."""
     robot = ModularRobot(body, brain)
 
-    # Create a modular robot scene.
-    # This is a combination of one or more modular robots positioned in a given terrain.
+    """
+    To simulate our newly created robot, we create a modular robot scene.
+    This scene is a combination of one or more modular robots positioned in a given terrain.
+    """
     scene = ModularRobotScene(terrain=terrains.flat())
     scene.add_robot(robot)
 
-    # Create a simulator that will perform the simulation.
-    # This tutorial chooses to use Mujoco, but your version of revolve might contain other simulators as well.
+    # Additionally to robots you can also add interactive objects to the scene.
+    scene.add_interactive_object(
+        Ball(radius=0.1, mass=0.1, pose=Pose(Vector3([-0.5, 0.5, 0])))
+    )
+
+    """
+    After we have the scene ready we create a simulator that will perform the simulation.
+    This tutorial chooses to use Mujoco, but your version of revolve might contain other simulators as well.
+    """
     simulator = LocalSimulator()
 
     # `batch_parameters` are important parameters for simulation.
diff --git a/examples/brain_with_feedback/requirements.txt b/examples/1_simulator_basics/1a_simulate_single_robot/requirements.txt
similarity index 100%
rename from examples/brain_with_feedback/requirements.txt
rename to examples/1_simulator_basics/1a_simulate_single_robot/requirements.txt
diff --git a/examples/custom_terrain/README.md b/examples/1_simulator_basics/1b_custom_terrain/README.md
similarity index 63%
rename from examples/custom_terrain/README.md
rename to examples/1_simulator_basics/1b_custom_terrain/README.md
index a8128f035..cdd69de9a 100644
--- a/examples/custom_terrain/README.md
+++ b/examples/1_simulator_basics/1b_custom_terrain/README.md
@@ -1,6 +1,7 @@
 In this example you create a custom terrain to put a modular robot in.
+Other examples for possible terrains can be found in `ci_group.terrains`
 
-Make sure you understand simulation, for example through the example 'simulate_single_robot'.
+Make sure you understand simulation, for example through the example `1a_simulate_single_robot`.
 
 You learn:
 - How to create a custom terrain.
diff --git a/examples/custom_terrain/main.py b/examples/1_simulator_basics/1b_custom_terrain/main.py
similarity index 100%
rename from examples/custom_terrain/main.py
rename to examples/1_simulator_basics/1b_custom_terrain/main.py
diff --git a/examples/compare_simulated_and_physical_robot/requirements.txt b/examples/1_simulator_basics/1b_custom_terrain/requirements.txt
similarity index 100%
rename from examples/compare_simulated_and_physical_robot/requirements.txt
rename to examples/1_simulator_basics/1b_custom_terrain/requirements.txt
diff --git a/examples/1_simulator_basics/README.md b/examples/1_simulator_basics/README.md
new file mode 100644
index 000000000..b3fa697f2
--- /dev/null
+++ b/examples/1_simulator_basics/README.md
@@ -0,0 +1,6 @@
+## 1) Start with the basics.
+
+In this collection of examples you will learn the basics of the simulator used for Revolve2.
+
+- In `1a_simulate_single_robot` you will learn how to simulate robots using Revolve2.
+- In `1b_custom_terrain` you learn how to make a terrain that you can use for your experiments.
diff --git a/examples/custom_brain/README.md b/examples/2_modular_robot_basics/2a_custom_brain/README.md
similarity index 84%
rename from examples/custom_brain/README.md
rename to examples/2_modular_robot_basics/2a_custom_brain/README.md
index 0bba583ff..4e8d7367b 100644
--- a/examples/custom_brain/README.md
+++ b/examples/2_modular_robot_basics/2a_custom_brain/README.md
@@ -1,6 +1,6 @@
 In this example you create a custom brain that controls a modular robot.
 
-Make sure you understand simulation, for example through the example 'simulate_single_robot'.
+Make sure you understand simulation, for example through the example `1a_simulate_single_robot`.
 
 You learn:
 - How to create a custom brain.
diff --git a/examples/custom_brain/main.py b/examples/2_modular_robot_basics/2a_custom_brain/main.py
similarity index 97%
rename from examples/custom_brain/main.py
rename to examples/2_modular_robot_basics/2a_custom_brain/main.py
index 996b1e78c..89f57d513 100644
--- a/examples/custom_brain/main.py
+++ b/examples/2_modular_robot_basics/2a_custom_brain/main.py
@@ -60,7 +60,7 @@ def __init__(
         second_right_active_hinge: ActiveHingeV2,
     ) -> None:
         """
-        Initialize the Object.
+        Initialize the Custom Brain Instance.
 
         :param first_left_active_hinge: First left active Hinge.
         :param second_left_active_hinge: Second left active Hinge.
@@ -79,7 +79,7 @@ def control(
         control_interface: ModularRobotControlInterface,
     ) -> None:
         """
-        Control the modular robot.
+        Control the modular robot using our custom brain.
 
         :param dt: Elapsed seconds since last call to this function.
         :param sensor_state: Interface for reading the current sensor state.
@@ -112,7 +112,7 @@ def __init__(
         second_right_active_hinge: ActiveHingeV2,
     ) -> None:
         """
-        Initialize the Object.
+        Initialize the Custom Brain.
 
         :param first_left_active_hinge: First left active Hinge.
         :param second_left_active_hinge: Second left active Hinge.
@@ -152,7 +152,7 @@ def main() -> None:
         second_right_active_hinge,
     ) = make_body()
 
-    # Create a brain for the robot.
+    # Create the custom brain for the robot.
     brain = CustomBrain(
         first_left_active_hinge,
         second_left_active_hinge,
diff --git a/examples/custom_brain/requirements.txt b/examples/2_modular_robot_basics/2a_custom_brain/requirements.txt
similarity index 100%
rename from examples/custom_brain/requirements.txt
rename to examples/2_modular_robot_basics/2a_custom_brain/requirements.txt
diff --git a/examples/brain_with_feedback/README.md b/examples/2_modular_robot_basics/2b_brain_with_feedback/README.md
similarity index 100%
rename from examples/brain_with_feedback/README.md
rename to examples/2_modular_robot_basics/2b_brain_with_feedback/README.md
diff --git a/examples/brain_with_feedback/main.py b/examples/2_modular_robot_basics/2b_brain_with_feedback/main.py
similarity index 93%
rename from examples/brain_with_feedback/main.py
rename to examples/2_modular_robot_basics/2b_brain_with_feedback/main.py
index 8c617fd4f..968d58ea0 100644
--- a/examples/brain_with_feedback/main.py
+++ b/examples/2_modular_robot_basics/2b_brain_with_feedback/main.py
@@ -49,6 +49,7 @@ def control(
         :param sensor_state: Interface for reading the current sensor state.
         :param control_interface: Interface for controlling the robot.
         """
+        # To get data from you sensors you need the sensor itself, with which you can query the sensor stare from the ModularRobotSensorState object.
         # Get the sensors from the active hinges
         sensors = [
             active_hinge.sensors.active_hinge_sensor
@@ -66,7 +67,7 @@ def control(
         ]
         logging.info(f"current positions: {current_positions}")
 
-        # Get the imu state
+        # Get the imu sensor state
         imu_state = sensor_state.get_imu_sensor_state(self.imu_sensor)
         logging.info(f"orientation: {imu_state.orientation}")
         logging.info(f"angular rate: {imu_state.angular_rate}")
@@ -119,7 +120,8 @@ def main() -> None:
     # Create a body for the robot.
     body = modular_robots_v2.gecko_v2()
 
-    # Add IMU Sensor to the core.
+    """Every module on the robot can have sensors, to add them you do the following: """
+    # Add an IMU Sensor to the core.
     body.core.add_sensor(imu := IMUSensor(position=Vector3([0.075, 0.075, 0.14])))
 
     # Create a brain for the robot.
diff --git a/examples/custom_terrain/requirements.txt b/examples/2_modular_robot_basics/2b_brain_with_feedback/requirements.txt
similarity index 100%
rename from examples/custom_terrain/requirements.txt
rename to examples/2_modular_robot_basics/2b_brain_with_feedback/requirements.txt
diff --git a/examples/2_modular_robot_basics/README.md b/examples/2_modular_robot_basics/README.md
new file mode 100644
index 000000000..3bf3c78c4
--- /dev/null
+++ b/examples/2_modular_robot_basics/README.md
@@ -0,0 +1,9 @@
+## 2) Learn about the Modular Robots
+
+Revolve2 is a framework, which is designed towards modular robot evolution, meaning robots are a collection of arbitrary modules.
+The Morphology (Body) of these robots can be evolved aswell as their Controller (Brain).
+
+In this collection of examples you will learn the following:
+
+- In `2a_custom_brain` you will walk through the process of making a custom brain to use for your robots.
+- In `2b_brain_with_feedback` you will learn how a brain can use sensors on the robots for some advanced control.
diff --git a/examples/experiment_setup/README.md b/examples/3_experiment_foundations/3a_experiment_setup/README.md
similarity index 85%
rename from examples/experiment_setup/README.md
rename to examples/3_experiment_foundations/3a_experiment_setup/README.md
index 6efb1c7d8..8b6e951e2 100644
--- a/examples/experiment_setup/README.md
+++ b/examples/3_experiment_foundations/3a_experiment_setup/README.md
@@ -1,4 +1,5 @@
 In this example you set up a trivial experiment with muliple repetitions.
+We will sample from a distribution given a few parameters and determine the ratio of success
 To change the experiments parameters, adjust the values in `config.py`.
 
 You learn:
diff --git a/examples/experiment_setup/config.py b/examples/3_experiment_foundations/3a_experiment_setup/config.py
similarity index 100%
rename from examples/experiment_setup/config.py
rename to examples/3_experiment_foundations/3a_experiment_setup/config.py
diff --git a/examples/3_experiment_foundations/3a_experiment_setup/main.py b/examples/3_experiment_foundations/3a_experiment_setup/main.py
new file mode 100644
index 000000000..e0340fc81
--- /dev/null
+++ b/examples/3_experiment_foundations/3a_experiment_setup/main.py
@@ -0,0 +1,104 @@
+"""Main script for the example."""
+
+import logging
+
+import config
+import numpy as np
+
+import revolve2.experimentation.rng
+from revolve2.experimentation.logging import setup_logging
+
+
+def run_experiment(num_samples: int, probability: float) -> None:
+    """
+    Run all runs of an experiment using the provided parameters.
+
+    :param num_samples: The number of samples to use for this experiment.
+    :param probability: The probablity to use for this experiment.
+    """
+    success_ratios = (
+        []
+    )  # Create a list where we will store the success ratio for each repetition.
+
+    for repetition in range(config.NUM_REPETITIONS):
+        logging.info(
+            f"Running experiment ( repetition {repetition} num_samples {num_samples} probability {probability} )"
+        )
+
+        """
+        First, we set up a random number generator, or 'rng'.
+        A rng is not actually random; it is an algorithm creating semi-randomness from a given starting number (the 'seed'), defined by you.
+        This is both very useful and a pitfall.
+        Using the same seed makes our experiment reproducible,
+        but using the same seed between two seperate experiments will generate the same numbers (usually bad).
+        As such we create a unique seed for each experiment and run.
+        We will do this by creating a seed based on the current time.
+        This seed will be stored in the logs.
+        If we ever need to reproduce our results we can then use the seed from the log instead of generating a new one.
+        The function automatically logs the seed for you, so you cannot forget to.
+        """
+        rng = revolve2.experimentation.rng.make_rng_time_seed()
+
+        # If you run with a set seed, use the following lines instead.
+        # SEED = 1234
+        # rng = revolve2.experimentation.rng.make_rng(SEED)
+
+        samples = rng.binomial(
+            n=1, p=probability, size=num_samples
+        )  # Perform the experiment, sampling from a binomial distribution using the given paramaters.
+        success_ratios.append(
+            np.sum(samples == 1) / num_samples
+        )  # Calculate the ratio of success and save it.
+
+    # Do some simple analysis using the performed repetitions.
+    std = np.std(success_ratios)
+    mean = np.mean(success_ratios)
+    logging.info(f"mean {mean} std {std}")
+
+
+def main() -> None:
+    """Run the simulation."""
+    """
+    First we set up the logging.
+    
+    This decides the level of severity of logged messages we want to display.
+    By default this is 'INFO' or more severe, and 'DEBUG' is excluded.
+    Furthermore, a standard message layout is set up.
+    If logging is not set up, important messages can be missed.
+    We also provide a file name, so the log will be written to both the console and that file.
+    """
+    setup_logging(file_name="log.txt")
+
+    # Let's print a simple message.
+    # We use the 'info' function to give the message the 'INFO' severity.
+    logging.info("Starting program.")
+    # The following message will be invisible, because it is 'DEBUG' severity.
+    logging.debug(
+        "This debug message is invisible, because we set our visible severity to 'INFO' and higher."
+    )
+
+    """
+    Our experiment will be very trivial; sample a number of times with a certain probability and determine the ratio of success.
+    Several probabilities and number of samples will be observed, and each experiment will be repeated a few times.
+    These parameters are defined in 'config.py'.
+    Iterate over all parameters.
+    """
+    for num_samples in config.NUM_SAMPLES:
+        for probability in config.PROBABILITIES:
+            # And run the experiment.
+            run_experiment(
+                num_samples=num_samples,
+                probability=probability,
+            )
+
+
+if __name__ == "__main__":
+    """
+    This clause is a very important idiom in python that you should understand as a programmer.
+    __name__ is only set to __main__ if it is directly ran as a script.
+    If the file is imported from another file the main function is not called.
+    If you do not perform this check the main function is ALWAYS called.
+    This leads to all kinds of unintended behaviour that is hard to debug.
+    See also various articles, such as https://stackoverflow.com/questions/419163/what-does-if-name-main-do
+    """
+    main()
diff --git a/examples/evaluate_multiple_interacting_robots/requirements.txt b/examples/3_experiment_foundations/3a_experiment_setup/requirements.txt
similarity index 100%
rename from examples/evaluate_multiple_interacting_robots/requirements.txt
rename to examples/3_experiment_foundations/3a_experiment_setup/requirements.txt
diff --git a/examples/evaluate_single_robot/README.md b/examples/3_experiment_foundations/3b_evaluate_single_robot/README.md
similarity index 50%
rename from examples/evaluate_single_robot/README.md
rename to examples/3_experiment_foundations/3b_evaluate_single_robot/README.md
index 4f372621b..8cd476217 100644
--- a/examples/evaluate_single_robot/README.md
+++ b/examples/3_experiment_foundations/3b_evaluate_single_robot/README.md
@@ -1,6 +1,7 @@
+No experiment is useful without some way to evaluate your results.
 In this example you simulate a single modular robot, and then calculate its xy displacement.
 
-To understand simulation, first see the 'simulate_single_robot' example.
+To understand simulation, first see the `1a_simulate_single_robot` example.
 
 You learn:
 - How to process simulation results.
diff --git a/examples/evaluate_single_robot/main.py b/examples/3_experiment_foundations/3b_evaluate_single_robot/main.py
similarity index 82%
rename from examples/evaluate_single_robot/main.py
rename to examples/3_experiment_foundations/3b_evaluate_single_robot/main.py
index bd2073f62..6106a9af4 100644
--- a/examples/evaluate_single_robot/main.py
+++ b/examples/3_experiment_foundations/3b_evaluate_single_robot/main.py
@@ -22,7 +22,6 @@ def main() -> None:
 
     # Create the robot.
     body = modular_robots_v2.gecko_v2()
-
     brain = BrainCpgNetworkNeighborRandom(body=body, rng=rng)
     robot = ModularRobot(body, brain)
 
@@ -42,15 +41,19 @@ def main() -> None:
         scenes=scene,
     )
 
+    """
+    Using the previously obtained scene_states we can now start to evaluate our robot.
+    Note in this example we simply use x-y displacement, but you can do any other way of evaluation as long as the required data is in the scene states.
+    """
     # Get the state at the beginning and end of the simulation.
     scene_state_begin = scene_states[0]
     scene_state_end = scene_states[-1]
 
-    # Retrieve the state of the modular robot.
+    # Retrieve the state of the modular robot, which also contains the location of the robot.
     robot_state_begin = scene_state_begin.get_modular_robot_simulation_state(robot)
     robot_state_end = scene_state_end.get_modular_robot_simulation_state(robot)
 
-    # Calculate the xy displacement.
+    # Calculate the xy displacement, using the locations of the robot.
     xy_displacement = fitness_functions.xy_displacement(
         robot_state_begin, robot_state_end
     )
diff --git a/examples/evaluate_multiple_isolated_robots/requirements.txt b/examples/3_experiment_foundations/3b_evaluate_single_robot/requirements.txt
similarity index 100%
rename from examples/evaluate_multiple_isolated_robots/requirements.txt
rename to examples/3_experiment_foundations/3b_evaluate_single_robot/requirements.txt
diff --git a/examples/3_experiment_foundations/3c_evaluate_multiple_isolated_robots/README.md b/examples/3_experiment_foundations/3c_evaluate_multiple_isolated_robots/README.md
new file mode 100644
index 000000000..27ec69b15
--- /dev/null
+++ b/examples/3_experiment_foundations/3c_evaluate_multiple_isolated_robots/README.md
@@ -0,0 +1,7 @@
+This example simulates modular robots that do not interact with each other, and then calculates their xy displacement.
+
+To understand better, first see the '3b_evaluate_single_robot' example.
+
+You learn:
+- How to simulate multiple isolated robots.
+- How to process their simulation results.
diff --git a/examples/evaluate_multiple_isolated_robots/main.py b/examples/3_experiment_foundations/3c_evaluate_multiple_isolated_robots/main.py
similarity index 78%
rename from examples/evaluate_multiple_isolated_robots/main.py
rename to examples/3_experiment_foundations/3c_evaluate_multiple_isolated_robots/main.py
index 235dda79e..a601dca53 100644
--- a/examples/evaluate_multiple_isolated_robots/main.py
+++ b/examples/3_experiment_foundations/3c_evaluate_multiple_isolated_robots/main.py
@@ -40,10 +40,13 @@ def main() -> None:
         scene.add_robot(robot)
         scenes.append(scene)
 
-    # Create the simulator.
-    # A simulator can run multiple scene in parallel.
-    # For the MuJoCo simulator, we can control this using the 'num_simulators' argument.
-    # Increasing this number causes more simulations to run at the same time.
+    """
+    Now we create a simulator.
+    A simulator can run multiple scene in parallel, and as such is very useful for simulating populations of robots..
+    For the MuJoCo simulator, we can control this using the 'num_simulators' argument.
+    Increasing this number causes more simulations to run at the same time.
+    Note that for optimal performance do not use more simulators than cpu cores available on your machine.
+    """
     simulator = LocalSimulator(headless=True, num_simulators=4)
 
     # Simulate all scenes.
@@ -53,7 +56,7 @@ def main() -> None:
         scenes=scenes,
     )
 
-    # Calculate the xy displacements.
+    # Calculate the xy displacements for all robots in the scenes we just simulated.
     xy_displacements = [
         fitness_functions.xy_displacement(
             states[0].get_modular_robot_simulation_state(robot),
diff --git a/examples/evaluate_single_robot/requirements.txt b/examples/3_experiment_foundations/3c_evaluate_multiple_isolated_robots/requirements.txt
similarity index 100%
rename from examples/evaluate_single_robot/requirements.txt
rename to examples/3_experiment_foundations/3c_evaluate_multiple_isolated_robots/requirements.txt
diff --git a/examples/3_experiment_foundations/3d_evaluate_multiple_interacting_robots/README.md b/examples/3_experiment_foundations/3d_evaluate_multiple_interacting_robots/README.md
new file mode 100644
index 000000000..cc25cd33d
--- /dev/null
+++ b/examples/3_experiment_foundations/3d_evaluate_multiple_interacting_robots/README.md
@@ -0,0 +1,9 @@
+In this example you simulate a multiple robots in the same scene, and then calculate their x-y displacement.
+Since the robots are within one scene, they can interact with one another.
+
+First see the `3c_evaluate_multiple_isolated_robots` example.
+This example is extremely similar, but will ommit some of the commentary.
+
+You learn:
+- How to put multiple robots in the same scene.
+- How to process the simulation results.
diff --git a/examples/evaluate_multiple_interacting_robots/main.py b/examples/3_experiment_foundations/3d_evaluate_multiple_interacting_robots/main.py
similarity index 90%
rename from examples/evaluate_multiple_interacting_robots/main.py
rename to examples/3_experiment_foundations/3d_evaluate_multiple_interacting_robots/main.py
index ce0fabe46..1f721dd21 100644
--- a/examples/evaluate_multiple_interacting_robots/main.py
+++ b/examples/3_experiment_foundations/3d_evaluate_multiple_interacting_robots/main.py
@@ -33,8 +33,10 @@ def main() -> None:
     brains = [BrainCpgNetworkNeighborRandom(body, rng) for body in bodies]
     robots = [ModularRobot(body, brain) for body, brain in zip(bodies, brains)]
 
-    # Create the scene and put all robots in it.
-    # We place the robots at seperate locations in the terrain so they do not overlap at the start of the simulation.
+    """
+    Contrary to the previous examples, we now create a single scene and put all robots in it. 
+    We place the robots at separate locations in the terrain so they do not overlap at the start of the simulation.
+    """
     scene = ModularRobotScene(terrain=terrains.flat())
     poses = [
         Pose(Vector3([1.0, 0.0, 0.0])),
diff --git a/examples/experiment_setup/requirements.txt b/examples/3_experiment_foundations/3d_evaluate_multiple_interacting_robots/requirements.txt
similarity index 100%
rename from examples/experiment_setup/requirements.txt
rename to examples/3_experiment_foundations/3d_evaluate_multiple_interacting_robots/requirements.txt
diff --git a/examples/3_experiment_foundations/README.md b/examples/3_experiment_foundations/README.md
new file mode 100644
index 000000000..1770c7328
--- /dev/null
+++ b/examples/3_experiment_foundations/README.md
@@ -0,0 +1,8 @@
+## 3) Learn how to properly setup experiments
+Simulating robots on its own is not that useful, so we actually want to make experiments that show us cool new things we did not know before.
+Here you will learn about a few things that are important in almost all experimental setups.
+
+- In `3a_experiment_setup` you will learn a way to structure a basic experiment in revolve2. Make sure to understand this example well.
+- 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`.
diff --git a/examples/simple_ea_xor/README.md b/examples/4_example_experiment_setups/4a_simple_ea_xor/README.md
similarity index 85%
rename from examples/simple_ea_xor/README.md
rename to examples/4_example_experiment_setups/4a_simple_ea_xor/README.md
index ee2cc0543..b080c8185 100644
--- a/examples/simple_ea_xor/README.md
+++ b/examples/4_example_experiment_setups/4a_simple_ea_xor/README.md
@@ -1,6 +1,6 @@
 Here you will optimize a neural network to calculate XOR, using an evolutionary algorithm.
 
-To better understand, first look at the `experiment_setup` example.
+To better understand, first look at the `3a_experiment_setup` example.
 
 You learn:
 - How to create a simple evolutionary loop.
diff --git a/examples/simple_ea_xor/config.py b/examples/4_example_experiment_setups/4a_simple_ea_xor/config.py
similarity index 100%
rename from examples/simple_ea_xor/config.py
rename to examples/4_example_experiment_setups/4a_simple_ea_xor/config.py
diff --git a/examples/4_example_experiment_setups/4a_simple_ea_xor/evaluate.py b/examples/4_example_experiment_setups/4a_simple_ea_xor/evaluate.py
new file mode 100644
index 000000000..7bd2c56e3
--- /dev/null
+++ b/examples/4_example_experiment_setups/4a_simple_ea_xor/evaluate.py
@@ -0,0 +1,60 @@
+"""Evaluation functions."""
+
+import numpy as np
+import numpy.typing as npt
+from genotype import Genotype
+
+from revolve2.experimentation.evolution.abstract_elements import Evaluator as Eval
+
+
+class Evaluator(Eval):
+    """Here we make an evaluator object, based on the abstract evaluator defined in Revolve2."""
+
+    @staticmethod
+    def evaluate_network(
+        params: npt.NDArray[np.float_], inputs: npt.NDArray[np.float_]
+    ) -> np.float_:
+        """
+        Pass two inputs through a fully connected relu network.
+
+        :param params: The parameters to evaluate.
+        :param inputs: Inputs for network. 2x1 floats.
+        :returns: The output of the network.
+        """
+        # First layer
+        n0 = np.maximum(0, np.dot(params[:2], inputs) + params[2])
+        n1 = np.maximum(0, np.dot(params[3:5], inputs) + params[5])
+
+        # Second layer
+        output: np.float_ = np.maximum(0, n0 * params[6] + n1 * params[7] + params[8])
+        return output
+
+    def evaluate(self, population: list[Genotype]) -> list[float]:
+        """
+        Measure one set of parameters.
+
+        :param population: The population of parameters to measure.
+        :returns: Negative sum of squared errors and each individual error. 5x1 floats.
+        """
+        results = []
+        for genotype in population:
+            # Define all possible inputs for xor and the expected outputs
+            inputs = np.array([[0, 0], [1, 0], [0, 1], [1, 1]])
+            expected_outputs = np.array([0, 1, 1, 0])
+
+            # Evaluate the provided network parameters
+            outputs = np.array(
+                [self.evaluate_network(genotype.parameters, input) for input in inputs]
+            )
+
+            # Calculate the difference between the network outputs and the expect outputs
+            errors = outputs - expected_outputs
+
+            """
+            Now we add the sum of squared errors to the results.
+            In our case 0 would be an optimal result.
+            We invert so we can maximize the fitness instead of minimize.
+            Finally we convert from a numpy float_ type to the python float type. This is not really important.
+            """
+            results.append(float(-np.sum(errors**2)))
+        return results
diff --git a/examples/simple_ea_xor/genotype.py b/examples/4_example_experiment_setups/4a_simple_ea_xor/genotype.py
similarity index 100%
rename from examples/simple_ea_xor/genotype.py
rename to examples/4_example_experiment_setups/4a_simple_ea_xor/genotype.py
diff --git a/examples/robot_bodybrain_ea/individual.py b/examples/4_example_experiment_setups/4a_simple_ea_xor/individual.py
similarity index 100%
rename from examples/robot_bodybrain_ea/individual.py
rename to examples/4_example_experiment_setups/4a_simple_ea_xor/individual.py
diff --git a/examples/4_example_experiment_setups/4a_simple_ea_xor/main.py b/examples/4_example_experiment_setups/4a_simple_ea_xor/main.py
new file mode 100644
index 000000000..b1c088e3a
--- /dev/null
+++ b/examples/4_example_experiment_setups/4a_simple_ea_xor/main.py
@@ -0,0 +1,221 @@
+"""Main script for the example."""
+
+import logging
+from typing import Any
+
+import config
+import numpy as np
+from evaluate import Evaluator
+from genotype import Genotype
+from individual import Individual
+from numpy.typing import NDArray
+
+from revolve2.experimentation.evolution.abstract_elements import Reproducer, Selector
+from revolve2.experimentation.logging import setup_logging
+from revolve2.experimentation.optimization.ea import population_management, selection
+from revolve2.experimentation.rng import make_rng_time_seed
+
+
+class ParentSelector(Selector):
+    """Here we create a selector object that helps us select the parents for reproduction."""
+
+    _rng: np.random.Generator
+    _offspring_size: int
+
+    def __init__(self, offspring_size: int) -> None:
+        """
+        Initialize the ParentSelector object.
+
+        :param offspring_size: The size of the offspring for selection.
+        """
+        self._rng = make_rng_time_seed()
+        self._offspring_size = offspring_size
+
+    def select(
+        self, population: list[Individual], **kwargs: Any
+    ) -> tuple[NDArray[np.int_], dict[str, list[Individual]]]:
+        """
+        Select pairs of parents using a tournament.selection procedure.
+
+        :param population: The population to select from.
+        :param kwargs: Additional kwargs that are not used in this example.
+        :returns: Pairs of indices of selected parents. offspring_size x 2 ints, and the parent population in the KWArgs dict.
+        """
+        final_selection = np.asarray(
+            [
+                selection.multiple_unique(
+                    2,
+                    [individual.genotype for individual in population],
+                    [individual.fitness for individual in population],
+                    lambda _, fitnesses: selection.tournament(
+                        self._rng, fitnesses, k=1
+                    ),
+                )
+                for _ in range(self._offspring_size)
+            ]
+        )
+        """We select not the parents directly, but their respective indices for the reproduction step."""
+        return final_selection, {"parent_population": population}
+
+
+class SurvivorSelector(Selector):
+    """Here we make a selector object that allows us to select survivor after evaluation."""
+
+    _rng: np.random.Generator
+
+    def __init__(self) -> None:
+        """Initialize the selector."""
+        self._rng = make_rng_time_seed()
+
+    def select(
+        self, population: list[Individual], **kwargs: Any
+    ) -> tuple[list[Individual], dict[Any, Any]]:
+        """
+        Select survivors using a tournament selection.
+
+        :param population: The initial population.
+        :param kwargs: Additional kwargs that contain the children to do selection with.
+        :returns: A newly created population.
+        :raises KeyError: If no children got passed.
+        """
+        offspring: list[Individual] | None = kwargs.get("children")
+        if offspring is None:
+            raise KeyError("No children passed.")
+
+        """
+        We want to get the survivors for the next generation, using our old population and the new children.
+        We determine the survivors by using a tournament, in which to random robots 'compete' against each other. 
+        This competition is based on the fitness value and the fitter robot will survive.
+        """
+        original_survivors, offspring_survivors = population_management.steady_state(
+            [i.genotype for i in population],
+            [i.fitness for i in population],
+            [i.genotype for i in offspring],
+            [i.fitness for i in offspring],
+            lambda n, genotypes, fitnesses: selection.multiple_unique(
+                n,
+                genotypes,
+                fitnesses,
+                lambda _, fitnesses: selection.tournament(self._rng, fitnesses, k=2),
+            ),
+        )
+
+        return [
+            Individual(
+                genotype=population[i].genotype,
+                fitness=population[i].fitness,
+            )
+            for i in original_survivors
+        ] + [
+            Individual(
+                genotype=offspring[i].genotype,
+                fitness=offspring[i].fitness,
+            )
+            for i in offspring_survivors
+        ], {}
+
+
+class CrossoverReproducer(Reproducer):
+    """We make a reproducer object to facilitate crossover operations."""
+
+    _rng: np.random.Generator
+
+    def __init__(self) -> None:
+        """Initialize the Reproducer."""
+        self._rng = make_rng_time_seed()
+
+    def reproduce(self, population: NDArray[np.int_], **kwargs: Any) -> list[Genotype]:
+        """
+        Make Individuals Reproduce.
+
+        :param population: The population.
+        :param kwargs: Additional arguments.
+        :return: The children.
+        :raises KeyError: If the parents are not passed.
+        """
+        parents: list[Individual] | None = kwargs.get(
+            "parent_population"
+        )  # We select the population of parents that were passed in KWArgs of the parent selector object.
+        if parents is None:
+            raise KeyError("No children passed.")
+        offspring = [
+            Genotype.crossover(
+                parents[parent1_i].genotype,
+                parents[parent2_i].genotype,
+                self._rng,
+            ).mutate(self._rng)
+            for parent1_i, parent2_i in population
+        ]
+        return offspring
+
+
+def main() -> None:
+    """Run the program."""
+    # Set up logging.
+    setup_logging()
+    rng = make_rng_time_seed()
+
+    # Create an initial population.
+    logging.info("Generating initial population.")
+    initial_genotypes = [
+        Genotype.random(
+            rng=rng,
+        )
+        for _ in range(config.POPULATION_SIZE)
+    ]
+    # Here we instantiate an evaluator, that allows us to evaluate a population of solutions.
+    evaluator = Evaluator()
+    # Now we instantiate a selector object to do parent selection.
+    parent_selector = ParentSelector(offspring_size=config.OFFSPRING_SIZE)
+    # We do the same to make survivor selection possible.
+    survivor_selector = SurvivorSelector()
+    # We also make a reproducer that allows us to get new individuals from our parents.
+    reproducer = CrossoverReproducer()
+
+    # Evaluate the initial population.
+    logging.info("Evaluating initial population.")
+    initial_fitnesses = evaluator.evaluate(initial_genotypes)
+
+    # Create a population of individuals, combining genotype with fitness.
+    population = [
+        Individual(genotype, fitness)
+        for genotype, fitness in zip(initial_genotypes, initial_fitnesses)
+    ]
+
+    # Set the current generation to 0.
+    generation_index = 0
+
+    # Start the actual optimization process.
+    logging.info("Start optimization process.")
+    while generation_index < config.NUM_GENERATIONS:
+        logging.info(f"Generation {generation_index + 1} / {config.NUM_GENERATIONS}.")
+
+        # Create offspring.
+        parent_pairs, parent_kwargs = parent_selector.select(population)
+        offspring_genotypes = reproducer.reproduce(
+            parent_pairs, **parent_kwargs
+        )  # we pass the parent pairs and the kwargs.
+
+        # Evaluate the offspring.
+        offspring_fitnesses = evaluator.evaluate(offspring_genotypes)
+
+        logging.info(f"Max fitness: {max(offspring_fitnesses)}")
+
+        # Make an intermediate offspring population.
+        offspring_population = [
+            Individual(genotype, fitness)
+            for genotype, fitness in zip(offspring_genotypes, offspring_fitnesses)
+        ]
+
+        # Create the next generation by selecting survivors between original population and offspring.
+        population, _ = survivor_selector.select(
+            population,
+            children=offspring_population,
+        )
+
+        # Increase the generation index counter.
+        generation_index += 1
+
+
+if __name__ == "__main__":
+    main()
diff --git a/examples/physical_robot_remote/requirements.txt b/examples/4_example_experiment_setups/4a_simple_ea_xor/requirements.txt
similarity index 100%
rename from examples/physical_robot_remote/requirements.txt
rename to examples/4_example_experiment_setups/4a_simple_ea_xor/requirements.txt
diff --git a/examples/simple_ea_xor_database/README.md b/examples/4_example_experiment_setups/4b_simple_ea_xor_database/README.md
similarity index 93%
rename from examples/simple_ea_xor_database/README.md
rename to examples/4_example_experiment_setups/4b_simple_ea_xor_database/README.md
index a99d25f1f..b7ad0cbe9 100644
--- a/examples/simple_ea_xor_database/README.md
+++ b/examples/4_example_experiment_setups/4b_simple_ea_xor_database/README.md
@@ -1,4 +1,4 @@
-This example adds a database to the `simple_ea_xor` example.
+This example adds a database to the `4a_simple_ea_xor` example.
 
 Naturally, look at that example first.
 
@@ -24,6 +24,7 @@ This software lets you declare the database structure in Python, simply by creat
 If you do not know what a dataclass is, it is worth looking that up.
 However, it will probably also become apparent how SQLAlchemy works without that knowledge.
 The main addition in this tutorial is that some classes are changed or added to follow this ORM description.
+These classes can be found in the `database_components` package.
 If at any point you do not understand something related to SQLAlchemy,
 be aware that it is a third-party library with an extensive documentation available to you online.
 
diff --git a/examples/simple_ea_xor_database/config.py b/examples/4_example_experiment_setups/4b_simple_ea_xor_database/config.py
similarity index 100%
rename from examples/simple_ea_xor_database/config.py
rename to examples/4_example_experiment_setups/4b_simple_ea_xor_database/config.py
diff --git a/examples/4_example_experiment_setups/4b_simple_ea_xor_database/database_components/__init__.py b/examples/4_example_experiment_setups/4b_simple_ea_xor_database/database_components/__init__.py
new file mode 100644
index 000000000..5e8ce692e
--- /dev/null
+++ b/examples/4_example_experiment_setups/4b_simple_ea_xor_database/database_components/__init__.py
@@ -0,0 +1,10 @@
+"""A collection of components used in the Database."""
+
+from ._base import Base
+from ._experiment import Experiment
+from ._generation import Generation
+from ._genotype import Genotype
+from ._individual import Individual
+from ._population import Population
+
+__all__ = ["Base", "Experiment", "Generation", "Genotype", "Individual", "Population"]
diff --git a/examples/simple_ea_xor_database/base.py b/examples/4_example_experiment_setups/4b_simple_ea_xor_database/database_components/_base.py
similarity index 100%
rename from examples/simple_ea_xor_database/base.py
rename to examples/4_example_experiment_setups/4b_simple_ea_xor_database/database_components/_base.py
diff --git a/examples/simple_ea_xor_database/experiment.py b/examples/4_example_experiment_setups/4b_simple_ea_xor_database/database_components/_experiment.py
similarity index 97%
rename from examples/simple_ea_xor_database/experiment.py
rename to examples/4_example_experiment_setups/4b_simple_ea_xor_database/database_components/_experiment.py
index cef2bacc6..5bc3127c5 100644
--- a/examples/simple_ea_xor_database/experiment.py
+++ b/examples/4_example_experiment_setups/4b_simple_ea_xor_database/database_components/_experiment.py
@@ -1,10 +1,11 @@
 """Experiment class."""
 
 import sqlalchemy.orm as orm
-from base import Base
 
 from revolve2.experimentation.database import HasId
 
+from ._base import Base
+
 
 class Experiment(Base, HasId):
     """
diff --git a/examples/simple_ea_xor_database/generation.py b/examples/4_example_experiment_setups/4b_simple_ea_xor_database/database_components/_generation.py
similarity index 91%
rename from examples/simple_ea_xor_database/generation.py
rename to examples/4_example_experiment_setups/4b_simple_ea_xor_database/database_components/_generation.py
index 218b2db6e..ee4ace2ee 100644
--- a/examples/simple_ea_xor_database/generation.py
+++ b/examples/4_example_experiment_setups/4b_simple_ea_xor_database/database_components/_generation.py
@@ -2,12 +2,13 @@
 
 import sqlalchemy
 import sqlalchemy.orm as orm
-from base import Base
-from experiment import Experiment
-from population import Population
 
 from revolve2.experimentation.database import HasId
 
+from ._base import Base
+from ._experiment import Experiment
+from ._population import Population
+
 
 class Generation(Base, HasId):
     """
diff --git a/examples/simple_ea_xor_database/genotype.py b/examples/4_example_experiment_setups/4b_simple_ea_xor_database/database_components/_genotype.py
similarity index 76%
rename from examples/simple_ea_xor_database/genotype.py
rename to examples/4_example_experiment_setups/4b_simple_ea_xor_database/database_components/_genotype.py
index f46d074bd..49d3ab402 100644
--- a/examples/simple_ea_xor_database/genotype.py
+++ b/examples/4_example_experiment_setups/4b_simple_ea_xor_database/database_components/_genotype.py
@@ -2,13 +2,13 @@
 
 from __future__ import annotations
 
-import config
 import numpy as np
-from base import Base
 
 from revolve2.experimentation.database import HasId
 from revolve2.experimentation.optimization.ea import Parameters as GenericParameters
 
+from ._base import Base
+
 
 class Genotype(Base, HasId, GenericParameters):
     """
@@ -28,18 +28,22 @@ class Genotype(Base, HasId, GenericParameters):
     def random(
         cls,
         rng: np.random.Generator,
+        num_parameters: int,
     ) -> Genotype:
         """
         Create a random genotype.
 
         :param rng: Random number generator.
+        :param num_parameters: Number of parameters for genotype.
         :returns: The created genotype.
         """
-        return Genotype(rng.random(size=config.NUM_PARAMETERS) * 2 - 1)
+        return Genotype(rng.random(size=num_parameters) * 2 - 1)
 
     def mutate(
         self,
         rng: np.random.Generator,
+        mutate_std: float,
+        num_parameters: int,
     ) -> Genotype:
         """
         Mutate this genotype.
@@ -47,11 +51,12 @@ def mutate(
         This genotype will not be changed; a mutated copy will be returned.
 
         :param rng: Random number generator.
+        :param mutate_std: The mutation probability.
+        :param num_parameters: Number of parameters for genotype.
         :returns: A mutated copy of the provided genotype.
         """
         return Genotype(
-            rng.normal(scale=config.MUTATE_STD, size=config.NUM_PARAMETERS)
-            + self.parameters
+            rng.normal(scale=mutate_std, size=num_parameters) + self.parameters
         )
 
     @classmethod
@@ -60,6 +65,7 @@ def crossover(
         parent1: Genotype,
         parent2: Genotype,
         rng: np.random.Generator,
+        num_parameters: int,
     ) -> Genotype:
         """
         Perform uniform crossover between two genotypes.
@@ -67,7 +73,8 @@ def crossover(
         :param parent1: The first genotype.
         :param parent2: The second genotype.
         :param rng: Random number generator.
+        :param num_parameters: The number of parameters for the genotype.
         :returns: A newly created genotype.
         """
-        mask = rng.random(config.NUM_PARAMETERS)
+        mask = rng.random(num_parameters)
         return Genotype(np.where(mask, parent1.parameters, parent2.parameters))
diff --git a/examples/simple_ea_xor_database/individual.py b/examples/4_example_experiment_setups/4b_simple_ea_xor_database/database_components/_individual.py
similarity index 96%
rename from examples/simple_ea_xor_database/individual.py
rename to examples/4_example_experiment_setups/4b_simple_ea_xor_database/database_components/_individual.py
index f330a73e2..bef55de73 100644
--- a/examples/simple_ea_xor_database/individual.py
+++ b/examples/4_example_experiment_setups/4b_simple_ea_xor_database/database_components/_individual.py
@@ -4,11 +4,12 @@
 
 import sqlalchemy
 import sqlalchemy.orm as orm
-from base import Base
-from genotype import Genotype
 
 from revolve2.experimentation.database import HasId
 
+from ._base import Base
+from ._genotype import Genotype
+
 
 @dataclass
 class Individual(Base, HasId, kw_only=True):
diff --git a/examples/simple_ea_xor_database/population.py b/examples/4_example_experiment_setups/4b_simple_ea_xor_database/database_components/_population.py
similarity index 92%
rename from examples/simple_ea_xor_database/population.py
rename to examples/4_example_experiment_setups/4b_simple_ea_xor_database/database_components/_population.py
index b56c52c27..a2b08a12e 100644
--- a/examples/simple_ea_xor_database/population.py
+++ b/examples/4_example_experiment_setups/4b_simple_ea_xor_database/database_components/_population.py
@@ -2,11 +2,12 @@
 
 import sqlalchemy.ext.orderinglist
 import sqlalchemy.orm as orm
-from base import Base
-from individual import Individual
 
 from revolve2.experimentation.database import HasId
 
+from ._base import Base
+from ._individual import Individual
+
 
 class Population(Base, HasId, kw_only=True):
     """
diff --git a/examples/4_example_experiment_setups/4b_simple_ea_xor_database/evaluate.py b/examples/4_example_experiment_setups/4b_simple_ea_xor_database/evaluate.py
new file mode 100644
index 000000000..5c371566c
--- /dev/null
+++ b/examples/4_example_experiment_setups/4b_simple_ea_xor_database/evaluate.py
@@ -0,0 +1,60 @@
+"""Evaluation functions."""
+
+import numpy as np
+import numpy.typing as npt
+from database_components import Genotype
+
+from revolve2.experimentation.evolution.abstract_elements import Evaluator as Eval
+
+
+class Evaluator(Eval):
+    """Here we make an evaluator object, based on the abstract evaluator defined in Revolve2."""
+
+    @staticmethod
+    def evaluate_network(
+        params: npt.NDArray[np.float_], inputs: npt.NDArray[np.float_]
+    ) -> np.float_:
+        """
+        Pass two inputs through a fully connected relu network.
+
+        :param params: The parameters to evaluate.
+        :param inputs: Inputs for network. 2x1 floats.
+        :returns: The output of the network.
+        """
+        # First layer
+        n0 = np.maximum(0, np.dot(params[:2], inputs) + params[2])
+        n1 = np.maximum(0, np.dot(params[3:5], inputs) + params[5])
+
+        # Second layer
+        output: np.float_ = np.maximum(0, n0 * params[6] + n1 * params[7] + params[8])
+        return output
+
+    def evaluate(self, population: list[Genotype]) -> list[float]:
+        """
+        Measure one set of parameters.
+
+        :param population: The population of parameters to measure.
+        :returns: Negative sum of squared errors and each individual error. 5x1 floats.
+        """
+        results = []
+        for genotype in population:
+            # Define all possible inputs for xor and the expected outputs
+            inputs = np.array([[0, 0], [1, 0], [0, 1], [1, 1]])
+            expected_outputs = np.array([0, 1, 1, 0])
+
+            # Evaluate the provided network parameters
+            outputs = np.array(
+                [self.evaluate_network(genotype.parameters, input) for input in inputs]
+            )
+
+            # Calculate the difference between the network outputs and the expect outputs
+            errors = outputs - expected_outputs
+
+            """
+            Now we add the sum of squared errors to the results.
+            In our case 0 would be an optimal result.
+            We invert so we can maximize the fitness instead of minimize.
+            Finally we convert from a numpy float_ type to the python float type. This is not really important.
+            """
+            results.append(float(-np.sum(errors**2)))
+        return results
diff --git a/examples/4_example_experiment_setups/4b_simple_ea_xor_database/main.py b/examples/4_example_experiment_setups/4b_simple_ea_xor_database/main.py
new file mode 100644
index 000000000..811456ba5
--- /dev/null
+++ b/examples/4_example_experiment_setups/4b_simple_ea_xor_database/main.py
@@ -0,0 +1,293 @@
+"""Main script for the example."""
+
+import logging
+from typing import Any
+
+import config
+import numpy as np
+from database_components import (
+    Base,
+    Experiment,
+    Generation,
+    Genotype,
+    Individual,
+    Population,
+)
+from evaluate import Evaluator
+from numpy.typing import NDArray
+from sqlalchemy.engine import Engine
+from sqlalchemy.orm import Session
+
+from revolve2.experimentation.database import OpenMethod, open_database_sqlite
+from revolve2.experimentation.evolution.abstract_elements import Reproducer, Selector
+from revolve2.experimentation.logging import setup_logging
+from revolve2.experimentation.optimization.ea import population_management, selection
+from revolve2.experimentation.rng import make_rng, make_rng_time_seed, seed_from_time
+
+
+class ParentSelector(Selector):
+    """Here we create a selector object that helps us select the parents for reproduction."""
+
+    _rng: np.random.Generator
+    _offspring_size: int
+
+    def __init__(self, offspring_size: int) -> None:
+        """
+        Initialize the ParentSelector object.
+
+        :param offspring_size: The size of the offspring for selection.
+        """
+        self._rng = make_rng_time_seed()
+        self._offspring_size = offspring_size
+
+    def select(
+        self, population: Population, **kwargs: Any
+    ) -> tuple[NDArray[np.int_], dict[str, Population]]:
+        """
+        Select pairs of parents using a tournament.
+
+        :param population: The population to select from.
+        :param kwargs: Additional kwargs that are not used in this example.
+        :returns: Pairs of indices of selected parents. offspring_size x 2 ints, and the parent population in the KWArgs dict.
+        """
+        return np.asarray(
+            [
+                selection.multiple_unique(
+                    2,
+                    [individual.genotype for individual in population.individuals],
+                    [individual.fitness for individual in population.individuals],
+                    lambda _, fitnesses: selection.tournament(
+                        self._rng, fitnesses, k=1
+                    ),
+                )
+                for _ in range(self._offspring_size)
+            ]
+        ), {"parent_population": population}
+
+
+class SurvivorSelector(Selector):
+    """Here we make a selector object that allows us to select survivor after evaluation."""
+
+    _rng: np.random.Generator
+
+    def __init__(self) -> None:
+        """Initialize the selector."""
+        self._rng = make_rng_time_seed()
+
+    def select(
+        self, population: Population, **kwargs: Any
+    ) -> tuple[Population, dict[Any, Any]]:
+        """
+        Select survivors using a tournament.
+
+        :param population: The initial population.
+        :param kwargs: Additional kwargs that contain the children to do selection with.
+        :returns: The selected population and empty kwargs in this implementation.
+        :raises KeyError: If no children got passed.
+        """
+        offspring: list[Individual] | None = kwargs.get("children")
+        if offspring is None:
+            raise KeyError("No children passed.")
+        original_survivors, offspring_survivors = population_management.steady_state(
+            [i.genotype for i in population.individuals],
+            [i.fitness for i in population.individuals],
+            [i.genotype for i in offspring],
+            [i.fitness for i in offspring],
+            lambda n, genotypes, fitnesses: selection.multiple_unique(
+                n,
+                genotypes,
+                fitnesses,
+                lambda _, fitnesses: selection.tournament(self._rng, fitnesses, k=2),
+            ),
+        )
+
+        return (
+            Population(
+                individuals=[
+                    Individual(
+                        genotype=population.individuals[i].genotype,
+                        fitness=population.individuals[i].fitness,
+                    )
+                    for i in original_survivors
+                ]
+                + [
+                    Individual(
+                        genotype=offspring[i].genotype,
+                        fitness=offspring[i].fitness,
+                    )
+                    for i in offspring_survivors
+                ]
+            ),
+            {},
+        )
+
+
+class CrossoverReproducer(Reproducer):
+    """We make a reproducer object to facilitate crossover operations."""
+
+    _rng: np.random.Generator
+
+    def __init__(self) -> None:
+        """Initialize the Reproducer."""
+        self._rng = make_rng_time_seed()
+
+    def reproduce(self, population: NDArray[np.int_], **kwargs: Any) -> list[Genotype]:
+        """
+        Make Individuals Reproduce.
+
+        :param population: The population.
+        :param kwargs: Additional arguments.
+        :returns: The reproduced population.
+        :raises KeyError: If the parents are not passed.
+        """
+        parents: list[Individual] | None = kwargs.get(
+            "parent_population"
+        )  # We select the population of parents that were passed in KWArgs of the parent selector object.
+        if parents is None:
+            raise KeyError("No children passed.")
+        offspring = [
+            Genotype.crossover(
+                parents[parent1_i].genotype,
+                parents[parent2_i].genotype,
+                self._rng,
+                num_parameters=config.NUM_PARAMETERS,
+            ).mutate(
+                self._rng,
+                mutate_std=config.MUTATE_STD,
+                num_parameters=config.NUM_PARAMETERS,
+            )
+            for parent1_i, parent2_i in population
+        ]
+        return offspring
+
+
+def run_experiment(dbengine: Engine) -> None:
+    """
+    Run an experiment.
+
+    :param dbengine: An openened database with matching initialize database structure.
+    """
+    logging.info("----------------")
+    logging.info("Start experiment")
+
+    # Set up the random number generator.
+    # We are manually generating the seed, because we are going to save it in our database.
+    rng_seed = seed_from_time()
+    rng = make_rng(rng_seed)
+
+    # Create a new experiment instance.
+    experiment = Experiment(rng_seed=rng_seed)
+
+    """
+    Now we save it to the database.
+    A session manages multiple changes to a database, which then can either be committed(accepted) or a rolled back(aborted) in case something bad happens in our code.
+    We add the experiment and commit as nothing can go wrong.
+    """
+    logging.info("Saving experiment configuration.")
+    with Session(dbengine) as session:
+        session.add(experiment)
+        session.commit()
+
+    # Create an initial population.
+    logging.info("Generating initial population.")
+    initial_genotypes = [
+        Genotype.random(rng=rng, num_parameters=config.NUM_PARAMETERS)
+        for _ in range(config.POPULATION_SIZE)
+    ]
+    # Here we instantiate an evaluator, that allows us to evaluate a population of solutions.
+    evaluator = Evaluator()
+    # Now we instantiate a selector object to do parent selection.
+    parent_selector = ParentSelector(offspring_size=config.OFFSPRING_SIZE)
+    # We do the same to make survivor selection possible.
+    survivor_selector = SurvivorSelector()
+    # We also make a reproducer that allows us to get new individuals from our parents.
+    reproducer = CrossoverReproducer()
+
+    # Evaluate the initial population.
+    logging.info("Evaluating initial population.")
+    initial_fitnesses = evaluator.evaluate(initial_genotypes)
+
+    # Create a population of individuals, combining genotype with fitness.
+    population = Population(
+        individuals=[
+            Individual(genotype=genotype, fitness=fitness)
+            for genotype, fitness in zip(initial_genotypes, initial_fitnesses)
+        ]
+    )
+
+    # Finish the zeroth generation and save it to the database.
+    # The generation references the experiment so we later know which experiment it was part of.
+    generation = Generation(
+        experiment=experiment, generation_index=0, population=population
+    )
+    logging.info("Saving generation.")
+    with Session(dbengine, expire_on_commit=False) as session:
+        session.add(generation)
+        session.commit()
+
+    # Start the actual optimization process.
+    logging.info("Start optimization process.")
+    while generation.generation_index < config.NUM_GENERATIONS:
+        logging.info(
+            f"Generation {generation.generation_index + 1} / {config.NUM_GENERATIONS}."
+        )
+
+        # Create offspring.
+        parent_pairs, parent_kwargs = parent_selector.select(population)
+        offspring_genotypes = reproducer.reproduce(
+            parent_pairs, **parent_kwargs
+        )  # we pass the parent pairs and the kwargs.
+
+        # Evaluate the offspring.
+        offspring_fitnesses = evaluator.evaluate(offspring_genotypes)
+
+        # Make an intermediate offspring population.
+        offspring_population = Population(
+            individuals=[
+                Individual(genotype=genotype, fitness=fitness)
+                for genotype, fitness in zip(offspring_genotypes, offspring_fitnesses)
+            ]
+        )
+
+        # Create the next generation by selecting survivors between original population and offspring.
+        population, _ = survivor_selector.select(
+            population,
+            children=offspring_population,
+        )
+
+        # Make it all into a generation and save it to the database.
+        generation = Generation(
+            experiment=experiment,
+            generation_index=generation.generation_index + 1,
+            population=population,
+        )
+        logging.info("Saving generation.")
+        with Session(dbengine, expire_on_commit=False) as session:
+            session.add(generation)
+            session.commit()
+
+
+def main() -> None:
+    """Run the program."""
+    # Set up logging.
+    setup_logging(file_name="log.txt")
+
+    """
+    We open the database, only if it does not already exist.
+    If it did something when wrong in a previous run of this program, and we must manually figure out what to do with the existing database.
+    (maybe throw away?)
+    """
+    dbengine = open_database_sqlite(
+        config.DATABASE_FILE, open_method=OpenMethod.NOT_EXISTS_AND_CREATE
+    )
+    # Create the structure of the database.
+    # Take a look at the 'Base' class.
+    Base.metadata.create_all(dbengine)
+
+    # We are running several repetitions of the same experiment.
+    for _ in range(config.NUM_REPETITIONS):
+        run_experiment(dbengine)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/examples/simple_ea_xor_database/plot.py b/examples/4_example_experiment_setups/4b_simple_ea_xor_database/plot.py
similarity index 96%
rename from examples/simple_ea_xor_database/plot.py
rename to examples/4_example_experiment_setups/4b_simple_ea_xor_database/plot.py
index ce442c84e..d80cfac9e 100644
--- a/examples/simple_ea_xor_database/plot.py
+++ b/examples/4_example_experiment_setups/4b_simple_ea_xor_database/plot.py
@@ -3,10 +3,7 @@
 import config
 import matplotlib.pyplot as plt
 import pandas
-from experiment import Experiment
-from generation import Generation
-from individual import Individual
-from population import Population
+from database_components import Experiment, Generation, Individual, Population
 from sqlalchemy import select
 
 from revolve2.experimentation.database import OpenMethod, open_database_sqlite
diff --git a/examples/robot_bodybrain_ea_database/requirements.txt b/examples/4_example_experiment_setups/4b_simple_ea_xor_database/requirements.txt
similarity index 100%
rename from examples/robot_bodybrain_ea_database/requirements.txt
rename to examples/4_example_experiment_setups/4b_simple_ea_xor_database/requirements.txt
diff --git a/examples/robot_bodybrain_ea/README.md b/examples/4_example_experiment_setups/4c_robot_bodybrain_ea/README.md
similarity index 84%
rename from examples/robot_bodybrain_ea/README.md
rename to examples/4_example_experiment_setups/4c_robot_bodybrain_ea/README.md
index 89ddec83d..601dc12a5 100644
--- a/examples/robot_bodybrain_ea/README.md
+++ b/examples/4_example_experiment_setups/4c_robot_bodybrain_ea/README.md
@@ -2,11 +2,12 @@ In this example you see an experiment that optimizes the bodies and brains of a
 The genotypes for both body and brain are CPPNWIN.
 
 Before starting this tutorial, it is useful to look at the 
-`evaluate_multiple_isolated_robots`, and `simple_ea_xor` examples.
+`3c_evaluate_multiple_isolated_robots`, and `4a_simple_ea_xor` examples.
 It is also nice to understand the concept of a cpg brain and CPPN, although not really needed.
 
 You learn:
 - How to optimize the body and brain of a robot using an EA.
+- How to use the ModularRobotEvolution object for easy evolutionary control.
 
 To change the parameters of the experiment use `config.py`.<br/>
 The evaluation of individual robots is done in `evaluator.py`.<br/>
diff --git a/examples/robot_bodybrain_ea/config.py b/examples/4_example_experiment_setups/4c_robot_bodybrain_ea/config.py
similarity index 100%
rename from examples/robot_bodybrain_ea/config.py
rename to examples/4_example_experiment_setups/4c_robot_bodybrain_ea/config.py
diff --git a/examples/robot_bodybrain_ea/evaluator.py b/examples/4_example_experiment_setups/4c_robot_bodybrain_ea/evaluator.py
similarity index 100%
rename from examples/robot_bodybrain_ea/evaluator.py
rename to examples/4_example_experiment_setups/4c_robot_bodybrain_ea/evaluator.py
diff --git a/examples/robot_bodybrain_ea/genotype.py b/examples/4_example_experiment_setups/4c_robot_bodybrain_ea/genotype.py
similarity index 100%
rename from examples/robot_bodybrain_ea/genotype.py
rename to examples/4_example_experiment_setups/4c_robot_bodybrain_ea/genotype.py
diff --git a/examples/simple_ea_xor/individual.py b/examples/4_example_experiment_setups/4c_robot_bodybrain_ea/individual.py
similarity index 100%
rename from examples/simple_ea_xor/individual.py
rename to examples/4_example_experiment_setups/4c_robot_bodybrain_ea/individual.py
diff --git a/examples/robot_bodybrain_ea/main.py b/examples/4_example_experiment_setups/4c_robot_bodybrain_ea/main.py
similarity index 94%
rename from examples/robot_bodybrain_ea/main.py
rename to examples/4_example_experiment_setups/4c_robot_bodybrain_ea/main.py
index 82dbb2e10..60995292e 100644
--- a/examples/robot_bodybrain_ea/main.py
+++ b/examples/4_example_experiment_setups/4c_robot_bodybrain_ea/main.py
@@ -256,8 +256,15 @@ def main() -> None:
     while generation_index < config.NUM_GENERATIONS:
         logging.info(f"Generation {generation_index + 1} / {config.NUM_GENERATIONS}.")
 
-        # Step the evolution forward.
-        population = modular_robot_evolution.step(population)
+        """
+        In contrast to the previous example we do not explicitly stat the order of operations here, but let the ModularRobotEvolution object do the scheduling.
+        This does not give a performance boost, but is more readable and less prone to errors due to mixing up the order.
+        
+        Not that you are not restricted to the classical ModularRobotEvolution object, since you can adjust the step function as you want.
+        """
+        population = modular_robot_evolution.step(
+            population
+        )  # Step the evolution forward.
 
         # Find the new best robot
         best_robot = find_best_robot(best_robot, population)
diff --git a/examples/robot_bodybrain_ea/requirements.txt b/examples/4_example_experiment_setups/4c_robot_bodybrain_ea/requirements.txt
similarity index 100%
rename from examples/robot_bodybrain_ea/requirements.txt
rename to examples/4_example_experiment_setups/4c_robot_bodybrain_ea/requirements.txt
diff --git a/examples/robot_bodybrain_ea/rerun.py b/examples/4_example_experiment_setups/4c_robot_bodybrain_ea/rerun.py
similarity index 100%
rename from examples/robot_bodybrain_ea/rerun.py
rename to examples/4_example_experiment_setups/4c_robot_bodybrain_ea/rerun.py
diff --git a/examples/robot_bodybrain_ea_database/README.md b/examples/4_example_experiment_setups/4d_robot_bodybrain_ea_database/README.md
similarity index 77%
rename from examples/robot_bodybrain_ea_database/README.md
rename to examples/4_example_experiment_setups/4d_robot_bodybrain_ea_database/README.md
index c92289349..5989f793d 100644
--- a/examples/robot_bodybrain_ea_database/README.md
+++ b/examples/4_example_experiment_setups/4d_robot_bodybrain_ea_database/README.md
@@ -1,6 +1,6 @@
 This is the `robot_bodybrain_ea` example, but with added saving of results to a database.
 
-Definitely first look at the `robot_bodybrain_ea` and `simple_ea_xor_database` examples.
+Definitely first look at the `4c_robot_bodybrain_ea` and `4b_simple_ea_xor_database` examples.
 Many explanation comments are omitted here.
 
 To visualize the evolved robots, use `rerun.py` with the pickled genotype you got from evolution.
diff --git a/examples/robot_bodybrain_ea_database/config.py b/examples/4_example_experiment_setups/4d_robot_bodybrain_ea_database/config.py
similarity index 100%
rename from examples/robot_bodybrain_ea_database/config.py
rename to examples/4_example_experiment_setups/4d_robot_bodybrain_ea_database/config.py
diff --git a/examples/4_example_experiment_setups/4d_robot_bodybrain_ea_database/database_components/__init__.py b/examples/4_example_experiment_setups/4d_robot_bodybrain_ea_database/database_components/__init__.py
new file mode 100644
index 000000000..5e8ce692e
--- /dev/null
+++ b/examples/4_example_experiment_setups/4d_robot_bodybrain_ea_database/database_components/__init__.py
@@ -0,0 +1,10 @@
+"""A collection of components used in the Database."""
+
+from ._base import Base
+from ._experiment import Experiment
+from ._generation import Generation
+from ._genotype import Genotype
+from ._individual import Individual
+from ._population import Population
+
+__all__ = ["Base", "Experiment", "Generation", "Genotype", "Individual", "Population"]
diff --git a/examples/robot_bodybrain_ea_database/base.py b/examples/4_example_experiment_setups/4d_robot_bodybrain_ea_database/database_components/_base.py
similarity index 100%
rename from examples/robot_bodybrain_ea_database/base.py
rename to examples/4_example_experiment_setups/4d_robot_bodybrain_ea_database/database_components/_base.py
diff --git a/examples/robot_bodybrain_ea_database/experiment.py b/examples/4_example_experiment_setups/4d_robot_bodybrain_ea_database/database_components/_experiment.py
similarity index 92%
rename from examples/robot_bodybrain_ea_database/experiment.py
rename to examples/4_example_experiment_setups/4d_robot_bodybrain_ea_database/database_components/_experiment.py
index ef368d8d4..e091ed409 100644
--- a/examples/robot_bodybrain_ea_database/experiment.py
+++ b/examples/4_example_experiment_setups/4d_robot_bodybrain_ea_database/database_components/_experiment.py
@@ -1,10 +1,11 @@
 """Experiment class."""
 
 import sqlalchemy.orm as orm
-from base import Base
 
 from revolve2.experimentation.database import HasId
 
+from ._base import Base
+
 
 class Experiment(Base, HasId):
     """Experiment description."""
diff --git a/examples/robot_brain_cmaes_database/generation.py b/examples/4_example_experiment_setups/4d_robot_bodybrain_ea_database/database_components/_generation.py
similarity index 87%
rename from examples/robot_brain_cmaes_database/generation.py
rename to examples/4_example_experiment_setups/4d_robot_bodybrain_ea_database/database_components/_generation.py
index 977c05ed8..711966252 100644
--- a/examples/robot_brain_cmaes_database/generation.py
+++ b/examples/4_example_experiment_setups/4d_robot_bodybrain_ea_database/database_components/_generation.py
@@ -2,12 +2,13 @@
 
 import sqlalchemy
 import sqlalchemy.orm as orm
-from base import Base
-from experiment import Experiment
-from population import Population
 
 from revolve2.experimentation.database import HasId
 
+from ._base import Base
+from ._experiment import Experiment
+from ._population import Population
+
 
 class Generation(Base, HasId):
     """A single finished iteration of CMA-ES."""
diff --git a/examples/robot_bodybrain_ea_database/genotype.py b/examples/4_example_experiment_setups/4d_robot_bodybrain_ea_database/database_components/_genotype.py
similarity index 99%
rename from examples/robot_bodybrain_ea_database/genotype.py
rename to examples/4_example_experiment_setups/4d_robot_bodybrain_ea_database/database_components/_genotype.py
index 406efd64b..b7d4881c0 100644
--- a/examples/robot_bodybrain_ea_database/genotype.py
+++ b/examples/4_example_experiment_setups/4d_robot_bodybrain_ea_database/database_components/_genotype.py
@@ -4,13 +4,14 @@
 
 import multineat
 import numpy as np
-from base import Base
 
 from revolve2.ci_group.genotypes.cppnwin.modular_robot import BrainGenotypeCpgOrm
 from revolve2.ci_group.genotypes.cppnwin.modular_robot.v2 import BodyGenotypeOrmV2
 from revolve2.experimentation.database import HasId
 from revolve2.modular_robot import ModularRobot
 
+from ._base import Base
+
 
 class Genotype(Base, HasId, BodyGenotypeOrmV2, BrainGenotypeCpgOrm):
     """SQLAlchemy model for a genotype for a modular robot body and brain."""
diff --git a/examples/robot_brain_cmaes_database/individual.py b/examples/4_example_experiment_setups/4d_robot_bodybrain_ea_database/database_components/_individual.py
similarity index 85%
rename from examples/robot_brain_cmaes_database/individual.py
rename to examples/4_example_experiment_setups/4d_robot_bodybrain_ea_database/database_components/_individual.py
index f745e3a2d..40e4eefc8 100644
--- a/examples/robot_brain_cmaes_database/individual.py
+++ b/examples/4_example_experiment_setups/4d_robot_bodybrain_ea_database/database_components/_individual.py
@@ -2,11 +2,11 @@
 
 from dataclasses import dataclass
 
-from base import Base
-from genotype import Genotype
-
 from revolve2.experimentation.optimization.ea import Individual as GenericIndividual
 
+from ._base import Base
+from ._genotype import Genotype
+
 
 @dataclass
 class Individual(
diff --git a/examples/robot_brain_cmaes_database/population.py b/examples/4_example_experiment_setups/4d_robot_bodybrain_ea_database/database_components/_population.py
similarity index 80%
rename from examples/robot_brain_cmaes_database/population.py
rename to examples/4_example_experiment_setups/4d_robot_bodybrain_ea_database/database_components/_population.py
index 7d622bb5b..7e43a0f7f 100644
--- a/examples/robot_brain_cmaes_database/population.py
+++ b/examples/4_example_experiment_setups/4d_robot_bodybrain_ea_database/database_components/_population.py
@@ -1,10 +1,10 @@
 """Population class."""
 
-from base import Base
-from individual import Individual
-
 from revolve2.experimentation.optimization.ea import Population as GenericPopulation
 
+from ._base import Base
+from ._individual import Individual
+
 
 class Population(Base, GenericPopulation[Individual], kw_only=True):
     """A population of individuals."""
diff --git a/examples/robot_bodybrain_ea_database/evaluator.py b/examples/4_example_experiment_setups/4d_robot_bodybrain_ea_database/evaluator.py
similarity index 98%
rename from examples/robot_bodybrain_ea_database/evaluator.py
rename to examples/4_example_experiment_setups/4d_robot_bodybrain_ea_database/evaluator.py
index 73d3b7493..0bb8d2bcd 100644
--- a/examples/robot_bodybrain_ea_database/evaluator.py
+++ b/examples/4_example_experiment_setups/4d_robot_bodybrain_ea_database/evaluator.py
@@ -1,6 +1,6 @@
 """Evaluator class."""
 
-from genotype import Genotype
+from database_components import Genotype
 
 from revolve2.ci_group import fitness_functions, terrains
 from revolve2.ci_group.simulation_parameters import make_standard_batch_parameters
diff --git a/examples/robot_bodybrain_ea_database/main.py b/examples/4_example_experiment_setups/4d_robot_bodybrain_ea_database/main.py
similarity index 98%
rename from examples/robot_bodybrain_ea_database/main.py
rename to examples/4_example_experiment_setups/4d_robot_bodybrain_ea_database/main.py
index 952ac9311..9aae096d6 100644
--- a/examples/robot_bodybrain_ea_database/main.py
+++ b/examples/4_example_experiment_setups/4d_robot_bodybrain_ea_database/main.py
@@ -7,13 +7,15 @@
 import multineat
 import numpy as np
 import numpy.typing as npt
-from base import Base
+from database_components import (
+    Base,
+    Experiment,
+    Generation,
+    Genotype,
+    Individual,
+    Population,
+)
 from evaluator import Evaluator
-from experiment import Experiment
-from generation import Generation
-from genotype import Genotype
-from individual import Individual
-from population import Population
 from sqlalchemy.engine import Engine
 from sqlalchemy.orm import Session
 
diff --git a/examples/robot_bodybrain_ea_database/plot.py b/examples/4_example_experiment_setups/4d_robot_bodybrain_ea_database/plot.py
similarity index 95%
rename from examples/robot_bodybrain_ea_database/plot.py
rename to examples/4_example_experiment_setups/4d_robot_bodybrain_ea_database/plot.py
index 009326c70..719d928ce 100644
--- a/examples/robot_bodybrain_ea_database/plot.py
+++ b/examples/4_example_experiment_setups/4d_robot_bodybrain_ea_database/plot.py
@@ -3,10 +3,7 @@
 import config
 import matplotlib.pyplot as plt
 import pandas
-from experiment import Experiment
-from generation import Generation
-from individual import Individual
-from population import Population
+from database_components import Experiment, Generation, Individual, Population
 from sqlalchemy import select
 
 from revolve2.experimentation.database import OpenMethod, open_database_sqlite
diff --git a/examples/simple_ea_xor_database/requirements.txt b/examples/4_example_experiment_setups/4d_robot_bodybrain_ea_database/requirements.txt
similarity index 100%
rename from examples/simple_ea_xor_database/requirements.txt
rename to examples/4_example_experiment_setups/4d_robot_bodybrain_ea_database/requirements.txt
diff --git a/examples/robot_bodybrain_ea_database/rerun.py b/examples/4_example_experiment_setups/4d_robot_bodybrain_ea_database/rerun.py
similarity index 94%
rename from examples/robot_bodybrain_ea_database/rerun.py
rename to examples/4_example_experiment_setups/4d_robot_bodybrain_ea_database/rerun.py
index 5fae1f1a5..54c158712 100644
--- a/examples/robot_bodybrain_ea_database/rerun.py
+++ b/examples/4_example_experiment_setups/4d_robot_bodybrain_ea_database/rerun.py
@@ -3,9 +3,8 @@
 import logging
 
 import config
+from database_components import Genotype, Individual
 from evaluator import Evaluator
-from genotype import Genotype
-from individual import Individual
 from sqlalchemy import select
 from sqlalchemy.orm import Session
 
diff --git a/examples/robot_brain_cmaes/README.md b/examples/4_example_experiment_setups/4e_robot_brain_cmaes/README.md
similarity index 80%
rename from examples/robot_brain_cmaes/README.md
rename to examples/4_example_experiment_setups/4e_robot_brain_cmaes/README.md
index f6156b36f..07e62b662 100644
--- a/examples/robot_brain_cmaes/README.md
+++ b/examples/4_example_experiment_setups/4e_robot_brain_cmaes/README.md
@@ -1,7 +1,7 @@
 Here you will set up an experiment that optimizes the brain of a given robot body using CMA-ES.
 As the body is static, the genotype of the brain will be a fixed length real-valued vector.
 
-Before starting this tutorial, it is useful to look at the `experiment_setup` and `evaluate_multiple_isolated_robots` examples.
+Before starting this tutorial, it is useful to look at the `3a_experiment_setup` and `3c_evaluate_multiple_isolated_robots` examples.
 It is also nice to understand the concept of a cpg brain, although not really needed.
 
 You learn:
diff --git a/examples/robot_brain_cmaes/config.py b/examples/4_example_experiment_setups/4e_robot_brain_cmaes/config.py
similarity index 100%
rename from examples/robot_brain_cmaes/config.py
rename to examples/4_example_experiment_setups/4e_robot_brain_cmaes/config.py
diff --git a/examples/robot_brain_cmaes/evaluator.py b/examples/4_example_experiment_setups/4e_robot_brain_cmaes/evaluator.py
similarity index 100%
rename from examples/robot_brain_cmaes/evaluator.py
rename to examples/4_example_experiment_setups/4e_robot_brain_cmaes/evaluator.py
diff --git a/examples/robot_brain_cmaes/main.py b/examples/4_example_experiment_setups/4e_robot_brain_cmaes/main.py
similarity index 100%
rename from examples/robot_brain_cmaes/main.py
rename to examples/4_example_experiment_setups/4e_robot_brain_cmaes/main.py
diff --git a/examples/robot_brain_cmaes/requirements.txt b/examples/4_example_experiment_setups/4e_robot_brain_cmaes/requirements.txt
similarity index 100%
rename from examples/robot_brain_cmaes/requirements.txt
rename to examples/4_example_experiment_setups/4e_robot_brain_cmaes/requirements.txt
diff --git a/examples/robot_brain_cmaes/rerun.py b/examples/4_example_experiment_setups/4e_robot_brain_cmaes/rerun.py
similarity index 100%
rename from examples/robot_brain_cmaes/rerun.py
rename to examples/4_example_experiment_setups/4e_robot_brain_cmaes/rerun.py
diff --git a/examples/robot_brain_cmaes_database/README.md b/examples/4_example_experiment_setups/4f_robot_brain_cmaes_database/README.md
similarity index 76%
rename from examples/robot_brain_cmaes_database/README.md
rename to examples/4_example_experiment_setups/4f_robot_brain_cmaes_database/README.md
index 4515919cd..0ffbc9517 100644
--- a/examples/robot_brain_cmaes_database/README.md
+++ b/examples/4_example_experiment_setups/4f_robot_brain_cmaes_database/README.md
@@ -1,6 +1,6 @@
 This is the `robot_brain_cmaes` example, but with added saving of results to a database.
 
-Definitely first look at the `robot_brain_cmaes` and `simple_ea_xor_database` examples.
+Definitely first look at the `4e_robot_brain_cmaes` and `4b_simple_ea_xor_database` examples.
 Many explanation comments are omitted here.
 
 To visualize the evolved robots, use `rerun.py` with the parameters you got from evolution.
diff --git a/examples/robot_brain_cmaes_database/config.py b/examples/4_example_experiment_setups/4f_robot_brain_cmaes_database/config.py
similarity index 100%
rename from examples/robot_brain_cmaes_database/config.py
rename to examples/4_example_experiment_setups/4f_robot_brain_cmaes_database/config.py
diff --git a/examples/4_example_experiment_setups/4f_robot_brain_cmaes_database/database_components/__init__.py b/examples/4_example_experiment_setups/4f_robot_brain_cmaes_database/database_components/__init__.py
new file mode 100644
index 000000000..5e8ce692e
--- /dev/null
+++ b/examples/4_example_experiment_setups/4f_robot_brain_cmaes_database/database_components/__init__.py
@@ -0,0 +1,10 @@
+"""A collection of components used in the Database."""
+
+from ._base import Base
+from ._experiment import Experiment
+from ._generation import Generation
+from ._genotype import Genotype
+from ._individual import Individual
+from ._population import Population
+
+__all__ = ["Base", "Experiment", "Generation", "Genotype", "Individual", "Population"]
diff --git a/examples/robot_brain_cmaes_database/base.py b/examples/4_example_experiment_setups/4f_robot_brain_cmaes_database/database_components/_base.py
similarity index 100%
rename from examples/robot_brain_cmaes_database/base.py
rename to examples/4_example_experiment_setups/4f_robot_brain_cmaes_database/database_components/_base.py
diff --git a/examples/robot_brain_cmaes_database/experiment.py b/examples/4_example_experiment_setups/4f_robot_brain_cmaes_database/database_components/_experiment.py
similarity index 92%
rename from examples/robot_brain_cmaes_database/experiment.py
rename to examples/4_example_experiment_setups/4f_robot_brain_cmaes_database/database_components/_experiment.py
index ef368d8d4..e091ed409 100644
--- a/examples/robot_brain_cmaes_database/experiment.py
+++ b/examples/4_example_experiment_setups/4f_robot_brain_cmaes_database/database_components/_experiment.py
@@ -1,10 +1,11 @@
 """Experiment class."""
 
 import sqlalchemy.orm as orm
-from base import Base
 
 from revolve2.experimentation.database import HasId
 
+from ._base import Base
+
 
 class Experiment(Base, HasId):
     """Experiment description."""
diff --git a/examples/robot_bodybrain_ea_database/generation.py b/examples/4_example_experiment_setups/4f_robot_brain_cmaes_database/database_components/_generation.py
similarity index 87%
rename from examples/robot_bodybrain_ea_database/generation.py
rename to examples/4_example_experiment_setups/4f_robot_brain_cmaes_database/database_components/_generation.py
index 977c05ed8..711966252 100644
--- a/examples/robot_bodybrain_ea_database/generation.py
+++ b/examples/4_example_experiment_setups/4f_robot_brain_cmaes_database/database_components/_generation.py
@@ -2,12 +2,13 @@
 
 import sqlalchemy
 import sqlalchemy.orm as orm
-from base import Base
-from experiment import Experiment
-from population import Population
 
 from revolve2.experimentation.database import HasId
 
+from ._base import Base
+from ._experiment import Experiment
+from ._population import Population
+
 
 class Generation(Base, HasId):
     """A single finished iteration of CMA-ES."""
diff --git a/examples/robot_brain_cmaes_database/genotype.py b/examples/4_example_experiment_setups/4f_robot_brain_cmaes_database/database_components/_genotype.py
similarity index 93%
rename from examples/robot_brain_cmaes_database/genotype.py
rename to examples/4_example_experiment_setups/4f_robot_brain_cmaes_database/database_components/_genotype.py
index a567b055c..e630b5a93 100644
--- a/examples/robot_brain_cmaes_database/genotype.py
+++ b/examples/4_example_experiment_setups/4f_robot_brain_cmaes_database/database_components/_genotype.py
@@ -2,11 +2,11 @@
 
 from __future__ import annotations
 
-from base import Base
-
 from revolve2.experimentation.database import HasId
 from revolve2.experimentation.optimization.ea import Parameters as GenericParameters
 
+from ._base import Base
+
 
 class Genotype(Base, HasId, GenericParameters):
     """A genotype that is an array of parameters."""
diff --git a/examples/robot_bodybrain_ea_database/individual.py b/examples/4_example_experiment_setups/4f_robot_brain_cmaes_database/database_components/_individual.py
similarity index 85%
rename from examples/robot_bodybrain_ea_database/individual.py
rename to examples/4_example_experiment_setups/4f_robot_brain_cmaes_database/database_components/_individual.py
index f745e3a2d..40e4eefc8 100644
--- a/examples/robot_bodybrain_ea_database/individual.py
+++ b/examples/4_example_experiment_setups/4f_robot_brain_cmaes_database/database_components/_individual.py
@@ -2,11 +2,11 @@
 
 from dataclasses import dataclass
 
-from base import Base
-from genotype import Genotype
-
 from revolve2.experimentation.optimization.ea import Individual as GenericIndividual
 
+from ._base import Base
+from ._genotype import Genotype
+
 
 @dataclass
 class Individual(
diff --git a/examples/robot_bodybrain_ea_database/population.py b/examples/4_example_experiment_setups/4f_robot_brain_cmaes_database/database_components/_population.py
similarity index 80%
rename from examples/robot_bodybrain_ea_database/population.py
rename to examples/4_example_experiment_setups/4f_robot_brain_cmaes_database/database_components/_population.py
index 7d622bb5b..7e43a0f7f 100644
--- a/examples/robot_bodybrain_ea_database/population.py
+++ b/examples/4_example_experiment_setups/4f_robot_brain_cmaes_database/database_components/_population.py
@@ -1,10 +1,10 @@
 """Population class."""
 
-from base import Base
-from individual import Individual
-
 from revolve2.experimentation.optimization.ea import Population as GenericPopulation
 
+from ._base import Base
+from ._individual import Individual
+
 
 class Population(Base, GenericPopulation[Individual], kw_only=True):
     """A population of individuals."""
diff --git a/examples/robot_brain_cmaes_database/evaluator.py b/examples/4_example_experiment_setups/4f_robot_brain_cmaes_database/evaluator.py
similarity index 100%
rename from examples/robot_brain_cmaes_database/evaluator.py
rename to examples/4_example_experiment_setups/4f_robot_brain_cmaes_database/evaluator.py
diff --git a/examples/robot_brain_cmaes_database/main.py b/examples/4_example_experiment_setups/4f_robot_brain_cmaes_database/main.py
similarity index 95%
rename from examples/robot_brain_cmaes_database/main.py
rename to examples/4_example_experiment_setups/4f_robot_brain_cmaes_database/main.py
index b04c1130d..4cc368651 100644
--- a/examples/robot_brain_cmaes_database/main.py
+++ b/examples/4_example_experiment_setups/4f_robot_brain_cmaes_database/main.py
@@ -4,13 +4,15 @@
 
 import cma
 import config
-from base import Base
+from database_components import (
+    Base,
+    Experiment,
+    Generation,
+    Genotype,
+    Individual,
+    Population,
+)
 from evaluator import Evaluator
-from experiment import Experiment
-from generation import Generation
-from genotype import Genotype
-from individual import Individual
-from population import Population
 from sqlalchemy.engine import Engine
 from sqlalchemy.orm import Session
 
diff --git a/examples/robot_brain_cmaes_database/plot.py b/examples/4_example_experiment_setups/4f_robot_brain_cmaes_database/plot.py
similarity index 95%
rename from examples/robot_brain_cmaes_database/plot.py
rename to examples/4_example_experiment_setups/4f_robot_brain_cmaes_database/plot.py
index 009326c70..719d928ce 100644
--- a/examples/robot_brain_cmaes_database/plot.py
+++ b/examples/4_example_experiment_setups/4f_robot_brain_cmaes_database/plot.py
@@ -3,10 +3,7 @@
 import config
 import matplotlib.pyplot as plt
 import pandas
-from experiment import Experiment
-from generation import Generation
-from individual import Individual
-from population import Population
+from database_components import Experiment, Generation, Individual, Population
 from sqlalchemy import select
 
 from revolve2.experimentation.database import OpenMethod, open_database_sqlite
diff --git a/examples/robot_brain_cmaes_database/requirements.txt b/examples/4_example_experiment_setups/4f_robot_brain_cmaes_database/requirements.txt
similarity index 100%
rename from examples/robot_brain_cmaes_database/requirements.txt
rename to examples/4_example_experiment_setups/4f_robot_brain_cmaes_database/requirements.txt
diff --git a/examples/robot_brain_cmaes_database/rerun.py b/examples/4_example_experiment_setups/4f_robot_brain_cmaes_database/rerun.py
similarity index 96%
rename from examples/robot_brain_cmaes_database/rerun.py
rename to examples/4_example_experiment_setups/4f_robot_brain_cmaes_database/rerun.py
index bfa3cd78a..f888307c1 100644
--- a/examples/robot_brain_cmaes_database/rerun.py
+++ b/examples/4_example_experiment_setups/4f_robot_brain_cmaes_database/rerun.py
@@ -3,9 +3,8 @@
 import logging
 
 import config
+from database_components import Genotype, Individual
 from evaluator import Evaluator
-from genotype import Genotype
-from individual import Individual
 from sqlalchemy import select
 from sqlalchemy.orm import Session
 
diff --git a/examples/4_example_experiment_setups/README.md b/examples/4_example_experiment_setups/README.md
new file mode 100644
index 000000000..ed0f72a72
--- /dev/null
+++ b/examples/4_example_experiment_setups/README.md
@@ -0,0 +1,12 @@
+## 4) Lets look into more complex experiment setups
+Most projects require some more complex setups to make them interesting research. 
+In this collection of examples we will show how such setups can be made and how you can incorporate databases for easy data collection.
+Additionally you will learn hwo to use the evolution abstraction layer in Revolve2.
+
+- `4a_simlpe_ea_xor` gives you a quick introduction on how you can setup a simple evolutionary algorithm for optimization.
+- In `4b_simple_ea_xor_database` we use the same logic as in the example before, but add a database to collect the information.
+- `4c_robot_bodybrain_ea` shows you how to use more abstraction objects and specifies how to optimize the brain and body of modular robots.
+- `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.
+
diff --git a/examples/physical_robot_remote/README.md b/examples/5_physical_modular_robots/5a_physical_robot_remote/README.md
similarity index 100%
rename from examples/physical_robot_remote/README.md
rename to examples/5_physical_modular_robots/5a_physical_robot_remote/README.md
diff --git a/examples/physical_robot_remote/main.py b/examples/5_physical_modular_robots/5a_physical_robot_remote/main.py
similarity index 85%
rename from examples/physical_robot_remote/main.py
rename to examples/5_physical_modular_robots/5a_physical_robot_remote/main.py
index 9e7657adc..b7ae25644 100644
--- a/examples/physical_robot_remote/main.py
+++ b/examples/5_physical_modular_robots/5a_physical_robot_remote/main.py
@@ -18,11 +18,13 @@ def make_body() -> (
 
     :returns: The created body and a tuple of all ActiveHinge objects for mapping later on.
     """
-    # A modular robot body follows a 'tree' structure.
-    # The 'Body' class automatically creates a center 'core'.
-    # From here, other modular can be attached.
-    # Modules can be attached in a rotated fashion.
-    # This can be any angle, although the original design takes into account only multiples of 90 degrees.
+    """
+    A modular robot body follows a 'tree' structure.
+    The 'Body' class automatically creates a center 'core'.
+    From here, other modular can be attached.
+    Modules can be attached in a rotated fashion.
+    This can be any angle, although the original design takes into account only multiples of 90 degrees.
+    """
     body = BodyV2()
     body.core_v2.left_face.bottom = ActiveHingeV2(RightAngles.DEG_0)
     body.core_v2.left_face.bottom.attachment = ActiveHingeV2(RightAngles.DEG_0)
@@ -50,7 +52,10 @@ def on_prepared() -> None:
 def main() -> None:
     """Remote control a physical modular robot."""
     rng = make_rng_time_seed()
-    # Create a modular robot, similar to what was done in the simulate_single_robot example. Of course, you can replace this with your own robot, such as one you have optimized using an evolutionary algorithm.
+    """
+    Create a modular robot, similar to what was done in the 1a_simulate_single_robot example.
+    Of course, you can replace this with your own robot, such as one you have optimized using an evolutionary algorithm.
+    """
     body, hinges = make_body()
     brain = BrainCpgNetworkNeighborRandom(body=body, rng=rng)
     robot = ModularRobot(body, brain)
@@ -103,6 +108,9 @@ def main() -> None:
         debug=True,
         on_prepared=on_prepared,
     )
+    """
+    Note that theoretically if you want the robot to be self controlled and not dependant on a external remote, you can run this script on the robot locally.
+    """
 
 
 if __name__ == "__main__":
diff --git a/examples/simple_ea_xor/requirements.txt b/examples/5_physical_modular_robots/5a_physical_robot_remote/requirements.txt
similarity index 100%
rename from examples/simple_ea_xor/requirements.txt
rename to examples/5_physical_modular_robots/5a_physical_robot_remote/requirements.txt
diff --git a/examples/compare_simulated_and_physical_robot/README.md b/examples/5_physical_modular_robots/5b_compare_simulated_and_physical_robot/README.md
similarity index 100%
rename from examples/compare_simulated_and_physical_robot/README.md
rename to examples/5_physical_modular_robots/5b_compare_simulated_and_physical_robot/README.md
diff --git a/examples/compare_simulated_and_physical_robot/main_physical.py b/examples/5_physical_modular_robots/5b_compare_simulated_and_physical_robot/main_physical.py
similarity index 85%
rename from examples/compare_simulated_and_physical_robot/main_physical.py
rename to examples/5_physical_modular_robots/5b_compare_simulated_and_physical_robot/main_physical.py
index 88e12af69..7c60caecf 100644
--- a/examples/compare_simulated_and_physical_robot/main_physical.py
+++ b/examples/5_physical_modular_robots/5b_compare_simulated_and_physical_robot/main_physical.py
@@ -15,11 +15,6 @@ def make_body() -> (
 
     :returns: The created body and a tuple of all ActiveHinge objects for mapping later on.
     """
-    # A modular robot body follows a 'tree' structure.
-    # The 'Body' class automatically creates a center 'core'.
-    # From here, other modular can be attached.
-    # Modules can be attached in a rotated fashion.
-    # This can be any angle, although the original design takes into account only multiples of 90 degrees.
     body = BodyV2()
     body.core_v2.left_face.bottom = ActiveHingeV2(RightAngles.DEG_0)
     body.core_v2.left_face.bottom.attachment = ActiveHingeV2(RightAngles.DEG_0)
diff --git a/examples/compare_simulated_and_physical_robot/main_simulation.py b/examples/5_physical_modular_robots/5b_compare_simulated_and_physical_robot/main_simulation.py
similarity index 100%
rename from examples/compare_simulated_and_physical_robot/main_simulation.py
rename to examples/5_physical_modular_robots/5b_compare_simulated_and_physical_robot/main_simulation.py
diff --git a/examples/simulate_single_robot/requirements.txt b/examples/5_physical_modular_robots/5b_compare_simulated_and_physical_robot/requirements.txt
similarity index 100%
rename from examples/simulate_single_robot/requirements.txt
rename to examples/5_physical_modular_robots/5b_compare_simulated_and_physical_robot/requirements.txt
diff --git a/examples/5_physical_modular_robots/README.md b/examples/5_physical_modular_robots/README.md
new file mode 100644
index 000000000..e5415b57e
--- /dev/null
+++ b/examples/5_physical_modular_robots/README.md
@@ -0,0 +1,8 @@
+## 5) Put your evolved robots into the real world.
+Simply simulating all your experiments might not be enough. 
+To validate if the experiments can reproduce on actual physical robots, you first need to build the robot.
+If you dont know how to start with this visit the [Revolve2 Documentation](https://ci-group.github.io/revolve2/). 
+Specifically look at *Creating a physical robot* and *Physical Robot Core Setup*.
+
+- In `5a_physical_robot_remote` you learn how to control your newly built robot remotely.
+- In `5b_compare_simulated_and_physical_robot` you will be able to check whether your built robot has ben built correctly, by comparing it to the simulation counterpart.
diff --git a/examples/evaluate_multiple_interacting_robots/README.md b/examples/evaluate_multiple_interacting_robots/README.md
deleted file mode 100644
index 49348e6ec..000000000
--- a/examples/evaluate_multiple_interacting_robots/README.md
+++ /dev/null
@@ -1,8 +0,0 @@
-In this example you simulate a multiple robots in the same scne, and then calculate their xy displacement.
-
-First see the 'evaluate_multiple_isolated_robots' example.
-This example is extremely similar, but will ommit some of the commentary.
-
-You learn:
-- How to put multiple robots in the same scene.
-- How to process the simulation results.
diff --git a/examples/evaluate_multiple_isolated_robots/README.md b/examples/evaluate_multiple_isolated_robots/README.md
deleted file mode 100644
index e1c4149ea..000000000
--- a/examples/evaluate_multiple_isolated_robots/README.md
+++ /dev/null
@@ -1,7 +0,0 @@
-This example simulates modular robots that do not interact, and then calculates their xy displacement.
-
-To understand better, first see the 'evaluate_single_robot' example.
-
-You learn:
-- How to simulate multiple isolated robots.
-- How to process their simulation results.
diff --git a/examples/experiment_setup/main.py b/examples/experiment_setup/main.py
deleted file mode 100644
index 50a088daf..000000000
--- a/examples/experiment_setup/main.py
+++ /dev/null
@@ -1,92 +0,0 @@
-"""Main script for the example."""
-
-import logging
-
-import config
-import numpy as np
-
-import revolve2.experimentation.rng
-from revolve2.experimentation.logging import setup_logging
-
-
-def run_experiment(num_samples: int, probability: float) -> None:
-    """
-    Run all runs of an experiment using the provided parameters.
-
-    :param num_samples: The number of samples to use for this experiment.
-    :param probability: The probablity to use for this experiment.
-    """
-    # Create a list where we will store the success ratio for each repetition.
-    success_ratios = []
-
-    for repetition in range(config.NUM_REPETITIONS):
-        logging.info(
-            f"Running experiment ( repetition {repetition} num_samples {num_samples} probability {probability} )"
-        )
-
-        # First, we set up a random number generator, or 'rng'.
-        # A rng is not actually random; it is an algorithm creating semi-randomness from a given starting number (the 'seed'), defined by you.
-        # This is both very useful and a pitfall.
-        # Using the same seed makes our experiment reproducible,
-        # but using the same seed between two seperate experiments will generate the same numbers (usually bad).
-        # As such we create a unique seed for each experiment and run.
-        # We will do this by creating a seed based on the current time.
-        # This seed will be stored in the logs.
-        # If we ever need to reproduce our results we can then use the seed from the log instead of generating a new one.
-        # The function automatically logs the seed for you, so you cannot forget to.
-        rng = revolve2.experimentation.rng.make_rng_time_seed()
-
-        # If you run with a set seed, use the following lines instead.
-        # SEED = 1234
-        # rng = revolve2.experimentation.rng.make_rng(SEED)
-
-        # Perform the experiment, sampling from a binomial distribution using the given paramaters.
-        samples = rng.binomial(n=1, p=probability, size=num_samples)
-        # Calculate the ratio of success and save it.
-        success_ratios.append(np.sum(samples == 1) / num_samples)
-
-    # Do some simple analysis using the performed repetitions.
-    std = np.std(success_ratios)
-    mean = np.mean(success_ratios)
-    logging.info(f"mean {mean} std {std}")
-
-
-def main() -> None:
-    """Run the simulation."""
-    # Set up logging.
-    # This decides the level of severity of logged messages we want to display.
-    # By default this is 'INFO' or more severe, and 'DEBUG' is excluded.
-    # Furthermore, a standard message layout is set up.
-    # If logging is not set up, important messages can be missed.
-    # We also provide a file name, so the log will be written to both the console and that file.
-    setup_logging(file_name="log.txt")
-
-    # Let's print a simple message.
-    # We use the 'info' function to give the message the 'INFO' severity.
-    logging.info("Starting program.")
-    # The following message will be invisible.
-    logging.debug(
-        "This debug message is invisible, because we set our visible severity to 'INFO' and higher."
-    )
-
-    # Our experiment will be very trivial; sample a number of times with a certain probability and determine the ratio of success.
-    # Several probabilities and number of samples will be observed, and each experiment will be repeated a few times.
-    # These parameters are defined in 'config.py'.
-    # Iterate over all parameters.
-    for num_samples in config.NUM_SAMPLES:
-        for probability in config.PROBABILITIES:
-            # And run the experiment.
-            run_experiment(
-                num_samples=num_samples,
-                probability=probability,
-            )
-
-
-# Below is a very important idiom in python that you should understand as a programmer.
-# __name__ is only set to __main__ if it is directly ran as a script.
-# If the file is imported from another file the main function is not called.
-# If you do not perform this check the main function is ALWAYS called.
-# This leads to all kinds of unintended behaviour that is hard to debug.
-# See also various articles, such as https://stackoverflow.com/questions/419163/what-does-if-name-main-do
-if __name__ == "__main__":
-    main()
diff --git a/examples/simple_ea_xor/evaluate.py b/examples/simple_ea_xor/evaluate.py
deleted file mode 100644
index 86305803a..000000000
--- a/examples/simple_ea_xor/evaluate.py
+++ /dev/null
@@ -1,48 +0,0 @@
-"""Evaluation functions."""
-
-import numpy as np
-import numpy.typing as npt
-
-
-def evaluate_network(
-    params: npt.NDArray[np.float_], inputs: npt.NDArray[np.float_]
-) -> np.float_:
-    """
-    Pass two inputs through a fully connected relu network.
-
-    :param params: The parameters to evaluate.
-    :param inputs: Inputs for network. 2x1 floats.
-    :returns: The output of the network.
-    """
-    # First layer
-    n0 = np.maximum(0, np.dot(params[:2], inputs) + params[2])
-    n1 = np.maximum(0, np.dot(params[3:5], inputs) + params[5])
-
-    # Second layer
-    output: np.float_ = np.maximum(0, n0 * params[6] + n1 * params[7] + params[8])
-
-    return output
-
-
-def evaluate(parameters: npt.NDArray[np.float_]) -> float:
-    """
-    Measure one set of parameters.
-
-    :param parameters: The parameters to measure.
-    :returns: Negative sum of squared errors and each individual error. 5x1 floats.
-    """
-    # Define all possible inputs for xor and the expected outputs
-    inputs = np.array([[0, 0], [1, 0], [0, 1], [1, 1]])
-    expected_outputs = np.array([0, 1, 1, 0])
-
-    # Evaluate the provided network parameters
-    outputs = np.array([evaluate_network(parameters, input) for input in inputs])
-
-    # Calculate the difference between the network outputs and the expect outputs
-    errors = outputs - expected_outputs
-
-    # Return the sum of squared errors.
-    # 0 would be an optimizal result.
-    # We invert so we can maximize the fitness instead of minimize.
-    # Finally we convert from a numpy float_ type to the python float type. This is not really important.
-    return float(-np.sum(errors**2))
diff --git a/examples/simple_ea_xor/main.py b/examples/simple_ea_xor/main.py
deleted file mode 100644
index e12c3f6e8..000000000
--- a/examples/simple_ea_xor/main.py
+++ /dev/null
@@ -1,157 +0,0 @@
-"""Main script for the example."""
-
-import logging
-
-import config
-import numpy as np
-import numpy.typing as npt
-from evaluate import evaluate
-from genotype import Genotype
-from individual import Individual
-
-from revolve2.experimentation.logging import setup_logging
-from revolve2.experimentation.optimization.ea import population_management, selection
-from revolve2.experimentation.rng import make_rng_time_seed
-
-
-def select_parents(
-    rng: np.random.Generator,
-    population: list[Individual],
-    offspring_size: int,
-) -> npt.NDArray[np.float_]:
-    """
-    Select pairs of parents using a tournament.
-
-    :param rng: Random number generator.
-    :param population: The population to select from.
-    :param offspring_size: The number of parent pairs to select.
-    :returns: Pairs of indices of selected parents. offspring_size x 2 ints.
-    """
-    return np.array(
-        [
-            selection.multiple_unique(
-                2,
-                [individual.genotype for individual in population],
-                [individual.fitness for individual in population],
-                lambda _, fitnesses: selection.tournament(rng, fitnesses, k=1),
-            )
-            for _ in range(offspring_size)
-        ],
-    )
-
-
-def select_survivors(
-    rng: np.random.Generator,
-    original_population: list[Individual],
-    offspring_population: list[Individual],
-) -> list[Individual]:
-    """
-    Select survivors using a tournament.
-
-    :param rng: Random number generator.
-    :param original_population: The population the parents come from.
-    :param offspring_population: The offspring.
-    :returns: A newly created population.
-    """
-    original_survivors, offspring_survivors = population_management.steady_state(
-        [i.genotype for i in original_population],
-        [i.fitness for i in original_population],
-        [i.genotype for i in offspring_population],
-        [i.fitness for i in offspring_population],
-        lambda n, genotypes, fitnesses: selection.multiple_unique(
-            n,
-            genotypes,
-            fitnesses,
-            lambda _, fitnesses: selection.tournament(rng, fitnesses, k=2),
-        ),
-    )
-
-    return [
-        Individual(
-            original_population[i].genotype,
-            original_population[i].fitness,
-        )
-        for i in original_survivors
-    ] + [
-        Individual(
-            offspring_population[i].genotype,
-            offspring_population[i].fitness,
-        )
-        for i in offspring_survivors
-    ]
-
-
-def main() -> None:
-    """Run the program."""
-    # Set up logging.
-    setup_logging()
-
-    # Set up the random number generator.
-    rng = make_rng_time_seed()
-
-    # Create an initial population.
-    logging.info("Generating initial population.")
-    initial_genotypes = [
-        Genotype.random(
-            rng=rng,
-        )
-        for _ in range(config.POPULATION_SIZE)
-    ]
-
-    # Evaluate the initial population.
-    logging.info("Evaluating initial population.")
-    initial_fitnesses = [
-        evaluate(genotype.parameters) for genotype in initial_genotypes
-    ]
-
-    # Create a population of individuals, combining genotype with fitness.
-    population = [
-        Individual(genotype, fitness)
-        for genotype, fitness in zip(initial_genotypes, initial_fitnesses)
-    ]
-
-    # Set the current generation to 0.
-    generation_index = 0
-
-    # Start the actual optimization process.
-    logging.info("Start optimization process.")
-    while generation_index < config.NUM_GENERATIONS:
-        logging.info(f"Generation {generation_index + 1} / {config.NUM_GENERATIONS}.")
-
-        # Create offspring.
-        parents = select_parents(rng, population, config.OFFSPRING_SIZE)
-        offspring_genotypes = [
-            Genotype.crossover(
-                population[parent1_i].genotype,
-                population[parent2_i].genotype,
-                rng,
-            ).mutate(rng)
-            for parent1_i, parent2_i in parents
-        ]
-
-        # Evaluate the offspring.
-        offspring_fitnesses = [
-            evaluate(genotype.parameters) for genotype in offspring_genotypes
-        ]
-
-        logging.info(f"Max fitness: {max(offspring_fitnesses)}")
-
-        # Make an intermediate offspring population.
-        offspring_population = [
-            Individual(genotype, fitness)
-            for genotype, fitness in zip(offspring_genotypes, offspring_fitnesses)
-        ]
-
-        # Create the next generation by selecting survivors between original population and offspring.
-        population = select_survivors(
-            rng,
-            population,
-            offspring_population,
-        )
-
-        # Increase the generation index counter.
-        generation_index += 1
-
-
-if __name__ == "__main__":
-    main()
diff --git a/examples/simple_ea_xor_database/evaluate.py b/examples/simple_ea_xor_database/evaluate.py
deleted file mode 100644
index 86305803a..000000000
--- a/examples/simple_ea_xor_database/evaluate.py
+++ /dev/null
@@ -1,48 +0,0 @@
-"""Evaluation functions."""
-
-import numpy as np
-import numpy.typing as npt
-
-
-def evaluate_network(
-    params: npt.NDArray[np.float_], inputs: npt.NDArray[np.float_]
-) -> np.float_:
-    """
-    Pass two inputs through a fully connected relu network.
-
-    :param params: The parameters to evaluate.
-    :param inputs: Inputs for network. 2x1 floats.
-    :returns: The output of the network.
-    """
-    # First layer
-    n0 = np.maximum(0, np.dot(params[:2], inputs) + params[2])
-    n1 = np.maximum(0, np.dot(params[3:5], inputs) + params[5])
-
-    # Second layer
-    output: np.float_ = np.maximum(0, n0 * params[6] + n1 * params[7] + params[8])
-
-    return output
-
-
-def evaluate(parameters: npt.NDArray[np.float_]) -> float:
-    """
-    Measure one set of parameters.
-
-    :param parameters: The parameters to measure.
-    :returns: Negative sum of squared errors and each individual error. 5x1 floats.
-    """
-    # Define all possible inputs for xor and the expected outputs
-    inputs = np.array([[0, 0], [1, 0], [0, 1], [1, 1]])
-    expected_outputs = np.array([0, 1, 1, 0])
-
-    # Evaluate the provided network parameters
-    outputs = np.array([evaluate_network(parameters, input) for input in inputs])
-
-    # Calculate the difference between the network outputs and the expect outputs
-    errors = outputs - expected_outputs
-
-    # Return the sum of squared errors.
-    # 0 would be an optimizal result.
-    # We invert so we can maximize the fitness instead of minimize.
-    # Finally we convert from a numpy float_ type to the python float type. This is not really important.
-    return float(-np.sum(errors**2))
diff --git a/examples/simple_ea_xor_database/main.py b/examples/simple_ea_xor_database/main.py
deleted file mode 100644
index 3c9587722..000000000
--- a/examples/simple_ea_xor_database/main.py
+++ /dev/null
@@ -1,225 +0,0 @@
-"""Main script for the example."""
-
-import logging
-
-import config
-import numpy as np
-import numpy.typing as npt
-from base import Base
-from evaluate import evaluate
-from experiment import Experiment
-from generation import Generation
-from genotype import Genotype
-from individual import Individual
-from population import Population
-from sqlalchemy.engine import Engine
-from sqlalchemy.orm import Session
-
-from revolve2.experimentation.database import OpenMethod, open_database_sqlite
-from revolve2.experimentation.logging import setup_logging
-from revolve2.experimentation.optimization.ea import population_management, selection
-from revolve2.experimentation.rng import make_rng, seed_from_time
-
-
-def select_parents(
-    rng: np.random.Generator,
-    population: Population,
-    offspring_size: int,
-) -> npt.NDArray[np.float_]:
-    """
-    Select pairs of parents using a tournament.
-
-    :param rng: Random number generator.
-    :param population: The population to select from.
-    :param offspring_size: The number of parent pairs to select.
-    :returns: Pairs of indices of selected parents. offspring_size x 2 ints.
-    """
-    return np.array(
-        [
-            selection.multiple_unique(
-                2,
-                [individual.genotype for individual in population.individuals],
-                [individual.fitness for individual in population.individuals],
-                lambda _, fitnesses: selection.tournament(rng, fitnesses, k=1),
-            )
-            for _ in range(offspring_size)
-        ],
-    )
-
-
-def select_survivors(
-    rng: np.random.Generator,
-    original_population: Population,
-    offspring_population: Population,
-) -> Population:
-    """
-    Select survivors using a tournament.
-
-    :param rng: Random number generator.
-    :param original_population: The population the parents come from.
-    :param offspring_population: The offspring.
-    :returns: A newly created population.
-    """
-    original_survivors, offspring_survivors = population_management.steady_state(
-        [i.genotype for i in original_population.individuals],
-        [i.fitness for i in original_population.individuals],
-        [i.genotype for i in offspring_population.individuals],
-        [i.fitness for i in offspring_population.individuals],
-        lambda n, genotypes, fitnesses: selection.multiple_unique(
-            n,
-            genotypes,
-            fitnesses,
-            lambda _, fitnesses: selection.tournament(rng, fitnesses, k=2),
-        ),
-    )
-
-    return Population(
-        individuals=[
-            Individual(
-                genotype=original_population.individuals[i].genotype,
-                fitness=original_population.individuals[i].fitness,
-            )
-            for i in original_survivors
-        ]
-        + [
-            Individual(
-                genotype=offspring_population.individuals[i].genotype,
-                fitness=offspring_population.individuals[i].fitness,
-            )
-            for i in offspring_survivors
-        ]
-    )
-
-
-def run_experiment(dbengine: Engine) -> None:
-    """
-    Run an experiment.
-
-    :param dbengine: An openened database with matching initialize database structure.
-    """
-    logging.info("----------------")
-    logging.info("Start experiment")
-
-    # Set up the random number generator.
-    # We are manually generating the seed, because we are going to save it in our database.
-    rng_seed = seed_from_time()
-    rng = make_rng(rng_seed)
-
-    # Create a new experiment instance.
-    experiment = Experiment(rng_seed=rng_seed)
-
-    # Save it to the database.
-    # A session manages multiple changes to a database,
-    # which then can either be committed(accepted) or a rolled back(aborted) in case something bad happens in our code.
-    # We add the experiment and commit as nothing can go wrong.
-    logging.info("Saving experiment configuration.")
-    with Session(dbengine) as session:
-        session.add(experiment)
-        session.commit()
-
-    # Create an initial population.
-    logging.info("Generating initial population.")
-    initial_genotypes = [
-        Genotype.random(
-            rng=rng,
-        )
-        for _ in range(config.POPULATION_SIZE)
-    ]
-
-    # Evaluate the initial population.
-    logging.info("Evaluating initial population.")
-    initial_fitnesses = [
-        evaluate(genotype.parameters) for genotype in initial_genotypes
-    ]
-
-    # Create a population of individuals, combining genotype with fitness.
-    population = Population(
-        individuals=[
-            Individual(genotype=genotype, fitness=fitness)
-            for genotype, fitness in zip(initial_genotypes, initial_fitnesses)
-        ]
-    )
-
-    # Finish the zeroth generation and save it to the database.
-    # The generation references the experiment so we later know which experiment it was part of.
-    generation = Generation(
-        experiment=experiment, generation_index=0, population=population
-    )
-    logging.info("Saving generation.")
-    with Session(dbengine, expire_on_commit=False) as session:
-        session.add(generation)
-        session.commit()
-
-    # Start the actual optimization process.
-    logging.info("Start optimization process.")
-    while generation.generation_index < config.NUM_GENERATIONS:
-        logging.info(
-            f"Generation {generation.generation_index + 1} / {config.NUM_GENERATIONS}."
-        )
-
-        # Create offspring.
-        parents = select_parents(rng, population, config.OFFSPRING_SIZE)
-        offspring_genotypes = [
-            Genotype.crossover(
-                population.individuals[parent1_i].genotype,
-                population.individuals[parent2_i].genotype,
-                rng,
-            ).mutate(rng)
-            for parent1_i, parent2_i in parents
-        ]
-
-        # Evaluate the offspring.
-        offspring_fitnesses = [
-            evaluate(genotype.parameters) for genotype in offspring_genotypes
-        ]
-
-        # Make an intermediate offspring population.
-        offspring_population = Population(
-            individuals=[
-                Individual(genotype=genotype, fitness=fitness)
-                for genotype, fitness in zip(offspring_genotypes, offspring_fitnesses)
-            ]
-        )
-
-        # Create the next generation by selecting survivors between original population and offspring.
-        population = select_survivors(
-            rng,
-            population,
-            offspring_population,
-        )
-
-        # Make it all into a generation and save it to the database.
-        generation = Generation(
-            experiment=experiment,
-            generation_index=generation.generation_index + 1,
-            population=population,
-        )
-        logging.info("Saving generation.")
-        with Session(dbengine, expire_on_commit=False) as session:
-            session.add(generation)
-            session.commit()
-
-
-def main() -> None:
-    """Run the program."""
-    # Set up logging.
-    setup_logging(file_name="log.txt")
-
-    # Open the database, only if it does not already exist.
-    # If it did something when wrong in a previous run of this program,
-    # and we must manually figure out what to do with the existing database.
-    # (maybe throw away?)
-    dbengine = open_database_sqlite(
-        config.DATABASE_FILE, open_method=OpenMethod.NOT_EXISTS_AND_CREATE
-    )
-    # Create the structure of the database.
-    # Take a look at the 'Base' class.
-    Base.metadata.create_all(dbengine)
-
-    # We are running several repetitions of the same experiment.
-    for _ in range(config.NUM_REPETITIONS):
-        run_experiment(dbengine)
-
-
-if __name__ == "__main__":
-    main()
diff --git a/project.yml b/project.yml
index 28b87c84f..2ec2f3e47 100644
--- a/project.yml
+++ b/project.yml
@@ -10,22 +10,22 @@ platform_independent_packages:
 platform_dependent_packages:
   - ci_group
 examples:
-  - brain_with_feedback
-  - custom_brain
-  - custom_terrain
-  - compare_simulated_and_physical_robot
-  - evaluate_multiple_interacting_robots
-  - evaluate_multiple_isolated_robots
-  - evaluate_single_robot
-  - experiment_setup
-  - physical_robot_remote
-  - robot_bodybrain_ea
-  - robot_bodybrain_ea_database
-  - robot_brain_cmaes
-  - robot_brain_cmaes_database
-  - simple_ea_xor
-  - simple_ea_xor_database
-  - simulate_single_robot
+  - 1_simulator_basics/1a_simulate_single_robot
+  - 1_simulator_basics/1b_custom_terrain
+  - 2_modular_robot_basics/2a_custom_brain
+  - 2_modular_robot_basics/2b_brain_with_feedback
+  - 3_experiment_foundations/3a_experiment_setup
+  - 3_experiment_foundations/3b_evaluate_single_robot
+  - 3_experiment_foundations/3c_evaluate_multiple_isolated_robots
+  - 3_experiment_foundations/3d_evaluate_multiple_interacting_robots
+  - 4_example_experiment_setups/4a_simple_ea_xor
+  - 4_example_experiment_setups/4b_simple_ea_xor_database
+  - 4_example_experiment_setups/4c_robot_bodybrain_ea
+  - 4_example_experiment_setups/4d_robot_bodybrain_ea_database
+  - 4_example_experiment_setups/4e_robot_brain_cmaes
+  - 4_example_experiment_setups/4f_robot_brain_cmaes_database
+  - 5_physical_modular_robots/5a_physical_robot_remote
+  - 5_physical_modular_robots/5b_compare_simulated_and_physical_robot
 tests-dir: tests
 examples-dir: examples
 revolve2-namespace: revolve2
diff --git a/requirements_dev.txt b/requirements_dev.txt
index 25e20b31f..a1df51e0e 100644
--- a/requirements_dev.txt
+++ b/requirements_dev.txt
@@ -9,7 +9,7 @@
 -e simulation[dev]
 -r ./codetools/requirements.txt
 -r ./docs/requirements.txt
--r ./examples/robot_bodybrain_ea_database/requirements.txt
--r ./examples/robot_brain_cmaes_database/requirements.txt
--r ./examples/simple_ea_xor_database/requirements.txt
+-r ./examples/4_example_experiment_setups/4d_robot_bodybrain_ea_database/requirements.txt
+-r ./examples/4_example_experiment_setups/4f_robot_brain_cmaes_database/requirements.txt
+-r ./examples/4_example_experiment_setups/4b_simple_ea_xor_database/requirements.txt
 -r ./tests/requirements.txt
diff --git a/requirements_editable.txt b/requirements_editable.txt
index a3f2b0252..73a42508c 100644
--- a/requirements_editable.txt
+++ b/requirements_editable.txt
@@ -7,6 +7,6 @@
 -e modular_robot_physical[remote]
 -e modular_robot[dev]
 -e simulation[dev]
--r ./examples/robot_bodybrain_ea_database/requirements.txt
--r ./examples/robot_brain_cmaes_database/requirements.txt
--r ./examples/simple_ea_xor_database/requirements.txt
+-r ./examples/4_example_experiment_setups/4d_robot_bodybrain_ea_database/requirements.txt
+-r ./examples/4_example_experiment_setups/4f_robot_brain_cmaes_database/requirements.txt
+-r ./examples/4_example_experiment_setups/4b_simple_ea_xor_database/requirements.txt


Test patch:
diff --git a/tests/examples/test_brain_with_feedback.py b/tests/examples/test_brain_with_feedback.py
index ed10f5d73..e26c9e28a 100644
--- a/tests/examples/test_brain_with_feedback.py
+++ b/tests/examples/test_brain_with_feedback.py
@@ -9,10 +9,12 @@
 
 def test_brain_with_feedback(mocker: Mock) -> None:
     """
-    Test brain_with_feedback example can complete.
+    Test 2b_brain_with_feedback example can complete.
     :param mocker: The mock object.
     """
-    exp_dir = os.path.join(EXAMPLES_DIR, "brain_with_feedback")
+    exp_dir = os.path.join(
+        EXAMPLES_DIR, "2_modular_robot_basics/2b_brain_with_feedback"
+    )
     # Clear any previously imported modules from examples directory from cache
     clear_exp_modules_from_cache()
     # Add examples directory to path, so we can import them without the examples being packages.
diff --git a/tests/examples/test_evaluate_multiple_isolated_robots.py b/tests/examples/test_evaluate_multiple_isolated_robots.py
index 68ffaf47d..2389f4028 100644
--- a/tests/examples/test_evaluate_multiple_isolated_robots.py
+++ b/tests/examples/test_evaluate_multiple_isolated_robots.py
@@ -11,11 +11,13 @@
 
 def test_evaluate_multiple_isolated_robots(mocker: Mock) -> None:
     """
-    Test evaluate_multiple_isolated_robots example can complete.
+    Test 3c_evaluate_multiple_isolated_robots example can complete.
 
     :param mocker: The mock object.
     """
-    exp_dir = os.path.join(EXAMPLES_DIR, "evaluate_multiple_isolated_robots")
+    exp_dir = os.path.join(
+        EXAMPLES_DIR, "3_experiment_foundations/3c_evaluate_multiple_isolated_robots"
+    )
     # Clear any previously imported modules from examples directory from cache
     clear_exp_modules_from_cache()
     # Add examples directory to path, so we can import them without the examples being packages.
diff --git a/tests/examples/test_evaluate_single_robot.py b/tests/examples/test_evaluate_single_robot.py
index 963cc5c1a..083c201ed 100644
--- a/tests/examples/test_evaluate_single_robot.py
+++ b/tests/examples/test_evaluate_single_robot.py
@@ -9,11 +9,13 @@
 
 def test_evaluate_single_robot(mocker: Mock) -> None:
     """
-    Test evaluate_single_robot example can complete.
+    Test 3b_evaluate_single_robot example can complete.
 
     :param mocker: The mock object.
     """
-    exp_dir = os.path.join(EXAMPLES_DIR, "evaluate_single_robot")
+    exp_dir = os.path.join(
+        EXAMPLES_DIR, "3_experiment_foundations/3b_evaluate_single_robot"
+    )
     # Clear any previously imported modules from examples directory from cache
     clear_exp_modules_from_cache()
     # Add examples directory to path, so we can import them without the examples being packages.
diff --git a/tests/examples/test_experiment_setup.py b/tests/examples/test_experiment_setup.py
index 6e8ec1106..ff8b81e5e 100644
--- a/tests/examples/test_experiment_setup.py
+++ b/tests/examples/test_experiment_setup.py
@@ -4,6 +4,6 @@
 
 
 def test_experiment_setup() -> None:
-    """Test experiment_setup example can complete."""
-    exp_dir = os.path.join(EXAMPLES_DIR, "experiment_setup")
+    """Test 3a_experiment_setup example can complete."""
+    exp_dir = os.path.join(EXAMPLES_DIR, "3_experiment_foundations/3a_experiment_setup")
     assert_command_succeeds(["python", os.path.join(exp_dir, "main.py")])
diff --git a/tests/examples/test_robot_bodybrain_ea_database.py b/tests/examples/test_robot_bodybrain_ea_database.py
index efa46e945..4a830a2ae 100644
--- a/tests/examples/test_robot_bodybrain_ea_database.py
+++ b/tests/examples/test_robot_bodybrain_ea_database.py
@@ -8,14 +8,16 @@
 
 def test_robot_bodybrain_ea_database(mocker: Mock, tmpdir: str) -> None:
     """
-    Test if robot_bodybrain_ea_database example can complete.
+    Test if 4d_robot_bodybrain_ea_database example can complete.
 
     The database file is written to a temporary directory, which is automatically deleted after the test.
 
     :param mocker: The mock object.
     :param tmpdir: Temporary directory for spawning database file.
     """
-    exp_dir = os.path.join(EXAMPLES_DIR, "robot_bodybrain_ea_database")
+    exp_dir = os.path.join(
+        EXAMPLES_DIR, "4_example_experiment_setups/4d_robot_bodybrain_ea_database"
+    )
 
     # Clear any previously imported modules from examples directory from cache
     clear_exp_modules_from_cache()
diff --git a/tests/examples/test_robot_brain_cmaes.py b/tests/examples/test_robot_brain_cmaes.py
index ee461b2b7..d701a33c3 100644
--- a/tests/examples/test_robot_brain_cmaes.py
+++ b/tests/examples/test_robot_brain_cmaes.py
@@ -8,11 +8,13 @@
 
 def test_robot_brain_cmaes(mocker: Mock) -> None:
     """
-    Test robot_brain_cmaes example can complete.
+    Test 4e_robot_brain_cmaes example can complete.
 
     :param mocker: The mock object.
     """
-    exp_dir = os.path.join(EXAMPLES_DIR, "robot_brain_cmaes")
+    exp_dir = os.path.join(
+        EXAMPLES_DIR, "4_example_experiment_setups/4e_robot_brain_cmaes"
+    )
 
     # Clear any previously imported modules from examples directory from cache
     clear_exp_modules_from_cache()
diff --git a/tests/examples/test_simple_ea_xor.py b/tests/examples/test_simple_ea_xor.py
index 961f6200f..8dc87622c 100644
--- a/tests/examples/test_simple_ea_xor.py
+++ b/tests/examples/test_simple_ea_xor.py
@@ -4,6 +4,6 @@
 
 
 def test_simple_ea_xor() -> None:
-    """Test simple_ea_xor example can complete."""
-    exp_dir = os.path.join(EXAMPLES_DIR, "simple_ea_xor")
+    """Test 4a_simple_ea_xor example can complete."""
+    exp_dir = os.path.join(EXAMPLES_DIR, "4_example_experiment_setups/4a_simple_ea_xor")
     assert_command_succeeds(["python", os.path.join(exp_dir, "main.py")])
diff --git a/tests/examples/test_simulate_single_robot.py b/tests/examples/test_simulate_single_robot.py
index 4daf43a2b..74481c759 100644
--- a/tests/examples/test_simulate_single_robot.py
+++ b/tests/examples/test_simulate_single_robot.py
@@ -9,11 +9,11 @@
 
 def test_simulate_single_robot(mocker: Mock) -> None:
     """
-    Test simulate_single_robot example can complete.
+    Test 1a_simulate_single_robot example can complete.
 
     :param mocker: The mock object.
     """
-    exp_dir = os.path.join(EXAMPLES_DIR, "simulate_single_robot")
+    exp_dir = os.path.join(EXAMPLES_DIR, "1_simulator_basics/1a_simulate_single_robot")
     # Clear any previously imported modules from examples directory from cache
     clear_exp_modules_from_cache()
     # Add examples directory to path, so we can import them without the examples being packages.