c889a57b6b
Test Suites / Build CI Environment (push) Has been cancelled
Test Suites / Basic Tests (push) Has been cancelled
Test Suites / End-to-End Tests (push) Has been cancelled
Test Suites / CLI Tests (push) Has been cancelled
Test Suites / Slow End-to-End Tests (push) Has been cancelled
Test Suites / Graph Database Tests (push) Has been cancelled
Test Suites / Vector DB Tests (push) Has been cancelled
Test Suites / Temporal Graph Test (push) Has been cancelled
Test Suites / Search Test on Different DBs (push) Has been cancelled
Test Suites / Example Tests (push) Has been cancelled
Test Suites / Notebook Tests (push) Has been cancelled
Test Suites / OS and Python Tests Ubuntu (push) Has been cancelled
Test Suites / OS and Python Tests Extended (push) Has been cancelled
Test Suites / LLM Test Suite (push) Has been cancelled
Test Suites / S3 File Storage Test (push) Has been cancelled
Test Suites / Run Integration Tests (push) Has been cancelled
Test Suites / MCP Tests (push) Has been cancelled
Test Suites / Docker Compose Test (push) Has been cancelled
Test Suites / Docker CI test (push) Has been cancelled
Test Suites / Relational DB Migration Tests (push) Has been cancelled
Test Suites / Distributed Cognee Test (push) Has been cancelled
Test Suites / DB Examples Tests (push) Has been cancelled
Test Suites / Test Completion Status (push) Has been cancelled
Test Suites / Claude Code Review (push) Has been cancelled
Test Suites / basic checks (push) Has been cancelled
build | Build and Push Cognee MCP Docker Image to dockerhub / docker-build-and-push (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
build | Build and Push Docker Image to dockerhub / docker-build-and-push (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges Core Functionality (3.11) (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges Core Functionality (3.12) (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges with Different Graph Databases (kuzu, kuzu) (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges with Different Graph Databases (neo4j, neo4j) (push) Has been cancelled
Weighted Edges Tests / Test Weighted Edges Examples (push) Has been cancelled
Weighted Edges Tests / Code Quality for Weighted Edges (push) Has been cancelled
133 lines
5.0 KiB
Python
133 lines
5.0 KiB
Python
"""Unit tests for the semantic layout — deterministic offline projection.
|
|
|
|
No LLM, no vector store: embeddings are handed in directly. Covers PCA
|
|
determinism + sign convention, [-1,1] normalization, the no-vector neighbor
|
|
fallback, isolated-node ring placement, de-overlap separation, and the UMAP
|
|
ImportError fallback (CI has no umap-learn).
|
|
"""
|
|
|
|
import logging
|
|
|
|
import numpy as np
|
|
|
|
from cognee.modules.visualization.layouts import semantic_layout
|
|
from cognee.modules.visualization.layouts.semantic_layout import (
|
|
MIN_SEPARATION,
|
|
SPREAD,
|
|
compute_positions,
|
|
)
|
|
|
|
|
|
def _nodes(ids, ntype="Entity"):
|
|
return [{"id": i, "type": ntype, "name": f"n-{i}"} for i in ids]
|
|
|
|
|
|
# Fixed 4-point embedding in 3-D: two tight pairs separated along axis 0.
|
|
FIXED_EMB = {
|
|
"a": [1.0, 0.0, 0.0],
|
|
"b": [1.1, 0.05, 0.0],
|
|
"c": [-1.0, 0.0, 0.0],
|
|
"d": [-1.1, -0.05, 0.0],
|
|
}
|
|
|
|
|
|
def test_pca_deterministic_across_runs():
|
|
nodes = _nodes(["a", "b", "c", "d"])
|
|
r1 = compute_positions(nodes, [], FIXED_EMB, seed=42)
|
|
r2 = compute_positions(nodes, [], FIXED_EMB, seed=42)
|
|
assert r1 == r2 # exact equality — pinned, deterministic
|
|
|
|
|
|
def test_all_nodes_positioned_and_within_spread():
|
|
nodes = _nodes(["a", "b", "c", "d"])
|
|
pos = compute_positions(nodes, [], FIXED_EMB, seed=42)
|
|
assert set(pos) == {"a", "b", "c", "d"}
|
|
for p in pos.values():
|
|
# De-overlap can nudge slightly past the box; allow a small margin.
|
|
assert -SPREAD - 0.2 <= p["x"] <= SPREAD + 0.2
|
|
assert -SPREAD - 0.2 <= p["y"] <= SPREAD + 0.2
|
|
|
|
|
|
def test_positions_invariant_to_node_order():
|
|
# The sign convention pins SVD's arbitrary internal sign, so the projection
|
|
# is a pure function of the embedding set — independent of node ordering.
|
|
fwd = _nodes(["a", "b", "c", "d"])
|
|
rev = _nodes(["d", "c", "b", "a"])
|
|
p_fwd = compute_positions(fwd, [], FIXED_EMB, seed=42)
|
|
p_rev = compute_positions(rev, [], FIXED_EMB, seed=42)
|
|
assert p_fwd == p_rev
|
|
|
|
|
|
def test_primary_axis_separates_clusters():
|
|
# The two tight pairs are far apart on embedding axis 0 -> they must land on
|
|
# opposite sides of the projection's primary axis.
|
|
nodes = _nodes(["a", "b", "c", "d"])
|
|
pos = compute_positions(nodes, [], FIXED_EMB, seed=42)
|
|
left = {"a", "b"}
|
|
right = {"c", "d"}
|
|
left_x = np.mean([pos[i]["x"] for i in left])
|
|
right_x = np.mean([pos[i]["x"] for i in right])
|
|
assert abs(left_x - right_x) > 0.5 # clearly separated
|
|
|
|
|
|
def test_no_vector_node_placed_at_neighbor_centroid():
|
|
# 'x' has no embedding but links to 'a' and 'c'; it should sit near their
|
|
# midpoint (small seeded jitter aside), not on the ring.
|
|
nodes = _nodes(["a", "b", "c", "d", "x"])
|
|
links = [{"source": "x", "target": "a"}, {"source": "x", "target": "c"}]
|
|
pos = compute_positions(nodes, links, FIXED_EMB, seed=42)
|
|
midpoint = np.array([(pos["a"]["x"] + pos["c"]["x"]) / 2, (pos["a"]["y"] + pos["c"]["y"]) / 2])
|
|
placed = np.array([pos["x"]["x"], pos["x"]["y"]])
|
|
assert np.linalg.norm(placed - midpoint) < 0.25 # jitter + de-overlap tolerance
|
|
|
|
|
|
def test_isolated_no_vector_node_on_ring():
|
|
# 'iso' has no embedding and no links -> deterministic ring, outside the box.
|
|
nodes = _nodes(["a", "b", "c", "d", "iso"])
|
|
pos = compute_positions(nodes, [], FIXED_EMB, seed=42)
|
|
r = np.hypot(pos["iso"]["x"], pos["iso"]["y"])
|
|
assert r > SPREAD # pushed to the ring radius (1.15 * spread)
|
|
|
|
|
|
def test_deoverlap_separates_coincident_points():
|
|
# Four identical embeddings would collapse to one point; de-overlap must
|
|
# spread them to at least ~MIN_SEPARATION apart.
|
|
nodes = _nodes(["a", "b", "c", "d"])
|
|
same = {k: [0.5, 0.5, 0.5] for k in ["a", "b", "c", "d"]}
|
|
pos = compute_positions(nodes, [], same, seed=42)
|
|
pts = np.array([[pos[i]["x"], pos[i]["y"]] for i in ["a", "b", "c", "d"]])
|
|
dists = [
|
|
np.linalg.norm(pts[i] - pts[j]) for i in range(len(pts)) for j in range(i + 1, len(pts))
|
|
]
|
|
assert min(dists) >= MIN_SEPARATION * SPREAD * 0.9
|
|
|
|
|
|
def test_umap_method_falls_back_to_pca(monkeypatch, caplog):
|
|
# Force the umap import to fail; method="umap" must fall back to PCA and log.
|
|
import builtins
|
|
|
|
real_import = builtins.__import__
|
|
|
|
def fake_import(name, *args, **kwargs):
|
|
if name == "umap":
|
|
raise ImportError("no umap")
|
|
return real_import(name, *args, **kwargs)
|
|
|
|
monkeypatch.setattr(builtins, "__import__", fake_import)
|
|
nodes = _nodes(["a", "b", "c", "d"])
|
|
with caplog.at_level(logging.INFO):
|
|
umap_pos = compute_positions(nodes, [], FIXED_EMB, method="umap", seed=42)
|
|
pca_pos = compute_positions(nodes, [], FIXED_EMB, method="pca", seed=42)
|
|
assert umap_pos == pca_pos # identical -> fell back to PCA
|
|
assert "falling back to PCA" in caplog.text # log half of the contract
|
|
|
|
|
|
def test_emit_js_carries_position_token():
|
|
assert "__SEMANTIC_POSITIONS__" in semantic_layout.emit_js()
|
|
|
|
|
|
def test_empty_and_single_node_graphs():
|
|
assert compute_positions([], [], {}) == {}
|
|
single = compute_positions(_nodes(["a"]), [], {"a": [1.0, 2.0, 3.0]}, seed=42)
|
|
assert set(single) == {"a"}
|