diff --git a/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/Layered.melk b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/Layered.melk index b3d9d094be..3db305361c 100644 --- a/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/Layered.melk +++ b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/Layered.melk @@ -201,6 +201,7 @@ algorithm layered(LayeredLayoutProvider) { supports org.eclipse.elk.interactiveLayout supports layering.layerId supports crossingMinimization.positionId + supports considerModelOrder.strategy supports considerModelOrder.longEdgeStrategy supports considerModelOrder.crossingCounterNodeInfluence @@ -208,6 +209,15 @@ algorithm layered(LayeredLayoutProvider) { supports considerModelOrder.noModelOrder supports considerModelOrder.components supports considerModelOrder.portModelOrder + supports considerModelOrder.groupModelOrder.cycleBreakingId + supports considerModelOrder.groupModelOrder.crossingMinimizationId + supports considerModelOrder.groupModelOrder.componentGroupId + supports considerModelOrder.groupModelOrder.cbGroupOrderStrategy + supports considerModelOrder.groupModelOrder.cmGroupOrderStrategy + supports considerModelOrder.groupModelOrder.cmEnforcedGroupOrders + supports considerModelOrder.groupModelOrder.cbPreferredSourceId + supports considerModelOrder.groupModelOrder.cbPreferredTargetId + supports generatePositionAndLayerIds } @@ -1078,6 +1088,73 @@ group considerModelOrder { targets parents requires org.eclipse.elk.alg.layered.considerModelOrder.strategy } + + group groupModelOrder { + option cycleBreakingId: int { + label "Group ID of the Node Type" + description + "Used to define partial ordering groups during cycle breaking. A lower group id means that the group is + sorted before other groups. A group model order of 0 is the default group." + default = 0 + targets nodes + requires noModelOrder == false + } + option crossingMinimizationId: int { + label "Group ID of the Node Type" + description + "Used to define partial ordering groups during crossing minimization. A lower group id means that the group is + sorted before other groups. A group model order of 0 is the default group." + default = 0 + targets nodes, edges, ports + requires noModelOrder == false + } + option componentGroupId: int { + label "Group ID of the Node Type" + description + "Used to define partial ordering groups during component packing. A lower group id means that the group is + sorted before other groups. A group model order of 0 is the default group." + default = 0 + targets nodes, edges, ports + requires noModelOrder == false + } + option cbGroupOrderStrategy: GroupOrderStrategy { + label "Cycle Breaking Group Ordering Strategy" + description "Determines how to count ordering violations during cycle breaking. NONE: They do not + count. ENFORCED: A group with a higher model order is before a node with a smaller. + MODEL_ORDER: The model order counts instead of the model order group id ordering." + default = GroupOrderStrategy.ONLY_WITHIN_GROUP + targets parents + } + option cbPreferredSourceId: int { + label "Cycle Breaking Preferred Source Id" + description "The model order group id for which should be preferred as a source if possible." + targets parents + requires cycleBreaking.strategy == CycleBreakingStrategy.SCC_NODE_TYPE + } + option cbPreferredTargetId: int { + label "Cycle Breaking Preferred Target Id" + description "The model order group id for which should be preferred as a target if possible." + targets parents + requires cycleBreaking.strategy == CycleBreakingStrategy.SCC_NODE_TYPE + } + option cmGroupOrderStrategy: GroupOrderStrategy { + label "Crossing Minimization Group Ordering Strategy" + description "Determines how to count ordering violations during crossing minimization. NONE: They do not + count. ENFORCED: A group with a lower id is before a group with a higher id. + MODEL_ORDER: The model order counts instead of the model order group id ordering." + default = GroupOrderStrategy.ONLY_WITHIN_GROUP + targets parents + } + option cmEnforcedGroupOrders: List { + label "Crossing Minimization Enforced Group Orders" + description "Holds all group ids which are enforcing their order during crossing minimization strategies. + E.g. if only groups 2 and -1 (default) enforce their ordering. Other groups e.g. the group of + timer nodes can be ordered arbitrarily if it helps and the mentioned groups may not change + their order." + targets parents + default = #[1, 2, 6, 7, 10, 11] + } + } } advanced option directionCongruency: DirectionCongruency { diff --git a/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/graph/Tarjan.java b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/graph/Tarjan.java new file mode 100644 index 0000000000..8b3ee12da9 --- /dev/null +++ b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/graph/Tarjan.java @@ -0,0 +1,110 @@ +/******************************************************************************* + * Copyright (c) 2025 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.graph; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.Stack; + +import org.eclipse.elk.alg.layered.options.InternalProperties; + +/** + * Tarjan implementation to be used during layered layout. + */ + + +public class Tarjan { + + public Tarjan (List edgesToBeReversed, List> stronglyConnectedComponents, + HashMap nodeToSCCID) { + this.edgesToBeReversed = edgesToBeReversed; + this.stronglyConnectedComponents = stronglyConnectedComponents; + this.nodeToSCCID = nodeToSCCID; + } + + private List edgesToBeReversed; + private int index = 0; + protected List> stronglyConnectedComponents; // FIXME Why no ordered set here? this is bad + private Stack stack = new Stack(); + private HashMap nodeToSCCID = new HashMap<>(); + + public void tarjan(final LGraph graph) { + index = 0; + stack = new Stack(); + for (LNode node : graph.getLayerlessNodes()) { + if (node.getProperty(InternalProperties.TARJAN_ID) == -1) { + stronglyConnected(node); + stack.clear(); + } + } + } + + public void stronglyConnected(final LNode v) { + v.setProperty(InternalProperties.TARJAN_ID, index); + v.setProperty(InternalProperties.TARJAN_LOWLINK, index); + index++; + stack.push(v); + v.setProperty(InternalProperties.TARJAN_ON_STACK, true); + for (LEdge edge : v.getConnectedEdges()) { + if (edge.getSource().getNode() != v && !edgesToBeReversed.contains(edge)) { + continue; + } + if (edge.getSource().getNode() == v && edgesToBeReversed.contains(edge)) { + continue; + } + LNode target = null; + if (edge.getTarget().getNode() == v) { + target = edge.getSource().getNode(); + } else { + target = edge.getTarget().getNode(); + } + if (target.getProperty(InternalProperties.TARJAN_ID) == -1) { + stronglyConnected(target); + v.setProperty(InternalProperties.TARJAN_LOWLINK, + Math.min(v.getProperty(InternalProperties.TARJAN_LOWLINK), + target.getProperty(InternalProperties.TARJAN_LOWLINK))); + } else if (target.getProperty(InternalProperties.TARJAN_ON_STACK)) { + v.setProperty(InternalProperties.TARJAN_LOWLINK, + Math.min(v.getProperty(InternalProperties.TARJAN_LOWLINK), + target.getProperty(InternalProperties.TARJAN_ID))); + } + } + if (v.getProperty(InternalProperties.TARJAN_LOWLINK) == v.getProperty(InternalProperties.TARJAN_ID)) { + Set sCC = new HashSet(); + LNode n = null; + do { + n = stack.pop(); + n.setProperty(InternalProperties.TARJAN_ON_STACK, false); + sCC.add(n); + } while (v != n); + if (sCC.size() >1) { + int index = stronglyConnectedComponents.size(); + stronglyConnectedComponents.add(sCC); + for (LNode node : sCC) { + nodeToSCCID.put(node, index); + } + } + } + } + + public void resetTarjan(final LGraph graph) { + for (LNode n : graph.getLayerlessNodes()) { + n.setProperty(InternalProperties.TARJAN_ON_STACK, false); + n.setProperty(InternalProperties.TARJAN_LOWLINK, -1); + n.setProperty(InternalProperties.TARJAN_ID, -1); + stack.clear(); + for (LEdge e : n.getConnectedEdges()) { + e.setProperty(InternalProperties.IS_PART_OF_CYCLE, false); + } + } + } +} diff --git a/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/graph/transform/ElkGraphImporter.java b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/graph/transform/ElkGraphImporter.java index a224e60754..19938f7c69 100644 --- a/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/graph/transform/ElkGraphImporter.java +++ b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/graph/transform/ElkGraphImporter.java @@ -10,6 +10,7 @@ package org.eclipse.elk.alg.layered.graph.transform; import java.util.EnumSet; +import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; @@ -228,15 +229,24 @@ private void calculateMinimumGraphSize(final ElkNode elkgraph, final LGraph lgra private void importFlatGraph(final ElkNode elkgraph, final LGraph lgraph) { // Transform the node's children, unless we're told not to int index = 0; + HashSet cbGroupModelOrders = new HashSet<>(); for (ElkNode child : elkgraph.getChildren()) { if (!child.getProperty(LayeredOptions.NO_LAYOUT)) { if (needsModelOrder(child)) { child.setProperty(InternalProperties.MODEL_ORDER, index); index++; + if (child.hasProperty(LayeredOptions.CONSIDER_MODEL_ORDER_GROUP_MODEL_ORDER_CYCLE_BREAKING_ID)) { + cbGroupModelOrders.add(child.getProperty(LayeredOptions.CONSIDER_MODEL_ORDER_GROUP_MODEL_ORDER_CYCLE_BREAKING_ID)); + } } transformNode(child, lgraph); } } + // Save the maximum node model order. + // This is relevant to create graph partitions based on model order and constraints. + lgraph.setProperty(InternalProperties.MAX_MODEL_ORDER_NODES, index); + // Save the number of model order groups. + lgraph.setProperty(InternalProperties.CB_NUM_MODEL_ORDER_GROUPS, cbGroupModelOrders.size()); // iterate the list of contained edges to preserve the 'input order' of the edges // (this is not part of the previous loop since all children must have already been transformed) @@ -298,6 +308,7 @@ private void importHierarchicalGraph(final ElkNode elkgraph, final LGraph lgraph // Model order index for nodes int index = 0; + HashSet cbGroupModelOrders = new HashSet<>(); // Transform the node's children elkGraphQueue.addAll(elkgraph.getChildren()); while (!elkGraphQueue.isEmpty()) { @@ -306,6 +317,9 @@ private void importHierarchicalGraph(final ElkNode elkgraph, final LGraph lgraph if (needsModelOrder(elknode)) { // Assign a model order to the nodes as they are read elknode.setProperty(InternalProperties.MODEL_ORDER, index++); + if (elknode.hasProperty(LayeredOptions.CONSIDER_MODEL_ORDER_GROUP_MODEL_ORDER_CYCLE_BREAKING_ID)) { + cbGroupModelOrders.add(elknode.getProperty(LayeredOptions.CONSIDER_MODEL_ORDER_GROUP_MODEL_ORDER_CYCLE_BREAKING_ID)); + } } // Check if the current node is to be laid out in the first place @@ -358,6 +372,11 @@ private void importHierarchicalGraph(final ElkNode elkgraph, final LGraph lgraph } } } + // Save the maximum node model order. + // This is relevant to create graph partitions based on model order and constraints. + lgraph.setProperty(InternalProperties.MAX_MODEL_ORDER_NODES, index); + // Save the number of model order groups. + lgraph.setProperty(InternalProperties.CB_NUM_MODEL_ORDER_GROUPS, cbGroupModelOrders.size()); // Model order index for edges. index = 0; @@ -458,22 +477,29 @@ private boolean needsModelOrder(final ElkNode child) { * @return True, if model order should be set. */ private boolean needsModelOrderBasedOnParent(final ElkNode elkgraph) { - return (elkgraph.getProperty(LayeredOptions.CONSIDER_MODEL_ORDER_STRATEGY) != OrderingStrategy.NONE - || elkgraph.getProperty(LayeredOptions.CYCLE_BREAKING_STRATEGY) == CycleBreakingStrategy.MODEL_ORDER - || elkgraph - .getProperty(LayeredOptions.CYCLE_BREAKING_STRATEGY) == CycleBreakingStrategy.GREEDY_MODEL_ORDER + + boolean modelOrderCycleBreaking = elkgraph.getProperty(LayeredOptions.CYCLE_BREAKING_STRATEGY) == CycleBreakingStrategy.MODEL_ORDER + || elkgraph.getProperty(LayeredOptions.CYCLE_BREAKING_STRATEGY) == CycleBreakingStrategy.BFS_NODE_ORDER + || elkgraph.getProperty(LayeredOptions.CYCLE_BREAKING_STRATEGY) == CycleBreakingStrategy.DFS_NODE_ORDER + || elkgraph.getProperty(LayeredOptions.CYCLE_BREAKING_STRATEGY) == CycleBreakingStrategy.GREEDY_MODEL_ORDER + || elkgraph.getProperty(LayeredOptions.CYCLE_BREAKING_STRATEGY) == CycleBreakingStrategy.SCC_CONNECTIVITY + || elkgraph.getProperty(LayeredOptions.CYCLE_BREAKING_STRATEGY) == CycleBreakingStrategy.SCC_NODE_TYPE; + boolean modelOrderLayering = elkgraph.getProperty(LayeredOptions.LAYERING_STRATEGY) == LayeringStrategy.BF_MODEL_ORDER + || elkgraph.getProperty(LayeredOptions.LAYERING_STRATEGY) == LayeringStrategy.DF_MODEL_ORDER + || elkgraph.getProperty(LayeredOptions.LAYERING_NODE_PROMOTION_STRATEGY) == NodePromotionStrategy.MODEL_ORDER_LEFT_TO_RIGHT + || elkgraph.getProperty(LayeredOptions.LAYERING_NODE_PROMOTION_STRATEGY) == NodePromotionStrategy.MODEL_ORDER_RIGHT_TO_LEFT; + boolean modelOrderCrossingMinimization = + // Maybe add the explicit strategies here + elkgraph.getProperty(LayeredOptions.CONSIDER_MODEL_ORDER_STRATEGY) != OrderingStrategy.NONE || elkgraph.getProperty(LayeredOptions.CROSSING_MINIMIZATION_FORCE_NODE_MODEL_ORDER) - || elkgraph - .getProperty(LayeredOptions.CONSIDER_MODEL_ORDER_COMPONENTS) != ComponentOrderingStrategy.NONE) - || elkgraph.getProperty( - LayeredOptions.LAYERING_NODE_PROMOTION_STRATEGY) == NodePromotionStrategy.MODEL_ORDER_LEFT_TO_RIGHT - || elkgraph.getProperty( - LayeredOptions.LAYERING_NODE_PROMOTION_STRATEGY) == NodePromotionStrategy.MODEL_ORDER_RIGHT_TO_LEFT - || elkgraph.getProperty( - LayeredOptions.LAYERING_STRATEGY) == LayeringStrategy.BF_MODEL_ORDER - || elkgraph.getProperty( - LayeredOptions.LAYERING_STRATEGY) == LayeringStrategy.DF_MODEL_ORDER; - } + // Maybe add the explicit strategies here + || elkgraph.getProperty(LayeredOptions.CONSIDER_MODEL_ORDER_COMPONENTS) != ComponentOrderingStrategy.NONE + + || elkgraph.getProperty(LayeredOptions.CONSIDER_MODEL_ORDER_CROSSING_COUNTER_NODE_INFLUENCE) != 0 + || elkgraph.getProperty(LayeredOptions.CONSIDER_MODEL_ORDER_CROSSING_COUNTER_PORT_INFLUENCE) != 0; + return modelOrderCycleBreaking || modelOrderLayering || modelOrderCrossingMinimization; + } + /** * Checks if the given node has any inside self loops. diff --git a/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/intermediate/SortByInputModelProcessor.java b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/intermediate/SortByInputModelProcessor.java index 969bf751b5..c54c390709 100644 --- a/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/intermediate/SortByInputModelProcessor.java +++ b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/intermediate/SortByInputModelProcessor.java @@ -62,9 +62,10 @@ public void process(final LGraph graph, final IElkProgressMonitor progressMonito final int previousLayerIndex = layerIndex == 0 ? 0 : layerIndex - 1; Layer previousLayer = graph.getLayers().get(previousLayerIndex); // Sort nodes before port sorting to have sorted nodes for in-layer feedback edge dummies. - ModelOrderNodeComparator comparator = new ModelOrderNodeComparator(previousLayer, + ModelOrderNodeComparator comparator = new ModelOrderNodeComparator(graph, previousLayer, graph.getProperty(LayeredOptions.CONSIDER_MODEL_ORDER_STRATEGY), - graph.getProperty(LayeredOptions.CONSIDER_MODEL_ORDER_LONG_EDGE_STRATEGY), true); + graph.getProperty(LayeredOptions.CONSIDER_MODEL_ORDER_LONG_EDGE_STRATEGY), + graph.getProperty(LayeredOptions.CONSIDER_MODEL_ORDER_GROUP_MODEL_ORDER_CM_GROUP_ORDER_STRATEGY), true); SortByInputModelProcessor.insertionSort(layer.getNodes(), comparator); for (LNode node : layer.getNodes()) { if (node.getProperty(LayeredOptions.PORT_CONSTRAINTS) != PortConstraints.FIXED_ORDER @@ -75,7 +76,7 @@ public void process(final LGraph graph, final IElkProgressMonitor progressMonito // (their minimal) model order. // Get minimal model order for target node Collections.sort(node.getPorts(), - new ModelOrderPortComparator(previousLayer, + new ModelOrderPortComparator(graph, previousLayer, graph.getProperty(LayeredOptions.CONSIDER_MODEL_ORDER_STRATEGY), longEdgeTargetNodePreprocessing(node), graph.getProperty(LayeredOptions.CONSIDER_MODEL_ORDER_PORT_MODEL_ORDER))); @@ -83,9 +84,10 @@ public void process(final LGraph graph, final IElkProgressMonitor progressMonito } } // Sort nodes after port sorting to also sort dummy feedback nodes from the current layer correctly. - comparator = new ModelOrderNodeComparator(previousLayer, + comparator = new ModelOrderNodeComparator(graph, previousLayer, graph.getProperty(LayeredOptions.CONSIDER_MODEL_ORDER_STRATEGY), - graph.getProperty(LayeredOptions.CONSIDER_MODEL_ORDER_LONG_EDGE_STRATEGY), false); + graph.getProperty(LayeredOptions.CONSIDER_MODEL_ORDER_LONG_EDGE_STRATEGY), + graph.getProperty(LayeredOptions.CONSIDER_MODEL_ORDER_GROUP_MODEL_ORDER_CM_GROUP_ORDER_STRATEGY), false); SortByInputModelProcessor.insertionSort(layer.getNodes(), comparator); progressMonitor.log("Layer " + layerIndex + ": " + layer); diff --git a/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/intermediate/preserveorder/CMGroupModelOrderCalculator.java b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/intermediate/preserveorder/CMGroupModelOrderCalculator.java new file mode 100644 index 0000000000..0a85f48151 --- /dev/null +++ b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/intermediate/preserveorder/CMGroupModelOrderCalculator.java @@ -0,0 +1,48 @@ +/******************************************************************************* + * Copyright (c) 2025 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.intermediate.preserveorder; + +import java.util.List; + +import org.eclipse.elk.alg.layered.graph.LGraph; +import org.eclipse.elk.alg.layered.graph.LGraphElement; +import org.eclipse.elk.alg.layered.options.GroupOrderStrategy; +import org.eclipse.elk.alg.layered.options.InternalProperties; +import org.eclipse.elk.alg.layered.options.LayeredOptions; + +/** + * Helper class to calculate the model order and group model order for crossing minimization. + */ +public class CMGroupModelOrderCalculator { + + public static int calculateModelOrderOrGroupModelOrder(LGraphElement element, LGraphElement other, LGraph parent, int offset) { + boolean enforceGroupModelOrder = parent.getProperty( + LayeredOptions.CONSIDER_MODEL_ORDER_GROUP_MODEL_ORDER_CM_GROUP_ORDER_STRATEGY) == GroupOrderStrategy.ENFORCED; + List enforcedOrders = parent.getProperty(LayeredOptions.CONSIDER_MODEL_ORDER_GROUP_MODEL_ORDER_CM_ENFORCED_GROUP_ORDERS); + if (!element.hasProperty(InternalProperties.MODEL_ORDER)) { + return -1; + } else if (enforceGroupModelOrder) { + // Check whether the order between both is enforced. Otherwise use model order. + if (enforcedOrders.contains(element.getProperty(LayeredOptions.CONSIDER_MODEL_ORDER_GROUP_MODEL_ORDER_CROSSING_MINIMIZATION_ID)) + && enforcedOrders.contains(other.getProperty(LayeredOptions.CONSIDER_MODEL_ORDER_GROUP_MODEL_ORDER_CROSSING_MINIMIZATION_ID))) { + // This means that we need to calculate the model order by groupId * number of nodes/port/edges + real model order. + return offset + * element.getProperty(LayeredOptions.CONSIDER_MODEL_ORDER_GROUP_MODEL_ORDER_CROSSING_MINIMIZATION_ID) + + element.getProperty(InternalProperties.MODEL_ORDER); + } + // Fallthrough case + } else { + // Case only model order. + return element.getProperty(InternalProperties.MODEL_ORDER); + } + return element.getProperty(InternalProperties.MODEL_ORDER); + } + +} diff --git a/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/intermediate/preserveorder/ModelOrderNodeComparator.java b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/intermediate/preserveorder/ModelOrderNodeComparator.java index d8750c6052..41e481e5ce 100644 --- a/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/intermediate/preserveorder/ModelOrderNodeComparator.java +++ b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/intermediate/preserveorder/ModelOrderNodeComparator.java @@ -14,10 +14,12 @@ import java.util.HashSet; import org.eclipse.elk.alg.layered.graph.LEdge; +import org.eclipse.elk.alg.layered.graph.LGraph; import org.eclipse.elk.alg.layered.graph.LNode; import org.eclipse.elk.alg.layered.graph.LNode.NodeType; import org.eclipse.elk.alg.layered.graph.LPort; import org.eclipse.elk.alg.layered.graph.Layer; +import org.eclipse.elk.alg.layered.options.GroupOrderStrategy; import org.eclipse.elk.alg.layered.options.InternalProperties; import org.eclipse.elk.alg.layered.options.LongEdgeOrderingStrategy; import org.eclipse.elk.alg.layered.options.OrderingStrategy; @@ -33,11 +35,21 @@ public class ModelOrderNodeComparator implements Comparator { */ private LNode[] previousLayer; + /** + * The graph. + */ + private LGraph graph; + /** * The ordering strategy. */ private final OrderingStrategy orderingStrategy; + /** + * The group ordering strategy. + */ + private final GroupOrderStrategy groupOrderStrategy; + /** * Each node has an entry of nodes for which it is bigger. */ @@ -64,9 +76,10 @@ public class ModelOrderNodeComparator implements Comparator { * @param orderingStrategy The ordering strategy * @param longEdgeOrderingStrategy The strategy to order dummy nodes and nodes with no connection the previous layer */ - public ModelOrderNodeComparator(final Layer thePreviousLayer, final OrderingStrategy orderingStrategy, - final LongEdgeOrderingStrategy longEdgeOrderingStrategy, boolean beforePorts) { - this(orderingStrategy, longEdgeOrderingStrategy, beforePorts); + public ModelOrderNodeComparator(final LGraph graph, final Layer thePreviousLayer, final OrderingStrategy orderingStrategy, + final LongEdgeOrderingStrategy longEdgeOrderingStrategy, GroupOrderStrategy groupOrderStrategy, + boolean beforePorts) { + this(graph, orderingStrategy, longEdgeOrderingStrategy, groupOrderStrategy, beforePorts); this.previousLayer = new LNode[thePreviousLayer.getNodes().size()]; thePreviousLayer.getNodes().toArray(this.previousLayer); } @@ -78,15 +91,19 @@ public ModelOrderNodeComparator(final Layer thePreviousLayer, final OrderingStra * @param orderingStrategy The ordering strategy * @param longEdgeOrderingStrategy The strategy to order dummy nodes and nodes with no connection the previous layer */ - public ModelOrderNodeComparator(final LNode[] previousLayer, final OrderingStrategy orderingStrategy, - final LongEdgeOrderingStrategy longEdgeOrderingStrategy, boolean beforePorts) { - this(orderingStrategy, longEdgeOrderingStrategy, beforePorts); + public ModelOrderNodeComparator(final LGraph graph, final LNode[] previousLayer, final OrderingStrategy orderingStrategy, + final LongEdgeOrderingStrategy longEdgeOrderingStrategy, final GroupOrderStrategy groupOrderStrategy, + boolean beforePorts) { + this(graph, orderingStrategy, longEdgeOrderingStrategy, groupOrderStrategy, beforePorts); this.previousLayer = previousLayer; } - private ModelOrderNodeComparator(final OrderingStrategy orderingStrategy, - final LongEdgeOrderingStrategy longEdgeOrderingStrategy, boolean beforePorts) { + private ModelOrderNodeComparator(final LGraph graph, final OrderingStrategy orderingStrategy, + final LongEdgeOrderingStrategy longEdgeOrderingStrategy, final GroupOrderStrategy groupOrderStrategy, + boolean beforePorts) { + this.graph = graph; this.orderingStrategy = orderingStrategy; + this.groupOrderStrategy = groupOrderStrategy; this.longEdgeNodeOrder = longEdgeOrderingStrategy; this.beforePorts = beforePorts; } @@ -113,6 +130,7 @@ public int compare(final LNode n1, final LNode n2) { } else if (biggerThan.get(n2).contains(n1)) { return 1; } + // If no model order is set, the one node is a dummy node and the nodes should be ordered // by the connected edges. // This kind of ordering should be preferred, if the order of the edges has priority. @@ -125,6 +143,7 @@ public int compare(final LNode n1, final LNode n2) { if (!p.getIncomingEdges().isEmpty()) { if (p.getIncomingEdges().get(0).getSource().getNode().getLayer().id == (n1.getLayer().id - 1)) { p1SourcePort = p.getIncomingEdges().get(0).getSource(); + break; } } } @@ -135,6 +154,7 @@ public int compare(final LNode n1, final LNode n2) { if (!p.getIncomingEdges().isEmpty()) { if (p.getIncomingEdges().get(0).getSource().getNode().getLayer().id == (n2.getLayer().id - 1)) { p2SourcePort = p.getIncomingEdges().get(0).getSource(); + break; } } } @@ -239,8 +259,9 @@ public int compare(final LNode n1, final LNode n2) { // Order nodes by their order in the model. // This is also the fallback case if one of the nodes is not connected to the previous layer. if (n1.hasProperty(InternalProperties.MODEL_ORDER) && n2.hasProperty(InternalProperties.MODEL_ORDER)) { - int n1ModelOrder = n1.getProperty(InternalProperties.MODEL_ORDER); - int n2ModelOrder = n2.getProperty(InternalProperties.MODEL_ORDER); + // Make a decision on group order if possible + int n1ModelOrder = CMGroupModelOrderCalculator.calculateModelOrderOrGroupModelOrder(n1, n2, graph, graph.getProperty(InternalProperties.MAX_MODEL_ORDER_NODES)); + int n2ModelOrder = CMGroupModelOrderCalculator.calculateModelOrderOrGroupModelOrder(n2, n1, graph, graph.getProperty(InternalProperties.MAX_MODEL_ORDER_NODES)); if (n1ModelOrder > n2ModelOrder) { updateBiggerAndSmallerAssociations(n1, n2); return 1; @@ -268,6 +289,7 @@ private int getModelOrderFromConnectedEdges(final LNode n) { if (sourcePort != null) { LEdge edge = sourcePort.getIncomingEdges().get(0); if (edge != null) { + // FIX ME I guess I should use group model order here. return edge.getProperty(InternalProperties.MODEL_ORDER); } } @@ -411,7 +433,7 @@ private int handleHelperDummyNodes(LNode n1, LNode n2) { // If both are target, I should not have this problem and can never be here, since these nodes // should have a previous layer node. if (n1SourceFeedbackNode && n2SourceFeedbackNode) { - int returnValue = new ModelOrderPortComparator(previousLayer, orderingStrategy, null, n2TargetFeedbackNode) + int returnValue = new ModelOrderPortComparator(graph, previousLayer, orderingStrategy, null, n2TargetFeedbackNode) .compare(n1dummyNodeSourcePort, n2dummyNodeSourcePort); if (returnValue > 0) { updateBiggerAndSmallerAssociations(n2, n1); diff --git a/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/intermediate/preserveorder/ModelOrderPortComparator.java b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/intermediate/preserveorder/ModelOrderPortComparator.java index d16d6277de..3974999bad 100644 --- a/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/intermediate/preserveorder/ModelOrderPortComparator.java +++ b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/intermediate/preserveorder/ModelOrderPortComparator.java @@ -16,10 +16,11 @@ import java.util.Map; import java.util.stream.Collectors; +import org.eclipse.elk.alg.layered.graph.LGraph; import org.eclipse.elk.alg.layered.graph.LNode; +import org.eclipse.elk.alg.layered.graph.LNode.NodeType; import org.eclipse.elk.alg.layered.graph.LPort; import org.eclipse.elk.alg.layered.graph.Layer; -import org.eclipse.elk.alg.layered.graph.LNode.NodeType; import org.eclipse.elk.alg.layered.options.InternalProperties; import org.eclipse.elk.alg.layered.options.OrderingStrategy; import org.eclipse.elk.core.options.PortSide; @@ -49,6 +50,11 @@ public class ModelOrderPortComparator implements Comparator { * The previous layer. */ private LNode[] previousLayer; + + /** + * The graph. + */ + private LGraph graph; private OrderingStrategy strategy; @@ -67,8 +73,9 @@ public class ModelOrderPortComparator implements Comparator { * @param previousLayer The previous layer * @param targetNodeModelOrder The minimal model order connecting to a target node. */ - public ModelOrderPortComparator(final Layer previousLayer, final OrderingStrategy strategy, + public ModelOrderPortComparator(final LGraph graph, final Layer previousLayer, final OrderingStrategy strategy, final Map targetNodeModelOrder, final boolean portModelOrder) { + this.graph = graph; this.previousLayer = new LNode[previousLayer.getNodes().size()]; this.strategy = strategy; previousLayer.getNodes().toArray(this.previousLayer); @@ -82,8 +89,9 @@ public ModelOrderPortComparator(final Layer previousLayer, final OrderingStrateg * @param previousLayer The previous layer * @param targetNodeModelOrder The minimal model order connecting to a target node. */ - public ModelOrderPortComparator(final LNode[] previousLayer, final OrderingStrategy strategy, + public ModelOrderPortComparator(final LGraph graph, final LNode[] previousLayer, final OrderingStrategy strategy, final Map targetNodeModelOrder, final boolean portModelOrder) { + this.graph = graph; this.previousLayer = previousLayer; this.strategy = strategy; this.targetNodeModelOrder = targetNodeModelOrder; @@ -228,8 +236,10 @@ public int compare(final LPort originalP1, final LPort originalP2) { if (this.strategy == OrderingStrategy.PREFER_NODES && p1TargetNode != null && p2TargetNode != null && p1TargetNode.hasProperty(InternalProperties.MODEL_ORDER) && p2TargetNode.hasProperty(InternalProperties.MODEL_ORDER)) { - int p1MO = p1TargetNode.getProperty(InternalProperties.MODEL_ORDER); - int p2MO = p2TargetNode.getProperty(InternalProperties.MODEL_ORDER); + int p1MO = CMGroupModelOrderCalculator.calculateModelOrderOrGroupModelOrder(p1TargetNode, p2TargetNode, + graph, graph.getProperty(InternalProperties.MAX_MODEL_ORDER_NODES)); + int p2MO = CMGroupModelOrderCalculator.calculateModelOrderOrGroupModelOrder(p2TargetNode, p1TargetNode, + graph, graph.getProperty(InternalProperties.MAX_MODEL_ORDER_NODES)); if (p1MO > p2MO) { updateBiggerAndSmallerAssociations(p1, p2, reverseOrder); return reverseOrder; @@ -258,10 +268,12 @@ public int compare(final LPort originalP1, final LPort originalP2) { int p1Order = 0; int p2Order = 0; if (p1.getOutgoingEdges().get(0).hasProperty(InternalProperties.MODEL_ORDER)) { - p1Order = p1.getOutgoingEdges().get(0).getProperty(InternalProperties.MODEL_ORDER); + p1Order = CMGroupModelOrderCalculator.calculateModelOrderOrGroupModelOrder(p1.getOutgoingEdges().get(0), + p2.getOutgoingEdges().get(0), graph, p1.getOutgoingEdges().size() + p1.getIncomingEdges().size()); } if (p2.getOutgoingEdges().get(0).hasProperty(InternalProperties.MODEL_ORDER)) { - p2Order = p2.getOutgoingEdges().get(0).getProperty(InternalProperties.MODEL_ORDER); + p2Order = CMGroupModelOrderCalculator.calculateModelOrderOrGroupModelOrder(p2.getOutgoingEdges().get(0), + p1.getOutgoingEdges().get(0), graph, p2.getOutgoingEdges().size() + p2.getIncomingEdges().size()); } // If both ports have the same target nodes, make sure that the backward edge is below the normal edge. @@ -307,8 +319,11 @@ public int compare(final LPort originalP1, final LPort originalP2) { // Use the port model order to compare them. This can always be very bad since these unconnected ports // can transitively order nodes that should be ordered differently. // This can only be prevented, if one handles the sorting such that unconnected ports are handled last. - int p1MO = p1.getProperty(InternalProperties.MODEL_ORDER); - int p2MO = p2.getProperty(InternalProperties.MODEL_ORDER); + int numberOfPorts = p1.getNode().getPorts().size(); + int p1MO = CMGroupModelOrderCalculator.calculateModelOrderOrGroupModelOrder(p1, p2, + graph, numberOfPorts); + int p2MO = CMGroupModelOrderCalculator.calculateModelOrderOrGroupModelOrder(p2, p1, + graph, numberOfPorts); // Still check the side, since WEST and SOUTH must be the other way around. if (p1.getSide() == PortSide.WEST && p2.getSide() == PortSide.WEST || p1.getSide() == PortSide.SOUTH && p2.getSide() == PortSide.SOUTH) { @@ -340,10 +355,15 @@ public int compare(final LPort originalP1, final LPort originalP2) { * second. */ public int checkPortModelOrder(final LPort p1, final LPort p2) { + int numberOfPorts = p1.getNode().getPorts().size(); if (p1.hasProperty(InternalProperties.MODEL_ORDER) && p2.hasProperty(InternalProperties.MODEL_ORDER)) { - return Integer.compare(p1.getProperty(InternalProperties.MODEL_ORDER), - p2.getProperty(InternalProperties.MODEL_ORDER)); + int p1Order = CMGroupModelOrderCalculator.calculateModelOrderOrGroupModelOrder(p1, p2, + graph, numberOfPorts); + int p2Order = CMGroupModelOrderCalculator.calculateModelOrderOrGroupModelOrder(p2, p1, + graph, numberOfPorts); + return Integer.compare(p1Order, + p2Order); } return 0; } diff --git a/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/options/CycleBreakingStrategy.java b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/options/CycleBreakingStrategy.java index 9e4151e34c..3b6765b6a5 100644 --- a/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/options/CycleBreakingStrategy.java +++ b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/options/CycleBreakingStrategy.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2010, 2017 Kiel University and others. + * Copyright (c) 2010, 2025 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 @@ -11,17 +11,22 @@ import org.eclipse.elk.alg.layered.LayeredPhases; import org.eclipse.elk.alg.layered.graph.LGraph; +import org.eclipse.elk.alg.layered.p1cycles.BFSNodeOrderCycleBreaker; +import org.eclipse.elk.alg.layered.p1cycles.DFSNodeOrderCycleBreaker; import org.eclipse.elk.alg.layered.p1cycles.DepthFirstCycleBreaker; import org.eclipse.elk.alg.layered.p1cycles.GreedyCycleBreaker; import org.eclipse.elk.alg.layered.p1cycles.GreedyModelOrderCycleBreaker; import org.eclipse.elk.alg.layered.p1cycles.InteractiveCycleBreaker; import org.eclipse.elk.alg.layered.p1cycles.ModelOrderCycleBreaker; +import org.eclipse.elk.alg.layered.p1cycles.SCCNodeTypeCycleBreaker; +import org.eclipse.elk.alg.layered.p1cycles.SCConnectivity; import org.eclipse.elk.core.alg.ILayoutPhase; import org.eclipse.elk.core.alg.ILayoutPhaseFactory; import org.eclipse.elk.graph.properties.AdvancedPropertyValue; /** * Enumeration of and factory for the different available cycle breaking strategies. + * The model order cycle breakers additionally allow a group model order to be set and enforced. * * @author msp * @author cds @@ -34,6 +39,7 @@ public enum CycleBreakingStrategy implements ILayoutPhaseFactory create() { case GREEDY_MODEL_ORDER: return new GreedyModelOrderCycleBreaker(); - + + case SCC_CONNECTIVITY: + return new SCConnectivity(); + + case SCC_NODE_TYPE: + return new SCCNodeTypeCycleBreaker(); + + case DFS_NODE_ORDER: + return new DFSNodeOrderCycleBreaker(); + + case BFS_NODE_ORDER: + return new BFSNodeOrderCycleBreaker(); + default: throw new IllegalArgumentException( "No implementation is available for the cycle breaker " + this.toString()); diff --git a/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/options/GroupOrderStrategy.java b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/options/GroupOrderStrategy.java new file mode 100644 index 0000000000..a51d0b93b3 --- /dev/null +++ b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/options/GroupOrderStrategy.java @@ -0,0 +1,32 @@ +/******************************************************************************* + * Copyright (c) 2025 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.options; + +/** + * Determines how to count ordering violations during layered phases. + * Hence, this must be implemented by the several model order strategies to work correctly. + */ +public enum GroupOrderStrategy { + /** + * Different group are not comparable neither by their group id nor by model order. However, if a total ordering + * is required one can of course still use either ordering to create it. + */ + ONLY_WITHIN_GROUP, + /** + * The model order is more important than the group id when comparing elements from different ordering groups. + * Therefore, this should most likely not be used. + */ + MODEL_ORDER, + /** + * The group id is more important than the model order then comparing elements from different ordering groups. + * The secondary criterion will be the model order. + */ + ENFORCED; +} 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..9ae05424ba 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 @@ -413,6 +413,16 @@ public final class InternalProperties { * Set for nodes and edges to preserve the order in the model file. */ public static final IProperty MODEL_ORDER = new Property<>("modelOrder"); + + /** + * Set on parents to save the maximum model order set on nodes. + */ + public static final IProperty MAX_MODEL_ORDER_NODES = new Property<>("modelOrder.maximum"); + + /** + * Set on parents to save the number of model order groups for cycle breaking. + */ + public static final IProperty CB_NUM_MODEL_ORDER_GROUPS = new Property<>("modelOrderGroups.cb.number"); /** * Set on ports to save their (long edge) target node. @@ -453,6 +463,30 @@ public final class InternalProperties { */ public static final IProperty> TARGET_NODE_MODEL_ORDER = new Property<>("targetNode.modelOrder"); + + /** + * Tarjans lowlink. The low-link value is initially equal to which number the node has during the initial DFS. + * If it's the first node visited, the value will be 0. + * If it's the second node, it will be 1. + * The third node has value 2, the fourth value 3, etc. + */ + public static final IProperty TARJAN_LOWLINK = new Property<>("tarjan.lowlink", Integer.MAX_VALUE); + + /** + * Tarjan node index. + */ + public static final IProperty TARJAN_ID = new Property<>("tarjan.id", -1); + + /** + * Trajan on stack property. Marks if a node is on the current depth-first stack. + */ + public static final IProperty TARJAN_ON_STACK = new Property<>("tarjan.onstack", false); + + /** + * Set during tarjan's algorithm. Indicates that a node is part of a cycle. + */ + public static final IProperty IS_PART_OF_CYCLE = new Property<>("partOfCycle", false); + /** * Hidden default constructor. */ diff --git a/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/p1cycles/BFSNodeOrderCycleBreaker.java b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/p1cycles/BFSNodeOrderCycleBreaker.java new file mode 100644 index 0000000000..17f3f87cca --- /dev/null +++ b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/p1cycles/BFSNodeOrderCycleBreaker.java @@ -0,0 +1,229 @@ +/******************************************************************************* + * Copyright (c) 2025 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.p1cycles; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Queue; +import java.util.TreeSet; + +import org.eclipse.elk.alg.layered.LayeredPhases; +import org.eclipse.elk.alg.layered.graph.LEdge; +import org.eclipse.elk.alg.layered.graph.LGraph; +import org.eclipse.elk.alg.layered.graph.LNode; +import org.eclipse.elk.alg.layered.intermediate.IntermediateProcessorStrategy; +import org.eclipse.elk.alg.layered.options.GroupOrderStrategy; +import org.eclipse.elk.alg.layered.options.InternalProperties; +import org.eclipse.elk.alg.layered.options.LayeredOptions; +import org.eclipse.elk.core.alg.ILayoutPhase; +import org.eclipse.elk.core.alg.LayoutProcessorConfiguration; +import org.eclipse.elk.core.util.IElkProgressMonitor; + +import com.google.common.collect.Iterables; + +/** + * Uses the Breadth-First-Search to traverse the graph and reverses edges if the node is already explored. + * + *

This cycle breaker does not support the {@link LayeredOptions#PRIORITY_DIRECTION} option + * that can be set on edges. Neither does it support layer constraints out of the box. + * If layer constraints should be observed, + * {@link org.eclipse.elk.alg.layered.intermediate.EdgeAndLayerConstraintEdgeReverser} and + * {@link org.eclipse.elk.alg.layered.intermediate.LayerConstraintProcessor} should + * be used.

+ * + *
+ *
Precondition:
an unlayered graph
+ *
Postcondition:
the graph has no cycles
+ *
+ * + * @see org.eclipse.elk.alg.layered.intermediate.EdgeAndLayerConstraintEdgeReverser + * @see org.eclipse.elk.alg.layered.intermediate.LayerConstraintProcessor + * + */ +public class BFSNodeOrderCycleBreaker implements ILayoutPhase { + + /** Intermediate processing configuration. */ + private static final LayoutProcessorConfiguration INTERMEDIATE_PROCESSING_CONFIGURATION = + LayoutProcessorConfiguration.create() + .addAfter(LayeredPhases.P5_EDGE_ROUTING, IntermediateProcessorStrategy.REVERSED_EDGE_RESTORER); + + /** Set of source nodes. */ + private HashSet sources; + + /** Set of sink nodes. */ + private HashSet sinks; + + /** Indicates whether a given node was already visited during BFS. */ + private boolean[] visited; + + /** + * Queues the nodes for BFS. + */ + private Queue bfsQueue; + + /** The list of edges to be reversed at the end of our little algorithmic adventure. */ + private List edgesToBeReversed; + + private LGraph graph; + + + @Override + public LayoutProcessorConfiguration getLayoutProcessorConfiguration(final LGraph graph) { + return INTERMEDIATE_PROCESSING_CONFIGURATION; + } + + @Override + public void process(final LGraph graph, final IElkProgressMonitor monitor) { + monitor.begin("Breadth-first cycle removal", 1); + + this.graph = graph; + List nodes = graph.getLayerlessNodes(); + + // initialize values for the algorithm + bfsQueue = new LinkedList(); + sources = new HashSet<>(); + sinks = new HashSet<>(); + visited = new boolean[nodes.size()]; + edgesToBeReversed = new ArrayList<>(); + + // Find all sources and sinks in the graph. + int index = 0; + for (LNode node : nodes) { + // The node id is used as index into our arrays + node.id = index; + if (Iterables.isEmpty(node.getIncomingEdges())) { + sources.add(node); + } + if (Iterables.isEmpty(node.getOutgoingEdges())) { + sinks.add(node); + } + index++; + } + + // Start BFS Search starting at each source sequentially. + // This means each source may add their connections to the queue such that we search breadth-first. + for (LNode source : sources) { + + //sequential bfs + bfsQueue.add(source); + bfsLoop(); + } + + bfsLoop(); + + // Start more BFS runs from the first node that has not been visited yet. This must be part of a cycle since it + // is not a source nodes + boolean changed = true; + while(changed) { + changed = false; + for (int i = 0; i < nodes.size(); i++) { + if (!visited[i]) { + LNode n = nodes.get(i); + assert n.id == i; + bfsQueue.add(n); + changed = true; + break; + } + } + bfsLoop(); + } + + + // Reverse "back edges" + for (LEdge edge : edgesToBeReversed) { + edge.reverse(graph, true); + graph.setProperty(InternalProperties.CYCLIC, true); + } + + // Cleanup + this.sources = null; + this.visited = null; + this.bfsQueue = null; + this.edgesToBeReversed = null; + + monitor.done(); + } + + private void bfsLoop() { + while(!bfsQueue.isEmpty()) { + bfs(bfsQueue.poll()); + } + } + + /** + * Visits a node and adds its connections to the BF-queue. + * @param n the node to visit + */ + private void bfs(final LNode n) { + // Return if the node was already visited. + if (visited[n.id]) { + return; + } + this.visited[n.id] = true; + + // Map to save the node model order of each edge connection. + HashMap> modelOrderMap = new HashMap>(); + boolean groupModelOrder = this.graph.getProperty(LayeredOptions.CONSIDER_MODEL_ORDER_GROUP_MODEL_ORDER_CB_GROUP_ORDER_STRATEGY) == GroupOrderStrategy.ENFORCED; + + // Create a map of edges and the model order of the node they lead to + for (LEdge e : n.getOutgoingEdges()) { + if (!e.getTarget().getNode().hasProperty(InternalProperties.MODEL_ORDER)) { + // Handle edges that connect to nodes without model order. + // They get a high unique value such that the first such node is the last one and the second the second last. + modelOrderMap.put(Integer.MAX_VALUE - modelOrderMap.size(), new HashSet(Arrays.asList(e))); + } else { + int targetModelOrder = 0; + LNode target = e.getTarget().getNode(); + // Find out whether the model order group id or the model order is more important. + if (groupModelOrder) { + // Get the biggest cycle breaking model order group. Now scale all groups such that + // maxModelOrderGroupSize * + model order creates a total ordering on all nodes. + // This orders all nodes without a group model order at the top. + // I leave this for know and find out whether this is desired. Maybe all need a group model order to begin with. + int maxModelOrderGroupSize = this.graph.getProperty(InternalProperties.MAX_MODEL_ORDER_NODES); + targetModelOrder = maxModelOrderGroupSize * target.getProperty(LayeredOptions.CONSIDER_MODEL_ORDER_GROUP_MODEL_ORDER_CYCLE_BREAKING_ID) + + target.getProperty(InternalProperties.MODEL_ORDER); + } else { + targetModelOrder = e.getTarget().getNode().getProperty(InternalProperties.MODEL_ORDER); + } + // If the long edge target node has a model order, add it to the map. + if (modelOrderMap.containsKey(targetModelOrder)){ + modelOrderMap.get(targetModelOrder).add(e); + } else { + modelOrderMap.put(targetModelOrder, new HashSet(Arrays.asList(e))); + } + } + } + // This holds all model orders of nodes connected to the current node sorted by model order. + // Basically this orders all different model orders (or group model orders) by priority. + TreeSet modelOrderSet = new TreeSet<>(modelOrderMap.keySet()); + + // Since the model order determines the iteration order of e + for (int key : modelOrderSet) { + LEdge out = modelOrderMap.get(key).iterator().next(); + // Do not visit self loops + if(out.isSelfLoop()) { + continue; + } + // If the target was already visited, reverse the edge to it. + LNode target = out.getTarget().getNode(); + // + if (this.visited[target.id] && !sources.contains(n) && !sinks.contains(target)) { + edgesToBeReversed.addAll(modelOrderMap.get(key)); + } else { + bfsQueue.add(target); + } + } + } +} \ No newline at end of file diff --git a/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/p1cycles/DFSNodeOrderCycleBreaker.java b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/p1cycles/DFSNodeOrderCycleBreaker.java new file mode 100644 index 0000000000..75a56d26dc --- /dev/null +++ b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/p1cycles/DFSNodeOrderCycleBreaker.java @@ -0,0 +1,208 @@ +/******************************************************************************* + * Copyright (c) 2025 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.p1cycles; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.TreeSet; + +import org.eclipse.elk.alg.layered.LayeredPhases; +import org.eclipse.elk.alg.layered.graph.LEdge; +import org.eclipse.elk.alg.layered.graph.LGraph; +import org.eclipse.elk.alg.layered.graph.LNode; +import org.eclipse.elk.alg.layered.intermediate.IntermediateProcessorStrategy; +import org.eclipse.elk.alg.layered.options.GroupOrderStrategy; +import org.eclipse.elk.alg.layered.options.InternalProperties; +import org.eclipse.elk.alg.layered.options.LayeredOptions; +import org.eclipse.elk.core.alg.ILayoutPhase; +import org.eclipse.elk.core.alg.LayoutProcessorConfiguration; +import org.eclipse.elk.core.util.IElkProgressMonitor; + +import com.google.common.collect.Iterables; + +/** + * Cycle breaker implementation that uses a depth-first traversal of the graph. Described in + *
    + *
  • Emden R. Gansner, Eleftherios Koutsofios, Stephen C. North, Kiem-Phong Vo, A technique for drawing directed + * graphs. Software Engineering 19(3), pp. 214-230, 1993.
  • + *
+ * While the {@link org.eclipse.elk.alg.layered.p1cycles.DepthFirstCycleBreaker} uses the edge order as the visiting order + * this cycle breaker uses the node (model order) for this. + * + *

+ * This cycle breaker does not support the {@link LayeredOptions#PRIORITY_DIRECTION} option that can be set on edges. + * Neither does it support layer constraints out of the box. If layer constraints should be observed, + * {@link org.eclipse.elk.alg.layered.intermediate.EdgeAndLayerConstraintEdgeReverser} and + * {@link org.eclipse.elk.alg.layered.intermediate.LayerConstraintProcessor} should be used. + *

+ * + *
+ *
Precondition:
+ *
an unlayered graph
+ *
Postcondition:
+ *
the graph has no cycles
+ *
+ * + * @see org.eclipse.elk.alg.layered.intermediate.EdgeAndLayerConstraintEdgeReverser + * @see org.eclipse.elk.alg.layered.intermediate.LayerConstraintProcessor + */ +public class DFSNodeOrderCycleBreaker implements ILayoutPhase { + + /** Intermediate processing configuration. */ + private static final LayoutProcessorConfiguration INTERMEDIATE_PROCESSING_CONFIGURATION = + LayoutProcessorConfiguration. create().addAfter(LayeredPhases.P5_EDGE_ROUTING, + IntermediateProcessorStrategy.REVERSED_EDGE_RESTORER); + + /** List of source nodes. */ + private List sources; + /** Indicates whether a given node was already visited during DFS. */ + private boolean[] visited; + /** + * A node is active during DFS if it is on our current DFS path. Any edge that leads back to an active node induces + * a cycle and needs to be reversed. + */ + private boolean[] active; + /** The list of edges to be reversed at the end of our little algorithmic adventure. */ + private List edgesToBeReversed; + + private LGraph graph; + + @Override + public LayoutProcessorConfiguration getLayoutProcessorConfiguration(final LGraph graph) { + return INTERMEDIATE_PROCESSING_CONFIGURATION; + } + + @Override + public void process(final LGraph graph, final IElkProgressMonitor monitor) { + monitor.begin("Depth-first cycle removal", 1); + + this.graph = graph; + List nodes = graph.getLayerlessNodes(); + + // initialize values for the algorithm + int nodeCount = nodes.size(); + + sources = new ArrayList<>(); + visited = new boolean[nodeCount]; + active = new boolean[nodeCount]; + edgesToBeReversed = new ArrayList<>(); + + int index = 0; + for (LNode node : nodes) { + // The node id is used as index into our arrays + node.id = index; + if (Iterables.isEmpty(node.getIncomingEdges())) { + sources.add(node); + } + index++; + } + + // From every source node start a DFS + for (LNode source : sources) { + dfs(source); + } + + // Start more DFS runs for all nodes that have not been visited yet. These must be part of a cycle since they + // are not source nodes + for (int i = 0; i < nodeCount; i++) { + if (!visited[i]) { + LNode n = nodes.get(i); + assert n.id == i; + dfs(n); + } + } + + // Reverse "back edges" + for (LEdge edge : edgesToBeReversed) { + edge.reverse(graph, true); + graph.setProperty(InternalProperties.CYCLIC, true); + } + + // Cleanup + this.sources = null; + this.visited = null; + this.active = null; + this.edgesToBeReversed = null; + + + monitor.done(); + } + + private void dfs(final LNode n) { + if (visited[n.id]) { + return; + } + + // We're now visiting the node, and it's active + this.visited[n.id] = true; + this.active[n.id] = true; + + HashMap> modelOrderMap = new HashMap>(); + boolean groupModelOrder = this.graph.getProperty(LayeredOptions.CONSIDER_MODEL_ORDER_GROUP_MODEL_ORDER_CB_GROUP_ORDER_STRATEGY) == GroupOrderStrategy.ENFORCED; + + + // Construct tree set to efficiently find next node to look at by finding all connected nodes and sorting them. + n.getOutgoingEdges().forEach(e -> { + if (e.getTarget().getNode().getProperty(InternalProperties.MODEL_ORDER) == null) { + // Handle edges that connect to nodes without model order. + // They get a high unique value such that the first such node is the last one and the second the second last. + modelOrderMap.put(Integer.MAX_VALUE - modelOrderMap.size(), new HashSet(Arrays.asList(e))); + } else { + int targetModelOrder = 0; + LNode target = e.getTarget().getNode(); + // Find out whether the model order group id or the model order is more important. + if (groupModelOrder) { + // Get the biggest cycle breaking model order group. Now scale all groups such that + // maxModelOrderGroupSize * + model order creates a total ordering on all nodes. + // This orders all nodes without a group model order at the top. + // I leave this for know and find out whether this is desired. Maybe all need a group model order to begin with. + int maxModelOrderGroupSize = this.graph.getProperty(InternalProperties.MAX_MODEL_ORDER_NODES); + targetModelOrder = maxModelOrderGroupSize * target.getProperty(LayeredOptions.CONSIDER_MODEL_ORDER_GROUP_MODEL_ORDER_CYCLE_BREAKING_ID) + + target.getProperty(InternalProperties.MODEL_ORDER); + } else { + targetModelOrder = e.getTarget().getNode().getProperty(InternalProperties.MODEL_ORDER); + } + // If the long edge target node has a model order, add it to the map. + if (modelOrderMap.containsKey(targetModelOrder)) { + modelOrderMap.get(targetModelOrder).add(e); + } else { + modelOrderMap.put(targetModelOrder, new HashSet(Arrays.asList(e))); + } + } + }); + // The model order serves as the key by which all elements are sorted. + // Construct this by * groupId + MO if ENFORCED, otherwise using model order is ok. + TreeSet modelOrderSet = new TreeSet<>(modelOrderMap.keySet()); + for (int key : modelOrderSet) { + LEdge out = modelOrderMap.get(key).iterator().next(); + // Ignore self loops + if (out.isSelfLoop()) { + continue; + } + + LNode target = out.getTarget().getNode(); + + // If the edge connects to an active node, we have found a path from said active node back to itself since + // active nodes are on our current path. That's a backward edge and needs to be reversed + if (this.active[target.id]) { + edgesToBeReversed.addAll(modelOrderMap.get(key)); + } else { + dfs(target); + } + } + + // We're leaving this node + this.active[n.id] = false; + } + +} \ No newline at end of file diff --git a/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/p1cycles/GreedyCycleBreaker.java b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/p1cycles/GreedyCycleBreaker.java index 5c3978e112..a93d27cef2 100644 --- a/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/p1cycles/GreedyCycleBreaker.java +++ b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/p1cycles/GreedyCycleBreaker.java @@ -72,6 +72,11 @@ public class GreedyCycleBreaker implements ILayoutPhase { /** list of sink nodes. */ private final LinkedList sinks = Lists.newLinkedList(); + /** + * The graph + */ + protected LGraph layeredGraph; + private Random random; @Override @@ -83,6 +88,8 @@ public LayoutProcessorConfiguration getLayoutProcessorCon public void process(final LGraph layeredGraph, final IElkProgressMonitor monitor) { monitor.begin("Greedy cycle removal", 1); + this.layeredGraph = layeredGraph; + List nodes = layeredGraph.getLayerlessNodes(); // initialize values for the algorithm (sum of priorities of incoming edges and outgoing diff --git a/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/p1cycles/GreedyModelOrderCycleBreaker.java b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/p1cycles/GreedyModelOrderCycleBreaker.java index 0aac2532ae..104fc1937e 100644 --- a/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/p1cycles/GreedyModelOrderCycleBreaker.java +++ b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/p1cycles/GreedyModelOrderCycleBreaker.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2022 Kiel University and others. + * Copyright (c) 2022, 2025 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 @@ -12,7 +12,9 @@ import java.util.List; import org.eclipse.elk.alg.layered.graph.LNode; +import org.eclipse.elk.alg.layered.options.GroupOrderStrategy; import org.eclipse.elk.alg.layered.options.InternalProperties; +import org.eclipse.elk.alg.layered.options.LayeredOptions; /** * Greedy Cycle Breaker that behaves the same as {@link GreedyCycleBreaker} but does not randomly choose an edge to @@ -27,14 +29,22 @@ public final class GreedyModelOrderCycleBreaker extends GreedyCycleBreaker { protected LNode chooseNodeWithMaxOutflow(final List nodes) { LNode returnNode = null; int minimumModelOrder = Integer.MAX_VALUE; + int offset = Math.max(layeredGraph.getLayerlessNodes().size(), layeredGraph.getProperty(InternalProperties.MAX_MODEL_ORDER_NODES)); + int bigOffset = offset * layeredGraph.getProperty(InternalProperties.CB_NUM_MODEL_ORDER_GROUPS); + GroupModelOrderCalculator moCalculator = new GroupModelOrderCalculator(); + boolean enforceGroupModelOrder = layeredGraph.getProperty( + LayeredOptions.CONSIDER_MODEL_ORDER_GROUP_MODEL_ORDER_CB_GROUP_ORDER_STRATEGY) == GroupOrderStrategy.ENFORCED; for (LNode node : nodes) { // In this step nodes without a model order are disregarded. // One could of course think of a different strategy regarding this aspect. - // FUTURE WORK: If multiple model order groups exist, one has to chose based on the priority of the groups. - if (node.hasProperty(InternalProperties.MODEL_ORDER) - && node.getProperty(InternalProperties.MODEL_ORDER) < minimumModelOrder) { - minimumModelOrder = node.getProperty(InternalProperties.MODEL_ORDER); - returnNode = node; + if (node.hasProperty(InternalProperties.MODEL_ORDER)) { + int modelOrder = enforceGroupModelOrder + ? moCalculator.computeConstraintGroupModelOrder(node, bigOffset, offset) + : moCalculator.computeConstraintModelOrder(node, offset); + if (minimumModelOrder > modelOrder) { + minimumModelOrder = modelOrder; + returnNode = node; + } } } if (returnNode == null) { diff --git a/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/p1cycles/GroupModelOrderCalculator.java b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/p1cycles/GroupModelOrderCalculator.java new file mode 100644 index 0000000000..51802ccbd3 --- /dev/null +++ b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/p1cycles/GroupModelOrderCalculator.java @@ -0,0 +1,110 @@ +/******************************************************************************* + * Copyright (c) 2025 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.p1cycles; + +import org.eclipse.elk.alg.layered.graph.LNode; +import org.eclipse.elk.alg.layered.options.InternalProperties; +import org.eclipse.elk.alg.layered.options.LayeredOptions; + +/** + * Helper class to calculate the model order and group model order. + * Needs to be reset to reuse it. + */ +public class GroupModelOrderCalculator { + + /** + * Counts how many first separate nodes occurred. + */ + private int firstSeparateNodes = 0; + /** + * Counts how many last separate nodes occurred. + */ + private int lastSeparateNodes = 0; + + /** + * Set model order to a value such that the constraint is respected and the ordering between nodes with + * the same constraint is preserved. + * The order should be FIRST_SEPARATE < FIRST < NORMAL < LAST < LAST_SEPARATE. The offset is used to make sure the + * all nodes have unique model orders. + * @param node The LNode + * @param offset The offset between FIRST, FIRST_SEPARATE, NORMAL, LAST_SEPARATE, and LAST nodes for unique order + * @return A unique model order + */ + public int computeConstraintModelOrder(final LNode node, final int offset) { + int modelOrder = 0; + switch (node.getProperty(LayeredOptions.LAYERING_LAYER_CONSTRAINT)) { + case FIRST_SEPARATE: + modelOrder = 2 * -offset + firstSeparateNodes; + firstSeparateNodes++; + break; + case FIRST: + modelOrder = -offset; + break; + case LAST: + modelOrder = offset; + break; + case LAST_SEPARATE: + modelOrder = 2 * offset + lastSeparateNodes; + lastSeparateNodes++; + break; + default: + break; + } + if (node.hasProperty(InternalProperties.MODEL_ORDER)) { + modelOrder += node.getProperty(InternalProperties.MODEL_ORDER); + } + return modelOrder; + } + + + /** + * Set group model order to a value such that the constraint is respected and the ordering between nodes with + * the same constraint is preserved. + * The order should be FIRST_SEPARATE < FIRST < NORMAL < LAST < LAST_SEPARATE. The offset is used to make sure the + * all nodes have unique group model orders. We calculate this offset by "highest model order * number of model order + * groups" and the small offset by using only the highest model order. + * @param node The LNode + * @param offset The offset between FIRST, FIRST_SEPARATE, NORMAL, LAST_SEPARATE, and LAST nodes for unique order + * @param smallOffset The offset between each model order group. + * @return A unique group model order + */ + public int computeConstraintGroupModelOrder(final LNode node, final int offset, final int smallOffset) { + int modelOrder = 0; + switch (node.getProperty(LayeredOptions.LAYERING_LAYER_CONSTRAINT)) { + case FIRST_SEPARATE: + modelOrder = 2 * -offset + firstSeparateNodes; + firstSeparateNodes++; + break; + case FIRST: + modelOrder = -offset; + break; + case LAST: + modelOrder = offset; + break; + case LAST_SEPARATE: + modelOrder = 2 * offset + lastSeparateNodes; + lastSeparateNodes++; + break; + default: + break; + } + if (node.hasProperty(InternalProperties.MODEL_ORDER)) { + modelOrder += node.getProperty(LayeredOptions.CONSIDER_MODEL_ORDER_GROUP_MODEL_ORDER_CYCLE_BREAKING_ID) + * smallOffset + node.getProperty(InternalProperties.MODEL_ORDER); + + } + return modelOrder; + } + + public void resetInternalCounters() { + this.firstSeparateNodes = 0; + this.lastSeparateNodes = 0; + } +} diff --git a/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/p1cycles/ModelOrderCycleBreaker.java b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/p1cycles/ModelOrderCycleBreaker.java index 79afcd858c..f58ea7977c 100644 --- a/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/p1cycles/ModelOrderCycleBreaker.java +++ b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/p1cycles/ModelOrderCycleBreaker.java @@ -1,5 +1,5 @@ /******************************************************************************* - * Copyright (c) 2021 Kiel University and others. + * Copyright (c) 2021, 2025 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 @@ -17,6 +17,7 @@ import org.eclipse.elk.alg.layered.graph.LNode; import org.eclipse.elk.alg.layered.graph.LPort; import org.eclipse.elk.alg.layered.intermediate.IntermediateProcessorStrategy; +import org.eclipse.elk.alg.layered.options.GroupOrderStrategy; import org.eclipse.elk.alg.layered.options.InternalProperties; import org.eclipse.elk.alg.layered.options.LayeredOptions; import org.eclipse.elk.alg.layered.options.PortType; @@ -27,7 +28,7 @@ import com.google.common.collect.Lists; /** - * A cycle breaker that reverses all edges that go against the model order, + * A cycle breaker that reverses all edges that go against the model order or group model order, * i.e. edges from high model order to low model order. * *
@@ -39,9 +40,6 @@ */ public final class ModelOrderCycleBreaker implements ILayoutPhase { - private int firstSeparateModelOrder; - private int lastSeparateModelOrder; - /** intermediate processing configuration. */ private static final LayoutProcessorConfiguration INTERMEDIATE_PROCESSING_CONFIGURATION = LayoutProcessorConfiguration.create() @@ -56,10 +54,6 @@ public LayoutProcessorConfiguration getLayoutProcessorCon public void process(final LGraph layeredGraph, final IElkProgressMonitor monitor) { monitor.begin("Model order cycle breaking", 1); - // Reset FIRST_SEPARATE and LAST_SEPARATE counters. - firstSeparateModelOrder = 0; - lastSeparateModelOrder = 0; - // gather edges that point to the wrong direction List revEdges = Lists.newArrayList(); @@ -68,20 +62,22 @@ public void process(final LGraph layeredGraph, final IElkProgressMonitor monitor // E.g. A node with the LAST constraint needs to have a model order m = modelOrder + offset // such that m > m(n) with m(n) being the model order of a normal node n (without constraints). // Such that the highest model order has to be used as an offset - int offset = layeredGraph.getLayerlessNodes().size(); - for (LNode node : layeredGraph.getLayerlessNodes()) { - if (node.hasProperty(InternalProperties.MODEL_ORDER)) { - offset = Math.max(offset, node.getProperty(InternalProperties.MODEL_ORDER) + 1); - } - } - + int offset = Math.max(layeredGraph.getLayerlessNodes().size(), layeredGraph.getProperty(InternalProperties.MAX_MODEL_ORDER_NODES)); + int bigOffset = offset * layeredGraph.getProperty(InternalProperties.CB_NUM_MODEL_ORDER_GROUPS); + boolean enforceGroupModelOrder = layeredGraph.getProperty( + LayeredOptions.CONSIDER_MODEL_ORDER_GROUP_MODEL_ORDER_CB_GROUP_ORDER_STRATEGY) == GroupOrderStrategy.ENFORCED; for (LNode source : layeredGraph.getLayerlessNodes()) { - int modelOrderSource = computeConstraintModelOrder(source, offset); + GroupModelOrderCalculator calculator = new GroupModelOrderCalculator(); + int modelOrderSource = enforceGroupModelOrder + ? calculator.computeConstraintGroupModelOrder(source, bigOffset, offset) + : calculator.computeConstraintModelOrder(source, offset); for (LPort port : source.getPorts(PortType.OUTPUT)) { for (LEdge edge : port.getOutgoingEdges()) { LNode target = edge.getTarget().getNode(); - int modelOrderTarget = computeConstraintModelOrder(target, offset); + int modelOrderTarget = enforceGroupModelOrder + ? calculator.computeConstraintGroupModelOrder(target, bigOffset, offset) + : calculator.computeConstraintModelOrder(target, offset); if (modelOrderTarget < modelOrderSource) { revEdges.add(edge); } @@ -97,39 +93,4 @@ public void process(final LGraph layeredGraph, final IElkProgressMonitor monitor revEdges.clear(); monitor.done(); } - - /** - * Set model order to a value such that the constraint is respected and the ordering between nodes with - * the same constraint is preserved. - * The order should be FIRST_SEPARATE < FIRST < NORMAL < LAST < LAST_SEPARATE. The offset is used to make sure the - * all nodes have unique model orders. - * @param node The LNode - * @param offset The offset between FIRST, FIRST_SEPARATE, NORMAL, LAST_SEPARATE, and LAST nodes for unique order - * @return A unique model order - */ - private int computeConstraintModelOrder(final LNode node, final int offset) { - int modelOrder = 0; - switch (node.getProperty(LayeredOptions.LAYERING_LAYER_CONSTRAINT)) { - case FIRST_SEPARATE: - modelOrder = 2 * -offset + firstSeparateModelOrder; - firstSeparateModelOrder++; - break; - case FIRST: - modelOrder = -offset; - break; - case LAST: - modelOrder = offset; - break; - case LAST_SEPARATE: - modelOrder = 2 * offset + lastSeparateModelOrder; - lastSeparateModelOrder++; - break; - default: - break; - } - if (node.hasProperty(InternalProperties.MODEL_ORDER)) { - modelOrder += node.getProperty(InternalProperties.MODEL_ORDER); - } - return modelOrder; - } } diff --git a/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/p1cycles/SCCModelOrderCycleBreaker.java b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/p1cycles/SCCModelOrderCycleBreaker.java new file mode 100644 index 0000000000..5d910ef240 --- /dev/null +++ b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/p1cycles/SCCModelOrderCycleBreaker.java @@ -0,0 +1,165 @@ +/******************************************************************************* + * Copyright (c) 2025 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.p1cycles; + +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +import org.eclipse.elk.alg.layered.LayeredPhases; +import org.eclipse.elk.alg.layered.graph.LEdge; +import org.eclipse.elk.alg.layered.graph.LGraph; +import org.eclipse.elk.alg.layered.graph.LNode; +import org.eclipse.elk.alg.layered.graph.Tarjan; +import org.eclipse.elk.alg.layered.intermediate.IntermediateProcessorStrategy; +import org.eclipse.elk.alg.layered.options.GroupOrderStrategy; +import org.eclipse.elk.alg.layered.options.InternalProperties; +import org.eclipse.elk.alg.layered.options.LayeredOptions; +import org.eclipse.elk.core.alg.ILayoutPhase; +import org.eclipse.elk.core.alg.LayoutProcessorConfiguration; +import org.eclipse.elk.core.util.IElkProgressMonitor; + +import com.google.common.collect.Lists; + +/** + * This Cycle Breaking Strategy relies on Tarjan's algorithm to find strongly connected components. + * It than selects the node with the maximum model order in the strongly connected components and reverses its out-going + * edges to nodes in the strongly connected component. + * + *
+ *
Precondition:
+ *
no self loops
+ *
Postcondition:
+ *
the graph has no cycles
+ *
+ * + */ +public abstract class SCCModelOrderCycleBreaker implements ILayoutPhase { + + /** + * List of strongly connected components calculated by tarjan. + */ + protected List> stronglyConnectedComponents = new LinkedList>(); + + /** + * Maps node to id of its strongly connected component. + */ + protected HashMap nodeToSCCID = new HashMap<>(); + + /** + * The edges to reverse. + */ + protected List revEdges = Lists.newArrayList(); + + /** + * The graph. + */ + protected LGraph graph; + + /** intermediate processing configuration. */ + private static final LayoutProcessorConfiguration INTERMEDIATE_PROCESSING_CONFIGURATION = + LayoutProcessorConfiguration.create() + .addAfter(LayeredPhases.P5_EDGE_ROUTING, IntermediateProcessorStrategy.REVERSED_EDGE_RESTORER); + + @Override + public LayoutProcessorConfiguration getLayoutProcessorConfiguration(final LGraph graph) { + return INTERMEDIATE_PROCESSING_CONFIGURATION; + } + + + @Override + public void process(final LGraph layeredGraph, final IElkProgressMonitor monitor) { + monitor.begin("Model order cycle breaking", 1); + + this.graph = layeredGraph; + + // gather edges that point to the wrong direction + revEdges = Lists.newArrayList(); + + // One needs an offset to make sure that the model order of nodes with port constraints is + // always lower/higher than that of other nodes. + // E.g. A node with the LAST constraint needs to have a model order m = modelOrder + offset + // such that m > m(n) with m(n) being the model order of a normal node n (without constraints). + // Such that the highest model order has to be used as an offset + int offset = Math.max(layeredGraph.getLayerlessNodes().size(), layeredGraph.getProperty(InternalProperties.MAX_MODEL_ORDER_NODES)); + + while (true) { + Tarjan t = new Tarjan(revEdges, stronglyConnectedComponents, nodeToSCCID); + t.resetTarjan(layeredGraph); + t.tarjan(layeredGraph); + + // If no Strongly connected components remain, the graph is acyclic. + if (stronglyConnectedComponents.size() == 0) { + break; + } + + // highest model order only incoming + findNodes(offset, offset * layeredGraph.getProperty(InternalProperties.CB_NUM_MODEL_ORDER_GROUPS)); + + // reverse the gathered edges + for (LEdge edge : revEdges) { + edge.reverse(layeredGraph, false); + edge.getSource().getNode().setProperty(LayeredOptions.LAYERING_LAYER_ID, + edge.getSource().getNode().getProperty(LayeredOptions.LAYERING_LAYER_ID) + 1); + layeredGraph.setProperty(InternalProperties.CYCLIC, true); + } + + stronglyConnectedComponents.clear(); + nodeToSCCID.clear(); + revEdges.clear(); + } + + monitor.done(); + monitor.log("Execution Time: " + monitor.getExecutionTime()); + } + + /** + * Find the nodes with the highest model order or group model order to reverse all its outgoing edges. + * @param offset Helper value to calculate constraint partitions. This has to be higher than model order such that + * "offset * primary criterion + secondary criterion" works. + */ + public void findNodes(int offset, int bigOffset) { + // All strongly connected components have one maximum element for which we can reverse all outgoing edges. + for (int i = 0; i < stronglyConnectedComponents.size(); i++) { + LNode max = null; + GroupModelOrderCalculator calculator = new GroupModelOrderCalculator(); + int maxModelOrder = Integer.MIN_VALUE; + for (LNode n : stronglyConnectedComponents.get(i)) { + // Check whether model order or the group model order is the primary criterion. + // If it is group model order, we need to handle this differently. + boolean enforceGroupModelOrder = this.graph.getProperty( + LayeredOptions.CONSIDER_MODEL_ORDER_GROUP_MODEL_ORDER_CB_GROUP_ORDER_STRATEGY) == GroupOrderStrategy.ENFORCED; + if (max == null) { + // Case first element + max = n; + maxModelOrder = enforceGroupModelOrder + ? calculator.computeConstraintGroupModelOrder(n, bigOffset, offset) + : calculator.computeConstraintModelOrder(n, offset); + } else { + // Find a new maximum if possible. + int modelOrderCurrent = enforceGroupModelOrder + ? calculator.computeConstraintGroupModelOrder(n, bigOffset, offset) + : calculator.computeConstraintModelOrder(n, offset); + if (maxModelOrder < modelOrderCurrent) { + max = n; + maxModelOrder = modelOrderCurrent; + } + } + } + for (LEdge edge : max.getOutgoingEdges()) { + // Reverse all edges to the same strongly connected component. + if (stronglyConnectedComponents.get(i).contains(edge.getTarget().getNode())) { + revEdges.add(edge); + } + } + } + } +} \ No newline at end of file diff --git a/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/p1cycles/SCCNodeTypeCycleBreaker.java b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/p1cycles/SCCNodeTypeCycleBreaker.java new file mode 100644 index 0000000000..3d5cc46e14 --- /dev/null +++ b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/p1cycles/SCCNodeTypeCycleBreaker.java @@ -0,0 +1,109 @@ +/******************************************************************************* + * Copyright (c) 2025 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.p1cycles; + +import org.eclipse.elk.alg.layered.graph.LEdge; +import org.eclipse.elk.alg.layered.graph.LNode; +import org.eclipse.elk.alg.layered.options.GroupOrderStrategy; +import org.eclipse.elk.alg.layered.options.LayeredOptions; + +import com.google.common.collect.Iterables; + +/** + * This cycle breaking strategy extends the {@link org.eclipse.elk.alg.layered.p1cycles.SCCModelOrderCycleBreaker}. + * The preferred node type for the minimum or maximum node can be defined. + * + *
+ *
Precondition:
+ *
no self loops
+ *
Postcondition:
+ *
the graph has no cycles
+ *
+ * + */ +public class SCCNodeTypeCycleBreaker extends SCCModelOrderCycleBreaker { + + @Override + public void findNodes(int offset, int bigOffset) { + for (int i = 0; i < stronglyConnectedComponents.size(); i++) { + // Nothing needs to be done if only one strongly connected component exists. + if (stronglyConnectedComponents.get(i).size() <= 1) { + continue; + } + LNode min = null; + LNode max = null; + int modelOrderMin = Integer.MAX_VALUE; + int modelOrderMax = Integer.MIN_VALUE; + // Check whether model order or the group model order is the primary criterion. + // If it is group model order, we need to handle this differently. + boolean enforceGroupModelOrder = this.graph.getProperty( + LayeredOptions.CONSIDER_MODEL_ORDER_GROUP_MODEL_ORDER_CB_GROUP_ORDER_STRATEGY) == GroupOrderStrategy.ENFORCED; + GroupModelOrderCalculator calculator = new GroupModelOrderCalculator(); + // Iterate over all strongly connected components to find the maximum/minimum node to reverse edges. + for (LNode n : stronglyConnectedComponents.get(i)) { + // First calculate initial values + if (min == null || max == null) { + min = n; + modelOrderMin = enforceGroupModelOrder + ? calculator.computeConstraintGroupModelOrder(n, bigOffset, offset) + : calculator.computeConstraintModelOrder(n, offset); + max = n; + modelOrderMax = modelOrderMin; + } else { + // For all not first nodes find the group model order and model order with constraints and update the + // minimum and maximum node. + int modelOrderCurrent = enforceGroupModelOrder + ? calculator.computeConstraintGroupModelOrder(n, bigOffset, offset) + : calculator.computeConstraintModelOrder(n, offset); + if (modelOrderMin > modelOrderCurrent) { + min = n; + modelOrderMin = modelOrderCurrent; + } + if (modelOrderMax < modelOrderCurrent) { + max = n; + modelOrderMax = modelOrderCurrent; + } + } + } + if (min.getProperty(LayeredOptions.CONSIDER_MODEL_ORDER_GROUP_MODEL_ORDER_CYCLE_BREAKING_ID) == graph + .getProperty(LayeredOptions.CONSIDER_MODEL_ORDER_GROUP_MODEL_ORDER_CB_PREFERRED_SOURCE_ID)) { + // If the minimum model order node is a preferred source node, reverse all its incoming edges. + for (LEdge edge : min.getIncomingEdges()) { + if (stronglyConnectedComponents.get(i).contains(edge.getSource().getNode())) { + revEdges.add(edge); + } + } + } else if (max.getProperty(LayeredOptions.CONSIDER_MODEL_ORDER_GROUP_MODEL_ORDER_CYCLE_BREAKING_ID) == graph + .getProperty(LayeredOptions.CONSIDER_MODEL_ORDER_GROUP_MODEL_ORDER_CB_PREFERRED_TARGET_ID)) { + // If the maximum model order node is a preferred node reverse all its outgoing edges. + for (LEdge edge : max.getOutgoingEdges()) { + if (stronglyConnectedComponents.get(i).contains(edge.getSource().getNode())) { + revEdges.add(edge); + } + } + } else { + // Use connectivity to decide. + if (Iterables.size(min.getIncomingEdges()) > Iterables.size(max.getOutgoingEdges())) { + for (LEdge edge : min.getIncomingEdges()) { + if (stronglyConnectedComponents.get(i).contains(edge.getSource().getNode())) { + revEdges.add(edge); + } + } + } else { + for (LEdge edge : max.getOutgoingEdges()) { + if (stronglyConnectedComponents.get(i).contains(edge.getTarget().getNode())) { + revEdges.add(edge); + } + } + } + } + } + } +} \ No newline at end of file diff --git a/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/p1cycles/SCConnectivity.java b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/p1cycles/SCConnectivity.java new file mode 100644 index 0000000000..ffe707f142 --- /dev/null +++ b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/p1cycles/SCConnectivity.java @@ -0,0 +1,96 @@ +/******************************************************************************* + * Copyright (c) 2025 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.p1cycles; + +import org.eclipse.elk.alg.layered.graph.LEdge; +import org.eclipse.elk.alg.layered.graph.LNode; +import org.eclipse.elk.alg.layered.options.GroupOrderStrategy; +import org.eclipse.elk.alg.layered.options.LayeredOptions; + +import com.google.common.collect.Iterables; + +/** + * Based on the SCCModelOrderCycleBreaker. This finds the nodes with minimum and maximum model order and reverses the + * incoming nodes of the minimum, if its the in-degree is greater than the out-degree of the maximum node. Else it + * reverses the out-going edges of the maximum node. + * If group model order should be enforced, this uses the group model order as a primary criterion. + * + *
+ *
Precondition:
+ *
no self loops
+ *
Postcondition:
+ *
the graph has no cycles
+ *
+ * + */ +public class SCConnectivity extends SCCModelOrderCycleBreaker { + + @Override + public void findNodes(int offset, int bigOffset) { + for (int i = 0; i < stronglyConnectedComponents.size(); i++) { + // Nothing needs to be done if only one strongly connected component exists. + if (stronglyConnectedComponents.get(i).size() <= 1) { + continue; + } + LNode min = null; + LNode max = null; + int modelOrderMin = Integer.MAX_VALUE; + int modelOrderMax = Integer.MIN_VALUE; + // Check whether model order or the group model order is the primary criterion. + // If it is group model order, we need to handle this differently. + boolean enforceGroupModelOrder = this.graph.getProperty( + LayeredOptions.CONSIDER_MODEL_ORDER_GROUP_MODEL_ORDER_CB_GROUP_ORDER_STRATEGY) == GroupOrderStrategy.ENFORCED; + GroupModelOrderCalculator calculator = new GroupModelOrderCalculator(); + // Iterate over all strongly connected components to find the maximum/minimum node to reverse edges. + for (LNode n : stronglyConnectedComponents.get(i)) { + // First calculate initial values + if (min == null || max == null) { + min = n; + modelOrderMin = enforceGroupModelOrder + ? calculator.computeConstraintGroupModelOrder(n, bigOffset, offset) + : calculator.computeConstraintModelOrder(n, offset); + max = n; + modelOrderMax = modelOrderMin; + } else { + + // For all not first nodes find the group model order and model order with constraints and update the + // minimum and maximum node. + int modelOrderCurrent = enforceGroupModelOrder + ? calculator.computeConstraintGroupModelOrder(n, bigOffset, offset) + : calculator.computeConstraintModelOrder(n, offset); + if (modelOrderMin > modelOrderCurrent) { + min = n; + modelOrderMin = modelOrderCurrent; + } + if (modelOrderMax < modelOrderCurrent) { + max = n; + modelOrderMax = modelOrderCurrent; + } + } + } + // If the minimum node has more incoming edges than the maximum node has outgoing edges, + // reverse all edges to the minimum node and remove it from the strongly connected component. + // If it is the other way around, reverse all outgoing edges of the maximum node. + if (Iterables.size(min.getIncomingEdges()) > Iterables.size(max.getOutgoingEdges())) { + for (LEdge edge : min.getIncomingEdges()) { + if (stronglyConnectedComponents.get(i).contains(edge.getSource().getNode())) { + revEdges.add(edge); + } + } + } else { + for (LEdge edge : max.getOutgoingEdges()) { + if (stronglyConnectedComponents.get(i).contains(edge.getTarget().getNode())) { + revEdges.add(edge); + } + } + } + } + } +} \ No newline at end of file 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..65fe03c057 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 @@ -27,6 +27,7 @@ import org.eclipse.elk.alg.layered.intermediate.SortByInputModelProcessor; import org.eclipse.elk.alg.layered.intermediate.preserveorder.ModelOrderNodeComparator; import org.eclipse.elk.alg.layered.intermediate.preserveorder.ModelOrderPortComparator; +import org.eclipse.elk.alg.layered.options.GroupOrderStrategy; import org.eclipse.elk.alg.layered.options.InternalProperties; import org.eclipse.elk.alg.layered.options.LayeredOptions; import org.eclipse.elk.alg.layered.options.LongEdgeOrderingStrategy; @@ -319,16 +320,21 @@ private double minimizeCrossingsNodePortOrderWithCounter(final GraphInfoHolder g /** * Compares all nodes in a each layer and counts how often they are not in model order. * This requires that the {@code SortByInputModelProcessor} ran previously. + * @param graph the graph * @param layers layers to check * @param strategy the ordering strategy to compare the nodes + * @param cmGroupOrderStrategy the strategy of the group model order * @return The number of model order conflicts */ - private int countModelOrderNodeChanges(final LNode[][] layers, final OrderingStrategy strategy) { + private int countModelOrderNodeChanges(final LGraph graph, final LNode[][] layers, final OrderingStrategy strategy, + final GroupOrderStrategy cmGroupOrderStrategy) { int previousLayer = -1; int wrongModelOrder = 0; for (LNode[] layer : layers) { - ModelOrderNodeComparator comp = new ModelOrderNodeComparator( - previousLayer == -1 ? layers[0] : layers[previousLayer], strategy, LongEdgeOrderingStrategy.EQUAL, false); + // FIXME I do not think that the NONE cmGroupOrderStrategy is respected here. + ModelOrderNodeComparator comp = new ModelOrderNodeComparator(graph, + previousLayer == -1 ? layers[0] : layers[previousLayer], strategy, LongEdgeOrderingStrategy.EQUAL, + cmGroupOrderStrategy, false); for (int i = 0; i < layer.length; i++) { for (int j = i + 1; j < layer.length; j++) { if (layer[i].hasProperty(InternalProperties.MODEL_ORDER) @@ -346,15 +352,17 @@ private int countModelOrderNodeChanges(final LNode[][] layers, final OrderingStr /** * Compares all ports in a each layer and counts how often they are not in model order. * This requires that the {@code SortByInputModelProcessor} ran previously. + * @param graph the graph * @param layers layers to check + * @param cmGroupOrderStrategy the strategy of the group model order * @return The number of model order conflicts */ - private int countModelOrderPortChanges(final LNode[][] layers) { + private int countModelOrderPortChanges(final LGraph graph, final LNode[][] layers, GroupOrderStrategy groupOrderStrategy) { int previousLayer = -1; int wrongModelOrder = 0; for (LNode[] layer : layers) { for (LNode lNode : layer) { - Comparator comp = new ModelOrderPortComparator( + Comparator comp = new ModelOrderPortComparator(graph, previousLayer == -1 ? layers[0] : layers[previousLayer], lNode.getGraph().getProperty(LayeredOptions.CONSIDER_MODEL_ORDER_STRATEGY), SortByInputModelProcessor.longEdgeTargetNodePreprocessing(lNode), @@ -410,15 +418,17 @@ private double countCurrentNumberOfCrossingsNodePortOrder(final GraphInfoHolder // The influence of port and node order can be configured. OrderingStrategy modelOrderStrategy = currentGraph.lGraph() .getProperty(LayeredOptions.CONSIDER_MODEL_ORDER_STRATEGY); + GroupOrderStrategy cmGroupOrderStrategy = currentGraph.lGraph() + .getProperty(LayeredOptions.CONSIDER_MODEL_ORDER_GROUP_MODEL_ORDER_CM_GROUP_ORDER_STRATEGY); double crossingCounterNodeInfluence = currentGraph.lGraph() .getProperty(LayeredOptions.CONSIDER_MODEL_ORDER_CROSSING_COUNTER_NODE_INFLUENCE); double crossingCounterPortInfluence = currentGraph.lGraph() .getProperty(LayeredOptions.CONSIDER_MODEL_ORDER_CROSSING_COUNTER_PORT_INFLUENCE); if (modelOrderStrategy != OrderingStrategy.NONE) { modelOrderInfluence += crossingCounterNodeInfluence - * countModelOrderNodeChanges(gD.currentNodeOrder(), modelOrderStrategy); + * countModelOrderNodeChanges(currentGraph.lGraph(), gD.currentNodeOrder(), modelOrderStrategy, cmGroupOrderStrategy); modelOrderInfluence += crossingCounterPortInfluence - * countModelOrderPortChanges(gD.currentNodeOrder()); + * countModelOrderPortChanges(currentGraph.lGraph(), gD.currentNodeOrder(), cmGroupOrderStrategy); } totalCrossings += gD.crossCounter().countAllCrossings(gD.currentNodeOrder()) + modelOrderInfluence; for (LGraph childLGraph : gD.childGraphs()) { diff --git a/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/p3order/ModelOrderBarycenterHeuristic.java b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/p3order/ModelOrderBarycenterHeuristic.java index f70fa6577d..e3c24623b5 100644 --- a/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/p3order/ModelOrderBarycenterHeuristic.java +++ b/plugins/org.eclipse.elk.alg.layered/src/org/eclipse/elk/alg/layered/p3order/ModelOrderBarycenterHeuristic.java @@ -16,8 +16,12 @@ import java.util.List; import java.util.Random; +import org.eclipse.elk.alg.layered.graph.LGraph; import org.eclipse.elk.alg.layered.graph.LNode; +import org.eclipse.elk.alg.layered.intermediate.preserveorder.CMGroupModelOrderCalculator; +import org.eclipse.elk.alg.layered.options.GroupOrderStrategy; import org.eclipse.elk.alg.layered.options.InternalProperties; +import org.eclipse.elk.alg.layered.options.LayerConstraint; import org.eclipse.elk.alg.layered.options.LayeredOptions; /** @@ -51,23 +55,47 @@ public class ModelOrderBarycenterHeuristic extends BarycenterHeuristic { public ModelOrderBarycenterHeuristic(final ForsterConstraintResolver constraintResolver, final Random random, final AbstractBarycenterPortDistributor portDistributor, final LNode[][] graph) { super(constraintResolver, random, portDistributor, graph); + // This may have the problem that the real nodes need to be compared first such that the dummy nodes do not + // overrule model order using the barycenter method. barycenterStateComparator = (n1, n2) -> { + if (n1.hasProperty(LayeredOptions.LAYERING_LAYER_CONSTRAINT) && (n1.getProperty(LayeredOptions.LAYERING_LAYER_CONSTRAINT) == LayerConstraint.FIRST_SEPARATE + || n1.getProperty(LayeredOptions.LAYERING_LAYER_CONSTRAINT) == LayerConstraint.LAST_SEPARATE) + || n2.hasProperty(LayeredOptions.LAYERING_LAYER_CONSTRAINT) && (n2.getProperty(LayeredOptions.LAYERING_LAYER_CONSTRAINT) == LayerConstraint.FIRST_SEPARATE + || n2.getProperty(LayeredOptions.LAYERING_LAYER_CONSTRAINT) == LayerConstraint.LAST_SEPARATE)) { + return 0; + } + LGraph lgraph = n1.getGraph(); + // First check whether the transitive dependencies already determine the ordering. int transitiveComparison = compareBasedOnTansitiveDependencies(n1, n2); if (transitiveComparison != 0) { return transitiveComparison; } + // If this is not the case, consider the different ordering modes for group model order for the comparator. if (n1.hasProperty(InternalProperties.MODEL_ORDER) && n2.hasProperty(InternalProperties.MODEL_ORDER)) { - int value = Integer.compare(n1.getProperty(InternalProperties.MODEL_ORDER), - n2.getProperty(InternalProperties.MODEL_ORDER)); + // The value is either a comparison both model orders or both group model orders with + // model order as secondary criterion. + int value = Integer.compare( + CMGroupModelOrderCalculator.calculateModelOrderOrGroupModelOrder(n1, n2, lgraph, lgraph.getProperty(InternalProperties.MAX_MODEL_ORDER_NODES)), + CMGroupModelOrderCalculator.calculateModelOrderOrGroupModelOrder(n2, n1, lgraph, lgraph.getProperty(InternalProperties.MAX_MODEL_ORDER_NODES))); + if (lgraph.getProperty(LayeredOptions.CONSIDER_MODEL_ORDER_GROUP_MODEL_ORDER_CM_GROUP_ORDER_STRATEGY) == GroupOrderStrategy.ONLY_WITHIN_GROUP) { + // Check list of enforced + // If two nodes are in separate model order groups, do not enforce their ordering and just use barycenter. + if (n1.getProperty(LayeredOptions.CONSIDER_MODEL_ORDER_GROUP_MODEL_ORDER_CROSSING_MINIMIZATION_ID) + != n2.getProperty(LayeredOptions.CONSIDER_MODEL_ORDER_GROUP_MODEL_ORDER_CROSSING_MINIMIZATION_ID)) { + value = 0; + } + } if (value < 0) { updateBiggerAndSmallerAssociations(n1, n2); + return value; } else if (value > 0) { updateBiggerAndSmallerAssociations(n2, n1); + return value; } - return value; } + // If one of the nodes has no model order, fall back to the barycenter method. return compareBasedOnBarycenter(n1, n2); }; }