From 044e4b2a4011f3286be2e7fecaa4bf4078ed40d8 Mon Sep 17 00:00:00 2001
From: Eddie Groh <groh@fortiss.org>
Date: Mon, 31 Jul 2023 13:46:04 +0200
Subject: [PATCH] Added betweeness centrality calculation

Issue-Ref: 4310
Issue-Url: https://git.fortiss.org/af3/af3/-/issues/4310

Signed-off-by: Eddie Groh <groh@fortiss.org>
---
 .../tooling/ext/quality/data/MetricKey.java   |   2 +
 .../quality/service/ModelQualityService.java  | 163 ++++++++++++++++++
 2 files changed, 165 insertions(+)

diff --git a/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/data/MetricKey.java b/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/data/MetricKey.java
index 148d1d752..3d494172e 100644
--- a/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/data/MetricKey.java
+++ b/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/data/MetricKey.java
@@ -40,6 +40,8 @@ public enum MetricKey {
 	SAFETY_LEVEL(false, true),
 	/** How deeply nested the corresponding element is */
 	NESTING_LEVEL(false),
+	/** The betweenness centrality of the corresponding element */
+	BETWEENESS_CENTRALITY(false),
 
 	//
 	// Metrics which are collected over all children nodes
diff --git a/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/service/ModelQualityService.java b/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/service/ModelQualityService.java
index 0991e64a1..2447695e8 100644
--- a/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/service/ModelQualityService.java
+++ b/org.fortiss.tooling.ext.quality/src/org/fortiss/tooling/ext/quality/service/ModelQualityService.java
@@ -29,15 +29,25 @@ import java.time.LocalDateTime;
 import java.time.format.DateTimeFormatter;
 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.Map;
 import java.util.Map.Entry;
 import java.util.Optional;
+import java.util.Queue;
+import java.util.Set;
+import java.util.Stack;
 import java.util.stream.Collectors;
 import java.util.stream.Stream;
 
 import org.eclipse.emf.common.util.EList;
 import org.eclipse.emf.ecore.EObject;
+import org.fortiss.tooling.base.model.base.EntryConnectorBase;
+import org.fortiss.tooling.base.model.base.ExitConnectorBase;
+import org.fortiss.tooling.base.model.element.IConnection;
+import org.fortiss.tooling.base.model.element.IConnector;
 import org.fortiss.tooling.base.model.element.IHierarchicElement;
 import org.fortiss.tooling.ext.quality.IMetricProvider;
 import org.fortiss.tooling.ext.quality.data.MetricDataManager;
@@ -189,6 +199,159 @@ public class ModelQualityService extends EObjectAwareServiceBase<IMetricProvider
 			}
 
 			manager.getHierarchicLookupTable().put(currentElement, node);
+
+			calculateBetweennessCentrality(currentElement, manager);
+		}
+	}
+
+	/**
+	 * Creates a list of nodes contained in the graph centered around the provided element
+	 * It will contain all elements contained within the provided element as well as all elements
+	 * connected to the provided element
+	 * 
+	 * @param scopeElement
+	 *            element defining the scope of the graph
+	 * @return node list
+	 */
+	private static Set<IHierarchicElement> getLocalGraphView(IHierarchicElement scopeElement) {
+		// Create a view consisting of all nodes inside this element, as well as all elements
+		// connected to this element
+
+		// This ensures that elements inside this element communicating with elements outside
+		// this element are properly recognized
+		Set<IHierarchicElement> graphNodes = new HashSet<>();
+		graphNodes.addAll(scopeElement.getContainedElements());
+
+		// Collect all elements to which currentElement has a outgoing connection
+		for(ExitConnectorBase exitConnector : EcoreUtils.pickInstanceOf(ExitConnectorBase.class,
+				scopeElement.getConnectors())) {
+			for(IConnection exitConnection : exitConnector.getOutgoing()) {
+				graphNodes.add(exitConnection.getTarget().getOwner());
+			}
+		}
+		// Collect all elements to which currentElement has a incoming connection
+		for(EntryConnectorBase entryConnector : EcoreUtils.pickInstanceOf(EntryConnectorBase.class,
+				scopeElement.getConnectors())) {
+			for(IConnection entryConnection : entryConnector.getIncoming()) {
+				graphNodes.add(entryConnection.getSource().getOwner());
+			}
+		}
+		return graphNodes;
+	}
+
+	/**
+	 * Calculates and saves the betweenness centrality of all elements contained inside the provided
+	 * scopeElement
+	 * 
+	 * @param scopeElement
+	 *            the scope of this calculation
+	 * @param manager
+	 *            location to save the metrics
+	 */
+	private static void calculateBetweennessCentrality(IHierarchicElement scopeElement,
+			MetricDataManager manager) {
+
+		// Abort if no elements are found
+		if(!scopeElement.getContainedElements().isEmpty()) {
+
+			// Calculate betweenness centrality
+
+			Set<IHierarchicElement> graphNodes = getLocalGraphView(scopeElement);
+			// Initialize the betweenness values for all nodes to zero
+			Map<IHierarchicElement, Double> betweenness = new HashMap<>();
+			for(IHierarchicElement elementNode : graphNodes) {
+				betweenness.put(elementNode, 0.0);
+			}
+
+			// Iterate over all collected nodes
+			for(IHierarchicElement elementNode : graphNodes) {
+
+				// Initialize all variables
+				Stack<IHierarchicElement> stack = new Stack<>();
+				Map<IHierarchicElement, List<IHierarchicElement>> pred = new HashMap<>();
+				Map<IHierarchicElement, Integer> dist = new HashMap<>();
+				Map<IHierarchicElement, Double> sigma = new HashMap<>();
+				Queue<IHierarchicElement> queue = new LinkedList<>();
+
+				for(IHierarchicElement v : graphNodes) {
+					pred.put(v, new ArrayList<>());
+					dist.put(v, -1);
+					sigma.put(v, 0.0);
+				}
+				sigma.put(elementNode, 1.0);
+				dist.put(elementNode, 0);
+				queue.add(elementNode);
+
+				while(!queue.isEmpty()) {
+					IHierarchicElement v = queue.remove();
+					stack.push(v);
+
+					// Iterate over all connectors going out from the element
+					for(ExitConnectorBase exitConnectors : EcoreUtils
+							.pickInstanceOf(ExitConnectorBase.class, v.getConnectors())) {
+						// Iterate over all connections
+						for(IConnection outgoingConnection : exitConnectors.getOutgoing()) {
+
+							List<IHierarchicElement> targetElements = new ArrayList<>();
+							IHierarchicElement primaryTargetElement =
+									outgoingConnection.getTarget().getOwner();
+							// When we have a connection to the scopeElement, we have a connection
+							// leaving the local scope
+							if(scopeElement == primaryTargetElement) {
+								// Continue searching for outgoing connections on the target
+								// Connector
+								IConnector targetConnector = outgoingConnection.getTarget();
+								for(IConnection secondaryOutgoingConnection : targetConnector
+										.getOutgoing()) {
+									targetElements.add(
+											secondaryOutgoingConnection.getTarget().getOwner());
+								}
+							} else {
+								targetElements.add(primaryTargetElement);
+							}
+
+							for(IHierarchicElement targetElement : targetElements) {
+
+								// Check if element is in node list, to avoid creating data for
+								// nodes outside of our graph
+								if(graphNodes.contains(targetElement)) {
+									if(dist.get(targetElement) < 0) {
+										queue.add(targetElement);
+										dist.put(targetElement, dist.get(v) + 1);
+									}
+									if(dist.get(targetElement) == dist.get(v) + 1) {
+										sigma.put(targetElement,
+												sigma.get(targetElement) + sigma.get(v));
+										pred.get(targetElement).add(v);
+									}
+								}
+							}
+						}
+					}
+				}
+				// Initialize delta
+				Map<IHierarchicElement, Double> delta = new HashMap<>();
+				for(IHierarchicElement v : graphNodes) {
+					delta.put(v, 0.0);
+				}
+
+				while(!stack.isEmpty()) {
+					IHierarchicElement w = stack.pop();
+					for(IHierarchicElement v : pred.get(w)) {
+						double c = (sigma.get(v) / sigma.get(w)) * (1.0 + delta.get(w));
+						delta.put(v, delta.get(v) + c);
+					}
+					if(!w.equals(elementNode)) {
+						betweenness.put(w, betweenness.get(w) + delta.get(w));
+					}
+				}
+			}
+
+			// Save metric
+			for(IHierarchicElement child : scopeElement.getContainedElements()) {
+				manager.getHierarchicLookupTable().get(child).getStoredMetrics()
+						.put(MetricKey.BETWEENESS_CENTRALITY, betweenness.get(child));
+			}
 		}
 	}
 
-- 
GitLab