diff --git a/docs/src/pages/metrics/layers.md b/docs/src/pages/metrics/layers.md
index 3d8bba61..d7e584af 100644
--- a/docs/src/pages/metrics/layers.md
+++ b/docs/src/pages/metrics/layers.md
@@ -352,7 +352,7 @@ representation of variations of metrics along street-fronts.
- The input `node_gdf` parameter is returned with additional columns populated with the calcualted metrics.
+ The input `node_gdf` parameter is returned with additional columns populated with the calcualted metrics. Three columns will be returned for each input landuse class and distance combination; a simple count of reachable locations, a distance weighted count of reachable locations, and the smallest distance to the nearest location.
@@ -391,6 +391,8 @@ print(nodes_gdf.columns)
print(nodes_gdf["cc_metric_c_400_weighted"])
# non-weighted form
print(nodes_gdf["cc_metric_c_400_non_weighted"])
+# nearest distance to landuse
+print(nodes_gdf["cc_metric_c_400_distance"])
```
diff --git a/pyproject.toml b/pyproject.toml
index ecf590de..fb35c63e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "cityseer"
-version = '4.4.0'
+version = '4.5.0'
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/layers.py b/pysrc/cityseer/metrics/layers.py
index 994d81b5..2059956d 100644
--- a/pysrc/cityseer/metrics/layers.py
+++ b/pysrc/cityseer/metrics/layers.py
@@ -193,7 +193,9 @@ def compute_accessibilities(
Returns
-------
nodes_gdf: GeoDataFrame
- The input `node_gdf` parameter is returned with additional columns populated with the calcualted metrics.
+ The input `node_gdf` parameter is returned with additional columns populated with the calcualted metrics. Three
+ columns will be returned for each input landuse class and distance combination; a simple count of reachable
+ locations, a distance weighted count of reachable locations, and the smallest distance to the nearest location.
data_gdf: GeoDataFrame
The input `data_gdf` is returned with two additional columns: `nearest_assigned` and `next_neareset_assign`.
@@ -223,6 +225,8 @@ def compute_accessibilities(
print(nodes_gdf["cc_metric_c_400_weighted"])
# non-weighted form
print(nodes_gdf["cc_metric_c_400_non_weighted"])
+ # nearest distance to landuse
+ print(nodes_gdf["cc_metric_c_400_distance"])
```
"""
@@ -253,9 +257,11 @@ def compute_accessibilities(
for acc_key in accessibility_keys:
for dist_key in distances:
ac_nw_data_key = config.prep_gdf_key(f"{acc_key}_{dist_key}_non_weighted")
- ac_wt_data_key = config.prep_gdf_key(f"{acc_key}_{dist_key}_weighted")
nodes_gdf[ac_nw_data_key] = result[acc_key].unweighted[dist_key] # type: ignore
+ ac_wt_data_key = config.prep_gdf_key(f"{acc_key}_{dist_key}_weighted")
nodes_gdf[ac_wt_data_key] = result[acc_key].weighted[dist_key] # type: ignore
+ ac_dist_data_key = config.prep_gdf_key(f"{acc_key}_{dist_key}_distance")
+ nodes_gdf[ac_dist_data_key] = result[acc_key].distance[dist_key] # type: ignore
return nodes_gdf, data_gdf
diff --git a/pysrc/cityseer/rustalgos.pyi b/pysrc/cityseer/rustalgos.pyi
index 5a7be8e9..82a5820e 100644
--- a/pysrc/cityseer/rustalgos.pyi
+++ b/pysrc/cityseer/rustalgos.pyi
@@ -538,6 +538,7 @@ def raos_quadratic_diversity(
class AccessibilityResult:
weighted: dict[int, npt.ArrayLike]
unweighted: dict[int, npt.ArrayLike]
+ distance: dict[int, npt.ArrayLike]
class MixedUsesResult:
hill: dict[int, dict[int, npt.ArrayLike]] | None
diff --git a/src/data.rs b/src/data.rs
index d54abc6b..fc118d58 100644
--- a/src/data.rs
+++ b/src/data.rs
@@ -16,6 +16,8 @@ pub struct AccessibilityResult {
weighted: HashMap>>,
#[pyo3(get)]
unweighted: HashMap>>,
+ #[pyo3(get)]
+ distance: HashMap>>,
}
#[pyclass]
pub struct MixedUsesResult {
@@ -328,6 +330,20 @@ impl DataMap {
)
})
.collect();
+ let dists: HashMap = accessibility_keys
+ .clone()
+ .into_iter()
+ .map(|acc_key| {
+ (
+ acc_key,
+ MetricResult::new(
+ distances.clone(),
+ network_structure.node_count(),
+ f32::INFINITY,
+ ),
+ )
+ })
+ .collect();
// indices
let node_indices: Vec = network_structure.node_indices();
// iter
@@ -372,6 +388,12 @@ impl DataMap {
let val_wt = clipped_beta_wt(b, mcw, data_dist);
metrics_wt[&lu_class].metric[i][*netw_src_idx]
.fetch_add(val_wt.unwrap(), Ordering::Relaxed);
+ let current_dist =
+ dists[&lu_class].metric[i][*netw_src_idx].load(Ordering::Relaxed);
+ if data_dist < current_dist {
+ dists[&lu_class].metric[i][*netw_src_idx]
+ .store(data_dist, Ordering::Relaxed);
+ }
}
}
}
@@ -384,6 +406,7 @@ impl DataMap {
AccessibilityResult {
weighted: metrics_wt[acc_key].load(),
unweighted: metrics[acc_key].load(),
+ distance: dists[acc_key].load(),
},
);
}
diff --git a/tests/metrics/test_layers.py b/tests/metrics/test_layers.py
index c23d9480..d46d6650 100644
--- a/tests/metrics/test_layers.py
+++ b/tests/metrics/test_layers.py
@@ -82,6 +82,13 @@ def test_compute_accessibilities(primal_graph):
atol=config.ATOL,
rtol=config.RTOL,
)
+ acc_data_key_dist = config.prep_gdf_key(f"{acc_key}_{dist_key}_distance")
+ assert np.allclose(
+ nodes_gdf[acc_data_key_dist].values,
+ accessibility_data[acc_key].distance[dist_key],
+ atol=config.ATOL,
+ rtol=config.RTOL,
+ )
# most integrity checks happen in underlying method
with pytest.raises(ValueError):
nodes_gdf, data_gdf = layers.compute_accessibilities(
diff --git a/tests/rustalgos/test_data.py b/tests/rustalgos/test_data.py
index 20d82ce1..3b03158f 100644
--- a/tests/rustalgos/test_data.py
+++ b/tests/rustalgos/test_data.py
@@ -46,7 +46,7 @@ def test_aggregate_to_src_idx(primal_graph):
nearest_netw_node = network_structure.get_node_payload(data_entry.nearest_assign)
nearest_assign_dist = tree_map[data_entry.nearest_assign].short_dist
# add tail
- if not np.isinf(nearest_assign_dist):
+ if not np.isposinf(nearest_assign_dist):
nearest_assign_dist += nearest_netw_node.coord.hypot(data_entry.coord)
else:
nearest_assign_dist = np.inf
@@ -55,7 +55,7 @@ def test_aggregate_to_src_idx(primal_graph):
next_nearest_netw_node = network_structure.get_node_payload(data_entry.next_nearest_assign)
next_nearest_assign_dist = tree_map[data_entry.next_nearest_assign].short_dist
# add tail
- if not np.isinf(next_nearest_assign_dist):
+ if not np.isposinf(next_nearest_assign_dist):
next_nearest_assign_dist += next_nearest_netw_node.coord.hypot(data_entry.coord)
else:
next_nearest_assign_dist = np.inf
@@ -65,9 +65,9 @@ def test_aggregate_to_src_idx(primal_graph):
assert data_key not in reachable_entries
elif deduplicate and data_key in ["45", "46", "47", "48"]:
assert data_key not in reachable_entries and "49" in reachable_entries
- elif np.isinf(nearest_assign_dist) and next_nearest_assign_dist < max_dist:
+ elif np.isposinf(nearest_assign_dist) and next_nearest_assign_dist < max_dist:
assert reachable_entries[data_key] - next_nearest_assign_dist < config.ATOL
- elif np.isinf(next_nearest_assign_dist) and nearest_assign_dist < max_dist:
+ elif np.isposinf(next_nearest_assign_dist) and nearest_assign_dist < max_dist:
assert reachable_entries[data_key] - nearest_assign_dist < config.ATOL
else:
assert (
@@ -116,6 +116,10 @@ def test_accessibility(primal_graph):
b_wt = 0
c_wt = 0
z_wt = 0
+ a_dist = np.inf
+ b_dist = np.inf
+ c_dist = np.inf
+ z_dist = np.inf
# iterate reachable
reachable_entries = data_map.aggregate_to_src_idx(src_idx, network_structure, max_dist)
for data_key, data_dist in reachable_entries.items():
@@ -127,15 +131,23 @@ def test_accessibility(primal_graph):
if data_class == "a":
a_nw += 1
a_wt += np.exp(-beta * data_dist)
+ if data_dist < a_dist:
+ a_dist = data_dist
elif data_class == "b":
b_nw += 1
b_wt += np.exp(-beta * data_dist)
+ if data_dist < b_dist:
+ b_dist = data_dist
elif data_class == "c":
c_nw += 1
c_wt += np.exp(-beta * data_dist)
+ if data_dist < c_dist:
+ c_dist = data_dist
elif data_class == "z":
z_nw += 1
z_wt += np.exp(-beta * data_dist)
+ if data_dist < z_dist:
+ z_dist = data_dist
# assertions
assert accessibilities["a"].unweighted[dist][src_idx] - a_nw < config.ATOL
assert accessibilities["b"].unweighted[dist][src_idx] - b_nw < config.ATOL
@@ -145,6 +157,22 @@ def test_accessibility(primal_graph):
assert accessibilities["b"].weighted[dist][src_idx] - b_wt < config.ATOL
assert accessibilities["c"].weighted[dist][src_idx] - c_wt < config.ATOL
assert accessibilities["z"].weighted[dist][src_idx] - z_wt < config.ATOL
+ if np.isfinite(a_dist):
+ assert accessibilities["a"].distance[dist][src_idx] - a_dist < config.ATOL
+ else:
+ assert np.isposinf(a_dist) and np.isposinf(accessibilities["a"].distance[dist][src_idx])
+ if np.isfinite(b_dist):
+ assert accessibilities["b"].distance[dist][src_idx] - b_dist < config.ATOL
+ else:
+ assert np.isposinf(b_dist) and np.isposinf(accessibilities["b"].distance[dist][src_idx])
+ if np.isfinite(c_dist):
+ assert accessibilities["c"].distance[dist][src_idx] - c_dist < config.ATOL
+ else:
+ assert np.isposinf(c_dist) and np.isposinf(accessibilities["c"].distance[dist][src_idx])
+ if np.isfinite(z_dist):
+ assert accessibilities["z"].distance[dist][src_idx] - z_dist < config.ATOL
+ else:
+ assert np.isposinf(z_dist) and np.isposinf(accessibilities["z"].distance[dist][src_idx])
# check for deduplication
assert z_nw in [0, 1]
assert z_wt <= 1