Skip to content
Open
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
13 changes: 12 additions & 1 deletion docs/source/api_reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -351,7 +351,18 @@ Network modifiers describe project-specific changes to road-network assumptions:
- `mobility.NewRoadModifier`
- `mobility.RoadLaneNumberModifier`

Use them only when the scenario changes the road network or speed assumptions. Document the geometry, speed, capacity, or lane-number assumption with the scenario.
Their geometry, speed, capacity, lane-number, border-penalty, and extract-date
arguments accept `mobility.ParameterValue`. Mobility resolves these values for
each scenario and iteration before preparing the corresponding road graph. See
[scenarios](scenarios.md) for an example.

Each modifier also accepts a pre-built parameter model when a project keeps its
configuration separate from mode construction:

- `mobility.BorderCrossingSpeedModifierParameters`
- `mobility.LimitedSpeedZonesModifierParameters`
- `mobility.NewRoadModifierParameters`
- `mobility.RoadLaneNumberModifierParameters`

## Group-Day-Trip Model

Expand Down
25 changes: 25 additions & 0 deletions docs/source/scenarios.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,31 @@ In this example:

This pattern is useful when a scenario assumption should appear after a few warm-up iterations. Remember that the model still replans during those warm-up iterations unless the run parameters restrict behaviour-change phases.

Road speed modifiers use the same pattern. For example, this changes a speed
limit from iteration 5 in one scenario:

```python
speed_zone = mobility.LimitedSpeedZonesModifier(
zones_geometry_file_path="inputs/speed-zones.gpkg",
max_speed=mobility.ParameterValue.by_scenario_and_iteration(
default=50.0,
safer_streets={
1: 50.0,
5: 30.0,
},
),
)

car = mobility.CarMode(
transport_zones,
speed_modifiers=[speed_zone],
)
```

All speed-modifier settings can vary this way, including GIS file paths. When
a selected value changes, Mobility prepares a matching modified road graph and
reuses it for later runs with the same inputs.

## Complete Small Scenario Example

This example declares a reference and a car-cost scenario, changes car distance cost from iteration 5, runs both scenarios, then compares final distance by mode.
Expand Down
4 changes: 4 additions & 0 deletions mobility/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,11 @@

from .transport.graphs.modified.modifiers import (
BorderCrossingSpeedModifier,
BorderCrossingSpeedModifierParameters,
LimitedSpeedZonesModifier,
LimitedSpeedZonesModifierParameters,
NewRoadModifier,
NewRoadModifierParameters,
RoadLaneNumberModifier,
RoadLaneNumberModifierParameters,
)
43 changes: 43 additions & 0 deletions mobility/transport/costs/generalized_cost.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
from __future__ import annotations

from mobility.runtime.assets.in_memory_asset import InMemoryAsset
from mobility.runtime.parameter_values import (
SensitivityCase,
resolve_parameter_values,
)


class GeneralizedCost(InMemoryAsset):
"""Base class for generalized costs that can vary during a model run."""

def for_iteration(
self,
iteration: int,
*,
travel_costs,
scenario: str | None = None,
sensitivity_case: SensitivityCase | None = None,
) -> "GeneralizedCost":
"""Return a generalized-cost variant with resolved parameter values."""
resolved_inputs = resolve_parameter_values(
self.inputs,
scenario=scenario,
iteration=iteration,
sensitivity_case=sensitivity_case,
)
resolved_inputs["travel_costs"] = travel_costs

if (
resolved_inputs == self.inputs
and travel_costs is self.inputs["travel_costs"]
):
return self

return self._from_resolved_inputs(resolved_inputs)

def _from_resolved_inputs(self, inputs: dict) -> "GeneralizedCost":
"""Create the concrete generalized cost from resolved inputs."""
raise NotImplementedError(
f"{self.__class__.__name__} should implement "
"_from_resolved_inputs()."
)
12 changes: 10 additions & 2 deletions mobility/transport/costs/path/path_generalized_cost.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

import pandas as pd

from mobility.runtime.assets.in_memory_asset import InMemoryAsset
from mobility.transport.costs.generalized_cost import GeneralizedCost
from mobility.transport.costs.od_flows_asset import VehicleODFlowsAsset

class PathGeneralizedCost(InMemoryAsset):
class PathGeneralizedCost(GeneralizedCost):

def __init__(self, travel_costs, parameters, mode_name):
inputs = {
Expand All @@ -14,6 +14,14 @@ def __init__(self, travel_costs, parameters, mode_name):
"mode_name": mode_name
}
super().__init__(inputs)

def _from_resolved_inputs(self, inputs: dict) -> "PathGeneralizedCost":
"""Create a path generalized cost from resolved inputs."""
return PathGeneralizedCost(
travel_costs=inputs["travel_costs"],
parameters=inputs["parameters"],
mode_name=inputs["mode_name"],
)


def get(
Expand Down
65 changes: 65 additions & 0 deletions mobility/transport/costs/path/path_travel_costs.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@
from mobility.transport.costs.travel_costs_asset import TravelCostsBase
from mobility.runtime.assets.file_asset import FileAsset
from mobility.runtime.assets.in_memory_asset import InMemoryAsset
from mobility.runtime.parameter_values import (
SensitivityCase,
resolve_parameter_values,
)
from mobility.runtime.r_integration.r_script_runner import RScriptRunner
from mobility.spatial.transport_zones import TransportZones
from mobility.transport.costs.parameters.path_routing_parameters import PathRoutingParameters
Expand Down Expand Up @@ -194,6 +198,9 @@ def __init__(
"contracted_path_graph": contracted_path_graph,
"routing_parameters": routing_parameters,
"osm_capacity_parameters": osm_capacity_parameters,
"congestion": bool(congestion),
"congestion_flows_scaling_factor": congestion_flows_scaling_factor,
"speed_modifiers": list(speed_modifiers),
"target_max_vehicles_per_od_endpoint": target_max_vehicles_per_od_endpoint,
"congestion_assignment_max_iterations": congestion_assignment_max_iterations,
"congestion_assignment_max_gap": congestion_assignment_max_gap,
Expand All @@ -202,6 +209,64 @@ def __init__(
}
super().__init__(inputs)

def for_iteration(
self,
iteration: int,
scenario: str | None = None,
sensitivity_case: SensitivityCase | None = None,
) -> "PathTravelCosts":
"""Return the path-cost variant selected for one iteration."""
routing_parameters = self.inputs["routing_parameters"]
resolved_routing_parameters = resolve_parameter_values(
routing_parameters,
scenario=scenario,
iteration=iteration,
sensitivity_case=sensitivity_case,
)

speed_modifiers = self.inputs["speed_modifiers"]
resolved_speed_modifiers = [
modifier.for_iteration(
iteration,
scenario=scenario,
sensitivity_case=sensitivity_case,
)
for modifier in speed_modifiers
]

if (
resolved_routing_parameters == routing_parameters
and resolved_speed_modifiers == speed_modifiers
):
return self

# Routing inputs define every graph below the simplified OSM graph.
# Constructing a variant aligns every downstream asset hash with the
# selected iteration. The files remain lazy and are only built on get().
return PathTravelCosts(
mode_name=self.inputs["mode_name"],
transport_zones=self.inputs["transport_zones"],
routing_parameters=resolved_routing_parameters,
osm_capacity_parameters=self.inputs["osm_capacity_parameters"],
congestion=self.inputs["congestion"],
congestion_flows_scaling_factor=self.inputs[
"congestion_flows_scaling_factor"
],
target_max_vehicles_per_od_endpoint=self.inputs[
"target_max_vehicles_per_od_endpoint"
],
congestion_assignment_max_iterations=self.inputs[
"congestion_assignment_max_iterations"
],
congestion_assignment_max_gap=self.inputs[
"congestion_assignment_max_gap"
],
congestion_assignment_retained_volume_share=self.inputs[
"congestion_assignment_retained_volume_share"
],
speed_modifiers=resolved_speed_modifiers,
)

@property
def freeflow_costs(self):
"""CH cost table, created only if static costs are requested."""
Expand Down
13 changes: 13 additions & 0 deletions mobility/transport/costs/travel_costs_asset.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,19 @@
class TravelCostsBase:
"""Shared helpers for travel-cost selectors and file-backed assets."""

def for_iteration(
self,
iteration: int,
scenario: str | None = None,
sensitivity_case=None,
):
"""Return this travel-cost asset for one iteration.

Travel costs with scenario-dependent routing inputs override this
method. Static travel costs can safely reuse the same asset.
"""
return self

def asset_for_road_flows(self, road_flow_asset):
"""Return the effective asset for one road-flow asset."""
return self
Expand Down
8 changes: 8 additions & 0 deletions mobility/transport/graphs/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,30 @@
from .contracted import ContractedPathGraph
from .modified import (
BorderCrossingSpeedModifier,
BorderCrossingSpeedModifierParameters,
LimitedSpeedZonesModifier,
LimitedSpeedZonesModifierParameters,
NewRoadModifier,
NewRoadModifierParameters,
RoadLaneNumberModifier,
RoadLaneNumberModifierParameters,
SpeedModifier,
)
from .simplified import SimplifiedPathGraph

__all__ = [
"BorderCrossingSpeedModifier",
"BorderCrossingSpeedModifierParameters",
"CongestedPathGraph",
"ContractedPathGraph",
"GraphGPKGExporter",
"LimitedSpeedZonesModifier",
"LimitedSpeedZonesModifierParameters",
"NewRoadModifier",
"NewRoadModifierParameters",
"PathGraph",
"RoadLaneNumberModifier",
"RoadLaneNumberModifierParameters",
"SimplifiedPathGraph",
"SpeedModifier",
]
8 changes: 8 additions & 0 deletions mobility/transport/graphs/modified/__init__.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
from .modified_path_graph import ModifiedPathGraph
from .modifiers import (
BorderCrossingSpeedModifier,
BorderCrossingSpeedModifierParameters,
LimitedSpeedZonesModifier,
LimitedSpeedZonesModifierParameters,
NewRoadModifier,
NewRoadModifierParameters,
RoadLaneNumberModifier,
RoadLaneNumberModifierParameters,
SpeedModifier,
)

__all__ = [
"BorderCrossingSpeedModifier",
"BorderCrossingSpeedModifierParameters",
"LimitedSpeedZonesModifier",
"LimitedSpeedZonesModifierParameters",
"ModifiedPathGraph",
"NewRoadModifier",
"NewRoadModifierParameters",
"RoadLaneNumberModifier",
"RoadLaneNumberModifierParameters",
"SpeedModifier",
]
8 changes: 8 additions & 0 deletions mobility/transport/graphs/modified/modifiers/__init__.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
from .speed_modifier import (
BorderCrossingSpeedModifier,
BorderCrossingSpeedModifierParameters,
LimitedSpeedZonesModifier,
LimitedSpeedZonesModifierParameters,
NewRoadModifier,
NewRoadModifierParameters,
RoadLaneNumberModifier,
RoadLaneNumberModifierParameters,
SpeedModifier,
)

__all__ = [
"BorderCrossingSpeedModifier",
"BorderCrossingSpeedModifierParameters",
"LimitedSpeedZonesModifier",
"LimitedSpeedZonesModifierParameters",
"NewRoadModifier",
"NewRoadModifierParameters",
"RoadLaneNumberModifier",
"RoadLaneNumberModifierParameters",
"SpeedModifier",
]
Loading
Loading