diff --git a/Gvisual/src/gvisual/GraphNeighborhoodAnalyzer.java b/Gvisual/src/gvisual/GraphNeighborhoodAnalyzer.java
new file mode 100644
index 0000000..9d313e6
--- /dev/null
+++ b/Gvisual/src/gvisual/GraphNeighborhoodAnalyzer.java
@@ -0,0 +1,435 @@
+package gvisual;
+
+import edu.uci.ics.jung.graph.Graph;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * Analyses k-hop neighborhoods for vertices in a graph, providing local
+ * structural insights that complement global metrics.
+ *
+ *
Capabilities
+ *
+ * - k-hop neighborhood — the set of vertices reachable within k
+ * steps from a source vertex (BFS layers)
+ * - Growth profile — how the reachable set size grows at each
+ * hop depth, revealing local topology (tree-like vs mesh-like)
+ * - Neighborhood overlap — Jaccard similarity of k-hop
+ * neighborhoods between two vertices, measuring structural proximity
+ * - Local density — edge density within the k-hop induced
+ * subgraph, indicating how tightly connected a neighbourhood is
+ * - Boundary vertices — vertices exactly k hops away (the
+ * frontier), useful for expansion analysis
+ * - Expansion rate — ratio of boundary size to interior size at
+ * each hop, quantifying how quickly influence spreads
+ * - Aggregate statistics — mean/min/max neighborhood sizes across
+ * all vertices for a given depth k
+ * - Text report — human-readable neighborhood summary
+ *
+ *
+ * All computations use BFS from the source vertex. For directed graphs
+ * the successor (out-edge) direction is followed. The source vertex itself
+ * is always at depth 0.
+ *
+ * @author zalenix
+ */
+public class GraphNeighborhoodAnalyzer {
+
+ private final Graph graph;
+
+ /**
+ * Constructs an analyser for the given graph.
+ *
+ * @param graph the graph to analyse (must not be null)
+ * @throws IllegalArgumentException if graph is null
+ */
+ public GraphNeighborhoodAnalyzer(Graph graph) {
+ if (graph == null) {
+ throw new IllegalArgumentException("Graph must not be null");
+ }
+ this.graph = graph;
+ }
+
+ // ------------------------------------------------------------------
+ // Core: BFS layer computation
+ // ------------------------------------------------------------------
+
+ /**
+ * Computes BFS layers from a source vertex up to depth {@code maxK}.
+ * Layer 0 contains only the source. Layer i contains vertices at
+ * exactly distance i from the source.
+ *
+ * @param source the starting vertex
+ * @param maxK maximum depth (must be ≥ 0)
+ * @return list of layers, where index i is the set of vertices at depth i
+ * @throws IllegalArgumentException if source is not in the graph or maxK < 0
+ */
+ public List> computeLayers(String source, int maxK) {
+ validateVertex(source);
+ if (maxK < 0) {
+ throw new IllegalArgumentException("maxK must be >= 0, got " + maxK);
+ }
+
+ List> layers = new ArrayList<>();
+ Set visited = new HashSet<>();
+ visited.add(source);
+ layers.add(Collections.singleton(source));
+
+ for (int depth = 1; depth <= maxK; depth++) {
+ Set frontier = new LinkedHashSet<>();
+ for (String v : layers.get(depth - 1)) {
+ for (String neighbor : graph.getNeighbors(v)) {
+ if (!visited.contains(neighbor)) {
+ frontier.add(neighbor);
+ visited.add(neighbor);
+ }
+ }
+ }
+ if (frontier.isEmpty()) {
+ break; // no more vertices to reach
+ }
+ layers.add(Collections.unmodifiableSet(frontier));
+ }
+ return layers;
+ }
+
+ // ------------------------------------------------------------------
+ // k-hop neighborhood
+ // ------------------------------------------------------------------
+
+ /**
+ * Returns the set of vertices within k hops of the source (inclusive).
+ *
+ * @param source starting vertex
+ * @param k hop distance (≥ 0)
+ * @return unmodifiable set of vertices within k hops
+ */
+ public Set getKHopNeighborhood(String source, int k) {
+ List> layers = computeLayers(source, k);
+ Set result = new LinkedHashSet<>();
+ for (Set layer : layers) {
+ result.addAll(layer);
+ }
+ return Collections.unmodifiableSet(result);
+ }
+
+ /**
+ * Returns boundary vertices — those at exactly distance k from source.
+ * If k exceeds the eccentricity, an empty set is returned.
+ *
+ * @param source starting vertex
+ * @param k exact hop distance
+ * @return unmodifiable set of boundary vertices
+ */
+ public Set getBoundary(String source, int k) {
+ List> layers = computeLayers(source, k);
+ if (k < layers.size()) {
+ return layers.get(k);
+ }
+ return Collections.emptySet();
+ }
+
+ // ------------------------------------------------------------------
+ // Growth profile
+ // ------------------------------------------------------------------
+
+ /**
+ * Computes the cumulative neighborhood size at each hop depth.
+ * Index 0 is always 1 (the source). The list has at most maxK+1 entries,
+ * fewer if the graph is exhausted before maxK.
+ *
+ * @param source starting vertex
+ * @param maxK maximum depth
+ * @return list of cumulative sizes at each depth
+ */
+ public List getGrowthProfile(String source, int maxK) {
+ List> layers = computeLayers(source, maxK);
+ List profile = new ArrayList<>();
+ int cumulative = 0;
+ for (Set layer : layers) {
+ cumulative += layer.size();
+ profile.add(cumulative);
+ }
+ return profile;
+ }
+
+ /**
+ * Computes the expansion rate at each hop depth.
+ * Expansion at depth d = |layer d| / |cumulative up to d-1|.
+ * Depth 0 has no meaningful expansion (returned as 0.0).
+ *
+ * @param source starting vertex
+ * @param maxK maximum depth
+ * @return list of expansion rates (length = number of layers computed)
+ */
+ public List getExpansionRates(String source, int maxK) {
+ List> layers = computeLayers(source, maxK);
+ List rates = new ArrayList<>();
+ int cumulative = 0;
+ for (int i = 0; i < layers.size(); i++) {
+ if (i == 0) {
+ rates.add(0.0);
+ cumulative += layers.get(i).size();
+ } else {
+ int boundary = layers.get(i).size();
+ double rate = cumulative > 0 ? (double) boundary / cumulative : 0.0;
+ rates.add(rate);
+ cumulative += boundary;
+ }
+ }
+ return rates;
+ }
+
+ // ------------------------------------------------------------------
+ // Local density
+ // ------------------------------------------------------------------
+
+ /**
+ * Computes the edge density of the induced subgraph on the k-hop
+ * neighborhood of the source vertex.
+ *
+ * Density = 2 * |edges in subgraph| / (n * (n-1)) for n > 1,
+ * where n is the neighborhood size. Returns 1.0 for a single vertex
+ * and 0.0 for an empty graph.
+ *
+ * @param source starting vertex
+ * @param k hop distance
+ * @return edge density in [0.0, 1.0]
+ */
+ public double getLocalDensity(String source, int k) {
+ Set neighborhood = getKHopNeighborhood(source, k);
+ int n = neighborhood.size();
+ if (n <= 1) {
+ return n == 1 ? 1.0 : 0.0;
+ }
+
+ int edgeCount = 0;
+ for (edge e : graph.getEdges()) {
+ String src = graph.getEndpoints(e).getFirst();
+ String dst = graph.getEndpoints(e).getSecond();
+ if (neighborhood.contains(src) && neighborhood.contains(dst)) {
+ edgeCount++;
+ }
+ }
+
+ double maxEdges = (double) n * (n - 1) / 2.0;
+ return edgeCount / maxEdges;
+ }
+
+ // ------------------------------------------------------------------
+ // Overlap / Similarity
+ // ------------------------------------------------------------------
+
+ /**
+ * Computes the Jaccard similarity of the k-hop neighborhoods of two
+ * vertices: |A ∩ B| / |A ∪ B|.
+ *
+ * @param v1 first vertex
+ * @param v2 second vertex
+ * @param k hop distance
+ * @return Jaccard similarity in [0.0, 1.0]
+ */
+ public double getNeighborhoodOverlap(String v1, String v2, int k) {
+ Set n1 = getKHopNeighborhood(v1, k);
+ Set n2 = getKHopNeighborhood(v2, k);
+
+ Set intersection = new HashSet<>(n1);
+ intersection.retainAll(n2);
+
+ Set union = new HashSet<>(n1);
+ union.addAll(n2);
+
+ if (union.isEmpty()) {
+ return 1.0; // both empty → identical
+ }
+ return (double) intersection.size() / union.size();
+ }
+
+ /**
+ * Computes the overlap coefficient of the k-hop neighborhoods:
+ * |A ∩ B| / min(|A|, |B|). More resilient to size differences
+ * than Jaccard.
+ *
+ * @param v1 first vertex
+ * @param v2 second vertex
+ * @param k hop distance
+ * @return overlap coefficient in [0.0, 1.0]
+ */
+ public double getOverlapCoefficient(String v1, String v2, int k) {
+ Set n1 = getKHopNeighborhood(v1, k);
+ Set n2 = getKHopNeighborhood(v2, k);
+
+ Set intersection = new HashSet<>(n1);
+ intersection.retainAll(n2);
+
+ int minSize = Math.min(n1.size(), n2.size());
+ if (minSize == 0) {
+ return n1.isEmpty() && n2.isEmpty() ? 1.0 : 0.0;
+ }
+ return (double) intersection.size() / minSize;
+ }
+
+ // ------------------------------------------------------------------
+ // Aggregate statistics
+ // ------------------------------------------------------------------
+
+ /**
+ * Result of aggregate neighborhood statistics across all vertices.
+ */
+ public static class AggregateStats {
+ private final int k;
+ private final double meanSize;
+ private final int minSize;
+ private final int maxSize;
+ private final double meanDensity;
+ private final String minVertex;
+ private final String maxVertex;
+
+ public AggregateStats(int k, double meanSize, int minSize, int maxSize,
+ double meanDensity, String minVertex, String maxVertex) {
+ this.k = k;
+ this.meanSize = meanSize;
+ this.minSize = minSize;
+ this.maxSize = maxSize;
+ this.meanDensity = meanDensity;
+ this.minVertex = minVertex;
+ this.maxVertex = maxVertex;
+ }
+
+ public int getK() { return k; }
+ public double getMeanSize() { return meanSize; }
+ public int getMinSize() { return minSize; }
+ public int getMaxSize() { return maxSize; }
+ public double getMeanDensity() { return meanDensity; }
+ public String getMinVertex() { return minVertex; }
+ public String getMaxVertex() { return maxVertex; }
+ }
+
+ /**
+ * Computes aggregate k-hop neighborhood statistics across all vertices.
+ *
+ * @param k hop distance
+ * @return aggregate statistics
+ * @throws IllegalArgumentException if k < 0 or graph is empty
+ */
+ public AggregateStats getAggregateStats(int k) {
+ if (k < 0) {
+ throw new IllegalArgumentException("k must be >= 0, got " + k);
+ }
+ Collection vertices = graph.getVertices();
+ if (vertices.isEmpty()) {
+ throw new IllegalArgumentException("Graph has no vertices");
+ }
+
+ int minSize = Integer.MAX_VALUE;
+ int maxSize = Integer.MIN_VALUE;
+ double totalSize = 0;
+ double totalDensity = 0;
+ String minVertex = null;
+ String maxVertex = null;
+
+ for (String v : vertices) {
+ Set hood = getKHopNeighborhood(v, k);
+ int size = hood.size();
+ double density = getLocalDensity(v, k);
+
+ totalSize += size;
+ totalDensity += density;
+
+ if (size < minSize) {
+ minSize = size;
+ minVertex = v;
+ }
+ if (size > maxSize) {
+ maxSize = size;
+ maxVertex = v;
+ }
+ }
+
+ int n = vertices.size();
+ return new AggregateStats(k,
+ totalSize / n, minSize, maxSize,
+ totalDensity / n, minVertex, maxVertex);
+ }
+
+ // ------------------------------------------------------------------
+ // Vertex ranking
+ // ------------------------------------------------------------------
+
+ /**
+ * Ranks all vertices by their k-hop neighborhood size (descending).
+ * Useful for identifying vertices with the widest local reach.
+ *
+ * @param k hop distance
+ * @return list of (vertex, size) pairs sorted by size descending
+ */
+ public List> rankByNeighborhoodSize(int k) {
+ if (k < 0) {
+ throw new IllegalArgumentException("k must be >= 0, got " + k);
+ }
+ Map sizes = new LinkedHashMap<>();
+ for (String v : graph.getVertices()) {
+ sizes.put(v, getKHopNeighborhood(v, k).size());
+ }
+ return sizes.entrySet().stream()
+ .sorted(Map.Entry.comparingByValue().reversed())
+ .collect(Collectors.toList());
+ }
+
+ // ------------------------------------------------------------------
+ // Report
+ // ------------------------------------------------------------------
+
+ /**
+ * Generates a human-readable text report of neighborhood analysis.
+ *
+ * @param source vertex to profile (use null for aggregate-only report)
+ * @param maxK maximum hop depth to report
+ * @return formatted text report
+ */
+ public String getTextReport(String source, int maxK) {
+ StringBuilder sb = new StringBuilder();
+ sb.append("=== Neighborhood Analysis Report ===\n");
+ sb.append(String.format("Graph: %d vertices, %d edges\n",
+ graph.getVertexCount(), graph.getEdgeCount()));
+ sb.append(String.format("Max depth: %d\n\n", maxK));
+
+ if (source != null && graph.containsVertex(source)) {
+ sb.append(String.format("--- Vertex '%s' ---\n", source));
+ List> layers = computeLayers(source, maxK);
+ List expansion = getExpansionRates(source, maxK);
+ int cumulative = 0;
+ for (int d = 0; d < layers.size(); d++) {
+ cumulative += layers.get(d).size();
+ String expStr = d == 0 ? "n/a" : String.format("%.3f", expansion.get(d));
+ sb.append(String.format(" Depth %d: layer=%d cumulative=%d expansion=%s\n",
+ d, layers.get(d).size(), cumulative, expStr));
+ }
+ sb.append(String.format(" Local density (k=%d): %.4f\n", maxK,
+ getLocalDensity(source, maxK)));
+ sb.append("\n");
+ }
+
+ sb.append("--- Aggregate Statistics ---\n");
+ for (int k = 1; k <= Math.min(maxK, 3); k++) {
+ if (graph.getVertexCount() == 0) break;
+ AggregateStats stats = getAggregateStats(k);
+ sb.append(String.format(" k=%d: mean=%.1f min=%d (%s) max=%d (%s) density=%.4f\n",
+ k, stats.getMeanSize(), stats.getMinSize(), stats.getMinVertex(),
+ stats.getMaxSize(), stats.getMaxVertex(), stats.getMeanDensity()));
+ }
+
+ return sb.toString();
+ }
+
+ // ------------------------------------------------------------------
+ // Helpers
+ // ------------------------------------------------------------------
+
+ private void validateVertex(String vertex) {
+ if (vertex == null || !graph.containsVertex(vertex)) {
+ throw new IllegalArgumentException(
+ "Vertex not found in graph: " + vertex);
+ }
+ }
+}
diff --git a/Gvisual/test/gvisual/GraphNeighborhoodAnalyzerTest.java b/Gvisual/test/gvisual/GraphNeighborhoodAnalyzerTest.java
new file mode 100644
index 0000000..2bc66d2
--- /dev/null
+++ b/Gvisual/test/gvisual/GraphNeighborhoodAnalyzerTest.java
@@ -0,0 +1,566 @@
+package gvisual;
+
+import edu.uci.ics.jung.graph.Graph;
+import edu.uci.ics.jung.graph.UndirectedSparseGraph;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.*;
+
+import static org.junit.Assert.*;
+
+/**
+ * Unit tests for {@link GraphNeighborhoodAnalyzer}.
+ */
+public class GraphNeighborhoodAnalyzerTest {
+
+ private Graph graph;
+
+ @Before
+ public void setUp() {
+ graph = new UndirectedSparseGraph<>();
+ }
+
+ private edge addEdge(String v1, String v2) {
+ edge e = new edge("f", v1, v2);
+ e.setWeight(1.0f);
+ if (!graph.containsVertex(v1)) graph.addVertex(v1);
+ if (!graph.containsVertex(v2)) graph.addVertex(v2);
+ graph.addEdge(e, v1, v2);
+ return e;
+ }
+
+ private void buildPath(int n) {
+ for (int i = 0; i < n; i++) graph.addVertex(String.valueOf(i));
+ for (int i = 0; i < n - 1; i++)
+ addEdge(String.valueOf(i), String.valueOf(i + 1));
+ }
+
+ private void buildComplete(int n) {
+ for (int i = 0; i < n; i++) graph.addVertex(String.valueOf(i));
+ for (int i = 0; i < n; i++)
+ for (int j = i + 1; j < n; j++)
+ addEdge(String.valueOf(i), String.valueOf(j));
+ }
+
+ private void buildStar(int n) {
+ graph.addVertex("c");
+ for (int i = 0; i < n; i++) {
+ graph.addVertex(String.valueOf(i));
+ addEdge("c", String.valueOf(i));
+ }
+ }
+
+ private void buildCycle(int n) {
+ for (int i = 0; i < n; i++) graph.addVertex(String.valueOf(i));
+ for (int i = 0; i < n; i++)
+ addEdge(String.valueOf(i), String.valueOf((i + 1) % n));
+ }
+
+ // ------------------------------------------------------------------
+ // Constructor validation
+ // ------------------------------------------------------------------
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testNullGraph() {
+ new GraphNeighborhoodAnalyzer(null);
+ }
+
+ // ------------------------------------------------------------------
+ // computeLayers
+ // ------------------------------------------------------------------
+
+ @Test
+ public void testLayersSingleVertex() {
+ graph.addVertex("A");
+ GraphNeighborhoodAnalyzer analyzer = new GraphNeighborhoodAnalyzer(graph);
+ List> layers = analyzer.computeLayers("A", 3);
+ assertEquals(1, layers.size());
+ assertTrue(layers.get(0).contains("A"));
+ }
+
+ @Test
+ public void testLayersPath5() {
+ buildPath(5); // 0-1-2-3-4
+ GraphNeighborhoodAnalyzer analyzer = new GraphNeighborhoodAnalyzer(graph);
+ List> layers = analyzer.computeLayers("0", 10);
+ assertEquals(5, layers.size());
+ assertEquals(Collections.singleton("0"), layers.get(0));
+ assertEquals(Collections.singleton("1"), layers.get(1));
+ assertEquals(Collections.singleton("2"), layers.get(2));
+ assertEquals(Collections.singleton("3"), layers.get(3));
+ assertEquals(Collections.singleton("4"), layers.get(4));
+ }
+
+ @Test
+ public void testLayersPath5FromMiddle() {
+ buildPath(5); // 0-1-2-3-4
+ GraphNeighborhoodAnalyzer analyzer = new GraphNeighborhoodAnalyzer(graph);
+ List> layers = analyzer.computeLayers("2", 10);
+ assertEquals(3, layers.size()); // depth 0, 1, 2
+ assertEquals(Collections.singleton("2"), layers.get(0));
+ assertTrue(layers.get(1).contains("1"));
+ assertTrue(layers.get(1).contains("3"));
+ assertEquals(2, layers.get(1).size());
+ assertTrue(layers.get(2).contains("0"));
+ assertTrue(layers.get(2).contains("4"));
+ }
+
+ @Test
+ public void testLayersCompleteGraph() {
+ buildComplete(5);
+ GraphNeighborhoodAnalyzer analyzer = new GraphNeighborhoodAnalyzer(graph);
+ List> layers = analyzer.computeLayers("0", 5);
+ assertEquals(2, layers.size()); // depth 0 and 1
+ assertEquals(1, layers.get(0).size());
+ assertEquals(4, layers.get(1).size());
+ }
+
+ @Test
+ public void testLayersMaxKZero() {
+ buildPath(3);
+ GraphNeighborhoodAnalyzer analyzer = new GraphNeighborhoodAnalyzer(graph);
+ List> layers = analyzer.computeLayers("0", 0);
+ assertEquals(1, layers.size());
+ assertEquals(Collections.singleton("0"), layers.get(0));
+ }
+
+ @Test
+ public void testLayersMaxKLimitsDepth() {
+ buildPath(5); // 0-1-2-3-4
+ GraphNeighborhoodAnalyzer analyzer = new GraphNeighborhoodAnalyzer(graph);
+ List> layers = analyzer.computeLayers("0", 2);
+ assertEquals(3, layers.size()); // 0, 1, 2 only
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testLayersInvalidVertex() {
+ buildPath(3);
+ GraphNeighborhoodAnalyzer analyzer = new GraphNeighborhoodAnalyzer(graph);
+ analyzer.computeLayers("Z", 2);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testLayersNegativeK() {
+ buildPath(3);
+ GraphNeighborhoodAnalyzer analyzer = new GraphNeighborhoodAnalyzer(graph);
+ analyzer.computeLayers("0", -1);
+ }
+
+ // ------------------------------------------------------------------
+ // getKHopNeighborhood
+ // ------------------------------------------------------------------
+
+ @Test
+ public void testKHopPath() {
+ buildPath(5); // 0-1-2-3-4
+ GraphNeighborhoodAnalyzer analyzer = new GraphNeighborhoodAnalyzer(graph);
+ Set hood = analyzer.getKHopNeighborhood("0", 2);
+ assertEquals(3, hood.size());
+ assertTrue(hood.containsAll(Arrays.asList("0", "1", "2")));
+ }
+
+ @Test
+ public void testKHopComplete() {
+ buildComplete(4);
+ GraphNeighborhoodAnalyzer analyzer = new GraphNeighborhoodAnalyzer(graph);
+ Set hood = analyzer.getKHopNeighborhood("0", 1);
+ assertEquals(4, hood.size()); // all vertices within 1 hop
+ }
+
+ @Test
+ public void testKHopDisconnected() {
+ graph.addVertex("A");
+ graph.addVertex("B");
+ graph.addVertex("C");
+ addEdge("A", "B");
+ // C is disconnected
+ GraphNeighborhoodAnalyzer analyzer = new GraphNeighborhoodAnalyzer(graph);
+ Set hood = analyzer.getKHopNeighborhood("A", 5);
+ assertEquals(2, hood.size());
+ assertTrue(hood.contains("A"));
+ assertTrue(hood.contains("B"));
+ assertFalse(hood.contains("C"));
+ }
+
+ @Test
+ public void testKHopZero() {
+ buildPath(3);
+ GraphNeighborhoodAnalyzer analyzer = new GraphNeighborhoodAnalyzer(graph);
+ Set hood = analyzer.getKHopNeighborhood("1", 0);
+ assertEquals(1, hood.size());
+ assertTrue(hood.contains("1"));
+ }
+
+ // ------------------------------------------------------------------
+ // getBoundary
+ // ------------------------------------------------------------------
+
+ @Test
+ public void testBoundaryPath() {
+ buildPath(5); // 0-1-2-3-4
+ GraphNeighborhoodAnalyzer analyzer = new GraphNeighborhoodAnalyzer(graph);
+ Set boundary = analyzer.getBoundary("0", 3);
+ assertEquals(1, boundary.size());
+ assertTrue(boundary.contains("3"));
+ }
+
+ @Test
+ public void testBoundaryStar() {
+ buildStar(4); // c-0, c-1, c-2, c-3
+ GraphNeighborhoodAnalyzer analyzer = new GraphNeighborhoodAnalyzer(graph);
+ Set boundary = analyzer.getBoundary("c", 1);
+ assertEquals(4, boundary.size());
+ }
+
+ @Test
+ public void testBoundaryBeyondEccentricity() {
+ buildPath(3); // 0-1-2
+ GraphNeighborhoodAnalyzer analyzer = new GraphNeighborhoodAnalyzer(graph);
+ Set boundary = analyzer.getBoundary("0", 10);
+ assertTrue(boundary.isEmpty());
+ }
+
+ // ------------------------------------------------------------------
+ // getGrowthProfile
+ // ------------------------------------------------------------------
+
+ @Test
+ public void testGrowthProfilePath() {
+ buildPath(5); // 0-1-2-3-4
+ GraphNeighborhoodAnalyzer analyzer = new GraphNeighborhoodAnalyzer(graph);
+ List profile = analyzer.getGrowthProfile("0", 10);
+ // Each hop adds 1 vertex: [1, 2, 3, 4, 5]
+ assertEquals(Arrays.asList(1, 2, 3, 4, 5), profile);
+ }
+
+ @Test
+ public void testGrowthProfileStar() {
+ buildStar(5);
+ GraphNeighborhoodAnalyzer analyzer = new GraphNeighborhoodAnalyzer(graph);
+ List profile = analyzer.getGrowthProfile("c", 3);
+ // Depth 0: {c} = 1, Depth 1: {0..4} = 6
+ assertEquals(Arrays.asList(1, 6), profile);
+ }
+
+ @Test
+ public void testGrowthProfileComplete() {
+ buildComplete(5);
+ GraphNeighborhoodAnalyzer analyzer = new GraphNeighborhoodAnalyzer(graph);
+ List profile = analyzer.getGrowthProfile("0", 5);
+ assertEquals(Arrays.asList(1, 5), profile);
+ }
+
+ // ------------------------------------------------------------------
+ // getExpansionRates
+ // ------------------------------------------------------------------
+
+ @Test
+ public void testExpansionRatesPath() {
+ buildPath(4); // 0-1-2-3
+ GraphNeighborhoodAnalyzer analyzer = new GraphNeighborhoodAnalyzer(graph);
+ List rates = analyzer.getExpansionRates("0", 10);
+ assertEquals(4, rates.size());
+ assertEquals(0.0, rates.get(0), 0.001); // depth 0: no expansion
+ assertEquals(1.0, rates.get(1), 0.001); // depth 1: 1 new / 1 prior
+ assertEquals(0.5, rates.get(2), 0.001); // depth 2: 1 new / 2 prior
+ assertEquals(1.0 / 3, rates.get(3), 0.001); // depth 3: 1 new / 3 prior
+ }
+
+ @Test
+ public void testExpansionRatesStar() {
+ buildStar(4);
+ GraphNeighborhoodAnalyzer analyzer = new GraphNeighborhoodAnalyzer(graph);
+ List rates = analyzer.getExpansionRates("c", 3);
+ assertEquals(2, rates.size());
+ assertEquals(0.0, rates.get(0), 0.001);
+ assertEquals(4.0, rates.get(1), 0.001); // 4 new / 1 prior
+ }
+
+ // ------------------------------------------------------------------
+ // getLocalDensity
+ // ------------------------------------------------------------------
+
+ @Test
+ public void testLocalDensityComplete() {
+ buildComplete(4);
+ GraphNeighborhoodAnalyzer analyzer = new GraphNeighborhoodAnalyzer(graph);
+ double density = analyzer.getLocalDensity("0", 1);
+ assertEquals(1.0, density, 0.001); // K4 is fully connected
+ }
+
+ @Test
+ public void testLocalDensityPath() {
+ buildPath(5); // 0-1-2-3-4
+ GraphNeighborhoodAnalyzer analyzer = new GraphNeighborhoodAnalyzer(graph);
+ // k=1 from vertex 2: neighborhood = {1,2,3}, edges = 1-2, 2-3 = 2 edges
+ // density = 2*2 / (3*2) = 0.667
+ double density = analyzer.getLocalDensity("2", 1);
+ assertEquals(2.0 / 3, density, 0.001);
+ }
+
+ @Test
+ public void testLocalDensitySingleVertex() {
+ graph.addVertex("A");
+ GraphNeighborhoodAnalyzer analyzer = new GraphNeighborhoodAnalyzer(graph);
+ assertEquals(1.0, analyzer.getLocalDensity("A", 0), 0.001);
+ }
+
+ @Test
+ public void testLocalDensityStar() {
+ buildStar(3); // c connected to 0, 1, 2
+ GraphNeighborhoodAnalyzer analyzer = new GraphNeighborhoodAnalyzer(graph);
+ // k=1 from c: neighborhood = {c,0,1,2}, 3 edges, max = 4*3/2 = 6
+ double density = analyzer.getLocalDensity("c", 1);
+ assertEquals(3.0 / 6, density, 0.001);
+ }
+
+ // ------------------------------------------------------------------
+ // getNeighborhoodOverlap
+ // ------------------------------------------------------------------
+
+ @Test
+ public void testOverlapSameVertex() {
+ buildPath(5);
+ GraphNeighborhoodAnalyzer analyzer = new GraphNeighborhoodAnalyzer(graph);
+ assertEquals(1.0, analyzer.getNeighborhoodOverlap("0", "0", 2), 0.001);
+ }
+
+ @Test
+ public void testOverlapAdjacentPath() {
+ buildPath(5); // 0-1-2-3-4
+ GraphNeighborhoodAnalyzer analyzer = new GraphNeighborhoodAnalyzer(graph);
+ // k=1: N(0)={0,1}, N(1)={0,1,2}
+ // intersection = {0,1}, union = {0,1,2}
+ double overlap = analyzer.getNeighborhoodOverlap("0", "1", 1);
+ assertEquals(2.0 / 3, overlap, 0.001);
+ }
+
+ @Test
+ public void testOverlapDisconnected() {
+ graph.addVertex("A");
+ graph.addVertex("B");
+ GraphNeighborhoodAnalyzer analyzer = new GraphNeighborhoodAnalyzer(graph);
+ // N(A)={A}, N(B)={B}, intersection empty, union = {A,B}
+ assertEquals(0.0, analyzer.getNeighborhoodOverlap("A", "B", 1), 0.001);
+ }
+
+ // ------------------------------------------------------------------
+ // getOverlapCoefficient
+ // ------------------------------------------------------------------
+
+ @Test
+ public void testOverlapCoefficientSubset() {
+ // A-B-C, k=2 from A = {A,B,C}, k=1 from B = {A,B,C}
+ addEdge("A", "B");
+ addEdge("B", "C");
+ GraphNeighborhoodAnalyzer analyzer = new GraphNeighborhoodAnalyzer(graph);
+ // N2(A)={A,B,C}, N1(B)={A,B,C}, overlap coeff = 3/3 = 1.0
+ assertEquals(1.0, analyzer.getOverlapCoefficient("A", "B", 2), 0.001);
+ }
+
+ @Test
+ public void testOverlapCoefficientDisjoint() {
+ graph.addVertex("A");
+ graph.addVertex("B");
+ GraphNeighborhoodAnalyzer analyzer = new GraphNeighborhoodAnalyzer(graph);
+ assertEquals(0.0, analyzer.getOverlapCoefficient("A", "B", 1), 0.001);
+ }
+
+ // ------------------------------------------------------------------
+ // getAggregateStats
+ // ------------------------------------------------------------------
+
+ @Test
+ public void testAggregateStatsComplete() {
+ buildComplete(4);
+ GraphNeighborhoodAnalyzer analyzer = new GraphNeighborhoodAnalyzer(graph);
+ GraphNeighborhoodAnalyzer.AggregateStats stats = analyzer.getAggregateStats(1);
+ assertEquals(1, stats.getK());
+ assertEquals(4.0, stats.getMeanSize(), 0.001); // all see all
+ assertEquals(4, stats.getMinSize());
+ assertEquals(4, stats.getMaxSize());
+ assertEquals(1.0, stats.getMeanDensity(), 0.001);
+ }
+
+ @Test
+ public void testAggregateStatsPath() {
+ buildPath(5); // 0-1-2-3-4
+ GraphNeighborhoodAnalyzer analyzer = new GraphNeighborhoodAnalyzer(graph);
+ GraphNeighborhoodAnalyzer.AggregateStats stats = analyzer.getAggregateStats(1);
+ // k=1 sizes: 0→2, 1→3, 2→3, 3→3, 4→2 → mean = 13/5 = 2.6
+ assertEquals(2.6, stats.getMeanSize(), 0.001);
+ assertEquals(2, stats.getMinSize());
+ assertEquals(3, stats.getMaxSize());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testAggregateStatsNegativeK() {
+ buildPath(3);
+ GraphNeighborhoodAnalyzer analyzer = new GraphNeighborhoodAnalyzer(graph);
+ analyzer.getAggregateStats(-1);
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testAggregateStatsEmptyGraph() {
+ GraphNeighborhoodAnalyzer analyzer = new GraphNeighborhoodAnalyzer(graph);
+ analyzer.getAggregateStats(1);
+ }
+
+ // ------------------------------------------------------------------
+ // rankByNeighborhoodSize
+ // ------------------------------------------------------------------
+
+ @Test
+ public void testRankPath() {
+ buildPath(5); // 0-1-2-3-4
+ GraphNeighborhoodAnalyzer analyzer = new GraphNeighborhoodAnalyzer(graph);
+ List> ranked = analyzer.rankByNeighborhoodSize(1);
+ assertEquals(5, ranked.size());
+ // Top entries should have size 3 (vertices 1, 2, or 3)
+ assertEquals(3, (int) ranked.get(0).getValue());
+ // Last entries should have size 2 (vertices 0 or 4)
+ assertEquals(2, (int) ranked.get(4).getValue());
+ }
+
+ @Test
+ public void testRankStar() {
+ buildStar(4); // c with leaves 0,1,2,3
+ GraphNeighborhoodAnalyzer analyzer = new GraphNeighborhoodAnalyzer(graph);
+ List> ranked = analyzer.rankByNeighborhoodSize(1);
+ // c has size 5 (c + 4 leaves), leaves have size 2 (self + c)
+ assertEquals("c", ranked.get(0).getKey());
+ assertEquals(5, (int) ranked.get(0).getValue());
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void testRankNegativeK() {
+ buildPath(3);
+ GraphNeighborhoodAnalyzer analyzer = new GraphNeighborhoodAnalyzer(graph);
+ analyzer.rankByNeighborhoodSize(-1);
+ }
+
+ // ------------------------------------------------------------------
+ // getTextReport
+ // ------------------------------------------------------------------
+
+ @Test
+ public void testTextReportNotEmpty() {
+ buildPath(5);
+ GraphNeighborhoodAnalyzer analyzer = new GraphNeighborhoodAnalyzer(graph);
+ String report = analyzer.getTextReport("2", 3);
+ assertNotNull(report);
+ assertTrue(report.contains("Neighborhood Analysis Report"));
+ assertTrue(report.contains("Vertex '2'"));
+ assertTrue(report.contains("Depth 0"));
+ assertTrue(report.contains("Aggregate Statistics"));
+ }
+
+ @Test
+ public void testTextReportNullSource() {
+ buildComplete(3);
+ GraphNeighborhoodAnalyzer analyzer = new GraphNeighborhoodAnalyzer(graph);
+ String report = analyzer.getTextReport(null, 2);
+ assertNotNull(report);
+ assertTrue(report.contains("Aggregate Statistics"));
+ assertFalse(report.contains("Vertex"));
+ }
+
+ @Test
+ public void testTextReportContainsExpansion() {
+ buildPath(4);
+ GraphNeighborhoodAnalyzer analyzer = new GraphNeighborhoodAnalyzer(graph);
+ String report = analyzer.getTextReport("0", 3);
+ assertTrue(report.contains("expansion="));
+ }
+
+ // ------------------------------------------------------------------
+ // Edge case: cycle graph
+ // ------------------------------------------------------------------
+
+ @Test
+ public void testCycleNeighborhood() {
+ buildCycle(6); // 0-1-2-3-4-5-0
+ GraphNeighborhoodAnalyzer analyzer = new GraphNeighborhoodAnalyzer(graph);
+ // k=1 from 0: {0, 1, 5}
+ Set hood = analyzer.getKHopNeighborhood("0", 1);
+ assertEquals(3, hood.size());
+ assertTrue(hood.containsAll(Arrays.asList("0", "1", "5")));
+
+ // k=3 from 0: should reach all 6 vertices
+ Set fullHood = analyzer.getKHopNeighborhood("0", 3);
+ assertEquals(6, fullHood.size());
+ }
+
+ @Test
+ public void testCycleGrowthProfile() {
+ buildCycle(6);
+ GraphNeighborhoodAnalyzer analyzer = new GraphNeighborhoodAnalyzer(graph);
+ List profile = analyzer.getGrowthProfile("0", 5);
+ // [1, 3, 5, 6]
+ assertEquals(4, profile.size());
+ assertEquals(1, (int) profile.get(0));
+ assertEquals(3, (int) profile.get(1));
+ assertEquals(5, (int) profile.get(2));
+ assertEquals(6, (int) profile.get(3));
+ }
+
+ // ------------------------------------------------------------------
+ // Edge case: single edge
+ // ------------------------------------------------------------------
+
+ @Test
+ public void testSingleEdge() {
+ addEdge("A", "B");
+ GraphNeighborhoodAnalyzer analyzer = new GraphNeighborhoodAnalyzer(graph);
+ assertEquals(2, analyzer.getKHopNeighborhood("A", 1).size());
+ assertEquals(1.0, analyzer.getLocalDensity("A", 1), 0.001);
+ assertEquals(1.0, analyzer.getNeighborhoodOverlap("A", "B", 1), 0.001);
+ }
+
+ // ------------------------------------------------------------------
+ // Consistency checks
+ // ------------------------------------------------------------------
+
+ @Test
+ public void testGrowthProfileMonotonicallyIncreasing() {
+ buildCycle(8);
+ GraphNeighborhoodAnalyzer analyzer = new GraphNeighborhoodAnalyzer(graph);
+ List profile = analyzer.getGrowthProfile("0", 10);
+ for (int i = 1; i < profile.size(); i++) {
+ assertTrue("Growth must be monotonically increasing",
+ profile.get(i) >= profile.get(i - 1));
+ }
+ }
+
+ @Test
+ public void testKHopContainsSelf() {
+ buildPath(5);
+ GraphNeighborhoodAnalyzer analyzer = new GraphNeighborhoodAnalyzer(graph);
+ for (String v : graph.getVertices()) {
+ Set hood = analyzer.getKHopNeighborhood(v, 2);
+ assertTrue("k-hop neighborhood must contain the source vertex",
+ hood.contains(v));
+ }
+ }
+
+ @Test
+ public void testOverlapSymmetric() {
+ buildPath(5);
+ GraphNeighborhoodAnalyzer analyzer = new GraphNeighborhoodAnalyzer(graph);
+ double ab = analyzer.getNeighborhoodOverlap("0", "4", 2);
+ double ba = analyzer.getNeighborhoodOverlap("4", "0", 2);
+ assertEquals("Overlap must be symmetric", ab, ba, 0.0001);
+ }
+
+ @Test
+ public void testLocalDensityBounded() {
+ buildPath(5);
+ GraphNeighborhoodAnalyzer analyzer = new GraphNeighborhoodAnalyzer(graph);
+ for (String v : graph.getVertices()) {
+ double d = analyzer.getLocalDensity(v, 2);
+ assertTrue("Density must be >= 0", d >= 0.0);
+ assertTrue("Density must be <= 1", d <= 1.0 + 0.0001);
+ }
+ }
+}