update 2021-05-27
This commit is contained in:
parent
d8d96c9c61
commit
cb29cd2473
|
|
@ -0,0 +1,5 @@
|
||||||
|
init:
|
||||||
|
pip install -r requirements.txt
|
||||||
|
|
||||||
|
test:
|
||||||
|
py.test tests
|
||||||
|
|
@ -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.
|
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.
|
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
|
PVReachesNode
|
||||||
--------------
|
^^^^^^^^^^^^^
|
||||||
|
|
||||||
This event signals that a private vehicle reaches a node, at a given time.
|
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.
|
||||||
|
|
|
||||||
|
|
@ -10,9 +10,9 @@
|
||||||
# add these directories to sys.path here. If the directory is relative to the
|
# 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.
|
# documentation root, use os.path.abspath to make it absolute, like shown here.
|
||||||
#
|
#
|
||||||
# import os
|
import os
|
||||||
# import sys
|
import sys
|
||||||
# sys.path.insert(0, os.path.abspath('.'))
|
sys.path.insert(0, os.path.abspath('..'))
|
||||||
|
|
||||||
|
|
||||||
# -- Project information -----------------------------------------------------
|
# -- Project information -----------------------------------------------------
|
||||||
|
|
@ -32,6 +32,8 @@ extensions = [
|
||||||
'sphinxcontrib.plantuml',
|
'sphinxcontrib.plantuml',
|
||||||
'sphinx_rtd_theme',
|
'sphinx_rtd_theme',
|
||||||
'matplotlib.sphinxext.plot_directive',
|
'matplotlib.sphinxext.plot_directive',
|
||||||
|
'sphinx.ext.autodoc',
|
||||||
|
'sphinx.ext.viewcode',
|
||||||
]
|
]
|
||||||
|
|
||||||
# Add any paths that contain templates here, relative to this directory.
|
# Add any paths that contain templates here, relative to this directory.
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,9 @@ It is intended at developers willing to improve or extend it.
|
||||||
.. toctree::
|
.. toctree::
|
||||||
:caption: References
|
:caption: References
|
||||||
|
|
||||||
|
/references/models/models
|
||||||
|
/references/initializer/initializer
|
||||||
|
/references/simulator/simulator
|
||||||
/references/changelog
|
/references/changelog
|
||||||
|
|
||||||
.. toctree::
|
.. toctree::
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
GTFS
|
||||||
|
====
|
||||||
|
|
||||||
|
.. automodule:: metrosim.initializer.gtfs
|
||||||
|
:members:
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
Initializer
|
||||||
|
===========
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
gtfs
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
Models
|
||||||
|
======
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
pt_network
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
Public-Transit Network
|
||||||
|
======================
|
||||||
|
|
||||||
|
.. automodule:: metrosim.models.pt_network
|
||||||
|
:members:
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
Manager
|
||||||
|
=======
|
||||||
|
|
||||||
|
.. autoclass:: metrosim.simulator.manager.MetroManager
|
||||||
|
:members:
|
||||||
|
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
Simulator
|
||||||
|
=========
|
||||||
|
|
||||||
|
.. toctree::
|
||||||
|
manager
|
||||||
|
|
@ -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."""
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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))
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
{
|
||||||
|
"random_seed": 42,
|
||||||
|
"segments": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"od_matrix": "./od_matrix"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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()
|
||||||
|
|
@ -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)
|
||||||
Loading…
Reference in New Issue