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; } }