From 375fac9b424ab3d5cb989030abfc9d49a2416ea8 Mon Sep 17 00:00:00 2001 From: Umang Sharaf Date: Fri, 4 Oct 2019 16:45:11 -0700 Subject: [PATCH 01/86] Fix bugs in utils/aimsun/load.py (#746) * Fix bugs in utils/aimsun/load.py 1. Updated the path to data.json 2. Added a check before querying centroid_config.origin_centroids and centroid_config.destination_centroids to prevent erroring out when the model doesn't have origin or destination centroids set. * Fixed bug * Renamed in more places --- flow/utils/aimsun/load.py | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/flow/utils/aimsun/load.py b/flow/utils/aimsun/load.py index fba3716f0..312e352eb 100644 --- a/flow/utils/aimsun/load.py +++ b/flow/utils/aimsun/load.py @@ -56,10 +56,16 @@ def get_dict_from_objects(sections, nodes, turnings, cen_connections): if not centroid_config: print('[load.py] ERROR: Centroid configuration ' + centroid_config_name + ' does not exist.') - for c in centroid_config.origin_centroids: - scenario_data['centroids'][c.id] = {'type': 'in'} - for c in centroid_config.destination_centroids: - scenario_data['centroids'][c.id] = {'type': 'out'} + else: + # load origin centroids only if they exist + if centroid_config.origin_centroids: + for c in centroid_config.origin_centroids: + scenario_data['centroids'][c.id] = {'type': 'in'} + + # load destination centroids only if they exist + if centroid_config.destination_centroids: + for c in centroid_config.destination_centroids: + scenario_data['centroids'][c.id] = {'type': 'out'} # load sections for s in sections: @@ -132,7 +138,7 @@ def get_dict_from_objects(sections, nodes, turnings, cen_connections): model.load(template_path) # collect the simulation parameters -params_file = 'flow/core/kernel/scenario/data.json' +params_file = 'flow/core/kernel/network/data.json' params_path = os.path.join(config.PROJECT_PATH, params_file) with open(params_path) as f: data = json.load(f) @@ -168,7 +174,7 @@ def get_dict_from_objects(sections, nodes, turnings, cen_connections): scenario_data = load_network() # save template's scenario into a file to be loaded into Flow's scenario -scenario_data_file = 'flow/core/kernel/scenario/scenario_data.json' +scenario_data_file = 'flow/core/kernel/network/scenario_data.json' scenario_data_path = os.path.join(config.PROJECT_PATH, scenario_data_file) with open(scenario_data_path, 'w') as f: json.dump(scenario_data, f, sort_keys=True, indent=4) @@ -177,7 +183,7 @@ def get_dict_from_objects(sections, nodes, turnings, cen_connections): # create a check file to announce that we are done # writing all the network data into the .json file -check_file = 'flow/core/kernel/scenario/scenario_data_check' +check_file = 'flow/core/kernel/network/scenario_data_check' check_file_path = os.path.join(config.PROJECT_PATH, check_file) open(check_file_path, 'a').close() From 67d1a5298e55dcac83e77768348e6f6e61cfab97 Mon Sep 17 00:00:00 2001 From: Nicholas Liu Date: Tue, 8 Oct 2019 12:45:23 -0700 Subject: [PATCH 02/86] Cleaned up tutorial07 run history and fixed LuSTScenario directories (#748) --- tutorials/tutorial07_network_templates.ipynb | 67 ++++++-------------- 1 file changed, 20 insertions(+), 47 deletions(-) diff --git a/tutorials/tutorial07_network_templates.ipynb b/tutorials/tutorial07_network_templates.ipynb index a6aac74e8..0eae4a71c 100644 --- a/tutorials/tutorial07_network_templates.ipynb +++ b/tutorials/tutorial07_network_templates.ipynb @@ -20,7 +20,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -66,11 +66,11 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ - "LuST_dir = \"/home/aboudy/LuSTScenario\"" + "LuST_dir = \"/path/to/LuSTScenario\"" ] }, { @@ -86,7 +86,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -106,7 +106,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -126,7 +126,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -152,36 +152,9 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/usr/local/lib/python3.6/dist-packages/numpy/core/fromnumeric.py:2957: RuntimeWarning: Mean of empty slice.\n", - " out=out, **kwargs)\n", - "/usr/local/lib/python3.6/dist-packages/numpy/core/_methods.py:80: RuntimeWarning: invalid value encountered in double_scalars\n", - " ret = ret.dtype.type(ret / rcount)\n" - ] - }, - { - "ename": "FatalTraCIError", - "evalue": "connection closed by SUMO", - "output_type": "error", - "traceback": [ - "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m", - "\u001b[0;31mFatalTraCIError\u001b[0m Traceback (most recent call last)", - "\u001b[0;32m\u001b[0m in \u001b[0;36m\u001b[0;34m()\u001b[0m\n\u001b[1;32m 16\u001b[0m \u001b[0;31m# run the simulation for 1000 steps\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 17\u001b[0m \u001b[0mexp\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mExperiment\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0menv\u001b[0m\u001b[0;34m=\u001b[0m\u001b[0menv\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 18\u001b[0;31m \u001b[0m_\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mexp\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mrun\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;36m1\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0;36m1000\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m", - "\u001b[0;32m~/Documents/flow/flow/core/experiment.py\u001b[0m in \u001b[0;36mrun\u001b[0;34m(self, num_runs, num_steps, rl_actions, convert_to_csv)\u001b[0m\n\u001b[1;32m 104\u001b[0m \u001b[0mstate\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0menv\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mreset\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 105\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mj\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mrange\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mnum_steps\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 106\u001b[0;31m \u001b[0mstate\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mreward\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mdone\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0m_\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0menv\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mstep\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mrl_actions\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mstate\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 107\u001b[0m vel[j] = np.mean(\n\u001b[1;32m 108\u001b[0m self.env.k.vehicle.get_speed(self.env.k.vehicle.get_ids()))\n", - "\u001b[0;32m~/Documents/flow/flow/envs/base.py\u001b[0m in \u001b[0;36mstep\u001b[0;34m(self, rl_actions)\u001b[0m\n\u001b[1;32m 325\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 326\u001b[0m \u001b[0;31m# advance the simulation in the simulator by one step\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m--> 327\u001b[0;31m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mk\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msimulation\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msimulation_step\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 328\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 329\u001b[0m \u001b[0;31m# store new observations in the vehicles and traffic lights class\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m~/Documents/flow/flow/core/kernel/simulation/traci.py\u001b[0m in \u001b[0;36msimulation_step\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 54\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0msimulation_step\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 55\u001b[0m \u001b[0;34m\"\"\"See parent class.\"\"\"\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 56\u001b[0;31m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mkernel_api\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0msimulationStep\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 57\u001b[0m \u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 58\u001b[0m \u001b[0;32mdef\u001b[0m \u001b[0mupdate\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0mself\u001b[0m\u001b[0;34m,\u001b[0m \u001b[0mreset\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m/usr/local/lib/python3.6/dist-packages/traci/connection.py\u001b[0m in \u001b[0;36msimulationStep\u001b[0;34m(self, step)\u001b[0m\n\u001b[1;32m 273\u001b[0m self._string += struct.pack(\"!BBi\", 1 +\n\u001b[1;32m 274\u001b[0m 1 + 4, tc.CMD_SIMSTEP, step)\n\u001b[0;32m--> 275\u001b[0;31m \u001b[0mresult\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_sendExact\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 276\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0msubscriptionResults\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_subscriptionMapping\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mvalues\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 277\u001b[0m \u001b[0msubscriptionResults\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mreset\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;32m/usr/local/lib/python3.6/dist-packages/traci/connection.py\u001b[0m in \u001b[0;36m_sendExact\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 95\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_socket\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mclose\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 96\u001b[0m \u001b[0;32mdel\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_socket\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0;32m---> 97\u001b[0;31m \u001b[0;32mraise\u001b[0m \u001b[0mFatalTraCIError\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"connection closed by SUMO\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[0m\u001b[1;32m 98\u001b[0m \u001b[0;32mfor\u001b[0m \u001b[0mcommand\u001b[0m \u001b[0;32min\u001b[0m \u001b[0mself\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0m_queue\u001b[0m\u001b[0;34m:\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n\u001b[1;32m 99\u001b[0m \u001b[0mprefix\u001b[0m \u001b[0;34m=\u001b[0m \u001b[0mresult\u001b[0m\u001b[0;34m.\u001b[0m\u001b[0mread\u001b[0m\u001b[0;34m(\u001b[0m\u001b[0;34m\"!BBB\"\u001b[0m\u001b[0;34m)\u001b[0m\u001b[0;34m\u001b[0m\u001b[0;34m\u001b[0m\u001b[0m\n", - "\u001b[0;31mFatalTraCIError\u001b[0m: connection closed by SUMO" - ] - } - ], + "outputs": [], "source": [ "# create the network\n", "network = TemplateNetwork(\n", @@ -216,14 +189,14 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ "new_net_params = NetParams(\n", " template={\n", " # network geometry features\n", - " \"net\": os.path.join(LuST_dir, \"network/lust.net.xml\")\n", + " \"net\": os.path.join(LuST_dir, \"scenario/lust.net.xml\")\n", " }\n", ")" ] @@ -241,16 +214,16 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ "new_net_params = NetParams(\n", " template={\n", " # network geometry features\n", - " \"net\": os.path.join(LuST_dir, \"network/lust.net.xml\"),\n", + " \"net\": os.path.join(LuST_dir, \"scenario/lust.net.xml\"),\n", " # features associated with the properties of drivers\n", - " \"vtype\": os.path.join(LuST_dir, \"network/vtype.add.xml\")\n", + " \"vtype\": os.path.join(LuST_dir, \"scenario/vtype.add.xml\")\n", " }\n", ")\n", "\n", @@ -269,20 +242,20 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ "new_net_params = NetParams(\n", " template={\n", " # network geometry features\n", - " \"net\": os.path.join(LuST_dir, \"network/lust.net.xml\"),\n", + " \"net\": os.path.join(LuST_dir, \"scenario/lust.net.xml\"),\n", " # features associated with the properties of drivers\n", - " \"vtype\": os.path.join(LuST_dir, \"network/vtypes.add.xml\"),\n", + " \"vtype\": os.path.join(LuST_dir, \"scenario/vtypes.add.xml\"),\n", " # features associated with the routes vehicles take\n", - " \"rou\": [os.path.join(LuST_dir, \"network/DUARoutes/local.0.rou.xml\"),\n", - " os.path.join(LuST_dir, \"network/DUARoutes/local.1.rou.xml\"),\n", - " os.path.join(LuST_dir, \"network/DUARoutes/local.2.rou.xml\")]\n", + " \"rou\": [os.path.join(LuST_dir, \"scenario/DUARoutes/local.0.rou.xml\"),\n", + " os.path.join(LuST_dir, \"scenario/DUARoutes/local.1.rou.xml\"),\n", + " os.path.join(LuST_dir, \"scenario/DUARoutes/local.2.rou.xml\")]\n", " }\n", ")\n", "\n", @@ -450,7 +423,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.8" + "version": "3.6.9" } }, "nbformat": 4, From 5eaa30e8d759036e93bbe490081994a8bca8fbf2 Mon Sep 17 00:00:00 2001 From: Yashar Zeinali Farid <34227133+Yasharzf@users.noreply.github.com> Date: Tue, 8 Oct 2019 13:05:07 -0700 Subject: [PATCH 03/86] Fixed Aimsun Bugs (#750) * Fix bugs in utils/aimsun/load.py 1. Updated the path to data.json 2. Added a check before querying centroid_config.origin_centroids and centroid_config.destination_centroids to prevent erroring out when the model doesn't have origin or destination centroids set. * Fixed bug * bug fixed * deleted the json files * PR fixes --- docs/source/examples.rst | 10 +++++----- docs/source/flow_setup.rst | 2 +- flow/core/kernel/network/aimsun.py | 7 ++++++- flow/core/kernel/network/scenario_data_check | 0 flow/core/params.py | 8 ++++---- flow/core/rewards.py | 2 +- flow/networks/base.py | 2 +- flow/scenarios/multi_loop.py | 2 +- flow/utils/aimsun/generate.py | 8 ++++---- flow/utils/aimsun/load.py | 4 ++-- flow/utils/aimsun/small_template.ang | Bin 166769 -> 167499 bytes .../{ring_scenario.png => ring_network.png} | Bin tutorials/tutorial05_networks.ipynb | 2 +- tutorials/tutorial10_traffic_lights.ipynb | 2 +- 14 files changed, 27 insertions(+), 22 deletions(-) create mode 100644 flow/core/kernel/network/scenario_data_check rename tutorials/img/{ring_scenario.png => ring_network.png} (100%) diff --git a/docs/source/examples.rst b/docs/source/examples.rst index ef04f53cd..7b6944547 100644 --- a/docs/source/examples.rst +++ b/docs/source/examples.rst @@ -9,7 +9,7 @@ A few points of clarification: * Occasionally we describe environments as *fully observed*. By this we mean that all relevant pieces of information (speed, relative positions, traffic light states) of the system are available to the controller. *Partially observed* refers to only a subset being available. This is the default configuration of the environment and the set of observations can be customized. -* Each environment has customizable environment/network parameters that can be used to configure it beyond what is described here. Particularly pertinent parameters are described. Additional parameters can be found by examining the documentation in the relevant scenario and environment files. +* Each environment has customizable environment/network parameters that can be used to configure it beyond what is described here. Particularly pertinent parameters are described. Additional parameters can be found by examining the documentation in the relevant network and environment files. * In the figures below, the following key is used for vehicle colors, where AV stands for autonomous vehicle. @@ -22,12 +22,12 @@ Figure Eight The figure-eight is a closed-ring version of an intersection. The goal is to maximize the system-wide velocity for fourteen vehicles, which necessitates spacing the vehicles so that they don't -run into conflicts at the merging points. The scenario is fully observed: all vehicles +run into conflicts at the merging points. The network is fully observed: all vehicles speeds and positions are visible to the controller. -This scenario is also a benchmark, and has been +This network is also a benchmark, and has been extensively tested at three penetration rates: 1 AV 13 humans, 7 AVs 7 humans, 14 AVs. -The scenario, pictured below, +The network, pictured below, is relatively light-weight and can be trained the quickest. It can serve both as a test that the training process is working correctly and as a study of the difficulty of controlling many vehicles simultaneously. @@ -51,7 +51,7 @@ and the speed of the leading vehicle. To make this task more difficult, the environment has a configurable parameter, `ring_length`, which can be set to a list containing the minimum and maximum ring-size. The autonomous vehicle must -learn to distinguish these scenarios from each other and pick the appropriate driving behavior. +learn to distinguish these networks from each other and pick the appropriate driving behavior. .. image:: ../img/stabilizing_the_ring.png :width: 400 diff --git a/docs/source/flow_setup.rst b/docs/source/flow_setup.rst index 69884ce57..6019e840e 100644 --- a/docs/source/flow_setup.rst +++ b/docs/source/flow_setup.rst @@ -178,7 +178,7 @@ The latter command should return an output similar to: /path/to/envs/aimsun_flow/bin/python -Copy the path up until right before /bin (i.e. /path/to/envs/aimsun_flow) and +Copy the path up until right before /lib (i.e. /path/to/envs/aimsun_flow/bin/python) and place it under the `AIMSUN_SITEPACKAGES` variable in your bashrc, like this: :: diff --git a/flow/core/kernel/network/aimsun.py b/flow/core/kernel/network/aimsun.py index 53b9bbe6a..906256f32 100644 --- a/flow/core/kernel/network/aimsun.py +++ b/flow/core/kernel/network/aimsun.py @@ -65,7 +65,7 @@ def generate_network(self, network): 'render': self.sim_params.render, "sim_step": self.sim_params.sim_step, "traffic_lights": None, - "scenario_name": self.sim_params.scenario_name, + "network_name": self.sim_params.network_name, "experiment_name": self.sim_params.experiment_name, "replication_name": self.sim_params.replication_name, "centroid_config_name": self.sim_params.centroid_config_name, @@ -276,6 +276,11 @@ def length(self): return sum(self.edge_length(edge_id) for edge_id in self.get_edge_list()) + def non_internal_length(self): + """See parent class.""" + return sum(self.edge_length(edge_id) + for edge_id in self.get_edge_list()) + def speed_limit(self, edge_id): """See parent class.""" try: diff --git a/flow/core/kernel/network/scenario_data_check b/flow/core/kernel/network/scenario_data_check new file mode 100644 index 000000000..e69de29bb diff --git a/flow/core/params.py b/flow/core/params.py index e503186b1..61b95223c 100755 --- a/flow/core/params.py +++ b/flow/core/params.py @@ -445,7 +445,7 @@ class AimsunParams(SimParams): specifies whether to render the radius of RL observation pxpm : int, optional specifies rendering resolution (pixel / meter) - scenario_name : str, optional + network_name : str, optional name of the network generated in Aimsun. experiment_name : str, optional name of the experiment generated in Aimsun @@ -453,7 +453,7 @@ class AimsunParams(SimParams): name of the replication generated in Aimsun. When loading an Aimsun template, this parameter must be set to the name of the replication to be run by the simulation; in this case, - the scenario_name and experiment_name parameters are not + the network_name and experiment_name parameters are not necessary as they will be obtained from the replication name. centroid_config_name : str, optional name of the centroid configuration to load in Aimsun. This @@ -477,7 +477,7 @@ def __init__(self, show_radius=False, pxpm=2, # set to match Flow_Aimsun.ang's scenario name - scenario_name="Dynamic Scenario 866", + network_name="Dynamic Scenario 866", # set to match Flow_Aimsun.ang's experiment name experiment_name="Micro SRC Experiment 867", # set to match Flow_Aimsun.ang's replication name @@ -488,7 +488,7 @@ def __init__(self, super(AimsunParams, self).__init__( sim_step, render, restart_instance, emission_path, save_render, sight_radius, show_radius, pxpm) - self.scenario_name = scenario_name + self.network_name = network_name self.experiment_name = experiment_name self.replication_name = replication_name self.centroid_config_name = centroid_config_name diff --git a/flow/core/rewards.py b/flow/core/rewards.py index 76c09b244..58fc5330e 100755 --- a/flow/core/rewards.py +++ b/flow/core/rewards.py @@ -237,7 +237,7 @@ def penalize_near_standstill(env, thresh=0.3, gain=1): This reward function is used to penalize vehicles below a specified threshold. This assists with discouraging RL from - gamifying a scenario, which can result in standstill behavior + gamifying a network, which can result in standstill behavior or similarly bad, near-zero velocities. Parameters diff --git a/flow/networks/base.py b/flow/networks/base.py index 2979a4b27..58964348f 100644 --- a/flow/networks/base.py +++ b/flow/networks/base.py @@ -1,4 +1,4 @@ -"""Contains the base scenario class.""" +"""Contains the base network class.""" from flow.core.params import InitialConfig from flow.core.params import TrafficLightParams diff --git a/flow/scenarios/multi_loop.py b/flow/scenarios/multi_loop.py index 5a7b6d01e..e4fb666df 100644 --- a/flow/scenarios/multi_loop.py +++ b/flow/scenarios/multi_loop.py @@ -1,6 +1,6 @@ """Pending deprecation file. -To view the actual content, go to: flow/scenarios/multi_ring.py +To view the actual content, go to: flow/networks/multi_ring.py """ from flow.utils.flow_warnings import deprecated from flow.networks.multi_ring import MultiRingNetwork diff --git a/flow/utils/aimsun/generate.py b/flow/utils/aimsun/generate.py index 41590c96b..31800369e 100644 --- a/flow/utils/aimsun/generate.py +++ b/flow/utils/aimsun/generate.py @@ -267,9 +267,9 @@ def generate_net(nodes, set_vehicles_color(model) # set API - scenario_name = data["scenario_name"] + network_name = data["network_name"] scenario = model.getCatalog().findByName( - scenario_name, model.getType("GKScenario")) # find scenario + network_name, model.getType("GKScenario")) # find scenario scenario_data = scenario.getInputData() scenario_data.addExtension(os.path.join( config.PROJECT_PATH, "flow/utils/aimsun/run.py"), True) @@ -366,9 +366,9 @@ def generate_net_osm(file_name, inflows, veh_types): set_vehicles_color(model) # set API - scenario_name = data["scenario_name"] + network_name = data["network_name"] scenario = model.getCatalog().findByName( - scenario_name, model.getType("GKScenario")) # find scenario + network_name, model.getType("GKScenario")) # find scenario scenario_data = scenario.getInputData() scenario_data.addExtension(os.path.join( config.PROJECT_PATH, "flow/utils/aimsun/run.py"), True) diff --git a/flow/utils/aimsun/load.py b/flow/utils/aimsun/load.py index 312e352eb..a04743383 100644 --- a/flow/utils/aimsun/load.py +++ b/flow/utils/aimsun/load.py @@ -174,7 +174,7 @@ def get_dict_from_objects(sections, nodes, turnings, cen_connections): scenario_data = load_network() # save template's scenario into a file to be loaded into Flow's scenario -scenario_data_file = 'flow/core/kernel/network/scenario_data.json' +scenario_data_file = 'flow/core/kernel/network/network_data.json' scenario_data_path = os.path.join(config.PROJECT_PATH, scenario_data_file) with open(scenario_data_path, 'w') as f: json.dump(scenario_data, f, sort_keys=True, indent=4) @@ -183,7 +183,7 @@ def get_dict_from_objects(sections, nodes, turnings, cen_connections): # create a check file to announce that we are done # writing all the network data into the .json file -check_file = 'flow/core/kernel/network/scenario_data_check' +check_file = 'flow/core/kernel/network/network_data_check' check_file_path = os.path.join(config.PROJECT_PATH, check_file) open(check_file_path, 'a').close() diff --git a/flow/utils/aimsun/small_template.ang b/flow/utils/aimsun/small_template.ang index f5d2ccd8ad1b4c3cdf3ea02f6b8d9c66fbd6081c..645de560b349b2ddf963196a58803e67706a209f 100644 GIT binary patch literal 167499 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%0DRQcW;Cetkf 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

W)~-Rwy!NGEmPU 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 J{{a91|Nju8!9xH5 literal 166769 zcmZs?1y~f{8wHFaB8}1@AOa%YB`hc?Al=<5u`J!8ba$t;bT19k4blz5(zWE$@D2Wd zUp^oEFmvvicgDMS?wtFcm+>5wM~;MqI*EjYB#q>bWQ1e@{F)+h0((v*b|hvbJ|uo5 zj(>;?IKqo$2ypyJY`}*fK%4->@h^rAIBE#A9wT^=JdxZ2*^rQ)IwCy)KfuK?k*tAB zIv{-mw$@0rzAeaeFWDXdFoP_zGWi(v81)ND6zKyJE0PM5J(3BM9k69Z0t08*A(;VN zR-g?5I_H4o{EsvFk8uQ!8v*f;(Vvjafivup9D#pD59pX4&^=ZpV}P{>&SM3lT!6hD zaQ1Nmr;m zw^5~287pb1CRs(m7<@qEL>pl!P&r^169Su!E(sxEj3NK3+9!~X-)~3H5TEDJc9?OP z>ZSrEch* z@!L~9P`s`4-Uy7S<|(f*+uIaC+@h!X_V&ak&cQ>E;y`*o1YTH@Wu{jNG9yrCd&M+N z!RDB{fyFk_F3x`E^=KGa_I>Jbg8D&CtEx;MgQ@YuFR#smYhmgKZ>HrmZs_*6EcQI@ z_IFc>qD(DIW(a+caQ{k;;!SdRxA|W}sxx_y5Ada{9ts%Z80rK9?dJ@<53AaE=bB7z z)7xDAjLIuBiKM_Ba|}jCM=u=)83ZE+UznNgv%id@u?AQDqQ|z6stvHc8x>~pSEp3)jnTk@V>t;{Z=v;;+p8n4 z-6vEnB77DM-*YaTZ}~$m(4zyDezb_tEGS1Ar=rJL<-8%+$FJE7_gU{zj#?x}8MDEc z=!{x2z^@UIM*Y{o&7ed%zIDLGDnK?T?l_$QI!FGwbLS&zz!AWXjy@Z+c^4GG`AB!2 za(?1UL(4UI0no=DOTn$yC2Wsz(nzCn?z@4FYP0J1&Ftj47qX}sN+M^S$#3qWfG#0X z(M6EF5U};z4&?lCZO}LO10Z>*%urceMO*Fq==I-iiTk`T}?Ac zsMBR&(Zj^-XmWS-ODmLStXD1u&H8Arw1a;%b8w;Loj^+yEGQaJSUQKcPfj!9>HbJZ z`GIgmd4Y<{;+>1ihMQh|Jx zVzuBZLZ0$uqfP^U+VjFuP){i7eEClN*VyI7aV=Mve9ZMhAGJ1GD;FQ^z>y@Y3jv3j zc?$V58jv>h24Xj6W`cCjQxRc-f9`Jg%S?rZhvm=6Tzj5udwFq8?{8H{410;DJ;_hM zuvmFtU_RM-dkK7dt-6s5G|JKRLtCzim#(MQ4j~;|#pETZBeaHT=R~jgnGqso~PS?WB zn;qFkvAS1GoQ2qEG8++*P!9PcEOPclZ?LMc&TmJ+YYbI0^5}GY$IEMrO9_IOHCR)i zYq=|6;3k#cx}1K`4$JjUi%$7be%I+NjO%&J?9uv?+Kfj0>f*Rg{klgY^LY9|h4KLo zBSU7ro}Dpp_Hdg|mh&_^5Z%EQfjn@g&WqoURvK(^>p^}sL|*ovyG{-jfX+GWDbWOY zZ?1TUJ!$rv4+>UNZshueICRJ;$Cj?s9Cb5W$HY6{!V1KLh3+lqXP&>mzTBZbB^Fq} z@tjh>zey`6mp?mRsq+L+i5y;g&orJ&INy5q7(EeR>QiQZyksZ z_OPRwskWXzoQmr}Eu1ZlmPJ`^P8EsCJ1*a^F9e(@ycoSU-A_Y1!WhbeE-qc2yiMy3 zB4HWiXrTS0ujx1aGHX* z4xcp76+Ey8Y7{}@0G#XSd<<8b!To(?yrJmb28^jdY^Ghr4 z7=;*Yb>XHsJ11;8_PC^%g-!B>YTuzVRcEU^$*`UMqDM$6LrN z^Ut&8`aDENlJDK4ATqw3<4S8QTh!^jbyeTDVGlB%_Rqa|XQK}8Jir%?dg_AFaNY@9 zCWGV8t>>?Xy$o#o9Z8}*4&VHh{CbyRJZ5w5~AlS4UdRZrQ!@|B~o0R_!k~>Myo;C58$O z6+4Uc!VHCChVNsBz6Bw-j-3`oKI}}XMV>}BTjH$}iSRy5SV|b@LU?8+dMK_VVWX(6 z1Tmi}y%___W#3(2o<=}S09D9z_9zCW9}D3X2~iQ=MQJinackXP{iTM8R+p_>fNch) zir_?~Z4^Zt0du_~rvn`)vm&RP9j9j=v~?#4e{9U%2(F@ddz-GPgicV@ zH6E2Ci^raM(y_f4sg6>36jZZ^n_8?yCv6>mew$(WAT)1>cRI1}auj z(;VJkK=;`$0&p=>DTQkXmB^PlW)jQ_t84Vq&cs!(BTLTi)OSQr8obJT;ugThUhZRX zN5)cb6!^+ggQ?bbTMLfm)zb77-Nr&0XS2E6lPfTCMwQd8?$)l%c%XVy=#fxRYHr=e z!IYeOQ*B536t684wu$du+gsaQ>-v1_jy1lrDc@}fg>b(*UWF8akZ^9ZNCMqJRwzt7bSe! zrlow?mi4<==_`y0T7URk2iL(X9$hB6mpTDN50U~g&o>h2!uVO)LcD&oY?V&UPEquR zQzXtdiT8Q0A>aoAsUwgo;Zzsc2kwh6hy|XLsSk_oWFsib=%`+t0-1A00b3>k2S)2< zovi&COw~lt!x4J;Qzpeo%%){qsy$KYyCr-m+$o;}=m73}A2iRoJrfc8N z-d8yW1Pn%DS>;}tD8=>zMI)Ctdl*NDxS0p zp1HorIZYkWlqD1DMbRDSnJS@owqak4AWDkg?Nc<(Cc~~$8s6sJ6NP!&rccO2ubF&r zm3Nh2r+&#DMpwK?E90w+w@;{r#jS{;sr&6bH=yi9FeB2gq2+(I69#WHE<6ZKgi%g8 zav-Z%VJ%m^b6$Fj2c!K?bx{+1ktQXh?+(Ao+4QdRc@rH{&m>O@3@qE8cIu2#a<1cHk0ZVY`J5SlyLVJaM9UN&>Ej(@&?La`0cQtYT4Jn> z%@DUv=owv(c!aOIUt40X%yk`Ykp7M>CoxwIER{)+(P!zEu2NTe1r9L)`BzcPq|p~T zy)rMg{v}IbFWDMSylmeGGGCMKuQv}k#cchGXC^sfTSl=uyR6|jkLQPBb@zH^JNOxBq4nHB@iV%&4w8u@93H$MZ zXi9e%FSjdCvltT$!2x+n4R7k#PX?GwFX9kGZ(t%^+0MTfWl5I*j z$jLv}NkKL!!2g59acR%MwmFW@o_rs3^Lt((H*@Vf<9o2%<-9}LL{erNMJo^3vIuoI z|DZoC5H6phHlHz+bcvngp9c`Za5hqn`3z}F52eYGH8&2rinrhky0dzUVKfaQ7L}4c zN9fCC{uqWso?)3q>}ZB*$7`)d55t+Y&Fu1i1(dy-yUfSohzt#eP`YUYBk>1jxV8{T zK?IDe5r&p#g3PKckpMCi#Ryb;sA2oQ#D#BW$7YSCNSVDLcLB5W zM%qEHf{;Lb-R00ee^=e7wzEV0Qb z5J_Ze`5GIIl_um=wA4FMrU7M4JhgOh+I-O@1XthtJ*PwaZIIwn{H(8 z&gOX}qp%>!PsB+9E&N)Ae_kGE&ki$4O~Kp~r*7)0@Lcf$bK5((=ot%N_QV=`u98Vb zoclwVzD^UhRvK&QUG~J7daeQ!1_<9n)^e97iL_0@4k6q{g=i_y(Oe^BQLp;1+na@| ztT@>2nZtDGmW)1xjqv1VR)O;Acg;Jm=$m>t$*(3O(@QJ`C!%HUW_ucY?1>w=nlq~? zJpyBtI&bK2I-H3cxB-Io@rwe4(Q0Zol)DCquu_UOrr*^Q1%ldnxJiJSnqW9Q>vdFG zeIRFuaFoy$)2mnFIH2iteF{m{GZGFohM)O^XknW#_hu>L4=o26+PdDGhs(#RduRQ{PU*Wh9?rf9T!(4s~%Mw+wrMWOaMXxTH(exgWb0hI5V z2KgW8OtYLg`|hNX>ckPbvZE^;0mDh}INej0`yf{$S!-Wq_`J8#9ZVNdpsWwtm1zru z@q&DBqZxYQv{QQ;xj8|rX;9wUo5tGtjF-am9Ni31&sBl2)&wYo`#=0Vu?kXq!vGbj z8h$=~1V?q2EE~pZz3|_5;FPe0;7@?I zakiw?dFrQTFi9z#O7bUi0nI!8|IK|Z!_sfso)(Sgd6DNoz=CMgIM7NOb>Z2b82vka zGAW;PUog`nY5ujsi>u<$795dFj&fyr{kwUDj{c-ZB-`ftcN*gBNh(2sR@dP94_6)R z+ZWX`<6TWG(_y`q{+Eh3zng@{OCqQEbLFbE1Ovww1J1;q!nbhe=c8Kt%8p-)h?0%t z%n^>B)l{u|L@gO6hkV}cy`N%ED3r+Y+Sge!_&%Y@Nlcbt^l9jSx%2@8RYd|7OPE-iqskpONcU&%B-iqHn-2=QKiA{`)Ni@B^O=+zUBv4ubvwOa zW(07Eiq1{D(~Fiz>~^l*)XUAi6Au+4tn;zV)YNFa@3SthMHFUrpUF5C6y&KF)Z$p( zL^;q;e>*^3I6SYx->XLWf-l@OLuxf(`T{i>)8abQJatbHKU`yH?A(5nvv6V9O|D_X zgxDYhK4HZWghFDx*{7cKrwE03m5q;1GH|ZDL~|7rV17|?K_Bsy(IY;q1~9#f;lZmx z2jlTS3fJ^g`UixA%DH2I*sthS^v>HkOd!S~@xUB+dVv;S#mmv1Qv7(Y)+Oi2l67mR z4sB&*-0})^@=}qLb>pXmY-FS?&T|}>eD-YG0r54mcYzx|im%S2_?Gh^w3PvLByFZb|!WWL*Ug21FPAV$LVGgo8{D=fKg=E6z5OUEGe zM*bSg#Z>F7jKd#?qHYvTJr3{FttH?_7eErxi?#FPQF(|3mpSK%fMlX{wI%jZQC^cRi;`fh;MbOBwZuI> zQ#4Wv52W*Ib2!k#{)$!ZD*R$7!k3k~Th_>W(0sdNe$kO&hEq&e)~d;DhFeF4`EztH zMGW&e5yDfVxvaPu-9pZ3U1g_ncke3#Yy9uos$-+|-tUpRp|d?ydr86(YVT3HVXE`1 z_)}jg5$|ES)eN3C`B%|{opXfe@2=a^VggM4BM*b07=Wg-*EDC;7r3hFUAL~;u4R&Bc}ADdM7L@@e=Vt{sHo_e`dUA1V_X>2)YRM+ z(obgqx@d3)1@Ml_&jaADHS};dP+j19hezB{<>y()v1~dmq@IZQDKG4zI%_ti_DBa8 zW^m;t3C)sBCYrwLT~sY_QnhfaWS6T{1Pfmx@C;%MYv)^!4n&)zDhYZg+G_l=A=h)N;(!9K z3@SE%1QP~a4azNr<|_zVhQ`l;#z!qGUI$)h<;dX?HonKhm`YQUhMf}VGBf+@d+ zoP!^A)ioZv!r9GG_iq+REx?m?OBaLyB}&Nq<(ii!o}xRv;8h_b6Hq(RC);l1VV@cP z5ClS=zUGJxO2r&Oehq(V6FMI=hE6`+zELsV)IICQ9fSi@?Jvfwrv>9~u1-~>&-Oao z`V!R2bM{kNsxuNvR3j^Cd!0hRxhB;%{cc^8LBLIJo90c0-5ZDI<+=EObDhqt^0G4Ik~;1)o%Dz$7j^L@QQZ?|E=70sN=0;
x-D>r?6QZUx88s21N{;*r!L#NR9tCT{|ed9k8hLT3}m8Z-zI+?2rP-4ONm$9g3XGo@LW7FKn%{m zPLXM!Q98l$JYIU{4Q{h;2w5D0SB#lm7UjmuWx|I%6Beu@Ak_t0`@HPW?`i~3EUngM z5D%Vb`}UC|0uro;THe03BQL`0AMRd`UkWM8U)92lE?z}+Ed>Aa!))2v+Ydg;w~IV{ z=XHuj?0&ixbgYv@&hp!lSRkeL3 zE@fb03E2+whX}t+_tdu>b<7B?5Zv;rfKJh-p%=ZM#u;(&s_s)KNZ14{%X;dsCcGGv z1)Pz%FuZdok*dC%_BqvZ(`QtUVRtqZ9H|YKfQg$87&1M4OxS9us*{^~9f*gm`m8n) zk7gAsad`26YfF`RBaqh5P0Br&nQ_6UP7D||JwyduDxf$+k7}|=DwNvrzjWS=v$uoy z3)phoYV%P{V)A}@W2Fx-{$Gm=r27DM#Vc2RhIchT3_=;h)O}-p(@}?GK7Ec=C%Puk zEH-_R4vjr=3YlH*vxZj_c(w3{c}PFH(?TujP2WRtz?57IVg0xAB@WCvLTt^lFQ0^&Y#|eb7lQ%E85iq6RfGL$e z76L?_T#Q<3AvSs0vj(^mGh?zw36LW7g_>IWAx9a4J-2kQU;Ke$_fmdCIGZysU{ufQ z9Im>`x`)5$#ZJvnnBNG)Ru>}wpFZ@_v4dFNwR&#bKAQE&({^VU5DOu+hAFipoyF<7 zx?mc(A<2I1PUG07Y+}{4QdgxYNogD$u)^n1DhzZMTg<$MQZ5tV>r6}rs-1HZJ&e9qw;svJWB}pkKT1LQVZD4Z zs9!M7ee0NutX_G#7x>;9-|6d8YVeQ{KP=%KLi+$e+9E*&(XyJOF@=wyZlIp|HZZqJ z8%Ow=7+x9(0rvB8DsKdwDLF*~RhNY*uYp5$axpWqak)8)3HQ zcKY(Ytc-ME{9)Rp%W`u#t)l&ft9Nn#o5=w2m@ZWUMCGFcC!Gf({&V12cmFabOfp?+ zsY+8T;mhec1ReX$2O8EK)1v(-UY0;^z^)}#}NlAJFR6UCo*9% z`L5~lnJ^*BCB{k5ZjA#1^nR=`|C^&mor@_ml*l2>0qT3Vj2-?ow34_#IEc85w_jAEQqpoR}U`xH` zyrt;1OGfj(^#21NhIV*lguf*cSkRrm&p6~>=hGdd7wncj&p+=fQWv0OG<(t9G~=>% zDUGt6Ls2`#Cp5{~N4o=*yXwqK3^^yrrp?{en#zre!DPodtV$&o3%~$YvuC4T*h$)jUkY3Za*8D1Lgq=l@uO1?6fqDsD4#!fC7DK%C8 zh#x0~<$w55;&Sqm^->gpm}6=24_KLVQUCC3F_yf8Co+}T)_Oqct$ZBeny-#p_fOu; zZv-cbeHT!nJN;Go+~sknQjg7l;^xIwMLQkRGgUC6rFX4PIolKa|F)z5w?|D)c(x~w zyhky!`E6bysx2R9XwM_&ttBH3dI8Rmc_vL8*tSt+NSm^L#-uw^pX0qnF;4Sd_OMHr zY7lQN0F8#P)iXuiXq&q2iI>0mSZIw!I*orq`6*9 z`i~x!Yqxg|@Tj5^*{h&PPkn(fW76flf0ndp74*-eu9Ge=5Th=faTR5O!XN$UzNB0) z>3r{_P5oM$Ra=H%JcIZFrnD>Gv#8xMTxi+fZG8(anss7rYb@s%z=w)j>gdcQ$>7br zi!@Z{_7`F+0EIL+&TAKwDtrFTNKm#^s$P?>;wrm%E6X_qy9Q4by;)!e%t}31OOUxr z=%Z<)QA^S7s@d&=c5{Lb7{9O@CUP5Gfk*DXF;_Blj2M}ysPyw#uWP8C>rcQ?B^700 z!!HVAH&7)l(1zVx>C%)C!cKZu+$vUqf1rv8;e7RtT1o)7Jz}+$T6b-wSlz-s6I;ec zL~(lFUiv=N&9W^;Q(Dg?@ft9oS~tI;IKhdAOVeHz2fBK|LvATN>r`Hzr(VvD4Hez9 z^qjN++w195Ch=ae7oaY#if~ekmmYpNM83!$>Z{i@qny%oH8s|5)2FP}RM9n_Ig_2p zNmebLestYtQja+P9~OPYik{OfmD1wxu*e%S6-}nCE2Y1XVv!eQD$1x*GASXX^3@|B zdmklFe?zNO(l!6Dd=PgRvjg~8ikZn=HMe<{M%U<_Dcu9)nVMWxVa*xldTb)5S=DO3 zzzbHO-Xl2om_4kkJsMJj|Eol(0l3t{{%Wt93ZaV93*M83Es>i&lwUceLR&%s8T`e}eaqG&AbV z_<{OcMHp^{9&23bvscru-gkdxzjnK>pImh{>-EE zhrNR717-b#DKED2QNi#`=Xtg6g~Mansg)E}008$tQEI9;MRn;PwtvKm=L^=y(qBce zGOwZ(vHoc-4_4+@lp?LRu;2tDFkc<(pSY@--+WXm34UB$fHcGN=Z;DbV8YLgH9}I< z2KWnhRQiB=(WJ4uK=?ksU=Q!b+-2k}=l46S=L?t}Istf;kM@)_?2gn@$I2bMS0k}a zFF=K7oM*(gfNPxpIT&wc$*%x&KvPDFCH(63wCz3$0x#2`p~a%5^JvcSfbaPQnHHqN zOm424xxUBCP2%sTVV(2k4xz|WPdcoUX>zURANBCb8jm3Rrtf-Qu}KScmE|5do5Ulr z{<@4uUf^>6dg_DJMzwgdZy`1r(+A&eHR5?MzX2wBq;5g=lJz$S*(w3T`8k}U0VVU2 z{^>LinJ3DXY15f|k7iJLyIJjTv32#sAhRq*LqFmPpRrC-yH5dp<9Z z<|1|-`s;WiAniW1iXQBXBdl91LFlYoFD7!7-0c{HeM*6wAn`(IWzD|SAekK@Ap%GVVqqFF}ria!!^bv4b`{8i-W>J@~0&88^VwPDAQOu@18 zYjQkgS*Xl_#pUY!Y;Hkwp@mUw6^lqc&%5ovLKwT!9MQep{@X^eeX!Me;15rSxWB4{ zah^}^_$mEPf3Biq^~yws3@mNqbK_iMVbtpIxZzW-W!L)vY}hd_9CfC8-brmac!Q-s z+H3_D3GoEGtC59Uy~=Z{7uvj`ExJzn{hM4wh4M6(Nd+uD)SjMGJfFnsSH$z~O}%$; z5zppeB*1@6vI~%(>gi%yqynznj&-RA^@^Ls`GfK~yWVTyf2bRm32kDZfVbBc$KhWh@Y=0ly_`4Q# z$LR}n)0Ja6TYRB5cfJR1#4XDUM#_uW5{c1{4`df;B20TKp*9J3-ofFXE0^YI^`H~> zj>YJ){_E1J(bFp%^frwA%z%Y$x>MoXyF%GYjV=vHj_?}&kI0c`NZ=q@AN88EMI7#S z`I|$_K2)+xl2Ra7vkIIJ6=PUcAn?# z(;P|5zrDoPDTcjz8iOC~9>_25b=#vU&&~)|{J75~A#0>_eiz4~EEU3=sfS1Jz2;KK zk?$kU*m`ol=QBKjf3&pu#zdAA-bIjtIphfm#JOli8tadSNJawXct zCg1N?@ROq?noJ`ULdz$}358bj1emTL-inC-bfrl!Ru}2Jkazb8nx~n$9B^+Jog|oP zHg+UNNjat$Lq{|Bq&V*tw-P60XVXmR?U0iH z+CZP9^NW?2>flFZMU{5}3PSp?JITyhieTxs&U=dPWdqW)r(W?=^|2=qh8q2It=Pl1 zfuDuKI;%FZ=dP5WF$vBcBP#Eu_8;n2Rq97tx4RXebKYb}!?UOd-n$l4$EUjwjTchM=S40w{elz6oAszXkHRxm6b9EUc*&iJQOjf|_qujf9)3|xZmnt#xV zTEiwii;0&_OXaerOfU|XMd)4@kQq>K*)EwqPZ2+-4?Lr_>&EQH!jI5y)1r3%k;P}2 ze1MhuEm+kFe*fMDbyOGr-MkQ&B87W>yi^mLU@Ehz-AYT*-_4tBw3Hz*_l^aD-nqwf zqg5hfJCY0jc`I+`y|ssN z|EkgC3}wH%|I%mA%94A&ZiT+tYm)5pbB%5f$ba+}d*Zdf(A2%Qs&H#XS^O4-{gBo- zlr>kb(3lajKG^NEp$6SFey9!Vs{(8b2R&E$z$bOGw&pIBP`gl#34Sy5yqlD;i;y4P zSUmF)mCHo(n^q3ecbD!yE8i*+Cyfdya`p?$I-xY-+~Y-Y(fL-e4<9GcBYFoVjqn&X z6=sI+bJb!q^J=kuSqaY!%2W>D$T4cEPMP6bu7m&ZTxEl_4|I5#JZxP+ku-BrbdA{& zrLwLbHFVRb>6t(VSVS-5zj1=z+6EC+n}go{Sxc$O2K|b+I86^5-e8!~OUQFt91Vlb zDHOwY-eXYN`_Ve<*e!gps0mec))ZH-Y$lee-@c30(&l*?knQdK+L`>b2k92Webb+x zhwB9^HdyzQ;kDidd;=Y-8i zc%#skgaqM1ZwMA|-nT}AmbvFzy>gYf$J~+H2JUZD_eZJLi&KNfjKl;E!(GGltg-ZO zee(%z*t}y^mJix90aC z4$sMV!X2?G%u(y{2@)A#IpaM!eicMY`W*GxNx1($ z?Kf3ij2necFI@k!hw#SvWxAX8ciWWEytOIcS{9-HlMIbK*59iiW+Utj_Mh{tX7n!* zZr=YaM0p*XF>Gmd*V^v|t<2Z4peX(LM}h1j>R^ViDs%Aa=2g*C4|@g*jL8Dkx62LO zmW5jJgJ;J62_lMHawlJnX(chfd?~bZHb||zra)}f$1Ab^Y^@Ey9d;$?@$N1Mr9N-5 zD`Jj(GihjS`n$)D;i1A5De7_i+oy#M*juX{A|cq-`#~F zmo_<$m%Kh8lV2612a-GX&%gL#tyR`MXOdb1qws^3#^)-TVU|lQoSLSe7jn9wsuuAO zbG*ErS@BhjYxc~wsWiOAPc&jyn^vNNX4=}fOC#NZwk{Dg!d)i*UhiL;MjJ}vO^sP~ z?6ro}hfEdz+M6UaDVlpnt#!aY>r>~pJAkj28{&~;y70?&6w#R7CyZv6^bE1`Awuh~ zJlTueVC}sv$_?xn-n&mT%Ad{aSzENfGimKdhwqb``{L{L1rjjW@Jsg!R;X*T9g7u@ zP9>mX@%g7?7Y-aJ7c*r!a;@55b;y)O(n=AC8w(C=z%I7-R3L>IcJS3X!Y}V^ErQ*W zmos*FoZi{ar#xu3)=R@!2yUOk+SZo|BM9e>6I9n0GR)-Kp1u8rcuMhMg_|oZfR;b9 zx2w4C@K%Yra^q(<5=v<)WW(GUMOjwev{^uP=|d7rpBCt&){7pxtu!yFAC0iUO7u7_ zrQ)?&#}1`K`8Lol<_3%=)rcjG-onK#h@r*_EfN#qnDz`KwQ`EJ31R-(68P z|82G3Zt98GY3I!QiE`EBr}7OP2%f`_+9K#>N+z_4Z2BsxBBb*b$Rx}rp`a>}ObLz~ z-LfYO5ekDpo2_K;S z@dWn;9yf1zcg=)UA2Rx5PF~j4l60CV+!Kzky$Y(ZE4-}isd>6L8B>ZjwQIaQf@=!A zVwfnYG@b)i6Nkm#R%#EkX6*h=wp}1A=_hfg_MsH1op$=hY)zvr6jb48J58wfrk;22 zE9LjlJC5vvyCfZpJ% zA3n9_h|OV-&yHO8S3Nw>oy|boI9t0#B4z|;QAa!8E|0| zS9jiTS6p^*Sqw&Y46(0vblI1d?F~)3c>BH+l}k>QvcAU9L(eNUURKXhoQR6&;HZxN zOs^MQ*u0|Kjx&s*la}bJxxdr z?^RveZuPSc=Jy|tCK)(iX#C-dqM`P^p20^5kyb0E!=c+>qoGA_rbdJgTm z6%7tkYEaDVQQgL?E$@|1&I3HQC1H^cl=EllUt^;^tLK-UDs6JPR#6(pGeYW_mN(gP zduaJd*7ybWu9w8^!TNtjk$At&@?3=eyglfM)Fy=beAPOun_nLU?J?h>{Ay+Bi)<{O z9B*7*B%O(!8(0aLIl7D3+K$8dMJ7R}(rttiDW`XCKCG!4SwVJkgRd2kEsVfwNK*O1 zDHWrbA6o!LSwKvtmYlFepd+_M!OQZyH_@@H-}&NOEaMT<+>LBkKB3Jc9guHVv&tK~S|K$=UcbJTFKb56l9sZp7%Ueb_xy_FnK2FbvZo zddjB3jwsUoVg#2lSN+%x>)ng1TnsVr9L;?WE@i2+3~@I%DfGfZRgXcCJC+C8HG9fm zB*>Iq_9*|lPzw0kNvnKlzF5(&wcm3W<@+ZKU$0ciC?xHke=-uiE&SEAQ`j$339q?w z&~xtDi1!DWG$yU3Wa6&Ze;EUaVU%Le2!xE|vb~P`!l=%UCq}BaezQ|8Ph3$xO3K}()O4Mar{>^=D&kA{>d>X@Jb25h9+xqVzlpfh;xK80fd319(w5XhV8nQz& zpFcU;&M;iQ<(Bh%gRqs*#1$W+u{_;OHTJ{|T@Yv9`&&^xkv#3jG3Ee))jn ztyn_F53;SO_@Gq})OAZqKNy5a5E+Y^im|pYh^M`GS8hGMct}pL_qXV&^I#w%^sjp- zZl~EFUSReCR zqzp4YwnhI|?EpTjAhu9=HlhE#5qTamJohPSiIO;DVu0-I+bLaSINhh|f^s975!dt& zMQ%goHGx0wmgJb)#Vxj~hi1)@O1O^P=o31Z7!aSdUL^S{hfyEbMNTnH)K_MnljhlN zrw-JS#8+5bBPgd^Oc

;Wh0Kc;ABb81`|ed474%Qs??mPqC1t;-!}w6p2ssSP{EZ zi0|Q!F{AtClA&qdZGC)lxzcXul)zcnY=o?q`9<=uJ{^Y5);MX)c8WnS560|%Qt`s)-9-th+3tgzVsSw<2RV56KvV`g?B-Y=t4}9LID$TQHg#qnIE=Cu;(9f<+X( z&x9{(IrRC1NfMr3MWsqcu9i&GV3;aG)N!!TgCZWSHF%7LZ%{zYb{?_;3pka0db2e= zpeB17plTe{aMh|D++&f$*uIe{N9^k^YZ;@$q1JEGhOM^m$M%A zpQZS;v>HBNsK{jL1Xkel2VuK?XAuTTEtP8Ok<5Oc^Tdwor(9o{m+zNko>_ppL>AcQrHr-U>I5Qr(nSiYGmI~MGLjE z6=5K<`UU<$Z*whEDK9Eo82EY#kCvF|leXng@K68QYSAl2)&RoL*eOsUXUayq#e%_^ z7yIXi#SSl<UUoeV!4g#SlsPOE zH5q4KID5;b)X=7M~z{J!BJh^xb}0Dn}HBs%ACS-L1y^ z#lbvnLZ(}KdF;V8nY3)lrYN(t`!)GCu{2g6cTU5&1rt}P%Bx@cvug$n`bnH{`qMM(Q_Nb6r&KG-4{7?mk$d0!`O_egfW^9-)bGy~02%Z*F zQXi#una&d9YHnhUeF#a4kxBreC7VD^XH3tBhqRahb_Nbk+p@0y80GlSwYyEU6y*ug zcWgCudn$(tBj~x3!xXftx`pw3xexg7bBbD_OJ|ZKat6+;bqTtKZ7&$2D)}P+{O-en zb@ozz4utSIp<3yyHn>qHOLMBJ-%%3en) zY%y6n2kD$)FaM0JHi~VS{wd9k@r!;jN@Ut}&GB1!`(}1L;`&#@m|r)}`qNblmgKnM zjX{KSSWD69+dQG$>?e{a$a2+ks)CCiQ+WiI?BUYJ0R&1Y8OR}m`@@Cysbhu>uQse) zd%H?p2sgpPQ5R)tHV>(3O*QCkRJB<}+(*cn)9M5o2Utbv6&zn+U6E4fI?Bc^Otpih zAvX%lMJ{PA&Anj-Hqoz$f_n?7mC@nOWlJ*ZbIs8E{f7uTmh)^2^g z$}4{T6Gzx$frHvJecs5SwnfLZ#)?Gw+^dXtt~5M9LTdsYf=fTo6$8=5*@h-2;{oM>tHS5!V?@rL-FXB(wPVS~8wp-Q z!2$F`->WYQ6U&;_`U-au?EpA%bRy3Sz^v; z*5eD|udREhqdRIJ=CE*g)iB|5NS1J`)=qCxBOg$mMQa>SxUu&~6NUP~W9|F*NGNiIibVo6`D*IzdD^H9vz(rV?#Q>>Kjokt zM+Ntqy0C;^J6g4_&o8j+DCRM`G!}lNN!72vBDA5evu=fy45Q#5(S6d$8lWp0P{NP? z!dyL7byxRM1y&!j2Ci1gDt{j(?|}Z|YgdKUck~MumSsC*O7Bkii;XXC%KX2-dts6v zGU3lu7w0rYU|_0#;rP4ta4qny$gD;WV+X~ozXoY$J6N>G#dy*$?PT`F>+(F7$!Atp zgY)rlG*3C`0Qy14%+jojIrX|-V5>=9CBe{#X_??ut5w&Wi-*{W2Zj*g5mLvgg4YXG zPK`24!#OW46+MM5{|^93K(@bfv$UwglLkdBvzUTl(~e-nTb2?4RJx8Tnw?1){Q63+ zGd__f)BE`w1zq34QG-R!P1ihdX(~}5L(=~)@Q6>21b3>dOTY_H@XRQFQpMrd@%Ih) z?ciXp-<3-liqntf9TOJ1xS@;hrU4DDu9wxH->|)rzC*Jvh+oQe14{qQ(0q5KErTC+ zR7|Di9isvDEYM8$X;3y~AuOwR+*se{X+T<#mRq#Yw0;hB+2T!Px-}@@*7{4$Z5aCc zohY!bZgGvI@BBS*3fRtVKMpOyB}hCH`rLH&k~D}HllJWaXkYQ~gBLmI1pi_fTD0TN ze*zW%bm3%R`muKxerxAP8T$@Uc~k;~(6hM-yQ#LHcQ2%Tv2|!TaRW8S^A&s^x07W& zf<2ZR2lc{%% z_F1V1t+YnW=;pO!)TJV3CVTybb6!1aZ-Z%z_ z6oh!oK4tZ3&2ba;X|m54?$4H(hl1O%dI{bh;#o`I{C@-vk+5<%c4%j<8=_k{9r%5X z^Zy=DHy~a?;+J(zhjyhK2*i-pR(3DwXG>JSj#mJzjO2kW?#WXOgs@^JzEa2u&l zSo4YNb(-S0<=SUeZkh*i;w|ISsswH2^*sv8&Rot|zYdSkImyLBHLk+55BiLYZ96NJ zc+xmii5gUPo1sHHJbS5HR&tlD#R6Fq{TJ*=h?s2}wzAdeX-w(VWlYYO3%>oN!`NYay7Zv4KB}st~x?T-j?$?om>L(f6p<6q`MH(S<;+9mc`PDJ@b6af- zyux!Gs@K8TPiW2;$b1$E14nKStvqLPpdz3v<`NsHxrqAlA7)5#fO;Na$uYh~TxC6{mevEN(w9P*a{`Wh^fBGq&fQR@R zK6HFe-H@a4$0X;^LNykG<6-WN>bc(NR|*>!9ZxY=+F#Y1%mT-ZQEQEr{6e4RpJtc& z6`zs+9!8bNhKu;7&){2eQ}(lL!HU$Saa_We5#tw8>%iwVyb*)OZKz3n`@Mf2cs3Ok zNx(4^rnz#ZI8~8N`-0#yv%4%()GaCvFG1g3J#&J zZZI*jx4MDfyqLP;bTx#eOsANbYpftC$K}6=aQ`om_QLMmpZ&u6(cS3e`IoTaM6b z-L{$6u~Mtd(vI2*c|8|ZOuiTkBY0p9CG>%PD zNf8WHd`Vi3%t&$$o1YNh$HOb9?FRN1p5RJ*3C#-<$w6Y@9;=~+FSaB3lStEhD39Z! zP0@&y$(9oqng2I|r(D>q7iu3i%i(am-a|ObKz;+q<)i3vmG0+o*_xzBhgj6CYyRk2 zgm_o~M0}xcX*2=13#~J$?XEDv~2K(u4!pQTs3=`ld zOa=Qq{=9`J08ykvJbxP|^L0{$BL3@iWcfuhy^_HEZx>c%Vt<$3jN{Wu5IHrxiUU7y z7uQ*l3r~39CH^aX30&z z&*}OU@!;Y7?=kE)8J8?04|2##7u;9vD}cX{lP9B_(Ga1Y7p?)t*A<@d5_3 z>*pk;#=kM=c@bs!7~Ps%7;&Mb(gI!?w~nV2VPU+AUu8}1Fe==Sgiu(`!DD!EJIM4%Fg;S`!kjA#e$!o;5ZLZI;`5h%-+v{ z@`lh=??f=QO2Zbrhq&YLJv>cn&zB<%NuSnZN;tmSP>i{L))MXVifql@%eS#d1|qDX zD6K5t;?QYOUdv)C36(BF2E8KA+v3a(Ytj$)82{!zDHUQRYp&$;IoD=PN#ow}4*dQ@ z(D!?#Z%`!v&}l++N{~51hJ2|5GXPHnLKNjXeY!kc3EngN zwiwZrypAWvv6eGznhPZ@|8_1xig{FgpWFg*>FEaRE%<~7?r?ZJcxDowPBHe;W*7xI z*!m$Wcqw;KCX2HM z!OL=Htss-y!dN2(+s@`%eg=O}?lIS8S_E&Uyqjn9lUXxFN`CxTpsp#cVtNP4u{7p6 zpOzdiS<4BUX?;3EB4BJu>uQ9_F8S|KYiURmf8IUIt%r(P0<`?E|>xs1`m6gTit>GLxBW)rG~x0gZWcW|hD zq}7sS&_3!H-Jr;MMHrLR2byJToZ0Silaaf4N$%OE5OL}I*H_bOzk*wEdEf|@eSeP; znIPJ^@MZY?o-d6>QShGvKjZo&nV6|LA`ZnoRkuhIl5>xUjrsy(Y745dUpUFnxRg4W zW0Fl2`?K$UDF#DYtiB5wRksz_3`l4|mi+QXMn_94MaF$gtuZ9l6cST-p`tw{Nc%qT*7 zPvf<0zd!f%wu|ik0={1am0`4FNUu0?^#|CRT|pjNEsWB`9MlT&v;+4+TRT;VFaC*d z1zuRv*T*H|4pm$R9g%&*_)@Mu!HzK@rQBGflJuSMFc4KkUc_9+yJ2t?_oN^io{FUA)BMS;#2qt?ok`k zlBTmh>vMH@LDRDqE6+NZH%RdK%pLswNg=AtZj)>Q*W5ik_WX7;$trhdlv~oe@uxvt zzeaymo?9j2Qccamtop|>d;r>$ukbk=4pY)5I#xBidfj*Ff;!K$9Y#N;C7P~HTrLhT zd@Fn}dLSjJsn?N3<_mj)2fWH&AWlIGmDcXNf!}QQkz-Nx(x)(6lwENpEyEI?y@fvJ z8avo>1PNtNP^40uc~$S!c^%98E!n(md;VGe#kA#{kH)1mUj@q`V%Bd-EmLVXFBJO} zIL`x;>^JUd!41Lqm=R51e3#{YY_Z_~0#wYlUCY@i?RL#Nn-@gcr>YSskIQbsddv%&TH{(n00Y>-dRPLkp|zD`y)# zg6uDBT<$Bn&5ZV81c}(O*Xq{s_@}^(6sz~DZ&hIWM-;RiE0C7$XXs2(tWMxx7VY$mGV1BlyX}I-thBkG4E+Pp2wkG*L1b zb|4+w&PBb?3N2_poXfwP+MU#1Bsg7Z;p%eOmCEUKKh?XygF%qZeR~&ml79~C1c;jl zc%v6ex$|O&Kg7-w{HgGXJU}be25LWB4!j&n6H)s?MQ`hNO|cRG5L6Z4>K?fPI={ii zcjW5|GkDH&Y?t!QdvI?s1uCkucbyDb?C0~-&2DjvSO+(E<*Yj17JHRYZ za(6)?Us@1fZ{JN&8cG$nZQNH?Jqw;~BPs}$Er&L}p#@x%${rE&?Pem1@#=P2r6sLl z=v#kp!WwF5c6+qZO7?c>Nm8TPqV9&%K9qgxf{MDDJQ*mvOZlcJ6&1J5U~%d45KmpJ zB^|2NOF|8R7)XyW+>fG{ZCl!Og|+gYY?;Znw5>yzrMBDl)b6M1hZKF<%@)iS@Fj#8 z{^xeHJT7C-%I_V0();*{^b%sA`V+ErE zK87ofym_F8145~OK-P*Wzo&INe#5zl3Dj{=#i$#lJy!Ggi}hqCeWA!XM;N^#-^A{# zOU*aSwKz0*&KQ&0CywS4lwR3X-Kydmt(RFXUsn%=-^TF_T{w&L%Qg%Roq2YwpYP6nnwxF?#&C3&-?B$!e z?2q$tW1T9FISwMWjoVAr8ni@d?&9*e-d?WagjHGeokB*N)%FiOCtCL@X1EFb==?if zb!ZtxRY)@u)f_t*Z|iQUeA)+Oem9*tcnmG!E1+@(bUh{4?7=n^@0zNV2c2JEJDY1T zo0TY}y9lRnJF5{LQgb_LI9%EL+Z0LN?u>O>g>7a^jcJL2fj`LpT(MdN>tY_Y6>mlA zT*1+#z)y$^1$g?rCn}F@gUFL9)=G|wxhXF_JkFmjRdWxaSlH~T@41+BVYOmpSv#`V zxO9Rb+v?0ee=W}y(*`|m7r)QooOR!#EqxbIKJ7T|ysco9I!QBa+Ro>wajq)cC{I5X}0om#Y2|I%mG)aeuqf3ascKf*V*Ap7SJ}aWvH_;kl%{$ulV>zC_C)gbv zQAomah1k#&ePm3`73KZiV$wt1o~7()wRLjG-iNx%5UVX~R;+ebv%1`fxJsB($IX$?J)Ct}QM0wq?pbuq52A(ChBUjcqwo3MYDvv(e0_WB*)3{&LJ#zj3rn_t<|iaPQh@j(gxp^uled(eY+9`ZnrBq!XRnc zT!Y0O!`)5s=wtsiC`&jUwIK2_)=IRmi?;BDCVNrV`{p_{J~$u6r~&g$R_Je!+2>W@ zgauB=@Kvc_t}DiCK=P=VBF`&q4%fkx<3=4*!fanrAvPsWly11$Ku?P3kyY@cBRRspR#5t)&BM~H#oxfS_XI~-1)dYa z_kv5%obA%l$mCaPUK!od;q#8C8)Zi?Lef^b>YXCA#9Ie@6$2`#MEi=H^^q2E$^Ei9 zDzDv4j#~3$bX%H=-vlb2imI2kjxtl!L|ex5&locm28#!}q>R=u~;jNg6*_uD(5?sIaf zf%Tt*%HcO->v!bOb+P^py<{t2`*kxe&*g9F8R;7lxs`j%kYSHO+~dn-ysB-f>IRqE zOU%6B$UfDY8pUW`ZbJ~ZL@{H2au(HgRfO(ycvHkS?)KyIcW%q)b{-LftKPqavUU?` z!DGG-6K{+*HFWo-*glj$!hfgUlHcQ#;@%aT#Ij|`X$ymj2z1RBY&{mabADVF?)+aY>4avROv>;_ zRw=}z&TX0CLYGoHX0z7q0f*$>X3g3U)5DUJMJ-t;4SDRp0Q>zJ`gByRRZzTf-DXER zRyZ{kA(&ZUtwtv~viUsNcE+xAqbUAo^A$m9C3j2Dgywh;M|)DRXlt2Yr;|k-NkY_7 za1$hKI~;de5sNY}RUl=x_z~ahn?4CDN5iig@Cy2jcm$j8P8ngeKzS2qp8L_Cqn_68 zk9!ZZ=l2S6rK&oVuyv`3nI}@hpS9X@9)pMtEbb^bP={-_N%e$-9L*jJA6nn7KN?Pu z{E=09WwIZAE<-&0Z| z5Wei)J`N*%eopsTjO_z^wDw)LW4DPTH~N!1ot$OO?Api@fLoBf?QbM-n+Yw4i*0Etetj*@uG1Zc18TXXH?PqovC2p}zmbaq4eE%!d)$_R`Pc;izfo@wyhsk=w`Ryac ze*CMO?J{|qxsP8xLmhGrrQj0#lcA+`LuzV4m?(OCp4&oa9X>m%iq-#}?H&y#vn>DspkjZ^b|M#ps-Q+(J z)M`7kSw`b(%d*$m@m3G`S5Q~&z%3dzta^iIY^{eD(RMz;(Y3p{Y^}ddN$Kpm{++5> z8|#*+#t|fxxVJ>P%+hqnre3^rV%6}@}OT6d$5YOUL9uzUsNmW(R%=6TG4>7B z&7W0U(A|>7^(ND^HVqGi$RVj_kTl!7IvG*I_O(THHT%~Hi?x7pvS6j)P)-tXn!nv= zK%XM*e&%8{(gOaiV?-_b1BjdRln=Tb(WIfY#-L88Yupdp%-eN-RA$7hP*;toDY}0L z`AEm@$IOI?jFtIyqEsphr46rVyeZ=%^er2d7Ra;d)k%@dP^yYZIV<|0t-YL^7d%BX zB!=>ZjyKZG7w5)I?!EJ%{QpHek%|d=*Vtc#RgRx(Zc07uV z>LS420A+EfE3%7Q!8dbnh1%D+Jgu)R9|TqQ^{sFlIs;BK@%EwIaqi}xnHp~-{5jB3 z991j#hCDSWsY~Bwd8OD&LJou~dsm~3o{|5S@3A~8?L$H~2|0cpIY+EBaSsJQDZfax zynqyYj63x<`m>Z*;W?RW`LLGX!)dqAIFr@ha=Gshf!&IH0WB$9GZ1xU_}Ncr8REC* zYX7Yu2TF;yvpHXTimij7asg}+5Z?%@wvnoWEY1jKeHm@xEL35Z^K|SjTkCq}L?tID ziQ$gz?i{);W|7;JY;ihQ@YMGXYx4j^j{hym@21azjwHL5_ddy5tNt145=98Qe#l0? zI{1p+C_!w2Wev}U#4owgB|Zfx&*Hz%@vK%v5>XPW*4`zwqgLbc?DfQ2z?Be=j7dS9 zSDq@i)!KKn1Zg+R6P3z?YV54iht@Xu4k2m;+v^=}lJyg;edH{?p~`+oJvEIG>^&0krQ$#h2Wc zt?8YD<27qpgUz=z+oen(OjD+NRD=wlLC`(Zd%5@hN<$P@QGzYw2>j%|DF3aVESsXu zqK+tG9`@WlW6ye2kwm&rmiNk-cGMuF#$E;$9a^jbY}Q}BjL;rR-k@@Qbw$hu7?L)v zM+}tuF=o1l^>D~rh981@4?{I)qnck=S8IaGeQ7lwDMgxT+IEg3Yd&i^bG#`Y<)`v5 zY_n7Gb!Xo7yCm%k!p9-J>dn@-)?-LGzS&zb=5_+g_Z{=N0;;Vgy5P`{uUhJ;o2|-slx3t}gqF+yDBm>}**AJ5Em?O< zvTGUN@JiPYK;3GfB4Uft%iKyf=?ky0gGRzb!W{SfQn6{V4{u`Gz+JL7f_u*|fU5pS zX_c#>>ldp`{}mi%4Yevu6t|pVFkIPRpDB+?`ZQtFOzAdrJxXAZRa#NrC4Po@8K2)p ziBFAx4|JtfYR}->pcR6gY0ELpJ<`jI*LG&Fv3+tzMhPnW-hD%g*y_;I&?t$+PlBon z02P~4t?AwzcN756q;N?-2%>+x_?w_|81|#w5`D6g!(^*wPoHmokUf7(YC|HvWdXIy z8z3T*%DJJw6?K%B-3VRMrbApz`GW_|E@}~VOUnqgiQpSSdFaohKVHQw2+OFh2P1R^ zYAtT(>!%2T>(wR%dL?h8&cQW{as|xRcm+$kw$x}lZ|XbEfU=p^NBUI#uFV-1E#TJB zxY_y}I+GKH+KIHUxi(LB?0?R`bA9f+ig91Y@2NCvZ9}Ssknh*6#*t9MqW{~>?qFWc zHY>HDIrQ`SsIE8Pn0GuB?FH`l`?MUfvI~UQ32s04BZZn9j&*)_)z?^7XUQxH&NVL!LjzFMiZD69i(@CwrIiIm_g^6aGV7S?`c?Y(G(C1)9JRFkqED?H|` z59jAfsug{z{Fk6&CG9>a5zBIyeS5Rcxbk)D@teJBQAU!mFcQT>BZxK24G{OwJ&`;8 zDxTa`tIU9-baxY8hL%T>XLVlc7dn4O*>9Fm#&28o2yS7VsI|gvKsA=S$)4y5QE@yH zSXmFp8oz@--p@BjfA9SZpzlU^(bG-IZL9O{cgl%6dc++qHdTW5x3N#x>>}?nPH3~g z8T}*gZ$-?WVJ#k7R^)#P>fB#{<|tdeZKtlYUZ)QMRrssd!Z4BZ*z0-D3Y&Ag*%9>r z1lKJ5tJazo-hHb(Mi`Cc*5*p~aprlfp_YA3l<)Y*js3LhTjL)2ss%>;zJ}k>jG3*g zp%-Ng3ClMIIM)1*!)4@U%F0vhLrcz|1-inUne(Qz<*V_nHk)W5Oxe2{8LaYV%aOq^ zlr&u7DAWs$<=rj_pEKXw=4NV?>B!M^lj&B@|)?Hyd5(4q|{Q*$PtRU zqZGJXRkYoYwvaXC5;Kl$3}K_SdcOY+Zl8twTapE<$6D)wRARHrM zXN-S&M2hmrHQ&MPLaRfSRt@_Q!$R4o%N3r?6WN|}3)#D&XCQAj z+R<51e**NK!?q~WVm!=MTtlKtv%+v0RQ`H%+-$Vb6u*0(aAVGPqSQJg+h;}(s+Qxw z&ZSOOBRt42k$Rm@qZnhi5pa0Rk5)^GiShr~z2hOqQ-y2}Z5OzzYr>eZs4cSlRGz(k2 z``76dj~b~{_pQ37%_M4es&s%3@fRvfC&zh4rcV_7b}- zDFS>6$+wDEoSB`dyvtzOfY5mV%GghXj{BoeZRKnx*Ar*}RK?dwBzMnT82)=IIaoA> z{yy06vAi$&{WiGWy6yp|W{rp3xJhF=Q>n43R&XX;nrpZ|s)<(|c@?PG=W48BRbF2~ z*lVEgY0H{m$=OF6Q^Fhfj)aIgXL>KwEooW#C-X0FTLoW|wr$td>j6#n@Xf~Y!}qy7 zKa%FY>HJF+t?&tBKxIR)8Xcwy-!fK zTxV^sBGlWkiP{+d`2vUxACZ^2C3QhFvqWf+JkI4Q!%OehK@eqM-&@l{u|HU7kr{1h zy98gk=NnGy*MM8!-b3B#oxJ+LI@`EgBqEHX*19J(~%0>Q;xoAJqNkHtv25Rl861&L1vc zo}eGJY28K~O+u{3f2fjXMs$Rmuo+SDb6avpR#v*A4W`h(@QtI4V}4`H?d1Bd4&{ecaW>6gCl)PESmOqj zZ7$3Ekl*KHKMg8Z#jM=6jZycUd(=}MiEOl$^KD0V$KqtzG1~57zsPF&@=g zb5DcP!m8DH9u{_0A?b=-NyR#OLo?-eIq$ZH9wAttC92n$x7HQ!_HnVb{-gZMTtNL? zOP(*W-yWk?!u&FO%54igjXj-O}hqrn^*IS8rgXJs%6Ue{LG3lU<*R+I$M{s5VERC@O=h%mtO5DS9|WGY8q2fIPuiwc#odeC84jeJ zY4-MaNt0u>et*&4a{7V4n9IOLpzfiMRh^;{dSQp?1eeMx?5;XRSG0^#o#M~ud-nxc zN86Y>RX;}97tltnJ>pxSHKV+}gyjWC_Sx;Z&-miHUwV(T{OuFliIBln{8_az=$drR z9#BUZfjWvihWm|bcuk(otDtH|)MeqWAuPVPY)Z?h-Z7dpj9b8$5bQU?(Y0w07&n<* zL>s!wci4qrP*uTy2rBBJxxRHMeGbS7)Z_E$8?;!@^N374#Z4J9dl9+Wk*7&B_C6RPTf6nDYKzH+qSk!1f* zdCJ%}GxMx}J$3?g#nrNX;3?{xB0u0f`_K;T9XHfPC+C9ro zHGH?Pli)GtI|I~hCT|)LG6}=f|7cJ4CxP}sD4Di}Ito9fzMKEn9CsE3QTFxsWQSpY z4rQ`} zqbd3$Vcp|g;?DPk%myv?$jCl)%^Dt2t}4TYp0RF#IPJb~=+Ii0xt=^b#6pfhdeOk` z=JjrAG$P-d(RPlv6(2ea`c7C^8;9%NcmDREPB1aQriX34E2>1g5peHc1r?!HUAZ3r1XXqY zCiZ^JRyo%<9^=;%Yk2|h)>+3DdIzz*j$c*W9HmZlWPdsHRwLBOuM&lMlRa<+?Z%79 zjmvD0&^h*F5oUV{z8!tpW-HT5ue{w;pl*oH*w@~aE9wD$=~df_VSG2J^P`9tx`p3$ z2lqyfov0~w9z?e5GUyxF^!btOEh*3fcHo}hZZ=04?N-nqdN}I?F~&vv_$e*)o7wYQ*4_OAN~Z1H ztEHm1&70A;3#SQn9x-c3IzX1Q?g$gbxt?_IjxH}j^?rH5<NTL-a%EL(^VuV~qK;RyD>mo7#hde_ zgT|NTJ?(PnGJF1j-~(;(CTA9+B}w|UT;{DU$oiI z-zU}17kq?OI9UafW7NVPy{DS-P|9ni`qJ4LR76)0EZMhwlc;~LN$5+?8@Sgqel*N@#v^{J;9@S%+(JYkp6iyS@vQkFvh*;^(k(tbd||+w5{%8-bUj!mYnyv=icHw zA2-Y?X~C;!yk^{kvc$faZBOH;H*Oa!R!UsRwJhKR&t`Wz=F+>s=|Llrvb)*Z z&~9!nHnj!Nb()@_4Vi0(R}9E}*A_OU_t@Ctge0mW2k&-2TdMgSutaSIbswp)Ki3 zN#gW=2{!XHf;vSs*ZJybR0-Yp)YKgfUL&meO6ycmlpW+G0Y}$ov(&U1`^TVSFsP`R zM@A;Rvc?u|v1X?H624ba|_N-tmGbX&Ds@rDv#*(@B0sY`X2oNSg9nx=QBHK*(($ zTOew=a9f(Fh0*1$CY901mDTKvh%?UvST$BX&E#`SM#8Q?mG8jf`--p1zfAe&UfW$r zwS5aZMZgFZ{tmwds?99-5F=84QGRR2t4{LqI+I~R`-&VX+9-j$K+IXrmg)p${tr>F;I`Mnz|KB*2SeRkos zi@#6C0E)9veWir*X^h7*j0&f*OU8Mrm?_mf0>B@woYq8)rid-%FwOHAKWndo^22jI zQDgxM*`?nKUj8b>{)rr0K^(C^cX13Z^)@7bcT5qJ$2_ET_unZE>QfOEyt>55q7&Rg zug4SA2kzxNkJ4A|I!*w*y8RV+j>a)K1rPL3#8(WP#feJIJ>6d0rRs9@{x`740Nky} zr>0t1UXWxT>gLNjnd*z}o{F5Mcd9=ON?TVaew`^Q^3X*Ud9L@Ev2n#1?n+-dgJ8%t zywzM!FLWmAtwR*x_yVXLvFbh-R+rztvqv1qq^rk{9(Sgl-as(+H$c@!ZH_ebsdeNY zhZ%izay2yDIJ%%htBOy%p3m2)y{fSAlJR;14?$H1E>>iytCk!LUH13&jJKJMupX(m z4Rw(B@{{3P*0bpi)EBr;p>9ZfgzH4#^TbygWfL&7vmBsH8gz(>9AQ_Gt-LGROvsf} z?v+{k(e{$NH9PsX{F%N}*GKOlA;~`WdLz%{>30jW?>8h(Uy#s6oaj>!fl&wZr#Er# zqFOEQ6{5k|=dD^b+*`PJ0_>R?B5&&=YKuFl^Q(J7H%b)t$uDVyD*KBJG+${FHj86< z{+Vj0>yb88RNkd4ahBt!uHbi9*`AMaWb;R#t)PU{ZQtHlTS;_d1{=jG!lKb zH}^d?#in5wbfT*8{5CY*6e4^aOURA)q~36YVb}Jy6AN6~ zH>|pp{~QkIKA7U$OunZqjQG2zXHNHV|sLa?nr6@-{fF#FLCIMCMq69s(ph+ zVC)m1bY3bZd;=kh9Ow}&=Ke6HQ_@lH-tg4UP}sl68tE5;_dd+RQ|~f-OYh2m2UHy) zD#EAk_6M;FgjGxFuveja`v7$w?_@+pQBVHtYW7ED-mkLzY2UeMM{C`m%)fX!Z%1Hv z7etMla+a|z>&P3H?B}bMhv0lbR!1(srzxW)zrD_ndb90a%_s=6TG0-^moaFrViY>N zS1s}8s&(7?MU$ZXkJOoU6E)No)G4^X;u6Eos$S4v#8NcnwvOs^6U9?pDnDE#I=LPPRHL^hkXVIeSmIHGEu0SAXnZV&7s$o-FRO z|GD0Jzp#%t=D_ayZqQX~%U-sf!jmP5%?4Dx)0Fox7^>{A^I_ZPRkfMniM(G{YT=kE zwTstKI49?zZcVJ>!dC>h0Md0Y4%hXUGj278Np)1CLbH(D&h#MO+xb4O_-$y+F@`@5 z%EnLKXD94fDsQ~`CO$7nR-LUDu zP;4XT(AZP93$sDiWAr@R-%rXJS#p35@Cw-1too<#eIUL$1(8Q4d8_$8l=$ z^I-2@Q2K(fpp-RQ?z`vOz#Zh}Wz?PCL!LiC_mVRXfGKG_h&9%0Y(J=+CBDXYKv&WgXTL9E)Z%00s@0OVTk0Wk z^n_-9dyl9tf1F25N)_K!W4KLtRvXJWzeU6r{Z#a8nYa0wclnjQS^jZe0pplbD?*%C z*i7fVZ#=B`-Tf>myIa*m{>V5h^79GT+*QuLOa4O(xDvu1@LSXEW-?(t9h)@xND!+)P~no#d(uH)mJuxg5&zAE;uS*!Jc zW3FJ2m_I*E4@)g%1boZNKz@B70PCEUAlLdbSX?w5JlSRL50* zGt3GtGuOza13v)DlkKA^K86+06@AZR>uqq?kRy$#)~n}k9&f$}(KD#uWX3y6+IKDgIq3E0a`R-=&WcciMACG; zk)M6Bty&R;zhE7Aq1DQ{Zrx^a*~`z_6ILtm1BT1P%A0!YwI?)FDi^r$8Ts#XGBTS5bR5`@PyTI_E@c0z|-hD?z1{~%s=9%!*K&8OShDG%_+jitYc!r&k7^ZQ3_n^T&#_XxPznJ zg?Dge@(lSUgu=ZdSJC#zd%O?LkseXTk+6<oT zYOHHw)dxv?0ussR&HA-3wl)o0y{GInUu4f8;W+9+SJ!AzY^nfbgStD&vPxFvCu zLpk@Af1^#T2lD_#l`eb5^!6k2Xv!;c#FOl)8Z*NhDaHJto@c0}w&!~3|2w!-kdkBH zmVZgx8W(y=wOWXZv1^P=lvIi^iiG4FJs4+xZ&ma#%A!)OgpwJsRL(#-Q=FD|KLPZ; zj#XaKQBK873u`|)LJ)LQ5(eD$<2#e*3sGEjwO#H@{66n<;(K6L1iy?NA^dV0!yAUI zay>71j7i{5)IEJ>Ep0NR6}Q`}O6)@N*{ruehY($3fv%!8$0yb)5H?ADAgRxrG(|gw z&+v+Kywl4N81saF%U5fqr?9o`BcQ#d#nfYP_28j_Q{5JsH|Yp=?x3D7o%}_SEuo#(i8P z%P4g?Z%{Qj4%y)pIJf0UfpR0(ERV_CJXJQdYU^?<$*x>}+3{FK4iNP)_;;*Uk!q%M z%P}OBsttCR{PzTDQxg?C?x8n+3*U${B?q(HpjxwZZ$q8KD0j>@jTNSMf0{kNSG#+c zJ?J{GQ=FdHq`pMmuTs8qs7b@=2@@r!Jgu_*(dj0sz1kqsciU`3x$+{dD?P|1FdY*l z^7kKsxG%#zZMwuXwSY`YyXKQzySW_6D;B6$S@Q{dFSipNw`gaa_R4G5>|=9=61W%m zV0oZWxvL?PVc#DB6{l&h7*>DJhkr%1$m0dlH2$n79PTq|tg5!_aLxBNZAs1b44t6_ zu2~-}50vs2Yh!iC-U1cVq_QLL=lbOx*h2aF0R8h%LEmh$l~be8OJW`XPxh=v+a3HkNgtSN7r{+L5hfqFnoSogID*%uS=b> z+V1OtYk&Iw?P`nXaa6umR^&IKAaPp{z&ALy9pU`k+D<#f)%o(^0fQehqXnHU_8q{j z`M$UMP`jY}?L`sBGi8RC-y+6L4;F4PYTW6^ehm0cAGUZ1<(JZ)CYxWXE^eeX{F;5N z{zj9Ba^TnKjt;NYR@GmD z9mqoihp2I{%jYiEs%WD~c)po5F=sh}K8Rwk ztfn8W_5D2PyL9MyTS^YvqKZ8s1P#wP1J)|GR&R1ucX;wg={eg@y)5Ol!zACo;E;W_Uq2<>|}#^Uk3 z_ooi}q%ZAWKz#IbdbMzC#w@`z2*ZDTQ|O-*^b^(zYpBACz>N1ydaShC0?$F z!gj_h*P}Lu0sbDS*aJ$Uaci5KWz-P{Nug%Ds5^%HnRHdp8f>EQcR}Ti(G>v7o-B%^ zw+&64ge1p`G>(Mk8(9!b|)U9#Gs%sN~baBp~AI(LNF+MOBQ#aIZ#j;H;%m`qKf6QhC-GiHEehRW4Sn>q z?{pJXwFtWx@iLyw&7DBm?EL+v5yz_G_vYFdv1a=cwx@2ER94MKZ|T?{1y!Na9?NIy zZ$)0V_K|bN05pk-&r1my7 z!jdzMHmZc@8vzt^*4(nI;+oBN4b`}FteN6sKTg;wE9eoRg&raCQo=3acL!2|WNsZ$(Aw&6dt}i@;&u(KsA(xx$O}NtM zdBjjXrFD~eQ2GAU9{nD+FX!{~{I+{?Z@2>bMom>K#YI2qWXu|w-CXY=0Bk^$zyADh zvY%Gbvr&xTy9JE1@RBJhcxwgn? z5pFlWz}oV9s%l-m!G62W^vKKXxms7Z*!v|$pEt1GIMQU9wYY$pHqYdcuCxDd%C-9( zdv}w4We(+8T*o)rlPe6?MYf`OtB)gW@l-t%ksv&8{Vg`)4K~Jg5Ro8zj1g|KlfV@a zQ6IZ;*IC7#O2l{UW;>OT15ZK3bL=tZd4qjTK8tSv5mS4a{k8zV4@889h>qMTc`dK! zKd(9Hd~G=52-&-#>cOt_`PkLd)mE;r6yqEg@b3+%wPpgh9oztIZ7ssc3@EKJFr|sN zp512N?lYv$<3A3{-fFvBTjb5$!{^+lq|VGmx>L31TEMN5&COA-p))y}YvLzMEi=PX zKymQj1Qoqk_TH|bOjoy#H2ImGVaWF5p4{ex;|p>>QqO7rxd7#ueRD^wr~9 z;4a^OP7|ybKZR`}|AcbZuYk=HdWxTWIZ9=BMjr->lTyt;3B)wlr zO0{Cp}*R&e#uwTa0wGU&k2=$-m7W3m+`Yk=(T&n{j?S&1WsU zoX?%h_FcO*bdzQtbz)=8YSwa-w6H?HiM!qgN?nIrzxV$C?0pNEUPoE*>~5Nd5aMPV zw&^x3hcv_(k~E|tgf!G9+1yGlOEzt3!In*SlWe=$U3WK0Q+Y&g3W6enh*abv$Q=|A zv4AKFDBiDJynq)j{&Epi5JCLUdpk38&dfP8=bUfj|2%(vB+2<^-kEu4=AHNcLj9qQ zmi1cVXDCY0c2Gp_kmD>bJAms-PNuzO`*J&G&HO_{B=RGK<{LZK%;a zk39P6OpjUZ=T`%s$%Eu+kpkX!t-7AMVEmf{r}b8`f&Zqxb98&1f2YK<(S5TLJ%%qM zAiynO_Ni zQns%Pg}cpJZDU`r#OmkXZQxtTdR_G18?~(IzS^b%z0DgET}7yBUXZhU)c@vn{j2hO z*3V_$tU?2449P#_)oTQ~=DXK)H5)81URT?SH*v)oC2~YD`~GQ^ekM^5owww;>J3D| zF>ho9zT##&oK*}a+jm}%dj#RkGaa2*6hA1rRLxo%<#k$g{EAx_ZCl$!lW%)jm22A@ zL2q<6>#t~C!OAG@LOjesqrAf*ujKQ*ihq_!AZ3wN#jLfpN8tXk`GcFRs1GX`Orx|Q zzq!xjXO;L2met^l(U^wR9&K?%=ZUS>r?al@~62NWM2-1!mW$mNZQkHw<1qQf<$;+ znSG_|re=+j0sf7&V&}5Iox+TlJ$O3AdvJ*7B3^*&N^%oWF0?5%SrI)Oj=#Qae4R_r zeLEm4s%WF!)zi-6YqJM=o2gt4jgrNzbdPOWidL6n;yLUtL`ATIJV4 znK8Tc0#6~2Ni?onFWCTYO}sT*$u>vqv2IOTB1oI1@T)=Yn484vkuJ z5392hbpi(mJ-q2C>JLfUlKzPWA&76f`qN5koy1gmn8kHYpj0jT=g>+QEsW;erE006 zoptsj?wCMv7!zP(#Lam=+-l_36;SS8h+=&QjqB9t&?XTMf^zG@TG*#r43yORM z@{y2s)h4RhRLn(P^;&g&4mDq=g}}{z)M}b=k^-c}Qyr@^qj%pe3Oq}SGVCFw!4sK< z+t$e*$NxQVw1@l~7x7)zl-8$La7Bpc#$#MB=*((T&xOX`9!uk0gq5H-jtMDHV{`Q% zDbaK3Qv)+Up1!t2_~L@}ZvD>qDDA~dbw4dTPw?Zmtfv>9roL@4-kv z$cO@G(dVzbmfIDxfb8T~j(P`;QgZXk@DsX%(7C$wf7dGUemDJTFChh^8Vq~V|Glr& zUplzIc}^m@(U|$IRCgG^CH$*PUC+Bk!>D%f+;x)H43x<}@bZNs4p?&RPWl4E#{WX86;3YbOuPta| zT=^GHE1eluSEKay$==aOs5Re-I=P#P#>rj(Cg>@E5 zvvLV_%yU+@x*J{bmuX*!-n`Kr1DgKoiDgxwCD%1f^W!GDJb9bA%_aVOd8FuNTrHoS zM4zb2^JT}>PH_x&SxoKPpcMco8;aqMMdkJlD&bV^>~N>hpLMzY3y#E@XHm*Aa8aK-5Y10*RPwgu3 zw<)_;p~!v1tk0RolenWUOvX~{b7HupLUhBa;!HTp(ehYuWLhru3TW+ zLs@dFefob^>D*f(72wCSJfSvsRN0%?-(W0;P|Vo5i2rnF?3BusI>RZE`X#8>m{2-n zC%m=9Kg|VHxu}b;rPfSbd-8rIFXdit2mBS-(db;*-|S3&u$q;wzPygwa0UB>gdFy} z7@rgiLHJ#iLYQXU_8V~$grkIiU4*ZV?3h0XZt*j&s#YpG68Q`~GQ+Qb-HZ|)nh`@U(~llToeh&3- ze&35)5an%*#B1Hz4`yCiz80>yIPlv(Xla$rm@vv=R`h7!hdK;bRq3)wxYovDc(MA% zHrKfa;|(R&;{d-~a(1=F!uA~sh;!=(T65TQ2Hep_$1}crjx8jj{}E2fEyJ%fa*OAq zkW82@&!^_@wT+YT)w}r2e^e5s=c9nWd=u_kG=93?!YB->s5$QDKbPWZzK>rr(i&?I zp_6o8O7^B5Osp-ODGSq?>|d}pGuxM0FT(0)(fmkg-1oXJMoclU zS;QL>)W?-M!%Lxt&La-{!peX?`H1wf=aN^SCC;z5r`DpYh#OZ2d89 z|J5|E)$tf+tX{wEH4i(!NCe_RPw?#wZezAEh>&$q7r9g^jvD>&`g;AwzKIXM^V|at)bt`6V#d=5SW#-a`5p2)i1a~0Gpv>Xvo?)rm<@4a&tEJzYy?cH>0%mb9 zO7bw)S+t6c1<;pTaBI?$`fJGh&mZTuZpjVmS3OM?V?$t+y16{u=iQZAucITI?{I`3 zzSG#)-!Q`oLG!)4P;2pomy?)dIn|wMbXt*rzC;E}&RTmPUHMTvG9tr{)qHen^@C_X zmk0)Fm6T`p?cSy&a|!I+Jl|zGQ<7)yhV&%ax{q9IUS|X(?opK1M5{kgl0lC!xjeo>sPBi|waX37PTG zp?NTL9Tn*GRSLQ-ox{|YRQi}?`=hI0;+U`Nh5CL4yo68Z))rMR(>Qi`bn~5x%FW+t zoOJZ1Qs5n5TBDX}`Na?#nVOh^~Vov#K*fM8M-7vC?GRjF(l%mMy(OiLYOgW0-b$E13SHA=O?Q z!ZuHovvM3z>{I%04xBY#-A1q9hHhQq6Ws$@0k-2z+&Oyk9C1wUp_*_@a9c)M>#t%P z+i~EY-j6)H)Jn_ymsjT%(lhaCC8tt3!QS<`9MA68t=5EG!k?7g>iX+go4z}sS3Rl7 z=113PI8&oMih3}d8F}kg^tIHtqnp+yJgd?cVDbmJT*Wg~+ zErYDmeVdfj`x!w$zF~ad&Ss-)+s_H4@UE!)zGDCE?QQ^>Vd$vovP#$0^^EGT?=a^z z1?-M7W~q8c@nnNcGM&+5vP0%)?Bce;Cvr!oYnoSf%r@zfrZa8OB<6n~HRia|)t7Nm z8?Je-Bj|CHb*yJPK9*U$?Dk34!>!%?s*(ufd~QO3a}K>yrwm#Kbbg3w+y#Sw25s)T zFgT2XRz|H!N!nWRO6L6S-`$p!W6Gbam2z0=ilf!FGNK@HpV&I8v9sM;uoe1isFqg4 z==}!;F;<#W2Jh_!#1PB^pj* zKuQ`_X?wWR5oK=eG;q0~?*v%3wL8zX`!($iM_?$ui16@O!{+PS=;Y#$DDrowoshmh z&hk+i#r0=Wxu4%At!4JMx88|Bh=RXJ{v~$SOVpkG#cij!Q%Rnye16eM+z0tr(V94~ zD4UXfhh&DK(14Sn824F*2DeAxPa;1xhJ9O|Cu6oJ@&#xYQ{gLj?A${jWAo{X(=fwt z(WD}8E|dF$J4PB`J!dU#98Bd1h_VVl{R=b~=X+3nZx#+yhf$B{f@Rj4)j4Esh$5E> zRQC(f_pNOLVfm1t z-r=~q7p+%19x^V6RKxKYO3}Y;3IFNNBee{zvQrF+6kp5nHH5cIzEbVi+wrxVvVH+6 zxHoNo7WYzrzXD4vx%i;-J0yOZ=U<|8+& zG}-$Z@IG8c6@9|<%af;S+u89EY6%zAR(5=ueckN%(%+?@JEX+7)KyD~bbAypPCHnp zor{1|g0(Wa=t3#aZ51wOcwESbv7LajVnId!w5SU%UdGwKxM){m8nzBMsXJ)^~3 z|HtgRtU3q zQs`fqE1?VCuktJoqJMU4!J1{HD1lv*zBO4(@wEuj*7biR{XQ1$GdGJIjOY-^(w~pm z6t7%p0q<0}SK)%przQCMW~Y7^RW1eXEqnX=U>leM0_stRncS4uSbdB~%R zYG_O&R_6oo5(P&vlj#o0n{RiabqH=^SJ3Ai$3;6P6!7RnDQ|x??Q-- zxioq(v`9$YD^{}^Ki_^wE%8I?X>e<;Kc*x{uFtAE$vC(5U`Ozkq+CeCCGR#)RHz-f zV`DRXvM|Ty3+@aqb{Xe4AA#$FkyudNl=hiQ>>TBCi`HF%Tms{UN?{G4GlW<k5LlqS_g=JZewlB zjo&7`nEV|joL|K09e1#)B)-(bdufN4v)O>T=#lx`E*AJk>U7*ZV0+;T^C{kx_ z)f-ikJ90vKGrcSYYBg5Mvyc{jl=JfGoK~bA9!EXXoHO!GXDlNbr;1x!ka?JIMa@XT zg~HciwAd3Ysx>jswtBmwnhn{VJ=$ZBRG-P-!2<{(&lTv*o0=bRwzgjFAXwaEFp|2( z&31K;d)dp;Si~2)aq^suhWay^Nrd=b)R@rCj?He{cnpS9^k%t||9pm#+crV#nfThd zW6b9i)m@rItL9{;pGes2glN%zMe*-Yn@KVf?qz%2L?-ji@qRtOwK>=nj`C9%y;H!> zcrKkA#L4HYW`h2{H@!(PgK4xGj-sZg2X|{&Q9e<6327DdJNWM`HS%nb=g#t6_6_pg zS(U}SZp!6Wu70zdowqec?&id5SykT*Z1I#8MrT4O=8~H>G*osxr>7UMvj$|L4Vf;q zey*r`>l(E5ae-N%`ai713qr@rRL9>BmiOQ_MZ)D?J(2p0_}K?!4^aQ2gtIGV>AVLy zl_6#7Qu8sa#hjzj(9Cx~du>Q3Tqe`hnf1n$7aYwZ)Aa9PZJl8nD&FwEO!xyTU-8zC zyItQGa6#U25k%PabnWK$pu*`@Sc$jzR%;`0yj{WW!h zeZ`)PjhZmI_jQ6uACDch3-#A!^U8B7v0Az5)UXl{Zg)<AC&wqnuolh+JEI$J`r}Y&jHiG=in%77JUwqccn__#;l=S8mHW%iy6Kiyr1!S3mTwAeLfOEOF$65MlVhL$^7A9?ukeb`a#XtdClC^RB zkdZ#u?MJ&8D?1dc;YoI`YH#R9ke!?gW3L3OO;@w0w90WAgbFX}9>@c+-|0tHr{a^} zSF5ElO#DY=?`v;`9jj@j{#=PCM41dapOfW7kY z1&cq}Way@E!fe~zx@4!~F|aCYt@TA^I*jFtsv7T0e*<{KNS!2~&A1Ax48}8$fFmbY z6)8u#7Bgn1JDuN6tANhL6O`4f>u1vM$GsK{SAFR@<*q2b2XDl^MpR)SRK`jNW>G>- z%J!r)_*kp_m9>+swVQT4IeTNmBKhDR!fiA?cjDqJ8;xdC80#)v!GHRqE5XY}xCo+0 z6zO;lsV+!yJ@}1$6yZDY*NgDm?TV~v*sk)eu8+rMIA{6l+^<2Z7wGa{Jmm3R1le^K z*90%mlQE(c{{L<@%3g?m9ayU)nRIMXE~|+G%{KAo+23+4;;Y*(!rKIY!kqa}{%w_a zwj>Lfza`eMdH>7B>Iv!woEQ98;bl2+vl5=7$kS2Xs?*q;BC-XyZeQ}U_obyV7}|PR&z4}aCm*KjLEQKV^7;!Z$D=l&HwJsDEMEhZk>l2Yy*;)nK#Y$idv}J7bSCX zp6=@1=mbvI7wq^6Tk!Ke+`RSQD3MBww~lg|jw5e5rogwLnm$Cra$>UDHWokQl|}_a z{i{3Q?T;#{L^h(%^UaZ(VZ=F$bpUpfVv5`~s73#NgP}VQhRf^j$V^I3a2b8>6jnB4 zRv6%C#XH93Ig%Lm43{+jPE}Qh+822s+TT#0r4~jpcOZhIx3j*Rqa!R5=41tu`{vwH z(-t%J97fFV0~_wkC?4 zr%3sx{@(#(Fr3&-qiw8hy~qF9)z=muWqg$Dn^!P{TRil3Gf0qfv{$9_d+RsZP?sD zmSbxs>}ql;?wE(mqpR2h0{dDQ~v*zE8)2%`V3T_i2ql$ z4R6X$a*;>Z-fI-)3~=*4yngwjHdOBK&wR23PYGp8hDOVC9&_S1J%S#^AUTsKc=c_r zhl9lO-qY=>EPy|zqyo9imj|?)(|jwm7Ba*$sTNG`8_VU^o&VNM%G>0~th?9Q*Nw$r zQ2v?>pu?q1mAW2lxmQLMq!OIfsUYE-Eof8hUnp7~{x+k``ckDn7Jb0`kYA0v$`!!U zQFJ6#Pz>oPYM(Ay^{edo8ea{smdC4!@%C4}xzH!c^$Mu=)%}Or+%)Zm6XD}3#xXAC z6zZXLHZ8az=MC;+T^K}bhguuoOT8EIpUW9fV;QyXLhKA>G@a_7Q%<*2NZH5uH6yF) zVWu`!J{;|1b|q&YORQhrk&0_dUKMxE3EK}i{$;I%T9d{%S;Z~P|8B~BlU&h$JC&~v zM|D=`H)kjQFpdDQNuvc`+)`6P#|c|48Af1#ZXN5(7(Tn+lt!IM*O-*og6Mjm5)akl zD`Hw;if=^q3)lc|P3qiiB^xY_SdDjaDfqDumr8TxGuh@m)727d>toiu-Z8CiRHWm)Du~cFTLY%-=A;W4Q}_aGv7zg)GZvp>rST??sDb-5ehs zn^Vb7Rp(pl435@Vu|>LNr46?meCZB*&JbB8^V)@cwrAm$<^pa-t0OT?zlZuF)Kn4n z7pJridY0X#w(+3f1L>=EQm*>c8iM;hfG8y&w+5L$MA|j;B2k4GihHcXcLu;U??VfG zXV^-%S@M0Nu2*IzC!%c9N8&zxH3Yz7=8qx{wJ?_H1RFvx!En9E=#E=W)f<#p7R#bI z{63^Hku8nKVK_R1k^E-zSsco|Q>%BQ4Qy;I%qiMu_UfC`?~~rQNbXENxBNZ$wp_&h z7kT0Y^9sN_977A{9R9sn55S+loyTK*E$FZY?zK_F#U9eE-CPkXSGRD>8IJ zo(=INbyefFBRJPM^m*>wBY`lv^#I_j99O-W|Z{1p3=Um?d zr}bGBC|X$Ddv(6XwlO7Cu&BQeijePqh!P3Go#Tt(P2g;>g{$)X{ev?Rhmv(C(W zQF=CiadV6C0^i28itMwy73lqsE6UK|s=zvjaHotnB>&#nR*p)@yygzgqj1XW=Y8rN zrnaOzJIVfnu6}{pq7?WH`eJcxY2TFS^DhC_xUUm!6gfuC*R{PB?%=Drx2B2?!%?{~hVE_B9Y2WQ746k|2TD`Yjk z2O6srIob^!2Y8GmH_D_EyJNyo{O!ulPG@(=H>--sfv!JM!u!K-l1p+gOR5sbO&<9~ zUEI9?x-b}wm&PM9oHdbJt~OTMo1UToNu~>`=j~#;t+Ny#R^$gSSr|@aXNMvm=vSLp z3+m6SZ8W01Sc0z1gR}@O1lvtlzsm5Vm_W%vkM6?wZz!?r`*Tm)HG*Lz+r#y+%6oHE z*@9Xg(fKk}Os>SVz8pbraXLV&-qIN6y=Wn;PW%eDMcJjOuP-C!|51|5buUsqeN#`O zKA)%@Weuadt6!g7FJ5yEXxy_W@(ryw?*Xo*pj&7SxD!VY2Z`cNuQtn9Ws+j2Tso;3 zy`QZg@8B$))V^8{=`3D0dvto_DHEu_8@ONjbBc}Uax27VooEEvr0dRHEaxpcpr(<|&Z&I9BA^6dwYv@JKo9mjh$sF6QjPMS2eY2w9k~W$pkN>1 zcZ#WmTs!1lDpk6#P-JbLW}eI660&^m$`PG;D#`l4x@iy-b zaOF3m{4BB^ud@UlVanhr^!y?6{!AGeO^hn0@` z7SEZy_R5tb!rO!4M#8N5?#0i1X;n6h-tQ`@p{pxkXt{D9XGs)|D{1PsGum-3x;vdbJ+h1CH1T% zZ#&SD>Z--D@3!ikW(e)n$Eo?X}_8q)tuNvj%ni zl9E`jN3yrgl)_93i)~VpX1|Sbbtj7q|o1e}o;wgi>UpU{I%2_a6m)zK<|4axr zfH%&!s?6}GzOBSJw5YY6M2*S4X`i!r>&FVZsA_$5L)hkP(G{+-Yrdj&NdKTM)bNnj zf!et4rAp%Q^Bh3Y(o7anY6TKoQq?be4QjyWzC~T!O8(QGXL%`0j59dHZYA0*iFTqY zc)qH9BuDlYa3?x6fKD|GL|3*BUz%B#^CnSe2e0)EzRP0Qf!0?t9VpuQuXkGdsdCqV zqq5T^t{Y%JM3Dv^zK||&j6HK?j84i^C9;LH>x+)%#zxyhb6h)?^ddL(d zls?qhhB8FxJabf8cpx1eFa)s1*<+bZFY^coybf<|Cj~Se59`;%KY$chlQsJ=VgVWGmL3&#t}#^8MVTsv=q7T?Q$| zjv<~iB;V?&o`$0^6kR3H9|>K4^L`j_*u92u?@GHM{f+B z;S4s$ZyW1e=k=}5&&T!7tWuHzoNTuvNZn>|7N#Uj+FCb7v?DBSI3WAk61A%irH|5{ zKuVjBYHw-AC6uc=UwU(FbpkK0`}ArheoEzY-pjX=G|A$-dL}ztbcWn}d~-_bB@Vyw>G2R-&nY3J5nLY>;O`d$EID)$m;E%|AbT+iVhfHQ7pYS>i^ z>JaLsL<`h`nVCD%QVq2FIO^v}eu&vIe|K}FV3{GWj6kyp908tL;4ZgY@p=OK z*2GTOtd?T58GUn8J$Nbo*+b?soqv&XUqo2*%vA}6II2c1Yro??ZyD#P!IqyU}7 z!Wxu-bUTxxN?W(L+>J`S!i1U+D~gZS+$@~@an@{}XI|xYeCwYCcuf5SB%h25I z*V?{8u56J}kho7io;A-Sgxr+)6%=#SxT(=ovscW;y8snCokFyA?eGTQZ;a+!*BYev zr%_TaYU>F`!_l3*M^`02o31!)WiGqYN3xfMtRd4%bb5I&qvduh>wPQjYf|Ir_pNGH ztKvMKxLh+)@EF;j@u3NR#@>D#;z5s!vQSHf&qNiCA!dbZ6Yo-@_pXvY*?CQ; z(28H4*K~aZhHq&RmmSNWx?(_yA8mZqMxwQOeza~4%NA$B5dHZjS96V%S%f{uZ-a~_ zJc4A>2f2r&mA%+&LP^$f;Cr*fqaDs$+rX|YzgKI>YeRLuC3MotB&{Z^ zaOFHBKQ3*>cIhAbm3U^BXH1z?MO#i-{+L?Oop-hRtkqq~vH)AuYclw*oJ;`u2;Xm@ z6=yO&Kv%?Y^s{gj3D-PV(RJnG*RVSKjW`Coh5K#}DbaJdYO;}UGFWCV1>`P_faAP> zs`CED&2%&Sy6PN+cc=;E^~WshL}}*1uJ0{*Vah9qSKB8Vz%99d)p_6Ru4GN!s^{Hz zWxMC&#K+Rpen{nWoQ76%i-65NV!6S%*{@FjHKcT%8S=tEiE_SA+IyVx=OUSQaQ}8T zr>fdkjUboYSNJc)$F}Lpwt6HWJ2#bm^1U3~I$ltTMPTT#vQH}c(q-St^as&tAAKBux*tz^G0 z2k!Ddy+v7aj{*%h)bAl*oP9aS?ziygl$$5aXMdVUiPn@S@-n8Uw{_!cjZyP0ZMLeF z?BO0FLp?%ik01AIb5Z4MhE=_&QSjs(HD&T&_)hK&$gv19#!Qwu#6-Z;}yPRcU`R>L9M2)Za$Udf~t6IMj z48{4bRckk6X1UVR$0gZ!2L}2fE`G{uODT)6)c(M(jIDB}sqO@2#ygF#9r$vEMy4+C z%$$3pD)(mL@psabCD-hgWb{BHI+6mU1pSLAm^Zb(yxlkEsPfk*q&&ZSQF}a!p3Oj+ z+#bO(l@6FlgtsB8@L_8xo=KvIh<`M?cOq9B6u-`i^1v z+fdM)=6h6lF0B4FB~jIRT_d+u*A?WJJ$SB2pLnAu+wOJh3F-#*I8~-q?cliP%MvI@ z!|7O@%^LnI_fJ|~I=|}QT@oKuL7A<<%AEBJ<+&BV%^O`FNNFGNU-DzjUjchYJsGv5 zn^ZnVSTgLMx4yY^B#>_!`Se!%rd@{h^NuE(^U%k_b6Vo-bgwR*IsMlm&$I47P*v8d zJ;+VX;)?*3Yd`)=dl=QVxOw2#d|m6VVW99^V*KiQ+kRPzy}dlrS7#;C{vN*A=gp2d zZdWyG9YB^lO>JlVa7?-_&wv{+>8GCnpER!zDXKjT|GZ}kZNUGu6Y~S z({NsHn@nEQ0gx_w+q&&N>%!nO#yUD19;1yao1Mnm7RjB|a$W(A z+Jn-|#S|V%NM(1XCQPBxk$0#53f8LKxi$&50-5Xf^v(>YzmIt6%oWNX*%DfJK0Zz- z2y#st)O0m#(!Uj3md0lAES|2!^FP)HcpPQt`OL?tsgHDvvtWqVMrxni#lv8x2ZE?@ zvQwE6?VNQS|^Y69OFxlZy|7U++gx|NT8*<;NGRWi7b=cn*UQo(Z^+K$uML~eq5FgB-^cO)hmi)&AT`i<*Ya@y zL|$>rqcha*NJ+MR9IR1K&RQm(w$43S3I0X#_Np_AA}VLNiV{`8675hvz9Jtv*GYR{ zqnXvbI}49L$SNri5(f;I&yfp`OJ@W*%_Z`q%pb>17ef4tYC$*mrbY})>`InZ(=+uR zRh4OPR#E16(wv0q)uLfK&uuks>dbwuM`v{0YVGEiHY$ZRNX``y90@?OMx zCoJN1Ds>rr6S{$d`>F@sn^nS`;9ImmcmJw&mJMMu6s{VkboP9^t&V;x$9GpNtKE|J z-&AO=WAKtfPq*|06~Bl0G=^r6I;Ct+Anh|042N;j%E%==To+R0;S?)C1FuJIob_+V z-n4~{-h#)Jo8N<|(;Z$pdCKqv1Kv?AV{<_ger>qizgHD;TAfq7-Z}=IUT?acTdsJ5 zlDz5iQp_S>GK_Cykt;vtY-L;hDk1U8TN>A-FDMocl)w$yh4%2VDhDMo=Ha5`cc$N; z(W;j^aih~&WjLLo-WMm+c1*x!T@TNpCsIDW+!W>Y78UaIEr?cIc564ltmh}GKR^CMOXG{zP=5G-xAKbG0WU(1!Z0F zBg)UGxT;5Jsm7FM;<>Uw*ygJrf#2pUx+&K-?=zJct;6+WAUAGde}N+@v|`N35k+yc zcJ;8Ox(}~58j49{qV+s+vmMOE%OtsWZrnUM+c%dt`vmUPD0)i(mzAe6B~djBI2QU{ z`?;I2iWF%vcQuMw#XyZeyP#W1XqHDR8|5A(k7fL z#I2y;@-a`0@m%`eV@m3t*rOc6(@GVNwEyM|N8XsUJRe=4?Ol~ndBq(Vzl2{X(Zp zZb3ziax~s`3jKc+(sl4WqeEq`Ua5J|`jCMNrv8#tmGI*ZS z9(gW37M(eQZc>C24I5)58Y=fIT4}G~KiyW^W%#@#=}0wq{92NW^3rt`bc}JtINrSw&%?zG zhp@9G9fe|@#ZFGTew27B`GwGxhpf>>*B+_F_u40oM}yj3 z?^cIjvdIy+d%5V%?aCi{SDrufh6ZtjL6A${@9Nl!`m5QD8ZPj()t&~?xj|R`lae~r zG5&65=6`0dih>VYx zM`x(o-q4@v9;U6GU*aC;T*&hgI*CY_+^Z-BFXBJl z8VQMD-IqZlsdx$IdYCXY5+j*Te6Dn7;&HLm8r`LNc(ww$j;QW@MzPMYPu>iULa*{U z%t?@sh|Be2s5KzU8P2@7`k4PP+lQRrx~m~Q+LWT)zpx+odT{ni<+mQsWxOrarR?W`_RS->M(UHdM4BjPv&@_QP^{13!AR`DRMjKHuG zjay~@S5?a)o+s&Z%!B^x5MR;-*)e>zV#rfMyc|TXj<0Itfa5&FU6p4zZl4#BGZ2WT?ia!!z)59FiY(kgXWg=nF&o*aS7`6YLw;h3ETL1G~pk z>bBd|hLr4a=P|sopv5MUk6$mzpK z_bZ4A-J3ht%k7rDlED6Qnj$rI;j`;icjEUX`^8OV->NznTL(F0+;M;#0^$I*g|TDv zV+_}c{oBg_UiiGkWfjj%E#g6bT}f|yQHgD;oO5dg*-0_0Z+z3$Y<1{Fb^FBVQNi=| z{&V{MHvE1HDFsxwhfHYqpmlFPd+l62kwoH4o<5x!LOd0=#zSUob!biuJ=WITISj2S zWIIe8ty~_}u%r7+WdiZtF7x-Uz8@`(;k+r;Qcrs;jP+=*xH(&n$CFB ze4Lcnn?xd&xL|j#%#AVd8G9X_liZ(7lhriOXy&qTKj=uX77-K@S zf)?eunq#p4zUXx1l_HRk9zmMjE@rjXHv3y+O3_=GMMr5u*VmNzm$`F- zo>He!Hli2#QceTv&T$eAPu|i<%1~$CYg2sWe@iSF-j`N^#bCX6OrMt6jiX>VHE|O& z&(#Hg)k<8fc)b!$YM#F*`#9x=+pVTBs*xfe^VxT7JmuiiP#w<@> zq9g;H^eI+xbDzCmRg%k1`F>#QC&-gEGHyjI7md4}6w z?=W+kx}U@tQmuzg83fuD@S5MRDCTq;Ywwc%>s$ueeK$b=akf>v7v0`*%;9n-+N*oP z=2EiHW%q0$akYV?d}eW%ZOh@|^ldm$<>jsZQ)!QEI2)JH_fYDRP2yar+T;x>1(66n zxox>V){7YdPbeCpkpI$`i!U&vkwJ2PoM?<$Q$cJ|}=zEZ^V1!dMmsf1&_i48|I?`pKqZS&O*WPS%p zp^e*mx>Av?A8xfBHz5A#Fz#vOo;7Dv(wJLdYh8$_%_ux^B)NX%2!kzEbqr7)Pi1RTEy}C~`FLw3Q#_clo>ZN*p zUVnoTgyyO^EH_)zl5!8h?nf!m)cdFqSwAN%4$YV2wv5)cFhWfV+s@jcvO%$R!V=US zY62<67}L}Vwl#XFYTsiN#2&eP7)bX)`D?CYo5hWWEtYf!r1>00Ey5hm3#k4KN_6d5 zk==fsQ(Oc7UZjuq3=(4wb|q2r_k{MBvcFSByG`te;kTdsFUAs|wM3u5%JaBLnf9nD zly=JPQQB?=|1C3Dp1r7Xs|A`{XBRtW1M7yI`Zs$#dPqXo-y~MbRe!uJPQ4-YlJ~JX zyGgTEZAbl#*;;2+$j9SV^K(FX)_AxhctQ4~&2v(f@o)kCOK5c&zQShKpZ@)^xLV*0 z==7iQtysNV2|sA>L*55y2H#U#^78h`?p_pJ=P9bz8pqGRw@^3I9er}W5{b^Q&)3;@ zCQ#y&yvJ2mk6Lh@=cYO)r|uet9A2`fC3b>qj?W^rsQP}_fHb?u%O4M^X(bln(~6>2 z@_VME?kk`$Mp_uHgw9p-SWVYCkj3vLVdr-Rc((5-c=Y+Q9Dogx-X`sh5YU&`TYDVt z-%d`4CaYbV_LFaJTKTg|@+Z6YaxSy}6*K&uME)9bo{2B1UESP~<&ekhz<4FEPZ@4I zv+^w-33pGfUsa`dh^F6KO6R01BuFp!s&M;s4X8CSTyOsCJhd>7<=VSx#4jp|s}L`c zyw`}W37%7w5z)VN(dHKYmXb*&>w9r*(N;r^SfTA0q)3{HTjTEt$&!kmbBbLHn=hk&Tif_$$cH5Kkoll z`hDKZt@<+O&Y}fDq76WXAWb*vSq>@5n{H>bpJ(Zp{LjKO3SXA_m7Iq%z0>8w<f7@R-hgD4JDGcPga3Q z(_y3l5c4EDA)qGUt5_v*)rQUeS43j0$y)X}qi5|_;>QgX^*o;51o?z%-W#`M_WSAg zN#^M6I4Et&nde`q%96-|P3>B)_^<_?36GiRovYl(_*kZX&z$MM4q0jHIfHs*WNN_D3;@Uz2Xs|H?Vn&nC9pI8vw@5gT_(eM_vUyvUp zugMb6gkmJ3Uklg*e1_C!lx91t-eqv<6L_zRy1&Ty&F+!x9>8tAq@*U>6iWeTm1r4E zZ~v?R$7WVIM44%Y$b1XgWM_A`A*?F`M#lMO^?rCQAses2T zEoMl3DF4}q_d$C&(JR!2XZoxnkFbk)!syyB=T}MUdDF|e9N$McL+APYfGdu`5m0iM!b??llLpthylzJ>}j4m+;wRE5Nu7AlqNG6OL@d` z#0`JVorEr&1L0AhiK?A)?}tkhb5w;_&eZnQV(m+9Kegj(+N>cOF~0de=j_RC)DVrs z#6ZEvv?4p?ipMLEc68)GS-wPwq&{Wolf&~t)*OR-$IEYZSF|~OC*_-|Ls`$f4Cx}L z@y>bWVdQeR8gnT6o6N8@BpiEaG)WDpLjMpsW<1Ga{&S62s!B4cO>v(3V`|8f!iXYn z&Dw#sNoPM1889();u>NPp4dF+V!~de$QOq+Bl%y6j#B3Q3~I@#sJA11#@foO#ywqu zCv99tiqncBkG+{Lc>WFGGA0sDla%@2QqFL3J?@F|uUyM`>2OIl3;M59S#MsZnq{P) zP~{N?4PiT92&H$!b6T6PC_KkLwD)B({{uz2{v^ARD=(uNkFy77D>PKzBxheno*{1x2G0Z z!uF#~I%#Axs*W$I0grq3M!pgCX1?BO!_zi^id?v<3@Y~-a)Xbs&e4{{mg?WRS7{G! z&*HP+H{M*xa4Y`CpkP%h`a)D-l{X8)P`=-xafSz^;R?J%yeS5aGkDo z>Gz*^-t4-Oj>k|B)A@+dnbKH7a~AEzg#NF_6rZO=-^GznVIG**RYV zu%n1qb(OYwj4eKSpS*A_c%zZzJ&)9dH(ecM0=9pN)Q0z+%w};6#Y~=b|Sgi@$%O!A3V0ywx!z^+86FCmP3tt$@CYj{MB3(~6-d2KA(ynq3 z>toM1ZdFdKS7u|=J97%@L@}eZEB}%9?knkU?O@F@7)BfUoSLq+VU3KXk5(d+`aJ0< zkJkm{o96kq6Dl7qTIC(sc#IOhN2%&+rd=(q^Cq;4p5qr-OJ};TDbWSS)%JiM$7P?D z{deW;DSB+j*St=ZFJ^)dW}l1fW^krOdJGkpH_xz@ar2q@g6UPY+zc?QJ+ zbbmmB2DvKQfO1~XSb3Ubq9+=RQ1fm!TT{Ogtz`c0)*XIaL7WTG0v)+VlID2!{-4M= zZLs;CRpd>jt?D!$_+Ww7SN1yyBG04cImkN`X?1NM6RVBg(A<>%-L)qlQ0`b-Gn3~b zHIM%y?f$qTFPm|dkA<&TU0$f?@#F8qna?C_^*!qsK0{M$a9-*s=Xm0|H&<0J&7%y4dbgS zH;y8Wbd^Gx#g40#pSl%)Nvukajy+ghP2Fa2M?o5cU*MIoB_RDS zzzplsYtJa$cd%7@?O)Mq^miCfUq8w%kyx$V92fYei?oxs$VeTcbF3Xb<*m}^_kL># zyW~7pd-yk5(OR{;&^EB%AU+gpQ;M>Wqx%l0`*OgU2d_+%9t1%srHVUYp|jiO$=)Yb z*r8`&ec%;8nSQ^}Vzs(4|~Ej217kn&+kroANYY)Oip6RAvfyp%S_t#vi}5$o^Tmz3he z9TG$U{9mB{-&>Xrf+uQ5GW?Xb^W^%40X-OwBwTh!! z1N{T~p!zFiKE0JCoxN!H0-8d+&pe)o$xOc|GAn3X^Yv-^-H;duEsaxx^|D*g-YVDS z+M1RGv%ULOhHP|hxmD|)?Drk~_kpvgYz}_y(zf*f9&38DE z&jbp%I9sJsReG@1rSq%)-L-pf#QWO`tUNBIa=yLW7_fJsv4e~X_jYoIHFs?hukP~f z*?W}8CwA-^`JqT0viN?I2hceB`X4Z&LRR@8@XQ;Iz)-p(-;(C*+RW!fx4M*j^UZuH z9$BLYY<;^j8y?8N!^m$?r48cUihiQN?~pgAj%{%E=an}aA>!SDCcP;?xjXF-uj#ER zKK4mFW%^gvU+qElXyx}|)zQ{lRFd_A>`v3H#gTX27l#UYl-7H^wxBxv81l&>9%jK% zoEWaw;>OH!W%|O(*9yO}?tekb>GpJ?XzXFc;*=q6`FwJs;BcRwNS$8nT-Trtk!)Z3 z*Tjlmb8AvT2E5CwK`g7^Ad!nw!PwQ?mYQ7gvmGAWe5E32Y3DJrGmFvG_uJh=YUSzN zcW(Y2IH4V0FBy3Q&SWygT+Tyvm9H=3r#5W!_DAKjsK1ug_>$@01H5rDlhC%(Hx#Hl z#oLlR70waVf?Jbz)Lp|ObtuKQ8#mxPP7$>EoPRi?H&*>1eU8)sPEGmDM$Fk%8335BG6&)^1OI^(-W(6v5;Pm;+p7R;1P8} zmA%!CATx|u$-3!k_Ar0532CTgNzvX!eh3M}@Z|gH{*Lm;05r>8A0PeDQr`aP>;rAD ze1Wp|E6AgJm*-@`3DA0B(doch&{;1f(-}qDjE7r;6zGcfseFcRd9P)(BsZSU+m>Ww zc6sIY`;ooO+c{TR4cG`^4 zsnVBpcn0RJisqF&iI-gtC`k#7#Dd~}t<( zCgLSQhkq$ZVv2&)I+y33?dC#W2JK~L>TF6SpPnZxsS8>3D|1y~%D+Qp6`B0rb{!ef z#CwExty8N%Si2rom|pF1N^&RrsAZXc(2meOtn$85RWC+|MwUIq%?Y;?bPf-@O7)>Y z&5|69#DLdwV+Osm5tc4M3 zp22q3w!QSG+Z7bn2K`NL@6(h%+=0!|4At3As@XWDOh$5wC{>J`}z znRy`0Ed2SZ;N(9slkKGJ(E3m))`@I1Dnlyz`iGpzrt9s?ywWPF{B&KFQDWCEm}ISj zFJi0gR@a2u=ryUch5($6twhG`4%hk>XQEOeF7=RDdg}b zkn+zpodK^THs-YHs>4?1!a4X3pU8YVXg?>F&SIa}U54=Bc2sI5T%JisX5l>bU6s_g za~Nq5+1>+rAhvis4@@oResw7Q|6>6j717Yl^BwV;)tU8VsTLM+Pxazv6=lW9d#@iu zet~jEm$w5s!A->U`0Re5Ny|5+bSa~75UKX zF4(sTDbLg~nNUyK&;Y4d0k z8dD+hXA$tsH^ibt67BEKz&V6|NwY{L08_*lk}ssr@&_6M$`weJ+s8Bh@jiZwy1lTZ zO*J&}!0bT^SA-STzfNpj+`cW#{o6%R-=-uhw9k8uG%EJnSnBV_^Nob+kIFU zud*KG=y28jHZk!~1(Hs_h5q+x=B@KsUZ%i{nmGAW$Wu@)j#HT=ehP1xtI~OKT@S-o zH$MvlR3JK8rZ+#htCH`XcfQ zRmyNx^8`Ioeq5>*d(psv;1+mz@*#kedKF@UMxYlA^6X4nvya)2m5ORNyW!o6E%nNZ z{U5F%ldu1@4=M5Nqx^clc>vWpoVLD%XYZV~zeT!kzS5K%(agC`7_6?Y7ej#u3au+7 zlb*Foyn*~R=}0be`Bv$1y%2a^4aX3Ux{Fa`)hy87Nq#S|?7tFxH^;-q!i>s1)7-M+ z_p7oPZ%r+#quYX^m^^WW1~6upd23c_B-#pilgIeD??HLe>RauO50L zWY4z5j>DK(kohghxnC$uw{Sbu=d$_6MxIyKG#7oHEvLF7@)u+VJXWk`bD^-axeYC- z`Yq-6$)jpXI?rvjuSpanC)KOY;u|~LZ3PcPsuV}K8&hT`-wGcSb+^)09rK!qQHSQh zlv=8_FO49Z*o@GtG+oUakH(~=hA7RV3uF<=$n5rLo5KpFR7u0s>x6jj)r2fqMD&Dv z=G9-vN~HEu2U|;Ou2=b|m1IsRj&U2}9Wo(i>_}$ck=)Al7zr|{;x?Y{te{5Xe@~3IlK-F-gFTQtVt;_$G%TjYf0J7p!+EE>D(`fAD{L`pN4+q+C$8F_HiSDB z*_E~icv;tkSrfPQS1{<%8c3@oZRe(vPg6Q~SAI;&td);=7x^>B%pFNZOPQTGC4HHkkDgk2N6%~!NFCn;X95~D|A z^r^~d_5(_LO}OLHNQX%I=JIh|{>Zy%nA|TmOMIQVFUR*>e|KZ%ct)jNm8D0OME%dD z+&lO^=*V#W;^l4A?h4&4jZ%WaGQOz2HEvY9jUGv%Ph$0Qzd)~2;y>q_k!`mr$7&!H zZmqQkB{sVte1^|{mEl#J|II}WVT7F?zJav)nom89W0QCOWB@nR0Ir-n6?g8Mtz=zF z??&;InY?+C6LMnmYN)I#Qq?J@60k{Mj7-vK#XsCHN{%^37;8H^z0WPNXS5+E}-XBK(&Uzf_-}@VKRmgL$6j6LTAC z5Wv;|XQx=DrYeG|BjcKMI%0o1g@YYAT_)M_rFW9+Us=zm>l4}Cc1hW~YEv-_NOg{r z25=cdEU(sVB^xZgRh`Ty-&N#U3Avs2v7gsG@^^68=#`#9JKrVr{H00@=TeS)#yI7k z{=D8s@N1strfW)&8c`deHPUVqwscdICn(Ws*e{Ri`$$n9y7GsX4omy-2(_S_r!%6S zN!^t!tF9Z}wNXj#R3FU&TkR;quM-_w8+OS}uFeBncP%5??*TEQEA}hM@?rWzH>>Y6 zjoi`>aE;GzrmAieMf1S z(29PXi}_EtA7{UzMcc>L9PODoXw77k;khI_4zSwwS{_#PeUXy*&QW}eL5D=~16Obl zZQI5kCFiBRo$0qcF!k^5Q}IhmB3*ry;s-OX6;2{$FN61Q1i7=BV4B*(C^g@&cGhR| z?+HSiPyv-vyN^}Apfe~#oK&_CT|3-B3G9w=1LbS!N57}y@9uZyIoW7F+v5f6OFo*W z%8FVWw!Ghjl1R+eT}zp_ZIO@KrnFvyd;=zx_I}Z_1B`rHQIu5Pdb_5&Kd-XUNDQMl zQv2F;bsMwjFw>KAy`o$`X0;w~{`jsZsk#=Q8#-pcy5Ta|uk>m*s}p!iXPa2BdchBC zy;rL?p6#!FQtfKSxt_e%V5rrRO{b4@<3a zUHaQQl-u$%tSCDJz&(+^gd^z};51L5K3;VD&OSWnXsFyTR{v3F_DS|t;=50#Ouw3<&u)LM{vVAIk8`@Z)%Q-CTn)(nZ2l_Duv6N(-nto<_8Cfc34ew?1t@$ zyQ>z|A=F}u4Wwd;_tCpayH)M`+W@YUVlb{saM)}m+Z=u_ zx;5FObk_Y+zu+FQPp{!u*RF?`uZ-cV*cZ@S-m%Er88)wwiXEWMAM&1C$_PD$)+^%L zw7el2qcES9R>ojh*VWQWHea6^m04_`7(Lv)!75di!Rid^txn=eCEeF-u9jkzUZfFx zh7oU@YqqAF`AvZrLA(LXUWuAe;?be8J*6sYOkVwq3UWUCt9{Jj-ylBcEdOe^C0S*@ zW?htyjcmS7$bvPmiPA|d-Gq{&4WU>IMWBS%LV2BnsL`U-UY>(Yv?8_zRO?0};4v0k zokL+~-L<%@Z)Dq;S!HOA^0RU6&K6Y_)1x!OHjO~aL2~jP)p>4XXS>xIfm9>Mt$MW* z>3$fc`b&!9m?aC%IN#-b98#c#Gjrm@EIuzx8|O3U~O3@k?U;X5k)Z@&t7Q&Pm~pnRO{f zRaRY>TSGhp#n2wY`yw=D6g|YIIZmJaAG9WVx!r?dMZz&TmZ<7?v9ldW|3<%4-T~qw zNf+ss%OLt=m|y1DC}6T)0@tJcNSel0w>}q>(njFE6F3=2@9rk{5EJ{Pv~g42n^c)@ z5c+{fBYB$uHP2h7>ghNu;Bx<>_MP>dW_CWr(^#l2t2Om81#Jm#4%$RMY>m2^HU>u zf;8o8$_EzUnH0#P>9((tnfDNyE=?g#BYg2MT9RS|eE-if^;&|OZnXNb2a)`~5pTpa z;*j!s6@M|>l*wevp2830eptg8+po`t!Q(>6Uo&+F?zV(^baYJmTXFp-@jR9 zW+ffM-$&Wof^12<4R6h&wrpp8_>8rV&d{b&DpY$}wzHkaX!Fc}uCoA{C(qXKZ7)VZ zQcg+ABRH37_$0Q`_PBDzaxT~`Ozy(Ex!wHd()51n3S?b~Igu;MPQXIqaf{O`o)r4& z=u)?RakY{x)c!29^2;1@U33aVvIN(F%RNKvwdCKINU2=yLWVtnutF&*FP0rwQT-$s>z6r9(LL@!TtXJhMK5hNNOd z#@zzX%t;ZuoD|XA)rH63!9t(J(*zb?&4KjlqmR;|qqz|t% z8ZP(iiMB__&wo{VzvWw!^GtHxHmK_Hv|9RPiV_pgF`ZcxtiChokI_^o-_d!Q=Q?6f zI*qHf99>szn;5O#n#dPt_s&P%C$e``s*Z%dUbW!Xe4*;DVVzUnI#g)y=Iy_z#Aa9( z@$J$^hSs@rXI)ObP;1Y~q&d-%q4hA(bgeErzA2lNFYbbTmZnm^5vbx=`uakhkZSIr zHdN<}s48E?CaVCSY{&{z!8cq5SiITc(Kw2<>Ht%!8J3ad`|S!eJEnBJrWyvXu<>X$ z>2MpX>^FKP#_vwt_Wz11%bf0$hAtw_ky*Osm=T0yW~2#~As1vmHP8D(;cAyxI?T5P z8S!==qf|y*@lQ%edTaY1y*}r%w;5Ju{;pr5-h5w+=G1`6{jXDXsCm6LNQEceF3GiY zb0NS{9*(mARaZ@kwTZSmKEv#6g^ji`)6Yq<@BYHQ*x3DZyDHsm7FIux!@t^Kk`K5+E zgVg#%6^|TaISw}sgw$-?)Pvd`5CCjT|f5I)#!Y6=&r_9n6ayl zNYxzO=*s^Yd0%@S-(2(;tdPFyCvlneV`{lXXV!0_mdgTX*6#t!&{nU1Fh$8oEDW9Z z5KrkE5KTNr@OAa(zB5-9G@pIo2;Ls@D+sCyyz*fB{d6>+eJ5MfZMCi9h>c{hb%DF59>+YczePI{Vip~6W7DTgFK((VJNd`Z>OtXD!6 zy5Ev!J!BOdJ+Hv6$YJW<5|QRQK@h!qu+(OC3OTF&EX#sKLhx==q=$_2Bx!9dhUbsa zTbxq0&FZm?XZqJxMRNY)^qy{psM?a9X%j2k*+X)ZqlfLWZ1;EW+3MG%-|uuvBivS9 zSCDG=AeTlG#*OZn=Cl{Y8gFw`KgLi8$2H%sK)HYBVzW1U zsJ=up7c<)VU$976<~^{mCZ61jkaox|xC42>GKv(xmcc9{`y9Xir&MKrz7?P$7)al| z5$1aBIM!L*ONl0!wrV4Q7J~YjQLQDgytLx``Oj6%`3a{d6@JGUCj;rqrkN`u>*n2x zqT23dOfP3?UOZih4)wD}Z-OIIMS3OO;Zj`Zd4@tcyXTp1PawUxq)2nP(r9Upd?iA; zh*%dt8Tc}TlJY5(q#lBB^YbTdU!OAb%*u!l#r4cE&6BUoa318DZ_9q9wOfoY{G1Yv z9pxz}-Rk#nU(x}JOgM%3?d8$z=;;**V+?g?6N-Ek1Y@J9za$xJDN^Do#_CGkE1eSx zGDGM}-mohV$I(EC2KW$M^kZK=D~i;ndR|xK`5+w4@jjUR$tuJ;W$}ddT!XWkWjU3i z1n7Lsqf9b^+6|?y#Wm@6ClSKENQD66MVuo*d<6b`7XN)(5tq6Q|9(0(UJmJ4=XsS$Pxf&=sLuaK4*8Qy(X2!RIEkpP2l>^ z8z%#HTX3DE~v@`oQTtisrmGm)7c5Q0oN}(2J<6Z!QJUxU>0yHw&6Sg z7S+li-R$FVtkZn5=d;_iom-#9U6$MNc*Ga|i}IY(K8zzDNi}#DV|zGDJ8@Q|)u!Db ztkOJb|8K-K%`5VP)INWpPlcJCMO`P+E9&t?9OGpAe?O=wJH!&a<1kJ^n~iOW?$dun zt;=2&*W2OZjN#4P?|2K#vp11-1y-^T@29+2buwDDG`h)GMM!S#t(~e^onn28{hLd= z+p9!w+^Z%~zR|W!p`dwx1*0XeB@hC)j;-GmKEGvXyOOWY6)%0Ps*3$4`2b$46HHYk zzN%X0stMV|jI#YU90e4dPFU4x~(FAs3dOLrRC=w$v7H} zYt|yuzBd)Uc|{FIao*W#iq>pheVj4b{LLM;58R}S-izLV{PyhJG=6z73Q`GXx{wwP zr!)&JkJsbut$#Z(7Q2dG)N`1>xMe@!ed}g|Axk*%dE@v7O*0p)^(alf_d7l}lU7*! zB-I^&A^^Upk((mU2pTWX<<}Cw74r-Et@wQLJCU6yW=}R`-VD+{v3i*LF!%jRl;pE7 zTI0z(;fxY5ZSuPmO@!%1;@5FCgkAD(R@atqvZ9-EZN=|V+9pQp;Ft6osAGEAvZ@U$ z%9^9;fzAj=9!MZ-&ZQ^#Qb8Mc4keudKRm`vt0OYZo=6V3?Nx7%?%~om?##|gd8C7> zX$C0wD6&D#8y1j!#K*>56od&QaX@J}kCK~GUCW}$>SNdG;xJ)0WH*)Uq!c5lqj-*` za?S|TOFP=fl>xODEbiN@^6yAF2J6myvolJ`Y?~N<8y;z=`?eBlH^;jmRp7O++=QZh zsny-@Tt$th9=VC#cH~W#-9`*J3?DYVhTg#dkyW&PMn%Gwh zB&ff6Vk78k=-eVBoz?#+ifIuKF}EGH%&(!cx2#yj-O-R)9rf0Z)LDcMv2|sXsoXv# z*|T-be5q8h0wkyH9vV#EwGy>FkLUTk3d_k=6K2URto9$Ovx3!FCEJX*He0!I_6h|R zCqyekmHoyPRWHnEAp*{K%sACk>50|v4(LWe`XF1D%xP;pY{p5)X2tMht9^D7OIw^= zmF|@=C!l+}sl>(%4MxbeS_tFd0n+f z;QnhOao1!;y=lvkNW^C-(WCk^o{pt=-#pLNSzDtZ)}(o{vtEt&PO{NScF&V2uXxT? zANV@lQlV!7=?8d&z4*4BQg*fFeE$1t-BOF=nV5}`7293$1|v_EyGMnsyAM1#sFI!O zX9Y94HT}OshCQh_-%4&?b#HDNTeUoFUQ4nsZk+>2z(-l0B(3u-0%@+@lJ?e=joP=HoZ-M9y8LGP)!3G)Rl^={*B3%~O%jKh#(_4@6s z(50_Wtd?$`@Q;+{!YLd7nT&p)RI{idkyrigWorptjYD zd(!V0YUC~U`87ynHt_1c{Gs95t1i>bc=Sb)I=jn<#~nGH{%)gplIx#X5ZrdJKUPv( zj^ap1m`@&M36P|6t-k`@Ekv(MY`U{oc7vkfmQbJa8&qcnH>Lfy;qBR-V)P8soii3c zVQcP0i=!d9-eB-=ERUkj`xWT9m*{B}=-nuqIw@pFxS+;aik@paI%78`KB23H6^E|P zMg2inw$5WMld3FpIo!H@paNkC%}{YvK758tvuZcK6@N;Jb&FmNxbK&kn#`aaa|yYe z+wm5hLpznt+Zz2CW-WN;X^D=+jGsH44rwF$B5AiNJwZNeGI*=|Vixbo#lvcky!vsN z6)?GXY)y(XjMiV>=F&_iq4jaUs>*U6+=JYLW&}wUm3^JKs(w#G<}`rTl5v)!NaKDRe!1wOln}+Up;S9P z$y6y+s~!POUK3nmxHdg*zDv-H(4Tn*G&ctWG^YDm+8R+30p_S$t3q|ct&Pa=Vl(=- zx#C;0mYbBs$Jz0}E?Fq?Ky&TUu2PC^=4)*$JCFxr&AVG~?%OTfXT!`ueH0%B^RV-= z=6$40ouuYbm)kvgjWyw!XExdtb>oY`J3#8C;pjNR2wq) z*SSvoan{u*t;zZz8=-EIXK(tnUqi16wR6z}LiZ8hLIXq4XQoFjVD$d%)|? zp>iq?E3tE4nqpACxla&eb3f=g!}jZJzQFnXs3Fx}S{ucAQ@bi}YMZOv!S_;n<@u9* z&x-0WU8hx9<#USXmK^*xZ|BZ476q&3iyJ%NBv(ryuu^+BWJ>U7fSY7}gU*&uRoOx~ z;F?fN&R@MOcGYVTtx9E8fIi5U@pP|VAzwukMh7n1^!}YLWEb3{#`SV+HI?L zQh?-^Qh!rvJz?HPc@eR9fvH-xuY3fY5>6sSqZu~SRam)De>YEr0~f0x|4n?=P`mca z|E8*|jb>KDyR=(%wJHAUS6vONsMGaZO1u#B)o4z!=GL}!6l>=M^yZfJyd$gTwFE7u z-Z+q@kUMIZN9M6Jkj-p$D!LyFHbsuI?I!_;02xtnH1`dR$R1c#?(#`KT`N@ZIQ@+hJ6# zIis2(WoI@Dk`s&7`oP%PZY^NXuM4C;9$nxn$QwNzScUEaavQ*DOrTGSOWUEX2cfn1 zS|8oqg9!TB2`sEbOLD$-QPbm=-2Nr~ep^nX8J35|%rTy?pk?#>@fmSCG$V$t>yxcy z+r8a3WG6d^v%y?u_Tuau`?&b0l-u_&?^i$1uL$V3^1S|Fd4KDDtvL$AC}u4aiE4Sl z+Y*{CHSPz=Q+=7f%8WLlTtpC!N|sp@(p>3&HpN{(dZWs(6%jDqf2pMEqf6^nRzuD9 z3^=T$PCDu3%(_$|dd3BrLm5w1vLg$HpOdJM8B-J4FJ$>Dt4+6?_t+dOF-8lr{Hr^a zcv{8ey$qotb@G39U?wfn+s@#r7On022PN74d_1XC>B{?ThDKpDfNtVE!l$)5lVRPp z4rH~*0)1G!|5iyp*d`HUO`_&n8r^gL^Ke7vXMa>zt}7t>&0Ozu#6BX_Gq zneIc!&ydt$Mui?nsS3W$XVJQn!^CyA%=TcTDvpvk$4PQMJG=(jV-@$hbsn?zG3I)! z{v@LV{%i^2OJYlhp$LCVAS7myjg}~8% zjK|3LNvvK;7MUS_%!vb%%ah>I*(%7RYFLp+70GB&$wIsms18!G3KCghN%Jy=r_trH z)mI$D+S?9}Uf|*!a|ybc3xTJnr6&iVZ8E$;!YWYhL|MunBMI@7>w-)kJ2I+6byCLg zOc&#~!RtpPGp#$U?914!J&N;wR(s<#S=BQ1WRkCLpM9txGnjcuLB?jH(h!?_U(T;Q zYmsp|rTw^7Po!3~dNzyEFZw?BiWJ07uP^Zorm8~s26{RNL*n+a-tsXsUBXtMd)+I4 z_@64Uj>N=VQ#~PoR~Mp zdhEF12Y5WQK+Q#09d7`)Ce3fQl66krlDr|!-fllMsQGrZmJLs5)z=ZrP3@g6&&1r1 z=qz1r;-lZF6jB-Ct^P>~cjWTj*ICI!;tYb}aQk{uePQ!ljnqLhst3gu^nOW!w}9LG zP`vWB##f{@;ecqT%#1D7-juD6R&ojn*^j3Gbzrv>E8mexr<)4>lTrw73i2+M&$edT zC?`-SFY+7g{9(`B%Pq7mj8JmI%cwy+Yulv`6v4Sv`ougf%Qb?~xlzmu{Cr$p<*i{R%U3;@bvn$~*t35Y6fvbV+1x^ZW z+`4$sg=}US$&TbQ$IaKhTwT_KR~!kId-X)(GJft58&dK*^*yM>n(W#kvvR8JMw6GzSReESQh4!+?@U;U-8z4M1(wQTN3 z(q%vu`ro;K$#bv&{abfm@vFb}SD$(KXJ7oa^xHjY`^De4^3ufX79ISV*L?AD$@iUI z`Z|KsegBVr&!={OV`$%1uju*9i_d$(OTb6*i|O}I9^A2S)$a`*`nfj_e&*6YUwY)Q z0^UsZT%VkK>T_2=^VqXqdf_84_}brXx*uu1>i+D{f8@_s{M0*kzv3l7{|oQ=j{}2` zAoyKd-toSkufKlLp|8L8voHAY-@gBWA4tFdT?*%qzxSR?ztjEQ!`Hw3vAf=N?Jp?c z{nw@6f3g4ErJsDq>yF;{{F{E_{1@K-$q9!4mshO0>#x7_-dleEEf4#FnK%9UtN(!E zKmQLueA`QI`0+!Ze&f)mAARuids7(dfwcX9U%TVQzjNIqpLF-ze|-KKH$CCsFEHNT z{Nm5O`~BDb(7`V~@X7Np{`&2YfUMe&O~3cQ`{iTbIK1~@@2`F4iO)Lu$t4eC_z(W} zM<4&Xt~>Vs@(W+H>MN69edU|E{nam6@$8q~{6G7ye((cVzjyl&@15lKdj?KCph`p@0AAFYfuqdtUqeXY79ZXI?=2(skDpW(Iy}@T>bi z`n*N&|KRe!ebF-s|LWSF$1i*P?3a(;_TVROJ^SVB@7_+|dmfp*`i0N^y-)0Y$A{j2 z{GZ<>4<| zeCe(K#{2$vM^FFjl5hUi!M}O^qds}_H*N%#3hb}C=|_M4y)U`)5kpUU?rVSdI(1+T zWJ6A0p{|1u``Es}U%v0=6F>3w8(#RWe|hW-@8bhM^oWBWd*-UWKmWGtKK{V2Cm+~B z`_c+S^f^zyHFar<|#{QCzV{PZ>Zwm)~{KfK`euYD2d-G4j% z{2#jEzx|wl{K%_5e`?Pw-~Mag_{NJrv%H7T;Tu={rC7?OXr*nV0yS9DBj_Pahh5 z$&=pj)7L!g!|#35)n}=F*RQ{I$A^yeJm==IH-7sOEBaRSy^YVq_7zvW`*i=BgS%e! zU(fobO?{&`a{CYb$NR2($2T5z?7}PG@v}qk{rg)!MeVy@bnNp#@ZFPdKK%B#|JVNi zxc#?pe316H>%Beyv-`GZ@7(>G_dkA3?>)c$>i+cG%5?0eJif9RVt-*e*{5r$g`D4F!<9q+|<3kU9$9Eq0@IU+Zcm9y)^XU&i@(q9X zt+yX}@=G6>-S>$@BR@_1+jZw{UpV{JuPwP{%L|_JFSj14f4}qC(06Wr!t?&=TUYj`{>bQ?|9vhe&H9_{Lqyv_#7@e{={28^46iDwXgchUmknq z$JZa_`|{=EPZ;~1UslK7^5E7JSN_e`+y0uzeai!fF8x^dQ}@6AU0bjErHB0M2L~8V z&$IepIdI_ofp0x$==$dz*fRB}d~S!|`K1+ieDp{5Kl)eRa?P{ea>K-0#>3~YdiG1c zIsJ~KpLq85?-+UTho1EjzSsTNeeNl{{`BDozxKASw|wwl->E*r_vp~S^u6g%@A~{L z_doAte{;jX-Rm3_R<$uRiLB-*D_x zKRxz)fA{BK|COJk`E-Bck5-@f#sl5Qwmy6MA^-lEpM26qUe~rAKXK>e(KqaQ`SU({ z%L{gW{K>ocUhlkj??3GjFB&bZuYykl)#L{M(0q^c~OH|Lp(y zqkrgSy7%EPedV|2c6?-i-y5E`?)C3^$-5rlb8^=+Ry^w1XZ~~Fv)}o`GyNa??YCYa zymjB#{mla(Uh<-&uYT7TztaEGYZt$O;rwpTM?dq$Eg#;`+oh+haU6tAK5tlxh2=^Vfy)o?rqoZ zThV`H{cFY#@BiG(H}X587QgOIpZVH%KfZ7A`;zXyb4Qv)hb;PkQ&RR14h}lSMIRA}l&Wlc25Qq@+CnXk{3Z9! z+<=4_O(a{K#;Rdm^_2Ep`?5DnT;$$j0U&%McI+(s`)l*%kA9l$ZeOMb^~oHI($DFODHOQ1T2--~=ob56AY#e&i5rPg?k*YHB|{fwDe z4mYc_!93Me#kuRD=940i%6o(R=p{&WsVSUaM|%t8*Ec&1G13AN`3bCUUjuGl#g-!G zP)=V*Ar$O!U7Sfb1MA+%Z-Iq+Waj2}9nKNEM;UjW;|FHI2mJQKogx9mGWYR+cLz=d zq3pKJZV0%19JQs2!{BY2XvmMp?e-0;X#`y7+m}x%t!?(p^N!rC$8)g;0370HhfnOt z3eHIvVIP={br-jSiVTZ4NC_WqXT8s*Qnj4S{Et^lk%FH(=gBW--Pm_;fbu6dZ)Gm| zBKyvIbBXyGNqoC=z~@N=Zz+cTrkz}^U~^?k*WwDXzW`JB6QkO(kD?x`o3n-xr~jMU zPeN@3?>(c)p~lgP3JfJsF%-b!Ri4D`l4(6uCCRK0bq6T^h$lvpy$#3aTkW) zRUvzd{F~qvSR{8T`fiNdMf0D&9B{gd+vwgh4cd0&XeRt{dw275cmG# zPG=tb4d4MeO$JF5UDfs%3!SqVj-6^$q#I!7(0^cNvqnFYDkL>y9Y1YOJ zFCRP(aP(y0P~)-qRkPy#%w>son$DJ(K-Wf#cpPTC-9#aUQAEQ(aVs#U{my;SLhiXf zYB zZ1o;yQ+MBMVj;HHl`*q-{4x)oqx77W((>^(I#3co*)HJA^n89FHR_U6=H5OUc!ya2 za&+}^AHDZJq30 zCv?x+nk$}b(~=jdpcMb`@b=Z+qF)2HSLakZl6>j)jtcziKKb?RY>C^D4B#?at$^YF z5IlD4Z)UMOi)&&kT$Rnk_jWD}+~-@5zkQK?U+2eu;dK|j7tOb0o%+&7mFm6RDjq;{e(_tx8XlVp_7l2zyj<1?@2Pb#DN7J1KAVx{~kSz>*b=D#xn<_%oU%q1{MZ`}(LGm^-VFvsa_d^3W zmjWh3u{bAhrAM`q!F~-I*-ewptsN|euMsm#MxZG>frP{6P2#_^HE$Fk14>FUpGFV& zPq&D$LH3TH6d*TpC(tMSG8N%9k1za?DBj}AeX}pGmN)ZuJ+8X%=>jgEYy9I=lF0+{ zvj3T}!)vcIL>4yxM-#dpG1euY$>vCD{AU5?i~SM(NV{P6lnsg-$;XsUM&DIM_tt(= znhxkCYH6MByazgsE+(W{p(Ihj_i_(mOp>Pd-8Z&R;`ZJXUXi2-*5P<&=|f<&_~Qm zeGWp=i!VOMEBnomsXB@^~Wv&EO-Z#V+0%9fqrzwek z_Xm8Qthx1|L`=-`C*f!*xw{r%YR!5M>o6nvd+TLV6Z6BJo8QXINSW)!=d0BVAvE04 z>G^SEfr0EzDZk%c@4^#=4Snvt){pkzjeuKxFJP&+*VHA8-5<=XZoOur)=sug1Iul^ za@l1<0Y>+%rn{>+C|KZ=H71Yi66it`{qwYxX{@;=YJD~LB(&;tk3+MgYgCUHJga+q zRmU5&F>}bBL>oe|&V6`eZ!ny5A+vrrc)bbjyLF0@uao)OpE3M~KrFM|A10V1;D0?! z4rs43vm5RD>76_wgQ&uQA#Ls`SDB&cz7!-8e9&-zP+Gess;$BYxdqR6 zmup@zT|@&x>-M?=K2dO2TX&bp*tFF8FLpJt%_{!EwOzNZnq9N*Y{j74`wbar-RZp= zP>}8B(g`PjT=TNxjR(4CU)GtJ@&hC?WOdt+6#+q~&vgN!VP#zYp4)p16cI-fXPkrv z%u{oMK4Z1(GL0zI&yc)!l9?fRsjQvp4mpND6 zKoi7t>^O}Xt`*Q)Yv1{pFsS3p!n%#)WA+Fm2e^b+*H;Cq2*1&3uK3)W&73BheP~tB ziMrYAc75Dz^JwUgT_?vAq_`GFl=l$nd2{s@*|OiX7>rkic$@v;ui1l0>EPr%=6~Gp zV!dhS0et3OTlhBF*sho68Ckgb9OqygzV%)uf^I6F4dttUl0yg(+n_g-y-uAbxMbOO z)q;}C>p@7mpI^|Hq7BE! zfpwj+f7*VPax#c;K82xEW}SG6w)S%0ea+R*jp^~jv74YRLuhAV>rsBK&;Q)cmV-^_4t%1{#9?rvKc?IT|Z`hjz1VJ|soGxWES9vx1+(TGOJRPLrUo??;}|fh|oY)LG8Yk+OL7Y^JvTWA&_YJ%3s_5wk0>r zCVjBTZPEio8FFQIk{?7hJ~ducB~64{XL4=TcKN|a)&aa$V<3C9zs-7EDy%=DJLdb? z$?3RIe9L(NKJK8{0IIX8?3Fsy_|6^pkN(;R_lLz}`wM>VC2CNAG+2I0$jeWsqw1M^ zWnVlk^R>I5yw7RNuRV(n<^(o6Qw}%q-Xv^-wJF#cWMwkbJy{FA#R@EPUf!F&znKzQ zSub*{`MED9^-4Y-efMFyNy&Pn;cs95a^;iS8|}4M*y67Lj5|N5wCuzrj``yRh$qHl zS+$Nd2W4p}&Ukg|ARrO?a?!uf<~Me`tX{(?d4@a=#n`vYy|UB&+kFg;azYwJv4xGyLz5#Gh0c0TB3jHPM-g`oN;ZOFMtz_aRyBZ-T>t9cgUa4_fA@Z zmvbChx<`M2cfF)AP33{v>=7mI^C=NEV zAw;p}9M;}Z6$$CdDf->*_6sP$^QYp=506u&2~M(S*R97HAN_I}rKLKX3}B?!kKjui zp5Y(}U<=T`$p6mu%=$gC=3`k-29(`S+px}B>L@kyw~BSk!(yAh;fmyCXekz`>Df~4 z{ai}}tez_8JHEHi(5> z4X96}(68sLx;&}c@_$J-@O;i4PHSO&m+o8x692l9s&prvyS?sv@)vN}rccVT*$pKI z!h={Ox-@4qb-e$60jV0$k`esiXBCi5v{Gf|?G&;CgOmDB)1UMLY z3krrLyFCsAD%jO!ZP%4}78<#h%9MuzuizPbS;bNCE~fWQ8?L}1eC952S4sps0n%=- zEA|G1FK^N8jK`&lIGHbJ^$bH*&m#w(pUOuXT5Nb(2R6e zL_5u(0zdc1$%^$(GQw&)BXvK*P{rwt7L%;1Zp)I?)P5og5fBKaFLDi?J zFZ#zxY!5Q;RApyZX5Uq<`NpV3%xu>ZG_6{>SJh^5Z=yfU0>6jZw);3lPqLyw!-?l+ zH)G*l*~jNqP8d=8NJpc5aZx9ct#9gX<@^5ULFL&;g?oBSv+HxqKV+Rb$9v>0^Sz{H z8q3?JL@on+Q1LL$d+4Mxhd`|kapM>H=k-Q#n*Pe!Xi;b_8)}&71OKT- zO=>RUrgTw~xXd^17PghJq962nU?ep4Eb6cK&@wyxclYReJ=Up$uQqYk8;Og6~9yI@C}s_M6uYzg-N@h7em(aV}jrW{`}75 z^}QaiJ5P+=b?}WWlAgf_peG-C=Zes>y>4#*&ApTQLlNZY756qM(ZyNZ{4F|G-?c>K zX6CZ(({StfCBEC%uFXTW{*rW$I;&lQ=fS62ajwjGN#ap%7EB&1`a_*zN7mc6V~)uLq*SYQ45@y{kVn+m%%CUz0wnw znzhT=CB$FPP}kT{D8CyZg!UdX;`;!>b437(_Mfi0`G*I zFbk?~h&R@eG=%rhbl{%T-+~yCD$`deU~`un27lf`P*Jv)jWU7@Pbw7ol`pP*V2od^ zHjogWsy2o~iTQ~%V?y{@yHJeV_bAelfO1I@{1NO>fpS@Rr1W+wd8WmsKax;7J!N7< zAqDw#PZ~M^?e&-sYpFjwU1MaVsIQN{?>mel!FiodWQireG>l^`IQzsFY=2i}b$Ldp zrKuCP{I7C2qY^+pz`4}S8q9l*WvN_ATgMD=PXs57tJPys-z6qm;_g2@)eV7B+#=QE zCEbz@zoHK2g*CGG4JNHvzN_nc;z0Y!0ixHlqXA` zTtcWt!zG3;Gi#(xVwm&)y0lA5<6moiNi)(4b`8GwdFUR6(HCQ zP}k^UYg7LB(2!sZxZSx)S?mpivMd)IPQpy8%R5LkTHC(Y?BH|>V)zrp>qEuWpzlDK zFid;k7IJgjQ6=()AQ0p<*UhFeu zUfCoO8=x2OZy`vHZDE+`@f>6e)SyC7H7ordO}jl1^QqqYuSzat&f$;d4DO>qoC3|i zp9*pgsL7!;cWwnA7Zyq&LIvztl>Yu`0ut}=!8K)Y74IYrq3ksh56dz0nPFnAYbq^a zX?{r2D;R(B<9RUn3U!>=9M65jFDOuhTrz1j>j6_n4^SV)ix`18S_4i^h6=}oc49Pi zxN0)G8~3f7cwR#C8`FIy<-ctN`|8TjkWGmULS#fpvq^UUcV6pwP2TTLN{ z1H-XpzMiq-O03lcb}{CVD+Zsnc&VhF68PfioY+x5g#fHGKvy|yfK2?}Fit#2EkRw! zFkQO$v@7^_)cHI9SUn=tz@+6*!_Z8xSxIqUNH~)(2W52qMox1rdD0d;go zHehfbMgjkvux6!_2NThmut%jRP&t-`G>0@gNwqk&O5jsm&tTBE@z}fDM{9p<81oG|zhRwL6t;L6hs|!+Zi(>9aYD!e=HMyZagshgW$`5NCi5dIx1IJ;!gupm z`W2a44M(_%okgyrDn@04HqMn+ZJ|nvOQB65v0pLx?eY4OVb>}YTTrbO!N~KmSWk8z zd;i|#3#1N#1-X)?aKS(eEi^IK3BCptAEM{?)jWHNyRO#2t5?;d)mQdXG^-7z)GV!wIV=fwI9fd8hxxmX3cR%2j12u| zaH*hGPZ8c4m6G5DOc}MIZO}nejpv*_&W9~s1;L>___%R~q)lj! zB~xPIl8rq>z9Z+im1g<^fiV*lN=Ay@?Od!(Tnn^(E2Q+|6ee{#Keq%56mHAsGN+^1 z%tf&xBZNsB^4G`C|LIbsMYD=kA`|pcuXJ#2_>$4Zk&!6u3f+f{cQc^>J}sb1gi-D6w#fW-Z^fGkAPQ&mWBN~V}3Yrz{UZ~mFAw>A@ zH%vxfZ2c+sPb zkmv?%TR3qz6UdPh?~YtB%gcz(KfX1wLM@|;=wJx>+Dh!E_iZDeb|N>;UI?j@-8>-4 z#P+m)I4B6hup%5G{N^TeK$<(GevY&@2i*5E!=PK{5A?D5YgaSKMGgUChSo{T^LXv> zZzUzyqF*<@i~bpUiC>w5diuT17?3Q&E^b84jME?Q0WSfZZC2W#4fS7Y1Jl+33#A!! zqlgwgAZNZ*_6v>%(=}ik(qXy?VT?lTIZ)!S#j&q#0FZKJN0$;`!VhN!f%AUWm30Cn!_D7+;TBNJUWJtjLEF>obSQrieAb9FW-edfxn#o8ndYS0G>y|%=Ije<% z6yH*H%^N6pO!kdMJD%xB<`gHis16wul=9U#$1C#dkRRj2Yk1){kxWYJxkeMS;kjjI!RNLANe7d$%oY;iB=tGAt^l85srCt^YDFy^UY9FZ*S2i2}3 zn&|xV3a-b=vp4Ph)8jA#^Q<&R$q+TzLS-^6$pL4uRgf)#TXP%+lEBpt9@YDaN9~}PzrZU*vq_Xo6{)a@!?2?Z56|WHQ4h1M3|+-1(-&*Owszwa-n|oU zu`?#0Z2^#(n(0(eG#SGkRy#u~^K9iiTFrLR^WfBp(x63QAn~9cpquxz`DlQ5!H3wB zpQ~lY8`d1mq&koe$7vXHlSooC>;&jUdL4j)^4H>Pze+i1G6bl9m0 z>RTaI*8FRH1{-~yzDLs})#_L0wm-*g{$oJhL73+32R4(G&Q6OFrCc2wp^m7e$RDOPM;uFCTbVdGSn#J zrI7Cvsx0rwP~RZ6M5a-RU{}N9ZkHlo4?;h7oSZFHVVGU&ooFV(t;#gtTYhV*$PXRr z3H2tn(?-RA-1>@S*}Z82E5!;u*MHb81HqfLZ`uD=K84>IEtl#tNiShrm1PAeZ~2$@ zEaVie?=F~DP>kF#F2yqSVSN8~J(>aY53mS{&NK*)l2?J2WV+2g=p8%tJm9WFyLrZ) zgB@EdNzWL`KEhZEI3Bzw(WE0V*C%6|m4hsMG*e2CC(OMKbU3gPw#K9l_J+Sr z0VB;M2_6PJf$nrwnWfY44KKDP5g#-R6gDnOQmaz__^IfLl*1YDSiiTjzz63W1-X|k zw77!YLmO8o{P#;D&u=@YuF8oOJXgciSlHG>n2J?GTfUE|!Nw~W&D~o_qbH8RxRu}% z*OF+;DJp+W=nS3K#)AHnvvztw&e0(Nv^F5*IB)aFju(?a-Q2BiCg^y zjE$qD1h^W!3N@ntj4#w3sjUw&IY@IG?ZX2WZB=qw2>N0|RS z=-=}(wl7Wu_!c7i?_T2@oOz~@*h@lmw+=(R>@lPW?Izuu{NvZgi^!7P5e(QwcAIrL zfM*j=qj;CW4Sk}5ZEl`-K8Nj(dX97W*-*?sm2#)OfxbeEBSbRu=iiT|2*7_5wZHngRgN&UP^G2`sc+tkxR5;#jHHo zPvz|GMYgplWLkb(DtfvZ3-S|OMaVr4CgICfs@>|i`iu9$=9%{24jQ?_sAHGYKo2Dp zi<)*CpBDssqChu6P6X|a6=;a7dUxpM7KPEV>FWNjn4p_s)%_hIBV}gBz%oNlDv)d4 zgI=81I3jWys9+_I*7wp!`>%}*cE^@9lROw2eg{>=BPyy`#q%BB z`Za>WUlkWBl_iN#!hwgxdVNg$$Cy36PP>nu3j?*AH7|rV4?Ix4HuO@->aXOyep6 zeM)uW6L{HB6jWR@;bBt}C3d#RIi}d?dn1180$egrMC1L_2;}P8Drx@usy|xG0~I#L zOYF>N>8Y;rV=YyGo{yQKsb5?Yw**#TI1!H3sj{|aY79PWLV?w?zH)2eZi-_K-^B=F z2Y8$X!}p6493(Sw52uGb2wLB0jvq6d12;ylbqH^ov~BU*75_FFY{{srmtFsy{4qQO zmML+M;8gPzriY}Qg zF^P_X;h;sfi2pcHA4FXw+2F)|{w0!ih4HX*(;yNyT7d;Lr!*!kXVK5E?Eom#aaPgR zP}XTZeHdm@hCv@)Vx9as3B z`G{b5y^|_f2fC&I8krxAT~jS97*-}Mm-s9jXgPL_@R(Xa4uVr}0yH@OY>r7Z183`! z+55>OF1JUCdM=M<*A!iv34Xjm(*3sE2!nk=KlU1MPcscFQsyvZbeQQ(VF)HMo)s1m zwq@h$T$6nrJLGzeBtmV2sh>|hEw~9k-1T??;WCYdI8Q@v|Lt<=CgADP6@% z2E-C`7Ig89s)GpLEATHenlNP07B%>!6-c)!F7J-@a0uLi*%RgaZmNwnSXo{8kwBG&9x;SiI2KZ1U?9SE;kzQpM_v( zi083%LH(Q2ml!HvK)44f;#Q-vXR@J8TiX>Y;U#kp2{|b@{}Rn6 zqiyA<5NUK7p=4>4F}0}?{6e_uQn2EhbBF6XOgjYNRJ^Y7E9YV5!$>WRMFH#T3#$>! zv5%7rP=AEGSdDe9zD%ywB|sPVVfZo>_VtM;;r}P zxW(OU1JvhcG=lgwnVVsN5FRquy){3FYgFizxqrxn4;2W4IXrGaWdBvN`}UjvC34_m z9IHx7$!|%=YKvMZn-RI_9cpxRJq7`CaJ~S7g^Ud+w%j8Z2HGlAiO;u0jvn__HC~E? z$kR?Meqj#237Qa%;24z6wgClyAfxV2iFIOb*zvYUM7bCL7cNAKZi5;A6lNM6U^s%f z?(f`#g}grF#@8@LKB6a~E0X;X!n#=txAA|YSv zl3X{O_8}wO|0T%vWJjnKQ-*)+y@ey+>FWTnF=V}(|x}I|$G4br#SCE4l zdPVMVj{2i#su{|2K10sZYpi=(14lE4xJYu7M4pu)geF!t1SeBHPv0DUHSmn)d11SI zYW;^UD7+ONbxv+GwMU=&b{pm%S{qri)2V0_=r$=T={J*U?-0MwS{k)PTS%6UHD(ko z_ccCiDOchp%g@^FXunrbYgP^x9k)0za1@z0sD5z%^pypTEf2VIVSlJ^AO<`lKM|*E z>kDGfn9M(}-O(YuZ40#02w6QB<#KMI|FN3$!;wG}2)m&}zLgSrfz$FMolh?+;V-eI zz@bwM1nWus<`iB%iRy>un6b}Ko%M8h9#lm(7RiZy4wbT{o5|&PGzIj>2F*f26cbZ| z^E&Q^5oh!3e5=98^wZ6TSz2}<2zNb`0ZR^?S4?Wa3YeM66P5jXV)cU2WvL0>IBu>t zw8Y$mP9w|U8uaFq*jNQilzlN(1rU@nl6k(e%No*8-o}29z;bOk0+0x zOW>#Yybs}5DMuByFy<}l9LgJ&rQ?Sb>uYN;<^!j{q)s>s(ltu(idSTrB>y-k^0o`+ zol``mai{%w(7TSc!ONK!JP$}RK8<6u{Y?nFF#3bqk=o2uQKyNSJmhyDP9D^ntPv~c z1^$h_&@jNe`<%H$J3}e}P*$$}h*eox0767Q0k@1=iB(8=yhu~6&GSDFR-;M}Akq03 zmMA?R2QR*ViNYLfB@*enflX3URBa8@O?Hmb7{VtX@~kehvgr*ZyB8KX`|e7ci%nD< zdR*SJgNy+0_E(6LR8f)CFF@%^iPSF;C7V9D7-q-n)v0(E`lTg@bYL3$RGU8d>DW8k zXeT!Ko%js>R`XevKNh4g;v8RI{Civ^TQGz#@Gp*#M|D65*{TWzV>+BjOPfH%Vnr?5 zEfA)hnFCZVObp z1P<=xFVYSnr30Ox9+|pVzN;u(6`c2S^~-OH9LDKEBA?`2P!2ak-4P~ zypbg7ImNy^>h6QciUABXTtt+xh)>OS2y3RzhgQoCbECtNiP3CmY(dDEF8D6luVKtP1y@$E}shBLLyd+;z;zWitbMn79;qNN6Oe_wQbMIP>P0DO3qM{$>~ zuEz~4e)KVl*fel~z4(q(zPi7k&Wp%D1XX{!nXNA*F>T0nHymAe0Xl$zMpV9M5jxh0 zUnJ;$!2MbgcGhWSfa>&|PR^F|^Nh>-FB~isLtKVn3)r1j5f5J5T(zoeE!GBtX?B&9I~!V=zy+bUwHl)*08JOrME&WbE}K1~H?)bHbO+9pw?rVF!}Kwzsf z@#2LcHPIun5jE^MvG(4ZBxe-eK7TLbr$>(xZxkbP>aZxLsQ7TIm5fsL4nx9&Afr#4 zkk?HxKye^d^w}k3MW#>FB}_0pU)uzI@ffO+ed-Lh_TtY;1>yu@?8$tTVxXc}5TI0% z899nMu-YR^abIu~ac>@Tp0TeOI{QZY`DYu$dpF`wqamu^VyYoExFPC%Nw$K78#&77 zs9_uOu{{y~B`WaU(k=#lDm@av_4FI79t*zY1&X__(djq*<}cHAKzldfLESP0r9SW)w;`3cwb)+y}Y zimc08ZO?4ZS%s1R319ykhE1pZIirVv7JD~@TiO>s%f(O9A5!Hu|5miVlx&Zo|N0zZS%UwZ#=ug7sqk}+ z=Iw^5inh~w$s;W5bHjT!e{idK2gnSWy2Bewe$yZv+I)8YC|e_v9|Q`VlDBMe6c(!4 z3-xWRBH(qg5TvEM|3@}~{@R>Xb=%>(qjTg^w#VV1FyoafS>urpUoE(2>X#t<>29tw zmCEJ|7zfkx67zt=@z|Z5$#Tx3QsmOfTZh&1+W%F|`HnvctH;}H*GTTo(UDFodsX*< zJf1x~1j(`9ReQM-ZD%EKcJjaaE4^eQSHC3jf?2IDj<9#4MW5ykd*J9?(Ek?7SqOf; z688wZ#13ASn1m6)kH4~@+6{!g*J9zim$%+;t8?M%V1SntRj+5|}aG@jQE ztAf*L(|^^w7z3?aN|YO$<~a@U`n>u%lJKrb5v`5o(67#x`g#cVZW(D=fKsSvYEujE z7nfkwS0_{nFdzT+e+{#(In+n+&P zUX)iNDklnmqvW*@=9+Kh<%HbW_F;NuGWQSuJ(D%@D|yQ@xXVj#C5CsJ9JG`Zu}da) z5Wf2aTT?R9s?bo*-@t#_Q>US0-Ryvk;G0qio0v-`tzCeH$1k`DVK^5a1}4>hl{{*& zkn+6iZ*W*lRtBs4ad^BS(na6;B{q~bL%ufIxs)9P!OWRS-yga`Q|&^bwRv1QYZym= zFXs|0Un5UIRsIB#WLl|e-@dWU?q4~Ory`-m#6Ia!D>mr!y%WLw#O+?M$_dcaH_oU| ztdeBT7?HN>xS!~lDp$&fc{tKXo`a|Xmv5N$y~W#c%a6Gj(e=*Oc18_umJQ=_JN4nK z^({{*lN+N*@d5MZ#`bep4ow4P z0Z9jzzLsI*-C;*hA|b`O1UPmVu-%aBDV@sSobwm;{iLRH2uCFc56})V>t4>15bcE4=oIW}s zV*xQ6xR_C=+>AM-RjN_AS)5(QhXoXe;V*g8$|B$IWWJPLz4*g?H1SG$HCUfp6R5~m zf{>T~Ys^Awa}&d-%IG7Wo1jnyvKpQO{{GR?2JUu${8JQ883l$Bug>B+z)25+GG=mOnVpXp#5+e> z&IQKCUf}kA{mM%i#HtZX;BP+^=#59VFK(qd64|h&`}^;U=It-0kSgZ^FC0BrIT&vS z5Iem0WS@S0%`*M@f5R;n#&b0e@{GU`gw{kFvk97;RH{vkgy$k^^rSp5SzsI;kXgPF z@*utlKBL*tbn!c#cN?A|i(}r3zcCWVAhb-4$U6n;qU3&FkNVt|juiRzkptD%ZD2CJm&ek(B_>Ntm{p;QM>gpyV%1?-fx!lkT2<;4a$ znTZMXZx^qHq^9i1>!>rMe7;(cTbZVEpmkZu>N7qw2CkHr?cO78WBIXbyr?}wMFBNC zMhIGxxyRqJ2@&&Zv=YT03Lom`UiNjvc*tAiREU4WTCI+9=HY)09{%kneuiR(hog+V zQ-zti46-6%xP7O!RL7mBC@pU4n9(_}Az&5rle~$>jx!E;-oO{@z!la;E zETt$*P%YQCGEvPx^+27BD5Dv`v{z%ZOLkU0vbMiafO4e+PM3># zl2`OW(W(~w!9#w$6vX>&#zmNJPeRn-)GCf*g+A44M49gA^e)CNY zR_pyd89@JnmXt%$fp=L7rd>!0t7+p~Y)Fs*Z>)4BEgV`!+i^FPh*$thj3np>4C+W@ z@{d3hv;NBS1x#CtMRb15e8rd1k8z#lH?1o+NBgsDM`>a*e^=Rncaz_5bMA)X%kIb> z$wze@pg-#lqU(40E_4urc+V8oL(~qvccGSraF)lw|A*KgjMtQ{gp|&gO~iAQiZS;n zKS`Y4Bp;36wXh_|Nc2<1Mr30O`Qy^z)rIJF9SP&KpqW|ol0~5)x$=IYijo{r;=9+S z&RpFTG`v#}J@9UwKc+3F%^B6R;H@#Y?SRV?&Z=ti93OjqNioJD(|&J-n(B51{}&>5 zAt|f5+D@J;ig_*|pg#VFeZK_Bk~kLha$I-zrz?`v|M_~SLUz$L0LL}RYNbR6Ldd6@ zdHi|b0XH1?AE^Wr;kL`gpYNDE&jIoit)t`fb2X1tCO_Y^@At#D)!@&NxIg9-J>kKc z&?9}sm!2{otP++*_Ba&B30F%ubAk|QKueFO&w4p_;J!};iuN_7OOinF)=|#;obANf zM}mQ2uW=LjrystAs14WekIJImtLylN3HH>889sE-H;Hx}PoaY5&i3@S98cyPPj0e; zbEiwC!;S7H&i3l^v3$2Hjd&T}&Vbj|0bVVujSZb5!%M*2B9P?9SVI6u8`2adaqurF!^V)V@CHi1vv@0tS@*_#jMC)UOSiSKYnrekFy; zKLsXrExxAwf8pML!vDNmRZqV?|Fs&1z~teUK7v~MWap@Wle2vi!r@kGJbM*SF8J{w zA8I)_P%Ph`QArtL{^~hOZWk1tt_uBebN8RJVAKnQZL#rb8if-ZpgFHuUNT zQ$Ms7{_&O@sOKq?N}4og+-;4$eXH!in`tYeNVuavL}zlkZcbJizXOr>7>%YIO->cnGN&7eu>0psLGzEj;&aZCs1hAoikKG)REQL#B~trAsp{}bp zi?iDd(C4kQU>-eEy{Ee>XK+$U0$BnQjy*3<%zg0}(4R0xEI-97xLq)0n37yQ&%WI> zoAz?VZmgmIru5l9{kK}+eTvRo)j2%|LDa+!d^ZhYk17|RkE3)aY$4BGOsAe*6JBD6 zP`LhO$XLuA{HAy9-Yn3Ek{5)o^LA#RRJO*^y6kzhG8~Ddz}-M-g=s)y<#BXox^J*0 zv1_yim9c2NY1rgG|CQX4+lOKLT8Q1EFMu(rkg9L2vt0iMo1|3jx|Fp@p`%(osJnl* zr@6pK$n^a0t$kwzY z8)fmcBEPzVFGeRc^>U#Lb!}UnS3P_4$h>>thCioA8D~i3rn}|MirZh~QGL@Y!W5UT*RQ0#~uf!P9)Qr=Y#* zlb#&%|2CiUKP`?@Px?NA5W$xkfg>XH*8dSt@>xst8yJUCf~kfK)qv$WT0lm8@jVjV zAOS)(+rBTO;!BC5f6LOH zX*-U9&Q3MQ4?TLjdoN#RSBMJCyeo&LgjT z-hf_nZ;Rl`pC17?J4%-mt>EGm7Alx4&Y-wa(C-g;qZM_I-w=LbgH zJhJD{{u~slDcX-WT9dLQNsfHRtg#(*0WA$&58DC2%wn}Nj}5v@THT`z12^|#v1Vnn z?|NBdDv`x)Q=928C-|m`ELFV{iqr=7&=GI40wy((S);U&Vj)IYkwBWKFK1FW{N)KK zSl=`JM7Y#pp3s6Xd~tIS1#d*MDw!caoL>TkMm*pLKHo6**FM7+Qac9I^D7VC|tBOO~wNPj#Try#@a* z1~MaQEz>TFzw_%JG;At|>K}L4i@@$AWliB`{m}bTJqnMJbObMY!rl;uI8c$EPsSIc zH_2u(`ok2%V;M@oytLm`of?PKZF2jJn|D;0$&z*inPQgcy2JSb@ztA`+ZLvP3e)ga z&Ne5l-T?%$MtFzPe3+hS{u3I5CVQmJB$GXdSOY6#o~ZtV%he+&TA3oBTwY2-4|Upm z2ne*VhESNf^%+j^I(wq9%#gh}j(w`%cMjOo#x4;PG=v*u1Ox~^0@Ss;M(F?8=MjNk z>0Go8?Hbjm7D?SnfZA+DWLIk@<-a^Bi{lLvaU>d@aG0&Nob)M=XOqEe1rF>gbS z@J;iWhH#JefLbw9)?8A@b9>Kqk0nz9to`qvM~{R)Zra;#v~Yyf zmAyYlY;3wmfdpHwTYt|3u0-=^BS(2C5C#3*|MC9V`Q>!u!s^JuQ8uW`bw6mNY+)4Z zC$v1~|M3yb;D>4_+eusb!cv{gu0J-jb}{p?2!x{dLiybK-9$$x-{rqN9yjOZEq5ic z{I~xSK)muaQ!H31_hUR~hY1el2prM|gZ;9vW!c33-(#CjY5m;-QMWsMy_;~xpdtk# zz9S%jU1$VN^qM)&kE5sBLcr)BJDtVm3En-m)GV_4a9YKNd znJ4{RM`M8&P2bQDekd>Z;&d6`hl%a%K6b-Zk74DkYT`uZ} zN+uH@F>lm3u<+Cn%aH zJo4AK{RRe1afb;=>eQc&gCY+SB32=~`zX~qrPfBs#YQw`LHQW$SdkHN8x4j@WUc!R zzNjH;=Mi*MQlur{%48p{gQR(p_Y`4Wjkd#Z6AKI>AgT?j^*f&pak^HzQT_doB{)J)g(o}TXNsh+95*IrBT z8pLK6INeO#eE;I+C7`37uqJ9HinndzM_aVsmu1wwPreP?P0G6p?&_`k%+#WaJMRl+7$O2T&1@1FB;&kQZ z7~=C&DrMt0cI$H?Zku-3pE>%P-D>x}aayNf)C7}R^gaKmWwVJ^#h=T%ot!Tk;?&>q zz^4@7PS2OC?j``TZ_)NjQI{V&yECj84XY}2^lSp~`SC0X$oNP@32bGFNm!4(?Q0iu ztqb`L`uB`JQDYJvaOxLfd&Jp}C!)CwMt=vKBh6FpPgoIK?Yb=_2l2HG;_|Lxe!|^5PzMDtj15D@c1FNiO=QNM z%2IOZ)i8%u*e0+3x!l}$PQuG8UeNYg(XeeHd3-3H3HY|elAz~1-8$!SJTy=&JI7e9 zS-GZGEo2&y?%ohG&u?Wkpjs>mEg;!?PH-jQKcklzieNzd7Ir-#zof2ySk~4FnCw@k z0;k}5h`+3BXATz{ovv$Jp+(kO+gF7wFE<5TSd`;@_2-<{E{m|Nj6+T&t=;~pX^d4FYXSsXs=Fx9AWT*+f#n3W$y_c3w$p zMtOG<%B(HSCPjo1Tj?uDCW(Nb4ui*=7fD$X)f64pJ*oGMjLN$OL^Umk4I)}si#W7{&;IH;W{)GTdH6jRtij!Uzn)C{ujyZ4%$}o3Nyi zU8Xl`8x8k|x39c?Ljuwoxl+dNy7cJ0yAHeaacqD5wSlUe(Z~)rI*OQ+Y=nW{I6l~) zK1WX$AeAsaKnZpT@#ih6Eyu#)T>`mAKYJeMD}KP^xRKYg(iFGlZYG768{W%vZ4Bp2 ze$YeNv$j|zB@Q?zrYTGV$;;$H?E5a%t^ZC9o4BD<>IkPm{io5bom3mqkx+)-`QioB zCz;c(R)ooas#A(0rmmWKIq5RmNZkZSqI*mJy*%bY`|n-bE06jQAtXuoRII`(aA$=lg#m16#iuQdgr;k{3tRc$%{B| zQnDQ5{!fRm)@o;K!q`aP+9EqyKCjD;zdUrKead$Kn{HzApJBg2*fiXXo&98X(-68K9mWSBd6ZK)?7jVS|^z-|6|>CwnW^VQv3u1r)l7H($;@; zQ2kw9{#<%jL5&qPb=$l=#4?#{+CQyWvlXx8tSw1wnV-k-;-_xCrBMDeARgz7I0^1(UMOsK zl5!jTwU1egeGsaUm)6*p7pT78X9)xGGkg~s>ZppN2}XNfq*H z_6e)M42^3b}WJ)zdR% zRtUNViPoC5=ZQ~$ShsIBZrGe*n$*7HT@5vZhkl8%rP}>}F(LOXa63%=9$inL{Xk!t zh(7agY{ISSoSQCcsxYov0LslA%-x{68P@HKWxx%qMNnFv6-^*+P;El&mQd(2jGhl6 z&d6I49P(~J>xGq7OBXVX#{Yr}DO{{lh_Sc|-Vt%HC>FtUJ6DJ6e{o+f&XMX!NCNjQ z0++8aQt~={uGqWBkM`REyo#y#tewA&iWw$o@IDDv^<>r|wkAv6&jXs?Jzt)6bX&@J zrVg8iszO@&Y^grVEqYhiETj8s6E^?HCER0+-TC)KsKlLWd!gZeDdwER)3nWs5*7xs zHWIC`A@Ew0umhKMhBD=8$^1XS5RtquVF&a?<=P6TDL?InT;5kq!obxS8qqxAm{gQ_ z_S1YOB*bavfzd`9*4yyN9#k}8T2|^sypk9ou^yDq7EpAI@DScAKF%35ZyQj7OZq|7 zWOKMZZaxr6K56-Ny+{~KAq|X#9~QR3K6||yE2>HI*r8AjlCZqkpZLB~D9A8_BQpwY z0sZIf0WSc(#-Prh1nGV1y%?dixhz8%;(i=jdS6+7*zZ;$4Ex}F0DV{zJrrZ|DX?z= z6Ii#tSnO-xJZB6YJo=ar%HZ&5hWW%??S2i?Gav0|N5A{su&Qd}2|~{{eoGQP>b8D- zK{?2rDf$u^*u0Kx#I@|yDL~KqO)Agi^RH(?<`uM0f`C0d41*s!HyAlk*Ter`ln7$T z6ul1ibAWj2jo9@`m|c=TL-;Y$ve_9e7{7{(XmwCSW&pJG&3#?2@@fuPxlo&Z<&NfjaK>^a$69M!ws z(0$sE?z|*sFa@&RA6(?LYdfed-|l<%su)DF!KaA*-wO1b^4IG&0Vk+C&A3`9@f43p z_y_(933`=GhuG^k?78;Y^oEDOCpAmb&ODL+FGA!$V3C-Az#{I?o@p&p^}aF#2*tVr z&!)JJ1#{!Bl*5uej%!$VG8C>-LigueK31bHY4KbkY(Tr4`!T}V&iTXc zfBuZQYu&wrkE?FqX1VYFlZ5unSz2C7$A4Xd+%99_BJJ;^0{4ej9;Iw= zj(wtyIEFdYYMLTflY8B27p}{c1pAL7gAOi-u#p-j)nI*8txOc`IWeT$L}iPbDA>-@zQbQST9&6~p zyCayzdLmPo3lK(-*F~R4VZ&^VJwttN{)a~dTx@aOe;RvxbikIU%w+-nsCpEcSgU##@-D3)DB8yuXAt{2Csq-#acMbw(1?6QKY(vjQ^MAmU~O1NG?KCgZY*~)$q`gS ztq}V7xT;H_0)z{#SWjxTKHXo@) zox`@%0OMWJ3o_Nfyyl19E5U1#geZuGarBeVA!c~ zFzP>*To};yB1(skX&qjVmN{^-Uq`)t7{9XVG*=JFK&|;q{<5lk#6FjO`}FSqSas;~ zx%aEqaXeK&>uDFNY!G^f5(gj@W`^n)Ofs-Dv6ih2M~GONL!Z^9(Z6 z@(y(&3z8Gh_b9mpkn%Eva1XNtU}~ zECg7H+&m4eFM6zz&&tY=8RDsp-^na1sl5?H4xI1}91QW75pZE z+V7{e35=fxs`|Y9*FwBC9c=QUrDQnckuN*is^7mYW{|;<%Sdm*xIczL8Ro_~;t3z% z!3Lga^ujOT_TnjObZUA6x&jB=in0T(=$`~S z){14XhCGuC-dQ%wY3*2c0VO@J&0L6DIL^Kz0Z)rzJU8AQm(l!<&Zi+Bkw8ZU6&Z zB*dHX&9N_z%GM@ik@$5Y=BmumRu7S{4FQfA_(a@w!Te7aPqWzjj&Wb;!5^% z7a(ad#{0;H*x(pk^ABW%99Wyk9a+xP+_MB~K4)pw{GYHKqlnYd_H~S<$D(8=s)+6R zBb76Ingnrkm zo|PL!uK4#JOgf-|<5p5I3C794*cds&AJfmGs=b_1w zAg|&fkxgr}D;z}^=U^md0oLL(;6Cx5lV9P*tC$DIRoi=on#d3A(8VuU1S2m+`3n9x zYecBzb_PXMsY*7y-(IbxBz1l2Vj~caXq9|i{>MT`*nVl?1G6dp*K*KO)x8 zgKlD%_sDl*<+57xMIN7t1dP?C#yL)lY3g&%&_DsDxl1mF)PlCc39jo7GcZ9Jj_Ff?cNQ_ zWS9akf?UZ6smsY;p>|XW*Ck}rE-Jn$DgKiyqtU}>?!BGOGk|#Gt8q2kk)Pw57W8kx zMmMPJJ75z6{>i3hQ>$9^5~xzuI@w;_I(;Evc6OcK`0#^M5H9F>$X%eM$sdfKro747 zV<$R+S@4LDyUO69CkWHovC` z;kp>4#~CrKUT1d7!@*&tV=u@AB%u%6rjk~r>|f3ho7KpaCQFJym)J@!a8tUzD=!gs z*65CVa^3Z%laI<6pW9yD)N3fQetfE)M2NlXpCJmevbqfcseTU!01tdwJ<-7^u{kN)!1#*Yb!^9RhNXmSP+Ro0_pQL#(xm%lf5GJ*46L)Z9Ii8 z(2&XGIactt|GHgz@LuAST3`F^sW}${8TLQ$otI4nw!_3Xt*a%Yi8Kd3tmIVmy5kfo zC+eqTFPR`uCzA3u)mS6I+Kpw6vB7lERQ9~d{+yRjz9}mV*UmFTi#4~ARM=Qvp!p;8-x+}2}P`bSvUH*imMg;j7XEtxwrj+ zzC|PUV<=<xut0$u$OhUP9P`GvniQqy#`RNKr>3QKO7K3m&yvW~MsTyV{0R3#4NmlXTPY@a z|K?(H|7`1fR72(^ph$)u=mGv9STTj$+W!jIkooRQe<8Qk{!iaVt4FbMBY;}_Q_rQ?Zv zQYnFws^787OcdQBcvwZcb~4D^FN?jJMf|^A@%*8!<=DPCTqqWld~@|1vx6ciOSgsd zBuGoQ!4oF^p~3is-0(n|wQG+%dPSnwl%Aljy00H9P?w@VkkTkz2+W@6dqjC6vNKKe znw}JQ3`xad1o?Ui?ziL#_2FW@q3O_P4w|H|t$e7bXz45u_df0%F6##Jhd_5u$X-LQ zd`WEl<&@#dU#y-GJiA@vf9|$emS{DcRjGGD#6pS~EJ*f?cmRE>Tu6JJ`FdT2G^*1N z5uT|RwRQQ*t^)%IxiiB*Q~AuI)F$#2-K7k>EU}3)0Emb&SJ-h6@YpRhCOu`e+xWir zwJZQU*Kt7&X=G!*k+&hi4po#7j7B{|5?6?c>`*!lT-z+empCqBR_ZbPR9quOoD*&l z-U;!R-mbW+NvO{@U5Q`$=^U+czshHaE{v$*&SYr0@lu3&F{t~xT6S4ieUpV_#pm#q zLptySfa@9Lu{!@CA`bmdhPF<8?J+M6E=%$n>-~^yRzB0_?V0<%Zf#_*y4?f@e@ycU z79YG-_C$U&*v7mWY-e6?(&eN`=LUSu>;_D&G@;Zcy~_xS^D zC_ig7zg_0mg57i>W&FO+VsYyyWF9~l#nO)Q$Y_iu(lP-Bu6N=;Hw^^UhbP3Wz!|_` z1?RN1AO_wOwKf`92wxxF56{#u<%qQA+NcvG(ojvI0_mc)D8wMb|4CNs;?6&nZxlm> z%=vy{*~bj)!#;>bM--J8SpZ{N!82?YAig?dKSpn5>rQ>C!?qI7KC~~O` z_*gQ(Lm#WRJOQfXJD@$J__xsPu+MDmzd6Ql8vD#vw&gr1=^uObr!gXnOu5pK1o$J< zkWj2y|Co0QTa$p%aF5jj3~&*Ul3|hCOtiaCp2O{uKX(+mvfdgD+Rh+*BJo!*5k^f{ zyyCy;P*8}v)H^tRIAxi<14$iegskY%t`I^G^@s@mI;eWGN13F5`kq!(*y15Nvljc~ z2y6;N|Me2|VlbwKViHqDhp++-r6c~GAbJn?oh$WU1n70rtojcNlUSHhqT@uWcff## z1FJV}*ZPvXvZ%#Z*cDsjK{Hlx!2iNx3QI92Ts$z#_oWgaDZL|-SHAD}6oG*n1(i<3 zjPSqUUkoj4YBg@c^|GuV?-yQq@Qysara(Ahe$}Ky-<-nEkJ+?!e0V+(=E09UPpf|} za2E3rfOo{!Q8l8rk45ttT09J2OfXESbGE4GJ?1-@>`b_xz|bQROyY+O`!8dJMA_Ae zg*i)9t(bV}+%^YhRM1J8YzuE4nxgf*D$`)8_GiO6EUB`jH?}7b@$9EQrRez?bi&p) zgPsz(os97~7$0^O%Vespo=wz!W6yr5rhpXbX@$G@z_lnq$^YuiCPwEjrWQ52U_6sb zeIT81M=|b%YS;`^v&oMO0e0G9*<5Rl@0E% zcH$W^1*QeQM=_tFQQ=mi7_cO0@Isn^NF48{biN|LqXy#%iCo)0T3)BG0L6u&uKP~R zPspaw`~I=P?KrW#X*@`YY`va@d;LLh1CUt|8*H}7Ej$He#+P*Q6E<8#sx4OV^%Yt1 z?FSOEQv)A4uhA8Kby`v7lOp2mvRz`ArDE8~0!z&R&%m^UJ8H?ZxuY9>&U#c6Tq+tH z^c9Z%?GvZKrj&pDy3HzjT)IIp^b+JQx_JL@GObTY)Jf-Mn6*H%mYh7U#RG-{980zVbY}E<%&iA z?nwv7l??Xkn$4Yj8+Ov@xvJ_DZll~=92<j8{SQ596gpKl{w^X54mJiEsZm`QG>k@$rSeV20j_7+bd7mWr zQK1+N#TvvixKv98S7M7dtyzihleImGj+4J2DY=TrnAp8w*bpZX2m{NYjT~ymZUyU6 zW{Xi}7l<^qoO#CzeDcc#F{fG&L!XT~>68!mx&P#|{(ZTcm9a26{GDbFnfETo=S9@T zD(X|nBJQE~%5vAXZ+t$$z|5+1p*%#A*JPL1`sa2@D={0M7z ztbS;#>WQV;k8+y##x#(pf!UvD0d*`3Cj;;5$^;-N?{dK6;oy)`B-r&7cXU21uG5vbj(DhPj^FKxJRgSX^ zr^WHM>eq%vO>)mK=wzOsLZ8?9B#0!-HspARl1;g$Yy(0C6T3OoQlpI8g~ft$*EP=e zbm<)h)M8xOMcx8tx_|ev5C==H=2Eh_G)_kEl5-OO{tdh_`+BjUNQ+GdKMy ztgY}0z%(yV0nDY(B`tGlM7NtVW*kv8QE0VJMhxXgSZ4F+oJgIF*=TdoO7JI;B<&_v z;~WX?%{2)+s>z#4pNu*DTazM`a++AJc_d^oEhQW#lyI4<)0QH{A$e2x&`ldc!99Vh z46gu4eA!CwPyG31%rS8{WI5WTFkzosPZ1)ww)!>Bi1fh2Es-X=LvNNGmTE*~oEEA^ zs{cR55g#Q|e{VT$`_0%c2PBz^%#srbw;<*`7rvFlzjJ2&>_VsG$?QRYIvf#$%}jbI zcT!ZP%$L%3!5}wITBBCrE=|d+6tS@p>LPJtC14ivDw;$(g`&Z>lcNN&noc8KE$6J8 z0Fe&%UD0Rcc9-j$w!y!YO%SCRDjM~n1VcvVIxvPkp3kWV2v)#Wn@~Ndk|iLD&oe1f z^w)XnjUz3;NcB=MN8`OiWXli!dPddZ(ooSIeJYSkT`!V=)sWn{HjVXOG*Ky1sYdyM z2=0yo@Ty$~D9CHrRGb3cYJQ{De|a*)_?wZbp&$Hq#PwUpt1kl-zBqKM{ zjbfrEojrY>dqL@{49-52BKrr(*eBvmdJOTWBhzjce zOroXUbxc|ilP3az@q>t&RFNED+)gFotAm#sT4pBLb>EEI#*;yJHMZJuWT~!QYglXP z(~;UgTnUv5W5!|hHeV|Ddmg7mmOYSnc_mm2=}>v7+-eK#`M+QMJHx;jvV42y*N3G% z{;b1*BirpX(s#qCC|TZY(tvwb6AkJ>CjW&rUU5$4`~C#OpES$h%Y}o@X0KS{t7NX^ z;V#Ecv{}&s6}T+W>)gA}5k4vq+)L$7s$JdI*=Uemdq$?U!j2~oT>zb`MUvZxJLS$d z?5qcOxcf78IkL7~nReY@J4nulju>M_x!Y9c&Y?T9dtui@lzLO(P&uV@aGfDki0XSS zt`D#kU}jky1=8hmAN;?)L8Rj@e_ZH1u;6pgl1-MP%>bvxSpaceUd0wtbJR6jEJkFx zuy}t(BjLZFR^oeAMvtD)Oj8QlesXeZGSayj+Wy(L9`)aLr*pR5=Bb>>JO31Q)?zs5 zrCL0qbn{|w&yl?Rsktoka-{t5lGc$uI!2v&`?UQk>g)qo;R4$3gVxTIqLnjT-OYG_ z0L+74|CS=Nil(VESlykY*%(%hZ*bt#WBW{KG5%H0*ex~%x9Js?^a4Y5N9SC1T0os862&0}I_f3==*J(IYp9xXiA;O(}2TAqy!(wk9 z_D}|sM*|MxfXkwV{V1ifT1V;y#ZG4m=KM1Uu4Lbm>DmFT{j)DoC%IolyP<1@duAa? zL)Ui7h?v;rXFIL&a)H{5yfg1;wZ-a#;vIntRy%K2uNgf%0yUc#bFKddzQHl8t*1SD z+vZ)?ep-NvPJR6ut=q@be*Tr$yYd^@x2y`Ucy{dDLBr+Sg91OSyvYv)0E^psnoVY0 zhfhMsCYjq5w$HQnLjrHovOqj<=Xum5RrQ;wHi8rn%XyUzUYoOD2M@EGxvaP0ZR^{Y zzt_87f3g5QVzDKM4=0xm-vqC_zq6;en@&#p7yR#EuFFIt7J%c zZ3}fy*8(^8=kfDb#fPKDVX0ml7>0eYI>GI-?b9PeXDQwn`SzqU&ZVS*2_C zT2J;zajU3e7y&86XXUQSR-wogUetU=ZI;m-IemW&s_vLL71Ud?G*64f4qk0(Hk5s( zvW_*%FV=DS@qNy5qYd-Z-(bZw`0(s<&Oq<9FU``@WUj&7l(>hc4MqOZ${VE&n!A1& zB25cFQ*%Hv=z|jTRY})$K!l&6{^Z%a*-*=tf)$H9)hKRSfk8*5s2u2LcBBK{W08$V zMij~({){*ZJjRj^tNLolH!0gsxj}w1&dtm>Nh?%_k*(wZB#)NAr;a0$I7p)8)Ry<) zx?aUlYlC`jh{+a}84NHuh++H&gohJ$ru$}(@5^w^G{8Qq%sSviKBI~CBdb|QzRTI% z&!U`g;F&hzke5HpMI9?+jEYW^#zvJ(QrOyr1p=tMX+YCsQe1|Cn;*X|SAUGp7-LKt z`07vo$wAINQeDBoBPmlUKf)JUym`%tFEH?1LiM^gytfQ?Qxlsu7OSiKGi#U5OQmaU z?$6OFsoezhftr;xVFg^Ry!Cc0h84TZ+|;`CgV-yvun_Bm9Y4trWrTdjiSkc8EySf_ zdJntQI23`O%MbV99NhlP)%#Dbrm2HJ%?I?4!$94XuhKJ&c}fa8CvrvCji@+W$tP|V zAe-29HLs0RJXH-cXYX9ct0hj4wA^ovQ=3I5IZLZ=_viD!Jz}!I#MF}(1y@3 zkU)$#J=BY>F{A*06NG-4af~PqPVX_JN*HDgkGdo}t{@%1T;&f^S((9J*TsXHFH~@J z{%BDf4N#^*No>L>vV!WEg`U#`5e>h^NdJ1XxMAl_Ia@WtnbV=t#cdan1N3+sG|e}R(`?iSLrwP-Y;WxW|7ZJ_-t58co~Yb zjr-&1D;J)JPlLS@GQ8E>yZx&i3ND4sH6BNTt=t_%-X zpNw8G>tAOqcn^GeTrV~N7!zE_Gq%%W^sV>p%^Kc^4AxjyoLa>+JbcZksE$nwi)4H%x3g>h_mv z(@#3jjdz}pF9k6b+|Ti8Y}##G)-s-7hji7B+0(6eG6B(^=JN}BE?aF|C(X&txTjn> z9!Drw+8bT^hYQ%?-LJ;8)6VYmZah~-XApRv>LyORv+3PeFOylH*WFWhCq8a2EBPXH z*5_OfTU%Z`m0y^TN+ICw?>>J5UPL_KmGADcO}aehJzTQgC#yFyGLM*Oc}|O~3tVzu z4WhF9#LaFx$|juaP zg;YUq#dICvUIP=zo^U8cQ3wP%gT~_jswSJyZ>m%6KY`tvH<=z>V3Zp6_&|kgs}V~l zeo^HRuEh)vWTlI0;Z57;Drz;PP&YfZ_IovDO%&hDz!Uv_VJc@7KVRwCS zagJ1KzpT&`r}qW$AUQZjZ-Jl|#cFUpXYgKJl$UCFJ6~qX$5`AV2H=;(YP8?(OYlFY z>(gVpl-iE-Hpo(RsB06VKW=jcjJsc-+L$gobO*b5y>I_MfN+{MJr7C<6sNFSb{f2M z-yOoY9O`!1ILZ4~LAp#Gt%&jCklFj(8svHMEG!}%iaOGvj47GlRE14eDX{U_Z_z9# zDy?TFK1DB2?*~%BCleJZaVk{9;fSK-LvPSHJzczL?ceuevyz;<(t;4$)KsG>jyQov`@v!{m zb(f~_<^Iy3a_RA=BC(b5B-3wFtr-@N0=YtmeoL$IH4p>iTw|{4H2D1u>Atzwq2lc# zI+J3-KeEPbs&wB~kbwC+@p?Lpptbn{%)QRIlSbA3wIhSSDzD(Ue{h6~C-dZB57G(T z62MW%d4|V)J@Db?5R?GDoaEWWvv(n=}HfMRGX0 z1zsjZ9ov0Z*p-`1>RhPmpKKUC#UyAlD0f%?5ga>SUTPxFRPS|Y)*I5b9t>YD^`Yc7 zKrm6$z?*5YmGpK}5(On#2DqHVnF5igS}(kUkS>m~={v&P2JNMA$r7|ls?^<|&GEdQ z5$II+dqhs^dFp=Ni}Cq5(+2BmmXRJWciyfwFrUwt+gIx) z%9O)IZvk*^{ym}wf$3=&F6P5{}-Q3rf7<3m4YH0JGPo49E* z{<#Ksza9{O_}f~2(`IB^dFuTnGFVAR!+DTRq{Az_{KFwFIRKMp(rh*H>ZK89LPy*Q z|87BgS!dSkIkKDk_Tq!q3a4d%8YE?Q@9Khg=j{kbp7%n-fsX6)j7+mpX)6ZO3NgWX z!{x$*S6xTPX>on8+eP#(%jWbgVcPBbE#=%(_F|%;+Q+>#HCZS4$)WAnm;hai!x|hq z-tW6Bw(f@>J>K`Rze)U3^4F5IDM1)W96OT%K)1;wWzr)?wu!aJb64Q{L|5Bao1c3Y z(-?|twwqs?uWP%H^$9`gu1X>Jud-({n{Ps9_7}Kps9le9A2!?(GP0Ixjf$XdHa7HF z8#O_L_7;fBLQmNDAGDpeXup$EWjtztOHFGA>LJd;+LJJ8T^TuzVC6{YC;TR0%%=th zaNzvkTd96JbUJD8z_`$eMRR>DcY8hG7Wh1YS$WD$FOaWm&Dr5@$09!DZ|1e#DMsfm zA9zsJWIY^toS`kDY3sN@hc0a$szA&7EigTw!w_m%kSL#C$v3geSsB;D{7kBV#Xnj% zs&+{FHLlc0@#OC80)uS@(QqaYWv<%=rGTNDcn;w2H!xvUd{pp(H)Lz!DgRZ_y^ym#r`qlPb6d@a|K7l}2;v7g=Z{kMJM&?30(^Lv($(T@cnHcr5!hwzff`MY;{{OA(_I z>$j+M6~M0K;eXA>xapz!)KAk@$Y#K-cmr?T>xU%aS?mFk0@iPEY#$S$38Q4 zMY~aV`KyL7uEPb&bY^bh;Su8zgu8OOLl?!?9mf`)srH43Xvyx&WFIxMm7ig zoaJ{EJhx&s3On#a%qM?bVrt!U3AZ`ZNwxlTVQ60Cm{hhvv>#>Nf>(0rh=eD2V{-mdd$+^^ErM>Q0YtR4c7G{$=kvhhWY-@MJqpX(kEgA!)Oy-#0!%no@Xm;^IUd% zyTPUwB7`66ZnIL1+jS@5^I7r+iiy!1UI}IAxTL#zS%9hnT>GiHDr@RnDBIrg8>L(UzscQ#S{H^d-%^2G5O6lnL z&!VOM(TnNEle!m4?}?$TF%v)jWWDg}(Km#zb(#)uYrqDxnSBGLoWhBU$sQTzU=VKq0q^e9TU z4P1OlG_@huu#5?GaUKOP>f}E>C|26@&3j4&NFV>TotxODy0a? zX7P{%$cQH~ho%N)2E0%w%UfcR86lS%2znZv4&*`0D+5Xu=>w|$x0VWSPh%Ok#+Jg> zG&q248>i{Twh4n?a-@4D+}sO*_1H_4yG9hH!M$F>Q|0#;1Aq#)P?p59{No8qWS{`; zv>GDyPW5eKhnd+^Xp~9Y#~jc_Dex-`$gtA5i!oo82uQZlZP{*vo2^sf)Kvo_ z0kIBqI3_Y64oISx9NO0G2Xy(8RL~Nef1lI=-LI979X}}5yM$X9D-F9`K zNi=myORnnjxBHLvUb4U%P99+G!M(8eO)csE7r3^FakfLEz)T%zF_|J0qINnu(9ZDL zkWiNg!8WHxWR-ejxctXX2o8r7HrV8I)qDrL(kD?(xrb0?F=g%{e=h5hyHEpIr4_ji z7BKwdpQJ;}0b8UQytk?QO7kxq(~}P0aF*+@4j|&ir@>^&l5+)X@Jwaahq98y4^u1C zF@Y<*dtQ0*vei|34tsSyn<9xRm~idXRVXF3bZMy}vC3h?2f`~tmNGUzsIiKI1-*%` zB-_J@M<8nx{#-SBNV>i+G|eK zIH$H&xpsjC=w-v{wW{A+b`skVOJ<#0ASCIz%~Gt?fnl98FJ>oE2eG_TM!MV#zN=G; z1t`5ZYw$3MM*j6Ia7j4P-sXqYD7U|;fTsRTw>rJzC0F@h2Rp?km1#Mo?w0iBTCpC& zfJ~67JJL7efnK#a;=-pEfZD69n7|L&x6oZzsgsSD)mT#gMnLZs4um+W^K}@&<6c+> zg1LkbW;kTmpzR$EdQB*0B9igzEHMKIeMydtSjN(%i|t=6^SaR<$h(@!?9_- z!LkCu6He$8F15(YMu~;i3!8^spD!{pOI?QPT#*vRZfB5HA!{VZnqG*X9b}@W_()CW zFMhkXr9rUGLJX*|ITQ`5@~LLVSYuR*Du=U#Esl@qdbz!ei|l`}2aGUoaUCaUt&WVc z0|aBvU7<{QH+Ya|$nN^4J)hs$Jv9OE+^oSBUS4yY_4@BVsfN-zkAq?z7bIy+aW2BF z!=vpN6d4i?v6JONIU$b92c``n%D{51dj0XOQ0nTSm1uN^_i zdg_}TD9cT~gB#(OU{n|!-M2WtujKYkynG>Wl&3tB>JwQ-hM{?0DY2$GE&TbG`E$8k zvLJLR$hh*E5!JxiWDts~avmSepIQ0nlw1@2M{TirJFtyXDl#o@fvx6VULfYGqDeUu zWP!Pzhn=DARQwp{c}0f04xRdaD=~52a(J#!urNSm;XQ?e)DGj?@&Jn6?9TLsOQ%0< zC~%ElUaw&(zBH-sSuiT){A3HhzOAP>U$&WTrx8QL$|5@}P8pwvGZ$=9rpB?BOxp@3 zH@%^JS3M5T=GDybTNvYvn>3!uQXCF0yZv5`phbxuWqtk5;b+Z&9>x+a;<6wD`L4Yb zMhW@Uje}Gp_=1DGP*%GjOQ79!q{{oxbkg&A%yqP>Wwdno*n3)dq0L#HQi|ig$EK+M z>8_lzE5;P6;^hs@^?{mnCyHexcj?xqUYz4)0!jfu08G%K^p=~_`vQ{(_wY2&-b}x{ zprcn(uE!fkatdXDmqF1}@J0U+_yxAg&5}XR#me$qHP`igs4(3rqxe~)$Mgtf`h4A2 z>*K~@<#p5`Yh{1~j=Uc3MopJOOMQRmpx>fGX4HPvZdC2U!;TUAF9**VMjL9wPlb(L zVW>{bqPkQLJQ)>+20sm#j>5kMiIkWf&nR1S3API;Wz)@24&kQ>$IHvg(>HG@ct z8IH!Q%GC7is~~N!nVg>aLpyP+JGQJqQ}^5i0?z|bEY`#yZf4+hZ6^;xiw`F&3mV#7 zPiP%AX`0fsx20awWsym?)?mHfycthmr)u43KhZzwk9!WE9yXLBlvfM}^x9(4v0sB(+3T)ujv7?0?QbRArwse)34ATnP*JO)84)+Ro2S_Q-m6IO~L?Yl~y-Yp6XtEWCilg={1k#b)U>?CP(=kyjBX0Coz zHBt)<{T83zKti-JqfM%!Uy|`hvN9`H%M#CzV)fd5nCFF^Jy;b~cMf;I-fXf07EGI{ z3-6=wmckhtYsz}UVZw(XmsYr22bUws*_&d2bY$8-{aY;hi3|01y2C090{}^dqFX3M zLmmDbMlU9Uo`z3}Z7r5p{ASrX#q>!u87*TD;z=PptqHf+mkAP~Ok=*xsEUH=f&4%q z$DQRRx=tofJZVa_^Y_Vca>9Zf+_fAx?HDet2chQYu3_GXHQC*u-Lgt&P!1iT7TdE` z>)+=KA;JM53z+fe`!dw!c+$xvtm(0tqP^>&Bkp7^>74s!;P$8nR zuOA7t3S1z6Tb0I>S^OS%?%GNxMyUKkwD!W5jUj$b-4OwhrdqK=eC|EP zySS4GglspZ13k5nM;Zm`Kf8k_Q2 zQa|i|4lG8f|G`jZx;JB+|Lix>dV9Kaqr(dRS@3X1bs=?>V&kLM^Kl zbf5Bmb%~z+kV0I}^9k@x;eFlVrIquWO}J-DLa?vupPre_l;_T4O{Q6z@D~Va^j&Doa%=|8P;Lg0jx$$znu;?BXkhUfPJQ%njk<< zQ!KRl=wZ@-O;z*iyHP(HEWexwE=xOa3|6J>GYZ)9&3V#=stsFF-YK&+W)yS*v?9wIZXRnLkFy*18}xi{yl#ezxNJ< zU{-9S1PE3(&V)^@D*C}7wu~TdT9u`qdOwheB(*$&3SCu{V0sr*_r*WX0y-E>yPH@G zeh>C{(0?N~6}kN~uGdA$jcHPJ~(RnkpgT$YzZdCSi;Ty!f>p!wmNyMjGcZ zh^f#$%oNZH^aQtfel`&8bl)pBo59Fb&TU;v@;GofyVqHAt5>QZlTC0{4Jf&rFB5=U z9aA(>-Nu|DJI;E-+ie@nG{Dn#rUYQdP`l(Zi$XqFBBwR+9IOZf)}J}UT_X4cri@%nuf6EK64tJ;91Q9ela_)f=%$sSuR>#C-FB_`5mj*r#HSQ6WshP zT}8{ORG9f2N9q2)d?Bjj>ruG5lHCg`-)cZe2fbtB$dLbQP~Sd z<7NVOQGkn6=48TqL-~=f0!BSIa1M%533*{&fN}@eFDA>{bfkiq_E6=T{ChaNjTx}W z^rY7n5jc4IDEb1#OEE=dr{s-qgWOmKRqu-dGV#wy`nCTa9D6J3LSUd8?a+UWX!UI8 zpxbhzQWj_v$kZC^R9K))(B52!ERCbZ{B8E2n2rvjY?(StIa6TP{XwZR&vtaJ2ic%h z+8!;}f9%%=l1Y(c7MDj!SJfv%|e`uhYF@d4$5UY(7OPQSSF#g1ya0IrpT9$9asrOkU})3ar6g zjCNU%7U5tP=&2%mB4~r!c{dp6rs!p@TAx&DgqJO&th<*v zi~=`*X=G>MmStivm}GynLK0I{Ki>gkuUqH7hqb*DwieSZ>bJR>`g#LmDgGaOMCcp&T|XD z&rTe%SxIhYSMUM%YbG46WjRAuM;ayoW?wkmrnWe&Pct#wK;Smi(>cbGV$WOJgWF@-#9+ z4Iu~?CxYWcgdbA4yc9!Xp%CG{msf=C1_;`|s^xPZ>r-&+tHAkYl zS0@A8xJbyenTS?*@A-WyT^gjlio}XF(??$>DCbNDw}E04s=M?q}7~kI9xZXe{n4a%0*dx zL#CezF_%0i4BO9fIWve~xd0#3ZqYe2BiDQvT}FYFjZ$NvTTe7Os%Su^Af=+fLm(>5 z9L5puYD_w%*PjRQ3&iM{Is0QyyDk1{zUxZ067mS`HUbNr)#D7xHK|!N+YWoPi>Ki2 zigNbNjEQL5mJ1z%WRcM)nsVx#W`4g;6XU$IWX^2xU?uKYZ@4OuUYpeFfmPSjrmM@b zA7@BFVn;8gB#|>@C!1R7Z+qRQArLks@*wR4JQIRYk@5WRb`K-gh`u`u&aPOk zL9LnO1VV=%?;|Za=qNXArtL6;%%%;BKCo5Y`*y=}6&eewUAOg8W!IhzD7AK7MR_J# zg!8mi&j%$43gC(f%!YF(H%k%S3pqtE*QV@EvTaRy8C-~4tCz3QVLXX^VJ4uOJH;JU zui=7`O)D|;Eld#D_L}Tpkq<~@IXO>BHa`{&#njuoaVylG^C81x1reN3Of_^ z98Eiap0tG|omA0^n$u@cEEew5L*!1j|HXKjpViu-JrWDlqex{E~9Zgmz5~|6u70tlnF#sJtZEzoOH@=h_Lc zycqI*Hr}Pe#ZSHAD>%_Jr!s=5Z044!;h9+F0!=nv?4A8jOCgH{q$Yd00q9u!2lvlU zUS98ju8yzx7yzX-_`=!5R#dUhiwfeOXNB1(29~5pkr0BOq9=+^C9)?z-G+s0%@;0l z$NBqN8Sda$)XmeVRs0GYU8GK=YmpL+d82L-&gQ`MvE;M0q_cFayUZa=B&@axw&{ar zPP#{HrpS>cY;uNrqKaz5^MM$9ka1bhq?S*S9m>~}c@Q3mY;$C7k^TPV2Zr9T5#<`J zIw7x~;oTvIYqkMFzgier3*1)1NU$ER+ZyD(9zk@W?U4cq#1(dW{%|y}Y4d>JH^0=7 zW&~2@9Z4-jA5?0*73k@rFZ#?@ZK4OhJ#3vEg%Re_Fxq-up3{W>ER&AtKs(Z_#41$WC3ZH4uGU>DOGdTUW=1w7(*GGSQ&d2{{|Lv4P z6Hwdz-0H`!Sgz_w<1?w68%<9s!B0XmwL0C;utmHKzk>0n4Hqw>Yg%t3uQz3!HCd3V z=|($d@Ntfzm}?=Lr9ZRlobEcV)Z528-y@V1EBJ;KPnk*_HigEkSY^RP-04cRn$R`XVr z4IC+d9rHzZA!h;py`FREcAc|tA>q3jI`sP(c;^9yasiiE#;i2HQ<`Eu5X@MGA$(_> zS}L{nkfrZp_-tZG(=(v&`Zn6D>kjC+dUIZXJZ{`%a#?TDPTI2eNyIom%5Jh~DEFqy zOV=}~T9KeOUu|6sZ7BG~3)e2$E7t#tVm!YYS)~;eu(v11x!u{i{Q7+EBNAF zPkqH%P1}|`Ae}MkWeSg2OH~y_q{>#N71a=Hs}z-#;6KB=4EAA*UxHJanKzu>Av|!N zxsON#>V3|M(T{c1LYrLX)?9t2`FoE<_gdYBIzFT|5$(tvasTA}P;fm-3n_F@Q8xvFwIafFK*-Ro>oJOVn7i0l)lUo+9%&O1Rw4S+DlG(N4N_I<3fI)?o|N zA)U%Pga*k+2#Dnkdyl6bxRb&@h!GacqwTd^SRp>pH-Pj}-BHhCK*%2jUTIP7ch)@H zzG9Gb!Pn%V?HnRis|M6tFlJ?r_#HgeN93~~$3X4S>tKu{@a7H!5IlO!CqZ=mX(QaI zW?h8zp+b373PNUw#K@~~c3n;lKl51HmX*3#PCVg`zU}ht7yT`an?K7@54MqQ1I#;! zw{5k0jkFa8w`6W4$fIo3JQ>Xt2}Fv$;BvLpDznS9plfi8B&Fhp^n?mgjJjmv^R%#n z0uz2Wp#qybx@W1QL9aDm=Yrp3TWrL8A4HBp$cE^Y-ctlM<^2^ERM5}DRk`dEz3cjW z5P^A1WH5i}Jw{MQCpe@(G~cB034yn0F&TW|(LJHF-kUamlD&xqUw)PWuQ0@B5cgMP z9d6f!d!pv#sssPh9U^EG43_mLw6iZ+c0?Zj(R+90L>)XFIgGb5hMt&@n~5s-BU$fnY*2F4YHd4pOl6{CDLKPLvg618)x93!RMoD_-PU3kS^q%vy7cEh% z_Yo$btE%;Z0C#Sd01@xHADd6&d}WA^ENralzKe*Jw%NFoQY$8v+so79YY zz{o^2qEf|};9f}?o48tTBlra3A+{l5EfsY4c2-=b z=g?zq_PgP__xrr|NVD&d#LleFQycsHoUB;`QIP%{sp<{2DPn8&7~~_t*Ec%E5@oK= z_f$8d(0c9q#xw?ffmbioaE0tKX^d8Xz+IDxB{%`#jW3+5Fsqk88OsR_erY_t7EPa6 zawLQ|F2-~8xIAGaK^7O*UgHROxlDA3Lod0@usiv&x?NBxy3*G)4%;DRE#}R@?hY6+uLoM@&0>_ z5es+-tJ7^$-ovEK8-T5wBkO*q>};Vf<|cmGq3&xdj^F+ebYqJ!V;^YIbfzj;b;k9V z=VsbR%f0fpI*M?r{Nj$k7Y*3#(=f(i|4Cl7)!JGK0=ewD12vgx8)IP{325pcxdRa4qR} z{nkoKvK*7mS#{r-WzFCNc<^h3BRssri@vQerXSX{$bR{ND$hcONeJE?L8%@jf6nS)q=rF{N1&c}G zCX@!;+E6`a-Q&Feb(J8}{RtOCOD;QSFoZ9GWKc;^zx3k)nNjZw(ATZhCE@J%6~iFE z3tZ` zUv;(D0G-Yd?2}dK-^$e_#ixVvUU=C&y_R+w`QGe0dDm({@wp&RzcNv#@xVE#_iZY^ zNgD)N@VWa7^TLf7w=WNc4~U)q+_79K#-|1C#u0K|_j{$*J$LPkaqFI@OV6~8+&qnb zqKl@>yRhQ$TI>B|e2{WW)Ci|#aSN_Wbw{0DEm{UW<{&}e82YZi21_Sb{=j{g5ds2_h24Ol5 z6^)5rJue48k*3bSSTqgyy?ZdBh;}81p}YGYor%#|`wXfMt2AaMIrG&_-%&)sYwo*Z zz`AJXgEyxv%OR-$ltCvqAUo=W&g}Jj&csZPet$Dz zcF-8cNZG6*qGm6B#A--FyJ^*-SPOyz_kn(y-IV`A&1T)eoMGNyXGUt(Q???P%7sOR3yHamlQbe{8&>^Nbul>@~kt<&4)V zE8S_3DU;qQG85bJwZq&DU--7bPdrxcXU8C++Df;%_|4vVI{He#^J^yo8XmVz2h0l9 zyW&5U{pgRUUQZ)^Q*QbZq8xS6+qe$+$s7UOdBfKljhEwB-qQbMC-M~2UCZO71COrI zO$jLmmYJDt;XPez{UoeY-8`0%UA-_-nLNg)1``7Bvehr2IANz#+;@?gId$)^2JE!> z-8FgCw15!PG;PK}Mfp4m)Wb7XsmcRzTw7e{TCMb7?>tfmz>i}?t%V;O%|Rw%{fBCU z5#@tcWvQ1B-oACSzle0xQwHTigM}TT!qc7CiCrH_kfa6SJ}_b6>vQ18LxA*6RJRTe z%_co9=X!WbJHZ&4J|ooX5%P$r8ZtRC_^|?9sxc}nft32Jp9)94@MF) zQY@co@Y=qZQE+#E(SCphdf7U*zn+b0pANB#Q+qS5IR=-xQUGS1XfZIc`NR*I!Fwwp zb|!IaA8ej?B-5{TX_Tk;^=X#K5UC503rba;j?rFHU|vYwE33Jk{i1_(KznSt^ZC!; zlH>2liW*VC=NLz|epz^MK-@v(0Mv0R^Zyqn+= zs%p2>(eNIGLn2^zQf0CIxQ6kHa_py)zgyx9O-wLa`p+)fK>v_tFgx*guiTN+dIg=t z(LUO74VQ2iDH^Zy-zOoAgF8fN|J2y&2c=vx9w&_qQ_2ggDJDh0^a@jaKe<9v|I@G! zdcU>fxXbFAVO@8lgdrQ}C{igUaw92a10J5I7z36KpKqhcs~mkwq{qyBL}}zIeY@|7 zM4F#t|8Atq8wp)Mnm8YXL^?X^3uo(v5RpQ{G5ZLV`S}|^PWpcp(H7LmPp^F)r|Fqu z38~d-CaPrkDZa`a%4L^rQ^^K7h8T;%K|XpYyw)g~;3{oL0zIE<_>BvSk3yA&G7m@b zg!&{U^jdSCKFAe``A5abO5@qCk{fyfOy+FW*=PL>UxuC`5gM_j20#V!y9_`z-v?3g zWh|TVC`r{Hr;Ozqa5oJsc-`C^$1iacr9_ZtJ=dK45gT6jLY2V*Ym`d=#ol`Hhy3REOlL+4ZTHtEPpkFO)BfUDvB zpLL-&{I`?C!VK<*3nR6xXK74Kp3dtLvo{3fQTq~BrZhcvy$>!HZxXTY&*>Hb3D^T8 z`;kOZvotNUgd{|gc^w4h?&VY^D*=Jn2wM~ZXt`w!K)1m|f%|RP-`k*lPv>Z3BXHXl z>+-X;aDw@BB9$`&GFBH0?|SmQ)$Imb8A4ke^6UD|%O_0b6w@ZD*v-=Ijp)?=%YMro zc~1nNkyW4T`3rIJcGRfgzu%RI;(E!XigMpN5RWH#)0-OnBL&&p0PgXy#VU0!wZ$_gh7eTl<{TZ|G`0}JQxa?x+)B3=xLJ$;KCu~(Pi3c1)Cuc}EmcyS(w z=P?E5F=gO3|9n+rz#9LsVx0H*F@=kDfUl1I2|2b_37g``+3IqMLW$iO9+35Ilr0lf zGN2~r$Oat?;27;&{QM6OI{d#ps9PWV#@jtxfBrK2TF2?%V;;-u)+gb_P>17}G3%cP z!*ZRq50f}#-@|)CJTqI<7wx>yv2fy`{{w^|07G*_%~hzy1sOxca__zmuB%!5?pO^D zzBw`qOFTI{-bFe~HSg=!Wyo}Rz8~7iEcr1$4dgg9gdK|e3D9Gtl3`LeEbxA)1U|=N zE**4vBfp;m6X@x)Rn^0d5pa^_$+d;?s6*U;1YYS7v}fA6f-==09}!!tM0IW*k;G>$ zj}yF2Gg7G}M@E#20h!XS?>Ch{|NQ;L?<-uJk!HG*==YVb(wD(Z}wdex=%vK5zfZ1>sFQee%uX<=HE2n&i27qeTl1=5FX; zFQc4>%hT1Qo+}X^mrkRwT7%cL6JOV2;ckhUjA;U8s~BU>B7VqM@hRnhhPvwZcGw9! z%&qG$xA5b>yYgbc**H&98mR2eg2(@+yJ^!AB_R{8Qg;b+{fs~!ZVyVeNrz&2t2aBG z$u+b=bvdhBZ?If!>PCt4lx0TIEl)r_`(DbZ9;JnDrH{_^Gqc6(31Uk`tFV4#88hVD z%_Hf4qwMAZ;1kDCK(WFWHRRa&Z zsjb>$WR?alB7nd7PFdQZ&L7N_L}h^P8~oAT-Iq}r6|!`}DXWU7a=MqnC8d013F5Mc zrXl%7*)cLW4p1{^HW1e$X@6q8Q+20#!>0L)nP=j7naCCHu5HMnYePjc+<>gdnQFf?*MlvsQ2;jkWq3`Jb zIkL%Ote8*8N=_9FlS~A}wQaVKKYb-YwLJ>g#>lfcR6*zXk}>8SkeD^s z(lMD(Hud6Q|8&&z{Um`JP0m0I05Ol66VoT<0;`xIGh!!HKUBs!=TlhEOLO;{CI<8Er015VIOHvo zAw9GRGK;n>X?_)M0gV5wtC@21yZ{v~7v*_3LZq-N>*eCs+M>7Mskro0*2?32h~6Z* zXxQs_Vini-^~V$w2u!wVWWCO>6ad)PXRXpLwbYX}x6{?~`6&&)TYVR(KpXuhu+Z~6 zAcg$nwXPfSw*{jkW`{bl(zhBCH65>`F!u($|mxv&Mq;{gjX zs63F3`5glQKNcYZ5vFM{?ro^ghrjgwG4Z<$k ztS#m`p*X$@ckYO}UG^)nwYT;Q^QcKNPsmdO@u=g3TJh@){@m~i#M9*i@~(B99O*?E z)iOo&DFhG)tAD1vu7!-WwS+Pe*@bkCPXl$1u~O~47D+{A^tyz7U1nWUH0g{%gkjz&o%h_g!q2CsplGujs(tRGI-?}sYv&~rc zV&O{O7V!r9-5ldMeh0%Ue7OdB_IH21&8$WaPgWlN0}t_1D=jM*-fZ)4J$l!(vIl=Q zyNZ~`Ro%;y;@S1G0pq5NfYmEjv&|K zhi#LL z^a3MxQx3%pLkF1zW)}+T!LvVzAlo*{)}WR(^Bm z=m{yQD0N8rxa8z}EFPF|+V`~1mg*I2;hyZ=qOtS#rk@;xYc+K) z9u+uw#aSORO;Q+hb=qNBt(w0K@#_{cBgOD%F-0UWtj9{pmJsg{G>_(8vJmW)tn=Ia z+*xSAtF5H*Q8S2Lj4SeD#pwI(>UwyVxsV3whtxzKE=v_2Z*?Abt-8X6)g}1F(j7Y( zZRb*9uIn$e!E6cl8E-DYbx6eWCRIZF?)dTXxHD^@^8UfrXlpYoo@r8S#J<2wV3WWn zBG*{#_e3^-#)RpB!G_L~5I4`ZJoy($h}l!X^k+FdgMc|=(_84wlA|6|0|s!`9FA*-kpSLLKzwTqv>h zqJLyN5n_C57-4f9tY5Jmdw_WA9I@K%EfGnFi8=n;%W#m(d*Ym0Vlm@kV}}@)IA1;MxQe1>W7?O# zEW_2oxRhD{k!57g(Lq8@0Zt~k8i`?KTfN|LXIVRSuyB@*lT3FBk=Yz*%j7N@LJ(Ic z!*HR~)6sKK=C!1x;5J*cYwygj510HuU2^3p4Ck7VjIB%Z z>5~TABa9Z?0X>Yyl646+R~`v_*jIJT=7{eCoUAaZ!)|pA2*_)VuU~iyy8es7eLZ&u zn~}lXf{-ZgV{r1$dz3ucbh~e$zH8^LbqjS>JMQ}<293XtA1ZwbD9A$=Cu=Yww1!K$!LO?V!@RD_-QM@V>|AeGFyd_4^Ui(TKY==T9H8~2YoFqa zzq}1`Dhnr!S;cgZ8BEzs24DTZ00ix2@Hk zI(pvseS_Yc6ciNd(f2v;$E-=}{_=w_lG0U+8!q{yucOf)lrVLEj1oVbT0~zT6-ND% zJ%OE_2(>v})+>}aBu{E!w&l5C1j=Kn_)BUVh>d8NO0oxS--oULBp$~Fi(Vqdf7flm z9dINqFMj^<95XQb}<~;{;Z=;kOZ=-FI+n!!uWy$_`YRORH3p;~^|WQfQF~Yem5m2>Od9Ow6mO z??SxJV>=tD=6MW=dEFkz4qqaVRVdoN@<7mx{>sa)#`g34Hu&sg!t9=Io))*1A6Buq z!8fVR)1YroD1kDZFUAuQ?JB;|KHAhr57@m?6*yOKmBZTkkbtGJ^>uDB2@<#pl=u!1 z|3~Iy2P?kr8Dq|5^Y~l=-?yp(@;bME{^u>6zBPz?eBC>F7+aJ$^o6~| zXB`PRHrDP(^|<~W5TCSUI1I@Z+**F~TTKk^ccW0ZFc=bio_}6o{#mu9$9*~(L)%sQ9yHp?c?@YzYC?A(jq-o8F1T4Lt9sYxt4?l<__0JS}-_sO(xP{O=D|8Q-Te zzM2GOopmq8#Rj`VTEqRtN)KkaYx+id_JJS5DF&r>I7c$Zr#fZ0aq4zxyl7X_HZ3VB zj?M~dOnB0ayLQQsbJJPd)ojD;c0A1Mj7)W=m_MhGtQ#K@vHS!oEoHeMr`8JW^yN0d z*RbwW?ehM4=C=cvB&26K;wF=+9q%kEu)v&y?4T$JY9>fo1Xs-tD!pR%3CSRiQ07Oo6%I$IMa8?kdf*{6 z_t`t=I-nMtm!`2&J8LY5a_K)_eag2m^%8x(%14Z;iFlymR-SWAWlJJBA|NnH&`KW- z{W2iz6mxyWutzg?K?F7}dpl#=6beAwPWP3~>v*m)KA*_V_b4}_jQb`56{468HZ8%(iA>AAyBEGh zc_K7s((EFXJjWpYwxhEcYziapJz@mqT~qAK*5ajad-ey`6T!nQ#Yl#093r*#J0n}A zUf!DTH?5%fI*T`x)s*;=L#)jfs{WB`MD;K(YZd1I*{haJWGWfb|kb6Lez{~8@Gv{b@);FGJrQF^y6y9iH;7@zm{kU zCASU(7|Xxzl3`)}Y(q9OTX5rY{Q0pLT;HC(RnEW~aJnx&e}P9`G1H>vpcvEi2v1RI zs|#>Kqf)xDY)O&Vk?UF@+B{dJbOSg68*le&giUU>r}JbMuH=?LD$m06?!|Z`SlN5c zhV-^+3z^=jex>bO$s*Usm~ACHx_fTC2>I&oaWvYDGy9J>CL$ogG-2LcDAb!1?TjX`~i1>qVoq8h1`dd$u zQMz2MqjgaZ0-GDkoNsW?Z)~dJ;fJu>x2@hKSq=8Ru5)X39{3UhNgs!Q zca4E>^^$}2Kxs{@@^%y0bZF)6sYXw0LRZ^WGMQ+f4}@H|n2ccv*PBn$PTZ*>Cfuri zpx4EGZZp+fpLPcH`Czr{cT9w6e_gO1WqO@pcS<)u0QJ?Gp8-~AB{L}k(Fd=sB4BGG(3C->s8^p}LpkJ2=)jhk@ zWe1-8^deMahlxdPA8Hc9R;?PC&vLJV-cYp@Y`(|+b?^>r-m_Q&UU(TgwAjoioB7tZ#WSGeW_ zT)nbbeBR~0+q5km?>8XVs9WZp)|PnJ+@tD=+Lw@@?}0iw?+6m%YN^7Z zx8wbq@8pv(c9 zfgNvJ>`N?;Ico-z&VLhl2fZa=vmjLY{2$b#*NIvvyDLsW6(ri(jgrqmnO$6d?f;?t zu*WL;Fl4GFC)z)oMG{%Gd(4jBm8WM2wEl(trEV)NYWh7E>)F4&ruIBzQL!Ac#N);6 zZ4?tqZ#8GMTC0CQUqXy3X46*ME-$;8Iv$hKuXAY;RGo|{*pqLB-4LA288QRv{njH; z#4Z~zK=t(GZ2pa0iAO^|zArC{m_Zi$R)V-KsSeAJZr{@q=C^{c+qeQQ!)A`89wfug zZRiwk_Ma8KV?Vv_+P{tJ-bupz@|(u5hCb!Ydag{a2vPN7+iP#Z^pqXP^T@tYYE&f% z_h95|8a=-od99o6GkSBunaKo&U}qp9RmBcVn`VvAN;%=G0WK5 z&C`!IyQT}V*7nb{FH4~Rh5pwfXp#DsE^x+I0{<6GNhGMC9niQE$P4qm=uYIFoShxR z=vR7Dei2~UQ-=fgFv$LHjU&XnflAfjM?UFFllILPPx$kzNyD>`Q+2=;h^cKdrI%+Y z;EhvByI6eK2vaV{!4p$@=G2q$L6Z#yru%!B{G7P=f zIBk3LWcZDWN~8!W=SK}W`UO+{W%)}Ba${_jK6MZuR$KK%;W>d((aU z7Cy}SlRV~bJK*FFF3$t8tr&*rJP5Ex(GJS!(YQP?{Z2W|xFVlunk0Ccf!uVaI=(Ft zB=OI;tUmR^helw=(o=&kix7YNV*IC8p)~&_Ob3VTPiAX1&((p)I|aQ!&{o}VTW*Tk z1&iE*(O*gTGx2?4pEm}l+ir4S(QPE3bso|V8{KE}0im5NkoXsc#=Ao@PEA1>IT?TJ z!;4Y3!E_C{VKitwQQBip3 zFzSV@9(kyl9fJ7ukxs7-U-6N%X&t6`Y6{FRut3uvcZVdO7rB!RU#H$+6s(VkvX&yE zTvrKk;9J_{t~nC)Bj4TkY5~v<{6U&|Le;b1S1eF3S;B$+&`gKs)`YTlEHm)@$8df( z;mpl*!hB$h+#aTXP|M{x377dBO?%YS8lVSfIOpiUX;vtWc^Io@c1kII}xlcZN74CukBD7H(g{vPcn6tWa zOTh+DGq4M|zrHQhQDkJE_rco>uhBx5)@th?8eU3gsvw6IVTGw#EFCfyig4oyZEu*i zMz}lXo+o;!u58okZCj$&LM)nW*G-yi)tj)VG?FmWu=c0GhqHks8Ztd=hqY`~C*Vlt z6+gXCDM$zkH93wBXJrTDvgZ;*3qjjg`DSegju?xp+ayZ>$rE6MnJmpjxTDXotLL!4 zgm0+&i&58?bxRMd$ehg43a7hhWX>^lP=lO%GzgLcyo95_eLsRySDwb)MWKwiZeCa8 z+sUCYol_@EKE-2iOqX4$`jR633vKn%l4a%a5UeK%$H$wrpo16##Ux7|rmL!4zJoNhUlVgx@wH;jDQtf*tC zV`GA!_YX<|f>T?$4cPXLejcsNr94Lv_k=OQDLY>b^}0qC_RLuC-#dJFf8he&4BGv;msgi5IE zRWbRG?LFcI`?%pFDkLQ%bGT=;Z9z`@LS-#tCAFi^&>5HKM9E_(h9T0k?#HZUh$ylX zzOHnrxXuYXXPHUHC1!&(EJRrJ#S^R}5aT58a?-8Iv%-FdBXSq7YsBZQNrrSMhUkO@ z&3Av%|oLlA;<>!@VSsR z>be~2s%L^S`V0D8=ESVETaujq$wMz1I^4t5a8VlQU~QOQm(D6+GZrt$JT=9l@ZC`E z*4#S()cw{z+j@J)Vl4+>&#)REZizc^S|GdWQ%Si7?6o6M{H45Wn=+^d$8H0CI=kkcr{ll5`o zQto?rQ&xHT&t-}AllBzIv~NI5(*00}zfpj{QH!{d&KT`ykme7K)Bz;eJ}A6MyC!}v zYE)_aG;jWG=;v9=E;qRVf075G+ok;i@tQQC-OUi+#}NN_moB~!u{UOU*K@50PWG?u z(1#bU07@Imp*nJVh|Yx!>|9sC`w|a5HbCf+53y2Xu;=7=go1(k`qE7htnP_E}^S5azTY%jwF-b-(E{7*_TsR)1xVjj|F z$vtF_1~v}yQwYpkx6yx{4U` zYY=4e02bzMj(WRY-VM!yzxKLl?S`3l$pf7Wq*X#l(Y*JWK2woez|Ju@;TMCUqND@I z3XVNgBnkn%$bLIb^pbR*`p3Ly3#8{C7<9+(kNQ|_qNza3ljt38Q|(V<1gB?1YeE0! zS`HlvY>&a*s(mRJNv1ch8;|qw3-EU2)!#lhloJLEW%KrC`7d_rB4XSy`0eSX9`OO6 z>L&x%>>gQ$Y(pOINIfLA*vo+r=vJd~ssyb(+Isx<&|6(OEuNx{65jt3KDO@36nsJ~ zP^XkrFOszj)RR{#DQo6Z#?cCBfj{1Ob@ZdSxS2GnUA+ZRwb83) zw_DljptjjoxjmrKa?3<)&kF{8Esd09jQBO>g0-hWtj-r9<=C_Bv>ISC-J@+=VdwQ;{!LC;FNoO+gf>J&LMs$?}~6SzjmNga#uG4d`OxI3fuh zLJ6!SFc|zHXLUVbztOCGZ&`^8A>tytvG8*<>>}s(-Q&>vqnG6Rnjf_8>b-S_5?$s;4*R50?L=@xvy4Ncpo-NdAv+^@D8 zj9}OpsI>?efg&EHK&DO3ne#EPWcg{ixYdMaZ3AzL9*4>7WMLYnRkhiKxRZrsl2(lr zCG<9L-Hzz)HLA73##0D^5~hXM81L5aY#vbR$?>wh4x_bztS9uX2zQUBZh=|rT`GVs zVo2;#B_R2S(wsE;C!*-q=g^&zC6&Z{R2AZ0ys6p)r|=Bp$lpX>(hBty)9{Z?rIQf8 zpOZ!c#FGw)6DsUdLFDVAQ$!Cbdmf#Jkj>|Atov%XSLPUI3tmAthsh2I*Qs9Tok9zS zCerx!-%wz)%*E7qRcr(EM8aFezj;b(M8Vsh_EW53feJ71p zxRAo`6OSoBL%SppJ9_N|>()Kr6t8nti=V9Ckc>2y7b#v(=sXhNjv{)aj!P7#jgG)k zsYo&(Xaf(!8IuU}OzDm*r*BFdFsT$jSF-2zo5)yQh7CAB<#cC<0yNT0Il0}v!^?%( zo`s+VizX?fh*yS`MCZc0cu&%ced&JGBor)!dsx*y3s)?oa0YsJ#WJgh)wM~tNIYv( zou_SgkFr0skH(kj(rY}ThD4w=FtB_2#E)?1;e7O*w&KfRL!l1VP`nCplQIJ9Xk%zm#S87iw*E%O+Ee;*SP>oY4 zmEqYq>Nwd|3rmV@Lq#r3`W5>_<;OAW<0kqOFda8n@o8Nvb&2NgF1RM9AT9JR9C!K) zdq@u>BAb5Ks3us*`!JHkWn%T9eKnYiZcqX13s}EmaN@N=-RXvz+Lei7@w=XmKBb^7 zN{9>u--=J27Z9O_=mS#@xHs>Cs&tjUt8fJ9iw~BX_1c4ZYpkv1r;S=-i{XuM2$Vok zto5PTQkeq&1ZQ7FjMp$J{4ciNF}kkq4fk%6#%}DkanjhfZQI69vg0(iZQHgRyRmI^ zC)vU2^FQC-^R97^IoFt<=C`$OT)(SUcnMnBq>7MW|KLp*;d8y%<~R3>_AesnaFP{q z{!Q^pB}IZf=*FOI_}{ESw`)*-QGW_mh4E22cfI850F>TkQOM{{5gLQdWEVg7U}}2V?PI4vF`s3=P2kWA`O|7UrJ2GP z_KoFX8gW)QnPw|UK6l3rD)=TL9SV*!5F>wG>xsqk@xM_j!hZU_w~_0vEAR)9mk_kBjS`uZ%EJbm02l1$wp}Ff47ym39hJYSx-Hbr>WyRKu|m z(xuty?5~}W_|waqv;SG`hJOfA?@qb%*jV*qHgAc(i@cXORW_~i5ZG+95}_5qR;sVI zUoSBFGe@?DICqfD;*~TJ@L~1$?Qiw=N{jA?TCK$mM0F(~<$IR17iY-d_v$Q#P&#v3ukWfcd%RNm|En__1{&NwpE8nR3I}w)qx8qPykq>4>qNSgVZj z5_aoBh+z&XeC^Jv3)@W@=76(|o#8iZKJuk60tC5c0_v;;&CpM|$OYe3rImh4GcYVD zymz=8ece=EmU(USdo)@;<407PG$3J^yJx@ijjF9JwcGcp_H5HiYt+E-MA5}~;CjuO zLA_Fk0Z4Qnaiv~8FNhXr>XP+j)BJr2N1+_};J4OqvSEhbxp^Y+INRe$FbbU+0i;Mx)lGeN{;Q} z4|9j31jDTCE;q9%3ybkUilg?n=-lMutPJV07os6_0GBB8%$^guScWf=w$3A8X&S;d zz=XWVS3t1gWsN=O;ZmE>Jj;ys*X4jG)DiwAf~i{PVkSRa3q6|ec2uOh z*J#LWrwtS`^T$+l4hP3@*Qs{wC`;N-JR4Ge#wPN*Fy^M;92F=m}n z&D`t@U5|lXxWs*Det0&-`r+&(bKNbc6c%~7mk5Oy$?%Ri2zHJ79dBM)>-x=Pc!dyo z`+&z0ncS1X;b)EhIn>JSDh3xeYpc0D56a9G%-E$(mu| z$z^2D4~w>%j83k|mSC)6gcNkDQ}8Sfa4}Vnd-G6cA(;_BVmp0@7)mB+7A=1Hu^sPn zT4h?W#9XdUW<)4;oj-`1X>>I5%hQIHHdZgXiGMvk@+z)@?=xRV8=7 zn&PKkgTj;Vl~iir`~(kPYXQw)!fbifqy)0I)^=qZL5guPrI_mp(ILq>eEG1=%dQ^W z=PW?}+vedQ$uzhZ%AtY?_&5PV>bRIR;G>uEL(J{Ba3I>;rqU#aRT!2Av!MO_P@A}T zLwzP&22XG7(1Z?6{WEvak`kXu> z^4|Vd)Yx--evlCQsp+>L50v*7+KDf`!!xzw)K(3*Puo-%q>He*@P>g-U|6@xzikjud5VBdA^{xwQ;|=_aVbn!+%8fZ}!E)2R&b(DKcvvPf z!9F#H@K^EXjZ-(pHn`cEWmJ89fk}0mSNNhzM`V@MytwGQc~GBZ1t#8)vg@DB!enxb z!f8=xJY?m{N9lr3*j~DnpObN#YC^M5wxdkwp)<;8iT0gH>z7|VD4oS8wgDQ`Wm@;z zDw_}>JO1Y)FAdu^LBXGTk4Rf-2v*SeqTW9@@fAn0psGWCZ9?5z`i~H0r@Z5kl{I;> zIRCOj{ef%~Hfu#x6?5f5bgH5_V}ryPSeZbz^$V@TV5S}+V6Ji%GbvGnk*>ve z^9%Fa_I3`P*hWn?=)AwGJge~a0kdFyI!y-mf?=eYACbsbe{m611;#H@z-!Y&7)lsz zr6>tXZ+sfqNR|K7E=iXqs8eJ}^8LX?^c-mB)h=5f6*6ky=nnlL~07BW7!Y?+85-CWLpfI;|@Tt8R?6|Ug# zW<+vpJj-``K^4C5EugKdE=u5EuDh+<*K~Zw&fg~GN#8pD>KWoT`EP6|LJ%a`BGw?T zzRz8FGI!P(Ek~|ACKUO5+TJyh>2cIE6OGl-x%d>k-upVwQFQ!25)?c&MvItNfM`sH zV8VZB^1pancdvT$O!;we>{NMd@5^Rxb5~c3# zU7aNz`Lg?MO%HK0N{;A^uj$!uV3Ap_QSkd~uKhoqIRmAsvw;Wls2dy&`}M#Az|tm3<^9a?)Z}@0lpyw|Bp18Y{&iyXb%f_JX2nSXcJO=dhyY?8&IjKU< zdAl8Ch|+2QoM?pQ^Bto5hl}1l?j_=VQ5lL1Me)23?jW`A^~#+9J9J$J?u_Lg%UQnc z>KQ*(qbAd>yeykV>Z9az0Zc%!4E(-plNvJ}=k=6#B;)VqHwT+pBO(7_z9v&5vDGV2 zI~pLJF2K#7UPxtyEuOO%KH?lL*2c4XKUd!d*V6e|_?#|*i>BO5P__{=$wmXHJ1~YD z+-I>9{&wJdGY~S^0bDKqD4)vY{}=?w&CCo@nso{b~pE7kVO#sqkKitXngsZs_Y1lKhA&F&Bu-wGOJV|`yBnEF$J^bNte zCRr_(pOm*lRRoSA!B}E8GpQ`|B{rZ7B6V}Y@t8a@G%43{tm%3n*+-WoGRD&38=K_+ZaOIY0YRSZ2p}f56e$A_{UgC9(YlUu7l}8M^+5cbFDj);KD)fJVC^aOztX z496j*ex2HS!`I4wxfe4oZ{0ct3wZUw%)0`T_4EIodLUnS8ZMju@9@a^xy=wmD`0V_ zM06#v#T0(2-m$}w!wgnEd1Jq0-VygzIW#2n!JGZhq~ix+aLyYccD&(YSRhTlu^?8s zerkd3|I;r$GJM&Q`bUcPfu5=FQl&>Xd)&hU4b~rl0FJwCz+k5jjpbz{{}uw53&1uIc(YZgb1WseG`<$@AjzXuBG!}WKwZq> zXq_c~;IjcI-ngbDkUvbs8r-&h-i+1IeZR%Y#MuH-p;61h4g4^2iqZ&4Q2I?|)y( z!1+k`{PQ;A+N3ZddH@I=Y>BJ~tWPJ)I6%Wed!~)`71z`4CV_?kBWMe1dsQ1$=gnv! zprWT=B=RL0OUo{|;-)G*4>2nBGgVO065ci~MYCg2zN!Ff#g(hm-f$VP`bdg261e)v zG4*ITyc3u_%Y^3xxH5>uG1*=vBXF7!>)n34nN2MHJk?>S>O6TLf~AI`HA@y-;XWzM zY%Hz%Ze@0fW!ZaOioYh{q4Z6<6x^#Q+woVC*GmI5a#Rai*rrW4V@Ao-TysZc@|YVdDIZrj{v3h0w%bZ{{0y>lTsK7sULb zNb6v$NiXmI`kbE0K*t5DItDX%bBmiW06G>uDYg?uXWn}SKF+2hKR~zX#||HN7k*iO z1Ca)^RZVh@%JCOB$!p`Q6*%?>f9A8X2(TD&sIT~oU!h~Mb+KfJvW%R$24_di($1Mp za5Y>b8k#N21+=~0^BVbG)?eFe;Q;1R#Q3+0gA!bc>*n_F)2Su851E;j%8z>P<8es$>maPfu`{9|i zD^%{61QBeKHG%w-l009kla&~cds+nef$xK#Jm5RpVUnhQ-XMJU`2X1a@3Q6QUV-x% zb}J@=aWac~Ux{QX4V2X&))oS#sJwO5pvx4tra$jAAP+W(y}= zem;0AAyX$%U*3q?Su=zClHk;$A4Nx5&0=aKV=)|84VUwfa+PK%Fu`7Q4t44dPn)(H;Uq8jh+ILa9eVLvCx4-%9@4ay3JQW|R zfwNw^CDgZ=T~V7a!3g|N25H{m%i=pMK@+G3@?r3Ir2{vrCmSJMa{X?so}bD~iCf%{ zm5y0+Q5{f_HV9@xXx*R- zUqKuw6dO95a3{%Kx&tmZMh;#6l?2N3%6LUof#fa%?S=j~xWZi`WL)%xa3qQ&Z<-rJ zZrG3o@t3K-qtdyQ(|JpY^DH}lgy~l2u1-wWW6E~IZzYC61nHsXu`V7&-tSnK<{sL@ z$B!8qDMHg&W|Qm=%GhKSHvsn0+vngKDA1GxAH9~(2Pn47eHw2YA1Uz*Z{TKq0$ihG z(cY;n)p*@Sqs;0xgIPr7==x)!+4&OzXV*syEU-K8qr2XZPo<9^J;FL}q^|$FV<0}z z)f)QsHX-oaCP&WZ=&|&oI%sMdZ78cX0y`SnobEiX&CNei9(Z%rpA$bO!9~5axI@iN z;;4jhCfL>qH!UaGjsuMt(hlUmy;{7aHJ9%;x~X))ezz^Xz zWDI|B(R!zLPQ@1qBb7~G@FAQQswVJGS00GX%5*^a%MjNO!E{=w{5K7fkA=RsPMZ;P zPExi)I{9S*OD1rdrAM-8{Q@l`np5s0C8z$SPsAZd$7^^BDz>BhC+OE9nsc15I%Lq` z4l^%y%v0AaybrSBY_$(b@@(;G-&g%6Es7(srN8$Gh$q+cv;KA-Bi8oC;|?@~Nn=&Q zo|ML%bzgasZU2ZQej>`GH|s%ib(-2z5)a3mO*U&bi5Fa6ta&D8Ab=eClm>=fFZJE} zqp;LPEpz4czN_(mpz{2 zeLwF>DeFC=u$i62S}aH;`38`T2myf-vHgvdU*Pv4O4gx$%2wHrkH=OW1J8qNzgnN` znC&QHFEjm9eaPcl`N!9!6u2}%nPn{=A)PM_DbL2^>w z0mx?N4I|dbW&7cAu1tGgr;h+o#X0--+g#2yrv9smUMW7a8csGJDAuprXX!jOir)Ps z%D~s5QqZF&hkDA^fI3R`>xQS}SPgob$nsny$V1WXYB>7?yy4*MP}||B`|+nel;63} zUYfv%AQyseVayP)Ls-cf`uGCrGn3Kh0!+whJ9`%nG?`OSb|5L{aG2dvwhM4BTY^H$ zIhPxvVhfhNdtUl9xz_n|{wpGw1HM*0spUI%n#|-K%2A>1)1zs)I?fR5$_Z6E1!0Dh}1j*b=oYq6-4nT4mO4r_3!;NkfAuPlLVV z`Uc9rYvYl|T};UtklNB5LM7zsqTp2T)LIetPB7d!c4n<^D1zAe;x zZkNGfg(v}-XRX@)8T-~9(8Q(^0$;AbW2Z=fFAm=B*!d~YLIV6Zc3s8W5`b>VE} zwXDJfT{1>;@D0bAI;w#A=3q2G&$d-uF^dO74S~xZVZk(2Dd#6_{y~YNl>YS4k45fX zVZvX*>#izbKQIP;lE^ZU<#Y&m9LsLq4jFU(T00yox}K* zJ!^R_ZEQMH6G=8&^GFUX_LJ?Hqo}9NKZze1Ap_C$BbV|u_2qmPl9fbvii9_r z1LmAb_}&h2g(vSW>hoiy)=J?a(Hl9QcHdgnhz#hZQ|?VBS9AH5h0|C&0m&>6y}GYf%31q2VYYcTq697{|!wW zR1Tb8os0DTx(dV$MkOhyMBrHpuRo>wCqOXf&U#%3dvExIt)s0+6oJ#jfNm+4P82^K zkL>&Tr{LTVdJWkY7uoXTR;nzyhx^^=#~Z@*4zu!M(v}Htnvd_IyGJYvZ8r;r{Ik4I zg#-fKzJ~?X(rj-s-mvVdiwW>0h`OT$Ur|f2T6W0T5PN8owwrms(XbVBo}=)$>GFY%P+3^A3n>iYC0Rtho?kuD2ewl*Pmr8P^n zi)Je<_#1R{&mnzPvB0Kt#9n|YZx()$;elqS&M_|7 zg0;mVV$Uj670*bJ4sUGp8xZ-X#)c827FR%(KkctR4s7n&GJdJSa3IcI8hdl)$pbtkbn&4^{bS{q1Sl?2+T-eYBzMn3`yj%T#D>*i#IGNcWTw zT{*r?VlVY9WF^=U3u&+M8SkfC^|A`_SpW8&>T_CbkR@I4K-ejtMzQJ3P~nDRt?Oy(`YThm%mnBOn>=Do(+m54QWV5=CZ2xh?AFy{?L_gXEyK|g7BJY%wJVd zxviRt$QU}!P>{u+L~L%EyN@@@PBG^#yh`YYq>gAH9>d3{PKCXIV(1Il>3|}X6(+1m zkGcxQZ9Km|ch4lu15`o~Oh`t-3hM&+gN}U1omHt#2p1xm-gsn+qFX zDepB6@d*~CASzy?c+oC<$Y2+o^y-m0Q(dK8YANiUA)yor-Q~vAv}M0i*|1UQQiX&E z3H!T^{L~_GCyYF;*JC=%T<{)x-cx3*HB0Z)7YTFhyanlde9<+P7>?nz@}v zo7RivT}pKxrJ2qeX9=k3EWtlSz00?qGL7cdz#pl1_|+Lox>H$}gfa(XFP7HO*NRPu zyQOoU)1H+}x7|4uAUNqEX@%J~y>cG~Gvi1uB?g&(zYd?os@4bpUchJChX~O z1KViCWG>4F$!D+2n}AQht|Jk+2)#?KybIl@kBET9w?hSTl&3D5Nx0Hv3GpD9}Q`jfZibnnUBKdG5a7nV2lFzSn|WpG0f zdDNc7XTe@lke8XQt*evN;-$@@0(!WyK^UnEudxd$Wyhc_&bhRfJhPsEMFQu2DfBD5 zc2BLR-T4}^9lf1Uu%3YBWWWDra_v*y_RTXiGlqtOcuvs-?jCoOm%n4p)gF5jZ-$mM zJntZ;bvMd9$!&418Tos8j)r1;7x9eYW1~q{(cnO|r%7!(hQEsBh%V}@aPcy)dT#k+ zzlV0}q*rZU8$B=aKUPcHhT69#43`7K?K9%}Qkz}84F`x^DVa;`y|W1?k*cH|RNy%$ zySr||-j^lOnGP&OPcmt2j>2im$j*eejPi2U7xz1&n|z8Qm2*5FuLWjtX!-m}a&G}M z5Z`Lfyt_flg%7<~!2y_YyG=wyavr9$f$QT3(T8fEZtV2XW0rUMEkOz;!*n zM9L)Lkt5j${F0WqP8?;4Lt)yMn&(WuF>{h$|5u5~90Z&Ig5Zf8zmPRydTJ_p$7`cX zu%?_WGtJZr19=*-@SUwBh)~x$8S=%W(7&WCCzr?bfQ@eT2L&Rk4-#BQfJvwhBV2 z+gxT{9du8e*H@DMyT1##_d;wh=-n+$|H|m_dRnY8RKmR+Ev(phEd~?`D zBtDPj+O=nEu4g?&zeB;Q-ekimqvSe2?hxU*E-KyClBY1ZD|uQ1=gakeSLT(d`l44rkb6K$md_DBG}B_aZ8`KzR6XN@ z>bc{>X%$HV(b3$IzhiAj3jEcxO7T0P$zX+lGK8y#C`qnQHrFjEdn%NRZ-YNGVV5{o zpRIt^#+Os1kaW4RUG>+aa)8ZlgNW7oPbW(klvV?y-|`(Azc<~UFPPsbq&s_X?T?0< z^~*KD*lJrOXtl|8y&E(M{e&^#-saaOs9c6imHK7dAr8Om$g>58U2Ao(-N^3D2y8P)!927Wp9rFP!}c-AkF*@in+q@xw$}&z!5{mtQ*h z|0hNm8XeK+;S&3qDWBCFh?Y`laFvknM&LrPumWUl1{E7ORwk;6uneV5o%hW`r_7@3 zRLza{Lc63oMFmNXf~~xEL{k6#h}NFj0c^*`cz7puW_k)wdu`kukP2E)pIrMqBaKof z{oNL@NR+=-L`zbJHM_n2>U0IHTAPZG;%i23u)2s^5Bu+3k@tmL2bVD0SLjG{0a!Ke z)MynG7@0#hO_KCU{<*@G)smQ^f6I+t{ZcM;qmJ4S$L{Wi0TIgIf9@h=Kgj)q{2b)B@QKr)BD)E_s?*Z=7qVw=>nP&?J z)j?TOO&U^A9b%@_P5oTCM{ZKgH`!~&OaZpY~f{u>HM4sbW| zmzUZQ1IqO!A(vuCp3|L zYhfbMG!&9U1~3yEXXC>s%W}VES!K;WM^UMea1Ze&)$2%5=&`f6gkw0f&6rFkjy_0K zb9leSuc8hxQ%JLGJfAQ!tGru!+XffNgeu8{by<=$zml{m7I>j!$vysg{_ZKTc|ge@;KKX96l{pyBYzIJ>>iNS7N4(9in z2>q99%Og51j{^xFLP!EbiYZLqLLfRKNS~KS71_HIBr{?Wv@c-%*0S%VXd3r!tc#s5 zeJ*Hz))#(|Q38~3Y-j>2VG4i6I7YL`D)Q|gML(HeJwBPn^-5fuh;#9xZ}uvQ@A-O` z<{-ZB&)j(OLQ@o@yBc@o)6Y$7%u6DvMfZ3gn8N_*!k(B^+HXH;2-jgl>oZ&}+Q|8CG;#Rq#1k!SO%FT6;V|oU=2U;jNulsd36RV4Qo^MWTamLYKXM@dti^Djo6a`U zlPKn0fE_~GkUfR-dFj{Vm74*Kq@@!LQL$ezSJ+| zgh)uxDvUT6ddE(uFGyxM;yWHqki{=f;qzUbqV?Rlz~lpxUn^om`_yVn_wvd;(<*DU ze&s$mbaa~}`=c2af5D zhJ^kONr48HuS;q9cxhNq*@6;{iE*?5*6pH=Y4STMvZyVSEzeJpd4`^w1%+PSAM^6_*C;{s#~gx-!&aH}0`dyY7tyjrgO)t(a~Qm- zbr0ni9zVQljAm$8@6Uq#UqAlz(Ru5e^8JyZ**(1qU2z}tF&-4@w@8}7_v9M(aNessy8%LBE8jP$gS2VuH0{?Ef$6S z_CzIS(5l)IN~kim8?D+nuT2luRH;H)G5QTlI1tmSsiV;-NQ za@e~mBAttKB;NwY3V)~nu$&DCEH;TL)a<3Dwn<(XTqW`1c646NAV7nApVdsy!uaG( zv>fraDQ*h{;dhsZ8bp#@l{~A2D^YrSrM8Sex(x3I3uVii7tJy^<64n2PENimuE^b< z@#At+w=P&@jdmP%pa3(h9EG>=Kz?VEnr&pfMbc40s?o|zig(%6=tTdB-8HdTNTd(1 zEQ^`ld@x^Hm5#7t_q;A!AmZ3O1YTrva<}YMhsMH|be1_%{LqneB!=ky`-Qanzp%! zRkK$G#8G)UD8E$nG7c?=4X5Fl6{(Z=`><$kNrZX#<-M%gEj1b@1K_*Tl`d$C4K^V}{DZ?*gxEwPuuILW^fda3Ezq$yiaL#f&7`zUV+{Y*3%&H&v%o%gYjPqaXi+oyB zcj_uxM&Oj5{U!uDPSZVl?LTh*^AtVTk?W84y=@CS#PP0=zt79(6f<@goo7dDmA-tBVGJ-1A9FV(L<0JJ9;afd z@4cNs_U_}(=z(sPs*j#PC5I9;F%BP_dCGsCHU0gR363s(@J>}W&q3tGW9JOyqv{2s z3z8Y?4VQGjaT&)IDa>74S%L2Y+>f!;pjKr6^_`Si6nn^yKB@e$G?qPTMA*rfHRST{Q*DM`~!pt>zu}&U<3jtOe`kPYd=R zY3W<(BhI^3cqNKi{NjBc=t0Lz-1daFOc)4tOKc?cZs$pFHFd$?6J4Oo>0teeH+`@` zz*fLE9_sLnw0ig?s~zM4vX6w@ZteEPHT=6~H~pa(+u_|sZL4TaCM!2x)$iQvHApoTI$#^%?vKC3(>`K7%$$ z|LbDE{67AY;QS06LR7I4cr;M><@3-{>PC3K_nNH%e@GqUmt@}QsPV5HqTTv=oTfML z#t$4b(&&|B0Cs-{!m9h;<$rFd-L@}(vRy_kQ+5C5qWRWBBK%IM2AJaueShtT_45?n zoV=Iu1I$g&c-so3e<1sRB#A!mL!=FwZ4JtAx@>s}P}TPAuQf{h`D)zP*CME{jRW(V zsu#4;%>H!;-0q~EEg>rs4~SP-gD~3<;~9?zs?7t8s(y;jbzZ-o`R?~P)AzN@3VfBc?G)+|Dfs}e09l8&9z*@HzJt1tY7c1Vk@Av`w=cU4Sc-i zuN^ib8MbL2vA7bPnKYrFgwAh-4lda)5l*hVggxG$8*ltC#<}+bms}C+tVEAzK zZA@jdGPhF$5DyESL(oxXAHPm(TW58_AWXZ^iabB>?r7T+a#CxJhGug$pDuXbBM4ax zGQ~A1>F~2PP^^O)hlA4&#`u!{5olBq0N034O0%?wu`ea#)A2lscsE2qc(Xj0+#;O- zzkPW>`HNtO=~_k zTY3Iw^INx!=RnhO#<<*MH;)y2SoYDGL{qvJqkMk@*H%^cYgAj_8D*?vncZn?UNQpD zv`Q)a-s%?zRpd=Un{|^kqW!TGUA>5PrWp9nYDG6RL**vxTpYFsr4f^u)iwza=kveE z{`NuUNo?)vR*aAi4czxHmW7HaANs{%S3!7Z&5HMn)y?8MhhRGhVq9>xjI?;9DcJws zsh!JcW%!2TQibxQmFg#Nj#Y+!U)WIzdD)9JOtoWodvicgr&OMb`~G6B#C#Q^%fwh7 zMIh|c*R9j5&|cqOA1U@ymlN;d+)eZ9@WF)4uLAMK>U-eoxSR#`2KSP6*xBfTE ziT}zG#v_o60Qhkq^fQUR$`qs_&k3x-DaX2&%vvl?cT0vi6{p;0qYs6~3dKI1Zyt%u z=~Pr}fJ$YM3D_?YS@OMmMO*<(w$ANnj;Wp8Vcs_TCReqlo;snaN3F9+FccJ0$$~a+ z1clJ~c<4Y$so!<#CF>~{{4pvZ=M#GQj#oH|zu%siPFQV_p#1=z{_m5!!Kdnw$6()z zb*804aGh*k>x6yv?J?Uc@amr~cLvrsP?Lq>d3$8_87L9Xl`g1#dwrsynwBubmM(wT=j}IwK!zpwGmW?|E`!>q3!bhbq)?6R7eNe!mNSQR(F{iUw z_tx+bMNj@}S28Jh9?oX4C4fy*qQ1gOUD)%Kqps&x22WFzSyL38(; zmbAE#X!Yb$LW*4O!NT>)7TK^T`&hofmN^nru7{4gd@Y8%r;g2^DBi;)|my=Yl~@d!H(e@Z0Y0j7?Sb0QQDUyhEcsjt7&nPb;qWBC=PIJg3LW z^7ibJA6lFC3$rzjJFst#ykH{u6r-ug(09c@^o~ zwUe4$bf}is`}NeXi=38!p9K|>9dcz8wlt^de4PdKYc{wpE4^<8+%XWjuCCAgQ4~rzXk9L7f-!?#6o2L{DO~o z|C^exI-ak?y0b+MsGXMl*vq$#Z^og9KGBY&rQQLVSn|lr?`!w<_VMoY>?Y3Gs9fsRlRUu5)`&tLbi?AlRW%>LPI-5L?kME45u?V%hlT>+Mpwlc2zA ztgE46Cld;zL47hU;UZNO&Rr0<0ddnFEf$N!Fs4@k7xNN`CGwD%h`LuF#OUGO$hYg1 zYo?S!h;fMF!6>4@RS~=ip&F4BKq@`fj@wT4RwL3R(isW;8bNSY-WhA}6=1$0fO%I$ z0xig%}g`!S&J&(wkt^$k;8FIL+^(IY6|fN#pvj| zoIWt93q01=gfyIAhbwsd-WAP2x*`f&X3RJ>EL=btthRf;gr4W{Qnnn0ET!7N#(Zs6 z?9HVA6wq}3Yo2E>s~Yk%EQ|@m^kd&)(-G`I|0(faD&RCDR>=Nw-zM^v(@EpHFC6Tx z1VMb2i~-cWl|uZTvp;%&%?i zp(y91!OerTxTZ`Y>qcrB#&Q&E5t&&A_9i9j*HPC!LbcLPc%aCW`E;idip)?u=WD2! zT`1>hcbmw{zGTZWZ*IIOg?!HichVi|u?AAL%}%DASH~*Hc(ZySW9lx57bNj0=8p7S zD}-c6w!$de=hlmk=tP-(g{(v(Q;Lt8%@}b~_ow<_v8Yis;vg+`=8a~Bad@}>X-Tox z+@L%|IN7@8F}QZL+Izoyo6A9?%l$T@F}3%0*#n2t;Tu{T#ujz=xa}&&1Su473F`eO z9A(HxaFb(H4I7Lj8MYjeA9O=jx_9PL*3VOZv4yan&UN(_dSs38IMl$I)$R_uLAOlD z*c^(0xhI>NqBwtQf%Wm3N{lU-dVvI;iV5&)iv5#`kC$ZYV<&8n{RhGob@xucOefDd zrJke1VyVn0^ONsvaX-&!7TlFW0a~T^9ne}&uUouN7KT$7{P!?aREFMDOZ*{ptihThDwjm9|=@Ww*n}s~N05C^(}~!Sf=+ zx7s4B<}vEp*i*cH4!{W5LU7s{Y}jx@b^8QOY}ZO6ZTSMmYJELF5f$LENO8BM#J_#> z+R)bMhp9(y@vP311Prl|hczQcr65ISt9)|C_T{n8`7#*`iWC-OJN&ZNtL}53fknjf zYZsQcJ%lu22f;^C385#S3!eA|8|`K~#JnD{%uuOH(N0$&Kw3oxh*_LJ z+*1!0rYR#^szTyV7)80B2g>Olx*tq&vW`yuKwmFLUmrr}*-h{Le5Vw^`#;>h^;29y z*Dji%0Rkj=&?LCKGq}6EyW8LzAVBco?lQQ$4Nh=(8Qk4{F7JEJck9-v^B3Hz{$bVb z>fOCoZ|mL9dL9nr!e5InQF!Ywb_+Ub&HB1jg=95oBhyg);97L><$xG2DnT$_4il!L zb7pFxd@=k`L>W3FGrrnPW=$ zO=Xp5!?dAM(=K7tgk$CZu6(P{($SGFQpV;TlTZnwLNuPzm_=LmK}==r@l@f1^%mTVY=QEyO>n0F!3T${(- zC4qteYZPaQo~!VkXN!3^b~VkVhXNqb=a)mYheq6ikjD*J+~yY3|9Qm#$^+w@ zw5<+|{4}`f_t|NO&krHR~W? zn`0%!>7|eP4$+*eyrsbWO+?3C0#rJtT0GXZc(uQ1G-l92{Ji$?&6FiDy2#ZRzcjDc zYt5jZq6|z_t(?DOI}YbbaQgI#6j{4WTQWE5baM1GFc60JX;Jm~E9`Q@1i)+$kX8S* zWXqrwN$hATm$GDRR^m7jqsuSZFJ;yk|;#p ziOhPv|CXD2IBXZ^xy3kN7<{*lh&PP?w6E#*PuKM-0*k3-G>ol>1Jqh?eqUH2Pun89K6r8jFPLQ?#4q%5uca`^8xI4;YY?FLJ>H`Yddk0 z3WdcRGBBQALwr)kH5oOKTr+3cG~QSnoQiBWS8gLMD4T1a zwCn%w=vI;E?h2APr+WpC3gC{r%+W9>?lQ((h4WBawy_~UWPKxyhlH58>8%AK#>SwS zs93DvZ!Ni1bzz#fFAqs{#3e}vZnJi@{5x9lOyGUB;z7fNEXqE{Os<|4^UurMlte7wKc0r?$MLVb5^(JBA#-na@kd{?QS>SO=fh0vlFR{sWRRR=Ls>L~E%_2RP!>JD34-XmR>J^8atI;yJgm5koprjF~_ z1^mv+ZnhvR5jc`3d%WtzdQ_ zkXl(_ELn>SvG91tvTg}wcx&O;ZEAygu;NRY%Za{3Jrp>NC-6gvvLl%E#+v4pICaV& z|EjvaR+!i{)^p-SKyDGES{pqd^jL#Gp2jn+#+H~w?{6a|G%e{A6w|(%%g9c*kc|H((vEv{9(37&WS&oUTLkR?Zl;fPPWJeG*4EhyX=YSs z@AQ*DDWE*_K4@?Lg@S%Wr0Zchn`Dn#d#;_*Ft58N%{OB?UEOvYjJP~Ca9h#OFG=lm z!HefM2PZa%>qfdplRdBbI1fjEh(M~c)xoY!Ys7z~-1k<_AIl~0c)V!PW$`dy1IJJ^ z3DaiWU(poZcaYRYooYW8==neJQEJC#BYd?%|mw!|PUUEa$sAte-&d*|@B;T$uNDLAYzR0aEvs`{&#J*y=gm`L=jECZ@)qIex6%LM=cCN>YiHXcOVCtjHBie4}rEI7R5g&r6?5_j6e(;`I61Kheah~xVy9>M|{wnJ~B1+Iqxw2-~uP6C?ZY|cd{(H0M zn^U)@&kscp)9oYba7`Da^-e6A=A{&?_MeQyKl|K~k6RD>ubQsnJfAaN9}>v@pQej0 z9s2w)?q5xpr=Bv?e9~ZFdp~3VT;W-IDgJ^u1LfeYj-`65;R+tg@Cw!k|tS?H_%UI@&w=05v0nHq0Dqe7R188rC9LX8sEmzqw+JA3LhvSrtbq$ZbJKqNDL{{{ zX*O6-tkGZw@0=;WGH-$(_b&RmErSshI=NzhIeHas#N)kcjDh^=up zbcR>2$h?n77zER&@{RqJ44H_hi%YHmK+7)QquJ%9l8reBNH8}G=sWQmyAiKM$JSX?NUyHkjlmB^? zxtvHutxK7}zRWY*UFH^S>%7hFPh+pwQfW1n346?6djvUG?-cM4JKy-Jtt%T3_!A)! zum9naALrD!O=*L*T4Z5jCm|8+mjw1RkDRz(n7I0%;(Yn^E!AvgV8i8R4&ED;#c~;0 z`O<3>_|w9tzA3^{)LqT3<9p2M>RL+6zhBme3>GC~4StJ39dXCY4U-~}G5c|jH01m5DL9G39s^`Q}_^FyX=dI;Fy-lA8m?=6`$W`;Kj)o z$vk{O%9|0d&f+7g_TA|m zgY96mOl5A0w8BLT8Q593j`&_@O)5GwmAh4Yi{p>)BO7zU&xFQH?Isjs?{t)t;1?&0ROh$`?yW+x#?u1WF?=ZsG- z2ZF`PL1Yy9?Cy6kLKG$UD-{-UA5db6x9wxl>TA`W5LK}69M4}UQWTkcAWcm^8@fsv zI@a&R*dV`AI7Jn7j*}D-ndC&Ohp)(QZiVlJ!@jp(Kuz#L^C;6vHv%%HT7p#Yx6l|> z5UE}D{Mg4J_CNy7Lbu{0aq)ok7#b0g zqXpYiRWhSHa8799P%R%!cm3+UG^aBhQz>t3F2d|YA#P_cVX!hJPH*(PemaT%S;~!! zpY@1Go@v+UU_$&Zp4%^*uky3Uo@HZG`Z6=k8G$j8)(7dA3DMkw{Yot`F&HadG*&&t zYSt`C2?salP&Vj0P);}O;WlYaY*6*#;we-)6qQrut=q{Ch<;vm@D|O|iPH2>aG&(2 zD@o};A9h=LNipVo>D@`|A8XWE)2^VB5jRuf2A(C_cJ$>Bj@}BEtD)SCDO3(`BdfC!heXMJfq3paIf2JtUQB4|`o+i2`#!!c zt<`&Tu`-2bc6bI=cX5yDW;jbw3<9i)8&{gV%{NtnjD=@QsSAos*bLq#^Sgv5PZXsF z`FlDuaTggq=;vGO^Ur%wS+cxht&s26+(pv&n>ZX3lPMghb$S3rMbzoVNhdOrrA45U zDj9MK?F}GuA2*19eeiLU(n1oeERC8Q`@y4D z!Et(s|1i)g)*gis;)E+#Qb-YJ%@>08^N@($uVTKl%zd~sdMHZguQcV(Q`Fw^$IEc< z!@p$6?u|M*nW+W*!8DEP(Yc4c)*lhh#<4O9zE4Uvw@)!?FiUM~R(!41;k#Mx`QmfC zwD9jUJ>~InU|TxE8TX?1?Tc1TsAZhgYm0RLaEEOEFkw-wcK_k3;kn{aO!s;bra(<4 z&QW`-EBb_Rs!?^*;SO(uy94Cpn-?l-e1{w-U#@1kyOt$*lH%es0eiA{h&2asnetW* zr6mr%399Sh_Og3(zJ)1o#@ZU=VKs@ge?js^-YJ(G*e6;0%&Tq4)ACW7ws~zND_8s| z?VdwVH-aQB|0uZTdWqEqAD?0gKfi$mzREv`w1?`s5Kuz`eE$Ns0hxDHu6NP5rRCqr zqO16_$DXdPsKlaTs~M)z zDtx1j85krDwjBKpq zB*p~*v-IDNKqM6=KARu4pYT%0eor8?cifiNgh<*|u^-Xzs7_l*aVs~)4S%&qh$g>( zE_lbb*mE&Fj*}S7`7FN^FZk_|qqpZ~tnw8>e8s8UWqtUq;Hy3yw3Qmfq{wx1WsojR zFOAkn1-S9#t@3t+a{u6#dF75F*BxkIUX?|2{O#;$#_vw49v4sG zE$Mk`(ku#|q3vFO`@E$U7P9e38ljIqxmbc3v2#rn7GG=xZ(!z`Ws(TBn&K>ug|9m3 zYcBWlj|W}oJ>@F%4}MlcPNsjF!92PyD7UZl5qJ{7eNDm;C$Z6H_v*<@F1eb*MBSL8 zObLn3N}xiSri{;*dE_Hk5{tqEBo=D?^_F_1`-*8|Q6})NUeK|+5vx;E;4^?1VW}>l z#>$82+0nsvZyZEVABStJ)Qc?$rg*&Mo*FAr_TVoG3T>cR)e0=X7F4Gf9@1)_Vr-t8zQ}r8ApC`w%fH9UJgx*t;^lQ5emCMeR){gyk(#WX`iJpIl zsmR#?0h&Cgp(5|-18ukDcr@M15MT|{2~XBcggT4;Z+YA+kEp}QxmUEKQ|wejvtC_* zp1^OSl#BUbvZ4*gcVW4I`6gpE!h@sEjkH2ho~6s)O%MK#+AQy{$Xs$ zz!fLK_bYu+D2(7%GY$ zG2rn%>Kg~Y5j3nF5m~m`gjSr||5lSW>Tq{iLqx_b`dv~#Ch@)=vQhh~7=>=I0Px3a zwHKjM^4KAsbtzM3)*DA>ML5?Gj!>7Bo&T5Hp(yWr-9M{X%IIDw>wc|X+a8r;sEOaj z2B98gspzYK36_rVQ95S}q2I^9p6O>Zl-) z;Vd)m;xJjquW74Ey<3ZDc=p?{Plmy2$=|barCVmI0N-zsFqx50(5$$tBNMX1Scy4OQeU~n}m0s6V>Hps@JzJve*2dwf6u_j*zZ2{$A7T!(-o#_Hg@Yx*ZgxkrPb2~vrECaQP87^kS`)h3 zWs{EBWfQXhS5_vn0}!m z9{k$mK-}`W+Fx~+(d_&xm2UsYnpIfl?wI5kNj7FXCT#sD3n8gD$nd&He0RGI z_=%C09Bj|uxxwisDOXayWfpEFhrwjhDY$s`96(gIwYF|1jX|Mt)!>skkcw|)sNR}Y zqt{r))ERR_#4#cft<~{J0hiT|zB82n(=)RUk?3n_<-gJSk1Vs}HWa!kxgCG-5iLF- zu)`3=q&i!lD^ZV($C}MSXN)M7@TIen4qc)Uss`ph!XpF%ygLMw&DE~#DwyQuqzVVC z&Qti0W><5oRp(iN1zZ3C9DxjDsD81N^dvb;C+`HDxfT`4yfV^rYpxLfhv6+NN_1!Q zdrPcQ=STh(`nzt~xvf-@-wBodtSXHj2wi-7hI|{b4GF#Y`q|50rW!A94P?D%$M(rj@FXI?>p;djxBKsljRB z;g&OKWlHj%$C$otDTD3dAYWCJJJdMnqhpWMPMUGmcMu74{-%&-(fXN4^bE(vdUR8< z!y~_hwt2}^dWrHKyuyL$l^AdcS%A|Fv9!2Z`&z4|sny~4Dw6`N$5okx#FX!6c4rS{YHJ<6FEgf>KzvO0%}kkxp%{Y&9t;iftqY0WvfN+Melge z82N!^iTlKzA%*Hv+p9XZp=@5&J-fxW*&7+svlH`z_zPL=1`H7Xx5}OA z!p$<@5EO%`8e@c!o!bi6wkAl3G`&2mHt*Wq9KvZXHWqlLl5JW^@v=6~CeM^-{MZN| z6V0fkmJNPD3B%OhnN8Y_gA$ZP9y-ze>As??g+c9w&}M(RF2j0(s6W@fa+C|m1?V}n z(n)$Mv7CV&3ASTEiZ@1oWj=Q-KGzOWNSN5HQ<~kg4^fty7rFB+eqF!$8>N@}HclVD zMrhg6+Fi?Zx10au+1bI{G>ejbM@O8i7O@9|hIpT+XXjhC zq@aw)5eGJ-H&&e6O;N5;oWw$3jIcB{Qdt9X*GzM{y9NK~jAtLL{;=AmZlv??d2$Md z9V{Ik#>Le|+&pTrGLSkQz{F>pA?;|Gsp zo~hiiTLDTfu|Im$l2$C4bIrYGgay$2KqBE)2_ppZ*^XKYQg#53j4xu>gX&mwFPl*L z_P4yuH4^J)?-5L4EV|kA1cTThv&S*~y42{z`r7-K;4i@wzQtB+-?F)LAG=pdt6r)s z{F^Tk>L=CDyM)^?2m@~eQR-*{aBNp{Nao5z_A+`e0kup&72zs)?e$72>@iLG2Uh;U z^17{3xm7jAYRBw4n1oQJ)1q;20C$M!(dCwtU9-Buho!2mKonDBU z2B>?&u|V0l&&@rn16b7GI&9L#e{vLMIRh*%MREmV&r?iB7l_eZqU~BIHjjC9?ZI~(=>%q5<&x*;X#pB z`hG(vB$dz4YL^PbHSlAy>Qg+de@nqd_^BO`7;4r|BHhiq zI5O%+=(b_xK(AO$B#IYKROH4~_FGwpmR-=CH|5^b;WrJYBQ$feGDp7@dkupszzU^3 z|AxI9SvlSaz2`99a*6!UNQz)07wyLXwvbsHFHiW7^=#Cygp2wal>Jm)|Wt@luMZU6Ov{0UY5n{RUQ zxZ}wpnvwqdFLYsJ%a{3gY6%7X-zIe!z~jVK$h@Y)Tqr~*IdgUUUX zi;}g;vGs%7j;fZXYpyfK!mw><&a+=`5?FFu-WxH!NzM*qz3?_I2}lY!J@Nl2*Y0&a zZ&JLOF|QwVw4_v8+p_ZYx>d>PBA@_2q(HEXI@O*w_xXAl6J&OnPu+AJaIevv+J z4BjGuFMBDQF<>T9GxjNx2m0vHrx8WQWn57%&V5wm5;0E0L|r?}a3I+l44J~v0a7b& zBeAt?+&O#N?SD54$9oAxxnh3e+QG5oUj9`6F$!ecN*CkeniU`E1g7_!m;?T;QvRF^ z_Pd)z?iJkUf3YM6@#NWOEItnBysAYil&JN?bYyLmo4)V>SAMjKsU}xVaI*tTl9753 z^zCQ3Sx(Jn_SP0gilQySNp9ng0xE5Pds1&2*J@Qe#!Axc!hV;>eGvg}=hKZHO6_=kd_1-AsS`UX@eS+1x9qu8klrZj}>~+#XMtl#ox$vKLc>QA< zI>lnV`QyC0C%*cc~0s zo~ElMJ3^<{VYd~OAKKk%+f2F|d))IQT*fDFEOEYA8Y=)Ju0_ET!&gJ9o@W!!zsWjv zz)yF2zvR4<99I%sZRsP!+{&_P>p?>PEjPeFHWdykS-oY1%-A%n1wA_nr=_w|F*k*> zs-b=%RhKkd%3Tw_e&w_cAnAM+&S@Ye!K$wh66HCUu^-{0eC}K$8Aanq@@Q6W=|X77(fs-Oi*3Ax|K!0M0NM)Q z0qMycwj}P}-9d~U5=163gYJe1z{mv;vWu2uX0M^XzV3$;Y%#>!*OSVkL7ntUu*nq2 z=8Wzo2s!+P)wNik?Wpwih4ZdE`eQNRZsdn%MAG={()aJfG`o zT(qQ_`(WBpkwzyM;$&8v9csmJaC9_o1HpQ4!3$(b9yU83zBW60C-ag)mY*8ru;dfK zjH|CfyCjdk2E769L>=4n>muP<1FdtszOKI@_d;GvNqtU(R0v}qLaaTmjjDd6F>|xN zCr0_ynq*0nq4j^%+-Y)=Cks3gLNBb%QQBKB64}>#h9FN1FLlvy>FXhsCze|6nDK{} zTWoBaCOY5kH#RnQ=<2yo;OdiH8xJKy=0&bd*B;!Z+Eo`0?aF$;4BE&h9g-fg3!=M5 zwj-da68z}sC;kX8LzZrE*)zMW@VbC0L(*|pLNwUk?6V zGalJI(@&?@efo+%`#bjz=^k1ftcUVE-kqa10E@KgTC14$E&MJ|x%7L#j{tn9g2`8t zQrBp{6t292A%k@it&SZ}Bb`B4zzr!<*cnKwf*KnXt8je(Rnz{H4~ zd%|o-0^7EMkKJ}We<bQTj1d|rS<&J__)$MV!~ANIF2nFwZ9XDiuL zj)NYDNT}yH(-{r9(txU?3wS%*`;?VYD1$95f07%%4EzxAXSdMjz}Z_J>9?=kyHiTT z2q)nYS-xS?bA+!W#oN%b&K_7hBPi`}Y~GA5T@#4?|ivWF=sXA(K48=9Grhtvp(c3oyu0TaTbd1+tBhhu|f-0@;K9CEKaG} zz9h<^Q5Dy(bYC7ed)~^LrIOn^Z$>C=b6BW%Fk?C!w<;_;m__heH zw3Brm1+15UEG}LhXdm>rd0njy8%HV?3M`t+zX9Qy>7Hh6sCY!b0M7@D^cvrE1j@WG z?_lNq-eET8{1h8ENb4my(U<+_zi;H+cnVS{7DW{jwfn&*Eg^s^(}^-ngrjFn z3-W`O%-ut|3=BR8HUGCX5C6Xx(p%rX=(yONw!OTv?1pjait>>k+jU>LweFm|??iR} zKB20(b{ojfAipK0$p0Ty4fuiko(Mt0YBr+Hb3JM_z1Fw;|3k{KwJM#&w8B3+=1Km+R@PI}>TZbv0&aQKylxoYodj>ROw`8p zrlo;W6P@?Fh~ul;0LKwAgU4kt&^wgurO%#!GdG~&p`ffOQP&(D^ z|B?lmFZj&(h;{iF);nHfFQyVJq5CE=dE4X_|ci`ViCUSq(|8_k#=>FH6kggoHy55p`?(mAG$~9{F ztc=n%XC_|_k?ULC+r>&IQJHL=ImEjwgVBFyF{FPX^QH#fXDcX~8go+rRsPgX0)fc0@S;nby@aMkhSO1l!++IcGzHF5B-xF% zI?RKjT~%q`X#a&`sW)cNdGp8!WCI*5CU9%_W8OJJ@O^f!2Q{At$CA`_N3y^fnz1?i zRkbe$CR{$OPJ)w3BsF|s$qW|`0F=gP8XB`}^z4^=;P(ql(K>Os;8zC~*DnA#0uW;| zF<85g6xOrx=uw19%?Z$A5>OIb#Gb|PkPs5dSjGo4UAa4p8K4wKtO}z!*6uuL+wO$tK6-D(;-W!bip(Nz3ubszTWcK4-ecsqLhCK@So!THVn7CoaB;b*DR! z1*(0)rUP_m*(?Elw>CCBnPm)mHrT#pNGP!SvU%~yWE(jW16HW~2HH~|cH&*1GiN|q z1~RH+;O0HdeMvvUYp<<@Y(HoZ=xMi@xXhMjkT|2+dbOwmJK0x;AI$4StK@g`(C1$H zGzK`f_tE25kX&_zW;g1$3BA*{ZDWwUE)J9UozH`Iu6O#Xf^JH}%-ZG&FOCL}cdsM+ zMj2o8_oD9gsIzg&{n#$yweCe9M?Wz&CISowvfdvP?ox1rsA==jE9Uyq&rII@DzDtf zO7WKzEm#7i>L@O4C4T&US%)CaVL&>3E4OH>LsT?S5Fz9A`xsVGyCQ@8m((7`Rueik zI-4_$x*x*M!HTgS%l%l?+hYk3i+`ZQE4e~CcCn&~q=boq4S-nd6)H0DIE!yL= zzvoaC`|c;loaY_L(sEK}8=cRA-N@Hav@AltCjPw?_i8+*}npc1qEO3j^w7h{{d* z-xn60t#zb?cQax32!=*JSs*;WsEfEk_)RvTod*BHcBAeai#zXyMOK!iLVpnJu0qR8 zDm<2mR|717cV~hv$AbK#DsP_-?@})=gr@ZA^JBjB&SBV125P-F3YOYSvA7Foq-lW# z?`RZ_0=%O`%kO1su@O$O4yg9_O#&COGO zF^`VF%JqS4&g~5!;2VI~NKd`a0#vXln{z-nmOW!+W*y4jq0I04@fACBE@K5=x1w|Z zPzvMk7Mof85*n#*ctHXFYcsK05780z_hZ@6R2OKX!wbIyYG4A<_+=HA zXfmwuq41F>?ZW{eG|Df87hh-5R5Ri8^(zi{yT+{f59Wb^(>U9`0 zs)zj+7As|F+b9s|^wQa`s!GB4zgPu<3!;<=L?voYasfspv^v>ROc`=>Tv#g3#ukMq7XopV7EF&UjK9Vb_Q zAyiO8uY9UR7|J8*Nh47+e49FK8HZ=PiLZ-^VMQq zV}Asw|F?|7lSkh@?&*n%rJgs!pLu%ESZke7JB3wmxxAxabr8Uj26z?hpZWn^K#WB5 zaz6-qKMht-*1edVnee$uG)Y|Q=Tv^zFeG7r2+tTsS>%1A{=B;wpKbieYmn)S+a3;P zZxVCoX;-;7L&KP}{CJQ8g*&X(f|6d3H@^fMjPjMXwVZ6%)SASN5I=ul5$E?Mk;>+F zgL=;s>$jV}-9owc_o%m*_jhOROK!Bpf(2S`yLyk$J$JmZDFvy=MbCtt7bRV<* zqg|3p@2iZ(Gy-jd)OnwSmIke>(AuQF|AkI5;6E>A3jI4b_KsC06~n;?aaQ-1_appHkm{YWxhH zeYImZ%;$+eu`}lq;!aogs$o115B7Zp80g&R?)|sI@o3wC=cfBsE#Kkg8PhjA_4OEi z=RbXX0>8YEf(*dic$AaQm_EsLMoKRL9ob(vX{F2VR4CMtdj)4_-DIOP*t9+5HmaPb zLa#;neY2&j_@7cB?w!>gty>wm6W%v%70~m<|MaB~oLwEP(wJ9xN$I1TyN@c8sz6c1UD?mWg$53tP z4Xuf~`kI>&GynyxPE$GjpBR4~Q26hddmL%AJ7WqY@F?G-5_|J$d-}cqIASBh3JtDJ05$}XroQ?&~4=_2W@?*@m$cmTEE`k|4&4) z8Fe+UMl;JW<0l>;f#|+3zF=R8{|waNv877D`>F{#_t1@^v+i$w8B~$|-$^gC#;mv> zGXS4-9d{}MZ=_=MX0rd|fWo@xnR_&~O$Gl;ZqT~t1uL+#h3r33&8~%LoTaZm&+WP1 zGkR4Vi9jCvI{p)+*N%AmhwC5v$r_aQhfiyFuTanK{j0-g)u*F@(9T8gUK6?F2?5LP zevP-i-zEQT;hT*Qe3gJg@>-`f$_4z#n&h?Jb@+V^q^n}DT@vC6JU~94gr6_DnXTss z6yeP77|v(5?P9ZR_^GZAm*ghcNw6MoPKx%^(8GPa+-87dh+_PJw!@aapr6@UYd5;r zLa_=H_Fk2bIsZTwoPY%%#hsbD^#6Y~Px)WI_a9zCtID~z!s2mWJG`I-B)%T=Gd5YJ ziHFyhnRw*>gAv#pbtZy+qEU-f%e$PnfWwFTKN0=LC6*Yzc_@57qlp6Pu2N+gOxgbz z?$?^L*KC};ip3yQ}xi}(EmLI`c?2jdt`N-y$NhA@b< z*0Z?QF~uW*g1IyndLJ$nCX_w808f7qPfgYqKZatDU+Ud?b*@j33DBv! zOW(gbuSqiXN1}BvcCmlhV3aen5K@XFUgw9=xYRK`Qf;l%xy0y_3r$*WIAyjc$Mz*3+^+GD*k&&0hufliLg zr5UJ1QFie8=yEWCy>T3iXl1i&+5b7@6sRQ)%Qv*_B$oWfAc#h3M=J} z;Xl1vmU-7Y1YO+vn2w1ZJ6OJ4cev78%kZl(J1E}+`%z4vIE!`P`ExS1#dfgCJvjB= zpbXp~TLd9;3W=3rCJpd0npWOJnFfT{W}Z>L{OzR9J=1UaA(14B(J$;gr)|qQx^xmo z4)Z-JZRd1mw~6DIE@d{a-M+1fKds^<3QZ|4Cd-p2h>YtsD%j6H7OYl#CFh*F{yDd4=1d$7u(y#s9UQ7$Cdy{18^knVH{L~7FVvzebn zno-wk6x-`EtHf&U)G8{0FT=^g74fD0kZc}iK`=X&!z}Fc#lK;cUHEveM-_hZoiM4t zspr}JpB^l?4cVypzoqX$HW9SG7O}U|G^9ZqNu>nKSZfci-zo3T5WM|DdV^#XXlG8C+J(doCKzxZuMIdqQ9awS&Br6kM?Z%B<-_>o~u zP-w2z%hH$C){Kf|Qyaa39Wg_V(q{7ZEj-=WHUGgIqZo*-j@pt43xPwLf zg8H>PgyO+Mb=Di(c$_#U1-A2POKn2IhJCDY*hLFa7Z;09Eew;x$5owMDw|o~a_298 z|6(;2FbTk}X}HI`j8~=7v}WvTAK5iNG;lA_rZv(~t_aiv6i2_nxwjZ>^F6M_oEMJxocAx#e z5J|o$2oG~;1(n*Q^*NPNAsv+XEeOBK)l$TRFqMZ5cTc9$;_=FJUKO`ljypJ;McGU(tAB{OT6n|RfT&F6i0462*_2c=aFBDTIdn{u@L}WGY_1R%(tUd zLlGQ=uq7zsE8n}A*hb*?s6v1)0uAt~LwdRY?53ROJM;h-^;49`n&Fx-R_& zioca`nj}HU%2P)ljU!^hU3uxhu9#kVL3{Miei1;rbr~XIWy#eHJ*rzS8A+he(YS5g9KUDwh&=$b^8JiQ{>xSh3F)iTO@O~PRw0&`DtHs8MdBcEi zFsKU5833aOaP87*&{1xylK-~`e~eS{^^{3CDImdUAq+T^qu*~_y>gW3s0yVHMx`lw z_iELRScR;q&TKoYxVZBvCoPnLM&JUM#D4KjVs*UKy*d*he|JU~u(G7CsnFDOv|MB{ zr~gd&vpx~9t*hd#z*R>Q!bNOl=b#1#+6Hi&C=UU@=p-z?(X*Z*ZDtQG_;ONz&mNqtiQ18I$ z=F)fLm0rC2YYuP-9s|M{dDN ztqTE*yKW;dDe0OeS+q1Kjt*?fW~}65vhZsVQaJ`&Xg*+Wd~I4_g5Z$ zLvb&6Kulr*F;a)u=xt95D<2OZeqrfINQG&IVHwpcK@|-XGHeaoj87@p~v_A_A9GbMr1uT8&I+9nx-71is_G<9z7I^ddg78b`Suj@- z9+}KLk-ada4P%CJYFXz3meVP3&B|7*HGCuzG4!S6Rs=Ffh~HKfjs1V*xZL zcg&81`61e_e&HiUFc<5c7Apwg{AXC&uKwGF0>E3j3f=dN?FMgrbm(}@Rz3&ck)bp+ zmHhBqFkw~kL%Nz2YpL)dkuFvbkTa z1-!c)VM*kd0rHu-t}LTspVCM2nHs{RG%gLYnaJDHFMRwG*EMzrUC*KSxzyP76U&Ol za8pxyv@DxI<*Q|BC0TY}n%`PQ5{Xbf&f@6D6dPFiUVsHJAz2QEy&#F^gLm@sKkp`N z=LeIMimPb?g-NjB!5u>fFJ>}EhgfopB)s0~RZz-Ce-mWp*N0dLHy_p|6DdpAaWhT?CSlq?JW5P3s0FpOf zXgbVOz(cZD+cE;-bfSiWF?2B+MZRKB;Vf2-M+)B_>E`x1aOGY>R}4V0T~vvxsm{^O z*%SZ97lrFbk$tfym;Rl)O|KqHQmi~xcojZa_5;PP-dBkyRoG>bH7Q=D+F;w$U?PHe zJP-O2@*sux>|?rUWd7Y-Uj`^5v2CQ=`cP|J|;TYQ-1It0<|Oi+YpTLohe_ zY>IStz|luELq7RSa!PL#1F6gbpl1F8C)l`#)1s%%Acgevn`Q1CDv^}AjAfH)Ca?7(2bL8^p$`T}+Bt<<$3%d>Lr?r7>;6)AB8VD$>4%88^OmX*m$t686sTs8yHHHgPmJiRaaw7rtH0N4Su0 zfLAn2?7pB^*hv5RNnTMf{6~{5I-1trN=*jy?icSKr#0Oyfhe^0WI3`X?qV*n8>KkfA2%UWP$?r1I5-0~Pp~GcGU88{X20p)KR`a@amP%cUB!^lv`jL<14L4vpLYI=E&86 z$NA&fa*T(|Cq)c_-r;yWiq1@!*HA!}QjJw+p>pi06}O}N`J;d73-i>QE`is-1J@C( zqw^Qre!x)9J|{AVr3m}#>r`t}ajrsVuqEyPtDK7oFy+@aDL+7OhV!X(c6xr1xp~{) z{}a>2Iw#2IpZ2*c!N@vQ%<*B$lf6BQM_jGwto7{5vQ5X0)V-wAlgp>|60kpYpz96G z5#it+LVWCFQ@f;mTdjX8b)efWu<4Uo!I)HkyrI%=@-x-5bBy1Sb$|?Wqh!SgveUb9 z>vL|?Sz8smqTIpnVn4xHyAy@A?y$T)K+7Un)gU+l%p^;Qdb=zlAu zuQxoD(Cm8AEt1q&fh#1w+WqZ5sb?pe-UaQ3On*62nw@2FF}4j5(GA@+%plyW>&An>(Txhv&caProkb8KbJZ$7k;=yt|`b_QbJCq=vYXv<7#HP@z8I%~)6K#O!0C22pl zWhLi`Osod|XoYIobeVN@;aOMix|r=9>ru1U=AgXJ-x-CiP{rVNGWuvZ>o#@%XupJ> z*>>u~gBRfL!R$;|Y2FwJSV8WLboGx3SO|ZHZb^~*uDV`v`UY@u#30W4a5hf&NdFH# zJA|ixjt{bLXTQ+BvGm^3Ooa=VZ`03hO*%xNU2;zTA_&Mv=_HrzzRE7>dF zoIr%ZPW#8CMIrsP`rme?m{ZgGi}x4U_=zQv-=9x&foj+(Ct{zxI?OYE(g8gsdmF_4MlNyMBI-uN{p#D2k;oyWxXN&4R@lw3X9gu{#Q%sKLT~-NP&Bg9T=xg~a9TDCUEs|f4 zKt^ZgAHK+ec}s$J3x*@csNew@_tzQuNS|UY*7*TR=H7sX%@>U38X8` zap>-pT4!&;tR(d-1=6#t?ERT9S0j)q-M4g?NU>^|MBDvH6mC*ya_AznEM77pUlM%P z@Z-;RPK9lt4mhX9G{jTa?LR6_S956nM|CMqLN(T4r?uE>%uh!5WI?n#xp;7&Aar)Z zHdqrXZ8IW<$Ttg$;>dYRXT_V9;VT+LUTg`o@kP`N(ui?cf~##rGyy1tD7)rxz|BrX z3ZL%339_a9+frw>G#@j=kr!{ha?17c`?-0M31+dBxC0{f3jTRNWj%OX!QvyGqqa9s z=%>hRX3ND_od|oe-k3D!P-AniSavj=TubI2Uz_UXG``T_ok7nxNZ}1(rgwcmTaP@q zL+#L(n>^ggN%NeIax*S0`?>h2Dn^!$WUj^fOBG(4ipYD2dwBQ@6_uOy%#kf#tl4ps z5KvQ*t?h|u$k)2n30hsHL$c5VAB4awv!3RFb3uAXcz#mScwFEtlyc()c~t$GrJ?dHqm!^yD1#b0bVGp=S@Qf_BOSZ|wS9ZZc+ zZIRQU)_bvj>5~{?W4Jr7AJvgUP6tT;$J3a-cepZ= zQ>1pPufN*HuW3NC(HP)3fcgJMLG2CH=a}v69VLbzCrn~YOgi^cxS&X+X5_OC8`mk z?VqF4xnI>{LFitX;_~}kgk{nzIEe+~T8!8z`*wQXlOx&BC|tW?$^yiP#RTzh2@hp@ zKMoqzZl5|DSYv*ayZyr~AOUKk$B`u~MBM#=HV&I->c##Y3D7#9;O6eUD|Ep| zq19}$XCieR{}H|7JS0uumUTOl{f4vm(>gaeZ&9GvbO}m_`riE{@J~_PO`+C!k)l|L ze+ysLAmP4VpXx$^UQ$Nbt5&c6jq>Hs+9i0lWUT5um=Vx7Bv@JWDYqG{;r05x@7fB6 z)vIY8&%1)Ws44Z9z?xi*R4`ZYPk5BxLMtVvWj|erR_O`heD0_y;y7yGuCH%~){}S3UGwbvjRZSp<&aH81+3EJtu?5c^&h}6F@yUr6n&f6k zP*p?jm23i>opb41#TQRZYbHiuaKZDBvnT;AW-@}{2{S|fnrimtx#C-WGYLvn8xz*@ zL#!w~uE<*Cv8f0-?F%J2wSSsAv_Gk)!d&ml%`vybMUBDao z+xVdw&zihEh4K_yCs$E&x^t@j%q2Ek6acW@-qMU}%A4^@ZMGaECm0NoEZt~3L6%L^ zS~6K-S~ineJ&CeBZRW-sHM*PMo6|U!>jnYZaH|!FoH%lfye8$ibe?G%$>5CAtbm#O zdu8#vn7?u_UeyoIKe?`R#um# zDXuv6U2h_l0=}azLCA|_Lh}2tUslNwHG6K!rufT#fNw~#Zh1Eo=3dNWS2FU9>QHj< zHy~IV@g_62AL@1i!`w@Q3FGTN3Jwj*1FCtIg7)dDxD};$+0B;cX021|`PC`ZtBK>L zl-x@!A30E%ymXL|`{s^yGrsUPng?^j)wF0(dzqnafM)qi7H&Tr1iiNE>MJDf$6 z=%IR#23SKm3@P7P%qRx2G4ODF>5|QhjbkHHUboO*e@UvSLJ~w2WLdaO7#B$qI_FuzHk`Ol!WD%ojMHtR ztxS&1Aj5r`be%hcbzX+e-3OQ}o`{#qEEI@rW%Qv4?i_I;sjJ4fdqT-!$S&|Z$rL&E zUfB#)yR6Sf4|2bw8V~t#pYCPPmggCRO8d<>aU2n7Rz>yhSxpCiw)$QS!8T7fnHg^Q z;@m8v#=>)sR-hWkpkSZ$4_JN{-@Ccr#&J4h@6m};i_}T4GR3>p#Y;TFwkrY~bSK*o z2u;OUus}M)3?fPGm3eh+g*m>(+Zeb01 z!J+)w6KFnmeMpanzGwLGH$H+T?nG#V$K|}a#a*-dP(cG+VnUSg^_l5W_e()MuvS3M z?-Ix@;^%`iqF>4p-1mRQ>lttJXf7ei3}=6K{;KEbj?`)wR<8svuCmvU?ugXWQ>lU0 zeg*qXI)kX@oOpny!P9)N^h2vbzhjk*jlUy%gOGA%n%|hZZ5{g7;D#?#IuR=#D;$rh zIS$R?m#yP)Gr@Y^U~sN(1S4DUxWvT*#aPZLzOB)U9y4CiHLut>?U1w1*!DZ^=Dr}% zHg~$?m$CFJnxOCg^oI+2;}x_Ajk}SLw)QEn0(5GmVgO%k+PsfeMi02Q!hQXiYYVs@~dM6E>+6G-!hdp0YPBqR(J@f%p6+M*6 zUecTEr;B6|$qniIU06wiEGgtoRkO>*I16pYVT!k|!nQZOg7j*^{MtB;o1oF^F7l++ z5gVd|0<@o1acA=C$%Ipw%Aw~jvl5RqNI{sDNz`?VJUoV{gPM&EF&wf%s9pguL3C;< z&jJmhEF^5FnM_aW{8hO(40^{wpHg!H-Q&&gJlcN8zNIdDYwyXeeCNC4Z*qN2KMh`j za^(%Lt#9@S9$JANhcIAg_xqDel+CNRSjtoPA1}j!?SfUfKfb0RN&%5;=#C-dV37mCYASk1-X`Lf=o>ptH05 z$bIY)eHO{zNDJO6ZKN3Ejv;KXX&_&hDE-+xLBom{^LcoKCo68n7x}g}27;gd>9=bM zrEVR&HL*>M4DGjXg?`{8Q6|3bdIpS3s3WqnXM9N~>m_PSt4oh&-`Lc0@D-b2HNSmc z{aL8iGJ?q%sV^sjz49}KbZxd@cTG>axGe6*&0?gzpqa#FAdRm|@uEB}WLh77Qm^AQ z|576dJhhmci(af)an~QfO89zRp@T`&_X><2JmGEl?NSNhfbiy)}EMG4G9bTS8k$4I} z=zi8<6L(T@_u!ya^V@!S{3-^}$301)dH+7}xgN@c-y0;M}*vJ9mM<9fuY0-U}BxPl=}gt&_Ro)a&q^)7nvf zQ*g-W`NX>p^rn@`RXd&-n(#k+4&8{U-CTO#f78t&*1`WzNh!sKQ4DLoCkbOfu9g}r zd#XfY@>1(iAm+D{i*1&D3f4>}=>vuG8|KBlOa$ShV zUS{9lrr-+jy^5ylHv;T!{#%Kc7jD^`)Eo3qe^FhpYZu-@AFTC13I2Z}>kQO-?Y4)m zLnP0`o$g(BCjT)s*KS{qD@oV8JSI~wcpeJccSP&|r|oC{SU&&Tt5CB@LAKpPZ<$=5 zv7GhyFEPt~TG8)y3&ldv*YSUhbDzCK3%}F(hew0y`N?A{U$w*O|>tdK(14ip7e>YRpz z`!w|*D-*~5_xcYLl55WUmpF|I?QXjR<0sF2Zw<{54k6gS8zQreK zkoagr)l39=DgQH!F>7zogj7iTcx7>A52GBH$(@wkLFHi(cwa51$HSe&s=;a`_Vo3Q zl>mU`f3huDyffjPvl4yD%i_*)1w%`Qts$O20-Dw0igxZc*|&6L4-R@$CA2h}~%Il4D#w zqLH0E*L54SI6LO;QU})1<=-q~-CI94}8@0aFI`g6{OY|U~UHi)SS}PcR1vw zXOslVje-5uiKv-HHUY3hUN@;YVh%nwzguhTxgt1qs7755vDgH|_1jnLBTmMlyNV3h zq+Y&o!|g>R@)4GnytE!=CI@2#G6$csRneP}P}wc26M+SW#{PZ+ay>G0`;KfXB*hcmGpw1BSZ%6FDr*Bk;x{6$?>ix21_K2oKf@?IGu%Bp({W06lGqa}NrliLJ0~w2cNcB> zv;Gi5hOGtk^GcCQ1Qa5J?Y`6@ts0Cf<%xdQT={8_1BCyp;hk9NsBDzYhW-@56Tv}o zHMYe))&Vg`N$2g1H9L9`!1)af4-YRzG&M1HEvU0=Xhx!|-Gl0MSxO-@wI&*+WJ^cf_zZv=AM$DX%NHBAXZ)2nxNbnz zfaj+3{WVc5T&t2Dra!UjJ{-FFdJb}h9dp0TdhN-tPnq=5${NyR zfHD2z3s07(&;z!qT(X=Emt%XdBbE4hJODP4MI;0NZI%>qrl9zL0EiH5>TAUYGV-bp zaTF8#N_;gFcI}`=i3RhOTZojC?mHD-G(mXdGk(80nrH;vYHopBIWLLO^cMw1QzSDi zFkUFMjaWoF{dc}tV_M65JqEh(;<>@j;hDM$6YWw_!G z)Dy#ERn5DkCD#zg;BrIGReJH&t$zcwS8-1nX+=Tis_p$J-riL-_9a|BtaQbG3o}ej zoTu8gZcW$%ZD-YG{w~r!y+dg$752Zd^ikhPz8pD~bC(LF9%74r#>5l^=#@3C`oi7H znP_C0+owu6kv9rAeCLfRy~8Ge2_M3cFiO}O?^*kI;$W#OJRKHvMT?Zia^F;RSim`E z6&i_B=9|O9J|8Y21fzuszvG_Kr|X2v8i{m>g7=yxLI8D(uBaL8Xj_-P!orAIq01j*g^;=xp-q`-k*wjQJ_Nf! z*F~odj=JU_fzmm(6+4lVDt%&BfbAR9NntVZr&BkhVxTkHCsX3}81>wc_O|YjxgwOn z#Yayu{~Teg@R1~)mCH(HXbLA@&TNI9Xl-aI>h#610sjFRL**lY`1bFf95Is_tcaF% z(3SBUU}`uPICrh(KE&|&<5}z$7s=lnuTN?I3RP5H)3N_GGsphS-ivY29CIMsyreK>86+X7Ts4p7XpL@!4pyXCU?# zqt6@rWl-hzp+G8{7856A9Bq#+Gxx(smC(@b0Z@h|2-(~Ux*y!%cmjVwj zFvBjF`+lL9x;O0A2(7_89wp+Jy156+OV>P|m*B>2TC2)N?|lYe9%QSS7G_&NFf2q_i=_HuYMZ4%xAK z1=vu=DX)^Ppp{$6Mo6v_zqVso%Hf5$H~-Tk!*k(TU0|&>m{j|YoC##2LrA)~auX~O z=D(N&tewB_69-9O(0ju5ZmvV%I$+sNwQEw^#Dji4jE?uMg*;prfC(J5*PxuD>=P-e^ z)#k2RbMx_=zaRH<%T>ietX@C(`cLPGdvZsE*W%*_XvyW*!f_Y{(?~G+hVR|p#P$$^O$Ny9AUrdPZ1f}y-=Dl-k2Z>$2&66s-zv_T&cJC~vPp$zdm(KG9 z>V16(PgYUGG>4OEcuC_-a}!DT4V!e3#dOh?1#Qw88&gozEZh}MHURD zL{k8Mf7w$sC3dp6K+gsoD}CZ=)j|u>nlWbY2d9Yn@8YL0{u;ZZbCEV)*#0My&(PUM zljo8(p+Isk8H6uf)d?mmk>syf3etG54J7K<4zM>Cg6~M?Gs1fcaWE$^45vC0a z{>>V)Y{#z>GTA{=&B8=4?8CJ}n_eu`$X`yVuTW7sh_}kIX*ktP1ckV#Iwe&sC|o5} z4he-pQYS95{}(9!he?7)C|obl?qEC}zuJZWB8fseH>;g8sgdmBhutp~&QcA?J%z!h z_sBf;z)vH7fcNUsF7k*=v5t#1BKfV1X5$6x%r*`WZL9!g$1>4`tz`^l5$2UeyF+XF zc96Pb#*aGJ~&)`j2VOz<|bQA14M`RZft zi#R9zh4=TnT>PQb&3h3~E8Ujhz@Hk|igCi}Fmu5^h&(<)rtMXa)mlwoI^kz{99m|N zPq+EZQX2?(B0RJ(39zuYk%gCm;zJuAvjq>-IWT}vMyyJC@8{|0LT%ra^0V`@$rQKW zQ}`NpiQRo0T&INIf_6{*1K1A_%=lLyFd6SDIO zbdjxWH2vq+MN3Z{%zg8|EA@KJ8Dsq#2R*3b9;Q?y{$c_;EEpqIB{qe|Q<{Q~gp zkkNZLbG-g7BvSF$I&1t4cqCp^?2)I?JA1lw^2or#rIxL2vDI+x6uEuAH6UKT zJ$(2jLbnsv3E9H?E0&4zL#emI=#a^{qaE2I(!O*Yj&cjBsBD?_bnxF0_)^N5)Iwi( zM7iRg7;h-mAzxSc_14q7le-iLBYUYWoTWbpw!L>_G_&*7P3ky$%mwP- zLwchwh#3amre1*9%=J}-W9chN@*hd_Rh3q`b5=VPin}7t@mrp zLX>g?gxE4vcKJ^mq;`p|I>@8li>_mSZp06I1E)MrbC4y$RVXSnBZVb!K0><4uCbvD zJ41J7*Lri7N)!~n+ev=Q#wClC5`GJ&N#zI<_5)#jRyxnYDjRul(^4>bbg!?!<4O9| z5oW0VvhQl?JXTT5M&(JM-+fiFQ0M}4d2iP>l$%wHalMQQN>W6Ua5=7*v}zq zE@DmzJ}&ZGE&T?Wk+eUNYZF2bbWudHF9>_!q%W%KWrlp=h7 zb!Wy7d~;D_c-h;+bkzwwKbiv)2CAR><+sR2IjoRpfV*xhF9T{x{3jEW&aD#W&Oy8j zXo&YmiVUt5+(~i+$-oU(hEZ^_jt$hNGT)&Zz)#Ln*|hvq9ZC9Om0KCS2lYK~kp(yY zX4xNaQMrNjL;(~@G2L+bvW@S2o-~J`JQRA|nd1J|D8Zf;^*v={UxlPEI=Y*DcS>vt z(rA>awv7tnY7lAvU@00h6pk<@-8?-nX}YRi=WkijWjR`5|1gGsp=jT0KWB{NlvGk< zGZYEPYl<(OfAa@24^Do?h&cwV2wGE=gjqjy7BN_LztOR7RB@h+y-|$xYi7GkJ@wo4 zH2x0Lb7e$qy-&D#c^(o+E)1WO4Xw_=P_6fQh{v3*o>aNyK=TeqPLHhCF>7Dd0!|T3 znmFr6Z39l6Pxj^+PfH`FF9w<~+KxS=L?bhevny zc8oI~+FN?Ip#HuB(+9xcnB*!!TdF1-mcy-Alec@-@3ZNeDL$PCdfJ9lXYpf}qYfp1 z>sSu2Y`4v0qN_BcN8p-5t0&5;&bUvq)p<@Jv)R0jz}TN65a{%k1tbDd-RqB$k{ z4hp{3ODiEp0-2Ci+ud@ zxv)0(#)vU4df}vNo3BBXpJ^9qIwIJ5XHulGiUgAI2m4!op3cgqL5??HGlrf`aoI*q zk5pRyl})~3yM){mQJr6NMxy80x>Gaa%VxU=F!L8Jd3Tqn@;X{@5fv{fQTcG(Q&FK z+>7k@*)bC~+4p+D(B!Ry=N^R6ust#V#n~h5VhnAU98sgd4omj?oKtICaCYOa^Evki z>YQlH&8X>_#m%^QGCKaj!m#CG)WqHEBTl8S=m>}zYrI3;5AFPwZ>=pH`LI^P&$M_$ z_(45%w?ds-v;yBc>*etEdUq*~Ieg4JjyH$Tqn2IkoOQ>k*~;8wdciY?Ca1@7^Q^_ql_dz)bzJE_#f) zJx*Xu$SnmoBT%sfq+meS3QL^&Zm>tI=r%msau zv&`-_>AYgB6y?x`cA0r?l`>@BqTD9p_*Q1+%@bGLyj3ExPTFiG+EUy6qp-(2$hRK) zN5TGUfv>B-=X<*=-~96HPB|nqXYzsnTl>cN<;;oz7u%uq`_o7ic|K7`827J0>Dr`K zk(R8y5nY-Feqs?ss6i^!Wk8xV>44GCiwSNDstZ`y`LdU1wVarslW)1JaeZADLIP=# ztZ5TT&E}n5tlLPCXaaI0-)&NIzgU{(SaeJYLgy5qzpZ_0G3QGeDdyuKE53Jk_~G~J zNa!mXgdSzDK6~^zgZ;+pC0(m=KPpdIPukLtIutLG*MfP}>*Ed#e_=MeYgfR-AP{7s z^8B*1;8foxlH_QEmgDLQ^#YTq$1EKXKW3z`XSjZ7Kjj_z2vd@z_m;fAirZ0Y@-)`& zmaOjqseI*o#q%1HMB#f`PY;Y;Rz?yqk<@l)XU45Hx_~D3Mp7WwdFU_QY(2_ItrfiR z%y}HDf*Qm5@)~?i%a@T#$Ny>vB<{Q}j!_iU@;((#eZL+E(L&s0VRos(@nA5h%{7pFO_H4m2!6dHc%`K_8u&GP85#nl(-al7VSOIh5-0~35a(Q>E#2NnBATk&|8y6?opqbqZC^!tJ< ze@}$Ze#41MEdA70eyOwP!ZYe|I%kf2jcekS|;I}%?@5Y$Qjd#YZ zn(@bP7&UZ`3m94JfuEPan@2L=I+JC;n%8X?h%8{rvCS|1D%Oj;k%s$>jHoKrp=o^g zx1z6KSTksBO01-k%D-DmcO(e`C>>Gxs|bbrI(wXcGt)(5Du~wZfKHT`rfdSErjB^~ z_A@Qn{t*-Vw~QtBh+TqmiIi*dfFf))Cw|Ley;imCcF+ zo}`o>n;K;cxTsEkP(NU$;WUj^wO)eym^yB6iJ8{wEEeK5H*cFA3NuQ618G}oUzgaM z9r(_HH7lKWX@~ZHCul|8+YX2pdTxucsF3BEUK^&%a{n`nfxRLy0bH`Ty3Whaz2PPr=-;vU20wbPpF)$5(Q)oG?;eEfYl!+5IS{CBi%@suJ zG$^lvIIUk-K2wt)ck9*eaAqzLiuQVa*7$@G(Hs0vdvJ#v=ZhyLD$%G$a{N@NQH)%b53wV7jttd{9lZ`50B1eZuzM*Lr}0tzWnJ7|;9EWR_n zG4+x9L&dt3Yp2Ckkp8>BL)YpXqL4H&KOaMtV$e&duCCk6i=M3-jY^|p_;E_gi8MG-UkRwM{JD=#= zcT|stJ;skF>N})o*%@a-8keY>mggzmWleM0^X8>0H5lQ}l_rY{6qtP33sPk2tvUttv-8H6Rp&qXnDlaJ zo=b3C(8554Rv=ii{#FZ)77S)19>hdX;C5lVaq%@Wv1uxw(}^@YrCej^byJSE89>nA z(XV4_u3Eit_t&rqx&P^-+ylv+E`mM^$rK<|0F|aa1qt%-; z28Yb08X{pDw0J2@q)_RWn#%U)*+|nHa-W3H`eUx#vL@C?<$55lg6fC#AsI)k`T<2i zTBY~Y^NFCQW`fiM^Do0IG!F(5TEbGFjJm+}@RMr8m!o?I>_?!Q$5Uq?C^}A@vqLob z_&Q61-qFwlk(>!8i1Hq4hVY1+ zq9xlcqc2O7^ZR^p$dFeN)_qX9*20T;=+L?7k#2`RVHLMpEo!ED0&9a)L30I;hvvDq z`RuCj9%*-a?{eb)i-rV=NvIfaK7~@n!rxoeCjQHZAFavB18cw6e)6hx4|5$ZsBxU= zqEv7O--Z2qP_nXqC7;yY55iiDkQlGF{{|HuwEd93F2VHh*kZ9yg50<~TA3%MMIMM& z7M*@Kp?8)kBVi=&YLLe{fOAiiDvu=X?Qc`|eqasJDY%ZcDrLG}6p$7;n#_0^`Dm`A zJe>rweSbIb>!wc~&6JfkD)z@x)p)MND|=4)%Qz-R$&6+aLVcC1+5GPOVDooJWng|nmB|_fGeAbau5+&UZZ>XxRLDeos!WFq z>V&ED_LWttLHhbbaMlqohEUb@YLA?S{TfSaM-3(p5~S0sky}o>x0n7(TIzdP+N_}a zR!$FuERCDIsl7vo4#i)g#*!x*Zw^Z*_iA6OdO3DZGcO3P_o`51KcE$_6wKHjEnez z+8bZb+&!u|Y+PPta0N~h$Dxr-$P=MV8SvhHwc!xK2W|+&{`4pTX~*dk#j?s0YTe-@ zR{2Ej%B=Wzgw`3q0>N5LpdvhpQ2nUrAI>ajk(n!Z5z0s8UL-I4yKP~^a-oKXN6qmr zo0=%nrum^=e7Ks6%6MfSggZN)q$~W?Ye?) z*JS>QM)UZ7x2!Zr4$T4fg15j39P2(7c8?Vsyf;sk=*&PQ zn;|TWuowfcZ%Uof7DKX!rcxahu_lID@YV!n-6L?qcNSj{tm2ekH zB_qxN)7XM4cEd7Eb}_4{O@ov)GyM)jlzS|?!`Xjra(Fs0xDC&OuXrW&8NP1dwIGJ9 zM|%sdo#Z;@+RIF~4BM-GPU(b5s2u0#IGT*!I4@YF-1Rf%HcUjMKm|^{T$j?0LY^9L zz)nGFW%iv*Va`v&vu&*wVvi_u%Y$Goa)5Z4=QkuXFHXL8lrMoRC<~<&g81_R6HrIxX7Juc#?G` z+bgX|optBZi=dC8AR*L=**3?U&T^Wq)VwjmRpHKRvcQWkdH){=+E3bpC&DLxZ*vxk z^1YQYVDj5dh|1-0sS8cA_M>t#bPb@cOM@qasgh`_2}9JX?RSD82yB&LNBVLc&!4&n zBJ;yrHda11ZK`qk_oimdtbwbf7e#~%5D;=A8yX-!zgio9Wppfe7Q^Li6Txk1Sq|7^ z@~33>sY0RwHMpI`_YCUjlN=xGO<<1<_g;ehd&|yb`qNyd3 zo8>|*5yQQp^G_jm=!ZN|)Vu=4vNbhzg>b3#^5tj!;h1@a$@I$XEJ6)lZf-Olld+s8 zFvyW{vcVeKY)Q|H`hDHY_t&K<(!hYg)PQtSWFmW>%^RCtBc^V!4E2qQYaXmzlds-+ zKVyM_W7i}sE+>EXdzA`l%j__TOmfgXw2L`-416De;r8Hth6oQZ>r70p5>Vnzip1F% z@RZFaZ>PuQNzZdidTl$AxNy(+j^y1=4aOXy-WIEn7`Ld0x)!ivVHFDeV#xPdz%Kmff;s7M!{Y?MZloO5r ze2`=!#BCGhvhAaPP}Uh0hJp*F6LqCda^h_@9W@RV^Xxt_v&`A~BHOV*JqG@Q>R*`3 zv1y2_k)iT_|3`?H(q_)<8_jUS4_x`&OaW(dwwAO`{Vdgi?4{#@Y%M%>UTwl%1@p@m zOr)?DOay^#G1-rW@D;dL5a1_Vmrwch)?mw>U1UyrpF2jU8OK6gwD$WmDn~~=t$If~ zlY$ey1Pi>d2+JT2Yc6ofDZZio~6$S8iJ{iM6W}@5?4f z#+AYvW3g}xS_XXs>VGXKM#Ll z`_E8XFKdxHYwcQ|t2H~*(bxprz33j`WS;0~48(E&m-&ZxRk1x_C)8d4ELQI<7*nC_ zV-PsO(byruaSzlTQf$;4-ZeRqUW|0Q5{y}1BjCt{-W8K%aa3QzSG<_Tj7O(gk#VP& zIfcSHM{ydagvK&~mR2OwI*j)AhZV%1^yyB9Nzg+hVKP^D>GvTYfBnicO0E>Kc2kum l5)9g#gJk@L=y$*UmMd$E6b(-5G5!oBL6dhhD5&@M{{sc\n", + "\n", "\n", "In order to recreate this network, we will design a *network* class. This class creates the configuration files needed to produce a transportation network within the simulator. It also specifies the location of edge nodes in the network, as well as the positioning of vehicles at the start of a run.\n", "\n", diff --git a/tutorials/tutorial10_traffic_lights.ipynb b/tutorials/tutorial10_traffic_lights.ipynb index 7930b63ab..0ebf6535b 100644 --- a/tutorials/tutorial10_traffic_lights.ipynb +++ b/tutorials/tutorial10_traffic_lights.ipynb @@ -17,7 +17,7 @@ "\n", "* Experiment script for RL version of traffic lights in grid: `examples/rllib/traffic_light_grid.py`\n", "* Experiment script for non-RL version of traffic lights in grid: `examples/sumo/traffic_light_grid.py`\n", - "* Network: `traffic_light_grid.py` (class TrafficLightGridScenario)\n", + "* Network: `traffic_light_grid.py` (class TrafficLightGridNetwork)\n", "* Environment for RL version of traffic lights in grid: (class TrafficLightGridEnv)\n", "* Environment for non-RL version of traffic lights in grid: (class AccelEnv)\n", "\n", From 84a7249a4378340080f6aadaf7a8a759df7302c8 Mon Sep 17 00:00:00 2001 From: kevin-thankyou-lin <33344633+kevin-thankyou-lin@users.noreply.github.com> Date: Sat, 12 Oct 2019 12:12:52 -0700 Subject: [PATCH 04/86] Update tutorial 11 (#751) * modify flow/config for aimsum * fixed tutorial 11 typos and updated import modules * Fix tutorial 11 inflow depart_speed speedLimit param * Fixed tutorial 11, replaced speedLimit with max and set one departure speed as a constant * Revert "Fixed tutorial 11, replaced speedLimit with max and set one departure speed as a constant" This reverts commit 77ebb92af71daa38a35911b3864d6f4795e78259. * replace speedLimit with max and used a constant for a departure speed --- flow/config.py | 1 + tutorials/tutorial11_inflows.ipynb | 43 +++++++++--------------------- 2 files changed, 13 insertions(+), 31 deletions(-) diff --git a/flow/config.py b/flow/config.py index 7130f6d8a..94c99f473 100644 --- a/flow/config.py +++ b/flow/config.py @@ -21,6 +21,7 @@ # path to the Aimsun_Next main directory (required for Aimsun simulations) AIMSUN_NEXT_PATH = os.environ.get("AIMSUN_NEXT_PATH", None) + # path to the aimsun_flow environment's main directory (required for Aimsun # simulations) AIMSUN_SITEPACKAGES = os.environ.get("AIMSUN_SITEPACKAGES", None) diff --git a/tutorials/tutorial11_inflows.ipynb b/tutorials/tutorial11_inflows.ipynb index 1a9fc18d2..3b841123d 100644 --- a/tutorials/tutorial11_inflows.ipynb +++ b/tutorials/tutorial11_inflows.ipynb @@ -123,11 +123,11 @@ "\n", "* `veh_type`: the type of the vehicles the inflow will create (this must match one of the types set in the `VehicleParams` object),\n", "* `edge`: the name of the edge (in the network) where the inflow will insert vehicles,\n", - "* `vehs_per_hour`: the number of vehicles entering from the edge per hour at most (it may not be achievable due to congestion and safe driving behavior).\n", + "* `vehs_per_hour`: the maximum number of vehicles entering from the edge per hour (this number may not be achievable due to congestion and safe driving behavior).\n", "\n", "More options are shown in [**section 3**](#3.-Customizing-inflows).\n", "\n", - "We begin by creating an inflow of vehicles at a rate of 2000 vehicules per hour on the main highway:" + "We begin by creating an inflow of vehicles at a rate of 2000 vehicles per hour on the main highway:" ] }, { @@ -145,7 +145,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Next, we create a second inflow of vehicles on the on-merge lane at a lower rate of 100 vehicules par hour." + "Next, we create a second inflow of vehicles on the inflow_merge lane at a lower rate of 100 vehicles per hour." ] }, { @@ -173,20 +173,6 @@ "## 2. Running simulations with inflows" ] }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "In the next section, we will add our inflows to our network and run a simulation to see them in action." - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 2. Running simulations with inflows" - ] - }, { "cell_type": "markdown", "metadata": {}, @@ -325,7 +311,7 @@ "\n", "- `period`: this is the time in seconds between two vehicles are inserted. For example, setting this to $5$ would result in vehicles entering the network every $5$ seconds (which is effectively the same as setting `vehs_per_hour` to $720$).\n", "\n", - "_Note that all these rates are **maximum** rates, meaning that if adding vehicles at the current rate would result in vehicles between too close to each other or colliding, then the rate will automatically be reduced._\n", + "_Note that all these rates are **maximum** rates, meaning that if adding vehicles at the current rate would result in vehicles being too close to each other or colliding, then the rate will automatically be reduced._\n", "\n", "Exactly **one** of these 3 parameters should be set, no more nor less. You can choose how you would rather have your vehicles enter the network. With `vehs_per_hour` and `period` (which are proportional to each other, use whichever is more convenient to define), vehicles will enter the network equally spaced, while the vehicles will be more randomly separated if you use `probability`.\n", "\n", @@ -348,7 +334,8 @@ "- `depart_speed`: this parameter lets you specify the speed at which the vehicles will enter the network. It should be a positive `float`, in meters per second. If this speed is unsafe, the departure of the vehicles is delayed. Just like for `depart_lane`, there are other options for this parameter, which are the following strings:\n", "\n", " - `\"random\"`: vehicles enter the edge with a random speed between 0 and the speed limit on the edge. The entering speed may be adapted to ensure that a safe distance to the leading vehicle is kept\n", - " - `\"speedLimit\"`: vehicles enter the edge with the maximum speed that is allowed on this edge. If that speed is unsafe, the departure is delayed.\n", + " - `\"max\"`: vehicle speeds at insertion will be adjusted to the maximum safe speed that allows insertion at the specified time to succeed\n", + "\n", " \n", "By default, `depart_speed` is set to 0.\n", "\n", @@ -418,7 +405,8 @@ "from flow.controllers import IDMController\n", "from flow.networks import MergeNetwork\n", "from flow.networks.merge import ADDITIONAL_NET_PARAMS\n", - "from flow.envs.loop.loop_accel import AccelEnv, ADDITIONAL_ENV_PARAMS\n", + "from flow.envs.ring.accel import AccelEnv, ADDITIONAL_ENV_PARAMS\n", + "\n", "\n", "\n", "# create a vehicle type\n", @@ -437,7 +425,7 @@ " edge=\"inflow_highway\",\n", " vehs_per_hour=10000,\n", " depart_lane=\"random\",\n", - " depart_speed=\"speedLimit\",\n", + " depart_speed=\"random\",\n", " color=\"white\")\n", "\n", "# inflow for (2)\n", @@ -453,7 +441,7 @@ " edge=\"inflow_merge\",\n", " probability=0.1,\n", " depart_lane=1, # left lane\n", - " depart_speed=\"random\",\n", + " depart_speed=\"max\",\n", " begin=60, # 1 minute\n", " number=30,\n", " color=\"red\")\n", @@ -490,18 +478,11 @@ "\n", "_ = exp.run(1, 10000)" ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { "kernelspec": { - "display_name": "Python [default]", + "display_name": "Python 3", "language": "python", "name": "python3" }, @@ -515,7 +496,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.5.2" + "version": "3.6.8" } }, "nbformat": 4, From 1906340e1fdfcd3623e718a6698b03fef04688b5 Mon Sep 17 00:00:00 2001 From: Ashkan Y Date: Mon, 14 Oct 2019 16:16:07 -0700 Subject: [PATCH 05/86] Changed the remaining old names (#754) --- tests/fast_tests/test_scenarios.py | 4 ++-- .../{test_green_wave.py => test_traffic_light_grid.py} | 0 tests/stress_tests/stress_test_start.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) rename tests/fast_tests/{test_green_wave.py => test_traffic_light_grid.py} (100%) diff --git a/tests/fast_tests/test_scenarios.py b/tests/fast_tests/test_scenarios.py index a1d6f70b1..f9dd47c04 100644 --- a/tests/fast_tests/test_scenarios.py +++ b/tests/fast_tests/test_scenarios.py @@ -102,7 +102,7 @@ def test_additional_net_params(self): class TestRingNetwork(unittest.TestCase): - """Tests LoopNetwork in flow/networks/ring.py.""" + """Tests RingNetwork in flow/networks/ring.py.""" def test_additional_net_params(self): """Ensures that not returning the correct params leads to an error.""" @@ -142,7 +142,7 @@ def test_additional_net_params(self): class TestMultiRingNetwork(unittest.TestCase): - """Tests MultiLoopNetwork in flow/networks/multi_ring.py.""" + """Tests MultiRingNetwork in flow/networks/multi_ring.py.""" def test_additional_net_params(self): """Ensures that not returning the correct params leads to an error.""" diff --git a/tests/fast_tests/test_green_wave.py b/tests/fast_tests/test_traffic_light_grid.py similarity index 100% rename from tests/fast_tests/test_green_wave.py rename to tests/fast_tests/test_traffic_light_grid.py diff --git a/tests/stress_tests/stress_test_start.py b/tests/stress_tests/stress_test_start.py index 65702a121..d176b2f75 100644 --- a/tests/stress_tests/stress_test_start.py +++ b/tests/stress_tests/stress_test_start.py @@ -28,7 +28,7 @@ def start(): initial_config = InitialConfig(bunching=20) network = RingNetwork( - name="sugiyama", + name="ring", vehicles=vehicles, net_params=net_params, initial_config=initial_config) From 054d3937af3082595f074bb58067e23e48df93cb Mon Sep 17 00:00:00 2001 From: Damian Dailisan Date: Mon, 14 Oct 2019 21:09:10 -0700 Subject: [PATCH 06/86] Aimsun fixes (#753) * get rid of save prompt in aimsun * bug: getLanesLength2D sums all lane lengths * was using wrong key for speed limit * add flow directory to PYTHONPATH * length2D is the proper way to get edge length --- flow/core/kernel/network/aimsun.py | 2 ++ flow/utils/aimsun/generate.py | 8 ++++---- flow/utils/aimsun/load.py | 7 ++----- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/flow/core/kernel/network/aimsun.py b/flow/core/kernel/network/aimsun.py index 906256f32..93b54041d 100644 --- a/flow/core/kernel/network/aimsun.py +++ b/flow/core/kernel/network/aimsun.py @@ -103,6 +103,8 @@ def generate_network(self, network): if os.path.exists(check_file_path): os.remove(check_file_path) + # we need to make flow directories visible to aimsun's python2.7 + os.environ["PYTHONPATH"] = config.PROJECT_PATH # path to the supplementary file that is used to generate an aimsun # network from a template template_path = network.net_params.template diff --git a/flow/utils/aimsun/generate.py b/flow/utils/aimsun/generate.py index 31800369e..bf0ae410e 100644 --- a/flow/utils/aimsun/generate.py +++ b/flow/utils/aimsun/generate.py @@ -275,7 +275,7 @@ def generate_net(nodes, config.PROJECT_PATH, "flow/utils/aimsun/run.py"), True) # save - gui.saveAs('flow.ang') + gui.save(model, 'flow.ang', GGui.GGuiSaveType.eSaveAs) def generate_net_osm(file_name, inflows, veh_types): @@ -374,7 +374,7 @@ def generate_net_osm(file_name, inflows, veh_types): config.PROJECT_PATH, "flow/utils/aimsun/run.py"), True) # save - gui.saveAs('flow.ang') + gui.save(model, 'flow.ang', GGui.GGuiSaveType.eSaveAs) def get_junctions(nodes): @@ -685,7 +685,7 @@ def create_meter(model, edge): """ section = model.getCatalog().findByName(edge, model.getType("GKSection")) meter_length = 2 - pos = section.getLanesLength2D() - meter_length + pos = section.length2D() - meter_length type = model.getType("GKMetering") cmd = model.createNewCmd(model.getType("GKSectionObject")) # TODO double check the zeros @@ -825,7 +825,7 @@ def set_sim_step(experiment, sim_step): for s in types.itervalues(): s_id = s.getId() num_lanes = s.getNbFullLanes() - length = s.getLanesLength2D() + length = s.length2D() speed = s.getSpeed() edge_osm[s_id] = {"speed": speed, "length": length, diff --git a/flow/utils/aimsun/load.py b/flow/utils/aimsun/load.py index a04743383..22773a2a0 100644 --- a/flow/utils/aimsun/load.py +++ b/flow/utils/aimsun/load.py @@ -72,11 +72,8 @@ def get_dict_from_objects(sections, nodes, turnings, cen_connections): scenario_data['sections'][s.id] = { 'name': s.name, 'numLanes': s.nb_full_lanes, - # FIXME this is a mean of the lanes lengths - # (bc they don't have to be all of the same size) - # it may not be 100% accurate - 'length': s.lanes_length_2D / s.nb_full_lanes, - 'max_speed': s.speed + 'length': s.length2D(), + 'speed': s.speed } # load nodes From 4a27159dd92c818f9c23dac8d18e63b2a37d139e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nathan=20Lichtl=C3=A9?= Date: Tue, 15 Oct 2019 06:24:14 +0200 Subject: [PATCH 07/86] New visualization tutorial (#635) * start rewriting tuto5 * new visualization tutorial * Add example to visualization tutorial * Print path where emission file is generated * Add trained ring policy for visualization tutorial * Display path of CSV emission file generated by --gen-emission * Rename section * removed rllab, added updated trained policy --- flow/visualize/visualizer_rllib.py | 4 + .../checkpoint_100/checkpoint-100 | Bin 0 -> 593 bytes .../checkpoint-100.tune_metadata | Bin 0 -> 183 bytes .../checkpoint_120/checkpoint-120 | Bin 0 -> 593 bytes .../checkpoint-120.tune_metadata | Bin 0 -> 183 bytes .../checkpoint_140/checkpoint-140 | Bin 0 -> 593 bytes .../checkpoint-140.tune_metadata | Bin 0 -> 183 bytes .../checkpoint_160/checkpoint-160 | Bin 0 -> 593 bytes .../checkpoint-160.tune_metadata | Bin 0 -> 183 bytes .../checkpoint_180/checkpoint-180 | Bin 0 -> 593 bytes .../checkpoint-180.tune_metadata | Bin 0 -> 183 bytes .../trained_ring/checkpoint_20/checkpoint-20 | Bin 0 -> 593 bytes .../checkpoint_20/checkpoint-20.tune_metadata | Bin 0 -> 183 bytes .../checkpoint_200/checkpoint-200 | Bin 0 -> 593 bytes .../checkpoint-200.tune_metadata | Bin 0 -> 183 bytes .../trained_ring/checkpoint_40/checkpoint-40 | Bin 0 -> 593 bytes .../checkpoint_40/checkpoint-40.tune_metadata | Bin 0 -> 183 bytes .../trained_ring/checkpoint_60/checkpoint-60 | Bin 0 -> 593 bytes .../checkpoint_60/checkpoint-60.tune_metadata | Bin 0 -> 183 bytes .../trained_ring/checkpoint_80/checkpoint-80 | Bin 0 -> 593 bytes .../checkpoint_80/checkpoint-80.tune_metadata | Bin 0 -> 183 bytes tutorials/data/trained_ring/params.json | 125 +++++++++ tutorials/data/trained_ring/params.pkl | Bin 0 -> 6463 bytes tutorials/data/trained_ring/progress.csv | 201 ++++++++++++++ tutorials/data/trained_ring/result.json | 200 +++++++++++++ tutorials/tutorial04_visualize.ipynb | 262 +++++++++++++++--- 26 files changed, 757 insertions(+), 35 deletions(-) create mode 100644 tutorials/data/trained_ring/checkpoint_100/checkpoint-100 create mode 100644 tutorials/data/trained_ring/checkpoint_100/checkpoint-100.tune_metadata create mode 100644 tutorials/data/trained_ring/checkpoint_120/checkpoint-120 create mode 100644 tutorials/data/trained_ring/checkpoint_120/checkpoint-120.tune_metadata create mode 100644 tutorials/data/trained_ring/checkpoint_140/checkpoint-140 create mode 100644 tutorials/data/trained_ring/checkpoint_140/checkpoint-140.tune_metadata create mode 100644 tutorials/data/trained_ring/checkpoint_160/checkpoint-160 create mode 100644 tutorials/data/trained_ring/checkpoint_160/checkpoint-160.tune_metadata create mode 100644 tutorials/data/trained_ring/checkpoint_180/checkpoint-180 create mode 100644 tutorials/data/trained_ring/checkpoint_180/checkpoint-180.tune_metadata create mode 100644 tutorials/data/trained_ring/checkpoint_20/checkpoint-20 create mode 100644 tutorials/data/trained_ring/checkpoint_20/checkpoint-20.tune_metadata create mode 100644 tutorials/data/trained_ring/checkpoint_200/checkpoint-200 create mode 100644 tutorials/data/trained_ring/checkpoint_200/checkpoint-200.tune_metadata create mode 100644 tutorials/data/trained_ring/checkpoint_40/checkpoint-40 create mode 100644 tutorials/data/trained_ring/checkpoint_40/checkpoint-40.tune_metadata create mode 100644 tutorials/data/trained_ring/checkpoint_60/checkpoint-60 create mode 100644 tutorials/data/trained_ring/checkpoint_60/checkpoint-60.tune_metadata create mode 100644 tutorials/data/trained_ring/checkpoint_80/checkpoint-80 create mode 100644 tutorials/data/trained_ring/checkpoint_80/checkpoint-80.tune_metadata create mode 100644 tutorials/data/trained_ring/params.json create mode 100644 tutorials/data/trained_ring/params.pkl create mode 100644 tutorials/data/trained_ring/progress.csv create mode 100644 tutorials/data/trained_ring/result.json diff --git a/flow/visualize/visualizer_rllib.py b/flow/visualize/visualizer_rllib.py index f9a94d8b2..e9ca9be4e 100644 --- a/flow/visualize/visualizer_rllib.py +++ b/flow/visualize/visualizer_rllib.py @@ -306,6 +306,10 @@ def visualizer_rllib(args): # convert the emission file into a csv file emission_to_csv(emission_path) + # print the location of the emission csv file + emission_path_csv = emission_path[:-4] + ".csv" + print("\nGenerated emission file at " + emission_path_csv) + # delete the .xml version of the emission file os.remove(emission_path) diff --git a/tutorials/data/trained_ring/checkpoint_100/checkpoint-100 b/tutorials/data/trained_ring/checkpoint_100/checkpoint-100 new file mode 100644 index 0000000000000000000000000000000000000000..9fcf839ed74c3de3b161bb44cc547b8bd5f82eda GIT binary patch literal 593 zcmZo*t}SHHh+t!2U?|To%1$jRWOVw^$iUD55oZUAr)B1pq!twyGS(I{MeqRyQc}|r zOLI!%3-WU^lPe3ElZz57^@?(GGL!U5OEPnc^&mR9{PNwvTDb~YG#d+9iz8Tpnu|*k zOHvEjY75yjn3MBLa|NaZTzk6;4%!K6^Y zo57p0y-+ZyP)Ng@IRfZM2Ae`*KR-XO|3CmHyctRgMUpz5f5gtYVjr|l$-cPc!G612 z_x3H|Wwa|?kiXwbBc8J5Jwa&^X6_(RoIjFRwP+ z8$G(YJL~=0{a5;gZCJka?7uiu#G&b-;sIN6)&o=RDwZ>VV5(q_Eo9N~V#tVQ@M`FdW=M_T1xl9`C1&QO7R5v2l(n{ytyB*HufFNv literal 0 HcmV?d00001 diff --git a/tutorials/data/trained_ring/checkpoint_100/checkpoint-100.tune_metadata b/tutorials/data/trained_ring/checkpoint_100/checkpoint-100.tune_metadata new file mode 100644 index 0000000000000000000000000000000000000000..d715df3e49b2e151c4a7e17ba37738d131749b4c GIT binary patch literal 183 zcmZo*t}SHHh~Q;lU`VYfNG;0DP0cHb&rB&~j8FiIrKKiY8mAhini!;5npm2dB_^d< zn3|;~8ycA9kfCd$pq!tv%m*kfu<`lAc zHS|U^L~uc+prWkq4%g?+{ny995WxqONiE1M&QD2&>0$G|zz%jNaZTzk6;4%!K6^Y zo57p0y-+ZyP)Ng@IRfZM2Ae`*KR-XO|3CmHyctRgMUpz5e=L4@-Cmeg%RW`_@qW!~ zclXWB;zTUTcskmeJtZ5B)>lqI1jVV~Vf4X|z{^h^(?SDMZ z*q_?TyLaJD4ZCj+hwSZQf7!R?{o1c()3uM^c>BKnjoa-+D#UjiU0r1#J^Pfk^tMCx z2g{c3{xNsYe&HK}Hb+Bd?Y}rv#G&b-;sIN6)&o=RDwZ>V^Hs`|A$2-&=R^0pI0AmB} AlK=n! literal 0 HcmV?d00001 diff --git a/tutorials/data/trained_ring/checkpoint_120/checkpoint-120.tune_metadata b/tutorials/data/trained_ring/checkpoint_120/checkpoint-120.tune_metadata new file mode 100644 index 0000000000000000000000000000000000000000..a6d2c2c80a405da5dd09abf56ceedc8a3364fcdb GIT binary patch literal 183 zcmZo*t}SHHh~Q;lU`VYfNG;0DP0cHb&rB&~j8FiIrKKiY8mAhini!;5npm2dB_^d< zn3|;~8ycA9kfCd$pq!tv%m*kfu<`lAc zG2G2%h~R=sK}A{J9q#b%%sbD(5WxqONiE1M&QD2&>0$Ft-~_v@II%1>B|fn@J|#1` Kq>#O{R1W|Lk~cO0 literal 0 HcmV?d00001 diff --git a/tutorials/data/trained_ring/checkpoint_140/checkpoint-140 b/tutorials/data/trained_ring/checkpoint_140/checkpoint-140 new file mode 100644 index 0000000000000000000000000000000000000000..0556862b279403aec6bca2880c908428f692b9ea GIT binary patch literal 593 zcmZo*t}SHHh+t!2U?|To%1$jRWOVw^$iUD55oZUAr)B1pq!twyGS(I{MeqRyQc}|r zOLI!%3-WU^lPe3ElZz57^@?(GGL!U5OEPnc^&mR9{PNwvTDb~YG#d+9iz8Tpnu|*k zOHvEjY75yjn3MBLa|NaZTzk6;4%!K6^Y zo57p0y-+ZyP)Ng@IRfZM2Ae`*KR-XO|3CmHyctRgMUpz5e|%uSX@9a&*Z#?>$NN>p zZtN4RQMdauC4YbN;#vD{>i5{)ILT}uRg<^(o3NK1(~Z%{v=e%9$X?9no1Y7q2*`rKJIbgMpz?w4_ihskl@R7}K0U zettnoW^QH`FjX+e7P4q~G5n}!@M_R$U`UPN1xl9`C1&QO7R5v2l(n{ytyB*HT%G4x literal 0 HcmV?d00001 diff --git a/tutorials/data/trained_ring/checkpoint_140/checkpoint-140.tune_metadata b/tutorials/data/trained_ring/checkpoint_140/checkpoint-140.tune_metadata new file mode 100644 index 0000000000000000000000000000000000000000..adcdc3630559c99751128aeb7d18aca0cb4c6b1b GIT binary patch literal 183 zcmZo*t}SHHh~Q;lU`VYfNG;0DP0cHb&rB&~j8FiIrKKiY8mAhini!;5npm2dB_^d< zn3|;~8ycA9kfCd$pq!tv%m*kfu<`lAc zHRv=jL~uc+prWkq4i6l1>yjB5BKUwZsRfzE`6;O|J#4-oxWFzePAp4JiBBw!PsvO! KDP->~)dK+kBR5O{ literal 0 HcmV?d00001 diff --git a/tutorials/data/trained_ring/checkpoint_160/checkpoint-160 b/tutorials/data/trained_ring/checkpoint_160/checkpoint-160 new file mode 100644 index 0000000000000000000000000000000000000000..9526f5f2dc16a3691dae9eb43b18ec1dc0580677 GIT binary patch literal 593 zcmZo*t}SHHh+t!2U?|To%1$jRWOVw^$iUD55oZUAr)B1pq!twyGS(I{MeqRyQc}|r zOLI!%3-WU^lPe3ElZz57^@?(GGL!U5OEPnc^&mR9{PNwvTDb~YG#d+9iz8Tpnu|*k zOHvEjY75yjn3MBLa|NaZTzk6;4%!K6^Y zo57p0y-+ZyP)Ng@IRfZM2Ae`*KR-XO|3CmHyctRgMUpz5f0VY}vVXVDz+SWQ$^Q5c zr}x=jGqJlETeg3m^Y(oWPZI6Y<_X!m_CMHLRw-zw`P0=V`0eWb6|J57zb|XD&s2}u zANzxGudb4k9Xrnv`IF2UfS}?X%s!VBgD=bM3AQI_%EBc+vhT(@N`oawPwY^8btRD|WJ literal 0 HcmV?d00001 diff --git a/tutorials/data/trained_ring/checkpoint_160/checkpoint-160.tune_metadata b/tutorials/data/trained_ring/checkpoint_160/checkpoint-160.tune_metadata new file mode 100644 index 0000000000000000000000000000000000000000..29b451459e3855d56634bb5c55293276d2fcfaa8 GIT binary patch literal 183 zcmZo*t}SHHh~Q;lU`VYfNG;0DP0cHb&rB&~j8FiIrKKiY8mAhini!;5npm2dB_^d< zn3|;~8ycA0$G2-~qd=II%1>B|fn@J|#1` Kq>#O{R1W|MpEp4O literal 0 HcmV?d00001 diff --git a/tutorials/data/trained_ring/checkpoint_180/checkpoint-180 b/tutorials/data/trained_ring/checkpoint_180/checkpoint-180 new file mode 100644 index 0000000000000000000000000000000000000000..5df269f563cc21a6c6a002a66803ed86f5533bf7 GIT binary patch literal 593 zcmZo*t}SHHh+t!2U?|To%1$jRWOVw^$iUD55oZUAr)B1pq!twyGS(I{MeqRyQc}|r zOLI!%3-WU^lPe3ElZz57^@?(GGL!U5OEPnc^&mR9{PNwvTDb~YG#d+9iz8Tpnu|*k zOHvEjY75yjn3MBLa|NaZTzk6;4%!K6^Y zo57p0y-+ZyP)Ng@IRfZM2Ae`*KR-XO|3CmHyctRgMUpz5e*`hyvCqmfv(NtfWdH1) z3-_IW*kV^-)w2KG@5B4pUIf_naZ1@Y^r-Hentaaoe{YAax5=jc!Oea9ISe}NZ!e13 zU)=m_&z#c=cJEkE*gO7bbm-P#I&h;)aNnWwN&9px`|QL-YHY?<|Oe@2Jd z_gZ&IXu&7#ybU`?i1E_n-S`UFAQJW^-C=@Lz6iX^D)dR*fCr~86pd>Rl tvkI6hm}3iBG`tu(mN0lVoL<6^8o>*cE-6aP%u6kbhr}ssZ6RB!9stj%xD?0Y0$HiWAFHQ{oeg<5Mz| KOA6ULOZ5PRq&V;Z literal 0 HcmV?d00001 diff --git a/tutorials/data/trained_ring/checkpoint_20/checkpoint-20 b/tutorials/data/trained_ring/checkpoint_20/checkpoint-20 new file mode 100644 index 0000000000000000000000000000000000000000..971b7a38cbfc3fb5bbe090519026e69d81a8d3e4 GIT binary patch literal 593 zcmZo*t}SHHh+t!2U?|To%1$jRWOVw^$iUD55oZUAr)B1pq!twyGS(I{MeqRyQc}|r zOLI!%3-WU^lPe3ElZz57^@?(GGL!U5OEPnc^&mR9{PNwvTDb~YG#d+9iz8Tpnu|*k zOHvEjY75yjn3MBLa|NaZTzk6;4%!K6^Y zo57p0y-+ZyP)Ng@IRfZM2Ae`*KR-XO|3CmHyctRgMUpz5e?(R+wO{1dZkN`)b^n!< zGW)}4XYV~e!*T!NlWX@fZTe$3`&5@5*W>;@*ZM2%YBs&ttLhfF-$aLDf8wdfb}Kl% z_WN{b+g0-m>|H)-y1k*}F?+MfWBXk-bM_f$Z)C*2cKsZ-$6N=zYZlYND(MmNKugKT;XFKk&Kk{ta(e*-!CSIAAKS zen9O1fBT+=-}axo{d51}_526aKc3y+&93iYH`Upp@~VWx95tZ>zk=@CCnnoFTx%9N za7&5pfc7Vj13sr-*h|YFv0rL>#J)c8$o~5ucI`iV#>WO29t?$|C52*1#ie?{nC1lX y^9xEcb2F=ese(DSkVV6b!9$3_tHEE0AvJ;*C|y#Nn39kfCd$pq!tv%m*kfu<`lAc zHTVlLL~uc+prWkq4qLCu?^R%6h~NXtq!wfr=clB?^sxC(UB|fn@J|#1` Kq>#O{R1W~PST;ET literal 0 HcmV?d00001 diff --git a/tutorials/data/trained_ring/checkpoint_200/checkpoint-200 b/tutorials/data/trained_ring/checkpoint_200/checkpoint-200 new file mode 100644 index 0000000000000000000000000000000000000000..6fc1e60583385b77b3fdee982ae85f90a33d3518 GIT binary patch literal 593 zcmZo*t}SHHh+t!2U?|To%1$jRWOVw^$iUD55oZUAr)B1pq!twyGS(I{MeqRyQc}|r zOLI!%3-WU^lPe3ElZz57^@?(GGL!U5OEPnc^&mR9{PNwvTDb~YG#d+9iz8Tpnu|*k zOHvEjY75yjn3MBLa|NaZTzk6;4%!K6^Y zo57p0y-+ZyP)Ng@IRfZM2Ae`*KR-XO|3CmHyctRgMUpz5f2b|IYrj0m%0A-G)BRgI zXYYG%y2y?xuw}n||Mh(dLMC=^BvtGyBZBr_`&n)45_`|~+V<`H<-#WK|Glx-{_XRa z{ccsldsDg9?D!*3+y5?PcKCCO`GCQ(*L!V`bnS~>(_(kvrt@ybV@wWF@e8aSHt;!o z7pdRkdk6OAj7i8Ua2~A|J&!f`&B=zvJdc8IIv7Y z{lI()28U-4zwJLL{cFF~EB*r&$>;WWv+Fx-Jn!t#$tUSBM@{I!uj6;^v**}5Tx%9N zurrqJzzl8913sr-*w?8k9Xh9~bm+5+lGBcSB`085fI*>XNugL$aj70Kra6Hk`2{7J uxtUeKRKXlu$fDuJaA6yR7lZV6hSUgNpma%5VrE`yQ9LA0S!)Z~O7#Gw!r$=# literal 0 HcmV?d00001 diff --git a/tutorials/data/trained_ring/checkpoint_200/checkpoint-200.tune_metadata b/tutorials/data/trained_ring/checkpoint_200/checkpoint-200.tune_metadata new file mode 100644 index 0000000000000000000000000000000000000000..131dbb7a6065d547e18fe644b0a7d63e2feb8cdd GIT binary patch literal 183 zcmZo*t}SHHh~Q;lU`VYfNG;0DP0cHb&rB&~j8FiIrKKiY8mAhini!;5npm2dB_^d< zn3|;~8ycA5WxkNf{L=bJ3RY5DK5T&A%YJmlUk5joS%{k)5GSwfFJC#;>5Dll=#Hr_>|1# Kl0x>*Qau1OMmQY+ literal 0 HcmV?d00001 diff --git a/tutorials/data/trained_ring/checkpoint_40/checkpoint-40 b/tutorials/data/trained_ring/checkpoint_40/checkpoint-40 new file mode 100644 index 0000000000000000000000000000000000000000..e32de786731a91c9fae9e2d696f92a38a9024e76 GIT binary patch literal 593 zcmZo*t}SHHh+t!2U?|To%1$jRWOVw^$iUD55oZUAr)B1pq!twyGS(I{MeqRyQc}|r zOLI!%3-WU^lPe3ElZz57^@?(GGL!U5OEPnc^&mR9{PNwvTDb~YG#d+9iz8Tpnu|*k zOHvEjY75yjn3MBLa|NaZTzk6;4%!K6^Y zo57p0y-+ZyP)Ng@IRfZM2Ae`*KR-XO|3CmHyctRgMUpz5e*~rMw?DP-m7SK#+5OFx zeEZ+E9pBAd9I#(=yY)Vn{1bMZGmhFhFSFWHEMI9?-*I8@#QyI6?3|AKf2}gI*IpE{ z|NW;~wlB`5+2$y%wQtdTV1Li%!Twtjjr$5artFJQnr7!IAhG*dLxR14IbYD7#tQZ{;XX#>WU49t?$|C52*1#ie?{nC1lX y^9xEcb2F=ese(DSkVV6bVS);S7sGrNhSUgNpma%5VrE`yQ9LA0S!)Z~O7#Fo`r|eL literal 0 HcmV?d00001 diff --git a/tutorials/data/trained_ring/checkpoint_40/checkpoint-40.tune_metadata b/tutorials/data/trained_ring/checkpoint_40/checkpoint-40.tune_metadata new file mode 100644 index 0000000000000000000000000000000000000000..78f8065fb1434a4ff399b0fea870ad7a1c82c0d3 GIT binary patch literal 183 zcmZo*t}SHHh~Q;lU`VYfNG;0DP0cHb&rB&~j8FiIrKKiY8mAhini!;5npm2dB_^d< zn3|;~8ycA9kfCd$pq!tv%m*kfu<`lAc zG0azCh~R=sK}A{J9S&c!eZP)@A%YJmlUk5joS%{k)5GSgzzlX-abj6&N_=8*d`f0= KNg;b@sU85=z&5P_ literal 0 HcmV?d00001 diff --git a/tutorials/data/trained_ring/checkpoint_60/checkpoint-60 b/tutorials/data/trained_ring/checkpoint_60/checkpoint-60 new file mode 100644 index 0000000000000000000000000000000000000000..f4b9ea4d727e695e3afe5d358c8adb0d8a77d842 GIT binary patch literal 593 zcmZo*t}SHHh+t!2U?|To%1$jRWOVw^$iUD55oZUAr)B1pq!twyGS(I{MeqRyQc}|r zOLI!%3-WU^lPe3ElZz57^@?(GGL!U5OEPnc^&mR9{PNwvTDb~YG#d+9iz8Tpnu|*k zOHvEjY75yjn3MBLa|NaZTzk6;4%!K6^Y zo57p0y-+ZyP)Ng@IRfZM2Ae`*KR-XO|3CmHyctRgMUpz5e{@%zvR|CZW3QfheSdBA zyM66Ji)=5=Oy9q;(PUqt!+AUYM@Q^-uI#t@;?-;CQt^N9YP0G4H|&bsAD!cEU!a$| zzoLDE&Fxi>Y#+64xBnOQ+WueO>;1pYr|f$nxN2YJq1ARRnoo93G->uZMn+Z+YulZNShT&c6e!m$a4w3g252%T<9$3n}#{NiU|ty8Ac0U1dMTTj7AI zxcUKW1_p4M?SRgAjsrfYUf8d%`E`K1{MP}If?p2SC;m8`J>#PR3=f7v(UL;3q~cONU`%rY z`S}GUnYo!&z*NB;Tgal}#c;!n!K>k&8AECWFHpLqC^0iHwJ07Er>wPwY^8bt%nIt_ literal 0 HcmV?d00001 diff --git a/tutorials/data/trained_ring/checkpoint_60/checkpoint-60.tune_metadata b/tutorials/data/trained_ring/checkpoint_60/checkpoint-60.tune_metadata new file mode 100644 index 0000000000000000000000000000000000000000..0fd02740ecebe199c1654593d84d35fea9d7eeec GIT binary patch literal 183 zcmZo*t}SHHh~Q;lU`VYfNG;0DP0cHb&rB&~j8FiIrKKiY8mAhini!;5npm2dB_^d< zn3|;~8ycA9kfCd$pq!tv%m*kfu<`lAc zHM}!ph~R=sK}A{J9nKtx6FtJf5WxqONiE1M&QD2&>0$HTzyfwzabj6&N_=8*d`f0= KNg;b@sU83g|2KmG literal 0 HcmV?d00001 diff --git a/tutorials/data/trained_ring/checkpoint_80/checkpoint-80 b/tutorials/data/trained_ring/checkpoint_80/checkpoint-80 new file mode 100644 index 0000000000000000000000000000000000000000..ca8554ee42773ec2754e1e817b685f25aebb7432 GIT binary patch literal 593 zcmZo*t}SHHh+t!2U?|To%1$jRWOVw^$iUD55oZUAr)B1pq!twyGS(I{MeqRyQc}|r zOLI!%3-WU^lPe3ElZz57^@?(GGL!U5OEPnc^&mR9{PNwvTDb~YG#d+9iz8Tpnu|*k zOHvEjY75yjn3MBLa|NaZTzk6;4%!K6^Y zo57p0y-+ZyP)Ng@IRfZM2Ae`*KR-XO|3CmHyctRgMUpz5e-znYuzxKmXIcD3kBzu3r1kHW#UR|*}Sa-ng#QMoL{mFfHcRu{wtIaZZ|A}Ae`xkDGw4d`L zbHANE-yRP}0XsIWJ@z7*AMImnKkk1hJbfRp_2zv$)^D-6{_Yknn@Lj|_FtSS;?VR^@qn#3>w&4vYwY_fBlojE*WKU#Zk4^ZkHUcy z;_3%NxELI!?fbTW+xMUQXIc0ngV1hg2d3W=4s+Cm4s4xw*Zx_Dy~DL; zkpmK*YzO{`a~|+H^}>F;xyQk$CLRYf4Luy2V?7V5(q_Eo9N~Vo>m8@M5s@WJrzR1xl9`C1&QO7R5v2l(n{ytyB*Hgwy71 literal 0 HcmV?d00001 diff --git a/tutorials/data/trained_ring/checkpoint_80/checkpoint-80.tune_metadata b/tutorials/data/trained_ring/checkpoint_80/checkpoint-80.tune_metadata new file mode 100644 index 0000000000000000000000000000000000000000..6602adafb05d5180afbba1028c33f739e34214ee GIT binary patch literal 183 zcmZo*t}SHHh~Q;lU`VYfNG;0DP0cHb&rB&~j8FiIrKKiY8mAhini!;5npm2dB_^d< zn3|;~8ycA9kfCd$pq!tv%m*kfu<`lAc zG1z%BL~uc+prWkq4wv3B*>y57MDPJ+QVTMR^HWk`df0p&*uX9;PAp4JiBBw!PsvO! KDP->~)dK*kq&3n2 literal 0 HcmV?d00001 diff --git a/tutorials/data/trained_ring/params.json b/tutorials/data/trained_ring/params.json new file mode 100644 index 000000000..b0da309ef --- /dev/null +++ b/tutorials/data/trained_ring/params.json @@ -0,0 +1,125 @@ +{ + "batch_mode": "truncate_episodes", + "callbacks": { + "on_episode_end": null, + "on_episode_start": null, + "on_episode_step": null, + "on_postprocess_traj": null, + "on_sample_end": null, + "on_train_result": null + }, + "clip_actions": false, + "clip_param": 0.3, + "clip_rewards": null, + "collect_metrics_timeout": 180, + "compress_observations": false, + "custom_resources_per_worker": {}, + "entropy_coeff": 0.0, + "entropy_coeff_schedule": null, + "env": "WaveAttenuationPOEnv-v0", + "env_config": { + "flow_params": "{\n \"env\": {\n \"additional_params\": {\n \"max_accel\": 1,\n \"max_decel\": 1,\n \"ring_length\": [\n 220,\n 270\n ]\n },\n \"clip_actions\": false,\n \"evaluate\": false,\n \"horizon\": 3000,\n \"sims_per_step\": 1,\n \"warmup_steps\": 750\n },\n \"env_name\": \"flow.envs.ring.wave_attenuation.WaveAttenuationPOEnv\",\n \"exp_tag\": \"stabilizing_the_ring\",\n \"initial\": {\n \"additional_params\": {},\n \"bunching\": 0,\n \"edges_distribution\": \"all\",\n \"lanes_distribution\": Infinity,\n \"min_gap\": 0,\n \"perturbation\": 0.0,\n \"shuffle\": false,\n \"spacing\": \"uniform\",\n \"x0\": 0\n },\n \"net\": {\n \"additional_params\": {\n \"lanes\": 1,\n \"length\": 260,\n \"resolution\": 40,\n \"speed_limit\": 30\n },\n \"inflows\": {\n \"_InFlows__flows\": []\n },\n \"osm_path\": null,\n \"template\": null\n },\n \"network\": \"flow.networks.ring.RingNetwork\",\n \"sim\": {\n \"color_vehicles\": true,\n \"emission_path\": null,\n \"lateral_resolution\": null,\n \"no_step_log\": true,\n \"num_clients\": 1,\n \"overtake_right\": false,\n \"port\": null,\n \"print_warnings\": true,\n \"pxpm\": 2,\n \"render\": false,\n \"restart_instance\": false,\n \"save_render\": false,\n \"seed\": null,\n \"show_radius\": false,\n \"sight_radius\": 25,\n \"sim_step\": 0.1,\n \"teleport_time\": -1\n },\n \"simulator\": \"traci\",\n \"veh\": [\n {\n \"acceleration_controller\": [\n \"IDMController\",\n {\n \"noise\": 0.2\n }\n ],\n \"car_following_params\": {\n \"controller_params\": {\n \"accel\": 2.6,\n \"carFollowModel\": \"IDM\",\n \"decel\": 4.5,\n \"impatience\": 0.5,\n \"maxSpeed\": 30,\n \"minGap\": 0,\n \"sigma\": 0.5,\n \"speedDev\": 0.1,\n \"speedFactor\": 1.0,\n \"tau\": 1.0\n },\n \"speed_mode\": 25\n },\n \"initial_speed\": 0,\n \"lane_change_controller\": [\n \"SimLaneChangeController\",\n {}\n ],\n \"lane_change_params\": {\n \"controller_params\": {\n \"laneChangeModel\": \"LC2013\",\n \"lcCooperative\": \"1.0\",\n \"lcKeepRight\": \"1.0\",\n \"lcSpeedGain\": \"1.0\",\n \"lcStrategic\": \"1.0\"\n },\n \"lane_change_mode\": 512\n },\n \"num_vehicles\": 21,\n \"routing_controller\": [\n \"ContinuousRouter\",\n {}\n ],\n \"veh_id\": \"human\"\n },\n {\n \"acceleration_controller\": [\n \"RLController\",\n {}\n ],\n \"car_following_params\": {\n \"controller_params\": {\n \"accel\": 2.6,\n \"carFollowModel\": \"IDM\",\n \"decel\": 4.5,\n \"impatience\": 0.5,\n \"maxSpeed\": 30,\n \"minGap\": 2.5,\n \"sigma\": 0.5,\n \"speedDev\": 0.1,\n \"speedFactor\": 1.0,\n \"tau\": 1.0\n },\n \"speed_mode\": 25\n },\n \"initial_speed\": 0,\n \"lane_change_controller\": [\n \"SimLaneChangeController\",\n {}\n ],\n \"lane_change_params\": {\n \"controller_params\": {\n \"laneChangeModel\": \"LC2013\",\n \"lcCooperative\": \"1.0\",\n \"lcKeepRight\": \"1.0\",\n \"lcSpeedGain\": \"1.0\",\n \"lcStrategic\": \"1.0\"\n },\n \"lane_change_mode\": 512\n },\n \"num_vehicles\": 1,\n \"routing_controller\": [\n \"ContinuousRouter\",\n {}\n ],\n \"veh_id\": \"rl\"\n }\n ]\n}", + "run": "PPO" + }, + "evaluation_config": {}, + "evaluation_interval": null, + "evaluation_num_episodes": 10, + "gamma": 0.999, + "grad_clip": null, + "horizon": 3000, + "ignore_worker_failures": false, + "input": "sampler", + "input_evaluation": [ + "is", + "wis" + ], + "kl_coeff": 0.2, + "kl_target": 0.02, + "lambda": 0.97, + "local_tf_session_args": { + "inter_op_parallelism_threads": 8, + "intra_op_parallelism_threads": 8 + }, + "log_level": "INFO", + "log_sys_usage": true, + "lr": 5e-05, + "lr_schedule": null, + "metrics_smoothing_episodes": 100, + "min_iter_time_s": 0, + "model": { + "conv_activation": "relu", + "conv_filters": null, + "custom_model": null, + "custom_options": {}, + "custom_preprocessor": null, + "dim": 84, + "fcnet_activation": "tanh", + "fcnet_hiddens": [ + 3, + 3 + ], + "framestack": true, + "free_log_std": false, + "grayscale": false, + "lstm_cell_size": 256, + "lstm_use_prev_action_reward": false, + "max_seq_len": 20, + "no_final_linear": false, + "state_shape": null, + "use_lstm": false, + "vf_share_layers": true, + "zero_mean": true + }, + "monitor": false, + "multiagent": { + "policies": {}, + "policies_to_train": null, + "policy_mapping_fn": null + }, + "num_cpus_for_driver": 1, + "num_cpus_per_worker": 1, + "num_envs_per_worker": 1, + "num_gpus": 0, + "num_gpus_per_worker": 0, + "num_sgd_iter": 10, + "num_workers": 2, + "observation_filter": "NoFilter", + "optimizer": {}, + "output": null, + "output_compress_columns": [ + "obs", + "new_obs" + ], + "output_max_file_size": 67108864, + "postprocess_inputs": false, + "preprocessor_pref": "deepmind", + "remote_env_batch_wait_ms": 0, + "remote_worker_envs": false, + "sample_async": false, + "sample_batch_size": 200, + "seed": null, + "sgd_minibatch_size": 128, + "shuffle_buffer_size": 0, + "shuffle_sequences": true, + "simple_optimizer": false, + "soft_horizon": false, + "synchronize_filters": true, + "tf_session_args": { + "allow_soft_placement": true, + "device_count": { + "CPU": 1 + }, + "gpu_options": { + "allow_growth": true + }, + "inter_op_parallelism_threads": 2, + "intra_op_parallelism_threads": 2, + "log_device_placement": false + }, + "timesteps_per_iteration": 0, + "train_batch_size": 60000, + "use_gae": true, + "vf_clip_param": 10.0, + "vf_loss_coeff": 1.0, + "vf_share_layers": false +} \ No newline at end of file diff --git a/tutorials/data/trained_ring/params.pkl b/tutorials/data/trained_ring/params.pkl new file mode 100644 index 0000000000000000000000000000000000000000..d48ee06582f165d2c591ebba4daceb1fd0970636 GIT binary patch literal 6463 zcmeHM&u<(_752u-#vVJH9}Wr2?jdS%SP5b|lOHRsgaCO-6ee+^1O(D5D$3n8-G!^W znyT(`>?qRSNTfP82e@+Q#DPBnE?huD9N`EDxb2niRZV-kC*wqDFCe84w(GrDRjQ&X}&rkm1#`{bDd)|EHPUpIil{U>wx2kl?l$Z$BxRZBxZa-+;YRr||X>=^C zCG}66o!VWJX`zi^``YXY!^T{y8lB+q8FJVjSXNt}ispqomBTaTU0siLF=1n=Dq$?S z>DxT6Wz?xOV&eS+Kt}9yLzAun{|Zsacqy>F!PxLzKo;*|uH{7L&FBYiH8uRe zB8CDzRu6^IEEl|h#YSA)O6N586mO)!j$5CK1CEH~w5?0RjFO;stJKKhra{x4N_n31 z=FYeO^~E3mdgmQ}2m}R@7|Ya0X>F|ZJ}Wt#x6P0L zd~Rv!+29Jk2zJpYS3<)Jc#=r+0VoIap4yubs5}OWP+j|`ABW`hMWjo_Vx5r%<$aM> z8G3%!>qwo4!>#^kUik4=&+y4n>-o$$ij^#2vk-eO`wVh_)Q4xCM|z+pHBqiRV51e z1DH2|1i^P_?S~J(hlm8%Bk{Ck6;EjxWZbS)@{k~`GQnuD)=Z+XNKVmsQyjaH?bbz{ z(I}r>=`LF&sjw`OwlZ?JCKqXXRHx}|6q88t;#H%&$ToO#(7`=N-AOs2*PQ~OR<+sn z>nGJ_S820)JXVV$WlJ6hf1|pPV{P)TOHa3GD4!f{7*K8?dlM!{`7~P@dUc{5J+fQLL3VY$7rlD?tmW zOx@dgxwIBZbc}Q4m_e!OVK}z{RDIflbP&8ceo>>ID=CVKChtTo(i5Z^-=nRPW`zBG zQ7E+`@!V`Bc2>o(!3(T@!Gg-CB|$LkAZ&2d3bSAatcnhACJX#7;)O{~_2Ae6EOO^n z7j1@GZg?VV@^5}wmXs}f2O9tML`5qm@R5mX>_p z+8p-wG8sJYNuKn_jAD*!4jj_^i1l&D9SwstZ(V<#nKx}u#^L6z{&b|JkEXrtXT49i zD1sC#@6@tj7Jf$UVR~b;6Ud&0%(3yY1AKmM9czY@`7zZZ%y)gRNVUU7sMmMq%XO=y z-#8T0oO`T~^TyY^PX*m7dMVZ)h8($#tDw)QYuy8&XIJqW<3QgFBbjunXWJtRrIxN# z=rp|9>oaE`P0vi``;f2wSosr<#ThSB@y5J;Ec1If5VyVc>ofTE`AElVJLxf(FIwz> zlZ`ZFaO^1A?rjgZu3bMa-&DM#wd@6w0GJD=$E1thGzD&6>lcK%ZV{awos(dhZ_0hdAla_DGB!zJH&C2BP*lPQ> zP%hXgZwsZri2lD9%KwErX;i03`f~YX0G4Fg)L@*JA3l5l0%aM`?#5kS^uoS6*=j(- z5Ni;a9-^*7n+PhRU|zcOUDAdaoN?n;5}bIrCMAtKp$zDl!L7gl^TX|{Zez+Z&_x$7 z2w^}uV9d)MF`YM4R?jx0fji&P;fx7fjawtLz`?=lgj=C@TGnVRxeaQ>Xn1BKST&<} zf}D(c@+J4dlvkqnRc%1`b@jk&HXh#7EHOCq&Cz(oT6FC(&-p32h{t=J1rBZWdv`7{ zj2+?qpZvoOLYofb#+}Ezu!wQ4&kz7<6_`qH4~iIgU(majaPyCaz$gOn9evxAi*T0g zG6Np)=4}?n;f`ud?Qabr*@5OvFUSpbiy}MUGb}ma#sS#kxkDl5+du4*@EAgdsdv zH|z1Dc3)8%kjgK}XXtFS|^C z(i=Ub3u%22DPvF48x|Yklq^d44Wr5%VjhRx*}g#Sz!@zHz{EUY3OHxWL>&2n$x}z`PT;ewz5@loNOU zuHyM_;+fpRx5{7tW#Cr!6kHYKG0Mpweo5c2+y$E^3{cC?rW$>Y{TJvlBDHIJ@)6^` zPDO$233w_jYYM2unMi67(mUDvVDQ0{D~;S~=lOT|&xAXJl^I\n", + "Visualization is different depending on which reinforcement learning library you are using, if any. Accordingly, the rest of this tutorial explains how to plot rewards, replay policies and collect data when using either no RL library, RLlib or rllab. " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": true + }, + "source": [ + "**Contents:**\n", + "\n", + "[How to visualize using SUMO without training](#2.1---Using-SUMO-without-training)\n", + "\n", + "[How to visualize using SUMO with RLlib](#2.2---Using-SUMO-with-RLlib)\n", + "\n", + "[How to visualize using SUMO with rllab](#2.3---Using-SUMO-with-rllab)\n", "\n", - "## Visualization with RLlib" + "[**_Example: visualize data on a ring trained using RLlib_**](#2.4---Example:-Visualize-data-on-a-ring-trained-using-RLlib)\n" ] }, { "cell_type": "markdown", - "metadata": {}, + "metadata": { + "collapsed": true + }, "source": [ - "### Plotting Reward\n", + "## 2. How to visualize" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": true + }, + "source": [ + "### 2.1 - Using SUMO without training\n", "\n", - "Similarly to how rllab handles reward plotting, RLlib supports reward visualization over the period of training using `tensorboard`. `tensorboard` takes one command-line input, `--logdir`, which is an rllib result directory (usually located within an experiment directory inside your `ray_results` directory). An example function call is below." + "_In this case, since there is no training, there is no reward to plot and no policy to replay._" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": true + }, + "source": [ + "#### Data collection and analysis\n", + "\n", + "SUMO-only experiments can generate emission CSV files seamlessly:\n", + "\n", + "First, you have to tell SUMO to generate the `emission.xml` files. You can do that by specifying `emission_path` in the simulation parameters (class `SumoParams`), which is the path where the emission files will be generated. For instance:" ] }, { @@ -46,16 +107,113 @@ "metadata": {}, "outputs": [], "source": [ - "! tensorboard --logdir /ray_results/experiment_dir/result/directory" + "from flow.core.params import SumoParams\n", + "\n", + "sumo_params = SumoParams(sim_step=0.1, render=True, emission_path='data')" ] }, { "cell_type": "markdown", - "metadata": {}, + "metadata": { + "collapsed": true + }, "source": [ - "If you do not wish to use `tensorboard`, you can also use the `flow/visualize/plot_ray_results.py` file. It takes as arguments the path to the `progress.csv` file located inside your experiment results directory, and the name(s) of the column(s) to plot. If you do not know what the name of the columns are, simply do not put any and a list of all available columns will be displayed to you. \n", + "Then, you have to tell Flow to convert these XML emission files into CSV files. To do that, pass in `convert_to_csv=True` to the `run` method of your experiment object. For instance:\n", "\n", - "Example usage:" + "```python\n", + "exp.run(1, 1500, convert_to_csv=True)\n", + "```" + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": true + }, + "source": [ + "When running experiments, Flow will now automatically create CSV files next to the SUMO-generated XML files." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": true + }, + "source": [ + "### 2.2 - Using SUMO with RLlib " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": true + }, + "source": [ + "#### Reward plotting\n", + "\n", + "RLlib supports reward visualization over the period of the training using the `tensorboard` command. It takes one command-line parameter, `--logdir`, which is an RLlib result directory. By default, it would be located within an experiment directory inside your `~/ray_results` directory. \n", + "\n", + "An example call would look like:\n", + "\n", + "`tensorboard --logdir ~/ray_results/experiment_dir/result/directory`\n", + "\n", + "You can also run `tensorboard --logdir ~/ray_results` if you want to select more than just one experiment.\n", + "\n", + "If you do not wish to use `tensorboard`, an other way is to use our `flow/visualize/plot_ray_results.py` tool. It takes as arguments:\n", + "\n", + "- the path to the `progress.csv` file located inside your experiment results directory (`~/ray_results/...`),\n", + "- the name(s) of the column(s) you wish to plot (reward or other things).\n", + "\n", + "An example call would look like:\n", + "\n", + "`flow/visualize/plot_ray_results.py ~/ray_results/experiment_dir/result/progress.csv training/return-average training/return-min`\n", + "\n", + "If you do not know what the names of the columns are, run the command without specifying any column:\n", + "\n", + "`flow/visualize/plot_ray_results.py ~/ray_results/experiment_dir/result/progress.csv`\n", + "\n", + "and the list of all available columns will be displayed to you." + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": true + }, + "source": [ + "#### Policy replay\n", + "\n", + "The tool to replay a policy trained using RLlib is located at `flow/visualize/visualizer_rllib.py`. It takes as argument, first the path to the experiment results (by default located within `~/ray_results`), and secondly the number of the checkpoint you wish to visualize (which correspond to the folder `checkpoint_` inside the experiment results directory).\n", + "\n", + "An example call would look like this:\n", + "\n", + "`python flow/visualize/visualizer_rllib.py ~/ray_results/experiment_dir/result/directory 1`\n", + "\n", + "There are other optional parameters which you can learn about by running `visualizer_rllib.py --help`. " + ] + }, + { + "cell_type": "markdown", + "metadata": { + "collapsed": true + }, + "source": [ + "#### Data collection and analysis\n", + "\n", + "Simulation data can be generated the same way as it is done [without training](#2.1---Using-SUMO-without-training).\n", + "\n", + "If you need to generate simulation data after the training, you can run a policy replay as mentioned above, and add the `--gen-emission` parameter.\n", + "\n", + "An example call would look like:\n", + "\n", + "`python flow/visualize/visualizer_rllib.py ~/ray_results/experiment_dir/result/directory 1 --gen_emission`" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 2.4 - Example: Visualize data on a ring trained using RLlib" ] }, { @@ -64,23 +222,32 @@ "metadata": {}, "outputs": [], "source": [ - "! plot_ray_results.py /ray_results/experiment_dir/progress.csv training/return-average training/return-min" + "!pwd # make sure you are in the flow/tutorials folder" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### Replaying a Trained Policy" + "The folder `flow/tutorials/data/trained_ring` contains the data generated in `ray_results` after training an agent on a ring scenario for 200 iterations using RLlib (the experiment can be found in `flow/examples/rllib/stabilizing_the_ring.py`).\n", + "\n", + "Let's first have a look at what's available in the `progress.csv` file:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!python ../flow/visualize/plot_ray_results.py data/trained_ring/progress.csv" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "The tool to replay a policy trained using RLlib is located in `flow/visualize/visualizer_rllib.py`. It takes as argument, first the path to the experiment results, and second the number of the checkpoint you wish to visualize. \n", - "\n", - "There are other optional parameters which you can learn about by running `visualizer_rllib.py --help`. " + "This gives us a list of everything that we can plot. Let's plot the reward and its boundaries:" ] }, { @@ -89,28 +256,37 @@ "metadata": {}, "outputs": [], "source": [ - "! python ../../flow/visualize/visualizer_rllib.py /ray_results/experiment_dir/result/directory 1" + "%matplotlib notebook\n", + "# if this doesn't display anything, try with \"%matplotlib inline\" instead\n", + "%run ../flow/visualize/plot_ray_results.py data/trained_ring/progress.csv \\\n", + "episode_reward_mean episode_reward_min episode_reward_max" ] }, { "cell_type": "markdown", - "metadata": { - "collapsed": true - }, + "metadata": {}, "source": [ - "


\n", + "We can see that the policy had already converged by the iteration 50.\n", "\n", - "## Data Collection and Analysis\n", - "Any Flow experiment can output its results to a CSV file containing the contents of SUMO's built-in `emission.xml` files, specifying speed, position, time, fuel consumption, and many other metrics for all vehicles in a network over time. \n", - "\n", - "This section describes how to generate those `emission.csv` files when replaying and analyzing a trained policy." + "Now let's see what this policy looks like. Run the following script, then click on the green arrow to run the simulation (you may have to click several times)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "!python ../flow/visualize/visualizer_rllib.py data/trained_ring 200 --horizon 2000" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### RLlib" + "The RL agent is properly stabilizing the ring! \n", + "\n", + "Indeed, without an RL agent, the vehicles start forming stop-and-go waves which significantly slows down the traffic, as you can see in this simulation:" ] }, { @@ -119,23 +295,39 @@ "metadata": {}, "outputs": [], "source": [ - "# --emission_to_csv does the same as above\n", - "! python ../../flow/visualize/visualizer_rllib.py results/sample_checkpoint 1 --gen_emission" + "!python ../examples/sumo/sugiyama.py" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In the trained ring folder, there is a checkpoint generated every 20 iterations. Try to run the second previous command but replace 200 by 20. On the reward plot, you can see that the reward is already quite high at iteration 20, but hasn't converged yet, so the agent will perform a little less well than at iteration 200." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "As in the rllab case, the `emission.csv` file can be found in `test_time_rollout/` and used from there." + "That's it for this example! Feel free to play around with the other scripts in `flow/visualize`. Run them with the `--help` parameter and it should tell you how to use it. Also, if you need the emission file for the trained ring, you can obtain it by running the following command:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": { + "scrolled": true + }, + "outputs": [], + "source": [ + "!python ../flow/visualize/visualizer_rllib.py data/trained_ring 200 --horizon 2000 --gen_emission" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "### SUMO\n", - "SUMO-only experiments can generate emission CSV files as well, based on an argument to the `experiment.run` method. `run` takes in arguments `(num_runs, num_steps, rl_actions=None, convert_to_csv=False)`. To generate an `emission.csv` file, pass in `convert_to_csv=True` in the Python file running your SUMO experiment." + "The path where the emission file is generated will be outputted at the end of the simulation." ] } ], @@ -155,7 +347,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.5.2" + "version": "3.6.8" }, "widgets": { "state": {}, From 0b518f6d402f8369579c99a35dcb5fcf349d4763 Mon Sep 17 00:00:00 2001 From: Eugene Vinitsky Date: Tue, 22 Oct 2019 17:49:05 -0700 Subject: [PATCH 08/86] (Bug): Fix the observation space in Multi wave attentuation env --- flow/envs/multiagent/ring/wave_attenuation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flow/envs/multiagent/ring/wave_attenuation.py b/flow/envs/multiagent/ring/wave_attenuation.py index b464c6514..f58f2c37a 100644 --- a/flow/envs/multiagent/ring/wave_attenuation.py +++ b/flow/envs/multiagent/ring/wave_attenuation.py @@ -49,7 +49,7 @@ class MultiWaveAttenuationPOEnv(MultiEnv): @property def observation_space(self): """See class definition.""" - return Box(low=0, high=1, shape=(3,), dtype=np.float32) + return Box(low=-1, high=1, shape=(3,), dtype=np.float32) @property def action_space(self): From 0a188f500051a643edbac6d89b3adc1e0e2eccce Mon Sep 17 00:00:00 2001 From: Damian Dailisan Date: Thu, 24 Oct 2019 14:28:45 -0700 Subject: [PATCH 09/86] Aimsun communication port no longer hardcoded (#761) --- flow/core/kernel/network/aimsun.py | 2 +- flow/core/kernel/simulation/aimsun.py | 3 --- flow/utils/aimsun/generate.py | 3 +++ flow/utils/aimsun/load.py | 5 +++++ flow/utils/aimsun/run.py | 3 ++- 5 files changed, 11 insertions(+), 5 deletions(-) diff --git a/flow/core/kernel/network/aimsun.py b/flow/core/kernel/network/aimsun.py index 93b54041d..d69b6ddd2 100644 --- a/flow/core/kernel/network/aimsun.py +++ b/flow/core/kernel/network/aimsun.py @@ -120,7 +120,7 @@ def generate_network(self, network): f.write(template_path) # start the aimsun process - aimsun_call = [aimsun_path, "-script", script_path] + aimsun_call = [aimsun_path, "-script", script_path, str(self.sim_params.port)] self.aimsun_proc = subprocess.Popen(aimsun_call) # merge types into edges diff --git a/flow/core/kernel/simulation/aimsun.py b/flow/core/kernel/simulation/aimsun.py index ef4ed0b24..91b4c4abd 100644 --- a/flow/core/kernel/simulation/aimsun.py +++ b/flow/core/kernel/simulation/aimsun.py @@ -61,9 +61,6 @@ def start_simulation(self, network, sim_params): a simulation, and creates a class to communicate with the simulation via an TCP connection. """ - # FIXME: hack - sim_params.port = 9999 - # save the simulation step size (for later use) self.sim_step = sim_params.sim_step diff --git a/flow/utils/aimsun/generate.py b/flow/utils/aimsun/generate.py index bf0ae410e..06ed1fe17 100644 --- a/flow/utils/aimsun/generate.py +++ b/flow/utils/aimsun/generate.py @@ -29,6 +29,9 @@ "EPSG:32601") model = gui.getActiveModel() +# HACK: Store port in author +port_string = sys.argv[1] +model.setAuthor(port_string) def generate_net(nodes, edges, diff --git a/flow/utils/aimsun/load.py b/flow/utils/aimsun/load.py index 22773a2a0..a3c1b69bb 100644 --- a/flow/utils/aimsun/load.py +++ b/flow/utils/aimsun/load.py @@ -5,6 +5,7 @@ import flow.config as config from flow.utils.aimsun.scripting_api import AimsunTemplate +import sys def load_network(): @@ -134,6 +135,10 @@ def get_dict_from_objects(sections, nodes, turnings, cen_connections): model = AimsunTemplate(GKSystem, GKGUISystem) model.load(template_path) +# HACK: Store port in author +port_string = sys.argv[1] +model.setAuthor(port_string) + # collect the simulation parameters params_file = 'flow/core/kernel/network/data.json' params_path = os.path.join(config.PROJECT_PATH, params_file) diff --git a/flow/utils/aimsun/run.py b/flow/utils/aimsun/run.py index adc9fa097..f290c8960 100644 --- a/flow/utils/aimsun/run.py +++ b/flow/utils/aimsun/run.py @@ -16,7 +16,8 @@ from thread import start_new_thread import numpy as np -PORT = 9999 +model = GKSystem.getSystem().getActiveModel() +PORT = int(model.getAuthor()) entered_vehicles = [] exited_vehicles = [] From f174056b29f5a3ffe789562eaf91f2820dca1235 Mon Sep 17 00:00:00 2001 From: Zhongxia Yan Date: Sat, 26 Oct 2019 16:50:04 -0400 Subject: [PATCH 10/86] disable stdout when invoking flow scripts (#763) --- flow/core/kernel/network/traci.py | 1 + flow/core/kernel/simulation/traci.py | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/flow/core/kernel/network/traci.py b/flow/core/kernel/network/traci.py index af9b1f67d..c9ac80772 100644 --- a/flow/core/kernel/network/traci.py +++ b/flow/core/kernel/network/traci.py @@ -515,6 +515,7 @@ def generate_net(self, ' --output-file=' + self.cfg_path + self.netfn + ' --no-internal-links="false"' ], + stdout=subprocess.DEVNULL, shell=True) # collect data from the generated network configuration file diff --git a/flow/core/kernel/simulation/traci.py b/flow/core/kernel/simulation/traci.py index 108d706c0..af68f6647 100644 --- a/flow/core/kernel/simulation/traci.py +++ b/flow/core/kernel/simulation/traci.py @@ -142,7 +142,10 @@ def start_simulation(self, network, sim_params): # Opening the I/O thread to SUMO self.sumo_proc = subprocess.Popen( - sumo_call, preexec_fn=os.setsid) + sumo_call, + stdout=subprocess.DEVNULL, + preexec_fn=os.setsid + ) # wait a small period of time for the subprocess to activate # before trying to connect with traci From 61f36f2bdd1012a49df58f49b5aedfb447294551 Mon Sep 17 00:00:00 2001 From: Kathy Jang Date: Mon, 28 Oct 2019 21:51:37 -0700 Subject: [PATCH 11/86] Fixed multi-agent open network __done__ bug (#767) --- flow/core/kernel/vehicle/traci.py | 13 +++++++++++++ flow/envs/multiagent/base.py | 5 +++++ 2 files changed, 18 insertions(+) diff --git a/flow/core/kernel/vehicle/traci.py b/flow/core/kernel/vehicle/traci.py index 0ed8ed220..ac95f599f 100644 --- a/flow/core/kernel/vehicle/traci.py +++ b/flow/core/kernel/vehicle/traci.py @@ -68,6 +68,7 @@ def __init__(self, # number of vehicles to exit the network for every time-step self._num_arrived = [] self._arrived_ids = [] + self._arrived_rl_ids = [] # whether or not to automatically color vehicles try: @@ -126,8 +127,11 @@ def update(self, reset): self.kernel_api.vehicle.getSubscriptionResults(veh_id) sim_obs = self.kernel_api.simulation.getSubscriptionResults() + arrived_rl_ids = [] # remove exiting vehicles from the vehicles class for veh_id in sim_obs[tc.VAR_ARRIVED_VEHICLES_IDS]: + if veh_id in self.get_rl_ids(): + arrived_rl_ids.append(veh_id) if veh_id in sim_obs[tc.VAR_TELEPORT_STARTING_VEHICLES_IDS]: # this is meant to resolve the KeyError bug when there are # collisions @@ -137,6 +141,7 @@ def update(self, reset): # haven't been removed already if vehicle_obs[veh_id] is None: vehicle_obs.pop(veh_id, None) + self._arrived_rl_ids.append(arrived_rl_ids) # add entering vehicles into the vehicles class for veh_id in sim_obs[tc.VAR_DEPARTED_VEHICLES_IDS]: @@ -165,6 +170,7 @@ def update(self, reset): self._num_arrived.clear() self._departed_ids.clear() self._arrived_ids.clear() + self._arrived_rl_ids.clear() # add vehicles from a network template, if applicable if hasattr(self.master_kernel.network.network, @@ -487,6 +493,13 @@ def get_arrived_ids(self): else: return 0 + def get_arrived_rl_ids(self): + """See parent class.""" + if len(self._arrived_rl_ids) > 0: + return self._arrived_rl_ids[-1] + else: + return 0 + def get_departed_ids(self): """See parent class.""" if len(self._departed_ids) > 0: diff --git a/flow/envs/multiagent/base.py b/flow/envs/multiagent/base.py index 4753c9f45..6019f73f5 100644 --- a/flow/envs/multiagent/base.py +++ b/flow/envs/multiagent/base.py @@ -121,6 +121,11 @@ def step(self, rl_actions): else: reward = self.compute_reward(rl_actions, fail=crash) + for rl_id in self.k.vehicle.get_arrived_rl_ids(): + done[rl_id] = True + reward[rl_id] = 0 + states[rl_id] = np.zeros(self.observation_space.shape[0]) + return states, reward, done, infos def reset(self, new_inflow_rate=None): From 4ff791b3af6c7d06f9ccc8148e9c6b97bdd19745 Mon Sep 17 00:00:00 2001 From: Kathy Jang Date: Wed, 30 Oct 2019 13:59:07 -0700 Subject: [PATCH 12/86] Updated versions for lxml and cloudpickle (#762) * Updated versions for lxml and cloudpickle * Updated lxml for environment.yml --- environment.yml | 2 +- requirements.txt | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/environment.yml b/environment.yml index 1eaf3aa3b..e7616aae6 100644 --- a/environment.yml +++ b/environment.yml @@ -3,7 +3,7 @@ name: flow dependencies: - python==3.6.8 - scipy==1.1.0 - - lxml==4.2.4 + - lxml==4.4.1 - six==1.11.0 - path.py - python-dateutil==2.7.3 diff --git a/requirements.txt b/requirements.txt index 483392a76..f079ba1ab 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,7 +1,7 @@ gym==0.14.0 numpy==1.16.0 scipy==1.1.0 -lxml==4.2.4 +lxml==4.4.1 pyprind==2.11.2 nose2==0.8.0 six==1.11.0 @@ -9,7 +9,7 @@ path.py joblib==0.10.3 python-dateutil==2.7.3 cached_property -cloudpickle==0.5.3 +cloudpickle==1.2.0 pyglet==1.3.2 matplotlib==3.0.0 imutils==0.5.1 From 2b9e7a2863f59836ba0f6a3f202787721afb75cb Mon Sep 17 00:00:00 2001 From: Damian Dailisan Date: Mon, 4 Nov 2019 20:16:08 -0800 Subject: [PATCH 13/86] Running multiple aimsun instances while training (#766) * Use port to generate unique auxiliary files * open multiple copies of ang file to avoid conflict * flake --- flow/core/kernel/network/aimsun.py | 21 ++++++++++++--------- flow/utils/aimsun/generate.py | 4 ++-- flow/utils/aimsun/load.py | 10 +++++----- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/flow/core/kernel/network/aimsun.py b/flow/core/kernel/network/aimsun.py index d69b6ddd2..0378d45a9 100644 --- a/flow/core/kernel/network/aimsun.py +++ b/flow/core/kernel/network/aimsun.py @@ -81,7 +81,7 @@ def generate_network(self, network): cur_dir = os.path.join(config.PROJECT_PATH, 'flow/core/kernel/network') # TODO: add current time - with open(os.path.join(cur_dir, 'data.json'), 'w') as outfile: + with open(os.path.join(cur_dir, 'data_%s.json' % self.sim_params.port), 'w') as outfile: json.dump(output, outfile, sort_keys=True, indent=4) # path to the Aimsun_Next binary @@ -94,11 +94,11 @@ def generate_network(self, network): # remove network data file if if still exists from # the previous simulation - data_file = 'flow/core/kernel/network/network_data.json' + data_file = 'flow/core/kernel/network/network_data_%s.json' % self.sim_params.port data_file_path = os.path.join(config.PROJECT_PATH, data_file) if os.path.exists(data_file_path): os.remove(data_file_path) - check_file = 'flow/core/kernel/network/network_data_check' + check_file = 'flow/core/kernel/network/network_data_check%s' % self.sim_params.port check_file_path = os.path.join(config.PROJECT_PATH, check_file) if os.path.exists(check_file_path): os.remove(check_file_path) @@ -115,9 +115,11 @@ def generate_network(self, network): script_path = osp.join(config.PROJECT_PATH, 'flow/utils/aimsun/load.py') file_path = osp.join(config.PROJECT_PATH, - 'flow/utils/aimsun/aimsun_template_path') + 'flow/utils/aimsun/aimsun_template_path_%s' % self.sim_params.port) with open(file_path, 'w') as f: - f.write(template_path) + f.write("%s_%s" % (template_path, self.sim_params.port)) + # instances must have unique template paths to avoid crashing? + os.popen('cp %s %s_%s' % (template_path, template_path, self.sim_params.port)) # start the aimsun process aimsun_call = [aimsun_path, "-script", script_path, str(self.sim_params.port)] @@ -152,10 +154,10 @@ def generate_network(self, network): else: # load network from template - scenar_file = "flow/core/kernel/network/network_data.json" + scenar_file = "flow/core/kernel/network/network_data_%s.json" % self.sim_params.port scenar_path = os.path.join(config.PROJECT_PATH, scenar_file) - check_file = "flow/core/kernel/network/network_data_check" + check_file = "flow/core/kernel/network/network_data_check_%s" % self.sim_params.port check_path = os.path.join(config.PROJECT_PATH, check_file) # a check file is created when all the network data @@ -176,7 +178,7 @@ def generate_network(self, network): # TODO load everything that is in content into the network else: - data_file = 'flow/utils/aimsun/osm_edges.json' + data_file = 'flow/utils/aimsun/osm_edges_%s.json' % self.sim_params.port filepath = os.path.join(config.PROJECT_PATH, data_file) while not os.path.exists(filepath): @@ -259,7 +261,8 @@ def close(self): # delete the json file that was used to read the network data cur_dir = os.path.join(config.PROJECT_PATH, 'flow/core/kernel/network') - os.remove(os.path.join(cur_dir, 'data.json')) + os.remove(os.path.join(cur_dir, 'data_%s.json' % self.sim_params.port)) + os.remove('%s_%s' % (self.network.net_params.template, self.sim_params.port)) ########################################################################### # State acquisition methods # diff --git a/flow/utils/aimsun/generate.py b/flow/utils/aimsun/generate.py index 06ed1fe17..25ed213b0 100644 --- a/flow/utils/aimsun/generate.py +++ b/flow/utils/aimsun/generate.py @@ -798,7 +798,7 @@ def set_sim_step(experiment, sim_step): # collect the network-specific data -data_file = 'flow/core/kernel/network/data.json' +data_file = 'flow/core/kernel/network/data_%s.json'%port_string with open(os.path.join(config.PROJECT_PATH, data_file)) as f: data = json.load(f) @@ -834,7 +834,7 @@ def set_sim_step(experiment, sim_step): "length": length, "numLanes": num_lanes} with open(os.path.join(config.PROJECT_PATH, - 'flow/utils/aimsun/osm_edges.json'), 'w') \ + 'flow/utils/aimsun/osm_edges_%s.json' % port_string), 'w') \ as outfile: json.dump(edge_osm, outfile, sort_keys=True, indent=4) diff --git a/flow/utils/aimsun/load.py b/flow/utils/aimsun/load.py index a3c1b69bb..6310179c7 100644 --- a/flow/utils/aimsun/load.py +++ b/flow/utils/aimsun/load.py @@ -122,10 +122,11 @@ def get_dict_from_objects(sections, nodes, turnings, cen_connections): return scenario_data +port_string = sys.argv[1] # collect template path file_path = os.path.join(config.PROJECT_PATH, - 'flow/utils/aimsun/aimsun_template_path') + 'flow/utils/aimsun/aimsun_template_path_%s'%port_string) with open(file_path, 'r') as f: template_path = f.readline() os.remove(file_path) @@ -136,11 +137,10 @@ def get_dict_from_objects(sections, nodes, turnings, cen_connections): model.load(template_path) # HACK: Store port in author -port_string = sys.argv[1] model.setAuthor(port_string) # collect the simulation parameters -params_file = 'flow/core/kernel/network/data.json' +params_file = 'flow/core/kernel/network/data_%s.json' % port_string params_path = os.path.join(config.PROJECT_PATH, params_file) with open(params_path) as f: data = json.load(f) @@ -176,7 +176,7 @@ def get_dict_from_objects(sections, nodes, turnings, cen_connections): scenario_data = load_network() # save template's scenario into a file to be loaded into Flow's scenario -scenario_data_file = 'flow/core/kernel/network/network_data.json' +scenario_data_file = 'flow/core/kernel/network/network_data_%s.json'%port_string scenario_data_path = os.path.join(config.PROJECT_PATH, scenario_data_file) with open(scenario_data_path, 'w') as f: json.dump(scenario_data, f, sort_keys=True, indent=4) @@ -185,7 +185,7 @@ def get_dict_from_objects(sections, nodes, turnings, cen_connections): # create a check file to announce that we are done # writing all the network data into the .json file -check_file = 'flow/core/kernel/network/network_data_check' +check_file = 'flow/core/kernel/network/network_data_check_%s'%port_string check_file_path = os.path.join(config.PROJECT_PATH, check_file) open(check_file_path, 'a').close() From c6fcf4fa688d54a59fe3168e5e21c1fb58c222f5 Mon Sep 17 00:00:00 2001 From: Yashar Zeinali Farid <34227133+Yasharzf@users.noreply.github.com> Date: Wed, 6 Nov 2019 14:13:53 -0800 Subject: [PATCH 14/86] Aimsun 8 4 install guide (#774) * added troubleshooting for Aimsun 8.4 installation * fixed bug - aimsun sitepackage installation guide * minor edits --- docs/source/flow_setup.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/source/flow_setup.rst b/docs/source/flow_setup.rst index 6019e840e..8a2650fad 100644 --- a/docs/source/flow_setup.rst +++ b/docs/source/flow_setup.rst @@ -178,12 +178,12 @@ The latter command should return an output similar to: /path/to/envs/aimsun_flow/bin/python -Copy the path up until right before /lib (i.e. /path/to/envs/aimsun_flow/bin/python) and +Copy the path up until right before /lib (i.e. /path/to/envs/aimsun_flow) and place it under the `AIMSUN_SITEPACKAGES` variable in your bashrc, like this: :: - export AIMSUN_SITEPACKAGES="/path/to/envs/aimsun_flow/bin/python" + export AIMSUN_SITEPACKAGES="/path/to/envs/aimsun_flow" Testing your installation ~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -198,6 +198,8 @@ to activate the `flow` env. Type: source activate flow python examples/aimsun/sugiyama.py +*Troubleshootig for Ubuntu users with Aimsun 8.4*: when you run the above example, you may get a subprocess.Popen error ``OSError: [Errno 8] Exec format error:``. +To fix this, go to the `Aimsun Next` main directory, open the `Aimsun_Next` binary with a text editor and add the shebang to the first line of the script ``#!/bin/sh``. (Optional) Install Ray RLlib ---------------------------- From 7a99a567d9fbc8e857e49c42c5d539b29562fb6a Mon Sep 17 00:00:00 2001 From: Eugene Vinitsky Date: Thu, 7 Nov 2019 16:14:26 -0800 Subject: [PATCH 15/86] Update README.md (#775) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7170f47d2..2a7d8bd4f 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ See [our website](https://flow-project.github.io/) for more information on the a # Technical questions -Please direct your technical questions to [Stack Overflow](https://stackoverflow.com) using the [flow-project](https://stackoverflow.com/questions/tagged/flow-project) tag. +If you have a bug, please report it. Otherwise, join the [Flow Users group](https://forms.gle/CuVBu6QtX3dfNaxz6) on Slack! You'll recieve an email shortly after filling out the form. # Getting involved From 27f0cfd0b382b535736568b0f154add08b89c6ce Mon Sep 17 00:00:00 2001 From: GilbertBahati <41528944+gilbertbahati@users.noreply.github.com> Date: Wed, 20 Nov 2019 13:06:15 -0800 Subject: [PATCH 16/86] added the Gipps car folowing model- first order model (#776) * added the Gipps car folowing model- first order model * added the Gipps car folowing model- first order model_ * tests for GippsController --- flow/controllers/__init__.py | 5 +- flow/controllers/car_following_models.py | 85 ++++++++++++++++++++++++ tests/fast_tests/test_controllers.py | 59 +++++++++++++++- 3 files changed, 146 insertions(+), 3 deletions(-) diff --git a/flow/controllers/__init__.py b/flow/controllers/__init__.py index e5899b702..01f4ad90f 100755 --- a/flow/controllers/__init__.py +++ b/flow/controllers/__init__.py @@ -14,7 +14,7 @@ from flow.controllers.base_controller import BaseController from flow.controllers.car_following_models import CFMController, \ BCMController, OVMController, LinearOVM, IDMController, \ - SimCarFollowingController, LACController + SimCarFollowingController, LACController, GippsController from flow.controllers.velocity_controllers import FollowerStopper, \ PISaturation @@ -34,5 +34,6 @@ "CFMController", "BCMController", "OVMController", "LinearOVM", "IDMController", "SimCarFollowingController", "FollowerStopper", "PISaturation", "StaticLaneChanger", "SimLaneChangeController", - "ContinuousRouter", "GridRouter", "BayBridgeRouter", "LACController" + "ContinuousRouter", "GridRouter", "BayBridgeRouter", "LACController", + "GippsController" ] diff --git a/flow/controllers/car_following_models.py b/flow/controllers/car_following_models.py index 50b0ceac6..cc1b4f508 100755 --- a/flow/controllers/car_following_models.py +++ b/flow/controllers/car_following_models.py @@ -499,3 +499,88 @@ class SimCarFollowingController(BaseController): def get_accel(self, env): """See parent class.""" return None + + +class GippsController(BaseController): + """Gipps' Model controller. + + For more information on this controller, see: + Traffic Flow Dynamics written by M.Treiber and A.Kesting + By courtesy of Springer publisher, http://www.springer.com + + http://www.traffic-flow-dynamics.org/res/SampleChapter11.pdf + + Usage + ----- + See BaseController for usage example. + + Attributes + ---------- + veh_id : str + Vehicle ID for SUMO identification + car_following_params : flow.core.param.SumoCarFollowingParams + see parent class + v0 : float + desirable velocity, in m/s (default: 30) + acc : float + max acceleration, in m/s2 (default: 1.5) + b : float + comfortable deceleration, in m/s2 (default: -1) + b_l : float + comfortable deceleration for leading vehicle , in m/s2 (default: -1) + s0 : float + linear jam distance for saftey, in m (default: 2) + tau : float + reaction time in s (default: 1) + noise : float + std dev of normal perturbation to the acceleration (default: 0) + fail_safe : str + type of flow-imposed failsafe the vehicle should posses, defaults + to no failsafe (None) + """ + + def __init__(self, + veh_id, + car_following_params=None, + v0=30, + acc=1.5, + b=-1, + b_l=-1, + s0=2, + tau=1, + delay=0, + noise=0, + fail_safe=None): + """Instantiate a Gipps' controller.""" + BaseController.__init__( + self, + veh_id, + car_following_params, + delay=delay, + fail_safe=fail_safe, + noise=noise + ) + + self.v_desired = v0 + self.acc = acc + self.b = b + self.b_l = b_l + self.s0 = s0 + self.tau = tau + + def get_accel(self, env): + """See parent class.""" + v = env.k.vehicle.get_speed(self.veh_id) + h = env.k.vehicle.get_headway(self.veh_id) + v_l = env.k.vehicle.get_speed( + env.k.vehicle.get_leader(self.veh_id)) + + # get velocity dynamics + v_acc = v + (2.5 * self.acc * self.tau * ( + 1 - (v / self.v_desired)) * np.sqrt(0.025 + (v / self.v_desired))) + v_safe = (self.tau * self.b) + np.sqrt(((self.tau**2) * (self.b**2)) - ( + self.b * ((2 * (h-self.s0)) - (self.tau * v) - ((v_l**2) / self.b_l)))) + + v_next = min(v_acc, v_safe, self.v_desired) + + return (v_next-v)/env.sim_step diff --git a/tests/fast_tests/test_controllers.py b/tests/fast_tests/test_controllers.py index a521a095e..32ec693da 100644 --- a/tests/fast_tests/test_controllers.py +++ b/tests/fast_tests/test_controllers.py @@ -7,7 +7,8 @@ from flow.controllers.routing_controllers import ContinuousRouter from flow.controllers.car_following_models import IDMController, \ - OVMController, BCMController, LinearOVM, CFMController, LACController + OVMController, BCMController, LinearOVM, CFMController, LACController, \ + GippsController from flow.controllers import FollowerStopper, PISaturation from tests.setup_scripts import ring_road_exp_setup import os @@ -635,5 +636,61 @@ def test_get_action(self): np.testing.assert_array_almost_equal(requested_accel, expected_accel) +class TestGippsontroller(unittest.TestCase): + """ + Tests that the Gipps Controller returning mathematically accurate values. + """ + + def setUp(self): + # add a few vehicles to the network using the requested model + # also make sure that the input params are what is expected + contr_params = { + "v0": 30, + "acc": 1.5, + "b": -1, + "b_l": -1, + "s0": 2, + "tau": 1, + "delay": 0, + "noise": 0, + } + + vehicles = VehicleParams() + vehicles.add( + veh_id="test", + acceleration_controller=(GippsController, contr_params), + routing_controller=(ContinuousRouter, {}), + car_following_params=SumoCarFollowingParams( + accel=15, decel=5), + num_vehicles=5) + + # create the environment and network classes for a ring road + self.env, _ = ring_road_exp_setup(vehicles=vehicles) + + def tearDown(self): + # terminate the traci instance + self.env.terminate() + + # free data used by the class + self.env = None + + def test_get_action(self): + self.env.reset() + ids = self.env.k.vehicle.get_ids() + + test_headways = [2, 4, 6, 8, 10] + for i, veh_id in enumerate(ids): + self.env.k.vehicle.set_headway(veh_id, test_headways[i]) + + requested_accel = [ + self.env.k.vehicle.get_acc_controller(veh_id).get_action(self.env) + for veh_id in ids + ] + + expected_accel = [0., 5.929271, 5.929271, 5.929271, 5.929271] + + np.testing.assert_array_almost_equal(requested_accel, expected_accel) + + if __name__ == '__main__': unittest.main() From 44e21a3a711c8f08fc49bf5945fb074e9f41c61d Mon Sep 17 00:00:00 2001 From: Eugene Vinitsky Date: Thu, 21 Nov 2019 20:08:54 -0500 Subject: [PATCH 17/86] (Bug): Change loop network to ring network in tutorial 8 (#786) --- tutorials/tutorial05_networks.ipynb | 22 +++++++++++----------- tutorials/tutorial08_environments.ipynb | 18 +++--------------- 2 files changed, 14 insertions(+), 26 deletions(-) diff --git a/tutorials/tutorial05_networks.ipynb b/tutorials/tutorial05_networks.ipynb index 57b53d001..ecbd1e703 100644 --- a/tutorials/tutorial05_networks.ipynb +++ b/tutorials/tutorial05_networks.ipynb @@ -19,7 +19,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": {}, "outputs": [], "source": [ @@ -66,7 +66,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 2, "metadata": {}, "outputs": [], "source": [ @@ -98,7 +98,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 3, "metadata": {}, "outputs": [], "source": [ @@ -140,7 +140,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "metadata": {}, "outputs": [], "source": [ @@ -217,7 +217,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "metadata": {}, "outputs": [], "source": [ @@ -245,7 +245,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "metadata": {}, "outputs": [], "source": [ @@ -273,7 +273,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "metadata": {}, "outputs": [], "source": [ @@ -345,7 +345,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "metadata": {}, "outputs": [], "source": [ @@ -377,7 +377,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "metadata": {}, "outputs": [], "source": [ @@ -405,7 +405,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 10, "metadata": {}, "outputs": [], "source": [ @@ -423,7 +423,7 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 11, "metadata": {}, "outputs": [], "source": [ diff --git a/tutorials/tutorial08_environments.ipynb b/tutorials/tutorial08_environments.ipynb index b02d25613..df53eb7c8 100644 --- a/tutorials/tutorial08_environments.ipynb +++ b/tutorials/tutorial08_environments.ipynb @@ -266,28 +266,16 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Round 0, return: 4253.8940561797535\n", - "Average, std return: 4253.8940561797535, 0.0\n", - "Average, std speed: 2.8359293707865048, 0.0\n", - "Closing connection to TraCI and stopping simulation.\n", - "Note, this may print an error message when it closes.\n" - ] - } - ], + "outputs": [], "source": [ "from flow.controllers import IDMController, ContinuousRouter\n", "from flow.core.experiment import Experiment\n", "from flow.core.params import SumoParams, EnvParams, \\\n", " InitialConfig, NetParams\n", "from flow.core.params import VehicleParams\n", - "from flow.networks.ring import LoopNetwork, ADDITIONAL_NET_PARAMS\n", + "from flow.networks.ring import RingNetwork, ADDITIONAL_NET_PARAMS\n", "\n", "sumo_params = SumoParams(sim_step=0.1, render=True)\n", "\n", From 7ff781588ff23f42af00b9b01a5720f85959d1d0 Mon Sep 17 00:00:00 2001 From: Zhongxia Yan Date: Mon, 25 Nov 2019 15:38:19 -0500 Subject: [PATCH 18/86] changed visualizer rllib to not open SUMO unnecessarily (#788) * changed visualizer rllib to not open SUMO unnecessarily --- flow/visualize/visualizer_rllib.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/flow/visualize/visualizer_rllib.py b/flow/visualize/visualizer_rllib.py index e9ca9be4e..89583101e 100644 --- a/flow/visualize/visualizer_rllib.py +++ b/flow/visualize/visualizer_rllib.py @@ -110,10 +110,7 @@ def visualizer_rllib(args): sim_params.render = 'drgb' sim_params.pxpm = 4 elif args.render_mode == 'sumo_gui': - sim_params.render = True - print('NOTE: With render mode {}, an extra instance of the SUMO GUI ' - 'will display before the GUI for visualizing the result. Click ' - 'the green Play arrow to continue.'.format(args.render_mode)) + sim_params.render = False # will be set to True below elif args.render_mode == 'no_render': sim_params.render = False if args.save_render: @@ -159,6 +156,9 @@ def visualizer_rllib(args): else: env = gym.make(env_name) + if args.render_mode == 'sumo_gui': + env.sim_params.render = True # set to True after initializing agent and env + if multiagent: rets = {} # map the agent id to its policy @@ -186,8 +186,9 @@ def visualizer_rllib(args): else: use_lstm = False - env.restart_simulation( - sim_params=sim_params, render=sim_params.render) + # if restart_instance, don't restart here because env.reset will restart later + if not sim_params.restart_instance: + env.restart_simulation(sim_params=sim_params, render=sim_params.render) # Simulate and collect metrics final_outflows = [] From 97b962a48d054df1127b066dbef7b294b5d8cf9c Mon Sep 17 00:00:00 2001 From: Damian Dailisan Date: Mon, 2 Dec 2019 05:02:34 +0800 Subject: [PATCH 19/86] render to video using sumo-gui (#784) * render to video using sumo-gui * flake * allow usage of sumo_gui in render * cleanup --- flow/envs/base.py | 19 ++++++++++++++++++- flow/visualize/visualizer_rllib.py | 23 +++-------------------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/flow/envs/base.py b/flow/envs/base.py index 0bc9324ae..7749ffea5 100755 --- a/flow/envs/base.py +++ b/flow/envs/base.py @@ -7,6 +7,8 @@ import traceback import numpy as np import random +import shutil +import subprocess from flow.renderer.pyglet_renderer import PygletRenderer as Renderer from flow.utils.flow_warnings import deprecated_attribute @@ -214,7 +216,10 @@ def __init__(self, # render a frame self.render(reset=True) elif self.sim_params.render in [True, False]: - pass # default to sumo-gui (if True) or sumo (if False) + # default to sumo-gui (if True) or sumo (if False) + if (self.sim_params.render is True) and self.sim_params.save_render: + self.path = os.path.expanduser('~')+'/flow_rendering/' + self.network.name + os.makedirs(self.path, exist_ok=True) else: raise FatalFlowError( 'Mode %s is not supported!' % self.sim_params.render) @@ -670,6 +675,15 @@ def terminate(self): # close pyglet renderer if self.sim_params.render in ['gray', 'dgray', 'rgb', 'drgb']: self.renderer.close() + # generate video + elif (self.sim_params.render is True) and self.sim_params.save_render: + images_dir = self.path.split('/')[-1] + speedup = 10 # multiplier: renders video so that `speedup` seconds is rendered in 1 real second + fps = speedup//self.sim_step + p = subprocess.Popen(["ffmpeg", "-y", "-r", str(fps), "-i", self.path+"/frame_%06d.png", + "-pix_fmt", "yuv420p", "%s/../%s.mp4" % (self.path, images_dir)]) + p.wait() + shutil.rmtree(self.path) except FileNotFoundError: # Skip automatic termination. Connection is probably already closed print(traceback.format_exc()) @@ -699,6 +713,9 @@ def render(self, reset=False, buffer_length=5): if len(self.frame_buffer) > buffer_length: self.frame_buffer.pop(0) self.sights_buffer.pop(0) + elif (self.sim_params.render is True) and self.sim_params.save_render: + # sumo-gui render + self.k.kernel_api.gui.screenshot("View #0", self.path+"/frame_%06d.png" % self.time_counter) def pyglet_render(self): """Render a frame using pyglet.""" diff --git a/flow/visualize/visualizer_rllib.py b/flow/visualize/visualizer_rllib.py index 89583101e..93102d60e 100644 --- a/flow/visualize/visualizer_rllib.py +++ b/flow/visualize/visualizer_rllib.py @@ -13,7 +13,6 @@ """ import argparse -from datetime import datetime import gym import numpy as np import os @@ -114,8 +113,9 @@ def visualizer_rllib(args): elif args.render_mode == 'no_render': sim_params.render = False if args.save_render: - sim_params.render = 'drgb' - sim_params.pxpm = 4 + if args.render_mode != 'sumo_gui': + sim_params.render = 'drgb' + sim_params.pxpm = 4 sim_params.save_render = True # Create and register a gym+rllib env @@ -314,23 +314,6 @@ def visualizer_rllib(args): # delete the .xml version of the emission file os.remove(emission_path) - # if we wanted to save the render, here we create the movie - if args.save_render: - dirs = os.listdir(os.path.expanduser('~')+'/flow_rendering') - # Ignore hidden files - dirs = [d for d in dirs if d[0] != '.'] - dirs.sort(key=lambda date: datetime.strptime(date, "%Y-%m-%d-%H%M%S")) - recent_dir = dirs[-1] - # create the movie - movie_dir = os.path.expanduser('~') + '/flow_rendering/' + recent_dir - save_dir = os.path.expanduser('~') + '/flow_movies' - if not os.path.exists(save_dir): - os.mkdir(save_dir) - os_cmd = "cd " + movie_dir + " && ffmpeg -i frame_%06d.png" - os_cmd += " -pix_fmt yuv420p " + dirs[-1] + ".mp4" - os_cmd += "&& cp " + dirs[-1] + ".mp4 " + save_dir + "/" - os.system(os_cmd) - def create_parser(): """Create the parser to capture CLI arguments.""" From ac23278fbb092e878d2d67d3ad6324eeef3ab9a3 Mon Sep 17 00:00:00 2001 From: Isaacb Date: Tue, 17 Dec 2019 21:19:01 -0600 Subject: [PATCH 20/86] Checked for edge case where vehicle speeds is empty (#795) --- flow/visualize/visualizer_rllib.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/flow/visualize/visualizer_rllib.py b/flow/visualize/visualizer_rllib.py index 93102d60e..918a16670 100644 --- a/flow/visualize/visualizer_rllib.py +++ b/flow/visualize/visualizer_rllib.py @@ -204,7 +204,12 @@ def visualizer_rllib(args): ret = 0 for _ in range(env_params.horizon): vehicles = env.unwrapped.k.vehicle - vel.append(np.mean(vehicles.get_speed(vehicles.get_ids()))) + speeds = vehicles.get_speed(vehicles.get_ids()) + + # only include non-empty speeds + if speeds: + vel.append(np.mean(speeds)) + if multiagent: action = {} for agent_id in state.keys(): From 99daa202742dc8ed74d2b7d3a0a76605503bd370 Mon Sep 17 00:00:00 2001 From: Ashkan Y Date: Mon, 23 Dec 2019 00:15:26 -0800 Subject: [PATCH 21/86] update tutorial 00 to reflect the new folder of multiagent env --- tutorials/tutorial00_flow.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tutorials/tutorial00_flow.ipynb b/tutorials/tutorial00_flow.ipynb index de3682c3e..3ff5030d1 100644 --- a/tutorials/tutorial00_flow.ipynb +++ b/tutorials/tutorial00_flow.ipynb @@ -79,7 +79,7 @@ "│ │ ├── traffic_light # logic for the traffic lights\n", "│ │ └── vehicle # logic for the vehicles\n", "│ ├── envs # environments (where states, actions and rewards are handled)\n", - "│ ├── multiagent_envs # multi-agent environments\n", + "│ │ └── multiagent # multi-agent environments\n", "│ ├── renderer # pyglet renderer\n", "│ ├── networks # networks (ie road networks)\n", "│ ├── utils # the files that don't fit anywhere else\n", From 1a44c3f2d1b889ff97a7e71e7b41b7fe1b30126c Mon Sep 17 00:00:00 2001 From: Aboudy Kreidieh Date: Tue, 24 Dec 2019 10:16:50 +0200 Subject: [PATCH 22/86] New example folder (#720) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * better file names for environments * change base_scenario.py to base.py for consistency * change WaveAttenuationMergePOEnv to MergePOEnv for consistency * initial pass at modifying examples page * changed "loop" to "ring" in envs * Renamed "loop" to "ring" in scenarios * More rename "loop" to "ring" * Final commit for "loop" to "merge" * merge multiagent_envs into envs * changed all apperance of green wave to traffic light grid * Fixed names for scenarios so that they match the environments. Everything is uniform now * moved scenario files to network and renamed Scenario->Network * removed all non-aimsun mentioned of scenario * added pending deprecation warning for scenario use * Removing cooperative_merge (later we called it "TwoRingsOneMerge" * pep8 * added deprecation warning for scenario to env input * Refactoring new files that are added after pulling from master * Change 'grid' to 'traffic_light_grid' across all files * addressed flake8 issues * PR fixes * PR fix * Fixed test_visualiszer.py errors * working version of simulate.py * bug fixes * fixed tests for simulate * cleanup * added script for running single agent rllib experiments * added multiagent rllib support to examples * Remove data.json * pydocstyle fixes * pep8 * modified some examples * added stable baselines support * bug fix * added aimsun template to exp_configs * added missing files * Update README.md * Update readme to reflect how to run examples, and to add all the examples * Renamed several files so the new naming matches our new naming convention * Update readme, flow_setup, change example paths in files * Update tutorial 00 for the new structure of examples * Rename density_exp and also update the ray_auoscale.yaml * fix train_rllib * fix pep8 issues * fix test for highway * check multiagent file name, remove HTTPS * properly update config dictionary * Fix the follwoing multiagent examples:{figure_eight, highway, ring} * Resolve flake8 issues * Fix errors in test_experiment_base_class * Fixing class TestCollisions with the new way of running experiments * pep8 * Fix errors in test_contollers * Fix the tests with ring_road_exp_setup * flake8 * Revert "flake8" This reverts commit 52d1a099fcc9c89a82a36362f4061cfa4451b8ce. * Revert "Fix the tests with ring_road_exp_setup" This reverts commit 1659fb3582b2078dd5a8cea56cd8817c810800b0. * Fix all tests that use setup_scripts * pep8 * Fix benchmark tests * (Thanks Kanaad) Fix the error that was causing many benchmarks and examples fail * Resolve merge conflict * Fix test_experiment_base_class errors * Fix test_collisions errors * Trying isolating bug * Testing to see if TestVisualizerRLlib is the issue * Testing if test_benchmarks is the problem * Revert "Testing if test_benchmarks is the problem" This reverts commit 050f5db2cf54f7ba69df08e367f430f393ba6c47. * Revert "Testing to see if TestVisualizerRLlib is the issue" This reverts commit 42978c49ec0ada00c78d9944780691e3f61febd1. * Revert "Trying isolating bug" This reverts commit c359d8fc9c8c32d000c102b5a2622a11e23660df. * trying to see if the error is in slow tests * Trying to see if test_examples or test_visualizers is the issue * Trying to see where the error occurs in fast_tests * See if example rename is the issue * typo * some minor cleanup * fixed some tests * resolved multiple registery calls bug * Fixing the random start of vehicles on ring * Fixing the bug that comes up randomly * Trying to remove randomness * flake8 issues * Remove unnecessary debugging code * update the hierarchy of exp_config to be more logical * Update examples/README.md Co-Authored-By: Aboudy Kreidieh * Update examples/README.md Co-Authored-By: Aboudy Kreidieh * Address aboudy's comments Co-authored-by: Nathan Lichtlé Co-authored-by: Ashkan Y. Co-authored-by: Kanaad Parvate --- docs/source/examples.rst | 9 +- docs/source/flow_setup.rst | 63 ++-- docs/source/multiagent.rst | 2 +- examples/README.md | 98 ++++-- examples/aimsun/bottlenecks.py | 108 ------- examples/aimsun/figure_eight.py | 60 ---- examples/aimsun/grid.py | 121 ------- examples/aimsun/merge.py | 96 ------ examples/aimsun/small_template.py | 36 --- examples/aimsun/sugiyama.py | 62 ---- .../exp_configs/non_rl/aimsun_template.py | 58 ++++ examples/exp_configs/non_rl/bay_bridge.py | 195 ++++++++++++ .../exp_configs/non_rl/bay_bridge_toll.py | 125 ++++++++ examples/exp_configs/non_rl/bottleneck.py | 108 +++++++ examples/exp_configs/non_rl/figure_eight.py | 62 ++++ examples/exp_configs/non_rl/highway.py | 78 +++++ examples/exp_configs/non_rl/highway_ramps.py | 113 +++++++ examples/exp_configs/non_rl/merge.py | 97 ++++++ examples/exp_configs/non_rl/minicity.py | 79 +++++ examples/exp_configs/non_rl/ring.py | 61 ++++ .../exp_configs/non_rl/traffic_light_grid.py | 235 ++++++++++++++ .../rl/multiagent}/multiagent_figure_eight.py | 126 ++------ .../rl/multiagent}/multiagent_highway.py | 140 +++----- .../rl/multiagent/multiagent_ring.py} | 119 ++----- .../multiagent_traffic_light_grid.py | 2 +- .../rl/singleagent/singleagent_bottleneck.py} | 76 +---- .../singleagent/singleagent_figure_eight.py} | 83 +---- .../rl/singleagent/singleagent_merge.py} | 82 +---- .../rl/singleagent/singleagent_ring.py} | 78 +---- .../singleagent_traffic_light_grid.py} | 120 ++----- ...tleneck_density_sweep_capacity_diagram.py} | 0 examples/rllib/__init__.py | 1 - examples/rllib/multiagent_exps/__init__.py | 1 - examples/simulate.py | 71 +++++ examples/stable_baselines/figure_eight.py | 146 --------- .../stable_baselines/stabilizing_highway.py | 187 ----------- .../stable_baselines/stabilizing_the_ring.py | 143 --------- .../stable_baselines/traffic_light_grid.py | 301 ------------------ .../stable_baselines/velocity_bottleneck.py | 211 ------------ examples/sumo/__init__.py | 1 - examples/sumo/bay_bridge.py | 204 ------------ examples/sumo/bay_bridge_toll.py | 132 -------- examples/sumo/bottlenecks.py | 202 ------------ examples/sumo/figure_eight.py | 69 ---- examples/sumo/highway.py | 81 ----- examples/sumo/highway_ramps.py | 116 ------- examples/sumo/merge.py | 100 ------ examples/sumo/minicity.py | 101 ------ examples/sumo/sugiyama.py | 68 ---- examples/sumo/traffic_light_grid.py | 233 -------------- examples/train_rllib.py | 157 +++++++++ examples/train_stable_baselines.py | 126 ++++++++ flow/benchmarks/baselines/bottleneck0.py | 28 +- flow/benchmarks/baselines/bottleneck1.py | 28 +- flow/benchmarks/baselines/bottleneck2.py | 28 +- flow/benchmarks/baselines/figureeight012.py | 29 +- flow/benchmarks/baselines/grid0.py | 28 +- flow/benchmarks/baselines/grid1.py | 28 +- flow/benchmarks/baselines/merge012.py | 30 +- flow/core/experiment.py | 31 +- flow/envs/base.py | 0 .../{__iniy__.py => __init__.py} | 0 flow/multiagent_envs/loop/loop_accel.py | 2 +- flow/utils/registry.py | 34 +- flow/visualize/capacity_diagram_generator.py | 2 +- tests/fast_tests/test_collisions.py | 23 +- tests/fast_tests/test_controllers.py | 33 +- .../fast_tests/test_environment_base_class.py | 20 +- tests/fast_tests/test_examples.py | 285 ++++++++--------- .../fast_tests/test_experiment_base_class.py | 102 ++++-- tests/fast_tests/test_files/ring_230.json | 2 +- tests/fast_tests/test_rewards.py | 20 +- tests/fast_tests/test_scenario_base_class.py | 38 +-- tests/fast_tests/test_traffic_light_grid.py | 10 +- tests/fast_tests/test_traffic_lights.py | 24 +- tests/fast_tests/test_vehicles.py | 20 +- tests/setup_scripts.py | 188 ++++++++++- tests/slow_tests/test_benchmarks.py | 5 + tests/stress_tests/stress_test_start.py | 2 +- tutorials/tutorial00_flow.ipynb | 11 +- tutorials/tutorial08_environments.ipynb | 4 +- tutorials/tutorial10_traffic_lights.ipynb | 10 +- tutorials/tutorial12_bottlenecks.ipynb | 6 +- tutorials/tutorial13_rllib_ec2.ipynb | 6 +- 84 files changed, 2395 insertions(+), 4025 deletions(-) delete mode 100644 examples/aimsun/bottlenecks.py delete mode 100644 examples/aimsun/figure_eight.py delete mode 100644 examples/aimsun/grid.py delete mode 100644 examples/aimsun/merge.py delete mode 100644 examples/aimsun/small_template.py delete mode 100644 examples/aimsun/sugiyama.py create mode 100644 examples/exp_configs/non_rl/aimsun_template.py create mode 100644 examples/exp_configs/non_rl/bay_bridge.py create mode 100644 examples/exp_configs/non_rl/bay_bridge_toll.py create mode 100644 examples/exp_configs/non_rl/bottleneck.py create mode 100755 examples/exp_configs/non_rl/figure_eight.py create mode 100644 examples/exp_configs/non_rl/highway.py create mode 100644 examples/exp_configs/non_rl/highway_ramps.py create mode 100644 examples/exp_configs/non_rl/merge.py create mode 100644 examples/exp_configs/non_rl/minicity.py create mode 100755 examples/exp_configs/non_rl/ring.py create mode 100644 examples/exp_configs/non_rl/traffic_light_grid.py rename examples/{rllib/multiagent_exps => exp_configs/rl/multiagent}/multiagent_figure_eight.py (54%) rename examples/{rllib/multiagent_exps => exp_configs/rl/multiagent}/multiagent_highway.py (61%) rename examples/{rllib/multiagent_exps/multiagent_stabilizing_the_ring.py => exp_configs/rl/multiagent/multiagent_ring.py} (55%) rename examples/{rllib/multiagent_exps => exp_configs/rl/multiagent}/multiagent_traffic_light_grid.py (99%) rename examples/{rllib/velocity_bottleneck.py => exp_configs/rl/singleagent/singleagent_bottleneck.py} (68%) rename examples/{rllib/figure_eight.py => exp_configs/rl/singleagent/singleagent_figure_eight.py} (51%) rename examples/{rllib/stabilizing_highway.py => exp_configs/rl/singleagent/singleagent_merge.py} (63%) rename examples/{rllib/stabilizing_the_ring.py => exp_configs/rl/singleagent/singleagent_ring.py} (55%) rename examples/{rllib/traffic_light_grid.py => exp_configs/rl/singleagent/singleagent_traffic_light_grid.py} (66%) rename examples/{sumo/density_exp.py => exp_scripts/bottleneck_density_sweep_capacity_diagram.py} (100%) delete mode 100644 examples/rllib/__init__.py delete mode 100644 examples/rllib/multiagent_exps/__init__.py create mode 100644 examples/simulate.py delete mode 100644 examples/stable_baselines/figure_eight.py delete mode 100644 examples/stable_baselines/stabilizing_highway.py delete mode 100644 examples/stable_baselines/stabilizing_the_ring.py delete mode 100644 examples/stable_baselines/traffic_light_grid.py delete mode 100644 examples/stable_baselines/velocity_bottleneck.py delete mode 100644 examples/sumo/__init__.py delete mode 100644 examples/sumo/bay_bridge.py delete mode 100644 examples/sumo/bay_bridge_toll.py delete mode 100644 examples/sumo/bottlenecks.py delete mode 100755 examples/sumo/figure_eight.py delete mode 100644 examples/sumo/highway.py delete mode 100644 examples/sumo/highway_ramps.py delete mode 100644 examples/sumo/merge.py delete mode 100644 examples/sumo/minicity.py delete mode 100755 examples/sumo/sugiyama.py delete mode 100644 examples/sumo/traffic_light_grid.py create mode 100644 examples/train_rllib.py create mode 100644 examples/train_stable_baselines.py mode change 100755 => 100644 flow/envs/base.py rename flow/multiagent_envs/{__iniy__.py => __init__.py} (100%) diff --git a/docs/source/examples.rst b/docs/source/examples.rst index 7b6944547..74bb08ba1 100644 --- a/docs/source/examples.rst +++ b/docs/source/examples.rst @@ -43,11 +43,10 @@ In this scenario, human drivers placed on a ring develop a travelling wave that decelerations and lowers the average velocity of the system. The goal is to train a single autonomous vehicle to eliminate the shockwave. -The pre-built run script for running this in human-only mode is `examples/sumo/sugiyama.py`. -The run scripts in rllib and aimsun are `examples/rllib/stabilizing_the_ring.py` and -`examples/aimsun/stabilizing_the_ring.py`. These control environments are partially observed: -the autonomous vehicle only observes its own distance to the leading vehicle, its speed, -and the speed of the leading vehicle. +The experiment config for this in human-only mode (non-RL) is `examples/exp_configs/non_rl/ring.py`. +The experiment config for RL-based is `examples/exp_configs/rl/singleagent/singleagent_ring.py`. +These control environments are partially observed: the autonomous vehicle only observes its own +distance to the leading vehicle, its speed, and the speed of the leading vehicle. To make this task more difficult, the environment has a configurable parameter, `ring_length`, which can be set to a list containing the minimum and maximum ring-size. The autonomous vehicle must diff --git a/docs/source/flow_setup.rst b/docs/source/flow_setup.rst index 8a2650fad..2b52e20de 100644 --- a/docs/source/flow_setup.rst +++ b/docs/source/flow_setup.rst @@ -110,8 +110,8 @@ Note that, if the above commands did not work, you may need to run *Troubleshooting*: If you are a Mac user and the above command gives you the error ``FXApp:openDisplay: unable to open display :0.0``, make sure to open the application XQuartz. -Testing your installation -~~~~~~~~~~~~~~~~~~~~~~~~~ +Testing your SUMO and Flow installation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ Once the above modules have been successfully installed, we can test the installation by running a few examples. Before trying to run any examples, be @@ -124,17 +124,17 @@ sure to enter your conda environment by typing: Let’s see some traffic action: :: - - python examples/sumo/sugiyama.py + + python examples/simulate.py ring Running the following should result in the loading of the SUMO GUI. Click the run button and you should see unstable traffic form after a few seconds, a la (Sugiyama et al, 2008). This means that you have Flow -properly configured with SUMO and Flow! +properly configured with SUMO! (Optional) Installing Aimsun ------------------ +---------------------------- In addition to SUMO, Flow supports the use of the traffic simulator "Aimsun". In order setup Flow with Aimsun, you will first need to install Aimsun. This @@ -185,8 +185,8 @@ place it under the `AIMSUN_SITEPACKAGES` variable in your bashrc, like this: export AIMSUN_SITEPACKAGES="/path/to/envs/aimsun_flow" -Testing your installation -~~~~~~~~~~~~~~~~~~~~~~~~~ +Testing your Aimsun installation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ To test that you installation was successful, you can try running one of the Aimsun examples within the Flow main directory. In order to do so, you need @@ -196,7 +196,7 @@ to activate the `flow` env. Type: source deactivate aimsun_flow source activate flow - python examples/aimsun/sugiyama.py + python examples/simulate.py ring --aimsun *Troubleshootig for Ubuntu users with Aimsun 8.4*: when you run the above example, you may get a subprocess.Popen error ``OSError: [Errno 8] Exec format error:``. To fix this, go to the `Aimsun Next` main directory, open the `Aimsun_Next` binary with a text editor and add the shebang to the first line of the script ``#!/bin/sh``. @@ -228,22 +228,8 @@ required libraries as specified at and then follow the setup instructions. -(Optional) Install Stable Baselines ----------------------------- - -An additional library that Flow supports is the fork of OpenAI's Baselines, Stable-Baselines. -First visit and -install the required packages and pip install the stable baselines package as described in their -installation instructions. - -You can test your installation by running - -:: - - python examples/stable_baselines/stabilizing_the_ring.py - -Testing your installation -~~~~~~~~~~~~~~~~~~~~~~~~~ +Testing your RLlib installation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ See `getting started with RLlib `_ for sample commands. @@ -257,11 +243,34 @@ In order to test run an Flow experiment in RLlib, try the following command: :: - python examples/rllib/stabilizing_the_ring.py + python examples/train_rllib.py singleagent_ring + If it does not fail, this means that you have Flow properly configured with RLlib. +(Optional) Install Stable Baselines +----------------------------------- + +An additional library that Flow supports is the fork of OpenAI's Baselines, Stable-Baselines. +First visit and +install the required packages and pip install the stable baselines package as described in their +installation instructions. + +Testing your Stable Baselines installation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +You can test your installation by running + +:: + + python examples/train_stable_baselines.py singleagent_ring + + + +(Optional) Visualizing with Tensorboard +--------------------------------------- + To visualize the training progress: :: @@ -285,6 +294,8 @@ jobs from there. ray teardown scripts/ray_autoscale.yaml + + (Optional) Direct install of SUMO from GitHub --------------------------------------------- diff --git a/docs/source/multiagent.rst b/docs/source/multiagent.rst index 1e4fa458a..4ce1ce209 100644 --- a/docs/source/multiagent.rst +++ b/docs/source/multiagent.rst @@ -50,4 +50,4 @@ A brief example of a multiagent environment: For further details look at our -`multiagent examples `_. +`multiagent examples `_. diff --git a/examples/README.md b/examples/README.md index f612bb1fb..907c7bda1 100644 --- a/examples/README.md +++ b/examples/README.md @@ -1,40 +1,79 @@ # Flow Examples Before continuing to the Flow examples, we recommend **installing Flow** by -executing the following [installation instructions]( +following the [installation instructions]( https://flow.readthedocs.io/en/latest/flow_setup.html). The **examples** folder provides several examples demonstrating how -both simulation and RL-oriented experiments can be setup and executed within -the Flow framework on a variety of traffic problems. These examples are .py -files that may be executed either from terminal or via an editor. For example, -in order to execute the sugiyama example in *examples/sumo*, we run: +both non-RL simulation and RL-oriented simulatons can be setup and executed +within the Flow framework on a variety of traffic problems. These examples are +python files that may be executed either from terminal or via a text editor. +For example, in order to execute the non-RL Ring example we run: ```shell -python /examples/sumo/sugiyama.py +python simulate.py ring ``` -The examples are distributed into the following sections: +The examples are categorized into the following 3 sections: -**examples/sumo/** contains examples of transportation network with vehicles +**non-RL examples** contains examples of transportation network with vehicles following human-dynamical models of driving behavior using the traffic -micro-simulator sumo. +micro-simulator sumo and traffic macro-simulator Aimsun. -**examples/aimsun/** contains examples of transportation network with vehicles -following human-dynamical models of driving behavior using the traffic -micro-simulator Aimsun. +To execute these examples, run + +```shell +python simulate.py EXP_CONFIG +``` +where `EXP_CONFIG` is the name of the experiment configuration file, as located in +`exp_configs/non_rl.` + +There are several *optional* arguments that can be added to the above command: + +```shell + python simulate.py EXP_CONFIG --num_runs n --render --aimsun --gen_emission +``` +where `--num_runs` indicates the number of simulations to run (default of `n` is 1), `--render` indicates whether to run the simulation during runtime (default is False), `--aimsun` indicates whether to run the simulation using the simulator Aimsun (the default simulator is SUMO), and `--gen_emission` indicates whether to generate an emission file from the simulation. + +**RL examples based on RLlib** provides similar networks as those presented in +the first point, but in the present of autonomous vehicle (AV) or traffic light agents +being trained through RL algorithms provided by *RLlib*. + +To execute these examples, run + +```shell + python train_rllib.py EXP_CONFIG +``` +where `EXP_CONFIG` is the name of the experiment configuration file, as located in +`exp_configs/rl/singleagent` or `exp_configs/rl/multiagent.` -**examples/rllib/** provides similar networks as those presented in the -previous point, but in the present of autonomous vehicle (AV) or traffic light -agents being trained through RL algorithms provided by *RLlib*. +**RL examples based on stable_baselines** provides similar networks as those +presented in the first point, but in the present of autonomous vehicle (AV) or traffic +light agents being trained through RL algorithms provided by OpenAI *stable +baselines*. + +To execute these examples, run + +```shell + python train_stable_baselines.py EXP_CONFIG +``` +where `EXP_CONFIG` is the name of the experiment configuration file, as located in +`exp_configs/rl/singleagent.` + +There are several *optional* arguments that can be added to the above command: + +```shell + python train_stable_baselines.py EXP_CONFIG --num_cpus n1 --num_steps n2 --rollout_size r +``` +where `--num_cpus` indicates the number of CPUs to use (default of `n1` is 1), `--num_steps` indicates the total steps to perform the learning (default of `n2` is 5000), and `--rollout_size` indicates the number of steps in a training batch (default of `r` is 1000) ## Simulated Examples -The following networks are available for simulation within flow, and -specifically the **examples/sumo** folder. Similar networks are available with -trainable variants in the examples/rllib and examples/aimsun folders; however, -they may be under different names. +The following networks are available for simulation within flow. These examples are +all available as non-RL examples, while some of them are also available (with +trainable variants) as RL examples, with RLlib or Stable Baselines. + ### bay_bridge.py \& bay_bridge_toll.py @@ -45,7 +84,7 @@ only on the toll booth and sections of the road leading up to it. ![](https://raw.githubusercontent.com/flow-project/flow/master/docs/img/bay_bridge.gif) -### bottlenecks.py +### bottleneck.py Example demonstrating formation of congestion in bottleneck @@ -73,9 +112,15 @@ Example of an open multi-lane network with human-driven vehicles. ![](https://raw.githubusercontent.com/flow-project/flow/master/docs/img/highway.gif) +### highway_ramps.py + +Example of a highway section network with on/off ramps + +![](picture to be added) + ### merge.py -Example of a merge network with human-driven vehicles. +Example of a straight road with an on-ramp merge. In the absence of autonomous vehicles, the network exhibits properties of convective instability, with perturbations propagating upstream from the merge @@ -85,15 +130,16 @@ point before exiting the network. ### minicity.py -Example of modified minicity of University of Delaware network with -human-driven vehicles. +Example of modified mini city developed under a +[collaboration with University of Delaware](https://sites.google.com/view/iccps-policy-transfer), +with human-driven vehicles. ![](https://raw.githubusercontent.com/flow-project/flow/master/docs/img/minicity.gif) -### sugiyama.py +### ring.py -Used as an example of sugiyama experiment. +Used as an example of a ring experiment. -This example consists of 22 IDM cars on a ring road creating shockwaves. +This example consists of 22 IDM cars driving on a ring road creating shockwaves. ![](https://raw.githubusercontent.com/flow-project/flow/master/docs/img/sugiyama.gif) diff --git a/examples/aimsun/bottlenecks.py b/examples/aimsun/bottlenecks.py deleted file mode 100644 index eabd2c135..000000000 --- a/examples/aimsun/bottlenecks.py +++ /dev/null @@ -1,108 +0,0 @@ -"""File demonstrating formation of congestion in bottleneck.""" - -from flow.core.params import AimsunParams, EnvParams, NetParams, \ - InitialConfig, InFlows -from flow.core.params import VehicleParams -from flow.core.params import TrafficLightParams - -from flow.networks.bottleneck import BottleneckNetwork -from flow.envs.bottleneck import BottleneckEnv -from flow.core.experiment import Experiment - -SCALING = 1 -DISABLE_TB = True -# If set to False, ALINEA will control the ramp meter -DISABLE_RAMP_METER = True -INFLOW = 1800 - - -def bottleneck_example(flow_rate, horizon, restart_instance=False, - render=None): - """ - Perform a simulation of vehicles on a bottleneck. - - Parameters - ---------- - flow_rate : float - total inflow rate of vehicles into the bottleneck - horizon : int - time horizon - restart_instance: bool, optional - whether to restart the instance upon reset - render: bool, optional - specifies whether to use the gui during execution - - Returns - ------- - exp: flow.core.experiment.Experiment - A non-rl experiment demonstrating the performance of human-driven - vehicles on a bottleneck. - """ - if render is None: - render = False - - sim_params = AimsunParams( - sim_step=0.5, - render=render, - restart_instance=restart_instance) - - vehicles = VehicleParams() - - vehicles.add( - veh_id="human", - num_vehicles=1) - - additional_env_params = { - "target_velocity": 40, - "max_accel": 1, - "max_decel": 1, - "lane_change_duration": 5, - "add_rl_if_exit": False, - "disable_tb": DISABLE_TB, - "disable_ramp_metering": DISABLE_RAMP_METER - } - env_params = EnvParams( - horizon=horizon, additional_params=additional_env_params) - - inflow = InFlows() - inflow.add( - veh_type="human", - edge="1", - vehsPerHour=flow_rate, - departLane="random", - departSpeed=10) - - traffic_lights = TrafficLightParams() - if not DISABLE_TB: - traffic_lights.add(node_id="2") - if not DISABLE_RAMP_METER: - traffic_lights.add(node_id="3") - - additional_net_params = {"scaling": SCALING, "speed_limit": 30/3.6} - net_params = NetParams( - inflows=inflow, - additional_params=additional_net_params) - - initial_config = InitialConfig( - spacing="random", - min_gap=5, - lanes_distribution=float("inf"), - edges_distribution=["2", "3", "4", "5"]) - - network = BottleneckNetwork( - name="bay_bridge_toll", - vehicles=vehicles, - net_params=net_params, - initial_config=initial_config, - traffic_lights=traffic_lights) - - env = BottleneckEnv(env_params, sim_params, network, simulator='aimsun') - - return Experiment(env) - - -if __name__ == '__main__': - # import the experiment variable - # inflow, number of steps, binary - exp = bottleneck_example(INFLOW, 1000, render=True) - exp.run(5, 1000) diff --git a/examples/aimsun/figure_eight.py b/examples/aimsun/figure_eight.py deleted file mode 100644 index 195388792..000000000 --- a/examples/aimsun/figure_eight.py +++ /dev/null @@ -1,60 +0,0 @@ -"""Example of a figure 8 network with human-driven vehicles. - -Right-of-way dynamics near the intersection causes vehicles to queue up on -either side of the intersection, leading to a significant reduction in the -average speed of vehicles in the network. -""" -from flow.controllers import IDMController -from flow.core.experiment import Experiment -from flow.core.params import AimsunParams, EnvParams, NetParams -from flow.core.params import VehicleParams -from flow.envs import TestEnv -from flow.networks.figure_eight import FigureEightNetwork, ADDITIONAL_NET_PARAMS - - -def figure_eight_example(render=None): - """Perform a simulation of vehicles on a figure eight. - - Parameters - ---------- - render: bool, optional - specifies whether to use the gui during execution - - Returns - ------- - exp: flow.core.experiment.Experiment - A non-rl experiment demonstrating the performance of human-driven - vehicles on a figure eight. - """ - sim_params = AimsunParams(sim_step=0.5, render=False, emission_path='data') - - if render is not None: - sim_params.render = render - - vehicles = VehicleParams() - vehicles.add( - veh_id="human", - acceleration_controller=(IDMController, {}), - num_vehicles=14) - - env_params = EnvParams() - - net_params = NetParams( - additional_params=ADDITIONAL_NET_PARAMS.copy()) - - network = FigureEightNetwork( - name="figure8", - vehicles=vehicles, - net_params=net_params) - - env = TestEnv(env_params, sim_params, network, simulator='aimsun') - - return Experiment(env) - - -if __name__ == "__main__": - # import the experiment variable - exp = figure_eight_example(render=True) - - # run for a set number of rollouts / time steps - exp.run(1, 1500) diff --git a/examples/aimsun/grid.py b/examples/aimsun/grid.py deleted file mode 100644 index 042d8828e..000000000 --- a/examples/aimsun/grid.py +++ /dev/null @@ -1,121 +0,0 @@ -"""Traffic Light Grid example.""" -from flow.core.experiment import Experiment -from flow.core.params import AimsunParams, EnvParams, InitialConfig, NetParams -from flow.core.params import VehicleParams -from flow.core.params import TrafficLightParams -from flow.envs.ring.accel import AccelEnv, ADDITIONAL_ENV_PARAMS -from flow.networks import TrafficLightGridNetwork - - -def traffic_light_grid_example(render=None): - """ - Perform a simulation of vehicles on a traffic light grid. - - Parameters - ---------- - render: bool, optional - specifies whether to use the gui during execution - - Returns - ------- - exp: flow.core.experiment.Experiment - A non-rl experiment demonstrating the performance of human-driven - vehicles and balanced traffic lights on a traffic light grid. - """ - inner_length = 300 - long_length = 500 - short_length = 300 - N_ROWS = 2 - N_COLUMNS = 3 - num_cars_left = 20 - num_cars_right = 20 - num_cars_top = 20 - num_cars_bot = 20 - tot_cars = (num_cars_left + num_cars_right) * N_COLUMNS \ - + (num_cars_top + num_cars_bot) * N_ROWS - - grid_array = { - "short_length": short_length, - "inner_length": inner_length, - "long_length": long_length, - "row_num": N_ROWS, - "col_num": N_COLUMNS, - "cars_left": num_cars_left, - "cars_right": num_cars_right, - "cars_top": num_cars_top, - "cars_bot": num_cars_bot - } - - sim_params = AimsunParams(sim_step=0.5, render=True) - - if render is not None: - sim_params.render = render - - vehicles = VehicleParams() - vehicles.add( - veh_id="human", - num_vehicles=tot_cars) - - env_params = EnvParams(additional_params=ADDITIONAL_ENV_PARAMS) - - tl_logic = TrafficLightParams(baseline=False) - phases = [{ - "duration": "31", - "minDur": "8", - "maxDur": "45", - "yellow": "3", - "state": "GGGrrrGGGrrr" - }, { - "duration": "6", - "minDur": "3", - "maxDur": "6", - "yellow": "3", - "state": "yyyrrryyyrrr" - }, { - "duration": "31", - "minDur": "8", - "maxDur": "45", - "yellow": "3", - "state": "rrrGGGrrrGGG" - }, { - "duration": "6", - "minDur": "3", - "maxDur": "6", - "yellow": "3", - "state": "rrryyyrrryyy" - }] - tl_logic.add("center0", phases=phases, programID=1) - tl_logic.add("center1", phases=phases, programID=1) - tl_logic.add("center2", tls_type="actuated", phases=phases, programID=1) - tl_logic.add("center3", phases=phases, programID=1) - tl_logic.add("center4", phases=phases, programID=1) - tl_logic.add("center5", tls_type="actuated", phases=phases, programID=1) - - additional_net_params = { - "grid_array": grid_array, - "speed_limit": 35, - "horizontal_lanes": 1, - "vertical_lanes": 1 - } - net_params = NetParams(additional_params=additional_net_params) - - initial_config = InitialConfig(spacing='custom') - - network = TrafficLightGridNetwork( - name="grid-intersection", - vehicles=vehicles, - net_params=net_params, - initial_config=initial_config, - traffic_lights=tl_logic) - - env = AccelEnv(env_params, sim_params, network, simulator='aimsun') - - return Experiment(env) - - -if __name__ == "__main__": - # import the experiment variable - exp = traffic_light_grid_example() - - # run for a set number of rollouts / time steps - exp.run(1, 1500) diff --git a/examples/aimsun/merge.py b/examples/aimsun/merge.py deleted file mode 100644 index 813e642b4..000000000 --- a/examples/aimsun/merge.py +++ /dev/null @@ -1,96 +0,0 @@ -"""Example of a merge network with human-driven vehicles. - -In the absence of autonomous vehicles, the network exhibits properties of -convective instability, with perturbations propagating upstream from the merge -point before exiting the network. -""" - -import flow.core.params as params -from flow.core.experiment import Experiment -from flow.networks.merge import MergeNetwork, ADDITIONAL_NET_PARAMS -from flow.controllers import IDMController -from flow.envs.merge import MergePOEnv, ADDITIONAL_ENV_PARAMS - -# inflow rate at the highway -HIGHWAY_RATE = 2000 -# inflow rate at the on-ramp -MERGE_RATE = 500 - - -def merge_example(render=None): - """Perform a simulation of vehicles on a merge. - - Parameters - ---------- - render: bool, optional - specifies whether to use the gui during execution - - Returns - ------- - exp: flow.core.experiment.Experiment - A non-rl experiment demonstrating the performance of human-driven - vehicles on a merge. - """ - sim_params = params.AimsunParams( - render=True, - emission_path="./data/", - sim_step=0.5, - restart_instance=False) - - if render is not None: - sim_params.render = render - - vehicles = params.VehicleParams() - vehicles.add( - veh_id="human", - acceleration_controller=(IDMController, { - "noise": 0.2 - }), - num_vehicles=5) - - env_params = params.EnvParams( - additional_params=ADDITIONAL_ENV_PARAMS, - sims_per_step=5, - warmup_steps=0) - - inflow = params.InFlows() - inflow.add( - veh_type="human", - edge="inflow_highway", - vehs_per_hour=HIGHWAY_RATE, - departLane="free", - departSpeed=10) - inflow.add( - veh_type="human", - edge="inflow_merge", - vehs_per_hour=MERGE_RATE, - departLane="free", - departSpeed=7.5) - - additional_net_params = ADDITIONAL_NET_PARAMS.copy() - additional_net_params["merge_lanes"] = 1 - additional_net_params["highway_lanes"] = 1 - additional_net_params["pre_merge_length"] = 500 - net_params = params.NetParams( - inflows=inflow, - additional_params=additional_net_params) - - initial_config = params.InitialConfig() - - network = MergeNetwork( - name="merge-baseline", - vehicles=vehicles, - net_params=net_params, - initial_config=initial_config) - - env = MergePOEnv(env_params, sim_params, network, simulator='aimsun') - - return Experiment(env) - - -if __name__ == "__main__": - # import the experiment variable - exp = merge_example() - - # run for a set number of rollouts / time steps - exp.run(1, 3600) diff --git a/examples/aimsun/small_template.py b/examples/aimsun/small_template.py deleted file mode 100644 index d70f6632b..000000000 --- a/examples/aimsun/small_template.py +++ /dev/null @@ -1,36 +0,0 @@ -"""Load an already existing Aimsun template and run the simulation.""" - -from flow.core.experiment import Experiment -from flow.core.params import AimsunParams, EnvParams, NetParams -from flow.core.params import VehicleParams -from flow.envs import TestEnv -from flow.networks import Network -from flow.core.params import InFlows -import flow.config as config -import os - -sim_params = AimsunParams( - sim_step=0.1, - render=True, - emission_path='data', - replication_name="Replication 930", - centroid_config_name="Centroid Configuration 910") - -env_params = EnvParams() -vehicles = VehicleParams() - -template_path = os.path.join(config.PROJECT_PATH, - "flow/utils/aimsun/small_template.ang") - -network = Network( - name="aimsun_small_template", - vehicles=vehicles, - net_params=NetParams( - inflows=InFlows(), - template=template_path - ) -) - -env = TestEnv(env_params, sim_params, network, simulator='aimsun') -exp = Experiment(env) -exp.run(1, 3000) diff --git a/examples/aimsun/sugiyama.py b/examples/aimsun/sugiyama.py deleted file mode 100644 index ac0e7d1a4..000000000 --- a/examples/aimsun/sugiyama.py +++ /dev/null @@ -1,62 +0,0 @@ -"""Used as an example of sugiyama experiment. - -This example consists of 22 IDM cars on a ring creating shockwaves. -""" - -from flow.controllers import IDMController -from flow.core.experiment import Experiment -from flow.core.params import AimsunParams, EnvParams, InitialConfig, NetParams -from flow.core.params import VehicleParams -from flow.envs import TestEnv -from flow.networks.ring import RingNetwork, ADDITIONAL_NET_PARAMS - - -def sugiyama_example(render=None): - """Perform a simulation of vehicles on a ring road. - - Parameters - ---------- - render : bool, optional - specifies whether to use the gui during execution - - Returns - ------- - exp: flow.core.experiment.Experiment - A non-rl experiment demonstrating the performance of human-driven - vehicles on a ring road. - """ - sim_params = AimsunParams(sim_step=0.5, render=True, emission_path='data') - - if render is not None: - sim_params.render = render - - vehicles = VehicleParams() - vehicles.add( - veh_id="idm", - acceleration_controller=(IDMController, {}), - num_vehicles=22) - - env_params = EnvParams() - - additional_net_params = ADDITIONAL_NET_PARAMS.copy() - net_params = NetParams(additional_params=additional_net_params) - - initial_config = InitialConfig(bunching=20) - - network = RingNetwork( - name="sugiyama", - vehicles=vehicles, - net_params=net_params, - initial_config=initial_config) - - env = TestEnv(env_params, sim_params, network, simulator='aimsun') - - return Experiment(env) - - -if __name__ == "__main__": - # import the experiment variable - exp = sugiyama_example() - - # run for a set number of rollouts / time steps - exp.run(1, 3000) diff --git a/examples/exp_configs/non_rl/aimsun_template.py b/examples/exp_configs/non_rl/aimsun_template.py new file mode 100644 index 000000000..e035074d9 --- /dev/null +++ b/examples/exp_configs/non_rl/aimsun_template.py @@ -0,0 +1,58 @@ +"""Load an already existing Aimsun template and run the simulation.""" +from flow.core.params import AimsunParams, EnvParams, NetParams +from flow.core.params import VehicleParams +from flow.core.params import InFlows +import flow.config as config +from flow.envs import TestEnv +from flow.networks import Network +import os + +# inflow rate at the highway +FLOW_RATE = 2000 + +# no vehicles in the network +vehicles = VehicleParams() + +# path to the imported Aimsun template +template_path = os.path.join(config.PROJECT_PATH, + "flow/utils/aimsun/small_template.ang") + + +flow_params = dict( + # name of the experiment + exp_tag='aimsun_small_template', + + # name of the flow environment the experiment is running on + env_name=TestEnv, + + # name of the network class the experiment is running on + network=Network, + + # simulator that is used by the experiment + simulator='aimsun', + + # sumo-related parameters (see flow.core.params.SumoParams) + sim=AimsunParams( + sim_step=0.1, + render=True, + emission_path='data', + replication_name="Replication 930", + centroid_config_name="Centroid Configuration 910" + ), + + # environment related parameters (see flow.core.params.EnvParams) + env=EnvParams( + horizon=3000, + ), + + # network-related parameters (see flow.core.params.NetParams and the + # network's documentation or ADDITIONAL_NET_PARAMS component) + net=NetParams( + inflows=InFlows(), + template=template_path + ), + + # vehicles to be placed in the network at the start of a rollout (see + # flow.core.params.VehicleParams) + veh=vehicles, +) diff --git a/examples/exp_configs/non_rl/bay_bridge.py b/examples/exp_configs/non_rl/bay_bridge.py new file mode 100644 index 000000000..d7d78360f --- /dev/null +++ b/examples/exp_configs/non_rl/bay_bridge.py @@ -0,0 +1,195 @@ +"""Bay Bridge simulation.""" + +import os +import urllib.request + +from flow.core.params import SumoParams, EnvParams, NetParams, InitialConfig, \ + SumoCarFollowingParams, SumoLaneChangeParams, InFlows +from flow.core.params import VehicleParams +from flow.core.params import TrafficLightParams +from flow.networks.bay_bridge import EDGES_DISTRIBUTION +from flow.controllers import SimCarFollowingController, BayBridgeRouter +from flow.envs import BayBridgeEnv +from flow.networks import BayBridgeNetwork + +USE_TRAFFIC_LIGHTS = False +USE_INFLOWS = False + + +TEMPLATE = os.path.join( + os.path.dirname(os.path.abspath(__file__)), "bay_bridge.net.xml") + +# download the template from AWS +if USE_TRAFFIC_LIGHTS: + my_url = "http://s3-us-west-1.amazonaws.com/flow.netfiles/" \ + "bay_bridge_TL_all_green.net.xml" +else: + my_url = "http://s3-us-west-1.amazonaws.com/flow.netfiles/" \ + "bay_bridge_junction_fix.net.xml" +my_file = urllib.request.urlopen(my_url) +data_to_write = my_file.read() + +with open(os.path.join(os.path.dirname(os.path.abspath(__file__)), TEMPLATE), + "wb+") as f: + f.write(data_to_write) + + +vehicles = VehicleParams() +vehicles.add( + veh_id="human", + acceleration_controller=(SimCarFollowingController, {}), + routing_controller=(BayBridgeRouter, {}), + car_following_params=SumoCarFollowingParams( + speedDev=0.2, + speed_mode="all_checks", + ), + lane_change_params=SumoLaneChangeParams( + lc_assertive=20, + lc_pushy=0.8, + lc_speed_gain=4.0, + model="LC2013", + lane_change_mode="no_lat_collide", + # lcKeepRight=0.8 + ), + num_vehicles=1400) + +inflow = InFlows() + +if USE_INFLOWS: + # south + inflow.add( + veh_type="human", + edge="183343422", + vehsPerHour=528, + departLane="0", + departSpeed=20) + inflow.add( + veh_type="human", + edge="183343422", + vehsPerHour=864, + departLane="1", + departSpeed=20) + inflow.add( + veh_type="human", + edge="183343422", + vehsPerHour=600, + departLane="2", + departSpeed=20) + + inflow.add( + veh_type="human", + edge="393649534", + probability=0.1, + departLane="0", + departSpeed=20) # no data for this + + # west + inflow.add( + veh_type="human", + edge="11189946", + vehsPerHour=1752, + departLane="0", + departSpeed=20) + inflow.add( + veh_type="human", + edge="11189946", + vehsPerHour=2136, + departLane="1", + departSpeed=20) + inflow.add( + veh_type="human", + edge="11189946", + vehsPerHour=576, + departLane="2", + departSpeed=20) + + # north + inflow.add( + veh_type="human", + edge="28413687#0", + vehsPerHour=2880, + departLane="0", + departSpeed=20) + inflow.add( + veh_type="human", + edge="28413687#0", + vehsPerHour=2328, + departLane="1", + departSpeed=20) + inflow.add( + veh_type="human", + edge="28413687#0", + vehsPerHour=3060, + departLane="2", + departSpeed=20) + inflow.add( + veh_type="human", + edge="11198593", + probability=0.1, + departLane="0", + departSpeed=20) # no data for this + inflow.add( + veh_type="human", + edge="11197889", + probability=0.1, + departLane="0", + departSpeed=20) # no data for this + + # midway through bridge + inflow.add( + veh_type="human", + edge="35536683", + probability=0.1, + departLane="0", + departSpeed=20) # no data for this + + +flow_params = dict( + # name of the experiment + exp_tag='bay_bridge', + + # name of the flow environment the experiment is running on + env_name=BayBridgeEnv, + + # name of the network class the experiment is running on + network=BayBridgeNetwork, + + # simulator that is used by the experiment + simulator='traci', + + # sumo-related parameters (see flow.core.params.SumoParams) + sim=SumoParams( + render=True, + sim_step=0.6, + overtake_right=True, + ), + + # environment related parameters (see flow.core.params.EnvParams) + env=EnvParams( + horizon=1500, + additional_params={}, + ), + + # network-related parameters (see flow.core.params.NetParams and the + # network's documentation or ADDITIONAL_NET_PARAMS component) + net=NetParams( + inflows=inflow, + template=TEMPLATE, + ), + + # vehicles to be placed in the network at the start of a rollout (see + # flow.core.params.VehicleParams) + veh=vehicles, + + # parameters specifying the positioning of vehicles upon initialization/ + # reset (see flow.core.params.InitialConfig) + initial=InitialConfig( + spacing="uniform", + min_gap=15, + edges_distribution=EDGES_DISTRIBUTION.copy(), + ), + + # traffic lights to be introduced to specific nodes (see + # flow.core.params.TrafficLightParams) + tls=TrafficLightParams(), +) diff --git a/examples/exp_configs/non_rl/bay_bridge_toll.py b/examples/exp_configs/non_rl/bay_bridge_toll.py new file mode 100644 index 000000000..1b8268aeb --- /dev/null +++ b/examples/exp_configs/non_rl/bay_bridge_toll.py @@ -0,0 +1,125 @@ +"""Bay Bridge toll example.""" + +import os +import urllib.request + +from flow.core.params import SumoParams, EnvParams, NetParams, InitialConfig, \ + SumoLaneChangeParams, SumoCarFollowingParams, InFlows +from flow.core.params import VehicleParams +from flow.networks.bay_bridge_toll import EDGES_DISTRIBUTION +from flow.controllers import SimCarFollowingController, BayBridgeRouter +from flow.envs import BayBridgeEnv +from flow.networks import BayBridgeTollNetwork + +USE_TRAFFIC_LIGHTS = False + +TEMPLATE = os.path.join( + os.path.dirname(os.path.abspath(__file__)), "bottleneck.net.xml") + + +# download the template from AWS +if USE_TRAFFIC_LIGHTS: + my_url = "http://s3-us-west-1.amazonaws.com/flow.netfiles/" \ + "bay_bridge_TL_all_green.net.xml" +else: + my_url = "http://s3-us-west-1.amazonaws.com/flow.netfiles/" \ + "bay_bridge_junction_fix.net.xml" +my_file = urllib.request.urlopen(my_url) +data_to_write = my_file.read() + +with open( + os.path.join(os.path.dirname(os.path.abspath(__file__)), TEMPLATE), + "wb+") as f: + f.write(data_to_write) + + +vehicles = VehicleParams() +vehicles.add( + veh_id="human", + acceleration_controller=(SimCarFollowingController, {}), + routing_controller=(BayBridgeRouter, {}), + car_following_params=SumoCarFollowingParams( + speedDev=0.2, + speed_mode="all_checks", + ), + lane_change_params=SumoLaneChangeParams( + model="LC2013", + lcCooperative=0.2, + lcSpeedGain=15, + lane_change_mode="no_lat_collide", + ), + num_vehicles=50) + + +inflow = InFlows() +inflow.add( + veh_type="human", + edge="393649534", + probability=0.2, + departLane="random", + departSpeed=10) +inflow.add( + veh_type="human", + edge="4757680", + probability=0.2, + departLane="random", + departSpeed=10) +inflow.add( + veh_type="human", + edge="32661316", + probability=0.2, + departLane="random", + departSpeed=10) +inflow.add( + veh_type="human", + edge="90077193#0", + vehs_per_hour=2000, + departLane="random", + departSpeed=10) + + +flow_params = dict( + # name of the experiment + exp_tag='bay_bridge_toll', + + # name of the flow environment the experiment is running on + env_name=BayBridgeEnv, + + # name of the network class the experiment is running on + network=BayBridgeTollNetwork, + + # simulator that is used by the experiment + simulator='traci', + + # sumo-related parameters (see flow.core.params.SumoParams) + sim=SumoParams( + render=True, + sim_step=0.4, + overtake_right=True, + ), + + # environment related parameters (see flow.core.params.EnvParams) + env=EnvParams( + horizon=1500, + additional_params={}, + ), + + # network-related parameters (see flow.core.params.NetParams and the + # network's documentation or ADDITIONAL_NET_PARAMS component) + net=NetParams( + inflows=inflow, + template=TEMPLATE, + ), + + # vehicles to be placed in the network at the start of a rollout (see + # flow.core.params.VehicleParams) + veh=vehicles, + + # parameters specifying the positioning of vehicles upon initialization/ + # reset (see flow.core.params.InitialConfig) + initial=InitialConfig( + spacing="uniform", + min_gap=15, + edges_distribution=EDGES_DISTRIBUTION.copy(), + ), +) diff --git a/examples/exp_configs/non_rl/bottleneck.py b/examples/exp_configs/non_rl/bottleneck.py new file mode 100644 index 000000000..f959f6d0f --- /dev/null +++ b/examples/exp_configs/non_rl/bottleneck.py @@ -0,0 +1,108 @@ +"""File demonstrating formation of congestion in bottleneck.""" + +from flow.core.params import SumoParams, EnvParams, NetParams, InitialConfig +from flow.core.params import InFlows, SumoLaneChangeParams, SumoCarFollowingParams +from flow.core.params import VehicleParams +from flow.core.params import TrafficLightParams +from flow.controllers import SimLaneChangeController, ContinuousRouter +from flow.envs import BottleneckEnv +from flow.networks import BottleneckNetwork + +SCALING = 1 +DISABLE_TB = True + +# If set to False, ALINEA will control the ramp meter +DISABLE_RAMP_METER = True +INFLOW = 2300 +HORIZON = 1000 + +vehicles = VehicleParams() +vehicles.add( + veh_id="human", + lane_change_controller=(SimLaneChangeController, {}), + routing_controller=(ContinuousRouter, {}), + car_following_params=SumoCarFollowingParams( + speed_mode=25, + ), + lane_change_params=SumoLaneChangeParams( + lane_change_mode=1621, + ), + num_vehicles=1) + +inflow = InFlows() +inflow.add( + veh_type="human", + edge="1", + vehsPerHour=INFLOW, + departLane="random", + departSpeed=10) + +traffic_lights = TrafficLightParams() +if not DISABLE_TB: + traffic_lights.add(node_id="2") +if not DISABLE_RAMP_METER: + traffic_lights.add(node_id="3") + + +flow_params = dict( + # name of the experiment + exp_tag='bay_bridge_toll', + + # name of the flow environment the experiment is running on + env_name=BottleneckEnv, + + # name of the network class the experiment is running on + network=BottleneckNetwork, + + # simulator that is used by the experiment + simulator='traci', + + # sumo-related parameters (see flow.core.params.SumoParams) + sim=SumoParams( + sim_step=0.5, + render=False, + overtake_right=False, + restart_instance=False + ), + + # environment related parameters (see flow.core.params.EnvParams) + env=EnvParams( + horizon=HORIZON, + additional_params={ + "target_velocity": 40, + "max_accel": 1, + "max_decel": 1, + "lane_change_duration": 5, + "add_rl_if_exit": False, + "disable_tb": DISABLE_TB, + "disable_ramp_metering": DISABLE_RAMP_METER + } + ), + + # network-related parameters (see flow.core.params.NetParams and the + # network's documentation or ADDITIONAL_NET_PARAMS component) + net=NetParams( + inflows=inflow, + additional_params={ + "scaling": SCALING, + "speed_limit": 23 + } + ), + + # vehicles to be placed in the network at the start of a rollout (see + # flow.core.params.VehicleParams) + veh=vehicles, + + # parameters specifying the positioning of vehicles upon initialization/ + # reset (see flow.core.params.InitialConfig) + initial=InitialConfig( + spacing="random", + min_gap=5, + lanes_distribution=float("inf"), + edges_distribution=["2", "3", "4", "5"] + ), + + # traffic lights to be introduced to specific nodes (see + # flow.core.params.TrafficLightParams) + tls=traffic_lights, +) diff --git a/examples/exp_configs/non_rl/figure_eight.py b/examples/exp_configs/non_rl/figure_eight.py new file mode 100755 index 000000000..13444261a --- /dev/null +++ b/examples/exp_configs/non_rl/figure_eight.py @@ -0,0 +1,62 @@ +"""Example of a figure 8 network with human-driven vehicles. + +Right-of-way dynamics near the intersection causes vehicles to queue up on +either side of the intersection, leading to a significant reduction in the +average speed of vehicles in the network. +""" +from flow.controllers import IDMController, StaticLaneChanger, ContinuousRouter +from flow.core.params import SumoParams, EnvParams, NetParams +from flow.core.params import VehicleParams, SumoCarFollowingParams +from flow.envs.ring.accel import ADDITIONAL_ENV_PARAMS +from flow.networks.figure_eight import ADDITIONAL_NET_PARAMS +from flow.envs import AccelEnv +from flow.networks import FigureEightNetwork + +vehicles = VehicleParams() +vehicles.add( + veh_id="idm", + acceleration_controller=(IDMController, {}), + lane_change_controller=(StaticLaneChanger, {}), + routing_controller=(ContinuousRouter, {}), + car_following_params=SumoCarFollowingParams( + speed_mode="obey_safe_speed", + decel=1.5, + ), + initial_speed=0, + num_vehicles=14) + + +flow_params = dict( + # name of the experiment + exp_tag='figure8', + + # name of the flow environment the experiment is running on + env_name=AccelEnv, + + # name of the network class the experiment is running on + network=FigureEightNetwork, + + # simulator that is used by the experiment + simulator='traci', + + # sumo-related parameters (see flow.core.params.SumoParams) + sim=SumoParams( + render=True, + ), + + # environment related parameters (see flow.core.params.EnvParams) + env=EnvParams( + horizon=1500, + additional_params=ADDITIONAL_ENV_PARAMS.copy(), + ), + + # network-related parameters (see flow.core.params.NetParams and the + # network's documentation or ADDITIONAL_NET_PARAMS component) + net=NetParams( + additional_params=ADDITIONAL_NET_PARAMS.copy(), + ), + + # vehicles to be placed in the network at the start of a rollout (see + # flow.core.params.VehicleParams) + veh=vehicles, +) diff --git a/examples/exp_configs/non_rl/highway.py b/examples/exp_configs/non_rl/highway.py new file mode 100644 index 000000000..7b2222301 --- /dev/null +++ b/examples/exp_configs/non_rl/highway.py @@ -0,0 +1,78 @@ +"""Example of an open multi-lane network with human-driven vehicles.""" + +from flow.controllers import IDMController +from flow.core.params import SumoParams, EnvParams, NetParams, InitialConfig +from flow.core.params import VehicleParams, InFlows +from flow.envs.ring.lane_change_accel import ADDITIONAL_ENV_PARAMS +from flow.networks.highway import HighwayNetwork, ADDITIONAL_NET_PARAMS +from flow.envs import LaneChangeAccelEnv + +vehicles = VehicleParams() +vehicles.add( + veh_id="human", + acceleration_controller=(IDMController, {}), + num_vehicles=20) +vehicles.add( + veh_id="human2", + acceleration_controller=(IDMController, {}), + num_vehicles=20) + +env_params = EnvParams(additional_params=ADDITIONAL_ENV_PARAMS) + +inflow = InFlows() +inflow.add( + veh_type="human", + edge="highway_0", + probability=0.25, + departLane="free", + departSpeed=20) +inflow.add( + veh_type="human2", + edge="highway_0", + probability=0.25, + departLane="free", + departSpeed=20) + + +flow_params = dict( + # name of the experiment + exp_tag='highway', + + # name of the flow environment the experiment is running on + env_name=LaneChangeAccelEnv, + + # name of the network class the experiment is running on + network=HighwayNetwork, + + # simulator that is used by the experiment + simulator='traci', + + # sumo-related parameters (see flow.core.params.SumoParams) + sim=SumoParams( + render=True, + ), + + # environment related parameters (see flow.core.params.EnvParams) + env=EnvParams( + horizon=1500, + additional_params=ADDITIONAL_ENV_PARAMS.copy(), + ), + + # network-related parameters (see flow.core.params.NetParams and the + # network's documentation or ADDITIONAL_NET_PARAMS component) + net=NetParams( + inflows=inflow, + additional_params=ADDITIONAL_NET_PARAMS.copy(), + ), + + # vehicles to be placed in the network at the start of a rollout (see + # flow.core.params.VehicleParams) + veh=vehicles, + + # parameters specifying the positioning of vehicles upon initialization/ + # reset (see flow.core.params.InitialConfig) + initial=InitialConfig( + spacing="uniform", + shuffle=True, + ), +) diff --git a/examples/exp_configs/non_rl/highway_ramps.py b/examples/exp_configs/non_rl/highway_ramps.py new file mode 100644 index 000000000..7aeba59cb --- /dev/null +++ b/examples/exp_configs/non_rl/highway_ramps.py @@ -0,0 +1,113 @@ +"""Example of a highway section network with on/off ramps.""" + +from flow.core.params import SumoParams, EnvParams, NetParams, InitialConfig +from flow.core.params import SumoCarFollowingParams, SumoLaneChangeParams +from flow.core.params import InFlows, VehicleParams, TrafficLightParams +from flow.networks.highway_ramps import ADDITIONAL_NET_PARAMS +from flow.envs.ring.accel import AccelEnv, ADDITIONAL_ENV_PARAMS +from flow.networks import HighwayRampsNetwork + +additional_net_params = ADDITIONAL_NET_PARAMS.copy() + +# lengths +additional_net_params["highway_length"] = 1200 +additional_net_params["on_ramps_length"] = 200 +additional_net_params["off_ramps_length"] = 200 + +# number of lanes +additional_net_params["highway_lanes"] = 3 +additional_net_params["on_ramps_lanes"] = 1 +additional_net_params["off_ramps_lanes"] = 1 + +# speed limits +additional_net_params["highway_speed"] = 30 +additional_net_params["on_ramps_speed"] = 20 +additional_net_params["off_ramps_speed"] = 20 + +# ramps +additional_net_params["on_ramps_pos"] = [400] +additional_net_params["off_ramps_pos"] = [800] + +# probability of exiting at the next off-ramp +additional_net_params["next_off_ramp_proba"] = 0.25 + +# inflow rates in vehs/hour +HIGHWAY_INFLOW_RATE = 4000 +ON_RAMPS_INFLOW_RATE = 350 + +vehicles = VehicleParams() +vehicles.add( + veh_id="human", + car_following_params=SumoCarFollowingParams( + speed_mode="obey_safe_speed", # for safer behavior at the merges + tau=1.5 # larger distance between cars + ), + lane_change_params=SumoLaneChangeParams(lane_change_mode=1621) +) + +inflows = InFlows() +inflows.add( + veh_type="human", + edge="highway_0", + vehs_per_hour=HIGHWAY_INFLOW_RATE, + depart_lane="free", + depart_speed="max", + name="highway_flow") +for i in range(len(additional_net_params["on_ramps_pos"])): + inflows.add( + veh_type="human", + edge="on_ramp_{}".format(i), + vehs_per_hour=ON_RAMPS_INFLOW_RATE, + depart_lane="first", + depart_speed="max", + name="on_ramp_flow") + + +flow_params = dict( + # name of the experiment + exp_tag='highway-ramp', + + # name of the flow environment the experiment is running on + env_name=AccelEnv, + + # name of the network class the experiment is running on + network=HighwayRampsNetwork, + + # simulator that is used by the experiment + simulator='traci', + + # sumo-related parameters (see flow.core.params.SumoParams) + sim=SumoParams( + render=True, + emission_path="./data/", + sim_step=0.2, + restart_instance=True + ), + + # environment related parameters (see flow.core.params.EnvParams) + env=EnvParams( + additional_params=ADDITIONAL_ENV_PARAMS, + horizon=3600, + sims_per_step=5, + warmup_steps=0 + ), + + # network-related parameters (see flow.core.params.NetParams and the + # network's documentation or ADDITIONAL_NET_PARAMS component) + net=NetParams( + inflows=inflows, + additional_params=additional_net_params + ), + + # vehicles to be placed in the network at the start of a rollout (see + # flow.core.params.VehicleParams) + veh=vehicles, + + # parameters specifying the positioning of vehicles upon initialization/ + # reset (see flow.core.params.InitialConfig) + initial=InitialConfig(), + + # traffic lights to be introduced to specific nodes (see + # flow.core.params.TrafficLightParams) + tls=TrafficLightParams(), +) diff --git a/examples/exp_configs/non_rl/merge.py b/examples/exp_configs/non_rl/merge.py new file mode 100644 index 000000000..2a1ff0455 --- /dev/null +++ b/examples/exp_configs/non_rl/merge.py @@ -0,0 +1,97 @@ +"""Example of a merge network with human-driven vehicles. + +In the absence of autonomous vehicles, the network exhibits properties of +convective instability, with perturbations propagating upstream from the merge +point before exiting the network. +""" + +from flow.core.params import SumoParams, EnvParams, \ + NetParams, InitialConfig, InFlows, SumoCarFollowingParams +from flow.core.params import VehicleParams +from flow.controllers import IDMController +from flow.envs.merge import MergePOEnv, ADDITIONAL_ENV_PARAMS +from flow.networks import MergeNetwork + +# inflow rate at the highway +FLOW_RATE = 2000 + +vehicles = VehicleParams() +vehicles.add( + veh_id="human", + acceleration_controller=(IDMController, { + "noise": 0.2 + }), + car_following_params=SumoCarFollowingParams( + speed_mode="obey_safe_speed", + ), + num_vehicles=5) + +inflow = InFlows() +inflow.add( + veh_type="human", + edge="inflow_highway", + vehs_per_hour=FLOW_RATE, + departLane="free", + departSpeed=10) +inflow.add( + veh_type="human", + edge="inflow_merge", + vehs_per_hour=100, + departLane="free", + departSpeed=7.5) + + +flow_params = dict( + # name of the experiment + exp_tag='merge-baseline', + + # name of the flow environment the experiment is running on + env_name=MergePOEnv, + + # name of the network class the experiment is running on + network=MergeNetwork, + + # simulator that is used by the experiment + simulator='traci', + + # sumo-related parameters (see flow.core.params.SumoParams) + sim=SumoParams( + render=True, + emission_path="./data/", + sim_step=0.2, + restart_instance=False, + ), + + # environment related parameters (see flow.core.params.EnvParams) + env=EnvParams( + horizon=3600, + additional_params=ADDITIONAL_ENV_PARAMS, + sims_per_step=5, + warmup_steps=0, + ), + + # network-related parameters (see flow.core.params.NetParams and the + # network's documentation or ADDITIONAL_NET_PARAMS component) + net=NetParams( + inflows=inflow, + additional_params={ + "merge_length": 100, + "pre_merge_length": 500, + "post_merge_length": 100, + "merge_lanes": 1, + "highway_lanes": 1, + "speed_limit": 30, + }, + ), + + # vehicles to be placed in the network at the start of a rollout (see + # flow.core.params.VehicleParams) + veh=vehicles, + + # parameters specifying the positioning of vehicles upon initialization/ + # reset (see flow.core.params.InitialConfig) + initial=InitialConfig( + spacing="uniform", + perturbation=5.0, + ), +) diff --git a/examples/exp_configs/non_rl/minicity.py b/examples/exp_configs/non_rl/minicity.py new file mode 100644 index 000000000..23b232480 --- /dev/null +++ b/examples/exp_configs/non_rl/minicity.py @@ -0,0 +1,79 @@ +"""Example of modified minicity network with human-driven vehicles.""" +from flow.controllers import IDMController +from flow.controllers import RLController +from flow.core.params import SumoParams, EnvParams, NetParams, InitialConfig +from flow.core.params import SumoCarFollowingParams, SumoLaneChangeParams +from flow.core.params import VehicleParams +from flow.envs.ring.accel import AccelEnv, ADDITIONAL_ENV_PARAMS +from flow.controllers.routing_controllers import MinicityRouter +from flow.networks import MiniCityNetwork + + +vehicles = VehicleParams() +vehicles.add( + veh_id="idm", + acceleration_controller=(IDMController, {}), + routing_controller=(MinicityRouter, {}), + car_following_params=SumoCarFollowingParams( + speed_mode=1, + ), + lane_change_params=SumoLaneChangeParams( + lane_change_mode="no_lat_collide", + ), + initial_speed=0, + num_vehicles=90) +vehicles.add( + veh_id="rl", + acceleration_controller=(RLController, {}), + routing_controller=(MinicityRouter, {}), + car_following_params=SumoCarFollowingParams( + speed_mode="obey_safe_speed", + ), + initial_speed=0, + num_vehicles=10) + + +flow_params = dict( + # name of the experiment + exp_tag='minicity', + + # name of the flow environment the experiment is running on + env_name=AccelEnv, + + # name of the network class the experiment is running on + network=MiniCityNetwork, + + # simulator that is used by the experiment + simulator='traci', + + # sumo-related parameters (see flow.core.params.SumoParams) + sim=SumoParams( + sim_step=0.25, + render='drgb', + save_render=False, + sight_radius=30, + pxpm=3, + show_radius=True, + ), + + # environment related parameters (see flow.core.params.EnvParams) + env=EnvParams( + horizon=750, + additional_params=ADDITIONAL_ENV_PARAMS + ), + + # network-related parameters (see flow.core.params.NetParams and the + # network's documentation or ADDITIONAL_NET_PARAMS component) + net=NetParams(), + + # vehicles to be placed in the network at the start of a rollout (see + # flow.core.params.VehicleParams) + veh=vehicles, + + # parameters specifying the positioning of vehicles upon initialization/ + # reset (see flow.core.params.InitialConfig) + initial=InitialConfig( + spacing="random", + min_gap=5, + ), +) diff --git a/examples/exp_configs/non_rl/ring.py b/examples/exp_configs/non_rl/ring.py new file mode 100755 index 000000000..bffbdf3ba --- /dev/null +++ b/examples/exp_configs/non_rl/ring.py @@ -0,0 +1,61 @@ +"""Used as an example of ring experiment. + +This example consists of 22 IDM cars on a ring creating shockwaves. +""" + +from flow.controllers import IDMController, ContinuousRouter +from flow.core.params import SumoParams, EnvParams, InitialConfig, NetParams +from flow.core.params import VehicleParams +from flow.envs.ring.accel import AccelEnv, ADDITIONAL_ENV_PARAMS +from flow.networks.ring import RingNetwork, ADDITIONAL_NET_PARAMS + + +vehicles = VehicleParams() +vehicles.add( + veh_id="idm", + acceleration_controller=(IDMController, {}), + routing_controller=(ContinuousRouter, {}), + num_vehicles=22) + + +flow_params = dict( + # name of the experiment + exp_tag='ring', + + # name of the flow environment the experiment is running on + env_name=AccelEnv, + + # name of the network class the experiment is running on + network=RingNetwork, + + # simulator that is used by the experiment + simulator='traci', + + # sumo-related parameters (see flow.core.params.SumoParams) + sim=SumoParams( + render=True, + sim_step=0.1, + ), + + # environment related parameters (see flow.core.params.EnvParams) + env=EnvParams( + horizon=1500, + additional_params=ADDITIONAL_ENV_PARAMS, + ), + + # network-related parameters (see flow.core.params.NetParams and the + # network's documentation or ADDITIONAL_NET_PARAMS component) + net=NetParams( + additional_params=ADDITIONAL_NET_PARAMS.copy(), + ), + + # vehicles to be placed in the network at the start of a rollout (see + # flow.core.params.VehicleParams) + veh=vehicles, + + # parameters specifying the positioning of vehicles upon initialization/ + # reset (see flow.core.params.InitialConfig) + initial=InitialConfig( + bunching=20, + ), +) diff --git a/examples/exp_configs/non_rl/traffic_light_grid.py b/examples/exp_configs/non_rl/traffic_light_grid.py new file mode 100644 index 000000000..2d9894e9d --- /dev/null +++ b/examples/exp_configs/non_rl/traffic_light_grid.py @@ -0,0 +1,235 @@ +"""Grid example.""" +from flow.controllers import GridRouter +from flow.core.params import SumoParams, EnvParams, InitialConfig, NetParams +from flow.core.params import VehicleParams +from flow.core.params import TrafficLightParams +from flow.core.params import SumoCarFollowingParams +from flow.core.params import InFlows +from flow.envs.ring.accel import AccelEnv, ADDITIONAL_ENV_PARAMS +from flow.networks import TrafficLightGridNetwork + +USE_INFLOWS = False + +v_enter = 10 +inner_length = 300 +long_length = 500 +short_length = 300 +n_rows = 2 +n_columns = 3 +num_cars_left = 20 +num_cars_right = 20 +num_cars_top = 20 +num_cars_bot = 20 +tot_cars = (num_cars_left + num_cars_right) * n_columns \ + + (num_cars_top + num_cars_bot) * n_rows + +grid_array = { + "short_length": short_length, + "inner_length": inner_length, + "long_length": long_length, + "row_num": n_rows, + "col_num": n_columns, + "cars_left": num_cars_left, + "cars_right": num_cars_right, + "cars_top": num_cars_top, + "cars_bot": num_cars_bot +} + + +def gen_edges(col_num, row_num): + """Generate the names of the outer edges in the grid network. + + Parameters + ---------- + col_num : int + number of columns in the grid + row_num : int + number of rows in the grid + + Returns + ------- + list of str + names of all the outer edges + """ + edges = [] + + # build the left and then the right edges + for i in range(col_num): + edges += ['left' + str(row_num) + '_' + str(i)] + edges += ['right' + '0' + '_' + str(i)] + + # build the bottom and then top edges + for i in range(row_num): + edges += ['bot' + str(i) + '_' + '0'] + edges += ['top' + str(i) + '_' + str(col_num)] + + return edges + + +def get_flow_params(col_num, row_num, additional_net_params): + """Define the network and initial params in the presence of inflows. + + Parameters + ---------- + col_num : int + number of columns in the grid + row_num : int + number of rows in the grid + additional_net_params : dict + network-specific parameters that are unique to the grid + + Returns + ------- + flow.core.params.InitialConfig + parameters specifying the initial configuration of vehicles in the + network + flow.core.params.NetParams + network-specific parameters used to generate the network + """ + initial = InitialConfig( + spacing='custom', lanes_distribution=float('inf'), shuffle=True) + + inflow = InFlows() + outer_edges = gen_edges(col_num, row_num) + for i in range(len(outer_edges)): + inflow.add( + veh_type='human', + edge=outer_edges[i], + probability=0.25, + departLane='free', + departSpeed=20) + + net = NetParams( + inflows=inflow, + additional_params=additional_net_params) + + return initial, net + + +def get_non_flow_params(enter_speed, add_net_params): + """Define the network and initial params in the absence of inflows. + + Note that when a vehicle leaves a network in this case, it is immediately + returns to the start of the row/column it was traversing, and in the same + direction as it was before. + + Parameters + ---------- + enter_speed : float + initial speed of vehicles as they enter the network. + add_net_params: dict + additional network-specific parameters (unique to the grid) + + Returns + ------- + flow.core.params.InitialConfig + parameters specifying the initial configuration of vehicles in the + network + flow.core.params.NetParams + network-specific parameters used to generate the network + """ + additional_init_params = {'enter_speed': enter_speed} + initial = InitialConfig( + spacing='custom', additional_params=additional_init_params) + net = NetParams(additional_params=add_net_params) + + return initial, net + + +vehicles = VehicleParams() +vehicles.add( + veh_id="human", + routing_controller=(GridRouter, {}), + car_following_params=SumoCarFollowingParams( + min_gap=2.5, + decel=7.5, # avoid collisions at emergency stops + ), + num_vehicles=tot_cars) + +env_params = EnvParams(additional_params=ADDITIONAL_ENV_PARAMS) + +tl_logic = TrafficLightParams(baseline=False) +phases = [{ + "duration": "31", + "minDur": "8", + "maxDur": "45", + "state": "GrGrGrGrGrGr" +}, { + "duration": "6", + "minDur": "3", + "maxDur": "6", + "state": "yryryryryryr" +}, { + "duration": "31", + "minDur": "8", + "maxDur": "45", + "state": "rGrGrGrGrGrG" +}, { + "duration": "6", + "minDur": "3", + "maxDur": "6", + "state": "ryryryryryry" +}] +tl_logic.add("center0", phases=phases, programID=1) +tl_logic.add("center1", phases=phases, programID=1) +tl_logic.add("center2", phases=phases, programID=1, tls_type="actuated") + +additional_net_params = { + "grid_array": grid_array, + "speed_limit": 35, + "horizontal_lanes": 1, + "vertical_lanes": 1 +} + +if USE_INFLOWS: + initial_config, net_params = get_flow_params( + col_num=n_columns, + row_num=n_rows, + additional_net_params=additional_net_params) +else: + initial_config, net_params = get_non_flow_params( + enter_speed=v_enter, + add_net_params=additional_net_params) + + +flow_params = dict( + # name of the experiment + exp_tag='grid-intersection', + + # name of the flow environment the experiment is running on + env_name=AccelEnv, + + # name of the network class the experiment is running on + network=TrafficLightGridNetwork, + + # simulator that is used by the experiment + simulator='traci', + + # sumo-related parameters (see flow.core.params.SumoParams) + sim=SumoParams( + sim_step=0.1, + render=True, + ), + + # environment related parameters (see flow.core.params.EnvParams) + env=EnvParams( + horizon=1500, + additional_params=ADDITIONAL_ENV_PARAMS.copy(), + ), + + # network-related parameters (see flow.core.params.NetParams and the + # network's documentation or ADDITIONAL_NET_PARAMS component) + net=net_params, + + # vehicles to be placed in the network at the start of a rollout (see + # flow.core.params.VehicleParams) + veh=vehicles, + + # parameters specifying the positioning of vehicles upon initialization/ + # reset (see flow.core.params.InitialConfig) + initial=initial_config, + + # traffic lights to be introduced to specific nodes (see + # flow.core.params.TrafficLightParams) + tls=tl_logic, +) diff --git a/examples/rllib/multiagent_exps/multiagent_figure_eight.py b/examples/exp_configs/rl/multiagent/multiagent_figure_eight.py similarity index 54% rename from examples/rllib/multiagent_exps/multiagent_figure_eight.py rename to examples/exp_configs/rl/multiagent/multiagent_figure_eight.py index c07739d26..d541c4edd 100644 --- a/examples/rllib/multiagent_exps/multiagent_figure_eight.py +++ b/examples/exp_configs/rl/multiagent/multiagent_figure_eight.py @@ -8,20 +8,7 @@ # the negative of the AV reward from copy import deepcopy -import json - -import ray -try: - from ray.rllib.agents.agent import get_agent_class -except ImportError: - from ray.rllib.agents.registry import get_agent_class from ray.rllib.agents.ppo.ppo_policy import PPOTFPolicy -from ray import tune -from ray.tune.registry import register_env -from ray.tune import run_experiments - -from flow.envs.multiagent import MultiAgentAccelEnv -from flow.networks import FigureEightNetwork from flow.controllers import ContinuousRouter from flow.controllers import IDMController from flow.controllers import RLController @@ -32,8 +19,10 @@ from flow.core.params import SumoCarFollowingParams from flow.core.params import VehicleParams from flow.networks.figure_eight import ADDITIONAL_NET_PARAMS +from flow.envs.multiagent import MultiAgentAccelEnv +from flow.networks import FigureEightNetwork from flow.utils.registry import make_create_env -from flow.utils.rllib import FlowParamsEncoder +from ray.tune.registry import register_env # time horizon of a single rollout HORIZON = 1500 @@ -41,6 +30,10 @@ N_ROLLOUTS = 4 # number of parallel workers N_CPUS = 2 +# number of human-driven vehicles +N_HUMANS = 13 +# number of automated vehicles +N_AVS = 1 # We place one autonomous vehicle and 13 human-driven vehicles in the network vehicles = VehicleParams() @@ -53,7 +46,7 @@ car_following_params=SumoCarFollowingParams( speed_mode='obey_safe_speed', ), - num_vehicles=13) + num_vehicles=N_HUMANS) vehicles.add( veh_id='rl', acceleration_controller=(RLController, {}), @@ -61,7 +54,7 @@ car_following_params=SumoCarFollowingParams( speed_mode='obey_safe_speed', ), - num_vehicles=1) + num_vehicles=N_AVS) flow_params = dict( # name of the experiment @@ -110,82 +103,25 @@ ) -def setup_exps(): - """Return the relevant components of an RLlib experiment. - - Returns - ------- - str - name of the training algorithm - str - name of the gym environment to be trained - dict - training configuration parameters - """ - alg_run = 'PPO' - agent_cls = get_agent_class(alg_run) - config = agent_cls._default_config.copy() - config['num_workers'] = N_CPUS - config['train_batch_size'] = HORIZON * N_ROLLOUTS - config['simple_optimizer'] = True - config['gamma'] = 0.999 # discount rate - config['model'].update({'fcnet_hiddens': [100, 50, 25]}) - config['use_gae'] = True - config['lambda'] = 0.97 - config['sgd_minibatch_size'] = 128 - config['kl_target'] = 0.02 - config['num_sgd_iter'] = 10 - config['horizon'] = HORIZON - config['clip_actions'] = False # FIXME(ev) temporary ray bug - config['observation_filter'] = 'NoFilter' - - # save the flow params for replay - flow_json = json.dumps( - flow_params, cls=FlowParamsEncoder, sort_keys=True, indent=4) - config['env_config']['flow_params'] = flow_json - config['env_config']['run'] = alg_run - - create_env, env_name = make_create_env(params=flow_params, version=0) - - # Register as rllib env - register_env(env_name, create_env) - - test_env = create_env() - obs_space = test_env.observation_space - act_space = test_env.action_space - - def gen_policy(): - return PPOTFPolicy, obs_space, act_space, {} - - # Setup PG with an ensemble of `num_policies` different policy graphs - policy_graphs = {'av': gen_policy(), 'adversary': gen_policy()} - - def policy_mapping_fn(agent_id): - return agent_id - - config.update({ - 'multiagent': { - 'policies': policy_graphs, - 'policy_mapping_fn': tune.function(policy_mapping_fn) - } - }) - return alg_run, env_name, config - - -if __name__ == '__main__': - - alg_run, env_name, config = setup_exps() - ray.init(num_cpus=N_CPUS+1) - - run_experiments({ - flow_params['exp_tag']: { - 'run': alg_run, - 'env': env_name, - 'checkpoint_freq': 1, - 'stop': { - 'training_iteration': 1 - }, - 'config': config, - # 'upload_dir': 's3://' - }, - }) +create_env, env_name = make_create_env(params=flow_params, version=0) + +# Register as rllib env +register_env(env_name, create_env) + +test_env = create_env() +obs_space = test_env.observation_space +act_space = test_env.action_space + + +def gen_policy(): + """Generate a policy in RLlib.""" + return PPOTFPolicy, obs_space, act_space, {} + + +# Setup PG with an ensemble of `num_policies` different policy graphs +POLICY_GRAPHS = {'av': gen_policy(), 'adversary': gen_policy()} + + +def policy_mapping_fn(agent_id): + """Map a policy in RLlib.""" + return agent_id diff --git a/examples/rllib/multiagent_exps/multiagent_highway.py b/examples/exp_configs/rl/multiagent/multiagent_highway.py similarity index 61% rename from examples/rllib/multiagent_exps/multiagent_highway.py rename to examples/exp_configs/rl/multiagent/multiagent_highway.py index 833614568..cec0b3fba 100644 --- a/examples/rllib/multiagent_exps/multiagent_highway.py +++ b/examples/exp_configs/rl/multiagent/multiagent_highway.py @@ -3,29 +3,17 @@ Trains a non-constant number of agents, all sharing the same policy, on the highway with ramps network. """ -import json -import ray -try: - from ray.rllib.agents.agent import get_agent_class -except ImportError: - from ray.rllib.agents.registry import get_agent_class from ray.rllib.agents.ppo.ppo_policy import PPOTFPolicy -from ray import tune -from ray.tune.registry import register_env -from ray.tune import run_experiments - from flow.controllers import RLController from flow.core.params import EnvParams, NetParams, InitialConfig, InFlows, \ VehicleParams, SumoParams, \ SumoCarFollowingParams, SumoLaneChangeParams - -from flow.utils.registry import make_create_env -from flow.utils.rllib import FlowParamsEncoder - -from flow.envs.multiagent import MultiAgentHighwayPOEnv from flow.envs.ring.accel import ADDITIONAL_ENV_PARAMS from flow.networks import HighwayRampsNetwork +from flow.envs.multiagent import MultiAgentHighwayPOEnv from flow.networks.highway_ramps import ADDITIONAL_NET_PARAMS +from flow.utils.registry import make_create_env +from ray.tune.registry import register_env # SET UP PARAMETERS FOR THE SIMULATION @@ -135,11 +123,19 @@ # SET UP FLOW PARAMETERS flow_params = dict( + # name of the experiment exp_tag='multiagent_highway', + + # name of the flow environment the experiment is running on env_name=MultiAgentHighwayPOEnv, + + # name of the network class the experiment is running on network=HighwayRampsNetwork, + + # simulator that is used by the experiment simulator='traci', + # environment related parameters (see flow.core.params.EnvParams) env=EnvParams( horizon=HORIZON, warmup_steps=200, @@ -147,100 +143,48 @@ additional_params=additional_env_params, ), + # sumo-related parameters (see flow.core.params.SumoParams) sim=SumoParams( sim_step=0.2, render=False, restart_instance=True ), + # network-related parameters (see flow.core.params.NetParams and the + # network's documentation or ADDITIONAL_NET_PARAMS component) net=NetParams( inflows=inflows, - additional_params=additional_net_params), + additional_params=additional_net_params + ), + # vehicles to be placed in the network at the start of a rollout (see + # flow.core.params.VehicleParams) veh=vehicles, + + # parameters specifying the positioning of vehicles upon initialization/ + # reset (see flow.core.params.InitialConfig) initial=InitialConfig(), ) -# SET UP EXPERIMENT - -def setup_exps(flow_params): - """Create the relevant components of a multiagent RLlib experiment. - - Parameters - ---------- - flow_params : dict - input flow-parameters - - Returns - ------- - str - name of the training algorithm - str - name of the gym environment to be trained - dict - training configuration parameters - """ - alg_run = 'PPO' - agent_cls = get_agent_class(alg_run) - config = agent_cls._default_config.copy() - config['num_workers'] = N_CPUS - config['train_batch_size'] = HORIZON * N_ROLLOUTS - config['simple_optimizer'] = True - config['gamma'] = 0.999 # discount rate - config['model'].update({'fcnet_hiddens': [32, 32]}) - config['lr'] = tune.grid_search([1e-5]) - config['horizon'] = HORIZON - config['clip_actions'] = False - config['observation_filter'] = 'NoFilter' - - # save the flow params for replay - flow_json = json.dumps( - flow_params, cls=FlowParamsEncoder, sort_keys=True, indent=4) - config['env_config']['flow_params'] = flow_json - config['env_config']['run'] = alg_run - - create_env, env_name = make_create_env(params=flow_params, version=0) - - # register as rllib env - register_env(env_name, create_env) - - # multiagent configuration - temp_env = create_env() - policy_graphs = {'av': (PPOTFPolicy, - temp_env.observation_space, - temp_env.action_space, - {})} - - def policy_mapping_fn(_): - return 'av' - - config.update({ - 'multiagent': { - 'policies': policy_graphs, - 'policy_mapping_fn': tune.function(policy_mapping_fn), - 'policies_to_train': ['av'] - } - }) - - return alg_run, env_name, config - - -# RUN EXPERIMENT - -if __name__ == '__main__': - alg_run, env_name, config = setup_exps(flow_params) - ray.init(num_cpus=N_CPUS + 1) - - run_experiments({ - flow_params['exp_tag']: { - 'run': alg_run, - 'env': env_name, - 'checkpoint_freq': 20, - 'checkpoint_at_end': True, - 'stop': { - 'training_iteration': N_TRAINING_ITERATIONS - }, - 'config': config, - }, - }) +# SET UP RLLIB MULTI-AGENT FEATURES + +create_env, env_name = make_create_env(params=flow_params, version=0) + +# register as rllib env +register_env(env_name, create_env) + +# multiagent configuration +test_env = create_env() +obs_space = test_env.observation_space +act_space = test_env.action_space + + +POLICY_GRAPHS = {'av': (PPOTFPolicy, obs_space, act_space, {})} + +POLICIES_TO_TRAIN = ['av'] + + +def policy_mapping_fn(_): + """Map a policy in RLlib.""" + return 'av' diff --git a/examples/rllib/multiagent_exps/multiagent_stabilizing_the_ring.py b/examples/exp_configs/rl/multiagent/multiagent_ring.py similarity index 55% rename from examples/rllib/multiagent_exps/multiagent_stabilizing_the_ring.py rename to examples/exp_configs/rl/multiagent/multiagent_ring.py index 300b475c0..e7688c87d 100644 --- a/examples/rllib/multiagent_exps/multiagent_stabilizing_the_ring.py +++ b/examples/exp_configs/rl/multiagent/multiagent_ring.py @@ -3,21 +3,7 @@ Creates a set of stabilizing the ring experiments to test if more agents -> fewer needed batches """ - -import json - -import ray -try: - from ray.rllib.agents.agent import get_agent_class -except ImportError: - from ray.rllib.agents.registry import get_agent_class from ray.rllib.agents.ppo.ppo_policy import PPOTFPolicy -from ray import tune -from ray.tune.registry import register_env -from ray.tune import run_experiments - -from flow.envs.multiagent import MultiWaveAttenuationPOEnv -from flow.networks import MultiRingNetwork from flow.controllers import ContinuousRouter from flow.controllers import IDMController from flow.controllers import RLController @@ -26,8 +12,10 @@ from flow.core.params import NetParams from flow.core.params import SumoParams from flow.core.params import VehicleParams +from flow.envs.multiagent import MultiWaveAttenuationPOEnv +from flow.networks import MultiRingNetwork from flow.utils.registry import make_create_env -from flow.utils.rllib import FlowParamsEncoder +from ray.tune.registry import register_env # make sure (sample_batch_size * num_workers ~= train_batch_size) # time horizon of a single rollout @@ -107,79 +95,28 @@ ) -def setup_exps(): - """Return the relevant components of an RLlib experiment. - - Returns - ------- - str - name of the training algorithm - str - name of the gym environment to be trained - dict - training configuration parameters - """ - alg_run = 'PPO' - agent_cls = get_agent_class(alg_run) - config = agent_cls._default_config.copy() - config['num_workers'] = N_CPUS - config['train_batch_size'] = HORIZON * N_ROLLOUTS - config['simple_optimizer'] = True - config['gamma'] = 0.999 # discount rate - config['model'].update({'fcnet_hiddens': [32, 32]}) - config['lr'] = tune.grid_search([1e-5]) - config['horizon'] = HORIZON - config['clip_actions'] = False # FIXME(ev) temporary ray bug - config['observation_filter'] = 'NoFilter' - - # save the flow params for replay - flow_json = json.dumps( - flow_params, cls=FlowParamsEncoder, sort_keys=True, indent=4) - config['env_config']['flow_params'] = flow_json - config['env_config']['run'] = alg_run - - create_env, env_name = make_create_env(params=flow_params, version=0) - - # Register as rllib env - register_env(env_name, create_env) - - test_env = create_env() - obs_space = test_env.observation_space - act_space = test_env.action_space - - def gen_policy(): - return PPOTFPolicy, obs_space, act_space, {} - - # Setup PG with an ensemble of `num_policies` different policy graphs - policy_graphs = {'av': gen_policy()} - - def policy_mapping_fn(_): - return 'av' - - config.update({ - 'multiagent': { - 'policies': policy_graphs, - 'policy_mapping_fn': tune.function(policy_mapping_fn), - 'policies_to_train': ['av'] - } - }) - - return alg_run, env_name, config - - -if __name__ == '__main__': - alg_run, env_name, config = setup_exps() - ray.init(num_cpus=N_CPUS + 1) - - run_experiments({ - flow_params['exp_tag']: { - 'run': alg_run, - 'env': env_name, - 'checkpoint_freq': 1, - 'stop': { - 'training_iteration': 1 - }, - 'config': config, - # 'upload_dir': 's3://' - }, - }) +create_env, env_name = make_create_env(params=flow_params, version=0) + +# Register as rllib env +register_env(env_name, create_env) + +test_env = create_env() +obs_space = test_env.observation_space +act_space = test_env.action_space + + +def gen_policy(): + """Generate a policy in RLlib.""" + return PPOTFPolicy, obs_space, act_space, {} + + +# Setup PG with an ensemble of `num_policies` different policy graphs +POLICY_GRAPHS = {'av': gen_policy()} + + +def policy_mapping_fn(_): + """Map a policy in RLlib.""" + return 'av' + + +POLICIES_TO_TRAIN = ['av'] diff --git a/examples/rllib/multiagent_exps/multiagent_traffic_light_grid.py b/examples/exp_configs/rl/multiagent/multiagent_traffic_light_grid.py similarity index 99% rename from examples/rllib/multiagent_exps/multiagent_traffic_light_grid.py rename to examples/exp_configs/rl/multiagent/multiagent_traffic_light_grid.py index 476e0763b..4182cd756 100644 --- a/examples/rllib/multiagent_exps/multiagent_traffic_light_grid.py +++ b/examples/exp_configs/rl/multiagent/multiagent_traffic_light_grid.py @@ -48,7 +48,6 @@ def make_flow_params(n_rows, n_columns, edge_inflow): number of columns in the traffic light grid edge_inflow : float - Returns ------- dict @@ -211,6 +210,7 @@ def gen_policy(): policy_graphs = {'av': gen_policy()} def policy_mapping_fn(_): + """Map a policy in RLlib.""" return 'av' config.update({ diff --git a/examples/rllib/velocity_bottleneck.py b/examples/exp_configs/rl/singleagent/singleagent_bottleneck.py similarity index 68% rename from examples/rllib/velocity_bottleneck.py rename to examples/exp_configs/rl/singleagent/singleagent_bottleneck.py index 1e6a16eae..25bd3b9f3 100644 --- a/examples/rllib/velocity_bottleneck.py +++ b/examples/exp_configs/rl/singleagent/singleagent_bottleneck.py @@ -3,26 +3,14 @@ Bottleneck in which the actions are specifying a desired velocity in a segment of space """ -import json - -import ray -try: - from ray.rllib.agents.agent import get_agent_class -except ImportError: - from ray.rllib.agents.registry import get_agent_class -from ray.tune import run_experiments -from ray.tune.registry import register_env - -from flow.envs import BottleneckDesiredVelocityEnv -from flow.networks import BottleneckNetwork -from flow.utils.registry import make_create_env -from flow.utils.rllib import FlowParamsEncoder from flow.core.params import SumoParams, EnvParams, InitialConfig, NetParams, \ InFlows, SumoCarFollowingParams, SumoLaneChangeParams from flow.core.params import TrafficLightParams from flow.core.params import VehicleParams from flow.controllers import RLController, ContinuousRouter, \ SimLaneChangeController +from flow.envs import BottleneckDesiredVelocityEnv +from flow.networks import BottleneckNetwork # time horizon of a single rollout HORIZON = 1000 @@ -161,63 +149,3 @@ # flow.core.params.TrafficLightParams) tls=traffic_lights, ) - - -def setup_exps(): - """Return the relevant components of an RLlib experiment. - - Returns - ------- - str - name of the training algorithm - str - name of the gym environment to be trained - dict - training configuration parameters - """ - alg_run = "PPO" - - agent_cls = get_agent_class(alg_run) - config = agent_cls._default_config.copy() - config["num_workers"] = N_CPUS - config["train_batch_size"] = HORIZON * N_ROLLOUTS - config["gamma"] = 0.999 # discount rate - config["model"].update({"fcnet_hiddens": [64, 64]}) - config["use_gae"] = True - config["lambda"] = 0.97 - config["kl_target"] = 0.02 - config["num_sgd_iter"] = 10 - config['clip_actions'] = False # FIXME(ev) temporary ray bug - config["horizon"] = HORIZON - - # save the flow params for replay - flow_json = json.dumps( - flow_params, cls=FlowParamsEncoder, sort_keys=True, indent=4) - config['env_config']['flow_params'] = flow_json - config['env_config']['run'] = alg_run - - create_env, gym_name = make_create_env(params=flow_params, version=0) - - # Register as rllib env - register_env(gym_name, create_env) - return alg_run, gym_name, config - - -if __name__ == "__main__": - alg_run, gym_name, config = setup_exps() - ray.init(num_cpus=N_CPUS + 1) - trials = run_experiments({ - flow_params["exp_tag"]: { - "run": alg_run, - "env": gym_name, - "config": { - **config - }, - "checkpoint_freq": 20, - "checkpoint_at_end": True, - "max_failures": 999, - "stop": { - "training_iteration": 200, - }, - } - }) diff --git a/examples/rllib/figure_eight.py b/examples/exp_configs/rl/singleagent/singleagent_figure_eight.py similarity index 51% rename from examples/rllib/figure_eight.py rename to examples/exp_configs/rl/singleagent/singleagent_figure_eight.py index 694b36449..58f0f2e27 100644 --- a/examples/rllib/figure_eight.py +++ b/examples/exp_configs/rl/singleagent/singleagent_figure_eight.py @@ -1,24 +1,10 @@ """Figure eight example.""" - -import json - -import ray -try: - from ray.rllib.agents.agent import get_agent_class -except ImportError: - from ray.rllib.agents.registry import get_agent_class -from ray.tune import run_experiments -from ray.tune.registry import register_env - -from flow.envs import AccelEnv -from flow.networks import FigureEightNetwork -from flow.utils.registry import make_create_env -from flow.utils.rllib import FlowParamsEncoder -from flow.core.params import SumoParams, EnvParams, InitialConfig, NetParams, \ - SumoCarFollowingParams -from flow.core.params import VehicleParams +from flow.core.params import SumoParams, EnvParams, InitialConfig, NetParams +from flow.core.params import VehicleParams, SumoCarFollowingParams from flow.controllers import IDMController, ContinuousRouter, RLController from flow.networks.figure_eight import ADDITIONAL_NET_PARAMS +from flow.envs import AccelEnv +from flow.networks import FigureEightNetwork # time horizon of a single rollout HORIZON = 1500 @@ -52,7 +38,7 @@ flow_params = dict( # name of the experiment - exp_tag='figure_eight_intersection_control', + exp_tag='singleagent_figure_eight', # name of the flow environment the experiment is running on env_name=AccelEnv, @@ -94,62 +80,3 @@ # reset (see flow.core.params.InitialConfig) initial=InitialConfig(), ) - - -def setup_exps(): - """Return the relevant components of an RLlib experiment. - - Returns - ------- - str - name of the training algorithm - str - name of the gym environment to be trained - dict - training configuration parameters - """ - alg_run = 'PPO' - agent_cls = get_agent_class(alg_run) - config = agent_cls._default_config.copy() - config['num_workers'] = N_CPUS - config['train_batch_size'] = HORIZON * N_ROLLOUTS - config['gamma'] = 0.999 # discount rate - config['model'].update({'fcnet_hiddens': [32, 32]}) - config['use_gae'] = True - config['lambda'] = 0.97 - config['kl_target'] = 0.02 - config['num_sgd_iter'] = 10 - config['clip_actions'] = False # FIXME(ev) temporary ray bug - config['horizon'] = HORIZON - - # save the flow params for replay - flow_json = json.dumps( - flow_params, cls=FlowParamsEncoder, sort_keys=True, indent=4) - config['env_config']['flow_params'] = flow_json - config['env_config']['run'] = alg_run - - create_env, gym_name = make_create_env(params=flow_params, version=0) - - # Register as rllib env - register_env(gym_name, create_env) - return alg_run, gym_name, config - - -if __name__ == '__main__': - alg_run, gym_name, config = setup_exps() - ray.init(num_cpus=N_CPUS + 1) - trials = run_experiments({ - flow_params['exp_tag']: { - 'run': alg_run, - 'env': gym_name, - 'config': { - **config - }, - 'checkpoint_freq': 20, - "checkpoint_at_end": True, - 'max_failures': 999, - 'stop': { - 'training_iteration': 200, - }, - } - }) diff --git a/examples/rllib/stabilizing_highway.py b/examples/exp_configs/rl/singleagent/singleagent_merge.py similarity index 63% rename from examples/rllib/stabilizing_highway.py rename to examples/exp_configs/rl/singleagent/singleagent_merge.py index 1a0e27994..6e6053300 100644 --- a/examples/rllib/stabilizing_highway.py +++ b/examples/exp_configs/rl/singleagent/singleagent_merge.py @@ -1,27 +1,15 @@ """Open merge example. Trains a a small percentage of rl vehicles to dissipate shockwaves caused by -merges in an open network. +on-ramp merge to a single lane open highway network. """ -import json - -import ray -try: - from ray.rllib.agents.agent import get_agent_class -except ImportError: - from ray.rllib.agents.registry import get_agent_class -from ray.tune import run_experiments -from ray.tune.registry import register_env - -from flow.envs import MergePOEnv -from flow.networks import MergeNetwork -from flow.utils.registry import make_create_env -from flow.utils.rllib import FlowParamsEncoder -from flow.core.params import SumoParams, EnvParams, InitialConfig, NetParams, \ - InFlows, SumoCarFollowingParams +from flow.core.params import SumoParams, EnvParams, InitialConfig +from flow.core.params import NetParams, InFlows, SumoCarFollowingParams from flow.networks.merge import ADDITIONAL_NET_PARAMS from flow.core.params import VehicleParams from flow.controllers import IDMController, RLController +from flow.envs import MergePOEnv +from flow.networks import MergeNetwork # experiment number # - 0: 10% RL penetration, 5 max controllable vehicles @@ -139,63 +127,3 @@ # reset (see flow.core.params.InitialConfig) initial=InitialConfig(), ) - - -def setup_exps(): - """Return the relevant components of an RLlib experiment. - - Returns - ------- - str - name of the training algorithm - str - name of the gym environment to be trained - dict - training configuration parameters - """ - alg_run = "PPO" - - agent_cls = get_agent_class(alg_run) - config = agent_cls._default_config.copy() - config["num_workers"] = N_CPUS - config["train_batch_size"] = HORIZON * N_ROLLOUTS - config["gamma"] = 0.999 # discount rate - config["model"].update({"fcnet_hiddens": [32, 32, 32]}) - config["use_gae"] = True - config["lambda"] = 0.97 - config["kl_target"] = 0.02 - config["num_sgd_iter"] = 10 - config['clip_actions'] = False # FIXME(ev) temporary ray bug - config["horizon"] = HORIZON - - # save the flow params for replay - flow_json = json.dumps( - flow_params, cls=FlowParamsEncoder, sort_keys=True, indent=4) - config['env_config']['flow_params'] = flow_json - config['env_config']['run'] = alg_run - - create_env, gym_name = make_create_env(params=flow_params, version=0) - - # Register as rllib env - register_env(gym_name, create_env) - return alg_run, gym_name, config - - -if __name__ == "__main__": - alg_run, gym_name, config = setup_exps() - ray.init(num_cpus=N_CPUS + 1) - trials = run_experiments({ - flow_params["exp_tag"]: { - "run": alg_run, - "env": gym_name, - "config": { - **config - }, - "checkpoint_freq": 20, - "checkpoint_at_end": True, - "max_failures": 999, - "stop": { - "training_iteration": 200, - }, - } - }) diff --git a/examples/rllib/stabilizing_the_ring.py b/examples/exp_configs/rl/singleagent/singleagent_ring.py similarity index 55% rename from examples/rllib/stabilizing_the_ring.py rename to examples/exp_configs/rl/singleagent/singleagent_ring.py index 7f3c87408..bfe84fa42 100644 --- a/examples/rllib/stabilizing_the_ring.py +++ b/examples/exp_configs/rl/singleagent/singleagent_ring.py @@ -3,24 +3,11 @@ Trains a single autonomous vehicle to stabilize the flow of 21 human-driven vehicles in a variable length ring road. """ - -import json - -import ray -try: - from ray.rllib.agents.agent import get_agent_class -except ImportError: - from ray.rllib.agents.registry import get_agent_class -from ray.tune import run_experiments -from ray.tune.registry import register_env - -from flow.envs import WaveAttenuationPOEnv -from flow.networks import RingNetwork -from flow.utils.registry import make_create_env -from flow.utils.rllib import FlowParamsEncoder from flow.core.params import SumoParams, EnvParams, InitialConfig, NetParams from flow.core.params import VehicleParams, SumoCarFollowingParams from flow.controllers import RLController, IDMController, ContinuousRouter +from flow.envs import WaveAttenuationPOEnv +from flow.networks import RingNetwork # time horizon of a single rollout HORIZON = 3000 @@ -64,6 +51,7 @@ sim=SumoParams( sim_step=0.1, render=False, + restart_instance=False ), # environment related parameters (see flow.core.params.EnvParams) @@ -96,63 +84,3 @@ # reset (see flow.core.params.InitialConfig) initial=InitialConfig(), ) - - -def setup_exps(): - """Return the relevant components of an RLlib experiment. - - Returns - ------- - str - name of the training algorithm - str - name of the gym environment to be trained - dict - training configuration parameters - """ - alg_run = "PPO" - - agent_cls = get_agent_class(alg_run) - config = agent_cls._default_config.copy() - config["num_workers"] = N_CPUS - config["train_batch_size"] = HORIZON * N_ROLLOUTS - config["gamma"] = 0.999 # discount rate - config["model"].update({"fcnet_hiddens": [3, 3]}) - config["use_gae"] = True - config["lambda"] = 0.97 - config["kl_target"] = 0.02 - config["num_sgd_iter"] = 10 - config['clip_actions'] = False # FIXME(ev) temporary ray bug - config["horizon"] = HORIZON - - # save the flow params for replay - flow_json = json.dumps( - flow_params, cls=FlowParamsEncoder, sort_keys=True, indent=4) - config['env_config']['flow_params'] = flow_json - config['env_config']['run'] = alg_run - - create_env, gym_name = make_create_env(params=flow_params, version=0) - - # Register as rllib env - register_env(gym_name, create_env) - return alg_run, gym_name, config - - -if __name__ == "__main__": - alg_run, gym_name, config = setup_exps() - ray.init(num_cpus=N_CPUS + 1) - trials = run_experiments({ - flow_params["exp_tag"]: { - "run": alg_run, - "env": gym_name, - "config": { - **config - }, - "checkpoint_freq": 20, - "checkpoint_at_end": True, - "max_failures": 999, - "stop": { - "training_iteration": 200, - }, - } - }) diff --git a/examples/rllib/traffic_light_grid.py b/examples/exp_configs/rl/singleagent/singleagent_traffic_light_grid.py similarity index 66% rename from examples/rllib/traffic_light_grid.py rename to examples/exp_configs/rl/singleagent/singleagent_traffic_light_grid.py index 766cbefc0..73fafc00b 100644 --- a/examples/rllib/traffic_light_grid.py +++ b/examples/exp_configs/rl/singleagent/singleagent_traffic_light_grid.py @@ -1,23 +1,10 @@ """Traffic Light Grid example.""" - -import json - -import ray -try: - from ray.rllib.agents.agent import get_agent_class -except ImportError: - from ray.rllib.agents.registry import get_agent_class -from ray.tune import run_experiments -from ray.tune.registry import register_env - -from flow.envs import TrafficLightGridPOEnv -from flow.networks import TrafficLightGridNetwork -from flow.utils.registry import make_create_env -from flow.utils.rllib import FlowParamsEncoder from flow.core.params import SumoParams, EnvParams, InitialConfig, NetParams, \ InFlows, SumoCarFollowingParams from flow.core.params import VehicleParams from flow.controllers import SimCarFollowingController, GridRouter +from flow.envs import TrafficLightGridPOEnv +from flow.networks import TrafficLightGridNetwork # time horizon of a single rollout HORIZON = 200 @@ -25,6 +12,9 @@ N_ROLLOUTS = 20 # number of parallel workers N_CPUS = 2 +# set to True if you would like to run the experiment with inflows of vehicles +# from the edges, and False otherwise +USE_INFLOWS = False def gen_edges(col_num, row_num): @@ -55,7 +45,7 @@ def gen_edges(col_num, row_num): return edges -def get_flow_params(col_num, row_num, additional_net_params): +def get_inflow_params(col_num, row_num, additional_net_params): """Define the network and initial params in the presence of inflows. Parameters @@ -178,6 +168,19 @@ def get_non_flow_params(enter_speed, add_net_params): routing_controller=(GridRouter, {}), num_vehicles=tot_cars) +# collect the initialization and network-specific parameters based on the +# choice to use inflows or not +if USE_INFLOWS: + initial_config, net_params = get_inflow_params( + col_num=N_COLUMNS, + row_num=N_ROWS, + additional_net_params=additional_net_params) +else: + initial_config, net_params = get_non_flow_params( + enter_speed=V_ENTER, + add_net_params=additional_net_params) + + flow_params = dict( # name of the experiment exp_tag='traffic_light_grid', @@ -206,7 +209,7 @@ def get_non_flow_params(enter_speed, add_net_params): # network-related parameters (see flow.core.params.NetParams and the # network's documentation or ADDITIONAL_NET_PARAMS component). This is # filled in by the setup_exps method below. - net=None, + net=net_params, # vehicles to be placed in the network at the start of a rollout (see # flow.core.params.VehicleParams) @@ -215,86 +218,5 @@ def get_non_flow_params(enter_speed, add_net_params): # parameters specifying the positioning of vehicles upon initialization/ # reset (see flow.core.params.InitialConfig). This is filled in by the # setup_exps method below. - initial=None, + initial=initial_config, ) - - -def setup_exps(use_inflows=False): - """Return the relevant components of an RLlib experiment. - - Parameters - ---------- - use_inflows : bool, optional - set to True if you would like to run the experiment with inflows of - vehicles from the edges, and False otherwise - - Returns - ------- - str - name of the training algorithm - str - name of the gym environment to be trained - dict - training configuration parameters - """ - # collect the initialization and network-specific parameters based on the - # choice to use inflows or not - if use_inflows: - initial_config, net_params = get_flow_params( - col_num=N_COLUMNS, - row_num=N_ROWS, - additional_net_params=additional_net_params) - else: - initial_config, net_params = get_non_flow_params( - enter_speed=V_ENTER, - add_net_params=additional_net_params) - - # add the new parameters to flow_params - flow_params['initial'] = initial_config - flow_params['net'] = net_params - - alg_run = 'PPO' - - agent_cls = get_agent_class(alg_run) - config = agent_cls._default_config.copy() - config['num_workers'] = N_CPUS - config['train_batch_size'] = HORIZON * N_ROLLOUTS - config['gamma'] = 0.999 # discount rate - config['model'].update({'fcnet_hiddens': [32, 32]}) - config['use_gae'] = True - config['lambda'] = 0.97 - config['kl_target'] = 0.02 - config['num_sgd_iter'] = 10 - config['clip_actions'] = False # FIXME(ev) temporary ray bug - config['horizon'] = HORIZON - - # save the flow params for replay - flow_json = json.dumps( - flow_params, cls=FlowParamsEncoder, sort_keys=True, indent=4) - config['env_config']['flow_params'] = flow_json - config['env_config']['run'] = alg_run - - create_env, gym_name = make_create_env(params=flow_params, version=0) - - # Register as rllib env - register_env(gym_name, create_env) - return alg_run, gym_name, config - - -if __name__ == '__main__': - alg_run, gym_name, config = setup_exps() - ray.init(num_cpus=N_CPUS + 1) - trials = run_experiments({ - flow_params['exp_tag']: { - 'run': alg_run, - 'env': gym_name, - 'config': { - **config - }, - 'checkpoint_freq': 20, - 'max_failures': 999, - 'stop': { - 'training_iteration': 200, - }, - } - }) diff --git a/examples/sumo/density_exp.py b/examples/exp_scripts/bottleneck_density_sweep_capacity_diagram.py similarity index 100% rename from examples/sumo/density_exp.py rename to examples/exp_scripts/bottleneck_density_sweep_capacity_diagram.py diff --git a/examples/rllib/__init__.py b/examples/rllib/__init__.py deleted file mode 100644 index d48602a8b..000000000 --- a/examples/rllib/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Empty init file to ensure that the examples documentation builds.""" diff --git a/examples/rllib/multiagent_exps/__init__.py b/examples/rllib/multiagent_exps/__init__.py deleted file mode 100644 index d48602a8b..000000000 --- a/examples/rllib/multiagent_exps/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Empty init file to ensure that the examples documentation builds.""" diff --git a/examples/simulate.py b/examples/simulate.py new file mode 100644 index 000000000..6c3164bcf --- /dev/null +++ b/examples/simulate.py @@ -0,0 +1,71 @@ +"""Runner script for non-RL simulations in flow. + +Usage + python simulate.py EXP_CONFIG --render +""" +import argparse +import sys +from flow.core.experiment import Experiment + + +def parse_args(args): + """Parse training options user can specify in command line. + + Returns + ------- + argparse.Namespace + the output parser object + """ + parser = argparse.ArgumentParser( + formatter_class=argparse.RawDescriptionHelpFormatter, + description="Parse argument used when running a Flow simulation.", + epilog="python simulate.py EXP_CONFIG --num_runs INT --render") + + # required input parameters + parser.add_argument( + 'exp_config', type=str, + help='Name of the experiment configuration file, as located in ' + 'exp_configs/non_rl.') + + # optional input parameters + parser.add_argument( + '--num_runs', type=int, default=1, + help='Number of simulations to run. Defaults to 1.') + parser.add_argument( + '--render', + action='store_true', + help='Specifies whether to run the simulation during runtime.') + parser.add_argument( + '--aimsun', + action='store_true', + help='Specifies whether to run the simulation using the simulator ' + 'Aimsun. If not specified, the simulator used is SUMO.') + parser.add_argument( + '--gen_emission', + action='store_true', + help='Specifies whether to generate an emission file from the ' + 'simulation.') + + return parser.parse_known_args(args)[0] + + +if __name__ == "__main__": + flags = parse_args(sys.argv[1:]) + + # Get the flow_params object. + module = __import__("exp_configs.non_rl", fromlist=[flags.exp_config]) + flow_params = getattr(module, flags.exp_config).flow_params + + # Update some variables based on inputs. + flow_params['sim'].render = flags.render + flow_params['simulator'] = 'aimsun' if flags.aimsun else 'traci' + + # specify an emission path if they are meant to be generated + if flags.gen_emission: + flow_params['sim'].emission_path = "./data" + + # Create the experiment object. + exp = Experiment(flow_params) + + # Run for the specified number of rollouts. + exp.run(flags.num_runs, convert_to_csv=flags.gen_emission) diff --git a/examples/stable_baselines/figure_eight.py b/examples/stable_baselines/figure_eight.py deleted file mode 100644 index 6e34fdf55..000000000 --- a/examples/stable_baselines/figure_eight.py +++ /dev/null @@ -1,146 +0,0 @@ -"""Ring road example. - -Trains a single autonomous vehicle to stabilize the flow of 21 human-driven -vehicles in a variable length ring road. -""" - -import argparse -import json -import os - -from stable_baselines.common.vec_env import DummyVecEnv, SubprocVecEnv -from stable_baselines import PPO2 - -from flow.envs import AccelEnv -from flow.networks import FigureEightNetwork -from flow.core.params import SumoParams, EnvParams, InitialConfig, NetParams -from flow.core.params import SumoCarFollowingParams -from flow.core.params import VehicleParams -from flow.controllers import IDMController, ContinuousRouter, RLController -from flow.networks.figure_eight import ADDITIONAL_NET_PARAMS -from flow.utils.registry import env_constructor -from flow.utils.rllib import FlowParamsEncoder, get_flow_params - -# time horizon of a single rollout -HORIZON = 1500 - - -# We place one autonomous vehicle and 13 human-driven vehicles in the network -vehicles = VehicleParams() -vehicles.add( - veh_id='human', - acceleration_controller=(IDMController, { - 'noise': 0.2 - }), - routing_controller=(ContinuousRouter, {}), - car_following_params=SumoCarFollowingParams( - speed_mode="obey_safe_speed", - decel=1.5, - ), - num_vehicles=13) -vehicles.add( - veh_id='rl', - acceleration_controller=(RLController, {}), - routing_controller=(ContinuousRouter, {}), - car_following_params=SumoCarFollowingParams( - speed_mode="obey_safe_speed", - decel=1.5, - ), - num_vehicles=1) - -flow_params = dict( - # name of the experiment - exp_tag='figure_eight_intersection_control', - - # name of the flow environment the experiment is running on - env_name=AccelEnv, - - # name of the network class the experiment is running on - network=FigureEightNetwork, - - # simulator that is used by the experiment - simulator='traci', - - # sumo-related parameters (see flow.core.params.SumoParams) - sim=SumoParams( - sim_step=0.1, - render=False, - ), - - # environment related parameters (see flow.core.params.EnvParams) - env=EnvParams( - horizon=HORIZON, - additional_params={ - 'target_velocity': 20, - 'max_accel': 3, - 'max_decel': 3, - 'sort_vehicles': False - }, - ), - - # network-related parameters (see flow.core.params.NetParams and the - # network's documentation or ADDITIONAL_NET_PARAMS component) - net=NetParams( - additional_params=ADDITIONAL_NET_PARAMS.copy(), - ), - - # vehicles to be placed in the network at the start of a rollout (see - # flow.core.params.VehicleParams) - veh=vehicles, - - # parameters specifying the positioning of vehicles upon initialization/ - # reset (see flow.core.params.InitialConfig) - initial=InitialConfig(), - -) - - -def run_model(num_cpus=1, rollout_size=50, num_steps=50): - """Run the model for num_steps if provided. The total rollout length is rollout_size.""" - if num_cpus == 1: - constructor = env_constructor(params=flow_params, version=0)() - env = DummyVecEnv([lambda: constructor]) # The algorithms require a vectorized environment to run - else: - env = SubprocVecEnv([env_constructor(params=flow_params, version=i) for i in range(num_cpus)]) - - model = PPO2('MlpPolicy', env, verbose=1, n_steps=rollout_size) - model.learn(total_timesteps=num_steps) - return model - - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument('--num_cpus', type=int, default=1, help='How many CPUs to use') - parser.add_argument('--num_steps', type=int, default=5000, help='How many total steps to perform learning over') - parser.add_argument('--rollout_size', type=int, default=1000, help='How many steps are in a training batch.') - parser.add_argument('--result_name', type=str, default='figure_eight', help='Name of saved model') - args = parser.parse_args() - model = run_model(args.num_cpus, args.rollout_size, args.num_steps) - # Save the model to a desired folder and then delete it to demonstrate loading - if not os.path.exists(os.path.realpath(os.path.expanduser('~/baseline_results'))): - os.makedirs(os.path.realpath(os.path.expanduser('~/baseline_results'))) - path = os.path.realpath(os.path.expanduser('~/baseline_results')) - save_path = os.path.join(path, args.result_name) - - print('Saving the trained model!') - model.save(save_path) - # dump the flow params - with open(os.path.join(path, args.result_name) + '.json', 'w') as outfile: - json.dump(flow_params, outfile, cls=FlowParamsEncoder, sort_keys=True, indent=4) - del model - del flow_params - - # Replay the result by loading the model - print('Loading the trained model and testing it out!') - model = PPO2.load(save_path) - flow_params = get_flow_params(os.path.join(path, args.result_name) + '.json') - flow_params['sim'].render = True - env_constructor = env_constructor(params=flow_params, version=0)() - env = DummyVecEnv([lambda: env_constructor]) # The algorithms require a vectorized environment to run - obs = env.reset() - reward = 0 - for i in range(flow_params['env'].horizon): - action, _states = model.predict(obs) - obs, rewards, dones, info = env.step(action) - reward += rewards - print('the final reward is {}'.format(reward)) diff --git a/examples/stable_baselines/stabilizing_highway.py b/examples/stable_baselines/stabilizing_highway.py deleted file mode 100644 index 0cdb3459f..000000000 --- a/examples/stable_baselines/stabilizing_highway.py +++ /dev/null @@ -1,187 +0,0 @@ -"""Open merge example. - -Trains a a small percentage of rl vehicles to dissipate shockwaves caused by -merges in an open network. -""" -import argparse -import json -import os - -from stable_baselines.common.vec_env import DummyVecEnv, SubprocVecEnv -from stable_baselines import PPO2 - -from flow.envs import MergePOEnv -from flow.networks import MergeNetwork -from flow.core.params import SumoParams, EnvParams, InitialConfig, InFlows, NetParams -from flow.core.params import VehicleParams, SumoCarFollowingParams -from flow.controllers import RLController, IDMController -from flow.networks.merge import ADDITIONAL_NET_PARAMS -from flow.utils.registry import env_constructor -from flow.utils.rllib import FlowParamsEncoder, get_flow_params - -# experiment number -# - 0: 10% RL penetration, 5 max controllable vehicles -# - 1: 25% RL penetration, 13 max controllable vehicles -# - 2: 33% RL penetration, 17 max controllable vehicles -EXP_NUM = 0 - -# time horizon of a single rollout -HORIZON = 600 -# number of rollouts per training iteration -N_ROLLOUTS = 20 -# number of parallel workers -N_CPUS = 2 - -# inflow rate at the highway -FLOW_RATE = 2000 -# percent of autonomous vehicles -RL_PENETRATION = [0.1, 0.25, 0.33][EXP_NUM] -# num_rl term (see ADDITIONAL_ENV_PARAMs) -NUM_RL = [5, 13, 17][EXP_NUM] - -# We consider a highway network with an upstream merging lane producing -# shockwaves -additional_net_params = ADDITIONAL_NET_PARAMS.copy() -additional_net_params["merge_lanes"] = 1 -additional_net_params["highway_lanes"] = 1 -additional_net_params["pre_merge_length"] = 500 - -# RL vehicles constitute 5% of the total number of vehicles -vehicles = VehicleParams() -vehicles.add( - veh_id="human", - acceleration_controller=(IDMController, { - "noise": 0.2 - }), - car_following_params=SumoCarFollowingParams( - speed_mode="obey_safe_speed", - ), - num_vehicles=5) -vehicles.add( - veh_id="rl", - acceleration_controller=(RLController, {}), - car_following_params=SumoCarFollowingParams( - speed_mode="obey_safe_speed", - ), - num_vehicles=0) - -# Vehicles are introduced from both sides of merge, with RL vehicles entering -# from the highway portion as well -inflow = InFlows() -inflow.add( - veh_type="human", - edge="inflow_highway", - vehs_per_hour=(1 - RL_PENETRATION) * FLOW_RATE, - departLane="free", - departSpeed=10) -inflow.add( - veh_type="rl", - edge="inflow_highway", - vehs_per_hour=RL_PENETRATION * FLOW_RATE, - departLane="free", - departSpeed=10) -inflow.add( - veh_type="human", - edge="inflow_merge", - vehs_per_hour=100, - departLane="free", - departSpeed=7.5) - -flow_params = dict( - # name of the experiment - exp_tag="stabilizing_open_network_merges", - - # name of the flow environment the experiment is running on - env_name=MergePOEnv, - - # name of the network class the experiment is running on - network=MergeNetwork, - - # simulator that is used by the experiment - simulator='traci', - - # sumo-related parameters (see flow.core.params.SumoParams) - sim=SumoParams( - sim_step=0.2, - render=False, - restart_instance=True, - ), - - # environment related parameters (see flow.core.params.EnvParams) - env=EnvParams( - horizon=HORIZON, - sims_per_step=5, - warmup_steps=0, - additional_params={ - "max_accel": 1.5, - "max_decel": 1.5, - "target_velocity": 20, - "num_rl": NUM_RL, - }, - ), - - # network-related parameters (see flow.core.params.NetParams and the - # network's documentation or ADDITIONAL_NET_PARAMS component) - net=NetParams( - inflows=inflow, - additional_params=additional_net_params, - ), - - # vehicles to be placed in the network at the start of a rollout (see - # flow.core.params.VehicleParams) - veh=vehicles, - - # parameters specifying the positioning of vehicles upon initialization/ - # reset (see flow.core.params.InitialConfig) - initial=InitialConfig(), -) - - -def run_model(num_cpus=1, rollout_size=50, num_steps=50): - """Run the model for num_steps if provided. The total rollout length is rollout_size.""" - if num_cpus == 1: - constructor = env_constructor(params=flow_params, version=0)() - env = DummyVecEnv([lambda: constructor]) # The algorithms require a vectorized environment to run - else: - env = SubprocVecEnv([env_constructor(params=flow_params, version=i) for i in range(num_cpus)]) - - model = PPO2('MlpPolicy', env, verbose=1, n_steps=rollout_size) - model.learn(total_timesteps=num_steps) - return model - - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument('--num_cpus', type=int, default=1, help='How many CPUs to use') - parser.add_argument('--num_steps', type=int, default=5000, help='How many total steps to perform learning over') - parser.add_argument('--rollout_size', type=int, default=1000, help='How many steps are in a training batch.') - parser.add_argument('--result_name', type=str, default='stabilize_highway', help='Name of saved model') - args = parser.parse_args() - model = run_model(args.num_cpus, args.rollout_size, args.num_steps) - # Save the model to a desired folder and then delete it to demonstrate loading - if not os.path.exists(os.path.realpath(os.path.expanduser('~/baseline_results'))): - os.makedirs(os.path.realpath(os.path.expanduser('~/baseline_results'))) - path = os.path.realpath(os.path.expanduser('~/baseline_results')) - save_path = os.path.join(path, args.result_name) - print('Saving the trained model!') - model.save(save_path) - # dump the flow params - with open(os.path.join(path, args.result_name) + '.json', 'w') as outfile: - json.dump(flow_params, outfile, cls=FlowParamsEncoder, sort_keys=True, indent=4) - del model - del flow_params - - # Replay the result by loading the model - print('Loading the trained model and testing it out!') - model = PPO2.load(save_path) - flow_params = get_flow_params(os.path.join(path, args.result_name) + '.json') - flow_params['sim'].render = True - env_constructor = env_constructor(params=flow_params, version=0)() - env = DummyVecEnv([lambda: env_constructor]) # The algorithms require a vectorized environment to run - obs = env.reset() - reward = 0 - for i in range(flow_params['env'].horizon): - action, _states = model.predict(obs) - obs, rewards, dones, info = env.step(action) - reward += rewards - print('the final reward is {}'.format(reward)) diff --git a/examples/stable_baselines/stabilizing_the_ring.py b/examples/stable_baselines/stabilizing_the_ring.py deleted file mode 100644 index 25f39bc1f..000000000 --- a/examples/stable_baselines/stabilizing_the_ring.py +++ /dev/null @@ -1,143 +0,0 @@ -"""Ring road example. - -Trains a single autonomous vehicle to stabilize the flow of 21 human-driven -vehicles in a variable length ring road. -""" - -import argparse -import json -import os - -from stable_baselines.common.vec_env import DummyVecEnv, SubprocVecEnv -from stable_baselines import PPO2 - -from flow.envs import WaveAttenuationPOEnv -from flow.networks import RingNetwork -from flow.core.params import SumoParams, EnvParams, InitialConfig, NetParams -from flow.core.params import VehicleParams, SumoCarFollowingParams -from flow.controllers import RLController, IDMController, ContinuousRouter -from flow.utils.registry import env_constructor -from flow.utils.rllib import FlowParamsEncoder, get_flow_params - -# time horizon of a single rollout -HORIZON = 3000 - -# We place one autonomous vehicle and 22 human-driven vehicles in the network -vehicles = VehicleParams() -vehicles.add( - veh_id="human", - acceleration_controller=(IDMController, { - "noise": 0.2 - }), - car_following_params=SumoCarFollowingParams( - min_gap=0 - ), - routing_controller=(ContinuousRouter, {}), - num_vehicles=21) -vehicles.add( - veh_id="rl", - acceleration_controller=(RLController, {}), - routing_controller=(ContinuousRouter, {}), - num_vehicles=1) - -flow_params = dict( - # name of the experiment - exp_tag="stabilizing_the_ring", - - # name of the flow environment the experiment is running on - env_name=WaveAttenuationPOEnv, - - # name of the network class the experiment is running on - network=RingNetwork, - - # simulator that is used by the experiment - simulator='traci', - - # sumo-related parameters (see flow.core.params.SumoParams) - sim=SumoParams( - sim_step=0.1, - render=False, - restart_instance=False - ), - - # environment related parameters (see flow.core.params.EnvParams) - env=EnvParams( - horizon=HORIZON, - warmup_steps=750, - clip_actions=False, - additional_params={ - "max_accel": 1, - "max_decel": 1, - "ring_length": [220, 270], - }, - ), - - # network-related parameters (see flow.core.params.NetParams and the - # network's documentation or ADDITIONAL_NET_PARAMS component) - net=NetParams( - additional_params={ - "length": 260, - "lanes": 1, - "speed_limit": 30, - "resolution": 40, - }, ), - - # vehicles to be placed in the network at the start of a rollout (see - # flow.core.params.VehicleParams) - veh=vehicles, - - # parameters specifying the positioning of vehicles upon initialization/ - # reset (see flow.core.params.InitialConfig) - initial=InitialConfig(), -) - - -def run_model(num_cpus=1, rollout_size=50, num_steps=50): - """Run the model for num_steps if provided. The total rollout length is rollout_size.""" - if num_cpus == 1: - constructor = env_constructor(params=flow_params, version=0)() - env = DummyVecEnv([lambda: constructor]) # The algorithms require a vectorized environment to run - else: - env = SubprocVecEnv([env_constructor(params=flow_params, version=i) for i in range(num_cpus)]) - - model = PPO2('MlpPolicy', env, verbose=1, n_steps=rollout_size) - model.learn(total_timesteps=num_steps) - return model - - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument('--num_cpus', type=int, default=1, help='How many CPUs to use') - parser.add_argument('--num_steps', type=int, default=5000, help='How many total steps to perform learning over') - parser.add_argument('--rollout_size', type=int, default=1000, help='How many steps are in a training batch.') - parser.add_argument('--result_name', type=str, default='stabilize_ring', help='Name of saved model') - args = parser.parse_args() - model = run_model(args.num_cpus, args.rollout_size, args.num_steps) - # Save the model to a desired folder and then delete it to demonstrate loading - if not os.path.exists(os.path.realpath(os.path.expanduser('~/baseline_results'))): - os.makedirs(os.path.realpath(os.path.expanduser('~/baseline_results'))) - path = os.path.realpath(os.path.expanduser('~/baseline_results')) - save_path = os.path.join(path, args.result_name) - - print('Saving the trained model!') - model.save(save_path) - # dump the flow params - with open(os.path.join(path, args.result_name) + '.json', 'w') as outfile: - json.dump(flow_params, outfile, cls=FlowParamsEncoder, sort_keys=True, indent=4) - del model - del flow_params - - # Replay the result by loading the model - print('Loading the trained model and testing it out!') - model = PPO2.load(save_path) - flow_params = get_flow_params(os.path.join(path, args.result_name) + '.json') - flow_params['sim'].render = True - env_constructor = env_constructor(params=flow_params, version=0)() - env = DummyVecEnv([lambda: env_constructor]) # The algorithms require a vectorized environment to run - obs = env.reset() - reward = 0 - for i in range(flow_params['env'].horizon): - action, _states = model.predict(obs) - obs, rewards, dones, info = env.step(action) - reward += rewards - print('the final reward is {}'.format(reward)) diff --git a/examples/stable_baselines/traffic_light_grid.py b/examples/stable_baselines/traffic_light_grid.py deleted file mode 100644 index 5419f0e21..000000000 --- a/examples/stable_baselines/traffic_light_grid.py +++ /dev/null @@ -1,301 +0,0 @@ -"""Traffic Light Grid example.""" - -import argparse -import json -import os - -from stable_baselines.common.vec_env import DummyVecEnv, SubprocVecEnv -from stable_baselines import PPO2 - -from flow.envs import TrafficLightGridPOEnv -from flow.networks import TrafficLightGridNetwork -from flow.core.params import SumoParams, EnvParams, InitialConfig, NetParams -from flow.core.params import SumoCarFollowingParams, InFlows -from flow.core.params import VehicleParams -from flow.controllers import SimCarFollowingController, GridRouter -from flow.utils.registry import env_constructor -from flow.utils.rllib import FlowParamsEncoder, get_flow_params - -# time horizon of a single rollout -HORIZON = 200 - - -def gen_edges(col_num, row_num): - """Generate the names of the outer edges in the traffic light grid network. - - Parameters - ---------- - col_num : int - number of columns in the traffic light grid - row_num : int - number of rows in the traffic light grid - - Returns - ------- - list of str - names of all the outer edges - """ - edges = [] - for i in range(col_num): - edges += ['left' + str(row_num) + '_' + str(i)] - edges += ['right' + '0' + '_' + str(i)] - - # build the left and then the right edgesØ - for i in range(row_num): - edges += ['bot' + str(i) + '_' + '0'] - edges += ['top' + str(i) + '_' + str(col_num)] - - return edges - - -def get_inflow_params(col_num, row_num, additional_net_params): - """Define the network and initial params in the presence of inflows. - - Parameters - ---------- - col_num : int - number of columns in the traffic light grid - row_num : int - number of rows in the traffic light grid - additional_net_params : dict - network-specific parameters that are unique to the traffic light grid - - Returns - ------- - flow.core.params.InitialConfig - parameters specifying the initial configuration of vehicles in the - network - flow.core.params.NetParams - network-specific parameters used to generate the network - """ - initial = InitialConfig( - spacing='custom', lanes_distribution=float('inf'), shuffle=True) - - inflow = InFlows() - outer_edges = gen_edges(col_num, row_num) - for i in range(len(outer_edges)): - inflow.add( - veh_type='idm', - edge=outer_edges[i], - probability=0.25, - departLane='free', - departSpeed=20) - - net = NetParams( - inflows=inflow, - additional_params=additional_net_params) - - return initial, net - - -def get_non_flow_params(enter_speed, add_net_params): - """Define the network and initial params in the absence of inflows. - - Note that when a vehicle leaves a network in this case, it is immediately - returns to the start of the row/column it was traversing, and in the same - direction as it was before. - - Parameters - ---------- - enter_speed : float - initial speed of vehicles as they enter the network. - add_net_params: dict - additional network-specific parameters (unique to the traffic light grid) - - Returns - ------- - flow.core.params.InitialConfig - parameters specifying the initial configuration of vehicles in the - network - flow.core.params.NetParams - network-specific parameters used to generate the network - """ - additional_init_params = {'enter_speed': enter_speed} - initial = InitialConfig( - spacing='custom', additional_params=additional_init_params) - net = NetParams( - additional_params=add_net_params) - - return initial, net - - -V_ENTER = 30 -INNER_LENGTH = 300 -LONG_LENGTH = 100 -SHORT_LENGTH = 300 -N_ROWS = 3 -N_COLUMNS = 3 -NUM_CARS_LEFT = 1 -NUM_CARS_RIGHT = 1 -NUM_CARS_TOP = 1 -NUM_CARS_BOT = 1 -tot_cars = (NUM_CARS_LEFT + NUM_CARS_RIGHT) * N_COLUMNS \ - + (NUM_CARS_BOT + NUM_CARS_TOP) * N_ROWS - -grid_array = { - "short_length": SHORT_LENGTH, - "inner_length": INNER_LENGTH, - "long_length": LONG_LENGTH, - "row_num": N_ROWS, - "col_num": N_COLUMNS, - "cars_left": NUM_CARS_LEFT, - "cars_right": NUM_CARS_RIGHT, - "cars_top": NUM_CARS_TOP, - "cars_bot": NUM_CARS_BOT -} - -additional_env_params = { - 'target_velocity': 50, - 'switch_time': 3.0, - 'num_observed': 2, - 'discrete': False, - 'tl_type': 'controlled' - } - -additional_net_params = { - 'speed_limit': 35, - 'grid_array': grid_array, - 'horizontal_lanes': 1, - 'vertical_lanes': 1 -} - -vehicles = VehicleParams() -vehicles.add( - veh_id='idm', - acceleration_controller=(SimCarFollowingController, {}), - car_following_params=SumoCarFollowingParams( - minGap=2.5, - decel=7.5, # avoid collisions at emergency stops - max_speed=V_ENTER, - speed_mode="all_checks", - ), - routing_controller=(GridRouter, {}), - num_vehicles=tot_cars) - -flow_params = dict( - # name of the experiment - exp_tag='traffic_light_grid', - - # name of the flow environment the experiment is running on - env_name=TrafficLightGridPOEnv, - - # name of the network class the experiment is running on - network=TrafficLightGridNetwork, - - # simulator that is used by the experiment - simulator='traci', - - # sumo-related parameters (see flow.core.params.SumoParams) - sim=SumoParams( - sim_step=1, - render=False, - ), - - # environment related parameters (see flow.core.params.EnvParams) - env=EnvParams( - horizon=HORIZON, - additional_params=additional_env_params, - ), - - # network-related parameters (see flow.core.params.NetParams and the - # network's documentation or ADDITIONAL_NET_PARAMS component). This is - # filled in by the setup_exps method below. - net=None, - - # vehicles to be placed in the network at the start of a rollout (see - # flow.core.params.VehicleParams) - veh=vehicles, - - # parameters specifying the positioning of vehicles upon initialization/ - # reset (see flow.core.params.InitialConfig). This is filled in by the - # setup_exps method below. - initial=None, -) - - -def setup_exps(use_inflows=False): - """Return the relevant components of an RLlib experiment. - - Parameters - ---------- - use_inflows : bool, optional - set to True if you would like to run the experiment with inflows of - vehicles from the edges, and False otherwise - - Returns - ------- - str - name of the training algorithm - str - name of the gym environment to be trained - dict - training configuration parameters - """ - # collect the initialization and network-specific parameters based on the - # choice to use inflows or not - if use_inflows: - initial_config, net_params = get_inflow_params( - col_num=N_COLUMNS, - row_num=N_ROWS, - additional_net_params=additional_net_params) - else: - initial_config, net_params = get_non_flow_params( - enter_speed=V_ENTER, - add_net_params=additional_net_params) - return initial_config, net_params - - -def run_model(num_cpus=1, rollout_size=50, num_steps=50, use_inflows=False): - """Run the model for num_steps if provided. The total rollout length is rollout_size.""" - initial_config, net_params = setup_exps(use_inflows) - # add the new parameters to flow_params - flow_params['initial'] = initial_config - flow_params['net'] = net_params - - if num_cpus == 1: - constructor = env_constructor(params=flow_params, version=0)() - env = DummyVecEnv([lambda: constructor]) # The algorithms require a vectorized environment to run - else: - env = SubprocVecEnv([env_constructor(params=flow_params, version=i) for i in range(num_cpus)]) - - model = PPO2('MlpPolicy', env, verbose=1, n_steps=rollout_size) - model.learn(total_timesteps=num_steps) - return model - - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument('--num_cpus', type=int, default=1, help='How many CPUs to use') - parser.add_argument('--num_steps', type=int, default=5000, help='How many total steps to perform learning over') - parser.add_argument('--rollout_size', type=int, default=1000, help='How many steps are in a training batch.') - parser.add_argument('--result_name', type=str, default='traffic_light_grid', help='Name of saved model') - parser.add_argument('--use_inflows', action='store_true') - args = parser.parse_args() - model = run_model(args.num_cpus, args.rollout_size, args.num_steps, args.use_inflows) - # Save the model to a desired folder and then delete it to demonstrate loading - if not os.path.exists(os.path.realpath(os.path.expanduser('~/baseline_results'))): - os.makedirs(os.path.realpath(os.path.expanduser('~/baseline_results'))) - path = os.path.realpath(os.path.expanduser('~/baseline_results')) - save_path = os.path.join(path, args.result_name) - # dump the model - model.save(save_path) - # dump the flow params - with open(os.path.join(path, args.result_name) + '.json', 'w') as outfile: - json.dump(flow_params, outfile, cls=FlowParamsEncoder, sort_keys=True, indent=4) - del model - del flow_params - - # Replay the result by loading the model - print('Loading the trained model and testing it out!') - model = PPO2.load(save_path) - flow_params = get_flow_params(os.path.join(path, args.result_name) + '.json') - flow_params['sim'].render = True - env_constructor = env_constructor(params=flow_params, version=0)() - env = DummyVecEnv([lambda: env_constructor]) # The algorithms require a vectorized environment to run - obs = env.reset() - reward = 0 - for i in range(flow_params['env'].horizon): - action, _states = model.predict(obs) - obs, rewards, dones, info = env.step(action) - reward += rewards - print('the final reward is {}'.format(reward)) diff --git a/examples/stable_baselines/velocity_bottleneck.py b/examples/stable_baselines/velocity_bottleneck.py deleted file mode 100644 index 413da8f7f..000000000 --- a/examples/stable_baselines/velocity_bottleneck.py +++ /dev/null @@ -1,211 +0,0 @@ -"""Bottleneck example. - -Bottleneck in which the actions are specifying a desired velocity -in a segment of space -""" -import argparse -import json -import os - -from stable_baselines.common.vec_env import DummyVecEnv, SubprocVecEnv -from stable_baselines import PPO2 - -from flow.envs import BottleneckDesiredVelocityEnv -from flow.networks import BottleneckNetwork -from flow.core.params import SumoParams, EnvParams, InitialConfig, NetParams, \ - InFlows, SumoCarFollowingParams, SumoLaneChangeParams -from flow.core.params import TrafficLightParams -from flow.core.params import VehicleParams -from flow.controllers import RLController, ContinuousRouter, \ - SimLaneChangeController -from flow.utils.registry import env_constructor -from flow.utils.rllib import FlowParamsEncoder, get_flow_params - -# time horizon of a single rollout -HORIZON = 1000 -# number of parallel workers -N_CPUS = 2 -# number of rollouts per training iteration -N_ROLLOUTS = N_CPUS * 4 - -SCALING = 1 -NUM_LANES = 4 * SCALING # number of lanes in the widest highway -DISABLE_TB = True -DISABLE_RAMP_METER = True -AV_FRAC = 0.10 - -vehicles = VehicleParams() -vehicles.add( - veh_id="human", - lane_change_controller=(SimLaneChangeController, {}), - routing_controller=(ContinuousRouter, {}), - car_following_params=SumoCarFollowingParams( - speed_mode="all_checks", - ), - lane_change_params=SumoLaneChangeParams( - lane_change_mode=0, - ), - num_vehicles=1 * SCALING) -vehicles.add( - veh_id="followerstopper", - acceleration_controller=(RLController, {}), - lane_change_controller=(SimLaneChangeController, {}), - routing_controller=(ContinuousRouter, {}), - car_following_params=SumoCarFollowingParams( - speed_mode=9, - ), - lane_change_params=SumoLaneChangeParams( - lane_change_mode=0, - ), - num_vehicles=1 * SCALING) - -controlled_segments = [("1", 1, False), ("2", 2, True), ("3", 2, True), - ("4", 2, True), ("5", 1, False)] -num_observed_segments = [("1", 1), ("2", 3), ("3", 3), ("4", 3), ("5", 1)] -additional_env_params = { - "target_velocity": 40, - "disable_tb": True, - "disable_ramp_metering": True, - "controlled_segments": controlled_segments, - "symmetric": False, - "observed_segments": num_observed_segments, - "reset_inflow": False, - "lane_change_duration": 5, - "max_accel": 3, - "max_decel": 3, - "inflow_range": [1000, 2000] -} - -# flow rate -flow_rate = 2300 * SCALING - -# percentage of flow coming out of each lane -inflow = InFlows() -inflow.add( - veh_type="human", - edge="1", - vehs_per_hour=flow_rate * (1 - AV_FRAC), - departLane="random", - departSpeed=10) -inflow.add( - veh_type="followerstopper", - edge="1", - vehs_per_hour=flow_rate * AV_FRAC, - departLane="random", - departSpeed=10) - -traffic_lights = TrafficLightParams() -if not DISABLE_TB: - traffic_lights.add(node_id="2") -if not DISABLE_RAMP_METER: - traffic_lights.add(node_id="3") - -additional_net_params = {"scaling": SCALING, "speed_limit": 23} -net_params = NetParams( - inflows=inflow, - additional_params=additional_net_params) - -flow_params = dict( - # name of the experiment - exp_tag="DesiredVelocity", - - # name of the flow environment the experiment is running on - env_name=BottleneckDesiredVelocityEnv, - - # name of the network class the experiment is running on - network=BottleneckNetwork, - - # simulator that is used by the experiment - simulator='traci', - - # sumo-related parameters (see flow.core.params.SumoParams) - sim=SumoParams( - sim_step=0.5, - render=False, - print_warnings=False, - restart_instance=True, - ), - - # environment related parameters (see flow.core.params.EnvParams) - env=EnvParams( - warmup_steps=40, - sims_per_step=1, - horizon=HORIZON, - additional_params=additional_env_params, - ), - - # network-related parameters (see flow.core.params.NetParams and the - # network's documentation or ADDITIONAL_NET_PARAMS component) - net=NetParams( - inflows=inflow, - additional_params=additional_net_params, - ), - - # vehicles to be placed in the network at the start of a rollout (see - # flow.core.params.VehicleParams) - veh=vehicles, - - # parameters specifying the positioning of vehicles upon initialization/ - # reset (see flow.core.params.InitialConfig) - initial=InitialConfig( - spacing="uniform", - min_gap=5, - lanes_distribution=float("inf"), - edges_distribution=["2", "3", "4", "5"], - ), - - # traffic lights to be introduced to specific nodes (see - # flow.core.params.TrafficLightParams) - tls=traffic_lights, -) - - -def run_model(num_cpus=1, rollout_size=50, num_steps=50): - """Run the model for num_steps if provided. The total rollout length is rollout_size.""" - if num_cpus == 1: - constructor = env_constructor(params=flow_params, version=0)() - env = DummyVecEnv([lambda: constructor]) # The algorithms require a vectorized environment to run - else: - env = SubprocVecEnv([env_constructor(params=flow_params, version=i) for i in range(num_cpus)]) - - model = PPO2('MlpPolicy', env, verbose=1, n_steps=rollout_size) - model.learn(total_timesteps=num_steps) - return model - - -if __name__ == "__main__": - parser = argparse.ArgumentParser() - parser.add_argument('--num_cpus', type=int, default=1, help='How many CPUs to use') - parser.add_argument('--num_steps', type=int, default=5000, help='How many total steps to perform learning over') - parser.add_argument('--rollout_size', type=int, default=1000, help='How many steps are in a training batch.') - parser.add_argument('--result_name', type=str, default='velocity_bottleneck', help='Name of saved model') - args = parser.parse_args() - model = run_model(args.num_cpus, args.rollout_size, args.num_steps) - # Save the model to a desired folder and then delete it to demonstrate loading - if not os.path.exists(os.path.realpath(os.path.expanduser('~/baseline_results'))): - os.makedirs(os.path.realpath(os.path.expanduser('~/baseline_results'))) - path = os.path.realpath(os.path.expanduser('~/baseline_results')) - save_path = os.path.join(path, args.result_name) - - print('Saving the trained model!') - model.save(save_path) - # dump the flow params - with open(os.path.join(path, args.result_name) + '.json', 'w') as outfile: - json.dump(flow_params, outfile, cls=FlowParamsEncoder, sort_keys=True, indent=4) - del model - del flow_params - - # Replay the result by loading the model - print('Loading the trained model and testing it out!') - model = PPO2.load(save_path) - flow_params = get_flow_params(os.path.join(path, args.result_name) + '.json') - flow_params['sim'].render = True - env_constructor = env_constructor(params=flow_params, version=0)() - env = DummyVecEnv([lambda: env_constructor]) # The algorithms require a vectorized environment to run - obs = env.reset() - reward = 0 - for i in range(flow_params['env'].horizon): - action, _states = model.predict(obs) - obs, rewards, dones, info = env.step(action) - reward += rewards - print('the final reward is {}'.format(reward)) diff --git a/examples/sumo/__init__.py b/examples/sumo/__init__.py deleted file mode 100644 index d48602a8b..000000000 --- a/examples/sumo/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Empty init file to ensure that the examples documentation builds.""" diff --git a/examples/sumo/bay_bridge.py b/examples/sumo/bay_bridge.py deleted file mode 100644 index 49b4f7d23..000000000 --- a/examples/sumo/bay_bridge.py +++ /dev/null @@ -1,204 +0,0 @@ -"""Bay Bridge simulation.""" - -import os -import urllib.request - -from flow.core.params import SumoParams, EnvParams, NetParams, InitialConfig, \ - SumoCarFollowingParams, SumoLaneChangeParams, InFlows -from flow.core.params import VehicleParams -from flow.core.params import TrafficLightParams - -from flow.core.experiment import Experiment -from flow.envs.bay_bridge import BayBridgeEnv -from flow.networks.bay_bridge import BayBridgeNetwork, EDGES_DISTRIBUTION -from flow.controllers import SimCarFollowingController, BayBridgeRouter - -TEMPLATE = os.path.join( - os.path.dirname(os.path.abspath(__file__)), "bay_bridge.net.xml") - - -def bay_bridge_example(render=None, - use_inflows=False, - use_traffic_lights=False): - """ - Perform a simulation of vehicles on the Oakland-San Francisco Bay Bridge. - - Parameters - ---------- - render: bool, optional - specifies whether to use the gui during execution - use_inflows: bool, optional - whether to activate inflows from the peripheries of the network - use_traffic_lights: bool, optional - whether to activate the traffic lights in the network - - Returns - ------- - exp: flow.core.experiment.Experiment - A non-rl experiment demonstrating the performance of human-driven - vehicles simulated by sumo on the Bay Bridge. - """ - sim_params = SumoParams(sim_step=0.6, overtake_right=True) - - if render is not None: - sim_params.render = render - - car_following_params = SumoCarFollowingParams( - speedDev=0.2, - speed_mode="all_checks", - ) - lane_change_params = SumoLaneChangeParams( - lc_assertive=20, - lc_pushy=0.8, - lc_speed_gain=4.0, - model="LC2013", - lane_change_mode="no_lat_collide", - # lcKeepRight=0.8 - ) - - vehicles = VehicleParams() - vehicles.add( - veh_id="human", - acceleration_controller=(SimCarFollowingController, {}), - routing_controller=(BayBridgeRouter, {}), - car_following_params=car_following_params, - lane_change_params=lane_change_params, - num_vehicles=1400) - - additional_env_params = {} - env_params = EnvParams(additional_params=additional_env_params) - - traffic_lights = TrafficLightParams() - - inflow = InFlows() - - if use_inflows: - # south - inflow.add( - veh_type="human", - edge="183343422", - vehsPerHour=528, - departLane="0", - departSpeed=20) - inflow.add( - veh_type="human", - edge="183343422", - vehsPerHour=864, - departLane="1", - departSpeed=20) - inflow.add( - veh_type="human", - edge="183343422", - vehsPerHour=600, - departLane="2", - departSpeed=20) - - inflow.add( - veh_type="human", - edge="393649534", - probability=0.1, - departLane="0", - departSpeed=20) # no data for this - - # west - inflow.add( - veh_type="human", - edge="11189946", - vehsPerHour=1752, - departLane="0", - departSpeed=20) - inflow.add( - veh_type="human", - edge="11189946", - vehsPerHour=2136, - departLane="1", - departSpeed=20) - inflow.add( - veh_type="human", - edge="11189946", - vehsPerHour=576, - departLane="2", - departSpeed=20) - - # north - inflow.add( - veh_type="human", - edge="28413687#0", - vehsPerHour=2880, - departLane="0", - departSpeed=20) - inflow.add( - veh_type="human", - edge="28413687#0", - vehsPerHour=2328, - departLane="1", - departSpeed=20) - inflow.add( - veh_type="human", - edge="28413687#0", - vehsPerHour=3060, - departLane="2", - departSpeed=20) - inflow.add( - veh_type="human", - edge="11198593", - probability=0.1, - departLane="0", - departSpeed=20) # no data for this - inflow.add( - veh_type="human", - edge="11197889", - probability=0.1, - departLane="0", - departSpeed=20) # no data for this - - # midway through bridge - inflow.add( - veh_type="human", - edge="35536683", - probability=0.1, - departLane="0", - departSpeed=20) # no data for this - - net_params = NetParams(inflows=inflow) - net_params.template = TEMPLATE - - # download the template from AWS - if use_traffic_lights: - my_url = "https://s3-us-west-1.amazonaws.com/flow.netfiles/" \ - "bay_bridge_TL_all_green.net.xml" - else: - my_url = "https://s3-us-west-1.amazonaws.com/flow.netfiles/" \ - "bay_bridge_junction_fix.net.xml" - my_file = urllib.request.urlopen(my_url) - data_to_write = my_file.read() - - with open( - os.path.join(os.path.dirname(os.path.abspath(__file__)), TEMPLATE), - "wb+") as f: - f.write(data_to_write) - - initial_config = InitialConfig( - spacing="uniform", - min_gap=15, - edges_distribution=EDGES_DISTRIBUTION.copy()) - - network = BayBridgeNetwork( - name="bay_bridge", - vehicles=vehicles, - traffic_lights=traffic_lights, - net_params=net_params, - initial_config=initial_config) - - env = BayBridgeEnv(env_params, sim_params, network) - - return Experiment(env) - - -if __name__ == "__main__": - # import the experiment variable - exp = bay_bridge_example( - render=True, use_inflows=False, use_traffic_lights=False) - - # run for a set number of rollouts / time steps - exp.run(1, 1500) diff --git a/examples/sumo/bay_bridge_toll.py b/examples/sumo/bay_bridge_toll.py deleted file mode 100644 index cd0aa110f..000000000 --- a/examples/sumo/bay_bridge_toll.py +++ /dev/null @@ -1,132 +0,0 @@ -"""Bay Bridge toll example.""" - -import os -import urllib.request - -from flow.core.params import SumoParams, EnvParams, NetParams, InitialConfig, \ - SumoLaneChangeParams, SumoCarFollowingParams, InFlows -from flow.core.params import VehicleParams - -from flow.core.experiment import Experiment -from flow.envs.bay_bridge import BayBridgeEnv - -from flow.networks.bay_bridge_toll import BayBridgeTollNetwork -from flow.networks.bay_bridge_toll import EDGES_DISTRIBUTION -from flow.controllers import SimCarFollowingController, BayBridgeRouter - -TEMPLATE = os.path.join( - os.path.dirname(os.path.abspath(__file__)), "bottleneck.net.xml") - - -def bay_bridge_toll_example(render=None, use_traffic_lights=False): - """Perform a simulation of the toll portion of the Bay Bridge. - - This consists of the toll booth and sections of the road leading up to it. - - Parameters - ---------- - render : bool, optional - specifies whether to use the gui during execution - use_traffic_lights: bool, optional - whether to activate the traffic lights in the network - - Note - ---- - Unlike the bay_bridge_example, inflows are always activated here. - """ - sim_params = SumoParams(sim_step=0.4, overtake_right=True) - - if render is not None: - sim_params.render = render - - car_following_params = SumoCarFollowingParams( - speedDev=0.2, - speed_mode="all_checks", - ) - lane_change_params = SumoLaneChangeParams( - model="LC2013", - lcCooperative=0.2, - lcSpeedGain=15, - lane_change_mode="no_lat_collide", - ) - - vehicles = VehicleParams() - - vehicles.add( - veh_id="human", - acceleration_controller=(SimCarFollowingController, {}), - routing_controller=(BayBridgeRouter, {}), - car_following_params=car_following_params, - lane_change_params=lane_change_params, - num_vehicles=50) - - additional_env_params = {} - env_params = EnvParams(additional_params=additional_env_params) - - inflow = InFlows() - - inflow.add( - veh_type="human", - edge="393649534", - probability=0.2, - departLane="random", - departSpeed=10) - inflow.add( - veh_type="human", - edge="4757680", - probability=0.2, - departLane="random", - departSpeed=10) - inflow.add( - veh_type="human", - edge="32661316", - probability=0.2, - departLane="random", - departSpeed=10) - inflow.add( - veh_type="human", - edge="90077193#0", - vehs_per_hour=2000, - departLane="random", - departSpeed=10) - - net_params = NetParams(inflows=inflow, template=TEMPLATE) - - # download the template from AWS - if use_traffic_lights: - my_url = "https://s3-us-west-1.amazonaws.com/flow.netfiles/" \ - "bay_bridge_TL_all_green.net.xml" - else: - my_url = "https://s3-us-west-1.amazonaws.com/flow.netfiles/" \ - "bay_bridge_junction_fix.net.xml" - my_file = urllib.request.urlopen(my_url) - data_to_write = my_file.read() - - with open( - os.path.join(os.path.dirname(os.path.abspath(__file__)), TEMPLATE), - "wb+") as f: - f.write(data_to_write) - - initial_config = InitialConfig( - spacing="uniform", # "random", - min_gap=15, - edges_distribution=EDGES_DISTRIBUTION.copy()) - - network = BayBridgeTollNetwork( - name="bay_bridge_toll", - vehicles=vehicles, - net_params=net_params, - initial_config=initial_config) - - env = BayBridgeEnv(env_params, sim_params, network) - - return Experiment(env) - - -if __name__ == "__main__": - # import the experiment variable - exp = bay_bridge_toll_example( - render=True, use_traffic_lights=False) - - # run for a set number of rollouts / time steps - exp.run(1, 1500) diff --git a/examples/sumo/bottlenecks.py b/examples/sumo/bottlenecks.py deleted file mode 100644 index 2d4c58c03..000000000 --- a/examples/sumo/bottlenecks.py +++ /dev/null @@ -1,202 +0,0 @@ -"""File demonstrating formation of congestion in bottleneck.""" - -from flow.core.params import SumoParams, EnvParams, NetParams, InitialConfig, \ - InFlows, SumoLaneChangeParams, SumoCarFollowingParams -from flow.core.params import VehicleParams -from flow.core.params import TrafficLightParams - -from flow.networks.bottleneck import BottleneckNetwork -from flow.controllers import SimLaneChangeController, ContinuousRouter -from flow.envs.bottleneck import BottleneckEnv -from flow.core.experiment import Experiment - -import logging - -import numpy as np -SCALING = 1 -DISABLE_TB = True - -# If set to False, ALINEA will control the ramp meter -DISABLE_RAMP_METER = True -INFLOW = 2300 - - -class BottleneckDensityExperiment(Experiment): - """Experiment object for bottleneck-specific simulations. - - Extends flow.core.experiment.Experiment - """ - - def __init__(self, env): - """Instantiate the bottleneck experiment.""" - super().__init__(env) - - def run(self, num_runs, num_steps, rl_actions=None, convert_to_csv=False): - """See parent class.""" - info_dict = {} - if rl_actions is None: - - def rl_actions(*_): - return None - - rets = [] - mean_rets = [] - ret_lists = [] - vels = [] - mean_vels = [] - std_vels = [] - mean_densities = [] - mean_outflows = [] - for i in range(num_runs): - vel = np.zeros(num_steps) - logging.info('Iter #' + str(i)) - ret = 0 - ret_list = [] - step_outflows = [] - step_densities = [] - state = self.env.reset() - for j in range(num_steps): - state, reward, done, _ = self.env.step(rl_actions(state)) - vel[j] = np.mean(self.env.k.vehicle.get_speed( - self.env.k.vehicle.get_ids())) - ret += reward - ret_list.append(reward) - - env = self.env - step_outflow = env.k.vehicle.get_outflow_rate(20) - density = self.env.get_bottleneck_density() - - step_outflows.append(step_outflow) - step_densities.append(density) - if done: - break - rets.append(ret) - vels.append(vel) - mean_densities.append(sum(step_densities[100:]) / - (num_steps - 100)) - env = self.env - outflow = env.k.vehicle.get_outflow_rate(10000) - mean_outflows.append(outflow) - mean_rets.append(np.mean(ret_list)) - ret_lists.append(ret_list) - mean_vels.append(np.mean(vel)) - std_vels.append(np.std(vel)) - print('Round {0}, return: {1}'.format(i, ret)) - - info_dict['returns'] = rets - info_dict['velocities'] = vels - info_dict['mean_returns'] = mean_rets - info_dict['per_step_returns'] = ret_lists - info_dict['average_outflow'] = np.mean(mean_outflows) - info_dict['per_rollout_outflows'] = mean_outflows - - info_dict['average_rollout_density_outflow'] = np.mean(mean_densities) - - print('Average, std return: {}, {}'.format( - np.mean(rets), np.std(rets))) - print('Average, std speed: {}, {}'.format( - np.mean(mean_vels), np.std(std_vels))) - self.env.terminate() - - return info_dict - - -def bottleneck_example(flow_rate, horizon, restart_instance=False, - render=None): - """ - Perform a simulation of vehicles on a bottleneck. - - Parameters - ---------- - flow_rate : float - total inflow rate of vehicles into the bottleneck - horizon : int - time horizon - restart_instance: bool, optional - whether to restart the instance upon reset - render: bool, optional - specifies whether to use the gui during execution - - Returns - ------- - exp: flow.core.experiment.Experiment - A non-rl experiment demonstrating the performance of human-driven - vehicles on a bottleneck. - """ - if render is None: - render = False - - sim_params = SumoParams( - sim_step=0.5, - render=render, - overtake_right=False, - restart_instance=restart_instance) - - vehicles = VehicleParams() - - vehicles.add( - veh_id="human", - lane_change_controller=(SimLaneChangeController, {}), - routing_controller=(ContinuousRouter, {}), - car_following_params=SumoCarFollowingParams( - speed_mode=25, - ), - lane_change_params=SumoLaneChangeParams( - lane_change_mode=1621, - ), - num_vehicles=1) - - additional_env_params = { - "target_velocity": 40, - "max_accel": 1, - "max_decel": 1, - "lane_change_duration": 5, - "add_rl_if_exit": False, - "disable_tb": DISABLE_TB, - "disable_ramp_metering": DISABLE_RAMP_METER - } - env_params = EnvParams( - horizon=horizon, additional_params=additional_env_params) - - inflow = InFlows() - inflow.add( - veh_type="human", - edge="1", - vehsPerHour=flow_rate, - departLane="random", - departSpeed=10) - - traffic_lights = TrafficLightParams() - if not DISABLE_TB: - traffic_lights.add(node_id="2") - if not DISABLE_RAMP_METER: - traffic_lights.add(node_id="3") - - additional_net_params = {"scaling": SCALING, "speed_limit": 23} - net_params = NetParams( - inflows=inflow, - additional_params=additional_net_params) - - initial_config = InitialConfig( - spacing="random", - min_gap=5, - lanes_distribution=float("inf"), - edges_distribution=["2", "3", "4", "5"]) - - network = BottleneckNetwork( - name="bay_bridge_toll", - vehicles=vehicles, - net_params=net_params, - initial_config=initial_config, - traffic_lights=traffic_lights) - - env = BottleneckEnv(env_params, sim_params, network) - - return BottleneckDensityExperiment(env) - - -if __name__ == '__main__': - # import the experiment variable - # inflow, number of steps, binary - exp = bottleneck_example(INFLOW, 1000, render=True) - exp.run(5, 1000) diff --git a/examples/sumo/figure_eight.py b/examples/sumo/figure_eight.py deleted file mode 100755 index e7cb23626..000000000 --- a/examples/sumo/figure_eight.py +++ /dev/null @@ -1,69 +0,0 @@ -"""Example of a figure 8 network with human-driven vehicles. - -Right-of-way dynamics near the intersection causes vehicles to queue up on -either side of the intersection, leading to a significant reduction in the -average speed of vehicles in the network. -""" -from flow.controllers import IDMController, StaticLaneChanger, ContinuousRouter -from flow.core.experiment import Experiment -from flow.core.params import SumoParams, EnvParams, NetParams, \ - SumoCarFollowingParams -from flow.core.params import VehicleParams -from flow.envs.ring.accel import AccelEnv, ADDITIONAL_ENV_PARAMS -from flow.networks.figure_eight import FigureEightNetwork, ADDITIONAL_NET_PARAMS - - -def figure_eight_example(render=None): - """ - Perform a simulation of vehicles on a figure eight. - - Parameters - ---------- - render: bool, optional - specifies whether to use the gui during execution - - Returns - ------- - exp: flow.core.experiment.Experiment - A non-rl experiment demonstrating the performance of human-driven - vehicles on a figure eight. - """ - sim_params = SumoParams(render=True) - - if render is not None: - sim_params.render = render - - vehicles = VehicleParams() - vehicles.add( - veh_id="idm", - acceleration_controller=(IDMController, {}), - lane_change_controller=(StaticLaneChanger, {}), - routing_controller=(ContinuousRouter, {}), - car_following_params=SumoCarFollowingParams( - speed_mode="obey_safe_speed", - decel=1.5, - ), - initial_speed=0, - num_vehicles=14) - - env_params = EnvParams(additional_params=ADDITIONAL_ENV_PARAMS) - - additional_net_params = ADDITIONAL_NET_PARAMS.copy() - net_params = NetParams(additional_params=additional_net_params) - - network = FigureEightNetwork( - name="FigureEight", - vehicles=vehicles, - net_params=net_params) - - env = AccelEnv(env_params, sim_params, network) - - return Experiment(env) - - -if __name__ == "__main__": - # import the experiment variable - exp = figure_eight_example() - - # run for a set number of rollouts / time steps - exp.run(1, 1500) diff --git a/examples/sumo/highway.py b/examples/sumo/highway.py deleted file mode 100644 index 5a41df32b..000000000 --- a/examples/sumo/highway.py +++ /dev/null @@ -1,81 +0,0 @@ -"""Example of an open multi-lane network with human-driven vehicles.""" - -from flow.controllers import IDMController -from flow.core.experiment import Experiment -from flow.core.params import SumoParams, EnvParams, \ - NetParams, InitialConfig, InFlows -from flow.core.params import VehicleParams -from flow.envs.ring.lane_change_accel import LaneChangeAccelEnv, \ - ADDITIONAL_ENV_PARAMS -from flow.networks.highway import HighwayNetwork, ADDITIONAL_NET_PARAMS - - -def highway_example(render=None): - """ - Perform a simulation of vehicles on a highway. - - Parameters - ---------- - render : bool, optional - specifies whether to use the gui during execution - - Returns - ------- - exp: flow.core.experiment.Experiment - A non-rl experiment demonstrating the performance of human-driven - vehicles on a figure eight. - """ - sim_params = SumoParams(render=True) - - if render is not None: - sim_params.render = render - - vehicles = VehicleParams() - vehicles.add( - veh_id="human", - acceleration_controller=(IDMController, {}), - num_vehicles=20) - vehicles.add( - veh_id="human2", - acceleration_controller=(IDMController, {}), - num_vehicles=20) - - env_params = EnvParams(additional_params=ADDITIONAL_ENV_PARAMS) - - inflow = InFlows() - inflow.add( - veh_type="human", - edge="highway_0", - probability=0.25, - departLane="free", - departSpeed=20) - inflow.add( - veh_type="human2", - edge="highway_0", - probability=0.25, - departLane="free", - departSpeed=20) - - additional_net_params = ADDITIONAL_NET_PARAMS.copy() - net_params = NetParams( - inflows=inflow, additional_params=additional_net_params) - - initial_config = InitialConfig(spacing="uniform", shuffle=True) - - network = HighwayNetwork( - name="highway", - vehicles=vehicles, - net_params=net_params, - initial_config=initial_config) - - env = LaneChangeAccelEnv(env_params, sim_params, network) - - return Experiment(env) - - -if __name__ == "__main__": - # import the experiment variable - exp = highway_example() - - # run for a set number of rollouts / time steps - exp.run(1, 1500) diff --git a/examples/sumo/highway_ramps.py b/examples/sumo/highway_ramps.py deleted file mode 100644 index f6b3ef004..000000000 --- a/examples/sumo/highway_ramps.py +++ /dev/null @@ -1,116 +0,0 @@ -"""Example of a highway section network with on/off ramps.""" - -from flow.core.params import SumoParams, EnvParams, \ - NetParams, InitialConfig, InFlows, SumoCarFollowingParams, \ - SumoLaneChangeParams -from flow.core.params import VehicleParams -from flow.core.experiment import Experiment -from flow.networks.highway_ramps import HighwayRampsNetwork, \ - ADDITIONAL_NET_PARAMS -from flow.envs.ring.accel import AccelEnv, ADDITIONAL_ENV_PARAMS - - -additional_net_params = ADDITIONAL_NET_PARAMS.copy() - -# lengths -additional_net_params["highway_length"] = 1200 -additional_net_params["on_ramps_length"] = 200 -additional_net_params["off_ramps_length"] = 200 - -# number of lanes -additional_net_params["highway_lanes"] = 3 -additional_net_params["on_ramps_lanes"] = 1 -additional_net_params["off_ramps_lanes"] = 1 - -# speed limits -additional_net_params["highway_speed"] = 30 -additional_net_params["on_ramps_speed"] = 20 -additional_net_params["off_ramps_speed"] = 20 - -# ramps -additional_net_params["on_ramps_pos"] = [400] -additional_net_params["off_ramps_pos"] = [800] - -# probability of exiting at the next off-ramp -additional_net_params["next_off_ramp_proba"] = 0.25 - -# inflow rates in vehs/hour -HIGHWAY_INFLOW_RATE = 4000 -ON_RAMPS_INFLOW_RATE = 350 - - -def highway_ramps_example(render=None): - """ - Perform a simulation of vehicles on a highway section with ramps. - - Parameters - ---------- - render: bool, optional - Specifies whether or not to use the GUI during the simulation. - - Returns - ------- - exp: flow.core.experiment.Experiment - A non-RL experiment demonstrating the performance of human-driven - vehicles on a highway section with on and off ramps. - """ - sim_params = SumoParams( - render=True, - emission_path="./data/", - sim_step=0.2, - restart_instance=True) - - if render is not None: - sim_params.render = render - - vehicles = VehicleParams() - vehicles.add( - veh_id="human", - car_following_params=SumoCarFollowingParams( - speed_mode="obey_safe_speed", # for safer behavior at the merges - tau=1.5 # larger distance between cars - ), - lane_change_params=SumoLaneChangeParams(lane_change_mode=1621)) - - env_params = EnvParams( - additional_params=ADDITIONAL_ENV_PARAMS, - sims_per_step=5, - warmup_steps=0) - - inflows = InFlows() - inflows.add( - veh_type="human", - edge="highway_0", - vehs_per_hour=HIGHWAY_INFLOW_RATE, - depart_lane="free", - depart_speed="max", - name="highway_flow") - for i in range(len(additional_net_params["on_ramps_pos"])): - inflows.add( - veh_type="human", - edge="on_ramp_{}".format(i), - vehs_per_hour=ON_RAMPS_INFLOW_RATE, - depart_lane="first", - depart_speed="max", - name="on_ramp_flow") - - net_params = NetParams( - inflows=inflows, - additional_params=additional_net_params) - - initial_config = InitialConfig() # no vehicles initially - - network = HighwayRampsNetwork( - name="highway-ramp", - vehicles=vehicles, - net_params=net_params, - initial_config=initial_config) - - env = AccelEnv(env_params, sim_params, network) - - return Experiment(env) - - -if __name__ == "__main__": - exp = highway_ramps_example() - exp.run(1, 3600, convert_to_csv=True) diff --git a/examples/sumo/merge.py b/examples/sumo/merge.py deleted file mode 100644 index 943662998..000000000 --- a/examples/sumo/merge.py +++ /dev/null @@ -1,100 +0,0 @@ -"""Example of a merge network with human-driven vehicles. - -In the absence of autonomous vehicles, the network exhibits properties of -convective instability, with perturbations propagating upstream from the merge -point before exiting the network. -""" - -from flow.core.params import SumoParams, EnvParams, \ - NetParams, InitialConfig, InFlows, SumoCarFollowingParams -from flow.core.params import VehicleParams -from flow.core.experiment import Experiment -from flow.networks.merge import MergeNetwork, ADDITIONAL_NET_PARAMS -from flow.controllers import IDMController -from flow.envs.merge import MergePOEnv, ADDITIONAL_ENV_PARAMS - -# inflow rate at the highway -FLOW_RATE = 2000 - - -def merge_example(render=None): - """ - Perform a simulation of vehicles on a merge. - - Parameters - ---------- - render: bool, optional - specifies whether to use the gui during execution - - Returns - ------- - exp: flow.core.experiment.Experiment - A non-rl experiment demonstrating the performance of human-driven - vehicles on a merge. - """ - sim_params = SumoParams( - render=True, - emission_path="./data/", - sim_step=0.2, - restart_instance=False) - - if render is not None: - sim_params.render = render - - vehicles = VehicleParams() - vehicles.add( - veh_id="human", - acceleration_controller=(IDMController, { - "noise": 0.2 - }), - car_following_params=SumoCarFollowingParams( - speed_mode="obey_safe_speed", - ), - num_vehicles=5) - - env_params = EnvParams( - additional_params=ADDITIONAL_ENV_PARAMS, - sims_per_step=5, - warmup_steps=0) - - inflow = InFlows() - inflow.add( - veh_type="human", - edge="inflow_highway", - vehs_per_hour=FLOW_RATE, - departLane="free", - departSpeed=10) - inflow.add( - veh_type="human", - edge="inflow_merge", - vehs_per_hour=100, - departLane="free", - departSpeed=7.5) - - additional_net_params = ADDITIONAL_NET_PARAMS.copy() - additional_net_params["merge_lanes"] = 1 - additional_net_params["highway_lanes"] = 1 - additional_net_params["pre_merge_length"] = 500 - net_params = NetParams( - inflows=inflow, - additional_params=additional_net_params) - - initial_config = InitialConfig(spacing="uniform", perturbation=5.0) - - network = MergeNetwork( - name="merge-baseline", - vehicles=vehicles, - net_params=net_params, - initial_config=initial_config) - - env = MergePOEnv(env_params, sim_params, network) - - return Experiment(env) - - -if __name__ == "__main__": - # import the experiment variable - exp = merge_example() - - # run for a set number of rollouts / time steps - exp.run(1, 3600, convert_to_csv=False) diff --git a/examples/sumo/minicity.py b/examples/sumo/minicity.py deleted file mode 100644 index 2f4b72741..000000000 --- a/examples/sumo/minicity.py +++ /dev/null @@ -1,101 +0,0 @@ -"""Example of modified minicity network with human-driven vehicles.""" -from flow.controllers import IDMController -from flow.controllers import RLController -from flow.core.experiment import Experiment -from flow.core.params import SumoParams, EnvParams, NetParams, InitialConfig -from flow.core.params import SumoCarFollowingParams, SumoLaneChangeParams -from flow.core.params import VehicleParams -from flow.envs.ring.accel import AccelEnv, ADDITIONAL_ENV_PARAMS -from flow.networks.minicity import MiniCityNetwork -from flow.controllers.routing_controllers import MinicityRouter -import numpy as np - -np.random.seed(204) - - -def minicity_example(render=None, - save_render=None, - sight_radius=None, - pxpm=None, - show_radius=None): - """Perform a simulation of modified minicity of University of Delaware. - - Parameters - ---------- - render: bool, optional - specifies whether to use the gui during execution - - Returns - ------- - exp: flow.core.experiment.Experiment - A non-rl experiment demonstrating the performance of human-driven - vehicles on the minicity network. - """ - sim_params = SumoParams(sim_step=0.25) - - # update sim_params values if provided as inputs - sim_params.render = render or sim_params.render - sim_params.save_render = save_render or sim_params.save_render - sim_params.sight_radius = sight_radius or sim_params.sight_radius - sim_params.pxpm = pxpm or sim_params.pxpm - sim_params.show_radius = show_radius or sim_params.show_radius - - vehicles = VehicleParams() - vehicles.add( - veh_id="idm", - acceleration_controller=(IDMController, {}), - routing_controller=(MinicityRouter, {}), - car_following_params=SumoCarFollowingParams( - speed_mode=1, - ), - lane_change_params=SumoLaneChangeParams( - lane_change_mode="no_lat_collide", - ), - initial_speed=0, - num_vehicles=90) - vehicles.add( - veh_id="rl", - acceleration_controller=(RLController, {}), - routing_controller=(MinicityRouter, {}), - car_following_params=SumoCarFollowingParams( - speed_mode="obey_safe_speed", - ), - initial_speed=0, - num_vehicles=10) - - env_params = EnvParams(additional_params=ADDITIONAL_ENV_PARAMS) - - net_params = NetParams() - - initial_config = InitialConfig( - spacing="random", - min_gap=5 - ) - network = MiniCityNetwork( - name="minicity", - vehicles=vehicles, - initial_config=initial_config, - net_params=net_params) - - env = AccelEnv(env_params, sim_params, network) - - return Experiment(env) - - -if __name__ == "__main__": - # import the experiment variable - # There are six modes of pyglet rendering: - # No rendering: minicity_example(render=False) - # SUMO-GUI rendering: minicity_example(render=True) - # Static grayscale rendering: minicity_example(render="gray") - # Dynamic grayscale rendering: minicity_example(render="dgray") - # Static RGB rendering: minicity_example(render="rgb") - # Dynamic RGB rendering: minicity_example(render="drgb") - exp = minicity_example(render='drgb', - save_render=False, - sight_radius=30, - pxpm=3, - show_radius=True) - - # run for a set number of rollouts / time steps - exp.run(1, 750) diff --git a/examples/sumo/sugiyama.py b/examples/sumo/sugiyama.py deleted file mode 100755 index a2c03d060..000000000 --- a/examples/sumo/sugiyama.py +++ /dev/null @@ -1,68 +0,0 @@ -"""Used as an example of sugiyama experiment. - -This example consists of 22 IDM cars on a ring creating shockwaves. -""" - -from flow.controllers import IDMController, ContinuousRouter -from flow.core.experiment import Experiment -from flow.core.params import SumoParams, EnvParams, \ - InitialConfig, NetParams, SumoCarFollowingParams -from flow.core.params import VehicleParams -from flow.envs.ring.accel import AccelEnv, ADDITIONAL_ENV_PARAMS -from flow.networks.ring import RingNetwork, ADDITIONAL_NET_PARAMS - - -def sugiyama_example(render=None): - """ - Perform a simulation of vehicles on a ring road. - - Parameters - ---------- - render : bool, optional - specifies whether to use the gui during execution - - Returns - ------- - exp: flow.core.experiment.Experiment - A non-rl experiment demonstrating the performance of human-driven - vehicles on a ring road. - """ - sim_params = SumoParams(sim_step=0.1, render=True) - - if render is not None: - sim_params.render = render - - vehicles = VehicleParams() - vehicles.add( - veh_id="idm", - acceleration_controller=(IDMController, {}), - car_following_params=SumoCarFollowingParams( - min_gap=0 - ), - routing_controller=(ContinuousRouter, {}), - num_vehicles=22) - - env_params = EnvParams(additional_params=ADDITIONAL_ENV_PARAMS) - - additional_net_params = ADDITIONAL_NET_PARAMS.copy() - net_params = NetParams(additional_params=additional_net_params) - - initial_config = InitialConfig(bunching=20) - - network = RingNetwork( - name="sugiyama", - vehicles=vehicles, - net_params=net_params, - initial_config=initial_config) - - env = AccelEnv(env_params, sim_params, network) - - return Experiment(env) - - -if __name__ == "__main__": - # import the experiment variable - exp = sugiyama_example() - - # run for a set number of rollouts / time steps - exp.run(1, 1500) diff --git a/examples/sumo/traffic_light_grid.py b/examples/sumo/traffic_light_grid.py deleted file mode 100644 index 5af8347ea..000000000 --- a/examples/sumo/traffic_light_grid.py +++ /dev/null @@ -1,233 +0,0 @@ -"""Traffic Light Grid example.""" -from flow.controllers import GridRouter -from flow.core.experiment import Experiment -from flow.core.params import SumoParams, EnvParams, InitialConfig, NetParams -from flow.core.params import VehicleParams -from flow.core.params import TrafficLightParams -from flow.core.params import SumoCarFollowingParams -from flow.core.params import InFlows -from flow.envs.ring.accel import AccelEnv, ADDITIONAL_ENV_PARAMS -from flow.networks import TrafficLightGridNetwork - - -def gen_edges(col_num, row_num): - """Generate the names of the outer edges in the traffic light grid network. - - Parameters - ---------- - col_num : int - number of columns in the traffic light grid - row_num : int - number of rows in the traffic light grid - - Returns - ------- - list of str - names of all the outer edges - """ - edges = [] - - # build the left and then the right edges - for i in range(col_num): - edges += ['left' + str(row_num) + '_' + str(i)] - edges += ['right' + '0' + '_' + str(i)] - - # build the bottom and then top edges - for i in range(row_num): - edges += ['bot' + str(i) + '_' + '0'] - edges += ['top' + str(i) + '_' + str(col_num)] - - return edges - - -def get_flow_params(col_num, row_num, additional_net_params): - """Define the network and initial params in the presence of inflows. - - Parameters - ---------- - col_num : int - number of columns in the traffic light grid - row_num : int - number of rows in the traffic light grid - additional_net_params : dict - network-specific parameters that are unique to the traffic light grid - - Returns - ------- - flow.core.params.InitialConfig - parameters specifying the initial configuration of vehicles in the - network - flow.core.params.NetParams - network-specific parameters used to generate the network - """ - initial = InitialConfig( - spacing='custom', lanes_distribution=float('inf'), shuffle=True) - - inflow = InFlows() - outer_edges = gen_edges(col_num, row_num) - for i in range(len(outer_edges)): - inflow.add( - veh_type='human', - edge=outer_edges[i], - probability=0.25, - departLane='free', - departSpeed=20) - - net = NetParams( - inflows=inflow, - additional_params=additional_net_params) - - return initial, net - - -def get_non_flow_params(enter_speed, add_net_params): - """Define the network and initial params in the absence of inflows. - - Note that when a vehicle leaves a network in this case, it is immediately - returns to the start of the row/column it was traversing, and in the same - direction as it was before. - - Parameters - ---------- - enter_speed : float - initial speed of vehicles as they enter the network. - add_net_params: dict - additional network-specific parameters (unique to the traffic light grid) - - Returns - ------- - flow.core.params.InitialConfig - parameters specifying the initial configuration of vehicles in the - network - flow.core.params.NetParams - network-specific parameters used to generate the network - """ - additional_init_params = {'enter_speed': enter_speed} - initial = InitialConfig( - spacing='custom', additional_params=additional_init_params) - net = NetParams(additional_params=add_net_params) - - return initial, net - - -def traffic_light_grid_example(render=None, use_inflows=False): - """ - Perform a simulation of vehicles on a traffic light grid. - - Parameters - ---------- - render: bool, optional - specifies whether to use the gui during execution - use_inflows : bool, optional - set to True if you would like to run the experiment with inflows of - vehicles from the edges, and False otherwise - - Returns - ------- - exp: flow.core.experiment.Experiment - A non-rl experiment demonstrating the performance of human-driven - vehicles and balanced traffic lights on a traffic light grid. - """ - v_enter = 10 - inner_length = 300 - long_length = 500 - short_length = 300 - n_rows = 2 - n_columns = 3 - num_cars_left = 20 - num_cars_right = 20 - num_cars_top = 20 - num_cars_bot = 20 - tot_cars = (num_cars_left + num_cars_right) * n_columns \ - + (num_cars_top + num_cars_bot) * n_rows - - grid_array = { - "short_length": short_length, - "inner_length": inner_length, - "long_length": long_length, - "row_num": n_rows, - "col_num": n_columns, - "cars_left": num_cars_left, - "cars_right": num_cars_right, - "cars_top": num_cars_top, - "cars_bot": num_cars_bot - } - - sim_params = SumoParams(sim_step=0.1, render=True) - - if render is not None: - sim_params.render = render - - vehicles = VehicleParams() - vehicles.add( - veh_id="human", - routing_controller=(GridRouter, {}), - car_following_params=SumoCarFollowingParams( - min_gap=2.5, - decel=7.5, # avoid collisions at emergency stops - ), - num_vehicles=tot_cars) - - env_params = EnvParams(additional_params=ADDITIONAL_ENV_PARAMS) - - tl_logic = TrafficLightParams(baseline=False) - phases = [{ - "duration": "31", - "minDur": "8", - "maxDur": "45", - "state": "GrGrGrGrGrGr" - }, { - "duration": "6", - "minDur": "3", - "maxDur": "6", - "state": "yryryryryryr" - }, { - "duration": "31", - "minDur": "8", - "maxDur": "45", - "state": "rGrGrGrGrGrG" - }, { - "duration": "6", - "minDur": "3", - "maxDur": "6", - "state": "ryryryryryry" - }] - tl_logic.add("center0", phases=phases, programID=1) - tl_logic.add("center1", phases=phases, programID=1) - tl_logic.add("center2", phases=phases, programID=1, tls_type="actuated") - - additional_net_params = { - "grid_array": grid_array, - "speed_limit": 35, - "horizontal_lanes": 1, - "vertical_lanes": 1 - } - - if use_inflows: - initial_config, net_params = get_flow_params( - col_num=n_columns, - row_num=n_rows, - additional_net_params=additional_net_params) - else: - initial_config, net_params = get_non_flow_params( - enter_speed=v_enter, - add_net_params=additional_net_params) - - network = TrafficLightGridNetwork( - name="grid-intersection", - vehicles=vehicles, - net_params=net_params, - initial_config=initial_config, - traffic_lights=tl_logic) - - env = AccelEnv(env_params, sim_params, network) - - return Experiment(env) - - -if __name__ == "__main__": - # import the experiment variable - exp = traffic_light_grid_example() - - # run for a set number of rollouts / time steps - exp.run(1, 1500) diff --git a/examples/train_rllib.py b/examples/train_rllib.py new file mode 100644 index 000000000..4f370c99c --- /dev/null +++ b/examples/train_rllib.py @@ -0,0 +1,157 @@ +"""Runner script for single and multi-agent RLlib experiments. + +This script performs an RL experiment using the PPO algorithm. Choice of +hyperparameters can be seen and adjusted from the code below. + +Usage + python train_rllib.py EXP_CONFIG +""" +import sys +import json +import argparse +import ray +from ray import tune +from ray.tune import run_experiments +from ray.tune.registry import register_env +from flow.utils.rllib import FlowParamsEncoder +from flow.utils.registry import make_create_env +try: + from ray.rllib.agents.agent import get_agent_class +except ImportError: + from ray.rllib.agents.registry import get_agent_class +from copy import deepcopy + + +def parse_args(args): + """Parse training options user can specify in command line. + + Returns + ------- + argparse.Namespace + the output parser object + """ + parser = argparse.ArgumentParser( + formatter_class=argparse.RawDescriptionHelpFormatter, + description="Parse argument used when running a Flow simulation.", + epilog="python train_rllib.py EXP_CONFIG") + + # required input parameters + parser.add_argument( + 'exp_config', type=str, + help='Name of the experiment configuration file, as located in ' + 'exp_configs/rl/singleagent' or 'exp_configs/rl/multiagent.') + + return parser.parse_known_args(args)[0] + + +def setup_exps(flow_params, + n_cpus, + n_rollouts, + policy_graphs=None, + policy_mapping_fn=None, + policies_to_train=None): + """Return the relevant components of an RLlib experiment. + + Parameters + ---------- + flow_params : dict + flow-specific parameters (see flow/utils/registry.py) + n_cpus : int + number of CPUs to run the experiment over + n_rollouts : int + number of rollouts per training iteration + policy_graphs : dict, optional + TODO + policy_mapping_fn : function, optional + TODO + policies_to_train : list of str, optional + TODO + + Returns + ------- + str + name of the training algorithm + str + name of the gym environment to be trained + dict + training configuration parameters + """ + horizon = flow_params['env'].horizon + + alg_run = "PPO" + + agent_cls = get_agent_class(alg_run) + config = deepcopy(agent_cls._default_config) + + config["num_workers"] = n_cpus + config["train_batch_size"] = horizon * n_rollouts + config["gamma"] = 0.999 # discount rate + config["model"].update({"fcnet_hiddens": [32, 32, 32]}) + config["use_gae"] = True + config["lambda"] = 0.97 + config["kl_target"] = 0.02 + config["num_sgd_iter"] = 10 + config['clip_actions'] = False # FIXME(ev) temporary ray bug + config["horizon"] = horizon + + # save the flow params for replay + flow_json = json.dumps( + flow_params, cls=FlowParamsEncoder, sort_keys=True, indent=4) + config['env_config']['flow_params'] = flow_json + config['env_config']['run'] = alg_run + + # multiagent configuration + if policy_graphs is not None: + print("policy_graphs", policy_graphs) + config['multiagent'].update({'policies': policy_graphs}) + if policy_mapping_fn is not None: + config['multiagent'].update({'policy_mapping_fn': tune.function(policy_mapping_fn)}) + if policies_to_train is not None: + config['multiagent'].update({'policies_to_train': policies_to_train}) + + create_env, gym_name = make_create_env(params=flow_params) + + # Register as rllib env + register_env(gym_name, create_env) + return alg_run, gym_name, config + + +if __name__ == "__main__": + flags = parse_args(sys.argv[1:]) + + # import relevant information from the exp_config script + module = __import__("exp_configs.rl.singleagent", fromlist=[flags.exp_config]) + module_ma = __import__("exp_configs.rl.multiagent", fromlist=[flags.exp_config]) + if hasattr(module, flags.exp_config): + submodule = getattr(module, flags.exp_config) + elif hasattr(module_ma, flags.exp_config): + submodule = getattr(module_ma, flags.exp_config) + else: + assert False, "Unable to find experiment config!" + flow_params = submodule.flow_params + n_cpus = submodule.N_CPUS + n_rollouts = submodule.N_ROLLOUTS + policy_graphs = getattr(submodule, "POLICY_GRAPHS", None) + policy_mapping_fn = getattr(submodule, "policy_mapping_fn", None) + policies_to_train = getattr(submodule, "policies_to_train", None) + + alg_run, gym_name, config = setup_exps( + flow_params, n_cpus, n_rollouts, + policy_graphs, policy_mapping_fn, policies_to_train) + + ray.init(num_cpus=n_cpus + 1) + trials = run_experiments({ + flow_params["exp_tag"]: { + "run": alg_run, + "env": gym_name, + "config": { + **config + }, + "checkpoint_freq": 20, + "checkpoint_at_end": True, + "max_failures": 999, + "stop": { + "training_iteration": 200, + }, + } + }) diff --git a/examples/train_stable_baselines.py b/examples/train_stable_baselines.py new file mode 100644 index 000000000..37cc42062 --- /dev/null +++ b/examples/train_stable_baselines.py @@ -0,0 +1,126 @@ +"""Runner script for implementing stable-baseline experiments with Flow. + +Usage + python train_stable_baselines.py EXP_CONFIG +""" +import argparse +import json +import os +import sys +from time import strftime + +from stable_baselines.common.vec_env import DummyVecEnv, SubprocVecEnv +from stable_baselines import PPO2 + +from flow.core.util import ensure_dir +from flow.utils.registry import env_constructor +from flow.utils.rllib import FlowParamsEncoder, get_flow_params + + +def parse_args(args): + """Parse training options user can specify in command line. + + Returns + ------- + argparse.Namespace + the output parser object + """ + parser = argparse.ArgumentParser( + formatter_class=argparse.RawDescriptionHelpFormatter, + description="Parse argument used when running a Flow simulation.", + epilog="python train_stable_baselines.py EXP_CONFIG") + + # required input parameters + parser.add_argument( + 'exp_config', type=str, + help='Name of the experiment configuration file, as located in ' + 'exp_configs/rl/singleagent.') + + # optional input parameters + parser.add_argument( + '--num_cpus', type=int, default=1, + help='How many CPUs to use') + parser.add_argument( + '--num_steps', type=int, default=5000, + help='How many total steps to perform learning over') + parser.add_argument( + '--rollout_size', type=int, default=1000, + help='How many steps are in a training batch.') + + return parser.parse_known_args(args)[0] + + +def run_model(flow_params, num_cpus=1, rollout_size=50, num_steps=50): + """Run the model for num_steps if provided. + + Parameters + ---------- + num_cpus : int + number of CPUs used during training + rollout_size : int + length of a single rollout + num_steps : int + total number of training steps + The total rollout length is rollout_size. + + Returns + ------- + stable_baselines.* + the trained model + """ + if num_cpus == 1: + constructor = env_constructor(params=flow_params, version=0)() + # The algorithms require a vectorized environment to run + env = DummyVecEnv([lambda: constructor]) + else: + env = SubprocVecEnv([env_constructor(params=flow_params, version=i) + for i in range(num_cpus)]) + + train_model = PPO2('MlpPolicy', env, verbose=1, n_steps=rollout_size) + train_model.learn(total_timesteps=num_steps) + return train_model + + +if __name__ == '__main__': + flags = parse_args(sys.argv[1:]) + + # Get the flow_params object. + module = __import__('exp_configs.rl.singleagent', fromlist=[flags.exp_config]) + flow_params = getattr(module, flags.exp_config).flow_params + + # Path to the saved files + exp_tag = flow_params['exp_tag'] + result_name = '{}/{}'.format(exp_tag, strftime("%Y-%m-%d-%H:%M:%S")) + + # Perform training. + print('Beginning training.') + model = run_model(flow_params, flags.num_cpus, flags.rollout_size, flags.num_steps) + + # Save the model to a desired folder and then delete it to demonstrate + # loading. + print('Saving the trained model!') + path = os.path.realpath(os.path.expanduser('~/baseline_results')) + ensure_dir(path) + save_path = os.path.join(path, result_name) + model.save(save_path) + + # dump the flow params + with open(os.path.join(path, result_name) + '.json', 'w') as outfile: + json.dump(flow_params, outfile, + cls=FlowParamsEncoder, sort_keys=True, indent=4) + + # Replay the result by loading the model + print('Loading the trained model and testing it out!') + model = PPO2.load(save_path) + flow_params = get_flow_params(os.path.join(path, result_name) + '.json') + flow_params['sim'].render = True + env_constructor = env_constructor(params=flow_params, version=0)() + # The algorithms require a vectorized environment to run + eval_env = DummyVecEnv([lambda: env_constructor]) + obs = eval_env.reset() + reward = 0 + for _ in range(flow_params['env'].horizon): + action, _states = model.predict(obs) + obs, rewards, dones, info = eval_env.step(action) + reward += rewards + print('the final reward is {}'.format(reward)) diff --git a/flow/benchmarks/baselines/bottleneck0.py b/flow/benchmarks/baselines/bottleneck0.py index 3df9f8350..1c29424fc 100644 --- a/flow/benchmarks/baselines/bottleneck0.py +++ b/flow/benchmarks/baselines/bottleneck0.py @@ -5,12 +5,10 @@ import numpy as np from flow.core.experiment import Experiment -from flow.core.params import InitialConfig from flow.core.params import InFlows from flow.core.params import SumoLaneChangeParams from flow.core.params import SumoCarFollowingParams from flow.core.params import VehicleParams -from flow.core.params import TrafficLightParams from flow.controllers import ContinuousRouter from flow.benchmarks.bottleneck0 import flow_params from flow.benchmarks.bottleneck0 import SCALING @@ -32,12 +30,9 @@ def bottleneck0_baseline(num_runs, render=True): flow.core.experiment.Experiment class needed to run simulations """ - exp_tag = flow_params['exp_tag'] sim_params = flow_params['sim'] env_params = flow_params['env'] net_params = flow_params['net'] - initial_config = flow_params.get('initial', InitialConfig()) - traffic_lights = flow_params.get('tls', TrafficLightParams()) # we want no autonomous vehicles in the simulation vehicles = VehicleParams() @@ -65,27 +60,10 @@ class needed to run simulations # set the evaluation flag to True env_params.evaluate = True - # import the network class - network_class = flow_params['network'] + flow_params['env'].horizon = env_params.horizon + exp = Experiment(flow_params) - # create the network object - network = network_class( - name=exp_tag, - vehicles=vehicles, - net_params=net_params, - initial_config=initial_config, - traffic_lights=traffic_lights - ) - - # import the environment class - env_class = flow_params['env_name'] - - # create the environment object - env = env_class(env_params, sim_params, network) - - exp = Experiment(env) - - results = exp.run(num_runs, env_params.horizon) + results = exp.run(num_runs) return np.mean(results['returns']), np.std(results['returns']) diff --git a/flow/benchmarks/baselines/bottleneck1.py b/flow/benchmarks/baselines/bottleneck1.py index 2ad0e6428..a4aaa6dc3 100644 --- a/flow/benchmarks/baselines/bottleneck1.py +++ b/flow/benchmarks/baselines/bottleneck1.py @@ -5,12 +5,10 @@ import numpy as np from flow.core.experiment import Experiment -from flow.core.params import InitialConfig from flow.core.params import InFlows from flow.core.params import SumoLaneChangeParams from flow.core.params import SumoCarFollowingParams from flow.core.params import VehicleParams -from flow.core.params import TrafficLightParams from flow.controllers import ContinuousRouter from flow.benchmarks.bottleneck1 import flow_params from flow.benchmarks.bottleneck1 import SCALING @@ -32,12 +30,9 @@ def bottleneck1_baseline(num_runs, render=True): flow.core.experiment.Experiment class needed to run simulations """ - exp_tag = flow_params['exp_tag'] sim_params = flow_params['sim'] env_params = flow_params['env'] net_params = flow_params['net'] - initial_config = flow_params.get('initial', InitialConfig()) - traffic_lights = flow_params.get('tls', TrafficLightParams()) # we want no autonomous vehicles in the simulation vehicles = VehicleParams() @@ -65,27 +60,10 @@ class needed to run simulations # set the evaluation flag to True env_params.evaluate = True - # import the network class - network_class = flow_params['network'] + flow_params['env'].horizon = env_params.horizon + exp = Experiment(flow_params) - # create the network object - network = network_class( - name=exp_tag, - vehicles=vehicles, - net_params=net_params, - initial_config=initial_config, - traffic_lights=traffic_lights - ) - - # import the environment class - env_class = flow_params['env_name'] - - # create the environment object - env = env_class(env_params, sim_params, network) - - exp = Experiment(env) - - results = exp.run(num_runs, env_params.horizon) + results = exp.run(num_runs) return np.mean(results['returns']), np.std(results['returns']) diff --git a/flow/benchmarks/baselines/bottleneck2.py b/flow/benchmarks/baselines/bottleneck2.py index c6343a61d..489d256b5 100644 --- a/flow/benchmarks/baselines/bottleneck2.py +++ b/flow/benchmarks/baselines/bottleneck2.py @@ -5,12 +5,10 @@ import numpy as np from flow.core.experiment import Experiment -from flow.core.params import InitialConfig from flow.core.params import InFlows from flow.core.params import SumoLaneChangeParams from flow.core.params import SumoCarFollowingParams from flow.core.params import VehicleParams -from flow.core.params import TrafficLightParams from flow.controllers import ContinuousRouter from flow.benchmarks.bottleneck2 import flow_params from flow.benchmarks.bottleneck2 import SCALING @@ -32,12 +30,9 @@ def bottleneck2_baseline(num_runs, render=True): flow.core.experiment.Experiment class needed to run simulations """ - exp_tag = flow_params['exp_tag'] sim_params = flow_params['sim'] env_params = flow_params['env'] net_params = flow_params['net'] - initial_config = flow_params.get('initial', InitialConfig()) - traffic_lights = flow_params.get('tls', TrafficLightParams()) # we want no autonomous vehicles in the simulation vehicles = VehicleParams() @@ -65,27 +60,10 @@ class needed to run simulations # set the evaluation flag to True env_params.evaluate = True - # import the network class - network_class = flow_params['network'] + flow_params['env'].horizon = env_params.horizon + exp = Experiment(flow_params) - # create the network object - network = network_class( - name=exp_tag, - vehicles=vehicles, - net_params=net_params, - initial_config=initial_config, - traffic_lights=traffic_lights - ) - - # import the environment class - env_class = flow_params['env_name'] - - # create the environment object - env = env_class(env_params, sim_params, network) - - exp = Experiment(env) - - results = exp.run(num_runs, env_params.horizon) + results = exp.run(num_runs) return np.mean(results['returns']), np.std(results['returns']) diff --git a/flow/benchmarks/baselines/figureeight012.py b/flow/benchmarks/baselines/figureeight012.py index 19218c168..9d2cbf3c1 100644 --- a/flow/benchmarks/baselines/figureeight012.py +++ b/flow/benchmarks/baselines/figureeight012.py @@ -5,10 +5,8 @@ import numpy as np from flow.core.experiment import Experiment -from flow.core.params import InitialConfig from flow.core.params import SumoCarFollowingParams from flow.core.params import VehicleParams -from flow.core.params import TrafficLightParams from flow.controllers import IDMController from flow.controllers import ContinuousRouter from flow.benchmarks.figureeight0 import flow_params @@ -30,12 +28,8 @@ def figure_eight_baseline(num_runs, render=True): Experiment class needed to run simulations """ - exp_tag = flow_params['exp_tag'] sim_params = flow_params['sim'] env_params = flow_params['env'] - net_params = flow_params['net'] - initial_config = flow_params.get('initial', InitialConfig()) - traffic_lights = flow_params.get('tls', TrafficLightParams()) # modify the rendering to match what is requested sim_params.render = render @@ -53,27 +47,10 @@ class needed to run simulations ), num_vehicles=14) - # import the network class - network_class = flow_params['network'] + flow_params['env'].horizon = env_params.horizon + exp = Experiment(flow_params) - # create the network object - network = network_class( - name=exp_tag, - vehicles=vehicles, - net_params=net_params, - initial_config=initial_config, - traffic_lights=traffic_lights - ) - - # import the environment class - env_class = flow_params['env_name'] - - # create the environment object - env = env_class(env_params, sim_params, network) - - exp = Experiment(env) - - results = exp.run(num_runs, env_params.horizon) + results = exp.run(num_runs) avg_speed = np.mean(results['mean_returns']) return avg_speed diff --git a/flow/benchmarks/baselines/grid0.py b/flow/benchmarks/baselines/grid0.py index eb27de4c6..05ca393ab 100644 --- a/flow/benchmarks/baselines/grid0.py +++ b/flow/benchmarks/baselines/grid0.py @@ -5,7 +5,6 @@ import numpy as np from flow.core.experiment import Experiment -from flow.core.params import InitialConfig from flow.core.params import TrafficLightParams from flow.benchmarks.grid0 import flow_params from flow.benchmarks.grid0 import N_ROWS @@ -28,12 +27,8 @@ def grid0_baseline(num_runs, render=True): flow.core.experiment.Experiment class needed to run simulations """ - exp_tag = flow_params['exp_tag'] sim_params = flow_params['sim'] - vehicles = flow_params['veh'] env_params = flow_params['env'] - net_params = flow_params['net'] - initial_config = flow_params.get('initial', InitialConfig()) # define the traffic light logic tl_logic = TrafficLightParams(baseline=False) @@ -57,27 +52,10 @@ class needed to run simulations # set the evaluation flag to True env_params.evaluate = True - # import the network class - network_class = flow_params['network'] + flow_params['env'].horizon = env_params.horizon + exp = Experiment(flow_params) - # create the network object - network = network_class( - name=exp_tag, - vehicles=vehicles, - net_params=net_params, - initial_config=initial_config, - traffic_lights=tl_logic - ) - - # import the environment class - env_class = flow_params['env_name'] - - # create the environment object - env = env_class(env_params, sim_params, network) - - exp = Experiment(env) - - results = exp.run(num_runs, env_params.horizon) + results = exp.run(num_runs) total_delay = np.mean(results['returns']) return total_delay diff --git a/flow/benchmarks/baselines/grid1.py b/flow/benchmarks/baselines/grid1.py index 049a6995a..76aea4c7f 100644 --- a/flow/benchmarks/baselines/grid1.py +++ b/flow/benchmarks/baselines/grid1.py @@ -5,7 +5,6 @@ import numpy as np from flow.core.experiment import Experiment -from flow.core.params import InitialConfig from flow.core.params import TrafficLightParams from flow.benchmarks.grid1 import flow_params from flow.benchmarks.grid1 import N_ROWS @@ -28,12 +27,8 @@ def grid1_baseline(num_runs, render=True): flow.core.experiment.Experiment class needed to run simulations """ - exp_tag = flow_params['exp_tag'] sim_params = flow_params['sim'] - vehicles = flow_params['veh'] env_params = flow_params['env'] - net_params = flow_params['net'] - initial_config = flow_params.get('initial', InitialConfig()) # define the traffic light logic tl_logic = TrafficLightParams(baseline=False) @@ -55,27 +50,10 @@ class needed to run simulations # set the evaluation flag to True env_params.evaluate = True - # import the network class - network_class = flow_params['network'] + flow_params['env'].horizon = env_params.horizon + exp = Experiment(flow_params) - # create the network object - network = network_class( - name=exp_tag, - vehicles=vehicles, - net_params=net_params, - initial_config=initial_config, - traffic_lights=tl_logic - ) - - # import the environment class - env_class = flow_params['env_name'] - - # create the environment object - env = env_class(env_params, sim_params, network) - - exp = Experiment(env) - - results = exp.run(num_runs, env_params.horizon) + results = exp.run(num_runs) total_delay = np.mean(results['returns']) return total_delay diff --git a/flow/benchmarks/baselines/merge012.py b/flow/benchmarks/baselines/merge012.py index 5bd4a3a39..6ed1b0b15 100644 --- a/flow/benchmarks/baselines/merge012.py +++ b/flow/benchmarks/baselines/merge012.py @@ -5,8 +5,6 @@ import numpy as np from flow.core.experiment import Experiment -from flow.core.params import InitialConfig -from flow.core.params import TrafficLightParams from flow.benchmarks.merge0 import flow_params @@ -26,13 +24,8 @@ def merge_baseline(num_runs, render=True): flow.core.experiment.Experiment class needed to run simulations """ - exp_tag = flow_params['exp_tag'] sim_params = flow_params['sim'] - vehicles = flow_params['veh'] env_params = flow_params['env'] - net_params = flow_params['net'] - initial_config = flow_params.get('initial', InitialConfig()) - traffic_lights = flow_params.get('tls', TrafficLightParams()) # modify the rendering to match what is requested sim_params.render = render @@ -40,27 +33,10 @@ class needed to run simulations # set the evaluation flag to True env_params.evaluate = True - # import the network class - network_class = flow_params['network'] + flow_params['env'].horizon = env_params.horizon + exp = Experiment(flow_params) - # create the network object - network = network_class( - name=exp_tag, - vehicles=vehicles, - net_params=net_params, - initial_config=initial_config, - traffic_lights=traffic_lights - ) - - # import the environment class - env_class = flow_params['env_name'] - - # create the environment object - env = env_class(env_params, sim_params, network) - - exp = Experiment(env) - - results = exp.run(num_runs, env_params.horizon) + results = exp.run(num_runs) avg_speed = np.mean(results['mean_returns']) return avg_speed diff --git a/flow/core/experiment.py b/flow/core/experiment.py index 2d53e9463..53547d851 100755 --- a/flow/core/experiment.py +++ b/flow/core/experiment.py @@ -7,6 +7,7 @@ import os from flow.core.util import emission_to_csv +from flow.utils.registry import make_create_env class Experiment: @@ -18,15 +19,15 @@ class Experiment: the actions of RL agents in the network, type the following: >>> from flow.envs import Env - >>> env = Env(...) - >>> exp = Experiment(env) # for some env - >>> exp.run(num_runs=1, num_steps=1000) + >>> flow_params = dict(...) # see the examples in exp_config + >>> exp = Experiment(flow_params) # for some experiment configuration + >>> exp.run(num_runs=1) If you wish to specify the actions of RL agents in the network, this may be done as follows: >>> rl_actions = lambda state: 0 # replace with something appropriate - >>> exp.run(num_runs=1, num_steps=1000, rl_actions=rl_actions) + >>> exp.run(num_runs=1, rl_actions=rl_actions) Finally, if you would like to like to plot and visualize your results, this class can generate csv files from emission files produced by sumo. These @@ -37,12 +38,12 @@ class can generate csv files from emission files produced by sumo. These ``emission_path`` attribute in ``SimParams`` to some path. >>> from flow.core.params import SimParams - >>> sim_params = SimParams(emission_path="./data") + >>> flow_params['sim'] = SimParams(emission_path="./data") Once you have included this in your environment, run your Experiment object as follows: - >>> exp.run(num_runs=1, num_steps=1000, convert_to_csv=True) + >>> exp.run(num_runs=1, convert_to_csv=True) After the experiment is complete, look at the "./data" directory. There will be two files, one with the suffix .xml and another with the suffix @@ -55,24 +56,26 @@ class can generate csv files from emission files produced by sumo. These the environment object the simulator will run """ - def __init__(self, env): + def __init__(self, flow_params): """Instantiate Experiment.""" - self.env = env + # Get the env name and a creator for the environment. + create_env, _ = make_create_env(flow_params) + + # Create the environment. + self.env = create_env() logging.info(" Starting experiment {} at {}".format( - env.network.name, str(datetime.datetime.utcnow()))) + self.env.network.name, str(datetime.datetime.utcnow()))) logging.info("Initializing environment.") - def run(self, num_runs, num_steps, rl_actions=None, convert_to_csv=False): - """Run the given network for a set number of runs and steps per run. + def run(self, num_runs, rl_actions=None, convert_to_csv=False): + """Run the given network for a set number of runs. Parameters ---------- num_runs : int number of runs the experiment should perform - num_steps : int - number of steps to be performs in each run of the experiment rl_actions : method, optional maps states to actions to be performed by the RL agents (if there are any) @@ -85,6 +88,8 @@ def run(self, num_runs, num_steps, rl_actions=None, convert_to_csv=False): info_dict : dict contains returns, average speed per step """ + num_steps = self.env.env_params.horizon + # raise an error if convert_to_csv is set to True but no emission # file will be generated, to avoid getting an error at the end of the # simulation diff --git a/flow/envs/base.py b/flow/envs/base.py old mode 100755 new mode 100644 diff --git a/flow/multiagent_envs/__iniy__.py b/flow/multiagent_envs/__init__.py similarity index 100% rename from flow/multiagent_envs/__iniy__.py rename to flow/multiagent_envs/__init__.py diff --git a/flow/multiagent_envs/loop/loop_accel.py b/flow/multiagent_envs/loop/loop_accel.py index f46e3e38d..918e8542b 100644 --- a/flow/multiagent_envs/loop/loop_accel.py +++ b/flow/multiagent_envs/loop/loop_accel.py @@ -1,6 +1,6 @@ """Pending deprecation file. -To view the actual content, go to: flow/envs/multiagent/traffic_light_grid.py +To view the actual content, go to: flow/envs/multiagent/ring/accel.py """ from flow.utils.flow_warnings import deprecated from flow.envs.multiagent.ring.accel import MultiAgentAccelEnv as MAAEnv diff --git a/flow/utils/registry.py b/flow/utils/registry.py index 5670eb224..3f6c9dad5 100644 --- a/flow/utils/registry.py +++ b/flow/utils/registry.py @@ -63,9 +63,16 @@ def make_create_env(params, version=0, render=None): if isinstance(params["env_name"], str): print("""Passing of strings for env_name will be deprecated. Please pass the Env instance instead.""") - env_name = params["env_name"] + '-v{}'.format(version) + base_env_name = params["env_name"] else: - env_name = params["env_name"].__name__ + '-v{}'.format(version) + base_env_name = params["env_name"].__name__ + + # deal with multiple environments being created under the same name + all_envs = gym.envs.registry.all() + env_ids = [env_spec.id for env_spec in all_envs] + while "{}-v{}".format(base_env_name, version) in env_ids: + version += 1 + env_name = "{}-v{}".format(base_env_name, version) if isinstance(params["network"], str): print("""Passing of strings for network will be deprecated. @@ -109,18 +116,17 @@ def create_env(*_): else: entry_point = params["env_name"].__module__ + ':' + params["env_name"].__name__ - try: - register( - id=env_name, - entry_point=entry_point, - kwargs={ - "env_params": env_params, - "sim_params": sim_params, - "network": network, - "simulator": params['simulator'] - }) - except Exception: - pass + # register the environment with OpenAI gym + register( + id=env_name, + entry_point=entry_point, + kwargs={ + "env_params": env_params, + "sim_params": sim_params, + "network": network, + "simulator": params['simulator'] + }) + return gym.envs.make(env_name) return create_env, env_name diff --git a/flow/visualize/capacity_diagram_generator.py b/flow/visualize/capacity_diagram_generator.py index d4bc2e9a2..566d7e428 100644 --- a/flow/visualize/capacity_diagram_generator.py +++ b/flow/visualize/capacity_diagram_generator.py @@ -1,7 +1,7 @@ """Generates capacity diagrams for the bottleneck. This method accepts as input a csv file containing the inflows and outflows -from several simulations as created by the file `examples/sumo/density_exp.py`, +from several simulations as created by the file `examples/exp_scripts/bottleneck_density_sweep_capacity_diagram.py`, e.g. 1000, 978 diff --git a/tests/fast_tests/test_collisions.py b/tests/fast_tests/test_collisions.py index dbb9fdcca..ec0e41f9a 100644 --- a/tests/fast_tests/test_collisions.py +++ b/tests/fast_tests/test_collisions.py @@ -50,27 +50,27 @@ def test_collide(self): net_params = NetParams(additional_params=additional_net_params) - env, _ = traffic_light_grid_mxn_exp_setup( + env, _, flow_params = traffic_light_grid_mxn_exp_setup( row_num=1, col_num=1, sim_params=sim_params, vehicles=vehicles, net_params=net_params) + # create an experiment object + exp = Experiment(flow_params) + env = exp.env # go through the env and set all the lights to green for i in range(env.rows * env.cols): env.k.traffic_light.set_state( node_id='center' + str(i), state="gggggggggggg") - # instantiate an experiment class - exp = Experiment(env) - - exp.run(50, 50) + exp.run(50) def test_collide_inflows(self): """Tests collisions in the presence of inflows.""" # create the environment and network classes for a ring road - sim_params = SumoParams(sim_step=1, render=False) + sim_params = SumoParams(sim_step=1, render=False, seed=40) total_vehicles = 0 vehicles = VehicleParams() vehicles.add( @@ -111,22 +111,23 @@ def test_collide_inflows(self): inflows=inflows, additional_params=additional_net_params) - env, _ = traffic_light_grid_mxn_exp_setup( + _, _, flow_params = traffic_light_grid_mxn_exp_setup( row_num=1, col_num=1, sim_params=sim_params, vehicles=vehicles, net_params=net_params) + # instantiate an experiment class + exp = Experiment(flow_params) + env = exp.env + # go through the env and set all the lights to green for i in range(env.rows * env.cols): env.k.traffic_light.set_state( node_id='center' + str(i), state="gggggggggggg") - # instantiate an experiment class - exp = Experiment(env) - - exp.run(50, 50) + exp.run(50) if __name__ == '__main__': diff --git a/tests/fast_tests/test_controllers.py b/tests/fast_tests/test_controllers.py index 32ec693da..1e5c6057d 100644 --- a/tests/fast_tests/test_controllers.py +++ b/tests/fast_tests/test_controllers.py @@ -45,7 +45,7 @@ def setUp(self): num_vehicles=5) # create the environment and network classes for a ring road - self.env, _ = ring_road_exp_setup(vehicles=vehicles) + self.env, _, _ = ring_road_exp_setup(vehicles=vehicles) def tearDown(self): # terminate the traci instance @@ -94,7 +94,7 @@ def setUp(self): num_vehicles=5) # create the environment and network classes for a ring road - self.env, _ = ring_road_exp_setup(vehicles=vehicles) + self.env, _, _ = ring_road_exp_setup(vehicles=vehicles) def tearDown(self): # terminate the traci instance @@ -149,7 +149,7 @@ def setUp(self): num_vehicles=5) # create the environment and network classes for a ring road - self.env, _ = ring_road_exp_setup(vehicles=vehicles) + self.env, _, _ = ring_road_exp_setup(vehicles=vehicles) def tearDown(self): # terminate the traci instance @@ -199,7 +199,7 @@ def setUp(self): num_vehicles=5) # create the environment and network classes for a ring road - self.env, _ = ring_road_exp_setup(vehicles=vehicles) + self.env, _, _ = ring_road_exp_setup(vehicles=vehicles) def tearDown(self): # terminate the traci instance @@ -246,7 +246,7 @@ def setUp(self): num_vehicles=5) # create the environment and network classes for a ring road - self.env, _ = ring_road_exp_setup(vehicles=vehicles) + self.env, _, _ = ring_road_exp_setup(vehicles=vehicles) def tearDown(self): # terminate the traci instance @@ -312,14 +312,15 @@ def setUp_failsafe(self, vehicles): initial_config = InitialConfig(bunching=10) # create the environment and network classes for a ring road - env, _ = ring_road_exp_setup( + _, _, flow_params = ring_road_exp_setup( vehicles=vehicles, env_params=env_params, net_params=net_params, initial_config=initial_config) + flow_params['env'].horizon = 200 # instantiate an experiment class - self.exp = Experiment(env) + self.exp = Experiment(flow_params) def tearDown_failsafe(self): # free data used by the class @@ -339,7 +340,7 @@ def test_no_crash_OVM(self): self.setUp_failsafe(vehicles=vehicles) # run the experiment, see if it fails - self.exp.run(1, 200) + self.exp.run(1) self.tearDown_failsafe() @@ -356,7 +357,7 @@ def test_no_crash_LinearOVM(self): self.setUp_failsafe(vehicles=vehicles) # run the experiment, see if it fails - self.exp.run(1, 200) + self.exp.run(1) self.tearDown_failsafe() @@ -381,7 +382,7 @@ def test_no_crash_OVM(self): self.setUp_failsafe(vehicles=vehicles) # run the experiment, see if it fails - self.exp.run(1, 200) + self.exp.run(1) self.tearDown_failsafe() @@ -399,7 +400,7 @@ def test_no_crash_LinearOVM(self): self.setUp_failsafe(vehicles=vehicles) # run the experiment, see if it fails - self.exp.run(1, 200) + self.exp.run(1) self.tearDown_failsafe() @@ -421,7 +422,7 @@ def setUp(self): net_params = NetParams(additional_params=additional_net_params) # create the environment and network classes for a ring road - self.env, _ = ring_road_exp_setup(net_params=net_params) + self.env, _, _ = ring_road_exp_setup(net_params=net_params) def tearDown(self): # terminate the traci instance @@ -479,7 +480,7 @@ def setUp(self): num_vehicles=5) # create the environment and network classes for a ring road - self.env, _ = ring_road_exp_setup(vehicles=vehicles) + self.env, _, _ = ring_road_exp_setup(vehicles=vehicles) def tearDown(self): # terminate the traci instance @@ -554,7 +555,7 @@ def setUp(self): num_vehicles=5) # create the environment and network classes for a ring road - self.env, _ = ring_road_exp_setup(vehicles=vehicles) + self.env, _, _ = ring_road_exp_setup(vehicles=vehicles) def tearDown(self): # terminate the traci instance @@ -609,7 +610,7 @@ def setUp(self): num_vehicles=5) # create the environment and network classes for a ring road - self.env, _ = ring_road_exp_setup(vehicles=vehicles) + self.env, _, _ = ring_road_exp_setup(vehicles=vehicles) def tearDown(self): # terminate the traci instance @@ -665,7 +666,7 @@ def setUp(self): num_vehicles=5) # create the environment and network classes for a ring road - self.env, _ = ring_road_exp_setup(vehicles=vehicles) + self.env, _, _ = ring_road_exp_setup(vehicles=vehicles) def tearDown(self): # terminate the traci instance diff --git a/tests/fast_tests/test_environment_base_class.py b/tests/fast_tests/test_environment_base_class.py index b2e529b3a..b5c6cbc17 100644 --- a/tests/fast_tests/test_environment_base_class.py +++ b/tests/fast_tests/test_environment_base_class.py @@ -47,7 +47,7 @@ def setUp(self): initial_config = InitialConfig(x0=5, shuffle=True) # create the environment and network classes for a ring road - self.env, _ = ring_road_exp_setup( + self.env, _, _ = ring_road_exp_setup( env_params=env_params, initial_config=initial_config, vehicles=vehicles) @@ -87,7 +87,7 @@ def setUp(self): sim_params = SumoParams() # create the environment and network classes for a ring road - self.env, _ = ring_road_exp_setup(sim_params=sim_params) + self.env, _, _ = ring_road_exp_setup(sim_params=sim_params) def tearDown(self): # terminate the traci instance @@ -133,7 +133,7 @@ def setUp(self): num_vehicles=5) # create the environment and network classes for a ring road - self.env, _ = ring_road_exp_setup( + self.env, _, _ = ring_road_exp_setup( net_params=net_params, env_params=env_params, vehicles=vehicles) def tearDown(self): @@ -264,7 +264,7 @@ def test_it_works(self): # than one env_params = EnvParams( warmup_steps=warmup_step, additional_params=ADDITIONAL_ENV_PARAMS) - env, _ = ring_road_exp_setup(env_params=env_params) + env, _, _ = ring_road_exp_setup(env_params=env_params) # time before running a reset t1 = 0 @@ -289,7 +289,7 @@ def test_it_works(self): env_params = EnvParams( sims_per_step=sims_per_step, additional_params=ADDITIONAL_ENV_PARAMS) - env, _ = ring_road_exp_setup(env_params=env_params) + env, _, _ = ring_road_exp_setup(env_params=env_params) env.reset() # time before running a step @@ -311,7 +311,7 @@ class TestAbstractMethods(unittest.TestCase): """ def setUp(self): - env, network = ring_road_exp_setup() + env, network, _ = ring_road_exp_setup() sim_params = SumoParams() # FIXME: make ambiguous env_params = EnvParams() self.env = Env(sim_params=sim_params, @@ -343,7 +343,7 @@ def test_all(self): # add an RL vehicle to ensure that its color will be distinct vehicles.add("rl", acceleration_controller=(RLController, {}), num_vehicles=1) - _, network = ring_road_exp_setup(vehicles=vehicles) + _, network, _ = ring_road_exp_setup(vehicles=vehicles) env = TestEnv(EnvParams(), SumoParams(), network) env.reset() @@ -421,7 +421,7 @@ class TestClipBoxActions(unittest.TestCase): """ def setUp(self): - env, network = ring_road_exp_setup() + env, network, _ = ring_road_exp_setup() sim_params = SumoParams() env_params = EnvParams() self.env = BoxEnv( @@ -469,13 +469,13 @@ class TestClipTupleActions(unittest.TestCase): """ def setUp(self): - env, scenario = ring_road_exp_setup() + env, network, _ = ring_road_exp_setup() sim_params = SumoParams() env_params = EnvParams() self.env = TupleEnv( sim_params=sim_params, env_params=env_params, - scenario=scenario) + network=network) def tearDown(self): self.env.terminate() diff --git a/tests/fast_tests/test_examples.py b/tests/fast_tests/test_examples.py index be9dcc9bf..2de1c685d 100644 --- a/tests/fast_tests/test_examples.py +++ b/tests/fast_tests/test_examples.py @@ -1,180 +1,148 @@ import os import unittest +import random import ray from ray.tune import run_experiments -from examples.rllib.figure_eight import setup_exps as figure_eight_setup -from examples.rllib.traffic_light_grid import setup_exps as traffic_light_grid_setup -from examples.rllib.stabilizing_highway import setup_exps as highway_setup -from examples.rllib.stabilizing_the_ring import setup_exps as ring_setup -from examples.rllib.velocity_bottleneck import setup_exps as bottleneck_setup -from examples.rllib.multiagent_exps.multiagent_figure_eight \ - import setup_exps as multi_figure_eight_setup -from examples.rllib.multiagent_exps.multiagent_stabilizing_the_ring \ - import setup_exps as multi_ring_setup -from examples.rllib.multiagent_exps.multiagent_traffic_light_grid \ - import setup_exps_PPO as multi_grid_setup -from examples.rllib.multiagent_exps.multiagent_traffic_light_grid \ - import make_flow_params as multi_grid_setup_flow_params -from examples.rllib.multiagent_exps.multiagent_highway import flow_params \ - as multi_highway_flow_params -from examples.rllib.multiagent_exps.multiagent_highway import setup_exps \ - as multi_highway_setup - -from examples.stable_baselines.figure_eight import run_model as run_figure_eight -from examples.stable_baselines.traffic_light_grid import run_model as run_traffic_light_grid -from examples.stable_baselines.stabilizing_highway import run_model as run_stabilizing_highway -from examples.stable_baselines.stabilizing_the_ring import run_model as run_stabilizing_ring -from examples.stable_baselines.velocity_bottleneck import run_model as run_velocity_bottleneck - -from examples.sumo.bay_bridge import bay_bridge_example -from examples.sumo.bay_bridge_toll import bay_bridge_toll_example -from examples.sumo.bottlenecks import bottleneck_example -from examples.sumo.density_exp import run_bottleneck -from examples.sumo.figure_eight import figure_eight_example -from examples.sumo.traffic_light_grid import traffic_light_grid_example -from examples.sumo.highway import highway_example -from examples.sumo.highway_ramps import highway_ramps_example -from examples.sumo.merge import merge_example -from examples.sumo.minicity import minicity_example -from examples.sumo.sugiyama import sugiyama_example +from flow.core.experiment import Experiment + +from examples.exp_configs.rl.singleagent.singleagent_figure_eight import flow_params as singleagent_figure_eight +# from examples.exp_configs.rl.singleagent.green_wave import flow_params as singleagent_green_wave +from examples.exp_configs.rl.singleagent.singleagent_merge import flow_params as singleagent_merge +from examples.exp_configs.rl.singleagent.singleagent_ring import flow_params as singleagent_ring +from examples.exp_configs.rl.singleagent.singleagent_bottleneck import flow_params as singleagent_bottleneck + +from examples.exp_configs.rl.multiagent.multiagent_figure_eight import flow_params as multiagent_figure_eight +from examples.exp_configs.rl.multiagent.multiagent_ring import \ + flow_params as multiagent_ring +# from examples.exp_configs.rl.multiagent.multiagent_traffic_light_grid import setup_exps_PPO as multi_grid_setup +# from examples.exp_configs.rl.multiagent.multiagent_traffic_light_grid import \ +# make_flow_params as multi_grid_setup_flow_params +from examples.exp_configs.rl.multiagent.multiagent_highway import flow_params as multiagent_highway + +from examples.train_stable_baselines import run_model as run_stable_baselines_model +from examples.train_rllib import setup_exps as setup_rllib_exps + +from examples.exp_configs.non_rl.bay_bridge import flow_params as non_rl_bay_bridge +from examples.exp_configs.non_rl.bay_bridge_toll import flow_params as non_rl_bay_bridge_toll +from examples.exp_configs.non_rl.bottleneck import flow_params as non_rl_bottleneck +from examples.exp_configs.non_rl.figure_eight import flow_params as non_rl_figure_eight +from examples.exp_configs.non_rl.traffic_light_grid import flow_params as non_rl_traffic_light_grid +from examples.exp_configs.non_rl.highway import flow_params as non_rl_highway +from examples.exp_configs.non_rl.highway_ramps import flow_params as non_rl_highway_ramps +from examples.exp_configs.non_rl.merge import flow_params as non_rl_merge +from examples.exp_configs.non_rl.minicity import flow_params as non_rl_minicity +from examples.exp_configs.non_rl.ring import flow_params as non_rl_ring os.environ['TEST_FLAG'] = 'True' os.environ['KMP_DUPLICATE_LIB_OK'] = 'True' +# This removes the randomness in this test +random.seed(a=10) -class TestSumoExamples(unittest.TestCase): - """Tests the example scripts in examples/sumo. - This is done by running the experiment function within each script for a +class TestNonRLExamples(unittest.TestCase): + """Tests the experiment configurations in examples/exp_configs/non_rl. + + This is done by running an experiment form of each config for a few time steps. Note that, this does not test for any refactoring changes done to the functions within the experiment class. """ def test_bottleneck(self): - """Verifies that examples/sumo/bottlenecks.py is working.""" - # import the experiment variable from the example - exp = bottleneck_example(20, 5, render=False) - - # run the experiment for a few time steps to ensure it doesn't fail - exp.run(1, 5) + """Verify that examples/exp_configs/non_rl/bottleneck.py is working.""" + self.run_simulation(non_rl_bottleneck) def test_figure_eight(self): - """Verifies that examples/sumo/figure_eight.py is working.""" - # import the experiment variable from the example - exp = figure_eight_example(render=False) - - # run the experiment for a few time steps to ensure it doesn't fail - exp.run(1, 5) + """Verify that examples/exp_configs/non_rl/figure_eight.py is working.""" + self.run_simulation(non_rl_figure_eight) def test_traffic_light_grid(self): - """Verifies that examples/sumo/traffic_light_grid.py is working.""" - # test the example in the absence of inflows - exp = traffic_light_grid_example(render=False, use_inflows=False) - exp.run(1, 5) - - # test the example in the presence of inflows - exp = traffic_light_grid_example(render=False, use_inflows=True) - exp.run(1, 5) + """Verify that examples/exp_configs/non_rl/traffic_light_grid.py is working.""" + self.run_simulation(non_rl_traffic_light_grid) def test_highway(self): - """Verifies that examples/sumo/highway.py is working.""" + """Verify that examples/exp_configs/non_rl/highway.py is working.""" # import the experiment variable from the example - exp = highway_example(render=False) - - # run the experiment for a few time steps to ensure it doesn't fail - exp.run(1, 5) + self.run_simulation(non_rl_highway) def test_highway_ramps(self): - """Verifies that examples/sumo/highway_ramps.py is working.""" - # import the experiment variable from the example - exp = highway_ramps_example(render=False) - - # run the experiment for a few time steps to ensure it doesn't fail - exp.run(1, 5) + """Verify that examples/exp_configs/non_rl/highway_ramps.py is working.""" + self.run_simulation(non_rl_highway_ramps) def test_merge(self): - """Verifies that examples/sumo/merge.py is working.""" - # import the experiment variable from the example - exp = merge_example(render=False) - - # run the experiment for a few time steps to ensure it doesn't fail - exp.run(1, 5) + """Verify that examples/exp_configs/non_rl/merge.py is working.""" + self.run_simulation(non_rl_merge) - def test_sugiyama(self): - """Verifies that examples/sumo/sugiyama.py is working.""" - # import the experiment variable from the example - exp = sugiyama_example(render=False) - - # run the experiment for a few time steps to ensure it doesn't fail - exp.run(1, 5) + def test_ring(self): + """Verify that examples/exp_configs/non_rl/ring.py is working.""" + self.run_simulation(non_rl_ring) def test_bay_bridge(self): - """Verifies that examples/sumo/bay_bridge.py is working.""" - # import the experiment variable from the example - exp = bay_bridge_example(render=False) - - # run the experiment for a few time steps to ensure it doesn't fail - exp.run(1, 5) - - # import the experiment variable from the example with inflows - exp = bay_bridge_example(render=False, use_inflows=True) - - # run the experiment for a few time steps to ensure it doesn't fail - exp.run(1, 5) + """Verify that examples/exp_configs/non_rl/bay_bridge.py is working.""" + # test without inflows and traffic lights + self.run_simulation(non_rl_bay_bridge) - # import the experiment variable from the example with traffic lights - exp = bay_bridge_example(render=False, use_traffic_lights=True) + # test with inflows + # FIXME - # run the experiment for a few time steps to ensure it doesn't fail - exp.run(1, 5) + # test with traffic lights + # FIXME def test_bay_bridge_toll(self): - """Verifies that examples/sumo/bay_bridge_toll.py is working.""" - # import the experiment variable from the example - exp = bay_bridge_toll_example(render=False) - - # run the experiment for a few time steps to ensure it doesn't fail - exp.run(1, 5) + """Verify that examples/exp_configs/non_rl/bay_bridge_toll.py is working.""" + self.run_simulation(non_rl_bay_bridge_toll) def test_minicity(self): - """Verifies that examples/sumo/minicity.py is working.""" - # import the experiment variable from the example - exp = minicity_example(render=False) + """Verify that examples/exp_configs/non_rl/minicity.py is working.""" + self.run_simulation(non_rl_minicity) + + @staticmethod + def run_simulation(flow_params): + # make the horizon small and set render to False + flow_params['sim'].render = False + flow_params['env'].horizon = 5 - # run the experiment for a few time steps to ensure it doesn't fail - exp.run(1, 5) + # create an experiment object + exp = Experiment(flow_params) - def test_density_exp(self): - """Verifies that examples/sumo/density_exp.py is working.""" - run_bottleneck.remote(100, 1, 10, render=False) + # run the experiment for one run + exp.run(1) class TestStableBaselineExamples(unittest.TestCase): - """Tests the example scripts in examples/stable_baselines. + """Tests the example scripts in examples/exp_configs/rl/singleagent for stable_baselines. - This is done by running each experiment in that folder for five time-steps - and confirming that it completes one rollout with two workers. + This is done by running each experiment in that folder for five time-steps + and confirming that it completes one rollout with two workers. """ + @staticmethod + def run_exp(flow_params): + run_stable_baselines_model(flow_params, 2, 5, 5) + + def test_singleagent_figure_eight(self): + self.run_exp(singleagent_figure_eight) + def test_run_traffic_light_grid(self): - run_traffic_light_grid(num_steps=5) + pass # FIXME - def test_run_figure_eight(self): - run_figure_eight(num_steps=5) + def test_green_wave_inflows(self): + pass # FIXME - def test_run_stabilizing_highway(self): - run_stabilizing_highway(num_steps=5) + def test_singleagent_merge(self): + self.run_exp(singleagent_merge) - def test_run_stabilizing_ring(self): - run_stabilizing_ring(num_steps=5) + def test_ring(self): + self.run_exp(singleagent_ring) - def test_run_velocity_bottleneck(self): - run_velocity_bottleneck(num_steps=5) + def test_bottleneck(self): + self.run_exp(singleagent_bottleneck) class TestRllibExamples(unittest.TestCase): - """Tests the example scripts in examples/rllib. + """Tests the example scripts in examples/exp_configs/rl/singleagent and + examples/exp_configs/rl/multiagent for RLlib. This is done by running each experiment in that folder for five time-steps and confirming that it completes one rollout with two workers. @@ -184,52 +152,65 @@ def setUp(self): if not ray.is_initialized(): ray.init(num_cpus=1) - def test_figure_eight(self): - alg_run, env_name, config = figure_eight_setup() - self.run_exp(alg_run, env_name, config) + def test_singleagent_figure_eight(self): + self.run_exp(singleagent_figure_eight) def test_traffic_light_grid(self): - # test the example in the absence of inflows - alg_run, env_name, config = traffic_light_grid_setup(use_inflows=False) - self.run_exp(alg_run, env_name, config) + pass # FIXME def test_traffic_light_grid_inflows(self): - # test the example in the presence of inflows - alg_run, env_name, config = traffic_light_grid_setup(use_inflows=True) - self.run_exp(alg_run, env_name, config) + pass # FIXME - def test_stabilizing_highway(self): - alg_run, env_name, config = highway_setup() - self.run_exp(alg_run, env_name, config) + def test_singleagent_merge(self): + self.run_exp(singleagent_merge) def test_ring(self): - alg_run, env_name, config = ring_setup() - self.run_exp(alg_run, env_name, config) + self.run_exp(singleagent_ring) def test_bottleneck(self): - alg_run, env_name, config = bottleneck_setup() - self.run_exp(alg_run, env_name, config) + self.run_exp(singleagent_bottleneck) def test_multi_figure_eight(self): - alg_run, env_name, config = multi_figure_eight_setup() - self.run_exp(alg_run, env_name, config) + from examples.exp_configs.rl.multiagent.multiagent_figure_eight import POLICY_GRAPHS as mf8pg + from examples.exp_configs.rl.multiagent.multiagent_figure_eight import policy_mapping_fn as mf8pmf + + kwargs = { + "policy_graphs": mf8pg, + "policy_mapping_fn": mf8pmf + } + self.run_exp(multiagent_figure_eight, **kwargs) def test_multi_ring(self): - alg_run, env_name, config = multi_ring_setup() - self.run_exp(alg_run, env_name, config) + from examples.exp_configs.rl.multiagent.multiagent_ring import POLICY_GRAPHS as mrpg + from examples.exp_configs.rl.multiagent.multiagent_ring import POLICIES_TO_TRAIN as mrpt + from examples.exp_configs.rl.multiagent.multiagent_ring import policy_mapping_fn as mrpmf + + kwargs = { + "policy_graphs": mrpg, + "policies_to_train": mrpt, + "policy_mapping_fn": mrpmf + } + self.run_exp(multiagent_ring, **kwargs) def test_multi_grid(self): - flow_params = multi_grid_setup_flow_params(1, 1, 300) - alg_run, env_name, config = multi_grid_setup(flow_params) - self.run_exp(alg_run, env_name, config) + pass # FIXME def test_multi_highway(self): - flow_params = multi_highway_flow_params - alg_run, env_name, config = multi_highway_setup(flow_params) - self.run_exp(alg_run, env_name, config) + from examples.exp_configs.rl.multiagent.multiagent_highway import POLICY_GRAPHS as mhpg + from examples.exp_configs.rl.multiagent.multiagent_highway import POLICIES_TO_TRAIN as mhpt + from examples.exp_configs.rl.multiagent.multiagent_highway import policy_mapping_fn as mhpmf + + kwargs = { + "policy_graphs": mhpg, + "policies_to_train": mhpt, + "policy_mapping_fn": mhpmf + } + self.run_exp(multiagent_highway, **kwargs) @staticmethod - def run_exp(alg_run, env_name, config): + def run_exp(flow_params, **kwargs): + alg_run, env_name, config = setup_rllib_exps(flow_params, 1, 1, **kwargs) + try: ray.init(num_cpus=1) except Exception as e: diff --git a/tests/fast_tests/test_experiment_base_class.py b/tests/fast_tests/test_experiment_base_class.py index c71dc890b..b3863a77c 100644 --- a/tests/fast_tests/test_experiment_base_class.py +++ b/tests/fast_tests/test_experiment_base_class.py @@ -4,11 +4,16 @@ from flow.core.experiment import Experiment from flow.core.params import VehicleParams -from flow.controllers import RLController, ContinuousRouter +from flow.controllers import IDMController, RLController, ContinuousRouter from flow.core.params import SumoCarFollowingParams from flow.core.params import SumoParams +from flow.core.params import EnvParams, InitialConfig, NetParams +from flow.core.params import TrafficLightParams +from flow.envs import AccelEnv +from flow.networks import RingNetwork from tests.setup_scripts import ring_road_exp_setup + import numpy as np os.environ["TEST_FLAG"] = "True" @@ -21,17 +26,19 @@ class TestNumSteps(unittest.TestCase): def setUp(self): # create the environment and network classes for a ring road - env, _ = ring_road_exp_setup() - + env, _, flow_params = ring_road_exp_setup() + flow_params['sim'].render = False + flow_params['env'].horizon = 10 # instantiate an experiment class - self.exp = Experiment(env) + self.exp = Experiment(flow_params) + self.exp.env = env def tearDown(self): # free up used memory self.exp = None def test_steps(self): - self.exp.run(num_runs=1, num_steps=10) + self.exp.run(num_runs=1) self.assertEqual(self.exp.env.time_counter, 10) @@ -45,17 +52,24 @@ class TestNumRuns(unittest.TestCase): def test_num_runs(self): # run the experiment for 1 run and collect the last position of all # vehicles - env, _ = ring_road_exp_setup() - exp = Experiment(env) - exp.run(num_runs=1, num_steps=10) + env, _, flow_params = ring_road_exp_setup() + flow_params['sim'].render = False + flow_params['env'].horizon = 10 + exp = Experiment(flow_params) + exp.env = env + exp.run(num_runs=1) vel1 = [exp.env.k.vehicle.get_speed(exp.env.k.vehicle.get_ids())] # run the experiment for 2 runs and collect the last position of all # vehicles - env, _ = ring_road_exp_setup() - exp = Experiment(env) - exp.run(num_runs=2, num_steps=10) + env, _, flow_params = ring_road_exp_setup() + flow_params['sim'].render = False + flow_params['env'].horizon = 10 + + exp = Experiment(flow_params) + exp.env = env + exp.run(num_runs=2) vel2 = [exp.env.k.vehicle.get_speed(exp.env.k.vehicle.get_ids())] @@ -84,11 +98,12 @@ def rl_actions(*_): ), num_vehicles=1) - env, _ = ring_road_exp_setup(vehicles=vehicles) - - exp = Experiment(env=env) - - exp.run(1, 10, rl_actions=rl_actions) + env, _, flow_params = ring_road_exp_setup(vehicles=vehicles) + flow_params['sim'].render = False + flow_params['env'].horizon = 10 + exp = Experiment(flow_params) + exp.env = env + exp.run(1, rl_actions=rl_actions) # check that the acceleration of the RL vehicle was that specified by # the rl_actions method @@ -105,23 +120,64 @@ class TestConvertToCSV(unittest.TestCase): def test_convert_to_csv(self): dir_path = os.path.dirname(os.path.realpath(__file__)) sim_params = SumoParams(emission_path="{}/".format(dir_path)) - env, network = ring_road_exp_setup(sim_params=sim_params) - exp = Experiment(env) - exp.run(num_runs=1, num_steps=10, convert_to_csv=True) - time.sleep(0.1) + vehicles = VehicleParams() + vehicles.add( + veh_id="idm", + acceleration_controller=(IDMController, {}), + routing_controller=(ContinuousRouter, {}), + car_following_params=SumoCarFollowingParams( + speed_mode="aggressive", + ), + num_vehicles=1) + + additional_env_params = { + "target_velocity": 8, + "max_accel": 1, + "max_decel": 1, + "sort_vehicles": False, + } + env_params = EnvParams( + horizon=10, + additional_params=additional_env_params) + + additional_net_params = { + "length": 230, + "lanes": 1, + "speed_limit": 30, + "resolution": 40 + } + net_params = NetParams(additional_params=additional_net_params) + + flow_params = dict( + exp_tag="RingRoadTest", + env_name=AccelEnv, + network=RingNetwork, + simulator='traci', + sim=sim_params, + env=env_params, + net=net_params, + veh=vehicles, + initial=InitialConfig(lanes_distribution=1), + tls=TrafficLightParams(), + ) + + exp = Experiment(flow_params) + exp.run(num_runs=1, convert_to_csv=True) + + time.sleep(1.0) # check that both the csv file exists and the xml file doesn't. self.assertFalse(os.path.isfile(dir_path + "/{}-emission.xml".format( - network.name))) + exp.env.network.name))) self.assertTrue(os.path.isfile(dir_path + "/{}-emission.csv".format( - network.name))) + exp.env.network.name))) time.sleep(0.1) # delete the files os.remove(os.path.expanduser(dir_path + "/{}-emission.csv".format( - network.name))) + exp.env.network.name))) if __name__ == '__main__': diff --git a/tests/fast_tests/test_files/ring_230.json b/tests/fast_tests/test_files/ring_230.json index 68be938aa..12b0ec61d 100644 --- a/tests/fast_tests/test_files/ring_230.json +++ b/tests/fast_tests/test_files/ring_230.json @@ -15,7 +15,7 @@ "warmup_steps": 750 }, "env_name": "WaveAttenuationPOEnv", - "exp_tag": "stabilizing_the_ring", + "exp_tag": "singleagent_ring", "initial": { "additional_params": {}, "bunching": 0, diff --git a/tests/fast_tests/test_rewards.py b/tests/fast_tests/test_rewards.py index be616e0bc..ac406b545 100644 --- a/tests/fast_tests/test_rewards.py +++ b/tests/fast_tests/test_rewards.py @@ -23,8 +23,8 @@ def test_desired_velocity(self): "target_velocity": np.sqrt(10), "max_accel": 1, "max_decel": 1, "sort_vehicles": False}) - env, _ = ring_road_exp_setup(vehicles=vehicles, - env_params=env_params) + env, _, _ = ring_road_exp_setup(vehicles=vehicles, + env_params=env_params) # check that the fail attribute leads to a zero return self.assertEqual(desired_velocity(env, fail=True), 0) @@ -61,7 +61,7 @@ def test_average_velocity(self): vehicles = VehicleParams() vehicles.add("test", num_vehicles=10) - env, _ = ring_road_exp_setup(vehicles=vehicles) + env, _, _ = ring_road_exp_setup(vehicles=vehicles) # check that the fail attribute leads to a zero return self.assertEqual(average_velocity(env, fail=True), 0) @@ -77,7 +77,7 @@ def test_average_velocity(self): # recreate the environment with no vehicles vehicles = VehicleParams() - env, _ = ring_road_exp_setup(vehicles=vehicles) + env, _, _ = ring_road_exp_setup(vehicles=vehicles) # check that the reward function return 0 in the case of no vehicles self.assertEqual(average_velocity(env, fail=False), 0) @@ -86,7 +86,7 @@ def test_min_delay(self): """Test the min_delay method.""" # try the case of an environment with no vehicles vehicles = VehicleParams() - env, _ = ring_road_exp_setup(vehicles=vehicles) + env, _, _ = ring_road_exp_setup(vehicles=vehicles) # check that the reward function return 0 in the case of no vehicles self.assertEqual(min_delay(env), 0) @@ -94,7 +94,7 @@ def test_min_delay(self): # try the case of multiple vehicles vehicles = VehicleParams() vehicles.add("test", num_vehicles=10) - env, _ = ring_road_exp_setup(vehicles=vehicles) + env, _, _ = ring_road_exp_setup(vehicles=vehicles) # check the min_delay upon reset self.assertAlmostEqual(min_delay(env), 0) @@ -114,8 +114,8 @@ def test_penalize_standstill(self): "target_velocity": 10, "max_accel": 1, "max_decel": 1, "sort_vehicles": False}) - env, _ = ring_road_exp_setup(vehicles=vehicles, - env_params=env_params) + env, _, _ = ring_road_exp_setup(vehicles=vehicles, + env_params=env_params) # check the penalty is acknowledging all vehicles self.assertEqual(penalize_standstill(env, gain=1), -10) @@ -137,8 +137,8 @@ def test_penalize_near_standstill(self): "target_velocity": 10, "max_accel": 1, "max_decel": 1, "sort_vehicles": False}) - env, _ = ring_road_exp_setup(vehicles=vehicles, - env_params=env_params) + env, _, _ = ring_road_exp_setup(vehicles=vehicles, + env_params=env_params) # check the penalty is acknowledging all vehicles self.assertEqual(penalize_near_standstill(env, gain=1), -10) diff --git a/tests/fast_tests/test_scenario_base_class.py b/tests/fast_tests/test_scenario_base_class.py index 18f449468..e563c07ab 100644 --- a/tests/fast_tests/test_scenario_base_class.py +++ b/tests/fast_tests/test_scenario_base_class.py @@ -42,7 +42,7 @@ class TestGetX(unittest.TestCase): def setUp(self): # create the environment and network classes for a figure eight - self.env, _ = figure_eight_exp_setup() + self.env, _, _ = figure_eight_exp_setup() def tearDown(self): # free data used by the class @@ -74,7 +74,7 @@ class TestGetEdge(unittest.TestCase): def setUp(self): # create the environment and network classes for a figure eight - self.env, _ = figure_eight_exp_setup() + self.env, _, _ = figure_eight_exp_setup() def tearDown(self): # free data used by the class @@ -128,7 +128,7 @@ def setUp_gen_start_pos(self, initial_config=InitialConfig()): num_vehicles=15) # create the environment and network classes for a ring road - self.env, _ = ring_road_exp_setup( + self.env, _, _ = ring_road_exp_setup( net_params=net_params, initial_config=initial_config, vehicles=vehicles) @@ -453,7 +453,7 @@ def setUp(self): initial_config = InitialConfig(x0=150) # create the environment and network classes for a ring road - self.env, _ = figure_eight_exp_setup( + self.env, _, _ = figure_eight_exp_setup( initial_config=initial_config, vehicles=vehicles) def tearDown(self): @@ -527,7 +527,7 @@ def setUp_gen_start_pos(self, initial_config=InitialConfig()): num_vehicles=5) # create the environment and network classes for a ring road - self.env, _ = ring_road_exp_setup( + self.env, _, _ = ring_road_exp_setup( net_params=net_params, initial_config=initial_config, vehicles=vehicles) @@ -609,7 +609,7 @@ def setUp(self): # create the environment and network classes for a variable lanes per # edge ring road - self.env, _ = variable_lanes_exp_setup( + self.env, _, _ = variable_lanes_exp_setup( vehicles=vehicles, initial_config=initial_config) def tearDown(self): @@ -644,7 +644,7 @@ def setUp(self): # create the environment and network classes for a variable lanes per # edge ring road - self.env, _ = variable_lanes_exp_setup( + self.env, _, _ = variable_lanes_exp_setup( vehicles=vehicles, initial_config=initial_config) @@ -666,7 +666,7 @@ def test_edge_length_edges(self): net_params = NetParams(additional_params=additional_net_params) # create the environment and network classes for a ring road - env, _ = ring_road_exp_setup(net_params=net_params) + env, _, _ = ring_road_exp_setup(net_params=net_params) self.assertEqual(env.k.network.edge_length("top"), 250) @@ -685,7 +685,7 @@ def test_edge_length_junctions(self): } net_params = NetParams(additional_params=additional_net_params) - env, _ = figure_eight_exp_setup(net_params=net_params) + env, _, _ = figure_eight_exp_setup(net_params=net_params) self.assertAlmostEqual( env.k.network.edge_length(":center_0"), 9.40) # FIXME: 6.2? @@ -711,7 +711,7 @@ def test_speed_limit_edges(self): net_params = NetParams(additional_params=additional_net_params) # create the environment and network classes for a figure eight - env, _ = ring_road_exp_setup(net_params=net_params) + env, _, _ = ring_road_exp_setup(net_params=net_params) self.assertAlmostEqual(env.k.network.speed_limit("top"), 60) @@ -730,7 +730,7 @@ def test_speed_limit_junctions(self): } net_params = NetParams(additional_params=additional_net_params) - env, network = figure_eight_exp_setup(net_params=net_params) + env, network, _ = figure_eight_exp_setup(net_params=net_params) self.assertAlmostEqual( env.k.network.speed_limit("bottom"), 60) @@ -756,7 +756,7 @@ def test_num_lanes_edges(self): net_params = NetParams(additional_params=additional_net_params) # create the environment and network classes for a figure eight - env, network = ring_road_exp_setup(net_params=net_params) + env, network, _ = ring_road_exp_setup(net_params=net_params) self.assertEqual(env.k.network.num_lanes("top"), 2) @@ -775,7 +775,7 @@ def test_num_lanes_junctions(self): } net_params = NetParams(additional_params=additional_net_params) - env, network = figure_eight_exp_setup(net_params=net_params) + env, network, _ = figure_eight_exp_setup(net_params=net_params) self.assertEqual(env.k.network.num_lanes("bottom"), 3) self.assertEqual(env.k.network.num_lanes(":top_0"), 3) @@ -789,7 +789,7 @@ class TestGetEdgeList(unittest.TestCase): def setUp(self): # create the environment and network classes for a figure eight - self.env, _ = figure_eight_exp_setup() + self.env, _, _ = figure_eight_exp_setup() def tearDown(self): # free data used by the class @@ -812,7 +812,7 @@ class TestGetJunctionList(unittest.TestCase): def setUp(self): # create the environment and network classes for a figure eight - self.env, _ = figure_eight_exp_setup() + self.env, _, _ = figure_eight_exp_setup() def tearDown(self): # free data used by the class @@ -840,7 +840,7 @@ def test_next_prev_edge_figure_eight(self): """ Tests the next_edge() and prev_edge() methods for the figure eight. """ - env, _ = figure_eight_exp_setup() + env, _, _ = figure_eight_exp_setup() next_edge = env.k.network.next_edge("bottom", 0) expected_next_edge = [(':center_1', 0)] @@ -854,7 +854,7 @@ def test_next_prev_edge_ring_road(self): """ Tests the next_edge() and prev_edge() methods for the ring road. """ - env, _ = ring_road_exp_setup() + env, _, _ = ring_road_exp_setup() next_edge = env.k.network.next_edge("top", 0) expected_next_edge = [(":left_0", 0)] @@ -869,7 +869,7 @@ def test_no_edge_ahead(self): Tests that, when there are no edges in front, next_edge() returns an empty list """ - env, _ = highway_exp_setup() + env, _, _ = highway_exp_setup() next_edge = env.k.network.next_edge( env.k.network.get_edge_list()[0], 0) self.assertTrue(len(next_edge) == 0) @@ -879,7 +879,7 @@ def test_no_edge_behind(self): Tests that, when there are no edges behind, prev_edge() returns an empty list """ - env, _ = highway_exp_setup() + env, _, _ = highway_exp_setup() prev_edge = env.k.network.prev_edge( env.k.network.get_edge_list()[0], 0) self.assertTrue(len(prev_edge) == 0) diff --git a/tests/fast_tests/test_traffic_light_grid.py b/tests/fast_tests/test_traffic_light_grid.py index fd14a20f6..224f3e53f 100644 --- a/tests/fast_tests/test_traffic_light_grid.py +++ b/tests/fast_tests/test_traffic_light_grid.py @@ -8,11 +8,12 @@ class Test1x1Environment(unittest.TestCase): def setUp(self): # create the environment and network classes for a traffic light grid network - self.env, _ = traffic_light_grid_mxn_exp_setup() + + self.env, _, flow_params = traffic_light_grid_mxn_exp_setup() self.env.reset() # instantiate an experiment class - self.exp = Experiment(self.env) + self.exp = Experiment(flow_params) def tearDown(self): # terminate the traci instance @@ -94,11 +95,12 @@ def test_get_distance_to_intersection(self): class Test2x2Environment(unittest.TestCase): def setUp(self): # create the environment and network classes for a traffic light grid network - self.env, _ = traffic_light_grid_mxn_exp_setup(row_num=2, col_num=2) + + self.env, _, flow_params = traffic_light_grid_mxn_exp_setup(row_num=2, col_num=2) self.env.reset() # instantiate an experiment class - self.exp = Experiment(self.env) + self.exp = Experiment(flow_params) def tearDown(self): # terminate the traci instance diff --git a/tests/fast_tests/test_traffic_lights.py b/tests/fast_tests/test_traffic_lights.py index a06d34a0d..d5c02b5bc 100644 --- a/tests/fast_tests/test_traffic_lights.py +++ b/tests/fast_tests/test_traffic_lights.py @@ -40,7 +40,7 @@ def test_single_lane(self): net_params = NetParams(additional_params=additional_net_params) # create the environment and network classes for a ring road - self.env, _ = ring_road_exp_setup( + self.env, _, _ = ring_road_exp_setup( net_params=net_params, traffic_lights=traffic_lights) self.env.reset() @@ -65,7 +65,7 @@ def test_multi_lane(self): net_params = NetParams(additional_params=additional_net_params) # create the environment and network classes for a ring road - self.env, _ = ring_road_exp_setup( + self.env, _, _ = ring_road_exp_setup( net_params=net_params, traffic_lights=traffic_lights) self.env.reset() @@ -96,7 +96,7 @@ def setUp(self): net_params = NetParams(additional_params=additional_net_params) # create the environment and network classes for a ring road - self.env, _ = ring_road_exp_setup( + self.env, _, _ = ring_road_exp_setup( net_params=net_params, traffic_lights=traffic_lights) def tearDown(self): @@ -153,7 +153,7 @@ def setUp(self): min_gap=2.5, tau=1.1), num_vehicles=16) - self.env, _ = traffic_light_grid_mxn_exp_setup( + self.env, _, _ = traffic_light_grid_mxn_exp_setup( row_num=1, col_num=3, vehicles=vehicles) def tearDown(self): @@ -228,17 +228,18 @@ def setUp(self): min_gap=2.5, tau=1.1), num_vehicles=16) - env, _ = traffic_light_grid_mxn_exp_setup( + _, _, flow_params = traffic_light_grid_mxn_exp_setup( row_num=1, col_num=3, vehicles=vehicles) - self.exp = Experiment(env) + flow_params['env'].horizon = 50 + self.exp = Experiment(flow_params) def tearDown(self): # free data used by the class self.exp = None def test_it_runs(self): - self.exp.run(5, 50) + self.exp.run(5) class TestIndividualLights(unittest.TestCase): @@ -285,17 +286,18 @@ def setUp(self): file="testindividuallights.xml", freq=100) - env, _ = traffic_light_grid_mxn_exp_setup( + _, _, flow_params = traffic_light_grid_mxn_exp_setup( row_num=1, col_num=4, tl_logic=tl_logic) - self.exp = Experiment(env) + flow_params['env'].horizon = 50 + self.exp = Experiment(flow_params) def tearDown(self): # free data used by the class self.exp = None def test_it_runs(self): - self.exp.run(5, 50) + self.exp.run(5) class TestCustomization(unittest.TestCase): @@ -330,7 +332,7 @@ def setUp(self): net_params = NetParams(additional_params=additional_net_params) # create the environment and network classes for a ring road - self.env, _ = ring_road_exp_setup( + self.env, _, _ = ring_road_exp_setup( net_params=net_params, traffic_lights=traffic_lights) def tearDown(self): diff --git a/tests/fast_tests/test_vehicles.py b/tests/fast_tests/test_vehicles.py index 593c8ccef..485a6a072 100644 --- a/tests/fast_tests/test_vehicles.py +++ b/tests/fast_tests/test_vehicles.py @@ -118,7 +118,7 @@ def test_add_vehicles_human(self): num_vehicles=4, acceleration_controller=(IDMController, {})) - env, _ = ring_road_exp_setup(vehicles=vehicles) + env, _, _ = ring_road_exp_setup(vehicles=vehicles) self.assertEqual(env.k.vehicle.num_vehicles, 7) self.assertEqual(len(env.k.vehicle.get_ids()), 7) @@ -138,7 +138,7 @@ def test_add_vehicles_rl(self): num_vehicles=10, acceleration_controller=(RLController, {})) - env, _ = ring_road_exp_setup(vehicles=vehicles) + env, _, _ = ring_road_exp_setup(vehicles=vehicles) self.assertEqual(env.k.vehicle.num_vehicles, 10) self.assertEqual(len(env.k.vehicle.get_ids()), 10) @@ -160,7 +160,7 @@ def test_remove(self): num_vehicles=10, acceleration_controller=(RLController, {})) - env, _ = ring_road_exp_setup(vehicles=vehicles) + env, _, _ = ring_road_exp_setup(vehicles=vehicles) # remove one human-driven vehicle and on rl vehicle env.k.vehicle.remove("test_0") @@ -225,7 +225,7 @@ def test_no_junctions_ring(self): initial_config = InitialConfig(lanes_distribution=float("inf")) - env, _ = ring_road_exp_setup( + env, _, _ = ring_road_exp_setup( net_params=net_params, vehicles=vehicles, initial_config=initial_config) @@ -279,7 +279,7 @@ def test_no_junctions_highway(self): "start_lanes": [1, 2, 0]} initial_config.additional_params = initial_pos - env, _ = highway_exp_setup( + env, _, _ = highway_exp_setup( sim_params=SumoParams(sim_step=0.1, render=False), net_params=net_params, vehicles=vehicles, @@ -355,7 +355,7 @@ def test_no_junctions_highway(self): "start_lanes": [0, 0, 0, 1, 1, 2, 2, 3, 3]} initial_config.additional_params = initial_pos - env, _ = highway_exp_setup( + env, _, _ = highway_exp_setup( sim_params=SumoParams(sim_step=0.1, render=False), net_params=net_params, vehicles=vehicles, @@ -419,7 +419,7 @@ def test_no_junctions_highway(self): "start_lanes": [1, 2, 0]} initial_config.additional_params = initial_pos - env, _ = highway_exp_setup( + env, _, _ = highway_exp_setup( sim_params=SumoParams(sim_step=0.1, render=False), net_params=net_params, vehicles=vehicles, @@ -486,7 +486,7 @@ def test_no_junctions_highway(self): "start_lanes": [0, 0, 0]} initial_config.additional_params = initial_pos - env, _ = highway_exp_setup( + env, _, _ = highway_exp_setup( sim_params=SumoParams(sim_step=0.1, render=False), net_params=net_params, vehicles=vehicles, @@ -542,7 +542,7 @@ def setUp(self): vehicles = VehicleParams() vehicles.add(veh_id="test", num_vehicles=20) - self.env, _ = ring_road_exp_setup(vehicles=vehicles) + self.env, _, _ = ring_road_exp_setup(vehicles=vehicles) def tearDown(self): # free data used by the class @@ -563,7 +563,7 @@ def test_obs_ids(self): vehicles = VehicleParams() vehicles.add(veh_id="test", num_vehicles=10) - env, _ = ring_road_exp_setup(vehicles=vehicles) + env, _, _ = ring_road_exp_setup(vehicles=vehicles) # test setting new observed values env.k.vehicle.set_observed("test_0") diff --git a/tests/setup_scripts.py b/tests/setup_scripts.py index 491e0e240..08d5b2c1e 100644 --- a/tests/setup_scripts.py +++ b/tests/setup_scripts.py @@ -15,11 +15,12 @@ from flow.core.params import TrafficLightParams from flow.core.params import VehicleParams from flow.envs.traffic_light_grid import TrafficLightGridTestEnv -from flow.envs.ring.accel import AccelEnv + from flow.networks.figure_eight import FigureEightNetwork from flow.networks.traffic_light_grid import TrafficLightGridNetwork from flow.networks.highway import HighwayNetwork from flow.networks.ring import RingNetwork +from flow.envs.ring.accel import AccelEnv def ring_road_exp_setup(sim_params=None, @@ -97,6 +98,41 @@ def ring_road_exp_setup(sim_params=None, # set default to no traffic lights traffic_lights = TrafficLightParams() + flow_params = dict( + # name of the experiment + exp_tag="RingRoadTest", + + # name of the flow environment the experiment is running on + env_name=AccelEnv, + + # name of the network class the experiment is running on + network=RingNetwork, + + # simulator that is used by the experiment + simulator='traci', + + # sumo-related parameters (see flow.core.params.SumoParams) + sim=sim_params, + + # environment related parameters (see flow.core.params.EnvParams) + env=env_params, + # network-related parameters (see flow.core.params.NetParams and the + # network's documentation or ADDITIONAL_NET_PARAMS component) + net=net_params, + + # vehicles to be placed in the network at the start of a rollout (see + # flow.core.params.VehicleParams) + veh=vehicles, + + # parameters specifying the positioning of vehicles upon initialization/ + # reset (see flow.core.params.InitialConfig) + initial=initial_config, + + # traffic lights to be introduced to specific nodes (see + # flow.core.params.TrafficLightParams) + tls=traffic_lights, + ) + # create the network network = RingNetwork( name="RingRoadTest", @@ -112,7 +148,7 @@ def ring_road_exp_setup(sim_params=None, # reset the environment env.reset() - return env, network + return env, network, flow_params def figure_eight_exp_setup(sim_params=None, @@ -190,6 +226,41 @@ def figure_eight_exp_setup(sim_params=None, # set default to no traffic lights traffic_lights = TrafficLightParams() + flow_params = dict( + # name of the experiment + exp_tag="FigureEightTest", + + # name of the flow environment the experiment is running on + env_name=AccelEnv, + + # name of the network class the experiment is running on + network=FigureEightNetwork, + + # simulator that is used by the experiment + simulator='traci', + + # sumo-related parameters (see flow.core.params.SumoParams) + sim=sim_params, + + # environment related parameters (see flow.core.params.EnvParams) + env=env_params, + # network-related parameters (see flow.core.params.NetParams and the + # network's documentation or ADDITIONAL_NET_PARAMS component) + net=net_params, + + # vehicles to be placed in the network at the start of a rollout (see + # flow.core.params.VehicleParams) + veh=vehicles, + + # parameters specifying the positioning of vehicles upon initialization/ + # reset (see flow.core.params.InitialConfig) + initial=initial_config, + + # traffic lights to be introduced to specific nodes (see + # flow.core.params.TrafficLightParams) + tls=traffic_lights, + ) + # create the network network = FigureEightNetwork( name="FigureEightTest", @@ -205,7 +276,7 @@ def figure_eight_exp_setup(sim_params=None, # reset the environment env.reset() - return env, network + return env, network, flow_params def highway_exp_setup(sim_params=None, @@ -284,6 +355,41 @@ def highway_exp_setup(sim_params=None, # set default to no traffic lights traffic_lights = TrafficLightParams() + flow_params = dict( + # name of the experiment + exp_tag="RingRoadTest", + + # name of the flow environment the experiment is running on + env_name=AccelEnv, + + # name of the network class the experiment is running on + network=HighwayNetwork, + + # simulator that is used by the experiment + simulator='traci', + + # sumo-related parameters (see flow.core.params.SumoParams) + sim=sim_params, + + # environment related parameters (see flow.core.params.EnvParams) + env=env_params, + # network-related parameters (see flow.core.params.NetParams and the + # network's documentation or ADDITIONAL_NET_PARAMS component) + net=net_params, + + # vehicles to be placed in the network at the start of a rollout (see + # flow.core.params.VehicleParams) + veh=vehicles, + + # parameters specifying the positioning of vehicles upon initialization/ + # reset (see flow.core.params.InitialConfig) + initial=initial_config, + + # traffic lights to be introduced to specific nodes (see + # flow.core.params.TrafficLightParams) + tls=traffic_lights, + ) + # create the network network = HighwayNetwork( name="RingRoadTest", @@ -299,7 +405,7 @@ def highway_exp_setup(sim_params=None, # reset the environment env.reset() - return env, network + return env, network, flow_params def traffic_light_grid_mxn_exp_setup(row_num=1, @@ -409,6 +515,41 @@ def traffic_light_grid_mxn_exp_setup(row_num=1, initial_config = InitialConfig( spacing="custom", additional_params={"enter_speed": 30}) + flow_params = dict( + # name of the experiment + exp_tag="Grid1x1Test", + + # name of the flow environment the experiment is running on + env_name=TrafficLightGridTestEnv, + + # name of the network class the experiment is running on + network=TrafficLightGridNetwork, + + # simulator that is used by the experiment + simulator='traci', + + # sumo-related parameters (see flow.core.params.SumoParams) + sim=sim_params, + + # environment related parameters (see flow.core.params.EnvParams) + env=env_params, + # network-related parameters (see flow.core.params.NetParams and the + # network's documentation or ADDITIONAL_NET_PARAMS component) + net=net_params, + + # vehicles to be placed in the network at the start of a rollout (see + # flow.core.params.VehicleParams) + veh=vehicles, + + # parameters specifying the positioning of vehicles upon initialization/ + # reset (see flow.core.params.InitialConfig) + initial=initial_config, + + # traffic lights to be introduced to specific nodes (see + # flow.core.params.TrafficLightParams) + tls=tl_logic + ) + # create the network network = TrafficLightGridNetwork( name="Grid1x1Test", @@ -424,7 +565,7 @@ def traffic_light_grid_mxn_exp_setup(row_num=1, # reset the environment env.reset() - return env, network + return env, network, flow_params def variable_lanes_exp_setup(sim_params=None, @@ -505,6 +646,41 @@ def variable_lanes_exp_setup(sim_params=None, # set default to no traffic lights traffic_lights = TrafficLightParams() + flow_params = dict( + # name of the experiment + exp_tag="VariableLaneRingRoadTest", + + # name of the flow environment the experiment is running on + env_name=AccelEnv, + + # name of the network class the experiment is running on + network=VariableLanesNetwork, + + # simulator that is used by the experiment + simulator='traci', + + # sumo-related parameters (see flow.core.params.SumoParams) + sim=sim_params, + + # environment related parameters (see flow.core.params.EnvParams) + env=env_params, + # network-related parameters (see flow.core.params.NetParams and the + # network's documentation or ADDITIONAL_NET_PARAMS component) + net=net_params, + + # vehicles to be placed in the network at the start of a rollout (see + # flow.core.params.VehicleParams) + veh=vehicles, + + # parameters specifying the positioning of vehicles upon initialization/ + # reset (see flow.core.params.InitialConfig) + initial=initial_config, + + # traffic lights to be introduced to specific nodes (see + # flow.core.params.TrafficLightParams) + tls=traffic_lights, + ) + # create the network network = VariableLanesNetwork( name="VariableLaneRingRoadTest", @@ -520,7 +696,7 @@ def variable_lanes_exp_setup(sim_params=None, # reset the environment env.reset() - return env, network + return env, network, flow_params class VariableLanesNetwork(RingNetwork): diff --git a/tests/slow_tests/test_benchmarks.py b/tests/slow_tests/test_benchmarks.py index be59ddcf3..c119d4bd8 100644 --- a/tests/slow_tests/test_benchmarks.py +++ b/tests/slow_tests/test_benchmarks.py @@ -34,6 +34,11 @@ from flow.benchmarks.stable_baselines.trpo_runner import parse_args from flow.benchmarks.stable_baselines.trpo_runner import save_model +import random + +# This removes the randomness in this test +random.seed(a=10) + N_CPUS = 1 ray.init(num_cpus=N_CPUS) diff --git a/tests/stress_tests/stress_test_start.py b/tests/stress_tests/stress_test_start.py index d176b2f75..ce457c1ee 100644 --- a/tests/stress_tests/stress_test_start.py +++ b/tests/stress_tests/stress_test_start.py @@ -34,7 +34,7 @@ def start(): initial_config=initial_config) env = AccelEnv(env_params, sim_params, network) - env._close() + env.close() ray.init() diff --git a/tutorials/tutorial00_flow.ipynb b/tutorials/tutorial00_flow.ipynb index 3ff5030d1..f23cb74bb 100644 --- a/tutorials/tutorial00_flow.ipynb +++ b/tutorials/tutorial00_flow.ipynb @@ -65,10 +65,11 @@ "flow\n", "├── docs # some random documents, don't worry about it\n", "├── examples # a lot of example codes using Flow -- this is where you want to head once you're done with the tutorials and want to start doing some real code\n", - "│ ├── aimsun # examples using just Aimsun, without training \n", - "│ ├── rllab # examples of training with rllab (avoid rllab, we are going to deprecate it soon!)\n", - "│ ├── rllib # examples of training with RLlib\n", - "│ └── sumo # examples using just SUMO, without training. You can run these right away to see what the networks look like!\n", + "│ └── exp_configs # configuration of the all the examples \n", + "│ ├── non_rl # configurations of examples with simulations (e.g. either SUMO or Aimsun) without any Reinforcement Learning\n", + "│ └── rl\n", + "│ ├── singleagent # configurations of examples with training single agent RL contollers\n", + "│ └── multiagent # configurations of examples with training multi agent RL contollers\n", "├── flow\n", "│ ├── benchmarks # several custom networks and configurations on which you can evaluate and compare different RL algorithms\n", "│ ├── controllers # implementations of controllers for the vehicles (IDM, Follower-Stopper...)\n", @@ -114,4 +115,4 @@ }, "nbformat": 4, "nbformat_minor": 2 -} +} \ No newline at end of file diff --git a/tutorials/tutorial08_environments.ipynb b/tutorials/tutorial08_environments.ipynb index df53eb7c8..16459a8bd 100644 --- a/tutorials/tutorial08_environments.ipynb +++ b/tutorials/tutorial08_environments.ipynb @@ -292,7 +292,7 @@ "\n", "initial_config = InitialConfig(bunching=20)\n", "\n", - "network = RingNetwork(name=\"sugiyama\",\n", + "network = RingNetwork(name=\"ring\",\n", " vehicles=vehicles,\n", " net_params=net_params,\n", " initial_config=initial_config)\n", @@ -378,7 +378,7 @@ "\n", " initial_config = InitialConfig(bunching=20)\n", "\n", - " network = RingNetwork(name=\"sugiyama-training\",\n", + " network = RingNetwork(name=\"ring-training\",\n", " vehicles=vehicles,\n", " net_params=net_params,\n", " initial_config=initial_config)\n", diff --git a/tutorials/tutorial10_traffic_lights.ipynb b/tutorials/tutorial10_traffic_lights.ipynb index 0ebf6535b..43a2bba97 100644 --- a/tutorials/tutorial10_traffic_lights.ipynb +++ b/tutorials/tutorial10_traffic_lights.ipynb @@ -15,9 +15,9 @@ "source": [ "This tutorial walks through how to add traffic lights to experiments. This tutorial will use the following files:\n", "\n", - "* Experiment script for RL version of traffic lights in grid: `examples/rllib/traffic_light_grid.py`\n", - "* Experiment script for non-RL version of traffic lights in grid: `examples/sumo/traffic_light_grid.py`\n", - "* Network: `traffic_light_grid.py` (class TrafficLightGridNetwork)\n", + "* Experiment config for RL version of traffic lights in grid: `examples/exp_configs/rl/singleagent/singleagent_traffic_light_grid.py`\n", + "* Experiment config for non-RL version of traffic lights in grid: `examples/exp_configs/non_rl/traffic_light_grid.py`\n", + "* Network: `traffic_light_grid.py` (class TrafficLightGridScenario)\n", "* Environment for RL version of traffic lights in grid: (class TrafficLightGridEnv)\n", "* Environment for non-RL version of traffic lights in grid: (class AccelEnv)\n", "\n", @@ -138,7 +138,7 @@ "metadata": {}, "outputs": [], "source": [ - " | | |\n", + " | | |\n", "-3-4-5-\n", " | | |\n", "-0-1-2-\n", @@ -723,4 +723,4 @@ }, "nbformat": 4, "nbformat_minor": 1 -} +} \ No newline at end of file diff --git a/tutorials/tutorial12_bottlenecks.ipynb b/tutorials/tutorial12_bottlenecks.ipynb index 2589f03c7..741390976 100644 --- a/tutorials/tutorial12_bottlenecks.ipynb +++ b/tutorials/tutorial12_bottlenecks.ipynb @@ -4,9 +4,9 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "# Tutorial 12: Bottlenecks Experiments\n", + "# Tutorial 12: Bottleneck Experiments\n", "\n", - "This tutorial walks you through the process of *running the bottlenecks experiments*. The bottleneck experiment, depicted in Fig. 1, is intended to illustrate the dynamics of traffic in a bottleneck. In particular, our bottleneck is intended to imitate the dynamics of traffic on the San Francisco-Oakland Bay Bridge, where fifteen lanes narrow to eight to five. In our bottleneck, we have N * 4 lanes narrow to N * 2 to N, where N is a scaling factor that can be used to increase the number of lanes. As demonstrated in Fig. 2, this bottleneck has a phenomenon known as *capacity drop*: as the number of vehicles flowing into the system increases the number of exiting vehicles initially increases. However, above a critical flow of entering vehicles, the output of the system starts to decrease as congestion forms. Just like in the San Francisco-Oakland Bay Bridge, there is a configurable toll booth and a set of traffic lights that can be used to control the inflow. Each of the merges is implemented as a zipper merge, in which the vehicles merge as late as possible. \n", + "This tutorial walks you through the process of *running the bottleneck experiments*. The bottleneck experiment, depicted in Fig. 1, is intended to illustrate the dynamics of traffic in a bottleneck. In particular, our bottleneck is intended to imitate the dynamics of traffic on the San Francisco-Oakland Bay Bridge, where fifteen lanes narrow to eight to five. In our bottleneck, we have N * 4 lanes narrow to N * 2 to N, where N is a scaling factor that can be used to increase the number of lanes. As demonstrated in Fig. 2, this bottleneck has a phenomenon known as *capacity drop*: as the number of vehicles flowing into the system increases the number of exiting vehicles initially increases. However, above a critical flow of entering vehicles, the output of the system starts to decrease as congestion forms. Just like in the San Francisco-Oakland Bay Bridge, there is a configurable toll booth and a set of traffic lights that can be used to control the inflow. Each of the merges is implemented as a zipper merge, in which the vehicles merge as late as possible. \n", "\n", "
\n", "\n", @@ -341,7 +341,7 @@ "\n", "While there are many different ways you might use autonomous vehicles (AVs) to try to prevent the effect of a capacity drop, here we demonstrate the particular control scheme used in \"Lagrangian control through deep-rl: Applications to bottleneck decongestion\" by E. Vinitsky, K. Parvate et. al. \n", "\n", - "The code referenced is in *examples/rllib/velocity_bottleneck.py*.\n", + "The code referenced is in *examples/exp_configs/rl/singleagent/singleagent_bottleneck.py*.\n", "\n", "We insert a flow of autonomous vehicles as a fraction of the total flow of the system. Due to randomness in the inflow, the number of AVs in the system varies.\n", "In this scheme, all of the AVs are controlled by a single controller. However, because neural network controllers necessarily take in a fixed sized input and have a fixed size output, we come up with a state parametrization and action parametrization that can handle the varying number of vehicles.\n", diff --git a/tutorials/tutorial13_rllib_ec2.ipynb b/tutorials/tutorial13_rllib_ec2.ipynb index 0bdf4c020..1811e0133 100644 --- a/tutorials/tutorial13_rllib_ec2.ipynb +++ b/tutorials/tutorial13_rllib_ec2.ipynb @@ -49,7 +49,7 @@ " \n", "2. Use the `ray exec` command to communicate with your cluster. \n", "\n", - " `ray exec ray_autoscale.yaml \"flow/examples/stabilizing_the_ring.py\"`\n", + " `ray exec ray_autoscale.yaml \"flow/examples/train_rllib.py singleagent_ring\"`\n", " * For a list of options you can provide to this command which will enable a variety of helpful options such as running in tmux or stopping after the command completes, view the link at the beginning of this tutorial.\n", " \n", "3. Attach to the cluster via `ray attach`.\n", @@ -61,7 +61,7 @@ "Note that the above steps 2-3 can become tedious if you create multiple clusters, and thus there are many versions of ray_autoscale.yaml lying around. For further explanation, read on: ray commands identify clusters according to the cluster_name attribute in ray_autoscale.yaml, so if you create 'test_0', test_1', 'test_2', 'test_3', and 'test_4' by simply erasing 'test_0' and replacing it with 'test_1', and so on, you would have to manually change the cluster_name in ray_autoscale.yaml to specify which cluster you intend to interact with while using `ray attach`, `ray exec`, or other `ray` commands. An alternative is this: when the cluster is created i.e. after `ray up ray_autoscale.yaml -y` is successful, it returns a ssh command to connect to that cluster's IP directly. When running multiple clusters, it can be useful to save these ssh commands.\n", "\n", "Note note, that a helpful, streamlined method of starting and executing a cluster in one fell swoop can be done via:
\n", - "4. `ray exec ray_autoscale.yaml flow/examples/stabilizing_the_ring.py --start`\n" + "4. `ray exec ray_autoscale.yaml \"flow/examples/train_rllib.py singleagent_ring\" --start`\n" ] }, { @@ -84,7 +84,7 @@ " - To scroll within the session: ctrl-b + \\[\n", " - To exit scroll mode: `q`\n", " \n", - "* Information about managing results: As usual, ray results will be automatically written to /$HOME/ray_results. To upload these results to Amazon s3, you should configure this step before running the experiment. An argument should be included within flow_params in the runner script (i.e. stabilizing_the_ring.py) in the following fashion (note the # CHANGE ME!!! comment):\n", + "* Information about managing results: As usual, ray results will be automatically written to /$HOME/ray_results. To upload these results to Amazon s3, you should configure this step before running the experiment. An argument should be included within flow_params in the runner script (i.e. singleagent_ring.py) in the following fashion (note the # CHANGE ME!!! comment):\n", "\n", "```\n", "if __name__ == \"__main__\":\n", From 0007f89a40096a0cbd66a5af6cf8999a1380a6a7 Mon Sep 17 00:00:00 2001 From: Ashkan Y Date: Fri, 27 Dec 2019 16:44:43 -0800 Subject: [PATCH 23/86] traffic light grid multiagent fix --- .../multiagent_traffic_light_grid.py | 368 ++++++------------ tests/fast_tests/test_examples.py | 14 +- 2 files changed, 130 insertions(+), 252 deletions(-) diff --git a/examples/exp_configs/rl/multiagent/multiagent_traffic_light_grid.py b/examples/exp_configs/rl/multiagent/multiagent_traffic_light_grid.py index 4182cd756..2340d67fc 100644 --- a/examples/exp_configs/rl/multiagent/multiagent_traffic_light_grid.py +++ b/examples/exp_configs/rl/multiagent/multiagent_traffic_light_grid.py @@ -35,262 +35,132 @@ # number of vehicles originating in the left, right, top, and bottom edges N_LEFT, N_RIGHT, N_TOP, N_BOTTOM = 1, 1, 1, 1 - -def make_flow_params(n_rows, n_columns, edge_inflow): - """ - Generate the flow params for the experiment. - - Parameters - ---------- - n_rows : int - number of rows in the traffic light grid - n_columns : int - number of columns in the traffic light grid - edge_inflow : float - - Returns - ------- - dict - flow_params object - """ - # we place a sufficient number of vehicles to ensure they confirm with the - # total number specified above. We also use a "right_of_way" speed mode to - # support traffic light compliance - vehicles = VehicleParams() - num_vehicles = (N_LEFT + N_RIGHT) * n_columns + (N_BOTTOM + N_TOP) * n_rows - vehicles.add( - veh_id="human", - acceleration_controller=(SimCarFollowingController, {}), - car_following_params=SumoCarFollowingParams( - min_gap=2.5, - max_speed=V_ENTER, - decel=7.5, # avoid collisions at emergency stops - speed_mode="right_of_way", - ), - routing_controller=(GridRouter, {}), - num_vehicles=num_vehicles) - - # inflows of vehicles are place on all outer edges (listed here) - outer_edges = [] - outer_edges += ["left{}_{}".format(n_rows, i) for i in range(n_columns)] - outer_edges += ["right0_{}".format(i) for i in range(n_rows)] - outer_edges += ["bot{}_0".format(i) for i in range(n_rows)] - outer_edges += ["top{}_{}".format(i, n_columns) for i in range(n_rows)] - - # equal inflows for each edge (as dictate by the EDGE_INFLOW constant) - inflow = InFlows() - for edge in outer_edges: - inflow.add( - veh_type="human", - edge=edge, - vehs_per_hour=edge_inflow, - departLane="free", - departSpeed=V_ENTER) - - flow_params = dict( - # name of the experiment - exp_tag="grid_0_{}x{}_i{}_multiagent".format(n_rows, n_columns, - edge_inflow), - - # name of the flow environment the experiment is running on - env_name=MultiTrafficLightGridPOEnv, - - # name of the network class the experiment is running on - network=TrafficLightGridNetwork, - - # simulator that is used by the experiment - simulator='traci', - - # sumo-related parameters (see flow.core.params.SumoParams) - sim=SumoParams( - restart_instance=True, - sim_step=1, - render=False, - ), - - # environment related parameters (see flow.core.params.EnvParams) - env=EnvParams( - horizon=HORIZON, - additional_params={ - "target_velocity": 50, - "switch_time": 3, - "num_observed": 2, - "discrete": False, - "tl_type": "actuated", - "num_local_edges": 4, - "num_local_lights": 4, - }, - ), - - # network-related parameters (see flow.core.params.NetParams and the - # network's documentation or ADDITIONAL_NET_PARAMS component) - net=NetParams( - inflows=inflow, - additional_params={ - "speed_limit": V_ENTER + 5, # inherited from grid0 benchmark - "grid_array": { - "short_length": SHORT_LENGTH, - "inner_length": INNER_LENGTH, - "long_length": LONG_LENGTH, - "row_num": n_rows, - "col_num": n_columns, - "cars_left": N_LEFT, - "cars_right": N_RIGHT, - "cars_top": N_TOP, - "cars_bot": N_BOTTOM, - }, - "horizontal_lanes": 1, - "vertical_lanes": 1, +EDGE_INFLOW = 300 # inflow rate of vehicles at every edge +N_ROWS = 3 # number of row of bidirectional lanes +N_COLUMNS = 3 # number of columns of bidirectional lanes + + +# we place a sufficient number of vehicles to ensure they confirm with the +# total number specified above. We also use a "right_of_way" speed mode to +# support traffic light compliance +vehicles = VehicleParams() +num_vehicles = (N_LEFT + N_RIGHT) * N_COLUMNS + (N_BOTTOM + N_TOP) * N_ROWS +vehicles.add( + veh_id="human", + acceleration_controller=(SimCarFollowingController, {}), + car_following_params=SumoCarFollowingParams( + min_gap=2.5, + max_speed=V_ENTER, + decel=7.5, # avoid collisions at emergency stops + speed_mode="right_of_way", + ), + routing_controller=(GridRouter, {}), + num_vehicles=num_vehicles) + +# inflows of vehicles are place on all outer edges (listed here) +outer_edges = [] +outer_edges += ["left{}_{}".format(N_ROWS, i) for i in range(N_COLUMNS)] +outer_edges += ["right0_{}".format(i) for i in range(N_ROWS)] +outer_edges += ["bot{}_0".format(i) for i in range(N_ROWS)] +outer_edges += ["top{}_{}".format(i, N_COLUMNS) for i in range(N_ROWS)] + +# equal inflows for each edge (as dictate by the EDGE_INFLOW constant) +inflow = InFlows() +for edge in outer_edges: + inflow.add( + veh_type="human", + edge=edge, + vehs_per_hour=EDGE_INFLOW, + departLane="free", + departSpeed=V_ENTER) + +flow_params = dict( + # name of the experiment + exp_tag="grid_0_{}x{}_i{}_multiagent".format(N_ROWS, N_COLUMNS, + EDGE_INFLOW), + + # name of the flow environment the experiment is running on + env_name=MultiTrafficLightGridPOEnv, + + # name of the network class the experiment is running on + network=TrafficLightGridNetwork, + + # simulator that is used by the experiment + simulator='traci', + + # sumo-related parameters (see flow.core.params.SumoParams) + sim=SumoParams( + restart_instance=True, + sim_step=1, + render=False, + ), + + # environment related parameters (see flow.core.params.EnvParams) + env=EnvParams( + horizon=HORIZON, + additional_params={ + "target_velocity": 50, + "switch_time": 3, + "num_observed": 2, + "discrete": False, + "tl_type": "actuated", + "num_local_edges": 4, + "num_local_lights": 4, + }, + ), + + # network-related parameters (see flow.core.params.NetParams and the + # network's documentation or ADDITIONAL_NET_PARAMS component) + net=NetParams( + inflows=inflow, + additional_params={ + "speed_limit": V_ENTER + 5, # inherited from grid0 benchmark + "grid_array": { + "short_length": SHORT_LENGTH, + "inner_length": INNER_LENGTH, + "long_length": LONG_LENGTH, + "row_num": N_ROWS, + "col_num": N_COLUMNS, + "cars_left": N_LEFT, + "cars_right": N_RIGHT, + "cars_top": N_TOP, + "cars_bot": N_BOTTOM, }, - ), - - # vehicles to be placed in the network at the start of a rollout (see - # flow.core.params.VehicleParams) - veh=vehicles, - - # parameters specifying the positioning of vehicles upon initialization - # or reset (see flow.core.params.InitialConfig) - initial=InitialConfig( - spacing='custom', - shuffle=True, - ), - ) - return flow_params - - -def setup_exps_PPO(flow_params): - """ - Experiment setup with PPO using RLlib. - - Parameters - ---------- - flow_params : dictionary of flow parameters - - Returns - ------- - str - name of the training algorithm - str - name of the gym environment to be trained - dict - training configuration parameters - """ - alg_run = 'PPO' - agent_cls = get_agent_class(alg_run) - config = agent_cls._default_config.copy() - config["num_workers"] = min(N_CPUS, N_ROLLOUTS) - config['train_batch_size'] = HORIZON * N_ROLLOUTS - config['simple_optimizer'] = True - config['gamma'] = 0.999 # discount rate - config['model'].update({'fcnet_hiddens': [32, 32]}) - config['lr'] = tune.grid_search([1e-5, 1e-4, 1e-3]) - config['horizon'] = HORIZON - config['clip_actions'] = False # FIXME(ev) temporary ray bug - config['observation_filter'] = 'NoFilter' - - # save the flow params for replay - flow_json = json.dumps( - flow_params, cls=FlowParamsEncoder, sort_keys=True, indent=4) - config['env_config']['flow_params'] = flow_json - config['env_config']['run'] = alg_run - - create_env, env_name = make_create_env(params=flow_params, version=0) - - # Register as rllib env - register_env(env_name, create_env) - - test_env = create_env() - obs_space = test_env.observation_space - act_space = test_env.action_space - - def gen_policy(): - return PPOTFPolicy, obs_space, act_space, {} - - # Setup PG with a single policy graph for all agents - policy_graphs = {'av': gen_policy()} - - def policy_mapping_fn(_): - """Map a policy in RLlib.""" - return 'av' - - config.update({ - 'multiagent': { - 'policies': policy_graphs, - 'policy_mapping_fn': tune.function(policy_mapping_fn), - 'policies_to_train': ['av'] - } - }) - - return alg_run, env_name, config - - -if __name__ == '__main__': - EXAMPLE_USAGE = """ - example usage: - python multiagent_traffic_light_grid.py --upload_dir= - """ - - parser = argparse.ArgumentParser( - formatter_class=argparse.RawDescriptionHelpFormatter, - description="[Flow] Issues multi-agent traffic light grid experiment", - epilog=EXAMPLE_USAGE) - - # required input parameters - parser.add_argument("--upload_dir", type=str, - help="S3 Bucket for uploading results.") + "horizontal_lanes": 1, + "vertical_lanes": 1, + }, + ), - # optional input parameters - parser.add_argument('--run_mode', type=str, default='local', - help="Experiment run mode (local | cluster)") - parser.add_argument('--algo', type=str, default='PPO', - help="RL method to use (PPO)") - parser.add_argument('--num_rows', type=int, default=3, - help="The number of rows in the traffic light grid network.") - parser.add_argument('--num_cols', type=int, default=3, - help="The number of columns in the traffic light grid network.") - parser.add_argument('--inflow_rate', type=int, default=300, - help="The inflow rate (veh/hr) per edge.") - args = parser.parse_args() + # vehicles to be placed in the network at the start of a rollout (see + # flow.core.params.VehicleParams) + veh=vehicles, - EDGE_INFLOW = args.inflow_rate # inflow rate of vehicles at every edge - N_ROWS = args.num_rows # number of row of bidirectional lanes - N_COLUMNS = args.num_cols # number of columns of bidirectional lanes + # parameters specifying the positioning of vehicles upon initialization + # or reset (see flow.core.params.InitialConfig) + initial=InitialConfig( + spacing='custom', + shuffle=True, + ), +) - flow_params = make_flow_params(N_ROWS, N_COLUMNS, EDGE_INFLOW) +create_env, env_name = make_create_env(params=flow_params, version=0) - upload_dir = args.upload_dir - RUN_MODE = args.run_mode - ALGO = args.algo +# Register as rllib env +register_env(env_name, create_env) - if ALGO == 'PPO': - alg_run, env_name, config = setup_exps_PPO(flow_params) - else: - raise NotImplementedError +test_env = create_env() +obs_space = test_env.observation_space +act_space = test_env.action_space - if RUN_MODE == 'local': - ray.init(num_cpus=N_CPUS + 1) - N_ITER = 1 - elif RUN_MODE == 'cluster': - ray.init(redis_address="localhost:6379") - N_ITER = 2000 +def gen_policy(): + """Generate a policy in RLlib.""" + return PPOTFPolicy, obs_space, act_space, {} - exp_tag = { - 'run': alg_run, - 'env': env_name, - 'checkpoint_freq': 25, - "max_failures": 10, - 'stop': { - 'training_iteration': N_ITER - }, - 'config': config, - "num_samples": 1, - } +# Setup PG with a single policy graph for all agents +POLICY_GRAPHS = {'av': gen_policy()} - if upload_dir: - exp_tag["upload_dir"] = "s3://{}".format(upload_dir) +def policy_mapping_fn(_): + """Map a policy in RLlib.""" + return 'av' - run_experiments( - { - flow_params["exp_tag"]: exp_tag - }, - ) +POLICIES_TO_TRAIN = ['av'] \ No newline at end of file diff --git a/tests/fast_tests/test_examples.py b/tests/fast_tests/test_examples.py index 2de1c685d..f5db49268 100644 --- a/tests/fast_tests/test_examples.py +++ b/tests/fast_tests/test_examples.py @@ -17,8 +17,7 @@ from examples.exp_configs.rl.multiagent.multiagent_ring import \ flow_params as multiagent_ring # from examples.exp_configs.rl.multiagent.multiagent_traffic_light_grid import setup_exps_PPO as multi_grid_setup -# from examples.exp_configs.rl.multiagent.multiagent_traffic_light_grid import \ -# make_flow_params as multi_grid_setup_flow_params +from examples.exp_configs.rl.multiagent.multiagent_traffic_light_grid import flow_params as multiagent_traffic_light_grid from examples.exp_configs.rl.multiagent.multiagent_highway import flow_params as multiagent_highway from examples.train_stable_baselines import run_model as run_stable_baselines_model @@ -193,7 +192,16 @@ def test_multi_ring(self): self.run_exp(multiagent_ring, **kwargs) def test_multi_grid(self): - pass # FIXME + from examples.exp_configs.rl.multiagent.multiagent_traffic_light_grid import POLICY_GRAPHS as mtlpg + from examples.exp_configs.rl.multiagent.multiagent_traffic_light_grid import POLICIES_TO_TRAIN as mtlpt + from examples.exp_configs.rl.multiagent.multiagent_traffic_light_grid import policy_mapping_fn as mtlpmf + + kwargs = { + "policy_graphs": mtlpg, + "policies_to_train": mtlpt, + "policy_mapping_fn": mtlpmf + } + self.run_exp(multiagent_traffic_light_grid, **kwargs) def test_multi_highway(self): from examples.exp_configs.rl.multiagent.multiagent_highway import POLICY_GRAPHS as mhpg From e46a5aa2c8b1ae4240462f39f5141a6bae46289d Mon Sep 17 00:00:00 2001 From: Ashkan Y Date: Fri, 27 Dec 2019 16:53:55 -0800 Subject: [PATCH 24/86] Imrpove naming of the tests in test_examples --- tests/fast_tests/test_examples.py | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/tests/fast_tests/test_examples.py b/tests/fast_tests/test_examples.py index f5db49268..a2715bc2a 100644 --- a/tests/fast_tests/test_examples.py +++ b/tests/fast_tests/test_examples.py @@ -8,7 +8,7 @@ from flow.core.experiment import Experiment from examples.exp_configs.rl.singleagent.singleagent_figure_eight import flow_params as singleagent_figure_eight -# from examples.exp_configs.rl.singleagent.green_wave import flow_params as singleagent_green_wave +from examples.exp_configs.rl.singleagent.singleagent_traffic_light_grid import flow_params as singleagent_traffic_light_grid from examples.exp_configs.rl.singleagent.singleagent_merge import flow_params as singleagent_merge from examples.exp_configs.rl.singleagent.singleagent_ring import flow_params as singleagent_ring from examples.exp_configs.rl.singleagent.singleagent_bottleneck import flow_params as singleagent_bottleneck @@ -16,7 +16,6 @@ from examples.exp_configs.rl.multiagent.multiagent_figure_eight import flow_params as multiagent_figure_eight from examples.exp_configs.rl.multiagent.multiagent_ring import \ flow_params as multiagent_ring -# from examples.exp_configs.rl.multiagent.multiagent_traffic_light_grid import setup_exps_PPO as multi_grid_setup from examples.exp_configs.rl.multiagent.multiagent_traffic_light_grid import flow_params as multiagent_traffic_light_grid from examples.exp_configs.rl.multiagent.multiagent_highway import flow_params as multiagent_highway @@ -123,19 +122,16 @@ def run_exp(flow_params): def test_singleagent_figure_eight(self): self.run_exp(singleagent_figure_eight) - def test_run_traffic_light_grid(self): - pass # FIXME - - def test_green_wave_inflows(self): + def test_singleagent_traffic_light_grid(self): pass # FIXME def test_singleagent_merge(self): self.run_exp(singleagent_merge) - def test_ring(self): + def test_singleagent_ring(self): self.run_exp(singleagent_ring) - def test_bottleneck(self): + def test_singleagent_bottleneck(self): self.run_exp(singleagent_bottleneck) @@ -154,19 +150,19 @@ def setUp(self): def test_singleagent_figure_eight(self): self.run_exp(singleagent_figure_eight) - def test_traffic_light_grid(self): + def test_singleagent_traffic_light_grid(self): pass # FIXME - def test_traffic_light_grid_inflows(self): + def test_singleagent_traffic_light_grid_inflows(self): pass # FIXME def test_singleagent_merge(self): self.run_exp(singleagent_merge) - def test_ring(self): + def test_singleagent_ring(self): self.run_exp(singleagent_ring) - def test_bottleneck(self): + def test_singleagent_bottleneck(self): self.run_exp(singleagent_bottleneck) def test_multi_figure_eight(self): @@ -191,7 +187,7 @@ def test_multi_ring(self): } self.run_exp(multiagent_ring, **kwargs) - def test_multi_grid(self): + def test_multi_traffic_light_grid(self): from examples.exp_configs.rl.multiagent.multiagent_traffic_light_grid import POLICY_GRAPHS as mtlpg from examples.exp_configs.rl.multiagent.multiagent_traffic_light_grid import POLICIES_TO_TRAIN as mtlpt from examples.exp_configs.rl.multiagent.multiagent_traffic_light_grid import policy_mapping_fn as mtlpmf From a876a55c8be3e10e0842bfdae72decc124c7d078 Mon Sep 17 00:00:00 2001 From: Radu Stochitoiu Date: Sun, 5 Jan 2020 15:49:11 +0200 Subject: [PATCH 25/86] Correct typo (#809) Modified "founded" to "found". --- tutorials/tutorial01_sumo.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tutorials/tutorial01_sumo.ipynb b/tutorials/tutorial01_sumo.ipynb index f7ae11cd5..6f8d2d2f7 100644 --- a/tutorials/tutorial01_sumo.ipynb +++ b/tutorials/tutorial01_sumo.ipynb @@ -158,7 +158,7 @@ "* **resolution**: resolution of the curves on the ring. Setting this value to 1 converts the ring to a diamond.\n", "\n", "\n", - "At times, other inputs may be needed from `NetParams` to recreate proper network features/behavior. These requirements can be founded in the network's documentation. For the ring road, no attributes are needed aside from the `additional_params` terms. Furthermore, for this exercise, we use the network's default parameters when creating the `NetParams` object." + "At times, other inputs may be needed from `NetParams` to recreate proper network features/behavior. These requirements can be found in the network's documentation. For the ring road, no attributes are needed aside from the `additional_params` terms. Furthermore, for this exercise, we use the network's default parameters when creating the `NetParams` object." ] }, { From 5bbbf88354219a5390e85c2a770e64c04beda79c Mon Sep 17 00:00:00 2001 From: Radu Stochitoiu Date: Sun, 5 Jan 2020 15:49:39 +0200 Subject: [PATCH 26/86] Correct typo (#810) --- tutorials/tutorial03_rllib.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tutorials/tutorial03_rllib.ipynb b/tutorials/tutorial03_rllib.ipynb index 9379fa008..016865e1b 100644 --- a/tutorials/tutorial03_rllib.ipynb +++ b/tutorials/tutorial03_rllib.ipynb @@ -26,7 +26,7 @@ "\n", "These parameters are explained in detail in exercise 1. Moreover, all parameters excluding vehicles (covered in section 2.2) do not change from the previous exercise. Accordingly, we specify them nearly as we have before, and leave further explanations of the parameters to exercise 1.\n", "\n", - "We begin by choosing the network the experiment will be trained on. We use one of Flow's builtin networks, located in `flow.networks`. A list of all available netowrks can be found by running the script below." + "We begin by choosing the network the experiment will be trained on. We use one of Flow's builtin networks, located in `flow.networks`. A list of all available networks can be found by running the script below." ] }, { From d2cd8934599e7f27e6cf39b6563519bdedd841f8 Mon Sep 17 00:00:00 2001 From: Ashkan Y Date: Sun, 5 Jan 2020 09:56:01 -0800 Subject: [PATCH 27/86] Update tutorials to match with the new way of running experiments (#802) * unused parameter * Update tutorials according to the new example folder * Adding final changes to the tutorials * Correct the env_name and network in flow_params * bug fix Co-authored-by: Aboudy Kreidieh --- .coveragerc | 2 + docs/source/index.rst | 2 +- .../exp_configs/non_rl/aimsun_template.py | 4 +- tutorials/README.md | 6 +- tutorials/tutorial01_sumo.ipynb | 43 ++++++++---- tutorials/tutorial02_aimsun.ipynb | 31 ++++++--- tutorials/tutorial03_rllib.ipynb | 8 +-- tutorials/tutorial04_visualize.ipynb | 2 +- tutorials/tutorial05_networks.ipynb | 25 +++++-- tutorials/tutorial06_osm.ipynb | 46 +++++++++++-- tutorials/tutorial07_network_templates.ipynb | 65 ++++++++++++++++--- tutorials/tutorial08_environments.ipynb | 28 ++++++-- tutorials/tutorial11_inflows.ipynb | 42 ++++++++++-- tutorials/tutorial12_bottlenecks.ipynb | 23 ++++++- 14 files changed, 260 insertions(+), 67 deletions(-) diff --git a/.coveragerc b/.coveragerc index 32deb76b5..769612895 100644 --- a/.coveragerc +++ b/.coveragerc @@ -13,6 +13,8 @@ omit = *aimsun/api.py *aimsun/generate.py *aimsun/load.py + *flow/multiagent_* + *flow/singleagent_* exclude_lines = if __name__ == .__main__.: diff --git a/docs/source/index.rst b/docs/source/index.rst index 7a187a0d3..2e1d42a23 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -9,7 +9,7 @@ Welcome to Flow `Flow `_ is a computational framework for deep RL and control experiments for traffic microsimulation. Visit `our website `_ for more information. Flow is a work in progress - input is welcome. Available documentation is limited for now. -`Tutorials `_ are available in iPython notebook format. +`Tutorials `_ are available in iPython notebook format. *If you are looking for Akvo Flow, their documentation can be found at* http://flowsupport.akvo.org. diff --git a/examples/exp_configs/non_rl/aimsun_template.py b/examples/exp_configs/non_rl/aimsun_template.py index e035074d9..7ab4fd13b 100644 --- a/examples/exp_configs/non_rl/aimsun_template.py +++ b/examples/exp_configs/non_rl/aimsun_template.py @@ -7,8 +7,6 @@ from flow.networks import Network import os -# inflow rate at the highway -FLOW_RATE = 2000 # no vehicles in the network vehicles = VehicleParams() @@ -31,7 +29,7 @@ # simulator that is used by the experiment simulator='aimsun', - # sumo-related parameters (see flow.core.params.SumoParams) + # Aimsun-related parameters sim=AimsunParams( sim_step=0.1, render=True, diff --git a/tutorials/README.md b/tutorials/README.md index a9845a686..d723f5607 100644 --- a/tutorials/README.md +++ b/tutorials/README.md @@ -20,10 +20,10 @@ cd /tutorials jupyter-notebook ``` -Instructions are written in each file. To do each exercise, first run all of +Instructions are written in each file. To do each tutorial, first run all of the cells in the Jupyter notebook. Then modify the ones that need to be modified in order to prevent any exceptions from being raised. Throughout these -exercises, you may find the +tutorials, you may find the [Flow documentation](https://flow.readthedocs.io/en/latest/) helpful. > **Common error:** if, when running a notebook, you run into an error of the form @@ -45,7 +45,7 @@ exercises, you may find the > ``` > (`cd /` is to make sure that `flow` is not in the folder you run `python` from). Then, in the Python interface that opens, run `import flow`. If you get an `ImportError`, this means you haven't installed Flow in your environment. Go back to the [installation instructions](https://flow.readthedocs.io/en/latest/flow_setup.html), especially the part where you do `pip install -e .` after having done `source activate flow`. -The content of each exercise is as follows: +The content of each tutorial is as follows: **Tutorial 0:** High-level introduction to Flow. diff --git a/tutorials/tutorial01_sumo.ipynb b/tutorials/tutorial01_sumo.ipynb index 6f8d2d2f7..53db96b13 100644 --- a/tutorials/tutorial01_sumo.ipynb +++ b/tutorials/tutorial01_sumo.ipynb @@ -6,15 +6,15 @@ "source": [ "# Tutorial 01: Running Sumo Simulations\n", "\n", - "This tutorial walks through the process of running non-RL traffic simulations in Flow. Simulations of this form act as non-autonomous baselines and depict the behavior of human dynamics on a network. Similar simulations may also be used to evaluate the performance of hand-designed controllers on a network. This tutorial focuses primarily on the former use case, while an example of the latter may be found in `exercise07_controllers.ipynb`.\n", + "This tutorial walks through the process of running non-RL traffic simulations in Flow. Simulations of this form act as non-autonomous baselines and depict the behavior of human dynamics on a network. Similar simulations may also be used to evaluate the performance of hand-designed controllers on a network. This tutorial focuses primarily on the former use case, while an example of the latter may be found in `tutorial09_controllers.ipynb`.\n", "\n", - "In this exercise, we simulate a initially perturbed single lane ring road. We witness in simulation that as time advances the initially perturbations do not dissipate, but instead propagates and expands until vehicles are forced to periodically stop and accelerate. For more information on this behavior, we refer the reader to the following article [1].\n", + "In this tutorial, we simulate a initially perturbed single lane ring road. We witness in simulation that as time advances the initially perturbations do not dissipate, but instead propagates and expands until vehicles are forced to periodically stop and accelerate. For more information on this behavior, we refer the reader to the following article [1].\n", "\n", "## 1. Components of a Simulation\n", "All simulations, both in the presence and absence of RL, require two components: a *network*, and an *environment*. Networks describe the features of the transportation network used in simulation. This includes the positions and properties of nodes and edges constituting the lanes and junctions, as well as properties of the vehicles, traffic lights, inflows, etc. in the network. Environments, on the other hand, initialize, reset, and advance simulations, and act the primary interface between the reinforcement learning algorithm and the network. Moreover, custom environments may be used to modify the dynamical features of an network.\n", "\n", "## 2. Setting up a Network\n", - "Flow contains a plethora of pre-designed networks used to replicate highways, intersections, and merges in both closed and open settings. All these networks are located in flow/networks. In order to recreate a ring road network, we begin by importing the network `RingNetwork`." + "Flow contains a plethora of pre-designed networks used to replicate highways, intersections, and merges in both closed and open settings. All these networks are located in `flow/networks`. In order to recreate a ring road network, we begin by importing the network `RingNetwork`." ] }, { @@ -150,7 +150,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Importing the `ADDITIONAL_NET_PARAMS` dict from the ring road network, we see that the required parameters are:\n", + "Importing the `ADDITIONAL_NET_PARAMS` dictionary from the ring road network, we see that the required parameters are:\n", "\n", "* **length**: length of the ring road\n", "* **lanes**: number of lanes\n", @@ -158,7 +158,7 @@ "* **resolution**: resolution of the curves on the ring. Setting this value to 1 converts the ring to a diamond.\n", "\n", "\n", - "At times, other inputs may be needed from `NetParams` to recreate proper network features/behavior. These requirements can be found in the network's documentation. For the ring road, no attributes are needed aside from the `additional_params` terms. Furthermore, for this exercise, we use the network's default parameters when creating the `NetParams` object." + "At times, other inputs may be needed from `NetParams` to recreate proper network features/behavior. These requirements can be found in the network's documentation. For the ring road, no attributes are needed aside from the `additional_params` terms. Furthermore, for this tutorial, we use the network's default parameters when creating the `NetParams` object." ] }, { @@ -198,7 +198,7 @@ "source": [ "### 2.5 TrafficLightParams\n", "\n", - "`TrafficLightParams` are used to describe the positions and types of traffic lights in the network. These inputs are outside the scope of this tutorial, and instead are covered in `exercise06_traffic_lights.ipynb`. For our example, we create an empty `TrafficLightParams` object, thereby ensuring that none are placed on any nodes." + "`TrafficLightParams` are used to describe the positions and types of traffic lights in the network. These inputs are outside the scope of this tutorial, and instead are covered in `tutorial10_traffic_lights.ipynb`. For our example, we create an empty `TrafficLightParams` object, thereby ensuring that none are placed on any nodes." ] }, { @@ -218,7 +218,7 @@ "source": [ "## 3. Setting up an Environment\n", "\n", - "Several envionrments in Flow exist to train autonomous agents of different forms (e.g. autonomous vehicles, traffic lights) to perform a variety of different tasks. These environments are often network or task specific; however, some can be deployed on an ambiguous set of networks as well. One such environment, `AccelEnv`, may be used to train a variable number of vehicles in a fully observable network with a *static* number of vehicles." + "Several envionrments in Flow exist to train autonomous agents of different forms (e.g. autonomous vehicles, traffic lights) to perform a variety of different tasks. These environments are often network- or task-specific; however, some can be deployed on an ambiguous set of networks as well. One such environment, `AccelEnv`, may be used to train a variable number of vehicles in a fully observable network with a *static* number of vehicles." ] }, { @@ -234,7 +234,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Although we will not be training any autonomous agents in this exercise, the use of an environment allows us to view the cumulative reward simulation rollouts receive in the absence of autonomy.\n", + "Although we will not be training any autonomous agents in this tutorial, the use of an environment allows us to view the cumulative reward simulation rollouts receive in the absence of autonomy.\n", "\n", "Envrionments in Flow are parametrized by three components:\n", "* `EnvParams`\n", @@ -264,7 +264,7 @@ "source": [ "### 3.2 EnvParams\n", "\n", - "`EnvParams` specify environment and experiment-specific parameters that either affect the training process or the dynamics of various components within the network. Much like `NetParams`, the attributes associated with this parameter are mostly environment specific, and can be found in the environment's `ADDITIONAL_ENV_PARAMS` dictionary." + "`EnvParams` specify environment and experiment-specific parameters that either affect the training process or the dynamics of various components within the network. Much like `NetParams`, the attributes associated with this parameter are mostly environment-specific, and can be found in the environment's `ADDITIONAL_ENV_PARAMS` dictionary." ] }, { @@ -317,7 +317,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "These objects may be used to simulate rollouts in the absence of reinforcement learning agents, as well as acquire behaviors and rewards that may be used as a baseline with which to compare the performance of the learning agent. In this case, we choose to run our experiment for one rollout consisting of 3000 steps (300 s).\n", + "This object may be used to simulate rollouts in the absence of reinforcement learning agents, as well as acquire behaviors and rewards that may be used as a baseline with which to compare the performance of the learning agent. In this case, we choose to run our experiment for one rollout consisting of 3000 steps (300 s).\n", "\n", "**Note**: When executing the below code, remeber to click on the Play button after the GUI is rendered." ] @@ -338,11 +338,26 @@ "# create the environment object\n", "env = AccelEnv(env_params, sumo_params, network)\n", "\n", - "# create the experiment object\n", - "exp = Experiment(env)\n", + "flow_params = dict(\n", + " exp_tag='ring_example',\n", + " env_name=AccelEnv,\n", + " network=RingNetwork,\n", + " simulator='traci',\n", + " sim=sumo_params,\n", + " env=env_params,\n", + " net=net_params,\n", + " veh=vehicles,\n", + " initial=initial_config,\n", + " tls=traffic_lights,\n", + ")\n", + "\n", + "# number of time steps\n", + "flow_params['env'].horizon = 3000\n", + "exp = Experiment(flow_params)\n", + "exp.env - env\n", "\n", - "# run the experiment for a set number of rollouts / time steps\n", - "_ = exp.run(1, 3000, convert_to_csv=True)" + "# run the sumo simulation\n", + "_ = exp.run(1, convert_to_csv=True)" ] }, { diff --git a/tutorials/tutorial02_aimsun.ipynb b/tutorials/tutorial02_aimsun.ipynb index 737955e29..99c3352ee 100644 --- a/tutorials/tutorial02_aimsun.ipynb +++ b/tutorials/tutorial02_aimsun.ipynb @@ -12,7 +12,7 @@ "\n", "Simulations of this form act as non-autonomous baselines and depict the behavior of human dynamics on a network. Similar simulations may also be used to evaluate the performance of hand-designed controllers on a network. This tutorial focuses primarily on the former use case, while an example of the latter may be found in `tutorial10_controllers.ipynb`.\n", "\n", - "In this exercise, we simulate an initially perturbed single lane ring road. We witness in simulation that as time advances, the initial perturbations do not dissipate, but instead propagate and expand until vehicles are forced to periodically stop and accelerate. For more information on this behavior, we refer the reader to the following article [1].\n", + "In this tutorial, we simulate an initially perturbed single lane ring road. We witness in simulation that as time advances, the initial perturbations do not dissipate, but instead propagate and expand until vehicles are forced to periodically stop and accelerate. For more information on this behavior, we refer the reader to the following article [1].\n", "\n", "## 1. Components of a Simulation\n", "All simulations, both in the presence and absence of RL, require two components: a *network*, and an *environment*. Networks describe the features of the transportation network used in simulation. This includes the positions and properties of nodes and edges constituting the lanes and junctions, as well as properties of the vehicles, traffic lights, inflows, etc. in the network. Environments, on the other hand, initialize, reset, and advance simulations, and act the primary interface between the reinforcement learning algorithm and the network. Moreover, custom environments may be used to modify the dynamical features of an network.\n", @@ -147,7 +147,7 @@ "* **resolution**: resolution of the curves on the ring. Setting this value to 1 converts the ring to a diamond.\n", "\n", "\n", - "At times, other inputs may be needed from `NetParams` to recreate proper network features/behavior. These requirements can be founded in the network's documentation. For the ring road, no attributes are needed aside from the `additional_params` terms. Furthermore, for this exercise, we use the network's default parameters when creating the `NetParams` object." + "At times, other inputs may be needed from `NetParams` to recreate proper network features/behavior. These requirements can be founded in the network's documentation. For the ring road, no attributes are needed aside from the `additional_params` terms. Furthermore, for this tutorial, we use the network's default parameters when creating the `NetParams` object." ] }, { @@ -187,7 +187,7 @@ "source": [ "### 2.5 TrafficLightParams\n", "\n", - "`TrafficLightParams` are used to desribe the positions and types of traffic lights in the network. These inputs are outside the scope of this tutorial, and instead are covered in `exercise06_traffic_lights.ipynb`. For our example, we create an empty `TrafficLightParams` object, thereby ensuring that none are placed on any nodes." + "`TrafficLightParams` are used to desribe the positions and types of traffic lights in the network. These inputs are outside the scope of this tutorial, and instead are covered in `tutorial10_traffic_lights.ipynb`. For our example, we create an empty `TrafficLightParams` object, thereby ensuring that none are placed on any nodes." ] }, { @@ -242,7 +242,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Although we will not be training any autonomous agents in this exercise, the use of an environment allows us to view the cumulative reward simulation rollouts receive in the absence of autonomy.\n", + "Although we will not be training any autonomous agents in this tutorial, the use of an environment allows us to view the cumulative reward simulation rollouts receive in the absence of autonomy.\n", "\n", "Envrionments in Flow are parametrized by three components:\n", "* `EnvParams`\n", @@ -347,11 +347,26 @@ "# create the environment object\n", "env = AccelEnv(env_params, sim_params, network, simulator='aimsun')\n", "\n", - "# create the experiment object\n", - "exp = Experiment(env)\n", + "flow_params = dict(\n", + " exp_tag='test_network',\n", + " env_name=AccelEnv,\n", + " network=RingNetwork,\n", + " simulator='aimsun',\n", + " sim=sim_params,\n", + " env=env_params,\n", + " net=net_params,\n", + " veh=vehicles,\n", + " initial=initial_config,\n", + " tls=traffic_lights\n", + ")\n", + "\n", + "# number of time steps\n", + "flow_params['env'].horizon = 3000\n", + "exp = Experiment(flow_params)\n", + "exp.env = env\n", "\n", - "# run the experiment for a set number of rollouts / time steps\n", - "_ = exp.run(1, 3000)" + "# run the Aimsun simulation\n", + "_ = exp.run(1)" ] }, { diff --git a/tutorials/tutorial03_rllib.ipynb b/tutorials/tutorial03_rllib.ipynb index 016865e1b..22139c270 100644 --- a/tutorials/tutorial03_rllib.ipynb +++ b/tutorials/tutorial03_rllib.ipynb @@ -8,13 +8,13 @@ "\n", "This tutorial walks you through the process of running traffic simulations in Flow with trainable RLlib-powered agents. Autonomous agents will learn to maximize a certain reward over the rollouts, using the [**RLlib**](https://ray.readthedocs.io/en/latest/rllib.html) library ([citation](https://arxiv.org/abs/1712.09381)) ([installation instructions](https://flow.readthedocs.io/en/latest/flow_setup.html#optional-install-ray-rllib)). Simulations of this form will depict the propensity of RL agents to influence the traffic of a human fleet in order to make the whole fleet more efficient (for some given metrics). \n", "\n", - "In this exercise, we simulate an initially perturbed single lane ring road, where we introduce a single autonomous vehicle. We witness that, after some training, that the autonomous vehicle learns to dissipate the formation and propagation of \"phantom jams\" which form when only human driver dynamics are involved.\n", + "In this tutorial, we simulate an initially perturbed single lane ring road, where we introduce a single autonomous vehicle. We witness that, after some training, that the autonomous vehicle learns to dissipate the formation and propagation of \"phantom jams\" which form when only human driver dynamics are involved.\n", "\n", "## 1. Components of a Simulation\n", "All simulations, both in the presence and absence of RL, require two components: a *network*, and an *environment*. Networks describe the features of the transportation network used in simulation. This includes the positions and properties of nodes and edges constituting the lanes and junctions, as well as properties of the vehicles, traffic lights, inflows, etc... in the network. Environments, on the other hand, initialize, reset, and advance simulations, and act as the primary interface between the reinforcement learning algorithm and the network. Moreover, custom environments may be used to modify the dynamical features of an network. Finally, in the RL case, it is in the *environment* that the state/action spaces and the reward function are defined. \n", "\n", "## 2. Setting up a Network\n", - "Flow contains a plethora of pre-designed networks used to replicate highways, intersections, and merges in both closed and open settings. All these networks are located in flow/networks. For this exercise, which involves a single lane ring road, we will use the network `RingNetwork`.\n", + "Flow contains a plethora of pre-designed networks used to replicate highways, intersections, and merges in both closed and open settings. All these networks are located in flow/networks. For this tutorial, which involves a single lane ring road, we will use the network `RingNetwork`.\n", "\n", "### 2.1 Setting up Network Parameters\n", "\n", @@ -24,7 +24,7 @@ "* net_params\n", "* initial_config\n", "\n", - "These parameters are explained in detail in exercise 1. Moreover, all parameters excluding vehicles (covered in section 2.2) do not change from the previous exercise. Accordingly, we specify them nearly as we have before, and leave further explanations of the parameters to exercise 1.\n", + "These parameters are explained in detail in `tutorial01_sumo.ipynb`. Moreover, all parameters excluding vehicles (covered in section 2.2) do not change from the previous tutorial. Accordingly, we specify them nearly as we have before, and leave further explanations of the parameters to `tutorial01_sumo.ipynb`.\n", "\n", "We begin by choosing the network the experiment will be trained on. We use one of Flow's builtin networks, located in `flow.networks`. A list of all available networks can be found by running the script below." ] @@ -95,7 +95,7 @@ "\n", "The dynamics of vehicles in the `Vehicles` class can either be depicted by sumo or by the dynamical methods located in flow/controllers. For human-driven vehicles, we use the IDM model for acceleration behavior, with exogenous gaussian acceleration noise with std 0.2 m/s2 to induce perturbations that produce stop-and-go behavior. In addition, we use the `ContinousRouter` routing controller so that the vehicles may maintain their routes closed networks.\n", "\n", - "As we have done in exercise 1, human-driven vehicles are defined in the `VehicleParams` class as follows:" + "As we have done in `tutorial01_sumo.ipynb`, human-driven vehicles are defined in the `VehicleParams` class as follows:" ] }, { diff --git a/tutorials/tutorial04_visualize.ipynb b/tutorials/tutorial04_visualize.ipynb index b629123e6..1331d0e3b 100644 --- a/tutorials/tutorial04_visualize.ipynb +++ b/tutorials/tutorial04_visualize.ipynb @@ -121,7 +121,7 @@ "Then, you have to tell Flow to convert these XML emission files into CSV files. To do that, pass in `convert_to_csv=True` to the `run` method of your experiment object. For instance:\n", "\n", "```python\n", - "exp.run(1, 1500, convert_to_csv=True)\n", + "exp.run(1, convert_to_csv=True)\n", "```" ] }, diff --git a/tutorials/tutorial05_networks.ipynb b/tutorials/tutorial05_networks.ipynb index ecbd1e703..7d1e70575 100644 --- a/tutorials/tutorial05_networks.ipynb +++ b/tutorials/tutorial05_networks.ipynb @@ -8,7 +8,7 @@ "\n", "This tutorial walks you through the process of generating custom networks. Networks define the network geometry of a task, as well as the constituents of the network, e.g. vehicles, traffic lights, etc... Various networks are available in Flow, depicting a diverse set of open and closed traffic networks such as ring roads, intersections, traffic light grids, straight highway merges, and more. \n", "\n", - "In this exercise, we will recreate the ring road network, seen in the figure below.\n", + "In this tutorial, we will recreate the ring road network, seen in the figure below.\n", "\n", "\n", "\n", @@ -370,7 +370,7 @@ "metadata": {}, "source": [ "## 3. Testing the New Network\n", - "In this section, we run a new sumo simulation using our newly generated network class. For information on running sumo experiments, see `exercise01_sumo.ipynb`.\n", + "In this section, we run a new sumo simulation using our newly generated network class. For information on running sumo experiments, see `tutorial01_sumo.ipynb`.\n", "\n", "We begin by defining some of the components needed to run a sumo experiment." ] @@ -456,10 +456,25 @@ "# AccelEnv allows us to test any newly generated network quickly\n", "env = AccelEnv(env_params, sumo_params, network)\n", "\n", - "exp = Experiment(env)\n", + "flow_params = dict(\n", + " exp_tag='test_network',\n", + " env_name=AccelEnv,\n", + " network=myNetwork,\n", + " simulator='traci',\n", + " sim=sumo_params,\n", + " env=env_params,\n", + " net=net_params,\n", + " veh=vehicles,\n", + " initial=initial_config,\n", + ")\n", + "\n", + "# number of time steps\n", + "flow_params['env'].horizon = 1500\n", + "exp = Experiment(flow_params)\n", + "exp.env = env\n", "\n", - "# run the sumo simulation for a set number of time steps\n", - "_ = exp.run(1, 1500)" + "# run the sumo simulation\n", + "_ = exp.run(1)" ] }, { diff --git a/tutorials/tutorial06_osm.ipynb b/tutorials/tutorial06_osm.ipynb index cb9ae749d..a721c9005 100644 --- a/tutorials/tutorial06_osm.ipynb +++ b/tutorials/tutorial06_osm.ipynb @@ -122,9 +122,25 @@ " network=network\n", ")\n", "\n", - "# run the simulation for 1000 steps\n", - "exp = Experiment(env=env)\n", - "exp.run(1, 1000)" + "flow_params = dict(\n", + " exp_tag='bay_bridge',\n", + " env_name=TestEnv,\n", + " network=Network,\n", + " simulator='traci',\n", + " sim=sim_params,\n", + " env=env_params,\n", + " net=net_params,\n", + " veh=vehicles,\n", + " initial=initial_config,\n", + ")\n", + "\n", + "# number of time steps\n", + "flow_params['env'].horizon = 1000\n", + "exp = Experiment(flow_params)\n", + "exp.env = env\n", + "\n", + "# run the sumo simulation\n", + "exp.run(1)" ] }, { @@ -256,7 +272,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### 2.3 Rerunning the SImulation\n", + "### 2.3 Rerunning the Simulation\n", "\n", "We are now ready to rerun the simulation with fully defined vehicle routes and a limited number of traversable edges. If we run the cell below, we can see the new simulation in action." ] @@ -282,9 +298,25 @@ " network=new_network\n", ")\n", "\n", - "# run the simulation for 1000 steps\n", - "exp = Experiment(env=env)\n", - "exp.run(1, 10000)" + "flow_params = dict(\n", + " exp_tag='bay_bridge',\n", + " env_name=TestEnv,\n", + " network=BayBridgeOSMNetwork,\n", + " simulator='traci',\n", + " sim=sim_params,\n", + " env=env_params,\n", + " net=net_params,\n", + " veh=vehicles,\n", + " initial=new_initial_config,\n", + ")\n", + "\n", + "# number of time steps\n", + "flow_params['env'].horizon = 10000\n", + "exp = Experiment(flow_params)\n", + "exp.env = env\n", + "\n", + "# run the sumo simulation\n", + "exp.run(1)" ] }, { diff --git a/tutorials/tutorial07_network_templates.ipynb b/tutorials/tutorial07_network_templates.ipynb index 0eae4a71c..9d3da420b 100644 --- a/tutorials/tutorial07_network_templates.ipynb +++ b/tutorials/tutorial07_network_templates.ipynb @@ -171,9 +171,25 @@ " network=network\n", ")\n", "\n", - "# run the simulation for 1000 steps\n", - "exp = Experiment(env=env)\n", - "_ = exp.run(1, 1000)" + "flow_params = dict(\n", + " exp_tag='template',\n", + " env_name=TestEnv,\n", + " network=TemplateNetwork,\n", + " simulator='traci',\n", + " sim=sim_params,\n", + " env=env_params,\n", + " net=net_params,\n", + " veh=vehicles,\n", + " initial=initial_config,\n", + ")\n", + "\n", + "# number of time steps\n", + "flow_params['env'].horizon = 1000\n", + "exp = Experiment(flow_params)\n", + "exp.env = env\n", + "\n", + "# run the sumo simulation\n", + "_ = exp.run(1)" ] }, { @@ -294,9 +310,25 @@ " network=network\n", ")\n", "\n", - "# run the simulation for 100000 steps\n", - "exp = Experiment(env=env)\n", - "_ = exp.run(1, 100000)" + "flow_params = dict(\n", + " exp_tag='template',\n", + " env_name=TestEnv,\n", + " network=Network,\n", + " simulator='traci',\n", + " sim=sim_params,\n", + " env=env_params,\n", + " net=new_net_params,\n", + " veh=new_vehicles,\n", + " initial=initial_config,\n", + ")\n", + "\n", + "# number of time steps\n", + "flow_params['env'].horizon = 100000\n", + "exp = Experiment(flow_params)\n", + "exp.env = env\n", + "\n", + "# run the sumo simulation\n", + "_ = exp.run(1)" ] }, { @@ -402,8 +434,25 @@ " simulator='aimsun' \n", ")\n", "\n", - "exp = Experiment(env)\n", - "exp.run(1, 1000)" + "flow_params = dict(\n", + " exp_tag='template',\n", + " env_name=TestEnv,\n", + " network=Network,\n", + " simulator='aimsun',\n", + " sim=sim_params,\n", + " env=env_params,\n", + " net=net_params,\n", + " veh=vehicles,\n", + " initial=initial_config,\n", + ")\n", + "\n", + "# number of time steps\n", + "flow_params['env'].horizon = 1000\n", + "exp = Experiment(flow_params)\n", + "exp.env = env\n", + "\n", + "# run the sumo simulation\n", + "_ = exp.run(1)" ] } ], diff --git a/tutorials/tutorial08_environments.ipynb b/tutorials/tutorial08_environments.ipynb index 16459a8bd..cf8360c62 100644 --- a/tutorials/tutorial08_environments.ipynb +++ b/tutorials/tutorial08_environments.ipynb @@ -15,7 +15,7 @@ "\n", "## 1. Creating an Environment Class\n", "\n", - "In this exercise we will create an environment in which the accelerations of a handful of vehicles in the network are specified by a single centralized agent, with the objective of the agent being to improve the average speed of all vehicle in the network. In order to create this environment, we begin by inheriting the base environment class located in *flow.envs*:" + "In this tutorial we will create an environment in which the accelerations of a handful of vehicles in the network are specified by a single centralized agent, with the objective of the agent being to improve the average speed of all vehicle in the network. In order to create this environment, we begin by inheriting the base environment class located in *flow.envs*:" ] }, { @@ -90,7 +90,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "In addition, `Tuple` objects (not used by this exercise) allow users to combine multiple `Box` elements together." + "In addition, `Tuple` objects (not used by this tutorial) allow users to combine multiple `Box` elements together." ] }, { @@ -229,7 +229,7 @@ "\n", "The `compute_reward` method returns the reward associated with any given state. These value may encompass returns from values within the state space (defined in section 1.5) or may contain information provided by the environment but not immediately available within the state, as is the case in partially observable tasks (or POMDPs).\n", "\n", - "For this exercise, we choose the reward function to be the average speed of all vehicles currently in the network. In order to extract this information from the environment, we use the `get_speed` method within the Vehicle kernel class to collect the current speed of all vehicles in the network, and return the average of these speeds as the reward. This is done as follows:" + "For this tutorial, we choose the reward function to be the average speed of all vehicles currently in the network. In order to extract this information from the environment, we use the `get_speed` method within the Vehicle kernel class to collect the current speed of all vehicles in the network, and return the average of these speeds as the reward. This is done as follows:" ] }, { @@ -303,9 +303,25 @@ "env = myEnv(env_params, sumo_params, network)\n", "#############################################################\n", "\n", - "exp = Experiment(env)\n", - "\n", - "_ = exp.run(1, 1500)" + "flow_params = dict(\n", + " exp_tag='ring',\n", + " env_name=myEnv,\n", + " network=RingNetwork,\n", + " simulator='traci',\n", + " sim=sumo_params,\n", + " env=env_params,\n", + " net=net_params,\n", + " veh=vehicles,\n", + " initial=initial_config,\n", + ")\n", + "\n", + "# number of time steps\n", + "flow_params['env'].horizon = 1500\n", + "exp = Experiment(flow_params)\n", + "exp.env = env\n", + "\n", + "# run the sumo simulation\n", + "_ = exp.run(1)" ] }, { diff --git a/tutorials/tutorial11_inflows.ipynb b/tutorials/tutorial11_inflows.ipynb index 3b841123d..8f950d961 100644 --- a/tutorials/tutorial11_inflows.ipynb +++ b/tutorials/tutorial11_inflows.ipynb @@ -235,9 +235,25 @@ "\n", "env = AccelEnv(env_params, sumo_params, network)\n", "\n", - "exp = Experiment(env)\n", + "flow_params = dict(\n", + " exp_tag='merge-example',\n", + " env_name=AccelEnv,\n", + " network=MergeNetwork,\n", + " simulator='traci',\n", + " sim=sumo_params,\n", + " env=env_params,\n", + " net=net_params,\n", + " veh=vehicles,\n", + " initial=initial_config,\n", + ")\n", "\n", - "_ = exp.run(1, 10000)" + "# number of time steps\n", + "flow_params['env'].horizon = 10000\n", + "exp = Experiment(flow_params)\n", + "exp.env = env\n", + "\n", + "# run the sumo simulation\n", + "_ = exp.run(1)" ] }, { @@ -474,9 +490,25 @@ "\n", "env = AccelEnv(env_params, sim_params, network)\n", "\n", - "exp = Experiment(env)\n", - "\n", - "_ = exp.run(1, 10000)" + "flow_params = dict(\n", + " exp_tag='merge-example',\n", + " env_name=AccelEnv,\n", + " network=MergeNetwork,\n", + " simulator='traci',\n", + " sim=sim_params,\n", + " env=env_params,\n", + " net=net_params,\n", + " veh=vehicles,\n", + " initial=initial_config,\n", + ")\n", + "\n", + "# number of time steps\n", + "flow_params['env'].horizon = 10000\n", + "exp = Experiment(flow_params)\n", + "exp.env = env\n", + "\n", + "# run the sumo simulation\n", + "_ = exp.run(1)" ] } ], diff --git a/tutorials/tutorial12_bottlenecks.ipynb b/tutorials/tutorial12_bottlenecks.ipynb index 741390976..53d660b67 100644 --- a/tutorials/tutorial12_bottlenecks.ipynb +++ b/tutorials/tutorial12_bottlenecks.ipynb @@ -137,8 +137,27 @@ " # The environment that defines the Markov decision process of our system\n", " env = BottleneckEnv(env_params, sim_params, network)\n", "\n", - " \n", - " Experiment(env).run(1, 1000)" + " \n", + " flow_params = dict(\n", + " exp_tag='bay_bridge_toll',\n", + " env_name=BottleneckEnv,\n", + " network=BottleneckNetwork,\n", + " simulator='traci',\n", + " sim=sim_params,\n", + " env=env_params,\n", + " net=net_params,\n", + " veh=vehicles,\n", + " initial=initial_config,\n", + " tls=traffic_lights,\n", + " )\n", + "\n", + " # number of time steps\n", + " flow_params['env'].horizon = 1000\n", + " exp = Experiment(flow_params)\n", + " exp.env = env\n", + "\n", + " # run the sumo simulation\n", + " _ = exp.run(1)" ] }, { From 824c613bbc79daccee21e5ba7acc24fd6e99daf1 Mon Sep 17 00:00:00 2001 From: Ashkan Y Date: Sun, 5 Jan 2020 09:56:47 -0800 Subject: [PATCH 28/86] Fix sim_params in the tutorials (#807) * Update tutorials * Update all tutorials * Apply suggestions from code review Co-authored-by: Aboudy Kreidieh --- tutorials/tutorial01_sumo.ipynb | 26 +++++++++++++++++++++---- tutorials/tutorial02_aimsun.ipynb | 17 +++++++++++++--- tutorials/tutorial03_rllib.ipynb | 16 +++++++-------- tutorials/tutorial04_visualize.ipynb | 2 +- tutorials/tutorial05_networks.ipynb | 4 ++-- tutorials/tutorial08_environments.ipynb | 8 ++++---- tutorials/tutorial11_inflows.ipynb | 4 ++-- 7 files changed, 53 insertions(+), 24 deletions(-) diff --git a/tutorials/tutorial01_sumo.ipynb b/tutorials/tutorial01_sumo.ipynb index 53db96b13..e598cd673 100644 --- a/tutorials/tutorial01_sumo.ipynb +++ b/tutorials/tutorial01_sumo.ipynb @@ -236,12 +236,30 @@ "source": [ "Although we will not be training any autonomous agents in this tutorial, the use of an environment allows us to view the cumulative reward simulation rollouts receive in the absence of autonomy.\n", "\n", - "Envrionments in Flow are parametrized by three components:\n", - "* `EnvParams`\n", + "Although we will not be training any autonomous agents in this exercise, the use of an environment allows us to view the cumulative reward simulation rollouts receive in the absence of autonomy.\n", + "\n", + "Envrionments in Flow are parametrized by several components, including the following attributes:\n", + "* `sim_params`\n", + "* `env_params`\n", + "* `network`\n", + "* `net_params`\n", + "* `initial_config`\n", + "* `network`\n", + "* `simulator`\n", + "\n", + "where `sim_params`, `env_params`, and `network` are the primary parameters of an environment. For the full list of attributes, please check `class Env` in `flow/envs/base.py`.\n", + "\n", + "Sumo envrionments in Flow are parametrized by three components:\n", "* `SumoParams`\n", + "* `EnvParams`\n", "* `Network`\n", "\n", "### 3.1 SumoParams\n", + "`SumoParams` specifies simulation-specific variables (e.g. `SumoParams` and `AimsunParams` are the variables related to SUMO and Aimsun simulator, respectively). These variables maay include the length a simulation step (in seconds), whether to render the GUI when running the experiment, and other variables. For this example, we consider a SUMO simulation, step length of 0.1s, and activate the GUI.\n", + "\n", + "Another useful parameter is `emission_path`, which is used to specify the path where the emissions output will be generated. They contain a lot of information about the simulation, for instance the position and speed of each car at each time step. If you do not specify any emission path, the emission file will not be generated. More on this in Section 5.\n", + "\n", + "### 3.1 SumoParams\n", "`SumoParams` specifies simulation-specific variables. These variables include the length a simulation step (in seconds) and whether to render the GUI when running the experiment. For this example, we consider a simulation step length of 0.1s and activate the GUI.\n", "\n", "Another useful parameter is `emission_path`, which is used to specify the path where the emissions output will be generated. They contain a lot of information about the simulation, for instance the position and speed of each car at each time step. If you do not specify any emission path, the emission file will not be generated. More on this in Section 5." @@ -255,7 +273,7 @@ "source": [ "from flow.core.params import SumoParams\n", "\n", - "sumo_params = SumoParams(sim_step=0.1, render=True, emission_path='data')" + "sim_params = SumoParams(sim_step=0.1, render=True, emission_path='data')" ] }, { @@ -336,7 +354,7 @@ " traffic_lights=traffic_lights)\n", "\n", "# create the environment object\n", - "env = AccelEnv(env_params, sumo_params, network)\n", + "env = AccelEnv(env_params, sim_params, network)\n", "\n", "flow_params = dict(\n", " exp_tag='ring_example',\n", diff --git a/tutorials/tutorial02_aimsun.ipynb b/tutorials/tutorial02_aimsun.ipynb index 99c3352ee..9b3dae34c 100644 --- a/tutorials/tutorial02_aimsun.ipynb +++ b/tutorials/tutorial02_aimsun.ipynb @@ -244,13 +244,24 @@ "source": [ "Although we will not be training any autonomous agents in this tutorial, the use of an environment allows us to view the cumulative reward simulation rollouts receive in the absence of autonomy.\n", "\n", - "Envrionments in Flow are parametrized by three components:\n", - "* `EnvParams`\n", + "Envrionments in Flow are parametrized by several components, including the following attributes:\n", + "* `sim_params`\n", + "* `env_params`\n", + "* `network`\n", + "* `net_params`\n", + "* `initial_config`\n", + "* `network`\n", + "* `simulator`\n", + "\n", + "where `sim_params`, `env_params`, and `network` the are primary parameters of an environment. For the full list of attributes, please check `class Env` in `flow/envs/base.py`.\n", + "\n", + "Aimsun envrionments in Flow are parametrized by three components:\n", "* `AimsunParams`\n", + "* `EnvParams`\n", "* `Network`\n", "\n", "### 3.1 AimsunParams\n", - "`AimsunParams` specifies simulation-specific variables. These variables include the length a simulation step (in seconds) and whether to render the GUI when running the experiment. For this example, we consider a simulation step length of 0.1s and set `render` within the simulation params to be True in order for vehicles to appear on the GUI during the simulation." + "`AimsunParams` specifies simulation-specific variables. These variables include the length a simulation step (in seconds) and whether to render the GUI when running the experiment. For this example, we set `render` within the simulation params to be True in order for vehicles to appear on the GUI during the simulation." ] }, { diff --git a/tutorials/tutorial03_rllib.ipynb b/tutorials/tutorial03_rllib.ipynb index 22139c270..465fa362b 100644 --- a/tutorials/tutorial03_rllib.ipynb +++ b/tutorials/tutorial03_rllib.ipynb @@ -162,13 +162,13 @@ "\n", "Several environments in Flow exist to train RL agents of different forms (e.g. autonomous vehicles, traffic lights) to perform a variety of different tasks. The use of an environment allows us to view the cumulative reward simulation rollouts receive, along with to specify the state/action spaces.\n", "\n", - "Envrionments in Flow are parametrized by three components:\n", - "* env_params\n", - "* sumo_params\n", - "* network\n", + "Sumo envrionments in Flow are parametrized by three components:\n", + "* `SumoParams`\n", + "* `EnvParams`\n", + "* `Network`\n", "\n", "### 3.1 SumoParams\n", - "`SumoParams` specifies simulation-specific variables. These variables include the length of any simulation step and whether to render the GUI when running the experiment. For this example, we consider a simulation step length of 0.1s and activate the GUI. \n", + "`SumoParams` specifies simulation-specific variables. These variables include the length of any simulation step and whether to render the GUI when running the experiment. For this example, we consider a simulation step length of 0.1s and deactivate the GUI. \n", "\n", "**Note** For training purposes, it is highly recommanded to deactivate the GUI in order to avoid global slow down. In such case, one just needs to specify the following: `render=False`" ] @@ -181,7 +181,7 @@ "source": [ "from flow.core.params import SumoParams\n", "\n", - "sumo_params = SumoParams(sim_step=0.1, render=False)" + "sim_params = SumoParams(sim_step=0.1, render=False)" ] }, { @@ -287,8 +287,8 @@ " network=network_name,\n", " # simulator that is used by the experiment\n", " simulator='traci',\n", - " # sumo-related parameters (see flow.core.params.SumoParams)\n", - " sim=sumo_params,\n", + " # simulation-related parameters\n", + " sim=sim_params,\n", " # environment related parameters (see flow.core.params.EnvParams)\n", " env=env_params,\n", " # network-related parameters (see flow.core.params.NetParams and\n", diff --git a/tutorials/tutorial04_visualize.ipynb b/tutorials/tutorial04_visualize.ipynb index 1331d0e3b..f31a31fdf 100644 --- a/tutorials/tutorial04_visualize.ipynb +++ b/tutorials/tutorial04_visualize.ipynb @@ -109,7 +109,7 @@ "source": [ "from flow.core.params import SumoParams\n", "\n", - "sumo_params = SumoParams(sim_step=0.1, render=True, emission_path='data')" + "sim_params = SumoParams(sim_step=0.1, render=True, emission_path='data')" ] }, { diff --git a/tutorials/tutorial05_networks.ipynb b/tutorials/tutorial05_networks.ipynb index 7d1e70575..653ee099d 100644 --- a/tutorials/tutorial05_networks.ipynb +++ b/tutorials/tutorial05_networks.ipynb @@ -391,7 +391,7 @@ " routing_controller=(ContinuousRouter, {}),\n", " num_vehicles=22)\n", "\n", - "sumo_params = SumoParams(sim_step=0.1, render=True)\n", + "sim_params = SumoParams(sim_step=0.1, render=True)\n", "\n", "initial_config = InitialConfig(bunching=40)" ] @@ -454,7 +454,7 @@ ")\n", "\n", "# AccelEnv allows us to test any newly generated network quickly\n", - "env = AccelEnv(env_params, sumo_params, network)\n", + "env = AccelEnv(env_params, sim_params, network)\n", "\n", "flow_params = dict(\n", " exp_tag='test_network',\n", diff --git a/tutorials/tutorial08_environments.ipynb b/tutorials/tutorial08_environments.ipynb index cf8360c62..2f08e27ed 100644 --- a/tutorials/tutorial08_environments.ipynb +++ b/tutorials/tutorial08_environments.ipynb @@ -277,7 +277,7 @@ "from flow.core.params import VehicleParams\n", "from flow.networks.ring import RingNetwork, ADDITIONAL_NET_PARAMS\n", "\n", - "sumo_params = SumoParams(sim_step=0.1, render=True)\n", + "sim_params = SumoParams(sim_step=0.1, render=True)\n", "\n", "vehicles = VehicleParams()\n", "vehicles.add(veh_id=\"idm\",\n", @@ -300,7 +300,7 @@ "#############################################################\n", "######## using my new environment for the simulation ########\n", "#############################################################\n", - "env = myEnv(env_params, sumo_params, network)\n", + "env = myEnv(env_params, sim_params, network)\n", "#############################################################\n", "\n", "flow_params = dict(\n", @@ -374,7 +374,7 @@ "\n", "\n", "def run_task(*_):\n", - " sumo_params = SumoParams(sim_step=0.1, render=False)\n", + " sim_params = SumoParams(sim_step=0.1, render=False)\n", "\n", " vehicles = VehicleParams()\n", " vehicles.add(veh_id=\"rl\",\n", @@ -404,7 +404,7 @@ " #######################################################\n", " env_name = \"myEnv\"\n", " #######################################################\n", - " pass_params = (env_name, sumo_params, vehicles, env_params, net_params,\n", + " pass_params = (env_name, sim_params, vehicles, env_params, net_params,\n", " initial_config, network)\n", "\n", " env = GymEnv(env_name, record_video=False, register_params=pass_params)\n", diff --git a/tutorials/tutorial11_inflows.ipynb b/tutorials/tutorial11_inflows.ipynb index 8f950d961..12ea12770 100644 --- a/tutorials/tutorial11_inflows.ipynb +++ b/tutorials/tutorial11_inflows.ipynb @@ -221,7 +221,7 @@ "from flow.envs.ring.accel import AccelEnv, ADDITIONAL_ENV_PARAMS\n", "from flow.core.experiment import Experiment\n", "\n", - "sumo_params = SumoParams(render=True,\n", + "sim_params = SumoParams(render=True,\n", " sim_step=0.2)\n", "\n", "env_params = EnvParams(additional_params=ADDITIONAL_ENV_PARAMS)\n", @@ -233,7 +233,7 @@ " net_params=net_params,\n", " initial_config=initial_config)\n", "\n", - "env = AccelEnv(env_params, sumo_params, network)\n", + "env = AccelEnv(env_params, sim_params, network)\n", "\n", "flow_params = dict(\n", " exp_tag='merge-example',\n", From f35f7b91e5dd16eed3250de26416597ad754691c Mon Sep 17 00:00:00 2001 From: Ashkan Y Date: Sun, 5 Jan 2020 12:27:09 -0800 Subject: [PATCH 29/86] Update remaining files to match with new way of running experiments (#803) * Update evaluate.py to match with new way of runnign experiments * flake8 issues * bug fix * bug fix Co-authored-by: Aboudy Kreidieh --- flow/utils/leaderboard/evaluate.py | 49 +++++++++++++++++++++++++++--- tests/fast_tests/test_examples.py | 3 +- 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/flow/utils/leaderboard/evaluate.py b/flow/utils/leaderboard/evaluate.py index fdac33c89..941309ca4 100644 --- a/flow/utils/leaderboard/evaluate.py +++ b/flow/utils/leaderboard/evaluate.py @@ -121,15 +121,56 @@ def get_state(self): env = env_class( env_params=env_params, sim_params=sim_params, network=network) - # create a Experiment object with the "rl_actions" method as - # described in the inputs. Note that the state may not be that which is + flow_params = dict( + # name of the experiment + exp_tag=exp_tag, + + # name of the flow environment the experiment is running on + env_name=env_class, + + # name of the network class the experiment is running on + network=network_class, + + # simulator that is used by the experiment + simulator='traci', + + # sumo-related parameters (see flow.core.params.SumoParams) + sim=sim_params, + + # environment related parameters (see flow.core.params.EnvParams) + env=env_params, + + # network-related parameters (see flow.core.params.NetParams and the + # network's documentation or ADDITIONAL_NET_PARAMS component) + net=net_params, + + # vehicles to be placed in the network at the start of a rollout (see + # flow.core.params.VehicleParams) + veh=vehicles, + + # parameters specifying the positioning of vehicles upon initialization/ + # reset (see flow.core.params.InitialConfig) + initial=initial_config, + + # traffic lights to be introduced to specific nodes (see + # flow.core.params.TrafficLightParams) + tls=traffic_lights, + ) + + # number of time steps + flow_params['env'].horizon = env.env_params.horizon + + # create a Experiment object. Note that the state may not be that which is # specified by the environment. - exp = Experiment(env=env) + exp = Experiment(flow_params) + exp.env = env + + exp = Experiment(flow_params) + exp.env = env # run the experiment and return the reward res = exp.run( num_runs=NUM_RUNS, - num_steps=env.env_params.horizon, rl_actions=_get_actions) return np.mean(res["returns"]), np.std(res["returns"]) diff --git a/tests/fast_tests/test_examples.py b/tests/fast_tests/test_examples.py index 2de1c685d..df8c881f9 100644 --- a/tests/fast_tests/test_examples.py +++ b/tests/fast_tests/test_examples.py @@ -119,7 +119,8 @@ class TestStableBaselineExamples(unittest.TestCase): """ @staticmethod def run_exp(flow_params): - run_stable_baselines_model(flow_params, 2, 5, 5) + train_model = run_stable_baselines_model(flow_params, 1, 4, 4) + train_model.env.close() def test_singleagent_figure_eight(self): self.run_exp(singleagent_figure_eight) From d61e0c99e95b882755d50a28a2137bbe23674c5c Mon Sep 17 00:00:00 2001 From: Aboudy Kreidieh Date: Sun, 5 Jan 2020 22:48:12 +0200 Subject: [PATCH 30/86] added plotly 2.4.0 dependency (#758) --- environment.yml | 1 + requirements.txt | 1 + 2 files changed, 2 insertions(+) diff --git a/environment.yml b/environment.yml index e7616aae6..3bdfebfa4 100644 --- a/environment.yml +++ b/environment.yml @@ -10,6 +10,7 @@ dependencies: - tensorflow==1.9.0 - cloudpickle==1.2.1 - setuptools==41.0.0 + - plotly==2.4.0 - pip: - gym==0.14.0 - pyprind==2.11.2 diff --git a/requirements.txt b/requirements.txt index f079ba1ab..430ef29c6 100644 --- a/requirements.txt +++ b/requirements.txt @@ -24,3 +24,4 @@ opencv-python boto3==1.4.8 redis~=2.10.6 pandas==0.24.2 +plotly==2.4.0 From 9f1c990dbb4e832968392e47a9541d75fd735c28 Mon Sep 17 00:00:00 2001 From: Ashkan Y Date: Tue, 7 Jan 2020 11:29:55 -0800 Subject: [PATCH 31/86] Fixed multi agent traffic light grid --- flow/envs/multiagent/traffic_light_grid.py | 6 ++---- tests/fast_tests/test_examples.py | 4 ++-- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/flow/envs/multiagent/traffic_light_grid.py b/flow/envs/multiagent/traffic_light_grid.py index 66854dd1d..a0438f828 100644 --- a/flow/envs/multiagent/traffic_light_grid.py +++ b/flow/envs/multiagent/traffic_light_grid.py @@ -70,7 +70,7 @@ def observation_space(self): high=1, shape=(3 * 4 * self.num_observed + 2 * self.num_local_edges + - 3 * (1 + self.num_local_lights), + 2 * (1 + self.num_local_lights), ), dtype=np.float32) return tl_box @@ -167,14 +167,12 @@ def get_state(self): self.observed_ids = all_observed_ids # Traffic light information - last_change = self.last_change.flatten() direction = self.direction.flatten() currently_yellow = self.currently_yellow.flatten() # This is a catch-all for when the relative_node method returns a -1 # (when there is no node in the direction sought). We add a last # item to the lists here, which will serve as a default value. # TODO(cathywu) are these values reasonable? - last_change = np.append(last_change, [0]) direction = np.append(direction, [0]) currently_yellow = np.append(currently_yellow, [1]) @@ -194,7 +192,7 @@ def get_state(self): observation = np.array(np.concatenate( [speeds[rl_id_num], dist_to_intersec[rl_id_num], edge_number[rl_id_num], density[local_edge_numbers], - velocity_avg[local_edge_numbers], last_change[local_id_nums], + velocity_avg[local_edge_numbers], direction[local_id_nums], currently_yellow[local_id_nums] ])) obs.update({rl_id: observation}) diff --git a/tests/fast_tests/test_examples.py b/tests/fast_tests/test_examples.py index 3fffcb4e5..5235feca2 100644 --- a/tests/fast_tests/test_examples.py +++ b/tests/fast_tests/test_examples.py @@ -124,7 +124,7 @@ def test_singleagent_figure_eight(self): self.run_exp(singleagent_figure_eight) def test_singleagent_traffic_light_grid(self): - pass # FIXME + self.run_exp(singleagent_traffic_light_grid) def test_singleagent_merge(self): self.run_exp(singleagent_merge) @@ -152,7 +152,7 @@ def test_singleagent_figure_eight(self): self.run_exp(singleagent_figure_eight) def test_singleagent_traffic_light_grid(self): - pass # FIXME + self.run_exp(singleagent_traffic_light_grid) def test_singleagent_traffic_light_grid_inflows(self): pass # FIXME From e52e53147704f319749cd5aea489b11ed577104b Mon Sep 17 00:00:00 2001 From: Ashkan Y Date: Tue, 7 Jan 2020 11:43:38 -0800 Subject: [PATCH 32/86] Flake8 --- .../multiagent_traffic_light_grid.py | 24 ++++++------------- tests/fast_tests/test_examples.py | 6 +++-- 2 files changed, 11 insertions(+), 19 deletions(-) diff --git a/examples/exp_configs/rl/multiagent/multiagent_traffic_light_grid.py b/examples/exp_configs/rl/multiagent/multiagent_traffic_light_grid.py index 2340d67fc..b8293f638 100644 --- a/examples/exp_configs/rl/multiagent/multiagent_traffic_light_grid.py +++ b/examples/exp_configs/rl/multiagent/multiagent_traffic_light_grid.py @@ -1,26 +1,13 @@ """Multi-agent traffic light example (single shared policy).""" -import json -import argparse - -import ray -try: - from ray.rllib.agents.agent import get_agent_class -except ImportError: - from ray.rllib.agents.registry import get_agent_class from ray.rllib.agents.ppo.ppo_policy import PPOTFPolicy -from ray import tune -from ray.tune.registry import register_env -from ray.tune import run_experiments - from flow.envs.multiagent import MultiTrafficLightGridPOEnv from flow.networks import TrafficLightGridNetwork from flow.core.params import SumoParams, EnvParams, InitialConfig, NetParams from flow.core.params import InFlows, SumoCarFollowingParams, VehicleParams from flow.controllers import SimCarFollowingController, GridRouter - +from ray.tune.registry import register_env from flow.utils.registry import make_create_env -from flow.utils.rllib import FlowParamsEncoder # Experiment parameters N_ROLLOUTS = 63 # number of rollouts per training iteration @@ -76,8 +63,7 @@ flow_params = dict( # name of the experiment - exp_tag="grid_0_{}x{}_i{}_multiagent".format(N_ROWS, N_COLUMNS, - EDGE_INFLOW), + exp_tag="grid_0_{}x{}_i{}_multiagent".format(N_ROWS, N_COLUMNS, EDGE_INFLOW), # name of the flow environment the experiment is running on env_name=MultiTrafficLightGridPOEnv, @@ -152,15 +138,19 @@ obs_space = test_env.observation_space act_space = test_env.action_space + def gen_policy(): """Generate a policy in RLlib.""" return PPOTFPolicy, obs_space, act_space, {} + # Setup PG with a single policy graph for all agents POLICY_GRAPHS = {'av': gen_policy()} + def policy_mapping_fn(_): """Map a policy in RLlib.""" return 'av' -POLICIES_TO_TRAIN = ['av'] \ No newline at end of file + +POLICIES_TO_TRAIN = ['av'] diff --git a/tests/fast_tests/test_examples.py b/tests/fast_tests/test_examples.py index 5235feca2..37ed87d5f 100644 --- a/tests/fast_tests/test_examples.py +++ b/tests/fast_tests/test_examples.py @@ -8,7 +8,8 @@ from flow.core.experiment import Experiment from examples.exp_configs.rl.singleagent.singleagent_figure_eight import flow_params as singleagent_figure_eight -from examples.exp_configs.rl.singleagent.singleagent_traffic_light_grid import flow_params as singleagent_traffic_light_grid +from examples.exp_configs.rl.singleagent.singleagent_traffic_light_grid import \ + flow_params as singleagent_traffic_light_grid from examples.exp_configs.rl.singleagent.singleagent_merge import flow_params as singleagent_merge from examples.exp_configs.rl.singleagent.singleagent_ring import flow_params as singleagent_ring from examples.exp_configs.rl.singleagent.singleagent_bottleneck import flow_params as singleagent_bottleneck @@ -16,7 +17,8 @@ from examples.exp_configs.rl.multiagent.multiagent_figure_eight import flow_params as multiagent_figure_eight from examples.exp_configs.rl.multiagent.multiagent_ring import \ flow_params as multiagent_ring -from examples.exp_configs.rl.multiagent.multiagent_traffic_light_grid import flow_params as multiagent_traffic_light_grid +from examples.exp_configs.rl.multiagent.multiagent_traffic_light_grid import \ + flow_params as multiagent_traffic_light_grid from examples.exp_configs.rl.multiagent.multiagent_highway import flow_params as multiagent_highway from examples.train_stable_baselines import run_model as run_stable_baselines_model From 55e39f1119924191ec4f207159438899ee24eee9 Mon Sep 17 00:00:00 2001 From: Aboudy Kreidieh Date: Wed, 8 Jan 2020 19:08:48 +0200 Subject: [PATCH 33/86] remove remaining rllab (#757) * removed most uses of rllab. Bug pending * added description that separate py files are needed * bug fix * cleanup to tutorials Co-authored-by: Ashkan Y. --- .coveragerc | 1 - docs/source/conf.py | 3 - tests/fast_tests/test_visualizers.py | 15 -- tutorials/README.md | 2 +- tutorials/tutorial00_flow.ipynb | 4 +- tutorials/tutorial01_sumo.ipynb | 13 +- tutorials/tutorial02_aimsun.ipynb | 11 - tutorials/tutorial04_visualize.ipynb | 8 +- tutorials/tutorial05_networks.ipynb | 37 +-- tutorials/tutorial06_osm.ipynb | 38 +-- tutorials/tutorial07_network_templates.ipynb | 48 +--- tutorials/tutorial08_environments.ipynb | 268 +++++++++++-------- tutorials/tutorial09_controllers.ipynb | 2 +- tutorials/tutorial10_traffic_lights.ipynb | 41 ++- tutorials/tutorial11_inflows.ipynb | 25 +- tutorials/tutorial12_bottlenecks.ipynb | 42 ++- 16 files changed, 230 insertions(+), 328 deletions(-) diff --git a/.coveragerc b/.coveragerc index 769612895..70674c543 100644 --- a/.coveragerc +++ b/.coveragerc @@ -5,7 +5,6 @@ omit = *__init__* */scripts/* */setup.py - *rllab* */flow/utils/shflags *dummy_server.py *aimsun.py diff --git a/docs/source/conf.py b/docs/source/conf.py index d228c6095..b2dc27e75 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -32,9 +32,6 @@ def __getattr__(cls, name): MOCK_MODULES = [ - 'rllab', - "rllab.core.serializable.Serializable", - 'rllab.sampler.utils.rollout', "AAPI", 'PyANGKernel', 'AAPI.GKGUISystem', diff --git a/tests/fast_tests/test_visualizers.py b/tests/fast_tests/test_visualizers.py index 3117f37f7..3ed86c596 100644 --- a/tests/fast_tests/test_visualizers.py +++ b/tests/fast_tests/test_visualizers.py @@ -1,5 +1,3 @@ -# from flow.visualize import visualizer_rllab as vs_rllab -# from flow.visualize.visualizer_rllab import visualizer_rllab from flow.visualize import visualizer_rllib as vs_rllib from flow.visualize.visualizer_rllib import visualizer_rllib import flow.visualize.capacity_diagram_generator as cdg @@ -55,19 +53,6 @@ def test_visualizer_multi(self): visualizer_rllib(pass_args) -# class TestVisualizerRLlab(unittest.TestCase): -# """Tests visualizer_rllab""" -# -# def test_visualizer(self): -# # current path -# current_path = os.path.realpath(__file__).rsplit('/', 1)[0] -# arg_str = '{}/../data/rllab_data/itr_0.pkl --num_rollouts 1 ' \ -# '--no_render'.format(current_path).split() -# parser = vs_rllab.create_parser() -# pass_args = parser.parse_args(arg_str) -# visualizer_rllab(pass_args) - - class TestPlotters(unittest.TestCase): def test_capacity_diagram_generator(self): diff --git a/tutorials/README.md b/tutorials/README.md index d723f5607..6b4823068 100644 --- a/tutorials/README.md +++ b/tutorials/README.md @@ -56,7 +56,7 @@ The content of each tutorial is as follows: **Tutorial 3:** Running RLlib experiments for mixed-autonomy traffic. **Tutorial 4:** Saving and visualizing resuls from non-RL simulations and -testing simulations in the presence of an RLlib/rllab agent. +testing simulations in the presence of an RLlib agent. **Tutorial 5:** Creating custom networks. diff --git a/tutorials/tutorial00_flow.ipynb b/tutorials/tutorial00_flow.ipynb index f23cb74bb..689a1dfdd 100644 --- a/tutorials/tutorial00_flow.ipynb +++ b/tutorials/tutorial00_flow.ipynb @@ -110,9 +110,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.7.0" + "version": "3.6.8" } }, "nbformat": 4, "nbformat_minor": 2 -} \ No newline at end of file +} diff --git a/tutorials/tutorial01_sumo.ipynb b/tutorials/tutorial01_sumo.ipynb index e598cd673..18e7cdf26 100644 --- a/tutorials/tutorial01_sumo.ipynb +++ b/tutorials/tutorial01_sumo.ipynb @@ -346,22 +346,12 @@ "metadata": {}, "outputs": [], "source": [ - "# create the network object\n", - "network = RingNetwork(name=\"ring_example\",\n", - " vehicles=vehicles,\n", - " net_params=net_params,\n", - " initial_config=initial_config,\n", - " traffic_lights=traffic_lights)\n", - "\n", - "# create the environment object\n", - "env = AccelEnv(env_params, sim_params, network)\n", - "\n", "flow_params = dict(\n", " exp_tag='ring_example',\n", " env_name=AccelEnv,\n", " network=RingNetwork,\n", " simulator='traci',\n", - " sim=sumo_params,\n", + " sim=sim_params,\n", " env=env_params,\n", " net=net_params,\n", " veh=vehicles,\n", @@ -372,7 +362,6 @@ "# number of time steps\n", "flow_params['env'].horizon = 3000\n", "exp = Experiment(flow_params)\n", - "exp.env - env\n", "\n", "# run the sumo simulation\n", "_ = exp.run(1, convert_to_csv=True)" diff --git a/tutorials/tutorial02_aimsun.ipynb b/tutorials/tutorial02_aimsun.ipynb index 9b3dae34c..91ddd2d7d 100644 --- a/tutorials/tutorial02_aimsun.ipynb +++ b/tutorials/tutorial02_aimsun.ipynb @@ -348,16 +348,6 @@ "metadata": {}, "outputs": [], "source": [ - "# create the network object\n", - "network = RingNetwork(name=\"ring_example\",\n", - " vehicles=vehicles,\n", - " net_params=net_params,\n", - " initial_config=initial_config,\n", - " traffic_lights=traffic_lights)\n", - "\n", - "# create the environment object\n", - "env = AccelEnv(env_params, sim_params, network, simulator='aimsun')\n", - "\n", "flow_params = dict(\n", " exp_tag='test_network',\n", " env_name=AccelEnv,\n", @@ -374,7 +364,6 @@ "# number of time steps\n", "flow_params['env'].horizon = 3000\n", "exp = Experiment(flow_params)\n", - "exp.env = env\n", "\n", "# run the Aimsun simulation\n", "_ = exp.run(1)" diff --git a/tutorials/tutorial04_visualize.ipynb b/tutorials/tutorial04_visualize.ipynb index f31a31fdf..3f4eb82bc 100644 --- a/tutorials/tutorial04_visualize.ipynb +++ b/tutorials/tutorial04_visualize.ipynb @@ -48,7 +48,7 @@ "collapsed": true }, "source": [ - "Visualization is different depending on which reinforcement learning library you are using, if any. Accordingly, the rest of this tutorial explains how to plot rewards, replay policies and collect data when using either no RL library, RLlib or rllab. " + "Visualization is different depending on which reinforcement learning library you are using, if any. Accordingly, the rest of this tutorial explains how to plot rewards, replay policies and collect data when using either no RL library, RLlib, or stable-baselines. " ] }, { @@ -63,9 +63,7 @@ "\n", "[How to visualize using SUMO with RLlib](#2.2---Using-SUMO-with-RLlib)\n", "\n", - "[How to visualize using SUMO with rllab](#2.3---Using-SUMO-with-rllab)\n", - "\n", - "[**_Example: visualize data on a ring trained using RLlib_**](#2.4---Example:-Visualize-data-on-a-ring-trained-using-RLlib)\n" + "[**_Example: visualize data on a ring trained using RLlib_**](#2.3---Example:-Visualize-data-on-a-ring-trained-using-RLlib)\n" ] }, { @@ -213,7 +211,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "### 2.4 - Example: Visualize data on a ring trained using RLlib" + "### 2.3 - Example: Visualize data on a ring trained using RLlib" ] }, { diff --git a/tutorials/tutorial05_networks.ipynb b/tutorials/tutorial05_networks.ipynb index 653ee099d..86387a80c 100644 --- a/tutorials/tutorial05_networks.ipynb +++ b/tutorials/tutorial05_networks.ipynb @@ -19,7 +19,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -66,7 +66,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -98,7 +98,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -140,7 +140,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -217,7 +217,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -245,7 +245,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -273,7 +273,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -345,7 +345,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -377,7 +377,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -405,7 +405,7 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -423,7 +423,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -446,22 +446,12 @@ "source": [ "from flow.core.experiment import Experiment\n", "\n", - "network = myNetwork( # we use the newly defined network class\n", - " name=\"test_network\",\n", - " vehicles=vehicles,\n", - " net_params=net_params,\n", - " initial_config=initial_config\n", - ")\n", - "\n", - "# AccelEnv allows us to test any newly generated network quickly\n", - "env = AccelEnv(env_params, sim_params, network)\n", - "\n", "flow_params = dict(\n", " exp_tag='test_network',\n", " env_name=AccelEnv,\n", " network=myNetwork,\n", " simulator='traci',\n", - " sim=sumo_params,\n", + " sim=sim_params,\n", " env=env_params,\n", " net=net_params,\n", " veh=vehicles,\n", @@ -471,7 +461,6 @@ "# number of time steps\n", "flow_params['env'].horizon = 1500\n", "exp = Experiment(flow_params)\n", - "exp.env = env\n", "\n", "# run the sumo simulation\n", "_ = exp.run(1)" @@ -501,7 +490,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.7" + "version": "3.6.8" } }, "nbformat": 4, diff --git a/tutorials/tutorial06_osm.ipynb b/tutorials/tutorial06_osm.ipynb index a721c9005..8a8eb387d 100644 --- a/tutorials/tutorial06_osm.ipynb +++ b/tutorials/tutorial06_osm.ipynb @@ -91,15 +91,7 @@ "sim_params = SumoParams(render=True)\n", "initial_config = InitialConfig()\n", "vehicles = VehicleParams()\n", - "vehicles.add('human', num_vehicles=100)\n", - "\n", - "# create the network\n", - "network = Network(\n", - " name='bay_bridge',\n", - " net_params=net_params,\n", - " initial_config=initial_config,\n", - " vehicles=vehicles\n", - ")" + "vehicles.add('human', num_vehicles=100)" ] }, { @@ -115,13 +107,6 @@ "metadata": {}, "outputs": [], "source": [ - "# create the environment\n", - "env = TestEnv(\n", - " env_params=env_params,\n", - " sim_params=sim_params,\n", - " network=network\n", - ")\n", - "\n", "flow_params = dict(\n", " exp_tag='bay_bridge',\n", " env_name=TestEnv,\n", @@ -137,10 +122,9 @@ "# number of time steps\n", "flow_params['env'].horizon = 1000\n", "exp = Experiment(flow_params)\n", - "exp.env = env\n", "\n", "# run the sumo simulation\n", - "exp.run(1)" + "_ = exp.run(1)" ] }, { @@ -283,21 +267,6 @@ "metadata": {}, "outputs": [], "source": [ - "# create the network\n", - "new_network = BayBridgeOSMNetwork(\n", - " name='bay_bridge',\n", - " net_params=net_params,\n", - " initial_config=new_initial_config,\n", - " vehicles=vehicles,\n", - ")\n", - "\n", - "# create the environment\n", - "env = TestEnv(\n", - " env_params=env_params,\n", - " sim_params=sim_params,\n", - " network=new_network\n", - ")\n", - "\n", "flow_params = dict(\n", " exp_tag='bay_bridge',\n", " env_name=TestEnv,\n", @@ -313,10 +282,9 @@ "# number of time steps\n", "flow_params['env'].horizon = 10000\n", "exp = Experiment(flow_params)\n", - "exp.env = env\n", "\n", "# run the sumo simulation\n", - "exp.run(1)" + "_ = exp.run(1)" ] }, { diff --git a/tutorials/tutorial07_network_templates.ipynb b/tutorials/tutorial07_network_templates.ipynb index 9d3da420b..2b35fb258 100644 --- a/tutorials/tutorial07_network_templates.ipynb +++ b/tutorials/tutorial07_network_templates.ipynb @@ -156,21 +156,6 @@ "metadata": {}, "outputs": [], "source": [ - "# create the network\n", - "network = TemplateNetwork(\n", - " name=\"template\",\n", - " net_params=net_params,\n", - " initial_config=initial_config,\n", - " vehicles=vehicles\n", - ")\n", - "\n", - "# create the environment\n", - "env = TestEnv(\n", - " env_params=env_params,\n", - " sim_params=sim_params,\n", - " network=network\n", - ")\n", - "\n", "flow_params = dict(\n", " exp_tag='template',\n", " env_name=TestEnv,\n", @@ -186,7 +171,6 @@ "# number of time steps\n", "flow_params['env'].horizon = 1000\n", "exp = Experiment(flow_params)\n", - "exp.env = env\n", "\n", "# run the sumo simulation\n", "_ = exp.run(1)" @@ -296,20 +280,6 @@ "metadata": {}, "outputs": [], "source": [ - "# create the network\n", - "network = Network(\n", - " name=\"template\",\n", - " net_params=new_net_params,\n", - " vehicles=new_vehicles\n", - ")\n", - "\n", - "# create the environment\n", - "env = TestEnv(\n", - " env_params=env_params,\n", - " sim_params=sim_params,\n", - " network=network\n", - ")\n", - "\n", "flow_params = dict(\n", " exp_tag='template',\n", " env_name=TestEnv,\n", @@ -325,7 +295,6 @@ "# number of time steps\n", "flow_params['env'].horizon = 100000\n", "exp = Experiment(flow_params)\n", - "exp.env = env\n", "\n", "# run the sumo simulation\n", "_ = exp.run(1)" @@ -420,20 +389,6 @@ "metadata": {}, "outputs": [], "source": [ - "network = Network(\n", - " name=\"template\", \n", - " net_params=net_params,\n", - " initial_config=initial_config,\n", - " vehicles=vehicles\n", - ")\n", - "\n", - "env = TestEnv(\n", - " env_params, \n", - " sim_params, \n", - " network, \n", - " simulator='aimsun' \n", - ")\n", - "\n", "flow_params = dict(\n", " exp_tag='template',\n", " env_name=TestEnv,\n", @@ -449,7 +404,6 @@ "# number of time steps\n", "flow_params['env'].horizon = 1000\n", "exp = Experiment(flow_params)\n", - "exp.env = env\n", "\n", "# run the sumo simulation\n", "_ = exp.run(1)" @@ -472,7 +426,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.9" + "version": "3.6.8" } }, "nbformat": 4, diff --git a/tutorials/tutorial08_environments.ipynb b/tutorials/tutorial08_environments.ipynb index 2f08e27ed..07e12b3cb 100644 --- a/tutorials/tutorial08_environments.ipynb +++ b/tutorials/tutorial08_environments.ipynb @@ -20,7 +20,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -54,7 +54,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -79,7 +79,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -95,7 +95,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -111,7 +111,7 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -138,7 +138,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -168,7 +168,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -199,7 +199,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -234,7 +234,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -292,23 +292,12 @@ "\n", "initial_config = InitialConfig(bunching=20)\n", "\n", - "network = RingNetwork(name=\"ring\",\n", - " vehicles=vehicles,\n", - " net_params=net_params,\n", - " initial_config=initial_config)\n", - "\n", - "#############################################################\n", - "######## using my new environment for the simulation ########\n", - "#############################################################\n", - "env = myEnv(env_params, sim_params, network)\n", - "#############################################################\n", - "\n", "flow_params = dict(\n", " exp_tag='ring',\n", - " env_name=myEnv,\n", + " env_name=myEnv, # using my new environment for the simulation\n", " network=RingNetwork,\n", " simulator='traci',\n", - " sim=sumo_params,\n", + " sim=sim_params,\n", " env=env_params,\n", " net=net_params,\n", " veh=vehicles,\n", @@ -318,7 +307,6 @@ "# number of time steps\n", "flow_params['env'].horizon = 1500\n", "exp = Experiment(flow_params)\n", - "exp.env = env\n", "\n", "# run the sumo simulation\n", "_ = exp.run(1)" @@ -332,7 +320,7 @@ "\n", "Next, we wish to train this environment in the presence of the autonomous vehicle agent to reduce the formation of waves in the network, thereby pushing the performance of vehicles in the network past the above expected return.\n", "\n", - "In order for an environment to be trainable in either RLLib for rllab (as we have shown in tutorials 2 and 3), the environment must be acccessable via import from *flow.envs*. In order to do so, copy the above envrionment onto a .py and import the environment in `flow.envs.__init__.py`. You can ensure that the process was successful by running the following command:" + "The below code block may be used to train the above environment using the Proximal Policy Optimization (PPO) algorithm provided by RLlib. In order to register the environment with OpenAI gym, the environment must first be placed in a separate \".py\" file and then imported via the script below. Then, the script immediately below should function regularly." ] }, { @@ -341,106 +329,174 @@ "metadata": {}, "outputs": [], "source": [ - "# NOTE: only runs if the above procedure have been performed\n", - "from flow.envs import myEnv" + "#############################################################\n", + "####### Replace this with the environment you created #######\n", + "#############################################################\n", + "from flow.envs import AccelEnv as myEnv" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "Once this is done, the below code block may be used to train the above environment using the Trust Region Policy Optimization (TRPO) algorithm provided by rllab. We do not recommend training this environment to completion within a jupyter notebook setting; however, once training is complete, visualization of the resulting policy should show that the autonomous vehicle learns to dissipate the formation and propagation of waves in the network." + "**Note**: We do not recommend training this environment to completion within a jupyter notebook setting; however, once training is complete, visualization of the resulting policy should show that the autonomous vehicle learns to dissipate the formation and propagation of waves in the network." ] }, { "cell_type": "code", "execution_count": null, - "metadata": {}, + "metadata": { + "scrolled": true + }, "outputs": [], "source": [ - "from rllab.envs.normalized_env import normalize\n", - "from rllab.misc.instrument import run_experiment_lite\n", - "from rllab.algos.trpo import TRPO\n", - "from rllab.baselines.linear_feature_baseline import LinearFeatureBaseline\n", - "from rllab.policies.gaussian_gru_policy import GaussianGRUPolicy\n", + "import json\n", + "import ray\n", + "from ray.rllib.agents.registry import get_agent_class\n", + "from ray.tune import run_experiments\n", + "from ray.tune.registry import register_env\n", "\n", - "from flow.networks.ring import RingNetwork\n", + "from flow.networks.ring import RingNetwork, ADDITIONAL_NET_PARAMS\n", + "from flow.utils.registry import make_create_env\n", + "from flow.utils.rllib import FlowParamsEncoder\n", + "from flow.core.params import SumoParams, EnvParams, InitialConfig, NetParams\n", + "from flow.core.params import VehicleParams, SumoCarFollowingParams\n", "from flow.controllers import RLController, IDMController, ContinuousRouter\n", - "from flow.core.params import VehicleParams\n", - "from flow.core.params import SumoParams, EnvParams, NetParams, InitialConfig\n", - "from rllab.envs.gym_env import GymEnv\n", "\n", + "\n", + "# time horizon of a single rollout\n", "HORIZON = 1500\n", + "# number of rollouts per training iteration\n", + "N_ROLLOUTS = 20\n", + "# number of parallel workers\n", + "N_CPUS = 2\n", + "\n", + "\n", + "# We place one autonomous vehicle and 22 human-driven vehicles in the network\n", + "vehicles = VehicleParams()\n", + "vehicles.add(\n", + " veh_id=\"human\",\n", + " acceleration_controller=(IDMController, {\n", + " \"noise\": 0.2\n", + " }),\n", + " car_following_params=SumoCarFollowingParams(\n", + " min_gap=0\n", + " ),\n", + " routing_controller=(ContinuousRouter, {}),\n", + " num_vehicles=21)\n", + "vehicles.add(\n", + " veh_id=\"rl\",\n", + " acceleration_controller=(RLController, {}),\n", + " routing_controller=(ContinuousRouter, {}),\n", + " num_vehicles=1)\n", + "\n", + "flow_params = dict(\n", + " # name of the experiment\n", + " exp_tag=\"stabilizing_the_ring\",\n", + "\n", + " # name of the flow environment the experiment is running on\n", + " env_name=myEnv, # <------ here we replace the environment with our new environment\n", + "\n", + " # name of the network class the experiment is running on\n", + " network=RingNetwork,\n", + "\n", + " # simulator that is used by the experiment\n", + " simulator='traci',\n", + "\n", + " # sumo-related parameters (see flow.core.params.SumoParams)\n", + " sim=SumoParams(\n", + " sim_step=0.1,\n", + " render=True,\n", + " ),\n", + "\n", + " # environment related parameters (see flow.core.params.EnvParams)\n", + " env=EnvParams(\n", + " horizon=HORIZON,\n", + " warmup_steps=750,\n", + " clip_actions=False,\n", + " additional_params={\n", + " \"target_velocity\": 20,\n", + " \"sort_vehicles\": False,\n", + " \"max_accel\": 1,\n", + " \"max_decel\": 1,\n", + " },\n", + " ),\n", + "\n", + " # network-related parameters (see flow.core.params.NetParams and the\n", + " # network's documentation or ADDITIONAL_NET_PARAMS component)\n", + " net=NetParams(\n", + " additional_params=ADDITIONAL_NET_PARAMS.copy()\n", + " ),\n", + "\n", + " # vehicles to be placed in the network at the start of a rollout (see\n", + " # flow.core.params.VehicleParams)\n", + " veh=vehicles,\n", + "\n", + " # parameters specifying the positioning of vehicles upon initialization/\n", + " # reset (see flow.core.params.InitialConfig)\n", + " initial=InitialConfig(\n", + " bunching=20,\n", + " ),\n", + ")\n", "\n", "\n", - "def run_task(*_):\n", - " sim_params = SumoParams(sim_step=0.1, render=False)\n", - "\n", - " vehicles = VehicleParams()\n", - " vehicles.add(veh_id=\"rl\",\n", - " acceleration_controller=(RLController, {}),\n", - " routing_controller=(ContinuousRouter, {}),\n", - " num_vehicles=1)\n", - " vehicles.add(veh_id=\"idm\",\n", - " acceleration_controller=(IDMController, {}),\n", - " routing_controller=(ContinuousRouter, {}),\n", - " num_vehicles=21)\n", - "\n", - " env_params = EnvParams(horizon=HORIZON,\n", - " additional_params=ADDITIONAL_ENV_PARAMS)\n", - "\n", - " additional_net_params = ADDITIONAL_NET_PARAMS.copy()\n", - " net_params = NetParams(additional_params=additional_net_params)\n", - "\n", - " initial_config = InitialConfig(bunching=20)\n", - "\n", - " network = RingNetwork(name=\"ring-training\",\n", - " vehicles=vehicles,\n", - " net_params=net_params,\n", - " initial_config=initial_config)\n", - "\n", - " #######################################################\n", - " ######## using my new environment for training ########\n", - " #######################################################\n", - " env_name = \"myEnv\"\n", - " #######################################################\n", - " pass_params = (env_name, sim_params, vehicles, env_params, net_params,\n", - " initial_config, network)\n", - "\n", - " env = GymEnv(env_name, record_video=False, register_params=pass_params)\n", - " horizon = env.horizon\n", - " env = normalize(env)\n", - "\n", - " policy = GaussianGRUPolicy(\n", - " env_spec=env.spec,\n", - " hidden_sizes=(5,),\n", - " )\n", - "\n", - " baseline = LinearFeatureBaseline(env_spec=env.spec)\n", - "\n", - " algo = TRPO(\n", - " env=env,\n", - " policy=policy,\n", - " baseline=baseline,\n", - " batch_size=30000,\n", - " max_path_length=horizon,\n", - " n_itr=500,\n", - " discount=0.999,\n", - " )\n", - " algo.train(),\n", - "\n", - "\n", - "exp_tag = \"stabilizing-the-ring\"\n", - "\n", - "for seed in [5]: # , 20, 68]:\n", - " run_experiment_lite(\n", - " run_task,\n", - " n_parallel=1,\n", - " snapshot_mode=\"all\",\n", - " seed=seed,\n", - " mode=\"local\",\n", - " exp_prefix=exp_tag,\n", - " )" + "def setup_exps():\n", + " \"\"\"Return the relevant components of an RLlib experiment.\n", + "\n", + " Returns\n", + " -------\n", + " str\n", + " name of the training algorithm\n", + " str\n", + " name of the gym environment to be trained\n", + " dict\n", + " training configuration parameters\n", + " \"\"\"\n", + " alg_run = \"PPO\"\n", + "\n", + " agent_cls = get_agent_class(alg_run)\n", + " config = agent_cls._default_config.copy()\n", + " config[\"num_workers\"] = N_CPUS\n", + " config[\"train_batch_size\"] = HORIZON * N_ROLLOUTS\n", + " config[\"gamma\"] = 0.999 # discount rate\n", + " config[\"model\"].update({\"fcnet_hiddens\": [3, 3]})\n", + " config[\"use_gae\"] = True\n", + " config[\"lambda\"] = 0.97\n", + " config[\"kl_target\"] = 0.02\n", + " config[\"num_sgd_iter\"] = 10\n", + " config['clip_actions'] = False # FIXME(ev) temporary ray bug\n", + " config[\"horizon\"] = HORIZON\n", + "\n", + " # save the flow params for replay\n", + " flow_json = json.dumps(\n", + " flow_params, cls=FlowParamsEncoder, sort_keys=True, indent=4)\n", + " config['env_config']['flow_params'] = flow_json\n", + " config['env_config']['run'] = alg_run\n", + "\n", + " create_env, gym_name = make_create_env(params=flow_params, version=0)\n", + "\n", + " # Register as rllib env\n", + " register_env(gym_name, create_env)\n", + " return alg_run, gym_name, config\n", + "\n", + "\n", + "alg_run, gym_name, config = setup_exps()\n", + "ray.init(num_cpus=N_CPUS + 1)\n", + "trials = run_experiments({\n", + " flow_params[\"exp_tag\"]: {\n", + " \"run\": alg_run,\n", + " \"env\": gym_name,\n", + " \"config\": {\n", + " **config\n", + " },\n", + " \"checkpoint_freq\": 20,\n", + " \"checkpoint_at_end\": True,\n", + " \"max_failures\": 999,\n", + " \"stop\": {\n", + " \"training_iteration\": 200,\n", + " },\n", + " }\n", + "})" ] }, { @@ -467,7 +523,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.7" + "version": "3.6.8" }, "widgets": { "state": {}, diff --git a/tutorials/tutorial09_controllers.ipynb b/tutorials/tutorial09_controllers.ipynb index eb09e7c3f..0aa110d11 100644 --- a/tutorials/tutorial09_controllers.ipynb +++ b/tutorials/tutorial09_controllers.ipynb @@ -238,7 +238,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.6.7" + "version": "3.6.8" }, "widgets": { "state": {}, diff --git a/tutorials/tutorial10_traffic_lights.ipynb b/tutorials/tutorial10_traffic_lights.ipynb index 43a2bba97..575952fc4 100644 --- a/tutorials/tutorial10_traffic_lights.ipynb +++ b/tutorials/tutorial10_traffic_lights.ipynb @@ -40,7 +40,7 @@ "outputs": [], "source": [ "from flow.core.params import NetParams\n", - "from flow.networks.grid import TrafficLightGridNetwork\n", + "from flow.networks.traffic_light_grid import TrafficLightGridNetwork\n", "from flow.core.params import TrafficLightParams\n", "from flow.core.params import SumoParams, EnvParams, InitialConfig, NetParams, \\\n", " InFlows, SumoCarFollowingParams\n", @@ -79,7 +79,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "metadata": {}, "outputs": [], "source": [ @@ -129,20 +129,13 @@ "source": [ "Once the `TrafficLightParams` class is instantiated, traffic lights can be added via the `add` function. One prerequisite of using this function is knowing the node id of any node you intend to manipulate. This information is baked into the experiment's network class, as well as the experiment's `nod.xml` file. For the experiment we are using with 2 rows and 3 columns, there are 6 nodes: \"center0\" to \"center5\". \n", "\n", - "This will be the ordering of \"centers\" in our network:" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - " | | |\n", - "-3-4-5-\n", - " | | |\n", - "-0-1-2-\n", - " | | |" + "This will be the ordering of \"centers\" in our network:\n", + "\n", + " | | |\n", + " -3-4-5-\n", + " | | |\n", + " -0-1-2-\n", + " | | |" ] }, { @@ -195,8 +188,7 @@ "additional_net_params = {\"grid_array\": grid_array, \"speed_limit\": 35,\n", " \"horizontal_lanes\": 1, \"vertical_lanes\": 1,\n", " \"traffic_lights\": True}\n", - "net_params = NetParams(no_internal_links=False,\n", - " additional_params=additional_net_params)\n", + "net_params = NetParams(additional_params=additional_net_params)\n", "\n", "network = TrafficLightGridNetwork(name=\"grid\",\n", " vehicles=VehicleParams(),\n", @@ -396,12 +388,15 @@ "metadata": {}, "outputs": [], "source": [ - "# keeps track of the last time the traffic lights in an intersection were allowed to change (the last time the lights were allowed to change from a red-green state to a red-yellow state.).\n", + "# keeps track of the last time the traffic lights in an intersection were allowed to change \n", + "# (the last time the lights were allowed to change from a red-green state to a red-yellow state).\n", "self.last_change = np.zeros((self.rows * self.cols, 1))\n", - "# keeps track of the direction of the intersection (the direction that is currently being allowed to flow. 0 indicates flow from top to bottom, and 1 indicates flow from left to right.)\n", + "# keeps track of the direction of the intersection (the direction that is currently being allowed\n", + "# to flow. 0 indicates flow from top to bottom, and 1 indicates flow from left to right.)\n", "self.direction = np.zeros((self.rows * self.cols, 1))\n", - "# value of 1 indicates that the intersection is in a red-yellow state (traffic lights are red for one way (e.g. north-south), while the traffic lights for the other way (e.g. west-east) are yellow\n", - ". 0 indicates that the intersection is in a red-green state.\n", + "# value of 1 indicates that the intersection is in a red-yellow state (traffic lights are red for \n", + "# one way (e.g. north-south), while the traffic lights for the other way (e.g. west-east) are yellow.\n", + "# 0 indicates that the intersection is in a red-green state.\n", "self.currently_yellow = np.zeros((self.rows * self.cols, 1))" ] }, @@ -723,4 +718,4 @@ }, "nbformat": 4, "nbformat_minor": 1 -} \ No newline at end of file +} diff --git a/tutorials/tutorial11_inflows.ipynb b/tutorials/tutorial11_inflows.ipynb index 12ea12770..a66ae0e30 100644 --- a/tutorials/tutorial11_inflows.ipynb +++ b/tutorials/tutorial11_inflows.ipynb @@ -228,19 +228,12 @@ "\n", "initial_config = InitialConfig()\n", "\n", - "network = MergeNetwork(name=\"merge-example\",\n", - " vehicles=vehicles,\n", - " net_params=net_params,\n", - " initial_config=initial_config)\n", - "\n", - "env = AccelEnv(env_params, sim_params, network)\n", - "\n", "flow_params = dict(\n", " exp_tag='merge-example',\n", " env_name=AccelEnv,\n", " network=MergeNetwork,\n", " simulator='traci',\n", - " sim=sumo_params,\n", + " sim=sim_params,\n", " env=env_params,\n", " net=net_params,\n", " veh=vehicles,\n", @@ -250,7 +243,6 @@ "# number of time steps\n", "flow_params['env'].horizon = 10000\n", "exp = Experiment(flow_params)\n", - "exp.env = env\n", "\n", "# run the sumo simulation\n", "_ = exp.run(1)" @@ -483,13 +475,6 @@ "\n", "initial_config = InitialConfig()\n", "\n", - "network = MergeNetwork(name=\"merge-example\",\n", - " vehicles=vehicles,\n", - " net_params=net_params,\n", - " initial_config=initial_config)\n", - "\n", - "env = AccelEnv(env_params, sim_params, network)\n", - "\n", "flow_params = dict(\n", " exp_tag='merge-example',\n", " env_name=AccelEnv,\n", @@ -505,11 +490,17 @@ "# number of time steps\n", "flow_params['env'].horizon = 10000\n", "exp = Experiment(flow_params)\n", - "exp.env = env\n", "\n", "# run the sumo simulation\n", "_ = exp.run(1)" ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { diff --git a/tutorials/tutorial12_bottlenecks.ipynb b/tutorials/tutorial12_bottlenecks.ipynb index 53d660b67..2481f240d 100644 --- a/tutorials/tutorial12_bottlenecks.ipynb +++ b/tutorials/tutorial12_bottlenecks.ipynb @@ -57,7 +57,12 @@ "\n", "import logging\n", "\n", - "def run_exp(flow_rate, scaling=1, disable_tb=True, disable_ramp_meter=True, n_crit=1000, feedback_coef=20):\n", + "def run_exp(flow_rate,\n", + " scaling=1,\n", + " disable_tb=True,\n", + " disable_ramp_meter=True,\n", + " n_crit=1000,\n", + " feedback_coef=20):\n", " # Set up SUMO to render the results, take a time_step of 0.5 seconds per simulation step\n", " sim_params = SumoParams(\n", " sim_step=0.5,\n", @@ -126,35 +131,22 @@ " lanes_distribution=float(\"inf\"),\n", " edges_distribution=[\"2\", \"3\", \"4\", \"5\"])\n", "\n", - " # Actually construct the network that constitutes the bottleneck.\n", - " network = BottleneckNetwork(\n", - " name=\"bay_bridge_toll\",\n", - " vehicles=vehicles,\n", - " net_params=net_params,\n", - " initial_config=initial_config,\n", - " traffic_lights=traffic_lights)\n", - "\n", - " # The environment that defines the Markov decision process of our system\n", - " env = BottleneckEnv(env_params, sim_params, network)\n", - "\n", - " \n", " flow_params = dict(\n", - " exp_tag='bay_bridge_toll',\n", - " env_name=BottleneckEnv,\n", - " network=BottleneckNetwork,\n", - " simulator='traci',\n", - " sim=sim_params,\n", - " env=env_params,\n", - " net=net_params,\n", - " veh=vehicles,\n", - " initial=initial_config,\n", - " tls=traffic_lights,\n", + " exp_tag='bay_bridge_toll',\n", + " env_name=BottleneckEnv,\n", + " network=BottleneckNetwork,\n", + " simulator='traci',\n", + " sim=sim_params,\n", + " env=env_params,\n", + " net=net_params,\n", + " veh=vehicles,\n", + " initial=initial_config,\n", + " tls=traffic_lights,\n", " )\n", "\n", " # number of time steps\n", " flow_params['env'].horizon = 1000\n", " exp = Experiment(flow_params)\n", - " exp.env = env\n", "\n", " # run the sumo simulation\n", " _ = exp.run(1)" @@ -414,7 +406,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.5.2" + "version": "3.6.8" } }, "nbformat": 4, From 6930f1e3c02a0fa0fbf3c98bb1ea50c7de347d61 Mon Sep 17 00:00:00 2001 From: Ashkan Y Date: Wed, 8 Jan 2020 09:09:10 -0800 Subject: [PATCH 34/86] Combine train_rllib.py and train_stable_baseline.py (#812) * Make train.py single file * flake8 * Fix remaining files * Few bug fixes * PR fix Co-authored-by: Aboudy Kreidieh --- docs/source/flow_setup.rst | 4 +- examples/README.md | 25 ++- examples/train.py | 258 +++++++++++++++++++++++++++ examples/train_rllib.py | 157 ---------------- examples/train_stable_baselines.py | 126 ------------- tests/fast_tests/test_examples.py | 4 +- tutorials/tutorial13_rllib_ec2.ipynb | 6 +- 7 files changed, 282 insertions(+), 298 deletions(-) create mode 100644 examples/train.py delete mode 100644 examples/train_rllib.py delete mode 100644 examples/train_stable_baselines.py diff --git a/docs/source/flow_setup.rst b/docs/source/flow_setup.rst index 2b52e20de..606a9d6d4 100644 --- a/docs/source/flow_setup.rst +++ b/docs/source/flow_setup.rst @@ -243,7 +243,7 @@ In order to test run an Flow experiment in RLlib, try the following command: :: - python examples/train_rllib.py singleagent_ring + python examples/train.py singleagent_ring If it does not fail, this means that you have Flow properly configured with @@ -264,7 +264,7 @@ You can test your installation by running :: - python examples/train_stable_baselines.py singleagent_ring + python examples/train.py singleagent_ring --rl_trainer Stable-Baselines diff --git a/examples/README.md b/examples/README.md index 907c7bda1..52c40dc0e 100644 --- a/examples/README.md +++ b/examples/README.md @@ -16,7 +16,9 @@ python simulate.py ring The examples are categorized into the following 3 sections: -**non-RL examples** contains examples of transportation network with vehicles +## non-RL examples + +These are examples of transportation network with vehicles following human-dynamical models of driving behavior using the traffic micro-simulator sumo and traffic macro-simulator Aimsun. @@ -35,36 +37,43 @@ There are several *optional* arguments that can be added to the above command: ``` where `--num_runs` indicates the number of simulations to run (default of `n` is 1), `--render` indicates whether to run the simulation during runtime (default is False), `--aimsun` indicates whether to run the simulation using the simulator Aimsun (the default simulator is SUMO), and `--gen_emission` indicates whether to generate an emission file from the simulation. -**RL examples based on RLlib** provides similar networks as those presented in -the first point, but in the present of autonomous vehicle (AV) or traffic light agents +## RL examples based on RLlib + +These examples are similar networks as those mentioned in *non-RL examples*, but in the +presence of autonomous vehicle (AV) or traffic light agents being trained through RL algorithms provided by *RLlib*. To execute these examples, run ```shell - python train_rllib.py EXP_CONFIG + python train.py EXP_CONFIG + (or python train.py EXP_CONFIG --rl_trainer RLlib) ``` where `EXP_CONFIG` is the name of the experiment configuration file, as located in `exp_configs/rl/singleagent` or `exp_configs/rl/multiagent.` -**RL examples based on stable_baselines** provides similar networks as those -presented in the first point, but in the present of autonomous vehicle (AV) or traffic +## RL examples based on stable_baselines + +These examples provide similar networks as those +mentioned in *non-RL examples*, but in the presence of autonomous vehicle (AV) or traffic light agents being trained through RL algorithms provided by OpenAI *stable baselines*. To execute these examples, run ```shell - python train_stable_baselines.py EXP_CONFIG + python train.py EXP_CONFIG --rl_trainer Stable-Baselines ``` where `EXP_CONFIG` is the name of the experiment configuration file, as located in `exp_configs/rl/singleagent.` +Note that, currently, multiagent experiments are only supported through RLlib. + There are several *optional* arguments that can be added to the above command: ```shell - python train_stable_baselines.py EXP_CONFIG --num_cpus n1 --num_steps n2 --rollout_size r + python train.py EXP_CONFIG --rl_trainer Stable-Baselines --num_cpus n1 --num_steps n2 --rollout_size r ``` where `--num_cpus` indicates the number of CPUs to use (default of `n1` is 1), `--num_steps` indicates the total steps to perform the learning (default of `n2` is 5000), and `--rollout_size` indicates the number of steps in a training batch (default of `r` is 1000) diff --git a/examples/train.py b/examples/train.py new file mode 100644 index 000000000..bbd482688 --- /dev/null +++ b/examples/train.py @@ -0,0 +1,258 @@ +"""Runner script for single and multi-agent reinforcement learning experiments. + +This script performs an RL experiment using the PPO algorithm. Choice of +hyperparameters can be seen and adjusted from the code below. + +Usage + python train.py EXP_CONFIG +""" + +import argparse +import json +import os +import sys +from time import strftime + +from stable_baselines.common.vec_env import DummyVecEnv, SubprocVecEnv +from stable_baselines import PPO2 + +import ray +from ray import tune +from ray.tune import run_experiments +from ray.tune.registry import register_env +from flow.utils.registry import make_create_env +try: + from ray.rllib.agents.agent import get_agent_class +except ImportError: + from ray.rllib.agents.registry import get_agent_class +from copy import deepcopy + +from flow.core.util import ensure_dir +from flow.utils.registry import env_constructor +from flow.utils.rllib import FlowParamsEncoder, get_flow_params + + +def parse_args(args): + """Parse training options user can specify in command line. + + Returns + ------- + argparse.Namespace + the output parser object + """ + parser = argparse.ArgumentParser( + formatter_class=argparse.RawDescriptionHelpFormatter, + description="Parse argument used when running a Flow simulation.", + epilog="python train.py EXP_CONFIG") + + # required input parameters + parser.add_argument( + 'exp_config', type=str, + help='Name of the experiment configuration file, as located in ' + 'exp_configs/rl/singleagent or exp_configs/rl/multiagent.') + + # optional input parameters + parser.add_argument( + '--rl_trainer', type=str, default="RLlib", + help='the RL trainer to use. either RLlib or Stable-Baselines') + + parser.add_argument( + '--num_cpus', type=int, default=1, + help='How many CPUs to use') + parser.add_argument( + '--num_steps', type=int, default=5000, + help='How many total steps to perform learning over') + parser.add_argument( + '--rollout_size', type=int, default=1000, + help='How many steps are in a training batch.') + + return parser.parse_known_args(args)[0] + + +def run_model_stablebaseline(flow_params, num_cpus=1, rollout_size=50, num_steps=50): + """Run the model for num_steps if provided. + + Parameters + ---------- + num_cpus : int + number of CPUs used during training + rollout_size : int + length of a single rollout + num_steps : int + total number of training steps + The total rollout length is rollout_size. + + Returns + ------- + stable_baselines.* + the trained model + """ + if num_cpus == 1: + constructor = env_constructor(params=flow_params, version=0)() + # The algorithms require a vectorized environment to run + env = DummyVecEnv([lambda: constructor]) + else: + env = SubprocVecEnv([env_constructor(params=flow_params, version=i) + for i in range(num_cpus)]) + + train_model = PPO2('MlpPolicy', env, verbose=1, n_steps=rollout_size) + train_model.learn(total_timesteps=num_steps) + return train_model + + +def setup_exps_rllib(flow_params, + n_cpus, + n_rollouts, + policy_graphs=None, + policy_mapping_fn=None, + policies_to_train=None): + """Return the relevant components of an RLlib experiment. + + Parameters + ---------- + flow_params : dict + flow-specific parameters (see flow/utils/registry.py) + n_cpus : int + number of CPUs to run the experiment over + n_rollouts : int + number of rollouts per training iteration + policy_graphs : dict, optional + TODO + policy_mapping_fn : function, optional + TODO + policies_to_train : list of str, optional + TODO + + Returns + ------- + str + name of the training algorithm + str + name of the gym environment to be trained + dict + training configuration parameters + """ + horizon = flow_params['env'].horizon + + alg_run = "PPO" + + agent_cls = get_agent_class(alg_run) + config = deepcopy(agent_cls._default_config) + + config["num_workers"] = n_cpus + config["train_batch_size"] = horizon * n_rollouts + config["gamma"] = 0.999 # discount rate + config["model"].update({"fcnet_hiddens": [32, 32, 32]}) + config["use_gae"] = True + config["lambda"] = 0.97 + config["kl_target"] = 0.02 + config["num_sgd_iter"] = 10 + config['clip_actions'] = False # FIXME(ev) temporary ray bug + config["horizon"] = horizon + + # save the flow params for replay + flow_json = json.dumps( + flow_params, cls=FlowParamsEncoder, sort_keys=True, indent=4) + config['env_config']['flow_params'] = flow_json + config['env_config']['run'] = alg_run + + # multiagent configuration + if policy_graphs is not None: + print("policy_graphs", policy_graphs) + config['multiagent'].update({'policies': policy_graphs}) + if policy_mapping_fn is not None: + config['multiagent'].update({'policy_mapping_fn': tune.function(policy_mapping_fn)}) + if policies_to_train is not None: + config['multiagent'].update({'policies_to_train': policies_to_train}) + + create_env, gym_name = make_create_env(params=flow_params) + + # Register as rllib env + register_env(gym_name, create_env) + return alg_run, gym_name, config + + +if __name__ == "__main__": + flags = parse_args(sys.argv[1:]) + + # import relevant information from the exp_config script + module = __import__("exp_configs.rl.singleagent", fromlist=[flags.exp_config]) + module_ma = __import__("exp_configs.rl.multiagent", fromlist=[flags.exp_config]) + if hasattr(module, flags.exp_config): + submodule = getattr(module, flags.exp_config) + elif hasattr(module_ma, flags.exp_config): + submodule = getattr(module_ma, flags.exp_config) + assert flags.rl_trainer == "RLlib", \ + "Currently, multiagent experiments are only supported through "\ + "RLlib. Try running this experiment using RLlib: 'python train.py EXP_CONFIG'" + else: + assert False, "Unable to find experiment config!" + if flags.rl_trainer == "RLlib": + flow_params = submodule.flow_params + n_cpus = submodule.N_CPUS + n_rollouts = submodule.N_ROLLOUTS + policy_graphs = getattr(submodule, "POLICY_GRAPHS", None) + policy_mapping_fn = getattr(submodule, "policy_mapping_fn", None) + policies_to_train = getattr(submodule, "policies_to_train", None) + + alg_run, gym_name, config = setup_exps_rllib( + flow_params, n_cpus, n_rollouts, + policy_graphs, policy_mapping_fn, policies_to_train) + + ray.init(num_cpus=n_cpus + 1) + trials = run_experiments({ + flow_params["exp_tag"]: { + "run": alg_run, + "env": gym_name, + "config": { + **config + }, + "checkpoint_freq": 20, + "checkpoint_at_end": True, + "max_failures": 999, + "stop": { + "training_iteration": 200, + }, + } + }) + + elif flags.rl_trainer == "Stable-Baselines": + flow_params = submodule.flow_params + # Path to the saved files + exp_tag = flow_params['exp_tag'] + result_name = '{}/{}'.format(exp_tag, strftime("%Y-%m-%d-%H:%M:%S")) + + # Perform training. + print('Beginning training.') + model = run_model_stablebaseline(flow_params, flags.num_cpus, flags.rollout_size, flags.num_steps) + + # Save the model to a desired folder and then delete it to demonstrate + # loading. + print('Saving the trained model!') + path = os.path.realpath(os.path.expanduser('~/baseline_results')) + ensure_dir(path) + save_path = os.path.join(path, result_name) + model.save(save_path) + + # dump the flow params + with open(os.path.join(path, result_name) + '.json', 'w') as outfile: + json.dump(flow_params, outfile, + cls=FlowParamsEncoder, sort_keys=True, indent=4) + + # Replay the result by loading the model + print('Loading the trained model and testing it out!') + model = PPO2.load(save_path) + flow_params = get_flow_params(os.path.join(path, result_name) + '.json') + flow_params['sim'].render = True + env_constructor = env_constructor(params=flow_params, version=0)() + # The algorithms require a vectorized environment to run + eval_env = DummyVecEnv([lambda: env_constructor]) + obs = eval_env.reset() + reward = 0 + for _ in range(flow_params['env'].horizon): + action, _states = model.predict(obs) + obs, rewards, dones, info = eval_env.step(action) + reward += rewards + print('the final reward is {}'.format(reward)) + else: + assert False, "rl_trainer should be either 'RLlib' or 'Stable-Baselines'!" diff --git a/examples/train_rllib.py b/examples/train_rllib.py deleted file mode 100644 index 4f370c99c..000000000 --- a/examples/train_rllib.py +++ /dev/null @@ -1,157 +0,0 @@ -"""Runner script for single and multi-agent RLlib experiments. - -This script performs an RL experiment using the PPO algorithm. Choice of -hyperparameters can be seen and adjusted from the code below. - -Usage - python train_rllib.py EXP_CONFIG -""" -import sys -import json -import argparse -import ray -from ray import tune -from ray.tune import run_experiments -from ray.tune.registry import register_env -from flow.utils.rllib import FlowParamsEncoder -from flow.utils.registry import make_create_env -try: - from ray.rllib.agents.agent import get_agent_class -except ImportError: - from ray.rllib.agents.registry import get_agent_class -from copy import deepcopy - - -def parse_args(args): - """Parse training options user can specify in command line. - - Returns - ------- - argparse.Namespace - the output parser object - """ - parser = argparse.ArgumentParser( - formatter_class=argparse.RawDescriptionHelpFormatter, - description="Parse argument used when running a Flow simulation.", - epilog="python train_rllib.py EXP_CONFIG") - - # required input parameters - parser.add_argument( - 'exp_config', type=str, - help='Name of the experiment configuration file, as located in ' - 'exp_configs/rl/singleagent' or 'exp_configs/rl/multiagent.') - - return parser.parse_known_args(args)[0] - - -def setup_exps(flow_params, - n_cpus, - n_rollouts, - policy_graphs=None, - policy_mapping_fn=None, - policies_to_train=None): - """Return the relevant components of an RLlib experiment. - - Parameters - ---------- - flow_params : dict - flow-specific parameters (see flow/utils/registry.py) - n_cpus : int - number of CPUs to run the experiment over - n_rollouts : int - number of rollouts per training iteration - policy_graphs : dict, optional - TODO - policy_mapping_fn : function, optional - TODO - policies_to_train : list of str, optional - TODO - - Returns - ------- - str - name of the training algorithm - str - name of the gym environment to be trained - dict - training configuration parameters - """ - horizon = flow_params['env'].horizon - - alg_run = "PPO" - - agent_cls = get_agent_class(alg_run) - config = deepcopy(agent_cls._default_config) - - config["num_workers"] = n_cpus - config["train_batch_size"] = horizon * n_rollouts - config["gamma"] = 0.999 # discount rate - config["model"].update({"fcnet_hiddens": [32, 32, 32]}) - config["use_gae"] = True - config["lambda"] = 0.97 - config["kl_target"] = 0.02 - config["num_sgd_iter"] = 10 - config['clip_actions'] = False # FIXME(ev) temporary ray bug - config["horizon"] = horizon - - # save the flow params for replay - flow_json = json.dumps( - flow_params, cls=FlowParamsEncoder, sort_keys=True, indent=4) - config['env_config']['flow_params'] = flow_json - config['env_config']['run'] = alg_run - - # multiagent configuration - if policy_graphs is not None: - print("policy_graphs", policy_graphs) - config['multiagent'].update({'policies': policy_graphs}) - if policy_mapping_fn is not None: - config['multiagent'].update({'policy_mapping_fn': tune.function(policy_mapping_fn)}) - if policies_to_train is not None: - config['multiagent'].update({'policies_to_train': policies_to_train}) - - create_env, gym_name = make_create_env(params=flow_params) - - # Register as rllib env - register_env(gym_name, create_env) - return alg_run, gym_name, config - - -if __name__ == "__main__": - flags = parse_args(sys.argv[1:]) - - # import relevant information from the exp_config script - module = __import__("exp_configs.rl.singleagent", fromlist=[flags.exp_config]) - module_ma = __import__("exp_configs.rl.multiagent", fromlist=[flags.exp_config]) - if hasattr(module, flags.exp_config): - submodule = getattr(module, flags.exp_config) - elif hasattr(module_ma, flags.exp_config): - submodule = getattr(module_ma, flags.exp_config) - else: - assert False, "Unable to find experiment config!" - flow_params = submodule.flow_params - n_cpus = submodule.N_CPUS - n_rollouts = submodule.N_ROLLOUTS - policy_graphs = getattr(submodule, "POLICY_GRAPHS", None) - policy_mapping_fn = getattr(submodule, "policy_mapping_fn", None) - policies_to_train = getattr(submodule, "policies_to_train", None) - - alg_run, gym_name, config = setup_exps( - flow_params, n_cpus, n_rollouts, - policy_graphs, policy_mapping_fn, policies_to_train) - - ray.init(num_cpus=n_cpus + 1) - trials = run_experiments({ - flow_params["exp_tag"]: { - "run": alg_run, - "env": gym_name, - "config": { - **config - }, - "checkpoint_freq": 20, - "checkpoint_at_end": True, - "max_failures": 999, - "stop": { - "training_iteration": 200, - }, - } - }) diff --git a/examples/train_stable_baselines.py b/examples/train_stable_baselines.py deleted file mode 100644 index 37cc42062..000000000 --- a/examples/train_stable_baselines.py +++ /dev/null @@ -1,126 +0,0 @@ -"""Runner script for implementing stable-baseline experiments with Flow. - -Usage - python train_stable_baselines.py EXP_CONFIG -""" -import argparse -import json -import os -import sys -from time import strftime - -from stable_baselines.common.vec_env import DummyVecEnv, SubprocVecEnv -from stable_baselines import PPO2 - -from flow.core.util import ensure_dir -from flow.utils.registry import env_constructor -from flow.utils.rllib import FlowParamsEncoder, get_flow_params - - -def parse_args(args): - """Parse training options user can specify in command line. - - Returns - ------- - argparse.Namespace - the output parser object - """ - parser = argparse.ArgumentParser( - formatter_class=argparse.RawDescriptionHelpFormatter, - description="Parse argument used when running a Flow simulation.", - epilog="python train_stable_baselines.py EXP_CONFIG") - - # required input parameters - parser.add_argument( - 'exp_config', type=str, - help='Name of the experiment configuration file, as located in ' - 'exp_configs/rl/singleagent.') - - # optional input parameters - parser.add_argument( - '--num_cpus', type=int, default=1, - help='How many CPUs to use') - parser.add_argument( - '--num_steps', type=int, default=5000, - help='How many total steps to perform learning over') - parser.add_argument( - '--rollout_size', type=int, default=1000, - help='How many steps are in a training batch.') - - return parser.parse_known_args(args)[0] - - -def run_model(flow_params, num_cpus=1, rollout_size=50, num_steps=50): - """Run the model for num_steps if provided. - - Parameters - ---------- - num_cpus : int - number of CPUs used during training - rollout_size : int - length of a single rollout - num_steps : int - total number of training steps - The total rollout length is rollout_size. - - Returns - ------- - stable_baselines.* - the trained model - """ - if num_cpus == 1: - constructor = env_constructor(params=flow_params, version=0)() - # The algorithms require a vectorized environment to run - env = DummyVecEnv([lambda: constructor]) - else: - env = SubprocVecEnv([env_constructor(params=flow_params, version=i) - for i in range(num_cpus)]) - - train_model = PPO2('MlpPolicy', env, verbose=1, n_steps=rollout_size) - train_model.learn(total_timesteps=num_steps) - return train_model - - -if __name__ == '__main__': - flags = parse_args(sys.argv[1:]) - - # Get the flow_params object. - module = __import__('exp_configs.rl.singleagent', fromlist=[flags.exp_config]) - flow_params = getattr(module, flags.exp_config).flow_params - - # Path to the saved files - exp_tag = flow_params['exp_tag'] - result_name = '{}/{}'.format(exp_tag, strftime("%Y-%m-%d-%H:%M:%S")) - - # Perform training. - print('Beginning training.') - model = run_model(flow_params, flags.num_cpus, flags.rollout_size, flags.num_steps) - - # Save the model to a desired folder and then delete it to demonstrate - # loading. - print('Saving the trained model!') - path = os.path.realpath(os.path.expanduser('~/baseline_results')) - ensure_dir(path) - save_path = os.path.join(path, result_name) - model.save(save_path) - - # dump the flow params - with open(os.path.join(path, result_name) + '.json', 'w') as outfile: - json.dump(flow_params, outfile, - cls=FlowParamsEncoder, sort_keys=True, indent=4) - - # Replay the result by loading the model - print('Loading the trained model and testing it out!') - model = PPO2.load(save_path) - flow_params = get_flow_params(os.path.join(path, result_name) + '.json') - flow_params['sim'].render = True - env_constructor = env_constructor(params=flow_params, version=0)() - # The algorithms require a vectorized environment to run - eval_env = DummyVecEnv([lambda: env_constructor]) - obs = eval_env.reset() - reward = 0 - for _ in range(flow_params['env'].horizon): - action, _states = model.predict(obs) - obs, rewards, dones, info = eval_env.step(action) - reward += rewards - print('the final reward is {}'.format(reward)) diff --git a/tests/fast_tests/test_examples.py b/tests/fast_tests/test_examples.py index df8c881f9..82d9bf6c2 100644 --- a/tests/fast_tests/test_examples.py +++ b/tests/fast_tests/test_examples.py @@ -21,8 +21,8 @@ # make_flow_params as multi_grid_setup_flow_params from examples.exp_configs.rl.multiagent.multiagent_highway import flow_params as multiagent_highway -from examples.train_stable_baselines import run_model as run_stable_baselines_model -from examples.train_rllib import setup_exps as setup_rllib_exps +from examples.train import run_model_stablebaseline as run_stable_baselines_model +from examples.train import setup_exps_rllib as setup_rllib_exps from examples.exp_configs.non_rl.bay_bridge import flow_params as non_rl_bay_bridge from examples.exp_configs.non_rl.bay_bridge_toll import flow_params as non_rl_bay_bridge_toll diff --git a/tutorials/tutorial13_rllib_ec2.ipynb b/tutorials/tutorial13_rllib_ec2.ipynb index 1811e0133..9bb6764d1 100644 --- a/tutorials/tutorial13_rllib_ec2.ipynb +++ b/tutorials/tutorial13_rllib_ec2.ipynb @@ -49,7 +49,7 @@ " \n", "2. Use the `ray exec` command to communicate with your cluster. \n", "\n", - " `ray exec ray_autoscale.yaml \"flow/examples/train_rllib.py singleagent_ring\"`\n", + " `ray exec ray_autoscale.yaml \"flow/examples/train.py singleagent_ring\"`\n", " * For a list of options you can provide to this command which will enable a variety of helpful options such as running in tmux or stopping after the command completes, view the link at the beginning of this tutorial.\n", " \n", "3. Attach to the cluster via `ray attach`.\n", @@ -61,7 +61,7 @@ "Note that the above steps 2-3 can become tedious if you create multiple clusters, and thus there are many versions of ray_autoscale.yaml lying around. For further explanation, read on: ray commands identify clusters according to the cluster_name attribute in ray_autoscale.yaml, so if you create 'test_0', test_1', 'test_2', 'test_3', and 'test_4' by simply erasing 'test_0' and replacing it with 'test_1', and so on, you would have to manually change the cluster_name in ray_autoscale.yaml to specify which cluster you intend to interact with while using `ray attach`, `ray exec`, or other `ray` commands. An alternative is this: when the cluster is created i.e. after `ray up ray_autoscale.yaml -y` is successful, it returns a ssh command to connect to that cluster's IP directly. When running multiple clusters, it can be useful to save these ssh commands.\n", "\n", "Note note, that a helpful, streamlined method of starting and executing a cluster in one fell swoop can be done via:
\n", - "4. `ray exec ray_autoscale.yaml \"flow/examples/train_rllib.py singleagent_ring\" --start`\n" + "4. `ray exec ray_autoscale.yaml \"flow/examples/train.py singleagent_ring\" --start`\n" ] }, { @@ -155,4 +155,4 @@ }, "nbformat": 4, "nbformat_minor": 2 -} +} \ No newline at end of file From 47cf289102fc0a915da73aa700496aaac27febf5 Mon Sep 17 00:00:00 2001 From: Ashkan Y Date: Fri, 10 Jan 2020 11:14:28 -0800 Subject: [PATCH 35/86] make render true by default --- examples/README.md | 4 ++-- examples/simulate.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/README.md b/examples/README.md index 52c40dc0e..f25f488c5 100644 --- a/examples/README.md +++ b/examples/README.md @@ -33,9 +33,9 @@ where `EXP_CONFIG` is the name of the experiment configuration file, as located There are several *optional* arguments that can be added to the above command: ```shell - python simulate.py EXP_CONFIG --num_runs n --render --aimsun --gen_emission + python simulate.py EXP_CONFIG --num_runs n --no_render --aimsun --gen_emission ``` -where `--num_runs` indicates the number of simulations to run (default of `n` is 1), `--render` indicates whether to run the simulation during runtime (default is False), `--aimsun` indicates whether to run the simulation using the simulator Aimsun (the default simulator is SUMO), and `--gen_emission` indicates whether to generate an emission file from the simulation. +where `--num_runs` indicates the number of simulations to run (default of `n` is 1), `--no_render` indicates whether to deactivate the simulation GUI during runtime (by default simulation GUI is active), `--aimsun` indicates whether to run the simulation using the simulator Aimsun (the default simulator is SUMO), and `--gen_emission` indicates whether to generate an emission file from the simulation. ## RL examples based on RLlib diff --git a/examples/simulate.py b/examples/simulate.py index 6c3164bcf..04967b830 100644 --- a/examples/simulate.py +++ b/examples/simulate.py @@ -1,7 +1,7 @@ """Runner script for non-RL simulations in flow. Usage - python simulate.py EXP_CONFIG --render + python simulate.py EXP_CONFIG --no_render """ import argparse import sys @@ -19,7 +19,7 @@ def parse_args(args): parser = argparse.ArgumentParser( formatter_class=argparse.RawDescriptionHelpFormatter, description="Parse argument used when running a Flow simulation.", - epilog="python simulate.py EXP_CONFIG --num_runs INT --render") + epilog="python simulate.py EXP_CONFIG --num_runs INT --no_render") # required input parameters parser.add_argument( @@ -32,7 +32,7 @@ def parse_args(args): '--num_runs', type=int, default=1, help='Number of simulations to run. Defaults to 1.') parser.add_argument( - '--render', + '--no_render', action='store_true', help='Specifies whether to run the simulation during runtime.') parser.add_argument( @@ -57,7 +57,7 @@ def parse_args(args): flow_params = getattr(module, flags.exp_config).flow_params # Update some variables based on inputs. - flow_params['sim'].render = flags.render + flow_params['sim'].render = not flags.no_render flow_params['simulator'] = 'aimsun' if flags.aimsun else 'traci' # specify an emission path if they are meant to be generated From 5eac69157ace8e691f951b6d6d39425d0a9711d6 Mon Sep 17 00:00:00 2001 From: Ashkan Y Date: Tue, 21 Jan 2020 01:18:23 -0800 Subject: [PATCH 36/86] Tutorial for multiagent (#808) * Creating tutorial for multi-agent * some cleanup * Making sure the jupyter file runs completely Co-authored-by: Aboudy Kreidieh --- tutorials/tutorial14_mutiagent.ipynb | 549 +++++++++++++++++++++++++++ 1 file changed, 549 insertions(+) create mode 100644 tutorials/tutorial14_mutiagent.ipynb diff --git a/tutorials/tutorial14_mutiagent.ipynb b/tutorials/tutorial14_mutiagent.ipynb new file mode 100644 index 000000000..4f28e1cdf --- /dev/null +++ b/tutorials/tutorial14_mutiagent.ipynb @@ -0,0 +1,549 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Tutorial 14: Multiagent\n", + "\n", + "This tutorial covers the implementation of multiagent experiments in Flow. It assumes some level of knowledge or experience in writing custom environments and running experiments with RLlib. The rest of the tutorial is organized as follows. Section 1 describes the procedure through which custom environments can be augmented to generate multiagent environments. Then, section 2 walks you through an example of running a multiagent environment\n", + "in RLlib." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. Creating a Multiagent Environment Class\n", + "\n", + "In this part we will be setting up steps to create a multiagent environment. We begin by importing the abstract multi-agent evironment class." + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "# import the base Multi-agent environment \n", + "from flow.envs.multiagent.base import MultiEnv" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In multiagent experiments, the agent can either share a policy (\"shared policy\") or have different policies (\"non-shared policy\"). In the following subsections, we describe the two." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 1.1 Shared policies\n", + "In the multi-agent environment with a shared policy, different agents will use the same policy. \n", + "\n", + "We define the environment class, and inherit properties from the Multi-agent version of base env." + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "class SharedMultiAgentEnv(MultiEnv):\n", + " pass" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This environment will provide the interface for running and modifying the multiagent experiment. Using this class, we are able to start the simulation (e.g. in SUMO), provide a network to specify a configuration and controllers, perform simulation steps, and reset the simulation to an initial configuration.\n", + "\n", + "For the multi-agent experiments, certain functions of the `MultiEnv` will be changed according to the agents. Some functions will be defined according to a *single* agent, while the other functions will be defined according to *all* agents.\n", + "\n", + "In the follwing functions, observation space and action space will be defined for a *single* agent (not all agents):\n", + "\n", + "* **observation_space**\n", + "* **action_space**\n", + "\n", + "For instance, in a multiagent traffic light grid, if each agents is considered as a single intersection controlling the traffic lights of the intersection, the observation space can be define as *normalized* velocities and distance to a *single* intersection for nearby vehicles, that is defined for every intersection. " + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "def observation_space(self):\n", + " \"\"\"State space that is partially observed.\n", + "\n", + " Velocities and distance to intersections for nearby\n", + " vehicles ('num_observed') from each direction.\n", + " \"\"\"\n", + " tl_box = Box(\n", + " low=0.,\n", + " high=1,\n", + " shape=(2 * 4 * self.num_observed),\n", + " dtype=np.float32)\n", + " return tl_box" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The action space can be defined for a *single* intersection as follows" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "def action_space(self):\n", + " \"\"\"See class definition.\"\"\"\n", + " if self.discrete: \n", + " # each intersection is an agent, and the action is simply 0 or 1. \n", + " # - 0 means no-change in the traffic light \n", + " # - 1 means switch the direction\n", + " return Discrete(2)\n", + " else:\n", + " return Box(low=0, high=1, shape=(1,), dtype=np.float32)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Conversely, the following functions (including their return values) will be defined to take into account *all* agents:\n", + "\n", + "* **apply_rl_actions**\n", + "* **get_state**\n", + "* **compute_reward**\n", + "\n", + "Instead of calculating actions, state, and reward for a single agent, in these functions, the ctions, state, and reward will be calculated for all the agents in the system. To do so, we create a dictionary with agent ids as keys and different parameters (actions, state, and reward ) as vaules. For example, in the following `_apply_rl_actions` function, based on the action of intersections (0 or 1), the state of the intersections' traffic lights will be changed." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "class SharedMultiAgentEnv(MultiEnv): \n", + " def _apply_rl_actions(self, rl_actions):\n", + " for agent_name in rl_actions:\n", + " action = rl_actions[agent_name]\n", + " # check if the action space is discrete\n", + " \n", + " # check if our timer has exceeded the yellow phase, meaning it\n", + " # should switch to red\n", + " if self.currently_yellow[tl_num] == 1: # currently yellow\n", + " self.last_change[tl_num] += self.sim_step\n", + " if self.last_change[tl_num] >= self.min_switch_time: # check if our timer has exceeded the yellow phase, meaning it\n", + " # should switch to red\n", + " if self.direction[tl_num] == 0:\n", + " self.k.traffic_light.set_state(\n", + " node_id='center{}'.format(tl_num),\n", + " state=\"GrGr\")\n", + " else:\n", + " self.k.traffic_light.set_state(\n", + " node_id='center{}'.format(tl_num),\n", + " state='rGrG')\n", + " self.currently_yellow[tl_num] = 0\n", + " else:\n", + " if action:\n", + " if self.direction[tl_num] == 0:\n", + " self.k.traffic_light.set_state(\n", + " node_id='center{}'.format(tl_num),\n", + " state='yryr')\n", + " else:\n", + " self.k.traffic_light.set_state(\n", + " node_id='center{}'.format(tl_num),\n", + " state='ryry')\n", + " self.last_change[tl_num] = 0.0\n", + " self.direction[tl_num] = not self.direction[tl_num]\n", + " self.currently_yellow[tl_num] = 1" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Similarly, the `get_state` and `compute_reward` methods support the dictionary structure and add the observation and reward, respectively, as a value for each correpsonding key, that is agent id. " + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "class SharedMultiAgentEnv(MultiEnv): \n", + "\n", + " def get_state(self):\n", + " \"\"\"Observations for each intersection\n", + "\n", + " :return: dictionary which contains agent-wise observations as follows:\n", + " - For the self.num_observed number of vehicles closest and incomingsp\n", + " towards traffic light agent, gives the vehicle velocity and distance to\n", + " intersection.\n", + " \"\"\"\n", + " # Normalization factors\n", + " max_speed = max(\n", + " self.k.network.speed_limit(edge)\n", + " for edge in self.k.network.get_edge_list())\n", + " max_dist = max(grid_array[\"short_length\"], grid_array[\"long_length\"],\n", + " grid_array[\"inner_length\"])\n", + "\n", + " # Observed vehicle information\n", + " speeds = []\n", + " dist_to_intersec = []\n", + "\n", + " for _, edges in self.network.node_mapping:\n", + " local_speeds = []\n", + " local_dists_to_intersec = []\n", + " # .... More code here (removed for simplicity of example)\n", + " # ....\n", + "\n", + " speeds.append(local_speeds)\n", + " dist_to_intersec.append(local_dists_to_intersec)\n", + " \n", + " obs = {}\n", + " for agent_id in self.k.traffic_light.get_ids():\n", + " # .... More code here (removed for simplicity of example)\n", + " # ....\n", + " observation = np.array(np.concatenate(speeds, dist_to_intersec))\n", + " obs.update({agent_id: observation})\n", + " return obs\n", + "\n", + "\n", + " def compute_reward(self, rl_actions, **kwargs):\n", + " if rl_actions is None:\n", + " return {}\n", + "\n", + " if self.env_params.evaluate:\n", + " rew = -rewards.min_delay_unscaled(self)\n", + " else:\n", + " rew = -rewards.min_delay_unscaled(self) \\\n", + " + rewards.penalize_standstill(self, gain=0.2)\n", + "\n", + " # each agent receives reward normalized by number of lights\n", + " rew /= self.num_traffic_lights\n", + "\n", + " rews = {}\n", + " for rl_id in rl_actions.keys():\n", + " rews[rl_id] = rew\n", + " return rews" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 1.2 Non-shared policies\n", + "\n", + "In the multi-agent environment with a non-shared policy, different agents will use different policies. In what follows we will see the two agents in a ring road using two different policies, 'adversary' and 'av' (non-adversary).\n", + "\n", + "Similarly to the shared policies, observation space and action space will be defined for a *single* agent (not all agents):\n", + "\n", + "* **observation_space**\n", + "* **action_space**\n", + "\n", + "And, the following functions (including their return values) will be defined to take into account *all* agents::\n", + "\n", + "* **apply_rl_actions**\n", + "* **get_state**\n", + "* **compute_reward**\n", + "\n", + "\\* Note that, when observation space and action space will be defined for a single agent, it means that all agents should have the same dimension (i.e. space) of observation and action, even when their policise are not the same. " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Let us start with defining `apply_rl_actions` function. In order to make it work for a non-shared policy multi-agent ring road, we define `rl_actions` as a combinations of each policy actions plus the `perturb_weight`." + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "class NonSharedMultiAgentEnv(MultiEnv):\n", + " def _apply_rl_actions(self, rl_actions):\n", + " # the names of all autonomous (RL) vehicles in the network\n", + " agent_ids = [\n", + " veh_id for veh_id in self.sorted_ids\n", + " if veh_id in self.k.vehicle.get_rl_ids()\n", + " ]\n", + " # define different actions for different multi-agents \n", + " av_action = rl_actions['av']\n", + " adv_action = rl_actions['adversary']\n", + " perturb_weight = self.env_params.additional_params['perturb_weight']\n", + " rl_action = av_action + perturb_weight * adv_action\n", + " \n", + " # use the base environment method to convert actions into accelerations for the rl vehicles\n", + " self.k.vehicle.apply_acceleration(agent_ids, rl_action)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In the `get_state` method, we define the state for each of the agents. Remember, the sate of the agents can be different. For the purpose of this example and simplicity, we define the state of the adversary and non-adversary agent to be the same. \n", + "\n", + "In the `compute_reward` method, the agents receive opposing speed rewards. The reward of the adversary agent is more when the speed of the vehicles is small, while the non-adversary agent tries to increase the speeds of the vehicles." + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "class NonSharedMultiAgentEnv(MultiEnv):\n", + " def get_state(self, **kwargs):\n", + " state = np.array([[\n", + " self.k.vehicle.get_speed(veh_id) / self.k.network.max_speed(),\n", + " self.k.vehicle.get_x_by_id(veh_id) / self.k.network.length()\n", + " ] for veh_id in self.sorted_ids])\n", + " state = np.ndarray.flatten(state)\n", + " return {'av': state, 'adversary': state}\n", + "\n", + " def compute_reward(self, rl_actions, **kwargs):\n", + " if self.env_params.evaluate:\n", + " reward = np.mean(self.k.vehicle.get_speed(\n", + " self.k.vehicle.get_ids()))\n", + " return {'av': reward, 'adversary': -reward}\n", + " else:\n", + " reward = rewards.desired_velocity(self, fail=kwargs['fail'])\n", + " return {'av': reward, 'adversary': -reward}" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. Running Multiagent Environment in RLlib" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "When running the experiment that uses a multiagent environment, we specify certain parameters in the `flow_params` dictionary. \n", + "\n", + "Similar to any other experiments, the following snippets of codes will be inserted into a blank python file (e.g. `new_multiagent_experiment.py`, and should be saved under `flow/examples/exp_configs/rl/multiagent/` directory. (all the basic imports and initialization of variables are omitted in this example for brevity)\n" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "from flow.envs.multiagent import MultiWaveAttenuationPOEnv\n", + "from flow.networks import MultiRingNetwork\n", + "from flow.core.params import SumoParams, EnvParams, NetParams, VehicleParams, InitialConfig\n", + "from flow.controllers import ContinuousRouter, IDMController, RLController\n", + "\n", + "# time horizon of a single rollout\n", + "HORIZON = 3000\n", + "# Number of rings\n", + "NUM_RINGS = 1\n", + "\n", + "vehicles = VehicleParams()\n", + "for i in range(NUM_RINGS):\n", + " vehicles.add(\n", + " veh_id='human_{}'.format(i),\n", + " acceleration_controller=(IDMController, {\n", + " 'noise': 0.2\n", + " }),\n", + " routing_controller=(ContinuousRouter, {}),\n", + " num_vehicles=21)\n", + " vehicles.add(\n", + " veh_id='rl_{}'.format(i),\n", + " acceleration_controller=(RLController, {}),\n", + " routing_controller=(ContinuousRouter, {}),\n", + " num_vehicles=1)\n", + "\n", + "flow_params = dict(\n", + " # name of the experiment\n", + " exp_tag='multiagent_ring_road',\n", + "\n", + " # name of the flow environment the experiment is running on\n", + " env_name=MultiWaveAttenuationPOEnv,\n", + "\n", + " # name of the network class the experiment is running on\n", + " network=MultiRingNetwork,\n", + "\n", + " # simulator that is used by the experiment\n", + " simulator='traci',\n", + "\n", + " # sumo-related parameters (see flow.core.params.SumoParams)\n", + " sim=SumoParams(\n", + " sim_step=0.1,\n", + " render=False,\n", + " ),\n", + "\n", + " # environment related parameters (see flow.core.params.EnvParams)\n", + " env=EnvParams(\n", + " horizon=HORIZON,\n", + " warmup_steps=750,\n", + " additional_params={\n", + " 'max_accel': 1,\n", + " 'max_decel': 1,\n", + " 'ring_length': [230, 230],\n", + " 'target_velocity': 4\n", + " },\n", + " ),\n", + "\n", + " # network-related parameters \n", + " net=NetParams(\n", + " additional_params={\n", + " 'length': 230,\n", + " 'lanes': 1,\n", + " 'speed_limit': 30,\n", + " 'resolution': 40,\n", + " 'num_rings': NUM_RINGS\n", + " },\n", + " ),\n", + "\n", + " # vehicles to be placed in the network at the start of a rollout\n", + " veh=vehicles,\n", + "\n", + " # parameters specifying the positioning of vehicles upon initialization/\n", + " # reset\n", + " initial=InitialConfig(bunching=20.0, spacing='custom'),\n", + ")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Then we run the following code to create the environment " + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "from flow.utils.registry import make_create_env\n", + "from ray.tune.registry import register_env\n", + "\n", + "create_env, env_name = make_create_env(params=flow_params, version=0)\n", + "\n", + "# Register as rllib env\n", + "register_env(env_name, create_env)\n", + "\n", + "test_env = create_env()\n", + "obs_space = test_env.observation_space\n", + "act_space = test_env.action_space" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 2.1 Shared policies\n", + "\n", + "When we run a shared-policy multiagent experiment, we refer to the same policy for each agent. In the example below the agents will use 'av' policy." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [], + "source": [ + "from ray.rllib.agents.ppo.ppo_policy import PPOTFPolicy\n", + "\n", + "def gen_policy():\n", + " \"\"\"Generate a policy in RLlib.\"\"\"\n", + " return PPOTFPolicy, obs_space, act_space, {}\n", + "\n", + "\n", + "# Setup PG with an ensemble of `num_policies` different policy graphs\n", + "POLICY_GRAPHS = {'av': gen_policy()}\n", + "\n", + "\n", + "def policy_mapping_fn(_):\n", + " \"\"\"Map a policy in RLlib.\"\"\"\n", + " return 'av'\n", + "\n", + "\n", + "POLICIES_TO_TRAIN = ['av']" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "### 2.2 Non-shared policies\n", + "\n", + "When we run the non-shared multiagent experiment, we refer to different policies for each agent. In the example below, the policy graph will have two policies, 'adversary' and 'av' (non-adversary)." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "def gen_policy():\n", + " \"\"\"Generate a policy in RLlib.\"\"\"\n", + " return PPOTFPolicy, obs_space, act_space, {}\n", + "\n", + "\n", + "# Setup PG with an ensemble of `num_policies` different policy graphs\n", + "POLICY_GRAPHS = {'av': gen_policy(), 'adversary': gen_policy()}\n", + "\n", + "\n", + "def policy_mapping_fn(agent_id):\n", + " \"\"\"Map a policy in RLlib.\"\"\"\n", + " return agent_id" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Lastly, just like any other experiments, we run our code using `train_rllib.py` as follows:\n", + "\n", + " python flow/examples/train_rllib.py new_multiagent_experiment.py" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.6.9" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} From 363d003b47c71916dc6c736ff270c6e5b560fe0a Mon Sep 17 00:00:00 2001 From: Dominik Kleiser Date: Fri, 31 Jan 2020 19:35:38 +0100 Subject: [PATCH 37/86] Fix path to ring simulation (#822) Signed-off-by: Dominik Kleiser --- tutorials/tutorial04_visualize.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tutorials/tutorial04_visualize.ipynb b/tutorials/tutorial04_visualize.ipynb index 3f4eb82bc..25da3f322 100644 --- a/tutorials/tutorial04_visualize.ipynb +++ b/tutorials/tutorial04_visualize.ipynb @@ -293,7 +293,7 @@ "metadata": {}, "outputs": [], "source": [ - "!python ../examples/sumo/sugiyama.py" + "!python ../examples/simulate.py ring" ] }, { From 509791145866f933bc6647614d88c85291549a54 Mon Sep 17 00:00:00 2001 From: chendiw <31671291+chendiw@users.noreply.github.com> Date: Mon, 10 Feb 2020 18:39:27 -0800 Subject: [PATCH 38/86] deleting unworking params from SumoChangeLaneParams, sublanes working (#830) * deleting unworking params from SumoChangeLaneParams * deleted unworking params, sublane working in highway : * Apply suggestions from code review * bug fix Co-authored-by: Aboudy Kreidieh --- examples/exp_configs/non_rl/highway.py | 11 ++++++++++- flow/core/params.py | 19 ------------------- tests/fast_tests/test_params.py | 9 ++------- 3 files changed, 12 insertions(+), 27 deletions(-) diff --git a/examples/exp_configs/non_rl/highway.py b/examples/exp_configs/non_rl/highway.py index 7b2222301..e7505f2d7 100644 --- a/examples/exp_configs/non_rl/highway.py +++ b/examples/exp_configs/non_rl/highway.py @@ -1,7 +1,7 @@ """Example of an open multi-lane network with human-driven vehicles.""" from flow.controllers import IDMController -from flow.core.params import SumoParams, EnvParams, NetParams, InitialConfig +from flow.core.params import SumoParams, EnvParams, NetParams, InitialConfig, SumoLaneChangeParams from flow.core.params import VehicleParams, InFlows from flow.envs.ring.lane_change_accel import ADDITIONAL_ENV_PARAMS from flow.networks.highway import HighwayNetwork, ADDITIONAL_NET_PARAMS @@ -11,10 +11,18 @@ vehicles.add( veh_id="human", acceleration_controller=(IDMController, {}), + lane_change_params=SumoLaneChangeParams( + model="SL2015", + lc_sublane=2.0, + ), num_vehicles=20) vehicles.add( veh_id="human2", acceleration_controller=(IDMController, {}), + lane_change_params=SumoLaneChangeParams( + model="SL2015", + lc_sublane=2.0, + ), num_vehicles=20) env_params = EnvParams(additional_params=ADDITIONAL_ENV_PARAMS) @@ -50,6 +58,7 @@ # sumo-related parameters (see flow.core.params.SumoParams) sim=SumoParams( render=True, + lateral_resolution=1.0, ), # environment related parameters (see flow.core.params.EnvParams) diff --git a/flow/core/params.py b/flow/core/params.py index 61b95223c..07cbdb4c8 100755 --- a/flow/core/params.py +++ b/flow/core/params.py @@ -914,10 +914,6 @@ class SumoLaneChangeParams: see lcPushyGap in Note lc_assertive : float, optional see lcAssertive in Note - lc_impatience : float, optional - see lcImpatience in Note - lc_time_to_impatience : float, optional - see lcTimeToImpatience in Note lc_accel_lat : float, optional see lcAccelLate in Note kwargs : dict @@ -942,8 +938,6 @@ def __init__(self, lc_pushy=0, lc_pushy_gap=0.6, lc_assertive=1, - lc_impatience=0, - lc_time_to_impatience=float("inf"), lc_accel_lat=1.0, **kwargs): """Instantiate SumoLaneChangeParams.""" @@ -998,17 +992,6 @@ def __init__(self, deprecated_attribute(self, "lcAssertive", "lc_assertive") lc_assertive = kwargs["lcAssertive"] - # check for deprecations (lcImpatience) - if "lcImpatience" in kwargs: - deprecated_attribute(self, "lcImpatience", "lc_impatience") - lc_impatience = kwargs["lcImpatience"] - - # check for deprecations (lcTimeToImpatience) - if "lcTimeToImpatience" in kwargs: - deprecated_attribute(self, "lcTimeToImpatience", - "lc_time_to_impatience") - lc_time_to_impatience = kwargs["lcTimeToImpatience"] - # check for deprecations (lcAccelLat) if "lcAccelLat" in kwargs: deprecated_attribute(self, "lcAccelLat", "lc_accel_lat") @@ -1042,8 +1025,6 @@ def __init__(self, "lcPushy": str(lc_pushy), "lcPushyGap": str(lc_pushy_gap), "lcAssertive": str(lc_assertive), - "lcImpatience": str(lc_impatience), - "lcTimeToImpatience": str(lc_time_to_impatience), "lcAccelLat": str(lc_accel_lat) } diff --git a/tests/fast_tests/test_params.py b/tests/fast_tests/test_params.py index d009af489..df4794d28 100644 --- a/tests/fast_tests/test_params.py +++ b/tests/fast_tests/test_params.py @@ -203,8 +203,7 @@ def test_lc_params(self): expected_attributes_2 = \ ["laneChangeModel", "lcStrategic", "lcCooperative", "lcSpeedGain", "lcKeepRight", "lcLookaheadLeft", "lcSpeedGainRight", "lcSublane", - "lcPushy", "lcPushyGap", "lcAssertive", "lcImpatience", - "lcTimeToImpatience", "lcAccelLat"] + "lcPushy", "lcPushyGap", "lcAssertive", "lcAccelLat"] self.assertCountEqual(attributes_2, expected_attributes_2) def test_wrong_model(self): @@ -240,9 +239,7 @@ def test_deprecated(self): lcSublane=1.0, lcPushy=0, lcPushyGap=0.6, - lcAssertive=1, - lcImpatience=0, - lcTimeToImpatience=float("inf")) + lcAssertive=1) # ensure that the attributes match their correct element in the # "controller_params" dict @@ -262,8 +259,6 @@ def test_deprecated(self): float(lc_params.controller_params["lcPushyGap"]), 0.6) self.assertAlmostEqual( float(lc_params.controller_params["lcAssertive"]), 1) - self.assertAlmostEqual( - float(lc_params.controller_params["lcImpatience"]), 0) if __name__ == '__main__': From 7dc20963f0db1cb0edf892611e88d6866866dd13 Mon Sep 17 00:00:00 2001 From: Eugene Vinitsky Date: Mon, 17 Feb 2020 08:44:41 -0800 Subject: [PATCH 39/86] Update README.md (#832) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 2a7d8bd4f..7d37223c5 100644 --- a/README.md +++ b/README.md @@ -21,7 +21,7 @@ See [our website](https://flow-project.github.io/) for more information on the a # Technical questions -If you have a bug, please report it. Otherwise, join the [Flow Users group](https://forms.gle/CuVBu6QtX3dfNaxz6) on Slack! You'll recieve an email shortly after filling out the form. +If you have a bug, please report it. Otherwise, join the [Flow Users group](https://join.slack.com/t/flow-users/shared_invite/enQtODQ0NDYxMTQyNDY2LTY1ZDVjZTljM2U0ODIxNTY5NTQ2MmUxMzYzNzc5NzU4ZTlmNGI2ZjFmNGU4YjVhNzE3NjcwZTBjNzIxYTg5ZmY) on Slack! # Getting involved From 3e8fc0ccc5130012e54fc9b882b72a3f857ce75e Mon Sep 17 00:00:00 2001 From: Aboudy Kreidieh Date: Tue, 18 Feb 2020 16:31:20 -0800 Subject: [PATCH 40/86] Multiagent environments (#818) * renamed MultiAgentAccelEnv -> AdversarialAccelEnv and it's dependents as well * added MultiAgentAccelPOEnv * bug fix * bug fix * added MultiAgentWaveAttenuationPOEnv * added MultiAgentMergePOEnv * added tests * test to observed * added reset to wave attenuation env * test to observed in wave attenuation env * added figure eight example * added multiagent merge example * renamed multiagent_ring -> lord_of_the_rings * added an additional test * added multiagent ring example * test not being hit * added time debugger * bug fix to done mask * bug associated with warmup steps --- .../rl/multiagent/adversarial_figure_eight.py | 127 ++++++++ .../rl/multiagent/lord_of_the_rings.py | 122 ++++++++ .../rl/multiagent/multiagent_figure_eight.py | 115 ++++---- .../rl/multiagent/multiagent_merge.py | 159 ++++++++++ .../rl/multiagent/multiagent_ring.py | 90 +++--- flow/core/experiment.py | 16 +- flow/envs/multiagent/__init__.py | 20 +- flow/envs/multiagent/base.py | 3 +- flow/envs/multiagent/merge.py | 190 ++++++++++++ flow/envs/multiagent/ring/accel.py | 187 +++++++++++- flow/envs/multiagent/ring/wave_attenuation.py | 185 +++++++++++- flow/envs/ring/wave_attenuation.py | 7 +- flow/multiagent_envs/__init__.py | 4 +- flow/multiagent_envs/loop/loop_accel.py | 6 +- tests/fast_tests/test_environments.py | 279 +++++++++++++++++- tests/fast_tests/test_examples.py | 45 ++- 16 files changed, 1427 insertions(+), 128 deletions(-) create mode 100644 examples/exp_configs/rl/multiagent/adversarial_figure_eight.py create mode 100644 examples/exp_configs/rl/multiagent/lord_of_the_rings.py create mode 100644 examples/exp_configs/rl/multiagent/multiagent_merge.py create mode 100644 flow/envs/multiagent/merge.py diff --git a/examples/exp_configs/rl/multiagent/adversarial_figure_eight.py b/examples/exp_configs/rl/multiagent/adversarial_figure_eight.py new file mode 100644 index 000000000..4fb81ce97 --- /dev/null +++ b/examples/exp_configs/rl/multiagent/adversarial_figure_eight.py @@ -0,0 +1,127 @@ +"""Example of a multi-agent environment containing a figure eight. + +This example consists of one autonomous vehicle and an adversary that is +allowed to perturb the accelerations of figure eight. +""" + +# WARNING: Expected total reward is zero as adversary reward is +# the negative of the AV reward + +from copy import deepcopy +from ray.rllib.agents.ppo.ppo_policy import PPOTFPolicy +from flow.controllers import ContinuousRouter +from flow.controllers import IDMController +from flow.controllers import RLController +from flow.core.params import EnvParams +from flow.core.params import InitialConfig +from flow.core.params import NetParams +from flow.core.params import SumoParams +from flow.core.params import SumoCarFollowingParams +from flow.core.params import VehicleParams +from flow.networks.figure_eight import ADDITIONAL_NET_PARAMS +from flow.envs.multiagent import AdversarialAccelEnv +from flow.networks import FigureEightNetwork +from flow.utils.registry import make_create_env +from ray.tune.registry import register_env + +# time horizon of a single rollout +HORIZON = 1500 +# number of rollouts per training iteration +N_ROLLOUTS = 4 +# number of parallel workers +N_CPUS = 2 +# number of human-driven vehicles +N_HUMANS = 13 +# number of automated vehicles +N_AVS = 1 + +# We place one autonomous vehicle and 13 human-driven vehicles in the network +vehicles = VehicleParams() +vehicles.add( + veh_id='human', + acceleration_controller=(IDMController, { + 'noise': 0.2 + }), + routing_controller=(ContinuousRouter, {}), + car_following_params=SumoCarFollowingParams( + speed_mode='obey_safe_speed', + ), + num_vehicles=N_HUMANS) +vehicles.add( + veh_id='rl', + acceleration_controller=(RLController, {}), + routing_controller=(ContinuousRouter, {}), + car_following_params=SumoCarFollowingParams( + speed_mode='obey_safe_speed', + ), + num_vehicles=N_AVS) + +flow_params = dict( + # name of the experiment + exp_tag='adversarial_figure_eight', + + # name of the flow environment the experiment is running on + env_name=AdversarialAccelEnv, + + # name of the network class the experiment is running on + network=FigureEightNetwork, + + # simulator that is used by the experiment + simulator='traci', + + # sumo-related parameters (see flow.core.params.SumoParams) + sim=SumoParams( + sim_step=0.1, + render=False, + ), + + # environment related parameters (see flow.core.params.EnvParams) + env=EnvParams( + horizon=HORIZON, + additional_params={ + 'target_velocity': 20, + 'max_accel': 3, + 'max_decel': 3, + 'perturb_weight': 0.03, + 'sort_vehicles': False + }, + ), + + # network-related parameters (see flow.core.params.NetParams and the + # network's documentation or ADDITIONAL_NET_PARAMS component) + net=NetParams( + additional_params=deepcopy(ADDITIONAL_NET_PARAMS), + ), + + # vehicles to be placed in the network at the start of a rollout (see + # flow.core.params.VehicleParams) + veh=vehicles, + + # parameters specifying the positioning of vehicles upon initialization/ + # reset (see flow.core.params.InitialConfig) + initial=InitialConfig(), +) + + +create_env, env_name = make_create_env(params=flow_params, version=0) + +# Register as rllib env +register_env(env_name, create_env) + +test_env = create_env() +obs_space = test_env.observation_space +act_space = test_env.action_space + + +def gen_policy(): + """Generate a policy in RLlib.""" + return PPOTFPolicy, obs_space, act_space, {} + + +# Setup PG with an ensemble of `num_policies` different policy graphs +POLICY_GRAPHS = {'av': gen_policy(), 'adversary': gen_policy()} + + +def policy_mapping_fn(agent_id): + """Map a policy in RLlib.""" + return agent_id diff --git a/examples/exp_configs/rl/multiagent/lord_of_the_rings.py b/examples/exp_configs/rl/multiagent/lord_of_the_rings.py new file mode 100644 index 000000000..e7688c87d --- /dev/null +++ b/examples/exp_configs/rl/multiagent/lord_of_the_rings.py @@ -0,0 +1,122 @@ +"""Ring road example. + +Creates a set of stabilizing the ring experiments to test if + more agents -> fewer needed batches +""" +from ray.rllib.agents.ppo.ppo_policy import PPOTFPolicy +from flow.controllers import ContinuousRouter +from flow.controllers import IDMController +from flow.controllers import RLController +from flow.core.params import EnvParams +from flow.core.params import InitialConfig +from flow.core.params import NetParams +from flow.core.params import SumoParams +from flow.core.params import VehicleParams +from flow.envs.multiagent import MultiWaveAttenuationPOEnv +from flow.networks import MultiRingNetwork +from flow.utils.registry import make_create_env +from ray.tune.registry import register_env + +# make sure (sample_batch_size * num_workers ~= train_batch_size) +# time horizon of a single rollout +HORIZON = 3000 +# Number of rings +NUM_RINGS = 1 +# number of rollouts per training iteration +N_ROLLOUTS = 20 # int(20/NUM_RINGS) +# number of parallel workers +N_CPUS = 2 # int(20/NUM_RINGS) + +# We place one autonomous vehicle and 21 human-driven vehicles in the network +vehicles = VehicleParams() +for i in range(NUM_RINGS): + vehicles.add( + veh_id='human_{}'.format(i), + acceleration_controller=(IDMController, { + 'noise': 0.2 + }), + routing_controller=(ContinuousRouter, {}), + num_vehicles=21) + vehicles.add( + veh_id='rl_{}'.format(i), + acceleration_controller=(RLController, {}), + routing_controller=(ContinuousRouter, {}), + num_vehicles=1) + +flow_params = dict( + # name of the experiment + exp_tag='lord_of_numrings{}'.format(NUM_RINGS), + + # name of the flow environment the experiment is running on + env_name=MultiWaveAttenuationPOEnv, + + # name of the network class the experiment is running on + network=MultiRingNetwork, + + # simulator that is used by the experiment + simulator='traci', + + # sumo-related parameters (see flow.core.params.SumoParams) + sim=SumoParams( + sim_step=0.1, + render=False, + ), + + # environment related parameters (see flow.core.params.EnvParams) + env=EnvParams( + horizon=HORIZON, + warmup_steps=750, + additional_params={ + 'max_accel': 1, + 'max_decel': 1, + 'ring_length': [230, 230], + 'target_velocity': 4 + }, + ), + + # network-related parameters (see flow.core.params.NetParams and the + # network's documentation or ADDITIONAL_NET_PARAMS component) + net=NetParams( + additional_params={ + 'length': 230, + 'lanes': 1, + 'speed_limit': 30, + 'resolution': 40, + 'num_rings': NUM_RINGS + }, ), + + # vehicles to be placed in the network at the start of a rollout (see + # flow.core.params.VehicleParams) + veh=vehicles, + + # parameters specifying the positioning of vehicles upon initialization/ + # reset (see flow.core.params.InitialConfig) + initial=InitialConfig(bunching=20.0, spacing='custom'), +) + + +create_env, env_name = make_create_env(params=flow_params, version=0) + +# Register as rllib env +register_env(env_name, create_env) + +test_env = create_env() +obs_space = test_env.observation_space +act_space = test_env.action_space + + +def gen_policy(): + """Generate a policy in RLlib.""" + return PPOTFPolicy, obs_space, act_space, {} + + +# Setup PG with an ensemble of `num_policies` different policy graphs +POLICY_GRAPHS = {'av': gen_policy()} + + +def policy_mapping_fn(_): + """Map a policy in RLlib.""" + return 'av' + + +POLICIES_TO_TRAIN = ['av'] diff --git a/examples/exp_configs/rl/multiagent/multiagent_figure_eight.py b/examples/exp_configs/rl/multiagent/multiagent_figure_eight.py index d541c4edd..0579bb978 100644 --- a/examples/exp_configs/rl/multiagent/multiagent_figure_eight.py +++ b/examples/exp_configs/rl/multiagent/multiagent_figure_eight.py @@ -1,67 +1,72 @@ -"""Example of a multi-agent environment containing a figure eight. - -This example consists of one autonomous vehicle and an adversary that is -allowed to perturb the accelerations of figure eight. -""" - -# WARNING: Expected total reward is zero as adversary reward is -# the negative of the AV reward - -from copy import deepcopy +"""Figure eight example.""" from ray.rllib.agents.ppo.ppo_policy import PPOTFPolicy -from flow.controllers import ContinuousRouter -from flow.controllers import IDMController -from flow.controllers import RLController -from flow.core.params import EnvParams -from flow.core.params import InitialConfig -from flow.core.params import NetParams -from flow.core.params import SumoParams -from flow.core.params import SumoCarFollowingParams -from flow.core.params import VehicleParams +from ray.tune.registry import register_env + +from flow.core.params import SumoParams, EnvParams, InitialConfig, NetParams +from flow.core.params import VehicleParams, SumoCarFollowingParams +from flow.controllers import IDMController, ContinuousRouter, RLController from flow.networks.figure_eight import ADDITIONAL_NET_PARAMS -from flow.envs.multiagent import MultiAgentAccelEnv +from flow.envs.multiagent import MultiAgentAccelPOEnv from flow.networks import FigureEightNetwork from flow.utils.registry import make_create_env -from ray.tune.registry import register_env # time horizon of a single rollout HORIZON = 1500 # number of rollouts per training iteration -N_ROLLOUTS = 4 +N_ROLLOUTS = 20 # number of parallel workers N_CPUS = 2 -# number of human-driven vehicles -N_HUMANS = 13 -# number of automated vehicles -N_AVS = 1 -# We place one autonomous vehicle and 13 human-driven vehicles in the network +# desired velocity for all vehicles in the network, in m/s +TARGET_VELOCITY = 20 +# maximum acceleration for autonomous vehicles, in m/s^2 +MAX_ACCEL = 3 +# maximum deceleration for autonomous vehicles, in m/s^2 +MAX_DECEL = 3 + +# number of automated vehicles. Must be one of [1, 2, 7, 14] +NUM_AUTOMATED = 2 + + +assert NUM_AUTOMATED in [1, 2, 7, 14], \ + "num_automated must be one of [1, 2, 7 14]" + + +# We evenly distribute the autonomous vehicles in between the human-driven +# vehicles in the network. +num_human = 14 - NUM_AUTOMATED +human_per_automated = int(num_human / NUM_AUTOMATED) + vehicles = VehicleParams() -vehicles.add( - veh_id='human', - acceleration_controller=(IDMController, { - 'noise': 0.2 - }), - routing_controller=(ContinuousRouter, {}), - car_following_params=SumoCarFollowingParams( - speed_mode='obey_safe_speed', - ), - num_vehicles=N_HUMANS) -vehicles.add( - veh_id='rl', - acceleration_controller=(RLController, {}), - routing_controller=(ContinuousRouter, {}), - car_following_params=SumoCarFollowingParams( - speed_mode='obey_safe_speed', - ), - num_vehicles=N_AVS) +for i in range(NUM_AUTOMATED): + vehicles.add( + veh_id='human_{}'.format(i), + acceleration_controller=(IDMController, { + 'noise': 0.2 + }), + routing_controller=(ContinuousRouter, {}), + car_following_params=SumoCarFollowingParams( + speed_mode="obey_safe_speed", + decel=1.5, + ), + num_vehicles=human_per_automated) + vehicles.add( + veh_id='rl_{}'.format(i), + acceleration_controller=(RLController, {}), + routing_controller=(ContinuousRouter, {}), + car_following_params=SumoCarFollowingParams( + speed_mode="obey_safe_speed", + accel=MAX_ACCEL, + decel=MAX_DECEL, + ), + num_vehicles=1) flow_params = dict( # name of the experiment exp_tag='multiagent_figure_eight', # name of the flow environment the experiment is running on - env_name=MultiAgentAccelEnv, + env_name=MultiAgentAccelPOEnv, # name of the network class the experiment is running on network=FigureEightNetwork, @@ -79,10 +84,9 @@ env=EnvParams( horizon=HORIZON, additional_params={ - 'target_velocity': 20, - 'max_accel': 3, - 'max_decel': 3, - 'perturb_weight': 0.03, + 'target_velocity': TARGET_VELOCITY, + 'max_accel': MAX_ACCEL, + 'max_decel': MAX_DECEL, 'sort_vehicles': False }, ), @@ -90,7 +94,7 @@ # network-related parameters (see flow.core.params.NetParams and the # network's documentation or ADDITIONAL_NET_PARAMS component) net=NetParams( - additional_params=deepcopy(ADDITIONAL_NET_PARAMS), + additional_params=ADDITIONAL_NET_PARAMS.copy(), ), # vehicles to be placed in the network at the start of a rollout (see @@ -119,9 +123,12 @@ def gen_policy(): # Setup PG with an ensemble of `num_policies` different policy graphs -POLICY_GRAPHS = {'av': gen_policy(), 'adversary': gen_policy()} +POLICY_GRAPHS = {'av': gen_policy()} -def policy_mapping_fn(agent_id): +def policy_mapping_fn(_): """Map a policy in RLlib.""" - return agent_id + return 'av' + + +POLICIES_TO_TRAIN = ['av'] diff --git a/examples/exp_configs/rl/multiagent/multiagent_merge.py b/examples/exp_configs/rl/multiagent/multiagent_merge.py new file mode 100644 index 000000000..bfc9fb3b7 --- /dev/null +++ b/examples/exp_configs/rl/multiagent/multiagent_merge.py @@ -0,0 +1,159 @@ +"""Open merge example. + +Trains a a small percentage of rl vehicles to dissipate shockwaves caused by +on-ramp merge to a single lane open highway network. +""" +from ray.rllib.agents.ppo.ppo_policy import PPOTFPolicy +from ray.tune.registry import register_env + +from flow.core.params import SumoParams, EnvParams, InitialConfig +from flow.core.params import NetParams, InFlows, SumoCarFollowingParams +from flow.networks.merge import ADDITIONAL_NET_PARAMS +from flow.core.params import VehicleParams +from flow.controllers import IDMController, RLController +from flow.envs.multiagent import MultiAgentMergePOEnv +from flow.networks import MergeNetwork +from flow.utils.registry import make_create_env + +# experiment number +# - 0: 10% RL penetration, 5 max controllable vehicles +# - 1: 25% RL penetration, 13 max controllable vehicles +# - 2: 33% RL penetration, 17 max controllable vehicles +EXP_NUM = 0 + +# time horizon of a single rollout +HORIZON = 600 +# number of rollouts per training iteration +N_ROLLOUTS = 20 +# number of parallel workers +N_CPUS = 2 + +# inflow rate at the highway +FLOW_RATE = 2000 +# percent of autonomous vehicles +RL_PENETRATION = [0.1, 0.25, 0.33][EXP_NUM] +# num_rl term (see ADDITIONAL_ENV_PARAMs) +NUM_RL = [5, 13, 17][EXP_NUM] + +# We consider a highway network with an upstream merging lane producing +# shockwaves +additional_net_params = ADDITIONAL_NET_PARAMS.copy() +additional_net_params["merge_lanes"] = 1 +additional_net_params["highway_lanes"] = 1 +additional_net_params["pre_merge_length"] = 500 + +# RL vehicles constitute 5% of the total number of vehicles +vehicles = VehicleParams() +vehicles.add( + veh_id="human", + acceleration_controller=(IDMController, { + "noise": 0.2 + }), + car_following_params=SumoCarFollowingParams( + speed_mode="obey_safe_speed", + ), + num_vehicles=5) +vehicles.add( + veh_id="rl", + acceleration_controller=(RLController, {}), + car_following_params=SumoCarFollowingParams( + speed_mode="obey_safe_speed", + ), + num_vehicles=0) + +# Vehicles are introduced from both sides of merge, with RL vehicles entering +# from the highway portion as well +inflow = InFlows() +inflow.add( + veh_type="human", + edge="inflow_highway", + vehs_per_hour=(1 - RL_PENETRATION) * FLOW_RATE, + departLane="free", + departSpeed=10) +inflow.add( + veh_type="rl", + edge="inflow_highway", + vehs_per_hour=RL_PENETRATION * FLOW_RATE, + departLane="free", + departSpeed=10) +inflow.add( + veh_type="human", + edge="inflow_merge", + vehs_per_hour=100, + departLane="free", + departSpeed=7.5) + +flow_params = dict( + # name of the experiment + exp_tag="multiagent_merge", + + # name of the flow environment the experiment is running on + env_name=MultiAgentMergePOEnv, + + # name of the network class the experiment is running on + network=MergeNetwork, + + # simulator that is used by the experiment + simulator='traci', + + # sumo-related parameters (see flow.core.params.SumoParams) + sim=SumoParams( + sim_step=0.2, + render=False, + restart_instance=True, + ), + + # environment related parameters (see flow.core.params.EnvParams) + env=EnvParams( + horizon=HORIZON, + sims_per_step=5, + warmup_steps=0, + additional_params={ + "max_accel": 1.5, + "max_decel": 1.5, + "target_velocity": 20, + }, + ), + + # network-related parameters (see flow.core.params.NetParams and the + # network's documentation or ADDITIONAL_NET_PARAMS component) + net=NetParams( + inflows=inflow, + additional_params=additional_net_params, + ), + + # vehicles to be placed in the network at the start of a rollout (see + # flow.core.params.VehicleParams) + veh=vehicles, + + # parameters specifying the positioning of vehicles upon initialization/ + # reset (see flow.core.params.InitialConfig) + initial=InitialConfig(), +) + + +create_env, env_name = make_create_env(params=flow_params, version=0) + +# Register as rllib env +register_env(env_name, create_env) + +test_env = create_env() +obs_space = test_env.observation_space +act_space = test_env.action_space + + +def gen_policy(): + """Generate a policy in RLlib.""" + return PPOTFPolicy, obs_space, act_space, {} + + +# Setup PG with an ensemble of `num_policies` different policy graphs +POLICY_GRAPHS = {'av': gen_policy()} + + +def policy_mapping_fn(_): + """Map a policy in RLlib.""" + return 'av' + + +POLICIES_TO_TRAIN = ['av'] diff --git a/examples/exp_configs/rl/multiagent/multiagent_ring.py b/examples/exp_configs/rl/multiagent/multiagent_ring.py index e7688c87d..a789174f4 100644 --- a/examples/exp_configs/rl/multiagent/multiagent_ring.py +++ b/examples/exp_configs/rl/multiagent/multiagent_ring.py @@ -1,57 +1,65 @@ """Ring road example. -Creates a set of stabilizing the ring experiments to test if - more agents -> fewer needed batches +Trains a number of autonomous vehicles to stabilize the flow of 22 vehicles in +a variable length ring road. """ from ray.rllib.agents.ppo.ppo_policy import PPOTFPolicy -from flow.controllers import ContinuousRouter -from flow.controllers import IDMController -from flow.controllers import RLController -from flow.core.params import EnvParams -from flow.core.params import InitialConfig -from flow.core.params import NetParams -from flow.core.params import SumoParams -from flow.core.params import VehicleParams -from flow.envs.multiagent import MultiWaveAttenuationPOEnv -from flow.networks import MultiRingNetwork -from flow.utils.registry import make_create_env from ray.tune.registry import register_env -# make sure (sample_batch_size * num_workers ~= train_batch_size) +from flow.core.params import SumoParams, EnvParams, InitialConfig, NetParams +from flow.core.params import VehicleParams, SumoCarFollowingParams +from flow.controllers import RLController, IDMController, ContinuousRouter +from flow.envs.multiagent import MultiAgentWaveAttenuationPOEnv +from flow.networks import RingNetwork +from flow.utils.registry import make_create_env + # time horizon of a single rollout HORIZON = 3000 -# Number of rings -NUM_RINGS = 1 # number of rollouts per training iteration -N_ROLLOUTS = 20 # int(20/NUM_RINGS) +N_ROLLOUTS = 20 # number of parallel workers -N_CPUS = 2 # int(20/NUM_RINGS) +N_CPUS = 2 +# number of automated vehicles. Must be less than or equal to 22. +NUM_AUTOMATED = 2 + + +# We evenly distribute the automated vehicles in the network. +num_human = 22 - NUM_AUTOMATED +humans_remaining = num_human -# We place one autonomous vehicle and 21 human-driven vehicles in the network vehicles = VehicleParams() -for i in range(NUM_RINGS): +for i in range(NUM_AUTOMATED): + # Add one automated vehicle. vehicles.add( - veh_id='human_{}'.format(i), - acceleration_controller=(IDMController, { - 'noise': 0.2 - }), - routing_controller=(ContinuousRouter, {}), - num_vehicles=21) - vehicles.add( - veh_id='rl_{}'.format(i), + veh_id="rl_{}".format(i), acceleration_controller=(RLController, {}), routing_controller=(ContinuousRouter, {}), num_vehicles=1) + # Add a fraction of the remaining human vehicles. + vehicles_to_add = round(humans_remaining / (NUM_AUTOMATED - i)) + humans_remaining -= vehicles_to_add + vehicles.add( + veh_id="human_{}".format(i), + acceleration_controller=(IDMController, { + "noise": 0.2 + }), + car_following_params=SumoCarFollowingParams( + min_gap=0 + ), + routing_controller=(ContinuousRouter, {}), + num_vehicles=vehicles_to_add) + + flow_params = dict( # name of the experiment - exp_tag='lord_of_numrings{}'.format(NUM_RINGS), + exp_tag="multiagent_ring", # name of the flow environment the experiment is running on - env_name=MultiWaveAttenuationPOEnv, + env_name=MultiAgentWaveAttenuationPOEnv, # name of the network class the experiment is running on - network=MultiRingNetwork, + network=RingNetwork, # simulator that is used by the experiment simulator='traci', @@ -60,17 +68,18 @@ sim=SumoParams( sim_step=0.1, render=False, + restart_instance=False ), # environment related parameters (see flow.core.params.EnvParams) env=EnvParams( horizon=HORIZON, warmup_steps=750, + clip_actions=False, additional_params={ - 'max_accel': 1, - 'max_decel': 1, - 'ring_length': [230, 230], - 'target_velocity': 4 + "max_accel": 1, + "max_decel": 1, + "ring_length": [220, 270], }, ), @@ -78,11 +87,10 @@ # network's documentation or ADDITIONAL_NET_PARAMS component) net=NetParams( additional_params={ - 'length': 230, - 'lanes': 1, - 'speed_limit': 30, - 'resolution': 40, - 'num_rings': NUM_RINGS + "length": 260, + "lanes": 1, + "speed_limit": 30, + "resolution": 40, }, ), # vehicles to be placed in the network at the start of a rollout (see @@ -91,7 +99,7 @@ # parameters specifying the positioning of vehicles upon initialization/ # reset (see flow.core.params.InitialConfig) - initial=InitialConfig(bunching=20.0, spacing='custom'), + initial=InitialConfig(), ) diff --git a/flow/core/experiment.py b/flow/core/experiment.py index 53547d851..7a66a58ee 100755 --- a/flow/core/experiment.py +++ b/flow/core/experiment.py @@ -115,6 +115,9 @@ def rl_actions(*_): mean_vels = [] std_vels = [] outflows = [] + t = time.time() + times = [] + vehicle_times = [] for i in range(num_runs): vel = np.zeros(num_steps) logging.info("Iter #" + str(i)) @@ -122,7 +125,11 @@ def rl_actions(*_): ret_list = [] state = self.env.reset() for j in range(num_steps): + t0 = time.time() state, reward, done, _ = self.env.step(rl_actions(state)) + t1 = time.time() + times.append(1 / (t1 - t0)) + vehicle_times.append(self.env.k.vehicle.num_vehicles / (t1 - t0)) vel[j] = np.mean( self.env.k.vehicle.get_speed(self.env.k.vehicle.get_ids())) ret += reward @@ -145,13 +152,16 @@ def rl_actions(*_): info_dict["per_step_returns"] = ret_lists info_dict["mean_outflows"] = np.mean(outflows) - print("Average, std return: {}, {}".format( + print("Average, std return: {}, {}".format( np.mean(rets), np.std(rets))) - print("Average, std speed: {}, {}".format( + print("Average, std speed: {}, {}".format( np.mean(mean_vels), np.std(mean_vels))) + print("Total time: ", time.time() - t) + print("steps/second: ", np.mean(times)) + print("vehicles.steps/second: ", np.mean(vehicle_times)) self.env.terminate() - if convert_to_csv: + if convert_to_csv and self.env.simulator == "traci": # wait a short period of time to ensure the xml file is readable time.sleep(0.1) diff --git a/flow/envs/multiagent/__init__.py b/flow/envs/multiagent/__init__.py index 4c43611aa..2ae6780b8 100644 --- a/flow/envs/multiagent/__init__.py +++ b/flow/envs/multiagent/__init__.py @@ -3,10 +3,22 @@ from flow.envs.multiagent.base import MultiEnv from flow.envs.multiagent.ring.wave_attenuation import \ MultiWaveAttenuationPOEnv - -from flow.envs.multiagent.ring.accel import MultiAgentAccelEnv +from flow.envs.multiagent.ring.wave_attenuation import \ + MultiAgentWaveAttenuationPOEnv +from flow.envs.multiagent.ring.accel import AdversarialAccelEnv +from flow.envs.multiagent.ring.accel import MultiAgentAccelPOEnv from flow.envs.multiagent.traffic_light_grid import MultiTrafficLightGridPOEnv from flow.envs.multiagent.highway import MultiAgentHighwayPOEnv +from flow.envs.multiagent.merge import MultiAgentMergePOEnv + -__all__ = ['MultiEnv', 'MultiAgentAccelEnv', 'MultiWaveAttenuationPOEnv', - 'MultiTrafficLightGridPOEnv', 'MultiAgentHighwayPOEnv'] +__all__ = [ + 'MultiEnv', + 'AdversarialAccelEnv', + 'MultiWaveAttenuationPOEnv', + 'MultiTrafficLightGridPOEnv', + 'MultiAgentHighwayPOEnv', + 'MultiAgentAccelPOEnv', + 'MultiAgentWaveAttenuationPOEnv', + 'MultiAgentMergePOEnv' +] diff --git a/flow/envs/multiagent/base.py b/flow/envs/multiagent/base.py index 6019f73f5..72b927505 100644 --- a/flow/envs/multiagent/base.py +++ b/flow/envs/multiagent/base.py @@ -108,7 +108,8 @@ def step(self, rl_actions): states = self.get_state() done = {key: key in self.k.vehicle.get_arrived_ids() for key in states.keys()} - if crash: + if crash or (self.time_counter >= self.env_params.warmup_steps + + self.env_params.horizon): done['__all__'] = True else: done['__all__'] = False diff --git a/flow/envs/multiagent/merge.py b/flow/envs/multiagent/merge.py new file mode 100644 index 000000000..42189419a --- /dev/null +++ b/flow/envs/multiagent/merge.py @@ -0,0 +1,190 @@ +"""Environment for training vehicles to reduce congestion in a merge.""" + +from flow.envs.multiagent.base import MultiEnv +from flow.core import rewards +from gym.spaces.box import Box +import numpy as np + + +ADDITIONAL_ENV_PARAMS = { + # maximum acceleration for autonomous vehicles, in m/s^2 + "max_accel": 3, + # maximum deceleration for autonomous vehicles, in m/s^2 + "max_decel": 3, + # desired velocity for all vehicles in the network, in m/s + "target_velocity": 25, +} + + +class MultiAgentMergePOEnv(MultiEnv): + """Partially observable multi-agent merge environment. + + This environment is used to train autonomous vehicles to attenuate the + formation and propagation of waves in an open merge network. + + Required from env_params: + + * max_accel: maximum acceleration for autonomous vehicles, in m/s^2 + * max_decel: maximum deceleration for autonomous vehicles, in m/s^2 + * target_velocity: desired velocity for all vehicles in the network, in m/s + + States + The observation consists of the speeds and bumper-to-bumper headways of + the vehicles immediately preceding and following autonomous vehicle, as + well as the ego speed of the autonomous vehicles. + + In order to maintain a fixed observation size, when the number of AVs + in the network is less than "num_rl", the extra entries are filled in + with zeros. Conversely, if the number of autonomous vehicles is greater + than "num_rl", the observations from the additional vehicles are not + included in the state space. + + Actions + The action space consists of a vector of bounded accelerations for each + autonomous vehicle $i$. In order to ensure safety, these actions are + bounded by failsafes provided by the simulator at every time step. + + In order to account for variability in the number of autonomous + vehicles, if n_AV < "num_rl" the additional actions provided by the + agent are not assigned to any vehicle. Moreover, if n_AV > "num_rl", + the additional vehicles are not provided with actions from the learning + agent, and instead act as human-driven vehicles as well. + + Rewards + The reward function encourages proximity of the system-level velocity + to a desired velocity, while slightly penalizing small time headways + among autonomous vehicles. + + Termination + A rollout is terminated if the time horizon is reached or if two + vehicles collide into one another. + """ + + def __init__(self, env_params, sim_params, network, simulator='traci'): + for p in ADDITIONAL_ENV_PARAMS.keys(): + if p not in env_params.additional_params: + raise KeyError( + 'Environment parameter "{}" not supplied'.format(p)) + + # used for visualization: the vehicles behind and after RL vehicles + # (ie the observed vehicles) will have a different color + self.leader = [] + self.follower = [] + + super().__init__(env_params, sim_params, network, simulator) + + @property + def action_space(self): + """See class definition.""" + return Box( + low=-abs(self.env_params.additional_params["max_decel"]), + high=self.env_params.additional_params["max_accel"], + shape=(1,), + dtype=np.float32) + + @property + def observation_space(self): + """See class definition.""" + return Box(low=-5, high=5, shape=(5,), dtype=np.float32) + + def _apply_rl_actions(self, rl_actions): + """See class definition.""" + for rl_id in enumerate(self.k.vehicle.get_rl_ids()): + if rl_id not in rl_actions.keys(): + # the vehicle just entered, so ignore + continue + self.k.vehicle.apply_acceleration(rl_id, rl_actions[rl_id]) + + def get_state(self, rl_id=None, **kwargs): + """See class definition.""" + observation = {} + self.leader = [] + self.follower = [] + + # normalizing constants + max_speed = self.k.network.max_speed() + max_length = self.k.network.length() + + for rl_id in self.k.vehicle.get_rl_ids(): + this_speed = self.k.vehicle.get_speed(rl_id) + lead_id = self.k.vehicle.get_leader(rl_id) + follower = self.k.vehicle.get_follower(rl_id) + + if lead_id in ["", None]: + # in case leader is not visible + lead_speed = max_speed + lead_head = max_length + else: + self.leader.append(lead_id) + lead_speed = self.k.vehicle.get_speed(lead_id) + lead_head = self.k.vehicle.get_x_by_id(lead_id) \ + - self.k.vehicle.get_x_by_id(rl_id) \ + - self.k.vehicle.get_length(rl_id) + + if follower in ["", None]: + # in case follower is not visible + follow_speed = 0 + follow_head = max_length + else: + self.follower.append(follower) + follow_speed = self.k.vehicle.get_speed(follower) + follow_head = self.k.vehicle.get_headway(follower) + + observation[rl_id] = np.array([ + this_speed / max_speed, + (lead_speed - this_speed) / max_speed, + lead_head / max_length, + (this_speed - follow_speed) / max_speed, + follow_head / max_length + ]) + + return observation + + def compute_reward(self, rl_actions, **kwargs): + """See class definition.""" + if self.env_params.evaluate: + return np.mean(self.k.vehicle.get_speed(self.k.vehicle.get_ids())) + else: + # return a reward of 0 if a collision occurred + if kwargs["fail"]: + return 0 + + # reward high system-level velocities + cost1 = rewards.desired_velocity(self, fail=kwargs["fail"]) + + # penalize small time headways + cost2 = 0 + t_min = 1 # smallest acceptable time headway + for rl_id in self.k.vehicle.get_rl_ids(): + lead_id = self.k.vehicle.get_leader(rl_id) + if lead_id not in ["", None] \ + and self.k.vehicle.get_speed(rl_id) > 0: + t_headway = max( + self.k.vehicle.get_headway(rl_id) / + self.k.vehicle.get_speed(rl_id), 0) + cost2 += min((t_headway - t_min) / t_min, 0) + + # weights for cost1 and cost2, respectively + eta1, eta2 = 1.00, 0.10 + + reward = max(eta1 * cost1 + eta2 * cost2, 0) + return {key: reward for key in self.k.vehicle.get_rl_ids()} + + def additional_command(self): + """See parent class. + + This method defines which vehicles are observed for visualization + purposes. + """ + for veh_id in self.leader + self.follower: + self.k.vehicle.set_observed(veh_id) + + def reset(self, new_inflow_rate=None): + """See parent class. + + In addition, a few variables that are specific to this class are + emptied before they are used by the new rollout. + """ + self.leader = [] + self.follower = [] + return super().reset() diff --git a/flow/envs/multiagent/ring/accel.py b/flow/envs/multiagent/ring/accel.py index 9db207b1b..cffd0948e 100644 --- a/flow/envs/multiagent/ring/accel.py +++ b/flow/envs/multiagent/ring/accel.py @@ -1,16 +1,49 @@ """Environment for training the acceleration behavior of vehicles in a ring.""" - import numpy as np +from gym.spaces import Box + from flow.core import rewards from flow.envs.ring.accel import AccelEnv from flow.envs.multiagent.base import MultiEnv -class MultiAgentAccelEnv(AccelEnv, MultiEnv): - """Adversarial multi-agent env. +ADDITIONAL_ENV_PARAMS = { + # maximum acceleration for autonomous vehicles, in m/s^2 + "max_accel": 1, + # maximum deceleration for autonomous vehicles, in m/s^2 + "max_decel": 1, + # desired velocity for all vehicles in the network, in m/s + "target_velocity": 20, +} + + +class AdversarialAccelEnv(AccelEnv, MultiEnv): + """Adversarial multi-agent acceleration env. + + States + The observation of both the AV and adversary agent consist of the + velocities and absolute position of all vehicles in the network. This + assumes a constant number of vehicles. + + Actions + * AV: The action space of the AV agent consists of a vector of bounded + accelerations for each autonomous vehicle. In order to ensure safety, + these actions are further bounded by failsafes provided by the + simulator at every time step. + * Adversary: The action space of the adversary agent consists of a + vector of perturbations to the accelerations issued by the AV agent. + These are directly added to the original accelerations by the AV + agent. - Multi-agent env with an adversarial agent perturbing - the accelerations of the autonomous vehicle + Rewards + * AV: The reward for the AV agent is equal to the mean speed of all + vehicles in the network. + * Adversary: The adversary receives a reward equal to the negative + reward issued to the AV agent. + + Termination + A rollout is terminated if the time horizon is reached or if two + vehicles collide into one another. """ def _apply_rl_actions(self, rl_actions): @@ -50,3 +83,147 @@ def get_state(self, **kwargs): ] for veh_id in self.sorted_ids]) state = np.ndarray.flatten(state) return {'av': state, 'adversary': state} + + +class MultiAgentAccelPOEnv(MultiEnv): + """Multi-agent acceleration environment for shared policies. + + This environment can used to train autonomous vehicles to achieve certain + desired speeds in a decentralized fashion. This should be applicable to + both closed and open network settings. + + Required from env_params: + + * max_accel: maximum acceleration for autonomous vehicles, in m/s^2 + * max_decel: maximum deceleration for autonomous vehicles, in m/s^2 + * target_velocity: desired velocity for all vehicles in the network, in m/s + + States + The observation of each agent (i.e. each autonomous vehicle) consists + of the speeds and bumper-to-bumper headways of the vehicles immediately + preceding and following autonomous vehicle, as well as the absolute + position and ego speed of the autonomous vehicles. This results in a + state space of size 6 for each agent. + + Actions + The action space for each agent consists of a scalar bounded + acceleration for each autonomous vehicle. In order to ensure safety, + these actions are further bounded by failsafes provided by the + simulator at every time step. + + Rewards + The reward function is the two-norm of the distance of the speed of the + vehicles in the network from the "target_velocity" term. For a + description of the reward, see: flow.core.rewards.desired_speed. This + reward is shared by all agents. + + Termination + A rollout is terminated if the time horizon is reached or if two + vehicles collide into one another. + """ + + def __init__(self, env_params, sim_params, network, simulator='traci'): + for p in ADDITIONAL_ENV_PARAMS.keys(): + if p not in env_params.additional_params: + raise KeyError( + 'Environment parameter "{}" not supplied'.format(p)) + + self.leader = [] + self.follower = [] + + super().__init__(env_params, sim_params, network, simulator) + + @property + def action_space(self): + """See class definition.""" + return Box( + low=-abs(self.env_params.additional_params["max_decel"]), + high=self.env_params.additional_params["max_accel"], + shape=(1, ), + dtype=np.float32) + + @property + def observation_space(self): + """See class definition.""" + return Box(low=-5, high=5, shape=(6,), dtype=np.float32) + + def _apply_rl_actions(self, rl_actions): + """See class definition.""" + for veh_id in self.k.vehicle.get_rl_ids(): + self.k.vehicle.apply_acceleration(veh_id, rl_actions[veh_id]) + + def compute_reward(self, rl_actions, **kwargs): + """See class definition.""" + # Compute the common reward. + reward = rewards.desired_velocity(self, fail=kwargs['fail']) + + # Reward is shared by all agents. + return {key: reward for key in self.k.vehicle.get_rl_ids()} + + def get_state(self, **kwargs): # FIXME + """See class definition.""" + self.leader = [] + self.follower = [] + obs = {} + + # normalizing constants + max_speed = self.k.network.max_speed() + max_length = self.k.network.length() + + for rl_id in self.k.vehicle.get_rl_ids(): + this_pos = self.k.vehicle.get_x_by_id(rl_id) + this_speed = self.k.vehicle.get_speed(rl_id) + lead_id = self.k.vehicle.get_leader(rl_id) + follower = self.k.vehicle.get_follower(rl_id) + + if lead_id in ["", None]: + # in case leader is not visible + lead_speed = max_speed + lead_head = max_length + else: + self.leader.append(lead_id) + lead_speed = self.k.vehicle.get_speed(lead_id) + lead_head = self.k.vehicle.get_x_by_id(lead_id) \ + - self.k.vehicle.get_x_by_id(rl_id) \ + - self.k.vehicle.get_length(rl_id) + + if follower in ["", None]: + # in case follower is not visible + follow_speed = 0 + follow_head = max_length + else: + self.follower.append(follower) + follow_speed = self.k.vehicle.get_speed(follower) + follow_head = self.k.vehicle.get_headway(follower) + + # Add the next observation. + obs[rl_id] = np.array([ + this_pos / max_length, + this_speed / max_speed, + (lead_speed - this_speed) / max_speed, + lead_head / max_length, + (this_speed - follow_speed) / max_speed, + follow_head / max_length + ]) + + return obs + + def additional_command(self): + """See parent class. + + This method defines which vehicles are observed for visualization + purposes. + """ + # specify observed vehicles + for veh_id in self.leader + self.follower: + self.k.vehicle.set_observed(veh_id) + + def reset(self): + """See parent class. + + In addition, a few variables that are specific to this class are + emptied before they are used by the new rollout. + """ + self.leader = [] + self.follower = [] + return super().reset() diff --git a/flow/envs/multiagent/ring/wave_attenuation.py b/flow/envs/multiagent/ring/wave_attenuation.py index f58f2c37a..811616c0f 100644 --- a/flow/envs/multiagent/ring/wave_attenuation.py +++ b/flow/envs/multiagent/ring/wave_attenuation.py @@ -10,7 +10,15 @@ import numpy as np from gym.spaces.box import Box +import random +from scipy.optimize import fsolve +from copy import deepcopy + +from flow.core.params import InitialConfig +from flow.core.params import NetParams from flow.envs.multiagent.base import MultiEnv +from flow.envs.ring.wave_attenuation import v_eq_max_function + ADDITIONAL_ENV_PARAMS = { # maximum acceleration of autonomous vehicles @@ -35,6 +43,7 @@ class MultiWaveAttenuationPOEnv(MultiEnv): States See parent class + Actions See parent class @@ -43,7 +52,6 @@ class MultiWaveAttenuationPOEnv(MultiEnv): Termination See parent class - """ @property @@ -125,7 +133,180 @@ def additional_command(self): lead_id = self.k.vehicle.get_leader(rl_id) or rl_id self.k.vehicle.set_observed(lead_id) - def gen_edges(self, i): + @staticmethod + def gen_edges(i): """Return the edges corresponding to the rl id.""" return ['top_{}'.format(i), 'left_{}'.format(i), 'right_{}'.format(i), 'bottom_{}'.format(i)] + + +class MultiAgentWaveAttenuationPOEnv(MultiEnv): + """Multi-agent variant of WaveAttenuationPOEnv. + + Required from env_params: + + * max_accel: maximum acceleration of autonomous vehicles + * max_decel: maximum deceleration of autonomous vehicles + * ring_length: bounds on the ranges of ring road lengths the autonomous + vehicle is trained on. If set to None, the environment sticks to the ring + road specified in the original network definition. + + States + The state of each agent (AV) consists of the speed and headway of the + ego vehicle, as well as the difference in speed between the ego vehicle + and its leader. There is no assumption on the number of vehicles in the + network. + + Actions + Actions are an acceleration for each rl vehicle, bounded by the maximum + accelerations and decelerations specified in EnvParams. + + Rewards + The reward function rewards high average speeds from all vehicles in + the network, and penalizes accelerations by the rl vehicle. This reward + is shared by all agents. + + Termination + A rollout is terminated if the time horizon is reached or if two + vehicles collide into one another. + """ + + def __init__(self, env_params, sim_params, network, simulator='traci'): + for p in ADDITIONAL_ENV_PARAMS.keys(): + if p not in env_params.additional_params: + raise KeyError( + 'Environment parameter \'{}\' not supplied'.format(p)) + + super().__init__(env_params, sim_params, network, simulator) + + @property + def observation_space(self): + """See class definition.""" + return Box(low=-5, high=5, shape=(3,), dtype=np.float32) + + @property + def action_space(self): + """See class definition.""" + return Box( + low=-np.abs(self.env_params.additional_params['max_decel']), + high=self.env_params.additional_params['max_accel'], + shape=(1,), + dtype=np.float32) + + def get_state(self): + """See class definition.""" + obs = {} + for rl_id in self.k.vehicle.get_rl_ids(): + lead_id = self.k.vehicle.get_leader(rl_id) or rl_id + + # normalizers + max_speed = 15. + max_length = self.env_params.additional_params['ring_length'][1] + + observation = np.array([ + self.k.vehicle.get_speed(rl_id) / max_speed, + (self.k.vehicle.get_speed(lead_id) - + self.k.vehicle.get_speed(rl_id)) + / max_speed, + self.k.vehicle.get_headway(rl_id) / max_length + ]) + obs.update({rl_id: observation}) + + return obs + + def _apply_rl_actions(self, rl_actions): + """Split the accelerations by ring.""" + if rl_actions: + rl_ids = list(rl_actions.keys()) + accel = list(rl_actions.values()) + self.k.vehicle.apply_acceleration(rl_ids, accel) + + def compute_reward(self, rl_actions, **kwargs): + """See class definition.""" + # in the warmup steps + if rl_actions is None: + return 0 + + vel = np.array([ + self.k.vehicle.get_speed(veh_id) + for veh_id in self.k.vehicle.get_ids() + ]) + + if any(vel < -100) or kwargs['fail']: + return 0. + + # reward average velocity + eta_2 = 4. + reward = eta_2 * np.mean(vel) / 20 + + # punish accelerations (should lead to reduced stop-and-go waves) + eta = 4 # 0.25 + mean_actions = np.mean(np.abs(list(rl_actions.values()))) + accel_threshold = 0 + + if mean_actions > accel_threshold: + reward += eta * (accel_threshold - mean_actions) + + return {key: reward for key in self.k.vehicle.get_rl_ids()} + + def additional_command(self): + """Define which vehicles are observed for visualization purposes.""" + # specify observed vehicles + for rl_id in self.k.vehicle.get_rl_ids(): + lead_id = self.k.vehicle.get_leader(rl_id) or rl_id + self.k.vehicle.set_observed(lead_id) + + def reset(self, new_inflow_rate=None): + """See parent class. + + The sumo instance is reset with a new ring length, and a number of + steps are performed with the rl vehicle acting as a human vehicle. + """ + # skip if ring length is None + if self.env_params.additional_params['ring_length'] is None: + return super().reset() + + # reset the step counter + self.step_counter = 0 + + # update the network + initial_config = InitialConfig(bunching=50, min_gap=0) + length = random.randint( + self.env_params.additional_params['ring_length'][0], + self.env_params.additional_params['ring_length'][1]) + additional_net_params = { + 'length': + length, + 'lanes': + self.net_params.additional_params['lanes'], + 'speed_limit': + self.net_params.additional_params['speed_limit'], + 'resolution': + self.net_params.additional_params['resolution'] + } + net_params = NetParams(additional_params=additional_net_params) + + self.network = self.network.__class__( + self.network.orig_name, self.network.vehicles, + net_params, initial_config) + self.k.vehicle = deepcopy(self.initial_vehicles) + self.k.vehicle.kernel_api = self.k.kernel_api + self.k.vehicle.master_kernel = self.k + + # solve for the velocity upper bound of the ring + v_guess = 4 + v_eq_max = fsolve(v_eq_max_function, np.array(v_guess), + args=(len(self.initial_ids), length))[0] + + print('\n-----------------------') + print('ring length:', net_params.additional_params['length']) + print('v_max:', v_eq_max) + print('-----------------------') + + # restart the sumo instance + self.restart_simulation( + sim_params=self.sim_params, + render=self.sim_params.render) + + # perform the generic reset function + return super().reset() diff --git a/flow/envs/ring/wave_attenuation.py b/flow/envs/ring/wave_attenuation.py index 3aeb037b1..ec10db16d 100644 --- a/flow/envs/ring/wave_attenuation.py +++ b/flow/envs/ring/wave_attenuation.py @@ -207,12 +207,7 @@ def reset(self): render=self.sim_params.render) # perform the generic reset function - observation = super().reset() - - # reset the timer to zero - self.time_counter = 0 - - return observation + return super().reset() class WaveAttenuationPOEnv(WaveAttenuationEnv): diff --git a/flow/multiagent_envs/__init__.py b/flow/multiagent_envs/__init__.py index e270fa397..75ffa34dc 100644 --- a/flow/multiagent_envs/__init__.py +++ b/flow/multiagent_envs/__init__.py @@ -3,13 +3,13 @@ from flow.multiagent_envs.multiagent_env import MultiEnv from flow.multiagent_envs.loop.wave_attenuation import \ MultiWaveAttenuationPOEnv -from flow.multiagent_envs.loop.loop_accel import MultiAgentAccelEnv +from flow.multiagent_envs.loop.loop_accel import AdversarialAccelEnv from flow.multiagent_envs.traffic_light_grid import MultiTrafficLightGridPOEnv from flow.multiagent_envs.highway import MultiAgentHighwayPOEnv __all__ = [ 'MultiEnv', - 'MultiAgentAccelEnv', + 'AdversarialAccelEnv', 'MultiWaveAttenuationPOEnv', 'MultiTrafficLightGridPOEnv', 'MultiAgentHighwayPOEnv' diff --git a/flow/multiagent_envs/loop/loop_accel.py b/flow/multiagent_envs/loop/loop_accel.py index 918e8542b..2fe79f421 100644 --- a/flow/multiagent_envs/loop/loop_accel.py +++ b/flow/multiagent_envs/loop/loop_accel.py @@ -3,12 +3,12 @@ To view the actual content, go to: flow/envs/multiagent/ring/accel.py """ from flow.utils.flow_warnings import deprecated -from flow.envs.multiagent.ring.accel import MultiAgentAccelEnv as MAAEnv +from flow.envs.multiagent.ring.accel import AdversarialAccelEnv as MAAEnv @deprecated('flow.multiagent_envs.loop.loop_accel', - 'flow.envs.multiagent.ring.accel.MultiAgentAccelEnv') -class MultiAgentAccelEnv(MAAEnv): + 'flow.envs.multiagent.ring.accel.AdversarialAccelEnv') +class AdversarialAccelEnv(MAAEnv): """See parent class.""" pass diff --git a/tests/fast_tests/test_environments.py b/tests/fast_tests/test_environments.py index 560729c6e..48628c4ec 100644 --- a/tests/fast_tests/test_environments.py +++ b/tests/fast_tests/test_environments.py @@ -18,6 +18,9 @@ TestEnv, BottleneckDesiredVelocityEnv, BottleneckEnv, BottleneckAccelEnv from flow.envs.ring.wave_attenuation import v_eq_max_function from flow.envs.multiagent import MultiAgentHighwayPOEnv +from flow.envs.multiagent import MultiAgentAccelPOEnv +from flow.envs.multiagent import MultiAgentWaveAttenuationPOEnv +from flow.envs.multiagent import MultiAgentMergePOEnv os.environ["TEST_FLAG"] = "True" @@ -944,6 +947,278 @@ def test_reset_inflows(self): env.k.vehicle.get_inflow_rate(250)/expected_inflow, 1, 1) +class TestMultiAgentAccelPOEnv(unittest.TestCase): + """Tests the MultiAgentAccelPOEnv environment in + flow/envs/multiagent/ring/accel.py""" + + def setUp(self): + vehicles = VehicleParams() + vehicles.add("rl", acceleration_controller=(RLController, {}), + num_vehicles=1) + vehicles.add("human", acceleration_controller=(IDMController, {}), + num_vehicles=1) + + self.sim_params = SumoParams() + self.network = RingNetwork( + name="test_ring", + vehicles=vehicles, + net_params=NetParams(additional_params=RING_PARAMS.copy()), + ) + self.env_params = EnvParams( + additional_params={ + 'max_accel': 1, + 'max_decel': 1, + "target_velocity": 25 + } + ) + + def tearDown(self): + self.sim_params = None + self.network = None + self.env_params = None + + def test_additional_env_params(self): + """Ensures that not returning the correct params leads to an error.""" + self.assertTrue( + test_additional_params( + env_class=MultiAgentAccelPOEnv, + sim_params=self.sim_params, + network=self.network, + additional_params={ + "max_accel": 1, + "max_decel": 1, + "target_velocity": 10, + } + ) + ) + + def test_observation_action_space(self): + """Tests the observation and action spaces upon initialization.""" + # create the environment + env = MultiAgentAccelPOEnv( + sim_params=self.sim_params, + network=self.network, + env_params=self.env_params + ) + + # check the observation space + self.assertTrue(test_space( + env.observation_space, + expected_size=6, + expected_min=-5, + expected_max=5 + )) + + # check the action space + self.assertTrue(test_space( + env.action_space, + expected_size=1, expected_min=-1, expected_max=1)) + + env.terminate() + + def test_observed(self): + """Ensures that the observed ids are returning the correct vehicles.""" + self.assertTrue( + test_observed( + env_class=MultiAgentMergePOEnv, + sim_params=self.sim_params, + network=self.network, + env_params=self.env_params, + expected_observed=["human_0"] + ) + ) + + +class TestMultiAgentWaveAttenuationPOEnv(unittest.TestCase): + """Tests the MultiAgentWaveAttenuationPOEnv environment in + flow/envs/multiagent/ring/wave_attenuation.py""" + + def setUp(self): + vehicles = VehicleParams() + vehicles.add("rl", acceleration_controller=(RLController, {}), + num_vehicles=1) + vehicles.add("human", acceleration_controller=(IDMController, {}), + num_vehicles=1) + + self.sim_params = SumoParams() + self.network = RingNetwork( + name="test_ring", + vehicles=vehicles, + net_params=NetParams(additional_params=RING_PARAMS.copy()), + ) + self.env_params = EnvParams( + additional_params={ + 'max_accel': 1, + 'max_decel': 1, + "ring_length": [220, 270] + } + ) + + def tearDown(self): + self.sim_params = None + self.network = None + self.env_params = None + + def test_additional_env_params(self): + """Ensures that not returning the correct params leads to an error.""" + self.assertTrue( + test_additional_params( + env_class=MultiAgentWaveAttenuationPOEnv, + sim_params=self.sim_params, + network=self.network, + additional_params={ + "max_accel": 1, + "max_decel": 1, + "ring_length": [220, 270], + } + ) + ) + + def test_observation_action_space(self): + """Tests the observation and action spaces upon initialization.""" + # create the environment + env = MultiAgentWaveAttenuationPOEnv( + sim_params=self.sim_params, + network=self.network, + env_params=self.env_params + ) + + # check the observation space + self.assertTrue(test_space( + env.observation_space, + expected_size=3, + expected_min=-5, + expected_max=5 + )) + + # check the action space + self.assertTrue(test_space( + env.action_space, + expected_size=1, + expected_min=-1, + expected_max=1 + )) + + env.terminate() + + def test_observed(self): + """Ensures that the observed ids are returning the correct vehicles.""" + self.assertTrue( + test_observed( + env_class=MultiAgentWaveAttenuationPOEnv, + sim_params=self.sim_params, + network=self.network, + env_params=self.env_params, + expected_observed=["human_0"] + ) + ) + + def test_reset(self): + """ + Tests that the reset method creating new ring lengths within the + requested range. + """ + # set a random seed to ensure the network lengths are always the same + # during testing + random.seed(9001) + + # create the environment + env = MultiAgentWaveAttenuationPOEnv( + sim_params=self.sim_params, + network=self.network, + env_params=self.env_params + ) + + # reset the network several times and check its length + self.assertEqual(env.k.network.non_internal_length(), 230) + env.reset() + self.assertEqual(env.k.network.non_internal_length(), 239) + env.reset() + self.assertEqual(env.k.network.non_internal_length(), 256) + + +class TestMultiAgentMergePOEnv(unittest.TestCase): + """Tests the MultiAgentMergePOEnv environment in + flow/envs/multiagent/merge.py""" + + def setUp(self): + vehicles = VehicleParams() + vehicles.add("rl", acceleration_controller=(RLController, {}), + num_vehicles=1) + vehicles.add("human", acceleration_controller=(IDMController, {}), + num_vehicles=1) + + self.sim_params = SumoParams() + self.network = MergeNetwork( + name="test_merge", + vehicles=vehicles, + net_params=NetParams(additional_params=MERGE_PARAMS.copy()), + ) + self.env_params = EnvParams( + additional_params={ + 'max_accel': 1, + 'max_decel': 1, + "target_velocity": 25 + } + ) + + def tearDown(self): + self.sim_params = None + self.network = None + self.env_params = None + + def test_additional_env_params(self): + """Ensures that not returning the correct params leads to an error.""" + self.assertTrue( + test_additional_params( + env_class=MultiAgentMergePOEnv, + sim_params=self.sim_params, + network=self.network, + additional_params={ + "max_accel": 1, + "max_decel": 1, + "target_velocity": 10, + } + ) + ) + + def test_observation_action_space(self): + """Tests the observation and action spaces upon initialization.""" + # create the environment + env = MultiAgentMergePOEnv( + sim_params=self.sim_params, + network=self.network, + env_params=self.env_params + ) + + # check the observation space + self.assertTrue(test_space( + env.observation_space, + expected_size=5, + expected_min=-5, + expected_max=5 + )) + + # check the action space + self.assertTrue(test_space( + env.action_space, + expected_size=1, expected_min=-1, expected_max=1)) + + env.terminate() + + def test_observed(self): + """Ensures that the observed ids are returning the correct vehicles.""" + self.assertTrue( + test_observed( + env_class=MultiAgentMergePOEnv, + sim_params=self.sim_params, + network=self.network, + env_params=self.env_params, + expected_observed=["human_0"] + ) + ) + + class TestMultiAgentHighwayPOEnv(unittest.TestCase): def setUp(self): @@ -961,7 +1236,9 @@ def setUp(self): ) self.env_params = EnvParams( additional_params={ - 'max_accel': 1, 'max_decel': 1, "target_velocity": 25 + 'max_accel': 1, + 'max_decel': 1, + "target_velocity": 25 } ) diff --git a/tests/fast_tests/test_examples.py b/tests/fast_tests/test_examples.py index 0162a4e66..89e4ea97b 100644 --- a/tests/fast_tests/test_examples.py +++ b/tests/fast_tests/test_examples.py @@ -14,9 +14,12 @@ from examples.exp_configs.rl.singleagent.singleagent_ring import flow_params as singleagent_ring from examples.exp_configs.rl.singleagent.singleagent_bottleneck import flow_params as singleagent_bottleneck +from examples.exp_configs.rl.multiagent.adversarial_figure_eight import flow_params as adversarial_figure_eight from examples.exp_configs.rl.multiagent.multiagent_figure_eight import flow_params as multiagent_figure_eight -from examples.exp_configs.rl.multiagent.multiagent_ring import \ - flow_params as multiagent_ring +from examples.exp_configs.rl.multiagent.multiagent_merge import flow_params as multiagent_merge +from examples.exp_configs.rl.multiagent.lord_of_the_rings import \ + flow_params as lord_of_the_rings +from examples.exp_configs.rl.multiagent.multiagent_ring import flow_params as multiagent_ring from examples.exp_configs.rl.multiagent.multiagent_traffic_light_grid import \ flow_params as multiagent_traffic_light_grid from examples.exp_configs.rl.multiagent.multiagent_highway import flow_params as multiagent_highway @@ -168,7 +171,17 @@ def test_singleagent_ring(self): def test_singleagent_bottleneck(self): self.run_exp(singleagent_bottleneck) - def test_multi_figure_eight(self): + def test_adversarial_figure_eight(self): + from examples.exp_configs.rl.multiagent.adversarial_figure_eight import POLICY_GRAPHS as af8pg + from examples.exp_configs.rl.multiagent.adversarial_figure_eight import policy_mapping_fn as af8pmf + + kwargs = { + "policy_graphs": af8pg, + "policy_mapping_fn": af8pmf + } + self.run_exp(adversarial_figure_eight, **kwargs) + + def test_multiagent_figure_eight(self): from examples.exp_configs.rl.multiagent.multiagent_figure_eight import POLICY_GRAPHS as mf8pg from examples.exp_configs.rl.multiagent.multiagent_figure_eight import policy_mapping_fn as mf8pmf @@ -178,18 +191,38 @@ def test_multi_figure_eight(self): } self.run_exp(multiagent_figure_eight, **kwargs) - def test_multi_ring(self): + def test_lord_of_the_rings(self): + from examples.exp_configs.rl.multiagent.lord_of_the_rings import POLICY_GRAPHS as ltrpg + from examples.exp_configs.rl.multiagent.lord_of_the_rings import POLICIES_TO_TRAIN as ltrpt + from examples.exp_configs.rl.multiagent.lord_of_the_rings import policy_mapping_fn as ltrpmf + + kwargs = { + "policy_graphs": ltrpg, + "policies_to_train": ltrpt, + "policy_mapping_fn": ltrpmf + } + self.run_exp(lord_of_the_rings, **kwargs) + + def test_multiagent_ring(self): from examples.exp_configs.rl.multiagent.multiagent_ring import POLICY_GRAPHS as mrpg - from examples.exp_configs.rl.multiagent.multiagent_ring import POLICIES_TO_TRAIN as mrpt from examples.exp_configs.rl.multiagent.multiagent_ring import policy_mapping_fn as mrpmf kwargs = { "policy_graphs": mrpg, - "policies_to_train": mrpt, "policy_mapping_fn": mrpmf } self.run_exp(multiagent_ring, **kwargs) + def test_multiagent_merge(self): + from examples.exp_configs.rl.multiagent.multiagent_merge import POLICY_GRAPHS as mmpg + from examples.exp_configs.rl.multiagent.multiagent_merge import policy_mapping_fn as mmpmf + + kwargs = { + "policy_graphs": mmpg, + "policy_mapping_fn": mmpmf + } + self.run_exp(multiagent_merge, **kwargs) + def test_multi_traffic_light_grid(self): from examples.exp_configs.rl.multiagent.multiagent_traffic_light_grid import POLICY_GRAPHS as mtlpg from examples.exp_configs.rl.multiagent.multiagent_traffic_light_grid import POLICIES_TO_TRAIN as mtlpt From bc65245504e590604ffa3a49376e3367c318a475 Mon Sep 17 00:00:00 2001 From: Eugene Vinitsky Date: Fri, 21 Feb 2020 18:53:04 -0800 Subject: [PATCH 41/86] Fix the extra ray windows that pop up (#836) --- flow/envs/base.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/flow/envs/base.py b/flow/envs/base.py index 7749ffea5..f20d3bb57 100644 --- a/flow/envs/base.py +++ b/flow/envs/base.py @@ -129,7 +129,10 @@ def __init__(self, self.network = scenario if scenario is not None else network self.net_params = self.network.net_params self.initial_config = self.network.initial_config - self.sim_params = sim_params + self.sim_params = deepcopy(sim_params) + # check whether we should be rendering + self.should_render = self.sim_params.render + self.sim_params.render = False time_stamp = ''.join(str(time.time()).split('.')) if os.environ.get("TEST_FLAG", 0): # 1.0 works with stress_test_start 10k times @@ -153,7 +156,7 @@ def __init__(self, # create the Flow kernel self.k = Kernel(simulator=self.simulator, - sim_params=sim_params) + sim_params=self.sim_params) # use the network class's network parameters to generate the necessary # network components within the network kernel @@ -166,7 +169,7 @@ def __init__(self, # the network kernel as an input in order to determine what network # needs to be simulated. kernel_api = self.k.simulation.start_simulation( - network=self.k.network, sim_params=sim_params) + network=self.k.network, sim_params=self.sim_params) # pass the kernel api to the kernel and it's subclasses self.k.pass_api(kernel_api) @@ -426,6 +429,13 @@ def reset(self): # reset the time counter self.time_counter = 0 + # Now that we've passed the possibly fake init steps some rl libraries + # do, we can feel free to actually render things + if self.should_render: + self.sim_params.render = True + # got to restart the simulation to make it actually display anything + self.restart_simulation(self.sim_params) + # warn about not using restart_instance when using inflows if len(self.net_params.inflows.get()) > 0 and \ not self.sim_params.restart_instance: From 4cd30ee686a5dffcd05b3989bd76db1d75c11f83 Mon Sep 17 00:00:00 2001 From: Aboudy Kreidieh Date: Thu, 27 Feb 2020 13:34:26 -0800 Subject: [PATCH 42/86] fixed sims_per_step done bug (#838) this was causing experiments to end prematurely if sims_per_step > 1 --- flow/envs/base.py | 5 +++-- flow/envs/multiagent/base.py | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/flow/envs/base.py b/flow/envs/base.py index f20d3bb57..5777e9868 100644 --- a/flow/envs/base.py +++ b/flow/envs/base.py @@ -395,8 +395,9 @@ def step(self, rl_actions): # test if the environment should terminate due to a collision or the # time horizon being met - done = (self.time_counter >= self.env_params.warmup_steps + - self.env_params.horizon) # or crash + done = (self.time_counter >= self.env_params.sims_per_step * + (self.env_params.warmup_steps + self.env_params.horizon) + or crash) # compute the info for each agent infos = {} diff --git a/flow/envs/multiagent/base.py b/flow/envs/multiagent/base.py index 72b927505..c4242ba75 100644 --- a/flow/envs/multiagent/base.py +++ b/flow/envs/multiagent/base.py @@ -108,8 +108,8 @@ def step(self, rl_actions): states = self.get_state() done = {key: key in self.k.vehicle.get_arrived_ids() for key in states.keys()} - if crash or (self.time_counter >= self.env_params.warmup_steps + - self.env_params.horizon): + if crash or (self.time_counter >= self.env_params.sims_per_step * + (self.env_params.warmup_steps + self.env_params.horizon)): done['__all__'] = True else: done['__all__'] = False From 8dba645b7de4205b5558c6277b3d6507c1d0fbbb Mon Sep 17 00:00:00 2001 From: Aboudy Kreidieh Date: Thu, 27 Feb 2020 13:34:43 -0800 Subject: [PATCH 43/86] removed dt parameter from IDMController (#837) --- flow/controllers/car_following_models.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/flow/controllers/car_following_models.py b/flow/controllers/car_following_models.py index cc1b4f508..f86c546e8 100755 --- a/flow/controllers/car_following_models.py +++ b/flow/controllers/car_following_models.py @@ -427,8 +427,6 @@ class IDMController(BaseController): acceleration exponent (default: 4) s0 : float linear jam distance, in m (default: 2) - dt : float - timestep, in s (default: 0.1) noise : float std dev of normal perturbation to the acceleration (default: 0) fail_safe : str @@ -445,7 +443,6 @@ def __init__(self, delta=4, s0=2, time_delay=0.0, - dt=0.1, noise=0, fail_safe=None, car_following_params=None): @@ -463,7 +460,6 @@ def __init__(self, self.b = b self.delta = delta self.s0 = s0 - self.dt = dt def get_accel(self, env): """See parent class.""" From 32927677e6db72c68393aa3433f9e813485619ac Mon Sep 17 00:00:00 2001 From: zpymyyn Date: Sun, 1 Mar 2020 23:33:20 +0200 Subject: [PATCH 44/86] usage argument correction (#843) * usage argument correction * usage argument correction 2 * usage argument correction 3 --- flow/benchmarks/rllib/ars_runner.py | 2 +- flow/benchmarks/rllib/es_runner.py | 2 +- flow/benchmarks/rllib/ppo_runner.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/flow/benchmarks/rllib/ars_runner.py b/flow/benchmarks/rllib/ars_runner.py index f765611dd..c5bcf8566 100644 --- a/flow/benchmarks/rllib/ars_runner.py +++ b/flow/benchmarks/rllib/ars_runner.py @@ -22,7 +22,7 @@ EXAMPLE_USAGE = """ example usage: - python ars_runner.py grid0 + python ars_runner.py --benchmark_name=grid0 Here the arguments are: benchmark_name - name of the benchmark to run num_rollouts - number of rollouts to train across diff --git a/flow/benchmarks/rllib/es_runner.py b/flow/benchmarks/rllib/es_runner.py index e71c01bef..2dd566679 100644 --- a/flow/benchmarks/rllib/es_runner.py +++ b/flow/benchmarks/rllib/es_runner.py @@ -19,7 +19,7 @@ EXAMPLE_USAGE = """ example usage: - python es_runner.py grid0 + python es_runner.py --benchmark_name=grid0 Here the arguments are: benchmark_name - name of the benchmark to run num_rollouts - number of rollouts to train across diff --git a/flow/benchmarks/rllib/ppo_runner.py b/flow/benchmarks/rllib/ppo_runner.py index 401955470..f53d86c8c 100644 --- a/flow/benchmarks/rllib/ppo_runner.py +++ b/flow/benchmarks/rllib/ppo_runner.py @@ -21,7 +21,7 @@ EXAMPLE_USAGE = """ example usage: - python ppo_runner.py grid0 + python ppo_runner.py --benchmark_name=grid0 Here the arguments are: benchmark_name - name of the benchmark to run num_rollouts - number of rollouts to train across From d4886d957e2a1b6bd251b64a19d1b286aad2cbcc Mon Sep 17 00:00:00 2001 From: Aboudy Kreidieh Date: Wed, 4 Mar 2020 14:37:49 -0800 Subject: [PATCH 45/86] Add a nonlocal example of follower stopper as requested (#846) * Add a nonlocal example of follower stopper as requested * Make v_des nonlocal --- flow/controllers/velocity_controllers.py | 48 ++++++++++++++++++++++++ 1 file changed, 48 insertions(+) diff --git a/flow/controllers/velocity_controllers.py b/flow/controllers/velocity_controllers.py index 8ce2710cc..2e4b7c22a 100644 --- a/flow/controllers/velocity_controllers.py +++ b/flow/controllers/velocity_controllers.py @@ -116,6 +116,54 @@ def get_accel(self, env): return (v_cmd - this_vel) / env.sim_step +class NonLocalFollowerStopper(FollowerStopper): + """Follower stopper that uses the average system speed to compute its acceleration.""" + + def get_accel(self, env): + """See parent class.""" + lead_id = env.k.vehicle.get_leader(self.veh_id) + this_vel = env.k.vehicle.get_speed(self.veh_id) + lead_vel = env.k.vehicle.get_speed(lead_id) + self.v_des = np.mean(env.k.vehicle.get_speed(env.k.vehicle.get_ids())) + + if self.v_des is None: + return None + + if lead_id is None: + v_cmd = self.v_des + else: + dx = env.k.vehicle.get_headway(self.veh_id) + dv_minus = min(lead_vel - this_vel, 0) + + dx_1 = self.dx_1_0 + 1 / (2 * self.d_1) * dv_minus ** 2 + dx_2 = self.dx_2_0 + 1 / (2 * self.d_2) * dv_minus ** 2 + dx_3 = self.dx_3_0 + 1 / (2 * self.d_3) * dv_minus ** 2 + v = min(max(lead_vel, 0), self.v_des) + # compute the desired velocity + if dx <= dx_1: + v_cmd = 0 + elif dx <= dx_2: + v_cmd = v * (dx - dx_1) / (dx_2 - dx_1) + elif dx <= dx_3: + v_cmd = v + (self.v_des - this_vel) * (dx - dx_2) \ + / (dx_3 - dx_2) + else: + v_cmd = self.v_des + + edge = env.k.vehicle.get_edge(self.veh_id) + + if edge == "": + return None + + if self.find_intersection_dist(env) <= 10 and \ + env.k.vehicle.get_edge(self.veh_id) in self.danger_edges or \ + env.k.vehicle.get_edge(self.veh_id)[0] == ":": + return None + else: + # compute the acceleration from the desired velocity + return (v_cmd - this_vel) / env.sim_step + + class PISaturation(BaseController): """Inspired by Dan Work's... work. From 7d632c7c3d0c01912020f14cff9212289f0edd94 Mon Sep 17 00:00:00 2001 From: Aboudy Kreidieh Date: Wed, 4 Mar 2020 20:56:08 -0800 Subject: [PATCH 46/86] Add flag auto_color flag to SumoParams and disable coloring if flag is off. (#849) * color specification * keep backward compaitble try/except * auto_update -> force_color_update, false by default * not in flake8 * i'm stupid this is better * add color to type params * added comments --- flow/core/kernel/vehicle/traci.py | 21 ++++++++++++--------- flow/core/params.py | 18 +++++++++++++----- 2 files changed, 25 insertions(+), 14 deletions(-) diff --git a/flow/core/kernel/vehicle/traci.py b/flow/core/kernel/vehicle/traci.py index ac95f599f..939d9eacc 100644 --- a/flow/core/kernel/vehicle/traci.py +++ b/flow/core/kernel/vehicle/traci.py @@ -72,9 +72,9 @@ def __init__(self, # whether or not to automatically color vehicles try: - self._color_vehicles = sim_params.color_vehicles + self._force_color_update = sim_params.force_color_update except AttributeError: - self._color_vehicles = False + self._force_color_update = False def initialize(self, vehicles): """Initialize vehicle state information. @@ -984,8 +984,10 @@ def update_vehicle_colors(self): """ for veh_id in self.get_rl_ids(): try: - # color rl vehicles red - self.set_color(veh_id=veh_id, color=RED) + # If vehicle is already being colored via argument to vehicles.add(), don't re-color it. + if self._force_color_update or 'color' not in self.type_parameters[self.get_type(veh_id)]: + # color rl vehicles red + self.set_color(veh_id=veh_id, color=RED) except (FatalTraCIError, TraCIException) as e: print('Error when updating rl vehicle colors:', e) @@ -993,7 +995,9 @@ def update_vehicle_colors(self): for veh_id in self.get_human_ids(): try: color = CYAN if veh_id in self.get_observed_ids() else WHITE - self.set_color(veh_id=veh_id, color=color) + # If vehicle is already being colored via argument to vehicles.add(), don't re-color it. + if self._force_color_update or 'color' not in self.type_parameters[self.get_type(veh_id)]: + self.set_color(veh_id=veh_id, color=color) except (FatalTraCIError, TraCIException) as e: print('Error when updating human vehicle colors:', e) @@ -1014,10 +1018,9 @@ def set_color(self, veh_id, color): The last term for sumo (transparency) is set to 255. """ - if self._color_vehicles: - r, g, b = color - self.kernel_api.vehicle.setColor( - vehID=veh_id, color=(r, g, b, 255)) + r, g, b = color + self.kernel_api.vehicle.setColor( + vehID=veh_id, color=(r, g, b, 255)) def add(self, veh_id, type_id, edge, pos, lane, speed): """See parent class.""" diff --git a/flow/core/params.py b/flow/core/params.py index 07cbdb4c8..fadffdc44 100755 --- a/flow/core/params.py +++ b/flow/core/params.py @@ -241,7 +241,8 @@ def add(self, initial_speed=0, num_vehicles=0, car_following_params=None, - lane_change_params=None): + lane_change_params=None, + color=None): """Add a sequence of vehicles to the list of vehicles in the network. Parameters @@ -292,6 +293,10 @@ def add(self, "car_following_params": car_following_params, "lane_change_params": lane_change_params} + if color: + type_params['color'] = color + self.type_parameters[veh_id]['color'] = color + # TODO: delete? self.initial.append({ "veh_id": @@ -384,7 +389,7 @@ class SimParams(object): specifies whether to render the radius of RL observation pxpm : int, optional specifies rendering resolution (pixel / meter) - color_vehicles : bool, optional + force_color_update : bool, optional whether or not to automatically color vehicles according to their types """ @@ -397,7 +402,7 @@ def __init__(self, sight_radius=25, show_radius=False, pxpm=2, - color_vehicles=True): + force_color_update=False): """Instantiate SimParams.""" self.sim_step = sim_step self.render = render @@ -407,7 +412,7 @@ def __init__(self, self.sight_radius = sight_radius self.pxpm = pxpm self.show_radius = show_radius - self.color_vehicles = color_vehicles + self.force_color_update = force_color_update class AimsunParams(SimParams): @@ -539,6 +544,8 @@ class SumoParams(SimParams): specifies whether to render the radius of RL observation pxpm : int, optional specifies rendering resolution (pixel / meter) + force_color_update : bool, optional + whether or not to automatically color vehicles according to their types overtake_right : bool, optional whether vehicles are allowed to overtake on the right as well as the left @@ -569,6 +576,7 @@ def __init__(self, sight_radius=25, show_radius=False, pxpm=2, + force_color_update=False, overtake_right=False, seed=None, restart_instance=False, @@ -578,7 +586,7 @@ def __init__(self, """Instantiate SumoParams.""" super(SumoParams, self).__init__( sim_step, render, restart_instance, emission_path, save_render, - sight_radius, show_radius, pxpm) + sight_radius, show_radius, pxpm, force_color_update) self.port = port self.lateral_resolution = lateral_resolution self.no_step_log = no_step_log From e2e50775c91352d7b7fc566b4bc77e0c05f25ae8 Mon Sep 17 00:00:00 2001 From: Kanaad Parvate Date: Mon, 9 Mar 2020 17:10:47 -0700 Subject: [PATCH 47/86] rendering fix (#861) --- flow/envs/multiagent/base.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/flow/envs/multiagent/base.py b/flow/envs/multiagent/base.py index c4242ba75..72d929dd5 100644 --- a/flow/envs/multiagent/base.py +++ b/flow/envs/multiagent/base.py @@ -148,6 +148,13 @@ def reset(self, new_inflow_rate=None): # reset the time counter self.time_counter = 0 + # Now that we've passed the possibly fake init steps some rl libraries + # do, we can feel free to actually render things + if self.should_render: + self.sim_params.render = True + # got to restart the simulation to make it actually display anything + self.restart_simulation(self.sim_params) + # warn about not using restart_instance when using inflows if len(self.net_params.inflows.get()) > 0 and \ not self.sim_params.restart_instance: From 2df2afd80ba6a72deae43f11d67cba18310bdf8f Mon Sep 17 00:00:00 2001 From: Kanaad Parvate Date: Wed, 11 Mar 2020 15:42:14 -0700 Subject: [PATCH 48/86] train from checkpoint (#853) --- examples/train.py | 37 +++++++++++++++++++++---------------- 1 file changed, 21 insertions(+), 16 deletions(-) diff --git a/examples/train.py b/examples/train.py index bbd482688..7f006a18f 100644 --- a/examples/train.py +++ b/examples/train.py @@ -65,6 +65,9 @@ def parse_args(args): parser.add_argument( '--rollout_size', type=int, default=1000, help='How many steps are in a training batch.') + parser.add_argument( + '--checkpoint_path', type=str, default=None, + help='Directory with checkpoint to restore training from.') return parser.parse_known_args(args)[0] @@ -199,22 +202,24 @@ def setup_exps_rllib(flow_params, flow_params, n_cpus, n_rollouts, policy_graphs, policy_mapping_fn, policies_to_train) - ray.init(num_cpus=n_cpus + 1) - trials = run_experiments({ - flow_params["exp_tag"]: { - "run": alg_run, - "env": gym_name, - "config": { - **config - }, - "checkpoint_freq": 20, - "checkpoint_at_end": True, - "max_failures": 999, - "stop": { - "training_iteration": 200, - }, - } - }) + ray.init(num_cpus=n_cpus + 1, object_store_memory=200 * 1024 * 1024) + exp_config = { + "run": alg_run, + "env": gym_name, + "config": { + **config + }, + "checkpoint_freq": 20, + "checkpoint_at_end": True, + "max_failures": 999, + "stop": { + "training_iteration": flags.num_steps, + }, + } + + if flags.checkpoint_path is not None: + exp_config['restore'] = flags.checkpoint_path + trials = run_experiments({flow_params["exp_tag"]: exp_config}) elif flags.rl_trainer == "Stable-Baselines": flow_params = submodule.flow_params From 80f3c47f55ce167dcf27164783234894d43ddda0 Mon Sep 17 00:00:00 2001 From: Eugene Vinitsky Date: Thu, 12 Mar 2020 17:21:41 -0700 Subject: [PATCH 49/86] I210 merge (#839) Add I210 network --- .../exp_configs/non_rl/i210_subnetwork.py | 111 + .../non_rl/i210_subnetwork_sweep.py | 151 + .../rl/multiagent/multiagent_i210.py | 181 + .../exp_configs/templates/sumo/test2.net.xml | 5677 +++++++++++++++++ .../exp_scripts/compute_210_calibration.py | 33 + examples/simulate.py | 10 +- examples/train.py | 9 +- flow/benchmarks/baselines/figureeight012.py | 2 +- flow/benchmarks/baselines/merge012.py | 2 +- flow/controllers/__init__.py | 4 +- flow/controllers/routing_controllers.py | 1 - flow/core/experiment.py | 108 +- flow/core/kernel/vehicle/traci.py | 27 + flow/core/params.py | 9 +- flow/envs/multiagent/__init__.py | 5 +- flow/envs/multiagent/i210.py | 225 + flow/networks/__init__.py | 3 +- flow/networks/i210_subnetwork.py | 141 + flow/utils/aimsun/Aimsun_Flow.ang | Bin 159038 -> 0 bytes flow/visualize/time_space_diagram.py | 178 +- requirements.txt | 2 +- scripts/ray_autoscale.yaml | 4 +- tests/fast_tests/test_controllers.py | 74 +- tests/fast_tests/test_examples.py | 58 + tests/fast_tests/test_files/i210_emission.csv | 27 + tests/fast_tests/test_visualizers.py | 71 +- 26 files changed, 7009 insertions(+), 104 deletions(-) create mode 100644 examples/exp_configs/non_rl/i210_subnetwork.py create mode 100644 examples/exp_configs/non_rl/i210_subnetwork_sweep.py create mode 100644 examples/exp_configs/rl/multiagent/multiagent_i210.py create mode 100644 examples/exp_configs/templates/sumo/test2.net.xml create mode 100644 examples/exp_scripts/compute_210_calibration.py create mode 100644 flow/envs/multiagent/i210.py create mode 100644 flow/networks/i210_subnetwork.py delete mode 100644 flow/utils/aimsun/Aimsun_Flow.ang create mode 100644 tests/fast_tests/test_files/i210_emission.csv diff --git a/examples/exp_configs/non_rl/i210_subnetwork.py b/examples/exp_configs/non_rl/i210_subnetwork.py new file mode 100644 index 000000000..c5077e262 --- /dev/null +++ b/examples/exp_configs/non_rl/i210_subnetwork.py @@ -0,0 +1,111 @@ +"""I-210 subnetwork example.""" +import os + +import numpy as np + +from flow.controllers.car_following_models import IDMController +from flow.core.params import SumoParams +from flow.core.params import EnvParams +from flow.core.params import NetParams +from flow.core.params import SumoLaneChangeParams +from flow.core.params import VehicleParams +from flow.core.params import InitialConfig +from flow.core.params import InFlows +import flow.config as config +from flow.envs import TestEnv +from flow.networks.i210_subnetwork import I210SubNetwork, EDGES_DISTRIBUTION + +# create the base vehicle type that will be used for inflows +vehicles = VehicleParams() +vehicles.add( + "human", + num_vehicles=0, + lane_change_params=SumoLaneChangeParams( + lane_change_mode="strategic", + ), + acceleration_controller=(IDMController, { + "a": 0.3, "b": 2.0, "noise": 0.45 + }), +) + +inflow = InFlows() +# main highway +inflow.add( + veh_type="human", + edge="119257914", + vehs_per_hour=8378, + departLane="random", + departSpeed=23) +# on ramp +# inflow.add( +# veh_type="human", +# edge="27414345", +# vehs_per_hour=321, +# departLane="random", +# departSpeed=20) +# inflow.add( +# veh_type="human", +# edge="27414342#0", +# vehs_per_hour=421, +# departLane="random", +# departSpeed=20) + +NET_TEMPLATE = os.path.join( + config.PROJECT_PATH, + "examples/exp_configs/templates/sumo/test2.net.xml") + +flow_params = dict( + # name of the experiment + exp_tag='I-210_subnetwork', + + # name of the flow environment the experiment is running on + env_name=TestEnv, + + # name of the network class the experiment is running on + network=I210SubNetwork, + + # simulator that is used by the experiment + simulator='traci', + + # simulation-related parameters + sim=SumoParams( + sim_step=0.8, + render=False, + color_by_speed=True, + ), + + # environment related parameters (see flow.core.params.EnvParams) + env=EnvParams( + horizon=4500, + ), + + # network-related parameters (see flow.core.params.NetParams and the + # network's documentation or ADDITIONAL_NET_PARAMS component) + net=NetParams( + inflows=inflow, + template=NET_TEMPLATE + ), + + # vehicles to be placed in the network at the start of a rollout (see + # flow.core.params.VehicleParams) + veh=vehicles, + + # parameters specifying the positioning of vehicles upon initialization/ + # reset (see flow.core.params.InitialConfig) + initial=InitialConfig( + edges_distribution=EDGES_DISTRIBUTION, + ), +) + +edge_id = "119257908#1-AddedOnRampEdge" +custom_callables = { + "avg_merge_speed": lambda env: np.nan_to_num(np.mean( + env.k.vehicle.get_speed(env.k.vehicle.get_ids_by_edge(edge_id)))), + "avg_outflow": lambda env: np.nan_to_num( + env.k.vehicle.get_outflow_rate(120)), + # we multiply by 5 to account for the vehicle length and by 1000 to convert + # into veh/km + "avg_density": lambda env: 5 * 1000 * len(env.k.vehicle.get_ids_by_edge( + edge_id)) / (env.k.network.edge_length(edge_id) + * env.k.network.num_lanes(edge_id)), +} diff --git a/examples/exp_configs/non_rl/i210_subnetwork_sweep.py b/examples/exp_configs/non_rl/i210_subnetwork_sweep.py new file mode 100644 index 000000000..28cba81ce --- /dev/null +++ b/examples/exp_configs/non_rl/i210_subnetwork_sweep.py @@ -0,0 +1,151 @@ +"""I-210 subnetwork example. + +In this case flow_params is a list of dicts. This is to test the effects of +multiple human-driver model parameters on the flow traffic. +""" +from collections import OrderedDict +from copy import deepcopy +import itertools +import os +import numpy as np + +from flow.core.params import SumoParams +from flow.core.params import EnvParams +from flow.core.params import NetParams +from flow.core.params import SumoLaneChangeParams +from flow.core.params import VehicleParams +from flow.core.params import InitialConfig +from flow.core.params import InFlows +import flow.config as config +from flow.envs import TestEnv +from flow.networks.i210_subnetwork import I210SubNetwork, EDGES_DISTRIBUTION + +# the default parameters for all lane change parameters +default_dict = { + "lane_change_mode": "strategic", + "model": "LC2013", + "lc_strategic": 1.0, + "lc_cooperative": 1.0, + "lc_speed_gain": 1.0, + "lc_keep_right": 1.0, + "lc_look_ahead_left": 2.0, + "lc_speed_gain_right": 1.0, + "lc_sublane": 1.0, + "lc_pushy": 0, + "lc_pushy_gap": 0.6, + "lc_assertive": 1, + "lc_accel_lat": 1.0 +} + +# values to sweep through for some lane change parameters +sweep_dict = OrderedDict({ + "lc_strategic": [1.0, 2.0, 4.0, 8.0], + "lc_cooperative": [1.0, 2.0], + "lc_look_ahead_left": [2.0, 4.0] +}) + +# Create a list of possible lane change parameter combinations. +all_names = sorted(sweep_dict) +combinations = itertools.product(*(sweep_dict[name] for name in all_names)) +combination_list = list(combinations) +res = [] +for val in combination_list: + curr_dict = {} + for elem, name in zip(val, all_names): + curr_dict[name] = elem + res.append(curr_dict) + +# Create a list of all possible flow_params dictionaries to sweep through the +# different lane change parameters. +flow_params = [] + +for lane_change_dict in res: + # no vehicles in the network. The lane change parameters of inflowing + # vehicles are updated here. + vehicles = VehicleParams() + update_dict = deepcopy(default_dict) + update_dict.update(lane_change_dict) + vehicles.add( + "human", + num_vehicles=0, + lane_change_params=SumoLaneChangeParams(**update_dict) + ) + + inflow = InFlows() + # main highway + inflow.add( + veh_type="human", + edge="119257914", + vehs_per_hour=8378, + # probability=1.0, + departLane="random", + departSpeed=20) + # on ramp + inflow.add( + veh_type="human", + edge="27414345", + vehs_per_hour=321, + departLane="random", + departSpeed=20) + inflow.add( + veh_type="human", + edge="27414342#0", + vehs_per_hour=421, + departLane="random", + departSpeed=20) + + NET_TEMPLATE = os.path.join( + config.PROJECT_PATH, + "examples/exp_configs/templates/sumo/test2.net.xml") + + params = dict( + # name of the experiment + exp_tag='I-210_subnetwork', + + # name of the flow environment the experiment is running on + env_name=TestEnv, + + # name of the network class the experiment is running on + network=I210SubNetwork, + + # simulator that is used by the experiment + simulator='traci', + + # simulation-related parameters + sim=SumoParams( + sim_step=0.8, + render=True, + color_by_speed=True + ), + + # environment related parameters (see flow.core.params.EnvParams) + env=EnvParams( + horizon=4500, # one hour of run time + ), + + # network-related parameters (see flow.core.params.NetParams and the + # network's documentation or ADDITIONAL_NET_PARAMS component) + net=NetParams( + inflows=inflow, + template=NET_TEMPLATE + ), + + # vehicles to be placed in the network at the start of a rollout (see + # flow.core.params.VehicleParams) + veh=vehicles, + + # parameters specifying the positioning of vehicles upon + # initialization/reset (see flow.core.params.InitialConfig) + initial=InitialConfig( + edges_distribution=EDGES_DISTRIBUTION, + ), + ) + + # Store the next flow_params dict. + flow_params.append(params) + + +custom_callables = { + "avg_merge_speed": lambda env: np.mean(env.k.vehicle.get_speed( + env.k.vehicle.get_ids_by_edge("119257908#1-AddedOnRampEdge"))) +} diff --git a/examples/exp_configs/rl/multiagent/multiagent_i210.py b/examples/exp_configs/rl/multiagent/multiagent_i210.py new file mode 100644 index 000000000..94f709ff4 --- /dev/null +++ b/examples/exp_configs/rl/multiagent/multiagent_i210.py @@ -0,0 +1,181 @@ +"""Multi-agent I-210 example. + +Trains a non-constant number of agents, all sharing the same policy, on the +highway with ramps network. +""" +import os + +from ray.rllib.agents.ppo.ppo_policy import PPOTFPolicy +from ray.tune.registry import register_env + +import flow.config as config +from flow.controllers.rlcontroller import RLController +from flow.core.params import EnvParams +from flow.core.params import NetParams +from flow.core.params import InitialConfig +from flow.core.params import InFlows +from flow.core.params import VehicleParams +from flow.core.params import SumoParams +from flow.core.params import SumoLaneChangeParams +from flow.networks.i210_subnetwork import I210SubNetwork, EDGES_DISTRIBUTION +from flow.envs.multiagent.i210 import I210MultiEnv, ADDITIONAL_ENV_PARAMS +from flow.utils.registry import make_create_env + +# SET UP PARAMETERS FOR THE SIMULATION + +# number of training iterations +N_TRAINING_ITERATIONS = 200 +# number of rollouts per training iteration +N_ROLLOUTS = 2 +# number of steps per rollout +HORIZON = 500 +# number of parallel workers +N_CPUS = 1 + +# percentage of autonomous vehicles compared to human vehicles on highway +PENETRATION_RATE = 10 + +# SET UP PARAMETERS FOR THE ENVIRONMENT +additional_env_params = ADDITIONAL_ENV_PARAMS.copy() +additional_env_params.update({ + 'max_accel': 1, + 'max_decel': 1, + # configure the observation space. Look at the I210MultiEnv class for more info. + 'lead_obs': True, +}) + +# CREATE VEHICLE TYPES AND INFLOWS +# no vehicles in the network +vehicles = VehicleParams() +vehicles.add( + "human", + num_vehicles=0, + lane_change_params=SumoLaneChangeParams( + lane_change_mode="strategic", + ) +) +vehicles.add( + "av", + acceleration_controller=(RLController, {}), + num_vehicles=0, +) + +inflow = InFlows() +# main highway +pen_rate = PENETRATION_RATE / 100 +assert pen_rate < 1.0, "your penetration rate is over 100%" +assert pen_rate > 0.0, "your penetration rate should be above zero" +inflow.add( + veh_type="human", + edge="119257914", + vehs_per_hour=8378 * pen_rate, + # probability=1.0, + departLane="random", + departSpeed=20) +# on ramp +# inflow.add( +# veh_type="human", +# edge="27414345", +# vehs_per_hour=321 * pen_rate, +# departLane="random", +# departSpeed=20) +# inflow.add( +# veh_type="human", +# edge="27414342#0", +# vehs_per_hour=421 * pen_rate, +# departLane="random", +# departSpeed=20) + +# Now add the AVs +# main highway +inflow.add( + veh_type="av", + edge="119257914", + vehs_per_hour=int(8378 * pen_rate), + # probability=1.0, + departLane="random", + departSpeed=20) +# # on ramp +# inflow.add( +# veh_type="av", +# edge="27414345", +# vehs_per_hour=int(321 * pen_rate), +# departLane="random", +# departSpeed=20) +# inflow.add( +# veh_type="av", +# edge="27414342#0", +# vehs_per_hour=int(421 * pen_rate), +# departLane="random", +# departSpeed=20) + +NET_TEMPLATE = os.path.join( + config.PROJECT_PATH, + "examples/exp_configs/templates/sumo/test2.net.xml") + +flow_params = dict( + # name of the experiment + exp_tag='I_210_subnetwork', + + # name of the flow environment the experiment is running on + env_name=I210MultiEnv, + + # name of the network class the experiment is running on + network=I210SubNetwork, + + # simulator that is used by the experiment + simulator='traci', + + # simulation-related parameters + sim=SumoParams( + sim_step=0.8, + render=False, + color_by_speed=True, + restart_instance=True + ), + + # environment related parameters (see flow.core.params.EnvParams) + env=EnvParams( + horizon=HORIZON, + sims_per_step=1, + additional_params=additional_env_params, + ), + + # network-related parameters (see flow.core.params.NetParams and the + # network's documentation or ADDITIONAL_NET_PARAMS component) + net=NetParams( + inflows=inflow, + template=NET_TEMPLATE + ), + + # vehicles to be placed in the network at the start of a rollout (see + # flow.core.params.VehicleParams) + veh=vehicles, + + # parameters specifying the positioning of vehicles upon initialization/ + # reset (see flow.core.params.InitialConfig) + initial=InitialConfig( + edges_distribution=EDGES_DISTRIBUTION, + ), +) + +# SET UP RLLIB MULTI-AGENT FEATURES + +create_env, env_name = make_create_env(params=flow_params, version=0) + +# register as rllib env +register_env(env_name, create_env) + +# multiagent configuration +test_env = create_env() +obs_space = test_env.observation_space +act_space = test_env.action_space + +POLICY_GRAPHS = {'av': (PPOTFPolicy, obs_space, act_space, {})} + +POLICIES_TO_TRAIN = ['av'] + + +def policy_mapping_fn(_): + """Map a policy in RLlib.""" + return 'av' diff --git a/examples/exp_configs/templates/sumo/test2.net.xml b/examples/exp_configs/templates/sumo/test2.net.xml new file mode 100644 index 000000000..00e3edcd5 --- /dev/null +++ b/examples/exp_configs/templates/sumo/test2.net.xml @@ -0,0 +1,5677 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/exp_scripts/compute_210_calibration.py b/examples/exp_scripts/compute_210_calibration.py new file mode 100644 index 000000000..d586a9019 --- /dev/null +++ b/examples/exp_scripts/compute_210_calibration.py @@ -0,0 +1,33 @@ +"""We load the calibrated data from calibrated_values and compute how accurate it is.""" +import numpy as np +import pandas as pd +import pickle as pkl +import os + +if __name__ == '__main__': + with open(os.path.abspath('../calibrated_values/info_dict.pkl'), 'rb') as file: + data = pkl.load(file) + + calibrated_data = pd.read_csv('../calibrated_values/i210_sub_merge_area_reduced.csv') + valid_section = calibrated_data[calibrated_data['oid'] == 8009307] + speeds = valid_section['speed'].to_numpy() / 3.6 # (km/h to m/s) + density = valid_section['density'] + outflow = valid_section['flow'] + + dict_to_idx = {'oid': 0, 'ent': 1, 'flow': 2, 'ttime': 3, + 'speed': 4, 'density': 5, 'lane_changes': 6, 'total_lane_changes': 7} + + errors = [] + # compute the speed errors for a given set of params + for experiment in data: + merge_speed = experiment['avg_merge_speed'] + # now sum it up in segments noting that the sim step is 0.8 + num_steps = int(120 / 0.8) + + step_sizes = np.arange(0, len(merge_speed), num_steps) + # sum up all the slices + summed_slices = np.add.reduceat(merge_speed, step_sizes) / num_steps + # throw away the last point and the first point before the network is formed + error = np.abs(np.mean(summed_slices[:-1] - speeds[:summed_slices.shape[0] - 1])) + errors.append(error) + print(errors) diff --git a/examples/simulate.py b/examples/simulate.py index 04967b830..60767b6b7 100644 --- a/examples/simulate.py +++ b/examples/simulate.py @@ -56,16 +56,22 @@ def parse_args(args): module = __import__("exp_configs.non_rl", fromlist=[flags.exp_config]) flow_params = getattr(module, flags.exp_config).flow_params + # Get the custom callables for the runner. + if hasattr(getattr(module, flags.exp_config), "custom_callables"): + callables = getattr(module, flags.exp_config).custom_callables + else: + callables = None + # Update some variables based on inputs. flow_params['sim'].render = not flags.no_render flow_params['simulator'] = 'aimsun' if flags.aimsun else 'traci' - # specify an emission path if they are meant to be generated + # Specify an emission path if they are meant to be generated. if flags.gen_emission: flow_params['sim'].emission_path = "./data" # Create the experiment object. - exp = Experiment(flow_params) + exp = Experiment(flow_params, callables) # Run for the specified number of rollouts. exp.run(flags.num_runs, convert_to_csv=flags.gen_emission) diff --git a/examples/train.py b/examples/train.py index 7f006a18f..a159c13ee 100644 --- a/examples/train.py +++ b/examples/train.py @@ -53,8 +53,8 @@ def parse_args(args): # optional input parameters parser.add_argument( - '--rl_trainer', type=str, default="RLlib", - help='the RL trainer to use. either RLlib or Stable-Baselines') + '--rl_trainer', type=str, default="rllib", + help='the RL trainer to use. either rllib or Stable-Baselines') parser.add_argument( '--num_cpus', type=int, default=1, @@ -150,7 +150,6 @@ def setup_exps_rllib(flow_params, config["lambda"] = 0.97 config["kl_target"] = 0.02 config["num_sgd_iter"] = 10 - config['clip_actions'] = False # FIXME(ev) temporary ray bug config["horizon"] = horizon # save the flow params for replay @@ -185,12 +184,12 @@ def setup_exps_rllib(flow_params, submodule = getattr(module, flags.exp_config) elif hasattr(module_ma, flags.exp_config): submodule = getattr(module_ma, flags.exp_config) - assert flags.rl_trainer == "RLlib", \ + assert flags.rl_trainer.lower() == "RLlib".lower(), \ "Currently, multiagent experiments are only supported through "\ "RLlib. Try running this experiment using RLlib: 'python train.py EXP_CONFIG'" else: assert False, "Unable to find experiment config!" - if flags.rl_trainer == "RLlib": + if flags.rl_trainer.lower() == "rllib": flow_params = submodule.flow_params n_cpus = submodule.N_CPUS n_rollouts = submodule.N_ROLLOUTS diff --git a/flow/benchmarks/baselines/figureeight012.py b/flow/benchmarks/baselines/figureeight012.py index 9d2cbf3c1..d18a968fa 100644 --- a/flow/benchmarks/baselines/figureeight012.py +++ b/flow/benchmarks/baselines/figureeight012.py @@ -51,7 +51,7 @@ class needed to run simulations exp = Experiment(flow_params) results = exp.run(num_runs) - avg_speed = np.mean(results['mean_returns']) + avg_speed = np.mean(results['returns']) return avg_speed diff --git a/flow/benchmarks/baselines/merge012.py b/flow/benchmarks/baselines/merge012.py index 6ed1b0b15..7851dc31f 100644 --- a/flow/benchmarks/baselines/merge012.py +++ b/flow/benchmarks/baselines/merge012.py @@ -37,7 +37,7 @@ class needed to run simulations exp = Experiment(flow_params) results = exp.run(num_runs) - avg_speed = np.mean(results['mean_returns']) + avg_speed = np.mean(results['returns']) return avg_speed diff --git a/flow/controllers/__init__.py b/flow/controllers/__init__.py index 01f4ad90f..6cb20077a 100755 --- a/flow/controllers/__init__.py +++ b/flow/controllers/__init__.py @@ -16,7 +16,7 @@ BCMController, OVMController, LinearOVM, IDMController, \ SimCarFollowingController, LACController, GippsController from flow.controllers.velocity_controllers import FollowerStopper, \ - PISaturation + PISaturation, NonLocalFollowerStopper # lane change controllers from flow.controllers.base_lane_changing_controller import \ @@ -35,5 +35,5 @@ "IDMController", "SimCarFollowingController", "FollowerStopper", "PISaturation", "StaticLaneChanger", "SimLaneChangeController", "ContinuousRouter", "GridRouter", "BayBridgeRouter", "LACController", - "GippsController" + "GippsController", "NonLocalFollowerStopper" ] diff --git a/flow/controllers/routing_controllers.py b/flow/controllers/routing_controllers.py index f9b346fdd..e6ccdde78 100755 --- a/flow/controllers/routing_controllers.py +++ b/flow/controllers/routing_controllers.py @@ -1,5 +1,4 @@ """Contains a list of custom routing controllers.""" - import random import numpy as np diff --git a/flow/core/experiment.py b/flow/core/experiment.py index 7a66a58ee..69a78cb0e 100755 --- a/flow/core/experiment.py +++ b/flow/core/experiment.py @@ -1,13 +1,11 @@ """Contains an experiment class for running simulations.""" - -import logging +from flow.core.util import emission_to_csv +from flow.utils.registry import make_create_env import datetime -import numpy as np +import logging import time import os - -from flow.core.util import emission_to_csv -from flow.utils.registry import make_create_env +import numpy as np class Experiment: @@ -52,12 +50,30 @@ class can generate csv files from emission files produced by sumo. These Attributes ---------- + custom_callables : dict < str, lambda > + strings and lambda functions corresponding to some information we want + to extract from the environment. The lambda will be called at each step + to extract information from the env and it will be stored in a dict + keyed by the str. env : flow.envs.Env the environment object the simulator will run """ - def __init__(self, flow_params): - """Instantiate Experiment.""" + def __init__(self, flow_params, custom_callables=None): + """Instantiate the Experiment class. + + Parameters + ---------- + flow_params : dict + flow-specific parameters + custom_callables : dict < str, lambda > + strings and lambda functions corresponding to some information we + want to extract from the environment. The lambda will be called at + each step to extract information from the env and it will be stored + in a dict keyed by the str. + """ + self.custom_callables = custom_callables or {} + # Get the env name and a creator for the environment. create_env, _ = make_create_env(flow_params) @@ -85,7 +101,7 @@ def run(self, num_runs, rl_actions=None, convert_to_csv=False): Returns ------- - info_dict : dict + info_dict : dict < str, Any > contains returns, average speed per step """ num_steps = self.env.env_params.horizon @@ -103,62 +119,64 @@ def run(self, num_runs, rl_actions=None, convert_to_csv=False): 'output should be generated. If you do not wish to generate ' 'emissions, set the convert_to_csv parameter to False.') - info_dict = {} + # used to store + info_dict = { + "returns": [], + "velocities": [], + "outflows": [], + } + info_dict.update({ + key: [] for key in self.custom_callables.keys() + }) + if rl_actions is None: def rl_actions(*_): return None - rets = [] - mean_rets = [] - ret_lists = [] - vels = [] - mean_vels = [] - std_vels = [] - outflows = [] + # time profiling information t = time.time() times = [] - vehicle_times = [] + for i in range(num_runs): - vel = np.zeros(num_steps) - logging.info("Iter #" + str(i)) ret = 0 - ret_list = [] + vel = [] + custom_vals = {key: [] for key in self.custom_callables.keys()} state = self.env.reset() for j in range(num_steps): t0 = time.time() state, reward, done, _ = self.env.step(rl_actions(state)) t1 = time.time() times.append(1 / (t1 - t0)) - vehicle_times.append(self.env.k.vehicle.num_vehicles / (t1 - t0)) - vel[j] = np.mean( - self.env.k.vehicle.get_speed(self.env.k.vehicle.get_ids())) + + # Compute the velocity speeds and cumulative returns. + veh_ids = self.env.k.vehicle.get_ids() + vel.append(np.mean(self.env.k.vehicle.get_speed(veh_ids))) ret += reward - ret_list.append(reward) + + # Compute the results for the custom callables. + for (key, lambda_func) in self.custom_callables.items(): + custom_vals[key].append(lambda_func(self.env)) if done: break - rets.append(ret) - vels.append(vel) - mean_rets.append(np.mean(ret_list)) - ret_lists.append(ret_list) - mean_vels.append(np.mean(vel)) - std_vels.append(np.std(vel)) - outflows.append(self.env.k.vehicle.get_outflow_rate(int(500))) + + # Store the information from the run in info_dict. + outflow = self.env.k.vehicle.get_outflow_rate(int(500)) + info_dict["returns"].append(ret) + info_dict["velocities"].append(np.mean(vel)) + info_dict["outflows"].append(outflow) + for key in custom_vals.keys(): + info_dict[key].append(np.mean(custom_vals[key])) + print("Round {0}, return: {1}".format(i, ret)) - info_dict["returns"] = rets - info_dict["velocities"] = vels - info_dict["mean_returns"] = mean_rets - info_dict["per_step_returns"] = ret_lists - info_dict["mean_outflows"] = np.mean(outflows) - - print("Average, std return: {}, {}".format( - np.mean(rets), np.std(rets))) - print("Average, std speed: {}, {}".format( - np.mean(mean_vels), np.std(mean_vels))) - print("Total time: ", time.time() - t) - print("steps/second: ", np.mean(times)) - print("vehicles.steps/second: ", np.mean(vehicle_times)) + # Print the averages/std for all variables in the info_dict. + for key in info_dict.keys(): + print("Average, std {}: {}, {}".format( + key, np.mean(info_dict[key]), np.std(info_dict[key]))) + + print("Total time:", time.time() - t) + print("steps/second:", np.mean(times)) self.env.terminate() if convert_to_csv and self.env.simulator == "traci": diff --git a/flow/core/kernel/vehicle/traci.py b/flow/core/kernel/vehicle/traci.py index 939d9eacc..745b49650 100644 --- a/flow/core/kernel/vehicle/traci.py +++ b/flow/core/kernel/vehicle/traci.py @@ -18,6 +18,11 @@ WHITE = (255, 255, 255) CYAN = (0, 255, 255) RED = (255, 0, 0) +GREEN = (0, 255, 0) +STEPS = 10 +rdelta = 255 / STEPS +# smoothly go from red to green as the speed increases +color_bins = [[int(255 - rdelta * i), int(rdelta * i), 0] for i in range(STEPS + 1)] class TraCIVehicle(KernelVehicle): @@ -72,6 +77,7 @@ def __init__(self, # whether or not to automatically color vehicles try: + self._color_by_speed = sim_params.color_by_speed self._force_color_update = sim_params.force_color_update except AttributeError: self._force_color_update = False @@ -1001,6 +1007,27 @@ def update_vehicle_colors(self): except (FatalTraCIError, TraCIException) as e: print('Error when updating human vehicle colors:', e) + for veh_id in self.get_ids(): + try: + if 'av' in veh_id: + color = RED + # If vehicle is already being colored via argument to vehicles.add(), don't re-color it. + if self._force_color_update or 'color' not in self.type_parameters[self.get_type(veh_id)]: + self.set_color(veh_id=veh_id, color=color) + except (FatalTraCIError, TraCIException) as e: + print('Error when updating human vehicle colors:', e) + + # color vehicles by speed if desired + if self._color_by_speed: + max_speed = self.master_kernel.network.max_speed() + speed_ranges = np.linspace(0, max_speed, STEPS) + for veh_id in self.get_ids(): + veh_speed = self.get_speed(veh_id) + bin_index = np.digitize(veh_speed, speed_ranges) + # If vehicle is already being colored via argument to vehicles.add(), don't re-color it. + if self._force_color_update or 'color' not in self.type_parameters[self.get_type(veh_id)]: + self.set_color(veh_id=veh_id, color=color_bins[bin_index]) + # clear the list of observed vehicles for veh_id in self.get_observed_ids(): self.remove_observed(veh_id) diff --git a/flow/core/params.py b/flow/core/params.py index fadffdc44..e424455bb 100755 --- a/flow/core/params.py +++ b/flow/core/params.py @@ -563,6 +563,9 @@ class SumoParams(SimParams): they teleport after teleport_time seconds num_clients : int, optional Number of clients that will connect to Traci + color_by_speed : bool + whether to color the vehicles by the speed they are moving at the + current time step """ def __init__(self, @@ -582,7 +585,8 @@ def __init__(self, restart_instance=False, print_warnings=True, teleport_time=-1, - num_clients=1): + num_clients=1, + color_by_speed=False): """Instantiate SumoParams.""" super(SumoParams, self).__init__( sim_step, render, restart_instance, emission_path, save_render, @@ -595,6 +599,7 @@ def __init__(self, self.print_warnings = print_warnings self.teleport_time = teleport_time self.num_clients = num_clients + self.color_by_speed = color_by_speed class EnvParams: @@ -1017,7 +1022,7 @@ def __init__(self, "lcCooperative": str(lc_cooperative), "lcSpeedGain": str(lc_speed_gain), "lcKeepRight": str(lc_keep_right), - # "lcLookaheadLeft": str(lcLookaheadLeft), + # "lcLookaheadLeft": str(lc_look_ahead_left), # "lcSpeedGainRight": str(lcSpeedGainRight) } elif model == "SL2015": diff --git a/flow/envs/multiagent/__init__.py b/flow/envs/multiagent/__init__.py index 2ae6780b8..f7889591d 100644 --- a/flow/envs/multiagent/__init__.py +++ b/flow/envs/multiagent/__init__.py @@ -10,7 +10,7 @@ from flow.envs.multiagent.traffic_light_grid import MultiTrafficLightGridPOEnv from flow.envs.multiagent.highway import MultiAgentHighwayPOEnv from flow.envs.multiagent.merge import MultiAgentMergePOEnv - +from flow.envs.multiagent.i210 import I210MultiEnv __all__ = [ 'MultiEnv', @@ -20,5 +20,6 @@ 'MultiAgentHighwayPOEnv', 'MultiAgentAccelPOEnv', 'MultiAgentWaveAttenuationPOEnv', - 'MultiAgentMergePOEnv' + 'MultiAgentMergePOEnv', + 'I210MultiEnv' ] diff --git a/flow/envs/multiagent/i210.py b/flow/envs/multiagent/i210.py new file mode 100644 index 000000000..409aeb14f --- /dev/null +++ b/flow/envs/multiagent/i210.py @@ -0,0 +1,225 @@ +"""Environment for training vehicles to reduce congestion in the I210.""" + +from gym.spaces import Box +import numpy as np + +from flow.core.rewards import average_velocity +from flow.envs.multiagent.base import MultiEnv + +# largest number of lanes on any given edge in the network +MAX_LANES = 6 + +ADDITIONAL_ENV_PARAMS = { + # maximum acceleration for autonomous vehicles, in m/s^2 + "max_accel": 1, + # maximum deceleration for autonomous vehicles, in m/s^2 + "max_decel": 1, + # whether we use an obs space that contains adjacent lane info or just the lead obs + "lead_obs": True, +} + + +class I210MultiEnv(MultiEnv): + """Partially observable multi-agent environment for the I-210 subnetworks. + + The policy is shared among the agents, so there can be a non-constant + number of RL vehicles throughout the simulation. + + Required from env_params: + + * max_accel: maximum acceleration for autonomous vehicles, in m/s^2 + * max_decel: maximum deceleration for autonomous vehicles, in m/s^2 + + The following states, actions and rewards are considered for one autonomous + vehicle only, as they will be computed in the same way for each of them. + + States + The observation consists of the speeds and bumper-to-bumper headways of + the vehicles immediately preceding and following autonomous vehicles in + all of the preceding lanes as well, a binary value indicating which of + these vehicles is autonomous, and the speed of the autonomous vehicle. + Missing vehicles are padded with zeros. + + Actions + The action consists of an acceleration, bound according to the + environment parameters, as well as three values that will be converted + into probabilities via softmax to decide of a lane change (left, none + or right). NOTE: lane changing is currently not enabled. It's a TODO. + + Rewards + The reward function encourages proximity of the system-level velocity + to a desired velocity specified in the environment parameters, while + slightly penalizing small time headways among autonomous vehicles. + + Termination + A rollout is terminated if the time horizon is reached or if two + vehicles collide into one another. + """ + + def __init__(self, env_params, sim_params, network, simulator='traci'): + super().__init__(env_params, sim_params, network, simulator) + self.lead_obs = env_params.additional_params.get("lead_obs") + + @property + def observation_space(self): + """See class definition.""" + # speed, speed of leader, headway + if self.lead_obs: + return Box( + low=-float('inf'), + high=float('inf'), + shape=(3,), + dtype=np.float32 + ) + # speed, dist to ego vehicle, binary value which is 1 if the vehicle is + # an AV + else: + leading_obs = 3 * MAX_LANES + follow_obs = 3 * MAX_LANES + + # speed and lane + self_obs = 2 + + return Box( + low=-float('inf'), + high=float('inf'), + shape=(leading_obs + follow_obs + self_obs,), + dtype=np.float32 + ) + + @property + def action_space(self): + """See class definition.""" + return Box( + low=-np.abs(self.env_params.additional_params['max_decel']), + high=self.env_params.additional_params['max_accel'], + shape=(1,), # (4,), + dtype=np.float32) + + def _apply_rl_actions(self, rl_actions): + """See class definition.""" + # in the warmup steps, rl_actions is None + if rl_actions: + for rl_id, actions in rl_actions.items(): + accel = actions[0] + + # lane_change_softmax = np.exp(actions[1:4]) + # lane_change_softmax /= np.sum(lane_change_softmax) + # lane_change_action = np.random.choice([-1, 0, 1], + # p=lane_change_softmax) + + self.k.vehicle.apply_acceleration(rl_id, accel) + # self.k.vehicle.apply_lane_change(rl_id, lane_change_action) + + def get_state(self): + """See class definition.""" + if self.lead_obs: + veh_info = {} + for rl_id in self.k.vehicle.get_rl_ids(): + speed = self.k.vehicle.get_speed(rl_id) + headway = self.k.vehicle.get_headway(rl_id) + lead_speed = self.k.vehicle.get_speed(self.k.vehicle.get_leader(rl_id)) + if lead_speed == -1001: + lead_speed = 0 + veh_info.update({rl_id: np.array([speed / 50.0, headway / 1000.0, lead_speed / 50.0])}) + else: + veh_info = {rl_id: np.concatenate((self.state_util(rl_id), + self.veh_statistics(rl_id))) + for rl_id in self.k.vehicle.get_rl_ids()} + return veh_info + + def compute_reward(self, rl_actions, **kwargs): + # TODO(@evinitsky) we need something way better than this. Something that adds + # in notions of local reward + """See class definition.""" + # in the warmup steps + if rl_actions is None: + return {} + + rewards = {} + for rl_id in self.k.vehicle.get_rl_ids(): + if self.env_params.evaluate: + # reward is speed of vehicle if we are in evaluation mode + reward = self.k.vehicle.get_speed(rl_id) + elif kwargs['fail']: + # reward is 0 if a collision occurred + reward = 0 + else: + # reward high system-level velocities + cost1 = average_velocity(self, fail=kwargs['fail']) + + # penalize small time headways + cost2 = 0 + t_min = 1 # smallest acceptable time headway + + lead_id = self.k.vehicle.get_leader(rl_id) + if lead_id not in ["", None] \ + and self.k.vehicle.get_speed(rl_id) > 0: + t_headway = max( + self.k.vehicle.get_headway(rl_id) / + self.k.vehicle.get_speed(rl_id), 0) + cost2 += min((t_headway - t_min) / t_min, 0) + + # weights for cost1, cost2, and cost3, respectively + eta1, eta2 = 1.00, 0.10 + + reward = max(eta1 * cost1 + eta2 * cost2, 0) + + rewards[rl_id] = reward + return rewards + + def additional_command(self): + """See parent class. + + Define which vehicles are observed for visualization purposes. + """ + # specify observed vehicles + for rl_id in self.k.vehicle.get_rl_ids(): + # leader + lead_id = self.k.vehicle.get_leader(rl_id) + if lead_id: + self.k.vehicle.set_observed(lead_id) + # follower + follow_id = self.k.vehicle.get_follower(rl_id) + if follow_id: + self.k.vehicle.set_observed(follow_id) + + def state_util(self, rl_id): + """Return an array of headway, tailway, leader speed, follower speed. + + Also return a 1 if leader is rl 0 otherwise, a 1 if follower is rl 0 otherwise. + If there are fewer than MAX_LANES the extra + entries are filled with -1 to disambiguate from zeros. + """ + veh = self.k.vehicle + lane_headways = veh.get_lane_headways(rl_id).copy() + lane_tailways = veh.get_lane_tailways(rl_id).copy() + lane_leader_speed = veh.get_lane_leaders_speed(rl_id).copy() + lane_follower_speed = veh.get_lane_followers_speed(rl_id).copy() + leader_ids = veh.get_lane_leaders(rl_id).copy() + follower_ids = veh.get_lane_followers(rl_id).copy() + rl_ids = self.k.vehicle.get_rl_ids() + is_leader_rl = [1 if l_id in rl_ids else 0 for l_id in leader_ids] + is_follow_rl = [1 if f_id in rl_ids else 0 for f_id in follower_ids] + diff = MAX_LANES - len(is_leader_rl) + if diff > 0: + # the minus 1 disambiguates missing cars from missing lanes + lane_headways += diff * [-1] + lane_tailways += diff * [-1] + lane_leader_speed += diff * [-1] + lane_follower_speed += diff * [-1] + is_leader_rl += diff * [-1] + is_follow_rl += diff * [-1] + lane_headways = np.asarray(lane_headways) / 1000 + lane_tailways = np.asarray(lane_tailways) / 1000 + lane_leader_speed = np.asarray(lane_leader_speed) / 100 + lane_follower_speed = np.asarray(lane_follower_speed) / 100 + return np.concatenate((lane_headways, lane_tailways, lane_leader_speed, + lane_follower_speed, is_leader_rl, + is_follow_rl)) + + def veh_statistics(self, rl_id): + """Return speed, edge information, and x, y about the vehicle itself.""" + speed = self.k.vehicle.get_speed(rl_id) / 100.0 + lane = (self.k.vehicle.get_lane(rl_id) + 1) / 10.0 + return np.array([speed, lane]) diff --git a/flow/networks/__init__.py b/flow/networks/__init__.py index a3677d83a..af849031d 100644 --- a/flow/networks/__init__.py +++ b/flow/networks/__init__.py @@ -15,10 +15,11 @@ from flow.networks.multi_ring import MultiRingNetwork from flow.networks.minicity import MiniCityNetwork from flow.networks.highway_ramps import HighwayRampsNetwork +from flow.networks.i210_subnetwork import I210SubNetwork __all__ = [ "Network", "BayBridgeNetwork", "BayBridgeTollNetwork", "BottleneckNetwork", "FigureEightNetwork", "TrafficLightGridNetwork", "HighwayNetwork", "RingNetwork", "MergeNetwork", "MultiRingNetwork", - "MiniCityNetwork", "HighwayRampsNetwork" + "MiniCityNetwork", "HighwayRampsNetwork", "I210SubNetwork" ] diff --git a/flow/networks/i210_subnetwork.py b/flow/networks/i210_subnetwork.py new file mode 100644 index 000000000..d8e05efb5 --- /dev/null +++ b/flow/networks/i210_subnetwork.py @@ -0,0 +1,141 @@ +"""Contains the I-210 sub-network class.""" + +from flow.networks.base import Network + +EDGES_DISTRIBUTION = [ + # Main highway + "119257914", + "119257908#0", + "119257908#1-AddedOnRampEdge", + "119257908#1", + "119257908#1-AddedOffRampEdge", + "119257908#2", + "119257908#3", + + # On-ramp + "27414345", + "27414342#0", + "27414342#1-AddedOnRampEdge", + + # Off-ramp + "173381935", +] + + +class I210SubNetwork(Network): + """A network used to simulate the I-210 sub-network. + + Usage + ----- + >>> from flow.core.params import NetParams + >>> from flow.core.params import VehicleParams + >>> from flow.core.params import InitialConfig + >>> from flow.networks import I210SubNetwork + >>> + >>> network = I210SubNetwork( + >>> name='I-210_subnetwork', + >>> vehicles=VehicleParams(), + >>> net_params=NetParams() + >>> ) + """ + + def specify_routes(self, net_params): + """See parent class. + + Routes for vehicles moving through the bay bridge from Oakland to San + Francisco. + """ + rts = { + # Main highway + "119257914": [ + (["119257914", "119257908#0", "119257908#1-AddedOnRampEdge", + "119257908#1", "119257908#1-AddedOffRampEdge", "119257908#2", + "119257908#3"], + 1), # HOV: 1509 (on ramp: 57), Non HOV: 6869 (onramp: 16) + # (["119257914", "119257908#0", "119257908#1-AddedOnRampEdge", + # "119257908#1", "119257908#1-AddedOffRampEdge", "173381935"], + # 17 / 8378) + ], + # "119257908#0": [ + # (["119257908#0", "119257908#1-AddedOnRampEdge", "119257908#1", + # "119257908#1-AddedOffRampEdge", "119257908#2", + # "119257908#3"], + # 1.0), + # # (["119257908#0", "119257908#1-AddedOnRampEdge", "119257908#1", + # # "119257908#1-AddedOffRampEdge", "173381935"], + # # 0.5), + # ], + # "119257908#1-AddedOnRampEdge": [ + # (["119257908#1-AddedOnRampEdge", "119257908#1", + # "119257908#1-AddedOffRampEdge", "119257908#2", + # "119257908#3"], + # 1.0), + # # (["119257908#1-AddedOnRampEdge", "119257908#1", + # # "119257908#1-AddedOffRampEdge", "173381935"], + # # 0.5), + # ], + # "119257908#1": [ + # (["119257908#1", "119257908#1-AddedOffRampEdge", "119257908#2", + # "119257908#3"], + # 1.0), + # # (["119257908#1", "119257908#1-AddedOffRampEdge", "173381935"], + # # 0.5), + # ], + # "119257908#1-AddedOffRampEdge": [ + # (["119257908#1-AddedOffRampEdge", "119257908#2", + # "119257908#3"], + # 1.0), + # # (["119257908#1-AddedOffRampEdge", "173381935"], + # # 0.5), + # ], + # "119257908#2": [ + # (["119257908#2", "119257908#3"], 1), + # ], + # "119257908#3": [ + # (["119257908#3"], 1), + # ], + # + # # On-ramp + # "27414345": [ + # (["27414345", "27414342#1-AddedOnRampEdge", + # "27414342#1", + # "119257908#1-AddedOnRampEdge", "119257908#1", + # "119257908#1-AddedOffRampEdge", "119257908#2", + # "119257908#3"], + # 1 - 9 / 321), + # (["27414345", "27414342#1-AddedOnRampEdge", + # "27414342#1", + # "119257908#1-AddedOnRampEdge", "119257908#1", + # "119257908#1-AddedOffRampEdge", "173381935"], + # 9 / 321), + # ], + # "27414342#0": [ + # (["27414342#0", "27414342#1-AddedOnRampEdge", + # "27414342#1", + # "119257908#1-AddedOnRampEdge", "119257908#1", + # "119257908#1-AddedOffRampEdge", "119257908#2", + # "119257908#3"], + # 1 - 20 / 421), + # (["27414342#0", "27414342#1-AddedOnRampEdge", + # "27414342#1", + # "119257908#1-AddedOnRampEdge", "119257908#1", + # "119257908#1-AddedOffRampEdge", "173381935"], + # 20 / 421), + # ], + # "27414342#1-AddedOnRampEdge": [ + # (["27414342#1-AddedOnRampEdge", "27414342#1", "119257908#1-AddedOnRampEdge", + # "119257908#1", "119257908#1-AddedOffRampEdge", "119257908#2", + # "119257908#3"], + # 0.5), + # (["27414342#1-AddedOnRampEdge", "27414342#1", "119257908#1-AddedOnRampEdge", + # "119257908#1", "119257908#1-AddedOffRampEdge", "173381935"], + # 0.5), + # ], + # + # # Off-ramp + # "173381935": [ + # (["173381935"], 1), + # ], + } + + return rts diff --git a/flow/utils/aimsun/Aimsun_Flow.ang b/flow/utils/aimsun/Aimsun_Flow.ang deleted file mode 100644 index 17119f4e95771d1699dd7052ac7eddbc651a1fab..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 159038 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
jyGTLlLd3=+ZLtI_n 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

=UIRENP@w7hYiPdEauHXgy`&7hGv$G1did|4M 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

(9CGdf=!(P97LQPW@H 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 N&1qb*{{;X5|NjZQ5%d56 diff --git a/flow/visualize/time_space_diagram.py b/flow/visualize/time_space_diagram.py index e712ed84b..d8dad01e9 100644 --- a/flow/visualize/time_space_diagram.py +++ b/flow/visualize/time_space_diagram.py @@ -17,19 +17,22 @@ python time_space_diagram.py .csv .json """ from flow.utils.rllib import get_flow_params -from flow.networks import RingNetwork, FigureEightNetwork, MergeNetwork +from flow.networks import RingNetwork, FigureEightNetwork, MergeNetwork, I210SubNetwork + +import argparse import csv from matplotlib import pyplot as plt from matplotlib.collections import LineCollection import matplotlib.colors as colors import numpy as np -import argparse + # networks that can be plotted by this method ACCEPTABLE_NETWORKS = [ RingNetwork, FigureEightNetwork, MergeNetwork, + I210SubNetwork ] @@ -52,7 +55,7 @@ def import_data_from_emission(fp): * "vel": speed at every sample """ # initialize all output variables - veh_id, t, edge, rel_pos, vel = [], [], [], [], [] + veh_id, t, edge, rel_pos, vel, lane = [], [], [], [], [], [] # import relevant data from emission file for record in csv.DictReader(open(fp)): @@ -61,15 +64,17 @@ def import_data_from_emission(fp): edge.append(record['edge_id']) rel_pos.append(record['relative_position']) vel.append(record['speed']) + lane.append(record['lane_number']) # we now want to separate data by vehicle ID - ret = {key: {'time': [], 'edge': [], 'pos': [], 'vel': []} + ret = {key: {'time': [], 'edge': [], 'pos': [], 'vel': [], 'lane': []} for key in np.unique(veh_id)} for i in range(len(veh_id)): ret[veh_id[i]]['time'].append(float(t[i])) ret[veh_id[i]]['edge'].append(edge[i]) ret[veh_id[i]]['pos'].append(float(rel_pos[i])) ret[veh_id[i]]['vel'].append(float(vel[i])) + ret[veh_id[i]]['lane'].append(float(lane[i])) return ret @@ -123,7 +128,8 @@ def get_time_space_data(data, params): switcher = { RingNetwork: _ring_road, MergeNetwork: _merge, - FigureEightNetwork: _figure_eight + FigureEightNetwork: _figure_eight, + I210SubNetwork: _i210_subnetwork } # Collect a list of all the unique times. @@ -136,7 +142,7 @@ def get_time_space_data(data, params): func = switcher[params['network']] # Execute the function - pos, speed = func(data, params, all_time) + pos, speed, all_time = func(data, params, all_time) return pos, speed, all_time @@ -212,7 +218,7 @@ def _merge(data, params, all_time): pos[ind, i] = abs_pos speed[ind, i] = spd - return pos, speed + return pos, speed, all_time def _ring_road(data, params, all_time): @@ -278,7 +284,89 @@ def _ring_road(data, params, all_time): pos[ind, i] = abs_pos speed[ind, i] = spd - return pos, speed + return pos, speed, all_time + + +def _i210_subnetwork(data, params, all_time): + r"""Generate position and speed data for the i210 subnetwork. + + We only look at the second to last lane of edge 119257908#1-AddedOnRampEdge + + Parameters + ---------- + data : dict of dict + Key = "veh_id": name of the vehicle \n Elements: + + * "time": time step at every sample + * "edge": edge ID at every sample + * "pos": relative position at every sample + * "vel": speed at every sample + params : dict + flow-specific parameters + all_time : array_like + a (n_steps,) vector representing the unique time steps in the + simulation + + Returns + ------- + as_array + n_steps x n_veh matrix specifying the absolute position of every + vehicle at every time step. Set to zero if the vehicle is not present + in the network at that time step. + as_array + n_steps x n_veh matrix specifying the speed of every vehicle at every + time step. Set to zero if the vehicle is not present in the network at + that time step. + """ + # import network data from flow params + # + # edge_starts = {"119257908#0": 0, + # "119257908#1-AddedOnRampEdge": 686.98} + desired_lane = 1 + edge_starts = {"119257914": 0, + "119257908#0": 61.58, + "119257908#1-AddedOnRampEdge": 686.98 + 61.58} + # edge_starts = {"119257908#0": 0} + # edge_starts = {"119257908#1-AddedOnRampEdge": 0} + # desired_lane = 5 + + # compute the absolute position + for veh_id in data.keys(): + data[veh_id]['abs_pos'] = _get_abs_pos_1_edge(data[veh_id]['edge'], + data[veh_id]['pos'], + edge_starts) + + # create the output variables + # TODO(@ev) handle subsampling better than this + low_time = int(0 / params['sim'].sim_step) + high_time = int(1600 / params['sim'].sim_step) + all_time = all_time[low_time:high_time] + + # track only vehicles that were around during this time period + observed_row_list = [] + pos = np.zeros((all_time.shape[0], len(data.keys()))) + speed = np.zeros((all_time.shape[0], len(data.keys()))) + for i, veh_id in enumerate(sorted(data.keys())): + for spd, abs_pos, ti, edge, lane in zip(data[veh_id]['vel'], + data[veh_id]['abs_pos'], + data[veh_id]['time'], + data[veh_id]['edge'], + data[veh_id]['lane']): + # avoid vehicles not on the relevant edges. Also only check the second to + # last lane + if edge not in edge_starts.keys() or ti not in all_time or lane != desired_lane: + continue + else: + if i not in observed_row_list: + observed_row_list.append(i) + ind = np.where(ti == all_time)[0] + pos[ind, i] = abs_pos + speed[ind, i] = spd + + pos = pos[:, observed_row_list] + speed = speed[:, observed_row_list] + + return pos, speed, all_time def _figure_eight(data, params, all_time): @@ -328,14 +416,14 @@ def _figure_eight(data, params, all_time): 'top': intersection / 2 + junction + inner, 'upper_ring': intersection + junction + 2 * inner, 'right': intersection + 3 * ring_edgelen + junction + 3 * inner, - 'left': 1.5*intersection + 3*ring_edgelen + 2*junction + 3*inner, - 'lower_ring': 2*intersection + 3*ring_edgelen + 2*junction + 4*inner, + 'left': 1.5 * intersection + 3 * ring_edgelen + 2 * junction + 3 * inner, + 'lower_ring': 2 * intersection + 3 * ring_edgelen + 2 * junction + 4 * inner, ':bottom_0': 0, ':center_1': intersection / 2 + inner, ':top_0': intersection + junction + inner, ':right_0': intersection + 3 * ring_edgelen + junction + 2 * inner, - ':center_0': 1.5*intersection + 3*ring_edgelen + junction + 3*inner, - ':left_0': 2 * intersection + 3*ring_edgelen + 2*junction + 3*inner, + ':center_0': 1.5 * intersection + 3 * ring_edgelen + junction + 3 * inner, + ':left_0': 2 * intersection + 3 * ring_edgelen + 2 * junction + 3 * inner, # for aimsun 'bottom_to_top': intersection / 2 + inner, 'right_to_left': junction + 3 * inner, @@ -358,7 +446,7 @@ def _figure_eight(data, params, all_time): speed[ind, i] = spd # reorganize data for space-time plot - figure_eight_len = 6*ring_edgelen + 2*intersection + 2*junction + 10*inner + figure_eight_len = 6 * ring_edgelen + 2 * intersection + 2 * junction + 10 * inner intersection_loc = [edgestarts[':center_1'] + intersection / 2, edgestarts[':center_0'] + intersection / 2] pos[pos < intersection_loc[0]] += figure_eight_len @@ -367,7 +455,7 @@ def _figure_eight(data, params, all_time): pos[pos > intersection_loc[1]] = \ - pos[pos > intersection_loc[1]] + figure_eight_len + intersection_loc[0] - return pos, speed + return pos, speed, all_time def _get_abs_pos(edge, rel_pos, edgestarts): @@ -395,6 +483,34 @@ def _get_abs_pos(edge, rel_pos, edgestarts): return ret +def _get_abs_pos_1_edge(edges, rel_pos, edge_starts): + """Compute the absolute positions from a subset of edges. + + This is the variable we will ultimately use to plot individual vehicles. + + Parameters + ---------- + edges : list of str + list of edges at every time step + rel_pos : list of float + list of relative positions at every time step + edge_starts : dict + the absolute starting position of every edge + + Returns + ------- + list of float + the absolute positive for every sample + """ + ret = [] + for edge_i, pos_i in zip(edges, rel_pos): + if edge_i in edge_starts.keys(): + ret.append(pos_i + edge_starts[edge_i]) + else: + ret.append(-1) + return ret + + if __name__ == '__main__': # create the parser parser = argparse.ArgumentParser( @@ -416,6 +532,8 @@ def _get_abs_pos(edge, rel_pos, edgestarts): help='rate at which steps are plotted.') parser.add_argument('--max_speed', type=int, default=8, help='The maximum speed in the color range.') + parser.add_argument('--min_speed', type=int, default=0, + help='The minimum speed in the color range.') parser.add_argument('--start', type=float, default=0, help='initial time (in sec) in the plot.') parser.add_argument('--stop', type=float, default=float('inf'), @@ -424,7 +542,11 @@ def _get_abs_pos(edge, rel_pos, edgestarts): args = parser.parse_args() # flow_params is imported as a dictionary - flow_params = get_flow_params(args.flow_params) + if '.json' in args.flow_params: + flow_params = get_flow_params(args.flow_params) + else: + module = __import__("examples.exp_configs.non_rl", fromlist=[args.flow_params]) + flow_params = getattr(module, args.flow_params).flow_params # import data from the emission.csv file emission_data = import_data_from_emission(args.emission_path) @@ -443,7 +565,7 @@ def _get_abs_pos(edge, rel_pos, edgestarts): # perform plotting operation fig = plt.figure(figsize=(16, 9)) ax = plt.axes() - norm = plt.Normalize(0, args.max_speed) + norm = plt.Normalize(args.min_speed, args.max_speed) cols = [] xmin = max(time[0], args.start) @@ -458,14 +580,20 @@ def _get_abs_pos(edge, rel_pos, edgestarts): for indx_car in range(pos.shape[1]): unique_car_pos = pos[:, indx_car] - # discontinuity from wraparound - disc = np.where(np.abs(np.diff(unique_car_pos)) >= 10)[0] + 1 - unique_car_time = np.insert(time, disc, np.nan) - unique_car_pos = np.insert(unique_car_pos, disc, np.nan) - unique_car_speed = np.insert(speed[:, indx_car], disc, np.nan) - - points = np.array( - [unique_car_time, unique_car_pos]).T.reshape(-1, 1, 2) + if flow_params['network'] == I210SubNetwork: + indices = np.where(pos[:, indx_car] != 0)[0] + unique_car_speed = speed[indices, indx_car] + points = np.array([time[indices], pos[indices, indx_car]]).T.reshape(-1, 1, 2) + else: + + # discontinuity from wraparound + disc = np.where(np.abs(np.diff(unique_car_pos)) >= 10)[0] + 1 + unique_car_time = np.insert(time, disc, np.nan) + unique_car_pos = np.insert(unique_car_pos, disc, np.nan) + unique_car_speed = np.insert(speed[:, indx_car], disc, np.nan) + # + points = np.array( + [unique_car_time, unique_car_pos]).T.reshape(-1, 1, 2) segments = np.concatenate([points[:-1], points[1:]], axis=1) lc = LineCollection(segments, cmap=my_cmap, norm=norm) @@ -480,7 +608,7 @@ def _get_abs_pos(edge, rel_pos, edgestarts): for col in cols: line = ax.add_collection(col) - cbar = plt.colorbar(line, ax=ax) + cbar = plt.colorbar(line, ax=ax, norm=norm) cbar.set_label('Velocity (m/s)', fontsize=20) cbar.ax.tick_params(labelsize=18) diff --git a/requirements.txt b/requirements.txt index 430ef29c6..546cb4e26 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,7 +11,7 @@ python-dateutil==2.7.3 cached_property cloudpickle==1.2.0 pyglet==1.3.2 -matplotlib==3.0.0 +matplotlib==3.1.0 imutils==0.5.1 numpydoc ray==0.7.3 diff --git a/scripts/ray_autoscale.yaml b/scripts/ray_autoscale.yaml index e6dab39bd..5bf2a9c4a 100644 --- a/scripts/ray_autoscale.yaml +++ b/scripts/ray_autoscale.yaml @@ -70,7 +70,9 @@ setup_commands: - cd flow && git fetch && git checkout origin/master head_setup_commands: - - pip install boto3==1.4.8 # 1.4.8 adds InstanceMarketOptions + - pip install boto3==1.10.45 # 1.4.8 adds InstanceMarketOptions + - pip install awscli==1.16.309 + - pip install pytz # Custom commands that will be run on worker nodes after common setup. worker_setup_commands: [] diff --git a/tests/fast_tests/test_controllers.py b/tests/fast_tests/test_controllers.py index 1e5c6057d..76146dbe6 100644 --- a/tests/fast_tests/test_controllers.py +++ b/tests/fast_tests/test_controllers.py @@ -9,7 +9,7 @@ from flow.controllers.car_following_models import IDMController, \ OVMController, BCMController, LinearOVM, CFMController, LACController, \ GippsController -from flow.controllers import FollowerStopper, PISaturation +from flow.controllers import FollowerStopper, PISaturation, NonLocalFollowerStopper from tests.setup_scripts import ring_road_exp_setup import os import numpy as np @@ -533,6 +533,78 @@ def test_find_intersection_dist(self): ids[0]).get_action(self.env)) +class TestNonLocalFollowerStopper(unittest.TestCase): + + """Makes sure that the nonlocal follower stopper runs.""" + + def setUp(self): + # add a few vehicles to the network using the requested model + # also make sure that the input params are what is expected + contr_params = {"v_des": 7.5} + + vehicles = VehicleParams() + vehicles.add( + veh_id="test_0", + acceleration_controller=(NonLocalFollowerStopper, contr_params), + routing_controller=(ContinuousRouter, {}), + car_following_params=SumoCarFollowingParams( + accel=20, decel=5), + num_vehicles=5) + + # create the environment and network classes for a ring road + self.env, _, _ = ring_road_exp_setup(vehicles=vehicles) + + def tearDown(self): + # terminate the traci instance + self.env.terminate() + + # free data used by the class + self.env = None + + def test_get_action(self): + self.env.reset() + ids = self.env.k.vehicle.get_ids() + + test_headways = [5, 10, 15, 20, 25] + test_speeds = [5, 7.5, 7.5, 8, 7] + for i, veh_id in enumerate(ids): + self.env.k.vehicle.set_headway(veh_id, test_headways[i]) + self.env.k.vehicle.test_set_speed(veh_id, test_speeds[i]) + + requested_accel = [ + self.env.k.vehicle.get_acc_controller(veh_id).get_action(self.env) + for veh_id in ids + ] + + expected_accel = [-3.33333333333333, -5.0, -5.0, -10.0, 0.0] + + np.testing.assert_array_almost_equal(requested_accel, expected_accel) + + def test_find_intersection_dist(self): + self.env.reset() + ids = self.env.k.vehicle.get_ids() + + test_edges = ["", "center"] + for i, veh_id in enumerate(ids): + if i < 2: + self.env.k.vehicle.test_set_edge(veh_id, test_edges[i]) + + requested = [ + self.env.k.vehicle.get_acc_controller( + veh_id).find_intersection_dist(self.env) + for veh_id in ids + ] + + expected = [-10, 0, 23.1, 34.7, 46.3] + + np.testing.assert_array_almost_equal(requested, expected) + + # we also check that the accel value is None when this value is + # negative + self.assertIsNone(self.env.k.vehicle.get_acc_controller( + ids[0]).get_action(self.env)) + + class TestPISaturation(unittest.TestCase): """ diff --git a/tests/fast_tests/test_examples.py b/tests/fast_tests/test_examples.py index 89e4ea97b..5c36d8760 100644 --- a/tests/fast_tests/test_examples.py +++ b/tests/fast_tests/test_examples.py @@ -1,3 +1,4 @@ +from copy import deepcopy import os import unittest import random @@ -15,6 +16,7 @@ from examples.exp_configs.rl.singleagent.singleagent_bottleneck import flow_params as singleagent_bottleneck from examples.exp_configs.rl.multiagent.adversarial_figure_eight import flow_params as adversarial_figure_eight +from examples.exp_configs.rl.multiagent.multiagent_i210 import flow_params as multiagent_i210 from examples.exp_configs.rl.multiagent.multiagent_figure_eight import flow_params as multiagent_figure_eight from examples.exp_configs.rl.multiagent.multiagent_merge import flow_params as multiagent_merge from examples.exp_configs.rl.multiagent.lord_of_the_rings import \ @@ -37,6 +39,7 @@ from examples.exp_configs.non_rl.merge import flow_params as non_rl_merge from examples.exp_configs.non_rl.minicity import flow_params as non_rl_minicity from examples.exp_configs.non_rl.ring import flow_params as non_rl_ring +from examples.exp_configs.non_rl.i210_subnetwork import flow_params as non_rl_i210 os.environ['TEST_FLAG'] = 'True' os.environ['KMP_DUPLICATE_LIB_OK'] = 'True' @@ -101,6 +104,10 @@ def test_minicity(self): """Verify that examples/exp_configs/non_rl/minicity.py is working.""" self.run_simulation(non_rl_minicity) + def test_i210(self): + """Verify that examples/exp_configs/non_rl/i210_subnetwork.py is working.""" + self.run_simulation(non_rl_i210) + @staticmethod def run_simulation(flow_params): # make the horizon small and set render to False @@ -247,6 +254,57 @@ def test_multi_highway(self): } self.run_exp(multiagent_highway, **kwargs) + def test_multiagent_i210(self): + from examples.exp_configs.rl.multiagent.multiagent_i210 import POLICIES_TO_TRAIN as mi210pr + from examples.exp_configs.rl.multiagent.multiagent_i210 import policy_mapping_fn as mi210mf + + from ray.rllib.agents.ppo.ppo_policy import PPOTFPolicy + from ray.tune.registry import register_env + from flow.utils.registry import make_create_env + # test observation space 1 + flow_params = deepcopy(multiagent_i210) + flow_params['env'].additional_params['lead_obs'] = True + create_env, env_name = make_create_env(params=flow_params, version=0) + + # register as rllib env + register_env(env_name, create_env) + + # multiagent configuration + test_env = create_env() + obs_space = test_env.observation_space + act_space = test_env.action_space + + POLICY_GRAPHS = {'av': (PPOTFPolicy, obs_space, act_space, {})} + + kwargs = { + "policy_graphs": POLICY_GRAPHS, + "policies_to_train": mi210pr, + "policy_mapping_fn": mi210mf + } + self.run_exp(flow_params, **kwargs) + + # test observation space 2 + flow_params = deepcopy(multiagent_i210) + flow_params['env'].additional_params['lead_obs'] = False + create_env, env_name = make_create_env(params=flow_params, version=0) + + # register as rllib env + register_env(env_name, create_env) + + # multiagent configuration + test_env = create_env() + obs_space = test_env.observation_space + act_space = test_env.action_space + + POLICY_GRAPHS = {'av': (PPOTFPolicy, obs_space, act_space, {})} + + kwargs = { + "policy_graphs": POLICY_GRAPHS, + "policies_to_train": mi210pr, + "policy_mapping_fn": mi210mf + } + self.run_exp(flow_params, **kwargs) + @staticmethod def run_exp(flow_params, **kwargs): alg_run, env_name, config = setup_rllib_exps(flow_params, 1, 1, **kwargs) diff --git a/tests/fast_tests/test_files/i210_emission.csv b/tests/fast_tests/test_files/i210_emission.csv new file mode 100644 index 000000000..d43c115a4 --- /dev/null +++ b/tests/fast_tests/test_files/i210_emission.csv @@ -0,0 +1,27 @@ +x,time,edge_id,eclass,type,PMx,speed,angle,CO,CO2,electricity,noise,lane_number,NOx,relative_position,route,y,id,fuel,HC,waiting +485.04,0.8,119257914,HBEFA3/PC_G_EU4,human,0.05,23.0,119.74,3.32,3793.12,0.0,70.29,1,1.17,5.1,route119257914_0,1068.18,flow_00.0,1.63,0.11,0.0 +500.91,1.6,119257914,HBEFA3/PC_G_EU4,human,0.0,22.84,119.74,0.0,0.0,0.0,69.9,1,0.0,23.37,route119257914_0,1059.12,flow_00.0,0.0,0.0,0.0 +517.1,2.4,119257914,HBEFA3/PC_G_EU4,human,0.15,23.31,119.74,78.83,7435.5,0.0,71.61,1,2.88,42.02,route119257914_0,1049.87,flow_00.0,3.2,0.54,0.0 +533.76,3.2,119257914,HBEFA3/PC_G_EU4,human,0.2,23.98,119.74,117.59,9250.57,0.0,72.56,1,3.72,61.21,route119257914_0,1040.35,flow_00.0,3.98,0.76,0.0 +550.83,4.0,119257908#0,HBEFA3/PC_G_EU4,human,0.12,24.25,118.82,55.17,6186.6,0.0,71.56,1,2.27,18.87,route119257914_0,1030.94,flow_00.0,2.66,0.4,0.0 +569.33,4.8,119257908#0,HBEFA3/PC_G_EU4,human,0.55,26.33,118.82,398.05,22491.67,0.0,79.56,1,9.92,39.93,route119257914_0,1020.77,flow_00.0,9.67,2.37,0.0 +488.22,0.8,119257914,HBEFA3/PC_G_EU4,human,0.05,23.0,119.74,3.32,3793.12,0.0,70.29,3,1.17,5.1,route119257914_0,1073.74,flow_00.1,1.63,0.11,0.0 +504.38,1.6,119257914,HBEFA3/PC_G_EU4,human,0.11,23.27,119.74,47.84,5933.43,0.0,71.06,3,2.18,23.72,route119257914_0,1064.51,flow_00.1,2.55,0.36,0.0 +521.17,2.4,119257914,HBEFA3/PC_G_EU4,human,0.25,24.18,119.74,158.54,11215.9,0.0,73.46,3,4.65,43.06,route119257914_0,1054.91,flow_00.1,4.82,1.0,0.0 +538.52,3.2,119257908#0,HBEFA3/PC_G_EU4,human,0.25,25.01,119.48,158.26,11085.45,0.0,73.63,3,4.57,1.33,route119257914_0,1045.03,flow_00.1,4.77,1.0,0.0 +556.27,4.0,119257908#0,HBEFA3/PC_G_EU4,human,0.16,25.39,118.82,86.12,7519.43,0.0,72.45,3,2.88,21.65,route119257914_0,1035.26,flow_00.1,3.23,0.58,0.0 +575.34,4.8,119257908#0,HBEFA3/PC_G_EU4,human,0.53,27.27,118.82,383.84,21586.2,0.0,79.03,3,9.47,43.46,route119257914_0,1024.77,flow_00.1,9.28,2.29,0.0 +486.63,2.4,119257914,HBEFA3/PC_G_EU4,human,0.05,23.0,119.74,3.32,3793.12,0.0,70.29,2,1.17,5.1,route119257914_0,1070.96,flow_00.2,1.63,0.11,0.0 +503.03,3.2,119257914,HBEFA3/PC_G_EU4,human,0.18,23.61,119.74,103.29,8596.1,0.0,72.17,2,3.42,23.98,route119257914_0,1061.59,flow_00.2,3.7,0.68,0.0 +519.7,4.0,119257914,HBEFA3/PC_G_EU4,human,0.14,24.0,119.74,73.27,7095.73,0.0,71.76,2,2.71,43.18,route119257914_0,1052.07,flow_00.2,3.05,0.51,0.0 +489.8,2.4,119257914,HBEFA3/PC_G_EU4,human,0.05,23.0,119.74,3.32,3793.12,0.0,70.29,4,1.17,5.1,route119257914_0,1076.52,flow_00.3,1.63,0.11,0.0 +505.86,3.2,119257914,HBEFA3/PC_G_EU4,human,0.08,23.12,119.74,23.4,4758.73,0.0,70.63,4,1.62,23.6,route119257914_0,1067.34,flow_00.3,2.05,0.22,0.0 +522.24,4.0,119257914,HBEFA3/PC_G_EU4,human,0.15,23.58,119.74,78.96,7417.2,0.0,71.71,4,2.87,42.46,route119257914_0,1057.98,flow_00.3,3.19,0.54,0.0 +488.22,2.4,119257914,HBEFA3/PC_G_EU4,human,0.05,23.0,119.74,3.32,3793.12,0.0,70.29,3,1.17,5.1,route119257914_0,1073.74,flow_00.4,1.63,0.11,0.0 +504.4,3.2,119257914,HBEFA3/PC_G_EU4,human,0.11,23.3,119.74,51.58,6113.11,0.0,71.13,3,2.26,23.74,route119257914_0,1064.5,flow_00.4,2.63,0.39,0.0 +520.58,4.0,119257914,HBEFA3/PC_G_EU4,human,0.06,23.31,119.74,6.86,3940.44,0.0,70.48,3,1.23,42.38,route119257914_0,1055.25,flow_00.4,1.69,0.13,0.0 +485.04,2.4,119257914,HBEFA3/PC_G_EU4,human,0.05,23.0,119.74,3.32,3793.12,0.0,70.29,1,1.17,5.1,route119257914_0,1068.18,flow_00.5,1.63,0.11,0.0 +500.55,3.2,119257914,HBEFA3/PC_G_EU4,human,0.0,22.33,119.74,0.0,0.0,0.0,68.86,1,0.0,22.97,route119257914_0,1059.32,flow_00.5,0.0,0.0,0.0 +515.98,4.0,119257914,HBEFA3/PC_G_EU4,human,0.0,22.21,119.74,0.0,0.0,0.0,69.59,1,0.0,40.73,route119257914_0,1050.51,flow_00.5,0.0,0.0,0.0 +488.22,4.0,119257914,HBEFA3/PC_G_EU4,human,0.05,23.0,119.74,3.32,3793.12,0.0,70.29,3,1.17,5.1,route119257914_0,1073.74,flow_00.6,1.63,0.11,0.0 +485.04,4.0,119257914,HBEFA3/PC_G_EU4,human,0.05,23.0,119.74,3.32,3793.12,0.0,70.29,1,1.17,5.1,route119257914_0,1068.18,flow_00.7,1.63,0.11,0.0 diff --git a/tests/fast_tests/test_visualizers.py b/tests/fast_tests/test_visualizers.py index 3ed86c596..7af413909 100644 --- a/tests/fast_tests/test_visualizers.py +++ b/tests/fast_tests/test_visualizers.py @@ -97,67 +97,81 @@ def test_time_space_diagram_figure_eight(self): 'time': [1.0, 2.0, 3.0, 4.0], 'vel': [0.0, 0.99, 1.98, 2.95], 'edge': ['upper_ring', 'upper_ring', 'upper_ring', - 'upper_ring']}, + 'upper_ring'], + 'lane': [0.0, 0.0, 0.0, 0.0]}, 'idm_4': {'pos': [56.02, 57.01, 58.99, 61.93], 'time': [1.0, 2.0, 3.0, 4.0], 'vel': [0.0, 0.99, 1.98, 2.95], 'edge': ['upper_ring', 'upper_ring', 'upper_ring', - 'upper_ring']}, + 'upper_ring'], + 'lane': [0.0, 0.0, 0.0, 0.0]}, 'idm_5': {'pos': [84.79, 85.78, 87.76, 90.7], 'time': [1.0, 2.0, 3.0, 4.0], 'vel': [0.0, 0.99, 1.98, 2.95], 'edge': ['upper_ring', 'upper_ring', 'upper_ring', - 'upper_ring']}, + 'upper_ring'], + 'lane': [0.0, 0.0, 0.0, 0.0]}, 'idm_2': {'pos': [28.77, 29.76, 1.63, 4.58], 'time': [1.0, 2.0, 3.0, 4.0], 'vel': [0.0, 0.99, 1.97, 2.95], - 'edge': ['top', 'top', 'upper_ring', 'upper_ring']}, + 'edge': ['top', 'top', 'upper_ring', 'upper_ring'], + 'lane': [0.0, 0.0, 0.0, 0.0]}, 'idm_13': {'pos': [106.79, 107.79, 109.77, 112.74], 'time': [1.0, 2.0, 3.0, 4.0], 'vel': [0.0, 0.99, 1.98, 2.96], 'edge': ['lower_ring', 'lower_ring', 'lower_ring', - 'lower_ring']}, + 'lower_ring'], + 'lane': [0.0, 0.0, 0.0, 0.0]}, 'idm_9': {'pos': [22.01, 23.0, 24.97, 27.92], 'time': [1.0, 2.0, 3.0, 4.0], 'vel': [0.0, 0.99, 1.97, 2.95], - 'edge': ['left', 'left', 'left', 'left']}, + 'edge': ['left', 'left', 'left', 'left'], + 'lane': [0.0, 0.0, 0.0, 0.0]}, 'idm_6': {'pos': [113.56, 114.55, 116.52, 119.47], 'time': [1.0, 2.0, 3.0, 4.0], 'vel': [0.0, 0.99, 1.97, 2.95], 'edge': ['upper_ring', 'upper_ring', 'upper_ring', - 'upper_ring']}, + 'upper_ring'], + 'lane': [0.0, 0.0, 0.0, 0.0]}, 'idm_8': {'pos': [29.44, 0.28, 2.03, 4.78], 'time': [1.0, 2.0, 3.0, 4.0], 'vel': [0.0, 0.84, 1.76, 2.75], 'edge': ['right', ':center_0', ':center_0', - ':center_0']}, + ':center_0'], + 'lane': [0.0, 0.0, 0.0, 0.0]}, 'idm_12': {'pos': [78.03, 79.02, 80.99, 83.94], 'time': [1.0, 2.0, 3.0, 4.0], 'vel': [0.0, 0.99, 1.98, 2.95], 'edge': ['lower_ring', 'lower_ring', 'lower_ring', - 'lower_ring']}, + 'lower_ring'], + 'lane': [0.0, 0.0, 0.0, 0.0]}, 'idm_10': {'pos': [20.49, 21.48, 23.46, 26.41], 'time': [1.0, 2.0, 3.0, 4.0], 'vel': [0.0, 0.99, 1.98, 2.95], 'edge': ['lower_ring', 'lower_ring', 'lower_ring', - 'lower_ring']}, + 'lower_ring'], + 'lane': [0.0, 0.0, 0.0, 0.0]}, 'idm_11': {'pos': [49.26, 50.25, 52.23, 55.17], 'time': [1.0, 2.0, 3.0, 4.0], 'vel': [0.0, 0.99, 1.98, 2.95], 'edge': ['lower_ring', 'lower_ring', 'lower_ring', - 'lower_ring']}, + 'lower_ring'], + 'lane': [0.0, 0.0, 0.0, 0.0]}, 'idm_1': {'pos': [0.0, 0.99, 2.97, 5.91], 'time': [1.0, 2.0, 3.0, 4.0], 'vel': [0.0, 0.99, 1.98, 2.95], - 'edge': ['top', 'top', 'top', 'top']}, + 'edge': ['top', 'top', 'top', 'top'], + 'lane': [0.0, 0.0, 0.0, 0.0]}, 'idm_7': {'pos': [0.67, 1.66, 3.64, 6.58], 'time': [1.0, 2.0, 3.0, 4.0], 'vel': [0.0, 0.99, 1.97, 2.94], - 'edge': ['right', 'right', 'right', 'right']}, + 'edge': ['right', 'right', 'right', 'right'], + 'lane': [0.0, 0.0, 0.0, 0.0]}, 'idm_0': {'pos': [0.0, 1.0, 2.98, 5.95], 'time': [1.0, 2.0, 3.0, 4.0], 'vel': [0.0, 1.0, 1.99, 2.97], - 'edge': ['bottom', 'bottom', 'bottom', 'bottom']} + 'edge': ['bottom', 'bottom', 'bottom', 'bottom'], + 'lane': [0.0, 0.0, 0.0, 0.0]} } dir_path = os.path.dirname(os.path.realpath(__file__)) actual_emission_data = tsd.import_data_from_emission( @@ -221,6 +235,35 @@ def test_time_space_diagram_merge(self): np.testing.assert_array_almost_equal(pos, expected_pos) np.testing.assert_array_almost_equal(speed, expected_speed) + def test_time_space_diagram_I210(self): + dir_path = os.path.dirname(os.path.realpath(__file__)) + emission_data = tsd.import_data_from_emission( + os.path.join(dir_path, 'test_files/i210_emission.csv')) + + module = __import__("examples.exp_configs.non_rl", fromlist=["i210_subnetwork"]) + flow_params = getattr(module, "i210_subnetwork").flow_params + pos, speed, _ = tsd.get_time_space_data(emission_data, flow_params) + + expected_pos = np.array( + [[5.1, 0., 0.], + [23.37, 0., 0.], + [42.02, 5.1, 0.], + [61.21, 22.97, 0.], + [80.45, 40.73, 5.1], + [101.51, 0., 0.]] + ) + expected_speed = np.array( + [[23., 0., 0.], + [22.84, 0., 0.], + [23.31, 23., 0.], + [23.98, 22.33, 0.], + [24.25, 22.21, 23.], + [26.33, 0., 0.]] + ) + + np.testing.assert_array_almost_equal(pos, expected_pos) + np.testing.assert_array_almost_equal(speed, expected_speed) + def test_time_space_diagram_ring_road(self): dir_path = os.path.dirname(os.path.realpath(__file__)) emission_data = tsd.import_data_from_emission( From 4e47f7aae7e3e94e75afdafd92a1ce8a749710ce Mon Sep 17 00:00:00 2001 From: Lucia Cipolina Kun Date: Tue, 17 Mar 2020 16:00:23 +0000 Subject: [PATCH 50/86] singleagent_traffic_light_grid (#871) Fix high value of traffic light --- .../rl/singleagent/singleagent_traffic_light_grid.py | 4 ++-- flow/envs/traffic_light_grid.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/exp_configs/rl/singleagent/singleagent_traffic_light_grid.py b/examples/exp_configs/rl/singleagent/singleagent_traffic_light_grid.py index 73fafc00b..085d26be9 100644 --- a/examples/exp_configs/rl/singleagent/singleagent_traffic_light_grid.py +++ b/examples/exp_configs/rl/singleagent/singleagent_traffic_light_grid.py @@ -9,12 +9,12 @@ # time horizon of a single rollout HORIZON = 200 # number of rollouts per training iteration -N_ROLLOUTS = 20 +N_ROLLOUTS = 30 # number of parallel workers N_CPUS = 2 # set to True if you would like to run the experiment with inflows of vehicles # from the edges, and False otherwise -USE_INFLOWS = False +USE_INFLOWS = True def gen_edges(col_num, row_num): diff --git a/flow/envs/traffic_light_grid.py b/flow/envs/traffic_light_grid.py index 4083ac3fe..53391a329 100644 --- a/flow/envs/traffic_light_grid.py +++ b/flow/envs/traffic_light_grid.py @@ -639,7 +639,7 @@ def observation_space(self): """ tl_box = Box( low=0., - high=1, + high=3, shape=(3 * 4 * self.num_observed * self.num_traffic_lights + 2 * len(self.k.network.get_edge_list()) + 3 * self.num_traffic_lights,), From f72760b1d383b4b1d5af04d4c4355b89dbde28a5 Mon Sep 17 00:00:00 2001 From: Eugene Vinitsky Date: Tue, 17 Mar 2020 11:01:03 -0700 Subject: [PATCH 51/86] Add energy reward from benni/joy (#864) * Add energy reward from benni/joy * Add tests --- flow/core/kernel/vehicle/aimsun.py | 4 ++++ flow/core/kernel/vehicle/base.py | 4 ++++ flow/core/kernel/vehicle/traci.py | 16 ++++++++++++++++ flow/core/rewards.py | 26 ++++++++++++++++++++++++++ flow/envs/base.py | 3 +++ flow/envs/multiagent/base.py | 3 +++ tests/fast_tests/test_rewards.py | 26 ++++++++++++++++++++++++++ 7 files changed, 82 insertions(+) diff --git a/flow/core/kernel/vehicle/aimsun.py b/flow/core/kernel/vehicle/aimsun.py index 9e791a10d..3320d1515 100644 --- a/flow/core/kernel/vehicle/aimsun.py +++ b/flow/core/kernel/vehicle/aimsun.py @@ -401,6 +401,10 @@ def add(self, veh_id, type_id, edge, pos, lane, speed): self.num_type[type_id] = 1 self.total_num_type[type_id] = 1 + def reset(self): + """See parent class.""" + pass + def remove(self, aimsun_id): """See parent class.""" veh_id = self._id_aimsun2flow[aimsun_id] diff --git a/flow/core/kernel/vehicle/base.py b/flow/core/kernel/vehicle/base.py index 59f59617e..d9fc773cd 100644 --- a/flow/core/kernel/vehicle/base.py +++ b/flow/core/kernel/vehicle/base.py @@ -100,6 +100,10 @@ def add(self, veh_id, type_id, edge, pos, lane, speed): """ raise NotImplementedError + def reset(self): + """Reset any additional state that needs to be reset.""" + raise NotImplementedError + def remove(self, veh_id): """Remove a vehicle. diff --git a/flow/core/kernel/vehicle/traci.py b/flow/core/kernel/vehicle/traci.py index 745b49650..657b89a94 100644 --- a/flow/core/kernel/vehicle/traci.py +++ b/flow/core/kernel/vehicle/traci.py @@ -82,6 +82,9 @@ def __init__(self, except AttributeError: self._force_color_update = False + # old speeds used to compute accelerations + self.previous_speeds = {} + def initialize(self, vehicles): """Initialize vehicle state information. @@ -127,8 +130,11 @@ def update(self, reset): specifies whether the simulator was reset in the last simulation step """ + # copy over the previous speeds + vehicle_obs = {} for veh_id in self.__ids: + self.previous_speeds[veh_id] = self.get_speed(veh_id) vehicle_obs[veh_id] = \ self.kernel_api.vehicle.getSubscriptionResults(veh_id) sim_obs = self.kernel_api.simulation.getSubscriptionResults() @@ -362,6 +368,10 @@ def _add_departed(self, veh_id, veh_type): return new_obs + def reset(self): + """See parent class.""" + self.previous_speeds = {} + def remove(self, veh_id): """See parent class.""" # remove from sumo @@ -513,6 +523,12 @@ def get_departed_ids(self): else: return 0 + def get_previous_speed(self, veh_id, error=-1001): + """See parent class.""" + if isinstance(veh_id, (list, np.ndarray)): + return [self.get_previous_speed(vehID, error) for vehID in veh_id] + return self.previous_speeds.get(veh_id, 0) + def get_speed(self, veh_id, error=-1001): """See parent class.""" if isinstance(veh_id, (list, np.ndarray)): diff --git a/flow/core/rewards.py b/flow/core/rewards.py index 58fc5330e..6de472af2 100755 --- a/flow/core/rewards.py +++ b/flow/core/rewards.py @@ -304,3 +304,29 @@ def punish_rl_lane_changes(env, penalty=1): total_lane_change_penalty -= penalty return total_lane_change_penalty + + +def energy_consumption(env, gain=.001): + """Calculate power consumption of a vehicle. + + Assumes vehicle is an average sized vehicle. + The power calculated here is the lower bound of the actual power consumed + by a vehicle. + """ + power = 0 + + M = 1200 # mass of average sized vehicle (kg) + g = 9.81 # gravitational acceleration (m/s^2) + Cr = 0.005 # rolling resistance coefficient + Ca = 0.3 # aerodynamic drag coefficient + rho = 1.225 # air density (kg/m^3) + A = 2.6 # vehicle cross sectional area (m^2) + for veh_id in env.k.vehicle.get_ids(): + speed = env.k.vehicle.get_speed(veh_id) + prev_speed = env.k.vehicle.get_previous_speed(veh_id) + + accel = abs(speed - prev_speed) / env.sim_step + + power += M * speed * accel + M * g * Cr * speed + 0.5 * rho * A * Ca * speed ** 3 + + return -gain * power diff --git a/flow/envs/base.py b/flow/envs/base.py index 5777e9868..1abb8a3c9 100644 --- a/flow/envs/base.py +++ b/flow/envs/base.py @@ -487,6 +487,9 @@ def reset(self): except (FatalTraCIError, TraCIException): print("Error during start: {}".format(traceback.format_exc())) + # do any additional resetting of the vehicle class needed + self.k.vehicle.reset() + # reintroduce the initial vehicles to the network for veh_id in self.initial_ids: type_id, edge, lane_index, pos, speed = \ diff --git a/flow/envs/multiagent/base.py b/flow/envs/multiagent/base.py index 72d929dd5..ec95474c6 100644 --- a/flow/envs/multiagent/base.py +++ b/flow/envs/multiagent/base.py @@ -205,6 +205,9 @@ def reset(self, new_inflow_rate=None): except (FatalTraCIError, TraCIException): print("Error during start: {}".format(traceback.format_exc())) + # do any additional resetting of the vehicle class needed + self.k.vehicle.reset() + # reintroduce the initial vehicles to the network for veh_id in self.initial_ids: type_id, edge, lane_index, pos, speed = \ diff --git a/tests/fast_tests/test_rewards.py b/tests/fast_tests/test_rewards.py index ac406b545..3f2e08cde 100644 --- a/tests/fast_tests/test_rewards.py +++ b/tests/fast_tests/test_rewards.py @@ -7,6 +7,7 @@ from flow.core.rewards import average_velocity, min_delay from flow.core.rewards import desired_velocity, boolean_action_penalty from flow.core.rewards import penalize_near_standstill, penalize_standstill +from flow.core.rewards import energy_consumption os.environ["TEST_FLAG"] = "True" @@ -151,6 +152,31 @@ def test_penalize_near_standstill(self): self.assertEqual(penalize_near_standstill(env, thresh=2), -10) self.assertEqual(penalize_near_standstill(env, thresh=0.5), -9) + def test_energy_consumption(self): + """Test the energy consumption method.""" + vehicles = VehicleParams() + vehicles.add("test", num_vehicles=10) + + env_params = EnvParams(additional_params={ + "target_velocity": 10, "max_accel": 1, "max_decel": 1, + "sort_vehicles": False}) + + env, _, _ = ring_road_exp_setup(vehicles=vehicles, + env_params=env_params) + + # check the penalty is zero at speed zero + self.assertEqual(energy_consumption(env, gain=1), 0) + + # change the speed of one vehicle + env.k.vehicle.test_set_speed("test_0", 1) + self.assertEqual(energy_consumption(env), -12.059337750000001) + + # check that stepping change the previous speeds and increases the energy consumption + env.step(rl_actions=None) + env.step(rl_actions=None) + self.assertGreater(env.k.vehicle.get_previous_speed("test_0"), 0.0) + self.assertLess(energy_consumption(env), -12.059337750000001) + def test_boolean_action_penalty(self): """Test the boolean_action_penalty method.""" actions = [False, False, False, False, False] From d3c5c831b46cc4a09c698b1b683e1205d6d73817 Mon Sep 17 00:00:00 2001 From: Eugene Vinitsky Date: Tue, 17 Mar 2020 12:21:37 -0700 Subject: [PATCH 52/86] Add ballistic integration as an option (#873) * Add ballistic integration as an option * bug fix Co-authored-by: AboudyKreidieh --- examples/exp_configs/non_rl/i210_subnetwork.py | 5 +++-- flow/core/kernel/simulation/traci.py | 4 ++++ flow/core/params.py | 6 +++++- flow/visualize/visualizer_rllib.py | 4 ++++ 4 files changed, 16 insertions(+), 3 deletions(-) diff --git a/examples/exp_configs/non_rl/i210_subnetwork.py b/examples/exp_configs/non_rl/i210_subnetwork.py index c5077e262..dd85c56cf 100644 --- a/examples/exp_configs/non_rl/i210_subnetwork.py +++ b/examples/exp_configs/non_rl/i210_subnetwork.py @@ -24,7 +24,7 @@ lane_change_mode="strategic", ), acceleration_controller=(IDMController, { - "a": 0.3, "b": 2.0, "noise": 0.45 + "a": 0.3, "b": 2.0, "noise": 0.5 }), ) @@ -69,9 +69,10 @@ # simulation-related parameters sim=SumoParams( - sim_step=0.8, + sim_step=0.5, render=False, color_by_speed=True, + use_ballistic=True ), # environment related parameters (see flow.core.params.EnvParams) diff --git a/flow/core/kernel/simulation/traci.py b/flow/core/kernel/simulation/traci.py index af68f6647..0ee29ada6 100644 --- a/flow/core/kernel/simulation/traci.py +++ b/flow/core/kernel/simulation/traci.py @@ -91,6 +91,10 @@ def start_simulation(self, network, sim_params): "--step-length", str(sim_params.sim_step) ] + # use a ballistic integration step (if request) + if sim_params.use_ballistic: + sumo_call.append("--step-method.ballistic") + # add step logs (if requested) if sim_params.no_step_log: sumo_call.append("--no-step-log") diff --git a/flow/core/params.py b/flow/core/params.py index e424455bb..5a7467580 100755 --- a/flow/core/params.py +++ b/flow/core/params.py @@ -566,6 +566,8 @@ class SumoParams(SimParams): color_by_speed : bool whether to color the vehicles by the speed they are moving at the current time step + use_ballistic: bool, optional + If true, use a ballistic integration step instead of an euler step """ def __init__(self, @@ -586,7 +588,8 @@ def __init__(self, print_warnings=True, teleport_time=-1, num_clients=1, - color_by_speed=False): + color_by_speed=False, + use_ballistic=False): """Instantiate SumoParams.""" super(SumoParams, self).__init__( sim_step, render, restart_instance, emission_path, save_render, @@ -600,6 +603,7 @@ def __init__(self, self.teleport_time = teleport_time self.num_clients = num_clients self.color_by_speed = color_by_speed + self.use_ballistic = use_ballistic class EnvParams: diff --git a/flow/visualize/visualizer_rllib.py b/flow/visualize/visualizer_rllib.py index 918a16670..8c38a91c1 100644 --- a/flow/visualize/visualizer_rllib.py +++ b/flow/visualize/visualizer_rllib.py @@ -74,6 +74,10 @@ def visualizer_rllib(args): sim_params = flow_params['sim'] setattr(sim_params, 'num_clients', 1) + # for hacks for old pkl files TODO: remove eventually + if not hasattr(sim_params, 'use_ballistic'): + sim_params.use_ballistic = False + # Determine agent and checkpoint config_run = config['env_config']['run'] if 'run' in config['env_config'] \ else None From d1688c6d533d5370b7f20b6776ddf191a7b32377 Mon Sep 17 00:00:00 2001 From: zpymyyn Date: Tue, 17 Mar 2020 21:22:34 +0200 Subject: [PATCH 53/86] add pip in yml to avoid wrong-version pip (#858) --- environment.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/environment.yml b/environment.yml index 3bdfebfa4..f57c8d33d 100644 --- a/environment.yml +++ b/environment.yml @@ -7,6 +7,7 @@ dependencies: - six==1.11.0 - path.py - python-dateutil==2.7.3 + - pip>=18.0 - tensorflow==1.9.0 - cloudpickle==1.2.1 - setuptools==41.0.0 From 4c49ab74022a613a713f93a4ff7828821dec8cad Mon Sep 17 00:00:00 2001 From: Kathy Jang Date: Thu, 19 Mar 2020 10:32:47 -0700 Subject: [PATCH 54/86] Fixed issue 840 (#841) --- flow/utils/rllib.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/flow/utils/rllib.py b/flow/utils/rllib.py index b5abc9a23..80193c22b 100644 --- a/flow/utils/rllib.py +++ b/flow/utils/rllib.py @@ -6,6 +6,7 @@ import json from copy import deepcopy import os +import sys import flow.envs from flow.core.params import SumoLaneChangeParams, SumoCarFollowingParams, \ @@ -207,6 +208,9 @@ def get_rllib_config(path): def get_rllib_pkl(path): """Return the data from the specified rllib configuration file.""" + dirname = os.path.dirname(__file__) + filename = os.path.join(dirname, '../../examples/') + sys.path.append(filename) config_path = os.path.join(path, "params.pkl") if not os.path.exists(config_path): config_path = os.path.join(path, "../params.pkl") From 35f5b5f1b96e6ca9db6175b0fd9ccdcfb30bff6b Mon Sep 17 00:00:00 2001 From: Kathy Jang Date: Thu, 19 Mar 2020 11:18:07 -0700 Subject: [PATCH 55/86] Added code to output json. Added code to resolve macOS matplotlib import error --- examples/simulate.py | 9 +++++++++ flow/visualize/time_space_diagram.py | 7 ++++++- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/examples/simulate.py b/examples/simulate.py index 60767b6b7..4ec46b974 100644 --- a/examples/simulate.py +++ b/examples/simulate.py @@ -5,7 +5,10 @@ """ import argparse import sys +import json +import os from flow.core.experiment import Experiment +from flow.utils.rllib import FlowParamsEncoder def parse_args(args): @@ -70,6 +73,12 @@ def parse_args(args): if flags.gen_emission: flow_params['sim'].emission_path = "./data" + # Create the flow_params object + json_filename = flow_params['exp_tag'] + with open(os.path.join(flow_params['sim'].emission_path, json_filename) + '.json', 'w') as outfile: + json.dump(flow_params, outfile, + cls=FlowParamsEncoder, sort_keys=True, indent=4) + # Create the experiment object. exp = Experiment(flow_params, callables) diff --git a/flow/visualize/time_space_diagram.py b/flow/visualize/time_space_diagram.py index d8dad01e9..a08ecdf0f 100644 --- a/flow/visualize/time_space_diagram.py +++ b/flow/visualize/time_space_diagram.py @@ -21,7 +21,12 @@ import argparse import csv -from matplotlib import pyplot as plt +try: + from matplotlib import pyplot as plt +except ImportError: + import matplotlib + matplotlib.use('TkAgg') + from matplotlib import pyplot as plt from matplotlib.collections import LineCollection import matplotlib.colors as colors import numpy as np From d6ed510694f97cfa2a76539b481ec2c81a920144 Mon Sep 17 00:00:00 2001 From: zpymyyn Date: Mon, 23 Mar 2020 22:09:09 +0200 Subject: [PATCH 56/86] specify python version for pip install (#859) To avoid error caused by multiple versions of python and pip --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index f9508507e..70b894da2 100644 --- a/setup.py +++ b/setup.py @@ -21,7 +21,7 @@ class build_ext(_build_ext.build_ext): def run(self): """Install traci wheels.""" subprocess.check_call( - ['pip', 'install', + ['python3','-m','pip', 'install', 'https://akreidieh.s3.amazonaws.com/sumo/flow-0.4.0/' 'sumotools-0.4.0-py3-none-any.whl']) From 0166e419330ba56c16df84a413bedb78e65be2b4 Mon Sep 17 00:00:00 2001 From: Aboudy Kreidieh Date: Tue, 24 Mar 2020 13:38:06 -0700 Subject: [PATCH 57/86] support for h-baselines (#874) * started adding support for h-baselines * some cleanup * some cleanup * pydocstyle * added test to parse_args * working support for multiagent envs * added tests for train_h_baselines * maybe a bug fix * maybe a bug fix * one more try * helping out coveralls * got rid of broken test * pep8 --- .coveragerc | 1 + .travis.yml | 12 +- docs/source/flow_setup.rst | 91 ++++++--- examples/README.md | 164 +++++++++++++--- examples/train.py | 300 ++++++++++++++++++++++-------- tests/fast_tests/test_examples.py | 63 +++++++ tests/fast_tests/test_util.py | 20 -- 7 files changed, 496 insertions(+), 155 deletions(-) diff --git a/.coveragerc b/.coveragerc index 70674c543..3505cadcb 100644 --- a/.coveragerc +++ b/.coveragerc @@ -20,3 +20,4 @@ exclude_lines = raise NotImplementedError @ray.remote def policy_mapping_fn* + def main(args)* diff --git a/.travis.yml b/.travis.yml index 30f3174a4..297281bc7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -54,6 +54,17 @@ before_install: - export AIMSUN_SITEPACKAGES="/home/travis/miniconda/envs/aimsun_flow" - export AIMSUN_NEXT_PATH="/home/user/Aimsun_Next_XXX" + # Install stable-baselines + - pip install stable_baselines==2.7.0 + + # Install h-baselines + - pushd $HOME + - git clone https://github.com/AboudyKreidieh/h-baselines.git + - pushd h-baselines + - pip install -e . + - popd + - popd + - ls ../ install: @@ -62,7 +73,6 @@ install: - pip install -e . - pip install coveralls - pip install jupyter - - pip install stable_baselines==2.7.0 script: - nose2 --with-coverage diff --git a/docs/source/flow_setup.rst b/docs/source/flow_setup.rst index 606a9d6d4..60734b7b1 100644 --- a/docs/source/flow_setup.rst +++ b/docs/source/flow_setup.rst @@ -2,7 +2,7 @@ .. contents:: Table of contents Local Installation of Flow -================== +========================== To get Flow running, you need three things: Flow, @@ -108,7 +108,9 @@ Note that, if the above commands did not work, you may need to run ``source ~/.bashrc`` or open a new terminal to update your $PATH variable. *Troubleshooting*: -If you are a Mac user and the above command gives you the error ``FXApp:openDisplay: unable to open display :0.0``, make sure to open the application XQuartz. +If you are a Mac user and the above command gives you the error +``FXApp:openDisplay: unable to open display :0.0``, make sure to open the +application XQuartz. Testing your SUMO and Flow installation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -151,7 +153,10 @@ during the execution of various tasks. The path should look something like: export AIMSUN_NEXT_PATH="/home/user/Aimsun_Next_X_Y_Z/" # Linux export AIMSUN_NEXT_PATH="/Applications/Aimsun Next.app/Contents/MacOS/" # OS X -`Note for Mac users:` when you download Aimsun, you will get a folder named "Programming". You need to rename it to "programming" (all lowercase) and to move it inside the "Aimsun Next.app/Contents/MacOS/" directory so that the python API can work. +`Note for Mac users:` when you download Aimsun, you will get a folder named +"Programming". You need to rename it to "programming" (all lowercase) and to +move it inside the "Aimsun Next.app/Contents/MacOS/" directory so that the +python API can work. In addition, being that Aimsun's python API is written to support Python 2.7.4, we will need to create a Python 2.7.4 conda environment that Aimsun can refer @@ -198,8 +203,11 @@ to activate the `flow` env. Type: source activate flow python examples/simulate.py ring --aimsun -*Troubleshootig for Ubuntu users with Aimsun 8.4*: when you run the above example, you may get a subprocess.Popen error ``OSError: [Errno 8] Exec format error:``. -To fix this, go to the `Aimsun Next` main directory, open the `Aimsun_Next` binary with a text editor and add the shebang to the first line of the script ``#!/bin/sh``. +*Troubleshootig for Ubuntu users with Aimsun 8.4*: when you run the above +example, you may get a subprocess.Popen error ``OSError: [Errno 8] Exec format error:``. +To fix this, go to the `Aimsun Next` main directory, open the `Aimsun_Next` +binary with a text editor and add the shebang to the first line of the script +``#!/bin/sh``. (Optional) Install Ray RLlib ---------------------------- @@ -211,10 +219,11 @@ RLlib is one such library. First visit and install the required packages. -If you are not intending to develop RL algorithms or customize rllib you don't need to do anything, -Ray was installed when you created the conda environment. +If you are not intending to develop RL algorithms or customize rllib you don't +need to do anything, Ray was installed when you created the conda environment. -If you are intending to modify Ray, the installation process for this library is as follows: +If you are intending to modify Ray, the installation process for this library +is as follows: :: @@ -249,6 +258,34 @@ In order to test run an Flow experiment in RLlib, try the following command: If it does not fail, this means that you have Flow properly configured with RLlib. + +Visualizing with Tensorboard +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +To visualize the training progress: + +:: + + tensorboard --logdir=~/ray_results + +If tensorboard is not installed, you can install with pip: + +:: + + pip install tensorboard + +For information on how to deploy a cluster, refer to the +`Ray instructions `_. +The basic workflow is running the following locally, ssh-ing into the host +machine, and starting jobs from there. + +:: + + pip install boto3 + ray create-or-update scripts/ray_autoscale.yaml + ray teardown scripts/ray_autoscale.yaml + + (Optional) Install Stable Baselines ----------------------------------- @@ -267,33 +304,29 @@ You can test your installation by running python examples/train.py singleagent_ring --rl_trainer Stable-Baselines +(Optional) Install h-baselines +------------------------------ -(Optional) Visualizing with Tensorboard ---------------------------------------- - -To visualize the training progress: +h-baselines is another variant of stable-baselines that support the use of +single-agent, multiagent, and hierarchical policies. To install h-baselines, +run the following commands: :: - tensorboard --logdir=~/ray_results - -If tensorboard is not installed, you can install with pip: + git clone https://github.com/AboudyKreidieh/h-baselines.git + cd h-baselines + source activate flow # if using a Flow environment + pip install -e . -:: - pip install tensorboard +Testing your h-baselines installation +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -For information on how to deploy a cluster, refer to the `Ray instructions `_. -The basic workflow is running the following locally, ssh-ing into the host machine, and starting -jobs from there. +You can test your installation by running :: - pip install boto3 - ray create-or-update scripts/ray_autoscale.yaml - ray teardown scripts/ray_autoscale.yaml - - + python examples/train.py singleagent_ring --rl_trainer h-baselines (Optional) Direct install of SUMO from GitHub @@ -358,16 +391,22 @@ If you have Ubuntu 14.04+, run the following command Virtual installation of Flow (using docker containers) -================================ +====================================================== To install a containerized Flow stack, run: + :: + docker run -d -p 5901:5901 -p 6901:6901 fywu85/flow-desktop:latest To access the docker container, go to the following URL and enter the default password `password`: + :: + http://localhost:6901/vnc.html To use the Jupyter Notebook inside the container, run: + :: + jupyter notebook --ip=127.0.0.1 diff --git a/examples/README.md b/examples/README.md index f25f488c5..a9d681131 100644 --- a/examples/README.md +++ b/examples/README.md @@ -10,8 +10,8 @@ within the Flow framework on a variety of traffic problems. These examples are python files that may be executed either from terminal or via a text editor. For example, in order to execute the non-RL Ring example we run: -```shell -python simulate.py ring +```shell script +python simulate.py "ring" ``` The examples are categorized into the following 3 sections: @@ -24,58 +24,168 @@ micro-simulator sumo and traffic macro-simulator Aimsun. To execute these examples, run -```shell +```shell script python simulate.py EXP_CONFIG ``` -where `EXP_CONFIG` is the name of the experiment configuration file, as located in -`exp_configs/non_rl.` +where `EXP_CONFIG` is the name of the experiment configuration file, as located +in `exp_configs/non_rl.` There are several *optional* arguments that can be added to the above command: -```shell +```shell script python simulate.py EXP_CONFIG --num_runs n --no_render --aimsun --gen_emission ``` -where `--num_runs` indicates the number of simulations to run (default of `n` is 1), `--no_render` indicates whether to deactivate the simulation GUI during runtime (by default simulation GUI is active), `--aimsun` indicates whether to run the simulation using the simulator Aimsun (the default simulator is SUMO), and `--gen_emission` indicates whether to generate an emission file from the simulation. +where `--num_runs` indicates the number of simulations to run (default of `n` +is 1), `--no_render` indicates whether to deactivate the simulation GUI during +runtime (by default simulation GUI is active), `--aimsun` indicates whether to +run the simulation using the simulator Aimsun (the default simulator is SUMO), +and `--gen_emission` indicates whether to generate an emission file from the +simulation. -## RL examples based on RLlib +## RL examples -These examples are similar networks as those mentioned in *non-RL examples*, but in the -presence of autonomous vehicle (AV) or traffic light agents -being trained through RL algorithms provided by *RLlib*. +### RLlib + +These examples are similar networks as those mentioned in *non-RL examples*, +but in the presence of autonomous vehicle (AV) or traffic light agents being +trained through RL algorithms provided by *RLlib*. To execute these examples, run -```shell - python train.py EXP_CONFIG - (or python train.py EXP_CONFIG --rl_trainer RLlib) +```shell script +python train.py EXP_CONFIG --rl_trainer "rllib" ``` -where `EXP_CONFIG` is the name of the experiment configuration file, as located in -`exp_configs/rl/singleagent` or `exp_configs/rl/multiagent.` +where `EXP_CONFIG` is the name of the experiment configuration file, as located +in `exp_configs/rl/singleagent` or `exp_configs/rl/multiagent.` -## RL examples based on stable_baselines +### stable-baselines These examples provide similar networks as those -mentioned in *non-RL examples*, but in the presence of autonomous vehicle (AV) or traffic -light agents being trained through RL algorithms provided by OpenAI *stable -baselines*. +mentioned in *non-RL examples*, but in the presence of autonomous vehicle (AV) +or traffic light agents being trained through RL algorithms provided by OpenAI +*stable-baselines*. To execute these examples, run -```shell - python train.py EXP_CONFIG --rl_trainer Stable-Baselines +```shell script +python train.py EXP_CONFIG --rl_trainer "stable-baselines" ``` -where `EXP_CONFIG` is the name of the experiment configuration file, as located in -`exp_configs/rl/singleagent.` +where `EXP_CONFIG` is the name of the experiment configuration file, as located +in `exp_configs/rl/singleagent.` Note that, currently, multiagent experiments are only supported through RLlib. There are several *optional* arguments that can be added to the above command: -```shell - python train.py EXP_CONFIG --rl_trainer Stable-Baselines --num_cpus n1 --num_steps n2 --rollout_size r +```shell script +python train.py EXP_CONFIG --rl_trainer "stable-baselines" --num_cpus n1 --num_steps n2 --rollout_size r +``` +where `--num_cpus` indicates the number of CPUs to use (default of `n1` is 1), +`--num_steps` indicates the total steps to perform the learning (default of +`n2` is 5000), and `--rollout_size` indicates the number of steps in a training +batch (default of `r` is 1000) + +### h-baselines + +A third RL algorithms package supported by the `train.py` script is +[h-baselines](https://github.com/AboudyKreidieh/h-baselines). In order to use +the algorithms supported by this package, begin by installing h-baselines by +following the setup instructions located +[here](https://flow.readthedocs.io/en/latest/flow_setup.html#optional-install-h-baselines). +A policy can be trained using one of the exp_configs as follows: + +```shell script +python examples/train.py singleagent_ring --rl_trainer h-baselines ``` -where `--num_cpus` indicates the number of CPUs to use (default of `n1` is 1), `--num_steps` indicates the total steps to perform the learning (default of `n2` is 5000), and `--rollout_size` indicates the number of steps in a training batch (default of `r` is 1000) + +**Logging:** + +The above script executes a training operation and begins logging training and +testing data under the path: *training_data/singleagent_ring/*. + +To visualize the statistics of various tensorflow operations in tensorboard, +type: + +```shell script +tensorboard --logdir /examples/training_data/singleagent_ring/ +``` + +Moreover, as training progressive, per-iteration and cumulative statistics are +printed as a table on your terminal. These statistics are stored under the csv +files *train.csv* and *eval.csv* (if also using an evaluation environment) +within the same directory. + +**Hyperparameters:** + +When using h-baseline, multiple new command-line arguments can be passed to +adjust the choice of algorithm and variable hyperparameters of the algorithms. +These new arguments are as follows: + +* `--alg` (*str*): The algorithm to use. Must be one of [TD3, SAC]. Defaults to + 'TD3'. +* `--evaluate` (*store_true*): whether to add an evaluation environment. The + evaluation environment is similar to the training environment, but with + `env_params.evaluate` set to True. +* `--n_training` (*int*): Number of training operations to perform. Each + training operation is performed on a new seed. Defaults to 1. +* `--total_steps` (*int*): Total number of timesteps used during training. + Defaults to 1000000. +* `--seed` (*int*): Sets the seed for numpy, tensorflow, and random. Defaults + to 1. +* `--log_interval` (*int*): the number of training steps before logging + training results. Defaults to 2000. +* `--eval_interval` (*int*): number of simulation steps in the training + environment before an evaluation is performed. Only relevant if `--evaluate` + is called. Defaults to 50000. +* `--save_interval` (int): number of simulation steps in the training + environment before the model is saved. Defaults to 50000. +* `--initial_exploration_steps` (*int*): number of timesteps that the policy is + run before training to initialize the replay buffer with samples. Defaults to + 10000. +* `--nb_train_steps` (*int*): the number of training steps. Defaults to 1. +* `--nb_rollout_steps` (*int*): the number of rollout steps. Defaults to 1. +* `--nb_eval_episodes` (*int*): the number of evaluation episodes. Only + relevant if `--evaluate` is called. Defaults to 50. +* `--reward_scale` (*float*): the value the reward should be scaled by. + Defaults to 1. +* `--buffer_size` (*int*): the max number of transitions to store. Defaults to + 200000. +* `--batch_size` (*int*): the size of the batch for learning the policy. + Defaults to 128. +* `--actor_lr` (*float*): the actor learning rate. Defaults to 3e-4. +* `--critic_lr` (*float*): the critic learning rate. Defaults to 3e-4. +* `--tau` (*float*): the soft update coefficient (keep old values, between 0 + and 1). Defatuls to 0.005. +* `--gamma` (*float*): the discount rate. Defaults to 0.99. +* `--layer_norm` (*store_true*): enable layer normalisation +* `--use_huber` (*store_true*): specifies whether to use the huber distance + function as the loss for the critic. If set to False, the mean-squared error + metric is used instead") +* `--actor_update_freq` (*int*): number of training steps per actor policy + update step. The critic policy is updated every training step. Only used when + the algorithm is set to "TD3". Defaults to 2. +* `--noise` (*float*): scaling term to the range of the action space, that is + subsequently used as the standard deviation of Gaussian noise added to the + action if `apply_noise` is set to True in `get_action`". Only used when the + algorithm is set to "TD3". Defaults to 0.1. +* `--target_policy_noise` (*float*): standard deviation term to the noise from + the output of the target actor policy. See TD3 paper for more. Only used when + the algorithm is set to "TD3". Defaults to 0.2. +* `--target_noise_clip` (*float*): clipping term for the noise injected in the + target actor policy. Only used when the algorithm is set to "TD3". Defaults + to 0.5. +* `--target_entropy` (*float*): target entropy used when learning the entropy + coefficient. If set to None, a heuristic value is used. Only used when the + algorithm is set to "SAC". Defaults to None. + +Additionally, the following arguments can be passed when training a multiagent +policy: + +* `--shared` (*store_true*): whether to use a shared policy for all agents +* `--maddpg` (*store_true*): whether to use an algorithm-specific variant of + the MADDPG algorithm + ## Simulated Examples diff --git a/examples/train.py b/examples/train.py index a159c13ee..a1288e2f0 100644 --- a/examples/train.py +++ b/examples/train.py @@ -6,12 +6,12 @@ Usage python train.py EXP_CONFIG """ - import argparse import json import os import sys from time import strftime +from copy import deepcopy from stable_baselines.common.vec_env import DummyVecEnv, SubprocVecEnv from stable_baselines import PPO2 @@ -20,16 +20,15 @@ from ray import tune from ray.tune import run_experiments from ray.tune.registry import register_env -from flow.utils.registry import make_create_env try: from ray.rllib.agents.agent import get_agent_class except ImportError: from ray.rllib.agents.registry import get_agent_class -from copy import deepcopy from flow.core.util import ensure_dir from flow.utils.registry import env_constructor from flow.utils.rllib import FlowParamsEncoder, get_flow_params +from flow.utils.registry import make_create_env def parse_args(args): @@ -72,11 +71,16 @@ def parse_args(args): return parser.parse_known_args(args)[0] -def run_model_stablebaseline(flow_params, num_cpus=1, rollout_size=50, num_steps=50): +def run_model_stablebaseline(flow_params, + num_cpus=1, + rollout_size=50, + num_steps=50): """Run the model for num_steps if provided. Parameters ---------- + flow_params : dict + flow-specific parameters num_cpus : int number of CPUs used during training rollout_size : int @@ -163,7 +167,8 @@ def setup_exps_rllib(flow_params, print("policy_graphs", policy_graphs) config['multiagent'].update({'policies': policy_graphs}) if policy_mapping_fn is not None: - config['multiagent'].update({'policy_mapping_fn': tune.function(policy_mapping_fn)}) + config['multiagent'].update( + {'policy_mapping_fn': tune.function(policy_mapping_fn)}) if policies_to_train is not None: config['multiagent'].update({'policies_to_train': policies_to_train}) @@ -174,89 +179,222 @@ def setup_exps_rllib(flow_params, return alg_run, gym_name, config -if __name__ == "__main__": - flags = parse_args(sys.argv[1:]) +def train_rllib(submodule, flags): + """Train policies using the PPO algorithm in RLlib.""" + flow_params = submodule.flow_params + n_cpus = submodule.N_CPUS + n_rollouts = submodule.N_ROLLOUTS + policy_graphs = getattr(submodule, "POLICY_GRAPHS", None) + policy_mapping_fn = getattr(submodule, "policy_mapping_fn", None) + policies_to_train = getattr(submodule, "policies_to_train", None) + + alg_run, gym_name, config = setup_exps_rllib( + flow_params, n_cpus, n_rollouts, + policy_graphs, policy_mapping_fn, policies_to_train) + + ray.init(num_cpus=n_cpus + 1, object_store_memory=200 * 1024 * 1024) + exp_config = { + "run": alg_run, + "env": gym_name, + "config": { + **config + }, + "checkpoint_freq": 20, + "checkpoint_at_end": True, + "max_failures": 999, + "stop": { + "training_iteration": flags.num_steps, + }, + } + + if flags.checkpoint_path is not None: + exp_config['restore'] = flags.checkpoint_path + run_experiments({flow_params["exp_tag"]: exp_config}) + + +def train_h_baselines(flow_params, args, multiagent): + """Train policies using SAC and TD3 with h-baselines.""" + from hbaselines.algorithms import OffPolicyRLAlgorithm + from hbaselines.utils.train import parse_options, get_hyperparameters + from hbaselines.envs.mixed_autonomy.envs import FlowEnv + + flow_params = deepcopy(flow_params) + + # Get the command-line arguments that are relevant here + args = parse_options(description="", example_usage="", args=args) + + # the base directory that the logged data will be stored in + base_dir = "training_data" + + # Create the training environment. + env = FlowEnv( + flow_params, + multiagent=multiagent, + shared=args.shared, + maddpg=args.maddpg, + render=args.render, + version=0 + ) + + # Create the evaluation environment. + if args.evaluate: + eval_flow_params = deepcopy(flow_params) + eval_flow_params['env'].evaluate = True + eval_env = FlowEnv( + eval_flow_params, + multiagent=multiagent, + shared=args.shared, + maddpg=args.maddpg, + render=args.render_eval, + version=1 + ) + else: + eval_env = None + + for i in range(args.n_training): + # value of the next seed + seed = args.seed + i + + # The time when the current experiment started. + now = strftime("%Y-%m-%d-%H:%M:%S") + + # Create a save directory folder (if it doesn't exist). + dir_name = os.path.join(base_dir, '{}/{}'.format(args.env_name, now)) + ensure_dir(dir_name) + + # Get the policy class. + if args.alg == "TD3": + if multiagent: + from hbaselines.multi_fcnet.td3 import MultiFeedForwardPolicy + policy = MultiFeedForwardPolicy + else: + from hbaselines.fcnet.td3 import FeedForwardPolicy + policy = FeedForwardPolicy + elif args.alg == "SAC": + if multiagent: + from hbaselines.multi_fcnet.sac import MultiFeedForwardPolicy + policy = MultiFeedForwardPolicy + else: + from hbaselines.fcnet.sac import FeedForwardPolicy + policy = FeedForwardPolicy + else: + raise ValueError("Unknown algorithm: {}".format(args.alg)) + + # Get the hyperparameters. + hp = get_hyperparameters(args, policy) + + # Add the seed for logging purposes. + params_with_extra = hp.copy() + params_with_extra['seed'] = seed + params_with_extra['env_name'] = args.env_name + params_with_extra['policy_name'] = policy.__name__ + params_with_extra['algorithm'] = args.alg + params_with_extra['date/time'] = now + + # Add the hyperparameters to the folder. + with open(os.path.join(dir_name, 'hyperparameters.json'), 'w') as f: + json.dump(params_with_extra, f, sort_keys=True, indent=4) + + # Create the algorithm object. + alg = OffPolicyRLAlgorithm( + policy=policy, + env=env, + eval_env=eval_env, + **hp + ) - # import relevant information from the exp_config script - module = __import__("exp_configs.rl.singleagent", fromlist=[flags.exp_config]) - module_ma = __import__("exp_configs.rl.multiagent", fromlist=[flags.exp_config]) + # Perform training. + alg.learn( + total_timesteps=args.total_steps, + log_dir=dir_name, + log_interval=args.log_interval, + eval_interval=args.eval_interval, + save_interval=args.save_interval, + initial_exploration_steps=args.initial_exploration_steps, + seed=seed, + ) + + +def train_stable_baselines(submodule, flags): + """Train policies using the PPO algorithm in stable-baselines.""" + flow_params = submodule.flow_params + # Path to the saved files + exp_tag = flow_params['exp_tag'] + result_name = '{}/{}'.format(exp_tag, strftime("%Y-%m-%d-%H:%M:%S")) + + # Perform training. + print('Beginning training.') + model = run_model_stablebaseline( + flow_params, flags.num_cpus, flags.rollout_size, flags.num_steps) + + # Save the model to a desired folder and then delete it to demonstrate + # loading. + print('Saving the trained model!') + path = os.path.realpath(os.path.expanduser('~/baseline_results')) + ensure_dir(path) + save_path = os.path.join(path, result_name) + model.save(save_path) + + # dump the flow params + with open(os.path.join(path, result_name) + '.json', 'w') as outfile: + json.dump(flow_params, outfile, + cls=FlowParamsEncoder, sort_keys=True, indent=4) + + # Replay the result by loading the model + print('Loading the trained model and testing it out!') + model = PPO2.load(save_path) + flow_params = get_flow_params(os.path.join(path, result_name) + '.json') + flow_params['sim'].render = True + env = env_constructor(params=flow_params, version=0)() + # The algorithms require a vectorized environment to run + eval_env = DummyVecEnv([lambda: env]) + obs = eval_env.reset() + reward = 0 + for _ in range(flow_params['env'].horizon): + action, _states = model.predict(obs) + obs, rewards, dones, info = eval_env.step(action) + reward += rewards + print('the final reward is {}'.format(reward)) + + +def main(args): + """Perform the training operations.""" + # Parse script-level arguments (not including package arguments). + flags = parse_args(args) + + # Import relevant information from the exp_config script. + module = __import__( + "exp_configs.rl.singleagent", fromlist=[flags.exp_config]) + module_ma = __import__( + "exp_configs.rl.multiagent", fromlist=[flags.exp_config]) + + # Import the sub-module containing the specified exp_config and determine + # whether the environment is single agent or multi-agent. if hasattr(module, flags.exp_config): submodule = getattr(module, flags.exp_config) + multiagent = False elif hasattr(module_ma, flags.exp_config): submodule = getattr(module_ma, flags.exp_config) - assert flags.rl_trainer.lower() == "RLlib".lower(), \ + assert flags.rl_trainer.lower() in ["rllib", "h-baselines"], \ "Currently, multiagent experiments are only supported through "\ - "RLlib. Try running this experiment using RLlib: 'python train.py EXP_CONFIG'" + "RLlib. Try running this experiment using RLlib: " \ + "'python train.py EXP_CONFIG'" + multiagent = True else: - assert False, "Unable to find experiment config!" + raise ValueError("Unable to find experiment config.") + + # Perform the training operation. if flags.rl_trainer.lower() == "rllib": + train_rllib(submodule, flags) + elif flags.rl_trainer.lower() == "stable-baselines": + train_stable_baselines(submodule, flags) + elif flags.rl_trainer.lower() == "h-baselines": flow_params = submodule.flow_params - n_cpus = submodule.N_CPUS - n_rollouts = submodule.N_ROLLOUTS - policy_graphs = getattr(submodule, "POLICY_GRAPHS", None) - policy_mapping_fn = getattr(submodule, "policy_mapping_fn", None) - policies_to_train = getattr(submodule, "policies_to_train", None) - - alg_run, gym_name, config = setup_exps_rllib( - flow_params, n_cpus, n_rollouts, - policy_graphs, policy_mapping_fn, policies_to_train) - - ray.init(num_cpus=n_cpus + 1, object_store_memory=200 * 1024 * 1024) - exp_config = { - "run": alg_run, - "env": gym_name, - "config": { - **config - }, - "checkpoint_freq": 20, - "checkpoint_at_end": True, - "max_failures": 999, - "stop": { - "training_iteration": flags.num_steps, - }, - } - - if flags.checkpoint_path is not None: - exp_config['restore'] = flags.checkpoint_path - trials = run_experiments({flow_params["exp_tag"]: exp_config}) - - elif flags.rl_trainer == "Stable-Baselines": - flow_params = submodule.flow_params - # Path to the saved files - exp_tag = flow_params['exp_tag'] - result_name = '{}/{}'.format(exp_tag, strftime("%Y-%m-%d-%H:%M:%S")) - - # Perform training. - print('Beginning training.') - model = run_model_stablebaseline(flow_params, flags.num_cpus, flags.rollout_size, flags.num_steps) - - # Save the model to a desired folder and then delete it to demonstrate - # loading. - print('Saving the trained model!') - path = os.path.realpath(os.path.expanduser('~/baseline_results')) - ensure_dir(path) - save_path = os.path.join(path, result_name) - model.save(save_path) - - # dump the flow params - with open(os.path.join(path, result_name) + '.json', 'w') as outfile: - json.dump(flow_params, outfile, - cls=FlowParamsEncoder, sort_keys=True, indent=4) - - # Replay the result by loading the model - print('Loading the trained model and testing it out!') - model = PPO2.load(save_path) - flow_params = get_flow_params(os.path.join(path, result_name) + '.json') - flow_params['sim'].render = True - env_constructor = env_constructor(params=flow_params, version=0)() - # The algorithms require a vectorized environment to run - eval_env = DummyVecEnv([lambda: env_constructor]) - obs = eval_env.reset() - reward = 0 - for _ in range(flow_params['env'].horizon): - action, _states = model.predict(obs) - obs, rewards, dones, info = eval_env.step(action) - reward += rewards - print('the final reward is {}'.format(reward)) + train_h_baselines(flow_params, args, multiagent) else: - assert False, "rl_trainer should be either 'RLlib' or 'Stable-Baselines'!" + raise ValueError("rl_trainer should be either 'rllib', 'h-baselines', " + "or 'stable-baselines'.") + + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/tests/fast_tests/test_examples.py b/tests/fast_tests/test_examples.py index 5c36d8760..a05fed68e 100644 --- a/tests/fast_tests/test_examples.py +++ b/tests/fast_tests/test_examples.py @@ -26,8 +26,10 @@ flow_params as multiagent_traffic_light_grid from examples.exp_configs.rl.multiagent.multiagent_highway import flow_params as multiagent_highway +from examples.train import parse_args as parse_train_args from examples.train import run_model_stablebaseline as run_stable_baselines_model from examples.train import setup_exps_rllib as setup_rllib_exps +from examples.train import train_h_baselines from examples.exp_configs.non_rl.bay_bridge import flow_params as non_rl_bay_bridge from examples.exp_configs.non_rl.bay_bridge_toll import flow_params as non_rl_bay_bridge_toll @@ -121,6 +123,42 @@ def run_simulation(flow_params): exp.run(1) +class TestTrain(unittest.TestCase): + + def test_parse_args(self): + """Tests the parse_args method in train.py.""" + # test the default case + args = parse_train_args(["exp_config"]) + + self.assertDictEqual(vars(args), { + 'exp_config': 'exp_config', + 'rl_trainer': 'rllib', + 'num_cpus': 1, + 'num_steps': 5000, + 'rollout_size': 1000, + 'checkpoint_path': None + }) + + # test the case when optional args are specified + args = parse_train_args([ + "exp_config", + "--rl_trainer", "h-baselines", + "--num_cpus" "2", + "--num_steps", "3", + "--rollout_size", "4", + "--checkpoint_path", "5", + ]) + + self.assertDictEqual(vars(args), { + 'checkpoint_path': '5', + 'exp_config': 'exp_config', + 'num_cpus': 1, + 'num_steps': 3, + 'rl_trainer': 'h-baselines', + 'rollout_size': 4 + }) + + class TestStableBaselineExamples(unittest.TestCase): """Tests the example scripts in examples/exp_configs/rl/singleagent for stable_baselines. @@ -148,6 +186,31 @@ def test_singleagent_bottleneck(self): self.run_exp(singleagent_bottleneck) +class TestHBaselineExamples(unittest.TestCase): + """Tests the functionality of the h-baselines features in train.py. + + This is done by running a set of experiments for 10 time-steps and + confirming that it runs. + """ + @staticmethod + def run_exp(flow_params, multiagent): + train_h_baselines( + flow_params=flow_params, + args=[ + flow_params["env_name"].__name__, + "--initial_exploration_steps", "1", + "--total_steps", "10" + ], + multiagent=multiagent, + ) + + def test_singleagent_ring(self): + self.run_exp(singleagent_ring.copy(), multiagent=False) + + def test_multiagent_ring(self): + self.run_exp(multiagent_ring.copy(), multiagent=True) + + class TestRllibExamples(unittest.TestCase): """Tests the example scripts in examples/exp_configs/rl/singleagent and examples/exp_configs/rl/multiagent for RLlib. diff --git a/tests/fast_tests/test_util.py b/tests/fast_tests/test_util.py index 458fadcf4..67386cc77 100644 --- a/tests/fast_tests/test_util.py +++ b/tests/fast_tests/test_util.py @@ -14,7 +14,6 @@ from flow.core.util import emission_to_csv from flow.envs import MergePOEnv from flow.networks import MergeNetwork -from flow.utils.flow_warnings import deprecated_attribute from flow.utils.registry import make_create_env from flow.utils.rllib import FlowParamsEncoder, get_flow_params @@ -60,25 +59,6 @@ def test_emission_to_csv(self): self.assertEqual(len(dict1), 104) -class TestWarnings(unittest.TestCase): - """Tests warning functions located in flow.utils.warnings""" - - def test_deprecated_attribute(self): - # dummy class - class Foo(object): - pass - - # dummy attribute name - dep_from = "bar_deprecated" - dep_to = "bar_new" - - # check the deprecation warning is printing what is expected - self.assertWarnsRegex( - PendingDeprecationWarning, - "The attribute bar_deprecated in Foo is deprecated, use bar_new " - "instead.", deprecated_attribute, Foo(), dep_from, dep_to) - - class TestRegistry(unittest.TestCase): """Tests the methods located in flow/utils/registry.py""" From 5603f035581c6db19c3c7f6fcc8ab1378fbd215b Mon Sep 17 00:00:00 2001 From: Aboudy Kreidieh Date: Wed, 25 Mar 2020 10:24:39 -0700 Subject: [PATCH 58/86] bug fix for num_rl_vehicles during reset (#884) --- flow/core/kernel/vehicle/traci.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flow/core/kernel/vehicle/traci.py b/flow/core/kernel/vehicle/traci.py index 657b89a94..50cd106c9 100644 --- a/flow/core/kernel/vehicle/traci.py +++ b/flow/core/kernel/vehicle/traci.py @@ -311,7 +311,6 @@ def _add_departed(self, veh_id, veh_type): if accel_controller[0] == RLController: if veh_id not in self.__rl_ids: self.__rl_ids.append(veh_id) - self.num_rl_vehicles += 1 else: if veh_id not in self.__human_ids: self.__human_ids.append(veh_id) @@ -362,6 +361,7 @@ def _add_departed(self, veh_id, veh_type): # make sure that the order of rl_ids is kept sorted self.__rl_ids.sort() + self.num_rl_vehicles = len(self.__rl_ids) # get the subscription results from the new vehicle new_obs = self.kernel_api.vehicle.getSubscriptionResults(veh_id) From 5e3e88742197c0587423b35c0ec9a9457ad75cf0 Mon Sep 17 00:00:00 2001 From: Kathy Jang Date: Thu, 9 Apr 2020 11:50:44 -0700 Subject: [PATCH 59/86] style --- examples/simulate.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/simulate.py b/examples/simulate.py index 4ec46b974..848f030a4 100644 --- a/examples/simulate.py +++ b/examples/simulate.py @@ -76,8 +76,8 @@ def parse_args(args): # Create the flow_params object json_filename = flow_params['exp_tag'] with open(os.path.join(flow_params['sim'].emission_path, json_filename) + '.json', 'w') as outfile: - json.dump(flow_params, outfile, - cls=FlowParamsEncoder, sort_keys=True, indent=4) + json.dump(flow_params, outfile, + cls=FlowParamsEncoder, sort_keys=True, indent=4) # Create the experiment object. exp = Experiment(flow_params, callables) From 95e63a8be6156826cc74b73838fbf519230ae775 Mon Sep 17 00:00:00 2001 From: chendiw <31671291+chendiw@users.noreply.github.com> Date: Tue, 21 Apr 2020 15:14:31 -0700 Subject: [PATCH 60/86] moved imports under functions in train.py (#903) * deleting unworking params from SumoChangeLaneParams * deleted unworking params, sublane working in highway : * moved imports inside functions * Apply suggestions from code review * bug fixes * bug fix Co-authored-by: Aboudy Kreidieh --- examples/train.py | 30 +++++++++++++++++------------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/examples/train.py b/examples/train.py index a1288e2f0..652d0efa5 100644 --- a/examples/train.py +++ b/examples/train.py @@ -13,18 +13,6 @@ from time import strftime from copy import deepcopy -from stable_baselines.common.vec_env import DummyVecEnv, SubprocVecEnv -from stable_baselines import PPO2 - -import ray -from ray import tune -from ray.tune import run_experiments -from ray.tune.registry import register_env -try: - from ray.rllib.agents.agent import get_agent_class -except ImportError: - from ray.rllib.agents.registry import get_agent_class - from flow.core.util import ensure_dir from flow.utils.registry import env_constructor from flow.utils.rllib import FlowParamsEncoder, get_flow_params @@ -94,6 +82,9 @@ def run_model_stablebaseline(flow_params, stable_baselines.* the trained model """ + from stable_baselines.common.vec_env import DummyVecEnv, SubprocVecEnv + from stable_baselines import PPO2 + if num_cpus == 1: constructor = env_constructor(params=flow_params, version=0)() # The algorithms require a vectorized environment to run @@ -139,6 +130,13 @@ def setup_exps_rllib(flow_params, dict training configuration parameters """ + from ray import tune + from ray.tune.registry import register_env + try: + from ray.rllib.agents.agent import get_agent_class + except ImportError: + from ray.rllib.agents.registry import get_agent_class + horizon = flow_params['env'].horizon alg_run = "PPO" @@ -181,6 +179,9 @@ def setup_exps_rllib(flow_params, def train_rllib(submodule, flags): """Train policies using the PPO algorithm in RLlib.""" + import ray + from ray.tune import run_experiments + flow_params = submodule.flow_params n_cpus = submodule.N_CPUS n_rollouts = submodule.N_ROLLOUTS @@ -216,7 +217,7 @@ def train_h_baselines(flow_params, args, multiagent): """Train policies using SAC and TD3 with h-baselines.""" from hbaselines.algorithms import OffPolicyRLAlgorithm from hbaselines.utils.train import parse_options, get_hyperparameters - from hbaselines.envs.mixed_autonomy.envs import FlowEnv + from hbaselines.envs.mixed_autonomy import FlowEnv flow_params = deepcopy(flow_params) @@ -317,6 +318,9 @@ def train_h_baselines(flow_params, args, multiagent): def train_stable_baselines(submodule, flags): """Train policies using the PPO algorithm in stable-baselines.""" + from stable_baselines.common.vec_env import DummyVecEnv + from stable_baselines import PPO2 + flow_params = submodule.flow_params # Path to the saved files exp_tag = flow_params['exp_tag'] From 86761a69dc3445d9850db9f0f0a63ff8b917d725 Mon Sep 17 00:00:00 2001 From: Aboudy Kreidieh Date: Sat, 2 May 2020 02:51:06 -0700 Subject: [PATCH 61/86] Bando / ghost edge (#917) * added bando model * added ghost edge to the highway network * added highway-single example * bug fixes * more tests --- examples/exp_configs/non_rl/highway_single.py | 110 ++++++++++++++++++ flow/controllers/__init__.py | 5 +- flow/controllers/car_following_models.py | 83 +++++++++++++ flow/networks/highway.py | 81 +++++++++++-- tests/fast_tests/test_controllers.py | 58 ++++++++- tests/fast_tests/test_examples.py | 5 + tests/fast_tests/test_scenarios.py | 61 +++++++++- tests/fast_tests/test_vehicles.py | 16 ++- tests/setup_scripts.py | 4 +- 9 files changed, 406 insertions(+), 17 deletions(-) create mode 100644 examples/exp_configs/non_rl/highway_single.py diff --git a/examples/exp_configs/non_rl/highway_single.py b/examples/exp_configs/non_rl/highway_single.py new file mode 100644 index 000000000..46b18c0e9 --- /dev/null +++ b/examples/exp_configs/non_rl/highway_single.py @@ -0,0 +1,110 @@ +"""Multi-agent highway with ramps example. + +Trains a non-constant number of agents, all sharing the same policy, on the +highway with ramps network. +""" +from flow.controllers import BandoFTLController +from flow.core.params import EnvParams +from flow.core.params import NetParams +from flow.core.params import InitialConfig +from flow.core.params import InFlows +from flow.core.params import VehicleParams +from flow.core.params import SumoParams +from flow.core.params import SumoLaneChangeParams +from flow.networks import HighwayNetwork +from flow.envs import TestEnv +from flow.networks.highway import ADDITIONAL_NET_PARAMS + +TRAFFIC_SPEED = 11 +END_SPEED = 16 +TRAFFIC_FLOW = 2056 +HORIZON = 3600 +INCLUDE_NOISE = False + +additional_net_params = ADDITIONAL_NET_PARAMS.copy() +additional_net_params.update({ + # length of the highway + "length": 2500, + # number of lanes + "lanes": 1, + # speed limit for all edges + "speed_limit": 30, + # number of edges to divide the highway into + "num_edges": 2, + # whether to include a ghost edge of length 500m. This edge is provided a + # different speed limit. + "use_ghost_edge": True, + # speed limit for the ghost edge + "ghost_speed_limit": END_SPEED +}) + +vehicles = VehicleParams() +vehicles.add( + "human", + num_vehicles=0, + lane_change_params=SumoLaneChangeParams( + lane_change_mode="strategic", + ), + acceleration_controller=(BandoFTLController, { + 'alpha': .5, + 'beta': 20.0, + 'h_st': 12.0, + 'h_go': 50.0, + 'v_max': 30.0, + 'noise': 1.0 if INCLUDE_NOISE else 0.0, + }), +) + +inflows = InFlows() +inflows.add( + veh_type="human", + edge="highway_0", + vehs_per_hour=TRAFFIC_FLOW, + depart_lane="free", + depart_speed=TRAFFIC_SPEED, + name="idm_highway_inflow") + +# SET UP FLOW PARAMETERS + +flow_params = dict( + # name of the experiment + exp_tag='highway-single', + + # name of the flow environment the experiment is running on + env_name=TestEnv, + + # name of the network class the experiment is running on + network=HighwayNetwork, + + # simulator that is used by the experiment + simulator='traci', + + # environment related parameters (see flow.core.params.EnvParams) + env=EnvParams( + horizon=HORIZON, + warmup_steps=0, + sims_per_step=1, + ), + + # sumo-related parameters (see flow.core.params.SumoParams) + sim=SumoParams( + sim_step=0.5, + render=False, + restart_instance=False + ), + + # network-related parameters (see flow.core.params.NetParams and the + # network's documentation or ADDITIONAL_NET_PARAMS component) + net=NetParams( + inflows=inflows, + additional_params=additional_net_params + ), + + # vehicles to be placed in the network at the start of a rollout (see + # flow.core.params.VehicleParams) + veh=vehicles, + + # parameters specifying the positioning of vehicles upon initialization/ + # reset (see flow.core.params.InitialConfig) + initial=InitialConfig(), +) diff --git a/flow/controllers/__init__.py b/flow/controllers/__init__.py index 6cb20077a..4dfcf05b7 100755 --- a/flow/controllers/__init__.py +++ b/flow/controllers/__init__.py @@ -14,7 +14,8 @@ from flow.controllers.base_controller import BaseController from flow.controllers.car_following_models import CFMController, \ BCMController, OVMController, LinearOVM, IDMController, \ - SimCarFollowingController, LACController, GippsController + SimCarFollowingController, LACController, GippsController, \ + BandoFTLController from flow.controllers.velocity_controllers import FollowerStopper, \ PISaturation, NonLocalFollowerStopper @@ -35,5 +36,5 @@ "IDMController", "SimCarFollowingController", "FollowerStopper", "PISaturation", "StaticLaneChanger", "SimLaneChangeController", "ContinuousRouter", "GridRouter", "BayBridgeRouter", "LACController", - "GippsController", "NonLocalFollowerStopper" + "GippsController", "NonLocalFollowerStopper", "BandoFTLController" ] diff --git a/flow/controllers/car_following_models.py b/flow/controllers/car_following_models.py index f86c546e8..42c9b2a9b 100755 --- a/flow/controllers/car_following_models.py +++ b/flow/controllers/car_following_models.py @@ -580,3 +580,86 @@ def get_accel(self, env): v_next = min(v_acc, v_safe, self.v_desired) return (v_next-v)/env.sim_step + + +class BandoFTLController(BaseController): + """Bando follow-the-leader controller. + + Usage + ----- + See BaseController for usage example. + + Attributes + ---------- + veh_id : str + Vehicle ID for SUMO identification + car_following_params : flow.core.params.SumoCarFollowingParams + see parent class + alpha : float + gain on desired velocity to current velocity difference + (default: 0.6) + beta : float + gain on lead car velocity and self velocity difference + (default: 0.9) + h_st : float + headway for stopping (default: 5) + h_go : float + headway for full speed (default: 35) + v_max : float + max velocity (default: 30) + time_delay : float + time delay (default: 0.5) + noise : float + std dev of normal perturbation to the acceleration (default: 0) + fail_safe : str + type of flow-imposed failsafe the vehicle should posses, defaults + to no failsafe (None) + """ + + def __init__(self, + veh_id, + car_following_params, + alpha=.5, + beta=20, + h_st=2, + h_go=10, + v_max=32, + want_max_accel=False, + time_delay=0, + noise=0, + fail_safe=None): + """Instantiate an Bando controller.""" + BaseController.__init__( + self, + veh_id, + car_following_params, + delay=time_delay, + fail_safe=fail_safe, + noise=noise, + ) + self.veh_id = veh_id + self.v_max = v_max + self.alpha = alpha + self.beta = beta + self.h_st = h_st + self.h_go = h_go + self.want_max_accel = want_max_accel + + def get_accel(self, env): + """See parent class.""" + lead_id = env.k.vehicle.get_leader(self.veh_id) + if not lead_id: # no car ahead + if self.want_max_accel: + return self.max_accel + + v_l = env.k.vehicle.get_speed(lead_id) + v = env.k.vehicle.get_speed(self.veh_id) + s = env.k.vehicle.get_headway(self.veh_id) + return self.accel_func(v, v_l, s) + + def accel_func(self, v, v_l, s): + """Compute the acceleration function.""" + v_h = self.v_max * ((np.tanh(s/self.h_st-2)+np.tanh(2))/(1+np.tanh(2))) + s_dot = v_l - v + u = self.alpha * (v_h - v) + self.beta * s_dot/(s**2) + return u diff --git a/flow/networks/highway.py b/flow/networks/highway.py index c63292067..7e9c18ad5 100644 --- a/flow/networks/highway.py +++ b/flow/networks/highway.py @@ -13,7 +13,12 @@ # speed limit for all edges "speed_limit": 30, # number of edges to divide the highway into - "num_edges": 1 + "num_edges": 1, + # whether to include a ghost edge of length 500m. This edge is provided a + # different speed limit. + "use_ghost_edge": False, + # speed limit for the ghost edge + "ghost_speed_limit": 25, } @@ -29,6 +34,9 @@ class HighwayNetwork(Network): * **lanes** : number of lanes in the highway * **speed_limit** : max speed limit of the highway * **num_edges** : number of edges to divide the highway into + * **use_ghost_edge** : whether to include a ghost edge of length 500m. This + edge is provided a different speed limit. + * **ghost_speed_limit** : speed limit for the ghost edge Usage ----- @@ -62,9 +70,7 @@ def __init__(self, if p not in net_params.additional_params: raise KeyError('Network parameter "{}" not supplied'.format(p)) - self.length = net_params.additional_params["length"] - self.lanes = net_params.additional_params["lanes"] - self.num_edges = net_params.additional_params.get("num_edges", 1) + self.end_length = 500 super().__init__(name, vehicles, net_params, initial_config, traffic_lights) @@ -83,6 +89,13 @@ def specify_nodes(self, net_params): "y": 0 }] + if self.net_params.additional_params["use_ghost_edge"]: + nodes += [{ + "id": "edge_{}".format(num_edges + 1), + "x": length + self.end_length, + "y": 0 + }] + return nodes def specify_edges(self, net_params): @@ -101,12 +114,22 @@ def specify_edges(self, net_params): "length": segment_length }] + if self.net_params.additional_params["use_ghost_edge"]: + edges += [{ + "id": "highway_end", + "type": "highway_end", + "from": "edge_{}".format(num_edges), + "to": "edge_{}".format(num_edges + 1), + "length": self.end_length + }] + return edges def specify_types(self, net_params): """See parent class.""" lanes = net_params.additional_params["lanes"] speed_limit = net_params.additional_params["speed_limit"] + end_speed_limit = net_params.additional_params["ghost_speed_limit"] types = [{ "id": "highwayType", @@ -114,6 +137,13 @@ def specify_types(self, net_params): "speed": speed_limit }] + if self.net_params.additional_params["use_ghost_edge"]: + types += [{ + "id": "highway_end", + "numLanes": lanes, + "speed": end_speed_limit + }] + return types def specify_routes(self, net_params): @@ -123,14 +153,51 @@ def specify_routes(self, net_params): for i in range(num_edges): rts["highway_{}".format(i)] = ["highway_{}".format(j) for j in range(i, num_edges)] + if self.net_params.additional_params["use_ghost_edge"]: + rts["highway_{}".format(i)].append("highway_end") return rts def specify_edge_starts(self): """See parent class.""" - edgestarts = [("highway_{}".format(i), 0) - for i in range(self.num_edges)] - return edgestarts + junction_length = 0.1 + length = self.net_params.additional_params["length"] + num_edges = self.net_params.additional_params.get("num_edges", 1) + + # Add the main edges. + edge_starts = [ + ("highway_{}".format(i), + i * (length / num_edges + junction_length)) + for i in range(num_edges) + ] + + if self.net_params.additional_params["use_ghost_edge"]: + edge_starts += [ + ("highway_end", length + num_edges * junction_length) + ] + + return edge_starts + + def specify_internal_edge_starts(self): + """See parent class.""" + junction_length = 0.1 + length = self.net_params.additional_params["length"] + num_edges = self.net_params.additional_params.get("num_edges", 1) + + # Add the junctions. + edge_starts = [ + (":edge_{}".format(i + 1), + (i + 1) * length / num_edges + i * junction_length) + for i in range(num_edges - 1) + ] + + if self.net_params.additional_params["use_ghost_edge"]: + edge_starts += [ + (":edge_{}".format(num_edges), + length + (num_edges - 1) * junction_length) + ] + + return edge_starts @staticmethod def gen_custom_start_pos(cls, net_params, initial_config, num_vehicles): diff --git a/tests/fast_tests/test_controllers.py b/tests/fast_tests/test_controllers.py index 76146dbe6..58967cef8 100644 --- a/tests/fast_tests/test_controllers.py +++ b/tests/fast_tests/test_controllers.py @@ -8,7 +8,7 @@ from flow.controllers.routing_controllers import ContinuousRouter from flow.controllers.car_following_models import IDMController, \ OVMController, BCMController, LinearOVM, CFMController, LACController, \ - GippsController + GippsController, BandoFTLController from flow.controllers import FollowerStopper, PISaturation, NonLocalFollowerStopper from tests.setup_scripts import ring_road_exp_setup import os @@ -709,7 +709,7 @@ def test_get_action(self): np.testing.assert_array_almost_equal(requested_accel, expected_accel) -class TestGippsontroller(unittest.TestCase): +class TestGippsController(unittest.TestCase): """ Tests that the Gipps Controller returning mathematically accurate values. """ @@ -765,5 +765,59 @@ def test_get_action(self): np.testing.assert_array_almost_equal(requested_accel, expected_accel) +class TestBandoFTLController(unittest.TestCase): + """ + Tests that the Bando Controller returning mathematically accurate values. + """ + + def setUp(self): + # add a few vehicles to the network using the requested model + # also make sure that the input params are what is expected + contr_params = { + "alpha": .5, + "beta": 20, + "h_st": 2, + "h_go": 10, + "v_max": 32, + "want_max_accel": False, + } + + vehicles = VehicleParams() + vehicles.add( + veh_id="test", + acceleration_controller=(BandoFTLController, contr_params), + routing_controller=(ContinuousRouter, {}), + car_following_params=SumoCarFollowingParams( + accel=15, decel=5), + num_vehicles=5) + + # create the environment and network classes for a ring road + self.env, _, _ = ring_road_exp_setup(vehicles=vehicles) + + def tearDown(self): + # terminate the traci instance + self.env.terminate() + + # free data used by the class + self.env = None + + def test_get_action(self): + self.env.reset() + ids = self.env.k.vehicle.get_ids() + + test_headways = [2, 4, 6, 8, 10] + for i, veh_id in enumerate(ids): + self.env.k.vehicle.set_headway(veh_id, test_headways[i]) + + requested_accel = [ + self.env.k.vehicle.get_acc_controller(veh_id).get_action(self.env) + for veh_id in ids + ] + + expected_accel = [1.649129, 7.853475, 14.057821, 15.70695, 15.959713] + + np.testing.assert_array_almost_equal(requested_accel, expected_accel) + + if __name__ == '__main__': unittest.main() diff --git a/tests/fast_tests/test_examples.py b/tests/fast_tests/test_examples.py index a05fed68e..336c17bf8 100644 --- a/tests/fast_tests/test_examples.py +++ b/tests/fast_tests/test_examples.py @@ -42,6 +42,7 @@ from examples.exp_configs.non_rl.minicity import flow_params as non_rl_minicity from examples.exp_configs.non_rl.ring import flow_params as non_rl_ring from examples.exp_configs.non_rl.i210_subnetwork import flow_params as non_rl_i210 +from examples.exp_configs.non_rl.highway_single import flow_params as non_rl_highway_single os.environ['TEST_FLAG'] = 'True' os.environ['KMP_DUPLICATE_LIB_OK'] = 'True' @@ -110,6 +111,10 @@ def test_i210(self): """Verify that examples/exp_configs/non_rl/i210_subnetwork.py is working.""" self.run_simulation(non_rl_i210) + def test_highway_single(self): + """Verify that examples/exp_configs/non_rl/highway_single.py is working.""" + self.run_simulation(non_rl_highway_single) + @staticmethod def run_simulation(flow_params): # make the horizon small and set render to False diff --git a/tests/fast_tests/test_scenarios.py b/tests/fast_tests/test_scenarios.py index f9dd47c04..d72a50b17 100644 --- a/tests/fast_tests/test_scenarios.py +++ b/tests/fast_tests/test_scenarios.py @@ -5,6 +5,7 @@ from flow.networks import BottleneckNetwork, FigureEightNetwork, \ TrafficLightGridNetwork, HighwayNetwork, RingNetwork, MergeNetwork, \ MiniCityNetwork, MultiRingNetwork +from tests.setup_scripts import highway_exp_setup __all__ = [ "MultiRingNetwork", "MiniCityNetwork" @@ -94,11 +95,69 @@ def test_additional_net_params(self): "length": 1000, "lanes": 4, "speed_limit": 30, - "num_edges": 1 + "num_edges": 1, + "use_ghost_edge": False, + "ghost_speed_limit": 25 } ) ) + def test_ghost_edge(self): + """Validate the functionality of the ghost edge feature.""" + # =================================================================== # + # Without a ghost edge # + # =================================================================== # + + # create the network + env, _, _ = highway_exp_setup( + net_params=NetParams(additional_params={ + "length": 1000, + "lanes": 4, + "speed_limit": 30, + "num_edges": 1, + "use_ghost_edge": False, + "ghost_speed_limit": 25 + }) + ) + env.reset() + + # check the network length + self.assertEqual(env.k.network.length(), 1000) + + # check the edge list + self.assertEqual(env.k.network.get_edge_list(), ["highway_0"]) + + # check the speed limits of the edges + self.assertEqual(env.k.network.speed_limit("highway_0"), 30) + + # =================================================================== # + # With a ghost edge # + # =================================================================== # + + # create the network + env, _, _ = highway_exp_setup( + net_params=NetParams(additional_params={ + "length": 1000, + "lanes": 4, + "speed_limit": 30, + "num_edges": 1, + "use_ghost_edge": True, + "ghost_speed_limit": 25 + }) + ) + env.reset() + + # check the network length + self.assertEqual(env.k.network.length(), 1500.1) + + # check the edge list + self.assertEqual(env.k.network.get_edge_list(), + ["highway_0", "highway_end"]) + + # check the speed limits of the edges + self.assertEqual(env.k.network.speed_limit("highway_0"), 30) + self.assertEqual(env.k.network.speed_limit("highway_end"), 25) + class TestRingNetwork(unittest.TestCase): diff --git a/tests/fast_tests/test_vehicles.py b/tests/fast_tests/test_vehicles.py index 485a6a072..b791bba64 100644 --- a/tests/fast_tests/test_vehicles.py +++ b/tests/fast_tests/test_vehicles.py @@ -258,7 +258,9 @@ def test_no_junctions_highway(self): "lanes": 3, "speed_limit": 30, "resolution": 40, - "num_edges": 1 + "num_edges": 1, + "use_ghost_edge": False, + "ghost_speed_limit": 25, } net_params = NetParams(additional_params=additional_net_params) vehicles = VehicleParams() @@ -330,7 +332,9 @@ def test_no_junctions_highway(self): "lanes": 4, "speed_limit": 30, "resolution": 40, - "num_edges": 1 + "num_edges": 1, + "use_ghost_edge": False, + "ghost_speed_limit": 25, } net_params = NetParams(additional_params=additional_net_params) vehicles = VehicleParams() @@ -398,7 +402,9 @@ def test_no_junctions_highway(self): "lanes": 3, "speed_limit": 30, "resolution": 40, - "num_edges": 3 + "num_edges": 3, + "use_ghost_edge": False, + "ghost_speed_limit": 25, } net_params = NetParams(additional_params=additional_net_params) vehicles = VehicleParams() @@ -465,7 +471,9 @@ def test_no_junctions_highway(self): "lanes": 3, "speed_limit": 30, "resolution": 40, - "num_edges": 3 + "num_edges": 3, + "use_ghost_edge": False, + "ghost_speed_limit": 25, } net_params = NetParams(additional_params=additional_net_params) vehicles = VehicleParams() diff --git a/tests/setup_scripts.py b/tests/setup_scripts.py index 08d5b2c1e..ac88d2e42 100644 --- a/tests/setup_scripts.py +++ b/tests/setup_scripts.py @@ -343,7 +343,9 @@ def highway_exp_setup(sim_params=None, "lanes": 1, "speed_limit": 30, "resolution": 40, - "num_edges": 1 + "num_edges": 1, + "use_ghost_edge": False, + "ghost_speed_limit": 25, } net_params = NetParams(additional_params=additional_net_params) From a26c9b7fe7d75f7b13cab40bf99f85c8e7d930b9 Mon Sep 17 00:00:00 2001 From: Eugene Vinitsky Date: Sun, 3 May 2020 23:47:51 -0700 Subject: [PATCH 62/86] Benchmark fix (#919) * Add the appropriate reward to the grid benchmark back * Put the bottleneck in a congested regime * Bump bottleneck inflows to put it in the congested regime --- flow/benchmarks/README.md | 6 +++--- flow/benchmarks/bottleneck0.py | 2 +- flow/benchmarks/bottleneck1.py | 2 +- flow/benchmarks/bottleneck2.py | 2 +- flow/benchmarks/grid0.py | 4 ++-- flow/benchmarks/grid1.py | 4 ++-- flow/envs/__init__.py | 3 ++- flow/envs/traffic_light_grid.py | 11 +++++++++++ 8 files changed, 23 insertions(+), 11 deletions(-) diff --git a/flow/benchmarks/README.md b/flow/benchmarks/README.md index 963ad5b70..bbcba9414 100644 --- a/flow/benchmarks/README.md +++ b/flow/benchmarks/README.md @@ -38,12 +38,12 @@ inflow = 300 veh/hour/lane S=(915,), A=(25,), T=400. this problem is to learn to avoid the *capacity drop* that is characteristic to bottleneck structures in transportation networks, and maximize the total outflow in a mixed-autonomy setting. -- `flow.benchmarks.bottleneck0` 4 lanes, inflow = 1900 veh/hour, 10% CAV +- `flow.benchmarks.bottleneck0` 4 lanes, inflow = 2500 veh/hour, 10% CAV penetration, no vehicles are allowed to lane change, S=(141,), A=(20,), T=1000. -- `flow.benchmarks.bottleneck1` 4 lanes, inflow = 1900 veh/hour, 10% CAV +- `flow.benchmarks.bottleneck1` 4 lanes, inflow = 2500 veh/hour, 10% CAV penetration, the human drivers follow the standard lane changing model in the simulator, S=(141,), A=(20,), T=1000. -- `flow.benchmarks.bottleneck2` 8 lanes, inflow = 3800 veh/hour, 10% CAV +- `flow.benchmarks.bottleneck2` 8 lanes, inflow = 5000 veh/hour, 10% CAV penetration, no vehicles are allowed to lane change, S=(281,), A=(40,), T=1000. ## Training on Custom Algorithms diff --git a/flow/benchmarks/bottleneck0.py b/flow/benchmarks/bottleneck0.py index b0e86844c..b07947ad7 100644 --- a/flow/benchmarks/bottleneck0.py +++ b/flow/benchmarks/bottleneck0.py @@ -66,7 +66,7 @@ } # flow rate -flow_rate = 2000 * SCALING +flow_rate = 2500 * SCALING # percentage of flow coming out of each lane inflow = InFlows() diff --git a/flow/benchmarks/bottleneck1.py b/flow/benchmarks/bottleneck1.py index 26ae6527a..9c8d9c192 100644 --- a/flow/benchmarks/bottleneck1.py +++ b/flow/benchmarks/bottleneck1.py @@ -66,7 +66,7 @@ } # flow rate -flow_rate = 2000 * SCALING +flow_rate = 2500 * SCALING # percentage of flow coming out of each lane inflow = InFlows() diff --git a/flow/benchmarks/bottleneck2.py b/flow/benchmarks/bottleneck2.py index 5052b3b88..4651d448b 100644 --- a/flow/benchmarks/bottleneck2.py +++ b/flow/benchmarks/bottleneck2.py @@ -66,7 +66,7 @@ } # flow rate -flow_rate = 2000 * SCALING +flow_rate = 2500 * SCALING # percentage of flow coming out of each lane inflow = InFlows() diff --git a/flow/benchmarks/grid0.py b/flow/benchmarks/grid0.py index 1655c3b3c..5c4ee5349 100644 --- a/flow/benchmarks/grid0.py +++ b/flow/benchmarks/grid0.py @@ -4,7 +4,7 @@ - **Observation Dimension**: (339, ) - **Horizon**: 400 steps """ -from flow.envs import TrafficLightGridPOEnv +from flow.envs import TrafficLightGridBenchmarkEnv from flow.networks import TrafficLightGridNetwork from flow.core.params import SumoParams, EnvParams, InitialConfig, NetParams, \ InFlows, SumoCarFollowingParams @@ -68,7 +68,7 @@ exp_tag="grid_0", # name of the flow environment the experiment is running on - env_name=TrafficLightGridPOEnv, + env_name=TrafficLightGridBenchmarkEnv, # name of the network class the experiment is running on network=TrafficLightGridNetwork, diff --git a/flow/benchmarks/grid1.py b/flow/benchmarks/grid1.py index ec2a27454..83055adfd 100644 --- a/flow/benchmarks/grid1.py +++ b/flow/benchmarks/grid1.py @@ -4,7 +4,7 @@ - **Observation Dimension**: (915, ) - **Horizon**: 400 steps """ -from flow.envs import TrafficLightGridPOEnv +from flow.envs import TrafficLightGridBenchmarkEnv from flow.networks import TrafficLightGridNetwork from flow.core.params import SumoParams, EnvParams, InitialConfig, NetParams, \ InFlows, SumoCarFollowingParams @@ -68,7 +68,7 @@ exp_tag="grid_1", # name of the flow environment the experiment is running on - env_name=TrafficLightGridPOEnv, + env_name=TrafficLightGridBenchmarkEnv, # name of the network class the experiment is running on network=TrafficLightGridNetwork, diff --git a/flow/envs/__init__.py b/flow/envs/__init__.py index 5befe6a33..611ed3d9a 100755 --- a/flow/envs/__init__.py +++ b/flow/envs/__init__.py @@ -4,7 +4,7 @@ from flow.envs.bottleneck import BottleneckAccelEnv, BottleneckEnv, \ BottleneckDesiredVelocityEnv from flow.envs.traffic_light_grid import TrafficLightGridEnv, \ - TrafficLightGridPOEnv, TrafficLightGridTestEnv + TrafficLightGridPOEnv, TrafficLightGridTestEnv, TrafficLightGridBenchmarkEnv from flow.envs.ring.lane_change_accel import LaneChangeAccelEnv, \ LaneChangeAccelPOEnv from flow.envs.ring.accel import AccelEnv @@ -33,6 +33,7 @@ 'WaveAttenuationPOEnv', 'TrafficLightGridEnv', 'TrafficLightGridPOEnv', + 'TrafficLightGridBenchmarkEnv', 'BottleneckDesiredVelocityEnv', 'TestEnv', 'BayBridgeEnv', diff --git a/flow/envs/traffic_light_grid.py b/flow/envs/traffic_light_grid.py index 53391a329..8be0cb8a5 100644 --- a/flow/envs/traffic_light_grid.py +++ b/flow/envs/traffic_light_grid.py @@ -731,6 +731,17 @@ def additional_command(self): [self.k.vehicle.set_observed(veh_id) for veh_id in self.observed_ids] +class TrafficLightGridBenchmarkEnv(TrafficLightGridPOEnv): + """Class used for the benchmarks in `Benchmarks for reinforcement learning inmixed-autonomy traffic`.""" + + def compute_reward(self, rl_actions, **kwargs): + """See class definition.""" + if self.env_params.evaluate: + return - rewards.min_delay_unscaled(self) + else: + return rewards.desired_velocity(self) + + class TrafficLightGridTestEnv(TrafficLightGridEnv): """ Class for use in testing. From 50be2d074027fb465fc4a9103b3cc09fb1123ede Mon Sep 17 00:00:00 2001 From: Yashar Zeinali Farid <34227133+Yasharzf@users.noreply.github.com> Date: Thu, 7 May 2020 23:51:53 -0700 Subject: [PATCH 63/86] get not departed vehicles (#922) * added function to kernel/vehicle to get number of not departed vehiles * fixed over indentation of the docstring * indentation edit * pep8 Co-authored-by: AboudyKreidieh --- flow/core/kernel/simulation/traci.py | 10 +++++--- flow/core/kernel/vehicle/base.py | 7 ++++++ flow/core/kernel/vehicle/traci.py | 37 ++++++++++++++++++++++------ 3 files changed, 43 insertions(+), 11 deletions(-) diff --git a/flow/core/kernel/simulation/traci.py b/flow/core/kernel/simulation/traci.py index 0ee29ada6..35b3c2612 100644 --- a/flow/core/kernel/simulation/traci.py +++ b/flow/core/kernel/simulation/traci.py @@ -46,9 +46,13 @@ def pass_api(self, kernel_api): # subscribe some simulation parameters needed to check for entering, # exiting, and colliding vehicles self.kernel_api.simulation.subscribe([ - tc.VAR_DEPARTED_VEHICLES_IDS, tc.VAR_ARRIVED_VEHICLES_IDS, - tc.VAR_TELEPORT_STARTING_VEHICLES_IDS, tc.VAR_TIME_STEP, - tc.VAR_DELTA_T + tc.VAR_DEPARTED_VEHICLES_IDS, + tc.VAR_ARRIVED_VEHICLES_IDS, + tc.VAR_TELEPORT_STARTING_VEHICLES_IDS, + tc.VAR_TIME_STEP, + tc.VAR_DELTA_T, + tc.VAR_LOADED_VEHICLES_NUMBER, + tc.VAR_DEPARTED_VEHICLES_NUMBER ]) def simulation_step(self): diff --git a/flow/core/kernel/vehicle/base.py b/flow/core/kernel/vehicle/base.py index d9fc773cd..c68d68c3a 100644 --- a/flow/core/kernel/vehicle/base.py +++ b/flow/core/kernel/vehicle/base.py @@ -290,6 +290,13 @@ def get_departed_ids(self): """Return the ids of vehicles that departed in the last time step.""" raise NotImplementedError + def get_num_not_departed(self): + """Return the number of vehicles not departed in the last time step. + + This includes vehicles that were loaded but not departed. + """ + raise NotImplementedError + def get_speed(self, veh_id, error=-1001): """Return the speed of the specified vehicle. diff --git a/flow/core/kernel/vehicle/traci.py b/flow/core/kernel/vehicle/traci.py index 50cd106c9..41b5093b2 100644 --- a/flow/core/kernel/vehicle/traci.py +++ b/flow/core/kernel/vehicle/traci.py @@ -22,7 +22,8 @@ STEPS = 10 rdelta = 255 / STEPS # smoothly go from red to green as the speed increases -color_bins = [[int(255 - rdelta * i), int(rdelta * i), 0] for i in range(STEPS + 1)] +color_bins = [[int(255 - rdelta * i), int(rdelta * i), 0] for i in + range(STEPS + 1)] class TraCIVehicle(KernelVehicle): @@ -56,6 +57,8 @@ def __init__(self, self.num_vehicles = 0 # number of rl vehicles in the network self.num_rl_vehicles = 0 + # number of vehicles loaded but not departed vehicles + self.num_not_departed = 0 # contains the parameters associated with each type of vehicle self.type_parameters = {} @@ -101,6 +104,7 @@ def initialize(self, vehicles): self.minGap = vehicles.minGap self.num_vehicles = 0 self.num_rl_vehicles = 0 + self.num_not_departed = 0 self.__vehicles.clear() for typ in vehicles.initial: @@ -183,11 +187,12 @@ def update(self, reset): self._departed_ids.clear() self._arrived_ids.clear() self._arrived_rl_ids.clear() + self.num_not_departed = 0 # add vehicles from a network template, if applicable if hasattr(self.master_kernel.network.network, "template_vehicles"): - for veh_id in self.master_kernel.network.network.\ + for veh_id in self.master_kernel.network.network. \ template_vehicles: vals = deepcopy(self.master_kernel.network.network. template_vehicles[veh_id]) @@ -212,6 +217,10 @@ def update(self, reset): self._departed_ids.append(sim_obs[tc.VAR_DEPARTED_VEHICLES_IDS]) self._arrived_ids.append(sim_obs[tc.VAR_ARRIVED_VEHICLES_IDS]) + # update the number of not departed vehicles + self.num_not_departed += sim_obs[tc.VAR_LOADED_VEHICLES_NUMBER] - \ + sim_obs[tc.VAR_DEPARTED_VEHICLES_NUMBER] + # update the "headway", "leader", and "follower" variables for veh_id in self.__ids: try: @@ -321,8 +330,12 @@ def _add_departed(self, veh_id, veh_type): # subscribe the new vehicle self.kernel_api.vehicle.subscribe(veh_id, [ - tc.VAR_LANE_INDEX, tc.VAR_LANEPOSITION, tc.VAR_ROAD_ID, - tc.VAR_SPEED, tc.VAR_EDGES, tc.VAR_POSITION, tc.VAR_ANGLE, + tc.VAR_LANE_INDEX, tc.VAR_LANEPOSITION, + tc.VAR_ROAD_ID, + tc.VAR_SPEED, + tc.VAR_EDGES, + tc.VAR_POSITION, + tc.VAR_ANGLE, tc.VAR_SPEED_WITHOUT_TRACI ]) self.kernel_api.vehicle.subscribeLeader(veh_id, 2000) @@ -523,6 +536,10 @@ def get_departed_ids(self): else: return 0 + def get_num_not_departed(self): + """See parent class.""" + return self.num_not_departed + def get_previous_speed(self, veh_id, error=-1001): """See parent class.""" if isinstance(veh_id, (list, np.ndarray)): @@ -1007,7 +1024,8 @@ def update_vehicle_colors(self): for veh_id in self.get_rl_ids(): try: # If vehicle is already being colored via argument to vehicles.add(), don't re-color it. - if self._force_color_update or 'color' not in self.type_parameters[self.get_type(veh_id)]: + if self._force_color_update or 'color' not in \ + self.type_parameters[self.get_type(veh_id)]: # color rl vehicles red self.set_color(veh_id=veh_id, color=RED) except (FatalTraCIError, TraCIException) as e: @@ -1018,7 +1036,8 @@ def update_vehicle_colors(self): try: color = CYAN if veh_id in self.get_observed_ids() else WHITE # If vehicle is already being colored via argument to vehicles.add(), don't re-color it. - if self._force_color_update or 'color' not in self.type_parameters[self.get_type(veh_id)]: + if self._force_color_update or 'color' not in \ + self.type_parameters[self.get_type(veh_id)]: self.set_color(veh_id=veh_id, color=color) except (FatalTraCIError, TraCIException) as e: print('Error when updating human vehicle colors:', e) @@ -1028,7 +1047,8 @@ def update_vehicle_colors(self): if 'av' in veh_id: color = RED # If vehicle is already being colored via argument to vehicles.add(), don't re-color it. - if self._force_color_update or 'color' not in self.type_parameters[self.get_type(veh_id)]: + if self._force_color_update or 'color' not in \ + self.type_parameters[self.get_type(veh_id)]: self.set_color(veh_id=veh_id, color=color) except (FatalTraCIError, TraCIException) as e: print('Error when updating human vehicle colors:', e) @@ -1041,7 +1061,8 @@ def update_vehicle_colors(self): veh_speed = self.get_speed(veh_id) bin_index = np.digitize(veh_speed, speed_ranges) # If vehicle is already being colored via argument to vehicles.add(), don't re-color it. - if self._force_color_update or 'color' not in self.type_parameters[self.get_type(veh_id)]: + if self._force_color_update or 'color' not in \ + self.type_parameters[self.get_type(veh_id)]: self.set_color(veh_id=veh_id, color=color_bins[bin_index]) # clear the list of observed vehicles From 7398f5014ad990dd7539c52be32a8ca68edb0c8f Mon Sep 17 00:00:00 2001 From: Yashar Zeinali Farid <34227133+Yasharzf@users.noreply.github.com> Date: Sat, 9 May 2020 15:31:44 -0700 Subject: [PATCH 64/86] changed _departed_ids, and _arrived_ids in the update function (#926) * changed _departed_ids, and _arrived_ids in the update function * fixed bug in get_departed_ids and get_arrived_ids --- flow/core/kernel/simulation/traci.py | 3 ++- flow/core/kernel/vehicle/traci.py | 27 ++++++++++----------------- 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/flow/core/kernel/simulation/traci.py b/flow/core/kernel/simulation/traci.py index 35b3c2612..2cd109024 100644 --- a/flow/core/kernel/simulation/traci.py +++ b/flow/core/kernel/simulation/traci.py @@ -52,7 +52,8 @@ def pass_api(self, kernel_api): tc.VAR_TIME_STEP, tc.VAR_DELTA_T, tc.VAR_LOADED_VEHICLES_NUMBER, - tc.VAR_DEPARTED_VEHICLES_NUMBER + tc.VAR_DEPARTED_VEHICLES_NUMBER, + tc.VAR_ARRIVED_VEHICLES_NUMBER ]) def simulation_step(self): diff --git a/flow/core/kernel/vehicle/traci.py b/flow/core/kernel/vehicle/traci.py index 41b5093b2..d165dbdea 100644 --- a/flow/core/kernel/vehicle/traci.py +++ b/flow/core/kernel/vehicle/traci.py @@ -71,11 +71,11 @@ def __init__(self, # number of vehicles that entered the network for every time-step self._num_departed = [] - self._departed_ids = [] + self._departed_ids = 0 # number of vehicles to exit the network for every time-step self._num_arrived = [] - self._arrived_ids = [] + self._arrived_ids = 0 self._arrived_rl_ids = [] # whether or not to automatically color vehicles @@ -184,8 +184,8 @@ def update(self, reset): self.prev_last_lc[veh_id] = -float("inf") self._num_departed.clear() self._num_arrived.clear() - self._departed_ids.clear() - self._arrived_ids.clear() + self._departed_ids = 0 + self._arrived_ids = 0 self._arrived_rl_ids.clear() self.num_not_departed = 0 @@ -211,11 +211,10 @@ def update(self, reset): self.__vehicles[veh_id]["last_lc"] = self.time_counter # updated the list of departed and arrived vehicles - self._num_departed.append( - len(sim_obs[tc.VAR_DEPARTED_VEHICLES_IDS])) - self._num_arrived.append(len(sim_obs[tc.VAR_ARRIVED_VEHICLES_IDS])) - self._departed_ids.append(sim_obs[tc.VAR_DEPARTED_VEHICLES_IDS]) - self._arrived_ids.append(sim_obs[tc.VAR_ARRIVED_VEHICLES_IDS]) + self._num_departed.append(sim_obs[tc.VAR_LOADED_VEHICLES_NUMBER]) + self._num_arrived.append(sim_obs[tc.VAR_ARRIVED_VEHICLES_NUMBER]) + self._departed_ids = sim_obs[tc.VAR_DEPARTED_VEHICLES_IDS] + self._arrived_ids = sim_obs[tc.VAR_ARRIVED_VEHICLES_IDS] # update the number of not departed vehicles self.num_not_departed += sim_obs[tc.VAR_LOADED_VEHICLES_NUMBER] - \ @@ -517,10 +516,7 @@ def get_num_arrived(self): def get_arrived_ids(self): """See parent class.""" - if len(self._arrived_ids) > 0: - return self._arrived_ids[-1] - else: - return 0 + return self._arrived_ids def get_arrived_rl_ids(self): """See parent class.""" @@ -531,10 +527,7 @@ def get_arrived_rl_ids(self): def get_departed_ids(self): """See parent class.""" - if len(self._departed_ids) > 0: - return self._departed_ids[-1] - else: - return 0 + return self._departed_ids def get_num_not_departed(self): """See parent class.""" From 3154f2659246bc0f1c74502d4db98df338dfa108 Mon Sep 17 00:00:00 2001 From: Eugene Vinitsky Date: Mon, 18 May 2020 13:07:30 -0400 Subject: [PATCH 65/86] Add MPG reward (#931) Add an MPG reward --- flow/core/kernel/vehicle/base.py | 14 ++++ flow/core/kernel/vehicle/traci.py | 12 +++- flow/core/rewards.py | 106 ++++++++++++++++++++++++++++++ 3 files changed, 131 insertions(+), 1 deletion(-) diff --git a/flow/core/kernel/vehicle/base.py b/flow/core/kernel/vehicle/base.py index c68d68c3a..706504027 100644 --- a/flow/core/kernel/vehicle/base.py +++ b/flow/core/kernel/vehicle/base.py @@ -297,6 +297,20 @@ def get_num_not_departed(self): """ raise NotImplementedError + def get_fuel_consumption(selfself, veh_id, error=-1001): + """Return the mpg / s of the specified vehicle. + + Parameters + ---------- + veh_id : str or list of str + vehicle id, or list of vehicle ids + error : any, optional + value that is returned if the vehicle is not found + Returns + ------- + float + """ + def get_speed(self, veh_id, error=-1001): """Return the speed of the specified vehicle. diff --git a/flow/core/kernel/vehicle/traci.py b/flow/core/kernel/vehicle/traci.py index d165dbdea..134bac49f 100644 --- a/flow/core/kernel/vehicle/traci.py +++ b/flow/core/kernel/vehicle/traci.py @@ -335,7 +335,8 @@ def _add_departed(self, veh_id, veh_type): tc.VAR_EDGES, tc.VAR_POSITION, tc.VAR_ANGLE, - tc.VAR_SPEED_WITHOUT_TRACI + tc.VAR_SPEED_WITHOUT_TRACI, + tc.VAR_FUELCONSUMPTION ]) self.kernel_api.vehicle.subscribeLeader(veh_id, 2000) @@ -370,6 +371,8 @@ def _add_departed(self, veh_id, veh_type): self.kernel_api.vehicle.getLaneIndex(veh_id) self.__sumo_obs[veh_id][tc.VAR_SPEED] = \ self.kernel_api.vehicle.getSpeed(veh_id) + self.__sumo_obs[veh_id][tc.VAR_FUELCONSUMPTION] = \ + self.kernel_api.vehicle.getFuelConsumption(veh_id) # make sure that the order of rl_ids is kept sorted self.__rl_ids.sort() @@ -533,6 +536,13 @@ def get_num_not_departed(self): """See parent class.""" return self.num_not_departed + def get_fuel_consumption(self, veh_id, error=-1001): + """Return fuel consumption in gallons/s.""" + ml_to_gallons = 0.000264172 + if isinstance(veh_id, (list, np.ndarray)): + return [self.get_fuel_consumption(vehID, error) for vehID in veh_id] + return self.__sumo_obs.get(veh_id, {}).get(tc.VAR_FUELCONSUMPTION, error) * ml_to_gallons + def get_previous_speed(self, veh_id, error=-1001): """See parent class.""" if isinstance(veh_id, (list, np.ndarray)): diff --git a/flow/core/rewards.py b/flow/core/rewards.py index 6de472af2..3cca916f5 100755 --- a/flow/core/rewards.py +++ b/flow/core/rewards.py @@ -330,3 +330,109 @@ def energy_consumption(env, gain=.001): power += M * speed * accel + M * g * Cr * speed + 0.5 * rho * A * Ca * speed ** 3 return -gain * power + + +def veh_energy_consumption(env, veh_id, gain=.001): + """Calculate power consumption of a vehicle. + + Assumes vehicle is an average sized vehicle. + The power calculated here is the lower bound of the actual power consumed + by a vehicle. + """ + power = 0 + + M = 1200 # mass of average sized vehicle (kg) + g = 9.81 # gravitational acceleration (m/s^2) + Cr = 0.005 # rolling resistance coefficient + Ca = 0.3 # aerodynamic drag coefficient + rho = 1.225 # air density (kg/m^3) + A = 2.6 # vehicle cross sectional area (m^2) + speed = env.k.vehicle.get_speed(veh_id) + prev_speed = env.k.vehicle.get_previous_speed(veh_id) + + accel = abs(speed - prev_speed) / env.sim_step + + power += M * speed * accel + M * g * Cr * speed + 0.5 * rho * A * Ca * speed ** 3 + + return -gain * power + + +def miles_per_megajoule(env, veh_ids=None, gain=.001): + """Calculate miles per mega-joule of either a particular vehicle or the total average of all the vehicles. + + Assumes vehicle is an average sized vehicle. + The power calculated here is the lower bound of the actual power consumed + by a vehicle. + + Parameters + ---------- + env : flow.envs.Env + the environment variable, which contains information on the current + state of the system. + veh_ids : [list] + list of veh_ids to compute the reward over + gain : float + scaling factor for the reward + """ + mpj = 0 + counter = 0 + if veh_ids is None: + veh_ids = env.k.vehicle.get_ids() + elif not isinstance(veh_ids, list): + veh_ids = [veh_ids] + for veh_id in veh_ids: + speed = env.k.vehicle.get_speed(veh_id) + # convert to be positive since the function called is a penalty + power = -veh_energy_consumption(env, veh_id, gain=1.0) + if power > 0 and speed >= 0.0: + counter += 1 + # meters / joule is (v * \delta t) / (power * \delta t) + mpj += speed / power + if counter > 0: + mpj /= counter + + # convert from meters per joule to miles per joule + mpj /= 1609.0 + # convert from miles per joule to miles per megajoule + mpj *= 10**6 + + return mpj * gain + + +def miles_per_gallon(env, veh_ids=None, gain=.001): + """Calculate mpg of either a particular vehicle or the total average of all the vehicles. + + Assumes vehicle is an average sized vehicle. + The power calculated here is the lower bound of the actual power consumed + by a vehicle. + + Parameters + ---------- + env : flow.envs.Env + the environment variable, which contains information on the current + state of the system. + veh_ids : [list] + list of veh_ids to compute the reward over + gain : float + scaling factor for the reward + """ + mpg = 0 + counter = 0 + if veh_ids is None: + veh_ids = env.k.vehicle.get_ids() + elif not isinstance(veh_ids, list): + veh_ids = [veh_ids] + for veh_id in veh_ids: + speed = env.k.vehicle.get_speed(veh_id) + gallons_per_s = env.k.vehicle.get_fuel_consumption(veh_id) + if gallons_per_s > 0 and speed >= 0.0: + counter += 1 + # meters / gallon is (v * \delta t) / (gallons_per_s * \delta t) + mpg += speed / gallons_per_s + if counter > 0: + mpg /= counter + + # convert from meters per gallon to miles per gallon + mpg /= 1609.0 + + return mpg * gain From a524f4dabb9b56333e45fc10d53e1666dfa7fef0 Mon Sep 17 00:00:00 2001 From: Kathy Jang Date: Thu, 21 May 2020 12:37:05 -0700 Subject: [PATCH 66/86] Updated ray_autoscale and requirements.txt --- requirements.txt | 2 +- scripts/ray_autoscale.yaml | 13 +++++++++++-- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/requirements.txt b/requirements.txt index 546cb4e26..ccb971a99 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,5 @@ gym==0.14.0 -numpy==1.16.0 +numpy==1.18.4 scipy==1.1.0 lxml==4.4.1 pyprind==2.11.2 diff --git a/scripts/ray_autoscale.yaml b/scripts/ray_autoscale.yaml index 5bf2a9c4a..0800ce324 100644 --- a/scripts/ray_autoscale.yaml +++ b/scripts/ray_autoscale.yaml @@ -68,11 +68,20 @@ worker_nodes: setup_commands: - cd flow && git fetch && git checkout origin/master - -head_setup_commands: + - pip install ray==0.8.0 + - pip install tabulate - pip install boto3==1.10.45 # 1.4.8 adds InstanceMarketOptions - pip install awscli==1.16.309 + - pip install stable-baselines - pip install pytz + - pip install torch==1.3.1 + - pip install tensorflow==2.0.0 + - pip install lz4 + - pip install dm-tree + - pip install numpy==1.18.4 + - ./flow/scripts/setup_sumo_ubuntu1604.sh + +head_setup_commands: [] # Custom commands that will be run on worker nodes after common setup. worker_setup_commands: [] From 7f782b895027c265dc84f5d397c3edf99432969e Mon Sep 17 00:00:00 2001 From: Kathy Jang Date: Thu, 21 May 2020 13:21:11 -0700 Subject: [PATCH 67/86] Reverted to original master's ray_autoscale.yaml and added 2 lines --- scripts/ray_autoscale.yaml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/scripts/ray_autoscale.yaml b/scripts/ray_autoscale.yaml index 0800ce324..27ac0898e 100644 --- a/scripts/ray_autoscale.yaml +++ b/scripts/ray_autoscale.yaml @@ -68,16 +68,9 @@ worker_nodes: setup_commands: - cd flow && git fetch && git checkout origin/master - - pip install ray==0.8.0 - - pip install tabulate - pip install boto3==1.10.45 # 1.4.8 adds InstanceMarketOptions - pip install awscli==1.16.309 - - pip install stable-baselines - pip install pytz - - pip install torch==1.3.1 - - pip install tensorflow==2.0.0 - - pip install lz4 - - pip install dm-tree - pip install numpy==1.18.4 - ./flow/scripts/setup_sumo_ubuntu1604.sh From 2232861f572b1ff2f4644af3392f1eddddf5715b Mon Sep 17 00:00:00 2001 From: bill zhao Date: Fri, 22 May 2020 10:41:21 -0700 Subject: [PATCH 68/86] Address MacOS Catalina SegFault issue when running sumo-gui (#938) --- docs/source/flow_setup.rst | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/source/flow_setup.rst b/docs/source/flow_setup.rst index 60734b7b1..cbe585d36 100644 --- a/docs/source/flow_setup.rst +++ b/docs/source/flow_setup.rst @@ -112,6 +112,22 @@ If you are a Mac user and the above command gives you the error ``FXApp:openDisplay: unable to open display :0.0``, make sure to open the application XQuartz. +*Troubleshooting*: +If you are a Mac user and the above command gives you the error +``Segmentation fault: 11``, make sure to reinstall ``fox`` using brew. +:: + + # Uninstall Catalina bottle of fox: + $ brew uninstall --ignore-dependencies fox + + # Edit brew Formula of fox: + $ brew edit fox + + # Comment out or delete the following line: sha256 "c6697be294c9a0458580564d59f8db32791beb5e67a05a6246e0b969ffc068bc" => :catalina + # Install Mojave bottle of fox: + $ brew install fox + + Testing your SUMO and Flow installation ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ From 5e2f859bcf001254bd9deb476be4ca1535e2af4c Mon Sep 17 00:00:00 2001 From: Yashar Zeinali Farid <34227133+Yasharzf@users.noreply.github.com> Date: Tue, 9 Jun 2020 14:19:58 -0700 Subject: [PATCH 69/86] Aimsun bugs (#907) * added aimsunparams * fixed aimsun default template * fixed bug in network close function * fixed bug in simulate.py * minor * minor * bug fix * minor tests Co-authored-by: AboudyKreidieh --- examples/simulate.py | 15 ++++++++++---- examples/train.py | 2 +- flow/core/kernel/network/aimsun.py | 4 +++- flow/utils/aimsun/Aimsun_Flow.ang | Bin 0 -> 159038 bytes tests/fast_tests/test_examples.py | 31 +++++++++++++++++++++++++++++ 5 files changed, 46 insertions(+), 6 deletions(-) create mode 100644 flow/utils/aimsun/Aimsun_Flow.ang diff --git a/examples/simulate.py b/examples/simulate.py index 848f030a4..d1dcc5a79 100644 --- a/examples/simulate.py +++ b/examples/simulate.py @@ -8,6 +8,8 @@ import json import os from flow.core.experiment import Experiment + +from flow.core.params import AimsunParams from flow.utils.rllib import FlowParamsEncoder @@ -20,7 +22,6 @@ def parse_args(args): the output parser object """ parser = argparse.ArgumentParser( - formatter_class=argparse.RawDescriptionHelpFormatter, description="Parse argument used when running a Flow simulation.", epilog="python simulate.py EXP_CONFIG --num_runs INT --no_render") @@ -65,17 +66,23 @@ def parse_args(args): else: callables = None - # Update some variables based on inputs. flow_params['sim'].render = not flags.no_render flow_params['simulator'] = 'aimsun' if flags.aimsun else 'traci' + # If Aimsun is being called, replace SumoParams with AimsunParams. + if flags.aimsun: + sim_params = AimsunParams() + sim_params.__dict__.update(flow_params['sim'].__dict__) + flow_params['sim'] = sim_params + # Specify an emission path if they are meant to be generated. if flags.gen_emission: flow_params['sim'].emission_path = "./data" # Create the flow_params object - json_filename = flow_params['exp_tag'] - with open(os.path.join(flow_params['sim'].emission_path, json_filename) + '.json', 'w') as outfile: + fp_ = flow_params['exp_tag'] + dir_ = flow_params['sim'].emission_path + with open(os.path.join(dir_, "{}.json".format(fp_)), 'w') as outfile: json.dump(flow_params, outfile, cls=FlowParamsEncoder, sort_keys=True, indent=4) diff --git a/examples/train.py b/examples/train.py index 652d0efa5..1b2f22476 100644 --- a/examples/train.py +++ b/examples/train.py @@ -306,7 +306,7 @@ def train_h_baselines(flow_params, args, multiagent): # Perform training. alg.learn( - total_timesteps=args.total_steps, + total_steps=args.total_steps, log_dir=dir_name, log_interval=args.log_interval, eval_interval=args.eval_interval, diff --git a/flow/core/kernel/network/aimsun.py b/flow/core/kernel/network/aimsun.py index 0378d45a9..89971bf58 100644 --- a/flow/core/kernel/network/aimsun.py +++ b/flow/core/kernel/network/aimsun.py @@ -262,7 +262,9 @@ def close(self): cur_dir = os.path.join(config.PROJECT_PATH, 'flow/core/kernel/network') os.remove(os.path.join(cur_dir, 'data_%s.json' % self.sim_params.port)) - os.remove('%s_%s' % (self.network.net_params.template, self.sim_params.port)) + if self.network.net_params.template is not None: + os.remove('%s_%s' % (self.network.net_params.template, + self.sim_params.port)) ########################################################################### # State acquisition methods # diff --git a/flow/utils/aimsun/Aimsun_Flow.ang b/flow/utils/aimsun/Aimsun_Flow.ang new file mode 100644 index 0000000000000000000000000000000000000000..17119f4e95771d1699dd7052ac7eddbc651a1fab GIT binary patch literal 159038 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

=UIRENP@w7hYiPdEauHXgy`&7hGv$G1did|4M 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

(9CGdf=!(P97LQPW@H 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 N&1qb*{{;X5|NjZQ5%d56 literal 0 HcmV?d00001 diff --git a/tests/fast_tests/test_examples.py b/tests/fast_tests/test_examples.py index 336c17bf8..0b385f28a 100644 --- a/tests/fast_tests/test_examples.py +++ b/tests/fast_tests/test_examples.py @@ -26,6 +26,7 @@ flow_params as multiagent_traffic_light_grid from examples.exp_configs.rl.multiagent.multiagent_highway import flow_params as multiagent_highway +from examples.simulate import parse_args as parse_simulate_args from examples.train import parse_args as parse_train_args from examples.train import run_model_stablebaseline as run_stable_baselines_model from examples.train import setup_exps_rllib as setup_rllib_exps @@ -59,6 +60,36 @@ class TestNonRLExamples(unittest.TestCase): done to the functions within the experiment class. """ + def test_parse_args(self): + """Validate the functionality of the parse_args method in simulate.py.""" + # test the default case + args = parse_simulate_args(["exp_config"]) + + self.assertDictEqual(vars(args), { + 'aimsun': False, + 'exp_config': 'exp_config', + 'gen_emission': False, + 'no_render': False, + 'num_runs': 1 + }) + + # test the case when optional args are specified + args = parse_simulate_args([ + "exp_config", + '--aimsun', + '--gen_emission', + '--no_render', + '--num_runs', '2' + ]) + + self.assertDictEqual(vars(args), { + 'aimsun': True, + 'exp_config': 'exp_config', + 'gen_emission': True, + 'no_render': True, + 'num_runs': 2 + }) + def test_bottleneck(self): """Verify that examples/exp_configs/non_rl/bottleneck.py is working.""" self.run_simulation(non_rl_bottleneck) From e06b1c11c145b2351c4adaff1aecd25d31aaaeea Mon Sep 17 00:00:00 2001 From: liljonnystyle Date: Wed, 10 Jun 2020 09:44:45 -0700 Subject: [PATCH 70/86] Converting base classes to abstract base classes (#935) * convert base classes into abstract base classes and add abstractmethod decorators * fix typo * revert changes on this file * NotImplementedError no longer needed * allow gen_custom_start_pos() to be not abstract method * implement dummy get_accel() method for rl controller * add docstring * add docstring * add docstring * revert changes to base Network class * add dummy implementations of abstract methods * fix abstract base class tests * import new TestEnvs * fix import statement * change TestEnv instantiation assertion checks * fix typos * change base vehicle class to abc * resolve conflict and raise NotImplementedError in traci and aimsun class methods * fix styling * fix styling * fix merge duplication and excess whitespace * newline at end of file * added ignore to abstract methods * moved testing envs to the tests folder Co-authored-by: AboudyKreidieh --- .coveragerc | 2 + flow/controllers/base_controller.py | 6 +- .../base_lane_changing_controller.py | 7 +- flow/controllers/base_routing_controller.py | 7 +- flow/controllers/rlcontroller.py | 4 + flow/core/kernel/vehicle/aimsun.py | 52 +++++- flow/core/kernel/vehicle/base.py | 165 ++++++++++++------ flow/envs/base.py | 15 +- flow/envs/bay_bridge.py | 16 ++ flow/envs/bottleneck.py | 7 + .../fast_tests/test_environment_base_class.py | 78 +++++++-- 11 files changed, 274 insertions(+), 85 deletions(-) diff --git a/.coveragerc b/.coveragerc index 3505cadcb..846588761 100644 --- a/.coveragerc +++ b/.coveragerc @@ -19,5 +19,7 @@ exclude_lines = if __name__ == .__main__.: raise NotImplementedError @ray.remote + @abstractmethod def policy_mapping_fn* def main(args)* + pragma: no cover diff --git a/flow/controllers/base_controller.py b/flow/controllers/base_controller.py index 41780826b..cef92d573 100755 --- a/flow/controllers/base_controller.py +++ b/flow/controllers/base_controller.py @@ -1,9 +1,10 @@ """Contains the base acceleration controller class.""" +from abc import ABCMeta, abstractmethod import numpy as np -class BaseController: +class BaseController(metaclass=ABCMeta): """Base class for flow-controlled acceleration behavior. Instantiates a controller and forces the user to pass a @@ -63,9 +64,10 @@ def __init__(self, self.car_following_params = car_following_params + @abstractmethod def get_accel(self, env): """Return the acceleration of the controller.""" - raise NotImplementedError + pass def get_action(self, env): """Convert the get_accel() acceleration into an action. diff --git a/flow/controllers/base_lane_changing_controller.py b/flow/controllers/base_lane_changing_controller.py index af009f992..eb2b566f5 100755 --- a/flow/controllers/base_lane_changing_controller.py +++ b/flow/controllers/base_lane_changing_controller.py @@ -1,7 +1,9 @@ """Contains the base lane change controller class.""" +from abc import ABCMeta, abstractmethod -class BaseLaneChangeController: + +class BaseLaneChangeController(metaclass=ABCMeta): """Base class for lane-changing controllers. Instantiates a controller and forces the user to pass a @@ -36,6 +38,7 @@ def __init__(self, veh_id, lane_change_params=None): self.veh_id = veh_id self.lane_change_params = lane_change_params + @abstractmethod def get_lane_change_action(self, env): """Specify the lane change action to be performed. @@ -55,7 +58,7 @@ def get_lane_change_action(self, env): float or int requested lane change action """ - raise NotImplementedError + pass def get_action(self, env): """Return the action of the lane change controller. diff --git a/flow/controllers/base_routing_controller.py b/flow/controllers/base_routing_controller.py index b001dc62e..17048ce7d 100755 --- a/flow/controllers/base_routing_controller.py +++ b/flow/controllers/base_routing_controller.py @@ -1,7 +1,9 @@ """Contains the base routing controller class.""" +from abc import ABCMeta, abstractmethod -class BaseRouter: + +class BaseRouter(metaclass=ABCMeta): """Base class for routing controllers. These controllers are used to dynamically change the routes of vehicles @@ -30,6 +32,7 @@ def __init__(self, veh_id, router_params): self.veh_id = veh_id self.router_params = router_params + @abstractmethod def choose_route(self, env): """Return the routing method implemented by the controller. @@ -45,4 +48,4 @@ def choose_route(self, env): is returned, the vehicle performs no routing action in the current time step. """ - raise NotImplementedError + pass diff --git a/flow/controllers/rlcontroller.py b/flow/controllers/rlcontroller.py index 61f53f11a..973de8fc9 100755 --- a/flow/controllers/rlcontroller.py +++ b/flow/controllers/rlcontroller.py @@ -37,3 +37,7 @@ def __init__(self, veh_id, car_following_params): self, veh_id, car_following_params) + + def get_accel(self, env): + """Pass, as this is never called; required to override abstractmethod.""" + pass diff --git a/flow/core/kernel/vehicle/aimsun.py b/flow/core/kernel/vehicle/aimsun.py index 3320d1515..ce0d026e5 100644 --- a/flow/core/kernel/vehicle/aimsun.py +++ b/flow/core/kernel/vehicle/aimsun.py @@ -403,7 +403,7 @@ def add(self, veh_id, type_id, edge, pos, lane, speed): def reset(self): """See parent class.""" - pass + raise NotImplementedError def remove(self, aimsun_id): """See parent class.""" @@ -517,7 +517,7 @@ def choose_routes(self, veh_id, route_choices): edge the vehicle is currently on. If a value of None is provided, the vehicle does not update its route """ - pass # FIXME + raise NotImplementedError # FIXME # for i, veh_id in enumerate(veh_ids): # if route_choices[i] is not None: # aimsun_id = self._id_flow2aimsun[veh_id] @@ -525,6 +525,10 @@ def choose_routes(self, veh_id, route_choices): # self.kernel_api.AKIVehTrackedModifyNextSections( # aimsun_id, size_next_sections, route_choices[i]) + def set_max_speed(self, veh_id, max_speed): + """See parent class.""" + raise NotImplementedError + ########################################################################### # Methods to visually distinguish vehicles by {RL, observed, unobserved} # ########################################################################### @@ -560,10 +564,30 @@ def get_observed_ids(self): """Return the list of observed vehicles.""" return self.__observed_ids + def get_color(self, veh_id): + """See parent class.""" + raise NotImplementedError + + def set_color(self, veh_id, color): + """See parent class.""" + raise NotImplementedError + ########################################################################### # State acquisition methods # ########################################################################### + def get_orientation(self, veh_id): + """See parent class.""" + raise NotImplementedError + + def get_timestep(self, veh_id): + """See parent class.""" + raise NotImplementedError + + def get_timedelta(self, veh_id): + """See parent class.""" + raise NotImplementedError + def get_ids(self): """See parent class.""" return self.__ids @@ -611,6 +635,22 @@ def get_num_arrived(self): else: return 0 + def get_arrived_ids(self): + """See parent class.""" + raise NotImplementedError + + def get_departed_ids(self): + """See parent class.""" + raise NotImplementedError + + def get_num_not_departed(self): + """See parent class.""" + raise NotImplementedError + + def get_fuel_consumption(self): + """See parent class.""" + raise NotImplementedError + def get_type(self, veh_id): """See parent class.""" if isinstance(veh_id, (list, np.ndarray)): @@ -627,6 +667,10 @@ def get_speed(self, veh_id, error=-1001): return [self.get_speed(veh, error) for veh in veh_id] return self.__vehicles[veh_id]['tracking_info'].CurrentSpeed / 3.6 + def get_default_speed(self, veh_id, error=-1001): + """See parent class.""" + raise NotImplementedError + def get_position(self, veh_id, error=-1001): """See parent class.""" if isinstance(veh_id, (list, np.ndarray)): @@ -806,3 +850,7 @@ def get_lane_followers_speed(self, veh_id, error=None): def get_lane_leaders_speed(self, veh_id, error=None): """See parent class.""" raise NotImplementedError + + def get_max_speed(self, veh_id, error): + """See parent class.""" + raise NotImplementedError diff --git a/flow/core/kernel/vehicle/base.py b/flow/core/kernel/vehicle/base.py index 706504027..d97ade984 100644 --- a/flow/core/kernel/vehicle/base.py +++ b/flow/core/kernel/vehicle/base.py @@ -1,7 +1,9 @@ """Script containing the base vehicle kernel class.""" +from abc import ABCMeta, abstractmethod -class KernelVehicle(object): + +class KernelVehicle(object, metaclass=ABCMeta): """Flow vehicle kernel. This kernel sub-class is used to interact with the simulator with regards @@ -66,6 +68,7 @@ def pass_api(self, kernel_api): # Methods for interacting with the simulator # ########################################################################### + @abstractmethod def update(self, reset): """Update the vehicle kernel with data from the current time step. @@ -78,8 +81,9 @@ def update(self, reset): specifies whether the simulator was reset in the last simulation step """ - raise NotImplementedError + pass + @abstractmethod def add(self, veh_id, type_id, edge, pos, lane, speed): """Add a vehicle to the network. @@ -98,12 +102,14 @@ def add(self, veh_id, type_id, edge, pos, lane, speed): speed : float starting speed of the added vehicle """ - raise NotImplementedError + pass + @abstractmethod def reset(self): """Reset any additional state that needs to be reset.""" - raise NotImplementedError + pass + @abstractmethod def remove(self, veh_id): """Remove a vehicle. @@ -119,8 +125,9 @@ def remove(self, veh_id): veh_id : str unique identifier of the vehicle to be removed """ - raise NotImplementedError + pass + @abstractmethod def apply_acceleration(self, veh_id, acc): """Apply the acceleration requested by a vehicle in the simulator. @@ -131,8 +138,9 @@ def apply_acceleration(self, veh_id, acc): acc : float or array_like requested accelerations from the vehicles """ - raise NotImplementedError + pass + @abstractmethod def apply_lane_change(self, veh_id, direction): """Apply an instantaneous lane-change to a set of vehicles. @@ -155,8 +163,9 @@ def apply_lane_change(self, veh_id, direction): ValueError If any of the direction values are not -1, 0, or 1. """ - raise NotImplementedError + pass + @abstractmethod def choose_routes(self, veh_id, route_choices): """Update the route choice of vehicles in the network. @@ -169,8 +178,9 @@ def choose_routes(self, veh_id, route_choices): edge the vehicle is currently on. If a value of None is provided, the vehicle does not update its route """ - raise NotImplementedError + pass + @abstractmethod def set_max_speed(self, veh_id, max_speed): """Update the maximum allowable speed by a vehicles in the network. @@ -181,123 +191,146 @@ def set_max_speed(self, veh_id, max_speed): max_speed : float desired max speed by the vehicle """ - raise NotImplementedError + pass ########################################################################### # Methods to visually distinguish vehicles by {RL, observed, unobserved} # ########################################################################### + @abstractmethod def update_vehicle_colors(self): """Modify the color of vehicles if rendering is active.""" - raise NotImplementedError + pass + @abstractmethod def set_observed(self, veh_id): """Add a vehicle to the list of observed vehicles.""" - raise NotImplementedError + pass + @abstractmethod def remove_observed(self, veh_id): """Remove a vehicle from the list of observed vehicles.""" - raise NotImplementedError + pass + @abstractmethod def get_observed_ids(self): """Return the list of observed vehicles.""" - raise NotImplementedError + pass + @abstractmethod def get_color(self, veh_id): """Return and RGB tuple of the color of the specified vehicle.""" - raise NotImplementedError + pass + @abstractmethod def set_color(self, veh_id, color): """Set the color of the specified vehicle with the RGB tuple.""" - raise NotImplementedError + pass ########################################################################### # State acquisition methods # ########################################################################### + @abstractmethod def get_orientation(self, veh_id): """Return the orientation of the vehicle of veh_id.""" - raise NotImplementedError + pass + @abstractmethod def get_timestep(self, veh_id): """Return the time step of the vehicle of veh_id.""" - raise NotImplementedError + pass + @abstractmethod def get_timedelta(self, veh_id): """Return the simulation time delta of the vehicle of veh_id.""" - raise NotImplementedError + pass + @abstractmethod def get_type(self, veh_id): """Return the type of the vehicle of veh_id.""" - raise NotImplementedError + pass + @abstractmethod def get_ids(self): """Return the names of all vehicles currently in the network.""" - raise NotImplementedError + pass + @abstractmethod def get_human_ids(self): """Return the names of all non-rl vehicles currently in the network.""" - raise NotImplementedError + pass + @abstractmethod def get_controlled_ids(self): """Return the names of all flow acceleration-controlled vehicles. This only include vehicles that are currently in the network. """ - raise NotImplementedError + pass + @abstractmethod def get_controlled_lc_ids(self): """Return the names of all flow lane change-controlled vehicles. This only include vehicles that are currently in the network. """ - raise NotImplementedError + pass + @abstractmethod def get_rl_ids(self): """Return the names of all rl-controlled vehicles in the network.""" - raise NotImplementedError + pass + @abstractmethod def get_ids_by_edge(self, edges): """Return the names of all vehicles in the specified edge. If no vehicles are currently in the edge, then returns an empty list. """ - raise NotImplementedError + pass + @abstractmethod def get_inflow_rate(self, time_span): """Return the inflow rate (in veh/hr) of vehicles from the network. This value is computed over the specified **time_span** seconds. """ - raise NotImplementedError + pass + @abstractmethod def get_outflow_rate(self, time_span): """Return the outflow rate (in veh/hr) of vehicles from the network. This value is computed over the specified **time_span** seconds. """ - raise NotImplementedError + pass + @abstractmethod def get_num_arrived(self): """Return the number of vehicles that arrived in the last time step.""" - raise NotImplementedError + pass + @abstractmethod def get_arrived_ids(self): """Return the ids of vehicles that arrived in the last time step.""" - raise NotImplementedError + pass + @abstractmethod def get_departed_ids(self): """Return the ids of vehicles that departed in the last time step.""" - raise NotImplementedError + pass + @abstractmethod def get_num_not_departed(self): """Return the number of vehicles not departed in the last time step. This includes vehicles that were loaded but not departed. """ - raise NotImplementedError + pass - def get_fuel_consumption(selfself, veh_id, error=-1001): + @abstractmethod + def get_fuel_consumption(self, veh_id, error=-1001): """Return the mpg / s of the specified vehicle. Parameters @@ -306,11 +339,14 @@ def get_fuel_consumption(selfself, veh_id, error=-1001): vehicle id, or list of vehicle ids error : any, optional value that is returned if the vehicle is not found + Returns ------- float """ + pass + @abstractmethod def get_speed(self, veh_id, error=-1001): """Return the speed of the specified vehicle. @@ -325,8 +361,9 @@ def get_speed(self, veh_id, error=-1001): ------- float """ - raise NotImplementedError + pass + @abstractmethod def get_default_speed(self, veh_id, error=-1001): """Return the expected speed if no control were applied. @@ -341,8 +378,9 @@ def get_default_speed(self, veh_id, error=-1001): ------- float """ - raise NotImplementedError + pass + @abstractmethod def get_position(self, veh_id, error=-1001): """Return the position of the vehicle relative to its current edge. @@ -357,8 +395,9 @@ def get_position(self, veh_id, error=-1001): ------- float """ - raise NotImplementedError + pass + @abstractmethod def get_edge(self, veh_id, error=""): """Return the edge the specified vehicle is currently on. @@ -373,8 +412,9 @@ def get_edge(self, veh_id, error=""): ------- str """ - raise NotImplementedError + pass + @abstractmethod def get_lane(self, veh_id, error=-1001): """Return the lane index of the specified vehicle. @@ -389,8 +429,9 @@ def get_lane(self, veh_id, error=-1001): ------- int """ - raise NotImplementedError + pass + @abstractmethod def get_route(self, veh_id, error=list()): """Return the route of the specified vehicle. @@ -405,8 +446,9 @@ def get_route(self, veh_id, error=list()): ------- list of str """ - raise NotImplementedError + pass + @abstractmethod def get_length(self, veh_id, error=-1001): """Return the length of the specified vehicle. @@ -421,8 +463,9 @@ def get_length(self, veh_id, error=-1001): ------- float """ - raise NotImplementedError + pass + @abstractmethod def get_leader(self, veh_id, error=""): """Return the leader of the specified vehicle. @@ -437,8 +480,9 @@ def get_leader(self, veh_id, error=""): ------- str """ - raise NotImplementedError + pass + @abstractmethod def get_follower(self, veh_id, error=""): """Return the follower of the specified vehicle. @@ -453,8 +497,9 @@ def get_follower(self, veh_id, error=""): ------- str """ - raise NotImplementedError + pass + @abstractmethod def get_headway(self, veh_id, error=-1001): """Return the headway of the specified vehicle(s). @@ -469,8 +514,9 @@ def get_headway(self, veh_id, error=-1001): ------- float """ - raise NotImplementedError + pass + @abstractmethod def get_last_lc(self, veh_id, error=-1001): """Return the last time step a vehicle changed lanes. @@ -489,8 +535,9 @@ def get_last_lc(self, veh_id, error=-1001): ------- int """ - raise NotImplementedError + pass + @abstractmethod def get_acc_controller(self, veh_id, error=None): """Return the acceleration controller of the specified vehicle. @@ -505,8 +552,9 @@ def get_acc_controller(self, veh_id, error=None): ------- object """ - raise NotImplementedError + pass + @abstractmethod def get_lane_changing_controller(self, veh_id, error=None): """Return the lane changing controller of the specified vehicle. @@ -521,8 +569,9 @@ def get_lane_changing_controller(self, veh_id, error=None): ------- object """ - raise NotImplementedError + pass + @abstractmethod def get_routing_controller(self, veh_id, error=None): """Return the routing controller of the specified vehicle. @@ -537,8 +586,9 @@ def get_routing_controller(self, veh_id, error=None): ------- object """ - raise NotImplementedError + pass + @abstractmethod def get_lane_headways(self, veh_id, error=list()): """Return the lane headways of the specified vehicles. @@ -556,8 +606,9 @@ def get_lane_headways(self, veh_id, error=list()): ------- list of float """ - raise NotImplementedError + pass + @abstractmethod def get_lane_leaders_speed(self, veh_id, error=list()): """Return the speed of the leaders of the specified vehicles. @@ -577,8 +628,9 @@ def get_lane_leaders_speed(self, veh_id, error=list()): ------- list of float """ - raise NotImplementedError + pass + @abstractmethod def get_lane_followers_speed(self, veh_id, error=list()): """Return the speed of the followers of the specified vehicles. @@ -598,8 +650,9 @@ def get_lane_followers_speed(self, veh_id, error=list()): ------- list of float """ - raise NotImplementedError + pass + @abstractmethod def get_lane_leaders(self, veh_id, error=list()): """Return the leaders for the specified vehicle in all lanes. @@ -614,8 +667,9 @@ def get_lane_leaders(self, veh_id, error=list()): ------- lis of str """ - raise NotImplementedError + pass + @abstractmethod def get_lane_tailways(self, veh_id, error=list()): """Return the lane tailways of the specified vehicle. @@ -633,8 +687,9 @@ def get_lane_tailways(self, veh_id, error=list()): ------- list of float """ - raise NotImplementedError + pass + @abstractmethod def get_lane_followers(self, veh_id, error=list()): """Return the followers for the specified vehicle in all lanes. @@ -649,8 +704,9 @@ def get_lane_followers(self, veh_id, error=list()): ------- list of str """ - raise NotImplementedError + pass + @abstractmethod def get_x_by_id(self, veh_id): """Provide a 1-D representation of the position of a vehicle. @@ -667,8 +723,9 @@ def get_x_by_id(self, veh_id): ------- float """ - raise NotImplementedError + pass + @abstractmethod def get_max_speed(self, veh_id, error): """Return the max speed of the specified vehicle. @@ -683,4 +740,4 @@ def get_max_speed(self, veh_id, error): ------- float """ - raise NotImplementedError + pass diff --git a/flow/envs/base.py b/flow/envs/base.py index 1abb8a3c9..c4462e8c8 100644 --- a/flow/envs/base.py +++ b/flow/envs/base.py @@ -1,5 +1,6 @@ """Base environment class. This is the parent of all other environments.""" +from abc import ABCMeta, abstractmethod from copy import deepcopy import os import atexit @@ -26,7 +27,7 @@ from flow.utils.exceptions import FatalFlowError -class Env(gym.Env): +class Env(gym.Env, metaclass=ABCMeta): """Base environment class. Provides the interface for interacting with various aspects of a traffic @@ -614,9 +615,11 @@ def apply_rl_actions(self, rl_actions=None): rl_clipped = self.clip_actions(rl_actions) self._apply_rl_actions(rl_clipped) + @abstractmethod def _apply_rl_actions(self, rl_actions): - raise NotImplementedError + pass + @abstractmethod def get_state(self): """Return the state of the simulation as perceived by the RL agent. @@ -628,9 +631,10 @@ def get_state(self): information on the state of the vehicles, which is provided to the agent """ - raise NotImplementedError + pass @property + @abstractmethod def action_space(self): """Identify the dimensions and bounds of the action space. @@ -641,9 +645,10 @@ def action_space(self): gym Box or Tuple type a bounded box depicting the shape and bounds of the action space """ - raise NotImplementedError + pass @property + @abstractmethod def observation_space(self): """Identify the dimensions and bounds of the observation space. @@ -655,7 +660,7 @@ def observation_space(self): a bounded box depicting the shape and bounds of the observation space """ - raise NotImplementedError + pass def compute_reward(self, rl_actions, **kwargs): """Reward function for the RL agent(s). diff --git a/flow/envs/bay_bridge.py b/flow/envs/bay_bridge.py index dcc5e991d..0c5570367 100644 --- a/flow/envs/bay_bridge.py +++ b/flow/envs/bay_bridge.py @@ -234,6 +234,22 @@ def compute_reward(self, rl_actions, **kwargs): # The below methods need to be updated by child classes. # ########################################################################### + @property + def action_space(self): + """See parent class. + + To be implemented by child classes. + """ + pass + + @property + def observation_space(self): + """See parent class. + + To be implemented by child classes. + """ + pass + def _apply_rl_actions(self, rl_actions): """See parent class. diff --git a/flow/envs/bottleneck.py b/flow/envs/bottleneck.py index c647e1d6e..a5f1508f9 100644 --- a/flow/envs/bottleneck.py +++ b/flow/envs/bottleneck.py @@ -472,6 +472,13 @@ def observation_space(self): shape=(1, ), dtype=np.float32) + def _apply_rl_actions(self, rl_actions): + """See parent class. + + To be implemented by child classes. + """ + pass + def compute_reward(self, rl_actions, **kwargs): """Outflow rate over last ten seconds normalized to max of 1.""" reward = self.k.vehicle.get_outflow_rate(10 * self.sim_step) / \ diff --git a/tests/fast_tests/test_environment_base_class.py b/tests/fast_tests/test_environment_base_class.py index b5c6cbc17..ee815393c 100644 --- a/tests/fast_tests/test_environment_base_class.py +++ b/tests/fast_tests/test_environment_base_class.py @@ -13,8 +13,9 @@ from tests.setup_scripts import ring_road_exp_setup, highway_exp_setup import os -import numpy as np import gym.spaces as spaces +from gym.spaces.box import Box +import numpy as np os.environ["TEST_FLAG"] = "True" @@ -25,6 +26,41 @@ YELLOW = (255, 255, 0) +class TestFailRLActionsEnv(Env): + """Test environment designed to fail _apply_rl_actions not-implemented test.""" + + @property + def action_space(self): + """See parent class.""" + return Box(low=0, high=0, shape=(0,), dtype=np.float32) # pragma: no cover + + @property + def observation_space(self): + """See parent class.""" + return Box(low=0, high=0, shape=(0,), dtype=np.float32) # pragma: no cover + + def get_state(self, **kwargs): + """See class definition.""" + return np.array([]) # pragma: no cover + + +class TestFailGetStateEnv(Env): + """Test environment designed to fail get_state not-implemented test.""" + + @property + def action_space(self): + """See parent class.""" + return Box(low=0, high=0, shape=(0,), dtype=np.float32) # pragma: no cover + + @property + def observation_space(self): + """See parent class.""" + return Box(low=0, high=0, shape=(0,), dtype=np.float32) # pragma: no cover + + def _apply_rl_actions(self, rl_actions): + return # pragma: no cover + + class TestShuffle(unittest.TestCase): """ Tests that, at resets, the ordering of vehicles changes while the starting @@ -311,28 +347,34 @@ class TestAbstractMethods(unittest.TestCase): """ def setUp(self): - env, network, _ = ring_road_exp_setup() - sim_params = SumoParams() # FIXME: make ambiguous - env_params = EnvParams() - self.env = Env(sim_params=sim_params, - env_params=env_params, - network=network) + self.env, self.network, _ = ring_road_exp_setup() + self.sim_params = SumoParams() # FIXME: make ambiguous + self.env_params = EnvParams() - def tearDown(self): - self.env.terminate() - self.env = None + def test_abstract_base_class(self): + """Checks that instantiating abstract base class raises an error.""" + with self.assertRaises(TypeError): + Env(sim_params=self.sim_params, + env_params=self.env_params, + network=self.network) def test_get_state(self): - """Checks that get_state raises an error.""" - self.assertRaises(NotImplementedError, self.env.get_state) - - def test_compute_reward(self): - """Checks that compute_reward returns 0.""" - self.assertEqual(self.env.compute_reward([]), 0) + """Checks that instantiating without get_state implemented + raises an error. + """ + with self.assertRaises(TypeError): + TestFailGetStateEnv(sim_params=self.sim_params, + env_params=self.env_params, + network=self.network) def test__apply_rl_actions(self): - self.assertRaises(NotImplementedError, self.env._apply_rl_actions, - rl_actions=None) + """Checks that instantiating without _apply_rl_actions + implemented raises an error. + """ + with self.assertRaises(TypeError): + TestFailRLActionsEnv(sim_params=self.sim_params, + env_params=self.env_params, + network=self.network) class TestVehicleColoring(unittest.TestCase): From 67dc1216e2f4822e9017f0f33cde45a060e810f4 Mon Sep 17 00:00:00 2001 From: Yashar Zeinali Farid <34227133+Yasharzf@users.noreply.github.com> Date: Thu, 11 Jun 2020 10:29:40 -0700 Subject: [PATCH 71/86] Update lane change mode (#948) * added new lane change modes * replaced 'no_lat_collide' with 'no_lc_safe' which is the new default lane change mode * bug fixes and PR reviews Co-authored-by: AboudyKreidieh --- examples/exp_configs/non_rl/bay_bridge.py | 2 +- .../exp_configs/non_rl/bay_bridge_toll.py | 2 +- examples/exp_configs/non_rl/minicity.py | 2 +- flow/core/params.py | 97 +++++++++++++++++-- tests/fast_tests/test_vehicles.py | 6 +- 5 files changed, 93 insertions(+), 16 deletions(-) diff --git a/examples/exp_configs/non_rl/bay_bridge.py b/examples/exp_configs/non_rl/bay_bridge.py index d7d78360f..f3e0c465f 100644 --- a/examples/exp_configs/non_rl/bay_bridge.py +++ b/examples/exp_configs/non_rl/bay_bridge.py @@ -48,7 +48,7 @@ lc_pushy=0.8, lc_speed_gain=4.0, model="LC2013", - lane_change_mode="no_lat_collide", + lane_change_mode="no_lc_safe", # lcKeepRight=0.8 ), num_vehicles=1400) diff --git a/examples/exp_configs/non_rl/bay_bridge_toll.py b/examples/exp_configs/non_rl/bay_bridge_toll.py index 1b8268aeb..0941823cb 100644 --- a/examples/exp_configs/non_rl/bay_bridge_toll.py +++ b/examples/exp_configs/non_rl/bay_bridge_toll.py @@ -46,7 +46,7 @@ model="LC2013", lcCooperative=0.2, lcSpeedGain=15, - lane_change_mode="no_lat_collide", + lane_change_mode="no_lc_safe", ), num_vehicles=50) diff --git a/examples/exp_configs/non_rl/minicity.py b/examples/exp_configs/non_rl/minicity.py index 23b232480..35d5edbce 100644 --- a/examples/exp_configs/non_rl/minicity.py +++ b/examples/exp_configs/non_rl/minicity.py @@ -18,7 +18,7 @@ speed_mode=1, ), lane_change_params=SumoLaneChangeParams( - lane_change_mode="no_lat_collide", + lane_change_mode="no_lc_safe", ), initial_speed=0, num_vehicles=90) diff --git a/flow/core/params.py b/flow/core/params.py index 5a7467580..79ad8d689 100755 --- a/flow/core/params.py +++ b/flow/core/params.py @@ -17,7 +17,27 @@ "all_checks": 31 } -LC_MODES = {"aggressive": 0, "no_lat_collide": 512, "strategic": 1621} +LC_MODES = { + "no_lc_safe": 512, + "no_lc_aggressive": 0, + "sumo_default": 1621, + "no_strategic_aggressive": 1108, + "no_strategic_safe": 1620, + "only_strategic_aggressive": 1, + "only_strategic_safe": 513, + "no_cooperative_aggressive": 1105, + "no_cooperative_safe": 1617, + "only_cooperative_aggressive": 4, + "only_cooperative_safe": 516, + "no_speed_gain_aggressive": 1093, + "no_speed_gain_safe": 1605, + "only_speed_gain_aggressive": 16, + "only_speed_gain_safe": 528, + "no_right_drive_aggressive": 1045, + "no_right_drive_safe": 1557, + "only_right_drive_aggressive": 64, + "only_right_drive_safe": 576 +} # Traffic light defaults PROGRAM_ID = 1 @@ -897,14 +917,71 @@ class SumoLaneChangeParams: ---------- lane_change_mode : str or int, optional may be one of the following: + * "no_lc_safe" (default): Disable all SUMO lane changing but still + handle safety checks (collision avoidance and safety-gap enforcement) + in the simulation. Binary is [001000000000] + * "no_lc_aggressive": SUMO lane changes are not executed, collision + avoidance and safety-gap enforcement are off. + Binary is [000000000000] + + * "sumo_default": Execute all changes requested by a custom controller + unless in conflict with TraCI. Binary is [011001010101]. + + * "no_strategic_aggressive": Execute all changes except strategic + (routing) lane changes unless in conflict with TraCI. Collision + avoidance and safety-gap enforcement are off. Binary is [010001010100] + * "no_strategic_safe": Execute all changes except strategic + (routing) lane changes unless in conflict with TraCI. Collision + avoidance and safety-gap enforcement are on. Binary is [011001010100] + * "only_strategic_aggressive": Execute only strategic (routing) lane + changes unless in conflict with TraCI. Collision avoidance and + safety-gap enforcement are off. Binary is [000000000001] + * "only_strategic_safe": Execute only strategic (routing) lane + changes unless in conflict with TraCI. Collision avoidance and + safety-gap enforcement are on. Binary is [001000000001] + + * "no_cooperative_aggressive": Execute all changes except cooperative + (change in order to allow others to change) lane changes unless in + conflict with TraCI. Collision avoidance and safety-gap enforcement + are off. Binary is [010001010001] + * "no_cooperative_safe": Execute all changes except cooperative + lane changes unless in conflict with TraCI. Collision avoidance and + safety-gap enforcement are on. Binary is [011001010001] + * "only_cooperative_aggressive": Execute only cooperative lane changes + unless in conflict with TraCI. Collision avoidance and safety-gap + enforcement are off. Binary is [000000000100] + * "only_cooperative_safe": Execute only cooperative lane changes + unless in conflict with TraCI. Collision avoidance and safety-gap + enforcement are on. Binary is [001000000100] + + * "no_speed_gain_aggressive": Execute all changes except speed gain (the + other lane allows for faster driving) lane changes unless in conflict + with TraCI. Collision avoidance and safety-gap enforcement are off. + Binary is [010001000101] + * "no_speed_gain_safe": Execute all changes except speed gain + lane changes unless in conflict with TraCI. Collision avoidance and + safety-gap enforcement are on. Binary is [011001000101] + * "only_speed_gain_aggressive": Execute only speed gain lane changes + unless in conflict with TraCI. Collision avoidance and safety-gap + enforcement are off. Binary is [000000010000] + * "only_speed_gain_safe": Execute only speed gain lane changes + unless in conflict with TraCI. Collision avoidance and safety-gap + enforcement are on. Binary is [001000010000] + + * "no_right_drive_aggressive": Execute all changes except right drive + (obligation to drive on the right) lane changes unless in conflict + with TraCI. Collision avoidance and safety-gap enforcement are off. + Binary is [010000010101] + * "no_right_drive_safe": Execute all changes except right drive + lane changes unless in conflict with TraCI. Collision avoidance and + safety-gap enforcement are on. Binary is [011000010101] + * "only_right_drive_aggressive": Execute only right drive lane changes + unless in conflict with TraCI. Collision avoidance and safety-gap + enforcement are off. Binary is [000001000000] + * "only_right_drive_safe": Execute only right drive lane changes + unless in conflict with TraCI. Collision avoidance and safety-gap + enforcement are on. Binary is [001001000000] - * "no_lat_collide" (default): Human cars will not make lane - changes, RL cars can lane change into any space, no matter how - likely it is to crash - * "strategic": Human cars make lane changes in accordance with SUMO - to provide speed boosts - * "aggressive": RL cars are not limited by sumo with regard to - their lane-change actions, and can crash longitudinally * int values may be used to define custom lane change modes for the given vehicles, specified at: http://sumo.dlr.de/wiki/TraCI/Change_Vehicle_State#lane_change_mode_.280xb6.29 @@ -943,7 +1020,7 @@ class SumoLaneChangeParams: """ def __init__(self, - lane_change_mode="no_lat_collide", + lane_change_mode="no_lc_safe", model="LC2013", lc_strategic=1.0, lc_cooperative=1.0, @@ -1051,7 +1128,7 @@ def __init__(self, elif not (isinstance(lane_change_mode, int) or isinstance(lane_change_mode, float)): logging.error("Setting lane change mode to default.") - lane_change_mode = LC_MODES["no_lat_collide"] + lane_change_mode = LC_MODES["no_lc_safe"] self.lane_change_mode = lane_change_mode diff --git a/tests/fast_tests/test_vehicles.py b/tests/fast_tests/test_vehicles.py index b791bba64..1ae2d1cf0 100644 --- a/tests/fast_tests/test_vehicles.py +++ b/tests/fast_tests/test_vehicles.py @@ -33,7 +33,7 @@ def test_speed_lane_change_modes(self): speed_mode='obey_safe_speed', ), lane_change_params=SumoLaneChangeParams( - lane_change_mode="no_lat_collide", + lane_change_mode="no_lc_safe", ) ) @@ -56,7 +56,7 @@ def test_speed_lane_change_modes(self): self.assertEqual(vehicles.type_parameters["typeB"][ "car_following_params"].speed_mode, 0) self.assertEqual(vehicles.type_parameters["typeB"][ - "lane_change_params"].lane_change_mode, 1621) + "lane_change_params"].lane_change_mode, 512) vehicles.add( "typeC", @@ -89,7 +89,7 @@ def test_controlled_id_params(self): speed_mode="obey_safe_speed", ), lane_change_params=SumoLaneChangeParams( - lane_change_mode="no_lat_collide", + lane_change_mode="no_lc_safe", )) default_mingap = SumoCarFollowingParams().controller_params["minGap"] self.assertEqual(vehicles.types[0]["type_params"]["minGap"], From 39fe260e63aaafeceb2d8c982eeebcb8faf582a6 Mon Sep 17 00:00:00 2001 From: liljonnystyle Date: Sat, 13 Jun 2020 18:27:14 -0700 Subject: [PATCH 72/86] Modify time-space diagram plotting (#969) * use pandas for data reshaping * fix flow params assertion * modify plotting for all i210 lanes * generalize plotting to all networks, update tests --- flow/visualize/time_space_diagram.py | 667 +++++++----------- tests/fast_tests/test_files/i210_emission.csv | 2 +- tests/fast_tests/test_visualizers.py | 404 ++++++----- 3 files changed, 455 insertions(+), 618 deletions(-) diff --git a/flow/visualize/time_space_diagram.py b/flow/visualize/time_space_diagram.py index a08ecdf0f..bc26ad855 100644 --- a/flow/visualize/time_space_diagram.py +++ b/flow/visualize/time_space_diagram.py @@ -20,7 +20,7 @@ from flow.networks import RingNetwork, FigureEightNetwork, MergeNetwork, I210SubNetwork import argparse -import csv +from collections import defaultdict try: from matplotlib import pyplot as plt except ImportError: @@ -30,6 +30,7 @@ from matplotlib.collections import LineCollection import matplotlib.colors as colors import numpy as np +import pandas as pd # networks that can be plotted by this method @@ -41,47 +42,46 @@ ] -def import_data_from_emission(fp): - r"""Import relevant data from the predefined emission (.csv) file. +def import_data_from_trajectory(fp, params=dict()): + r"""Import and preprocess data from the Flow trajectory (.csv) file. Parameters ---------- fp : str file path (for the .csv formatted file) + params : dict + flow-specific parameters, including: + + * "network" (str): name of the network that was used when generating + the emission file. Must be one of the network names mentioned in + ACCEPTABLE_NETWORKS, + * "net_params" (flow.core.params.NetParams): network-specific + parameters. This is used to collect the lengths of various network + links. Returns ------- - dict of dict - Key = "veh_id": name of the vehicle \n Elements: - - * "time": time step at every sample - * "edge": edge ID at every sample - * "pos": relative position at every sample - * "vel": speed at every sample + pd.DataFrame """ - # initialize all output variables - veh_id, t, edge, rel_pos, vel, lane = [], [], [], [], [], [] - - # import relevant data from emission file - for record in csv.DictReader(open(fp)): - veh_id.append(record['id']) - t.append(record['time']) - edge.append(record['edge_id']) - rel_pos.append(record['relative_position']) - vel.append(record['speed']) - lane.append(record['lane_number']) - - # we now want to separate data by vehicle ID - ret = {key: {'time': [], 'edge': [], 'pos': [], 'vel': [], 'lane': []} - for key in np.unique(veh_id)} - for i in range(len(veh_id)): - ret[veh_id[i]]['time'].append(float(t[i])) - ret[veh_id[i]]['edge'].append(edge[i]) - ret[veh_id[i]]['pos'].append(float(rel_pos[i])) - ret[veh_id[i]]['vel'].append(float(vel[i])) - ret[veh_id[i]]['lane'].append(float(lane[i])) + # Read trajectory csv into pandas dataframe + df = pd.read_csv(fp) - return ret + # Convert column names for backwards compatibility using emissions csv + column_conversions = { + 'time': 'time_step', + 'lane_number': 'lane_id', + } + df = df.rename(columns=column_conversions) + if 'distance' not in df.columns: + df['distance'] = _get_abs_pos(df, params) + + # Compute line segment ends by shifting dataframe by 1 row + df[['next_pos', 'next_time']] = df.groupby('id')[['distance', 'time_step']].shift(-1) + + # Remove nans from data + df = df[df['next_time'].notna()] + + return df def get_time_space_data(data, params): @@ -89,13 +89,8 @@ def get_time_space_data(data, params): Parameters ---------- - data : dict of dict - Key = "veh_id": name of the vehicle \n Elements: - - * "time": time step at every sample - * "edge": edge ID at every sample - * "pos": relative position at every sample - * "vel": speed at every sample + data : pd.DataFrame + cleaned dataframe of the trajectory data params : dict flow-specific parameters, including: @@ -108,17 +103,14 @@ def get_time_space_data(data, params): Returns ------- - as_array - n_steps x n_veh matrix specifying the absolute position of every - vehicle at every time step. Set to zero if the vehicle is not present - in the network at that time step. - as_array - n_steps x n_veh matrix specifying the speed of every vehicle at every - time step. Set to zero if the vehicle is not present in the network at - that time step. - as_array - a (n_steps,) vector representing the unique time steps in the - simulation + ndarray (or dict of ndarray) + 3d array (n_segments x 2 x 2) containing segments to be plotted. + every inner 2d array is comprised of two 1d arrays representing + [start time, start distance] and [end time, end distance] pairs. + + in the case of I210, the nested arrays are wrapped into a dict, + keyed on the lane number, so that each lane can be plotted + separately. Raises ------ @@ -127,7 +119,7 @@ def get_time_space_data(data, params): """ # check that the network is appropriate assert params['network'] in ACCEPTABLE_NETWORKS, \ - 'Network must be one of: ' + ', '.join(ACCEPTABLE_NETWORKS) + 'Network must be one of: ' + ', '.join([network.__name__ for network in ACCEPTABLE_NETWORKS]) # switcher used to compute the positions based on the type of network switcher = { @@ -137,22 +129,16 @@ def get_time_space_data(data, params): I210SubNetwork: _i210_subnetwork } - # Collect a list of all the unique times. - all_time = [] - for veh_id in data.keys(): - all_time.extend(data[veh_id]['time']) - all_time = np.sort(np.unique(all_time)) - # Get the function from switcher dictionary func = switcher[params['network']] # Execute the function - pos, speed, all_time = func(data, params, all_time) + segs, data = func(data) - return pos, speed, all_time + return segs, data -def _merge(data, params, all_time): +def _merge(data): r"""Generate position and speed data for the merge. This only include vehicles on the main highway, and not on the adjacent @@ -160,73 +146,28 @@ def _merge(data, params, all_time): Parameters ---------- - data : dict of dict - Key = "veh_id": name of the vehicle \n Elements: - - * "time": time step at every sample - * "edge": edge ID at every sample - * "pos": relative position at every sample - * "vel": speed at every sample - params : dict - flow-specific parameters - all_time : array_like - a (n_steps,) vector representing the unique time steps in the - simulation + data : pd.DataFrame + cleaned dataframe of the trajectory data Returns ------- - as_array - n_steps x n_veh matrix specifying the absolute position of every - vehicle at every time step. Set to zero if the vehicle is not present - in the network at that time step. - as_array - n_steps x n_veh matrix specifying the speed of every vehicle at every - time step. Set to zero if the vehicle is not present in the network at - that time step. + ndarray + 3d array (n_segments x 2 x 2) containing segments to be plotted. + every inner 2d array is comprised of two 1d arrays representing + [start time, start distance] and [end time, end distance] pairs. + pd.DataFrame + modified trajectory dataframe """ - # import network data from flow params - inflow_edge_len = 100 - premerge = params['net'].additional_params['pre_merge_length'] - postmerge = params['net'].additional_params['post_merge_length'] - - # generate edge starts - edgestarts = { - 'inflow_highway': 0, - 'left': inflow_edge_len + 0.1, - 'center': inflow_edge_len + premerge + 22.6, - 'inflow_merge': inflow_edge_len + premerge + postmerge + 22.6, - 'bottom': 2 * inflow_edge_len + premerge + postmerge + 22.7, - ':left_0': inflow_edge_len, - ':center_0': inflow_edge_len + premerge + 0.1, - ':center_1': inflow_edge_len + premerge + 0.1, - ':bottom_0': 2 * inflow_edge_len + premerge + postmerge + 22.6 - } + # Omit ghost edges + keep_edges = {'inflow_merge', 'bottom', ':bottom_0'} + data = data[data['edge_id'].isin(keep_edges)] + + segs = data[['time_step', 'distance', 'next_time', 'next_pos']].values.reshape((len(data), 2, 2)) + + return segs, data + - # compute the absolute position - for veh_id in data.keys(): - data[veh_id]['abs_pos'] = _get_abs_pos(data[veh_id]['edge'], - data[veh_id]['pos'], edgestarts) - - # prepare the speed and absolute position in a way that is compatible with - # the space-time diagram, and compute the number of vehicles at each step - pos = np.zeros((all_time.shape[0], len(data.keys()))) - speed = np.zeros((all_time.shape[0], len(data.keys()))) - for i, veh_id in enumerate(sorted(data.keys())): - for spd, abs_pos, ti, edge in zip(data[veh_id]['vel'], - data[veh_id]['abs_pos'], - data[veh_id]['time'], - data[veh_id]['edge']): - # avoid vehicles outside the main highway - if edge in ['inflow_merge', 'bottom', ':bottom_0']: - continue - ind = np.where(ti == all_time)[0] - pos[ind, i] = abs_pos - speed[ind, i] = spd - - return pos, speed, all_time - - -def _ring_road(data, params, all_time): +def _ring_road(data): r"""Generate position and speed data for the ring road. Vehicles that reach the top of the plot simply return to the bottom and @@ -234,147 +175,61 @@ def _ring_road(data, params, all_time): Parameters ---------- - data : dict of dict - Key = "veh_id": name of the vehicle \n Elements: - - * "time": time step at every sample - * "edge": edge ID at every sample - * "pos": relative position at every sample - * "vel": speed at every sample - params : dict - flow-specific parameters - all_time : array_like - a (n_steps,) vector representing the unique time steps in the - simulation + data : pd.DataFrame + cleaned dataframe of the trajectory data Returns ------- - as_array - n_steps x n_veh matrix specifying the absolute position of every - vehicle at every time step. Set to zero if the vehicle is not present - in the network at that time step. - as_array - n_steps x n_veh matrix specifying the speed of every vehicle at every - time step. Set to zero if the vehicle is not present in the network at - that time step. + ndarray + 3d array (n_segments x 2 x 2) containing segments to be plotted. + every inner 2d array is comprised of two 1d arrays representing + [start time, start distance] and [end time, end distance] pairs. + pd.DataFrame + unmodified trajectory dataframe """ - # import network data from flow params - ring_length = params['net'].additional_params["length"] - junction_length = 0.1 # length of inter-edge junctions - - edgestarts = { - "bottom": 0, - ":right_0": 0.25 * ring_length, - "right": 0.25 * ring_length + junction_length, - ":top_0": 0.5 * ring_length + junction_length, - "top": 0.5 * ring_length + 2 * junction_length, - ":left_0": 0.75 * ring_length + 2 * junction_length, - "left": 0.75 * ring_length + 3 * junction_length, - ":bottom_0": ring_length + 3 * junction_length - } - - # compute the absolute position - for veh_id in data.keys(): - data[veh_id]['abs_pos'] = _get_abs_pos(data[veh_id]['edge'], - data[veh_id]['pos'], edgestarts) + segs = data[['time_step', 'distance', 'next_time', 'next_pos']].values.reshape((len(data), 2, 2)) - # create the output variables - pos = np.zeros((all_time.shape[0], len(data.keys()))) - speed = np.zeros((all_time.shape[0], len(data.keys()))) - for i, veh_id in enumerate(sorted(data.keys())): - for spd, abs_pos, ti in zip(data[veh_id]['vel'], - data[veh_id]['abs_pos'], - data[veh_id]['time']): - ind = np.where(ti == all_time)[0] - pos[ind, i] = abs_pos - speed[ind, i] = spd + return segs, data - return pos, speed, all_time +def _i210_subnetwork(data): + r"""Generate time and position data for the i210 subnetwork. -def _i210_subnetwork(data, params, all_time): - r"""Generate position and speed data for the i210 subnetwork. - - We only look at the second to last lane of edge 119257908#1-AddedOnRampEdge + We generate plots for all lanes, so the segments are wrapped in + a dictionary. Parameters ---------- - data : dict of dict - Key = "veh_id": name of the vehicle \n Elements: - - * "time": time step at every sample - * "edge": edge ID at every sample - * "pos": relative position at every sample - * "vel": speed at every sample - params : dict - flow-specific parameters - all_time : array_like - a (n_steps,) vector representing the unique time steps in the - simulation + data : pd.DataFrame + cleaned dataframe of the trajectory data Returns ------- - as_array - n_steps x n_veh matrix specifying the absolute position of every - vehicle at every time step. Set to zero if the vehicle is not present - in the network at that time step. - as_array - n_steps x n_veh matrix specifying the speed of every vehicle at every - time step. Set to zero if the vehicle is not present in the network at - that time step. + dict of ndarray + dictionary of 3d array (n_segments x 2 x 2) containing segments + to be plotted. the dictionary is keyed on lane numbers, with the + values being the 3d array representing the segments. every inner + 2d array is comprised of two 1d arrays representing + [start time, start distance] and [end time, end distance] pairs. + pd.DataFrame + modified trajectory dataframe """ - # import network data from flow params - # - # edge_starts = {"119257908#0": 0, - # "119257908#1-AddedOnRampEdge": 686.98} - desired_lane = 1 - edge_starts = {"119257914": 0, - "119257908#0": 61.58, - "119257908#1-AddedOnRampEdge": 686.98 + 61.58} - # edge_starts = {"119257908#0": 0} - # edge_starts = {"119257908#1-AddedOnRampEdge": 0} - # desired_lane = 5 - - # compute the absolute position - for veh_id in data.keys(): - data[veh_id]['abs_pos'] = _get_abs_pos_1_edge(data[veh_id]['edge'], - data[veh_id]['pos'], - edge_starts) - - # create the output variables - # TODO(@ev) handle subsampling better than this - low_time = int(0 / params['sim'].sim_step) - high_time = int(1600 / params['sim'].sim_step) - all_time = all_time[low_time:high_time] - - # track only vehicles that were around during this time period - observed_row_list = [] - pos = np.zeros((all_time.shape[0], len(data.keys()))) - speed = np.zeros((all_time.shape[0], len(data.keys()))) - for i, veh_id in enumerate(sorted(data.keys())): - for spd, abs_pos, ti, edge, lane in zip(data[veh_id]['vel'], - data[veh_id]['abs_pos'], - data[veh_id]['time'], - data[veh_id]['edge'], - data[veh_id]['lane']): - # avoid vehicles not on the relevant edges. Also only check the second to - # last lane - if edge not in edge_starts.keys() or ti not in all_time or lane != desired_lane: - continue - else: - if i not in observed_row_list: - observed_row_list.append(i) - ind = np.where(ti == all_time)[0] - pos[ind, i] = abs_pos - speed[ind, i] = spd - - pos = pos[:, observed_row_list] - speed = speed[:, observed_row_list] - - return pos, speed, all_time - - -def _figure_eight(data, params, all_time): + # Omit ghost edges + omit_edges = {'ghost0', '119257908#3'} + data.loc[:, :] = data[~data['edge_id'].isin(omit_edges)] + + # Reset lane numbers that are offset by ramp lanes + offset_edges = set(data[data['lane_id'] == 5]['edge_id'].unique()) + data.loc[data['edge_id'].isin(offset_edges), 'lane_id'] -= 1 + + segs = dict() + for lane, df in data.groupby('lane_id'): + segs[lane] = df[['time_step', 'distance', 'next_time', 'next_pos']].values.reshape((len(df), 2, 2)) + + return segs, data + + +def _figure_eight(data): r"""Generate position and speed data for the figure eight. The vehicles traveling towards the intersection from one side will be @@ -383,137 +238,165 @@ def _figure_eight(data, params, all_time): Parameters ---------- - data : dict of dict - Key = "veh_id": name of the vehicle \n Elements: - - * "time": time step at every sample - * "edge": edge ID at every sample - * "pos": relative position at every sample - * "vel": speed at every sample - params : dict - flow-specific parameters - all_time : array_like - a (n_steps,) vector representing the unique time steps in the - simulation + data : pd.DataFrame + cleaned dataframe of the trajectory data Returns ------- - as_array - n_steps x n_veh matrix specifying the absolute position of every - vehicle at every time step. Set to zero if the vehicle is not present - in the network at that time step. - as_array - n_steps x n_veh matrix specifying the speed of every vehicle at every - time step. Set to zero if the vehicle is not present in the network at - that time step. + ndarray + 3d array (n_segments x 2 x 2) containing segments to be plotted. + every inner 2d array is comprised of two 1d arrays representing + [start time, start distance] and [end time, end distance] pairs. + pd.DataFrame + unmodified trajectory dataframe """ - # import network data from flow params - net_params = params['net'] - ring_radius = net_params.additional_params['radius_ring'] - ring_edgelen = ring_radius * np.pi / 2. - intersection = 2 * ring_radius - junction = 2.9 + 3.3 * net_params.additional_params['lanes'] - inner = 0.28 - - # generate edge starts - edgestarts = { - 'bottom': inner, - 'top': intersection / 2 + junction + inner, - 'upper_ring': intersection + junction + 2 * inner, - 'right': intersection + 3 * ring_edgelen + junction + 3 * inner, - 'left': 1.5 * intersection + 3 * ring_edgelen + 2 * junction + 3 * inner, - 'lower_ring': 2 * intersection + 3 * ring_edgelen + 2 * junction + 4 * inner, - ':bottom_0': 0, - ':center_1': intersection / 2 + inner, - ':top_0': intersection + junction + inner, - ':right_0': intersection + 3 * ring_edgelen + junction + 2 * inner, - ':center_0': 1.5 * intersection + 3 * ring_edgelen + junction + 3 * inner, - ':left_0': 2 * intersection + 3 * ring_edgelen + 2 * junction + 3 * inner, - # for aimsun - 'bottom_to_top': intersection / 2 + inner, - 'right_to_left': junction + 3 * inner, - } + segs = data[['time_step', 'distance', 'next_time', 'next_pos']].values.reshape((len(data), 2, 2)) + + return segs, data + - # compute the absolute position - for veh_id in data.keys(): - data[veh_id]['abs_pos'] = _get_abs_pos(data[veh_id]['edge'], - data[veh_id]['pos'], edgestarts) - - # create the output variables - pos = np.zeros((all_time.shape[0], len(data.keys()))) - speed = np.zeros((all_time.shape[0], len(data.keys()))) - for i, veh_id in enumerate(sorted(data.keys())): - for spd, abs_pos, ti in zip(data[veh_id]['vel'], - data[veh_id]['abs_pos'], - data[veh_id]['time']): - ind = np.where(ti == all_time)[0] - pos[ind, i] = abs_pos - speed[ind, i] = spd - - # reorganize data for space-time plot - figure_eight_len = 6 * ring_edgelen + 2 * intersection + 2 * junction + 10 * inner - intersection_loc = [edgestarts[':center_1'] + intersection / 2, - edgestarts[':center_0'] + intersection / 2] - pos[pos < intersection_loc[0]] += figure_eight_len - pos[np.logical_and(pos > intersection_loc[0], pos < intersection_loc[1])] \ - += - intersection_loc[1] - pos[pos > intersection_loc[1]] = \ - - pos[pos > intersection_loc[1]] + figure_eight_len + intersection_loc[0] - - return pos, speed, all_time - - -def _get_abs_pos(edge, rel_pos, edgestarts): +def _get_abs_pos(df, params): """Compute the absolute positions from edges and relative positions. This is the variable we will ultimately use to plot individual vehicles. Parameters ---------- - edge : list of str - list of edges at every time step - rel_pos : list of float - list of relative positions at every time step - edgestarts : dict - the absolute starting position of every edge + df : pd.DataFrame + dataframe of trajectory data + params : dict + flow-specific parameters Returns ------- - list of float + pd.Series the absolute positive for every sample """ - ret = [] - for edge_i, pos_i in zip(edge, rel_pos): - ret.append(pos_i + edgestarts[edge_i]) + if params['network'] == MergeNetwork: + inflow_edge_len = 100 + premerge = params['net'].additional_params['pre_merge_length'] + postmerge = params['net'].additional_params['post_merge_length'] + + # generate edge starts + edgestarts = { + 'inflow_highway': 0, + 'left': inflow_edge_len + 0.1, + 'center': inflow_edge_len + premerge + 22.6, + 'inflow_merge': inflow_edge_len + premerge + postmerge + 22.6, + 'bottom': 2 * inflow_edge_len + premerge + postmerge + 22.7, + ':left_0': inflow_edge_len, + ':center_0': inflow_edge_len + premerge + 0.1, + ':center_1': inflow_edge_len + premerge + 0.1, + ':bottom_0': 2 * inflow_edge_len + premerge + postmerge + 22.6 + } + elif params['network'] == RingNetwork: + ring_length = params['net'].additional_params["length"] + junction_length = 0.1 # length of inter-edge junctions + + edgestarts = { + "bottom": 0, + ":right_0": 0.25 * ring_length, + "right": 0.25 * ring_length + junction_length, + ":top_0": 0.5 * ring_length + junction_length, + "top": 0.5 * ring_length + 2 * junction_length, + ":left_0": 0.75 * ring_length + 2 * junction_length, + "left": 0.75 * ring_length + 3 * junction_length, + ":bottom_0": ring_length + 3 * junction_length + } + elif params['network'] == FigureEightNetwork: + net_params = params['net'] + ring_radius = net_params.additional_params['radius_ring'] + ring_edgelen = ring_radius * np.pi / 2. + intersection = 2 * ring_radius + junction = 2.9 + 3.3 * net_params.additional_params['lanes'] + inner = 0.28 + + # generate edge starts + edgestarts = { + 'bottom': inner, + 'top': intersection / 2 + junction + inner, + 'upper_ring': intersection + junction + 2 * inner, + 'right': intersection + 3 * ring_edgelen + junction + 3 * inner, + 'left': 1.5 * intersection + 3 * ring_edgelen + 2 * junction + 3 * inner, + 'lower_ring': 2 * intersection + 3 * ring_edgelen + 2 * junction + 4 * inner, + ':bottom_0': 0, + ':center_1': intersection / 2 + inner, + ':top_0': intersection + junction + inner, + ':right_0': intersection + 3 * ring_edgelen + junction + 2 * inner, + ':center_0': 1.5 * intersection + 3 * ring_edgelen + junction + 3 * inner, + ':left_0': 2 * intersection + 3 * ring_edgelen + 2 * junction + 3 * inner, + # for aimsun + 'bottom_to_top': intersection / 2 + inner, + 'right_to_left': junction + 3 * inner, + } + else: + edgestarts = defaultdict(float) + + ret = df.apply(lambda x: x['relative_position'] + edgestarts[x['edge_id']], axis=1) + + if params['network'] == FigureEightNetwork: + # reorganize data for space-time plot + figure_eight_len = 6 * ring_edgelen + 2 * intersection + 2 * junction + 10 * inner + intersection_loc = [edgestarts[':center_1'] + intersection / 2, + edgestarts[':center_0'] + intersection / 2] + ret.loc[ret < intersection_loc[0]] += figure_eight_len + ret.loc[(ret > intersection_loc[0]) & (ret < intersection_loc[1])] += -intersection_loc[1] + ret.loc[ret > intersection_loc[1]] = \ + - ret.loc[ret > intersection_loc[1]] + figure_eight_len + intersection_loc[0] return ret -def _get_abs_pos_1_edge(edges, rel_pos, edge_starts): - """Compute the absolute positions from a subset of edges. +def plot_tsd(ax, df, segs, args, lane=None): + """Plot the time-space diagram. - This is the variable we will ultimately use to plot individual vehicles. + Take the pre-processed segments and other meta-data, then plot all the line segments. Parameters ---------- - edges : list of str - list of edges at every time step - rel_pos : list of float - list of relative positions at every time step - edge_starts : dict - the absolute starting position of every edge + ax : matplotlib.axes.Axes + figure axes that will be plotted on + df : pd.DataFrame + data used for axes bounds and speed coloring + segs : list of list of lists + line segments to be plotted, where each segment is a list of two [x,y] pairs + args : dict + parsed arguments + lane : int, optional + lane number to be shown in plot title Returns ------- - list of float - the absolute positive for every sample + None """ - ret = [] - for edge_i, pos_i in zip(edges, rel_pos): - if edge_i in edge_starts.keys(): - ret.append(pos_i + edge_starts[edge_i]) - else: - ret.append(-1) - return ret + norm = plt.Normalize(args.min_speed, args.max_speed) + + xmin = max(df['time_step'].min(), args.start) + xmax = min(df['time_step'].max(), args.stop) + xbuffer = (xmax - xmin) * 0.025 # 2.5% of range + ymin, ymax = df['distance'].min(), df['distance'].max() + ybuffer = (ymax - ymin) * 0.025 # 2.5% of range + + ax.set_xlim(xmin - xbuffer, xmax + xbuffer) + ax.set_ylim(ymin - ybuffer, ymax + ybuffer) + + lc = LineCollection(segs, cmap=my_cmap, norm=norm) + lc.set_array(df['speed'].values) + lc.set_linewidth(1) + ax.add_collection(lc) + ax.autoscale() + + if lane: + ax.set_title('Time-Space Diagram: Lane {}'.format(lane), fontsize=25) + else: + ax.set_title('Time-Space Diagram', fontsize=25) + ax.set_ylabel('Position (m)', fontsize=20) + ax.set_xlabel('Time (s)', fontsize=20) + plt.xticks(fontsize=18) + plt.yticks(fontsize=18) + + cbar = plt.colorbar(lc, ax=ax, norm=norm) + cbar.set_label('Velocity (m/s)', fontsize=20) + cbar.ax.tick_params(labelsize=18) if __name__ == '__main__': @@ -525,8 +408,8 @@ def _get_abs_pos_1_edge(edges, rel_pos, edge_starts): '.json') # required arguments - parser.add_argument('emission_path', type=str, - help='path to the csv file.') + parser.add_argument('trajectory_path', type=str, + help='path to the Flow trajectory csv file.') parser.add_argument('flow_params', type=str, help='path to the flow_params json file.') @@ -553,12 +436,6 @@ def _get_abs_pos_1_edge(edges, rel_pos, edge_starts): module = __import__("examples.exp_configs.non_rl", fromlist=[args.flow_params]) flow_params = getattr(module, args.flow_params).flow_params - # import data from the emission.csv file - emission_data = import_data_from_emission(args.emission_path) - - # compute the position and speed for all vehicles at all times - pos, speed, time = get_time_space_data(emission_data, flow_params) - # some plotting parameters cdict = { 'red': ((0, 0, 0), (0.2, 1, 1), (0.6, 1, 1), (1, 0, 0)), @@ -567,64 +444,34 @@ def _get_abs_pos_1_edge(edges, rel_pos, edge_starts): } my_cmap = colors.LinearSegmentedColormap('my_colormap', cdict, 1024) - # perform plotting operation - fig = plt.figure(figsize=(16, 9)) - ax = plt.axes() - norm = plt.Normalize(args.min_speed, args.max_speed) - cols = [] + # Read trajectory csv into pandas dataframe + traj_df = import_data_from_trajectory(args.trajectory_path, flow_params) - xmin = max(time[0], args.start) - xmax = min(time[-1], args.stop) - xbuffer = (xmax - xmin) * 0.025 # 2.5% of range - ymin, ymax = np.amin(pos), np.amax(pos) - ybuffer = (ymax - ymin) * 0.025 # 2.5% of range + # Convert df data into segments for plotting + segs, traj_df = get_time_space_data(traj_df, flow_params) - ax.set_xlim(xmin - xbuffer, xmax + xbuffer) - ax.set_ylim(ymin - ybuffer, ymax + ybuffer) + if flow_params['network'] == I210SubNetwork: + nlanes = traj_df['lane_id'].nunique() + fig = plt.figure(figsize=(16, 9*nlanes)) - for indx_car in range(pos.shape[1]): - unique_car_pos = pos[:, indx_car] - - if flow_params['network'] == I210SubNetwork: - indices = np.where(pos[:, indx_car] != 0)[0] - unique_car_speed = speed[indices, indx_car] - points = np.array([time[indices], pos[indices, indx_car]]).T.reshape(-1, 1, 2) - else: - - # discontinuity from wraparound - disc = np.where(np.abs(np.diff(unique_car_pos)) >= 10)[0] + 1 - unique_car_time = np.insert(time, disc, np.nan) - unique_car_pos = np.insert(unique_car_pos, disc, np.nan) - unique_car_speed = np.insert(speed[:, indx_car], disc, np.nan) - # - points = np.array( - [unique_car_time, unique_car_pos]).T.reshape(-1, 1, 2) - segments = np.concatenate([points[:-1], points[1:]], axis=1) - lc = LineCollection(segments, cmap=my_cmap, norm=norm) - - # Set the values used for color mapping - lc.set_array(unique_car_speed) - lc.set_linewidth(1.75) - cols.append(lc) - - plt.title(args.title, fontsize=25) - plt.ylabel('Position (m)', fontsize=20) - plt.xlabel('Time (s)', fontsize=20) - - for col in cols: - line = ax.add_collection(col) - cbar = plt.colorbar(line, ax=ax, norm=norm) - cbar.set_label('Velocity (m/s)', fontsize=20) - cbar.ax.tick_params(labelsize=18) + for lane, df in traj_df.groupby('lane_id'): + ax = plt.subplot(nlanes, 1, lane+1) - plt.xticks(fontsize=18) - plt.yticks(fontsize=18) + plot_tsd(ax, df, segs[lane], args, lane) + else: + # perform plotting operation + fig = plt.figure(figsize=(16, 9)) + ax = plt.axes() + + plot_tsd(ax, traj_df, segs, args) ########################################################################### # Note: For MergeNetwork only # if flow_params['network'] == 'MergeNetwork': # - plt.plot(time, [0] * pos.shape[0], linewidth=3, color="white") # - plt.plot(time, [-0.1] * pos.shape[0], linewidth=3, color="white") # + plt.plot([df['time_step'].min(), df['time_step'].max()], + [0, 0], linewidth=3, color="white") # + plt.plot([df['time_step'].min(), df['time_step'].max()], + [-0.1, -0.1], linewidth=3, color="white") # ########################################################################### plt.show() diff --git a/tests/fast_tests/test_files/i210_emission.csv b/tests/fast_tests/test_files/i210_emission.csv index d43c115a4..ec63cf9cf 100644 --- a/tests/fast_tests/test_files/i210_emission.csv +++ b/tests/fast_tests/test_files/i210_emission.csv @@ -1,4 +1,4 @@ -x,time,edge_id,eclass,type,PMx,speed,angle,CO,CO2,electricity,noise,lane_number,NOx,relative_position,route,y,id,fuel,HC,waiting +x,time,edge_id,eclass,type,PMx,speed,angle,CO,CO2,electricity,noise,lane_number,NOx,distance,route,y,id,fuel,HC,waiting 485.04,0.8,119257914,HBEFA3/PC_G_EU4,human,0.05,23.0,119.74,3.32,3793.12,0.0,70.29,1,1.17,5.1,route119257914_0,1068.18,flow_00.0,1.63,0.11,0.0 500.91,1.6,119257914,HBEFA3/PC_G_EU4,human,0.0,22.84,119.74,0.0,0.0,0.0,69.9,1,0.0,23.37,route119257914_0,1059.12,flow_00.0,0.0,0.0,0.0 517.1,2.4,119257914,HBEFA3/PC_G_EU4,human,0.15,23.31,119.74,78.83,7435.5,0.0,71.61,1,2.88,42.02,route119257914_0,1049.87,flow_00.0,3.2,0.54,0.0 diff --git a/tests/fast_tests/test_visualizers.py b/tests/fast_tests/test_visualizers.py index 7af413909..d2f4a20a4 100644 --- a/tests/fast_tests/test_visualizers.py +++ b/tests/fast_tests/test_visualizers.py @@ -91,236 +91,226 @@ def test_capacity_diagram_generator(self): np.testing.assert_array_almost_equal(std_outflows, expected_stds) def test_time_space_diagram_figure_eight(self): - # check that the exported data matches the expected emission file data - fig8_emission_data = { - 'idm_3': {'pos': [27.25, 28.25, 30.22, 33.17], - 'time': [1.0, 2.0, 3.0, 4.0], - 'vel': [0.0, 0.99, 1.98, 2.95], - 'edge': ['upper_ring', 'upper_ring', 'upper_ring', - 'upper_ring'], - 'lane': [0.0, 0.0, 0.0, 0.0]}, - 'idm_4': {'pos': [56.02, 57.01, 58.99, 61.93], - 'time': [1.0, 2.0, 3.0, 4.0], - 'vel': [0.0, 0.99, 1.98, 2.95], - 'edge': ['upper_ring', 'upper_ring', 'upper_ring', - 'upper_ring'], - 'lane': [0.0, 0.0, 0.0, 0.0]}, - 'idm_5': {'pos': [84.79, 85.78, 87.76, 90.7], - 'time': [1.0, 2.0, 3.0, 4.0], - 'vel': [0.0, 0.99, 1.98, 2.95], - 'edge': ['upper_ring', 'upper_ring', 'upper_ring', - 'upper_ring'], - 'lane': [0.0, 0.0, 0.0, 0.0]}, - 'idm_2': {'pos': [28.77, 29.76, 1.63, 4.58], - 'time': [1.0, 2.0, 3.0, 4.0], - 'vel': [0.0, 0.99, 1.97, 2.95], - 'edge': ['top', 'top', 'upper_ring', 'upper_ring'], - 'lane': [0.0, 0.0, 0.0, 0.0]}, - 'idm_13': {'pos': [106.79, 107.79, 109.77, 112.74], - 'time': [1.0, 2.0, 3.0, 4.0], - 'vel': [0.0, 0.99, 1.98, 2.96], - 'edge': ['lower_ring', 'lower_ring', 'lower_ring', - 'lower_ring'], - 'lane': [0.0, 0.0, 0.0, 0.0]}, - 'idm_9': {'pos': [22.01, 23.0, 24.97, 27.92], - 'time': [1.0, 2.0, 3.0, 4.0], - 'vel': [0.0, 0.99, 1.97, 2.95], - 'edge': ['left', 'left', 'left', 'left'], - 'lane': [0.0, 0.0, 0.0, 0.0]}, - 'idm_6': {'pos': [113.56, 114.55, 116.52, 119.47], - 'time': [1.0, 2.0, 3.0, 4.0], - 'vel': [0.0, 0.99, 1.97, 2.95], - 'edge': ['upper_ring', 'upper_ring', 'upper_ring', - 'upper_ring'], - 'lane': [0.0, 0.0, 0.0, 0.0]}, - 'idm_8': {'pos': [29.44, 0.28, 2.03, 4.78], - 'time': [1.0, 2.0, 3.0, 4.0], - 'vel': [0.0, 0.84, 1.76, 2.75], - 'edge': ['right', ':center_0', ':center_0', - ':center_0'], - 'lane': [0.0, 0.0, 0.0, 0.0]}, - 'idm_12': {'pos': [78.03, 79.02, 80.99, 83.94], - 'time': [1.0, 2.0, 3.0, 4.0], - 'vel': [0.0, 0.99, 1.98, 2.95], - 'edge': ['lower_ring', 'lower_ring', 'lower_ring', - 'lower_ring'], - 'lane': [0.0, 0.0, 0.0, 0.0]}, - 'idm_10': {'pos': [20.49, 21.48, 23.46, 26.41], - 'time': [1.0, 2.0, 3.0, 4.0], - 'vel': [0.0, 0.99, 1.98, 2.95], - 'edge': ['lower_ring', 'lower_ring', 'lower_ring', - 'lower_ring'], - 'lane': [0.0, 0.0, 0.0, 0.0]}, - 'idm_11': {'pos': [49.26, 50.25, 52.23, 55.17], - 'time': [1.0, 2.0, 3.0, 4.0], - 'vel': [0.0, 0.99, 1.98, 2.95], - 'edge': ['lower_ring', 'lower_ring', 'lower_ring', - 'lower_ring'], - 'lane': [0.0, 0.0, 0.0, 0.0]}, - 'idm_1': {'pos': [0.0, 0.99, 2.97, 5.91], - 'time': [1.0, 2.0, 3.0, 4.0], - 'vel': [0.0, 0.99, 1.98, 2.95], - 'edge': ['top', 'top', 'top', 'top'], - 'lane': [0.0, 0.0, 0.0, 0.0]}, - 'idm_7': {'pos': [0.67, 1.66, 3.64, 6.58], - 'time': [1.0, 2.0, 3.0, 4.0], - 'vel': [0.0, 0.99, 1.97, 2.94], - 'edge': ['right', 'right', 'right', 'right'], - 'lane': [0.0, 0.0, 0.0, 0.0]}, - 'idm_0': {'pos': [0.0, 1.0, 2.98, 5.95], - 'time': [1.0, 2.0, 3.0, 4.0], - 'vel': [0.0, 1.0, 1.99, 2.97], - 'edge': ['bottom', 'bottom', 'bottom', 'bottom'], - 'lane': [0.0, 0.0, 0.0, 0.0]} - } dir_path = os.path.dirname(os.path.realpath(__file__)) - actual_emission_data = tsd.import_data_from_emission( - os.path.join(dir_path, 'test_files/fig8_emission.csv')) - self.assertDictEqual(fig8_emission_data, actual_emission_data) - - # test get_time_space_data for figure eight networks flow_params = tsd.get_flow_params( os.path.join(dir_path, 'test_files/fig8.json')) - pos, speed, _ = tsd.get_time_space_data( - actual_emission_data, flow_params) - - expected_pos = np.array( - [[60, 23.8, 182.84166941, 154.07166941, 125.30166941, 96.54166941, - -203.16166941, -174.40166941, -145.63166941, -116.86166941, - -88.09166941, -59.33, -30.56, -1.79], - [59, 22.81, 181.85166941, 153.08166941, 124.31166941, 95.54166941, - -202.17166941, -173.40166941, -144.64166941, -115.87166941, - -87.10166941, -58.34, -29.72, -0.8], - [57.02, 20.83, 179.87166941, 151.10166941, 122.34166941, - 93.56166941, -200.02166941, -171.43166941, -142.66166941, - -113.89166941, -85.13166941, -56.36, -27.97, 208.64166941]] + emission_data = tsd.import_data_from_trajectory( + os.path.join(dir_path, 'test_files/fig8_emission.csv'), flow_params) + + segs, _ = tsd.get_time_space_data(emission_data, flow_params) + + expected_segs = np.array([ + [[1., 60.], [2., 59.]], + [[2., 59.], [3., 57.02]], + [[3., 57.02], [4., 54.05]], + [[1., 23.8], [2., 22.81]], + [[2., 22.81], [3., 20.83]], + [[3., 20.83], [4., 17.89]], + [[1., 182.84166941], [2., 181.85166941]], + [[2., 181.85166941], [3., 179.87166941]], + [[3., 179.87166941], [4., 176.92166941]], + [[1., 154.07166941], [2., 153.08166941]], + [[2., 153.08166941], [3., 151.10166941]], + [[3., 151.10166941], [4., 148.16166941]], + [[1., 125.30166941], [2., 124.31166941]], + [[2., 124.31166941], [3., 122.34166941]], + [[3., 122.34166941], [4., 119.39166941]], + [[1., 96.54166941], [2., 95.54166941]], + [[2., 95.54166941], [3., 93.56166941]], + [[3., 93.56166941], [4., 90.59166941]], + [[1., -203.16166941], [2., -202.17166941]], + [[2., -202.17166941], [3., -200.02166941]], + [[3., -200.02166941], [4., -197.07166941]], + [[1., -174.40166941], [2., -173.40166941]], + [[2., -173.40166941], [3., -171.43166941]], + [[3., -171.43166941], [4., -168.48166941]], + [[1., -145.63166941], [2., -144.64166941]], + [[2., -144.64166941], [3., -142.66166941]], + [[3., -142.66166941], [4., -139.72166941]], + [[1., -116.86166941], [2., -115.87166941]], + [[2., -115.87166941], [3., -113.89166941]], + [[3., -113.89166941], [4., -110.95166941]], + [[1., -88.09166941], [2., -87.10166941]], + [[2., -87.10166941], [3., -85.13166941]], + [[3., -85.13166941], [4., -82.18166941]], + [[1., -59.33], [2., -58.34]], + [[2., -58.34], [3., -56.36]], + [[3., -56.36], [4., -53.42]], + [[1., -30.56], [2., -29.72]], + [[2., -29.72], [3., -27.97]], + [[3., -27.97], [4., -25.22]], + [[1., -1.79], [2., -0.8]], + [[2., -0.8], [3., 208.64166941]], + [[3., 208.64166941], [4., 205.69166941]]] ) - expected_speed = np.array([ - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [1, 0.99, 0.99, 0.99, 0.99, 0.99, 0.99, 0.99, 0.99, 0.99, 0.99, - 0.99, 0.84, 0.99], - [1.99, 1.98, 1.98, 1.98, 1.98, 1.98, 1.97, 1.98, 1.98, 1.98, 1.97, - 1.97, 1.76, 1.97] - ]) - np.testing.assert_array_almost_equal(pos[:-1, :], expected_pos) - np.testing.assert_array_almost_equal(speed[:-1, :], expected_speed) + np.testing.assert_array_almost_equal(segs, expected_segs) def test_time_space_diagram_merge(self): dir_path = os.path.dirname(os.path.realpath(__file__)) - emission_data = tsd.import_data_from_emission( - os.path.join(dir_path, 'test_files/merge_emission.csv')) - flow_params = tsd.get_flow_params( os.path.join(dir_path, 'test_files/merge.json')) - pos, speed, _ = tsd.get_time_space_data(emission_data, flow_params) - - expected_pos = np.array( - [[4.86, 180.32, 361.32, 547.77, 0], - [4.88, 180.36, 361.36, 547.8, 0], - [4.95, 180.43, 361.44, 547.87, 0], - [5.06, 180.54, 361.56, 547.98, 0], - [5.21, 180.68, 361.72, 548.12, 0], - [5.4, 180.86, 0, 0, 0]] - ) - expected_speed = np.array( - [[0, 0, 0, 0, 0], - [0.15, 0.17, 0.19, 0.14, 0], - [0.35, 0.37, 0.39, 0.34, 0], - [0.54, 0.57, 0.59, 0.54, 0], - [0.74, 0.7, 0.79, 0.71, 0], - [0.94, 0.9, 0, 0, 0]] + emission_data = tsd.import_data_from_trajectory( + os.path.join(dir_path, 'test_files/merge_emission.csv'), flow_params) + + segs, _ = tsd.get_time_space_data(emission_data, flow_params) + + expected_segs = np.array([ + [[2.0000e-01, 7.2949e+02], [4.0000e-01, 7.2953e+02]], + [[4.0000e-01, 7.2953e+02], [6.0000e-01, 7.2961e+02]], + [[6.0000e-01, 7.2961e+02], [8.0000e-01, 7.2973e+02]], + [[8.0000e-01, 7.2973e+02], [1.0000e+00, 7.2988e+02]]] ) - np.testing.assert_array_almost_equal(pos, expected_pos) - np.testing.assert_array_almost_equal(speed, expected_speed) + np.testing.assert_array_almost_equal(segs, expected_segs) def test_time_space_diagram_I210(self): dir_path = os.path.dirname(os.path.realpath(__file__)) - emission_data = tsd.import_data_from_emission( - os.path.join(dir_path, 'test_files/i210_emission.csv')) - module = __import__("examples.exp_configs.non_rl", fromlist=["i210_subnetwork"]) flow_params = getattr(module, "i210_subnetwork").flow_params - pos, speed, _ = tsd.get_time_space_data(emission_data, flow_params) - - expected_pos = np.array( - [[5.1, 0., 0.], - [23.37, 0., 0.], - [42.02, 5.1, 0.], - [61.21, 22.97, 0.], - [80.45, 40.73, 5.1], - [101.51, 0., 0.]] - ) - expected_speed = np.array( - [[23., 0., 0.], - [22.84, 0., 0.], - [23.31, 23., 0.], - [23.98, 22.33, 0.], - [24.25, 22.21, 23.], - [26.33, 0., 0.]] - ) - - np.testing.assert_array_almost_equal(pos, expected_pos) - np.testing.assert_array_almost_equal(speed, expected_speed) + emission_data = tsd.import_data_from_trajectory( + os.path.join(dir_path, 'test_files/i210_emission.csv'), flow_params) + + segs, _ = tsd.get_time_space_data(emission_data, flow_params) + + expected_segs = { + 1: np.array([ + [[0.8, 5.1], [1.6, 23.37]], + [[1.6, 23.37], [2.4, 42.02]], + [[2.4, 42.02], [3.2, 61.21]], + [[3.2, 61.21], [4., 18.87]], + [[4., 18.87], [4.8, 39.93]], + [[2.4, 5.1], [3.2, 22.97]], + [[3.2, 22.97], [4., 40.73]]] + ), + 2: np.array([ + [[2.4, 5.1], [3.2, 23.98]], + [[3.2, 23.98], [4., 43.18]]] + ), + 3: np.array([ + [[0.8, 5.1], [1.6, 23.72]], + [[1.6, 23.72], [2.4, 43.06]], + [[2.4, 43.06], [3.2, 1.33]], + [[3.2, 1.33], [4., 21.65]], + [[4., 21.65], [4.8, 43.46]], + [[2.4, 5.1], [3.2, 23.74]], + [[3.2, 23.74], [4., 42.38]]] + ), + 4: np.array([ + [[2.4, 5.1], [3.2, 23.6]], + [[3.2, 23.6], [4., 42.46]]] + )} + + for lane, expected_seg in expected_segs.items(): + np.testing.assert_array_almost_equal(segs[lane], expected_seg) def test_time_space_diagram_ring_road(self): dir_path = os.path.dirname(os.path.realpath(__file__)) - emission_data = tsd.import_data_from_emission( - os.path.join(dir_path, 'test_files/ring_230_emission.csv')) - flow_params = tsd.get_flow_params( os.path.join(dir_path, 'test_files/ring_230.json')) - pos, speed, _ = tsd.get_time_space_data(emission_data, flow_params) - - expected_pos = np.array( - [[0.0000e+00, 9.5500e+00, 9.5550e+01, 1.0510e+02, 1.1465e+02, - 1.2429e+02, 1.3384e+02, 1.4338e+02, 1.5293e+02, 1.6247e+02, - 1.7202e+02, 1.8166e+02, 1.9090e+01, 1.9121e+02, 2.0075e+02, - 2.8640e+01, 3.8180e+01, 4.7730e+01, 5.7270e+01, 6.6920e+01, - 7.6460e+01, 8.6010e+01], - [1.0000e-02, 9.5500e+00, 9.5560e+01, 1.0511e+02, 1.1465e+02, - 1.2430e+02, 1.3384e+02, 1.4339e+02, 1.5294e+02, 1.6248e+02, - 1.7203e+02, 1.8167e+02, 1.9100e+01, 1.9122e+02, 2.0076e+02, - 2.8640e+01, 3.8190e+01, 4.7740e+01, 5.7280e+01, 6.6930e+01, - 7.6470e+01, 8.6020e+01], - [2.0000e-02, 9.5700e+00, 9.5580e+01, 1.0512e+02, 1.1467e+02, - 1.2431e+02, 1.3386e+02, 1.4341e+02, 1.5295e+02, 1.6250e+02, - 1.7204e+02, 1.8169e+02, 1.9110e+01, 1.9123e+02, 2.0078e+02, - 2.8660e+01, 3.8210e+01, 4.7750e+01, 5.7300e+01, 6.6940e+01, - 7.6490e+01, 8.6030e+01], - [5.0000e-02, 9.5900e+00, 9.5600e+01, 1.0515e+02, 1.1469e+02, - 1.2434e+02, 1.3388e+02, 1.4343e+02, 1.5297e+02, 1.6252e+02, - 1.7207e+02, 1.8171e+02, 1.9140e+01, 1.9126e+02, 2.0081e+02, - 2.8680e+01, 3.8230e+01, 4.7770e+01, 5.7320e+01, 6.6970e+01, - 7.6510e+01, 8.6060e+01], - [8.0000e-02, 9.6200e+00, 9.5630e+01, 1.0518e+02, 1.1472e+02, - 1.2437e+02, 1.3391e+02, 1.4346e+02, 1.5301e+02, 1.6255e+02, - 1.7210e+02, 1.8174e+02, 1.9170e+01, 1.9129e+02, 2.0085e+02, - 2.8710e+01, 3.8260e+01, 4.7810e+01, 5.7350e+01, 6.7000e+01, - 7.6540e+01, 8.6090e+01], - [1.2000e-01, 9.6600e+00, 9.5670e+01, 1.0522e+02, 1.1476e+02, - 1.2441e+02, 0.0000e+00, 0.0000e+00, 0.0000e+00, 0.0000e+00, - 0.0000e+00, 0.0000e+00, 0.0000e+00, 0.0000e+00, 0.0000e+00, - 0.0000e+00, 0.0000e+00, 0.0000e+00, 0.0000e+00, 0.0000e+00, - 0.0000e+00, 0.0000e+00]] + emission_data = tsd.import_data_from_trajectory( + os.path.join(dir_path, 'test_files/ring_230_emission.csv'), flow_params) + + segs, _ = tsd.get_time_space_data(emission_data, flow_params) + + expected_segs = np.array([ + [[1.0000e-01, 0.0000e+00], [2.0000e-01, 1.0000e-02]], + [[2.0000e-01, 1.0000e-02], [3.0000e-01, 2.0000e-02]], + [[3.0000e-01, 2.0000e-02], [4.0000e-01, 5.0000e-02]], + [[4.0000e-01, 5.0000e-02], [5.0000e-01, 8.0000e-02]], + [[5.0000e-01, 8.0000e-02], [6.0000e-01, 1.2000e-01]], + [[1.0000e-01, 9.5500e+00], [2.0000e-01, 9.5500e+00]], + [[2.0000e-01, 9.5500e+00], [3.0000e-01, 9.5700e+00]], + [[3.0000e-01, 9.5700e+00], [4.0000e-01, 9.5900e+00]], + [[4.0000e-01, 9.5900e+00], [5.0000e-01, 9.6200e+00]], + [[5.0000e-01, 9.6200e+00], [6.0000e-01, 9.6600e+00]], + [[1.0000e-01, 9.5550e+01], [2.0000e-01, 9.5560e+01]], + [[2.0000e-01, 9.5560e+01], [3.0000e-01, 9.5580e+01]], + [[3.0000e-01, 9.5580e+01], [4.0000e-01, 9.5600e+01]], + [[4.0000e-01, 9.5600e+01], [5.0000e-01, 9.5630e+01]], + [[5.0000e-01, 9.5630e+01], [6.0000e-01, 9.5670e+01]], + [[1.0000e-01, 1.0510e+02], [2.0000e-01, 1.0511e+02]], + [[2.0000e-01, 1.0511e+02], [3.0000e-01, 1.0512e+02]], + [[3.0000e-01, 1.0512e+02], [4.0000e-01, 1.0515e+02]], + [[4.0000e-01, 1.0515e+02], [5.0000e-01, 1.0518e+02]], + [[5.0000e-01, 1.0518e+02], [6.0000e-01, 1.0522e+02]], + [[1.0000e-01, 1.1465e+02], [2.0000e-01, 1.1465e+02]], + [[2.0000e-01, 1.1465e+02], [3.0000e-01, 1.1467e+02]], + [[3.0000e-01, 1.1467e+02], [4.0000e-01, 1.1469e+02]], + [[4.0000e-01, 1.1469e+02], [5.0000e-01, 1.1472e+02]], + [[5.0000e-01, 1.1472e+02], [6.0000e-01, 1.1476e+02]], + [[1.0000e-01, 1.2429e+02], [2.0000e-01, 1.2430e+02]], + [[2.0000e-01, 1.2430e+02], [3.0000e-01, 1.2431e+02]], + [[3.0000e-01, 1.2431e+02], [4.0000e-01, 1.2434e+02]], + [[4.0000e-01, 1.2434e+02], [5.0000e-01, 1.2437e+02]], + [[5.0000e-01, 1.2437e+02], [6.0000e-01, 1.2441e+02]], + [[1.0000e-01, 1.3384e+02], [2.0000e-01, 1.3384e+02]], + [[2.0000e-01, 1.3384e+02], [3.0000e-01, 1.3386e+02]], + [[3.0000e-01, 1.3386e+02], [4.0000e-01, 1.3388e+02]], + [[4.0000e-01, 1.3388e+02], [5.0000e-01, 1.3391e+02]], + [[1.0000e-01, 1.4338e+02], [2.0000e-01, 1.4339e+02]], + [[2.0000e-01, 1.4339e+02], [3.0000e-01, 1.4341e+02]], + [[3.0000e-01, 1.4341e+02], [4.0000e-01, 1.4343e+02]], + [[4.0000e-01, 1.4343e+02], [5.0000e-01, 1.4346e+02]], + [[1.0000e-01, 1.5293e+02], [2.0000e-01, 1.5294e+02]], + [[2.0000e-01, 1.5294e+02], [3.0000e-01, 1.5295e+02]], + [[3.0000e-01, 1.5295e+02], [4.0000e-01, 1.5297e+02]], + [[4.0000e-01, 1.5297e+02], [5.0000e-01, 1.5301e+02]], + [[1.0000e-01, 1.6247e+02], [2.0000e-01, 1.6248e+02]], + [[2.0000e-01, 1.6248e+02], [3.0000e-01, 1.6250e+02]], + [[3.0000e-01, 1.6250e+02], [4.0000e-01, 1.6252e+02]], + [[4.0000e-01, 1.6252e+02], [5.0000e-01, 1.6255e+02]], + [[1.0000e-01, 1.7202e+02], [2.0000e-01, 1.7203e+02]], + [[2.0000e-01, 1.7203e+02], [3.0000e-01, 1.7204e+02]], + [[3.0000e-01, 1.7204e+02], [4.0000e-01, 1.7207e+02]], + [[4.0000e-01, 1.7207e+02], [5.0000e-01, 1.7210e+02]], + [[1.0000e-01, 1.8166e+02], [2.0000e-01, 1.8167e+02]], + [[2.0000e-01, 1.8167e+02], [3.0000e-01, 1.8169e+02]], + [[3.0000e-01, 1.8169e+02], [4.0000e-01, 1.8171e+02]], + [[4.0000e-01, 1.8171e+02], [5.0000e-01, 1.8174e+02]], + [[1.0000e-01, 1.9090e+01], [2.0000e-01, 1.9100e+01]], + [[2.0000e-01, 1.9100e+01], [3.0000e-01, 1.9110e+01]], + [[3.0000e-01, 1.9110e+01], [4.0000e-01, 1.9140e+01]], + [[4.0000e-01, 1.9140e+01], [5.0000e-01, 1.9170e+01]], + [[1.0000e-01, 1.9121e+02], [2.0000e-01, 1.9122e+02]], + [[2.0000e-01, 1.9122e+02], [3.0000e-01, 1.9123e+02]], + [[3.0000e-01, 1.9123e+02], [4.0000e-01, 1.9126e+02]], + [[4.0000e-01, 1.9126e+02], [5.0000e-01, 1.9129e+02]], + [[1.0000e-01, 2.0075e+02], [2.0000e-01, 2.0076e+02]], + [[2.0000e-01, 2.0076e+02], [3.0000e-01, 2.0078e+02]], + [[3.0000e-01, 2.0078e+02], [4.0000e-01, 2.0081e+02]], + [[4.0000e-01, 2.0081e+02], [5.0000e-01, 2.0085e+02]], + [[1.0000e-01, 2.8640e+01], [2.0000e-01, 2.8640e+01]], + [[2.0000e-01, 2.8640e+01], [3.0000e-01, 2.8660e+01]], + [[3.0000e-01, 2.8660e+01], [4.0000e-01, 2.8680e+01]], + [[4.0000e-01, 2.8680e+01], [5.0000e-01, 2.8710e+01]], + [[1.0000e-01, 3.8180e+01], [2.0000e-01, 3.8190e+01]], + [[2.0000e-01, 3.8190e+01], [3.0000e-01, 3.8210e+01]], + [[3.0000e-01, 3.8210e+01], [4.0000e-01, 3.8230e+01]], + [[4.0000e-01, 3.8230e+01], [5.0000e-01, 3.8260e+01]], + [[1.0000e-01, 4.7730e+01], [2.0000e-01, 4.7740e+01]], + [[2.0000e-01, 4.7740e+01], [3.0000e-01, 4.7750e+01]], + [[3.0000e-01, 4.7750e+01], [4.0000e-01, 4.7770e+01]], + [[4.0000e-01, 4.7770e+01], [5.0000e-01, 4.7810e+01]], + [[1.0000e-01, 5.7270e+01], [2.0000e-01, 5.7280e+01]], + [[2.0000e-01, 5.7280e+01], [3.0000e-01, 5.7300e+01]], + [[3.0000e-01, 5.7300e+01], [4.0000e-01, 5.7320e+01]], + [[4.0000e-01, 5.7320e+01], [5.0000e-01, 5.7350e+01]], + [[1.0000e-01, 6.6920e+01], [2.0000e-01, 6.6930e+01]], + [[2.0000e-01, 6.6930e+01], [3.0000e-01, 6.6940e+01]], + [[3.0000e-01, 6.6940e+01], [4.0000e-01, 6.6970e+01]], + [[4.0000e-01, 6.6970e+01], [5.0000e-01, 6.7000e+01]], + [[1.0000e-01, 7.6460e+01], [2.0000e-01, 7.6470e+01]], + [[2.0000e-01, 7.6470e+01], [3.0000e-01, 7.6490e+01]], + [[3.0000e-01, 7.6490e+01], [4.0000e-01, 7.6510e+01]], + [[4.0000e-01, 7.6510e+01], [5.0000e-01, 7.6540e+01]], + [[1.0000e-01, 8.6010e+01], [2.0000e-01, 8.6020e+01]], + [[2.0000e-01, 8.6020e+01], [3.0000e-01, 8.6030e+01]], + [[3.0000e-01, 8.6030e+01], [4.0000e-01, 8.6060e+01]], + [[4.0000e-01, 8.6060e+01], [5.0000e-01, 8.6090e+01]]] ) - expected_speed = np.array([ - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [0.08, 0.08, 0.08, 0.08, 0.08, 0.08, 0.08, 0.08, 0.08, 0.08, 0.08, - 0.08, 0.08, 0.08, 0.1, 0.08, 0.08, 0.08, 0.08, 0.08, 0.08, 0.08], - [0.16, 0.16, 0.16, 0.16, 0.16, 0.16, 0.16, 0.16, 0.16, 0.16, 0.16, - 0.16, 0.16, 0.16, 0.2, 0.16, 0.16, 0.16, 0.16, 0.16, 0.16, 0.16], - [0.23, 0.23, 0.23, 0.23, 0.23, 0.23, 0.23, 0.23, 0.23, 0.23, 0.23, - 0.23, 0.23, 0.23, 0.29, 0.23, 0.23, 0.23, 0.23, 0.23, 0.23, 0.23], - [0.31, 0.31, 0.31, 0.31, 0.31, 0.31, 0.31, 0.31, 0.31, 0.31, 0.31, - 0.31, 0.31, 0.31, 0.39, 0.31, 0.31, 0.31, 0.31, 0.31, 0.31, 0.31], - [0.41, 0.41, 0.41, 0.41, 0.41, 0.41, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, - 0, 0, 0, 0, 0, 0] - ]) - - np.testing.assert_array_almost_equal(pos, expected_pos) - np.testing.assert_array_almost_equal(speed, expected_speed) + + np.testing.assert_array_almost_equal(segs, expected_segs) def test_plot_ray_results(self): dir_path = os.path.dirname(os.path.realpath(__file__)) From 5b38dc491c45b2a0861d84e02aae508b054df243 Mon Sep 17 00:00:00 2001 From: Pengyuan Zhou Date: Mon, 15 Jun 2020 01:51:36 +0300 Subject: [PATCH 73/86] perhaps it means to ignore instead of add? (#966) --- flow/core/kernel/simulation/traci.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/flow/core/kernel/simulation/traci.py b/flow/core/kernel/simulation/traci.py index 2cd109024..8d51b8e25 100644 --- a/flow/core/kernel/simulation/traci.py +++ b/flow/core/kernel/simulation/traci.py @@ -100,7 +100,7 @@ def start_simulation(self, network, sim_params): if sim_params.use_ballistic: sumo_call.append("--step-method.ballistic") - # add step logs (if requested) + # ignore step logs (if requested) if sim_params.no_step_log: sumo_call.append("--no-step-log") From 86c2e4cf73341611850ff7b312c681c31fd6d702 Mon Sep 17 00:00:00 2001 From: Aboudy Kreidieh Date: Mon, 15 Jun 2020 11:20:47 -0700 Subject: [PATCH 74/86] I210 highway updated (#934) * removed sweep * added updated single lane highway network config * added i210 xml files with downstream edge * added I210Router * added updated i210 features * minor cleanup * better parameters based on when congestion propagates * bug fixes * bug fixes * more bug fixes * bug fixes * minor * broader tests for scenario * added tests for specify_routes * PR fixes --- examples/exp_configs/non_rl/highway_single.py | 61 +- .../exp_configs/non_rl/i210_subnetwork.py | 110 +- .../non_rl/i210_subnetwork_sweep.py | 151 - .../rl/multiagent/multiagent_i210.py | 12 +- .../templates/sumo/i210_with_ghost_cell.xml | 5719 +++++++++++++++++ .../i210_with_ghost_cell_with_downstream.xml | 5719 +++++++++++++++++ flow/controllers/__init__.py | 5 +- flow/controllers/routing_controllers.py | 26 + flow/networks/highway.py | 20 +- flow/networks/i210_subnetwork.py | 247 +- flow/networks/ring.py | 2 +- tests/fast_tests/test_scenarios.py | 189 +- tests/fast_tests/test_vehicles.py | 4 + tests/setup_scripts.py | 1 + 14 files changed, 11945 insertions(+), 321 deletions(-) delete mode 100644 examples/exp_configs/non_rl/i210_subnetwork_sweep.py create mode 100644 examples/exp_configs/templates/sumo/i210_with_ghost_cell.xml create mode 100644 examples/exp_configs/templates/sumo/i210_with_ghost_cell_with_downstream.xml diff --git a/examples/exp_configs/non_rl/highway_single.py b/examples/exp_configs/non_rl/highway_single.py index 46b18c0e9..0ced89f27 100644 --- a/examples/exp_configs/non_rl/highway_single.py +++ b/examples/exp_configs/non_rl/highway_single.py @@ -1,9 +1,5 @@ -"""Multi-agent highway with ramps example. - -Trains a non-constant number of agents, all sharing the same policy, on the -highway with ramps network. -""" -from flow.controllers import BandoFTLController +"""Example of an open network with human-driven vehicles.""" +from flow.controllers import IDMController from flow.core.params import EnvParams from flow.core.params import NetParams from flow.core.params import InitialConfig @@ -11,15 +7,21 @@ from flow.core.params import VehicleParams from flow.core.params import SumoParams from flow.core.params import SumoLaneChangeParams +from flow.core.params import SumoCarFollowingParams from flow.networks import HighwayNetwork from flow.envs import TestEnv from flow.networks.highway import ADDITIONAL_NET_PARAMS -TRAFFIC_SPEED = 11 -END_SPEED = 16 -TRAFFIC_FLOW = 2056 -HORIZON = 3600 -INCLUDE_NOISE = False +# the speed of vehicles entering the network +TRAFFIC_SPEED = 24.1 +# the maximum speed at the downstream boundary edge +END_SPEED = 6.0 +# the inflow rate of vehicles +TRAFFIC_FLOW = 2215 +# the simulation time horizon (in steps) +HORIZON = 1500 +# whether to include noise in the car-following models +INCLUDE_NOISE = True additional_net_params = ADDITIONAL_NET_PARAMS.copy() additional_net_params.update({ @@ -31,28 +33,30 @@ "speed_limit": 30, # number of edges to divide the highway into "num_edges": 2, - # whether to include a ghost edge of length 500m. This edge is provided a - # different speed limit. + # whether to include a ghost edge. This edge is provided a different speed + # limit. "use_ghost_edge": True, # speed limit for the ghost edge - "ghost_speed_limit": END_SPEED + "ghost_speed_limit": END_SPEED, + # length of the downstream ghost edge with the reduced speed limit + "boundary_cell_length": 300, }) vehicles = VehicleParams() vehicles.add( "human", - num_vehicles=0, + acceleration_controller=(IDMController, { + 'a': 1.3, + 'b': 2.0, + 'noise': 0.3 if INCLUDE_NOISE else 0.0 + }), + car_following_params=SumoCarFollowingParams( + min_gap=0.5 + ), lane_change_params=SumoLaneChangeParams( - lane_change_mode="strategic", + model="SL2015", + lc_sublane=2.0, ), - acceleration_controller=(BandoFTLController, { - 'alpha': .5, - 'beta': 20.0, - 'h_st': 12.0, - 'h_go': 50.0, - 'v_max': 30.0, - 'noise': 1.0 if INCLUDE_NOISE else 0.0, - }), ) inflows = InFlows() @@ -64,8 +68,6 @@ depart_speed=TRAFFIC_SPEED, name="idm_highway_inflow") -# SET UP FLOW PARAMETERS - flow_params = dict( # name of the experiment exp_tag='highway-single', @@ -82,14 +84,15 @@ # environment related parameters (see flow.core.params.EnvParams) env=EnvParams( horizon=HORIZON, - warmup_steps=0, - sims_per_step=1, + warmup_steps=500, + sims_per_step=3, ), # sumo-related parameters (see flow.core.params.SumoParams) sim=SumoParams( - sim_step=0.5, + sim_step=0.4, render=False, + use_ballistic=True, restart_instance=False ), diff --git a/examples/exp_configs/non_rl/i210_subnetwork.py b/examples/exp_configs/non_rl/i210_subnetwork.py index dd85c56cf..eda037068 100644 --- a/examples/exp_configs/non_rl/i210_subnetwork.py +++ b/examples/exp_configs/non_rl/i210_subnetwork.py @@ -1,9 +1,9 @@ """I-210 subnetwork example.""" import os - import numpy as np -from flow.controllers.car_following_models import IDMController +from flow.controllers import IDMController +from flow.controllers import I210Router from flow.core.params import SumoParams from flow.core.params import EnvParams from flow.core.params import NetParams @@ -15,7 +15,49 @@ from flow.envs import TestEnv from flow.networks.i210_subnetwork import I210SubNetwork, EDGES_DISTRIBUTION -# create the base vehicle type that will be used for inflows +# =========================================================================== # +# Specify some configurable constants. # +# =========================================================================== # + +# whether to include the upstream ghost edge in the network +WANT_GHOST_CELL = True +# whether to include the downstream slow-down edge in the network +WANT_DOWNSTREAM_BOUNDARY = True +# whether to include vehicles on the on-ramp +ON_RAMP = True +# the inflow rate of vehicles (in veh/hr) +INFLOW_RATE = 5 * 2215 +# the speed of inflowing vehicles from the main edge (in m/s) +INFLOW_SPEED = 24.1 + +# =========================================================================== # +# Specify the path to the network template. # +# =========================================================================== # + +if WANT_DOWNSTREAM_BOUNDARY: + NET_TEMPLATE = os.path.join( + config.PROJECT_PATH, + "examples/exp_configs/templates/sumo/i210_with_ghost_cell_with_" + "downstream.xml") +elif WANT_GHOST_CELL: + NET_TEMPLATE = os.path.join( + config.PROJECT_PATH, + "examples/exp_configs/templates/sumo/i210_with_ghost_cell.xml") +else: + NET_TEMPLATE = os.path.join( + config.PROJECT_PATH, + "examples/exp_configs/templates/sumo/test2.net.xml") + +# If the ghost cell is not being used, remove it from the initial edges that +# vehicles can be placed on. +edges_distribution = EDGES_DISTRIBUTION.copy() +if not WANT_GHOST_CELL: + edges_distribution.remove("ghost0") + +# =========================================================================== # +# Specify vehicle-specific information and inflows. # +# =========================================================================== # + vehicles = VehicleParams() vehicles.add( "human", @@ -24,35 +66,39 @@ lane_change_mode="strategic", ), acceleration_controller=(IDMController, { - "a": 0.3, "b": 2.0, "noise": 0.5 + "a": 1.3, + "b": 2.0, + "noise": 0.3, }), + routing_controller=(I210Router, {}) if ON_RAMP else None, ) inflow = InFlows() # main highway inflow.add( veh_type="human", - edge="119257914", - vehs_per_hour=8378, - departLane="random", - departSpeed=23) + edge="ghost0" if WANT_GHOST_CELL else "119257914", + vehs_per_hour=INFLOW_RATE, + departLane="best", + departSpeed=INFLOW_SPEED) # on ramp -# inflow.add( -# veh_type="human", -# edge="27414345", -# vehs_per_hour=321, -# departLane="random", -# departSpeed=20) -# inflow.add( -# veh_type="human", -# edge="27414342#0", -# vehs_per_hour=421, -# departLane="random", -# departSpeed=20) - -NET_TEMPLATE = os.path.join( - config.PROJECT_PATH, - "examples/exp_configs/templates/sumo/test2.net.xml") +if ON_RAMP: + inflow.add( + veh_type="human", + edge="27414345", + vehs_per_hour=500, + departLane="random", + departSpeed=10) + inflow.add( + veh_type="human", + edge="27414342#0", + vehs_per_hour=500, + departLane="random", + departSpeed=10) + +# =========================================================================== # +# Generate the flow_params dict with all relevant simulation information. # +# =========================================================================== # flow_params = dict( # name of the experiment @@ -69,7 +115,7 @@ # simulation-related parameters sim=SumoParams( - sim_step=0.5, + sim_step=0.4, render=False, color_by_speed=True, use_ballistic=True @@ -77,14 +123,18 @@ # environment related parameters (see flow.core.params.EnvParams) env=EnvParams( - horizon=4500, + horizon=10000, ), # network-related parameters (see flow.core.params.NetParams and the # network's documentation or ADDITIONAL_NET_PARAMS component) net=NetParams( inflows=inflow, - template=NET_TEMPLATE + template=NET_TEMPLATE, + additional_params={ + "on_ramp": ON_RAMP, + "ghost_edge": WANT_GHOST_CELL, + } ), # vehicles to be placed in the network at the start of a rollout (see @@ -94,10 +144,14 @@ # parameters specifying the positioning of vehicles upon initialization/ # reset (see flow.core.params.InitialConfig) initial=InitialConfig( - edges_distribution=EDGES_DISTRIBUTION, + edges_distribution=edges_distribution, ), ) +# =========================================================================== # +# Specify custom callable that is logged during simulation runtime. # +# =========================================================================== # + edge_id = "119257908#1-AddedOnRampEdge" custom_callables = { "avg_merge_speed": lambda env: np.nan_to_num(np.mean( diff --git a/examples/exp_configs/non_rl/i210_subnetwork_sweep.py b/examples/exp_configs/non_rl/i210_subnetwork_sweep.py deleted file mode 100644 index 28cba81ce..000000000 --- a/examples/exp_configs/non_rl/i210_subnetwork_sweep.py +++ /dev/null @@ -1,151 +0,0 @@ -"""I-210 subnetwork example. - -In this case flow_params is a list of dicts. This is to test the effects of -multiple human-driver model parameters on the flow traffic. -""" -from collections import OrderedDict -from copy import deepcopy -import itertools -import os -import numpy as np - -from flow.core.params import SumoParams -from flow.core.params import EnvParams -from flow.core.params import NetParams -from flow.core.params import SumoLaneChangeParams -from flow.core.params import VehicleParams -from flow.core.params import InitialConfig -from flow.core.params import InFlows -import flow.config as config -from flow.envs import TestEnv -from flow.networks.i210_subnetwork import I210SubNetwork, EDGES_DISTRIBUTION - -# the default parameters for all lane change parameters -default_dict = { - "lane_change_mode": "strategic", - "model": "LC2013", - "lc_strategic": 1.0, - "lc_cooperative": 1.0, - "lc_speed_gain": 1.0, - "lc_keep_right": 1.0, - "lc_look_ahead_left": 2.0, - "lc_speed_gain_right": 1.0, - "lc_sublane": 1.0, - "lc_pushy": 0, - "lc_pushy_gap": 0.6, - "lc_assertive": 1, - "lc_accel_lat": 1.0 -} - -# values to sweep through for some lane change parameters -sweep_dict = OrderedDict({ - "lc_strategic": [1.0, 2.0, 4.0, 8.0], - "lc_cooperative": [1.0, 2.0], - "lc_look_ahead_left": [2.0, 4.0] -}) - -# Create a list of possible lane change parameter combinations. -all_names = sorted(sweep_dict) -combinations = itertools.product(*(sweep_dict[name] for name in all_names)) -combination_list = list(combinations) -res = [] -for val in combination_list: - curr_dict = {} - for elem, name in zip(val, all_names): - curr_dict[name] = elem - res.append(curr_dict) - -# Create a list of all possible flow_params dictionaries to sweep through the -# different lane change parameters. -flow_params = [] - -for lane_change_dict in res: - # no vehicles in the network. The lane change parameters of inflowing - # vehicles are updated here. - vehicles = VehicleParams() - update_dict = deepcopy(default_dict) - update_dict.update(lane_change_dict) - vehicles.add( - "human", - num_vehicles=0, - lane_change_params=SumoLaneChangeParams(**update_dict) - ) - - inflow = InFlows() - # main highway - inflow.add( - veh_type="human", - edge="119257914", - vehs_per_hour=8378, - # probability=1.0, - departLane="random", - departSpeed=20) - # on ramp - inflow.add( - veh_type="human", - edge="27414345", - vehs_per_hour=321, - departLane="random", - departSpeed=20) - inflow.add( - veh_type="human", - edge="27414342#0", - vehs_per_hour=421, - departLane="random", - departSpeed=20) - - NET_TEMPLATE = os.path.join( - config.PROJECT_PATH, - "examples/exp_configs/templates/sumo/test2.net.xml") - - params = dict( - # name of the experiment - exp_tag='I-210_subnetwork', - - # name of the flow environment the experiment is running on - env_name=TestEnv, - - # name of the network class the experiment is running on - network=I210SubNetwork, - - # simulator that is used by the experiment - simulator='traci', - - # simulation-related parameters - sim=SumoParams( - sim_step=0.8, - render=True, - color_by_speed=True - ), - - # environment related parameters (see flow.core.params.EnvParams) - env=EnvParams( - horizon=4500, # one hour of run time - ), - - # network-related parameters (see flow.core.params.NetParams and the - # network's documentation or ADDITIONAL_NET_PARAMS component) - net=NetParams( - inflows=inflow, - template=NET_TEMPLATE - ), - - # vehicles to be placed in the network at the start of a rollout (see - # flow.core.params.VehicleParams) - veh=vehicles, - - # parameters specifying the positioning of vehicles upon - # initialization/reset (see flow.core.params.InitialConfig) - initial=InitialConfig( - edges_distribution=EDGES_DISTRIBUTION, - ), - ) - - # Store the next flow_params dict. - flow_params.append(params) - - -custom_callables = { - "avg_merge_speed": lambda env: np.mean(env.k.vehicle.get_speed( - env.k.vehicle.get_ids_by_edge("119257908#1-AddedOnRampEdge"))) -} diff --git a/examples/exp_configs/rl/multiagent/multiagent_i210.py b/examples/exp_configs/rl/multiagent/multiagent_i210.py index 94f709ff4..a6d194708 100644 --- a/examples/exp_configs/rl/multiagent/multiagent_i210.py +++ b/examples/exp_configs/rl/multiagent/multiagent_i210.py @@ -35,6 +35,10 @@ # percentage of autonomous vehicles compared to human vehicles on highway PENETRATION_RATE = 10 +# TODO: temporary fix +edges_distribution = EDGES_DISTRIBUTION.copy() +edges_distribution.remove("ghost0") + # SET UP PARAMETERS FOR THE ENVIRONMENT additional_env_params = ADDITIONAL_ENV_PARAMS.copy() additional_env_params.update({ @@ -145,7 +149,11 @@ # network's documentation or ADDITIONAL_NET_PARAMS component) net=NetParams( inflows=inflow, - template=NET_TEMPLATE + template=NET_TEMPLATE, + additional_params={ + "on_ramp": False, + "ghost_edge": False + } ), # vehicles to be placed in the network at the start of a rollout (see @@ -155,7 +163,7 @@ # parameters specifying the positioning of vehicles upon initialization/ # reset (see flow.core.params.InitialConfig) initial=InitialConfig( - edges_distribution=EDGES_DISTRIBUTION, + edges_distribution=edges_distribution, ), ) diff --git a/examples/exp_configs/templates/sumo/i210_with_ghost_cell.xml b/examples/exp_configs/templates/sumo/i210_with_ghost_cell.xml new file mode 100644 index 000000000..66e5a1131 --- /dev/null +++ b/examples/exp_configs/templates/sumo/i210_with_ghost_cell.xml @@ -0,0 +1,5719 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/exp_configs/templates/sumo/i210_with_ghost_cell_with_downstream.xml b/examples/exp_configs/templates/sumo/i210_with_ghost_cell_with_downstream.xml new file mode 100644 index 000000000..10d4d8d45 --- /dev/null +++ b/examples/exp_configs/templates/sumo/i210_with_ghost_cell_with_downstream.xml @@ -0,0 +1,5719 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/flow/controllers/__init__.py b/flow/controllers/__init__.py index 4dfcf05b7..a61d16980 100755 --- a/flow/controllers/__init__.py +++ b/flow/controllers/__init__.py @@ -28,7 +28,7 @@ # routing controllers from flow.controllers.base_routing_controller import BaseRouter from flow.controllers.routing_controllers import ContinuousRouter, \ - GridRouter, BayBridgeRouter + GridRouter, BayBridgeRouter, I210Router __all__ = [ "RLController", "BaseController", "BaseLaneChangeController", "BaseRouter", @@ -36,5 +36,6 @@ "IDMController", "SimCarFollowingController", "FollowerStopper", "PISaturation", "StaticLaneChanger", "SimLaneChangeController", "ContinuousRouter", "GridRouter", "BayBridgeRouter", "LACController", - "GippsController", "NonLocalFollowerStopper", "BandoFTLController" + "GippsController", "NonLocalFollowerStopper", "BandoFTLController", + "I210Router" ] diff --git a/flow/controllers/routing_controllers.py b/flow/controllers/routing_controllers.py index e6ccdde78..02aa34cb4 100755 --- a/flow/controllers/routing_controllers.py +++ b/flow/controllers/routing_controllers.py @@ -124,3 +124,29 @@ def choose_route(self, env): new_route = super().choose_route(env) return new_route + + +class I210Router(ContinuousRouter): + """Assists in choosing routes in select cases for the I-210 sub-network. + + Extension to the Continuous Router. + + Usage + ----- + See base class for usage example. + """ + + def choose_route(self, env): + """See parent class.""" + edge = env.k.vehicle.get_edge(self.veh_id) + lane = env.k.vehicle.get_lane(self.veh_id) + + # vehicles on these edges in lanes 4 and 5 are not going to be able to + # make it out in time + if edge == "119257908#1-AddedOffRampEdge" and lane in [5, 4, 3]: + new_route = env.available_routes[ + "119257908#1-AddedOffRampEdge"][0][0] + else: + new_route = super().choose_route(env) + + return new_route diff --git a/flow/networks/highway.py b/flow/networks/highway.py index 7e9c18ad5..e48331cf9 100644 --- a/flow/networks/highway.py +++ b/flow/networks/highway.py @@ -14,11 +14,13 @@ "speed_limit": 30, # number of edges to divide the highway into "num_edges": 1, - # whether to include a ghost edge of length 500m. This edge is provided a - # different speed limit. + # whether to include a ghost edge. This edge is provided a different speed + # limit. "use_ghost_edge": False, # speed limit for the ghost edge "ghost_speed_limit": 25, + # length of the downstream ghost edge with the reduced speed limit + "boundary_cell_length": 500 } @@ -34,9 +36,11 @@ class HighwayNetwork(Network): * **lanes** : number of lanes in the highway * **speed_limit** : max speed limit of the highway * **num_edges** : number of edges to divide the highway into - * **use_ghost_edge** : whether to include a ghost edge of length 500m. This - edge is provided a different speed limit. + * **use_ghost_edge** : whether to include a ghost edge. This edge is + provided a different speed limit. * **ghost_speed_limit** : speed limit for the ghost edge + * **boundary_cell_length** : length of the downstream ghost edge with the + reduced speed limit Usage ----- @@ -70,8 +74,6 @@ def __init__(self, if p not in net_params.additional_params: raise KeyError('Network parameter "{}" not supplied'.format(p)) - self.end_length = 500 - super().__init__(name, vehicles, net_params, initial_config, traffic_lights) @@ -80,6 +82,7 @@ def specify_nodes(self, net_params): length = net_params.additional_params["length"] num_edges = net_params.additional_params.get("num_edges", 1) segment_lengths = np.linspace(0, length, num_edges+1) + end_length = net_params.additional_params["boundary_cell_length"] nodes = [] for i in range(num_edges+1): @@ -92,7 +95,7 @@ def specify_nodes(self, net_params): if self.net_params.additional_params["use_ghost_edge"]: nodes += [{ "id": "edge_{}".format(num_edges + 1), - "x": length + self.end_length, + "x": length + end_length, "y": 0 }] @@ -103,6 +106,7 @@ def specify_edges(self, net_params): length = net_params.additional_params["length"] num_edges = net_params.additional_params.get("num_edges", 1) segment_length = length/float(num_edges) + end_length = net_params.additional_params["boundary_cell_length"] edges = [] for i in range(num_edges): @@ -120,7 +124,7 @@ def specify_edges(self, net_params): "type": "highway_end", "from": "edge_{}".format(num_edges), "to": "edge_{}".format(num_edges + 1), - "length": self.end_length + "length": end_length }] return edges diff --git a/flow/networks/i210_subnetwork.py b/flow/networks/i210_subnetwork.py index d8e05efb5..b86a0dc8a 100644 --- a/flow/networks/i210_subnetwork.py +++ b/flow/networks/i210_subnetwork.py @@ -1,9 +1,18 @@ """Contains the I-210 sub-network class.""" - from flow.networks.base import Network +from flow.core.params import InitialConfig +from flow.core.params import TrafficLightParams + +ADDITIONAL_NET_PARAMS = { + # whether to include vehicle on the on-ramp + "on_ramp": False, + # whether to include the downstream slow-down edge in the network + "ghost_edge": False, +} EDGES_DISTRIBUTION = [ # Main highway + "ghost0", "119257914", "119257908#0", "119257908#1-AddedOnRampEdge", @@ -25,6 +34,12 @@ class I210SubNetwork(Network): """A network used to simulate the I-210 sub-network. + Requires from net_params: + + * **on_ramp** : whether to include vehicle on the on-ramp + * **ghost_edge** : whether to include the downstream slow-down edge in the + network + Usage ----- >>> from flow.core.params import NetParams @@ -39,103 +54,145 @@ class I210SubNetwork(Network): >>> ) """ - def specify_routes(self, net_params): - """See parent class. + def __init__(self, + name, + vehicles, + net_params, + initial_config=InitialConfig(), + traffic_lights=TrafficLightParams()): + """Initialize the I210 sub-network scenario.""" + for p in ADDITIONAL_NET_PARAMS.keys(): + if p not in net_params.additional_params: + raise KeyError('Network parameter "{}" not supplied'.format(p)) + + super(I210SubNetwork, self).__init__( + name=name, + vehicles=vehicles, + net_params=net_params, + initial_config=initial_config, + traffic_lights=traffic_lights, + ) - Routes for vehicles moving through the bay bridge from Oakland to San - Francisco. - """ + def specify_routes(self, net_params): + """See parent class.""" rts = { - # Main highway "119257914": [ - (["119257914", "119257908#0", "119257908#1-AddedOnRampEdge", - "119257908#1", "119257908#1-AddedOffRampEdge", "119257908#2", - "119257908#3"], - 1), # HOV: 1509 (on ramp: 57), Non HOV: 6869 (onramp: 16) - # (["119257914", "119257908#0", "119257908#1-AddedOnRampEdge", - # "119257908#1", "119257908#1-AddedOffRampEdge", "173381935"], - # 17 / 8378) - ], - # "119257908#0": [ - # (["119257908#0", "119257908#1-AddedOnRampEdge", "119257908#1", - # "119257908#1-AddedOffRampEdge", "119257908#2", - # "119257908#3"], - # 1.0), - # # (["119257908#0", "119257908#1-AddedOnRampEdge", "119257908#1", - # # "119257908#1-AddedOffRampEdge", "173381935"], - # # 0.5), - # ], - # "119257908#1-AddedOnRampEdge": [ - # (["119257908#1-AddedOnRampEdge", "119257908#1", - # "119257908#1-AddedOffRampEdge", "119257908#2", - # "119257908#3"], - # 1.0), - # # (["119257908#1-AddedOnRampEdge", "119257908#1", - # # "119257908#1-AddedOffRampEdge", "173381935"], - # # 0.5), - # ], - # "119257908#1": [ - # (["119257908#1", "119257908#1-AddedOffRampEdge", "119257908#2", - # "119257908#3"], - # 1.0), - # # (["119257908#1", "119257908#1-AddedOffRampEdge", "173381935"], - # # 0.5), - # ], - # "119257908#1-AddedOffRampEdge": [ - # (["119257908#1-AddedOffRampEdge", "119257908#2", - # "119257908#3"], - # 1.0), - # # (["119257908#1-AddedOffRampEdge", "173381935"], - # # 0.5), - # ], - # "119257908#2": [ - # (["119257908#2", "119257908#3"], 1), - # ], - # "119257908#3": [ - # (["119257908#3"], 1), - # ], - # - # # On-ramp - # "27414345": [ - # (["27414345", "27414342#1-AddedOnRampEdge", - # "27414342#1", - # "119257908#1-AddedOnRampEdge", "119257908#1", - # "119257908#1-AddedOffRampEdge", "119257908#2", - # "119257908#3"], - # 1 - 9 / 321), - # (["27414345", "27414342#1-AddedOnRampEdge", - # "27414342#1", - # "119257908#1-AddedOnRampEdge", "119257908#1", - # "119257908#1-AddedOffRampEdge", "173381935"], - # 9 / 321), - # ], - # "27414342#0": [ - # (["27414342#0", "27414342#1-AddedOnRampEdge", - # "27414342#1", - # "119257908#1-AddedOnRampEdge", "119257908#1", - # "119257908#1-AddedOffRampEdge", "119257908#2", - # "119257908#3"], - # 1 - 20 / 421), - # (["27414342#0", "27414342#1-AddedOnRampEdge", - # "27414342#1", - # "119257908#1-AddedOnRampEdge", "119257908#1", - # "119257908#1-AddedOffRampEdge", "173381935"], - # 20 / 421), - # ], - # "27414342#1-AddedOnRampEdge": [ - # (["27414342#1-AddedOnRampEdge", "27414342#1", "119257908#1-AddedOnRampEdge", - # "119257908#1", "119257908#1-AddedOffRampEdge", "119257908#2", - # "119257908#3"], - # 0.5), - # (["27414342#1-AddedOnRampEdge", "27414342#1", "119257908#1-AddedOnRampEdge", - # "119257908#1", "119257908#1-AddedOffRampEdge", "173381935"], - # 0.5), - # ], - # - # # Off-ramp - # "173381935": [ - # (["173381935"], 1), - # ], + (["119257914", + "119257908#0", + "119257908#1-AddedOnRampEdge", + "119257908#1", + "119257908#1-AddedOffRampEdge", + "119257908#2", + "119257908#3"], 1.0), + ] } + if net_params.additional_params["ghost_edge"]: + rts.update({ + "ghost0": [ + (["ghost0", + "119257914", + "119257908#0", + "119257908#1-AddedOnRampEdge", + "119257908#1", + "119257908#1-AddedOffRampEdge", + "119257908#2", + "119257908#3"], 1), + ], + }) + + if net_params.additional_params["on_ramp"]: + rts.update({ + # Main highway + "119257908#0": [ + (["119257908#0", + "119257908#1-AddedOnRampEdge", + "119257908#1", + "119257908#1-AddedOffRampEdge", + "119257908#2", + "119257908#3"], 1.0), + ], + "119257908#1-AddedOnRampEdge": [ + (["119257908#1-AddedOnRampEdge", + "119257908#1", + "119257908#1-AddedOffRampEdge", + "119257908#2", + "119257908#3"], 1.0), + ], + "119257908#1": [ + (["119257908#1", + "119257908#1-AddedOffRampEdge", + "119257908#2", + "119257908#3"], 1.0), + ], + "119257908#1-AddedOffRampEdge": [ + (["119257908#1-AddedOffRampEdge", + "119257908#2", + "119257908#3"], 1.0), + ], + "119257908#2": [ + (["119257908#2", + "119257908#3"], 1), + ], + "119257908#3": [ + (["119257908#3"], 1), + ], + + # On-ramp + "27414345": [ + (["27414345", + "27414342#1-AddedOnRampEdge", + "27414342#1", + "119257908#1-AddedOnRampEdge", + "119257908#1", + "119257908#1-AddedOffRampEdge", + "119257908#2", + "119257908#3"], 1 - 9 / 321), + (["27414345", + "27414342#1-AddedOnRampEdge", + "27414342#1", + "119257908#1-AddedOnRampEdge", + "119257908#1", + "119257908#1-AddedOffRampEdge", + "173381935"], 9 / 321), + ], + "27414342#0": [ + (["27414342#0", + "27414342#1-AddedOnRampEdge", + "27414342#1", + "119257908#1-AddedOnRampEdge", + "119257908#1", + "119257908#1-AddedOffRampEdge", + "119257908#2", + "119257908#3"], 1 - 20 / 421), + (["27414342#0", + "27414342#1-AddedOnRampEdge", + "27414342#1", + "119257908#1-AddedOnRampEdge", + "119257908#1", + "119257908#1-AddedOffRampEdge", + "173381935"], 20 / 421), + ], + "27414342#1-AddedOnRampEdge": [ + (["27414342#1-AddedOnRampEdge", + "27414342#1", + "119257908#1-AddedOnRampEdge", + "119257908#1", + "119257908#1-AddedOffRampEdge", + "119257908#2", + "119257908#3"], 0.5), + (["27414342#1-AddedOnRampEdge", + "27414342#1", + "119257908#1-AddedOnRampEdge", + "119257908#1", + "119257908#1-AddedOffRampEdge", + "173381935"], 0.5), + ], + + # Off-ramp + "173381935": [ + (["173381935"], 1), + ], + }) + return rts diff --git a/flow/networks/ring.py b/flow/networks/ring.py index de4d17503..ceef22a78 100755 --- a/flow/networks/ring.py +++ b/flow/networks/ring.py @@ -37,7 +37,7 @@ class RingNetwork(Network): >>> from flow.core.params import NetParams >>> from flow.core.params import VehicleParams >>> from flow.core.params import InitialConfig - >>> from flow.scenarios import RingNetwork + >>> from flow.networks import RingNetwork >>> >>> network = RingNetwork( >>> name='ring_road', diff --git a/tests/fast_tests/test_scenarios.py b/tests/fast_tests/test_scenarios.py index d72a50b17..5fccdcb3b 100644 --- a/tests/fast_tests/test_scenarios.py +++ b/tests/fast_tests/test_scenarios.py @@ -5,8 +5,11 @@ from flow.networks import BottleneckNetwork, FigureEightNetwork, \ TrafficLightGridNetwork, HighwayNetwork, RingNetwork, MergeNetwork, \ MiniCityNetwork, MultiRingNetwork +from flow.networks import I210SubNetwork from tests.setup_scripts import highway_exp_setup +import flow.config as config + __all__ = [ "MultiRingNetwork", "MiniCityNetwork" ] @@ -97,7 +100,8 @@ def test_additional_net_params(self): "speed_limit": 30, "num_edges": 1, "use_ghost_edge": False, - "ghost_speed_limit": 25 + "ghost_speed_limit": 25, + "boundary_cell_length": 300, } ) ) @@ -116,7 +120,8 @@ def test_ghost_edge(self): "speed_limit": 30, "num_edges": 1, "use_ghost_edge": False, - "ghost_speed_limit": 25 + "ghost_speed_limit": 25, + "boundary_cell_length": 300, }) ) env.reset() @@ -131,7 +136,7 @@ def test_ghost_edge(self): self.assertEqual(env.k.network.speed_limit("highway_0"), 30) # =================================================================== # - # With a ghost edge # + # With a ghost edge (300m, 25m/s) # # =================================================================== # # create the network @@ -142,13 +147,14 @@ def test_ghost_edge(self): "speed_limit": 30, "num_edges": 1, "use_ghost_edge": True, - "ghost_speed_limit": 25 + "ghost_speed_limit": 25, + "boundary_cell_length": 300, }) ) env.reset() # check the network length - self.assertEqual(env.k.network.length(), 1500.1) + self.assertEqual(env.k.network.length(), 1300.1) # check the edge list self.assertEqual(env.k.network.get_edge_list(), @@ -158,6 +164,35 @@ def test_ghost_edge(self): self.assertEqual(env.k.network.speed_limit("highway_0"), 30) self.assertEqual(env.k.network.speed_limit("highway_end"), 25) + # =================================================================== # + # With a ghost edge (500m, 10m/s) # + # =================================================================== # + + # create the network + env, _, _ = highway_exp_setup( + net_params=NetParams(additional_params={ + "length": 1000, + "lanes": 4, + "speed_limit": 30, + "num_edges": 1, + "use_ghost_edge": True, + "ghost_speed_limit": 10, + "boundary_cell_length": 500, + }) + ) + env.reset() + + # check the network length + self.assertEqual(env.k.network.length(), 1500.1) + + # check the edge list + self.assertEqual(env.k.network.get_edge_list(), + ["highway_0", "highway_end"]) + + # check the speed limits of the edges + self.assertEqual(env.k.network.speed_limit("highway_0"), 30) + self.assertEqual(env.k.network.speed_limit("highway_end"), 10) + class TestRingNetwork(unittest.TestCase): @@ -219,6 +254,150 @@ def test_additional_net_params(self): ) +class TestI210SubNetwork(unittest.TestCase): + + """Tests I210SubNetwork in flow/networks/i210_subnetwork.py.""" + + def test_additional_net_params(self): + """Ensures that not returning the correct params leads to an error.""" + self.assertTrue( + test_additional_params( + network_class=I210SubNetwork, + additional_params={ + "on_ramp": False, + "ghost_edge": False, + } + ) + ) + + def test_specify_routes(self): + """Validates that the routes are properly specified for the network. + + This is done simply by checking the initial edges routes are specified + from, which alternates based on choice of network configuration. + + This method tests the routes for the following cases: + + 1. on_ramp = False, ghost_edge = False + 2. on_ramp = True, ghost_edge = False + 3. on_ramp = False, ghost_edge = True + 4. on_ramp = True, ghost_edge = True + """ + # test case 1 + network = I210SubNetwork( + name='test-3', + vehicles=VehicleParams(), + net_params=NetParams( + template=os.path.join( + config.PROJECT_PATH, + "examples/exp_configs/templates/sumo/test2.net.xml" + ), + additional_params={ + "on_ramp": False, + "ghost_edge": False, + }, + ), + ) + + self.assertEqual( + ['119257914'], + sorted(list(network.specify_routes(network.net_params).keys())) + ) + + del network + + # test case 2 + network = I210SubNetwork( + name='test-3', + vehicles=VehicleParams(), + net_params=NetParams( + template=os.path.join( + config.PROJECT_PATH, + "examples/exp_configs/templates/sumo/test2.net.xml" + ), + additional_params={ + "on_ramp": True, + "ghost_edge": True, + }, + ), + ) + + self.assertEqual( + ['119257908#0', + '119257908#1', + '119257908#1-AddedOffRampEdge', + '119257908#1-AddedOnRampEdge', + '119257908#2', + '119257908#3', + '119257914', + '173381935', + '27414342#0', + '27414342#1-AddedOnRampEdge', + '27414345', + 'ghost0'], + sorted(list(network.specify_routes(network.net_params).keys())) + ) + + del network + + # test case 3 + network = I210SubNetwork( + name='test-3', + vehicles=VehicleParams(), + net_params=NetParams( + template=os.path.join( + config.PROJECT_PATH, + "examples/exp_configs/templates/sumo/test2.net.xml" + ), + additional_params={ + "on_ramp": False, + "ghost_edge": True, + }, + ), + ) + + self.assertEqual( + ['119257914', 'ghost0'], + sorted(list(network.specify_routes(network.net_params).keys())) + ) + + del network + + # test case 4 + network = I210SubNetwork( + name='test-3', + vehicles=VehicleParams(), + net_params=NetParams( + template=os.path.join( + config.PROJECT_PATH, + "examples/exp_configs/templates/sumo/test2.net.xml" + ), + additional_params={ + "on_ramp": True, + "ghost_edge": True, + }, + ), + ) + + self.assertEqual( + ['119257908#0', + '119257908#1', + '119257908#1-AddedOffRampEdge', + '119257908#1-AddedOnRampEdge', + '119257908#2', + '119257908#3', + '119257914', + '173381935', + '27414342#0', + '27414342#1-AddedOnRampEdge', + '27414345', + 'ghost0'], + sorted(list(network.specify_routes(network.net_params).keys())) + ) + + del network + + ############################################################################### # Utility methods # ############################################################################### diff --git a/tests/fast_tests/test_vehicles.py b/tests/fast_tests/test_vehicles.py index 1ae2d1cf0..7e1405007 100644 --- a/tests/fast_tests/test_vehicles.py +++ b/tests/fast_tests/test_vehicles.py @@ -261,6 +261,7 @@ def test_no_junctions_highway(self): "num_edges": 1, "use_ghost_edge": False, "ghost_speed_limit": 25, + "boundary_cell_length": 300, } net_params = NetParams(additional_params=additional_net_params) vehicles = VehicleParams() @@ -335,6 +336,7 @@ def test_no_junctions_highway(self): "num_edges": 1, "use_ghost_edge": False, "ghost_speed_limit": 25, + "boundary_cell_length": 300, } net_params = NetParams(additional_params=additional_net_params) vehicles = VehicleParams() @@ -405,6 +407,7 @@ def test_no_junctions_highway(self): "num_edges": 3, "use_ghost_edge": False, "ghost_speed_limit": 25, + "boundary_cell_length": 300, } net_params = NetParams(additional_params=additional_net_params) vehicles = VehicleParams() @@ -474,6 +477,7 @@ def test_no_junctions_highway(self): "num_edges": 3, "use_ghost_edge": False, "ghost_speed_limit": 25, + "boundary_cell_length": 300, } net_params = NetParams(additional_params=additional_net_params) vehicles = VehicleParams() diff --git a/tests/setup_scripts.py b/tests/setup_scripts.py index ac88d2e42..343bad906 100644 --- a/tests/setup_scripts.py +++ b/tests/setup_scripts.py @@ -346,6 +346,7 @@ def highway_exp_setup(sim_params=None, "num_edges": 1, "use_ghost_edge": False, "ghost_speed_limit": 25, + "boundary_cell_length": 300, } net_params = NetParams(additional_params=additional_net_params) From 9e16b19092a14e0912cefd687a209e5aaba79a3f Mon Sep 17 00:00:00 2001 From: Aboudy Kreidieh Date: Mon, 15 Jun 2020 21:08:20 -0700 Subject: [PATCH 75/86] I210 updated (#970) * updated i210 simulation * increased the time horizon * added edge starts --- .../exp_configs/non_rl/i210_subnetwork.py | 27 +++--- .../i210_with_ghost_cell_with_downstream.xml | 14 +-- flow/networks/i210_subnetwork.py | 88 +++++++++++++++++++ 3 files changed, 112 insertions(+), 17 deletions(-) diff --git a/examples/exp_configs/non_rl/i210_subnetwork.py b/examples/exp_configs/non_rl/i210_subnetwork.py index eda037068..b0c58c894 100644 --- a/examples/exp_configs/non_rl/i210_subnetwork.py +++ b/examples/exp_configs/non_rl/i210_subnetwork.py @@ -24,11 +24,15 @@ # whether to include the downstream slow-down edge in the network WANT_DOWNSTREAM_BOUNDARY = True # whether to include vehicles on the on-ramp -ON_RAMP = True +ON_RAMP = False # the inflow rate of vehicles (in veh/hr) -INFLOW_RATE = 5 * 2215 +INFLOW_RATE = 2050 # the speed of inflowing vehicles from the main edge (in m/s) -INFLOW_SPEED = 24.1 +INFLOW_SPEED = 25.5 +# horizon over which to run the env +HORIZON = 1500 +# steps to run before follower-stopper is allowed to take control +WARMUP_STEPS = 600 # =========================================================================== # # Specify the path to the network template. # @@ -75,12 +79,13 @@ inflow = InFlows() # main highway -inflow.add( - veh_type="human", - edge="ghost0" if WANT_GHOST_CELL else "119257914", - vehs_per_hour=INFLOW_RATE, - departLane="best", - departSpeed=INFLOW_SPEED) +for lane in [0, 1, 2, 3, 4]: + inflow.add( + veh_type="human", + edge="ghost0" if WANT_GHOST_CELL else "119257914", + vehs_per_hour=INFLOW_RATE, + departLane=lane, + departSpeed=INFLOW_SPEED) # on ramp if ON_RAMP: inflow.add( @@ -123,7 +128,9 @@ # environment related parameters (see flow.core.params.EnvParams) env=EnvParams( - horizon=10000, + horizon=HORIZON, + warmup_steps=WARMUP_STEPS, + sims_per_step=3 ), # network-related parameters (see flow.core.params.NetParams and the diff --git a/examples/exp_configs/templates/sumo/i210_with_ghost_cell_with_downstream.xml b/examples/exp_configs/templates/sumo/i210_with_ghost_cell_with_downstream.xml index 10d4d8d45..ee508b730 100644 --- a/examples/exp_configs/templates/sumo/i210_with_ghost_cell_with_downstream.xml +++ b/examples/exp_configs/templates/sumo/i210_with_ghost_cell_with_downstream.xml @@ -3501,11 +3501,11 @@ - - - - - + + + + + @@ -4727,8 +4727,8 @@ - - + + diff --git a/flow/networks/i210_subnetwork.py b/flow/networks/i210_subnetwork.py index b86a0dc8a..f4315b07f 100644 --- a/flow/networks/i210_subnetwork.py +++ b/flow/networks/i210_subnetwork.py @@ -65,6 +65,26 @@ def __init__(self, if p not in net_params.additional_params: raise KeyError('Network parameter "{}" not supplied'.format(p)) + # The length of each edge and junction is a fixed term that can be + # found in the xml file. + self.length_with_ghost_edge = [ + ("ghost0", 573.08), + (":300944378_0", 0.30), + ("119257914", 61.28), + (":300944379_0", 0.31), + ("119257908#0", 696.97), + (":300944436_0", 2.87), + ("119257908#1-AddedOnRampEdge", 97.20), + (":119257908#1-AddedOnRampNode_0", 3.24), + ("119257908#1", 239.68), + (":119257908#1-AddedOffRampNode_0", 3.24), + ("119257908#1-AddedOffRampEdge", 98.50), + (":1686591010_1", 5.46), + ("119257908#2", 576.61), + (":1842086610_1", 4.53), + ("119257908#3", 17.49), + ] + super(I210SubNetwork, self).__init__( name=name, vehicles=vehicles, @@ -196,3 +216,71 @@ def specify_routes(self, net_params): }) return rts + + def specify_edge_starts(self): + """See parent class.""" + if self.net_params.additional_params["ghost_edge"]: + # Collect the names of all the edges. + edge_names = [ + e[0] for e in self.length_with_ghost_edge + if not e[0].startswith(":") + ] + + edge_starts = [] + for edge in edge_names: + # Find the position of the edge in the list of tuples. + edge_pos = next( + i for i in range(len(self.length_with_ghost_edge)) + if self.length_with_ghost_edge[i][0] == edge + ) + + # Sum of lengths until the edge is reached to compute the + # starting position of the edge. + edge_starts.append(( + edge, + sum(e[1] for e in self.length_with_ghost_edge[:edge_pos]) + )) + + elif self.net_params.additional_params["on_ramp"]: + # TODO: this will incorporated in the future, if needed. + edge_starts = [] + + else: + # TODO: this will incorporated in the future, if needed. + edge_starts = [] + + return edge_starts + + def specify_internal_edge_starts(self): + """See parent class.""" + if self.net_params.additional_params["ghost_edge"]: + # Collect the names of all the junctions. + edge_names = [ + e[0] for e in self.length_with_ghost_edge + if e[0].startswith(":") + ] + + edge_starts = [] + for edge in edge_names: + # Find the position of the edge in the list of tuples. + edge_pos = next( + i for i in range(len(self.length_with_ghost_edge)) + if self.length_with_ghost_edge[i][0] == edge + ) + + # Sum of lengths until the edge is reached to compute the + # starting position of the edge. + edge_starts.append(( + edge, + sum(e[1] for e in self.length_with_ghost_edge[:edge_pos]) + )) + + elif self.net_params.additional_params["on_ramp"]: + # TODO: this will incorporated in the future, if needed. + edge_starts = [] + + else: + # TODO: this will incorporated in the future, if needed. + edge_starts = [] + + return edge_starts From 7824d88317c84df8de4e93e57773ba217e1638e5 Mon Sep 17 00:00:00 2001 From: liljonnystyle Date: Fri, 19 Jun 2020 13:11:21 -0700 Subject: [PATCH 76/86] implement HighwayNetwork for Time-Space Diagrams (#979) * implement HighwayNetwork for Time-Space Diagrams * fix flake8 * PR fixes * update docstrings Co-authored-by: AboudyKreidieh --- flow/visualize/time_space_diagram.py | 41 ++++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/flow/visualize/time_space_diagram.py b/flow/visualize/time_space_diagram.py index bc26ad855..3c7ab8b21 100644 --- a/flow/visualize/time_space_diagram.py +++ b/flow/visualize/time_space_diagram.py @@ -17,7 +17,7 @@ python time_space_diagram.py .csv .json """ from flow.utils.rllib import get_flow_params -from flow.networks import RingNetwork, FigureEightNetwork, MergeNetwork, I210SubNetwork +from flow.networks import RingNetwork, FigureEightNetwork, MergeNetwork, I210SubNetwork, HighwayNetwork import argparse from collections import defaultdict @@ -38,7 +38,8 @@ RingNetwork, FigureEightNetwork, MergeNetwork, - I210SubNetwork + I210SubNetwork, + HighwayNetwork ] @@ -103,7 +104,7 @@ def get_time_space_data(data, params): Returns ------- - ndarray (or dict of ndarray) + ndarray (or dict < str, np.ndarray >) 3d array (n_segments x 2 x 2) containing segments to be plotted. every inner 2d array is comprised of two 1d arrays representing [start time, start distance] and [end time, end distance] pairs. @@ -126,7 +127,8 @@ def get_time_space_data(data, params): RingNetwork: _ring_road, MergeNetwork: _merge, FigureEightNetwork: _figure_eight, - I210SubNetwork: _i210_subnetwork + I210SubNetwork: _i210_subnetwork, + HighwayNetwork: _highway, } # Get the function from switcher dictionary @@ -167,6 +169,33 @@ def _merge(data): return segs, data +def _highway(data): + r"""Generate time and position data for the highway. + + We generate plots for all lanes, so the segments are wrapped in + a dictionary. + + Parameters + ---------- + data : pd.DataFrame + cleaned dataframe of the trajectory data + + Returns + ------- + ndarray + 3d array (n_segments x 2 x 2) containing segments to be plotted. + every inner 2d array is comprised of two 1d arrays representing + [start time, start distance] and [end time, end distance] pairs. + pd.DataFrame + modified trajectory dataframe + """ + data.loc[:, :] = data[(data['distance'] > 500)] + data.loc[:, :] = data[(data['distance'] < 2300)] + segs = data[['time_step', 'distance', 'next_time', 'next_pos']].values.reshape((len(data), 2, 2)) + + return segs, data + + def _ring_road(data): r"""Generate position and speed data for the ring road. @@ -205,7 +234,7 @@ def _i210_subnetwork(data): Returns ------- - dict of ndarray + dict < str, np.ndarray > dictionary of 3d array (n_segments x 2 x 2) containing segments to be plotted. the dictionary is keyed on lane numbers, with the values being the 3d array representing the segments. every inner @@ -329,6 +358,8 @@ def _get_abs_pos(df, params): 'bottom_to_top': intersection / 2 + inner, 'right_to_left': junction + 3 * inner, } + elif params['network'] == HighwayNetwork: + return df['x'] else: edgestarts = defaultdict(float) From 641f724a9fa661b95075f877c2c1fe7dd10ca939 Mon Sep 17 00:00:00 2001 From: Aboudy Kreidieh Date: Mon, 22 Jun 2020 10:24:57 -0700 Subject: [PATCH 77/86] fixed h-baselines bug (#982) * fixed h-baselines bug * potential bug fix --- examples/train.py | 37 ++++--------------------------- tests/fast_tests/test_examples.py | 10 ++++----- 2 files changed, 9 insertions(+), 38 deletions(-) diff --git a/examples/train.py b/examples/train.py index 1b2f22476..5f8edbb22 100644 --- a/examples/train.py +++ b/examples/train.py @@ -213,13 +213,10 @@ def train_rllib(submodule, flags): run_experiments({flow_params["exp_tag"]: exp_config}) -def train_h_baselines(flow_params, args, multiagent): +def train_h_baselines(env_name, args, multiagent): """Train policies using SAC and TD3 with h-baselines.""" from hbaselines.algorithms import OffPolicyRLAlgorithm from hbaselines.utils.train import parse_options, get_hyperparameters - from hbaselines.envs.mixed_autonomy import FlowEnv - - flow_params = deepcopy(flow_params) # Get the command-line arguments that are relevant here args = parse_options(description="", example_usage="", args=args) @@ -227,31 +224,6 @@ def train_h_baselines(flow_params, args, multiagent): # the base directory that the logged data will be stored in base_dir = "training_data" - # Create the training environment. - env = FlowEnv( - flow_params, - multiagent=multiagent, - shared=args.shared, - maddpg=args.maddpg, - render=args.render, - version=0 - ) - - # Create the evaluation environment. - if args.evaluate: - eval_flow_params = deepcopy(flow_params) - eval_flow_params['env'].evaluate = True - eval_env = FlowEnv( - eval_flow_params, - multiagent=multiagent, - shared=args.shared, - maddpg=args.maddpg, - render=args.render_eval, - version=1 - ) - else: - eval_env = None - for i in range(args.n_training): # value of the next seed seed = args.seed + i @@ -299,8 +271,8 @@ def train_h_baselines(flow_params, args, multiagent): # Create the algorithm object. alg = OffPolicyRLAlgorithm( policy=policy, - env=env, - eval_env=eval_env, + env="flow:{}".format(env_name), + eval_env="flow:{}".format(env_name) if args.evaluate else None, **hp ) @@ -393,8 +365,7 @@ def main(args): elif flags.rl_trainer.lower() == "stable-baselines": train_stable_baselines(submodule, flags) elif flags.rl_trainer.lower() == "h-baselines": - flow_params = submodule.flow_params - train_h_baselines(flow_params, args, multiagent) + train_h_baselines(flags.exp_config, args, multiagent) else: raise ValueError("rl_trainer should be either 'rllib', 'h-baselines', " "or 'stable-baselines'.") diff --git a/tests/fast_tests/test_examples.py b/tests/fast_tests/test_examples.py index 0b385f28a..b5faf6517 100644 --- a/tests/fast_tests/test_examples.py +++ b/tests/fast_tests/test_examples.py @@ -229,11 +229,11 @@ class TestHBaselineExamples(unittest.TestCase): confirming that it runs. """ @staticmethod - def run_exp(flow_params, multiagent): + def run_exp(env_name, multiagent): train_h_baselines( - flow_params=flow_params, + env_name=env_name, args=[ - flow_params["env_name"].__name__, + env_name, "--initial_exploration_steps", "1", "--total_steps", "10" ], @@ -241,10 +241,10 @@ def run_exp(flow_params, multiagent): ) def test_singleagent_ring(self): - self.run_exp(singleagent_ring.copy(), multiagent=False) + self.run_exp("singleagent_ring", multiagent=False) def test_multiagent_ring(self): - self.run_exp(multiagent_ring.copy(), multiagent=True) + self.run_exp("multiagent_ring", multiagent=True) class TestRllibExamples(unittest.TestCase): From af0b4f68dcadb542b2b472c1d1d59e9adafa86a1 Mon Sep 17 00:00:00 2001 From: Kathy Jang Date: Mon, 22 Jun 2020 12:30:42 -0700 Subject: [PATCH 78/86] Replicated changes in 867. Done bug (#980) * Replicated changes in 867. Changes only made to traci * Aimsun changes minus reset --- flow/core/kernel/vehicle/aimsun.py | 17 +++++++++++++++++ flow/core/kernel/vehicle/traci.py | 7 +++++-- flow/envs/multiagent/base.py | 2 +- 3 files changed, 23 insertions(+), 3 deletions(-) diff --git a/flow/core/kernel/vehicle/aimsun.py b/flow/core/kernel/vehicle/aimsun.py index ce0d026e5..16c94558a 100644 --- a/flow/core/kernel/vehicle/aimsun.py +++ b/flow/core/kernel/vehicle/aimsun.py @@ -65,6 +65,7 @@ def __init__(self, # number of vehicles to exit the network for every time-step self._num_arrived = [] self._arrived_ids = [] + self._arrived_rl_ids = [] # contains conversion from Flow-ID to Aimsun-ID self._id_aimsun2flow = {} @@ -174,11 +175,17 @@ def update(self, reset): added_vehicles = self.kernel_api.get_entered_ids() exited_vehicles = self.kernel_api.get_exited_ids() + # keep track of arrived rl vehicles + arrived_rl_ids = [] + # add the new vehicles if they should be tracked for aimsun_id in added_vehicles: veh_type = self.kernel_api.get_vehicle_type_name(aimsun_id) if veh_type in self.tracked_vehicle_types: self._add_departed(aimsun_id) + if aimsun_id in self.get_rl_ids(): + arrived_rl_ids.append(aimsun_id) + self._arrived_rl_ids.append(arrived_rl_ids) # remove the exited vehicles if they were tracked if not reset: @@ -639,6 +646,16 @@ def get_arrived_ids(self): """See parent class.""" raise NotImplementedError + def get_arrived_rl_ids(self, k=1): + """See parent class.""" + if len(self._arrived_rl_ids) > 0: + arrived = [] + for arr in self._arrived_rl_ids[-k:]: + arrived.extend(arr) + return arrived + else: + return 0 + def get_departed_ids(self): """See parent class.""" raise NotImplementedError diff --git a/flow/core/kernel/vehicle/traci.py b/flow/core/kernel/vehicle/traci.py index 134bac49f..6f119b7bb 100644 --- a/flow/core/kernel/vehicle/traci.py +++ b/flow/core/kernel/vehicle/traci.py @@ -521,10 +521,13 @@ def get_arrived_ids(self): """See parent class.""" return self._arrived_ids - def get_arrived_rl_ids(self): + def get_arrived_rl_ids(self, k=1): """See parent class.""" if len(self._arrived_rl_ids) > 0: - return self._arrived_rl_ids[-1] + arrived = [] + for arr in self._arrived_rl_ids[-k:]: + arrived.extend(arr) + return arrived else: return 0 diff --git a/flow/envs/multiagent/base.py b/flow/envs/multiagent/base.py index ec95474c6..2d9c3cd78 100644 --- a/flow/envs/multiagent/base.py +++ b/flow/envs/multiagent/base.py @@ -122,7 +122,7 @@ def step(self, rl_actions): else: reward = self.compute_reward(rl_actions, fail=crash) - for rl_id in self.k.vehicle.get_arrived_rl_ids(): + for rl_id in self.k.vehicle.get_arrived_rl_ids(self.env_params.sims_per_step): done[rl_id] = True reward[rl_id] = 0 states[rl_id] = np.zeros(self.observation_space.shape[0]) From c319a6c0d9ebf20777f3ebc42fda45947c6fef0b Mon Sep 17 00:00:00 2001 From: liljonnystyle Date: Wed, 8 Jul 2020 11:13:23 -0700 Subject: [PATCH 79/86] merge custom output and failsafes to master (#981) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * merge custom output and failsafes to master * add write_to_csv() function to master * include pipeline README.md * add data pipeline __init__ * add experiment.py changes * Update simulate.py * Update simulate.py * Update simulate.py * Update experiment.py * Update experiment.py * fix flake8 issues * fix flake8 issues * fixed h-baselines bug * Update experiment.py * potential bug fix * merge custom output and failsafes to master * add write_to_csv() function to master * include pipeline README.md * add data pipeline __init__ * add experiment.py changes * Update simulate.py * Update simulate.py * Update simulate.py * Update experiment.py * Update experiment.py * fix flake8 issues * fix flake8 issues * Update experiment.py * Replicated changes in 867. Done bug (#980) * Replicated changes in 867. Changes only made to traci * Aimsun changes minus reset * address aboudy comments * revert change * change warning print to ValueError message * update to new update_accel methods * address brent comments * fix import typo * address comments * add display_warnings boolean * add get_next_speed() function to base vehicle class * revert addition of get_next_speed * merge custom output and failsafes to master * add write_to_csv() function to master * include pipeline README.md * add data pipeline __init__ * add experiment.py changes * Update simulate.py * Update simulate.py * Update simulate.py * Update experiment.py * Update experiment.py * fix flake8 issues * fix flake8 issues * Update experiment.py * add experiment.py changes * Update simulate.py * Update simulate.py * Update simulate.py * Update experiment.py * Update experiment.py * fix flake8 issues * address aboudy comments * revert change * change warning print to ValueError message * update to new update_accel methods * address brent comments * fix import typo * address comments * add display_warnings boolean * add get_next_speed() function to base vehicle class * revert addition of get_next_speed * remove duped line from rebase * Update base_controller.py * fix some bugs * revert change to get_feasible_action call signature * change print syntax to be python3.5 compliant * add tests for new failsafe features * fix failsafe unit tests * fix failsafe unit tests * fix unittest syntax * fix typo * smooth default to True * rearrange raise exception for test coverage * some minor fixes * cleanup * moved simulation logging to the simulation kernel (#991) * moved simulation logging to the simulation kernel * pydocstyle * PR fixes * bug fix Co-authored-by: AboudyKreidieh Co-authored-by: Kathy Jang Co-authored-by: Nathan Lichtlé --- flow/controllers/base_controller.py | 169 ++++++++++++++-- flow/controllers/car_following_models.py | 52 +++-- flow/core/experiment.py | 27 +-- flow/core/kernel/simulation/traci.py | 182 ++++++++++++++++-- flow/core/kernel/vehicle/base.py | 35 +++- flow/core/kernel/vehicle/traci.py | 66 ++++++- flow/visualize/time_space_diagram.py | 9 +- tests/fast_tests/test_controllers.py | 169 ++++++++++++++++ .../fast_tests/test_experiment_base_class.py | 36 +++- 9 files changed, 665 insertions(+), 80 deletions(-) diff --git a/flow/controllers/base_controller.py b/flow/controllers/base_controller.py index cef92d573..3c9985360 100755 --- a/flow/controllers/base_controller.py +++ b/flow/controllers/base_controller.py @@ -34,8 +34,12 @@ class BaseController(metaclass=ABCMeta): specified to in this model are as desired. delay : int delay in applying the action (time) - fail_safe : str - Should be either "instantaneous" or "safe_velocity" + fail_safe : list of str or str + List of failsafes which can be "instantaneous", "safe_velocity", + "feasible_accel", or "obey_speed_limit". The order of applying the + falsafes will be based on the order in the list. + display_warnings : bool + Flag for toggling on/off printing failsafe warnings to screen. noise : double variance of the gaussian from which to sample a noisy acceleration """ @@ -45,6 +49,7 @@ def __init__(self, car_following_params, delay=0, fail_safe=None, + display_warnings=True, noise=0): """Instantiate the base class for acceleration behavior.""" self.veh_id = veh_id @@ -56,7 +61,29 @@ def __init__(self, self.delay = delay # longitudinal failsafe used by the vehicle - self.fail_safe = fail_safe + if isinstance(fail_safe, str): + failsafe_list = [fail_safe] + elif isinstance(fail_safe, list) or fail_safe is None: + failsafe_list = fail_safe + else: + failsafe_list = None + raise ValueError("fail_safe should be string or list of strings. Setting fail_safe to None\n") + + failsafe_map = { + 'instantaneous': self.get_safe_action_instantaneous, + 'safe_velocity': self.get_safe_velocity_action, + 'feasible_accel': lambda _, accel: self.get_feasible_action(accel), + 'obey_speed_limit': self.get_obey_speed_limit_action + } + self.failsafes = [] + if failsafe_list: + for check in failsafe_list: + if check in failsafe_map: + self.failsafes.append(failsafe_map.get(check)) + else: + raise ValueError('Skipping {}, as it is not a valid failsafe.'.format(check)) + + self.display_warnings = display_warnings self.max_accel = car_following_params.controller_params['accel'] # max deaccel should always be a positive @@ -77,8 +104,8 @@ def get_action(self, env): time step. This method also augments the controller with the desired level of - stochastic noise, and utlizes the "instantaneous" or "safe_velocity" - failsafes if requested. + stochastic noise, and utlizes the "instantaneous", "safe_velocity", + "feasible_accel", and/or "obey_speed_limit" failsafes if requested. Parameters ---------- @@ -90,6 +117,12 @@ def get_action(self, env): float the modified form of the acceleration """ + # clear the current stored accels of this vehicle to None + env.k.vehicle.update_accel(self.veh_id, None, noise=False, failsafe=False) + env.k.vehicle.update_accel(self.veh_id, None, noise=False, failsafe=True) + env.k.vehicle.update_accel(self.veh_id, None, noise=True, failsafe=False) + env.k.vehicle.update_accel(self.veh_id, None, noise=True, failsafe=True) + # this is to avoid abrupt decelerations when a vehicle has just entered # a network and it's data is still not subscribed if len(env.k.vehicle.get_edge(self.veh_id)) == 0: @@ -107,16 +140,26 @@ def get_action(self, env): if accel is None: return None + # store the acceleration without noise to each vehicle + # run fail safe if requested + env.k.vehicle.update_accel(self.veh_id, accel, noise=False, failsafe=False) + accel_no_noise_with_failsafe = accel + + for failsafe in self.failsafes: + accel_no_noise_with_failsafe = failsafe(env, accel_no_noise_with_failsafe) + + env.k.vehicle.update_accel(self.veh_id, accel_no_noise_with_failsafe, noise=False, failsafe=True) + # add noise to the accelerations, if requested if self.accel_noise > 0: - accel += np.random.normal(0, self.accel_noise) + accel += np.sqrt(env.sim_step) * np.random.normal(0, self.accel_noise) + env.k.vehicle.update_accel(self.veh_id, accel, noise=True, failsafe=False) - # run the failsafes, if requested - if self.fail_safe == 'instantaneous': - accel = self.get_safe_action_instantaneous(env, accel) - elif self.fail_safe == 'safe_velocity': - accel = self.get_safe_velocity_action(env, accel) + # run the fail-safes, if requested + for failsafe in self.failsafes: + accel = failsafe(env, accel) + env.k.vehicle.update_accel(self.veh_id, accel, noise=True, failsafe=True) return accel def get_safe_action_instantaneous(self, env, action): @@ -162,6 +205,13 @@ def get_safe_action_instantaneous(self, env, action): # if the vehicle will crash into the vehicle ahead of it in the # next time step (assuming the vehicle ahead of it is not # moving), then stop immediately + if self.display_warnings: + print( + "=====================================\n" + "Vehicle {} is about to crash. Instantaneous acceleration " + "clipping applied.\n" + "=====================================".format(self.veh_id)) + return -this_vel / sim_step else: # if the vehicle is not in danger of crashing, continue with @@ -223,8 +273,8 @@ def safe_velocity(self, env): Returns ------- float - maximum safe velocity given a maximum deceleration and delay in - performing the breaking action + maximum safe velocity given a maximum deceleration, delay in + performing the breaking action, and speed limit """ lead_id = env.k.vehicle.get_leader(self.veh_id) lead_vel = env.k.vehicle.get_speed(lead_id) @@ -235,4 +285,97 @@ def safe_velocity(self, env): v_safe = 2 * h / env.sim_step + dv - this_vel * (2 * self.delay) + # check for speed limit FIXME: this is not called + # this_edge = env.k.vehicle.get_edge(self.veh_id) + # edge_speed_limit = env.k.network.speed_limit(this_edge) + + if this_vel > v_safe: + if self.display_warnings: + print( + "=====================================\n" + "Speed of vehicle {} is greater than safe speed. Safe velocity " + "clipping applied.\n" + "=====================================".format(self.veh_id)) + return v_safe + + def get_obey_speed_limit_action(self, env, action): + """Perform the "obey_speed_limit" failsafe action. + + Checks if the computed acceleration would put us above edge speed limit. + If it would, output the acceleration that would put at the speed limit + velocity. + + Parameters + ---------- + env : flow.envs.Env + current environment, which contains information of the state of the + network at the current time step + action : float + requested acceleration action + + Returns + ------- + float + the requested action clipped by the speed limit + """ + # check for speed limit + this_edge = env.k.vehicle.get_edge(self.veh_id) + edge_speed_limit = env.k.network.speed_limit(this_edge) + + this_vel = env.k.vehicle.get_speed(self.veh_id) + sim_step = env.sim_step + + if this_vel + action * sim_step > edge_speed_limit: + if edge_speed_limit > 0: + if self.display_warnings: + print( + "=====================================\n" + "Speed of vehicle {} is greater than speed limit. Obey " + "speed limit clipping applied.\n" + "=====================================".format(self.veh_id)) + return (edge_speed_limit - this_vel) / sim_step + else: + return -this_vel / sim_step + else: + return action + + def get_feasible_action(self, action): + """Perform the "feasible_accel" failsafe action. + + Checks if the computed acceleration would put us above maximum + acceleration or deceleration. If it would, output the acceleration + equal to maximum acceleration or deceleration. + + Parameters + ---------- + action : float + requested acceleration action + + Returns + ------- + float + the requested action clipped by the feasible acceleration or + deceleration. + """ + if action > self.max_accel: + action = self.max_accel + + if self.display_warnings: + print( + "=====================================\n" + "Acceleration of vehicle {} is greater than the max " + "acceleration. Feasible acceleration clipping applied.\n" + "=====================================".format(self.veh_id)) + + if action < -self.max_deaccel: + action = -self.max_deaccel + + if self.display_warnings: + print( + "=====================================\n" + "Deceleration of vehicle {} is greater than the max " + "deceleration. Feasible acceleration clipping applied.\n" + "=====================================".format(self.veh_id)) + + return action diff --git a/flow/controllers/car_following_models.py b/flow/controllers/car_following_models.py index 42c9b2a9b..f5b7399bc 100755 --- a/flow/controllers/car_following_models.py +++ b/flow/controllers/car_following_models.py @@ -56,7 +56,8 @@ def __init__(self, v_des=8, time_delay=0.0, noise=0, - fail_safe=None): + fail_safe=None, + display_warnings=True): """Instantiate a CFM controller.""" BaseController.__init__( self, @@ -64,7 +65,9 @@ def __init__(self, car_following_params, delay=time_delay, fail_safe=fail_safe, - noise=noise) + noise=noise, + display_warnings=display_warnings, + ) self.veh_id = veh_id self.k_d = k_d @@ -132,7 +135,8 @@ def __init__(self, v_des=8, time_delay=0.0, noise=0, - fail_safe=None): + fail_safe=None, + display_warnings=True): """Instantiate a Bilateral car-following model controller.""" BaseController.__init__( self, @@ -140,7 +144,9 @@ def __init__(self, car_following_params, delay=time_delay, fail_safe=fail_safe, - noise=noise) + noise=noise, + display_warnings=display_warnings, + ) self.veh_id = veh_id self.k_d = k_d @@ -212,7 +218,8 @@ def __init__(self, a=0, time_delay=0.0, noise=0, - fail_safe=None): + fail_safe=None, + display_warnings=True): """Instantiate a Linear Adaptive Cruise controller.""" BaseController.__init__( self, @@ -220,7 +227,9 @@ def __init__(self, car_following_params, delay=time_delay, fail_safe=fail_safe, - noise=noise) + noise=noise, + display_warnings=display_warnings, + ) self.veh_id = veh_id self.k_1 = k_1 @@ -289,7 +298,8 @@ def __init__(self, v_max=30, time_delay=0, noise=0, - fail_safe=None): + fail_safe=None, + display_warnings=True): """Instantiate an Optimal Vehicle Model controller.""" BaseController.__init__( self, @@ -297,7 +307,9 @@ def __init__(self, car_following_params, delay=time_delay, fail_safe=fail_safe, - noise=noise) + noise=noise, + display_warnings=display_warnings, + ) self.veh_id = veh_id self.v_max = v_max self.alpha = alpha @@ -364,7 +376,8 @@ def __init__(self, h_st=5, time_delay=0.0, noise=0, - fail_safe=None): + fail_safe=None, + display_warnings=True): """Instantiate a Linear OVM controller.""" BaseController.__init__( self, @@ -372,7 +385,9 @@ def __init__(self, car_following_params, delay=time_delay, fail_safe=fail_safe, - noise=noise) + noise=noise, + display_warnings=display_warnings, + ) self.veh_id = veh_id # 4.8*1.85 for case I, 3.8*1.85 for case II, per Nakayama self.v_max = v_max @@ -445,6 +460,7 @@ def __init__(self, time_delay=0.0, noise=0, fail_safe=None, + display_warnings=True, car_following_params=None): """Instantiate an IDM controller.""" BaseController.__init__( @@ -453,7 +469,9 @@ def __init__(self, car_following_params, delay=time_delay, fail_safe=fail_safe, - noise=noise) + noise=noise, + display_warnings=display_warnings, + ) self.v0 = v0 self.T = T self.a = a @@ -546,7 +564,8 @@ def __init__(self, tau=1, delay=0, noise=0, - fail_safe=None): + fail_safe=None, + display_warnings=True): """Instantiate a Gipps' controller.""" BaseController.__init__( self, @@ -554,8 +573,9 @@ def __init__(self, car_following_params, delay=delay, fail_safe=fail_safe, - noise=noise - ) + noise=noise, + display_warnings=display_warnings, + ) self.v_desired = v0 self.acc = acc @@ -627,7 +647,8 @@ def __init__(self, want_max_accel=False, time_delay=0, noise=0, - fail_safe=None): + fail_safe=None, + display_warnings=True): """Instantiate an Bando controller.""" BaseController.__init__( self, @@ -636,6 +657,7 @@ def __init__(self, delay=time_delay, fail_safe=fail_safe, noise=noise, + display_warnings=display_warnings, ) self.veh_id = veh_id self.v_max = v_max diff --git a/flow/core/experiment.py b/flow/core/experiment.py index 69a78cb0e..d97f96582 100755 --- a/flow/core/experiment.py +++ b/flow/core/experiment.py @@ -1,10 +1,8 @@ """Contains an experiment class for running simulations.""" -from flow.core.util import emission_to_csv from flow.utils.registry import make_create_env -import datetime +from datetime import datetime import logging import time -import os import numpy as np @@ -81,7 +79,7 @@ def __init__(self, flow_params, custom_callables=None): self.env = create_env() logging.info(" Starting experiment {} at {}".format( - self.env.network.name, str(datetime.datetime.utcnow()))) + self.env.network.name, str(datetime.utcnow()))) logging.info("Initializing environment.") @@ -170,6 +168,11 @@ def rl_actions(*_): print("Round {0}, return: {1}".format(i, ret)) + # Save emission data at the end of every rollout. This is skipped + # by the internal method if no emission path was specified. + if self.env.simulator == "traci": + self.env.k.simulation.save_emission(run_id=i) + # Print the averages/std for all variables in the info_dict. for key in info_dict.keys(): print("Average, std {}: {}, {}".format( @@ -179,20 +182,4 @@ def rl_actions(*_): print("steps/second:", np.mean(times)) self.env.terminate() - if convert_to_csv and self.env.simulator == "traci": - # wait a short period of time to ensure the xml file is readable - time.sleep(0.1) - - # collect the location of the emission file - dir_path = self.env.sim_params.emission_path - emission_filename = \ - "{0}-emission.xml".format(self.env.network.name) - emission_path = os.path.join(dir_path, emission_filename) - - # convert the emission file into a csv - emission_to_csv(emission_path) - - # Delete the .xml version of the emission file. - os.remove(emission_path) - return info_dict diff --git a/flow/core/kernel/simulation/traci.py b/flow/core/kernel/simulation/traci.py index 8d51b8e25..387f7b03a 100644 --- a/flow/core/kernel/simulation/traci.py +++ b/flow/core/kernel/simulation/traci.py @@ -11,6 +11,7 @@ import logging import subprocess import signal +import csv # Number of retries on restarting SUMO before giving up @@ -21,6 +22,32 @@ class TraCISimulation(KernelSimulation): """Sumo simulation kernel. Extends flow.core.kernel.simulation.KernelSimulation + + Attributes + ---------- + sumo_proc : subprocess.Popen + contains the subprocess.Popen instance used to start traci + sim_step : float + seconds per simulation step + emission_path : str or None + Path to the folder in which to create the emissions output. Emissions + output is not generated if this value is not specified + time : float + used to internally keep track of the simulation time + stored_data : dict >> + a dict object used to store additional data if an emission file is + provided. The first key corresponds to the name of the vehicle, the + second corresponds to the time the sample was issued, and the final + keys represent the additional data stored at every given time for every + vehicle, and consists of the following keys: + + * acceleration (no noise): the accelerations issued to the vehicle, + excluding noise + * acceleration (requested): the requested acceleration by the vehicle, + including noise + * acceleration (actual): the actual acceleration by the vehicle, + collected by computing the difference between the speeds of the + vehicle and dividing it by the sim_step term """ def __init__(self, master_kernel): @@ -33,8 +60,12 @@ def __init__(self, master_kernel): sub-kernels) """ KernelSimulation.__init__(self, master_kernel) - # contains the subprocess.Popen instance used to start traci + self.sumo_proc = None + self.sim_step = None + self.emission_path = None + self.time = 0 + self.stored_data = dict() def pass_api(self, kernel_api): """See parent class. @@ -62,10 +93,61 @@ def simulation_step(self): def update(self, reset): """See parent class.""" - pass + if reset: + self.time = 0 + else: + self.time += self.sim_step + + # Collect the additional data to store in the emission file. + if self.emission_path is not None: + kv = self.master_kernel.vehicle + for veh_id in self.master_kernel.vehicle.get_ids(): + t = round(self.time, 2) + + # some miscellaneous pre-processing + position = kv.get_2d_position(veh_id) + + # Make sure dictionaries corresponding to the vehicle and + # time are available. + if veh_id not in self.stored_data.keys(): + self.stored_data[veh_id] = dict() + if t not in self.stored_data[veh_id].keys(): + self.stored_data[veh_id][t] = dict() + + # Add the speed, position, and lane data. + self.stored_data[veh_id][t].update({ + "speed": kv.get_speed(veh_id), + "lane_number": kv.get_lane(veh_id), + "edge_id": kv.get_edge(veh_id), + "relative_position": kv.get_position(veh_id), + "x": position[0], + "y": position[1], + "headway": kv.get_headway(veh_id), + "leader_id": kv.get_leader(veh_id), + "follower_id": kv.get_follower(veh_id), + "leader_rel_speed": + kv.get_speed(kv.get_leader(veh_id)) + - kv.get_speed(veh_id), + "target_accel_with_noise_with_failsafe": + kv.get_accel(veh_id, noise=True, failsafe=True), + "target_accel_no_noise_no_failsafe": + kv.get_accel(veh_id, noise=False, failsafe=False), + "target_accel_with_noise_no_failsafe": + kv.get_accel(veh_id, noise=True, failsafe=False), + "target_accel_no_noise_with_failsafe": + kv.get_accel(veh_id, noise=False, failsafe=True), + "realized_accel": + kv.get_realized_accel(veh_id), + "road_grade": kv.get_road_grade(veh_id), + "distance": kv.get_distance(veh_id), + }) def close(self): """See parent class.""" + # Save the emission data to a csv. + if self.emission_path is not None: + self.save_emission() + self.kernel_api.close() def check_collision(self): @@ -75,10 +157,24 @@ def check_collision(self): def start_simulation(self, network, sim_params): """Start a sumo simulation instance. - This method uses the configuration files created by the network class - to initialize a sumo instance. Also initializes a traci connection to - interface with sumo from Python. + This method performs the following operations: + + 1. It collect the simulation step size and the emission path + information. If an emission path is specifies, it ensures that the + path exists. + 2. It also uses the configuration files created by the network class to + initialize a sumo instance. + 3. Finally, It initializes a traci connection to interface with sumo + from Python and returns the connection. """ + # Save the simulation step size (for later use). + self.sim_step = sim_params.sim_step + + # Update the emission path term. + self.emission_path = sim_params.emission_path + if self.emission_path is not None: + ensure_dir(self.emission_path) + error = None for _ in range(RETRIES_ON_ERROR): try: @@ -109,17 +205,6 @@ def start_simulation(self, network, sim_params): sumo_call.append("--lateral-resolution") sumo_call.append(str(sim_params.lateral_resolution)) - # add the emission path to the sumo command (if requested) - if sim_params.emission_path is not None: - ensure_dir(sim_params.emission_path) - emission_out = os.path.join( - sim_params.emission_path, - "{0}-emission.xml".format(network.name)) - sumo_call.append("--emission-output") - sumo_call.append(emission_out) - else: - emission_out = None - if sim_params.overtake_right: sumo_call.append("--lanechange.overtake-right") sumo_call.append("true") @@ -146,7 +231,7 @@ def start_simulation(self, network, sim_params): if sim_params.num_clients > 1: logging.info(" Num clients are" + str(sim_params.num_clients)) - logging.debug(" Emission file: " + str(emission_out)) + logging.debug(" Emission file: " + str(self.emission_path)) logging.debug(" Step length: " + str(sim_params.sim_step)) # Opening the I/O thread to SUMO @@ -180,3 +265,66 @@ def teardown_sumo(self): os.killpg(self.sumo_proc.pid, signal.SIGTERM) except Exception as e: print("Error during teardown: {}".format(e)) + + def save_emission(self, run_id=0): + """Save any collected emission data to a csv file. + + If not data was collected, nothing happens. Moreover, any internally + stored data by this class is clear whenever data is stored. + + Parameters + ---------- + run_id : int + the rollout number, appended to the name of the emission file. Used + to store emission files from multiple rollouts run sequentially. + """ + # If there is no stored data, ignore this operation. This is to ensure + # that data isn't deleted if the operation is called twice. + if len(self.stored_data) == 0: + return + + # Get a csv name for the emission file. + name = "{}-{}_emission.csv".format( + self.master_kernel.network.network.name, run_id) + + # The name of all stored data-points (excluding id and time) + stored_ids = [ + "x", + "y", + "speed", + "headway", + "leader_id", + "target_accel_with_noise_with_failsafe", + "target_accel_no_noise_no_failsafe", + "target_accel_with_noise_no_failsafe", + "target_accel_no_noise_with_failsafe", + "realized_accel", + "road_grade", + "edge_id", + "lane_number", + "distance", + "relative_position", + "follower_id", + "leader_rel_speed", + ] + + # Update the stored data to push to the csv file. + final_data = {"time": [], "id": []} + final_data.update({key: [] for key in stored_ids}) + + for veh_id in self.stored_data.keys(): + for t in self.stored_data[veh_id].keys(): + final_data['time'].append(t) + final_data['id'].append(veh_id) + for key in stored_ids: + final_data[key].append(self.stored_data[veh_id][t][key]) + + with open(os.path.join(self.emission_path, name), "w") as f: + print(os.path.join(self.emission_path, name), self.emission_path) + writer = csv.writer(f, delimiter=',') + writer.writerow(final_data.keys()) + writer.writerows(zip(*final_data.values())) + + # Clear all memory from the stored data. This is useful if this + # function is called in between resets. + self.stored_data.clear() diff --git a/flow/core/kernel/vehicle/base.py b/flow/core/kernel/vehicle/base.py index d97ade984..a433b8924 100644 --- a/flow/core/kernel/vehicle/base.py +++ b/flow/core/kernel/vehicle/base.py @@ -128,15 +128,19 @@ def remove(self, veh_id): pass @abstractmethod - def apply_acceleration(self, veh_id, acc): + def apply_acceleration(self, veh_id, acc, smooth=True): """Apply the acceleration requested by a vehicle in the simulator. + In SUMO, this function applies slowDown method which applies smoothing. + Parameters ---------- veh_id : str or list of str list of vehicle identifiers acc : float or array_like requested accelerations from the vehicles + smooth : bool + whether to apply acceleration smoothly or not, default: True """ pass @@ -741,3 +745,32 @@ def get_max_speed(self, veh_id, error): float """ pass + + ########################################################################### + # Methods for Datapipeline # + ########################################################################### + + @abstractmethod + def get_accel(self, veh_id, noise=True, failsafe=True): + """Return the acceleration of vehicle with veh_id.""" + pass + + @abstractmethod + def update_accel(self, veh_id, accel, noise=True, failsafe=True): + """Update stored acceleration of vehicle with veh_id.""" + pass + + @abstractmethod + def get_2d_position(self, veh_id, error=-1001): + """Return (x, y) position of vehicle with veh_id.""" + pass + + @abstractmethod + def get_realized_accel(self, veh_id): + """Return the acceleration that the vehicle actually make.""" + pass + + @abstractmethod + def get_road_grade(self, veh_id): + """Return the road-grade of the vehicle with veh_id.""" + pass diff --git a/flow/core/kernel/vehicle/traci.py b/flow/core/kernel/vehicle/traci.py index 6f119b7bb..39bfb35da 100644 --- a/flow/core/kernel/vehicle/traci.py +++ b/flow/core/kernel/vehicle/traci.py @@ -336,7 +336,8 @@ def _add_departed(self, veh_id, veh_type): tc.VAR_POSITION, tc.VAR_ANGLE, tc.VAR_SPEED_WITHOUT_TRACI, - tc.VAR_FUELCONSUMPTION + tc.VAR_FUELCONSUMPTION, + tc.VAR_DISTANCE ]) self.kernel_api.vehicle.subscribeLeader(veh_id, 2000) @@ -952,18 +953,22 @@ def _prev_edge_followers(self, veh_id, edge_dict, lane, num_edges): return tailway, follower - def apply_acceleration(self, veh_ids, acc): + def apply_acceleration(self, veh_ids, acc, smooth=True): """See parent class.""" - # to hand the case of a single vehicle + # to handle the case of a single vehicle if type(veh_ids) == str: veh_ids = [veh_ids] acc = [acc] for i, vid in enumerate(veh_ids): if acc[i] is not None and vid in self.get_ids(): + self.__vehicles[vid]["accel"] = acc[i] this_vel = self.get_speed(vid) next_vel = max([this_vel + acc[i] * self.sim_step, 0]) - self.kernel_api.vehicle.slowDown(vid, next_vel, 1e-3) + if smooth: + self.kernel_api.vehicle.slowDown(vid, next_vel, 1e-3) + else: + self.kernel_api.vehicle.setSpeed(vid, next_vel) def apply_lane_change(self, veh_ids, direction): """See parent class.""" @@ -993,7 +998,7 @@ def apply_lane_change(self, veh_ids, direction): # perform the requested lane action action in TraCI if target_lane != this_lane: self.kernel_api.vehicle.changeLane( - veh_id, int(target_lane), 100000) + veh_id, int(target_lane), self.sim_step) if veh_id in self.get_rl_ids(): self.prev_last_lc[veh_id] = \ @@ -1013,6 +1018,8 @@ def choose_routes(self, veh_ids, route_choices): def get_x_by_id(self, veh_id): """See parent class.""" + if isinstance(veh_id, (list, np.ndarray)): + return [self.get_x_by_id(vehID) for vehID in veh_id] if self.get_edge(veh_id) == '': # occurs when a vehicle crashes is teleported for some other reason return 0. @@ -1121,3 +1128,52 @@ def get_max_speed(self, veh_id, error=-1001): def set_max_speed(self, veh_id, max_speed): """See parent class.""" self.kernel_api.vehicle.setMaxSpeed(veh_id, max_speed) + + def get_accel(self, veh_id, noise=True, failsafe=True): + """See parent class.""" + metric_name = 'accel' + if noise: + metric_name += '_with_noise' + else: + metric_name += '_no_noise' + if failsafe: + metric_name += '_with_falsafe' + else: + metric_name += '_no_failsafe' + + if metric_name not in self.__vehicles[veh_id]: + self.__vehicles[veh_id][metric_name] = None + return self.__vehicles[veh_id][metric_name] + + def update_accel(self, veh_id, accel, noise=True, failsafe=True): + """See parent class.""" + metric_name = 'accel' + if noise: + metric_name += '_with_noise' + else: + metric_name += '_no_noise' + if failsafe: + metric_name += '_with_falsafe' + else: + metric_name += '_no_failsafe' + + self.__vehicles[veh_id][metric_name] = accel + + def get_realized_accel(self, veh_id): + """See parent class.""" + if self.get_distance(veh_id) == 0: + return 0 + return (self.get_speed(veh_id) - self.get_previous_speed(veh_id)) / self.sim_step + + def get_2d_position(self, veh_id, error=-1001): + """See parent class.""" + return self.__sumo_obs.get(veh_id, {}).get(tc.VAR_POSITION, error) + + def get_distance(self, veh_id, error=-1001): + """See parent class.""" + return self.__sumo_obs.get(veh_id, {}).get(tc.VAR_DISTANCE, error) + + def get_road_grade(self, veh_id): + """See parent class.""" + # TODO : Brent + return 0 diff --git a/flow/visualize/time_space_diagram.py b/flow/visualize/time_space_diagram.py index 3c7ab8b21..9f3da553d 100644 --- a/flow/visualize/time_space_diagram.py +++ b/flow/visualize/time_space_diagram.py @@ -141,7 +141,7 @@ def get_time_space_data(data, params): def _merge(data): - r"""Generate position and speed data for the merge. + r"""Generate time and position data for the merge. This only include vehicles on the main highway, and not on the adjacent on-ramp. @@ -172,9 +172,6 @@ def _merge(data): def _highway(data): r"""Generate time and position data for the highway. - We generate plots for all lanes, so the segments are wrapped in - a dictionary. - Parameters ---------- data : pd.DataFrame @@ -197,7 +194,7 @@ def _highway(data): def _ring_road(data): - r"""Generate position and speed data for the ring road. + r"""Generate time and position data for the ring road. Vehicles that reach the top of the plot simply return to the bottom and continue. @@ -259,7 +256,7 @@ def _i210_subnetwork(data): def _figure_eight(data): - r"""Generate position and speed data for the figure eight. + r"""Generate time and position data for the figure eight. The vehicles traveling towards the intersection from one side will be plotted from the top downward, while the vehicles from the other side will diff --git a/tests/fast_tests/test_controllers.py b/tests/fast_tests/test_controllers.py index 58967cef8..bef765396 100644 --- a/tests/fast_tests/test_controllers.py +++ b/tests/fast_tests/test_controllers.py @@ -405,6 +405,175 @@ def test_no_crash_LinearOVM(self): self.tearDown_failsafe() +class TestFeasibleAccelFailsafe(TestInstantaneousFailsafe): + """ + Tests that the feasible accel failsafe of the base acceleration controller + does not fail under extreme conditions. + """ + + def test_no_crash_OVM(self): + vehicles = VehicleParams() + vehicles.add( + veh_id="test", + acceleration_controller=(OVMController, { + "fail_safe": "feasible_accel" + }), + routing_controller=(ContinuousRouter, {}), + num_vehicles=10, + ) + + self.setUp_failsafe(vehicles=vehicles) + + # run the experiment, see if it fails + self.exp.run(1) + + self.tearDown_failsafe() + + def test_no_crash_LinearOVM(self): + vehicles = VehicleParams() + vehicles.add( + veh_id="test", + acceleration_controller=(LinearOVM, { + "fail_safe": "feasible_accel" + }), + routing_controller=(ContinuousRouter, {}), + num_vehicles=10, + ) + + self.setUp_failsafe(vehicles=vehicles) + + # run the experiment, see if it fails + self.exp.run(1) + + self.tearDown_failsafe() + + +class TestObeySpeedLimitFailsafe(TestInstantaneousFailsafe): + """ + Tests that the obey speed limit failsafe of the base acceleration controller + does not fail under extreme conditions. + """ + + def test_no_crash_OVM(self): + vehicles = VehicleParams() + vehicles.add( + veh_id="test", + acceleration_controller=(OVMController, { + "fail_safe": "obey_speed_limit" + }), + routing_controller=(ContinuousRouter, {}), + num_vehicles=10, + ) + + self.setUp_failsafe(vehicles=vehicles) + + # run the experiment, see if it fails + self.exp.run(1) + + self.tearDown_failsafe() + + def test_no_crash_LinearOVM(self): + vehicles = VehicleParams() + vehicles.add( + veh_id="test", + acceleration_controller=(LinearOVM, { + "fail_safe": "obey_speed_limit" + }), + routing_controller=(ContinuousRouter, {}), + num_vehicles=10, + ) + + self.setUp_failsafe(vehicles=vehicles) + + # run the experiment, see if it fails + self.exp.run(1) + + self.tearDown_failsafe() + + +class TestBrokenFailsafe(TestInstantaneousFailsafe): + """ + Tests that the failsafe logic triggers exceptions when instantiated + incorrectly. + """ + + def test_invalid_failsafe_string(self): + vehicles = VehicleParams() + vehicles.add( + veh_id="test", + acceleration_controller=(OVMController, { + "fail_safe": "default" + }), + routing_controller=(ContinuousRouter, {}), + num_vehicles=10, + ) + + additional_env_params = { + "target_velocity": 8, + "max_accel": 3, + "max_decel": 3, + "sort_vehicles": False + } + env_params = EnvParams(additional_params=additional_env_params) + + additional_net_params = { + "length": 100, + "lanes": 1, + "speed_limit": 30, + "resolution": 40 + } + net_params = NetParams(additional_params=additional_net_params) + + initial_config = InitialConfig(bunching=10) + + # create the environment and network classes, see that it raises ValueError + with self.assertRaises(ValueError): + ring_road_exp_setup(vehicles=vehicles, + env_params=env_params, + net_params=net_params, + initial_config=initial_config) + + self.tearDown_failsafe() + + def test_invalid_failsafe_type(self): + vehicles = VehicleParams() + vehicles.add( + veh_id="test", + acceleration_controller=(LinearOVM, { + "fail_safe": True + }), + routing_controller=(ContinuousRouter, {}), + num_vehicles=10, + ) + + additional_env_params = { + "target_velocity": 8, + "max_accel": 3, + "max_decel": 3, + "sort_vehicles": False + } + env_params = EnvParams(additional_params=additional_env_params) + + additional_net_params = { + "length": 100, + "lanes": 1, + "speed_limit": 30, + "resolution": 40 + } + net_params = NetParams(additional_params=additional_net_params) + + initial_config = InitialConfig(bunching=10) + + # create the environment and network classes, see that it raises ValueError + with self.assertRaises(ValueError): + ring_road_exp_setup(vehicles=vehicles, + env_params=env_params, + net_params=net_params, + initial_config=initial_config) + + self.tearDown_failsafe() + + class TestStaticLaneChanger(unittest.TestCase): """ Makes sure that vehicles with a static lane-changing controller do not diff --git a/tests/fast_tests/test_experiment_base_class.py b/tests/fast_tests/test_experiment_base_class.py index b3863a77c..458af1027 100644 --- a/tests/fast_tests/test_experiment_base_class.py +++ b/tests/fast_tests/test_experiment_base_class.py @@ -1,6 +1,7 @@ import unittest import os import time +import csv from flow.core.experiment import Experiment from flow.core.params import VehicleParams @@ -168,15 +169,44 @@ def test_convert_to_csv(self): time.sleep(1.0) # check that both the csv file exists and the xml file doesn't. - self.assertFalse(os.path.isfile(dir_path + "/{}-emission.xml".format( + self.assertFalse(os.path.isfile(dir_path + "/{}-0_emission.xml".format( exp.env.network.name))) - self.assertTrue(os.path.isfile(dir_path + "/{}-emission.csv".format( + self.assertTrue(os.path.isfile(dir_path + "/{}-0_emission.csv".format( exp.env.network.name))) + # check that the keys within the emission file matches its expected + # values + with open(dir_path + "/{}-0_emission.csv".format( + exp.env.network.name), "r") as f: + reader = csv.reader(f) + header = next(reader) + + self.assertListEqual(header, [ + "time", + "id", + "x", + "y", + "speed", + "headway", + "leader_id", + "target_accel_with_noise_with_failsafe", + "target_accel_no_noise_no_failsafe", + "target_accel_with_noise_no_failsafe", + "target_accel_no_noise_with_failsafe", + "realized_accel", + "road_grade", + "edge_id", + "lane_number", + "distance", + "relative_position", + "follower_id", + "leader_rel_speed", + ]) + time.sleep(0.1) # delete the files - os.remove(os.path.expanduser(dir_path + "/{}-emission.csv".format( + os.remove(os.path.expanduser(dir_path + "/{}-0_emission.csv".format( exp.env.network.name))) From d36da2e5144a40072b11e39f9da6725cc3f4441e Mon Sep 17 00:00:00 2001 From: liljonnystyle Date: Wed, 8 Jul 2020 11:40:32 -0700 Subject: [PATCH 80/86] add 210 edgestarts for backwards compatibility (#985) * fixed h-baselines bug * potential bug fix * add 210 edgestarts for backwards compatibility * add 210 edgestarts for backwards compatibility * add 210 edgestarts for backwards compatibility * add 210 edgestarts for backwards compatibility * fastforward PR 989 * fix typo Co-authored-by: AboudyKreidieh --- flow/visualize/time_space_diagram.py | 66 +++++++++++++++++++++------- 1 file changed, 51 insertions(+), 15 deletions(-) diff --git a/flow/visualize/time_space_diagram.py b/flow/visualize/time_space_diagram.py index 9f3da553d..4914fc6a7 100644 --- a/flow/visualize/time_space_diagram.py +++ b/flow/visualize/time_space_diagram.py @@ -27,7 +27,8 @@ import matplotlib matplotlib.use('TkAgg') from matplotlib import pyplot as plt -from matplotlib.collections import LineCollection +from matplotlib.collections import LineCollection, PatchCollection +from matplotlib.patches import Rectangle import matplotlib.colors as colors import numpy as np import pandas as pd @@ -186,8 +187,6 @@ def _highway(data): pd.DataFrame modified trajectory dataframe """ - data.loc[:, :] = data[(data['distance'] > 500)] - data.loc[:, :] = data[(data['distance'] < 2300)] segs = data[['time_step', 'distance', 'next_time', 'next_pos']].values.reshape((len(data), 2, 2)) return segs, data @@ -240,10 +239,6 @@ def _i210_subnetwork(data): pd.DataFrame modified trajectory dataframe """ - # Omit ghost edges - omit_edges = {'ghost0', '119257908#3'} - data.loc[:, :] = data[~data['edge_id'].isin(omit_edges)] - # Reset lane numbers that are offset by ramp lanes offset_edges = set(data[data['lane_id'] == 5]['edge_id'].unique()) data.loc[data['edge_id'].isin(offset_edges), 'lane_id'] -= 1 @@ -357,6 +352,22 @@ def _get_abs_pos(df, params): } elif params['network'] == HighwayNetwork: return df['x'] + elif params['network'] == I210SubNetwork: + edgestarts = { + '119257914': -5.0999999999995795, + '119257908#0': 56.49000000018306, + ':300944379_0': 56.18000000000016, + ':300944436_0': 753.4599999999871, + '119257908#1-AddedOnRampEdge': 756.3299999991157, + ':119257908#1-AddedOnRampNode_0': 853.530000000022, + '119257908#1': 856.7699999997207, + ':119257908#1-AddedOffRampNode_0': 1096.4499999999707, + '119257908#1-AddedOffRampEdge': 1099.6899999995558, + ':1686591010_1': 1198.1899999999541, + '119257908#2': 1203.6499999994803, + ':1842086610_1': 1780.2599999999056, + '119257908#3': 1784.7899999996537, + } else: edgestarts = defaultdict(float) @@ -374,7 +385,7 @@ def _get_abs_pos(df, params): return ret -def plot_tsd(ax, df, segs, args, lane=None): +def plot_tsd(ax, df, segs, args, lane=None, ghost_edges=None, ghost_bounds=None): """Plot the time-space diagram. Take the pre-processed segments and other meta-data, then plot all the line segments. @@ -391,6 +402,10 @@ def plot_tsd(ax, df, segs, args, lane=None): parsed arguments lane : int, optional lane number to be shown in plot title + ghost_edges : list or set of str + ghost edge names to be greyed out, default None + ghost_bounds : tuple + lower and upper bounds of domain, excluding ghost edges, default None Returns ------- @@ -398,8 +413,7 @@ def plot_tsd(ax, df, segs, args, lane=None): """ norm = plt.Normalize(args.min_speed, args.max_speed) - xmin = max(df['time_step'].min(), args.start) - xmax = min(df['time_step'].max(), args.stop) + xmin, xmax = df['time_step'].min(), df['time_step'].max() xbuffer = (xmax - xmin) * 0.025 # 2.5% of range ymin, ymax = df['distance'].min(), df['distance'].max() ybuffer = (ymax - ymin) * 0.025 # 2.5% of range @@ -413,6 +427,25 @@ def plot_tsd(ax, df, segs, args, lane=None): ax.add_collection(lc) ax.autoscale() + rects = [] + if ghost_edges: + y_domain_min = df[~df['edge_id'].isin(ghost_edges)]['distance'].min() + y_domain_max = df[~df['edge_id'].isin(ghost_edges)]['distance'].max() + rects.append(Rectangle((xmin, y_domain_min), args.start - xmin, y_domain_max - y_domain_min)) + rects.append(Rectangle((xmin, ymin), xmax - xmin, y_domain_min - ymin)) + rects.append(Rectangle((xmin, y_domain_max), xmax - xmin, ymax - y_domain_max)) + elif ghost_bounds: + rects.append(Rectangle((xmin, ghost_bounds[0]), args.start - xmin, ghost_bounds[1] - ghost_bounds[0])) + rects.append(Rectangle((xmin, ymin), xmax - xmin, ghost_bounds[0] - ymin)) + rects.append(Rectangle((xmin, ghost_bounds[1]), xmax - xmin, ymax - ghost_bounds[1])) + else: + rects.append(Rectangle((xmin, ymin), args.start - xmin, ymax - ymin)) + + if rects: + pc = PatchCollection(rects, facecolor='grey', alpha=0.5, edgecolor=None) + pc.set_zorder(20) + ax.add_collection(pc) + if lane: ax.set_title('Time-Space Diagram: Lane {}'.format(lane), fontsize=25) else: @@ -452,8 +485,6 @@ def plot_tsd(ax, df, segs, args, lane=None): help='The minimum speed in the color range.') parser.add_argument('--start', type=float, default=0, help='initial time (in sec) in the plot.') - parser.add_argument('--stop', type=float, default=float('inf'), - help='final time (in sec) in the plot.') args = parser.parse_args() @@ -485,13 +516,17 @@ def plot_tsd(ax, df, segs, args, lane=None): for lane, df in traj_df.groupby('lane_id'): ax = plt.subplot(nlanes, 1, lane+1) - plot_tsd(ax, df, segs[lane], args, lane) + plot_tsd(ax, df, segs[lane], args, int(lane+1), ghost_edges={'ghost0', '119257908#3'}) + plt.tight_layout() else: # perform plotting operation fig = plt.figure(figsize=(16, 9)) ax = plt.axes() - plot_tsd(ax, traj_df, segs, args) + if flow_params['network'] == HighwayNetwork: + plot_tsd(ax, traj_df, segs, args, ghost_bounds=(500, 2300)) + else: + plot_tsd(ax, traj_df, segs, args) ########################################################################### # Note: For MergeNetwork only # @@ -502,4 +537,5 @@ def plot_tsd(ax, df, segs, args, lane=None): [-0.1, -0.1], linewidth=3, color="white") # ########################################################################### - plt.show() + outfile = args.trajectory_path.replace('csv', 'png') + plt.savefig(outfile) From b40e4353f2a4736c3eb1e652e48e6497c43b5d5a Mon Sep 17 00:00:00 2001 From: Aboudy Kreidieh Date: Wed, 8 Jul 2020 12:22:38 -0700 Subject: [PATCH 81/86] Requirements update (#963) * updated requirements.txt and environment.yml * Visualizer tests fixes * remove .func Co-authored-by: akashvelu --- environment.yml | 25 ++++++----- flow/visualize/visualizer_rllib.py | 4 +- requirements.txt | 11 +++-- .../multi_agent/checkpoint_1/checkpoint-1 | Bin 10209 -> 20358 bytes .../checkpoint_1/checkpoint-1.tune_metadata | Bin 180 -> 210 bytes tests/data/rllib_data/multi_agent/params.json | 40 +++++++++++------- tests/data/rllib_data/multi_agent/params.pkl | Bin 17562 -> 21381 bytes .../single_agent/checkpoint_1/checkpoint-1 | Bin 582 -> 26194 bytes .../checkpoint_1/checkpoint-1.tune_metadata | Bin 180 -> 210 bytes .../data/rllib_data/single_agent/params.json | 26 ++++++++---- tests/data/rllib_data/single_agent/params.pkl | Bin 6414 -> 6687 bytes 11 files changed, 64 insertions(+), 42 deletions(-) diff --git a/environment.yml b/environment.yml index f57c8d33d..97d9ad6f8 100644 --- a/environment.yml +++ b/environment.yml @@ -1,18 +1,17 @@ name: flow dependencies: - - python==3.6.8 - - scipy==1.1.0 - - lxml==4.4.1 - - six==1.11.0 - - path.py - - python-dateutil==2.7.3 - - pip>=18.0 - - tensorflow==1.9.0 - - cloudpickle==1.2.1 - - setuptools==41.0.0 - - plotly==2.4.0 + - python==3.7.3 - pip: + - scipy==1.1.0 + - lxml==4.4.1 + - six==1.11.0 + - path.py + - python-dateutil==2.7.3 + - pip>=18.0 + - tensorflow==1.15.2 + - setuptools==41.0.0 + - plotly==2.4.0 - gym==0.14.0 - pyprind==2.11.2 - nose2==0.8.0 @@ -21,9 +20,9 @@ dependencies: - matplotlib==3.0.0 - dill - lz4 - - ray==0.7.3 + - ray==0.8.0 - setproctitle - psutil - opencv-python - - boto3==1.4.8 + - boto3==1.10.45 - redis~=2.10.6 diff --git a/flow/visualize/visualizer_rllib.py b/flow/visualize/visualizer_rllib.py index 8c38a91c1..c1dd83193 100644 --- a/flow/visualize/visualizer_rllib.py +++ b/flow/visualize/visualizer_rllib.py @@ -166,7 +166,7 @@ def visualizer_rllib(args): if multiagent: rets = {} # map the agent id to its policy - policy_map_fn = config['multiagent']['policy_mapping_fn'].func + policy_map_fn = config['multiagent']['policy_mapping_fn'] for key in config['multiagent']['policies'].keys(): rets[key] = [] else: @@ -177,7 +177,7 @@ def visualizer_rllib(args): if multiagent: state_init = {} # map the agent id to its policy - policy_map_fn = config['multiagent']['policy_mapping_fn'].func + policy_map_fn = config['multiagent']['policy_mapping_fn'] size = config['model']['lstm_cell_size'] for key in config['multiagent']['policies'].keys(): state_init[key] = [np.zeros(size, np.float32), diff --git a/requirements.txt b/requirements.txt index ccb971a99..a4f6f83f8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -9,19 +9,24 @@ path.py joblib==0.10.3 python-dateutil==2.7.3 cached_property -cloudpickle==1.2.0 pyglet==1.3.2 matplotlib==3.1.0 imutils==0.5.1 numpydoc -ray==0.7.3 +ray==0.8.0 opencv-python dill lz4 setproctitle psutil opencv-python -boto3==1.4.8 +boto3==1.10.45 redis~=2.10.6 pandas==0.24.2 plotly==2.4.0 +tabulate +tensorflow==1.15.2 +awscli==1.16.309 +torch==1.4.0 +pytz +tensorboardX diff --git a/tests/data/rllib_data/multi_agent/checkpoint_1/checkpoint-1 b/tests/data/rllib_data/multi_agent/checkpoint_1/checkpoint-1 index 0693ed4b62a9cabcdbecb267201ea862144f212c..d346e9dc58b39a5b511ced70927eac1d0d32579b 100644 GIT binary patch literal 20358 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 literal 10209 zcmXYX2{cvT`@RSv$&g4&6b*)=TK$u^&epqmDkiULJcu??8{r^Kr zJ8if5A6zHi})gh%fPln#~qUq3f44xMZ-Vm~e|RDM;c!d!bX&v9d&VL}z1oSf|b zjg|j3_9Ee-O8yxZ1==DaeSW`D&|)fV4|xp33)XX$o^KU)HV5K9!5r?@#EkBW{ zt;51e?%WmwH{qbqAVG&{VbP0e+{U7Q407?|(vFufct}zBZ^L|;ck~Z#KxyuqIqUJG zOdOoH@(}v*`FN>=5e}@&!^Zv7&8(;B3UjPfg>#zag<#SL>Z*~jxcLe=L|>w@Rt+>f z*_*3?I{ zxqEV=xaVhGCz?zNCdWqz*Iv;PuADSZ=rX#T8}~$!D?8tWdn7!Nd*i@+h#U|H#fO_w zs@hy=+odIJ$aChVl#HNURx6IKc!6EAzd=Vo3qN+0lJ{5rxl&OsIHN5M-Sn4n?Yd*g z4~u@dbI?qQouf$Ng2*hN)xi3zy5WWfI;Ik-W?yhyV&=)g~+p_yCgsN!@ zYYGP-;iDq=m6M(Dskt3jdVeV=wf8VDS@{@e@tI=IjlvSn$fi8r*YaZCiql0L^-t!U zKG8Z(=dN;&s(&TVqNJ49o_?5j@M}HqYep$&^~ExtMOZ0MC$E~9J?$Xp^M_o{zOX!w znq~>-idzDY8dY&(FPHMpR+REKIv(ac-B`veytJOfP0TbozcZ7!Kg> z!CED7kU9>dZiiW~ICs3_xD$SSn~jppCR7=>kjT5#;pp*h{8q1uZ~9hY$1@L*^m;^Q ze$rv(zMW&c%Z<@Q`8>`ZT|)gM7vO_Ksjx@S2nIS^h_X=yJ+vW~OzAiZ4lfl*b(Aq| z@=8Tr=MpyS#SgkhgwpdjQb?SNH<)ZN0mnl(snX8CU#G?y6~2?68x@nn~*E9Nd;hv^@e;OvE`A#P1FtsVa!{goU++AEv% z$f!e>!hFzd3}$~T%!Xxk*O)2xv z<8vu=W!RDBrN&5SEu9b=a#rP{sC2bTSC8>&BNa&;?%R;N$@u`jpmz3WB2T5q{`Nr>>a*M zj`Nm6=m%dMihf7VTQ4KVHH`Mthk4 zk69$w>mGetD~@X)QZ!q$6p}Q{@k-DnFy4L?(^a!^8Ak~gzBos!Zi|6L^mA%`!2*l3 zwIKbaDDZAokP?k#dfHP7KW|ATF7YBz=wnXMrklO>Zwl&3CgI+;0$O@s1-?b`!75%7 zE8PEMX3D0J5WgzC>^wqb$NLh_)KYjUohZ0FO$n=y6_P_ex~Qfuju+mS!=g78aIbzl zU8l@MvT|9`@2g@;Q_S z&zcQ}Z6Z+Sq)Yx?%B0gnSFznwe-Mi~9?n?`v zRhy{Xn{KkeGZ3_!*W&Qgc_1Zn4d(@1zz-uUY1>FC`Mmuc-tJ!nB~zEcl%)W=b~=zz z^N374H5I?ioQiq!hIrFu5e9h=(z8#wRe)tY%-itE! z^jtYsJZvkoRWc9VzIM=P+i!IFHF@m4kjg}tPQ(6$QS#X08}nOK1RXuOuycnBof)En zFFdTlA|Qtr{!GFpjmg;EX3Lr$V~KtZLq*G{0vjxb6+OZ1?o&agBU?UV?&l`@`G*{8 zq_i^Z%em~!G6NjJFjAux5c{nxKW3IF$sub4j@6L?-!fjS^Zi$?W_q=7NW|6lVn@z1*)}lki@(A(4&K!!LIxg8zZ?2hwpZh2TP|@)$)82y;qe~ zemP6!t_6^Ou_~N!C=JF%?ZullE6CC`6~-l`3QcvNQa{yx;v!m#WlOwZ{?0sXjo}b$ z;(%#uW$C+JEZR&nWxBR5!gi_Sg8S35$){J_aN{)_YF;&q4BKXzWoY&uryC zd)0|o&nQ1mT_nD7AI1b{k=R%{a&Jx@E*ouS-F$P%cTN=PvD`;H zl(k7tWCL^`SO$_UGO$?lIrIFR5o%?(lXESP$RFcbpsbP%lC`>!-1?bxt<}I^4O-As z^PPHg_)sw36Sh3`#pj)IV0(5hY&Y?Q+8|5p+PfVaKP`dpKev*eNHb={IYTqCpr#Yfz3gSb*~HV!?w#a6xyi77;R&=qXoAAV zGV<#)#nlrUVeLj+^jejU;bt#b_nAnN{QX!5Ra@#+u#ZaXj;&Rzby#ONg_>R8z)W;H z0(NQzn7j8nbK<=*O5_HU?5GYB+oTKqpUyE}m-UH(+*-^?XTkZXD!q2B1bzM)z_4f! zy_N2Pg8Y04&{;&ZziPocjyC=}xCTpR8PGi|CqwMlPUymV4EXjA7gjBXx^-u%Xth6j zZ%IUcjUA4XENszG0?pq>n0&OA8tBM^ET)<92Jca|$qCejxk)svbkTulk0P3mpcQ); zQ%ddd;ss?G*P#iv=9H+}mlJn)W%3}r#q^A0li>Em2zZ{_MMd5|r|jQQ^xT{XVJ3#K zdgTQ0IPO48= z)cHT9ZvzgH#dT?%$`8rZPHZx;oHEub(FgMGH!-_5tpoFO8t}5Rz-&qQRt!J20*fj> zbC|^vwEIa4&W($~)}}jTn~N#7x#qE(d)4uCmk%8Kk^;RYE(Et{!iSh45-jk**^_Ru zp8xJro-G5eJ;P?3zb8XjTmn6IVIp#ze~^v>NxI8H7qmk2apg%3QX=Jre_W=Z{G0}i z+q#q*-ndSEzeHf;ZwqRq-AUG|)G+TXkC4sbktDq4GaZ*P8IKtmQ5)%bAZ+CkY%PTT zoEdOCX%_|(1u{uT0Tg~7!IjDa@}wl1HQKq4xqNpeabH_Ovt{H#WobRFNQ^;u6<@Y* zUL9&nDkDz#Kmx9d;O6VM=+%c7WXH(K)}_aclR1*#$ee9`cv={`18-A|13# z?*ty#CrFIlw9s4>f!I+qyP z%8)>P26m@kVg5dIrLraoRKHPpCUTX3kn|R-!;c{W3F+gIL%pW`C&Nw6FFadjH(#Te}A%f*n?4BBF*3Av+l z;klavgt;hSZ0|LCZ^t_}#kGvdcpymydNLR}s|jWMt*GAV8K9v%7b`_;NR?(VtG=m; zEIt{Iu8~)0rjj3=2$4r~;e6bud=H}NPqV1zjWGCikS^S`37R&Fv;4>@Xr;Ih-3^?O zDG-HEuQ!leKA}+1R13!wPBYfOl(Bc@BqUF`O4`RakchUKkeqCeCTr&~i`gT1iob`h zJ6ejT9Q{~hjkoN|HTAr-h=1hu$%$l9mJe-pK1R+in~dNb0e^mqVEgA0vh>zNG8&o) zDfefA#@CzlbKN&JwVpb97L2fbU&(u;5bEA?IAy>qkI#|ldeYnfiTQkG7 zQ{2ULma382sPr#>qjQ+4-`RbFQ7OhOK>dWNCog`{KV!a9i7t=ayE_ ze{$)i>C(Mog8cO&rj71SW|HzneDzBKrYAI|1rpV#`ALWVaJtv&nf+I#ZT5+C%Jh7L zoFF6e5x=_kCa*-iiJ$a#h1o(w`{n>K%8wBL%(HrH#`~{iaJt75NT(yiJRDHkdYL|K%TyIml0qlN3aIzcsDb zkTpGZ_ybQt!19e=z2SsJOc1y^Z!%Rd@a272=wKT8uGn;`hpFjX{Q&+h%W_ULM%Q`JNeEsh=j?e!D8NF#=#!U$n4%f*Lo4w}pHA)l6>IzR!5& z@Co3_Z$rf!j4;d73c4C&xH~5~awmUZhkHJS3p3`+2yI$!!=(-hs8Vqj-Zg9k_xg9V zYlZ~(rukIv6~pzyRQo{g!ryV!Li;(WW$AL={(5ky<_Iy^lPi2tsK-^?q|L4K+%3e{ zKIp1yEBx~J7;>Y>3Fmg~#bjO$)TUSovn>}0%VtFj8x<|FUBwt@$?xEbpUl9|Z4SaB z%lX1RKmTG=iX`{XZB_2KW!5e+5vJd~3V)Wr0>4^u zu6glJjD0MDTRqIV$_{^-&h@~2+c6GDZGADyOojWs-$VHJus_$MQ$^U_be5*|iNYQ3 zL0aBZ#_QZy$}9G&;B}AYbDU}lIllA|C*tr?jyPM+@sX+Gz5I~L5wEG_B>y_ZvllPs zlzk}Y`N@}YN<6AKX%>0B2ajENnro_f@6<|pG0_z~Wz{N9Xw%!~rt-1%L(6#U50>%P zDJF5$>Z&+@eD-nl=T~xK6-s$CjVd`&$J=N_-U~tNuq9pg-+snM(*j-7Z*f%3WoUF2 zLmXGw!C;Ub%4yd@-n|TgOw}=PU6F>|Gq$YNrTwU{`+?eo>?e|;j#NqeG}FO3$`r>M z;==R=G;qRZ#$QStb06E|z$FDx)LIL6nwI2(n#mVi^FpK`d=j`C;CE0Lm+Z`x1xAvJ<(A9|hJ9p(0ho*L_b9foE`r#G4 zw=j^IwX2qX9Mk+hE?5lq-i~BOff?#_Wuu^aJ9sgZX_re98kgvSs8s>|xu=sV=xM?^ zvvfH2^cxWy+sjiO{78I=A7o7*`#xHCj~;nBM6J#&L&MRd{LrF6h&(WlEPA|;COI6y z^z~J+5fu?3xB4e>YFLBuRsm% z>Pn;Stt~i9*PYxj8`H=72s7c9Jn|rvi5kBeIy1MBkOPNl@Uwj+&1(m-t@=$*bo8)w z?th4n@)G#FkBb9)$05jNv*E7K$;F)WwEV(Tl72-FvPB;=lCNDDJ)d-BWX0jKWhy*Q zSAl`qh7i5Pn610YMRS`qQ52ogp6H>10rMkQxVS&{xaSalwn- z)PI#bsynqIZ&?s&zLP{IZ2d_MpKIU}DRo@E8KaraWN!Om;#0+EIOPk;os1cTDTspUHa}Sp zyF#LokWP*y9>kTo!$j=%ezWhl%m}~uBo$sa20>3S>=RqgPCPgOh08>#-)b9B4thj0 zpUJXQXH%HBVT8W?84nJX8{vxDT8unhOCs-2B_XQ1aO(I}`oU~7$|tyzoH2b?EOQ$y zNONQYgSsH$?MAXSCy~B;f0iv!eM0=a=i-t<9XxH8M`sj2qZ$V1iEXzrr6 z@EZdxVD!K{Z!@))%47BRF>rih1OEJY5N_Q+2t~%vSj(rkDChfq`oJNSO11=(m7_{D z_0w!vIO7mwq}GPyLx*5Ae+t|>8c(G+J*UQdIc$%fB3z`2RA?1U>(^J3xQL06*=CQK z4S6JA_B(%G`!G4;t4MWkzM!Q$jL4#q7|fG%fj`CPsdfbmi|xy>>7X?+h&}+?^M=W} zQA6nF4Zyt^eR!HAiun~DG}h0A%9t-8K3|=oqB5WT_a%c&YST6Q^yG}-y81=pk(?_K z(|u1wy6vf;znKY?I>ZiZ??cz!vrvI+0Rdkd$>2%_nEL294c?N1^PRnE&z{RP*D@cR zCYF;XpGRbQ>UsLIH^eOJ+A0E4b{MtuIPG1`AoH%5F0W94t>MKWFIS5PE9>d5%GIR# ziUAm3wx(@?3t-ZM3~YMcL9B|C+2jX~Oq{3({0UiWX6IQ+5A`&W4yDa>!`>}q>dJWX zeS6(lZe58nqMM0P*E72LS~03VRKb^bZ_}->7trSxxwPi+M5H7W!y@`=ZvSjp)T9B^ zuU%nZoaiRG4{yTMVOgA8_<-)3yB{xfCW23WIb6A{hYO8%jIbk((J68oPCSWQo% z-N6J+la~|aE=d?{K0*?&w+kL^J4DwlSB9F&8}U-@GsdJx3{zKRgZth>rX(T5w7u3D ze7`l(<2TZv|Lr7HoS%SqKi0tUj)`cx;|A0E^%MC$Qbn#jdW^jTj>yh*f}5|$oo`P_IL= zKQil4a+rZ?(NA#@V20b!90QvhHu|oDaRE+n<*}1;Brpq3eB8ziRPNjjz z$MW*13VfHEfvk!s_qby=)*lvynvA)yu3;INzOG?iTZic3sXrj+#Sst&HjxR{18kqo z7e*!X6qGEkg<$Z-JKg(LeV2gY%y}GcAj=o(D0ly9L{ApuMK6ei8*|pHWOT=itQx_3$vtsOCWnfsOEKW7L zBEYxf@kv2C8g1=|Z&OY}OGrLj`1J%{{CdN5Z>9)7d}0avYZSO63Q1(|7*{qHXo7{D z6#n)bhyFq0f{Vjtg1qww=HD#k=r7)ab55$G#cmTQoqhyk6ypWg+NRP@ zjwVEU$iUa0CgwqGAFXh1z(=Ac?1_bU;bwrrm^K*-Tl3_omMX!^(h_KS*_ZBXjDxuj zJ4v{GDt62|2#UVHsdSMMEU1%Ya;uGTO~eK~<|bf5KbEsiI-#bqK50 z2KGk~$$v2e71o`mrV){tE|zcBAC-w07Hy;#^V}d)ejSdRT|&-mQ^8@a9n>kV38pz| zU{kyq)c$)wQw;~{lSL}fV%N#Ud$zIXU-#1a-rf8!OJ&&4zieQ-`W$?xv7dCgx(k~3 zY=n-kC(J5vg=St3qxxPRwmZyX7mw>>VkT&jymDXGbj2gS=ujJxx%rL^Jn+XGzk2B= z5odaGx;^Fv9b%I@>&e|TK12=c!(DfGqViK61|p?k>gF`OA(KdJglf#xd5?(+2jF9s zJN(>x2&PE}vrz|c(&G~=Nr(O>JaTP>k>YA##79rGvO7dRU(yEpqyols`=+tS|)j*bc-~O z4F9I-XGLM@EH!A%&Z4Q2LV8^+5a#p)evD26`^jBIw!j-T?)Ss}i`8Vmv=^*Um4lWL0{5V?f0E}IT$eq>BqSW7>K3P&(`)#& zxMe;`2~WnH_MgPEHk<5xQx3ePd#T=ue?+7C3(f!Aj+67J5Uns@6*WXNr5p76g>$5@{|J45(VoZzY{9SH2^e6~i$bx@@Ig(9!u?1XI2;b0 zYfWi|$9IxaI}1l9%fpj2E&8E;2Xxm|!rCQYnV`#*e*6&+&Nli#tG;QZ4D|B z;DgUcTG*fiYvIKERRFbnLFE24FgSC8lpe_--@`R%X4EvS_*#t#i|-KK;-{>k&s`KzY(HgD(Sz!Had03yZ9HLBqkP+J}g5UGx zvHRV6`p~@>-%DI1&Fyb_9_iv;ik!Titub%qnD#PcBPxMZuGqdh#A$+&G zNW)un@Z>gEIC;MjzHVL(!VAjW$Es=6J@yxK&1;ww!i@PC%Fjd8v=?M$!?-a`{~FaW zlVc(V`Jf&AnAYvcr0YKg(qBdSu%dY<2D(iE{*G^KSr}mR!vip*Zoo`pLn8k3t0&(w zHc}uxozv~FUrB$bj^@05tn+SQPGazANMjK$QB@ANG#X1|Y) z!@MXD)c%x4e{Vg=>UbrBLCsh8@C_AU`C%wObO;1RpRmhv5}USJftj&O9*3&a;L6yZ zv!<%lOnO=t%0IAzSie{}_9`EG43ofhOC<{9lbN1d4-#<|Q6?KRNo zvw~^3918XJtLcMnRqU&h7iK;76}(MP4)AUcRdLkJOL<;trM%ihMLd7$N}hRh2`_PJ zDQ|W`C8yrHg5&?Aio^UZ<$WfVoQ=AroVT47oHuv#dDq7LJ;}zUysxe$JoC;X-o~?a zye$30oR}?zyv>=1c};2sycWqy&c(87o>$Kij)iS0Peb7lFX>-3&(JbSu=|}7uX|d& z>FTgI{F&|(1iJSY^Cv%>CeXfgo_~G83x0jlC(|mLX7=mH9@ExYjNsb^8MD*fPfT%@ zky+V?eZ1yl0#obrgZ!$?C44O};IC+!#BW!*YN{Ifm^VJ^6#r*V9?yJ0&1{d-27cay z`KH#zL;SLte|avM?tDGla?XveE;43U6A7IX9;)oWGeUpNeJ3?`ynRP_(6*qMF{hrG kcj)-Jj_ZfUYM`g=*s1U^zo3x7FyH_4+llW9m59*)f66*xxBvhE diff --git a/tests/data/rllib_data/multi_agent/checkpoint_1/checkpoint-1.tune_metadata b/tests/data/rllib_data/multi_agent/checkpoint_1/checkpoint-1.tune_metadata index 7eef2ef15bba26f49eb7e79079714b5c7015bddd..febe7b205e46a15ce78f3248344fddfc47a3eb3e 100644 GIT binary patch delta 147 zcmdnOc!_a>yh3Vfs%f%?foYO~Ns5V?WolxwrMYoZvN4dKY+`6)IMH4^!=N;Ri-Ccm zBr`WPz9hdSF{hB#-68FY_Claw1Rsz{Eyyg+Pf0C?%CdQjM(_fKixbOIQ{oeg<5Mz| mOA6ULBe;QrMTwR1WvNBQnfZBz91*NQ5d%F7J%d8dQau2Xqbj8U delta 117 zcmcb_xP@_oyh4hlr9q;lk*PtFQId&iN}`2{shOprp-HNxMUqi!qUA(;ZLfq^5nK!m z3?-SlsqrQGC5btOtnLm)-yTdlz`zi}2b4)I$SlrJNiBxTvU!U{@B)R46U$Ol;uDMG NQ!", - "Box(3,)", + "Box(6,)", "Box(1,)", {} ] }, - "policies_to_train": [ - "av" - ], - "policy_mapping_fn": "tune.function(.policy_mapping_fn at 0x7fda132e6c80>)" + "policies_to_train": null, + "policy_mapping_fn": "" }, + "no_done_at_end": false, + "no_eager_on_workers": false, "num_cpus_for_driver": 1, "num_cpus_per_worker": 1, "num_envs_per_worker": 1, "num_gpus": 0, "num_gpus_per_worker": 0, - "num_sgd_iter": 30, + "num_sgd_iter": 10, "num_workers": 2, + "object_store_memory": 0, + "object_store_memory_per_worker": 0, "observation_filter": "NoFilter", "optimizer": {}, "output": null, @@ -110,7 +118,7 @@ "sgd_minibatch_size": 128, "shuffle_buffer_size": 0, "shuffle_sequences": true, - "simple_optimizer": true, + "simple_optimizer": false, "soft_horizon": false, "synchronize_filters": true, "tf_session_args": { @@ -126,7 +134,7 @@ "log_device_placement": false }, "timesteps_per_iteration": 0, - "train_batch_size": 60000, + "train_batch_size": 30000, "use_gae": true, "vf_clip_param": 10.0, "vf_loss_coeff": 1.0, diff --git a/tests/data/rllib_data/multi_agent/params.pkl b/tests/data/rllib_data/multi_agent/params.pkl index cd832aa1c3eb1713e608fef452dbe168746e4cfa..192cf7558830fe2e280e383cf7777e9ee669a7f0 100644 GIT binary patch delta 10700 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 Z!X-tCnR%&2@x>*HC8>q1wS{b@dH^{&vL65d diff --git a/tests/data/rllib_data/single_agent/checkpoint_1/checkpoint-1.tune_metadata b/tests/data/rllib_data/single_agent/checkpoint_1/checkpoint-1.tune_metadata index e83b72aeaf2291e2f177e78504c94fde7e5a3392..55b72be28978f4b959b001b57aad8683ce0f562a 100644 GIT binary patch delta 105 zcmdnOc!_a>yn>;LiIGWil3_}kiMdH~YHCudrJ;dks!3v6iiufrs?kJyZ7GA&2rdQ& zhLX(O)cBJ8lEj=sR(FTQ4ck^VOl;9ph~Ngw6(v^2m!%dJXXfV>azwBKMGW*T^b86) GOZ5Pp1Rp;D delta 75 zcmcb_xP@_oyh2K{ak8bMiII7tiLptlxw)lqD0&z+vtKr&yE|cKHf@4J z!HdOE8YW&u@vH|At``s9yoeX?>QBf)@CP`XL|Ut}u*|;iy*E4G_x4v_U6|WVJuA$P z4c4RH*-kc$0<=xp17N(jsmrH@W%;$RV2vlZ*N7B2);TShLqb$_1g;}a+R(dEin`yb z>Efn(WJT+nc^q*;1LR;qXoy_Gg*>NMtnca0Nlt45)hQ!w8us=&lOaVe4FNd~@u*Bqb2F}-k@2MNokn245Q zt(4>)W6=;SlDkG(FJF|gu_pV*?A4l2w`~ZU+-4;7YzM1j?e*ln5jL%%u?0H~Bpgo} zSzNJva6AYFsu3?@fPi>)5noRET8=p`i8xBEl7E*U(qGm>5J{&K@!+u0H@A<$kSLOYfGGRxmZ*Q-cjsk-_Tn3NY2YhnNb+pQ!E0kLLCGXQ8jj#X|jFCV+sl rRuVRoR__BANX1FD^bnsKZczi+$R%OV{fGTGCH^t}P}59#cjoFnXm$xn~ZNi9pwnbO1J>F4G@Sy*J# z#7POv%-+lszrUUQi_unK2X_xoaei7!d`5mzW>tRPX&nzxnl9-g4lUW6_ zu_Plk9%S`qS(Y3|ZixLr*BO~jp22!rqBuD^yA1I>alb>G@4CPOr%_h%RmYR{7 zoReA%lq@MKoqUo_d$T0_EXK{3IVLbNo9LNN-pD0Bxt1$r@&+!6$*;LqF&mk3O`aei zz4?}a2os~xrMVD;wi(OSDG7N zo?n!mT2ws6o2iEb#7{3M1#%cBYl&x0l-Cm#C@D(J%!^M-EJ@CYFV3t=o#LDDYNC(Q VW;@ZhjGNz!S2Hq7PR^Fx0st4Wrak}w From 7198c375098eb668dc210e841403e3b78367ff7d Mon Sep 17 00:00:00 2001 From: liljonnystyle Date: Thu, 16 Jul 2020 10:20:15 -0700 Subject: [PATCH 82/86] Add kjang96 as codeowner (#1001) --- CODEOWNERS | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CODEOWNERS b/CODEOWNERS index 175783157..1a0ba0d97 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -2,7 +2,7 @@ # Each line is a file pattern followed by one or more owners. # These owners will be the default owners for everything in the repo. -* @cathywu @eugenevinitsky @AboudyKreidieh @kanaadp +* @eugenevinitsky @AboudyKreidieh @kjang96 # Order is important. The last matching pattern has the most precedence. # So if a pull request only touches javascript files, only these owners From 8a935a8e306ea0be2d07f8c2891b489bc93bc83c Mon Sep 17 00:00:00 2001 From: Kathy Jang Date: Fri, 17 Jul 2020 16:27:52 -0700 Subject: [PATCH 83/86] Added doc for libsumo installation on Mac (#1005) * Added doc for libsumo installation on Mac * Added code shell --- docs/libsumo_mac.md | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 docs/libsumo_mac.md diff --git a/docs/libsumo_mac.md b/docs/libsumo_mac.md new file mode 100644 index 000000000..7a02d3823 --- /dev/null +++ b/docs/libsumo_mac.md @@ -0,0 +1,23 @@ +# How to install Libsumo for Mac OS + +This is adapted from an email exchange with the SUMO staff. + + + +To install libsumo requires re-building and installing SUMO from source. + +## Steps + +- **Install swig:** ```brew install swig``` +- **Clone the repo:** ```git clone https://github.com/eclipse/sumo.git``` +- **Create a “cmake-build” directory inside sumo/build/ and navigate to it:** ```mkdir build/cmake-build && cd build/cmake-build``` + +**The next 3 steps are inside that directory** + +- ```cmake ../..``` +- ```make``` +- ```make install``` + +## Additional Notes +- You can test if libsumo has been built looking at (./testlibsumo) inside the sumo/bin/ directory. +- Bear in mind to use libsumo with the same Python version with which CMake built SUMO. \ No newline at end of file From 69add1095b7bdcaf6731920461a48273ce49448c Mon Sep 17 00:00:00 2001 From: Ashkan Date: Mon, 31 Aug 2020 10:56:57 -0700 Subject: [PATCH 84/86] Update question Github issue tempate Update question Github issue template to refer people to Slack channel, and not StackOverflow --- .github/ISSUE_TEMPLATE/question.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md index bbc2ee05e..1cb6b448d 100644 --- a/.github/ISSUE_TEMPLATE/question.md +++ b/.github/ISSUE_TEMPLATE/question.md @@ -8,7 +8,10 @@ about: If you have a question or run into some problem with Flow (e.g. during th PLEASE DO NOT ASK YOUR QUESTIONS HERE ON GITHUB! If you have a question or run into some problem (e.g. during installation or when running an example) -with Flow, please direct your technical questions to Stack Overflow using the "flow-project" tag. +with Flow, please direct technical questions to the Flow project Slack channel here: -link: https://stackoverflow.com/questions/tagged/flow-project -tag: flow-project +https://join.slack.com/t/flow-users/shared_invite/enQtODQ0NDYxMTQyNDY2LTY1ZDVjZTljM2U0ODIxNTY5NTQ2MmUxMzYzNzc5NzU4ZTlmNGI2ZjFmNGU4YjVhNzE3NjcwZTBjNzIxYTg5ZmY + +If you have a non-technical inquiry, please send us an email: + +https://flow-project.github.io/contact.html From 183b0c5138396d3f46124ea0d357df6a69d1c2d6 Mon Sep 17 00:00:00 2001 From: roireshef Date: Mon, 23 Nov 2020 21:24:39 +0200 Subject: [PATCH 85/86] removed os.setsid since it's blocking when running from within Ray process (#1024) --- flow/core/kernel/simulation/traci.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/flow/core/kernel/simulation/traci.py b/flow/core/kernel/simulation/traci.py index 387f7b03a..20643ee5b 100644 --- a/flow/core/kernel/simulation/traci.py +++ b/flow/core/kernel/simulation/traci.py @@ -237,8 +237,7 @@ def start_simulation(self, network, sim_params): # Opening the I/O thread to SUMO self.sumo_proc = subprocess.Popen( sumo_call, - stdout=subprocess.DEVNULL, - preexec_fn=os.setsid + stdout=subprocess.DEVNULL ) # wait a small period of time for the subprocess to activate From a511c41c48e6b928bb2060de8ad1ef3c3e3d9554 Mon Sep 17 00:00:00 2001 From: roireshef Date: Fri, 11 Dec 2020 21:26:14 +0200 Subject: [PATCH 86/86] Better handling temporary files for running at scale (#1026) * moved temporary files to default (os dependent) temp dir, added removal of temp files at termination when using .net.xml file path * flake8 fix --- flow/core/kernel/network/traci.py | 48 ++++++++++++++----------------- 1 file changed, 22 insertions(+), 26 deletions(-) diff --git a/flow/core/kernel/network/traci.py b/flow/core/kernel/network/traci.py index c9ac80772..df57ea1a9 100644 --- a/flow/core/kernel/network/traci.py +++ b/flow/core/kernel/network/traci.py @@ -1,4 +1,5 @@ """Script containing the TraCI network kernel class.""" +import tempfile from flow.core.kernel.network import BaseKernelNetwork from flow.core.util import makexml, printxml, ensure_dir @@ -54,10 +55,8 @@ def __init__(self, master_kernel, sim_params): # directories for the network-specific files that will be generated by # the `generate_network` method - self.net_path = os.path.dirname(os.path.abspath(__file__)) \ - + '/debug/net/' - self.cfg_path = os.path.dirname(os.path.abspath(__file__)) \ - + '/debug/cfg/' + self.net_path = os.path.join(tempfile.gettempdir(), 'flow/debug/net/') + self.cfg_path = os.path.join(tempfile.gettempdir(), 'flow/debug/cfg/') ensure_dir('%s' % self.net_path) ensure_dir('%s' % self.cfg_path) @@ -221,31 +220,28 @@ def close(self): is to prevent them from building up in the debug folder. Note that in the case of import .net.xml files we do not want to delete them. """ + # Those files are being created even if self.network.net_params.template is a path to .net.xml file + files = [self.cfg_path + self.guifn, + self.cfg_path + self.addfn, + self.cfg_path + self.roufn, + self.cfg_path + self.sumfn] + if self.network.net_params.template is None: + files += [self.net_path + self.nodfn, + self.net_path + self.edgfn, + self.net_path + self.cfgfn, + self.net_path + self.confn, + self.net_path + self.typfn, + self.cfg_path + self.netfn] + + for file in files: try: - os.remove(self.net_path + self.nodfn) - os.remove(self.net_path + self.edgfn) - os.remove(self.net_path + self.cfgfn) - os.remove(self.cfg_path + self.addfn) - os.remove(self.cfg_path + self.guifn) - os.remove(self.cfg_path + self.netfn) - os.remove(self.cfg_path + self.roufn) - os.remove(self.cfg_path + self.sumfn) - except FileNotFoundError: + os.remove(file) + except (FileNotFoundError, OSError): # the files were never created - pass - - # the connection file is not always created - try: - os.remove(self.net_path + self.confn) - except OSError: - pass - - # neither is the type file - try: - os.remove(self.net_path + self.typfn) - except OSError: - pass + # the connection file is not always created + # neither is the type file + continue def get_edge(self, x): """See parent class."""