flamapy-z3 2.6.0.dev4__tar.gz → 2.6.0.dev11__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 (33) hide show
  1. {flamapy_z3-2.6.0.dev4 → flamapy_z3-2.6.0.dev11}/PKG-INFO +3 -3
  2. {flamapy_z3-2.6.0.dev4 → flamapy_z3-2.6.0.dev11}/flamapy/metamodels/z3_metamodel/models/z3_model.py +3 -0
  3. flamapy_z3-2.6.0.dev11/flamapy/metamodels/z3_metamodel/operations/interfaces/attribute_optimization.py +12 -0
  4. {flamapy_z3-2.6.0.dev4 → flamapy_z3-2.6.0.dev11}/flamapy/metamodels/z3_metamodel/operations/z3_all_feature_bounds.py +11 -0
  5. {flamapy_z3-2.6.0.dev4 → flamapy_z3-2.6.0.dev11}/flamapy/metamodels/z3_metamodel/operations/z3_false_optional_features.py +6 -6
  6. {flamapy_z3-2.6.0.dev4 → flamapy_z3-2.6.0.dev11}/flamapy/metamodels/z3_metamodel/operations/z3_feature_bounds.py +12 -0
  7. {flamapy_z3-2.6.0.dev4 → flamapy_z3-2.6.0.dev11}/flamapy/metamodels/z3_metamodel/transformations/fm_to_z3.py +52 -1
  8. {flamapy_z3-2.6.0.dev4 → flamapy_z3-2.6.0.dev11}/flamapy_z3.egg-info/PKG-INFO +3 -3
  9. {flamapy_z3-2.6.0.dev4 → flamapy_z3-2.6.0.dev11}/flamapy_z3.egg-info/SOURCES.txt +1 -0
  10. {flamapy_z3-2.6.0.dev4 → flamapy_z3-2.6.0.dev11}/flamapy_z3.egg-info/requires.txt +2 -2
  11. {flamapy_z3-2.6.0.dev4 → flamapy_z3-2.6.0.dev11}/pyproject.toml +3 -3
  12. flamapy_z3-2.6.0.dev11/tests/test_tseytin.py +69 -0
  13. flamapy_z3-2.6.0.dev4/flamapy/metamodels/z3_metamodel/operations/interfaces/attribute_optimization.py +0 -27
  14. {flamapy_z3-2.6.0.dev4 → flamapy_z3-2.6.0.dev11}/README.md +0 -0
  15. {flamapy_z3-2.6.0.dev4 → flamapy_z3-2.6.0.dev11}/flamapy/metamodels/z3_metamodel/__init__.py +0 -0
  16. {flamapy_z3-2.6.0.dev4 → flamapy_z3-2.6.0.dev11}/flamapy/metamodels/z3_metamodel/models/__init__.py +0 -0
  17. {flamapy_z3-2.6.0.dev4 → flamapy_z3-2.6.0.dev11}/flamapy/metamodels/z3_metamodel/operations/__init__.py +0 -0
  18. {flamapy_z3-2.6.0.dev4 → flamapy_z3-2.6.0.dev11}/flamapy/metamodels/z3_metamodel/operations/interfaces/__init__.py +0 -0
  19. {flamapy_z3-2.6.0.dev4 → flamapy_z3-2.6.0.dev11}/flamapy/metamodels/z3_metamodel/operations/z3_attribute_optimization.py +0 -0
  20. {flamapy_z3-2.6.0.dev4 → flamapy_z3-2.6.0.dev11}/flamapy/metamodels/z3_metamodel/operations/z3_backbone.py +0 -0
  21. {flamapy_z3-2.6.0.dev4 → flamapy_z3-2.6.0.dev11}/flamapy/metamodels/z3_metamodel/operations/z3_configurations.py +0 -0
  22. {flamapy_z3-2.6.0.dev4 → flamapy_z3-2.6.0.dev11}/flamapy/metamodels/z3_metamodel/operations/z3_configurations_number.py +0 -0
  23. {flamapy_z3-2.6.0.dev4 → flamapy_z3-2.6.0.dev11}/flamapy/metamodels/z3_metamodel/operations/z3_core_features.py +0 -0
  24. {flamapy_z3-2.6.0.dev4 → flamapy_z3-2.6.0.dev11}/flamapy/metamodels/z3_metamodel/operations/z3_dead_features.py +0 -0
  25. {flamapy_z3-2.6.0.dev4 → flamapy_z3-2.6.0.dev11}/flamapy/metamodels/z3_metamodel/operations/z3_satisfiable.py +0 -0
  26. {flamapy_z3-2.6.0.dev4 → flamapy_z3-2.6.0.dev11}/flamapy/metamodels/z3_metamodel/operations/z3_satisfiable_configuration.py +0 -0
  27. {flamapy_z3-2.6.0.dev4 → flamapy_z3-2.6.0.dev11}/flamapy/metamodels/z3_metamodel/operations/z3_variable_bounds_complex.py +0 -0
  28. {flamapy_z3-2.6.0.dev4 → flamapy_z3-2.6.0.dev11}/flamapy/metamodels/z3_metamodel/transformations/__init__.py +0 -0
  29. {flamapy_z3-2.6.0.dev4 → flamapy_z3-2.6.0.dev11}/flamapy_z3.egg-info/dependency_links.txt +0 -0
  30. {flamapy_z3-2.6.0.dev4 → flamapy_z3-2.6.0.dev11}/flamapy_z3.egg-info/top_level.txt +0 -0
  31. {flamapy_z3-2.6.0.dev4 → flamapy_z3-2.6.0.dev11}/setup.cfg +0 -0
  32. {flamapy_z3-2.6.0.dev4 → flamapy_z3-2.6.0.dev11}/setup.py +0 -0
  33. {flamapy_z3-2.6.0.dev4 → flamapy_z3-2.6.0.dev11}/tests/test_z3_metamodel.py +0 -0
@@ -1,14 +1,14 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flamapy-z3
3
- Version: 2.6.0.dev4
3
+ Version: 2.6.0.dev11
4
4
  Summary: z3-plugin for the automated analysis of feature models
5
5
  Author-email: Flamapy <flamapy@us.es>
6
6
  License-Expression: GPL-3.0-or-later
7
7
  Project-URL: Homepage, https://github.com/flamapy/z3_metamodel
8
8
  Requires-Python: >=3.11
9
9
  Description-Content-Type: text/markdown
10
- Requires-Dist: flamapy-fw~=2.6.0.dev4
11
- Requires-Dist: flamapy-fm~=2.6.0.dev4
10
+ Requires-Dist: flamapy-fw~=2.6.0.dev11
11
+ Requires-Dist: flamapy-fm~=2.6.0.dev11
12
12
  Requires-Dist: z3-solver~=4.14.1.0
13
13
  Provides-Extra: dev
14
14
  Requires-Dist: pytest; extra == "dev"
@@ -35,6 +35,9 @@ class Z3Model(VariabilityModel):
35
35
  self.attributes: dict[str, list[Any]] = {} # attr_name -> [z3var]
36
36
  self.attributes_types: dict[str, AttributeType] = {} # attr_name -> AttributeType
37
37
  self.constraints: list[Any] = [] # list of z3 expressions
38
+ # Auxiliary (Tseytin) boolean variables. Kept out of ``features`` so that feature
39
+ # enumeration/counting/bounds operations ignore them.
40
+ self.auxiliary_variables: list[Any] = []
38
41
  self.original_model: Optional[VariabilityModel] = None
39
42
 
40
43
  def create_const(self, ftype: FeatureType | AttributeType, value: Any) -> Any:
@@ -0,0 +1,12 @@
1
+ """Backward-compatible re-export.
2
+
3
+ ``AttributeOptimization`` and ``OptimizationGoal`` now live in the core framework so
4
+ that every backend shares one interface. This module keeps the historical import path
5
+ ``flamapy.metamodels.z3_metamodel.operations.interfaces.attribute_optimization`` working.
6
+ """
7
+ from flamapy.core.operations.attribute_optimization import (
8
+ AttributeOptimization,
9
+ OptimizationGoal,
10
+ )
11
+
12
+ __all__ = ["AttributeOptimization", "OptimizationGoal"]
@@ -3,6 +3,7 @@ from typing import Any, cast
3
3
 
4
4
  from flamapy.core.models import VariabilityModel
5
5
  from flamapy.core.operations import Operation
6
+ from flamapy.core.operations.descriptor import OperationDescriptor
6
7
  from flamapy.metamodels.z3_metamodel.models import Z3Model
7
8
  from flamapy.metamodels.z3_metamodel.operations import Z3FeatureBounds
8
9
  from flamapy.metamodels.fm_metamodel.models import FeatureType
@@ -16,6 +17,16 @@ class Z3AllFeatureBounds(Operation):
16
17
  (Integer, Real, String length) in the Z3 model.
17
18
  """
18
19
 
20
+ facade = OperationDescriptor(
21
+ doc=(
22
+ 'Returns the value bounds for all typed (Integer, Real, String) attribute\n'
23
+ "features in the model. Each entry maps a feature name to a dict with 'min',\n"
24
+ "'max', and 'bounded' keys. Requires the z3 metamodel plugin."
25
+ ),
26
+ returns='Union[None, Dict[str, Dict[str, Any]]]',
27
+ name='all_feature_bounds', operation='Z3AllFeatureBounds', default_backend='z3'
28
+ )
29
+
19
30
  def __init__(self) -> None:
20
31
  # El resultado será un diccionario donde la clave es el nombre de la variable
21
32
  # y el valor es el diccionario de bounds (min, max, bounded).
@@ -10,7 +10,7 @@ from flamapy.metamodels.fm_metamodel.models import FeatureModel
10
10
  from flamapy.metamodels.z3_metamodel.models import Z3Model
11
11
 
12
12
 
13
- LOGGER = logging.getLogger('PySATFalseOptionalFeatures')
13
+ LOGGER = logging.getLogger('Z3FalseOptionalFeatures')
14
14
 
15
15
 
16
16
  class Z3FalseOptionalFeatures(FalseOptionalFeatures):
@@ -26,11 +26,11 @@ class Z3FalseOptionalFeatures(FalseOptionalFeatures):
26
26
 
27
27
  def execute(self, model: VariabilityModel) -> 'Z3FalseOptionalFeatures':
28
28
  z3_model = cast(Z3Model, model)
29
- try:
30
- feature_model = cast(FeatureModel, model.original_model)
31
- except FlamaException:
32
- LOGGER.exception("The transformation didn't attach the source model, "
33
- "which is required for this operation.")
29
+ original_model = getattr(model, 'original_model', None)
30
+ if original_model is None:
31
+ raise FlamaException("The transformation didn't attach the source model, "
32
+ "which is required for this operation.")
33
+ feature_model = cast(FeatureModel, original_model)
34
34
  self._result = get_false_optional_features(z3_model, feature_model)
35
35
  return self
36
36
 
@@ -5,6 +5,7 @@ import z3
5
5
 
6
6
  from flamapy.core.models import VariabilityModel
7
7
  from flamapy.core.operations import Operation
8
+ from flamapy.core.operations.descriptor import OperationDescriptor, Input
8
9
  from flamapy.metamodels.z3_metamodel.models import Z3Model
9
10
  from flamapy.metamodels.fm_metamodel.models import FeatureType
10
11
 
@@ -20,6 +21,17 @@ class Z3FeatureBounds(Operation):
20
21
  and the length bounds (min_len/max_len) for string features, with safety checks.
21
22
  """
22
23
 
24
+ facade = OperationDescriptor(
25
+ doc=(
26
+ 'Returns the value bounds for a specific typed attribute feature. The result\n'
27
+ "dict contains 'min', 'max', and 'bounded' keys. Requires the z3 metamodel\n"
28
+ 'plugin.'
29
+ ),
30
+ returns='Union[None, Dict[str, Any]]',
31
+ name='feature_bounds', operation='Z3FeatureBounds', default_backend='z3',
32
+ inputs=(Input('variable_name', str, required=True, setter='set_variable_name'),),
33
+ )
34
+
23
35
  def __init__(self) -> None:
24
36
  self._result: dict[str, Any] = {}
25
37
  self._variable_name: Optional[str] = None
@@ -61,10 +61,16 @@ class FmToZ3(ModelToModel):
61
61
  def get_destination_extension() -> str:
62
62
  return "z3"
63
63
 
64
- def __init__(self, source_model: FeatureModel) -> None:
64
+ def __init__(self, source_model: FeatureModel, cnf_method: str = 'direct') -> None:
65
65
  self.source_model = source_model
66
66
  self.destination_model: Z3Model = Z3Model()
67
67
  self._counter: int = 0
68
+ # 'direct' (default) builds Z3 boolean expressions straight from the AST. 'tseytin'
69
+ # applies the Tseytin CNF encoding to purely propositional boolean constraints
70
+ # (others fall back to 'direct'). Framework-only opt-in; Z3 already clausifies
71
+ # internally, so this exists for cross-backend uniformity/experimentation, not speed.
72
+ self.cnf_method = cnf_method
73
+ self._aux_counter: int = 0
68
74
 
69
75
  def transform(self) -> Z3Model:
70
76
  self.destination_model = Z3Model()
@@ -258,9 +264,54 @@ class FmToZ3(ModelToModel):
258
264
  self._add_inactive_parent_constraints(parent, children)
259
265
 
260
266
  def _add_constraint_formula(self, ctc: Constraint) -> None:
267
+ if self.cnf_method == 'tseytin' and self._is_propositional_boolean(ctc):
268
+ self._add_tseytin_constraint(ctc)
269
+ return
261
270
  expr = self._get_expression(ctc.ast.root, None)
262
271
  self.destination_model.add_constraint(expr)
263
272
 
273
+ def _is_propositional_boolean(self, ctc: Constraint) -> bool:
274
+ """Whether a constraint is purely propositional over boolean features, i.e. safe
275
+ for the Tseytin CNF encoding. Arithmetic/aggregation/typed constraints are not."""
276
+ if any(op not in LOGICAL_OPERATORS for op in ctc.ast.get_operators()):
277
+ return False
278
+ for operand in ctc.ast.get_operands():
279
+ if not isinstance(operand, str):
280
+ return False
281
+ info = self.destination_model.get_variable(operand)
282
+ if info is None or info.ftype != FeatureType.BOOLEAN:
283
+ return False
284
+ return True
285
+
286
+ def _add_tseytin_constraint(self, ctc: Constraint) -> None:
287
+ clauses, aux_names = ctc.ast.get_clauses_with_aux(method='tseytin')
288
+ ctx = self.destination_model.ctx
289
+ # Fresh, globally-unique Z3 Bool per auxiliary name (same-named Bools in one Z3
290
+ # context are the same variable, so per-constraint names must not collide).
291
+ local: dict[str, Any] = {}
292
+ for name in aux_names:
293
+ self._aux_counter += 1
294
+ aux_var = z3.Bool(f'__tseytin_aux_{self._aux_counter}', ctx=ctx)
295
+ local[name] = aux_var
296
+ self.destination_model.auxiliary_variables.append(aux_var)
297
+
298
+ def literal_expr(term: str) -> Any:
299
+ negated = term.startswith('-')
300
+ name = term[1:] if negated else term
301
+ if name in local:
302
+ expr = local[name]
303
+ else:
304
+ info = self.destination_model.get_variable(name)
305
+ if info is None:
306
+ raise FlamaException(f'Unsupported feature: {name}')
307
+ expr = info.sel
308
+ return z3.Not(expr) if negated else expr
309
+
310
+ for clause in clauses:
311
+ literals = [literal_expr(term) for term in clause]
312
+ formula = literals[0] if len(literals) == 1 else z3.Or(*literals)
313
+ self.destination_model.add_constraint(formula)
314
+
264
315
  def _get_expression(self, node: Node, parent: Optional[Node]) -> z3.ExprRef:
265
316
  if node.is_term():
266
317
  return self._get_term_expression(node, parent)
@@ -1,14 +1,14 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flamapy-z3
3
- Version: 2.6.0.dev4
3
+ Version: 2.6.0.dev11
4
4
  Summary: z3-plugin for the automated analysis of feature models
5
5
  Author-email: Flamapy <flamapy@us.es>
6
6
  License-Expression: GPL-3.0-or-later
7
7
  Project-URL: Homepage, https://github.com/flamapy/z3_metamodel
8
8
  Requires-Python: >=3.11
9
9
  Description-Content-Type: text/markdown
10
- Requires-Dist: flamapy-fw~=2.6.0.dev4
11
- Requires-Dist: flamapy-fm~=2.6.0.dev4
10
+ Requires-Dist: flamapy-fw~=2.6.0.dev11
11
+ Requires-Dist: flamapy-fm~=2.6.0.dev11
12
12
  Requires-Dist: z3-solver~=4.14.1.0
13
13
  Provides-Extra: dev
14
14
  Requires-Dist: pytest; extra == "dev"
@@ -26,4 +26,5 @@ flamapy_z3.egg-info/SOURCES.txt
26
26
  flamapy_z3.egg-info/dependency_links.txt
27
27
  flamapy_z3.egg-info/requires.txt
28
28
  flamapy_z3.egg-info/top_level.txt
29
+ tests/test_tseytin.py
29
30
  tests/test_z3_metamodel.py
@@ -1,5 +1,5 @@
1
- flamapy-fw~=2.6.0.dev4
2
- flamapy-fm~=2.6.0.dev4
1
+ flamapy-fw~=2.6.0.dev11
2
+ flamapy-fm~=2.6.0.dev11
3
3
  z3-solver~=4.14.1.0
4
4
 
5
5
  [dev]
@@ -4,15 +4,15 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "flamapy-z3"
7
- version = "2.6.0.dev4"
7
+ version = "2.6.0.dev11"
8
8
  description = "z3-plugin for the automated analysis of feature models"
9
9
  readme = "README.md"
10
10
  license = "GPL-3.0-or-later"
11
11
  authors = [{ name = "Flamapy", email = "flamapy@us.es" }]
12
12
  requires-python = ">=3.11"
13
13
  dependencies = [
14
- "flamapy-fw~=2.6.0.dev4",
15
- "flamapy-fm~=2.6.0.dev4",
14
+ "flamapy-fw~=2.6.0.dev11",
15
+ "flamapy-fm~=2.6.0.dev11",
16
16
  "z3-solver~=4.14.1.0",
17
17
  ]
18
18
 
@@ -0,0 +1,69 @@
1
+ """The opt-in Tseytin CNF encoding must yield the same Z3 analysis results as the
2
+ default direct translation, and must fall back to the direct translation for any
3
+ constraint that is not purely propositional over boolean features."""
4
+ import os
5
+ import tempfile
6
+
7
+ from flamapy.metamodels.fm_metamodel.transformations import UVLReader
8
+ from flamapy.metamodels.z3_metamodel.transformations import FmToZ3
9
+ from flamapy.metamodels.z3_metamodel.operations.z3_satisfiable import Z3Satisfiable
10
+ from flamapy.metamodels.z3_metamodel.operations.z3_configurations import Z3Configurations
11
+ from flamapy.metamodels.z3_metamodel.operations.z3_configurations_number import (
12
+ Z3ConfigurationsNumber,
13
+ )
14
+
15
+
16
+ # Purely propositional model: every cross-tree constraint is boolean.
17
+ _PROP_UVL = """features
18
+ Root {abstract}
19
+ optional
20
+ A
21
+ B
22
+ C
23
+ D
24
+ constraints
25
+ (A | B) => (C <=> D)
26
+ A => !B
27
+ """
28
+
29
+ # All-arithmetic constraints; the Tseytin path must fall back to direct translation.
30
+ _ARITH_MODEL = 'resources/models/uvl_models/fm03_integer_conditional_bounded.uvl'
31
+
32
+
33
+ def _build_from_uvl_text(text, cnf_method):
34
+ handle, path = tempfile.mkstemp(suffix='.uvl')
35
+ try:
36
+ with os.fdopen(handle, 'w') as file:
37
+ file.write(text)
38
+ return FmToZ3(UVLReader(path).transform(), cnf_method=cnf_method).transform()
39
+ finally:
40
+ os.remove(path)
41
+
42
+
43
+ def _projected(model):
44
+ configs = Z3Configurations().execute(model).get_result()
45
+ return {frozenset(k for k, v in c.elements.items() if v is True) for c in configs}
46
+
47
+
48
+ def test_tseytin_matches_direct_on_propositional_model() -> None:
49
+ direct = _build_from_uvl_text(_PROP_UVL, 'direct')
50
+ tseytin = _build_from_uvl_text(_PROP_UVL, 'tseytin')
51
+
52
+ assert Z3ConfigurationsNumber().execute(direct).get_result() == \
53
+ Z3ConfigurationsNumber().execute(tseytin).get_result()
54
+ assert _projected(direct) == _projected(tseytin)
55
+ assert tseytin.auxiliary_variables # gates were introduced
56
+ # Auxiliary variables must not have been registered as features.
57
+ assert not set(str(a) for a in tseytin.auxiliary_variables) & set(tseytin.features.keys())
58
+
59
+
60
+ def test_tseytin_falls_back_for_arithmetic_constraints() -> None:
61
+ direct = FmToZ3(UVLReader(_ARITH_MODEL).transform(), cnf_method='direct').transform()
62
+ tseytin = FmToZ3(UVLReader(_ARITH_MODEL).transform(), cnf_method='tseytin').transform()
63
+
64
+ # Nothing was propositional, so no auxiliary variables were created.
65
+ assert tseytin.auxiliary_variables == []
66
+ assert Z3Satisfiable().execute(direct).get_result() == \
67
+ Z3Satisfiable().execute(tseytin).get_result()
68
+ assert Z3ConfigurationsNumber().execute(direct).get_result() == \
69
+ Z3ConfigurationsNumber().execute(tseytin).get_result()
@@ -1,27 +0,0 @@
1
- from abc import abstractmethod
2
- from enum import Enum
3
-
4
- from flamapy.core.operations import Operation
5
- from flamapy.metamodels.configuration_metamodel.models import Configuration
6
- from flamapy.metamodels.fm_metamodel.models import Attribute
7
-
8
-
9
- class OptimizationGoal(Enum):
10
- MAXIMIZE = 'Maximize'
11
- MINIMIZE = 'Minimize'
12
-
13
-
14
- class AttributeOptimization(Operation):
15
- """This operation returns the configurations that optimize the given attribute(s)."""
16
-
17
- @abstractmethod
18
- def __init__(self) -> None:
19
- pass
20
-
21
- @abstractmethod
22
- def set_attributes(self, attributes: dict[Attribute, OptimizationGoal]) -> None:
23
- pass
24
-
25
- @abstractmethod
26
- def optimize(self) -> list[Configuration]:
27
- pass