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