Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions superblockify/attribute.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,3 +232,56 @@ def determine_minmax_val(graph, minmax_val, attr, attr_type="edge"):
f"but the first value must be smaller than the second."
)
return minmax_val


def aggregate_edge_attr(graph, key, func, dismiss_none=True):
"""Aggregate edge attributes by function.

Parameters
----------
graph : networkx.Graph
Input graph, subgraph or view
key : immutable
Edge attribute key
func : function
Function to aggregate values. Able to handle lists.
dismiss_none : bool, optional
If True, dismiss None values. Default: True

Returns
-------
dict
Dictionary of aggregated edge attributes

Raises
------
KeyError
If there are no edge attributes for the given key.

Examples
--------
>>> G = nx.Graph()
>>> G.add_edge(1, 2, weight=1)
>>> G.add_edge(2, 3, weight=2)
>>> G.add_edge(3, 4, weight=3)
>>> G.add_edge(4, 1, weight=None)
>>> aggregate_edge_attr(G, 'weight', sum)
6
>>> aggregate_edge_attr(G, 'weight', lambda x: sum(x)/len(x))
2.0
>>> aggregate_edge_attr(G, 'weight', lambda x: x)
[1, 2, 3]
"""
# Get edge attributes
edge_attr = get_edge_attributes(graph, key)
# Check if there are any
if not bool(edge_attr):
raise KeyError(
f"Graph with {len(graph)} node(s) has no edge attributes for "
f"the key '{key}'."
)
# Dismiss None values
if dismiss_none:
edge_attr = {k: v for k, v in edge_attr.items() if v is not None}
# Aggregate
return func(list(edge_attr.values()))
69 changes: 68 additions & 1 deletion superblockify/metrics/measures.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@
from numba import njit, prange, int32, int64, float32, float64
from numpy import sum as npsum

from ..attribute import aggregate_edge_attr
from ..config import logger
from ..utils import __edges_to_1d, __edge_to_1d
from ..utils import __edges_to_1d, __edge_to_1d, percentual_increase


def calculate_directness(distance_matrix, measure1, measure2):
Expand Down Expand Up @@ -736,3 +737,69 @@ def __calculate_high_bc_anisotropy(coord_high_bc):
eigvals = np.sort(eigvals)[::-1]
# Anisotropy
return eigvals[0] / eigvals[1]


def add_ltn_means(components, edge_attr):
"""Add mean of attributes to each LTN.

Writes the mean of the specified edge attribute(s) to each LTN in the list of
components. The mean is calculated as the mean of each attribute in the LTN
subgraph.
Works in-place and adds `mean_{attr}` to each LTN.

Parameters
----------
components : list of dict
List of dictionaries of LTN components.
edge_attr : key or list of keys
Edge attribute(s) to calculate the mean of.
"""
# Loop over LTNs
for component in components:
# Loop over attributes
for attr in edge_attr if isinstance(edge_attr, list) else [edge_attr]:
# Calculate mean
component[f"mean_{attr}"] = aggregate_edge_attr(
component["subgraph"], attr, np.mean, dismiss_none=True
)


def add_relative_changes(components, attr_pairs):
"""Add relative difference of attributes to each LTN.

Measured in terms of percentual increase using
:func:`superblockify.utils.percentual_increase`.

Write the relative percentual change of the specified edge attribute(s) to each
LTN in the list of components. The relative change is the percentual change of
the first to the second attribute.
Works in-place and adds `change_{attr1}` to each LTN.
If `attr1` has a value of 2 and `attr2` has a value of 1, the relative change is
-0.5, a 50% decrease. If `attr1` has a value of 4 and `attr2` has a value of 6,
the relative change is 0.5, a 50% increase.

Parameters
----------
components : list of dict
List of dictionaries of LTN components.
attr_pairs : list of tuples with two keys
List of attribute pairs to calculate the relative change of.

Raises
------
KeyError
If any key cannot be found in the LTNs.
"""
# Loop over LTNs
for component in components:
# Loop over attribute pairs
for attr1, attr2 in (
attr_pairs if isinstance(attr_pairs, list) else [attr_pairs]
):
# Calculate relative change
try:
component[f"change_{attr1}"] = percentual_increase(
component[attr1], component[attr2]
)
except KeyError as err:
raise KeyError(f"Key {err} not found in LTNs.") from err
30 changes: 30 additions & 0 deletions superblockify/metrics/metric.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@
calculate_coverage,
betweenness_centrality,
calculate_high_bc_clustering,
add_ltn_means,
add_relative_changes,
)
from .plot import (
plot_distance_matrices,
Expand Down Expand Up @@ -303,6 +305,34 @@ def calculate_all(
write_relative_increase_to_edges(
partitioner.graph, self.distance_matrix, self.node_list, "N", "S"
)
add_ltn_means(
partitioner.get_ltns(),
edge_attr=[
"edge_betweenness_normal",
"edge_betweenness_length",
"edge_betweenness_linear",
"edge_betweenness_normal_restricted",
"edge_betweenness_length_restricted",
"edge_betweenness_linear_restricted",
],
)
add_relative_changes(
partitioner.get_ltns(),
[
(
"mean_edge_betweenness_normal",
"mean_edge_betweenness_normal_restricted",
),
(
"mean_edge_betweenness_length",
"mean_edge_betweenness_length_restricted",
),
(
"mean_edge_betweenness_linear",
"mean_edge_betweenness_linear_restricted",
),
],
)

if make_plots:
# sort distance matrix dictionaries to follow start with E, S, N, ...
Expand Down
24 changes: 15 additions & 9 deletions superblockify/partitioning/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -222,9 +222,7 @@ def run(
self.set_sparsified_from_components()

# Set representative nodes
set_representative_nodes(
self.components if self.components else self.partitions
)
set_representative_nodes(self.get_ltns())

# Calculate travel times for partitioned graph
add_edge_travel_times_restricted(self.graph, self.sparsified)
Expand Down Expand Up @@ -258,9 +256,7 @@ def run(
replace_max_speeds=replace_max_speeds,
**kwargs,
)
calculate_component_metrics(
self.components if self.components else self.partitions
)
calculate_component_metrics(self.get_ltns())

@abstractmethod
def partition_graph(self, make_plots=False, **kwargs):
Expand Down Expand Up @@ -540,7 +536,7 @@ def set_sparsified_from_components(self):
# list of edges in partitions, use components if not None, else partitions
edges_in_partitions = {
edge
for component in (self.components if self.components else self.partitions)
for component in self.get_ltns()
for edge in component["subgraph"].edges(keys=True)
}
self.sparsified = self.graph.edge_subgraph(
Expand Down Expand Up @@ -622,13 +618,23 @@ def add_component_tessellation(self, **tess_kwargs):
"""
# Tessellate graph
add_edge_cells(self.graph, **tess_kwargs)
for comp in self.components if self.components else self.partitions:
for comp in self.get_ltns():
cells = ox.graph_to_gdfs(comp["subgraph"], nodes=False, edges=True)
# Move cells to geometry column for diss
cells["geometry"] = cells["cell"]
del cells["cell"]
comp["cell"] = cells.dissolve()["geometry"].iloc[0]

def get_ltns(self):
"""Get LTN list.

Returns
-------
list
Reference to self.components, if it is used, otherwise self.partitions
"""
return self.components if self.components else self.partitions

def get_partition_nodes(self):
"""Get the nodes of the partitioned graph.

Expand Down Expand Up @@ -669,7 +675,7 @@ def get_partition_nodes(self):
"subgraph": part["subgraph"],
"rep_node": part["representative_node_id"],
}
for part in (self.components if self.components else self.partitions)
for part in self.get_ltns()
# if there is the key "ignore" and it is True, ignore the partition
if not (part.get("ignore") is True)
]
Expand Down
8 changes: 2 additions & 6 deletions superblockify/partitioning/checks.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,7 @@ def components_are_connected(partitioning):
"""
found = True

for component in (
partitioning.components if partitioning.components else partitioning.partitions
):
for component in partitioning.get_ltns():
if not is_weakly_connected(component["subgraph"]):
logger.warning(
"The subgraph %s of %s is not connected.",
Expand Down Expand Up @@ -266,9 +264,7 @@ def components_are_connect_sparsified(partitioning):
Whether each subgraph is connected to the sparsified graph
"""

for component in (
partitioning.components if partitioning.components else partitioning.partitions
):
for component in partitioning.get_ltns():
# subgraph and sparsified graph are connected if there is at least one node
# that is contained in both
if not any(
Expand Down
6 changes: 2 additions & 4 deletions superblockify/partitioning/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,14 +195,12 @@ def _get_ltns(partitioner, nodes, ltn_boundary):
partitioner.add_component_tessellation()
else:
# add node geometry for representative node from nodes layer
for _, part in enumerate(
partitioner.components if partitioner.components else partitioner.partitions
):
for _, part in enumerate(partitioner.get_ltns()):
part["representative_node_point"] = nodes.loc[
part["representative_node_id"], "geometry"
]
ltns = GeoDataFrame.from_dict(
partitioner.components if partitioner.components else partitioner.partitions,
partitioner.get_ltns(),
geometry="cell" if ltn_boundary else "representative_node_point",
orient="columns",
crs=partitioner.graph.graph["crs"],
Expand Down
46 changes: 46 additions & 0 deletions superblockify/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@
array_equal,
empty,
int64 as np_int64,
sign,
inf,
isinf,
nan,
)
from osmnx.stats import count_streets_per_node
from shapely import wkt
Expand Down Expand Up @@ -342,6 +346,12 @@ def load_graphml_dtypes(filepath=None, attribute_label=None, attribute_dtype=Non
"population": float,
"area": float,
"cell_id": int,
"edge_betweenness_normal": float,
"edge_betweenness_length": float,
"edge_betweenness_linear": float,
"edge_betweenness_normal_restricted": float,
"edge_betweenness_length_restricted": float,
"edge_betweenness_linear_restricted": float,
}
graph_dtypes = {
"simplified": bool,
Expand Down Expand Up @@ -378,3 +388,39 @@ def load_graphml_dtypes(filepath=None, attribute_label=None, attribute_dtype=Non
graph_dtypes=graph_dtypes,
)
return graph


def percentual_increase(val_1, val_2):
"""Compute the percentual increase between two values.

3 -> 4 = 33.33% = 1/3
4 -> 3 = -25.00% = -1/4

Parameters
----------
val_1 : float
The first value.
val_2 : float
The second value.

Returns
-------
float
The relative difference between the two values.

Notes
-----
If both values are zero, the result is zero.
If one value is zero, the result is +-infinity.
"""
if val_1 == val_2:
return 0
if val_1 == 0 or val_2 == 0:
return inf * (sign(val_2 - val_1) if val_2 != 0 else -1)
if isinf(val_1) and isinf(val_2):
return nan
if isinf(val_1):
return -inf
if isinf(val_2):
return inf * sign(val_1) * (sign(val_2) if val_2 != 0 else 1)
return (val_2 / val_1) - 1
Loading