From a304a7d3bbc7e15b6d2e1b3a3638ae15e14c95e9 Mon Sep 17 00:00:00 2001 From: A-lamo Date: Thu, 30 Jan 2025 11:38:53 +0100 Subject: [PATCH 01/11] gui stuff --- .../optimization/ea/selection/__init__.py | 11 +- .../optimization/ea/selection/_roulette.py | 35 ++ gui/backend_example/README.md | 7 + gui/backend_example/_multineat_params.py | 47 +++ gui/backend_example/config.py | 8 + .../config_simulation_parameters.py | 4 + .../database_components/__init__.py | 10 + .../database_components/_base.py | 9 + .../database_components/_experiment.py | 16 + .../database_components/_generation.py | 26 ++ .../database_components/_genotype.py | 90 +++++ .../database_components/_individual.py | 17 + .../database_components/_population.py | 12 + gui/backend_example/evaluator.py | 74 ++++ gui/backend_example/fitness_functions.py | 75 +++++ gui/backend_example/main.py | 317 ++++++++++++++++++ gui/backend_example/main_from_gui.py | 154 +++++++++ gui/backend_example/plot.py | 105 ++++++ gui/backend_example/reproducer_methods.py | 61 ++++ gui/backend_example/requirements.txt | 2 + gui/backend_example/rerun.py | 46 +++ gui/backend_example/selector_methods.py | 163 +++++++++ gui/backend_example/simulation_parameters.py | 27 ++ gui/backend_example/terrains.py | 161 +++++++++ gui/viewer/main_window.py | 317 ++++++++++++++++++ gui/viewer/parameters.py | 0 gui/viewer/parsing.py | 63 ++++ gui/viewer/results_viewer.py | 0 gui/viewer/styles.qss | 0 .../revolve2/modular_robot/body/_module.py | 1 + .../_brain_cpg_network_neighbor.py | 2 +- .../cppnwin/modular_robot/v2/_body_develop.py | 64 ++-- .../modular_robot/v2/_body_genotype_v2.py | 41 ++- 33 files changed, 1933 insertions(+), 32 deletions(-) create mode 100644 experimentation/revolve2/experimentation/optimization/ea/selection/_roulette.py create mode 100644 gui/backend_example/README.md create mode 100644 gui/backend_example/_multineat_params.py create mode 100644 gui/backend_example/config.py create mode 100644 gui/backend_example/config_simulation_parameters.py create mode 100644 gui/backend_example/database_components/__init__.py create mode 100644 gui/backend_example/database_components/_base.py create mode 100644 gui/backend_example/database_components/_experiment.py create mode 100644 gui/backend_example/database_components/_generation.py create mode 100644 gui/backend_example/database_components/_genotype.py create mode 100644 gui/backend_example/database_components/_individual.py create mode 100644 gui/backend_example/database_components/_population.py create mode 100644 gui/backend_example/evaluator.py create mode 100644 gui/backend_example/fitness_functions.py create mode 100644 gui/backend_example/main.py create mode 100644 gui/backend_example/main_from_gui.py create mode 100644 gui/backend_example/plot.py create mode 100644 gui/backend_example/reproducer_methods.py create mode 100644 gui/backend_example/requirements.txt create mode 100644 gui/backend_example/rerun.py create mode 100644 gui/backend_example/selector_methods.py create mode 100644 gui/backend_example/simulation_parameters.py create mode 100644 gui/backend_example/terrains.py create mode 100644 gui/viewer/main_window.py create mode 100644 gui/viewer/parameters.py create mode 100644 gui/viewer/parsing.py create mode 100644 gui/viewer/results_viewer.py create mode 100644 gui/viewer/styles.qss diff --git a/experimentation/revolve2/experimentation/optimization/ea/selection/__init__.py b/experimentation/revolve2/experimentation/optimization/ea/selection/__init__.py index 977e1e37b..29a3846c0 100644 --- a/experimentation/revolve2/experimentation/optimization/ea/selection/__init__.py +++ b/experimentation/revolve2/experimentation/optimization/ea/selection/__init__.py @@ -1,8 +1,9 @@ """Functions for selecting individuals from populations in EA algorithms.""" -from ._multiple_unique import multiple_unique -from ._pareto_frontier import pareto_frontier -from ._topn import topn -from ._tournament import tournament +from revolve2.experimentation.optimization.ea.selection._multiple_unique import multiple_unique +from revolve2.experimentation.optimization.ea.selection._pareto_frontier import pareto_frontier +from revolve2.experimentation.optimization.ea.selection._topn import topn +from revolve2.experimentation.optimization.ea.selection._tournament import tournament +from revolve2.experimentation.optimization.ea.selection._roulette import roulette -__all__ = ["multiple_unique", "pareto_frontier", "topn", "tournament"] +__all__ = ["multiple_unique", "pareto_frontier", "topn", "tournament", "roulette"] diff --git a/experimentation/revolve2/experimentation/optimization/ea/selection/_roulette.py b/experimentation/revolve2/experimentation/optimization/ea/selection/_roulette.py new file mode 100644 index 000000000..b0e4189e9 --- /dev/null +++ b/experimentation/revolve2/experimentation/optimization/ea/selection/_roulette.py @@ -0,0 +1,35 @@ +import random +from typing import TypeVar + +from ._argsort import argsort +from ._supports_lt import SupportsLt + +Genotype = TypeVar("Genotype") +Fitness = TypeVar("Fitness", bound=SupportsLt) + +def roulette(n: int, genotypes: list[Genotype], fitnesses: list[Fitness]) -> list[int]: + """ + Perform roulette wheel selection to choose n genotypes probabilistically based on fitness. + + :param n: The number of genotypes to select. + :param genotypes: The genotypes. Ignored, but kept for compatibility with other selection functions. + :param fitnesses: Fitnesses of the genotypes. + :returns: Indices of the selected genotypes. + """ + assert len(fitnesses) >= n, "Number of selections cannot exceed population size" + + # Normalize fitness values to ensure all are positive + min_fitness = min(fitnesses) + if min_fitness < 0: + fitnesses = [f - min_fitness for f in fitnesses] # Shift all values to be positive + + total_fitness = sum(fitnesses) + assert total_fitness > 0, "Total fitness must be greater than zero for roulette selection" + + # Compute selection probabilities + probabilities = [f / total_fitness for f in fitnesses] + + # Perform roulette wheel selection + selected_indices = random.choices(range(len(fitnesses)), weights=probabilities, k=n) + + return selected_indices \ No newline at end of file diff --git a/gui/backend_example/README.md b/gui/backend_example/README.md new file mode 100644 index 000000000..5989f793d --- /dev/null +++ b/gui/backend_example/README.md @@ -0,0 +1,7 @@ +This is the `robot_bodybrain_ea` example, but with added saving of results to a database. + +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. +Running `plot.py` allows you to plot the robots fitness metrics over each generation. diff --git a/gui/backend_example/_multineat_params.py b/gui/backend_example/_multineat_params.py new file mode 100644 index 000000000..22c69956b --- /dev/null +++ b/gui/backend_example/_multineat_params.py @@ -0,0 +1,47 @@ +import multineat + + +def get_multineat_params() -> multineat.Parameters: + """ + Get Multineat Parameters. + + :returns: The parameters. + """ + multineat_params = multineat.Parameters() + + multineat_params.MutateRemLinkProb = 0.02 + multineat_params.RecurrentProb = 0.0 + multineat_params.OverallMutationRate = 0.15 + multineat_params.MutateAddLinkProb = 0.08 + multineat_params.MutateAddNeuronProb = 0.01 + multineat_params.MutateWeightsProb = 0.90 + multineat_params.MaxWeight = 8.0 + multineat_params.WeightMutationMaxPower = 0.2 + multineat_params.WeightReplacementMaxPower = 1.0 + multineat_params.MutateActivationAProb = 0.0 + multineat_params.ActivationAMutationMaxPower = 0.5 + multineat_params.MinActivationA = 0.05 + multineat_params.MaxActivationA = 6.0 + + multineat_params.MutateNeuronActivationTypeProb = 0.03 + + multineat_params.MutateOutputActivationFunction = False + + multineat_params.ActivationFunction_SignedSigmoid_Prob = 0.0 + multineat_params.ActivationFunction_UnsignedSigmoid_Prob = 0.0 + multineat_params.ActivationFunction_Tanh_Prob = 1.0 + multineat_params.ActivationFunction_TanhCubic_Prob = 0.0 + multineat_params.ActivationFunction_SignedStep_Prob = 1.0 + multineat_params.ActivationFunction_UnsignedStep_Prob = 0.0 + multineat_params.ActivationFunction_SignedGauss_Prob = 1.0 + multineat_params.ActivationFunction_UnsignedGauss_Prob = 0.0 + multineat_params.ActivationFunction_Abs_Prob = 0.0 + multineat_params.ActivationFunction_SignedSine_Prob = 1.0 + multineat_params.ActivationFunction_UnsignedSine_Prob = 0.0 + multineat_params.ActivationFunction_Linear_Prob = 1.0 + + multineat_params.MutateNeuronTraitsProb = 0.0 + multineat_params.MutateLinkTraitsProb = 0.0 + + multineat_params.AllowLoops = False + return multineat_params diff --git a/gui/backend_example/config.py b/gui/backend_example/config.py new file mode 100644 index 000000000..42e51eace --- /dev/null +++ b/gui/backend_example/config.py @@ -0,0 +1,8 @@ +DATABASE_FILE = "database.sqlite" +GENERATIONAL = False +STEADY_STATE = True +NUM_GENERATIONS = 100 +NUM_REPETITIONS = 1 +NUM_SIMULATORS = 8 +OFFSPRING_SIZE = 100 +POPULATION_SIZE = 100 diff --git a/gui/backend_example/config_simulation_parameters.py b/gui/backend_example/config_simulation_parameters.py new file mode 100644 index 000000000..f2f66d3b1 --- /dev/null +++ b/gui/backend_example/config_simulation_parameters.py @@ -0,0 +1,4 @@ +STANDARD_CONTROL_FREQUENCY = 20 +STANDARD_SAMPLING_FREQUENCY = 5 +STANDARD_SIMULATION_TIME = 30 +STANDARD_SIMULATION_TIMESTEP = 0.001 diff --git a/gui/backend_example/database_components/__init__.py b/gui/backend_example/database_components/__init__.py new file mode 100644 index 000000000..5e8ce692e --- /dev/null +++ b/gui/backend_example/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/gui/backend_example/database_components/_base.py b/gui/backend_example/database_components/_base.py new file mode 100644 index 000000000..4e6daad0a --- /dev/null +++ b/gui/backend_example/database_components/_base.py @@ -0,0 +1,9 @@ +"""Base class.""" + +import sqlalchemy.orm as orm + + +class Base(orm.MappedAsDataclass, orm.DeclarativeBase): + """Base class for all SQLAlchemy models in this example.""" + + pass diff --git a/gui/backend_example/database_components/_experiment.py b/gui/backend_example/database_components/_experiment.py new file mode 100644 index 000000000..e091ed409 --- /dev/null +++ b/gui/backend_example/database_components/_experiment.py @@ -0,0 +1,16 @@ +"""Experiment class.""" + +import sqlalchemy.orm as orm + +from revolve2.experimentation.database import HasId + +from ._base import Base + + +class Experiment(Base, HasId): + """Experiment description.""" + + __tablename__ = "experiment" + + # The seed for the rng. + rng_seed: orm.Mapped[int] = orm.mapped_column(nullable=False) diff --git a/gui/backend_example/database_components/_generation.py b/gui/backend_example/database_components/_generation.py new file mode 100644 index 000000000..711966252 --- /dev/null +++ b/gui/backend_example/database_components/_generation.py @@ -0,0 +1,26 @@ +"""Generation class.""" + +import sqlalchemy +import sqlalchemy.orm as orm + +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.""" + + __tablename__ = "generation" + + experiment_id: orm.Mapped[int] = orm.mapped_column( + sqlalchemy.ForeignKey("experiment.id"), nullable=False, init=False + ) + experiment: orm.Mapped[Experiment] = orm.relationship() + generation_index: orm.Mapped[int] = orm.mapped_column(nullable=False) + population_id: orm.Mapped[int] = orm.mapped_column( + sqlalchemy.ForeignKey("population.id"), nullable=False, init=False + ) + population: orm.Mapped[Population] = orm.relationship() diff --git a/gui/backend_example/database_components/_genotype.py b/gui/backend_example/database_components/_genotype.py new file mode 100644 index 000000000..3b16928c6 --- /dev/null +++ b/gui/backend_example/database_components/_genotype.py @@ -0,0 +1,90 @@ +"""Genotype class.""" + +from __future__ import annotations + +import multineat +import numpy as np + +from revolve2.experimentation.database import HasId +from revolve2.modular_robot import ModularRobot +from revolve2.standards.genotypes.cppnwin.modular_robot import BrainGenotypeCpgOrm +from revolve2.standards.genotypes.cppnwin.modular_robot.v2 import BodyGenotypeOrmV2 + +from ._base import Base + + +class Genotype(Base, HasId, BodyGenotypeOrmV2, BrainGenotypeCpgOrm): + """SQLAlchemy model for a genotype for a modular robot body and brain.""" + + __tablename__ = "genotype" + + @classmethod + def random( + cls, + innov_db_body: multineat.InnovationDatabase, + innov_db_brain: multineat.InnovationDatabase, + rng: np.random.Generator, + ) -> Genotype: + """ + Create a random genotype. + + :param innov_db_body: Multineat innovation database for the body. See Multineat library. + :param innov_db_brain: Multineat innovation database for the brain. See Multineat library. + :param rng: Random number generator. + :returns: The created genotype. + """ + body = cls.random_body(innov_db_body, rng) + brain = cls.random_brain(innov_db_brain, rng) + + return Genotype(body=body.body, brain=brain.brain) + + def mutate( + self, + innov_db_body: multineat.InnovationDatabase, + innov_db_brain: multineat.InnovationDatabase, + rng: np.random.Generator, + ) -> Genotype: + """ + Mutate this genotype. + + This genotype will not be changed; a mutated copy will be returned. + + :param innov_db_body: Multineat innovation database for the body. See Multineat library. + :param innov_db_brain: Multineat innovation database for the brain. See Multineat library. + :param rng: Random number generator. + :returns: A mutated copy of the provided genotype. + """ + body = self.mutate_body(innov_db_body, rng) + brain = self.mutate_brain(innov_db_brain, rng) + + return Genotype(body=body.body, brain=brain.brain) + + @classmethod + def crossover( + cls, + parent1: Genotype, + parent2: Genotype, + rng: np.random.Generator, + ) -> Genotype: + """ + Perform crossover between two genotypes. + + :param parent1: The first genotype. + :param parent2: The second genotype. + :param rng: Random number generator. + :returns: A newly created genotype. + """ + body = cls.crossover_body(parent1, parent2, rng) + brain = cls.crossover_brain(parent1, parent2, rng) + + return Genotype(body=body.body, brain=brain.brain) + + def develop(self) -> ModularRobot: + """ + Develop the genotype into a modular robot. + + :returns: The created robot. + """ + body = self.develop_body() + brain = self.develop_brain(body=body) + return ModularRobot(body=body, brain=brain) diff --git a/gui/backend_example/database_components/_individual.py b/gui/backend_example/database_components/_individual.py new file mode 100644 index 000000000..40e4eefc8 --- /dev/null +++ b/gui/backend_example/database_components/_individual.py @@ -0,0 +1,17 @@ +"""Individual class.""" + +from dataclasses import dataclass + +from revolve2.experimentation.optimization.ea import Individual as GenericIndividual + +from ._base import Base +from ._genotype import Genotype + + +@dataclass +class Individual( + Base, GenericIndividual[Genotype], population_table="population", kw_only=True +): + """An individual in a population.""" + + __tablename__ = "individual" diff --git a/gui/backend_example/database_components/_population.py b/gui/backend_example/database_components/_population.py new file mode 100644 index 000000000..7e43a0f7f --- /dev/null +++ b/gui/backend_example/database_components/_population.py @@ -0,0 +1,12 @@ +"""Population class.""" + +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.""" + + __tablename__ = "population" diff --git a/gui/backend_example/evaluator.py b/gui/backend_example/evaluator.py new file mode 100644 index 000000000..da3a94026 --- /dev/null +++ b/gui/backend_example/evaluator.py @@ -0,0 +1,74 @@ +"""Evaluator class.""" + +from database_components import Genotype + +from revolve2.experimentation.evolution.abstract_elements import Evaluator as Eval +from revolve2.modular_robot_simulation import ( + ModularRobotScene, + Terrain, + simulate_scenes, +) +from revolve2.simulators.mujoco_simulator import LocalSimulator +from revolve2.standards import fitness_functions, terrains +from revolve2.standards.simulation_parameters import make_standard_batch_parameters + + +class Evaluator(Eval): + """Provides evaluation of robots.""" + + _simulator: LocalSimulator + _terrain: Terrain + + def __init__( + self, + headless: bool, + num_simulators: int, + ) -> None: + """ + Initialize this object. + + :param headless: `headless` parameter for the physics simulator. + :param num_simulators: `num_simulators` parameter for the physics simulator. + """ + self._simulator = LocalSimulator( + headless=headless, num_simulators=num_simulators + ) + self._terrain = terrains.flat() + + def evaluate( + self, + population: list[Genotype], + ) -> list[float]: + """ + Evaluate multiple robots. + + Fitness is the distance traveled on the xy plane. + + :param population: The robots to simulate. + :returns: Fitnesses of the robots. + """ + robots = [genotype.develop() for genotype in population] + # Create the scenes. + scenes = [] + for robot in robots: + scene = ModularRobotScene(terrain=self._terrain) + scene.add_robot(robot) + scenes.append(scene) + + # Simulate all scenes. + scene_states = simulate_scenes( + simulator=self._simulator, + batch_parameters=make_standard_batch_parameters(), + scenes=scenes, + ) + + # Calculate the xy displacements. + xy_displacements = [ + fitness_functions.xy_displacement( + states[0].get_modular_robot_simulation_state(robot), + states[-1].get_modular_robot_simulation_state(robot), + ) + for robot, states in zip(robots, scene_states) + ] + + return xy_displacements diff --git a/gui/backend_example/fitness_functions.py b/gui/backend_example/fitness_functions.py new file mode 100644 index 000000000..774b6d315 --- /dev/null +++ b/gui/backend_example/fitness_functions.py @@ -0,0 +1,75 @@ +"""Standard fitness functions for modular robots.""" + +import math + +from revolve2.modular_robot_simulation import ModularRobotSimulationState + + +def xy_displacement( + begin_state: ModularRobotSimulationState, end_state: ModularRobotSimulationState +) -> float: + """ + Calculate the distance traveled on the xy-plane by a single modular robot. + + :param begin_state: Begin state of the robot. + :param end_state: End state of the robot. + :returns: The calculated fitness. + """ + begin_position = begin_state.get_pose().position + end_position = end_state.get_pose().position + return math.sqrt( + (begin_position.x - end_position.x) ** 2 + + (begin_position.y - end_position.y) ** 2 + ) + +def x_speed_Miras2021(x_distance: float, simulation_time = float) -> float: + """Goal: + Calculate the fitness for speed in x direction for a single modular robot according to + Miras (2021). + ------------------------------------------------------------------------------------------- + Input: + x_distance: The distance traveled in the x direction. + simulation_time: The time of the simulation. + ------------------------------------------------------------------------------------------- + Output: + The calculated fitness. + """ + # Begin and end Position + + # Calculate the speed in x direction + vx = float((x_distance / simulation_time) * 100) + if vx > 0: + return vx + elif vx == 0: + return -0.1 + else: + return vx / 10 + +def x_efficiency(xbest: float, eexp: float, simulation_time: float) -> float: + """Goal: + Calculate the efficiency of a robot for locomotion in x direction. + ------------------------------------------------------------------------------------------- + Input: + xbest: The furthest distance traveled in the x direction. + eexp: The energy expended. + simulation_time: The time of the simulation. + ------------------------------------------------------------------------------------------- + Output: + The calculated fitness. + """ + def food(xbest, bmet): + # Get food + if xbest <= 0: + return 0 + else: + food = (xbest / 0.05) * (80 * bmet) + return food + + def scale_EEXP(eexp, bmet): + return eexp / 346 * (80 * bmet) + + # Get baseline metabolism + bmet = 80 + battery = -bmet * simulation_time + food(xbest, bmet) - scale_EEXP(eexp, bmet) + + return battery \ No newline at end of file diff --git a/gui/backend_example/main.py b/gui/backend_example/main.py new file mode 100644 index 000000000..9b1407ff4 --- /dev/null +++ b/gui/backend_example/main.py @@ -0,0 +1,317 @@ +"""Main script for the example.""" + +import logging +from typing import Any + +import config +import multineat +import numpy as np +import numpy.typing as npt +from database_components import ( + Base, + Experiment, + Generation, + Genotype, + Individual, + Population, +) +from evaluator import Evaluator +from sqlalchemy.engine import Engine +from sqlalchemy.orm import Session + +from revolve2.experimentation.database import OpenMethod, open_database_sqlite +from revolve2.experimentation.evolution import ModularRobotEvolution +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, seed_from_time + + +class ParentSelector(Selector): + """Selector class for parent selection.""" + + rng: np.random.Generator + offspring_size: int + + def __init__(self, offspring_size: int, rng: np.random.Generator) -> None: + """ + Initialize the parent selector. + + :param offspring_size: The offspring size. + :param rng: The rng generator. + """ + self.offspring_size = offspring_size + self.rng = rng + + def select( + self, population: Population, **kwargs: Any + ) -> tuple[npt.NDArray[np.int_], dict[str, Population]]: + """ + Select the parents. + + :param population: The population of robots. + :param kwargs: Other parameters. + :return: The parent pairs. + """ + return np.array( + [ + selection.multiple_unique( + selection_size=2, + + population=[individual.genotype for individual in population.individuals], + fitnesses=[individual.fitness for individual in population.individuals], + + selection_function=lambda _, fitnesses: selection.tournament( + rng=self.rng, fitnesses=fitnesses, k=2), + ) + for _ in range(self.offspring_size) + ], + ), {"parent_population": population} + + +class SurvivorSelector(Selector): + """Selector class for survivor selection.""" + + rng: np.random.Generator + + def __init__(self, rng: np.random.Generator) -> None: + """ + Initialize the parent selector. + + :param rng: The rng generator. + """ + self.rng = rng + + def select( + self, population: Population, **kwargs: Any + ) -> tuple[Population, dict[str, Any]]: + """ + Select survivors using a tournament. + + :param population: The population the parents come from. + :param kwargs: The offspring, with key 'offspring_population'. + :returns: A newly created population. + :raises ValueError: If the population is empty. + """ + offspring = kwargs.get("children") + offspring_fitness = kwargs.get("child_task_performance") + if offspring is None or offspring_fitness is None: + raise ValueError( + "No offspring was passed with positional argument 'children' and / or 'child_task_performance'." + ) + + original_survivors, offspring_survivors = population_management.steady_state( + old_genotypes=[i.genotype for i in population.individuals], + old_fitnesses=[i.fitness for i in population.individuals], + new_genotypes=offspring, + new_fitnesses=offspring_fitness, + selection_function=lambda n, genotypes, fitnesses: selection.multiple_unique( + selection_size=n, + population=genotypes, + fitnesses=fitnesses, + selection_function=lambda _, fitnesses: selection.tournament( + rng=self.rng, fitnesses=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], + fitness=offspring_fitness[i], + ) + for i in offspring_survivors + ] + ), + {}, + ) + + +class CrossoverReproducer(Reproducer): + """A simple crossover reproducer using multineat.""" + + rng: np.random.Generator + innov_db_body: multineat.InnovationDatabase + innov_db_brain: multineat.InnovationDatabase + + def __init__( + self, + rng: np.random.Generator, + innov_db_body: multineat.InnovationDatabase, + innov_db_brain: multineat.InnovationDatabase, + ): + """ + Initialize the reproducer. + + :param rng: The ranfom generator. + :param innov_db_body: The innovation database for the body. + :param innov_db_brain: The innovation database for the brain. + """ + self.rng = rng + self.innov_db_body = innov_db_body + self.innov_db_brain = innov_db_brain + + def reproduce( + self, population: npt.NDArray[np.int_], **kwargs: Any + ) -> list[Genotype]: + """ + Reproduce the population by crossover. + + :param population: The parent pairs. + :param kwargs: Additional keyword arguments. + :return: The genotypes of the children. + :raises ValueError: If the parent population is not passed as a kwarg `parent_population`. + """ + parent_population: Population | None = kwargs.get("parent_population") + if parent_population is None: + raise ValueError("No parent population given.") + + offspring_genotypes = [ + Genotype.crossover( + parent_population.individuals[parent1_i].genotype, + parent_population.individuals[parent2_i].genotype, + self.rng, + ).mutate(self.innov_db_body, self.innov_db_brain, self.rng) + for parent1_i, parent2_i in population + ] + return offspring_genotypes + + +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. + rng_seed = seed_from_time() + rng = make_rng(rng_seed) + + # Create and save the experiment instance. + experiment = Experiment(rng_seed=rng_seed) + logging.info("Saving experiment configuration.") + with Session(dbengine) as session: + session.add(experiment) + session.commit() + + # CPPN innovation databases. + innov_db_body = multineat.InnovationDatabase() + innov_db_brain = multineat.InnovationDatabase() + + """ + Here we initialize the components used for the evolutionary process. + + - evaluator: Allows us to evaluate a population of modular robots. + - parent_selector: Allows us to select parents from a population of modular robots. + - survivor_selector: Allows us to select survivors from a population. + - crossover_reproducer: Allows us to generate offspring from parents. + - modular_robot_evolution: The evolutionary process as a object that can be iterated. + """ + evaluator = Evaluator(headless=True, num_simulators=config.NUM_SIMULATORS) + parent_selector = ParentSelector(offspring_size=config.OFFSPRING_SIZE, rng=rng) + survivor_selector = SurvivorSelector(rng=rng) + crossover_reproducer = CrossoverReproducer( + rng=rng, innov_db_body=innov_db_body, innov_db_brain=innov_db_brain + ) + + modular_robot_evolution = ModularRobotEvolution( + parent_selection=parent_selector, + survivor_selection=survivor_selector, + evaluator=evaluator, + reproducer=crossover_reproducer, + ) + + # Create an initial population, as we cant start from nothing. + logging.info("Generating initial population.") + initial_genotypes = [ + Genotype.random( + innov_db_body=innov_db_body, + innov_db_brain=innov_db_brain, + rng=rng, + ) + for _ in range(config.POPULATION_SIZE) + ] + + # 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, strict=True + ) + ] + ) + + # Finish the zeroth generation and save it to the database. + generation = Generation( + experiment=experiment, generation_index=0, population=population + ) + save_to_db(dbengine, generation) + + # 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}." + ) + + # Here we iterate the evolutionary process using the step. + population = modular_robot_evolution.step(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, + ) + save_to_db(dbengine, generation) + + +def main() -> None: + """Run the program.""" + # Set up logging. + setup_logging(file_name="log.txt") + + # Open the database, only if it does not already exists. + dbengine = open_database_sqlite( + config.DATABASE_FILE, open_method=OpenMethod.NOT_EXISTS_AND_CREATE + ) + # Create the structure of the database. + Base.metadata.create_all(dbengine) + + # Run the experiment several times. + for _ in range(config.NUM_REPETITIONS): + run_experiment(dbengine) + + +def save_to_db(dbengine: Engine, generation: Generation) -> None: + """ + Save the current generation to the database. + + :param dbengine: The database engine. + :param generation: The current generation. + """ + logging.info("Saving generation.") + with Session(dbengine, expire_on_commit=False) as session: + session.add(generation) + session.commit() + + +if __name__ == "__main__": + main() diff --git a/gui/backend_example/main_from_gui.py b/gui/backend_example/main_from_gui.py new file mode 100644 index 000000000..6c60e39c6 --- /dev/null +++ b/gui/backend_example/main_from_gui.py @@ -0,0 +1,154 @@ +"""Main script for the example.""" + +import logging + +import config +import multineat +from database_components import ( + Base, + Experiment, + Generation, + Genotype, + Individual, + Population, +) +from evaluator import Evaluator +from sqlalchemy.engine import Engine +from sqlalchemy.orm import Session + +from reproducer_methods import CrossoverReproducer +from selector_methods import ParentSelector, SurvivorSelector + +from revolve2.experimentation.database import OpenMethod, open_database_sqlite +from revolve2.experimentation.evolution import ModularRobotEvolution +from revolve2.experimentation.logging import setup_logging +from revolve2.experimentation.rng import make_rng, seed_from_time + + +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. + rng_seed = seed_from_time() + rng = make_rng(rng_seed) + + # Create and save the experiment instance. + experiment = Experiment(rng_seed=rng_seed) + logging.info("Saving experiment configuration.") + with Session(dbengine) as session: + session.add(experiment) + session.commit() + + # CPPN innovation databases. + innov_db_body = multineat.InnovationDatabase() + innov_db_brain = multineat.InnovationDatabase() + + """ + Here we initialize the components used for the evolutionary process. + + - evaluator: Allows us to evaluate a population of modular robots. + - parent_selector: Allows us to select parents from a population of modular robots. + - survivor_selector: Allows us to select survivors from a population. + - crossover_reproducer: Allows us to generate offspring from parents. + - modular_robot_evolution: The evolutionary process as a object that can be iterated. + """ + evaluator = Evaluator(headless=True, num_simulators=config.NUM_SIMULATORS) + parent_selector = ParentSelector(offspring_size=config.OFFSPRING_SIZE, rng=rng) + survivor_selector = SurvivorSelector(rng=rng) + crossover_reproducer = CrossoverReproducer(rng=rng, innov_db_body=innov_db_body, innov_db_brain=innov_db_brain) + + modular_robot_evolution = ModularRobotEvolution( + parent_selection=parent_selector, + survivor_selection=survivor_selector, + evaluator=evaluator, + reproducer=crossover_reproducer, + ) + + # Create an initial population, as we cant start from nothing. + logging.info("Generating initial population.") + initial_genotypes = [ + Genotype.random( + innov_db_body=innov_db_body, + innov_db_brain=innov_db_brain, + rng=rng, + ) + for _ in range(config.POPULATION_SIZE) + ] + + # 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, strict=True + ) + ] + ) + + # Finish the zeroth generation and save it to the database. + generation = Generation( + experiment=experiment, generation_index=0, population=population + ) + save_to_db(dbengine, generation) + + # 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}." + ) + + # Here we iterate the evolutionary process using the step. + population = modular_robot_evolution.step(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, + ) + save_to_db(dbengine, generation) + + +def main() -> None: + """Run the program.""" + # Set up logging. + setup_logging(file_name="log.txt") + + # Open the database, only if it does not already exists. + dbengine = open_database_sqlite( + config.DATABASE_FILE, open_method=OpenMethod.NOT_EXISTS_AND_CREATE + ) + # Create the structure of the database. + Base.metadata.create_all(dbengine) + + # Run the experiment several times. + for _ in range(config.NUM_REPETITIONS): + run_experiment(dbengine) + + +def save_to_db(dbengine: Engine, generation: Generation) -> None: + """ + Save the current generation to the database. + + :param dbengine: The database engine. + :param generation: The current generation. + """ + logging.info("Saving generation.") + with Session(dbengine, expire_on_commit=False) as session: + session.add(generation) + session.commit() + + +if __name__ == "__main__": + main() diff --git a/gui/backend_example/plot.py b/gui/backend_example/plot.py new file mode 100644 index 000000000..4dca0979d --- /dev/null +++ b/gui/backend_example/plot.py @@ -0,0 +1,105 @@ +"""Plot fitness over generations for all experiments, averaged.""" + +import config +import matplotlib.pyplot as plt +import pandas +from database_components import Experiment, Generation, Individual, Population +from sqlalchemy import select + +from revolve2.experimentation.database import OpenMethod, open_database_sqlite +from revolve2.experimentation.logging import setup_logging +import argparse + +def argument_parser() -> argparse.ArgumentParser: + """Create an argument parser.""" + parser = argparse.ArgumentParser(description="Plot fitness over generations for all experiments, averaged.") + return parser + +def main() -> None: + """Run the program.""" + setup_logging() + + dbengine = open_database_sqlite( + "../viewer/"+config.DATABASE_FILE, open_method=OpenMethod.OPEN_IF_EXISTS + ) + + df = pandas.read_sql( + select( + Experiment.id.label("experiment_id"), + Generation.generation_index, + Individual.fitness, + ) + .join_from(Experiment, Generation, Experiment.id == Generation.experiment_id) + .join_from(Generation, Population, Generation.population_id == Population.id) + .join_from(Population, Individual, Population.id == Individual.population_id), + dbengine, + ) + + agg_per_experiment_per_generation = ( + df.groupby(["experiment_id", "generation_index"]) + .agg({"fitness": ["max", "mean"]}) + .reset_index() + ) + agg_per_experiment_per_generation.columns = [ + "experiment_id", + "generation_index", + "max_fitness", + "mean_fitness", + ] + + agg_per_generation = ( + agg_per_experiment_per_generation.groupby("generation_index") + .agg({"max_fitness": ["mean", "std"], "mean_fitness": ["mean", "std"]}) + .reset_index() + ) + agg_per_generation.columns = [ + "generation_index", + "max_fitness_mean", + "max_fitness_std", + "mean_fitness_mean", + "mean_fitness_std", + ] + + plt.figure() + + # Plot max + plt.plot( + agg_per_generation["generation_index"], + agg_per_generation["max_fitness_mean"], + label="Max fitness", + color="b", + ) + plt.fill_between( + agg_per_generation["generation_index"], + agg_per_generation["max_fitness_mean"] - agg_per_generation["max_fitness_std"], + agg_per_generation["max_fitness_mean"] + agg_per_generation["max_fitness_std"], + color="b", + alpha=0.2, + ) + + # Plot mean + plt.plot( + agg_per_generation["generation_index"], + agg_per_generation["mean_fitness_mean"], + label="Mean fitness", + color="r", + ) + plt.fill_between( + agg_per_generation["generation_index"], + agg_per_generation["mean_fitness_mean"] + - agg_per_generation["mean_fitness_std"], + agg_per_generation["mean_fitness_mean"] + + agg_per_generation["mean_fitness_std"], + color="r", + alpha=0.2, + ) + + plt.xlabel("Generation index") + plt.ylabel("Fitness") + plt.title("Mean and max fitness across repetitions with std as shade") + plt.legend() + plt.show() + + +if __name__ == "__main__": + main() diff --git a/gui/backend_example/reproducer_methods.py b/gui/backend_example/reproducer_methods.py new file mode 100644 index 000000000..692656b8d --- /dev/null +++ b/gui/backend_example/reproducer_methods.py @@ -0,0 +1,61 @@ + +import multineat + +import numpy as np +from typing import Any +import numpy.typing as npt + +from revolve2.experimentation.evolution.abstract_elements import Reproducer +from database_components import ( + Genotype, + Population, +) + +class CrossoverReproducer(Reproducer): + """A simple crossover reproducer using multineat.""" + + rng: np.random.Generator + innov_db_body: multineat.InnovationDatabase + innov_db_brain: multineat.InnovationDatabase + + def __init__( + self, + rng: np.random.Generator, + innov_db_body: multineat.InnovationDatabase, + innov_db_brain: multineat.InnovationDatabase, + ): + """ + Initialize the reproducer. + + :param rng: The ranfom generator. + :param innov_db_body: The innovation database for the body. + :param innov_db_brain: The innovation database for the brain. + """ + self.rng = rng + self.innov_db_body = innov_db_body + self.innov_db_brain = innov_db_brain + + def reproduce( + self, population: npt.NDArray[np.int_], **kwargs: Any + ) -> list[Genotype]: + """ + Reproduce the population by crossover. + + :param population: The parent pairs. + :param kwargs: Additional keyword arguments. + :return: The genotypes of the children. + :raises ValueError: If the parent population is not passed as a kwarg `parent_population`. + """ + parent_population: Population | None = kwargs.get("parent_population") + if parent_population is None: + raise ValueError("No parent population given.") + + offspring_genotypes = [ + Genotype.crossover( + parent_population.individuals[parent1_i].genotype, + parent_population.individuals[parent2_i].genotype, + self.rng, + ).mutate(self.innov_db_body, self.innov_db_brain, self.rng) + for parent1_i, parent2_i in population + ] + return offspring_genotypes \ No newline at end of file diff --git a/gui/backend_example/requirements.txt b/gui/backend_example/requirements.txt new file mode 100644 index 000000000..373c93ed6 --- /dev/null +++ b/gui/backend_example/requirements.txt @@ -0,0 +1,2 @@ +pandas>=2.1.0 +matplotlib>=3.8.0 diff --git a/gui/backend_example/rerun.py b/gui/backend_example/rerun.py new file mode 100644 index 000000000..54c158712 --- /dev/null +++ b/gui/backend_example/rerun.py @@ -0,0 +1,46 @@ +"""Rerun the best robot between all experiments.""" + +import logging + +import config +from database_components import Genotype, Individual +from evaluator import Evaluator +from sqlalchemy import select +from sqlalchemy.orm import Session + +from revolve2.experimentation.database import OpenMethod, open_database_sqlite +from revolve2.experimentation.logging import setup_logging + + +def main() -> None: + """Perform the rerun.""" + setup_logging() + + # Load the best individual from the database. + dbengine = open_database_sqlite( + config.DATABASE_FILE, open_method=OpenMethod.OPEN_IF_EXISTS + ) + + with Session(dbengine) as ses: + row = ses.execute( + select(Genotype, Individual.fitness) + .join_from(Genotype, Individual, Genotype.id == Individual.genotype_id) + .order_by(Individual.fitness.desc()) + .limit(1) + ).one() + assert row is not None + + genotype = row[0] + fitness = row[1] + + logging.info(f"Best fitness: {fitness}") + + # Create the evaluator. + evaluator = Evaluator(headless=False, num_simulators=1) + + # Show the robot. + evaluator.evaluate([genotype]) + + +if __name__ == "__main__": + main() diff --git a/gui/backend_example/selector_methods.py b/gui/backend_example/selector_methods.py new file mode 100644 index 000000000..82318768b --- /dev/null +++ b/gui/backend_example/selector_methods.py @@ -0,0 +1,163 @@ +from revolve2.experimentation.evolution.abstract_elements import Selector +from revolve2.experimentation.optimization.ea import population_management, selection +import numpy as np +import numpy.typing as npt +from typing import Any +from database_components import ( + Individual, + Population, +) +import config + + + +class ParentSelector(Selector): + """Selector class for parent selection.""" + + rng: np.random.Generator + offspring_size: int + + def __init__(self, offspring_size: int, rng: np.random.Generator, + generational=config.GENERATIONAL, steady_state=config.STEADY_STATE, + selection_func=selection.tournament) -> None: + """ + Initialize the parent selector. + + :param offspring_size: The offspring size. + :param rng: The rng generator. + """ + self.offspring_size = offspring_size + self.rng = rng + self.generational = generational + self.steady_state = steady_state + self.selection_func = selection_func + + + def select( + self, population: Population, **kwargs: Any + ) -> tuple[npt.NDArray[np.int_], dict[str, Population]]: + """ + Select the parents. + + :param population: The population of robots. + :param kwargs: Other parameters. + :return: The parent pairs. + """ + if self.generational: + return np.array( + [ + selection.multiple_unique( + selection_size=2, + + population=[individual.genotype for individual in population.individuals], + fitnesses=[individual.fitness for individual in population.individuals], + + selection_function=lambda _, fitnesses: self.selection_func( + rng=self.rng, fitnesses=fitnesses, k=2), + ) + for _ in range(self.offspring_size) + ], + ), {"parent_population": population} + else: + return np.array( + [ + selection.multiple_unique( + selection_size=2, + + population=[individual.genotype for individual in population.individuals], + fitnesses=[individual.fitness for individual in population.individuals], + + selection_function=lambda _, fitnesses: self.selection_func( + rng=self.rng, fitnesses=fitnesses, k=2), + ) + for _ in range(self.offspring_size) + ], + ), {"parent_population": population} + +class SurvivorSelector(Selector): + """Selector class for survivor selection.""" + + rng: np.random.Generator + + def __init__(self, rng: np.random.Generator, generational=config.GENERATIONAL, + steady_state=config.STEADY_STATE, selection_func=selection.tournament) -> None: + """ + Initialize the parent selector. + + :param rng: The rng generator. + """ + self.rng = rng + self.generational = generational + self.steady_state = steady_state + self.selection_func = selection_func + + def select( + self, population: Population, **kwargs: Any + ) -> tuple[Population, dict[str, Any]]: + """ + Select survivors using a tournament. + + :param population: The population the parents come from. + :param kwargs: The offspring, with key 'offspring_population'. + :returns: A newly created population. + :raises ValueError: If the population is empty. + """ + offspring = kwargs.get("children") + offspring_fitness = kwargs.get("child_task_performance") + if offspring is None or offspring_fitness is None: + raise ValueError( + "No offspring was passed with positional argument 'children' and / or 'child_task_performance'." + ) + + if self.generational: + original_survivors, offspring_survivors = population_management.generational( + old_genotypes=[i.genotype for i in population.individuals], + old_fitnesses=[i.fitness for i in population.individuals], + new_genotypes=offspring, + new_fitnesses=offspring_fitness, + selection_function=lambda n, genotypes, fitnesses: selection.multiple_unique( + selection_size=n, + population=genotypes, + fitnesses=fitnesses, + selection_function=lambda _, fitnesses: self.selection_func( + rng=self.rng, fitnesses=fitnesses, k=2 + ), + ), + ) + + else: + original_survivors, offspring_survivors = population_management.steady_state( + + old_genotypes=[i.genotype for i in population.individuals], + old_fitnesses=[i.fitness for i in population.individuals], + new_genotypes=offspring, + new_fitnesses=offspring_fitness, + + selection_function=lambda n, genotypes, fitnesses: selection.multiple_unique( + selection_size=n, + population=genotypes, + fitnesses=fitnesses, + selection_function=lambda _, fitnesses: self.selection_func( + rng=self.rng, fitnesses=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], + fitness=offspring_fitness[i], + ) + for i in offspring_survivors + ] + ), + {}, + ) diff --git a/gui/backend_example/simulation_parameters.py b/gui/backend_example/simulation_parameters.py new file mode 100644 index 000000000..5ffbd2cec --- /dev/null +++ b/gui/backend_example/simulation_parameters.py @@ -0,0 +1,27 @@ +"""Standard simulation functions and parameters.""" + +from revolve2.simulation.simulator import BatchParameters +from config_simulation_parameters import STANDARD_SIMULATION_TIME, STANDARD_SAMPLING_FREQUENCY, STANDARD_SIMULATION_TIMESTEP, STANDARD_CONTROL_FREQUENCY + + +def make_standard_batch_parameters( + simulation_time: int = STANDARD_SIMULATION_TIME, + sampling_frequency: float | None = STANDARD_SAMPLING_FREQUENCY, + simulation_timestep: float = STANDARD_SIMULATION_TIMESTEP, + control_frequency: float = STANDARD_CONTROL_FREQUENCY, +) -> BatchParameters: + """ + Create batch parameters as standardized within the CI Group. + + :param simulation_time: As defined in the `BatchParameters` class. + :param sampling_frequency: As defined in the `BatchParameters` class. + :param simulation_timestep: As defined in the `BatchParameters` class. + :param control_frequency: As defined in the `BatchParameters` class. + :returns: The create batch parameters. + """ + return BatchParameters( + simulation_time=simulation_time, + sampling_frequency=sampling_frequency, + simulation_timestep=simulation_timestep, + control_frequency=control_frequency, + ) diff --git a/gui/backend_example/terrains.py b/gui/backend_example/terrains.py new file mode 100644 index 000000000..17940f699 --- /dev/null +++ b/gui/backend_example/terrains.py @@ -0,0 +1,161 @@ +"""Standard terrains.""" + +import math + +import numpy as np +import numpy.typing as npt +from noise import pnoise2 +from pyrr import Vector3 + +from revolve2.modular_robot_simulation import Terrain +from revolve2.simulation.scene import Pose +from revolve2.simulation.scene.geometry import GeometryHeightmap, GeometryPlane +from revolve2.simulation.scene.vector2 import Vector2 + + +def flat(size: Vector2 = Vector2([20.0, 20.0])) -> Terrain: + """ + Create a flat plane terrain. + + :param size: Size of the plane. + :returns: The created terrain. + """ + return Terrain( + static_geometry=[ + GeometryPlane( + pose=Pose(), + mass=0.0, + size=size, + ) + ] + ) + + +def crater( + size: tuple[float, float], + ruggedness: float, + curviness: float, + granularity_multiplier: float = 1.0, +) -> Terrain: + r""" + Create a crater-like terrain with rugged floor using a heightmap. + + It will look like:: + + | | + \_ .' + '.,^_..' + + A combination of the rugged and bowl heightmaps. + + :param size: Size of the crater. + :param ruggedness: How coarse the ground is. + :param curviness: Height of the edges of the crater. + :param granularity_multiplier: Multiplier for how many edges are used in the heightmap. + :returns: The created terrain. + """ + NUM_EDGES = 100 # arbitrary constant to get a nice number of edges + + num_edges = ( + int(NUM_EDGES * size[0] * granularity_multiplier), + int(NUM_EDGES * size[1] * granularity_multiplier), + ) + + rugged = rugged_heightmap( + size=size, + num_edges=num_edges, + density=1.5, + ) + bowl = bowl_heightmap(num_edges=num_edges) + + max_height = ruggedness + curviness + if max_height == 0.0: + heightmap = np.zeros(num_edges) + max_height = 1.0 + else: + heightmap = (ruggedness * rugged + curviness * bowl) / (ruggedness + curviness) + + return Terrain( + static_geometry=[ + GeometryHeightmap( + pose=Pose(), + mass=0.0, + size=Vector3([size[0], size[1], max_height]), + base_thickness=0.1 + ruggedness, + heights=heightmap, + ) + ] + ) + + +def rugged_heightmap( + size: tuple[float, float], + num_edges: tuple[int, int], + density: float = 1.0, +) -> npt.NDArray[np.float_]: + """ + Create a rugged terrain heightmap. + + It will look like:: + + ..^.__,^._.-. + + Be aware: the maximum height of the heightmap is not actually 1. + It is around [-1,1] but not exactly. + + :param size: Size of the heightmap. + :param num_edges: How many edges to use for the heightmap. + :param density: How coarse the ruggedness is. + :returns: The created heightmap as a 2 dimensional array. + """ + OCTAVE = 10 + C1 = 4.0 # arbitrary constant to get nice noise + + return np.fromfunction( + np.vectorize( + lambda y, x: pnoise2( + x / num_edges[0] * C1 * size[0] * density, + y / num_edges[1] * C1 * size[1] * density, + OCTAVE, + ), + otypes=[float], + ), + num_edges, + dtype=float, + ) + + +def bowl_heightmap( + num_edges: tuple[int, int], +) -> npt.NDArray[np.float_]: + r""" + Create a terrain heightmap in the shape of a bowl. + + It will look like:: + + | | + \ / + '.___.' + + The height of the edges of the bowl is 1.0 and the center is 0.0. + + :param num_edges: How many edges to use for the heightmap. + :returns: The created heightmap as a 2 dimensional array. + """ + return np.fromfunction( + np.vectorize( + lambda y, x: ( + (x / num_edges[0] * 2.0 - 1.0) ** 2 + + (y / num_edges[1] * 2.0 - 1.0) ** 2 + if math.sqrt( + (x / num_edges[0] * 2.0 - 1.0) ** 2 + + (y / num_edges[1] * 2.0 - 1.0) ** 2 + ) + <= 1.0 + else 0.0 + ), + otypes=[float], + ), + num_edges, + dtype=float, + ) diff --git a/gui/viewer/main_window.py b/gui/viewer/main_window.py new file mode 100644 index 000000000..772adca07 --- /dev/null +++ b/gui/viewer/main_window.py @@ -0,0 +1,317 @@ +from PyQt5.QtWidgets import ( + QApplication, QMainWindow, QTabWidget, + QWidget, QVBoxLayout, QLabel, + QComboBox, QPushButton, QMessageBox, + QLineEdit, QHBoxLayout, QButtonGroup, + QStackedWidget) +from parsing import get_functions_from_file, get_function_names_from_init, get_config_parameters_from_file, save_config_parameters +import subprocess +import os + +class RobotEvolutionGUI(QMainWindow): + def __init__(self): + super().__init__() + self.setWindowTitle("Robot Evolution System") + self.setGeometry(100, 100, 800, 600) + + self.simulation_process = None + + self.tab_widget = QTabWidget(self) + self.setCentralWidget(self.tab_widget) + + self.fitness_functions = get_functions_from_file("../backend_example/fitness_functions.py") + + self.terrains = get_functions_from_file("../backend_example/terrains.py") + + self.path_simulation_parameters = "../backend_example/config_simulation_parameters.py" + self.simulation_parameters = get_config_parameters_from_file(self.path_simulation_parameters) + + self.path_evolution_parameters = "../backend_example/config.py" + self.evolution_parameters = get_config_parameters_from_file(self.path_evolution_parameters) + self.is_generational = self.evolution_parameters.get("GENERATIONAL") + + self.selection_path = "/home/aronf/Desktop/EvolutionaryComputing/work/revolve2/experimentation/revolve2/experimentation/optimization/ea/selection/" + self.selection_functions = get_function_names_from_init(self.selection_path) + + # Step 1: Define Robot Phenotypes + self.tab_widget.addTab(self.create_phenotype_tab(), "Robot Phenotypes") + + # Step 2: Define Environment + self.tab_widget.addTab(self.create_environment_tab(), "Environment & Task") + + # Step 3: Define Genotypes + self.tab_widget.addTab(self.create_genotype_tab(), "Robot Genotypes") + + # Step 4: Fitness Function + self.tab_widget.addTab(self.create_fitness_tab(), "Fitness Function") + + # Step 5: Evolution Parameters + self.tab_widget.addTab(self.create_ea_tab(), "Evolutionary Algorithm") + + # Step 6: Selection + self.tab_widget.addTab(self.create_selection_tab(), "Selection Algorithms") + + # Step 7: Simulator Selection + self.tab_widget.addTab(self.create_simulation_parameters_tab(), "Physics Simulator") + + # Step 8: Run Simulation + self.tab_widget.addTab(self.create_run_simulation_tab(), "Run Simulation") + + # Step 9: Plot Results + self.tab_widget.addTab(self.create_plot_tab(), "Plot Results") + + + def run_simulation(self): + # Run the simulation + self.simulation_process = subprocess.Popen(["python", "../backend_example/main_from_gui.py"]) + + def stop_simulation(self): + """Stop the running simulation.""" + if self.simulation_process and self.simulation_process.poll() is None: + self.simulation_process.terminate() + self.simulation_process.wait() + print("Simulation stopped successfully.") + else: + print("No active simulation to stop.") + + def plot_results(self): + # Run the plot script + subprocess.Popen(["python", "../backend_example/plot.py"]) + + def save_config_changes(self, file_path, inputs): + """Update a config with new values from the GUI.""" + new_values = {} + for key, input_field in inputs.items(): + if type(input_field) == bool: + new_values[key] = input_field + continue + else: + text_value = input_field.text() + + try: + new_values[key] = eval(text_value) # Careful with eval in untrusted inputs! + except: + new_values[key] = text_value + + save_config_parameters(file_path, new_values) + + QMessageBox.information(self, "Success", "Config file updated!") + + def create_phenotype_tab(self): + widget = QWidget() + layout = QVBoxLayout() + layout.addWidget(QLabel("Define Robot Phenotypes")) + # Dropdown for phenotypes + phenotypes_dropdown = QComboBox() + phenotypes_dropdown.addItems(["Phenotype A", "Phenotype B", "Phenotype C"]) + layout.addWidget(phenotypes_dropdown) + widget.setLayout(layout) + return widget + + def create_genotype_tab(self): + widget = QWidget() + layout = QVBoxLayout() + layout.addWidget(QLabel("UNDER CONSTRUCTION:\n Define Mutation and Crossover Operators")) + # Mutation operator + mutation_dropdown = QComboBox() + mutation_dropdown.addItems(["Operator A", "Operator B", "Operator C"]) + layout.addWidget(QLabel("Mutation Operator:")) + layout.addWidget(mutation_dropdown) + # Crossover operator + crossover_dropdown = QComboBox() + crossover_dropdown.addItems(["Operator X", "Operator Y", "Operator Z"]) + layout.addWidget(QLabel("Crossover Operator:")) + layout.addWidget(crossover_dropdown) + widget.setLayout(layout) + return widget + + def create_selection_tab(self): + widget = QWidget() + layout = QVBoxLayout() + layout.addWidget(QLabel("Define Parent and Surivor Selection Types")) + parent_dropdown = QComboBox() + parent_dropdown.addItems(self.selection_functions) + layout.addWidget(QLabel("Parent Selection: ")) + layout.addWidget(parent_dropdown) + # Crossover operator + survivor_dropdown = QComboBox() + survivor_dropdown.addItems(self.selection_functions) + layout.addWidget(QLabel("Survivor Selection:")) + layout.addWidget(survivor_dropdown) + widget.setLayout(layout) + return widget + + def create_ea_tab(self): + widget = QWidget() + layout = QVBoxLayout() + + # Stacked widget for toggling between views + self.stacked_widget = QStackedWidget() + + # Generational View + self.view1 = QWidget() + v1_layout = QVBoxLayout() + v1_layout.addWidget(QLabel("Generational GA Parameters")) + self.view1.setLayout(v1_layout) + + # Steady-State View + self.view2 = QWidget() + v2_layout = QVBoxLayout() + v2_layout.addWidget(QLabel("Steady-State GA Parameters")) + self.view2.setLayout(v2_layout) + + self.inputs_evolution = {} + + # Add parameter fields for generational views + for key, value in self.evolution_parameters.items(): + input_layout = QHBoxLayout() + input_label = QLabel(f"{key}:") + input_field = QLineEdit(str(value)) + self.inputs_evolution[key] = input_field + if key in ["GENERATIONAL", "STEADY_STATE"]: + self.inputs_evolution["GENERATIONAL"] = True + self.inputs_evolution["STEADY_STATE"] = False + continue + else: + input_layout.addWidget(input_label) + input_layout.addWidget(input_field) + v1_layout.addLayout(input_layout) + + # Add parameter fields for steady_state view + for key, value in self.evolution_parameters.items(): + input_layout = QHBoxLayout() + input_label = QLabel(f"{key}:") + input_field = QLineEdit(str(value)) + self.inputs_evolution[key] = input_field + if key in ["GENERATIONAL", "STEADY_STATE"]: + self.inputs_evolution["GENERATIONAL"] = False + self.inputs_evolution["STEADY_STATE"] = True + continue + else: + input_layout.addWidget(input_label) + input_layout.addWidget(input_field) + v2_layout.addLayout(input_layout) + + self.stacked_widget.addWidget(self.view1) + self.stacked_widget.addWidget(self.view2) + + # Button to switch modes + self.switch_button = QPushButton("") + self.switch_button.clicked.connect(self.toggle_view) + layout.addWidget(self.switch_button) + layout.addWidget(self.stacked_widget) + + # Save Button + save_button = QPushButton("Save Changes") + save_button.clicked.connect(lambda: self.save_config_changes(self.path_evolution_parameters, self.inputs_evolution)) + layout.addWidget(save_button) + + widget.setLayout(layout) + + # Set initial state + self.update_view() + + return widget + + def toggle_view(self): + """Toggle between Generational and Steady-State mode.""" + self.is_generational = not self.is_generational + + # Save the updated mode to config + self.inputs_evolution["GENERATIONAL"] = self.is_generational + self.inputs_evolution["STEADY_STATE"] = not self.is_generational + + # Update UI + self.update_view() + + def update_view(self): + """Update the UI based on the current mode.""" + if self.is_generational: + self.stacked_widget.setCurrentWidget(self.view1) + self.switch_button.setText("Switch to Steady-State") + else: + self.stacked_widget.setCurrentWidget(self.view2) + self.switch_button.setText("Switch to Generational") + + def create_environment_tab(self): + widget = QWidget() + layout = QVBoxLayout() + layout.addWidget(QLabel("Define Environment and Task")) + # Environment settings + layout.addWidget(QLabel("Environment Terrains: ")) + environment_dropdown = QComboBox() # Example: Environment parameter input + environment_dropdown.addItems(self.terrains.keys()) + layout.addWidget(environment_dropdown) + widget.setLayout(layout) + layout + # Task selection + task_dropdown = QComboBox() + task_dropdown.addItems(["Task 1", "Task 2", "Task 3"]) + layout.addWidget(QLabel("Target Task:")) + layout.addWidget(task_dropdown) + widget.setLayout(layout) + return widget + + def create_fitness_tab(self): + widget = QWidget() + layout = QVBoxLayout() + layout.addWidget(QLabel("Define Fitness Function")) + fitness_dropdown = QComboBox() + fitness_dropdown.addItems(self.fitness_functions.keys()) + layout.addWidget(fitness_dropdown) + widget.setLayout(layout) + return widget + + def create_simulation_parameters_tab(self): + widget = QWidget() + layout = QVBoxLayout() + layout.addWidget(QLabel("Edit Simulation Parameters")) + self.inputs_simulation = {} + for key, value in self.simulation_parameters.items(): + input_layout = QHBoxLayout() + input_label = QLabel(f"{key}:") + input_field = QLineEdit(str(value)) + self.inputs_simulation[key] = input_field + input_layout.addWidget(input_label) + input_layout.addWidget(input_field) + layout.addLayout(input_layout) + + save_button = QPushButton("Save Changes") + save_button.clicked.connect(lambda: self.save_config_changes(self.path_simulation_parameters, self.inputs_simulation)) + + layout.addWidget(save_button) + widget.setLayout(layout) + return widget + + def create_run_simulation_tab(self): + widget = QWidget() + layout = QVBoxLayout() + layout.addWidget(QLabel("Run Simulation")) + run_button = QPushButton("Run Simulation") + run_button.clicked.connect(self.run_simulation) + layout.addWidget(run_button) + widget.setLayout(layout) + + layout.addWidget(QLabel("Stop Simulation")) + stop_button = QPushButton("Stop Simulation") + stop_button.clicked.connect(self.stop_simulation) + layout.addWidget(stop_button) + widget.setLayout(layout) + return widget + + def create_plot_tab(self): + widget = QWidget() + layout = QVBoxLayout() + layout.addWidget(QLabel("Plot Results")) + plot_button = QPushButton("Plot Results") + plot_button.clicked.connect(self.plot_results) + layout.addWidget(plot_button) + widget.setLayout(layout) + return widget + +if __name__ == "__main__": + import sys + app = QApplication(sys.argv) + window = RobotEvolutionGUI() + window.show() + sys.exit(app.exec_()) diff --git a/gui/viewer/parameters.py b/gui/viewer/parameters.py new file mode 100644 index 000000000..e69de29bb diff --git a/gui/viewer/parsing.py b/gui/viewer/parsing.py new file mode 100644 index 000000000..2cbc67e15 --- /dev/null +++ b/gui/viewer/parsing.py @@ -0,0 +1,63 @@ +import importlib.util +import inspect +import os + + +def get_functions_from_file(file_path): + """ + Load a Python file and extract all functions defined in it. + """ + if not os.path.exists(file_path): + raise FileNotFoundError(f"File not found: {file_path}") + + module_name = os.path.splitext(os.path.basename(file_path))[0] + spec = importlib.util.spec_from_file_location(module_name, file_path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + + # Extract all functions + functions = { + name: func + for name, func in inspect.getmembers(module, inspect.isfunction) + } + return functions + +def get_function_names_from_init(folder_path): + """ + Import the __init__.py file from the given folder and extract the function names listed in the __all__ variable. + """ + init_file = os.path.join(folder_path, "__init__.py") + if not os.path.exists(init_file): + raise FileNotFoundError(f"__init__.py not found in folder: {folder_path}") + + # Dynamically load the __init__.py file + module_name = os.path.basename(folder_path) # Use folder name as module name + spec = importlib.util.spec_from_file_location(module_name, init_file) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + + # Extract function names from __all__ list in __init__.py + if hasattr(module, "__all__"): + module.__all__.remove('multiple_unique') + return module.__all__ + else: + raise AttributeError(f"__all__ not found in {init_file}") + +def get_config_parameters_from_file(file_path): + """Dynamically load variables from a config file as a dictionary.""" + if not os.path.exists(file_path): + with open(file_path, "w") as f: + f.write("# Default config file\n") + spec = importlib.util.spec_from_file_location("config", file_path) + config = importlib.util.module_from_spec(spec) + spec.loader.exec_module(config) + return {key: getattr(config, key) for key in dir(config) if (not key.startswith("__"))} + +def save_config_parameters(file_path, values): + """Save the modified values back to a config file.""" + with open(file_path, "w") as f: + for key, value in values.items(): + if isinstance(value, str): + f.write(f'{key} = "{value}"\n') + else: + f.write(f"{key} = {value}\n") diff --git a/gui/viewer/results_viewer.py b/gui/viewer/results_viewer.py new file mode 100644 index 000000000..e69de29bb diff --git a/gui/viewer/styles.qss b/gui/viewer/styles.qss new file mode 100644 index 000000000..e69de29bb diff --git a/modular_robot/revolve2/modular_robot/body/_module.py b/modular_robot/revolve2/modular_robot/body/_module.py index 90ed8ab23..7cdaf5b72 100644 --- a/modular_robot/revolve2/modular_robot/body/_module.py +++ b/modular_robot/revolve2/modular_robot/body/_module.py @@ -225,6 +225,7 @@ def can_set_child(self, child_index: int) -> bool: if self._children.get(child_index, True): return True return False + # return child_index not in self._children or not self._children[child_index] def neighbours(self, within_range: int) -> list[Module]: """ diff --git a/standards/revolve2/standards/genotypes/cppnwin/modular_robot/_brain_cpg_network_neighbor.py b/standards/revolve2/standards/genotypes/cppnwin/modular_robot/_brain_cpg_network_neighbor.py index 7eb45089d..a6b4e3487 100644 --- a/standards/revolve2/standards/genotypes/cppnwin/modular_robot/_brain_cpg_network_neighbor.py +++ b/standards/revolve2/standards/genotypes/cppnwin/modular_robot/_brain_cpg_network_neighbor.py @@ -36,7 +36,7 @@ def _make_weights( body: Body, ) -> tuple[list[float], list[float]]: brain_net = multineat.NeuralNetwork() - self._genotype.BuildPhenotype(brain_net) + self._genotype.BuildCPPN(brain_net) internal_weights = [ self._evaluate_network( diff --git a/standards/revolve2/standards/genotypes/cppnwin/modular_robot/v2/_body_develop.py b/standards/revolve2/standards/genotypes/cppnwin/modular_robot/v2/_body_develop.py index d4e39a0cf..7fb196f13 100644 --- a/standards/revolve2/standards/genotypes/cppnwin/modular_robot/v2/_body_develop.py +++ b/standards/revolve2/standards/genotypes/cppnwin/modular_robot/v2/_body_develop.py @@ -2,6 +2,7 @@ from queue import Queue from typing import Any +import matplotlib.pyplot as plt import multineat import numpy as np from numpy.typing import NDArray @@ -22,28 +23,26 @@ class __Module: def develop( genotype: multineat.Genome, + visualize: bool = False, # Add a flag to control visualization ) -> BodyV2: """ - Develop a CPPNWIN genotype into a modular robot body. + Develop a CPPNWIN genotype into a modular robot body with optional step-by-step visualization. It is important that the genotype was created using a compatible function. :param genotype: The genotype to create the body from. - :returns: The create body. + :param visualize: Whether to visualize the body development process. + :returns: The created body. """ - max_parts = 20 # Determine the maximum parts available for a robots body. + max_parts = 20 # Determine the maximum parts available for a robot's body. body_net = ( multineat.NeuralNetwork() ) # Instantiate the CPPN network for body construction. - genotype.BuildPhenotype(body_net) # Build the CPPN from the genotype of the robot. - - to_explore: Queue[__Module] = ( - Queue() - ) # Here we have a queue that is used to build our robot. + genotype.BuildCPPN(body_net) # Build the CPPN from the genotype of the robot. + to_explore: Queue[__Module] = Queue() # Queue used to build the robot. grid = np.zeros( shape=(max_parts * 2 + 1, max_parts * 2 + 1, max_parts * 2 + 1), dtype=np.uint8 ) - body = BodyV2() core_position = Vector3( @@ -63,6 +62,12 @@ def develop( ) ) + # Prepare for visualization if enabled + if visualize: + fig = plt.figure() + ax = fig.add_subplot(111, projection="3d") + plt.show(block=False) + while not to_explore.empty(): module = to_explore.get() @@ -72,6 +77,12 @@ def develop( if child is not None: to_explore.put(child) part_count += 1 + if visualize: + __visualize_structure(grid, ax) + + if visualize: + plt.pause(0.001) # Allow the plot to update smoothly + return body @@ -106,7 +117,7 @@ def __evaluate_cppn( The output ranges between [0,1] and we have 4 rotations available (0, 90, 180, 270). """ - angle = max(0, int(outputs[0] * 4 - 1e-6)) * (np.pi / 2.0) + angle = max(0, int(outputs[1] * 4 - 1e-6)) * (np.pi / 2.0) return module_type, angle @@ -128,7 +139,8 @@ def __add_child( if grid[tuple(position)] > 0: return None - """Now we anjust the position for the potential new module to fit the attachment point of the parent, additionally we query the CPPN for child type and angle of the child.""" + """Now we adjust the position for the potential new module to fit the attachment point of the parent, + additionally we query the CPPN for child type and angle of the child""" new_pos = np.array(np.round(position + attachment_point.offset), dtype=np.int64) child_type, angle = __evaluate_cppn(body_net, new_pos, chain_length) @@ -143,18 +155,12 @@ def __add_child( up = __rotate(module.up, forward, Quaternion.from_eulers([angle, 0, 0])) module.module_reference.set_child(child, attachment_index) - return __Module( - position, - forward, - up, - chain_length, - child, - ) + return __Module(position, forward, up, chain_length, child) def __rotate(a: Vector3, b: Vector3, rotation: Quaternion) -> Vector3: """ - Rotates vector a, a given angle around b. + Rotates vector a around the axis defined by vector b by an angle defined in the rotation quaternion. Rodrigues' rotation formula is used. :param a: Vector a. :param b: Vector b. @@ -178,3 +184,23 @@ def __vec3_int(vector: Vector3) -> Vector3[np.int_]: :return: The integer vector. """ return Vector3(list(map(lambda v: int(round(v)), vector)), dtype=np.int64) + + +def __visualize_structure(grid: NDArray[np.uint8], ax: plt.Axes) -> None: + """ + Visualize the structure of the robot's body using Matplotlib. + + :param grid: The 3D grid containing the robot body. + :param ax: The Matplotlib Axes3D object to draw on. + """ + ax.clear() + ax.set_xlabel("X") + ax.set_ylabel("Y") + ax.set_zlabel("Z") + + # Get the occupied grid positions + x, y, z = np.nonzero(grid) + + ax.scatter(x, y, z, c="r", marker="o") + plt.draw() + plt.pause(0.5) \ No newline at end of file diff --git a/standards/revolve2/standards/genotypes/cppnwin/modular_robot/v2/_body_genotype_v2.py b/standards/revolve2/standards/genotypes/cppnwin/modular_robot/v2/_body_genotype_v2.py index 9bd895527..ab42f7c78 100644 --- a/standards/revolve2/standards/genotypes/cppnwin/modular_robot/v2/_body_genotype_v2.py +++ b/standards/revolve2/standards/genotypes/cppnwin/modular_robot/v2/_body_genotype_v2.py @@ -8,11 +8,11 @@ from revolve2.modular_robot.body.v2 import BodyV2 -from ..._multineat_genotype_pickle_wrapper import MultineatGenotypePickleWrapper -from ..._multineat_rng_from_random import multineat_rng_from_random -from ..._random_multineat_genotype import random_multineat_genotype -from .._multineat_params import get_multineat_params -from ._body_develop import develop +from revolve2.standards.genotypes.cppnwin._multineat_genotype_pickle_wrapper import MultineatGenotypePickleWrapper +from revolve2.standards.genotypes.cppnwin._multineat_rng_from_random import multineat_rng_from_random +from revolve2.standards.genotypes.cppnwin._random_multineat_genotype import random_multineat_genotype +from revolve2.standards.genotypes.cppnwin.modular_robot._multineat_params import get_multineat_params +from revolve2.standards.genotypes.cppnwin.modular_robot.v2._body_develop import develop @dataclass @@ -43,7 +43,7 @@ def random_body( innov_db=innov_db, rng=multineat_rng, multineat_params=cls._MULTINEAT_PARAMS, - output_activation_func=multineat.ActivationFunction.UNSIGNED_SINE, + output_activation_func=multineat.ActivationFunction.UNSIGNED_SIGMOID, # changed from UNSIGNED_SIGMOID num_inputs=5, # bias(always 1), pos_x, pos_y, pos_z, chain_length num_outputs=2, # block_type, rotation_type num_initial_mutations=cls._NUM_INITIAL_MUTATIONS, @@ -72,13 +72,27 @@ def mutate_body( MultineatGenotypePickleWrapper( self.body.genotype.MutateWithConstraints( False, - multineat.SearchMode.BLENDED, + multineat.SearchMode.BLENDED, # meaning that mutation can complexify or simplify innov_db, self._MULTINEAT_PARAMS, multineat_rng, ) ) ) + + """ + Genome Genome::MutateWithConstraints(bool t_baby_is_clone, const SearchMode a_searchMode, + InnovationDatabase &a_innov_database, const Parameters &a_Parameters, RNG &a_RNG) const + { + Genome clone;o + do { + clone = Genome(*this); + clone.Mutate(t_baby_is_clone, a_searchMode, a_innov_database, a_Parameters, a_RNG); + } while (clone.FailsConstraints(a_Parameters)); + + return clone; + } + """ @classmethod def crossover_body( @@ -108,6 +122,19 @@ def crossover_body( ) ) ) + + """ + Genome Genome::MateWithConstraints(Genome const& a_dad, bool a_averagemating, bool a_interspecies, + RNG &a_RNG, Parameters const& a_Parameters) const { + + Genome offspring; + do { + offspring = Mate(a_dad, a_averagemating, a_interspecies, a_RNG, a_Parameters); + } while (offspring.FailsConstraints(a_Parameters)); + + return offspring; + } + """ def develop_body(self) -> BodyV2: """ From 49deb174c8e8a9b56ff6eef50ec5252eacbf7e4b Mon Sep 17 00:00:00 2001 From: A-lamo Date: Thu, 30 Jan 2025 11:45:20 +0100 Subject: [PATCH 02/11] Whole lot of stuff but mainly GUI --- codetools/mypy/mypy.ini | 3 - .../physical_robot_core_setup/index.rst | 8 +- examples/0_logic_flow_ea/0a_.ipynb | 720 ++++++++++++++++++ examples/3_experiment_foundations/README.md | 2 - .../4_example_experiment_setups/README.md | 1 - .../5a_physical_robot_remote/main.py | 9 - experimentation/pyproject.toml | 2 +- .../optimization/ea/selection/__init__.py | 11 +- .../optimization/ea/selection/_roulette.py | 35 + gui/backend_example/README.md | 7 + gui/backend_example/_multineat_params.py | 47 ++ gui/backend_example/config.py | 8 + .../config_simulation_parameters.py | 4 + .../database_components/__init__.py | 10 + .../database_components/_base.py | 9 + .../database_components/_experiment.py | 16 + .../database_components/_generation.py | 26 + .../database_components/_genotype.py | 90 +++ .../database_components/_individual.py | 17 + .../database_components/_population.py | 12 + gui/backend_example/evaluator.py | 74 ++ gui/backend_example/fitness_functions.py | 75 ++ gui/backend_example/main.py | 317 ++++++++ gui/backend_example/main_from_gui.py | 154 ++++ gui/backend_example/plot.py | 105 +++ gui/backend_example/reproducer_methods.py | 61 ++ gui/backend_example/requirements.txt | 2 + gui/backend_example/rerun.py | 46 ++ gui/backend_example/selector_methods.py | 163 ++++ gui/backend_example/simulation_parameters.py | 27 + gui/backend_example/terrains.py | 161 ++++ gui/viewer/main_window.py | 317 ++++++++ gui/viewer/parameters.py | 0 gui/viewer/parsing.py | 63 ++ gui/viewer/results_viewer.py | 0 gui/viewer/styles.qss | 0 modular_robot/pyproject.toml | 2 +- .../revolve2/modular_robot/body/_module.py | 1 + .../modular_robot/body/v2/_active_hinge_v2.py | 12 +- .../modular_robot/body/v2/_brick_v2.py | 5 +- .../brain/cpg/_cpg_network_structure.py | 5 +- modular_robot_physical/pyproject.toml | 7 +- .../_physical_interface.py | 4 +- .../v2/_v2_physical_interface.py | 32 +- .../modular_robot_physical/remote/_remote.py | 62 +- .../robot_daemon/_robo_server_impl.py | 21 +- modular_robot_simulation/pyproject.toml | 6 +- project.yml | 1 - requirements_dev.txt | 1 - requirements_editable.txt | 2 - simulation/pyproject.toml | 2 +- simulators/mujoco_simulator/pyproject.toml | 4 +- .../_control_interface_impl.py | 2 - .../mujoco_simulator/_simulate_scene.py | 6 +- .../viewers/_custom_mujoco_viewer.py | 5 +- standards/pyproject.toml | 4 +- .../cppnwin/_random_multineat_genotype.py | 2 +- .../_brain_cpg_network_neighbor.py | 2 +- .../cppnwin/modular_robot/v2/_body_develop.py | 4 +- .../modular_robot/v2/_body_genotype_v2.py | 48 +- 60 files changed, 2671 insertions(+), 171 deletions(-) create mode 100644 examples/0_logic_flow_ea/0a_.ipynb create mode 100644 experimentation/revolve2/experimentation/optimization/ea/selection/_roulette.py create mode 100644 gui/backend_example/README.md create mode 100644 gui/backend_example/_multineat_params.py create mode 100644 gui/backend_example/config.py create mode 100644 gui/backend_example/config_simulation_parameters.py create mode 100644 gui/backend_example/database_components/__init__.py create mode 100644 gui/backend_example/database_components/_base.py create mode 100644 gui/backend_example/database_components/_experiment.py create mode 100644 gui/backend_example/database_components/_generation.py create mode 100644 gui/backend_example/database_components/_genotype.py create mode 100644 gui/backend_example/database_components/_individual.py create mode 100644 gui/backend_example/database_components/_population.py create mode 100644 gui/backend_example/evaluator.py create mode 100644 gui/backend_example/fitness_functions.py create mode 100644 gui/backend_example/main.py create mode 100644 gui/backend_example/main_from_gui.py create mode 100644 gui/backend_example/plot.py create mode 100644 gui/backend_example/reproducer_methods.py create mode 100644 gui/backend_example/requirements.txt create mode 100644 gui/backend_example/rerun.py create mode 100644 gui/backend_example/selector_methods.py create mode 100644 gui/backend_example/simulation_parameters.py create mode 100644 gui/backend_example/terrains.py create mode 100644 gui/viewer/main_window.py create mode 100644 gui/viewer/parameters.py create mode 100644 gui/viewer/parsing.py create mode 100644 gui/viewer/results_viewer.py create mode 100644 gui/viewer/styles.qss diff --git a/codetools/mypy/mypy.ini b/codetools/mypy/mypy.ini index 053c18e55..4094d5608 100644 --- a/codetools/mypy/mypy.ini +++ b/codetools/mypy/mypy.ini @@ -45,9 +45,6 @@ ignore_missing_imports = True [mypy-scipy.*] ignore_missing_imports = True -[mypy-sklearn.*] -ignore_missing_imports = True - [mypy-cairo.*] ignore_missing_imports = True diff --git a/docs/source/physical_robot_core_setup/index.rst b/docs/source/physical_robot_core_setup/index.rst index 0a5736ba5..533a0b640 100644 --- a/docs/source/physical_robot_core_setup/index.rst +++ b/docs/source/physical_robot_core_setup/index.rst @@ -39,9 +39,6 @@ On the RPi adjust the config in `/boot/config.txt` or on newer systems `/boot/fi ------------------ Setting up the RPi ------------------ -**Note**: For students in the CI Group, the RPi is already set up. If the heads are labeled as `flashed`, it means they are already flashed with the setup image, so the following steps are unnecessary. Additionally, the flashed heads are already connected to the *ThymioNet* Wi-Fi. However, the IP address on the head changes from time to time, so you should use the serial connection to log in and obtain the correct IP address. For instructions on how to establish a serial connection, please refer to the section below. -Also, note that ongoing development changes will continue in revolve2-modular-robot_physical and revolve2-robohat packages, so make sure to pip install the latest version in your virtual environment. - This step is the same for all types of hardware. #. Flash the SD card with Raspberry Pi OS (previously Raspbian). Some Important notes: @@ -109,8 +106,8 @@ Setting up Revolve2 on the robot requires different steps, depending on the hard * V1: :code:`pip install "revolve2-modular_robot_physical[botv1] @ git+https://github.com/ci-group/revolve2.git@#subdirectory=modular_robot_physical"`. * V2: :code:`pip install "revolve2-modular_robot_physical[botv2] @ git+https://github.com/ci-group/revolve2.git@#subdirectory=modular_robot_physical"`. - For example, if you want to install the version tagged as 1.2.3, the command would be: - :code:`pip install "revolve2-modular_robot_physical[botv2] @ git+https://github.com/ci-group/revolve2.git@1.2.3#subdirectory=modular_robot_physical"` + For example, if you want to install the version tagged as 1.2.2, the command would be: + :code:`pip install "revolve2-modular_robot_physical[botv2] @ git+https://github.com/ci-group/revolve2.git@1.2.2#subdirectory=modular_robot_physical"` #. Set up the Revolve2 physical robot daemon: #. Create a systemd service file: :code:`sudo nano /etc/systemd/system/robot-daemon.service` @@ -136,7 +133,6 @@ Setting up Revolve2 on the robot requires different steps, depending on the hard #. Here, the :code:`Nice=-10` line sets a high priority for the daemon (lower values are higher priority, with -20 being the highest priority). The :code:`-l` option in the :code:`ExecStart` line tells :code:`robot-daemon` to only listen on the localhost interface. The :code:`-n localhost` option ensures that robot-daemon only runs if it can connect to localhost (preventing certain failure cases). #. Enable and start the service: :code:`sudo systemctl daemon-reload` & :code:`sudo systemctl enable robot-daemon` & :code:`sudo systemctl start robot-daemon`. #. Check if it is running properly using: :code:`sudo systemctl status robot-daemon` - #. If it's not running properly, check the logs using: :code:`journalctl -u robot-daemon -e` ^^^^^^^^^^^^^^^^^^^ V1 Additional Steps diff --git a/examples/0_logic_flow_ea/0a_.ipynb b/examples/0_logic_flow_ea/0a_.ipynb new file mode 100644 index 000000000..5a4cc1f24 --- /dev/null +++ b/examples/0_logic_flow_ea/0a_.ipynb @@ -0,0 +1,720 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Robot Evolution System within the **Revolve** Framework\n", + "\n", + "In this notebook, you will learn to create a **robot evolution system** within the **Revolve** framework. The workflow will follow the operations outlined in the Book *XXX*:\n", + "\n", + "![image info](images/ER_pipeline.png \"Title\") \n", + "\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Target (Revolve-Specific) Setup:\n", + "- **Define robot phenotype** (predefined)\n", + "- **Define robot environment** (terrains)\n", + "- **Define the target behavior** (task-closely related to fitness) target tasks : example. targeted locomotion -> fitness functions" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n" + ] + } + ], + "source": [ + "\"\"\"Robot Phenotype : A robot's phenotype is combination of its morphology and its controller.\"\"\"\n", + "\n", + "\n", + "# Revolve uses modular robot morphologies made up of a core module, (passive)bricks and (actuated) hinges.\n", + "# To learn about the inticacies of the modules see the following files (ctrl + click to open file):\n", + "from revolve2.modular_robot.body.v2 import _attachment_face_core_v2, CoreV2, BrickV2, ActiveHingeV2, BodyV2\n", + "\n", + "# A robot's brain structure is determined by its morphology. \n", + "# Each joint of a robot (active_hinge) corresponds to a neuron (CPG) in the robot's brain,\n", + "# The connections between neurons define how each joint influences the others, and thus the robot's movement. \n", + "# To learn about CPGs see the methodology section of the paper. (https://doi.org/10.1038/s41598-023-48338-4)\n", + "\n", + "# The following files define the brain structure of the robot:\n", + "# Movement arises from the output of the CPG (angle), which is passed on to the actuated hinges (active hinges), \n", + "# which in turn (depending on the morhopology) move the robot.\n", + "from revolve2.modular_robot.brain.cpg import _brain_cpg_instance, _brain_cpg_network_neighbor, _make_cpg_network_structure_neighbor, _cpg_network_structure, _make_cpg_network_structure_neighbor\n", + "\n", + "# Combining the morphology and the brain structure, we get the robot's phenotype.\n", + "# An agent in the simulation will be thus defined by its body and brain." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "\"\"\"Robot Environment : The setting in which the robot is placed, and tested.\"\"\"\n", + "\n", + "# Each robot is placed in a simulated environment, where it can move around freely or interact with objects or other robots.\n", + "# The surface of the environment can be edited to create different terrains. \n", + "\n", + "# The following files contain example terrains that can be used:\n", + "from revolve2.standards import terrains" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "\"\"\"Robot Target Behaviour : The desired behaviour of the robot in the environment.\"\"\"\n", + "\n", + "# Robots can be evolved, to perform a task that corresponds to maximizing a certain fitness function.\n", + "from revolve2.standards import fitness_functions" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Representation:\n", + "- **Define robot genotype**\n", + "- **Define fitness function**" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "\"\"\"Robot Genotype : Body genotype and Brain genotype are separate, and are combined to form the robot's phenotype.\"\"\"\n", + "\n", + "# imports\n", + "from revolve2.standards.modular_robots_v2 import spider_v2\n", + "\n", + "\n", + "# The following is an example of a robot's body genotype.\n", + "\n", + "body = spider_v2() # a simple cross shaped robot, wirth 4 'legs' and two joints (active hinges) per leg.\n", + "\n", + "# See the multineat documentation for more information on the genotype. \n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "import multineat\n", + "from revolve2.experimentation.rng import make_rng_time_seed\n", + "from revolve2.standards.genotypes.cppnwin._multineat_rng_from_random import multineat_rng_from_random\n", + "\n", + "rng = make_rng_time_seed()\n", + "mtn_rng = multineat_rng_from_random(rng)\n", + "\n", + "# genome=multineat.Genome(unsigned int a_ID,\n", + "# unsigned int a_NumInputs,\n", + "# unsigned int a_NumHidden, // ignored for seed_type == 0, specifies number of hidden units if seed_type == 1\n", + "# unsigned int a_NumOutputs,\n", + "# bool a_FS_NEAT, ActivationFunction a_OutputActType,\n", + "# ActivationFunction a_HiddenActType,\n", + "# unsigned int a_SeedType,\n", + "# const Parameters &a_Parameters,\n", + "# unsigned int a_NumLayers)\n", + "\n", + "genome = multineat.Genome(0,\n", + " 2,\n", + " 0,\n", + " 2, \n", + " False,\n", + " multineat.ActivationFunction.SIGNED_SIGMOID,\n", + " multineat.ActivationFunction.SIGNED_SIGMOID,\n", + " 0,\n", + " multineat.Parameters(),\n", + " 1\n", + " )\n", + "\n", + "\n", + "substrate = multineat.Substrate([iter([i for i in range(2)]), iter([i for i in range(2)])],\n", + " [],\n", + " [iter([i for i in range(2)]), iter([i for i in range(2)])])\n", + "nn = multineat.NeuralNetwork()\n", + "\n", + "genome.BuildHyperNEATPhenotype(nn, substrate, mtn_rng)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "DoublesList[0.5, 0.5]\n" + ] + } + ], + "source": [] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "None\n", + "0.0\n", + "====================================================================\n", + "Genome:\n", + "==================================\n", + "\n", + "====================================================================\n", + "Neurons:\n", + "==================================\n", + "ID: 1 : b(0) : \n", + "ID: 2 : b(0) : \n", + "ID: 3 : b(0) : \n", + "ID: 4 : b(0) : \n", + "ID: 5 : b(0) : \n", + "ID: 6 : b(0) : \n", + "ID: 7 : b(0) : \n", + "==================================\n", + "Links:\n", + "==================================\n", + "ID: 1 : w(0) : \n", + "ID: 2 : w(0) : \n", + "ID: 3 : w(0) : \n", + "ID: 4 : w(0) : \n", + "ID: 5 : w(0) : \n", + "ID: 6 : w(0) : \n", + "ID: 7 : w(0) : \n", + "ID: 8 : w(0) : \n", + "ID: 9 : w(0) : \n", + "ID: 10 : w(0) : \n", + "==================================\n", + "====================================================================\n" + ] + } + ], + "source": [ + "import multineat\n", + "\n", + "params = multineat.Parameters()\n", + "\n", + "g = multineat.Genome(0, # id\n", + " 3, # (num_inputs+1)\n", + " 2, # if seed type is 1 : num_hidden_nodes , else : ignored\n", + " 2, # num_outputs\n", + " False, # FS_NEAT (feature selection NEAT https://nn.cs.utexas.edu/?whiteson:gecco05)\n", + " multineat.ActivationFunction.UNSIGNED_SIGMOID, # output activation type\n", + " multineat.ActivationFunction.UNSIGNED_SIGMOID, # hidden activation type\n", + " 1, # seed type\n", + " params, # multineat parameters\n", + " 1 # number of hidden layers (number of hidden nodes is multiplied by this value)\n", + " )\n", + "\n", + "\n", + "\"\"\"\n", + "\t\t\tNumInputs = 1;\n", + "\t\t\tNumHidden = 0;\n", + "\t\t\tNumOutputs = 1;\n", + "\t\t\tFS_NEAT = 0;\n", + "\t\t\tFS_NEAT_links = 1;\n", + "\t\t\tHiddenActType = UNSIGNED_SIGMOID;\n", + "\t\t\tOutputActType = UNSIGNED_SIGMOID;\n", + "\t\t\tSeedType = GenomeSeedType::PERCEPTRON;\n", + "\t\t\tNumLayers = 0;\n", + "\"\"\"\n", + "\n", + "print(g.PrintAllTraits())\n", + "\n", + "g.MutateWithConstraints()\n", + "\n", + "# print(g.GetLinkTraits(True)) # [ (from, to, dict, weight) ]\n", + "# print(g.GetNeuronTraits())" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "'\\n Genome::Genome(const Parameters &a_Parameters,\\n\\t\\t\\t\\t const GenomeInitStruct &in\\n )\\n {\\n ASSERT((a_NumInputs > 1) && (a_NumOutputs > 0));\\n RNG t_RNG;\\n t_RNG.TimeSeed();\\n\\n m_ID = 0;\\n int t_innovnum = 1, t_nnum = 1;\\n\\t\\tGenomeSeedType seed_type = in.SeedType;\\n \\n // override seed_type if 0 hidden units are specified\\n if ((seed_type == LAYERED) && (in.NumHidden == 0))\\n {\\n\\t\\t\\tseed_type = PERCEPTRON;\\n }\\n\\n if (a_Parameters.DontUseBiasNeuron == false)\\n {\\n\\n // Create the input neurons.\\n // Warning! The last one is a bias!\\n // The order of the neurons is very important. It is the following: INPUTS, BIAS, OUTPUTS, HIDDEN ... (no limit)\\n for (unsigned int i = 0; i < (in.NumInputs - 1); i++)\\n {\\n NeuronGene n = NeuronGene(INPUT, t_nnum, 0.0);\\n // Initialize the traits\\n //n.InitTraits(a_Parameters.NeuronTraits, t_RNG); // no need to init traits for inputs\\n m_NeuronGenes.emplace_back(n);\\n t_nnum++;\\n }\\n // add the bias\\n NeuronGene n = NeuronGene(BIAS, t_nnum, 0.0);\\n // Initialize the traits\\n //n.InitTraits(a_Parameters.NeuronTraits, t_RNG); // no need to init traits for inputs\\n\\n m_NeuronGenes.emplace_back(n);\\n t_nnum++;\\n }\\n else\\n {\\n // Create the input neurons without marking the last node as bias.\\n // The order of the neurons is very important. It is the following: INPUTS, OUTPUTS, HIDDEN ... (no limit)\\n for (unsigned int i = 0; i < in.NumInputs; i++)\\n {\\n NeuronGene n = NeuronGene(INPUT, t_nnum, 0.0);\\n // Initialize the traits\\n //n.InitTraits(a_Parameters.NeuronTraits, t_RNG); // no need to init traits for inputs\\n\\n m_NeuronGenes.emplace_back(n);\\n t_nnum++;\\n }\\n }\\n\\n // now the outputs\\n for (unsigned int i = 0; i < (in.NumOutputs); i++)\\n {\\n NeuronGene t_ngene(OUTPUT, t_nnum, 1.0);\\n // Initialize the neuron gene\\'s properties\\n t_ngene.Init((a_Parameters.MinActivationA + a_Parameters.MaxActivationA) / 2.0f,\\n (a_Parameters.MinActivationB + a_Parameters.MaxActivationB) / 2.0f,\\n (a_Parameters.MinNeuronTimeConstant + a_Parameters.MaxNeuronTimeConstant) / 2.0f,\\n (a_Parameters.MinNeuronBias + a_Parameters.MaxNeuronBias) / 2.0f,\\n in.OutputActType);\\n // Initialize the traits\\n t_ngene.InitTraits(a_Parameters.NeuronTraits, t_RNG);\\n\\n m_NeuronGenes.emplace_back(t_ngene);\\n t_nnum++;\\n }\\n \\n // Now add LEO\\n /*if (a_Parameters.Leo)\\n {\\n NeuronGene t_ngene(OUTPUT, t_nnum, 1.0);\\n // Initialize the neuron gene\\'s properties\\n t_ngene.Init((a_Parameters.MinActivationA + a_Parameters.MaxActivationA) / 2.0f,\\n (a_Parameters.MinActivationB + a_Parameters.MaxActivationB) / 2.0f,\\n (a_Parameters.MinNeuronTimeConstant + a_Parameters.MaxNeuronTimeConstant) / 2.0f,\\n (a_Parameters.MinNeuronBias + a_Parameters.MaxNeuronBias) / 2.0f,\\n UNSIGNED_STEP);\\n // Initialize the traits\\n t_ngene.InitTraits(a_Parameters.NeuronTraits, t_RNG);\\n\\n m_NeuronGenes.emplace_back(t_ngene);\\n t_nnum++;\\n in.NumOutputs++;\\n }*/\\n\\n // add and connect hidden neurons if seed type is != 0\\n if ((in.SeedType == LAYERED) && (in.NumHidden > 0))\\n {\\n double lt_inc = 1.0 / (in.NumLayers+1);\\n double initlt = lt_inc;\\n for (unsigned int n = 0; n < in.NumLayers; n++)\\n {\\n for (unsigned int i = 0; i < in.NumHidden; i++)\\n {\\n NeuronGene t_ngene(HIDDEN, t_nnum, 1.0);\\n // Initialize the neuron gene\\'s properties\\n t_ngene.Init((a_Parameters.MinActivationA + a_Parameters.MaxActivationA) / 2.0f,\\n (a_Parameters.MinActivationB + a_Parameters.MaxActivationB) / 2.0f,\\n (a_Parameters.MinNeuronTimeConstant + a_Parameters.MaxNeuronTimeConstant) / 2.0f,\\n (a_Parameters.MinNeuronBias + a_Parameters.MaxNeuronBias) / 2.0f,\\n in.HiddenActType);\\n // Initialize the traits\\n t_ngene.InitTraits(a_Parameters.NeuronTraits, t_RNG);\\n t_ngene.m_SplitY = initlt;\\n \\n m_NeuronGenes.emplace_back(t_ngene);\\n t_nnum++;\\n }\\n \\n initlt += lt_inc;\\n }\\n\\n if (!in.FS_NEAT)\\n {\\n int last_dest_id = in.NumInputs + in.NumOutputs + 1;\\n int last_src_id = 1;\\n int prev_layer_size = in.NumInputs;\\n \\n for (unsigned int n = 0; n < in.NumLayers; n++)\\n {\\n // The links from each previous layer to this hidden node\\n for (unsigned int i = 0; i < in.NumHidden; i++)\\n {\\n for (unsigned int j = 0; j < prev_layer_size; j++)\\n {\\n // add the link\\n // created with zero weights. needs future random initialization. !!!!!!!!\\n // init traits (TODO: maybe init empty traits?)\\n LinkGene l = LinkGene(j + last_src_id, i + last_dest_id, t_innovnum, 0.0, false);\\n l.InitTraits(a_Parameters.LinkTraits, t_RNG);\\n m_LinkGenes.emplace_back(l);\\n t_innovnum++;\\n }\\n }\\n \\n last_dest_id += in.NumHidden;\\n if (n == 0)\\n {\\n // for the first hidden layer, jump over the outputs too\\n last_src_id += prev_layer_size + in.NumOutputs;\\n }\\n else\\n {\\n last_src_id += prev_layer_size;\\n }\\n prev_layer_size = in.NumHidden;\\n }\\n \\n last_dest_id = in.NumInputs + 1;\\n \\n // The links from each previous layer to this output node\\n for (unsigned int i = 0; i < in.NumOutputs; i++)\\n {\\n for (unsigned int j = 0; j < prev_layer_size; j++)\\n {\\n // add the link\\n // created with zero weights. needs future random initialization. !!!!!!!!\\n // init traits (TODO: maybe init empty traits?)\\n LinkGene l = LinkGene(j + last_src_id, i + last_dest_id, t_innovnum, 0.0, false);\\n l.InitTraits(a_Parameters.LinkTraits, t_RNG);\\n m_LinkGenes.emplace_back(l);\\n t_innovnum++;\\n }\\n }\\n \\n /*if (a_Parameters.DontUseBiasNeuron == false)\\n {\\n // Connect the bias as well\\n for (unsigned int i = 0; i < a_NumOutputs; i++)\\n {\\n // add the link\\n // created with zero weights. needs future random initialization. !!!!!!!!\\n LinkGene l = LinkGene(a_NumInputs, i + last_dest_id, t_innovnum, 0.0, false);\\n l.InitTraits(a_Parameters.LinkTraits, t_RNG);\\n m_LinkGenes.emplace_back(l);\\n t_innovnum++;\\n }\\n }*/\\n }\\n }\\n else // The links connecting every input to every output - perceptron structure\\n {\\n if ((!in.FS_NEAT) && (seed_type == PERCEPTRON))\\n {\\n for (unsigned int i = 0; i < (in.NumOutputs); i++)\\n {\\n for (unsigned int j = 0; j < in.NumInputs; j++)\\n {\\n // add the link\\n // created with zero weights. needs future random initialization. !!!!!!!!\\n LinkGene l = LinkGene(j + 1, i + in.NumInputs + 1, t_innovnum, 0.0, false);\\n l.InitTraits(a_Parameters.LinkTraits, t_RNG);\\n m_LinkGenes.emplace_back(l);\\n t_innovnum++;\\n }\\n }\\n }\\n else\\n {\\n // Start very minimally - connect a random input to each output\\n // Also connect the bias to every output\\n \\n std::vector< std::pair > made_already;\\n bool there=false;\\n int linksmade = 0;\\n \\n // do this a few times for more initial links created\\n // TODO: make sure the innovations don\\'t repeat for the same input/output pairs\\n while(linksmade < in.FS_NEAT_links)\\n {\\n for (unsigned int i = 0; i < in.NumOutputs; i++)\\n {\\n int t_inp_id = t_RNG.RandInt(1, in.NumInputs - 1);\\n int t_bias_id = in.NumInputs;\\n int t_outp_id = in.NumInputs + 1 + i;\\n \\n // check if there already\\n there=false;\\n for(auto it = made_already.begin(); it != made_already.end(); it++)\\n {\\n if ((it->first == t_inp_id) && (it->second == t_outp_id))\\n {\\n there = true;\\n break;\\n }\\n }\\n \\n if (!there)\\n {\\n // created with zero weights. needs future random initialization. !!!!!!!!\\n LinkGene l = LinkGene(t_inp_id, t_outp_id, t_innovnum, 0.0, false);\\n l.InitTraits(a_Parameters.LinkTraits, t_RNG);\\n m_LinkGenes.emplace_back(l);\\n t_innovnum++;\\n \\n if (a_Parameters.DontUseBiasNeuron == false)\\n {\\n LinkGene bl = LinkGene(t_bias_id, t_outp_id, t_innovnum, 0.0, false);\\n bl.InitTraits(a_Parameters.LinkTraits, t_RNG);\\n m_LinkGenes.emplace_back(bl);\\n t_innovnum++;\\n }\\n \\n linksmade++;\\n made_already.push_back(std::make_pair(t_inp_id, t_outp_id));\\n }\\n }\\n }\\n }\\n }\\n\\n if (in.FS_NEAT && (in.FS_NEAT_links==1))\\n {\\n throw std::runtime_error(\"Known bug - don\\'t use FS-NEAT with just 1 link and 1/1/1 genome\");\\n }\\n \\n // Also initialize the Genome\\'s traits\\n m_GenomeGene.InitTraits(a_Parameters.GenomeTraits, t_RNG);\\n\\n m_Evaluated = false;\\n m_NumInputs = in.NumInputs;\\n m_NumOutputs = in.NumOutputs;\\n m_Fitness = 0.0;\\n m_AdjustedFitness = 0.0;\\n m_OffspringAmount = 0.0;\\n m_Depth = 0;\\n m_PhenotypeBehavior = NULL;\\n \\n m_initial_num_neurons = NumNeurons();\\n m_initial_num_links = NumLinks();\\n }\\n'" + ] + }, + "execution_count": 16, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "\"\"\"\n", + " Genome::Genome(const Parameters &a_Parameters,\n", + "\t\t\t\t const GenomeInitStruct &in\n", + " )\n", + " {\n", + " ASSERT((a_NumInputs > 1) && (a_NumOutputs > 0));\n", + " RNG t_RNG;\n", + " t_RNG.TimeSeed();\n", + "\n", + " m_ID = 0;\n", + " int t_innovnum = 1, t_nnum = 1;\n", + "\t\tGenomeSeedType seed_type = in.SeedType;\n", + " \n", + " // override seed_type if 0 hidden units are specified\n", + " if ((seed_type == LAYERED) && (in.NumHidden == 0))\n", + " {\n", + "\t\t\tseed_type = PERCEPTRON;\n", + " }\n", + "\n", + " if (a_Parameters.DontUseBiasNeuron == false)\n", + " {\n", + "\n", + " // Create the input neurons.\n", + " // Warning! The last one is a bias!\n", + " // The order of the neurons is very important. It is the following: INPUTS, BIAS, OUTPUTS, HIDDEN ... (no limit)\n", + " for (unsigned int i = 0; i < (in.NumInputs - 1); i++)\n", + " {\n", + " NeuronGene n = NeuronGene(INPUT, t_nnum, 0.0);\n", + " // Initialize the traits\n", + " //n.InitTraits(a_Parameters.NeuronTraits, t_RNG); // no need to init traits for inputs\n", + " m_NeuronGenes.emplace_back(n);\n", + " t_nnum++;\n", + " }\n", + " // add the bias\n", + " NeuronGene n = NeuronGene(BIAS, t_nnum, 0.0);\n", + " // Initialize the traits\n", + " //n.InitTraits(a_Parameters.NeuronTraits, t_RNG); // no need to init traits for inputs\n", + "\n", + " m_NeuronGenes.emplace_back(n);\n", + " t_nnum++;\n", + " }\n", + " else\n", + " {\n", + " // Create the input neurons without marking the last node as bias.\n", + " // The order of the neurons is very important. It is the following: INPUTS, OUTPUTS, HIDDEN ... (no limit)\n", + " for (unsigned int i = 0; i < in.NumInputs; i++)\n", + " {\n", + " NeuronGene n = NeuronGene(INPUT, t_nnum, 0.0);\n", + " // Initialize the traits\n", + " //n.InitTraits(a_Parameters.NeuronTraits, t_RNG); // no need to init traits for inputs\n", + "\n", + " m_NeuronGenes.emplace_back(n);\n", + " t_nnum++;\n", + " }\n", + " }\n", + "\n", + " // now the outputs\n", + " for (unsigned int i = 0; i < (in.NumOutputs); i++)\n", + " {\n", + " NeuronGene t_ngene(OUTPUT, t_nnum, 1.0);\n", + " // Initialize the neuron gene's properties\n", + " t_ngene.Init((a_Parameters.MinActivationA + a_Parameters.MaxActivationA) / 2.0f,\n", + " (a_Parameters.MinActivationB + a_Parameters.MaxActivationB) / 2.0f,\n", + " (a_Parameters.MinNeuronTimeConstant + a_Parameters.MaxNeuronTimeConstant) / 2.0f,\n", + " (a_Parameters.MinNeuronBias + a_Parameters.MaxNeuronBias) / 2.0f,\n", + " in.OutputActType);\n", + " // Initialize the traits\n", + " t_ngene.InitTraits(a_Parameters.NeuronTraits, t_RNG);\n", + "\n", + " m_NeuronGenes.emplace_back(t_ngene);\n", + " t_nnum++;\n", + " }\n", + " \n", + " // Now add LEO\n", + " /*if (a_Parameters.Leo)\n", + " {\n", + " NeuronGene t_ngene(OUTPUT, t_nnum, 1.0);\n", + " // Initialize the neuron gene's properties\n", + " t_ngene.Init((a_Parameters.MinActivationA + a_Parameters.MaxActivationA) / 2.0f,\n", + " (a_Parameters.MinActivationB + a_Parameters.MaxActivationB) / 2.0f,\n", + " (a_Parameters.MinNeuronTimeConstant + a_Parameters.MaxNeuronTimeConstant) / 2.0f,\n", + " (a_Parameters.MinNeuronBias + a_Parameters.MaxNeuronBias) / 2.0f,\n", + " UNSIGNED_STEP);\n", + " // Initialize the traits\n", + " t_ngene.InitTraits(a_Parameters.NeuronTraits, t_RNG);\n", + "\n", + " m_NeuronGenes.emplace_back(t_ngene);\n", + " t_nnum++;\n", + " in.NumOutputs++;\n", + " }*/\n", + "\n", + " // add and connect hidden neurons if seed type is != 0\n", + " if ((in.SeedType == LAYERED) && (in.NumHidden > 0))\n", + " {\n", + " double lt_inc = 1.0 / (in.NumLayers+1);\n", + " double initlt = lt_inc;\n", + " for (unsigned int n = 0; n < in.NumLayers; n++)\n", + " {\n", + " for (unsigned int i = 0; i < in.NumHidden; i++)\n", + " {\n", + " NeuronGene t_ngene(HIDDEN, t_nnum, 1.0);\n", + " // Initialize the neuron gene's properties\n", + " t_ngene.Init((a_Parameters.MinActivationA + a_Parameters.MaxActivationA) / 2.0f,\n", + " (a_Parameters.MinActivationB + a_Parameters.MaxActivationB) / 2.0f,\n", + " (a_Parameters.MinNeuronTimeConstant + a_Parameters.MaxNeuronTimeConstant) / 2.0f,\n", + " (a_Parameters.MinNeuronBias + a_Parameters.MaxNeuronBias) / 2.0f,\n", + " in.HiddenActType);\n", + " // Initialize the traits\n", + " t_ngene.InitTraits(a_Parameters.NeuronTraits, t_RNG);\n", + " t_ngene.m_SplitY = initlt;\n", + " \n", + " m_NeuronGenes.emplace_back(t_ngene);\n", + " t_nnum++;\n", + " }\n", + " \n", + " initlt += lt_inc;\n", + " }\n", + "\n", + " if (!in.FS_NEAT)\n", + " {\n", + " int last_dest_id = in.NumInputs + in.NumOutputs + 1;\n", + " int last_src_id = 1;\n", + " int prev_layer_size = in.NumInputs;\n", + " \n", + " for (unsigned int n = 0; n < in.NumLayers; n++)\n", + " {\n", + " // The links from each previous layer to this hidden node\n", + " for (unsigned int i = 0; i < in.NumHidden; i++)\n", + " {\n", + " for (unsigned int j = 0; j < prev_layer_size; j++)\n", + " {\n", + " // add the link\n", + " // created with zero weights. needs future random initialization. !!!!!!!!\n", + " // init traits (TODO: maybe init empty traits?)\n", + " LinkGene l = LinkGene(j + last_src_id, i + last_dest_id, t_innovnum, 0.0, false);\n", + " l.InitTraits(a_Parameters.LinkTraits, t_RNG);\n", + " m_LinkGenes.emplace_back(l);\n", + " t_innovnum++;\n", + " }\n", + " }\n", + " \n", + " last_dest_id += in.NumHidden;\n", + " if (n == 0)\n", + " {\n", + " // for the first hidden layer, jump over the outputs too\n", + " last_src_id += prev_layer_size + in.NumOutputs;\n", + " }\n", + " else\n", + " {\n", + " last_src_id += prev_layer_size;\n", + " }\n", + " prev_layer_size = in.NumHidden;\n", + " }\n", + " \n", + " last_dest_id = in.NumInputs + 1;\n", + " \n", + " // The links from each previous layer to this output node\n", + " for (unsigned int i = 0; i < in.NumOutputs; i++)\n", + " {\n", + " for (unsigned int j = 0; j < prev_layer_size; j++)\n", + " {\n", + " // add the link\n", + " // created with zero weights. needs future random initialization. !!!!!!!!\n", + " // init traits (TODO: maybe init empty traits?)\n", + " LinkGene l = LinkGene(j + last_src_id, i + last_dest_id, t_innovnum, 0.0, false);\n", + " l.InitTraits(a_Parameters.LinkTraits, t_RNG);\n", + " m_LinkGenes.emplace_back(l);\n", + " t_innovnum++;\n", + " }\n", + " }\n", + " \n", + " /*if (a_Parameters.DontUseBiasNeuron == false)\n", + " {\n", + " // Connect the bias as well\n", + " for (unsigned int i = 0; i < a_NumOutputs; i++)\n", + " {\n", + " // add the link\n", + " // created with zero weights. needs future random initialization. !!!!!!!!\n", + " LinkGene l = LinkGene(a_NumInputs, i + last_dest_id, t_innovnum, 0.0, false);\n", + " l.InitTraits(a_Parameters.LinkTraits, t_RNG);\n", + " m_LinkGenes.emplace_back(l);\n", + " t_innovnum++;\n", + " }\n", + " }*/\n", + " }\n", + " }\n", + " else // The links connecting every input to every output - perceptron structure\n", + " {\n", + " if ((!in.FS_NEAT) && (seed_type == PERCEPTRON))\n", + " {\n", + " for (unsigned int i = 0; i < (in.NumOutputs); i++)\n", + " {\n", + " for (unsigned int j = 0; j < in.NumInputs; j++)\n", + " {\n", + " // add the link\n", + " // created with zero weights. needs future random initialization. !!!!!!!!\n", + " LinkGene l = LinkGene(j + 1, i + in.NumInputs + 1, t_innovnum, 0.0, false);\n", + " l.InitTraits(a_Parameters.LinkTraits, t_RNG);\n", + " m_LinkGenes.emplace_back(l);\n", + " t_innovnum++;\n", + " }\n", + " }\n", + " }\n", + " else\n", + " {\n", + " // Start very minimally - connect a random input to each output\n", + " // Also connect the bias to every output\n", + " \n", + " std::vector< std::pair > made_already;\n", + " bool there=false;\n", + " int linksmade = 0;\n", + " \n", + " // do this a few times for more initial links created\n", + " // TODO: make sure the innovations don't repeat for the same input/output pairs\n", + " while(linksmade < in.FS_NEAT_links)\n", + " {\n", + " for (unsigned int i = 0; i < in.NumOutputs; i++)\n", + " {\n", + " int t_inp_id = t_RNG.RandInt(1, in.NumInputs - 1);\n", + " int t_bias_id = in.NumInputs;\n", + " int t_outp_id = in.NumInputs + 1 + i;\n", + " \n", + " // check if there already\n", + " there=false;\n", + " for(auto it = made_already.begin(); it != made_already.end(); it++)\n", + " {\n", + " if ((it->first == t_inp_id) && (it->second == t_outp_id))\n", + " {\n", + " there = true;\n", + " break;\n", + " }\n", + " }\n", + " \n", + " if (!there)\n", + " {\n", + " // created with zero weights. needs future random initialization. !!!!!!!!\n", + " LinkGene l = LinkGene(t_inp_id, t_outp_id, t_innovnum, 0.0, false);\n", + " l.InitTraits(a_Parameters.LinkTraits, t_RNG);\n", + " m_LinkGenes.emplace_back(l);\n", + " t_innovnum++;\n", + " \n", + " if (a_Parameters.DontUseBiasNeuron == false)\n", + " {\n", + " LinkGene bl = LinkGene(t_bias_id, t_outp_id, t_innovnum, 0.0, false);\n", + " bl.InitTraits(a_Parameters.LinkTraits, t_RNG);\n", + " m_LinkGenes.emplace_back(bl);\n", + " t_innovnum++;\n", + " }\n", + " \n", + " linksmade++;\n", + " made_already.push_back(std::make_pair(t_inp_id, t_outp_id));\n", + " }\n", + " }\n", + " }\n", + " }\n", + " }\n", + "\n", + " if (in.FS_NEAT && (in.FS_NEAT_links==1))\n", + " {\n", + " throw std::runtime_error(\"Known bug - don't use FS-NEAT with just 1 link and 1/1/1 genome\");\n", + " }\n", + " \n", + " // Also initialize the Genome's traits\n", + " m_GenomeGene.InitTraits(a_Parameters.GenomeTraits, t_RNG);\n", + "\n", + " m_Evaluated = false;\n", + " m_NumInputs = in.NumInputs;\n", + " m_NumOutputs = in.NumOutputs;\n", + " m_Fitness = 0.0;\n", + " m_AdjustedFitness = 0.0;\n", + " m_OffspringAmount = 0.0;\n", + " m_Depth = 0;\n", + " m_PhenotypeBehavior = NULL;\n", + " \n", + " m_initial_num_neurons = NumNeurons();\n", + " m_initial_num_links = NumLinks();\n", + " }\n", + "\"\"\"" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[(0, ), (1, ), (2, ), (3, ), (4, ), (5, )]\n" + ] + } + ], + "source": [ + "\"\"\"How exactly does a robot's body determine its brain structure?\"\"\"\n", + "\n", + "# imports\n", + "from revolve2.standards.modular_robots_v2 import *\n", + "from revolve2.modular_robot import ModularRobot\n", + "from revolve2.modular_robot.brain.cpg._make_cpg_network_structure_neighbor import active_hinges_to_cpg_network_structure_neighbor\n", + "\n", + "\n", + "# Example robot body\n", + "body = gecko_v2()\n", + "\n", + "\"\"\"\n", + "def gecko_v2() -> BodyV2:\n", + " # Sample robot with new HW config.\n", + "\n", + " body = BodyV2()\n", + "\n", + " body.core_v2.right_face.bottom = ActiveHingeV2(0.0)\n", + " body.core_v2.right_face.bottom.attachment = BrickV2(0.0)\n", + "\n", + " body.core_v2.left_face.bottom = ActiveHingeV2(0.0)\n", + " body.core_v2.left_face.bottom.attachment = BrickV2(0.0)\n", + "\n", + " body.core_v2.back_face.bottom = ActiveHingeV2(np.pi / 2.0)\n", + " body.core_v2.back_face.bottom.attachment = BrickV2(-np.pi / 2.0)\n", + " body.core_v2.back_face.bottom.attachment.front = ActiveHingeV2(np.pi / 2.0)\n", + " body.core_v2.back_face.bottom.attachment.front.attachment = BrickV2(-np.pi / 2.0)\n", + " body.core_v2.back_face.bottom.attachment.front.attachment.left = ActiveHingeV2(0.0)\n", + " body.core_v2.back_face.bottom.attachment.front.attachment.right = ActiveHingeV2(0.0)\n", + " body.core_v2.back_face.bottom.attachment.front.attachment.left.attachment = BrickV2(\n", + " 0.0\n", + " )\n", + " body.core_v2.back_face.bottom.attachment.front.attachment.right.attachment = (\n", + " BrickV2(0.0)\n", + " )\n", + " \n", + " return body\n", + "\"\"\"\n", + "\n", + "# Get the all the active hinges in the body\n", + "active_hinges = body.find_modules_of_type(ActiveHingeV2)\n", + "\n", + "# This returns CpgNetworkStructure object and a mapping between the indicies of the CPGs and, the active hinges they correspond to.\n", + "cpg_network_structure = active_hinges_to_cpg_network_structure_neighbor(active_hinges)[1]\n", + "\n", + "# \n", + "\n", + "\n", + "print(cpg_network_structure)\n", + "\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "['ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'ActiveHingeV2', 'ActiveHingeV2', 'BrickV2', 'BrickV2', 'BrickV2', 'BrickV2']\n", + "Counter({'ActiveHingeV2': 4161, 'BrickV2': 2621})\n" + ] + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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pIcfflh33333Ods3bdqkoKAglZWV1dmxf85//dd/KTg4WH/729/OO3799dcrLS1NlZWVmjJlirp3765mzZopOjpaSUlJOnz48C/aL1AXHn74YQUFBdlLq1atNHToUO3evfui+9Xma/bOO+/U0KFDzzu2detWBQUFaffu3fr888/1wAMPKCYmRk2bNlW3bt00f/78WukBvxwCDCRJS5cu1RNPPKEtW7Zc8TfUpk2bKiIiopY6u/qPfdddd6lVq1ZasWLFOWNbtmzRvn37lJKSoh9++EGffvqp/v3f/12ffvqp3nrrLRUVFemuu+76RfsF6srQoUN15MgRHTlyRLm5uQoJCdEdd9xxwfrKyspafc2mpKQoJydH33777Tljy5cvV58+fRQXF6f8/HxFRETo9ddfV2Fhof7t3/5N6enpWrBgQa30gV+IhV+9Y8eOWc2bN7e++uora+TIkdYf/vCHgPF3333X6tOnj+VwOKxWrVpZd999t2VZlnXbbbdZkgIWy7Ks5cuXWy6Xy7IsyyoqKrIkWV9++WXAnHPmzLGuu+46e33Pnj3W0KFDrWbNmlkRERHWgw8+aB09etQeT05OtoYPH35O7x999JElyfr+++/PObZlWdb06dOtHj16WK+99prVrl07y+l0WiNHjrT8fr9d4/f7rdGjR1thYWGW2+225syZY912223Wk08+adecOnXKeuqpp6zo6GgrLCzMuuWWW6yPPvrIHk9LS7Ouv/76c/pLTk62+vbte+4/+v+3c+dOS5L1zTffXLAGMMH5XqNbt261JFmlpaXWgQMHLEnW6tWrrQEDBlgOh8Navnz5Oa9Zy7rwe45lWVa7du2suXPn2uuvvvqq5XK5rA0bNliVlZVWZGSk9dxzzwXMV/0et2jRogv2/y//8i/WoEGDLvv88cvjCgy0Zs0ade3aVV26dNGDDz6oZcuW2X/GPDs7W//8z/+s22+/XZ999plyc3N1yy23SJLeeusttWnTRjNmzLB/6vqpzp07q0+fPlq5cmXA9pUrV2r06NGSpLKyMg0ePFg33XSTPvnkE61bt04lJSW6//77a+X89u/fr3feeUdZWVnKysrS5s2bNXPmTHs8LS1N27Zt07vvvqucnBxt3bpVn376acAc48ePV15enlavXq3du3frvvvu09ChQ/X1119L+sdPfl9//bW2bNli73P8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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "# import outputs from brick_vs_hinge.txt\n", + "output_lst = []\n", + "with open('brick_vs_hinge.txt', 'r') as f:\n", + " for line in f:\n", + " output_lst.append(line.strip(\".\"))\n", + "\n", + "output_lst = [output_lst[i].split(\",\")[1].split(\".\")[5].split(\" \")[0] for i in range(len(output_lst))]\n", + "print(output_lst)\n", + "\n", + "# plot the ouput_lst, by counting the number of class occurances\n", + "from collections import Counter\n", + "import matplotlib.pyplot as plt\n", + "\n", + "counter = Counter(output_lst)\n", + "print(counter)\n", + "plt.bar(counter.keys(), counter.values())\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Recombination:\n", + "- **Define mutation operator**\n", + "- **Define crossover operator**" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Selection:\n", + "- **Define parent selection method**\n", + "- **Define survivor selection method**" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Parameters:\n", + "- **Population size**\n", + "- **Operator parameters** (crossover/mutation rate, terminal condition, etc.)" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": ".venv", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.7" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/examples/3_experiment_foundations/README.md b/examples/3_experiment_foundations/README.md index a7df2ae37..1770c7328 100644 --- a/examples/3_experiment_foundations/README.md +++ b/examples/3_experiment_foundations/README.md @@ -6,5 +6,3 @@ Here you will learn about a few things that are important in almost all experime - In `3b_evaluate_single_robot` we will see how you can evaluate robots in ane experiment. - Since evaluating one robot is not very interesting in `3c_evaluate_multiple_isolated_robots` you will see how this evaluation can be done for a population of robots. - Alternatively if you want multiple robots interacting with each other look into example `3d_evaluate_multiple_interacting_robots`. - - diff --git a/examples/4_example_experiment_setups/README.md b/examples/4_example_experiment_setups/README.md index 92c300d26..ed0f72a72 100644 --- a/examples/4_example_experiment_setups/README.md +++ b/examples/4_example_experiment_setups/README.md @@ -9,5 +9,4 @@ Additionally you will learn hwo to use the evolution abstraction layer in Revolv - `4d_robot_bodybrain_ea_database` is the same example, with the addition of databases to allow for data storage. - If you want to add learning into your experiments look at `4e_robot_brain_cmaes`, in which we add learning to a **single** robot. - `4f_robot_brain_cmaes_database` does the same thing as the previous example with the addition of a database. -- Finally you can learn about the exploration of the initial population and their morphological features in `4g_explore_initial_population` diff --git a/examples/5_physical_modular_robots/5a_physical_robot_remote/main.py b/examples/5_physical_modular_robots/5a_physical_robot_remote/main.py index 1cb5f521c..b7ae25644 100644 --- a/examples/5_physical_modular_robots/5a_physical_robot_remote/main.py +++ b/examples/5_physical_modular_robots/5a_physical_robot_remote/main.py @@ -1,12 +1,9 @@ """An example on how to remote control a physical modular robot.""" -from pyrr import Vector3 - from revolve2.experimentation.rng import make_rng_time_seed from revolve2.modular_robot import ModularRobot from revolve2.modular_robot.body import RightAngles from revolve2.modular_robot.body.base import ActiveHinge -from revolve2.modular_robot.body.sensors import CameraSensor from revolve2.modular_robot.body.v2 import ActiveHingeV2, BodyV2, BrickV2 from revolve2.modular_robot.brain.cpg import BrainCpgNetworkNeighborRandom from revolve2.modular_robot_physical import Config, UUIDKey @@ -43,10 +40,6 @@ def make_body() -> ( body.core_v2.right_face.bottom, body.core_v2.right_face.bottom.attachment, ) - """Here we add a camera sensor to the core. If you don't have a physical camera attached, uncomment this line.""" - body.core.add_sensor( - CameraSensor(position=Vector3([0, 0, 0]), camera_size=(480, 640)) - ) return body, active_hinges @@ -107,7 +100,6 @@ def main() -> None: Create a Remote for the physical modular robot. Make sure to target the correct hardware type and fill in the correct IP and credentials. The debug flag is turned on. If the remote complains it cannot keep up, turning off debugging might improve performance. - If you want to display the camera view, set display_camera_view to True. """ print("Initializing robot..") run_remote( @@ -115,7 +107,6 @@ def main() -> None: hostname="localhost", # "Set the robot IP here. debug=True, on_prepared=on_prepared, - display_camera_view=False, ) """ 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. diff --git a/experimentation/pyproject.toml b/experimentation/pyproject.toml index 95fdeb2eb..32bf2e99d 100644 --- a/experimentation/pyproject.toml +++ b/experimentation/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "revolve2-experimentation" -version = "1.2.3" +version = "1.2.2" description = "Revolve2: Tools for experimentation." readme = "../README.md" authors = [ diff --git a/experimentation/revolve2/experimentation/optimization/ea/selection/__init__.py b/experimentation/revolve2/experimentation/optimization/ea/selection/__init__.py index 977e1e37b..29a3846c0 100644 --- a/experimentation/revolve2/experimentation/optimization/ea/selection/__init__.py +++ b/experimentation/revolve2/experimentation/optimization/ea/selection/__init__.py @@ -1,8 +1,9 @@ """Functions for selecting individuals from populations in EA algorithms.""" -from ._multiple_unique import multiple_unique -from ._pareto_frontier import pareto_frontier -from ._topn import topn -from ._tournament import tournament +from revolve2.experimentation.optimization.ea.selection._multiple_unique import multiple_unique +from revolve2.experimentation.optimization.ea.selection._pareto_frontier import pareto_frontier +from revolve2.experimentation.optimization.ea.selection._topn import topn +from revolve2.experimentation.optimization.ea.selection._tournament import tournament +from revolve2.experimentation.optimization.ea.selection._roulette import roulette -__all__ = ["multiple_unique", "pareto_frontier", "topn", "tournament"] +__all__ = ["multiple_unique", "pareto_frontier", "topn", "tournament", "roulette"] diff --git a/experimentation/revolve2/experimentation/optimization/ea/selection/_roulette.py b/experimentation/revolve2/experimentation/optimization/ea/selection/_roulette.py new file mode 100644 index 000000000..b0e4189e9 --- /dev/null +++ b/experimentation/revolve2/experimentation/optimization/ea/selection/_roulette.py @@ -0,0 +1,35 @@ +import random +from typing import TypeVar + +from ._argsort import argsort +from ._supports_lt import SupportsLt + +Genotype = TypeVar("Genotype") +Fitness = TypeVar("Fitness", bound=SupportsLt) + +def roulette(n: int, genotypes: list[Genotype], fitnesses: list[Fitness]) -> list[int]: + """ + Perform roulette wheel selection to choose n genotypes probabilistically based on fitness. + + :param n: The number of genotypes to select. + :param genotypes: The genotypes. Ignored, but kept for compatibility with other selection functions. + :param fitnesses: Fitnesses of the genotypes. + :returns: Indices of the selected genotypes. + """ + assert len(fitnesses) >= n, "Number of selections cannot exceed population size" + + # Normalize fitness values to ensure all are positive + min_fitness = min(fitnesses) + if min_fitness < 0: + fitnesses = [f - min_fitness for f in fitnesses] # Shift all values to be positive + + total_fitness = sum(fitnesses) + assert total_fitness > 0, "Total fitness must be greater than zero for roulette selection" + + # Compute selection probabilities + probabilities = [f / total_fitness for f in fitnesses] + + # Perform roulette wheel selection + selected_indices = random.choices(range(len(fitnesses)), weights=probabilities, k=n) + + return selected_indices \ No newline at end of file diff --git a/gui/backend_example/README.md b/gui/backend_example/README.md new file mode 100644 index 000000000..5989f793d --- /dev/null +++ b/gui/backend_example/README.md @@ -0,0 +1,7 @@ +This is the `robot_bodybrain_ea` example, but with added saving of results to a database. + +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. +Running `plot.py` allows you to plot the robots fitness metrics over each generation. diff --git a/gui/backend_example/_multineat_params.py b/gui/backend_example/_multineat_params.py new file mode 100644 index 000000000..22c69956b --- /dev/null +++ b/gui/backend_example/_multineat_params.py @@ -0,0 +1,47 @@ +import multineat + + +def get_multineat_params() -> multineat.Parameters: + """ + Get Multineat Parameters. + + :returns: The parameters. + """ + multineat_params = multineat.Parameters() + + multineat_params.MutateRemLinkProb = 0.02 + multineat_params.RecurrentProb = 0.0 + multineat_params.OverallMutationRate = 0.15 + multineat_params.MutateAddLinkProb = 0.08 + multineat_params.MutateAddNeuronProb = 0.01 + multineat_params.MutateWeightsProb = 0.90 + multineat_params.MaxWeight = 8.0 + multineat_params.WeightMutationMaxPower = 0.2 + multineat_params.WeightReplacementMaxPower = 1.0 + multineat_params.MutateActivationAProb = 0.0 + multineat_params.ActivationAMutationMaxPower = 0.5 + multineat_params.MinActivationA = 0.05 + multineat_params.MaxActivationA = 6.0 + + multineat_params.MutateNeuronActivationTypeProb = 0.03 + + multineat_params.MutateOutputActivationFunction = False + + multineat_params.ActivationFunction_SignedSigmoid_Prob = 0.0 + multineat_params.ActivationFunction_UnsignedSigmoid_Prob = 0.0 + multineat_params.ActivationFunction_Tanh_Prob = 1.0 + multineat_params.ActivationFunction_TanhCubic_Prob = 0.0 + multineat_params.ActivationFunction_SignedStep_Prob = 1.0 + multineat_params.ActivationFunction_UnsignedStep_Prob = 0.0 + multineat_params.ActivationFunction_SignedGauss_Prob = 1.0 + multineat_params.ActivationFunction_UnsignedGauss_Prob = 0.0 + multineat_params.ActivationFunction_Abs_Prob = 0.0 + multineat_params.ActivationFunction_SignedSine_Prob = 1.0 + multineat_params.ActivationFunction_UnsignedSine_Prob = 0.0 + multineat_params.ActivationFunction_Linear_Prob = 1.0 + + multineat_params.MutateNeuronTraitsProb = 0.0 + multineat_params.MutateLinkTraitsProb = 0.0 + + multineat_params.AllowLoops = False + return multineat_params diff --git a/gui/backend_example/config.py b/gui/backend_example/config.py new file mode 100644 index 000000000..42e51eace --- /dev/null +++ b/gui/backend_example/config.py @@ -0,0 +1,8 @@ +DATABASE_FILE = "database.sqlite" +GENERATIONAL = False +STEADY_STATE = True +NUM_GENERATIONS = 100 +NUM_REPETITIONS = 1 +NUM_SIMULATORS = 8 +OFFSPRING_SIZE = 100 +POPULATION_SIZE = 100 diff --git a/gui/backend_example/config_simulation_parameters.py b/gui/backend_example/config_simulation_parameters.py new file mode 100644 index 000000000..f2f66d3b1 --- /dev/null +++ b/gui/backend_example/config_simulation_parameters.py @@ -0,0 +1,4 @@ +STANDARD_CONTROL_FREQUENCY = 20 +STANDARD_SAMPLING_FREQUENCY = 5 +STANDARD_SIMULATION_TIME = 30 +STANDARD_SIMULATION_TIMESTEP = 0.001 diff --git a/gui/backend_example/database_components/__init__.py b/gui/backend_example/database_components/__init__.py new file mode 100644 index 000000000..5e8ce692e --- /dev/null +++ b/gui/backend_example/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/gui/backend_example/database_components/_base.py b/gui/backend_example/database_components/_base.py new file mode 100644 index 000000000..4e6daad0a --- /dev/null +++ b/gui/backend_example/database_components/_base.py @@ -0,0 +1,9 @@ +"""Base class.""" + +import sqlalchemy.orm as orm + + +class Base(orm.MappedAsDataclass, orm.DeclarativeBase): + """Base class for all SQLAlchemy models in this example.""" + + pass diff --git a/gui/backend_example/database_components/_experiment.py b/gui/backend_example/database_components/_experiment.py new file mode 100644 index 000000000..e091ed409 --- /dev/null +++ b/gui/backend_example/database_components/_experiment.py @@ -0,0 +1,16 @@ +"""Experiment class.""" + +import sqlalchemy.orm as orm + +from revolve2.experimentation.database import HasId + +from ._base import Base + + +class Experiment(Base, HasId): + """Experiment description.""" + + __tablename__ = "experiment" + + # The seed for the rng. + rng_seed: orm.Mapped[int] = orm.mapped_column(nullable=False) diff --git a/gui/backend_example/database_components/_generation.py b/gui/backend_example/database_components/_generation.py new file mode 100644 index 000000000..711966252 --- /dev/null +++ b/gui/backend_example/database_components/_generation.py @@ -0,0 +1,26 @@ +"""Generation class.""" + +import sqlalchemy +import sqlalchemy.orm as orm + +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.""" + + __tablename__ = "generation" + + experiment_id: orm.Mapped[int] = orm.mapped_column( + sqlalchemy.ForeignKey("experiment.id"), nullable=False, init=False + ) + experiment: orm.Mapped[Experiment] = orm.relationship() + generation_index: orm.Mapped[int] = orm.mapped_column(nullable=False) + population_id: orm.Mapped[int] = orm.mapped_column( + sqlalchemy.ForeignKey("population.id"), nullable=False, init=False + ) + population: orm.Mapped[Population] = orm.relationship() diff --git a/gui/backend_example/database_components/_genotype.py b/gui/backend_example/database_components/_genotype.py new file mode 100644 index 000000000..3b16928c6 --- /dev/null +++ b/gui/backend_example/database_components/_genotype.py @@ -0,0 +1,90 @@ +"""Genotype class.""" + +from __future__ import annotations + +import multineat +import numpy as np + +from revolve2.experimentation.database import HasId +from revolve2.modular_robot import ModularRobot +from revolve2.standards.genotypes.cppnwin.modular_robot import BrainGenotypeCpgOrm +from revolve2.standards.genotypes.cppnwin.modular_robot.v2 import BodyGenotypeOrmV2 + +from ._base import Base + + +class Genotype(Base, HasId, BodyGenotypeOrmV2, BrainGenotypeCpgOrm): + """SQLAlchemy model for a genotype for a modular robot body and brain.""" + + __tablename__ = "genotype" + + @classmethod + def random( + cls, + innov_db_body: multineat.InnovationDatabase, + innov_db_brain: multineat.InnovationDatabase, + rng: np.random.Generator, + ) -> Genotype: + """ + Create a random genotype. + + :param innov_db_body: Multineat innovation database for the body. See Multineat library. + :param innov_db_brain: Multineat innovation database for the brain. See Multineat library. + :param rng: Random number generator. + :returns: The created genotype. + """ + body = cls.random_body(innov_db_body, rng) + brain = cls.random_brain(innov_db_brain, rng) + + return Genotype(body=body.body, brain=brain.brain) + + def mutate( + self, + innov_db_body: multineat.InnovationDatabase, + innov_db_brain: multineat.InnovationDatabase, + rng: np.random.Generator, + ) -> Genotype: + """ + Mutate this genotype. + + This genotype will not be changed; a mutated copy will be returned. + + :param innov_db_body: Multineat innovation database for the body. See Multineat library. + :param innov_db_brain: Multineat innovation database for the brain. See Multineat library. + :param rng: Random number generator. + :returns: A mutated copy of the provided genotype. + """ + body = self.mutate_body(innov_db_body, rng) + brain = self.mutate_brain(innov_db_brain, rng) + + return Genotype(body=body.body, brain=brain.brain) + + @classmethod + def crossover( + cls, + parent1: Genotype, + parent2: Genotype, + rng: np.random.Generator, + ) -> Genotype: + """ + Perform crossover between two genotypes. + + :param parent1: The first genotype. + :param parent2: The second genotype. + :param rng: Random number generator. + :returns: A newly created genotype. + """ + body = cls.crossover_body(parent1, parent2, rng) + brain = cls.crossover_brain(parent1, parent2, rng) + + return Genotype(body=body.body, brain=brain.brain) + + def develop(self) -> ModularRobot: + """ + Develop the genotype into a modular robot. + + :returns: The created robot. + """ + body = self.develop_body() + brain = self.develop_brain(body=body) + return ModularRobot(body=body, brain=brain) diff --git a/gui/backend_example/database_components/_individual.py b/gui/backend_example/database_components/_individual.py new file mode 100644 index 000000000..40e4eefc8 --- /dev/null +++ b/gui/backend_example/database_components/_individual.py @@ -0,0 +1,17 @@ +"""Individual class.""" + +from dataclasses import dataclass + +from revolve2.experimentation.optimization.ea import Individual as GenericIndividual + +from ._base import Base +from ._genotype import Genotype + + +@dataclass +class Individual( + Base, GenericIndividual[Genotype], population_table="population", kw_only=True +): + """An individual in a population.""" + + __tablename__ = "individual" diff --git a/gui/backend_example/database_components/_population.py b/gui/backend_example/database_components/_population.py new file mode 100644 index 000000000..7e43a0f7f --- /dev/null +++ b/gui/backend_example/database_components/_population.py @@ -0,0 +1,12 @@ +"""Population class.""" + +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.""" + + __tablename__ = "population" diff --git a/gui/backend_example/evaluator.py b/gui/backend_example/evaluator.py new file mode 100644 index 000000000..da3a94026 --- /dev/null +++ b/gui/backend_example/evaluator.py @@ -0,0 +1,74 @@ +"""Evaluator class.""" + +from database_components import Genotype + +from revolve2.experimentation.evolution.abstract_elements import Evaluator as Eval +from revolve2.modular_robot_simulation import ( + ModularRobotScene, + Terrain, + simulate_scenes, +) +from revolve2.simulators.mujoco_simulator import LocalSimulator +from revolve2.standards import fitness_functions, terrains +from revolve2.standards.simulation_parameters import make_standard_batch_parameters + + +class Evaluator(Eval): + """Provides evaluation of robots.""" + + _simulator: LocalSimulator + _terrain: Terrain + + def __init__( + self, + headless: bool, + num_simulators: int, + ) -> None: + """ + Initialize this object. + + :param headless: `headless` parameter for the physics simulator. + :param num_simulators: `num_simulators` parameter for the physics simulator. + """ + self._simulator = LocalSimulator( + headless=headless, num_simulators=num_simulators + ) + self._terrain = terrains.flat() + + def evaluate( + self, + population: list[Genotype], + ) -> list[float]: + """ + Evaluate multiple robots. + + Fitness is the distance traveled on the xy plane. + + :param population: The robots to simulate. + :returns: Fitnesses of the robots. + """ + robots = [genotype.develop() for genotype in population] + # Create the scenes. + scenes = [] + for robot in robots: + scene = ModularRobotScene(terrain=self._terrain) + scene.add_robot(robot) + scenes.append(scene) + + # Simulate all scenes. + scene_states = simulate_scenes( + simulator=self._simulator, + batch_parameters=make_standard_batch_parameters(), + scenes=scenes, + ) + + # Calculate the xy displacements. + xy_displacements = [ + fitness_functions.xy_displacement( + states[0].get_modular_robot_simulation_state(robot), + states[-1].get_modular_robot_simulation_state(robot), + ) + for robot, states in zip(robots, scene_states) + ] + + return xy_displacements diff --git a/gui/backend_example/fitness_functions.py b/gui/backend_example/fitness_functions.py new file mode 100644 index 000000000..774b6d315 --- /dev/null +++ b/gui/backend_example/fitness_functions.py @@ -0,0 +1,75 @@ +"""Standard fitness functions for modular robots.""" + +import math + +from revolve2.modular_robot_simulation import ModularRobotSimulationState + + +def xy_displacement( + begin_state: ModularRobotSimulationState, end_state: ModularRobotSimulationState +) -> float: + """ + Calculate the distance traveled on the xy-plane by a single modular robot. + + :param begin_state: Begin state of the robot. + :param end_state: End state of the robot. + :returns: The calculated fitness. + """ + begin_position = begin_state.get_pose().position + end_position = end_state.get_pose().position + return math.sqrt( + (begin_position.x - end_position.x) ** 2 + + (begin_position.y - end_position.y) ** 2 + ) + +def x_speed_Miras2021(x_distance: float, simulation_time = float) -> float: + """Goal: + Calculate the fitness for speed in x direction for a single modular robot according to + Miras (2021). + ------------------------------------------------------------------------------------------- + Input: + x_distance: The distance traveled in the x direction. + simulation_time: The time of the simulation. + ------------------------------------------------------------------------------------------- + Output: + The calculated fitness. + """ + # Begin and end Position + + # Calculate the speed in x direction + vx = float((x_distance / simulation_time) * 100) + if vx > 0: + return vx + elif vx == 0: + return -0.1 + else: + return vx / 10 + +def x_efficiency(xbest: float, eexp: float, simulation_time: float) -> float: + """Goal: + Calculate the efficiency of a robot for locomotion in x direction. + ------------------------------------------------------------------------------------------- + Input: + xbest: The furthest distance traveled in the x direction. + eexp: The energy expended. + simulation_time: The time of the simulation. + ------------------------------------------------------------------------------------------- + Output: + The calculated fitness. + """ + def food(xbest, bmet): + # Get food + if xbest <= 0: + return 0 + else: + food = (xbest / 0.05) * (80 * bmet) + return food + + def scale_EEXP(eexp, bmet): + return eexp / 346 * (80 * bmet) + + # Get baseline metabolism + bmet = 80 + battery = -bmet * simulation_time + food(xbest, bmet) - scale_EEXP(eexp, bmet) + + return battery \ No newline at end of file diff --git a/gui/backend_example/main.py b/gui/backend_example/main.py new file mode 100644 index 000000000..9b1407ff4 --- /dev/null +++ b/gui/backend_example/main.py @@ -0,0 +1,317 @@ +"""Main script for the example.""" + +import logging +from typing import Any + +import config +import multineat +import numpy as np +import numpy.typing as npt +from database_components import ( + Base, + Experiment, + Generation, + Genotype, + Individual, + Population, +) +from evaluator import Evaluator +from sqlalchemy.engine import Engine +from sqlalchemy.orm import Session + +from revolve2.experimentation.database import OpenMethod, open_database_sqlite +from revolve2.experimentation.evolution import ModularRobotEvolution +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, seed_from_time + + +class ParentSelector(Selector): + """Selector class for parent selection.""" + + rng: np.random.Generator + offspring_size: int + + def __init__(self, offspring_size: int, rng: np.random.Generator) -> None: + """ + Initialize the parent selector. + + :param offspring_size: The offspring size. + :param rng: The rng generator. + """ + self.offspring_size = offspring_size + self.rng = rng + + def select( + self, population: Population, **kwargs: Any + ) -> tuple[npt.NDArray[np.int_], dict[str, Population]]: + """ + Select the parents. + + :param population: The population of robots. + :param kwargs: Other parameters. + :return: The parent pairs. + """ + return np.array( + [ + selection.multiple_unique( + selection_size=2, + + population=[individual.genotype for individual in population.individuals], + fitnesses=[individual.fitness for individual in population.individuals], + + selection_function=lambda _, fitnesses: selection.tournament( + rng=self.rng, fitnesses=fitnesses, k=2), + ) + for _ in range(self.offspring_size) + ], + ), {"parent_population": population} + + +class SurvivorSelector(Selector): + """Selector class for survivor selection.""" + + rng: np.random.Generator + + def __init__(self, rng: np.random.Generator) -> None: + """ + Initialize the parent selector. + + :param rng: The rng generator. + """ + self.rng = rng + + def select( + self, population: Population, **kwargs: Any + ) -> tuple[Population, dict[str, Any]]: + """ + Select survivors using a tournament. + + :param population: The population the parents come from. + :param kwargs: The offspring, with key 'offspring_population'. + :returns: A newly created population. + :raises ValueError: If the population is empty. + """ + offspring = kwargs.get("children") + offspring_fitness = kwargs.get("child_task_performance") + if offspring is None or offspring_fitness is None: + raise ValueError( + "No offspring was passed with positional argument 'children' and / or 'child_task_performance'." + ) + + original_survivors, offspring_survivors = population_management.steady_state( + old_genotypes=[i.genotype for i in population.individuals], + old_fitnesses=[i.fitness for i in population.individuals], + new_genotypes=offspring, + new_fitnesses=offspring_fitness, + selection_function=lambda n, genotypes, fitnesses: selection.multiple_unique( + selection_size=n, + population=genotypes, + fitnesses=fitnesses, + selection_function=lambda _, fitnesses: selection.tournament( + rng=self.rng, fitnesses=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], + fitness=offspring_fitness[i], + ) + for i in offspring_survivors + ] + ), + {}, + ) + + +class CrossoverReproducer(Reproducer): + """A simple crossover reproducer using multineat.""" + + rng: np.random.Generator + innov_db_body: multineat.InnovationDatabase + innov_db_brain: multineat.InnovationDatabase + + def __init__( + self, + rng: np.random.Generator, + innov_db_body: multineat.InnovationDatabase, + innov_db_brain: multineat.InnovationDatabase, + ): + """ + Initialize the reproducer. + + :param rng: The ranfom generator. + :param innov_db_body: The innovation database for the body. + :param innov_db_brain: The innovation database for the brain. + """ + self.rng = rng + self.innov_db_body = innov_db_body + self.innov_db_brain = innov_db_brain + + def reproduce( + self, population: npt.NDArray[np.int_], **kwargs: Any + ) -> list[Genotype]: + """ + Reproduce the population by crossover. + + :param population: The parent pairs. + :param kwargs: Additional keyword arguments. + :return: The genotypes of the children. + :raises ValueError: If the parent population is not passed as a kwarg `parent_population`. + """ + parent_population: Population | None = kwargs.get("parent_population") + if parent_population is None: + raise ValueError("No parent population given.") + + offspring_genotypes = [ + Genotype.crossover( + parent_population.individuals[parent1_i].genotype, + parent_population.individuals[parent2_i].genotype, + self.rng, + ).mutate(self.innov_db_body, self.innov_db_brain, self.rng) + for parent1_i, parent2_i in population + ] + return offspring_genotypes + + +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. + rng_seed = seed_from_time() + rng = make_rng(rng_seed) + + # Create and save the experiment instance. + experiment = Experiment(rng_seed=rng_seed) + logging.info("Saving experiment configuration.") + with Session(dbengine) as session: + session.add(experiment) + session.commit() + + # CPPN innovation databases. + innov_db_body = multineat.InnovationDatabase() + innov_db_brain = multineat.InnovationDatabase() + + """ + Here we initialize the components used for the evolutionary process. + + - evaluator: Allows us to evaluate a population of modular robots. + - parent_selector: Allows us to select parents from a population of modular robots. + - survivor_selector: Allows us to select survivors from a population. + - crossover_reproducer: Allows us to generate offspring from parents. + - modular_robot_evolution: The evolutionary process as a object that can be iterated. + """ + evaluator = Evaluator(headless=True, num_simulators=config.NUM_SIMULATORS) + parent_selector = ParentSelector(offspring_size=config.OFFSPRING_SIZE, rng=rng) + survivor_selector = SurvivorSelector(rng=rng) + crossover_reproducer = CrossoverReproducer( + rng=rng, innov_db_body=innov_db_body, innov_db_brain=innov_db_brain + ) + + modular_robot_evolution = ModularRobotEvolution( + parent_selection=parent_selector, + survivor_selection=survivor_selector, + evaluator=evaluator, + reproducer=crossover_reproducer, + ) + + # Create an initial population, as we cant start from nothing. + logging.info("Generating initial population.") + initial_genotypes = [ + Genotype.random( + innov_db_body=innov_db_body, + innov_db_brain=innov_db_brain, + rng=rng, + ) + for _ in range(config.POPULATION_SIZE) + ] + + # 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, strict=True + ) + ] + ) + + # Finish the zeroth generation and save it to the database. + generation = Generation( + experiment=experiment, generation_index=0, population=population + ) + save_to_db(dbengine, generation) + + # 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}." + ) + + # Here we iterate the evolutionary process using the step. + population = modular_robot_evolution.step(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, + ) + save_to_db(dbengine, generation) + + +def main() -> None: + """Run the program.""" + # Set up logging. + setup_logging(file_name="log.txt") + + # Open the database, only if it does not already exists. + dbengine = open_database_sqlite( + config.DATABASE_FILE, open_method=OpenMethod.NOT_EXISTS_AND_CREATE + ) + # Create the structure of the database. + Base.metadata.create_all(dbengine) + + # Run the experiment several times. + for _ in range(config.NUM_REPETITIONS): + run_experiment(dbengine) + + +def save_to_db(dbengine: Engine, generation: Generation) -> None: + """ + Save the current generation to the database. + + :param dbengine: The database engine. + :param generation: The current generation. + """ + logging.info("Saving generation.") + with Session(dbengine, expire_on_commit=False) as session: + session.add(generation) + session.commit() + + +if __name__ == "__main__": + main() diff --git a/gui/backend_example/main_from_gui.py b/gui/backend_example/main_from_gui.py new file mode 100644 index 000000000..6c60e39c6 --- /dev/null +++ b/gui/backend_example/main_from_gui.py @@ -0,0 +1,154 @@ +"""Main script for the example.""" + +import logging + +import config +import multineat +from database_components import ( + Base, + Experiment, + Generation, + Genotype, + Individual, + Population, +) +from evaluator import Evaluator +from sqlalchemy.engine import Engine +from sqlalchemy.orm import Session + +from reproducer_methods import CrossoverReproducer +from selector_methods import ParentSelector, SurvivorSelector + +from revolve2.experimentation.database import OpenMethod, open_database_sqlite +from revolve2.experimentation.evolution import ModularRobotEvolution +from revolve2.experimentation.logging import setup_logging +from revolve2.experimentation.rng import make_rng, seed_from_time + + +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. + rng_seed = seed_from_time() + rng = make_rng(rng_seed) + + # Create and save the experiment instance. + experiment = Experiment(rng_seed=rng_seed) + logging.info("Saving experiment configuration.") + with Session(dbengine) as session: + session.add(experiment) + session.commit() + + # CPPN innovation databases. + innov_db_body = multineat.InnovationDatabase() + innov_db_brain = multineat.InnovationDatabase() + + """ + Here we initialize the components used for the evolutionary process. + + - evaluator: Allows us to evaluate a population of modular robots. + - parent_selector: Allows us to select parents from a population of modular robots. + - survivor_selector: Allows us to select survivors from a population. + - crossover_reproducer: Allows us to generate offspring from parents. + - modular_robot_evolution: The evolutionary process as a object that can be iterated. + """ + evaluator = Evaluator(headless=True, num_simulators=config.NUM_SIMULATORS) + parent_selector = ParentSelector(offspring_size=config.OFFSPRING_SIZE, rng=rng) + survivor_selector = SurvivorSelector(rng=rng) + crossover_reproducer = CrossoverReproducer(rng=rng, innov_db_body=innov_db_body, innov_db_brain=innov_db_brain) + + modular_robot_evolution = ModularRobotEvolution( + parent_selection=parent_selector, + survivor_selection=survivor_selector, + evaluator=evaluator, + reproducer=crossover_reproducer, + ) + + # Create an initial population, as we cant start from nothing. + logging.info("Generating initial population.") + initial_genotypes = [ + Genotype.random( + innov_db_body=innov_db_body, + innov_db_brain=innov_db_brain, + rng=rng, + ) + for _ in range(config.POPULATION_SIZE) + ] + + # 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, strict=True + ) + ] + ) + + # Finish the zeroth generation and save it to the database. + generation = Generation( + experiment=experiment, generation_index=0, population=population + ) + save_to_db(dbengine, generation) + + # 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}." + ) + + # Here we iterate the evolutionary process using the step. + population = modular_robot_evolution.step(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, + ) + save_to_db(dbengine, generation) + + +def main() -> None: + """Run the program.""" + # Set up logging. + setup_logging(file_name="log.txt") + + # Open the database, only if it does not already exists. + dbengine = open_database_sqlite( + config.DATABASE_FILE, open_method=OpenMethod.NOT_EXISTS_AND_CREATE + ) + # Create the structure of the database. + Base.metadata.create_all(dbengine) + + # Run the experiment several times. + for _ in range(config.NUM_REPETITIONS): + run_experiment(dbengine) + + +def save_to_db(dbengine: Engine, generation: Generation) -> None: + """ + Save the current generation to the database. + + :param dbengine: The database engine. + :param generation: The current generation. + """ + logging.info("Saving generation.") + with Session(dbengine, expire_on_commit=False) as session: + session.add(generation) + session.commit() + + +if __name__ == "__main__": + main() diff --git a/gui/backend_example/plot.py b/gui/backend_example/plot.py new file mode 100644 index 000000000..4dca0979d --- /dev/null +++ b/gui/backend_example/plot.py @@ -0,0 +1,105 @@ +"""Plot fitness over generations for all experiments, averaged.""" + +import config +import matplotlib.pyplot as plt +import pandas +from database_components import Experiment, Generation, Individual, Population +from sqlalchemy import select + +from revolve2.experimentation.database import OpenMethod, open_database_sqlite +from revolve2.experimentation.logging import setup_logging +import argparse + +def argument_parser() -> argparse.ArgumentParser: + """Create an argument parser.""" + parser = argparse.ArgumentParser(description="Plot fitness over generations for all experiments, averaged.") + return parser + +def main() -> None: + """Run the program.""" + setup_logging() + + dbengine = open_database_sqlite( + "../viewer/"+config.DATABASE_FILE, open_method=OpenMethod.OPEN_IF_EXISTS + ) + + df = pandas.read_sql( + select( + Experiment.id.label("experiment_id"), + Generation.generation_index, + Individual.fitness, + ) + .join_from(Experiment, Generation, Experiment.id == Generation.experiment_id) + .join_from(Generation, Population, Generation.population_id == Population.id) + .join_from(Population, Individual, Population.id == Individual.population_id), + dbengine, + ) + + agg_per_experiment_per_generation = ( + df.groupby(["experiment_id", "generation_index"]) + .agg({"fitness": ["max", "mean"]}) + .reset_index() + ) + agg_per_experiment_per_generation.columns = [ + "experiment_id", + "generation_index", + "max_fitness", + "mean_fitness", + ] + + agg_per_generation = ( + agg_per_experiment_per_generation.groupby("generation_index") + .agg({"max_fitness": ["mean", "std"], "mean_fitness": ["mean", "std"]}) + .reset_index() + ) + agg_per_generation.columns = [ + "generation_index", + "max_fitness_mean", + "max_fitness_std", + "mean_fitness_mean", + "mean_fitness_std", + ] + + plt.figure() + + # Plot max + plt.plot( + agg_per_generation["generation_index"], + agg_per_generation["max_fitness_mean"], + label="Max fitness", + color="b", + ) + plt.fill_between( + agg_per_generation["generation_index"], + agg_per_generation["max_fitness_mean"] - agg_per_generation["max_fitness_std"], + agg_per_generation["max_fitness_mean"] + agg_per_generation["max_fitness_std"], + color="b", + alpha=0.2, + ) + + # Plot mean + plt.plot( + agg_per_generation["generation_index"], + agg_per_generation["mean_fitness_mean"], + label="Mean fitness", + color="r", + ) + plt.fill_between( + agg_per_generation["generation_index"], + agg_per_generation["mean_fitness_mean"] + - agg_per_generation["mean_fitness_std"], + agg_per_generation["mean_fitness_mean"] + + agg_per_generation["mean_fitness_std"], + color="r", + alpha=0.2, + ) + + plt.xlabel("Generation index") + plt.ylabel("Fitness") + plt.title("Mean and max fitness across repetitions with std as shade") + plt.legend() + plt.show() + + +if __name__ == "__main__": + main() diff --git a/gui/backend_example/reproducer_methods.py b/gui/backend_example/reproducer_methods.py new file mode 100644 index 000000000..692656b8d --- /dev/null +++ b/gui/backend_example/reproducer_methods.py @@ -0,0 +1,61 @@ + +import multineat + +import numpy as np +from typing import Any +import numpy.typing as npt + +from revolve2.experimentation.evolution.abstract_elements import Reproducer +from database_components import ( + Genotype, + Population, +) + +class CrossoverReproducer(Reproducer): + """A simple crossover reproducer using multineat.""" + + rng: np.random.Generator + innov_db_body: multineat.InnovationDatabase + innov_db_brain: multineat.InnovationDatabase + + def __init__( + self, + rng: np.random.Generator, + innov_db_body: multineat.InnovationDatabase, + innov_db_brain: multineat.InnovationDatabase, + ): + """ + Initialize the reproducer. + + :param rng: The ranfom generator. + :param innov_db_body: The innovation database for the body. + :param innov_db_brain: The innovation database for the brain. + """ + self.rng = rng + self.innov_db_body = innov_db_body + self.innov_db_brain = innov_db_brain + + def reproduce( + self, population: npt.NDArray[np.int_], **kwargs: Any + ) -> list[Genotype]: + """ + Reproduce the population by crossover. + + :param population: The parent pairs. + :param kwargs: Additional keyword arguments. + :return: The genotypes of the children. + :raises ValueError: If the parent population is not passed as a kwarg `parent_population`. + """ + parent_population: Population | None = kwargs.get("parent_population") + if parent_population is None: + raise ValueError("No parent population given.") + + offspring_genotypes = [ + Genotype.crossover( + parent_population.individuals[parent1_i].genotype, + parent_population.individuals[parent2_i].genotype, + self.rng, + ).mutate(self.innov_db_body, self.innov_db_brain, self.rng) + for parent1_i, parent2_i in population + ] + return offspring_genotypes \ No newline at end of file diff --git a/gui/backend_example/requirements.txt b/gui/backend_example/requirements.txt new file mode 100644 index 000000000..373c93ed6 --- /dev/null +++ b/gui/backend_example/requirements.txt @@ -0,0 +1,2 @@ +pandas>=2.1.0 +matplotlib>=3.8.0 diff --git a/gui/backend_example/rerun.py b/gui/backend_example/rerun.py new file mode 100644 index 000000000..54c158712 --- /dev/null +++ b/gui/backend_example/rerun.py @@ -0,0 +1,46 @@ +"""Rerun the best robot between all experiments.""" + +import logging + +import config +from database_components import Genotype, Individual +from evaluator import Evaluator +from sqlalchemy import select +from sqlalchemy.orm import Session + +from revolve2.experimentation.database import OpenMethod, open_database_sqlite +from revolve2.experimentation.logging import setup_logging + + +def main() -> None: + """Perform the rerun.""" + setup_logging() + + # Load the best individual from the database. + dbengine = open_database_sqlite( + config.DATABASE_FILE, open_method=OpenMethod.OPEN_IF_EXISTS + ) + + with Session(dbengine) as ses: + row = ses.execute( + select(Genotype, Individual.fitness) + .join_from(Genotype, Individual, Genotype.id == Individual.genotype_id) + .order_by(Individual.fitness.desc()) + .limit(1) + ).one() + assert row is not None + + genotype = row[0] + fitness = row[1] + + logging.info(f"Best fitness: {fitness}") + + # Create the evaluator. + evaluator = Evaluator(headless=False, num_simulators=1) + + # Show the robot. + evaluator.evaluate([genotype]) + + +if __name__ == "__main__": + main() diff --git a/gui/backend_example/selector_methods.py b/gui/backend_example/selector_methods.py new file mode 100644 index 000000000..82318768b --- /dev/null +++ b/gui/backend_example/selector_methods.py @@ -0,0 +1,163 @@ +from revolve2.experimentation.evolution.abstract_elements import Selector +from revolve2.experimentation.optimization.ea import population_management, selection +import numpy as np +import numpy.typing as npt +from typing import Any +from database_components import ( + Individual, + Population, +) +import config + + + +class ParentSelector(Selector): + """Selector class for parent selection.""" + + rng: np.random.Generator + offspring_size: int + + def __init__(self, offspring_size: int, rng: np.random.Generator, + generational=config.GENERATIONAL, steady_state=config.STEADY_STATE, + selection_func=selection.tournament) -> None: + """ + Initialize the parent selector. + + :param offspring_size: The offspring size. + :param rng: The rng generator. + """ + self.offspring_size = offspring_size + self.rng = rng + self.generational = generational + self.steady_state = steady_state + self.selection_func = selection_func + + + def select( + self, population: Population, **kwargs: Any + ) -> tuple[npt.NDArray[np.int_], dict[str, Population]]: + """ + Select the parents. + + :param population: The population of robots. + :param kwargs: Other parameters. + :return: The parent pairs. + """ + if self.generational: + return np.array( + [ + selection.multiple_unique( + selection_size=2, + + population=[individual.genotype for individual in population.individuals], + fitnesses=[individual.fitness for individual in population.individuals], + + selection_function=lambda _, fitnesses: self.selection_func( + rng=self.rng, fitnesses=fitnesses, k=2), + ) + for _ in range(self.offspring_size) + ], + ), {"parent_population": population} + else: + return np.array( + [ + selection.multiple_unique( + selection_size=2, + + population=[individual.genotype for individual in population.individuals], + fitnesses=[individual.fitness for individual in population.individuals], + + selection_function=lambda _, fitnesses: self.selection_func( + rng=self.rng, fitnesses=fitnesses, k=2), + ) + for _ in range(self.offspring_size) + ], + ), {"parent_population": population} + +class SurvivorSelector(Selector): + """Selector class for survivor selection.""" + + rng: np.random.Generator + + def __init__(self, rng: np.random.Generator, generational=config.GENERATIONAL, + steady_state=config.STEADY_STATE, selection_func=selection.tournament) -> None: + """ + Initialize the parent selector. + + :param rng: The rng generator. + """ + self.rng = rng + self.generational = generational + self.steady_state = steady_state + self.selection_func = selection_func + + def select( + self, population: Population, **kwargs: Any + ) -> tuple[Population, dict[str, Any]]: + """ + Select survivors using a tournament. + + :param population: The population the parents come from. + :param kwargs: The offspring, with key 'offspring_population'. + :returns: A newly created population. + :raises ValueError: If the population is empty. + """ + offspring = kwargs.get("children") + offspring_fitness = kwargs.get("child_task_performance") + if offspring is None or offspring_fitness is None: + raise ValueError( + "No offspring was passed with positional argument 'children' and / or 'child_task_performance'." + ) + + if self.generational: + original_survivors, offspring_survivors = population_management.generational( + old_genotypes=[i.genotype for i in population.individuals], + old_fitnesses=[i.fitness for i in population.individuals], + new_genotypes=offspring, + new_fitnesses=offspring_fitness, + selection_function=lambda n, genotypes, fitnesses: selection.multiple_unique( + selection_size=n, + population=genotypes, + fitnesses=fitnesses, + selection_function=lambda _, fitnesses: self.selection_func( + rng=self.rng, fitnesses=fitnesses, k=2 + ), + ), + ) + + else: + original_survivors, offspring_survivors = population_management.steady_state( + + old_genotypes=[i.genotype for i in population.individuals], + old_fitnesses=[i.fitness for i in population.individuals], + new_genotypes=offspring, + new_fitnesses=offspring_fitness, + + selection_function=lambda n, genotypes, fitnesses: selection.multiple_unique( + selection_size=n, + population=genotypes, + fitnesses=fitnesses, + selection_function=lambda _, fitnesses: self.selection_func( + rng=self.rng, fitnesses=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], + fitness=offspring_fitness[i], + ) + for i in offspring_survivors + ] + ), + {}, + ) diff --git a/gui/backend_example/simulation_parameters.py b/gui/backend_example/simulation_parameters.py new file mode 100644 index 000000000..5ffbd2cec --- /dev/null +++ b/gui/backend_example/simulation_parameters.py @@ -0,0 +1,27 @@ +"""Standard simulation functions and parameters.""" + +from revolve2.simulation.simulator import BatchParameters +from config_simulation_parameters import STANDARD_SIMULATION_TIME, STANDARD_SAMPLING_FREQUENCY, STANDARD_SIMULATION_TIMESTEP, STANDARD_CONTROL_FREQUENCY + + +def make_standard_batch_parameters( + simulation_time: int = STANDARD_SIMULATION_TIME, + sampling_frequency: float | None = STANDARD_SAMPLING_FREQUENCY, + simulation_timestep: float = STANDARD_SIMULATION_TIMESTEP, + control_frequency: float = STANDARD_CONTROL_FREQUENCY, +) -> BatchParameters: + """ + Create batch parameters as standardized within the CI Group. + + :param simulation_time: As defined in the `BatchParameters` class. + :param sampling_frequency: As defined in the `BatchParameters` class. + :param simulation_timestep: As defined in the `BatchParameters` class. + :param control_frequency: As defined in the `BatchParameters` class. + :returns: The create batch parameters. + """ + return BatchParameters( + simulation_time=simulation_time, + sampling_frequency=sampling_frequency, + simulation_timestep=simulation_timestep, + control_frequency=control_frequency, + ) diff --git a/gui/backend_example/terrains.py b/gui/backend_example/terrains.py new file mode 100644 index 000000000..17940f699 --- /dev/null +++ b/gui/backend_example/terrains.py @@ -0,0 +1,161 @@ +"""Standard terrains.""" + +import math + +import numpy as np +import numpy.typing as npt +from noise import pnoise2 +from pyrr import Vector3 + +from revolve2.modular_robot_simulation import Terrain +from revolve2.simulation.scene import Pose +from revolve2.simulation.scene.geometry import GeometryHeightmap, GeometryPlane +from revolve2.simulation.scene.vector2 import Vector2 + + +def flat(size: Vector2 = Vector2([20.0, 20.0])) -> Terrain: + """ + Create a flat plane terrain. + + :param size: Size of the plane. + :returns: The created terrain. + """ + return Terrain( + static_geometry=[ + GeometryPlane( + pose=Pose(), + mass=0.0, + size=size, + ) + ] + ) + + +def crater( + size: tuple[float, float], + ruggedness: float, + curviness: float, + granularity_multiplier: float = 1.0, +) -> Terrain: + r""" + Create a crater-like terrain with rugged floor using a heightmap. + + It will look like:: + + | | + \_ .' + '.,^_..' + + A combination of the rugged and bowl heightmaps. + + :param size: Size of the crater. + :param ruggedness: How coarse the ground is. + :param curviness: Height of the edges of the crater. + :param granularity_multiplier: Multiplier for how many edges are used in the heightmap. + :returns: The created terrain. + """ + NUM_EDGES = 100 # arbitrary constant to get a nice number of edges + + num_edges = ( + int(NUM_EDGES * size[0] * granularity_multiplier), + int(NUM_EDGES * size[1] * granularity_multiplier), + ) + + rugged = rugged_heightmap( + size=size, + num_edges=num_edges, + density=1.5, + ) + bowl = bowl_heightmap(num_edges=num_edges) + + max_height = ruggedness + curviness + if max_height == 0.0: + heightmap = np.zeros(num_edges) + max_height = 1.0 + else: + heightmap = (ruggedness * rugged + curviness * bowl) / (ruggedness + curviness) + + return Terrain( + static_geometry=[ + GeometryHeightmap( + pose=Pose(), + mass=0.0, + size=Vector3([size[0], size[1], max_height]), + base_thickness=0.1 + ruggedness, + heights=heightmap, + ) + ] + ) + + +def rugged_heightmap( + size: tuple[float, float], + num_edges: tuple[int, int], + density: float = 1.0, +) -> npt.NDArray[np.float_]: + """ + Create a rugged terrain heightmap. + + It will look like:: + + ..^.__,^._.-. + + Be aware: the maximum height of the heightmap is not actually 1. + It is around [-1,1] but not exactly. + + :param size: Size of the heightmap. + :param num_edges: How many edges to use for the heightmap. + :param density: How coarse the ruggedness is. + :returns: The created heightmap as a 2 dimensional array. + """ + OCTAVE = 10 + C1 = 4.0 # arbitrary constant to get nice noise + + return np.fromfunction( + np.vectorize( + lambda y, x: pnoise2( + x / num_edges[0] * C1 * size[0] * density, + y / num_edges[1] * C1 * size[1] * density, + OCTAVE, + ), + otypes=[float], + ), + num_edges, + dtype=float, + ) + + +def bowl_heightmap( + num_edges: tuple[int, int], +) -> npt.NDArray[np.float_]: + r""" + Create a terrain heightmap in the shape of a bowl. + + It will look like:: + + | | + \ / + '.___.' + + The height of the edges of the bowl is 1.0 and the center is 0.0. + + :param num_edges: How many edges to use for the heightmap. + :returns: The created heightmap as a 2 dimensional array. + """ + return np.fromfunction( + np.vectorize( + lambda y, x: ( + (x / num_edges[0] * 2.0 - 1.0) ** 2 + + (y / num_edges[1] * 2.0 - 1.0) ** 2 + if math.sqrt( + (x / num_edges[0] * 2.0 - 1.0) ** 2 + + (y / num_edges[1] * 2.0 - 1.0) ** 2 + ) + <= 1.0 + else 0.0 + ), + otypes=[float], + ), + num_edges, + dtype=float, + ) diff --git a/gui/viewer/main_window.py b/gui/viewer/main_window.py new file mode 100644 index 000000000..772adca07 --- /dev/null +++ b/gui/viewer/main_window.py @@ -0,0 +1,317 @@ +from PyQt5.QtWidgets import ( + QApplication, QMainWindow, QTabWidget, + QWidget, QVBoxLayout, QLabel, + QComboBox, QPushButton, QMessageBox, + QLineEdit, QHBoxLayout, QButtonGroup, + QStackedWidget) +from parsing import get_functions_from_file, get_function_names_from_init, get_config_parameters_from_file, save_config_parameters +import subprocess +import os + +class RobotEvolutionGUI(QMainWindow): + def __init__(self): + super().__init__() + self.setWindowTitle("Robot Evolution System") + self.setGeometry(100, 100, 800, 600) + + self.simulation_process = None + + self.tab_widget = QTabWidget(self) + self.setCentralWidget(self.tab_widget) + + self.fitness_functions = get_functions_from_file("../backend_example/fitness_functions.py") + + self.terrains = get_functions_from_file("../backend_example/terrains.py") + + self.path_simulation_parameters = "../backend_example/config_simulation_parameters.py" + self.simulation_parameters = get_config_parameters_from_file(self.path_simulation_parameters) + + self.path_evolution_parameters = "../backend_example/config.py" + self.evolution_parameters = get_config_parameters_from_file(self.path_evolution_parameters) + self.is_generational = self.evolution_parameters.get("GENERATIONAL") + + self.selection_path = "/home/aronf/Desktop/EvolutionaryComputing/work/revolve2/experimentation/revolve2/experimentation/optimization/ea/selection/" + self.selection_functions = get_function_names_from_init(self.selection_path) + + # Step 1: Define Robot Phenotypes + self.tab_widget.addTab(self.create_phenotype_tab(), "Robot Phenotypes") + + # Step 2: Define Environment + self.tab_widget.addTab(self.create_environment_tab(), "Environment & Task") + + # Step 3: Define Genotypes + self.tab_widget.addTab(self.create_genotype_tab(), "Robot Genotypes") + + # Step 4: Fitness Function + self.tab_widget.addTab(self.create_fitness_tab(), "Fitness Function") + + # Step 5: Evolution Parameters + self.tab_widget.addTab(self.create_ea_tab(), "Evolutionary Algorithm") + + # Step 6: Selection + self.tab_widget.addTab(self.create_selection_tab(), "Selection Algorithms") + + # Step 7: Simulator Selection + self.tab_widget.addTab(self.create_simulation_parameters_tab(), "Physics Simulator") + + # Step 8: Run Simulation + self.tab_widget.addTab(self.create_run_simulation_tab(), "Run Simulation") + + # Step 9: Plot Results + self.tab_widget.addTab(self.create_plot_tab(), "Plot Results") + + + def run_simulation(self): + # Run the simulation + self.simulation_process = subprocess.Popen(["python", "../backend_example/main_from_gui.py"]) + + def stop_simulation(self): + """Stop the running simulation.""" + if self.simulation_process and self.simulation_process.poll() is None: + self.simulation_process.terminate() + self.simulation_process.wait() + print("Simulation stopped successfully.") + else: + print("No active simulation to stop.") + + def plot_results(self): + # Run the plot script + subprocess.Popen(["python", "../backend_example/plot.py"]) + + def save_config_changes(self, file_path, inputs): + """Update a config with new values from the GUI.""" + new_values = {} + for key, input_field in inputs.items(): + if type(input_field) == bool: + new_values[key] = input_field + continue + else: + text_value = input_field.text() + + try: + new_values[key] = eval(text_value) # Careful with eval in untrusted inputs! + except: + new_values[key] = text_value + + save_config_parameters(file_path, new_values) + + QMessageBox.information(self, "Success", "Config file updated!") + + def create_phenotype_tab(self): + widget = QWidget() + layout = QVBoxLayout() + layout.addWidget(QLabel("Define Robot Phenotypes")) + # Dropdown for phenotypes + phenotypes_dropdown = QComboBox() + phenotypes_dropdown.addItems(["Phenotype A", "Phenotype B", "Phenotype C"]) + layout.addWidget(phenotypes_dropdown) + widget.setLayout(layout) + return widget + + def create_genotype_tab(self): + widget = QWidget() + layout = QVBoxLayout() + layout.addWidget(QLabel("UNDER CONSTRUCTION:\n Define Mutation and Crossover Operators")) + # Mutation operator + mutation_dropdown = QComboBox() + mutation_dropdown.addItems(["Operator A", "Operator B", "Operator C"]) + layout.addWidget(QLabel("Mutation Operator:")) + layout.addWidget(mutation_dropdown) + # Crossover operator + crossover_dropdown = QComboBox() + crossover_dropdown.addItems(["Operator X", "Operator Y", "Operator Z"]) + layout.addWidget(QLabel("Crossover Operator:")) + layout.addWidget(crossover_dropdown) + widget.setLayout(layout) + return widget + + def create_selection_tab(self): + widget = QWidget() + layout = QVBoxLayout() + layout.addWidget(QLabel("Define Parent and Surivor Selection Types")) + parent_dropdown = QComboBox() + parent_dropdown.addItems(self.selection_functions) + layout.addWidget(QLabel("Parent Selection: ")) + layout.addWidget(parent_dropdown) + # Crossover operator + survivor_dropdown = QComboBox() + survivor_dropdown.addItems(self.selection_functions) + layout.addWidget(QLabel("Survivor Selection:")) + layout.addWidget(survivor_dropdown) + widget.setLayout(layout) + return widget + + def create_ea_tab(self): + widget = QWidget() + layout = QVBoxLayout() + + # Stacked widget for toggling between views + self.stacked_widget = QStackedWidget() + + # Generational View + self.view1 = QWidget() + v1_layout = QVBoxLayout() + v1_layout.addWidget(QLabel("Generational GA Parameters")) + self.view1.setLayout(v1_layout) + + # Steady-State View + self.view2 = QWidget() + v2_layout = QVBoxLayout() + v2_layout.addWidget(QLabel("Steady-State GA Parameters")) + self.view2.setLayout(v2_layout) + + self.inputs_evolution = {} + + # Add parameter fields for generational views + for key, value in self.evolution_parameters.items(): + input_layout = QHBoxLayout() + input_label = QLabel(f"{key}:") + input_field = QLineEdit(str(value)) + self.inputs_evolution[key] = input_field + if key in ["GENERATIONAL", "STEADY_STATE"]: + self.inputs_evolution["GENERATIONAL"] = True + self.inputs_evolution["STEADY_STATE"] = False + continue + else: + input_layout.addWidget(input_label) + input_layout.addWidget(input_field) + v1_layout.addLayout(input_layout) + + # Add parameter fields for steady_state view + for key, value in self.evolution_parameters.items(): + input_layout = QHBoxLayout() + input_label = QLabel(f"{key}:") + input_field = QLineEdit(str(value)) + self.inputs_evolution[key] = input_field + if key in ["GENERATIONAL", "STEADY_STATE"]: + self.inputs_evolution["GENERATIONAL"] = False + self.inputs_evolution["STEADY_STATE"] = True + continue + else: + input_layout.addWidget(input_label) + input_layout.addWidget(input_field) + v2_layout.addLayout(input_layout) + + self.stacked_widget.addWidget(self.view1) + self.stacked_widget.addWidget(self.view2) + + # Button to switch modes + self.switch_button = QPushButton("") + self.switch_button.clicked.connect(self.toggle_view) + layout.addWidget(self.switch_button) + layout.addWidget(self.stacked_widget) + + # Save Button + save_button = QPushButton("Save Changes") + save_button.clicked.connect(lambda: self.save_config_changes(self.path_evolution_parameters, self.inputs_evolution)) + layout.addWidget(save_button) + + widget.setLayout(layout) + + # Set initial state + self.update_view() + + return widget + + def toggle_view(self): + """Toggle between Generational and Steady-State mode.""" + self.is_generational = not self.is_generational + + # Save the updated mode to config + self.inputs_evolution["GENERATIONAL"] = self.is_generational + self.inputs_evolution["STEADY_STATE"] = not self.is_generational + + # Update UI + self.update_view() + + def update_view(self): + """Update the UI based on the current mode.""" + if self.is_generational: + self.stacked_widget.setCurrentWidget(self.view1) + self.switch_button.setText("Switch to Steady-State") + else: + self.stacked_widget.setCurrentWidget(self.view2) + self.switch_button.setText("Switch to Generational") + + def create_environment_tab(self): + widget = QWidget() + layout = QVBoxLayout() + layout.addWidget(QLabel("Define Environment and Task")) + # Environment settings + layout.addWidget(QLabel("Environment Terrains: ")) + environment_dropdown = QComboBox() # Example: Environment parameter input + environment_dropdown.addItems(self.terrains.keys()) + layout.addWidget(environment_dropdown) + widget.setLayout(layout) + layout + # Task selection + task_dropdown = QComboBox() + task_dropdown.addItems(["Task 1", "Task 2", "Task 3"]) + layout.addWidget(QLabel("Target Task:")) + layout.addWidget(task_dropdown) + widget.setLayout(layout) + return widget + + def create_fitness_tab(self): + widget = QWidget() + layout = QVBoxLayout() + layout.addWidget(QLabel("Define Fitness Function")) + fitness_dropdown = QComboBox() + fitness_dropdown.addItems(self.fitness_functions.keys()) + layout.addWidget(fitness_dropdown) + widget.setLayout(layout) + return widget + + def create_simulation_parameters_tab(self): + widget = QWidget() + layout = QVBoxLayout() + layout.addWidget(QLabel("Edit Simulation Parameters")) + self.inputs_simulation = {} + for key, value in self.simulation_parameters.items(): + input_layout = QHBoxLayout() + input_label = QLabel(f"{key}:") + input_field = QLineEdit(str(value)) + self.inputs_simulation[key] = input_field + input_layout.addWidget(input_label) + input_layout.addWidget(input_field) + layout.addLayout(input_layout) + + save_button = QPushButton("Save Changes") + save_button.clicked.connect(lambda: self.save_config_changes(self.path_simulation_parameters, self.inputs_simulation)) + + layout.addWidget(save_button) + widget.setLayout(layout) + return widget + + def create_run_simulation_tab(self): + widget = QWidget() + layout = QVBoxLayout() + layout.addWidget(QLabel("Run Simulation")) + run_button = QPushButton("Run Simulation") + run_button.clicked.connect(self.run_simulation) + layout.addWidget(run_button) + widget.setLayout(layout) + + layout.addWidget(QLabel("Stop Simulation")) + stop_button = QPushButton("Stop Simulation") + stop_button.clicked.connect(self.stop_simulation) + layout.addWidget(stop_button) + widget.setLayout(layout) + return widget + + def create_plot_tab(self): + widget = QWidget() + layout = QVBoxLayout() + layout.addWidget(QLabel("Plot Results")) + plot_button = QPushButton("Plot Results") + plot_button.clicked.connect(self.plot_results) + layout.addWidget(plot_button) + widget.setLayout(layout) + return widget + +if __name__ == "__main__": + import sys + app = QApplication(sys.argv) + window = RobotEvolutionGUI() + window.show() + sys.exit(app.exec_()) diff --git a/gui/viewer/parameters.py b/gui/viewer/parameters.py new file mode 100644 index 000000000..e69de29bb diff --git a/gui/viewer/parsing.py b/gui/viewer/parsing.py new file mode 100644 index 000000000..2cbc67e15 --- /dev/null +++ b/gui/viewer/parsing.py @@ -0,0 +1,63 @@ +import importlib.util +import inspect +import os + + +def get_functions_from_file(file_path): + """ + Load a Python file and extract all functions defined in it. + """ + if not os.path.exists(file_path): + raise FileNotFoundError(f"File not found: {file_path}") + + module_name = os.path.splitext(os.path.basename(file_path))[0] + spec = importlib.util.spec_from_file_location(module_name, file_path) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + + # Extract all functions + functions = { + name: func + for name, func in inspect.getmembers(module, inspect.isfunction) + } + return functions + +def get_function_names_from_init(folder_path): + """ + Import the __init__.py file from the given folder and extract the function names listed in the __all__ variable. + """ + init_file = os.path.join(folder_path, "__init__.py") + if not os.path.exists(init_file): + raise FileNotFoundError(f"__init__.py not found in folder: {folder_path}") + + # Dynamically load the __init__.py file + module_name = os.path.basename(folder_path) # Use folder name as module name + spec = importlib.util.spec_from_file_location(module_name, init_file) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + + # Extract function names from __all__ list in __init__.py + if hasattr(module, "__all__"): + module.__all__.remove('multiple_unique') + return module.__all__ + else: + raise AttributeError(f"__all__ not found in {init_file}") + +def get_config_parameters_from_file(file_path): + """Dynamically load variables from a config file as a dictionary.""" + if not os.path.exists(file_path): + with open(file_path, "w") as f: + f.write("# Default config file\n") + spec = importlib.util.spec_from_file_location("config", file_path) + config = importlib.util.module_from_spec(spec) + spec.loader.exec_module(config) + return {key: getattr(config, key) for key in dir(config) if (not key.startswith("__"))} + +def save_config_parameters(file_path, values): + """Save the modified values back to a config file.""" + with open(file_path, "w") as f: + for key, value in values.items(): + if isinstance(value, str): + f.write(f'{key} = "{value}"\n') + else: + f.write(f"{key} = {value}\n") diff --git a/gui/viewer/results_viewer.py b/gui/viewer/results_viewer.py new file mode 100644 index 000000000..e69de29bb diff --git a/gui/viewer/styles.qss b/gui/viewer/styles.qss new file mode 100644 index 000000000..e69de29bb diff --git a/modular_robot/pyproject.toml b/modular_robot/pyproject.toml index bd78aa0b1..0e61199d2 100644 --- a/modular_robot/pyproject.toml +++ b/modular_robot/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "revolve2-modular-robot" -version = "1.2.3" +version = "1.2.2" description = "Revolve2: Everything for defining modular robots." readme = "../README.md" authors = [ diff --git a/modular_robot/revolve2/modular_robot/body/_module.py b/modular_robot/revolve2/modular_robot/body/_module.py index 90ed8ab23..7cdaf5b72 100644 --- a/modular_robot/revolve2/modular_robot/body/_module.py +++ b/modular_robot/revolve2/modular_robot/body/_module.py @@ -225,6 +225,7 @@ def can_set_child(self, child_index: int) -> bool: if self._children.get(child_index, True): return True return False + # return child_index not in self._children or not self._children[child_index] def neighbours(self, within_range: int) -> list[Module]: """ diff --git a/modular_robot/revolve2/modular_robot/body/v2/_active_hinge_v2.py b/modular_robot/revolve2/modular_robot/body/v2/_active_hinge_v2.py index 307119369..f061e6d76 100644 --- a/modular_robot/revolve2/modular_robot/body/v2/_active_hinge_v2.py +++ b/modular_robot/revolve2/modular_robot/body/v2/_active_hinge_v2.py @@ -23,21 +23,21 @@ def __init__(self, rotation: float | RightAngles): range=1.047197551, effort=0.948013269, velocity=6.338968228, - frame_bounding_box=Vector3([0.018, 0.052, 0.0165891]), - frame_offset=0.04495, - servo1_bounding_box=Vector3([0.05125, 0.0512, 0.020]), - servo2_bounding_box=Vector3([0.002, 0.052, 0.052]), + frame_bounding_box=Vector3([0.018, 0.053, 0.0165891]), + frame_offset=0.04525, + servo1_bounding_box=Vector3([0.0583, 0.0512, 0.020]), + servo2_bounding_box=Vector3([0.002, 0.053, 0.053]), frame_mass=0.01632, servo1_mass=0.058, servo2_mass=0.025, - servo_offset=0.0239, + servo_offset=0.0299, joint_offset=0.0119, static_friction=1.0, dynamic_friction=1.0, armature=0.002, pid_gain_p=5.0, pid_gain_d=0.05, - child_offset=0.05125 / 2 + 0.002 + 0.01, + child_offset=0.0583 / 2 + 0.002, sensors=[ ActiveHingeSensor() ], # By default, V2 robots have ActiveHinge sensors, since the hardware also supports them natively. diff --git a/modular_robot/revolve2/modular_robot/body/v2/_brick_v2.py b/modular_robot/revolve2/modular_robot/body/v2/_brick_v2.py index c8588ee7c..6990e420b 100644 --- a/modular_robot/revolve2/modular_robot/body/v2/_brick_v2.py +++ b/modular_robot/revolve2/modular_robot/body/v2/_brick_v2.py @@ -13,11 +13,10 @@ def __init__(self, rotation: float | RightAngles): :param rotation: The modules' rotation. """ - w, h, d = 0.075, 0.075, 0.075 super().__init__( rotation=rotation, - bounding_box=Vector3([w, h, d]), + bounding_box=Vector3([0.06288625, 0.06288625, 0.0603]), mass=0.06043, - child_offset=d / 2.0, + child_offset=0.06288625 / 2.0, sensors=[], ) diff --git a/modular_robot/revolve2/modular_robot/brain/cpg/_cpg_network_structure.py b/modular_robot/revolve2/modular_robot/brain/cpg/_cpg_network_structure.py index bb808fbac..8103fb009 100644 --- a/modular_robot/revolve2/modular_robot/brain/cpg/_cpg_network_structure.py +++ b/modular_robot/revolve2/modular_robot/brain/cpg/_cpg_network_structure.py @@ -156,10 +156,7 @@ def make_uniform_state(self, value: float) -> npt.NDArray[np.float_]: :param value: The value to use for all states :returns: The array of states. """ - _half = self.num_states // 2 - mask = np.hstack([np.full(_half, 1), np.full(_half, -1)]) - - return mask * value + return np.full(self.num_states, value) @property def num_cpgs(self) -> int: diff --git a/modular_robot_physical/pyproject.toml b/modular_robot_physical/pyproject.toml index 2229334c4..6adc977a7 100644 --- a/modular_robot_physical/pyproject.toml +++ b/modular_robot_physical/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "revolve2-modular-robot-physical" -version = "1.2.3" +version = "1.2.2" description = "Revolve2: Everything for physical modular robot control. This package is intended to be installed on the modular robot hardware." readme = "../README.md" authors = [ @@ -27,14 +27,13 @@ packages = [{ include = "revolve2" }] [tool.poetry.dependencies] python = "^3.10,<3.12" -revolve2-modular-robot = "1.2.3" +revolve2-modular-robot = "1.2.2" pyrr = "^0.10.3" typed-argparse = "^0.3.1" pycapnp = { version = "^2.0.0b2" } pigpio = { version = "^1.78", optional = true } -revolve2-robohat = { version = "0.6.3", optional = true } +revolve2-robohat = { version = "0.5.0", optional = true } rpi-lgpio = { version = "0.5", optional = true } -opencv-python = "^4.10.0.84" # cpnp-stub-generator is disabled because it depends on pycapnp <2.0.0. # It is rarely used and by developers only, so we remove it for now and developers can install it manually. # If you manually install it make sure you also install the correct pycpanp version afterwards as it will be overridden. diff --git a/modular_robot_physical/revolve2/modular_robot_physical/physical_interfaces/_physical_interface.py b/modular_robot_physical/revolve2/modular_robot_physical/physical_interfaces/_physical_interface.py index 937cda4ed..bbc9f5d16 100644 --- a/modular_robot_physical/revolve2/modular_robot_physical/physical_interfaces/_physical_interface.py +++ b/modular_robot_physical/revolve2/modular_robot_physical/physical_interfaces/_physical_interface.py @@ -1,5 +1,5 @@ from abc import ABC, abstractmethod -from typing import Optional, Sequence +from typing import Sequence import numpy as np from numpy.typing import NDArray @@ -79,7 +79,7 @@ def get_imu_specific_force(self) -> Vector3: """ @abstractmethod - def get_camera_view(self) -> Optional[NDArray[np.uint8]]: + def get_camera_view(self) -> NDArray[np.uint8]: """ Get the current view from the camera. diff --git a/modular_robot_physical/revolve2/modular_robot_physical/physical_interfaces/v2/_v2_physical_interface.py b/modular_robot_physical/revolve2/modular_robot_physical/physical_interfaces/v2/_v2_physical_interface.py index c462e6c28..5f73f5923 100644 --- a/modular_robot_physical/revolve2/modular_robot_physical/physical_interfaces/v2/_v2_physical_interface.py +++ b/modular_robot_physical/revolve2/modular_robot_physical/physical_interfaces/v2/_v2_physical_interface.py @@ -1,13 +1,13 @@ import math -from typing import Optional, Sequence +from typing import Sequence +import cv2 import numpy as np from numpy.typing import NDArray from pyrr import Vector3 from robohatlib.hal.assemblyboard.PwmPlug import PwmPlug from robohatlib.hal.assemblyboard.servo.ServoData import ServoData from robohatlib.hal.assemblyboard.ServoAssemblyConfig import ServoAssemblyConfig -from robohatlib.hal.Camera import Camera from robohatlib.Robohat import Robohat from .._physical_interface import PhysicalInterface @@ -84,7 +84,6 @@ def __init__(self, debug: bool, dry: bool) -> None: self._robohat.init(servoboard_1_datas_list, servoboard_2_datas_list) self._robohat.do_buzzer_beep() self._robohat.set_servo_direct_mode(_mode=True) - self.cam: Camera = self._robohat.get_camera() def set_servo_targets(self, pins: list[int], targets: list[float]) -> None: """ @@ -178,18 +177,21 @@ def get_imu_specific_force(self) -> Vector3: raise RuntimeError("Could not get IMU acceleration reading!") return Vector3(accel) - def get_camera_view(self) -> Optional[NDArray[np.uint8]]: + def get_camera_view(self) -> NDArray[np.uint8]: """ Get the current view from the camera. - :returns: An image captured from robohatlib. - """ - try: - image = self.cam.get_capture_array() - if image is None: - print("No image captured (camera may not be available).") - return None - return image.astype(np.uint8) - except RuntimeError as e: - print(f"Runtime error encountered: {e}") - return None + :returns: A dummy image until robohatlib has camera support. + """ + image = np.zeros((3, 100, 100), dtype=int) + cv2.putText( + image, + "Dummy Image", + (10, 10), + fontFace=cv2.FONT_HERSHEY_SIMPLEX, + fontScale=1, + color=(255, 0, 0), + thickness=1, + lineType=2, + ) + return image diff --git a/modular_robot_physical/revolve2/modular_robot_physical/remote/_remote.py b/modular_robot_physical/revolve2/modular_robot_physical/remote/_remote.py index 1af6be7d4..6bbf0feca 100644 --- a/modular_robot_physical/revolve2/modular_robot_physical/remote/_remote.py +++ b/modular_robot_physical/revolve2/modular_robot_physical/remote/_remote.py @@ -3,7 +3,6 @@ from typing import Callable import capnp -import cv2 import numpy as np from numpy.typing import NDArray from pyrr import Vector3 @@ -51,7 +50,6 @@ async def _run_remote_impl( port: int, debug: bool, manual_mode: bool, - display_camera_view: bool, ) -> None: active_hinge_sensor_to_pin = { UUIDKey(key.value.sensors.active_hinge_sensor): pin @@ -209,7 +207,6 @@ async def _run_remote_impl( pin_controls = _active_hinge_targets_to_pin_controls( config, control_interface._set_active_hinges ) - match hardware_type: case HardwareType.v1: await service.control( @@ -245,13 +242,6 @@ async def _run_remote_impl( camera_sensor_states=camera_sensor_states, ) - # Display camera image - if display_camera_view: - _display_camera_view( - config.modular_robot.body.core.sensors.camera_sensor, - sensor_readings, - ) - if battery_print_timer > 5.0: print( f"Battery level is at {sensor_readings.battery * 100.0}%." @@ -275,17 +265,11 @@ def _capnp_to_camera_view( :param camera_size: The camera size to reconstruct the image. :return: The NDArray imag. """ - r_channel = np.array(image.r, dtype=np.uint8).reshape( - (camera_size[0], camera_size[1]) - ) - g_channel = np.array(image.g, dtype=np.uint8).reshape( - (camera_size[0], camera_size[1]) - ) - b_channel = np.array(image.b, dtype=np.uint8).reshape( - (camera_size[0], camera_size[1]) - ) - rgb_image = cv2.merge((r_channel, g_channel, b_channel)).astype(np.uint8) - return rgb_image + np_image = np.zeros(shape=(3, *camera_size), dtype=np.uint8) + np_image[0] = np.array(image.r).reshape(camera_size).astype(np.uint8) + np_image[1] = np.array(image.g).reshape(camera_size).astype(np.uint8) + np_image[2] = np.array(image.b).reshape(camera_size).astype(np.uint8) + return np_image def _get_imu_sensor_state( @@ -321,17 +305,10 @@ def _get_camera_sensor_state( :param camera_sensor: The sensor in question. :param sensor_readings: The sensor readings. :return: The Sensor state. - :raises RuntimeError: If the camera image is empty. """ if camera_sensor is None: return {} else: - image = sensor_readings.cameraView - if len(image.r) == 0 and len(image.g) == 0 and len(image.b) == 0: - raise RuntimeError( - "Camera image is empty. Are you sure you have attached a camera? " - "If you don't want to get the camera state, don't add it to the body." - ) return { UUIDKey(camera_sensor): CameraSensorStateImpl( _capnp_to_camera_view( @@ -341,32 +318,6 @@ def _get_camera_sensor_state( } -def _display_camera_view( - camera_sensor: CameraSensor | None, - sensor_readings: robot_daemon_protocol_capnp.SensorReadings, -) -> None: - """ - Display a camera view from the camera readings. - - :param camera_sensor: The sensor in question. - :param sensor_readings: The sensor readings. - :raises RuntimeError: If the camera image is empty. - """ - if camera_sensor is None: - raise RuntimeError( - "Can't display camera because there is no camera added in the body" - ) - else: - image = sensor_readings.cameraView - if len(image.r) == 0 and len(image.g) == 0 and len(image.b) == 0: - raise RuntimeError("Image is emtpy so nothing can be displayed") - rgb_image = _capnp_to_camera_view( - sensor_readings.cameraView, camera_sensor.camera_size - ) - cv2.imshow("Captured Image", rgb_image) - cv2.waitKey(1) - - def run_remote( config: Config, hostname: str, @@ -374,7 +325,6 @@ def run_remote( port: int = STANDARD_PORT, debug: bool = False, manual_mode: bool = False, - display_camera_view: bool = False, ) -> None: """ Control a robot remotely, running the controller on your local machine. @@ -385,7 +335,6 @@ def run_remote( :param port: Port the robot daemon uses. :param debug: Enable debug messages. :param manual_mode: Enable manual controls for the robot, ignoring the brain. - :param display_camera_view: Display the camera view of the robot. """ asyncio.run( capnp.run( @@ -396,7 +345,6 @@ def run_remote( port=port, debug=debug, manual_mode=manual_mode, - display_camera_view=display_camera_view, ) ) ) diff --git a/modular_robot_physical/revolve2/modular_robot_physical/robot_daemon/_robo_server_impl.py b/modular_robot_physical/revolve2/modular_robot_physical/robot_daemon/_robo_server_impl.py index 0f33f6774..39e8ab6b8 100644 --- a/modular_robot_physical/revolve2/modular_robot_physical/robot_daemon/_robo_server_impl.py +++ b/modular_robot_physical/revolve2/modular_robot_physical/robot_daemon/_robo_server_impl.py @@ -256,6 +256,7 @@ def _get_sensor_readings( pins_readings.append(value) battery = self._physical_interface.get_battery_level() + imu_orientation = self._physical_interface.get_imu_orientation() imu_specific_force = self._physical_interface.get_imu_specific_force() imu_angular_rate = self._physical_interface.get_imu_angular_rate() @@ -283,7 +284,7 @@ def _vector3_to_capnp(vector: Vector3) -> capnpVector3: ) @staticmethod - def _camera_view_to_capnp(image: NDArray[np.uint8] | None) -> capnpImage: + def _camera_view_to_capnp(image: NDArray[np.uint8]) -> capnpImage: """ Convert an image as an NDArray into an capnp compatible Image. @@ -292,16 +293,8 @@ def _camera_view_to_capnp(image: NDArray[np.uint8] | None) -> capnpImage: :param image: The NDArray image. :return: The capnp Image object. """ - # Convert each channel to a list of Int32 for Cap'n Proto - if image is None: - return robot_daemon_protocol_capnp.Image( - r=[], - g=[], - b=[], - ) - else: - return robot_daemon_protocol_capnp.Image( - r=image[:, :, 0].astype(np.int32).flatten().tolist(), - g=image[:, :, 1].astype(np.int32).flatten().tolist(), - b=image[:, :, 2].astype(np.int32).flatten().tolist(), - ) + return robot_daemon_protocol_capnp.Image( + r=image[0].flatten().tolist(), + g=image[1].flatten().tolist(), + b=image[2].flatten().tolist(), + ) diff --git a/modular_robot_simulation/pyproject.toml b/modular_robot_simulation/pyproject.toml index de4b726d3..1cc03c4b8 100644 --- a/modular_robot_simulation/pyproject.toml +++ b/modular_robot_simulation/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "revolve2-modular-robot-simulation" -version = "1.2.3" +version = "1.2.2" description = "Revolve2: Functionality to define scenes with modular robots in a terrain and simulate them." readme = "../README.md" authors = [ @@ -27,8 +27,8 @@ packages = [{ include = "revolve2" }] [tool.poetry.dependencies] python = "^3.10,<3.12" -revolve2-modular-robot = "1.2.3" -revolve2-simulation = "1.2.3" +revolve2-modular-robot = "1.2.2" +revolve2-simulation = "1.2.2" [tool.poetry.extras] dev = [] diff --git a/project.yml b/project.yml index f79ddd3cf..5bfcc86ba 100644 --- a/project.yml +++ b/project.yml @@ -24,7 +24,6 @@ examples: - 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 - - 4_example_experiment_setups/4g_explore_initial_population - 5_physical_modular_robots/5a_physical_robot_remote - 5_physical_modular_robots/5b_compare_simulated_and_physical_robot tests-dir: tests diff --git a/requirements_dev.txt b/requirements_dev.txt index 7feb074c6..5c453d647 100644 --- a/requirements_dev.txt +++ b/requirements_dev.txt @@ -12,5 +12,4 @@ -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 ./examples/4_example_experiment_setups/4g_explore_initial_population/requirements.txt -r ./tests/requirements.txt diff --git a/requirements_editable.txt b/requirements_editable.txt index d9b9ddeda..cec14ab6c 100644 --- a/requirements_editable.txt +++ b/requirements_editable.txt @@ -10,5 +10,3 @@ -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 ./examples/4_example_experiment_setups/4g_explore_initial_population/requirements.txt - diff --git a/simulation/pyproject.toml b/simulation/pyproject.toml index e8a72bf47..5400f337c 100644 --- a/simulation/pyproject.toml +++ b/simulation/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "revolve2-simulation" -version = "1.2.3" +version = "1.2.2" description = "Revolve2: Physics simulation abstraction layer." readme = "../README.md" authors = [ diff --git a/simulators/mujoco_simulator/pyproject.toml b/simulators/mujoco_simulator/pyproject.toml index f29b30dbc..b32fca2c4 100644 --- a/simulators/mujoco_simulator/pyproject.toml +++ b/simulators/mujoco_simulator/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "revolve2-mujoco-simulator" -version = "1.2.3" +version = "1.2.2" description = "Revolve2: MuJoCo simulator." readme = "../../README.md" authors = [ @@ -28,7 +28,7 @@ packages = [{ include = "revolve2" }] [tool.poetry.dependencies] python = "^3.10,<3.12" -revolve2-simulation = "1.2.3" +revolve2-simulation = "1.2.2" mujoco-python-viewer = "^0.1.3" mujoco = "^2.2.0" dm-control = "^1.0.3" diff --git a/simulators/mujoco_simulator/revolve2/simulators/mujoco_simulator/_control_interface_impl.py b/simulators/mujoco_simulator/revolve2/simulators/mujoco_simulator/_control_interface_impl.py index fd4f1a4fe..a63808a1c 100644 --- a/simulators/mujoco_simulator/revolve2/simulators/mujoco_simulator/_control_interface_impl.py +++ b/simulators/mujoco_simulator/revolve2/simulators/mujoco_simulator/_control_interface_impl.py @@ -40,9 +40,7 @@ def set_joint_hinge_position_target( assert ( maybe_hinge_joint_mujoco is not None ), "Hinge joint does not exist in this scene." - # Set position target self._data.ctrl[maybe_hinge_joint_mujoco.ctrl_index_position] = position - # Set velocity target self._data.ctrl[maybe_hinge_joint_mujoco.ctrl_index_velocity] = 0.0 diff --git a/simulators/mujoco_simulator/revolve2/simulators/mujoco_simulator/_simulate_scene.py b/simulators/mujoco_simulator/revolve2/simulators/mujoco_simulator/_simulate_scene.py index 8c86f573e..a701515a6 100644 --- a/simulators/mujoco_simulator/revolve2/simulators/mujoco_simulator/_simulate_scene.py +++ b/simulators/mujoco_simulator/revolve2/simulators/mujoco_simulator/_simulate_scene.py @@ -179,11 +179,7 @@ def simulate_scene( if not headless or ( record_settings is not None and time >= last_video_time + video_step ): - _status = viewer.render() - - # Check if simulation was closed - if _status == -1: - break + viewer.render() # capture video frame if it's time if record_settings is not None and time >= last_video_time + video_step: diff --git a/simulators/mujoco_simulator/revolve2/simulators/mujoco_simulator/viewers/_custom_mujoco_viewer.py b/simulators/mujoco_simulator/revolve2/simulators/mujoco_simulator/viewers/_custom_mujoco_viewer.py index a9b7a1a48..82fd6a243 100755 --- a/simulators/mujoco_simulator/revolve2/simulators/mujoco_simulator/viewers/_custom_mujoco_viewer.py +++ b/simulators/mujoco_simulator/revolve2/simulators/mujoco_simulator/viewers/_custom_mujoco_viewer.py @@ -80,10 +80,7 @@ def render(self) -> int | None: :return: A cycle position if applicable. """ - if self.is_alive: - super().render() - else: - return -1 + super().render() if self._viewer_mode == CustomMujocoViewerMode.MANUAL: return self._position return None diff --git a/standards/pyproject.toml b/standards/pyproject.toml index 7b137e0c5..b66ac907b 100644 --- a/standards/pyproject.toml +++ b/standards/pyproject.toml @@ -9,7 +9,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "revolve2-standards" -version = "1.2.3" +version = "1.2.2" description = "Revolve2: Standard tools, parameters, terrains, robots and more for simulations and experiments." readme = "../README.md" authors = [ @@ -38,7 +38,7 @@ script = "revolve2/standards/morphological_novelty_metric/_build_cmodule.py" [tool.poetry.dependencies] python = "^3.10,<3.12" -revolve2-modular-robot-simulation = "1.2.3" +revolve2-modular-robot-simulation = "1.2.2" noise = "^1.2.2" multineat = "^0.12" sqlalchemy = "^2.0.0" diff --git a/standards/revolve2/standards/genotypes/cppnwin/_random_multineat_genotype.py b/standards/revolve2/standards/genotypes/cppnwin/_random_multineat_genotype.py index 306dadcdb..ac405f74b 100644 --- a/standards/revolve2/standards/genotypes/cppnwin/_random_multineat_genotype.py +++ b/standards/revolve2/standards/genotypes/cppnwin/_random_multineat_genotype.py @@ -33,7 +33,7 @@ def random_multineat_genotype( False, # FS_NEAT output_activation_func, # output activation type multineat.ActivationFunction.UNSIGNED_SIGMOID, # hidden activation type - 0, # seed_type + 1, # seed_type multineat_params, 1, # number of hidden layers ) diff --git a/standards/revolve2/standards/genotypes/cppnwin/modular_robot/_brain_cpg_network_neighbor.py b/standards/revolve2/standards/genotypes/cppnwin/modular_robot/_brain_cpg_network_neighbor.py index 7eb45089d..a6b4e3487 100644 --- a/standards/revolve2/standards/genotypes/cppnwin/modular_robot/_brain_cpg_network_neighbor.py +++ b/standards/revolve2/standards/genotypes/cppnwin/modular_robot/_brain_cpg_network_neighbor.py @@ -36,7 +36,7 @@ def _make_weights( body: Body, ) -> tuple[list[float], list[float]]: brain_net = multineat.NeuralNetwork() - self._genotype.BuildPhenotype(brain_net) + self._genotype.BuildCPPN(brain_net) internal_weights = [ self._evaluate_network( diff --git a/standards/revolve2/standards/genotypes/cppnwin/modular_robot/v2/_body_develop.py b/standards/revolve2/standards/genotypes/cppnwin/modular_robot/v2/_body_develop.py index f65a92c96..7fb196f13 100644 --- a/standards/revolve2/standards/genotypes/cppnwin/modular_robot/v2/_body_develop.py +++ b/standards/revolve2/standards/genotypes/cppnwin/modular_robot/v2/_body_develop.py @@ -38,7 +38,7 @@ def develop( body_net = ( multineat.NeuralNetwork() ) # Instantiate the CPPN network for body construction. - genotype.BuildPhenotype(body_net) # Build the CPPN from the genotype of the robot. + genotype.BuildCPPN(body_net) # Build the CPPN from the genotype of the robot. to_explore: Queue[__Module] = Queue() # Queue used to build the robot. grid = np.zeros( shape=(max_parts * 2 + 1, max_parts * 2 + 1, max_parts * 2 + 1), dtype=np.uint8 @@ -203,4 +203,4 @@ def __visualize_structure(grid: NDArray[np.uint8], ax: plt.Axes) -> None: ax.scatter(x, y, z, c="r", marker="o") plt.draw() - plt.pause(0.5) + plt.pause(0.5) \ No newline at end of file diff --git a/standards/revolve2/standards/genotypes/cppnwin/modular_robot/v2/_body_genotype_v2.py b/standards/revolve2/standards/genotypes/cppnwin/modular_robot/v2/_body_genotype_v2.py index 90a70554e..ab42f7c78 100644 --- a/standards/revolve2/standards/genotypes/cppnwin/modular_robot/v2/_body_genotype_v2.py +++ b/standards/revolve2/standards/genotypes/cppnwin/modular_robot/v2/_body_genotype_v2.py @@ -8,11 +8,11 @@ from revolve2.modular_robot.body.v2 import BodyV2 -from ..._multineat_genotype_pickle_wrapper import MultineatGenotypePickleWrapper -from ..._multineat_rng_from_random import multineat_rng_from_random -from ..._random_multineat_genotype import random_multineat_genotype -from .._multineat_params import get_multineat_params -from ._body_develop import develop +from revolve2.standards.genotypes.cppnwin._multineat_genotype_pickle_wrapper import MultineatGenotypePickleWrapper +from revolve2.standards.genotypes.cppnwin._multineat_rng_from_random import multineat_rng_from_random +from revolve2.standards.genotypes.cppnwin._random_multineat_genotype import random_multineat_genotype +from revolve2.standards.genotypes.cppnwin.modular_robot._multineat_params import get_multineat_params +from revolve2.standards.genotypes.cppnwin.modular_robot.v2._body_develop import develop @dataclass @@ -43,7 +43,7 @@ def random_body( innov_db=innov_db, rng=multineat_rng, multineat_params=cls._MULTINEAT_PARAMS, - output_activation_func=multineat.ActivationFunction.UNSIGNED_SINE, + output_activation_func=multineat.ActivationFunction.UNSIGNED_SIGMOID, # changed from UNSIGNED_SIGMOID num_inputs=5, # bias(always 1), pos_x, pos_y, pos_z, chain_length num_outputs=2, # block_type, rotation_type num_initial_mutations=cls._NUM_INITIAL_MUTATIONS, @@ -72,13 +72,27 @@ def mutate_body( MultineatGenotypePickleWrapper( self.body.genotype.MutateWithConstraints( False, - multineat.SearchMode.BLENDED, + multineat.SearchMode.BLENDED, # meaning that mutation can complexify or simplify innov_db, self._MULTINEAT_PARAMS, multineat_rng, ) ) ) + + """ + Genome Genome::MutateWithConstraints(bool t_baby_is_clone, const SearchMode a_searchMode, + InnovationDatabase &a_innov_database, const Parameters &a_Parameters, RNG &a_RNG) const + { + Genome clone;o + do { + clone = Genome(*this); + clone.Mutate(t_baby_is_clone, a_searchMode, a_innov_database, a_Parameters, a_RNG); + } while (clone.FailsConstraints(a_Parameters)); + + return clone; + } + """ @classmethod def crossover_body( @@ -108,12 +122,24 @@ def crossover_body( ) ) ) - - def develop_body(self, visualize: bool = False) -> BodyV2: + + """ + Genome Genome::MateWithConstraints(Genome const& a_dad, bool a_averagemating, bool a_interspecies, + RNG &a_RNG, Parameters const& a_Parameters) const { + + Genome offspring; + do { + offspring = Mate(a_dad, a_averagemating, a_interspecies, a_RNG, a_Parameters); + } while (offspring.FailsConstraints(a_Parameters)); + + return offspring; + } + """ + + def develop_body(self) -> BodyV2: """ Develop the genotype into a modular robot. - :param visualize: Whether to plot the mapping from genotype to phenotype for visualization. :returns: The created robot. """ - return develop(self.body.genotype, visualize=visualize) + return develop(self.body.genotype) From d7d3ab65850bf56c086116799fef298de41791b6 Mon Sep 17 00:00:00 2001 From: A-lamo Date: Thu, 30 Jan 2025 12:00:38 +0100 Subject: [PATCH 03/11] GUI: changed database plotting location to resources --- gui/backend_example/plot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/backend_example/plot.py b/gui/backend_example/plot.py index 4dca0979d..dedd34731 100644 --- a/gui/backend_example/plot.py +++ b/gui/backend_example/plot.py @@ -20,7 +20,7 @@ def main() -> None: setup_logging() dbengine = open_database_sqlite( - "../viewer/"+config.DATABASE_FILE, open_method=OpenMethod.OPEN_IF_EXISTS + "../resources/"+config.DATABASE_FILE, open_method=OpenMethod.OPEN_IF_EXISTS ) df = pandas.read_sql( From 3a3f5b00068e3d9f7b9b26fce73df2145dbf7dc8 Mon Sep 17 00:00:00 2001 From: A-lamo Date: Thu, 6 Feb 2025 17:34:02 +0100 Subject: [PATCH 04/11] small tweaks to gui --- gui/backend_example/config.py | 2 +- gui/backend_example/main_from_gui.py | 3 ++- gui/viewer/main_window.py | 13 ++++++------- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/gui/backend_example/config.py b/gui/backend_example/config.py index 42e51eace..e4fecc471 100644 --- a/gui/backend_example/config.py +++ b/gui/backend_example/config.py @@ -4,5 +4,5 @@ NUM_GENERATIONS = 100 NUM_REPETITIONS = 1 NUM_SIMULATORS = 8 -OFFSPRING_SIZE = 100 +OFFSPRING_SIZE = 200 POPULATION_SIZE = 100 diff --git a/gui/backend_example/main_from_gui.py b/gui/backend_example/main_from_gui.py index 6c60e39c6..752c30510 100644 --- a/gui/backend_example/main_from_gui.py +++ b/gui/backend_example/main_from_gui.py @@ -1,6 +1,7 @@ """Main script for the example.""" import logging +from datetime import datetime import config import multineat @@ -127,7 +128,7 @@ def main() -> None: # Open the database, only if it does not already exists. dbengine = open_database_sqlite( - config.DATABASE_FILE, open_method=OpenMethod.NOT_EXISTS_AND_CREATE + f"{datetime.now()}"+config.DATABASE_FILE, open_method=OpenMethod.NOT_EXISTS_AND_CREATE ) # Create the structure of the database. Base.metadata.create_all(dbengine) diff --git a/gui/viewer/main_window.py b/gui/viewer/main_window.py index 772adca07..4af0cf679 100644 --- a/gui/viewer/main_window.py +++ b/gui/viewer/main_window.py @@ -2,11 +2,10 @@ QApplication, QMainWindow, QTabWidget, QWidget, QVBoxLayout, QLabel, QComboBox, QPushButton, QMessageBox, - QLineEdit, QHBoxLayout, QButtonGroup, - QStackedWidget) + QLineEdit, QHBoxLayout,QStackedWidget) from parsing import get_functions_from_file, get_function_names_from_init, get_config_parameters_from_file, save_config_parameters import subprocess -import os + class RobotEvolutionGUI(QMainWindow): def __init__(self): @@ -151,13 +150,13 @@ def create_ea_tab(self): # Generational View self.view1 = QWidget() v1_layout = QVBoxLayout() - v1_layout.addWidget(QLabel("Generational GA Parameters")) + v1_layout.addWidget(QLabel("Non-Overlapping (comma) Generations")) self.view1.setLayout(v1_layout) # Steady-State View self.view2 = QWidget() v2_layout = QVBoxLayout() - v2_layout.addWidget(QLabel("Steady-State GA Parameters")) + v2_layout.addWidget(QLabel("Overlapping (plus) Generations")) self.view2.setLayout(v2_layout) self.inputs_evolution = {} @@ -228,10 +227,10 @@ def update_view(self): """Update the UI based on the current mode.""" if self.is_generational: self.stacked_widget.setCurrentWidget(self.view1) - self.switch_button.setText("Switch to Steady-State") + self.switch_button.setText("Switch to Overlapping") else: self.stacked_widget.setCurrentWidget(self.view2) - self.switch_button.setText("Switch to Generational") + self.switch_button.setText("Switch to Non-Overlapping") def create_environment_tab(self): widget = QWidget() From 5ee3d751c9a47ed52c5add5a202795ca92cb23ab Mon Sep 17 00:00:00 2001 From: A-lamo Date: Thu, 6 Feb 2025 18:49:04 +0100 Subject: [PATCH 05/11] GUI updates: plotting is a bit more dynamic, and added a readme. --- gui/README.md | 9 +++++++ gui/viewer/main_window.py | 33 +++++++++++++++++++------ gui/viewer/parsing.py | 51 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 86 insertions(+), 7 deletions(-) create mode 100644 gui/README.md diff --git a/gui/README.md b/gui/README.md new file mode 100644 index 000000000..2f4b8d4a1 --- /dev/null +++ b/gui/README.md @@ -0,0 +1,9 @@ + +## 1) Using the GUI. + +This GUI is a new addition to revolve, its goal is to bundle up some of the functionalities of our robot evolution system and allow for easy parametrization of evolutionary runs. + +- To use the GUI simply navigate to the "gui/viewer/" folder +- Then run the **main_window.py** file from your terminal: python main_window.py + +Note: This is still very much under development, a lot of the planned features do not do anything yet (this will be indicated by the GUI). diff --git a/gui/viewer/main_window.py b/gui/viewer/main_window.py index 4af0cf679..779c65ed0 100644 --- a/gui/viewer/main_window.py +++ b/gui/viewer/main_window.py @@ -3,7 +3,8 @@ QWidget, QVBoxLayout, QLabel, QComboBox, QPushButton, QMessageBox, QLineEdit, QHBoxLayout,QStackedWidget) -from parsing import get_functions_from_file, get_function_names_from_init, get_config_parameters_from_file, save_config_parameters +from parsing import (get_functions_from_file, get_config_parameters_from_file, + save_config_parameters, get_selection_names_from_init, get_files_from_path) import subprocess @@ -29,8 +30,9 @@ def __init__(self): self.evolution_parameters = get_config_parameters_from_file(self.path_evolution_parameters) self.is_generational = self.evolution_parameters.get("GENERATIONAL") - self.selection_path = "/home/aronf/Desktop/EvolutionaryComputing/work/revolve2/experimentation/revolve2/experimentation/optimization/ea/selection/" - self.selection_functions = get_function_names_from_init(self.selection_path) + self.selection_functions = get_selection_names_from_init() + + self.database_path = "../resources/databases/" # Step 1: Define Robot Phenotypes self.tab_widget.addTab(self.create_phenotype_tab(), "Robot Phenotypes") @@ -74,8 +76,11 @@ def stop_simulation(self): print("No active simulation to stop.") def plot_results(self): - # Run the plot script - subprocess.Popen(["python", "../backend_example/plot.py"]) + selected_file = self.database_dropdown.currentText() + if selected_file: # Ensure a file is selected + subprocess.Popen(["python", "../backend_example/plot.py", selected_file]) + else: + print("No database selected.") def save_config_changes(self, file_path, inputs): """Update a config with new values from the GUI.""" @@ -297,14 +302,28 @@ def create_run_simulation_tab(self): layout.addWidget(stop_button) widget.setLayout(layout) return widget - + def create_plot_tab(self): widget = QWidget() layout = QVBoxLayout() - layout.addWidget(QLabel("Plot Results")) + input_layout = QHBoxLayout() + + label = QLabel("Plot Results from Database:") + self.combo_box = QComboBox() + self.combo_box.addItems(get_files_from_path(self.database_path)) + + input_layout.addWidget(label) + input_layout.addWidget(self.combo_box) + + input_layout.setStretch(0, 1) # Label takes less space + input_layout.setStretch(1, 3) # Dropdown takes more space + plot_button = QPushButton("Plot Results") plot_button.clicked.connect(self.plot_results) + + layout.addLayout(input_layout) layout.addWidget(plot_button) + widget.setLayout(layout) return widget diff --git a/gui/viewer/parsing.py b/gui/viewer/parsing.py index 2cbc67e15..962f670e4 100644 --- a/gui/viewer/parsing.py +++ b/gui/viewer/parsing.py @@ -1,6 +1,15 @@ import importlib.util import inspect import os +from pathlib import Path + + +def get_files_from_path(db_directory): + """Populate the dropdown with files from the directory.""" + if os.path.exists(db_directory): + return [f for f in os.listdir(db_directory) if os.path.isfile(os.path.join(db_directory, f))] + else: + print(f"There are no files in the directory {db_directory}") def get_functions_from_file(file_path): @@ -26,6 +35,7 @@ def get_function_names_from_init(folder_path): """ Import the __init__.py file from the given folder and extract the function names listed in the __all__ variable. """ + init_file = os.path.join(folder_path, "__init__.py") if not os.path.exists(init_file): raise FileNotFoundError(f"__init__.py not found in folder: {folder_path}") @@ -43,6 +53,47 @@ def get_function_names_from_init(folder_path): else: raise AttributeError(f"__all__ not found in {init_file}") +def get_selection_names_from_init(): + """ + Import the __init__.py file from the given folder and extract the function names listed in the __all__ variable. + """ + # Get the script's directory + script_path = Path(__file__).resolve() + + # Dynamically find the root of your project (assuming 'revolve2' is the project root) + for parent in script_path.parents: + if parent.name == "revolve2": + project_root = parent + break + else: + raise FileNotFoundError("Could not determine the project root (revolve2).") + + # Define the correct selection path dynamically + selection_path = project_root / "experimentation" / "revolve2" / "experimentation" / "optimization" / "ea" / "selection" + + # Ensure selection_path exists + if not selection_path.exists(): + raise FileNotFoundError(f"Selection path does not exist: {selection_path}") + + init_file = selection_path / "__init__.py" + + if not init_file.exists(): + raise FileNotFoundError(f"__init__.py not found in folder: {selection_path}") + + # Dynamically load the __init__.py file + module_name = selection_path.name # Use folder name as module name + spec = importlib.util.spec_from_file_location(module_name, init_file) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + + # Extract function names from __all__ list in __init__.py + if hasattr(module, "__all__"): + if 'multiple_unique' in module.__all__: + module.__all__.remove('multiple_unique') # Remove specific function if needed + return module.__all__ + else: + raise AttributeError(f"__all__ not found in {init_file}") + def get_config_parameters_from_file(file_path): """Dynamically load variables from a config file as a dictionary.""" if not os.path.exists(file_path): From 515c9113f477ad10a7143b3c8039cd8030a2ca55 Mon Sep 17 00:00:00 2001 From: A-lamo Date: Thu, 6 Feb 2025 18:49:48 +0100 Subject: [PATCH 06/11] further updates to gui and plotting --- gui/backend_example/main_from_gui.py | 2 +- gui/backend_example/plot.py | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/gui/backend_example/main_from_gui.py b/gui/backend_example/main_from_gui.py index 752c30510..453e57065 100644 --- a/gui/backend_example/main_from_gui.py +++ b/gui/backend_example/main_from_gui.py @@ -128,7 +128,7 @@ def main() -> None: # Open the database, only if it does not already exists. dbengine = open_database_sqlite( - f"{datetime.now()}"+config.DATABASE_FILE, open_method=OpenMethod.NOT_EXISTS_AND_CREATE + "../resources/databases/"+f"{datetime.now()}"+config.DATABASE_FILE, open_method=OpenMethod.NOT_EXISTS_AND_CREATE ) # Create the structure of the database. Base.metadata.create_all(dbengine) diff --git a/gui/backend_example/plot.py b/gui/backend_example/plot.py index dedd34731..055b081e2 100644 --- a/gui/backend_example/plot.py +++ b/gui/backend_example/plot.py @@ -9,6 +9,7 @@ from revolve2.experimentation.database import OpenMethod, open_database_sqlite from revolve2.experimentation.logging import setup_logging import argparse +import sys def argument_parser() -> argparse.ArgumentParser: """Create an argument parser.""" @@ -18,9 +19,11 @@ def argument_parser() -> argparse.ArgumentParser: def main() -> None: """Run the program.""" setup_logging() + + database_path = f"../resources/databases/{sys.argv[1]}" dbengine = open_database_sqlite( - "../resources/"+config.DATABASE_FILE, open_method=OpenMethod.OPEN_IF_EXISTS + database_path, open_method=OpenMethod.OPEN_IF_EXISTS ) df = pandas.read_sql( From a86e57d7189a49bede6f443cf0604df08ff022d8 Mon Sep 17 00:00:00 2001 From: A-lamo Date: Thu, 6 Feb 2025 19:08:02 +0100 Subject: [PATCH 07/11] gui updates: integrated rerun.py --- gui/backend_example/rerun.py | 7 ++-- gui/viewer/main_window.py | 68 +++++++++++++++++++++++++++--------- 2 files changed, 55 insertions(+), 20 deletions(-) diff --git a/gui/backend_example/rerun.py b/gui/backend_example/rerun.py index 54c158712..be71a503d 100644 --- a/gui/backend_example/rerun.py +++ b/gui/backend_example/rerun.py @@ -2,7 +2,7 @@ import logging -import config +import sys from database_components import Genotype, Individual from evaluator import Evaluator from sqlalchemy import select @@ -16,9 +16,10 @@ def main() -> None: """Perform the rerun.""" setup_logging() - # Load the best individual from the database. + database_path = f"../resources/databases/{sys.argv[1]}" + dbengine = open_database_sqlite( - config.DATABASE_FILE, open_method=OpenMethod.OPEN_IF_EXISTS + database_path, open_method=OpenMethod.OPEN_IF_EXISTS ) with Session(dbengine) as ses: diff --git a/gui/viewer/main_window.py b/gui/viewer/main_window.py index 779c65ed0..bd1ddfb62 100644 --- a/gui/viewer/main_window.py +++ b/gui/viewer/main_window.py @@ -34,8 +34,8 @@ def __init__(self): self.database_path = "../resources/databases/" - # Step 1: Define Robot Phenotypes - self.tab_widget.addTab(self.create_phenotype_tab(), "Robot Phenotypes") + # # Step 1: Define Robot Phenotypes + # self.tab_widget.addTab(self.create_phenotype_tab(), "Robot Phenotypes") # Step 2: Define Environment self.tab_widget.addTab(self.create_environment_tab(), "Environment & Task") @@ -50,7 +50,7 @@ def __init__(self): self.tab_widget.addTab(self.create_ea_tab(), "Evolutionary Algorithm") # Step 6: Selection - self.tab_widget.addTab(self.create_selection_tab(), "Selection Algorithms") + self.tab_widget.addTab(self.create_selection_tab(), "Selection Algorithms (UNDER DEVELOPMENT)") # Step 7: Simulator Selection self.tab_widget.addTab(self.create_simulation_parameters_tab(), "Physics Simulator") @@ -58,6 +58,8 @@ def __init__(self): # Step 8: Run Simulation self.tab_widget.addTab(self.create_run_simulation_tab(), "Run Simulation") + self.tab_widget.addTab(self.create_rerun_tab(), "Visualize Best Individual") + # Step 9: Plot Results self.tab_widget.addTab(self.create_plot_tab(), "Plot Results") @@ -76,12 +78,19 @@ def stop_simulation(self): print("No active simulation to stop.") def plot_results(self): - selected_file = self.database_dropdown.currentText() + selected_file = self.database_dropdown_plot.currentText() if selected_file: # Ensure a file is selected subprocess.Popen(["python", "../backend_example/plot.py", selected_file]) else: print("No database selected.") + def rerun(self): + selected_file = self.database_dropdown_rerun.currentText() + if selected_file: # Ensure a file is selected + subprocess.Popen(["python", "../backend_example/rerun.py", selected_file]) + else: + print("No database selected.") + def save_config_changes(self, file_path, inputs): """Update a config with new values from the GUI.""" new_values = {} @@ -101,16 +110,16 @@ def save_config_changes(self, file_path, inputs): QMessageBox.information(self, "Success", "Config file updated!") - def create_phenotype_tab(self): - widget = QWidget() - layout = QVBoxLayout() - layout.addWidget(QLabel("Define Robot Phenotypes")) - # Dropdown for phenotypes - phenotypes_dropdown = QComboBox() - phenotypes_dropdown.addItems(["Phenotype A", "Phenotype B", "Phenotype C"]) - layout.addWidget(phenotypes_dropdown) - widget.setLayout(layout) - return widget + # def create_phenotype_tab(self): + # widget = QWidget() + # layout = QVBoxLayout() + # layout.addWidget(QLabel("Define Robot Phenotypes")) + # # Dropdown for phenotypes + # phenotypes_dropdown = QComboBox() + # phenotypes_dropdown.addItems(["Phenotype A", "Phenotype B", "Phenotype C"]) + # layout.addWidget(phenotypes_dropdown) + # widget.setLayout(layout) + # return widget def create_genotype_tab(self): widget = QWidget() @@ -132,6 +141,7 @@ def create_genotype_tab(self): def create_selection_tab(self): widget = QWidget() layout = QVBoxLayout() + layout.addWidget(QLabel("UNDER DEVELOPMENT")) layout.addWidget(QLabel("Define Parent and Surivor Selection Types")) parent_dropdown = QComboBox() parent_dropdown.addItems(self.selection_functions) @@ -309,11 +319,11 @@ def create_plot_tab(self): input_layout = QHBoxLayout() label = QLabel("Plot Results from Database:") - self.combo_box = QComboBox() - self.combo_box.addItems(get_files_from_path(self.database_path)) + self.database_dropdown_plot = QComboBox() + self.database_dropdown_plot.addItems(get_files_from_path(self.database_path)) input_layout.addWidget(label) - input_layout.addWidget(self.combo_box) + input_layout.addWidget(self.database_dropdown_plot) input_layout.setStretch(0, 1) # Label takes less space input_layout.setStretch(1, 3) # Dropdown takes more space @@ -326,6 +336,30 @@ def create_plot_tab(self): widget.setLayout(layout) return widget + + def create_rerun_tab(self): + widget = QWidget() + layout = QVBoxLayout() + input_layout = QHBoxLayout() + + label = QLabel("Visualize Best Individual from Database:") + self.database_dropdown_rerun = QComboBox() + self.database_dropdown_rerun.addItems(get_files_from_path(self.database_path)) + + input_layout.addWidget(label) + input_layout.addWidget(self.database_dropdown_rerun) + + input_layout.setStretch(0, 1) # Label takes less space + input_layout.setStretch(1, 3) # Dropdown takes more space + + plot_button = QPushButton("Visualize") + plot_button.clicked.connect(self.rerun) + + layout.addLayout(input_layout) + layout.addWidget(plot_button) + + widget.setLayout(layout) + return widget if __name__ == "__main__": import sys From 92ab32ec2d23893a53c5b71f04bdd972f7b89c44 Mon Sep 17 00:00:00 2001 From: A-lamo Date: Thu, 6 Feb 2025 20:10:13 +0100 Subject: [PATCH 08/11] start of GUI beta. --- README.md | 8 +++++++- gui/README.md | 3 ++- gui/backend_example/config.py | 10 +++++----- gui/backend_example/evaluator.py | 11 ++++++++--- gui/backend_example/rerun.py | 2 ++ gui/backend_example/terrains.py | 8 ++++---- gui/viewer/main_window.py | 28 ++++++++++------------------ 7 files changed, 38 insertions(+), 32 deletions(-) diff --git a/README.md b/README.md index 42f028e04..42aea3da2 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ + # Revolve2 @@ -49,4 +50,9 @@ robot_state_end = scene_state_end.get_modular_robot_simulation_state(robot) xy_displacement = fitness_functions.xy_displacement( robot_state_begin, robot_state_end ) -``` \ No newline at end of file +``` + +## Furthermore you can beta test the new GUI by running: +`cd gui/viewer/` + +`python main_window.py` diff --git a/gui/README.md b/gui/README.md index 2f4b8d4a1..ab14c30e1 100644 --- a/gui/README.md +++ b/gui/README.md @@ -1,9 +1,10 @@ ## 1) Using the GUI. -This GUI is a new addition to revolve, its goal is to bundle up some of the functionalities of our robot evolution system and allow for easy parametrization of evolutionary runs. +This GUI is a very new addition to revolve, its goal is to bundle up some of the functionalities of our robot evolution system and allow for easy parametrization of evolutionary runs. - To use the GUI simply navigate to the "gui/viewer/" folder - Then run the **main_window.py** file from your terminal: python main_window.py + Note: This is still very much under development, a lot of the planned features do not do anything yet (this will be indicated by the GUI). diff --git a/gui/backend_example/config.py b/gui/backend_example/config.py index e4fecc471..25ae3b6b0 100644 --- a/gui/backend_example/config.py +++ b/gui/backend_example/config.py @@ -1,8 +1,8 @@ DATABASE_FILE = "database.sqlite" -GENERATIONAL = False -STEADY_STATE = True -NUM_GENERATIONS = 100 +GENERATIONAL = True +STEADY_STATE = False +NUM_GENERATIONS = 10 NUM_REPETITIONS = 1 -NUM_SIMULATORS = 8 -OFFSPRING_SIZE = 200 +NUM_SIMULATORS = 16 +OFFSPRING_SIZE = 100 POPULATION_SIZE = 100 diff --git a/gui/backend_example/evaluator.py b/gui/backend_example/evaluator.py index da3a94026..5251a0bf7 100644 --- a/gui/backend_example/evaluator.py +++ b/gui/backend_example/evaluator.py @@ -9,7 +9,9 @@ simulate_scenes, ) from revolve2.simulators.mujoco_simulator import LocalSimulator -from revolve2.standards import fitness_functions, terrains + +import sys +import fitness_functions, terrains from revolve2.standards.simulation_parameters import make_standard_batch_parameters @@ -23,6 +25,7 @@ def __init__( self, headless: bool, num_simulators: int, + terrain=terrains.flat() ) -> None: """ Initialize this object. @@ -33,8 +36,10 @@ def __init__( self._simulator = LocalSimulator( headless=headless, num_simulators=num_simulators ) - self._terrain = terrains.flat() - + if terrain != terrains.flat(): + self._terrain = eval("terrains."+terrain+"()") + else: + self._terrain = terrains.flat() def evaluate( self, population: list[Genotype], diff --git a/gui/backend_example/rerun.py b/gui/backend_example/rerun.py index be71a503d..10e06881e 100644 --- a/gui/backend_example/rerun.py +++ b/gui/backend_example/rerun.py @@ -21,6 +21,8 @@ def main() -> None: dbengine = open_database_sqlite( database_path, open_method=OpenMethod.OPEN_IF_EXISTS ) + + # terrain = sys.argv[2] with Session(dbengine) as ses: row = ses.execute( diff --git a/gui/backend_example/terrains.py b/gui/backend_example/terrains.py index 17940f699..20fafe910 100644 --- a/gui/backend_example/terrains.py +++ b/gui/backend_example/terrains.py @@ -32,10 +32,10 @@ def flat(size: Vector2 = Vector2([20.0, 20.0])) -> Terrain: def crater( - size: tuple[float, float], - ruggedness: float, - curviness: float, - granularity_multiplier: float = 1.0, + size= [50, 50], + ruggedness= 0.5, + curviness=1, + granularity_multiplier= 1.0, ) -> Terrain: r""" Create a crater-like terrain with rugged floor using a heightmap. diff --git a/gui/viewer/main_window.py b/gui/viewer/main_window.py index bd1ddfb62..a19f1fb63 100644 --- a/gui/viewer/main_window.py +++ b/gui/viewer/main_window.py @@ -37,36 +37,28 @@ def __init__(self): # # Step 1: Define Robot Phenotypes # self.tab_widget.addTab(self.create_phenotype_tab(), "Robot Phenotypes") - # Step 2: Define Environment self.tab_widget.addTab(self.create_environment_tab(), "Environment & Task") - # Step 3: Define Genotypes self.tab_widget.addTab(self.create_genotype_tab(), "Robot Genotypes") - # Step 4: Fitness Function self.tab_widget.addTab(self.create_fitness_tab(), "Fitness Function") - # Step 5: Evolution Parameters self.tab_widget.addTab(self.create_ea_tab(), "Evolutionary Algorithm") - # Step 6: Selection self.tab_widget.addTab(self.create_selection_tab(), "Selection Algorithms (UNDER DEVELOPMENT)") - # Step 7: Simulator Selection self.tab_widget.addTab(self.create_simulation_parameters_tab(), "Physics Simulator") - # Step 8: Run Simulation self.tab_widget.addTab(self.create_run_simulation_tab(), "Run Simulation") self.tab_widget.addTab(self.create_rerun_tab(), "Visualize Best Individual") - # Step 9: Plot Results self.tab_widget.addTab(self.create_plot_tab(), "Plot Results") def run_simulation(self): - # Run the simulation - self.simulation_process = subprocess.Popen(["python", "../backend_example/main_from_gui.py"]) + terrain = self.environment_dropdown.currentText() + self.simulation_process = subprocess.Popen(["python", "../backend_example/main_from_gui.py", terrain]) def stop_simulation(self): """Stop the running simulation.""" @@ -124,7 +116,7 @@ def save_config_changes(self, file_path, inputs): def create_genotype_tab(self): widget = QWidget() layout = QVBoxLayout() - layout.addWidget(QLabel("UNDER CONSTRUCTION:\n Define Mutation and Crossover Operators")) + layout.addWidget(QLabel("UNDER DEVELOPMENT - \n Define Mutation and Crossover Operators")) # Mutation operator mutation_dropdown = QComboBox() mutation_dropdown.addItems(["Operator A", "Operator B", "Operator C"]) @@ -250,14 +242,14 @@ def update_view(self): def create_environment_tab(self): widget = QWidget() layout = QVBoxLayout() - layout.addWidget(QLabel("Define Environment and Task")) + layout.addWidget(QLabel("UNDER DEVELOPMENT - Define Environment and Task")) # Environment settings layout.addWidget(QLabel("Environment Terrains: ")) - environment_dropdown = QComboBox() # Example: Environment parameter input - environment_dropdown.addItems(self.terrains.keys()) - layout.addWidget(environment_dropdown) + self.environment_dropdown = QComboBox() # Example: Environment parameter input + self.environment_dropdown.addItems(self.terrains.keys()) + layout.addWidget(self.environment_dropdown) widget.setLayout(layout) - layout + # Task selection task_dropdown = QComboBox() task_dropdown.addItems(["Task 1", "Task 2", "Task 3"]) @@ -266,10 +258,10 @@ def create_environment_tab(self): widget.setLayout(layout) return widget - def create_fitness_tab(self): + def create_fitness_tab(self): # under development widget = QWidget() layout = QVBoxLayout() - layout.addWidget(QLabel("Define Fitness Function")) + layout.addWidget(QLabel("UNDER DEVELOPMENT - Define Fitness Function")) fitness_dropdown = QComboBox() fitness_dropdown.addItems(self.fitness_functions.keys()) layout.addWidget(fitness_dropdown) From 29f46401a6fba4a64a528cf3d6fc46f7743e0647 Mon Sep 17 00:00:00 2001 From: A-lamo Date: Thu, 6 Feb 2025 20:19:38 +0100 Subject: [PATCH 09/11] Align with master branch. --- docs/source/physical_robot_core_setup/index.rst | 10 +++++++--- examples/4_example_experiment_setups/README.md | 2 ++ .../5a_physical_robot_remote/main.py | 11 ++++++++++- experimentation/pyproject.toml | 2 +- 4 files changed, 20 insertions(+), 5 deletions(-) diff --git a/docs/source/physical_robot_core_setup/index.rst b/docs/source/physical_robot_core_setup/index.rst index 533a0b640..8a9e5b711 100644 --- a/docs/source/physical_robot_core_setup/index.rst +++ b/docs/source/physical_robot_core_setup/index.rst @@ -39,6 +39,9 @@ On the RPi adjust the config in `/boot/config.txt` or on newer systems `/boot/fi ------------------ Setting up the RPi ------------------ +**Note**: For students in the CI Group, the RPi is already set up. If the heads are labeled as `flashed`, it means they are already flashed with the setup image, so the following steps are unnecessary. Additionally, the flashed heads are already connected to the *ThymioNet* Wi-Fi. However, the IP address on the head changes from time to time, so you should use the serial connection to log in and obtain the correct IP address. For instructions on how to establish a serial connection, please refer to the section below. +Also, note that ongoing development changes will continue in revolve2-modular-robot_physical and revolve2-robohat packages, so make sure to pip install the latest version in your virtual environment. + This step is the same for all types of hardware. #. Flash the SD card with Raspberry Pi OS (previously Raspbian). Some Important notes: @@ -106,8 +109,8 @@ Setting up Revolve2 on the robot requires different steps, depending on the hard * V1: :code:`pip install "revolve2-modular_robot_physical[botv1] @ git+https://github.com/ci-group/revolve2.git@#subdirectory=modular_robot_physical"`. * V2: :code:`pip install "revolve2-modular_robot_physical[botv2] @ git+https://github.com/ci-group/revolve2.git@#subdirectory=modular_robot_physical"`. - For example, if you want to install the version tagged as 1.2.2, the command would be: - :code:`pip install "revolve2-modular_robot_physical[botv2] @ git+https://github.com/ci-group/revolve2.git@1.2.2#subdirectory=modular_robot_physical"` + For example, if you want to install the version tagged as 1.2.3, the command would be: + :code:`pip install "revolve2-modular_robot_physical[botv2] @ git+https://github.com/ci-group/revolve2.git@1.2.3#subdirectory=modular_robot_physical"` #. Set up the Revolve2 physical robot daemon: #. Create a systemd service file: :code:`sudo nano /etc/systemd/system/robot-daemon.service` @@ -133,6 +136,7 @@ Setting up Revolve2 on the robot requires different steps, depending on the hard #. Here, the :code:`Nice=-10` line sets a high priority for the daemon (lower values are higher priority, with -20 being the highest priority). The :code:`-l` option in the :code:`ExecStart` line tells :code:`robot-daemon` to only listen on the localhost interface. The :code:`-n localhost` option ensures that robot-daemon only runs if it can connect to localhost (preventing certain failure cases). #. Enable and start the service: :code:`sudo systemctl daemon-reload` & :code:`sudo systemctl enable robot-daemon` & :code:`sudo systemctl start robot-daemon`. #. Check if it is running properly using: :code:`sudo systemctl status robot-daemon` + #. If it's not running properly, check the logs using: :code:`journalctl -u robot-daemon -e` ^^^^^^^^^^^^^^^^^^^ V1 Additional Steps @@ -173,4 +177,4 @@ There are two examples (https://github.com/ci-group/revolve2/tree/master/example **5a_physical_robot_remote**: Control a physical modular robot by running its brain locally on your computer and streaming movement instructions to the physical modular robot. -**5b_compare_simulated_and_physical_robot**: Create a physical robot with a simulated twin. You will use two separate scripts, one for the simulated robot and one for the physical robot. With this duplicate you can check whether you have built the physical robot correctly, by comparing it to its simulated counterpart. +**5b_compare_simulated_and_physical_robot**: Create a physical robot with a simulated twin. You will use two separate scripts, one for the simulated robot and one for the physical robot. With this duplicate you can check whether you have built the physical robot correctly, by comparing it to its simulated counterpart. \ No newline at end of file diff --git a/examples/4_example_experiment_setups/README.md b/examples/4_example_experiment_setups/README.md index ed0f72a72..8d168df85 100644 --- a/examples/4_example_experiment_setups/README.md +++ b/examples/4_example_experiment_setups/README.md @@ -1,3 +1,4 @@ + ## 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. @@ -9,4 +10,5 @@ Additionally you will learn hwo to use the evolution abstraction layer in Revolv - `4d_robot_bodybrain_ea_database` is the same example, with the addition of databases to allow for data storage. - If you want to add learning into your experiments look at `4e_robot_brain_cmaes`, in which we add learning to a **single** robot. - `4f_robot_brain_cmaes_database` does the same thing as the previous example with the addition of a database. +- Finally you can learn about the exploration of the initial population and their morphological features in `4g_explore_initial_population` diff --git a/examples/5_physical_modular_robots/5a_physical_robot_remote/main.py b/examples/5_physical_modular_robots/5a_physical_robot_remote/main.py index b7ae25644..6b59982a1 100644 --- a/examples/5_physical_modular_robots/5a_physical_robot_remote/main.py +++ b/examples/5_physical_modular_robots/5a_physical_robot_remote/main.py @@ -1,9 +1,12 @@ """An example on how to remote control a physical modular robot.""" +from pyrr import Vector3 + from revolve2.experimentation.rng import make_rng_time_seed from revolve2.modular_robot import ModularRobot from revolve2.modular_robot.body import RightAngles from revolve2.modular_robot.body.base import ActiveHinge +from revolve2.modular_robot.body.sensors import CameraSensor from revolve2.modular_robot.body.v2 import ActiveHingeV2, BodyV2, BrickV2 from revolve2.modular_robot.brain.cpg import BrainCpgNetworkNeighborRandom from revolve2.modular_robot_physical import Config, UUIDKey @@ -40,6 +43,10 @@ def make_body() -> ( body.core_v2.right_face.bottom, body.core_v2.right_face.bottom.attachment, ) + """Here we add a camera sensor to the core. If you don't have a physical camera attached, uncomment this line.""" + body.core.add_sensor( + CameraSensor(position=Vector3([0, 0, 0]), camera_size=(480, 640)) + ) return body, active_hinges @@ -100,6 +107,7 @@ def main() -> None: Create a Remote for the physical modular robot. Make sure to target the correct hardware type and fill in the correct IP and credentials. The debug flag is turned on. If the remote complains it cannot keep up, turning off debugging might improve performance. + If you want to display the camera view, set display_camera_view to True. """ print("Initializing robot..") run_remote( @@ -107,6 +115,7 @@ def main() -> None: hostname="localhost", # "Set the robot IP here. debug=True, on_prepared=on_prepared, + display_camera_view=False, ) """ 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. @@ -114,4 +123,4 @@ def main() -> None: if __name__ == "__main__": - main() + main() \ No newline at end of file diff --git a/experimentation/pyproject.toml b/experimentation/pyproject.toml index 32bf2e99d..95fdeb2eb 100644 --- a/experimentation/pyproject.toml +++ b/experimentation/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api" [tool.poetry] name = "revolve2-experimentation" -version = "1.2.2" +version = "1.2.3" description = "Revolve2: Tools for experimentation." readme = "../README.md" authors = [ From d210dac18daecfecbe28d637b8bd55c6d8d76102 Mon Sep 17 00:00:00 2001 From: Aron Ferencz <2727043@student.vu.nl> Date: Tue, 11 Feb 2025 09:53:11 +0100 Subject: [PATCH 10/11] formatted with black. --- .../5a_physical_robot_remote/main.py | 2 +- .../optimization/ea/selection/__init__.py | 8 +++++-- .../optimization/ea/selection/_roulette.py | 21 +++++++++------- .../cppnwin/modular_robot/v2/_body_develop.py | 2 +- .../modular_robot/v2/_body_genotype_v2.py | 24 ++++++++++++------- 5 files changed, 37 insertions(+), 20 deletions(-) diff --git a/examples/5_physical_modular_robots/5a_physical_robot_remote/main.py b/examples/5_physical_modular_robots/5a_physical_robot_remote/main.py index 6b59982a1..1cb5f521c 100644 --- a/examples/5_physical_modular_robots/5a_physical_robot_remote/main.py +++ b/examples/5_physical_modular_robots/5a_physical_robot_remote/main.py @@ -123,4 +123,4 @@ def main() -> None: if __name__ == "__main__": - main() \ No newline at end of file + main() diff --git a/experimentation/revolve2/experimentation/optimization/ea/selection/__init__.py b/experimentation/revolve2/experimentation/optimization/ea/selection/__init__.py index 29a3846c0..82a762b0e 100644 --- a/experimentation/revolve2/experimentation/optimization/ea/selection/__init__.py +++ b/experimentation/revolve2/experimentation/optimization/ea/selection/__init__.py @@ -1,7 +1,11 @@ """Functions for selecting individuals from populations in EA algorithms.""" -from revolve2.experimentation.optimization.ea.selection._multiple_unique import multiple_unique -from revolve2.experimentation.optimization.ea.selection._pareto_frontier import pareto_frontier +from revolve2.experimentation.optimization.ea.selection._multiple_unique import ( + multiple_unique, +) +from revolve2.experimentation.optimization.ea.selection._pareto_frontier import ( + pareto_frontier, +) from revolve2.experimentation.optimization.ea.selection._topn import topn from revolve2.experimentation.optimization.ea.selection._tournament import tournament from revolve2.experimentation.optimization.ea.selection._roulette import roulette diff --git a/experimentation/revolve2/experimentation/optimization/ea/selection/_roulette.py b/experimentation/revolve2/experimentation/optimization/ea/selection/_roulette.py index b0e4189e9..0f48f7562 100644 --- a/experimentation/revolve2/experimentation/optimization/ea/selection/_roulette.py +++ b/experimentation/revolve2/experimentation/optimization/ea/selection/_roulette.py @@ -7,6 +7,7 @@ Genotype = TypeVar("Genotype") Fitness = TypeVar("Fitness", bound=SupportsLt) + def roulette(n: int, genotypes: list[Genotype], fitnesses: list[Fitness]) -> list[int]: """ Perform roulette wheel selection to choose n genotypes probabilistically based on fitness. @@ -17,19 +18,23 @@ def roulette(n: int, genotypes: list[Genotype], fitnesses: list[Fitness]) -> lis :returns: Indices of the selected genotypes. """ assert len(fitnesses) >= n, "Number of selections cannot exceed population size" - + # Normalize fitness values to ensure all are positive min_fitness = min(fitnesses) if min_fitness < 0: - fitnesses = [f - min_fitness for f in fitnesses] # Shift all values to be positive - + fitnesses = [ + f - min_fitness for f in fitnesses + ] # Shift all values to be positive + total_fitness = sum(fitnesses) - assert total_fitness > 0, "Total fitness must be greater than zero for roulette selection" - + assert ( + total_fitness > 0 + ), "Total fitness must be greater than zero for roulette selection" + # Compute selection probabilities probabilities = [f / total_fitness for f in fitnesses] - + # Perform roulette wheel selection selected_indices = random.choices(range(len(fitnesses)), weights=probabilities, k=n) - - return selected_indices \ No newline at end of file + + return selected_indices diff --git a/standards/revolve2/standards/genotypes/cppnwin/modular_robot/v2/_body_develop.py b/standards/revolve2/standards/genotypes/cppnwin/modular_robot/v2/_body_develop.py index a903cec1f..8fc74eef2 100644 --- a/standards/revolve2/standards/genotypes/cppnwin/modular_robot/v2/_body_develop.py +++ b/standards/revolve2/standards/genotypes/cppnwin/modular_robot/v2/_body_develop.py @@ -243,4 +243,4 @@ def __visualize_structure(grid: NDArray[np.uint8], ax: plt.Axes) -> None: ax.scatter(x, y, z, c="r", marker="o") plt.draw() - plt.pause(0.5) \ No newline at end of file + plt.pause(0.5) diff --git a/standards/revolve2/standards/genotypes/cppnwin/modular_robot/v2/_body_genotype_v2.py b/standards/revolve2/standards/genotypes/cppnwin/modular_robot/v2/_body_genotype_v2.py index ab42f7c78..be8bd28bd 100644 --- a/standards/revolve2/standards/genotypes/cppnwin/modular_robot/v2/_body_genotype_v2.py +++ b/standards/revolve2/standards/genotypes/cppnwin/modular_robot/v2/_body_genotype_v2.py @@ -8,10 +8,18 @@ from revolve2.modular_robot.body.v2 import BodyV2 -from revolve2.standards.genotypes.cppnwin._multineat_genotype_pickle_wrapper import MultineatGenotypePickleWrapper -from revolve2.standards.genotypes.cppnwin._multineat_rng_from_random import multineat_rng_from_random -from revolve2.standards.genotypes.cppnwin._random_multineat_genotype import random_multineat_genotype -from revolve2.standards.genotypes.cppnwin.modular_robot._multineat_params import get_multineat_params +from revolve2.standards.genotypes.cppnwin._multineat_genotype_pickle_wrapper import ( + MultineatGenotypePickleWrapper, +) +from revolve2.standards.genotypes.cppnwin._multineat_rng_from_random import ( + multineat_rng_from_random, +) +from revolve2.standards.genotypes.cppnwin._random_multineat_genotype import ( + random_multineat_genotype, +) +from revolve2.standards.genotypes.cppnwin.modular_robot._multineat_params import ( + get_multineat_params, +) from revolve2.standards.genotypes.cppnwin.modular_robot.v2._body_develop import develop @@ -43,7 +51,7 @@ def random_body( innov_db=innov_db, rng=multineat_rng, multineat_params=cls._MULTINEAT_PARAMS, - output_activation_func=multineat.ActivationFunction.UNSIGNED_SIGMOID, # changed from UNSIGNED_SIGMOID + output_activation_func=multineat.ActivationFunction.UNSIGNED_SIGMOID, # changed from UNSIGNED_SIGMOID num_inputs=5, # bias(always 1), pos_x, pos_y, pos_z, chain_length num_outputs=2, # block_type, rotation_type num_initial_mutations=cls._NUM_INITIAL_MUTATIONS, @@ -72,14 +80,14 @@ def mutate_body( MultineatGenotypePickleWrapper( self.body.genotype.MutateWithConstraints( False, - multineat.SearchMode.BLENDED, # meaning that mutation can complexify or simplify + multineat.SearchMode.BLENDED, # meaning that mutation can complexify or simplify innov_db, self._MULTINEAT_PARAMS, multineat_rng, ) ) ) - + """ Genome Genome::MutateWithConstraints(bool t_baby_is_clone, const SearchMode a_searchMode, InnovationDatabase &a_innov_database, const Parameters &a_Parameters, RNG &a_RNG) const @@ -122,7 +130,7 @@ def crossover_body( ) ) ) - + """ Genome Genome::MateWithConstraints(Genome const& a_dad, bool a_averagemating, bool a_interspecies, RNG &a_RNG, Parameters const& a_Parameters) const { From ac5c614416ff8aa0947d0d0997340a551492e73d Mon Sep 17 00:00:00 2001 From: Aron Ferencz <2727043@student.vu.nl> Date: Tue, 11 Feb 2025 13:01:21 +0100 Subject: [PATCH 11/11] added my email address. --- gui/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/README.md b/gui/README.md index ab14c30e1..508d715ee 100644 --- a/gui/README.md +++ b/gui/README.md @@ -7,4 +7,4 @@ This GUI is a very new addition to revolve, its goal is to bundle up some of the - Then run the **main_window.py** file from your terminal: python main_window.py -Note: This is still very much under development, a lot of the planned features do not do anything yet (this will be indicated by the GUI). +Note: This is still very much under development, a lot of the planned features do not do anything yet (this will be indicated by the GUI). created by Áron Ferencz, if you have any issues or ideas you can reach me at the following email address: a.r.ferencz@student.vu.nl