Skip to content

How To

Build a Scenario using API

Scenario objects manage how a collection of projects is applied to the networks.

Scenarios are built from a base scenario and a list of project cards.

A project card is a YAML file (or similar) that describes a change to the network. The project card can contain multiple changes, each of which is applied to the network in sequence.

Create a Scenario

Instantiate a scenario by seeding it with a base scenario and optionally some project cards.

from network_wrangler import create_scenario

my_scenario = create_scenario(
    base_scenario=my_base_year_scenario,
    card_search_dir=project_card_directory,
    filter_tags=["baseline2050"],
)

A base_year_scenario is a dictionary representation of key components of a scenario:

  • road_net: RoadwayNetwork instance
  • transit_net: TransitNetwork instance
  • applied_projects: list of projects that have been applied to the base scenario so that the scenario knows if there will be conflicts with future projects or if a future project’s pre-requisite is satisfied.
  • conflicts: dictionary of conflicts for project that have been applied to the base scenario so that the scenario knows if there will be conflicts with future projects.
my_base_year_scenario = {
    "road_net": load_from_roadway_dir(STPAUL_DIR),
    "transit_net": load_transit(STPAUL_DIR),
    "applied_projects": [],
    "conflicts": {},
}

Add Projects to a Scenario

In addition to adding projects when you create the scenario, project cards can be added to a scenario using the add_project_cards method.

from projectcard import read_cards

project_card_dict = read_cards(card_location, filter_tags=["Baseline2030"], recursive=True)
my_scenario.add_project_cards(project_card_dict.values())

Where card_location can be a single path, list of paths, a directory, or a glob pattern.

Apply Projects to a Scenario

Projects can be applied to a scenario using the apply_all_projects method. Before applying projects, the scenario will check that all pre-requisites are satisfied, that there are no conflicts, and that the projects are in the planned projects list.

If you want to check the order of projects before applying them, you can use the queued_projects prooperty.

my_scenario.queued_projects
my_scenario.apply_all_projects()

You can review the resulting scenario, roadway network, and transit networks.

my_scenario.applied_projects
my_scenario.road_net.links_gdf.explore()
my_scenario.transit_net.feed.shapes_gdf.explore()

Write a Scenario to Disk

Scenarios (and their networks) can be written to disk using the write method which in addition to writing out roadway and transit networks, will serialize the scenario to a yaml-like file and can also write out the project cards that have been applied.

my_scenario.write(
    "output_dir",
    "scenario_name_to_use",
    overwrite=True,
    projects_write=True,
    file_format="parquet",
)
Example Serialized Scenario File
applied_projects: &id001
- project a
- project b
base_scenario:
applied_projects: *id001
roadway:
    dir: /Users/elizabeth/Documents/urbanlabs/MetCouncil/NetworkWrangler/working/network_wrangler/examples/small
    file_format: geojson
transit:
    dir: /Users/elizabeth/Documents/urbanlabs/MetCouncil/NetworkWrangler/working/network_wrangler/examples/small
config:
CPU:
    EST_PD_READ_SPEED:
    csv: 0.03
    geojson: 0.03
    json: 0.15
    parquet: 0.005
    txt: 0.04
IDS:
    ML_LINK_ID_METHOD: range
    ML_LINK_ID_RANGE: &id002 !!python/tuple
    - 950000
    - 999999
    ML_LINK_ID_SCALAR: 15000
    ML_NODE_ID_METHOD: range
    ML_NODE_ID_RANGE: *id002
    ML_NODE_ID_SCALAR: 15000
    ROAD_SHAPE_ID_METHOD: scalar
    ROAD_SHAPE_ID_SCALAR: 1000
    TRANSIT_SHAPE_ID_METHOD: scalar
    TRANSIT_SHAPE_ID_SCALAR: 1000000
MODEL_ROADWAY:
    ADDITIONAL_COPY_FROM_GP_TO_ML: []
    ADDITIONAL_COPY_TO_ACCESS_EGRESS: []
    ML_OFFSET_METERS: -10
conflicts: {}
corequisites: {}
name: first_scenario
prerequisites: {}
roadway:
dir: /Users/elizabeth/Documents/urbanlabs/MetCouncil/NetworkWrangler/working/network_wrangler/tests/out/first_scenario/roadway
file_format: parquet
transit:
dir: /Users/elizabeth/Documents/urbanlabs/MetCouncil/NetworkWrangler/working/network_wrangler/tests/out/first_scenario/transit
file_format: txt

Load a scenario from disk

And if you want to reload scenario that you “wrote”, you can use the load_scenario function.

from network_wrangler import load_scenario

my_scenario = load_scenario("output_dir/scenario_name_to_use_scenario.yml")

additional examples

You can see additional scenario creating capabilities in the example jupyter notebook Scenario Building Example.ipynb.

Build a Scenario from a Scenario Configuration File

Scenario configuration for Network Wrangler.

You can build a scenario and write out the output from a scenario configuration file using the code below. This is very useful when you are running a specific scenario with minor variations over again because you can enter your config file into version control. In addition to the completed roadway and transit files, the output will provide a record of how the scenario was run.

Usage
    from scenario import build_scenario_from_config
    my_scenario = build_scenario_from_config(my_scenario_config)

Where my_scenario_config can be a:

  • Path to a scenario config file in yaml/toml/json (recommended),
  • Dictionary which is in the same structure of a scenario config file, or
  • A ScenarioConfig() instance.

Notes on relative paths in scenario configs

  • Relative paths are recognized by a preceeding “.”.
  • Relative paths within output_scenario for roadway, transit, and project_cards are interpreted to be relative to output_scenario.path.
  • All other relative paths are interpreted to be relative to directory of the scenario config file. (Or if scenario config is provided as a dictionary, relative paths will be interpreted as relative to the current working directory.)
Example Scenario Config
name: "my_scenario"
base_scenario:
    roadway:
        dir: "path/to/roadway_network"
        file_format: "geojson"
        read_in_shapes: True
    transit:
        dir: "path/to/transit_network"
        file_format: "txt"
    applied_projects:
        - "project1"
        - "project2"
    conflicts:
        "project3": ["project1", "project2"]
        "project4": ["project1"]
projects:
    project_card_filepath:
        - "path/to/projectA.yaml"
        - "path/to/projectB.yaml"
    filter_tags:
        - "tag1"
output_scenario:
    overwrite: True
    roadway:
        out_dir: "path/to/output/roadway"
        prefix: "my_scenario"
        file_format: "geojson"
        true_shape: False
    transit:
        out_dir: "path/to/output/transit"
        prefix: "my_scenario"
        file_format: "txt"
    project_cards:
        out_dir: "path/to/output/project_cards"

wrangler_config: "path/to/wrangler_config.yaml"
Extended Usage

Load a configuration from a file:

from network_wrangler.configs import load_scenario_config

my_scenario_config = load_scenario_config("path/to/config.yaml")

Access the configuration:

my_scenario_config.base_transit_network.path
>> path/to/transit_network

Change Wrangler Configuration

Configuration for parameters for Network Wrangler.

Users can change a handful of parameters which control the way Wrangler runs. These parameters can be saved as a wrangler config file which can be read in repeatedly to make sure the same parameters are used each time.

Usage

At runtime, you can specify configurable parameters at the scenario level which will then also be assigned and accessible to the roadway and transit networks.

create_scenario(...config = myconfig)

Or if you are not using Scenario functionality, you can specify the config when you read in a RoadwayNetwork.

load_roadway_from_dir(**roadway, config=myconfig)
load_transit(**transit, config=myconfig)

my_config can be a:

  • Path to a config file in yaml/toml/json (recommended),
  • List of paths to config files (in case you want to split up various sub-configurations)
  • Dictionary which is in the same structure of a config file, or
  • A WranglerConfig() instance.

If not provided, Wrangler will use reasonable defaults.

Default Wrangler Configuration Values

If not explicitly provided, the following default values are used:

IDS:
    TRANSIT_SHAPE_ID_METHOD: scalar
    TRANSIT_SHAPE_ID_SCALAR: 1000000
    ROAD_SHAPE_ID_METHOD: scalar
    ROAD_SHAPE_ID_SCALAR: 1000
    ML_LINK_ID_METHOD: range
    ML_LINK_ID_RANGE: (950000, 999999)
    ML_LINK_ID_SCALAR: 15000
    ML_NODE_ID_METHOD: range
    ML_NODE_ID_RANGE: (950000, 999999)
    ML_NODE_ID_SCALAR: 15000
EDITS:
    EXISTING_VALUE_CONFLIC: warn
    OVERWRITE_SCOPED: conflicting
MODEL_ROADWAY:
    ML_OFFSET_METERS: int = -10
    ADDITIONAL_COPY_FROM_GP_TO_ML: []
    ADDITIONAL_COPY_TO_ACCESS_EGRESS: []
CPU:
    EST_PD_READ_SPEED:
        csv: 0.03
        parquet: 0.005
        geojson: 0.03
        json: 0.15
        txt: 0.04
Extended usage

Load the default configuration:

from network_wrangler.configs import DefaultConfig

Access the configuration:

from network_wrangler.configs import DefaultConfig
DefaultConfig.MODEL_ROADWAY.ML_OFFSET_METERS
>> -10

Modify the default configuration in-line:

from network_wrangler.configs import DefaultConfig

DefaultConfig.MODEL_ROADWAY.ML_OFFSET_METERS = 20

Load a configuration from a file:

from network_wrangler.configs import load_wrangler_config

config = load_wrangler_config("path/to/config.yaml")

Set a configuration value:

config.MODEL_ROADWAY.ML_OFFSET_METERS = 10

CpuConfig

Bases: ConfigItem

CPU Configuration - Will not change any outcomes.

Attributes:

Name Type Description
EST_PD_READ_SPEED dict[str, float]

Read sec / MB - WILL DEPEND ON SPECIFIC COMPUTER

Source code in network_wrangler/configs/wrangler.py
@dataclass
class CpuConfig(ConfigItem):
    """CPU Configuration -  Will not change any outcomes.

    Attributes:
        EST_PD_READ_SPEED: Read sec / MB - WILL DEPEND ON SPECIFIC COMPUTER
    """

    EST_PD_READ_SPEED: dict[str, float] = Field(
        default_factory=lambda: {
            "csv": 0.03,
            "parquet": 0.005,
            "geojson": 0.03,
            "json": 0.15,
            "txt": 0.04,
        }
    )

EditsConfig

Bases: ConfigItem

Configuration for Edits.

Attributes:

Name Type Description
EXISTING_VALUE_CONFLICT Literal['warn', 'error', 'skip']

Only used if ‘existing’ provided in project card and existing doesn’t match the existing network value. One of error, warn, or skip. error will raise an error, warn will warn the user, and skip will skip the change for that specific property (note it will still apply any remaining property changes). Defaults to warn. Can be overridden by setting existing_value_conflict in a roadway_property_change project card.

OVERWRITE_SCOPED Literal['conflicting', 'all', 'error']

How to handle conflicts with existing values. Should be one of “conflicting”, “all”, or False. “conflicting” will only overwrite values where the scope only partially overlaps with the existing value. “all” will overwrite all the scoped values. “error” will error if there is any overlap. Default is “conflicting”. Can be changed at the project-level by setting overwrite_scoped in a roadway_property_change project card.

Source code in network_wrangler/configs/wrangler.py
@dataclass
class EditsConfig(ConfigItem):
    """Configuration for Edits.

    Attributes:
        EXISTING_VALUE_CONFLICT: Only used if 'existing' provided in project card and
            `existing` doesn't match the existing network value. One of `error`, `warn`, or `skip`.
            `error` will raise an error, `warn` will warn the user, and `skip` will skip the change
            for that specific property (note it will still apply any remaining property changes).
            Defaults to `warn`. Can be overridden by setting `existing_value_conflict` in
            a `roadway_property_change` project card.

        OVERWRITE_SCOPED: How to handle conflicts with existing values.
            Should be one of "conflicting", "all", or False.
            "conflicting" will only overwrite values where the scope only partially overlaps with
            the existing value. "all" will overwrite all the scoped values. "error" will error if
            there is any overlap. Default is "conflicting". Can be changed at the project-level
            by setting `overwrite_scoped` in a `roadway_property_change` project card.
    """

    EXISTING_VALUE_CONFLICT: Literal["warn", "error", "skip"] = "warn"
    OVERWRITE_SCOPED: Literal["conflicting", "all", "error"] = "conflicting"

IdGenerationConfig

Bases: ConfigItem

Model Roadway Configuration.

Attributes:

Name Type Description
TRANSIT_SHAPE_ID_METHOD Literal['scalar']

method for creating a shape_id for a transit shape. Should be “scalar”.

TRANSIT_SHAPE_ID_SCALAR int

scalar value to add to general purpose lane to create a shape_id for a transit shape.

ROAD_SHAPE_ID_METHOD Literal['scalar']

method for creating a shape_id for a roadway shape. Should be “scalar”.

ROAD_SHAPE_ID_SCALAR int

scalar value to add to general purpose lane to create a shape_id for a roadway shape.

ML_LINK_ID_METHOD Literal['range', 'scalar']

method for creating a model_link_id for an associated link for a parallel managed lane.

ML_LINK_ID_RANGE tuple[int, int]

range of model_link_ids to use when creating an associated link for a parallel managed lane.

ML_LINK_ID_SCALAR int

scalar value to add to general purpose lane to create a model_link_id when creating an associated link for a parallel managed lane.

ML_NODE_ID_METHOD Literal['range', 'scalar']

method for creating a model_node_id for an associated node for a parallel managed lane.

ML_NODE_ID_RANGE tuple[int, int]

range of model_node_ids to use when creating an associated node for a parallel managed lane.

ML_NODE_ID_SCALAR int

scalar value to add to general purpose lane node ides create a model_node_id when creating an associated nodes for parallel managed lane.

Source code in network_wrangler/configs/wrangler.py
@dataclass
class IdGenerationConfig(ConfigItem):
    """Model Roadway Configuration.

    Attributes:
        TRANSIT_SHAPE_ID_METHOD: method for creating a shape_id for a transit shape.
            Should be "scalar".
        TRANSIT_SHAPE_ID_SCALAR: scalar value to add to general purpose lane to create a
            shape_id for a transit shape.
        ROAD_SHAPE_ID_METHOD: method for creating a shape_id for a roadway shape.
            Should be "scalar".
        ROAD_SHAPE_ID_SCALAR: scalar value to add to general purpose lane to create a
            shape_id for a roadway shape.
        ML_LINK_ID_METHOD: method for creating a model_link_id for an associated
            link for a parallel managed lane.
        ML_LINK_ID_RANGE: range of model_link_ids to use when creating an associated
            link for a parallel managed lane.
        ML_LINK_ID_SCALAR: scalar value to add to general purpose lane to create a
            model_link_id when creating an associated link for a parallel managed lane.
        ML_NODE_ID_METHOD: method for creating a model_node_id for an associated node
            for a parallel managed lane.
        ML_NODE_ID_RANGE: range of model_node_ids to use when creating an associated
            node for a parallel managed lane.
        ML_NODE_ID_SCALAR: scalar value to add to general purpose lane node ides create
            a model_node_id when creating an associated nodes for parallel managed lane.
    """

    TRANSIT_SHAPE_ID_METHOD: Literal["scalar"] = "scalar"
    TRANSIT_SHAPE_ID_SCALAR: int = 1000000
    ROAD_SHAPE_ID_METHOD: Literal["scalar"] = "scalar"
    ROAD_SHAPE_ID_SCALAR: int = 1000
    ML_LINK_ID_METHOD: Literal["range", "scalar"] = "scalar"
    ML_LINK_ID_RANGE: tuple[int, int] = (950000, 999999)
    ML_LINK_ID_SCALAR: int = 3000000
    ML_NODE_ID_METHOD: Literal["range", "scalar"] = "range"
    ML_NODE_ID_RANGE: tuple[int, int] = (950000, 999999)
    ML_NODE_ID_SCALAR: int = 15000

ModelRoadwayConfig

Bases: ConfigItem

Model Roadway Configuration.

Attributes:

Name Type Description
ML_OFFSET_METERS int

Offset in meters for managed lanes.

ADDITIONAL_COPY_FROM_GP_TO_ML list[str]

Additional fields to copy from general purpose to managed lanes.

ADDITIONAL_COPY_TO_ACCESS_EGRESS list[str]

Additional fields to copy to access and egress links.

Source code in network_wrangler/configs/wrangler.py
@dataclass
class ModelRoadwayConfig(ConfigItem):
    """Model Roadway Configuration.

    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_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_TO_ACCESS_EGRESS: list[str] = Field(default_factory=list)

WranglerConfig

Bases: ConfigItem

Configuration for Network Wrangler.

Attributes:

Name Type Description
IDS IdGenerationConfig

Parameteters governing how new ids are generated.

MODEL_ROADWAY ModelRoadwayConfig

Parameters governing how the model roadway is created.

CPU CpuConfig

Parameters for accessing CPU information. Will not change any outcomes.

EDITS EditsConfig

Parameters governing how edits are handled.

Source code in network_wrangler/configs/wrangler.py
@dataclass
class WranglerConfig(ConfigItem):
    """Configuration for Network Wrangler.

    Attributes:
        IDS: Parameteters governing how new ids are generated.
        MODEL_ROADWAY: Parameters governing how the model roadway is created.
        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()

Review changes beetween networks

Review Added Managed Lanes

from network_wrangler import load_roadway_from_dir
from projectcard import read_card
from pathlib import Path

EXAMPLE_DIR = Path.cwd().parent / "examples"
STPAUL = EXAMPLE_DIR / "stpaul"
STPAUL_ROAD = load_roadway_from_dir(STPAUL)

card_path = STPAUL / "project_cards" / "road.prop_change.managed_lanes.yml"
card = read_card(card_path)
stpaul_build = STPAUL_ROAD.apply(card)

ml_map = STPAUL_ROAD.links_df[STPAUL_ROAD.links_df.managed > 0].explore(
    color="blue",
    tiles="CartoDB positron",
    name="Managed Lanes",
    style_kwds={"opacity": 0.6, "weight": 20}
)

added_managed_lanes = stpaul_build.links_df[(stpaul_build.links_df.managed > 0) & (STPAUL_ROAD.links_df.managed == 0)]

added_managed_lanes.explore(
    m=ml_map,
    color="red",
    name="Added Managed Lanes",
    style_kwds={"opacity": 0.6, "weight": 20}
)

additional examples

You can see additional scenario review capabilities in the example jupyter notebook Visual Checks.ipynb.

Review selected facilities

Review selected links

from network_wrangler import load_roadway_from_dir
from pathlib import Path

EXAMPLE_DIR = Path.cwd().parent / "examples"
STPAUL = EXAMPLE_DIR / "stpaul"

STPAUL_ROAD = load_roadway_from_dir(STPAUL)
sel_dict = {
  "links": {
      "modes": ["walk"],
      "name": ["Valley Street"],
  },
  "from": {"model_node_id": 174762},
  "to": {"model_node_id": 43041},
}
STPAUL_ROAD.get_selection(sel_dict).selected_links_df.explore(
  color="red", style_kwds={"opacity": 0.6, "weight": 20}
)

additional examples

You can see additional interactive exploration of how selections work and how to review them in the Jupyter notebook Roadway Network Search.ipynb.

Create your own example data from Open Street Map

This script builds a basic OpenStreetMap (OSM) road network for a specified place.

This script uses the network_wrangler library to build a roadway network from OSM data. It allows you to specify the place name, network type, output path, and file format for the resulting network.

Usage

python build_basic_osm_roadnet.py <place_name> [--type <type>] [--path <path>] [--file_format <file_format>]

Parameters:

Name Type Description Default
place_name str

Name of the place to build the road network for.

required
--type Optional[str]

Type of network to build Defaults to drive.

required
--path Optional[str]

Path to write the network. Defaults to current working directory.

required
--file_format Optional[str]

File format for writing the network. Defaults to geojson.

required
Example
python build_basic_osm_roadnet.py "San Francisco, California" --type "drive" --path "/path/to/output" --file_format "geojson"

additional examples

You can review the process in this script step-wise and interactively create your own networks from OSM with variation in the underlying assumptions in the Jupyter notebook Create Network from OSM.ipynb.

Review separated model network managed lanes

Review model network

m_net = stpaul_build.model_net
model_net_map = m_net.gp_links_df.explore(
    tiles="CartoDB positron",
    color="blue",
    style_kwds={"opacity": 0.6, "weight": 10}
)
m_net.ml_links_df.explore(m=model_net_map, color="red", style_kwds={"opacity": 0.6, "weight": 10})
m_net.dummy_links_df.explore(m=model_net_map, color="green", style_kwds={"opacity": 0.6, "weight": 10})

additional examples

You can learn more about visualization of networks in the Jupyter notebook Network Viewer.ipynb.

{! include-markdown(“https://raw.githubusercontent.com/network-wrangler/projectcard/refs/heads/main/docs/how-to.md”) !}