1414import random
1515import time
1616
17+ # example usage:
18+ # examples\addons\optimize\bin_packing_forms.py
19+ # examples\addons\optimize\tsp.py
20+
1721
1822class DNA (abc .ABC ):
1923 """Abstract DNA class."""
@@ -412,7 +416,7 @@ def pick(self, count: int) -> Iterable[DNA]:
412416 ...
413417
414418 @abc .abstractmethod
415- def reset (self , strands : Iterable [DNA ]):
419+ def reset (self , candidates : Iterable [DNA ]):
416420 ...
417421
418422
@@ -483,7 +487,22 @@ def purge(self):
483487
484488
485489class GeneticOptimizer :
486- """Optimization Algorithm."""
490+ """A genetic algorithm (GA) is a meta-heuristic inspired by the process of
491+ natural selection. Genetic algorithms are commonly used to generate
492+ high-quality solutions to optimization and search problems by relying on
493+ biologically inspired operators such as mutation, crossover and selection.
494+
495+ Source: https://en.wikipedia.org/wiki/Genetic_algorithm
496+
497+ This implementation searches always for the maximum fitness, fitness
498+ comparisons are always done by the "greater than" operator (">").
499+ The algorithm supports negative values to search for the minimum fitness
500+ (e.g. Travelling Salesmen Problem: -900 > -1000). Reset the start fitness
501+ by the method :meth:`reset_fitness` accordingly::
502+
503+ optimizer.reset_fitness(-1e99)
504+
505+ """
487506
488507 def __init__ (
489508 self ,
@@ -492,11 +511,11 @@ def __init__(
492511 max_fitness : float = 1.0 ,
493512 ):
494513 if max_generations < 1 :
495- raise ValueError ("max_generations < 1 " )
514+ raise ValueError ("requires max_generations > 0 " )
496515 # data:
497516 self .name = "GeneticOptimizer"
498517 self .log = Log ()
499- self ._dna_strands : List [DNA ] = []
518+ self .candidates : List [DNA ] = []
500519
501520 # core components:
502521 self .evaluator : Evaluator = evaluator
@@ -510,8 +529,11 @@ def __init__(
510529 self .max_runtime : float = 1e99
511530 self .max_stagnation = 100
512531 self .crossover_rate = 0.70
513- self .mutation_rate = 0.001
532+ self .mutation_rate = 0.01
514533 self .elitism : int = 2
534+ # percentage (0.1 = 10%) of DNA strands with least fitness to ignore in
535+ # next generation
536+ self .threshold : float = 0.0
515537
516538 # state of last (current) generation:
517539 self .generation : int = 0
@@ -530,12 +552,12 @@ def is_executed(self) -> bool:
530552 return bool (self .generation )
531553
532554 @property
533- def dna_count (self ) -> int :
534- return len (self ._dna_strands )
555+ def count (self ) -> int :
556+ return len (self .candidates )
535557
536- def add_dna (self , dna : Iterable [DNA ]):
558+ def add_candidates (self , dna : Iterable [DNA ]):
537559 if not self .is_executed :
538- self ._dna_strands .extend (dna )
560+ self .candidates .extend (dna )
539561 else :
540562 raise TypeError ("already executed" )
541563
@@ -546,7 +568,7 @@ def execute(
546568 ) -> None :
547569 if self .is_executed :
548570 raise TypeError ("can only run once" )
549- if not self ._dna_strands :
571+ if not self .candidates :
550572 print ("no DNA defined!" )
551573 t0 = time .perf_counter ()
552574 self .start_time = t0
@@ -569,7 +591,7 @@ def execute(
569591 def measure_fitness (self ) -> None :
570592 self .stagnation += 1
571593 fitness_sum : float = 0.0
572- for dna in self ._dna_strands :
594+ for dna in self .candidates :
573595 if dna .fitness is not None :
574596 fitness_sum += dna .fitness
575597 continue
@@ -584,7 +606,7 @@ def measure_fitness(self) -> None:
584606
585607 self .hall_of_fame .purge ()
586608 try :
587- avg_fitness = fitness_sum / len (self ._dna_strands )
609+ avg_fitness = fitness_sum / len (self .candidates )
588610 except ZeroDivisionError :
589611 avg_fitness = 0.0
590612 self .log .add (
@@ -595,22 +617,22 @@ def measure_fitness(self) -> None:
595617
596618 def next_generation (self ) -> None :
597619 selector = self .selection
598- selector .reset (self ._dna_strands )
599- dna_strands : List [DNA ] = []
600- count = len (self ._dna_strands )
620+ selector .reset (self .candidates )
621+ candidates : List [DNA ] = []
622+ count = len (self .candidates )
601623
602624 if self .elitism > 0 :
603- dna_strands .extend (self .hall_of_fame .get (self .elitism ))
625+ candidates .extend (self .hall_of_fame .get (self .elitism ))
604626
605- while len (dna_strands ) < count :
627+ while len (candidates ) < count :
606628 dna1 , dna2 = selector .pick (2 )
607629 dna1 = dna1 .copy ()
608630 dna2 = dna2 .copy ()
609631 self .recombine (dna1 , dna2 )
610632 self .mutate (dna1 , dna2 )
611- dna_strands .append (dna1 )
612- dna_strands .append (dna2 )
613- self ._dna_strands = dna_strands
633+ candidates .append (dna1 )
634+ candidates .append (dna2 )
635+ self .candidates = candidates
614636
615637 def recombine (self , dna1 : DNA , dna2 : DNA ):
616638 if random .random () < self .crossover_rate :
@@ -630,50 +652,50 @@ class RouletteSelection(Selection):
630652 """Selection by fitness values."""
631653
632654 def __init__ (self , negative_values = False ):
633- self ._strands : List [DNA ] = []
655+ self ._candidates : List [DNA ] = []
634656 self ._weights : List [float ] = []
635657 self ._negative_values = bool (negative_values )
636658
637- def reset (self , strands : Iterable [DNA ]):
659+ def reset (self , candidates : Iterable [DNA ]):
638660 # dna.fitness is not None here!
639- self ._strands = list (strands )
661+ self ._candidates = list (candidates )
640662 if self ._negative_values :
641663 self ._weights = list (
642- conv_negative_weights (dna .fitness for dna in self ._strands ) # type: ignore
664+ conv_negative_weights (dna .fitness for dna in self ._candidates ) # type: ignore
643665 )
644666 else :
645- self ._weights = [dna .fitness for dna in self ._strands ] # type: ignore
667+ self ._weights = [dna .fitness for dna in self ._candidates ] # type: ignore
646668
647669 def pick (self , count : int ) -> Iterable [DNA ]:
648- return random .choices (self ._strands , self ._weights , k = count )
670+ return random .choices (self ._candidates , self ._weights , k = count )
649671
650672
651673class RankBasedSelection (RouletteSelection ):
652674 """Selection by rank of fitness."""
653675
654- def reset (self , strands : Iterable [DNA ]):
676+ def reset (self , candidates : Iterable [DNA ]):
655677 # dna.fitness is not None here!
656- self ._strands = list (strands )
657- self ._strands .sort (key = dna_fitness ) # type: ignore
678+ self ._candidates = list (candidates )
679+ self ._candidates .sort (key = dna_fitness ) # type: ignore
658680 # weight of best_fitness == len(strands)
659681 # and decreases until 1 for the least fitness
660- self ._weights = list (range (1 , len (self ._strands ) + 1 ))
682+ self ._weights = list (range (1 , len (self ._candidates ) + 1 ))
661683
662684
663685class TournamentSelection (Selection ):
664686 """Selection by choosing the best of a certain count of candidates."""
665687
666688 def __init__ (self , candidates : int ):
667- self ._strands : List [DNA ] = []
689+ self ._candidates : List [DNA ] = []
668690 self .candidates = candidates
669691
670- def reset (self , strands : Iterable [DNA ]):
671- self ._strands = list (strands )
692+ def reset (self , candidates : Iterable [DNA ]):
693+ self ._candidates = list (candidates )
672694
673695 def pick (self , count : int ) -> Iterable [DNA ]:
674696 for _ in range (count ):
675697 values = [
676- random .choice (self ._strands ) for _ in range (self .candidates )
698+ random .choice (self ._candidates ) for _ in range (self .candidates )
677699 ]
678700 values .sort (key = dna_fitness ) # type: ignore
679701 yield values [- 1 ]
0 commit comments