Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
53b2ee7
Make add_centroid_connectors() more configurable
lmz Sep 9, 2025
2246575
Fix import for python 3.12
lmz Sep 11, 2025
1df56c1
Add centroid links shapes
lmz Sep 12, 2025
7dd19e3
Add method, add_direction_to_links()
lmz Sep 23, 2025
47a82fb
Add write() to RoadwayNetwork to enable subclass to override
lmz Nov 14, 2025
caf83d1
Update centroid connector name to distinguish direction
lmz Nov 14, 2025
d16a156
Accept RoadwayNetwork or subclass
lmz Nov 19, 2025
fcaedb5
Use SafeFileHandler instead of FileHandler to handle Windows flush er…
lmz Nov 26, 2025
3c1bf91
ML_geometry is also a complex column
lmz Nov 26, 2025
22e3678
Handle conversion to/from dict/pydantic for json io
lmz Nov 26, 2025
fea483f
Also handle conversion for geometry columns to/from geojson dicts
lmz Nov 26, 2025
73af385
Fix reading scoped attributes
lmz Nov 27, 2025
057288b
Larger link offsets
lmz Nov 27, 2025
53ce4e2
Add __str__() method and note ferry_only is a link attribute
lmz Dec 2, 2025
9ec71a5
Update __str__ methods
lmz Dec 4, 2025
b029dfb
Use scenario name from file if it exists, rather than argument (due t…
lmz Dec 4, 2025
5d7776b
Fix bug where GP_model_node_id wasn't getting set
lmz Dec 5, 2025
e391209
Fix typo causing syntax warning
lmz Dec 11, 2025
35a3e5b
Simplify __repr__ and move bulk of it to summary()
lmz Jan 8, 2026
9a00e20
Make add_centroid_connectors() more configurable
lmz Sep 9, 2025
07dfacf
Fix import for python 3.12
lmz Sep 11, 2025
b5a1f6b
Add centroid links shapes
lmz Sep 12, 2025
2a9b6e4
Add method, add_direction_to_links()
lmz Sep 23, 2025
cb730d7
Add write() to RoadwayNetwork to enable subclass to override
lmz Nov 14, 2025
259a4a6
Update centroid connector name to distinguish direction
lmz Nov 14, 2025
16175f7
Accept RoadwayNetwork or subclass
lmz Nov 19, 2025
8387fc5
Use SafeFileHandler instead of FileHandler to handle Windows flush er…
lmz Nov 26, 2025
f8e7fea
ML_geometry is also a complex column
lmz Nov 26, 2025
6796535
Handle conversion to/from dict/pydantic for json io
lmz Nov 26, 2025
81b2b8c
Also handle conversion for geometry columns to/from geojson dicts
lmz Nov 26, 2025
80c6526
Fix reading scoped attributes
lmz Nov 27, 2025
8199c9d
Larger link offsets
lmz Nov 27, 2025
f03b9fa
Add __str__() method and note ferry_only is a link attribute
lmz Dec 2, 2025
0a2f914
Update __str__ methods
lmz Dec 4, 2025
1aed06f
Use scenario name from file if it exists, rather than argument (due t…
lmz Dec 4, 2025
1e9663b
Fix bug where GP_model_node_id wasn't getting set
lmz Dec 5, 2025
6bce721
Fix typo causing syntax warning
lmz Dec 11, 2025
33dc0a5
Merge branch 'centroids' of https://github.com/BayAreaMetro/network_w…
lmz Jan 8, 2026
8e7f7f8
Move some functions from utils\transit.py to transit\filter.py
lmz Jan 8, 2026
74fa921
Move create_feed_frequencies() to models\gtfs\converters.py
lmz Jan 9, 2026
03fc88d
Add docstrings and try to handle managed lane creation errors
lmz Jan 13, 2026
528efc8
Add default_node_attribute_dict parameter to create_feed_from_gtfs_mo…
lmz Jan 23, 2026
4ac27a8
Model network creation: convert ADDITIONAL_COPY_FROM_GP_TO_ML to link…
lmz Jan 23, 2026
4f49c7a
Centroid & connector creation: add default link/node attribute parame…
lmz Jan 23, 2026
8d8b6d9
Marking as TODO for possible remove based on #408 comments
lmz Jan 26, 2026
bc6df4f
Rename create_bus_routes() -> route_shapes_between_stops()
lmz Jan 26, 2026
649d09d
Move constants to configs\wrangler.py TransitConfig
lmz Jan 27, 2026
9ce6d75
Marking additional transit.py TODOs and constrain frequency_method va…
lmz Jan 28, 2026
f4160dd
Constrain frequency_method using pydantic
lmz Jan 28, 2026
89c023c
Added missing import
lmz Feb 20, 2026
d7d17c7
Bug fix: don't create MultiLineString shapes
lmz Mar 4, 2026
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
3 changes: 2 additions & 1 deletion docs/networks.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,8 @@ All properties preceded by `ML_` will be copied, without that prefix, to the man
The following are controlled by parameters which can be set using WranglerConfig:

Geometry of managed lanes will be defined as a shape offset by the parameter `ML_OFFSET_METERS`.
Properties defined in the parameter `ADDITIONAL_COPY_FROM_GP_TO_ML` are also copied from the parent link.
Link properties defined in the parameter `ADDITIONAL_COPY_FROM_GP_LINK_TO_ML` are also copied from the parent link.
Node properties defined in the parameter `ADDITIONAL_COPY_FROM_GP_NODE_TO_ML` are copied from the general purpose nodes to the managed lane nodes.

New `model_node_id` s and `model_link_ids` are generated based either on ranges or using a scalar from the GP link based on: `ML_LINK_ID_METHOD`, `ML_NODE_ID_METHOD`, `ML_LINK_ID_RANGE`, `ML_NODE_ID_RANGE`, `ML_LINK_ID_SCALAR`, `ML_NODE_ID_SCALAR`

Expand Down
66 changes: 58 additions & 8 deletions network_wrangler/configs/wrangler.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,8 @@
OVERWRITE_SCOPED: conflicting
MODEL_ROADWAY:
ML_OFFSET_METERS: int = -10
ADDITIONAL_COPY_FROM_GP_TO_ML: []
ADDITIONAL_COPY_FROM_GP_LINK_TO_ML: []
ADDITIONAL_COPY_FROM_GP_NODE_TO_ML: []
ADDITIONAL_COPY_TO_ACCESS_EGRESS: []
CPU:
EST_PD_READ_SPEED:
Expand Down Expand Up @@ -101,6 +102,7 @@

"""

from dataclasses import field
from typing import Literal

from pydantic import Field
Expand Down Expand Up @@ -178,16 +180,62 @@ class ModelRoadwayConfig(ConfigItem):

Attributes:
ML_OFFSET_METERS: Offset in meters for managed lanes.
ADDITIONAL_COPY_FROM_GP_TO_ML: Additional fields to copy from general purpose to managed
lanes.
ADDITIONAL_COPY_FROM_GP_LINK_TO_ML: Additional link fields to copy from general purpose
to managed lanes.
ADDITIONAL_COPY_FROM_GP_NODE_TO_ML: Additional node fields to copy from general purpose
to managed lane nodes.
ADDITIONAL_COPY_TO_ACCESS_EGRESS: Additional fields to copy to access and egress links.
"""

ML_OFFSET_METERS: int = -10
ADDITIONAL_COPY_FROM_GP_TO_ML: list[str] = Field(default_factory=list)
ADDITIONAL_COPY_FROM_GP_LINK_TO_ML: list[str] = Field(default_factory=list)
ADDITIONAL_COPY_FROM_GP_NODE_TO_ML: list[str] = Field(default_factory=list)
ADDITIONAL_COPY_TO_ACCESS_EGRESS: list[str] = Field(default_factory=list)


@dataclass
class TransitConfig(ConfigItem):
"""Transit Configuration.

Attributes:
K_NEAREST_CANDIDATES: Number of nearest candidate nodes to consider in stop matching
when using name scoring. Used in
[`match_bus_stops_to_roadway_nodes()`][network_wrangler.utils.transit.match_bus_stops_to_roadway_nodes].
NAME_MATCH_WEIGHT: Weight for name match score in combined scoring. 0.9 means 90% name
match, 10% distance. Used in
[`match_bus_stops_to_roadway_nodes()`][network_wrangler.utils.transit.match_bus_stops_to_roadway_nodes].
MIN_SUBSTRING_MATCH_LENGTH: Minimum string length required for substring matching.
Prevents spurious matches with single letters. Used in
[`assess_stop_name_roadway_compatibility()`][network_wrangler.utils.transit.assess_stop_name_roadway_compatibility].
SHAPE_DISTANCE_TOLERANCE: Maximum ratio of path distance to shortest distance in
shape-aware routing. 1.10 means paths up to 110% of shortest distance are considered.
Used in [`route_shapes_between_stops()`][network_wrangler.utils.transit.route_shapes_between_stops]
and [`find_shape_aware_shortest_path()`][network_wrangler.utils.transit.find_shape_aware_shortest_path].
MAX_SHAPE_CANDIDATE_PATHS: Maximum number of candidate paths to evaluate when doing
shape-aware routing. Used in
[`find_shape_aware_shortest_path()`][network_wrangler.utils.transit.find_shape_aware_shortest_path].
NEAREST_K_SHAPES_TO_STOPS: Number of nearest shape points to check for each stop.
FIRST_LAST_SHAPE_STOP_IDX: For loops, the first stop must match one of the first
FIRST_LAST_SHAPE_STOP_IDX shapes, and the last stop must match one of the last
FIRST_LAST_SHAPE_STOP_IDX shapes. Used in
[`route_shapes_between_stops()`][network_wrangler.utils.transit.route_shapes_between_stops].
MAX_DISTANCE_STOP_FEET: Maximum distance in feet for a stop to match to a node.
Used in [`match_bus_stops_to_roadway_nodes()`][network_wrangler.utils.transit.match_bus_stops_to_roadway_nodes].
MAX_DISTANCE_STOP_METERS: Maximum distance in meters for a stop to match to a node.
Used in [`match_bus_stops_to_roadway_nodes()`][network_wrangler.utils.transit.match_bus_stops_to_roadway_nodes].
"""

K_NEAREST_CANDIDATES: int = 20
NAME_MATCH_WEIGHT: float = 0.9
MIN_SUBSTRING_MATCH_LENGTH: int = 3
SHAPE_DISTANCE_TOLERANCE: float = 1.10
MAX_SHAPE_CANDIDATE_PATHS: int = 20
NEAREST_K_SHAPES_TO_STOPS: int = 20
FIRST_LAST_SHAPE_STOP_IDX: int = 10
MAX_DISTANCE_STOP_FEET: float = 528.0 # 0.10 * 5280 FEET_PER_MILE
MAX_DISTANCE_STOP_METERS: float = 150.0 # 0.15 * 1000 METERS_PER_KILOMETER


@dataclass
class CpuConfig(ConfigItem):
"""CPU Configuration - Will not change any outcomes.
Expand All @@ -214,14 +262,16 @@ class WranglerConfig(ConfigItem):
Attributes:
IDS: Parameteters governing how new ids are generated.
MODEL_ROADWAY: Parameters governing how the model roadway is created.
TRANSIT: Parameters governing transit network processing.
CPU: Parameters for accessing CPU information. Will not change any outcomes.
EDITS: Parameters governing how edits are handled.
"""

IDS: IdGenerationConfig = IdGenerationConfig()
MODEL_ROADWAY: ModelRoadwayConfig = ModelRoadwayConfig()
CPU: CpuConfig = CpuConfig()
EDITS: EditsConfig = EditsConfig()
IDS: IdGenerationConfig = field(default_factory=IdGenerationConfig)
MODEL_ROADWAY: ModelRoadwayConfig = field(default_factory=ModelRoadwayConfig)
TRANSIT: TransitConfig = field(default_factory=TransitConfig)
CPU: CpuConfig = field(default_factory=CpuConfig)
EDITS: EditsConfig = field(default_factory=EditsConfig)


DefaultConfig = WranglerConfig()
50 changes: 48 additions & 2 deletions network_wrangler/logger.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,50 @@
WranglerLogger = logging.getLogger("WranglerLogger")


class SafeFileHandler(logging.FileHandler):
"""FileHandler that safely handles flush errors on Windows.

On Windows, Python's logging can encounter OSError: [Errno 22] Invalid argument
when flushing log files due to:
- Rapid consecutive writes overwhelming Windows file handle buffering
- Large log files causing buffer flush issues
- Windows-specific file handle limitations in console/terminal environments

This handler catches and ignores flush errors while periodically logging
that they're occurring to alert users without spamming the error output.

See: https://bugs.python.org/issue13415 and related Windows logging issues
"""

def __init__(self, *args, **kwargs):

Check failure on line 28 in network_wrangler/logger.py

View workflow job for this annotation

GitHub Actions / tests (3.10)

ruff (D107)

network_wrangler/logger.py:28:9: D107 Missing docstring in `__init__`
super().__init__(*args, **kwargs)
self._flush_error_count = 0
self._last_reported_error_count = 0

def flush(self):
"""Flush with error handling for Windows OSError issues.

Catches OSError during flush operations (common on Windows) and tracks
how many times this occurs. Reports every 1000th error to alert users
without being too noisy.
"""
try:
super().flush()
except OSError as e:
# Windows-specific flush errors - track but don't crash
self._flush_error_count += 1

# Log every 1000th error to avoid spam while still alerting users
if self._flush_error_count % 1000 == 0:
# Use print to avoid recursive logging issues
print(

Check failure on line 49 in network_wrangler/logger.py

View workflow job for this annotation

GitHub Actions / tests (3.10)

ruff (T201)

network_wrangler/logger.py:49:17: T201 `print` found help: Remove `print`
f"WARNING: {self._flush_error_count} log flush errors encountered "
f"(Windows file handle issue). Logging continues but some debug "
f"messages may be delayed or lost. Error: {e}",
file=sys.stderr
)


def setup_logging(
info_log_filename: Optional[Path] = None,
debug_log_filename: Optional[Path] = None,
Expand Down Expand Up @@ -50,14 +94,16 @@
default_info_f = f"network_wrangler_{datetime.now().strftime('%Y_%m_%d__%H_%M_%S')}.info.log"
info_log_filename = info_log_filename or Path.cwd() / default_info_f

info_file_handler = logging.FileHandler(Path(info_log_filename), mode=file_mode)
# Use SafeFileHandler instead of FileHandler to handle Windows flush errors
info_file_handler = SafeFileHandler(Path(info_log_filename), mode=file_mode)
info_file_handler.setLevel(logging.INFO)
info_file_handler.setFormatter(FORMAT)
WranglerLogger.addHandler(info_file_handler)

# create debug file only when debug_log_filename is provided
if debug_log_filename:
debug_log_handler = logging.FileHandler(Path(debug_log_filename), mode=file_mode)
# Use SafeFileHandler instead of FileHandler to handle Windows flush errors
debug_log_handler = SafeFileHandler(Path(debug_log_filename), mode=file_mode)
debug_log_handler.setLevel(logging.DEBUG)
debug_log_handler.setFormatter(FORMAT)
WranglerLogger.addHandler(debug_log_handler)
Expand Down
Loading
Loading