Skip to content

Commit 0ed4e96

Browse files
José Ángel Galindo DuarteJosé Ángel Galindo Duarte
authored andcommitted
fix: count reliably via UniGen's ApproxMC and drop pyapproxmc
The standalone pyapproxmc binding mis-configures ApproxMC's projected counting on feature-model CNFs with implied variables (the root and mandatory-relation unit clauses), undercounting by a factor of two (e.g. 408 instead of 816). UniGen's ApproxMC invocation does not have that problem, so obtain the count through pyunigen (num=0, projected onto the feature variables) and drop the pyapproxmc dependency entirely — pyunigen bundles ApproxMC and covers both counting and sampling. Add a regression test for models with implied variables.
1 parent 7f1d298 commit 0ed4e96

4 files changed

Lines changed: 47 additions & 5 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ It complements the existing backends:
1919
pip install flamapy-sharpsat
2020
```
2121

22-
This pulls in `pyapproxmc` and `pyunigen` (and the flamapy core/FM/SAT plugins).
22+
This pulls in `pyunigen` (which bundles ApproxMC), and the flamapy core/FM/SAT plugins.
2323

2424
## Operations
2525

flamapy/metamodels/sharpsat_metamodel/operations/sharpsat_configurations_number.py

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from typing import cast
22

3-
import pyapproxmc
3+
import pyunigen
44

55
from flamapy.core.models import VariabilityModel
66
from flamapy.core.operations import ConfigurationsNumber
@@ -13,6 +13,12 @@ class SharpSATConfigurationsNumber(ConfigurationsNumber):
1313
Counting is projected onto the feature variables, so auxiliary variables (e.g. Tseytin
1414
gates) do not inflate the result. ApproxMC gives an (epsilon, delta) probabilistic
1515
approximation that scales far better than exact enumeration.
16+
17+
The count is obtained through UniGen's binding (``pyunigen`` with ``num=0``, which runs
18+
ApproxMC's projected count without drawing samples). The standalone ``pyapproxmc`` binding
19+
mis-configures ApproxMC's projected counting on feature-model CNFs with implied variables
20+
(the root and mandatory-relation unit clauses), undercounting by a factor of two; UniGen's
21+
ApproxMC invocation does not have that problem.
1622
"""
1723

1824
def __init__(self, seed: int = 1) -> None:
@@ -27,9 +33,11 @@ def get_result(self) -> int:
2733

2834
def execute(self, model: VariabilityModel) -> 'SharpSATConfigurationsNumber':
2935
sharpsat_model = cast(SharpSATModel, model)
30-
counter = pyapproxmc.Counter(seed=self._seed)
36+
counter = pyunigen.Sampler(seed=self._seed)
3137
for clause in sharpsat_model.clauses:
3238
counter.add_clause(clause)
33-
cell_count, hash_count = counter.count(sharpsat_model.feature_variables())
39+
cell_count, hash_count, _samples = counter.sample(
40+
num=0, sampling_set=sharpsat_model.feature_variables()
41+
)
3442
self._result = cell_count * (2 ** hash_count)
3543
return self

pyproject.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ dependencies = [
1414
"flamapy-fw~=2.6.0.dev4",
1515
"flamapy-fm~=2.6.0.dev4",
1616
"flamapy-sat~=2.6.0.dev4",
17-
"pyapproxmc~=4.3",
1817
"pyunigen~=2.5",
1918
]
2019

tests/test_sharpsat.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,31 @@ def _is_valid(fm: FeatureModel, selected: set) -> bool:
6060
return op.execute(sat_model).get_result()
6161

6262

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+
6388
def test_approximate_count_matches_exact_on_small_model():
6489
fm = _fm()
6590
exact = PySATConfigurationsNumber().execute(FmToPysat(fm).transform()).get_result()
@@ -68,6 +93,16 @@ def test_approximate_count_matches_exact_on_small_model():
6893
assert approx == exact
6994

7095

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+
71106
def test_sampling_returns_valid_configurations():
72107
fm = _fm()
73108
model = FmToSharpSAT(fm).transform()

0 commit comments

Comments
 (0)