Repository: networkx/networkx
Pull request: Added `NodeNotFound` exceptions to `_apply_prediction` and `simrank`, and ignored isolated nodes in `panther_similarity`
closes issue #7108
<!--
Please use pre-commit to lint your code.
For more details check out step 1 and 4 of
https://networkx.org/documentation/latest/developer/contribute.html
-->
Please let me know if I should add any tests for these new `NodeNotFound` exceptions or mention them in the `Raises` sections. I didn't add any because it seemed excessive.

Thank you :)

Problem statement:
Unhandled Exceptions in `panther_similarity`, `simrank_similarity`, `preferential_attachment` and `jaccard_coefficient`
Hello, \
I've encountered a few corner cases while using Link Prediction and Similarity Measures Algorithms that lead to exceptions that aren't currently handled.
### 1. Panther Similarity ValueError with Isolated Nodes
- **Current Behavior**: 
  - Executing `nx.panther_similarity` on a graph with isolated nodes results in `ValueError: probabilities contain NaN`. This issue might be arising due to the presence of an isolated node.
- **Steps to Reproduce**:
```python
G = nx.Graph()
G.add_node(1)
G.add_edges_from([(2, 3), (3, 4), (4, 5), (5, 6)])
nx.panther_similarity(G, source = 2)
```

### 2. SimRank Similarity ValueError Instead of NodeNotFound
- **Current Behavior**: 
  - Calling `nx.simrank_similarity` on a graph where node 7 does not exist results in `ValueError: 7 is not in list`.
- **Expected Behavior**: 
  - The function should raise a `NodeNotFound` exception for better clarity when a non-existent node is specified.
- **Steps to Reproduce**:
```python
G = nx.Graph()
G.add_edges_from([(1, 1),(1, 2), (1, 3), (4, 5), (5, 6)])
nx.simrank_similarity(G, source=7)
```

### 3. Preferential Attachment TypeError and Incorrect Exception
- **Current Behavior**: 
  - Using `nx.preferential_attachment` raises `TypeError: 'int' object is not iterable` and `NetworkXError: nbunch is not a node or a sequence of nodes`.
- **Expected Behavior**: 
  - The function should raise a `NetworkXError: v is not in the graph` for non-existent nodes in the pairs provided.
- **Steps to Reproduce**:
```python
G = nx.Graph()
G.add_edges_from([(1, 1),(1, 2), (1, 3), (4, 5), (5, 6)])
preds = nx.preferential_attachment(G, [(1, 2),(3, 7)])
for u, v, p in preds:
    print(f"Preferential Attachment between nodes {u} and {v}: {p}")
```

### 4. Jaccard Coefficient KeyError for Non-existent Node
- **Current Behavior**: 
  - Executing `nx.jaccard_coefficient` where node `7` does not exist in `G` results in `KeyError: 7`.
- **Expected Behavior**: 
  - The function should raise a `NetworkXError: v is not in the graph` for non-existent nodes in the pairs provided.
- **Steps to Reproduce**:
```python
G = nx.Graph()
G.add_edges_from([(1, 1),(1, 2), (1, 3), (4, 5), (5, 6)])
preds = nx.jaccard_coefficient(G, [(1, 2),(3, 7)])
for u, v, p in preds:
    print(f"Jaccard similarity between nodes {u} and {v}: {p}")
```

### Environment

Python version: 3.9.10 \
NetworkX version: 3.2.1

Hints from issue discussion:
@iany0 Thanks! I've made a PR for this :)

Commit message: style fix

Patch to review:
diff --git a/networkx/algorithms/link_prediction.py b/networkx/algorithms/link_prediction.py
index 7335a77f5e0..21e89fbf160 100644
--- a/networkx/algorithms/link_prediction.py
+++ b/networkx/algorithms/link_prediction.py
@@ -37,6 +37,12 @@ def _apply_prediction(G, func, ebunch=None):
     """
     if ebunch is None:
         ebunch = nx.non_edges(G)
+    else:
+        for u, v in ebunch:
+            if u not in G:
+                raise nx.NodeNotFound(f"Node {u} not in G.")
+            if v not in G:
+                raise nx.NodeNotFound(f"Node {v} not in G.")
     return ((u, v, func(u, v)) for u, v in ebunch)
 
 
@@ -72,6 +78,14 @@ def resource_allocation_index(G, ebunch=None):
         An iterator of 3-tuples in the form (u, v, p) where (u, v) is a
         pair of nodes and p is their resource allocation index.
 
+    Raises
+    ------
+    NetworkXNotImplemented
+        If `G` is a `DiGraph`, a `Multigraph` or a `MultiDiGraph`.
+
+    NodeNotFound
+        If `ebunch` has a node that is not in `G`.
+
     Examples
     --------
     >>> G = nx.complete_graph(5)
@@ -127,6 +141,14 @@ def jaccard_coefficient(G, ebunch=None):
         An iterator of 3-tuples in the form (u, v, p) where (u, v) is a
         pair of nodes and p is their Jaccard coefficient.
 
+    Raises
+    ------
+    NetworkXNotImplemented
+        If `G` is a `DiGraph`, a `Multigraph` or a `MultiDiGraph`.
+
+    NodeNotFound
+        If `ebunch` has a node that is not in `G`.
+
     Examples
     --------
     >>> G = nx.complete_graph(5)
@@ -186,6 +208,14 @@ def adamic_adar_index(G, ebunch=None):
         An iterator of 3-tuples in the form (u, v, p) where (u, v) is a
         pair of nodes and p is their Adamic-Adar index.
 
+    Raises
+    ------
+    NetworkXNotImplemented
+        If `G` is a `DiGraph`, a `Multigraph` or a `MultiDiGraph`.
+
+    NodeNotFound
+        If `ebunch` has a node that is not in `G`.
+
     Examples
     --------
     >>> G = nx.complete_graph(5)
@@ -264,6 +294,17 @@ def common_neighbor_centrality(G, ebunch=None, alpha=0.8):
         pair of nodes and p is their Common Neighbor and Centrality based
         Parameterized Algorithm(CCPA) score.
 
+    Raises
+    ------
+    NetworkXNotImplemented
+        If `G` is a `DiGraph`, a `Multigraph` or a `MultiDiGraph`.
+
+    NetworkXAlgorithmError
+        If self loops exsists in `ebunch` or in `G` (if `ebunch` is `None`).
+
+    NodeNotFound
+        If `ebunch` has a node that is not in `G`.
+
     Examples
     --------
     >>> G = nx.complete_graph(5)
@@ -286,7 +327,7 @@ def common_neighbor_centrality(G, ebunch=None, alpha=0.8):
 
         def predict(u, v):
             if u == v:
-                raise nx.NetworkXAlgorithmError("Self links are not supported")
+                raise nx.NetworkXAlgorithmError("Self loops are not supported")
 
             return sum(1 for _ in nx.common_neighbors(G, u, v))
 
@@ -296,7 +337,7 @@ def predict(u, v):
 
         def predict(u, v):
             if u == v:
-                raise nx.NetworkXAlgorithmError("Self links are not supported")
+                raise nx.NetworkXAlgorithmError("Self loops are not supported")
             path_len = spl[u].get(v, inf)
 
             return alpha * sum(1 for _ in nx.common_neighbors(G, u, v)) + (
@@ -338,6 +379,14 @@ def preferential_attachment(G, ebunch=None):
         An iterator of 3-tuples in the form (u, v, p) where (u, v) is a
         pair of nodes and p is their preferential attachment score.
 
+    Raises
+    ------
+    NetworkXNotImplemented
+        If `G` is a `DiGraph`, a `Multigraph` or a `MultiDiGraph`.
+
+    NodeNotFound
+        If `ebunch` has a node that is not in `G`.
+
     Examples
     --------
     >>> G = nx.complete_graph(5)
@@ -402,6 +451,17 @@ def cn_soundarajan_hopcroft(G, ebunch=None, community="community"):
         An iterator of 3-tuples in the form (u, v, p) where (u, v) is a
         pair of nodes and p is their score.
 
+    Raises
+    ------
+    NetworkXNotImplemented
+        If `G` is a `DiGraph`, a `Multigraph` or a `MultiDiGraph`.
+
+    NetworkXAlgorithmError
+        If no community information is available for a node in `ebunch` or in `G` (if `ebunch` is `None`).
+
+    NodeNotFound
+        If `ebunch` has a node that is not in `G`.
+
     Examples
     --------
     >>> G = nx.path_graph(3)
@@ -477,6 +537,17 @@ def ra_index_soundarajan_hopcroft(G, ebunch=None, community="community"):
         An iterator of 3-tuples in the form (u, v, p) where (u, v) is a
         pair of nodes and p is their score.
 
+    Raises
+    ------
+    NetworkXNotImplemented
+        If `G` is a `DiGraph`, a `Multigraph` or a `MultiDiGraph`.
+
+    NetworkXAlgorithmError
+        If no community information is available for a node in `ebunch` or in `G` (if `ebunch` is `None`).
+
+    NodeNotFound
+        If `ebunch` has a node that is not in `G`.
+
     Examples
     --------
     >>> G = nx.Graph()
@@ -553,6 +624,18 @@ def within_inter_cluster(G, ebunch=None, delta=0.001, community="community"):
         An iterator of 3-tuples in the form (u, v, p) where (u, v) is a
         pair of nodes and p is their WIC measure.
 
+    Raises
+    ------
+    NetworkXNotImplemented
+        If `G` is a `DiGraph`, a `Multigraph` or a `MultiDiGraph`.
+
+    NetworkXAlgorithmError
+        - If `delta` is less than or equal to zero.
+        - If no community information is available for a node in `ebunch` or in `G` (if `ebunch` is `None`).
+
+    NodeNotFound
+        If `ebunch` has a node that is not in `G`.
+
     Examples
     --------
     >>> G = nx.Graph()
@@ -601,4 +684,6 @@ def _community(G, u, community):
     try:
         return node_u[community]
     except KeyError as err:
-        raise nx.NetworkXAlgorithmError("No community information") from err
+        raise nx.NetworkXAlgorithmError(
+            f"No community information available for Node {u}"
+        ) from err
diff --git a/networkx/algorithms/similarity.py b/networkx/algorithms/similarity.py
index 3d943c20d7e..f00fcc09e73 100644
--- a/networkx/algorithms/similarity.py
+++ b/networkx/algorithms/similarity.py
@@ -1275,6 +1275,14 @@ def simrank(G, u, v):
         If neither ``source`` nor ``target`` is ``None``, this returns
         the similarity value for the given pair of nodes.
 
+    Raises
+    -------
+    ExceededMaxIterations
+        If the algorithm does not converge within ``max_iterations``.
+
+    NodeNotFound
+        If either ``source`` or ``target`` is not in `G`.
+
     Examples
     --------
     >>> G = nx.cycle_graph(2)
@@ -1311,8 +1319,21 @@ def simrank(G, u, v):
     import numpy as np
 
     nodelist = list(G)
-    s_indx = None if source is None else nodelist.index(source)
-    t_indx = None if target is None else nodelist.index(target)
+    if source is not None:
+        if source not in nodelist:
+            raise nx.NodeNotFound(f"Source node {source} not in G")
+        else:
+            s_indx = nodelist.index(source)
+    else:
+        s_indx = None
+
+    if target is not None:
+        if target not in nodelist:
+            raise nx.NodeNotFound(f"Target node {target} not in G")
+        else:
+            t_indx = nodelist.index(target)
+    else:
+        t_indx = None
 
     x = _simrank_similarity_numpy(
         G, s_indx, t_indx, importance_factor, max_iterations, tolerance
@@ -1535,6 +1556,18 @@ def panther_similarity(
         the self-similarity (i.e., ``v``) will not be included in
         the returned dictionary.
 
+    Raises
+    -------
+    NetworkXUnfeasible
+        If `source` is an isolated node.
+
+    NodeNotFound
+        If `source` is not in `G`.
+
+    Notes
+    -------
+        The isolated nodes in `G` are ignored.
+
     Examples
     --------
     >>> G = nx.star_graph(10)
@@ -1550,6 +1583,18 @@ def panther_similarity(
     """
     import numpy as np
 
+    if source not in G:
+        raise nx.NodeNotFound(f"Source node {source} not in G")
+
+    isolates = list(nx.isolates(G))
+
+    if source in isolates:
+        raise nx.NetworkXUnfeasible(
+            f"Panther similarity is not defined for the isolated source node {source}."
+        )
+
+    G = G.subgraph([node for node in G.nodes if node not in isolates]).copy()
+
     num_nodes = G.number_of_nodes()
     if num_nodes < k:
         warnings.warn(
diff --git a/networkx/algorithms/tests/test_link_prediction.py b/networkx/algorithms/tests/test_link_prediction.py
index 7fc04d20672..b4643b3420d 100644
--- a/networkx/algorithms/tests/test_link_prediction.py
+++ b/networkx/algorithms/tests/test_link_prediction.py
@@ -34,23 +34,17 @@ def test_S4(self):
         G = nx.star_graph(4)
         self.test(G, [(1, 2)], [(1, 2, 0.25)])
 
-    def test_notimplemented(self):
-        assert pytest.raises(
-            nx.NetworkXNotImplemented, self.func, nx.DiGraph([(0, 1), (1, 2)]), [(0, 2)]
-        )
-        assert pytest.raises(
-            nx.NetworkXNotImplemented,
-            self.func,
-            nx.MultiGraph([(0, 1), (1, 2)]),
-            [(0, 2)],
-        )
+    @pytest.mark.parametrize("graph_type", (nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph))
+    def test_notimplemented(self, graph_type):
         assert pytest.raises(
-            nx.NetworkXNotImplemented,
-            self.func,
-            nx.MultiDiGraph([(0, 1), (1, 2)]),
-            [(0, 2)],
+            nx.NetworkXNotImplemented, self.func, graph_type([(0, 1), (1, 2)]), [(0, 2)]
         )
 
+    def test_node_not_found(self):
+        G = nx.Graph()
+        G.add_edges_from([(0, 1), (0, 2), (2, 3)])
+        assert pytest.raises(nx.NodeNotFound, self.func, G, [(0, 4)])
+
     def test_no_common_neighbor(self):
         G = nx.Graph()
         G.add_nodes_from([0, 1])
@@ -80,23 +74,17 @@ def test_P4(self):
         G = nx.path_graph(4)
         self.test(G, [(0, 2)], [(0, 2, 0.5)])
 
-    def test_notimplemented(self):
-        assert pytest.raises(
-            nx.NetworkXNotImplemented, self.func, nx.DiGraph([(0, 1), (1, 2)]), [(0, 2)]
-        )
-        assert pytest.raises(
-            nx.NetworkXNotImplemented,
-            self.func,
-            nx.MultiGraph([(0, 1), (1, 2)]),
-            [(0, 2)],
-        )
+    @pytest.mark.parametrize("graph_type", (nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph))
+    def test_notimplemented(self, graph_type):
         assert pytest.raises(
-            nx.NetworkXNotImplemented,
-            self.func,
-            nx.MultiDiGraph([(0, 1), (1, 2)]),
-            [(0, 2)],
+            nx.NetworkXNotImplemented, self.func, graph_type([(0, 1), (1, 2)]), [(0, 2)]
         )
 
+    def test_node_not_found(self):
+        G = nx.Graph()
+        G.add_edges_from([(0, 1), (0, 2), (2, 3)])
+        assert pytest.raises(nx.NodeNotFound, self.func, G, [(0, 4)])
+
     def test_no_common_neighbor(self):
         G = nx.Graph()
         G.add_edges_from([(0, 1), (2, 3)])
@@ -131,23 +119,17 @@ def test_S4(self):
         G = nx.star_graph(4)
         self.test(G, [(1, 2)], [(1, 2, 1 / math.log(4))])
 
-    def test_notimplemented(self):
-        assert pytest.raises(
-            nx.NetworkXNotImplemented, self.func, nx.DiGraph([(0, 1), (1, 2)]), [(0, 2)]
-        )
-        assert pytest.raises(
-            nx.NetworkXNotImplemented,
-            self.func,
-            nx.MultiGraph([(0, 1), (1, 2)]),
-            [(0, 2)],
-        )
+    @pytest.mark.parametrize("graph_type", (nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph))
+    def test_notimplemented(self, graph_type):
         assert pytest.raises(
-            nx.NetworkXNotImplemented,
-            self.func,
-            nx.MultiDiGraph([(0, 1), (1, 2)]),
-            [(0, 2)],
+            nx.NetworkXNotImplemented, self.func, graph_type([(0, 1), (1, 2)]), [(0, 2)]
         )
 
+    def test_node_not_found(self):
+        G = nx.Graph()
+        G.add_edges_from([(0, 1), (0, 2), (2, 3)])
+        assert pytest.raises(nx.NodeNotFound, self.func, G, [(0, 4)])
+
     def test_no_common_neighbor(self):
         G = nx.Graph()
         G.add_nodes_from([0, 1])
@@ -190,6 +172,11 @@ def test_notimplemented(self, graph_type):
             nx.NetworkXNotImplemented, self.func, graph_type([(0, 1), (1, 2)]), [(0, 2)]
         )
 
+    def test_node_not_found(self):
+        G = nx.Graph()
+        G.add_edges_from([(0, 1), (0, 2), (2, 3)])
+        assert pytest.raises(nx.NodeNotFound, self.func, G, [(0, 4)])
+
     def test_no_common_neighbor(self):
         G = nx.Graph()
         G.add_nodes_from([0, 1])
@@ -223,23 +210,17 @@ def test_S4(self):
         G = nx.star_graph(4)
         self.test(G, [(0, 2)], [(0, 2, 4)])
 
-    def test_notimplemented(self):
-        assert pytest.raises(
-            nx.NetworkXNotImplemented, self.func, nx.DiGraph([(0, 1), (1, 2)]), [(0, 2)]
-        )
-        assert pytest.raises(
-            nx.NetworkXNotImplemented,
-            self.func,
-            nx.MultiGraph([(0, 1), (1, 2)]),
-            [(0, 2)],
-        )
+    @pytest.mark.parametrize("graph_type", (nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph))
+    def test_notimplemented(self, graph_type):
         assert pytest.raises(
-            nx.NetworkXNotImplemented,
-            self.func,
-            nx.MultiDiGraph([(0, 1), (1, 2)]),
-            [(0, 2)],
+            nx.NetworkXNotImplemented, self.func, graph_type([(0, 1), (1, 2)]), [(0, 2)]
         )
 
+    def test_node_not_found(self):
+        G = nx.Graph()
+        G.add_edges_from([(0, 1), (0, 2), (2, 3)])
+        assert pytest.raises(nx.NodeNotFound, self.func, G, [(0, 4)])
+
     def test_zero_degrees(self):
         G = nx.Graph()
         G.add_nodes_from([0, 1])
@@ -282,17 +263,21 @@ def test_S4(self):
         G.nodes[4]["community"] = 0
         self.test(G, [(1, 2)], [(1, 2, 2)])
 
-    def test_notimplemented(self):
-        G = nx.DiGraph([(0, 1), (1, 2)])
-        G.add_nodes_from([0, 1, 2], community=0)
-        assert pytest.raises(nx.NetworkXNotImplemented, self.func, G, [(0, 2)])
-        G = nx.MultiGraph([(0, 1), (1, 2)])
-        G.add_nodes_from([0, 1, 2], community=0)
-        assert pytest.raises(nx.NetworkXNotImplemented, self.func, G, [(0, 2)])
-        G = nx.MultiDiGraph([(0, 1), (1, 2)])
+    @pytest.mark.parametrize("graph_type", (nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph))
+    def test_notimplemented(self, graph_type):
+        G = graph_type([(0, 1), (1, 2)])
         G.add_nodes_from([0, 1, 2], community=0)
         assert pytest.raises(nx.NetworkXNotImplemented, self.func, G, [(0, 2)])
 
+    def test_node_not_found(self):
+        G = nx.Graph()
+        G.add_edges_from([(0, 1), (0, 2), (2, 3)])
+        G.nodes[0]["community"] = 0
+        G.nodes[1]["community"] = 1
+        G.nodes[2]["community"] = 0
+        G.nodes[3]["community"] = 0
+        assert pytest.raises(nx.NodeNotFound, self.func, G, [(0, 4)])
+
     def test_no_common_neighbor(self):
         G = nx.Graph()
         G.add_nodes_from([0, 1])
@@ -387,17 +372,21 @@ def test_S4(self):
         G.nodes[4]["community"] = 0
         self.test(G, [(1, 2)], [(1, 2, 0.25)])
 
-    def test_notimplemented(self):
-        G = nx.DiGraph([(0, 1), (1, 2)])
-        G.add_nodes_from([0, 1, 2], community=0)
-        assert pytest.raises(nx.NetworkXNotImplemented, self.func, G, [(0, 2)])
-        G = nx.MultiGraph([(0, 1), (1, 2)])
-        G.add_nodes_from([0, 1, 2], community=0)
-        assert pytest.raises(nx.NetworkXNotImplemented, self.func, G, [(0, 2)])
-        G = nx.MultiDiGraph([(0, 1), (1, 2)])
+    @pytest.mark.parametrize("graph_type", (nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph))
+    def test_notimplemented(self, graph_type):
+        G = graph_type([(0, 1), (1, 2)])
         G.add_nodes_from([0, 1, 2], community=0)
         assert pytest.raises(nx.NetworkXNotImplemented, self.func, G, [(0, 2)])
 
+    def test_node_not_found(self):
+        G = nx.Graph()
+        G.add_edges_from([(0, 1), (0, 2), (2, 3)])
+        G.nodes[0]["community"] = 0
+        G.nodes[1]["community"] = 1
+        G.nodes[2]["community"] = 0
+        G.nodes[3]["community"] = 0
+        assert pytest.raises(nx.NodeNotFound, self.func, G, [(0, 4)])
+
     def test_no_common_neighbor(self):
         G = nx.Graph()
         G.add_nodes_from([0, 1])
@@ -495,17 +484,21 @@ def test_S4(self):
         G.nodes[4]["community"] = 0
         self.test(G, [(1, 2)], [(1, 2, 1 / self.delta)])
 
-    def test_notimplemented(self):
-        G = nx.DiGraph([(0, 1), (1, 2)])
-        G.add_nodes_from([0, 1, 2], community=0)
-        assert pytest.raises(nx.NetworkXNotImplemented, self.func, G, [(0, 2)])
-        G = nx.MultiGraph([(0, 1), (1, 2)])
-        G.add_nodes_from([0, 1, 2], community=0)
-        assert pytest.raises(nx.NetworkXNotImplemented, self.func, G, [(0, 2)])
-        G = nx.MultiDiGraph([(0, 1), (1, 2)])
+    @pytest.mark.parametrize("graph_type", (nx.DiGraph, nx.MultiGraph, nx.MultiDiGraph))
+    def test_notimplemented(self, graph_type):
+        G = graph_type([(0, 1), (1, 2)])
         G.add_nodes_from([0, 1, 2], community=0)
         assert pytest.raises(nx.NetworkXNotImplemented, self.func, G, [(0, 2)])
 
+    def test_node_not_found(self):
+        G = nx.Graph()
+        G.add_edges_from([(0, 1), (0, 2), (2, 3)])
+        G.nodes[0]["community"] = 0
+        G.nodes[1]["community"] = 1
+        G.nodes[2]["community"] = 0
+        G.nodes[3]["community"] = 0
+        assert pytest.raises(nx.NodeNotFound, self.func, G, [(0, 4)])
+
     def test_no_common_neighbor(self):
         G = nx.Graph()
         G.add_nodes_from([0, 1])
diff --git a/networkx/algorithms/tests/test_similarity.py b/networkx/algorithms/tests/test_similarity.py
index b7fcf90fa43..420c709c173 100644
--- a/networkx/algorithms/tests/test_similarity.py
+++ b/networkx/algorithms/tests/test_similarity.py
@@ -699,6 +699,16 @@ def test_simrank_max_iterations(self, alg):
         G = nx.cycle_graph(5)
         pytest.raises(nx.ExceededMaxIterations, alg, G, max_iterations=10)
 
+    def test_simrank_source_not_found(self):
+        G = nx.cycle_graph(5)
+        with pytest.raises(nx.NodeNotFound, match="Source node 10 not in G"):
+            nx.simrank_similarity(G, source=10)
+
+    def test_simrank_target_not_found(self):
+        G = nx.cycle_graph(5)
+        with pytest.raises(nx.NodeNotFound, match="Target node 10 not in G"):
+            nx.simrank_similarity(G, target=10)
+
     def test_simrank_between_versions(self):
         G = nx.cycle_graph(5)
         # _python tolerance 1e-4
@@ -819,6 +829,21 @@ def test_panther_similarity_weighted(self):
         sim = nx.panther_similarity(G, "v1", path_length=2, weight="w")
         assert sim == expected
 
+    def test_panther_similarity_source_not_found(self):
+        G = nx.Graph()
+        G.add_edges_from([(0, 1), (0, 2), (0, 3), (1, 2), (2, 4)])
+        with pytest.raises(nx.NodeNotFound, match="Source node 10 not in G"):
+            nx.panther_similarity(G, source=10)
+
+    def test_panther_similarity_isolated(self):
+        G = nx.Graph()
+        G.add_nodes_from(range(5))
+        with pytest.raises(
+            nx.NetworkXUnfeasible,
+            match="Panther similarity is not defined for the isolated source node 1.",
+        ):
+            nx.panther_similarity(G, source=1)
+
     def test_generate_random_paths_unweighted(self):
         np.random.seed(42)
 


Code review comments:
diff_hunk: @@ -1550,6 +1583,18 @@ def panther_similarity(
     """
     import numpy as np
 
+    if source not in G:
+        raise nx.NodeNotFound(f"Source node {source} not in G")
+
+    isolates = list(nx.isolates(G))
path: networkx/algorithms/similarity.py
text: @user1:
I think you only use the isolates by checking membership.
That is O(len(list)) for a list, but O(1) for a set.
I think you don't need them to be ordered. So maybe its better to use a set instead of a list.