matsim-proto/matsim.py

430 lines
14 KiB
Python
Raw Normal View History

import math
import subprocess
import gzip
import shutil
import geopandas as gpd
from shapely.geometry import Point
import hub.helpers.constants as cte
from lxml import etree
CONFIG_DTD = "http://www.matsim.org/files/dtd/config_v2.dtd"
FACILITIES_DTD = "http://www.matsim.org/files/dtd/facilities_v1.dtd"
POPULATION_DTD = "http://www.matsim.org/files/dtd/population_v5.dtd"
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': []
}
def _export(self):
self._export_facilities()
self._export_network()
self._export_population()
self._export_config()
def _export_facilities(self):
buildings_shape_data = {
'id': [],
'geometry': []
}
facilities_xml = etree.Element('facilities', name=self._facilities['name'])
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': math.ceil(capacity),
'opentime': _convert_schedules(building_schedules)
}
facility_xml = etree.SubElement(facilities_xml, 'facility', {
'id': f"{facility['id']}",
'x': f"{facility['x']}",
'y': f"{facility['y']}",
})
activity_xml = etree.SubElement(facility_xml, 'activity', {
'type': f"{activity_info['type']}"
})
etree.SubElement(activity_xml, 'capacity', {
'value': f"{activity_info['capacity']}"
})
etree.SubElement(activity_xml, 'opentime', {
'day': f"{activity_info['opentime'][0]['day']}",
'start_time': f"{activity_info['opentime'][0]['start_time']}",
'end_time': f"{activity_info['opentime'][0]['end_time']}"
})
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 = etree.tostring(facilities_xml, pretty_print=True, encoding='UTF-8').decode('utf-8')
# 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 version=\"1.0\" encoding=\"utf-8\"?>\n")
file.write(f"<!DOCTYPE facilities SYSTEM \"{FACILITIES_DTD}\">\n")
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):
population = etree.Element("population")
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']),
'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(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
id += 1
# Convert the Python dictionary to an XML string
xml_content = etree.tostring(population, pretty_print=True, encoding='UTF-8').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 version=\"1.0\" encoding=\"utf-8\"?>\n")
file.write(f"<!DOCTYPE population SYSTEM \"{POPULATION_DTD}\">\n")
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):
root = etree.Element('config')
# ======== NETWORK ========= #
network_path_module = etree.SubElement(root, 'module', {
'name': 'network'
})
_add_param(network_path_module, 'inputNetworkFile', f"{self._city.name}_network.xml.gz")
# ======== POPULATION ========= #
population_path_module = etree.SubElement(root, 'module', {
'name': 'plans'
})
_add_param(population_path_module, 'inputPlansFile', f"{self._city.name}_population.xml.gz")
# ======== FACILITIES ========= #
facilities_path_module = etree.SubElement(root, 'module', {
'name': 'facilities'
})
_add_param(facilities_path_module, 'inputFacilitiesFile', f"{self._city.name}_facilities.xml.gz")
_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:
_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:
_add_param(qsim_module, param[0], param[1])
# ======== SCORING ========= #
score_module = etree.SubElement(root, 'module', {
'name': 'planCalcScore'
})
_add_param(score_module, 'BrainExpBeta', '1.0')
_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:
_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:
_add_parameterset(scoring_paramset, "modeParams", [("mode", mode)] + parameters)
activity_paramsets = [
('residential',[
('priority', '1'),
('typicalDuration', '13:00:00'),
('minimalDuration', '01:00:00'),
]),
('medium office',[
('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'),
]),
('warehouse',[
('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'),
]),
('stand alone retail',[
('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:
_add_parameterset(scoring_paramset, "activityParams", [("activityType", activity_type)] + parameters)
# ======== STRATEGY ========= #
strategy_module = etree.SubElement(root, 'module', {
'name': 'strategy'
})
_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:
_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
_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
_add_param(subtour_module, 'considerCarAvailability', 'true')
# Defines all the modes available, including chain-based modes, seperated by commas
_add_param(subtour_module, 'modes', 'car,walk')
xml_content = etree.tostring(root, pretty_print=True, encoding='UTF-8').decode('utf-8')
# Write the XML to the file
output_file = f"{self._output_file_path}/{self._city.name}_config.xml"
with open(output_file, 'w') as file:
file.write("<?xml version=\"1.0\" encoding=\"utf-8\"?>\n")
file.write(f"<!DOCTYPE config SYSTEM \"{CONFIG_DTD}\">\n")
file.write(xml_content)
def _add_param(parent, name, value):
etree.SubElement(parent, "param", {
'name': name,
'value': value
})
def _add_parameterset(parent, type, parameters):
parameterset = etree.SubElement(parent, "parameterset", {
'type': type
})
for name, value in parameters:
_add_param(parameterset, name, value)
def _convert_schedules(building_schedules):
converted_schedules = []
opening_hour = 0
closing_hour = 0
schedule = building_schedules[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': f"{opening_hour:02}:00:00",
'end_time': f"{closing_hour:02}:00:00"
})
return converted_schedules