Nothing
# =============================================================================
# Tests for audit-identified bugs — ground truth equivalence
# =============================================================================
# ---------------------------------------------------------------------------
# Bug 1: detect_duplicate_edges / aggregate_duplicate_edges must respect
# directed vs undirected semantics
# Ground truth: igraph::simplify() — the reference implementation
# ---------------------------------------------------------------------------
test_that("directed: A->B and B->A are distinct edges, not duplicates", {
edges <- data.frame(
from = c(1, 2, 1),
to = c(2, 1, 3),
weight = c(0.5, 0.8, 0.3)
)
# Undirected: 1-2 appears twice (A->B and B->A canonicalize to same key)
res_undir <- detect_duplicate_edges(edges, directed = FALSE)
expect_true(res_undir$has_duplicates)
# Directed: 1->2 and 2->1 are distinct — no duplicates
res_dir <- detect_duplicate_edges(edges, directed = TRUE)
expect_false(res_dir$has_duplicates)
})
test_that("directed: true duplicates (same direction) are still caught", {
edges <- data.frame(
from = c(1, 1, 2),
to = c(2, 2, 1),
weight = c(0.5, 0.3, 0.8)
)
res <- detect_duplicate_edges(edges, directed = TRUE)
expect_true(res$has_duplicates)
expect_equal(length(res$info), 1)
expect_equal(res$info[[1]]$count, 2)
})
test_that("aggregate_duplicate_edges preserves direction for directed graphs", {
edges <- data.frame(
from = c(1, 2, 1),
to = c(2, 1, 2),
weight = c(0.4, 0.8, 0.6)
)
# Directed aggregation: 1->2 appears twice (sum = 1.0), 2->1 once (0.8)
agg_dir <- aggregate_duplicate_edges(edges, method = "sum", directed = TRUE)
expect_equal(nrow(agg_dir), 2)
row_12 <- agg_dir[agg_dir$from == 1 & agg_dir$to == 2, ]
row_21 <- agg_dir[agg_dir$from == 2 & agg_dir$to == 1, ]
expect_equal(row_12$weight, 1.0)
expect_equal(row_21$weight, 0.8)
# Undirected aggregation: all three merge to one canonical edge 1-2
agg_undir <- aggregate_duplicate_edges(edges, method = "sum", directed = FALSE)
expect_equal(nrow(agg_undir), 1)
expect_equal(agg_undir$weight, 1.8)
})
test_that("aggregate_duplicate_edges directed matches igraph::simplify", {
# Build a directed igraph with duplicate 1->2 edges
g <- igraph::make_empty_graph(n = 4, directed = TRUE)
g <- igraph::add_edges(g, c(1,2, 1,2, 2,1, 1,3, 3,4))
igraph::E(g)$weight <- c(0.3, 0.7, 0.5, 0.4, 0.6)
g_simple <- igraph::simplify(g, remove.multiple = TRUE, remove.loops = TRUE,
edge.attr.comb = list(weight = "sum"))
# Extract igraph result as edge list
ig_edges <- igraph::as_data_frame(g_simple, what = "edges")
# Build the same edge list for cograph
co_edges <- data.frame(
from = c(1, 1, 2, 1, 3),
to = c(2, 2, 1, 3, 4),
weight = c(0.3, 0.7, 0.5, 0.4, 0.6)
)
co_agg <- aggregate_duplicate_edges(co_edges, method = "sum", directed = TRUE)
# Both should produce 4 edges after merging 1->2 duplicates
expect_equal(nrow(co_agg), nrow(ig_edges))
# Compare edge weights — sort both by from,to for stable comparison
ig_sorted <- ig_edges[order(ig_edges$from, ig_edges$to), ]
co_sorted <- co_agg[order(co_agg$from, co_agg$to), ]
expect_equal(co_sorted$weight, ig_sorted$weight)
expect_equal(co_sorted$from, ig_sorted$from)
expect_equal(co_sorted$to, ig_sorted$to)
})
test_that("aggregate_duplicate_edges undirected matches igraph::simplify", {
# Undirected igraph: 1--2 with two edges, 2--1 with another
g <- igraph::make_empty_graph(n = 3, directed = FALSE)
g <- igraph::add_edges(g, c(1,2, 1,2, 2,3))
igraph::E(g)$weight <- c(0.3, 0.7, 0.5)
g_simple <- igraph::simplify(g, remove.multiple = TRUE, remove.loops = TRUE,
edge.attr.comb = list(weight = "mean"))
ig_edges <- igraph::as_data_frame(g_simple, what = "edges")
co_edges <- data.frame(
from = c(1, 1, 2),
to = c(2, 2, 3),
weight = c(0.3, 0.7, 0.5)
)
co_agg <- aggregate_duplicate_edges(co_edges, method = "mean", directed = FALSE)
expect_equal(nrow(co_agg), nrow(ig_edges))
ig_sorted <- ig_edges[order(ig_edges$from, ig_edges$to), ]
co_sorted <- co_agg[order(co_agg$from, co_agg$to), ]
expect_equal(co_sorted$weight, ig_sorted$weight)
})
test_that("simplify.cograph_network respects directedness", {
# Directed network: 1->2 and 2->1 should both survive
dir_mat <- matrix(c(0, 0.5, 0, 0.8, 0, 0, 0, 0.3, 0), 3, 3, byrow = TRUE)
rownames(dir_mat) <- colnames(dir_mat) <- c("A", "B", "C")
net_dir <- as_cograph(dir_mat)
expect_true(cograph::is_directed(net_dir))
edges_before <- get_edges(net_dir)
net_simplified <- simplify(net_dir)
edges_after <- get_edges(net_simplified)
# A->B (0.5) and B->A (0.8) are distinct — both must survive
expect_true(any(edges_after$from == 1 & edges_after$to == 2))
expect_true(any(edges_after$from == 2 & edges_after$to == 1))
expect_equal(nrow(edges_before), nrow(edges_after))
})
test_that("simplify.cograph_network directed matches igraph::simplify", {
# 5-node directed network with some reciprocal edges
mat <- matrix(0, 5, 5)
mat[1, 2] <- 0.3; mat[2, 1] <- 0.7
mat[1, 3] <- 0.5; mat[3, 1] <- 0.2
mat[2, 3] <- 0.4
mat[4, 5] <- 0.9; mat[5, 4] <- 0.1
rownames(mat) <- colnames(mat) <- LETTERS[1:5]
# igraph reference
g <- igraph::graph_from_adjacency_matrix(mat, mode = "directed", weighted = TRUE)
g_simple <- igraph::simplify(g, remove.loops = TRUE, remove.multiple = TRUE)
ig_n <- igraph::ecount(g_simple)
# cograph
net <- as_cograph(mat)
net_s <- simplify(net)
co_n <- nrow(get_edges(net_s))
expect_equal(co_n, ig_n)
})
# ---------------------------------------------------------------------------
# Bug 2: layout$name should be layout$get_type()
# Ground truth: CographLayout R6 class API
# ---------------------------------------------------------------------------
test_that("CographLayout has no public $name field", {
layout <- CographLayout$new("spring")
# $name should be NULL (no such field)
expect_null(layout$name)
# get_type() should work
expect_equal(layout$get_type(), "spring")
})
test_that("compute_layout_for_cograph records correct layout name via CographLayout", {
mat <- matrix(c(0, 1, 0, 1, 0, 1, 0, 1, 0), 3, 3)
rownames(mat) <- colnames(mat) <- c("A", "B", "C")
# String layout via cograph()
net_str <- cograph(mat, layout = "circle")
expect_equal(net_str$meta$layout$name, "circle")
# CographLayout object via compute_layout_for_cograph
net <- as_cograph(mat)
layout_obj <- CographLayout$new("circle")
net_r6 <- compute_layout_for_cograph(net, layout = layout_obj)
# Before the fix this returned "custom" (layout$name was NULL)
expect_equal(net_r6$meta$layout$name, "circle")
})
# ---------------------------------------------------------------------------
# Bug 3: is_directed() must work on R6 CographNetwork, S3 cograph_network,
# and igraph objects
# Ground truth: direct property access on each object type
# ---------------------------------------------------------------------------
test_that("is_directed works on R6 CographNetwork (directed)", {
mat <- matrix(c(0, 1, 0, 0), 2, 2)
r6 <- CographNetwork$new(mat)
# R6 active binding
expect_true(r6$is_directed)
# Global function
expect_true(cograph::is_directed(r6))
})
test_that("is_directed works on R6 CographNetwork (undirected)", {
mat <- matrix(c(0, 1, 1, 0), 2, 2)
r6 <- CographNetwork$new(mat)
expect_false(r6$is_directed)
expect_false(cograph::is_directed(r6))
})
test_that("is_directed works on S3 cograph_network", {
mat_dir <- matrix(c(0, 0.5, 0, 0.8, 0, 0, 0, 0.3, 0), 3, 3, byrow = TRUE)
rownames(mat_dir) <- colnames(mat_dir) <- c("A", "B", "C")
net_dir <- as_cograph(mat_dir)
expect_true(cograph::is_directed(net_dir))
mat_undir <- matrix(c(0, 1, 1, 1, 0, 1, 1, 1, 0), 3, 3)
rownames(mat_undir) <- colnames(mat_undir) <- c("A", "B", "C")
net_undir <- as_cograph(mat_undir)
expect_false(cograph::is_directed(net_undir))
})
test_that("is_directed agrees across R6, S3, and igraph for the same matrix", {
# Directed
mat_dir <- matrix(c(0, 1, 0, 0), 2, 2)
rownames(mat_dir) <- colnames(mat_dir) <- c("A", "B")
r6_dir <- CographNetwork$new(mat_dir)
s3_dir <- as_cograph(mat_dir)
ig_dir <- igraph::graph_from_adjacency_matrix(mat_dir, mode = "directed",
weighted = TRUE)
expect_identical(cograph::is_directed(r6_dir), cograph::is_directed(s3_dir))
expect_identical(cograph::is_directed(s3_dir), igraph::is_directed(ig_dir))
# Undirected
mat_undir <- matrix(c(0, 1, 1, 0), 2, 2)
rownames(mat_undir) <- colnames(mat_undir) <- c("A", "B")
r6_undir <- CographNetwork$new(mat_undir)
s3_undir <- as_cograph(mat_undir)
ig_undir <- igraph::graph_from_adjacency_matrix(mat_undir, mode = "undirected",
weighted = TRUE)
expect_identical(cograph::is_directed(r6_undir), cograph::is_directed(s3_undir))
expect_identical(cograph::is_directed(s3_undir), igraph::is_directed(ig_undir))
})
# ---------------------------------------------------------------------------
# Edge case: backward compatibility — defaults unchanged
# ---------------------------------------------------------------------------
test_that("detect_duplicate_edges default (directed=FALSE) preserves old behavior", {
edges <- data.frame(from = c(1, 2), to = c(2, 1), weight = c(0.5, 0.8))
# Default = undirected: 1-2 and 2-1 are duplicates
res <- detect_duplicate_edges(edges)
expect_true(res$has_duplicates)
})
test_that("aggregate_duplicate_edges default (directed=FALSE) preserves old behavior", {
edges <- data.frame(from = c(1, 2), to = c(2, 1), weight = c(0.5, 0.8))
agg <- aggregate_duplicate_edges(edges, method = "mean")
expect_equal(nrow(agg), 1)
expect_equal(agg$weight, 0.65)
})
# ---------------------------------------------------------------------------
# Bug 4: fast_greedy must recompute weights after directed -> undirected collapse
# ---------------------------------------------------------------------------
test_that("detect_communities fast_greedy handles collapsed directed weights", {
mat <- matrix(c(
0, 1, 2, 0,
3, 0, 4, 0,
0, 0, 0, 5,
6, 0, 0, 0
), 4, 4, byrow = TRUE)
result <- detect_communities(mat, method = "fast_greedy", directed = TRUE)
expect_s3_class(result, "cograph_communities")
expect_equal(nrow(result), 4)
})
# ---------------------------------------------------------------------------
# Bug 5: parse_edgelist auto-detect should not treat duplicate same-direction
# rows as directed
# ---------------------------------------------------------------------------
test_that("parse_edgelist duplicate same-direction rows remain undirected", {
df <- data.frame(from = c("A", "A"), to = c("B", "B"))
result <- parse_edgelist(df)
expect_false(result$directed)
})
test_that("parse_edgelist reciprocal rows are directed", {
df <- data.frame(from = c("A", "B"), to = c("B", "A"))
result <- parse_edgelist(df)
expect_true(result$directed)
})
# ---------------------------------------------------------------------------
# Bug 6: custom layout coordinates must have exactly one row per node
# ---------------------------------------------------------------------------
test_that("cograph rejects custom layout row-count mismatch", {
mat <- matrix(c(0, 1, 1, 0), 2, 2)
expect_error(
cograph(mat, layout = data.frame(x = 0, y = 0)),
"one row per node"
)
})
test_that("sn_layout rejects custom layout row-count mismatch", {
mat <- matrix(c(0, 1, 1, 0), 2, 2)
net <- cograph(mat)
expect_error(
sn_layout(net, data.frame(x = 0, y = 0)),
"one row per node"
)
})
test_that("CographNetwork rejects custom layout row-count mismatch", {
mat <- matrix(c(0, 1, 1, 0), 2, 2)
net <- CographNetwork$new(mat)
expect_error(
net$set_layout_coords(data.frame(x = 0, y = 0)),
"one row per node"
)
})
# ---------------------------------------------------------------------------
# Bug 7: filter predicates must be logical and length 1 or n
# ---------------------------------------------------------------------------
test_that("filter_nodes rejects wrong-length logical predicates", {
mat <- matrix(c(0, 1, 1, 0), 2, 2)
expect_error(
filter_nodes(as_cograph(mat), c(TRUE, FALSE, TRUE)),
"length 1 or 2"
)
})
test_that("filter_edges rejects wrong-length logical predicates", {
mat <- matrix(c(0, 1, 1, 0), 2, 2)
expect_error(
filter_edges(as_cograph(mat), c(TRUE, FALSE, TRUE)),
"length 1 or 1"
)
})
test_that("filter_nodes rejects non-logical predicates", {
mat <- matrix(c(0, 1, 1, 0), 2, 2)
expect_error(
filter_nodes(as_cograph(mat), c(1, 0)),
"logical vectors"
)
})
# ---------------------------------------------------------------------------
# Bug 8: signed psychometric expected influence should normalize by max abs
# ---------------------------------------------------------------------------
test_that("psych_network normalizes signed expected influence by max abs", {
w <- matrix(
c(
0, -0.8, 0.2,
-0.8, 0, 0.1,
0.2, 0.1, 0
),
nrow = 3,
byrow = TRUE,
dimnames = list(c("A", "B", "C"), c("A", "B", "C"))
)
raw <- centrality(
w,
measures = "expected_influence_1",
normalized = FALSE
)[["expected_influence_1_all"]]
expected <- raw / max(abs(raw))
explicit <- centrality(
w,
measures = "expected_influence_1",
normalized = TRUE,
psych_network = TRUE
)[["expected_influence_1_all"]]
auto <- centrality(
w,
measures = "expected_influence_1",
normalized = TRUE,
psych_network = NULL
)[["expected_influence_1_all"]]
expect_equal(explicit, expected)
expect_equal(auto, expected)
expect_lte(max(abs(explicit)), 1)
})
test_that("psych_network can be disabled to preserve generic normalization", {
w <- matrix(
c(
0, -0.8, 0.2,
-0.8, 0, 0.1,
0.2, 0.1, 0
),
nrow = 3,
byrow = TRUE,
dimnames = list(c("A", "B", "C"), c("A", "B", "C"))
)
raw <- centrality(
w,
measures = "expected_influence_1",
normalized = FALSE
)[["expected_influence_1_all"]]
legacy <- centrality(
w,
measures = "expected_influence_1",
normalized = TRUE,
psych_network = FALSE
)[["expected_influence_1_all"]]
expect_equal(legacy, raw / max(raw))
expect_gt(max(abs(legacy)), 1)
})
Any scripts or data that you put into this service are public.
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.