partial matsim implementation

This commit is contained in:
Guille Gutierrez 2024-04-15 07:12:09 +02:00
parent c6f6498a23
commit b0b5916f0b
5 changed files with 350 additions and 8 deletions

2
hub/data/traffic/osm/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
.gitignore
!.gitignore

View File

View File

@ -0,0 +1,25 @@
import logging
class Osm:
_osm_file = {
'CA.02.5935': 'https://download.geofabrik.de/north-america/canada/british-columbia-latest.osm.pbf',
'CA.10.06': 'https://download.geofabrik.de/north-america/canada/quebec-latest.osm.pbf',
'CA.10.13': 'https://download.geofabrik.de/north-america/canada/quebec-latest.osm.pbf',
'CA.10.14': 'https://download.geofabrik.de/north-america/canada/quebec-latest.osm.pbf',
'CA.10.16': 'https://download.geofabrik.de/north-america/canada/quebec-latest.osm.pbf',
'DE.01.082': 'https://download.geofabrik.de/europe/germany/baden-wuerttemberg-latest.osm.pbf',
'US.NY.047': 'https://download.geofabrik.de/north-america/us/new-york-latest.osm.pbf',
'CA.10.12': 'https://download.geofabrik.de/north-america/canada/quebec-latest.osm.pbf',
'IL.01.': 'https://download.geofabrik.de/asia/israel-and-palestine-latest.osm.pbf',
'ES.07.PM': 'https://download.geofabrik.de/europe/spain/islas-baleares-latest.osm.pbf'
}
def __init__(self):
pass
def pfb_file(self, region_code):
if region_code in self._osm_file.keys():
return self._osm_file[region_code]
logging.error('Specific osm data unknown for %s', region_code)
raise NotImplementedError('Specific osm data unknown for %s', region_code)

View File

@ -1,10 +1,15 @@
import gzip import gzip
import math import math
import shutil import shutil
import subprocess
from pathlib import Path
import geopandas as gpd import geopandas as gpd
import requests
from lxml import etree from lxml import etree
from shapely import Polygon from shapely import Polygon
from helpers.osm import Osm
import hub.helpers.constants as cte
class Matsim: class Matsim:
@ -28,6 +33,22 @@ class Matsim:
self._export_population() self._export_population()
self._export_config() self._export_config()
@property
def _jar_path(self):
"""
Get the matsim-network-from-osm installation path
:return: str
"""
return shutil.which('matsim-network-from-osm.jar')
@property
def _java_path(self):
"""
Get the matsim-network-from-osm installation path
:return: str
"""
return shutil.which('java')
def _export_facilities(self): def _export_facilities(self):
""" """
Exports the city's facilities data to XML and shapefile formats. Exports the city's facilities data to XML and shapefile formats.
@ -86,8 +107,6 @@ class Matsim:
except Exception as ex: except Exception as ex:
print('error: ', ex) print('error: ', ex)
# todo: this may be changed to
viewport = Polygon([ viewport = Polygon([
(self._city.lower_corner[self._x], self._city.upper_corner[self._y]), (self._city.lower_corner[self._x], self._city.upper_corner[self._y]),
(self._city.upper_corner[self._x], self._city.upper_corner[self._y]), (self._city.upper_corner[self._x], self._city.upper_corner[self._y]),
@ -133,8 +152,8 @@ class Matsim:
if day[0:3] != 'hol': if day[0:3] != 'hol':
converted_schedules.append({ converted_schedules.append({
'day': day[0:3], 'day': day[0:3],
'start_time': f"{opening_hour:02}:00:00", 'start_time': f'{opening_hour:02}:00:00',
'end_time': f"{closing_hour:02}:00:00" 'end_time': f'{closing_hour:02}:00:00'
}) })
return converted_schedules return converted_schedules
@ -148,15 +167,310 @@ class Matsim:
:param xml_content: The XML content to be written to the file. :param xml_content: The XML content to be written to the file.
:param xml_dtd: An optional DOCTYPE declaration to be included at the beginning of the file. :param xml_dtd: An optional DOCTYPE declaration to be included at the beginning of the file.
""" """
if xml_dtd is None: if xml_dtd is None:
xml_dtd = '' xml_dtd = ''
with open(output_file, 'w') as file: with open(output_file, 'w') as file:
file.write("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n") file.write('<?xml version="1.0" encoding="utf-8"?>\n')
file.write(xml_dtd) file.write(xml_dtd)
file.write(xml_content) file.write(xml_content)
with open(output_file, 'rb') as f_in: with open(output_file, 'rb') as f_in:
with gzip.open(output_file + '.gz', 'wb') as f_out: with gzip.open(output_file + '.gz', 'wb') as f_out:
shutil.copyfileobj(f_in, f_out) shutil.copyfileobj(f_in, f_out)
def _export_network(self):
"""
Generates a transportation network file from the city's OpenStreetMap data and buildings shapefile.
"""
url = Osm().pfb_file(self._city.location.region_code)
path = (Path(__file__).parent.parent.parent / f'data/weather/epw/{url.rsplit("/", 1)[1]}').resolve()
if not path.exists():
with open(path, 'wb') as pbf_file:
pbf_file.write(requests.get(url, allow_redirects=True).content)
command = [
self._java_path,
'-jar', self._jar_path,
path,
f'{self._output_file_path}/buildings_shapefile.shp',
f'{self._output_file_path}/{self._city.name}_network.xml.gz',
self._crs
]
subprocess.run(command)
def _export_population(self):
"""
Generates and exports the city's population data to an XML file.
"""
population = etree.Element('population')
person_id = 0
try:
# Generate work facilities
work = []
for facility in self._facilities['facility']:
if facility['activity'][0]['type'] != cte.RESIDENTIAL:
work.append({
'type': facility['activity'][0]['type'],
'capacity': int(facility['activity'][0]['capacity']),
'facility': facility['id'],
'x': facility['x'],
'y': facility['y'],
'start_time': '08:00:00',
'end_time': '18:00:00'
})
# Generate the population from residential places first
current_work = 0
for facility in self._facilities['facility']:
if facility['activity'][0]['type'] == cte.RESIDENTIAL:
max_capacity = int(facility['activity'][0]['capacity'])
for i in range(max_capacity):
person = etree.SubElement(population, 'person', {
'id': str(person_id),
'sex': 'm',
'age': '32',
'car_avail': 'always',
'employed': 'yes',
})
plan = etree.SubElement(person, 'plan', {'selected': 'yes'})
# Residential activity
etree.SubElement(plan, 'act', {
'type': f'{facility["activity"][0]["type"]}',
'facility': f'{facility["id"]}',
'x': f'{facility["x"]}',
'y': f'{facility["y"]}',
'end_time': '7:30:00'
})
# Leg to work
etree.SubElement(plan, 'leg', {'mode': 'car'})
# Work activity
etree.SubElement(plan, 'act', {
'type': f'{work[current_work]["type"]}',
'facility': f'{work[current_work]["facility"]}',
'x': f'{work[current_work]["x"]}',
'y': f'{work[current_work]["y"]}',
'start_time': f'{work[current_work]["start_time"]}',
'end_time': f'{work[current_work]["end_time"]}',
})
# Leg to home
etree.SubElement(plan, 'leg', {'mode': 'car'})
# Residential activity (return)
etree.SubElement(plan, 'act', {
'type': f'{facility["activity"][0]["type"]}',
'facility': f'{facility["id"]}',
'x': f'{facility["x"]}',
'y': f'{facility["y"]}',
})
work[current_work]['capacity'] -= 1
if work[current_work]['capacity'] == 0:
current_work += 1
person_id += 1
except Exception as ex:
print('error: ', ex)
# Write xml content to file
xml_content = etree.tostring(population, pretty_print=True, encoding='UTF-8').decode('utf-8')
output_file = f'{self._output_file_path}/{self._city.name}_population.xml'
self._save_xml(
output_file,
xml_content,
f'<!DOCTYPE population SYSTEM "http://www.matsim.org/files/dtd/population_v5.dtd">\n'
)
@staticmethod
def _add_param(parent, name, value):
"""
Helper function to add a parameter to an XML element.
:param parent: The parent XML element to which the parameter should be added.
:param name: The name of the parameter.
:param value: The value of the parameter.
"""
etree.SubElement(parent, 'param', {
'name': name,
'value': value
})
@staticmethod
def _add_parameterset(parent, attribute_type, parameters):
"""
Helper function to add a set of parameters to an XML element.
:param parent: The parent XML element to which the parameterset should be added.
:param attribute_type: The attribute type for the parameterset.
:param parameters: A list of tuples, each containing the name and value of a parameter.
"""
parameterset = etree.SubElement(parent, 'parameterset', {
'type': attribute_type
})
for name, value in parameters:
Matsim._add_param(parameterset, name, value)
def _export_config(self):
"""
Creates and exports the simulation configuration settings to an XML file.
"""
root = etree.Element('config')
# ======== NETWORK ========= #
network_path_module = etree.SubElement(root, 'module', {
'name': 'network'
})
Matsim._add_param(network_path_module, 'inputNetworkFile', f'{self._city.name}_network.xml.gz/')
# ======== POPULATION ========= #
population_path_module = etree.SubElement(root, 'module', {
'name': 'plans'
})
Matsim._add_param(population_path_module, 'inputPlansFile', f'{self._city.name}_population.xml.gz')
# ======== FACILITIES ========= #
facilities_path_module = etree.SubElement(root, 'module', {
'name': 'facilities'
})
Matsim._add_param(facilities_path_module, 'inputFacilitiesFile', f'{self._city.name}_facilities.xml.gz')
Matsim._add_param(facilities_path_module, 'facilitiesSource', 'fromFile')
# ======== CONTROLER ========= #
controler_module = etree.SubElement(root, 'module', {
'name': 'controler'
})
controler_params = [
('writeEventsInterval', '1000'),
('writePlansInterval', '1000'),
('eventsFileFormat', 'xml'),
('outputDirectory', f"{self._output_file_path}/Montreal"),
('firstIteration', '0'),
('lastIteration', '10'),
('mobsim', 'qsim'),
]
for param in controler_params:
Matsim._add_param(controler_module, param[0], param[1])
# ======== QSIM ========= #
qsim_module = etree.SubElement(root, 'module', {
'name': 'qsim'
})
qsim_params = [
('startTime', '00:00:00'),
('endTime', '30:00:00'),
('flowCapacityFactor', '1.00'),
('storageCapacityFactor', '1.00'),
('numberOfThreads', '1'),
('snapshotperiod', '00:00:01'),
('removeStuckVehicles', 'false'),
('stuckTime', '3600.0'),
('timeStepSize', '00:00:01'),
('trafficDynamics', 'queue'),
]
for param in qsim_params:
Matsim._add_param(qsim_module, param[0], param[1])
# ======== SCORING ========= #
score_module = etree.SubElement(root, 'module', {
'name': 'planCalcScore'
})
Matsim._add_param(score_module, 'BrainExpBeta', '1.0')
Matsim._add_param(score_module, 'learningRate', '1.0')
scoring_paramset = etree.SubElement(score_module, 'parameterset', {
'type': 'scoringParameters'
})
scoring_paramset_params = [
('earlyDeparture', '0.0'),
('lateArrival', '0.0'),
('marginalUtilityOfMoney', '0.062'),
('performing', '0.96'),
('utilityOfLineSwitch', '0.0'),
('waitingPt', '-0.18'),
]
for param in scoring_paramset_params:
Matsim._add_param(scoring_paramset, param[0], param[1])
mode_paramsets = [
('car',
[
('marginalUtilityOfTraveling_util_hr', '0.0'),
('constant', '-0.562'),
('monetaryDistanceRate', '-0.0004')
]),
('walk',
[
('marginalUtilityOfTraveling_util_hr', '-1.14'),
('constant', '0.0'),
('marginalUtilityOfDistance_util_m', '0.0')
])
]
for mode, parameters in mode_paramsets:
Matsim._add_parameterset(scoring_paramset, 'modeParams', [('mode', mode)] + parameters)
activity_paramsets = [
('residential',[
('priority', '1'),
('typicalDuration', '13:00:00'),
('minimalDuration', '01:00:00'),
]),
]
for facility in self._facilities['facility']:
if not any(activity[0] == facility['activity'][0]['type'] for activity in activity_paramsets):
activity_paramsets.append(
(facility['activity'][0]['type'], [
('priority', '1'),
('typicalDuration', '09:00:00'),
('minimalDuration', '08:00:00'),
('openingTime', '08:00:00'),
('earliestEndTime', '17:00:00'),
('latestStartTime', '09:00:00'),
('closingTime', '18:00:00'),
])
)
for activity_type, parameters in activity_paramsets:
Matsim._add_parameterset(scoring_paramset, "activityParams", [("activityType", activity_type)] + parameters)
# ======== STRATEGY ========= #
strategy_module = etree.SubElement(root, 'module', {
'name': 'strategy'
})
Matsim._add_param(strategy_module, 'maxAgentPlanMemorySize', '6')
strategy_paramsets = [
('ChangeExpBeta',[
('weight', '0.7'),
]),
('ReRoute',[
('disableAfterIteration', '2900'),
('weight', '0.01'),
]),
('SubtourModeChoice', [
('disableAfterIteration', '2900'),
('weight', '0.01'),
]),
('TimeAllocationMutator', [
('disableAfterIteration', '2900'),
('weight', '0.01'),
]),
]
for name, parameters in strategy_paramsets:
Matsim._add_parameterset(strategy_module, 'strategysettings', [("strategyName", name)] + parameters)
# ======== SUBTOUR MODE CHOICE ========= #
subtour_module = etree.SubElement(root, 'module', {
'name': 'subtourModeChoice'
})
# Defines the chain-based modes, seperated by commas
Matsim._add_param(subtour_module, 'chainBasedModes', 'car')
# Defines whether car availability must be considered or not. An agent has no car only if it has no license, or never access to a car
Matsim._add_param(subtour_module, 'considerCarAvailability', 'true')
# Defines all the modes available, including chain-based modes, seperated by commas
Matsim._add_param(subtour_module, 'modes', 'car,walk')
# Write xml content to file
xml_content = etree.tostring(root, pretty_print=True, encoding='UTF-8').decode('utf-8')
output_file = f"{self._output_file_path}/{self._city.name}_config.xml"
self._save_xml(output_file, xml_content, f'<!DOCTYPE config SYSTEM "http://www.matsim.org/files/dtd/config_v2.dtd">\n')

View File

@ -23,4 +23,5 @@ geopandas
triangle triangle
psycopg2-binary psycopg2-binary
Pillow Pillow
pathlib pathlib
lxml