Skip to content

Commit 11dceac

Browse files
WIP
1 parent e47b9df commit 11dceac

File tree

8 files changed

+689
-87
lines changed

8 files changed

+689
-87
lines changed

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

Lines changed: 217 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: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
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+
class SparseIntDWGEdgeRemovalCalculator {
19+
private final Graph<String, DefaultWeightedEdge> graph;
20+
SparseIntDirectedWeightedGraph sparseGraph;
21+
List<Triple<Integer, Integer, Double>> sparseEdges;
22+
List<Integer> sparseEdgesAboveDiagonal;
23+
private final double sumOfEdgeWeightsAboveDiagonal;
24+
int vertexCount;
25+
Map<String, Integer> vertexToInt;
26+
Map<Integer, String> intToVertex;
27+
28+
29+
SparseIntDWGEdgeRemovalCalculator(
30+
Graph<String, DefaultWeightedEdge> graph,
31+
SparseIntDirectedWeightedGraph sparseGraph,
32+
List<Triple<Integer, Integer, Double>> sparseEdges,
33+
List<Integer> sparseEdgesAboveDiagonal,
34+
double sumOfEdgeWeightsAboveDiagonal,
35+
int vertexCount,
36+
Map<String, Integer> vertexToInt,
37+
Map<Integer, String> intToVertex) {
38+
//TODO: Use concurrent types where possible
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+
// reversing corrects rendering of the DSM
88+
// with sources as rows and targets as columns
89+
// was needed after AI solution was generated and iterated
90+
Collections.reverse(sparseIntSortedActivities);
91+
92+
return new CopyOnWriteArrayList<>(sparseIntSortedActivities);
93+
}
94+
95+
/**
96+
* Kosaraju SCC detector avoids stack overflow.
97+
* It is used by JGraphT's CycleDetector, and makes sense to use it here as well for consistency
98+
*
99+
* @param graph
100+
* @return
101+
*/
102+
private List<Set<Integer>> findStronglyConnectedSparseGraphComponents(Graph<Integer, Integer> graph) {
103+
KosarajuStrongConnectivityInspector<Integer, Integer> kosaraju =
104+
new KosarajuStrongConnectivityInspector<>(graph);
105+
return kosaraju.stronglyConnectedSets();
106+
}
107+
108+
private List<Integer> topologicalSortSparseGraph(List<Set<Integer>> sccs, Graph<Integer, Integer> graph) {
109+
List<Integer> sortedActivities = new ArrayList<>();
110+
Set<Integer> visited = new HashSet<>();
111+
112+
sccs.parallelStream()
113+
.flatMap(Set::parallelStream)
114+
.filter(activity -> !visited.contains(activity))
115+
.forEach(activity -> topologicalSortUtilSparseGraph(activity, visited, sortedActivities, graph));
116+
117+
118+
Collections.reverse(sortedActivities);
119+
return sortedActivities;
120+
}
121+
122+
private void topologicalSortUtilSparseGraph(
123+
Integer activity, Set<Integer> visited, List<Integer> sortedActivities, Graph<Integer, Integer> graph) {
124+
visited.add(activity);
125+
126+
for (Integer neighbor : Graphs.successorListOf(graph, activity)) {
127+
if (!visited.contains(neighbor)) {
128+
topologicalSortUtilSparseGraph(neighbor, visited, sortedActivities, graph);
129+
}
130+
}
131+
132+
sortedActivities.add(activity);
133+
}
134+
135+
private List<Integer> getSparseEdgesAboveDiagonal(SparseIntDirectedWeightedGraph sparseGraph, List<Integer> sortedActivities) {
136+
ConcurrentLinkedQueue<Integer> sparseEdgesAboveDiagonal = new ConcurrentLinkedQueue<>();
137+
138+
int size = sortedActivities.size();
139+
IntStream.range(0, size).parallel().forEach(i -> {
140+
for (int j = i + 1; j < size; j++) {
141+
Integer edge = sparseGraph.getEdge(
142+
sortedActivities.get(i),
143+
sortedActivities.get(j)
144+
);
145+
if (edge != null) {
146+
sparseEdgesAboveDiagonal.add(edge);
147+
}
148+
}
149+
});
150+
151+
return new ArrayList<>(sparseEdgesAboveDiagonal);
152+
}
153+
154+
private List<Integer> topologicalParallelSortSparseGraph(List<Set<Integer>> sccs, Graph<Integer, Integer> graph) {
155+
ConcurrentLinkedQueue<Integer> sortedActivities = new ConcurrentLinkedQueue<>();
156+
Set<Integer> visited = new ConcurrentSkipListSet<>();
157+
158+
sccs.stream()
159+
.flatMap(Set::parallelStream)
160+
.filter(activity -> !visited.contains(activity))
161+
.forEach(activity -> topologicalSortUtilSparseGraph(activity, visited, sortedActivities, graph));
162+
163+
ArrayList<Integer> sortedActivitiesList = new ArrayList<>(sortedActivities);
164+
Collections.reverse(sortedActivitiesList);
165+
return sortedActivitiesList;
166+
}
167+
168+
private void topologicalSortUtilSparseGraph(
169+
Integer activity, Set<Integer> visited, ConcurrentLinkedQueue<Integer> sortedActivities, Graph<Integer, Integer> graph) {
170+
visited.add(activity);
171+
172+
for (Integer neighbor : Graphs.successorListOf(graph, activity)) {
173+
if (!visited.contains(neighbor)) {
174+
topologicalSortUtilSparseGraph(neighbor, visited, sortedActivities, graph);
175+
}
176+
}
177+
178+
sortedActivities.add(activity);
179+
}
180+
181+
}

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)