Metadata-Version: 2.4
Name: network-centrality
Version: 0.1.0
Summary: Centrality and origin-destination analysis workflows for spatial transportation networks.
Author: Brian Almdale
License-Expression: GPL-3.0-or-later
Project-URL: Homepage, https://github.com/balmda/network-centrality
Project-URL: Repository, https://github.com/balmda/network-centrality
Project-URL: Issues, https://github.com/balmda/network-centrality/issues
Project-URL: Changelog, https://github.com/balmda/network-centrality/blob/main/CHANGELOG.md
Keywords: centrality,evacuation,geospatial,network-analysis,networkx,origin-destination,osmnx,transportation
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Science/Research
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Scientific/Engineering :: GIS
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: geopandas>=1.0
Requires-Dist: networkx>=3.4
Requires-Dist: osmnx>=2.0
Requires-Dist: pandas>=2.2
Requires-Dist: shapely>=2.0
Provides-Extra: postgis
Requires-Dist: geoalchemy2>=0.15; extra == "postgis"
Requires-Dist: psycopg2-binary>=2.9; extra == "postgis"
Requires-Dist: sqlalchemy>=2.0; extra == "postgis"
Provides-Extra: dev
Requires-Dist: build>=1.2; extra == "dev"
Requires-Dist: pytest>=8.0; extra == "dev"
Requires-Dist: pytest-cov>=5.0; extra == "dev"
Requires-Dist: twine>=6.0; extra == "dev"
Dynamic: license-file

# network-centrality

`network-centrality` is a GeoDataFrame- and OSMnx-first Python package for calculating standard and origin-destination-weighted centrality on spatial transportation networks. It supports OSMnx graphs, user-prepared node and edge layers, optional GeoPackage or PostGIS I/O, configurable routing costs, approach-connector network splitting, hard crossing constraints, soft routing penalties, optional Level of Traffic Stress logic, and aggregation of split-edge results back to their original parent links. The package is useful for identifying important network links under distance-, time-, demand-, stress-, or policy-constrained routing scenarios.

Copyright © 2026 Brian Almdale

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

## Package status

The package begins at version `0.1.0` and is under active development. An open-source license has not yet been selected, so no license file is included in this initial package structure.

## Installation

### Conda development environment

Create the provided conda environment, activate it, and install the package in editable mode:

```bash
conda env create -f environment.yml
conda activate network-centrality
python -m pip install -e .
```

The conda environment includes the optional PostGIS dependencies and development tools.

### Pip editable installation

From an existing Python 3.11+ environment:

```bash
python -m pip install -e .
```

Install PostGIS support and development tools when needed:

```bash
python -m pip install -e ".[postgis,dev]"
```

## Quick start

The following matches the main workflow in `examples/example_usage.py`:

```python
import osmnx as ox

from network_centrality import (
    add_osmnx_travel_time,
    load_osm_graph_from_place,
    run_centrality,
    write_centrality_outputs,
)

G = load_osm_graph_from_place(
    place="Seattle, Washington, USA",
    network_type="drive",
    exclude_highway_tags={"motorway", "motorway_link", "service"},
)
G = ox.project_graph(G)
G = add_osmnx_travel_time(G, fallback_speed_kph=40.0)

nodes, edges = run_centrality(
    G=G,
    cost_col="travel_time",
    approx_k=1_000,
    run_standard_metrics=True,
)

write_centrality_outputs(
    nodes=nodes,
    edges=edges,
    out_gpkg="outputs/network_centrality.gpkg",
    dry_run=False,
)
```

Run the complete parameterized example without writing files:

```bash
python examples/example_usage.py
```

Add `--write` to create the output GeoPackage. Use `python examples/example_usage.py --help` to review the location, OD-input, cost-cutoff, and output options.

## Public API

### Configuration

- `ODWeightSpec`: Defines origin and destination node-weight columns for implicit all-to-all OD centrality.
- `MatrixODSpec`: Defines the origin, destination, demand, and output columns used by an explicit OD matrix.
- `DEFAULT_SEGMENT_LTS_MULTIPLIERS`: Default segment LTS routing-cost multipliers.
- `DEFAULT_CROSSING_LTS_PENALTIES`: Default crossing LTS additive penalties.

### Graph construction and preparation

- `load_osm_graph_from_place()`: Downloads an OSMnx graph and optionally filters it by OSM highway tags.
- `filter_graph_edges_by_highway()`: Filters an existing graph using included or excluded highway tags.
- `graph_from_user_gdfs()`: Converts routing-ready node and edge GeoDataFrames into an OSMnx-compatible graph.
- `build_nodes_from_edge_endpoints()`: Derives a node layer from edge start and end geometries.
- `make_edges_bidirectional()`: Duplicates logically undirected edges in the reverse direction.
- `ensure_graph_edge_ids()`: Preserves unique IDs and assigns stable `{u}_{v}_{key}` IDs to missing, null, or duplicate values.
- `collapse_to_min_cost_digraph()`: Keeps the lowest-cost parallel edge for each directed node pair.
- `validate_positive_weight()`: Validates that each graph edge has a positive routing cost.
- `make_approach_points_from_graph()`: Creates setback approach points near both endpoints of each directed edge.

### Routing costs

- `add_length_from_geometry()`: Calculates metric edge length when the selected length field is incomplete.
- `add_travel_time_from_speed()`: Calculates travel time in seconds from meters and miles per hour.
- `add_osmnx_travel_time()`: Uses OSMnx speed and travel-time preparation helpers.

### Approach constraints and stress networks

- `match_approach_points_to_edge_endpoints()`: Matches approach points to edges and their nearest endpoint nodes.
- `add_unsignalized_arterial_crossing_filter()`: Creates allowed and optional penalty fields for unsignalized arterial crossings.
- `build_approach_split_constraint_network()`: Splits links at approach points and applies generic segment or connector constraints.
- `build_approach_split_stress_network()`: Provides the backward-compatible LTS wrapper around generic splitting.
- `make_allowed_subgraph()`: Removes edges that fail a selected boolean constraint field.
- `make_low_stress_subgraph()`: Provides the backward-compatible low-stress subgraph alias.

### Centrality metrics

- `standard_edge_betweenness_df()`: Calculates weighted edge betweenness on a directed graph.
- `standard_node_centrality_df()`: Calculates node betweenness, closeness, degree, in-degree, and out-degree.
- `od_weighted_edge_centrality_from_node_weights()`: Routes implicit OD demand based on origin-weight times destination-weight.
- `od_weighted_edge_centrality_from_matrix()`: Routes demand from an explicit OD matrix.
- `add_percentile_columns()`: Adds percentile ranks for selected result fields.
- `add_weighted_composite_score()`: Combines selected metrics into a weighted score.
- `run_centrality()`: Orchestrates graph preparation, standard metrics, OD metrics, percentiles, and composite scoring.

### OD preparation

- `snap_points_to_graph_nodes()`: Snaps point geometries to their nearest graph nodes.
- `make_node_weights_from_polygons()`: Converts polygon attributes into aggregated graph-node weights.
- `make_block_group_od_matrix()`: Materializes a polygon-to-polygon OD matrix using network shortest-path costs.

### Result aggregation and comparison

- `aggregate_split_edges_to_parent()`: Aggregates split-edge metrics to original parent-edge IDs.
- `compare_parent_scores()`: Compares full and constrained scenario metrics at the parent-edge level.
- `compare_full_and_low_stress_parent_scores()`: Provides the backward-compatible LTS comparison alias.

### Input and output

- `read_gpkg_network()`: Reads edge and optional node layers from a GeoPackage.
- `read_postgis_network()`: Reads edge and optional node queries through a generic SQLAlchemy connection.
- `write_centrality_outputs()`: Writes nodes and edges to a GeoPackage; defaults to `dry_run=True`.
- `write_outputs_to_postgis()`: Writes nodes and edges to PostGIS; defaults to `dry_run=True`.

## Routing-cost units

`cost_col` controls every shortest-path calculation. The units of `max_cost` or `od_max_cost` must match the selected field:

- `length`: typically meters.
- `travel_time`: seconds when generated by OSMnx or `add_travel_time_from_speed()`.
- `routing_cost`: user-defined units, often length-equivalent after penalties.
- `bike_stress_cost`: length-equivalent units under the default LTS multiplier and penalty approach.

## Known limitations and notes

- `collapse_to_min_cost_digraph()` retains only the lowest-cost parallel edge for each directed `u`/`v` pair during centrality routing. Results are assigned to that retained edge ID; higher-cost parallel edges receive no routed score.
- Approximate betweenness is controlled by `approx_k`. Passing `None`, or a value at least as large as the graph node count, requests exact NetworkX calculation and can become expensive on large networks.
- Implicit node-weight OD centrality and explicit OD-matrix centrality perform repeated shortest-path routing. Runtime can grow rapidly with the number of origins, destinations, and reachable OD pairs.
- `make_block_group_od_matrix()` materializes individual polygon-to-polygon pairs. For large datasets, `make_node_weights_from_polygons()` with implicit node-weight centrality is generally more memory efficient.
- Approach splitting assumes LineString edge geometry and requires careful QA of duplicate or incorrectly matched approach points. Duplicate projected distances on the same edge retain the first matched point, matching the prototype behavior.
- Approach-point creation and distance matching should use a projected CRS with meter units. `make_approach_points_from_graph()` explicitly requires a projected graph.
- When a base cost is missing during approach splitting, the prototype falls back to geometry length in the layer CRS. This is only meaningful when that CRS uses appropriate distance units.
- Standard centrality metrics do not use OD pairs. OD inputs affect only the OD-weighted edge-centrality fields and any composite score that includes those fields.
- The package does not create or manage database credentials. PostGIS helpers accept a caller-provided SQLAlchemy engine or connection.
- All write-capable public helpers default to `dry_run=True`; callers must explicitly set `dry_run=False` to modify a GeoPackage or database.

## Prototype migration notes

The package preserves the reusable behavior of `centrality_tool_generic_approach_constraints.py` while removing executable testing blocks and hardcoded local paths. Two conflicting definitions of `ensure_graph_edge_ids()` were consolidated: unique existing IDs remain unchanged, while missing, null, and duplicate IDs receive stable IDs based on `u`, `v`, and `key`. Seattle-specific functions were not included in the importable package; their workflow informed the parameterized example script.
