diff --git a/.github/workflows/cpp-tests.yml b/.github/workflows/cpp-tests.yml new file mode 100644 index 0000000..84a7c8b --- /dev/null +++ b/.github/workflows/cpp-tests.yml @@ -0,0 +1,31 @@ +name: C++ tests + +on: + pull_request: + push: + branches: ["main"] + workflow_dispatch: + +env: + FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true + +jobs: + test: + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + build_type: [Debug, Release] + runs-on: ${{ matrix.os }} + + steps: + - uses: actions/checkout@v4 + + - name: Configure + run: cmake -S . -B build -DCMAKE_BUILD_TYPE=${{ matrix.build_type }} + + - name: Build + run: cmake --build build --config ${{ matrix.build_type }} --parallel 2 + + - name: Test + run: ctest --test-dir build --build-config ${{ matrix.build_type }} --output-on-failure diff --git a/.github/workflows/wheels.yml b/.github/workflows/wheels.yml index 80c6780..f268bb6 100644 --- a/.github/workflows/wheels.yml +++ b/.github/workflows/wheels.yml @@ -37,7 +37,7 @@ jobs: CIBW_BUILD: "cp38-* cp39-* cp310-* cp311-* cp312-*" CIBW_SKIP: "pp* *-musllinux_*" CIBW_TEST_COMMAND: >- - python -c "import genetic_algorithm_lib as ga; cfg = ga.Config(); print(cfg)" + python -c "import genetic_algorithm_lib as ga; s = ga.SearchConfig(); s.population_size = 6; s.iterations = 2; s.dimension = 2; s.seed = 1; c = ga.PsoConfig(); c.search = s; r = ga.ParticleSwarmOptimizer(c).optimize(lambda x: 1.0 / (1.0 + sum(v * v for v in x))); assert len(r.best_solution) == 2" - name: Upload wheels uses: actions/upload-artifact@v4 diff --git a/.gitignore b/.gitignore index 6b698f5..707bac1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ build/ +/build-*/ .scikit-build/ _skbuild/ .venv/ diff --git a/CMakeLists.txt b/CMakeLists.txt index 63063a2..7a42e0b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,8 @@ cmake_minimum_required(VERSION 3.16) project(GeneticAlgorithm VERSION 1.0.0 LANGUAGES CXX) +find_package(Threads REQUIRED) + # Set C++ standard set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) @@ -18,6 +20,12 @@ set(CORE_SOURCES src/genetic_algorithm.cpp src/c_api.cpp src/algorithms/moea/nsga2.cpp + src/aco/ant_colony.cpp + src/aco/continuous_ant_colony.cpp + src/fuzzy/fuzzy_c_means.cpp + src/gsa/gravitational_search.cpp + src/hybrid/metaheuristic_pipeline.cpp + src/pso/particle_swarm.cpp ) # Build reusable library (framework) @@ -32,6 +40,7 @@ target_include_directories(genetic_algorithm PUBLIC $ ) set_target_properties(genetic_algorithm PROPERTIES OUTPUT_NAME "genetic_algorithm") +target_link_libraries(genetic_algorithm PUBLIC Threads::Threads) # Warnings: keep them compiler-appropriate (avoid injecting GCC/Clang flags into MSVC). if(MSVC) @@ -41,6 +50,8 @@ else() endif() if(NOT SKBUILD) + enable_testing() + # Main executable add_executable(simple-ga-test simple-ga-test.cc @@ -102,6 +113,12 @@ if(NOT SKBUILD) RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/examples" ) + add_executable(ga-metaheuristics-minimal examples/metaheuristics_minimal.cpp) + target_link_libraries(ga-metaheuristics-minimal PRIVATE genetic_algorithm) + set_target_properties(ga-metaheuristics-minimal PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/examples" + ) + # Tests (lightweight sanity checks) add_executable(operators-sanity tests/operators_sanity.cc) target_link_libraries(operators-sanity PRIVATE genetic_algorithm) @@ -151,6 +168,22 @@ if(NOT SKBUILD) RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/tests" ) + add_executable(metaheuristics-sanity tests/metaheuristics_sanity.cc) + target_link_libraries(metaheuristics-sanity PRIVATE genetic_algorithm) + set_target_properties(metaheuristics-sanity PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/tests" + ) + + add_test(NAME operators-sanity COMMAND operators-sanity) + add_test(NAME nsga2-sanity COMMAND nsga2-sanity) + add_test(NAME nsga3-sanity COMMAND nsga3-sanity) + add_test(NAME c-api-sanity COMMAND c-api-sanity) + add_test(NAME features-foundation-sanity COMMAND features-foundation-sanity) + add_test(NAME process-distributed-sanity COMMAND process-distributed-sanity) + add_test(NAME advanced-features-sanity COMMAND advanced-features-sanity) + add_test(NAME representations-core-sanity COMMAND representations-core-sanity) + add_test(NAME metaheuristics-sanity COMMAND metaheuristics-sanity) + # Benchmark suite add_executable(ga-benchmark benchmark/benchmark_main.cc @@ -160,6 +193,12 @@ if(NOT SKBUILD) set_target_properties(ga-benchmark PROPERTIES RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" ) + + add_executable(metaheuristics-benchmark benchmark/metaheuristics_benchmark.cc) + target_link_libraries(metaheuristics-benchmark PRIVATE genetic_algorithm) + set_target_properties(metaheuristics-benchmark PROPERTIES + RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/bin" + ) endif() # ---------------------------------------------------------------- Python bindings @@ -221,6 +260,14 @@ if(Python3_FOUND AND pybind11_FOUND) LIBRARY DESTINATION lib/python/genetic_algorithm_lib RUNTIME DESTINATION lib/python/genetic_algorithm_lib ) + add_test( + NAME python-bindings-sanity + COMMAND "${Python3_EXECUTABLE}" "${CMAKE_SOURCE_DIR}/python/bindings_sanity.py" + ) + set_tests_properties(python-bindings-sanity PROPERTIES + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" + ENVIRONMENT "GA_PYTHON_MODULE_DIR=${CMAKE_BINARY_DIR}/python" + ) endif() message(STATUS "Python bindings: ENABLED (pybind11 ${pybind11_VERSION})") else() @@ -250,10 +297,6 @@ if(NOT SKBUILD) ) endif() -# Add test target (if you want to add unit tests later) -# enable_testing() -# add_test(NAME GA_Test COMMAND simple-ga-test) - # Print configuration summary message(STATUS "=== Genetic Algorithm Build Configuration ===") message(STATUS "Build type: ${CMAKE_BUILD_TYPE}") diff --git a/FEATURE_CHECKLIST.md b/FEATURE_CHECKLIST.md index a47d6ec..7cf72da 100644 --- a/FEATURE_CHECKLIST.md +++ b/FEATURE_CHECKLIST.md @@ -20,6 +20,11 @@ Legend: implemented, partial, planned | Checkpointing | implemented | binary + JSON checkpoint save/load manager in `include/ga/checkpoint/checkpoint.hpp` | | Adaptive operators | implemented | adaptive rate controller in `include/ga/adaptive/adaptive_policy.hpp` | | Hybrid optimization | implemented | GA+local-search hybrid pipeline in `include/ga/hybrid/hybrid_optimizer.hpp` | +| PSO family | implemented | global/local, constriction, bare-bones, fully informed, quantum, and binary variants in `include/ga/pso/*` | +| ACO family | implemented | five graph ACO variants plus continuous ACOR in `include/ga/aco/*` | +| Gravitational Search (GSA) | implemented | continuous K-best GSA in `include/ga/gsa/*` | +| Fuzzy optimization support | implemented | fuzzy C-means and opt-in, user-configurable Sugeno adaptive controller in `include/ga/fuzzy/*` | +| Heterogeneous hybrid pipeline | implemented | caller-selected optimizer order, GA adapter, and solution-transfer pipeline in `include/ga/metaheuristics/*` and `include/ga/hybrid/metaheuristic_pipeline.hpp` | | Constraint handling | implemented | hard/soft/repair constraint toolkit in `include/ga/constraints/constraints.hpp` | | Experiment tracking | implemented | experiment tracker with CSV export in `include/ga/tracking/experiment_tracker.hpp` | | Visualization support | implemented | CSV export helpers for fitness/pareto/diversity in `include/ga/visualization/export.hpp` | @@ -29,7 +34,7 @@ Legend: implemented, partial, planned | Core representations (all types) | implemented | `VectorGenome`, `BitsetGenome`, `PermutationGenome`, `TreeGenome`, `SetGenome`, `MapGenome`, `NdArrayGenome` in `include/ga/representations/*` | | Core abstractions | implemented | `IGenome`, `Individual`, `IProblem`, `Evaluation` in `include/ga/core/*`; `Population`, `IEvaluator`, `IAlgorithm`, `OptimizationResult` added | | High-level API | implemented | fluent `Optimizer` API for single/multi-objective in `include/ga/api/optimizer.hpp`; `OptimizerBuilder` fluent builder in `include/ga/api/builder.hpp` | -| C and Python export path | implemented | Python bindings (`python/ga_bindings.cpp`); C ABI wrapper (`include/ga/c_api.h`, `src/c_api.cpp`) | +| C and Python export path | implemented | Python bindings include PSO/ACO/ACOR/GSA/fuzzy/user-defined pipelines (`python/ga_bindings.cpp`); C ABI wrapper (`include/ga/c_api.h`, `src/c_api.cpp`) | ## 2) Complete Checklist (What, Why, How) @@ -411,4 +416,3 @@ public: - adaptive operators, hybrid local search, ergonomic optimizer API 7. Language exports: - C wrapper and expanded Python bindings - diff --git a/IMPLEMENTATION_STATUS.md b/IMPLEMENTATION_STATUS.md index f9d959f..8c603ca 100644 --- a/IMPLEMENTATION_STATUS.md +++ b/IMPLEMENTATION_STATUS.md @@ -36,14 +36,26 @@ This report documents the comprehensive analysis and testing of the genetic algo - Co-evolution Engine: PASS - Integration Test (all features together): PASS +8. **representations-core-sanity** - Representations and core abstractions: PASS + +9. **metaheuristics-sanity** - New optimization suite: PASS + - Seven PSO families: PASS + - Five graph ACO variants and continuous ACOR: PASS + - GSA: PASS + - Fuzzy C-means and fuzzy adaptive control: PASS + - GA/PSO solution-transfer pipeline: PASS + - Exact evaluation accounting and no-elitism best retention: PASS + ### Examples (All Working) - **ga-minimal**: Basic single-objective GA - Working - **ga-nsga2-minimal**: Multi-objective NSGA-II - Working - **ga-optimizer-minimal**: High-level optimizer API - Working +- **ga-metaheuristics-minimal**: Fuzzy-controlled GA/PSO/ACOR pipeline - Working ### Benchmark Suite (All Working) - **Operators Benchmark**: Performance testing of all operators - Working - **Functions Benchmark**: Optimization of test functions (Sphere, Rastrigin, Ackley, Schwefel, Rosenbrock) - Working +- **Metaheuristics Benchmark**: PSO family, ACOR, and GSA timing/evaluation comparison - Working - All benchmarks complete successfully and generate reports ## Implementation Status by Feature @@ -66,6 +78,20 @@ This report documents the comprehensive analysis and testing of the genetic algo - 11 Mutation operators in `mutation/` - 6 Selection operators in `selection-operator/` +### ✅ Metaheuristics Suite +- **Particle Swarm Optimization** (`include/ga/pso/`, `src/pso/`) + - Global-best, ring local-best, constriction, bare-bones, fully informed, + quantum-behaved, and binary variants +- **Ant Colony Optimization** (`include/ga/aco/`, `src/aco/`) + - Ant System, Elitist AS, Rank-Based AS, ACS, MAX-MIN AS, and continuous ACOR +- **Gravitational Search** (`include/ga/gsa/`, `src/gsa/`) + - Continuous GSA with decreasing K-best interaction +- **Fuzzy support** (`include/ga/fuzzy/`, `src/fuzzy/`) + - Standalone fuzzy C-means and a reusable Sugeno adaptive controller +- **Composition** (`include/ga/metaheuristics/`, `include/ga/hybrid/`) + - Common continuous-optimizer contract, GA adapter, and sequential + solution-transfer pipeline + ### ✅ Multi-Objective Optimization (100% Complete) - **NSGA-II** (`src/algorithms/moea/nsga2.cpp`) - Non-dominated sorting @@ -208,7 +234,7 @@ This report documents the comprehensive analysis and testing of the genetic algo ## Code Quality Metrics ### Test Coverage -- **7 Comprehensive Test Suites**: All passing +- **9 Comprehensive Test Suites**: All passing - **37,000+ lines** of test code across all test files - **35+ operators** individually tested - **All advanced features** have dedicated tests @@ -283,7 +309,7 @@ The genetic algorithm library is feature-complete according to the FEATURE_CHECK - Co-evolution framework - All integration scenarios -**Total Test Status: 7/7 test suites passing, 0 failures** +**Total Test Status: 9/9 test suites passing, 0 failures** The library now has: - ✅ Production-ready code quality diff --git a/METAHEURISTICS.md b/METAHEURISTICS.md new file mode 100644 index 0000000..e36f2b1 --- /dev/null +++ b/METAHEURISTICS.md @@ -0,0 +1,161 @@ +# Metaheuristics suite + +The C++17 core provides interoperable population-based optimizers for continuous, +binary, graph, and clustering problems. Include the complete surface with: + +```cpp +#include +``` + +All continuous optimizers maximize `ga::Fitness`. Convert a minimization +objective to fitness, for example `1.0 / (1.0 + cost)` or `-cost`. Graph ACO +directly minimizes edge cost. + +## Implemented algorithms + +| Family | Implemented variants | +|---|---| +| PSO | global-best, ring local-best, constriction, bare-bones, fully informed, quantum-behaved, binary | +| Graph ACO | Ant System, Elitist Ant System, Rank-Based Ant System, Ant Colony System, MAX-MIN Ant System | +| Continuous ACO | ACOR (archive-based continuous ant colony optimization) | +| GSA | continuous gravitational search with decreasing K-best agents | +| Fuzzy | fuzzy C-means clustering and a reusable zero-order Sugeno adaptive controller | +| Hybrid | sequential heterogeneous pipeline with solution transfer, including a GA adapter | + +“All ACO/PSO types” is not a finite category in the literature. This release +implements the major distinct families above; new variants can implement +`IContinuousOptimizer` and immediately participate in the hybrid pipeline. + +## User-controlled design + +The library does not select an algorithm, construct a hybrid, reorder stages, +or enable adaptation automatically. Each optimizer can run independently. A +hybrid exists only when the caller creates a `MetaheuristicPipeline` and calls +`add(...)` in the exact desired order; the pipeline preserves that order and +only transfers solutions between those explicitly selected stages. + +All algorithm hyperparameters are exposed through public configuration structs. +Their defaults are conveniences, not an automatic tuning policy. An adaptive +controller is also opt-in: leaving `controller` unset keeps the configured +algorithm parameters fixed. `FuzzyControllerConfig` exposes its membership +thresholds, improvement scale, and every Sugeno rule consequent. + +Robustness here means validated configurations and controller outputs, bounded +solutions, rejection of non-finite fitness values, deterministic seeded runs, +exact evaluation accounting, and cross-platform tests. It does not mean hidden +algorithm selection or hidden hyperparameter tuning. + +## Shared continuous interface + +```cpp +class IContinuousOptimizer { +public: + virtual std::string name() const = 0; + virtual ga::core::OptimizationResult optimize( + const ga::Fitness& fitness, + const SeedPopulation& seeds = {}) = 0; +}; +``` + +`SearchConfig` controls population size, iteration budget, dimension, uniform +bounds, deterministic seed, and objective-evaluation threads. Results report the +best solution, best/average histories, completed iterations, and exact objective +evaluation count. + +## User-selected hybrid with optional fuzzy control + +```cpp +ga::fuzzy::FuzzyControllerConfig fuzzyCfg; +fuzzyCfg.improvementScale = 20.0; +fuzzyCfg.lowDiversityStagnant = {1.55, 0.80, 1.30, 1.45}; +auto fuzzy = std::make_shared(fuzzyCfg); + +ga::Config gaCfg; +gaCfg.dimension = 10; +gaCfg.bounds = {-5.0, 5.0}; + +ga::pso::PsoConfig psoCfg; +psoCfg.search.dimension = 10; +psoCfg.search.bounds = {-5.0, 5.0}; +psoCfg.variant = ga::pso::PsoVariant::Constriction; +psoCfg.controller = fuzzy; // Optional: omit for fixed PSO parameters. + +ga::aco::AcorConfig acorCfg; +acorCfg.search.dimension = 10; +acorCfg.search.bounds = {-5.0, 5.0}; +acorCfg.controller = fuzzy; // Optional. + +ga::hybrid::MetaheuristicPipeline pipeline; +pipeline.add(std::make_unique(gaCfg)) + .add(std::make_unique(psoCfg)) + .add(std::make_unique(acorCfg)); + +auto result = pipeline.optimize(fitness); +``` + +The caller above explicitly chooses GA, then PSO, then ACOR. Removing, replacing, +or reordering an `add(...)` call changes the hybrid accordingly. The optional +controller observes normalized population diversity, recent improvement, +and stagnation. It returns dimensionless exploration, exploitation, evaporation, +and randomization multipliers. Each algorithm maps those signals to its native +parameters. A single shared controller can therefore adapt every stage without +coupling the algorithms to fuzzy-logic implementation details. + +## Graph ACO example + +```cpp +ga::aco::DenseGraph graph({ + {0, 2, 9, 10}, + {2, 0, 6, 4}, + {9, 6, 0, 8}, + {10, 4, 8, 0}, +}); + +ga::aco::AntColonyConfig cfg; +cfg.variant = ga::aco::AntColonyVariant::MaxMinAntSystem; +cfg.controller = std::make_shared(); +auto tour = ga::aco::AntColonyOptimizer(cfg).solve(graph); +``` + +`DenseGraph` supports symmetric and directed complete graphs. Candidate lists, +precomputed heuristic values, contiguous pheromone storage, and allocation reuse +keep the tour-construction inner loop compact. + +## Fuzzy C-means example + +```cpp +ga::fuzzy::FuzzyCMeansConfig cfg; +cfg.clusters = 3; +cfg.fuzziness = 2.0; +auto clusters = ga::fuzzy::FuzzyCMeans(cfg).fit(data); +auto hardLabels = clusters.labels(); +``` + +The result retains the soft membership matrix, centers, objective history, +convergence flag, and iteration count. + +## Performance notes + +- Fitness batches can be evaluated concurrently with `SearchConfig::threads`. + The fitness callback must be safe for concurrent calls when `threads > 1`. +- Expensive buffers and graph heuristics are allocated or computed outside hot loops. +- The existing GA now evaluates each mutated offspring exactly once and caches its + bound vectors, removing redundant objective calls and per-offspring allocations. +- Fixed seeds make algorithm comparisons reproducible. + +Run the deterministic tests and release benchmark with: + +```bash +cmake -S . -B build -DCMAKE_BUILD_TYPE=Release +cmake --build build --parallel +./build/tests/metaheuristics-sanity +./build/bin/metaheuristics-benchmark +``` + +## Python API + +The same algorithms, configuration fields, optional fuzzy controller, seeded +solutions, results, and caller-defined pipeline order are exposed by the +`genetic_algorithm_lib` Python package. See +[`python/metaheuristics_example.py`](python/metaheuristics_example.py) for a +complete example and [`python/README.md`](python/README.md) for the API guide. diff --git a/README.md b/README.md index e38d114..070e22e 100644 --- a/README.md +++ b/README.md @@ -64,6 +64,11 @@ print("History length:", len(result.best_history)) - Benchmark suite (operator speed, function optimization, scalability) - Plugin-style registries (create/register operators by name in Python) - Optional parallel evaluation helpers +- C++ and Python APIs for user-configured PSO, graph/continuous ACO, GSA, + fuzzy C-means, and opt-in hybrid optimization + +See [METAHEURISTICS.md](METAHEURISTICS.md) for the supported variants, shared C++ +interface, configurable adaptive fuzzy controller, and user-selected hybrid examples. ## Supported representations diff --git a/benchmark/metaheuristics_benchmark.cc b/benchmark/metaheuristics_benchmark.cc new file mode 100644 index 0000000..6412e78 --- /dev/null +++ b/benchmark/metaheuristics_benchmark.cc @@ -0,0 +1,79 @@ +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ga/metaheuristics.hpp" + +namespace { + +double rastrigin(const std::vector& x) { + const double pi = std::acos(-1.0); + double value = 10.0 * static_cast(x.size()); + for (double xi : x) { + value += xi * xi - 10.0 * std::cos(2.0 * pi * xi); + } + return 1.0 / (1.0 + value); +} + +template +void measure(const std::string& name, Function&& run) { + const auto begin = std::chrono::steady_clock::now(); + const auto result = run(); + const auto elapsed = std::chrono::duration( + std::chrono::steady_clock::now() - begin); + std::cout << std::left << std::setw(24) << name << std::right << std::setw(12) + << std::fixed << std::setprecision(3) << elapsed.count() << std::setw(16) + << result.evaluations << std::setw(18) << std::setprecision(8) + << result.bestFitness << '\n'; +} + +} // namespace + +int main() { + auto controller = std::make_shared(); + ga::metaheuristics::SearchConfig search; + search.populationSize = 80; + search.iterations = 150; + search.dimension = 30; + search.bounds = {-5.12, 5.12}; + search.seed = 42; + + std::cout << std::left << std::setw(24) << "algorithm" << std::right + << std::setw(12) << "ms" << std::setw(16) << "evaluations" + << std::setw(18) << "best fitness" << '\n'; + + for (const auto variant : {ga::pso::PsoVariant::GlobalBest, + ga::pso::PsoVariant::LocalBest, + ga::pso::PsoVariant::Constriction, + ga::pso::PsoVariant::BareBones, + ga::pso::PsoVariant::FullyInformed, + ga::pso::PsoVariant::QuantumBehaved}) { + ga::pso::PsoConfig config; + config.search = search; + config.variant = variant; + config.controller = controller; + ga::pso::ParticleSwarmOptimizer optimizer(config); + const std::string name = optimizer.name(); + measure(name, [&] { return optimizer.optimize(rastrigin); }); + } + + ga::aco::AcorConfig acorConfig; + acorConfig.search = search; + acorConfig.archiveSize = 80; + acorConfig.sampleCount = 40; + acorConfig.controller = controller; + ga::aco::ContinuousAntColonyOptimizer acor(acorConfig); + measure(acor.name(), [&] { return acor.optimize(rastrigin); }); + + ga::gsa::GsaConfig gsaConfig; + gsaConfig.search = search; + gsaConfig.controller = controller; + ga::gsa::GravitationalSearchOptimizer gsa(gsaConfig); + measure(gsa.name(), [&] { return gsa.optimize(rastrigin); }); + return 0; +} diff --git a/examples/metaheuristics_minimal.cpp b/examples/metaheuristics_minimal.cpp new file mode 100644 index 0000000..0b483e4 --- /dev/null +++ b/examples/metaheuristics_minimal.cpp @@ -0,0 +1,64 @@ +#include +#include +#include + +#include "ga/metaheuristics.hpp" + +namespace { + +double sphere(const std::vector& x) { + double sum = 0.0; + for (double value : x) { + sum += value * value; + } + return 1.0 / (1.0 + sum); // The library maximizes fitness. +} + +} // namespace + +int main() { + ga::fuzzy::FuzzyControllerConfig fuzzyConfig; + fuzzyConfig.improvementScale = 20.0; + fuzzyConfig.lowDiversityStagnant = {1.55, 0.80, 1.30, 1.45}; + auto fuzzyController = + std::make_shared(fuzzyConfig); + + ga::Config gaConfig; + gaConfig.populationSize = 40; + gaConfig.generations = 50; + gaConfig.dimension = 5; + gaConfig.bounds = {-5.0, 5.0}; + gaConfig.seed = 42; + + ga::pso::PsoConfig psoConfig; + psoConfig.search.populationSize = 40; + psoConfig.search.iterations = 50; + psoConfig.search.dimension = 5; + psoConfig.search.bounds = {-5.0, 5.0}; + psoConfig.search.seed = 43; + psoConfig.variant = ga::pso::PsoVariant::Constriction; + psoConfig.controller = fuzzyController; // Optional; omit for fixed parameters. + + ga::aco::AcorConfig acorConfig; + acorConfig.search.iterations = 40; + acorConfig.search.dimension = 5; + acorConfig.search.bounds = {-5.0, 5.0}; + acorConfig.search.seed = 44; + acorConfig.archiveSize = 40; + acorConfig.sampleCount = 20; + acorConfig.controller = fuzzyController; // Optional. + + // The caller alone chooses the algorithms and their exact execution order. + ga::hybrid::MetaheuristicPipeline hybrid; + hybrid.add(std::make_unique(gaConfig)) + .add(std::make_unique(psoConfig)) + .add(std::make_unique(acorConfig)); + + const auto run = hybrid.optimizeDetailed(sphere); + std::cout << "Best fitness: " << run.combined.bestFitness << '\n'; + std::cout << "Evaluations: " << run.combined.evaluations << '\n'; + for (const auto& stage : run.stages) { + std::cout << stage.optimizer << ": " << stage.result.bestFitness << '\n'; + } + return 0; +} diff --git a/examples/minimal.cpp b/examples/minimal.cpp index 384bddc..09a8da5 100644 --- a/examples/minimal.cpp +++ b/examples/minimal.cpp @@ -3,10 +3,14 @@ #include #include "ga/genetic_algorithm.hpp" +namespace { +constexpr double kPi = 3.14159265358979323846; +} + static double rastrigin(const std::vector& x) { const double A = 10.0; - double sum = A * x.size(); - for (double xi : x) sum += xi*xi - A*std::cos(2*M_PI*xi); + double sum = A * static_cast(x.size()); + for (double xi : x) sum += xi*xi - A*std::cos(2*kPi*xi); // Convert minimization objective to maximization fitness double f = sum; return 1000.0 / (1.0 + f); diff --git a/include/ga/aco/ant_colony.hpp b/include/ga/aco/ant_colony.hpp new file mode 100644 index 0000000..2d9d5e1 --- /dev/null +++ b/include/ga/aco/ant_colony.hpp @@ -0,0 +1,77 @@ +#pragma once + +#include +#include +#include + +#include "ga/metaheuristics/common.hpp" + +namespace ga { +namespace aco { + +class DenseGraph { +public: + explicit DenseGraph(std::vector> costs, + bool symmetric = true); + + std::size_t size() const noexcept { return size_; } + double cost(std::size_t from, std::size_t to) const noexcept { + return costs_[from * size_ + to]; + } + bool symmetric() const noexcept { return symmetric_; } + +private: + std::size_t size_ = 0; + std::vector costs_; + bool symmetric_ = true; +}; + +enum class AntColonyVariant { + AntSystem, + ElitistAntSystem, + RankBasedAntSystem, + AntColonySystem, + MaxMinAntSystem +}; + +struct AntColonyConfig { + std::size_t ants = 30; + std::size_t iterations = 100; + double alpha = 1.0; + double beta = 2.0; + double evaporation = 0.1; + double depositScale = 1.0; + double initialPheromone = 1.0; + double elitistWeight = 2.0; + std::size_t rankCount = 6; + double exploitationProbability = 0.9; + double localEvaporation = 0.1; + std::size_t candidateListSize = 20; + double minPheromone = 0.0; + double maxPheromone = 0.0; + AntColonyVariant variant = AntColonyVariant::AntSystem; + unsigned seed = 0; + std::shared_ptr controller; +}; + +struct AntColonyResult { + std::vector bestTour; + double bestCost = 0.0; + std::vector bestCostHistory; + std::size_t evaluations = 0; + std::size_t iterations = 0; +}; + +class AntColonyOptimizer { +public: + explicit AntColonyOptimizer(AntColonyConfig config = {}); + + AntColonyResult solve(const DenseGraph& graph) const; + const AntColonyConfig& config() const noexcept { return config_; } + +private: + AntColonyConfig config_; +}; + +} // namespace aco +} // namespace ga diff --git a/include/ga/aco/continuous_ant_colony.hpp b/include/ga/aco/continuous_ant_colony.hpp new file mode 100644 index 0000000..fc684a3 --- /dev/null +++ b/include/ga/aco/continuous_ant_colony.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include + +#include "ga/metaheuristics/common.hpp" + +namespace ga { +namespace aco { + +struct AcorConfig { + ga::metaheuristics::SearchConfig search; + std::size_t archiveSize = 50; + std::size_t sampleCount = 25; + double locality = 0.5; + double convergenceSpeed = 0.85; + std::shared_ptr controller; +}; + +class ContinuousAntColonyOptimizer final + : public ga::metaheuristics::IContinuousOptimizer { +public: + explicit ContinuousAntColonyOptimizer(AcorConfig config = {}); + + std::string name() const override { return "ACOR"; } + ga::core::OptimizationResult optimize( + const ga::Fitness& fitness, + const ga::metaheuristics::SeedPopulation& seeds = {}) override; + + const AcorConfig& config() const noexcept { return config_; } + +private: + AcorConfig config_; +}; + +} // namespace aco +} // namespace ga diff --git a/include/ga/config.hpp b/include/ga/config.hpp index 4302789..5d8588b 100644 --- a/include/ga/config.hpp +++ b/include/ga/config.hpp @@ -1,8 +1,9 @@ #pragma once -#include -#include +#include #include +#include +#include namespace ga { @@ -33,8 +34,10 @@ struct Config { struct Result { std::vector bestGenes; double bestFitness = -1e300; - std::vector bestHistory; // best per generation + std::vector bestHistory; // best-so-far per generation std::vector avgHistory; // average per generation + std::size_t evaluations = 0; + std::size_t iterations = 0; }; } // namespace ga diff --git a/include/ga/fuzzy/fuzzy_c_means.hpp b/include/ga/fuzzy/fuzzy_c_means.hpp new file mode 100644 index 0000000..9dc035f --- /dev/null +++ b/include/ga/fuzzy/fuzzy_c_means.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include +#include + +namespace ga { +namespace fuzzy { + +struct FuzzyCMeansConfig { + std::size_t clusters = 3; + std::size_t maxIterations = 300; + double fuzziness = 2.0; + double tolerance = 1e-6; + unsigned seed = 0; +}; + +struct FuzzyCMeansResult { + std::vector> centers; + std::vector> membership; + std::vector objectiveHistory; + std::size_t iterations = 0; + bool converged = false; + + std::vector labels() const; +}; + +class FuzzyCMeans { +public: + explicit FuzzyCMeans(FuzzyCMeansConfig config = {}); + + FuzzyCMeansResult fit(const std::vector>& data) const; + const FuzzyCMeansConfig& config() const noexcept { return config_; } + +private: + FuzzyCMeansConfig config_; +}; + +} // namespace fuzzy +} // namespace ga diff --git a/include/ga/fuzzy/fuzzy_controller.hpp b/include/ga/fuzzy/fuzzy_controller.hpp new file mode 100644 index 0000000..e3ca5bf --- /dev/null +++ b/include/ga/fuzzy/fuzzy_controller.hpp @@ -0,0 +1,139 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "ga/metaheuristics/common.hpp" + +namespace ga { +namespace fuzzy { + +struct FuzzyControllerConfig { + // Low is zero at lowZero, medium peaks at mediumCenter, and high starts at + // highStart. Inputs are normalized to [0, 1]. + double lowZero = 0.55; + double mediumCenter = 0.50; + double highStart = 0.45; + double improvementScale = 20.0; + + // Zero-order Sugeno consequents. Every multiplier is caller-configurable. + ga::metaheuristics::ControlSignal lowDiversityStagnant{1.55, 0.80, 1.30, 1.45}; + ga::metaheuristics::ControlSignal lowDiversitySlow{1.35, 0.90, 1.20, 1.30}; + ga::metaheuristics::ControlSignal balanced{1.00, 1.00, 1.00, 1.00}; + ga::metaheuristics::ControlSignal diverseProductive{0.78, 1.25, 0.85, 0.80}; + ga::metaheuristics::ControlSignal diverseStagnant{1.10, 0.95, 1.10, 1.15}; +}; + +// A zero-order Sugeno-style controller. Inputs and outputs are normalized so +// one controller can adapt PSO, ACOR, GSA, and ACO without knowing their units. +class FuzzyAdaptiveController final : public ga::metaheuristics::IAdaptiveController { +public: + explicit FuzzyAdaptiveController(FuzzyControllerConfig config = {}) + : config_(std::move(config)) { + validate(); + } + + ga::metaheuristics::ControlSignal update( + const ga::metaheuristics::ProgressState& state) const override { + const auto diversity = memberships(state.normalizedDiversity); + const auto progress = memberships( + std::clamp(state.relativeImprovement * config_.improvementScale, 0.0, 1.0)); + const auto stagnation = memberships(state.stagnation); + + // Consequents are ordered low, medium, high. Weighted-average + // defuzzification keeps parameter changes smooth between regimes. + WeightedOutput exploration; + WeightedOutput exploitation; + WeightedOutput evaporation; + WeightedOutput randomization; + + addSignal(exploration, exploitation, evaporation, randomization, + diversity[0] * stagnation[2], config_.lowDiversityStagnant); + addSignal(exploration, exploitation, evaporation, randomization, + diversity[0] * progress[0], config_.lowDiversitySlow); + addSignal(exploration, exploitation, evaporation, randomization, + diversity[1], config_.balanced); + addSignal(exploration, exploitation, evaporation, randomization, + diversity[2] * progress[2], config_.diverseProductive); + addSignal(exploration, exploitation, evaporation, randomization, + diversity[2] * stagnation[2], config_.diverseStagnant); + + ga::metaheuristics::ControlSignal signal; + signal.exploration = exploration.value(1.0); + signal.exploitation = exploitation.value(1.0); + signal.evaporation = evaporation.value(1.0); + signal.randomization = randomization.value(1.0); + return signal; + } + + const FuzzyControllerConfig& config() const noexcept { return config_; } + +private: + struct WeightedOutput { + double weightedSum = 0.0; + double weight = 0.0; + + double value(double fallback) const { + return weight > 0.0 ? weightedSum / weight : fallback; + } + }; + + static void addRule(WeightedOutput& output, double strength, double consequent) { + output.weightedSum += strength * consequent; + output.weight += strength; + } + + static void addSignal(WeightedOutput& exploration, + WeightedOutput& exploitation, + WeightedOutput& evaporation, + WeightedOutput& randomization, + double strength, + const ga::metaheuristics::ControlSignal& signal) { + addRule(exploration, strength, signal.exploration); + addRule(exploitation, strength, signal.exploitation); + addRule(evaporation, strength, signal.evaporation); + addRule(randomization, strength, signal.randomization); + } + + std::array memberships(double value) const { + const double x = std::clamp(value, 0.0, 1.0); + const double low = std::clamp( + (config_.lowZero - x) / config_.lowZero, 0.0, 1.0); + const double medium = x <= config_.mediumCenter + ? x / config_.mediumCenter + : (1.0 - x) / (1.0 - config_.mediumCenter); + const double high = std::clamp( + (x - config_.highStart) / (1.0 - config_.highStart), 0.0, 1.0); + return {low, std::clamp(medium, 0.0, 1.0), high}; + } + + static bool validSignal(const ga::metaheuristics::ControlSignal& signal) { + return std::isfinite(signal.exploration) && signal.exploration > 0.0 && + std::isfinite(signal.exploitation) && signal.exploitation > 0.0 && + std::isfinite(signal.evaporation) && signal.evaporation > 0.0 && + std::isfinite(signal.randomization) && signal.randomization > 0.0; + } + + void validate() const { + if (!std::isfinite(config_.lowZero) || !std::isfinite(config_.mediumCenter) || + !std::isfinite(config_.highStart) || + !std::isfinite(config_.improvementScale) || config_.highStart <= 0.0 || + config_.highStart > config_.mediumCenter || + config_.mediumCenter > config_.lowZero || config_.lowZero >= 1.0 || + config_.improvementScale <= 0.0 || + !validSignal(config_.lowDiversityStagnant) || + !validSignal(config_.lowDiversitySlow) || !validSignal(config_.balanced) || + !validSignal(config_.diverseProductive) || + !validSignal(config_.diverseStagnant)) { + throw std::invalid_argument("invalid fuzzy-controller configuration"); + } + } + + FuzzyControllerConfig config_; +}; + +} // namespace fuzzy +} // namespace ga diff --git a/include/ga/genetic_algorithm.hpp b/include/ga/genetic_algorithm.hpp index 91add5c..6102d20 100644 --- a/include/ga/genetic_algorithm.hpp +++ b/include/ga/genetic_algorithm.hpp @@ -19,6 +19,8 @@ class GeneticAlgorithm { // Run GA for provided fitness, return statistics Result run(const Fitness& fitness); + Result run(const Fitness& fitness, + const std::vector>& initialSolutions); // Access to operators for customization void setMutationOperator(std::unique_ptr op); @@ -37,9 +39,15 @@ class GeneticAlgorithm { std::unique_ptr mutation_; std::unique_ptr crossover_; - - std::vector initPopulation_(const Fitness& f); - std::pair crossoverPair_(const Individual& p1, const Individual& p2, const Fitness& f); + std::vector lowerBounds_; + std::vector upperBounds_; + + std::vector initPopulation_( + const Fitness& f, + const std::vector>& initialSolutions, + std::size_t& evaluations); + std::pair crossoverPair_(const Individual& p1, + const Individual& p2); void mutate_(Individual& ind); }; diff --git a/include/ga/gsa/gravitational_search.hpp b/include/ga/gsa/gravitational_search.hpp new file mode 100644 index 0000000..a041fdf --- /dev/null +++ b/include/ga/gsa/gravitational_search.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include + +#include "ga/metaheuristics/common.hpp" + +namespace ga { +namespace gsa { + +struct GsaConfig { + ga::metaheuristics::SearchConfig search; + double gravitationalConstant = 100.0; + double decay = 20.0; + double epsilon = 1e-12; + double finalEliteFraction = 0.02; + std::shared_ptr controller; +}; + +class GravitationalSearchOptimizer final + : public ga::metaheuristics::IContinuousOptimizer { +public: + explicit GravitationalSearchOptimizer(GsaConfig config = {}); + + std::string name() const override { return "GSA"; } + ga::core::OptimizationResult optimize( + const ga::Fitness& fitness, + const ga::metaheuristics::SeedPopulation& seeds = {}) override; + + const GsaConfig& config() const noexcept { return config_; } + +private: + GsaConfig config_; +}; + +} // namespace gsa +} // namespace ga diff --git a/include/ga/hybrid/metaheuristic_pipeline.hpp b/include/ga/hybrid/metaheuristic_pipeline.hpp new file mode 100644 index 0000000..bb06494 --- /dev/null +++ b/include/ga/hybrid/metaheuristic_pipeline.hpp @@ -0,0 +1,49 @@ +#pragma once + +#include +#include +#include +#include + +#include "ga/metaheuristics/common.hpp" + +namespace ga { +namespace hybrid { + +struct StageResult { + std::string optimizer; + ga::core::OptimizationResult result; +}; + +struct PipelineResult { + ga::core::OptimizationResult combined; + std::vector stages; +}; + +// Sequential heterogeneous hybrid. Each stage receives the best solutions +// found by prior stages as seeds, so this is solution transfer rather than a +// simple winner-takes-all portfolio. +class MetaheuristicPipeline final + : public ga::metaheuristics::IContinuousOptimizer { +public: + MetaheuristicPipeline& add( + std::unique_ptr optimizer); + MetaheuristicPipeline& addShared( + std::shared_ptr optimizer); + + std::string name() const override { return "hybrid-pipeline"; } + ga::core::OptimizationResult optimize( + const ga::Fitness& fitness, + const ga::metaheuristics::SeedPopulation& seeds = {}) override; + PipelineResult optimizeDetailed( + const ga::Fitness& fitness, + const ga::metaheuristics::SeedPopulation& seeds = {}); + + std::size_t size() const noexcept { return stages_.size(); } + +private: + std::vector> stages_; +}; + +} // namespace hybrid +} // namespace ga diff --git a/include/ga/metaheuristics.hpp b/include/ga/metaheuristics.hpp new file mode 100644 index 0000000..07561cb --- /dev/null +++ b/include/ga/metaheuristics.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include "ga/aco/ant_colony.hpp" +#include "ga/aco/continuous_ant_colony.hpp" +#include "ga/fuzzy/fuzzy_c_means.hpp" +#include "ga/fuzzy/fuzzy_controller.hpp" +#include "ga/gsa/gravitational_search.hpp" +#include "ga/hybrid/metaheuristic_pipeline.hpp" +#include "ga/metaheuristics/common.hpp" +#include "ga/metaheuristics/genetic_algorithm_adapter.hpp" +#include "ga/pso/particle_swarm.hpp" diff --git a/include/ga/metaheuristics/common.hpp b/include/ga/metaheuristics/common.hpp new file mode 100644 index 0000000..7ac336f --- /dev/null +++ b/include/ga/metaheuristics/common.hpp @@ -0,0 +1,278 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ga/config.hpp" +#include "ga/core/result.hpp" + +namespace ga { +namespace metaheuristics { + +using SeedPopulation = std::vector>; + +struct SearchConfig { + std::size_t populationSize = 50; + std::size_t iterations = 100; + std::size_t dimension = 10; + ga::Bounds bounds{-5.12, 5.12}; + unsigned seed = 0; + std::size_t threads = 1; +}; + +struct ProgressState { + std::size_t iteration = 0; + std::size_t maxIterations = 1; + double normalizedDiversity = 0.0; + double relativeImprovement = 0.0; + double stagnation = 0.0; +}; + +struct ControlSignal { + double exploration = 1.0; + double exploitation = 1.0; + double evaporation = 1.0; + double randomization = 1.0; +}; + +class IAdaptiveController { +public: + virtual ~IAdaptiveController() = default; + virtual ControlSignal update(const ProgressState& state) const = 0; +}; + +class IContinuousOptimizer { +public: + virtual ~IContinuousOptimizer() = default; + virtual std::string name() const = 0; + virtual ga::core::OptimizationResult optimize( + const ga::Fitness& fitness, + const SeedPopulation& seeds = {}) = 0; +}; + +namespace detail { + +inline void validateSearchConfig(const SearchConfig& cfg) { + if (cfg.populationSize == 0) { + throw std::invalid_argument("populationSize must be greater than zero"); + } + if (cfg.iterations == 0) { + throw std::invalid_argument("iterations must be greater than zero"); + } + if (cfg.dimension == 0) { + throw std::invalid_argument("dimension must be greater than zero"); + } + if (!std::isfinite(cfg.bounds.lower) || !std::isfinite(cfg.bounds.upper) || + cfg.bounds.lower >= cfg.bounds.upper || + !std::isfinite(cfg.bounds.upper - cfg.bounds.lower)) { + throw std::invalid_argument("bounds must be finite and lower < upper"); + } + if (cfg.threads == 0) { + throw std::invalid_argument("threads must be greater than zero"); + } +} + +inline std::mt19937 makeRng(unsigned seed) { + return seed == 0 ? std::mt19937{std::random_device{}()} : std::mt19937{seed}; +} + +inline void clampToBounds(std::vector& value, const ga::Bounds& bounds) { + for (double& x : value) { + x = std::clamp(x, bounds.lower, bounds.upper); + } +} + +inline SeedPopulation makePopulation(const SearchConfig& cfg, + const SeedPopulation& seeds, + std::mt19937& rng) { + validateSearchConfig(cfg); + SeedPopulation population; + population.reserve(cfg.populationSize); + + for (const auto& seed : seeds) { + if (population.size() == cfg.populationSize) { + break; + } + if (seed.size() != cfg.dimension) { + throw std::invalid_argument("seed solution dimension does not match SearchConfig"); + } + if (!std::all_of(seed.begin(), seed.end(), [](double value) { + return std::isfinite(value); + })) { + throw std::invalid_argument("seed solutions must contain only finite values"); + } + population.push_back(seed); + clampToBounds(population.back(), cfg.bounds); + } + + std::uniform_real_distribution uniform(cfg.bounds.lower, cfg.bounds.upper); + while (population.size() < cfg.populationSize) { + std::vector solution(cfg.dimension); + for (double& x : solution) { + x = uniform(rng); + } + population.push_back(std::move(solution)); + } + return population; +} + +inline std::vector evaluateBatch(const SeedPopulation& population, + const ga::Fitness& fitness, + std::size_t threads) { + if (!fitness) { + throw std::invalid_argument("fitness callback is empty"); + } + std::vector values(population.size()); + if (population.empty()) { + return values; + } + + const std::size_t workerCount = std::min( + std::max(1, threads), population.size()); + if (workerCount == 1) { + for (std::size_t i = 0; i < population.size(); ++i) { + values[i] = fitness(population[i]); + } + } else { + const std::size_t block = (population.size() + workerCount - 1) / workerCount; + std::vector> tasks; + tasks.reserve(workerCount); + for (std::size_t worker = 0; worker < workerCount; ++worker) { + const std::size_t begin = worker * block; + const std::size_t end = std::min(population.size(), begin + block); + if (begin >= end) { + break; + } + tasks.emplace_back(std::async(std::launch::async, [&, begin, end] { + for (std::size_t i = begin; i < end; ++i) { + values[i] = fitness(population[i]); + } + })); + } + for (auto& task : tasks) { + task.get(); + } + } + for (double value : values) { + if (!std::isfinite(value)) { + throw std::domain_error("fitness callback returned a non-finite value"); + } + } + return values; +} + +inline std::size_t bestIndex(const std::vector& fitness) { + if (fitness.empty()) { + throw std::invalid_argument("cannot select from an empty fitness vector"); + } + return static_cast( + std::distance(fitness.begin(), std::max_element(fitness.begin(), fitness.end()))); +} + +inline double mean(const std::vector& values) { + return values.empty() + ? 0.0 + : std::accumulate(values.begin(), values.end(), 0.0) / + static_cast(values.size()); +} + +inline double normalizedDiversity(const SeedPopulation& population, + const ga::Bounds& bounds) { + if (population.size() < 2 || population.front().empty()) { + return 0.0; + } + const std::size_t dimension = population.front().size(); + std::vector centroid(dimension, 0.0); + for (const auto& solution : population) { + for (std::size_t d = 0; d < dimension; ++d) { + centroid[d] += solution[d]; + } + } + for (double& value : centroid) { + value /= static_cast(population.size()); + } + + double squaredDistance = 0.0; + for (const auto& solution : population) { + for (std::size_t d = 0; d < dimension; ++d) { + const double delta = solution[d] - centroid[d]; + squaredDistance += delta * delta; + } + } + const double rms = std::sqrt( + squaredDistance / static_cast(population.size() * dimension)); + return std::clamp(rms / (bounds.upper - bounds.lower), 0.0, 1.0); +} + +inline double relativeImprovement(double current, double previous) { + if (!std::isfinite(previous)) { + return 1.0; + } + return std::max(0.0, current - previous) / + std::max(1.0, std::abs(previous)); +} + +inline ControlSignal controlSignal(const IAdaptiveController* controller, + std::size_t iteration, + std::size_t maxIterations, + double diversity, + double improvement, + std::size_t stagnantIterations) { + if (controller == nullptr) { + return {}; + } + ProgressState state; + state.iteration = iteration; + state.maxIterations = maxIterations; + state.normalizedDiversity = std::clamp(diversity, 0.0, 1.0); + state.relativeImprovement = std::clamp(improvement, 0.0, 1.0); + state.stagnation = maxIterations == 0 + ? 0.0 + : std::clamp(static_cast(stagnantIterations) / + static_cast(maxIterations), + 0.0, + 1.0); + ControlSignal signal = controller->update(state); + const bool valid = std::isfinite(signal.exploration) && + signal.exploration > 0.0 && + std::isfinite(signal.exploitation) && + signal.exploitation > 0.0 && + std::isfinite(signal.evaporation) && + signal.evaporation > 0.0 && + std::isfinite(signal.randomization) && + signal.randomization > 0.0; + if (!valid) { + throw std::domain_error( + "adaptive controller multipliers must be finite and greater than zero"); + } + return signal; +} + +inline void updateResult(ga::core::OptimizationResult& result, + const SeedPopulation& population, + const std::vector& fitness, + std::size_t evaluations) { + const std::size_t index = bestIndex(fitness); + if (result.bestSolution.empty() || fitness[index] > result.bestFitness) { + result.bestSolution = population[index]; + result.bestFitness = fitness[index]; + } + result.bestHistory.push_back(result.bestFitness); + result.avgHistory.push_back(mean(fitness)); + result.evaluations = evaluations; +} + +} // namespace detail +} // namespace metaheuristics +} // namespace ga diff --git a/include/ga/metaheuristics/genetic_algorithm_adapter.hpp b/include/ga/metaheuristics/genetic_algorithm_adapter.hpp new file mode 100644 index 0000000..1390310 --- /dev/null +++ b/include/ga/metaheuristics/genetic_algorithm_adapter.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include + +#include "ga/genetic_algorithm.hpp" +#include "ga/metaheuristics/common.hpp" + +namespace ga { +namespace metaheuristics { + +class GeneticAlgorithmAdapter final : public IContinuousOptimizer { +public: + explicit GeneticAlgorithmAdapter(ga::Config config) + : config_(std::move(config)) {} + + std::string name() const override { return "GA"; } + + ga::core::OptimizationResult optimize( + const ga::Fitness& fitness, + const SeedPopulation& seeds = {}) override { + ga::GeneticAlgorithm algorithm(config_); + const ga::Result source = algorithm.run(fitness, seeds); + + ga::core::OptimizationResult result; + result.bestSolution = source.bestGenes; + result.bestFitness = source.bestFitness; + result.bestHistory = source.bestHistory; + result.avgHistory = source.avgHistory; + result.evaluations = source.evaluations; + result.generations = source.iterations; + return result; + } + +private: + ga::Config config_; +}; + +} // namespace metaheuristics +} // namespace ga diff --git a/include/ga/pso/particle_swarm.hpp b/include/ga/pso/particle_swarm.hpp new file mode 100644 index 0000000..c43b0c3 --- /dev/null +++ b/include/ga/pso/particle_swarm.hpp @@ -0,0 +1,50 @@ +#pragma once + +#include + +#include "ga/metaheuristics/common.hpp" + +namespace ga { +namespace pso { + +enum class PsoVariant { + GlobalBest, + LocalBest, + Constriction, + BareBones, + FullyInformed, + QuantumBehaved, + Binary +}; + +struct PsoConfig { + ga::metaheuristics::SearchConfig search; + PsoVariant variant = PsoVariant::GlobalBest; + double inertia = 0.7298; + double cognitive = 1.49618; + double social = 1.49618; + double constriction = 0.7298; + double velocityClamp = 0.2; + double binaryVelocityClamp = 4.0; + std::size_t neighborhoodRadius = 1; + double quantumBeta = 0.75; + std::shared_ptr controller; +}; + +class ParticleSwarmOptimizer final : public ga::metaheuristics::IContinuousOptimizer { +public: + explicit ParticleSwarmOptimizer(PsoConfig config = {}); + + std::string name() const override; + ga::core::OptimizationResult optimize( + const ga::Fitness& fitness, + const ga::metaheuristics::SeedPopulation& seeds = {}) override; + + const PsoConfig& config() const noexcept { return config_; } + +private: + PsoConfig config_; +}; + +} // namespace pso +} // namespace ga diff --git a/pyproject.toml b/pyproject.toml index ed17c8f..f3edb51 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [build-system] requires = [ "scikit-build-core>=0.10", - "pybind11>=2.6", + "pybind11>=3.0", ] build-backend = "scikit_build_core.build" diff --git a/python/README.md b/python/README.md index 71dc681..eb6da7a 100644 --- a/python/README.md +++ b/python/README.md @@ -41,8 +41,54 @@ Run the bundled example: ```bash python3 python/example.py +python3 python/metaheuristics_example.py ``` +## User-configured metaheuristics + +All metaheuristics in the C++ suite are also exposed through Python: + +- PSO: global-best, local-best, constriction, bare-bones, fully informed, + quantum-behaved, and binary +- Graph ACO: Ant System, Elitist AS, Rank-Based AS, ACS, and MAX-MIN AS +- Continuous ACOR and continuous GSA +- Fuzzy C-means and the optional configurable fuzzy controller +- The user-ordered `MetaheuristicPipeline` and `GeneticAlgorithmAdapter` + +```python +import genetic_algorithm_lib as ga + +search = ga.SearchConfig() +search.population_size = 40 +search.iterations = 50 +search.dimension = 5 +search.bounds = ga.Bounds(-5.0, 5.0) +search.seed = 42 + +pso_cfg = ga.PsoConfig() +pso_cfg.search = search +pso_cfg.variant = ga.PsoVariant.constriction + +gsa_cfg = ga.GsaConfig() +gsa_cfg.search = search + +pipeline = ga.MetaheuristicPipeline() +pipeline.add(ga.ParticleSwarmOptimizer(pso_cfg)) +pipeline.add(ga.GravitationalSearchOptimizer(gsa_cfg)) + +result = pipeline.optimize(lambda x: 1.0 / (1.0 + sum(v * v for v in x))) +``` + +The library never chooses or reorders stages. Controllers default to `None`, +so adaptive behavior occurs only when the caller assigns one. Python fitness +callbacks are supported with any `SearchConfig.threads` value, although pure +Python callbacks remain limited by the Python GIL and may not become faster +with multiple evaluation threads. + +Advanced users may subclass `AdaptiveController` or `ContinuousOptimizer` in +Python and insert those implementations into the same pipeline. Their Python +object lifetimes are retained safely by the C++ core. + ## NSGA-II Objective-Space Utilities - `ga.Nsga2` diff --git a/python/bindings_sanity.py b/python/bindings_sanity.py index 2511917..f15ad49 100644 --- a/python/bindings_sanity.py +++ b/python/bindings_sanity.py @@ -11,7 +11,10 @@ # Allow running from the source tree after a local CMake build. repo_root = os.path.join(os.path.dirname(__file__), "..") sys.path.insert(0, os.path.join(repo_root, "python")) - sys.path.insert(0, os.path.join(repo_root, "build", "python")) + module_dir = os.environ.get( + "GA_PYTHON_MODULE_DIR", os.path.join(repo_root, "build", "python") + ) + sys.path.insert(0, module_dir) import genetic_algorithm_lib as ga @@ -60,6 +63,143 @@ def main() -> None: res = opt.optimize(sphere_fitness) assert len(res.best_genes) == 3 + # User-configured metaheuristics + search = ga.SearchConfig() + search.population_size = 12 + search.iterations = 5 + search.dimension = 3 + search.bounds = ga.Bounds(-2.0, 2.0) + search.seed = 31 + search.threads = 2 + + fuzzy_cfg = ga.FuzzyControllerConfig() + fuzzy_cfg.improvement_scale = 12.0 + fuzzy_cfg.low_diversity_stagnant = ga.ControlSignal(1.8, 0.8, 1.2, 1.5) + fuzzy = ga.FuzzyAdaptiveController(fuzzy_cfg) + state = ga.ProgressState() + state.stagnation = 1.0 + assert fuzzy.update(state).exploration > 1.0 + + class FixedController(ga.AdaptiveController): + def update(self, _state): + return ga.ControlSignal(1.2, 1.0, 1.0, 1.0) + + custom_controller_cfg = ga.PsoConfig() + custom_controller_cfg.search = search + # Deliberately assign a temporary object: the C++ config must retain the + # Python override for the optimizer's full lifetime. + custom_controller_cfg.controller = FixedController() + custom_controller_result = ga.ParticleSwarmOptimizer( + custom_controller_cfg + ).optimize(sphere_fitness) + assert len(custom_controller_result.best_solution) == 3 + + pso_variants = [ + ga.PsoVariant.global_best, + ga.PsoVariant.local_best, + ga.PsoVariant.constriction, + ga.PsoVariant.bare_bones, + ga.PsoVariant.fully_informed, + ga.PsoVariant.quantum_behaved, + ga.PsoVariant.binary, + ] + for index, variant in enumerate(pso_variants): + pso_cfg = ga.PsoConfig() + pso_cfg.search = search + pso_cfg.search.seed = 40 + index + pso_cfg.variant = variant + pso_cfg.controller = fuzzy + if variant == ga.PsoVariant.binary: + pso_cfg.search.bounds = ga.Bounds(0.0, 1.0) + pso_result = ga.ParticleSwarmOptimizer(pso_cfg).optimize(sphere_fitness) + assert len(pso_result.best_solution) == 3 + + graph = ga.DenseGraph([ + [0.0, 2.0, 9.0, 10.0], + [2.0, 0.0, 6.0, 4.0], + [9.0, 6.0, 0.0, 8.0], + [10.0, 4.0, 8.0, 0.0], + ]) + aco_variants = [ + ga.AntColonyVariant.ant_system, + ga.AntColonyVariant.elitist_ant_system, + ga.AntColonyVariant.rank_based_ant_system, + ga.AntColonyVariant.ant_colony_system, + ga.AntColonyVariant.max_min_ant_system, + ] + for index, variant in enumerate(aco_variants): + aco_cfg = ga.AntColonyConfig() + aco_cfg.ants = 8 + aco_cfg.iterations = 5 + aco_cfg.seed = 60 + index + aco_cfg.variant = variant + aco_cfg.controller = fuzzy + tour = ga.AntColonyOptimizer(aco_cfg).solve(graph) + assert len(tour.best_tour) == graph.size + + acor_cfg = ga.AcorConfig() + acor_cfg.search = search + acor_cfg.archive_size = 12 + acor_cfg.sample_count = 8 + acor_cfg.controller = fuzzy + acor = ga.ContinuousAntColonyOptimizer(acor_cfg) + assert len(acor.optimize(sphere_fitness).best_solution) == 3 + + gsa_cfg = ga.GsaConfig() + gsa_cfg.search = search + gsa_cfg.controller = fuzzy + gsa = ga.GravitationalSearchOptimizer(gsa_cfg) + assert len(gsa.optimize(sphere_fitness).best_solution) == 3 + + fcm_cfg = ga.FuzzyCMeansConfig() + fcm_cfg.clusters = 2 + fcm_cfg.seed = 73 + clusters = ga.FuzzyCMeans(fcm_cfg).fit([ + [0.0, 0.1], [0.1, 0.0], [-0.1, 0.0], + [9.9, 10.0], [10.0, 9.9], [10.1, 10.0], + ]) + assert len(clusters.centers) == 2 + assert len(clusters.labels()) == 6 + + # The user alone selects the hybrid members and their exact order. + ga_cfg = ga.Config() + ga_cfg.population_size = 12 + ga_cfg.generations = 5 + ga_cfg.dimension = 3 + ga_cfg.bounds = ga.Bounds(-2.0, 2.0) + ga_cfg.seed = 81 + pipeline_pso_cfg = ga.PsoConfig() + pipeline_pso_cfg.search = search + pipeline_pso_cfg.variant = ga.PsoVariant.constriction + pipeline = ga.MetaheuristicPipeline() + pipeline.add(ga.GeneticAlgorithmAdapter(ga_cfg)) + pipeline.add(ga.ParticleSwarmOptimizer(pipeline_pso_cfg)) + pipeline.add(ga.ContinuousAntColonyOptimizer(acor_cfg)) + hybrid_result = pipeline.optimize_detailed(sphere_fitness) + assert [stage.optimizer for stage in hybrid_result.stages] == [ + "GA", "PSO-constriction", "ACOR" + ] + assert len(hybrid_result.combined.best_solution) == 3 + + class PythonStage(ga.ContinuousOptimizer): + def name(self): + return "python-stage" + + def optimize(self, fitness, seeds): + stage_result = ga.OptimizationResult() + stage_result.best_solution = list(seeds[0]) if seeds else [0.0] * 3 + stage_result.best_fitness = fitness(stage_result.best_solution) + stage_result.best_history = [stage_result.best_fitness] + stage_result.avg_history = [stage_result.best_fitness] + stage_result.evaluations = 1 + stage_result.generations = 1 + return stage_result + + extension_pipeline = ga.MetaheuristicPipeline() + extension_pipeline.add(PythonStage()) + extension_result = extension_pipeline.optimize_detailed(sphere_fitness) + assert extension_result.stages[0].optimizer == "python-stage" + mo = opt.optimize_multi_objective_nsga2( [lambda x: x[0] * x[0], lambda x: (x[0] - 1.0) * (x[0] - 1.0)], population_size=16, diff --git a/python/ga_bindings.cpp b/python/ga_bindings.cpp index fb04b10..e0289f3 100644 --- a/python/ga_bindings.cpp +++ b/python/ga_bindings.cpp @@ -6,6 +6,9 @@ * - ga.Bounds - Gene bounds (lower, upper) * - ga.Result - Run results * - ga.GeneticAlgorithm - Main GA class + * - ga.ParticleSwarmOptimizer / AntColonyOptimizer / GSA / ACOR + * - ga.FuzzyCMeans / FuzzyAdaptiveController + * - ga.MetaheuristicPipeline - User-ordered optimizer composition * - Operator factories: make_gaussian_mutation, make_uniform_mutation, * make_one_point_crossover, make_two_point_crossover */ @@ -14,6 +17,7 @@ #include #include +#include #include #include #include @@ -39,6 +43,7 @@ #include "ga/gp/tree_builder.hpp" #include "ga/gp/type_system.hpp" #include "ga/hybrid/hybrid_optimizer.hpp" +#include "ga/metaheuristics.hpp" #include "ga/moea/nsga3.hpp" #include "ga/moea/mo_cmaes.hpp" #include "ga/moea/spea2.hpp" @@ -156,6 +161,65 @@ using DoubleBatchEvaluator = double, std::function&)>>; +class PyAdaptiveController : public ga::metaheuristics::IAdaptiveController, + public py::trampoline_self_life_support { +public: + using ga::metaheuristics::IAdaptiveController::IAdaptiveController; + + ga::metaheuristics::ControlSignal update( + const ga::metaheuristics::ProgressState& state) const override { + PYBIND11_OVERRIDE_PURE( + ga::metaheuristics::ControlSignal, + ga::metaheuristics::IAdaptiveController, + update, + state); + } +}; + +class PyContinuousOptimizer : public ga::metaheuristics::IContinuousOptimizer, + public py::trampoline_self_life_support { +public: + using ga::metaheuristics::IContinuousOptimizer::IContinuousOptimizer; + + std::string name() const override { + PYBIND11_OVERRIDE_PURE( + std::string, ga::metaheuristics::IContinuousOptimizer, name); + } + + ga::core::OptimizationResult optimize( + const ga::Fitness& fitness, + const ga::metaheuristics::SeedPopulation& seeds) override { + PYBIND11_OVERRIDE_PURE( + ga::core::OptimizationResult, + ga::metaheuristics::IContinuousOptimizer, + optimize, + fitness, + seeds); + } +}; + +static ga::Fitness wrapPythonFitness(py::function fitness) { + return [fitness = std::move(fitness)](const std::vector& genes) { + py::gil_scoped_acquire acquire; + return fitness(genes).cast(); + }; +} + +template +static ga::core::OptimizationResult optimizeFromPython( + Optimizer& optimizer, + py::function fitness, + const ga::metaheuristics::SeedPopulation& seeds) { + ga::Fitness wrapped = wrapPythonFitness(std::move(fitness)); + py::gil_scoped_release release; + return optimizer.optimize(wrapped, seeds); +} + +static std::shared_ptr mutableController( + const std::shared_ptr& controller) { + return std::const_pointer_cast(controller); +} + PYBIND11_MODULE(_core, m) { m.doc() = "Genetic Algorithm framework — C++ core with Python bindings"; @@ -195,6 +259,8 @@ PYBIND11_MODULE(_core, m) { .def_readonly("best_fitness", &ga::Result::bestFitness, "Fitness of the best individual") .def_readonly("best_history", &ga::Result::bestHistory, "Best fitness per generation") .def_readonly("avg_history", &ga::Result::avgHistory, "Average fitness per generation") + .def_readonly("evaluations", &ga::Result::evaluations, "Number of fitness evaluations") + .def_readonly("iterations", &ga::Result::iterations, "Number of completed generations") .def("__repr__", [](const ga::Result& r){ return ""; }); @@ -210,6 +276,365 @@ PYBIND11_MODULE(_core, m) { .def_readwrite("evaluations", &ga::core::OptimizationResult::evaluations) .def_readwrite("generations", &ga::core::OptimizationResult::generations); + // ----------------------------------------------------------- Metaheuristics + py::class_(m, "SearchConfig") + .def(py::init<>()) + .def_readwrite("population_size", &ga::metaheuristics::SearchConfig::populationSize) + .def_readwrite("iterations", &ga::metaheuristics::SearchConfig::iterations) + .def_readwrite("dimension", &ga::metaheuristics::SearchConfig::dimension) + .def_readwrite("bounds", &ga::metaheuristics::SearchConfig::bounds) + .def_readwrite("seed", &ga::metaheuristics::SearchConfig::seed) + .def_readwrite("threads", &ga::metaheuristics::SearchConfig::threads); + + py::class_(m, "ProgressState") + .def(py::init<>()) + .def_readwrite("iteration", &ga::metaheuristics::ProgressState::iteration) + .def_readwrite("max_iterations", &ga::metaheuristics::ProgressState::maxIterations) + .def_readwrite("normalized_diversity", + &ga::metaheuristics::ProgressState::normalizedDiversity) + .def_readwrite("relative_improvement", + &ga::metaheuristics::ProgressState::relativeImprovement) + .def_readwrite("stagnation", &ga::metaheuristics::ProgressState::stagnation); + + py::class_(m, "ControlSignal") + .def(py::init<>()) + .def(py::init([](double exploration, + double exploitation, + double evaporation, + double randomization) { + ga::metaheuristics::ControlSignal signal; + signal.exploration = exploration; + signal.exploitation = exploitation; + signal.evaporation = evaporation; + signal.randomization = randomization; + return signal; + }), + py::arg("exploration") = 1.0, + py::arg("exploitation") = 1.0, + py::arg("evaporation") = 1.0, + py::arg("randomization") = 1.0) + .def_readwrite("exploration", &ga::metaheuristics::ControlSignal::exploration) + .def_readwrite("exploitation", &ga::metaheuristics::ControlSignal::exploitation) + .def_readwrite("evaporation", &ga::metaheuristics::ControlSignal::evaporation) + .def_readwrite("randomization", &ga::metaheuristics::ControlSignal::randomization); + + py::class_( + m, "AdaptiveController", "Base class for optional adaptive controllers") + .def(py::init<>()) + .def("update", &ga::metaheuristics::IAdaptiveController::update, + py::arg("state")); + + py::class_(m, "FuzzyControllerConfig") + .def(py::init<>()) + .def_readwrite("low_zero", &ga::fuzzy::FuzzyControllerConfig::lowZero) + .def_readwrite("medium_center", &ga::fuzzy::FuzzyControllerConfig::mediumCenter) + .def_readwrite("high_start", &ga::fuzzy::FuzzyControllerConfig::highStart) + .def_readwrite("improvement_scale", + &ga::fuzzy::FuzzyControllerConfig::improvementScale) + .def_readwrite("low_diversity_stagnant", + &ga::fuzzy::FuzzyControllerConfig::lowDiversityStagnant) + .def_readwrite("low_diversity_slow", + &ga::fuzzy::FuzzyControllerConfig::lowDiversitySlow) + .def_readwrite("balanced", &ga::fuzzy::FuzzyControllerConfig::balanced) + .def_readwrite("diverse_productive", + &ga::fuzzy::FuzzyControllerConfig::diverseProductive) + .def_readwrite("diverse_stagnant", + &ga::fuzzy::FuzzyControllerConfig::diverseStagnant); + + py::class_( + m, "FuzzyAdaptiveController") + .def(py::init(), + py::arg("config") = ga::fuzzy::FuzzyControllerConfig{}) + .def("update", &ga::fuzzy::FuzzyAdaptiveController::update, + py::arg("state")) + .def_property_readonly("config", [](const ga::fuzzy::FuzzyAdaptiveController& self) { + return self.config(); + }); + + py::class_( + m, "ContinuousOptimizer", "Base class for continuous optimizers") + .def(py::init<>()) + .def("name", &ga::metaheuristics::IContinuousOptimizer::name) + .def("optimize", + [](ga::metaheuristics::IContinuousOptimizer& self, + py::function fitness, + const ga::metaheuristics::SeedPopulation& seeds) { + return optimizeFromPython(self, std::move(fitness), seeds); + }, + py::arg("fitness"), + py::arg("seeds") = ga::metaheuristics::SeedPopulation{}); + + py::enum_(m, "PsoVariant") + .value("global_best", ga::pso::PsoVariant::GlobalBest) + .value("local_best", ga::pso::PsoVariant::LocalBest) + .value("constriction", ga::pso::PsoVariant::Constriction) + .value("bare_bones", ga::pso::PsoVariant::BareBones) + .value("fully_informed", ga::pso::PsoVariant::FullyInformed) + .value("quantum_behaved", ga::pso::PsoVariant::QuantumBehaved) + .value("binary", ga::pso::PsoVariant::Binary); + + py::class_(m, "PsoConfig") + .def(py::init<>()) + .def_readwrite("search", &ga::pso::PsoConfig::search) + .def_readwrite("variant", &ga::pso::PsoConfig::variant) + .def_readwrite("inertia", &ga::pso::PsoConfig::inertia) + .def_readwrite("cognitive", &ga::pso::PsoConfig::cognitive) + .def_readwrite("social", &ga::pso::PsoConfig::social) + .def_readwrite("constriction", &ga::pso::PsoConfig::constriction) + .def_readwrite("velocity_clamp", &ga::pso::PsoConfig::velocityClamp) + .def_readwrite("binary_velocity_clamp", + &ga::pso::PsoConfig::binaryVelocityClamp) + .def_readwrite("neighborhood_radius", + &ga::pso::PsoConfig::neighborhoodRadius) + .def_readwrite("quantum_beta", &ga::pso::PsoConfig::quantumBeta) + .def_property("controller", + [](const ga::pso::PsoConfig& config) { + return mutableController(config.controller); + }, + [](ga::pso::PsoConfig& config, + std::shared_ptr controller) { + config.controller = std::move(controller); + }); + + py::class_( + m, "ParticleSwarmOptimizer") + .def(py::init(), py::arg("config") = ga::pso::PsoConfig{}) + .def("name", &ga::pso::ParticleSwarmOptimizer::name) + .def("optimize", + [](ga::pso::ParticleSwarmOptimizer& self, + py::function fitness, + const ga::metaheuristics::SeedPopulation& seeds) { + return optimizeFromPython(self, std::move(fitness), seeds); + }, + py::arg("fitness"), + py::arg("seeds") = ga::metaheuristics::SeedPopulation{}) + .def_property_readonly("config", [](const ga::pso::ParticleSwarmOptimizer& self) { + return self.config(); + }); + + py::class_(m, "DenseGraph") + .def(py::init>, bool>(), + py::arg("costs"), py::arg("symmetric") = true) + .def_property_readonly("size", &ga::aco::DenseGraph::size) + .def_property_readonly("symmetric", &ga::aco::DenseGraph::symmetric) + .def("cost", &ga::aco::DenseGraph::cost, py::arg("from_node"), py::arg("to_node")); + + py::enum_(m, "AntColonyVariant") + .value("ant_system", ga::aco::AntColonyVariant::AntSystem) + .value("elitist_ant_system", ga::aco::AntColonyVariant::ElitistAntSystem) + .value("rank_based_ant_system", ga::aco::AntColonyVariant::RankBasedAntSystem) + .value("ant_colony_system", ga::aco::AntColonyVariant::AntColonySystem) + .value("max_min_ant_system", ga::aco::AntColonyVariant::MaxMinAntSystem); + + py::class_(m, "AntColonyConfig") + .def(py::init<>()) + .def_readwrite("ants", &ga::aco::AntColonyConfig::ants) + .def_readwrite("iterations", &ga::aco::AntColonyConfig::iterations) + .def_readwrite("alpha", &ga::aco::AntColonyConfig::alpha) + .def_readwrite("beta", &ga::aco::AntColonyConfig::beta) + .def_readwrite("evaporation", &ga::aco::AntColonyConfig::evaporation) + .def_readwrite("deposit_scale", &ga::aco::AntColonyConfig::depositScale) + .def_readwrite("initial_pheromone", &ga::aco::AntColonyConfig::initialPheromone) + .def_readwrite("elitist_weight", &ga::aco::AntColonyConfig::elitistWeight) + .def_readwrite("rank_count", &ga::aco::AntColonyConfig::rankCount) + .def_readwrite("exploitation_probability", + &ga::aco::AntColonyConfig::exploitationProbability) + .def_readwrite("local_evaporation", &ga::aco::AntColonyConfig::localEvaporation) + .def_readwrite("candidate_list_size", &ga::aco::AntColonyConfig::candidateListSize) + .def_readwrite("min_pheromone", &ga::aco::AntColonyConfig::minPheromone) + .def_readwrite("max_pheromone", &ga::aco::AntColonyConfig::maxPheromone) + .def_readwrite("variant", &ga::aco::AntColonyConfig::variant) + .def_readwrite("seed", &ga::aco::AntColonyConfig::seed) + .def_property("controller", + [](const ga::aco::AntColonyConfig& config) { + return mutableController(config.controller); + }, + [](ga::aco::AntColonyConfig& config, + std::shared_ptr controller) { + config.controller = std::move(controller); + }); + + py::class_(m, "AntColonyResult") + .def(py::init<>()) + .def_readonly("best_tour", &ga::aco::AntColonyResult::bestTour) + .def_readonly("best_cost", &ga::aco::AntColonyResult::bestCost) + .def_readonly("best_cost_history", &ga::aco::AntColonyResult::bestCostHistory) + .def_readonly("evaluations", &ga::aco::AntColonyResult::evaluations) + .def_readonly("iterations", &ga::aco::AntColonyResult::iterations); + + py::class_(m, "AntColonyOptimizer") + .def(py::init(), + py::arg("config") = ga::aco::AntColonyConfig{}) + .def("solve", [](const ga::aco::AntColonyOptimizer& self, + const ga::aco::DenseGraph& graph) { + py::gil_scoped_release release; + return self.solve(graph); + }, py::arg("graph")) + .def_property_readonly("config", [](const ga::aco::AntColonyOptimizer& self) { + return self.config(); + }); + + py::class_(m, "AcorConfig") + .def(py::init<>()) + .def_readwrite("search", &ga::aco::AcorConfig::search) + .def_readwrite("archive_size", &ga::aco::AcorConfig::archiveSize) + .def_readwrite("sample_count", &ga::aco::AcorConfig::sampleCount) + .def_readwrite("locality", &ga::aco::AcorConfig::locality) + .def_readwrite("convergence_speed", &ga::aco::AcorConfig::convergenceSpeed) + .def_property("controller", + [](const ga::aco::AcorConfig& config) { + return mutableController(config.controller); + }, + [](ga::aco::AcorConfig& config, + std::shared_ptr controller) { + config.controller = std::move(controller); + }); + + py::class_( + m, "ContinuousAntColonyOptimizer") + .def(py::init(), py::arg("config") = ga::aco::AcorConfig{}) + .def("name", &ga::aco::ContinuousAntColonyOptimizer::name) + .def("optimize", + [](ga::aco::ContinuousAntColonyOptimizer& self, + py::function fitness, + const ga::metaheuristics::SeedPopulation& seeds) { + return optimizeFromPython(self, std::move(fitness), seeds); + }, + py::arg("fitness"), + py::arg("seeds") = ga::metaheuristics::SeedPopulation{}) + .def_property_readonly("config", + [](const ga::aco::ContinuousAntColonyOptimizer& self) { + return self.config(); + }); + + py::class_(m, "GsaConfig") + .def(py::init<>()) + .def_readwrite("search", &ga::gsa::GsaConfig::search) + .def_readwrite("gravitational_constant", &ga::gsa::GsaConfig::gravitationalConstant) + .def_readwrite("decay", &ga::gsa::GsaConfig::decay) + .def_readwrite("epsilon", &ga::gsa::GsaConfig::epsilon) + .def_readwrite("final_elite_fraction", &ga::gsa::GsaConfig::finalEliteFraction) + .def_property("controller", + [](const ga::gsa::GsaConfig& config) { + return mutableController(config.controller); + }, + [](ga::gsa::GsaConfig& config, + std::shared_ptr controller) { + config.controller = std::move(controller); + }); + + py::class_( + m, "GravitationalSearchOptimizer") + .def(py::init(), py::arg("config") = ga::gsa::GsaConfig{}) + .def("name", &ga::gsa::GravitationalSearchOptimizer::name) + .def("optimize", + [](ga::gsa::GravitationalSearchOptimizer& self, + py::function fitness, + const ga::metaheuristics::SeedPopulation& seeds) { + return optimizeFromPython(self, std::move(fitness), seeds); + }, + py::arg("fitness"), + py::arg("seeds") = ga::metaheuristics::SeedPopulation{}) + .def_property_readonly("config", + [](const ga::gsa::GravitationalSearchOptimizer& self) { + return self.config(); + }); + + py::class_(m, "FuzzyCMeansConfig") + .def(py::init<>()) + .def_readwrite("clusters", &ga::fuzzy::FuzzyCMeansConfig::clusters) + .def_readwrite("max_iterations", &ga::fuzzy::FuzzyCMeansConfig::maxIterations) + .def_readwrite("fuzziness", &ga::fuzzy::FuzzyCMeansConfig::fuzziness) + .def_readwrite("tolerance", &ga::fuzzy::FuzzyCMeansConfig::tolerance) + .def_readwrite("seed", &ga::fuzzy::FuzzyCMeansConfig::seed); + + py::class_(m, "FuzzyCMeansResult") + .def(py::init<>()) + .def_readonly("centers", &ga::fuzzy::FuzzyCMeansResult::centers) + .def_readonly("membership", &ga::fuzzy::FuzzyCMeansResult::membership) + .def_readonly("objective_history", &ga::fuzzy::FuzzyCMeansResult::objectiveHistory) + .def_readonly("iterations", &ga::fuzzy::FuzzyCMeansResult::iterations) + .def_readonly("converged", &ga::fuzzy::FuzzyCMeansResult::converged) + .def("labels", &ga::fuzzy::FuzzyCMeansResult::labels); + + py::class_(m, "FuzzyCMeans") + .def(py::init(), + py::arg("config") = ga::fuzzy::FuzzyCMeansConfig{}) + .def("fit", &ga::fuzzy::FuzzyCMeans::fit, py::arg("data"), + py::call_guard()) + .def_property_readonly("config", [](const ga::fuzzy::FuzzyCMeans& self) { + return self.config(); + }); + + py::class_( + m, "GeneticAlgorithmAdapter") + .def(py::init(), py::arg("config")) + .def("name", &ga::metaheuristics::GeneticAlgorithmAdapter::name) + .def("optimize", + [](ga::metaheuristics::GeneticAlgorithmAdapter& self, + py::function fitness, + const ga::metaheuristics::SeedPopulation& seeds) { + return optimizeFromPython(self, std::move(fitness), seeds); + }, + py::arg("fitness"), + py::arg("seeds") = ga::metaheuristics::SeedPopulation{}); + + py::class_(m, "MetaheuristicStageResult") + .def_readonly("optimizer", &ga::hybrid::StageResult::optimizer) + .def_readonly("result", &ga::hybrid::StageResult::result); + + py::class_(m, "MetaheuristicPipelineResult") + .def_readonly("combined", &ga::hybrid::PipelineResult::combined) + .def_readonly("stages", &ga::hybrid::PipelineResult::stages); + + py::class_( + m, "MetaheuristicPipeline") + .def(py::init<>()) + .def("add", + [](ga::hybrid::MetaheuristicPipeline& self, + std::shared_ptr optimizer) + -> ga::hybrid::MetaheuristicPipeline& { + return self.addShared(std::move(optimizer)); + }, + py::arg("optimizer"), + py::return_value_policy::reference_internal) + .def("name", &ga::hybrid::MetaheuristicPipeline::name) + .def("size", &ga::hybrid::MetaheuristicPipeline::size) + .def("optimize", + [](ga::hybrid::MetaheuristicPipeline& self, + py::function fitness, + const ga::metaheuristics::SeedPopulation& seeds) { + return optimizeFromPython(self, std::move(fitness), seeds); + }, + py::arg("fitness"), + py::arg("seeds") = ga::metaheuristics::SeedPopulation{}) + .def("optimize_detailed", + [](ga::hybrid::MetaheuristicPipeline& self, + py::function fitness, + const ga::metaheuristics::SeedPopulation& seeds) { + ga::Fitness wrapped = wrapPythonFitness(std::move(fitness)); + py::gil_scoped_release release; + return self.optimizeDetailed(wrapped, seeds); + }, + py::arg("fitness"), + py::arg("seeds") = ga::metaheuristics::SeedPopulation{}); + // ---------------------------------------------------------- Core abstractions py::class_(m, "Evaluation", "Evaluation/objective record") .def(py::init<>()) @@ -374,7 +799,11 @@ PYBIND11_MODULE(_core, m) { >>> print(result.best_fitness) )") .def(py::init(), py::arg("config")) - .def("run", &ga::GeneticAlgorithm::run, py::arg("fitness"), + .def("run", + py::overload_cast>&>( + &ga::GeneticAlgorithm::run), + py::arg("fitness"), py::arg("initial_solutions") = std::vector>{}, "Run the GA with the given fitness callable (list[float] -> float). Higher is better.") .def("set_mutation_operator", &ga::GeneticAlgorithm::setMutationOperator, py::arg("op"), "Set a custom mutation operator") diff --git a/python/metaheuristics_example.py b/python/metaheuristics_example.py new file mode 100644 index 0000000..4e592b9 --- /dev/null +++ b/python/metaheuristics_example.py @@ -0,0 +1,68 @@ +"""Run user-configured C++ metaheuristics from Python.""" + +from __future__ import annotations + +import genetic_algorithm_lib as ga + + +def sphere_fitness(solution: list[float]) -> float: + return 1.0 / (1.0 + sum(value * value for value in solution)) + + +def search_config(seed: int) -> ga.SearchConfig: + config = ga.SearchConfig() + config.population_size = 40 + config.iterations = 50 + config.dimension = 5 + config.bounds = ga.Bounds(-5.0, 5.0) + config.seed = seed + config.threads = 1 + return config + + +def main() -> None: + # Optional fuzzy control. Leave each optimizer's controller as None when + # fixed, user-supplied algorithm parameters are desired. + fuzzy_config = ga.FuzzyControllerConfig() + fuzzy_config.improvement_scale = 20.0 + fuzzy_config.low_diversity_stagnant = ga.ControlSignal(1.55, 0.80, 1.30, 1.45) + fuzzy = ga.FuzzyAdaptiveController(fuzzy_config) + + ga_config = ga.Config() + ga_config.population_size = 40 + ga_config.generations = 50 + ga_config.dimension = 5 + ga_config.bounds = ga.Bounds(-5.0, 5.0) + ga_config.seed = 42 + + pso_config = ga.PsoConfig() + pso_config.search = search_config(43) + pso_config.variant = ga.PsoVariant.constriction + pso_config.inertia = 0.7298 + pso_config.cognitive = 1.49618 + pso_config.social = 1.49618 + pso_config.controller = fuzzy + + acor_config = ga.AcorConfig() + acor_config.search = search_config(44) + acor_config.archive_size = 40 + acor_config.sample_count = 20 + acor_config.locality = 0.5 + acor_config.convergence_speed = 0.85 + acor_config.controller = fuzzy + + # Only these explicitly added stages run, in precisely this order. + pipeline = ga.MetaheuristicPipeline() + pipeline.add(ga.GeneticAlgorithmAdapter(ga_config)) + pipeline.add(ga.ParticleSwarmOptimizer(pso_config)) + pipeline.add(ga.ContinuousAntColonyOptimizer(acor_config)) + + result = pipeline.optimize_detailed(sphere_fitness) + print("Best fitness:", result.combined.best_fitness) + print("Evaluations:", result.combined.evaluations) + for stage in result.stages: + print(stage.optimizer, stage.result.best_fitness) + + +if __name__ == "__main__": + main() diff --git a/src/aco/ant_colony.cpp b/src/aco/ant_colony.cpp new file mode 100644 index 0000000..bb1f255 --- /dev/null +++ b/src/aco/ant_colony.cpp @@ -0,0 +1,379 @@ +#include "ga/aco/ant_colony.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ga { +namespace aco { +namespace { + +struct Tour { + std::vector nodes; + double cost = std::numeric_limits::infinity(); +}; + +void validate(const AntColonyConfig& config) { + const bool finite = std::isfinite(config.alpha) && std::isfinite(config.beta) && + std::isfinite(config.evaporation) && + std::isfinite(config.depositScale) && + std::isfinite(config.initialPheromone) && + std::isfinite(config.elitistWeight) && + std::isfinite(config.exploitationProbability) && + std::isfinite(config.localEvaporation) && + std::isfinite(config.minPheromone) && + std::isfinite(config.maxPheromone); + if (!finite || config.ants == 0 || config.iterations == 0 || config.alpha < 0.0 || + config.beta < 0.0 || config.evaporation <= 0.0 || + config.evaporation >= 1.0 || config.depositScale <= 0.0 || + config.initialPheromone <= 0.0 || config.elitistWeight < 0.0 || + config.rankCount == 0 || config.exploitationProbability < 0.0 || + config.exploitationProbability > 1.0 || config.localEvaporation <= 0.0 || + config.localEvaporation >= 1.0 || config.minPheromone < 0.0 || + config.maxPheromone < 0.0 || + (config.maxPheromone > 0.0 && + config.minPheromone > config.maxPheromone)) { + throw std::invalid_argument("invalid ant-colony configuration"); + } +} + +void updateEdge(std::vector& pheromone, + std::size_t size, + bool symmetric, + std::size_t from, + std::size_t to, + double delta) { + pheromone[from * size + to] += delta; + if (symmetric) { + pheromone[to * size + from] += delta; + } +} + +void depositTour(std::vector& pheromone, + std::size_t size, + bool symmetric, + const Tour& tour, + double amount) { + for (std::size_t i = 0; i < size; ++i) { + updateEdge(pheromone, + size, + symmetric, + tour.nodes[i], + tour.nodes[(i + 1) % size], + amount); + } +} + +double routeDiversity(const std::vector& tours, std::size_t size) { + if (tours.size() < 2 || size < 2) { + return 0.0; + } + std::unordered_set edges; + edges.reserve(tours.size() * size); + for (const auto& tour : tours) { + for (std::size_t i = 0; i < size; ++i) { + const std::size_t from = tour.nodes[i]; + const std::size_t to = tour.nodes[(i + 1) % size]; + edges.insert(from * size + to); + } + } + const double minimumEdges = static_cast(size); + const double maximumEdges = static_cast(std::min(size * size, tours.size() * size)); + if (maximumEdges <= minimumEdges) { + return 0.0; + } + return std::clamp((static_cast(edges.size()) - minimumEdges) / + (maximumEdges - minimumEdges), + 0.0, + 1.0); +} + +} // namespace + +DenseGraph::DenseGraph(std::vector> costs, bool symmetric) + : size_(costs.size()), symmetric_(symmetric) { + if (size_ < 2) { + throw std::invalid_argument("ACO graph must contain at least two nodes"); + } + costs_.reserve(size_ * size_); + for (std::size_t i = 0; i < size_; ++i) { + if (costs[i].size() != size_) { + throw std::invalid_argument("ACO cost matrix must be square"); + } + for (std::size_t j = 0; j < size_; ++j) { + const double value = costs[i][j]; + if (!std::isfinite(value) || (i != j && value <= 0.0) || + (i == j && value < 0.0)) { + throw std::invalid_argument( + "ACO costs must be finite, with positive off-diagonal entries"); + } + costs_.push_back(value); + } + } + if (symmetric_) { + for (std::size_t i = 0; i < size_; ++i) { + for (std::size_t j = i + 1; j < size_; ++j) { + const double scale = std::max({1.0, std::abs(cost(i, j)), std::abs(cost(j, i))}); + if (std::abs(cost(i, j) - cost(j, i)) > 1e-12 * scale) { + throw std::invalid_argument( + "symmetric ACO graph requires a symmetric cost matrix"); + } + } + } + } +} + +AntColonyOptimizer::AntColonyOptimizer(AntColonyConfig config) + : config_(std::move(config)) { + validate(config_); +} + +AntColonyResult AntColonyOptimizer::solve(const DenseGraph& graph) const { + const std::size_t size = graph.size(); + std::mt19937 rng = ga::metaheuristics::detail::makeRng(config_.seed); + std::uniform_real_distribution uniform01(0.0, 1.0); + std::uniform_int_distribution startNode(0, size - 1); + + std::vector pheromone(size * size, config_.initialPheromone); + std::vector heuristic(size * size, 0.0); + std::vector> candidateLists(size); + for (std::size_t from = 0; from < size; ++from) { + candidateLists[from].reserve(size - 1); + for (std::size_t to = 0; to < size; ++to) { + if (from != to) { + heuristic[from * size + to] = 1.0 / graph.cost(from, to); + candidateLists[from].push_back(to); + } + } + std::sort(candidateLists[from].begin(), candidateLists[from].end(), + [&](std::size_t a, std::size_t b) { + return graph.cost(from, a) < graph.cost(from, b); + }); + if (config_.candidateListSize > 0 && + candidateLists[from].size() > config_.candidateListSize) { + candidateLists[from].resize(config_.candidateListSize); + } + } + + AntColonyResult result; + result.bestCost = std::numeric_limits::infinity(); + result.bestCostHistory.reserve(config_.iterations); + std::size_t stagnant = 0; + double previousImprovement = 1.0; + ga::metaheuristics::ControlSignal control; + + for (std::size_t iteration = 0; iteration < config_.iterations; ++iteration) { + std::vector tours(config_.ants); + for (Tour& tour : tours) { + tour.nodes.reserve(size); + std::vector visited(size, 0); + std::vector available; + std::vector desirability; + available.reserve(size); + desirability.reserve(size); + + std::size_t current = startNode(rng); + tour.nodes.push_back(current); + visited[current] = 1; + tour.cost = 0.0; + + while (tour.nodes.size() < size) { + available.clear(); + for (std::size_t node : candidateLists[current]) { + if (!visited[node]) { + available.push_back(node); + } + } + if (available.empty()) { + for (std::size_t node = 0; node < size; ++node) { + if (!visited[node]) { + available.push_back(node); + } + } + } + + desirability.resize(available.size()); + for (std::size_t i = 0; i < available.size(); ++i) { + const std::size_t next = available[i]; + desirability[i] = + std::pow(pheromone[current * size + next], + config_.alpha * control.exploitation) * + std::pow(heuristic[current * size + next], + config_.beta * control.exploitation); + } + + std::size_t selected = 0; + const double exploitationProbability = std::clamp( + config_.exploitationProbability / + std::max(control.randomization, 1e-12), + 0.0, + 1.0); + if (config_.variant == AntColonyVariant::AntColonySystem && + uniform01(rng) < exploitationProbability) { + selected = static_cast(std::distance( + desirability.begin(), + std::max_element(desirability.begin(), desirability.end()))); + } else { + std::discrete_distribution choose( + desirability.begin(), desirability.end()); + selected = choose(rng); + } + + const std::size_t next = available[selected]; + tour.cost += graph.cost(current, next); + if (config_.variant == AntColonyVariant::AntColonySystem) { + const std::size_t edge = current * size + next; + pheromone[edge] = (1.0 - config_.localEvaporation) * pheromone[edge] + + config_.localEvaporation * config_.initialPheromone; + if (graph.symmetric()) { + pheromone[next * size + current] = pheromone[edge]; + } + } + current = next; + tour.nodes.push_back(current); + visited[current] = 1; + } + tour.cost += graph.cost(tour.nodes.back(), tour.nodes.front()); + if (config_.variant == AntColonyVariant::AntColonySystem) { + const std::size_t from = tour.nodes.back(); + const std::size_t to = tour.nodes.front(); + const std::size_t edge = from * size + to; + pheromone[edge] = (1.0 - config_.localEvaporation) * pheromone[edge] + + config_.localEvaporation * config_.initialPheromone; + if (graph.symmetric()) { + pheromone[to * size + from] = pheromone[edge]; + } + } + } + + std::sort(tours.begin(), tours.end(), [](const Tour& a, const Tour& b) { + return a.cost < b.cost; + }); + const double previousBest = result.bestCost; + if (tours.front().cost < result.bestCost) { + result.bestCost = tours.front().cost; + result.bestTour = tours.front().nodes; + stagnant = 0; + } else { + ++stagnant; + } + if (std::isfinite(previousBest)) { + previousImprovement = std::max(0.0, previousBest - result.bestCost) / + std::max(1.0, std::abs(previousBest)); + } + + control = ga::metaheuristics::detail::controlSignal( + config_.controller.get(), + iteration, + config_.iterations, + routeDiversity(tours, size), + previousImprovement, + stagnant); + const double evaporation = std::clamp( + config_.evaporation * control.evaporation, 1e-6, 1.0 - 1e-6); + if (config_.variant != AntColonyVariant::AntColonySystem) { + for (std::size_t from = 0; from < size; ++from) { + for (std::size_t to = 0; to < size; ++to) { + if (from != to) { + pheromone[from * size + to] *= 1.0 - evaporation; + } + } + } + } + + Tour bestSoFar{result.bestTour, result.bestCost}; + switch (config_.variant) { + case AntColonyVariant::AntSystem: + for (const Tour& tour : tours) { + depositTour(pheromone, + size, + graph.symmetric(), + tour, + config_.depositScale / tour.cost); + } + break; + case AntColonyVariant::ElitistAntSystem: + for (const Tour& tour : tours) { + depositTour(pheromone, + size, + graph.symmetric(), + tour, + config_.depositScale / tour.cost); + } + depositTour(pheromone, + size, + graph.symmetric(), + bestSoFar, + config_.elitistWeight * config_.depositScale / bestSoFar.cost); + break; + case AntColonyVariant::RankBasedAntSystem: { + const std::size_t ranks = std::min(config_.rankCount, tours.size()); + for (std::size_t rank = 0; rank + 1 < ranks; ++rank) { + const double weight = static_cast(ranks - rank - 1); + depositTour(pheromone, + size, + graph.symmetric(), + tours[rank], + weight * config_.depositScale / tours[rank].cost); + } + depositTour(pheromone, + size, + graph.symmetric(), + bestSoFar, + static_cast(ranks) * config_.depositScale / + bestSoFar.cost); + break; + } + case AntColonyVariant::AntColonySystem: + for (std::size_t i = 0; i < size; ++i) { + const std::size_t from = bestSoFar.nodes[i]; + const std::size_t to = bestSoFar.nodes[(i + 1) % size]; + const double updated = + (1.0 - evaporation) * pheromone[from * size + to] + + evaporation * config_.depositScale / bestSoFar.cost; + pheromone[from * size + to] = updated; + if (graph.symmetric()) { + pheromone[to * size + from] = updated; + } + } + break; + case AntColonyVariant::MaxMinAntSystem: { + depositTour(pheromone, + size, + graph.symmetric(), + iteration % 5 == 0 ? bestSoFar : tours.front(), + config_.depositScale / + (iteration % 5 == 0 ? bestSoFar.cost : tours.front().cost)); + const double maximum = config_.maxPheromone > 0.0 + ? config_.maxPheromone + : config_.depositScale / + (evaporation * result.bestCost); + const double minimum = config_.minPheromone > 0.0 + ? config_.minPheromone + : maximum / (2.0 * static_cast(size)); + for (std::size_t from = 0; from < size; ++from) { + for (std::size_t to = 0; to < size; ++to) { + if (from != to) { + double& value = pheromone[from * size + to]; + value = std::clamp(value, minimum, maximum); + } + } + } + break; + } + } + + result.bestCostHistory.push_back(result.bestCost); + result.evaluations += config_.ants; + result.iterations = iteration + 1; + } + return result; +} + +} // namespace aco +} // namespace ga diff --git a/src/aco/continuous_ant_colony.cpp b/src/aco/continuous_ant_colony.cpp new file mode 100644 index 0000000..9928e3f --- /dev/null +++ b/src/aco/continuous_ant_colony.cpp @@ -0,0 +1,128 @@ +#include "ga/aco/continuous_ant_colony.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace ga { +namespace aco { + +ContinuousAntColonyOptimizer::ContinuousAntColonyOptimizer(AcorConfig config) + : config_(std::move(config)) { + ga::metaheuristics::detail::validateSearchConfig(config_.search); + if (config_.archiveSize < 2 || config_.sampleCount == 0 || + !std::isfinite(config_.locality) || config_.locality <= 0.0 || + !std::isfinite(config_.convergenceSpeed) || config_.convergenceSpeed <= 0.0) { + throw std::invalid_argument("invalid ACOR configuration"); + } +} + +ga::core::OptimizationResult ContinuousAntColonyOptimizer::optimize( + const ga::Fitness& fitness, + const ga::metaheuristics::SeedPopulation& seeds) { + using namespace ga::metaheuristics; + SearchConfig archiveConfig = config_.search; + archiveConfig.populationSize = config_.archiveSize; + std::mt19937 rng = detail::makeRng(archiveConfig.seed); + SeedPopulation archive = detail::makePopulation(archiveConfig, seeds, rng); + std::vector archiveFitness = + detail::evaluateBatch(archive, fitness, archiveConfig.threads); + + auto sortArchive = [&] { + std::vector order(archive.size()); + std::iota(order.begin(), order.end(), 0); + std::sort(order.begin(), order.end(), [&](std::size_t a, std::size_t b) { + return archiveFitness[a] > archiveFitness[b]; + }); + SeedPopulation sortedArchive; + std::vector sortedFitness; + sortedArchive.reserve(archive.size()); + sortedFitness.reserve(archive.size()); + for (std::size_t index : order) { + sortedArchive.push_back(std::move(archive[index])); + sortedFitness.push_back(archiveFitness[index]); + } + archive = std::move(sortedArchive); + archiveFitness = std::move(sortedFitness); + }; + sortArchive(); + + const double pi = std::acos(-1.0); + std::vector weights(config_.archiveSize, 0.0); + for (std::size_t rank = 0; rank < config_.archiveSize; ++rank) { + const double scaled = static_cast(rank) / + (config_.locality * static_cast(config_.archiveSize)); + weights[rank] = std::exp(-0.5 * scaled * scaled) / + (config_.locality * static_cast(config_.archiveSize) * + std::sqrt(2.0 * pi)); + } + std::discrete_distribution selectKernel(weights.begin(), weights.end()); + + ga::core::OptimizationResult result; + result.bestFitness = -std::numeric_limits::infinity(); + std::size_t evaluations = config_.archiveSize; + std::size_t stagnant = 0; + double lastImprovement = 1.0; + detail::updateResult(result, archive, archiveFitness, evaluations); + + for (std::size_t iteration = 0; iteration < config_.search.iterations; ++iteration) { + const double previousBest = result.bestFitness; + const ControlSignal signal = detail::controlSignal( + config_.controller.get(), + iteration, + config_.search.iterations, + detail::normalizedDiversity(archive, config_.search.bounds), + lastImprovement, + stagnant); + + SeedPopulation samples(config_.sampleCount, + std::vector(config_.search.dimension)); + for (auto& sample : samples) { + const std::size_t kernel = selectKernel(rng); + for (std::size_t d = 0; d < config_.search.dimension; ++d) { + double absoluteDeviation = 0.0; + for (std::size_t j = 0; j < config_.archiveSize; ++j) { + if (j != kernel) { + absoluteDeviation += std::abs(archive[j][d] - archive[kernel][d]); + } + } + double sigma = config_.convergenceSpeed * signal.exploration * + absoluteDeviation / + static_cast(config_.archiveSize - 1); + sigma = std::max(sigma, + (config_.search.bounds.upper - config_.search.bounds.lower) * + 1e-12); + std::normal_distribution normal(archive[kernel][d], sigma); + sample[d] = std::clamp(normal(rng), + config_.search.bounds.lower, + config_.search.bounds.upper); + } + } + + std::vector sampleFitness = + detail::evaluateBatch(samples, fitness, config_.search.threads); + evaluations += config_.sampleCount; + archive.reserve(config_.archiveSize + config_.sampleCount); + archiveFitness.reserve(config_.archiveSize + config_.sampleCount); + for (std::size_t i = 0; i < samples.size(); ++i) { + archive.push_back(std::move(samples[i])); + archiveFitness.push_back(sampleFitness[i]); + } + sortArchive(); + archive.resize(config_.archiveSize); + archiveFitness.resize(config_.archiveSize); + + detail::updateResult(result, archive, archiveFitness, evaluations); + result.generations = iteration + 1; + lastImprovement = detail::relativeImprovement(result.bestFitness, previousBest); + stagnant = result.bestFitness > previousBest ? 0 : stagnant + 1; + } + return result; +} + +} // namespace aco +} // namespace ga diff --git a/src/fuzzy/fuzzy_c_means.cpp b/src/fuzzy/fuzzy_c_means.cpp new file mode 100644 index 0000000..8f40bf6 --- /dev/null +++ b/src/fuzzy/fuzzy_c_means.cpp @@ -0,0 +1,175 @@ +#include "ga/fuzzy/fuzzy_c_means.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace ga { +namespace fuzzy { +namespace { + +double squaredDistance(const std::vector& a, const std::vector& b) { + double sum = 0.0; + for (std::size_t i = 0; i < a.size(); ++i) { + const double delta = a[i] - b[i]; + sum += delta * delta; + } + return sum; +} + +void validateData(const std::vector>& data, + const FuzzyCMeansConfig& config) { + if (data.empty() || data.front().empty()) { + throw std::invalid_argument("fuzzy C-means data must be non-empty"); + } + const std::size_t dimension = data.front().size(); + for (const auto& row : data) { + if (row.size() != dimension) { + throw std::invalid_argument("fuzzy C-means data must be rectangular"); + } + for (double value : row) { + if (!std::isfinite(value)) { + throw std::invalid_argument("fuzzy C-means data must be finite"); + } + } + } + if (config.clusters == 0 || config.clusters > data.size()) { + throw std::invalid_argument("clusters must be in [1, sample count]"); + } + if (config.maxIterations == 0) { + throw std::invalid_argument("maxIterations must be greater than zero"); + } + if (!std::isfinite(config.fuzziness) || config.fuzziness <= 1.0) { + throw std::invalid_argument("fuzziness must be finite and greater than one"); + } + if (!std::isfinite(config.tolerance) || config.tolerance <= 0.0) { + throw std::invalid_argument("tolerance must be finite and positive"); + } +} + +} // namespace + +FuzzyCMeans::FuzzyCMeans(FuzzyCMeansConfig config) : config_(std::move(config)) {} + +std::vector FuzzyCMeansResult::labels() const { + std::vector out; + out.reserve(membership.size()); + for (const auto& row : membership) { + if (row.empty()) { + out.push_back(0); + } else { + out.push_back(static_cast( + std::distance(row.begin(), std::max_element(row.begin(), row.end())))); + } + } + return out; +} + +FuzzyCMeansResult FuzzyCMeans::fit( + const std::vector>& data) const { + validateData(data, config_); + const std::size_t samples = data.size(); + const std::size_t dimension = data.front().size(); + const std::size_t clusters = config_.clusters; + + std::mt19937 rng(config_.seed == 0 ? std::random_device{}() : config_.seed); + std::uniform_real_distribution uniform(0.0, 1.0); + + FuzzyCMeansResult result; + result.membership.assign(samples, std::vector(clusters)); + result.centers.assign(clusters, std::vector(dimension, 0.0)); + result.objectiveHistory.reserve(config_.maxIterations); + + for (auto& row : result.membership) { + double total = 0.0; + for (double& membership : row) { + membership = std::max(uniform(rng), std::numeric_limits::epsilon()); + total += membership; + } + for (double& membership : row) { + membership /= total; + } + } + + std::vector> nextMembership( + samples, std::vector(clusters, 0.0)); + std::vector distances(clusters, 0.0); + const double exponent = 1.0 / (config_.fuzziness - 1.0); + + auto updateCenters = [&] { + for (std::size_t c = 0; c < clusters; ++c) { + std::fill(result.centers[c].begin(), result.centers[c].end(), 0.0); + double denominator = 0.0; + for (std::size_t i = 0; i < samples; ++i) { + const double weight = std::pow(result.membership[i][c], config_.fuzziness); + denominator += weight; + for (std::size_t d = 0; d < dimension; ++d) { + result.centers[c][d] += weight * data[i][d]; + } + } + const double safeDenominator = std::max( + denominator, std::numeric_limits::min()); + for (double& value : result.centers[c]) { + value /= safeDenominator; + } + } + }; + + for (std::size_t iteration = 0; iteration < config_.maxIterations; ++iteration) { + updateCenters(); + + double objective = 0.0; + double maxChange = 0.0; + for (std::size_t i = 0; i < samples; ++i) { + std::size_t coincident = clusters; + for (std::size_t c = 0; c < clusters; ++c) { + distances[c] = squaredDistance(data[i], result.centers[c]); + objective += std::pow(result.membership[i][c], config_.fuzziness) * + distances[c]; + if (distances[c] <= std::numeric_limits::epsilon()) { + coincident = c; + } + } + + if (coincident < clusters) { + std::fill(nextMembership[i].begin(), nextMembership[i].end(), 0.0); + nextMembership[i][coincident] = 1.0; + } else { + for (std::size_t c = 0; c < clusters; ++c) { + double denominator = 0.0; + for (std::size_t k = 0; k < clusters; ++k) { + denominator += std::pow(distances[c] / distances[k], exponent); + } + nextMembership[i][c] = 1.0 / denominator; + } + } + + for (std::size_t c = 0; c < clusters; ++c) { + maxChange = std::max( + maxChange, + std::abs(nextMembership[i][c] - result.membership[i][c])); + } + } + + result.membership.swap(nextMembership); + result.objectiveHistory.push_back(objective); + result.iterations = iteration + 1; + if (maxChange <= config_.tolerance) { + result.converged = true; + break; + } + } + + // Membership is updated after centers inside the loop. Recompute once so + // the returned centers correspond exactly to the returned memberships. + updateCenters(); + + return result; +} + +} // namespace fuzzy +} // namespace ga diff --git a/src/genetic_algorithm.cpp b/src/genetic_algorithm.cpp index 98c2d03..ba50575 100644 --- a/src/genetic_algorithm.cpp +++ b/src/genetic_algorithm.cpp @@ -24,7 +24,20 @@ static std::mt19937 make_rng(unsigned seed) { } GeneticAlgorithm::GeneticAlgorithm(const Config& cfg) - : cfg_(cfg), rng_(make_rng(cfg.seed)) { + : cfg_(cfg), + rng_(make_rng(cfg.seed)), + lowerBounds_(static_cast(std::max(0, cfg.dimension)), cfg.bounds.lower), + upperBounds_(static_cast(std::max(0, cfg.dimension)), cfg.bounds.upper) { + if (cfg_.populationSize <= 0 || cfg_.generations < 0 || cfg_.dimension <= 0 || + !std::isfinite(cfg_.bounds.lower) || !std::isfinite(cfg_.bounds.upper) || + !std::isfinite(cfg_.bounds.upper - cfg_.bounds.lower) || + !std::isfinite(cfg_.crossoverRate) || !std::isfinite(cfg_.mutationRate) || + !std::isfinite(cfg_.eliteRatio) || + cfg_.bounds.lower >= cfg_.bounds.upper || cfg_.crossoverRate < 0.0 || + cfg_.crossoverRate > 1.0 || cfg_.mutationRate < 0.0 || + cfg_.mutationRate > 1.0 || cfg_.eliteRatio < 0.0 || cfg_.eliteRatio > 1.0) { + throw std::invalid_argument("Invalid genetic algorithm configuration"); + } // Default operators mutation_ = makeGaussianMutation(cfg.seed); crossover_ = makeOnePointCrossover(cfg.seed); @@ -40,39 +53,70 @@ void GeneticAlgorithm::setCrossoverOperator(std::unique_ptr o crossover_ = std::move(op); } -std::vector GeneticAlgorithm::initPopulation_(const Fitness& f) { +std::vector GeneticAlgorithm::initPopulation_( + const Fitness& f, + const std::vector>& initialSolutions, + std::size_t& evaluations) { std::uniform_real_distribution dist(cfg_.bounds.lower, cfg_.bounds.upper); std::vector pop; pop.reserve(cfg_.populationSize); - for (int i = 0; i < cfg_.populationSize; ++i) { + + for (const auto& seed : initialSolutions) { + if (static_cast(pop.size()) == cfg_.populationSize) { + break; + } + if (seed.size() != static_cast(cfg_.dimension)) { + throw std::invalid_argument("Initial solution dimension does not match Config"); + } + if (!std::all_of(seed.begin(), seed.end(), [](double value) { + return std::isfinite(value); + })) { + throw std::invalid_argument("Initial solutions must contain only finite values"); + } + Individual ind; + ind.genes = seed; + for (double& gene : ind.genes) { + gene = std::clamp(gene, cfg_.bounds.lower, cfg_.bounds.upper); + } + ind.fitness = f(ind.genes); + if (!std::isfinite(ind.fitness)) { + throw std::domain_error("Fitness callback returned a non-finite value"); + } + ++evaluations; + pop.push_back(std::move(ind)); + } + + while (static_cast(pop.size()) < cfg_.populationSize) { Individual ind; ind.genes.resize(cfg_.dimension); for (double& g : ind.genes) g = dist(rng_); ind.fitness = f(ind.genes); + if (!std::isfinite(ind.fitness)) { + throw std::domain_error("Fitness callback returned a non-finite value"); + } + ++evaluations; pop.push_back(std::move(ind)); } return pop; } pair -GeneticAlgorithm::crossoverPair_(const Individual& p1, const Individual& p2, const Fitness& f) { +GeneticAlgorithm::crossoverPair_(const Individual& p1, const Individual& p2) { std::uniform_real_distribution prob(0.0, 1.0); if (prob(rng_) < cfg_.crossoverRate) { auto children = crossover_->crossover(p1.genes, p2.genes); - Individual c1{children.first, f(children.first)}; - Individual c2{children.second, f(children.second)}; + Individual c1{std::move(children.first), 0.0}; + Individual c2{std::move(children.second), 0.0}; return {std::move(c1), std::move(c2)}; } return {p1, p2}; } void GeneticAlgorithm::mutate_(Individual& ind) { - std::vector lows(cfg_.dimension, cfg_.bounds.lower); - std::vector highs(cfg_.dimension, cfg_.bounds.upper); if (auto* g = dynamic_cast(mutation_.get())) { - g->mutate(ind.genes, cfg_.mutationRate, 0.1, lows, highs); + g->mutate(ind.genes, cfg_.mutationRate, 0.1, lowerBounds_, upperBounds_); } else if (auto* u = dynamic_cast(mutation_.get())) { - u->mutate(ind.genes, cfg_.mutationRate, lows, highs); + u->mutate(ind.genes, cfg_.mutationRate, lowerBounds_, upperBounds_); } for (double& x : ind.genes) { if (x < cfg_.bounds.lower) x = cfg_.bounds.lower; @@ -81,13 +125,21 @@ void GeneticAlgorithm::mutate_(Individual& ind) { } Result GeneticAlgorithm::run(const Fitness& fitness) { + return run(fitness, {}); +} + +Result GeneticAlgorithm::run( + const Fitness& fitness, + const std::vector>& initialSolutions) { if (!crossover_ || !mutation_) throw std::runtime_error("Operators not set"); + if (!fitness) throw std::invalid_argument("Fitness callback is empty"); - auto pop = initPopulation_(fitness); + std::size_t evaluations = 0; + auto pop = initPopulation_(fitness, initialSolutions, evaluations); Result res; - res.bestHistory.reserve(cfg_.generations); - res.avgHistory.reserve(cfg_.generations); + res.bestHistory.reserve(static_cast(cfg_.generations) + 1); + res.avgHistory.reserve(static_cast(cfg_.generations) + 1); std::uniform_int_distribution pick(0, (int)pop.size() - 1); @@ -99,9 +151,11 @@ Result GeneticAlgorithm::run(const Fitness& fitness) { sum += P[i].fitness; if (P[i].fitness > best) { best = P[i].fitness; best_i = i; } } - res.bestGenes = P[best_i].genes; - res.bestFitness = best; - res.bestHistory.push_back(best); + if (res.bestGenes.empty() || best > res.bestFitness) { + res.bestGenes = P[best_i].genes; + res.bestFitness = best; + } + res.bestHistory.push_back(res.bestFitness); res.avgHistory.push_back(sum / P.size()); }; @@ -116,7 +170,9 @@ Result GeneticAlgorithm::run(const Fitness& fitness) { if (elites > 0) { std::vector idx(pop.size()); std::iota(idx.begin(), idx.end(), 0); - std::nth_element(idx.begin(), idx.begin()+elites, idx.end(), [&](size_t i, size_t j){ return pop[i].fitness > pop[j].fitness; }); + if (elites < static_cast(idx.size())) { + std::nth_element(idx.begin(), idx.begin()+elites, idx.end(), [&](size_t i, size_t j){ return pop[i].fitness > pop[j].fitness; }); + } for (int i = 0; i < elites; ++i) next.push_back(pop[idx[i]]); } @@ -124,19 +180,31 @@ Result GeneticAlgorithm::run(const Fitness& fitness) { while ((int)next.size() < cfg_.populationSize) { const auto& p1 = pop[pick(rng_)]; const auto& p2 = pop[pick(rng_)]; - auto [c1, c2] = crossoverPair_(p1, p2, fitness); + auto [c1, c2] = crossoverPair_(p1, p2); mutate_(c1); - mutate_(c2); c1.fitness = fitness(c1.genes); - c2.fitness = fitness(c2.genes); + if (!std::isfinite(c1.fitness)) { + throw std::domain_error("Fitness callback returned a non-finite value"); + } + ++evaluations; next.push_back(std::move(c1)); - if ((int)next.size() < cfg_.populationSize) next.push_back(std::move(c2)); + if ((int)next.size() < cfg_.populationSize) { + mutate_(c2); + c2.fitness = fitness(c2.genes); + if (!std::isfinite(c2.fitness)) { + throw std::domain_error("Fitness callback returned a non-finite value"); + } + ++evaluations; + next.push_back(std::move(c2)); + } } pop.swap(next); compute_stats(pop); + res.iterations = static_cast(gen + 1); } + res.evaluations = evaluations; return res; } diff --git a/src/gsa/gravitational_search.cpp b/src/gsa/gravitational_search.cpp new file mode 100644 index 0000000..c3e38b0 --- /dev/null +++ b/src/gsa/gravitational_search.cpp @@ -0,0 +1,139 @@ +#include "ga/gsa/gravitational_search.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace ga { +namespace gsa { + +GravitationalSearchOptimizer::GravitationalSearchOptimizer(GsaConfig config) + : config_(std::move(config)) { + ga::metaheuristics::detail::validateSearchConfig(config_.search); + if (!std::isfinite(config_.gravitationalConstant) || + config_.gravitationalConstant <= 0.0 || !std::isfinite(config_.decay) || + config_.decay < 0.0 || !std::isfinite(config_.epsilon) || + config_.epsilon <= 0.0 || config_.finalEliteFraction <= 0.0 || + config_.finalEliteFraction > 1.0) { + throw std::invalid_argument("invalid GSA configuration"); + } +} + +ga::core::OptimizationResult GravitationalSearchOptimizer::optimize( + const ga::Fitness& fitness, + const ga::metaheuristics::SeedPopulation& seeds) { + using namespace ga::metaheuristics; + const auto& search = config_.search; + std::mt19937 rng = detail::makeRng(search.seed); + std::uniform_real_distribution uniform01(0.0, 1.0); + + SeedPopulation positions = detail::makePopulation(search, seeds, rng); + SeedPopulation velocities( + search.populationSize, std::vector(search.dimension, 0.0)); + SeedPopulation accelerations = velocities; + std::vector values = detail::evaluateBatch(positions, fitness, search.threads); + std::vector masses(search.populationSize, 0.0); + std::vector ranked(search.populationSize); + std::iota(ranked.begin(), ranked.end(), 0); + + ga::core::OptimizationResult result; + result.bestFitness = -std::numeric_limits::infinity(); + std::size_t evaluations = search.populationSize; + std::size_t stagnant = 0; + double lastImprovement = 1.0; + detail::updateResult(result, positions, values, evaluations); + + for (std::size_t iteration = 0; iteration < search.iterations; ++iteration) { + const double previousBest = result.bestFitness; + const auto minmax = std::minmax_element(values.begin(), values.end()); + const double worst = *minmax.first; + const double best = *minmax.second; + const double spread = best - worst; + + if (spread <= config_.epsilon) { + std::fill(masses.begin(), masses.end(), + 1.0 / static_cast(masses.size())); + } else { + double massSum = 0.0; + for (std::size_t i = 0; i < values.size(); ++i) { + masses[i] = (values[i] - worst) / spread + config_.epsilon; + massSum += masses[i]; + } + for (double& mass : masses) { + mass /= massSum; + } + } + + std::sort(ranked.begin(), ranked.end(), [&](std::size_t a, std::size_t b) { + return values[a] > values[b]; + }); + const double progress = static_cast(iteration) / + static_cast(search.iterations); + const double eliteFraction = 1.0 - + progress * (1.0 - config_.finalEliteFraction); + const std::size_t eliteCount = std::max( + 1, + static_cast( + std::ceil(eliteFraction * static_cast(search.populationSize)))); + + const ControlSignal signal = detail::controlSignal( + config_.controller.get(), + iteration, + search.iterations, + detail::normalizedDiversity(positions, search.bounds), + lastImprovement, + stagnant); + const double gravity = config_.gravitationalConstant * signal.exploration * + std::exp(-config_.decay * progress); + + for (auto& acceleration : accelerations) { + std::fill(acceleration.begin(), acceleration.end(), 0.0); + } + for (std::size_t i = 0; i < search.populationSize; ++i) { + for (std::size_t rank = 0; rank < eliteCount; ++rank) { + const std::size_t j = ranked[rank]; + if (i == j) { + continue; + } + double squaredDistance = 0.0; + for (std::size_t d = 0; d < search.dimension; ++d) { + const double delta = positions[j][d] - positions[i][d]; + squaredDistance += delta * delta; + } + const double inverseDistance = + 1.0 / (std::sqrt(squaredDistance) + config_.epsilon); + for (std::size_t d = 0; d < search.dimension; ++d) { + accelerations[i][d] += uniform01(rng) * gravity * masses[j] * + (positions[j][d] - positions[i][d]) * + inverseDistance; + } + } + } + + for (std::size_t i = 0; i < search.populationSize; ++i) { + for (std::size_t d = 0; d < search.dimension; ++d) { + velocities[i][d] = uniform01(rng) * velocities[i][d] + + signal.exploitation * accelerations[i][d]; + positions[i][d] = std::clamp( + positions[i][d] + velocities[i][d], + search.bounds.lower, + search.bounds.upper); + } + } + + values = detail::evaluateBatch(positions, fitness, search.threads); + evaluations += search.populationSize; + detail::updateResult(result, positions, values, evaluations); + result.generations = iteration + 1; + lastImprovement = detail::relativeImprovement(result.bestFitness, previousBest); + stagnant = result.bestFitness > previousBest ? 0 : stagnant + 1; + } + return result; +} + +} // namespace gsa +} // namespace ga diff --git a/src/hybrid/metaheuristic_pipeline.cpp b/src/hybrid/metaheuristic_pipeline.cpp new file mode 100644 index 0000000..50412e5 --- /dev/null +++ b/src/hybrid/metaheuristic_pipeline.cpp @@ -0,0 +1,82 @@ +#include "ga/hybrid/metaheuristic_pipeline.hpp" + +#include +#include +#include +#include + +namespace ga { +namespace hybrid { + +MetaheuristicPipeline& MetaheuristicPipeline::add( + std::unique_ptr optimizer) { + return addShared(std::shared_ptr( + std::move(optimizer))); +} + +MetaheuristicPipeline& MetaheuristicPipeline::addShared( + std::shared_ptr optimizer) { + if (!optimizer) { + throw std::invalid_argument("cannot add a null optimizer to the hybrid pipeline"); + } + stages_.push_back(std::move(optimizer)); + return *this; +} + +ga::core::OptimizationResult MetaheuristicPipeline::optimize( + const ga::Fitness& fitness, + const ga::metaheuristics::SeedPopulation& seeds) { + return optimizeDetailed(fitness, seeds).combined; +} + +PipelineResult MetaheuristicPipeline::optimizeDetailed( + const ga::Fitness& fitness, + const ga::metaheuristics::SeedPopulation& seeds) { + if (!fitness) { + throw std::invalid_argument("fitness callback is empty"); + } + if (stages_.empty()) { + throw std::logic_error("hybrid pipeline contains no optimizers"); + } + + PipelineResult pipeline; + pipeline.combined.bestFitness = -std::numeric_limits::infinity(); + ga::metaheuristics::SeedPopulation transfer = seeds; + pipeline.stages.reserve(stages_.size()); + + for (auto& optimizer : stages_) { + StageResult stage; + stage.optimizer = optimizer->name(); + stage.result = optimizer->optimize(fitness, transfer); + if (stage.result.bestSolution.empty()) { + throw std::runtime_error(stage.optimizer + " returned an empty best solution"); + } + + if (pipeline.combined.bestSolution.empty() || + stage.result.bestFitness > pipeline.combined.bestFitness) { + pipeline.combined.bestSolution = stage.result.bestSolution; + pipeline.combined.bestFitness = stage.result.bestFitness; + } + pipeline.combined.bestHistory.insert( + pipeline.combined.bestHistory.end(), + stage.result.bestHistory.begin(), + stage.result.bestHistory.end()); + pipeline.combined.avgHistory.insert( + pipeline.combined.avgHistory.end(), + stage.result.avgHistory.begin(), + stage.result.avgHistory.end()); + pipeline.combined.evaluations += stage.result.evaluations; + pipeline.combined.generations += stage.result.generations; + + transfer.clear(); + transfer.push_back(stage.result.bestSolution); + if (stage.result.bestSolution != pipeline.combined.bestSolution) { + transfer.push_back(pipeline.combined.bestSolution); + } + pipeline.stages.push_back(std::move(stage)); + } + return pipeline; +} + +} // namespace hybrid +} // namespace ga diff --git a/src/pso/particle_swarm.cpp b/src/pso/particle_swarm.cpp new file mode 100644 index 0000000..1e6281e --- /dev/null +++ b/src/pso/particle_swarm.cpp @@ -0,0 +1,245 @@ +#include "ga/pso/particle_swarm.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace ga { +namespace pso { +namespace { + +void validate(const PsoConfig& config) { + ga::metaheuristics::detail::validateSearchConfig(config.search); + if (!std::isfinite(config.inertia) || !std::isfinite(config.cognitive) || + !std::isfinite(config.social) || !std::isfinite(config.constriction) || + !std::isfinite(config.velocityClamp) || + !std::isfinite(config.binaryVelocityClamp) || + !std::isfinite(config.quantumBeta) || + config.inertia < 0.0 || config.cognitive < 0.0 || config.social < 0.0 || + config.constriction <= 0.0 || config.velocityClamp <= 0.0 || + config.binaryVelocityClamp <= 0.0 || + config.quantumBeta <= 0.0) { + throw std::invalid_argument("PSO coefficients must be non-negative and scales positive"); + } + if (config.neighborhoodRadius == 0) { + throw std::invalid_argument("neighborhoodRadius must be greater than zero"); + } +} + +const char* variantName(PsoVariant variant) { + switch (variant) { + case PsoVariant::GlobalBest: return "PSO-global-best"; + case PsoVariant::LocalBest: return "PSO-local-best"; + case PsoVariant::Constriction: return "PSO-constriction"; + case PsoVariant::BareBones: return "PSO-bare-bones"; + case PsoVariant::FullyInformed: return "PSO-fully-informed"; + case PsoVariant::QuantumBehaved: return "PSO-quantum-behaved"; + case PsoVariant::Binary: return "PSO-binary"; + } + return "PSO"; +} + +std::size_t localBestIndex(std::size_t particle, + std::size_t radius, + const std::vector& personalFitness) { + const std::size_t count = personalFitness.size(); + std::size_t best = particle; + for (std::size_t offset = 1; offset <= std::min(radius, count - 1); ++offset) { + const std::size_t left = (particle + count - offset) % count; + const std::size_t right = (particle + offset) % count; + if (personalFitness[left] > personalFitness[best]) { + best = left; + } + if (personalFitness[right] > personalFitness[best]) { + best = right; + } + } + return best; +} + +} // namespace + +ParticleSwarmOptimizer::ParticleSwarmOptimizer(PsoConfig config) + : config_(std::move(config)) { + validate(config_); +} + +std::string ParticleSwarmOptimizer::name() const { + return variantName(config_.variant); +} + +ga::core::OptimizationResult ParticleSwarmOptimizer::optimize( + const ga::Fitness& fitness, + const ga::metaheuristics::SeedPopulation& seeds) { + using namespace ga::metaheuristics; + const auto& search = config_.search; + std::mt19937 rng = detail::makeRng(search.seed); + SeedPopulation positions = detail::makePopulation(search, seeds, rng); + const std::size_t particles = positions.size(); + const std::size_t dimension = search.dimension; + const double range = search.bounds.upper - search.bounds.lower; + const double maxVelocity = config_.variant == PsoVariant::Binary + ? config_.binaryVelocityClamp + : config_.velocityClamp * range; + + if (config_.variant == PsoVariant::Binary) { + std::bernoulli_distribution bit(0.5); + const std::size_t seeded = std::min(seeds.size(), particles); + for (std::size_t i = 0; i < particles; ++i) { + for (double& value : positions[i]) { + value = i < seeded ? (value >= 0.5 ? 1.0 : 0.0) + : (bit(rng) ? 1.0 : 0.0); + } + } + } + + std::uniform_real_distribution uniform01(0.0, 1.0); + std::uniform_real_distribution initialVelocity(-maxVelocity, maxVelocity); + SeedPopulation velocities(particles, std::vector(dimension, 0.0)); + for (auto& velocity : velocities) { + for (double& value : velocity) { + value = initialVelocity(rng); + } + } + + std::vector values = detail::evaluateBatch(positions, fitness, search.threads); + SeedPopulation personalBest = positions; + std::vector personalFitness = values; + std::size_t globalIndex = detail::bestIndex(personalFitness); + std::vector globalBest = personalBest[globalIndex]; + double globalFitness = personalFitness[globalIndex]; + + ga::core::OptimizationResult result; + result.bestFitness = -std::numeric_limits::infinity(); + std::size_t evaluations = particles; + std::size_t stagnant = 0; + double lastImprovement = 1.0; + detail::updateResult(result, positions, values, evaluations); + + for (std::size_t iteration = 0; iteration < search.iterations; ++iteration) { + const double previousBest = globalFitness; + const double diversity = detail::normalizedDiversity(positions, search.bounds); + const ControlSignal signal = detail::controlSignal( + config_.controller.get(), + iteration, + search.iterations, + diversity, + lastImprovement, + stagnant); + + std::vector meanPersonalBest(dimension, 0.0); + if (config_.variant == PsoVariant::QuantumBehaved) { + for (const auto& best : personalBest) { + for (std::size_t d = 0; d < dimension; ++d) { + meanPersonalBest[d] += best[d]; + } + } + for (double& value : meanPersonalBest) { + value /= static_cast(particles); + } + } + + for (std::size_t i = 0; i < particles; ++i) { + std::size_t guideIndex = globalIndex; + if (config_.variant == PsoVariant::LocalBest) { + guideIndex = localBestIndex(i, config_.neighborhoodRadius, personalFitness); + } + const auto& guide = personalBest[guideIndex]; + + for (std::size_t d = 0; d < dimension; ++d) { + const double r1 = uniform01(rng); + const double r2 = uniform01(rng); + double next = positions[i][d]; + + switch (config_.variant) { + case PsoVariant::BareBones: { + const double mean = 0.5 * (personalBest[i][d] + guide[d]); + const double sigma = std::max( + std::abs(personalBest[i][d] - guide[d]) * signal.exploration, + range * 1e-12); + std::normal_distribution normal(mean, sigma); + next = normal(rng); + break; + } + case PsoVariant::FullyInformed: { + double informed = 0.0; + for (std::size_t j = 0; j < particles; ++j) { + informed += uniform01(rng) * (personalBest[j][d] - positions[i][d]); + } + informed /= static_cast(particles); + velocities[i][d] = config_.inertia * signal.exploration * velocities[i][d] + + (config_.cognitive + config_.social) * + signal.exploitation * informed; + velocities[i][d] = std::clamp(velocities[i][d], -maxVelocity, maxVelocity); + next += velocities[i][d]; + break; + } + case PsoVariant::QuantumBehaved: { + const double phi = uniform01(rng); + const double attractor = phi * personalBest[i][d] + (1.0 - phi) * guide[d]; + const double u = std::max(uniform01(rng), std::numeric_limits::min()); + const double direction = uniform01(rng) < 0.5 ? -1.0 : 1.0; + next = attractor + direction * config_.quantumBeta * signal.exploration * + std::abs(meanPersonalBest[d] - positions[i][d]) * + std::log(1.0 / u); + break; + } + default: { + const double cognitive = config_.cognitive * signal.exploitation * r1 * + (personalBest[i][d] - positions[i][d]); + const double social = config_.social * signal.exploitation * r2 * + (guide[d] - positions[i][d]); + const double inertiaCoefficient = + config_.variant == PsoVariant::Constriction ? 1.0 : config_.inertia; + const double inertia = inertiaCoefficient * signal.exploration * + velocities[i][d]; + velocities[i][d] = inertia + cognitive + social; + if (config_.variant == PsoVariant::Constriction) { + velocities[i][d] *= config_.constriction; + } + velocities[i][d] = std::clamp(velocities[i][d], -maxVelocity, maxVelocity); + if (config_.variant == PsoVariant::Binary) { + const double probability = 1.0 / (1.0 + std::exp(-velocities[i][d])); + next = uniform01(rng) < probability ? 1.0 : 0.0; + } else { + next += velocities[i][d]; + } + break; + } + } + + positions[i][d] = config_.variant == PsoVariant::Binary + ? next + : std::clamp(next, search.bounds.lower, search.bounds.upper); + } + } + + values = detail::evaluateBatch(positions, fitness, search.threads); + evaluations += particles; + for (std::size_t i = 0; i < particles; ++i) { + if (values[i] > personalFitness[i]) { + personalFitness[i] = values[i]; + personalBest[i] = positions[i]; + } + } + globalIndex = detail::bestIndex(personalFitness); + globalBest = personalBest[globalIndex]; + globalFitness = personalFitness[globalIndex]; + lastImprovement = detail::relativeImprovement(globalFitness, previousBest); + stagnant = globalFitness > previousBest ? 0 : stagnant + 1; + + detail::updateResult(result, positions, values, evaluations); + result.generations = iteration + 1; + } + + result.bestSolution = std::move(globalBest); + result.bestFitness = globalFitness; + return result; +} + +} // namespace pso +} // namespace ga diff --git a/tests/advanced_features_sanity.cc b/tests/advanced_features_sanity.cc index 877978c..76b0f02 100644 --- a/tests/advanced_features_sanity.cc +++ b/tests/advanced_features_sanity.cc @@ -34,6 +34,7 @@ // Test result tracking static int tests_passed = 0; static int tests_failed = 0; +static constexpr double kPi = 3.14159265358979323846; #define TEST_ASSERT(cond, msg) \ do { \ @@ -66,9 +67,9 @@ double sphere(const std::vector& x) { double rastrigin(const std::vector& x) { const double A = 10.0; - double sum = A * x.size(); + double sum = A * static_cast(x.size()); for (double xi : x) { - sum += xi * xi - A * std::cos(2.0 * M_PI * xi); + sum += xi * xi - A * std::cos(2.0 * kPi * xi); } // Convert minimization to maximization return 1000.0 / (1.0 + sum); @@ -82,7 +83,7 @@ std::vector zdt1(const std::vector& x) { for (size_t i = 1; i < x.size(); ++i) { g += x[i]; } - g = 1.0 + 9.0 * g / (x.size() - 1); + g = 1.0 + 9.0 * g / static_cast(x.size() - 1); double h = 1.0 - std::sqrt(f1 / g); double f2 = g * h; return {f1, f2}; diff --git a/tests/features_foundation_sanity.cc b/tests/features_foundation_sanity.cc index b31b815..0680eb0 100644 --- a/tests/features_foundation_sanity.cc +++ b/tests/features_foundation_sanity.cc @@ -94,6 +94,7 @@ int main() { throw std::runtime_error("distributed evaluator failed"); } +#if defined(__unix__) || defined(__APPLE__) ga::evaluation::ProcessDistributedExecutor proc_eval( [](const std::vector& x) { double s = 0.0; @@ -108,6 +109,7 @@ int main() { !approx(proc_out[2], 25.0)) { throw std::runtime_error("process distributed evaluator failed"); } +#endif // ES / CMA-ES ga::es::EvolutionStrategy es({10, 30, 12, 5, 0.2, -2.0, 2.0, true, 11}); diff --git a/tests/metaheuristics_sanity.cc b/tests/metaheuristics_sanity.cc new file mode 100644 index 0000000..da144c6 --- /dev/null +++ b/tests/metaheuristics_sanity.cc @@ -0,0 +1,293 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "ga/metaheuristics.hpp" + +namespace { + +int failures = 0; + +#define CHECK(condition, message) \ + do { \ + if (!(condition)) { \ + std::cerr << "[FAIL] " << (message) << '\n'; \ + ++failures; \ + return; \ + } \ + } while (false) + +double sphereFitness(const std::vector& solution) { + double sum = 0.0; + for (double value : solution) { + sum += value * value; + } + return 1.0 / (1.0 + sum); +} + +bool nonDecreasing(const std::vector& values) { + return std::adjacent_find(values.begin(), values.end(), std::greater()) == + values.end(); +} + +bool nonIncreasing(const std::vector& values) { + return std::adjacent_find(values.begin(), values.end(), std::less()) == + values.end(); +} + +void testPsoVariants() { + const std::vector variants{ + ga::pso::PsoVariant::GlobalBest, + ga::pso::PsoVariant::LocalBest, + ga::pso::PsoVariant::Constriction, + ga::pso::PsoVariant::BareBones, + ga::pso::PsoVariant::FullyInformed, + ga::pso::PsoVariant::QuantumBehaved, + }; + + for (ga::pso::PsoVariant variant : variants) { + ga::pso::PsoConfig config; + config.search.populationSize = 24; + config.search.iterations = 35; + config.search.dimension = 4; + config.search.bounds = {-5.0, 5.0}; + config.search.seed = 71; + config.search.threads = 2; + config.variant = variant; + ga::pso::ParticleSwarmOptimizer optimizer(config); + const auto result = optimizer.optimize(sphereFitness); + CHECK(result.bestSolution.size() == config.search.dimension, + optimizer.name() + " returned a wrong-dimensional solution"); + CHECK(result.bestHistory.size() == config.search.iterations + 1, + optimizer.name() + " returned incomplete history"); + CHECK(nonDecreasing(result.bestHistory), + optimizer.name() + " best history is not monotonic"); + CHECK(result.evaluations == + config.search.populationSize * (config.search.iterations + 1), + optimizer.name() + " evaluation count is wrong"); + CHECK(result.bestFitness >= result.bestHistory.front(), + optimizer.name() + " lost its initial best"); + } + + ga::pso::PsoConfig binaryConfig; + binaryConfig.search.populationSize = 30; + binaryConfig.search.iterations = 30; + binaryConfig.search.dimension = 12; + binaryConfig.search.bounds = {0.0, 1.0}; + binaryConfig.search.seed = 42; + binaryConfig.variant = ga::pso::PsoVariant::Binary; + ga::pso::ParticleSwarmOptimizer binary(binaryConfig); + const auto result = binary.optimize([](const std::vector& x) { + return std::accumulate(x.begin(), x.end(), 0.0); + }); + CHECK(result.bestFitness >= 9.0, "binary PSO did not optimize OneMax"); + CHECK(std::all_of(result.bestSolution.begin(), result.bestSolution.end(), + [](double x) { return x == 0.0 || x == 1.0; }), + "binary PSO returned a non-binary solution"); + std::cout << "[PASS] PSO variants\n"; +} + +void testContinuousOptimizersAndFuzzyControl() { + auto controller = std::make_shared(); + + ga::aco::AcorConfig acorConfig; + acorConfig.search.iterations = 35; + acorConfig.search.dimension = 4; + acorConfig.search.bounds = {-5.0, 5.0}; + acorConfig.search.seed = 13; + acorConfig.archiveSize = 30; + acorConfig.sampleCount = 20; + acorConfig.controller = controller; + ga::aco::ContinuousAntColonyOptimizer acor(acorConfig); + const auto acorResult = acor.optimize(sphereFitness, {{4.0, 4.0, 4.0, 4.0}}); + CHECK(acorResult.bestSolution.size() == 4, "ACOR result dimension is wrong"); + CHECK(nonDecreasing(acorResult.bestHistory), "ACOR history is not monotonic"); + CHECK(acorResult.evaluations == 30 + 35 * 20, "ACOR evaluation count is wrong"); + + ga::gsa::GsaConfig gsaConfig; + gsaConfig.search.populationSize = 30; + gsaConfig.search.iterations = 35; + gsaConfig.search.dimension = 4; + gsaConfig.search.bounds = {-5.0, 5.0}; + gsaConfig.search.seed = 19; + gsaConfig.controller = controller; + ga::gsa::GravitationalSearchOptimizer gsa(gsaConfig); + const auto gsaResult = gsa.optimize(sphereFitness); + CHECK(gsaResult.bestSolution.size() == 4, "GSA result dimension is wrong"); + CHECK(nonDecreasing(gsaResult.bestHistory), "GSA history is not monotonic"); + + ga::metaheuristics::ProgressState stuck; + stuck.normalizedDiversity = 0.02; + stuck.relativeImprovement = 0.0; + stuck.stagnation = 1.0; + const auto explore = controller->update(stuck); + CHECK(explore.exploration > 1.0 && explore.randomization > 1.0, + "fuzzy controller did not increase exploration when stuck"); + + ga::fuzzy::FuzzyControllerConfig userFuzzyConfig; + userFuzzyConfig.lowDiversityStagnant = {12.25, 0.70, 1.40, 1.80}; + userFuzzyConfig.lowDiversitySlow = userFuzzyConfig.lowDiversityStagnant; + const ga::fuzzy::FuzzyAdaptiveController userController(userFuzzyConfig); + ga::metaheuristics::ProgressState userState = stuck; + userState.normalizedDiversity = 0.0; + const auto userSignal = userController.update(userState); + CHECK(std::abs(userSignal.exploration - 12.25) < 1e-12 && + std::abs(userSignal.exploitation - 0.70) < 1e-12 && + std::abs(userSignal.evaporation - 1.40) < 1e-12 && + std::abs(userSignal.randomization - 1.80) < 1e-12, + "fuzzy controller did not honor user-defined consequents"); + const auto forwarded = ga::metaheuristics::detail::controlSignal( + &userController, 0, 1, 0.0, 0.0, 1); + CHECK(std::abs(forwarded.exploration - 12.25) < 1e-12, + "shared controller path changed a valid user-defined multiplier"); + + bool invalidRejected = false; + try { + ga::fuzzy::FuzzyControllerConfig invalidConfig; + invalidConfig.improvementScale = 0.0; + const ga::fuzzy::FuzzyAdaptiveController invalidController(invalidConfig); + (void)invalidController; + } catch (const std::invalid_argument&) { + invalidRejected = true; + } + CHECK(invalidRejected, "fuzzy controller accepted an invalid user configuration"); + std::cout << "[PASS] ACOR, GSA, and fuzzy control\n"; +} + +void testGraphAcoVariants() { + ga::aco::DenseGraph graph({ + {0, 2, 9, 10, 7}, + {2, 0, 6, 4, 3}, + {9, 6, 0, 8, 5}, + {10, 4, 8, 0, 6}, + {7, 3, 5, 6, 0}, + }); + const std::vector variants{ + ga::aco::AntColonyVariant::AntSystem, + ga::aco::AntColonyVariant::ElitistAntSystem, + ga::aco::AntColonyVariant::RankBasedAntSystem, + ga::aco::AntColonyVariant::AntColonySystem, + ga::aco::AntColonyVariant::MaxMinAntSystem, + }; + + for (ga::aco::AntColonyVariant variant : variants) { + ga::aco::AntColonyConfig config; + config.ants = 20; + config.iterations = 25; + config.seed = 23; + config.variant = variant; + config.controller = std::make_shared(); + const auto result = ga::aco::AntColonyOptimizer(config).solve(graph); + CHECK(result.bestTour.size() == graph.size(), "ACO returned an incomplete tour"); + CHECK(std::set(result.bestTour.begin(), result.bestTour.end()).size() == + graph.size(), + "ACO returned a tour with duplicate nodes"); + CHECK(std::isfinite(result.bestCost) && result.bestCost > 0.0, + "ACO returned an invalid tour cost"); + CHECK(result.bestCostHistory.size() == config.iterations, + "ACO returned incomplete history"); + CHECK(nonIncreasing(result.bestCostHistory), "ACO best-cost history is not monotonic"); + CHECK(result.evaluations == config.ants * config.iterations, + "ACO evaluation count is wrong"); + } + std::cout << "[PASS] graph ACO variants\n"; +} + +void testFuzzyCMeans() { + const std::vector> data{ + {-0.1, 0.0}, {0.0, 0.1}, {0.1, -0.1}, + {9.9, 10.0}, {10.0, 10.1}, {10.1, 9.9}, + }; + ga::fuzzy::FuzzyCMeansConfig config; + config.clusters = 2; + config.seed = 42; + config.tolerance = 1e-8; + const auto result = ga::fuzzy::FuzzyCMeans(config).fit(data); + CHECK(result.centers.size() == 2, "FCM returned the wrong number of centers"); + CHECK(result.membership.size() == data.size(), "FCM returned incomplete membership"); + CHECK(result.converged, "FCM did not converge on separated clusters"); + CHECK(nonIncreasing(result.objectiveHistory), "FCM objective history is not monotonic"); + for (const auto& row : result.membership) { + const double sum = std::accumulate(row.begin(), row.end(), 0.0); + CHECK(std::abs(sum - 1.0) < 1e-9, "FCM memberships do not sum to one"); + } + const auto labels = result.labels(); + CHECK(labels[0] == labels[1] && labels[1] == labels[2], + "FCM split the first cluster"); + CHECK(labels[3] == labels[4] && labels[4] == labels[5] && labels[0] != labels[3], + "FCM failed to separate the second cluster"); + std::cout << "[PASS] fuzzy C-means\n"; +} + +void testHybridAndGaEvaluationEfficiency() { + ga::Config gaConfig; + gaConfig.populationSize = 11; + gaConfig.generations = 8; + gaConfig.dimension = 3; + gaConfig.bounds = {-5.0, 5.0}; + gaConfig.eliteRatio = 0.0; + gaConfig.seed = 7; + + std::atomic calls{0}; + ga::GeneticAlgorithm gaOnly(gaConfig); + const auto gaResult = gaOnly.run( + [&](const std::vector& x) { + ++calls; + return sphereFitness(x); + }, + {{0.0, 0.0, 0.0}}); + CHECK(calls == gaResult.evaluations, "GA evaluation accounting is wrong"); + CHECK(std::abs(gaResult.bestFitness - 1.0) < 1e-12, + "GA forgot a global best when elitism was disabled"); + const std::size_t elites = static_cast( + std::round(gaConfig.eliteRatio * gaConfig.populationSize)); + const std::size_t expectedCalls = static_cast(gaConfig.populationSize) + + static_cast(gaConfig.generations) * + (static_cast(gaConfig.populationSize) - elites); + CHECK(calls == expectedCalls, "GA still performs redundant offspring evaluations"); + + ga::pso::PsoConfig psoConfig; + psoConfig.search.populationSize = 20; + psoConfig.search.iterations = 20; + psoConfig.search.dimension = 3; + psoConfig.search.bounds = {-5.0, 5.0}; + psoConfig.search.seed = 8; + + ga::hybrid::MetaheuristicPipeline pipeline; + pipeline.add(std::make_unique(gaConfig)) + .add(std::make_unique(psoConfig)); + const auto result = pipeline.optimizeDetailed(sphereFitness); + CHECK(result.stages.size() == 2, "hybrid pipeline did not run every stage"); + CHECK(result.stages[0].optimizer == "GA" && + result.stages[1].optimizer == "PSO-global-best", + "hybrid pipeline did not preserve the user-selected stage order"); + CHECK(result.combined.bestFitness >= result.stages[0].result.bestFitness, + "hybrid pipeline lost the GA solution"); + CHECK(result.combined.evaluations == result.stages[0].result.evaluations + + result.stages[1].result.evaluations, + "hybrid pipeline evaluation accounting is wrong"); + std::cout << "[PASS] hybrid pipeline and GA evaluation efficiency\n"; +} + +} // namespace + +int main() { + testPsoVariants(); + testContinuousOptimizersAndFuzzyControl(); + testGraphAcoVariants(); + testFuzzyCMeans(); + testHybridAndGaEvaluationEfficiency(); + + if (failures != 0) { + std::cerr << failures << " metaheuristic test group(s) failed\n"; + return EXIT_FAILURE; + } + std::cout << "All metaheuristic tests passed\n"; + return EXIT_SUCCESS; +} diff --git a/tests/process_distributed_sanity.cc b/tests/process_distributed_sanity.cc index c7d1d15..e15a0bc 100644 --- a/tests/process_distributed_sanity.cc +++ b/tests/process_distributed_sanity.cc @@ -25,6 +25,7 @@ int main() { }, 3); +#if defined(__unix__) || defined(__APPLE__) const std::vector> batch = { {1.0, 2.0, 2.0}, {3.0, 4.0}, @@ -52,6 +53,20 @@ int main() { std::cout << "[PASS] Process distributed executor sanity checks\n"; return 0; +#else + bool unsupportedRejected = false; + try { + (void)exec.execute({{1.0}}); + } catch (const std::runtime_error&) { + unsupportedRejected = true; + } + if (!unsupportedRejected) { + throw std::runtime_error( + "non-POSIX process executor did not report unsupported operation"); + } + std::cout << "[PASS] Process executor reports unsupported platform\n"; + return 0; +#endif } catch (const std::exception& e) { std::cerr << "[FAIL] " << e.what() << "\n"; return 1;