diff --git a/docs/src/pages/metrics/networks.md b/docs/src/pages/metrics/networks.md
index 679827a6..9a701442 100644
--- a/docs/src/pages/metrics/networks.md
+++ b/docs/src/pages/metrics/networks.md
@@ -42,8 +42,9 @@ This effect is amplified for denser regions of the network.
- Segmentised versions of centrality measures should not be computed on dual graph topologies because street segment
lengths would be duplicated for each permutation of dual edge spanning street intersections. By way of example,
the contribution of a single edge segment at a four-way intersection would be duplicated three times.
-- Global closeness is strongly discouraged because it does not behave suitably for localised graphs. Harmonic
-closeness should be used instead.
+- The usual formulations of closeness or normalised closeness are discouraged because these do not behave
+suitably for localised graphs. Harmonic closeness or Hillier normalisation (which resembles a simplified form of
+Improved Closeness Centrality proposed by Wasserman and Faust) should be used instead.
- Network decomposition can be a useful strategy when working at small distance thresholds, and confers advantages
such as more regularly spaced snapshots and fewer artefacts at small distance thresholds where street edges
intersect distance thresholds. However, the regular spacing of the decomposed segments will introduce spikes in the
@@ -110,9 +111,7 @@ may therefore be preferable when working at small thresholds on decomposed netwo
Compute node-based network centrality using the shortest path heuristic.
:::note
Node weights are taken into account when computing centralities. These would typically be initialised at 1 unless
-manually specified. Consider use of
-[`graphs.nx_weight_by_dissolved_edges`](/tools/graphs#nx-weight-by-dissolved-edges) when working with complex
-network representations.
+manually specified.
:::
### Parameters
@@ -213,10 +212,11 @@ network representations.
| key | formula | notes |
| ----------------------| :------:| ----- |
| node_density | $$\sum_{j\neq{i}}^{n}1$$ | A summation of nodes. |
-| node_farness | $$\sum_{j\neq{i}}^{n}d_{(i,j)}$$ | A summation of distances in metres. |
-| node_cycles | $$\sum_{j\neq{i}j=cycle}^{n}1$$ | A summation of network cycles. |
-| node_harmonic | $$\sum_{j\neq{i}}^{n}\frac{1}{Z_{(i,j)}}$$ | Harmonic closeness is an appropriate form of closeness centrality for localised implementations constrained by the threshold $d_{max}$. |
+| node_harmonic | $$\sum_{j\neq{i}}^{n}\frac{1}{d_{(i,j)}}$$ | Harmonic closeness is an appropriate form of closeness centrality for localised implementations constrained by the threshold $d_{max}$. |
+| node_hillier | $$\frac{(n-1)^2}{\sum_{j \neq i}^{n} d_{(i,j)}}$$ | The square of node density divided by farness. This is also a simplified form of Improved Closeness Centrality. |
| node_beta | $$\sum_{j\neq{i}}^{n} \\ \exp(-\beta\cdot d[i,j])$$ | Also known as the gravity index. This is a spatial impedance metric differentiated from other closeness centralities by the use of an explicit $\beta$ parameter, which can be used to model the decay in walking tolerance as distances increase. |
+| node_cycles | $$\sum_{j\neq{i}j=cycle}^{n}1$$ | A summation of network cycles. |
+| node_farness | $$\sum_{j\neq{i}}^{n}d_{(i,j)}$$ | A summation of distances in metres. |
| node_betweenness | $$\sum_{j\neq{i}}^{n}\sum_{k\neq{j}\neq{i}}^{n}1$$ | Betweenness centrality summing all shortest-paths traversing each node $i$. |
| node_betweenness_beta | $$\sum_{j\neq{i}}^{n}\sum_{k\neq{j}\neq{i}}^{n} \\ \exp(-\beta\cdot d[j,k])$$ | Applies a spatial impedance decay function to betweenness centrality. $d$ represents the full distance from any $j$ to $k$ node pair passing through node $i$. |
@@ -281,9 +281,7 @@ network representations.
Compute node-based network centrality using the simplest path (angular) heuristic.
:::note
Node weights are taken into account when computing centralities. These would typically be initialised at 1 unless
-manually specified. Consider use of
-[`graphs.nx_weight_by_dissolved_edges`](/tools/graphs#nx-weight-by-dissolved-edges) when working with complex
-network representations.
+manually specified.
:::
### Parameters
@@ -383,7 +381,10 @@ network representations.
| key | formula | notes |
| ----------------------| :------:| ----- |
-| node_harmonic_simplest | $$\sum_{j\neq{i}}^{n}\frac{1}{Z_{(i,j)}}$$ | Harmonic closeness is an appropriate form of closeness centrality for localised implementations constrained by the threshold $d_{max}$. |
+| node_density_simplest | $$\sum_{j\neq{i}}^{n}1$$ | A summation of nodes. |
+| node_harmonic_simplest | $$\sum_{j\neq{i}}^{n}\frac{1}{d_{(i,j)}}$$ | Harmonic closeness is an appropriate form of closeness centrality for localised implementations constrained by the threshold $d_{max}$. |
+| node_hillier_simplest | $$\frac{(n-1)^2}{\sum_{j \neq i}^{n} d_{(i,j)}}$$ | The square of node density divided by farness. This is also a simplified form of Improved Closeness Centrality. |
+| node_farness_simplest | $$\sum_{j\neq{i}}^{n}d_{(i,j)}$$ | A summation of distances in metres. |
| node_betweenness_simplest | $$\sum_{j\neq{i}}^{n}\sum_{k\neq{j}\neq{i}}^{n}1$$ | Betweenness centrality summing all shortest-paths traversing each node $i$. |
The following keys use the simplest-path (shortest-angular-path) heuristic, and are available when the `angular` parameter is explicitly set to `True`:
diff --git a/docs/src/pages/rustalgos/rustalgos.md b/docs/src/pages/rustalgos/rustalgos.md
index 9c1c29c3..e72de21a 100644
--- a/docs/src/pages/rustalgos/rustalgos.md
+++ b/docs/src/pages/rustalgos/rustalgos.md
@@ -2726,7 +2726,7 @@ datapoints are not located with high spatial precision.
-node_xs: list[float]
+node_lives: list[bool]
@@ -2741,7 +2741,7 @@ datapoints are not located with high spatial precision.
-node_lives: list[bool]
+node_xs: list[float]
diff --git a/pdm.lock b/pdm.lock
index 2c15e697..b3e47408 100644
--- a/pdm.lock
+++ b/pdm.lock
@@ -4,7 +4,7 @@
[metadata]
groups = ["default", "dev"]
strategy = ["cross_platform"]
-lock_version = "4.4"
+lock_version = "4.4.1"
content_hash = "sha256:b29b9a0e2bc6aadd9d9f82a1d0ebaf99a11f37ca18da1d999a8e921cc6182f31"
[[package]]
@@ -465,32 +465,32 @@ files = [
[[package]]
name = "coverage"
-version = "7.3.3"
+version = "7.3.4"
requires_python = ">=3.8"
summary = "Code coverage measurement for Python"
files = [
- {file = "coverage-7.3.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d874434e0cb7b90f7af2b6e3309b0733cde8ec1476eb47db148ed7deeb2a9494"},
- {file = "coverage-7.3.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ee6621dccce8af666b8c4651f9f43467bfbf409607c604b840b78f4ff3619aeb"},
- {file = "coverage-7.3.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1367aa411afb4431ab58fd7ee102adb2665894d047c490649e86219327183134"},
- {file = "coverage-7.3.3-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1f0f8f0c497eb9c9f18f21de0750c8d8b4b9c7000b43996a094290b59d0e7523"},
- {file = "coverage-7.3.3-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db0338c4b0951d93d547e0ff8d8ea340fecf5885f5b00b23be5aa99549e14cfd"},
- {file = "coverage-7.3.3-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:d31650d313bd90d027f4be7663dfa2241079edd780b56ac416b56eebe0a21aab"},
- {file = "coverage-7.3.3-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:9437a4074b43c177c92c96d051957592afd85ba00d3e92002c8ef45ee75df438"},
- {file = "coverage-7.3.3-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:9e17d9cb06c13b4f2ef570355fa45797d10f19ca71395910b249e3f77942a837"},
- {file = "coverage-7.3.3-cp310-cp310-win32.whl", hash = "sha256:eee5e741b43ea1b49d98ab6e40f7e299e97715af2488d1c77a90de4a663a86e2"},
- {file = "coverage-7.3.3-cp310-cp310-win_amd64.whl", hash = "sha256:593efa42160c15c59ee9b66c5f27a453ed3968718e6e58431cdfb2d50d5ad284"},
- {file = "coverage-7.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8c944cf1775235c0857829c275c777a2c3e33032e544bcef614036f337ac37bb"},
- {file = "coverage-7.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:eda7f6e92358ac9e1717ce1f0377ed2b9320cea070906ece4e5c11d172a45a39"},
- {file = "coverage-7.3.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c854c1d2c7d3e47f7120b560d1a30c1ca221e207439608d27bc4d08fd4aeae8"},
- {file = "coverage-7.3.3-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:222b038f08a7ebed1e4e78ccf3c09a1ca4ac3da16de983e66520973443b546bc"},
- {file = "coverage-7.3.3-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff4800783d85bff132f2cc7d007426ec698cdce08c3062c8d501ad3f4ea3d16c"},
- {file = "coverage-7.3.3-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fc200cec654311ca2c3f5ab3ce2220521b3d4732f68e1b1e79bef8fcfc1f2b97"},
- {file = "coverage-7.3.3-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:307aecb65bb77cbfebf2eb6e12009e9034d050c6c69d8a5f3f737b329f4f15fb"},
- {file = "coverage-7.3.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:ffb0eacbadb705c0a6969b0adf468f126b064f3362411df95f6d4f31c40d31c1"},
- {file = "coverage-7.3.3-cp311-cp311-win32.whl", hash = "sha256:79c32f875fd7c0ed8d642b221cf81feba98183d2ff14d1f37a1bbce6b0347d9f"},
- {file = "coverage-7.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:243576944f7c1a1205e5cd658533a50eba662c74f9be4c050d51c69bd4532936"},
- {file = "coverage-7.3.3-pp38.pp39.pp310-none-any.whl", hash = "sha256:d299d379b676812e142fb57662a8d0d810b859421412b4d7af996154c00c31bb"},
- {file = "coverage-7.3.3.tar.gz", hash = "sha256:df04c64e58df96b4427db8d0559e95e2df3138c9916c96f9f6a4dd220db2fdb7"},
+ {file = "coverage-7.3.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:aff2bd3d585969cc4486bfc69655e862028b689404563e6b549e6a8244f226df"},
+ {file = "coverage-7.3.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e4353923f38d752ecfbd3f1f20bf7a3546993ae5ecd7c07fd2f25d40b4e54571"},
+ {file = "coverage-7.3.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ea473c37872f0159294f7073f3fa72f68b03a129799f3533b2bb44d5e9fa4f82"},
+ {file = "coverage-7.3.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5214362abf26e254d749fc0c18af4c57b532a4bfde1a057565616dd3b8d7cc94"},
+ {file = "coverage-7.3.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f99b7d3f7a7adfa3d11e3a48d1a91bb65739555dd6a0d3fa68aa5852d962e5b1"},
+ {file = "coverage-7.3.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:74397a1263275bea9d736572d4cf338efaade2de9ff759f9c26bcdceb383bb49"},
+ {file = "coverage-7.3.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:f154bd866318185ef5865ace5be3ac047b6d1cc0aeecf53bf83fe846f4384d5d"},
+ {file = "coverage-7.3.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e0d84099ea7cba9ff467f9c6f747e3fc3906e2aadac1ce7b41add72e8d0a3712"},
+ {file = "coverage-7.3.4-cp310-cp310-win32.whl", hash = "sha256:3f477fb8a56e0c603587b8278d9dbd32e54bcc2922d62405f65574bd76eba78a"},
+ {file = "coverage-7.3.4-cp310-cp310-win_amd64.whl", hash = "sha256:c75738ce13d257efbb6633a049fb2ed8e87e2e6c2e906c52d1093a4d08d67c6b"},
+ {file = "coverage-7.3.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:997aa14b3e014339d8101b9886063c5d06238848905d9ad6c6eabe533440a9a7"},
+ {file = "coverage-7.3.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:8a9c5bc5db3eb4cd55ecb8397d8e9b70247904f8eca718cc53c12dcc98e59fc8"},
+ {file = "coverage-7.3.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27ee94f088397d1feea3cb524e4313ff0410ead7d968029ecc4bc5a7e1d34fbf"},
+ {file = "coverage-7.3.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ce03e25e18dd9bf44723e83bc202114817f3367789052dc9e5b5c79f40cf59d"},
+ {file = "coverage-7.3.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:85072e99474d894e5df582faec04abe137b28972d5e466999bc64fc37f564a03"},
+ {file = "coverage-7.3.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a877810ef918d0d345b783fc569608804f3ed2507bf32f14f652e4eaf5d8f8d0"},
+ {file = "coverage-7.3.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:9ac17b94ab4ca66cf803f2b22d47e392f0977f9da838bf71d1f0db6c32893cb9"},
+ {file = "coverage-7.3.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:36d75ef2acab74dc948d0b537ef021306796da551e8ac8b467810911000af66a"},
+ {file = "coverage-7.3.4-cp311-cp311-win32.whl", hash = "sha256:47ee56c2cd445ea35a8cc3ad5c8134cb9bece3a5cb50bb8265514208d0a65928"},
+ {file = "coverage-7.3.4-cp311-cp311-win_amd64.whl", hash = "sha256:11ab62d0ce5d9324915726f611f511a761efcca970bd49d876cf831b4de65be5"},
+ {file = "coverage-7.3.4-pp38.pp39.pp310-none-any.whl", hash = "sha256:b1e0f25ae99cf247abfb3f0fac7ae25739e4cd96bf1afa3537827c576b4847e5"},
+ {file = "coverage-7.3.4.tar.gz", hash = "sha256:020d56d2da5bc22a0e00a5b0d54597ee91ad72446fa4cf1b97c35022f6b6dbf0"},
]
[[package]]
@@ -1671,7 +1671,7 @@ files = [
[[package]]
name = "pandas-stubs"
-version = "2.1.1.230928"
+version = "2.1.4.231218"
requires_python = ">=3.9"
summary = "Type annotations for pandas"
dependencies = [
@@ -1679,8 +1679,8 @@ dependencies = [
"types-pytz>=2022.1.1",
]
files = [
- {file = "pandas_stubs-2.1.1.230928-py3-none-any.whl", hash = "sha256:992d97159e054ca3175ebe8321ac5616cf6502dd8218b03bb0eaf3c4f6939037"},
- {file = "pandas_stubs-2.1.1.230928.tar.gz", hash = "sha256:ce1691c71c5d67b8f332da87763f7f54650f46895d99964d588c3a5d79e2cacc"},
+ {file = "pandas_stubs-2.1.4.231218-py3-none-any.whl", hash = "sha256:9c9a8db37b83ff4ff9f672644099abc624ed407aa46d9dcb5f305de9925b3d29"},
+ {file = "pandas_stubs-2.1.4.231218.tar.gz", hash = "sha256:f0dd07b3bb2935ddcff9c7b7ba9076cf3529b968a0dee96fab53f5f8747974f7"},
]
[[package]]
@@ -1786,12 +1786,12 @@ files = [
[[package]]
name = "pip"
-version = "23.3.1"
+version = "23.3.2"
requires_python = ">=3.7"
summary = "The PyPA recommended tool for installing Python packages."
files = [
- {file = "pip-23.3.1-py3-none-any.whl", hash = "sha256:55eb67bb6171d37447e82213be585b75fe2b12b359e993773aca4de9247a052b"},
- {file = "pip-23.3.1.tar.gz", hash = "sha256:1fcaa041308d01f14575f6d0d2ea4b75a3e2871fe4f9c694976f908768e14174"},
+ {file = "pip-23.3.2-py3-none-any.whl", hash = "sha256:5052d7889c1f9d05224cd41741acb7c5d6fa735ab34e339624a614eaaa7e7d76"},
+ {file = "pip-23.3.2.tar.gz", hash = "sha256:7fd9972f96db22c8077a1ee2691b172c8089b17a5652a44494a9ecb0d78f9149"},
]
[[package]]
@@ -1962,15 +1962,15 @@ files = [
[[package]]
name = "pyright"
-version = "1.1.341"
+version = "1.1.342"
requires_python = ">=3.7"
summary = "Command line wrapper for pyright"
dependencies = [
"nodeenv>=1.6.0",
]
files = [
- {file = "pyright-1.1.341-py3-none-any.whl", hash = "sha256:f5800daf9d5780ebf6c6e04064a6d20da99c0ef16efd77526f83cc8d8551ff9f"},
- {file = "pyright-1.1.341.tar.gz", hash = "sha256:b891721f3abd10635cc4fd3076bcff5b7676567dc3a629997ed59a0d30034a87"},
+ {file = "pyright-1.1.342-py3-none-any.whl", hash = "sha256:9a7b95b3a2a90ef7b4297221173956f7f2db2a6cf4f0c7493f2a2349d38305fc"},
+ {file = "pyright-1.1.342.tar.gz", hash = "sha256:c8e9785b9080c1aaf2a2efad706249ec65e15abd5f542ee455956acfd404d273"},
]
[[package]]
diff --git a/pyproject.toml b/pyproject.toml
index 00b318d1..950f49e7 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "cityseer"
-version = '4.6.7'
+version = '4.6.8'
description = "Computational tools for network-based pedestrian-scale urban analysis"
readme = "README.md"
requires-python = ">=3.10, <3.12"
diff --git a/pysrc/cityseer/metrics/networks.py b/pysrc/cityseer/metrics/networks.py
index c50082cf..bce40074 100644
--- a/pysrc/cityseer/metrics/networks.py
+++ b/pysrc/cityseer/metrics/networks.py
@@ -40,8 +40,9 @@
- Segmentised versions of centrality measures should not be computed on dual graph topologies because street segment
lengths would be duplicated for each permutation of dual edge spanning street intersections. By way of example,
the contribution of a single edge segment at a four-way intersection would be duplicated three times.
-- Global closeness is strongly discouraged because it does not behave suitably for localised graphs. Harmonic
-closeness should be used instead.
+- The usual formulations of closeness or normalised closeness are discouraged because these do not behave
+suitably for localised graphs. Harmonic closeness or Hillier normalisation (which resembles a simplified form of
+Improved Closeness Centrality proposed by Wasserman and Faust) should be used instead.
- Network decomposition can be a useful strategy when working at small distance thresholds, and confers advantages
such as more regularly spaced snapshots and fewer artefacts at small distance thresholds where street edges
intersect distance thresholds. However, the regular spacing of the decomposed segments will introduce spikes in the
@@ -87,9 +88,7 @@ def node_centrality_shortest(
:::note
Node weights are taken into account when computing centralities. These would typically be initialised at 1 unless
- manually specified. Consider use of
- [`graphs.nx_weight_by_dissolved_edges`](/tools/graphs#nx-weight-by-dissolved-edges) when working with complex
- network representations.
+ manually specified.
:::
Parameters
@@ -138,14 +137,16 @@ def node_centrality_shortest(
| key | formula | notes |
| ----------------------| :------:| ----- |
| node_density | $$\sum_{j\neq{i}}^{n}1$$ | A summation of nodes. |
- | node_farness | $$\sum_{j\neq{i}}^{n}d_{(i,j)}$$ | A summation of distances in metres. |
- | node_cycles | $$\sum_{j\neq{i}j=cycle}^{n}1$$ | A summation of network cycles. |
- | node_harmonic | $$\sum_{j\neq{i}}^{n}\frac{1}{Z_{(i,j)}}$$ | Harmonic closeness is an appropriate form
+ | node_harmonic | $$\sum_{j\neq{i}}^{n}\frac{1}{d_{(i,j)}}$$ | Harmonic closeness is an appropriate form
of closeness centrality for localised implementations constrained by the threshold $d_{max}$. |
+ | node_hillier | $$\frac{(n-1)^2}{\sum_{j \neq i}^{n} d_{(i,j)}}$$ | The square of node density divided by
+ farness. This is also a simplified form of Improved Closeness Centrality. |
| node_beta | $$\sum_{j\neq{i}}^{n} \\ \exp(-\beta\cdot d[i,j])$$ | Also known as the gravity index.
This is a spatial impedance metric differentiated from other closeness centralities by the use of an
explicit $\beta$ parameter, which can be used to model the decay in walking tolerance as distances
increase. |
+ | node_cycles | $$\sum_{j\neq{i}j=cycle}^{n}1$$ | A summation of network cycles. |
+ | node_farness | $$\sum_{j\neq{i}}^{n}d_{(i,j)}$$ | A summation of distances in metres. |
| node_betweenness | $$\sum_{j\neq{i}}^{n}\sum_{k\neq{j}\neq{i}}^{n}1$$ | Betweenness centrality summing all
shortest-paths traversing each node $i$. |
| node_betweenness_beta | $$\sum_{j\neq{i}}^{n}\sum_{k\neq{j}\neq{i}}^{n} \\ \exp(-\beta\cdot d[j,k])$$ | Applies a
@@ -175,6 +176,9 @@ def node_centrality_shortest(
for distance in distances: # type: ignore
data_key = config.prep_gdf_key(f"{measure_name}_{distance}")
nodes_gdf[data_key] = getattr(result, measure_name)[distance]
+ for distance in distances: # type: ignore
+ data_key = config.prep_gdf_key(f"node_hillier_{distance}")
+ nodes_gdf[data_key] = result.node_density[distance] / result.node_farness[distance] # type: ignore
if compute_betweenness is True:
for measure_name in ["node_betweenness", "node_betweenness_beta"]:
for distance in distances: # type: ignore
@@ -198,9 +202,7 @@ def node_centrality_simplest(
:::note
Node weights are taken into account when computing centralities. These would typically be initialised at 1 unless
- manually specified. Consider use of
- [`graphs.nx_weight_by_dissolved_edges`](/tools/graphs#nx-weight-by-dissolved-edges) when working with complex
- network representations.
+ manually specified.
:::
Parameters
@@ -248,8 +250,12 @@ def node_centrality_simplest(
| key | formula | notes |
| ----------------------| :------:| ----- |
- | node_harmonic_simplest | $$\sum_{j\neq{i}}^{n}\frac{1}{Z_{(i,j)}}$$ | Harmonic closeness is an appropriate form
+ | node_density_simplest | $$\sum_{j\neq{i}}^{n}1$$ | A summation of nodes. |
+ | node_harmonic_simplest | $$\sum_{j\neq{i}}^{n}\frac{1}{d_{(i,j)}}$$ | Harmonic closeness is an appropriate form
of closeness centrality for localised implementations constrained by the threshold $d_{max}$. |
+ | node_hillier_simplest | $$\frac{(n-1)^2}{\sum_{j \neq i}^{n} d_{(i,j)}}$$ | The square of node density divided by
+ farness. This is also a simplified form of Improved Closeness Centrality. |
+ | node_farness_simplest | $$\sum_{j\neq{i}}^{n}d_{(i,j)}$$ | A summation of distances in metres. |
| node_betweenness_simplest | $$\sum_{j\neq{i}}^{n}\sum_{k\neq{j}\neq{i}}^{n}1$$ | Betweenness centrality summing
all shortest-paths traversing each node $i$. |
@@ -273,9 +279,18 @@ def node_centrality_simplest(
total=network_structure.node_count(), rust_struct=network_structure, partial_func=partial_func
)
if compute_closeness is True:
+ for distance in distances: # type: ignore
+ data_key = config.prep_gdf_key(f"node_density_simplest_{distance}")
+ nodes_gdf[data_key] = result.node_density[distance] # type: ignore
for distance in distances: # type: ignore
data_key = config.prep_gdf_key(f"node_harmonic_simplest_{distance}")
nodes_gdf[data_key] = result.node_harmonic[distance] # type: ignore
+ for distance in distances: # type: ignore
+ data_key = config.prep_gdf_key(f"node_hillier_simplest_{distance}")
+ nodes_gdf[data_key] = result.node_density[distance] / result.node_farness[distance] # type: ignore
+ for distance in distances: # type: ignore
+ data_key = config.prep_gdf_key(f"node_farness_simplest_{distance}")
+ nodes_gdf[data_key] = result.node_farness[distance] # type: ignore
if compute_betweenness is True:
for distance in distances: # type: ignore
data_key = config.prep_gdf_key(f"node_betweenness_simplest_{distance}")
diff --git a/pysrc/cityseer/rustalgos.pyi b/pysrc/cityseer/rustalgos.pyi
index 82a5820e..3fdbb968 100644
--- a/pysrc/cityseer/rustalgos.pyi
+++ b/pysrc/cityseer/rustalgos.pyi
@@ -369,6 +369,8 @@ class CentralityShortestResult:
node_betweenness_beta: dict[int, npt.ArrayLike]
class CentralitySimplestResult:
+ node_density: dict[int, npt.ArrayLike]
+ node_farness: dict[int, npt.ArrayLike]
node_harmonic: dict[int, npt.ArrayLike]
node_betweenness: dict[int, npt.ArrayLike]
diff --git a/src/centrality.rs b/src/centrality.rs
index cbfdf267..14e55fe7 100644
--- a/src/centrality.rs
+++ b/src/centrality.rs
@@ -31,6 +31,10 @@ pub struct CentralityShortestResult {
}
#[pyclass]
pub struct CentralitySimplestResult {
+ #[pyo3(get)]
+ node_density: Option>>>,
+ #[pyo3(get)]
+ node_farness: Option>>>,
#[pyo3(get)]
node_harmonic: Option>>>,
#[pyo3(get)]
@@ -408,6 +412,8 @@ impl NetworkStructure {
// iter
let result = py.allow_threads(move || {
// metrics
+ let node_density = MetricResult::new(distances.clone(), self.graph.node_count(), 0.0);
+ let node_farness = MetricResult::new(distances.clone(), self.graph.node_count(), 0.0);
let node_harmonic = MetricResult::new(distances.clone(), self.graph.node_count(), 0.0);
let node_betweenness =
MetricResult::new(distances.clone(), self.graph.node_count(), 0.0);
@@ -439,6 +445,10 @@ impl NetworkStructure {
let distance = distances[i];
if node_visit.short_dist <= distance as f32 {
let ang = 1.0 + (node_visit.simpl_dist / 180.0);
+ node_density.metric[i][*src_idx]
+ .fetch_add(1.0 * wt, Ordering::Relaxed);
+ node_farness.metric[i][*src_idx]
+ .fetch_add(ang * wt, Ordering::Relaxed);
node_harmonic.metric[i][*src_idx]
.fetch_add((1.0 / ang) * wt, Ordering::Relaxed);
}
@@ -466,6 +476,16 @@ impl NetworkStructure {
}
});
CentralitySimplestResult {
+ node_density: if compute_closeness {
+ Some(node_density.load())
+ } else {
+ None
+ },
+ node_farness: if compute_closeness {
+ Some(node_farness.load())
+ } else {
+ None
+ },
node_harmonic: if compute_closeness {
Some(node_harmonic.load())
} else {
diff --git a/tests/metrics/test_networks.py b/tests/metrics/test_networks.py
index 3010ad23..e0b01133 100644
--- a/tests/metrics/test_networks.py
+++ b/tests/metrics/test_networks.py
@@ -37,11 +37,28 @@ def test_node_centrality_shortest(primal_graph):
if _closeness is True:
for measure_name in ["node_beta", "node_cycles", "node_density", "node_farness", "node_harmonic"]:
data_key = config.prep_gdf_key(f"{measure_name}_{distance}")
- assert np.allclose(nodes_gdf[data_key], getattr(node_result_short, measure_name)[distance])
+ assert np.allclose(
+ nodes_gdf[data_key],
+ getattr(node_result_short, measure_name)[distance],
+ atol=config.ATOL,
+ rtol=config.RTOL,
+ )
+ assert np.allclose(
+ nodes_gdf[config.prep_gdf_key(f"node_hillier_{distance}")],
+ node_result_short.node_density[distance] / node_result_short.node_farness[distance],
+ equal_nan=True,
+ atol=config.ATOL,
+ rtol=config.RTOL,
+ )
if _betweenness is True:
for measure_name in ["node_betweenness", "node_betweenness_beta"]:
data_key = config.prep_gdf_key(f"{measure_name}_{distance}")
- assert np.allclose(nodes_gdf[data_key], getattr(node_result_short, measure_name)[distance])
+ assert np.allclose(
+ nodes_gdf[data_key],
+ getattr(node_result_short, measure_name)[distance],
+ atol=config.ATOL,
+ rtol=config.RTOL,
+ )
def test_node_centrality_simplest(primal_graph):
@@ -70,22 +87,42 @@ def test_node_centrality_simplest(primal_graph):
)
for distance in distances:
if _closeness is True:
- data_key = config.prep_gdf_key(f"node_harmonic_simplest_{distance}")
assert np.allclose(
- nodes_gdf[data_key],
+ nodes_gdf[config.prep_gdf_key(f"node_density_simplest_{distance}")],
+ node_result_simplest.node_density[distance],
+ equal_nan=True,
+ atol=config.ATOL,
+ rtol=config.RTOL,
+ )
+ assert np.allclose(
+ nodes_gdf[config.prep_gdf_key(f"node_farness_simplest_{distance}")],
+ node_result_simplest.node_farness[distance],
+ equal_nan=True,
+ atol=config.ATOL,
+ rtol=config.RTOL,
+ )
+ assert np.allclose(
+ nodes_gdf[config.prep_gdf_key(f"node_hillier_simplest_{distance}")],
+ node_result_simplest.node_density[distance] / node_result_simplest.node_farness[distance],
+ equal_nan=True,
+ atol=config.ATOL,
+ rtol=config.RTOL,
+ )
+ assert np.allclose(
+ nodes_gdf[config.prep_gdf_key(f"node_harmonic_simplest_{distance}")],
node_result_simplest.node_harmonic[distance],
+ equal_nan=True,
atol=config.ATOL,
rtol=config.RTOL,
)
if _betweenness is True:
- for measure_name in ["node_betweenness_simplest"]:
- data_key = config.prep_gdf_key(f"node_betweenness_simplest_{distance}")
- assert np.allclose(
- nodes_gdf[data_key],
- node_result_simplest.node_betweenness[distance],
- atol=config.ATOL,
- rtol=config.RTOL,
- )
+ assert np.allclose(
+ nodes_gdf[config.prep_gdf_key(f"node_betweenness_simplest_{distance}")],
+ node_result_simplest.node_betweenness[distance],
+ equal_nan=True,
+ atol=config.ATOL,
+ rtol=config.RTOL,
+ )
def test_segment_centrality(primal_graph):
diff --git a/tests/rustalgos/test_centrality.py b/tests/rustalgos/test_centrality.py
index 939cb2d4..e7091486 100644
--- a/tests/rustalgos/test_centrality.py
+++ b/tests/rustalgos/test_centrality.py
@@ -390,7 +390,6 @@ def test_local_node_centrality_shortest(primal_graph):
betw_wt: npt.NDArray[np.float32] = np.full((d_n, n_nodes), 0.0, dtype=np.float32)
dens: npt.NDArray[np.float32] = np.full((d_n, n_nodes), 0.0, dtype=np.float32)
far_short_dist: npt.NDArray[np.float32] = np.full((d_n, n_nodes), 0.0, dtype=np.float32)
- far_simpl_dist: npt.NDArray[np.float32] = np.full((d_n, n_nodes), 0.0, dtype=np.float32)
harmonic_cl: npt.NDArray[np.float32] = np.full((d_n, n_nodes), 0.0, dtype=np.float32)
grav: npt.NDArray[np.float32] = np.full((d_n, n_nodes), 0.0, dtype=np.float32)
cyc: npt.NDArray[np.float32] = np.full((d_n, n_nodes), 0.0, dtype=np.float32)
@@ -408,7 +407,7 @@ def test_local_node_centrality_shortest(primal_graph):
# continue if exceeds max
if np.isinf(to_short_dist):
continue
- for d_idx in range(len(distances)):
+ for d_idx, _ in enumerate(distances):
dist_cutoff = distances[d_idx]
beta = betas[d_idx]
if to_short_dist <= dist_cutoff:
@@ -417,7 +416,6 @@ def test_local_node_centrality_shortest(primal_graph):
# aggregate values
dens[d_idx][src_idx] += 1
far_short_dist[d_idx][src_idx] += to_short_dist
- far_simpl_dist[d_idx][src_idx] += to_simpl_dist
harmonic_cl[d_idx][src_idx] += 1 / to_short_dist
grav[d_idx][src_idx] += np.exp(-beta * to_short_dist)
# cycles
@@ -605,6 +603,17 @@ def test_local_centrality_all(diamond_graph):
compute_closeness=True,
compute_betweenness=True,
)
+ # node density
+ # additive nodes
+ assert np.allclose(node_result_simplest.node_density[50], [0, 0, 0, 0], atol=config.ATOL, rtol=config.RTOL)
+ assert np.allclose(node_result_simplest.node_density[150], [2, 3, 3, 2], atol=config.ATOL, rtol=config.RTOL)
+ assert np.allclose(node_result_simplest.node_density[250], [3, 3, 3, 3], atol=config.ATOL, rtol=config.RTOL)
+ # node farness
+ # additive angular distances
+ # additive 1 + (angle / 180)
+ assert np.allclose(node_result_simplest.node_farness[50], [0, 0, 0, 0], atol=config.ATOL, rtol=config.RTOL)
+ assert np.allclose(node_result_simplest.node_farness[150], [2, 3, 3, 2], atol=config.ATOL, rtol=config.RTOL)
+ assert np.allclose(node_result_simplest.node_farness[250], [3.333, 3, 3, 3.333], atol=config.ATOL, rtol=config.RTOL)
# node harmonic angular
# additive 1 / (1 + (to_imp / 180))
assert np.allclose(node_result_simplest.node_harmonic[50], [0, 0, 0, 0], atol=config.ATOL, rtol=config.RTOL)