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

# 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.869 seconds)

Gallery generated by Sphinx-Gallery