flamapy-fw 2.5.0.dev0__tar.gz → 2.6.0__tar.gz

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 (70) hide show
  1. flamapy_fw-2.6.0/PKG-INFO +32 -0
  2. flamapy_fw-2.6.0/README.md +14 -0
  3. {flamapy_fw-2.5.0.dev0 → flamapy_fw-2.6.0}/flamapy/core/discover.py +17 -7
  4. flamapy_fw-2.6.0/flamapy/core/models/__init__.py +4 -0
  5. {flamapy_fw-2.5.0.dev0 → flamapy_fw-2.6.0}/flamapy/core/models/ast.py +140 -9
  6. {flamapy_fw-2.5.0.dev0 → flamapy_fw-2.6.0}/flamapy/core/operations/__init__.py +10 -0
  7. flamapy_fw-2.6.0/flamapy/core/operations/attribute_optimization.py +68 -0
  8. flamapy_fw-2.6.0/flamapy/core/operations/configurations.py +29 -0
  9. flamapy_fw-2.6.0/flamapy/core/operations/configurations_number.py +30 -0
  10. flamapy_fw-2.6.0/flamapy/core/operations/core_features.py +29 -0
  11. flamapy_fw-2.6.0/flamapy/core/operations/dead_features.py +29 -0
  12. flamapy_fw-2.6.0/flamapy/core/operations/descriptor.py +61 -0
  13. flamapy_fw-2.6.0/flamapy/core/operations/false_optional_features.py +33 -0
  14. {flamapy_fw-2.5.0.dev0 → flamapy_fw-2.6.0}/flamapy/core/operations/metrics_operation.py +1 -1
  15. {flamapy_fw-2.5.0.dev0 → flamapy_fw-2.6.0}/flamapy/core/operations/sampling.py +19 -0
  16. flamapy_fw-2.6.0/flamapy/core/operations/satisfiable.py +28 -0
  17. flamapy_fw-2.6.0/flamapy/core/operations/satisfiable_configuration.py +47 -0
  18. {flamapy_fw-2.5.0.dev0 → flamapy_fw-2.6.0}/flamapy/core/plugins.py +24 -34
  19. flamapy_fw-2.6.0/flamapy/core/utils.py +17 -0
  20. {flamapy_fw-2.5.0.dev0 → flamapy_fw-2.6.0}/flamapy/metamodels/configuration_metamodel/models/configuration.py +15 -0
  21. flamapy_fw-2.6.0/flamapy_fw.egg-info/PKG-INFO +32 -0
  22. {flamapy_fw-2.5.0.dev0 → flamapy_fw-2.6.0}/flamapy_fw.egg-info/SOURCES.txt +2 -0
  23. {flamapy_fw-2.5.0.dev0 → flamapy_fw-2.6.0}/pyproject.toml +3 -3
  24. flamapy_fw-2.5.0.dev0/PKG-INFO +0 -43
  25. flamapy_fw-2.5.0.dev0/README.md +0 -25
  26. flamapy_fw-2.5.0.dev0/flamapy/core/models/__init__.py +0 -4
  27. flamapy_fw-2.5.0.dev0/flamapy/core/operations/configurations.py +0 -14
  28. flamapy_fw-2.5.0.dev0/flamapy/core/operations/configurations_number.py +0 -13
  29. flamapy_fw-2.5.0.dev0/flamapy/core/operations/core_features.py +0 -14
  30. flamapy_fw-2.5.0.dev0/flamapy/core/operations/dead_features.py +0 -14
  31. flamapy_fw-2.5.0.dev0/flamapy/core/operations/false_optional_features.py +0 -17
  32. flamapy_fw-2.5.0.dev0/flamapy/core/operations/satisfiable.py +0 -13
  33. flamapy_fw-2.5.0.dev0/flamapy/core/operations/satisfiable_configuration.py +0 -18
  34. flamapy_fw-2.5.0.dev0/flamapy/core/utils.py +0 -9
  35. flamapy_fw-2.5.0.dev0/flamapy_fw.egg-info/PKG-INFO +0 -43
  36. {flamapy_fw-2.5.0.dev0 → flamapy_fw-2.6.0}/LICENSE +0 -0
  37. {flamapy_fw-2.5.0.dev0 → flamapy_fw-2.6.0}/flamapy/core/__init__.py +0 -0
  38. {flamapy_fw-2.5.0.dev0 → flamapy_fw-2.6.0}/flamapy/core/config.py +0 -0
  39. {flamapy_fw-2.5.0.dev0 → flamapy_fw-2.6.0}/flamapy/core/exceptions.py +0 -0
  40. {flamapy_fw-2.5.0.dev0 → flamapy_fw-2.6.0}/flamapy/core/models/variability_model.py +0 -0
  41. {flamapy_fw-2.5.0.dev0 → flamapy_fw-2.6.0}/flamapy/core/operations/abstract_operation.py +0 -0
  42. {flamapy_fw-2.5.0.dev0 → flamapy_fw-2.6.0}/flamapy/core/operations/atomic_sets.py +0 -0
  43. {flamapy_fw-2.5.0.dev0 → flamapy_fw-2.6.0}/flamapy/core/operations/average_branching_factor.py +0 -0
  44. {flamapy_fw-2.5.0.dev0 → flamapy_fw-2.6.0}/flamapy/core/operations/commonality.py +0 -0
  45. {flamapy_fw-2.5.0.dev0 → flamapy_fw-2.6.0}/flamapy/core/operations/count_leafs.py +0 -0
  46. {flamapy_fw-2.5.0.dev0 → flamapy_fw-2.6.0}/flamapy/core/operations/error_detection.py +0 -0
  47. {flamapy_fw-2.5.0.dev0 → flamapy_fw-2.6.0}/flamapy/core/operations/error_diagnosis.py +0 -0
  48. {flamapy_fw-2.5.0.dev0 → flamapy_fw-2.6.0}/flamapy/core/operations/estimated_configurations_number.py +0 -0
  49. {flamapy_fw-2.5.0.dev0 → flamapy_fw-2.6.0}/flamapy/core/operations/filter.py +0 -0
  50. {flamapy_fw-2.5.0.dev0 → flamapy_fw-2.6.0}/flamapy/core/operations/variability.py +0 -0
  51. {flamapy_fw-2.5.0.dev0 → flamapy_fw-2.6.0}/flamapy/core/transformations/__init__.py +0 -0
  52. {flamapy_fw-2.5.0.dev0 → flamapy_fw-2.6.0}/flamapy/core/transformations/abstract_transformation.py +0 -0
  53. {flamapy_fw-2.5.0.dev0 → flamapy_fw-2.6.0}/flamapy/core/transformations/model_to_model.py +0 -0
  54. {flamapy_fw-2.5.0.dev0 → flamapy_fw-2.6.0}/flamapy/core/transformations/model_to_text.py +0 -0
  55. {flamapy_fw-2.5.0.dev0 → flamapy_fw-2.6.0}/flamapy/core/transformations/text_to_model.py +0 -0
  56. {flamapy_fw-2.5.0.dev0 → flamapy_fw-2.6.0}/flamapy/metamodels/configuration_metamodel/__init__.py +0 -0
  57. {flamapy_fw-2.5.0.dev0 → flamapy_fw-2.6.0}/flamapy/metamodels/configuration_metamodel/models/__init__.py +0 -0
  58. {flamapy_fw-2.5.0.dev0 → flamapy_fw-2.6.0}/flamapy/metamodels/configuration_metamodel/transformations/__init__.py +0 -0
  59. {flamapy_fw-2.5.0.dev0 → flamapy_fw-2.6.0}/flamapy/metamodels/configuration_metamodel/transformations/configuration_basic_reader.py +0 -0
  60. {flamapy_fw-2.5.0.dev0 → flamapy_fw-2.6.0}/flamapy/metamodels/configuration_metamodel/transformations/configuration_basic_writer.py +0 -0
  61. {flamapy_fw-2.5.0.dev0 → flamapy_fw-2.6.0}/flamapy/metamodels/configuration_metamodel/transformations/configuration_json_reader.py +0 -0
  62. {flamapy_fw-2.5.0.dev0 → flamapy_fw-2.6.0}/flamapy/metamodels/configuration_metamodel/transformations/configuration_json_writer.py +0 -0
  63. {flamapy_fw-2.5.0.dev0 → flamapy_fw-2.6.0}/flamapy/metamodels/configuration_metamodel/transformations/uvls_json_reader.py +0 -0
  64. {flamapy_fw-2.5.0.dev0 → flamapy_fw-2.6.0}/flamapy/metamodels/configuration_metamodel/transformations/uvls_json_writer.py +0 -0
  65. {flamapy_fw-2.5.0.dev0 → flamapy_fw-2.6.0}/flamapy_fw.egg-info/dependency_links.txt +0 -0
  66. {flamapy_fw-2.5.0.dev0 → flamapy_fw-2.6.0}/flamapy_fw.egg-info/entry_points.txt +0 -0
  67. {flamapy_fw-2.5.0.dev0 → flamapy_fw-2.6.0}/flamapy_fw.egg-info/requires.txt +0 -0
  68. {flamapy_fw-2.5.0.dev0 → flamapy_fw-2.6.0}/flamapy_fw.egg-info/top_level.txt +0 -0
  69. {flamapy_fw-2.5.0.dev0 → flamapy_fw-2.6.0}/setup.cfg +0 -0
  70. {flamapy_fw-2.5.0.dev0 → flamapy_fw-2.6.0}/setup.py +0 -0
@@ -0,0 +1,32 @@
1
+ Metadata-Version: 2.4
2
+ Name: flamapy-fw
3
+ Version: 2.6.0
4
+ Summary: Flamapy is a Python-based AAFM framework that takes into consideration previous AAFM tool designs and enables multi-solver and multi-metamodel support for the integration of AAFM tooling on the Python ecosystem.
5
+ Author-email: Flamapy <flamapy@us.es>
6
+ License-Expression: GPL-3.0-or-later
7
+ Project-URL: Homepage, https://github.com/flamapy/core
8
+ Requires-Python: >=3.11
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Provides-Extra: dev
12
+ Requires-Dist: pytest; extra == "dev"
13
+ Requires-Dist: pytest-mock; extra == "dev"
14
+ Requires-Dist: prospector; extra == "dev"
15
+ Requires-Dist: mypy; extra == "dev"
16
+ Requires-Dist: coverage; extra == "dev"
17
+ Dynamic: license-file
18
+
19
+ # flamapy-fw
20
+
21
+ The core framework of [flamapy](https://flamapy.org): the plugin-discovery
22
+ mechanism and the base classes (operations, transformations, variability models)
23
+ that every flamapy metamodel plugin builds on. It contains no analysis logic by
24
+ itself.
25
+
26
+ **Documentation:** https://docs.flamapy.org/framework
27
+
28
+ ## Installation
29
+
30
+ ```bash
31
+ pip install flamapy-fw
32
+ ```
@@ -0,0 +1,14 @@
1
+ # flamapy-fw
2
+
3
+ The core framework of [flamapy](https://flamapy.org): the plugin-discovery
4
+ mechanism and the base classes (operations, transformations, variability models)
5
+ that every flamapy metamodel plugin builds on. It contains no analysis logic by
6
+ itself.
7
+
8
+ **Documentation:** https://docs.flamapy.org/framework
9
+
10
+ ## Installation
11
+
12
+ ```bash
13
+ pip install flamapy-fw
14
+ ```
@@ -11,10 +11,12 @@ from flamapy.core.exceptions import TransformationNotFound
11
11
  from flamapy.core.exceptions import ConfigurationNotFound
12
12
  from flamapy.core.models import VariabilityModel
13
13
  from flamapy.core.operations import Operation
14
+ from flamapy.core.operations.descriptor import OperationDescriptor, collect_descriptors
14
15
  from flamapy.core.plugins import Operations, Plugin, Plugins
15
16
  from flamapy.core.transformations import Transformation
16
17
  from flamapy.core.transformations.text_to_model import TextToModel
17
18
  from flamapy.core.transformations.model_to_model import ModelToModel
19
+ from flamapy.core.utils import filename_matches_extension
18
20
  from flamapy.metamodels.configuration_metamodel.models.configuration import Configuration
19
21
 
20
22
 
@@ -103,6 +105,13 @@ class DiscoverMetamodels:
103
105
 
104
106
  return operations
105
107
 
108
+ def available_operations(self) -> dict[str, OperationDescriptor]:
109
+ """Facade descriptors of every discovered operation that declares one, keyed by public name.
110
+
111
+ This is the single source the facade, CLI and REST derive their operation surface from.
112
+ """
113
+ return collect_descriptors(self.get_operations())
114
+
106
115
  def get_transformations(self) -> list[Type[Transformation]]:
107
116
  """Get transformations for all modules"""
108
117
  transformations: list[Type[Transformation]] = []
@@ -256,14 +265,15 @@ class DiscoverMetamodels:
256
265
 
257
266
  def __transform_to_model_from_file(self, file: str) -> VariabilityModel:
258
267
  t2m_transformations = self.get_transformations_t2m()
259
- extension = file.rsplit(".", maxsplit=1)[-1]
260
- t2m_filters = filter(
261
- lambda t2m: t2m.get_source_extension() == extension, t2m_transformations
262
- )
263
-
264
- t2m = next(t2m_filters, None)
265
- if t2m is None:
268
+ # Prefer the most specific matching extension (e.g. 'uvl.json' over 'json').
269
+ candidates = [
270
+ t2m
271
+ for t2m in t2m_transformations
272
+ if filename_matches_extension(file, t2m.get_source_extension())
273
+ ]
274
+ if not candidates:
266
275
  raise TransformationNotFound()
276
+ t2m = max(candidates, key=lambda t: len(t.get_source_extension()))
267
277
 
268
278
  return t2m(file).transform()
269
279
 
@@ -0,0 +1,4 @@
1
+ from .variability_model import VariabilityModel, VariabilityElement # pylint: disable=cyclic-import
2
+ from .ast import AST, ASTOperation, NodeType # pylint: disable=cyclic-import
3
+
4
+ __all__ = ["AST", "ASTOperation", "NodeType", "VariabilityElement", "VariabilityModel"]
@@ -1,6 +1,8 @@
1
- from typing import Any
1
+ from typing import Any, Optional
2
2
  from enum import Enum
3
3
 
4
+ from flamapy.core.exceptions import FlamaException
5
+
4
6
 
5
7
  class ASTOperation(Enum):
6
8
  # logical operators
@@ -69,11 +71,25 @@ AGGREGATION_OPERATORS = [
69
71
  ]
70
72
 
71
73
 
74
+ class NodeType(Enum):
75
+ """Semantic type of a leaf node in the AST."""
76
+ FEATURE = "Feature"
77
+ LITERAL = "Literal"
78
+ OPERATOR = "Operator"
79
+
80
+
72
81
  class Node:
73
- def __init__(self, data: Any, left: "Node" = None, right: "Node" = None): # type: ignore
82
+ def __init__(
83
+ self,
84
+ data: Any,
85
+ left: "Node" = None, # type: ignore[assignment]
86
+ right: "Node" = None, # type: ignore[assignment]
87
+ node_type: Optional[NodeType] = None,
88
+ ):
74
89
  self.data = data
75
90
  self.left = left
76
91
  self.right = right
92
+ self.node_type = node_type
77
93
 
78
94
  def is_term(self) -> bool:
79
95
  return not self.is_op()
@@ -93,6 +109,19 @@ class Node:
93
109
  def is_aggregate_op(self) -> bool:
94
110
  return self.is_op() and self.data in AGGREGATION_OPERATORS
95
111
 
112
+ def is_feature(self) -> bool:
113
+ """Return True if this node is explicitly marked as a feature reference."""
114
+ return self.node_type == NodeType.FEATURE
115
+
116
+ def is_literal(self) -> bool:
117
+ """Return True if this node is a literal value (not a feature reference).
118
+
119
+ Falls back to a type-based check for nodes without an explicit node_type.
120
+ """
121
+ if self.node_type is not None:
122
+ return self.node_type == NodeType.LITERAL
123
+ return isinstance(self.data, (int, float))
124
+
96
125
  def __str__(self) -> str:
97
126
  data = self.data.value if self.is_op() else str(self.data)
98
127
  if self.left and self.right:
@@ -160,10 +189,31 @@ class AST:
160
189
  def to_cnf(self) -> "AST":
161
190
  return convert_into_cnf(self)
162
191
 
163
- def get_clauses(self) -> list[list[Any]]:
192
+ def get_clauses(self, method: str = 'distributive') -> list[list[Any]]:
193
+ """Return the CNF clauses of this AST.
194
+
195
+ ``method='distributive'`` (default) uses the classic distributive expansion and
196
+ returns clauses over the formula's own variables. ``method='tseytin'`` uses the
197
+ Tseytin transformation; the returned clauses may contain fresh auxiliary
198
+ variables. Use :meth:`get_clauses_with_aux` when those auxiliary variable names
199
+ are needed (e.g. to register them as non-feature solver variables).
200
+ """
201
+ if method == 'tseytin':
202
+ clauses, _ = tseytin_cnf(self)
203
+ return clauses
164
204
  ast = self.to_cnf()
165
205
  return get_clauses(ast)
166
206
 
207
+ def get_clauses_with_aux(self, method: str = 'tseytin') -> tuple[list[list[Any]], list[str]]:
208
+ """Return ``(clauses, aux_names)`` for the given CNF ``method``.
209
+
210
+ For ``'tseytin'`` this exposes the auxiliary variable names introduced by the
211
+ encoding. For ``'distributive'`` the auxiliary list is always empty.
212
+ """
213
+ if method == 'tseytin':
214
+ return tseytin_cnf(self)
215
+ return get_clauses(self.to_cnf()), []
216
+
167
217
  def get_operators(self) -> list[ASTOperation]:
168
218
  operators = []
169
219
  stack = [self.root]
@@ -236,20 +286,22 @@ def simplify_formula(ast: AST) -> AST:
236
286
  )
237
287
  elif logic_op == ASTOperation.EQUIVALENCE:
238
288
  # Replace P <=> Q with P => Q ∧ Q => P.
239
- left = simplify_formula(AST.create_binary_operation(ASTOperation.IMPLIES, left, right)).root
240
- right = simplify_formula(
289
+ new_left = simplify_formula(
290
+ AST.create_binary_operation(ASTOperation.IMPLIES, left, right)
291
+ ).root
292
+ new_right = simplify_formula(
241
293
  AST.create_binary_operation(ASTOperation.IMPLIES, right, left)
242
294
  ).root
243
- result = AST.create_binary_operation(ASTOperation.AND, left, right)
295
+ result = AST.create_binary_operation(ASTOperation.AND, new_left, new_right)
244
296
  elif logic_op == ASTOperation.XOR:
245
297
  # Replace P XOR Q with (P ∧ !Q) v (!P ∧ Q).
246
- left = simplify_formula(
298
+ new_left = simplify_formula(
247
299
  AST.create_binary_operation(ASTOperation.AND, left, Node(ASTOperation.NOT, right))
248
300
  ).root
249
- left = simplify_formula(
301
+ new_right = simplify_formula(
250
302
  AST.create_binary_operation(ASTOperation.AND, Node(ASTOperation.NOT, left), right)
251
303
  ).root
252
- result = AST.create_binary_operation(ASTOperation.OR, left, right)
304
+ result = AST.create_binary_operation(ASTOperation.OR, new_left, new_right)
253
305
  elif logic_op == ASTOperation.AND:
254
306
  left = simplify_formula(AST(left)).root
255
307
  right = simplify_formula(AST(right)).root
@@ -349,6 +401,85 @@ def propagate_negation(node: Node, negated: bool = False) -> AST:
349
401
  return result
350
402
 
351
403
 
404
+ TSEYTIN_AUX_PREFIX = "__tseytin_"
405
+
406
+
407
+ def _negate_literal(literal: Any) -> Any:
408
+ """Flip the sign of a CNF literal that uses the ``"-name"`` string convention."""
409
+ if isinstance(literal, str) and literal.startswith("-"):
410
+ return literal[1:]
411
+ return "-" + str(literal)
412
+
413
+
414
+ def _tseytin_gate_clauses(op: ASTOperation, gate: Any, a: Any, b: Any) -> list[list[Any]]:
415
+ """Biconditional clauses encoding ``gate <=> (a op b)`` for a binary logical ``op``.
416
+
417
+ ``a`` and ``b`` are literals; ``gate`` is the fresh auxiliary literal. Encoding each
418
+ connective directly (rather than expanding it first) is what keeps the transformation
419
+ linear in the formula size.
420
+ """
421
+ g, na, nb, ng = gate, _negate_literal(a), _negate_literal(b), _negate_literal(gate)
422
+ if op == ASTOperation.AND:
423
+ return [[ng, a], [ng, b], [g, na, nb]]
424
+ if op == ASTOperation.OR:
425
+ return [[g, na], [g, nb], [ng, a, b]]
426
+ if op in (ASTOperation.IMPLIES, ASTOperation.REQUIRES): # a -> b == (not a) or b
427
+ return [[g, a], [g, nb], [ng, na, b]]
428
+ if op == ASTOperation.EXCLUDES: # (not a) or (not b)
429
+ return [[g, a], [g, b], [ng, na, nb]]
430
+ if op == ASTOperation.EQUIVALENCE: # a <-> b
431
+ return [[ng, na, b], [ng, a, nb], [g, a, b], [g, na, nb]]
432
+ if op == ASTOperation.XOR: # a xor b
433
+ return [[ng, a, b], [ng, na, nb], [g, na, b], [g, a, nb]]
434
+ raise FlamaException(
435
+ f"Operation '{op}' is not a propositional connective supported by the Tseytin "
436
+ f"transformation."
437
+ )
438
+
439
+
440
+ def tseytin_cnf(ast: AST) -> tuple[list[list[Any]], list[str]]:
441
+ """Return an equisatisfiable CNF of ``ast`` using the Tseytin transformation.
442
+
443
+ Unlike the distributive :func:`to_cnf`, this produces a CNF whose size is linear
444
+ in the size of the formula by introducing one fresh auxiliary variable per gate.
445
+ Each logical connective is encoded directly (no distributive expansion), so formulas
446
+ that blow up under :func:`to_cnf` (XOR chains, wide equivalences) stay compact. Gates
447
+ are encoded as full biconditionals (``g <=> subformula``) so that every model of the
448
+ original formula extends to exactly one model of the encoding; this preserves model
449
+ counts and configuration enumeration (once auxiliary variables are projected out).
450
+
451
+ Returns a tuple ``(clauses, aux_names)`` where ``clauses`` is a list of clauses
452
+ (each a list of literals in the same ``"-name"`` string convention as
453
+ :func:`get_clauses`) and ``aux_names`` lists every auxiliary variable name
454
+ introduced (each prefixed with :data:`TSEYTIN_AUX_PREFIX`) so callers can register
455
+ them as solver variables that are not features.
456
+
457
+ Raises :class:`FlamaException` if the formula contains a non-propositional operator
458
+ (arithmetic, comparison or aggregation).
459
+ """
460
+ clauses: list[list[Any]] = []
461
+ aux_names: list[str] = []
462
+ counter = [0]
463
+
464
+ def literal_of(node: Node) -> Any:
465
+ if node.is_term():
466
+ return node.data
467
+ if node.data == ASTOperation.NOT:
468
+ # Negation of a single literal is free; no gate needed.
469
+ return _negate_literal(literal_of(node.left))
470
+ left_lit = literal_of(node.left)
471
+ right_lit = literal_of(node.right)
472
+ counter[0] += 1
473
+ gate = f"{TSEYTIN_AUX_PREFIX}{counter[0]}"
474
+ aux_names.append(gate)
475
+ clauses.extend(_tseytin_gate_clauses(node.data, gate, left_lit, right_lit))
476
+ return gate
477
+
478
+ root_literal = literal_of(ast.root)
479
+ clauses.append([root_literal]) # assert the whole formula is true
480
+ return clauses, aux_names
481
+
482
+
352
483
  def get_clauses(ast: AST) -> list[list[Any]]:
353
484
  """Return the list of clauses represented by the AST root node in conjunctive normal form."""
354
485
  node = ast.root
@@ -17,9 +17,15 @@ from .variability import Variability # pylint: disable=cyclic-import
17
17
  from .sampling import Sampling # pylint: disable=cyclic-import
18
18
  from .atomic_sets import AtomicSets # pylint: disable=cyclic-import
19
19
  from .metrics_operation import Metrics # pylint: disable=cyclic-import
20
+ from .attribute_optimization import ( # pylint: disable=cyclic-import
21
+ AttributeOptimization,
22
+ OptimizationGoal,
23
+ )
24
+ from .descriptor import Input, OperationDescriptor, collect_descriptors
20
25
 
21
26
  __all__ = [
22
27
  "AtomicSets",
28
+ "AttributeOptimization",
23
29
  "AverageBranchingFactor",
24
30
  "Commonality",
25
31
  "Configurations",
@@ -32,10 +38,14 @@ __all__ = [
32
38
  "EstimatedConfigurationsNumber",
33
39
  "FalseOptionalFeatures",
34
40
  "Filter",
41
+ "Input",
35
42
  "Metrics",
36
43
  "Operation",
44
+ "OperationDescriptor",
45
+ "OptimizationGoal",
37
46
  "Sampling",
38
47
  "Satisfiable",
39
48
  "SatisfiableConfiguration",
40
49
  "Variability",
50
+ "collect_descriptors",
41
51
  ]
@@ -0,0 +1,68 @@
1
+ from abc import abstractmethod
2
+ from enum import Enum
3
+ from typing import Any
4
+
5
+ from flamapy.core.operations import Operation
6
+ from flamapy.core.operations.descriptor import OperationDescriptor, Input
7
+
8
+
9
+ class OptimizationGoal(Enum):
10
+ MAXIMIZE = 'Maximize'
11
+ MINIMIZE = 'Minimize'
12
+
13
+
14
+ def _attribute_optimization_inputs(operation: Any, facade: Any, kwargs: dict[str, Any]) -> None:
15
+ objectives = kwargs['objectives']
16
+ if isinstance(objectives, str):
17
+ attributes = {objectives: OptimizationGoal.MINIMIZE}
18
+ else:
19
+ attributes = {
20
+ name: (OptimizationGoal.MINIMIZE if str(goal).lower().startswith('min')
21
+ else OptimizationGoal.MAXIMIZE)
22
+ for name, goal in dict(objectives).items()
23
+ }
24
+ operation.set_attributes(attributes)
25
+
26
+
27
+ def _attribute_optimization_result(result: Any) -> Any:
28
+ # z3 returns (configuration, values) tuples; sat returns bare configurations.
29
+ return [item[0] if isinstance(item, tuple) else item for item in result]
30
+
31
+
32
+ class AttributeOptimization(Operation):
33
+ """Return the configuration(s) that optimize the given numeric feature attribute(s).
34
+
35
+ Attributes are identified by name and mapped to an :class:`OptimizationGoal`. The
36
+ objective for each attribute is the sum of its values over the selected features.
37
+ """
38
+
39
+ facade = OperationDescriptor(
40
+ doc=(
41
+ 'Returns the configuration(s) that optimize one or more numeric feature\n'
42
+ 'attributes.\n'
43
+ '\n'
44
+ '``objectives`` is either a single attribute name (defaults to minimizing it)\n'
45
+ 'or a mapping ``{attribute_name: "Minimize"|"Maximize"}``. ``backend`` selects\n'
46
+ 'the plugin: "sat" performs single-objective MaxSAT optimization; "z3"\n'
47
+ 'additionally supports typed attributes and multi-objective (Pareto)\n'
48
+ 'optimization. Defaults to "sat".'
49
+ ),
50
+ returns='Union[None, List[Configuration]]',
51
+ name='attribute_optimization', operation='AttributeOptimization', default_backend='sat',
52
+ selectable_backend=True,
53
+ inputs=(Input('objectives', Any, required=True),),
54
+ input_adapter=_attribute_optimization_inputs,
55
+ result_adapter=_attribute_optimization_result,
56
+ )
57
+
58
+ @abstractmethod
59
+ def __init__(self) -> None:
60
+ pass
61
+
62
+ @abstractmethod
63
+ def set_attributes(self, attributes: dict[str, OptimizationGoal]) -> None:
64
+ """Set the attributes to optimize as a mapping ``{attribute_name: OptimizationGoal}``."""
65
+
66
+ @abstractmethod
67
+ def optimize(self) -> list[Any]:
68
+ """Return the optimal configuration(s)."""
@@ -0,0 +1,29 @@
1
+ from abc import abstractmethod
2
+
3
+ from flamapy.core.operations import Operation
4
+ from flamapy.core.operations.descriptor import OperationDescriptor
5
+ from flamapy.metamodels.configuration_metamodel.models.configuration import Configuration
6
+
7
+
8
+ class Configurations(Operation):
9
+ facade = OperationDescriptor(
10
+ doc=(
11
+ 'These are the individual outcomes that can be produced from a feature model.\n'
12
+ 'Each product is a combination of features that satisfies all the constraints\n'
13
+ 'and dependencies in the feature model.\n'
14
+ '\n'
15
+ '``backend`` selects the analysis plugin ("sat", "bdd" or "z3"); defaults to\n'
16
+ 'bdd.'
17
+ ),
18
+ returns='Union[None, List[Configuration]]',
19
+ name='configurations', operation='Configurations', default_backend='bdd',
20
+ selectable_backend=True,
21
+ )
22
+
23
+ @abstractmethod
24
+ def __init__(self) -> None:
25
+ pass
26
+
27
+ @abstractmethod
28
+ def get_configurations(self) -> list[Configuration]:
29
+ pass
@@ -0,0 +1,30 @@
1
+ from abc import abstractmethod
2
+
3
+ from flamapy.core.operations import Operation
4
+ from flamapy.core.operations.descriptor import OperationDescriptor
5
+
6
+
7
+ class ConfigurationsNumber(Operation):
8
+ facade = OperationDescriptor(
9
+ doc=(
10
+ 'This is the total number of different full configurations that can be produced\n'
11
+ "from a feature model. It's calculated by considering all possible combinations\n"
12
+ 'of features, taking into account the constraints and dependencies between\n'
13
+ 'features.\n'
14
+ '\n'
15
+ '``backend`` selects the analysis plugin ("sat", "bdd", "z3", or "sharpsat" for\n'
16
+ 'a scalable approximate count via the optional flamapy-sharpsat plugin);\n'
17
+ 'defaults to bdd.'
18
+ ),
19
+ returns='Union[None, int]',
20
+ name='configurations_number', operation='ConfigurationsNumber', default_backend='bdd',
21
+ selectable_backend=True,
22
+ )
23
+
24
+ @abstractmethod
25
+ def __init__(self) -> None:
26
+ pass
27
+
28
+ @abstractmethod
29
+ def get_configurations_number(self) -> int:
30
+ pass
@@ -0,0 +1,29 @@
1
+ from abc import abstractmethod
2
+
3
+ from flamapy.core.operations import Operation
4
+ from flamapy.core.operations.descriptor import OperationDescriptor
5
+ from flamapy.core.models.variability_model import VariabilityElement
6
+
7
+
8
+ class CoreFeatures(Operation):
9
+ facade = OperationDescriptor(
10
+ doc=(
11
+ 'These are the features that are present in all products of a product line. In\n'
12
+ 'a feature model, they are the features that are mandatory and not optional.\n'
13
+ 'Core features define the commonality among all products in a product line.\n'
14
+ '\n'
15
+ '``backend`` selects the analysis plugin ("sat", "bdd" or "z3"); defaults to\n'
16
+ 'sat.'
17
+ ),
18
+ returns='Union[None, List[str]]',
19
+ name='core_features', operation='CoreFeatures', default_backend='sat',
20
+ selectable_backend=True,
21
+ )
22
+
23
+ @abstractmethod
24
+ def __init__(self) -> None:
25
+ pass
26
+
27
+ @abstractmethod
28
+ def get_core_features(self) -> list[VariabilityElement]:
29
+ pass
@@ -0,0 +1,29 @@
1
+ from abc import abstractmethod
2
+
3
+ from flamapy.core.operations import Operation
4
+ from flamapy.core.operations.descriptor import OperationDescriptor
5
+ from flamapy.core.models.variability_model import VariabilityElement
6
+
7
+
8
+ class DeadFeatures(Operation):
9
+ facade = OperationDescriptor(
10
+ doc=(
11
+ 'These are features that, due to the constraints and dependencies in the\n'
12
+ 'feature model, cannot be included in any valid product. Dead features are\n'
13
+ 'usually a sign of an error in the feature model.\n'
14
+ '\n'
15
+ '``backend`` selects the analysis plugin ("sat", "bdd" or "z3"); defaults to\n'
16
+ 'sat.'
17
+ ),
18
+ returns='Union[None, List[str]]',
19
+ name='dead_features', operation='DeadFeatures', default_backend='sat',
20
+ selectable_backend=True,
21
+ )
22
+
23
+ @abstractmethod
24
+ def __init__(self) -> None:
25
+ pass
26
+
27
+ @abstractmethod
28
+ def get_dead_features(self) -> list[VariabilityElement]:
29
+ pass
@@ -0,0 +1,61 @@
1
+ """Self-describing metadata for operations, so the facade/CLI/REST can be *derived* from the
2
+ installed operations instead of hand-written.
3
+
4
+ An operation interface (or a bespoke concrete operation) declares a class attribute ``facade`` of
5
+ type :class:`OperationDescriptor`. It states the operation's stable public name, which backend it
6
+ runs on, its inputs (and how to wire them), and optional input/result adapters for the few
7
+ operations whose facade method is not a plain ``set params -> execute``. Concrete backend
8
+ implementations inherit the descriptor from their interface, so there is exactly one descriptor
9
+ per *logical* operation.
10
+ """
11
+ from dataclasses import dataclass
12
+ from typing import Any, Callable, Iterable, Optional
13
+
14
+
15
+ @dataclass(frozen=True)
16
+ class Input:
17
+ """One input of a facade operation and how to feed it to the underlying operation."""
18
+
19
+ name: str
20
+ type: Any = str # a type or typing special form (e.g. Any), for CLI/REST
21
+ default: Any = None
22
+ required: bool = False
23
+ setter: Optional[str] = None # operation setter for the value, e.g. 'set_sample_size'
24
+ kind: str = 'scalar' # 'scalar' | 'configuration' | 'file' (CLI/REST marshalling)
25
+
26
+
27
+ @dataclass(frozen=True)
28
+ class OperationDescriptor:
29
+ """Everything the facade/CLI/REST need to expose an operation without hand-written code."""
30
+
31
+ name: str # stable public (facade/CLI/REST) name
32
+ operation: str # name passed to DiscoverMetamodels.use_operation
33
+ kind: str = 'operation' # 'operation' | 'producer' | 'transformer'
34
+ default_backend: Optional[str] = None # None => runs directly on the feature model
35
+ backends: Optional[tuple[str, ...]] = None # allowed backends (None => any implementer)
36
+ selectable_backend: bool = False # facade exposes a backend= kwarg (else fixed)
37
+ inputs: tuple[Input, ...] = ()
38
+ # For the ~12 non-uniform methods: custom wiring / result reshaping. When absent, the generic
39
+ # "call each Input.setter, then execute, then get_result" path is used.
40
+ input_adapter: Optional[Callable[..., Any]] = None
41
+ result_adapter: Optional[Callable[[Any], Any]] = None
42
+ doc: str = ''
43
+ returns: str = 'Any' # return annotation (source text) for the stub
44
+
45
+ @property
46
+ def needs_backend(self) -> bool:
47
+ return self.default_backend is not None
48
+
49
+
50
+ def collect_descriptors(operations: Iterable[type]) -> dict[str, OperationDescriptor]:
51
+ """Collect the ``facade`` descriptors from a set of operation classes, keyed by public name.
52
+
53
+ Concrete backend operations inherit their interface's descriptor, so several classes may yield
54
+ the same descriptor; they are de-duplicated by ``descriptor.name`` (first one wins).
55
+ """
56
+ descriptors: dict[str, OperationDescriptor] = {}
57
+ for operation in operations:
58
+ descriptor = getattr(operation, 'facade', None)
59
+ if isinstance(descriptor, OperationDescriptor):
60
+ descriptors.setdefault(descriptor.name, descriptor)
61
+ return descriptors
@@ -0,0 +1,33 @@
1
+ from abc import abstractmethod
2
+
3
+ from flamapy.core.operations import Operation
4
+ from flamapy.core.operations.descriptor import OperationDescriptor
5
+ from flamapy.core.models.variability_model import VariabilityElement
6
+
7
+
8
+ class FalseOptionalFeatures(Operation):
9
+ """A feature is defined as `false optional` if the selection of its parent makes the feature
10
+ itself selected as well, although it is defined as optional and not mandatory.
11
+
12
+ A feature in a group can be also `false optional`.
13
+ In general, false optional features should be marked as mandatory for maintainability purposes.
14
+ """
15
+
16
+ facade = OperationDescriptor(
17
+ doc=(
18
+ 'These are features that appear to be optional in the feature model, but due to\n'
19
+ 'the constraints and dependencies, must be included in every valid product.\n'
20
+ 'Like dead features, false optional features are usually a sign of an error in\n'
21
+ 'the feature model.\n'
22
+ '\n'
23
+ '``backend`` selects the analysis plugin ("sat", "bdd" or "z3"); defaults to\n'
24
+ 'sat.'
25
+ ),
26
+ returns='Union[None, List[str]]',
27
+ name='false_optional_features', operation='FalseOptionalFeatures', default_backend='sat',
28
+ selectable_backend=True,
29
+ )
30
+
31
+ @abstractmethod
32
+ def get_false_optional_features(self) -> list[VariabilityElement]:
33
+ pass
@@ -19,10 +19,10 @@ class Metrics(Operation, metaclass=ABCMeta):
19
19
  """
20
20
 
21
21
  filter: Optional[list[str]] = None
22
- result: list[dict[str, Any]] = []
23
22
 
24
23
  def __init__(self) -> None:
25
24
  self.model: Optional[VariabilityModel] = None
25
+ self.result: list[dict[str, Any]] = []
26
26
 
27
27
  @abstractmethod
28
28
  def calculate_metamodel_metrics(self, model: VariabilityModel) -> list[dict[str, Any]]:
@@ -2,6 +2,7 @@ from abc import abstractmethod
2
2
 
3
3
  from flamapy.metamodels.configuration_metamodel.models.configuration import Configuration
4
4
  from flamapy.core.operations import Operation
5
+ from flamapy.core.operations.descriptor import OperationDescriptor, Input
5
6
 
6
7
 
7
8
  class Sampling(Operation):
@@ -9,6 +10,24 @@ class Sampling(Operation):
9
10
  (or configurations) from within a variability model.
10
11
  """
11
12
 
13
+ facade = OperationDescriptor(
14
+ doc=(
15
+ 'Returns a random sample of valid configurations of the given size. When\n'
16
+ 'with_replacement is True, the same configuration may appear more than once.\n'
17
+ '\n'
18
+ '``backend`` selects the analysis plugin; defaults to bdd. sat and bdd\n'
19
+ 'implement (deterministic/enumerating) sampling; "sharpsat" provides almost-\n'
20
+ 'uniform sampling via the optional flamapy-sharpsat plugin.'
21
+ ),
22
+ returns='Union[None, List[Configuration]]',
23
+ name='sampling', operation='Sampling', default_backend='bdd',
24
+ selectable_backend=True,
25
+ inputs=(
26
+ Input('size', int, required=True, setter='set_sample_size'),
27
+ Input('with_replacement', bool, default=False, setter='set_with_replacement'),
28
+ ),
29
+ )
30
+
12
31
  @abstractmethod
13
32
  def __init__(self) -> None:
14
33
  pass