All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.neo4j.gds.louvain.Louvain Maven / Gradle / Ivy

/*
 * Copyright (c) "Neo4j"
 * Neo4j Sweden AB [http://neo4j.com]
 *
 * This file is part of Neo4j.
 *
 * Neo4j is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see .
 */
package org.neo4j.gds.louvain;

import org.neo4j.gds.Algorithm;
import org.neo4j.gds.RelationshipType;
import org.neo4j.gds.algorithms.community.CommunityCompanion;
import org.neo4j.gds.api.Graph;
import org.neo4j.gds.api.IdMap;
import org.neo4j.gds.api.properties.nodes.NodePropertyValues;
import org.neo4j.gds.api.properties.relationships.RelationshipIterator;
import org.neo4j.gds.core.Aggregation;
import org.neo4j.gds.core.concurrency.Concurrency;
import org.neo4j.gds.core.concurrency.DefaultPool;
import org.neo4j.gds.core.concurrency.ParallelUtil;
import org.neo4j.gds.core.loading.construction.GraphFactory;
import org.neo4j.gds.core.loading.construction.RelationshipsBuilder;
import org.neo4j.gds.core.utils.OriginalIdNodePropertyValues;
import org.neo4j.gds.core.utils.partition.Partition;
import org.neo4j.gds.core.utils.partition.PartitionUtils;
import org.neo4j.gds.core.utils.progress.tasks.ProgressTracker;
import org.neo4j.gds.modularityoptimization.ModularityOptimization;
import org.neo4j.gds.modularityoptimization.ModularityOptimizationResult;
import org.neo4j.gds.termination.TerminationFlag;

import java.util.Optional;
import java.util.OptionalLong;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.atomic.AtomicLong;

import static org.neo4j.gds.core.concurrency.ParallelUtil.DEFAULT_BATCH_SIZE;

public final class Louvain extends Algorithm {

    private final Graph rootGraph;
    private final NodePropertyValues seedingValues;
    private final ExecutorService executorService;
    // results
    private final LouvainDendrogramManager dendrogramManager;
    private double[] modularities;
    private int ranLevels;

    private final int maxLevels;

    private final Concurrency concurrency;

    private final int maxIterations;

    private final double tolerance;

    private final boolean trackIntermediateCommunities;

    public Louvain(
        Graph graph,
        Concurrency concurrency,
        int maxIterations,
        double tolerance,
        int maxLevels,
        boolean trackIntermediateCommunities,
        String seedProperty,
        ProgressTracker progressTracker,
        ExecutorService executorService,
        TerminationFlag terminationFlag
    ) {
        super(progressTracker);
        this.rootGraph = graph;
        this.maxIterations = maxIterations;
        this.concurrency = concurrency;
        this.seedingValues = Optional.ofNullable(seedProperty)
            .map(seedParameter -> CommunityCompanion.extractSeedingNodePropertyValues(graph, seedParameter))
            .orElse(null);
        this.executorService = executorService;
        this.dendrogramManager = new LouvainDendrogramManager(
            graph.nodeCount(),
            maxLevels,
            trackIntermediateCommunities
        );
        this.tolerance = tolerance;
        this.modularities = new double[maxLevels];
        this.maxLevels = maxLevels;
        this.trackIntermediateCommunities = trackIntermediateCommunities;

        this.terminationFlag = terminationFlag;
    }

    @Override
    public LouvainResult compute() {
        progressTracker.beginSubTask();

        Graph workingGraph = rootGraph;
        NodePropertyValues nextSeedingValues = seedingValues;

        boolean resized = false;

        long oldNodeCount = rootGraph.nodeCount();
        for (ranLevels = 0; ranLevels < maxLevels; ranLevels++) {

            terminationFlag.assertRunning();

            var modularityOptimizationResult = runModularityOptimization(
                workingGraph,
                nextSeedingValues
            );

            modularities[ranLevels] = modularityOptimizationResult.modularity();
            dendrogramManager.prepareNextLevel(ranLevels);

            long maxCommunityId = buildDendrogram(
                workingGraph,
                ranLevels,
                modularityOptimizationResult
            );

            workingGraph = summarizeGraph(workingGraph, modularityOptimizationResult, maxCommunityId);
            nextSeedingValues = new OriginalIdNodePropertyValues(workingGraph) {
                @Override
                public OptionalLong getMaxLongPropertyValue() {
                    // We want to use the maxSeedCommunity with value 0 in all subsequent iterations
                    return OptionalLong.empty();
                }
            };

            if (workingGraph.nodeCount() == oldNodeCount
                || workingGraph.nodeCount() == 1
                || hasConverged()
            ) {
                resized = true;
                resizeResultArrays();
                break;
            }
            oldNodeCount = workingGraph.nodeCount();
        }
        if (!resized && !trackIntermediateCommunities) {
            resizeResultArrays();
        }
        progressTracker.endSubTask();
        return new LouvainResult(
            dendrogramManager.getCurrent(),
            levels(),
            dendrogramManager,
            modularities,
            modularities[levels() - 1]
        );
    }

    private void resizeResultArrays() {
        int numLevels = levels();
        double[] resizedModularities = new double[numLevels];
        if (numLevels < maxLevels) {
            System.arraycopy(this.modularities, 0, resizedModularities, 0, numLevels);
            this.modularities = resizedModularities;
        }
        dendrogramManager.resizeDendrogram(numLevels);

    }

    private long buildDendrogram(
        Graph workingGraph,
        int level,
        ModularityOptimizationResult modularityOptimizationResult
    ) {
        AtomicLong maxCommunityId = new AtomicLong(0L);
        ParallelUtil.parallelForEachNode(rootGraph.nodeCount(), concurrency, terminationFlag, nodeId -> {
            long prevId = level == 0
                ? nodeId
                : workingGraph.toMappedNodeId(dendrogramManager.getPrevious(nodeId));

            long communityId = modularityOptimizationResult.communityId(prevId);

            boolean updatedMaxCurrentId;
            do {
                var currentMaxId = maxCommunityId.get();
                if (communityId > currentMaxId) {
                    updatedMaxCurrentId = maxCommunityId.compareAndSet(currentMaxId, communityId);
                } else {
                    updatedMaxCurrentId = true;
                }
            } while (!updatedMaxCurrentId);

            dendrogramManager.set(nodeId, communityId);
        });

        return maxCommunityId.get();
    }

    private ModularityOptimizationResult runModularityOptimization(Graph louvainGraph, NodePropertyValues seed) {
        ModularityOptimization modularityOptimization = new ModularityOptimization(
            louvainGraph,
            maxIterations,
            tolerance,
            seed,
            concurrency,
            DEFAULT_BATCH_SIZE,
            DefaultPool.INSTANCE,
            progressTracker,
            terminationFlag
        );

        return modularityOptimization.compute();
    }

    private Graph summarizeGraph(
        Graph workingGraph,
        ModularityOptimizationResult modularityOptimizationResult,
        long maxCommunityId
    ) {
        var nodesBuilder = GraphFactory.initNodesBuilder()
            .maxOriginalId(maxCommunityId)
            .concurrency(concurrency)
            .build();

        terminationFlag.assertRunning();

        workingGraph.forEachNode((nodeId) -> {
            nodesBuilder.addNode(modularityOptimizationResult.communityId(nodeId));
            return true;
        });

        terminationFlag.assertRunning();

        IdMap idMap = nodesBuilder.build().idMap();
        RelationshipsBuilder relationshipsBuilder = GraphFactory.initRelationshipsBuilder()
            .nodes(idMap)
            .relationshipType(RelationshipType.of("IGNORED"))
            .orientation(rootGraph.schema().direction().toOrientation())
            .addPropertyConfig(GraphFactory.PropertyConfig.builder()
                .propertyKey("property")
                .aggregation(Aggregation.SUM)
                .build())
            .executorService(executorService)
            .build();

        // using degreePartitioning did not show an improvement -- assuming as tasks are too small
        var relationshipCreators = PartitionUtils.rangePartition(
            concurrency,
            workingGraph.nodeCount(),
            partition ->
                new RelationshipCreator(
                    relationshipsBuilder,
                    modularityOptimizationResult,
                    workingGraph.concurrentCopy(),
                    partition
                ),
            Optional.empty()
        );

        ParallelUtil.run(relationshipCreators, executorService);

        return GraphFactory.create(idMap, relationshipsBuilder.build());
    }

    private boolean hasConverged() {
        if (ranLevels == 0) {
            return false;
        }

        double previousModularity = modularities[ranLevels - 1];
        double currentModularity = modularities[ranLevels];
        return !(currentModularity > previousModularity && Math.abs(currentModularity - previousModularity) > tolerance);
    }

    private int levels() {
        return this.ranLevels == 0 ? 1 : this.ranLevels;
    }

    static final class RelationshipCreator implements Runnable {

        private final RelationshipsBuilder relationshipsBuilder;

        private final ModularityOptimizationResult modularityOptimizationResult;

        private final RelationshipIterator relationshipIterator;

        private final Partition partition;

        private RelationshipCreator(
            RelationshipsBuilder relationshipsBuilder,
            ModularityOptimizationResult modularityOptimizationResult,
            RelationshipIterator relationshipIterator,
            Partition partition
        ) {
            this.relationshipsBuilder = relationshipsBuilder;
            this.modularityOptimizationResult = modularityOptimizationResult;
            this.relationshipIterator = relationshipIterator;
            this.partition = partition;
        }

        @Override
        public void run() {
            partition.consume(nodeId -> {
                long communityId = modularityOptimizationResult.communityId(nodeId);
                relationshipIterator.forEachRelationship(nodeId, 1.0, (source, target, property) -> {
                    //ignore scaling alltogether
                        relationshipsBuilder.add(
                            communityId,
                            modularityOptimizationResult.communityId(target),
                            property
                        );


                    return true;
                });
            });
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy