Repository: networkx/networkx

Issue:
Allow edge weights for `harmonic_diameter` 
The harmonic diameter (which is the harmonic mean of all distances) from #5251 uses path lengths for unweighted networks. It would be good to allow weights path lengths via a keyword argument like `weight=None`. This kwarg can have the same name doc_string entry and optional function interface as for the shortest_path functions.   

Discussion:
Raj3110: > This seems like the right idea, but some details still need some work.
> 
> Also - is there a reason to choose one weighted shortest path algorithm (`dijkstra`) over another (e.g. `bellman_ford`)?

I preferred Dijkstra's algorithm over Bellman-Ford for weighted shortest paths due to its better performance, especially in sparse graphs with non-negative weights. However, Bellman-Ford is necessary for graphs with negative weights or to detect negative cycles. Here in NetworkX we used Dijkstra's in functions like `eccentricity` and `barycenter`, assuming non-negative weights for general-purpose graph analysis.
rossbar: Another improvement would be to add tests to ensure the weighted `harmonic_diameter` is correct!
rossbar: Since this didn't make the 3.4 release, I took the liberty of pushing up a signature change to maintain backward compatibility. My approval stands, though testing would be welcome!
dschult: I took the liberty of pushing up some tests.

I think this is now ready. @rossbar can you take a look at the tests?
rossbar: > I think this is now ready. @rossbar can you take a look at the tests?

Tests LGTM, thanks @dschult !


Patch:
diff --git a/networkx/algorithms/distance_measures.py b/networkx/algorithms/distance_measures.py
index 8e15bf8d9..282bd7016 100644
--- a/networkx/algorithms/distance_measures.py
+++ b/networkx/algorithms/distance_measures.py
@@ -387,8 +387,8 @@ def diameter(G, e=None, usebounds=False, weight=None):
     return max(e.values())
 
 
-@nx._dispatchable
-def harmonic_diameter(G, sp=None):
+@nx._dispatchable(edge_attrs="weight")
+def harmonic_diameter(G, sp=None, *, weight=None):
     """Returns the harmonic diameter of the graph G.
 
     The harmonic diameter of a graph is the harmonic mean of the distances
@@ -415,6 +415,15 @@ def harmonic_diameter(G, sp=None):
     sp : dict of dicts, optional
        All-pairs shortest path lengths as a dictionary of dictionaries
 
+    weight : string, function, or None (default=None)
+        If None, every edge has weight/distance 1.
+        If a string, use this edge attribute as the edge weight.
+        Any edge attribute not present defaults to 1.
+        If a function, the weight of an edge is the value returned by the function.
+        The function must accept exactly three positional arguments:
+        the two endpoints of an edge and the dictionary of edge attributes for
+        that edge. The function must return a number.
+
     Returns
     -------
     hd : float
@@ -432,7 +441,7 @@ def harmonic_diameter(G, sp=None):
     sum_invd = 0
     for n in G:
         if sp is None:
-            length = nx.single_source_shortest_path_length(G, n)
+            length = nx.single_source_dijkstra_path_length(G, n, weight=weight)
         else:
             try:
                 length = sp[n]


Test patch:
diff --git a/networkx/algorithms/tests/test_distance_measures.py b/networkx/algorithms/tests/test_distance_measures.py
index 0b3840fd2..6a8e0372b 100644
--- a/networkx/algorithms/tests/test_distance_measures.py
+++ b/networkx/algorithms/tests/test_distance_measures.py
@@ -16,8 +16,7 @@ def test__extrema_bounding_invalid_compute_kwarg():
 
 class TestDistance:
     def setup_method(self):
-        G = cnlti(nx.grid_2d_graph(4, 4), first_label=1, ordering="sorted")
-        self.G = G
+        self.G = cnlti(nx.grid_2d_graph(4, 4), first_label=1, ordering="sorted")
 
     def test_eccentricity(self):
         assert nx.eccentricity(self.G, 1) == 6
@@ -54,7 +53,8 @@ class TestDistance:
         assert nx.diameter(self.G) == 6
 
     def test_harmonic_diameter(self):
-        assert abs(nx.harmonic_diameter(self.G) - 2.0477815699658715) < 1e-12
+        assert nx.harmonic_diameter(self.G) == pytest.approx(2.0477815699658715)
+        assert nx.harmonic_diameter(nx.star_graph(3)) == pytest.approx(1.333333)
 
     def test_harmonic_diameter_empty(self):
         assert math.isnan(nx.harmonic_diameter(nx.empty_graph()))
@@ -70,6 +70,17 @@ class TestDistance:
         DG.add_edge(0, 1)
         assert nx.harmonic_diameter(DG) == 2
 
+    def test_harmonic_diameter_weighted_paths(self):
+        G = nx.star_graph(3)
+        # check defaults
+        G.add_weighted_edges_from([(*e, 1) for i, e in enumerate(G.edges)], "weight")
+        assert nx.harmonic_diameter(G) == pytest.approx(1.333333)
+        assert nx.harmonic_diameter(G, weight="weight") == pytest.approx(1.333333)
+
+        # check impact of weights and alternate weight name
+        G.add_weighted_edges_from([(*e, i) for i, e in enumerate(G.edges)], "dist")
+        assert nx.harmonic_diameter(G, weight="dist") == pytest.approx(1.8)
+
     def test_radius(self):
         assert nx.radius(self.G) == 4