package i5.las2peer.services.ocd.algorithms;
import i5.las2peer.services.ocd.graphs.Cover;
import i5.las2peer.services.ocd.graphs.CoverCreationType;
import i5.las2peer.services.ocd.graphs.CustomGraph;
import i5.las2peer.services.ocd.graphs.GraphType;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import org.la4j.matrix.Matrix;
import org.la4j.matrix.sparse.CCSMatrix;
import org.la4j.vector.Vector;
import org.la4j.vector.Vectors;
import org.la4j.vector.dense.BasicVector;
import org.la4j.vector.functor.VectorAccumulator;
import org.la4j.vector.sparse.CompressedVector;
import y.base.Edge;
import y.base.EdgeCursor;
import y.base.Node;
import y.base.NodeCursor;
public class SskAlgorithm implements OcdAlgorithm {
/**
* The iteration bound for the leadership calculation phase.
* The default value is 1000. Must be greater than 0.
*/
private int leadershipIterationBound = 1000;
/**
* The precision factor for the leadership calculation phase.
* The phase ends when the infinity norm of the difference between the updated vector
* and the previous one is smaller than this factor.
* The default value is 0.001. Must be greater than 0 and smaller than infinity.
* Recommended are values close to 0.
*/
private double leadershipPrecisionFactor = 0.001;
/**
* The iteration bound for the membership assignation phase.
* The default value is 1000. Must be greater than 0.
*/
private int membershipsIterationBound = 1000;
/**
* The precision factor for the membership assignation phase.
* The phase ends when the infinity norm of the difference between the updated membership
* matrix and the previous one is smaller than this factor.
* The default value is 0.001. Must be greater than 0 and smaller than infinity.
* Recommended are values close to 0.
*/
private double membershipsPrecisionFactor = 0.001;
/*
* PARAMETER NAMES
*/
protected static final String LEADERSHIP_PRECISION_FACTOR_NAME = "leadershipPrecisionFactor";
protected static final String LEADERSHIP_ITERATION_BOUND_NAME = "leadershipIterationBound";
protected static final String MEMBERSHIPS_PRECISION_FACTOR_NAME = "membershipsPrecisionFactor";
protected static final String MEMBERSHIPS_ITERATION_BOUND_NAME = "membershipsIterationBound";
/**
* Creates a standard instance of the algorithm.
* All attributes are assigned their default values.
*/
public SskAlgorithm() {
}
@Override
public CoverCreationType getAlgorithmType() {
return CoverCreationType.SSK_ALGORITHM;
}
@Override
public Map<String, String> getParameters() {
Map<String, String> parameters = new HashMap<String, String>();
parameters.put(LEADERSHIP_ITERATION_BOUND_NAME, Integer.toString(leadershipIterationBound));
parameters.put(LEADERSHIP_PRECISION_FACTOR_NAME, Double.toString(leadershipPrecisionFactor));
parameters.put(MEMBERSHIPS_ITERATION_BOUND_NAME, Integer.toString(membershipsIterationBound));
parameters.put(MEMBERSHIPS_PRECISION_FACTOR_NAME, Double.toString(membershipsPrecisionFactor));
return parameters;
}
@Override
public void setParameters(Map<String, String> parameters) throws IllegalArgumentException {
if(parameters.containsKey(LEADERSHIP_ITERATION_BOUND_NAME)) {
leadershipIterationBound = Integer.parseInt(parameters.get(LEADERSHIP_ITERATION_BOUND_NAME));
if(leadershipIterationBound <= 0) {
throw new IllegalArgumentException();
}
parameters.remove(LEADERSHIP_ITERATION_BOUND_NAME);
}
if(parameters.containsKey(LEADERSHIP_PRECISION_FACTOR_NAME)) {
leadershipPrecisionFactor = Double.parseDouble(parameters.get(LEADERSHIP_PRECISION_FACTOR_NAME));
if(leadershipPrecisionFactor <= 0 || leadershipPrecisionFactor == Double.POSITIVE_INFINITY) {
throw new IllegalArgumentException();
}
parameters.remove(LEADERSHIP_PRECISION_FACTOR_NAME);
}
if(parameters.containsKey(MEMBERSHIPS_ITERATION_BOUND_NAME)) {
membershipsIterationBound = Integer.parseInt(parameters.get(MEMBERSHIPS_ITERATION_BOUND_NAME));
if(membershipsIterationBound <= 0) {
throw new IllegalArgumentException();
}
parameters.remove(MEMBERSHIPS_ITERATION_BOUND_NAME);
}
if(parameters.containsKey(MEMBERSHIPS_PRECISION_FACTOR_NAME)) {
membershipsPrecisionFactor = Double.parseDouble(parameters.get(MEMBERSHIPS_PRECISION_FACTOR_NAME));
if(membershipsPrecisionFactor <= 0 || membershipsPrecisionFactor == Double.POSITIVE_INFINITY) {
throw new IllegalArgumentException();
}
parameters.remove(MEMBERSHIPS_PRECISION_FACTOR_NAME);
}
if(parameters.size() > 0) {
throw new IllegalArgumentException();
}
}
@Override
public Set<GraphType> compatibleGraphTypes() {
Set<GraphType> types = new HashSet<GraphType>();
types.add(GraphType.DIRECTED);
types.add(GraphType.WEIGHTED);
return types;
}
@Override
public Cover detectOverlappingCommunities(CustomGraph graph) throws InterruptedException {
Matrix transitionMatrix = calculateTransitionMatrix(graph);
Vector totalInfluences = executeRandomWalk(transitionMatrix);
Map<Node, Integer> leaders = determineGlobalLeaders(graph, transitionMatrix, totalInfluences);
Matrix memberships = calculateMemberships(graph, leaders);
return new Cover(graph, memberships);
}
/*
* Determines the membership matrix through a random walk process.
* @param graph The graph being analyzed.
* @param leaders A mapping from the community leader nodes to the indices of their communities.
* @return The membership matrix.
*/
protected Matrix calculateMemberships(CustomGraph graph, Map<Node, Integer> leaders) throws InterruptedException {
Matrix coefficients = initMembershipCoefficientMatrix(graph, leaders);
Matrix memberships;
Matrix updatedMemberships = initMembershipMatrix(graph, leaders);
Vector membershipContributionVector;
Vector updatedMembershipVector;
NodeCursor nodes = graph.nodes();
Node node;
NodeCursor successors;
Node successor;
double coefficient;
int iteration = 0;
do {
memberships = updatedMemberships;
updatedMemberships = new CCSMatrix(memberships.rows(), memberships.columns());
while(nodes.ok()) {
if(Thread.interrupted()) {
throw new InterruptedException();
}
node = nodes.node();
if(!leaders.keySet().contains(node)) {
successors = node.successors();
updatedMembershipVector = new CompressedVector(memberships.columns());
while(successors.ok()) {
successor = successors.node();
coefficient = coefficients.get(successor.index(), node.index());
membershipContributionVector = memberships.getRow(successor.index()).multiply(coefficient);
updatedMembershipVector = updatedMembershipVector.add(membershipContributionVector);
successors.next();
}
updatedMemberships.setRow(node.index(), updatedMembershipVector);
}
else {
updatedMemberships.set(node.index(), leaders.get(node), 1);
}
nodes.next();
}
nodes.toFirst();
iteration++;
} while (getMaxDifference(updatedMemberships, memberships) > membershipsPrecisionFactor
&& iteration < membershipsIterationBound);
return memberships;
}
/*
* Returns the maximum difference between two matrices.
* It is calculated entry-wise as the greatest absolute value
* of any entry in the difference among the two matrices.
* @param matA The first matrix.
* @param matB The second matrix.
* @return The maximum difference.
*/
protected double getMaxDifference(Matrix matA, Matrix matB) throws InterruptedException {
Matrix diffMatrix = matA.subtract(matB);
double maxDifference = 0;
double curDifference;
VectorAccumulator accumulator = Vectors.mkInfinityNormAccumulator();
for(int i=0; i<diffMatrix.columns(); i++) {
if(Thread.interrupted()) {
throw new InterruptedException();
}
curDifference = diffMatrix.getColumn(i).fold(accumulator);
if(curDifference > maxDifference) {
maxDifference = curDifference;
}
}
return maxDifference;
}
/*
* Initializes the membership matrix for the memberships assignation phase.
* Leader nodes are set to belong entirely to their own community. All other nodes
* have equal memberships for all communities.
* @param graph The graph being analyzed.
* @param leaders A mapping from the leader nodes to their community indices.
* @return The initial membership matrix.
*/
protected Matrix initMembershipMatrix(CustomGraph graph, Map<Node, Integer> leaders) throws InterruptedException {
int communityCount = Collections.max(leaders.values()) + 1;
Matrix memberships = new CCSMatrix(graph.nodeCount(), communityCount);
NodeCursor nodes = graph.nodes();
Node node;
while(nodes.ok()) {
if(Thread.interrupted()) {
throw new InterruptedException();
}
node = nodes.node();
if(leaders.keySet().contains(node)) {
memberships.set(node.index(), leaders.get(node), 1);
}
else {
for(int i=0; i<memberships.columns(); i++) {
memberships.set(node.index(), i, 1d / (double)communityCount);
}
}
nodes.next();
}
return memberships;
}
/*
* Initializes the membership coefficient matrix C for the memberships assignation phase.
* The coefficient of the membership vector of node i for the calculation of the updated
* memberships of node j is stored in entry C_ij, where i and j are the node indices.
* @param graph The graph being analyzed.
* @param leaders A mapping from the leader nodes to their community indices.
* @return The membership coefficient matrix.
*/
protected Matrix initMembershipCoefficientMatrix(CustomGraph graph, Map<Node, Integer> leaders) throws InterruptedException {
Matrix coefficients = new CCSMatrix(graph.nodeCount(), graph.nodeCount());
EdgeCursor edges = graph.edges();
Edge edge;
while(edges.ok()) {
if(Thread.interrupted()) {
throw new InterruptedException();
}
edge = edges.edge();
coefficients.set(edge.target().index(), edge.source().index(), graph.getEdgeWeight(edge));
edges.next();
}
Vector column;
double norm;
for(int i=0; i<coefficients.columns(); i++) {
if(Thread.interrupted()) {
throw new InterruptedException();
}
column = coefficients.getColumn(i);
norm = column.fold(Vectors.mkManhattanNormAccumulator());
if(norm > 0) {
coefficients.setColumn(i, column.divide(norm));
}
}
return coefficients;
}
/*
* Returns the global leaders of the graph.
* @param graph The graph being analyzed.
* @param transitionMatrix The transition matrix used for the random walk.
* @param totalInfluences The total influences determined via the random walk
* @return The global leaders of the graph. The community index of each leader node is
* derivable from the mapping. Note that several leaders may belong to the same
* community.
*/
protected Map<Node, Integer> determineGlobalLeaders(CustomGraph graph, Matrix transitionMatrix, Vector totalInfluences) throws InterruptedException{
NodeCursor nodes = graph.nodes();
Node node;
NodeCursor successors;
Node successor;
double relativeInfluence;
double maxRelativeInfluence;
List<Node> maxRelativeInfluenceNeighbors;
Map<Node, Integer> communityLeaders = new HashMap<Node, Integer>();
List<Node> currentCommunityLeaders;
double nodeInfluenceOnNeighbor;
double neighborInfluenceOnNode;
int communityCount = 0;
while(nodes.ok()) {
if(Thread.interrupted()) {
throw new InterruptedException();
}
node = nodes.node();
successors = node.successors();
maxRelativeInfluence = Double.NEGATIVE_INFINITY;
maxRelativeInfluenceNeighbors = new ArrayList<Node>();
while(successors.ok()) {
successor = successors.node();
relativeInfluence = transitionMatrix.get(successor.index(), node.index());
if(relativeInfluence >= maxRelativeInfluence) {
if(relativeInfluence > maxRelativeInfluence) {
maxRelativeInfluenceNeighbors.clear();
maxRelativeInfluence = relativeInfluence;
}
maxRelativeInfluenceNeighbors.add(successor);
}
successors.next();
}
currentCommunityLeaders = new ArrayList<Node>();
currentCommunityLeaders.add(node);
for(int i=0; i<maxRelativeInfluenceNeighbors.size(); i++) {
Node maxRelativeInfluenceNeighbor = maxRelativeInfluenceNeighbors.get(i);
nodeInfluenceOnNeighbor = totalInfluences.get(node.index())
* transitionMatrix.get(node.index(), maxRelativeInfluenceNeighbor.index());
neighborInfluenceOnNode = totalInfluences.get(maxRelativeInfluenceNeighbor.index())
* maxRelativeInfluence;
if(neighborInfluenceOnNode > nodeInfluenceOnNeighbor) {
/*
* Not a leader
*/
currentCommunityLeaders.clear();
break;
}
else if (neighborInfluenceOnNode == nodeInfluenceOnNeighbor) {
/*
* There are potentially several community leaders.
*/
if(maxRelativeInfluenceNeighbor.index() < node.index()) {
/*
* Will detected community leaders only once in the iteration over the
* node with the lowest index.
*/
currentCommunityLeaders.clear();
break;
}
else {
/*
* Node has the lowest index of the potential leaders for the current
* community. The additional potential leader is added.
*/
currentCommunityLeaders.add(maxRelativeInfluenceNeighbor);
}
}
}
for(int i=0; i<currentCommunityLeaders.size(); i++) {
communityLeaders.put(currentCommunityLeaders.get(i), communityCount);
}
if(currentCommunityLeaders.size() > 0) {
communityCount++;
}
nodes.next();
}
return communityLeaders;
}
/*
* Executes a random walk on the transition matrix and returns the total node influences.
* @param transitionMatrix The transition matrix.
* @return A vector containing the total influence of each node under the corresponding node index.
*/
protected Vector executeRandomWalk(Matrix transitionMatrix) throws InterruptedException {
Vector vec1 = new BasicVector(transitionMatrix.columns());
for(int i=0; i<vec1.length(); i++) {
vec1.set(i, 1.0 / vec1.length());
}
Vector vec2 = new BasicVector(vec1.length());
for(int i=0; vec1.subtract(vec2).fold(Vectors.mkInfinityNormAccumulator()) > leadershipPrecisionFactor
&& i < leadershipIterationBound; i++) {
if(Thread.interrupted()) {
throw new InterruptedException();
}
vec2 = new BasicVector(vec1);
vec1 = transitionMatrix.multiply(vec1);
}
return vec1;
}
/*
* Calculates the transition matrix for the random walk phase.
* @param graph The graph being analyzed.
* @return The normalized transition matrix.
*/
protected Matrix calculateTransitionMatrix(CustomGraph graph) throws InterruptedException {
Matrix transitionMatrix = new CCSMatrix(graph.nodeCount(), graph.nodeCount());
NodeCursor nodes = graph.nodes();
Node node;
NodeCursor predecessors;
Node predecessor;
while(nodes.ok()) {
if(Thread.interrupted()) {
throw new InterruptedException();
}
node = nodes.node();
predecessors = node.predecessors();
while(predecessors.ok()) {
predecessor = predecessors.node();
transitionMatrix.set(node.index(), predecessor.index(), calculateTransitiveLinkWeight(graph, node, predecessor));
predecessors.next();
}
nodes.next();
}
Vector column;
double norm;
for(int i=0; i<transitionMatrix.columns(); i++) {
if(Thread.interrupted()) {
throw new InterruptedException();
}
column = transitionMatrix.getColumn(i);
norm = column.fold(Vectors.mkManhattanNormAccumulator());
if(norm > 0) {
transitionMatrix.setColumn(i, column.divide(norm));
}
}
return transitionMatrix;
}
/*
* Calculates the transitive link weight from source to target.
* Note that target must be a successor of source.
* @param graph The graph being analyzed.
* @param target The link target.
* @param source The link source.
* @return The transitive link weight from source to target.
*/
protected double calculateTransitiveLinkWeight(CustomGraph graph, Node target, Node source) {
NodeCursor successors = source.successors();
Node successor;
double transitiveLinkWeight = 0;
double linkWeight;
Edge targetEdge;
while(successors.ok()) {
successor = successors.node();
if(successor != target) {
targetEdge = successor.getEdgeTo(target);
if(targetEdge != null) {
/*
* Contribution to the transitive link weight is chosen as the minimum weight
* of the two triangle edges.
*/
linkWeight = graph.getEdgeWeight(source.getEdgeTo(successor));
linkWeight = Math.min(linkWeight, graph.getEdgeWeight(targetEdge));
transitiveLinkWeight += linkWeight;
}
}
else {
transitiveLinkWeight += graph.getEdgeWeight(source.getEdgeTo(target));
}
successors.next();
}
return transitiveLinkWeight;
}
}