diff --git a/src/main/java/de/featjar/feature/model/FeatureTree.java b/src/main/java/de/featjar/feature/model/FeatureTree.java index 889c05c2..50e43be6 100644 --- a/src/main/java/de/featjar/feature/model/FeatureTree.java +++ b/src/main/java/de/featjar/feature/model/FeatureTree.java @@ -134,6 +134,7 @@ protected FeatureTree(FeatureTree otherFeatureTree) { feature = otherFeatureTree.feature; parentGroupID = otherFeatureTree.parentGroupID; cardinality = otherFeatureTree.cardinality.clone(); + childrenGroups = new ArrayList<>(otherFeatureTree.childrenGroups.size()); otherFeatureTree.childrenGroups.stream().map(Group::clone).forEach(childrenGroups::add); attributeValues = otherFeatureTree.cloneAttributes(); } diff --git a/src/main/java/de/featjar/feature/model/Features.java b/src/main/java/de/featjar/feature/model/Features.java new file mode 100644 index 00000000..ad0693d8 --- /dev/null +++ b/src/main/java/de/featjar/feature/model/Features.java @@ -0,0 +1,36 @@ +package de.featjar.feature.model; + +import de.featjar.formula.structure.Expressions; +import de.featjar.formula.structure.IFormula; +import de.featjar.formula.structure.predicate.NotEquals; +import de.featjar.formula.structure.term.value.Constant; +import de.featjar.formula.structure.term.value.Variable; + +/** + * Defines useful methods to wrap a bool or numeric feature into a IFormula: + * bool: {@link de.featjar.formula.structure.predicate.Literal} + * int: {@link NotEquals 0} + * float: {@link NotEquals 0} + * + * Numeric features are therefore selected, if there value is not 0. + * + * @author Jonas Hanke + */ +public class Features { + + public static IFormula createFeatureFormel(IFeature feature) { + return createFeatureFormel(feature, feature.getName().orElse("")); + } + + public static IFormula createFeatureFormel(IFeature feature, String featureName) { + if (feature.getType().equals(Boolean.class)) { + return Expressions.literal(featureName); + } else if (feature.getType().equals(Integer.class)) { + return new NotEquals(new Variable(featureName, feature.getType()), new Constant(0)); + } else if(feature.getType().equals(Float.class)) { + return new NotEquals(new Variable(featureName, feature.getType()), new Constant(0.0f)); + } else { + throw new UnsupportedOperationException("Unsupported feature type: " + feature.getType()); + } + } +} diff --git a/src/main/java/de/featjar/feature/model/io/tikz/TikzGraphicalFeatureModelFormat.java b/src/main/java/de/featjar/feature/model/io/tikz/TikzGraphicalFeatureModelFormat.java new file mode 100644 index 00000000..f53f53c3 --- /dev/null +++ b/src/main/java/de/featjar/feature/model/io/tikz/TikzGraphicalFeatureModelFormat.java @@ -0,0 +1,81 @@ +package de.featjar.feature.model.io.tikz; + +import de.featjar.base.data.Problem; +import de.featjar.base.data.Result; +import de.featjar.base.io.format.IFormat; +import de.featjar.feature.model.IFeatureModel; +import de.featjar.feature.model.IFeatureTree; +import de.featjar.feature.model.io.tikz.format.TikzHeadFormat; +import de.featjar.feature.model.io.tikz.format.TikzMainFormat; +import de.featjar.feature.model.io.tikz.helper.TikzAttributeHelper; + +import java.util.ArrayList; +import java.util.List; + +/** + * This class is moved from FeatureIDE to FeatJAR. The former class was written by Simon Wenk and Yang Liu. + * We did some changes, moved in different classes, and we rewrote some code and added new functions. + * + * @author Felix Behme + * @author Lara Merza + * @author Jonas Hanke + */ +public class TikzGraphicalFeatureModelFormat implements IFormat { + + public static String LINE_SEPERATOR = System.lineSeparator(); + + private TikzAttributeHelper.FilterType filterType = TikzAttributeHelper.FilterType.WITH_OUT; // default + private List filterValues = new ArrayList<>(); // default + + public TikzGraphicalFeatureModelFormat() {} // default + + public TikzGraphicalFeatureModelFormat(TikzAttributeHelper.FilterType filterType, List filterValues) { + this.filterType = filterType; + this.filterValues = filterValues; + } + + @Override + public Result serialize(IFeatureModel featureModel) { + StringBuilder stringBuilder = new StringBuilder(); + List problemList = new ArrayList<>(); + + stringBuilder.append("\\documentclass[border=5pt]{standalone}").append(LINE_SEPERATOR); + TikzHeadFormat.header(stringBuilder, problemList, false); + + stringBuilder + .append("\\begin{document}").append(LINE_SEPERATOR) + .append(" %---The Feature Diagram-----------------------------------------------------").append(LINE_SEPERATOR); + for (IFeatureTree featureTree : featureModel.getRoots()) { + new TikzMainFormat(featureModel, featureTree, stringBuilder, filterType, filterValues).printForest(); + } + stringBuilder + .append(LINE_SEPERATOR) + .append("\t%---------------------------------------------------------------------------").append(LINE_SEPERATOR) + .append("\\end{document}"); + + return Result.of(stringBuilder.toString(), problemList); + } + + public void setFilterType(TikzAttributeHelper.FilterType filterType) { + this.filterType = filterType; + } + + public void setFilterValues(List filterValues) { + this.filterValues = filterValues; + } + + @Override + public boolean supportsWrite() { + return true; + } + + @Override + public String getFileExtension() { + return ".tex"; + } + + @Override + public String getName() { + return "LaTeX-Document with TikZ"; + } +} \ No newline at end of file diff --git a/src/main/java/de/featjar/feature/model/io/tikz/color/TikzFeatureColor.java b/src/main/java/de/featjar/feature/model/io/tikz/color/TikzFeatureColor.java new file mode 100644 index 00000000..304ad1c7 --- /dev/null +++ b/src/main/java/de/featjar/feature/model/io/tikz/color/TikzFeatureColor.java @@ -0,0 +1,42 @@ +package de.featjar.feature.model.io.tikz.color; + +/** + * Color in tikz for later implemntation (colors doesnt exists in features in the moment) + * + * @author Felix Behme + * @author Lara Merza + * @author Jonas Hanke + */ +public enum TikzFeatureColor { + + RED("redColor"), + ORANGE("orangeColor"), + YELLOW("yellowColor"), + DARK_GREEN("darkGreenColor"), + LIGHT_GREEN("lightGreenColor"), + CYAN("cyanColor"), + LIGHT_GRAY("lightGrayColor"), + BLUE("blueColor"), + MAGENTA("magentaColor"), + PINK("pinkColor"), + NO_COLOR(""); + + public static String color(String tikzFeatureColor) { + for (TikzFeatureColor tikzFeatureColors : values()) { + if (tikzFeatureColors.getColor().equalsIgnoreCase(tikzFeatureColor)) { + return tikzFeatureColors.getColor(); + } + } + return NO_COLOR.getColor(); + } + + final String color; + + TikzFeatureColor(String color) { + this.color = color; + } + + public String getColor() { + return color; + } +} diff --git a/src/main/java/de/featjar/feature/model/io/tikz/format/TikzHeadFormat.java b/src/main/java/de/featjar/feature/model/io/tikz/format/TikzHeadFormat.java new file mode 100644 index 00000000..02fc8dbf --- /dev/null +++ b/src/main/java/de/featjar/feature/model/io/tikz/format/TikzHeadFormat.java @@ -0,0 +1,49 @@ +package de.featjar.feature.model.io.tikz.format; + +import de.featjar.base.data.Problem; +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.util.List; + +/** + * This class prints the header of a LaTex document containing the Tikz definitions. + * + * @author Felix Behme + * @author Lara Merza + * @author Jonas Hanke + */ +public class TikzHeadFormat { + + public static void header(StringBuilder stringBuilder, List problemList, boolean hasVerticalLayout) { + String replacement = String.format( // + " parent anchor = %s," + System.lineSeparator() // + + " child anchor = %s," + System.lineSeparator() // + + "%s" // + + " l sep = 2em," + System.lineSeparator() // + + " s sep = 1em," // + + "%s", // + hasVerticalLayout ? "east" : "south", // + hasVerticalLayout ? "west" : "north", // + hasVerticalLayout ? " grow' = east," + System.lineSeparator() : "", // + hasVerticalLayout ? " tier/.pgfmath=level()," : ""); + + InputStream inputStream = TikzHeadFormat.class.getClassLoader().getResourceAsStream("head.tex"); + + if (inputStream == null) { + problemList.add(new Problem("InputStream in header is null / not found")); + return; + } + + try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, StandardCharsets.UTF_8))) { + String line; + while (((line = bufferedReader.readLine()) != null)) { + if (line.contains("{replaceWithVerticalSetting}")) { + line = replacement; + } + stringBuilder.append(line).append("\n"); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/main/java/de/featjar/feature/model/io/tikz/format/TikzMainFormat.java b/src/main/java/de/featjar/feature/model/io/tikz/format/TikzMainFormat.java new file mode 100644 index 00000000..efb9bf33 --- /dev/null +++ b/src/main/java/de/featjar/feature/model/io/tikz/format/TikzMainFormat.java @@ -0,0 +1,126 @@ +package de.featjar.feature.model.io.tikz.format; + +import de.featjar.base.tree.Trees; +import de.featjar.feature.model.IConstraint; +import de.featjar.feature.model.IFeatureModel; +import de.featjar.feature.model.IFeatureTree; +import de.featjar.feature.model.io.tikz.TikzGraphicalFeatureModelFormat; +import de.featjar.feature.model.io.tikz.helper.TikzAttributeHelper; +import de.featjar.feature.model.io.tikz.helper.TikzMatrixHelper; +import de.featjar.feature.model.io.tikz.helper.TikzMatrixType; +import de.featjar.feature.model.io.tikz.helper.PrintVisitor; +import de.featjar.formula.io.textual.ExpressionSerializer; +import de.featjar.formula.io.textual.LaTexSymbols; + +import java.util.List; + +/** + * This class generates the Tikz representation of a {@link IFeatureModel} including all constraints ({@link IConstraint}). + * + * @author Felix Behme + * @author Lara Merza + * @author Jonas Hanke + */ +public class TikzMainFormat { + + private final IFeatureModel featureModel; + private final IFeatureTree featureTree; + private final StringBuilder stringBuilder; + private final TikzAttributeHelper.FilterType filterType; + private final List filterValues; + + public TikzMainFormat(IFeatureModel featureModel , IFeatureTree featureTree, StringBuilder stringBuilder, TikzAttributeHelper.FilterType filterType, List filterValues) { + this.featureModel = featureModel; + this.featureTree = featureTree; + this.stringBuilder = stringBuilder; + this.filterType = filterType; + this.filterValues = filterValues; + } + + /** + * Build the complete tree of the FeatureModel. + */ + public void printForest() { + stringBuilder + .append("\\begin{forest}").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR) + .append("\tfeatureDiagram").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR).append("\t"); + + PrintVisitor printVisitor = new PrintVisitor(filterType, filterValues); + Trees.traverse(featureTree, printVisitor); + stringBuilder.append(printVisitor.getResult().get()); + + postProcessing(); + stringBuilder.append("\t").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR); + if (!featureTree.getFeature().isHidden()) { + printLegend(); + } + if(!featureModel.getConstraints().isEmpty()) { + printConstraints(); + } + stringBuilder.append("\\end{forest}").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR); + } + + /** + * Processes a String to make special symbols LaTeX compatible. + */ + private void postProcessing() { + stringBuilder.replace(0, stringBuilder.length(), stringBuilder.toString().replace("_", "\\_")); + } + + private void printLegend() { + TikzMatrixHelper tikzMatrixHelper = new TikzMatrixHelper(TikzMatrixType.LEGEND); + + if (stringBuilder.indexOf(",abstract") != -1 && stringBuilder.indexOf(",concrete") != -1) { + tikzMatrixHelper.writeNode("[abstract,label=right:Abstract Feature] {}"); + tikzMatrixHelper.writeNode("[concrete,label=right:Concrete Feature] {}"); + } else if (stringBuilder.indexOf(",abstract") != -1) { + tikzMatrixHelper.writeNode("[abstract,label=right:Feature] {}"); + } else if (stringBuilder.indexOf(",concrete") != -1) { + tikzMatrixHelper.writeNode("[concrete,label=right:Feature] {}"); + } + + if (stringBuilder.indexOf(",mandatory") != -1) { + tikzMatrixHelper.writeNode("[mandatory,label=right:Mandatory] {}"); + } + + if (stringBuilder.indexOf(",optional") != -1) { + tikzMatrixHelper.writeNode("[optional,label=right:Optional] {}"); + } + + if (stringBuilder.indexOf(",or") != -1) { + tikzMatrixHelper + .writeFillDraw("(0.1,0) - +(-0,-0.2) - +(0.2,-0.2)- +(0.1,0)") + .writeDraw("(0.1,0) -- +(-0.2, -0.4)") + .writeDraw("(0.1,0) -- +(0.2,-0.4)") + .writeFill("(0,-0.2) arc (240:300:0.2)") + .writeNode("[label=right:Or Group] {}"); + } + + if (stringBuilder.indexOf(",alternative") != -1) { + tikzMatrixHelper + .writeDraw("(0.1,0) -- +(-0.2, -0.4)") + .writeDraw("(0.1,0) -- +(0.2,-0.4)") + .writeDraw("(0,-0.2) arc (240:300:0.2)") + .writeNode("[label=right:Alternative Group] {}"); + } + + stringBuilder.append(tikzMatrixHelper.build()); + } + + private void printConstraints() { + ExpressionSerializer expressionSerializer = new ExpressionSerializer(); + expressionSerializer.setEnquoteAlways(true); + expressionSerializer.setSymbols(LaTexSymbols.INSTANCE); + + stringBuilder.append(" \\matrix [below=1mm of current bounding box] {").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR); + for (IConstraint constraint : featureModel.getConstraints()) { + String text = constraint.getFormula().traverse(expressionSerializer).get(); + text = text.replaceAll("\"([\\w\" ]+)\"", " \\\\text\\{$1\\} "); // wrap all words in \text{} // replace with $2 + text = text.replaceAll("\\s+", " "); // remove unnecessary whitespace characters + stringBuilder.append(" \\node {\\(").append(text).append("\\)}; \\\\").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR); + + expressionSerializer.reset(); + } + stringBuilder.append(" };").append(TikzGraphicalFeatureModelFormat.LINE_SEPERATOR); + } +} \ No newline at end of file diff --git a/src/main/java/de/featjar/feature/model/io/tikz/helper/PrintVisitor.java b/src/main/java/de/featjar/feature/model/io/tikz/helper/PrintVisitor.java new file mode 100644 index 00000000..72496063 --- /dev/null +++ b/src/main/java/de/featjar/feature/model/io/tikz/helper/PrintVisitor.java @@ -0,0 +1,123 @@ +package de.featjar.feature.model.io.tikz.helper; + +import de.featjar.base.FeatJAR; +import de.featjar.base.data.Result; +import de.featjar.base.tree.visitor.ITreeVisitor; +import de.featjar.feature.model.FeatureTree; +import de.featjar.feature.model.IFeature; +import de.featjar.feature.model.IFeatureTree; +import java.util.ArrayList; +import java.util.List; + +/** + * This class travers a given {@link IFeatureTree} and generates the Tikz representation of the tree. + * + * @author Felix Behme + * @author Lara Merza + * @author Jonas Hanke + */ +public class PrintVisitor implements ITreeVisitor { + + private final StringBuilder stringBuilder; + + private final TikzAttributeHelper.FilterType filterType; + private final List filterValues; + + public PrintVisitor() { + this.stringBuilder = new StringBuilder(); + this.filterType = TikzAttributeHelper.FilterType.WITH_OUT; // default + this.filterValues = new ArrayList<>(); + } + + public PrintVisitor(TikzAttributeHelper.FilterType filterType, List filterValues) { + this.stringBuilder = new StringBuilder(); + this.filterType = filterType; + this.filterValues = filterValues; + } + + @Override + public TraversalAction firstVisit(List path) { + IFeature feature = ITreeVisitor.getCurrentNode(path).getFeature(); + + new TikzAttributeHelper(feature, stringBuilder) + .addFilterValue(filterValues) + .setFilterType(filterType) + .build(); + insertFeatureType(feature); + insertFeatureCardinality(feature); + insertGroupCardinality(feature); + + return TraversalAction.CONTINUE; + } + + @Override + public TraversalAction lastVisit(List path) { + stringBuilder.append("]"); + return TraversalAction.CONTINUE; + } + + @Override + public Result getResult() { + return Result.of(stringBuilder.toString()); + } + + private void insertFeatureType(IFeature feature) { + if (feature.isAbstract()) { + stringBuilder.append(",abstract"); + } + + if (feature.isConcrete()) { + stringBuilder.append(",concrete"); + } + } + + private void insertFeatureCardinality(IFeature feature) { + IFeatureTree featureTree = feature.getFeatureTree().orElse(null); + FeatureTree.Group featureTreeParentGroup = feature.getFeatureTree().get().getParentGroup().orElse(null); + + if (isNotRootFeature(feature) && featureTreeParentGroup != null && featureTreeParentGroup.isAnd()) { + if (featureTree.getFeatureCardinalityLowerBound() == 0 && + featureTree.getFeatureCardinalityUpperBound() == 1) { + stringBuilder.append(",optional"); + } else if(featureTree.getFeatureCardinalityLowerBound() == 1 && + featureTree.getFeatureCardinalityUpperBound() == 1) { + stringBuilder.append(",mandatory"); + } else { + stringBuilder.append(String.format(",featurecardinality={%d}{%d}", + feature.getFeatureTree().get().getFeatureCardinalityLowerBound(), + feature.getFeatureTree().get().getFeatureCardinalityUpperBound())); + } + } + } + + private void insertGroupCardinality(IFeature feature) { + IFeatureTree featureTree = feature.getFeatureTree().orElse(null); + + if (isNotRootFeature(feature)) { + int previousChildrenCount = 1; + for(int i = 0; i < featureTree.getChildrenGroups().size(); i++) { + if(featureTree.getChildrenGroup(i).isPresent()) { + FeatureTree.Group group = featureTree.getChildrenGroup(i).get(); + + int childrenCount = featureTree.getChildren(i).size(); + if(group.isOr()) { + stringBuilder.append(String.format(",or={%d}{%d}{%d}", previousChildrenCount, previousChildrenCount + childrenCount - 1, + (2 * previousChildrenCount + childrenCount - 1) / 2)); + } else if(group.isAlternative()) { + stringBuilder.append(String.format(",alternative={%d}{%d}{%d}", previousChildrenCount, previousChildrenCount + childrenCount - 1, + (2 * previousChildrenCount + childrenCount - 1) / 2)); + } else if(group.isCardinalityGroup()) { + stringBuilder.append(String.format(",groupcardinality={%d}{%d}{%d}{%d}{%d}", previousChildrenCount, previousChildrenCount + childrenCount - 1, + (2 * previousChildrenCount + childrenCount - 1) / 2, group.getLowerBound(), group.getUpperBound())); + } + + previousChildrenCount += childrenCount; + } + } + } + } + + private boolean isNotRootFeature(IFeature feature) { + return !feature.getFeatureModel().getRootFeatures().contains(feature); + } +} \ No newline at end of file diff --git a/src/main/java/de/featjar/feature/model/io/tikz/helper/TikzAttributeHelper.java b/src/main/java/de/featjar/feature/model/io/tikz/helper/TikzAttributeHelper.java new file mode 100644 index 00000000..67cd86a1 --- /dev/null +++ b/src/main/java/de/featjar/feature/model/io/tikz/helper/TikzAttributeHelper.java @@ -0,0 +1,152 @@ +package de.featjar.feature.model.io.tikz.helper; + +import de.featjar.base.data.IAttribute; +import de.featjar.feature.model.IFeature; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * This class allows you to display the attributes of a feature. You can also filter specific names to display + * in the tikz file. + * + * @author Felix Behme + * @author Lara Merza + * @author Jonas Hanke + */ +public class TikzAttributeHelper { + + private final String MULTICOLUMN = "\\multicolumn{2}{c}{{0}} \\\\\\hline" + System.lineSeparator(); + private final String VALUE = "\\small\\texttt{{0} ({1})} &\\small\\texttt{= {2}} \\\\" + System.lineSeparator(); + + private final IFeature feature; + private final StringBuilder stringBuilder; + + private List filter; + private FilterType filterType = FilterType.WITH_OUT; // FALLBACK + + public TikzAttributeHelper(IFeature feature, StringBuilder stringBuilder) { + this.feature = feature; + this.stringBuilder = stringBuilder; + this.filter = new ArrayList<>(); + } + + public TikzAttributeHelper(IFeature feature, StringBuilder stringBuilder, List filter) { + this.feature = feature; + this.stringBuilder = stringBuilder; + this.filter = filter; + } + + /** + * Set a filter type to customize your output for the attributes + * @param filterType (DISPLAY or WITH_OUT) + * DISPLAY: Shows every value in the filter + * WITH_OUT: Shows every value that isn't in the filter + * @return this class + */ + public TikzAttributeHelper setFilterType(FilterType filterType) { + this.filterType = filterType; + return this; + } + + /** + * + * @param values (the keys, words or whatever that will be filtered in the running process. + * @return this + */ + public TikzAttributeHelper addFilterValue(List values) { + filter.addAll(values); + return this; + } + + private void writeAttributes(IFeature feature) { + StringBuilder stringBuilderInternal = new StringBuilder(); + String featureName = feature.getName().orElse(""); + + Map, Object> iAttributeObjectMap = feature.getAttributes().orElse(null); + // check: if the attribute map is empty or null + if (iAttributeObjectMap == null || iAttributeObjectMap.isEmpty() || countAttributeToDisplay(iAttributeObjectMap) == 0) { + stringBuilder.append("[").append(featureName); // add the name without multicolumn + return; + } + stringBuilder.append("[").append(replace(MULTICOLUMN, featureName)); + + iAttributeObjectMap.forEach((iAttribute, object) -> { + writeAttributes(iAttribute, object, stringBuilderInternal); + }); + + stringBuilder.append(stringBuilderInternal); + stringBuilder.append(",align=ll"); + } + + private void writeAttributes(IAttribute attribute, Object object, StringBuilder stringBuilder) { + // filter with type the current boolean value for the running process + if (filterWithType(attribute.getName().toUpperCase())) { + return; // ignore attribute + } + stringBuilder.append(replace(VALUE, attribute.getName(), attribute.getType().getSimpleName(), object)); + replace(VALUE, 12, 2); + } + + /** + * Count all items to display in the feature for more functions and more beauty + * + * @param values (Map of the feature with his attributes) + * @return count if items to diplay + */ + private int countAttributeToDisplay(Map, Object> values) { + int size = values.size(); + for (IAttribute attribute : values.keySet()) { + if (filterWithType(attribute.getName().toUpperCase())) { + size-=1; + } + } + return size; + } + + private String replace(String key, Object... values) { + String result = key; + for (short i = 0; i < values.length; i++) { + // Synatx: Hello my name is {0} and I am from {1} -> Hello my name is FeatJar, and I am from Germany + result = result.replace("{" + i + "}", values[i].toString()); + } + return result; + } + + private void makeListUpperCase() { + List upperFilter = new ArrayList<>(); + filter.forEach(value -> { + // allows filtering with contains and no streams. + upperFilter.add(value.toUpperCase()); + }); + filter = upperFilter; + } + + private boolean filterWithType(String key) { + if (filter.isEmpty()) { + return false; + } + if (filterType == FilterType.DISPLAY) { + return !filter.contains(key); + } + if (filterType == FilterType.WITH_OUT) { + return filter.contains(key); + } + + return false; + } + + /** + * Paste everything together in the string builder. + */ + public void build() { + makeListUpperCase(); + writeAttributes(feature); + } + + public enum FilterType { + DISPLAY, + WITH_OUT; + } + +} diff --git a/src/main/java/de/featjar/feature/model/io/tikz/helper/TikzMatrixHelper.java b/src/main/java/de/featjar/feature/model/io/tikz/helper/TikzMatrixHelper.java new file mode 100644 index 00000000..7edf33d6 --- /dev/null +++ b/src/main/java/de/featjar/feature/model/io/tikz/helper/TikzMatrixHelper.java @@ -0,0 +1,60 @@ +package de.featjar.feature.model.io.tikz.helper; + +/** + * This class helps to build a matrix in latex. Choose a header from MatrixStyle and write your nodes, draw or whatever. + * + * @author Felix Behme + * @author Lara Merza + * @author Jonas Hanke + */ +public class TikzMatrixHelper { + + private final static String NODE = " \\node {replace}; \\\\" + System.lineSeparator(); + private final static String DRAW = " \\draw[drawColor] {replace};" + System.lineSeparator(); + private final static String FILL_DRAW = " \\filldraw[drawColor] {replace}; " + System.lineSeparator(); + private final static String FILL = " \\fill[drawColor] {replace};" + System.lineSeparator(); + + private final StringBuilder stringBuilder; + private final String header ; + + public TikzMatrixHelper(TikzMatrixType tikzMatrixType) { + this.stringBuilder = new StringBuilder(); + this.header = tikzMatrixType.getHeader(); + writeHeader(); + } + + private void writeHeader() { + stringBuilder.append(header); + } + + private void writeFooter() { + stringBuilder.append(" };") + .append(System.lineSeparator()); + } + + public TikzMatrixHelper writeNode(String value) { + stringBuilder.append(NODE.replace("{replace}", value)); + return this; + } + + public TikzMatrixHelper writeDraw(String value) { + stringBuilder.append(DRAW.replace("{replace}", value)); + return this; + } + + public TikzMatrixHelper writeFillDraw(String value) { + stringBuilder.append(FILL_DRAW.replace("{replace}", value)); + return this; + } + + public TikzMatrixHelper writeFill(String value) { + stringBuilder.append(FILL.replace("{replace}", value)); + return this; + } + + public StringBuilder build() { + writeFooter(); + return stringBuilder; + } + +} diff --git a/src/main/java/de/featjar/feature/model/io/tikz/helper/TikzMatrixType.java b/src/main/java/de/featjar/feature/model/io/tikz/helper/TikzMatrixType.java new file mode 100644 index 00000000..9617a385 --- /dev/null +++ b/src/main/java/de/featjar/feature/model/io/tikz/helper/TikzMatrixType.java @@ -0,0 +1,25 @@ +package de.featjar.feature.model.io.tikz.helper; + +/** + * @author Felix Behme + * @author Lara Merza + * @author Jonas Hanke + */ +public enum TikzMatrixType { + + LEGEND(" \\matrix [anchor=north west] at (current bounding box.north east) {" + System.lineSeparator() + " \\node [placeholder] {}; \\\\" + System.lineSeparator() + + " };" + System.lineSeparator() + " \\matrix [draw=drawColor,anchor=north west] at (current bounding box.north east) {" + System.lineSeparator() + + " \\node [label=center:\\underline{Legend:}] {}; \\\\" + System.lineSeparator()), + CONSTRAINS(""), + ATTRIBUTES(""); + + final String header; + + TikzMatrixType(String header) { + this.header = header; + } + + public String getHeader() { + return header; + } +} diff --git a/src/main/java/de/featjar/feature/model/transformer/ComputeAttributeAggregate.java b/src/main/java/de/featjar/feature/model/transformer/ComputeAttributeAggregate.java new file mode 100644 index 00000000..773ce021 --- /dev/null +++ b/src/main/java/de/featjar/feature/model/transformer/ComputeAttributeAggregate.java @@ -0,0 +1,29 @@ +package de.featjar.feature.model.transformer; + +import de.featjar.base.computation.AComputation; +import de.featjar.base.computation.Progress; +import de.featjar.base.data.Result; +import de.featjar.formula.structure.IFormula; + +import java.util.List; + +public class ComputeAttributeAggregate extends AComputation { + + /*protected static final Dependency ATTRIBUTE_AGGREGATE = Dependency.newDependency(IAttributeAggregate.class); + protected static final Dependency> VARIABLES = Dependency.newDependency(List.class); + + public ComputeAttributeAggregate(IComputation attributeAggregate, + IComputation> variables, + IComputation> values) { + super(attributeAggregate, variables, values); + } + + protected ComputeAttributeAggregate(ComputeFormula other) { + super(other); + }*/ + + @Override + public Result compute(List dependencyList, Progress progress) { + return Result.empty(); + } +} diff --git a/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java b/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java index dcacf5bb..145317d9 100644 --- a/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java +++ b/src/main/java/de/featjar/feature/model/transformer/ComputeFormula.java @@ -21,16 +21,19 @@ package de.featjar.feature.model.transformer; import de.featjar.base.computation.AComputation; +import de.featjar.base.computation.Computations; import de.featjar.base.computation.Dependency; import de.featjar.base.computation.IComputation; import de.featjar.base.computation.Progress; +import de.featjar.base.data.Attribute; +import de.featjar.base.data.IAttribute; import de.featjar.base.data.Range; import de.featjar.base.data.Result; +import de.featjar.base.tree.Trees; import de.featjar.feature.model.FeatureTree.Group; -import de.featjar.feature.model.IFeature; +import de.featjar.feature.model.Features; import de.featjar.feature.model.IFeatureModel; import de.featjar.feature.model.IFeatureTree; -import de.featjar.formula.structure.Expressions; import de.featjar.formula.structure.IFormula; import de.featjar.formula.structure.connective.And; import de.featjar.formula.structure.connective.AtLeast; @@ -40,11 +43,8 @@ import de.featjar.formula.structure.connective.Implies; import de.featjar.formula.structure.connective.Or; import de.featjar.formula.structure.connective.Reference; -import de.featjar.formula.structure.predicate.Literal; import de.featjar.formula.structure.term.value.Variable; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; +import java.util.*; /** * Transforms a feature model into a boolean formula. @@ -53,9 +53,13 @@ */ public class ComputeFormula extends AComputation { protected static final Dependency FEATURE_MODEL = Dependency.newDependency(IFeatureModel.class); + protected static final Dependency SIMPLE_TRANSLATION = Dependency.newDependency(Boolean.class); + + static Attribute literalNameAttribute = new Attribute<>("literalName", String.class); + private Boolean hasCardinalityFeatures = Boolean.FALSE; public ComputeFormula(IComputation formula) { - super(formula); + super(formula, Computations.of(Boolean.FALSE)); } protected ComputeFormula(ComputeFormula other) { @@ -67,84 +71,220 @@ public Result compute(List dependencyList, Progress progress) IFeatureModel featureModel = FEATURE_MODEL.get(dependencyList); ArrayList constraints = new ArrayList<>(); HashSet variables = new HashSet<>(); - featureModel.getFeatureTreeStream().forEach(node -> { - // TODO use better error value - IFeature feature = node.getFeature(); - String featureName = feature.getName().orElse(""); - Variable variable = new Variable(featureName, feature.getType()); - variables.add(variable); + Map, Object>> attributes = new LinkedHashMap<>(); - // TODO take featureRanges into Account - Result potentialParentTree = node.getParent(); - Literal featureLiteral = Expressions.literal(featureName); - if (potentialParentTree.isEmpty()) { - handleRoot(constraints, featureLiteral, node); - } else { - handleParent(constraints, featureLiteral, node); - } - handleGroups(constraints, featureLiteral, node); + if (SIMPLE_TRANSLATION.get(dependencyList)) { + IFeatureTree iFeatureTree = featureModel.getRoots().get(0); + + ComputeSimpleFormulaVisitor simpleVisitor = + new ComputeSimpleFormulaVisitor(constraints, variables, attributes); + Trees.traverse(iFeatureTree, simpleVisitor); + + hasCardinalityFeatures = simpleVisitor.getHasCardinalityFeature(); + } else { + traverseFeatureModel(featureModel, constraints, variables, attributes); + } + + ReplaceAttributeAggregate replaceAttributeAggregate = + new ReplaceAttributeAggregate(attributes, hasCardinalityFeatures); + featureModel.getConstraints().forEach(constraint -> { + Trees.traverse(constraint.getFormula(), replaceAttributeAggregate); + + constraints.add(constraint.getFormula()); }); + Reference reference = new Reference(new And(constraints)); reference.setFreeVariables(variables); return Result.of(reference); } - private void handleParent(ArrayList constraints, Literal featureLiteral, IFeatureTree node) { - constraints.add(new Implies( - featureLiteral, - Expressions.literal( - node.getParent().get().getFeature().getName().orElse("")))); + private void traverseFeatureModel( + IFeatureModel featureModel, + ArrayList constraints, + HashSet variables, + Map, Object>> attributes) { + + for (IFeatureTree root : featureModel.getRoots()) { + + // collect the attributes of root + Variable variable = new Variable( + root.getFeature().getName().get(), root.getFeature().getType()); + variables.add(variable); + if (root.getFeature().getAttributes().isPresent()) { + attributes.put(Features.createFeatureFormel(root.getFeature()), root.getFeature().getAttributes().get()); + } + + IFormula rootFormula = Features.createFeatureFormel(root.getFeature()); + if (root.isMandatory()) { + constraints.add(rootFormula); + } + handleGroups(rootFormula, root, constraints); + + addChildConstraints(root, constraints, variables, attributes); + } + } + + private void addChildConstraints( + IFeatureTree node, + ArrayList constraints, + HashSet variables, + Map, Object>> attributes) { + + // collect the attributes of all features + // TODO: check if the variables need to be duplicated? + Variable variable = new Variable( + node.getFeature().getName().get(), node.getFeature().getType()); + variables.add(variable); + if (node.getFeature().getAttributes().isPresent()) { + attributes.put(Features.createFeatureFormel(node.getFeature()), node.getFeature().getAttributes().get()); + } + + IFormula parentFormula = Features.createFeatureFormel(node.getFeature(), getFormulaName(node)); + + for (IFeatureTree child : node.getChildren()) { + + if (isCardinalityFeature(child)) { + hasCardinalityFeatures = Boolean.TRUE; + + int upperBound = child.getFeatureCardinalityUpperBound(); + int lowerBound = child.getFeatureCardinalityLowerBound(); + + LinkedList constraintGroupFormulas = new LinkedList<>(); + + for (int i = 1; i <= upperBound; i++) { + + String formulaName = getFormulaName(child) + "_" + i; + if (cardinalityFeatureAbove(child)) { + formulaName += "." + getFormulaName(node); + } + + // clone only tree for traversal, not its children + IFeatureTree cardinalityClone = child.cloneTree(); + cardinalityClone.mutate().setAttributeValue(literalNameAttribute, formulaName); + + IFormula currentFormula = Features.createFeatureFormel(child.getFeature(), formulaName); + + // add all the constraints + // imply parent + constraints.add(new Implies(currentFormula, parentFormula)); + // implication chain part + if (i > 1) { + IFormula previousFormula = constraintGroupFormulas.getLast(); + constraints.add(new Implies(currentFormula, previousFormula)); + } + // group constraints + handleGroups(currentFormula, cardinalityClone, constraints); + + constraintGroupFormulas.add(currentFormula); + + addChildConstraints(cardinalityClone, constraints, variables, attributes); + } + // check if 0 and do not add implication + if (lowerBound != 0) + constraints.add(new Implies(parentFormula, new AtLeast(lowerBound, constraintGroupFormulas))); + + return; + } else { + + String formulaName = getFormulaName(child); + if (cardinalityFeatureAbove(child)) { + formulaName += "." + getFormulaName(node); + } + + IFormula childFeatureFormula = Features.createFeatureFormel(child.getFeature(), formulaName); + child.mutate().setAttributeValue(literalNameAttribute, formulaName); + + // add constraints + // always add parent implications (child implies parent) + constraints.add(new Implies(childFeatureFormula, parentFormula)); + + // handle group + handleGroups(childFeatureFormula, child, constraints); + + addChildConstraints(child, constraints, variables, attributes); + } + } + } + + private String getFormulaName(IFeatureTree node) { + String literalName = ""; + if (node.getAttributeValue(literalNameAttribute).isEmpty()) { + literalName = node.getFeature().getName().orElse(""); + } else { + literalName = node.getAttributeValue(literalNameAttribute).orElse(""); + } + return literalName; } - private void handleRoot(ArrayList constraints, Literal featureLiteral, IFeatureTree node) { - if (node.isMandatory()) { - constraints.add(featureLiteral); + private boolean cardinalityFeatureAbove(IFeatureTree child) { + + if (!child.getParent().isPresent()) return false; + + if (isCardinalityFeature(child.getParent().get())) { + return true; + } else { + return cardinalityFeatureAbove(child.getParent().get()); } } - private void handleGroups(ArrayList constraints, Literal featureLiteral, IFeatureTree node) { + private boolean isCardinalityFeature(IFeatureTree node) { + + if (node.getFeatureCardinalityUpperBound() > 1) { + return true; + } + return false; + } + + // private void handleGroups(ArrayList constraints, IFormula featureLiteral, IFeatureTree node) { + private void handleGroups(IFormula featureFormula, IFeatureTree node, ArrayList constraints) { List childrenGroups = node.getChildrenGroups(); int groupCount = childrenGroups.size(); - ArrayList> groupLiterals = new ArrayList<>(groupCount); + ArrayList> groupFormulas = new ArrayList<>(groupCount); + for (int i = 0; i < groupCount; i++) { - groupLiterals.add(null); + groupFormulas.add(null); } + List children = node.getChildren(); for (IFeatureTree childNode : children) { - Literal childLiteral = - Expressions.literal(childNode.getFeature().getName().orElse("")); + + String childFormulaName = getFormulaName(childNode); + if (childNode.getAttributeValue(literalNameAttribute).isEmpty() && cardinalityFeatureAbove(childNode)) + childFormulaName += "." + getFormulaName(node); + + IFormula childFormula = Features.createFeatureFormel(childNode.getFeature(), childFormulaName); if (childNode.isMandatory()) { - constraints.add(new Implies(featureLiteral, childLiteral)); + constraints.add(new Implies(featureFormula, childFormula)); } int groupID = childNode.getParentGroupID(); - List list = groupLiterals.get(groupID); + List list = groupFormulas.get(groupID); if (list == null) { - groupLiterals.set(groupID, list = new ArrayList<>()); + groupFormulas.set(groupID, list = new ArrayList<>()); } - list.add(childLiteral); + list.add(childFormula); } for (int i = 0; i < groupCount; i++) { Group group = childrenGroups.get(i); if (group != null) { if (group.isOr()) { - constraints.add(new Implies(featureLiteral, new Or(groupLiterals.get(i)))); + constraints.add(new Implies(featureFormula, new Or(groupFormulas.get(i)))); } else if (group.isAlternative()) { - constraints.add(new Implies(featureLiteral, new Choose(1, groupLiterals.get(i)))); + constraints.add(new Implies(featureFormula, new Choose(1, groupFormulas.get(i)))); } else { int lowerBound = group.getLowerBound(); int upperBound = group.getUpperBound(); if (lowerBound > 0) { if (upperBound != Range.OPEN) { constraints.add(new Implies( - featureLiteral, new Between(lowerBound, upperBound, groupLiterals.get(i)))); + featureFormula, new Between(lowerBound, upperBound, groupFormulas.get(i)))); } else { - constraints.add(new Implies(featureLiteral, new AtMost(upperBound, groupLiterals.get(i)))); + constraints.add(new Implies(featureFormula, new AtMost(upperBound, groupFormulas.get(i)))); } } else { if (upperBound != Range.OPEN) { - constraints.add(new Implies(featureLiteral, new AtLeast(lowerBound, groupLiterals.get(i)))); + constraints.add(new Implies(featureFormula, new AtLeast(lowerBound, groupFormulas.get(i)))); } } } diff --git a/src/main/java/de/featjar/feature/model/transformer/ComputeSimpleFormulaVisitor.java b/src/main/java/de/featjar/feature/model/transformer/ComputeSimpleFormulaVisitor.java new file mode 100644 index 00000000..d976558c --- /dev/null +++ b/src/main/java/de/featjar/feature/model/transformer/ComputeSimpleFormulaVisitor.java @@ -0,0 +1,225 @@ +/* + * Copyright (C) 2025 FeatJAR-Development-Team + * + * This file is part of FeatJAR-feature-model. + * + * feature-model is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, + * or (at your option) any later version. + * + * feature-model is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with feature-model. If not, see . + * + * See for further information. + */ +package de.featjar.feature.model.transformer; + +import de.featjar.base.FeatJAR; +import de.featjar.base.data.IAttribute; +import de.featjar.base.data.Range; +import de.featjar.base.data.Result; +import de.featjar.base.tree.visitor.ITreeVisitor; +import de.featjar.feature.model.FeatureTree.Group; +import de.featjar.feature.model.Features; +import de.featjar.feature.model.IFeature; +import de.featjar.feature.model.IFeatureTree; +import de.featjar.formula.structure.Expressions; +import de.featjar.formula.structure.IFormula; +import de.featjar.formula.structure.connective.AtLeast; +import de.featjar.formula.structure.connective.AtMost; +import de.featjar.formula.structure.connective.Between; +import de.featjar.formula.structure.connective.Choose; +import de.featjar.formula.structure.connective.Implies; +import de.featjar.formula.structure.connective.Or; +import de.featjar.formula.structure.predicate.Literal; +import de.featjar.formula.structure.predicate.NotEquals; +import de.featjar.formula.structure.term.value.Constant; +import de.featjar.formula.structure.term.value.Variable; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Map; + +/** + * This visitor implements a simple translation of IFeatureModel to boolean + * formula. In this implementation, a cardinality feature can not be a parent. + * The next non-cardinality feature will be the parent instead within the + * boolean representation. + */ +public class ComputeSimpleFormulaVisitor implements ITreeVisitor { + + protected ArrayList constraints; + protected HashSet variables; + private Map, Object>> attributes; + private Boolean hasCardinalityFeature; + + public Boolean getHasCardinalityFeature() { + return hasCardinalityFeature; + } + + public ComputeSimpleFormulaVisitor( + ArrayList constraints, + HashSet variables, + Map, Object>> attributes) { + this.constraints = constraints; + this.variables = variables; + this.attributes = attributes; + this.hasCardinalityFeature = false; + } + + @Override + public TraversalAction firstVisit(List path) { + IFeatureTree node = ITreeVisitor.getCurrentNode(path); + + // TODO use better error value + IFeature feature = node.getFeature(); + String featureName = feature.getName().orElse(""); + + // TODO: do not add variable if its a cardinality var. Add duplicates instead + Variable variable = new Variable(featureName, feature.getType()); + variables.add(variable); + + if (node.getFeature().getAttributes().isPresent()) { + // name is an attribute as well + attributes.put(Features.createFeatureFormel(feature), node.getFeature().getAttributes().get()); + } + + // TODO take featureRanges into Account + Result potentialParentTree = node.getParent(); + //Literal featureLiteral = Expressions.literal(featureName); + + IFormula featureFormula = Features.createFeatureFormel(node.getFeature()); + + if (potentialParentTree.isEmpty()) { + handleRoot(featureFormula, node); + } else if (node.getFeatureCardinalityUpperBound() > 1) { + handleCardinalityFeature(featureFormula, node); + } else { + handleParent(featureFormula, node); + } + + handleGroups(featureFormula, node); + + return ITreeVisitor.super.firstVisit(path); + } + + + + private void handleParent(IFormula featureLiteral, IFeatureTree node) { + // cardinal features must not be a parent + IFormula parentFormula = getNextNonCardinalityParent(node); + constraints.add(new Implies(featureLiteral, parentFormula)); + } + + private void handleRoot(IFormula featureLiteral, IFeatureTree node) { + if (node.isMandatory()) { + constraints.add(featureLiteral); + } + } + + private void handleCardinalityFeature(IFormula featureFormula, IFeatureTree node) { + hasCardinalityFeature = Boolean.TRUE; + + int lowerBound = node.getFeatureCardinalityLowerBound(); + int upperBound = node.getFeatureCardinalityUpperBound(); + + ArrayList featureList = new ArrayList(); + + // add literals and implication to parent + String literalName = ""; + IFormula parentFormula = getNextNonCardinalityParent(node); + for (int i = 1; i <= upperBound; i++) { + literalName = node.getFeature().getName().get() + "_" + i; + featureFormula = Features.createFeatureFormel(node.getFeature(), literalName); + handleParent(featureFormula, node); + + if (i > 1) { + // add to implication chain + IFormula previousLiteral = featureList.get(featureList.size() - 1); + constraints.add(new Implies(featureFormula, previousLiteral)); + } + + featureList.add(featureFormula); + } + + // add cardinality constraint + // check if 0 and do not add implication + if (lowerBound != 0) constraints.add(new Implies(parentFormula, new AtLeast(lowerBound, featureList))); + } + + private IFormula getNextNonCardinalityParent(IFeatureTree node) { + + // if it is possible that root can be as well a cardinality feature - there must + // be an alternative + node = node.getParent().get(); + + if (node.getFeatureCardinalityUpperBound() > 1) { + return getNextNonCardinalityParent(node); + } + + return Features.createFeatureFormel(node.getFeature()); + } + + private void handleGroups(IFormula featureFormula, IFeatureTree node) { + List childrenGroups = node.getChildrenGroups(); + int groupCount = childrenGroups.size(); + ArrayList> groupLiterals = new ArrayList<>(groupCount); + for (int i = 0; i < groupCount; i++) { + groupLiterals.add(null); + } + + // if node is cardinality feature, set feature literal to parent with no + // cardinality + if (node.getFeatureCardinalityUpperBound() > 1) { + featureFormula = getNextNonCardinalityParent(node); + } + + List children = node.getChildren(); + for (IFeatureTree childNode : children) { + IFormula childFormula = Features.createFeatureFormel(childNode.getFeature()); + + if (childNode.isMandatory()) { + constraints.add(new Implies(featureFormula, childFormula)); + } + + int groupID = childNode.getParentGroupID(); + List list = groupLiterals.get(groupID); + if (list == null) { + groupLiterals.set(groupID, list = new ArrayList<>()); + } + list.add(childFormula); + } + + for (int i = 0; i < groupCount; i++) { + Group group = childrenGroups.get(i); + if (group != null) { + if (group.isOr()) { + constraints.add(new Implies(featureFormula, new Or(groupLiterals.get(i)))); + } else if (group.isAlternative()) { + constraints.add(new Implies(featureFormula, new Choose(1, groupLiterals.get(i)))); + } else { + int lowerBound = group.getLowerBound(); + int upperBound = group.getUpperBound(); + if (lowerBound > 0) { + if (upperBound != Range.OPEN) { + constraints.add(new Implies( + featureFormula, new Between(lowerBound, upperBound, groupLiterals.get(i)))); + } else { + constraints.add(new Implies(featureFormula, new AtMost(upperBound, groupLiterals.get(i)))); + } + } else { + if (upperBound != Range.OPEN) { + constraints.add(new Implies(featureFormula, new AtLeast(lowerBound, groupLiterals.get(i)))); + } + } + } + } + } + } +} \ No newline at end of file diff --git a/src/main/java/de/featjar/feature/model/transformer/ReplaceAttributeAggregate.java b/src/main/java/de/featjar/feature/model/transformer/ReplaceAttributeAggregate.java new file mode 100644 index 00000000..0dfdafe3 --- /dev/null +++ b/src/main/java/de/featjar/feature/model/transformer/ReplaceAttributeAggregate.java @@ -0,0 +1,77 @@ +package de.featjar.feature.model.transformer; + +import de.featjar.base.data.IAttribute; +import de.featjar.base.data.Result; +import de.featjar.base.data.Void; +import de.featjar.base.tree.visitor.ITreeVisitor; +import de.featjar.formula.structure.IExpression; +import de.featjar.formula.structure.IFormula; +import de.featjar.formula.structure.term.aggregate.IAttributeAggregate; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * Implements tree visitor {@link ITreeVisitor}. + * Each {@link IAttributeAggregate} placeholder in a formula will be replaced with the correct formula. + * + * @author Lara Merza + * @author Felix Behme + * @author Jonas Hanke + */ +public class ReplaceAttributeAggregate implements ITreeVisitor { + + private final Map, Object>> attributes; + private final boolean hasCardinalityFeatures; + + public ReplaceAttributeAggregate( + Map, Object>> attributes, Boolean hasCardinalityFeatures) { + this.attributes = attributes; + this.hasCardinalityFeatures = hasCardinalityFeatures; + } + + @Override + public TraversalAction lastVisit(List path) { + final IExpression expression = ITreeVisitor.getCurrentNode(path); + + if (expression instanceof IAttributeAggregate) { + + if (hasCardinalityFeatures) { + throw new UnsupportedOperationException( + "Attribute aggregates and cardinality features can not be translated."); + } + + final Result parent = ITreeVisitor.getParentNode(path); + if (parent.isPresent()) { + ArrayList filteredFeatures = new ArrayList<>(); + ArrayList values = new ArrayList<>(); + String attributeFilter = ((IAttributeAggregate) expression).getAttributeFilter(); + + // formula -> feature as a formula, value -> attribute map + attributes.forEach((formula, value) -> { + Optional, Object>> attributeMatch = value.entrySet().stream() + .filter(predicate -> predicate.getKey().getName().equals(attributeFilter)) + .findFirst(); + + if (attributeMatch.isPresent()) { + filteredFeatures.add(formula); + values.add(attributeMatch.get().getValue()); + } + }); + + Result result = ((IAttributeAggregate) expression).translate(filteredFeatures, values); + if (result.isPresent()) { + parent.get().replaceChild(expression, result.get()); + } + } + } + + return TraversalAction.CONTINUE; + } + + @Override + public Result getResult() { + return Result.ofVoid(); + } +} \ No newline at end of file diff --git a/src/main/resources/head.tex b/src/main/resources/head.tex new file mode 100644 index 00000000..6567c0d8 --- /dev/null +++ b/src/main/resources/head.tex @@ -0,0 +1,116 @@ +%---required packages & variable definitions------------------------------------ +\usepackage{forest} +\usepackage{amsmath} +\usepackage{xcolor} +\usetikzlibrary{angles} +\usetikzlibrary{positioning} +\definecolor{drawColor}{RGB}{128 128 128} +\newcommand{\circleSize}{0.25em} +%------------------------------------------------------------------------------- +%---Define the style of the tree------------------------------------------------ +\forestset{ + /tikz/mandatory/.style={ + circle,fill=drawColor, + draw=drawColor, + inner sep=\circleSize + }, + /tikz/optional/.style={ + circle, + fill=white, + draw=drawColor, + inner sep=\circleSize + }, + featureDiagram/.style={ + for tree={ + draw = drawColor, + edge = {draw=drawColor}, + anchor=north, + parent anchor = south, + child anchor = north, + l sep = 2em, + s sep = 1em, + } + }, + /tikz/abstract/.style={ + fill = blue!85!cyan!5, + draw = drawColor + }, + /tikz/concrete/.style={ + fill = blue!85!cyan!20, + draw = drawColor + }, + mandatory/.style={ + edge label+={ + node [mandatory] {} + } + }, + optional/.style={ + edge label+={ + node [optional] {} + } + }, + featurecardinality/.style n args={2}{ + edge label+={ + node[midway,fill=white,font=\scriptsize]{#1,#2} + } + }, + or/.style n args={3}{ + tikz+={ + \path + (!{current, n=#1}.parent) coordinate (A) -- (!c.children) coordinate (B) -- (!{current, n=#2}.parent) coordinate (C) -- (!{current, n=#3}.parent) coordinate (D) + let \p1 = (A), \p2 = (B), \p3 = (C), \p4 = (D), \n1 = {veclen(\x2 - \x1, \y2 - \y1)}, \n2 = {veclen(\x2 - \x3, \y2 - \y3)}, \n3 = {veclen(\x2 - \x4, \y2 - \y4)} + in pic[ + fill=drawColor, + angle radius={min(\n1/2,\n2/2,\n3/2)} + ]{angle}; + } + }, + alternative/.style n args={3}{ + tikz+={ + \path + (!{current, n=#1}.parent) coordinate (A) -- (!c.children) coordinate (B) -- (!{current, n=#2}.parent) coordinate (C) -- (!{current, n=#3}.parent) coordinate (D) + let \p1 = (A), \p2 = (B), \p3 = (C), \p4 = (D), \n1 = {veclen(\x2 - \x1, \y2 - \y1)}, \n2 = {veclen(\x2 - \x3, \y2 - \y3)}, \n3 = {veclen(\x2 - \x4, \y2 - \y4)} + in pic[ + draw=drawColor, + angle radius={min(\n1/2,\n2/2,\n3/2)} + ]{angle}; + } + }, + groupcardinality/.style n args={5}{ + tikz+={ + \path (!{current, n=#1}.parent) coordinate (A) -- (!c.children) coordinate (B) -- (!{current, n=#2}.parent) coordinate (C) -- (!{current, n=#3}.parent) coordinate (D) + let \p1 = (A), \p2 = (B), \p3 = (C), \p4 = (D), \n1 = {veclen(\x2 - \x1, \y2 - \y1)}, \n2 = {veclen(\x2 - \x3, \y2 - \y3)}, \n3 = {veclen(\x2 - \x4, \y2 - \y4)} + in pic[ + draw=drawColor, + angle radius={min(\n1/2,\n2/2,\n3/2)}, + pic text={#4,#5}, + pic text options={ + scale=0.6, + fill=white, + inner sep=0.3pt + } + ]{angle}; + } + }, + /tikz/placeholder/.style={ + }, + collapsed/.style={ + rounded corners, + no edge, + for tree={ + fill opacity=0, + draw opacity=0, + l = 0em, + } + }, + /tikz/hiddenNodes/.style={ + midway, + rounded corners, + draw=drawColor, + fill=white, + minimum size = 1.2em, + minimum width = 0.8em, + scale=0.9 + } +} +%------------------------------------------------------------------------------- \ No newline at end of file diff --git a/src/main/resources/test/test-output.tex b/src/main/resources/test/test-output.tex new file mode 100644 index 00000000..8d5cf7df --- /dev/null +++ b/src/main/resources/test/test-output.tex @@ -0,0 +1,157 @@ +\documentclass[border=5pt]{standalone} +%---required packages & variable definitions------------------------------------ +\usepackage{forest} +\usepackage{amsmath} +\usepackage{xcolor} +\usetikzlibrary{angles} +\usetikzlibrary{positioning} +\definecolor{drawColor}{RGB}{128 128 128} +\newcommand{\circleSize}{0.25em} +%------------------------------------------------------------------------------- +%---Define the style of the tree------------------------------------------------ +\forestset{ + /tikz/mandatory/.style={ + circle,fill=drawColor, + draw=drawColor, + inner sep=\circleSize + }, + /tikz/optional/.style={ + circle, + fill=white, + draw=drawColor, + inner sep=\circleSize + }, + featureDiagram/.style={ + for tree={ + draw = drawColor, + edge = {draw=drawColor}, + anchor=north, + parent anchor = south, + child anchor = north, + l sep = 2em, + s sep = 1em, + } + }, + /tikz/abstract/.style={ + fill = blue!85!cyan!5, + draw = drawColor + }, + /tikz/concrete/.style={ + fill = blue!85!cyan!20, + draw = drawColor + }, + mandatory/.style={ + edge label+={ + node [mandatory] {} + } + }, + optional/.style={ + edge label+={ + node [optional] {} + } + }, + featurecardinality/.style n args={2}{ + edge label+={ + node[midway,fill=white,font=\scriptsize]{#1,#2} + } + }, + or/.style n args={3}{ + tikz+={ + \path + (!{current, n=#1}.parent) coordinate (A) -- (!c.children) coordinate (B) -- (!{current, n=#2}.parent) coordinate (C) -- (!{current, n=#3}.parent) coordinate (D) + let \p1 = (A), \p2 = (B), \p3 = (C), \p4 = (D), \n1 = {veclen(\x2 - \x1, \y2 - \y1)}, \n2 = {veclen(\x2 - \x3, \y2 - \y3)}, \n3 = {veclen(\x2 - \x4, \y2 - \y4)} + in pic[ + fill=drawColor, + angle radius={min(\n1/2,\n2/2,\n3/2)} + ]{angle}; + } + }, + alternative/.style n args={3}{ + tikz+={ + \path + (!{current, n=#1}.parent) coordinate (A) -- (!c.children) coordinate (B) -- (!{current, n=#2}.parent) coordinate (C) -- (!{current, n=#3}.parent) coordinate (D) + let \p1 = (A), \p2 = (B), \p3 = (C), \p4 = (D), \n1 = {veclen(\x2 - \x1, \y2 - \y1)}, \n2 = {veclen(\x2 - \x3, \y2 - \y3)}, \n3 = {veclen(\x2 - \x4, \y2 - \y4)} + in pic[ + draw=drawColor, + angle radius={min(\n1/2,\n2/2,\n3/2)} + ]{angle}; + } + }, + groupcardinality/.style n args={5}{ + tikz+={ + \path (!{current, n=#1}.parent) coordinate (A) -- (!c.children) coordinate (B) -- (!{current, n=#2}.parent) coordinate (C) -- (!{current, n=#3}.parent) coordinate (D) + let \p1 = (A), \p2 = (B), \p3 = (C), \p4 = (D), \n1 = {veclen(\x2 - \x1, \y2 - \y1)}, \n2 = {veclen(\x2 - \x3, \y2 - \y3)}, \n3 = {veclen(\x2 - \x4, \y2 - \y4)} + in pic[ + draw=drawColor, + angle radius={min(\n1/2,\n2/2,\n3/2)}, + pic text={#4,#5}, + pic text options={ + scale=0.6, + fill=white, + inner sep=0.3pt + } + ]{angle}; + } + }, + /tikz/placeholder/.style={ + }, + collapsed/.style={ + rounded corners, + no edge, + for tree={ + fill opacity=0, + draw opacity=0, + l = 0em, + } + }, + /tikz/hiddenNodes/.style={ + midway, + rounded corners, + draw=drawColor, + fill=white, + minimum size = 1.2em, + minimum width = 0.8em, + scale=0.9 + } +} +%------------------------------------------------------------------------------- +\begin{document} + %---The Feature Diagram----------------------------------------------------- +\begin{forest} + featureDiagram + [Hello,abstract[Feature,abstract,featurecardinality={0}{2},alternative={1}{2}{1},or={3}{4}{3},groupcardinality={5}{6}{5}{7}{8}[\multicolumn{2}{c}{Wonderful1} \\\hline +\small\texttt{who (String)} &\small\texttt{= you} \\ +\small\texttt{when (String)} &\small\texttt{= now} \\ +,align=ll,concrete][Beautiful1,concrete][Wonderful2,concrete][Beautiful2,concrete,groupcardinality={1}{3}{2}{0}{2}[Meaningful1,concrete][Meaningful2,concrete][Meaningful3,concrete]][\multicolumn{2}{c}{Wonderful3} \\\hline +\small\texttt{who (String)} &\small\texttt{= you} \\ +,align=ll,concrete][Beautiful3,concrete]][\multicolumn{2}{c}{World1} \\\hline +\small\texttt{size (Double)} &\small\texttt{= 6000.0} \\ +\small\texttt{population (Integer)} &\small\texttt{= 1} \\ +,align=ll,concrete,optional][World2,concrete,mandatory]] + \matrix [anchor=north west] at (current bounding box.north east) { + \node [placeholder] {}; \\ + }; + \matrix [draw=drawColor,anchor=north west] at (current bounding box.north east) { + \node [label=center:\underline{Legend:}] {}; \\ + \node [abstract,label=right:Abstract Feature] {}; \\ + \node [concrete,label=right:Concrete Feature] {}; \\ + \node [mandatory,label=right:Mandatory] {}; \\ + \node [optional,label=right:Optional] {}; \\ + \filldraw[drawColor] (0.1,0) - +(-0,-0.2) - +(0.2,-0.2)- +(0.1,0); + \draw[drawColor] (0.1,0) -- +(-0.2, -0.4); + \draw[drawColor] (0.1,0) -- +(0.2,-0.4); + \fill[drawColor] (0,-0.2) arc (240:300:0.2); + \node [label=right:Or Group] {}; \\ + \draw[drawColor] (0.1,0) -- +(-0.2, -0.4); + \draw[drawColor] (0.1,0) -- +(0.2,-0.4); + \draw[drawColor] (0,-0.2) arc (240:300:0.2); + \node [label=right:Alternative Group] {}; \\ + }; + \matrix [below=1mm of current bounding box] { + \node {\( \text{World1} \land \text{Wonderful1} \)}; \\ + \node {\( \text{World2} \Rightarrow ( \text{Beautiful2} \Leftrightarrow \text{Beautiful3} )\)}; \\ + }; +\end{forest} + + %--------------------------------------------------------------------------- +\end{document} \ No newline at end of file diff --git a/src/test/java/de/featjar/feature/model/TranslateFormulaTest.java b/src/test/java/de/featjar/feature/model/TranslateFormulaTest.java new file mode 100644 index 00000000..63eeb12b --- /dev/null +++ b/src/test/java/de/featjar/feature/model/TranslateFormulaTest.java @@ -0,0 +1,113 @@ +package de.featjar.feature.model; + +import de.featjar.base.FeatJAR; +import de.featjar.base.computation.Computations; +import de.featjar.base.data.identifier.Identifiers; +import de.featjar.base.tree.Trees; +import de.featjar.feature.model.transformer.ComputeFormula; +import de.featjar.formula.structure.Expressions; +import de.featjar.formula.structure.IFormula; +import de.featjar.formula.structure.connective.And; +import de.featjar.formula.structure.connective.Reference; +import de.featjar.formula.structure.predicate.Literal; +import de.featjar.formula.structure.predicate.NotEquals; +import de.featjar.formula.structure.term.value.Constant; +import de.featjar.formula.structure.term.value.Variable; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +/** + * @author Lara Merza + * @author Felix Behme + * @author Jonas Hanke + */ + +public class TranslateFormulaTest { + + @BeforeAll + public static void insert() { + FeatJAR.testConfiguration().initialize(); + } + + @Test + public void testInteger() { + IFeatureModel featureModel = new FeatureModel(Identifiers.newCounterIdentifier()); + addValues(featureModel, Integer.class); + + IFormula result = Computations.of(featureModel) + .map(ComputeFormula::new) + .compute(); + IFormula formula = buildFormula(Integer.class, 0); + Assertions.assertTrue(Trees.equals(result, formula), result.print() + "\n" + formula.print()); + FeatJAR.log().info("Integer Test expected value: " + formula.print()); + FeatJAR.log().info("Integer Test result output: " + result.print()); + } + + @Test + public void testBoolean() { + IFeatureModel featureModel = new FeatureModel(Identifiers.newCounterIdentifier()); + addValues(featureModel, Boolean.class); + + IFormula result = Computations.of(featureModel) + .map(ComputeFormula::new) + .compute(); + IFormula formula = buildBooleanForumla(); + Assertions.assertTrue(Trees.equals(result, formula), result.print() + "\n" + formula.print()); + FeatJAR.log().info("Boolean Test expected value: " + formula.print()); + FeatJAR.log().info("Boolean Test result output: " + result.print()); + } + + @Test + public void testFloat() { + IFeatureModel featureModel = new FeatureModel(Identifiers.newCounterIdentifier()); + addValues(featureModel, Float.class); + + IFormula result = Computations.of(featureModel) + .map(ComputeFormula::new) + .compute(); + IFormula formula = buildFormula(Float.class, (float) 0.0); + Assertions.assertTrue(Trees.equals(result, formula), result.print() + "\n" + formula.print()); + FeatJAR.log().info("Float Test expected value: " + formula.print()); + FeatJAR.log().info("Float Test result output: " + result.print()); + } + + private void addValues(IFeatureModel featureModel, Class type) { + IFeature root = featureModel.mutate().addFeature("root"); + IFeatureTree rootTree = featureModel.mutate().addFeatureTreeRoot(root); + rootTree.getRoot().mutate().toAndGroup(); + for (short i = 0; i < 5; i++) { + IFeature feature = featureModel.mutate().addFeature(i + "feature"); + feature.mutate().setName("feature" + i); + feature.mutate().setType(type); + rootTree.mutate().addFeatureBelow(feature); + + FeatJAR.log().info("Added Feature " + feature.getName().get() + " with type " + feature.getType()); + } + } + + private IFormula buildFormula(Class type, Object expectedValue) { + return new Reference(new And( + Expressions.implies(new NotEquals(new Variable("feature0", type), + new Constant(expectedValue, type)), new Literal("root")), + Expressions.implies(new NotEquals(new Variable("feature1", type), + new Constant(expectedValue, type)), new Literal("root")), + Expressions.implies(new NotEquals(new Variable("feature2", type), + new Constant(expectedValue, type)), new Literal("root")), + Expressions.implies(new NotEquals(new Variable("feature3", type), + new Constant(expectedValue, type)), new Literal("root")), + Expressions.implies(new NotEquals(new Variable("feature4", type), + new Constant(expectedValue, type)), new Literal("root")) + )); + } + + private IFormula buildBooleanForumla() { + return new Reference(new And( + Expressions.implies(Expressions.literal("feature0"), Expressions.literal("root")), + Expressions.implies(Expressions.literal("feature1"), Expressions.literal("root")), + Expressions.implies(Expressions.literal("feature2"), Expressions.literal("root")), + Expressions.implies(Expressions.literal("feature3"), Expressions.literal("root")), + Expressions.implies(Expressions.literal("feature4"), Expressions.literal("root")) + )); + } +} \ No newline at end of file diff --git a/src/test/java/de/featjar/feature/model/io/tikz/AttributeFilterTest.java b/src/test/java/de/featjar/feature/model/io/tikz/AttributeFilterTest.java new file mode 100644 index 00000000..1950e0d9 --- /dev/null +++ b/src/test/java/de/featjar/feature/model/io/tikz/AttributeFilterTest.java @@ -0,0 +1,117 @@ +package de.featjar.feature.model.io.tikz; + +import de.featjar.base.FeatJAR; +import de.featjar.base.data.Attribute; +import de.featjar.base.data.identifier.Identifiers; +import de.featjar.feature.model.FeatureModel; +import de.featjar.feature.model.IFeature; +import de.featjar.feature.model.IFeatureModel; +import de.featjar.feature.model.io.tikz.helper.TikzAttributeHelper; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.util.List; + +/** + * Test for displaying the attributes in tikz (filter) + * + * @author Felix Behme + * @author Lara Merza + * @author Jonas Hanke + */ +public class AttributeFilterTest { + + private static IFeatureModel featureModel; + private static IFeature feature; + + private final String FIRST_TEST_OUTPUT = "[\\multicolumn{2}{c}{attribute-test} \\\\\\hline\n" + + "\\small\\texttt{int-attribute (Integer)} &\\small\\texttt{= 1} \\\\\n" + + ",align=ll"; + private final String SECOND_TEST_OUTPUT = "[\\multicolumn{2}{c}{attribute-test} \\\\\\hline\n" + + "\\small\\texttt{int-attribute (Integer)} &\\small\\texttt{= 1} \\\\\n" + + "\\small\\texttt{bool-attribute (Boolean)} &\\small\\texttt{= true} \\\\\n" + + ",align=ll"; + private final String THIRD_TEST_OUTPUT = "[\\multicolumn{2}{c}{attribute-test} \\\\\\hline\n" + + "\\small\\texttt{name (String)} &\\small\\texttt{= attribute-test} \\\\\n" + + "\\small\\texttt{bool-attribute (Boolean)} &\\small\\texttt{= true} \\\\\n" + + "\\small\\texttt{string-attribute (String)} &\\small\\texttt{= hallo} \\\\\n" + + ",align=ll"; + private final String FOURTH_TEST_OUTPUT = "[\\multicolumn{2}{c}{attribute-test} \\\\\\hline\n" + + "\\small\\texttt{name (String)} &\\small\\texttt{= attribute-test} \\\\\n" + + "\\small\\texttt{string-attribute (String)} &\\small\\texttt{= hallo} \\\\\n" + + ",align=ll"; + + @BeforeAll + public static void init() { + FeatJAR.testConfiguration().initialize(); + + FeatureModel featureModel = new FeatureModel(Identifiers.newCounterIdentifier()); + + IFeature feature = featureModel.addFeature("attribute-test"); + + Attribute attribute = new Attribute<>("test", "int-attribute", Integer.class); + Attribute attribute2 = new Attribute<>("test", "bool-attribute", Boolean.class); + Attribute attribute3 = new Attribute<>("test", "string-attribute", String.class); + + feature.mutate().setAttributeValue(attribute, 1); + feature.mutate().setAttributeValue(attribute2, true); + feature.mutate().setAttributeValue(attribute3, "hallo"); + + AttributeFilterTest.featureModel = featureModel; + AttributeFilterTest.feature = feature; + } + + @Test + public void test1() { + StringBuilder stringBuilder = new StringBuilder(); + + TikzAttributeHelper tikzAttributeHelper = new TikzAttributeHelper(feature, stringBuilder) + .setFilterType(TikzAttributeHelper.FilterType.DISPLAY) + .addFilterValue(List.of("int-attribute")); + + tikzAttributeHelper.build(); + + Assertions.assertEquals(stringBuilder.toString(), FIRST_TEST_OUTPUT); + } + + @Test + public void test2() { + StringBuilder stringBuilder = new StringBuilder(); + + TikzAttributeHelper tikzAttributeHelper = new TikzAttributeHelper(feature, stringBuilder) + .setFilterType(TikzAttributeHelper.FilterType.DISPLAY) + .addFilterValue(List.of("int-attribute", "bool-attribute")); + + tikzAttributeHelper.build(); + + Assertions.assertEquals(stringBuilder.toString(), SECOND_TEST_OUTPUT); + } + + @Test + public void test3() { + StringBuilder stringBuilder = new StringBuilder(); + + TikzAttributeHelper tikzAttributeHelper = new TikzAttributeHelper(feature, stringBuilder) + .setFilterType(TikzAttributeHelper.FilterType.WITH_OUT) + .addFilterValue(List.of("int-attribute")); + + tikzAttributeHelper.build(); + + Assertions.assertEquals(stringBuilder.toString(), THIRD_TEST_OUTPUT); + } + + @Test + public void test4() { + StringBuilder stringBuilder = new StringBuilder(); + + TikzAttributeHelper tikzAttributeHelper = new TikzAttributeHelper(feature, stringBuilder) + .setFilterType(TikzAttributeHelper.FilterType.WITH_OUT) + .addFilterValue(List.of("int-attribute", "bool-attribute")); + + tikzAttributeHelper.build(); + + Assertions.assertEquals(stringBuilder.toString(), FOURTH_TEST_OUTPUT); + } + +} diff --git a/src/test/java/de/featjar/feature/model/io/tikz/FeatureModelDisplayTikzTest.java b/src/test/java/de/featjar/feature/model/io/tikz/FeatureModelDisplayTikzTest.java new file mode 100644 index 00000000..81caf8c0 --- /dev/null +++ b/src/test/java/de/featjar/feature/model/io/tikz/FeatureModelDisplayTikzTest.java @@ -0,0 +1,179 @@ +package de.featjar.feature.model.io.tikz; + +import de.featjar.base.FeatJAR; +import de.featjar.base.data.Attribute; +import de.featjar.base.data.Range; +import de.featjar.base.data.identifier.Identifiers; +import de.featjar.feature.model.*; +import de.featjar.feature.model.io.tikz.helper.TikzAttributeHelper; +import de.featjar.formula.structure.Expressions; +import de.featjar.formula.structure.connective.*; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import java.io.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.List; + +/** + * Test the full output with a test feature model and attributes, constrains and more + * + * @author Felix Behme + * @author Lara Merza + * @author Jonas Hanke + */ +public class FeatureModelDisplayTikzTest { + + private static IFeatureModel featureModel; + + @BeforeAll + public static void init() { + FeatJAR.testConfiguration().initialize(); + + FeatureModel featureModel = new FeatureModel(Identifiers.newCounterIdentifier()); + + IFeature featureRootS = featureModel.mutate().addFeature("Hello"); + IFeature feature = featureModel.mutate().addFeature("Feature"); + IFeature world1 = featureModel.mutate().addFeature("World1"); + world1.mutate().setAttributeValue(new Attribute<>("size", Double.class), 6000.0); + world1.mutate().setAttributeValue(new Attribute<>("population", Integer.class), 1); + IFeature world2 = featureModel.mutate().addFeature("World2"); + IFeature wonderful1 = featureModel.mutate().addFeature("Wonderful1"); + wonderful1.mutate().setAttributeValue(new Attribute<>("who", String.class), "you"); + wonderful1.mutate().setAttributeValue(new Attribute<>( "when", String.class), "now"); + IFeature beautiful1 = featureModel.mutate().addFeature("Beautiful1"); + IFeature wonderful2 = featureModel.mutate().addFeature("Wonderful2"); + IFeature beautiful2 = featureModel.mutate().addFeature("Beautiful2"); + IFeature wonderful3 = featureModel.mutate().addFeature("Wonderful3"); + wonderful3.mutate().setAttributeValue(new Attribute<>("who", String.class), "you"); + IFeature beautiful3 = featureModel.mutate().addFeature("Beautiful3"); + IFeature meaningful1 = featureModel.mutate().addFeature("Meaningful1"); + IFeature meaningful2 = featureModel.mutate().addFeature("Meaningful2"); + IFeature meaningful3 = featureModel.mutate().addFeature("Meaningful3"); + + featureRootS.mutate().setAbstract(); + + // first tree + IFeatureTree rootTree = + featureModel.mutate().addFeatureTreeRoot(featureRootS); + rootTree.mutate().toAndGroup(); + + IFeatureTree firstFeatureTree = rootTree.mutate().addFeatureBelow(feature); + feature.mutate().setAbstract(); + int group1 = firstFeatureTree.mutate().addAlternativeGroup(); + int group2 = firstFeatureTree.mutate().addOrGroup(); + int group3 = firstFeatureTree.mutate().addCardinalityGroup(Range.of(7,8)); + + firstFeatureTree.mutate().addFeatureBelow(wonderful1, 0, group1); + firstFeatureTree.mutate().setFeatureCardinality(Range.of(0,2)); + firstFeatureTree.mutate().addFeatureBelow(beautiful1, 1, group1); + + firstFeatureTree.mutate().addFeatureBelow(wonderful2, 2, group2); + IFeatureTree beautiful2FeatureTree = firstFeatureTree.mutate().addFeatureBelow(beautiful2, 3, group2); + + int group4 = beautiful2FeatureTree.mutate().addCardinalityGroup(Range.of(0,2)); + beautiful2FeatureTree.mutate().addFeatureBelow(meaningful1, 0, group4); + beautiful2FeatureTree.mutate().addFeatureBelow(meaningful2, 1, group4); + beautiful2FeatureTree.mutate().addFeatureBelow(meaningful3, 2, group4); + + firstFeatureTree.mutate().addFeatureBelow(wonderful3, 4, group3); + firstFeatureTree.mutate().addFeatureBelow(beautiful3, 5, group3); + + rootTree.mutate().addFeatureBelow(world1); + + IFeatureTree world2FeatureTree = rootTree.mutate().addFeatureBelow(world2); + world2FeatureTree.mutate().setFeatureCardinality(Range.of(1, 1)); + + // Constraints + featureModel.addConstraint(new And(Expressions.literal("World1"), Expressions.literal("Wonderful1"))); + featureModel.addConstraint(new Implies(Expressions.literal("World2"), new BiImplies(Expressions.literal("Beautiful2"), Expressions.literal("Beautiful3")))); + + FeatureModelDisplayTikzTest.featureModel = featureModel; + } + + @Test + public void perform() { + StringBuilder expectedOutput = loadTestFile(); + + if (expectedOutput == null) { + throw new IllegalStateException("File is null"); + } + + String value = expectedOutput.toString(); + + TikzGraphicalFeatureModelFormat tikzGraphicalFeatureModelFormat = new TikzGraphicalFeatureModelFormat( + TikzAttributeHelper.FilterType.WITH_OUT, + List.of("name", "abstract") + ); + tikzGraphicalFeatureModelFormat.serialize(featureModel).ifPresent(output -> { + FeatJAR.log().info("Expected Output: " + value); + FeatJAR.log().info("Acutally Output: " + output); + + Assertions.assertEquals(value, output); + }); + } + + // Todo: Add @Test here and remove the other @Test on the method perform + // @Test + public void createTestFile() { + TikzGraphicalFeatureModelFormat tikzGraphicalFeatureModelFormat = new TikzGraphicalFeatureModelFormat( + TikzAttributeHelper.FilterType.WITH_OUT, + List.of("name", "abstract") + ); + tikzGraphicalFeatureModelFormat.serialize(featureModel).ifPresent(this::writeToFile); + } + + /** + * This method can be used to write the output in a file with ignoring tabs or spaces. + * (It will be written correctly) + * + * @param "output" from the "new" file + */ + private void writeToFile(String value) { + Path filePath = Paths.get("src", "main", "resources", "test", "test-output.tex"); + + try { + Files.createDirectories(filePath.getParent()); + + try (BufferedWriter writer = Files.newBufferedWriter(filePath)) { + writer.write(value); + } + } catch (IOException e) { + e.printStackTrace(); + } + } + + /** + * This method the test-output.tex from the resource (resource/test) file. + * + * @return Output of the file as a StringBuilder + */ + private StringBuilder loadTestFile() { + StringBuilder stringBuilderFile = new StringBuilder(); + InputStream inputStream = getClass().getClassLoader().getResourceAsStream("test/test-output.tex"); + + if (inputStream == null) { + return null; + } + + try (BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream))) { + String line; + while ((line = bufferedReader.readLine()) != null) { + stringBuilderFile.append(line).append(System.lineSeparator()); + } + + } catch (IOException e) { + throw new RuntimeException(e); + } + + // Remove last add (its empty) + int lastIndex = stringBuilderFile.lastIndexOf("\n"); + if (lastIndex >= 0) { + stringBuilderFile.delete(lastIndex, stringBuilderFile.length()); + } + + return stringBuilderFile; + } +} \ No newline at end of file diff --git a/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java b/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java new file mode 100644 index 00000000..a9caabe0 --- /dev/null +++ b/src/test/java/de/featjar/feature/model/transformer/ComputeFormulaTest.java @@ -0,0 +1,596 @@ +/* + * Copyright (C) 2025 FeatJAR-Development-Team + * + * This file is part of FeatJAR-feature-model. + * + * feature-model is free software: you can redistribute it and/or modify it + * under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3.0 of the License, + * or (at your option) any later version. + * + * feature-model is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with feature-model. If not, see . + * + * See for further information. + */ +package de.featjar.feature.model.transformer; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import de.featjar.base.computation.ComputeConstant; +import de.featjar.base.data.Attribute; +import de.featjar.base.data.Range; +import de.featjar.base.data.identifier.Identifiers; +import de.featjar.feature.model.FeatureModel; +import de.featjar.feature.model.IFeature; +import de.featjar.feature.model.IFeatureModel; +import de.featjar.feature.model.IFeatureTree; +import de.featjar.formula.structure.IFormula; +import de.featjar.formula.structure.connective.And; +import de.featjar.formula.structure.connective.Between; +import de.featjar.formula.structure.connective.Choose; +import de.featjar.formula.structure.connective.Implies; +import de.featjar.formula.structure.connective.Or; +import de.featjar.formula.structure.connective.Reference; +import de.featjar.formula.structure.predicate.LessThan; +import de.featjar.formula.structure.predicate.Literal; +import de.featjar.formula.structure.predicate.NotEquals; +import de.featjar.formula.structure.term.aggregate.AttributeSum; +import de.featjar.formula.structure.term.function.IfThenElse; +import de.featjar.formula.structure.term.function.RealAdd; +import de.featjar.formula.structure.term.value.Constant; +import de.featjar.formula.structure.term.value.Variable; +import java.util.Arrays; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class ComputeFormulaTest { + private IFeatureModel featureModel; + private IFormula expected; + + @BeforeEach + public void createFeatureModel() { + featureModel = new FeatureModel(Identifiers.newCounterIdentifier()); + } + + @Test + void simpleWithTwoCardinalities() { + IFeatureTree rootTree = + featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + rootTree.mutate().makeMandatory(); + rootTree.mutate().toAndGroup(); + + // create and set cardinality for the child feature + IFeature childFeature1 = featureModel.mutate().addFeature("A"); + IFeatureTree childFeature1Tree = rootTree.mutate().addFeatureBelow(childFeature1); + childFeature1Tree.mutate().setFeatureCardinality(Range.of(0, 2)); + + IFeature childFeature2 = featureModel.mutate().addFeature("B"); + IFeatureTree childFeature2Tree = childFeature1Tree.mutate().addFeatureBelow(childFeature2); + childFeature2Tree.mutate().setFeatureCardinality(Range.of(0, 2)); + + expected = new Reference(new And( + new Literal("root"), + new Implies(new Literal("A_1"), new Literal("root")), + new Implies(new Literal("A_2"), new Literal("root")), + new Implies(new Literal("A_2"), new Literal("A_1")), + new Implies(new Literal("B_1"), new Literal("root")), + new Implies(new Literal("B_2"), new Literal("root")), + new Implies(new Literal("B_2"), new Literal("B_1")))); + + executeSimpleTest(); + } + + @Test + void simpleWithTwoCardinalitiesNumericFeatures() { + IFeatureTree rootTree = + featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + rootTree.mutate().makeMandatory(); + rootTree.mutate().toAndGroup(); + + // create and set cardinality for the child feature + IFeature childFeature1 = featureModel.mutate().addFeature("A"); + childFeature1.mutate().setType(Integer.class); + IFeatureTree childFeature1Tree = rootTree.mutate().addFeatureBelow(childFeature1); + childFeature1Tree.mutate().setFeatureCardinality(Range.of(0, 2)); + + IFeature childFeature2 = featureModel.mutate().addFeature("B"); + childFeature2.mutate().setType(Float.class); + IFeatureTree childFeature2Tree = childFeature1Tree.mutate().addFeatureBelow(childFeature2); + childFeature2Tree.mutate().setFeatureCardinality(Range.of(0, 2)); + + expected = new Reference(new And( + new Literal("root"), + new Implies(new NotEquals(new Variable("A_1", Integer.class), new Constant(0)), new Literal("root")), + new Implies(new NotEquals(new Variable("A_2", Integer.class), new Constant(0)), new Literal("root")), + new Implies(new NotEquals(new Variable("A_2", Integer.class), new Constant(0)), new NotEquals(new Variable("A_1", Integer.class), new Constant(0))), + new Implies(new NotEquals(new Variable("B_1", Float.class), new Constant(0.0f)), new Literal("root")), + new Implies(new NotEquals(new Variable("B_2", Float.class), new Constant(0.0f)), new Literal("root")), + new Implies(new NotEquals(new Variable("B_2", Float.class), new Constant(0.0f)), new NotEquals(new Variable("B_1", Float.class), new Constant(0.0f))))); + + executeSimpleTest(); + } + + @Test + void withTwoCardinalies() { + IFeatureTree rootTree = + featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + rootTree.mutate().makeMandatory(); + rootTree.mutate().toAndGroup(); + + // create and set cardinality for the child feature + IFeature childFeature1 = featureModel.mutate().addFeature("A"); + IFeatureTree childFeature1Tree = rootTree.mutate().addFeatureBelow(childFeature1); + childFeature1Tree.mutate().setFeatureCardinality(Range.of(0, 2)); + + IFeature childFeature2 = featureModel.mutate().addFeature("B"); + IFeatureTree childFeature2Tree = childFeature1Tree.mutate().addFeatureBelow(childFeature2); + childFeature2Tree.mutate().setFeatureCardinality(Range.of(0, 2)); + + expected = new Reference(new And( + new Literal("root"), + new Implies(new Literal("A_1"), new Literal("root")), + new Implies(new Literal("B_1.A_1"), new Literal("A_1")), + new Implies(new Literal("B_2.A_1"), new Literal("A_1")), + new Implies(new Literal("B_2.A_1"), new Literal("B_1.A_1")), + new Implies(new Literal("A_2"), new Literal("root")), + new Implies(new Literal("A_2"), new Literal("A_1")), + new Implies(new Literal("B_1.A_2"), new Literal("A_2")), + new Implies(new Literal("B_2.A_2"), new Literal("A_2")), + new Implies(new Literal("B_2.A_2"), new Literal("B_1.A_2")))); + + executeTest(); + } + + @Test + void simpleWithCardinalityAndChildGroup() { + IFeatureTree rootTree = + featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + rootTree.mutate().makeMandatory(); + rootTree.mutate().toAndGroup(); + + // create and set cardinality for the child feature + IFeature childFeature1 = featureModel.mutate().addFeature("A"); + IFeatureTree childFeature1Tree = rootTree.mutate().addFeatureBelow(childFeature1); + childFeature1Tree.mutate().setFeatureCardinality(Range.of(0, 2)); + + childFeature1Tree.mutate().toAlternativeGroup(); + + IFeature childFeature2 = featureModel.mutate().addFeature("B"); + childFeature1Tree.mutate().addFeatureBelow(childFeature2); + + IFeature childFeature3 = featureModel.mutate().addFeature("C"); + childFeature1Tree.mutate().addFeatureBelow(childFeature3); + + expected = new Reference(new And( + new Literal("root"), + new Implies(new Literal("A_1"), new Literal("root")), + new Implies(new Literal("A_2"), new Literal("root")), + new Implies(new Literal("A_2"), new Literal("A_1")), + new Implies(new Literal("root"), new Choose(1, Arrays.asList(new Literal("B"), new Literal("C")))), + new Implies(new Literal("B"), new Literal("root")), + new Implies(new Literal("C"), new Literal("root")))); + + executeSimpleTest(); + } + + @Test + void simpleWithCardinalityAndChildGroupNumericFeatures() { + IFeatureTree rootTree = + featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + rootTree.mutate().makeMandatory(); + rootTree.mutate().toAndGroup(); + + // create and set cardinality for the child feature + IFeature childFeature1 = featureModel.mutate().addFeature("A"); + childFeature1.mutate().setType(Float.class); + IFeatureTree childFeature1Tree = rootTree.mutate().addFeatureBelow(childFeature1); + childFeature1Tree.mutate().setFeatureCardinality(Range.of(0, 2)); + + childFeature1Tree.mutate().toAlternativeGroup(); + + IFeature childFeature2 = featureModel.mutate().addFeature("B"); + childFeature2.mutate().setType(Integer.class); + childFeature1Tree.mutate().addFeatureBelow(childFeature2); + + IFeature childFeature3 = featureModel.mutate().addFeature("C"); + childFeature1Tree.mutate().addFeatureBelow(childFeature3); + + expected = new Reference(new And( + new Literal("root"), + new Implies(new NotEquals(new Variable("A_1", Float.class), new Constant(0.0f)), new Literal("root")), + new Implies(new NotEquals(new Variable("A_2", Float.class), new Constant(0.0f)), new Literal("root")), + new Implies(new NotEquals(new Variable("A_2", Float.class), new Constant(0.0f)), new NotEquals(new Variable("A_1", Float.class), new Constant(0.0f))), + new Implies(new Literal("root"), new Choose(1, Arrays.asList(new NotEquals(new Variable("B", Integer.class), new Constant(0)), new Literal("C")))), + new Implies(new NotEquals(new Variable("B", Integer.class), new Constant(0)), new Literal("root")), + new Implies(new Literal("C"), new Literal("root")))); + + executeSimpleTest(); + } + + @Test + void withCardinalityAndChildInbetween() { + IFeatureTree rootTree = + featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + rootTree.mutate().makeMandatory(); + rootTree.mutate().toAndGroup(); + + // create and set cardinality for the child feature + IFeature childFeature1 = featureModel.mutate().addFeature("A"); + IFeatureTree childFeature1Tree = rootTree.mutate().addFeatureBelow(childFeature1); + childFeature1Tree.mutate().setFeatureCardinality(Range.of(0, 2)); + + IFeature childFeature2 = featureModel.mutate().addFeature("B"); + IFeatureTree childFeature1Tree2 = childFeature1Tree.mutate().addFeatureBelow(childFeature2); + + IFeature childFeature3 = featureModel.mutate().addFeature("C"); + IFeatureTree childFeature1Tree3 = childFeature1Tree2.mutate().addFeatureBelow(childFeature3); + childFeature1Tree3.mutate().setFeatureCardinality(Range.of(0, 2)); + + expected = new Reference(new And( + new Literal("root"), + new Implies(new Literal("A_1"), new Literal("root")), + new Implies(new Literal("B.A_1"), new Literal("A_1")), + new Implies(new Literal("C_1.B.A_1"), new Literal("B.A_1")), + new Implies(new Literal("C_2.B.A_1"), new Literal("B.A_1")), + new Implies(new Literal("C_2.B.A_1"), new Literal("C_1.B.A_1")), + new Implies(new Literal("A_2"), new Literal("root")), + new Implies(new Literal("A_2"), new Literal("A_1")), + new Implies(new Literal("B.A_2"), new Literal("A_2")), + new Implies(new Literal("C_1.B.A_2"), new Literal("B.A_2")), + new Implies(new Literal("C_2.B.A_2"), new Literal("B.A_2")), + new Implies(new Literal("C_2.B.A_2"), new Literal("C_1.B.A_2")))); + + executeTest(); + } + + @Test + void withTwoGroups() { + IFeatureTree rootTree = + featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + rootTree.mutate().makeMandatory(); + rootTree.mutate().toAndGroup(); + + IFeature childFeature1 = featureModel.mutate().addFeature("A"); + rootTree.mutate().addFeatureBelow(childFeature1); + + IFeature childFeature2 = featureModel.mutate().addFeature("B"); + rootTree.mutate().addFeatureBelow(childFeature2); + + rootTree.mutate().toAlternativeGroup(); + int orGroupId = rootTree.mutate().addOrGroup(); + + IFeature childFeature3 = featureModel.mutate().addFeature("C"); + IFeatureTree childFeatureTree3 = rootTree.mutate().addFeatureBelow(childFeature3); + childFeatureTree3.mutate().setParentGroupID(orGroupId); + + IFeature childFeature4 = featureModel.mutate().addFeature("D"); + IFeatureTree addFeatureBelow4 = rootTree.mutate().addFeatureBelow(childFeature4); + addFeatureBelow4.mutate().setParentGroupID(orGroupId); + + expected = new Reference(new And( + new Literal("root"), + new Implies(new Literal("root"), new Choose(1, Arrays.asList(new Literal("A"), new Literal("B")))), + new Implies(new Literal("root"), new Or(Arrays.asList(new Literal("C"), new Literal("D")))), + new Implies(new Literal("A"), new Literal("root")), + new Implies(new Literal("B"), new Literal("root")), + new Implies(new Literal("C"), new Literal("root")), + new Implies(new Literal("D"), new Literal("root")))); + + executeTest(); + } + + @Test + void withCardinalityAndChildGroup() { + IFeatureTree rootTree = + featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + rootTree.mutate().makeMandatory(); + rootTree.mutate().toAndGroup(); + + // create and set cardinality for the child feature + IFeature childFeature1 = featureModel.mutate().addFeature("A"); + IFeatureTree childFeature1Tree = rootTree.mutate().addFeatureBelow(childFeature1); + childFeature1Tree.mutate().setFeatureCardinality(Range.of(0, 2)); + + childFeature1Tree.mutate().toAlternativeGroup(); + + IFeature childFeature2 = featureModel.mutate().addFeature("B"); + childFeature1Tree.mutate().addFeatureBelow(childFeature2); + + IFeature childFeature3 = featureModel.mutate().addFeature("C"); + childFeature1Tree.mutate().addFeatureBelow(childFeature3); + + expected = new Reference(new And( + new Literal("root"), + new Implies(new Literal("A_1"), new Literal("root")), + new Implies( + new Literal("A_1"), new Choose(1, Arrays.asList(new Literal("B.A_1"), new Literal("C.A_1")))), + new Implies(new Literal("B.A_1"), new Literal("A_1")), + new Implies(new Literal("C.A_1"), new Literal("A_1")), + new Implies(new Literal("A_2"), new Literal("root")), + new Implies(new Literal("A_2"), new Literal("A_1")), + new Implies( + new Literal("A_2"), new Choose(1, Arrays.asList(new Literal("B.A_2"), new Literal("C.A_2")))), + new Implies(new Literal("B.A_2"), new Literal("A_2")), + new Implies(new Literal("C.A_2"), new Literal("A_2")))); + + executeTest(); + } + + @Test + void withCardinalityAndChildChildGroup() { + IFeatureTree rootTree = + featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + rootTree.mutate().makeMandatory(); + rootTree.mutate().toAndGroup(); + + // create and set cardinality for the child feature + IFeature childFeature1 = featureModel.mutate().addFeature("A"); + IFeatureTree childFeature1Tree = rootTree.mutate().addFeatureBelow(childFeature1); + childFeature1Tree.mutate().setFeatureCardinality(Range.of(0, 2)); + + childFeature1Tree.mutate().toAlternativeGroup(); + + IFeature childFeature2 = featureModel.mutate().addFeature("B"); + childFeature1Tree.mutate().addFeatureBelow(childFeature2); + + IFeature childFeature3 = featureModel.mutate().addFeature("C"); + IFeatureTree childFeature1Tree2 = childFeature1Tree.mutate().addFeatureBelow(childFeature3); + + childFeature1Tree2.mutate().toOrGroup(); + + IFeature childFeature4 = featureModel.mutate().addFeature("D"); + childFeature1Tree2.mutate().addFeatureBelow(childFeature4); + + IFeature childFeature5 = featureModel.mutate().addFeature("E"); + childFeature1Tree2.mutate().addFeatureBelow(childFeature5); + + expected = new Reference(new And( + new Literal("root"), + new Implies(new Literal("A_1"), new Literal("root")), + new Implies( + new Literal("A_1"), new Choose(1, Arrays.asList(new Literal("B.A_1"), new Literal("C.A_1")))), + new Implies(new Literal("B.A_1"), new Literal("A_1")), + new Implies(new Literal("C.A_1"), new Literal("A_1")), + + // sub-subtree + new Implies( + new Literal("C.A_1"), new Or(Arrays.asList(new Literal("D.C.A_1"), new Literal("E.C.A_1")))), + new Implies(new Literal("D.C.A_1"), new Literal("C.A_1")), + new Implies(new Literal("E.C.A_1"), new Literal("C.A_1")), + new Implies(new Literal("A_2"), new Literal("root")), + new Implies(new Literal("A_2"), new Literal("A_1")), + new Implies( + new Literal("A_2"), new Choose(1, Arrays.asList(new Literal("B.A_2"), new Literal("C.A_2")))), + new Implies(new Literal("B.A_2"), new Literal("A_2")), + new Implies(new Literal("C.A_2"), new Literal("A_2")), + + // second sub-subtree + new Implies( + new Literal("C.A_2"), new Or(Arrays.asList(new Literal("D.C.A_2"), new Literal("E.C.A_2")))), + new Implies(new Literal("D.C.A_2"), new Literal("C.A_2")), + new Implies(new Literal("E.C.A_2"), new Literal("C.A_2")))); + + executeTest(); + } + + @Test + void onlyRoot() { + + // root and nothing else + featureModel + .mutate() + .addFeatureTreeRoot(featureModel.mutate().addFeature("root")) + .mutate() + .makeMandatory(); + + // root must be selected + expected = new Reference(new And(new Literal("root"))); + + executeTest(); + } + + @Test + void oneFeature() { + + // root + IFeatureTree rootTree = + featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + rootTree.mutate().makeMandatory(); + rootTree.mutate().toAndGroup(); + + // create and add our only child + IFeature childFeature = featureModel.mutate().addFeature("Test1"); + rootTree.mutate().addFeatureBelow(childFeature); + + expected = new Reference(new And(new Literal("root"), new Implies(new Literal("Test1"), new Literal("root")))); + + executeTest(); + } + + static Attribute cpuAttribute = new Attribute<>("cpu", Boolean.class); + static Attribute gpuAttribute = new Attribute<>("cpu", Boolean.class); + + static Attribute costAttribute = new Attribute<>("cost", Double.class); + + @Test + void simpleOneFeatureAndAttributeAggregate() { + + // root + IFeatureTree rootTree = + featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + rootTree.mutate().makeMandatory(); + rootTree.mutate().toAndGroup(); + + // create and add our only child + IFeature childFeature = featureModel.mutate().addFeature("A"); + rootTree.mutate().addFeatureBelow(childFeature); + + // add attribute to aggregate + childFeature.mutate().setAttributeValue(costAttribute, 10.0); + + // cross-tree constraint for aggregate testing + IFormula aggregateConstraint = new LessThan(new AttributeSum("cost"), new Constant(200.0, Double.class)); + featureModel.mutate().addConstraint(aggregateConstraint); + + expected = new Reference(new And( + new Literal("root"), + new Implies(new Literal("A"), new Literal("root")), + new LessThan( + new RealAdd(new IfThenElse( + new Literal("A"), new Constant(10.0, Double.class), new Constant(0.0, Double.class))), + new Constant(200.0, Double.class)))); + + executeSimpleTest(); + } + + @Test + void cardinalityAndAttributeAggregate() { + IFeatureTree rootTree = + featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + rootTree.mutate().makeMandatory(); + rootTree.mutate().toAndGroup(); + + // create and add our only child + IFeature childFeature = featureModel.mutate().addFeature("A"); + IFeatureTree childFeatureTree = rootTree.mutate().addFeatureBelow(childFeature); + childFeatureTree.mutate().setFeatureCardinality(Range.of(0, 4)); + + // add attribute to aggregate + childFeature.mutate().setAttributeValue(costAttribute, 10.0); + + // cross-tree constraint for aggregate testing + IFormula aggregateConstraint = new LessThan(new AttributeSum("cost"), new Constant(200.0, Double.class)); + featureModel.mutate().addConstraint(aggregateConstraint); + + executeExpectedException(); + } + + @Test + void simpleCardinalityAndAttributeAggregate() { + IFeatureTree rootTree = + featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + rootTree.mutate().makeMandatory(); + rootTree.mutate().toAndGroup(); + + // create and add our only child + IFeature childFeature = featureModel.mutate().addFeature("A"); + IFeatureTree childFeatureTree = rootTree.mutate().addFeatureBelow(childFeature); + childFeatureTree.mutate().setFeatureCardinality(Range.of(0, 4)); + + // add attribute to aggregate + childFeature.mutate().setAttributeValue(costAttribute, 10.0); + + // cross-tree constraint for aggregate testing + IFormula aggregateConstraint = new LessThan(new AttributeSum("cost"), new Constant(200.0, Double.class)); + featureModel.mutate().addConstraint(aggregateConstraint); + + executeSimpleExpectedException(); + } + + @Test + void withCardinalityGroup() { + IFeatureTree rootTree = + featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + rootTree.mutate().makeMandatory(); + rootTree.mutate().toCardinalityGroup(Range.of(2, 3)); + + // create and set cardinality for the child feature + IFeature childFeature1 = featureModel.mutate().addFeature("A"); + rootTree.mutate().addFeatureBelow(childFeature1); + + IFeature childFeature2 = featureModel.mutate().addFeature("B"); + rootTree.mutate().addFeatureBelow(childFeature2); + + IFeature childFeature3 = featureModel.mutate().addFeature("C"); + rootTree.mutate().addFeatureBelow(childFeature3); + + expected = new Reference(new And( + new Literal("root"), + new Implies( + new Literal("root"), + new Between(2, 3, Arrays.asList(new Literal("A"), new Literal("B"), new Literal("C")))), + new Implies(new Literal("A"), new Literal("root")), + new Implies(new Literal("B"), new Literal("root")), + new Implies(new Literal("C"), new Literal("root")))); + + executeTest(); + } + + @Test + void withOneCardinalityFeature() { + IFeatureTree rootTree = + featureModel.mutate().addFeatureTreeRoot(featureModel.mutate().addFeature("root")); + rootTree.mutate().makeMandatory(); + rootTree.mutate().toAndGroup(); + + // create and set cardinality for the child feature + IFeature childFeature = featureModel.mutate().addFeature("A"); + IFeatureTree childFeatureTree1 = rootTree.mutate().addFeatureBelow(childFeature); + childFeatureTree1.mutate().setFeatureCardinality(Range.of(0, 2)); + + // add normal feature below + IFeature childFeature2 = featureModel.mutate().addFeature("B"); + childFeatureTree1.mutate().addFeatureBelow(childFeature2); + + expected = new Reference(new And( + new Literal("root"), + new Implies(new Literal("A_1"), new Literal("root")), + new Implies(new Literal("B.A_1"), new Literal("A_1")), + new Implies(new Literal("A_2"), new Literal("root")), + new Implies(new Literal("A_2"), new Literal("A_1")), + new Implies(new Literal("B.A_2"), new Literal("A_2")))); + + executeTest(); + } + + private void executeTest() { + + ComputeConstant computeConstant = new ComputeConstant(featureModel); + ComputeFormula computeFormula = new ComputeFormula(computeConstant); + + IFormula resultFormula = computeFormula.computeResult().get(); + + // assert + assertEquals(expected, resultFormula); + } + + private void executeSimpleTest() { + + ComputeConstant computeConstant = new ComputeConstant(featureModel); + ComputeFormula computeFormula = new ComputeFormula(computeConstant); + + IFormula resultFormula = computeFormula + .set(ComputeFormula.SIMPLE_TRANSLATION, Boolean.TRUE) + .computeResult() + .get(); + + // assert + assertEquals(expected, resultFormula); + } + + private void executeExpectedException() { + ComputeConstant computeConstant = new ComputeConstant(featureModel); + ComputeFormula computeFormula = new ComputeFormula(computeConstant); + + assertThrows( + UnsupportedOperationException.class, + () -> computeFormula.computeResult().orElseThrow()); + } + + private void executeSimpleExpectedException() { + ComputeConstant computeConstant = new ComputeConstant(featureModel); + ComputeFormula computeFormula = new ComputeFormula(computeConstant); + + assertThrows(UnsupportedOperationException.class, () -> computeFormula + .set(ComputeFormula.SIMPLE_TRANSLATION, Boolean.TRUE) + .computeResult() + .orElseThrow()); + } +} diff --git a/src/test/java/de/featjar/feature/model/transformer/ReplaceAttributeAggregateTest.java b/src/test/java/de/featjar/feature/model/transformer/ReplaceAttributeAggregateTest.java new file mode 100644 index 00000000..f6353895 --- /dev/null +++ b/src/test/java/de/featjar/feature/model/transformer/ReplaceAttributeAggregateTest.java @@ -0,0 +1,209 @@ +package de.featjar.feature.model.transformer; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import de.featjar.base.FeatJAR; +import de.featjar.base.data.Attribute; +import de.featjar.base.data.IAttribute; +import de.featjar.base.tree.Trees; +import de.featjar.formula.structure.Expressions; +import de.featjar.formula.structure.IFormula; +import de.featjar.formula.structure.connective.And; +import de.featjar.formula.structure.connective.Implies; +import de.featjar.formula.structure.predicate.LessThan; +import de.featjar.formula.structure.predicate.Literal; +import de.featjar.formula.structure.predicate.NotEquals; +import de.featjar.formula.structure.term.aggregate.AttributeAverage; +import de.featjar.formula.structure.term.aggregate.AttributeSum; +import de.featjar.formula.structure.term.function.IfThenElse; +import de.featjar.formula.structure.term.function.IntegerAdd; +import de.featjar.formula.structure.term.function.RealAdd; +import de.featjar.formula.structure.term.function.RealDivide; +import de.featjar.formula.structure.term.value.Constant; +import de.featjar.formula.structure.term.value.Variable; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +public class ReplaceAttributeAggregateTest { + + private static Map, Object>> attributes; + + @BeforeAll + public static void init() { + FeatJAR.testConfiguration().initialize(); + + attributes = new LinkedHashMap<>(); + attributes.put( + Expressions.literal("cpu"), + Map.of( + new Attribute<>("cost", Long.class), + 10L, + new Attribute<>("required", Boolean.class), + true, + new Attribute<>("power", Float.class), + 104.5f + ) + ); + attributes.put( + Expressions.literal("gpu"), + Map.of( + new Attribute<>("cost", Long.class), + 100L, + new Attribute<>("required", Boolean.class), + false, + new Attribute<>("power", Float.class), + 200.5f + ) + ); + attributes.put( + Expressions.literal("ram"), + Map.of( + new Attribute<>("cost", Long.class), + 20L, + new Attribute<>("required", Boolean.class), + true + ) + ); + attributes.put( + Expressions.literal("motherboard"), + Map.of( + new Attribute<>("required", Boolean.class), + true, + new Attribute<>("power", Float.class), + 3.5f + ) + ); + attributes.put(Expressions.literal("power_supply"), Collections.emptyMap()); + attributes.put( + new NotEquals(new Variable("refreshrate", Double.class), new Constant(0.0)), + Map.of( + new Attribute<>("required", Boolean.class), + true, + new Attribute<>("power", Float.class), + 1.0f + ) + ); + } + + @Test + public void test1() { + IFormula test = new LessThan(new AttributeSum("cost"), new Constant(200L, Long.class)); + ReplaceAttributeAggregate replaceAttributeAggregate = new ReplaceAttributeAggregate(attributes, false); + Trees.traverse(test, replaceAttributeAggregate); + + IFormula comparison = new LessThan( + new IntegerAdd( + new IfThenElse( + new Literal("cpu"), + new Constant(10L, Long.class), + new Constant(0L, Long.class)), + new IfThenElse( + new Literal("gpu"), + new Constant(100L, Long.class), + new Constant(0L, Long.class)), + new IfThenElse( + new Literal("ram"), + new Constant(20L, Long.class), + new Constant(0L, Long.class))), + new Constant(200L, Long.class)); + + assertTrue(test.equalsTree(comparison)); + } + + @Test + public void test2() { + IFormula test = new LessThan(new AttributeSum("cost"), new AttributeAverage("power")); + ReplaceAttributeAggregate replaceAttributeAggregate = new ReplaceAttributeAggregate(attributes, false); + Trees.traverse(test, replaceAttributeAggregate); + + IFormula comparison = new LessThan( + new IntegerAdd( + new IfThenElse( + new Literal("cpu"), + new Constant(10L, Long.class), + new Constant(0L, Long.class)), + new IfThenElse( + new Literal("gpu"), + new Constant(100L, Long.class), + new Constant(0L, Long.class)), + new IfThenElse( + new Literal("ram"), + new Constant(20L, Long.class), + new Constant(0L, Long.class))), + new RealDivide( + new RealAdd( + new IfThenElse( + new Literal("cpu"), + new Constant(104.5, Double.class), + new Constant(0.0, Double.class)), + new IfThenElse( + new Literal("gpu"), + new Constant(200.5, Double.class), + new Constant(0.0, Double.class)), + new IfThenElse( + new Literal("motherboard"), + new Constant(3.5, Double.class), + new Constant(0.0, Double.class)), + new IfThenElse( + new NotEquals(new Variable("refreshrate", Double.class), new Constant(0.0)), + new Constant(1.0, Double.class), + new Constant(0.0, Double.class)) + ), + new RealAdd( + new IfThenElse( + new Literal("cpu"), + new Constant(1.0, Double.class), + new Constant(0.0, Double.class)), + new IfThenElse( + new Literal("gpu"), + new Constant(1.0, Double.class), + new Constant(0.0, Double.class)), + new IfThenElse( + new Literal("motherboard"), + new Constant(1.0, Double.class), + new Constant(0.0, Double.class)), + new IfThenElse( + new NotEquals(new Variable("refreshrate", Double.class), new Constant(0.0)), + new Constant(1.0, Double.class), + new Constant(0.0, Double.class)) + ) + )); + + assertTrue(test.equalsTree(comparison)); + } + + @Test + public void test3() { + IFormula test = new And( + new Implies( + new Literal("cables"), new LessThan(new AttributeSum("cost"), new Constant(200L, Long.class))), + new Literal("case")); + ReplaceAttributeAggregate replaceAttributeAggregate = new ReplaceAttributeAggregate(attributes, false); + Trees.traverse(test, replaceAttributeAggregate); + + IFormula comparison = new And( + new Implies( + new Literal("cables"), + new LessThan( + new IntegerAdd( + new IfThenElse( + new Literal("cpu"), + new Constant(10L, Long.class), + new Constant(0L, Long.class)), + new IfThenElse( + new Literal("gpu"), + new Constant(100L, Long.class), + new Constant(0L, Long.class)), + new IfThenElse( + new Literal("ram"), + new Constant(20L, Long.class), + new Constant(0L, Long.class))), + new Constant(200L, Long.class))), + new Literal("case")); + + assertTrue(test.equalsTree(comparison)); + } +} \ No newline at end of file diff --git a/src/test/java/show/TikzShow.java b/src/test/java/show/TikzShow.java new file mode 100644 index 00000000..68245ede --- /dev/null +++ b/src/test/java/show/TikzShow.java @@ -0,0 +1,57 @@ +package show; + +import de.featjar.base.FeatJAR; +import de.featjar.base.data.identifier.Identifiers; +import de.featjar.base.io.IO; +import de.featjar.feature.model.FeatureModel; +import de.featjar.feature.model.IFeature; +import de.featjar.feature.model.IFeatureTree; +import de.featjar.feature.model.io.tikz.TikzGraphicalFeatureModelFormat; +import de.featjar.feature.model.io.tikz.helper.TikzAttributeHelper; +import java.io.IOException; +import java.nio.file.Paths; +import java.util.List; + +public class TikzShow { + + public static void main(String[] args) { + FeatJAR.testConfiguration().initialize(); // init FeatJAR + + FeatureModel featureModel = new FeatureModel(Identifiers.newCounterIdentifier()); // create feature model + + IFeature featureRoot = featureModel.mutate().addFeature("Hello"); + featureRoot.mutate().setAbstract(); + + IFeature feature = featureModel.mutate().addFeature("Feature"); + feature.mutate().setAbstract(); + + IFeature world = featureModel.mutate().addFeature("World"); + IFeature wonderful = featureModel.mutate().addFeature("Wonderful"); + IFeature beautiful = featureModel.mutate().addFeature("Beautiful"); + + IFeatureTree rootTree = featureModel.mutate().addFeatureTreeRoot(featureRoot); // create tree and make it to an and group + rootTree.mutate().toAndGroup(); + + IFeatureTree firstFeatureTree = rootTree.mutate().addFeatureBelow(feature); + firstFeatureTree.mutate().toOrGroup(); + + firstFeatureTree.mutate().addFeatureBelow(wonderful); + firstFeatureTree.mutate().addFeatureBelow(beautiful); + + rootTree.mutate().addFeatureBelow(world); + + TikzGraphicalFeatureModelFormat tikzGraphicalFeatureModelFormat = new TikzGraphicalFeatureModelFormat( + TikzAttributeHelper.FilterType.WITH_OUT, + List.of("name") + ); + + try { + // write class output to a file + IO.save(featureModel, Paths.get("src", "test", "java", "show", "test-output-show.tex"), tikzGraphicalFeatureModelFormat); + FeatJAR.log().info("Build run successfully"); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + +}