From c071a933789b11176456b553e35f335c32fb194d Mon Sep 17 00:00:00 2001 From: Tobias Weimar Date: Wed, 18 Dec 2024 15:01:43 +0100 Subject: [PATCH] Median Heuristic implemented Implemented a Median Heuristic that works similarly to the Barycenter Heuristic. Added the appropriate option called crossingMinimization.strategy: MEDIAN_LAYER_SWEEP --- .../options/CrossingMinimizationStrategy.java | 14 +- .../layered/options/InternalProperties.java | 5 + .../layered/p3order/BarycenterHeuristic.java | 48 +++-- .../alg/layered/p3order/GraphInfoHolder.java | 7 +- .../p3order/LayerSweepCrossingMinimizer.java | 4 +- .../alg/layered/p3order/MedianHeuristic.java | 200 ++++++++++++++++++ 6 files changed, 252 insertions(+), 26 deletions(-) create mode 100644 plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/p3order/MedianHeuristic.java diff --git a/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/options/CrossingMinimizationStrategy.java b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/options/CrossingMinimizationStrategy.java index 3435e83a33..f9d166513c 100644 --- a/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/options/CrossingMinimizationStrategy.java +++ b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/options/CrossingMinimizationStrategy.java @@ -32,9 +32,15 @@ public enum CrossingMinimizationStrategy implements ILayoutPhaseFactory create() { switch (this) { + // TODO add new case for median heuristic + // or replace one with median heuristic case LAYER_SWEEP: return new LayerSweepCrossingMinimizer(CrossMinType.BARYCENTER); + case MEDIAN_LAYER_SWEEP: + return new LayerSweepCrossingMinimizer(CrossMinType.MEDIAN); + case INTERACTIVE: return new InteractiveCrossingMinimizer(); diff --git a/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/options/InternalProperties.java b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/options/InternalProperties.java index c05db6165c..aea91cac6c 100644 --- a/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/options/InternalProperties.java +++ b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/options/InternalProperties.java @@ -453,6 +453,11 @@ public final class InternalProperties { */ public static final IProperty> TARGET_NODE_MODEL_ORDER = new Property<>("targetNode.modelOrder"); + /** + * The weight of a node as used by the MedianHeuristic class. + */ + public static final IProperty WEIGHT = new Property<>("medianHeuristic.weight"); + /** * Hidden default constructor. */ diff --git a/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/p3order/BarycenterHeuristic.java b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/p3order/BarycenterHeuristic.java index 76c0ea97ab..5527a086b3 100644 --- a/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/p3order/BarycenterHeuristic.java +++ b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/p3order/BarycenterHeuristic.java @@ -46,25 +46,6 @@ public class BarycenterHeuristic implements ICrossingMinimizationHeuristic { /** The Barycenter PortDistributor is used to ask for the port ranks.*/ protected final AbstractBarycenterPortDistributor portDistributor; - /** - * Constructs a Barycenter heuristic for crossing minimization. - * - * @param constraintResolver - * the constraint resolver - * @param random - * the random number generator - * @param portDistributor - * calculates the port ranks for the barycenter heuristic. - * @param graph - * current node order - */ - public BarycenterHeuristic(final ForsterConstraintResolver constraintResolver, final Random random, - final AbstractBarycenterPortDistributor portDistributor, final LNode[][] graph) { - this.constraintResolver = constraintResolver; - this.random = random; - this.portDistributor = portDistributor; - } - /** * Don't use! * Only public to be accessible by a test. @@ -75,6 +56,7 @@ public void minimizeCrossings(final List layer, final boolean preOrdered, if (randomize) { // Randomize barycenters (we don't need to update the edge count in this case; // there are no edges of interest since we're only concerned with one layer) + // simply a permutation of nodes in layer randomizeBarycenters(layer); } else { // Calculate barycenters and assign barycenters to barycenterless node groups @@ -88,6 +70,7 @@ public void minimizeCrossings(final List layer, final boolean preOrdered, ModelOrderBarycenterHeuristic.insertionSort(layer, barycenterStateComparator, (ModelOrderBarycenterHeuristic) this); } else { + // use comparator based on "<" Collections.sort(layer, barycenterStateComparator); } @@ -193,6 +176,25 @@ protected void calculateBarycenters(final List nodes, final boolean forwa } } + /** + * Constructs a Barycenter heuristic for crossing minimization. + * + * @param constraintResolver + * the constraint resolver + * @param random + * the random number generator + * @param portDistributor + * calculates the port ranks for the barycenter heuristic. + * @param graph + * current node order + */ + public BarycenterHeuristic(final ForsterConstraintResolver constraintResolver, final Random random, + final AbstractBarycenterPortDistributor portDistributor, final LNode[][] graph) { + this.constraintResolver = constraintResolver; + this.random = random; + this.portDistributor = portDistributor; + } + /** the amount of random value to add to each calculated barycenter. */ private static final float RANDOM_AMOUNT = 0.07f; @@ -210,7 +212,7 @@ protected void calculateBarycenters(final List nodes, final boolean forwa * @return a pair containing the summed port positions of the connected ports as the first, and * the number of connected edges as the second entry. */ - private void calculateBarycenter(final LNode node, final boolean forward) { + private void calculateBarycenter(final LNode node, final boolean forward) {// javadoc for this method outdated // Check if the node group's barycenter was already computed if (stateOf(node).visited) { @@ -240,7 +242,7 @@ private void calculateBarycenter(final LNode node, final boolean forward) { // Update this node group's values stateOf(node).degree += stateOf(fixedNode).degree; stateOf(node).summedWeight += stateOf(fixedNode).summedWeight; - } + } // else { } } else { stateOf(node).summedWeight += portRanks[fixedPort.id]; stateOf(node).degree++; @@ -351,10 +353,14 @@ public boolean minimizeCrossings(final LNode[][] order, final int freeLayerIndex @Override public boolean setFirstLayerOrder(final LNode[][] order, final boolean isForwardSweep) { + // if sweeping forward, startIndex = 0, else the last existing element of order int startIndex = startIndex(isForwardSweep, order.length); + // extract first layer into List List nodes = Lists.newArrayList( order[startIndex]); + // randomize nodes' barycenters minimizeCrossings(nodes, false, true, isForwardSweep); + // fill first layer with nodes int index = 0; for (LNode nodeGroup : nodes) { order[startIndex][index++] = nodeGroup; diff --git a/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/p3order/GraphInfoHolder.java b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/p3order/GraphInfoHolder.java index fc8de7395e..2c69536921 100644 --- a/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/p3order/GraphInfoHolder.java +++ b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/p3order/GraphInfoHolder.java @@ -62,7 +62,6 @@ public class GraphInfoHolder implements IInitializable { private AllCrossingsCounter crossingsCounter; private int nPorts; - /** * Create object collecting information about a graph. * @@ -76,7 +75,7 @@ public class GraphInfoHolder implements IInitializable { public GraphInfoHolder(final LGraph graph, final CrossMinType crossMinType, final List graphs) { lGraph = graph; currentNodeOrder = graph.toNodeArray(); - + // Hierarchy information. parent = lGraph.getParentNode(); hasParent = parent != null; @@ -92,7 +91,7 @@ public GraphInfoHolder(final LGraph graph, final CrossMinType crossMinType, fina layerSweepTypeDecider = new LayerSweepTypeDecider(this); List initializables = Lists.newArrayList(this, crossingsCounter, layerSweepTypeDecider, portDistributor); - + if (crossMinType == CrossMinType.BARYCENTER && !graph.getProperty(LayeredOptions.CROSSING_MINIMIZATION_FORCE_NODE_MODEL_ORDER)) { ForsterConstraintResolver constraintResolver = new ForsterConstraintResolver(currentNodeOrder); @@ -105,6 +104,8 @@ public GraphInfoHolder(final LGraph graph, final CrossMinType crossMinType, fina initializables.add(constraintResolver); crossMinimizer = new ModelOrderBarycenterHeuristic(constraintResolver, random, (AbstractBarycenterPortDistributor) portDistributor, currentNodeOrder); + } else if (crossMinType == CrossMinType.MEDIAN) { + crossMinimizer = new MedianHeuristic(random); } else { crossMinimizer = new GreedySwitchHeuristic(crossMinType, this); } diff --git a/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/p3order/LayerSweepCrossingMinimizer.java b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/p3order/LayerSweepCrossingMinimizer.java index d81f2a4332..d735b763ee 100644 --- a/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/p3order/LayerSweepCrossingMinimizer.java +++ b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/p3order/LayerSweepCrossingMinimizer.java @@ -649,7 +649,9 @@ public enum CrossMinType { /** Use one-sided GreedySwitchHeuristic. */ ONE_SIDED_GREEDY_SWITCH, /** Use two-sided GreedySwitchHeuristic. */ - TWO_SIDED_GREEDY_SWITCH + TWO_SIDED_GREEDY_SWITCH, + /** Use MedianHeuristic */ + MEDIAN } /** intermediate processing configuration. */ diff --git a/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/p3order/MedianHeuristic.java b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/p3order/MedianHeuristic.java new file mode 100644 index 0000000000..7e474eed77 --- /dev/null +++ b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/p3order/MedianHeuristic.java @@ -0,0 +1,200 @@ +/******************************************************************************* + * Copyright (c) 2024 Kiel University and others. + * + * This program and the accompanying materials are made available under the + * terms of the Eclipse Public License 2.0 which is available at + * http://www.eclipse.org/legal/epl-2.0. + * + * SPDX-License-Identifier: EPL-2.0 + *******************************************************************************/ +package org.eclipse.elk.alg.layered.p3order; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Comparator; +import java.util.List; +import java.util.Random; + +import org.eclipse.elk.alg.layered.graph.LEdge; +import org.eclipse.elk.alg.layered.graph.LNode; +import org.eclipse.elk.alg.layered.options.InternalProperties; +import org.eclipse.elk.alg.layered.p3order.options.MedianHeuristicProperties; + +import com.google.common.collect.Lists; + +/** + * @author tobias + * + */ +public class MedianHeuristic implements ICrossingMinimizationHeuristic { + + /** the random number generator. */ + protected final Random random; + + public MedianHeuristic(Random random) { + this.random = random; + Math.random(); + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.elk.alg.layered.p3order.ICrossingMinimizationHeuristic#alwaysImproves() + */ + @Override + public boolean alwaysImproves() { + return false; + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.elk.alg.layered.p3order.ICrossingMinimizationHeuristic#setFirstLayerOrder(org.eclipse.elk.alg.layered + * .graph.LNode[][], boolean) + */ + @Override + public boolean setFirstLayerOrder(LNode[][] order, boolean forwardSweep) { + // determine first index ( + int firstIndex = forwardSweep ? 0 : Math.max(0, order.length - 1); + // extract firstLayer from 2D-array + List firstLayer = Lists.newArrayList(order[firstIndex]); + // set random weights for nodes in firstLayer + for (LNode node : firstLayer) { + node.setProperty(MedianHeuristicProperties.WEIGHT, random.nextDouble()); + } + // sort firstLayer by their (randomized) weights + // Collections.sort() is order-preserving (stable) + Collections.sort(firstLayer, weightComparator); + // insert nodes back into array + int index = 0; + for (LNode node : firstLayer) { + order[firstIndex][index++] = node; + // overwrite the weight with an integer from 1 to n + node.setProperty(MedianHeuristicProperties.WEIGHT, (double) index); + // should the node's id be set as well? + } + + return false; + } + + /* + * (non-Javadoc) + * + * @see + * org.eclipse.elk.alg.layered.p3order.ICrossingMinimizationHeuristic#minimizeCrossings(org.eclipse.elk.alg.layered. + * graph.LNode[][], int, boolean, boolean) + */ + @Override + public boolean minimizeCrossings(LNode[][] order, int freeLayerIndex, boolean forwardSweep, boolean isFirstSweep) { + List freeLayer = Lists.newArrayList(order[freeLayerIndex]); + // calculate Medians for the free Layer (does not sort the free layer) + calculateMedians(freeLayer, forwardSweep ? freeLayerIndex - 1 : freeLayerIndex + 1); + // sort the free Layer + Collections.sort(freeLayer, weightComparator); + // and insert the free Layer back into the array + int index = 0; + for (LNode node : freeLayer) { + order[freeLayerIndex][index++] = node; + // should the node's id be set as well? + } + return false; + } + + /** + * calculates the medians and writes them into the nodes as a property + * + * @param nodes + * the list of nodes for which medians should be calculated, all in one layer + * @param referenceLayer + * the Layer from which nodes should be taken into account when calculating medians + */ + private void calculateMedians(List nodes, int referenceLayer) { + // minimum and maximum weight in free layer + double minWeight = Double.MIN_VALUE; + double maxWeight = Double.MAX_VALUE; + // a list to be filled with nodes whose weights cannot be calculated from the referenceLayer alone. + List toRevisit = new ArrayList<>(); + + // iterate through nodes + for (LNode node : nodes) { + List connectedNodes = new ArrayList<>(); + // for every outgoing and incoming edge + for (LEdge edge : node.getIncomingEdges()) { + // add the adjacent node's weight to the weight list + LNode target = edge.getTarget().getNode(); + LNode source = edge.getSource().getNode(); + // TODO are the layer ids reliably set? + if (target.getLayer().id == referenceLayer) { + connectedNodes.add(target); + } + if (source.getLayer().id == referenceLayer) { + connectedNodes.add(source); + } + } + for (LEdge edge : node.getOutgoingEdges()) { + LNode target = edge.getTarget().getNode(); + LNode source = edge.getSource().getNode(); + if (target.getLayer().id == referenceLayer) { + connectedNodes.add(target); + } + if (source.getLayer().id == referenceLayer) { + connectedNodes.add(source); + } + } + // if the node's weight cannot be determined from referenceLayer (no connected nodes in that layer), save + // this node in order to revisit it later + if (connectedNodes.isEmpty()) { + toRevisit.add(node); + } else { + // Collections.sort() is stable + // therefore, no sort of normalization is needed + // nodes will not keep switching places if they have the same weight + Collections.sort(connectedNodes, weightComparator); + // calculate weight from the median of connected nodes' weights + double newWeight = + connectedNodes.get(connectedNodes.size() / 2).getProperty(MedianHeuristicProperties.WEIGHT); + node.setProperty(InternalProperties.WEIGHT, newWeight); + // update minWeight and maxWeight + minWeight = Math.min(minWeight, newWeight); + maxWeight = Math.max(maxWeight, newWeight); + } + } + // if no nodes had any weight, avgWeight = 0 + double avgWeight = (maxWeight + minWeight) / 2.0; + // go through yet unvisited nodes and set their weight to the layer's average + for (LNode n : toRevisit) { + n.setProperty(InternalProperties.WEIGHT, avgWeight); + } + } + + /* + * (non-Javadoc) + * + * @see org.eclipse.elk.alg.layered.p3order.ICrossingMinimizationHeuristic#isDeterministic() + */ + @Override + public boolean isDeterministic() { + return true; + } + + /** + * Compares two {@link LNode}s based on their weights. Assume that both nodes have weights set. + */ + protected Comparator weightComparator = (n1, n2) -> { + Double w1 = n1.getProperty(InternalProperties.WEIGHT); + Double w2 = n2.getProperty(InternalProperties.WEIGHT); + if (w1 != null && w2 != null) { + return w1.compareTo(w2); + } + // everything from here should not occur as weightComparator will only be called once weights have been set + // will it?? + else if (w1 != null) { + return -1; + } else if (w2 != null) { + return 1; + } + return 0; + }; + +}