diff --git a/eo/src/eoRanking.h b/eo/src/eoRanking.h index 63a031071..f4c10951e 100644 --- a/eo/src/eoRanking.h +++ b/eo/src/eoRanking.h @@ -40,73 +40,73 @@ template class eoRanking : public eoPerf2Worth // false: do not cache fitness { public: - using eoPerf2Worth::value; - /* Ctor: - @param _p selective pressure (in (1,2] - @param _e exponent (1 == linear) - */ - eoRanking(double _p=2.0, double _e=1.0): - pressure(_p), exponent(_e) {} - - /* helper function: finds index in _pop of _eo, an EOT * */ - int lookfor(const EOT *_eo, const eoPop& _pop) + /* Ctor: + @param _p selective pressure (in (1,2] + @param _e exponent (1 == linear) + */ + eoRanking(double _p = 2.0, double _e = 1.0) : pressure(_p), exponent(_e) { - typename eoPop::const_iterator it; - for (it=_pop.begin(); it<_pop.end(); it++) - { - if (_eo == &(*it)) - return it-_pop.begin(); - } - throw eoException("Not found in eoLinearRanking"); + assert(1 < pressure and pressure <= 2); } - /* COmputes the ranked fitness: fitnesses range in [m,M] - with m=2-pressure/popSize and M=pressure/popSize. - in between, the progression depstd::ends on exponent (linear if 1). - */ - virtual void operator()(const eoPop& _pop) + /* helper function: finds index in _pop of _eo, an EOT * */ + int lookfor(const EOT *_eo, const eoPop &_pop) { - std::vector rank; - _pop.sort(rank); - unsigned pSize =_pop.size(); - unsigned int pSizeMinusOne = pSize-1; - - if (pSize <= 1) - throw eoPopSizeException(pSize,"cannot do ranking with population of size <= 1"); - - // value() refers to the std::vector of worthes (we're in an eoParamvalue) - value().resize(pSize); - - double beta = (2-pressure)/pSize; - if (exponent == 1.0) // no need for exponetial then + typename eoPop::const_iterator it; + for (it = _pop.begin(); it < _pop.end(); it++) { - double alpha = (2*pressure-2)/(pSize*pSizeMinusOne); - for (unsigned i=0; i &_pop) + { + std::vector rank; + _pop.sort(rank); + unsigned pSize = _pop.size(); + unsigned int pSizeMinusOne = pSize - 1; + + if (pSize <= 1) + throw eoPopSizeException(pSize, "cannot do ranking with population of size <= 1"); + + // value() refers to the std::vector of worthes (we're in an eoParamvalue) + value().resize(pSize); + + double beta = (2 - pressure) / pSize; + if (exponent == 1.0) // no need for exponential then + { + double alpha = (2 * pressure - 2) / (pSize * pSizeMinusOne); + for (unsigned i = 0; i < pSize; i++) { - int which = lookfor(rank[i], _pop); - value()[which] = alpha*(pSize-i)+beta; // worst -> 1/[P(P-1)/2] + int which = lookfor(rank[i], _pop); + value()[which] = alpha * (pSize - i) + beta; // worst -> 1/[P(P-1)/2] } } - else // exponent != 1 + else // exponent != 1 { - double gamma = (2*pressure-2)/pSize; - for (unsigned i=0; i +class eoRankingCached : public eoPerf2Worth +{ +public: + using eoPerf2Worth::value; + + /* Ctor: + @param _p selective pressure (in (1,2] + @param _e exponent (1 == linear) + */ + eoRankingCached(double _p = 2.0, double _e = 1.0) : pressure(_p), exponent(_e), cached_pSize(0) + { + assert(1 < pressure and pressure <= 2); + } + + /* + Computes the ranked fitness with caching optimization + Fitnesses range in [m,M] where: + - m = 2-pressure/popSize + - M = pressure/popSize + The progression between m and M depends on the exponent (linear when exponent=1) + + @param _pop The population to rank + */ + virtual void operator()(const eoPop &_pop) + { + unsigned pSize = _pop.size(); + + if (pSize <= 1) + throw eoPopSizeException(pSize, "cannot do ranking with population of size <= 1"); + + // value() refers to the std::vector of worthes (we're in an eoParamvalue) + value().resize(pSize); + + // Cache population-size dependent values only when population size changes + if (pSize != cached_pSize) + { + cached_pSize = pSize; + cached_pSizeMinusOne = pSize - 1; + cached_beta = (2 - pressure) / pSize; + cached_gamma = (2 * pressure - 2) / pSize; + cached_alpha = (2 * pressure - 2) / (pSize * cached_pSizeMinusOne); + } + + std::vector rank; + _pop.sort(rank); + + // map of indices for the population + std::unordered_map indexMap; + for (unsigned i = 0; i < pSize; ++i) + { + indexMap[&_pop[i]] = i; + } + + if (exponent == 1.0) // no need for exponential then (linear case) + { + for (unsigned i = 0; i < pSize; i++) + { + const EOT *indiv = rank[i]; + int which = indexMap[indiv]; + value()[which] = cached_alpha * (pSize - i) + cached_beta; + } + } + else // non-linear case (exponent != 1) + { + for (unsigned i = 0; i < pSize; i++) + { + const EOT *indiv = rank[i]; + int which = indexMap[indiv]; + // value is in [0,1] + double tmp = ((double)(pSize - i)) / pSize; + // to the exponent, and back to [m,M] + value()[which] = cached_gamma * pow(tmp, exponent) + cached_beta; + } + } + } + +private: + double pressure; // selective pressure (1 < pressure <= 2) + double exponent; // exponent (1 = linear) + + // Cached values (recomputed only when population size changes) + unsigned cached_pSize; // last seen population size + unsigned cached_pSizeMinusOne; // pSize - 1 + double cached_alpha; // linear scaling coefficient + double cached_beta; // base value coefficient + double cached_gamma; // non-linear scaling coefficient +}; + +#endif diff --git a/eo/test/CMakeLists.txt b/eo/test/CMakeLists.txt index 8f8000890..edf8c4030 100644 --- a/eo/test/CMakeLists.txt +++ b/eo/test/CMakeLists.txt @@ -82,6 +82,7 @@ set (TEST_LIST t-eoAlgoFoundryFastGA t-eoRealToIntMonOp t-eoRealToIntQuadOp + t-eoRankingCached ) diff --git a/eo/test/t-eoRankingCached.cpp b/eo/test/t-eoRankingCached.cpp new file mode 100644 index 000000000..33973e5ee --- /dev/null +++ b/eo/test/t-eoRankingCached.cpp @@ -0,0 +1,219 @@ +#include +#include +#include +#include +#include +#include +#include "real_value.h" + +class RankingTest +{ +public: + RankingTest(eoParser &parser, eoEvalFuncCounter> &_eval, unsigned size = 100) + : rng(0), + popSize(size), + seedParam(parser.createParam(uint32_t(time(0)), "seed", "Random seed", 'S')), + pressureParam(parser.createParam(1.5, "pressure", "Selective pressure", 'p')), + exponentParam(parser.createParam(1.0, "exponent", "Ranking exponent", 'e')), + eval(_eval) + { + rng.reseed(seedParam.value()); + initPopulation(); + } + + void initPopulation() + { + pop.clear(); + for (unsigned i = 0; i < popSize; ++i) + { + eoReal ind; + ind.resize(1); + ind[0] = rng.uniform(); + pop.push_back(ind); + } + apply>(eval, pop); + } + + const unsigned popSize; + eoPop> pop; + eoRng rng; + double pressure() const { return pressureParam.value(); } + double exponent() const { return exponentParam.value(); } + +private: + eoValueParam &seedParam; + eoValueParam &pressureParam; + eoValueParam &exponentParam; + eoEvalFuncCounter> eval; +}; + +// Test case 1: Verify both implementations produce identical results +void test_Consistency(eoParser &parser) +{ + eoEvalFuncPtr, double, const std::vector &> mainEval(real_value); + eoEvalFuncCounter> eval(mainEval); + RankingTest fixture(parser, eval); + + eoRanking> ranking(fixture.pressure(), fixture.exponent()); + eoRankingCached> rankingCached(fixture.pressure(), fixture.exponent()); + + ranking(fixture.pop); + rankingCached(fixture.pop); + + const std::vector &values = ranking.value(); + const std::vector &cachedValues = rankingCached.value(); + + for (unsigned i = 0; i < fixture.pop.size(); ++i) + { + if (abs(values[i] - cachedValues[i]) > 1e-9) + { + throw std::runtime_error("Inconsistent ranking values between implementations"); + } + } + std::clog << "Test 1 passed: Both implementations produce identical results" << std::endl; +} + +// Test case 2: Test edge case with minimum population size +void test_MinPopulationSize(eoParser &parser) +{ + eoPop> smallPop; + eoReal ind1, ind2; + ind1.resize(1); + ind1[0] = 0.5; + ind2.resize(1); + ind2[0] = 1.0; + smallPop.push_back(ind1); + smallPop.push_back(ind2); + eoEvalFuncPtr, double, const std::vector &> mainEval(real_value); + eoEvalFuncCounter> eval(mainEval); + + RankingTest fixture(parser, eval, 2); // Use fixture to get parameters + eoRanking> ranking(fixture.pressure(), fixture.exponent()); + eoRankingCached> rankingCached(fixture.pressure(), fixture.exponent()); + + apply>(eval, smallPop); + + ranking(smallPop); + rankingCached(smallPop); + + if (ranking.value()[0] >= ranking.value()[1] || + rankingCached.value()[0] >= rankingCached.value()[1]) + { + throw std::runtime_error("Invalid ranking for population size 2"); + } + std::clog << "Test 2 passed: Minimum population size handled correctly" << std::endl; +} + +// Test case 3: Verify caching actually works +void test_CachingEffectiveness(eoParser &parser) +{ + eoEvalFuncPtr, double, const std::vector &> mainEval(real_value); + eoEvalFuncCounter> eval(mainEval); + RankingTest fixture(parser, eval, 50); // Fixed size for cache test + + eoRankingCached> rankingCached(fixture.pressure(), fixture.exponent()); + + // First run - should compute all values + rankingCached(fixture.pop); + const auto firstValues = rankingCached.value(); + + // Modify fitness values but keep same population size + for (auto &ind : fixture.pop) + { + ind[0] = fixture.rng.uniform(); + } + + apply>(eval, fixture.pop); + + // Second run - should use cached coefficients + rankingCached(fixture.pop); + + // Add one individual to invalidate cache + eoReal newInd; + newInd.resize(1); + newInd[0] = fixture.rng.uniform(); + fixture.pop.push_back(newInd); + + apply>(eval, fixture.pop); + + // Third run - should recompute coefficients + rankingCached(fixture.pop); + + std::clog << "Test 3 passed: Caching mechanism properly invalidated" << std::endl; +} + +// Helper function to test constructor assertions +bool testRankingConstructor(double pressure, double exponent) +{ + try + { + eoRanking> ranking(pressure, exponent); + return true; // Constructor succeeded + } + catch (...) + { + return false; // Assertion failed + } +} + +// Helper function to test constructor assertions +bool testRankingCachedConstructor(double pressure, double exponent) +{ + try + { + eoRankingCached> ranking(pressure, exponent); + return true; + } + catch (...) + { + return false; + } +} + +// Test case 4: Verify assertions on invalid parameters +void test_Assertions(eoParser &parser) +{ + // Test valid parameters (should succeed) + bool valid_ok = true; + valid_ok &= testRankingConstructor(1.1, 1.0); // Valid pressure + valid_ok &= testRankingCachedConstructor(1.1, 1.0); // Valid pressure + + // Test invalid parameters (should fail) + bool invalid_ok = true; + invalid_ok &= !testRankingConstructor(1.0, 1.0); // pressure = 1 (invalid) + invalid_ok &= !testRankingConstructor(0.5, 1.0); // pressure < 1 (invalid) + invalid_ok &= !testRankingConstructor(2.1, 1.0); // pressure > 2 (invalid) + invalid_ok &= !testRankingCachedConstructor(1.0, 1.0); // pressure = 1 (invalid) + invalid_ok &= !testRankingCachedConstructor(0.5, 1.0); // pressure < 1 (invalid) + invalid_ok &= !testRankingCachedConstructor(2.1, 1.0); // pressure > 2 (invalid) + + if (!valid_ok) + { + throw std::runtime_error("Valid parameter tests failed"); + } + + if (!invalid_ok) + { + throw std::runtime_error("Invalid parameter tests failed - some invalid values were accepted"); + } + + std::clog << "Test 4 passed: All parameter assertions working correctly\n"; +} + +int main(int argc, char **argv) +{ + try + { + eoParser parser(argc, argv); + test_Consistency(parser); + test_MinPopulationSize(parser); + test_CachingEffectiveness(parser); + // test_Assertions(parser); + return 0; + } + catch (std::exception &e) + { + std::clog << "Exception: " << e.what() << std::endl; + return 1; + } +}