From 2750bb9b908713cbf201782e9f6e9ab8e0167af5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20=C5=9Aliwak?= Date: Thu, 19 Mar 2020 17:08:53 +0100 Subject: [PATCH 01/10] [yul-phaser] TestHelpers: Generic operator << for printing tuples in boost tests --- test/yulPhaser/TestHelpers.h | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/test/yulPhaser/TestHelpers.h b/test/yulPhaser/TestHelpers.h index 2bf755b18..514d2eda7 100644 --- a/test/yulPhaser/TestHelpers.h +++ b/test/yulPhaser/TestHelpers.h @@ -33,12 +33,39 @@ #include #include +#include + #include #include #include #include +#include #include +// OPERATORS FOR BOOST::TEST + +/// Output operator for arbitrary two-element tuples. +/// Necessary to make BOOST_TEST() work with such tuples. +template +std::ostream& operator<<(std::ostream& _output, std::tuple const& _tuple) +{ + _output << "(" << std::get<0>(_tuple) << ", " << std::get<1>(_tuple) << ")"; + return _output; +} + +namespace boost::test_tools::tt_detail +{ + +// Boost won't find find the << operator unless we put it in the std namespace which is illegal. +// The recommended solution is to overload print_log_value<> struct and make it use our global operator. +template +struct print_log_value> +{ + void operator()(std::ostream& _output, std::tuple const& _tuple) { ::operator<<(_output, _tuple); } +}; + +} + namespace solidity::phaser::test { From 7381068dcc2932eed964d61302cf8cf07dc7ed55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20=C5=9Aliwak?= Date: Wed, 11 Mar 2020 03:01:09 +0100 Subject: [PATCH 02/10] [yul-phaser] Make the Population constructor that takes individuals public and use it to speed up some operations --- test/yulPhaser/Population.cpp | 17 +++++++++++++++++ tools/yulPhaser/AlgorithmRunner.cpp | 6 +++--- tools/yulPhaser/Population.h | 7 +++---- 3 files changed, 23 insertions(+), 7 deletions(-) diff --git a/test/yulPhaser/Population.cpp b/test/yulPhaser/Population.cpp index 7a9172749..0cb0fe29f 100644 --- a/test/yulPhaser/Population.cpp +++ b/test/yulPhaser/Population.cpp @@ -104,6 +104,23 @@ BOOST_FIXTURE_TEST_CASE(constructor_should_copy_chromosomes_compute_fitness_and_ BOOST_TEST(individuals[2].chromosome == chromosomes[1]); } +BOOST_FIXTURE_TEST_CASE(constructor_should_accept_individuals_without_recalculating_fitness, PopulationFixture) +{ + vector customIndividuals = { + Individual(Chromosome("aaaccc"), 20), + Individual(Chromosome("aaa"), 10), + Individual(Chromosome("aaaf"), 30), + }; + assert(customIndividuals[0].fitness != m_fitnessMetric->evaluate(customIndividuals[0].chromosome)); + assert(customIndividuals[1].fitness != m_fitnessMetric->evaluate(customIndividuals[1].chromosome)); + assert(customIndividuals[2].fitness != m_fitnessMetric->evaluate(customIndividuals[2].chromosome)); + + Population population(m_fitnessMetric, customIndividuals); + + vector expectedIndividuals{customIndividuals[1], customIndividuals[0], customIndividuals[2]}; + BOOST_TEST(population.individuals() == expectedIndividuals); +} + BOOST_FIXTURE_TEST_CASE(makeRandom_should_get_chromosome_lengths_from_specified_generator, PopulationFixture) { size_t chromosomeCount = 30; diff --git a/tools/yulPhaser/AlgorithmRunner.cpp b/tools/yulPhaser/AlgorithmRunner.cpp index c402e5d84..402735e0d 100644 --- a/tools/yulPhaser/AlgorithmRunner.cpp +++ b/tools/yulPhaser/AlgorithmRunner.cpp @@ -187,16 +187,16 @@ Population AlgorithmRunner::randomiseDuplicates( if (_population.individuals().size() == 0) return _population; - vector chromosomes{_population.individuals()[0].chromosome}; + vector individuals{_population.individuals()[0]}; size_t duplicateCount = 0; for (size_t i = 1; i < _population.individuals().size(); ++i) if (_population.individuals()[i].chromosome == _population.individuals()[i - 1].chromosome) ++duplicateCount; else - chromosomes.push_back(_population.individuals()[i].chromosome); + individuals.push_back(_population.individuals()[i]); return ( - Population(_population.fitnessMetric(), chromosomes) + + Population(_population.fitnessMetric(), individuals) + Population::makeRandom(_population.fitnessMetric(), duplicateCount, _minChromosomeLength, _maxChromosomeLength) ); } diff --git a/tools/yulPhaser/Population.h b/tools/yulPhaser/Population.h index 40d51498b..cf82a5aa8 100644 --- a/tools/yulPhaser/Population.h +++ b/tools/yulPhaser/Population.h @@ -81,6 +81,9 @@ public: _fitnessMetric, chromosomesToIndividuals(*_fitnessMetric, std::move(_chromosomes)) ) {} + explicit Population(std::shared_ptr _fitnessMetric, std::vector _individuals): + m_fitnessMetric(std::move(_fitnessMetric)), + m_individuals{sortedIndividuals(std::move(_individuals))} {} static Population makeRandom( std::shared_ptr _fitnessMetric, @@ -112,10 +115,6 @@ public: friend std::ostream& operator<<(std::ostream& _stream, Population const& _population); private: - explicit Population(std::shared_ptr _fitnessMetric, std::vector _individuals): - m_fitnessMetric(std::move(_fitnessMetric)), - m_individuals{sortedIndividuals(std::move(_individuals))} {} - static std::vector chromosomesToIndividuals( FitnessMetric& _fitnessMetric, std::vector _chromosomes From b6f8ecf755df9a112e4be6212ed8eff4e65a0aec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20=C5=9Aliwak?= Date: Wed, 11 Mar 2020 02:07:54 +0100 Subject: [PATCH 03/10] [yul-phaser] Selections+PairSelections: Add RandomSubset and PairsFromRandomSubset --- test/yulPhaser/PairSelections.cpp | 72 ++++++++++++++++++++++++++++++ test/yulPhaser/Selections.cpp | 56 +++++++++++++++++++++++ tools/yulPhaser/PairSelections.cpp | 38 ++++++++++++++++ tools/yulPhaser/PairSelections.h | 22 +++++++++ tools/yulPhaser/Selections.cpp | 10 +++++ tools/yulPhaser/Selections.h | 22 +++++++++ 6 files changed, 220 insertions(+) diff --git a/test/yulPhaser/PairSelections.cpp b/test/yulPhaser/PairSelections.cpp index 64109470f..62a4dd4bc 100644 --- a/test/yulPhaser/PairSelections.cpp +++ b/test/yulPhaser/PairSelections.cpp @@ -119,6 +119,78 @@ BOOST_AUTO_TEST_CASE(materialise_should_return_no_pairs_if_collection_has_one_el BOOST_TEST(RandomPairSelection(2.0).materialise(1).empty()); } +BOOST_AUTO_TEST_SUITE_END() +BOOST_AUTO_TEST_SUITE(PairsFromRandomSubsetTest) + +BOOST_AUTO_TEST_CASE(materialise_should_return_random_values_with_equal_probabilities) +{ + constexpr int collectionSize = 1000; + constexpr double selectionChance = 0.7; + constexpr double relativeTolerance = 0.001; + constexpr double expectedValue = selectionChance; + constexpr double variance = selectionChance * (1 - selectionChance); + + SimulationRNG::reset(1); + vector> pairs = PairsFromRandomSubset(selectionChance).materialise(collectionSize); + vector bernoulliTrials(collectionSize, 0); + for (auto& pair: pairs) + { + BOOST_REQUIRE(get<1>(pair) < collectionSize); + BOOST_REQUIRE(get<1>(pair) < collectionSize); + bernoulliTrials[get<0>(pair)] = 1.0; + bernoulliTrials[get<1>(pair)] = 1.0; + } + + BOOST_TEST(abs(mean(bernoulliTrials) - expectedValue) < expectedValue * relativeTolerance); + BOOST_TEST(abs(meanSquaredError(bernoulliTrials, expectedValue) - variance) < variance * relativeTolerance); +} + +BOOST_AUTO_TEST_CASE(materialise_should_return_only_values_that_can_be_used_as_collection_indices) +{ + const size_t collectionSize = 200; + constexpr double selectionChance = 0.5; + + vector> pairs = PairsFromRandomSubset(selectionChance).materialise(collectionSize); + + BOOST_TEST(all_of(pairs.begin(), pairs.end(), [&](auto const& pair){ return get<0>(pair) <= collectionSize; })); + BOOST_TEST(all_of(pairs.begin(), pairs.end(), [&](auto const& pair){ return get<1>(pair) <= collectionSize; })); +} + +BOOST_AUTO_TEST_CASE(materialise_should_use_unique_indices) +{ + constexpr size_t collectionSize = 200; + constexpr double selectionChance = 0.5; + + vector> pairs = PairsFromRandomSubset(selectionChance).materialise(collectionSize); + set indices; + for (auto& pair: pairs) + { + indices.insert(get<0>(pair)); + indices.insert(get<1>(pair)); + } + + BOOST_TEST(indices.size() == 2 * pairs.size()); +} + +BOOST_AUTO_TEST_CASE(materialise_should_return_no_indices_if_collection_is_empty) +{ + BOOST_TEST(PairsFromRandomSubset(0.0).materialise(0).empty()); + BOOST_TEST(PairsFromRandomSubset(0.5).materialise(0).empty()); + BOOST_TEST(PairsFromRandomSubset(1.0).materialise(0).empty()); +} + +BOOST_AUTO_TEST_CASE(materialise_should_return_no_pairs_if_selection_chance_is_zero) +{ + BOOST_TEST(PairsFromRandomSubset(0.0).materialise(0).empty()); + BOOST_TEST(PairsFromRandomSubset(0.0).materialise(100).empty()); +} + +BOOST_AUTO_TEST_CASE(materialise_should_return_all_pairs_if_selection_chance_is_one) +{ + BOOST_TEST(PairsFromRandomSubset(1.0).materialise(0).empty()); + BOOST_TEST(PairsFromRandomSubset(1.0).materialise(100).size() == 50); +} + BOOST_AUTO_TEST_SUITE_END() BOOST_AUTO_TEST_SUITE(PairMosaicSelectionTest) diff --git a/test/yulPhaser/Selections.cpp b/test/yulPhaser/Selections.cpp index 02a85f4f3..d25766ae6 100644 --- a/test/yulPhaser/Selections.cpp +++ b/test/yulPhaser/Selections.cpp @@ -25,9 +25,11 @@ #include #include +#include #include using namespace std; +using namespace solidity::util; namespace solidity::phaser::test { @@ -199,6 +201,60 @@ BOOST_AUTO_TEST_CASE(materialise_should_return_no_indices_if_collection_is_empty BOOST_TEST(RandomSelection(2.0).materialise(0).empty()); } +BOOST_AUTO_TEST_SUITE_END() +BOOST_AUTO_TEST_SUITE(RandomSubsetTest) + +BOOST_AUTO_TEST_CASE(materialise_should_return_random_values_with_equal_probabilities) +{ + constexpr int collectionSize = 1000; + constexpr double selectionChance = 0.7; + constexpr double relativeTolerance = 0.001; + constexpr double expectedValue = selectionChance; + constexpr double variance = selectionChance * (1 - selectionChance); + + SimulationRNG::reset(1); + auto indices = convertContainer>(RandomSubset(selectionChance).materialise(collectionSize)); + + vector bernoulliTrials(collectionSize); + for (size_t i = 0; i < collectionSize; ++i) + bernoulliTrials[i] = indices.count(i); + + BOOST_TEST(abs(mean(bernoulliTrials) - expectedValue) < expectedValue * relativeTolerance); + BOOST_TEST(abs(meanSquaredError(bernoulliTrials, expectedValue) - variance) < variance * relativeTolerance); +} + +BOOST_AUTO_TEST_CASE(materialise_should_return_only_values_that_can_be_used_as_collection_indices) +{ + const size_t collectionSize = 200; + vector indices = RandomSubset(0.5).materialise(collectionSize); + + BOOST_TEST(all_of(indices.begin(), indices.end(), [&](auto const& index){ return index <= collectionSize; })); +} + +BOOST_AUTO_TEST_CASE(materialise_should_return_indices_in_the_same_order_they_are_in_the_container) +{ + const size_t collectionSize = 200; + vector indices = RandomSubset(0.5).materialise(collectionSize); + + for (size_t i = 1; i < indices.size(); ++i) + BOOST_TEST(indices[i - 1] < indices[i]); +} + +BOOST_AUTO_TEST_CASE(materialise_should_return_no_indices_if_collection_is_empty) +{ + BOOST_TEST(RandomSubset(0.5).materialise(0).empty()); +} + +BOOST_AUTO_TEST_CASE(materialise_should_return_no_indices_if_selection_chance_is_zero) +{ + BOOST_TEST(RandomSubset(0.0).materialise(10).empty()); +} + +BOOST_AUTO_TEST_CASE(materialise_should_return_all_indices_if_selection_chance_is_one) +{ + BOOST_TEST(RandomSubset(1.0).materialise(10).size() == 10); +} + BOOST_AUTO_TEST_SUITE_END() BOOST_AUTO_TEST_SUITE_END() BOOST_AUTO_TEST_SUITE_END() diff --git a/tools/yulPhaser/PairSelections.cpp b/tools/yulPhaser/PairSelections.cpp index 8f3fd0f7f..fe85fef9c 100644 --- a/tools/yulPhaser/PairSelections.cpp +++ b/tools/yulPhaser/PairSelections.cpp @@ -17,6 +17,7 @@ #include +#include #include #include @@ -47,6 +48,43 @@ vector> RandomPairSelection::materialise(size_t _poolSize) return selection; } +vector> PairsFromRandomSubset::materialise(size_t _poolSize) const +{ + vector selectedIndices = RandomSubset(m_selectionChance).materialise(_poolSize); + + if (selectedIndices.size() % 2 != 0) + { + if (selectedIndices.size() < _poolSize && SimulationRNG::bernoulliTrial(0.5)) + { + do + { + size_t extraIndex = SimulationRNG::uniformInt(0, selectedIndices.size() - 1); + if (find(selectedIndices.begin(), selectedIndices.end(), extraIndex) == selectedIndices.end()) + selectedIndices.push_back(extraIndex); + } while (selectedIndices.size() % 2 != 0); + } + else + selectedIndices.erase(selectedIndices.begin() + SimulationRNG::uniformInt(0, selectedIndices.size() - 1)); + } + assert(selectedIndices.size() % 2 == 0); + + vector> selectedPairs; + for (size_t i = selectedIndices.size() / 2; i > 0; --i) + { + size_t position1 = SimulationRNG::uniformInt(0, selectedIndices.size() - 1); + size_t value1 = selectedIndices[position1]; + selectedIndices.erase(selectedIndices.begin() + position1); + size_t position2 = SimulationRNG::uniformInt(0, selectedIndices.size() - 1); + size_t value2 = selectedIndices[position2]; + selectedIndices.erase(selectedIndices.begin() + position2); + + selectedPairs.push_back({value1, value2}); + } + assert(selectedIndices.size() == 0); + + return selectedPairs; +} + vector> PairMosaicSelection::materialise(size_t _poolSize) const { if (_poolSize < 2) diff --git a/tools/yulPhaser/PairSelections.h b/tools/yulPhaser/PairSelections.h index 7778d6567..a80fff897 100644 --- a/tools/yulPhaser/PairSelections.h +++ b/tools/yulPhaser/PairSelections.h @@ -69,6 +69,28 @@ private: double m_selectionSize; }; + +/** + * A selection that goes over all elements in a container, for each one independently decides + * whether to select it or not and then randomly combines those elements into pairs. If the number + * of elements is odd, randomly decides whether to take one more or exclude one. + * + * Each element has the same chance of being selected and can be selected at most once. + * The number of selected elements is random and can be different with each call to + * @a materialise(). + */ +class PairsFromRandomSubset: public PairSelection +{ +public: + explicit PairsFromRandomSubset(double _selectionChance): + m_selectionChance(_selectionChance) {} + + std::vector> materialise(size_t _poolSize) const override; + +private: + double m_selectionChance; +}; + /** * A selection that selects pairs of elements at specific, fixed positions indicated by a repeating * "pattern". If the positions in the pattern exceed the size of the container, they are capped at diff --git a/tools/yulPhaser/Selections.cpp b/tools/yulPhaser/Selections.cpp index abc080fde..920a7d7f1 100644 --- a/tools/yulPhaser/Selections.cpp +++ b/tools/yulPhaser/Selections.cpp @@ -20,6 +20,7 @@ #include #include +#include using namespace std; using namespace solidity::phaser; @@ -58,3 +59,12 @@ vector RandomSelection::materialise(size_t _poolSize) const return selection; } +vector RandomSubset::materialise(size_t _poolSize) const +{ + vector selection; + for (size_t index = 0; index < _poolSize; ++index) + if (SimulationRNG::bernoulliTrial(m_selectionChance)) + selection.push_back(index); + + return selection; +} diff --git a/tools/yulPhaser/Selections.h b/tools/yulPhaser/Selections.h index 46d975bbd..a0ed2657f 100644 --- a/tools/yulPhaser/Selections.h +++ b/tools/yulPhaser/Selections.h @@ -118,4 +118,26 @@ private: double m_selectionSize; }; +/** + * A selection that goes over all elements in a container, for each one independently deciding + * whether to select it or not. Each element has the same chance of being selected and can be + * selected at most once. The order of selected elements is the same as the order of elements in + * the container. The number of selected elements is random and can be different with each call + * to @a materialise(). + */ +class RandomSubset: public Selection +{ +public: + explicit RandomSubset(double _selectionChance): + m_selectionChance(_selectionChance) + { + assert(0.0 <= _selectionChance && _selectionChance <= 1.0); + } + + std::vector materialise(size_t _poolSize) const override; + +private: + double m_selectionChance; +}; + } From 0837a62d5c0d40abae7884a0bc111dd8a64590e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20=C5=9Aliwak?= Date: Wed, 11 Mar 2020 02:11:58 +0100 Subject: [PATCH 04/10] [yul-phaser] Mutations: Add symmetricRandomPointCrossover() --- test/yulPhaser/Mutations.cpp | 14 +++++++++++++ tools/yulPhaser/Mutations.cpp | 37 ++++++++++++++++++++++++++++------- tools/yulPhaser/Mutations.h | 7 +++++++ 3 files changed, 51 insertions(+), 7 deletions(-) diff --git a/test/yulPhaser/Mutations.cpp b/test/yulPhaser/Mutations.cpp index df58cec54..d98fa7604 100644 --- a/test/yulPhaser/Mutations.cpp +++ b/test/yulPhaser/Mutations.cpp @@ -225,6 +225,20 @@ BOOST_AUTO_TEST_CASE(randomPointCrossover_should_swap_chromosome_parts_at_random BOOST_TEST(result2 == Chromosome("cccaaaaaaa")); } +BOOST_AUTO_TEST_CASE(symmetricRandomPointCrossover_should_swap_chromosome_parts_at_random_point) +{ + function crossover = symmetricRandomPointCrossover(); + + SimulationRNG::reset(1); + tuple result1 = crossover(Chromosome("aaaaaaaaaa"), Chromosome("cccccc")); + tuple expectedPair1 = {Chromosome("aaaccc"), Chromosome("cccaaaaaaa")}; + BOOST_TEST(result1 == expectedPair1); + + tuple result2 = crossover(Chromosome("cccccc"), Chromosome("aaaaaaaaaa")); + tuple expectedPair2 = {Chromosome("ccccccaaaa"), Chromosome("aaaaaa")}; + BOOST_TEST(result2 == expectedPair2); +} + BOOST_AUTO_TEST_CASE(randomPointCrossover_should_only_consider_points_available_on_both_chromosomes) { SimulationRNG::reset(1); diff --git a/tools/yulPhaser/Mutations.cpp b/tools/yulPhaser/Mutations.cpp index 86f815198..9cf741707 100644 --- a/tools/yulPhaser/Mutations.cpp +++ b/tools/yulPhaser/Mutations.cpp @@ -98,7 +98,7 @@ function phaser::alternativeMutations( namespace { -Chromosome buildChromosomesBySwappingParts( +ChromosomePair fixedPointSwap( Chromosome const& _chromosome1, Chromosome const& _chromosome2, size_t _crossoverPoint @@ -109,11 +109,19 @@ Chromosome buildChromosomesBySwappingParts( auto begin1 = _chromosome1.optimisationSteps().begin(); auto begin2 = _chromosome2.optimisationSteps().begin(); + auto end1 = _chromosome1.optimisationSteps().end(); + auto end2 = _chromosome2.optimisationSteps().end(); - return Chromosome( - vector(begin1, begin1 + _crossoverPoint) + - vector(begin2 + _crossoverPoint, _chromosome2.optimisationSteps().end()) - ); + return { + Chromosome( + vector(begin1, begin1 + _crossoverPoint) + + vector(begin2 + _crossoverPoint, end2) + ), + Chromosome( + vector(begin2, begin2 + _crossoverPoint) + + vector(begin1 + _crossoverPoint, end1) + ), + }; } } @@ -129,7 +137,22 @@ function phaser::randomPointCrossover() assert(minPoint <= minLength); size_t randomPoint = SimulationRNG::uniformInt(minPoint, minLength); - return buildChromosomesBySwappingParts(_chromosome1, _chromosome2, randomPoint); + return get<0>(fixedPointSwap(_chromosome1, _chromosome2, randomPoint)); + }; +} + +function phaser::symmetricRandomPointCrossover() +{ + return [=](Chromosome const& _chromosome1, Chromosome const& _chromosome2) + { + size_t minLength = min(_chromosome1.length(), _chromosome2.length()); + + // Don't use position 0 (because this just swaps the values) unless it's the only choice. + size_t minPoint = (minLength > 0? 1 : 0); + assert(minPoint <= minLength); + + size_t randomPoint = SimulationRNG::uniformInt(minPoint, minLength); + return fixedPointSwap(_chromosome1, _chromosome2, randomPoint); }; } @@ -142,6 +165,6 @@ function phaser::fixedPointCrossover(double _crossoverPoint) size_t minLength = min(_chromosome1.length(), _chromosome2.length()); size_t concretePoint = static_cast(round(minLength * _crossoverPoint)); - return buildChromosomesBySwappingParts(_chromosome1, _chromosome2, concretePoint); + return get<0>(fixedPointSwap(_chromosome1, _chromosome2, concretePoint)); }; } diff --git a/tools/yulPhaser/Mutations.h b/tools/yulPhaser/Mutations.h index bff48c52b..5554a1d2d 100644 --- a/tools/yulPhaser/Mutations.h +++ b/tools/yulPhaser/Mutations.h @@ -28,8 +28,11 @@ namespace solidity::phaser { +using ChromosomePair = std::tuple; + using Mutation = Chromosome(Chromosome const&); using Crossover = Chromosome(Chromosome const&, Chromosome const&); +using SymmetricCrossover = ChromosomePair(Chromosome const&, Chromosome const&); // MUTATIONS @@ -61,6 +64,10 @@ std::function alternativeMutations( /// position at which to perform perform @a fixedPointCrossover. std::function randomPointCrossover(); +/// Symmetric version of @a randomPointCrossover(). Creates an operator that returns a pair +/// containing both possible results for the same crossover point. +std::function symmetricRandomPointCrossover(); + /// Creates a crossover operator that always chooses a point that lies at @a _crossoverPoint /// percent of the length of the shorter chromosome. Then creates a new chromosome by /// splitting both inputs at the crossover point and stitching output from the first half or first From 59011fcde6586150148792e3309cd3e15fe9e74a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20=C5=9Aliwak?= Date: Wed, 11 Mar 2020 23:45:46 +0100 Subject: [PATCH 05/10] [yul-phaser] Mutations: Add mutationSequence() --- test/yulPhaser/Mutations.cpp | 33 +++++++++++++++++++++++++++++++++ tools/yulPhaser/Mutations.cpp | 12 ++++++++++++ tools/yulPhaser/Mutations.h | 3 +++ 3 files changed, 48 insertions(+) diff --git a/test/yulPhaser/Mutations.cpp b/test/yulPhaser/Mutations.cpp index d98fa7604..33c623f84 100644 --- a/test/yulPhaser/Mutations.cpp +++ b/test/yulPhaser/Mutations.cpp @@ -212,6 +212,39 @@ BOOST_AUTO_TEST_CASE(alternativeMutations_should_always_choose_second_mutation_i BOOST_TEST(mutation(chromosome) == Chromosome("f")); } +BOOST_AUTO_TEST_CASE(mutationSequence_should_apply_all_mutations) +{ + Chromosome chromosome("aaaaa"); + function mutation = mutationSequence({ + geneSubstitution(3, Chromosome("g").optimisationSteps()[0]), + geneSubstitution(2, Chromosome("f").optimisationSteps()[0]), + geneSubstitution(1, Chromosome("c").optimisationSteps()[0]), + }); + + BOOST_TEST(mutation(chromosome) == Chromosome("acfga")); +} + +BOOST_AUTO_TEST_CASE(mutationSequence_apply_mutations_in_the_order_they_are_given) +{ + Chromosome chromosome("aa"); + function mutation = mutationSequence({ + geneSubstitution(0, Chromosome("g").optimisationSteps()[0]), + geneSubstitution(1, Chromosome("c").optimisationSteps()[0]), + geneSubstitution(0, Chromosome("f").optimisationSteps()[0]), + geneSubstitution(1, Chromosome("o").optimisationSteps()[0]), + }); + + BOOST_TEST(mutation(chromosome) == Chromosome("fo")); +} + +BOOST_AUTO_TEST_CASE(mutationSequence_should_return_unmodified_chromosome_if_given_no_mutations) +{ + Chromosome chromosome("aa"); + function mutation = mutationSequence({}); + + BOOST_TEST(mutation(chromosome) == chromosome); +} + BOOST_AUTO_TEST_CASE(randomPointCrossover_should_swap_chromosome_parts_at_random_point) { function crossover = randomPointCrossover(); diff --git a/tools/yulPhaser/Mutations.cpp b/tools/yulPhaser/Mutations.cpp index 9cf741707..98689a810 100644 --- a/tools/yulPhaser/Mutations.cpp +++ b/tools/yulPhaser/Mutations.cpp @@ -95,6 +95,18 @@ function phaser::alternativeMutations( }; } +function phaser::mutationSequence(vector> _mutations) +{ + return [=](Chromosome const& _chromosome) + { + Chromosome mutatedChromosome = _chromosome; + for (size_t i = 0; i < _mutations.size(); ++i) + mutatedChromosome = _mutations[i](move(mutatedChromosome)); + + return mutatedChromosome; + }; +} + namespace { diff --git a/tools/yulPhaser/Mutations.h b/tools/yulPhaser/Mutations.h index 5554a1d2d..d545aa093 100644 --- a/tools/yulPhaser/Mutations.h +++ b/tools/yulPhaser/Mutations.h @@ -58,6 +58,9 @@ std::function alternativeMutations( std::function _mutation2 ); +/// Creates a mutation operator that sequentially applies all the operators given in @a _mutations. +std::function mutationSequence(std::vector> _mutations); + // CROSSOVER /// Creates a crossover operator that randomly selects a number between 0 and 1 and uses it as the From ef8d0888af7accf12207c8288a9d82e11f5121cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20=C5=9Aliwak?= Date: Wed, 11 Mar 2020 02:13:55 +0100 Subject: [PATCH 06/10] [yul-phaser] Population: Add symmetricCrossoverWithRemainder() --- test/yulPhaser/Population.cpp | 55 ++++++++++++++++++++++++++++++++++ tools/yulPhaser/Population.cpp | 31 +++++++++++++++++++ tools/yulPhaser/Population.h | 4 +++ 3 files changed, 90 insertions(+) diff --git a/test/yulPhaser/Population.cpp b/test/yulPhaser/Population.cpp index 0cb0fe29f..12555845c 100644 --- a/test/yulPhaser/Population.cpp +++ b/test/yulPhaser/Population.cpp @@ -48,6 +48,14 @@ namespace solidity::phaser::test class PopulationFixture { protected: + static ChromosomePair twoStepSwap(Chromosome const& _chromosome1, Chromosome const& _chromosome2) + { + return ChromosomePair{ + Chromosome(vector{_chromosome1.optimisationSteps()[0], _chromosome2.optimisationSteps()[1]}), + Chromosome(vector{_chromosome2.optimisationSteps()[0], _chromosome1.optimisationSteps()[1]}), + }; + } + shared_ptr m_fitnessMetric = make_shared(); }; @@ -309,6 +317,53 @@ BOOST_FIXTURE_TEST_CASE(crossover_should_return_empty_population_if_selection_is BOOST_TEST(population.crossover(selection, fixedPointCrossover(0.5)).individuals().empty()); } +BOOST_FIXTURE_TEST_CASE(symmetricCrossoverWithRemainder_should_return_crossed_population_and_remainder, PopulationFixture) +{ + Population population(m_fitnessMetric, {Chromosome("aa"), Chromosome("cc"), Chromosome("gg"), Chromosome("hh")}); + PairMosaicSelection selection({{2, 1}}, 0.25); + assert(selection.materialise(population.individuals().size()) == (vector>{{2, 1}})); + + Population expectedCrossedPopulation(m_fitnessMetric, {Chromosome("gc"), Chromosome("cg")}); + Population expectedRemainder(m_fitnessMetric, {Chromosome("aa"), Chromosome("hh")}); + + BOOST_TEST( + population.symmetricCrossoverWithRemainder(selection, twoStepSwap) == + (tuple{expectedCrossedPopulation, expectedRemainder}) + ); +} + +BOOST_FIXTURE_TEST_CASE(symmetricCrossoverWithRemainder_should_allow_crossing_the_same_individual_multiple_times, PopulationFixture) +{ + Population population(m_fitnessMetric, {Chromosome("aa"), Chromosome("cc"), Chromosome("gg"), Chromosome("hh")}); + PairMosaicSelection selection({{0, 0}, {2, 1}}, 1.0); + assert(selection.materialise(population.individuals().size()) == (vector>{{0, 0}, {2, 1}, {0, 0}, {2, 1}})); + + Population expectedCrossedPopulation(m_fitnessMetric, { + Chromosome("aa"), Chromosome("aa"), + Chromosome("aa"), Chromosome("aa"), + Chromosome("gc"), Chromosome("cg"), + Chromosome("gc"), Chromosome("cg"), + }); + Population expectedRemainder(m_fitnessMetric, {Chromosome("hh")}); + + BOOST_TEST( + population.symmetricCrossoverWithRemainder(selection, twoStepSwap) == + (tuple{expectedCrossedPopulation, expectedRemainder}) + ); +} + +BOOST_FIXTURE_TEST_CASE(symmetricCrossoverWithRemainder_should_return_empty_population_if_selection_is_empty, PopulationFixture) +{ + Population population(m_fitnessMetric, {Chromosome("aa"), Chromosome("cc")}); + PairMosaicSelection selection({}, 0.0); + assert(selection.materialise(population.individuals().size()).empty()); + + BOOST_TEST( + population.symmetricCrossoverWithRemainder(selection, twoStepSwap) == + (tuple{Population(m_fitnessMetric), population}) + ); +} + BOOST_AUTO_TEST_SUITE_END() BOOST_AUTO_TEST_SUITE_END() diff --git a/tools/yulPhaser/Population.cpp b/tools/yulPhaser/Population.cpp index b336daf73..acdae1eb7 100644 --- a/tools/yulPhaser/Population.cpp +++ b/tools/yulPhaser/Population.cpp @@ -117,6 +117,37 @@ Population Population::crossover(PairSelection const& _selection, function Population::symmetricCrossoverWithRemainder( + PairSelection const& _selection, + function _symmetricCrossover +) const +{ + vector indexSelected(m_individuals.size(), false); + + vector crossedIndividuals; + for (auto const& [i, j]: _selection.materialise(m_individuals.size())) + { + auto children = _symmetricCrossover( + m_individuals[i].chromosome, + m_individuals[j].chromosome + ); + crossedIndividuals.emplace_back(move(get<0>(children)), *m_fitnessMetric); + crossedIndividuals.emplace_back(move(get<1>(children)), *m_fitnessMetric); + indexSelected[i] = true; + indexSelected[j] = true; + } + + vector remainder; + for (size_t i = 0; i < indexSelected.size(); ++i) + if (!indexSelected[i]) + remainder.emplace_back(m_individuals[i]); + + return { + Population(m_fitnessMetric, crossedIndividuals), + Population(m_fitnessMetric, remainder), + }; +} + namespace solidity::phaser { diff --git a/tools/yulPhaser/Population.h b/tools/yulPhaser/Population.h index cf82a5aa8..2d79dfef2 100644 --- a/tools/yulPhaser/Population.h +++ b/tools/yulPhaser/Population.h @@ -100,6 +100,10 @@ public: Population select(Selection const& _selection) const; Population mutate(Selection const& _selection, std::function _mutation) const; Population crossover(PairSelection const& _selection, std::function _crossover) const; + std::tuple symmetricCrossoverWithRemainder( + PairSelection const& _selection, + std::function _symmetricCrossover + ) const; friend Population operator+(Population _a, Population _b); From 8c86a4983d70eda377db17d3f4833698052c342b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20=C5=9Aliwak?= Date: Wed, 11 Mar 2020 02:14:30 +0100 Subject: [PATCH 07/10] [yul-phaser] Population: Add combine() --- test/yulPhaser/Population.cpp | 8 ++++++++ tools/yulPhaser/Population.cpp | 5 +++++ tools/yulPhaser/Population.h | 1 + 3 files changed, 14 insertions(+) diff --git a/test/yulPhaser/Population.cpp b/test/yulPhaser/Population.cpp index 12555845c..1657e97c9 100644 --- a/test/yulPhaser/Population.cpp +++ b/test/yulPhaser/Population.cpp @@ -364,6 +364,14 @@ BOOST_FIXTURE_TEST_CASE(symmetricCrossoverWithRemainder_should_return_empty_popu ); } +BOOST_FIXTURE_TEST_CASE(combine_should_add_two_populations_from_a_pair, PopulationFixture) +{ + Population population1(m_fitnessMetric, {Chromosome("aa"), Chromosome("hh")}); + Population population2(m_fitnessMetric, {Chromosome("gg"), Chromosome("cc")}); + + BOOST_TEST(Population::combine({population1, population2}) == population1 + population2); +} + BOOST_AUTO_TEST_SUITE_END() BOOST_AUTO_TEST_SUITE_END() diff --git a/tools/yulPhaser/Population.cpp b/tools/yulPhaser/Population.cpp index acdae1eb7..1b846122e 100644 --- a/tools/yulPhaser/Population.cpp +++ b/tools/yulPhaser/Population.cpp @@ -163,6 +163,11 @@ Population operator+(Population _a, Population _b) } +Population Population::combine(std::tuple _populationPair) +{ + return get<0>(_populationPair) + get<1>(_populationPair); +} + bool Population::operator==(Population const& _other) const { // We consider populations identical only if they share the same exact instance of the metric. diff --git a/tools/yulPhaser/Population.h b/tools/yulPhaser/Population.h index 2d79dfef2..c405f702c 100644 --- a/tools/yulPhaser/Population.h +++ b/tools/yulPhaser/Population.h @@ -106,6 +106,7 @@ public: ) const; friend Population operator+(Population _a, Population _b); + static Population combine(std::tuple _populationPair); std::shared_ptr fitnessMetric() { return m_fitnessMetric; } std::vector const& individuals() const { return m_individuals; } From 879f6e17e97bd772377471752b28f6e93afb1aca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20=C5=9Aliwak?= Date: Wed, 11 Mar 2020 02:29:20 +0100 Subject: [PATCH 08/10] [yul-phaser] GenerationalElitistWithExclusivePools: Refactor runNextRound() for less nesting and more readability --- tools/yulPhaser/GeneticAlgorithms.cpp | 36 +++++++++++++-------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/tools/yulPhaser/GeneticAlgorithms.cpp b/tools/yulPhaser/GeneticAlgorithms.cpp index 432f3fe38..c38c3bc95 100644 --- a/tools/yulPhaser/GeneticAlgorithms.cpp +++ b/tools/yulPhaser/GeneticAlgorithms.cpp @@ -43,24 +43,24 @@ Population RandomAlgorithm::runNextRound(Population _population) Population GenerationalElitistWithExclusivePools::runNextRound(Population _population) { double elitePoolSize = 1.0 - (m_options.mutationPoolSize + m_options.crossoverPoolSize); - RangeSelection elite(0.0, elitePoolSize); + + RangeSelection elitePool(0.0, elitePoolSize); + RandomSelection mutationPoolFromElite(m_options.mutationPoolSize / elitePoolSize); + RandomPairSelection crossoverPoolFromElite(m_options.crossoverPoolSize / elitePoolSize); + + std::function mutationOperator = alternativeMutations( + m_options.randomisationChance, + geneRandomisation(m_options.percentGenesToRandomise), + alternativeMutations( + m_options.deletionVsAdditionChance, + geneDeletion(m_options.percentGenesToAddOrDelete), + geneAddition(m_options.percentGenesToAddOrDelete) + ) + ); + std::function crossoverOperator = randomPointCrossover(); return - _population.select(elite) + - _population.select(elite).mutate( - RandomSelection(m_options.mutationPoolSize / elitePoolSize), - alternativeMutations( - m_options.randomisationChance, - geneRandomisation(m_options.percentGenesToRandomise), - alternativeMutations( - m_options.deletionVsAdditionChance, - geneDeletion(m_options.percentGenesToAddOrDelete), - geneAddition(m_options.percentGenesToAddOrDelete) - ) - ) - ) + - _population.select(elite).crossover( - RandomPairSelection(m_options.crossoverPoolSize / elitePoolSize), - randomPointCrossover() - ); + _population.select(elitePool) + + _population.select(elitePool).mutate(mutationPoolFromElite, mutationOperator) + + _population.select(elitePool).crossover(crossoverPoolFromElite, crossoverOperator); } From f6783c60b215f859133f0ab2c395c04bf183934f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20=C5=9Aliwak?= Date: Wed, 11 Mar 2020 02:15:25 +0100 Subject: [PATCH 09/10] [yul-phaser] GeneticAlgorithms: Add ClassicGeneticAlgorithm --- test/yulPhaser/GeneticAlgorithms.cpp | 204 ++++++++++++++++++++++++++ tools/yulPhaser/GeneticAlgorithms.cpp | 62 ++++++++ tools/yulPhaser/GeneticAlgorithms.h | 55 +++++++ 3 files changed, 321 insertions(+) diff --git a/test/yulPhaser/GeneticAlgorithms.cpp b/test/yulPhaser/GeneticAlgorithms.cpp index f7a5e7a92..1b902d978 100644 --- a/test/yulPhaser/GeneticAlgorithms.cpp +++ b/test/yulPhaser/GeneticAlgorithms.cpp @@ -31,6 +31,7 @@ using namespace std; using namespace boost::unit_test::framework; using namespace boost::test_tools; +using namespace solidity::util; namespace solidity::phaser::test { @@ -41,6 +42,18 @@ protected: shared_ptr m_fitnessMetric = make_shared(); }; +class ClassicGeneticAlgorithmFixture: public GeneticAlgorithmFixture +{ +protected: + ClassicGeneticAlgorithm::Options m_options = { + /* elitePoolSize = */ 0.0, + /* crossoverChance = */ 0.0, + /* mutationChance = */ 0.0, + /* deletionChance = */ 0.0, + /* additionChance = */ 0.0, + }; +}; + BOOST_AUTO_TEST_SUITE(Phaser) BOOST_AUTO_TEST_SUITE(GeneticAlgorithmsTest) BOOST_AUTO_TEST_SUITE(RandomAlgorithmTest) @@ -186,6 +199,197 @@ BOOST_FIXTURE_TEST_CASE(runNextRound_should_generate_individuals_in_the_crossove })); } +BOOST_AUTO_TEST_SUITE_END() +BOOST_AUTO_TEST_SUITE(ClassicGeneticAlgorithmTest) + +BOOST_FIXTURE_TEST_CASE(runNextRound_should_select_individuals_with_probability_proportional_to_fitness, ClassicGeneticAlgorithmFixture) +{ + constexpr double relativeTolerance = 0.1; + constexpr size_t populationSize = 1000; + assert(populationSize % 4 == 0 && "Choose a number divisible by 4 for this test"); + + auto population = + Population::makeRandom(m_fitnessMetric, populationSize / 4, 0, 0) + + Population::makeRandom(m_fitnessMetric, populationSize / 4, 1, 1) + + Population::makeRandom(m_fitnessMetric, populationSize / 4, 2, 2) + + Population::makeRandom(m_fitnessMetric, populationSize / 4, 3, 3); + + map expectedProbabilities = { + {0, 4.0 / (4 + 3 + 2 + 1)}, + {1, 3.0 / (4 + 3 + 2 + 1)}, + {2, 2.0 / (4 + 3 + 2 + 1)}, + {3, 1.0 / (4 + 3 + 2 + 1)}, + }; + double const expectedValue = ( + 0.0 * expectedProbabilities[0] + + 1.0 * expectedProbabilities[1] + + 2.0 * expectedProbabilities[2] + + 3.0 * expectedProbabilities[3] + ); + double const variance = ( + (0.0 - expectedValue) * (0.0 - expectedValue) * expectedProbabilities[0] + + (1.0 - expectedValue) * (1.0 - expectedValue) * expectedProbabilities[1] + + (2.0 - expectedValue) * (2.0 - expectedValue) * expectedProbabilities[2] + + (3.0 - expectedValue) * (3.0 - expectedValue) * expectedProbabilities[3] + ); + + ClassicGeneticAlgorithm algorithm(m_options); + Population newPopulation = algorithm.runNextRound(population); + + BOOST_TEST(newPopulation.individuals().size() == population.individuals().size()); + + vector newFitness = chromosomeLengths(newPopulation); + BOOST_TEST(abs(mean(newFitness) - expectedValue) < expectedValue * relativeTolerance); + BOOST_TEST(abs(meanSquaredError(newFitness, expectedValue) - variance) < variance * relativeTolerance); +} + +BOOST_FIXTURE_TEST_CASE(runNextRound_should_select_only_individuals_existing_in_the_original_population, ClassicGeneticAlgorithmFixture) +{ + constexpr size_t populationSize = 1000; + auto population = Population::makeRandom(m_fitnessMetric, populationSize, 1, 10); + + set originalSteps; + for (auto const& individual: population.individuals()) + originalSteps.insert(toString(individual.chromosome)); + + ClassicGeneticAlgorithm algorithm(m_options); + Population newPopulation = algorithm.runNextRound(population); + + for (auto const& individual: newPopulation.individuals()) + BOOST_TEST(originalSteps.count(toString(individual.chromosome)) == 1); +} + +BOOST_FIXTURE_TEST_CASE(runNextRound_should_do_crossover, ClassicGeneticAlgorithmFixture) +{ + auto population = Population(m_fitnessMetric, { + Chromosome("aa"), Chromosome("aa"), Chromosome("aa"), + Chromosome("ff"), Chromosome("ff"), Chromosome("ff"), + Chromosome("gg"), Chromosome("gg"), Chromosome("gg"), + }); + + set originalSteps{"aa", "ff", "gg"}; + set crossedSteps{"af", "fa", "fg", "gf", "ga", "ag"}; + + m_options.crossoverChance = 0.8; + ClassicGeneticAlgorithm algorithm(m_options); + + SimulationRNG::reset(1); + Population newPopulation = algorithm.runNextRound(population); + + size_t totalCrossed = 0; + size_t totalUnchanged = 0; + for (auto const& individual: newPopulation.individuals()) + { + totalCrossed += crossedSteps.count(toString(individual.chromosome)); + totalUnchanged += originalSteps.count(toString(individual.chromosome)); + } + BOOST_TEST(totalCrossed + totalUnchanged == newPopulation.individuals().size()); + BOOST_TEST(totalCrossed >= 2); +} + +BOOST_FIXTURE_TEST_CASE(runNextRound_should_do_mutation, ClassicGeneticAlgorithmFixture) +{ + m_options.mutationChance = 0.6; + ClassicGeneticAlgorithm algorithm(m_options); + + constexpr size_t populationSize = 1000; + constexpr double relativeTolerance = 0.05; + double const expectedValue = m_options.mutationChance; + double const variance = m_options.mutationChance * (1 - m_options.mutationChance); + + Chromosome chromosome("aaaaaaaaaa"); + vector chromosomes(populationSize, chromosome); + Population population(m_fitnessMetric, chromosomes); + + SimulationRNG::reset(1); + Population newPopulation = algorithm.runNextRound(population); + + vector bernoulliTrials; + for (auto const& individual: newPopulation.individuals()) + { + string steps = toString(individual.chromosome); + for (char step: steps) + bernoulliTrials.push_back(static_cast(step != 'a')); + } + + BOOST_TEST(abs(mean(bernoulliTrials) - expectedValue) < expectedValue * relativeTolerance); + BOOST_TEST(abs(meanSquaredError(bernoulliTrials, expectedValue) - variance) < variance * relativeTolerance); +} + +BOOST_FIXTURE_TEST_CASE(runNextRound_should_do_deletion, ClassicGeneticAlgorithmFixture) +{ + m_options.deletionChance = 0.6; + ClassicGeneticAlgorithm algorithm(m_options); + + constexpr size_t populationSize = 1000; + constexpr double relativeTolerance = 0.05; + double const expectedValue = m_options.deletionChance; + double const variance = m_options.deletionChance * (1 - m_options.deletionChance); + + Chromosome chromosome("aaaaaaaaaa"); + vector chromosomes(populationSize, chromosome); + Population population(m_fitnessMetric, chromosomes); + + SimulationRNG::reset(1); + Population newPopulation = algorithm.runNextRound(population); + + vector bernoulliTrials; + for (auto const& individual: newPopulation.individuals()) + { + string steps = toString(individual.chromosome); + for (size_t i = 0; i < chromosome.length(); ++i) + bernoulliTrials.push_back(static_cast(i >= steps.size())); + } + + BOOST_TEST(abs(mean(bernoulliTrials) - expectedValue) < expectedValue * relativeTolerance); + BOOST_TEST(abs(meanSquaredError(bernoulliTrials, expectedValue) - variance) < variance * relativeTolerance); +} + +BOOST_FIXTURE_TEST_CASE(runNextRound_should_do_addition, ClassicGeneticAlgorithmFixture) +{ + m_options.additionChance = 0.6; + ClassicGeneticAlgorithm algorithm(m_options); + + constexpr size_t populationSize = 1000; + constexpr double relativeTolerance = 0.05; + double const expectedValue = m_options.additionChance; + double const variance = m_options.additionChance * (1 - m_options.additionChance); + + Chromosome chromosome("aaaaaaaaaa"); + vector chromosomes(populationSize, chromosome); + Population population(m_fitnessMetric, chromosomes); + + SimulationRNG::reset(1); + Population newPopulation = algorithm.runNextRound(population); + + vector bernoulliTrials; + for (auto const& individual: newPopulation.individuals()) + { + string steps = toString(individual.chromosome); + for (size_t i = 0; i < chromosome.length() + 1; ++i) + { + BOOST_REQUIRE(chromosome.length() <= steps.size() && steps.size() <= 2 * chromosome.length() + 1); + bernoulliTrials.push_back(static_cast(i < steps.size() - chromosome.length())); + } + } + + BOOST_TEST(abs(mean(bernoulliTrials) - expectedValue) < expectedValue * relativeTolerance); + BOOST_TEST(abs(meanSquaredError(bernoulliTrials, expectedValue) - variance) < variance * relativeTolerance); +} + +BOOST_FIXTURE_TEST_CASE(runNextRound_should_preserve_elite, ClassicGeneticAlgorithmFixture) +{ + auto population = Population::makeRandom(m_fitnessMetric, 4, 3, 3) + Population::makeRandom(m_fitnessMetric, 6, 5, 5); + assert((chromosomeLengths(population) == vector{3, 3, 3, 3, 5, 5, 5, 5, 5, 5})); + + m_options.elitePoolSize = 0.5; + m_options.deletionChance = 1.0; + ClassicGeneticAlgorithm algorithm(m_options); + Population newPopulation = algorithm.runNextRound(population); + + BOOST_TEST((chromosomeLengths(newPopulation) == vector{0, 0, 0, 0, 0, 3, 3, 3, 3, 5})); +} + BOOST_AUTO_TEST_SUITE_END() BOOST_AUTO_TEST_SUITE_END() BOOST_AUTO_TEST_SUITE_END() diff --git a/tools/yulPhaser/GeneticAlgorithms.cpp b/tools/yulPhaser/GeneticAlgorithms.cpp index c38c3bc95..701bdf28c 100644 --- a/tools/yulPhaser/GeneticAlgorithms.cpp +++ b/tools/yulPhaser/GeneticAlgorithms.cpp @@ -64,3 +64,65 @@ Population GenerationalElitistWithExclusivePools::runNextRound(Population _popul _population.select(elitePool).mutate(mutationPoolFromElite, mutationOperator) + _population.select(elitePool).crossover(crossoverPoolFromElite, crossoverOperator); } + +Population ClassicGeneticAlgorithm::runNextRound(Population _population) +{ + Population elite = _population.select(RangeSelection(0.0, m_options.elitePoolSize)); + Population rest = _population.select(RangeSelection(m_options.elitePoolSize, 1.0)); + + Population selectedPopulation = select(_population, rest.individuals().size()); + + Population crossedPopulation = Population::combine( + selectedPopulation.symmetricCrossoverWithRemainder( + PairsFromRandomSubset(m_options.crossoverChance), + symmetricRandomPointCrossover() + ) + ); + + std::function mutationOperator = mutationSequence({ + geneRandomisation(m_options.mutationChance), + geneDeletion(m_options.deletionChance), + geneAddition(m_options.additionChance), + }); + + RangeSelection all(0.0, 1.0); + Population mutatedPopulation = crossedPopulation.mutate(all, mutationOperator); + + return elite + mutatedPopulation; +} + +Population ClassicGeneticAlgorithm::select(Population _population, size_t _selectionSize) +{ + if (_population.individuals().size() == 0) + return _population; + + size_t maxFitness = 0; + for (auto const& individual: _population.individuals()) + maxFitness = max(maxFitness, individual.fitness); + + size_t rouletteRange = 0; + for (auto const& individual: _population.individuals()) + // Add 1 to make sure that every chromosome has non-zero probability of being chosen + rouletteRange += maxFitness + 1 - individual.fitness; + + vector selectedIndividuals; + for (size_t i = 0; i < _selectionSize; ++i) + { + uint32_t ball = SimulationRNG::uniformInt(0, rouletteRange - 1); + + size_t cumulativeFitness = 0; + for (auto const& individual: _population.individuals()) + { + size_t pocketSize = maxFitness + 1 - individual.fitness; + if (ball < cumulativeFitness + pocketSize) + { + selectedIndividuals.push_back(individual); + break; + } + cumulativeFitness += pocketSize; + } + } + + assert(selectedIndividuals.size() == _selectionSize); + return Population(_population.fitnessMetric(), selectedIndividuals); +} diff --git a/tools/yulPhaser/GeneticAlgorithms.h b/tools/yulPhaser/GeneticAlgorithms.h index a2a7484d6..0d1f0375f 100644 --- a/tools/yulPhaser/GeneticAlgorithms.h +++ b/tools/yulPhaser/GeneticAlgorithms.h @@ -139,4 +139,59 @@ private: Options m_options; }; +/** + * A typical genetic algorithm that works in three distinct phases, each resulting in a new, + * modified population: + * - selection: chromosomes are selected from the population with probability proportional to their + * fitness. A chromosome can be selected more than once. The new population has the same size as + * the old one. + * - crossover: first, for each chromosome we decide whether it undergoes crossover or not + * (according to crossover chance parameter). Then each selected chromosome is randomly paired + * with one other selected chromosome. Each pair produces a pair of children and gets replaced by + * it in the population. + * - mutation: we go over each gene in the population and independently decide whether to mutate it + * or not (according to mutation chance parameters). This is repeated for every mutation type so + * one gene can undergo mutations of multiple types in a single round. + * + * This implementation also has the ability to preserve the top chromosomes in each round. + */ +class ClassicGeneticAlgorithm: public GeneticAlgorithm +{ +public: + struct Options + { + double elitePoolSize; ///< Percentage of the population treated as the elite. + double crossoverChance; ///< The chance of a particular chromosome being selected for crossover. + double mutationChance; ///< The chance of a particular gene being randomised in @a geneRandomisation mutation. + double deletionChance; ///< The chance of a particular gene being deleted in @a geneDeletion mutation. + double additionChance; ///< The chance of a particular gene being added in @a geneAddition mutation. + + bool isValid() const + { + return ( + 0 <= elitePoolSize && elitePoolSize <= 1.0 && + 0 <= crossoverChance && crossoverChance <= 1.0 && + 0 <= mutationChance && mutationChance <= 1.0 && + 0 <= deletionChance && deletionChance <= 1.0 && + 0 <= additionChance && additionChance <= 1.0 + ); + } + }; + + ClassicGeneticAlgorithm(Options const& _options): + m_options(_options) + { + assert(_options.isValid()); + } + + Options const& options() const { return m_options; } + + Population runNextRound(Population _population) override; + +private: + static Population select(Population _population, size_t _selectionSize); + + Options m_options; +}; + } From 0efea99fa5e173a56364ddfee64783523bf9a8f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kamil=20=C5=9Aliwak?= Date: Wed, 11 Mar 2020 02:16:00 +0100 Subject: [PATCH 10/10] [yul-phaser] Phaser: Allow selecting the classic algorithm on the command line --- test/yulPhaser/Phaser.cpp | 17 ++++++++++++++ tools/yulPhaser/Phaser.cpp | 46 ++++++++++++++++++++++++++++++++++++++ tools/yulPhaser/Phaser.h | 6 +++++ 3 files changed, 69 insertions(+) diff --git a/test/yulPhaser/Phaser.cpp b/test/yulPhaser/Phaser.cpp index 633462865..c365ce310 100644 --- a/test/yulPhaser/Phaser.cpp +++ b/test/yulPhaser/Phaser.cpp @@ -52,6 +52,11 @@ protected: /* gewepDeletionVsAdditionChance = */ 0.3, /* gewepGenesToRandomise = */ 0.4, /* gewepGenesToAddOrDelete = */ 0.2, + /* classicElitePoolSize = */ 0.0, + /* classicCrossoverChance = */ 0.75, + /* classicMutationChance = */ 0.2, + /* classicDeletionChance = */ 0.2, + /* classicAdditionChance = */ 0.2, }; }; @@ -122,6 +127,18 @@ BOOST_FIXTURE_TEST_CASE(build_should_select_the_right_algorithm_and_pass_the_opt BOOST_TEST(gewepAlgorithm->options().deletionVsAdditionChance == m_options.gewepDeletionVsAdditionChance); BOOST_TEST(gewepAlgorithm->options().percentGenesToRandomise == m_options.gewepGenesToRandomise.value()); BOOST_TEST(gewepAlgorithm->options().percentGenesToAddOrDelete == m_options.gewepGenesToAddOrDelete.value()); + + m_options.algorithm = Algorithm::Classic; + unique_ptr algorithm3 = GeneticAlgorithmFactory::build(m_options, 100); + BOOST_REQUIRE(algorithm3 != nullptr); + + auto classicAlgorithm = dynamic_cast(algorithm3.get()); + BOOST_REQUIRE(classicAlgorithm != nullptr); + BOOST_TEST(classicAlgorithm->options().elitePoolSize == m_options.classicElitePoolSize); + BOOST_TEST(classicAlgorithm->options().crossoverChance == m_options.classicCrossoverChance); + BOOST_TEST(classicAlgorithm->options().mutationChance == m_options.classicMutationChance); + BOOST_TEST(classicAlgorithm->options().deletionChance == m_options.classicDeletionChance); + BOOST_TEST(classicAlgorithm->options().additionChance == m_options.classicAdditionChance); } BOOST_FIXTURE_TEST_CASE(build_should_set_random_algorithm_elite_pool_size_based_on_population_size_if_not_specified, GeneticAlgorithmFactoryFixture) diff --git a/tools/yulPhaser/Phaser.cpp b/tools/yulPhaser/Phaser.cpp index 2e38edbf0..6f7be3257 100644 --- a/tools/yulPhaser/Phaser.cpp +++ b/tools/yulPhaser/Phaser.cpp @@ -58,6 +58,7 @@ map const AlgorithmToStringMap = { {Algorithm::Random, "random"}, {Algorithm::GEWEP, "GEWEP"}, + {Algorithm::Classic, "classic"}, }; map const StringToAlgorithmMap = invertMap(AlgorithmToStringMap); @@ -107,6 +108,11 @@ GeneticAlgorithmFactory::Options GeneticAlgorithmFactory::Options::fromCommandLi _arguments.count("gewep-genes-to-add-or-delete") > 0 ? _arguments["gewep-genes-to-add-or-delete"].as() : optional{}, + _arguments["classic-elite-pool-size"].as(), + _arguments["classic-crossover-chance"].as(), + _arguments["classic-mutation-chance"].as(), + _arguments["classic-deletion-chance"].as(), + _arguments["classic-addition-chance"].as(), }; } @@ -151,6 +157,16 @@ unique_ptr GeneticAlgorithmFactory::build( /* percentGenesToAddOrDelete = */ percentGenesToAddOrDelete, }); } + case Algorithm::Classic: + { + return make_unique(ClassicGeneticAlgorithm::Options{ + /* elitePoolSize = */ _options.classicElitePoolSize, + /* crossoverChance = */ _options.classicCrossoverChance, + /* mutationChance = */ _options.classicMutationChance, + /* deletionChance = */ _options.classicDeletionChance, + /* additionChance = */ _options.classicAdditionChance, + }); + } default: assertThrow(false, solidity::util::Exception, "Invalid Algorithm value."); } @@ -475,6 +491,36 @@ Phaser::CommandLineDescription Phaser::buildCommandLineDescription() ; keywordDescription.add(gewepAlgorithmDescription); + po::options_description classicGeneticAlgorithmDescription("CLASSIC GENETIC ALGORITHM", lineLength, minDescriptionLength); + classicGeneticAlgorithmDescription.add_options() + ( + "classic-elite-pool-size", + po::value()->value_name("")->default_value(0), + "Percentage of population to regenerate using mutations in each round." + ) + ( + "classic-crossover-chance", + po::value()->value_name("")->default_value(0.75), + "Chance of a chromosome being selected for crossover." + ) + ( + "classic-mutation-chance", + po::value()->value_name("")->default_value(0.01), + "Chance of a gene being mutated." + ) + ( + "classic-deletion-chance", + po::value()->value_name("")->default_value(0.01), + "Chance of a gene being deleted." + ) + ( + "classic-addition-chance", + po::value()->value_name("")->default_value(0.01), + "Chance of a random gene being added." + ) + ; + keywordDescription.add(classicGeneticAlgorithmDescription); + po::options_description randomAlgorithmDescription("RANDOM ALGORITHM", lineLength, minDescriptionLength); randomAlgorithmDescription.add_options() ( diff --git a/tools/yulPhaser/Phaser.h b/tools/yulPhaser/Phaser.h index 77e9e48c8..2896dd090 100644 --- a/tools/yulPhaser/Phaser.h +++ b/tools/yulPhaser/Phaser.h @@ -58,6 +58,7 @@ enum class Algorithm { Random, GEWEP, + Classic, }; enum class MetricChoice @@ -101,6 +102,11 @@ public: double gewepDeletionVsAdditionChance; std::optional gewepGenesToRandomise; std::optional gewepGenesToAddOrDelete; + double classicElitePoolSize; + double classicCrossoverChance; + double classicMutationChance; + double classicDeletionChance; + double classicAdditionChance; static Options fromCommandLine(boost::program_options::variables_map const& _arguments); };