RidePy Tutorial 2: Faster simulations using Cython

In Tutorial 1 we have seen how to set up a basic simulation, run, and analyze it. This tutorial is concerned with significantly increasing simulation performance by replacing the crucial components with their Cython equivalents.

To demonstrate this, we will first show the complete process again in a condensed form, using the Python components. Afterwards, we will do the same using the Cython components.

The result processing and data analytics steps will not be covered again, as they are identical to using the Python components.

Simulation using Python components

First, we import all necessary Python components:

from ridepy.util.spaces import Euclidean2D
from ridepy.util.request_generators import RandomRequestGenerator
from ridepy.fleet_state import SlowSimpleFleetState
from ridepy.vehicle_state import VehicleState
from ridepy.util.dispatchers import BruteForceTotalTravelTimeMinimizingDispatcher

import itertools as it

Now we can set up the simulation as explained in Tutorial 1:

space = Euclidean2D()

rg = RandomRequestGenerator(
    rate=10,
    max_pickup_delay=3,
    max_delivery_delay_rel=1.9,
    space=space,
    seed=42,
)

n_buses = 50
initial_location = (0, 0)

fs = SlowSimpleFleetState(
    initial_locations={vehicle_id: initial_location for vehicle_id in range(n_buses)},
    seat_capacities=8,
    space=space,
    dispatcher=BruteForceTotalTravelTimeMinimizingDispatcher(),
    vehicle_state_class=VehicleState,
)

transportation_requests = it.islice(rg, 100)

And finally run the simulation. This time we will use the %time magic to record the execution time:

%time events = list(fs.simulate(transportation_requests))
CPU times: user 476 ms, sys: 0 ns, total: 476 ms
Wall time: 371 ms

The simulation is now done and events have been produced:

events[200:203]
[{'event_type': 'RequestAcceptanceEvent',
  'timestamp': 4.229722339631084,
  'request_id': 49,
  'origin': (0.5503253124498481, 0.05058832952488124),
  'destination': (0.9992824684127266, 0.8360275850799519),
  'pickup_timewindow_min': 4.229722339631084,
  'pickup_timewindow_max': 7.229722339631084,
  'delivery_timewindow_min': 4.229722339631084,
  'delivery_timewindow_max': 6.853344745923743},
 {'event_type': 'PickupEvent',
  'timestamp': 4.331264366244138,
  'request_id': 33,
  'vehicle_id': 1},
 {'event_type': 'DeliveryEvent',
  'timestamp': 4.338826458385998,
  'request_id': 35,
  'vehicle_id': 3}]

Simulation using Cython components

Now let’s repeat the same process using the Cython components. The crucial step to do this is to change the imports to using the Cython equivalents of the TransportSpace, the VehicleState, the TransportationRequest, and the Dispatcher. To avoid name collisions with the Python components we will import them as prefixed with Cy/cy:

from ridepy.util.spaces_cython import Euclidean2D as CyEuclidean2D
from ridepy.data_structures_cython import TransportationRequest as CyTransportationRequest
from ridepy.vehicle_state_cython import VehicleState as CyVehicleState
from ridepy.util.dispatchers_cython import (
    BruteForceTotalTravelTimeMinimizingDispatcher as CyBruteForceTotalTravelTimeMinimizingDispatcher,
)

Now that’s basically it. We will now do the same configuration as above, using the new Cython components.

There are two little extra changes we have to make: The first is to supply our RandomRequestGenerator with the Cython TransportationRequest type as request_class to make it supply those instead of the Python ones.

Secondly, the Cython Dispatcher needs to know about the type of spatial coordinates it is dealing with and therefore needs to be handed the TransportSpace’s loc_type attribute.

space = CyEuclidean2D()

rg = RandomRequestGenerator(
    rate=10,
    max_pickup_delay=3,
    max_delivery_delay_rel=1.9,
    space=space,
    seed=42,
    request_class=CyTransportationRequest
)

n_buses = 50
initial_location = (0, 0)

fs = SlowSimpleFleetState(
    initial_locations={vehicle_id: initial_location for vehicle_id in range(n_buses)},
    seat_capacities=8,
    space=space,
    dispatcher=CyBruteForceTotalTravelTimeMinimizingDispatcher(space.loc_type),
    vehicle_state_class=CyVehicleState,
)

transportation_requests = it.islice(rg, 100)

Now we’re ready to run the cythonized simulation:

%time cy_events = list(fs.simulate(transportation_requests))
CPU times: user 9.1 ms, sys: 97 µs, total: 9.2 ms
Wall time: 9.16 ms

The simulation done and again, events have been produced:

cy_events[200:203]
[{'event_type': 'RequestAcceptanceEvent',
  'timestamp': 4.229722339631084,
  'request_id': 49,
  'origin': (0.5503253124498481, 0.05058832952488124),
  'destination': (0.9992824684127266, 0.8360275850799519),
  'pickup_timewindow_min': 4.229722339631084,
  'pickup_timewindow_max': 7.229722339631084,
  'delivery_timewindow_min': 4.229722339631084,
  'delivery_timewindow_max': 6.853344745923743},
 {'event_type': 'PickupEvent',
  'timestamp': 4.331264366244138,
  'request_id': 33,
  'vehicle_id': 1},
 {'event_type': 'DeliveryEvent',
  'timestamp': 4.338826458385998,
  'request_id': 35,
  'vehicle_id': 3}]

So that’s it. In this case, the Cython version was more than 20 times faster than the Python version.