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.
- flamapy_bdd-2.9.0.dev0/PKG-INFO +70 -0
- flamapy_bdd-2.9.0.dev0/README.md +45 -0
- flamapy_bdd-2.9.0.dev0/flamapy/metamodels/bdd_metamodel/__init__.py +49 -0
- {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/models/bdd_model.py +120 -2
- flamapy_bdd-2.9.0.dev0/flamapy/metamodels/bdd_metamodel/models/utils/logic2bdd_writer.py +125 -0
- flamapy_bdd-2.9.0.dev0/flamapy/metamodels/bdd_metamodel/models/utils/ordering.py +130 -0
- {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/models/utils/pl_model.py +96 -52
- {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/operations/__init__.py +4 -1
- flamapy_bdd-2.9.0.dev0/flamapy/metamodels/bdd_metamodel/operations/bdd_backbone.py +61 -0
- {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/operations/bdd_commonality_factor.py +32 -0
- {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/operations/bdd_configurations_number.py +1 -1
- {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/operations/bdd_core_features.py +2 -5
- {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/operations/bdd_dead_features.py +2 -5
- {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/operations/bdd_false_optional_features.py +9 -10
- {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/operations/bdd_feature_inclusion_probability.py +96 -34
- {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/operations/bdd_homogeneity.py +12 -11
- {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/operations/bdd_metrics.py +36 -16
- {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/operations/bdd_pure_optional_features.py +6 -5
- {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/operations/bdd_sampling.py +1 -1
- {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/operations/bdd_satisfiable_configuration.py +9 -13
- flamapy_bdd-2.9.0.dev0/flamapy/metamodels/bdd_metamodel/operations/bdd_traversals.py +211 -0
- {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/operations/bdd_unique_features.py +33 -29
- {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/operations/bdd_variability.py +6 -0
- {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/operations/bdd_variant_features.py +5 -5
- flamapy_bdd-2.9.0.dev0/flamapy/metamodels/bdd_metamodel/transformations/dddmp_reader.py +188 -0
- flamapy_bdd-2.9.0.dev0/flamapy/metamodels/bdd_metamodel/transformations/fm_to_bdd.py +234 -0
- flamapy_bdd-2.9.0.dev0/flamapy_bdd.egg-info/PKG-INFO +70 -0
- {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy_bdd.egg-info/SOURCES.txt +14 -1
- flamapy_bdd-2.9.0.dev0/flamapy_bdd.egg-info/entry_points.txt +2 -0
- flamapy_bdd-2.9.0.dev0/flamapy_bdd.egg-info/requires.txt +15 -0
- {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/pyproject.toml +12 -3
- flamapy_bdd-2.9.0.dev0/tests/test_bdd_commonality.py +111 -0
- flamapy_bdd-2.9.0.dev0/tests/test_bdd_derived_metrics.py +140 -0
- {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/tests/test_bdd_metamodel.py +79 -0
- flamapy_bdd-2.9.0.dev0/tests/test_bdd_traversals.py +186 -0
- flamapy_bdd-2.9.0.dev0/tests/test_build_strategies.py +241 -0
- flamapy_bdd-2.9.0.dev0/tests/test_cacheable_bdd.py +43 -0
- flamapy_bdd-2.9.0.dev0/tests/test_dddmp_reader.py +86 -0
- flamapy_bdd-2.9.0.dev0/tests/test_exact_inclusion.py +118 -0
- flamapy_bdd-2.9.0.dev0/tests/test_reserved_names.py +56 -0
- flamapy_bdd-2.6.0.dev11/PKG-INFO +0 -34
- flamapy_bdd-2.6.0.dev11/README.md +0 -12
- flamapy_bdd-2.6.0.dev11/flamapy/metamodels/bdd_metamodel/__init__.py +0 -0
- flamapy_bdd-2.6.0.dev11/flamapy/metamodels/bdd_metamodel/transformations/dddmp_reader.py +0 -65
- flamapy_bdd-2.6.0.dev11/flamapy/metamodels/bdd_metamodel/transformations/fm_to_bdd.py +0 -40
- flamapy_bdd-2.6.0.dev11/flamapy_bdd.egg-info/PKG-INFO +0 -34
- flamapy_bdd-2.6.0.dev11/flamapy_bdd.egg-info/requires.txt +0 -11
- {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/LICENSE +0 -0
- {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/models/__init__.py +0 -0
- {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/models/utils/__init__.py +0 -0
- {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/models/utils/txtcnf.py +0 -0
- {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/operations/bdd_configurations.py +0 -0
- {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
- {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/operations/bdd_product_distribution.py +0 -0
- {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/operations/bdd_satisfiable.py +0 -0
- {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/operations/interfaces/__init__.py +0 -0
- {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/operations/interfaces/commonality_factor.py +0 -0
- {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/operations/interfaces/feature_inclusion_probability.py +0 -0
- {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/operations/interfaces/homogeneity.py +0 -0
- {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/operations/interfaces/product_distribution.py +0 -0
- {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/operations/interfaces/pure_optional_features.py +0 -0
- {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/operations/interfaces/unique_features.py +0 -0
- {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/operations/interfaces/variability.py +0 -0
- {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/operations/interfaces/variant_features.py +0 -0
- {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/transformations/__init__.py +0 -0
- {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/transformations/dddmp_writer.py +0 -0
- {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/transformations/json_reader.py +0 -0
- {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/transformations/json_writer.py +0 -0
- {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/transformations/pdf_writer.py +0 -0
- {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/transformations/png_writer.py +0 -0
- {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy/metamodels/bdd_metamodel/transformations/svg_writer.py +0 -0
- {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy_bdd.egg-info/dependency_links.txt +0 -0
- {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/flamapy_bdd.egg-info/top_level.txt +0 -0
- {flamapy_bdd-2.6.0.dev11 → flamapy_bdd-2.9.0.dev0}/setup.cfg +0 -0
- {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.
|
|
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}.")
|