Skip to content
This repository has been archived by the owner on May 4, 2024. It is now read-only.

Commit

Permalink
Louvain: reduce communities to a single node
Browse files Browse the repository at this point in the history
  • Loading branch information
floriandejonckheere committed Apr 26, 2024
1 parent 4e99d26 commit 6fa7114
Show file tree
Hide file tree
Showing 2 changed files with 136 additions and 17 deletions.
129 changes: 112 additions & 17 deletions lib/mosaik/algorithms/louvain.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,60 +10,97 @@ class Louvain < Algorithm
EPSILON = 1e-6

def call
# Assign initial set of communities (each vertex in its own community)
graph.vertices.each_value.with_index do |vertex, i|
graph
.add_cluster("C#{i}")
.add_vertex(vertex)
end

# Calculate initial modularity
modularity = modularity_for(graph)
modularity = modularity(graph)

info "Initial modularity: #{modularity}"

# Use a separate variable to store the (reduced) graph through the iterations
reduced_graph = graph

# Final mapping of vertices to communities
mapping = graph
.vertices
.keys
.index_with { |vertex_id| vertex_id }

# Iterate until no further improvement in modularity
1.step do |i|
debug "Iteration #{i}: initial modularity = #{modularity}"
# Assign initial set of communities (each vertex in its own community)
reduced_graph.vertices.each_value do |vertex|
reduced_graph
.add_cluster(vertex.id)
.add_vertex(vertex)
end

# Calculate initial modularity
initial_modularity = modularity(reduced_graph)

debug "Iteration #{i}: modularity=#{initial_modularity}, vertices=#{reduced_graph.vertices.count}, communities=#{reduced_graph.clusters.count}"

# Phase 1: reassign vertices to optimize modularity
graph.vertices.each_value do |vertex|
reassign_vertex(vertex)
reduced_graph.vertices.each_value do |vertex|
reassign_vertex(reduced_graph, vertex)
end

# Phase 2: reduce communities to a single node
# TODO: Implement this phase
g, reduced_mapping = reduce_graph(reduced_graph)

debug "Reduced #{reduced_graph.vertices.count} vertices to #{g.vertices.count} vertices"
debug "Mapping: #{reduced_mapping.inspect}"
debug "Changes: #{reduced_mapping.select { |a, b| a != b }.inspect}"

if options[:visualize]
MOSAIK::Graph::Visualizer
.new(options, g)
.to_svg("louvain_#{i}")
end

# Merge the reduced mapping with the original mapping
mapping = mapping.transform_values { |v| reduced_mapping[v] }

# Calculate final modularity
final_modularity = modularity_for(graph)
final_modularity = modularity(graph)

# Stop iterating if no further improvement in modularity
break if final_modularity - modularity <= EPSILON

# Update modularity
modularity = final_modularity

# Update the reduced graph
reduced_graph = g
end

info "Final modularity: #{modularity}"

# Copy the final communities to the original graph
graph.clusters.clear
mapping.each do |vertex_id, community_id|
graph
.find_or_add_cluster(community_id)
.add_vertex(graph.find_vertex(vertex_id))
end
end

private

def reassign_vertex(vertex)
def reassign_vertex(graph, vertex)
# Initialize best community as current community
best_community = graph.clusters.values.find { |cluster| cluster.vertices.include? vertex }

# Initialize best modularity gain
best_gain = 0.0

# Initialize best modularity
best_modularity = modularity_for(graph)
best_modularity = modularity(graph)

# Store the original community of the vertex
community = graph.clusters.values.find { |cluster| cluster.vertices.include? vertex }

# Iterate over all neighbours of the vertex
vertex.edges.each_key do |neighbour_id|
# Find the community of the neighbour
neighbour = graph.find_vertex(neighbour_id)
neighbour_community = graph.clusters.values.find { |cluster| cluster.vertices.include? neighbour }

Expand All @@ -75,7 +112,7 @@ def reassign_vertex(vertex)
neighbour_community.add_vertex(vertex)

# Calculate the new modularity
new_modularity = modularity_for(graph)
new_modularity = modularity(graph)

# Calculate the modularity gain
gain = new_modularity - best_modularity
Expand All @@ -99,7 +136,65 @@ def reassign_vertex(vertex)
best_modularity
end

def modularity_for(graph)
def reduce_graph(graph)
raise NotImplementedError, "Directed graphs are not supported" if graph.directed

# Create a new graph
reduced_graph = Graph::Graph.new(directed: graph.directed)

# Mapping of vertices to communities
reduced_mapping = graph
.clusters
.each_with_object({}) { |(community_id, cluster), mapping| cluster.vertices.each { |vertex| mapping[vertex.id] = community_id } }

# Iterate over all communities
graph.clusters.each_value do |cluster|
# Create a new vertex for the community
reduced_graph.add_vertex(cluster.id)
end

# Iterate over all combinations of vertices
weights = graph.vertices.keys.combination(2).filter_map do |v1, v2|
# Find all edges between the two vertices
edges = Set.new(graph.find_edges(v1, v2) + graph.find_edges(v2, v1))

# Skip if there are no edges
next if edges.empty?

# Find the communities of the vertices
c1 = reduced_mapping[v1]
c2 = reduced_mapping[v2]

# Skip if the communities are the same
next if c1 == c2

# Calculate the weight for the aggregate edge
weight = edges.sum { |e| e.attributes.fetch(:weight, 0.0) }

[[c1, c2], weight]
end

# Transform weights into a hash
weights = weights
.group_by(&:first)
.transform_values { |es| es.sum(&:last) }

# Add new edges to the reduced graph
reduced_graph.vertices.keys.combination(2).each do |v1, v2|
weight = weights.fetch([v1, v2], 0.0) + weights.fetch([v2, v1], 0.0)

# Skip if the weight is zero
next if weight.zero?

reduced_graph
.add_edge(v1, v2, weight:)
end

# Return the reduced graph and mapping
[reduced_graph, reduced_mapping]
end

def modularity(graph)
Metrics::Modularity
.new(options, graph)
.evaluate
Expand Down
24 changes: 24 additions & 0 deletions spec/mosaik/algorithms/louvain_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# frozen_string_literal: true
# typed: true

RSpec.describe MOSAIK::Algorithms::Louvain do
subject(:algorithm) { described_class.new(options, graph) }

let(:options) { {} }

include_context "with a simple undirected graph"

describe "#reduce_graph" do
it "returns a reduced graph" do
reduced_graph, reduced_mapping = algorithm.send(:reduce_graph, graph)

expect(reduced_graph.vertices.keys).to eq ["A", "B", "C"]

expect(reduced_graph.find_vertex("A").edges.transform_values { |es| es.map(&:attributes) }).to eq "B" => [{ weight: 3.0 }], "C" => [{ weight: 2.0 }]
expect(reduced_graph.find_vertex("B").edges.transform_values { |es| es.map(&:attributes) }).to eq "A" => [{ weight: 3.0 }]
expect(reduced_graph.find_vertex("C").edges.transform_values { |es| es.map(&:attributes) }).to eq "A" => [{ weight: 2.0 }]

expect(reduced_mapping).to eq "A" => "A", "B" => "A", "C" => "B", "D" => "A", "E" => "B", "F" => "C"
end
end
end

0 comments on commit 6fa7114

Please sign in to comment.