flamapy-sdd 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.
Files changed (25) hide show
  1. {flamapy_sdd-2.6.0.dev11 → flamapy_sdd-2.9.0.dev0}/PKG-INFO +3 -3
  2. flamapy_sdd-2.9.0.dev0/flamapy/metamodels/sdd_metamodel/__init__.py +24 -0
  3. {flamapy_sdd-2.6.0.dev11 → flamapy_sdd-2.9.0.dev0}/flamapy/metamodels/sdd_metamodel/operations/sdd_core_features.py +6 -2
  4. {flamapy_sdd-2.6.0.dev11 → flamapy_sdd-2.9.0.dev0}/flamapy/metamodels/sdd_metamodel/transformations/fm_to_sdd.py +2 -2
  5. {flamapy_sdd-2.6.0.dev11 → flamapy_sdd-2.9.0.dev0}/flamapy_sdd.egg-info/PKG-INFO +3 -3
  6. {flamapy_sdd-2.6.0.dev11 → flamapy_sdd-2.9.0.dev0}/flamapy_sdd.egg-info/SOURCES.txt +1 -0
  7. flamapy_sdd-2.9.0.dev0/flamapy_sdd.egg-info/entry_points.txt +2 -0
  8. {flamapy_sdd-2.6.0.dev11 → flamapy_sdd-2.9.0.dev0}/flamapy_sdd.egg-info/requires.txt +2 -2
  9. {flamapy_sdd-2.6.0.dev11 → flamapy_sdd-2.9.0.dev0}/pyproject.toml +6 -3
  10. flamapy_sdd-2.9.0.dev0/tests/test_sdd.py +166 -0
  11. flamapy_sdd-2.6.0.dev11/flamapy/metamodels/sdd_metamodel/__init__.py +0 -0
  12. flamapy_sdd-2.6.0.dev11/tests/test_sdd.py +0 -82
  13. {flamapy_sdd-2.6.0.dev11 → flamapy_sdd-2.9.0.dev0}/LICENSE +0 -0
  14. {flamapy_sdd-2.6.0.dev11 → flamapy_sdd-2.9.0.dev0}/README.md +0 -0
  15. {flamapy_sdd-2.6.0.dev11 → flamapy_sdd-2.9.0.dev0}/flamapy/metamodels/sdd_metamodel/models/__init__.py +0 -0
  16. {flamapy_sdd-2.6.0.dev11 → flamapy_sdd-2.9.0.dev0}/flamapy/metamodels/sdd_metamodel/models/sdd_model.py +0 -0
  17. {flamapy_sdd-2.6.0.dev11 → flamapy_sdd-2.9.0.dev0}/flamapy/metamodels/sdd_metamodel/operations/__init__.py +0 -0
  18. {flamapy_sdd-2.6.0.dev11 → flamapy_sdd-2.9.0.dev0}/flamapy/metamodels/sdd_metamodel/operations/sdd_configurations_number.py +0 -0
  19. {flamapy_sdd-2.6.0.dev11 → flamapy_sdd-2.9.0.dev0}/flamapy/metamodels/sdd_metamodel/operations/sdd_dead_features.py +0 -0
  20. {flamapy_sdd-2.6.0.dev11 → flamapy_sdd-2.9.0.dev0}/flamapy/metamodels/sdd_metamodel/operations/sdd_satisfiable.py +0 -0
  21. {flamapy_sdd-2.6.0.dev11 → flamapy_sdd-2.9.0.dev0}/flamapy/metamodels/sdd_metamodel/transformations/__init__.py +0 -0
  22. {flamapy_sdd-2.6.0.dev11 → flamapy_sdd-2.9.0.dev0}/flamapy_sdd.egg-info/dependency_links.txt +0 -0
  23. {flamapy_sdd-2.6.0.dev11 → flamapy_sdd-2.9.0.dev0}/flamapy_sdd.egg-info/top_level.txt +0 -0
  24. {flamapy_sdd-2.6.0.dev11 → flamapy_sdd-2.9.0.dev0}/setup.cfg +0 -0
  25. {flamapy_sdd-2.6.0.dev11 → flamapy_sdd-2.9.0.dev0}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flamapy-sdd
3
- Version: 2.6.0.dev11
3
+ Version: 2.9.0.dev0
4
4
  Summary: flamapy-sdd is a plugin to flamapy providing Sentential Decision Diagram (SDD) knowledge compilation for exact analysis of feature models
5
5
  Author-email: Flamapy <flamapy@us.es>
6
6
  License-Expression: GPL-3.0-or-later
@@ -8,8 +8,8 @@ Project-URL: Homepage, https://github.com/flamapy/sdd_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.6.0.dev11
12
- Requires-Dist: flamapy-fm~=2.6.0.dev11
11
+ Requires-Dist: flamapy-fw~=2.9.0.dev0
12
+ Requires-Dist: flamapy-fm~=2.9.0.dev0
13
13
  Requires-Dist: PySDD~=1.0
14
14
  Provides-Extra: dev
15
15
  Requires-Dist: pytest; extra == "dev"
@@ -0,0 +1,24 @@
1
+ from flamapy.core.manifest import OperationCapability, PluginManifest
2
+ from flamapy.core.models.language_level import LanguageLevel, MajorLevel, MinorLevel
3
+
4
+ # Costs are estimated milliseconds from the 2026-11-06 calibration (betty synthetic models at 1%
5
+ # and 30% constraint density plus JHipster and busybox; worst case of the two densities, never
6
+ # decreasing). Compilation depends on the constraint density far more than on the size: the
7
+ # sparse models compile in 0.3 s up to 2,000 features, the dense 500-feature one takes 7.5 s and
8
+ # the dense 1,000-feature one (like busybox, 1,108 features) does not finish in 5 minutes.
9
+ # Counting is then immediate (66 ms at 500 features), but core/dead features condition the SDD
10
+ # per variable: 41 s for core and more than 3 minutes for dead on the dense 500-feature model.
11
+ MANIFEST = PluginManifest(
12
+ name='sdd',
13
+ calibration='2026-11-06',
14
+ extension='sdd',
15
+ supported_level=LanguageLevel(MajorLevel.BOOLEAN, {MinorLevel.GROUP_CARDINALITY}),
16
+ max_features=2000,
17
+ transformation_cost_curve=((50, 3), (100, 5), (200, 10), (500, 7500), (1000, 300000)),
18
+ operations={
19
+ 'satisfiable': OperationCapability(cost=1),
20
+ 'configurations_number': OperationCapability(cost_curve=((200, 1), (500, 66))),
21
+ 'core_features': OperationCapability(cost_curve=((100, 1), (200, 5), (500, 41000))),
22
+ 'dead_features': OperationCapability(cost_curve=((100, 1), (200, 4), (500, 180000))),
23
+ },
24
+ )
@@ -9,7 +9,8 @@ class SDDCoreFeatures(CoreFeatures):
9
9
  """Core features: present in every valid configuration.
10
10
 
11
11
  A feature is core iff no model selects it as false, i.e. conjoining its negative literal
12
- with the compiled SDD yields the false node.
12
+ with the compiled SDD yields the false node. A void model has no core features (the
13
+ convention of the SAT and BDD plugins), not vacuously all of them.
13
14
  """
14
15
 
15
16
  def __init__(self) -> None:
@@ -24,7 +25,10 @@ class SDDCoreFeatures(CoreFeatures):
24
25
  def execute(self, model: VariabilityModel) -> 'SDDCoreFeatures':
25
26
  sdd_model = cast(SDDModel, model)
26
27
  manager = sdd_model.manager
27
- core = []
28
+ core: list[Any] = []
29
+ if sdd_model.root.is_false(): # void model
30
+ self._result = core
31
+ return self
28
32
  for variable, name in sdd_model.features.items():
29
33
  if (sdd_model.root & manager.literal(-variable)).is_false():
30
34
  core.append(name)
@@ -1,7 +1,7 @@
1
1
  from pysdd.sdd import SddManager, Vtree
2
2
 
3
3
  from flamapy.core.transformations import ModelToModel
4
- from flamapy.metamodels.fm_metamodel.models import FeatureModel, ClauseSet
4
+ from flamapy.metamodels.fm_metamodel.models import ClauseSet, DEFAULT_CNF_METHOD, FeatureModel
5
5
  from flamapy.metamodels.sdd_metamodel.models import SDDModel
6
6
 
7
7
 
@@ -20,7 +20,7 @@ class FmToSDD(ModelToModel):
20
20
  def get_destination_extension() -> str:
21
21
  return 'sdd'
22
22
 
23
- def __init__(self, source_model: FeatureModel, cnf_method: str = 'distributive') -> None:
23
+ def __init__(self, source_model: FeatureModel, cnf_method: str = DEFAULT_CNF_METHOD) -> None:
24
24
  self.source_model = source_model
25
25
  self.cnf_method = cnf_method
26
26
  self.destination_model = SDDModel()
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flamapy-sdd
3
- Version: 2.6.0.dev11
3
+ Version: 2.9.0.dev0
4
4
  Summary: flamapy-sdd is a plugin to flamapy providing Sentential Decision Diagram (SDD) knowledge compilation for exact analysis of feature models
5
5
  Author-email: Flamapy <flamapy@us.es>
6
6
  License-Expression: GPL-3.0-or-later
@@ -8,8 +8,8 @@ Project-URL: Homepage, https://github.com/flamapy/sdd_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.6.0.dev11
12
- Requires-Dist: flamapy-fm~=2.6.0.dev11
11
+ Requires-Dist: flamapy-fw~=2.9.0.dev0
12
+ Requires-Dist: flamapy-fm~=2.9.0.dev0
13
13
  Requires-Dist: PySDD~=1.0
14
14
  Provides-Extra: dev
15
15
  Requires-Dist: pytest; extra == "dev"
@@ -15,6 +15,7 @@ flamapy/metamodels/sdd_metamodel/transformations/fm_to_sdd.py
15
15
  flamapy_sdd.egg-info/PKG-INFO
16
16
  flamapy_sdd.egg-info/SOURCES.txt
17
17
  flamapy_sdd.egg-info/dependency_links.txt
18
+ flamapy_sdd.egg-info/entry_points.txt
18
19
  flamapy_sdd.egg-info/requires.txt
19
20
  flamapy_sdd.egg-info/top_level.txt
20
21
  tests/test_sdd.py
@@ -0,0 +1,2 @@
1
+ [flamapy.plugins]
2
+ sdd = flamapy.metamodels.sdd_metamodel
@@ -1,5 +1,5 @@
1
- flamapy-fw~=2.6.0.dev11
2
- flamapy-fm~=2.6.0.dev11
1
+ flamapy-fw~=2.9.0.dev0
2
+ flamapy-fm~=2.9.0.dev0
3
3
  PySDD~=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-sdd"
7
- version = "2.6.0.dev11"
7
+ version = "2.9.0.dev0"
8
8
  description = "flamapy-sdd is a plugin to flamapy providing Sentential Decision Diagram (SDD) knowledge compilation for exact 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.dev11",
15
- "flamapy-fm~=2.6.0.dev11",
14
+ "flamapy-fw~=2.9.0.dev0",
15
+ "flamapy-fm~=2.9.0.dev0",
16
16
  "PySDD~=1.0",
17
17
  ]
18
18
 
@@ -25,6 +25,9 @@ dev = [
25
25
  "coverage",
26
26
  ]
27
27
 
28
+ [project.entry-points."flamapy.plugins"]
29
+ sdd = "flamapy.metamodels.sdd_metamodel"
30
+
28
31
  [project.urls]
29
32
  Homepage = "https://github.com/flamapy/sdd_metamodel"
30
33
 
@@ -0,0 +1,166 @@
1
+ """Tests for the SDD metamodel: exact counting, satisfiability, core/dead features.
2
+
3
+ Oracles are computed by brute-forcing the CNF from fm_metamodel's solver-agnostic ClauseSet,
4
+ so CI needs no SAT plugin — only flamapy-fm, which is already a declared dependency.
5
+ """
6
+ import os
7
+ import tempfile
8
+ from itertools import product
9
+
10
+ from flamapy.metamodels.fm_metamodel.transformations import UVLReader
11
+ from flamapy.metamodels.fm_metamodel.models import FeatureModel, ClauseSet
12
+ from flamapy.metamodels.sdd_metamodel.transformations import FmToSDD
13
+ from flamapy.metamodels.sdd_metamodel.operations import (
14
+ SDDConfigurationsNumber,
15
+ SDDSatisfiable,
16
+ SDDCoreFeatures,
17
+ SDDDeadFeatures,
18
+ )
19
+
20
+
21
+ def _valid_configs(fm: FeatureModel) -> tuple:
22
+ """(valid configurations as frozensets of selected names, all 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, set(clause_set.variables)
34
+
35
+
36
+ def _exact_count(fm: FeatureModel) -> int:
37
+ return len(_valid_configs(fm)[0])
38
+
39
+
40
+ def _core_features(fm: FeatureModel) -> set:
41
+ configs, names = _valid_configs(fm)
42
+ return {name for name in names if configs and all(name in cfg for cfg in configs)}
43
+
44
+
45
+ def _dead_features(fm: FeatureModel) -> set:
46
+ configs, names = _valid_configs(fm)
47
+ return names - (set().union(*configs) if configs else set())
48
+
49
+ _UVL = """features
50
+ Root {abstract}
51
+ mandatory
52
+ Base
53
+ optional
54
+ A
55
+ B
56
+ C
57
+ constraints
58
+ A => B
59
+ A | B
60
+ """
61
+
62
+ _UNSAT_UVL = """features
63
+ Root {abstract}
64
+ mandatory
65
+ A
66
+ constraints
67
+ !A
68
+ """
69
+
70
+
71
+ def _fm(uvl):
72
+ handle, path = tempfile.mkstemp(suffix='.uvl')
73
+ try:
74
+ with os.fdopen(handle, 'w') as file:
75
+ file.write(uvl)
76
+ return UVLReader(path).transform()
77
+ finally:
78
+ os.remove(path)
79
+
80
+
81
+ def test_count_matches_sat_enumeration():
82
+ fm = _fm(_UVL)
83
+ exact = _exact_count(fm)
84
+ sdd = SDDConfigurationsNumber().execute(FmToSDD(fm).transform()).get_result()
85
+ assert sdd == exact
86
+
87
+
88
+ def test_satisfiable():
89
+ assert SDDSatisfiable().execute(FmToSDD(_fm(_UVL)).transform()).get_result() is True
90
+
91
+
92
+ def test_unsatisfiable_model():
93
+ assert SDDSatisfiable().execute(FmToSDD(_fm(_UNSAT_UVL)).transform()).get_result() is False
94
+
95
+
96
+ def test_core_features_match_sat():
97
+ fm = _fm(_UVL)
98
+ expected = _core_features(fm)
99
+ sdd = set(SDDCoreFeatures().execute(FmToSDD(fm).transform()).get_result())
100
+ assert sdd == expected
101
+ assert 'Base' in sdd and 'Root' in sdd # mandatory chain is core
102
+
103
+
104
+ def test_dead_features_match_sat():
105
+ fm = _fm(_UVL)
106
+ expected = _dead_features(fm)
107
+ sdd = set(SDDDeadFeatures().execute(FmToSDD(fm).transform()).get_result())
108
+ assert sdd == expected # no dead features in this model
109
+
110
+
111
+ def test_void_model_has_no_core_features():
112
+ fm = _fm(_UNSAT_UVL)
113
+ model = FmToSDD(fm).transform()
114
+ assert SDDCoreFeatures().execute(model).get_result() == []
115
+ assert set(SDDCoreFeatures().execute(model).get_result()) == _core_features(fm)
116
+ assert set(SDDDeadFeatures().execute(model).get_result()) == _dead_features(fm)
117
+
118
+
119
+ _TSEYTIN_UVL = """features
120
+ Root {abstract}
121
+ optional
122
+ A
123
+ B
124
+ C
125
+ D
126
+ E
127
+ F
128
+ constraints
129
+ (A & B & C) | (D & E & F) | (B & E & F)
130
+ """
131
+
132
+
133
+ def test_cnf_methods_agree():
134
+ """Tseytin auxiliary variables must not change the count or the core/dead features.
135
+
136
+ The constraint is wide enough for 'auto' to choose Tseytin (27 distributive clauses).
137
+ """
138
+ fm = _fm(_TSEYTIN_UVL)
139
+ assert ClauseSet.from_feature_model(fm, cnf_method='auto').auxiliary_variables
140
+ expected = (_exact_count(fm), _core_features(fm), _dead_features(fm))
141
+ for method in ('distributive', 'tseytin', 'auto'):
142
+ model = FmToSDD(fm, cnf_method=method).transform()
143
+ got = (SDDConfigurationsNumber().execute(model).get_result(),
144
+ set(SDDCoreFeatures().execute(model).get_result()),
145
+ set(SDDDeadFeatures().execute(model).get_result()))
146
+ assert got == expected, method
147
+
148
+
149
+ # [1..2] over A, B, C (3 + 3 selections) times D optional: 12 configurations.
150
+ _GROUP_UVL = """features
151
+ Root {abstract}
152
+ [1..2]
153
+ A
154
+ B
155
+ C
156
+ optional
157
+ D
158
+ """
159
+
160
+
161
+ def test_group_cardinality():
162
+ fm = _fm(_GROUP_UVL)
163
+ model = FmToSDD(fm).transform()
164
+ assert SDDConfigurationsNumber().execute(model).get_result() == 12
165
+ assert set(SDDCoreFeatures().execute(model).get_result()) == _core_features(fm) == {'Root'}
166
+ assert set(SDDDeadFeatures().execute(model).get_result()) == _dead_features(fm) == set()
@@ -1,82 +0,0 @@
1
- """Tests for the SDD metamodel: exact counting, satisfiability, core/dead features.
2
-
3
- Oracles come from the SAT backend (a declared dependency) so CI needs no extra plugin.
4
- """
5
- import os
6
- import tempfile
7
-
8
- from flamapy.metamodels.fm_metamodel.transformations import UVLReader
9
- from flamapy.metamodels.sdd_metamodel.transformations import FmToSDD
10
- from flamapy.metamodels.sdd_metamodel.operations import (
11
- SDDConfigurationsNumber,
12
- SDDSatisfiable,
13
- SDDCoreFeatures,
14
- SDDDeadFeatures,
15
- )
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_core_features import PySATCoreFeatures
21
- from flamapy.metamodels.pysat_metamodel.operations.pysat_dead_features import PySATDeadFeatures
22
-
23
- _UVL = """features
24
- Root {abstract}
25
- mandatory
26
- Base
27
- optional
28
- A
29
- B
30
- C
31
- constraints
32
- A => B
33
- A | B
34
- """
35
-
36
- _UNSAT_UVL = """features
37
- Root {abstract}
38
- mandatory
39
- A
40
- constraints
41
- !A
42
- """
43
-
44
-
45
- def _fm(uvl):
46
- handle, path = tempfile.mkstemp(suffix='.uvl')
47
- try:
48
- with os.fdopen(handle, 'w') as file:
49
- file.write(uvl)
50
- return UVLReader(path).transform()
51
- finally:
52
- os.remove(path)
53
-
54
-
55
- def test_count_matches_sat_enumeration():
56
- fm = _fm(_UVL)
57
- exact = PySATConfigurationsNumber().execute(FmToPysat(fm).transform()).get_result()
58
- sdd = SDDConfigurationsNumber().execute(FmToSDD(fm).transform()).get_result()
59
- assert sdd == exact
60
-
61
-
62
- def test_satisfiable():
63
- assert SDDSatisfiable().execute(FmToSDD(_fm(_UVL)).transform()).get_result() is True
64
-
65
-
66
- def test_unsatisfiable_model():
67
- assert SDDSatisfiable().execute(FmToSDD(_fm(_UNSAT_UVL)).transform()).get_result() is False
68
-
69
-
70
- def test_core_features_match_sat():
71
- fm = _fm(_UVL)
72
- sat = set(PySATCoreFeatures().execute(FmToPysat(fm).transform()).get_result())
73
- sdd = set(SDDCoreFeatures().execute(FmToSDD(fm).transform()).get_result())
74
- assert sdd == sat
75
- assert 'Base' in sdd and 'Root' in sdd # mandatory chain is core
76
-
77
-
78
- def test_dead_features_match_sat():
79
- fm = _fm(_UVL)
80
- sat = set(PySATDeadFeatures().execute(FmToPysat(fm).transform()).get_result())
81
- sdd = set(SDDDeadFeatures().execute(FmToSDD(fm).transform()).get_result())
82
- assert sdd == sat # no dead features in this model