From b0b5916f0b71c519d29248a917b3ba9121ac64d5 Mon Sep 17 00:00:00 2001 From: guille Date: Mon, 15 Apr 2024 07:12:09 +0200 Subject: [PATCH] partial matsim implementation --- hub/data/traffic/osm/.gitignore | 2 + hub/exports/traffic/helpers/__init__.py | 0 hub/exports/traffic/helpers/osm.py | 25 ++ hub/exports/traffic/matsim.py | 328 +++++++++++++++++++++++- requirements.txt | 3 +- 5 files changed, 350 insertions(+), 8 deletions(-) create mode 100644 hub/data/traffic/osm/.gitignore create mode 100644 hub/exports/traffic/helpers/__init__.py create mode 100644 hub/exports/traffic/helpers/osm.py diff --git a/hub/data/traffic/osm/.gitignore b/hub/data/traffic/osm/.gitignore new file mode 100644 index 00000000..cf57c3b0 --- /dev/null +++ b/hub/data/traffic/osm/.gitignore @@ -0,0 +1,2 @@ +.gitignore +!.gitignore \ No newline at end of file diff --git a/hub/exports/traffic/helpers/__init__.py b/hub/exports/traffic/helpers/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/hub/exports/traffic/helpers/osm.py b/hub/exports/traffic/helpers/osm.py new file mode 100644 index 00000000..2ded04f0 --- /dev/null +++ b/hub/exports/traffic/helpers/osm.py @@ -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) diff --git a/hub/exports/traffic/matsim.py b/hub/exports/traffic/matsim.py index 83dca21a..bfd94cd3 100644 --- a/hub/exports/traffic/matsim.py +++ b/hub/exports/traffic/matsim.py @@ -1,10 +1,15 @@ import gzip import math import shutil +import subprocess +from pathlib import Path import geopandas as gpd +import requests from lxml import etree from shapely import Polygon +from helpers.osm import Osm +import hub.helpers.constants as cte class Matsim: @@ -28,6 +33,22 @@ class Matsim: self._export_population() 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): """ Exports the city's facilities data to XML and shapefile formats. @@ -86,8 +107,6 @@ class Matsim: except Exception as ex: print('error: ', ex) - # todo: this may be changed to - viewport = Polygon([ (self._city.lower_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': converted_schedules.append({ 'day': day[0:3], - 'start_time': f"{opening_hour:02}:00:00", - 'end_time': f"{closing_hour:02}:00:00" + 'start_time': f'{opening_hour:02}:00:00', + 'end_time': f'{closing_hour:02}:00:00' }) return converted_schedules @@ -148,15 +167,310 @@ class Matsim: :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. """ - if xml_dtd is None: xml_dtd = '' - with open(output_file, 'w') as file: - file.write("\n") + file.write('\n') file.write(xml_dtd) file.write(xml_content) with open(output_file, 'rb') as f_in: with gzip.open(output_file + '.gz', 'wb') as 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'\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'\n') diff --git a/requirements.txt b/requirements.txt index 93b455ca..f65c7240 100644 --- a/requirements.txt +++ b/requirements.txt @@ -23,4 +23,5 @@ geopandas triangle psycopg2-binary Pillow -pathlib \ No newline at end of file +pathlib +lxml \ No newline at end of file