diff --git a/README.ipynb b/README.ipynb index 85093dc..28eaa70 100644 --- a/README.ipynb +++ b/README.ipynb @@ -73,23 +73,23 @@ { "data": { "text/plain": [ - "[{'id': UUID('8b77fc1d-befe-4ad3-924c-1774223b7b60'),\n", + "[{'id': UUID('8aa2c907-479b-444c-8c15-995a9015183a'),\n", " 'experiment': None,\n", - " 'created': datetime.datetime(2023, 3, 4, 15, 24, 49, 325435),\n", + " 'created': datetime.datetime(2023, 3, 27, 11, 25, 55, 969290),\n", " 'parents': [],\n", " 'generation': 1,\n", " 'score': None,\n", - " 'chromosome_1': Haploid([0, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0], dtype=uint8),\n", - " 'chromosome_2': Haploid([1, 0, 1, 1, 1, 0, 0, 1, 0, 1], dtype=uint8),\n", + " 'chromosome_1': Haploid([1, 0, 0, 0, 1, 1, 0, 1, 1, 1, 0, 0], dtype=uint8),\n", + " 'chromosome_2': Haploid([0, 1, 0, 0, 0, 1, 0, 0, 1, 0], dtype=uint8),\n", " 'simple_attribute': 1.0},\n", - " {'id': UUID('a4460974-a45a-4ed2-8937-55ea211bb520'),\n", + " {'id': UUID('94f78963-e4bf-4059-8b81-03b62587edd3'),\n", " 'experiment': None,\n", - " 'created': datetime.datetime(2023, 3, 4, 15, 24, 49, 325564),\n", + " 'created': datetime.datetime(2023, 3, 27, 11, 25, 55, 969420),\n", " 'parents': [],\n", " 'generation': 1,\n", " 'score': None,\n", - " 'chromosome_1': Haploid([1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0], dtype=uint8),\n", - " 'chromosome_2': Haploid([1, 0, 1, 1, 1, 0, 0, 1, 0, 1], dtype=uint8),\n", + " 'chromosome_1': Haploid([1, 0, 1, 0, 0, 0, 1, 0, 1, 1, 1, 1], dtype=uint8),\n", + " 'chromosome_2': Haploid([0, 0, 0, 1, 0, 1, 0, 1, 1, 1], dtype=uint8),\n", " 'simple_attribute': 1.0}]" ] }, @@ -140,7 +140,7 @@ { "data": { "text/plain": [ - "<__main__.MyFitness at 0x7f19e0744f40>" + "<__main__.MyFitness at 0x7fdb4574c490>" ] }, "execution_count": 2, @@ -199,7 +199,7 @@ { "data": { "text/plain": [ - "MyAlgorithm(selection1=, mutation=, selection2=, crossover=, survival_rate=0.4)" + "MyAlgorithm(selection1=, mutation=, selection2=, crossover=, survival_rate=0.4)" ] }, "execution_count": 3, @@ -254,8 +254,8 @@ "data": { "text/plain": [ "Evolutionary algorithm execution report:\n", - " Executed generations: 12\n", - " Best phenotype: 7b13630f-d07c-4ff6-8be1-df6d6ceb06ca\n", + " Executed generations: 17\n", + " Best phenotype: 4558f0a4-26c7-4803-8c65-91b110a3d84e\n", " Best score: 10" ] }, @@ -267,13 +267,11 @@ "source": [ "import gevopy as ea\n", "\n", - "experiment = ea.Experiment(\n", - " fitness=MyFitness(cache=True, scheduler=\"synchronous\"),\n", - " algorithm=MyAlgorithm(survival_rate=0.2),\n", - ")\n", - "\n", + "experiment = ea.Experiment()\n", "with experiment.session() as session:\n", " session.add_phenotypes([MyGenotype() for _ in range(20)])\n", + " session.algorithm = MyAlgorithm(survival_rate=0.2)\n", + " session.fitness = MyFitness(cache=True, scheduler=\"synchronous\")\n", " statistics = session.run(max_generation=20, max_score=10)\n", "\n", "experiment.close()\n", diff --git a/README.md b/README.md index 78055b5..b00cebc 100644 --- a/README.md +++ b/README.md @@ -159,29 +159,23 @@ instantiating the experiment to store all phenotypes during the execution. ```python import gevopy as ea -experiment = ea.Experiment( - fitness=MyFitness(cache=True, scheduler="synchronous"), - algorithm=MyAlgorithm(survival_rate=0.2), -) - +experiment = ea.Experiment() with experiment.session() as session: session.add_phenotypes([MyGenotype() for _ in range(20)]) + session.algorithm = MyAlgorithm(survival_rate=0.2) + session.fitness = MyFitness(cache=True, scheduler="synchronous") statistics = session.run(max_generation=20, max_score=10) experiment.close() statistics ``` - - - Evolutionary algorithm execution report: Executed generations: 12 Best phenotype: 7b13630f-d07c-4ff6-8be1-df6d6ceb06ca Best score: 10 - >The method `run` forces the evolution of the experiment which is updated on each cycle. After the method is completed, you can force again te evolution process using higher inputs for `max_generations` or `max_score`. diff --git a/examples/database_evolution.ipynb b/examples/database_evolution.ipynb index 4ec9ead..a990823 100644 --- a/examples/database_evolution.ipynb +++ b/examples/database_evolution.ipynb @@ -85,30 +85,34 @@ "name": "stderr", "output_type": "stream", "text": [ - "2022-12-15 09:06:03,357 - gevopy.Experiment - INFO\n", - " [44d71da0-2345-4686-842e-b7dbee7000af]: [gen:0]: Start of evolutionary experiment execution\n", - "2022-12-15 09:06:03,391 - gevopy.Experiment - INFO\n", - " [44d71da0-2345-4686-842e-b7dbee7000af]: [gen:1]: Completed cycle; 8\n", - "2022-12-15 09:06:03,411 - gevopy.Experiment - INFO\n", - " [44d71da0-2345-4686-842e-b7dbee7000af]: [gen:2]: Completed cycle; 9\n", - "2022-12-15 09:06:03,433 - gevopy.Experiment - INFO\n", - " [44d71da0-2345-4686-842e-b7dbee7000af]: [gen:3]: Completed cycle; 9\n", - "2022-12-15 09:06:03,454 - gevopy.Experiment - INFO\n", - " [44d71da0-2345-4686-842e-b7dbee7000af]: [gen:4]: Completed cycle; 10\n", - "2022-12-15 09:06:03,455 - gevopy.Experiment - INFO\n", - " [44d71da0-2345-4686-842e-b7dbee7000af]: [gen:4]: Experiment execution completed successfully\n" + "2023-03-27 11:23:48,181 - gevopy.Experiment - INFO\n", + " [MyExperiment]: Experiment evolution phase:1 start\n", + "2023-03-27 11:23:48,182 - gevopy.Experiment - INFO\n", + " [gen:0]: Start of evolutionary experiment execution\n", + "2023-03-27 11:23:48,208 - gevopy.Experiment - INFO\n", + " [gen:1]: Completed cycle; 8\n", + "2023-03-27 11:23:48,223 - gevopy.Experiment - INFO\n", + " [gen:2]: Completed cycle; 9\n", + "2023-03-27 11:23:48,237 - gevopy.Experiment - INFO\n", + " [gen:3]: Completed cycle; 9\n", + "2023-03-27 11:23:48,252 - gevopy.Experiment - INFO\n", + " [gen:4]: Completed cycle; 10\n", + "2023-03-27 11:23:48,253 - gevopy.Experiment - INFO\n", + " [gen:4]: Experiment execution completed successfully\n" ] } ], "source": [ "experiment = ea.Experiment(\n", - " fitness=MyFitness(cache=True, scheduler=\"synchronous\"),\n", - " algorithm=MyAlgorithm(survival_rate=0.2),\n", " database=Neo4jInterface(URI, auth=AUTH),\n", + " name=\"MyExperiment\",\n", ")\n", "\n", "with experiment.session() as session:\n", " session.add_phenotypes([MyGenotype() for _ in range(12)])\n", + " session.algorithm = MyAlgorithm(survival_rate=0.2)\n", + " session.fitness = MyFitness(cache=True, scheduler=\"synchronous\")\n", + " session.logger.info(\"Experiment evolution phase:1 start\")\n", " statistics = session.run(max_score=10)\n" ] }, @@ -122,7 +126,7 @@ "text/plain": [ "Evolutionary algorithm execution report:\n", " Executed generations: 4\n", - " Best phenotype: 2123579d-b38d-4f19-8f37-2fd261f83b68\n", + " Best phenotype: 00897083-f603-4b24-8d83-dff50af2b61a\n", " Best score: 10" ] }, @@ -170,7 +174,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.8" + "version": "3.10.10" }, "orig_nbformat": 4, "vscode": { diff --git a/examples/stepped_evolution.ipynb b/examples/stepped_evolution.ipynb new file mode 100644 index 0000000..95a1bae --- /dev/null +++ b/examples/stepped_evolution.ipynb @@ -0,0 +1,173 @@ +{ + "cells": [ + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "#### Import dependencies and configure globals\n", + "This example, shows how to use gevopy to evolve a simple Haploid genotype in order to obtain a phenotype with at least 10 ones in it's chromosome. In the process, all the phenotypes are stored inside a neo4j database.\n", + "\n", + "To do so, it is required to import gevopy and generate the Genotype, Fitness and Algorithm required. For the aim of simplicity, we will use some already defined examples from this library. Additionally it is required to import the class Neo4jInterface to specify the connection with our database.\n", + "\n", + "Additionally, you can use neo4j library to verify the connection and the logging module to increase the verbosity of the evolution process." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "import logging\n", + "\n", + "import gevopy as ea\n", + "from examples.algorimths import BasicPonderated as MyAlgorithm\n", + "from examples.evaluation import MostOnes, Random\n", + "from examples.genotypes import Bacteria as MyGenotype\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "LOG_FORMAT = \"%(asctime)s - %(name)s - %(levelname)s\\n %(message)s\"\n", + "logging.basicConfig(level=\"INFO\", format=LOG_FORMAT)\n", + "logger = logging.getLogger(\"gevopy.Experiment\")\n", + "logger.setLevel(logging.INFO) # Only Experiment INFO\n" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Create and run the experiment\n", + "Execute an experiment by creating an instance and assigning the argument database with the driver. The experiment will automaticaly manage sessions and use the methods .execute_read and .execute_write.\n", + "\n", + "Use the experiment session as a normal example to evolve a Genotype. As minimalist example, add some phenotypes as a base and start the evolution by using .run method." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "experiment = ea.Experiment(\n", + " algorithm=MyAlgorithm(survival_rate=0.2),\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2023-03-27 11:21:18,005 - gevopy.Experiment - INFO\n", + " [c410ba56-a2c5-429b-bd50-b39f9924669a]: Starting experiment phase 1\n", + "2023-03-27 11:21:18,007 - gevopy.Experiment - INFO\n", + " [gen:0]: Start of evolutionary experiment execution\n", + "2023-03-27 11:21:18,023 - gevopy.Experiment - INFO\n", + " [gen:1]: Completed cycle; 9\n", + "2023-03-27 11:21:18,031 - gevopy.Experiment - INFO\n", + " [gen:2]: Completed cycle; 9\n", + "2023-03-27 11:21:18,039 - gevopy.Experiment - INFO\n", + " [gen:3]: Completed cycle; 9\n", + "2023-03-27 11:21:18,047 - gevopy.Experiment - INFO\n", + " [gen:4]: Completed cycle; 10\n", + "2023-03-27 11:21:18,048 - gevopy.Experiment - INFO\n", + " [gen:4]: Experiment execution completed successfully\n", + "2023-03-27 11:21:18,049 - gevopy.Experiment - INFO\n", + " [c410ba56-a2c5-429b-bd50-b39f9924669a]: Starting experiment phase 2\n", + "2023-03-27 11:21:18,050 - gevopy.Experiment - INFO\n", + " [gen:0]: Start of evolutionary experiment execution\n", + "2023-03-27 11:21:18,061 - gevopy.Experiment - INFO\n", + " [gen:1]: Completed cycle; 0.842471456650234\n", + "2023-03-27 11:21:18,069 - gevopy.Experiment - INFO\n", + " [gen:2]: Completed cycle; 0.9858978453747482\n", + "2023-03-27 11:21:18,070 - gevopy.Experiment - INFO\n", + " [gen:2]: Experiment execution completed successfully\n" + ] + }, + { + "data": { + "text/plain": [ + "Evolutionary algorithm execution report:\n", + " Executed generations: 2\n", + " Best phenotype: f43d92c9-ccdc-4e08-9f0b-17c991037d14\n", + " Best score: 0.9858978453747482" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "with experiment.session() as session:\n", + " session.add_phenotypes([MyGenotype() for _ in range(12)])\n", + " session.fitness = MostOnes()\n", + " session.logger.info(\"Starting experiment phase 1\")\n", + " statistics_1 = session.run(max_score=10)\n", + " session.reset_score()\n", + " session.fitness = Random()\n", + " session.logger.info(\"Starting experiment phase 2\")\n", + " statistics_2 = session.run(max_score=0.95)\n", + "\n", + "statistics_2" + ] + }, + { + "attachments": {}, + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### Close connections\n", + "Unless you created them using the with statement, call the .close method on all Driver and Session instances out of the experiment to release any resources still held by them." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "experiment.close()\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3.10.8 64-bit", + "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.10.10" + }, + "orig_nbformat": 4, + "vscode": { + "interpreter": { + "hash": "949777d72b0d2535278d3dc13498b2535136f6dfe0678499012e853ee9abcab1" + } + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/src/gevopy/VERSION b/src/gevopy/VERSION index e6d5cb8..1cc5f65 100755 --- a/src/gevopy/VERSION +++ b/src/gevopy/VERSION @@ -1 +1 @@ -1.0.2 \ No newline at end of file +1.1.0 \ No newline at end of file diff --git a/src/gevopy/experiments.py b/src/gevopy/experiments.py index d0deeaf..4b0f3a1 100644 --- a/src/gevopy/experiments.py +++ b/src/gevopy/experiments.py @@ -14,10 +14,9 @@ import contextlib import logging import uuid -from typing import List, Optional +from typing import List -from pydantic import (BaseModel, Extra, Field, PositiveInt, PrivateAttr, - root_validator) +from pydantic import BaseModel, Extra, Field, PrivateAttr import gevopy.algorithms import gevopy.database @@ -44,13 +43,9 @@ class Experiment(BaseModel): """Base class for evolution experiments. Provides the essential attributes to create and run an experiment. - :param fitness: Fitness instance to evaluate phenotypes - :param algorithm: Algorithm instance to evolve phenotypes :param name: Experiment name, if none, generates an uuid4 string :param database: Database interface object, defaults to EmptyInterface """ - fitness: gevopy.fitness.FitnessModel - algorithm: gevopy.algorithms.Algorithm = DEFAULT_ALGORITHM database: gevopy.database.Interface = EmptyInterface() name: str = Field(default_factory=lambda: str(uuid.uuid4())) _logger: logging.Logger = PrivateAttr() @@ -58,20 +53,13 @@ class Experiment(BaseModel): def __init__(self, **data): super().__init__(**data) self._logger = logging.getLogger(f"{__package__}.Experiment") - self._logger = self.Logger(self.logger, {"exp": self}) + self._logger = self.Logger(self._logger, {"exp": self}) class Logger(logging.LoggerAdapter): # pylint: disable=missing-class-docstring def process(self, msg, kwargs): return f"[{self.extra['exp'].name}]: {msg}", kwargs - @property - def logger(self): - """Experiment logger, used to trace and print experiment info. - :return: Experiment.Logger - """ - return self._logger - @contextlib.contextmanager def session(self, *args, **kwds): """Function to generate a context session to interface the experiment. @@ -79,15 +67,16 @@ def session(self, *args, **kwds): :param kwds: Same as database.session key arguments """ with self.database.session(*args, **kwds) as db_session: - self.logger.debug("Enter session with: %s %s", args, kwds) + self._logger.debug("Enter session with: %s %s", args, kwds) yield Session(experiment=self, database=db_session) - self.logger.debug("Exit session with: %s %s", args, kwds) + self._logger.debug("Exit session with: %s %s", args, kwds) def close(self, *args, **kwds): """Function to close the database interface driver. :param args: Same as database Driver close possitional arguments :param kwds: Same as database Driver close key arguments """ + self._logger.debug("Closing experiment with: %s %s", args, kwds) self.database.close(*args, **kwds) @@ -95,17 +84,38 @@ class Session(BaseModel): """Base class for evolution experiment sessions. Provides the essential attributes to prepare the execution conditions. :param experiment: Experiment name the session is linked with + :param fitness: Fitness instance to evaluate phenotypes + :param algorithm: Algorithm instance to evolve phenotypes :param database: Database interface session, """ - experiment: Experiment + fitness: gevopy.fitness.FitnessModel = None + algorithm: gevopy.algorithms.Algorithm = DEFAULT_ALGORITHM database: gevopy.database.SessionContainer + _logger: logging.Logger = PrivateAttr() _population: List[GenotypeModel] = PrivateAttr(default=[]) class Config: # pylint: disable=missing-class-docstring arbitrary_types_allowed = True + def __init__(self, experiment, **data): + super().__init__(experiment=experiment, **data) + self._logger = logging.getLogger(f"{__package__}.Experiment") + self._logger = self.Logger(self._logger, {"exp": experiment}) + + class Logger(logging.LoggerAdapter): + # pylint: disable=missing-class-docstring + def process(self, msg, kwargs): + return f"[{self.extra['exp'].name}]: {msg}", kwargs + + @property + def logger(self): + """Session logger, used to trace and print experiment info. + :return: Session.Logger + """ + return self._logger + def save_phenotypes(self, phenotypes): """Saves the phenotypes to the experiment database. :param phenotypes: List of phenotypes to add to the experiment @@ -115,14 +125,6 @@ def save_phenotypes(self, phenotypes): iserial_phenotypes = (p.dict(serialize=True) for p in phenotypes) self.database.add_phenotypes(iserial_phenotypes) # Iter to speed up - def run(self, **execution_kwds): - """Executes the algorithm until a stop condition is met. - :param max_generation: The maximum number of loops to run - :param max_score: The score required to stop the evolution - :return: Generated Execution instance - """ - return Execution(**execution_kwds).run(session=self) - def add_phenotypes(self, phenotypes, save=True): """Adds phenotypes to the experiment session population. :param phenotypes: List of phenotypes to add to the experiment @@ -168,6 +170,50 @@ def generate_offspring(self, algorithm, save=True): if save: self.save_phenotypes(self._population) + def reset_score(self, save=True): + """Resets the score of the current population of phenotypes. + :param save: Flag to save new population status in database + """ + for phenotype in self._population: + phenotype.score = None + if save: + self.save_phenotypes(self._population) + + def run(self, max_generation=None, max_score=None): + """Executes the algorithm until a stop condition is met. + :param max_generation: The maximum number of loops to run + :param max_score: The score required to stop the evolution + :return: Generated Execution instance + """ + if (max_generation is None) and (max_score is None): + raise ValueError('Either max_generation or max_score is required') + if max_generation and not isinstance(max_generation, int): + raise TypeError('Expected positive int for max_generation') + if max_generation and max_generation < 0: + raise ValueError('Expected positive int for max_generation') + if max_score and not isinstance(max_score, (float, int)): + raise TypeError('Expected int or float for max_score') + + execution = Execution(experiment=self.experiment) + logger = execution._logger + + try: + logger.info("Start of evolutionary experiment execution") + self.eval_phenotypes(self.fitness, save=True) # Evaluate 1st pop + execution.halloffame.update(self.get_phenotypes()) + while not execution.completed(max_generation, max_score): + execution.generation += 1 # Increase generation index + self.generate_offspring(self.algorithm, save=False) + self.eval_phenotypes(self.fitness, save=True) + execution.halloffame.update(self.get_phenotypes()) + logger.info("Completed cycle; %s", execution.best_score) + except Exception as err: + logger.error("Error %s raised during experiment execution", err) + raise err + else: + logger.info("Experiment execution completed successfully") + return execution + class Execution(BaseModel): """Base class for evolution algorithm execution. This class uses an @@ -177,10 +223,15 @@ class Execution(BaseModel): Note that if neither max_generation or max_score are defined, the constructor raises ValueError for required valid end conditions. """ - max_generation: Optional[PositiveInt] = None - max_score: Optional[float] halloffame: gevopy.tools.HallOfFame = gevopy.tools.HallOfFame(3) generation: int = 0 + _logger: logging.Logger = PrivateAttr() + + def __init__(self, experiment): + super().__init__() + logger_data = {"exp": experiment.name, "exe": self} + self._logger = logging.getLogger(f"{__package__}.Experiment") + self._logger = self.Logger(self._logger, logger_data) class Config: # pylint: disable=missing-class-docstring @@ -195,16 +246,6 @@ def __repr__(self) -> str: f" Best score: {self.best_score}\n" ) - @root_validator() - def check_max_gen_or_score(cls, values): - """Checks for valid end conditions in the Execution""" - # pylint: disable=no-self-argument - max_generation = values.get('max_generation') - max_score = values.get("max_score") - if (max_generation is None) and (max_score is None): - raise ValueError('Either max_generation or max_score is required') - return values - @property def best_score(self): """Best score reached by the evaluated phenotypes during the run. @@ -215,45 +256,21 @@ def best_score(self): except IndexError: # Empty if not started return None - class RunLogger(logging.LoggerAdapter): + class Logger(logging.LoggerAdapter): # pylint: disable=missing-class-docstring def process(self, msg, kwargs): return f"[gen:{self.extra['exe'].generation}]: {msg}", kwargs - def run(self, session): - """Executes the algorithm until a stop condition is met. - :param session: Experiment session used for the evolution execution - :return: Completed Execution instance (statistics) - """ - logger = self.RunLogger(session.experiment.logger, dict(exe=self)) - algorithm = session.experiment.algorithm - fitness = session.experiment.fitness - try: - logger.info("Start of evolutionary experiment execution") - session.eval_phenotypes(fitness, save=True) # Evaluate first pop - self.halloffame.update(session.get_phenotypes()) - while not self.completed: - self.generation += 1 # Increase generation index - session.generate_offspring(algorithm, save=False) - session.eval_phenotypes(fitness, save=True) - self.halloffame.update(session.get_phenotypes()) - logger.info("Completed cycle; %s", self.best_score) - except Exception as err: - logger.error("Error %s raised during experiment execution", err) - raise err - else: - logger.info("Experiment execution completed successfully") - return self - - @property - def completed(self): + def completed(self, max_generation, max_score): """Evaluates if the final generation or required score is reached. + :param max_generation: The maximum number of loops to run + :param max_score: The score required to stop the evolution :return: True if evolution conditions are met, False otherwise """ - if self.max_generation: - if self.generation and self.max_generation <= self.generation: + if max_generation: + if self.generation and max_generation <= self.generation: return True - if self.max_score: - if self.best_score and self.max_score <= self.best_score: + if max_score: + if self.best_score and max_score <= self.best_score: return True return False # If any of the defined diff --git a/tests/test_requirements/test_experiments.py b/tests/test_requirements/test_experiments.py index 3f8b59c..81d6be1 100755 --- a/tests/test_requirements/test_experiments.py +++ b/tests/test_requirements/test_experiments.py @@ -3,49 +3,22 @@ # pylint: disable=unused-argument import uuid +from inspect import ismethod -from pytest import fixture, mark, raises + +from pytest import fixture, mark import gevopy as ea -from gevopy.algorithms import Algorithm from gevopy.database import Interface -from gevopy.fitness import FitnessModel # Module fixtures --------------------------------------------------- -@fixture(scope="class", params=[5]) -def max_generation(request): - """Parametrization for the maximum generations to run on experiment""" - return request.param - - -@fixture(scope="class", params=[0.8]) -def max_score(request): - """Parametrization for the maximum score to achieve on experiment""" - return request.param - - @fixture(scope="class") def experiment_name(): """Fixture to generate an experiment name for testing""" return f"Experiment_{uuid.uuid4()}" -@fixture(scope="class") -def session(experiment, population): - """Fixture to open and return an experiment session for evolution""" - with experiment.session() as session: - session.add_phenotypes(population) - yield session - session.del_experiment() - - -@fixture(scope="class") -def execution(session, max_generation, max_score): - """Fixture to run an experiment and return execution""" - return session.run(max_generation=max_generation, max_score=max_score) - - # Requirements ------------------------------------------------------ class AttrRequirements: """Tests group for Experiment instances attributes""" @@ -55,16 +28,6 @@ def test_attr_name(self, experiment): assert hasattr(experiment, "name") assert isinstance(experiment.name, str) - def test_attr_fitness(self, experiment): - """Test fitness is instance of FitnessModel""" - assert hasattr(experiment, "fitness") - assert isinstance(experiment.fitness, FitnessModel) - - def test_attr_algorithm(self, experiment): - """Test algorithm is instance of Algorithm""" - assert hasattr(experiment, "algorithm") - assert isinstance(experiment.algorithm, Algorithm) - @mark.parametrize("db_interface", ["Neo4jInterface"], indirect=True) def test_neo4j_database(self, experiment): """Test experiment has a correct 'database' attribute""" @@ -81,48 +44,22 @@ def test_none_database(self, experiment): class ExecRequirements: """Tests group for experiment execution""" - @mark.parametrize("max_score", [None], indirect=True) - def test_generation_stop(self, execution, max_generation): - """Test execution stops when maximum generation is reached""" - assert execution.generation == max_generation - assert execution.best_score > 0.0 - - @mark.parametrize("max_generation", [None], indirect=True) - def test_score_stop(self, execution, max_score): - """Test execution stops when maximum score is reached""" - assert execution.best_score >= max_score - assert execution.generation >= 0 - - def test_attr_generation(self, execution): - """Test generation attr returns int after first execution""" - assert hasattr(execution, "generation") - assert isinstance(execution.generation, int) - - def test_attr_score(self, execution): - """Test score attr returns float/int after first execution""" - assert hasattr(execution, "best_score") - assert isinstance(execution.best_score, (float, int)) - - -class ErrRequirements: - """Tests group for experiment exceptions requirements""" + def test_context_session(self, experiment): + """Test experiment has a session context manager""" + assert hasattr(experiment, "session") + assert ismethod(experiment.session) # TODO: Test context manager - def test_unknown_args(self, session, max_score): - """Test score attr returns float/int after first execution""" - with raises(ValueError): - session.run(unknown_kwarg="something", max_score=max_score) + def test_method_close(self, experiment): + """Test experiment can be closed when experiment ends""" + assert hasattr(experiment, "close") + assert ismethod(experiment.close) # TODO: Test correct closing # Parametrization --------------------------------------------------- -class TestExperiments(AttrRequirements, ExecRequirements, ErrRequirements): +class TestExperiments(AttrRequirements, ExecRequirements): """Parametrization for genetic evolution 'Experiment'""" @fixture(scope="class") - def experiment(self, experiment_name, db_interface, fitness, algorithm): + def experiment(self, experiment_name, db_interface): """Parametrization to define the algorithm configuration to use""" - return ea.Experiment( - name=experiment_name, - database=db_interface, - fitness=fitness(), - algorithm=algorithm(), - ) + return ea.Experiment(name=experiment_name, database=db_interface) diff --git a/tests/test_requirements/test_sessions.py b/tests/test_requirements/test_sessions.py new file mode 100755 index 0000000..029e958 --- /dev/null +++ b/tests/test_requirements/test_sessions.py @@ -0,0 +1,109 @@ +"""Module to test evolution experiment sessions""" +# pylint: disable=redefined-outer-name +# pylint: disable=unused-argument + +from inspect import ismethod + +from pytest import fixture, mark, raises + +import gevopy as ea +from gevopy.algorithms import Algorithm +from gevopy.fitness import FitnessModel + + +# Module fixtures --------------------------------------------------- +@fixture(scope="class", params=[5]) +def max_generation(request): + """Parametrization for the maximum generations to run on session""" + return request.param + + +@fixture(scope="class", params=[0.8]) +def max_score(request): + """Parametrization for the maximum score to achieve on session""" + return request.param + + +@fixture(scope="class") +def experiment(db_interface): + """Fixture to generate an experiment for testing""" + return ea.Experiment(database=db_interface) + + +@fixture(scope="class") +def execution(session, max_generation, max_score): + """Fixture to run an experiment session and return execution""" + return session.run(max_generation=max_generation, max_score=max_score) + + +# Requirements ------------------------------------------------------ +class AttrRequirements: + """Tests group for Session instances attributes""" + + def test_attr_fitness(self, session): + """Test fitness is instance of FitnessModel""" + assert hasattr(session, "fitness") + assert isinstance(session.fitness, FitnessModel) + + def test_attr_algorithm(self, session): + """Test algorithm is instance of Algorithm""" + assert hasattr(session, "algorithm") + assert isinstance(session.algorithm, Algorithm) + + +class ExecRequirements: + """Tests group for session execution""" + + @mark.parametrize("max_score", [None], indirect=True) + def test_generation_stop(self, execution, max_generation): + """Test execution stops when maximum generation is reached""" + assert execution.generation == max_generation + assert execution.best_score > 0.0 + + @mark.parametrize("max_generation", [None], indirect=True) + def test_score_stop(self, execution, max_score): + """Test execution stops when maximum score is reached""" + assert execution.best_score >= max_score + assert execution.generation >= 0 + + def test_attr_generation(self, execution): + """Test generation attr returns int after first execution""" + assert hasattr(execution, "generation") + assert isinstance(execution.generation, int) + + def test_attr_score(self, execution): + """Test score attr returns float/int after first execution""" + assert hasattr(execution, "best_score") + assert isinstance(execution.best_score, (float, int)) + + +class ErrRequirements: + """Tests group for session exceptions requirements""" + + def test_unknown_args(self, session, max_score): + """Test score attr returns float/int after first execution""" + with raises(TypeError): + session.run(unknown_kwarg="something", max_score=max_score) + + +# Parametrization --------------------------------------------------- +class TestSessions(AttrRequirements, ExecRequirements, ErrRequirements): + """Parametrization for genetic evolution 'Session'""" + + @fixture(scope="class") + def session(self, experiment, population, algorithm, fitness): + """Fixture to open and return an experiment session for evolution""" + with experiment.session() as session: + session.add_phenotypes(population) + session.algorithm = algorithm() + session.fitness = fitness() + yield session + session.del_experiment() + + def test_reset_score(self, session): + """Test reset_score resets phenotype scores""" + session.run(max_generation=1) + assert hasattr(session, "reset_score") + assert ismethod(session.reset_score) + session.reset_score() + assert all(x.score is None for x in session._population)