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.

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

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

from aequilibrae.paths import TransitAssignment, TransitClass
from aequilibrae.utils.create_example import create_example
import numpy as np

# Imports for GTFS import
from aequilibrae.transit import Transit

# Imports for SF transit graph construction
from aequilibrae.project.database_connection import database_connection
from aequilibrae.transit.transit_graph_builder import TransitGraphBuilder

Let’s create an empty project on an arbitrary folder.

fldr = join(gettempdir(), uuid4().hex)

project = create_example(fldr, "coquimbo")

As the Coquimbo example already has a complete GTFS model, we shall remove its public transport database for the sake of this example.

remove(join(fldr, "public_transport.sqlite"))

Let’s import the GTFS feed.

dest_path = join(fldr, "gtfs_coquimbo.zip")

Now we create our Transit object and import the GTFS feed into our model. This will automatically create a new public transport database.

data = Transit(project)

transit = data.new_gtfs_builder(agency="LISANCO", file_path=dest_path)

To load the data, we must choose one date. We’re going to continue with 2016-04-13 but feel free to experiment with any other available dates. Transit class has a function allowing you to check dates for the GTFS feed. It should take approximately 2 minutes to load the data.

transit.load_date("2016-04-13")

Let’s save this model for later use.

transit.save_to_disk()

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(). We drop gemoetry here for the sake of display.

graph = data.create_graph(with_outer_stop_transfers=False, with_walking_edges=False, blocking_centroid_flows=False, connector_method="overlapping_regions")
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 20000000075 1_20001003000 31
363 364 alighting 20000000076 1_20001003000 32
364 365 alighting 20000000077 1_20001003000 33
365 366 alighting 20000000078 1_20001003000 34
366 367 alighting 20000000053 1_20001003000 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_20001001000 0 212 290 90.000000 inf 1
1 2 on-board 1_20001001000 1 213 291 90.000000 inf 1
2 3 on-board 1_20001001000 2 214 292 120.000000 inf 1
3 4 on-board 1_20001001000 3 215 293 120.000000 inf 1
4 5 on-board 1_20001001000 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()
/opt/hostedtoolcache/Python/3.10.14/x64/lib/python3.10/site-packages/aequilibrae/project/network/network.py:327: FutureWarning: Downcasting object dtype arrays on .fillna, .ffill, .bfill is deprecated and will change in a future version. Call result.infer_objects(copy=False) instead. To opt-in to the future behavior, set `pd.set_option('future.no_silent_downcasting', True)`
  df = pd.read_sql(sql, conn).fillna(value=np.nan)

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.14/x64/lib/python3.10/site-packages/aequilibrae/transit/transit_graph_builder.py:1214: 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.14/x64/lib/python3.10/site-packages/aequilibrae/transit/transit.py:91: 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.14/x64/lib/python3.10/site-packages/aequilibrae/transit/transit.py:105: 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.

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 20000000075 1_20001003000 31
363 364 alighting 20000000076 1_20001003000 32
364 365 alighting 20000000077 1_20001003000 33
365 366 alighting 20000000078 1_20001003000 34
366 367 alighting 20000000053 1_20001003000 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_20001001000 0 212 290 90.000000 inf 1
1 2 on-board 1_20001001000 1 213 291 90.000000 inf 1
2 3 on-board 1_20001001000 2 214 292 120.000000 inf 1
3 4 on-board 1_20001001000 3 215 293 120.000000 inf 1
4 5 on-board 1_20001001000 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()

Spiess & Florian assignment#

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.

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

assig = TransitAssignment()

# Create the assignment class
assigclass = TransitClass(name="pt", graph=transit_graph, matrix=mat)
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 with the mock demand matrx for all TransitClass's added.

assig.execute()

View the results

assig.results()
pt_volume
link_id
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



We can also access the TransitAssignmentResults object from the TransitClass

assigclass.results
<aequilibrae.paths.results.assignment_results.TransitAssignmentResults object at 0x7fa8d0d5dd50>

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 11.919 seconds)

Gallery generated by Sphinx-Gallery