Skip to content

Commit

Permalink
Store features as tree
Browse files Browse the repository at this point in the history
  • Loading branch information
DoctorJohn committed Sep 15, 2024
1 parent 6bb6ba7 commit ba88d80
Show file tree
Hide file tree
Showing 17 changed files with 142 additions and 120 deletions.
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,
exported_features: list[Feature],
exported_require_constraints: list[Constraint],
):
self.exported_features = exported_features
self.exported_require_constraints = exported_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.exported_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.exported_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.exported_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.exported_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.exported_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.exported_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.exported_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([], [], [])
exported_features: list[Feature] = []
exported_require_constraints: list[Constraint] = []

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

return cfm
return CFM(
root=exported_features[0],
require_constraints=exported_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

0 comments on commit ba88d80

Please sign in to comment.