flamapy-fm 2.0.0.dev1__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 (34) hide show
  1. flamapy/metamodels/fm_metamodel/__init__.py +0 -0
  2. flamapy/metamodels/fm_metamodel/models/__init__.py +17 -0
  3. flamapy/metamodels/fm_metamodel/models/feature_model.py +606 -0
  4. flamapy/metamodels/fm_metamodel/operations/__init__.py +20 -0
  5. flamapy/metamodels/fm_metamodel/operations/fm_atomic_sets.py +47 -0
  6. flamapy/metamodels/fm_metamodel/operations/fm_average_branching_factor.py +36 -0
  7. flamapy/metamodels/fm_metamodel/operations/fm_core_features.py +71 -0
  8. flamapy/metamodels/fm_metamodel/operations/fm_count_leafs.py +26 -0
  9. flamapy/metamodels/fm_metamodel/operations/fm_estimated_configurations_number.py +55 -0
  10. flamapy/metamodels/fm_metamodel/operations/fm_feature_ancestors.py +38 -0
  11. flamapy/metamodels/fm_metamodel/operations/fm_generate_random_attribute.py +100 -0
  12. flamapy/metamodels/fm_metamodel/operations/fm_leaf_features.py +27 -0
  13. flamapy/metamodels/fm_metamodel/operations/fm_max_depth_tree.py +26 -0
  14. flamapy/metamodels/fm_metamodel/operations/fm_metrics.py +770 -0
  15. flamapy/metamodels/fm_metamodel/transformations/__init__.py +28 -0
  16. flamapy/metamodels/fm_metamodel/transformations/afm_reader.py +229 -0
  17. flamapy/metamodels/fm_metamodel/transformations/afm_writer.py +140 -0
  18. flamapy/metamodels/fm_metamodel/transformations/clafer_writer.py +151 -0
  19. flamapy/metamodels/fm_metamodel/transformations/featureide_reader.py +185 -0
  20. flamapy/metamodels/fm_metamodel/transformations/featureide_writer.py +138 -0
  21. flamapy/metamodels/fm_metamodel/transformations/glencoe_reader.py +136 -0
  22. flamapy/metamodels/fm_metamodel/transformations/glencoe_writer.py +105 -0
  23. flamapy/metamodels/fm_metamodel/transformations/json_reader.py +145 -0
  24. flamapy/metamodels/fm_metamodel/transformations/json_writer.py +125 -0
  25. flamapy/metamodels/fm_metamodel/transformations/pysat_to_fm.py +69 -0
  26. flamapy/metamodels/fm_metamodel/transformations/splot_writer.py +75 -0
  27. flamapy/metamodels/fm_metamodel/transformations/uvl_reader.py +427 -0
  28. flamapy/metamodels/fm_metamodel/transformations/uvl_writer.py +134 -0
  29. flamapy/metamodels/fm_metamodel/transformations/xml_reader.py +128 -0
  30. flamapy_fm-2.0.0.dev1.dist-info/METADATA +37 -0
  31. flamapy_fm-2.0.0.dev1.dist-info/RECORD +34 -0
  32. flamapy_fm-2.0.0.dev1.dist-info/WHEEL +5 -0
  33. flamapy_fm-2.0.0.dev1.dist-info/dependency_links.txt +1 -0
  34. flamapy_fm-2.0.0.dev1.dist-info/top_level.txt +1 -0
File without changes
@@ -0,0 +1,17 @@
1
+ from .feature_model import (
2
+ Feature,
3
+ Constraint,
4
+ FeatureModel,
5
+ Relation,
6
+ Domain,
7
+ Range,
8
+ Attribute
9
+ )
10
+
11
+ __all__ = ['Constraint',
12
+ 'FeatureModel',
13
+ 'Feature',
14
+ 'Relation',
15
+ 'Domain',
16
+ 'Range',
17
+ 'Attribute']
@@ -0,0 +1,606 @@
1
+ from typing import Any, Optional
2
+ from functools import total_ordering
3
+
4
+ from flamapy.core.exceptions import FlamaException
5
+ from flamapy.core.models import AST, VariabilityModel, VariabilityElement, ASTOperation
6
+ from flamapy.core.models.ast import simplify_formula, propagate_negation, to_cnf
7
+
8
+
9
+ class Relation:
10
+ def __init__(
11
+ self, parent: "Feature", children: list["Feature"], card_min: int, card_max: int
12
+ ) -> None:
13
+ self.parent = parent
14
+ self.children = children
15
+ self.card_min = card_min
16
+ self.card_max = card_max
17
+
18
+ def add_child(self, feature: "Feature") -> None:
19
+ self.children.append(feature)
20
+
21
+ def is_mandatory(self) -> bool:
22
+ return self.card_min == 1 and self.card_max == 1 and len(self.children) == 1
23
+
24
+ def is_optional(self) -> bool:
25
+ return self.card_min == 0 and self.card_max == 1 and len(self.children) == 1
26
+
27
+ def is_or(self) -> bool:
28
+ return (
29
+ self.card_min == 1
30
+ and self.card_max == len(self.children)
31
+ and len(self.children) > 1
32
+ )
33
+
34
+ def is_alternative(self) -> bool:
35
+ return self.card_min == 1 and self.card_max == 1 and len(self.children) > 1
36
+
37
+ def is_mutex(self) -> bool:
38
+ return self.card_min == 0 and self.card_max == 1 and len(self.children) > 1
39
+
40
+ def is_cardinal(self) -> bool:
41
+ return (
42
+ self.is_group()
43
+ and not self.is_alternative()
44
+ and not self.is_or()
45
+ and not self.is_mutex()
46
+ )
47
+
48
+ def is_group(self) -> bool:
49
+ return len(self.children) > 1
50
+
51
+ def __str__(self) -> str:
52
+ parent_name = self.parent.name if self.parent else ""
53
+ res = f"{parent_name}[{self.card_min},{self.card_max}]"
54
+ for _child in self.children:
55
+ res += _child.name + " "
56
+ if self.is_alternative():
57
+ relation_type = "alternative"
58
+ elif self.is_or():
59
+ relation_type = "or"
60
+ elif self.is_mandatory():
61
+ relation_type = "mandatory"
62
+ elif self.is_optional():
63
+ relation_type = "optional"
64
+ elif self.is_mutex():
65
+ relation_type = "mutex"
66
+ elif self.is_cardinal():
67
+ relation_type = "cardinality"
68
+ else:
69
+ relation_type = "Other"
70
+ res = f"({relation_type}) " + res
71
+ return res
72
+
73
+ def __hash__(self) -> int:
74
+ return hash(
75
+ (self.parent, frozenset(self.children), self.card_min, self.card_max)
76
+ )
77
+
78
+ def __eq__(self, other: Any) -> bool:
79
+ return (
80
+ isinstance(other, Relation)
81
+ and self.parent == other.parent
82
+ and sorted(self.children) == sorted(other.children)
83
+ and self.card_min == other.card_min
84
+ and self.card_max == other.card_max
85
+ )
86
+
87
+ def __lt__(self, other: Any) -> bool:
88
+ return str(self) < str(other)
89
+
90
+
91
+ @total_ordering
92
+ class Feature(VariabilityElement):
93
+
94
+ def __init__(
95
+ self,
96
+ name: str,
97
+ relations: Optional[list["Relation"]] = None,
98
+ parent: Optional["Feature"] = None,
99
+ is_abstract: bool = False,
100
+ ):
101
+ super().__init__(name)
102
+ self.name = name
103
+ self.relations = [] if relations is None else relations
104
+ self.parent = self._get_parent() if parent is None else parent
105
+ self.is_abstract = is_abstract
106
+ self.attributes = list["Attribute"]([])
107
+
108
+ def is_empty(self) -> bool:
109
+ return self.parent is None and self.relations == []
110
+
111
+ def add_relation(self, relation: "Relation") -> None:
112
+ self.relations.append(relation)
113
+ for child in relation.children:
114
+ child.parent = self
115
+
116
+ def add_attribute(self, attribute: "Attribute") -> None:
117
+ attribute.parent = self
118
+ self.attributes.append(attribute)
119
+
120
+ def get_attributes(self) -> list["Attribute"]:
121
+ return self.attributes
122
+
123
+ def set_attributes(self, attributes: list["Attribute"]) -> None:
124
+ self.attributes = attributes
125
+
126
+ def get_relations(self) -> list["Relation"]:
127
+ return self.relations
128
+
129
+ def get_parent(self) -> Optional["Feature"]:
130
+ return self.parent
131
+
132
+ def _get_parent(self) -> Optional["Feature"]:
133
+ return next((r.parent for r in self.get_relations() if not r.children), None)
134
+
135
+ def get_children(self) -> list["Feature"]:
136
+ """Direct children of the feature regardless the relation type."""
137
+ return [f for r in self.get_relations() for f in r.children]
138
+
139
+ def is_root(self) -> bool:
140
+ return self.parent is None
141
+
142
+ def is_mandatory(self) -> bool:
143
+ return self.parent is not None and any(
144
+ r.is_mandatory() and self in r.children for r in self.parent.get_relations()
145
+ )
146
+
147
+ def is_optional(self) -> bool:
148
+ return self.parent is not None and any(
149
+ r.is_optional() and self in r.children for r in self.parent.get_relations()
150
+ )
151
+
152
+ def is_or_group(self) -> bool:
153
+ return any(r.is_or() for r in self.get_relations())
154
+
155
+ def is_alternative_group(self) -> bool:
156
+ return any(r.is_alternative() for r in self.get_relations())
157
+
158
+ def is_mutex_group(self) -> bool:
159
+ return any(r.is_mutex() for r in self.get_relations())
160
+
161
+ def is_cardinality_group(self) -> bool:
162
+ return any(r.is_cardinal() for r in self.get_relations())
163
+
164
+ def is_group(self) -> bool:
165
+ return any(r.is_group() for r in self.get_relations())
166
+
167
+ def is_multiple_group_decomposition(self) -> bool:
168
+ return sum(r.is_group() for r in self.get_relations()) > 1
169
+
170
+ def is_leaf(self) -> bool:
171
+ return len(self.get_relations()) == 0
172
+
173
+ def __str__(self) -> str:
174
+ return self.name
175
+
176
+ def __hash__(self) -> int:
177
+ return hash(self.name)
178
+
179
+ def __eq__(self, other: Any) -> bool:
180
+ return isinstance(other, Feature) and self.name == other.name
181
+
182
+ def __lt__(self, other: Any) -> bool:
183
+ return str(self) < str(other)
184
+
185
+
186
+ class Constraint:
187
+ def __init__(self, name: str, ast: AST):
188
+ self.name = name
189
+ self._ast = ast
190
+
191
+ @property
192
+ def ast(self) -> AST:
193
+ return self._ast
194
+
195
+ @ast.setter
196
+ def ast(self, ast: AST) -> None:
197
+ self._ast = ast
198
+
199
+ def get_features(self) -> list[str]:
200
+ """List of features' names involved in the constraint."""
201
+ features = set()
202
+ stack = [self.ast.root]
203
+ while stack:
204
+ node = stack.pop()
205
+ if node.is_unique_term():
206
+ features.add(node.data)
207
+ elif node.is_unary_op():
208
+ stack.append(node.left)
209
+ elif node.is_binary_op():
210
+ stack.append(node.right)
211
+ stack.append(node.left)
212
+ return list(features)
213
+
214
+ def is_simple_constraint(self) -> bool:
215
+ """Return true if the constraint is a simple constraint (requires or excludes)."""
216
+ return self.is_requires_constraint() or self.is_excludes_constraint()
217
+
218
+ def is_complex_constraint(self) -> bool:
219
+ """Return true if the constraint is a complex constraint
220
+ (i.e., it is not a simple constraint)."""
221
+ return not self.is_simple_constraint()
222
+
223
+ def is_requires_constraint(self) -> bool:
224
+ """Return true if the constraint is a requires constraint."""
225
+ root_op = self._ast.root
226
+ if root_op.is_binary_op():
227
+ if root_op.data in [ASTOperation.REQUIRES, ASTOperation.IMPLIES]:
228
+ return root_op.left.is_term() and root_op.right.is_term()
229
+
230
+ if root_op.data == ASTOperation.OR:
231
+ neg_left = (
232
+ root_op.left.data == ASTOperation.NOT
233
+ and root_op.left.left.is_term()
234
+ )
235
+ neg_right = (
236
+ root_op.right.data == ASTOperation.NOT
237
+ and root_op.right.left.is_term()
238
+ )
239
+ return (
240
+ neg_left
241
+ and root_op.right.is_term()
242
+ or neg_right
243
+ and root_op.left.is_term()
244
+ )
245
+ return False
246
+
247
+ def is_excludes_constraint(self) -> bool:
248
+ """Return true if the constraint is an excludes constraint."""
249
+ root_op = self._ast.root
250
+ if root_op.is_binary_op():
251
+ if root_op.data in [ASTOperation.EXCLUDES, ASTOperation.XOR]:
252
+ return root_op.left.is_term() and root_op.right.is_term()
253
+
254
+ if root_op.data in [ASTOperation.REQUIRES, ASTOperation.IMPLIES]:
255
+ neg_right = (
256
+ root_op.right.data == ASTOperation.NOT
257
+ and root_op.right.left.is_term()
258
+ )
259
+ return root_op.left.is_term() and neg_right
260
+
261
+ if root_op.data == ASTOperation.OR:
262
+ neg_left = (
263
+ root_op.left.data == ASTOperation.NOT
264
+ and root_op.left.left.is_term()
265
+ )
266
+ neg_right = (
267
+ root_op.right.data == ASTOperation.NOT
268
+ and root_op.right.left.is_term()
269
+ )
270
+ return neg_left and neg_right
271
+ return False
272
+
273
+ def is_pseudocomplex_constraint(self) -> bool:
274
+ """Return true if the constraint is a pseudo-complex constraint
275
+ (i.e., it can be transformed to a set of simple constraints)."""
276
+ split_ctcs = split_constraint(self)
277
+ return len(split_ctcs) > 1 and all(
278
+ self.is_simple_constraint() for ctc in split_ctcs
279
+ )
280
+
281
+ def is_strictcomplex_constraint(self) -> bool:
282
+ """Return true if the constraint is a strict-complex constraint
283
+ (i.e., it cannot be transformed to a set of simple constraints)."""
284
+ split_ctcs = split_constraint(self)
285
+ return any(self.is_complex_constraint() for ctc in split_ctcs)
286
+
287
+ def __str__(self) -> str:
288
+ return f"({self.name}) {str(self.ast)}"
289
+
290
+ def __hash__(self) -> int:
291
+ return hash(str(self.ast).lower())
292
+
293
+ def __eq__(self, other: Any) -> bool:
294
+ return (
295
+ isinstance(other, Constraint)
296
+ and str(self.ast).lower() == str(other.ast).lower()
297
+ )
298
+
299
+ def __lt__(self, other: Any) -> bool:
300
+ return str(self.ast).lower() < str(other.ast).lower()
301
+
302
+
303
+ class FeatureModel(VariabilityModel):
304
+ @staticmethod
305
+ def get_extension() -> str:
306
+ return "fm"
307
+
308
+ def __init__(
309
+ self, root: "Feature", constraints: Optional[list["Constraint"]] = None
310
+ ) -> None:
311
+ self.root = root
312
+ self.ctcs = [] if constraints is None else constraints
313
+
314
+ def get_relations(self, feature: Optional["Feature"] = None) -> list["Relation"]:
315
+ if self.root is None or self.root.is_empty():
316
+ return []
317
+ if feature is None:
318
+ feature = self.root
319
+ relations = []
320
+ for relation in feature.relations:
321
+ relations.append(relation)
322
+ for _feature in relation.children:
323
+ relations.extend(self.get_relations(_feature))
324
+ return relations
325
+
326
+ def get_features(self) -> list["Feature"]:
327
+ features: list["Feature"] = []
328
+ if self.root is not None:
329
+ features.append(self.root)
330
+ for relation in self.get_relations():
331
+ features.extend(relation.children)
332
+ return features
333
+
334
+ def get_constraints(self) -> list["Constraint"]:
335
+ return self.ctcs
336
+
337
+ def get_mandatory_features(self) -> list["Feature"]:
338
+ return [f for f in self.get_features() if f.is_mandatory()]
339
+
340
+ def get_optional_features(self) -> list["Feature"]:
341
+ return [f for f in self.get_features() if f.is_optional()]
342
+
343
+ def get_alternative_group_features(self) -> list["Feature"]:
344
+ return [f for f in self.get_features() if f.is_alternative_group()]
345
+
346
+ def get_or_group_features(self) -> list["Feature"]:
347
+ return [f for f in self.get_features() if f.is_or_group()]
348
+
349
+ def get_feature_by_name(self, feature_name: str) -> Optional["Feature"]:
350
+ return next((f for f in self.get_features() if f.name == feature_name), None)
351
+
352
+ def get_complex_constraints(self) -> list["Constraint"]:
353
+ return [c for c in self.get_constraints() if c.is_complex_constraint()]
354
+
355
+ def get_simple_constraints(self) -> list["Constraint"]:
356
+ return [c for c in self.get_constraints() if c.is_simple_constraint()]
357
+
358
+ def get_pseudocomplex_constraints(self) -> list["Constraint"]:
359
+ return [c for c in self.get_constraints() if c.is_pseudocomplex_constraint()]
360
+
361
+ def get_strictcomplex_constraints(self) -> list["Constraint"]:
362
+ return [c for c in self.get_constraints() if c.is_strictcomplex_constraint()]
363
+
364
+ def get_excludes_constraints(self) -> list["Constraint"]:
365
+ return [c for c in self.get_constraints() if c.is_excludes_constraint()]
366
+
367
+ def get_requires_constraints(self) -> list["Constraint"]:
368
+ return [c for c in self.get_constraints() if c.is_requires_constraint()]
369
+
370
+ def import_model(
371
+ self, root: Feature, parent: Feature, ctcs: list[Constraint]
372
+ ) -> None:
373
+ root.parent = parent
374
+ for ctc in ctcs:
375
+ if ctc not in self.ctcs:
376
+ self.ctcs.append(ctc)
377
+
378
+ def __str__(self) -> str:
379
+ res = "root: " + ("None" if self.root is None else self.root.name) + "\r\n"
380
+ res += "Relations:\r\n"
381
+ for i, relation in enumerate(self.get_relations()):
382
+ res += f"R{i}: {relation}\r\n"
383
+ for i, ctc in enumerate(self.ctcs):
384
+ res += f"CTC{i}: {ctc}\r\n"
385
+ attributes_res = ""
386
+ for feature in self.get_features():
387
+ for attribute in feature.get_attributes():
388
+ attributes_res += f"{attribute}" + "\r\n"
389
+ if attributes_res != "":
390
+ res += "Attributes:\r\n" + attributes_res
391
+ return res
392
+
393
+ def __hash__(self) -> int:
394
+ return hash(
395
+ (
396
+ self.root,
397
+ frozenset(self.get_features()),
398
+ frozenset(self.get_relations()),
399
+ frozenset(self.ctcs),
400
+ )
401
+ )
402
+
403
+ def __eq__(self, other: Any) -> bool:
404
+ return (
405
+ isinstance(other, FeatureModel)
406
+ and self.root == other.root
407
+ and sorted(self.get_features()) == sorted(other.get_features())
408
+ and sorted(self.get_relations()) == sorted(other.get_relations())
409
+ and sorted(self.get_constraints()) == sorted(other.get_constraints())
410
+ )
411
+
412
+
413
+ class Range:
414
+ def __init__(self, min_value: int, max_value: int):
415
+ self.min_value: int = min_value
416
+ self.max_value: int = max_value
417
+
418
+ def __str__(self) -> str:
419
+ return "[ " + str(self.min_value) + " to " + str(self.max_value) + "]"
420
+
421
+
422
+ class Domain:
423
+ def __init__(
424
+ self, ranges: Optional[list["Range"]], elements: Optional[list["Any"]]
425
+ ):
426
+ self.range_list = [] if ranges is None else ranges
427
+ self.element_list = [] if elements is None else elements
428
+
429
+ def get_range_list(self) -> list["Range"]:
430
+ return self.range_list
431
+
432
+ def get_element_list(self) -> list["Any"]:
433
+ return self.element_list
434
+
435
+ def add_range(self, new_range: Range) -> None:
436
+ self.range_list.append(new_range)
437
+
438
+ def add_element(self, element: Any) -> None:
439
+ self.element_list.append(element)
440
+
441
+ def set_range_list(self, range_list: list["Range"]) -> None:
442
+ self.range_list = range_list
443
+
444
+ def set_element_list(self, element_list: list["Any"]) -> None:
445
+ self.element_list = element_list
446
+
447
+ def __str__(self) -> str:
448
+ result = ""
449
+ element_list = self.element_list
450
+ if len(element_list) > 0:
451
+ result = str(element_list)
452
+
453
+ range_list = self.range_list
454
+ if len(range_list) > 0:
455
+ result = result + "Integer"
456
+ for rng in range_list:
457
+ result = result + str(rng)
458
+
459
+ return result
460
+
461
+
462
+ class Attribute:
463
+ def __init__(
464
+ self,
465
+ name: str,
466
+ domain: Optional[Domain] = None,
467
+ default_value: Any = None,
468
+ null_value: Optional["Any"] = None,
469
+ ):
470
+ self.name: "str" = name
471
+ self.parent: Optional["Feature"] = None
472
+ self.domain: Optional["Domain"] = domain
473
+ self.default_value: "Any" = default_value
474
+ self.null_value: Optional[Any] = null_value
475
+
476
+ def get_name(self) -> str:
477
+ return self.name
478
+
479
+ def get_parent(self) -> Optional["Feature"]:
480
+ return self.parent
481
+
482
+ def get_domain(self) -> Domain:
483
+ if self.domain is None:
484
+ raise FlamaException("Attribute domain is not defined")
485
+ return self.domain
486
+
487
+ def get_default_value(self) -> Any:
488
+ return self.default_value
489
+
490
+ def get_null_value(self) -> Any:
491
+ return self.null_value
492
+
493
+ def set_name(self, name: str) -> None:
494
+ self.name = name
495
+
496
+ def set_parent(self, parent: Feature) -> None:
497
+ self.parent = parent
498
+
499
+ def set_domain(self, domain: Domain) -> None:
500
+ self.domain = domain
501
+
502
+ def set_default_value(self, default_value: Any) -> None:
503
+ self.default_value = default_value
504
+
505
+ def set_null_value(self, null_value: Any) -> None:
506
+ self.null_value = null_value
507
+
508
+ def __str__(self) -> str:
509
+ if self.parent is None:
510
+ raise TypeError("self.parent is None, expected Feature type")
511
+
512
+ result = "[" + self.parent.name + "." + self.name + "]"
513
+ if self.domain is not None:
514
+ result = result + "Domain: " + str(self.domain)
515
+ if self.default_value is not None:
516
+ result = result + "Default value: " + str(self.default_value)
517
+ if self.null_value is not None:
518
+ result = result + "Null value: " + str(self.null_value)
519
+
520
+ return result
521
+
522
+
523
+ # This is a list of ultils to work with constraints
524
+ def get_new_ctc_name(ctcs_names: list[str], prefix_name: str) -> str:
525
+ """Return a new name for a constraint (based on the provided prefix) that is not already
526
+ in the given list of constraints' names."""
527
+ count = 1
528
+ new_name = f"{prefix_name}"
529
+ while new_name in ctcs_names:
530
+ new_name = f"{prefix_name}{count}"
531
+ count += 1
532
+ return new_name
533
+
534
+
535
+ def left_right_features_from_simple_constraint(
536
+ simple_ctc: Constraint,
537
+ ) -> tuple[str, str]:
538
+ """Return the names of the features involved in a simple constraint.
539
+
540
+ A simple constraint can be a requires constraint or an excludes constraint.
541
+ A requires constraint can be represented in the AST of the constraint with one of the
542
+ following structures:
543
+ A requires B
544
+ A => B
545
+ !A v B
546
+ An excludes constraint can be represented in the AST of the constraint with one of the
547
+ following structures:
548
+ A excludes B
549
+ A => !B
550
+ !A v !B
551
+ """
552
+ root_op = simple_ctc.ast.root
553
+ if root_op.data in [
554
+ ASTOperation.REQUIRES,
555
+ ASTOperation.IMPLIES,
556
+ ASTOperation.EXCLUDES,
557
+ ]:
558
+ left = root_op.left.data
559
+ right = root_op.right.data
560
+ if right == ASTOperation.NOT:
561
+ right = root_op.right.left.data
562
+ elif root_op.data == ASTOperation.OR:
563
+ left = root_op.left.data
564
+ right = root_op.right.data
565
+ if left == ASTOperation.NOT and right == ASTOperation.NOT: # excludes
566
+ left = root_op.left.left.data
567
+ right = root_op.right.left.data
568
+ elif left == ASTOperation.NOT: # implies: A -> B
569
+ left = root_op.left.left.data
570
+ right = root_op.right.data
571
+ elif right == ASTOperation.NOT: # implies: B -> A
572
+ left = root_op.right.left.data
573
+ right = root_op.left.data
574
+ return (left, right)
575
+
576
+
577
+ def split_constraint(constraint: Constraint) -> list[Constraint]:
578
+ """Given a constraint, split it in multiple constraints separated by the AND operator."""
579
+ asts = split_formula(constraint.ast)
580
+ asts_simplified = [simplify_formula(ast) for ast in asts]
581
+ asts = []
582
+ for ctc in asts_simplified:
583
+ asts.extend(split_formula(ctc))
584
+
585
+ asts_negation_propagated = [propagate_negation(ast.root) for ast in asts]
586
+ asts = []
587
+ for ctc in asts_negation_propagated:
588
+ asts.extend(split_formula(ctc))
589
+
590
+ asts_cnf = [to_cnf(ast) for ast in asts]
591
+ asts = []
592
+ for ctc in asts_cnf:
593
+ asts.extend(split_formula(ctc))
594
+ return [Constraint(f"{constraint.name}{i}", ast) for i, ast in enumerate(asts)]
595
+
596
+
597
+ def split_formula(formula: AST) -> list[AST]:
598
+ """Given a formula, returns a list of formulas separated by the AND operator."""
599
+ res = []
600
+ node = formula.root
601
+ if node.data == ASTOperation.AND:
602
+ res.extend(split_formula(AST(node.left)))
603
+ res.extend(split_formula(AST(node.right)))
604
+ else:
605
+ res.append(formula)
606
+ return res
@@ -0,0 +1,20 @@
1
+ from .fm_core_features import FMCoreFeatures, get_core_features
2
+ from .fm_count_leafs import FMCountLeafs, count_leaf_features
3
+ from .fm_leaf_features import FMLeafFeatures, get_leaf_features
4
+ from .fm_average_branching_factor import FMAverageBranchingFactor, average_branching_factor
5
+ from .fm_feature_ancestors import FMFeatureAncestors, get_feature_ancestors
6
+ from .fm_max_depth_tree import FMMaxDepthTree, max_depth_tree
7
+ from .fm_estimated_configurations_number import FMEstimatedConfigurationsNumber, count_configurations
8
+ from .fm_atomic_sets import FMAtomicSets, get_atomic_sets
9
+ from .fm_metrics import FMMetrics
10
+ from .fm_generate_random_attribute import GenerateRandomAttribute
11
+
12
+ __all__ = ['FMCoreFeatures', 'get_core_features',
13
+ 'FMCountLeafs', 'count_leaf_features',
14
+ 'FMLeafFeatures', 'get_leaf_features',
15
+ 'FMAverageBranchingFactor', 'average_branching_factor',
16
+ 'FMFeatureAncestors', 'get_feature_ancestors',
17
+ 'FMMaxDepthTree', 'max_depth_tree',
18
+ 'count_configurations',
19
+ 'FMAtomicSets', 'get_atomic_sets', 'FMMetrics',
20
+ 'GenerateRandomAttribute', 'FMEstimatedConfigurationsNumber']
@@ -0,0 +1,47 @@
1
+ from typing import cast
2
+
3
+ from flamapy.core.models import VariabilityModel
4
+ from flamapy.core.operations.atomic_sets import AtomicSets
5
+ from flamapy.metamodels.fm_metamodel.models import FeatureModel, Feature
6
+
7
+
8
+ class FMAtomicSets(AtomicSets):
9
+
10
+ def __init__(self) -> None:
11
+ self.result: list[set[Feature]] = []
12
+
13
+ def get_result(self) -> list[set[Feature]]:
14
+ return self.result
15
+
16
+ def execute(self, model: VariabilityModel) -> 'FMAtomicSets':
17
+ fm_model = cast(FeatureModel, model)
18
+ self.result = get_atomic_sets(fm_model)
19
+ return self
20
+
21
+ def atomic_sets(self) -> list[set[Feature]]:
22
+ return self.get_result()
23
+
24
+
25
+ def get_atomic_sets(feature_model: FeatureModel) -> list[set[Feature]]:
26
+ # if feature_model.root is None:
27
+ # return []
28
+
29
+ atomic_sets = []
30
+ root = feature_model.root
31
+ atomic_set = {root}
32
+ atomic_sets.append(atomic_set)
33
+ compute_atomic_sets(atomic_sets, root, atomic_set)
34
+ return atomic_sets
35
+
36
+
37
+ def compute_atomic_sets(atomic_sets: list[set[Feature]],
38
+ feature: Feature,
39
+ current_set: set[Feature]) -> None:
40
+ for child in feature.get_children():
41
+ if child.is_mandatory():
42
+ current_set.add(child)
43
+ compute_atomic_sets(atomic_sets, child, current_set)
44
+ else:
45
+ new_as = {child}
46
+ atomic_sets.append(new_as)
47
+ compute_atomic_sets(atomic_sets, child, new_as)