Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Store features as tree #38

Merged
merged 1 commit into from
Sep 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 13 additions & 5 deletions cfmtoolbox/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,12 +64,21 @@ def __str__(self) -> str:

@dataclass
class CFM:
features: list[Feature]
root: Feature
require_constraints: list[Constraint]
exclude_constraints: list[Constraint]

@property
def features(self) -> list[Feature]:
features = [self.root]

for feature in features:
features.extend(feature.children)

return features

def is_unbound(self) -> bool:
return self.features[0].is_unbound()
return self.root.is_unbound()


@dataclass
Expand All @@ -79,11 +88,10 @@ class FeatureNode:

def validate(self, cfm: CFM) -> bool:
# Check if root feature is valid
root_feature = cfm.features[0]
if root_feature.name != self.value.split("#")[0]:
if cfm.root.name != self.value.split("#")[0]:
return False

if not self.validate_children(root_feature):
if not self.validate_children(cfm.root):
return False

return self.validate_constraints(cfm)
Expand Down
13 changes: 6 additions & 7 deletions cfmtoolbox/plugins/big_m.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,25 +4,24 @@

@app.command()
def apply_big_m(model: CFM) -> CFM:
global_upper_bound = get_global_upper_bound(model.features[0])
global_upper_bound = get_global_upper_bound(model.root)

replace_infinite_upper_bound_with_global_upper_bound(
model.features[0], global_upper_bound
)
replace_infinite_upper_bound_with_global_upper_bound(model.root, global_upper_bound)

print("Successfully applied Big-M global bound.")

return model


def get_global_upper_bound(feature: Feature):
def get_global_upper_bound(feature: Feature) -> int:
global_upper_bound = feature.instance_cardinality.intervals[-1].upper
local_upper_bound = global_upper_bound

# Terminate calculation if the upper bound is infinite
if local_upper_bound is None:
if global_upper_bound is None:
return 0

local_upper_bound = global_upper_bound

# Recursively calculate the global upper bound by multiplying the upper bounds of all paths
# from the root feature excluding paths that contain a feature with an infinite upper bound
for child in feature.children:
Expand Down
14 changes: 7 additions & 7 deletions cfmtoolbox/plugins/featureide_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,15 +85,15 @@ def parse_feature(feature_element: Element, parent: Feature | None) -> Feature:
return feature


def parse_root(root_element: Element) -> list[Feature]:
def parse_root(root_element: Element) -> tuple[Feature, list[Feature]]:
root = parse_feature(root_element, parent=None)

features = [root]

for feature in features:
features.extend(feature.children)

return features
return root, features


def parse_formula_value_and_feature(
Expand Down Expand Up @@ -173,17 +173,17 @@ def parse_constraints(
return (require_constraints, exclude_constraints, eliminated_constraints)


def parse_cfm(root: Element) -> CFM:
struct = root.find("struct")
def parse_cfm(root_element: Element) -> CFM:
struct = root_element.find("struct")

if struct is None:
raise TypeError("No valid Feature structure found in XML file")

root_struct = struct[0]
features = parse_root(root_struct)
root_feature, all_features = parse_root(root_struct)

require_constraints, exclude_constraints, eliminated_constraints = (
parse_constraints(root.find("constraints"), features)
parse_constraints(root_element.find("constraints"), all_features)
)

formatted_eliminated_constraints = [
Expand All @@ -198,7 +198,7 @@ def parse_cfm(root: Element) -> CFM:
file=sys.stderr,
)

return CFM(features, require_constraints, exclude_constraints)
return CFM(root_feature, require_constraints, exclude_constraints)


@app.importer(".xml")
Expand Down
4 changes: 1 addition & 3 deletions cfmtoolbox/plugins/json_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@

@app.exporter(".json")
def export_json(cfm: CFM) -> bytes:
root_feature = cfm.features[0]

serialized_root = serialize_feature(root_feature)
serialized_root = serialize_feature(cfm.root)
serialized_constraints = list(
map(serialize_constraint, [*cfm.require_constraints, *cfm.exclude_constraints])
)
Expand Down
8 changes: 4 additions & 4 deletions cfmtoolbox/plugins/json_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ def parse_cfm(serialized_cfm: JSON) -> CFM:
f"CFM constraints must be a list: {serialized_cfm['constraints']}"
)

features = parse_root(serialized_cfm["root"])
root, features = parse_root(serialized_cfm["root"])

constraints = [
parse_constraint(serialized_constraint, features)
Expand All @@ -31,21 +31,21 @@ def parse_cfm(serialized_cfm: JSON) -> CFM:
exclude_constraints = list(filter(lambda c: not c.require, constraints))

return CFM(
features=features,
root=root,
require_constraints=require_constraints,
exclude_constraints=exclude_constraints,
)


def parse_root(serialized_root: JSON) -> list[Feature]:
def parse_root(serialized_root: JSON) -> tuple[Feature, list[Feature]]:
root = parse_feature(serialized_root, parent=None)

features = [root]

for feature in features:
features.extend(feature.children)

return features
return root, features


def parse_feature(serialized_feature: JSON, /, parent: Feature | None) -> Feature:
Expand Down
6 changes: 3 additions & 3 deletions cfmtoolbox/plugins/one_wise_sampling.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ def __init__(self, model: CFM):
self.model = model

def one_wise_sampling(self) -> list[FeatureNode]:
self.calculate_border_assignments(self.model.features[0])
self.calculate_border_assignments(self.model.root)

samples = []

Expand All @@ -70,9 +70,9 @@ def generate_valid_sample(self):
while True:
self.global_feature_count = defaultdict(int)
self.covered_assignments = set()
self.covered_assignments.add((self.model.features[0].name, 1))
self.covered_assignments.add((self.model.root.name, 1))
random_feature_node = self.generate_random_feature_node_with_assignment(
self.model.features[0]
self.model.root
)
if (
random_feature_node.validate(self.model)
Expand Down
4 changes: 1 addition & 3 deletions cfmtoolbox/plugins/random_sampling.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,7 @@ def __init__(self, model: CFM):
def random_sampling(self) -> FeatureNode:
while True:
self.global_feature_count = defaultdict(int)
random_feature_node = self.generate_random_feature_node(
self.model.features[0]
)
random_feature_node = self.generate_random_feature_node(self.model.root)
if random_feature_node.validate(self.model):
break

Expand Down
6 changes: 2 additions & 4 deletions cfmtoolbox/plugins/uvl_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,13 +139,11 @@ def serialize_all_constraints(

@app.exporter(".uvl")
def export_uvl(cfm: CFM) -> bytes:
root_feature = cfm.features[0]

includes = serialize_includes()
root = serialize_root_feature(root_feature)
root = serialize_root_feature(cfm.root)

feature_strings = [
indent(serialize_features(child), "\t\t\t") for child in root_feature.children
indent(serialize_features(child), "\t\t\t") for child in cfm.root.children
]

features = "".join(feature_strings) + "\n"
Expand Down
42 changes: 27 additions & 15 deletions cfmtoolbox/plugins/uvl_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,13 @@ class ConstraintType(Enum):


class CustomListener(UVLPythonListener):
def __init__(self, cfm: CFM):
self.cfm = cfm
def __init__(
self,
imported_features: list[Feature],
imported_require_constraints: list[Constraint],
):
self.imported_features = imported_features
self.imported_require_constraints = imported_require_constraints
self.references: list[str] = []
self.feature_cardinalities: list[Cardinality] = []
self.features: list[Feature] = []
Expand Down Expand Up @@ -141,7 +146,7 @@ def exitFeature(self, ctx: UVLPythonParser.FeatureContext):
)
self.feature_map[name] = feature
self.features.append(feature)
self.cfm.features.append(feature)
self.imported_features.append(feature)
if len(self.group_features_count) > 0:
self.group_features_count[-1] += 1
else:
Expand Down Expand Up @@ -204,7 +209,7 @@ def exitFeature(self, ctx: UVLPythonParser.FeatureContext):
features,
)
parent_feature.children.append(feature)
self.cfm.require_constraints.append(
self.imported_require_constraints.append(
Constraint(
True,
parent_feature,
Expand All @@ -230,15 +235,17 @@ def exitFeature(self, ctx: UVLPythonParser.FeatureContext):
[
Interval(
min_cardinality,
max_cardinality
if max_cardinality is not None and max_cardinality > 0
else None,
(
max_cardinality
if max_cardinality is not None and max_cardinality > 0
else None
),
)
]
)
self.feature_map[name] = parent_feature
self.features.append(parent_feature)
self.cfm.features.append(parent_feature)
self.imported_features.append(parent_feature)
self.groups = self.groups[:-new_groups]
if len(self.group_features_count) > 0:
self.group_features_count[-1] += 1
Expand Down Expand Up @@ -282,7 +289,7 @@ def exitFeature(self, ctx: UVLPythonParser.FeatureContext):
child.parent = feature
self.feature_map[name] = feature
self.features.append(feature)
self.cfm.features.append(feature)
self.imported_features.append(feature)
if len(self.group_features_count) > 0:
self.group_features_count[-1] += 1

Expand Down Expand Up @@ -313,7 +320,7 @@ def exitConstraintLine(self, ctx: UVLPythonParser.ConstraintLineContext):
op = self.constraint_types.pop()

if op == ConstraintType.IMPLICATION:
self.cfm.require_constraints.append(
self.imported_require_constraints.append(
Constraint(
True,
self.feature_map[ref_1],
Expand All @@ -323,7 +330,7 @@ def exitConstraintLine(self, ctx: UVLPythonParser.ConstraintLineContext):
)
)
elif op == ConstraintType.EQUIVALENCE:
self.cfm.require_constraints.append(
self.imported_require_constraints.append(
Constraint(
True,
self.feature_map[ref_1],
Expand All @@ -332,7 +339,7 @@ def exitConstraintLine(self, ctx: UVLPythonParser.ConstraintLineContext):
Cardinality([Interval(1, None)]),
)
)
self.cfm.require_constraints.append(
self.imported_require_constraints.append(
Constraint(
True,
self.feature_map[ref_2],
Expand Down Expand Up @@ -380,12 +387,17 @@ def import_uvl(data: bytes):
token_stream = CommonTokenStream(lexer)
parser = UVLPythonParser(token_stream)

cfm = CFM([], [], [])
imported_features: list[Feature] = []
imported_require_constraints: list[Constraint] = []

listener = CustomListener(cfm)
listener = CustomListener(imported_features, imported_require_constraints)
parser.removeErrorListeners()
parser.addErrorListener(CustomErrorListener())
parser.addParseListener(listener)
parser.featureModel() # start parsing

return cfm
return CFM(
root=imported_features[0],
require_constraints=imported_require_constraints,
exclude_constraints=[],
)
2 changes: 1 addition & 1 deletion tests/plugins/test_big_m.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def test_apply_big_m_with_loaded_model(model: CFM):


def test_get_global_upper_bound(model: CFM):
feature = model.features[0]
feature = model.root
assert big_m.get_global_upper_bound(feature) == 12


Expand Down
2 changes: 1 addition & 1 deletion tests/plugins/test_debugging.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ def test_stringify_cfm():
)

cfm = CFM(
[feature],
feature,
[Constraint(True, feature, Cardinality([]), feature, Cardinality([]))],
[],
)
Expand Down
5 changes: 3 additions & 2 deletions tests/plugins/test_featureide_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def test_featureide_import():
path = Path("tests/data/sandwich.xml")
cfm = import_featureide(path.read_bytes())
assert len(cfm.features) == 11
assert cfm.features[0].name == "Sandwich"
assert cfm.root.name == "Sandwich"


@pytest.mark.parametrize(
Expand Down Expand Up @@ -205,8 +205,9 @@ def test_parse_root():
assert struct is not None

root_struct = struct[0]
feature_list = parse_root(root_struct)
root_feature, feature_list = parse_root(root_struct)

assert root_feature.name == "Sandwich"
assert len(feature_list) == 11
assert feature_list[0].name == "Sandwich"
assert feature_list[1].name == "Bread"
Expand Down
2 changes: 1 addition & 1 deletion tests/plugins/test_json_export.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def test_export_json():
)

cfm = CFM(
features=[root],
root=root,
require_constraints=[],
exclude_constraints=[],
)
Expand Down
5 changes: 3 additions & 2 deletions tests/plugins/test_json_import.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ def test_import_json():
path = Path("tests/data/sandwich.json")
cfm = json_import.import_json(path.read_bytes())
assert len(cfm.features) == 12
assert cfm.features[0].name == "sandwich"
assert cfm.root.name == "sandwich"


@pytest.mark.parametrize(
Expand Down Expand Up @@ -119,7 +119,7 @@ def test_parse_cfm_parses_root_and_constraints():


def test_parse_root_returns_all_features_in_the_tree():
features = json_import.parse_root(
root, features = json_import.parse_root(
{
"name": "sandwich",
"instance_cardinality": {"intervals": []},
Expand Down Expand Up @@ -159,6 +159,7 @@ def test_parse_root_returns_all_features_in_the_tree():
}
)

assert root.name == "sandwich"
assert len(features) == 5
assert features[0].name == "sandwich"
assert features[1].name == "meat"
Expand Down
Loading
Loading