diff --git a/README.md b/README.md index 5276ba33..5652e64b 100644 --- a/README.md +++ b/README.md @@ -53,7 +53,7 @@ compile 'org.gephi:graphstore:0.8.0' ## Dependencies -GraphStore is built for JRE 17+ and depends on FastUtil and Colt. +GraphStore is built for JRE 17+ and depends on FastUtil. For a complete list of dependencies, consult the `pom.xml` file. diff --git a/pom.xml b/pom.xml index 462e27ad..14e2c0da 100644 --- a/pom.xml +++ b/pom.xml @@ -67,11 +67,6 @@ fastutil 8.5.16 - - colt - colt - 1.2.0 - diff --git a/src/main/java/org/gephi/graph/impl/ColumnObserverImpl.java b/src/main/java/org/gephi/graph/impl/ColumnObserverImpl.java index d90aea76..f6a38ef4 100644 --- a/src/main/java/org/gephi/graph/impl/ColumnObserverImpl.java +++ b/src/main/java/org/gephi/graph/impl/ColumnObserverImpl.java @@ -15,8 +15,7 @@ */ package org.gephi.graph.impl; -import cern.colt.bitvector.BitVector; -import cern.colt.bitvector.QuickBitVector; +import java.util.BitSet; import it.unimi.dsi.fastutil.objects.ObjectArrayList; import it.unimi.dsi.fastutil.objects.ObjectList; import it.unimi.dsi.fastutil.objects.ObjectLists; @@ -40,7 +39,7 @@ public class ColumnObserverImpl implements ColumnObserver { protected boolean destroyed; // Config protected final boolean withDiff; - protected BitVector bitVector; + protected BitSet bitVector; // Cache protected ColumnDiffImpl columnDiff; @@ -109,7 +108,7 @@ private void refreshDiff() { boolean node = AttributeUtils.isNodeColumn(column); columnDiff = node ? new NodeColumnDiffImpl() : new EdgeColumnDiffImpl(); - int size = bitVector.size(); + int size = bitVector.length(); for (int i = 0; i < size; i++) { boolean t = bitVector.get(i); @@ -179,19 +178,11 @@ public EdgeIterable getTouchedElements() { private void ensureVectorSize(ElementImpl element) { int sid = element.getStoreId(); if (bitVector == null) { - bitVector = new BitVector(sid + 1); - } else if (sid >= bitVector.size()) { - int newSize = Math.min(Math + int initialSize = Math.min(Math .max(sid + 1, (int) (sid * GraphStoreConfiguration.COLUMNDIFF_GROWING_FACTOR)), Integer.MAX_VALUE); - bitVector = growBitVector(bitVector, newSize); + bitVector = new BitSet(initialSize); } - } - - private BitVector growBitVector(BitVector bitVector, int size) { - long[] elements = bitVector.elements(); - long[] newElements = QuickBitVector.makeBitVector(size, 1); - System.arraycopy(elements, 0, newElements, 0, elements.length); - return new BitVector(newElements, size); + // BitSet grows automatically when setting bits, no need to manually grow } private void readLock() { diff --git a/src/main/java/org/gephi/graph/impl/GraphViewImpl.java b/src/main/java/org/gephi/graph/impl/GraphViewImpl.java index 6842d67a..ae02fb75 100644 --- a/src/main/java/org/gephi/graph/impl/GraphViewImpl.java +++ b/src/main/java/org/gephi/graph/impl/GraphViewImpl.java @@ -15,11 +15,9 @@ */ package org.gephi.graph.impl; -import cern.colt.bitvector.BitVector; -import cern.colt.bitvector.QuickBitVector; -import it.unimi.dsi.fastutil.ints.IntOpenHashSet; import java.util.ArrayList; import java.util.Arrays; +import java.util.BitSet; import java.util.Collection; import java.util.Iterator; import java.util.List; @@ -40,8 +38,8 @@ public class GraphViewImpl implements GraphView { protected final boolean nodeView; protected final boolean edgeView; protected final GraphAttributesImpl attributes; - protected BitVector nodeBitVector; - protected BitVector edgeBitVector; + protected BitSet nodeBitVector; + protected BitSet edgeBitVector; protected int storeId; // Version protected final GraphVersion version; @@ -64,11 +62,11 @@ public GraphViewImpl(final GraphStore store, boolean nodes, boolean edges) { this.edgeView = edges; this.attributes = new GraphAttributesImpl(); if (nodes) { - this.nodeBitVector = new BitVector(store.nodeStore.maxStoreId()); + this.nodeBitVector = new BitSet(store.nodeStore.maxStoreId()); } else { this.nodeBitVector = null; } - this.edgeBitVector = new BitVector(store.edgeStore.maxStoreId()); + this.edgeBitVector = new BitSet(store.edgeStore.maxStoreId()); this.typeCounts = new int[GraphStoreConfiguration.VIEW_DEFAULT_TYPE_COUNT]; this.mutualEdgeTypeCounts = new int[GraphStoreConfiguration.VIEW_DEFAULT_TYPE_COUNT]; @@ -85,17 +83,18 @@ public GraphViewImpl(final GraphViewImpl view, boolean nodes, boolean edges) { this.edgeView = edges; this.attributes = new GraphAttributesImpl(); if (nodes) { - this.nodeBitVector = view.nodeBitVector.copy(); + this.nodeBitVector = (BitSet) view.nodeBitVector.clone(); this.nodeCount = view.nodeCount; } else { this.nodeBitVector = null; } this.edgeCount = view.edgeCount; - this.edgeBitVector = view.edgeBitVector.copy(); + this.edgeBitVector = (BitSet) view.edgeBitVector.clone(); this.typeCounts = new int[view.typeCounts.length]; System.arraycopy(view.typeCounts, 0, typeCounts, 0, view.typeCounts.length); this.mutualEdgeTypeCounts = new int[view.mutualEdgeTypeCounts.length]; System.arraycopy(view.mutualEdgeTypeCounts, 0, mutualEdgeTypeCounts, 0, view.mutualEdgeTypeCounts.length); + this.mutualEdgesCount = view.mutualEdgesCount; this.directedDecorator = new GraphViewDecorator(graphStore, this, false); this.undirectedDecorator = new GraphViewDecorator(graphStore, this, true); this.version = graphStore.version != null ? new GraphVersion(directedDecorator) : null; @@ -144,9 +143,6 @@ public boolean addNode(final Node node) { int edgeid = edge.storeId; boolean edgeisSet = edgeBitVector.get(edgeid); if (!edgeisSet) { - - incrementEdgeVersion(); - addEdge(edge); } // End @@ -268,23 +264,25 @@ public boolean removeNodeAll(final Collection nodes) { public boolean retainNodes(final Collection c) { if (nodeView) { if (!c.isEmpty()) { - IntOpenHashSet set = new IntOpenHashSet(c.size()); + // Build BitSet of nodes to retain + BitSet retainSet = new BitSet(graphStore.nodeStore.maxStoreId()); for (Node o : c) { checkValidNodeObject(o); - set.add(o.getStoreId()); + retainSet.set(o.getStoreId()); } - boolean changed = false; - int nodeSize = nodeBitVector.size(); - for (int i = 0; i < nodeSize; i++) { - boolean t = nodeBitVector.get(i); - if (t && !set.contains(i)) { - if (removeNode(getNode(i))) { - changed = true; - } - } + // Find nodes to remove: nodes in this view but NOT in retain set + // This is equivalent to: nodeBitVector AND NOT retainSet + BitSet nodesToRemove = (BitSet) nodeBitVector.clone(); + nodesToRemove.andNot(retainSet); + + if (nodesToRemove.isEmpty()) { + return false; } - return changed; + + // Bulk remove nodes + bulkRemoveNodes(nodesToRemove); + return true; } else if (nodeCount != 0) { clear(); return true; @@ -296,22 +294,25 @@ public boolean retainNodes(final Collection c) { public boolean retainEdges(final Collection c) { if (edgeView) { if (!c.isEmpty()) { - IntOpenHashSet set = new IntOpenHashSet(c.size()); + // Build BitSet of edges to retain + BitSet retainSet = new BitSet(graphStore.edgeStore.maxStoreId()); for (Edge o : c) { checkValidEdgeObject(o); - set.add(o.getStoreId()); + retainSet.set(o.getStoreId()); } - boolean changed = false; - int edgeSize = edgeBitVector.size(); - for (int i = 0; i < edgeSize; i++) { - boolean t = edgeBitVector.get(i); - if (t && !set.contains(i)) { - removeEdge(getEdge(i)); - changed = true; - } + // Find edges to remove: edges in this view but NOT in retain set + // This is equivalent to: edgeBitVector AND NOT retainSet + BitSet edgesToRemove = (BitSet) edgeBitVector.clone(); + edgesToRemove.andNot(retainSet); + + if (edgesToRemove.isEmpty()) { + return false; } - return changed; + + // Bulk remove edges + bulkRemoveEdges(edgesToRemove); + return true; } else if (edgeCount != 0) { clearEdges(); return true; @@ -413,16 +414,10 @@ public void clearEdges() { public void fill() { if (nodeView) { - if (nodeCount > 0) { - nodeBitVector = new BitVector(graphStore.nodeStore.maxStoreId()); - } - nodeBitVector.not(); + nodeBitVector.set(0, graphStore.nodeStore.maxStoreId(), true); this.nodeCount = graphStore.nodeStore.size(); } - if (edgeCount > 0) { - edgeBitVector = new BitVector(graphStore.edgeStore.maxStoreId()); - } - edgeBitVector.not(); + edgeBitVector.set(0, graphStore.edgeStore.maxStoreId()); this.edgeCount = graphStore.edgeStore.size(); int typeLength = graphStore.edgeStore.longDictionary.length; @@ -474,90 +469,126 @@ public boolean containsEdge(final Edge edge) { } public void intersection(final GraphViewImpl otherView) { - BitVector nodeOtherBitVector = otherView.nodeBitVector; - BitVector edgeOtherBitVector = otherView.edgeBitVector; + BitSet nodeOtherBitVector = otherView.nodeBitVector; + BitSet edgeOtherBitVector = otherView.edgeBitVector; if (nodeView) { - int nodeSize = nodeBitVector.size(); - for (int i = 0; i < nodeSize; i++) { - boolean t = nodeBitVector.get(i); - boolean o = nodeOtherBitVector.get(i); - if (t && !o) { - removeNode(getNode(i)); - } + // Find nodes to remove: nodes in this view but NOT in other view + BitSet nodesToRemove = (BitSet) nodeBitVector.clone(); + nodesToRemove.andNot(nodeOtherBitVector); + + if (!nodesToRemove.isEmpty()) { + // Bulk remove nodes + bulkRemoveNodes(nodesToRemove); } } if (edgeView) { - int edgeSize = edgeBitVector.size(); - for (int i = 0; i < edgeSize; i++) { - boolean t = edgeBitVector.get(i); - boolean o = edgeOtherBitVector.get(i); - if (t && !o) { - removeEdge(getEdge(i)); - } + // Find edges to remove: edges in this view but NOT in other view + BitSet edgesToRemove = (BitSet) edgeBitVector.clone(); + edgesToRemove.andNot(edgeOtherBitVector); + + if (!edgesToRemove.isEmpty()) { + // Bulk remove edges + bulkRemoveEdges(edgesToRemove); } } } public void union(final GraphViewImpl otherView) { - BitVector nodeOtherBitVector = otherView.nodeBitVector; - BitVector edgeOtherBitVector = otherView.edgeBitVector; + BitSet nodeOtherBitVector = otherView.nodeBitVector; + BitSet edgeOtherBitVector = otherView.edgeBitVector; if (nodeView) { - int nodeSize = nodeBitVector.size(); - for (int i = 0; i < nodeSize; i++) { - boolean t = nodeBitVector.get(i); - boolean o = nodeOtherBitVector.get(i); - if (!t && o) { - addNode(getNode(i)); - } + // Find nodes to add: nodes in other view but NOT in this view + BitSet nodesToAdd = (BitSet) nodeOtherBitVector.clone(); + nodesToAdd.andNot(nodeBitVector); + + if (!nodesToAdd.isEmpty()) { + // Bulk add nodes + bulkAddNodes(nodesToAdd); } } if (edgeView) { - int edgeSize = edgeBitVector.size(); - for (int i = 0; i < edgeSize; i++) { - boolean t = edgeBitVector.get(i); - boolean o = edgeOtherBitVector.get(i); - if (!t && o) { - addEdge(getEdge(i)); - } + // Find edges to add: edges in other view but NOT in this view + BitSet edgesToAdd = (BitSet) edgeOtherBitVector.clone(); + edgesToAdd.andNot(edgeBitVector); + + if (!edgesToAdd.isEmpty()) { + // Bulk add edges + bulkAddEdges(edgesToAdd); } } } public void not() { + // Flip node bits if this is a node view if (nodeView) { - nodeBitVector.not(); + nodeBitVector.flip(0, graphStore.nodeStore.maxStoreId()); this.nodeCount = graphStore.nodeStore.size() - this.nodeCount; + incrementNodeVersion(); } - edgeBitVector.not(); + // Flip edge bits + edgeBitVector.flip(0, graphStore.edgeStore.maxStoreId()); + + // Update edge counts by subtracting from totals this.edgeCount = graphStore.edgeStore.size() - this.edgeCount; - for (int i = 0; i < typeCounts.length; i++) { + + // Ensure type count arrays are sized to match the store + int storeTypeLength = graphStore.edgeStore.longDictionary.length; + if (typeCounts.length < storeTypeLength) { + int[] newTypeCounts = new int[storeTypeLength]; + System.arraycopy(typeCounts, 0, newTypeCounts, 0, typeCounts.length); + typeCounts = newTypeCounts; + + int[] newMutualCounts = new int[storeTypeLength]; + System.arraycopy(mutualEdgeTypeCounts, 0, newMutualCounts, 0, mutualEdgeTypeCounts.length); + mutualEdgeTypeCounts = newMutualCounts; + } + + // Invert all type counts (including types that weren't in the view before) + for (int i = 0; i < storeTypeLength; i++) { this.typeCounts[i] = graphStore.edgeStore.longDictionary[i].size() - this.typeCounts[i]; } - for (int i = 0; i < mutualEdgeTypeCounts.length; i++) { + for (int i = 0; i < graphStore.edgeStore.mutualEdgesTypeSize.length; i++) { this.mutualEdgeTypeCounts[i] = graphStore.edgeStore.mutualEdgesTypeSize[i] - this.mutualEdgeTypeCounts[i]; } this.mutualEdgesCount = graphStore.edgeStore.mutualEdgesSize - this.mutualEdgesCount; - if (nodeView) { - incrementNodeVersion(); - } incrementEdgeVersion(); + // If node view is enabled, remove edges with invalid endpoints + // Optimization: Only iterate through edges that are NOW in the view (after + // flip) + // instead of all edges in the store if (nodeView) { - for (Edge e : graphStore.edgeStore) { - boolean t = edgeBitVector.get(e.getStoreId()); - if (t && (!nodeBitVector.get(e.getSource().getStoreId()) || !nodeBitVector - .get(e.getTarget().getStoreId()))) { - removeEdge((EdgeImpl) e); + BitSet edgesToRemove = new BitSet(); + + // Iterate only over edges that are set in the view (much faster for sparse + // views) + for (int edgeId = edgeBitVector.nextSetBit(0); edgeId >= 0; edgeId = edgeBitVector.nextSetBit(edgeId + 1)) { + EdgeImpl edge = getEdge(edgeId); + if (edge == null) { + // SAFETY: Edge no longer exists in store + edgesToRemove.set(edgeId); + continue; } + // Check if both endpoints are in the node view + if (!nodeBitVector.get(edge.source.storeId) || !nodeBitVector.get(edge.target.storeId)) { + edgesToRemove.set(edgeId); + } + } + + // Bulk remove invalid edges + if (!edgesToRemove.isEmpty()) { + bulkRemoveEdgesForNot(edgesToRemove); } } + // Rebuild indexes (necessary for NOT operation as the view content has + // completely changed) if (nodeView) { IndexStore nodeIndexStore = graphStore.nodeTable.store.indexStore; if (nodeIndexStore != null) { @@ -590,6 +621,251 @@ public void addEdgeInNodeView(EdgeImpl edge) { } } + /** + * Bulk remove nodes from the view. This is more efficient than removing nodes + * one by one as it batches index updates and increments version only once. + */ + private void bulkRemoveNodes(BitSet nodesToRemove) { + // First pass: collect all edges to remove (incident to removed nodes) + BitSet edgesToRemove = new BitSet(); + for (int nodeId = nodesToRemove.nextSetBit(0); nodeId >= 0; nodeId = nodesToRemove.nextSetBit(nodeId + 1)) { + NodeImpl node = getNode(nodeId); + if (node == null) { + continue; // SAFETY: Node was removed from store (storeId reused or deleted) + } + + EdgeInOutIterator itr = graphStore.edgeStore.edgeIterator(node, false); + while (itr.hasNext()) { + EdgeImpl edge = itr.next(); + int edgeId = edge.storeId; + if (edgeBitVector.get(edgeId)) { + edgesToRemove.set(edgeId); + } + } + } + + // Remove edges in bulk + if (!edgesToRemove.isEmpty()) { + bulkRemoveEdges(edgesToRemove); + } + + // Update node bit vector + int removedCount = nodesToRemove.cardinality(); + nodeBitVector.andNot(nodesToRemove); + nodeCount -= removedCount; + incrementNodeVersion(); + + // Bulk update indexes + IndexStore indexStore = graphStore.nodeTable.store.indexStore; + TimeIndexStore timeIndexStore = graphStore.timeStore.nodeIndexStore; + + if (indexStore != null || timeIndexStore != null) { + for (int i = nodesToRemove.nextSetBit(0); i >= 0; i = nodesToRemove.nextSetBit(i + 1)) { + NodeImpl node = getNode(i); + if (node != null) { // SAFETY: Skip if node no longer exists + if (indexStore != null) { + indexStore.clearInView(node, this); + } + if (timeIndexStore != null) { + timeIndexStore.clearInView(node, this); + } + } + } + } + } + + /** + * Bulk add nodes to the view. This is more efficient than adding nodes one by + * one as it batches index updates and increments version only once. + */ + private void bulkAddNodes(BitSet nodesToAdd) { + // Update node bit vector + int addedCount = nodesToAdd.cardinality(); + nodeBitVector.or(nodesToAdd); + nodeCount += addedCount; + incrementNodeVersion(); + + // Bulk update indexes + IndexStore indexStore = graphStore.nodeTable.store.indexStore; + TimeIndexStore timeIndexStore = graphStore.timeStore.nodeIndexStore; + + if (indexStore != null || timeIndexStore != null) { + for (int i = nodesToAdd.nextSetBit(0); i >= 0; i = nodesToAdd.nextSetBit(i + 1)) { + NodeImpl node = getNode(i); + if (node != null) { // SAFETY: Skip if node no longer exists + if (indexStore != null) { + indexStore.indexInView(node, this); + } + if (timeIndexStore != null) { + timeIndexStore.indexInView(node, this); + } + } + } + } + + // If nodeView && !edgeView, add edges between newly added nodes and existing + // nodes + if (nodeView && !edgeView) { + BitSet edgesToAdd = new BitSet(); + for (int nodeId = nodesToAdd.nextSetBit(0); nodeId >= 0; nodeId = nodesToAdd.nextSetBit(nodeId + 1)) { + NodeImpl node = getNode(nodeId); + if (node == null) { + continue; // SAFETY: Skip if node no longer exists + } + + EdgeInOutIterator itr = graphStore.edgeStore.edgeIterator(node, false); + while (itr.hasNext()) { + EdgeImpl edge = itr.next(); + NodeImpl opposite = edge.source == node ? edge.target : edge.source; + // Check if opposite node is in view and edge is not already in view + if (nodeBitVector.get(opposite.storeId) && !edgeBitVector.get(edge.storeId)) { + edgesToAdd.set(edge.storeId); + } + } + } + + if (!edgesToAdd.isEmpty()) { + bulkAddEdges(edgesToAdd); + } + } + } + + /** + * Bulk remove edges from the view. This is more efficient than removing edges + * one by one as it updates stats in bulk and increments version only once. + */ + private void bulkRemoveEdges(BitSet edgesToRemove) { + // Update edge bit vector + int removedCount = edgesToRemove.cardinality(); + edgeBitVector.andNot(edgesToRemove); + edgeCount -= removedCount; + + // Update type counts and mutual edge counts + for (int i = edgesToRemove.nextSetBit(0); i >= 0; i = edgesToRemove.nextSetBit(i + 1)) { + EdgeImpl edge = getEdge(i); + if (edge == null) { + continue; // SAFETY: Edge was removed from store (storeId reused or deleted) + } + + int type = edge.type; + ensureTypeCountArrayCapacity(type); + typeCounts[type]--; + + if (edge.isMutual() && !edge.isSelfLoop()) { + EdgeImpl reverseEdge = graphStore.edgeStore.get(edge.target, edge.source, edge.type, false); + if (reverseEdge != null && containsEdge(reverseEdge)) { + mutualEdgeTypeCounts[type]--; + mutualEdgesCount--; + } + } + } + + incrementEdgeVersion(); + + // Bulk update indexes + IndexStore indexStore = graphStore.edgeTable.store.indexStore; + TimeIndexStore timeIndexStore = graphStore.timeStore.edgeIndexStore; + + if (indexStore != null || timeIndexStore != null) { + for (int i = edgesToRemove.nextSetBit(0); i >= 0; i = edgesToRemove.nextSetBit(i + 1)) { + EdgeImpl edge = getEdge(i); + if (edge != null) { // SAFETY: Skip if edge no longer exists + if (indexStore != null) { + indexStore.clearInView(edge, this); + } + if (timeIndexStore != null) { + timeIndexStore.clearInView(edge, this); + } + } + } + } + } + + /** + * Bulk add edges to the view. This is more efficient than adding edges one by + * one as it updates stats in bulk and increments version only once. + */ + private void bulkAddEdges(BitSet edgesToAdd) { + // Update edge bit vector + int addedCount = edgesToAdd.cardinality(); + edgeBitVector.or(edgesToAdd); + edgeCount += addedCount; + + // Update type counts and mutual edge counts + for (int i = edgesToAdd.nextSetBit(0); i >= 0; i = edgesToAdd.nextSetBit(i + 1)) { + EdgeImpl edge = getEdge(i); + if (edge == null) { + continue; // SAFETY: Skip if edge no longer exists in store + } + + int type = edge.type; + ensureTypeCountArrayCapacity(type); + typeCounts[type]++; + + if (edge.isMutual() && !edge.isSelfLoop()) { + EdgeImpl reverseEdge = graphStore.edgeStore.get(edge.target, edge.source, edge.type, false); + if (reverseEdge != null && containsEdge(reverseEdge)) { + mutualEdgeTypeCounts[type]++; + mutualEdgesCount++; + } + } + } + + incrementEdgeVersion(); + + // Bulk update indexes + IndexStore indexStore = graphStore.edgeTable.store.indexStore; + TimeIndexStore timeIndexStore = graphStore.timeStore.edgeIndexStore; + + if (indexStore != null || timeIndexStore != null) { + for (int i = edgesToAdd.nextSetBit(0); i >= 0; i = edgesToAdd.nextSetBit(i + 1)) { + EdgeImpl edge = getEdge(i); + if (edge != null) { // SAFETY: Skip if edge no longer exists + if (indexStore != null) { + indexStore.indexInView(edge, this); + } + if (timeIndexStore != null) { + timeIndexStore.indexInView(edge, this); + } + } + } + } + } + + /** + * Special bulk remove for the not() operation. This updates the bit vector and + * stats but does NOT increment version or update indexes (those are handled + * separately in not()). + */ + private void bulkRemoveEdgesForNot(BitSet edgesToRemove) { + // Update edge bit vector + int removedCount = edgesToRemove.cardinality(); + edgeBitVector.andNot(edgesToRemove); + edgeCount -= removedCount; + + // Update type counts and mutual edge counts + for (int i = edgesToRemove.nextSetBit(0); i >= 0; i = edgesToRemove.nextSetBit(i + 1)) { + EdgeImpl edge = getEdge(i); + if (edge == null) { + continue; // SAFETY: Skip if edge no longer exists in store + } + + int type = edge.type; + ensureTypeCountArrayCapacity(type); + typeCounts[type]--; + + if (edge.isMutual() && !edge.isSelfLoop()) { + EdgeImpl reverseEdge = graphStore.edgeStore.get(edge.target, edge.source, edge.type, false); + if (reverseEdge != null && containsEdge(reverseEdge)) { + mutualEdgeTypeCounts[type]--; + mutualEdgesCount--; + } + } + } + // Note: Version increment and index updates are handled by the caller (not() + // method) + } + public int getNodeCount() { if (nodeView) { return nodeCount; @@ -687,33 +963,11 @@ protected void destroyAllObservers() { } protected void ensureNodeVectorSize(NodeImpl node) { - int sid = node.storeId; - if (sid >= nodeBitVector.size()) { - int newSize = Math.min(Math - .max(sid + 1, (int) (sid * GraphStoreConfiguration.VIEW_GROWING_FACTOR)), Integer.MAX_VALUE); - nodeBitVector = growBitVector(nodeBitVector, newSize); - } - } - - private void ensureNodeVectorSize(int size) { - if (size > nodeBitVector.size()) { - nodeBitVector = growBitVector(nodeBitVector, size); - } - } - - private void ensureEdgeVectorSize(int size) { - if (size > edgeBitVector.size()) { - edgeBitVector = growBitVector(edgeBitVector, size); - } + // BitSet automatically grows as needed, no manual resizing required } protected void ensureEdgeVectorSize(EdgeImpl edge) { - int sid = edge.storeId; - if (sid >= edgeBitVector.size()) { - int newSize = Math.min(Math - .max(sid + 1, (int) (sid * GraphStoreConfiguration.VIEW_GROWING_FACTOR)), Integer.MAX_VALUE); - edgeBitVector = growBitVector(edgeBitVector, newSize); - } + // BitSet automatically grows as needed, no manual resizing required } protected void setEdgeType(EdgeImpl edgeImpl, int oldType, boolean wasMutual) { @@ -779,13 +1033,10 @@ private void removeEdge(EdgeImpl edgeImpl) { if (indexStore != null) { indexStore.clearInView(edgeImpl, this); } - } - - private BitVector growBitVector(BitVector bitVector, int size) { - long[] elements = bitVector.elements(); - long[] newElements = QuickBitVector.makeBitVector(size, 1); - System.arraycopy(elements, 0, newElements, 0, elements.length); - return new BitVector(newElements, size); + TimeIndexStore timeIndexStore = graphStore.timeStore.edgeIndexStore; + if (timeIndexStore != null) { + timeIndexStore.clearInView(edgeImpl, this); + } } private NodeImpl getNode(int id) { diff --git a/src/main/java/org/gephi/graph/impl/Serialization.java b/src/main/java/org/gephi/graph/impl/Serialization.java index f50449bb..721b41bd 100644 --- a/src/main/java/org/gephi/graph/impl/Serialization.java +++ b/src/main/java/org/gephi/graph/impl/Serialization.java @@ -15,7 +15,7 @@ */ package org.gephi.graph.impl; -import cern.colt.bitvector.BitVector; +import java.util.BitSet; import it.unimi.dsi.fastutil.booleans.BooleanArrayList; import it.unimi.dsi.fastutil.booleans.BooleanOpenHashSet; import it.unimi.dsi.fastutil.bytes.Byte2ObjectOpenHashMap; @@ -667,8 +667,8 @@ private GraphViewImpl deserializeGraphView(final DataInput is) throws IOExceptio int storeId = (Integer) deserialize(is); int nodeCount = (Integer) deserialize(is); int edgeCount = (Integer) deserialize(is); - BitVector nodeCountVector = (BitVector) deserialize(is); - BitVector edgeCountVector = (BitVector) deserialize(is); + BitSet nodeCountVector = (BitSet) deserialize(is); + BitSet edgeCountVector = (BitSet) deserialize(is); int[] typeCounts = (int[]) deserialize(is); int[] mutualEdgeTypeCounts = (int[]) deserialize(is); int mutualEdgesCount = (Integer) deserialize(is); @@ -695,15 +695,36 @@ private GraphViewImpl deserializeGraphView(final DataInput is) throws IOExceptio return view; } - private void serializeBitVector(final DataOutput out, final BitVector bitVector) throws IOException { - serialize(out, bitVector.size()); - serialize(out, bitVector.elements()); + // Made compatible with legacy BitVector serialization, which was in place until + // version 0.8.1 + public void serializeBitSet(final DataOutput out, final BitSet bitSet) throws IOException { + // BitSet.length() returns the index of the highest set bit + 1 + // This gives us the logical size (0 if empty) + int size = bitSet.length(); + + serialize(out, size); + + // Get the long array from BitSet + long[] words = bitSet.toLongArray(); + + // Calculate how many longs BitVector would use for this size + int requiredLongs = (size + 63) / 64; + + // Create array with the exact required size (matching BitVector format) + long[] elements = new long[requiredLongs]; + + // Copy the BitSet data + System.arraycopy(words, 0, elements, 0, Math.min(words.length, requiredLongs)); + + serialize(out, elements); } - private BitVector deserializeBitVector(final DataInput is) throws IOException, ClassNotFoundException { + public BitSet deserializeBitSet(final DataInput is) throws IOException, ClassNotFoundException { int size = (Integer) deserialize(is); long[] elements = (long[]) deserialize(is); - return new BitVector(elements, size); + + // BitSet.valueOf() handles the long array correctly + return BitSet.valueOf(elements); } private void serializeGraphStoreConfiguration(final DataOutput out) throws IOException { @@ -1558,10 +1579,10 @@ protected void serialize(final DataOutput out, final Object obj) throws IOExcept GraphViewImpl b = (GraphViewImpl) obj; out.write(GRAPH_VIEW); serializeGraphView(out, b); - } else if (obj instanceof BitVector) { - BitVector bv = (BitVector) obj; + } else if (obj instanceof BitSet) { + BitSet bs = (BitSet) obj; out.write(BIT_VECTOR); - serializeBitVector(out, bv); + serializeBitSet(out, bs); } else if (obj instanceof GraphVersion) { GraphVersion b = (GraphVersion) obj; out.write(GRAPH_VERSION); @@ -2122,7 +2143,7 @@ protected Object deserialize(DataInput is) throws IOException, ClassNotFoundExce ret = deserializeGraphView(is); break; case BIT_VECTOR: - ret = deserializeBitVector(is); + ret = deserializeBitSet(is); break; case GRAPH_STORE_CONFIGURATION: ret = deserializeGraphStoreConfiguration(is); diff --git a/src/test/java/org/gephi/graph/impl/GraphViewDecoratorTest.java b/src/test/java/org/gephi/graph/impl/GraphViewDecoratorTest.java index ad340afb..a3352163 100644 --- a/src/test/java/org/gephi/graph/impl/GraphViewDecoratorTest.java +++ b/src/test/java/org/gephi/graph/impl/GraphViewDecoratorTest.java @@ -786,6 +786,7 @@ public void testIsIncident() { Edge edge = graphStore.factory.newEdge("edge", n1, n1, EdgeTypeStore.NULL_LABEL, 1.0, true); graphStore.addEdge(edge); + view.addEdge(edge); // Explicitly add edge to view Assert.assertTrue(graph.isIncident(edge, graph.getEdge("0"))); } diff --git a/src/test/java/org/gephi/graph/impl/GraphViewImplTest.java b/src/test/java/org/gephi/graph/impl/GraphViewImplTest.java index 875f6b23..e9e46e84 100644 --- a/src/test/java/org/gephi/graph/impl/GraphViewImplTest.java +++ b/src/test/java/org/gephi/graph/impl/GraphViewImplTest.java @@ -15,7 +15,10 @@ */ package org.gephi.graph.impl; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Collections; +import java.util.List; import org.gephi.graph.api.DirectedSubgraph; import org.gephi.graph.api.Edge; import org.gephi.graph.api.Graph; @@ -507,4 +510,673 @@ public void testEdgeViewSetEdgeTypeMutualEdges() { Assert.assertEquals(view.getUndirectedEdgeCount(0), 0); Assert.assertEquals(view.getUndirectedEdgeCount(1), 1); } + + @Test + public void testCopyConstructor() { + GraphStore graphStore = GraphGenerator.generateTinyGraphStoreWithMutualEdge(); + GraphViewStore store = graphStore.viewStore; + GraphViewImpl view = store.createView(); + view.fill(); + + // Original view should have mutual edges counted + int originalEdgeCount = view.getEdgeCount(); + int originalMutualCount = view.mutualEdgesCount; + int originalUndirectedCount = view.getUndirectedEdgeCount(); + + // Create a copy using the copy constructor + GraphViewImpl copiedView = new GraphViewImpl(view, true, true); + + // Verify + Assert.assertEquals(copiedView.getNodeCount(), view + .getNodeCount(), "Node count should be the same in copied view"); + Assert.assertEquals(copiedView.nodeBitVector, view.nodeBitVector, "Node bit vector should be the same in copied view"); + Assert.assertEquals(copiedView.edgeBitVector, view.edgeBitVector, "Edge bit vector should be the same in copied view"); + + // Verify that mutualEdgesCount was copied correctly + Assert.assertEquals(copiedView.mutualEdgesCount, originalMutualCount, "mutualEdgesCount should be copied in copy constructor"); + Assert.assertEquals(copiedView.getEdgeCount(), originalEdgeCount); + Assert.assertEquals(copiedView + .getUndirectedEdgeCount(), originalUndirectedCount, "getUndirectedEdgeCount() should return correct value after copy"); + } + + @Test + public void testFilledViewRequiresExplicitAdd() { + // Test that users must explicitly add edges to filled views + GraphStore graphStore = GraphGenerator.generateSmallGraphStore(); + GraphViewStore store = graphStore.viewStore; + GraphViewImpl view = store.createView(); + + view.fill(); + int initialEdgeCount = view.getEdgeCount(); + + // Add a new edge to the main graph store + NodeImpl n1 = graphStore.getNode("0"); + NodeImpl n2 = graphStore.getNode("1"); + EdgeImpl newEdge = new EdgeImpl("newEdge", n1, n2, 0, 1.0, true); + graphStore.addEdge(newEdge); + + // Edge should not be in view yet + Assert.assertFalse(view.containsEdge(newEdge)); + + // Explicitly add the edge to the view + boolean added = view.addEdge(newEdge); + + // Now it should be in the view + Assert.assertTrue(added, "addEdge should return true"); + Assert.assertTrue(view.containsEdge(newEdge), "Edge should be in view after explicit add"); + Assert.assertEquals(view.getEdgeCount(), initialEdgeCount + 1); + } + + // ========== Tests for Mutual Edge Counts in Bulk Operations ========== + + @Test + public void testIntersectionWithMutualEdges() { + GraphStore graphStore = GraphGenerator.generateTinyGraphStoreWithMutualEdge(); + GraphViewStore store = graphStore.viewStore; + GraphViewImpl view1 = store.createView(); + GraphViewImpl view2 = store.createView(); + + // Fill both views + view1.fill(); + view2.fill(); + + // Initial state: both views have mutual edges (mutual count is 1 for a pair) + Assert.assertEquals(view1.mutualEdgesCount, 1, "View1 should have mutual count of 1"); + Assert.assertEquals(view1.getEdgeCount(), 2, "View1 should have 2 edges"); + Assert.assertEquals(view1.getUndirectedEdgeCount(), 1, "View1 should have 1 undirected edge"); + + // Remove one of the mutual edges from view2 + EdgeImpl e1 = graphStore.getEdge("1"); + view2.removeEdge(e1); + + // After removing one mutual edge, view2 should have no mutual edges + Assert.assertEquals(view2.mutualEdgesCount, 0, "View2 should have 0 mutual edges after removal"); + Assert.assertEquals(view2.getEdgeCount(), 1, "View2 should have 1 edge"); + Assert.assertEquals(view2.getUndirectedEdgeCount(), 1, "View2 should have 1 undirected edge"); + + // Intersection should result in view1 losing its mutual edge status + view1.intersection(view2); + + Assert.assertEquals(view1.mutualEdgesCount, 0, "View1 should have 0 mutual edges after intersection"); + Assert.assertEquals(view1.getEdgeCount(), 1, "View1 should have 1 edge total"); + Assert.assertEquals(view1.getUndirectedEdgeCount(), 1, "View1 should have 1 undirected edge"); + } + + @Test + public void testUnionWithMutualEdges() { + GraphStore graphStore = GraphGenerator.generateTinyGraphStoreWithMutualEdge(); + GraphViewStore store = graphStore.viewStore; + GraphViewImpl view1 = store.createView(); + GraphViewImpl view2 = store.createView(); + + EdgeImpl e0 = graphStore.getEdge("0"); + EdgeImpl e1 = graphStore.getEdge("1"); + NodeImpl n1 = e0.getSource(); + NodeImpl n2 = e0.getTarget(); + + // View1 has only one edge of the mutual pair + view1.addNode(n1); + view1.addNode(n2); + view1.addEdge(e0); + + // View2 has only the other edge of the mutual pair + view2.addNode(n1); + view2.addNode(n2); + view2.addEdge(e1); + + // Neither view should have mutual edges yet + Assert.assertEquals(view1.mutualEdgesCount, 0, "View1 should have 0 mutual edges initially"); + Assert.assertEquals(view2.mutualEdgesCount, 0, "View2 should have 0 mutual edges initially"); + + // Union should create mutual edges (mutual count is 1 for a pair) + view1.union(view2); + + Assert.assertEquals(view1.mutualEdgesCount, 1, "View1 should have mutual count of 1 after union"); + Assert.assertEquals(view1.getEdgeCount(), 2, "View1 should have 2 edges total"); + Assert.assertEquals(view1.getUndirectedEdgeCount(), 1, "View1 should have 1 undirected edge"); + } + + @Test + public void testNotWithMutualEdges() { + GraphStore graphStore = GraphGenerator.generateTinyGraphStoreWithMutualEdge(); + GraphViewStore store = graphStore.viewStore; + GraphViewImpl view = store.createView(); + + EdgeImpl e0 = graphStore.getEdge("0"); + EdgeImpl e1 = graphStore.getEdge("1"); + NodeImpl n1 = e0.getSource(); + NodeImpl n2 = e0.getTarget(); + + // Add only one edge of the mutual pair + view.addNode(n1); + view.addNode(n2); + view.addEdge(e0); + + Assert.assertEquals(view.mutualEdgesCount, 0, "View should have 0 mutual edges initially"); + Assert.assertEquals(view.getEdgeCount(), 1, "View should have 1 edge"); + Assert.assertEquals(view.getNodeCount(), 2, "View should have 2 nodes"); + + // NOT operation flips both nodes and edges + // Since there are only 2 nodes total in the graph, after NOT we have 0 nodes + // Edges without endpoints get removed, so we end up with 0 edges + view.not(); + + Assert.assertEquals(view.getNodeCount(), 0, "View should have 0 nodes after NOT (graph has 2 nodes total)"); + Assert.assertEquals(view.getEdgeCount(), 0, "View should have 0 edges after NOT (no nodes, so no edges)"); + } + + // ========== Tests for Multi-Type Edge Counts in Bulk Operations ========== + + @Test + public void testIntersectionMultipleEdgeTypes() { + GraphStore graphStore = GraphGenerator.generateSmallMultiTypeGraphStore(); + GraphViewStore store = graphStore.viewStore; + GraphViewImpl view1 = store.createView(); + GraphViewImpl view2 = store.createView(); + + // Fill both views + view1.fill(); + view2.fill(); + + // Verify initial state has multiple edge types + int type0CountInitial = view1.getEdgeCount(0); + int type1CountInitial = view1.getEdgeCount(1); + Assert.assertTrue(type0CountInitial > 0, "Should have type 0 edges"); + Assert.assertTrue(type1CountInitial > 0, "Should have type 1 edges"); + + // Remove all type 0 edges from view2 + for (Edge e : graphStore.getEdges().toArray()) { + if (e.getType() == 0) { + view2.removeEdge(e); + } + } + + Assert.assertEquals(view2.getEdgeCount(0), 0, "View2 should have 0 type 0 edges"); + Assert.assertEquals(view2.getEdgeCount(1), type1CountInitial, "View2 should still have all type 1 edges"); + + // Intersection should remove all type 0 edges from view1 + view1.intersection(view2); + + Assert.assertEquals(view1.getEdgeCount(0), 0, "View1 should have 0 type 0 edges after intersection"); + Assert.assertEquals(view1 + .getEdgeCount(1), type1CountInitial, "View1 should have all type 1 edges after intersection"); + int type2CountInitial = view1.getEdgeCount(2); + Assert.assertEquals(view1 + .getEdgeCount(), type1CountInitial + type2CountInitial, "Total edge count should match sum of type 1 and type 2"); + } + + @Test + public void testUnionMultipleEdgeTypes() { + GraphStore graphStore = GraphGenerator.generateSmallMultiTypeGraphStore(); + GraphViewStore store = graphStore.viewStore; + GraphViewImpl view1 = store.createView(); + GraphViewImpl view2 = store.createView(); + + // Add only type 0 edges to view1 + for (Node n : graphStore.getNodes()) { + view1.addNode(n); + } + for (Edge e : graphStore.getEdges().toArray()) { + if (e.getType() == 0) { + view1.addEdge(e); + } + } + + // Add only type 1 and type 2 edges to view2 + for (Node n : graphStore.getNodes()) { + view2.addNode(n); + } + for (Edge e : graphStore.getEdges().toArray()) { + if (e.getType() == 1 || e.getType() == 2) { + view2.addEdge(e); + } + } + + int type0Count = view1.getEdgeCount(0); + int type1Count = view2.getEdgeCount(1); + int type2Count = view2.getEdgeCount(2); + + Assert.assertTrue(type0Count > 0, "View1 should have type 0 edges"); + Assert.assertEquals(view2.getEdgeCount(0), 0, "View2 should have no type 0 edges"); + Assert.assertTrue(type1Count > 0, "View2 should have type 1 edges"); + Assert.assertTrue(type2Count > 0, "View2 should have type 2 edges"); + + // Union should combine both types + view1.union(view2); + + Assert.assertEquals(view1.getEdgeCount(0), type0Count, "View1 should have all type 0 edges after union"); + Assert.assertEquals(view1.getEdgeCount(1), type1Count, "View1 should have all type 1 edges after union"); + Assert.assertEquals(view1.getEdgeCount(2), type2Count, "View1 should have all type 2 edges after union"); + Assert.assertEquals(view1 + .getEdgeCount(), type0Count + type1Count + type2Count, "Total should be sum of all types"); + } + + @Test + public void testNotMultipleEdgeTypes() { + GraphStore graphStore = GraphGenerator.generateSmallMultiTypeGraphStore(); + GraphViewStore store = graphStore.viewStore; + GraphViewImpl view = store.createView(); + + // Add all nodes but only type 0 edges + for (Node n : graphStore.getNodes()) { + view.addNode(n); + } + for (Edge e : graphStore.getEdges().toArray()) { + if (e.getType() == 0) { + view.addEdge(e); + } + } + + int type0Count = view.getEdgeCount(0); + int nodeCount = view.getNodeCount(); + int totalNodesInStore = graphStore.getNodeCount(); + + Assert.assertTrue(type0Count > 0, "View should have type 0 edges"); + + // NOT operation flips both nodes and edges + // Since we have all nodes, after NOT we have 0 nodes + // All edges get removed because they have no valid endpoints + view.not(); + + Assert.assertEquals(view + .getNodeCount(), totalNodesInStore - nodeCount, "View should have inverted node count after NOT"); + Assert.assertEquals(view + .getEdgeCount(), 0, "View should have 0 edges after NOT (no nodes means no valid edges)"); + } + + // ========== Tests for Empty View Edge Cases ========== + + @Test + public void testIntersectionWithEmptyView() { + GraphStore graphStore = GraphGenerator.generateSmallGraphStore(); + GraphViewStore store = graphStore.viewStore; + GraphViewImpl view1 = store.createView(); + GraphViewImpl view2 = store.createView(); // Empty view + + // Fill view1 + view1.fill(); + int initialNodeCount = view1.getNodeCount(); + int initialEdgeCount = view1.getEdgeCount(); + + Assert.assertTrue(initialNodeCount > 0, "View1 should have nodes"); + Assert.assertTrue(initialEdgeCount > 0, "View1 should have edges"); + Assert.assertEquals(view2.getNodeCount(), 0, "View2 should be empty"); + Assert.assertEquals(view2.getEdgeCount(), 0, "View2 should be empty"); + + // Intersection with empty view should result in empty view1 + view1.intersection(view2); + + Assert.assertEquals(view1.getNodeCount(), 0, "View1 should be empty after intersection with empty view"); + Assert.assertEquals(view1.getEdgeCount(), 0, "View1 should have no edges after intersection with empty view"); + } + + @Test + public void testIntersectionOfEmptyView() { + GraphStore graphStore = GraphGenerator.generateSmallGraphStore(); + GraphViewStore store = graphStore.viewStore; + GraphViewImpl view1 = store.createView(); // Empty view + GraphViewImpl view2 = store.createView(); + + // Fill view2 + view2.fill(); + + Assert.assertEquals(view1.getNodeCount(), 0, "View1 should be empty"); + Assert.assertTrue(view2.getNodeCount() > 0, "View2 should have nodes"); + + // Intersection of empty view with filled view should stay empty + view1.intersection(view2); + + Assert.assertEquals(view1.getNodeCount(), 0, "View1 should still be empty after intersection"); + Assert.assertEquals(view1.getEdgeCount(), 0, "View1 should still have no edges after intersection"); + } + + @Test + public void testUnionWithEmptyView() { + GraphStore graphStore = GraphGenerator.generateSmallGraphStore(); + GraphViewStore store = graphStore.viewStore; + GraphViewImpl view1 = store.createView(); + GraphViewImpl view2 = store.createView(); // Empty view + + // Fill view1 + view1.fill(); + int initialNodeCount = view1.getNodeCount(); + int initialEdgeCount = view1.getEdgeCount(); + + Assert.assertTrue(initialNodeCount > 0, "View1 should have nodes"); + Assert.assertTrue(initialEdgeCount > 0, "View1 should have edges"); + Assert.assertEquals(view2.getNodeCount(), 0, "View2 should be empty"); + + // Union with empty view should not change view1 + view1.union(view2); + + Assert.assertEquals(view1.getNodeCount(), initialNodeCount, "View1 node count should not change"); + Assert.assertEquals(view1.getEdgeCount(), initialEdgeCount, "View1 edge count should not change"); + } + + @Test + public void testUnionOfEmptyView() { + GraphStore graphStore = GraphGenerator.generateSmallGraphStore(); + GraphViewStore store = graphStore.viewStore; + GraphViewImpl view1 = store.createView(); // Empty view + GraphViewImpl view2 = store.createView(); + + // Fill view2 + view2.fill(); + int view2NodeCount = view2.getNodeCount(); + int view2EdgeCount = view2.getEdgeCount(); + + Assert.assertEquals(view1.getNodeCount(), 0, "View1 should be empty"); + Assert.assertTrue(view2NodeCount > 0, "View2 should have nodes"); + + // Union of empty view with filled view should fill view1 + view1.union(view2); + + Assert.assertEquals(view1.getNodeCount(), view2NodeCount, "View1 should have same node count as view2"); + Assert.assertEquals(view1.getEdgeCount(), view2EdgeCount, "View1 should have same edge count as view2"); + } + + @Test + public void testNotOnEmptyView() { + GraphStore graphStore = GraphGenerator.generateSmallGraphStore(); + GraphViewStore store = graphStore.viewStore; + GraphViewImpl view = store.createView(); // Empty view + + int totalNodes = graphStore.getNodeCount(); + int totalEdges = graphStore.getEdgeCount(); + + Assert.assertEquals(view.getNodeCount(), 0, "View should be empty initially"); + Assert.assertEquals(view.getEdgeCount(), 0, "View should have no edges initially"); + + // NOT on empty view should fill it completely + view.not(); + + Assert.assertEquals(view.getNodeCount(), totalNodes, "View should have all nodes after NOT"); + Assert.assertEquals(view.getEdgeCount(), totalEdges, "View should have all edges after NOT"); + + // Verify all elements are present + for (Node n : graphStore.getNodes()) { + Assert.assertTrue(view.containsNode((NodeImpl) n), "View should contain all nodes after NOT"); + } + for (Edge e : graphStore.getEdges()) { + Assert.assertTrue(view.containsEdge((EdgeImpl) e), "View should contain all edges after NOT"); + } + } + + @Test + public void testIntersectionBothEmpty() { + GraphStore graphStore = GraphGenerator.generateSmallGraphStore(); + GraphViewStore store = graphStore.viewStore; + GraphViewImpl view1 = store.createView(); // Empty + GraphViewImpl view2 = store.createView(); // Empty + + // Both views empty + Assert.assertEquals(view1.getNodeCount(), 0, "View1 should be empty"); + Assert.assertEquals(view2.getNodeCount(), 0, "View2 should be empty"); + + // Intersection of two empty views should stay empty + view1.intersection(view2); + + Assert.assertEquals(view1.getNodeCount(), 0, "View1 should still be empty"); + Assert.assertEquals(view1.getEdgeCount(), 0, "View1 should still have no edges"); + } + + @Test + public void testUnionBothEmpty() { + GraphStore graphStore = GraphGenerator.generateSmallGraphStore(); + GraphViewStore store = graphStore.viewStore; + GraphViewImpl view1 = store.createView(); // Empty + GraphViewImpl view2 = store.createView(); // Empty + + // Both views empty + Assert.assertEquals(view1.getNodeCount(), 0, "View1 should be empty"); + Assert.assertEquals(view2.getNodeCount(), 0, "View2 should be empty"); + + // Union of two empty views should stay empty + view1.union(view2); + + Assert.assertEquals(view1.getNodeCount(), 0, "View1 should still be empty"); + Assert.assertEquals(view1.getEdgeCount(), 0, "View1 should still have no edges"); + } + + // ========== Tests for Retain Operations ========== + + @Test + public void testRetainNodesBasic() { + GraphStore graphStore = GraphGenerator.generateSmallGraphStore(); + GraphViewStore store = graphStore.viewStore; + GraphViewImpl view = store.createView(); + + view.fill(); + int initialNodeCount = view.getNodeCount(); + int initialEdgeCount = view.getEdgeCount(); + + // Retain all nodes - should return false (no change) + boolean changed = view.retainNodes(graphStore.getNodes().toCollection()); + Assert.assertFalse(changed, "Retaining all nodes should return false"); + Assert.assertEquals(view.getNodeCount(), initialNodeCount, "Node count should not change"); + Assert.assertEquals(view.getEdgeCount(), initialEdgeCount, "Edge count should not change"); + + // Retain subset of nodes + NodeImpl n1 = graphStore.getNode("0"); + NodeImpl n2 = graphStore.getNode("1"); + changed = view.retainNodes(Arrays.asList(n1, n2)); + + Assert.assertTrue(changed, "Retaining subset should return true"); + Assert.assertEquals(view.getNodeCount(), 2, "Should have exactly 2 nodes"); + Assert.assertTrue(view.containsNode(n1), "Should contain node 0"); + Assert.assertTrue(view.containsNode(n2), "Should contain node 1"); + } + + @Test + public void testRetainNodesEmpty() { + GraphStore graphStore = GraphGenerator.generateSmallGraphStore(); + GraphViewStore store = graphStore.viewStore; + GraphViewImpl view = store.createView(); + + view.fill(); + + // Retain empty collection - should clear everything + boolean changed = view.retainNodes(Collections.emptyList()); + + Assert.assertTrue(changed, "Retaining empty list should return true"); + Assert.assertEquals(view.getNodeCount(), 0, "Should have no nodes"); + Assert.assertEquals(view.getEdgeCount(), 0, "Should have no edges"); + } + + @Test + public void testRetainEdgesBasic() { + GraphStore graphStore = GraphGenerator.generateSmallGraphStore(); + GraphViewStore store = graphStore.viewStore; + GraphViewImpl view = store.createView(false, true); // Edge view only + + view.fill(); + int initialEdgeCount = view.getEdgeCount(); + + // Retain all edges - should return false (no change) + boolean changed = view.retainEdges(graphStore.getEdges().toCollection()); + Assert.assertFalse(changed, "Retaining all edges should return false"); + Assert.assertEquals(view.getEdgeCount(), initialEdgeCount, "Edge count should not change"); + + // Retain subset of edges + EdgeImpl e1 = graphStore.getEdge("0"); + EdgeImpl e2 = graphStore.getEdge("1"); + changed = view.retainEdges(Arrays.asList(e1, e2)); + + Assert.assertTrue(changed, "Retaining subset should return true"); + Assert.assertEquals(view.getEdgeCount(), 2, "Should have exactly 2 edges"); + Assert.assertTrue(view.containsEdge(e1), "Should contain edge 0"); + Assert.assertTrue(view.containsEdge(e2), "Should contain edge 1"); + } + + @Test + public void testRetainEdgesEmpty() { + GraphStore graphStore = GraphGenerator.generateSmallGraphStore(); + GraphViewStore store = graphStore.viewStore; + GraphViewImpl view = store.createView(false, true); // Edge view only + + view.fill(); + + // Retain empty collection - should clear all edges + boolean changed = view.retainEdges(Collections.emptyList()); + + Assert.assertTrue(changed, "Retaining empty list should return true"); + Assert.assertEquals(view.getEdgeCount(), 0, "Should have no edges"); + } + + @Test + public void testRetainNodesWithMutualEdges() { + GraphStore graphStore = GraphGenerator.generateTinyGraphStoreWithMutualEdge(); + GraphViewStore store = graphStore.viewStore; + GraphViewImpl view = store.createView(); + + view.fill(); + + EdgeImpl e0 = graphStore.getEdge("0"); + EdgeImpl e1 = graphStore.getEdge("1"); + NodeImpl n1 = e0.getSource(); + NodeImpl n2 = e0.getTarget(); + + // Initial state: view has mutual edges + Assert.assertEquals(view.mutualEdgesCount, 1, "View should have mutual count of 1"); + Assert.assertEquals(view.getEdgeCount(), 2, "View should have 2 edges"); + + // Retain both nodes - mutual edges should remain + boolean changed = view.retainNodes(Arrays.asList(n1, n2)); + + Assert.assertFalse(changed, "Retaining all nodes should return false"); + Assert.assertEquals(view.mutualEdgesCount, 1, "Mutual edges should remain"); + Assert.assertEquals(view.getEdgeCount(), 2, "Should still have 2 edges"); + } + + @Test + public void testRetainEdgesWithMultipleTypes() { + GraphStore graphStore = GraphGenerator.generateSmallMultiTypeGraphStore(); + GraphViewStore store = graphStore.viewStore; + GraphViewImpl view = store.createView(false, true); // Edge view only + + view.fill(); + + int type0Count = view.getEdgeCount(0); + int type1Count = view.getEdgeCount(1); + int type2Count = view.getEdgeCount(2); + + Assert.assertTrue(type0Count > 0, "Should have type 0 edges"); + Assert.assertTrue(type1Count > 0, "Should have type 1 edges"); + Assert.assertTrue(type2Count > 0, "Should have type 2 edges"); + + // Collect only type 0 edges to retain + List type0Edges = new ArrayList<>(); + for (Edge e : graphStore.getEdges().toArray()) { + if (e.getType() == 0) { + type0Edges.add(e); + } + } + + // Retain only type 0 edges + boolean changed = view.retainEdges(type0Edges); + + Assert.assertTrue(changed, "Should have removed edges"); + Assert.assertEquals(view.getEdgeCount(0), type0Count, "Should still have all type 0 edges"); + Assert.assertEquals(view.getEdgeCount(1), 0, "Should have no type 1 edges"); + Assert.assertEquals(view.getEdgeCount(2), 0, "Should have no type 2 edges"); + Assert.assertEquals(view.getEdgeCount(), type0Count, "Total should match type 0 count"); + } + + @Test + public void testRetainNodesWithMultipleEdgeTypes() { + GraphStore graphStore = GraphGenerator.generateSmallMultiTypeGraphStore(); + GraphViewStore store = graphStore.viewStore; + GraphViewImpl view = store.createView(); + + view.fill(); + + int initialType0Count = view.getEdgeCount(0); + int initialType1Count = view.getEdgeCount(1); + + // Retain subset of nodes + List nodesToRetain = new ArrayList<>(); + int count = 0; + for (Node n : graphStore.getNodes()) { + nodesToRetain.add(n); + count++; + if (count >= 5) { + break; // Keep first 5 nodes + } + } + + boolean changed = view.retainNodes(nodesToRetain); + + Assert.assertTrue(changed, "Should have removed nodes"); + Assert.assertEquals(view.getNodeCount(), 5, "Should have exactly 5 nodes"); + + // Edge counts should have decreased but type tracking should still be correct + int newType0Count = view.getEdgeCount(0); + int newType1Count = view.getEdgeCount(1); + + Assert.assertTrue(newType0Count <= initialType0Count, "Type 0 count should not increase"); + Assert.assertTrue(newType1Count <= initialType1Count, "Type 1 count should not increase"); + Assert.assertEquals(view.getEdgeCount(), newType0Count + newType1Count + view + .getEdgeCount(2), "Total edge count should match sum of types"); + } + + @Test + public void testRetainNodesLargeScale() { + // Test bulk operation performance with larger dataset + GraphStore graphStore = GraphGenerator.generateSmallGraphStore(); + GraphViewStore store = graphStore.viewStore; + GraphViewImpl view = store.createView(); + + view.fill(); + int totalNodes = view.getNodeCount(); + + // Retain half the nodes + List nodesToRetain = new ArrayList<>(); + int count = 0; + for (Node n : graphStore.getNodes()) { + if (count % 2 == 0) { + nodesToRetain.add(n); + } + count++; + } + + boolean changed = view.retainNodes(nodesToRetain); + + Assert.assertTrue(changed, "Should have removed nodes"); + Assert.assertTrue(view.getNodeCount() <= totalNodes / 2 + 1, "Should have roughly half the nodes"); + Assert.assertTrue(view.getNodeCount() >= totalNodes / 2 - 1, "Should have roughly half the nodes"); + + // Verify all retained nodes are in the view + for (Node n : nodesToRetain) { + Assert.assertTrue(view.containsNode((NodeImpl) n), "Retained node should be in view"); + } + } + + @Test + public void testRetainEdgesOnlyView() { + // Test retain edges when nodeView=false, edgeView=true + GraphStore graphStore = GraphGenerator.generateSmallGraphStore(); + GraphViewStore store = graphStore.viewStore; + GraphViewImpl view = store.createView(false, true); + + view.fill(); + + List edgesToRetain = new ArrayList<>(); + int count = 0; + for (Edge e : graphStore.getEdges().toArray()) { + edgesToRetain.add(e); + count++; + if (count >= 10) { + break; + } + } + + boolean changed = view.retainEdges(edgesToRetain); + + Assert.assertTrue(changed, "Should have removed edges"); + Assert.assertEquals(view.getEdgeCount(), 10, "Should have exactly 10 edges"); + + for (Edge e : edgesToRetain) { + Assert.assertTrue(view.containsEdge((EdgeImpl) e), "Retained edge should be in view"); + } + } } diff --git a/src/test/java/org/gephi/graph/impl/SerializationTest.java b/src/test/java/org/gephi/graph/impl/SerializationTest.java index c56c346f..561a28c6 100644 --- a/src/test/java/org/gephi/graph/impl/SerializationTest.java +++ b/src/test/java/org/gephi/graph/impl/SerializationTest.java @@ -15,7 +15,6 @@ */ package org.gephi.graph.impl; -import cern.colt.bitvector.BitVector; import it.unimi.dsi.fastutil.booleans.BooleanArrayList; import it.unimi.dsi.fastutil.booleans.BooleanOpenHashSet; import it.unimi.dsi.fastutil.bytes.Byte2ObjectOpenHashMap; @@ -49,12 +48,14 @@ import java.time.ZoneId; import java.util.ArrayList; import java.util.Arrays; +import java.util.BitSet; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Locale; import java.util.Map; +import java.util.Random; import java.util.Set; import org.gephi.graph.api.Configuration; import org.gephi.graph.api.GraphModel; @@ -315,14 +316,14 @@ public void testGraphView() throws IOException, ClassNotFoundException { } @Test - public void testBitVector() throws IOException, ClassNotFoundException { - BitVector bitVector = new BitVector(10); + public void testBitSet() throws IOException, ClassNotFoundException { + BitSet bitVector = new BitSet(10); bitVector.set(1); bitVector.set(4); Serialization ser = new Serialization(null); byte[] buf = ser.serialize(bitVector); - BitVector l = (BitVector) ser.deserialize(buf); + BitSet l = (BitSet) ser.deserialize(buf); Assert.assertEquals(bitVector, l); } @@ -1266,4 +1267,27 @@ public void serializeGraphModel(DataOutput out, GraphModelImpl model) throws IOE GraphModelImpl read = ser.deserializeGraphModelWithoutVersionPrefix(dio.reset(bytes), Serialization.VERSION); Assert.assertTrue(read.deepEquals(gm)); } + + @Test + public void testBitVectorEqual() throws Exception { + Serialization ser = new Serialization(); + + Random random = new Random(); + for (int i = 0; i < 20000; i += 97) { + BitSet bs = new BitSet(i); + + int p = random.nextInt(99) + 1; + for (int j = 0; j < i; j++) { + if (random.nextInt(100) < p) { + bs.set(j); + } + } + DataInputOutput dio = new DataInputOutput(); + ser.serializeBitSet(dio, bs); + byte[] bytes = dio.toByteArray(); + + BitSet deserializedBs = ser.deserializeBitSet(dio.reset(bytes)); + Assert.assertEquals(bs, deserializedBs); + } + } }