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 27, 2024
1 parent ff84d46 commit 4308911
Show file tree
Hide file tree
Showing 2 changed files with 144 additions and 10 deletions.
122 changes: 112 additions & 10 deletions lib/mosaik/algorithms/louvain.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,46 +10,89 @@ 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
debug "Calculating initial modularity"

# Calculate initial modularity
modularity = modularity_for(graph)

info "Initial modularity: #{modularity}"

# Store the original graph
original_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)
graph.vertices.each_value do |vertex|
graph
.add_cluster(vertex.id)
.add_vertex(vertex)
end

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

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

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

yield g, reduced_mapping if block_given?
debug "Reduced #{graph.vertices.size} vertices to #{g.vertices.size} vertices"
debug "Mapping: #{reduced_mapping.inspect}"
debug "Changes: #{reduced_mapping.reject { |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)

info "Iteration #{i}: end modularity=#{final_modularity}, vertices=#{graph.vertices.count}, communities=#{graph.clusters.count}"

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

# Update modularity
modularity = final_modularity

# Update the reduced graph
@graph = g
end

info "Final modularity: #{modularity}"

# Copy the final communities to the original graph
original_graph.clusters.clear

mapping.each do |vertex_id, community_id|
original_graph
.find_or_add_cluster(community_id)
.add_vertex(original_graph.find_vertex(vertex_id))
rescue => e
binding.b

raise
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 }

Expand All @@ -64,6 +107,7 @@ def reassign_vertex(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 Down Expand Up @@ -99,6 +143,64 @@ def reassign_vertex(vertex)
best_modularity
end

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 (non-empty) community
reduced_graph.add_vertex(cluster.id) if cluster.vertices.any?
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].sort, 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_for(graph)
Metrics::Modularity
.new(options, graph)
Expand Down
32 changes: 32 additions & 0 deletions spec/mosaik/algorithms/louvain_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# 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 "#call" do
it "optimizes modularity" do
expect { algorithm.call }.to change { graph.clusters.count }.from(0).to(3)

expect(graph.clusters.map { |_, cluster| cluster.vertices.map(&:id) }).to match_array [["A", "B"], ["C"], ["D", "E", "F"]]
end
end

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 4308911

Please sign in to comment.