penjadwalan-ga/app/Services/Scheduling/GeneticAlgorithmEngine.php

464 lines
18 KiB
PHP

<?php
namespace App\Services\Scheduling;
use App\Services\Scheduling\Data\SchedulingDataset;
/**
* GeneticAlgorithmEngine bertanggung jawab mengorganisir alur eksekusi populasi GA:
* Inisialisasi -> Evaluasi -> Seleksi Tournament -> Crossover -> Mutasi -> Elitisme.
*/
class GeneticAlgorithmEngine
{
private FitnessEvaluator $fitnessEvaluator;
public function __construct(
private int $populationSize = 50,
private float $crossoverRate = 0.8,
private float $mutationRate = 0.1,
private int $maxGenerations = 500,
private int $tournamentSize = 3,
private int $elitismCount = 2,
private ?int $seed = null
) {
$this->fitnessEvaluator = new FitnessEvaluator();
}
/**
* Jalankan proses GA untuk mencari solusi jadwal terbaik.
*/
public function run(SchedulingDataset $dataset): array
{
if ($this->seed !== null) {
mt_srand($this->seed);
}
$startTime = microtime(true);
// 1. Inisialisasi Populasi Awal
$population = $this->initializePopulation($dataset);
// Evaluasi Populasi Awal
foreach ($population as $chromosome) {
$this->fitnessEvaluator->evaluate($chromosome, $dataset);
}
$bestChromosome = $this->getBestChromosome($population);
$generationCount = 0;
$generationLogs = [];
// Loop Generasi
while ($generationCount < $this->maxGenerations && $bestChromosome->fitness < 1.0) {
$currentGenNumber = $generationCount; // Generasi saat ini (0, 1, 2, dst.)
$generationCount++;
$newPopulation = [];
$sampleSelectionLogs = [];
$sampleCrossoverLogs = [];
$sampleMutationLogs = [];
// Elitisme: Ambil n individu terbaik tanpa perubahan
usort($population, fn(Chromosome $a, Chromosome $b) => $b->fitness <=> $a->fitness);
for ($i = 0; $i < $this->elitismCount; $i++) {
$newPopulation[] = $population[$i]->clone();
}
// Reproduksi hingga populasi baru penuh
while (count($newPopulation) < $this->populationSize) {
// Seleksi Tournament (k=3)
[$parent1, $tLog1] = $this->tournamentSelectWithLog($population);
[$parent2, $tLog2] = $this->tournamentSelectWithLog($population);
if (count($sampleSelectionLogs) < 2) {
$sampleSelectionLogs[] = $tLog1;
$sampleSelectionLogs[] = $tLog2;
}
// Crossover
$rCross = round(mt_rand() / mt_getrandmax(), 4);
if ($rCross <= $this->crossoverRate) {
[$child1, $child2, $swappedKelas] = $this->crossoverWithLog($parent1, $parent2);
if (count($sampleCrossoverLogs) < 2) {
$kelasStr = !empty($swappedKelas) ? implode(', ', array_map(fn($k) => "K{$k}", array_slice($swappedKelas, 0, 5))) : 'K01';
$sampleCrossoverLogs[] = "Uniform Crossover (r={$rCross} <= Pc {$this->crossoverRate}): Menukarkan alokasi gen kelas [{$kelasStr}...] antara Induk 1 & Induk 2 -> Menghasilkan Anak 1 & 2.";
}
} else {
$child1 = $parent1->clone();
$child2 = $parent2->clone();
if (count($sampleCrossoverLogs) < 2) {
$sampleCrossoverLogs[] = "Crossover Dilewati (r={$rCross} > Pc {$this->crossoverRate}): Anak mengklon penuh gen dari Induk.";
}
}
// Mutasi
$mutLog1 = $this->mutateWithLog($child1, $dataset);
$mutLog2 = $this->mutateWithLog($child2, $dataset);
if (!empty($mutLog1) && count($sampleMutationLogs) < 2) {
$sampleMutationLogs[] = "Mutasi Anak 1: " . $mutLog1;
}
if (!empty($mutLog2) && count($sampleMutationLogs) < 2) {
$sampleMutationLogs[] = "Mutasi Anak 2: " . $mutLog2;
}
// Evaluasi Fitness Anak
$this->fitnessEvaluator->evaluate($child1, $dataset);
$this->fitnessEvaluator->evaluate($child2, $dataset);
$newPopulation[] = $child1;
if (count($newPopulation) < $this->populationSize) {
$newPopulation[] = $child2;
}
}
$operatorSamples = [
'selection' => $sampleSelectionLogs,
'crossover' => $sampleCrossoverLogs,
'mutation' => empty($sampleMutationLogs) ? ["Mutasi dilewati (tidak ada gen acak yang memenuhi rate Pm = {$this->mutationRate})."] : $sampleMutationLogs,
];
// Simpan log generasi saat ini (termasuk Generasi 0) beserta sampel operator yang memprosesnya
if ($currentGenNumber <= 10 || $currentGenNumber % 5 === 0) {
$generationLogs[] = $this->captureGenerationLog($currentGenNumber, $bestChromosome, $population, $dataset, $operatorSamples);
}
$population = $newPopulation;
$bestChromosome = $this->getBestChromosome($population);
}
// Simpan log untuk generasi terakhir (Final Solusi)
$finalOperatorSamples = [
'isFinal' => true,
'reason' => $bestChromosome->fitness == 1.0 ? "Solusi Optimal (Fitness = 1.0) telah ditemukan. Kriteria Penghentian Terpenuhi." : "Batas Maksimum Generasi ({$this->maxGenerations}) telah dicapai."
];
$generationLogs[] = $this->captureGenerationLog($generationCount, $bestChromosome, $population, $dataset, $finalOperatorSamples);
$executionTime = round(microtime(true) - $startTime, 3);
$memoryUsage = round(memory_get_usage() / 1024, 2); // KiloBytes
$lastLog = !empty($generationLogs) ? end($generationLogs) : null;
$avgFitness = $lastLog['avgFitness'] ?? 0;
return [
'bestChromosome' => $bestChromosome,
'generations' => $generationCount,
'executionTime' => $executionTime,
'memoryUsage' => $memoryUsage,
'avgFitness' => $avgFitness,
'isOptimal' => $bestChromosome->fitness == 1.0,
'generationLogs' => $generationLogs,
];
}
/**
* Membuat snapshot log generasi untuk analisis visual persis seperti gambar acuan skripsi
*/
/**
* Membuat snapshot log generasi untuk analisis visual persis seperti gambar acuan skripsi
*/
private function captureGenerationLog(int $gen, Chromosome $best, array $population, SchedulingDataset $dataset, array $operatorSamples = []): array
{
$namaHariMap = [1 => 'Senin', 2 => 'Selasa', 3 => 'Rabu', 4 => 'Kamis', 5 => 'Jumat', 6 => 'Sabtu', 7 => 'Minggu'];
$individus = [];
$fitnesses = [];
foreach ($population as $idx => $ind) {
$fitnesses[] = $ind->fitness;
// Analisis per gen individu untuk deteksi kode pelanggaran B1..B7
$guruSlotMap = [];
$ruanganSlotMap = [];
$kelasSlotMap = [];
$kelasGenesMap = [];
$geneViolations = [];
foreach ($ind->genes as $gIdx => $g) {
$violations = [];
$kelas = $dataset->getKelas($g->kelasId);
$guru = $dataset->getGuru($g->guruId);
// B1: Bentrok Guru
$guruSlotKey = "{$g->guruId}_{$g->hariId}_{$g->sesiId}";
if (isset($guruSlotMap[$guruSlotKey])) {
$violations[] = 'B1';
} else {
$guruSlotMap[$guruSlotKey] = true;
}
// B2: Bentrok Ruangan (Reguler)
if (!$kelas->isEkstrakurikuler) {
$ruanganSlotKey = "{$g->ruanganId}_{$g->hariId}_{$g->sesiId}";
if (isset($ruanganSlotMap[$ruanganSlotKey])) {
$violations[] = 'B2';
} else {
$ruanganSlotMap[$ruanganSlotKey] = true;
}
}
// B3: Ketersediaan Guru
if (!$guru->tersediaAt(new \App\Services\Scheduling\Data\SchedulingSlot($g->hariId, $g->sesiId))) {
$violations[] = 'B3';
}
// B4: Bentrok Kelas
$kelasSlotKey = "{$g->kelasId}_{$g->hariId}_{$g->sesiId}";
if (isset($kelasSlotMap[$kelasSlotKey])) {
$violations[] = 'B4';
} else {
$kelasSlotMap[$kelasSlotKey] = true;
}
// B5: Kualifikasi Level
if ($guru->levelMaksimal < $kelas->level) {
$violations[] = 'B5';
}
$kelasGenesMap[$g->kelasId][] = ['gene' => $g, 'index' => $gIdx];
$geneViolations[$gIdx] = $violations;
}
// B6 & B7
foreach ($kelasGenesMap as $kelasId => $gItems) {
if (count($gItems) == 2) {
$g1 = $gItems[0]['gene']; $idx1 = $gItems[0]['index'];
$g2 = $gItems[1]['gene']; $idx2 = $gItems[1]['index'];
// B6: Sebaran Hari
$selisihHari = abs($g1->hariId - $g2->hariId);
if ($g1->hariId == $g2->hariId || $selisihHari < 2) {
$geneViolations[$idx1][] = 'B6';
$geneViolations[$idx2][] = 'B6';
}
// B7: Konsistensi Guru
if ($g1->guruId != $g2->guruId) {
$geneViolations[$idx1][] = 'B7';
$geneViolations[$idx2][] = 'B7';
}
}
}
// Format Array Gen dengan Penanda Pelanggaran Terperinci
$genesFormatted = [];
foreach ($ind->genes as $gIdx => $g) {
$hariStr = $namaHariMap[$g->hariId] ?? 'Hari_' . $g->hariId;
$vCode = !empty($geneViolations[$gIdx]) ? $geneViolations[$gIdx][0] : null; // Ambil kode penalti pertama
$genesFormatted[] = [
'text' => "[K:{$g->kelasId}, G:{$g->guruId}, R:{$g->ruanganId}, S:{$g->sesiId}, {$hariStr}_{$g->indexPertemuan}]",
'violation' => $vCode
];
}
$individus[] = [
'index' => $idx,
'fitness' => round($ind->fitness, 4),
'penalty' => $ind->totalPenalty,
'genesFormatted' => $genesFormatted
];
}
$bestGenString = [];
foreach ($best->genes as $g) {
$hariStr = $namaHariMap[$g->hariId] ?? 'Hari_' . $g->hariId;
$bestGenString[] = "[K:{$g->kelasId}, G:{$g->guruId}, R:{$g->ruanganId}, S:{$g->sesiId}, {$hariStr}_{$g->indexPertemuan}]";
}
$minFitness = round(min($fitnesses), 4);
$maxFitness = round(max($fitnesses), 4);
$avgFitness = round(array_sum($fitnesses) / count($fitnesses), 4);
return [
'generation' => $gen,
'fitness' => round($best->fitness, 4),
'minFitness' => $minFitness,
'maxFitness' => $maxFitness,
'avgFitness' => $avgFitness,
'penalty' => $best->totalPenalty,
'breakdown' => $best->penaltyBreakdown,
'bestIndividuString' => implode(',', $bestGenString),
'individus' => $individus,
'operatorSamples' => $operatorSamples,
];
}
/**
* Tournament Selection (k=3) dengan pencatatan log
*/
private function tournamentSelectWithLog(array $population): array
{
$tournament = [];
$details = [];
for ($i = 0; $i < $this->tournamentSize; $i++) {
$randIdx = array_rand($population);
$ind = $population[$randIdx];
$tournament[] = $ind;
$details[] = "Individu[{$randIdx}] (Fitness: " . round($ind->fitness, 4) . ")";
}
$winner = $this->getBestChromosome($tournament);
$logStr = "Adu Turnamen k=3: " . implode(" VS ", $details) . " -> Winner: Fitness " . round($winner->fitness, 4);
return [$winner, $logStr];
}
/**
* Inisialisasi populasi acak
*/
private function initializePopulation(SchedulingDataset $dataset): array
{
$population = [];
for ($i = 0; $i < $this->populationSize; $i++) {
$genes = [];
foreach ($dataset->kelasList as $kelas) {
// Tentukan berapa gen untuk kelas ini
for ($pertemuan = 1; $pertemuan <= $kelas->frekuensiPerMinggu; $pertemuan++) {
if ($kelas->isEkstrakurikuler) {
// Kunci slot (H, S, R) sesuai fix, GA cari Guru acak
$hariId = $kelas->hariFix;
$sesiId = $kelas->sesiFix;
$ruanganId = $kelas->ruanganFixId;
} else {
// Reguler: Slot (H, S) & Ruangan diacak
$slot = $dataset->allSlots[array_rand($dataset->allSlots)];
$hariId = $slot->hari;
$sesiId = $slot->sesi;
$ruanganId = $dataset->ruanganRobotkuIds[array_rand($dataset->ruanganRobotkuIds)];
}
// Pilih guru yang kualifikasi level-nya memenuhi (levelMaksimal >= levelKelas)
$guruIds = array_keys(array_filter(
$dataset->guruList,
fn($g) => $g->levelMaksimal >= $kelas->level
));
if (empty($guruIds)) {
$guruIds = array_keys($dataset->guruList);
}
$guruId = $guruIds[array_rand($guruIds)];
$genes[] = new Gene(
kelasId: $kelas->id,
indexPertemuan: $pertemuan,
guruId: $guruId,
hariId: $hariId,
sesiId: $sesiId,
ruanganId: $ruanganId
);
}
}
$population[] = new Chromosome($genes);
}
return $population;
}
/**
* Tournament Selection (k=3)
*/
private function tournamentSelect(array $population): Chromosome
{
$tournament = [];
for ($i = 0; $i < $this->tournamentSize; $i++) {
$tournament[] = $population[array_rand($population)];
}
return $this->getBestChromosome($tournament);
}
/**
* Crossover (Uniform Crossover per Kelas untuk menjaga konsistensi B7)
*/
private function crossoverWithLog(Chromosome $parent1, Chromosome $parent2): array
{
$child1Genes = [];
$child2Genes = [];
$swappedKelas = [];
// Grouping per kelasId
$genesByKelas1 = [];
$genesByKelas2 = [];
foreach ($parent1->genes as $g) { $genesByKelas1[$g->kelasId][] = $g; }
foreach ($parent2->genes as $g) { $genesByKelas2[$g->kelasId][] = $g; }
foreach ($genesByKelas1 as $kelasId => $gList1) {
$gList2 = $genesByKelas2[$kelasId];
if (mt_rand(0, 1) === 1) {
foreach ($gList1 as $g) { $child1Genes[] = $g->clone(); }
foreach ($gList2 as $g) { $child2Genes[] = $g->clone(); }
$swappedKelas[] = $kelasId;
} else {
foreach ($gList2 as $g) { $child1Genes[] = $g->clone(); }
foreach ($gList1 as $g) { $child2Genes[] = $g->clone(); }
}
}
return [new Chromosome($child1Genes), new Chromosome($child2Genes), $swappedKelas];
}
/**
* Resetting Mutation dengan Smart Repair B7 (Konsistensi Guru)
*/
private function mutateWithLog(Chromosome $chromosome, SchedulingDataset $dataset): string
{
$namaHariMap = [1 => 'Senin', 2 => 'Selasa', 3 => 'Rabu', 4 => 'Kamis', 5 => 'Jumat', 6 => 'Sabtu', 7 => 'Minggu'];
$mutatedDetails = [];
// Mutasi acak gen
foreach ($chromosome->genes as $gene) {
$r = round(mt_rand() / mt_getrandmax(), 4);
if ($r <= $this->mutationRate) {
$kelas = $dataset->getKelas($gene->kelasId);
$oldGuru = $gene->guruId;
$oldHari = $namaHariMap[$gene->hariId] ?? 'Hari_'.$gene->hariId;
$oldSesi = $gene->sesiId;
// Ganti Guru dari yang qualified
$guruIds = array_keys(array_filter(
$dataset->guruList,
fn($g) => $g->levelMaksimal >= $kelas->level
));
if (!empty($guruIds)) {
$gene->guruId = $guruIds[array_rand($guruIds)];
}
// Jika kelas reguler, acak ulang (H, S, R)
if (!$kelas->isEkstrakurikuler) {
$slot = $dataset->allSlots[array_rand($dataset->allSlots)];
$gene->hariId = $slot->hari;
$gene->sesiId = $slot->sesi;
$gene->ruanganId = $dataset->ruanganRobotkuIds[array_rand($dataset->ruanganRobotkuIds)];
}
$newHari = $namaHariMap[$gene->hariId] ?? 'Hari_'.$gene->hariId;
$mutatedDetails[] = "Gen [K:{$gene->kelasId}, G:{$oldGuru}] dimutasi (r={$r} <= Pm {$this->mutationRate}) -> G:{$gene->guruId}, Slot: {$oldHari}_S{$oldSesi} diubah ke {$newHari}_S{$gene->sesiId}";
}
}
// Smart Fix B7 (Menyamakan guru pengajar untuk kelas 2x/minggu)
$kelasMap = [];
foreach ($chromosome->genes as $g) {
$kelasMap[$g->kelasId][] = $g;
}
foreach ($kelasMap as $genes) {
if (count($genes) == 2) {
if ($genes[0]->guruId != $genes[1]->guruId) {
$genes[1]->guruId = $genes[0]->guruId;
}
}
}
if (empty($mutatedDetails)) {
return "Mutasi dilewati pada individu ini (tidak ada gen acak dengan r <= Pm {$this->mutationRate}). Smart repair B7 aktif.";
}
return implode(' | ', array_slice($mutatedDetails, 0, 2)) . " + Smart Repair B7";
}
/**
* Ambil kromosom terbaik dari populasi
*/
private function getBestChromosome(array $population): Chromosome
{
$best = $population[0];
foreach ($population as $ind) {
if ($ind->fitness > $best->fitness) {
$best = $ind;
}
}
return $best;
}
}