flamapy 2.6.0.dev4__py3-none-any.whl → 2.6.0.dev5__py3-none-any.whl

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.
@@ -5,7 +5,7 @@ from functools import wraps
5
5
  import inspect
6
6
  from pathlib import Path
7
7
  from shutil import copytree
8
- from typing import Any, List, Tuple, Optional
8
+ from typing import Any, List, Tuple, Optional, Union, get_args, get_origin
9
9
  from types import FunctionType
10
10
 
11
11
  from flamapy.interfaces.python.flamapy_feature_model import FLAMAFeatureModel
@@ -92,30 +92,43 @@ def generate_plugin(args): # type: ignore
92
92
  print("Plugin generated!")
93
93
 
94
94
 
95
+ def resolve_annotation_type(annotation: Any) -> type:
96
+ """Map a parameter annotation to a concrete type usable as an argparse 'type'.
97
+
98
+ Unwraps Optional[X]/Union[X, None] to X; falls back to str for anything
99
+ that is not a plain class (argparse requires a callable).
100
+ """
101
+ if annotation is inspect.Parameter.empty:
102
+ return str
103
+ if get_origin(annotation) is Union:
104
+ args = [arg for arg in get_args(annotation) if arg is not type(None)]
105
+ annotation = args[0] if args else str
106
+ return annotation if isinstance(annotation, type) else str
107
+
108
+
95
109
  def setup_dynamic_commands(subparsers, dynamic_commands): # type: ignore
96
110
  for name, docstring, method, parameters in dynamic_commands:
97
111
  subparser = subparsers.add_parser(name, help=docstring)
98
112
  subparser.add_argument("model_path", type=str, help="Path to the feature model file")
99
- if "configuration_path" in [param.name for param in parameters]:
100
- subparser.add_argument(
101
- "--configuration_path",
102
- type=str,
103
- help="Path to the configuration file",
104
- required=False,
105
- )
106
113
  for param in parameters:
107
114
  arg_name = param.name
108
115
  if arg_name not in ["model_path"]: # Avoid duplicates
116
+ arg_type = resolve_annotation_type(param.annotation)
109
117
  if param.default == param.empty: # Positional argument
118
+ subparser.add_argument(arg_name, type=arg_type, help=arg_type.__name__)
119
+ elif arg_type is bool: # Boolean flag (type=bool would parse 'False' as True)
110
120
  subparser.add_argument(
111
- arg_name, type=param.annotation, help=param.annotation.__name__
121
+ f"--{arg_name}",
122
+ action=argparse.BooleanOptionalAction,
123
+ default=param.default,
124
+ help="Optional bool",
112
125
  )
113
126
  else: # Optional argument
114
127
  subparser.add_argument(
115
128
  f"--{arg_name}",
116
- type=param.annotation,
129
+ type=arg_type,
117
130
  default=param.default,
118
- help=f"Optional {param.annotation.__name__}",
131
+ help=f"Optional {arg_type.__name__}",
119
132
  )
120
133
  subparser.set_defaults(func=method, method_name=name, parameters=parameters)
121
134
 
@@ -5,6 +5,7 @@ from flamapy.core.discover import DiscoverMetamodels
5
5
  from flamapy.core.models import VariabilityModel
6
6
  from flamapy.metamodels.fm_metamodel.models import FeatureModel
7
7
  from flamapy.core.exceptions import FlamaException
8
+ from flamapy.core.operations import OptimizationGoal
8
9
  from flamapy.metamodels.configuration_metamodel.models import Configuration
9
10
 
10
11
  logger = logging.getLogger(__name__)
@@ -16,6 +17,7 @@ class Backend(str, Enum):
16
17
  SAT = "sat"
17
18
  BDD = "bdd"
18
19
  Z3 = "z3"
20
+ SHARPSAT = "sharpsat"
19
21
 
20
22
 
21
23
  class FLAMAFeatureModel:
@@ -24,6 +26,7 @@ class FLAMAFeatureModel:
24
26
  Backend.SAT: ("_transform_to_sat", "sat_model"),
25
27
  Backend.BDD: ("_transform_to_bdd", "bdd_model"),
26
28
  Backend.Z3: ("_transform_to_z3", "z3_model"),
29
+ Backend.SHARPSAT: ("_transform_to_sharpsat", "sharpsat_model"),
27
30
  }
28
31
 
29
32
  def __init__(self, model_path: str, backend: Optional[str] = None):
@@ -42,6 +45,7 @@ class FLAMAFeatureModel:
42
45
  self.sat_model = None
43
46
  self.bdd_model = None
44
47
  self.z3_model = None
48
+ self.sharpsat_model = None
45
49
  self.diagnosis_model = None
46
50
  self.backend = Backend(backend) if backend is not None else None
47
51
 
@@ -84,6 +88,14 @@ class FLAMAFeatureModel:
84
88
  if self.z3_model is None:
85
89
  self.z3_model = self.discover_metamodel.use_transformation_m2m(self.fm_model, "z3")
86
90
 
91
+ def _transform_to_sharpsat(self) -> None:
92
+ if self.sharpsat_model is None:
93
+ # Requires the optional flamapy-sharpsat plugin. use_transformation_m2m raises a
94
+ # FlamaException if it is not installed, which the operations surface to the caller.
95
+ self.sharpsat_model = self.discover_metamodel.use_transformation_m2m(
96
+ self.fm_model, "sharpsat"
97
+ )
98
+
87
99
  def _transform_to_diagnosis(self) -> None:
88
100
  if self.diagnosis_model is None:
89
101
  self.diagnosis_model = self.discover_metamodel.use_transformation_m2m(
@@ -338,7 +350,8 @@ class FLAMAFeatureModel:
338
350
  combinations of features, taking into account the constraints and
339
351
  dependencies between features.
340
352
 
341
- ``backend`` selects the analysis plugin ("sat", "bdd" or "z3"); defaults to bdd.
353
+ ``backend`` selects the analysis plugin ("sat", "bdd", "z3", or "sharpsat" for a
354
+ scalable approximate count via the optional flamapy-sharpsat plugin); defaults to bdd.
342
355
  """
343
356
  try:
344
357
  model = self._select_backend(backend, Backend.BDD)
@@ -604,8 +617,9 @@ class FLAMAFeatureModel:
604
617
  Returns a random sample of valid configurations of the given size. When
605
618
  with_replacement is True, the same configuration may appear more than once.
606
619
 
607
- ``backend`` selects the analysis plugin; defaults to bdd. Only sat and bdd
608
- implement sampling.
620
+ ``backend`` selects the analysis plugin; defaults to bdd. sat and bdd implement
621
+ (deterministic/enumerating) sampling; "sharpsat" provides almost-uniform sampling
622
+ via the optional flamapy-sharpsat plugin.
609
623
  """
610
624
  try:
611
625
  model = self._select_backend(backend, Backend.BDD)
@@ -618,6 +632,76 @@ class FLAMAFeatureModel:
618
632
  logger.error("Error: %s", exception)
619
633
  return None
620
634
 
635
+ def t_wise_sampling(
636
+ self, t: int = 2, backend: Optional[str] = None
637
+ ) -> Union[None, List[Configuration]]:
638
+ """
639
+ Returns a t-wise (combinatorial) sample: a set of valid configurations that covers
640
+ every satisfiable combination of ``t`` feature selections (pairwise for ``t = 2``).
641
+
642
+ ``backend`` selects the analysis plugin; only "sat" implements t-wise sampling.
643
+ """
644
+ try:
645
+ model = self._select_backend(backend, Backend.SAT)
646
+ operation = self.discover_metamodel.get_operation(model, "PySATTWiseSampling")
647
+ operation.set_t(t)
648
+ operation.execute(model)
649
+ return operation.get_result()
650
+ except FlamaException as exception:
651
+ logger.error("Error: %s", exception)
652
+ return None
653
+
654
+ def minimum_configuration(
655
+ self, backend: Optional[str] = None
656
+ ) -> Union[None, Configuration]:
657
+ """
658
+ Returns a valid configuration with the fewest selected features (the minimum
659
+ working configuration).
660
+
661
+ ``backend`` selects the analysis plugin; only "sat" implements this operation.
662
+ """
663
+ try:
664
+ model = self._select_backend(backend, Backend.SAT)
665
+ operation = self.discover_metamodel.get_operation(model, "PySATMinimumConfiguration")
666
+ operation.execute(model)
667
+ return operation.get_result()
668
+ except FlamaException as exception:
669
+ logger.error("Error: %s", exception)
670
+ return None
671
+
672
+ def attribute_optimization(
673
+ self, objectives: Any, backend: Optional[str] = None
674
+ ) -> Union[None, List[Configuration]]:
675
+ """
676
+ Returns the configuration(s) that optimize one or more numeric feature attributes.
677
+
678
+ ``objectives`` is either a single attribute name (defaults to minimizing it) or a
679
+ mapping ``{attribute_name: "Minimize"|"Maximize"}``. ``backend`` selects the plugin:
680
+ "sat" performs single-objective MaxSAT optimization; "z3" additionally supports
681
+ typed attributes and multi-objective (Pareto) optimization. Defaults to "sat".
682
+ """
683
+ def _goal(value: Any) -> OptimizationGoal:
684
+ return (OptimizationGoal.MINIMIZE
685
+ if str(value).lower().startswith('min')
686
+ else OptimizationGoal.MAXIMIZE)
687
+
688
+ if isinstance(objectives, str):
689
+ attributes = {objectives: OptimizationGoal.MINIMIZE}
690
+ else:
691
+ attributes = {name: _goal(goal) for name, goal in dict(objectives).items()}
692
+
693
+ try:
694
+ model = self._select_backend(backend, Backend.SAT)
695
+ operation = self.discover_metamodel.get_operation(model, "AttributeOptimization")
696
+ operation.set_attributes(attributes)
697
+ operation.execute(model)
698
+ # z3 returns (configuration, values) tuples; sat returns configurations.
699
+ return [item[0] if isinstance(item, tuple) else item
700
+ for item in operation.get_result()]
701
+ except FlamaException as exception:
702
+ logger.error("Error: %s", exception)
703
+ return None
704
+
621
705
  # Z3-based operations
622
706
 
623
707
  def all_feature_bounds(self) -> Union[None, Dict[str, Dict[str, Any]]]:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: flamapy
3
- Version: 2.6.0.dev4
3
+ Version: 2.6.0.dev5
4
4
  Summary: Flamapy feature model is a distribution of the flama framework containing all plugins required to analyze feature models. It also offers a richer API and a complete command line interface and documentation.
5
5
  Author-email: Flamapy <flamapy@us.es>
6
6
  License-Expression: GPL-3.0-or-later
@@ -8,17 +8,19 @@ Project-URL: Homepage, https://github.com/flamapy/flamapy-feature-model
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.dev4
12
- Requires-Dist: flamapy-fm~=2.6.0.dev4
13
- Requires-Dist: flamapy-sat~=2.6.0.dev4
14
- Requires-Dist: flamapy-bdd~=2.6.0.dev4
15
- Requires-Dist: flamapy-z3~=2.6.0.dev4
11
+ Requires-Dist: flamapy-fw~=2.6.0.dev5
12
+ Requires-Dist: flamapy-fm~=2.6.0.dev5
13
+ Requires-Dist: flamapy-sat~=2.6.0.dev5
14
+ Requires-Dist: flamapy-bdd~=2.6.0.dev5
15
+ Requires-Dist: flamapy-z3~=2.6.0.dev5
16
16
  Provides-Extra: dev
17
17
  Requires-Dist: pytest; extra == "dev"
18
18
  Requires-Dist: pytest-mock; extra == "dev"
19
19
  Requires-Dist: prospector; extra == "dev"
20
20
  Requires-Dist: mypy; extra == "dev"
21
21
  Requires-Dist: coverage; extra == "dev"
22
+ Provides-Extra: sharpsat
23
+ Requires-Dist: flamapy-sharpsat~=2.6.0.dev4; extra == "sharpsat"
22
24
  Dynamic: license-file
23
25
 
24
26
  # flamapy
@@ -0,0 +1,9 @@
1
+ flamapy/commands/__init__.py,sha256=KKFyT5PDLYCE_FUD6RMtZGHmP0XIUvoPoTSxzsfAwks,7289
2
+ flamapy/interfaces/python/__init__.py,sha256=NVQH1l6O9ElD8YvUVBXJN0Y_ETmu3AbyQBZJJ9qiuoU,86
3
+ flamapy/interfaces/python/flamapy_feature_model.py,sha256=hcP6kVVYV1nJ1vFTwTo_4rOp84h40NGsUREDo0I6zbI,34546
4
+ flamapy-2.6.0.dev5.dist-info/licenses/LICENSE,sha256=46mU2C5kSwOnkqkw9XQAJlhBL2JAf1_uCD8lVcXyMRg,7652
5
+ flamapy-2.6.0.dev5.dist-info/METADATA,sha256=6c6BHMwLKgvb1Vw3jKMccouvL9vLrP_6txJ-d7B6HG8,1355
6
+ flamapy-2.6.0.dev5.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
7
+ flamapy-2.6.0.dev5.dist-info/entry_points.txt,sha256=CS_W4K9LW9xOf-7ymWD58KklNHBnhMHm0RXAZ9JLgVo,57
8
+ flamapy-2.6.0.dev5.dist-info/top_level.txt,sha256=2YBI9dg4GVk28AJuyNu6dldi5Riwt5SP0zpNe2lSU5M,8
9
+ flamapy-2.6.0.dev5.dist-info/RECORD,,
@@ -1,9 +0,0 @@
1
- flamapy/commands/__init__.py,sha256=S9KJB8u4Ww5_6KQYs70iwQFbtDm08J7UvE7vxAB6DeE,6609
2
- flamapy/interfaces/python/__init__.py,sha256=NVQH1l6O9ElD8YvUVBXJN0Y_ETmu3AbyQBZJJ9qiuoU,86
3
- flamapy/interfaces/python/flamapy_feature_model.py,sha256=ojxMc3XtAvnBs9T7ETLo1rFpt1g0XRMc8MRdHf1HJ2w,30598
4
- flamapy-2.6.0.dev4.dist-info/licenses/LICENSE,sha256=46mU2C5kSwOnkqkw9XQAJlhBL2JAf1_uCD8lVcXyMRg,7652
5
- flamapy-2.6.0.dev4.dist-info/METADATA,sha256=jbaO1-9RFf15ynsn6ftjbaoy0eV8Mp3JjwAKSowwD3o,1265
6
- flamapy-2.6.0.dev4.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
7
- flamapy-2.6.0.dev4.dist-info/entry_points.txt,sha256=CS_W4K9LW9xOf-7ymWD58KklNHBnhMHm0RXAZ9JLgVo,57
8
- flamapy-2.6.0.dev4.dist-info/top_level.txt,sha256=2YBI9dg4GVk28AJuyNu6dldi5Riwt5SP0zpNe2lSU5M,8
9
- flamapy-2.6.0.dev4.dist-info/RECORD,,