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
@@ -1,20 +1,29 @@
1
1
  """Tests related to the SQLModels."""
2
2
 
3
+ import numpy as np
3
4
  from sqlmodel import Session, select
4
5
 
5
6
  from desdeo.api.models import (
6
- ArchiveEntryBase,
7
- ArchiveEntryDB,
8
7
  Bounds,
9
8
  ConstantDB,
10
9
  ConstraintDB,
11
10
  DiscreteRepresentationDB,
11
+ ENautilusState,
12
12
  ExtraFunctionDB,
13
+ ForestProblemMetaData,
14
+ Group,
15
+ GroupIteration,
13
16
  InteractiveSessionDB,
17
+ NIMBUSClassificationState,
18
+ NIMBUSFinalState,
19
+ NIMBUSInitializationState,
20
+ NIMBUSSaveState,
14
21
  ObjectiveDB,
15
22
  PreferenceDB,
16
23
  ProblemDB,
24
+ ProblemMetaDataDB,
17
25
  ReferencePoint,
26
+ RepresentativeNonDominatedSolutions,
18
27
  RPMState,
19
28
  ScalarizationFunctionDB,
20
29
  SimulatorDB,
@@ -22,9 +31,13 @@ from desdeo.api.models import (
22
31
  TensorConstantDB,
23
32
  TensorVariableDB,
24
33
  User,
34
+ UserSavedSolutionDB,
25
35
  VariableDB,
26
36
  )
27
- from desdeo.mcdm import rpm_solve_solutions
37
+ from desdeo.api.models.gdm.gnimbus import (
38
+ OptimizationPreference,
39
+ )
40
+ from desdeo.mcdm import enautilus_step, generate_starting_point, rpm_solve_solutions, solve_sub_problems
28
41
  from desdeo.problem.schema import (
29
42
  Constant,
30
43
  Constraint,
@@ -46,6 +59,7 @@ from desdeo.problem.testproblems import (
46
59
  dtlz2,
47
60
  momip_ti2,
48
61
  momip_ti7,
62
+ multi_valued_constraint_problem,
49
63
  nimbus_test_problem,
50
64
  pareto_navigator_test_problem,
51
65
  re21,
@@ -63,7 +77,7 @@ from desdeo.problem.testproblems import (
63
77
  spanish_sustainability_problem,
64
78
  zdt1,
65
79
  )
66
- from desdeo.tools import available_solvers
80
+ from desdeo.tools import PyomoBonminSolver, available_solvers
67
81
 
68
82
 
69
83
  def compare_models(
@@ -135,7 +149,7 @@ def test_tensor_constant(session_and_user: dict[str, Session | list[User]]):
135
149
  # check that original added TensorConstant and fetched match
136
150
  assert db_tensor == from_db_tensor
137
151
 
138
- from_db_tensor_dump = from_db_tensor.model_dump()
152
+ from_db_tensor_dump = from_db_tensor.model_dump(exclude={"id", "problem_id"})
139
153
  t_tensor_validated = TensorConstant.model_validate(from_db_tensor_dump)
140
154
 
141
155
  assert t_tensor_validated == t_tensor
@@ -159,7 +173,7 @@ def test_constant(session_and_user: dict[str, Session | list[User]]):
159
173
 
160
174
  assert db_constant == from_db_constant
161
175
 
162
- from_db_constant_dump = from_db_constant.model_dump()
176
+ from_db_constant_dump = from_db_constant.model_dump(exclude={"id", "problem_id"})
163
177
  constant_validated = Constant.model_validate(from_db_constant_dump)
164
178
 
165
179
  assert constant_validated == constant
@@ -191,7 +205,7 @@ def test_variable(session_and_user: dict[str, Session | list[User]]):
191
205
 
192
206
  assert db_variable == from_db_variable
193
207
 
194
- from_db_variable_dump = from_db_variable.model_dump()
208
+ from_db_variable_dump = from_db_variable.model_dump(exclude={"id", "problem_id"})
195
209
  variable_validated = Variable.model_validate(from_db_variable_dump)
196
210
 
197
211
  assert variable_validated == variable
@@ -224,7 +238,7 @@ def test_tensor_variable(session_and_user: dict[str, Session | list[User]]):
224
238
 
225
239
  assert db_t_variable == from_db_t_variable
226
240
 
227
- from_db_t_variable_dump = from_db_t_variable.model_dump()
241
+ from_db_t_variable_dump = from_db_t_variable.model_dump(exclude={"id", "problem_id"})
228
242
  t_variable_validated = TensorVariable.model_validate(from_db_t_variable_dump)
229
243
 
230
244
  assert t_variable_validated == t_variable
@@ -264,7 +278,7 @@ def test_objective(session_and_user: dict[str, Session | list[User]]):
264
278
 
265
279
  assert db_objective == from_db_objective
266
280
 
267
- from_db_objective_dump = from_db_objective.model_dump()
281
+ from_db_objective_dump = from_db_objective.model_dump(exclude={"id", "problem_id"})
268
282
  objective_validated = Objective.model_validate(from_db_objective_dump)
269
283
 
270
284
  assert objective_validated == objective
@@ -300,7 +314,7 @@ def test_constraint(session_and_user: dict[str, Session | list[User]]):
300
314
 
301
315
  assert db_constraint == from_db_constraint
302
316
 
303
- from_db_constraint_dump = from_db_constraint.model_dump()
317
+ from_db_constraint_dump = from_db_constraint.model_dump(exclude={"id", "problem_id"})
304
318
  constraint_validated = Constraint.model_validate(from_db_constraint_dump)
305
319
 
306
320
  assert constraint_validated == constraint
@@ -333,7 +347,7 @@ def test_scalarization_function(session_and_user: dict[str, Session | list[User]
333
347
 
334
348
  assert db_scalarization == from_db_scalarization
335
349
 
336
- from_db_scalarization_dump = from_db_scalarization.model_dump()
350
+ from_db_scalarization_dump = from_db_scalarization.model_dump(exclude={"id", "problem_id"})
337
351
  scalarization_validated = ScalarizationFunction.model_validate(from_db_scalarization_dump)
338
352
 
339
353
  assert scalarization_validated == scalarization
@@ -366,7 +380,7 @@ def test_extra_function(session_and_user: dict[str, Session | list[User]]):
366
380
 
367
381
  assert db_extra == from_db_extra
368
382
 
369
- from_db_extra_dump = from_db_extra.model_dump()
383
+ from_db_extra_dump = from_db_extra.model_dump(exclude={"id", "problem_id"})
370
384
  extra_validated = ExtraFunction.model_validate(from_db_extra_dump)
371
385
 
372
386
  assert extra_validated == extra
@@ -395,7 +409,7 @@ def test_discrete_representation(session_and_user: dict[str, Session | list[User
395
409
 
396
410
  assert db_discrete == from_db_discrete
397
411
 
398
- from_db_discrete_dump = from_db_discrete.model_dump()
412
+ from_db_discrete_dump = from_db_discrete.model_dump(exclude={"id", "problem_id"})
399
413
  discrete_validated = DiscreteRepresentation.model_validate(from_db_discrete_dump)
400
414
 
401
415
  assert discrete_validated == discrete
@@ -425,7 +439,7 @@ def test_simulator(session_and_user: dict[str, Session | list[User]]):
425
439
 
426
440
  assert db_simulator == from_db_simulator
427
441
 
428
- from_db_simulator_dump = from_db_simulator.model_dump()
442
+ from_db_simulator_dump = from_db_simulator.model_dump(exclude={"id", "problem_id"})
429
443
  simulator_validated = Simulator.model_validate(from_db_simulator_dump)
430
444
 
431
445
  assert simulator_validated == simulator
@@ -464,6 +478,7 @@ def test_from_problem_to_d_and_back(session_and_user: dict[str, Session | list[U
464
478
  spanish_sustainability_problem(),
465
479
  zdt1(10),
466
480
  dtlz2(5, 3),
481
+ multi_valued_constraint_problem(),
467
482
  momip_ti2(),
468
483
  momip_ti7(),
469
484
  nimbus_test_problem(),
@@ -493,49 +508,84 @@ def test_from_problem_to_d_and_back(session_and_user: dict[str, Session | list[U
493
508
  assert compare_models(problem, problem_db)
494
509
 
495
510
 
496
- def test_archive_entry(session_and_user: dict[str, Session | list[User]]):
497
- """Test that the archive works as intended."""
511
+ def test_user_save_solutions(session_and_user: dict[str, Session | list[User]]):
512
+ """Test that user_save_solutions correctly saves solutions to the usersavedsolutiondb in the database."""
498
513
  session = session_and_user["session"]
499
514
  user = session_and_user["user"]
500
515
 
501
- problem = dtlz2(n_variables=5, n_objectives=3)
502
- problem_db = ProblemDB.from_problem(problem, user)
503
-
504
- session.add(problem_db)
505
- session.commit()
506
- session.refresh(problem_db)
516
+ # Create test solutions with proper dictionary values
517
+ objective_values = {"f_1": 1.2, "f_2": 0.9}
518
+ variable_values = {"x_1": 5.2, "x_2": 8.0, "x_3": -4.2}
519
+
520
+ user_id = user.id
521
+ problem_id = 1
522
+
523
+ test_solutions = [
524
+ UserSavedSolutionDB(
525
+ name="Solution 1",
526
+ objective_values=objective_values,
527
+ variable_values=variable_values,
528
+ user_id=user_id,
529
+ problem_id=problem_id,
530
+ origin_state_id=1,
531
+ ),
532
+ UserSavedSolutionDB(
533
+ name="Solution 2",
534
+ objective_values=objective_values,
535
+ variable_values=variable_values,
536
+ solution_index=2,
537
+ user_id=user_id,
538
+ problem_id=problem_id,
539
+ origin_state_id=2,
540
+ ),
541
+ ]
507
542
 
508
- variable_values = {"x_1": 0.3, "x_2": 0.8, "x_3": 0.1, "x_4": 0.6, "x_5": 0.9}
509
- objective_values = {"f_1": 1.2, "f_2": 0.9, "f_3": 1.5}
510
- constraint_values = {"g_1": 0.1}
511
- extra_func_values = {"extra_1": 5000, "extra_2": 600000}
543
+ num_test_solutions = len(test_solutions)
512
544
 
513
- archive_entry = ArchiveEntryBase(variable_values=variable_values, objective_values=objective_values)
545
+ # Create NIMBUSSaveState
546
+ save_state = NIMBUSSaveState(solutions=test_solutions)
514
547
 
515
- archive_entry_db = ArchiveEntryDB.model_validate(
516
- archive_entry,
517
- update={
518
- "user_id": user.id,
519
- "problem_id": problem_db.id,
520
- "constraint_values": constraint_values,
521
- "extra_func_values": extra_func_values,
522
- },
523
- )
548
+ # Create StateDB with NIMBUSSaveState
549
+ state_db = StateDB.create(session, problem_id=problem_id, state=save_state)
524
550
 
525
- session.add(archive_entry_db)
551
+ session.add(state_db)
526
552
  session.commit()
527
- session.refresh(archive_entry_db)
528
-
529
- from_db = session.get(ArchiveEntryDB, archive_entry_db.id)
530
-
531
- assert from_db.user_id == user.id
532
- assert from_db.problem_id == problem_db.id
533
- assert from_db == user.archive[0]
534
- assert compare_models(from_db.problem, problem_db)
535
- assert from_db.variable_values == variable_values
536
- assert from_db.objective_values == objective_values
537
- assert from_db.constraint_values == constraint_values
538
- assert from_db.extra_func_values == extra_func_values
553
+ session.refresh(state_db)
554
+
555
+ # Verify the solutions were saved
556
+ saved_solutions = session.exec(select(UserSavedSolutionDB)).all()
557
+ assert len(saved_solutions) == num_test_solutions
558
+
559
+ # Verify the content of the first solution
560
+ first_solution = saved_solutions[0]
561
+ assert first_solution.name == "Solution 1"
562
+ assert first_solution.objective_values == objective_values
563
+ assert first_solution.variable_values == variable_values
564
+ assert first_solution.origin_state_id == 1
565
+ assert first_solution.solution_index is None
566
+ assert first_solution.user_id == user.id
567
+ assert first_solution.problem_id == problem_id
568
+
569
+ # Verify the content of the second solution
570
+ second_solution = saved_solutions[1]
571
+ assert second_solution.name == "Solution 2"
572
+ assert second_solution.objective_values == objective_values
573
+ assert second_solution.variable_values == variable_values
574
+ assert second_solution.origin_state_id == 2
575
+ assert second_solution.solution_index == 2
576
+ assert second_solution.user_id == user.id
577
+ assert second_solution.problem_id == problem_id
578
+
579
+ # Verify state relationship
580
+ saved_state = session.exec(select(StateDB).where(StateDB.id == state_db.id)).first()
581
+ assert isinstance(saved_state.state, NIMBUSSaveState)
582
+ assert len(saved_state.state.solutions) == num_test_solutions
583
+
584
+ # Check that relationships are formed
585
+ session.refresh(user)
586
+
587
+ assert len(user.archive) == len(test_solutions)
588
+ assert len(session.get(ProblemDB, problem_id).solutions) == len(test_solutions)
539
589
 
540
590
 
541
591
  def test_preference_models(session_and_user: dict[str, Session | list[User]]):
@@ -579,9 +629,6 @@ def test_preference_models(session_and_user: dict[str, Session | list[User]]):
579
629
  assert from_db_bounds.user == user
580
630
  assert from_db_ref_point.user == user
581
631
 
582
- assert from_db_bounds.solutions == []
583
- assert from_db_ref_point.solutions == []
584
-
585
632
 
586
633
  def test_rpm_state(session_and_user: dict[str, Session | list[User]]):
587
634
  """Test the RPM state that it works correctly."""
@@ -609,30 +656,26 @@ def test_rpm_state(session_and_user: dict[str, Session | list[User]]):
609
656
  problem,
610
657
  asp_levels_1,
611
658
  scalarization_options=scalarization_options,
612
- solver=available_solvers[solver],
659
+ solver=available_solvers[solver]["constructor"],
613
660
  solver_options=solver_options,
614
661
  )
615
662
 
616
663
  # create preferences
617
664
 
618
665
  rp_1 = ReferencePoint(aspiration_levels=asp_levels_1)
619
- preferences = PreferenceDB(user_id=user.id, problem_id=problem_db.id, preference=rp_1)
620
-
621
- session.add(preferences)
622
- session.commit()
623
- session.refresh(preferences)
624
666
 
625
667
  # create state
626
668
 
627
669
  rpm_state = RPMState(
670
+ preferences=rp_1,
628
671
  scalarization_options=scalarization_options,
629
672
  solver=solver,
630
673
  solver_options=solver_options,
631
674
  solver_results=results,
632
675
  )
633
676
 
634
- state_1 = StateDB(
635
- problem_id=problem_db.id, preference_id=preferences.id, session_id=isession.id, parent_id=None, state=rpm_state
677
+ state_1 = StateDB.create(
678
+ database_session=session, problem_id=problem_db.id, session_id=isession.id, state=rpm_state
636
679
  )
637
680
 
638
681
  session.add(state_1)
@@ -641,8 +684,6 @@ def test_rpm_state(session_and_user: dict[str, Session | list[User]]):
641
684
 
642
685
  asp_levels_2 = {"f_1": 0.6, "f_2": 0.4, "f_3": 0.5}
643
686
 
644
- problem = Problem.from_problemdb(problem_db)
645
-
646
687
  scalarization_options = None
647
688
  solver = "pyomo_bonmin"
648
689
  solver_options = None
@@ -651,31 +692,36 @@ def test_rpm_state(session_and_user: dict[str, Session | list[User]]):
651
692
  problem,
652
693
  asp_levels_2,
653
694
  scalarization_options=scalarization_options,
654
- solver=available_solvers[solver],
695
+ solver=available_solvers[solver]["constructor"],
696
+ solver_options=solver_options,
697
+ )
698
+
699
+ # create state
700
+
701
+ rpm_state = RPMState(
702
+ scalarization_options=scalarization_options,
703
+ solver=solver,
655
704
  solver_options=solver_options,
705
+ solver_results=results,
656
706
  )
657
707
 
658
708
  # create preferences
659
709
 
660
710
  rp_2 = ReferencePoint(aspiration_levels=asp_levels_2)
661
- preferences = PreferenceDB(user_id=user.id, problem_id=problem_db.id, preference=rp_2)
662
-
663
- session.add(preferences)
664
- session.commit()
665
- session.refresh(preferences)
666
711
 
667
712
  # create state
668
713
 
669
714
  rpm_state = RPMState(
715
+ preferences=rp_2,
670
716
  scalarization_options=scalarization_options,
671
717
  solver=solver,
672
718
  solver_options=solver_options,
673
719
  solver_results=results,
674
720
  )
675
721
 
676
- state_2 = StateDB(
722
+ state_2 = StateDB.create(
723
+ database_session=session,
677
724
  problem_id=problem_db.id,
678
- preference_id=preferences.id,
679
725
  session_id=isession.id,
680
726
  parent_id=state_1.id,
681
727
  state=rpm_state,
@@ -690,8 +736,8 @@ def test_rpm_state(session_and_user: dict[str, Session | list[User]]):
690
736
  assert len(state_1.children) == 1
691
737
  assert state_1.children[0] == state_2
692
738
 
693
- assert state_1.preference.preference == rp_1
694
- assert state_2.preference.preference == rp_2
739
+ assert state_1.state.preferences == rp_1
740
+ assert state_2.state.preferences == rp_2
695
741
 
696
742
  assert state_2.problem == problem_db
697
743
  assert state_2.session.user == user
@@ -699,3 +745,435 @@ def test_rpm_state(session_and_user: dict[str, Session | list[User]]):
699
745
  assert state_2.children == []
700
746
  assert state_2.parent.problem == problem_db
701
747
  assert state_2.parent.session.user == user
748
+
749
+
750
+ def test_problem_metadata(session_and_user: dict[str, Session | list[User]]):
751
+ """Test that the problem metadata can be put into database and brought back."""
752
+ session = session_and_user["session"]
753
+ user = session_and_user["user"]
754
+
755
+ # Just some test problem to attach the metadata to
756
+ problem = ProblemDB.from_problem(dtlz2(5, 3), user=user)
757
+
758
+ session.add(problem)
759
+ session.commit()
760
+ session.refresh(problem)
761
+
762
+ representative_name = "Test solutions"
763
+ representative_description = "These solutions are used for testing"
764
+ representative_variables = {"x_1": [1.1, 2.2, 3.3], "x_2": [-1.1, -2.2, -3.3]}
765
+ representative_objectives = {"f_1": [0.1, 0.5, 0.9], "f_2": [-0.1, 0.2, 199.2], "f_1_min": [], "f_2_min": []}
766
+ solution_data = representative_variables | representative_objectives
767
+ representative_ideal = {"f_1": 0.1, "f_2": -0.1}
768
+ representative_nadir = {"f_1": 0.9, "f_2": 199.2}
769
+
770
+ metadata = ProblemMetaDataDB(
771
+ problem_id=problem.id,
772
+ )
773
+
774
+ session.add(metadata)
775
+ session.commit()
776
+ session.refresh(metadata)
777
+
778
+ forest_metadata = ForestProblemMetaData(
779
+ metadata_id=metadata.id,
780
+ map_json="type: string",
781
+ schedule_dict={"type": "dict"},
782
+ years=["type:", "list", "of", "strings"],
783
+ stand_id_field="type: string",
784
+ )
785
+
786
+ repr_metadata = RepresentativeNonDominatedSolutions(
787
+ metadata_id=metadata.id,
788
+ name=representative_name,
789
+ description=representative_description,
790
+ solution_data=solution_data,
791
+ ideal=representative_ideal,
792
+ nadir=representative_nadir,
793
+ )
794
+
795
+ session.add(forest_metadata)
796
+ session.add(repr_metadata)
797
+ session.commit()
798
+ session.refresh(forest_metadata)
799
+ session.refresh(repr_metadata)
800
+
801
+ statement = select(ProblemMetaDataDB).where(ProblemMetaDataDB.problem_id == problem.id)
802
+ from_db_metadata = session.exec(statement).first()
803
+
804
+ assert from_db_metadata.id is not None
805
+ assert from_db_metadata.problem_id == problem.id
806
+
807
+ metadata_forest = from_db_metadata.forest_metadata[0]
808
+
809
+ assert isinstance(metadata_forest, ForestProblemMetaData)
810
+ assert metadata_forest.map_json == "type: string"
811
+ assert metadata_forest.schedule_dict == {"type": "dict"}
812
+ assert metadata_forest.years == ["type:", "list", "of", "strings"]
813
+ assert metadata_forest.stand_id_field == "type: string"
814
+
815
+ metadata_representative = from_db_metadata.representative_nd_metadata[0]
816
+
817
+ assert isinstance(metadata_representative, RepresentativeNonDominatedSolutions)
818
+ assert metadata_representative.name == representative_name
819
+ assert metadata_representative.solution_data == solution_data
820
+ assert metadata_representative.ideal == representative_ideal
821
+ assert metadata_representative.nadir == representative_nadir
822
+
823
+ assert problem.problem_metadata == from_db_metadata
824
+
825
+
826
+ def test_group(session_and_user: dict[str, Session | list[User]]):
827
+ """なに?!ちょっとまって。。。ドクメンタはどこですか???"""
828
+ session: Session = session_and_user["session"]
829
+ user: User = session_and_user["user"]
830
+
831
+ group = Group(
832
+ name="TestGroup",
833
+ owner_id=user.id,
834
+ user_ids=[user.id],
835
+ problem_id=1,
836
+ )
837
+
838
+ session.add(group)
839
+ session.commit()
840
+ session.refresh(group)
841
+
842
+ assert group.id == 1
843
+ assert group.user_ids[0] == user.id
844
+ assert group.name == "TestGroup"
845
+
846
+
847
+ def test_gnimbus_datas(session_and_user: dict[str, Session | list[User]]):
848
+ session: Session = session_and_user["session"]
849
+ user: User = session_and_user["user"]
850
+
851
+ group = Group(
852
+ name="TestGroup",
853
+ owner_id=user.id,
854
+ user_ids=[user.id],
855
+ problem_id=1,
856
+ )
857
+
858
+ session.add(group)
859
+ session.commit()
860
+ session.refresh(group)
861
+
862
+ giter = GroupIteration(
863
+ problem_id=1,
864
+ group_id=group.id,
865
+ info_container=OptimizationPreference(
866
+ set_preferences={},
867
+ ),
868
+ notified={},
869
+ parent_id=None,
870
+ parent=None,
871
+ child=None,
872
+ )
873
+ session.add(giter)
874
+ session.commit()
875
+ session.refresh(giter)
876
+
877
+ assert type(giter.info_container) is OptimizationPreference
878
+ assert giter.problem_id == 1
879
+ assert giter.group_id == group.id
880
+
881
+
882
+ def test_enautilus_state(session_and_user: dict[str, Session | list[User]]):
883
+ """Test the E-NAUTILUS state that it works correctly."""
884
+ session = session_and_user["session"]
885
+ user = session_and_user["user"]
886
+
887
+ # create interactive session
888
+ isession = InteractiveSessionDB(user_id=user.id)
889
+
890
+ session.add(isession)
891
+ session.commit()
892
+ session.refresh(isession)
893
+
894
+ # use dummy problem
895
+ dummy_problem = Problem(
896
+ name="Synthetic-4D",
897
+ description="Unit-test Problem for E-NAUTILUS",
898
+ variables=[Variable(name="x", symbol="x", variable_type=VariableTypeEnum.real)],
899
+ objectives=[
900
+ Objective(name="f1", symbol="f1", maximize=False),
901
+ Objective(name="f2", symbol="f2", maximize=True),
902
+ Objective(name="f3", symbol="f3", maximize=False),
903
+ Objective(name="f4", symbol="f4", maximize=True),
904
+ ],
905
+ )
906
+
907
+ x = np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0])
908
+ f1 = np.array([0.40, 0.60, 0.50, 0.70, 0.45, 0.55, 0.65, 0.48])
909
+ f2 = np.array([4.00, 3.80, 4.10, 3.70, 4.05, 3.90, 3.60, 4.20])
910
+ f3 = np.array([1.00, 1.30, 1.10, 1.40, 1.05, 1.20, 1.35, 1.15])
911
+ f4 = np.array([2.50, 2.30, 2.60, 2.20, 2.55, 2.40, 2.10, 2.65])
912
+
913
+ nadir = {"f1": np.max(f1), "f2": np.min(f2), "f3": np.max(f3), "f4": np.min(f4)}
914
+ ideal = {"f1": np.min(f1), "f2": np.max(f2), "f3": np.min(f3), "f4": np.max(f4)}
915
+
916
+ non_dom_data = {
917
+ "x": x.tolist(),
918
+ "f1": f1.tolist(),
919
+ "f1_min": f1.tolist(),
920
+ "f2": f2.tolist(),
921
+ "f2_min": (-f2).tolist(),
922
+ "f3": f3.tolist(),
923
+ "f3_min": f3.tolist(),
924
+ "f4": f4.tolist(),
925
+ "f4_min": (-f4).tolist(),
926
+ }
927
+
928
+ # add problem to DB and refresh it
929
+ problemdb = ProblemDB.from_problem(dummy_problem, user)
930
+
931
+ session.add(problemdb)
932
+ session.commit()
933
+ session.refresh(problemdb)
934
+
935
+ metadata = ProblemMetaDataDB(
936
+ problem_id=problemdb.id,
937
+ )
938
+
939
+ # add metadata to DB
940
+ session.add(metadata)
941
+ session.commit()
942
+ session.refresh(metadata)
943
+
944
+ reprdata = RepresentativeNonDominatedSolutions(
945
+ metadata_id=metadata.id,
946
+ name="Dummy data",
947
+ description="Dummy data for a problem",
948
+ solution_data=non_dom_data,
949
+ ideal=ideal,
950
+ nadir=nadir,
951
+ )
952
+
953
+ # add reprdata to DB
954
+ session.add(reprdata)
955
+ session.commit()
956
+ session.refresh(reprdata)
957
+
958
+ # test the nautilus step state
959
+ # first iteration
960
+ selected_point = nadir
961
+ reachable_indices = list(range(len(x))) # entire front reachable
962
+
963
+ total_iters = 2 # DM first thinks 2 iterations are enough
964
+ n_points = 3 # DM wants to see 3 points at first
965
+ current = 0
966
+
967
+ # First iteration
968
+ res = enautilus_step(
969
+ problem=dummy_problem,
970
+ non_dominated_points=non_dom_data,
971
+ current_iteration=current,
972
+ iterations_left=total_iters - current,
973
+ selected_point=selected_point,
974
+ reachable_point_indices=reachable_indices,
975
+ number_of_intermediate_points=n_points,
976
+ )
977
+
978
+ enautilus_state = ENautilusState(
979
+ non_dominated_solutions_id=reprdata.id,
980
+ current_iteration=current,
981
+ iterations_left=total_iters - current,
982
+ selected_point=selected_point,
983
+ reachable_point_indices=reachable_indices,
984
+ number_of_intermediate_points=n_points,
985
+ enautilus_results=res,
986
+ )
987
+
988
+ state_1 = StateDB.create(
989
+ database_session=session,
990
+ problem_id=problemdb.id,
991
+ session_id=isession.id,
992
+ parent_id=None,
993
+ state=enautilus_state,
994
+ )
995
+
996
+ session.add(state_1)
997
+ session.commit()
998
+ session.refresh(state_1)
999
+
1000
+ # Second iteration
1001
+ res_2 = enautilus_step(
1002
+ problem=dummy_problem,
1003
+ non_dominated_points=non_dom_data,
1004
+ current_iteration=res.current_iteration,
1005
+ iterations_left=res.iterations_left,
1006
+ selected_point=res.intermediate_points[0],
1007
+ reachable_point_indices=res.reachable_point_indices[0],
1008
+ number_of_intermediate_points=n_points,
1009
+ )
1010
+
1011
+ enautilus_state_2 = ENautilusState(
1012
+ non_dominated_solutions_id=reprdata.id,
1013
+ current_iteration=res.current_iteration,
1014
+ iterations_left=res.iterations_left,
1015
+ selected_point=res.intermediate_points[0],
1016
+ reachable_point_indices=res.reachable_point_indices[0],
1017
+ number_of_intermediate_points=n_points,
1018
+ enautilus_results=res_2,
1019
+ )
1020
+
1021
+ state_2 = StateDB.create(
1022
+ database_session=session,
1023
+ problem_id=problemdb.id,
1024
+ session_id=isession.id,
1025
+ parent_id=enautilus_state.id,
1026
+ state=enautilus_state_2,
1027
+ )
1028
+
1029
+ session.add(state_2)
1030
+ session.commit()
1031
+ session.refresh(state_2)
1032
+
1033
+ assert state_1.problem_id == problemdb.id
1034
+ assert state_2.problem_id == problemdb.id
1035
+
1036
+ assert state_1.session_id == isession.id
1037
+ assert state_2.session_id == isession.id
1038
+
1039
+ assert state_1.parent is None
1040
+ assert state_2.parent == state_1
1041
+
1042
+ assert len(state_1.children) == 1
1043
+ assert state_1.children[0] == state_2
1044
+ assert state_2.children == []
1045
+
1046
+
1047
+ def test_nimbus_models(session_and_user: dict[str, Session | list[User]]):
1048
+ """Test that the NIMBUS models are in working order."""
1049
+ session: Session = session_and_user["session"]
1050
+ user: User = session_and_user["user"]
1051
+ problem_db = user.problems[0]
1052
+ problem = Problem.from_problemdb(problem_db)
1053
+
1054
+ isession = InteractiveSessionDB(user_id=user.id)
1055
+
1056
+ session.add(isession)
1057
+ session.commit()
1058
+ session.refresh(isession)
1059
+
1060
+ # 1. Initialize the NIMBUS problem (NIMBUSInitializationState)
1061
+ results_1 = generate_starting_point(
1062
+ problem=problem,
1063
+ )
1064
+ nimbus_init_state = NIMBUSInitializationState(solver_results=results_1)
1065
+
1066
+ state_1 = StateDB.create(
1067
+ database_session=session, problem_id=problem_db.id, session_id=isession.id, state=nimbus_init_state
1068
+ )
1069
+
1070
+ session.add(state_1)
1071
+ session.commit()
1072
+ session.refresh(state_1)
1073
+
1074
+ actual_state_1: NIMBUSInitializationState = state_1.state
1075
+
1076
+ assert type(actual_state_1) is NIMBUSInitializationState
1077
+ assert np.allclose(
1078
+ [x for _, x in actual_state_1.solver_results.optimal_objectives.items()],
1079
+ [x for _, x in results_1.optimal_objectives.items()],
1080
+ 0.001,
1081
+ )
1082
+
1083
+ # 2. Solve sub problems (NIMBUSClassificationState)
1084
+ aspirations = {"f_1": 0.1, "f_2": 0.9, "f_3": 0.6}
1085
+
1086
+ results_2 = solve_sub_problems(
1087
+ problem=problem, current_objectives=results_1.optimal_objectives, reference_point=aspirations, num_desired=4
1088
+ )
1089
+ nimbus_classification_state = NIMBUSClassificationState(
1090
+ preferences=ReferencePoint(aspiration_levels=aspirations),
1091
+ current_objectives=results_1.optimal_objectives,
1092
+ previous_preferences=ReferencePoint(aspiration_levels=aspirations),
1093
+ solver_results=results_2,
1094
+ )
1095
+
1096
+ state_2 = StateDB.create(
1097
+ database_session=session, problem_id=problem_db.id, session_id=isession.id, state=nimbus_classification_state
1098
+ )
1099
+
1100
+ session.add(state_2)
1101
+ session.commit()
1102
+ session.refresh(state_2)
1103
+
1104
+ actual_state_2: NIMBUSClassificationState = state_2.state
1105
+
1106
+ assert type(actual_state_2) is NIMBUSClassificationState
1107
+ assert np.allclose(
1108
+ [x for _, x in actual_state_2.preferences.aspiration_levels.items()], [x for _, x in aspirations.items()], 0.001
1109
+ )
1110
+ assert np.allclose(
1111
+ [x for _, x in actual_state_2.solver_results[0].optimal_objectives.items()],
1112
+ [x for _, x in results_2[0].optimal_objectives.items()],
1113
+ 0.001,
1114
+ )
1115
+
1116
+ # 3. (TODO) Save a found solution (NIMBUSSaveState)
1117
+ # 4. Finalize the NIMBUS process (NIMBUSFinalState)
1118
+ nimbus_final_state = NIMBUSFinalState(
1119
+ solution_origin_state_id=state_2.state.id,
1120
+ solution_result_index=0,
1121
+ solver_results=results_2[0]
1122
+ )
1123
+
1124
+ state_3 = StateDB.create(
1125
+ database_session=session, problem_id=problem_db.id, session_id=isession.id, state=nimbus_final_state
1126
+ )
1127
+
1128
+ session.add(state_3)
1129
+ session.commit()
1130
+ session.refresh(state_3)
1131
+
1132
+ actual_state_3: NIMBUSFinalState = state_3.state
1133
+ assert type(actual_state_3) is NIMBUSFinalState
1134
+ assert np.allclose(
1135
+ [x for _, x in actual_state_3.solver_results.optimal_objectives.items()],
1136
+ [x for _, x in results_2[0].optimal_objectives.items()],
1137
+ 0.01,
1138
+ )
1139
+ assert np.allclose(
1140
+ [x for _, x in actual_state_3.solver_results.optimal_variables.items()],
1141
+ [x for _, x in results_2[0].optimal_variables.items()],
1142
+ 0.01,
1143
+ )
1144
+
1145
+
1146
+ def test_nimbus_initialize_w_multidimensional_constraints(session_and_user: dict[str, Session | list[User]]):
1147
+ """Test that the NIMBUS initialization model works with multidimensional constraints."""
1148
+ session: Session = session_and_user["session"]
1149
+ user: User = session_and_user["user"]
1150
+ problem = multi_valued_constraint_problem()
1151
+
1152
+ problem_db = ProblemDB.from_problem(problem, user)
1153
+ isession = InteractiveSessionDB(user_id=user.id)
1154
+
1155
+ session.add(isession)
1156
+ session.add(problem_db)
1157
+ session.commit()
1158
+ session.refresh(isession)
1159
+ session.refresh(problem_db)
1160
+
1161
+ results_1 = generate_starting_point(problem=problem, solver=PyomoBonminSolver)
1162
+ nimbus_init_state = NIMBUSInitializationState(solver_results=results_1)
1163
+
1164
+ state_1 = StateDB.create(
1165
+ database_session=session, problem_id=problem_db.id, session_id=isession.id, state=nimbus_init_state
1166
+ )
1167
+
1168
+ session.add(state_1)
1169
+ session.commit()
1170
+ session.refresh(state_1)
1171
+
1172
+ actual_state_1: NIMBUSInitializationState = state_1.state
1173
+
1174
+ assert type(actual_state_1) is NIMBUSInitializationState
1175
+ assert np.allclose(
1176
+ [x for _, x in actual_state_1.solver_results.optimal_objectives.items()],
1177
+ [x for _, x in results_1.optimal_objectives.items()],
1178
+ 0.001,
1179
+ )