Skip to content
This repository was archived by the owner on Jun 18, 2026. It is now read-only.

Commit f097681

Browse files
feat: RandomWalkAnalyzer — random walk statistics for network analysis
Monte Carlo random walk simulation with: - Hitting time, commute distance, cover time, return time - Mixing time via transition matrix power iteration - Stationary distribution (degree-proportional for undirected) - Walk trace and visit frequency for visualization - Summary statistics Usage: new RandomWalkAnalyzer().hittingTime(graph, src, dst)
1 parent 4e3c5de commit f097681

1 file changed

Lines changed: 289 additions & 0 deletions

File tree

Lines changed: 289 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,289 @@
1+
package gvisual;
2+
3+
import edu.uci.ics.jung.graph.Graph;
4+
import java.util.*;
5+
6+
/**
7+
* Analyzes random walk behavior on graphs — a fundamental tool for
8+
* understanding information diffusion, network navigability, and
9+
* structural properties of social networks.
10+
*
11+
* <p>Random walks model how a "walker" traverses a graph by repeatedly
12+
* moving to a uniformly random neighbor. The statistics of these walks
13+
* reveal deep structural properties:</p>
14+
*
15+
* <ul>
16+
* <li><strong>Hitting time</strong> — expected steps to reach node t from s</li>
17+
* <li><strong>Commute distance</strong> — H(s,t) + H(t,s), a symmetric metric</li>
18+
* <li><strong>Cover time</strong> — expected steps to visit every node</li>
19+
* <li><strong>Mixing time</strong> — steps until distribution converges to stationary</li>
20+
* <li><strong>Return time</strong> — expected steps to return to start</li>
21+
* <li><strong>Stationary distribution</strong> — long-run visit probabilities</li>
22+
* </ul>
23+
*
24+
* @author zalenix
25+
*/
26+
public class RandomWalkAnalyzer {
27+
28+
private final Random rng;
29+
private final int defaultSimulations;
30+
31+
public RandomWalkAnalyzer() {
32+
this(10000, new Random());
33+
}
34+
35+
public RandomWalkAnalyzer(int simulations, Random rng) {
36+
if (simulations < 1) throw new IllegalArgumentException("simulations must be >= 1");
37+
if (rng == null) throw new IllegalArgumentException("rng must not be null");
38+
this.defaultSimulations = simulations;
39+
this.rng = rng;
40+
}
41+
42+
public <V, E> double hittingTime(Graph<V, E> graph, V source, V target) {
43+
validateGraph(graph);
44+
validateNode(graph, source, "source");
45+
validateNode(graph, target, "target");
46+
if (source.equals(target)) return 0.0;
47+
48+
long totalSteps = 0;
49+
int reached = 0;
50+
int maxSteps = graph.getVertexCount() * graph.getVertexCount() * 10;
51+
52+
for (int sim = 0; sim < defaultSimulations; sim++) {
53+
int steps = simulateWalkToTarget(graph, source, target, maxSteps);
54+
if (steps >= 0) { totalSteps += steps; reached++; }
55+
}
56+
return reached == 0 ? Double.POSITIVE_INFINITY : (double) totalSteps / reached;
57+
}
58+
59+
public <V, E> Map<V, Double> hittingTimesFrom(Graph<V, E> graph, V source) {
60+
validateGraph(graph);
61+
validateNode(graph, source, "source");
62+
Map<V, Double> result = new LinkedHashMap<>();
63+
for (V target : graph.getVertices()) {
64+
result.put(target, hittingTime(graph, source, target));
65+
}
66+
return result;
67+
}
68+
69+
public <V, E> double commuteDistance(Graph<V, E> graph, V nodeA, V nodeB) {
70+
return hittingTime(graph, nodeA, nodeB) + hittingTime(graph, nodeB, nodeA);
71+
}
72+
73+
public <V, E> double coverTime(Graph<V, E> graph, V source) {
74+
validateGraph(graph);
75+
validateNode(graph, source, "source");
76+
long totalSteps = 0;
77+
int maxSteps = graph.getVertexCount() * graph.getVertexCount() * 20;
78+
for (int sim = 0; sim < defaultSimulations; sim++) {
79+
totalSteps += simulateCoverWalk(graph, source, maxSteps);
80+
}
81+
return (double) totalSteps / defaultSimulations;
82+
}
83+
84+
public <V, E> double returnTime(Graph<V, E> graph, V node) {
85+
validateGraph(graph);
86+
validateNode(graph, node, "node");
87+
if (graph.degree(node) == 0) return Double.POSITIVE_INFINITY;
88+
long totalSteps = 0;
89+
int maxSteps = graph.getVertexCount() * graph.getVertexCount() * 10;
90+
for (int sim = 0; sim < defaultSimulations; sim++) {
91+
totalSteps += simulateReturnWalk(graph, node, maxSteps);
92+
}
93+
return (double) totalSteps / defaultSimulations;
94+
}
95+
96+
public <V, E> int mixingTime(Graph<V, E> graph, double epsilon) {
97+
validateGraph(graph);
98+
if (epsilon <= 0 || epsilon >= 1) throw new IllegalArgumentException("epsilon must be in (0,1)");
99+
int n = graph.getVertexCount();
100+
if (n == 0) return 0;
101+
102+
List<V> nodeList = new ArrayList<>(graph.getVertices());
103+
Map<V, Integer> nodeIndex = new HashMap<>();
104+
for (int i = 0; i < nodeList.size(); i++) nodeIndex.put(nodeList.get(i), i);
105+
106+
Map<V, Double> stationary = stationaryDistribution(graph);
107+
double[] stationaryArr = new double[n];
108+
for (int i = 0; i < n; i++) stationaryArr[i] = stationary.get(nodeList.get(i));
109+
110+
double[][] P = buildTransitionMatrix(graph, nodeList, nodeIndex);
111+
int maxTime = n * n * 5;
112+
double[][] dist = new double[n][n];
113+
for (int i = 0; i < n; i++) dist[i][i] = 1.0;
114+
115+
for (int t = 1; t <= maxTime; t++) {
116+
double[][] newDist = new double[n][n];
117+
for (int start = 0; start < n; start++)
118+
for (int j = 0; j < n; j++)
119+
for (int k = 0; k < n; k++)
120+
newDist[start][j] += dist[start][k] * P[k][j];
121+
dist = newDist;
122+
123+
double maxTV = 0;
124+
for (int start = 0; start < n; start++) {
125+
double tv = 0;
126+
for (int j = 0; j < n; j++) tv += Math.abs(dist[start][j] - stationaryArr[j]);
127+
maxTV = Math.max(maxTV, tv / 2.0);
128+
}
129+
if (maxTV <= epsilon) return t;
130+
}
131+
return maxTime;
132+
}
133+
134+
public <V, E> Map<V, Double> stationaryDistribution(Graph<V, E> graph) {
135+
validateGraph(graph);
136+
Map<V, Double> dist = new LinkedHashMap<>();
137+
int totalDegree = 0;
138+
for (V v : graph.getVertices()) totalDegree += graph.degree(v);
139+
140+
if (totalDegree == 0) {
141+
double uniform = 1.0 / graph.getVertexCount();
142+
for (V v : graph.getVertices()) dist.put(v, uniform);
143+
return dist;
144+
}
145+
for (V v : graph.getVertices()) dist.put(v, (double) graph.degree(v) / totalDegree);
146+
return dist;
147+
}
148+
149+
public <V, E> List<V> walkTrace(Graph<V, E> graph, V source, int steps) {
150+
validateGraph(graph);
151+
validateNode(graph, source, "source");
152+
if (steps < 0) throw new IllegalArgumentException("steps must be >= 0");
153+
154+
List<V> trace = new ArrayList<>(steps + 1);
155+
V current = source;
156+
trace.add(current);
157+
for (int i = 0; i < steps; i++) {
158+
List<V> neighbors = new ArrayList<>(graph.getNeighbors(current));
159+
if (neighbors.isEmpty()) break;
160+
current = neighbors.get(rng.nextInt(neighbors.size()));
161+
trace.add(current);
162+
}
163+
return trace;
164+
}
165+
166+
public <V, E> Map<V, Double> visitFrequency(Graph<V, E> graph, V source, int steps) {
167+
List<V> trace = walkTrace(graph, source, steps);
168+
Map<V, Double> freq = new LinkedHashMap<>();
169+
for (V v : graph.getVertices()) freq.put(v, 0.0);
170+
for (V v : trace) freq.put(v, freq.get(v) + 1);
171+
double total = trace.size();
172+
for (V v : freq.keySet()) freq.put(v, freq.get(v) / total);
173+
return freq;
174+
}
175+
176+
public <V, E> WalkSummary<V> summarize(Graph<V, E> graph) {
177+
validateGraph(graph);
178+
Map<V, Double> stationary = stationaryDistribution(graph);
179+
V mostVisited = null; double maxProb = -1;
180+
V leastVisited = null; double minProb = Double.MAX_VALUE;
181+
for (Map.Entry<V, Double> e : stationary.entrySet()) {
182+
if (e.getValue() > maxProb) { maxProb = e.getValue(); mostVisited = e.getKey(); }
183+
if (e.getValue() < minProb) { minProb = e.getValue(); leastVisited = e.getKey(); }
184+
}
185+
double ct = coverTime(graph, mostVisited);
186+
return new WalkSummary<>(graph.getVertexCount(), graph.getEdgeCount(), stationary,
187+
mostVisited, maxProb, leastVisited, minProb, ct, mostVisited);
188+
}
189+
190+
public static class WalkSummary<V> {
191+
private final int nodeCount, edgeCount;
192+
private final Map<V, Double> stationaryDistribution;
193+
private final V mostVisitedNode, leastVisitedNode, coverTimeSource;
194+
private final double mostVisitedProb, leastVisitedProb, coverTimeFromBest;
195+
196+
public WalkSummary(int nc, int ec, Map<V, Double> sd, V mv, double mvp,
197+
V lv, double lvp, double ct, V cts) {
198+
this.nodeCount = nc; this.edgeCount = ec;
199+
this.stationaryDistribution = Collections.unmodifiableMap(sd);
200+
this.mostVisitedNode = mv; this.mostVisitedProb = mvp;
201+
this.leastVisitedNode = lv; this.leastVisitedProb = lvp;
202+
this.coverTimeFromBest = ct; this.coverTimeSource = cts;
203+
}
204+
public int getNodeCount() { return nodeCount; }
205+
public int getEdgeCount() { return edgeCount; }
206+
public Map<V, Double> getStationaryDistribution() { return stationaryDistribution; }
207+
public V getMostVisitedNode() { return mostVisitedNode; }
208+
public double getMostVisitedProb() { return mostVisitedProb; }
209+
public V getLeastVisitedNode() { return leastVisitedNode; }
210+
public double getLeastVisitedProb() { return leastVisitedProb; }
211+
public double getCoverTimeFromBest() { return coverTimeFromBest; }
212+
public V getCoverTimeSource() { return coverTimeSource; }
213+
214+
@Override public String toString() {
215+
return String.format("WalkSummary{nodes=%d, edges=%d, mostVisited=%s(%.4f), " +
216+
"leastVisited=%s(%.4f), coverTime=%.1f from %s}",
217+
nodeCount, edgeCount, mostVisitedNode, mostVisitedProb,
218+
leastVisitedNode, leastVisitedProb, coverTimeFromBest, coverTimeSource);
219+
}
220+
}
221+
222+
// ── Private Helpers ────────────────────────────────────────────────
223+
224+
private <V, E> int simulateWalkToTarget(Graph<V, E> graph, V source, V target, int maxSteps) {
225+
V current = source;
226+
for (int step = 1; step <= maxSteps; step++) {
227+
List<V> neighbors = new ArrayList<>(graph.getNeighbors(current));
228+
if (neighbors.isEmpty()) return -1;
229+
current = neighbors.get(rng.nextInt(neighbors.size()));
230+
if (current.equals(target)) return step;
231+
}
232+
return -1;
233+
}
234+
235+
private <V, E> long simulateCoverWalk(Graph<V, E> graph, V source, int maxSteps) {
236+
Set<V> visited = new HashSet<>();
237+
V current = source;
238+
visited.add(current);
239+
Set<V> reachable = new HashSet<>();
240+
Queue<V> q = new LinkedList<>();
241+
q.add(source); reachable.add(source);
242+
while (!q.isEmpty()) { V v = q.poll(); for (V n : graph.getNeighbors(v)) if (reachable.add(n)) q.add(n); }
243+
int target = reachable.size();
244+
if (visited.size() >= target) return 0;
245+
for (int step = 1; step <= maxSteps; step++) {
246+
List<V> nb = new ArrayList<>(graph.getNeighbors(current));
247+
if (nb.isEmpty()) return step;
248+
current = nb.get(rng.nextInt(nb.size()));
249+
visited.add(current);
250+
if (visited.size() >= target) return step;
251+
}
252+
return maxSteps;
253+
}
254+
255+
private <V, E> long simulateReturnWalk(Graph<V, E> graph, V node, int maxSteps) {
256+
List<V> nb = new ArrayList<>(graph.getNeighbors(node));
257+
if (nb.isEmpty()) return maxSteps;
258+
V current = nb.get(rng.nextInt(nb.size()));
259+
for (int step = 2; step <= maxSteps; step++) {
260+
if (current.equals(node)) return step;
261+
nb = new ArrayList<>(graph.getNeighbors(current));
262+
if (nb.isEmpty()) return maxSteps;
263+
current = nb.get(rng.nextInt(nb.size()));
264+
}
265+
return maxSteps;
266+
}
267+
268+
private <V, E> double[][] buildTransitionMatrix(Graph<V, E> graph, List<V> nodeList, Map<V, Integer> idx) {
269+
int n = nodeList.size();
270+
double[][] P = new double[n][n];
271+
for (int i = 0; i < n; i++) {
272+
V v = nodeList.get(i);
273+
Collection<V> neighbors = graph.getNeighbors(v);
274+
int deg = neighbors.size();
275+
if (deg == 0) { P[i][i] = 1.0; }
276+
else { for (V nb : neighbors) P[i][idx.get(nb)] += 1.0 / deg; }
277+
}
278+
return P;
279+
}
280+
281+
private <V, E> void validateGraph(Graph<V, E> graph) {
282+
if (graph == null) throw new IllegalArgumentException("graph must not be null");
283+
}
284+
285+
private <V, E> void validateNode(Graph<V, E> graph, V node, String name) {
286+
if (node == null) throw new IllegalArgumentException(name + " must not be null");
287+
if (!graph.containsVertex(node)) throw new IllegalArgumentException(name + " not found in graph: " + node);
288+
}
289+
}

0 commit comments

Comments
 (0)