From 895b26df7a806a2136c2f7a46d56b542326e561f Mon Sep 17 00:00:00 2001 From: Kristóf Marussy Date: Wed, 6 Sep 2023 19:05:10 +0200 Subject: feat(dse): transformation rule builder --- .../visualization/ModelVisualizerAdapter.java | 32 ++ .../visualization/ModelVisualizerBuilder.java | 16 + .../visualization/ModelVisualizerStoreAdapter.java | 22 ++ .../visualization/internal/FileFormat.java | 26 ++ .../internal/ModelVisualizeStoreAdapterImpl.java | 60 ++++ .../internal/ModelVisualizerAdapterImpl.java | 387 +++++++++++++++++++++ .../internal/ModelVisualizerBuilderImpl.java | 55 +++ 7 files changed, 598 insertions(+) create mode 100644 subprojects/store-dse-visualization/src/main/java/tools/refinery/visualization/ModelVisualizerAdapter.java create mode 100644 subprojects/store-dse-visualization/src/main/java/tools/refinery/visualization/ModelVisualizerBuilder.java create mode 100644 subprojects/store-dse-visualization/src/main/java/tools/refinery/visualization/ModelVisualizerStoreAdapter.java create mode 100644 subprojects/store-dse-visualization/src/main/java/tools/refinery/visualization/internal/FileFormat.java create mode 100644 subprojects/store-dse-visualization/src/main/java/tools/refinery/visualization/internal/ModelVisualizeStoreAdapterImpl.java create mode 100644 subprojects/store-dse-visualization/src/main/java/tools/refinery/visualization/internal/ModelVisualizerAdapterImpl.java create mode 100644 subprojects/store-dse-visualization/src/main/java/tools/refinery/visualization/internal/ModelVisualizerBuilderImpl.java (limited to 'subprojects/store-dse-visualization/src/main') diff --git a/subprojects/store-dse-visualization/src/main/java/tools/refinery/visualization/ModelVisualizerAdapter.java b/subprojects/store-dse-visualization/src/main/java/tools/refinery/visualization/ModelVisualizerAdapter.java new file mode 100644 index 00000000..ae87d8ac --- /dev/null +++ b/subprojects/store-dse-visualization/src/main/java/tools/refinery/visualization/ModelVisualizerAdapter.java @@ -0,0 +1,32 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.visualization; + +import tools.refinery.store.adapter.ModelAdapter; +import tools.refinery.store.map.Version; +import tools.refinery.store.tuple.Tuple; +import tools.refinery.visualization.internal.ModelVisualizerBuilderImpl; + +import java.util.Collection; + +public interface ModelVisualizerAdapter extends ModelAdapter { + + ModelVisualizerStoreAdapter getStoreAdapter(); + static ModelVisualizerBuilder builder() { + return new ModelVisualizerBuilderImpl(); + } + + public void addTransition(Version from, Version to, String action); + + + public void addTransition(Version from, Version to, String action, Tuple activation); + public void addState(Version state); + public void addState(Version state, Collection fitness); + public void addState(Version state, String label); + public void addSolution(Version state); + public void visualize(); + +} diff --git a/subprojects/store-dse-visualization/src/main/java/tools/refinery/visualization/ModelVisualizerBuilder.java b/subprojects/store-dse-visualization/src/main/java/tools/refinery/visualization/ModelVisualizerBuilder.java new file mode 100644 index 00000000..592f5fcf --- /dev/null +++ b/subprojects/store-dse-visualization/src/main/java/tools/refinery/visualization/ModelVisualizerBuilder.java @@ -0,0 +1,16 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.visualization; + +import tools.refinery.store.adapter.ModelAdapterBuilder; +import tools.refinery.visualization.internal.FileFormat; + +public interface ModelVisualizerBuilder extends ModelAdapterBuilder { + ModelVisualizerBuilder withOutputpath(String outputpath); + ModelVisualizerBuilder withFormat(FileFormat format); + ModelVisualizerBuilder saveDesignSpace(); + ModelVisualizerBuilder saveStates(); +} diff --git a/subprojects/store-dse-visualization/src/main/java/tools/refinery/visualization/ModelVisualizerStoreAdapter.java b/subprojects/store-dse-visualization/src/main/java/tools/refinery/visualization/ModelVisualizerStoreAdapter.java new file mode 100644 index 00000000..46663b2a --- /dev/null +++ b/subprojects/store-dse-visualization/src/main/java/tools/refinery/visualization/ModelVisualizerStoreAdapter.java @@ -0,0 +1,22 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.visualization; + +import tools.refinery.store.adapter.ModelStoreAdapter; +import tools.refinery.visualization.internal.FileFormat; + +import java.util.Set; + +public interface ModelVisualizerStoreAdapter extends ModelStoreAdapter { + + String getOutputPath(); + + boolean isRenderDesignSpace(); + + boolean isRenderStates(); + + Set getFormats(); +} diff --git a/subprojects/store-dse-visualization/src/main/java/tools/refinery/visualization/internal/FileFormat.java b/subprojects/store-dse-visualization/src/main/java/tools/refinery/visualization/internal/FileFormat.java new file mode 100644 index 00000000..c5dffeb2 --- /dev/null +++ b/subprojects/store-dse-visualization/src/main/java/tools/refinery/visualization/internal/FileFormat.java @@ -0,0 +1,26 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.visualization.internal; + +public enum FileFormat { + BMP("bmp"), + DOT("dot"), + JPEG("jpg"), + PDF("pdf"), + PLAIN("plain"), + PNG("png"), + SVG("svg"); + + private final String format; + + FileFormat(String format) { + this.format = format; + } + + public String getFormat() { + return format; + } +} diff --git a/subprojects/store-dse-visualization/src/main/java/tools/refinery/visualization/internal/ModelVisualizeStoreAdapterImpl.java b/subprojects/store-dse-visualization/src/main/java/tools/refinery/visualization/internal/ModelVisualizeStoreAdapterImpl.java new file mode 100644 index 00000000..04be22d6 --- /dev/null +++ b/subprojects/store-dse-visualization/src/main/java/tools/refinery/visualization/internal/ModelVisualizeStoreAdapterImpl.java @@ -0,0 +1,60 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.visualization.internal; + +import tools.refinery.store.adapter.ModelAdapter; +import tools.refinery.store.model.Model; +import tools.refinery.store.model.ModelStore; +import tools.refinery.visualization.ModelVisualizerStoreAdapter; + +import java.util.Set; + +public class ModelVisualizeStoreAdapterImpl implements ModelVisualizerStoreAdapter { + private final ModelStore store; + private final String outputPath; + private final boolean renderDesignSpace; + private final boolean renderStates; + private final Set formats; + + public ModelVisualizeStoreAdapterImpl(ModelStore store, String outputPath, Set formats, + boolean renderDesignSpace, boolean renderStates) { + this.store = store; + this.outputPath = outputPath; + this.formats = formats; + this.renderDesignSpace = renderDesignSpace; + this.renderStates = renderStates; + } + + @Override + public ModelStore getStore() { + return store; + } + + @Override + public ModelAdapter createModelAdapter(Model model) { + return new ModelVisualizerAdapterImpl(model, this); + } + + @Override + public String getOutputPath() { + return outputPath; + } + + @Override + public boolean isRenderDesignSpace() { + return renderDesignSpace; + } + + @Override + public boolean isRenderStates() { + return renderStates; + } + + @Override + public Set getFormats() { + return formats; + } +} diff --git a/subprojects/store-dse-visualization/src/main/java/tools/refinery/visualization/internal/ModelVisualizerAdapterImpl.java b/subprojects/store-dse-visualization/src/main/java/tools/refinery/visualization/internal/ModelVisualizerAdapterImpl.java new file mode 100644 index 00000000..531969b4 --- /dev/null +++ b/subprojects/store-dse-visualization/src/main/java/tools/refinery/visualization/internal/ModelVisualizerAdapterImpl.java @@ -0,0 +1,387 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.visualization.internal; + +import tools.refinery.store.map.Version; +import tools.refinery.store.model.Interpretation; +import tools.refinery.store.model.Model; +import tools.refinery.store.representation.AnySymbol; +import tools.refinery.store.representation.TruthValue; +import tools.refinery.store.tuple.Tuple; +import tools.refinery.visualization.ModelVisualizerAdapter; +import tools.refinery.visualization.ModelVisualizerStoreAdapter; + +import java.io.*; +import java.util.*; +import java.util.stream.Collectors; + +public class ModelVisualizerAdapterImpl implements ModelVisualizerAdapter { + private final Model model; + private final ModelVisualizerStoreAdapter storeAdapter; + private final Map> allInterpretations; + private final StringBuilder designSpaceBuilder = new StringBuilder(); + private final Map states = new HashMap<>(); + private int transitionCounter = 0; + private Integer numberOfStates = 0; + private final String outputPath; + private final Set formats; + private final boolean renderDesignSpace; + private final boolean renderStates; + + private static final Map truthValueToDot = Map.of( + TruthValue.TRUE, "1", + TruthValue.FALSE, "0", + TruthValue.UNKNOWN, "½", + TruthValue.ERROR, "E", + true, "1", + false, "0" + ); + + public ModelVisualizerAdapterImpl(Model model, ModelVisualizerStoreAdapter storeAdapter) { + this.model = model; + this.storeAdapter = storeAdapter; + this.outputPath = storeAdapter.getOutputPath(); + this.formats = storeAdapter.getFormats(); + if (formats.isEmpty()) { + formats.add(FileFormat.SVG); + } + this.renderDesignSpace = storeAdapter.isRenderDesignSpace(); + this.renderStates = storeAdapter.isRenderStates(); + + this.allInterpretations = new HashMap<>(); + for (var symbol : storeAdapter.getStore().getSymbols()) { + var arity = symbol.arity(); + if (arity < 1 || arity > 2) { + continue; + } + var interpretation = (Interpretation) model.getInterpretation(symbol); + allInterpretations.put(symbol, interpretation); + } + designSpaceBuilder.append("digraph designSpace {\n"); + designSpaceBuilder.append(""" + nodesep=0 + ranksep=5 + node[ + \tstyle=filled + \tfillcolor=white + ] + """); + } + + @Override + public Model getModel() { + return model; + } + + @Override + public ModelVisualizerStoreAdapter getStoreAdapter() { + return storeAdapter; + } + + private String createDotForCurrentModelState() { + + var unaryTupleToInterpretationsMap = new HashMap>>(); + + var sb = new StringBuilder(); + + sb.append("digraph model {\n"); + sb.append(""" + node [ + \tstyle="filled, rounded" + \tshape=plain + \tpencolor="#00000088" + \tfontname="Helvetica" + ] + """); + sb.append(""" + edge [ + \tlabeldistance=3 + \tfontname="Helvetica" + ] + """); + + for (var entry : allInterpretations.entrySet()) { + var key = entry.getKey(); + var arity = key.arity(); + var cursor = entry.getValue().getAll(); + if (arity == 1) { + while (cursor.move()) { + unaryTupleToInterpretationsMap.computeIfAbsent(cursor.getKey(), k -> new LinkedHashSet<>()) + .add(entry.getValue()); + } + } else if (arity == 2) { + while (cursor.move()) { + var tuple = cursor.getKey(); + for (var i = 0; i < tuple.getSize(); i++) { + var id = tuple.get(i); + unaryTupleToInterpretationsMap.computeIfAbsent(Tuple.of(id), k -> new LinkedHashSet<>()); + } + sb.append(drawEdge(cursor.getKey(), key, entry.getValue())); + } + } + } + for (var entry : unaryTupleToInterpretationsMap.entrySet()) { + sb.append(drawElement(entry)); + } + sb.append("}"); + return sb.toString(); + } + + private StringBuilder drawElement(Map.Entry>> entry) { + var sb = new StringBuilder(); + + var tableStyle = " CELLSPACING=\"0\" BORDER=\"2\" CELLBORDER=\"0\" CELLPADDING=\"4\" STYLE=\"ROUNDED\""; + + var key = entry.getKey(); + var id = key.get(0); + var mainLabel = String.valueOf(id); + var interpretations = entry.getValue(); + var backgroundColor = toBackgroundColorString(averageColor(interpretations)); + + sb.append(id); + sb.append(" [\n"); + sb.append("\tfillcolor=\"").append(backgroundColor).append("\"\n"); + sb.append("\tlabel="); + if (interpretations.isEmpty()) { + sb.append("<\n\t").append(mainLabel).append(""); + } + else { + sb.append("<\n\t\t") + .append(mainLabel).append("\n"); + for (var interpretation : interpretations) { + var rawValue = interpretation.get(key); + + if (rawValue == null || rawValue.equals(TruthValue.FALSE) || rawValue.equals(false)) { + continue; + } + var color = "black"; + if (rawValue.equals(TruthValue.ERROR)) { + color = "red"; + } + var value = truthValueToDot.getOrDefault(rawValue, rawValue.toString()); + var symbol = interpretation.getSymbol(); + + if (symbol.valueType() == String.class) { + value = "\"" + value + "\""; + } + sb.append("\t\t") + .append(interpretation.getSymbol().name()) + .append("") + .append("=").append(value) + .append("\n"); + } + } + sb.append("\t\t>\n"); + sb.append("]\n"); + + return sb; + } + + private String drawEdge(Tuple edge, AnySymbol symbol, Interpretation interpretation) { + var value = interpretation.get(edge); + + if (value == null || value.equals(TruthValue.FALSE) || value.equals(false)) { + return ""; + } + + var sb = new StringBuilder(); + var style = "solid"; + var color = "black"; + if (value.equals(TruthValue.UNKNOWN)) { + style = "dotted"; + } + else if (value.equals(TruthValue.ERROR)) { + style = "dashed"; + color = "red"; + } + + var from = edge.get(0); + var to = edge.get(1); + var name = symbol.name(); + sb.append(from).append(" -> ").append(to) + .append(" [\n\tstyle=").append(style) + .append("\n\tcolor=").append(color) + .append("\n\tfontcolor=").append(color) + .append("\n\tlabel=\"").append(name) + .append("\"]\n"); + return sb.toString(); + } + + private String toBackgroundColorString(Integer[] backgroundColor) { + if (backgroundColor.length == 3) + return String.format("#%02x%02x%02x", backgroundColor[0], backgroundColor[1], backgroundColor[2]); + else if (backgroundColor.length == 4) + return String.format("#%02x%02x%02x%02x", backgroundColor[0], backgroundColor[1], backgroundColor[2], + backgroundColor[3]); + return null; + } + + private Integer[] typeColor(String name) { + var random = new Random(name.hashCode()); + return new Integer[] { random.nextInt(128) + 128, random.nextInt(128) + 128, random.nextInt(128) + 128 }; + } + + private Integer[] averageColor(Set> interpretations) { + if(interpretations.isEmpty()) { + return new Integer[]{256, 256, 256}; + } + // TODO: Only use interpretations where the value is not false (or unknown) + var symbols = interpretations.stream() + .map(i -> typeColor(i.getSymbol().name())).toArray(Integer[][]::new); + + + + return new Integer[] { + Arrays.stream(symbols).map(i -> i[0]).collect(Collectors.averagingInt(Integer::intValue)).intValue(), + Arrays.stream(symbols).map(i -> i[1]).collect(Collectors.averagingInt(Integer::intValue)).intValue(), + Arrays.stream(symbols).map(i -> i[2]).collect(Collectors.averagingInt(Integer::intValue)).intValue() + }; + } + + private String createDotForModelState(Version version) { + var currentVersion = model.getState(); + model.restore(version); + var graph = createDotForCurrentModelState(); + model.restore(currentVersion); + return graph; + } + + private boolean saveDot(String dot, String filePath) { + File file = new File(filePath); + file.getParentFile().mkdirs(); + + try (FileWriter writer = new FileWriter(file)) { + writer.write(dot); + } catch (Exception e) { + e.printStackTrace(); + return false; + } + return true; + } + + private boolean renderDot(String dot, String filePath) { + return renderDot(dot, FileFormat.SVG, filePath); + } + + private boolean renderDot(String dot, FileFormat format, String filePath) { + try { + Process process = new ProcessBuilder("dot", "-T" + format.getFormat(), "-o", filePath).start(); + + OutputStream osToProcess = process.getOutputStream(); + PrintWriter pwToProcess = new PrintWriter(osToProcess); + pwToProcess.write(dot); + pwToProcess.close(); + } catch (IOException e) { + e.printStackTrace(); + return false; + } + return true; + } + + @Override + public void addTransition(Version from, Version to, String action) { + designSpaceBuilder.append(states.get(from)).append(" -> ").append(states.get(to)) + .append(" [label=\"").append(transitionCounter++).append(": ").append(action).append("\"]\n"); + } + + @Override + public void addTransition(Version from, Version to, String action, Tuple activation) { + designSpaceBuilder.append(states.get(from)).append(" -> ").append(states.get(to)) + .append(" [label=\"").append(transitionCounter++).append(": ").append(action).append(" / "); + + + for (int i = 0; i < activation.getSize(); i++) { + designSpaceBuilder.append(activation.get(i)); + if (i < activation.getSize() - 1) { + designSpaceBuilder.append(", "); + } + } + designSpaceBuilder.append("\"]\n"); + } + + @Override + public void addState(Version state) { + if (states.containsKey(state)) { + return; + } + states.put(state, numberOfStates++); + designSpaceBuilder.append(states.get(state)).append(" [URL=\"./").append(states.get(state)).append(".svg\"]\n"); + } + + @Override + public void addState(Version state, Collection fitness) { + var labelBuilder = new StringBuilder(); + for (var f : fitness) { + labelBuilder.append(f).append(", "); + } + addState(state, labelBuilder.toString()); + } + + @Override + public void addState(Version state, String label) { + if (states.containsKey(state)) { + return; + } + states.put(state, numberOfStates++); + designSpaceBuilder.append(states.get(state)).append(" [label = \"").append(states.get(state)).append(" ("); + designSpaceBuilder.append(label); + designSpaceBuilder.append(")\"\n").append("URL=\"./").append(states.get(state)).append(".svg\"]\n"); + } + + @Override + public void addSolution(Version state) { + addState(state); + designSpaceBuilder.append(states.get(state)).append(" [shape = doublecircle]\n"); + } + + private String buildDesignSpaceDot() { + designSpaceBuilder.append("}"); + return designSpaceBuilder.toString(); + } + + private boolean saveDesignSpace(String path) { + saveDot(buildDesignSpaceDot(), path + "/designSpace.dot"); + for (var entry : states.entrySet()) { + saveDot(createDotForModelState(entry.getKey()), path + "/" + entry.getValue() + ".dot"); + } + return true; + } + + private void renderDesignSpace(String path, Set formats) { + File filePath = new File(path); + filePath.mkdirs(); + if (renderStates) { + for (var entry : states.entrySet()) { + var stateId = entry.getValue(); + var stateDot = createDotForModelState(entry.getKey()); + for (var format : formats) { + if (format == FileFormat.DOT) { + saveDot(stateDot, path + "/" + stateId + ".dot"); + } + else { + renderDot(stateDot, format, path + "/" + stateId + "." + format.getFormat()); + } + } + } + } + if (renderDesignSpace) { + var designSpaceDot = buildDesignSpaceDot(); + for (var format : formats) { + if (format == FileFormat.DOT) { + saveDot(designSpaceDot, path + "/designSpace.dot"); + } + else { + renderDot(designSpaceDot, format, path + "/designSpace." + format.getFormat()); + } + } + } + } + + @Override + public void visualize() { + renderDesignSpace(outputPath, formats); + } +} diff --git a/subprojects/store-dse-visualization/src/main/java/tools/refinery/visualization/internal/ModelVisualizerBuilderImpl.java b/subprojects/store-dse-visualization/src/main/java/tools/refinery/visualization/internal/ModelVisualizerBuilderImpl.java new file mode 100644 index 00000000..e4d801d8 --- /dev/null +++ b/subprojects/store-dse-visualization/src/main/java/tools/refinery/visualization/internal/ModelVisualizerBuilderImpl.java @@ -0,0 +1,55 @@ +/* + * SPDX-FileCopyrightText: 2021-2023 The Refinery Authors + * + * SPDX-License-Identifier: EPL-2.0 + */ +package tools.refinery.visualization.internal; + +import tools.refinery.store.adapter.AbstractModelAdapterBuilder; +import tools.refinery.store.model.ModelStore; +import tools.refinery.visualization.ModelVisualizerBuilder; + +import java.util.LinkedHashSet; +import java.util.Set; + +public class ModelVisualizerBuilderImpl + extends AbstractModelAdapterBuilder + implements ModelVisualizerBuilder { + private String outputPath; + private boolean saveDesignSpace = false; + private boolean saveStates = false; + private Set formats = new LinkedHashSet<>(); + + @Override + protected ModelVisualizeStoreAdapterImpl doBuild(ModelStore store) { + return new ModelVisualizeStoreAdapterImpl(store, outputPath, formats, saveDesignSpace, saveStates); + } + + @Override + public ModelVisualizerBuilder withOutputpath(String outputpath) { + checkNotConfigured(); + this.outputPath = outputpath; + return this; + } + + @Override + public ModelVisualizerBuilder withFormat(FileFormat format) { + checkNotConfigured(); + this.formats.add(format); + return this; + } + + @Override + public ModelVisualizerBuilder saveDesignSpace() { + checkNotConfigured(); + this.saveDesignSpace = true; + return this; + } + + @Override + public ModelVisualizerBuilder saveStates() { + checkNotConfigured(); + this.saveStates = true; + return this; + } +} -- cgit v1.2.3-54-g00ecf