Skip to content

Commit

Permalink
adds max_ang_diff param to dissolve
Browse files Browse the repository at this point in the history
  • Loading branch information
songololo committed Oct 18, 2023
1 parent 17d7a17 commit 03ccc0a
Show file tree
Hide file tree
Showing 5 changed files with 208 additions and 65 deletions.
149 changes: 99 additions & 50 deletions demos/graph_corrections.ipynb

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "cityseer"
version = '4.2.2'
version = '4.2.3'
description = "Computational tools for network-based pedestrian-scale urban analysis"
readme = "README.md"
requires-python = ">=3.10, <3.12"
Expand Down
15 changes: 12 additions & 3 deletions pysrc/cityseer/tools/graphs.py
Original file line number Diff line number Diff line change
Expand Up @@ -1352,13 +1352,15 @@ def prepare_dual_node_key(start_nd_key: NodeKey, end_nd_key: NodeKey, edge_idx:
return g_dual


def nx_weight_by_dissolved_edges(nx_multigraph: MultiGraph, dissolve_distance: int = 20) -> MultiGraph:
def nx_weight_by_dissolved_edges(
nx_multigraph: MultiGraph, dissolve_distance: int = 20, max_ang_diff: int = 15
) -> MultiGraph:
"""
Generates graph node weightings based on the ratio of directly adjacent edges to total nearby edges.
This is used to control for unintended amplification of centrality measures where redundant network representations
(e.g. complicated intersections or duplicitious segments, i.e. street, sidewalk, cycleway, busway) tend to inflate
centrality scores. This method is intended for 'messier' network representations (e.g. OSM).
(e.g. duplicitious segments such as adjacent street, sidewalk, cycleway, busway) tend to inflate centrality scores.
This method is intended for 'messier' network representations (e.g. OSM).
Parameters
----------
Expand All @@ -1367,6 +1369,9 @@ def nx_weight_by_dissolved_edges(nx_multigraph: MultiGraph, dissolve_distance: i
edge attributes containing `LineString` geoms.
dissolve_distance: int
A distance to use when buffering edges to calculate the weighting. 20m by default.
max_ang_diff: int
Only count a nearby adjacent edge as duplicitous if the angular difference between edges is less than
`max_ang_diff`. 15 degrees by default.
Returns
-------
Expand Down Expand Up @@ -1402,6 +1407,10 @@ def nx_weight_by_dissolved_edges(nx_multigraph: MultiGraph, dissolve_distance: i
# get linestring
edge_data = g_multi_copy[nearby_start_nd_key][nearby_end_nd_key][nearby_edge_idx]
nearby_edge_geom: geometry.LineString = edge_data["geom"]
# get angular difference
ang_diff = util.measure_angle_diff_betw_linestrings(edge_geom.coords, nearby_edge_geom.coords)
if ang_diff > max_ang_diff:
continue
# find length of geom intersecting buff
edge_itx = nearby_edge_geom.intersection(edge_geom_buff)
if edge_itx and edge_itx.geom_type == "LineString":
Expand Down
44 changes: 33 additions & 11 deletions pysrc/cityseer/tools/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,18 @@ def measure_bearing(xy_1: npt.NDArray[np.float_], xy_2: npt.NDArray[np.float_])
def measure_coords_angle(
coords_1: npt.NDArray[np.float_], coords_2: npt.NDArray[np.float_], coords_3: npt.NDArray[np.float_]
) -> float:
"""Measures angle between three coordinate pairs."""
"""
Measures angle between three coordinate pairs.
Change is from one line segment to the next, so shares middle coord.
"""
# arctan2 is y / x order
a_1: float = measure_bearing(coords_2, coords_1)
a_2: float = measure_bearing(coords_3, coords_2)
angle = np.abs((a_2 - a_1 + 180) % 360 - 180)
# alternative
# A: npt.NDArray[np.float_] = coords_2 - coords_1
# B: npt.NDArray[np.float_] = coords_3 - coords_2
# alt_angle = np.abs(np.degrees(np.math.atan2(np.linalg.det([A, B]), np.dot(A, B))))
return angle


Expand All @@ -64,16 +71,31 @@ def _measure_linestring_angle(linestring_coords: ListCoordsType, idx_a: int, idx
coords_1: npt.NDArray[np.float_] = np.array(linestring_coords[idx_a])[:2]
coords_2: npt.NDArray[np.float_] = np.array(linestring_coords[idx_b])[:2]
coords_3: npt.NDArray[np.float_] = np.array(linestring_coords[idx_c])[:2]
# arctan2 is y / x order
a_1: float = measure_bearing(coords_2, coords_1)
a_2: float = measure_bearing(coords_3, coords_2)
angle = np.abs((a_2 - a_1 + 180) % 360 - 180)
# alternative
# A: npt.NDArray[np.float_] = coords_2 - coords_1
# B: npt.NDArray[np.float_] = coords_3 - coords_2
# alt_angle = np.abs(np.degrees(np.math.atan2(np.linalg.det([A, B]), np.dot(A, B))))

return angle
return measure_coords_angle(coords_1, coords_2, coords_3)


def measure_angle_diff_betw_linestrings(linestring_coords_a: ListCoordsType, linestring_coords_b: ListCoordsType):
"""Measures the angular difference between the bearings of two sets of linestring coords."""

min_angle = np.inf
for flip_a in [True, False]:
_ls_coords_a = linestring_coords_a
if flip_a:
_ls_coords_a = list(reversed(linestring_coords_a))
for flip_b in [True, False]:
_ls_coords_b = linestring_coords_b
if flip_b:
_ls_coords_b = list(reversed(linestring_coords_b))
coords_1 = _ls_coords_a[0]
coords_2 = _ls_coords_a[-1]
coords_3 = _ls_coords_b[0]
coords_4 = _ls_coords_b[-1]
a_1 = measure_bearing(coords_2, coords_1)
a_2 = measure_bearing(coords_4, coords_3)
angle = np.abs((a_2 - a_1 + 180) % 360 - 180)
min_angle = min(min_angle, angle)

return min_angle


def measure_cumulative_angle(linestring_coords: ListCoordsType) -> float:
Expand Down
63 changes: 63 additions & 0 deletions tests/tools/test_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,74 @@
from __future__ import annotations

import pytest
from shapely import geometry

from cityseer.metrics import networks
from cityseer.tools import io, util


def test_measure_bearing():
"""
right is zero, left is 180
up is positive, bottom is negative
"""
assert util.measure_bearing([0, 0], [1, 0]) == 0
assert util.measure_bearing([0, 0], [1, 1]) == 45
assert util.measure_bearing([0, 0], [0, 1]) == 90
assert util.measure_bearing([0, 0], [-1, 0]) == 180
assert util.measure_bearing([0, 0], [-1, -1]) == -135
assert util.measure_bearing([0, 0], [0, -1]) == -90
assert util.measure_bearing([0, 0], [1, -1]) == -45


def test_measure_coords_angle():
""" """
for coord_set, expected_angle in [
([[0, 0], [1, 0], [2, 0]], 0),
([[0, 0], [1, 0], [2, 1]], 45),
([[0, 0], [1, 0], [1, 1]], 90),
([[0, 0], [1, 0], [0, 1]], 135),
]:
assert util.measure_coords_angle(coord_set[0], coord_set[1], coord_set[2]) == expected_angle
# flip order, angle should be the same
assert util.measure_coords_angle(coord_set[2], coord_set[1], coord_set[0]) == expected_angle


def test_measure_linestring_angle():
""" """
for coord_set, expected_angle in [
([[0, 0], [1, 0], [2, 0]], 0),
([[0, 0], [1, 0], [2, 1]], 45),
([[0, 0], [1, 0], [1, 1]], 90),
([[0, 0], [1, 0], [0, 1]], 135),
]:
assert util._measure_linestring_angle(coord_set, 0, 1, 2) == expected_angle
assert util._measure_linestring_angle(coord_set, 2, 1, 0) == expected_angle
# flip
assert util._measure_linestring_angle(list(reversed(coord_set)), 0, 1, 2) == expected_angle
assert util._measure_linestring_angle(list(reversed(coord_set)), 2, 1, 0) == expected_angle


def test_measure_angle_diff_betw_linestrings():
""" """
coords_a = [[0, 0], [1, 0]]
coords_b = [[0, 0], [1, 1]]
coords_c = [[0, 0], [0, 1]]
assert util.measure_angle_diff_betw_linestrings(coords_a, coords_b) == 45
assert util.measure_angle_diff_betw_linestrings(coords_b, coords_a) == 45
#
assert util.measure_angle_diff_betw_linestrings(coords_a, coords_c) == 90
assert util.measure_angle_diff_betw_linestrings(coords_c, coords_a) == 90
#
assert util.measure_angle_diff_betw_linestrings(coords_b, coords_c) == 45
assert util.measure_angle_diff_betw_linestrings(coords_c, coords_b) == 45
# try reversed sets
assert util.measure_angle_diff_betw_linestrings(list(reversed(coords_a)), coords_b) == 45
assert util.measure_angle_diff_betw_linestrings(list(reversed(coords_b)), coords_a) == 45
assert util.measure_angle_diff_betw_linestrings(coords_a, list(reversed(coords_b))) == 45
assert util.measure_angle_diff_betw_linestrings(coords_b, list(reversed(coords_a))) == 45


def test_add_node(diamond_graph):
new_name, is_dupe = util.add_node(diamond_graph, ["0", "1"], 50, 50)
assert is_dupe is False
Expand Down

0 comments on commit 03ccc0a

Please sign in to comment.