Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ logfiles/
stashmerge.sh
jacksnbs
demos
uv.lock

__pycache__
*.ipynb_checkpoints
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ We want this community to be friendly and respectful to each other. Please read
1. Create a Python virtual environment. For convenience, our Makefile provides a target called 'venv' that will create a virtual environment for you. Run the following command: `make venv`
1. Activate the virtual environment. If you used the Makefile target in the previous step, activate the virtual environment by the running the following command: `source venv-hnx/bin/activate`
1. Install the library in development mode: `pip install -e .`
1. Install testing dependencies: `pip install -e .['testing'] `
1. Install testing dependencies: `pip install -e '.[testing]' `
1. Do the changes you want and ensure all tests pass by running `python -m pytest` before sending a pull request.

### Commit message convention
Expand Down
58 changes: 58 additions & 0 deletions hypernetx/classes/hypergraph.py
Original file line number Diff line number Diff line change
Expand Up @@ -2392,6 +2392,64 @@ def distance(self, source, target, s=1):

return dist

def k_nearest_neighbors(self, node, k, s=1):
"""
Return nodes nearest to ``node`` by shortest :term:`s-walk` distance.

This uses the same s-walk metric as :meth:`distance`, not the
s-edge-sharing definition in :meth:`neighbors`.

Parameters
----------
node : hashable
A node in the hypergraph.

k : int
Consider the ``k``-th smallest s-walk distance among other nodes,
then return every other node at distance less than or equal to
that value. Ties at the cutoff may yield more than ``k`` nodes.

s : positive int, optional, default=1
Minimum number of shared edges between consecutive nodes in an
s-walk.

Returns
-------
list
Node uids of the k-nearest neighbors (including ties).

See Also
--------
distance
neighbors
get_linegraph

Notes
-----
Nodes in other s-components are omitted. Distances match
:meth:`distance` for each returned neighbor.
"""
if k <= 0:
return []
if node not in self.nodes:
warnings.warn(f"{node} is not in hypergraph {self.name}.")
return []

g = self.get_linegraph(s=s, edges=False)
try:
lengths = nx.single_source_shortest_path_length(g, node)
except nx.NodeNotFound:
return []

candidates = [(u, d) for u, d in lengths.items() if u != node]
if not candidates:
return []

candidates.sort(key=lambda pair: (pair[1], pair[0]))
k_eff = min(k, len(candidates))
kth_dist = candidates[k_eff - 1][1]
return [u for u, d in candidates if d <= kth_dist]

def edge_distance(self, source, target, s=1):
"""
Returns the shortest :term:`s-walk` distance between two edges in the hypergraph.
Expand Down
60 changes: 60 additions & 0 deletions tests/classes/test_hypergraph.py
Original file line number Diff line number Diff line change
Expand Up @@ -1445,6 +1445,66 @@ def test_distance(lesmis):
assert h.distance("ME", "FN", s=3) == np.inf


def test_k_nearest_neighbors(lesmis):
h = Hypergraph(lesmis.edgedict)
knn1 = h.k_nearest_neighbors("ME", 1)
assert set(knn1) == {
"CL",
"CV",
"GE",
"GG",
"IS",
"JL",
"JV",
"MB",
"MC",
"MR",
"MT",
"MY",
"NP",
"PG",
"SN",
}
for u in knn1:
assert h.distance("ME", u) == 1

knn_s2 = h.k_nearest_neighbors("ME", 1, s=2)
assert set(knn_s2) == {"MB", "MY"}
for u in knn_s2:
assert h.distance("ME", u, s=2) == 1


def test_k_nearest_neighbors_sevenbysix(sevenbysix):
hg = Hypergraph(sevenbysix.edgedict)
assert set(hg.k_nearest_neighbors("A", 1)) == {"C", "E", "K", "T2", "V"}


def test_k_nearest_neighbors_ties():
h = Hypergraph({"e1": ["v", "a"], "e2": ["v", "b"], "e3": ["v", "c"]})
knn = h.k_nearest_neighbors("v", 1)
assert set(knn) == {"a", "b", "c"}
assert len(knn) > 1


def test_k_nearest_neighbors_invalid_node(lesmis):
h = Hypergraph(lesmis.edgedict)
with pytest.warns(UserWarning, match="NEMO is not in hypergraph"):
assert h.k_nearest_neighbors("NEMO", 1) == []


def test_k_nearest_neighbors_k_nonpositive(lesmis):
h = Hypergraph(lesmis.edgedict)
assert h.k_nearest_neighbors("ME", 0) == []
assert h.k_nearest_neighbors("ME", -1) == []


def test_k_nearest_neighbors_k_large(lesmis):
h = Hypergraph(lesmis.edgedict)
knn = h.k_nearest_neighbors("ME", 1000)
assert len(knn) == len(h.nodes) - 1
assert "ME" not in knn


def test_edge_distance(lesmis):
h = Hypergraph(lesmis.edgedict)
assert h.edge_distance(1, 4) == 2
Expand Down