From 87fe96f3c67993ab1e4b131d73ecf276482e65a9 Mon Sep 17 00:00:00 2001 From: nagilooh Date: Thu, 3 Aug 2023 16:58:25 +0200 Subject: Improve visualization -Display values from all relevant interpretations -Support TruthValue -Add tabular formatting -Add colors --- .../internal/ModelVisualizerAdapterImpl.java | 214 +++++++++++++++++---- 1 file changed, 178 insertions(+), 36 deletions(-) (limited to 'subprojects/visualization/src/main') diff --git a/subprojects/visualization/src/main/java/tools/refinery/visualization/internal/ModelVisualizerAdapterImpl.java b/subprojects/visualization/src/main/java/tools/refinery/visualization/internal/ModelVisualizerAdapterImpl.java index 33c5a43b..8555da5f 100644 --- a/subprojects/visualization/src/main/java/tools/refinery/visualization/internal/ModelVisualizerAdapterImpl.java +++ b/subprojects/visualization/src/main/java/tools/refinery/visualization/internal/ModelVisualizerAdapterImpl.java @@ -4,13 +4,14 @@ 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.HashMap; -import java.util.Map; +import java.util.*; +import java.util.stream.Collectors; public class ModelVisualizerAdapterImpl implements ModelVisualizerAdapter { private final Model model; @@ -20,6 +21,15 @@ public class ModelVisualizerAdapterImpl implements ModelVisualizerAdapter { private final Map states = new HashMap<>(); private int transitionCounter = 0; private Integer numberOfStates = 0; + private static final Map truthValueToDot = new HashMap<>() + {{ + put(TruthValue.TRUE, "1"); + put(TruthValue.FALSE, "0"); + put(TruthValue.UNKNOWN, "½"); + put(TruthValue.ERROR, "E"); + put(true, "1"); + put(false, "0"); + }}; public ModelVisualizerAdapterImpl(Model model, ModelVisualizerStoreAdapter storeAdapter) { this.model = model; @@ -30,22 +40,16 @@ public class ModelVisualizerAdapterImpl implements ModelVisualizerAdapter { if (arity < 1 || arity > 2) { continue; } - var interpretation = model.getInterpretation(symbol); - var valueType = symbol.valueType(); - Interpretation castInterpretation; - if (valueType == Boolean.class) { - castInterpretation = (Interpretation) interpretation; - } - // TODO: support TruthValue -// else if (valueType == TruthValue.class) { -// castInterpretation = (Interpretation) interpretation; -// } - else { - continue; - } - interpretations.put(symbol, castInterpretation); + var interpretation = (Interpretation) model.getInterpretation(symbol); + interpretations.put(symbol, interpretation); } designSpaceBuilder.append("digraph designSpace {\n"); + designSpaceBuilder.append(""" + node[ + style=filled + fillcolor=white + ] + """); } @Override @@ -60,29 +64,165 @@ public class ModelVisualizerAdapterImpl implements ModelVisualizerAdapter { @Override public 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 : interpretations.entrySet()) { var key = entry.getKey(); var arity = key.arity(); var cursor = entry.getValue().getAll(); - while (cursor.move()) { - if (arity == 1) { - var id = cursor.getKey().get(0); - sb.append("\t").append(id).append(" [label=\"").append(key.name()).append(": ").append(id) - .append("\"]\n"); - } else { - var from = cursor.getKey().get(0); - var to = cursor.getKey().get(1); - sb.append("\t").append(from).append(" -> ").append(to).append(" [label=\"").append(key.name()) - .append("\"]\n"); + 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[] typePredicateColor(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 -> typePredicateColor(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() + }; + } + @Override public String createDotForModelState(Version version) { var currentVersion = model.getState(); @@ -129,15 +269,14 @@ public class ModelVisualizerAdapterImpl implements ModelVisualizerAdapter { @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"); - + 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(" / "); + 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++) { @@ -151,12 +290,16 @@ public class ModelVisualizerAdapterImpl implements ModelVisualizerAdapter { @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 addSolution(Version state) { + addState(state); designSpaceBuilder.append(states.get(state)).append(" [shape = doublecircle]\n"); } @@ -168,8 +311,8 @@ public class ModelVisualizerAdapterImpl implements ModelVisualizerAdapter { @Override public boolean saveDesignSpace(String path) { saveDot(buildDesignSpaceDot(), path + "/designSpace.dot"); - for (var state : states.keySet()) { - saveDot(createDotForModelState(state), path + "/" + states.get(state) + ".dot"); + for (var entry : states.entrySet()) { + saveDot(createDotForModelState(entry.getKey()), path + "/" + entry.getValue() + ".dot"); } return true; } @@ -182,11 +325,10 @@ public class ModelVisualizerAdapterImpl implements ModelVisualizerAdapter { @Override public boolean renderDesignSpace(String path, FileFormat format) { for (var entry : states.entrySet()) { - var state = entry.getKey(); var stateId = entry.getValue(); - var stateDot = createDotForModelState(state); + var stateDot = createDotForModelState(entry.getKey()); saveDot(stateDot, path + "/" + stateId + ".dot"); - renderDot(stateDot, path + "/" + stateId + "." + format.getFormat()); + renderDot(stateDot, format, path + "/" + stateId + "." + format.getFormat()); } var designSpaceDot = buildDesignSpaceDot(); saveDot(designSpaceDot, path + "/designSpace.dot"); -- cgit v1.2.3-54-g00ecf