flamapy-bdd 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 (75) hide show
  1. flamapy_bdd-2.9.0.dev0/PKG-INFO +70 -0
  2. flamapy_bdd-2.9.0.dev0/README.md +45 -0
  3. flamapy_bdd-2.9.0.dev0/flamapy/metamodels/bdd_metamodel/__init__.py +49 -0
  4. {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/models/bdd_model.py +120 -2
  5. flamapy_bdd-2.9.0.dev0/flamapy/metamodels/bdd_metamodel/models/utils/logic2bdd_writer.py +125 -0
  6. flamapy_bdd-2.9.0.dev0/flamapy/metamodels/bdd_metamodel/models/utils/ordering.py +130 -0
  7. {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/models/utils/pl_model.py +96 -52
  8. {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/operations/__init__.py +4 -1
  9. flamapy_bdd-2.9.0.dev0/flamapy/metamodels/bdd_metamodel/operations/bdd_backbone.py +61 -0
  10. {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/operations/bdd_commonality_factor.py +32 -0
  11. {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/operations/bdd_configurations_number.py +1 -1
  12. {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/operations/bdd_core_features.py +2 -5
  13. {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/operations/bdd_dead_features.py +2 -5
  14. {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/operations/bdd_false_optional_features.py +9 -10
  15. {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/operations/bdd_feature_inclusion_probability.py +96 -34
  16. {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/operations/bdd_homogeneity.py +12 -11
  17. {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/operations/bdd_metrics.py +36 -16
  18. {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/operations/bdd_pure_optional_features.py +6 -5
  19. {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/operations/bdd_sampling.py +1 -1
  20. {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/operations/bdd_satisfiable_configuration.py +9 -13
  21. flamapy_bdd-2.9.0.dev0/flamapy/metamodels/bdd_metamodel/operations/bdd_traversals.py +211 -0
  22. {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/operations/bdd_unique_features.py +33 -29
  23. {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/operations/bdd_variability.py +6 -0
  24. {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/operations/bdd_variant_features.py +5 -5
  25. flamapy_bdd-2.9.0.dev0/flamapy/metamodels/bdd_metamodel/transformations/dddmp_reader.py +188 -0
  26. flamapy_bdd-2.9.0.dev0/flamapy/metamodels/bdd_metamodel/transformations/fm_to_bdd.py +234 -0
  27. flamapy_bdd-2.9.0.dev0/flamapy_bdd.egg-info/PKG-INFO +70 -0
  28. {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy_bdd.egg-info/SOURCES.txt +14 -1
  29. flamapy_bdd-2.9.0.dev0/flamapy_bdd.egg-info/entry_points.txt +2 -0
  30. flamapy_bdd-2.9.0.dev0/flamapy_bdd.egg-info/requires.txt +15 -0
  31. {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/pyproject.toml +12 -3
  32. flamapy_bdd-2.9.0.dev0/tests/test_bdd_commonality.py +111 -0
  33. flamapy_bdd-2.9.0.dev0/tests/test_bdd_derived_metrics.py +140 -0
  34. {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/tests/test_bdd_metamodel.py +79 -0
  35. flamapy_bdd-2.9.0.dev0/tests/test_bdd_traversals.py +186 -0
  36. flamapy_bdd-2.9.0.dev0/tests/test_build_strategies.py +241 -0
  37. flamapy_bdd-2.9.0.dev0/tests/test_cacheable_bdd.py +43 -0
  38. flamapy_bdd-2.9.0.dev0/tests/test_dddmp_reader.py +86 -0
  39. flamapy_bdd-2.9.0.dev0/tests/test_exact_inclusion.py +118 -0
  40. flamapy_bdd-2.9.0.dev0/tests/test_reserved_names.py +56 -0
  41. flamapy_bdd-2.6.0.dev11/PKG-INFO +0 -34
  42. flamapy_bdd-2.6.0.dev11/README.md +0 -12
  43. flamapy_bdd-2.6.0.dev11/flamapy/metamodels/bdd_metamodel/__init__.py +0 -0
  44. flamapy_bdd-2.6.0.dev11/flamapy/metamodels/bdd_metamodel/transformations/dddmp_reader.py +0 -65
  45. flamapy_bdd-2.6.0.dev11/flamapy/metamodels/bdd_metamodel/transformations/fm_to_bdd.py +0 -40
  46. flamapy_bdd-2.6.0.dev11/flamapy_bdd.egg-info/PKG-INFO +0 -34
  47. flamapy_bdd-2.6.0.dev11/flamapy_bdd.egg-info/requires.txt +0 -11
  48. {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/LICENSE +0 -0
  49. {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/models/__init__.py +0 -0
  50. {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/models/utils/__init__.py +0 -0
  51. {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/models/utils/txtcnf.py +0 -0
  52. {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/operations/bdd_configurations.py +0 -0
  53. {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/operations/bdd_configurations_with_n_features.py +0 -0
  54. {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/operations/bdd_product_distribution.py +0 -0
  55. {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/operations/bdd_satisfiable.py +0 -0
  56. {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/operations/interfaces/__init__.py +0 -0
  57. {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/operations/interfaces/commonality_factor.py +0 -0
  58. {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/operations/interfaces/feature_inclusion_probability.py +0 -0
  59. {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/operations/interfaces/homogeneity.py +0 -0
  60. {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/operations/interfaces/product_distribution.py +0 -0
  61. {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/operations/interfaces/pure_optional_features.py +0 -0
  62. {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/operations/interfaces/unique_features.py +0 -0
  63. {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/operations/interfaces/variability.py +0 -0
  64. {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/operations/interfaces/variant_features.py +0 -0
  65. {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/transformations/__init__.py +0 -0
  66. {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/transformations/dddmp_writer.py +0 -0
  67. {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/transformations/json_reader.py +0 -0
  68. {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/transformations/json_writer.py +0 -0
  69. {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/transformations/pdf_writer.py +0 -0
  70. {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/transformations/png_writer.py +0 -0
  71. {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/transformations/svg_writer.py +0 -0
  72. {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy_bdd.egg-info/dependency_links.txt +0 -0
  73. {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy_bdd.egg-info/top_level.txt +0 -0
  74. {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/setup.cfg +0 -0
  75. {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/setup.py +0 -0
@@ -0,0 +1,70 @@
1
+ Metadata-Version: 2.4
2
+ Name: flamapy-bdd
3
+ Version: 2.9.0.dev0
4
+ Summary: bdd-plugin for the automated analysis of feature models
5
+ Author-email: Flamapy <flamapy@us.es>
6
+ License-Expression: GPL-3.0-or-later
7
+ Project-URL: Homepage, https://github.com/flamapy/bdd_metamodel
8
+ Requires-Python: >=3.11
9
+ Description-Content-Type: text/markdown
10
+ License-File: LICENSE
11
+ Requires-Dist: flamapy-fw~=2.9.0.dev0
12
+ Requires-Dist: flamapy-fm~=2.9.0.dev0
13
+ Requires-Dist: dd~=0.6.0
14
+ Requires-Dist: graphviz~=0.20
15
+ Provides-Extra: dev
16
+ Requires-Dist: pytest; extra == "dev"
17
+ Requires-Dist: pytest-mock; extra == "dev"
18
+ Requires-Dist: prospector; extra == "dev"
19
+ Requires-Dist: mypy; extra == "dev"
20
+ Requires-Dist: coverage; extra == "dev"
21
+ Provides-Extra: test
22
+ Requires-Dist: pytest; extra == "test"
23
+ Requires-Dist: flamapy-sat~=2.9.0.dev0; extra == "test"
24
+ Dynamic: license-file
25
+
26
+ # flamapy-bdd
27
+
28
+ BDD-based analysis for [flamapy](https://flamapy.org) feature models, built on
29
+ the [dd](https://github.com/tulip-control/dd) library.
30
+
31
+ **Documentation:** https://docs.flamapy.org/framework/plugins/bdd_plugin
32
+
33
+ ## Installation
34
+
35
+ ```bash
36
+ pip install flamapy-bdd
37
+ ```
38
+
39
+ `pip install dd` provides dd's pure-Python engine. For larger models, build dd against
40
+ [CUDD](https://github.com/ivmai/cudd), which is much faster and enables dynamic variable
41
+ reordering:
42
+
43
+ ```bash
44
+ pip install cython
45
+ DD_FETCH=1 DD_CUDD=1 pip install --force-reinstall --no-deps --no-build-isolation --no-binary dd "dd==0.6.0"
46
+ ```
47
+
48
+ ## How the BDD is built
49
+
50
+ `FmToBDD` conjoins the clauses of the feature model one at a time over a static
51
+ variable order inspired by Logic2BDD. With the CUDD engine it also reorders variables
52
+ dynamically while building. Every option can be passed to `FmToBDD(...)` or set with an
53
+ environment variable; explicit arguments take precedence.
54
+
55
+ | Option | Environment variable | Values (default first) |
56
+ |--------|----------------------|------------------------|
57
+ | `builder` | `FLAMAPY_BDD_BUILDER` | `python`; `native` and `auto` are experimental and need a native package that is not released yet |
58
+ | `build` | `FLAMAPY_BDD_BUILD` | `incremental`, `monolithic` (the whole formula at once) |
59
+ | `var_order` | `FLAMAPY_BDD_VAR_ORDER` | `tree` (feature-tree pre-order), `force` (FORCE heuristic), `model` |
60
+ | `clause_order` | `FLAMAPY_BDD_CLAUSE_ORDER` | `minspan`, `minmax`, `model` |
61
+ | `reorder` | `FLAMAPY_BDD_REORDER` | `auto` (on with CUDD, off otherwise), `1`, `0` |
62
+
63
+ The options only change how the BDD is built, never the configurations it represents.
64
+
65
+ With dd's pure-Python engine, building and walking a BDD recurses once per variable, so the
66
+ plugin raises Python's recursion limit (to about 1,000 + 4 per variable) when it builds or loads
67
+ a BDD. The limit applies to the whole process and is not lowered afterwards, because the
68
+ operations on the BDD need it too; keep it in mind when flamapy-bdd runs inside a server or in
69
+ Pyodide. See
70
+ `flamapy/metamodels/bdd_metamodel/models/utils/ordering.py` and `benchmarks/`.
@@ -0,0 +1,45 @@
1
+ # flamapy-bdd
2
+
3
+ BDD-based analysis for [flamapy](https://flamapy.org) feature models, built on
4
+ the [dd](https://github.com/tulip-control/dd) library.
5
+
6
+ **Documentation:** https://docs.flamapy.org/framework/plugins/bdd_plugin
7
+
8
+ ## Installation
9
+
10
+ ```bash
11
+ pip install flamapy-bdd
12
+ ```
13
+
14
+ `pip install dd` provides dd's pure-Python engine. For larger models, build dd against
15
+ [CUDD](https://github.com/ivmai/cudd), which is much faster and enables dynamic variable
16
+ reordering:
17
+
18
+ ```bash
19
+ pip install cython
20
+ DD_FETCH=1 DD_CUDD=1 pip install --force-reinstall --no-deps --no-build-isolation --no-binary dd "dd==0.6.0"
21
+ ```
22
+
23
+ ## How the BDD is built
24
+
25
+ `FmToBDD` conjoins the clauses of the feature model one at a time over a static
26
+ variable order inspired by Logic2BDD. With the CUDD engine it also reorders variables
27
+ dynamically while building. Every option can be passed to `FmToBDD(...)` or set with an
28
+ environment variable; explicit arguments take precedence.
29
+
30
+ | Option | Environment variable | Values (default first) |
31
+ |--------|----------------------|------------------------|
32
+ | `builder` | `FLAMAPY_BDD_BUILDER` | `python`; `native` and `auto` are experimental and need a native package that is not released yet |
33
+ | `build` | `FLAMAPY_BDD_BUILD` | `incremental`, `monolithic` (the whole formula at once) |
34
+ | `var_order` | `FLAMAPY_BDD_VAR_ORDER` | `tree` (feature-tree pre-order), `force` (FORCE heuristic), `model` |
35
+ | `clause_order` | `FLAMAPY_BDD_CLAUSE_ORDER` | `minspan`, `minmax`, `model` |
36
+ | `reorder` | `FLAMAPY_BDD_REORDER` | `auto` (on with CUDD, off otherwise), `1`, `0` |
37
+
38
+ The options only change how the BDD is built, never the configurations it represents.
39
+
40
+ With dd's pure-Python engine, building and walking a BDD recurses once per variable, so the
41
+ plugin raises Python's recursion limit (to about 1,000 + 4 per variable) when it builds or loads
42
+ a BDD. The limit applies to the whole process and is not lowered afterwards, because the
43
+ operations on the BDD need it too; keep it in mind when flamapy-bdd runs inside a server or in
44
+ Pyodide. See
45
+ `flamapy/metamodels/bdd_metamodel/models/utils/ordering.py` and `benchmarks/`.
@@ -0,0 +1,49 @@
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 with the pure-Python dd
5
+ # engine (what 2.9 ships): betty synthetic models at 1% and 30% constraint density plus
6
+ # JHipster and busybox, worst case of the two densities, never decreasing. Compiling the BDD is
7
+ # the wall, and it is erratic: 0.2 s for both 200-feature models, 0.4 s for the sparse
8
+ # 500-feature model but more than 5 minutes for the dense one, 1.7 s / 32 s at 1,000, and
9
+ # busybox (1,108 features) did not compile in 10 minutes. The journal benchmark with the same
10
+ # engine (300 s cap, real UVLHub models) compiled 29 of 41 models of 501-1,000 features and 2
11
+ # of 120 beyond, hence the 1,000-feature ceiling. Once compiled, every query is cheap (counting
12
+ # 29 ms, core/dead 140 ms, false-optional 300 ms at 1,000 features). The experimental native
13
+ # builder (flamapy-cudd, not released) is opt-in and not reflected here.
14
+ _MAX_FEATURES = 1000
15
+
16
+ MANIFEST = PluginManifest(
17
+ name='bdd',
18
+ calibration='2026-11-06',
19
+ extension='bdd',
20
+ supported_level=LanguageLevel(MajorLevel.BOOLEAN, {MinorLevel.GROUP_CARDINALITY}),
21
+ # Every operation needs the compiled BDD, so the ceiling is plugin-wide.
22
+ max_features=_MAX_FEATURES,
23
+ transformation_cost_curve=((50, 11), (100, 23), (200, 200), (1000, 300000)),
24
+ operations={
25
+ 'satisfiable': OperationCapability(cost=1),
26
+ 'satisfiable_configuration': OperationCapability(cost=1),
27
+ 'configurations_number': OperationCapability(
28
+ cost_curve=((100, 1), (200, 4), (500, 9), (1000, 29))),
29
+ # Two counts: all the products and those that include the configuration.
30
+ 'commonality': OperationCapability(
31
+ cost_curve=((100, 2), (200, 8), (500, 18), (1000, 58))),
32
+ 'core_features': OperationCapability(
33
+ cost_curve=((100, 1), (200, 20), (500, 33), (1000, 130))),
34
+ 'dead_features': OperationCapability(
35
+ cost_curve=((100, 1), (200, 20), (500, 30), (1000, 140))),
36
+ 'false_optional_features': OperationCapability(
37
+ cost_curve=((50, 2), (100, 3), (200, 42), (500, 72), (1000, 300))),
38
+ # Uniform random sampling over the compiled BDD.
39
+ 'sampling': OperationCapability(
40
+ cost_curve=((50, 2), (100, 3), (200, 29), (500, 57), (1000, 200))),
41
+ # Listing is bounded by the number of configurations, not by the BDD: about 3 us and
42
+ # 1 KB per configuration (measured 2026-11-08 on 16 UVLHub models of 1e3-1.5e6
43
+ # configurations), so a million already takes ~1 GB. Using the exact counts of the SoSyM
44
+ # benchmark (rq2_bdd_final), the share of UVLHub models with at most a million
45
+ # configurations is 100% up to 30 features, 76% at 31-50, 36% at 51-75 and under 25%
46
+ # beyond; with the same ~70% rule as the compilation ceiling, the limit is 50 features.
47
+ 'configurations': OperationCapability(max_features=50),
48
+ },
49
+ )
@@ -1,6 +1,8 @@
1
+ import importlib
1
2
  import logging
3
+ import sys
2
4
  from enum import Enum
3
- from typing import Any, Optional
5
+ from typing import Any, Iterable, Optional
4
6
 
5
7
  try:
6
8
  from dd.cudd import BDD
@@ -11,6 +13,77 @@ from flamapy.core.models import VariabilityModel
11
13
 
12
14
  logger = logging.getLogger(__name__)
13
15
 
16
+ # The dd engine in use: 'cudd' (C extension, only when dd is built against CUDD) or the
17
+ # pure-Python 'autoref' that a plain `pip install dd` provides.
18
+ DD_BACKEND = BDD.__module__.rsplit('.', 1)[-1]
19
+ _reorder = importlib.import_module(BDD.__module__).reorder # sifting, in both engines
20
+
21
+ # Dynamic reordering while building (see BDDModel.build_bdd_incremental): sift once the
22
+ # manager holds REORDER_MIN_NODES nodes, then again each time it grows by REORDER_GROWTH
23
+ # since the last reordering. Small BDDs are never reordered: sifting thousands of variables
24
+ # costs more than it saves on a BDD that stays small.
25
+ REORDER_MIN_NODES = 50_000
26
+ REORDER_GROWTH = 2.0
27
+
28
+ # The pure-Python engine recurses once per BDD level (and the node-walking operations too),
29
+ # so Python's recursion limit must grow with the number of variables.
30
+ _RECURSION_BASE = 1_000
31
+ _RECURSION_PER_VARIABLE = 4
32
+
33
+
34
+ def exact_count(bdd: Any, node: Any, nvars: int) -> int:
35
+ """Exact model count of `node` over `nvars` variables, with Python integers.
36
+
37
+ Works on any dd engine with complement edges: the nodes of the support are counted
38
+ bottom-up (without recursion) over the levels of the support, and the variables outside
39
+ the support double the count.
40
+ """
41
+ def regular(edge: Any) -> Any:
42
+ return ~edge if edge.negated else edge
43
+
44
+ nodes: list[Any] = []
45
+ seen: set[Any] = set()
46
+ top = regular(node)
47
+ stack = [top] if top.var is not None else []
48
+ while stack:
49
+ current = stack.pop()
50
+ if current in seen:
51
+ continue
52
+ seen.add(current)
53
+ nodes.append(current)
54
+ for edge in (current.low, current.high):
55
+ child = regular(edge)
56
+ if child.var is not None and child not in seen:
57
+ stack.append(child)
58
+ level = {var: bdd.level_of_var(var) for var in {n.var for n in nodes}}
59
+ rank = {var: i for i, var in enumerate(sorted(level, key=level.__getitem__))}
60
+ n_support = len(rank)
61
+ counts: dict[Any, int] = {}
62
+
63
+ def edge_count(edge: Any, parent_rank: int) -> int:
64
+ """Models of `edge` over the support variables below `parent_rank`."""
65
+ if edge.var is None:
66
+ value, child_rank = (0 if edge.negated else 1), n_support
67
+ else:
68
+ child_rank = rank[edge.var]
69
+ value = counts[regular(edge)]
70
+ if edge.negated:
71
+ value = 2 ** (n_support - child_rank) - value
72
+ return value * 2 ** (child_rank - parent_rank - 1)
73
+
74
+ for current in sorted(nodes, key=lambda n: rank[n.var], reverse=True):
75
+ current_rank = rank[current.var]
76
+ counts[current] = (edge_count(current.low, current_rank)
77
+ + edge_count(current.high, current_rank))
78
+ return edge_count(node, -1) * 2 ** max(nvars - n_support, 0)
79
+
80
+
81
+ def ensure_recursion_limit(n_variables: int) -> None:
82
+ """Raise Python's recursion limit, if needed, for BDDs over `n_variables` variables."""
83
+ needed = _RECURSION_BASE + _RECURSION_PER_VARIABLE * n_variables
84
+ if sys.getrecursionlimit() < needed:
85
+ sys.setrecursionlimit(needed)
86
+
14
87
 
15
88
  class BDDModel(VariabilityModel):
16
89
  """A Binary Decision Diagram (BDD) representation of the feature model.
@@ -39,12 +112,43 @@ class BDDModel(VariabilityModel):
39
112
 
40
113
  def build_bdd(self, expression: str, variables: list[str]) -> None:
41
114
  """Built a BDD from an expression representing a logical formula."""
115
+ ensure_recursion_limit(len(variables))
42
116
  self.bdd.configure(reordering=False) # Disable dynamic reordering for consistency
43
117
  for var in variables:
44
118
  self.bdd.declare(var) # Declare variables in the BDD manager
45
119
  self.vars_order = variables # The order is crucial to detect skipped variables
46
120
  self.root = self.bdd.add_expr(expression) # Build the logical formula (root node)
47
121
 
122
+ def build_bdd_incremental(self,
123
+ clauses: Iterable[str],
124
+ variables: list[str],
125
+ reorder: bool = False) -> None:
126
+ """Build a BDD as the conjunction of `clauses`, one clause at a time.
127
+
128
+ The variables are declared in the given order (the first one is the top level).
129
+ Conjoining clause by clause keeps the intermediate BDDs small when the clauses are
130
+ scheduled well, instead of parsing one huge expression. With `reorder`, the variables
131
+ are sifted between clauses when the BDD grows (REORDER_MIN_NODES, REORDER_GROWTH);
132
+ the engine's own automatic reordering stays disabled so that levels never change in
133
+ the middle of an operation, and `vars_order` is set to the final level order.
134
+ """
135
+ ensure_recursion_limit(len(variables))
136
+ self.bdd.configure(reordering=False)
137
+ for var in variables:
138
+ self.bdd.declare(var)
139
+ threshold = REORDER_MIN_NODES
140
+ root = self.bdd.true
141
+ for clause in clauses:
142
+ root = root & self.bdd.add_expr(clause)
143
+ if root == self.bdd.false:
144
+ break # The model is void: further clauses cannot change the result
145
+ if reorder and len(self.bdd) >= threshold:
146
+ _reorder(self.bdd)
147
+ threshold = max(REORDER_MIN_NODES, int(REORDER_GROWTH * len(self.bdd)))
148
+ self.root = root
149
+ # Operations rely on vars_order[i] being the variable at level i.
150
+ self.vars_order = sorted(variables, key=self.bdd.level_of_var)
151
+
48
152
  def get_expression(self) -> str:
49
153
  """ Converts the BDD to a readable Boolean expression string."""
50
154
  return self.bdd.to_expr(self.root)
@@ -55,7 +159,19 @@ class BDDModel(VariabilityModel):
55
159
 
56
160
  def count_solutions(self) -> int:
57
161
  """Count the number of valid configurations (SAT)."""
58
- return self.bdd.count(self.root, len(self.vars_order)) # count(node, n_vars)
162
+ return self.count_models(self.root, len(self.vars_order))
163
+
164
+ def count_models(self, node: Any, nvars: int) -> int:
165
+ """Exact number of assignments of `nvars` variables (a superset of the support of
166
+ `node`) that satisfy `node`.
167
+
168
+ dd's pure-Python engine counts with Python integers; the CUDD engine counts with
169
+ floating point, which is approximate beyond 2**53 and overflows beyond ~1e308, so for
170
+ CUDD the count is computed here with integers.
171
+ """
172
+ if DD_BACKEND != "cudd":
173
+ return int(self.bdd.count(node, nvars))
174
+ return exact_count(self.bdd, node, nvars)
59
175
 
60
176
  def dump_structure(self) -> None:
61
177
  """Prints the structure of the BDD for debugging purposes."""
@@ -102,6 +218,7 @@ class BDDModel(VariabilityModel):
102
218
  def load_bdd(path: str, vars: Optional[list[str]] = None) -> 'BDDModel':
103
219
  bdd_model = BDDModel()
104
220
  if vars is not None:
221
+ ensure_recursion_limit(len(vars))
105
222
  for var in vars:
106
223
  bdd_model.bdd.declare(var)
107
224
  roots = bdd_model.bdd.load(path)
@@ -112,6 +229,7 @@ class BDDModel(VariabilityModel):
112
229
  else:
113
230
  bdd_model.root = roots
114
231
  all_vars = list(bdd_model.bdd.vars)
232
+ ensure_recursion_limit(len(all_vars))
115
233
  sorted_vars = sorted(all_vars, key=bdd_model.bdd.level_of_var)
116
234
  bdd_model.vars_order = sorted_vars
117
235
  bdd_model.vars_features = {var: var for var in bdd_model.vars_order}
@@ -0,0 +1,125 @@
1
+ """Write a feature model as Logic2BDD input (a variable file and an expression file).
2
+
3
+ Logic2BDD (D. Fernández-Amorós, UNED) reads the variables, one order, from the variable file
4
+ and one constraint per line from the expression file. Its grammar has some traps that this
5
+ writer avoids:
6
+
7
+ * `<->` binds tighter than `and`/`or`, so every binary operation is written in parentheses.
8
+ * Operators must be separated by spaces (`a->b` would be read as one symbol).
9
+ * `and`, `or`, `not`, `xor`, `mux`, `if`... are keywords, so variables are written with
10
+ neutral names (`v0`, `v1`, ...) and the mapping to the real names is returned.
11
+ * Every line, the last one included, ends with a newline and there are no blank lines.
12
+
13
+ Groups use Logic2BDD's n-ary operators: `mux(...)` (at most one) for alternative and mutex
14
+ groups, which CUDD builds directly instead of from pairwise clauses.
15
+ """
16
+ import itertools
17
+ from dataclasses import dataclass, field
18
+
19
+ from flamapy.core.models.ast import ASTOperation, Node
20
+ from flamapy.metamodels.fm_metamodel.models import Constraint, FeatureModel, Relation
21
+
22
+
23
+ @dataclass
24
+ class Logic2BDDInput:
25
+ """Logic2BDD input files and the mapping between their names and the variables."""
26
+ variables: list[str] # variable names (the model's), in order
27
+ names: dict[str, str] # Logic2BDD name -> variable name
28
+ expressions: list[str] = field(default_factory=list)
29
+
30
+ @property
31
+ def var_text(self) -> str:
32
+ to_l2b = {var: name for name, var in self.names.items()}
33
+ return " ".join(to_l2b[var] for var in self.variables) + "\n"
34
+
35
+ @property
36
+ def exp_text(self) -> str:
37
+ return "".join(f"{line}\n" for line in self.expressions)
38
+
39
+
40
+ def feature_model_to_logic2bdd(feature_model: FeatureModel,
41
+ order: list[str]) -> Logic2BDDInput:
42
+ """Translate `feature_model` (with sanitized names) using the variable order `order`."""
43
+ names = {f"v{index}": var for index, var in enumerate(order)}
44
+ to_l2b = {var: name for name, var in names.items()}
45
+ result = Logic2BDDInput(variables=list(order), names=names)
46
+ if feature_model.root is None:
47
+ return result
48
+ result.expressions.append(to_l2b[feature_model.root.name])
49
+ for feature in feature_model.get_features():
50
+ for relation in feature.get_relations():
51
+ result.expressions.extend(_relation_lines(relation, to_l2b))
52
+ for constraint in feature_model.get_logical_constraints():
53
+ result.expressions.append(_constraint_line(constraint, to_l2b))
54
+ return result
55
+
56
+
57
+ def _or(items: list[str]) -> str:
58
+ return items[0] if len(items) == 1 else "(" + " or ".join(items) + ")"
59
+
60
+
61
+ def _relation_lines(relation: Relation, to_l2b: dict[str, str]) -> list[str]:
62
+ parent = to_l2b[relation.parent.name]
63
+ children = list(dict.fromkeys(to_l2b[child.name] for child in relation.children))
64
+ at_most_one = [f"mux({', '.join(children)})"] if len(children) > 1 else []
65
+ lines: list[str] = []
66
+ if relation.is_mandatory():
67
+ lines = [f"({parent} <-> {children[0]})"]
68
+ elif relation.is_optional():
69
+ lines = [f"({children[0]} -> {parent})"]
70
+ elif relation.is_or():
71
+ lines = [f"({parent} <-> {_or(children)})"]
72
+ elif relation.is_alternative():
73
+ lines = [f"({parent} <-> {_or(children)})", *at_most_one]
74
+ elif relation.is_mutex():
75
+ lines = [*(f"({child} -> {parent})" for child in children), *at_most_one]
76
+ elif relation.is_cardinal():
77
+ lines = _cardinality_lines(parent, children, relation.card_min, relation.card_max)
78
+ return lines
79
+
80
+
81
+ def _cardinality_lines(parent: str, children: list[str], card_min: int,
82
+ card_max: int) -> list[str]:
83
+ """The same constraints as PLModel, not the same clauses: every selection outside
84
+ [min, max] under the parent is forbidden, and "no child without its parent" is one
85
+ ``child -> parent`` line per child where PLModel enumerates the subsets."""
86
+ card_max = len(children) if card_max == -1 else card_max
87
+ lines = [f"({child} -> {parent})" for child in children]
88
+ for size in range(len(children) + 1):
89
+ if card_min <= size <= card_max:
90
+ continue
91
+ for selected in itertools.combinations(children, size):
92
+ literals = [f"(not {parent})"]
93
+ literals += [f"(not {child})" if child in selected else child for child in children]
94
+ lines.append(_or(literals))
95
+ return lines
96
+
97
+
98
+ _BINARY = {
99
+ ASTOperation.AND: "and",
100
+ ASTOperation.OR: "or",
101
+ ASTOperation.IMPLIES: "->",
102
+ ASTOperation.REQUIRES: "->",
103
+ ASTOperation.EQUIVALENCE: "<->",
104
+ }
105
+
106
+
107
+ def _constraint_line(constraint: Constraint, to_l2b: dict[str, str]) -> str:
108
+ return _node_text(constraint.ast.root, to_l2b)
109
+
110
+
111
+ def _node_text(node: Node, to_l2b: dict[str, str]) -> str:
112
+ if not node.is_op():
113
+ return to_l2b[str(node.data)]
114
+ operation = node.data
115
+ if operation == ASTOperation.NOT:
116
+ return f"(not {_node_text(node.left, to_l2b)})"
117
+ left = _node_text(node.left, to_l2b)
118
+ right = _node_text(node.right, to_l2b)
119
+ if operation == ASTOperation.EXCLUDES:
120
+ return f"({left} -> (not {right}))"
121
+ if operation == ASTOperation.XOR:
122
+ return f"(({left} or {right}) and (not ({left} and {right})))"
123
+ if operation in _BINARY:
124
+ return f"({left} {_BINARY[operation]} {right})"
125
+ raise ValueError(f"Operation {operation} is not supported by Logic2BDD.")
@@ -0,0 +1,130 @@
1
+ """Static variable orders and clause schedules for building a BDD clause by clause.
2
+
3
+ The size of a BDD depends heavily on the variable order, and the cost of building it
4
+ clause by clause depends on the order in which clauses are conjoined. These heuristics follow
5
+ the ideas of Logic2BDD (D. Fernández-Amorós, R. Heradio et al.) adapted to feature models:
6
+
7
+ Variable orders
8
+ ``model`` the order of `FeatureModel.get_features()`.
9
+ ``tree`` depth-first pre-order of the feature tree (a feature followed by its subtree).
10
+ ``force`` the FORCE hypergraph heuristic (Aloul, Markov and Sakallah, 2003), seeded with
11
+ the tree order: variables that share clauses are pulled together, which shortens
12
+ the span of cross-tree constraints.
13
+
14
+ Clause schedules
15
+ ``model`` the order in which the clauses were generated.
16
+ ``minspan`` clauses with a smaller span (distance between their first and last variable
17
+ in the order) first; ties broken by the position of their first variable.
18
+ ``minmax`` clauses whose deepest variable is higher in the order first.
19
+
20
+ All functions are pure and deterministic: the same inputs always give the same result.
21
+ """
22
+ from typing import Sequence
23
+
24
+ from flamapy.core.exceptions import FlamaException
25
+ from flamapy.metamodels.bdd_metamodel.models.utils.pl_model import PLClause
26
+
27
+
28
+ class BDDOptionError(FlamaException, ValueError):
29
+ """An invalid BDD build option (argument or FLAMAPY_BDD_* variable). A FlamaException like
30
+ every other invalid transformation option; also a ValueError, as it was before 2.9."""
31
+
32
+
33
+ VARIABLE_ORDERS = ("model", "tree", "force")
34
+ CLAUSE_ORDERS = ("model", "minspan", "minmax")
35
+
36
+ FORCE_MAX_ITERATIONS = 50
37
+
38
+
39
+ def variable_order(variables: Sequence[str],
40
+ clauses: Sequence[PLClause],
41
+ strategy: str = "tree",
42
+ model_order: Sequence[str] = ()) -> list[str]:
43
+ """Return the variables in the order given by `strategy`.
44
+
45
+ `variables` must be in feature-tree pre-order (as `PLModel.variables`); `model_order` is
46
+ only used by the ``model`` strategy.
47
+ """
48
+ if strategy == "tree":
49
+ return list(variables)
50
+ if strategy == "model":
51
+ ordered = [var for var in model_order if var in set(variables)]
52
+ seen = set(ordered)
53
+ return ordered + [var for var in variables if var not in seen]
54
+ if strategy == "force":
55
+ return force_order(variables, [clause.support for clause in clauses])
56
+ raise BDDOptionError(f"Unknown variable order '{strategy}'. Use one of {VARIABLE_ORDERS}.")
57
+
58
+
59
+ def total_span(order: Sequence[str], supports: Sequence[Sequence[str]]) -> int:
60
+ """Sum over the clauses of the distance between their first and last variable."""
61
+ position = {var: index for index, var in enumerate(order)}
62
+ span = 0
63
+ for support in supports:
64
+ levels = [position[var] for var in support if var in position]
65
+ if len(levels) > 1:
66
+ span += max(levels) - min(levels)
67
+ return span
68
+
69
+
70
+ def force_order(initial: Sequence[str], supports: Sequence[Sequence[str]]) -> list[str]:
71
+ """FORCE: move every variable to the mean centre of gravity of its clauses, repeat.
72
+
73
+ Each iteration computes the centre of gravity of every clause (the mean position of its
74
+ variables), places every variable at the mean of the centres of the clauses it belongs
75
+ to, and re-sorts. The best order found (smallest total span) is returned; ties keep the
76
+ previous position, so the result is deterministic.
77
+ """
78
+ order = list(initial)
79
+ edges = [tuple(dict.fromkeys(var for var in support if var in set(order)))
80
+ for support in supports]
81
+ edges = [edge for edge in edges if len(edge) > 1]
82
+ if not edges:
83
+ return order
84
+ var_edges: dict[str, list[int]] = {var: [] for var in order}
85
+ for index, edge in enumerate(edges):
86
+ for var in edge:
87
+ var_edges[var].append(index)
88
+
89
+ best_order = order
90
+ best_span = total_span(order, edges)
91
+ for _ in range(FORCE_MAX_ITERATIONS):
92
+ position = {var: index for index, var in enumerate(order)}
93
+ centres = [sum(position[var] for var in edge) / len(edge) for edge in edges]
94
+ target = {
95
+ var: (sum(centres[i] for i in var_edges[var]) / len(var_edges[var])
96
+ if var_edges[var] else float(position[var]))
97
+ for var in order
98
+ }
99
+ order = sorted(order, key=lambda var: (target[var], position[var]))
100
+ span = total_span(order, edges)
101
+ if span >= best_span:
102
+ break
103
+ best_order, best_span = order, span
104
+ return best_order
105
+
106
+
107
+ def clause_order(clauses: Sequence[PLClause],
108
+ order: Sequence[str],
109
+ strategy: str = "minspan") -> list[PLClause]:
110
+ """Return the clauses in the order given by `strategy` for the variable order `order`."""
111
+ if strategy == "model":
112
+ return list(clauses)
113
+ position = {var: index for index, var in enumerate(order)}
114
+
115
+ def bounds(clause: PLClause) -> tuple[int, int]:
116
+ levels = [position[var] for var in clause.support if var in position] or [0]
117
+ return min(levels), max(levels)
118
+
119
+ indexed = list(enumerate(clauses))
120
+ if strategy == "minspan":
121
+ def span_key(item: tuple[int, PLClause]) -> tuple[int, int, int]:
122
+ low, high = bounds(item[1])
123
+ return (high - low, low, item[0])
124
+ return [clause for _, clause in sorted(indexed, key=span_key)]
125
+ if strategy == "minmax":
126
+ def max_key(item: tuple[int, PLClause]) -> tuple[int, int, int]:
127
+ low, high = bounds(item[1])
128
+ return (high, low, item[0])
129
+ return [clause for _, clause in sorted(indexed, key=max_key)]
130
+ raise BDDOptionError(f"Unknown clause order '{strategy}'. Use one of {CLAUSE_ORDERS}.")