fuzzy-dl-owl2 1.0.6__py3-none-any.whl → 1.0.8__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 (86) hide show
  1. fuzzy_dl_owl2/fuzzydl/__init__.py +2 -1
  2. fuzzy_dl_owl2/fuzzydl/assertion/assertion.py +1 -0
  3. fuzzy_dl_owl2/fuzzydl/assertion/atomic_assertion.py +2 -0
  4. fuzzy_dl_owl2/fuzzydl/classification_node.py +64 -0
  5. fuzzy_dl_owl2/fuzzydl/concept/__init__.py +2 -0
  6. fuzzy_dl_owl2/fuzzydl/concept/atomic_concept.py +1 -1
  7. fuzzy_dl_owl2/fuzzydl/concept/choquet_integral.py +6 -0
  8. fuzzy_dl_owl2/fuzzydl/concept/concept.py +8 -0
  9. fuzzy_dl_owl2/fuzzydl/concept/concrete/crisp_concrete_concept.py +1 -0
  10. fuzzy_dl_owl2/fuzzydl/concept/concrete/fuzzy_concrete_concept.py +1 -0
  11. fuzzy_dl_owl2/fuzzydl/concept/concrete/fuzzy_number/triangular_fuzzy_number.py +12 -0
  12. fuzzy_dl_owl2/fuzzydl/concept/concrete/left_concrete_concept.py +3 -0
  13. fuzzy_dl_owl2/fuzzydl/concept/concrete/linear_concrete_concept.py +3 -0
  14. fuzzy_dl_owl2/fuzzydl/concept/concrete/modified_concrete_concept.py +4 -0
  15. fuzzy_dl_owl2/fuzzydl/concept/concrete/right_concrete_concept.py +2 -0
  16. fuzzy_dl_owl2/fuzzydl/concept/concrete/trapezoidal_concrete_concept.py +1 -0
  17. fuzzy_dl_owl2/fuzzydl/concept/concrete/triangular_concrete_concept.py +2 -0
  18. fuzzy_dl_owl2/fuzzydl/concept/modified/linearly_modified_concept.py +3 -0
  19. fuzzy_dl_owl2/fuzzydl/concept/modified/modified_concept.py +3 -0
  20. fuzzy_dl_owl2/fuzzydl/concept/modified/triangularly_modified_concept.py +3 -0
  21. fuzzy_dl_owl2/fuzzydl/concept/negated_nominal.py +3 -0
  22. fuzzy_dl_owl2/fuzzydl/concept/operator_concept.py +8 -0
  23. fuzzy_dl_owl2/fuzzydl/concept/qowa_concept.py +4 -0
  24. fuzzy_dl_owl2/fuzzydl/concept/quasi_sugeno_integral.py +3 -0
  25. fuzzy_dl_owl2/fuzzydl/concept/sigma_concept.py +71 -0
  26. fuzzy_dl_owl2/fuzzydl/concept/sigma_count.py +56 -0
  27. fuzzy_dl_owl2/fuzzydl/concept/sugeno_integral.py +4 -0
  28. fuzzy_dl_owl2/fuzzydl/concept_equivalence.py +5 -0
  29. fuzzy_dl_owl2/fuzzydl/concrete_feature.py +6 -0
  30. fuzzy_dl_owl2/fuzzydl/domain_axiom.py +3 -0
  31. fuzzy_dl_owl2/fuzzydl/feature_function.py +12 -3
  32. fuzzy_dl_owl2/fuzzydl/fuzzydl_to_owl2.py +3 -1
  33. fuzzy_dl_owl2/fuzzydl/general_concept_inclusion.py +6 -0
  34. fuzzy_dl_owl2/fuzzydl/individual/created_individual.py +41 -2
  35. fuzzy_dl_owl2/fuzzydl/individual/individual.py +14 -0
  36. fuzzy_dl_owl2/fuzzydl/individual/representative_individual.py +9 -0
  37. fuzzy_dl_owl2/fuzzydl/knowledge_base.py +2046 -250
  38. fuzzy_dl_owl2/fuzzydl/label.py +18 -10
  39. fuzzy_dl_owl2/fuzzydl/milp/expression.py +45 -24
  40. fuzzy_dl_owl2/fuzzydl/milp/inequation.py +20 -0
  41. fuzzy_dl_owl2/fuzzydl/milp/milp_helper.py +1398 -60
  42. fuzzy_dl_owl2/fuzzydl/milp/show_variables_helper.py +82 -0
  43. fuzzy_dl_owl2/fuzzydl/milp/solution.py +23 -0
  44. fuzzy_dl_owl2/fuzzydl/milp/term.py +4 -1
  45. fuzzy_dl_owl2/fuzzydl/milp/variable.py +7 -0
  46. fuzzy_dl_owl2/fuzzydl/modifier/linear_modifier.py +3 -0
  47. fuzzy_dl_owl2/fuzzydl/modifier/modifier.py +21 -0
  48. fuzzy_dl_owl2/fuzzydl/parser/dl_parser.py +48 -7
  49. fuzzy_dl_owl2/fuzzydl/primitive_concept_definition.py +7 -0
  50. fuzzy_dl_owl2/fuzzydl/query/__init__.py +1 -0
  51. fuzzy_dl_owl2/fuzzydl/query/all_instances_query.py +80 -1
  52. fuzzy_dl_owl2/fuzzydl/query/bnp_query.py +2 -0
  53. fuzzy_dl_owl2/fuzzydl/query/classification_query.py +26 -0
  54. fuzzy_dl_owl2/fuzzydl/query/defuzzify/defuzzify_query.py +2 -1
  55. fuzzy_dl_owl2/fuzzydl/query/defuzzify/lom_defuzzify_query.py +4 -0
  56. fuzzy_dl_owl2/fuzzydl/query/defuzzify/mom_defuzzify_query.py +6 -2
  57. fuzzy_dl_owl2/fuzzydl/query/defuzzify/som_defuzzify_query.py +2 -0
  58. fuzzy_dl_owl2/fuzzydl/query/instance_query.py +5 -0
  59. fuzzy_dl_owl2/fuzzydl/query/kb_satisfiable_query.py +12 -2
  60. fuzzy_dl_owl2/fuzzydl/query/max/max_instance_query.py +6 -1
  61. fuzzy_dl_owl2/fuzzydl/query/max/max_query.py +7 -1
  62. fuzzy_dl_owl2/fuzzydl/query/max/max_related_query.py +6 -1
  63. fuzzy_dl_owl2/fuzzydl/query/max/max_satisfiable_query.py +15 -1
  64. fuzzy_dl_owl2/fuzzydl/query/max/max_subsumes_query.py +4 -1
  65. fuzzy_dl_owl2/fuzzydl/query/min/min_instance_query.py +6 -1
  66. fuzzy_dl_owl2/fuzzydl/query/min/min_query.py +7 -1
  67. fuzzy_dl_owl2/fuzzydl/query/min/min_related_query.py +5 -1
  68. fuzzy_dl_owl2/fuzzydl/query/min/min_satisfiable_query.py +17 -1
  69. fuzzy_dl_owl2/fuzzydl/query/min/min_subsumes_query.py +47 -7
  70. fuzzy_dl_owl2/fuzzydl/query/query.py +5 -2
  71. fuzzy_dl_owl2/fuzzydl/query/related_query.py +8 -1
  72. fuzzy_dl_owl2/fuzzydl/query/satisfiable_query.py +17 -0
  73. fuzzy_dl_owl2/fuzzydl/query/subsumption_query.py +5 -0
  74. fuzzy_dl_owl2/fuzzydl/range_axiom.py +4 -0
  75. fuzzy_dl_owl2/fuzzydl/relation.py +5 -0
  76. fuzzy_dl_owl2/fuzzydl/restriction/has_value_restriction.py +2 -0
  77. fuzzy_dl_owl2/fuzzydl/restriction/restriction.py +3 -0
  78. fuzzy_dl_owl2/fuzzydl/role_parent_with_degree.py +6 -1
  79. fuzzy_dl_owl2/fuzzydl/util/config_reader.py +34 -2
  80. fuzzy_dl_owl2/fuzzydl/util/constants.py +105 -6
  81. fuzzy_dl_owl2/fuzzyowl2/fuzzyowl2.py +3 -1
  82. fuzzy_dl_owl2-1.0.8.dist-info/METADATA +817 -0
  83. {fuzzy_dl_owl2-1.0.6.dist-info → fuzzy_dl_owl2-1.0.8.dist-info}/RECORD +85 -81
  84. fuzzy_dl_owl2-1.0.6.dist-info/METADATA +0 -340
  85. {fuzzy_dl_owl2-1.0.6.dist-info → fuzzy_dl_owl2-1.0.8.dist-info}/LICENSE +0 -0
  86. {fuzzy_dl_owl2-1.0.6.dist-info → fuzzy_dl_owl2-1.0.8.dist-info}/WHEEL +0 -0
@@ -2,16 +2,19 @@ from __future__ import annotations
2
2
 
3
3
  import copy
4
4
  import os
5
+ import re
6
+ import time
7
+ import traceback
5
8
  import typing
6
9
 
7
- import gurobipy as gp
8
- from gurobipy import GRB
10
+ import networkx as nx
9
11
 
10
12
  from fuzzy_dl_owl2.fuzzydl.assertion.assertion import Assertion
11
13
  from fuzzy_dl_owl2.fuzzydl.concept.concept import Concept
12
14
  from fuzzy_dl_owl2.fuzzydl.concept.interface.has_value_interface import (
13
15
  HasValueInterface,
14
16
  )
17
+ from fuzzy_dl_owl2.fuzzydl.concept.sigma_count import SigmaCount
15
18
  from fuzzy_dl_owl2.fuzzydl.degree.degree import Degree
16
19
  from fuzzy_dl_owl2.fuzzydl.degree.degree_numeric import DegreeNumeric
17
20
  from fuzzy_dl_owl2.fuzzydl.degree.degree_variable import DegreeVariable
@@ -30,6 +33,7 @@ from fuzzy_dl_owl2.fuzzydl.util.config_reader import ConfigReader
30
33
  from fuzzy_dl_owl2.fuzzydl.util.constants import (
31
34
  ConceptType,
32
35
  InequalityType,
36
+ MILPProvider,
33
37
  VariableType,
34
38
  )
35
39
  from fuzzy_dl_owl2.fuzzydl.util.util import Util
@@ -37,10 +41,17 @@ from fuzzy_dl_owl2.fuzzydl.util.util import Util
37
41
 
38
42
  # @utils.singleton
39
43
  class MILPHelper:
44
+ """MILP problem manager, storing the problem and calling an external solver."""
45
+
46
+ PARTITION: bool = False
47
+ # Indicates whether we want to show the membership degrees to linguistic labels or not.
40
48
  PRINT_LABELS: bool = True
49
+ # Indicates whether we want to show the value of the variables or not.
41
50
  PRINT_VARIABLES: bool = True
42
51
 
43
52
  def __init__(self) -> None:
53
+ self.nominal_variables: bool = False
54
+ self.cardinalities: list[SigmaCount] = list()
44
55
  self.constraints: list[Inequation] = list()
45
56
  self.crisp_concepts: set[str] = set()
46
57
  self.crisp_roles: set[str] = set()
@@ -52,6 +63,8 @@ class MILPHelper:
52
63
 
53
64
  def clone(self) -> typing.Self:
54
65
  milp: MILPHelper = MILPHelper()
66
+ milp.nominal_variables = self.nominal_variables
67
+ milp.cardinalities = [c.clone() for c in self.cardinalities]
55
68
  milp.constraints = [c.clone() for c in self.constraints]
56
69
  milp.crisp_concepts = copy.deepcopy(self.crisp_concepts)
57
70
  milp.crisp_roles = copy.deepcopy(self.crisp_roles)
@@ -62,8 +75,37 @@ class MILPHelper:
62
75
  milp.variables = [v.clone() for v in self.variables]
63
76
  return milp
64
77
 
65
- def optimize(self, objective: Expression) -> Solution:
66
- return self.solve_gurobi(objective)
78
+ def optimize(self, objective: Expression) -> typing.Optional[Solution]:
79
+ """
80
+ It optimizes an expression using a solvers from MILPProvider.
81
+
82
+ Args:
83
+ objective (Expression): Expression to be optimized.
84
+
85
+ Raises:
86
+ ValueError: If MILPProvider is not known.
87
+
88
+ Returns:
89
+ typing.Optional[Solution]: An optimal solution of the expression
90
+ """
91
+ Util.debug(f"Running MILP solver: {ConfigReader.MILP_PROVIDER.name}")
92
+ if ConfigReader.MILP_PROVIDER == MILPProvider.GUROBI:
93
+ return self.solve_gurobi(objective)
94
+ elif ConfigReader.MILP_PROVIDER == MILPProvider.MIP:
95
+ return self.solve_mip(objective)
96
+ elif ConfigReader.MILP_PROVIDER in [
97
+ MILPProvider.PULP,
98
+ MILPProvider.PULP_GLPK,
99
+ MILPProvider.PULP_HIGHS,
100
+ MILPProvider.PULP_CPLEX,
101
+ ]:
102
+ return self.solve_pulp(objective)
103
+ # elif ConfigReader.MILP_PROVIDER == MILPProvider.SCIPY:
104
+ # return self.solve_scipy(objective)
105
+ else:
106
+ raise ValueError(
107
+ f"Unsupported MILP provider: {ConfigReader.MILP_PROVIDER.name}"
108
+ )
67
109
 
68
110
  @typing.overload
69
111
  def print_instance_of_labels(
@@ -74,6 +116,7 @@ class MILPHelper:
74
116
  def print_instance_of_labels(self, name: str, value: float) -> None: ...
75
117
 
76
118
  def print_instance_of_labels(self, *args) -> None:
119
+ """Shows the membership degrees to some linguistic labels."""
77
120
  assert len(args) in [2, 3]
78
121
  assert isinstance(args[0], str)
79
122
  if len(args) == 2:
@@ -87,7 +130,15 @@ class MILPHelper:
87
130
  def __print_instance_of_labels_1(
88
131
  self, f_name: str, ind_name: str, value: float
89
132
  ) -> None:
90
- name = f"{f_name}({ind_name})"
133
+ """
134
+ Shows the membership degrees to some linguistic labels.
135
+
136
+ Args:
137
+ f_name (str): Name of the feature.
138
+ ind_name (str): Name of the individual.
139
+ value (float): Value of the feature for the given individual.
140
+ """
141
+ name: str = f"{f_name}({ind_name})"
91
142
  labels = self.show_vars.get_labels(name)
92
143
  for f in labels:
93
144
  Util.info(
@@ -95,6 +146,13 @@ class MILPHelper:
95
146
  )
96
147
 
97
148
  def __print_instance_of_labels_2(self, name: str, value: float) -> None:
149
+ """
150
+ Shows the membership degrees to some linguistic labels.
151
+
152
+ Args:
153
+ name (str): Name of the feature (individual).
154
+ value (float): Value of the feature for the given individual.
155
+ """
98
156
  labels = self.show_vars.get_labels(name)
99
157
  for f in labels:
100
158
  Util.info(
@@ -102,6 +160,7 @@ class MILPHelper:
102
160
  )
103
161
 
104
162
  def get_new_variable(self, v_type: VariableType) -> Variable:
163
+ """Gets a new variable with the indicated type."""
105
164
  while True:
106
165
  new_var: Variable = Variable.get_new_variable(v_type)
107
166
  var_name = str(new_var)
@@ -210,6 +269,9 @@ class MILPHelper:
210
269
  raise ValueError
211
270
 
212
271
  def __get_variable_1(self, var_name: str) -> Variable:
272
+ """
273
+ Gets a variable with the given name, creating a new one of type SEMI_CONTINUOUS in [0, 1] if it does not exist.
274
+ """
213
275
  if var_name in self.number_of_variables:
214
276
  for variable in self.variables:
215
277
  if str(variable) == var_name:
@@ -220,26 +282,69 @@ class MILPHelper:
220
282
  return var
221
283
 
222
284
  def __get_variable_2(self, var_name: str, v_type: VariableType) -> Variable:
285
+ """
286
+ Gets a variable with the indicated name and bound.
287
+
288
+ Only used by DatatypeReasoner.
289
+ """
223
290
  var: Variable = self.get_variable(var_name)
224
291
  var.set_type(v_type)
225
292
  return var
226
293
 
227
294
  def __get_variable_3(self, ass: Assertion) -> Variable:
295
+ """
296
+ Gets a variable taking the value of a concept assertion, creating a new one of type SEMI_CONTINUOUS in [0, 1] if it does not exist.
297
+
298
+ Args:
299
+ ass (Assertion): A fuzzy concept assertion.
300
+
301
+ Returns:
302
+ Variable: A variable taking the value of the assertion.
303
+ """
228
304
  return self.get_variable(ass.get_individual(), ass.get_concept())
229
305
 
230
306
  def __get_variable_4(self, rel: Relation) -> Variable:
307
+ """
308
+ Gets a variable taking the value of a role assertion, creating a new one of type SEMI_CONTINUOUS in [0, 1] if it does not exist.
309
+
310
+ Args:
311
+ ass (Assertion): A fuzzy role assertion.
312
+
313
+ Returns:
314
+ Variable: A variable taking the value of the assertion.
315
+ """
231
316
  a: Individual = rel.get_subject_individual()
232
317
  b: Individual = rel.get_object_individual()
233
318
  role: str = rel.get_role_name()
234
319
  return self.get_variable(a, b, role)
235
320
 
236
321
  def __get_variable_5(self, ind: Individual, restrict: Restriction) -> Variable:
322
+ """
323
+ Gets a variable taking the value of a universal restriction, creating a new one of type SEMI_CONTINUOUS in [0, 1] if it does not exist.
324
+
325
+ Args:
326
+ ind (Individual): Subject individual of the restrictions.
327
+ restrict (Restriction): A fuzzy role assertion.
328
+
329
+ Returns:
330
+ Variable: A variable taking the value of the assertion.
331
+ """
237
332
  var: Variable = self.get_variable(f"{ind}:{restrict.get_name_without_degree()}")
238
333
  if self.show_vars.show_individuals(str(ind)):
239
334
  self.show_vars.add_variable(var, str(var))
240
335
  return var
241
336
 
242
337
  def __get_variable_6(self, ind: Individual, c: Concept) -> Variable:
338
+ """
339
+ Gets a variable taking the value of a concept assertion, creating a new one of type SEMI_CONTINUOUS in [0, 1] if it does not exist.
340
+
341
+ Args:
342
+ ind (Individual): An individual.
343
+ c (Concept): A fuzzy concept.
344
+
345
+ Returns:
346
+ Variable: A variable taking the value of the assertion.
347
+ """
243
348
  if c.type == ConceptType.HAS_VALUE:
244
349
  assert isinstance(c, HasValueInterface)
245
350
 
@@ -249,6 +354,16 @@ class MILPHelper:
249
354
  return self.get_variable(ind, str(c))
250
355
 
251
356
  def __get_variable_7(self, ind: Individual, concept_name: str) -> Variable:
357
+ """
358
+ Gets a variable taking the value of a concept assertion, creating a new one of type SEMI_CONTINUOUS in [0, 1] if it does not exist.
359
+
360
+ Args:
361
+ ind (Individual): An individual.
362
+ concept_name (str): A fuzzy concept name.
363
+
364
+ Returns:
365
+ Variable: A variable taking the value of the assertion.
366
+ """
252
367
  var: Variable = self.get_variable(f"{ind}:{concept_name}")
253
368
  if concept_name in self.crisp_concepts:
254
369
  var.set_binary_variable()
@@ -259,11 +374,34 @@ class MILPHelper:
259
374
  return var
260
375
 
261
376
  def __get_variable_8(self, a: Individual, b: Individual, role: str) -> Variable:
377
+ """
378
+ Gets a variable taking the value of a role assertion, creating a new one of type SEMI_CONTINUOUS in [0, 1] if it does not exist.
379
+
380
+ Args:
381
+ a (Individual): Object individual.
382
+ b (Individual): Subject individual.
383
+ role (str): A role name.
384
+
385
+ Returns:
386
+ Variable: A variable taking the value of the assertion.
387
+ """
262
388
  return self.get_variable(a, b, role, VariableType.SEMI_CONTINUOUS)
263
389
 
264
390
  def __get_variable_9(
265
391
  self, a: Individual, b: Individual, role: str, v_type: VariableType
266
392
  ) -> Variable:
393
+ """
394
+ Gets a variable taking the value of a role assertion, creating a new one of type SEMI_CONTINUOUS in [0, 1] if it does not exist.
395
+
396
+ Args:
397
+ a (Individual): Object individual.
398
+ b (Individual): Subject individual.
399
+ role (str): A role name.
400
+ v_type (VariableType): Type of the variable.
401
+
402
+ Returns:
403
+ Variable: A variable taking the value of the assertion.
404
+ """
267
405
  return self.get_variable(str(a), str(b), role, v_type)
268
406
 
269
407
  def __get_variable_10(
@@ -281,9 +419,28 @@ class MILPHelper:
281
419
  return var
282
420
 
283
421
  def __get_variable_11(self, ind: CreatedIndividual) -> Variable:
422
+ """
423
+ Gets a variable taking the value of a concrete individual.
424
+
425
+ Args:
426
+ ind (CreatedIndividual): A concrete individual.
427
+
428
+ Returns:
429
+ Variable: A variable taking the value of the assertion.
430
+ """
284
431
  return self.get_variable(ind, VariableType.CONTINUOUS)
285
432
 
286
433
  def __get_variable_12(self, ind: CreatedIndividual, v_type: VariableType) -> None:
434
+ """
435
+ Gets a variable taking the value of a concrete individual.
436
+
437
+ Args:
438
+ ind (CreatedIndividual): A concrete individual.
439
+ v_type (VariableType): Type of the variable.
440
+
441
+ Returns:
442
+ Variable: A variable taking the value of the assertion.
443
+ """
287
444
  if ind.get_parent() is None:
288
445
  parent_name: str = "unknown_parent"
289
446
  else:
@@ -304,6 +461,18 @@ class MILPHelper:
304
461
  x_c.set_type(v_type)
305
462
  return x_c
306
463
 
464
+ def exists_variable(self, a: Individual, b: Individual, role: str) -> bool:
465
+ """
466
+ Checks if a variable taking the value of a role assertion exists.
467
+
468
+ Args:
469
+ a (Individual): Object individual.
470
+ b (Individual): Subject individual.
471
+ role (str): A role name.
472
+ """
473
+ var_name: str = f"({a},{b}):{role}"
474
+ return var_name in self.number_of_variables
475
+
307
476
  @typing.overload
308
477
  def has_variable(self, name: str) -> bool: ...
309
478
 
@@ -320,9 +489,11 @@ class MILPHelper:
320
489
  raise ValueError
321
490
 
322
491
  def __has_variable_1(self, name: str) -> bool:
492
+ """Cheks if there is a variable with the given name."""
323
493
  return name in self.number_of_variables
324
494
 
325
495
  def __has_variable_2(self, ass: Assertion) -> bool:
496
+ """Cheks if there is a variable for a concept assertion."""
326
497
  return self.has_variable(ass.get_name_without_degree())
327
498
 
328
499
  @typing.overload
@@ -341,22 +512,69 @@ class MILPHelper:
341
512
  return self.__get_nominal_variable_2(*args)
342
513
 
343
514
  def __get_nominal_variable_1(self, i1: str) -> Variable:
515
+ """
516
+ Gets a variable taking the value of an individual i1 belonging to the nominal concept {i1}.
517
+
518
+ Args:
519
+ i1 (str): An individual.
520
+
521
+ Returns:
522
+ Variable: A variable taking the value of the assertion i1:{i1}.
523
+ """
344
524
  return self.get_nominal_variable(i1, i1)
345
525
 
346
526
  def __get_nominal_variable_2(self, i1: str, i2: str) -> Variable:
527
+ """
528
+ Gets a variable taking the value of an individual i1 belonging to the nominal concept {i2}.
529
+
530
+ Args:
531
+ i1 (str): An individual that is subject of the assertion.
532
+ i2 (str): An individual representing the nominal concept.
533
+
534
+ Returns:
535
+ Variable: A variable taking the value of the assertion i1:{i2}.
536
+ """
347
537
  var_name = f"{i1}:{{ {i2} }}"
348
538
  v: Variable = self.get_variable(var_name)
349
539
  v.set_type(VariableType.BINARY)
350
540
  return v
351
541
 
542
+ def is_nominal_variable(self, i: str) -> bool:
543
+ """Checks if a variable 'i' is a nominal variable."""
544
+ # s: list[str] = i.split(":{")
545
+ # if len(s) != 2:
546
+ # return False
547
+ # return s[1] == f"{s[0]}" + "}"
548
+ pattern = re.compile(r"([^:]+):\{\1\}")
549
+ return len(pattern.findall(i)) > 0
550
+
551
+ def has_nominal_variable(self, terms: list[Term]) -> bool:
552
+ """Checks if a collection of terms has a nominal variable."""
553
+ for term in terms:
554
+ if self.is_nominal_variable(str(term.get_var())):
555
+ return True
556
+ return False
557
+
352
558
  def exists_nominal_variable(self, i: str) -> bool:
559
+ """Checks if there exists a variable taking the value of an individual i belonging to the nominal concept {i}."""
353
560
  var_name: str = f"{i}:{{ {i} }}"
354
561
  return var_name in list(map(str, self.variables))
355
562
 
356
563
  def get_negated_nominal_variable(self, i1: str, i2: str) -> Variable:
564
+ """
565
+ Gets a variable taking the value of an individual i1 not belonging to the nominal concept {i2}.
566
+
567
+ Args:
568
+ i1 (str): An individual that is subject of the assertion.
569
+ i2 (str): An individual representing the nominal concept.
570
+
571
+ Returns:
572
+ Variable: A variable taking the value of the assertion i1: not {i2}.
573
+ """
357
574
  var_name: str = f"{i1}: not {{ {i2} }}"
358
575
  flag: bool = var_name in list(map(str, self.variables))
359
576
  v: Variable = self.get_variable(var_name)
577
+ # First time the variable is created, x_{a:{o} } = 1 - x_{a: not {o} }
360
578
  if not flag:
361
579
  v.set_type(VariableType.BINARY)
362
580
  not_v: Variable = self.get_nominal_variable(i1, i2)
@@ -433,9 +651,23 @@ class MILPHelper:
433
651
  def __add_new_constraint_1(
434
652
  self, expr: Expression, constraint_type: InequalityType
435
653
  ) -> None:
654
+ """
655
+ Adds a new inequality of the form: expr constraint_type 0.
656
+
657
+ Args:
658
+ expr (Expression): An expression in the left side of the inequality.
659
+ constraint_type (InequalityType): Type of the constraint (EQ, GR, LE).
660
+ """
436
661
  self.constraints.append(Inequation(expr, constraint_type))
437
662
 
438
663
  def __add_new_constraint_2(self, x: Variable, n: float) -> None:
664
+ """
665
+ Adds a new inequality of the form: x >= n.
666
+
667
+ Args:
668
+ x (Variable): A variable.
669
+ n (float): A real number.
670
+ """
439
671
  self.add_new_constraint(
440
672
  Expression(Term(1.0, x)),
441
673
  InequalityType.GREATER_THAN,
@@ -443,14 +675,34 @@ class MILPHelper:
443
675
  )
444
676
 
445
677
  def __add_new_constraint_3(self, ass: Assertion, n: float) -> None:
678
+ """
679
+ Given a fuzzy assertion a:C >= L and a number n, adds an inequality of the form: xAss >= n.
680
+
681
+ Args:
682
+ ass (Assertion): A fuzzy assertion.
683
+ n (float): A real number.
684
+ """
446
685
  self.add_new_constraint(self.get_variable(ass), n)
447
686
 
448
687
  def __add_new_constraint_4(self, x: Variable, d: Degree) -> None:
688
+ """
689
+ Add an inequality of the form: x >= d.
690
+
691
+ Args:
692
+ x (Variable): A variable.
693
+ d (Degree): A degree.
694
+ """
449
695
  self.add_new_constraint(
450
696
  Expression(Term(1.0, x)), InequalityType.GREATER_THAN, d
451
697
  )
452
698
 
453
699
  def __add_new_constraint_5(self, ass: Assertion) -> None:
700
+ """
701
+ Adds a new inequality encoded in a fuzzy assertion.
702
+
703
+ Args:
704
+ ass (Assertion): A fuzzy assertion.
705
+ """
454
706
  x_ass: Variable = self.get_variable(ass)
455
707
  ass_name: str = str(x_ass)
456
708
  deg: Degree = ass.get_lower_limit()
@@ -463,6 +715,14 @@ class MILPHelper:
463
715
  def __add_new_constraint_6(
464
716
  self, expr: Expression, constraint_type: InequalityType, degree: Degree
465
717
  ) -> None:
718
+ """
719
+ Adds a new inequality of the form: expr constraint_type degree.
720
+
721
+ Args:
722
+ expr (Expression): An expression in the left side of the inequality.
723
+ constraint_type (InequalityType): Type of the constraint (EQ, GR, LE).
724
+ degree (Degree): A degree in the right side of the inequality.
725
+ """
466
726
  self.constraints.append(
467
727
  degree.create_inequality_with_degree_rhs(expr, constraint_type)
468
728
  )
@@ -470,22 +730,50 @@ class MILPHelper:
470
730
  def __add_new_constraint_7(
471
731
  self, expr: Expression, constraint_type: InequalityType, n: float
472
732
  ) -> None:
733
+ """
734
+ Adds a new inequality of the form: expr constraint_type n.
735
+
736
+ Args:
737
+ expr (Expression): An expression in the left side of the inequality.
738
+ constraint_type (InequalityType): Type of the constraint (EQ, GR, LE).
739
+ n (float): A real number expression in the right side of the inequality.
740
+ """
473
741
  self.add_new_constraint(expr, constraint_type, DegreeNumeric.get_degree(n))
474
742
 
475
743
  def add_equality(self, var1: Variable, var2: Variable) -> None:
744
+ """
745
+ Add an equality of the form: var1 = var2.
746
+ """
476
747
  self.add_new_constraint(
477
748
  Expression(Term(1.0, var1), Term(-1.0, var2)), InequalityType.EQUAL
478
749
  )
479
750
 
480
751
  def add_string_feature(self, role: str) -> None:
752
+ """Adds a string feature."""
481
753
  self.string_features.add(role)
482
754
 
483
755
  def add_string_value(self, value: str, int_value: int) -> None:
756
+ """
757
+ Relates the value of a string feature with an integer value.
758
+
759
+ Args:
760
+ value (str): Value of a string feature.
761
+ int_value (int): Corresponding integer value.
762
+ """
484
763
  self.string_values[int_value] = value
485
764
 
486
765
  def change_variable_names(
487
766
  self, old_name: str, new_name: str, old_is_created_individual: bool
488
767
  ) -> None:
768
+ """
769
+ Replaces the name of the variables including an individual name with the name of another individual name.
770
+
771
+ Args:
772
+ old_name (str): Old individual name.
773
+ new_name (str): New individual name.
774
+ old_is_created_individual (bool): Indicates whether the old individual is a created individual or not.
775
+ """
776
+
489
777
  old_values: list[str] = [f"{old_name},", f",{old_name}", f"{old_name}:"]
490
778
  new_values: list[str] = [f"{new_name},", f",{new_name}", f"{new_name}:"]
491
779
  to_process: list[Variable] = copy.deepcopy(self.variables)
@@ -500,6 +788,7 @@ class MILPHelper:
500
788
  if old_is_created_individual:
501
789
  self.add_equality(v1, v2)
502
790
  else:
791
+ # a:{b} => x_{a:C}) \geq x_{b:C}
503
792
  a_is_b: Variable = self.get_nominal_variable(new_name, old_name)
504
793
  self.add_new_constraint(
505
794
  Expression(
@@ -515,8 +804,10 @@ class MILPHelper:
515
804
  begin1: int = name1.index(s1)
516
805
  name2: str = str(v2)
517
806
  begin2: int = name2.index(s2)
807
+ # They are not similar because the parts before s1 and s2 have different lengths.
518
808
  if begin1 != begin2:
519
809
  return False
810
+ # If the parts before and after s1/s2 coincide, they are similar.
520
811
  return (
521
812
  name1[:begin1] == name2[:begin2]
522
813
  and name1[begin1 + len(s1) :] == name2[begin2 + len(s2) :]
@@ -557,33 +848,48 @@ class MILPHelper:
557
848
  def __get_ordered_permutation_2(
558
849
  self, x: list[Variable], z: list[list[Variable]]
559
850
  ) -> list[Variable]:
851
+ """
852
+ Gets an ordered permutation of the variables.
853
+
854
+ Args:
855
+ x (list[Variable]): A vector of input variables.
856
+ z (list[list[Variable]]): A matrix of intermediate variables.
857
+
858
+ Returns:
859
+ list[Variable]: A permutation of the input variables such that y[0] >= y[1] >= ... >= y[n-1]
860
+ """
560
861
  n: int = len(x)
862
+ # New n [0,1] variables yi
561
863
  y: list[Variable] = [
562
864
  self.get_new_variable(VariableType.SEMI_CONTINUOUS) for _ in range(n)
563
865
  ]
866
+ # y1 >= y2 >= ... >= yn
564
867
  for i in range(n - 1):
565
868
  self.add_new_constraint(
566
869
  Expression(Term(1.0, y[i]), Term(-1.0, y[i + 1])),
567
870
  InequalityType.GREATER_THAN,
568
871
  )
872
+ # for each i,j : yi - kz_{ij} <= xj
569
873
  for i in range(n):
570
874
  for j in range(n):
571
875
  self.add_new_constraint(
572
876
  Expression(Term(1.0, x[j]), Term(-1.0, y[i]), Term(1.0, z[i][j])),
573
877
  InequalityType.GREATER_THAN,
574
878
  )
879
+ # for each i,j : xj <= yi + kz_{ij}
575
880
  for i in range(n):
576
881
  for j in range(n):
577
882
  self.add_new_constraint(
578
883
  Expression(Term(1.0, x[j]), Term(-1.0, y[i]), Term(-1.0, z[i][j])),
579
884
  InequalityType.LESS_THAN,
580
885
  )
886
+ # for each i : \sum_{j} z_{ij} = n - 1
581
887
  for i in range(n):
582
888
  exp: Expression = Expression(1.0 - n)
583
889
  for j in range(n):
584
890
  exp.add_term(Term(1.0, z[i][j]))
585
891
  self.add_new_constraint(exp, InequalityType.EQUAL)
586
-
892
+ # for each j : \sum_{i} z_{ij} = n - 1
587
893
  for i in range(n):
588
894
  exp: Expression = Expression(1.0 - n)
589
895
  for j in range(n):
@@ -591,9 +897,379 @@ class MILPHelper:
591
897
  self.add_new_constraint(exp, InequalityType.EQUAL)
592
898
  return y
593
899
 
900
+ def __bfs(self, graph: nx.Graph, solution: dict[int, int]) -> int:
901
+ # Number of nodes
902
+ n: int = graph.number_of_nodes()
903
+
904
+ # Solution is a mapping: variable -> partition
905
+ # Initial partition value is 0
906
+ for i in range(n):
907
+ solution[i] = 0
908
+
909
+ # Number of partition
910
+ p: int = 1
911
+
912
+ # Iterate over not processed nodes
913
+ queue: list[int] = list()
914
+ for i in range(n - 1):
915
+ # Skip node if processed
916
+ if solution[i] != 0:
917
+ continue
918
+ queue = [i]
919
+ solution[i] = p
920
+ self.__compute_partition(queue, solution, p, graph)
921
+
922
+ # Next partition
923
+ p += 1
924
+ return p - 1
925
+
926
+ def __compute_partition(
927
+ self, queue: list[int], solution: dict[int, int], p: int, graph: nx.Graph
928
+ ) -> None:
929
+
930
+ while len(queue) > 0:
931
+ current: int = queue.pop()
932
+ neighbors: list[int] = list(graph.neighbors(current))
933
+ if len(neighbors) == 0:
934
+ continue
935
+ for j in neighbors:
936
+ if solution[j] != 0:
937
+ continue
938
+ solution[j] = p
939
+ queue.append(j)
940
+
941
+ def set_nominal_variables(self, value: bool) -> None:
942
+ self.nominal_variables = value
943
+
944
+ def __remove_nominal_variables(self) -> None:
945
+ constraints_to_remove: list[int] = []
946
+ variable_to_remove: list[int] = []
947
+ for i, constraint in enumerate(self.constraints):
948
+ terms: list[Term] = constraint.get_terms()
949
+ if self.has_nominal_variable(terms):
950
+ constraints_to_remove.append(i)
951
+ for i, variable in enumerate(self.variables):
952
+ if self.is_nominal_variable(str(variable)):
953
+ variable_to_remove.append(i)
954
+
955
+ self.constraints = [
956
+ constraint
957
+ for i, constraint in enumerate(self.constraints)
958
+ if i not in constraints_to_remove
959
+ ]
960
+ self.variables = [
961
+ variable
962
+ for i, variable in enumerate(self.variables)
963
+ if i not in variable_to_remove
964
+ ]
965
+
966
+ def __get_graph(self) -> nx.Graph:
967
+ g: nx.Graph = nx.Graph()
968
+
969
+ # Create nodes
970
+ n: int = len(self.variables)
971
+ for i in range(n):
972
+ g.add_node(i)
973
+
974
+ # Create edges
975
+ edge: int = 0
976
+ for constraint in self.constraints:
977
+ terms: list[Term] = constraint.get_terms()
978
+ if len(terms) == 0:
979
+ continue
980
+ first_var: int = self.variables.index(terms[0].get_var())
981
+ for term in terms[1:]:
982
+ other_var: int = self.variables.index(term.get_var())
983
+ # Edges between first and other
984
+ edge += 1
985
+ g.add_edge(first_var, other_var, number=edge)
986
+
987
+ return g
988
+
989
+ def __common_partition_part(
990
+ self, objective: Expression
991
+ ) -> tuple[list[Variable], dict[int, int], int, list[int], int, int]:
992
+
993
+ objectives: list[Variable] = list()
994
+
995
+ # Partition time
996
+ init_time: int = time.perf_counter_ns()
997
+
998
+ # Graph
999
+ solution: dict[int, int] = dict()
1000
+ num_partitions: int = self.__bfs(self.__get_graph(), solution)
1001
+
1002
+ # Mapping partition -> number of objective variables in partition
1003
+ num_variables_in_partition: list[int] = [0] * num_partitions
1004
+
1005
+ # Compute objective coefficients
1006
+ for term in objective.get_terms():
1007
+ v: Variable = term.get_var()
1008
+ objectives.append(v)
1009
+ index: int = self.variables.index(v)
1010
+ num_partition: int = solution.get(index) - 1
1011
+ num_variables_in_partition[num_partition] += 1
1012
+
1013
+ # Compute two or more partitions
1014
+ two_or_more: int = 0
1015
+ count: int = 0
1016
+ for i in range(num_partitions):
1017
+ if num_variables_in_partition[i] > 1:
1018
+ two_or_more += 1
1019
+ count += num_variables_in_partition[i]
1020
+
1021
+ end_time: int = time.perf_counter_ns()
1022
+ total_time: float = (end_time - init_time) * 1e-9
1023
+ Util.debug(f"Partition time: {total_time} s")
1024
+ return (
1025
+ objectives,
1026
+ solution,
1027
+ num_partitions,
1028
+ num_variables_in_partition,
1029
+ two_or_more,
1030
+ count,
1031
+ )
1032
+
1033
+ def __solve_gurobi_using_partitions(
1034
+ self, objective: Expression
1035
+ ) -> typing.Optional[Solution]:
1036
+ import gurobipy as gp
1037
+ from gurobipy import GRB
1038
+
1039
+ (
1040
+ objectives,
1041
+ solution,
1042
+ num_partitions,
1043
+ num_variables_in_partition,
1044
+ two_or_more,
1045
+ count,
1046
+ ) = self.__common_partition_part(objective)
1047
+
1048
+ if two_or_more == 0:
1049
+ MILPHelper.PARTITION = False
1050
+ return self.solve_gurobi(objective)
1051
+
1052
+ # Specific algorithm starts here
1053
+ try:
1054
+ Util.debug(
1055
+ f"There are {two_or_more} partitions with {count} dependent objective variables"
1056
+ )
1057
+
1058
+ # PROBLEMS with 1 or less
1059
+ env = gp.Env(empty=True)
1060
+ if not ConfigReader.DEBUG_PRINT:
1061
+ env.setParam("OutputFlag", 0)
1062
+ env.setParam("IntFeasTol", 1e-9)
1063
+ env.setParam("BarConvTol", 0)
1064
+ env.start()
1065
+
1066
+ model: gp.Model = gp.Model("partition-model-1-or-less", env=env)
1067
+
1068
+ # Create variables
1069
+ vars_gurobi: dict[str, gp.Var] = dict()
1070
+
1071
+ var_types: dict[VariableType, str] = {
1072
+ VariableType.BINARY: GRB.BINARY,
1073
+ VariableType.INTEGER: GRB.INTEGER,
1074
+ VariableType.CONTINUOUS: GRB.CONTINUOUS,
1075
+ VariableType.SEMI_CONTINUOUS: GRB.SEMICONT,
1076
+ }
1077
+ var_name_map: dict[str, str] = {
1078
+ str(v): f"x{i}" for i, v in enumerate(self.variables)
1079
+ }
1080
+ for i, curr_variable in enumerate(self.variables):
1081
+ num_partition: int = solution.get(i) - 1
1082
+ if num_variables_in_partition[num_partition] > 1:
1083
+ continue # Next variable
1084
+ v_type: VariableType = curr_variable.get_type()
1085
+
1086
+ Util.debug(
1087
+ (
1088
+ f"Variable -- "
1089
+ f"[{curr_variable.get_lower_bound()}, {curr_variable.get_upper_bound()}] - "
1090
+ f"Obj value = 0 - "
1091
+ f"Var type = {v_type.name} -- "
1092
+ f"Var = {curr_variable}"
1093
+ )
1094
+ )
1095
+
1096
+ vars_gurobi[var_name_map[str(curr_variable)]] = model.addVar(
1097
+ lb=curr_variable.get_lower_bound(),
1098
+ ub=curr_variable.get_upper_bound(),
1099
+ obj=0,
1100
+ vtype=var_types[v_type],
1101
+ name=var_name_map[str(curr_variable)],
1102
+ )
1103
+
1104
+ # Integrate new variables
1105
+ model.update()
1106
+
1107
+ constraint_name: str = "constraint"
1108
+ # Add constraints
1109
+ for i, constraint in enumerate(self.constraints):
1110
+ if constraint in self.constraints[:i]:
1111
+ continue
1112
+ if constraint.is_zero():
1113
+ continue
1114
+
1115
+ curr_name: str = f"{constraint_name}_{i + 1}"
1116
+ expr: gp.LinExpr = gp.LinExpr()
1117
+ for term in constraint.get_terms():
1118
+ index: int = self.variables.index(term.get_var())
1119
+ num_partition: int = solution.get(index) - 1
1120
+ if num_variables_in_partition[num_partition] > 1:
1121
+ break # Exit for term loop
1122
+ v: gp.Var = vars_gurobi[var_name_map[str(term.get_var())]]
1123
+ c: float = term.get_coeff()
1124
+ if c == 0:
1125
+ continue
1126
+ expr.add(v, c)
1127
+
1128
+ if expr.size() == 0:
1129
+ continue
1130
+
1131
+ if constraint.get_type() == InequalityType.EQUAL:
1132
+ gp_constraint: gp.Constr = expr == constraint.get_constant()
1133
+ elif constraint.get_type() == InequalityType.LESS_THAN:
1134
+ gp_constraint: gp.Constr = expr <= constraint.get_constant()
1135
+ elif constraint.get_type() == InequalityType.GREATER_THAN:
1136
+ gp_constraint: gp.Constr = expr >= constraint.get_constant()
1137
+
1138
+ model.addConstr(gp_constraint, curr_name)
1139
+ Util.debug(f"{curr_name}: {constraint}")
1140
+
1141
+ # Integrate new constraints
1142
+ model.update()
1143
+
1144
+ # Optimize model
1145
+ model.optimize()
1146
+ Util.debug(f"Model:")
1147
+
1148
+ # Return solution
1149
+ if model.Status == GRB.INFEASIBLE:
1150
+ return Solution(Solution.INCONSISTENT_KB)
1151
+
1152
+ # One for each partition with two or more variables, plus one for the rest (all partitions with 0 and 1)
1153
+ sol: Solution = Solution(1.0)
1154
+
1155
+ # PROBLEMS with 2 or more
1156
+ for obj_var in objectives:
1157
+ env = gp.Env(empty=True)
1158
+ if not ConfigReader.DEBUG_PRINT:
1159
+ env.setParam("OutputFlag", 0)
1160
+ env.setParam("IntFeasTol", 1e-9)
1161
+ env.setParam("BarConvTol", 0)
1162
+ env.start()
1163
+
1164
+ model: gp.Model = gp.Model("partition-model-2-or-more", env=env)
1165
+
1166
+ index: int = self.variables.index(obj_var)
1167
+ problem: int = solution.get(index) - 1
1168
+
1169
+ vars_gurobi: dict[str, gp.Var] = dict()
1170
+
1171
+ # Create variables
1172
+ for i, curr_variable in enumerate(self.variables):
1173
+ num_partition: int = solution.get(i) - 1
1174
+ if num_partition != problem:
1175
+ continue
1176
+
1177
+ v_type: VariableType = curr_variable.get_type()
1178
+ ov: float = 1.0 if i == self.variables.index(obj_var) else 0.0
1179
+
1180
+ Util.debug(
1181
+ (
1182
+ f"Variable -- "
1183
+ f"[{curr_variable.get_lower_bound()}, {curr_variable.get_upper_bound()}] - "
1184
+ f"Obj value = {ov} - "
1185
+ f"Var type = {v_type.name} -- "
1186
+ f"Var = {curr_variable}"
1187
+ )
1188
+ )
1189
+
1190
+ vars_gurobi[var_name_map[str(curr_variable)]] = model.addVar(
1191
+ lb=curr_variable.get_lower_bound(),
1192
+ ub=curr_variable.get_upper_bound(),
1193
+ obj=ov,
1194
+ vtype=var_types[v_type],
1195
+ name=var_name_map[str(curr_variable)],
1196
+ )
1197
+
1198
+ # Integrate new variables
1199
+ model.update()
1200
+
1201
+ constraint_name: str = "constraint"
1202
+ # Add constraints
1203
+ for i, constraint in enumerate(self.constraints):
1204
+ if constraint in self.constraints[:i]:
1205
+ continue
1206
+ if constraint.is_zero():
1207
+ continue
1208
+
1209
+ curr_name: str = f"{constraint_name}_{i + 1}"
1210
+ expr: gp.LinExpr = gp.LinExpr()
1211
+ for term in constraint.get_terms():
1212
+ index: int = self.variables.index(term.get_var())
1213
+ num_partition: int = solution.get(index) - 1
1214
+ if num_partition != problem:
1215
+ break # Exit for term loop
1216
+ v: gp.Var = vars_gurobi[var_name_map[str(term.get_var())]]
1217
+ c: float = term.get_coeff()
1218
+ if c == 0:
1219
+ continue
1220
+ expr.add(v, c)
1221
+
1222
+ if expr.size() == 0:
1223
+ continue
1224
+
1225
+ if constraint.get_type() == InequalityType.EQUAL:
1226
+ gp_constraint: gp.Constr = expr == constraint.get_constant()
1227
+ elif constraint.get_type() == InequalityType.LESS_THAN:
1228
+ gp_constraint: gp.Constr = expr <= constraint.get_constant()
1229
+ elif constraint.get_type() == InequalityType.GREATER_THAN:
1230
+ gp_constraint: gp.Constr = expr >= constraint.get_constant()
1231
+
1232
+ model.addConstr(gp_constraint, curr_name)
1233
+ Util.debug(f"{curr_name}: {constraint}")
1234
+
1235
+ # Integrate new constraints
1236
+ model.update()
1237
+
1238
+ # Optimize model
1239
+ model.optimize()
1240
+
1241
+ # Return solution
1242
+ if model.Status == GRB.INFEASIBLE:
1243
+ return Solution(Solution.INCONSISTENT_KB)
1244
+ else:
1245
+ result: float = Util.round(abs(model.ObjVal))
1246
+ sol = Solution(result)
1247
+ name: str = str(obj_var)
1248
+ sol.add_showed_variable(name, result)
1249
+
1250
+ model.printQuality()
1251
+ model.printStats()
1252
+
1253
+ return sol
1254
+ except gp.GurobiError as e:
1255
+ Util.error(f"Error code: {e.errno}. {e.message}")
1256
+ return None
1257
+
594
1258
  def solve_gurobi(self, objective: Expression) -> typing.Optional[Solution]:
1259
+ """
1260
+ Solves a MILP problem using Gurobi.
1261
+ """
1262
+
1263
+ import gurobipy as gp
1264
+ from gurobipy import GRB
1265
+
1266
+ if not self.nominal_variables:
1267
+ self.__remove_nominal_variables()
1268
+
1269
+ if MILPHelper.PARTITION:
1270
+ return self.__solve_gurobi_using_partitions(objective)
1271
+
595
1272
  try:
596
- Util.debug("Running MILP solver: Gurobi")
597
1273
  Util.debug(f"Objective function -> {objective}")
598
1274
 
599
1275
  num_binary_vars: int = 0
@@ -605,6 +1281,7 @@ class MILPHelper:
605
1281
 
606
1282
  if objective is not None:
607
1283
  for term in objective.get_terms():
1284
+ # Compute objective coefficients
608
1285
  index = self.variables.index(term.get_var())
609
1286
  objective_value[index] += term.get_coeff()
610
1287
 
@@ -615,38 +1292,46 @@ class MILPHelper:
615
1292
  env.setParam("BarConvTol", 0)
616
1293
  env.start()
617
1294
 
618
- model = gp.Model("model", env=env)
619
- vars_gurobi: list[gp.Var] = []
1295
+ model: gp.Model = gp.Model("model", env=env)
1296
+ vars_gurobi: dict[str, gp.Var] = dict()
620
1297
  show_variable: list[bool] = [False] * size
621
1298
 
622
1299
  my_vars: list[Variable] = self.show_vars.get_variables()
623
1300
 
624
- for i in range(size):
625
- v: Variable = self.variables[i]
626
- v_type: VariableType = v.get_type()
1301
+ var_types: dict[VariableType, str] = {
1302
+ VariableType.BINARY: GRB.BINARY,
1303
+ VariableType.INTEGER: GRB.INTEGER,
1304
+ VariableType.CONTINUOUS: GRB.CONTINUOUS,
1305
+ VariableType.SEMI_CONTINUOUS: GRB.SEMICONT,
1306
+ }
1307
+ var_name_map: dict[str, str] = {
1308
+ str(v): f"x{i}" for i, v in enumerate(self.variables)
1309
+ }
1310
+
1311
+ # Create variables
1312
+ for i, curr_variable in enumerate(self.variables):
1313
+ v_type: VariableType = curr_variable.get_type()
627
1314
  ov: float = objective_value[i]
628
1315
 
629
1316
  Util.debug(
630
1317
  (
631
1318
  f"Variable -- "
632
- f"[{v.get_lower_bound()}, {v.get_upper_bound()}] - "
1319
+ f"[{curr_variable.get_lower_bound()}, {curr_variable.get_upper_bound()}] - "
633
1320
  f"Obj value = {ov} - "
634
1321
  f"Var type = {v_type.name} -- "
635
- f"Var = {v}"
1322
+ f"Var = {curr_variable}"
636
1323
  )
637
1324
  )
638
1325
 
639
- vars_gurobi.append(
640
- model.addVar(
641
- lb=v.get_lower_bound(),
642
- ub=v.get_upper_bound(),
643
- obj=ov,
644
- vtype=v_type.name,
645
- name=str(v),
646
- )
1326
+ vars_gurobi[var_name_map[str(curr_variable)]] = model.addVar(
1327
+ lb=curr_variable.get_lower_bound(),
1328
+ ub=curr_variable.get_upper_bound(),
1329
+ obj=ov,
1330
+ vtype=var_types[v_type],
1331
+ name=var_name_map[str(curr_variable)],
647
1332
  )
648
1333
 
649
- if v in my_vars:
1334
+ if curr_variable in my_vars:
650
1335
  show_variable[i] = True
651
1336
 
652
1337
  if v_type == VariableType.BINARY:
@@ -658,16 +1343,22 @@ class MILPHelper:
658
1343
  elif v_type == VariableType.SEMI_CONTINUOUS:
659
1344
  num_up_vars += 1
660
1345
 
1346
+ # Integrate new variables
661
1347
  model.update()
662
1348
 
663
1349
  Util.debug(f"# constraints -> {len(self.constraints)}")
664
1350
  constraint_name: str = "constraint"
1351
+ # Add constraints
665
1352
  for i, constraint in enumerate(self.constraints):
1353
+ if constraint in self.constraints[:i]:
1354
+ continue
1355
+ if constraint.is_zero():
1356
+ continue
1357
+
666
1358
  curr_name: str = f"{constraint_name}_{i + 1}"
667
1359
  expr: gp.LinExpr = gp.LinExpr()
668
1360
  for term in constraint.get_terms():
669
- index: int = self.variables.index(term.get_var())
670
- v: gp.Var = vars_gurobi[index]
1361
+ v: gp.Var = vars_gurobi[var_name_map[str(term.get_var())]]
671
1362
  c: float = term.get_coeff()
672
1363
  if c == 0:
673
1364
  continue
@@ -684,32 +1375,38 @@ class MILPHelper:
684
1375
  gp_constraint: gp.Constr = expr >= constraint.get_constant()
685
1376
 
686
1377
  model.addConstr(gp_constraint, curr_name)
687
- Util.debug(f"{curr_name}: {gp_constraint}")
1378
+ Util.debug(f"{curr_name}: {constraint}")
688
1379
 
1380
+ # Integrate new constraints
689
1381
  model.update()
1382
+
1383
+ # Optimize model
690
1384
  model.optimize()
691
1385
 
692
- model.write(os.path.join(constants.RESULTS_PATH, "model.lp"))
693
- model.write(os.path.join(constants.RESULTS_PATH, "solution.json"))
1386
+ model.write(os.path.join(constants.RESULTS_PATH, "gurobi_model.lp"))
1387
+ model.write(os.path.join(constants.RESULTS_PATH, "gurobi_solution.json"))
694
1388
 
695
1389
  Util.debug(f"Model:")
696
1390
  sol: Solution = None
697
- if model.Status == GRB.INFEASIBLE and ConfigReader.RELAX_MILP:
698
- self.__handle_model_infeasibility(model)
1391
+ # if model.Status == GRB.INFEASIBLE and ConfigReader.RELAX_MILP:
1392
+ # self.__gurobi_handle_model_infeasibility(model)
699
1393
 
1394
+ # Return solution
700
1395
  if model.Status == GRB.INFEASIBLE:
701
- sol = Solution(False)
1396
+ sol = Solution(Solution.INCONSISTENT_KB)
702
1397
  else:
1398
+ result: float = Util.round(abs(model.ObjVal))
1399
+ sol = Solution(result)
703
1400
  for i in range(size):
704
1401
  if ConfigReader.DEBUG_PRINT or show_variable[i]:
705
- name: str = vars_gurobi[i].VarName
706
- value: float = round(vars_gurobi[i].X, 6)
707
- if self.PRINT_VARIABLES:
708
- Util.debug(f"{name} = {value}")
1402
+ name: str = self.variables[i].name
1403
+ value: float = round(vars_gurobi[var_name_map[name]].X, 6)
1404
+ if show_variable[i]:
1405
+ sol.add_showed_variable(name, value)
1406
+ # if self.PRINT_VARIABLES:
1407
+ Util.debug(f"{name} = {value}")
709
1408
  if self.PRINT_LABELS:
710
1409
  self.print_instance_of_labels(name, value)
711
- result: float = Util.round(abs(model.ObjVal))
712
- sol = Solution(result)
713
1410
 
714
1411
  model.printQuality()
715
1412
  model.printStats()
@@ -718,54 +1415,678 @@ class MILPHelper:
718
1415
  f"{constants.STAR_SEPARATOR}Statistics{constants.STAR_SEPARATOR}"
719
1416
  )
720
1417
  Util.debug("MILP problem:")
1418
+ # Show number of variables
721
1419
  Util.debug(f"\t\tSemi continuous variables: {num_up_vars}")
722
1420
  Util.debug(f"\t\tBinary variables: {num_binary_vars}")
723
1421
  Util.debug(f"\t\tContinuous variables: {num_free_vars}")
724
1422
  Util.debug(f"\t\tInteger variables: {num_integer_vars}")
725
1423
  Util.debug(f"\t\tTotal variables: {len(self.variables)}")
1424
+ # Show number of constraints
726
1425
  Util.debug(f"\t\tConstraints: {len(self.constraints)}")
727
1426
  return sol
728
1427
  except gp.GurobiError as e:
729
1428
  Util.error(f"Error code: {e.errno}. {e.message}")
730
1429
  return None
731
1430
 
732
- def __handle_model_infeasibility(self, model: gp.Model) -> None:
733
- model.computeIIS()
734
- # Print out the IIS constraints and variables
735
- Util.debug("The following constraints and variables are in the IIS:")
736
- Util.debug("Constraints:")
737
- for c in model.getConstrs():
738
- assert isinstance(c, gp.Constr)
739
- if c.IISConstr:
740
- Util.debug(f"\t\t{c.ConstrName}: {model.getRow(c)} {c.Sense} {c.RHS}")
741
-
742
- Util.debug("Variables:")
743
- for v in model.getVars():
744
- if v.IISLB:
745
- Util.debug(f"\t\t{v.VarName} ≥ {v.LB}")
746
- if v.IISUB:
747
- Util.debug(f"\t\t{v.VarName} ≤ {v.UB}")
748
-
749
- Util.debug("Relaxing the variable bounds:")
750
- # relaxing only variable bounds
751
- model.feasRelaxS(0, False, True, False)
752
- # for relaxing variable bounds and constraint bounds use
753
- # model.feasRelaxS(0, False, True, True)
754
- model.optimize()
1431
+ # def __gurobi_handle_model_infeasibility(self, model: typing.Any) -> None:
1432
+ # import gurobipy as gp
1433
+
1434
+ # model: gp.Model = typing.cast(gp.Model, model)
1435
+ # model.computeIIS()
1436
+ # # Print out the IIS constraints and variables
1437
+ # Util.debug("The following constraints and variables are in the IIS:")
1438
+ # Util.debug("Constraints:")
1439
+ # for c in model.getConstrs():
1440
+ # assert isinstance(c, gp.Constr)
1441
+ # if c.IISConstr:
1442
+ # Util.debug(f"\t\t{c.ConstrName}: {model.getRow(c)} {c.Sense} {c.RHS}")
1443
+
1444
+ # Util.debug("Variables:")
1445
+ # for v in model.getVars():
1446
+ # if v.IISLB:
1447
+ # Util.debug(f"\t\t{v.VarName} ≥ {v.LB}")
1448
+ # if v.IISUB:
1449
+ # Util.debug(f"\t\t{v.VarName} {v.UB}")
1450
+
1451
+ # Util.debug("Relaxing the variable bounds:")
1452
+ # # relaxing only variable bounds
1453
+ # model.feasRelaxS(0, False, True, False)
1454
+ # # for relaxing variable bounds and constraint bounds use
1455
+ # # model.feasRelaxS(0, False, True, True)
1456
+ # model.optimize()
1457
+
1458
+ def solve_mip(self, objective: Expression) -> typing.Optional[Solution]:
1459
+ import mip
1460
+
1461
+ try:
1462
+ Util.debug(f"Objective function -> {objective}")
1463
+
1464
+ num_binary_vars: int = 0
1465
+ num_free_vars: int = 0
1466
+ num_integer_vars: int = 0
1467
+ num_up_vars: int = 0
1468
+ size: int = len(self.variables)
1469
+ objective_value: list[float] = [0.0] * size
1470
+
1471
+ if objective is not None:
1472
+ for term in objective.get_terms():
1473
+ index = self.variables.index(term.get_var())
1474
+ objective_value[index] += term.get_coeff()
1475
+
1476
+ model: mip.Model = mip.Model(
1477
+ name="FuzzyDL", sense=mip.MINIMIZE, solver_name=mip.CBC
1478
+ )
1479
+ model.verbose = 0
1480
+ model.infeas_tol = 1e-9
1481
+ model.integer_tol = 1e-9
1482
+ model.max_mip_gap = ConfigReader.EPSILON
1483
+ model.emphasis = mip.SearchEmphasis.OPTIMALITY
1484
+ model.opt_tol = 0
1485
+ model.preprocess = 1
1486
+
1487
+ if ConfigReader.DEBUG_PRINT:
1488
+ model.verbose = 1
1489
+
1490
+ vars_mip: dict[str, mip.Var] = dict()
1491
+ show_variable: list[bool] = [False] * size
1492
+
1493
+ my_vars: list[Variable] = self.show_vars.get_variables()
1494
+ var_types: dict[VariableType, str] = {
1495
+ VariableType.BINARY: mip.BINARY,
1496
+ VariableType.INTEGER: mip.INTEGER,
1497
+ VariableType.CONTINUOUS: mip.CONTINUOUS,
1498
+ VariableType.SEMI_CONTINUOUS: mip.CONTINUOUS,
1499
+ }
1500
+ var_name_map: dict[str, str] = {
1501
+ str(v): f"x{i}" for i, v in enumerate(self.variables)
1502
+ }
1503
+
1504
+ for i, curr_variable in enumerate(self.variables):
1505
+ v_type: VariableType = curr_variable.get_type()
1506
+ ov: float = objective_value[i]
1507
+
1508
+ Util.debug(
1509
+ (
1510
+ f"Variable -- "
1511
+ f"[{curr_variable.get_lower_bound()}, {curr_variable.get_upper_bound()}] - "
1512
+ f"Obj value = {ov} - "
1513
+ f"Var type = {v_type.name} -- "
1514
+ f"Var = {curr_variable}"
1515
+ )
1516
+ )
1517
+
1518
+ vars_mip[var_name_map[str(curr_variable)]] = model.add_var(
1519
+ name=var_name_map[str(curr_variable)],
1520
+ var_type=var_types[v_type],
1521
+ lb=curr_variable.get_lower_bound(),
1522
+ ub=curr_variable.get_upper_bound(),
1523
+ obj=ov,
1524
+ )
1525
+
1526
+ if curr_variable in my_vars:
1527
+ show_variable[i] = True
1528
+
1529
+ if v_type == VariableType.BINARY:
1530
+ num_binary_vars += 1
1531
+ elif v_type == VariableType.CONTINUOUS:
1532
+ num_free_vars += 1
1533
+ elif v_type == VariableType.INTEGER:
1534
+ num_integer_vars += 1
1535
+ elif v_type == VariableType.SEMI_CONTINUOUS:
1536
+ num_up_vars += 1
1537
+
1538
+ Util.debug(f"# constraints -> {len(self.constraints)}")
1539
+ constraint_name: str = "constraint"
1540
+ for i, constraint in enumerate(self.constraints):
1541
+ if constraint in self.constraints[:i]:
1542
+ continue
1543
+ if constraint.is_zero():
1544
+ continue
1545
+ curr_name: str = f"{constraint_name}_{i + 1}"
1546
+ expr: mip.LinExpr = mip.xsum(
1547
+ term.get_coeff() * vars_mip[var_name_map[str(term.get_var())]]
1548
+ for term in constraint.get_terms()
1549
+ )
1550
+
1551
+ if constraint.get_type() == InequalityType.EQUAL:
1552
+ gp_constraint: mip.Constr = expr == constraint.get_constant()
1553
+ elif constraint.get_type() == InequalityType.LESS_THAN:
1554
+ gp_constraint: mip.Constr = expr <= constraint.get_constant()
1555
+ elif constraint.get_type() == InequalityType.GREATER_THAN:
1556
+ gp_constraint: mip.Constr = expr >= constraint.get_constant()
1557
+
1558
+ model.add_constr(gp_constraint, curr_name)
1559
+ Util.debug(f"{curr_name}: {constraint}")
1560
+
1561
+ model.objective = mip.xsum(
1562
+ ov * vars_mip[var_name_map[str(self.variables[i])]]
1563
+ for i, ov in enumerate(objective_value)
1564
+ if ov != 0
1565
+ )
1566
+
1567
+ # model.optimize(relax=ConfigReader.RELAX_MILP)
1568
+ model.optimize()
1569
+
1570
+ model.write(os.path.join(constants.RESULTS_PATH, "mip_model.lp"))
1571
+
1572
+ Util.debug(f"Model:")
1573
+ sol: Solution = None
1574
+ if model.status == mip.OptimizationStatus.INFEASIBLE:
1575
+ sol = Solution(Solution.INCONSISTENT_KB)
1576
+ else:
1577
+ model.write(os.path.join(constants.RESULTS_PATH, "mip_solution.sol"))
1578
+ result: float = Util.round(abs(model.objective_value))
1579
+ sol = Solution(result)
1580
+ for i in range(size):
1581
+ if ConfigReader.DEBUG_PRINT or show_variable[i]:
1582
+ name: str = self.variables[i].name
1583
+ value: float = round(vars_mip[var_name_map[name]].x, 6)
1584
+ if show_variable[i]:
1585
+ sol.add_showed_variable(name, value)
1586
+ # if self.PRINT_VARIABLES:
1587
+ Util.debug(f"{name} = {value}")
1588
+ if self.PRINT_LABELS:
1589
+ self.print_instance_of_labels(name, value)
1590
+
1591
+ Util.debug(
1592
+ f"{constants.STAR_SEPARATOR}Statistics{constants.STAR_SEPARATOR}"
1593
+ )
1594
+ Util.debug("MILP problem:")
1595
+ Util.debug(f"\t\tSemi continuous variables: {num_up_vars}")
1596
+ Util.debug(f"\t\tBinary variables: {num_binary_vars}")
1597
+ Util.debug(f"\t\tContinuous variables: {num_free_vars}")
1598
+ Util.debug(f"\t\tInteger variables: {num_integer_vars}")
1599
+ Util.debug(f"\t\tTotal variables: {len(self.variables)}")
1600
+ Util.debug(f"\t\tConstraints: {len(self.constraints)}")
1601
+ return sol
1602
+ except Exception as e:
1603
+ Util.error(f"Error: {e} {traceback.format_exc()}")
1604
+ return None
1605
+
1606
+ def solve_pulp(self, objective: Expression) -> typing.Optional[Solution]:
1607
+ import pulp
1608
+
1609
+ try:
1610
+ Util.debug(f"Objective function -> {objective}")
1611
+
1612
+ num_binary_vars: int = 0
1613
+ num_free_vars: int = 0
1614
+ num_integer_vars: int = 0
1615
+ num_up_vars: int = 0
1616
+ size: int = len(self.variables)
1617
+ objective_value: list[float] = [0.0] * size
1618
+ show_variable: list[bool] = [False] * size
1619
+ my_vars: list[Variable] = self.show_vars.get_variables()
1620
+
1621
+ if objective is not None:
1622
+ for term in objective.get_terms():
1623
+ objective_value[
1624
+ self.variables.index(term.get_var())
1625
+ ] += term.get_coeff()
1626
+
1627
+ model = pulp.LpProblem(
1628
+ f"FuzzyDL-{ConfigReader.MILP_PROVIDER.upper()}", pulp.LpMinimize
1629
+ )
1630
+
1631
+ var_types: dict[VariableType, str] = {
1632
+ VariableType.BINARY: pulp.LpBinary,
1633
+ VariableType.INTEGER: pulp.LpInteger,
1634
+ VariableType.CONTINUOUS: pulp.LpContinuous,
1635
+ VariableType.SEMI_CONTINUOUS: pulp.LpContinuous,
1636
+ }
1637
+
1638
+ vars_pulp: dict[str, pulp.LpVariable] = dict()
1639
+ var_name_map: dict[str, str] = {
1640
+ str(v): f"x{i}" for i, v in enumerate(self.variables)
1641
+ }
1642
+ semicontinuous_var_counter: int = 1
1643
+ semicontinuous_var_name: str = "semic_z"
1644
+ for i, curr_variable in enumerate(self.variables):
1645
+ v_type: VariableType = curr_variable.get_type()
1646
+ Util.debug(
1647
+ (
1648
+ f"Variable -- "
1649
+ f"[{curr_variable.get_lower_bound()}, {curr_variable.get_upper_bound()}] - "
1650
+ f"Obj value = {objective_value[i]} - "
1651
+ f"Var type = {v_type.name} -- "
1652
+ f"Var = {curr_variable}"
1653
+ )
1654
+ )
1655
+
1656
+ vars_pulp[var_name_map[str(curr_variable)]] = pulp.LpVariable(
1657
+ name=var_name_map[str(curr_variable)],
1658
+ lowBound=(
1659
+ curr_variable.get_lower_bound()
1660
+ if curr_variable.get_lower_bound() != float("-inf")
1661
+ else None
1662
+ ),
1663
+ upBound=(
1664
+ curr_variable.get_upper_bound()
1665
+ if curr_variable.get_upper_bound() != float("inf")
1666
+ else None
1667
+ ),
1668
+ cat=var_types[v_type],
1669
+ )
1670
+
1671
+ if curr_variable in my_vars:
1672
+ show_variable[i] = True
1673
+
1674
+ if (
1675
+ v_type == VariableType.SEMI_CONTINUOUS
1676
+ and ConfigReader.MILP_PROVIDER
1677
+ in [
1678
+ MILPProvider.PULP_GLPK,
1679
+ MILPProvider.PULP_CPLEX,
1680
+ ]
1681
+ ):
1682
+ # Semi Continuous variables are not handled by GLPK and HiGHS
1683
+ # if x in [L, U] u {0} is semi continuous, then add the following constraints
1684
+ # L * y <= x <= U * y, where y in {0, 1} is a binary variable
1685
+ bin_var = pulp.LpVariable(
1686
+ name=f"{semicontinuous_var_name}{semicontinuous_var_counter}",
1687
+ cat=pulp.LpBinary,
1688
+ )
1689
+ constraint_1 = (
1690
+ vars_pulp[var_name_map[str(curr_variable)]]
1691
+ >= bin_var * curr_variable.get_lower_bound()
1692
+ )
1693
+ constraint_2 = (
1694
+ vars_pulp[var_name_map[str(curr_variable)]]
1695
+ <= bin_var * curr_variable.get_upper_bound()
1696
+ )
1697
+ if constraint_1 not in model.constraints.values():
1698
+ model.addConstraint(
1699
+ constraint_1, name=f"constraint_{bin_var.name}_1"
1700
+ )
1701
+ if constraint_2 not in model.constraints.values():
1702
+ model.addConstraint(
1703
+ constraint_2, name=f"constraint_{bin_var.name}_2"
1704
+ )
1705
+ semicontinuous_var_counter += 1
1706
+ Util.debug(
1707
+ (
1708
+ f"New Variable -- "
1709
+ f"[{bin_var.lowBound}, {bin_var.upBound}] - "
1710
+ f"Var type = {bin_var.cat} -- "
1711
+ f"Var = {bin_var.name}"
1712
+ )
1713
+ )
1714
+ Util.debug(f"New Constraint 1 -- {constraint_1}")
1715
+ Util.debug(f"New Constraint 2 -- {constraint_2}")
1716
+
1717
+ if v_type == VariableType.BINARY:
1718
+ num_binary_vars += 1
1719
+ elif v_type == VariableType.CONTINUOUS:
1720
+ num_free_vars += 1
1721
+ elif v_type == VariableType.INTEGER:
1722
+ num_integer_vars += 1
1723
+ elif v_type == VariableType.SEMI_CONTINUOUS:
1724
+ num_up_vars += 1
1725
+
1726
+ Util.debug(f"# constraints -> {len(self.constraints)}")
1727
+ constraint_name: str = "constraint"
1728
+ pulp_sense: dict[InequalityType, int] = {
1729
+ InequalityType.EQUAL: pulp.LpConstraintEQ,
1730
+ InequalityType.LESS_THAN: pulp.LpConstraintLE,
1731
+ InequalityType.GREATER_THAN: pulp.LpConstraintGE,
1732
+ }
1733
+ for i, constraint in enumerate(self.constraints):
1734
+ if constraint in self.constraints[:i]:
1735
+ continue
1736
+ # ignore zero constraints
1737
+ if constraint.is_zero():
1738
+ continue
1739
+
1740
+ curr_name: str = f"{constraint_name}_{i + 1}"
1741
+ pulp_expr: pulp.LpAffineExpression = pulp.lpSum(
1742
+ term.get_coeff() * vars_pulp[var_name_map[str(term.get_var())]]
1743
+ for term in constraint.get_terms()
1744
+ )
1745
+ pulp_constraint: pulp.LpConstraint = pulp.LpConstraint(
1746
+ e=pulp_expr,
1747
+ sense=pulp_sense[constraint.get_type()],
1748
+ rhs=constraint.get_constant(),
1749
+ )
1750
+
1751
+ # ignore zero constraints of type a * x - a * x
1752
+ if (
1753
+ len(pulp_constraint) == 1
1754
+ and list(pulp_constraint.values())[0] == 0
1755
+ and pulp_constraint.constant == 0
1756
+ ):
1757
+ continue
1758
+
1759
+ model.addConstraint(pulp_constraint, name=curr_name)
1760
+ Util.debug(f"{curr_name}: {constraint}")
1761
+
1762
+ if ConfigReader.MILP_PROVIDER == MILPProvider.PULP:
1763
+ solver = pulp.PULP_CBC_CMD(
1764
+ mip=True,
1765
+ msg=ConfigReader.DEBUG_PRINT,
1766
+ gapRel=1e-9,
1767
+ presolve=True,
1768
+ keepFiles=False, # ConfigReader.DEBUG_PRINT,
1769
+ logPath=(
1770
+ os.path.join(".", "logs", f"pulp_{pulp.PULP_CBC_CMD.name}.log")
1771
+ if ConfigReader.DEBUG_PRINT
1772
+ else None
1773
+ ),
1774
+ options=[
1775
+ "--primalTolerance", # feasibility tolerance
1776
+ "1e-9",
1777
+ "--integerTolerance", # integer feasibility tolerance
1778
+ "1e-9",
1779
+ "--ratioGap", # relative mip gap
1780
+ str(ConfigReader.EPSILON),
1781
+ "--allowableGap", # optimality gap tolerance
1782
+ "0",
1783
+ "--preprocess", # enable preprocessing
1784
+ "on",
1785
+ ],
1786
+ )
1787
+ elif ConfigReader.MILP_PROVIDER == MILPProvider.PULP_GLPK:
1788
+ solver = pulp.GLPK_CMD(
1789
+ mip=True,
1790
+ msg=ConfigReader.DEBUG_PRINT,
1791
+ keepFiles=False, # ConfigReader.DEBUG_PRINT,
1792
+ options=[
1793
+ "--presol", # use presolver (default; assumes --scale and --adv)
1794
+ "--exact", # use simplex method based on exact arithmetic
1795
+ "--xcheck", # check final basis using exact arithmetic
1796
+ "--intopt", # enforce MIP (Mixed Integer Programming)
1797
+ "--mipgap",
1798
+ str(
1799
+ ConfigReader.EPSILON
1800
+ ), # no relative gap between primal & best bound
1801
+ ]
1802
+ + (
1803
+ [
1804
+ "--log",
1805
+ os.path.join(".", "logs", f"pulp_{pulp.GLPK_CMD.name}.log"),
1806
+ ]
1807
+ if ConfigReader.DEBUG_PRINT
1808
+ else []
1809
+ ),
1810
+ )
1811
+ elif ConfigReader.MILP_PROVIDER == MILPProvider.PULP_HIGHS:
1812
+ solver = pulp.HiGHS(
1813
+ mip=True,
1814
+ msg=ConfigReader.DEBUG_PRINT,
1815
+ gapRel=1e-6,
1816
+ log_file=(
1817
+ os.path.join(".", "logs", f"pulp_{pulp.HiGHS.name}.log")
1818
+ if ConfigReader.DEBUG_PRINT
1819
+ else None
1820
+ ),
1821
+ primal_feasibility_tolerance=1e-9,
1822
+ dual_feasibility_tolerance=1e-9,
1823
+ mip_feasibility_tolerance=1e-9,
1824
+ presolve="on",
1825
+ parallel="on",
1826
+ write_solution_to_file=True,
1827
+ write_solution_style=1,
1828
+ solution_file=os.path.join(
1829
+ constants.RESULTS_PATH, "highs_solution.sol"
1830
+ ),
1831
+ write_model_file=os.path.join(
1832
+ constants.RESULTS_PATH, "highs_model.lp"
1833
+ ),
1834
+ )
1835
+ elif ConfigReader.MILP_PROVIDER == MILPProvider.PULP_CPLEX:
1836
+ solver = pulp.CPLEX_CMD(
1837
+ # path="/Applications/CPLEX_Studio2211/cplex/bin/arm64_osx/cplex",
1838
+ mip=True,
1839
+ msg=ConfigReader.DEBUG_PRINT,
1840
+ gapRel=1e-9,
1841
+ keepFiles=False, # ConfigReader.DEBUG_PRINT,
1842
+ logPath=(
1843
+ os.path.join(".", "logs", f"pulp_{pulp.CPLEX_CMD.name}.log")
1844
+ if ConfigReader.DEBUG_PRINT
1845
+ else None
1846
+ ),
1847
+ )
1848
+
1849
+ model.objective = pulp.lpSum(
1850
+ ov * vars_pulp[var_name_map[str(self.variables[i])]]
1851
+ for i, ov in enumerate(objective_value)
1852
+ if ov != 0
1853
+ )
1854
+ result = model.solve(solver=solver)
1855
+ if ConfigReader.MILP_PROVIDER == MILPProvider.PULP_CPLEX:
1856
+ for file in os.listdir("./"):
1857
+ if "clone" in file:
1858
+ os.remove(file)
1859
+
1860
+ Util.debug(f"Model:")
1861
+ sol: Solution = None
1862
+ if result != pulp.LpStatusOptimal:
1863
+ sol = Solution(Solution.INCONSISTENT_KB)
1864
+ else:
1865
+ result: float = Util.round(abs(model.objective.value()))
1866
+ sol = Solution(result)
1867
+ var_dict: dict[str, pulp.LpVariable] = model.variablesDict()
1868
+ for i in range(size):
1869
+ if ConfigReader.DEBUG_PRINT or show_variable[i]:
1870
+ name: str = self.variables[i].name
1871
+ value: float = (
1872
+ round(var_dict[var_name_map[name]].value(), 6)
1873
+ if var_name_map[name] in var_dict
1874
+ else 0.0
1875
+ )
1876
+ if show_variable[i]:
1877
+ sol.add_showed_variable(name, value)
1878
+ # if self.PRINT_VARIABLES:
1879
+ Util.debug(f"{name} = {value}")
1880
+ if self.PRINT_LABELS:
1881
+ self.print_instance_of_labels(name, value)
1882
+
1883
+ Util.debug(
1884
+ f"{constants.STAR_SEPARATOR}Statistics{constants.STAR_SEPARATOR}"
1885
+ )
1886
+ Util.debug("MILP problem:")
1887
+ Util.debug(f"\t\tSemi continuous variables: {num_up_vars}")
1888
+ Util.debug(f"\t\tBinary variables: {num_binary_vars}")
1889
+ Util.debug(f"\t\tContinuous variables: {num_free_vars}")
1890
+ Util.debug(f"\t\tInteger variables: {num_integer_vars}")
1891
+ Util.debug(f"\t\tTotal variables: {len(self.variables)}")
1892
+ Util.debug(f"\t\tConstraints: {len(self.constraints)}")
1893
+ return sol
1894
+ except Exception as e:
1895
+ Util.error(f"Error: {e} {traceback.format_exc()}")
1896
+ return None
1897
+
1898
+ # def solve_scipy(self, objective: Expression) -> typing.Optional[Solution]:
1899
+ # import numpy as np
1900
+ # from scipy.optimize import milp, OptimizeResult, LinearConstraint, Bounds, linprog, linprog_verbose_callback, show_options
1901
+
1902
+ # num_binary_vars: int = 0
1903
+ # num_free_vars: int = 0
1904
+ # num_integer_vars: int = 0
1905
+ # num_up_vars: int = 0
1906
+ # size: int = len(self.variables)
1907
+ # objective_value: list[float] = [0.0] * size
1908
+ # show_variable: list[bool] = [False] * size
1909
+ # my_vars: list[Variable] = self.show_vars.get_variables()
1910
+
1911
+ # if objective is not None:
1912
+ # for term in objective.get_terms():
1913
+ # index = self.variables.index(term.get_var())
1914
+ # objective_value[index] += term.get_coeff()
1915
+
1916
+ # var_types: dict[VariableType, str] = {
1917
+ # VariableType.BINARY: 1,
1918
+ # VariableType.CONTINUOUS: 0,
1919
+ # VariableType.INTEGER: 1,
1920
+ # VariableType.SEMI_CONTINUOUS: 2,
1921
+ # }
1922
+
1923
+ # for i, curr_variable in enumerate(self.variables):
1924
+ # v_type: VariableType = curr_variable.get_type()
1925
+
1926
+ # Util.debug(
1927
+ # (
1928
+ # f"Variable -- "
1929
+ # f"[{curr_variable.get_lower_bound()}, {curr_variable.get_upper_bound()}] - "
1930
+ # f"Obj value = {objective_value[i]} - "
1931
+ # f"Var type = {v_type.name} -- "
1932
+ # f"Var = {curr_variable}"
1933
+ # )
1934
+ # )
1935
+
1936
+ # if curr_variable in my_vars:
1937
+ # show_variable[i] = True
1938
+
1939
+ # if v_type == VariableType.BINARY:
1940
+ # num_binary_vars += 1
1941
+ # elif v_type == VariableType.CONTINUOUS:
1942
+ # num_free_vars += 1
1943
+ # elif v_type == VariableType.INTEGER:
1944
+ # num_integer_vars += 1
1945
+ # elif v_type == VariableType.SEMI_CONTINUOUS:
1946
+ # num_up_vars += 1
1947
+
1948
+ # Util.debug(f"# constraints -> {len(self.constraints)}")
1949
+ # constraint_name: str = "constraint"
1950
+ # matrix_A = np.zeros((len(self.constraints), len(self.variables)))
1951
+ # inequality_A = np.zeros((len(self.constraints), len(self.variables)))
1952
+ # equality_A = np.zeros((len(self.constraints), len(self.variables)))
1953
+ # lb = np.zeros(len(self.constraints))
1954
+ # ub = np.zeros(len(self.constraints))
1955
+ # in_ub = np.zeros(len(self.constraints))
1956
+ # eq_ub = np.zeros(len(self.constraints))
1957
+ # for i, constraint in enumerate(self.constraints):
1958
+ # curr_name: str = f"{constraint_name}_{i + 1}"
1959
+ # row = np.zeros(len(self.variables))
1960
+ # for term in constraint.get_terms():
1961
+ # row[self.variables.index(term.get_var())] = term.get_coeff()
1962
+ # if np.allclose(row, 0):
1963
+ # continue
1964
+ # Util.debug(f"{curr_name}: {constraint}")
1965
+ # matrix_A[i, :] = row
1966
+ # if constraint.type == InequalityType.EQUAL:
1967
+ # equality_A[i, :] = row
1968
+ # eq_ub[i] = constraint.get_constant()
1969
+
1970
+ # lb[i] = constraint.get_constant()
1971
+ # ub[i] = constraint.get_constant()
1972
+ # elif constraint.type == InequalityType.LESS_THAN:
1973
+ # inequality_A[i, :] = row
1974
+ # in_ub[i] = constraint.get_constant()
1975
+
1976
+ # lb[i] = -np.inf
1977
+ # ub[i] = constraint.get_constant()
1978
+ # elif constraint.type == InequalityType.GREATER_THAN:
1979
+ # inequality_A[i, :] = -row
1980
+ # in_ub[i] = -constraint.get_constant()
1981
+
1982
+ # lb[i] = constraint.get_constant()
1983
+ # ub[i] = np.inf
1984
+
1985
+ # indices = np.all(matrix_A == 0, axis=1)
1986
+ # matrix_A = np.delete(matrix_A, indices, axis=0)
1987
+ # lb = np.delete(lb, indices, axis=0)
1988
+ # ub = np.delete(ub, indices, axis=0)
1989
+
1990
+ # indices = np.all(inequality_A == 0, axis=1)
1991
+ # inequality_A = np.delete(inequality_A, indices, axis=0)
1992
+ # in_ub = np.delete(in_ub, indices, axis=0)
1993
+
1994
+ # indices = np.all(equality_A == 0, axis=1)
1995
+ # equality_A = np.delete(equality_A, indices, axis=0)
1996
+ # eq_ub = np.delete(eq_ub, indices, axis=0)
1997
+
1998
+ # bounds = Bounds(
1999
+ # [var.get_lower_bound() for var in self.variables],
2000
+ # [var.get_upper_bound() for var in self.variables],
2001
+ # keep_feasible=True,
2002
+ # )
2003
+ # integrality = np.array([var_types[var.get_type()] for var in self.variables])
2004
+ # constraint = LinearConstraint(
2005
+ # matrix_A, lb, ub, keep_feasible=True
2006
+ # )
2007
+
2008
+ # result: OptimizeResult = milp(
2009
+ # c=np.array(objective_value),
2010
+ # integrality=integrality,
2011
+ # constraints=constraint,
2012
+ # bounds=bounds,
2013
+ # options={
2014
+ # "disp": ConfigReader.DEBUG_PRINT,
2015
+ # "presolve": True,
2016
+ # "mip_rel_gap": 1e-6,
2017
+ # },
2018
+ # )
2019
+
2020
+ # result: OptimizeResult = linprog(
2021
+ # c=np.array(objective_value),
2022
+ # A_ub=inequality_A,
2023
+ # b_ub=in_ub,
2024
+ # A_eq=equality_A,
2025
+ # b_eq=eq_ub,
2026
+ # method="highs-ipm",
2027
+ # integrality=integrality,
2028
+ # bounds=[(var.get_lower_bound(), var.get_upper_bound()) for var in self.variables],
2029
+ # options={
2030
+ # "disp": ConfigReader.DEBUG_PRINT,
2031
+ # "presolve": False,
2032
+ # "mip_rel_gap": 1e-3,
2033
+ # "ipm_optimality_tolerance": 1e-5,
2034
+ # },
2035
+ # # callback=linprog_verbose_callback if ConfigReader.DEBUG_PRINT else None
2036
+ # )
2037
+
2038
+ # Util.debug(f"Model:\n{result}")
2039
+
2040
+ # sol: Solution = None
2041
+ # if not result.success:
2042
+ # sol = Solution(Solution.INCONSISTENT_KB)
2043
+ # else:
2044
+ # for i in range(size):
2045
+ # if ConfigReader.DEBUG_PRINT or show_variable[i]:
2046
+ # name: str = self.variables[i].name
2047
+ # value: float = (
2048
+ # round(result.x[i], 6)
2049
+ # )
2050
+ # if self.PRINT_VARIABLES:
2051
+ # Util.debug(f"{name} = {value}")
2052
+ # if self.PRINT_LABELS:
2053
+ # self.print_instance_of_labels(name, value)
2054
+ # result: float = Util.round(abs(result.fun))
2055
+ # sol = Solution(result)
2056
+
2057
+ # Util.debug(
2058
+ # f"{constants.STAR_SEPARATOR}Statistics{constants.STAR_SEPARATOR}"
2059
+ # )
2060
+ # Util.debug("MILP problem:")
2061
+ # Util.debug(f"\t\tSemi continuous variables: {num_up_vars}")
2062
+ # Util.debug(f"\t\tBinary variables: {num_binary_vars}")
2063
+ # Util.debug(f"\t\tContinuous variables: {num_free_vars}")
2064
+ # Util.debug(f"\t\tInteger variables: {num_integer_vars}")
2065
+ # Util.debug(f"\t\tTotal variables: {len(self.variables)}")
2066
+ # Util.debug(f"\t\tConstraints: {len(self.constraints)}")
2067
+ # return sol
755
2068
 
756
2069
  def add_crisp_concept(self, concept_name: str) -> None:
2070
+ """Defines a concept to be crisp."""
757
2071
  self.crisp_concepts.add(concept_name)
758
2072
 
759
2073
  def add_crisp_role(self, role_name: str) -> None:
2074
+ """Defines a role to be crisp."""
760
2075
  self.crisp_roles.add(role_name)
761
2076
 
762
2077
  def is_crisp_concept(self, concept_name: str) -> bool:
2078
+ """Checks if a concept is crisp or not."""
763
2079
  return concept_name in self.crisp_concepts
764
2080
 
765
2081
  def is_crisp_role(self, role_name: str) -> bool:
2082
+ """Checks if a role is crisp or not."""
766
2083
  return role_name in self.crisp_roles
767
2084
 
768
2085
  def set_binary_variables(self) -> None:
2086
+ """Transforms every [0,1]-variable into a {0,1} variable."""
2087
+ # set all variables binary, except
2088
+ # - those that hold the value of a datatype filler
2089
+ # - free variables in constraints
769
2090
  for v in self.variables:
770
2091
  if v.get_datatype_filler_type() or v.get_type() in (
771
2092
  VariableType.CONTINUOUS,
@@ -775,14 +2096,31 @@ class MILPHelper:
775
2096
  v.set_binary_variable()
776
2097
 
777
2098
  def get_name_for_integer(self, i: int) -> typing.Optional[str]:
2099
+ """Gets the name of the i-th variable."""
778
2100
  for name, i2 in self.number_of_variables.items():
779
2101
  if i == i2:
780
2102
  return name
781
2103
  return None
782
2104
 
783
2105
  def get_number_for_assertion(self, ass: Assertion) -> int:
2106
+ """Gets an integer codification of an assertion."""
784
2107
  return self.number_of_variables.get(str(self.get_variable(ass)))
785
2108
 
786
2109
  def add_contradiction(self) -> None:
2110
+ """Add a contradiction to make the fuzzy KB unsatisfiable"""
787
2111
  self.constraints.clear()
788
2112
  self.add_new_constraint(Expression(1.0), InequalityType.EQUAL)
2113
+
2114
+ def add_cardinality_list(self, sc: SigmaCount) -> None:
2115
+ """
2116
+ SigmaCount(r,C,O,d)^I(w) = d^I(xSigma)
2117
+
2118
+ Args:
2119
+ sc (SigmaCount):
2120
+ xSigma: Free variable taking the value \sigma_{i2 \in O} r(i1, i2) \otimes C(i2)
2121
+ i1: Name of an individual, subject of the relation.
2122
+ O: Set of individuals candidates to be the object of the relation.
2123
+ r: Role.
2124
+ C: Concept.
2125
+ """
2126
+ self.cardinalities.append(sc)