desdeo 2.0.0__py3-none-any.whl → 2.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- desdeo/adm/ADMAfsar.py +551 -0
- desdeo/adm/ADMChen.py +414 -0
- desdeo/adm/BaseADM.py +119 -0
- desdeo/adm/__init__.py +11 -0
- desdeo/api/__init__.py +6 -6
- desdeo/api/app.py +38 -28
- desdeo/api/config.py +65 -44
- desdeo/api/config.toml +23 -12
- desdeo/api/db.py +10 -8
- desdeo/api/db_init.py +12 -6
- desdeo/api/models/__init__.py +220 -20
- desdeo/api/models/archive.py +16 -27
- desdeo/api/models/emo.py +128 -0
- desdeo/api/models/enautilus.py +69 -0
- desdeo/api/models/gdm/gdm_aggregate.py +139 -0
- desdeo/api/models/gdm/gdm_base.py +69 -0
- desdeo/api/models/gdm/gdm_score_bands.py +114 -0
- desdeo/api/models/gdm/gnimbus.py +138 -0
- desdeo/api/models/generic.py +104 -0
- desdeo/api/models/generic_states.py +401 -0
- desdeo/api/models/nimbus.py +158 -0
- desdeo/api/models/preference.py +44 -6
- desdeo/api/models/problem.py +274 -64
- desdeo/api/models/session.py +4 -1
- desdeo/api/models/state.py +419 -52
- desdeo/api/models/user.py +7 -6
- desdeo/api/models/utopia.py +25 -0
- desdeo/api/routers/_EMO.backup +309 -0
- desdeo/api/routers/_NIMBUS.py +6 -3
- desdeo/api/routers/emo.py +497 -0
- desdeo/api/routers/enautilus.py +237 -0
- desdeo/api/routers/gdm/gdm_aggregate.py +234 -0
- desdeo/api/routers/gdm/gdm_base.py +420 -0
- desdeo/api/routers/gdm/gdm_score_bands/gdm_score_bands_manager.py +398 -0
- desdeo/api/routers/gdm/gdm_score_bands/gdm_score_bands_routers.py +377 -0
- desdeo/api/routers/gdm/gnimbus/gnimbus_manager.py +698 -0
- desdeo/api/routers/gdm/gnimbus/gnimbus_routers.py +591 -0
- desdeo/api/routers/generic.py +233 -0
- desdeo/api/routers/nimbus.py +705 -0
- desdeo/api/routers/problem.py +201 -4
- desdeo/api/routers/reference_point_method.py +20 -44
- desdeo/api/routers/session.py +50 -26
- desdeo/api/routers/user_authentication.py +180 -26
- desdeo/api/routers/utils.py +187 -0
- desdeo/api/routers/utopia.py +230 -0
- desdeo/api/schema.py +10 -4
- desdeo/api/tests/conftest.py +94 -2
- desdeo/api/tests/test_enautilus.py +330 -0
- desdeo/api/tests/test_models.py +550 -72
- desdeo/api/tests/test_routes.py +902 -43
- desdeo/api/utils/_database.py +263 -0
- desdeo/api/utils/database.py +28 -266
- desdeo/api/utils/emo_database.py +40 -0
- desdeo/core.py +7 -0
- desdeo/emo/__init__.py +154 -24
- desdeo/emo/hooks/archivers.py +18 -2
- desdeo/emo/methods/EAs.py +128 -5
- desdeo/emo/methods/bases.py +9 -56
- desdeo/emo/methods/templates.py +111 -0
- desdeo/emo/operators/crossover.py +544 -42
- desdeo/emo/operators/evaluator.py +10 -14
- desdeo/emo/operators/generator.py +127 -24
- desdeo/emo/operators/mutation.py +212 -41
- desdeo/emo/operators/scalar_selection.py +202 -0
- desdeo/emo/operators/selection.py +956 -214
- desdeo/emo/operators/termination.py +124 -16
- desdeo/emo/options/__init__.py +108 -0
- desdeo/emo/options/algorithms.py +435 -0
- desdeo/emo/options/crossover.py +164 -0
- desdeo/emo/options/generator.py +131 -0
- desdeo/emo/options/mutation.py +260 -0
- desdeo/emo/options/repair.py +61 -0
- desdeo/emo/options/scalar_selection.py +66 -0
- desdeo/emo/options/selection.py +127 -0
- desdeo/emo/options/templates.py +383 -0
- desdeo/emo/options/termination.py +143 -0
- desdeo/gdm/__init__.py +22 -0
- desdeo/gdm/gdmtools.py +45 -0
- desdeo/gdm/score_bands.py +114 -0
- desdeo/gdm/voting_rules.py +50 -0
- desdeo/mcdm/__init__.py +23 -1
- desdeo/mcdm/enautilus.py +338 -0
- desdeo/mcdm/gnimbus.py +484 -0
- desdeo/mcdm/nautilus_navigator.py +7 -6
- desdeo/mcdm/reference_point_method.py +70 -0
- desdeo/problem/__init__.py +16 -11
- desdeo/problem/evaluator.py +4 -5
- desdeo/problem/external/__init__.py +18 -0
- desdeo/problem/external/core.py +356 -0
- desdeo/problem/external/pymoo_provider.py +266 -0
- desdeo/problem/external/runtime.py +44 -0
- desdeo/problem/gurobipy_evaluator.py +37 -12
- desdeo/problem/infix_parser.py +1 -16
- desdeo/problem/json_parser.py +7 -11
- desdeo/problem/pyomo_evaluator.py +25 -6
- desdeo/problem/schema.py +73 -55
- desdeo/problem/simulator_evaluator.py +65 -15
- desdeo/problem/testproblems/__init__.py +26 -11
- desdeo/problem/testproblems/benchmarks_server.py +120 -0
- desdeo/problem/testproblems/cake_problem.py +185 -0
- desdeo/problem/testproblems/dmitry_forest_problem_discrete.py +71 -0
- desdeo/problem/testproblems/forest_problem.py +77 -69
- desdeo/problem/testproblems/multi_valued_constraints.py +119 -0
- desdeo/problem/testproblems/{river_pollution_problem.py → river_pollution_problems.py} +28 -22
- desdeo/problem/testproblems/single_objective.py +289 -0
- desdeo/problem/testproblems/zdt_problem.py +4 -1
- desdeo/problem/utils.py +1 -1
- desdeo/tools/__init__.py +39 -21
- desdeo/tools/desc_gen.py +22 -0
- desdeo/tools/generics.py +22 -2
- desdeo/tools/group_scalarization.py +3090 -0
- desdeo/tools/indicators_binary.py +107 -1
- desdeo/tools/indicators_unary.py +3 -16
- desdeo/tools/message.py +33 -2
- desdeo/tools/non_dominated_sorting.py +4 -3
- desdeo/tools/patterns.py +9 -7
- desdeo/tools/pyomo_solver_interfaces.py +49 -36
- desdeo/tools/reference_vectors.py +118 -351
- desdeo/tools/scalarization.py +340 -1413
- desdeo/tools/score_bands.py +491 -328
- desdeo/tools/utils.py +117 -49
- desdeo/tools/visualizations.py +67 -0
- desdeo/utopia_stuff/utopia_problem.py +1 -1
- desdeo/utopia_stuff/utopia_problem_old.py +1 -1
- {desdeo-2.0.0.dist-info → desdeo-2.1.1.dist-info}/METADATA +47 -30
- desdeo-2.1.1.dist-info/RECORD +180 -0
- {desdeo-2.0.0.dist-info → desdeo-2.1.1.dist-info}/WHEEL +1 -1
- desdeo-2.0.0.dist-info/RECORD +0 -120
- /desdeo/api/utils/{logger.py → _logger.py} +0 -0
- {desdeo-2.0.0.dist-info → desdeo-2.1.1.dist-info/licenses}/LICENSE +0 -0
desdeo/emo/operators/mutation.py
CHANGED
|
@@ -16,17 +16,18 @@ from desdeo.tools.message import (
|
|
|
16
16
|
Message,
|
|
17
17
|
MutationMessageTopics,
|
|
18
18
|
PolarsDataFrameMessage,
|
|
19
|
+
TerminatorMessageTopics,
|
|
19
20
|
)
|
|
20
|
-
from desdeo.tools.patterns import Subscriber
|
|
21
|
+
from desdeo.tools.patterns import Publisher, Subscriber
|
|
21
22
|
|
|
22
23
|
|
|
23
24
|
class BaseMutation(Subscriber):
|
|
24
25
|
"""A base class for mutation operators."""
|
|
25
26
|
|
|
26
27
|
@abstractmethod
|
|
27
|
-
def __init__(self, problem: Problem,
|
|
28
|
-
"""Initialize a
|
|
29
|
-
super().__init__(
|
|
28
|
+
def __init__(self, problem: Problem, verbosity: int, publisher: Publisher):
|
|
29
|
+
"""Initialize a mutation operator."""
|
|
30
|
+
super().__init__(verbosity=verbosity, publisher=publisher)
|
|
30
31
|
self.problem = problem
|
|
31
32
|
self.variable_symbols = [var.symbol for var in problem.get_flattened_variables()]
|
|
32
33
|
self.lower_bounds = [var.lowerbound for var in problem.get_flattened_variables()]
|
|
@@ -83,21 +84,23 @@ class BoundedPolynomialMutation(BaseMutation):
|
|
|
83
84
|
*,
|
|
84
85
|
problem: Problem,
|
|
85
86
|
seed: int,
|
|
87
|
+
verbosity: int,
|
|
88
|
+
publisher: Publisher,
|
|
86
89
|
mutation_probability: float | None = None,
|
|
87
90
|
distribution_index: float = 20,
|
|
88
|
-
**kwargs,
|
|
89
91
|
):
|
|
90
92
|
"""Initialize a bounded polynomial mutation operator.
|
|
91
93
|
|
|
92
94
|
Args:
|
|
93
95
|
problem (Problem): The problem object.
|
|
94
96
|
seed (int): The seed for the random number generator.
|
|
97
|
+
verbosity (int): The verbosity level of the operator. See the `provided_topics` attribute to see what
|
|
98
|
+
messages are provided at each verbosity level. Recommended value is 1.
|
|
99
|
+
publisher (Publisher): The publisher to which the operator will send messages.
|
|
95
100
|
mutation_probability (float | None, optional): The probability of mutation. Defaults to None.
|
|
96
|
-
distribution_index (float, optional): The
|
|
97
|
-
kwargs: Additional keyword arguments. These are passed to the Subscriber class. At the very least, the
|
|
98
|
-
publisher must be passed. See the Subscriber class for more information.
|
|
101
|
+
distribution_index (float, optional): The distribution index for polynomial mutation. Defaults to 20.
|
|
99
102
|
"""
|
|
100
|
-
super().__init__(problem,
|
|
103
|
+
super().__init__(problem, verbosity=verbosity, publisher=publisher)
|
|
101
104
|
if self.variable_combination != VariableDomainTypeEnum.continuous:
|
|
102
105
|
raise ValueError("This mutation operator only works with continuous variables.")
|
|
103
106
|
if mutation_probability is None:
|
|
@@ -125,7 +128,7 @@ class BoundedPolynomialMutation(BaseMutation):
|
|
|
125
128
|
# TODO(@light-weaver): Extract to a numba jitted function
|
|
126
129
|
self.offspring_original = offsprings
|
|
127
130
|
self.parents = parents # Not used, but kept for consistency
|
|
128
|
-
offspring = offsprings.to_numpy()
|
|
131
|
+
offspring = offsprings.to_numpy(writable=True)
|
|
129
132
|
min_val = np.ones_like(offspring) * self.lower_bounds
|
|
130
133
|
max_val = np.ones_like(offspring) * self.upper_bounds
|
|
131
134
|
k = self.rng.random(size=offspring.shape)
|
|
@@ -242,8 +245,9 @@ class BinaryFlipMutation(BaseMutation):
|
|
|
242
245
|
*,
|
|
243
246
|
problem: Problem,
|
|
244
247
|
seed: int,
|
|
248
|
+
verbosity: int,
|
|
249
|
+
publisher: Publisher,
|
|
245
250
|
mutation_probability: float | None = None,
|
|
246
|
-
**kwargs,
|
|
247
251
|
):
|
|
248
252
|
"""Initialize a binary flip mutation operator.
|
|
249
253
|
|
|
@@ -253,10 +257,11 @@ class BinaryFlipMutation(BaseMutation):
|
|
|
253
257
|
mutation_probability (float | None, optional): The probability of mutation. If None,
|
|
254
258
|
the probability will be set to be 1/n, where n is the number of decision variables
|
|
255
259
|
in the problem. Defaults to None.
|
|
256
|
-
|
|
257
|
-
|
|
260
|
+
verbosity (int): The verbosity level of the operator. See the `provided_topics` attribute to see what
|
|
261
|
+
messages are provided at each verbosity level. Recommended value is 1.
|
|
262
|
+
publisher (Publisher): The publisher to which the operator will send messages.
|
|
258
263
|
"""
|
|
259
|
-
super().__init__(problem,
|
|
264
|
+
super().__init__(problem, verbosity=verbosity, publisher=publisher)
|
|
260
265
|
|
|
261
266
|
if self.variable_combination != VariableDomainTypeEnum.binary:
|
|
262
267
|
raise ValueError("This mutation operator only works with binary variables.")
|
|
@@ -284,7 +289,7 @@ class BinaryFlipMutation(BaseMutation):
|
|
|
284
289
|
"""
|
|
285
290
|
self.offspring_original = copy.copy(offsprings)
|
|
286
291
|
self.parents = parents # Not used, but kept for consistency
|
|
287
|
-
offspring = offsprings.to_numpy().astype(dtype=np.bool)
|
|
292
|
+
offspring = offsprings.to_numpy(writable=True).astype(dtype=np.bool)
|
|
288
293
|
|
|
289
294
|
# create a boolean mask based on the mutation probability
|
|
290
295
|
flip_mask = self.rng.random(offspring.shape) < self.mutation_probability
|
|
@@ -373,8 +378,9 @@ class IntegerRandomMutation(BaseMutation):
|
|
|
373
378
|
*,
|
|
374
379
|
problem: Problem,
|
|
375
380
|
seed: int,
|
|
381
|
+
verbosity: int,
|
|
382
|
+
publisher: Publisher,
|
|
376
383
|
mutation_probability: float | None = None,
|
|
377
|
-
**kwargs,
|
|
378
384
|
):
|
|
379
385
|
"""Initialize a random integer mutation operator.
|
|
380
386
|
|
|
@@ -384,10 +390,11 @@ class IntegerRandomMutation(BaseMutation):
|
|
|
384
390
|
mutation_probability (float | None, optional): The probability of mutation. If None,
|
|
385
391
|
the probability will be set to be 1/n, where n is the number of decision variables
|
|
386
392
|
in the problem. Defaults to None.
|
|
387
|
-
|
|
388
|
-
|
|
393
|
+
verbosity (int): The verbosity level of the operator. See the `provided_topics` attribute to see what
|
|
394
|
+
messages are provided at each verbosity level. Recommended value is 1.
|
|
395
|
+
publisher (Publisher): The publisher to which the operator will send messages.
|
|
389
396
|
"""
|
|
390
|
-
super().__init__(problem,
|
|
397
|
+
super().__init__(problem, verbosity=verbosity, publisher=publisher)
|
|
391
398
|
|
|
392
399
|
if self.variable_combination != VariableDomainTypeEnum.integer:
|
|
393
400
|
raise ValueError("This mutation operator only works with integer variables.")
|
|
@@ -416,7 +423,7 @@ class IntegerRandomMutation(BaseMutation):
|
|
|
416
423
|
self.offspring_original = copy.copy(offsprings)
|
|
417
424
|
self.parents = parents # Not used, but kept for consistency
|
|
418
425
|
|
|
419
|
-
population = offsprings.to_numpy().astype(int)
|
|
426
|
+
population = offsprings.to_numpy(writable=True).astype(int)
|
|
420
427
|
|
|
421
428
|
# create a boolean mask based on the mutation probability
|
|
422
429
|
mutation_mask = self.rng.random(population.shape) < self.mutation_probability
|
|
@@ -507,8 +514,9 @@ class MixedIntegerRandomMutation(BaseMutation):
|
|
|
507
514
|
*,
|
|
508
515
|
problem: Problem,
|
|
509
516
|
seed: int,
|
|
517
|
+
verbosity: int,
|
|
518
|
+
publisher: Publisher,
|
|
510
519
|
mutation_probability: float | None = None,
|
|
511
|
-
**kwargs,
|
|
512
520
|
):
|
|
513
521
|
"""Initialize a random mixed_integer mutation operator.
|
|
514
522
|
|
|
@@ -518,10 +526,11 @@ class MixedIntegerRandomMutation(BaseMutation):
|
|
|
518
526
|
mutation_probability (float | None, optional): The probability of mutation. If None,
|
|
519
527
|
the probability will be set to be 1/n, where n is the number of decision variables
|
|
520
528
|
in the problem. Defaults to None.
|
|
521
|
-
|
|
522
|
-
|
|
529
|
+
verbosity (int): The verbosity level of the operator. See the `provided_topics` attribute to see what
|
|
530
|
+
messages are provided at each verbosity level. Recommended value is 1.
|
|
531
|
+
publisher (Publisher): The publisher to which the operator will send messages.
|
|
523
532
|
"""
|
|
524
|
-
super().__init__(problem,
|
|
533
|
+
super().__init__(problem, verbosity=verbosity, publisher=publisher)
|
|
525
534
|
|
|
526
535
|
if mutation_probability is None:
|
|
527
536
|
self.mutation_probability = 1 / len(self.variable_symbols)
|
|
@@ -548,7 +557,7 @@ class MixedIntegerRandomMutation(BaseMutation):
|
|
|
548
557
|
self.offspring_original = copy.copy(offsprings)
|
|
549
558
|
self.parents = parents # Not used, but kept for consistency
|
|
550
559
|
|
|
551
|
-
population = offsprings.to_numpy().astype(float)
|
|
560
|
+
population = offsprings.to_numpy(writable=True).astype(float)
|
|
552
561
|
|
|
553
562
|
# create a boolean mask based on the mutation probability
|
|
554
563
|
mutation_mask = self.rng.random(population.shape) < self.mutation_probability
|
|
@@ -649,9 +658,10 @@ class MPTMutation(BaseMutation):
|
|
|
649
658
|
*,
|
|
650
659
|
problem: Problem,
|
|
651
660
|
seed: int,
|
|
661
|
+
verbosity: int,
|
|
662
|
+
publisher: Publisher,
|
|
652
663
|
mutation_probability: float | None = None,
|
|
653
664
|
mutation_exponent: float = 2.0,
|
|
654
|
-
**kwargs,
|
|
655
665
|
):
|
|
656
666
|
"""Initialize a small mutation operator.
|
|
657
667
|
|
|
@@ -660,10 +670,12 @@ class MPTMutation(BaseMutation):
|
|
|
660
670
|
seed (int): RNG seed.
|
|
661
671
|
mutation_probability (float | None): Probability of mutation per gene.
|
|
662
672
|
mutation_exponent (float): Controls strength of small mutation (larger means smaller mutations).
|
|
663
|
-
|
|
673
|
+
verbosity (int): The verbosity level of the operator. See the `provided_topics` attribute to see what
|
|
674
|
+
messages are provided at each verbosity level. Recommended value is 1.
|
|
675
|
+
publisher (Publisher): The publisher to which the operator will send messages.
|
|
664
676
|
publisher must be passed. See the Subscriber class for more information.
|
|
665
677
|
"""
|
|
666
|
-
super().__init__(problem,
|
|
678
|
+
super().__init__(problem, verbosity=verbosity, publisher=publisher)
|
|
667
679
|
self.rng = np.random.default_rng(seed)
|
|
668
680
|
self.seed = seed
|
|
669
681
|
self.mutation_exponent = mutation_exponent
|
|
@@ -699,7 +711,7 @@ class MPTMutation(BaseMutation):
|
|
|
699
711
|
self.offspring_original = copy.copy(offsprings)
|
|
700
712
|
self.parents = parents
|
|
701
713
|
|
|
702
|
-
population = offsprings.to_numpy().astype(float)
|
|
714
|
+
population = offsprings.to_numpy(writable=True).astype(float)
|
|
703
715
|
|
|
704
716
|
for i in range(population.shape[0]):
|
|
705
717
|
for j, var in enumerate(self.problem.variables):
|
|
@@ -757,6 +769,9 @@ class MPTMutation(BaseMutation):
|
|
|
757
769
|
]
|
|
758
770
|
|
|
759
771
|
|
|
772
|
+
#TODO (@light-weaver): Get rid of the max_generations parameter and instead get it from a message
|
|
773
|
+
# Additionally, allow the operator to use max_evaluations as a basis for decay
|
|
774
|
+
# Make sure that the ratio never exceeds 1.0, otherwise there will be issues
|
|
760
775
|
class NonUniformMutation(BaseMutation):
|
|
761
776
|
"""Non-uniform mutation operator.
|
|
762
777
|
|
|
@@ -779,17 +794,18 @@ class NonUniformMutation(BaseMutation):
|
|
|
779
794
|
@property
|
|
780
795
|
def interested_topics(self):
|
|
781
796
|
"""The message topics that the mutation operator is interested in."""
|
|
782
|
-
return []
|
|
797
|
+
return [TerminatorMessageTopics.GENERATION]
|
|
783
798
|
|
|
784
799
|
def __init__(
|
|
785
800
|
self,
|
|
786
801
|
*,
|
|
787
802
|
problem: Problem,
|
|
788
803
|
seed: int,
|
|
804
|
+
max_generations: int,
|
|
805
|
+
verbosity: int,
|
|
806
|
+
publisher: Publisher,
|
|
789
807
|
mutation_probability: float | None = None,
|
|
790
808
|
b: float = 5.0, # decay parameter
|
|
791
|
-
max_generations: int,
|
|
792
|
-
**kwargs,
|
|
793
809
|
):
|
|
794
810
|
"""Initialize a Non-uniform mutation operator.
|
|
795
811
|
|
|
@@ -801,9 +817,11 @@ class NonUniformMutation(BaseMutation):
|
|
|
801
817
|
b (float): Non-uniform mutation decay parameter. Higher values cause
|
|
802
818
|
faster reduction in mutation strength over generations.
|
|
803
819
|
max_generations (int): Maximum number of generations in the evolutionary run. Used to scale mutation decay.
|
|
804
|
-
|
|
820
|
+
verbosity (int): The verbosity level of the operator. See the `provided_topics` attribute to see what
|
|
821
|
+
messages are provided at each verbosity level. Recommended value is 1.
|
|
822
|
+
publisher (Publisher): The publisher to which the operator will send messages.
|
|
805
823
|
"""
|
|
806
|
-
super().__init__(problem,
|
|
824
|
+
super().__init__(problem, verbosity=verbosity, publisher=publisher)
|
|
807
825
|
self.rng = np.random.default_rng(seed)
|
|
808
826
|
self.seed = seed
|
|
809
827
|
self.b = b
|
|
@@ -858,7 +876,7 @@ class NonUniformMutation(BaseMutation):
|
|
|
858
876
|
self.offspring_original = copy.copy(offsprings)
|
|
859
877
|
self.parents = parents
|
|
860
878
|
|
|
861
|
-
population = offsprings.to_numpy().astype(float)
|
|
879
|
+
population = offsprings.to_numpy(writable=True).astype(float)
|
|
862
880
|
|
|
863
881
|
for i in range(population.shape[0]):
|
|
864
882
|
for j, var in enumerate(self.problem.variables):
|
|
@@ -875,9 +893,17 @@ class NonUniformMutation(BaseMutation):
|
|
|
875
893
|
|
|
876
894
|
return self.offspring
|
|
877
895
|
|
|
878
|
-
def update(self,
|
|
896
|
+
def update(self, message: Message):
|
|
879
897
|
"""Update current generation (used to reduce mutation strength over time)."""
|
|
880
|
-
|
|
898
|
+
if not isinstance(message.topic, TerminatorMessageTopics):
|
|
899
|
+
return
|
|
900
|
+
if not isinstance(message.value, int):
|
|
901
|
+
return
|
|
902
|
+
if message.topic != TerminatorMessageTopics.GENERATION:
|
|
903
|
+
raise ValueError(
|
|
904
|
+
f"Expected message topic {TerminatorMessageTopics.GENERATION}, got {message.topic}."
|
|
905
|
+
)
|
|
906
|
+
self.current_generation = message.value
|
|
881
907
|
|
|
882
908
|
def state(self) -> Sequence[Message]:
|
|
883
909
|
"""Return state messages."""
|
|
@@ -923,8 +949,9 @@ class SelfAdaptiveGaussianMutation(BaseMutation):
|
|
|
923
949
|
*,
|
|
924
950
|
problem: Problem,
|
|
925
951
|
seed: int,
|
|
952
|
+
verbosity: int,
|
|
953
|
+
publisher: Publisher,
|
|
926
954
|
mutation_probability: float | None = None,
|
|
927
|
-
**kwargs,
|
|
928
955
|
):
|
|
929
956
|
"""Initialize the self-adaptive Gaussian mutation operator.
|
|
930
957
|
|
|
@@ -933,7 +960,9 @@ class SelfAdaptiveGaussianMutation(BaseMutation):
|
|
|
933
960
|
seed (int): Seed for the random number generator to ensure reproducibility.
|
|
934
961
|
mutation_probability (float | None): Probability of mutating each gene.
|
|
935
962
|
If None, it defaults to 1 divided by the number of variables.
|
|
936
|
-
|
|
963
|
+
verbosity (int): The verbosity level of the operator. See the `provided_topics` attribute to see what
|
|
964
|
+
messages are provided at each verbosity level. Recommended value is 1.
|
|
965
|
+
publisher (Publisher): The publisher to which the operator will send messages.
|
|
937
966
|
|
|
938
967
|
Attributes:
|
|
939
968
|
rng (Generator): NumPy random number generator initialized with the given seed.
|
|
@@ -943,7 +972,7 @@ class SelfAdaptiveGaussianMutation(BaseMutation):
|
|
|
943
972
|
tau_prime (float): Global learning rate, used in step size adaptation.
|
|
944
973
|
tau (float): Local learning rate, used in step size adaptation.
|
|
945
974
|
"""
|
|
946
|
-
super().__init__(problem=problem,
|
|
975
|
+
super().__init__(problem=problem, verbosity=verbosity, publisher=publisher)
|
|
947
976
|
|
|
948
977
|
self.rng = np.random.default_rng(seed)
|
|
949
978
|
self.seed = seed
|
|
@@ -975,7 +1004,7 @@ class SelfAdaptiveGaussianMutation(BaseMutation):
|
|
|
975
1004
|
self.offspring_original = offsprings
|
|
976
1005
|
self.parents = parents
|
|
977
1006
|
|
|
978
|
-
offspring_array = offsprings.to_numpy().astype(float)
|
|
1007
|
+
offspring_array = offsprings.to_numpy(writable=True).astype(float)
|
|
979
1008
|
|
|
980
1009
|
if step_sizes is None:
|
|
981
1010
|
step_sizes = np.full_like(offspring_array, fill_value=0.1)
|
|
@@ -1051,3 +1080,145 @@ class SelfAdaptiveGaussianMutation(BaseMutation):
|
|
|
1051
1080
|
value=self.mutation_probability,
|
|
1052
1081
|
),
|
|
1053
1082
|
]
|
|
1083
|
+
|
|
1084
|
+
|
|
1085
|
+
class PowerMutation(BaseMutation):
|
|
1086
|
+
"""Implements the Power Mutation (PM) operator for real and integer variables."""
|
|
1087
|
+
|
|
1088
|
+
@property
|
|
1089
|
+
def provided_topics(self) -> dict[int, Sequence[MutationMessageTopics]]:
|
|
1090
|
+
"""The message topics provided by the mutation operator."""
|
|
1091
|
+
return {
|
|
1092
|
+
0: [],
|
|
1093
|
+
1: [MutationMessageTopics.MUTATION_PROBABILITY],
|
|
1094
|
+
2: [
|
|
1095
|
+
MutationMessageTopics.MUTATION_PROBABILITY,
|
|
1096
|
+
MutationMessageTopics.OFFSPRING_ORIGINAL,
|
|
1097
|
+
MutationMessageTopics.OFFSPRINGS,
|
|
1098
|
+
],
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
@property
|
|
1102
|
+
def interested_topics(self):
|
|
1103
|
+
"""The message topics that the mutation operator listens to (none in this case)."""
|
|
1104
|
+
return []
|
|
1105
|
+
|
|
1106
|
+
def __init__(
|
|
1107
|
+
self,
|
|
1108
|
+
*,
|
|
1109
|
+
problem: Problem,
|
|
1110
|
+
seed: int,
|
|
1111
|
+
verbosity: int,
|
|
1112
|
+
publisher: Publisher,
|
|
1113
|
+
p: float = 1.5,
|
|
1114
|
+
mutation_probability: float | None = None,
|
|
1115
|
+
):
|
|
1116
|
+
"""Initialize the PowerMutation operator.
|
|
1117
|
+
|
|
1118
|
+
Args:
|
|
1119
|
+
problem (Problem): The problem definition containing variable bounds and types.
|
|
1120
|
+
seed (int): Random seed for reproducibility.
|
|
1121
|
+
p (float): Power distribution parameter. Controls the perturbation magnitude. Default is 1.5.
|
|
1122
|
+
mutation_probability (float | None): Per-variable mutation probability. Defaults to 1/n.
|
|
1123
|
+
verbosity (int): The verbosity level of the operator. See the `provided_topics` attribute to see what
|
|
1124
|
+
messages are provided at each verbosity level. Recommended value is 1.
|
|
1125
|
+
publisher (Publisher): The publisher to which the operator will send messages.
|
|
1126
|
+
"""
|
|
1127
|
+
super().__init__(problem, verbosity=verbosity, publisher=publisher)
|
|
1128
|
+
self.p = p
|
|
1129
|
+
self.mutation_probability = (
|
|
1130
|
+
mutation_probability if mutation_probability is not None else 1 / len(self.variable_symbols)
|
|
1131
|
+
)
|
|
1132
|
+
self.rng = np.random.default_rng(seed)
|
|
1133
|
+
self.seed = seed
|
|
1134
|
+
self.offspring_original: pl.DataFrame
|
|
1135
|
+
self.parents: pl.DataFrame
|
|
1136
|
+
self.offspring: pl.DataFrame
|
|
1137
|
+
|
|
1138
|
+
def do(self, offsprings: pl.DataFrame, parents: pl.DataFrame) -> pl.DataFrame:
|
|
1139
|
+
"""Apply Power Mutation to the given offspring population.
|
|
1140
|
+
|
|
1141
|
+
Args:
|
|
1142
|
+
offsprings (pl.DataFrame): The offspring population to mutate.
|
|
1143
|
+
parents (pl.DataFrame): The parent population
|
|
1144
|
+
|
|
1145
|
+
Returns:
|
|
1146
|
+
pl.DataFrame: Mutated offspring population.
|
|
1147
|
+
"""
|
|
1148
|
+
self.offspring_original = copy.copy(offsprings)
|
|
1149
|
+
self.parents = parents
|
|
1150
|
+
|
|
1151
|
+
if self.mutation_probability == 0.0:
|
|
1152
|
+
self.offspring = offsprings.clone()
|
|
1153
|
+
self.notify()
|
|
1154
|
+
return self.offspring
|
|
1155
|
+
|
|
1156
|
+
population = offsprings.to_numpy(writable=True).astype(float)
|
|
1157
|
+
mutation_mask = self.rng.random(population.shape) < self.mutation_probability
|
|
1158
|
+
mutated = population.copy()
|
|
1159
|
+
|
|
1160
|
+
for i, var in enumerate(self.problem.variables):
|
|
1161
|
+
lower_bound, upper_bound = var.lowerbound, var.upperbound
|
|
1162
|
+
x_i = population[:, i]
|
|
1163
|
+
|
|
1164
|
+
u_i = self.rng.random(len(x_i)) # uniform random number
|
|
1165
|
+
s_i = u_i ** (1 / self.p) # random number that follows the power distribution
|
|
1166
|
+
|
|
1167
|
+
r_i = self.rng.random(len(x_i)) # another uniform random number
|
|
1168
|
+
direction = ((x_i - lower_bound) / (upper_bound - lower_bound)) < r_i # used as condition
|
|
1169
|
+
|
|
1170
|
+
xi_mutated = np.where(direction, x_i - s_i * (x_i - lower_bound), x_i + s_i * (upper_bound - x_i))
|
|
1171
|
+
|
|
1172
|
+
# Apply mutation based on mask
|
|
1173
|
+
mutated[:, i] = np.where(mutation_mask[:, i], xi_mutated, x_i)
|
|
1174
|
+
|
|
1175
|
+
# Convert back to DataFrame
|
|
1176
|
+
self.offspring = pl.from_numpy(mutated, schema=self.variable_symbols).select(pl.all()).cast(pl.Float64)
|
|
1177
|
+
self.notify()
|
|
1178
|
+
|
|
1179
|
+
return self.offspring
|
|
1180
|
+
|
|
1181
|
+
def update(self, *_, **__):
|
|
1182
|
+
"""No update logic needed."""
|
|
1183
|
+
|
|
1184
|
+
def state(self) -> Sequence[Message]:
|
|
1185
|
+
"""Return mutation-related state messages based on verbosity level.
|
|
1186
|
+
|
|
1187
|
+
Returns:
|
|
1188
|
+
List of messages reporting mutation probability, input, and output (at higher verbosity).
|
|
1189
|
+
"""
|
|
1190
|
+
if self.offspring_original is None or self.offspring is None or self.verbosity == 0:
|
|
1191
|
+
return []
|
|
1192
|
+
|
|
1193
|
+
if self.verbosity == 1:
|
|
1194
|
+
return [
|
|
1195
|
+
FloatMessage(
|
|
1196
|
+
topic=MutationMessageTopics.MUTATION_PROBABILITY,
|
|
1197
|
+
source=self.__class__.__name__,
|
|
1198
|
+
value=self.mutation_probability,
|
|
1199
|
+
),
|
|
1200
|
+
]
|
|
1201
|
+
|
|
1202
|
+
# Verbosity == 2
|
|
1203
|
+
return [
|
|
1204
|
+
PolarsDataFrameMessage(
|
|
1205
|
+
topic=MutationMessageTopics.OFFSPRING_ORIGINAL,
|
|
1206
|
+
source=self.__class__.__name__,
|
|
1207
|
+
value=self.offspring_original,
|
|
1208
|
+
),
|
|
1209
|
+
PolarsDataFrameMessage(
|
|
1210
|
+
topic=MutationMessageTopics.PARENTS,
|
|
1211
|
+
source=self.__class__.__name__,
|
|
1212
|
+
value=self.parents,
|
|
1213
|
+
),
|
|
1214
|
+
PolarsDataFrameMessage(
|
|
1215
|
+
topic=MutationMessageTopics.OFFSPRINGS,
|
|
1216
|
+
source=self.__class__.__name__,
|
|
1217
|
+
value=self.offspring,
|
|
1218
|
+
),
|
|
1219
|
+
FloatMessage(
|
|
1220
|
+
topic=MutationMessageTopics.MUTATION_PROBABILITY,
|
|
1221
|
+
source=self.__class__.__name__,
|
|
1222
|
+
value=self.mutation_probability,
|
|
1223
|
+
),
|
|
1224
|
+
]
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
"""Classs for scalar selection operators."""
|
|
2
|
+
|
|
3
|
+
from abc import abstractmethod
|
|
4
|
+
from collections.abc import Sequence
|
|
5
|
+
|
|
6
|
+
import numpy as np
|
|
7
|
+
import polars as pl
|
|
8
|
+
|
|
9
|
+
from desdeo.emo.operators.selection import SolutionType
|
|
10
|
+
from desdeo.tools.message import Message, SelectorMessageTopics
|
|
11
|
+
from desdeo.tools.patterns import Publisher, Subscriber
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class BaseScalarSelector(Subscriber):
|
|
15
|
+
"""A base class for selection operators."""
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def provided_topics(self):
|
|
19
|
+
return {
|
|
20
|
+
0: [],
|
|
21
|
+
1: [],
|
|
22
|
+
2: [],
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def interested_topics(self):
|
|
27
|
+
return [
|
|
28
|
+
SelectorMessageTopics.SELECTED_FITNESS,
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
def __init__(self, verbosity: int, publisher: Publisher):
|
|
32
|
+
"""Initialize a selection operator."""
|
|
33
|
+
super().__init__(verbosity=verbosity, publisher=publisher)
|
|
34
|
+
self.fitness: np.ndarray | None = None
|
|
35
|
+
|
|
36
|
+
@abstractmethod
|
|
37
|
+
def _do(
|
|
38
|
+
self,
|
|
39
|
+
solutions: tuple[SolutionType, pl.DataFrame],
|
|
40
|
+
fitness: np.ndarray | None = None,
|
|
41
|
+
) -> tuple[SolutionType, pl.DataFrame]:
|
|
42
|
+
"""Perform the selection operation.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
solutions (tuple[SolutionType, pl.DataFrame]): the decision variables as the first element.
|
|
46
|
+
The second element is the objective values, targets, and constraint violations.
|
|
47
|
+
fitness (np.ndarray | None, optional): The fitness values of the solutions. If None, the fitness is
|
|
48
|
+
calculated from the messages sent by the publisher.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
SolutionType: The selected decision variables.
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
def do(
|
|
55
|
+
self,
|
|
56
|
+
solutions: tuple[SolutionType, pl.DataFrame],
|
|
57
|
+
fitness: np.ndarray | None = None,
|
|
58
|
+
) -> tuple[SolutionType, pl.DataFrame]:
|
|
59
|
+
"""Perform the selection operation.
|
|
60
|
+
|
|
61
|
+
Args:
|
|
62
|
+
solutions (tuple[SolutionType, pl.DataFrame]): the decision variables as the first element.
|
|
63
|
+
The second element is the objective values, targets, and constraint violations.
|
|
64
|
+
fitness (np.ndarray | None, optional): The fitness values of the solutions. If None, the fitness is
|
|
65
|
+
calculated from the messages sent by the publisher.
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
SolutionType: The selected decision variables.
|
|
69
|
+
"""
|
|
70
|
+
if fitness is not None and self.fitness is not None:
|
|
71
|
+
raise RuntimeError("The fitness is being set twice.")
|
|
72
|
+
if fitness is None and self.fitness is None:
|
|
73
|
+
raise RuntimeError(
|
|
74
|
+
"The fitness is not set. Either pass it as an argument or make sure the publisher sends it."
|
|
75
|
+
)
|
|
76
|
+
if fitness is None:
|
|
77
|
+
fitness = self.fitness
|
|
78
|
+
if len(fitness) != len(solutions[0]):
|
|
79
|
+
raise ValueError(
|
|
80
|
+
f"The length of the fitness array ({len(fitness)}) does not match"
|
|
81
|
+
f" the number of solutions ({len(solutions[0])})."
|
|
82
|
+
)
|
|
83
|
+
returnval = self._do(solutions, fitness)
|
|
84
|
+
self.fitness = None # Reset fitness after selection
|
|
85
|
+
return returnval
|
|
86
|
+
|
|
87
|
+
def update(self, message: Message) -> None:
|
|
88
|
+
"""Update the operator with a message.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
message (Message): The message to update the operator with.
|
|
92
|
+
"""
|
|
93
|
+
if message.topic == SelectorMessageTopics.SELECTED_FITNESS and isinstance(message.value, np.ndarray):
|
|
94
|
+
self.fitness = message.value
|
|
95
|
+
else:
|
|
96
|
+
raise ValueError(f"Unknown message topic: {message.topic}")
|
|
97
|
+
|
|
98
|
+
def state(self) -> Sequence[Message]:
|
|
99
|
+
return []
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class TournamentSelection(BaseScalarSelector):
|
|
103
|
+
"""A tournament selection operator."""
|
|
104
|
+
|
|
105
|
+
def __init__(
|
|
106
|
+
self,
|
|
107
|
+
*,
|
|
108
|
+
winner_size: int,
|
|
109
|
+
verbosity: int,
|
|
110
|
+
publisher: Publisher,
|
|
111
|
+
tournament_size: int = 2,
|
|
112
|
+
seed: int | None = None,
|
|
113
|
+
selection_probability: float | None = None,
|
|
114
|
+
) -> None:
|
|
115
|
+
"""Initialize the tournament selection operator.
|
|
116
|
+
|
|
117
|
+
Args:
|
|
118
|
+
winner_size (int): The number of winners to select.
|
|
119
|
+
verbosity (int): The verbosity level of the operator.
|
|
120
|
+
publisher (Publisher): The publisher to send messages to.
|
|
121
|
+
tournament_size (int, optional): The size of the tournament. Defaults to 2, which corresponds to binary
|
|
122
|
+
tournament.
|
|
123
|
+
seed (int | None, optional): The seed for the random number generator. If None, the deterministic tournament
|
|
124
|
+
selection is used, i.e., the solution with the highest fitness in the tournament is always selected.
|
|
125
|
+
Otherwise the selection is stochastic, and the solution is selected with a probability proportional to
|
|
126
|
+
its fitness. Defaults to None.
|
|
127
|
+
selection_probability (float | None, optional): The probability of selecting a solution in the tournament.
|
|
128
|
+
If None, but a seed is provided, then the probabilities are proportional to the fitness values of the
|
|
129
|
+
solutions in the tournament. If None and no seed is provided, then the selection is deterministic.
|
|
130
|
+
If a value is provided, and the seed is not None, then the selection is stochastic, and the
|
|
131
|
+
probabilities of choosing the k-best solution in the tournament is given by p * (1 - p) ** (k - 1),
|
|
132
|
+
where p is the selection probability. Note that doing selection with a probability proportional to
|
|
133
|
+
fitness is equivalent to roulette wheel selection.
|
|
134
|
+
"""
|
|
135
|
+
super().__init__(verbosity=verbosity, publisher=publisher)
|
|
136
|
+
self.winner_size = winner_size
|
|
137
|
+
self.tournament_size = tournament_size
|
|
138
|
+
self.seed = seed
|
|
139
|
+
self.rng = np.random.default_rng(seed)
|
|
140
|
+
self.selection_probability = selection_probability
|
|
141
|
+
if self.seed is None and self.selection_probability is not None:
|
|
142
|
+
raise ValueError(
|
|
143
|
+
"If selection_probability is provided, seed must also be provided to ensure stochastic selection."
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
@staticmethod
|
|
147
|
+
def deterministic_select(indices: np.ndarray, fitness: np.ndarray) -> int:
|
|
148
|
+
"""Select the index of the solution with the highest fitness from the given indices.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
indices (np.ndarray): The indices of the solutions to select from.
|
|
152
|
+
fitness (np.ndarray): The fitness values of the solutions.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
int: The index of the solution with the highest fitness.
|
|
156
|
+
"""
|
|
157
|
+
return indices[np.argmax(fitness)]
|
|
158
|
+
|
|
159
|
+
def stochastic_select(self, indices: np.ndarray, fitness: np.ndarray) -> int:
|
|
160
|
+
"""Select the index of the solution with a probability proportional to its fitness from the given indices.
|
|
161
|
+
|
|
162
|
+
Args:
|
|
163
|
+
indices (np.ndarray): The indices of the solutions to select from.
|
|
164
|
+
fitness (np.ndarray): The fitness values of the solutions.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
int: The index of the selected solution.
|
|
168
|
+
"""
|
|
169
|
+
if self.selection_probability is None:
|
|
170
|
+
probabilities = fitness / np.sum(fitness)
|
|
171
|
+
probabilities = np.cumsum(probabilities)
|
|
172
|
+
else:
|
|
173
|
+
indices = indices[np.argsort(fitness)[::-1]] # Sort indices by fitness in descending order
|
|
174
|
+
probabilities = np.array(
|
|
175
|
+
[self.selection_probability * (1 - self.selection_probability) ** i for i in range(len(indices))]
|
|
176
|
+
)
|
|
177
|
+
random_value = self.rng.random()
|
|
178
|
+
selected_index = np.searchsorted(probabilities, random_value)
|
|
179
|
+
return indices[selected_index]
|
|
180
|
+
|
|
181
|
+
def _do(
|
|
182
|
+
self, solutions: tuple[SolutionType, pl.DataFrame], fitness: np.ndarray | None = None
|
|
183
|
+
) -> tuple[SolutionType, pl.DataFrame]:
|
|
184
|
+
"""Perform the tournament selection operation.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
solutions (tuple[SolutionType, pl.DataFrame]): The decision variables and their outputs.
|
|
188
|
+
fitness (np.ndarray | None, optional): The fitness values of the solutions.
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
tuple[SolutionType, pl.DataFrame]: The selected decision variables and their outputs.
|
|
192
|
+
"""
|
|
193
|
+
selected_indices = np.zeros(self.winner_size, dtype=int)
|
|
194
|
+
for i in range(self.winner_size):
|
|
195
|
+
tournament_indices = self.rng.choice(range(len(solutions[0])), size=self.tournament_size, replace=True)
|
|
196
|
+
if self.seed is None:
|
|
197
|
+
selected_indices[i] = self.deterministic_select(tournament_indices, fitness[tournament_indices])
|
|
198
|
+
else:
|
|
199
|
+
selected_indices[i] = self.stochastic_select(tournament_indices, fitness[tournament_indices])
|
|
200
|
+
selected_solutions = solutions[0][selected_indices]
|
|
201
|
+
selected_outputs = solutions[1][selected_indices]
|
|
202
|
+
return selected_solutions, selected_outputs
|