Skip to main content
`Ctrl`+`K`

Note

Go to the end to download the full example code.

# Public transport assignment with Optimal Strategies

In this example, we import a GTFS feed to our model, create a public transport network, create project match connectors, and perform a Spiess & Florian assignment. Click here to check out the article.

We use data from Coquimbo, a city in La Serena Metropolitan Area in Chile.

References

- Transit assignment

See also

Several functions, methods, classes and modules are used in this example:

- `aequilibrae.transit.Transit()`

- `aequilibrae.transit.TransitGraphBuilder()`

- `aequilibrae.paths.TransitClass()`

- `aequilibrae.paths.TransitAssignment()`

- `aequilibrae.matrix.AequilibraeMatrix()`

    # Imports for example construction
    from uuid import uuid4
    from os.path import join
    from tempfile import gettempdir

    from aequilibrae.transit import Transit
    from aequilibrae.utils.create_example import create_example

    # Let's create an empty project on an arbitrary folder.
    fldr = join(gettempdir(), uuid4().hex)

    project = create_example(fldr, "coquimbo")

Let’s create our `Transit` object.

    data = Transit(project)

## Graph building

Let’s build the transit network. We’ll disable `outer_stop_transfers` and `walking_edges` because Coquimbo doesn’t have any parent stations.

For the OD connections we’ll use the `overlapping_regions` method and create some accurate line geometry later. Creating the graph should only take a moment. By default zoning information is pulled from the project network. If you have your own zoning information add it using `graph.add_zones(zones)` then `graph.create_graph()`.

    graph = data.create_graph(with_outer_stop_transfers=False, with_walking_edges=False, blocking_centroid_flows=False, connector_method="overlapping_regions")

    # We drop geometry here for the sake of display.
    graph.vertices.drop(columns="geometry")

| node_id | node_type | stop_id | line_id | line_seg_idx | taz_id
--- | --- | --- | --- | --- | --- | ---
index |  |  |  |  |  |
0 | 1 | od |  |  | -1 | 1
1 | 2 | od |  |  | -1 | 2
2 | 3 | od |  |  | -1 | 3
3 | 4 | od |  |  | -1 | 4
4 | 5 | od |  |  | -1 | 5
... | ... | ... | ... | ... | ... | ...
362 | 363 | alighting | 10000000075 | 1_10001003000 | 31 |
363 | 364 | alighting | 10000000076 | 1_10001003000 | 32 |
364 | 365 | alighting | 10000000077 | 1_10001003000 | 33 |
365 | 366 | alighting | 10000000078 | 1_10001003000 | 34 |
366 | 367 | alighting | 10000000053 | 1_10001003000 | 35 |

367 rows × 6 columns

    graph.edges

| link_id | link_type | line_id | stop_id | line_seg_idx | b_node | a_node | trav_time | freq | o_line_id | d_line_id | direction
--- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | ---
index |  |  |  |  |  |  |  |  |  |  |  |
0 | 1 | on-board | 1_10001001000 |  | 0 | 212 | 290 | 90.000000 | inf |  |  | 1
1 | 2 | on-board | 1_10001001000 |  | 1 | 213 | 291 | 90.000000 | inf |  |  | 1
2 | 3 | on-board | 1_10001001000 |  | 2 | 214 | 292 | 120.000000 | inf |  |  | 1
3 | 4 | on-board | 1_10001001000 |  | 3 | 215 | 293 | 120.000000 | inf |  |  | 1
4 | 5 | on-board | 1_10001001000 |  | 4 | 216 | 294 | 120.000000 | inf |  |  | 1
... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ...
641 | 642 | egress_connector |  |  | -1 | 166 | 112 | 630.543431 | inf |  |  | 1
642 | 643 | egress_connector |  |  | -1 | 177 | 112 | 754.081137 | inf |  |  | 1
643 | 644 | egress_connector |  |  | -1 | 178 | 112 | 386.262027 | inf |  |  | 1
644 | 645 | egress_connector |  |  | -1 | 179 | 112 | 558.108038 | inf |  |  | 1
645 | 646 | egress_connector |  |  | -1 | 166 | 113 | 604.831967 | inf |  |  | 1

646 rows × 12 columns

The graphs also also stored in the `Transit.graphs` dictionary. They are keyed by the ‘period_id’ they were created for. A graph for a different ‘period_id’ can be created by providing `period_id=` in the `Transit.create_graph` call. You can view previously created periods with the `Periods` object.

    periods = project.network.periods
    periods.data

| period_id | period_start | period_end | period_description
--- | --- | --- | --- | ---
0 | 1 | 0 | 86400 | Default time period, whole day

## Connector project matching

    project.network.build_graphs()

Now we’ll create the line strings for the access connectors, this step is optinal but provides more accurate distance estimations and better looking geometry.

Because Coquimbo doesn’t have many walking edges we’ll match onto the `"c"` graph.

    graph.create_line_geometry(method="connector project match", graph="c")

    /opt/hostedtoolcache/Python/3.10.15/x64/lib/python3.10/site-packages/aequilibrae/transit/transit_graph_builder.py:1215: UserWarning: In its current implementation, the "connector project match" method may take a while for large networks.
      warnings.warn(

## Saving and reloading

Lets save all graphs to the ‘public_transport.sqlite’ database.

    data.save_graphs()

    /opt/hostedtoolcache/Python/3.10.15/x64/lib/python3.10/site-packages/aequilibrae/transit/transit.py:99: UserWarning: Currently only a single transit graph can be saved and reloaded. Multiple graph support is plan for a future release.
      warnings.warn(

We can reload the saved graphs with `data.load`. This will create new `TransitGraphBuilder`'s based on the ‘period_id’ of the saved graphs. The graph configuration is stored in the ‘transit_graph_config’ table in ‘project_database.sqlite’ as serialised JSON.

    data.load()

    /opt/hostedtoolcache/Python/3.10.15/x64/lib/python3.10/site-packages/aequilibrae/transit/transit.py:113: UserWarning: Currently only a single transit graph can be saved and reloaded. Multiple graph support is plan for a future release. `period_ids` argument is currently ignored.
      warnings.warn(

Links and nodes are stored in a similar manner to the ‘project_database.sqlite’ database.

## Reading back into AequilibraE

You can create back in a particular graph via it’s ‘period_id’.

    from aequilibrae.project.database_connection import database_connection
    from aequilibrae.transit.transit_graph_builder import TransitGraphBuilder

    pt_con = database_connection("transit")

    graph_db = TransitGraphBuilder.from_db(pt_con, periods.default_period.period_id)
    graph_db.vertices.drop(columns="geometry")

| node_id | node_type | stop_id | line_id | line_seg_idx | taz_id
--- | --- | --- | --- | --- | --- | ---
0 | 1 | od |  |  | -1 | 1
1 | 2 | od |  |  | -1 | 2
2 | 3 | od |  |  | -1 | 3
3 | 4 | od |  |  | -1 | 4
4 | 5 | od |  |  | -1 | 5
... | ... | ... | ... | ... | ... | ...
362 | 363 | alighting | 10000000075 | 1_10001003000 | 31 |
363 | 364 | alighting | 10000000076 | 1_10001003000 | 32 |
364 | 365 | alighting | 10000000077 | 1_10001003000 | 33 |
365 | 366 | alighting | 10000000078 | 1_10001003000 | 34 |
366 | 367 | alighting | 10000000053 | 1_10001003000 | 35 |

367 rows × 6 columns

    graph_db.edges

| link_id | link_type | line_id | stop_id | line_seg_idx | b_node | a_node | trav_time | freq | o_line_id | d_line_id | direction
--- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | ---
0 | 1 | on-board | 1_10001001000 |  | 0 | 212 | 290 | 90.000000 | inf |  |  | 1
1 | 2 | on-board | 1_10001001000 |  | 1 | 213 | 291 | 90.000000 | inf |  |  | 1
2 | 3 | on-board | 1_10001001000 |  | 2 | 214 | 292 | 120.000000 | inf |  |  | 1
3 | 4 | on-board | 1_10001001000 |  | 3 | 215 | 293 | 120.000000 | inf |  |  | 1
4 | 5 | on-board | 1_10001001000 |  | 4 | 216 | 294 | 120.000000 | inf |  |  | 1
... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ...
641 | 642 | egress_connector |  |  | -1 | 166 | 112 | 1039.945114 | inf |  |  | 1
642 | 643 | egress_connector |  |  | -1 | 177 | 112 | 1149.832226 | inf |  |  | 1
643 | 644 | egress_connector |  |  | -1 | 178 | 112 | 508.359402 | inf |  |  | 1
644 | 645 | egress_connector |  |  | -1 | 179 | 112 | 968.705083 | inf |  |  | 1
645 | 646 | egress_connector |  |  | -1 | 166 | 113 | 1831.651685 | inf |  |  | 1

646 rows × 12 columns

## Converting to a AequilibraE graph object

To perform an assignment we need to convert the graph builder into a graph.

    transit_graph = graph.to_transit_graph()

## Mock demand matrix

We’ll create a mock demand matrix with demand 1 for every zone. We’ll also need to convert from `zone_id`'s to `node_id`'s.

    import numpy as np
    from aequilibrae.matrix import AequilibraeMatrix

    zones_in_the_model = len(transit_graph.centroids)

    names_list = ['pt']

    mat = AequilibraeMatrix()
    mat.create_empty(zones=zones_in_the_model,
                     matrix_names=names_list,
                     memory_only=True)
    mat.index = transit_graph.centroids[:]
    mat.matrices[:, :, 0] = np.full((zones_in_the_model, zones_in_the_model), 1.0)
    mat.computational_view()

## Hyperpath generation/assignment

We’ll create a `TransitAssignment` object as well as a `TransitClass`

    from aequilibrae.paths import TransitAssignment, TransitClass

    # Create the assignment class
    assigclass = TransitClass(name="pt", graph=transit_graph, matrix=mat)

    assig = TransitAssignment()

    assig.add_class(assigclass)

    # We need to tell AequilbraE where to find the appropriate fields we want to use,
    # as well as the assignment algorithm to use.
    assig.set_time_field("trav_time")
    assig.set_frequency_field("freq")

    assig.set_algorithm("os")

    # When there's multiple matrix cores we'll also need to set the core to use for the demand.
    assigclass.set_demand_matrix_core("pt")

    # Let's perform the assignment for the transit classes added
    assig.execute()

View the results

    assig.results()

| pt_volume
--- | ---
479 | 0.0
480 | 35.0
481 | 24.0
482 | 18.0
483 | 12.0
... | ...
74 | 71.0
75 | 48.0
76 | 48.0
77 | 54.0
78 | 27.0

646 rows × 1 columns

## Saving results

We’ll be saving the results to another sqlite db called ‘results_database.sqlite’. The ‘results’ table with ‘project_database.sqlite’ contains some metadata about each table in ‘results_database.sqlite’.

    assig.save_results(table_name='hyperpath example')

Wrapping up

    project.close()

Total running time of the script: (0 minutes 4.959 seconds)

Gallery generated by Sphinx-Gallery