update 2021-05-27

This commit is contained in:
LucasJavaudin 2021-05-27 10:27:41 +02:00
parent d8d96c9c61
commit cb29cd2473
25 changed files with 1025 additions and 6 deletions

5
Makefile Normal file
View File

@ -0,0 +1,5 @@
init:
pip install -r requirements.txt
test:
py.test tests

View File

@ -4,11 +4,100 @@ Within-Day Model
The within-day model simulates the movements of vehicles and individuals in the network, for a single day.
It is an event-based model that triggers event according to the time of the day.
This page lists all events in MetroSim.
Events
------
PVReachesNode
--------------
^^^^^^^^^^^^^
This event signals that a private vehicle reaches a node, at a given time.
Expected Travel Times
---------------------
At each node of the network, car drivers choose the next edge that they will take.
The choice is made based on the expected utility.
For each downstream edge, the expected utility depends on
- the past travel time of the car driver from the origin to the current node,
- the observed travel time on the downstream edge,
- the expected travel time from the target node to the destination,
- the observed road toll on the downstream edge,
- the expected road toll from the target node to the destination.
This mean that MetroSim should know, for each pair of nodes :math:`(s, t)` and for each departure time :math:`t_d`, all feasible and non-Pareto-dominated pairs :math:`({tt}, \tau)` where :math:`{tt}` is the travel time from :math:`s` to :math:`t` and :math:`\tau` is the toll paid.
A pair :math:`({tt}, \tau)` is feasible if there is at least one path, from :math:`s` to :math:`t`, starting at time :math:`t_d` such that the travel time is :math:`{tt}` and the toll paid is :math:`\tau`.
A pair :math:`({tt}, \tau)` is Pareto dominated if there is another feasible pair :math:`({tt}', \tau')` such that :math:`{tt}\geq{tt}'` and :math:`\tau\geq\tau'`, where at least one inequality is strict.
It can be very computationally and memory intensive to compute and store all the feasible and non-Pareto-dominated :math:`({tt}, \tau)` pairs, for all node pairs and for all departure times (or even for some departure-time intervals).
Therefore, each time a car driver wants to compute the expected travel utility from a source node :math:`s` to a target node :math:`t`, starting at time :math:`t_d`, MetroSim uses the following rules:
1. If the pairs :math:`({tt}, \tau)` are known for both a starting time :math:`t_d' \in [t_d - M, t_d - m]` and for a starting time :math:`t_d'' \in [t_d + m, t_d + M]`, then compute expected travel times using linear interpolation.
2. If the pairs :math:`({tt}, \tau)` are known for a starting time :math:`t_d' \in [t_d - m, t_d + m]`, then use the expected travel times with starting time :math:`t_d'` as a proxy for the expected travel times with starting time :math:`t_d`.
3. If the pairs :math:`({tt}, \tau)` are known for a starting time :math:`t_d' \in [t_d - M, t_d - m]` only, then compute expected travel times with starting time :math:`t_d'' = t_d' + 2 \cdot M`, store the results and use a linear interpolation between :math:`t_d'` and :math:`t_d''`.
4. In all other cases, compute expected travel times with starting time :math:`t_d`, store the results and use them.
The parameters :math:`m` and :math:`M` can be fixed by the user.
Reasonable values are :math:`m=30` seconds and :math:`M=10` minutes.
If memory is getting low, we can remove all the expected travel-time results such that the starting time is earlier than the current time of the simulation minus :math:`M`.
When computing the expected travel times from :math:`s` to :math:`t`, we can also get the expected travel times from :math:`s` to any node :math:`t'` on the fastest paths.
Example
^^^^^^^
We consider the following road network, where all edges only go from West to East.
To keep it simple, free-flow travel-times are set to 1 minute for each edge and we assume that they are never congested.
The edge from node 2 to node 4 is the only edge with a road toll, set to 1 euro.
We set :math:`m=30` seconds and :math:`M=10` minutes.
+-----------+---+--------+--------------------+------------------------------+-------------------------------+
| From / to | 1 | 2 | 3 | 4 | 5 |
+-----------+---+--------+--------------------+------------------------------+-------------------------------+
| 1 | X | (1, 0) | X | X | X |
+-----------+---+--------+--------------------+------------------------------+-------------------------------+
| 2 | X | X | (1, 0) | (1, 1) | X |
+-----------+---+--------+--------------------+------------------------------+-------------------------------+
| 3 | X | X | X | (1, 0) | X |
+-----------+---+--------+--------------------+------------------------------+-------------------------------+
| 4 | X | X | X | X | (1, 0) |
+-----------+---+--------+--------------------+------------------------------+-------------------------------+
| 5 | X | X | X | X | X |
+-----------+---+--------+--------------------+------------------------------+-------------------------------+
.. plot:: plots/network.py
Example road network
Assume that the first car entering the network goes from node 1 to node 5, starting at 08:00:00.
Following rule 4., we compute expected travel times with starting time 08:00:00.
Two paths are found: the path (1, 2, 3, 4, 5) with 4-minute travel-time and no toll and the path (1, 2, 4, 5) with 3-minute travel-time and a toll of 1 euro.
The car driver chooses the best path to take, given his preferences (value of time, schedule utility, etc.).
At the same time, we have also found two paths from node 1 to node 4, with their travel-time and toll values, starting at 08:00:00.
And we have found two paths from node 2 to node 5, with their travel-time and toll values, starting at 08:01:00.
All these results are stored in memory (only the travel times and tolls are stored, not the paths).
+-----------+---+--------+--------------------+------------------------------+-------------------------------+
| From / to | 1 | 2 | 3 | 4 | 5 |
+-----------+---+--------+--------------------+------------------------------+-------------------------------+
| 1 | X | (1, 0) | (2, 0) at 08:00:00 | [(3, 0), (2, 1)] at 08:00:00 | [(4, 0), (3, 1)] at 08:00:00 |
+-----------+---+--------+--------------------+------------------------------+-------------------------------+
| 2 | X | X | (1, 0) | (1, 1) | [(3, 0), (2, 1)] at 08:01:00 |
+-----------+---+--------+--------------------+------------------------------+-------------------------------+
| 3 | X | X | X | (1, 0) | (2, 0) at 08:02:00 |
+-----------+---+--------+--------------------+------------------------------+-------------------------------+
| 4 | X | X | X | X | (1, 0) |
+-----------+---+--------+--------------------+------------------------------+-------------------------------+
| 5 | X | X | X | X | X |
+-----------+---+--------+--------------------+------------------------------+-------------------------------+
Assume that the next car to move goes from node 1 to node 4, starting at time 08:00:45.
The only known results between node 1 and node 4 are for a starting time at 08:00:00.
Then, following rule 3., we compute the fastest paths for a starting time at 08:10:00.
We find the same two paths as previously.
.. warning::
TODO: Enrich the algorithm to consider cordon tolls, mobility permits and class-specific policies.

View File

@ -10,9 +10,9 @@
# add these directories to sys.path here. If the directory is relative to the
# documentation root, use os.path.abspath to make it absolute, like shown here.
#
# import os
# import sys
# sys.path.insert(0, os.path.abspath('.'))
import os
import sys
sys.path.insert(0, os.path.abspath('..'))
# -- Project information -----------------------------------------------------
@ -32,6 +32,8 @@ extensions = [
'sphinxcontrib.plantuml',
'sphinx_rtd_theme',
'matplotlib.sphinxext.plot_directive',
'sphinx.ext.autodoc',
'sphinx.ext.viewcode',
]
# Add any paths that contain templates here, relative to this directory.

View File

@ -35,6 +35,9 @@ It is intended at developers willing to improve or extend it.
.. toctree::
:caption: References
/references/models/models
/references/initializer/initializer
/references/simulator/simulator
/references/changelog
.. toctree::

32
docs/plots/network.py Normal file
View File

@ -0,0 +1,32 @@
import matplotlib.pyplot as plt
# Update matplotlib parameters.
params = {'text.usetex': True,
'figure.dpi': 200,
'font.size': 14,
'font.serif': [],
'font.sans-serif': [],
'font.monospace': [],
'axes.labelsize': 16,
'axes.titlesize': 18,
'axes.linewidth': .6,
'legend.fontsize': 14,
'xtick.labelsize': 12,
'ytick.labelsize': 12,
'font.family': 'serif'}
plt.rcParams.update(params)
plt.figure(figsize=(8, 2))
plt.plot([0, 1, 3, 4], [1, 1, 1, 1], '-o', color='black')
plt.plot([1, 2, 3], [1, 0, 1], '-o', color='black')
plt.annotate('1', (0, 1), (0, .9), va='top', ha='center')
plt.annotate('2', (1, 1), (1, .9), va='top', ha='center')
plt.annotate('3', (2, 0), (2, -.1), va='top', ha='center')
plt.annotate('4', (3, 1), (3, .9), va='top', ha='center')
plt.annotate('5', (4, 1), (4, .9), va='top', ha='center')
plt.axis('off')
plt.tight_layout()
plt.show()

View File

@ -0,0 +1,5 @@
GTFS
====
.. automodule:: metrosim.initializer.gtfs
:members:

View File

@ -0,0 +1,5 @@
Initializer
===========
.. toctree::
gtfs

View File

@ -0,0 +1,5 @@
Models
======
.. toctree::
pt_network

View File

@ -0,0 +1,5 @@
Public-Transit Network
======================
.. automodule:: metrosim.models.pt_network
:members:

View File

@ -0,0 +1,6 @@
Manager
=======
.. autoclass:: metrosim.simulator.manager.MetroManager
:members:

View File

@ -0,0 +1,5 @@
Simulator
=========
.. toctree::
manager

18
metrosim/exceptions.py Normal file
View File

@ -0,0 +1,18 @@
"""This module contains the exceptions of MetroSim.
"""
# Copyright Lucas Javaudin - All Rights Reserved
# Unauthorized copying of this file, via any medium is strictly prohibited
# Proprietary and confidential
# Written by Lucas Javaudin <me@lucasjavaudin.com>, 2021
class MetroIOError(Exception):
"""Error reading or writing a file."""
class MetroInputError(Exception):
"""Bad format of input file."""
class MetroUnsupportedError(Exception):
"""Unsupported feature."""

View File

View File

@ -0,0 +1,209 @@
"""This module contains function to create MetroSim input data from GTFS files.
"""
# Copyright Lucas Javaudin - All Rights Reserved
# Unauthorized copying of this file, via any medium is strictly prohibited
# Proprietary and confidential
# Written by Lucas Javaudin <me@lucasjavaudin.com>, 2021
import zipfile
from zipfile import ZipFile
from io import TextIOWrapper
import csv
from datetime import datetime, timedelta
from metrosim.models.pt_network import PTNetwork
from metrosim.exceptions import MetroIOError, MetroInputError
INT_TO_WEEKDAY = {0: 'monday', 1: 'tuesday', 2: 'wednesday', 3: 'thursday',
4: 'friday', 5: 'saturday', 6: 'sunday'}
def gtfs_to_ptnetwork(gtfs_path, at_date, start_time=timedelta(0),
end_time=timedelta(hours=30)):
"""Create a PTNetwork instance from a GTFS file.
The GTFS file must respect the `reference
<https://developers.google.com/transit/gtfs/reference>`_ defined by Google,
although some files and fields are not used.
The required files are `trips.txt`, `routes.txt`, `stop_times.txt` and
`stops.txt`. Also, either `calendar.txt` or `calendar_dates.txt` (or both)
must be in the GTFS file.
The parameters can be used to filter a specific day and time period. By
default, the PTNetwork covers the entire day.
The PTNetwork cannot cover more than one day.
:param str gtfs_path: Path to the GTFS zipfile on the computer.
:param at_date: Only trips that run during this day are added to the
PTNetwork.
:type at_date: :class:`datetime.date`
:param start_time: Only trips that run after this time of the day are added
to the PTNetwork. It represents an amount of time after midnight (it can
be larger than 24 hours).
:type start_time: :class:`datetime.timedelta`
:param end_time: Only trips that run before this time of the day are added
to the PTNetwork. It represents an amount of time after midnight (it can
be larger than 24 hours).
:type end_time: :class:`datetime.timedelta`
:return: :class:`metrosim.models.pt_network.PTNetwork`
:rtype: PTNetwork
"""
if not isinstance(gtfs_path, str) or not zipfile.is_zipfile(gtfs_path):
raise MetroIOError('Invalid zip file')
with ZipFile(gtfs_path) as gtfs_zip:
# Check that the GTFS file has all the required files.
required_files = ('trips.txt', 'routes.txt', 'stop_times.txt',
'stops.txt')
for filename in required_files:
if filename not in gtfs_zip.namelist():
msg = (
'The GTFS file must have the following required files: {}')
raise MetroInputError(msg.format(required_files))
# Find the active services at the desired date.
active_services = set()
if 'calendar.txt' in gtfs_zip.namelist():
with gtfs_zip.open('calendar.txt') as calendar_csv:
reader = csv.DictReader(TextIOWrapper(calendar_csv, 'utf-8'))
services = _get_services_from_calendar(reader, at_date)
active_services = active_services.union(services)
if 'calendar_dates.txt' in gtfs_zip.namelist():
with gtfs_zip.open('calendar_dates.txt') as calendar_dates_csv:
reader = csv.DictReader(
TextIOWrapper(calendar_dates_csv, 'utf-8'))
services, no_services = _get_services_from_calendar_dates(
reader, at_date)
active_services = active_services.union(services)
active_services = active_services.difference(no_services)
if not active_services:
# No service found in the GTFS at the desired date.
msg = 'The GTFS file does not cover the date: {}'
raise MetroInputError(msg.format(at_date))
pt_network = PTNetwork()
# Read the PT stops.
with gtfs_zip.open('stops.txt') as stops_csv:
reader = csv.DictReader(TextIOWrapper(stops_csv, 'utf-8'))
pt_network._read_nodes(reader)
# Find active trips and routes at the desired date.
with gtfs_zip.open('trips.txt') as trips_csv:
reader = csv.DictReader(TextIOWrapper(trips_csv, 'utf-8'))
trips, routes = _get_active_at_date(reader, active_services)
# Add the valid routes to the PT network.
with gtfs_zip.open('routes.txt') as routes_csv:
reader = csv.DictReader(TextIOWrapper(routes_csv, 'utf-8'))
pt_network._add_valid_routes(reader, routes)
# Add the valid trips to the PT network.
with gtfs_zip.open('trips.txt') as trips_csv:
reader = csv.DictReader(TextIOWrapper(trips_csv, 'utf-8'))
pt_network._add_valid_trips(reader, trips)
# Add edges to the PT network from the stop_times.
with gtfs_zip.open('stop_times.txt') as stop_times_csv:
reader = csv.DictReader(TextIOWrapper(stop_times_csv, 'utf-8'))
pt_network._read_stop_times(reader, start_time, end_time)
pt_network._remove_unused_platforms()
if 'transfers.txt' in gtfs_zip.namelist():
# Add edges to the PT network from transfers.
with gtfs_zip.open('transfers.txt') as transfers_times_csv:
reader = csv.DictReader(
TextIOWrapper(transfers_times_csv, 'utf-8'))
pt_network._read_transfers(reader)
pt_network._add_generic_transfers()
pt_network._remove_unused_nodes()
print('Succesfully created a PT network with {} nodes and {} edges'.format(
pt_network.number_of_nodes(), pt_network.number_of_edges()))
return pt_network
def _get_services_from_calendar(reader, at_date):
"""Returns the set of services enabled at a given date.
The function reads the services from a csv.DictReader representing the
`calendar.txt` file of a GTFS.
:param reader: DictReader of a CSV representing calendar.txt.
:param at_date: Date for which the active services are found.
:type at_date: :class:`datetime.date`
:return: Set with the ids of the services enables at the given date.
:rtype: set
"""
# Get the weeday of the desired date.
at_weekday = INT_TO_WEEKDAY[at_date.weekday()]
valid_services = set()
for row in reader:
start_date = datetime.strptime(row['start_date'], '%Y%m%d').date()
end_date = datetime.strptime(row['end_date'], '%Y%m%d').date()
if start_date > at_date or end_date < at_date:
# The service does not overlap with the desired date.
continue
if row[at_weekday] == '1':
# The service is active for the desired date.
valid_services.add(row['service_id'])
return valid_services
def _get_services_from_calendar_dates(reader, at_date):
"""Returns the services explicitely enabled or disabled at a given date.
The function reads the services from a csv.DictReader representing the
`calendar_dates.txt` file of a GTFS.
:param reader: DictReader of a CSV representing calendar_dates.txt.
:param at_date: Date for which the added and removed services are found.
:type at_date: :class:`datetime.date`
:return: Tuple with a set of the ids of the services explicitely enabled
and a set of the ids of the services explicitely disabled, at the given
date.
:rtype: tuple
"""
valid_services = set()
invalid_services = set()
for row in reader:
row_date = datetime.strptime(row['date'], '%Y%m%d').date()
if row_date == at_date:
if row['exception_type'] == '1':
# exception_type = 1 indicates that service has been added for
# this date.
valid_services.add(row['service_id'])
elif row['exception_type'] == '2':
# exception_type = 2 indicates that service has been removed
# for this date.
invalid_services.add(row['service_id'])
return valid_services, invalid_services
def _get_active_at_date(reader, services):
"""Returns the trips and routes that run given a set of services.
The function reads the trips and routes from a csv.DictReader representing
the `trips.txt` file of a GTFS.
Only the trips whose service is in the set of services given as argument
are returned.
:param reader: DictReader of a CSV representing trips.txt.
:param set services: Set with the ids of active services.
:return: Tuple with a set of the ids of the trips and a set of the ids of
the routes that are active.
:rtype: tuple
"""
trips = set()
routes = set()
for row in reader:
if row['service_id'] in services:
trips.add(row['trip_id'])
routes.add(row['route_id'])
return trips, routes

View File

View File

@ -0,0 +1,27 @@
from metrosim.models.modes import mode_from_dict
class _ModeChoice(object):
def __init__(self, modes):
self.modes = modes
def modes_from_dict(mode_dicts):
modes = list()
for mode_data in mode_dicts:
modes.append(mode_from_dict(mode_data))
return modes
class DeterministicModeChoice(_ModeChoice):
def from_dict(mode_dicts):
modes = _ModeChoice.modes_from_dict(mode_dicts)
return DeterministicModeChoice(modes)
class StochasticModeChoice(_ModeChoice):
def from_dict(mode_dicts):
modes = _ModeChoice.modes_from_dict(mode_dicts)
return StochasticModeChoice(modes)

View File

@ -0,0 +1,130 @@
"""This module contains the classes describing the population of a simulation.
"""
# Copyright Lucas Javaudin - All Rights Reserved
# Unauthorized copying of this file, via any medium is strictly prohibited
# Proprietary and confidential
# Written by Lucas Javaudin <me@lucasjavaudin.com>, 2021
import os
import json
import numpy as np
import pandas as pd
from metrosim.exceptions import (
MetroIOError, MetroInputError, MetroUnsupportedError
)
from metrosim.models.schedule_utility import AlphaBetaGammaModel
from metrosim.models.mode_choice import (
DeterministicModeChoice, StochasticModeChoice
)
class Agent(object):
def __init__(self, agent_id, schedule_utility, mode_choice):
self.id = agent_id
self.schedule_utility = schedule_utility
self.mode_choice = mode_choice
def from_dict(data):
if 'id' not in data:
raise MetroInputError("Each Agent must have a valid id")
schedule_utility_data = data.get('schedule_utility', dict())
schedule_utility = super()._load_schedule_utility(
schedule_utility_data)
mode_choice_data = data.get('mode_choice', dict())
mode_choice = super()._load_mode_choice(mode_choice_data)
return Agent(data['id'], schedule_utility, mode_choice)
def _load_schedule_utility(data):
utility_type = data.get('type', None)
parameters = data.get('parameters', dict())
if utility_type == 'alpha-beta-gamma':
return AlphaBetaGammaModel.from_dict(parameters)
elif utility_type:
msg = "Unsupported schedule-utility type: {}"
raise MetroUnsupportedError(msg.format(utility_type))
else:
raise MetroInputError("Each Agent must have a schedule utility")
def _load_mode_choice(data):
choice_type = data.get('type', None)
modes = data.get('modes', list())
if choice_type == 'deterministic':
return DeterministicModeChoice.from_dict(modes)
elif choice_type == 'stochastic':
return StochasticModeChoice.from_dict(modes)
elif choice_type:
msg = "Unsupported mode-choice type: {}"
raise MetroUnsupportedError(msg.format(choice_type))
else:
raise MetroInputError("Each Agent must have a mode choice")
def get_pre_day_choice(self):
# Compute the expected utility for each continuous mode.
best_utility = -np.inf
best_mode = None
for mode in self.cont_modes:
results = mode.get_exp_utility(self)
self.mode_results[mode] = results
if results['exp_utility'] > best_utility:
best_utility = results['exp_utility']
best_mode = mode
for mode in self.det_modes:
results = mode.get_exp_utility(self, threshold=best_utility)
self.mode_results[mode] = results
class RawPopulation(object):
def __init__(self, segments, random_seed=None):
if not segments:
raise MetroInputError("A RawPopulation must have at least one "
"PopulationSegment")
self.segments = segments
self.random_seed = random_seed
def from_file(filename):
if not isinstance(filename, str):
raise TypeError("filename must be a str, got a {}"
.format(type(filename)))
if not os.path.isfile(filename):
raise MetroIOError("File not found: {}".format(filename))
try:
with open(filename, 'r') as f:
data = json.load(f)
except json.decoder.JSONDecodeError:
raise MetroIOError("Invalid JSON file: {}".format(filename))
segments = list()
for segment in data.get('segments', []):
segments.append(PopulationSegment(**segment))
random_seed = data.get('random_seed', None)
return RawPopulation(segments, random_seed)
class PopulationSegment(object):
def __init__(self, id, od_matrix):
self.id = id
self.od_matrix = ODMatrix.from_file(od_matrix)
class ODMatrix(object):
def from_file(filename):
if not isinstance(filename, str):
raise TypeError("filename must be a str, got a {}"
.format(type(filename)))
if not os.path.isfile(filename):
raise MetroIOError("File not found: {}".format(filename))
df = pd.read_csv(filename)

View File

@ -0,0 +1,330 @@
"""This module contains the classes describing the public-transit network of a
simulation.
"""
# Copyright Lucas Javaudin - All Rights Reserved
# Unauthorized copying of this file, via any medium is strictly prohibited
# Proprietary and confidential
# Written by Lucas Javaudin <me@lucasjavaudin.com>, 2021
from enum import Enum
from datetime import timedelta
import re
from collections import defaultdict
import networkx as nx
class PTNetwork(nx.MultiDiGraph):
"""Class representing a public-transit network.
The public-transit (PT) network is represented as a Multi-edges Directed
Graph. As such, it inherits all methods from :class:`nx.MultiDiGraph`.
On initialization, all arguments are passed to :class:`nx.MultiDiGraph`.
Compared to a :class:`nx.MultiDiGraph`, a PTNetwork has two additional dict
attributes:
- `routes`: Used to stored data on the routes of the PTNetwork.
- `trips`: Used to stored data on the trips of the PTNetwork.
"""
def __init__(self, *args, **kwargs):
self.routes = dict()
self.trips = dict()
super().__init__(*args, **kwargs)
def _read_nodes(self, reader):
"""Add nodes to the PT network from a csv.DictReader.
The CSV file read is assumed to have the format of stops.txt from the
GTFS standard. In praticular, the file must have a columns `stop_id`
and `location_type`.
:param reader: DictReader of a CSV representing stops.txt.
"""
for row in reader:
try:
lat = float(row.get('stop_lat'))
lon = float(row.get('stop_lon'))
except ValueError:
# Cannot parse latitude and longitude.
lat, lon = None, None
try:
location_type = PTNodeType(int(row.get('location_type', 0)))
except ValueError:
# Invalid location type, skip this node.
continue
self.add_node(
row['stop_id'],
name=row.get('stop_name', ''),
lat=lat,
lon=lon,
node_type=location_type,
parent_id=row.get('parent_station'),
)
def _read_stop_times(self, reader, from_time=timedelta(0),
to_time=timedelta(hours=30)):
"""Add edges to the PT network from a csv.DictReader.
These edges represent a public-transit trip from one stop to a
successive stop, with a specific departure and arrival time.
The CSV file read is assumed to have the format of stop_times.txt from
the GTFS standard. In particular, the file must have columns `trip_id`,
`stop_id`, `arrival_time`, `departure_time` and `stop_sequence`.
The parameters `from_time` and `to_time` can be used to constrain the
edges to a specific time period.
:param reader: DictReader of a CSV representing stop_times.txt.
:param from_time: Consider only trips whose departure time is later
than this time.
:param to_time: Consider only trips whose arrival time is earlier than
this time.
:type from_time: datetime.timedelta
:type to_time: datetime.timedelta
"""
if not self.trips:
# No trip is active, nothing to do.
return
# Initialize a dictionary to hold all the stop_times of a trip.
legs_dict = defaultdict(list)
invalid_stop_types = 0
for row in reader:
if row.get('trip_id') not in self.trips:
# Discard stop_time of invalid trips.
continue
if (
row.get('pickup_type', '0') != '0'
or row.get('drop_off_type', '0') != '0'
or row.get('continuous_pickup', '1') != '1'
or row.get('continuous_drop_off', '1') != '1'
):
# Not a regular pickup or drop off, unsupported.
invalid_stop_types += 1
continue
legs_dict[row['trip_id']].append(row)
if invalid_stop_types:
print('Warning: Discarded {} stop-times with invalid pickup or '
'dropoff'.format(invalid_stop_types))
for trip_id, stop_times in legs_dict.items():
# Sort stop_times by increasing stop_sequence.
stop_times = sorted(
stop_times, key=lambda x: int(x['stop_sequence']))
for prev_stop, next_stop in zip(stop_times[:-1], stop_times[1:]):
dep_time = _parse_time(prev_stop['departure_time'])
arr_time = _parse_time(next_stop['arrival_time'])
if dep_time < from_time or arr_time > to_time:
# This trip stop is not in the desired time window.
continue
route_id = self.trips[trip_id]['route_id']
self.add_edge(
prev_stop['stop_id'],
next_stop['stop_id'],
dep_time=dep_time,
arr_time=arr_time,
travel_time=arr_time - dep_time,
trip_id=trip_id,
route_id=route_id,
type=self.routes[route_id]['type'],
)
def _add_valid_routes(self, reader, routes):
"""Add route data to the PT network.
From the GTFS reference, a route is a group of trips that are displayed
to riders as a single service.
The CSV file read is assumed to have the format of routes.txt from the
GTFS standard. In particular, the file must have columns `route_id` and
`route_type`.
The data stored are the name and the type of the route. The type of the
route must be a valid PTType.
:param reader: DictReader of a CSV representing routes.txt.
:param routes: Set of route_id to include.
"""
invalid_route_types = 0
for row in reader:
if row['route_id'] not in routes:
continue
try:
route_type = PTType(int(row['route_type']))
except ValueError:
# This route type is not supported.
invalid_route_types += 1
continue
# Get the name of the route.
route_name = (
row.get('route_short_name', '')
or row.get('route_long_name', '')
)
# Add the route data to the PTNetwork instance.
self.routes[row['route_id']] = dict(
type=route_type,
name=route_name,
)
if invalid_route_types:
print('Warning: Discarded {} routes with an invalid '
'transportation mode.'.format(invalid_route_types))
def _add_valid_trips(self, reader, trips):
"""Add trip data to the PT network.
From the GTFS reference, a trip is a sequence of two or more stops that
occur during a specific time period.
The CSV file read is assumed to have the format of trips.txt from the
GTFS standard. In particular, the file must have columns `trip_id` and
`route_id`.
The data stored are the route_id, the name and the headsign of the
trip.
Only trips whose route_id is in the `routes` attribute of the PTNetwork
are stored.
:param reader: DictReader of a CSV representing trips.txt.
:param trips: Set of trip_id to include.
"""
for row in reader:
if row['trip_id'] not in trips:
continue
if row['route_id'] not in self.routes:
# Route must be added to the PTNetwork before adding trips.
continue
self.trips[row['trip_id']] = dict(
route_id=row['route_id'],
name=row.get('trip_short_name'),
headsign=row.get('trip_headsign'),
)
def _read_transfers(self, reader):
"""Add edges to the PT network from a csv.DictReader.
These edges represent a transfer from one platform (or stop) to
another.
The CSV file read is assumed to have the format of transfers.txt from
the GTFS standard. In particular, the file must have columns
`from_stop_id`, `to_stop_id`, `transfer_type`, `min_transfer_time`.
:param reader: DictReader of a CSV representing stop_times.txt.
"""
for row in reader:
if row.get('transfer_type', '3') == '3':
# Transfer is impossible or transfer_type is missing.
continue
if (row['from_stop_id'] not in self
or row['to_stop_id'] not in self):
# One of the node is not in the network.
continue
try:
transfer_seconds = int(row.get('min_transfer_time', 60))
except ValueError:
# Default is a transfer of 1 minute.
transfer_seconds = 60
self.add_edge(
row['from_stop_id'],
row['to_stop_id'],
travel_time=timedelta(seconds=transfer_seconds),
type=PTType.TRANSFER,
)
def _remove_unused_platforms(self):
"""Remove from the PTNetwork all nodes of type PTNodeType.PLATFORM,
with no trip edge.
"""
nodes_to_remove = set()
for node_id, data in self.nodes(data=True):
if (data['node_type'] == PTNodeType.PLATFORM
and not self.pred[node_id] and not self.succ[node_id]):
# Platform has no trip, remove it.
nodes_to_remove.add(node_id)
self.remove_nodes_from(nodes_to_remove)
print('Warning: Remove {} platforms with no trip'.format(
len(nodes_to_remove)))
def _add_generic_transfers(self, transfer_seconds=60):
"""Add edges representing transfers from each node to their parent or
their children.
:param transfer_seconds: Travel time of the transfers, in seconds.
"""
for node_id, data in self.nodes(data=True):
parent_id = data['parent_id']
if not parent_id:
continue
# Add a TRANSFER edge from the node to its parent.
self.add_edge(
node_id,
parent_id,
travel_time=timedelta(seconds=transfer_seconds),
type=PTType.TRANSFER,
)
# Add a TRANSFER edge from the parent to its child.
self.add_edge(
parent_id,
node_id,
travel_time=timedelta(seconds=transfer_seconds),
type=PTType.TRANSFER,
)
def _remove_unused_nodes(self):
"""Remove all nodes with no edge."""
nodes_to_remove = set()
for node_id in self.nodes:
if not self.pred[node_id] and not self.succ[node_id]:
nodes_to_remove.add(node_id)
self.remove_nodes_from(nodes_to_remove)
class PTType(Enum):
"""Enum representing all public-transit modes supported.
The enumeration is based on the GTFS reference of route_type in routes.txt,
with the addition of transfers type.
"""
TRAM = 0
METRO = 1
RAIL = 2
# BUS = 3
TRANSFER = 20
class PTNodeType(Enum):
"""Enum for the different types of nodes.
The enumeration is based on the GTFS reference of location_type in
stops.txt.
"""
PLATFORM = 0
STATION = 1
ENTRANCE = 2
GENERIC = 3
BOARDING_AREA = 4
def _parse_time(time_str):
"""Returns a timedelta from a time string.
:param time_str: String with a format "%H:%M:%S".
"""
p = '(?P<hour>[0-9]{2}):(?P<minute>[0-5][0-9]):(?P<second>[0-5][0-9])'
m = re.match(p, time_str)
if m:
return timedelta(
hours=int(m.group('hour')),
minutes=int(m.group('minute')),
seconds=int(m.group('second')),
)
else:
# Invalid time string.
return timedelta(0)

View File

@ -0,0 +1,32 @@
import time
from metrosim.exceptions import MetroInputError
class AlphaBetaGammaModel(object):
mandatory_parameters = ('beta', 'gamma', 'tstar')
def __init__(self, beta, gamma, tstar, delta=0, morning=True):
self.beta = beta
self.gamma = gamma
self.tstar = tstar
self.delta = delta
self.morning = morning
def from_dict(data):
# Check that all mandatory parameters are in the data.
for parameter in super().mandatory_parameters:
if parameter not in data:
msg = (
"Mandatory parameter for alpha-beta-gamma preferences: {}"
)
raise MetroInputError(msg.format(parameter))
# Convert tstar input to time.time instance using format '%H:%M:%S'.
try:
tstar = time.strptime(data['tstar'], format='%H:%M:%S')
except ValueError:
raise MetroInputError("Invalid time: {}".format(data['tstar']))
return AlphaBetaGammaModel(beta=data['beta'], gamma=data['gamma'],
tstar=tstar, delta=data.get('delta', None),
morning=data.get('morning', None))

View File

@ -0,0 +1,35 @@
[
{
"id": 1,
"schedule_utility": {
"type": "alpha-beta-gamma",
"parameters": {
"beta": 5,
"gamma": 20,
"delta": 0,
"tstar": "08:30:00",
"morning": true
}
},
"mode_choice": {
"type": "deterministic",
"modes": [
{
"type": "car",
"value_of_time": 10,
"departure_time": {
"type": "stochastic",
"parameters": {
"mu": 1,
"u": 0.5
}
},
"route_choice": {
"type": "deterministic",
"see_congestion": true
}
}
]
}
}
]

View File

@ -0,0 +1,9 @@
{
"random_seed": 42,
"segments": [
{
"id": 1,
"od_matrix": "./od_matrix"
}
]
}

View File

View File

@ -0,0 +1,42 @@
# Copyright Lucas Javaudin - All Rights Reserved
# Unauthorized copying of this file, via any medium is strictly prohibited
# Proprietary and confidential
# Written by Lucas Javaudin <me@lucasjavaudin.com>, 2021
class MetroManager(object):
def __init__(self):
pass
def run_pre_day_model(self):
"""Returns events from the pre-day model.
The pre-day model computes the decisions made by the agents before the
day start: mode choice, departure-time choice and (for public-transit
only) path choice. The decisions depend on the current state of the
network.
:returns: one event for each agent, describing the mode and
departure-time chosen
:rtype: list
"""
events = list()
for agent in self.population.agents:
events.append(agent.get_pre_day_choice(self.network_state))
return events
def run_within_day_model(self, events):
"""Returns the state of the network resulting from agent and vehicle
movements.
The within-day model simulates the movements of agents and vehicles in
the network through an event-based model.
:param events: initial set of events
:returns: state of the network
"""
while events:
event = events.pop()
self.network_state = event.execute(self.network_state)
return self.network_state

View File

@ -0,0 +1,14 @@
import os
import sys
from metrosim import RawPopulation
BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
sys.path.insert(0, BASE_DIR)
# Load the raw population of the simple simulation example from its directory.
pop_file = os.path.join(BASE_DIR, 'metrosim/simple_simulation/population.json')
raw_pop = RawPopulation.from_file(pop_file)
# Generate the agents from the raw population.
agents = raw_pop.generate()

View File

@ -0,0 +1,11 @@
import os
import sys
from metrosim import Simulation
BASE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
sys.path.insert(0, BASE_DIR)
# Load the simple_simulation raw data from its directory.
simple_sim_dir = os.path.join(BASE_DIR, 'metrosim/simple_simulation/')
simple_sim = Simulation.from_directory(simple_sim_dir)