/* * File: Permanence.java * Authors: Jeremy D. Wendt * Company: Sandia National Laboratories * Project: Cognitive Foundry * * Copyright 2016, Sandia Corporation. * Under the terms of Contract DE-AC04-94AL85000, there is a non-exclusive * license for use of this work by or on behalf of the U.S. Government. * Export of this program may require a license from the United States * Government. See CopyrightHistory.txt for complete details. * */ package gov.sandia.cognition.graph.community; import gov.sandia.cognition.annotation.PublicationReference; import gov.sandia.cognition.annotation.PublicationType; import gov.sandia.cognition.graph.DirectedNodeEdgeGraph; import gov.sandia.cognition.graph.GraphMetrics; import gov.sandia.cognition.collection.IntArrayList; import java.util.HashSet; import java.util.Set; @PublicationReference(author = "Tanmoy Chakraborty, Sriram Srinivasan, " + "Niloy Ganguly, Animesh Mukherjee, and Sanjukta Bhowmick", title = "On the permanence of vertices in network communities", year = 2014, type = PublicationType.Conference) /** * This class computes the permanence-maximizing (approximation) algorithm as * described in the cited paper. This algorithm forms communities in an * agglomerative fashion, attempting to maximize permanence. * * @author jdwendt * @param <NodeNameType> The node name type in the graph */ public class Permanence<NodeNameType> { /** * The discovered partitioning. Before and during computation, not * guaranteed to be anything good. */ private final MutableNodePartitioning<NodeNameType> partitions; /** * The to-be partitioned graph */ private final DirectedNodeEdgeGraph<NodeNameType> graph; /** * The maximum number of passes to do the iterative algorithm */ private final int maxNumPasses; /** * The minimum performance gain to be considered over each iteration */ private final double minPermanenceGain; /** * The resulting graph permanence */ private double graphPermanence; /** * Initialize the class with the input graph and parameters * * @param graph The graph to be partitioned * @param maxNumPasses The maximum number of passes to perform * @param minPermanenceGain The minimum permanence gain to be considered for * each iteration */ public Permanence(DirectedNodeEdgeGraph<NodeNameType> graph, int maxNumPasses, double minPermanenceGain) { this.partitions = new MutableNodePartitioning<>(graph); this.graph = graph; this.maxNumPasses = maxNumPasses; this.minPermanenceGain = minPermanenceGain; this.graphPermanence = -2; } /** * Performs the actual permanence-maximization computation and returns the * resulting partitioning. * * @return The resulting partitioning */ public NodePartitioning<NodeNameType> solveCommunities() { int n = graph.getNumNodes(); int iter = 0; double sum = 0; double oldSum = -1; GraphMetrics<NodeNameType> metrics = new GraphMetrics<>(graph); Set<Integer> curNeighborCommunities = new HashSet<>(); while ((Math.abs(sum - oldSum) > minPermanenceGain) && (iter < maxNumPasses)) { ++iter; oldSum = sum; sum = 0; IntArrayList order = IntArrayList.range(n); order.randomizeOrder(); for (int ii = 0; ii < n; ++ii) { int i = order.get(ii); // Skip degree 1 nodes -- they add them to their siblings' communities at the end if (metrics.degree(i) == 1) { continue; } int curPart = partitions.getPartitionById(i); double iPerm = CommunityMetrics.computeOneNodePermanenceById(metrics, partitions, i, graph); if (iPerm == 1) { sum += iPerm; continue; } double neighborsCurPerm = 0; curNeighborCommunities.clear(); Set<Integer> neighbors = metrics.neighborIds(i); for (int neighbor : neighbors) { // Ignore degree 1 neighbors if (metrics.degree(neighbor) > 1) { neighborsCurPerm += CommunityMetrics.computeOneNodePermanenceById( metrics, partitions, neighbor, graph); int part = partitions.getPartitionById(neighbor); if (part != curPart) { curNeighborCommunities.add(part); } } } for (int neigComm : curNeighborCommunities) { partitions.moveNodeById(i, neigComm); double newPerm = CommunityMetrics.computeOneNodePermanenceById( metrics, partitions, i, graph); double neighborsNewPerm = 0; for (int neighbor : neighbors) { // Ignore degree 1 neighbors if (metrics.degree(neighbor) > 1) { neighborsNewPerm += CommunityMetrics.computeOneNodePermanenceById( metrics, partitions, neighbor, graph); } } // Their code seems to do only the net increase, even though // their paper indicates they want both to increase. I // believe it's a bug in the paper. // if ((iPerm < newPerm) && (neighborsCurPerm // < neighborsNewPerm)) if ((iPerm + neighborsCurPerm) < (newPerm + neighborsNewPerm)) { iPerm = newPerm; // TODO: Why don't they update neighborsCurPerm in the paper? // Assuming it's a bug in the paper, this next line does that neighborsCurPerm = neighborsNewPerm; curPart = neigComm; // Speed-up for if I've now discovered the best community if (iPerm == 1) { break; } } else { partitions.moveNodeById(i, curPart); } } sum += iPerm; } // Add all degree 1 nodes for (int i = 0; i < n; ++i) { if (metrics.degree(i) != 1) { continue; } // It only has one neighbor, and this gets it NodeNameType neighbor = metrics.neighbors(i).iterator().next(); int neighborCom = partitions.getPartition(neighbor); partitions.moveNodeById(i, neighborCom); } } partitions.removeEmptyPartitions(); // Recompute whole-graph permanence as it will have changed by adding the degree 1 nodes graphPermanence = CommunityMetrics.computeGraphPermanance(graph, metrics, partitions); return partitions; } /** * Returns the permanence discovered by running solveCommunities. If called * before solveCommunities, this throws an exception. * * @return the permanence discovered by running solveCommunities */ public double permanence() { if (graphPermanence < -1) { throw new RuntimeException("Permanence not yet computed"); } return graphPermanence; } }