package i5.las2peer.services.ocd.graphs; import i5.las2peer.services.ocd.metrics.OcdMetricLog; import i5.las2peer.services.ocd.metrics.OcdMetricType; import i5.las2peer.services.ocd.utils.NonZeroEntriesVectorProcedure; import java.awt.Color; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import javax.persistence.CascadeType; import javax.persistence.Column; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; import javax.persistence.IdClass; import javax.persistence.JoinColumn; import javax.persistence.JoinColumns; import javax.persistence.OneToMany; import javax.persistence.OneToOne; import org.la4j.matrix.Matrix; import org.la4j.matrix.sparse.CCSMatrix; import org.la4j.vector.Vector; import org.la4j.vector.Vectors; import y.base.Node; import y.base.NodeCursor; /** * Represents a cover, i.e. the result of an overlapping community detection algorithm holding the community structure and additional meta data. * @author Sebastian * */ @Entity @IdClass(CoverId.class) public class Cover { /////////////////// DATABASE COLUMN NAMES /* * Database column name definitions. */ public static final String graphIdColumnName = "GRAPH_ID"; public static final String graphUserColumnName = "USER_NAME"; private static final String nameColumnName = "NAME"; // private static final String descriptionColumnName = "DESCRIPTION"; public static final String idColumnName = "ID"; // private static final String lastUpdateColumnName = "LAST_UPDATE"; private static final String creationMethodColumnName = "CREATION_METHOD"; public static final String simCostsColumnName = "SIMILARITYCOSTS"; /* * Field name definitions for JPQL queries. */ public static final String GRAPH_FIELD_NAME = "graph"; public static final String CREATION_METHOD_FIELD_NAME = "creationMethod"; public static final String METRICS_FIELD_NAME = "metrics"; public static final String ID_FIELD_NAME = "id"; ///////////////////////// ATTRIBUTES /** * System generated persistence id. */ @Id @GeneratedValue(strategy=GenerationType.IDENTITY) @Column(name = idColumnName) private long id; /** * The graph that the cover is based on. */ @Id //@ManyToOne(fetch=FetchType.LAZY) @JoinColumns( { @JoinColumn(name = graphIdColumnName, referencedColumnName = CustomGraph.idColumnName), @JoinColumn(name = graphUserColumnName, referencedColumnName = CustomGraph.userColumnName) }) private CustomGraph graph = new CustomGraph(); /** * The name of the cover. */ @Column(name = nameColumnName) private String name = ""; // /** // * A description of the cover. // */ // @Column(name = descriptionColumnName) // private String description = ""; // /** // * Last time of modification. // */ // @Version // @Column(name = lastUpdateColumnName) // private Timestamp lastUpdate; /** * Logged data about the algorithm that created the cover. */ @OneToOne(orphanRemoval = true, cascade={CascadeType.ALL}) @JoinColumn(name = creationMethodColumnName) private CoverCreationLog creationMethod = new CoverCreationLog(CoverCreationType.UNDEFINED, new HashMap<String, String>(), new HashSet<GraphType>()); /** * The communities forming the cover. */ @OneToMany(mappedBy = "cover", orphanRemoval = true, cascade={CascadeType.ALL} /*, fetch=FetchType.LAZY */) private List<Community> communities = new ArrayList<Community>(); /** * The metric logs calculated for the cover. */ @OneToMany(mappedBy = "cover", orphanRemoval = true, cascade={CascadeType.ALL}) private List<OcdMetricLog> metrics = new ArrayList<OcdMetricLog>(); @Column(name = simCostsColumnName) private double simCosts; /////////////////////////////////////////// METHODS AND CONSTRUCTORS /** * Creates a new instance. * Only for persistence purposes. */ protected Cover() { } /** * Creates a new instance. * @param graph The graph that the cover is based on. */ public Cover(CustomGraph graph) { this.graph = graph; } /** * Creates an instance of a cover by deriving the communities from a membership matrix. * Note that the membership matrix (and consequently the cover) will automatically be row-wise normalized according to the 1-norm. * @param graph The corresponding graph. * @param memberships A membership matrix, with non-negative entries. Contains one row for each node and one column for each community. * Entry (i,j) in row i and column j represents the membership degree / belonging factor of the node with index i * with respect to the community with index j. */ public Cover(CustomGraph graph, Matrix memberships) { this.graph = graph; setMemberships(memberships, true); } /** * Getter for the id. * @return The id. */ public long getId() { return id; } /** * Setter for the graph that the cover is based on. * @param graph The graph. */ public void setGraph(CustomGraph graph) { this.graph = graph; } /** * Getter for the graph that the cover is based on. * @return The graph. */ public CustomGraph getGraph() { return graph; } /** * Getter for the membership matrix representing the community structure. * @return The membership matrix. Contains one row for each node and one column for each community. * Entry (i,j) in row i and column j represents the membership degree / belonging factor of the node with index i * with respect to the community with index j. All entries are non-negative and the matrix is row-wise normalized according to the 1-norm. */ public Matrix getMemberships() { Matrix memberships = new CCSMatrix(graph.nodeCount(), communities.size()); Map<CustomNode, Node> reverseNodeMap = new HashMap<CustomNode, Node>(); NodeCursor nodes = graph.nodes(); while(nodes.ok()) { Node node = nodes.node(); reverseNodeMap.put(graph.getCustomNode(node), node); nodes.next(); } for(int i=0; i<communities.size(); i++) { Community community = communities.get(i); for(Map.Entry<Node, Double> membership : community.getMemberships().entrySet()) { memberships.set(membership.getKey().index(), i, membership.getValue()); } } return memberships; } /* * Sets the communities from a membership matrix. All metric logs (besides optionally the execution time) will be removed from the cover. * Note that the membership matrix (and consequently the cover) will automatically be row normalized. * @param memberships A membership matrix, with non negative entries. Each row i contains the belonging factors of the node with index i * of the corresponding graph. Hence the number of rows corresponds the number of graph nodes and the number of columns the * number of communities. * @param keepExecutionTime Decides whether the (first) execution time metric log is kept. */ protected void setMemberships(Matrix memberships, boolean keepExecutionTime) { if(memberships.rows() != graph.nodeCount()) { throw new IllegalArgumentException("The row number of the membership matrix must correspond to the graph node count."); } communities.clear(); OcdMetricLog executionTime = getMetric(OcdMetricType.EXECUTION_TIME); metrics.clear(); if(executionTime != null && keepExecutionTime) { metrics.add(executionTime); } memberships = this.normalizeMembershipMatrix(memberships); Node[] nodes = graph.getNodeArray(); for(int j=0; j<memberships.columns(); j++) { Community community = new Community(this); communities.add(community); } for(int i=0; i<memberships.rows(); i++) { NonZeroEntriesVectorProcedure procedure = new NonZeroEntriesVectorProcedure(); memberships.getRow(i).eachNonZero(procedure); List<Integer> nonZeroEntries = procedure.getNonZeroEntries(); for(int j : nonZeroEntries) { Community community = communities.get(j); community.setBelongingFactor(nodes[i], memberships.get(i, j)); } } } /** * Sets the communities from a membership matrix. All metric logs (besides optionally the execution time) will be removed from the cover. * Note that the membership matrix (and consequently the cover) will automatically be row-wise normalized according to the 1-norm. * @param memberships A membership matrix, with non negative entries. Each row i contains the belonging factors of the node with index i * of the corresponding graph. Hence the number of rows corresponds the number of graph nodes and the number of columns the * number of communities. */ public void setMemberships(Matrix memberships) { setMemberships(memberships, false); } /** * Getter for the cover name. * @return The name. */ public String getName() { return name; } /** * Setter for the cover name. * @param name The name. */ public void setName(String name) { this.name = name; } // /** // * Getter for the cover description. // * @return // */ // public String getDescription() { // return description; // } // /** // * Setter for the cover description. // * @param description // */ // public void setDescription(String description) { // this.description = description; // } // public Timestamp getLastUpdate() { // return lastUpdate; // } /** * Getter for the cover creation method. * @return The creation method. */ public CoverCreationLog getCreationMethod() { return creationMethod; } /** * Setter for the cover creation method. * @param creationMethod The creation method. */ public void setCreationMethod(CoverCreationLog creationMethod) { if(creationMethod != null) { this.creationMethod = creationMethod; } else { this.creationMethod = new CoverCreationLog(CoverCreationType.UNDEFINED, new HashMap<String, String>(), new HashSet<GraphType>()); } } /** * Getter for the metric logs calculated for the cover. * @return The metric logs. */ public List<OcdMetricLog> getMetrics() { return metrics; } /** * Setter for the metric logs calculated for the cover. * @param metrics The metric logs. */ public void setMetrics(List<OcdMetricLog> metrics) { this.metrics.clear(); for(OcdMetricLog metric : metrics) { if(metric != null) this.metrics.add(metric); } } /** * Returns the first metric occurrence with the corresponding metric type. * @param metricType The metric type. * @return The metric. Null if no such metric exists. */ public OcdMetricLog getMetric(OcdMetricType metricType) { for(OcdMetricLog metric : this.metrics){ if(metricType == metric.getType()) { return metric; } } return null; } /** * Adds a metric log to the cover. * @param metric The metric log. */ public void addMetric(OcdMetricLog metric) { if(metric != null) { this.metrics.add(metric); } } /** * Removes a metric log from the cover. * @param metric The metric log. */ public void removeMetric(OcdMetricLog metric) { this.metrics.remove(metric); } /** * Returns the community count of the cover. * @return The community count. */ public int communityCount() { return communities.size(); } /** * Returns the indices of the communities that a node is member of. * @param node The node. * @return The community indices. */ public List<Integer> getCommunityIndices(Node node) { List<Integer> communityIndices = new ArrayList<Integer>(); for(int j=0; j < communities.size(); j++) { if(this.communities.get(j).getBelongingFactor(node) > 0) { communityIndices.add(j); } } return communityIndices; } /** * Getter for the belonging factor / membership degree of a node for a certain community. * @param node The node. * @param communityIndex The community index. * @return The belonging factor. */ public double getBelongingFactor(Node node, int communityIndex) { return communities.get(communityIndex).getBelongingFactor(node); } /** * Getter for the name of a certain community. * @param communityIndex The community index. * @return The name. */ public String getCommunityName(int communityIndex) { return communities.get(communityIndex).getName(); } /** * Setter for the name of a certain community. * @param communityIndex The community index. * @param name The name. */ public void setCommunityName(int communityIndex, String name) { communities.get(communityIndex).setName(name); } /** * Getter for the color of a certain community. * @param communityIndex The community index. * @return The color. */ public Color getCommunityColor(int communityIndex) { return communities.get(communityIndex).getColor(); } /** * Setter for the color of a certain community. * @param communityIndex The community index. * @param color The color. */ public void setCommunityColor(int communityIndex, Color color) { communities.get(communityIndex).setColor(color); } /** * Normalizes each row of a matrix using the one norm. * Note that a unit vector column is added for each row that is equal * to zero to create a separate node community. * @param matrix The memberships matrix to be normalized and set. * @return The normalized membership matrix. */ protected Matrix normalizeMembershipMatrix(Matrix matrix) { List<Integer> zeroRowIndices = new ArrayList<Integer>(); for(int i=0; i<matrix.rows(); i++) { Vector row = matrix.getRow(i); double norm = row.fold(Vectors.mkManhattanNormAccumulator()); if(norm != 0) { row = row.divide(norm); matrix.setRow(i, row); } else { zeroRowIndices.add(i); } } /* * Resizing also rows is required in case there are zero columns. */ matrix = matrix.resize(graph.nodeCount(), matrix.columns() + zeroRowIndices.size()); for(int i = 0; i < zeroRowIndices.size(); i++) { matrix.set(zeroRowIndices.get(i), matrix.columns() - zeroRowIndices.size() + i, 1d); } return matrix; } /** * Filters the cover membership matrix by removing insignificant membership values. * The cover is then normalized and empty communities are removed. All metric results * besides the execution time are removed as well. * @param threshold A threshold value, all entries below the threshold will be set to 0, unless they are the maximum * belonging factor of the node. */ public void filterMembershipsbyThreshold(double threshold) { Matrix memberships = this.getMemberships(); for(int i=0; i<memberships.rows(); i++) { setRowEntriesBelowThresholdToZero(memberships, i, threshold); } this.setMemberships(memberships , true); removeEmptyCommunities(); } @Override public String toString() { String coverString = "Cover: " + getName() + "\n"; coverString += "Graph: " + getGraph().getName() + "\n"; coverString += "Algorithm: " + getCreationMethod().getType().toString() + "\n" + "params:" + "\n"; for(Map.Entry<String, String> entry : getCreationMethod().getParameters().entrySet()) { coverString += entry.getKey() + " = " + entry.getValue() + "\n"; } coverString += "Community Count: " + communityCount() + "\n"; OcdMetricLog metric; for(int i=0; i<metrics.size(); i++) { metric = metrics.get(i); coverString += metric.getType().toString() + " = "; coverString += metric.getValue() + "\n" + "params:" + "\n"; for(Map.Entry<String, String> entry : metric.getParameters().entrySet()) { coverString += entry.getKey() + " = " + entry.getValue() + "\n"; } coverString += "\n"; } coverString += "Membership Matrix\n"; coverString += getMemberships().toString(); return coverString; } /** * Returns the size (i.e. the amount of members) of a certain community. * @param communityIndex The community index. * @return The size. */ public int getCommunitySize(int communityIndex) { return communities.get(communityIndex).getSize(); } /** * Filters a matrix row by setting all entries which are lower than a threshold value and the row's max entry to zero. * @param matrix The matrix. * @param rowIndex The index of the row to filter. * @param threshold The threshold. */ protected void setRowEntriesBelowThresholdToZero(Matrix matrix, int rowIndex, double threshold) { Vector row = matrix.getRow(rowIndex); double rowThreshold = Math.min(row.fold(Vectors.mkMaxAccumulator()), threshold); BelowThresholdEntriesVectorProcedure procedure = new BelowThresholdEntriesVectorProcedure(rowThreshold); row.eachNonZero(procedure); List<Integer> belowThresholdEntries = procedure.getBelowThresholdEntries(); for(int i : belowThresholdEntries) { row.set(i, 0); } matrix.setRow(rowIndex, row); } /** * Removes all empty communities from the graph. * A community is considered to be empty when it does not have any members, * i.e. the corresponding belonging factor equals 0 for each node. */ protected void removeEmptyCommunities() { Iterator<Community> it = communities.iterator(); while(it.hasNext()) { Community community = it.next(); if(community.getSize() == 0) { it.remove(); } } } public void setSimCosts(double costs){ this.simCosts = costs; } public double getSimCosts(){ return this.simCosts; } /** * Get the community structure of a cover, i.e. * the number of communities of a certain size * YLi */ public Map<Integer,Integer> getCommunityStructure(){ Map<Integer,Integer> communityStructure=new HashMap<Integer,Integer>(); for (int i=0;i<communities.size();i++){ int size=communities.get(i).getSize(); if (communityStructure.keySet().contains(size)){ communityStructure.put(size, communityStructure.get(size) + 1); }else{ communityStructure.put(size, 1); } } return communityStructure; } }