Skip to content

Commit efccd28

Browse files
Merge pull request #153 from refactorfirst/improve-analysis-performance
Improve analysis performance
2 parents 37babee + e1372cc commit efccd28

File tree

9 files changed

+704
-101
lines changed

9 files changed

+704
-101
lines changed

dsm/src/main/java/org/hjug/dsm/DSM.java

Lines changed: 219 additions & 60 deletions
Large diffs are not rendered by default.

dsm/src/main/java/org/hjug/dsm/EdgeToRemoveInfo.java

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,7 @@
66
@Data
77
public class EdgeToRemoveInfo {
88
private final DefaultWeightedEdge edge;
9-
private final double edgeWeight;
10-
private final int edgeInCycleCount;
9+
private final int removedEdgeWeight;
1110
private final int newCycleCount;
12-
private final double averageCycleNodeCount;
1311
private final double payoff; // impact / effort
1412
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package org.hjug.dsm;
2+
3+
import org.jgrapht.Graph;
4+
import org.jgrapht.alg.cycle.CycleDetector;
5+
import org.jgrapht.alg.cycle.JohnsonSimpleCycles;
6+
import org.jgrapht.graph.AsSubgraph;
7+
8+
import java.util.*;
9+
10+
public class OptimalBackEdgeRemover<V, E> {
11+
private Graph<V, E> graph;
12+
13+
/**
14+
* Constructor initializing with the target graph.
15+
* @param graph The directed weighted graph to analyze
16+
*/
17+
public OptimalBackEdgeRemover(Graph<V, E> graph) {
18+
this.graph = graph;
19+
}
20+
21+
/**
22+
* Finds the optimal back edge(s) to remove to move the graph closer to a DAG.
23+
* @return A set of edges to remove
24+
*/
25+
public Set<E> findOptimalBackEdgesToRemove() {
26+
CycleDetector<V, E> cycleDetector = new CycleDetector<>(graph);
27+
28+
// If the graph is already acyclic, return empty set
29+
if (!cycleDetector.detectCycles()) {
30+
return Collections.emptySet();
31+
}
32+
33+
// Find all cycles in the graph
34+
JohnsonSimpleCycles<V, E> cycleFinder = new JohnsonSimpleCycles<>(graph);
35+
List<List<V>> originalCycles = cycleFinder.findSimpleCycles();
36+
int originalCycleCount = originalCycles.size();
37+
38+
// Identify edges that are part of at least one cycle
39+
Set<E> edgesInCycles = new HashSet<>();
40+
for (List<V> cycle : originalCycles) {
41+
for (int i = 0; i < cycle.size(); i++) {
42+
V source = cycle.get(i);
43+
V target = cycle.get((i + 1) % cycle.size());
44+
E edge = graph.getEdge(source, target);
45+
edgesInCycles.add(edge);
46+
}
47+
}
48+
49+
// Calculate cycle elimination count for each edge
50+
Map<E, Integer> edgeCycleEliminationCount = new HashMap<>();
51+
for (E edge : edgesInCycles) {
52+
// Create a subgraph without this edge
53+
Graph<V, E> subgraph = new AsSubgraph<>(graph, graph.vertexSet(), new HashSet<>(graph.edgeSet()));
54+
subgraph.removeEdge(edge);
55+
56+
// Calculate how many cycles would be eliminated
57+
JohnsonSimpleCycles<V, E> subgraphCycleFinder = new JohnsonSimpleCycles<>(subgraph);
58+
List<List<V>> remainingCycles = subgraphCycleFinder.findSimpleCycles();
59+
int cyclesEliminated = originalCycleCount - remainingCycles.size();
60+
61+
edgeCycleEliminationCount.put(edge, cyclesEliminated);
62+
}
63+
64+
// Find edges that eliminate the most cycles
65+
int maxCycleElimination = 0;
66+
List<E> maxEliminationEdges = new ArrayList<>();
67+
68+
for (Map.Entry<E, Integer> entry : edgeCycleEliminationCount.entrySet()) {
69+
if (entry.getValue() > maxCycleElimination) {
70+
maxCycleElimination = entry.getValue();
71+
maxEliminationEdges.clear();
72+
maxEliminationEdges.add(entry.getKey());
73+
} else if (entry.getValue() == maxCycleElimination) {
74+
maxEliminationEdges.add(entry.getKey());
75+
}
76+
}
77+
78+
// If no cycles are eliminated (shouldn't happen), return empty set
79+
if (maxEliminationEdges.isEmpty() || maxCycleElimination == 0) {
80+
return Collections.emptySet();
81+
}
82+
83+
// If multiple edges eliminate the same number of cycles, choose the one with the lowest weight
84+
if (maxEliminationEdges.size() > 1) {
85+
double minWeight = Double.MAX_VALUE;
86+
List<E> minWeightEdges = new ArrayList<>();
87+
88+
for (E edge : maxEliminationEdges) {
89+
double weight = graph.getEdgeWeight(edge);
90+
if (weight < minWeight) {
91+
minWeight = weight;
92+
minWeightEdges.clear();
93+
minWeightEdges.add(edge);
94+
} else if (weight == minWeight) {
95+
minWeightEdges.add(edge);
96+
}
97+
}
98+
99+
return new HashSet<>(minWeightEdges);
100+
}
101+
102+
// Return the single edge that eliminates the most cycles
103+
return new HashSet<>(maxEliminationEdges);
104+
}
105+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package org.hjug.dsm;
2+
3+
import lombok.extern.slf4j.Slf4j;
4+
import org.jgrapht.alg.cycle.CycleDetector;
5+
import org.jgrapht.graph.AsSubgraph;
6+
import org.jgrapht.opt.graph.sparse.SparseIntDirectedWeightedGraph;
7+
8+
import java.util.HashMap;
9+
import java.util.Map;
10+
11+
@Slf4j
12+
public class SparseGraphCircularReferenceChecker {
13+
14+
private final Map<Integer, AsSubgraph<Integer, Integer>> uniqueSubGraphs = new HashMap<>();
15+
16+
/**
17+
* Detects cycles in the graph that is passed in
18+
* and returns the unique cycles in the graph as a map of subgraphs
19+
*
20+
* @param graph
21+
* @return a Map of unique cycles in the graph
22+
*/
23+
public Map<Integer, AsSubgraph<Integer, Integer>> getCycles(SparseIntDirectedWeightedGraph graph) {
24+
25+
if (!uniqueSubGraphs.isEmpty()) {
26+
return uniqueSubGraphs;
27+
}
28+
29+
// use CycleDetector.findCycles()?
30+
Map<Integer, AsSubgraph<Integer, Integer>> cycles = detectCycles(graph);
31+
32+
cycles.forEach((vertex, subGraph) -> {
33+
int vertexCount = subGraph.vertexSet().size();
34+
int edgeCount = subGraph.edgeSet().size();
35+
36+
if (vertexCount > 1 && edgeCount > 1 && !isDuplicateSubGraph(subGraph, vertex)) {
37+
uniqueSubGraphs.put(vertex, subGraph);
38+
log.debug("Vertex: {} vertex count: {} edge count: {}", vertex, vertexCount, edgeCount);
39+
}
40+
});
41+
42+
return uniqueSubGraphs;
43+
}
44+
45+
private boolean isDuplicateSubGraph(AsSubgraph<Integer, Integer> subGraph, Integer vertex) {
46+
if (!uniqueSubGraphs.isEmpty()) {
47+
for (AsSubgraph<Integer, Integer> renderedSubGraph : uniqueSubGraphs.values()) {
48+
if (renderedSubGraph.vertexSet().size() == subGraph.vertexSet().size()
49+
&& renderedSubGraph.edgeSet().size()
50+
== subGraph.edgeSet().size()
51+
&& renderedSubGraph.vertexSet().contains(vertex)) {
52+
return true;
53+
}
54+
}
55+
}
56+
57+
return false;
58+
}
59+
60+
private Map<Integer, AsSubgraph<Integer, Integer>> detectCycles(
61+
SparseIntDirectedWeightedGraph graph) {
62+
Map<Integer, AsSubgraph<Integer, Integer>> cyclesForEveryVertexMap = new HashMap<>();
63+
CycleDetector<Integer, Integer> cycleDetector = new CycleDetector<>(graph);
64+
cycleDetector.findCycles().forEach(v -> {
65+
AsSubgraph<Integer, Integer> subGraph =
66+
new AsSubgraph<>(graph, cycleDetector.findCyclesContainingVertex(v));
67+
cyclesForEveryVertexMap.put(v, subGraph);
68+
});
69+
return cyclesForEveryVertexMap;
70+
}
71+
}
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
package org.hjug.dsm;
2+
3+
import org.jgrapht.Graph;
4+
import org.jgrapht.Graphs;
5+
import org.jgrapht.alg.connectivity.KosarajuStrongConnectivityInspector;
6+
import org.jgrapht.alg.util.Triple;
7+
import org.jgrapht.graph.DefaultWeightedEdge;
8+
import org.jgrapht.opt.graph.sparse.SparseIntDirectedWeightedGraph;
9+
10+
import java.util.*;
11+
import java.util.concurrent.ConcurrentHashMap;
12+
import java.util.concurrent.ConcurrentLinkedQueue;
13+
import java.util.concurrent.ConcurrentSkipListSet;
14+
import java.util.concurrent.CopyOnWriteArrayList;
15+
import java.util.stream.Collectors;
16+
import java.util.stream.IntStream;
17+
18+
// TODO: Delete
19+
class SparseIntDWGEdgeRemovalCalculator {
20+
private final Graph<String, DefaultWeightedEdge> graph;
21+
SparseIntDirectedWeightedGraph sparseGraph;
22+
List<Triple<Integer, Integer, Double>> sparseEdges;
23+
List<Integer> sparseEdgesAboveDiagonal;
24+
private final double sumOfEdgeWeightsAboveDiagonal;
25+
int vertexCount;
26+
Map<String, Integer> vertexToInt;
27+
Map<Integer, String> intToVertex;
28+
29+
30+
SparseIntDWGEdgeRemovalCalculator(
31+
Graph<String, DefaultWeightedEdge> graph,
32+
SparseIntDirectedWeightedGraph sparseGraph,
33+
List<Triple<Integer, Integer, Double>> sparseEdges,
34+
List<Integer> sparseEdgesAboveDiagonal,
35+
double sumOfEdgeWeightsAboveDiagonal,
36+
int vertexCount,
37+
Map<String, Integer> vertexToInt,
38+
Map<Integer, String> intToVertex) {
39+
this.graph = graph;
40+
this.sparseGraph = sparseGraph;
41+
this.sparseEdges = new CopyOnWriteArrayList<>(sparseEdges);
42+
this.sparseEdgesAboveDiagonal = new CopyOnWriteArrayList<>(sparseEdgesAboveDiagonal);
43+
this.sumOfEdgeWeightsAboveDiagonal = sumOfEdgeWeightsAboveDiagonal;
44+
this.vertexCount = vertexCount;
45+
this.vertexToInt = new ConcurrentHashMap<>(vertexToInt);
46+
this.intToVertex = new ConcurrentHashMap<>(intToVertex);
47+
48+
}
49+
50+
public List<EdgeToRemoveInfo> getImpactOfSparseEdgesAboveDiagonalIfRemoved() {
51+
return sparseEdgesAboveDiagonal.parallelStream()
52+
.map(this::calculateSparseEdgeToRemoveInfo)
53+
.sorted(Comparator.comparing(EdgeToRemoveInfo::getPayoff).thenComparing(EdgeToRemoveInfo::getRemovedEdgeWeight))
54+
.collect(Collectors.toList());
55+
}
56+
57+
private EdgeToRemoveInfo calculateSparseEdgeToRemoveInfo(Integer edgeToRemove) {
58+
//clone graph and remove edge
59+
int source = sparseGraph.getEdgeSource(edgeToRemove);
60+
int target = sparseGraph.getEdgeTarget(edgeToRemove);
61+
double weight = sparseGraph.getEdgeWeight(edgeToRemove);
62+
Triple<Integer, Integer, Double> removedEdge = Triple.of(source, target, weight);
63+
64+
List<Triple<Integer, Integer, Double>> tempUpdatedEdgeList = new ArrayList<>(sparseEdges);
65+
tempUpdatedEdgeList.remove(removedEdge);
66+
List<Triple<Integer, Integer, Double>> updatedEdgeList = new CopyOnWriteArrayList<>(tempUpdatedEdgeList);
67+
68+
SparseIntDirectedWeightedGraph improvedGraph = new SparseIntDirectedWeightedGraph(vertexCount, updatedEdgeList);
69+
70+
// find edges above diagonal
71+
List<Integer> sortedSparseVertices = orderVertices(improvedGraph);
72+
List<Integer> updatedEdges = getSparseEdgesAboveDiagonal(improvedGraph, sortedSparseVertices);
73+
74+
// calculate new graph statistics
75+
int newEdgeCount = updatedEdges.size();
76+
double newEdgeWeightSum = updatedEdges.stream()
77+
.mapToDouble(improvedGraph::getEdgeWeight).sum();
78+
DefaultWeightedEdge defaultWeightedEdge =
79+
graph.getEdge(intToVertex.get(source), intToVertex.get(target));
80+
double payoff = (sumOfEdgeWeightsAboveDiagonal - newEdgeWeightSum) / weight;
81+
return new EdgeToRemoveInfo(defaultWeightedEdge, (int) weight, newEdgeCount, payoff);
82+
}
83+
84+
private List<Integer> orderVertices(SparseIntDirectedWeightedGraph sparseGraph) {
85+
List<Set<Integer>> sccs = new CopyOnWriteArrayList<>(findStronglyConnectedSparseGraphComponents(sparseGraph));
86+
// List<Integer> sparseIntSortedActivities = topologicalSortSparseGraph(sccs, sparseGraph);
87+
List<Integer> sparseIntSortedActivities = topologicalParallelSortSparseGraph(sccs, sparseGraph);
88+
// reversing corrects rendering of the DSM
89+
// with sources as rows and targets as columns
90+
// was needed after AI solution was generated and iterated
91+
Collections.reverse(sparseIntSortedActivities);
92+
93+
return new CopyOnWriteArrayList<>(sparseIntSortedActivities);
94+
}
95+
96+
/**
97+
* Kosaraju SCC detector avoids stack overflow.
98+
* It is used by JGraphT's CycleDetector, and makes sense to use it here as well for consistency
99+
*
100+
* @param graph
101+
* @return
102+
*/
103+
private List<Set<Integer>> findStronglyConnectedSparseGraphComponents(Graph<Integer, Integer> graph) {
104+
KosarajuStrongConnectivityInspector<Integer, Integer> kosaraju =
105+
new KosarajuStrongConnectivityInspector<>(graph);
106+
return kosaraju.stronglyConnectedSets();
107+
}
108+
109+
private List<Integer> topologicalSortSparseGraph(List<Set<Integer>> sccs, Graph<Integer, Integer> graph) {
110+
List<Integer> sortedActivities = new ArrayList<>();
111+
Set<Integer> visited = new HashSet<>();
112+
113+
sccs.parallelStream()
114+
.flatMap(Set::parallelStream)
115+
.filter(activity -> !visited.contains(activity))
116+
.forEach(activity -> topologicalSortUtilSparseGraph(activity, visited, sortedActivities, graph));
117+
118+
119+
Collections.reverse(sortedActivities);
120+
return sortedActivities;
121+
}
122+
123+
private void topologicalSortUtilSparseGraph(
124+
Integer activity, Set<Integer> visited, List<Integer> sortedActivities, Graph<Integer, Integer> graph) {
125+
visited.add(activity);
126+
127+
for (Integer neighbor : Graphs.successorListOf(graph, activity)) {
128+
if (!visited.contains(neighbor)) {
129+
topologicalSortUtilSparseGraph(neighbor, visited, sortedActivities, graph);
130+
}
131+
}
132+
133+
sortedActivities.add(activity);
134+
}
135+
136+
private List<Integer> getSparseEdgesAboveDiagonal(SparseIntDirectedWeightedGraph sparseGraph, List<Integer> sortedActivities) {
137+
ConcurrentLinkedQueue<Integer> sparseEdgesAboveDiagonal = new ConcurrentLinkedQueue<>();
138+
139+
int size = sortedActivities.size();
140+
IntStream.range(0, size).parallel().forEach(i -> {
141+
for (int j = i + 1; j < size; j++) {
142+
Integer edge = sparseGraph.getEdge(
143+
sortedActivities.get(i),
144+
sortedActivities.get(j)
145+
);
146+
if (edge != null) {
147+
sparseEdgesAboveDiagonal.add(edge);
148+
}
149+
}
150+
});
151+
152+
return new ArrayList<>(sparseEdgesAboveDiagonal);
153+
}
154+
155+
private List<Integer> topologicalParallelSortSparseGraph(List<Set<Integer>> sccs, Graph<Integer, Integer> graph) {
156+
ConcurrentLinkedQueue<Integer> sortedActivities = new ConcurrentLinkedQueue<>();
157+
Set<Integer> visited = new ConcurrentSkipListSet<>();
158+
159+
sccs.parallelStream()
160+
.flatMap(Set::parallelStream)
161+
.filter(activity -> !visited.contains(activity))
162+
.forEach(activity -> topologicalSortUtilSparseGraph(activity, visited, sortedActivities, graph));
163+
164+
ArrayList<Integer> sortedActivitiesList = new ArrayList<>(sortedActivities);
165+
Collections.reverse(sortedActivitiesList);
166+
return sortedActivitiesList;
167+
}
168+
169+
private void topologicalSortUtilSparseGraph(
170+
Integer activity, Set<Integer> visited, ConcurrentLinkedQueue<Integer> sortedActivities, Graph<Integer, Integer> graph) {
171+
visited.add(activity);
172+
173+
Graphs.successorListOf(graph, activity).parallelStream()
174+
.filter(neighbor -> !visited.contains(neighbor))
175+
.forEach(neighbor -> topologicalSortUtilSparseGraph(neighbor, visited, sortedActivities, graph));
176+
177+
sortedActivities.add(activity);
178+
}
179+
180+
}

dsm/src/test/java/org/hjug/dsm/DSMTest.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,5 @@ void getImpactOfEdgesAboveDiagonalIfRemoved() {
129129

130130
assertEquals("(H : E)", infos.get(0).getEdge().toString());
131131
assertEquals(2, infos.get(0).getNewCycleCount());
132-
assertEquals(4.5, infos.get(0).getAverageCycleNodeCount());
133132
}
134133
}

0 commit comments

Comments
 (0)