Skip to content

Commit fcc70c1

Browse files
committed
refactor names in genetic_algorithm.py
1 parent f1ea2f1 commit fcc70c1

File tree

5 files changed

+82
-65
lines changed

5 files changed

+82
-65
lines changed

examples/addons/optimize/bin_packing_forms.py

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,15 +76,17 @@ def feedback(optimizer: ga.GeneticOptimizer):
7676
evaluator = bp.SubSetEvaluator(packer)
7777
optimizer = ga.GeneticOptimizer(evaluator, max_generations=GENERATIONS)
7878
optimizer.name = "pack item subset"
79-
optimizer.add_dna(ga.BitDNA.n_random(DNA_COUNT, len(packer.items)))
79+
optimizer.crossover_rate = 0.9
80+
optimizer.mutation_rate = 0.01
81+
optimizer.add_candidates(ga.BitDNA.n_random(DNA_COUNT, len(packer.items)))
8082
print(
8183
f"\nGenetic algorithm search: {optimizer.name}\n"
82-
f"max generations={optimizer.max_generations}, DNA count={optimizer.dna_count}"
84+
f"max generations={optimizer.max_generations}, DNA count={optimizer.count}"
8385
)
8486
optimizer.execute(feedback, interval=3)
8587
print(
86-
f"GeneticOptimizer: {optimizer.generation} generations x {optimizer.dna_count} "
87-
f"DNA strands, best result:"
88+
f"GeneticOptimizer: {optimizer.generation} generations x {optimizer.count} "
89+
f"candidates, best result:"
8890
)
8991
evaluator = cast(bp.SubSetEvaluator, optimizer.evaluator)
9092
best_packer = evaluator.run_packer(optimizer.best_dna)

examples/addons/optimize/tsp.py

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -77,13 +77,6 @@ def evaluate(self, dna: ga.DNA) -> float:
7777
return -sum_dist([self.cities[i] for i in dna])
7878

7979

80-
class SAEvaluator(TSPEvaluator):
81-
"""Traveling Salesmen Problem"""
82-
83-
def evaluate(self, dna: ga.DNA) -> float:
84-
return abs(super().evaluate(dna))
85-
86-
8780
def show_log(log: ga.Log, name: str):
8881
x = []
8982
y = []
@@ -143,11 +136,11 @@ def genetic_probing(data, seed):
143136
# preserve <elitism> overall best solutions in each generation
144137
optimizer.elitism = ELITISM
145138

146-
optimizer.add_dna(ga.UniqueIntDNA.n_random(300, length=len(data)))
139+
optimizer.add_candidates(ga.UniqueIntDNA.n_random(300, length=len(data)))
147140
optimizer.execute(feedback, 2)
148141

149142
print(
150-
f"GeneticOptimizer: {optimizer.generation} generations x {optimizer.dna_count} "
143+
f"GeneticOptimizer: {optimizer.generation} generations x {optimizer.count} "
151144
f"DNA strands, best result:"
152145
)
153146
evaluator = cast(TSPEvaluator, optimizer.evaluator)

profiling/binpacking.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ def make_subset_optimizer(
100100
evaluator = bp.SubSetEvaluator(packer)
101101
optimizer = ga.GeneticOptimizer(evaluator, max_generations=generations)
102102
optimizer.name = "pack item subset"
103-
optimizer.add_dna(ga.BitDNA.n_random(dna_count, len(packer.items)))
103+
optimizer.add_candidates(ga.BitDNA.n_random(dna_count, len(packer.items)))
104104
return optimizer
105105

106106

@@ -115,11 +115,11 @@ def feedback(optimizer: ga.GeneticOptimizer):
115115

116116
print(
117117
f"\nGenetic algorithm search: {optimizer.name}\n"
118-
f"max generations={optimizer.max_generations}, DNA count={optimizer.dna_count}"
118+
f"max generations={optimizer.max_generations}, DNA count={optimizer.count}"
119119
)
120120
optimizer.execute(feedback, interval=3.0)
121121
print(
122-
f"GeneticOptimizer: {optimizer.generation} generations x {optimizer.dna_count} "
122+
f"GeneticOptimizer: {optimizer.generation} generations x {optimizer.count} "
123123
f"DNA strands, best result:"
124124
)
125125
evaluator = cast(bp.SubSetEvaluator, optimizer.evaluator)

src/ezdxf/addons/genetic_algorithm.py

Lines changed: 56 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@
1414
import random
1515
import time
1616

17+
# example usage:
18+
# examples\addons\optimize\bin_packing_forms.py
19+
# examples\addons\optimize\tsp.py
20+
1721

1822
class 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

485489
class 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

651673
class 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

663685
class 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]

tests/test_08_addons/test_817_genetic_algorithm.py

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ def test_subscription_setter(self):
8080
assert dna[-4:] == [True, False, False, False]
8181

8282

83-
class TestUniquIntDNA:
83+
class TestUniqueIntDNA:
8484
def test_init_value(self):
8585
dna = ga.UniqueIntDNA(10)
8686
assert dna.is_valid is True
@@ -178,41 +178,41 @@ def test_new_random_dna(self):
178178

179179
class TestHallOfFame:
180180
@pytest.fixture
181-
def strands(self):
181+
def candidates(self):
182182
s = []
183183
for fitness in range(1, 10):
184184
dna = ga.BitDNA.random(5)
185185
dna.fitness = 0.1 * fitness
186186
s.append(dna)
187187
return s
188188

189-
def test_build(self, strands):
189+
def test_build(self, candidates):
190190
hof = ga.HallOfFame(3)
191-
for dna in strands:
191+
for dna in candidates:
192192
hof.add(dna)
193193
assert [dna.fitness for dna in hof] == pytest.approx([0.9, 0.8, 0.7])
194194

195-
def test_get_n_best(self, strands):
195+
def test_get_n_best(self, candidates):
196196
hof = ga.HallOfFame(3)
197-
for dna in strands:
197+
for dna in candidates:
198198
hof.add(dna)
199199
result = hof.get(2)
200200
assert result[0].fitness == pytest.approx(0.9)
201201
assert result[1].fitness == pytest.approx(0.8)
202202

203-
def test_get_n_best_negative_values(self, strands):
204-
for dna in strands:
203+
def test_get_n_best_negative_values(self, candidates):
204+
for dna in candidates:
205205
dna.fitness = -dna.fitness
206206
hof = ga.HallOfFame(3)
207-
for dna in strands:
207+
for dna in candidates:
208208
hof.add(dna)
209209
result = hof.get(2)
210210
assert result[0].fitness == pytest.approx(-0.1)
211211
assert result[1].fitness == pytest.approx(-0.2)
212212

213-
def test_purge(self, strands):
213+
def test_purge(self, candidates):
214214
hof = ga.HallOfFame(3)
215-
for dna in strands:
215+
for dna in candidates:
216216
hof.add(dna)
217217
hof.purge()
218218
assert len(hof._unique_entries) == 3
@@ -235,11 +235,11 @@ def test_scramble_mutate():
235235

236236

237237
def test_tournament_selection():
238-
strands = [ga.UniqueIntDNA(10) for _ in range(10)]
239-
for index, dna in enumerate(strands):
238+
candidates = [ga.UniqueIntDNA(10) for _ in range(10)]
239+
for index, dna in enumerate(candidates):
240240
dna.fitness = index
241241
selection = ga.TournamentSelection(2)
242-
selection.reset(strands)
242+
selection.reset(candidates)
243243

244244
result = list(selection.pick(2))
245245
assert len(result) == 2
@@ -360,7 +360,7 @@ def test_execution(self, packer):
360360
packer.add_bin(*MEDIUM_BOX)
361361
evaluator = DummyEvaluator(packer)
362362
optimizer = ga.GeneticOptimizer(evaluator, 10)
363-
optimizer.add_dna(ga.BitDNA.n_random(20, len(packer.items)))
363+
optimizer.add_candidates(ga.BitDNA.n_random(20, len(packer.items)))
364364
optimizer.execute()
365365
assert optimizer.generation == 10
366366
assert optimizer.best_fitness > 0.1

0 commit comments

Comments
 (0)