phpOMS/Algorithm/Optimization/GeneticOptimization.php
2024-03-20 07:21:26 +00:00

158 lines
4.8 KiB
PHP

<?php
/**
* Jingga
*
* PHP Version 8.2
*
* @package phpOMS\Algorithm\Optimization
* @copyright Dennis Eichhorn
* @license OMS License 2.0
* @version 1.0.0
* @link https://jingga.app
*/
declare(strict_types=1);
namespace phpOMS\Algorithm\Optimization;
/**
* Perform genetic algorithm (GA).
*
* @package phpOMS\Algorithm\Optimization
* @license OMS License 2.0
* @link https://jingga.app
* @since 1.0.0
*/
class GeneticOptimization
{
/**
* Constructor
*
* @since 1.0.0
* @codeCoverageIgnore
*/
private function __construct()
{
}
/*
// Fitness function (may require to pass solution space as \Closure variable)
// E.g.
// highest value of some sorts (e.g. profit)
// most elements (e.g. jobs)
// lowest costs
// combination of criteria = points (where some criteria are mandatory/optional)
public static function fitness($x)
{
return $x;
}
public static function mutate($parameters, $mutationRate)
{
for ($i = 0; $i < \count($parameters); $i++) {
if (\mt_rand(0, 1000) / 1000 < $mutationRate) {
$parameters[$i] = 1 - $parameters[$i];
}
}
return $parameters;
}
public static function crossover($parent1, $parent2, $parameterCount)
{
$crossoverPoint = \mt_rand(1, $parameterCount - 1);
$child1 = \array_merge(
\array_slice($parent1, 0, $crossoverPoint),
\array_slice($parent2, $crossoverPoint)
);
$child2 = \array_merge(
\array_slice($parent2, 0, $crossoverPoint),
\array_slice($parent1, $crossoverPoint)
);
return [$child1, $child2];
}
*/
/**
* Perform optimization
*
* @example See unit test for example use case
*
* @param array<array> $population List of all elements with their parameters (i.e. list of "objects" as arrays).
* The constraints are defined as array values.
* @param \Closure $fitness Fitness function calculates score/feasibility of solution
* @param \Closure $mutate Mutation function to change the parameters of an "object"
* @param \Closure $crossover Crossover function to exchange parameter values between "objects".
* Sometimes single parameters can be exchanged but sometimes interdependencies exist between parameters which is why this function is required.
* @param int $generations Number of generations to create
* @param float $mutationRate Rate at which parameters are changed.
* How this is used depends on the mutate function.
*
* @return array{solutions:array, fitnesses:float[]}
*
* @since 1.0.0
*/
public static function optimize(
array $population,
\Closure $fitness,
\Closure $mutate,
\Closure $crossover,
int $generations = 500,
float $mutationRate = 0.1
) : array
{
$populationSize = \count($population);
$parameterCount = $populationSize === 0 ? 0 : \count(\reset($population));
// Genetic Algorithm Loop
for ($generation = 0; $generation < $generations; ++$generation) {
$fitnessScores = [];
foreach ($population as $parameters) {
$fitnessScores[] = ($fitness)($parameters);
}
// Select parents for crossover based on fitness scores
$parents = [];
for ($i = 0; $i < $populationSize; ++$i) {
do {
$parentIndex1 = \array_rand($population);
$parentIndex2 = \array_rand($population);
} while ($parentIndex1 === $parentIndex2);
$parents[] = $fitnessScores[$parentIndex1] > $fitnessScores[$parentIndex2]
? $population[$parentIndex1]
: $population[$parentIndex2];
}
// Crossover and mutation to create next generation
$newPopulation = [];
for ($i = 0; $i < $populationSize; $i += 2) {
$crossover = ($crossover)($parents[$i], $parents[$i + 1], $parameterCount);
$child1 = ($mutate)($crossover[0], $mutationRate);
$child2 = ($mutate)($crossover[1], $mutationRate);
$newPopulation[] = $child1;
$newPopulation[] = $child2;
}
$population = $newPopulation;
}
$fitnesses = [];
foreach ($population as $key => $parameters) {
$fitnesses[$key] = ($fitness)($parameters);
}
\asort($fitnesses);
return [
'solutions' => $population,
'fitnesses' => $fitnesses,
];
}
}