flamapy 2.6.0.dev3__tar.gz → 2.6.0.dev5__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.
- {flamapy-2.6.0.dev3 → flamapy-2.6.0.dev5}/PKG-INFO +8 -6
- {flamapy-2.6.0.dev3 → flamapy-2.6.0.dev5}/flamapy/commands/__init__.py +24 -11
- {flamapy-2.6.0.dev3 → flamapy-2.6.0.dev5}/flamapy/interfaces/python/flamapy_feature_model.py +87 -3
- {flamapy-2.6.0.dev3 → flamapy-2.6.0.dev5}/flamapy.egg-info/PKG-INFO +8 -6
- {flamapy-2.6.0.dev3 → flamapy-2.6.0.dev5}/flamapy.egg-info/SOURCES.txt +2 -1
- flamapy-2.6.0.dev5/flamapy.egg-info/requires.txt +15 -0
- {flamapy-2.6.0.dev3 → flamapy-2.6.0.dev5}/pyproject.toml +10 -6
- {flamapy-2.6.0.dev3 → flamapy-2.6.0.dev5}/tests/test_basic_units.py +1 -1
- flamapy-2.6.0.dev5/tests/test_new_operations.py +110 -0
- flamapy-2.6.0.dev3/flamapy.egg-info/requires.txt +0 -12
- {flamapy-2.6.0.dev3 → flamapy-2.6.0.dev5}/LICENSE +0 -0
- {flamapy-2.6.0.dev3 → flamapy-2.6.0.dev5}/README.md +0 -0
- {flamapy-2.6.0.dev3 → flamapy-2.6.0.dev5}/flamapy/interfaces/python/__init__.py +0 -0
- {flamapy-2.6.0.dev3 → flamapy-2.6.0.dev5}/flamapy.egg-info/dependency_links.txt +0 -0
- {flamapy-2.6.0.dev3 → flamapy-2.6.0.dev5}/flamapy.egg-info/entry_points.txt +0 -0
- {flamapy-2.6.0.dev3 → flamapy-2.6.0.dev5}/flamapy.egg-info/top_level.txt +0 -0
- {flamapy-2.6.0.dev3 → flamapy-2.6.0.dev5}/setup.cfg +0 -0
- {flamapy-2.6.0.dev3 → flamapy-2.6.0.dev5}/setup.py +0 -0
- {flamapy-2.6.0.dev3 → flamapy-2.6.0.dev5}/tests/test_cli_basic_units.py +0 -0
- {flamapy-2.6.0.dev3 → flamapy-2.6.0.dev5}/tests/test_documentation_examples.py +0 -0
- {flamapy-2.6.0.dev3 → flamapy-2.6.0.dev5}/tests/test_fama_suite_bdd.py +0 -0
- {flamapy-2.6.0.dev3 → flamapy-2.6.0.dev5}/tests/test_fama_suite_sat.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flamapy
|
|
3
|
-
Version: 2.6.0.
|
|
3
|
+
Version: 2.6.0.dev5
|
|
4
4
|
Summary: Flamapy feature model is a distribution of the flama framework containing all plugins required to analyze feature models. It also offers a richer API and a complete command line interface and documentation.
|
|
5
5
|
Author-email: Flamapy <flamapy@us.es>
|
|
6
6
|
License-Expression: GPL-3.0-or-later
|
|
@@ -8,17 +8,19 @@ Project-URL: Homepage, https://github.com/flamapy/flamapy-feature-model
|
|
|
8
8
|
Requires-Python: >=3.11
|
|
9
9
|
Description-Content-Type: text/markdown
|
|
10
10
|
License-File: LICENSE
|
|
11
|
-
Requires-Dist: flamapy-fw~=2.6.0.
|
|
12
|
-
Requires-Dist: flamapy-fm~=2.6.0.
|
|
13
|
-
Requires-Dist: flamapy-sat~=2.6.0.
|
|
14
|
-
Requires-Dist: flamapy-bdd~=2.6.0.
|
|
15
|
-
Requires-Dist: flamapy-z3~=2.6.0.
|
|
11
|
+
Requires-Dist: flamapy-fw~=2.6.0.dev5
|
|
12
|
+
Requires-Dist: flamapy-fm~=2.6.0.dev5
|
|
13
|
+
Requires-Dist: flamapy-sat~=2.6.0.dev5
|
|
14
|
+
Requires-Dist: flamapy-bdd~=2.6.0.dev5
|
|
15
|
+
Requires-Dist: flamapy-z3~=2.6.0.dev5
|
|
16
16
|
Provides-Extra: dev
|
|
17
17
|
Requires-Dist: pytest; extra == "dev"
|
|
18
18
|
Requires-Dist: pytest-mock; extra == "dev"
|
|
19
19
|
Requires-Dist: prospector; extra == "dev"
|
|
20
20
|
Requires-Dist: mypy; extra == "dev"
|
|
21
21
|
Requires-Dist: coverage; extra == "dev"
|
|
22
|
+
Provides-Extra: sharpsat
|
|
23
|
+
Requires-Dist: flamapy-sharpsat~=2.6.0.dev4; extra == "sharpsat"
|
|
22
24
|
Dynamic: license-file
|
|
23
25
|
|
|
24
26
|
# flamapy
|
|
@@ -5,7 +5,7 @@ from functools import wraps
|
|
|
5
5
|
import inspect
|
|
6
6
|
from pathlib import Path
|
|
7
7
|
from shutil import copytree
|
|
8
|
-
from typing import Any, List, Tuple, Optional
|
|
8
|
+
from typing import Any, List, Tuple, Optional, Union, get_args, get_origin
|
|
9
9
|
from types import FunctionType
|
|
10
10
|
|
|
11
11
|
from flamapy.interfaces.python.flamapy_feature_model import FLAMAFeatureModel
|
|
@@ -92,30 +92,43 @@ def generate_plugin(args): # type: ignore
|
|
|
92
92
|
print("Plugin generated!")
|
|
93
93
|
|
|
94
94
|
|
|
95
|
+
def resolve_annotation_type(annotation: Any) -> type:
|
|
96
|
+
"""Map a parameter annotation to a concrete type usable as an argparse 'type'.
|
|
97
|
+
|
|
98
|
+
Unwraps Optional[X]/Union[X, None] to X; falls back to str for anything
|
|
99
|
+
that is not a plain class (argparse requires a callable).
|
|
100
|
+
"""
|
|
101
|
+
if annotation is inspect.Parameter.empty:
|
|
102
|
+
return str
|
|
103
|
+
if get_origin(annotation) is Union:
|
|
104
|
+
args = [arg for arg in get_args(annotation) if arg is not type(None)]
|
|
105
|
+
annotation = args[0] if args else str
|
|
106
|
+
return annotation if isinstance(annotation, type) else str
|
|
107
|
+
|
|
108
|
+
|
|
95
109
|
def setup_dynamic_commands(subparsers, dynamic_commands): # type: ignore
|
|
96
110
|
for name, docstring, method, parameters in dynamic_commands:
|
|
97
111
|
subparser = subparsers.add_parser(name, help=docstring)
|
|
98
112
|
subparser.add_argument("model_path", type=str, help="Path to the feature model file")
|
|
99
|
-
if "configuration_path" in [param.name for param in parameters]:
|
|
100
|
-
subparser.add_argument(
|
|
101
|
-
"--configuration_path",
|
|
102
|
-
type=str,
|
|
103
|
-
help="Path to the configuration file",
|
|
104
|
-
required=False,
|
|
105
|
-
)
|
|
106
113
|
for param in parameters:
|
|
107
114
|
arg_name = param.name
|
|
108
115
|
if arg_name not in ["model_path"]: # Avoid duplicates
|
|
116
|
+
arg_type = resolve_annotation_type(param.annotation)
|
|
109
117
|
if param.default == param.empty: # Positional argument
|
|
118
|
+
subparser.add_argument(arg_name, type=arg_type, help=arg_type.__name__)
|
|
119
|
+
elif arg_type is bool: # Boolean flag (type=bool would parse 'False' as True)
|
|
110
120
|
subparser.add_argument(
|
|
111
|
-
arg_name,
|
|
121
|
+
f"--{arg_name}",
|
|
122
|
+
action=argparse.BooleanOptionalAction,
|
|
123
|
+
default=param.default,
|
|
124
|
+
help="Optional bool",
|
|
112
125
|
)
|
|
113
126
|
else: # Optional argument
|
|
114
127
|
subparser.add_argument(
|
|
115
128
|
f"--{arg_name}",
|
|
116
|
-
type=
|
|
129
|
+
type=arg_type,
|
|
117
130
|
default=param.default,
|
|
118
|
-
help=f"Optional {
|
|
131
|
+
help=f"Optional {arg_type.__name__}",
|
|
119
132
|
)
|
|
120
133
|
subparser.set_defaults(func=method, method_name=name, parameters=parameters)
|
|
121
134
|
|
{flamapy-2.6.0.dev3 → flamapy-2.6.0.dev5}/flamapy/interfaces/python/flamapy_feature_model.py
RENAMED
|
@@ -5,6 +5,7 @@ from flamapy.core.discover import DiscoverMetamodels
|
|
|
5
5
|
from flamapy.core.models import VariabilityModel
|
|
6
6
|
from flamapy.metamodels.fm_metamodel.models import FeatureModel
|
|
7
7
|
from flamapy.core.exceptions import FlamaException
|
|
8
|
+
from flamapy.core.operations import OptimizationGoal
|
|
8
9
|
from flamapy.metamodels.configuration_metamodel.models import Configuration
|
|
9
10
|
|
|
10
11
|
logger = logging.getLogger(__name__)
|
|
@@ -16,6 +17,7 @@ class Backend(str, Enum):
|
|
|
16
17
|
SAT = "sat"
|
|
17
18
|
BDD = "bdd"
|
|
18
19
|
Z3 = "z3"
|
|
20
|
+
SHARPSAT = "sharpsat"
|
|
19
21
|
|
|
20
22
|
|
|
21
23
|
class FLAMAFeatureModel:
|
|
@@ -24,6 +26,7 @@ class FLAMAFeatureModel:
|
|
|
24
26
|
Backend.SAT: ("_transform_to_sat", "sat_model"),
|
|
25
27
|
Backend.BDD: ("_transform_to_bdd", "bdd_model"),
|
|
26
28
|
Backend.Z3: ("_transform_to_z3", "z3_model"),
|
|
29
|
+
Backend.SHARPSAT: ("_transform_to_sharpsat", "sharpsat_model"),
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
def __init__(self, model_path: str, backend: Optional[str] = None):
|
|
@@ -42,6 +45,7 @@ class FLAMAFeatureModel:
|
|
|
42
45
|
self.sat_model = None
|
|
43
46
|
self.bdd_model = None
|
|
44
47
|
self.z3_model = None
|
|
48
|
+
self.sharpsat_model = None
|
|
45
49
|
self.diagnosis_model = None
|
|
46
50
|
self.backend = Backend(backend) if backend is not None else None
|
|
47
51
|
|
|
@@ -84,6 +88,14 @@ class FLAMAFeatureModel:
|
|
|
84
88
|
if self.z3_model is None:
|
|
85
89
|
self.z3_model = self.discover_metamodel.use_transformation_m2m(self.fm_model, "z3")
|
|
86
90
|
|
|
91
|
+
def _transform_to_sharpsat(self) -> None:
|
|
92
|
+
if self.sharpsat_model is None:
|
|
93
|
+
# Requires the optional flamapy-sharpsat plugin. use_transformation_m2m raises a
|
|
94
|
+
# FlamaException if it is not installed, which the operations surface to the caller.
|
|
95
|
+
self.sharpsat_model = self.discover_metamodel.use_transformation_m2m(
|
|
96
|
+
self.fm_model, "sharpsat"
|
|
97
|
+
)
|
|
98
|
+
|
|
87
99
|
def _transform_to_diagnosis(self) -> None:
|
|
88
100
|
if self.diagnosis_model is None:
|
|
89
101
|
self.diagnosis_model = self.discover_metamodel.use_transformation_m2m(
|
|
@@ -338,7 +350,8 @@ class FLAMAFeatureModel:
|
|
|
338
350
|
combinations of features, taking into account the constraints and
|
|
339
351
|
dependencies between features.
|
|
340
352
|
|
|
341
|
-
``backend`` selects the analysis plugin ("sat", "bdd" or "
|
|
353
|
+
``backend`` selects the analysis plugin ("sat", "bdd", "z3", or "sharpsat" for a
|
|
354
|
+
scalable approximate count via the optional flamapy-sharpsat plugin); defaults to bdd.
|
|
342
355
|
"""
|
|
343
356
|
try:
|
|
344
357
|
model = self._select_backend(backend, Backend.BDD)
|
|
@@ -604,8 +617,9 @@ class FLAMAFeatureModel:
|
|
|
604
617
|
Returns a random sample of valid configurations of the given size. When
|
|
605
618
|
with_replacement is True, the same configuration may appear more than once.
|
|
606
619
|
|
|
607
|
-
``backend`` selects the analysis plugin; defaults to bdd.
|
|
608
|
-
|
|
620
|
+
``backend`` selects the analysis plugin; defaults to bdd. sat and bdd implement
|
|
621
|
+
(deterministic/enumerating) sampling; "sharpsat" provides almost-uniform sampling
|
|
622
|
+
via the optional flamapy-sharpsat plugin.
|
|
609
623
|
"""
|
|
610
624
|
try:
|
|
611
625
|
model = self._select_backend(backend, Backend.BDD)
|
|
@@ -618,6 +632,76 @@ class FLAMAFeatureModel:
|
|
|
618
632
|
logger.error("Error: %s", exception)
|
|
619
633
|
return None
|
|
620
634
|
|
|
635
|
+
def t_wise_sampling(
|
|
636
|
+
self, t: int = 2, backend: Optional[str] = None
|
|
637
|
+
) -> Union[None, List[Configuration]]:
|
|
638
|
+
"""
|
|
639
|
+
Returns a t-wise (combinatorial) sample: a set of valid configurations that covers
|
|
640
|
+
every satisfiable combination of ``t`` feature selections (pairwise for ``t = 2``).
|
|
641
|
+
|
|
642
|
+
``backend`` selects the analysis plugin; only "sat" implements t-wise sampling.
|
|
643
|
+
"""
|
|
644
|
+
try:
|
|
645
|
+
model = self._select_backend(backend, Backend.SAT)
|
|
646
|
+
operation = self.discover_metamodel.get_operation(model, "PySATTWiseSampling")
|
|
647
|
+
operation.set_t(t)
|
|
648
|
+
operation.execute(model)
|
|
649
|
+
return operation.get_result()
|
|
650
|
+
except FlamaException as exception:
|
|
651
|
+
logger.error("Error: %s", exception)
|
|
652
|
+
return None
|
|
653
|
+
|
|
654
|
+
def minimum_configuration(
|
|
655
|
+
self, backend: Optional[str] = None
|
|
656
|
+
) -> Union[None, Configuration]:
|
|
657
|
+
"""
|
|
658
|
+
Returns a valid configuration with the fewest selected features (the minimum
|
|
659
|
+
working configuration).
|
|
660
|
+
|
|
661
|
+
``backend`` selects the analysis plugin; only "sat" implements this operation.
|
|
662
|
+
"""
|
|
663
|
+
try:
|
|
664
|
+
model = self._select_backend(backend, Backend.SAT)
|
|
665
|
+
operation = self.discover_metamodel.get_operation(model, "PySATMinimumConfiguration")
|
|
666
|
+
operation.execute(model)
|
|
667
|
+
return operation.get_result()
|
|
668
|
+
except FlamaException as exception:
|
|
669
|
+
logger.error("Error: %s", exception)
|
|
670
|
+
return None
|
|
671
|
+
|
|
672
|
+
def attribute_optimization(
|
|
673
|
+
self, objectives: Any, backend: Optional[str] = None
|
|
674
|
+
) -> Union[None, List[Configuration]]:
|
|
675
|
+
"""
|
|
676
|
+
Returns the configuration(s) that optimize one or more numeric feature attributes.
|
|
677
|
+
|
|
678
|
+
``objectives`` is either a single attribute name (defaults to minimizing it) or a
|
|
679
|
+
mapping ``{attribute_name: "Minimize"|"Maximize"}``. ``backend`` selects the plugin:
|
|
680
|
+
"sat" performs single-objective MaxSAT optimization; "z3" additionally supports
|
|
681
|
+
typed attributes and multi-objective (Pareto) optimization. Defaults to "sat".
|
|
682
|
+
"""
|
|
683
|
+
def _goal(value: Any) -> OptimizationGoal:
|
|
684
|
+
return (OptimizationGoal.MINIMIZE
|
|
685
|
+
if str(value).lower().startswith('min')
|
|
686
|
+
else OptimizationGoal.MAXIMIZE)
|
|
687
|
+
|
|
688
|
+
if isinstance(objectives, str):
|
|
689
|
+
attributes = {objectives: OptimizationGoal.MINIMIZE}
|
|
690
|
+
else:
|
|
691
|
+
attributes = {name: _goal(goal) for name, goal in dict(objectives).items()}
|
|
692
|
+
|
|
693
|
+
try:
|
|
694
|
+
model = self._select_backend(backend, Backend.SAT)
|
|
695
|
+
operation = self.discover_metamodel.get_operation(model, "AttributeOptimization")
|
|
696
|
+
operation.set_attributes(attributes)
|
|
697
|
+
operation.execute(model)
|
|
698
|
+
# z3 returns (configuration, values) tuples; sat returns configurations.
|
|
699
|
+
return [item[0] if isinstance(item, tuple) else item
|
|
700
|
+
for item in operation.get_result()]
|
|
701
|
+
except FlamaException as exception:
|
|
702
|
+
logger.error("Error: %s", exception)
|
|
703
|
+
return None
|
|
704
|
+
|
|
621
705
|
# Z3-based operations
|
|
622
706
|
|
|
623
707
|
def all_feature_bounds(self) -> Union[None, Dict[str, Dict[str, Any]]]:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flamapy
|
|
3
|
-
Version: 2.6.0.
|
|
3
|
+
Version: 2.6.0.dev5
|
|
4
4
|
Summary: Flamapy feature model is a distribution of the flama framework containing all plugins required to analyze feature models. It also offers a richer API and a complete command line interface and documentation.
|
|
5
5
|
Author-email: Flamapy <flamapy@us.es>
|
|
6
6
|
License-Expression: GPL-3.0-or-later
|
|
@@ -8,17 +8,19 @@ Project-URL: Homepage, https://github.com/flamapy/flamapy-feature-model
|
|
|
8
8
|
Requires-Python: >=3.11
|
|
9
9
|
Description-Content-Type: text/markdown
|
|
10
10
|
License-File: LICENSE
|
|
11
|
-
Requires-Dist: flamapy-fw~=2.6.0.
|
|
12
|
-
Requires-Dist: flamapy-fm~=2.6.0.
|
|
13
|
-
Requires-Dist: flamapy-sat~=2.6.0.
|
|
14
|
-
Requires-Dist: flamapy-bdd~=2.6.0.
|
|
15
|
-
Requires-Dist: flamapy-z3~=2.6.0.
|
|
11
|
+
Requires-Dist: flamapy-fw~=2.6.0.dev5
|
|
12
|
+
Requires-Dist: flamapy-fm~=2.6.0.dev5
|
|
13
|
+
Requires-Dist: flamapy-sat~=2.6.0.dev5
|
|
14
|
+
Requires-Dist: flamapy-bdd~=2.6.0.dev5
|
|
15
|
+
Requires-Dist: flamapy-z3~=2.6.0.dev5
|
|
16
16
|
Provides-Extra: dev
|
|
17
17
|
Requires-Dist: pytest; extra == "dev"
|
|
18
18
|
Requires-Dist: pytest-mock; extra == "dev"
|
|
19
19
|
Requires-Dist: prospector; extra == "dev"
|
|
20
20
|
Requires-Dist: mypy; extra == "dev"
|
|
21
21
|
Requires-Dist: coverage; extra == "dev"
|
|
22
|
+
Provides-Extra: sharpsat
|
|
23
|
+
Requires-Dist: flamapy-sharpsat~=2.6.0.dev4; extra == "sharpsat"
|
|
22
24
|
Dynamic: license-file
|
|
23
25
|
|
|
24
26
|
# flamapy
|
|
@@ -4,18 +4,18 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "flamapy"
|
|
7
|
-
version = "2.6.0.
|
|
7
|
+
version = "2.6.0.dev5"
|
|
8
8
|
description = "Flamapy feature model is a distribution of the flama framework containing all plugins required to analyze feature models. It also offers a richer API and a complete command line interface and documentation."
|
|
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.
|
|
15
|
-
"flamapy-fm~=2.6.0.
|
|
16
|
-
"flamapy-sat~=2.6.0.
|
|
17
|
-
"flamapy-bdd~=2.6.0.
|
|
18
|
-
"flamapy-z3~=2.6.0.
|
|
14
|
+
"flamapy-fw~=2.6.0.dev5",
|
|
15
|
+
"flamapy-fm~=2.6.0.dev5",
|
|
16
|
+
"flamapy-sat~=2.6.0.dev5",
|
|
17
|
+
"flamapy-bdd~=2.6.0.dev5",
|
|
18
|
+
"flamapy-z3~=2.6.0.dev5",
|
|
19
19
|
]
|
|
20
20
|
|
|
21
21
|
[project.optional-dependencies]
|
|
@@ -26,6 +26,10 @@ dev = [
|
|
|
26
26
|
"mypy",
|
|
27
27
|
"coverage",
|
|
28
28
|
]
|
|
29
|
+
# Optional scalable counting/sampling backend (enables backend="sharpsat").
|
|
30
|
+
sharpsat = [
|
|
31
|
+
"flamapy-sharpsat~=2.6.0.dev4",
|
|
32
|
+
]
|
|
29
33
|
|
|
30
34
|
[project.scripts]
|
|
31
35
|
flamapy = "flamapy.commands:flamapy_cli"
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Facade tests for the newly exposed operations: attribute optimization, minimum
|
|
2
|
+
configuration, t-wise sampling, and the sharpsat backend."""
|
|
3
|
+
import itertools
|
|
4
|
+
import os
|
|
5
|
+
import tempfile
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from flamapy.interfaces.python.flamapy_feature_model import FLAMAFeatureModel
|
|
10
|
+
|
|
11
|
+
_ATTRIBUTED_UVL = """features
|
|
12
|
+
Root {abstract}
|
|
13
|
+
optional
|
|
14
|
+
A {Cost 3}
|
|
15
|
+
B {Cost 5}
|
|
16
|
+
C {Cost 2}
|
|
17
|
+
constraints
|
|
18
|
+
A | B
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
_PLAIN_UVL = """features
|
|
22
|
+
Root {abstract}
|
|
23
|
+
optional
|
|
24
|
+
A
|
|
25
|
+
B
|
|
26
|
+
C
|
|
27
|
+
D
|
|
28
|
+
constraints
|
|
29
|
+
(A | B) => (C <=> D)
|
|
30
|
+
A => !B
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _model(uvl):
|
|
35
|
+
handle, path = tempfile.mkstemp(suffix='.uvl')
|
|
36
|
+
with os.fdopen(handle, 'w') as file:
|
|
37
|
+
file.write(uvl)
|
|
38
|
+
return path
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _sel(configuration):
|
|
42
|
+
return {name for name, value in configuration.elements.items() if value}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# --- attribute optimization -------------------------------------------------
|
|
46
|
+
|
|
47
|
+
def test_attribute_optimization_sat_minimize():
|
|
48
|
+
fm = FLAMAFeatureModel(_model(_ATTRIBUTED_UVL))
|
|
49
|
+
result = fm.attribute_optimization("Cost", backend="sat")
|
|
50
|
+
assert [_sel(c) for c in result] == [{"A", "Root"}]
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_attribute_optimization_sat_maximize():
|
|
54
|
+
fm = FLAMAFeatureModel(_model(_ATTRIBUTED_UVL))
|
|
55
|
+
result = fm.attribute_optimization({"Cost": "Maximize"}, backend="sat")
|
|
56
|
+
assert [_sel(c) for c in result] == [{"A", "B", "C", "Root"}]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def test_attribute_optimization_sat_matches_z3():
|
|
60
|
+
path = _model(_ATTRIBUTED_UVL)
|
|
61
|
+
sat = FLAMAFeatureModel(path).attribute_optimization({"Cost": "Minimize"}, backend="sat")
|
|
62
|
+
z3 = FLAMAFeatureModel(path).attribute_optimization({"Cost": "Minimize"}, backend="z3")
|
|
63
|
+
assert {frozenset(_sel(c)) for c in sat} == {frozenset(_sel(c)) for c in z3}
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# --- minimum configuration --------------------------------------------------
|
|
67
|
+
|
|
68
|
+
def test_minimum_configuration():
|
|
69
|
+
# The attributed model has an "A | B" constraint, so the minimum working
|
|
70
|
+
# configuration is Root plus exactly one of A/B.
|
|
71
|
+
fm = FLAMAFeatureModel(_model(_ATTRIBUTED_UVL))
|
|
72
|
+
selected = _sel(fm.minimum_configuration(backend="sat"))
|
|
73
|
+
assert "Root" in selected
|
|
74
|
+
assert len(selected) == 2 and len(selected & {"A", "B"}) == 1
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# --- t-wise sampling --------------------------------------------------------
|
|
78
|
+
|
|
79
|
+
def test_t_wise_sampling_covers_valid_pairs():
|
|
80
|
+
fm = FLAMAFeatureModel(_model(_PLAIN_UVL))
|
|
81
|
+
sample = fm.t_wise_sampling(2, backend="sat")
|
|
82
|
+
selected_sets = [_sel(c) for c in sample]
|
|
83
|
+
# every satisfiable pair is covered; A&B is unsatisfiable (A => !B) so may be absent.
|
|
84
|
+
features = ["A", "B", "C", "D"]
|
|
85
|
+
for f1, f2 in itertools.combinations(features, 2):
|
|
86
|
+
for v1, v2 in itertools.product((True, False), repeat=2):
|
|
87
|
+
covered = any((f1 in s) == v1 and (f2 in s) == v2 for s in selected_sets)
|
|
88
|
+
if not covered:
|
|
89
|
+
assert (f1, f2, v1, v2) == ("A", "B", True, True)
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
# --- sharpsat backend -------------------------------------------------------
|
|
93
|
+
|
|
94
|
+
def test_sharpsat_backend_count_matches_bdd():
|
|
95
|
+
path = _model(_PLAIN_UVL)
|
|
96
|
+
exact = FLAMAFeatureModel(path).configurations_number(backend="bdd")
|
|
97
|
+
approx = FLAMAFeatureModel(path).configurations_number(backend="sharpsat")
|
|
98
|
+
assert approx == exact
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def test_sharpsat_backend_sampling_returns_valid_configurations():
|
|
102
|
+
path = _model(_PLAIN_UVL)
|
|
103
|
+
fm = FLAMAFeatureModel(path)
|
|
104
|
+
sample = fm.sampling(6, with_replacement=True, backend="sharpsat")
|
|
105
|
+
assert len(sample) == 6
|
|
106
|
+
# each sampled configuration is a valid full configuration
|
|
107
|
+
for configuration in sample:
|
|
108
|
+
elements = {f.name: (f.name in _sel(configuration))
|
|
109
|
+
for f in FLAMAFeatureModel(path).fm_model.get_features()}
|
|
110
|
+
assert FLAMAFeatureModel(path).satisfiable_configuration(elements) is True
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|