diff --git a/__pycache__/matsim.cpython-39.pyc b/__pycache__/matsim.cpython-39.pyc new file mode 100644 index 0000000..69dfed8 Binary files /dev/null and b/__pycache__/matsim.cpython-39.pyc differ diff --git a/__pycache__/matsim_engine.cpython-39.pyc b/__pycache__/matsim_engine.cpython-39.pyc index 2e6783e..2fc4117 100644 Binary files a/__pycache__/matsim_engine.cpython-39.pyc and b/__pycache__/matsim_engine.cpython-39.pyc differ diff --git a/main.py b/main.py index 70275e2..c26bfc8 100644 --- a/main.py +++ b/main.py @@ -6,7 +6,7 @@ from hub.imports.usage_factory import UsageFactory from hub.helpers.dictionaries import Dictionaries from matsim_engine import MatSimEngine -from matsim_visualizer import MatSimVisualizer +from matsim import Matsim try: file_path = (Path(__file__).parent / 'input_files' / 'summerschool_all_buildings.geojson') @@ -24,7 +24,8 @@ try: ConstructionFactory(construction_format, city).enrich() UsageFactory(usage_format, city).enrich() - MatSimEngine(city, 'output_files') + # Matsim(city, 'output_files')._export() + MatSimEngine('output_files/Montreal_config.xml').run() # visualizer = MatSimVisualizer('output_files/network.xml.gz', 'output_files/output_events.xml.gz') # visualizer.visualize() diff --git a/matsim.py b/matsim.py new file mode 100644 index 0000000..acf3fc4 --- /dev/null +++ b/matsim.py @@ -0,0 +1,266 @@ +import math +import subprocess +import xmltodict +import gzip +import shutil + +import geopandas as gpd +from shapely.geometry import Point +import hub.helpers.constants as cte +from lxml import etree + +# TODO: remove xmltodict completely and replace with lxml as it doesnt allow for repeated mixed ordered tags +class Matsim: + def __init__(self, city, output_file_path): + self.city = city + self.output_file_path = output_file_path + + self.facilities = { + '@name': self.city.name + ' Facilities', + 'facility': [] + } + + self.population = etree.Element("population") + + def _export(self): + self._export_facilities() + self._export_network() + self._export_population() + self._export_config() + + def _export_facilities(self): + buildings_shape_data = { + 'id': [], + 'geometry': [] + } + + for building in self.city.buildings: + for surface in building.grounds: + for coord in surface.solid_polygon.coordinates: + buildings_shape_data['id'].append(f"{building.name}") + buildings_shape_data['geometry'].append(Point(coord[0], coord[1])) + + facility = { + '@id': building.name, + '@x': str(building.centroid[0]), + '@y': str(building.centroid[1]), + 'activity': [] + } + + if len(building.thermal_zones_from_internal_zones) > 1: + raise NotImplementedError("multi-zone buildings aren't yet supported") + + building_schedules = [] + + capacity = 0 + for thermal_zone in building.thermal_zones_from_internal_zones: + capacity = thermal_zone.occupancy.occupancy_density * building.floor_area * building.storeys_above_ground + for schedule in thermal_zone.occupancy.occupancy_schedules: + building_schedules.append(schedule) + + activity_info = { + '@type': building.function, + 'capacity': { + '@value': math.ceil(capacity) + }, + 'opentime': _convert_schedules(building_schedules) + } + + facility['activity'].append(activity_info) + self.facilities['facility'].append(facility) + + gdf = gpd.GeoDataFrame( + buildings_shape_data, + crs=self.city.srs_name + ) + gdf.to_file("input_files/buildings_shapefile.shp") + + # Convert the Python dictionary to an XML string + xml_content = xmltodict.unparse({'facilities': self.facilities}, pretty=True, short_empty_elements=True) + + # Write the XML to the file + output_file = f"{self.output_file_path}/{self.city.name}_facilities.xml" + with open(output_file, 'w') as file: + 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): + java_path = "java" + jar_path = "matsim-network-from-osm.jar" + command = [ + java_path, + "-jar", jar_path, + "input_files/merged-network.osm.pbf", + "input_files/buildings_shapefile.shp", + f"{self.output_file_path}/{self.city.name}_network.xml.gz" + ] + subprocess.run(command) + + def _export_population(self): + id = 0 + + # 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']['@value']), + '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']['@value']) + for _ in range(max_capacity): + person = etree.SubElement(self.population, 'person', { + 'id': str(id), + 'sex': 'm', + 'age': '32', + 'car_avail': 'always', + 'employed': 'yes', + }) + plan = etree.SubElement(person, 'plan', {'selected': 'yes'}) + + # Residential activity + etree.SubElement(plan, 'act', { + 'type': facility['activity'][0]['@type'], + 'facility': facility['@id'], + 'x': facility['@x'], + 'y': facility['@y'], + 'end_time': '7:30:00' + }) + + # Leg to work + etree.SubElement(plan, 'leg', {'mode': 'car'}) + + # Work activity + etree.SubElement(plan, 'act', { + 'type': work[current_work]['type'], + 'facility': work[current_work]['facility'], + 'x': work[current_work]['x'], + 'y': work[current_work]['y'], + 'start_time': work[current_work]['start_time'], + 'end_time': work[current_work]['end_time'], + }) + + # Leg to home + etree.SubElement(plan, 'leg', {'mode': 'car'}) + + # Residential activity (return) + etree.SubElement(plan, 'act', { + 'type': facility['activity'][0]['@type'], + 'facility': facility['@id'], + 'x': facility['@x'], + 'y': facility['@y'], + }) + + work[current_work]['capacity'] -= 1 + if work[current_work]['capacity'] == 0: + current_work += 1 + + id += 1 + + # Convert the Python dictionary to an XML string + xml_content = etree.tostring(self.population, pretty_print=True, encoding='UTF-8', xml_declaration=True).decode('utf-8') + + # Write the XML to the file + output_file = f"{self.output_file_path}/{self.city.name}_population.xml" + with open(output_file, 'w') as file: + 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_config(self): + parameterset = [] + + for facility in self.facilities['facility']: + if facility['activity'][0]['@type'] == cte.RESIDENTIAL: + parameterset.append({'@type':'activityParams', 'param': [ + {'@name':'activityType','@value':facility['activity'][0]['@type']}, + {'@name':'typicalDuration', '@value' : '12:00:00'}, + {'@name':'priority','@value': '1'} + ]}) + else: + parameterset.append({'@type':'activityParams', 'param': [ + {'@name': 'activityType', '@value': facility['activity'][0]['@type']}, + {'@name':'openingTime','@value': '08:00:00'}, + {'@name':'closingTime','@value': '18:00:00'}, + {'@name':'typicalDuration','@value': '08:00:00'}, + {'@name':'priority','@value': '1'} + ] + }) + + config = { + 'module': [ + {'@name': 'network', 'param': {'@name':'inputNetworkFile', '@value': f"{self.city.name}_network.xml.gz"}}, + {'@name': 'plans', 'param': {'@name':'inputPlansFile', '@value': f"{self.city.name}_population.xml.gz"}}, + {'@name': 'facilities', 'param': {'@name':'inputFacilitiesFile', '@value': f"{self.city.name}_facilities.xml.gz"}}, + {'@name': 'controler', 'param': [ + {'@name': 'outputDirectory', '@value': '/output'}, + {'@name': 'firstIteration', '@value': '0'}, + {'@name': 'lastIteration', '@value': '10'}, + ]}, + {'@name': 'qsim', 'param': [ + {'@name': 'startTime', '@value': '00:00:00'}, + {'@name': 'endTime', '@value': '00:00:00'}, + {'@name': 'snapshotperiod', '@value': '00:00:00'}, + ]}, + {'@name':'planCalcStore','param':[ + {'@name':'learningRate', '@value':'1.0'}, + {'@name':'BrainExpBeta', '@value':'2.0'}, + {'@name':'lateArrival', '@value':'-18'}, + {'@name':'earlyDeparture', '@value':'-0'}, + {'@name':'performing', '@value':'+6'}, + {'@name':'waiting', '@value':'-0'}, + ],'parameterset': parameterset}, + {'@name':'strategy','param':[ + {'@name': 'maxAgentPlanMemorySize', '@value': '5'}, + {'@name': 'ModuleProbability_1', '@value': '0.9'}, + {'@name': 'Module_1', '@value': 'BestScore'}, + {'@name': 'ModuleProbability_2', '@value': '0.1'}, + {'@name': 'Module_2', '@value': 'ReRoute'}, + ]} + ] + } + + xml_content = xmltodict.unparse({'config': config}, pretty=True, short_empty_elements=True) + + with open(f"{self.output_file_path}/{self.city.name}_config.xml", 'w') as file: + file.write(xml_content) + +def _convert_schedules(building_schedules): + converted_schedules = [] + for schedule in building_schedules: + opening_hour = 0 + closing_hour = 0 + + for i, value in enumerate(schedule.values): + if value > 0: + opening_hour = i + break + + for i, value in reversed(list(enumerate(schedule.values))): + if value > 0: + closing_hour = i + break + + for day in schedule.day_types: + if day[0:3] != 'hol': + converted_schedules.append({ + '@day': day[0:3], + '@start_time': opening_hour, + '@end_time': closing_hour + }) + return converted_schedules diff --git a/matsim_engine.py b/matsim_engine.py index 32dc968..6df3717 100644 --- a/matsim_engine.py +++ b/matsim_engine.py @@ -9,121 +9,8 @@ from matsim_activity_to_matsim_schedule import MatsimActivityToMatsimSchedule from hub_function_to_matsim_activity import HubFunctionToMatsimActivity class MatSimEngine: - def __init__(self, city, output_file_path): - self._city = city - self._output_file_path = output_file_path - - facilities_dict = { - 'facilities': { - '@name': 'Montreal Facilities', - 'facility': [] - } - } - - hub_function_to_matsim = HubFunctionToMatsimActivity() - matsim_schedule = MatsimActivityToMatsimSchedule() - - buildings_shape_data = { - 'id': [], - 'geometry': [] - } - - # 1- Facilities - # TODO: this should come from the factories, please check idf generation - for building in city.buildings: - activity = hub_function_to_matsim.dictionary[building.function].split(',') - - for surface in building.grounds: - for coord in surface.solid_polygon.coordinates: - buildings_shape_data['id'].append(f"{building.name}") - buildings_shape_data['geometry'].append(Point(coord[0], coord[1])) - - facility = { - '@id': building.name, - '@x': str(building.centroid[0]), - '@y': str(building.centroid[1]), - 'activity': [] - } - - if len(building.thermal_zones_from_internal_zones) > 1: - raise NotImplementedError("multi-zone buildings aren't yet supported") - - building_schedules = [] - - capacity = 0 - for thermal_zone in building.thermal_zones_from_internal_zones: - capacity = thermal_zone.occupancy.occupancy_density * building.floor_area * building.storeys_above_ground - for schedule in thermal_zone.occupancy.occupancy_schedules: - building_schedules.append(schedule) - - for new_activity in activity: - activity_info = { - '@type': new_activity, - 'capacity': { - '@value': math.ceil(capacity) - }, - 'opentime': [] - } - - for schedule in building_schedules: - opening_hour = 0 - closing_hour = 0 - - # Find opening hour (first hour > 0) - for i, value in enumerate(schedule.values): - if value > 0: - opening_hour = i - break - - for i, value in reversed(list(enumerate(schedule.values))): - if value > 0: - closing_hour = i - break - - for day in schedule.day_types: - if day[0:3] != 'hol': - activity_info['opentime'].append({ - '@day': day[0:3], - '@start_time': opening_hour, - '@end_time': closing_hour - }) - - facility['activity'].append(activity_info) - - facilities_dict['facilities']['facility'].append(facility) - - gdf = gpd.GeoDataFrame( - buildings_shape_data, - crs=city.srs_name - ) - - gdf.to_file("input_files/buildings_shapefile.shp") - - # Convert the Python dictionary to an XML string - xml_content = xmltodict.unparse(facilities_dict, pretty=True, short_empty_elements=True) - - # Write the XML to the file - with open(f"{output_file_path}/{city.name}_facilities.xml", 'w') as file: - file.write(xml_content) - - # 2- Network - # First get only the part of the network necessary for the simulation - java_path = "java" - jar_path = "matsim-network-from-osm.jar" - command = [ - java_path, - "-jar", jar_path, - "input_files/merged-network.osm.pbf", - "input_files/buildings_shapefile.shp", - f"{output_file_path}/network.xml.gz" - ] - subprocess.run(command) - - # 3- Population - - # 3.1 - Public Transport - - # 4- Config Generation + def __init__(self, config_file_path): + self._config_file_path = config_file_path def run(self): java_path = "java" @@ -131,6 +18,6 @@ class MatSimEngine: command = [java_path, "-jar", jar_path] # Must generate this config file first. - # command.append(config_file_path) + command.append(self._config_file_path) subprocess.run(command)