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

Commit 7379a0d

Browse files
perf: add lightweight Monte Carlo simulation paths in InfluenceSpreadSimulator
The monteCarlo() method runs thousands of simulation trials, but each trial previously created full SimulationResult objects with per-round RoundSnapshot deep-copies (LinkedHashMap clone of entire state per round) and InfectionEvent timeline lists. None of this data is read by monteCarlo — it only needs getTotalInfected(), getRoundCount(), and getFinalState(). Add LightweightResult class and three lightweight simulation methods (simulateICLightweight, simulateLTLightweight, simulateSIRLightweight) that skip snapshot/timeline creation entirely. For a 1000-node graph with 10 rounds and 10,000 trials, this eliminates ~100K unnecessary Map deep-copies and ~100K ArrayList allocations. The public simulation APIs (simulateIC, simulateLT, simulateSIR) are unchanged — only the internal Monte Carlo hot path uses the fast path.
1 parent da457ef commit 7379a0d

1 file changed

Lines changed: 128 additions & 7 deletions

File tree

Gvisual/src/gvisual/InfluenceSpreadSimulator.java

Lines changed: 128 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -389,22 +389,25 @@ public MonteCarloResult monteCarlo(Collection<String> seeds,
389389
Map<String, Integer> infectionFrequency = new LinkedHashMap<>();
390390

391391
for (int i = 0; i < numTrials; i++) {
392-
SimulationResult result;
392+
// Use lightweight simulation (no snapshots/timeline) to avoid
393+
// O(rounds × V) deep-copy overhead per trial. Monte Carlo only
394+
// needs final state + round count, not per-round snapshots.
395+
LightweightResult lr;
393396
switch (model) {
394397
case INDEPENDENT_CASCADE:
395-
result = simulateIC(seeds, probability, maxRounds); break;
398+
lr = simulateICLightweight(seeds, probability, maxRounds); break;
396399
case LINEAR_THRESHOLD:
397-
result = simulateLT(seeds, maxRounds); break;
400+
lr = simulateLTLightweight(seeds, maxRounds); break;
398401
case SIR:
399-
result = simulateSIR(seeds, probability, recoveryRate, maxRounds); break;
402+
lr = simulateSIRLightweight(seeds, probability, recoveryRate, maxRounds); break;
400403
default:
401404
throw new IllegalArgumentException("Unknown model: " + model);
402405
}
403406

404-
spreads.add(result.getTotalInfected());
405-
durations.add(result.getRoundCount());
407+
spreads.add(lr.totalInfected);
408+
durations.add(lr.rounds);
406409

407-
for (Map.Entry<String, NodeState> entry : result.getFinalState().entrySet()) {
410+
for (Map.Entry<String, NodeState> entry : lr.finalState.entrySet()) {
408411
if (entry.getValue() == NodeState.INFECTED ||
409412
entry.getValue() == NodeState.RECOVERED) {
410413
infectionFrequency.merge(entry.getKey(), 1, Integer::sum);
@@ -509,6 +512,124 @@ public VaccinationStrategy findVaccinationTargets(int k) {
509512
return new VaccinationStrategy(targets, totalEdgesBlocked, coverageRatio);
510513
}
511514

515+
// ─── Lightweight simulation (Monte Carlo fast path) ───────────
516+
517+
/**
518+
* Minimal result for Monte Carlo: final state + round count only.
519+
* Avoids the per-round LinkedHashMap deep-copies and InfectionEvent
520+
* timeline that {@link SimulationResult} carries.
521+
*/
522+
private static class LightweightResult {
523+
final Map<String, NodeState> finalState;
524+
final int rounds;
525+
final int totalInfected;
526+
527+
LightweightResult(Map<String, NodeState> finalState, int rounds) {
528+
this.finalState = finalState;
529+
this.rounds = rounds;
530+
int cnt = 0;
531+
for (NodeState s : finalState.values())
532+
if (s == NodeState.INFECTED || s == NodeState.RECOVERED) cnt++;
533+
this.totalInfected = cnt;
534+
}
535+
}
536+
537+
private LightweightResult simulateICLightweight(Collection<String> seeds,
538+
double probability, int maxRounds) {
539+
Map<String, NodeState> state = initState(seeds);
540+
Set<String> newlyInfected = new LinkedHashSet<>();
541+
for (String seed : seeds)
542+
if (graph.containsVertex(seed)) newlyInfected.add(seed);
543+
int round = 0;
544+
545+
while (!newlyInfected.isEmpty()) {
546+
round++;
547+
if (maxRounds > 0 && round > maxRounds) break;
548+
Set<String> nextInfected = new LinkedHashSet<>();
549+
for (String node : newlyInfected) {
550+
for (String neighbor : getNeighbors(node)) {
551+
if (state.get(neighbor) == NodeState.SUSCEPTIBLE) {
552+
double edgeProb = getEdgeProbability(node, neighbor, probability);
553+
if (random.nextDouble() < edgeProb) {
554+
state.put(neighbor, NodeState.INFECTED);
555+
nextInfected.add(neighbor);
556+
}
557+
}
558+
}
559+
}
560+
for (String node : newlyInfected) state.put(node, NodeState.RECOVERED);
561+
newlyInfected = nextInfected;
562+
}
563+
return new LightweightResult(state, round);
564+
}
565+
566+
private LightweightResult simulateLTLightweight(Collection<String> seeds, int maxRounds) {
567+
Map<String, NodeState> state = initState(seeds);
568+
Map<String, Double> thresholds = new HashMap<>();
569+
for (String node : graph.getVertices()) thresholds.put(node, random.nextDouble());
570+
571+
int round = 0;
572+
boolean changed = true;
573+
while (changed) {
574+
round++;
575+
if (maxRounds > 0 && round > maxRounds) break;
576+
changed = false;
577+
List<String> toActivate = new ArrayList<>();
578+
for (String node : graph.getVertices()) {
579+
if (state.get(node) != NodeState.SUSCEPTIBLE) continue;
580+
Collection<String> influencers = getPredecessors(node);
581+
if (influencers.isEmpty()) continue;
582+
int active = 0;
583+
for (String nb : influencers)
584+
if (state.get(nb) == NodeState.INFECTED || state.get(nb) == NodeState.RECOVERED)
585+
active++;
586+
if ((double) active / influencers.size() >= thresholds.get(node))
587+
toActivate.add(node);
588+
}
589+
for (String node : toActivate) {
590+
state.put(node, NodeState.INFECTED);
591+
changed = true;
592+
}
593+
}
594+
return new LightweightResult(state, round);
595+
}
596+
597+
private LightweightResult simulateSIRLightweight(Collection<String> seeds,
598+
double infectionRate,
599+
double recoveryRate, int maxRounds) {
600+
Map<String, NodeState> state = initState(seeds);
601+
Set<String> currentlyInfected = new LinkedHashSet<>();
602+
for (String seed : seeds)
603+
if (graph.containsVertex(seed)) currentlyInfected.add(seed);
604+
int round = 0;
605+
606+
while (!currentlyInfected.isEmpty()) {
607+
round++;
608+
if (maxRounds > 0 && round > maxRounds) break;
609+
Set<String> toInfect = new LinkedHashSet<>();
610+
Set<String> toRecover = new LinkedHashSet<>();
611+
for (String node : currentlyInfected) {
612+
for (String neighbor : getNeighbors(node)) {
613+
if (state.get(neighbor) == NodeState.SUSCEPTIBLE && !toInfect.contains(neighbor)) {
614+
double edgeProb = getEdgeProbability(node, neighbor, infectionRate);
615+
if (random.nextDouble() < edgeProb) toInfect.add(neighbor);
616+
}
617+
}
618+
}
619+
for (String node : currentlyInfected)
620+
if (random.nextDouble() < recoveryRate) toRecover.add(node);
621+
for (String node : toInfect) {
622+
state.put(node, NodeState.INFECTED);
623+
currentlyInfected.add(node);
624+
}
625+
for (String node : toRecover) {
626+
state.put(node, NodeState.RECOVERED);
627+
currentlyInfected.remove(node);
628+
}
629+
}
630+
return new LightweightResult(state, round);
631+
}
632+
512633
// ─── Helpers ────────────────────────────────────────────────
513634

514635
private void validateSeeds(Collection<String> seeds) {

0 commit comments

Comments
 (0)