flamapy-sat 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 (81) hide show
  1. {flamapy_sat-2.6.0.dev11/flamapy_sat.egg-info → flamapy_sat-2.9.0.dev0}/PKG-INFO +6 -3
  2. flamapy_sat-2.9.0.dev0/flamapy/metamodels/pysat_diagnosis_metamodel/__init__.py +9 -0
  3. flamapy_sat-2.9.0.dev0/flamapy/metamodels/pysat_diagnosis_metamodel/models/__init__.py +8 -0
  4. flamapy_sat-2.9.0.dev0/flamapy/metamodels/pysat_diagnosis_metamodel/models/explanation.py +35 -0
  5. {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/flamapy/metamodels/pysat_diagnosis_metamodel/models/pysat_diagnosis_model.py +4 -1
  6. {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/__init__.py +10 -0
  7. flamapy_sat-2.9.0.dev0/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/fastdiag.py +2 -0
  8. flamapy_sat-2.9.0.dev0/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/hsdag/hsdag.py +2 -0
  9. flamapy_sat-2.9.0.dev0/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/hsdag/labeler/fastdiag_labeler.py +5 -0
  10. flamapy_sat-2.9.0.dev0/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/hsdag/labeler/labeler.py +6 -0
  11. flamapy_sat-2.9.0.dev0/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/hsdag/labeler/quickxplain_labeler.py +5 -0
  12. flamapy_sat-2.9.0.dev0/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/hsdag/node.py +2 -0
  13. flamapy_sat-2.9.0.dev0/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/quickxplain.py +2 -0
  14. flamapy_sat-2.9.0.dev0/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/utils.py +2 -0
  15. flamapy_sat-2.9.0.dev0/flamapy/metamodels/pysat_diagnosis_metamodel/operations/pysat_explanations.py +257 -0
  16. {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/flamapy/metamodels/pysat_diagnosis_metamodel/transformations/fm_to_diag_pysat.py +3 -2
  17. flamapy_sat-2.9.0.dev0/flamapy/metamodels/pysat_metamodel/__init__.py +46 -0
  18. flamapy_sat-2.9.0.dev0/flamapy/metamodels/pysat_metamodel/models/pysat_model.py +102 -0
  19. {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/flamapy/metamodels/pysat_metamodel/operations/__init__.py +12 -0
  20. flamapy_sat-2.9.0.dev0/flamapy/metamodels/pysat_metamodel/operations/pysat_atomic_sets.py +108 -0
  21. flamapy_sat-2.9.0.dev0/flamapy/metamodels/pysat_metamodel/operations/pysat_backbone.py +90 -0
  22. flamapy_sat-2.9.0.dev0/flamapy/metamodels/pysat_metamodel/operations/pysat_commonality.py +67 -0
  23. flamapy_sat-2.9.0.dev0/flamapy/metamodels/pysat_metamodel/operations/pysat_configurations.py +67 -0
  24. {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/flamapy/metamodels/pysat_metamodel/operations/pysat_configurations_number.py +3 -4
  25. flamapy_sat-2.9.0.dev0/flamapy/metamodels/pysat_metamodel/operations/pysat_false_optional_features.py +106 -0
  26. {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/flamapy/metamodels/pysat_metamodel/operations/pysat_filter.py +5 -7
  27. {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/flamapy/metamodels/pysat_metamodel/operations/pysat_metrics.py +20 -6
  28. {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/flamapy/metamodels/pysat_metamodel/operations/pysat_minimum_configuration.py +24 -0
  29. {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/flamapy/metamodels/pysat_metamodel/operations/pysat_sampling.py +6 -7
  30. flamapy_sat-2.9.0.dev0/flamapy/metamodels/pysat_metamodel/operations/pysat_testing.py +212 -0
  31. flamapy_sat-2.9.0.dev0/flamapy/metamodels/pysat_metamodel/operations/pysat_twise_sampling.py +347 -0
  32. {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/flamapy/metamodels/pysat_metamodel/transformations/dimacs_reader.py +6 -0
  33. {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/flamapy/metamodels/pysat_metamodel/transformations/dimacs_writer.py +5 -1
  34. {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/flamapy/metamodels/pysat_metamodel/transformations/fm_to_pysat.py +5 -3
  35. {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0/flamapy_sat.egg-info}/PKG-INFO +6 -3
  36. {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/flamapy_sat.egg-info/SOURCES.txt +5 -0
  37. flamapy_sat-2.9.0.dev0/flamapy_sat.egg-info/entry_points.txt +3 -0
  38. flamapy_sat-2.9.0.dev0/flamapy_sat.egg-info/requires.txt +14 -0
  39. {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/pyproject.toml +13 -3
  40. flamapy_sat-2.6.0.dev11/flamapy/metamodels/pysat_diagnosis_metamodel/__init__.py +0 -0
  41. flamapy_sat-2.6.0.dev11/flamapy/metamodels/pysat_diagnosis_metamodel/models/__init__.py +0 -5
  42. flamapy_sat-2.6.0.dev11/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/fastdiag.py +0 -95
  43. flamapy_sat-2.6.0.dev11/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/hsdag/hsdag.py +0 -312
  44. flamapy_sat-2.6.0.dev11/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/hsdag/labeler/fastdiag_labeler.py +0 -71
  45. flamapy_sat-2.6.0.dev11/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/hsdag/labeler/labeler.py +0 -49
  46. flamapy_sat-2.6.0.dev11/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/hsdag/labeler/quickxplain_labeler.py +0 -69
  47. flamapy_sat-2.6.0.dev11/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/hsdag/node.py +0 -78
  48. flamapy_sat-2.6.0.dev11/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/quickxplain.py +0 -88
  49. flamapy_sat-2.6.0.dev11/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/utils.py +0 -46
  50. flamapy_sat-2.6.0.dev11/flamapy/metamodels/pysat_metamodel/__init__.py +0 -0
  51. flamapy_sat-2.6.0.dev11/flamapy/metamodels/pysat_metamodel/models/pysat_model.py +0 -39
  52. flamapy_sat-2.6.0.dev11/flamapy/metamodels/pysat_metamodel/operations/pysat_backbone.py +0 -83
  53. flamapy_sat-2.6.0.dev11/flamapy/metamodels/pysat_metamodel/operations/pysat_commonality.py +0 -50
  54. flamapy_sat-2.6.0.dev11/flamapy/metamodels/pysat_metamodel/operations/pysat_configurations.py +0 -43
  55. flamapy_sat-2.6.0.dev11/flamapy/metamodels/pysat_metamodel/operations/pysat_false_optional_features.py +0 -57
  56. flamapy_sat-2.6.0.dev11/flamapy/metamodels/pysat_metamodel/operations/pysat_twise_sampling.py +0 -102
  57. flamapy_sat-2.6.0.dev11/flamapy_sat.egg-info/requires.txt +0 -10
  58. {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/LICENSE +0 -0
  59. {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/README.md +0 -0
  60. {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/__init__.py +0 -0
  61. {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/checker.py +0 -0
  62. {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/hsdag/__init__.py +0 -0
  63. {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/diagnosis/hsdag/labeler/__init__.py +0 -0
  64. {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/pysat_abstract_identifier.py +0 -0
  65. {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/pysat_configuration_diagnosis.py +0 -0
  66. {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/pysat_conflict.py +0 -0
  67. {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/flamapy/metamodels/pysat_diagnosis_metamodel/operations/pysat_diagnosis.py +0 -0
  68. {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/flamapy/metamodels/pysat_diagnosis_metamodel/transformations/__init__.py +0 -0
  69. {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/flamapy/metamodels/pysat_metamodel/models/__init__.py +0 -0
  70. {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/flamapy/metamodels/pysat_metamodel/models/txtcnf_model.py +0 -0
  71. {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/flamapy/metamodels/pysat_metamodel/operations/pysat_attribute_optimization.py +0 -0
  72. {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/flamapy/metamodels/pysat_metamodel/operations/pysat_core_features.py +0 -0
  73. {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/flamapy/metamodels/pysat_metamodel/operations/pysat_dead_features.py +0 -0
  74. {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/flamapy/metamodels/pysat_metamodel/operations/pysat_satisfiable.py +0 -0
  75. {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/flamapy/metamodels/pysat_metamodel/operations/pysat_satisfiable_configuration.py +0 -0
  76. {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/flamapy/metamodels/pysat_metamodel/transformations/__init__.py +0 -0
  77. {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/flamapy/metamodels/pysat_metamodel/transformations/cnf_to_pysat.py +0 -0
  78. {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/flamapy_sat.egg-info/dependency_links.txt +0 -0
  79. {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/flamapy_sat.egg-info/top_level.txt +0 -0
  80. {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/setup.cfg +0 -0
  81. {flamapy_sat-2.6.0.dev11 → flamapy_sat-2.9.0.dev0}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flamapy-sat
3
- Version: 2.6.0.dev11
3
+ Version: 2.9.0.dev0
4
4
  Summary: flamapy-sat is a plugin to flamapy module
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/pysat_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: python-sat~=0.1.7.dev1
14
14
  Provides-Extra: dev
15
15
  Requires-Dist: pytest; extra == "dev"
@@ -17,6 +17,9 @@ Requires-Dist: pytest-mock; extra == "dev"
17
17
  Requires-Dist: prospector; extra == "dev"
18
18
  Requires-Dist: mypy; extra == "dev"
19
19
  Requires-Dist: coverage; extra == "dev"
20
+ Provides-Extra: test
21
+ Requires-Dist: pytest; extra == "test"
22
+ Requires-Dist: flamapy-z3~=2.9.0.dev0; extra == "test"
20
23
  Dynamic: license-file
21
24
 
22
25
  # flamapy-sat
@@ -0,0 +1,9 @@
1
+ from flamapy.core.manifest import PluginManifest
2
+ from flamapy.core.models.language_level import LanguageLevel, MajorLevel, MinorLevel
3
+
4
+ MANIFEST = PluginManifest(
5
+ name='pysat_diagnosis',
6
+ extension='pysat_diagnosis',
7
+ # Same encoder as the SAT plugin (ClauseSet), so the same language level.
8
+ supported_level=LanguageLevel(MajorLevel.BOOLEAN, {MinorLevel.GROUP_CARDINALITY}),
9
+ )
@@ -0,0 +1,8 @@
1
+ from .pysat_diagnosis_model import DiagnosisModel
2
+ from .explanation import Explanation, ExplanationItem
3
+
4
+ __all__ = [
5
+ 'DiagnosisModel',
6
+ 'Explanation',
7
+ 'ExplanationItem',
8
+ ]
@@ -0,0 +1,35 @@
1
+ """Human-readable explanation values returned by the explain_* operations.
2
+
3
+ Each item names a relationship or cross-tree constraint of the model (the same
4
+ descriptions the diagnosis encoder records), and keeps the underlying assumption index so
5
+ tools can act on the result programmatically.
6
+ """
7
+ from dataclasses import dataclass
8
+
9
+
10
+ @dataclass(frozen=True)
11
+ class ExplanationItem:
12
+ constraint_repr: str # human-readable description of the relationship/constraint
13
+ index: int # assumption id in the diagnosis model (machine-usable)
14
+ kind: str # 'relationship' (tree) | 'constraint' (cross-tree)
15
+
16
+ def __str__(self) -> str:
17
+ return self.constraint_repr
18
+
19
+
20
+ @dataclass(frozen=True)
21
+ class Explanation:
22
+ items: tuple[ExplanationItem, ...] = ()
23
+
24
+ def descriptions(self) -> list[str]:
25
+ return [item.constraint_repr for item in self.items]
26
+
27
+ def __bool__(self) -> bool:
28
+ return bool(self.items)
29
+
30
+ def __str__(self) -> str:
31
+ if not self.items:
32
+ return 'No conflict found.'
33
+ return '\n'.join(
34
+ f'{number}. {item.constraint_repr}'
35
+ for number, item in enumerate(self.items, start=1))
@@ -117,7 +117,10 @@ class DiagnosisModel(PySATModel):
117
117
  test_case: Configuration = None) -> None:
118
118
  assumption: List[int] = []
119
119
 
120
- id_assumption = len(self.variables) + 1
120
+ # Assumption selectors start above every variable of the encoding: with Tseytin
121
+ # ('tseytin'/'auto') the ids after the features belong to auxiliary variables.
122
+ # Without auxiliary variables this is len(self.variables) + 1, as before.
123
+ id_assumption = self.top_variable() + 1
121
124
  id_assumption = self._prepare_assumptions_for_kb(assumption, id_assumption)
122
125
 
123
126
  start_id_configuration = len(assumption)
@@ -8,6 +8,12 @@ from .pysat_configuration_diagnosis import (
8
8
  PySATConfigurationRepair,
9
9
  PySATFeatureExplanation,
10
10
  )
11
+ from .pysat_explanations import (
12
+ PySATExplainDeadFeature,
13
+ PySATExplainFalseOptional,
14
+ PySATExplainVoidModel,
15
+ PySATMinimalCorrections,
16
+ )
11
17
 
12
18
 
13
19
  __all__ = [
@@ -16,5 +22,9 @@ __all__ = [
16
22
  'PySATConfigurationRepair',
17
23
  'PySATConflict',
18
24
  'PySATDiagnosis',
25
+ 'PySATExplainDeadFeature',
26
+ 'PySATExplainFalseOptional',
27
+ 'PySATExplainVoidModel',
19
28
  'PySATFeatureExplanation',
29
+ 'PySATMinimalCorrections',
20
30
  ]
@@ -0,0 +1,2 @@
1
+ """Moved to flamapy.core.reasoning in 2.9; this shim is removed in 3.0."""
2
+ from flamapy.core.reasoning.fastdiag import FastDiag # noqa: F401
@@ -0,0 +1,2 @@
1
+ """Moved to flamapy.core.reasoning in 2.9; this shim is removed in 3.0."""
2
+ from flamapy.core.reasoning.hsdag.hsdag import HSDAG # noqa: F401
@@ -0,0 +1,5 @@
1
+ """Moved to flamapy.core.reasoning in 2.9; this shim is removed in 3.0."""
2
+ from flamapy.core.reasoning.hsdag.labeler.fastdiag_labeler import ( # noqa: F401
3
+ FastDiagLabeler,
4
+ FastDiagParameters,
5
+ )
@@ -0,0 +1,6 @@
1
+ """Moved to flamapy.core.reasoning in 2.9; this shim is removed in 3.0."""
2
+ from flamapy.core.reasoning.hsdag.labeler.labeler import ( # noqa: F401
3
+ AbstractHSParameters,
4
+ IHSLabelable,
5
+ LabelerType,
6
+ )
@@ -0,0 +1,5 @@
1
+ """Moved to flamapy.core.reasoning in 2.9; this shim is removed in 3.0."""
2
+ from flamapy.core.reasoning.hsdag.labeler.quickxplain_labeler import ( # noqa: F401
3
+ QuickXPlainLabeler,
4
+ QuickXPlainParameters,
5
+ )
@@ -0,0 +1,2 @@
1
+ """Moved to flamapy.core.reasoning in 2.9; this shim is removed in 3.0."""
2
+ from flamapy.core.reasoning.hsdag.node import Node, NodeStatus # noqa: F401
@@ -0,0 +1,2 @@
1
+ """Moved to flamapy.core.reasoning in 2.9; this shim is removed in 3.0."""
2
+ from flamapy.core.reasoning.quickxplain import QuickXPlain # noqa: F401
@@ -0,0 +1,2 @@
1
+ """Moved to flamapy.core.reasoning in 2.9; this shim is removed in 3.0."""
2
+ from flamapy.core.reasoning.utils import * # noqa: F403
@@ -0,0 +1,257 @@
1
+ """User-facing explanation operations: minimal conflicts in constraint names, not indices.
2
+
3
+ Both operations compute a QuickXPlain minimal conflict over the model's relationships and
4
+ cross-tree constraints and report it through the :class:`Explanation` value type, whose
5
+ items carry the encoder's human-readable descriptions.
6
+
7
+ They rebuild a pristine diagnosis model from ``original_model`` on every execution:
8
+ ``prepare_diagnosis_task`` mutates the encoded clauses in place, so reusing the (facade-
9
+ cached) model across calls with different inputs would corrupt the assumption ids.
10
+ """
11
+ from typing import Optional, cast
12
+
13
+ from flamapy.core.exceptions import FlamaException
14
+ from flamapy.core.models import VariabilityModel
15
+ from flamapy.core.operations import Operation
16
+ from flamapy.core.operations.descriptor import OperationDescriptor, Input
17
+ from flamapy.core.reasoning import QuickXPlain
18
+ from flamapy.core.reasoning.hsdag import HSDAG
19
+ from flamapy.core.reasoning.hsdag.labeler.fastdiag_labeler import (
20
+ FastDiagLabeler,
21
+ FastDiagParameters,
22
+ )
23
+ from flamapy.metamodels.fm_metamodel.models import DEFAULT_CNF_METHOD
24
+ from flamapy.metamodels.configuration_metamodel.models import Configuration
25
+
26
+ from ..models.explanation import Explanation, ExplanationItem
27
+ from ..models.pysat_diagnosis_model import DiagnosisModel
28
+ from ..transformations.fm_to_diag_pysat import FmToDiagPysat
29
+ from .diagnosis.checker import ConsistencyChecker
30
+
31
+
32
+ def _explanation_descriptions(explanation: Explanation) -> list[str]:
33
+ return explanation.descriptions()
34
+
35
+
36
+ def _corrections_descriptions(explanations: list[Explanation]) -> list[list[str]]:
37
+ return [explanation.descriptions() for explanation in explanations]
38
+
39
+
40
+ def _prepared_model(
41
+ model: DiagnosisModel, test_case: Optional[Configuration]
42
+ ) -> DiagnosisModel:
43
+ """A pristine diagnosis model with the task prepared.
44
+
45
+ ``prepare_diagnosis_task`` mutates the encoded clauses in place, so the (facade-cached)
46
+ input model is never prepared directly: we re-encode from ``original_model``.
47
+ """
48
+ original = getattr(model, 'original_model', None)
49
+ cnf_method = getattr(model, 'cnf_method', None) or DEFAULT_CNF_METHOD
50
+ working = (FmToDiagPysat(original, cnf_method=cnf_method).transform()
51
+ if original is not None else model)
52
+ working.prepare_diagnosis_task(test_case=test_case)
53
+ return working
54
+
55
+
56
+ def _explanation_from(working: DiagnosisModel, indices: list[int]) -> Explanation:
57
+ original = getattr(working, 'original_model', None)
58
+ constraint_reprs = (
59
+ {str(ctc) for ctc in original.get_constraints()} if original is not None else set())
60
+ return Explanation(tuple(
61
+ ExplanationItem(
62
+ constraint_repr=working.constraint_assumption_map[index],
63
+ index=index,
64
+ kind='constraint'
65
+ if working.constraint_assumption_map[index] in constraint_reprs
66
+ else 'relationship',
67
+ )
68
+ for index in sorted(indices)))
69
+
70
+
71
+ def _minimal_conflict(
72
+ model: DiagnosisModel, test_case: Optional[Configuration], solver_name: str
73
+ ) -> Explanation:
74
+ working = _prepared_model(model, test_case)
75
+ checker = ConsistencyChecker(solver_name, working.get_kb())
76
+ try:
77
+ conflict = QuickXPlain(checker).find_conflict(working.get_c(), working.get_b())
78
+ finally:
79
+ checker.delete()
80
+ return _explanation_from(working, conflict)
81
+
82
+
83
+ class PySATExplainVoidModel(Operation):
84
+ """Why is the model void? A minimal, human-readable conflict among its constraints."""
85
+
86
+ facade = OperationDescriptor(
87
+ doc=(
88
+ 'Explains why the feature model is void (encodes no valid configuration): a\n'
89
+ 'minimal set of relationships/cross-tree constraints that is already\n'
90
+ 'unsatisfiable (a QuickXPlain minimal conflict), in human-readable form.\n'
91
+ 'Returns an empty list when the model is satisfiable. Requires the\n'
92
+ 'pysat_diagnosis plugin.'
93
+ ),
94
+ returns='Union[None, List[str]]',
95
+ name='explain_void_model', operation='PySATExplainVoidModel',
96
+ default_backend='pysat_diagnosis',
97
+ result_adapter=_explanation_descriptions,
98
+ )
99
+
100
+ def __init__(self) -> None:
101
+ self.result = Explanation()
102
+ self.solver_name = 'glucose3'
103
+
104
+ def execute(self, model: VariabilityModel) -> 'PySATExplainVoidModel':
105
+ self.result = _minimal_conflict(cast(DiagnosisModel, model), None, self.solver_name)
106
+ return self
107
+
108
+ def get_result(self) -> Explanation:
109
+ return self.result
110
+
111
+ def get_explanation(self) -> Explanation:
112
+ return self.result
113
+
114
+
115
+ class PySATExplainFalseOptional(Operation):
116
+ """Why is this feature false-optional? Conflict of the model plus parent without child."""
117
+
118
+ facade = OperationDescriptor(
119
+ doc=(
120
+ 'Explains why a feature is false-optional (declared optional but present in\n'
121
+ 'every valid configuration that includes its parent): a minimal set of\n'
122
+ 'relationships/cross-tree constraints that forbids deselecting it while its\n'
123
+ 'parent is selected (a QuickXPlain minimal conflict of the model plus\n'
124
+ '"parent = true, feature = false"), in human-readable form. Returns an empty\n'
125
+ 'list when the feature is not false-optional. Requires the pysat_diagnosis\n'
126
+ 'plugin.'
127
+ ),
128
+ returns='Union[None, List[str]]',
129
+ name='explain_false_optional', operation='PySATExplainFalseOptional',
130
+ default_backend='pysat_diagnosis',
131
+ inputs=(Input('feature_name', str, required=True, setter='set_feature'),),
132
+ result_adapter=_explanation_descriptions,
133
+ )
134
+
135
+ def __init__(self) -> None:
136
+ self.feature = ''
137
+ self.result = Explanation()
138
+ self.solver_name = 'glucose3'
139
+
140
+ def set_feature(self, feature_name: str) -> None:
141
+ self.feature = feature_name
142
+
143
+ def execute(self, model: VariabilityModel) -> 'PySATExplainFalseOptional':
144
+ diag_model = cast(DiagnosisModel, model)
145
+ original = getattr(diag_model, 'original_model', None)
146
+ feature = (original.get_feature_by_name(self.feature)
147
+ if original is not None else None)
148
+ if feature is None:
149
+ raise FlamaException(f"Feature '{self.feature}' is not in the model.")
150
+ parent = feature.get_parent()
151
+ if parent is None:
152
+ raise FlamaException(
153
+ f"Feature '{self.feature}' is the root; it cannot be false-optional.")
154
+ test_case = Configuration({parent.name: True, self.feature: False})
155
+ try:
156
+ self.result = _minimal_conflict(diag_model, test_case, self.solver_name)
157
+ except KeyError as error:
158
+ raise FlamaException(
159
+ f"Feature '{self.feature}' is not in the model.") from error
160
+ return self
161
+
162
+ def get_result(self) -> Explanation:
163
+ return self.result
164
+
165
+ def get_explanation(self) -> Explanation:
166
+ return self.result
167
+
168
+
169
+ class PySATMinimalCorrections(Operation):
170
+ """Minimal constraint sets whose removal makes a void model satisfiable (FastDiag)."""
171
+
172
+ facade = OperationDescriptor(
173
+ doc=(
174
+ 'Returns the minimal corrections for a void feature model: each correction is\n'
175
+ 'a minimal set of relationships/cross-tree constraints whose removal makes\n'
176
+ 'the model satisfiable (FastDiag diagnoses enumerated via HSDAG), in\n'
177
+ 'human-readable form. Returns an empty list when the model is already\n'
178
+ 'satisfiable. ``max_corrections`` bounds how many are enumerated. Requires\n'
179
+ 'the pysat_diagnosis plugin.'
180
+ ),
181
+ returns='Union[None, List[List[str]]]',
182
+ name='minimal_corrections', operation='PySATMinimalCorrections',
183
+ default_backend='pysat_diagnosis',
184
+ inputs=(Input('max_corrections', int, default=None, setter='set_max_corrections'),),
185
+ result_adapter=_corrections_descriptions,
186
+ )
187
+
188
+ def __init__(self) -> None:
189
+ self.max_corrections = -1 # -1 means no limit
190
+ self.result: list[Explanation] = []
191
+ self.solver_name = 'glucose3'
192
+
193
+ def set_max_corrections(self, max_corrections: int) -> None:
194
+ self.max_corrections = max_corrections
195
+
196
+ def execute(self, model: VariabilityModel) -> 'PySATMinimalCorrections':
197
+ working = _prepared_model(cast(DiagnosisModel, model), None)
198
+ checker = ConsistencyChecker(self.solver_name, working.get_kb())
199
+ try:
200
+ parameters = FastDiagParameters(working.get_c(), [], working.get_b())
201
+ hsdag = HSDAG(FastDiagLabeler(checker, parameters))
202
+ hsdag.max_number_diagnoses = self.max_corrections
203
+ hsdag.construct()
204
+ diagnoses = hsdag.get_diagnoses()
205
+ finally:
206
+ checker.delete()
207
+ self.result = [_explanation_from(working, diagnosis) for diagnosis in diagnoses]
208
+ return self
209
+
210
+ def get_result(self) -> list[Explanation]:
211
+ return self.result
212
+
213
+ def get_corrections(self) -> list[Explanation]:
214
+ return self.result
215
+
216
+
217
+ class PySATExplainDeadFeature(Operation):
218
+ """Why is this feature dead? A minimal conflict of the model plus 'feature = true'."""
219
+
220
+ facade = OperationDescriptor(
221
+ doc=(
222
+ 'Explains why a feature is dead (appears in no valid configuration): a\n'
223
+ 'minimal set of relationships/cross-tree constraints that forbids selecting\n'
224
+ 'it (a QuickXPlain minimal conflict of the model plus "feature = true"), in\n'
225
+ 'human-readable form. Returns an empty list when the feature is not dead.\n'
226
+ 'Requires the pysat_diagnosis plugin.'
227
+ ),
228
+ returns='Union[None, List[str]]',
229
+ name='explain_dead_feature', operation='PySATExplainDeadFeature',
230
+ default_backend='pysat_diagnosis',
231
+ inputs=(Input('feature_name', str, required=True, setter='set_feature'),),
232
+ result_adapter=_explanation_descriptions,
233
+ )
234
+
235
+ def __init__(self) -> None:
236
+ self.feature = ''
237
+ self.result = Explanation()
238
+ self.solver_name = 'glucose3'
239
+
240
+ def set_feature(self, feature_name: str) -> None:
241
+ self.feature = feature_name
242
+
243
+ def execute(self, model: VariabilityModel) -> 'PySATExplainDeadFeature':
244
+ diag_model = cast(DiagnosisModel, model)
245
+ test_case = Configuration({self.feature: True})
246
+ try:
247
+ self.result = _minimal_conflict(diag_model, test_case, self.solver_name)
248
+ except KeyError as error:
249
+ raise FlamaException(
250
+ f"Feature '{self.feature}' is not in the model.") from error
251
+ return self
252
+
253
+ def get_result(self) -> Explanation:
254
+ return self.result
255
+
256
+ def get_explanation(self) -> Explanation:
257
+ return self.result
@@ -1,4 +1,4 @@
1
- from flamapy.metamodels.fm_metamodel.models import FeatureModel, ClauseSet
1
+ from flamapy.metamodels.fm_metamodel.models import DEFAULT_CNF_METHOD, ClauseSet, FeatureModel
2
2
  from flamapy.metamodels.pysat_metamodel.transformations.fm_to_pysat import FmToPysat
3
3
  from ..models.pysat_diagnosis_model import DiagnosisModel
4
4
 
@@ -18,7 +18,7 @@ class FmToDiagPysat(FmToPysat):
18
18
  def get_destination_extension() -> str:
19
19
  return 'pysat_diagnosis'
20
20
 
21
- def __init__(self, source_model: FeatureModel, cnf_method: str = 'distributive') -> None:
21
+ def __init__(self, source_model: FeatureModel, cnf_method: str = DEFAULT_CNF_METHOD) -> None:
22
22
  super().__init__(source_model, cnf_method)
23
23
  self.destination_model = DiagnosisModel()
24
24
 
@@ -30,6 +30,7 @@ class FmToDiagPysat(FmToPysat):
30
30
  model.variables = dict(clause_set.variables)
31
31
  model.features = dict(clause_set.features)
32
32
  model.auxiliary_variables = set(clause_set.auxiliary_variables)
33
+ model.cnf_method = self.cnf_method
33
34
  model.original_model = self.source_model
34
35
  # Clause → constraint mapping (root feature / relations / cross-tree constraints).
35
36
  for description, clauses in clause_set.clause_groups:
@@ -0,0 +1,46 @@
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, 20-10,000 features, plus JHipster and busybox; worst case of the
6
+ # two densities, carried forward so a curve never decreases). SAT queries scale to the largest
7
+ # models (satisfiable 8 ms, core/dead 70 ms and false-optional 300 ms at 10,000 features), so the
8
+ # only limits are on the operations that enumerate every configuration.
9
+ _SAT_QUERY = ((2000, 1), (5000, 4), (10000, 8))
10
+ _IMPLICATION_QUERIES = ((1000, 2), (2000, 4), (5000, 40), (10000, 70))
11
+
12
+ MANIFEST = PluginManifest(
13
+ name='pysat',
14
+ calibration='2026-11-06',
15
+ extension='pysat',
16
+ supported_level=LanguageLevel(MajorLevel.BOOLEAN, {MinorLevel.GROUP_CARDINALITY}),
17
+ transformation_cost_curve=((200, 1), (1000, 4), (2000, 12), (5000, 19), (10000, 46)),
18
+ operations={
19
+ 'satisfiable': OperationCapability(cost_curve=_SAT_QUERY),
20
+ 'satisfiable_configuration': OperationCapability(cost_curve=_SAT_QUERY),
21
+ 'core_features': OperationCapability(cost_curve=_IMPLICATION_QUERIES),
22
+ 'dead_features': OperationCapability(cost_curve=_IMPLICATION_QUERIES),
23
+ 'false_optional_features': OperationCapability(
24
+ cost_curve=((500, 2), (1000, 6), (2000, 34), (5000, 110), (10000, 300))),
25
+ # Atomic sets: the classes of features with the same value in every configuration,
26
+ # proved with grouped implication queries like the backbone.
27
+ 'atomic_sets': OperationCapability(cost_curve=_IMPLICATION_QUERIES),
28
+ 'minimum_configuration': OperationCapability(
29
+ cost_curve=((200, 2), (1000, 5), (2000, 30), (5000, 38), (10000, 110))),
30
+ # Pairwise sampling (the only t-wise implementation): 1.2 s at 200 features, 56 s at
31
+ # 500, 6.5 min at 1,000; the 2,000-feature model did not finish within 10 minutes.
32
+ 't_wise_sampling': OperationCapability(
33
+ cost_curve=((50, 100), (100, 310), (200, 1200), (500, 56000), (1000, 390000),
34
+ (2000, 600000))),
35
+ # PySATSampling returns valid configurations, but in the solver's fixed order instead of
36
+ # uniformly at random (BDD sampling is uniform), so it is not the sample the operation
37
+ # promises: approximate, and the planner only takes it on allow_approximate.
38
+ 'sampling': OperationCapability(
39
+ exact=False, cost_curve=((1000, 1), (5000, 9), (10000, 15))),
40
+ # Counting and listing enumerate every configuration: exponential in the number of
41
+ # configurations rather than in the features (6 ms on the 20-feature calibration model).
42
+ 'configurations_number': OperationCapability(max_features=30, cost=400),
43
+ 'configurations': OperationCapability(max_features=30, cost=400),
44
+ 'commonality': OperationCapability(max_features=30, cost=400),
45
+ },
46
+ )
@@ -0,0 +1,102 @@
1
+ from itertools import chain
2
+ from typing import Iterable, Iterator, Optional
3
+
4
+ from pysat.formula import CNF
5
+ from pysat.solvers import Solver
6
+
7
+ from flamapy.core.exceptions import FlamaException
8
+ from flamapy.core.models import VariabilityModel
9
+
10
+
11
+ class PySATModel(VariabilityModel):
12
+ @staticmethod
13
+ def get_extension() -> str:
14
+ return 'pysat'
15
+
16
+ def __init__(self) -> None:
17
+ self._cnf = CNF()
18
+ self.variables: dict[str, int] = {} # feature's name -> id
19
+ self.features: dict[int, str] = {} # id -> feature's name
20
+ # Auxiliary (Tseytin) variable ids that are not features. They must be excluded
21
+ # from feature enumeration/counting; models over the features are unaffected.
22
+ self.auxiliary_variables: set[int] = set()
23
+ # The ClauseSet encoding the clauses come from ('auto', 'distributive' or 'tseytin'),
24
+ # or None when flamapy did not encode them (e.g. a model read from DIMACS). Operations
25
+ # that re-encode the feature model use it to keep the same encoding.
26
+ self.cnf_method: Optional[str] = None
27
+ self.original_model: VariabilityModel
28
+
29
+ def add_clause(self, clause: list[int]) -> None:
30
+ self._cnf.append(clause)
31
+
32
+ def get_variable(self, key: str) -> int:
33
+ if key not in self.variables:
34
+ raise FlamaException(f'Feature {key} not found')
35
+ return self.variables[key]
36
+
37
+ def get_all_clauses(self) -> CNF:
38
+ return self._cnf
39
+
40
+ # ---- feature projection helpers -------------------------------------------------------
41
+ # With cnf_method 'tseytin'/'auto' the CNF also contains auxiliary variables. Results are
42
+ # always reported over the features only; these helpers keep that in one place.
43
+
44
+ def is_feature(self, variable: int) -> bool:
45
+ """Whether a variable (or literal) is a feature, i.e. not an auxiliary variable."""
46
+ return abs(variable) in self.features
47
+
48
+ def is_auxiliary(self, variable: int) -> bool:
49
+ """Whether a variable (or literal) is an auxiliary (Tseytin) variable."""
50
+ return abs(variable) in self.auxiliary_variables
51
+
52
+ def feature_variables(self) -> list[int]:
53
+ """The ids of the feature variables, in increasing order."""
54
+ return sorted(self.features)
55
+
56
+ def top_variable(self) -> int:
57
+ """The highest variable id used by the encoding (features and auxiliary variables).
58
+
59
+ New variables (e.g. diagnosis assumption selectors) must start above it.
60
+ """
61
+ return max(chain(self.features, self.variables.values(), self.auxiliary_variables),
62
+ default=0)
63
+
64
+ def selected_features(self, assignment: Iterable[int]) -> list[str]:
65
+ """Names of the features set to true in a solver assignment, in assignment order.
66
+
67
+ Auxiliary variables are skipped.
68
+ """
69
+ return [self.features[literal] for literal in assignment
70
+ if literal > 0 and literal in self.features]
71
+
72
+ def enum_feature_models(self, solver: Solver,
73
+ assumptions: Iterable[int] = ()) -> Iterator[list[int]]:
74
+ """Enumerate the solver's models, one per distinct assignment of the features.
75
+
76
+ ``solver`` must already hold this model's clauses. Without auxiliary variables this is
77
+ exactly ``solver.enum_models``. With them, each model found is blocked on its feature
78
+ literals only, so models that differ only in auxiliary variables count once. (The
79
+ Tseytin gates of ClauseSet are biconditionals, which already makes the auxiliary
80
+ assignment unique, but the operations do not have to rely on that.) Like
81
+ ``enum_models``, this adds the blocking clauses to ``solver``.
82
+ """
83
+ assumptions = list(assumptions)
84
+ if not self.auxiliary_variables:
85
+ yield from solver.enum_models(assumptions=assumptions)
86
+ return
87
+ feature_variables = self.feature_variables()
88
+ while solver.solve(assumptions=assumptions):
89
+ model = solver.get_model()
90
+ yield model
91
+ true_literals = set(model)
92
+ solver.add_clause([-variable if variable in true_literals else variable
93
+ for variable in feature_variables])
94
+
95
+ def __eq__(self, other: object) -> bool:
96
+ if other is None or not isinstance(other, PySATModel):
97
+ return False
98
+ return set(self.get_all_clauses().clauses) == set(other.get_all_clauses().clauses)
99
+
100
+ def __hash__(self) -> int:
101
+ clause_tuples = (tuple(clause) for clause in self.get_all_clauses().clauses)
102
+ return hash(frozenset(clause_tuples))
@@ -3,6 +3,7 @@ from .pysat_satisfiable_configuration import PySATSatisfiableConfiguration
3
3
  from .pysat_configurations import PySATConfigurations
4
4
  from .pysat_configurations_number import PySATConfigurationsNumber
5
5
  from .pysat_commonality import PySATCommonality
6
+ from .pysat_atomic_sets import PySATAtomicSets
6
7
  from .pysat_filter import PySATFilter
7
8
  from .pysat_backbone import PySATBackbone
8
9
  from .pysat_core_features import PySATCoreFeatures
@@ -12,20 +13,31 @@ from .pysat_metrics import PySATMetrics
12
13
  from .pysat_attribute_optimization import PySATAttributeOptimization
13
14
  from .pysat_minimum_configuration import PySATMinimumConfiguration
14
15
  from .pysat_twise_sampling import PySATTWiseSampling
16
+ from .pysat_sampling import PySATSampling
17
+ from .pysat_testing import (
18
+ PySATCoverage,
19
+ PySATCoveringArray,
20
+ PySATSampleReduction,
21
+ )
15
22
 
16
23
 
17
24
  __all__ = [
25
+ 'PySATAtomicSets',
18
26
  'PySATAttributeOptimization',
19
27
  'PySATBackbone',
20
28
  'PySATCommonality',
21
29
  'PySATConfigurations',
22
30
  'PySATConfigurationsNumber',
23
31
  'PySATCoreFeatures',
32
+ 'PySATCoverage',
33
+ 'PySATCoveringArray',
24
34
  'PySATDeadFeatures',
25
35
  'PySATFalseOptionalFeatures',
26
36
  'PySATFilter',
27
37
  'PySATMetrics',
28
38
  'PySATMinimumConfiguration',
39
+ 'PySATSampleReduction',
40
+ 'PySATSampling',
29
41
  'PySATSatisfiable',
30
42
  'PySATSatisfiableConfiguration',
31
43
  'PySATTWiseSampling',