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.
Files changed (130) hide show
  1. desdeo/adm/ADMAfsar.py +551 -0
  2. desdeo/adm/ADMChen.py +414 -0
  3. desdeo/adm/BaseADM.py +119 -0
  4. desdeo/adm/__init__.py +11 -0
  5. desdeo/api/__init__.py +6 -6
  6. desdeo/api/app.py +38 -28
  7. desdeo/api/config.py +65 -44
  8. desdeo/api/config.toml +23 -12
  9. desdeo/api/db.py +10 -8
  10. desdeo/api/db_init.py +12 -6
  11. desdeo/api/models/__init__.py +220 -20
  12. desdeo/api/models/archive.py +16 -27
  13. desdeo/api/models/emo.py +128 -0
  14. desdeo/api/models/enautilus.py +69 -0
  15. desdeo/api/models/gdm/gdm_aggregate.py +139 -0
  16. desdeo/api/models/gdm/gdm_base.py +69 -0
  17. desdeo/api/models/gdm/gdm_score_bands.py +114 -0
  18. desdeo/api/models/gdm/gnimbus.py +138 -0
  19. desdeo/api/models/generic.py +104 -0
  20. desdeo/api/models/generic_states.py +401 -0
  21. desdeo/api/models/nimbus.py +158 -0
  22. desdeo/api/models/preference.py +44 -6
  23. desdeo/api/models/problem.py +274 -64
  24. desdeo/api/models/session.py +4 -1
  25. desdeo/api/models/state.py +419 -52
  26. desdeo/api/models/user.py +7 -6
  27. desdeo/api/models/utopia.py +25 -0
  28. desdeo/api/routers/_EMO.backup +309 -0
  29. desdeo/api/routers/_NIMBUS.py +6 -3
  30. desdeo/api/routers/emo.py +497 -0
  31. desdeo/api/routers/enautilus.py +237 -0
  32. desdeo/api/routers/gdm/gdm_aggregate.py +234 -0
  33. desdeo/api/routers/gdm/gdm_base.py +420 -0
  34. desdeo/api/routers/gdm/gdm_score_bands/gdm_score_bands_manager.py +398 -0
  35. desdeo/api/routers/gdm/gdm_score_bands/gdm_score_bands_routers.py +377 -0
  36. desdeo/api/routers/gdm/gnimbus/gnimbus_manager.py +698 -0
  37. desdeo/api/routers/gdm/gnimbus/gnimbus_routers.py +591 -0
  38. desdeo/api/routers/generic.py +233 -0
  39. desdeo/api/routers/nimbus.py +705 -0
  40. desdeo/api/routers/problem.py +201 -4
  41. desdeo/api/routers/reference_point_method.py +20 -44
  42. desdeo/api/routers/session.py +50 -26
  43. desdeo/api/routers/user_authentication.py +180 -26
  44. desdeo/api/routers/utils.py +187 -0
  45. desdeo/api/routers/utopia.py +230 -0
  46. desdeo/api/schema.py +10 -4
  47. desdeo/api/tests/conftest.py +94 -2
  48. desdeo/api/tests/test_enautilus.py +330 -0
  49. desdeo/api/tests/test_models.py +550 -72
  50. desdeo/api/tests/test_routes.py +902 -43
  51. desdeo/api/utils/_database.py +263 -0
  52. desdeo/api/utils/database.py +28 -266
  53. desdeo/api/utils/emo_database.py +40 -0
  54. desdeo/core.py +7 -0
  55. desdeo/emo/__init__.py +154 -24
  56. desdeo/emo/hooks/archivers.py +18 -2
  57. desdeo/emo/methods/EAs.py +128 -5
  58. desdeo/emo/methods/bases.py +9 -56
  59. desdeo/emo/methods/templates.py +111 -0
  60. desdeo/emo/operators/crossover.py +544 -42
  61. desdeo/emo/operators/evaluator.py +10 -14
  62. desdeo/emo/operators/generator.py +127 -24
  63. desdeo/emo/operators/mutation.py +212 -41
  64. desdeo/emo/operators/scalar_selection.py +202 -0
  65. desdeo/emo/operators/selection.py +956 -214
  66. desdeo/emo/operators/termination.py +124 -16
  67. desdeo/emo/options/__init__.py +108 -0
  68. desdeo/emo/options/algorithms.py +435 -0
  69. desdeo/emo/options/crossover.py +164 -0
  70. desdeo/emo/options/generator.py +131 -0
  71. desdeo/emo/options/mutation.py +260 -0
  72. desdeo/emo/options/repair.py +61 -0
  73. desdeo/emo/options/scalar_selection.py +66 -0
  74. desdeo/emo/options/selection.py +127 -0
  75. desdeo/emo/options/templates.py +383 -0
  76. desdeo/emo/options/termination.py +143 -0
  77. desdeo/gdm/__init__.py +22 -0
  78. desdeo/gdm/gdmtools.py +45 -0
  79. desdeo/gdm/score_bands.py +114 -0
  80. desdeo/gdm/voting_rules.py +50 -0
  81. desdeo/mcdm/__init__.py +23 -1
  82. desdeo/mcdm/enautilus.py +338 -0
  83. desdeo/mcdm/gnimbus.py +484 -0
  84. desdeo/mcdm/nautilus_navigator.py +7 -6
  85. desdeo/mcdm/reference_point_method.py +70 -0
  86. desdeo/problem/__init__.py +16 -11
  87. desdeo/problem/evaluator.py +4 -5
  88. desdeo/problem/external/__init__.py +18 -0
  89. desdeo/problem/external/core.py +356 -0
  90. desdeo/problem/external/pymoo_provider.py +266 -0
  91. desdeo/problem/external/runtime.py +44 -0
  92. desdeo/problem/gurobipy_evaluator.py +37 -12
  93. desdeo/problem/infix_parser.py +1 -16
  94. desdeo/problem/json_parser.py +7 -11
  95. desdeo/problem/pyomo_evaluator.py +25 -6
  96. desdeo/problem/schema.py +73 -55
  97. desdeo/problem/simulator_evaluator.py +65 -15
  98. desdeo/problem/testproblems/__init__.py +26 -11
  99. desdeo/problem/testproblems/benchmarks_server.py +120 -0
  100. desdeo/problem/testproblems/cake_problem.py +185 -0
  101. desdeo/problem/testproblems/dmitry_forest_problem_discrete.py +71 -0
  102. desdeo/problem/testproblems/forest_problem.py +77 -69
  103. desdeo/problem/testproblems/multi_valued_constraints.py +119 -0
  104. desdeo/problem/testproblems/{river_pollution_problem.py → river_pollution_problems.py} +28 -22
  105. desdeo/problem/testproblems/single_objective.py +289 -0
  106. desdeo/problem/testproblems/zdt_problem.py +4 -1
  107. desdeo/problem/utils.py +1 -1
  108. desdeo/tools/__init__.py +39 -21
  109. desdeo/tools/desc_gen.py +22 -0
  110. desdeo/tools/generics.py +22 -2
  111. desdeo/tools/group_scalarization.py +3090 -0
  112. desdeo/tools/indicators_binary.py +107 -1
  113. desdeo/tools/indicators_unary.py +3 -16
  114. desdeo/tools/message.py +33 -2
  115. desdeo/tools/non_dominated_sorting.py +4 -3
  116. desdeo/tools/patterns.py +9 -7
  117. desdeo/tools/pyomo_solver_interfaces.py +49 -36
  118. desdeo/tools/reference_vectors.py +118 -351
  119. desdeo/tools/scalarization.py +340 -1413
  120. desdeo/tools/score_bands.py +491 -328
  121. desdeo/tools/utils.py +117 -49
  122. desdeo/tools/visualizations.py +67 -0
  123. desdeo/utopia_stuff/utopia_problem.py +1 -1
  124. desdeo/utopia_stuff/utopia_problem_old.py +1 -1
  125. {desdeo-2.0.0.dist-info → desdeo-2.1.1.dist-info}/METADATA +47 -30
  126. desdeo-2.1.1.dist-info/RECORD +180 -0
  127. {desdeo-2.0.0.dist-info → desdeo-2.1.1.dist-info}/WHEEL +1 -1
  128. desdeo-2.0.0.dist-info/RECORD +0 -120
  129. /desdeo/api/utils/{logger.py → _logger.py} +0 -0
  130. {desdeo-2.0.0.dist-info → desdeo-2.1.1.dist-info/licenses}/LICENSE +0 -0
@@ -7,7 +7,6 @@ in multiobjective optimization are defined here.
7
7
  import copy
8
8
  from abc import abstractmethod
9
9
  from collections.abc import Sequence
10
- from random import shuffle
11
10
 
12
11
  import numpy as np
13
12
  import polars as pl
@@ -19,15 +18,15 @@ from desdeo.tools.message import (
19
18
  Message,
20
19
  PolarsDataFrameMessage,
21
20
  )
22
- from desdeo.tools.patterns import Subscriber
21
+ from desdeo.tools.patterns import Publisher, Subscriber
23
22
 
24
23
 
25
24
  class BaseCrossover(Subscriber):
26
25
  """A base class for crossover operators."""
27
26
 
28
- def __init__(self, problem: Problem, **kwargs):
27
+ def __init__(self, problem: Problem, verbosity: int, publisher: Publisher):
29
28
  """Initialize a crossover operator."""
30
- super().__init__(**kwargs)
29
+ super().__init__(verbosity=verbosity, publisher=publisher)
31
30
  self.problem = problem
32
31
  self.variable_symbols = [var.symbol for var in problem.get_flattened_variables()]
33
32
  self.lower_bounds = [var.lowerbound for var in problem.get_flattened_variables()]
@@ -61,7 +60,7 @@ class SimulatedBinaryCrossover(BaseCrossover):
61
60
  """
62
61
 
63
62
  @property
64
- def provided_topics(self) -> dict[str, Sequence[CrossoverMessageTopics]]:
63
+ def provided_topics(self) -> dict[int, Sequence[CrossoverMessageTopics]]:
65
64
  """The message topics provided by the crossover operator."""
66
65
  return {
67
66
  0: [],
@@ -80,22 +79,30 @@ class SimulatedBinaryCrossover(BaseCrossover):
80
79
  return []
81
80
 
82
81
  def __init__(
83
- self, *, problem: Problem, seed: int, xover_probability: float = 1.0, xover_distribution: float = 30, **kwargs
82
+ self,
83
+ *,
84
+ problem: Problem,
85
+ seed: int,
86
+ verbosity: int,
87
+ publisher: Publisher,
88
+ xover_probability: float = 1.0,
89
+ xover_distribution: float = 30,
84
90
  ):
85
91
  """Initialize a simulated binary crossover operator.
86
92
 
87
93
  Args:
88
94
  problem (Problem): the problem object.
89
95
  seed (int): the seed for the random number generator.
96
+ verbosity (int): the verbosity level of the component. The keys in `provided_topics` tell what
97
+ topics are provided by the operator at each verbosity level. Recommended to be set to 1.
98
+ publisher (Publisher): the publisher to which the operator will publish messages.
90
99
  xover_probability (float, optional): the crossover probability
91
100
  parameter. Ranges between 0 and 1.0. Defaults to 1.0.
92
101
  xover_distribution (float, optional): the crossover distribution
93
102
  parameter. Must be positive. Defaults to 30.
94
- kwargs: Additional keyword arguments. These are passed to the Subscriber class. At the very least, the
95
- publisher must be passed. See the Subscriber class for more information.
96
103
  """
97
104
  # Subscribes to no topics, so no need to stroe/pass the topics to the super class.
98
- super().__init__(problem, **kwargs)
105
+ super().__init__(problem, verbosity=verbosity, publisher=publisher)
99
106
  self.problem = problem
100
107
 
101
108
  if not 0 <= xover_probability <= 1:
@@ -135,7 +142,7 @@ class SimulatedBinaryCrossover(BaseCrossover):
135
142
 
136
143
  if to_mate is None:
137
144
  shuffled_ids = list(range(pop_size))
138
- shuffle(shuffled_ids)
145
+ self.rng.shuffle(shuffled_ids)
139
146
  else:
140
147
  shuffled_ids = to_mate
141
148
  mating_pop = parent_decvars[shuffled_ids]
@@ -216,16 +223,17 @@ class SimulatedBinaryCrossover(BaseCrossover):
216
223
  class SinglePointBinaryCrossover(BaseCrossover):
217
224
  """A class that defines the single point binary crossover operation."""
218
225
 
219
- def __init__(self, *, problem: Problem, seed: int, **kwargs):
226
+ def __init__(self, *, problem: Problem, seed: int, verbosity: int, publisher: Publisher):
220
227
  """Initialize the single point binary crossover operator.
221
228
 
222
229
  Args:
223
230
  problem (Problem): the problem object.
224
231
  seed (int): the seed used in the random number generator for choosing the crossover point.
225
- kwargs: Additional keyword arguments. These are passed to the Subscriber class. At the very least, the
226
- publisher must be passed. See the Subscriber class for more information.
232
+ verbosity (int): the verbosity level of the component. The keys in `provided_topics` tell what
233
+ topics are provided by the operator at each verbosity level.
234
+ publisher (Publisher): the publisher to which the operator will publish messages.
227
235
  """
228
- super().__init__(problem, **kwargs)
236
+ super().__init__(problem, verbosity=verbosity, publisher=publisher)
229
237
  self.seed = seed
230
238
 
231
239
  self.parent_population: pl.DataFrame
@@ -234,7 +242,7 @@ class SinglePointBinaryCrossover(BaseCrossover):
234
242
  self.seed = seed
235
243
 
236
244
  @property
237
- def provided_topics(self) -> dict[str, Sequence[CrossoverMessageTopics]]:
245
+ def provided_topics(self) -> dict[int, Sequence[CrossoverMessageTopics]]:
238
246
  """The message topics provided by the single point binary crossover operator."""
239
247
  return {
240
248
  0: [],
@@ -273,7 +281,7 @@ class SinglePointBinaryCrossover(BaseCrossover):
273
281
 
274
282
  if to_mate is None:
275
283
  shuffled_ids = list(range(pop_size))
276
- shuffle(shuffled_ids)
284
+ self.rng.shuffle(shuffled_ids)
277
285
  else:
278
286
  shuffled_ids = copy.copy(to_mate)
279
287
 
@@ -356,16 +364,17 @@ class SinglePointBinaryCrossover(BaseCrossover):
356
364
  class UniformIntegerCrossover(BaseCrossover):
357
365
  """A class that defines the uniform integer crossover operation."""
358
366
 
359
- def __init__(self, *, problem: Problem, seed: int, **kwargs):
367
+ def __init__(self, *, problem: Problem, seed: int, verbosity: int, publisher: Publisher):
360
368
  """Initialize the uniform integer crossover operator.
361
369
 
362
370
  Args:
363
371
  problem (Problem): the problem object.
364
372
  seed (int): the seed used in the random number generator for choosing the crossover point.
365
- kwargs: Additional keyword arguments. These are passed to the Subscriber class. At the very least, the
366
- publisher must be passed. See the Subscriber class for more information.
373
+ verbosity (int): the verbosity level of the component. The keys in `provided_topics` tell what
374
+ topics are provided by the operator at each verbosity level. Recommended to be set to 1.
375
+ publisher (Publisher): the publisher to which the operator will publish messages.
367
376
  """
368
- super().__init__(problem, **kwargs)
377
+ super().__init__(problem, verbosity=verbosity, publisher=publisher)
369
378
  self.seed = seed
370
379
 
371
380
  self.parent_population: pl.DataFrame
@@ -374,7 +383,7 @@ class UniformIntegerCrossover(BaseCrossover):
374
383
  self.seed = seed
375
384
 
376
385
  @property
377
- def provided_topics(self) -> dict[str, Sequence[CrossoverMessageTopics]]:
386
+ def provided_topics(self) -> dict[int, Sequence[CrossoverMessageTopics]]:
378
387
  """The message topics provided by the single point binary crossover operator."""
379
388
  return {
380
389
  0: [],
@@ -413,7 +422,7 @@ class UniformIntegerCrossover(BaseCrossover):
413
422
 
414
423
  if to_mate is None:
415
424
  shuffled_ids = list(range(pop_size))
416
- shuffle(shuffled_ids)
425
+ self.rng.shuffle(shuffled_ids)
417
426
  else:
418
427
  shuffled_ids = copy.copy(to_mate)
419
428
 
@@ -485,16 +494,17 @@ class UniformMixedIntegerCrossover(BaseCrossover):
485
494
  stuff...
486
495
  """
487
496
 
488
- def __init__(self, *, problem: Problem, seed: int, **kwargs):
497
+ def __init__(self, *, problem: Problem, seed: int, verbosity: int, publisher: Publisher):
489
498
  """Initialize the uniform integer crossover operator.
490
499
 
491
500
  Args:
492
501
  problem (Problem): the problem object.
493
502
  seed (int): the seed used in the random number generator for choosing the crossover point.
494
- kwargs: Additional keyword arguments. These are passed to the Subscriber class. At the very least, the
495
- publisher must be passed. See the Subscriber class for more information.
503
+ verbosity (int): the verbosity level of the component. The keys in `provided_topics` tell what
504
+ topics are provided by the operator at each verbosity level. Recommended to be set to 1.
505
+ publisher (Publisher): the publisher to which the operator will publish messages.
496
506
  """
497
- super().__init__(problem, **kwargs)
507
+ super().__init__(problem, verbosity=verbosity, publisher=publisher)
498
508
  self.seed = seed
499
509
 
500
510
  self.parent_population: pl.DataFrame
@@ -503,7 +513,7 @@ class UniformMixedIntegerCrossover(BaseCrossover):
503
513
  self.seed = seed
504
514
 
505
515
  @property
506
- def provided_topics(self) -> dict[str, Sequence[CrossoverMessageTopics]]:
516
+ def provided_topics(self) -> dict[int, Sequence[CrossoverMessageTopics]]:
507
517
  """The message topics provided by the single point binary crossover operator."""
508
518
  return {
509
519
  0: [],
@@ -542,7 +552,7 @@ class UniformMixedIntegerCrossover(BaseCrossover):
542
552
 
543
553
  if to_mate is None:
544
554
  shuffled_ids = list(range(pop_size))
545
- shuffle(shuffled_ids)
555
+ self.rng.shuffle(shuffled_ids)
546
556
  else:
547
557
  shuffled_ids = copy.copy(to_mate)
548
558
 
@@ -634,15 +644,19 @@ class BlendAlphaCrossover(BaseCrossover):
634
644
  self,
635
645
  *,
636
646
  problem: Problem,
637
- seed: int = 0,
647
+ verbosity: int,
648
+ publisher: Publisher,
649
+ seed: int,
638
650
  alpha: float = 0.5,
639
651
  xover_probability: float = 1.0,
640
- **kwargs,
641
652
  ):
642
653
  """Initialize the blend alpha crossover operator.
643
654
 
644
655
  Args:
645
656
  problem (Problem): the problem object.
657
+ verbosity (int): the verbosity level of the component. The keys in `provided_topics` tell what
658
+ topics are provided by the operator at each verbosity level. Recommended to be set to 1.
659
+ publisher (Publisher): the publisher to which the operator will publish messages.
646
660
  seed (int): the seed used in the random number generator for choosing the crossover point.
647
661
  alpha (float, optional): non-negative blending factor 'alpha' that controls the extent to which
648
662
  offspring may be sampled outside the interval defined by each pair of parent
@@ -650,10 +664,8 @@ class BlendAlphaCrossover(BaseCrossover):
650
664
  parents range, larger alpha allows some outliers. Defaults to 0.5.
651
665
  xover_probability (float, optional): the crossover probability parameter.
652
666
  Ranges between 0 and 1.0. Defaults to 1.0.
653
- kwargs: Additional keyword arguments. These are passed to the Subscriber class. At the very least, the
654
- publisher must be passed. See the Subscriber class for more information.
655
667
  """
656
- super().__init__(problem=problem, **kwargs)
668
+ super().__init__(problem=problem, verbosity=verbosity, publisher=publisher)
657
669
 
658
670
  if problem.variable_domain is not VariableDomainTypeEnum.continuous:
659
671
  raise ValueError("BlendAlphaCrossover only works on continuous problems.")
@@ -666,6 +678,7 @@ class BlendAlphaCrossover(BaseCrossover):
666
678
  self.alpha = alpha
667
679
  self.xover_probability = xover_probability
668
680
  self.seed = seed
681
+ self.rng = np.random.default_rng(self.seed)
669
682
 
670
683
  self.parent_population: pl.DataFrame | None = None
671
684
  self.offspring_population: pl.DataFrame | None = None
@@ -695,7 +708,7 @@ class BlendAlphaCrossover(BaseCrossover):
695
708
  parent_decision_vars = population[self.variable_symbols].to_numpy()
696
709
  if to_mate is None:
697
710
  shuffled_ids = list(range(pop_size))
698
- shuffle(shuffled_ids)
711
+ self.rng.shuffle(shuffled_ids)
699
712
  else:
700
713
  shuffled_ids = copy.copy(to_mate)
701
714
 
@@ -710,22 +723,20 @@ class BlendAlphaCrossover(BaseCrossover):
710
723
  parents1 = mating_pop[0::2, :]
711
724
  parents2 = mating_pop[1::2, :]
712
725
 
713
- c_min = np.minimum(parents1, parents2)
714
- c_max = np.maximum(parents1, parents2)
726
+ c_min = np.array(self.lower_bounds)
727
+ c_max = np.array(self.upper_bounds)
715
728
  span = c_max - c_min
716
729
 
717
730
  lower = c_min - self.alpha * span
718
731
  upper = c_max + self.alpha * span
719
732
 
720
- rng = np.random.default_rng(self.seed)
721
-
722
- unoform_1 = rng.random((mating_pop_size // 2, num_var))
723
- uniform_2 = rng.random((mating_pop_size // 2, num_var))
733
+ uniform_1 = self.rng.random((mating_pop_size // 2, num_var))
734
+ uniform_2 = self.rng.random((mating_pop_size // 2, num_var))
724
735
 
725
- offspring1 = lower + unoform_1 * (upper - lower)
736
+ offspring1 = lower + uniform_1 * (upper - lower)
726
737
  offspring2 = lower + uniform_2 * (upper - lower)
727
738
 
728
- mask = rng.random(mating_pop_size // 2) > self.xover_probability
739
+ mask = self.rng.random(mating_pop_size // 2) > self.xover_probability
729
740
  offspring1[mask, :] = parents1[mask, :]
730
741
  offspring2[mask, :] = parents2[mask, :]
731
742
 
@@ -778,3 +789,494 @@ class BlendAlphaCrossover(BaseCrossover):
778
789
  ]
779
790
  )
780
791
  return msgs
792
+
793
+
794
+ class SingleArithmeticCrossover(BaseCrossover):
795
+ """Single Arithmetic Crossover for continuous problems."""
796
+
797
+ @property
798
+ def provided_topics(self) -> dict[int, Sequence[CrossoverMessageTopics]]:
799
+ """The message topics provided by the single arithmetic crossover operator."""
800
+ return {
801
+ 0: [], # No topics for 0
802
+ 1: [
803
+ CrossoverMessageTopics.XOVER_PROBABILITY, # Probability of crossover
804
+ ],
805
+ 2: [
806
+ CrossoverMessageTopics.XOVER_PROBABILITY, # Crossover probability
807
+ CrossoverMessageTopics.PARENTS, # Parents involved in crossover
808
+ CrossoverMessageTopics.OFFSPRINGS, # Offsprings created from crossover
809
+ ],
810
+ }
811
+
812
+ @property
813
+ def interested_topics(self):
814
+ """The message topics that the single arithmetic crossover operator is interested in."""
815
+ return []
816
+
817
+ def __init__(
818
+ self,
819
+ problem: Problem,
820
+ verbosity: int,
821
+ publisher: Publisher,
822
+ seed: int,
823
+ xover_probability: float = 1.0,
824
+ ):
825
+ """Initialize the single arithmetic crossover operator.
826
+
827
+ Args:
828
+ problem (Problem): the problem object.
829
+ verbosity (int): the verbosity level of the component. The keys in `provided_topics` tell what
830
+ topics are provided by the operator at each verbosity level. Recommended to be set to 1.
831
+ publisher (Publisher): the publisher to which the operator will publish messages.
832
+ xover_probability (float): probability of performing crossover.
833
+ seed (int): random seed for reproducibility.
834
+ """
835
+ super().__init__(problem=problem, verbosity=verbosity, publisher=publisher)
836
+
837
+ if not 0 <= xover_probability <= 1:
838
+ raise ValueError("Crossover probability must be in [0, 1].")
839
+
840
+ self.xover_probability = xover_probability
841
+ self.seed = seed
842
+ self.parent_population: pl.DataFrame | None = None
843
+ self.offspring_population: pl.DataFrame | None = None
844
+ self.rng = np.random.default_rng(self.seed)
845
+
846
+ def do(self, *, population: pl.DataFrame, to_mate: list[int] | None = None) -> pl.DataFrame:
847
+ """Perform Single Arithmetic Crossover.
848
+
849
+ Args:
850
+ population (pl.DataFrame): the population to perform the crossover with. The DataFrame
851
+ contains the decision vectors, the target vectors, and the constraint vectors.
852
+ to_mate (list[int] | None): the indices of the population members that should
853
+ participate in the crossover. If `None`, the whole population is subject
854
+ to the crossover.
855
+
856
+ Returns:
857
+ pl.DataFrame: the offspring resulting from the crossover.
858
+ """
859
+ self.parent_population = population
860
+ pop_size = population.shape[0]
861
+ num_vars = len(self.variable_symbols)
862
+
863
+ parents = population[self.variable_symbols].to_numpy()
864
+
865
+ if to_mate is None:
866
+ mating_indices = list(range(pop_size))
867
+ self.rng.shuffle(mating_indices)
868
+ else:
869
+ mating_indices = copy.copy(to_mate)
870
+
871
+ mating_pop_size = len(mating_indices)
872
+ original_pop_size = mating_pop_size
873
+
874
+ if mating_pop_size % 2 == 1:
875
+ mating_indices.append(mating_indices[0])
876
+ mating_pop_size += 1
877
+
878
+ mating_pool = parents[mating_indices, :]
879
+
880
+ parents1 = mating_pool[0::2, :]
881
+ parents2 = mating_pool[1::2, :]
882
+
883
+ mask = self.rng.random(mating_pop_size // 2) <= self.xover_probability
884
+ gene_pos = self.rng.integers(0, num_vars, size=mating_pop_size // 2)
885
+
886
+ # Initialize offspring as exact copies
887
+ offspring1 = parents1.copy()
888
+ offspring2 = parents2.copy()
889
+
890
+ # Apply crossover only for selected pairs
891
+ row_idx = np.arange(len(mask))[mask]
892
+ col_idx = gene_pos[mask]
893
+
894
+ avg = 0.5 * (parents1[row_idx, col_idx] + parents2[row_idx, col_idx])
895
+
896
+ # Use advanced indexing to set arithmetic crossover gene
897
+ offspring1[row_idx, col_idx] = avg
898
+ offspring2[row_idx, col_idx] = avg
899
+
900
+ for i, k in zip(row_idx, col_idx, strict=True):
901
+ offspring1[i, k + 1 :] = parents2[i, k + 1 :]
902
+ offspring2[i, k + 1 :] = parents1[i, k + 1 :]
903
+ offspring1[i, :k] = parents1[i, :k]
904
+ offspring2[i, :k] = parents2[i, :k]
905
+
906
+ offspring = np.vstack((offspring1, offspring2))
907
+ if original_pop_size % 2 == 1:
908
+ offspring = offspring[:-1, :]
909
+
910
+ self.offspring_population = pl.from_numpy(offspring, schema=self.variable_symbols).select(
911
+ pl.all().cast(pl.Float64)
912
+ )
913
+ self.notify()
914
+ return self.offspring_population
915
+
916
+ def update(self, *_, **__):
917
+ """Do nothing."""
918
+
919
+ def state(self) -> Sequence[Message]:
920
+ """Return the state of the single arithmetic crossover operator."""
921
+ if self.parent_population is None:
922
+ return []
923
+
924
+ msgs: list[Message] = []
925
+
926
+ # Messages for crossover probability
927
+ if self.verbosity >= 1:
928
+ msgs.append(
929
+ FloatMessage(
930
+ topic=CrossoverMessageTopics.XOVER_PROBABILITY,
931
+ source=self.__class__.__name__,
932
+ value=self.xover_probability,
933
+ )
934
+ )
935
+
936
+ # Messages for parents and offspring
937
+ if self.verbosity >= 2: # More detailed info
938
+ msgs.extend(
939
+ [
940
+ PolarsDataFrameMessage(
941
+ topic=CrossoverMessageTopics.PARENTS,
942
+ source=self.__class__.__name__,
943
+ value=self.parent_population,
944
+ ),
945
+ PolarsDataFrameMessage(
946
+ topic=CrossoverMessageTopics.OFFSPRINGS,
947
+ source=self.__class__.__name__,
948
+ value=self.offspring_population,
949
+ ),
950
+ ]
951
+ )
952
+
953
+ return msgs
954
+
955
+
956
+ class LocalCrossover(BaseCrossover):
957
+ """Local Crossover for continuous problems."""
958
+
959
+ @property
960
+ def provided_topics(self) -> dict[int, Sequence[CrossoverMessageTopics]]:
961
+ """The message topics provided by the local crossover operator."""
962
+ return {
963
+ 0: [],
964
+ 1: [
965
+ CrossoverMessageTopics.XOVER_PROBABILITY,
966
+ ],
967
+ 2: [
968
+ CrossoverMessageTopics.XOVER_PROBABILITY,
969
+ CrossoverMessageTopics.PARENTS,
970
+ CrossoverMessageTopics.OFFSPRINGS,
971
+ ],
972
+ }
973
+
974
+ @property
975
+ def interested_topics(self):
976
+ """The message topics that the local crossover operator is interested in."""
977
+ return []
978
+
979
+ def __init__(
980
+ self,
981
+ problem: Problem,
982
+ verbosity: int,
983
+ publisher: Publisher,
984
+ seed: int,
985
+ xover_probability: float = 1.0,
986
+ ):
987
+ """Initialize the local crossover operator.
988
+
989
+ Args:
990
+ problem (Problem): the problem object.
991
+ verbosity (int): the verbosity level of the component. The keys in `provided_topics` tell what
992
+ topics are provided by the operator at each verbosity level. Recommended to be set to 1.
993
+ publisher (Publisher): the publisher to which the operator will publish messages.
994
+ xover_probability (float): probability of performing crossover.
995
+ seed (int): random seed for reproducibility.
996
+ """
997
+ super().__init__(problem=problem, verbosity=verbosity, publisher=publisher)
998
+
999
+ if not 0 <= xover_probability <= 1:
1000
+ raise ValueError("Crossover probability must be in [0, 1].")
1001
+
1002
+ self.xover_probability = xover_probability
1003
+ self.seed = seed
1004
+ self.rng = np.random.default_rng(self.seed)
1005
+ self.parent_population: pl.DataFrame | None = None
1006
+ self.offspring_population: pl.DataFrame | None = None
1007
+
1008
+ def do(self, *, population: pl.DataFrame, to_mate: list[int] | None = None) -> pl.DataFrame:
1009
+ """Perform Local Crossover.
1010
+
1011
+ Args:
1012
+ population (pl.DataFrame): the population to perform the crossover with. The DataFrame
1013
+ contains the decision vectors, the target vectors, and the constraint vectors.
1014
+ to_mate (list[int] | None): the indices of the population members that should
1015
+ participate in the crossover. If `None`, the whole population is subject
1016
+ to the crossover.
1017
+
1018
+ Returns:
1019
+ pl.DataFrame: the offspring resulting from the crossover.
1020
+ """
1021
+ self.parent_population = population
1022
+ pop_size = population.shape[0]
1023
+ num_var = len(self.variable_symbols)
1024
+
1025
+ parent_decision_vars = population[self.variable_symbols].to_numpy()
1026
+
1027
+ if to_mate is None:
1028
+ shuffled_ids = list(range(pop_size))
1029
+ self.rng.shuffle(shuffled_ids)
1030
+ else:
1031
+ shuffled_ids = to_mate.copy()
1032
+
1033
+ mating_pop_size = len(shuffled_ids)
1034
+ if mating_pop_size % 2 == 1:
1035
+ shuffled_ids.append(shuffled_ids[0])
1036
+ mating_pop_size += 1
1037
+
1038
+ mating_pop = parent_decision_vars[shuffled_ids]
1039
+ parents1 = mating_pop[0::2]
1040
+ parents2 = mating_pop[1::2]
1041
+
1042
+ offspring = np.empty((mating_pop_size, num_var))
1043
+
1044
+ for i in range(mating_pop_size // 2):
1045
+ if self.rng.random() < self.xover_probability:
1046
+ alpha = self.rng.random(num_var)
1047
+
1048
+ offspring[2 * i] = alpha * parents1[i] + (1 - alpha) * parents2[i]
1049
+ offspring[2 * i + 1] = (1 - alpha) * parents1[i] + alpha * parents2[i]
1050
+ else:
1051
+ offspring[2 * i] = parents1[i]
1052
+ offspring[2 * i + 1] = parents2[i]
1053
+
1054
+ self.offspring_population = pl.from_numpy(offspring, schema=self.variable_symbols).select(
1055
+ pl.all().cast(pl.Float64)
1056
+ )
1057
+
1058
+ self.notify()
1059
+ return self.offspring_population
1060
+
1061
+ def update(self, *_, **__):
1062
+ """Do nothing."""
1063
+
1064
+ def state(self) -> Sequence[Message]:
1065
+ """Return the state of the local crossover operator."""
1066
+ if self.parent_population is None:
1067
+ return []
1068
+
1069
+ msgs: list[Message] = []
1070
+
1071
+ if self.verbosity >= 1:
1072
+ msgs.append(
1073
+ FloatMessage(
1074
+ topic=CrossoverMessageTopics.XOVER_PROBABILITY,
1075
+ source=self.__class__.__name__,
1076
+ value=self.xover_probability,
1077
+ )
1078
+ )
1079
+ if self.verbosity >= 2:
1080
+ msgs.extend(
1081
+ [
1082
+ PolarsDataFrameMessage(
1083
+ topic=CrossoverMessageTopics.PARENTS,
1084
+ source=self.__class__.__name__,
1085
+ value=self.parent_population,
1086
+ ),
1087
+ PolarsDataFrameMessage(
1088
+ topic=CrossoverMessageTopics.OFFSPRINGS,
1089
+ source=self.__class__.__name__,
1090
+ value=self.offspring_population,
1091
+ ),
1092
+ ]
1093
+ )
1094
+ return msgs
1095
+
1096
+
1097
+ class BoundedExponentialCrossover(BaseCrossover):
1098
+ """Bounded‐exponential (BEX) crossover for continuous problems."""
1099
+
1100
+ @property
1101
+ def provided_topics(self) -> dict[int, Sequence[CrossoverMessageTopics]]:
1102
+ """The message topics provided by the bounded exponential crossover operator."""
1103
+ return {
1104
+ 0: [],
1105
+ 1: [
1106
+ CrossoverMessageTopics.XOVER_PROBABILITY,
1107
+ CrossoverMessageTopics.LAMBDA,
1108
+ ],
1109
+ 2: [
1110
+ CrossoverMessageTopics.XOVER_PROBABILITY,
1111
+ CrossoverMessageTopics.LAMBDA,
1112
+ CrossoverMessageTopics.PARENTS,
1113
+ CrossoverMessageTopics.OFFSPRINGS,
1114
+ ],
1115
+ }
1116
+
1117
+ @property
1118
+ def interested_topics(self):
1119
+ """The message topics provided by the bounded exponential crossover operator."""
1120
+ return []
1121
+
1122
+ def __init__(
1123
+ self,
1124
+ *,
1125
+ problem: Problem,
1126
+ verbosity: int,
1127
+ publisher: Publisher,
1128
+ seed: int,
1129
+ lambda_: float = 1.0,
1130
+ xover_probability: float = 1.0,
1131
+ ):
1132
+ """Initialize the bounded‐exponential crossover operator.
1133
+
1134
+ Args:
1135
+ problem (Problem): the problem object.
1136
+ verbosity (int): the verbosity level of the component. The keys in `provided_topics` tell what
1137
+ topics are provided by the operator at each verbosity level. Recommended to be set to 1.
1138
+ publisher (Publisher): the publisher to which the operator will publish messages.
1139
+ seed (int): random seed for the internal generator.
1140
+ lambda_ (float, optional): positive scale λ for the exponential distribution.
1141
+ Defaults to 1.0.
1142
+ xover_probability (float, optional): probability of applying crossover
1143
+ to each pair. Defaults to 1.0.
1144
+ """
1145
+ super().__init__(problem=problem, verbosity=verbosity, publisher=publisher)
1146
+
1147
+ if problem.variable_domain is not VariableDomainTypeEnum.continuous:
1148
+ raise ValueError("BoundedExponentialCrossover only works on continuous problems.")
1149
+ if lambda_ <= 0:
1150
+ raise ValueError("lambda_ must be positive.")
1151
+ if not 0 <= xover_probability <= 1:
1152
+ raise ValueError("xover_probability must be in [0,1].")
1153
+
1154
+ self.lambda_ = lambda_
1155
+ self.xover_probability = xover_probability
1156
+ self.seed = seed
1157
+ self.rng = np.random.default_rng(self.seed)
1158
+
1159
+ self.parent_population: pl.DataFrame | None = None
1160
+ self.offspring_population: pl.DataFrame | None = None
1161
+
1162
+ def do(
1163
+ self,
1164
+ *,
1165
+ population: pl.DataFrame,
1166
+ to_mate: list[int] | None = None,
1167
+ ) -> pl.DataFrame:
1168
+ """Perform bounded‐exponential crossover.
1169
+
1170
+ Args:
1171
+ population (pl.DataFrame): the population to perform the crossover with. The DataFrame
1172
+ contains the decision vectors, the target vectors, and the constraint vectors.
1173
+ to_mate (list[int] | None): the indices of the population members that should
1174
+ participate in the crossover. If `None`, the whole population is subject
1175
+ to the crossover.
1176
+
1177
+ Returns:
1178
+ pl.DataFrame: the offspring resulting from the crossover.
1179
+ """
1180
+ self.parent_population = population
1181
+ pop_size = population.shape[0]
1182
+ num_var = len(self.variable_symbols)
1183
+
1184
+ parent_decision_vars = population[self.variable_symbols].to_numpy()
1185
+ if to_mate is None:
1186
+ shuffled_ids = list(range(pop_size))
1187
+ self.rng.shuffle(shuffled_ids)
1188
+ else:
1189
+ shuffled_ids = copy.copy(to_mate)
1190
+
1191
+ mating_pop_size = len(shuffled_ids)
1192
+ original_pop_size = mating_pop_size
1193
+ if mating_pop_size % 2 == 1:
1194
+ shuffled_ids.append(shuffled_ids[0])
1195
+ mating_pop_size += 1
1196
+
1197
+ mating_pop = parent_decision_vars[shuffled_ids]
1198
+
1199
+ parents1 = mating_pop[0::2, :]
1200
+ parents2 = mating_pop[1::2, :]
1201
+
1202
+ x_lower = np.array(self.lower_bounds)
1203
+ x_upper = np.array(self.upper_bounds)
1204
+ span = parents2 - parents1 # y_i - x_1
1205
+
1206
+ u_i = self.rng.random((mating_pop_size // 2, num_var)) # random integers
1207
+ r_i = self.rng.random((mating_pop_size // 2, num_var))
1208
+
1209
+ exp_lower_1 = np.exp((x_lower - parents1) / (self.lambda_ * span))
1210
+ exp_upper_1 = np.exp((parents1 - x_upper) / (self.lambda_ * span))
1211
+
1212
+ exp_lower_2 = np.exp((x_lower - parents2) / (self.lambda_ * span))
1213
+ exp_upper_2 = np.exp((parents2 - x_upper) / (self.lambda_ * span))
1214
+
1215
+ beta_1 = np.where(
1216
+ r_i <= 0.5,
1217
+ self.lambda_ * np.log(exp_lower_1 + u_i * (1 - exp_lower_1)),
1218
+ -self.lambda_ * np.log(1 - u_i * (1 - exp_upper_1)),
1219
+ )
1220
+
1221
+ beta_2 = np.where(
1222
+ r_i <= 0.5,
1223
+ self.lambda_ * np.log(exp_lower_2 + u_i * (1 - exp_lower_2)),
1224
+ -self.lambda_ * np.log(1 - u_i * (1 - exp_upper_2)),
1225
+ )
1226
+
1227
+ offspring1 = parents1 + beta_1 * span
1228
+ offspring2 = parents2 + beta_2 * span
1229
+
1230
+ mask = self.rng.random(mating_pop_size // 2) > self.xover_probability
1231
+ offspring1[mask, :] = parents1[mask, :]
1232
+ offspring2[mask, :] = parents2[mask, :]
1233
+
1234
+ children = np.vstack((offspring1, offspring2))
1235
+ if original_pop_size % 2 == 1:
1236
+ children = children[:-1, :]
1237
+
1238
+ self.offspring_population = pl.from_numpy(children, schema=self.variable_symbols).select(
1239
+ pl.all().cast(pl.Float64)
1240
+ )
1241
+ self.notify()
1242
+ return self.offspring_population
1243
+
1244
+ def update(self, *_, **__):
1245
+ """Do nothing."""
1246
+
1247
+ def state(self) -> Sequence[Message]:
1248
+ """Return the state of the crossover operator."""
1249
+ if getattr(self, "parent_population", None) is None:
1250
+ return []
1251
+ msgs: list[Message] = []
1252
+ if self.verbosity >= 1:
1253
+ msgs.append(
1254
+ FloatMessage(
1255
+ topic=CrossoverMessageTopics.XOVER_PROBABILITY,
1256
+ source=self.__class__.__name__,
1257
+ value=self.xover_probability,
1258
+ )
1259
+ )
1260
+ msgs.append(
1261
+ FloatMessage(
1262
+ topic=CrossoverMessageTopics.LAMBDA,
1263
+ source=self.__class__.__name__,
1264
+ value=self.lambda_,
1265
+ )
1266
+ )
1267
+ if self.verbosity >= 2:
1268
+ msgs.extend(
1269
+ [
1270
+ PolarsDataFrameMessage(
1271
+ topic=CrossoverMessageTopics.PARENTS,
1272
+ source=self.__class__.__name__,
1273
+ value=self.parent_population,
1274
+ ),
1275
+ PolarsDataFrameMessage(
1276
+ topic=CrossoverMessageTopics.OFFSPRINGS,
1277
+ source=self.__class__.__name__,
1278
+ value=self.offspring_population,
1279
+ ),
1280
+ ]
1281
+ )
1282
+ return msgs