flamapy-sharpsat 2.6.0.dev11__tar.gz → 2.9.0.dev0__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_sharpsat-2.6.0.dev11/flamapy_sharpsat.egg-info → flamapy_sharpsat-2.9.0.dev0}/PKG-INFO +4 -3
- flamapy_sharpsat-2.9.0.dev0/flamapy/metamodels/sharpsat_metamodel/__init__.py +26 -0
- flamapy_sharpsat-2.9.0.dev0/flamapy/metamodels/sharpsat_metamodel/operations/_satcheck.py +16 -0
- {flamapy_sharpsat-2.6.0.dev11 → flamapy_sharpsat-2.9.0.dev0}/flamapy/metamodels/sharpsat_metamodel/operations/sharpsat_configurations_number.py +10 -0
- {flamapy_sharpsat-2.6.0.dev11 → flamapy_sharpsat-2.9.0.dev0}/flamapy/metamodels/sharpsat_metamodel/operations/sharpsat_sampling.py +18 -4
- {flamapy_sharpsat-2.6.0.dev11 → flamapy_sharpsat-2.9.0.dev0}/flamapy/metamodels/sharpsat_metamodel/transformations/fm_to_sharpsat.py +2 -2
- {flamapy_sharpsat-2.6.0.dev11 → flamapy_sharpsat-2.9.0.dev0/flamapy_sharpsat.egg-info}/PKG-INFO +4 -3
- {flamapy_sharpsat-2.6.0.dev11 → flamapy_sharpsat-2.9.0.dev0}/flamapy_sharpsat.egg-info/SOURCES.txt +2 -0
- flamapy_sharpsat-2.9.0.dev0/flamapy_sharpsat.egg-info/entry_points.txt +2 -0
- {flamapy_sharpsat-2.6.0.dev11 → flamapy_sharpsat-2.9.0.dev0}/flamapy_sharpsat.egg-info/requires.txt +3 -2
- {flamapy_sharpsat-2.6.0.dev11 → flamapy_sharpsat-2.9.0.dev0}/pyproject.toml +7 -3
- flamapy_sharpsat-2.9.0.dev0/tests/test_sharpsat.py +242 -0
- flamapy_sharpsat-2.6.0.dev11/flamapy/metamodels/sharpsat_metamodel/__init__.py +0 -0
- flamapy_sharpsat-2.6.0.dev11/tests/test_sharpsat.py +0 -140
- {flamapy_sharpsat-2.6.0.dev11 → flamapy_sharpsat-2.9.0.dev0}/LICENSE +0 -0
- {flamapy_sharpsat-2.6.0.dev11 → flamapy_sharpsat-2.9.0.dev0}/README.md +0 -0
- {flamapy_sharpsat-2.6.0.dev11 → flamapy_sharpsat-2.9.0.dev0}/flamapy/metamodels/sharpsat_metamodel/models/__init__.py +0 -0
- {flamapy_sharpsat-2.6.0.dev11 → flamapy_sharpsat-2.9.0.dev0}/flamapy/metamodels/sharpsat_metamodel/models/sharpsat_model.py +0 -0
- {flamapy_sharpsat-2.6.0.dev11 → flamapy_sharpsat-2.9.0.dev0}/flamapy/metamodels/sharpsat_metamodel/operations/__init__.py +0 -0
- {flamapy_sharpsat-2.6.0.dev11 → flamapy_sharpsat-2.9.0.dev0}/flamapy/metamodels/sharpsat_metamodel/transformations/__init__.py +0 -0
- {flamapy_sharpsat-2.6.0.dev11 → flamapy_sharpsat-2.9.0.dev0}/flamapy_sharpsat.egg-info/dependency_links.txt +0 -0
- {flamapy_sharpsat-2.6.0.dev11 → flamapy_sharpsat-2.9.0.dev0}/flamapy_sharpsat.egg-info/top_level.txt +0 -0
- {flamapy_sharpsat-2.6.0.dev11 → flamapy_sharpsat-2.9.0.dev0}/setup.cfg +0 -0
- {flamapy_sharpsat-2.6.0.dev11 → flamapy_sharpsat-2.9.0.dev0}/setup.py +0 -0
{flamapy_sharpsat-2.6.0.dev11/flamapy_sharpsat.egg-info → flamapy_sharpsat-2.9.0.dev0}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flamapy-sharpsat
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.9.0.dev0
|
|
4
4
|
Summary: flamapy-sharpsat is a plugin to flamapy providing approximate model counting and almost-uniform sampling
|
|
5
5
|
Author-email: Flamapy <flamapy@us.es>
|
|
6
6
|
License-Expression: GPL-3.0-or-later
|
|
@@ -8,9 +8,10 @@ Project-URL: Homepage, https://github.com/flamapy/sharpsat_metamodel
|
|
|
8
8
|
Requires-Python: >=3.11
|
|
9
9
|
Description-Content-Type: text/markdown
|
|
10
10
|
License-File: LICENSE
|
|
11
|
-
Requires-Dist: flamapy-fw~=2.
|
|
12
|
-
Requires-Dist: flamapy-fm~=2.
|
|
11
|
+
Requires-Dist: flamapy-fw~=2.9.0.dev0
|
|
12
|
+
Requires-Dist: flamapy-fm~=2.9.0.dev0
|
|
13
13
|
Requires-Dist: pyunigen~=2.5
|
|
14
|
+
Requires-Dist: python-sat
|
|
14
15
|
Provides-Extra: dev
|
|
15
16
|
Requires-Dist: pytest; extra == "dev"
|
|
16
17
|
Requires-Dist: pytest-mock; extra == "dev"
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
from flamapy.core.manifest import OperationCapability, PluginManifest
|
|
2
|
+
from flamapy.core.models.language_level import LanguageLevel, MajorLevel, MinorLevel
|
|
3
|
+
|
|
4
|
+
# Approximate counting (ApproxMC) and almost-uniform sampling (UniGen): never chosen by the
|
|
5
|
+
# planner unless the caller allows approximate results. Since 2.9 both operations pre-check
|
|
6
|
+
# satisfiability (pyunigen segfaults on UNSAT input) and short-circuit void models.
|
|
7
|
+
# Costs are estimated milliseconds from the 2026-11-06 calibration (betty synthetic models at
|
|
8
|
+
# 1% and 30% constraint density plus JHipster and busybox; worst case, never decreasing):
|
|
9
|
+
# counting takes 0.1 s at 100 features and 1.6 s at 200; from 500 features on it depends on the
|
|
10
|
+
# model (1.5-8 s on the dense 1,000/2,000-feature models, but the sparse 500-feature one and
|
|
11
|
+
# busybox did not finish within 3 minutes), so the curve carries the cap and the ceiling is the
|
|
12
|
+
# largest size measured.
|
|
13
|
+
MANIFEST = PluginManifest(
|
|
14
|
+
name='sharpsat',
|
|
15
|
+
calibration='2026-11-06',
|
|
16
|
+
extension='sharpsat',
|
|
17
|
+
supported_level=LanguageLevel(MajorLevel.BOOLEAN, {MinorLevel.GROUP_CARDINALITY}),
|
|
18
|
+
max_features=2000,
|
|
19
|
+
transformation_cost_curve=((200, 1), (500, 2), (1000, 4), (2000, 10)),
|
|
20
|
+
operations={
|
|
21
|
+
'configurations_number': OperationCapability(
|
|
22
|
+
exact=False, cost_curve=((50, 30), (100, 100), (200, 1600), (500, 180000))),
|
|
23
|
+
'sampling': OperationCapability(
|
|
24
|
+
exact=False, cost_curve=((50, 100), (100, 420), (200, 3500), (500, 70000))),
|
|
25
|
+
},
|
|
26
|
+
)
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""Satisfiability pre-check guarding the pyunigen calls.
|
|
2
|
+
|
|
3
|
+
pyunigen (the UniGen/ApproxMC binding) segfaults the whole interpreter when handed an
|
|
4
|
+
unsatisfiable formula (found by the calibration runs during the 2.9 development, where it
|
|
5
|
+
silently killed worker pools), so every operation checks the CNF with a plain SAT call first and
|
|
6
|
+
short-circuits the trivial answer (count 0 / empty sample) instead of crashing.
|
|
7
|
+
"""
|
|
8
|
+
from pysat.solvers import Solver
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def satisfiable(clauses: list[list[int]]) -> bool:
|
|
12
|
+
solver = Solver(name='glucose3', bootstrap_with=clauses)
|
|
13
|
+
try:
|
|
14
|
+
return bool(solver.solve())
|
|
15
|
+
finally:
|
|
16
|
+
solver.delete()
|
|
@@ -6,6 +6,8 @@ from flamapy.core.models import VariabilityModel
|
|
|
6
6
|
from flamapy.core.operations import ConfigurationsNumber
|
|
7
7
|
from flamapy.metamodels.sharpsat_metamodel.models import SharpSATModel
|
|
8
8
|
|
|
9
|
+
from ._satcheck import satisfiable
|
|
10
|
+
|
|
9
11
|
|
|
10
12
|
class SharpSATConfigurationsNumber(ConfigurationsNumber):
|
|
11
13
|
"""Approximate the number of valid configurations with ApproxMC.
|
|
@@ -19,8 +21,13 @@ class SharpSATConfigurationsNumber(ConfigurationsNumber):
|
|
|
19
21
|
mis-configures ApproxMC's projected counting on feature-model CNFs with implied variables
|
|
20
22
|
(the root and mandatory-relation unit clauses), undercounting by a factor of two; UniGen's
|
|
21
23
|
ApproxMC invocation does not have that problem.
|
|
24
|
+
|
|
25
|
+
Unsatisfiable models short-circuit to 0 through a plain SAT pre-check: pyunigen
|
|
26
|
+
segfaults the interpreter on UNSAT input.
|
|
22
27
|
"""
|
|
23
28
|
|
|
29
|
+
exact = False # reported in the OperationResult provenance envelope
|
|
30
|
+
|
|
24
31
|
def __init__(self, seed: int = 1) -> None:
|
|
25
32
|
self._result = 0
|
|
26
33
|
self._seed = seed
|
|
@@ -33,6 +40,9 @@ class SharpSATConfigurationsNumber(ConfigurationsNumber):
|
|
|
33
40
|
|
|
34
41
|
def execute(self, model: VariabilityModel) -> 'SharpSATConfigurationsNumber':
|
|
35
42
|
sharpsat_model = cast(SharpSATModel, model)
|
|
43
|
+
if not satisfiable(sharpsat_model.clauses):
|
|
44
|
+
self._result = 0
|
|
45
|
+
return self
|
|
36
46
|
counter = pyunigen.Sampler(seed=self._seed)
|
|
37
47
|
for clause in sharpsat_model.clauses:
|
|
38
48
|
counter.add_clause(clause)
|
|
@@ -7,6 +7,8 @@ from flamapy.core.operations import Sampling
|
|
|
7
7
|
from flamapy.metamodels.configuration_metamodel.models.configuration import Configuration
|
|
8
8
|
from flamapy.metamodels.sharpsat_metamodel.models import SharpSATModel
|
|
9
9
|
|
|
10
|
+
from ._satcheck import satisfiable
|
|
11
|
+
|
|
10
12
|
|
|
11
13
|
class SharpSATSampling(Sampling):
|
|
12
14
|
"""Almost-uniform sampling of valid configurations with UniGen.
|
|
@@ -14,8 +16,14 @@ class SharpSATSampling(Sampling):
|
|
|
14
16
|
Unlike an enumerate-first-N sampler, UniGen draws configurations almost uniformly at
|
|
15
17
|
random. Sampling is projected onto the feature variables, and an optional partial
|
|
16
18
|
configuration fixes features as assumptions.
|
|
19
|
+
|
|
20
|
+
Unsatisfiable input (a void model, or a partial configuration that contradicts it)
|
|
21
|
+
short-circuits to an empty sample through a plain SAT pre-check: pyunigen segfaults
|
|
22
|
+
the interpreter on UNSAT input.
|
|
17
23
|
"""
|
|
18
24
|
|
|
25
|
+
exact = False # reported in the OperationResult provenance envelope
|
|
26
|
+
|
|
19
27
|
def __init__(self, seed: int = 1) -> None:
|
|
20
28
|
self._sample_size = 1
|
|
21
29
|
self._with_replacement = False
|
|
@@ -40,14 +48,20 @@ class SharpSATSampling(Sampling):
|
|
|
40
48
|
|
|
41
49
|
def execute(self, model: VariabilityModel) -> 'SharpSATSampling':
|
|
42
50
|
sharpsat_model = cast(SharpSATModel, model)
|
|
43
|
-
|
|
44
|
-
for clause in sharpsat_model.clauses:
|
|
45
|
-
sampler.add_clause(clause)
|
|
51
|
+
clauses = [list(clause) for clause in sharpsat_model.clauses]
|
|
46
52
|
if self._partial_configuration is not None:
|
|
47
53
|
for name, value in self._partial_configuration.elements.items():
|
|
48
54
|
variable = sharpsat_model.variables.get(name)
|
|
49
55
|
if variable is not None:
|
|
50
|
-
|
|
56
|
+
clauses.append([variable if value else -variable])
|
|
57
|
+
|
|
58
|
+
if not satisfiable(clauses):
|
|
59
|
+
self._result = []
|
|
60
|
+
return self
|
|
61
|
+
|
|
62
|
+
sampler = pyunigen.Sampler(seed=self._seed)
|
|
63
|
+
for clause in clauses:
|
|
64
|
+
sampler.add_clause(clause)
|
|
51
65
|
|
|
52
66
|
_cells, _hashes, samples = sampler.sample(
|
|
53
67
|
num=self._sample_size, sampling_set=sharpsat_model.feature_variables()
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
from flamapy.core.transformations import ModelToModel
|
|
2
|
-
from flamapy.metamodels.fm_metamodel.models import
|
|
2
|
+
from flamapy.metamodels.fm_metamodel.models import ClauseSet, DEFAULT_CNF_METHOD, FeatureModel
|
|
3
3
|
from flamapy.metamodels.sharpsat_metamodel.models import SharpSATModel
|
|
4
4
|
|
|
5
5
|
|
|
@@ -19,7 +19,7 @@ class FmToSharpSAT(ModelToModel):
|
|
|
19
19
|
def get_destination_extension() -> str:
|
|
20
20
|
return 'sharpsat'
|
|
21
21
|
|
|
22
|
-
def __init__(self, source_model: FeatureModel, cnf_method: str =
|
|
22
|
+
def __init__(self, source_model: FeatureModel, cnf_method: str = DEFAULT_CNF_METHOD) -> None:
|
|
23
23
|
self.source_model = source_model
|
|
24
24
|
self.cnf_method = cnf_method
|
|
25
25
|
self.destination_model = SharpSATModel()
|
{flamapy_sharpsat-2.6.0.dev11 → flamapy_sharpsat-2.9.0.dev0/flamapy_sharpsat.egg-info}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: flamapy-sharpsat
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.9.0.dev0
|
|
4
4
|
Summary: flamapy-sharpsat is a plugin to flamapy providing approximate model counting and almost-uniform sampling
|
|
5
5
|
Author-email: Flamapy <flamapy@us.es>
|
|
6
6
|
License-Expression: GPL-3.0-or-later
|
|
@@ -8,9 +8,10 @@ Project-URL: Homepage, https://github.com/flamapy/sharpsat_metamodel
|
|
|
8
8
|
Requires-Python: >=3.11
|
|
9
9
|
Description-Content-Type: text/markdown
|
|
10
10
|
License-File: LICENSE
|
|
11
|
-
Requires-Dist: flamapy-fw~=2.
|
|
12
|
-
Requires-Dist: flamapy-fm~=2.
|
|
11
|
+
Requires-Dist: flamapy-fw~=2.9.0.dev0
|
|
12
|
+
Requires-Dist: flamapy-fm~=2.9.0.dev0
|
|
13
13
|
Requires-Dist: pyunigen~=2.5
|
|
14
|
+
Requires-Dist: python-sat
|
|
14
15
|
Provides-Extra: dev
|
|
15
16
|
Requires-Dist: pytest; extra == "dev"
|
|
16
17
|
Requires-Dist: pytest-mock; extra == "dev"
|
{flamapy_sharpsat-2.6.0.dev11 → flamapy_sharpsat-2.9.0.dev0}/flamapy_sharpsat.egg-info/SOURCES.txt
RENAMED
|
@@ -6,6 +6,7 @@ flamapy/metamodels/sharpsat_metamodel/__init__.py
|
|
|
6
6
|
flamapy/metamodels/sharpsat_metamodel/models/__init__.py
|
|
7
7
|
flamapy/metamodels/sharpsat_metamodel/models/sharpsat_model.py
|
|
8
8
|
flamapy/metamodels/sharpsat_metamodel/operations/__init__.py
|
|
9
|
+
flamapy/metamodels/sharpsat_metamodel/operations/_satcheck.py
|
|
9
10
|
flamapy/metamodels/sharpsat_metamodel/operations/sharpsat_configurations_number.py
|
|
10
11
|
flamapy/metamodels/sharpsat_metamodel/operations/sharpsat_sampling.py
|
|
11
12
|
flamapy/metamodels/sharpsat_metamodel/transformations/__init__.py
|
|
@@ -13,6 +14,7 @@ flamapy/metamodels/sharpsat_metamodel/transformations/fm_to_sharpsat.py
|
|
|
13
14
|
flamapy_sharpsat.egg-info/PKG-INFO
|
|
14
15
|
flamapy_sharpsat.egg-info/SOURCES.txt
|
|
15
16
|
flamapy_sharpsat.egg-info/dependency_links.txt
|
|
17
|
+
flamapy_sharpsat.egg-info/entry_points.txt
|
|
16
18
|
flamapy_sharpsat.egg-info/requires.txt
|
|
17
19
|
flamapy_sharpsat.egg-info/top_level.txt
|
|
18
20
|
tests/test_sharpsat.py
|
|
@@ -4,16 +4,17 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "flamapy-sharpsat"
|
|
7
|
-
version = "2.
|
|
7
|
+
version = "2.9.0.dev0"
|
|
8
8
|
description = "flamapy-sharpsat is a plugin to flamapy providing approximate model counting and almost-uniform sampling"
|
|
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.
|
|
15
|
-
"flamapy-fm~=2.
|
|
14
|
+
"flamapy-fw~=2.9.0.dev0",
|
|
15
|
+
"flamapy-fm~=2.9.0.dev0",
|
|
16
16
|
"pyunigen~=2.5",
|
|
17
|
+
"python-sat",
|
|
17
18
|
]
|
|
18
19
|
|
|
19
20
|
[project.optional-dependencies]
|
|
@@ -25,6 +26,9 @@ dev = [
|
|
|
25
26
|
"coverage",
|
|
26
27
|
]
|
|
27
28
|
|
|
29
|
+
[project.entry-points."flamapy.plugins"]
|
|
30
|
+
sharpsat = "flamapy.metamodels.sharpsat_metamodel"
|
|
31
|
+
|
|
28
32
|
[project.urls]
|
|
29
33
|
Homepage = "https://github.com/flamapy/sharpsat_metamodel"
|
|
30
34
|
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
"""Tests for the sharpsat metamodel: approximate counting and almost-uniform sampling."""
|
|
2
|
+
import os
|
|
3
|
+
import tempfile
|
|
4
|
+
from itertools import product
|
|
5
|
+
|
|
6
|
+
import pytest
|
|
7
|
+
|
|
8
|
+
from flamapy.metamodels.fm_metamodel.transformations import UVLReader
|
|
9
|
+
from flamapy.metamodels.fm_metamodel.models import FeatureModel, ClauseSet
|
|
10
|
+
from flamapy.metamodels.sharpsat_metamodel.transformations import FmToSharpSAT
|
|
11
|
+
from flamapy.metamodels.sharpsat_metamodel.operations import (
|
|
12
|
+
SharpSATConfigurationsNumber,
|
|
13
|
+
SharpSATSampling,
|
|
14
|
+
)
|
|
15
|
+
from flamapy.metamodels.configuration_metamodel.models.configuration import Configuration
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# Exact oracle: enumerate the valid configurations by brute-forcing the CNF produced by
|
|
19
|
+
# fm_metamodel's solver-agnostic ClauseSet — no SAT backend, so the tests only depend on
|
|
20
|
+
# flamapy-fm (already required) and match sharpsat's own encoding path.
|
|
21
|
+
def _valid_configs(fm: FeatureModel) -> set:
|
|
22
|
+
"""Valid configurations as frozensets of selected feature names."""
|
|
23
|
+
clause_set = ClauseSet.from_feature_model(fm)
|
|
24
|
+
name_of = {vid: name for name, vid in clause_set.variables.items()}
|
|
25
|
+
var_ids = sorted({abs(lit) for clause in clause_set.clauses for lit in clause}
|
|
26
|
+
| set(clause_set.variables.values()))
|
|
27
|
+
configs = set()
|
|
28
|
+
for bits in product((False, True), repeat=len(var_ids)):
|
|
29
|
+
assignment = dict(zip(var_ids, bits))
|
|
30
|
+
if all(any((lit > 0) == assignment[abs(lit)] for lit in clause)
|
|
31
|
+
for clause in clause_set.clauses):
|
|
32
|
+
configs.add(frozenset(name_of[v] for v in var_ids if assignment[v] and v in name_of))
|
|
33
|
+
return configs
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _exact_count(fm: FeatureModel) -> int:
|
|
37
|
+
return len(_valid_configs(fm))
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
_UVL = """features
|
|
41
|
+
Root {abstract}
|
|
42
|
+
optional
|
|
43
|
+
A
|
|
44
|
+
B
|
|
45
|
+
C
|
|
46
|
+
D
|
|
47
|
+
constraints
|
|
48
|
+
(A | B) => (C <=> D)
|
|
49
|
+
A => !B
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _fm() -> FeatureModel:
|
|
54
|
+
handle, path = tempfile.mkstemp(suffix='.uvl')
|
|
55
|
+
try:
|
|
56
|
+
with os.fdopen(handle, 'w') as file:
|
|
57
|
+
file.write(_UVL)
|
|
58
|
+
return UVLReader(path).transform()
|
|
59
|
+
finally:
|
|
60
|
+
os.remove(path)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _all_feature_names(fm: FeatureModel) -> list[str]:
|
|
64
|
+
return [feature.name for feature in fm.get_features()]
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def _is_valid(fm: FeatureModel, selected: set) -> bool:
|
|
68
|
+
return frozenset(selected) in _valid_configs(fm)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
_IMPLIED_VARS_UVL = """features
|
|
72
|
+
Root {abstract}
|
|
73
|
+
mandatory
|
|
74
|
+
Base
|
|
75
|
+
mandatory
|
|
76
|
+
Core
|
|
77
|
+
optional
|
|
78
|
+
A
|
|
79
|
+
B
|
|
80
|
+
C
|
|
81
|
+
constraints
|
|
82
|
+
A => B
|
|
83
|
+
"""
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _fm_from(uvl):
|
|
87
|
+
handle, path = tempfile.mkstemp(suffix='.uvl')
|
|
88
|
+
try:
|
|
89
|
+
with os.fdopen(handle, 'w') as file:
|
|
90
|
+
file.write(uvl)
|
|
91
|
+
return UVLReader(path).transform()
|
|
92
|
+
finally:
|
|
93
|
+
os.remove(path)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def test_approximate_count_matches_exact_on_small_model():
|
|
97
|
+
fm = _fm()
|
|
98
|
+
exact = _exact_count(fm)
|
|
99
|
+
approx = SharpSATConfigurationsNumber().execute(FmToSharpSAT(fm).transform()).get_result()
|
|
100
|
+
# ApproxMC's default (epsilon, delta) is exact on a model this small.
|
|
101
|
+
assert approx == exact
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def test_count_is_correct_with_implied_variables():
|
|
105
|
+
# Regression guard: models with implied variables (mandatory chains, root unit clause)
|
|
106
|
+
# must not be undercounted. The standalone pyapproxmc binding halved these; the UniGen
|
|
107
|
+
# ApproxMC invocation used by the counter does not.
|
|
108
|
+
fm = _fm_from(_IMPLIED_VARS_UVL)
|
|
109
|
+
exact = _exact_count(fm)
|
|
110
|
+
approx = SharpSATConfigurationsNumber().execute(FmToSharpSAT(fm).transform()).get_result()
|
|
111
|
+
assert approx == exact
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_sampling_returns_valid_configurations():
|
|
115
|
+
fm = _fm()
|
|
116
|
+
model = FmToSharpSAT(fm).transform()
|
|
117
|
+
op = SharpSATSampling()
|
|
118
|
+
op.set_sample_size(6)
|
|
119
|
+
op.set_with_replacement(True)
|
|
120
|
+
sample = op.execute(model).get_sample()
|
|
121
|
+
assert len(sample) == 6
|
|
122
|
+
for configuration in sample:
|
|
123
|
+
selected = {name for name, value in configuration.elements.items() if value}
|
|
124
|
+
assert _is_valid(fm, selected), f"invalid configuration sampled: {selected}"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_sampling_respects_partial_configuration():
|
|
128
|
+
fm = _fm()
|
|
129
|
+
model = FmToSharpSAT(fm).transform()
|
|
130
|
+
op = SharpSATSampling()
|
|
131
|
+
op.set_sample_size(5)
|
|
132
|
+
op.set_with_replacement(True)
|
|
133
|
+
op.set_partial_configuration(Configuration({'A': True}))
|
|
134
|
+
for configuration in op.execute(model).get_sample():
|
|
135
|
+
selected = {name for name, value in configuration.elements.items() if value}
|
|
136
|
+
assert 'A' in selected # the fixed feature is always present
|
|
137
|
+
assert 'B' not in selected # A => !B forces B out
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def test_without_replacement_deduplicates():
|
|
141
|
+
fm = _fm()
|
|
142
|
+
model = FmToSharpSAT(fm).transform()
|
|
143
|
+
op = SharpSATSampling()
|
|
144
|
+
op.set_sample_size(20)
|
|
145
|
+
op.set_with_replacement(False)
|
|
146
|
+
sample = op.execute(model).get_sample()
|
|
147
|
+
keys = [frozenset(n for n, v in c.elements.items() if v) for c in sample]
|
|
148
|
+
assert len(keys) == len(set(keys)) # no duplicates
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
# ---------------------------------------------------------------------------
|
|
152
|
+
# UNSAT guard: pyunigen segfaults the interpreter on unsatisfiable input, so both
|
|
153
|
+
# operations must short-circuit without ever reaching it.
|
|
154
|
+
# ---------------------------------------------------------------------------
|
|
155
|
+
|
|
156
|
+
_VOID_UVL = """features
|
|
157
|
+
Root {abstract}
|
|
158
|
+
mandatory
|
|
159
|
+
A
|
|
160
|
+
B
|
|
161
|
+
constraints
|
|
162
|
+
A => !B
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def test_void_model_counts_zero_without_crashing():
|
|
167
|
+
model = FmToSharpSAT(_fm_from(_VOID_UVL)).transform()
|
|
168
|
+
operation = SharpSATConfigurationsNumber()
|
|
169
|
+
assert operation.execute(model).get_result() == 0
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def test_void_model_samples_empty_without_crashing():
|
|
173
|
+
model = FmToSharpSAT(_fm_from(_VOID_UVL)).transform()
|
|
174
|
+
operation = SharpSATSampling()
|
|
175
|
+
operation.set_sample_size(5)
|
|
176
|
+
assert operation.execute(model).get_result() == []
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def test_unsatisfiable_partial_configuration_samples_empty():
|
|
180
|
+
model = FmToSharpSAT(_fm()).transform()
|
|
181
|
+
operation = SharpSATSampling()
|
|
182
|
+
operation.set_sample_size(3)
|
|
183
|
+
# A and B are mutually exclusive in _UVL (A => !B).
|
|
184
|
+
operation.set_partial_configuration(Configuration({'A': True, 'B': True}))
|
|
185
|
+
assert operation.execute(model).get_result() == []
|
|
186
|
+
|
|
187
|
+
|
|
188
|
+
def test_satisfiable_behavior_unchanged_by_the_guard():
|
|
189
|
+
fm = _fm()
|
|
190
|
+
model = FmToSharpSAT(fm).transform()
|
|
191
|
+
operation = SharpSATConfigurationsNumber()
|
|
192
|
+
assert operation.execute(model).get_result() == _exact_count(fm)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
_TSEYTIN_UVL = """features
|
|
196
|
+
Root {abstract}
|
|
197
|
+
optional
|
|
198
|
+
A
|
|
199
|
+
B
|
|
200
|
+
C
|
|
201
|
+
D
|
|
202
|
+
E
|
|
203
|
+
F
|
|
204
|
+
constraints
|
|
205
|
+
(A & B & C) | (D & E & F) | (B & E & F)
|
|
206
|
+
"""
|
|
207
|
+
|
|
208
|
+
# [1..2] over A, B, C (3 + 3 selections) times D optional: 12 configurations.
|
|
209
|
+
_GROUP_UVL = """features
|
|
210
|
+
Root {abstract}
|
|
211
|
+
[1..2]
|
|
212
|
+
A
|
|
213
|
+
B
|
|
214
|
+
C
|
|
215
|
+
optional
|
|
216
|
+
D
|
|
217
|
+
"""
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def test_cnf_methods_agree():
|
|
221
|
+
"""Tseytin auxiliary variables must not change the count or let samples leak them."""
|
|
222
|
+
fm = _fm_from(_TSEYTIN_UVL)
|
|
223
|
+
assert ClauseSet.from_feature_model(fm, cnf_method='auto').auxiliary_variables
|
|
224
|
+
exact = _exact_count(fm)
|
|
225
|
+
for method in ('distributive', 'tseytin', 'auto'):
|
|
226
|
+
model = FmToSharpSAT(fm, cnf_method=method).transform()
|
|
227
|
+
assert SharpSATConfigurationsNumber().execute(model).get_result() == exact, method
|
|
228
|
+
sampler = SharpSATSampling()
|
|
229
|
+
sampler.set_sample_size(5)
|
|
230
|
+
for config in sampler.execute(model).get_sample():
|
|
231
|
+
assert _is_valid(fm, set(config.get_selected_elements())), method
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def test_group_cardinality():
|
|
235
|
+
fm = _fm_from(_GROUP_UVL)
|
|
236
|
+
model = FmToSharpSAT(fm).transform()
|
|
237
|
+
assert SharpSATConfigurationsNumber().execute(model).get_result() == 12
|
|
238
|
+
sampler = SharpSATSampling()
|
|
239
|
+
sampler.set_sample_size(5)
|
|
240
|
+
for config in sampler.execute(model).get_sample():
|
|
241
|
+
selected = set(config.get_selected_elements())
|
|
242
|
+
assert 1 <= len(selected & {'A', 'B', 'C'}) <= 2
|
|
File without changes
|
|
@@ -1,140 +0,0 @@
|
|
|
1
|
-
"""Tests for the sharpsat metamodel: approximate counting and almost-uniform sampling."""
|
|
2
|
-
import os
|
|
3
|
-
import tempfile
|
|
4
|
-
|
|
5
|
-
import pytest
|
|
6
|
-
|
|
7
|
-
from flamapy.metamodels.fm_metamodel.transformations import UVLReader
|
|
8
|
-
from flamapy.metamodels.fm_metamodel.models import FeatureModel
|
|
9
|
-
from flamapy.metamodels.sharpsat_metamodel.transformations import FmToSharpSAT
|
|
10
|
-
from flamapy.metamodels.sharpsat_metamodel.operations import (
|
|
11
|
-
SharpSATConfigurationsNumber,
|
|
12
|
-
SharpSATSampling,
|
|
13
|
-
)
|
|
14
|
-
|
|
15
|
-
# Exact oracle and validity oracle via SAT (a declared dependency, unlike BDD).
|
|
16
|
-
from flamapy.metamodels.pysat_metamodel.transformations import FmToPysat
|
|
17
|
-
from flamapy.metamodels.pysat_metamodel.operations.pysat_configurations_number import (
|
|
18
|
-
PySATConfigurationsNumber,
|
|
19
|
-
)
|
|
20
|
-
from flamapy.metamodels.pysat_metamodel.operations.pysat_satisfiable_configuration import (
|
|
21
|
-
PySATSatisfiableConfiguration,
|
|
22
|
-
)
|
|
23
|
-
from flamapy.metamodels.configuration_metamodel.models.configuration import Configuration
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
_UVL = """features
|
|
27
|
-
Root {abstract}
|
|
28
|
-
optional
|
|
29
|
-
A
|
|
30
|
-
B
|
|
31
|
-
C
|
|
32
|
-
D
|
|
33
|
-
constraints
|
|
34
|
-
(A | B) => (C <=> D)
|
|
35
|
-
A => !B
|
|
36
|
-
"""
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
def _fm() -> FeatureModel:
|
|
40
|
-
handle, path = tempfile.mkstemp(suffix='.uvl')
|
|
41
|
-
try:
|
|
42
|
-
with os.fdopen(handle, 'w') as file:
|
|
43
|
-
file.write(_UVL)
|
|
44
|
-
return UVLReader(path).transform()
|
|
45
|
-
finally:
|
|
46
|
-
os.remove(path)
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
def _all_feature_names(fm: FeatureModel) -> list[str]:
|
|
50
|
-
return [feature.name for feature in fm.get_features()]
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
def _is_valid(fm: FeatureModel, selected: set) -> bool:
|
|
54
|
-
sat_model = FmToPysat(fm).transform()
|
|
55
|
-
elements = {name: (name in selected) for name in _all_feature_names(fm)}
|
|
56
|
-
configuration = Configuration(elements)
|
|
57
|
-
configuration.set_full(True)
|
|
58
|
-
op = PySATSatisfiableConfiguration()
|
|
59
|
-
op.set_configuration(configuration)
|
|
60
|
-
return op.execute(sat_model).get_result()
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
_IMPLIED_VARS_UVL = """features
|
|
64
|
-
Root {abstract}
|
|
65
|
-
mandatory
|
|
66
|
-
Base
|
|
67
|
-
mandatory
|
|
68
|
-
Core
|
|
69
|
-
optional
|
|
70
|
-
A
|
|
71
|
-
B
|
|
72
|
-
C
|
|
73
|
-
constraints
|
|
74
|
-
A => B
|
|
75
|
-
"""
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
def _fm_from(uvl):
|
|
79
|
-
handle, path = tempfile.mkstemp(suffix='.uvl')
|
|
80
|
-
try:
|
|
81
|
-
with os.fdopen(handle, 'w') as file:
|
|
82
|
-
file.write(uvl)
|
|
83
|
-
return UVLReader(path).transform()
|
|
84
|
-
finally:
|
|
85
|
-
os.remove(path)
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
def test_approximate_count_matches_exact_on_small_model():
|
|
89
|
-
fm = _fm()
|
|
90
|
-
exact = PySATConfigurationsNumber().execute(FmToPysat(fm).transform()).get_result()
|
|
91
|
-
approx = SharpSATConfigurationsNumber().execute(FmToSharpSAT(fm).transform()).get_result()
|
|
92
|
-
# ApproxMC's default (epsilon, delta) is exact on a model this small.
|
|
93
|
-
assert approx == exact
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
def test_count_is_correct_with_implied_variables():
|
|
97
|
-
# Regression guard: models with implied variables (mandatory chains, root unit clause)
|
|
98
|
-
# must not be undercounted. The standalone pyapproxmc binding halved these; the UniGen
|
|
99
|
-
# ApproxMC invocation used by the counter does not.
|
|
100
|
-
fm = _fm_from(_IMPLIED_VARS_UVL)
|
|
101
|
-
exact = PySATConfigurationsNumber().execute(FmToPysat(fm).transform()).get_result()
|
|
102
|
-
approx = SharpSATConfigurationsNumber().execute(FmToSharpSAT(fm).transform()).get_result()
|
|
103
|
-
assert approx == exact
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
def test_sampling_returns_valid_configurations():
|
|
107
|
-
fm = _fm()
|
|
108
|
-
model = FmToSharpSAT(fm).transform()
|
|
109
|
-
op = SharpSATSampling()
|
|
110
|
-
op.set_sample_size(6)
|
|
111
|
-
op.set_with_replacement(True)
|
|
112
|
-
sample = op.execute(model).get_sample()
|
|
113
|
-
assert len(sample) == 6
|
|
114
|
-
for configuration in sample:
|
|
115
|
-
selected = {name for name, value in configuration.elements.items() if value}
|
|
116
|
-
assert _is_valid(fm, selected), f"invalid configuration sampled: {selected}"
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
def test_sampling_respects_partial_configuration():
|
|
120
|
-
fm = _fm()
|
|
121
|
-
model = FmToSharpSAT(fm).transform()
|
|
122
|
-
op = SharpSATSampling()
|
|
123
|
-
op.set_sample_size(5)
|
|
124
|
-
op.set_with_replacement(True)
|
|
125
|
-
op.set_partial_configuration(Configuration({'A': True}))
|
|
126
|
-
for configuration in op.execute(model).get_sample():
|
|
127
|
-
selected = {name for name, value in configuration.elements.items() if value}
|
|
128
|
-
assert 'A' in selected # the fixed feature is always present
|
|
129
|
-
assert 'B' not in selected # A => !B forces B out
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
def test_without_replacement_deduplicates():
|
|
133
|
-
fm = _fm()
|
|
134
|
-
model = FmToSharpSAT(fm).transform()
|
|
135
|
-
op = SharpSATSampling()
|
|
136
|
-
op.set_sample_size(20)
|
|
137
|
-
op.set_with_replacement(False)
|
|
138
|
-
sample = op.execute(model).get_sample()
|
|
139
|
-
keys = [frozenset(n for n, v in c.elements.items() if v) for c in sample]
|
|
140
|
-
assert len(keys) == len(set(keys)) # no duplicates
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{flamapy_sharpsat-2.6.0.dev11 → flamapy_sharpsat-2.9.0.dev0}/flamapy_sharpsat.egg-info/top_level.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|